Merge branch 'master' of https://github.com/highfidelity/hifi into toggleOverlay

Conflicts:
	interface/src/Application.cpp
This commit is contained in:
Brad Hefta-Gaub 2016-02-25 16:21:38 -08:00
commit b0c286bc3b
78 changed files with 2213 additions and 1297 deletions

View file

@ -835,41 +835,40 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) {
if (audioEnvGroupObject[AUDIO_ZONES].isObject()) {
const QJsonObject& zones = audioEnvGroupObject[AUDIO_ZONES].toObject();
const QString X_RANGE = "x_range";
const QString Y_RANGE = "y_range";
const QString Z_RANGE = "z_range";
const QString X_MIN = "x_min";
const QString X_MAX = "x_max";
const QString Y_MIN = "y_min";
const QString Y_MAX = "y_max";
const QString Z_MIN = "z_min";
const QString Z_MAX = "z_max";
foreach (const QString& zone, zones.keys()) {
QJsonObject zoneObject = zones[zone].toObject();
if (zoneObject.contains(X_RANGE) && zoneObject.contains(Y_RANGE) && zoneObject.contains(Z_RANGE)) {
QStringList xRange = zoneObject.value(X_RANGE).toString().split("-", QString::SkipEmptyParts);
QStringList yRange = zoneObject.value(Y_RANGE).toString().split("-", QString::SkipEmptyParts);
QStringList zRange = zoneObject.value(Z_RANGE).toString().split("-", QString::SkipEmptyParts);
if (zoneObject.contains(X_MIN) && zoneObject.contains(X_MAX) && zoneObject.contains(Y_MIN) &&
zoneObject.contains(Y_MAX) && zoneObject.contains(Z_MIN) && zoneObject.contains(Z_MAX)) {
if (xRange.size() == 2 && yRange.size() == 2 && zRange.size() == 2) {
float xMin, xMax, yMin, yMax, zMin, zMax;
bool ok, allOk = true;
xMin = xRange[0].toFloat(&ok);
allOk &= ok;
xMax = xRange[1].toFloat(&ok);
allOk &= ok;
yMin = yRange[0].toFloat(&ok);
allOk &= ok;
yMax = yRange[1].toFloat(&ok);
allOk &= ok;
zMin = zRange[0].toFloat(&ok);
allOk &= ok;
zMax = zRange[1].toFloat(&ok);
allOk &= ok;
float xMin, xMax, yMin, yMax, zMin, zMax;
bool ok, allOk = true;
xMin = zoneObject.value(X_MIN).toString().toFloat(&ok);
allOk &= ok;
xMax = zoneObject.value(X_MAX).toString().toFloat(&ok);
allOk &= ok;
yMin = zoneObject.value(Y_MIN).toString().toFloat(&ok);
allOk &= ok;
yMax = zoneObject.value(Y_MAX).toString().toFloat(&ok);
allOk &= ok;
zMin = zoneObject.value(Z_MIN).toString().toFloat(&ok);
allOk &= ok;
zMax = zoneObject.value(Z_MAX).toString().toFloat(&ok);
allOk &= ok;
if (allOk) {
glm::vec3 corner(xMin, yMin, zMin);
glm::vec3 dimensions(xMax - xMin, yMax - yMin, zMax - zMin);
AABox zoneAABox(corner, dimensions);
_audioZones.insert(zone, zoneAABox);
qDebug() << "Added zone:" << zone << "(corner:" << corner
<< ", dimensions:" << dimensions << ")";
}
if (allOk) {
glm::vec3 corner(xMin, yMin, zMin);
glm::vec3 dimensions(xMax - xMin, yMax - yMin, zMax - zMin);
AABox zoneAABox(corner, dimensions);
_audioZones.insert(zone, zoneAABox);
qDebug() << "Added zone:" << zone << "(corner:" << corner
<< ", dimensions:" << dimensions << ")";
}
}
}

View file

@ -236,10 +236,6 @@ OctreeServer::OctreeServer(ReceivedMessage& message) :
{
_averageLoopTime.updateAverage(0);
qDebug() << "Octree server starting... [" << this << "]";
// make sure the AccountManager has an Auth URL for payment redemptions
AccountManager::getInstance().setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL);
}
OctreeServer::~OctreeServer() {

View file

@ -271,8 +271,9 @@ FunctionEnd
@CPACK_NSIS_PAGE_COMPONENTS@
Page custom PostInstallOptionsPage ReadPostInstallOptions
!insertmacro MUI_PAGE_INSTFILES
Page custom PostInstallOptionsPage HandlePostInstallOptions
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
@ -341,6 +342,227 @@ FunctionEnd
;Only for solid compression (by default, solid compression is enabled for BZIP2 and LZMA)
ReserveFile "@POST_INSTALL_OPTIONS_PATH@"
; Make sure nsDialogs is included before we use it
!include "nsdialogs.nsh"
;--------------------------------
; Post Install Options
Var PostInstallDialog
Var DesktopClientCheckbox
Var DesktopServerCheckbox
Var ServerStartupCheckbox
Var LaunchNowCheckbox
Var CurrentOffset
Var OffsetUnits
Var CopyFromProductionCheckbox
!macro SetPostInstallOption Checkbox OptionName Default
; reads the value for the given post install option to the registry
ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}"
${If} $0 == "NO"
; the value in the registry says it should not be checked
${NSD_SetState} ${Checkbox} ${BST_UNCHECKED}
${ElseIf} $0 == "YES"
; the value in the registry says it should be checked
${NSD_SetState} ${Checkbox} ${BST_CHECKED}
${Else}
; the value in the registry was not in the expected format, use default
${NSD_SetState} ${Checkbox} ${Default}
${EndIf}
!macroend
Function PostInstallOptionsPage
!insertmacro MUI_HEADER_TEXT "Setup Options" ""
nsDialogs::Create 1018
Pop $PostInstallDialog
${If} $PostInstallDialog == error
Abort
${EndIf}
StrCpy $CurrentOffset 0
StrCpy $OffsetUnits u
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_SHORTCUT_NAME@"
Pop $DesktopClientCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for High Fidelity @CONSOLE_SHORTCUT_NAME@"
Pop $DesktopServerCheckbox
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED}
IntOp $CurrentOffset $CurrentOffset + 15
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity @CONSOLE_SHORTCUT_NAME@ on startup"
Pop $ServerStartupCheckbox
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED}
IntOp $CurrentOffset $CurrentOffset + 15
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Server Console after install"
${Else}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity after install"
${EndIf}
Pop $LaunchNowCheckbox
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
${If} @PR_BUILD@ == 1
; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${NSD_SetState} $DesktopServerCheckbox ${BST_UNCHECKED}
${NSD_SetState} $ServerStartupCheckbox ${BST_UNCHECKED}
${EndIf}
; push the offset
IntOp $CurrentOffset $CurrentOffset + 15
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install"
Pop $CopyFromProductionCheckbox
${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED}
${EndIf}
nsDialogs::Show
FunctionEnd
!macro WritePostInstallOption OptionName Option
; writes the value for the given post install option to the registry
WriteRegStr HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" ${Option}
!macroend
Var DesktopClientState
Var DesktopServerState
Var ServerStartupState
Var LaunchNowState
Var CopyFromProductionState
Function ReadPostInstallOptions
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
; check if the user asked for a desktop shortcut to High Fidelity
${NSD_GetState} $DesktopClientCheckbox $DesktopClientState
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
; check if the user asked for a desktop shortcut to Server Console
${NSD_GetState} $DesktopServerCheckbox $DesktopServerState
; check if the user asked to have Server Console launched every startup
${NSD_GetState} $ServerStartupCheckbox $ServerStartupState
${EndIf}
${If} @PR_BUILD@ == 1
; check if we need to copy settings/content from production for this PR build
${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState
${EndIf}
; check if we need to launch an application post-install
${NSD_GetState} $LaunchNowCheckbox $LaunchNowState
FunctionEnd
Function HandlePostInstallOptions
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
; check if the user asked for a desktop shortcut to High Fidelity
${If} $DesktopClientState == ${BST_CHECKED}
CreateShortCut "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"
!insertmacro WritePostInstallOption "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@" YES
${Else}
!insertmacro WritePostInstallOption @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ NO
${EndIf}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
; check if the user asked for a desktop shortcut to Server Console
${If} $DesktopServerState == ${BST_CHECKED}
CreateShortCut "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
!insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ YES
${Else}
!insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO
${EndIf}
; check if the user asked to have Server Console launched every startup
${If} $ServerStartupState == ${BST_CHECKED}
; in case we added a shortcut in the global context, pull that now
SetShellVarContext all
Delete "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk"
; make a startup shortcut in this user's current context
SetShellVarContext current
CreateShortCut "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
; reset the shell var context back
SetShellVarContext all
!insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ YES
${Else}
!insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO
${EndIf}
${EndIf}
${If} @PR_BUILD@ == 1
; check if we need to copy settings/content from production for this PR build
${If} $CopyFromProductionState == ${BST_CHECKED}
SetShellVarContext current
StrCpy $0 "$APPDATA\@BUILD_ORGANIZATION@"
; we need to copy whatever is in the data folder for production build to the data folder for this build
CreateDirectory $0
ClearErrors
; copy the data from production build to this PR build
CopyFiles "$APPDATA\High Fidelity\*" $0
; handle an error in copying files
IfErrors 0 NoError
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground \
"There was a problem copying your production content and settings to $0 for this PR build.$\r$\n$\r$\nPlease copy them manually."
NoError:
SetShellVarContext all
${EndIf}
${EndIf}
${If} $LaunchNowState == ${BST_CHECKED}
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES
; both launches use the explorer trick in case the user has elevated permissions for the installer
; it won't be possible to use this approach if either application should be launched with a command line param
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"'
${Else}
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"'
${EndIf}
${Else}
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO
${EndIf}
FunctionEnd
;--------------------------------
;Installer Sections
@ -469,233 +691,11 @@ Section "-Core installation"
@CPACK_NSIS_EXTRA_INSTALL_COMMANDS@
; Handle whichever post install options were set
Call HandlePostInstallOptions
SectionEnd
; Make sure nsDialogs is included before we use it
!include "nsdialogs.nsh"
Var PostInstallDialog
Var OptionsLabel
Var DesktopClientCheckbox
Var DesktopServerCheckbox
Var ServerStartupCheckbox
Var LaunchNowCheckbox
Var CurrentOffset
Var OffsetUnits
Var CopyFromProductionCheckbox
!macro SetPostInstallOption Checkbox OptionName Default
; reads the value for the given post install option to the registry
ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}"
${If} $0 == "NO"
; the value in the registry says it should not be checked
${NSD_SetState} ${Checkbox} ${BST_UNCHECKED}
${ElseIf} $0 == "YES"
; the value in the registry says it should be checked
${NSD_SetState} ${Checkbox} ${BST_CHECKED}
${Else}
; the value in the registry was not in the expected format, use default
${NSD_SetState} ${Checkbox} ${Default}
${EndIf}
!macroend
Function PostInstallOptionsPage
; Set the text on the dialog button to match finish, hide the back and cancel buttons
GetDlgItem $R1 $hwndparent 1
SendMessage $R1 ${WM_SETTEXT} 0 "STR:&Finish"
GetDlgItem $R3 $hwndparent 3
ShowWindow $R3 0
nsDialogs::Create 1018
Pop $PostInstallDialog
${If} $PostInstallDialog == error
Abort
${EndIf}
${NSD_CreateLabel} 0 0 100% 12u "Setup Options"
Pop $OptionsLabel
; Set label to bold
CreateFont $R2 "Arial" 10 700
SendMessage $OptionsLabel ${WM_SETFONT} $R2 0
; Force label redraw
ShowWindow $OptionsLabel ${SW_HIDE}
ShowWindow $OptionsLabel ${SW_SHOW}
StrCpy $CurrentOffset 15
StrCpy $OffsetUnits u
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_SHORTCUT_NAME@"
Pop $DesktopClientCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for High Fidelity @CONSOLE_SHORTCUT_NAME@"
Pop $DesktopServerCheckbox
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED}
IntOp $CurrentOffset $CurrentOffset + 15
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity @CONSOLE_SHORTCUT_NAME@ on startup"
Pop $ServerStartupCheckbox
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED}
IntOp $CurrentOffset $CurrentOffset + 15
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Server Console Now"
${Else}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Now"
${EndIf}
Pop $LaunchNowCheckbox
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
${If} @PR_BUILD@ == 1
; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${NSD_SetState} $DesktopServerCheckbox ${BST_UNCHECKED}
${NSD_SetState} $ServerStartupCheckbox ${BST_UNCHECKED}
${EndIf}
; push the offset
IntOp $CurrentOffset $CurrentOffset + 15
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install"
Pop $CopyFromProductionCheckbox
${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED}
${EndIf}
nsDialogs::Show
FunctionEnd
!macro WritePostInstallOption OptionName Option
; writes the value for the given post install option to the registry
WriteRegStr HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" ${Option}
!macroend
Var DesktopClientState
Var DesktopServerState
Var ServerStartupState
Var LaunchNowState
Var CopyFromProductionState
Function HandlePostInstallOptions
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
; check if the user asked for a desktop shortcut to High Fidelity
${NSD_GetState} $DesktopClientCheckbox $DesktopClientState
${If} $DesktopClientState == ${BST_CHECKED}
CreateShortCut "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"
!insertmacro WritePostInstallOption "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@" YES
${Else}
!insertmacro WritePostInstallOption @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ NO
${EndIf}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
; check if the user asked for a desktop shortcut to Server Console
${NSD_GetState} $DesktopServerCheckbox $DesktopServerState
${If} $DesktopServerState == ${BST_CHECKED}
CreateShortCut "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
!insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ YES
${Else}
!insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO
${EndIf}
; check if the user asked to have Server Console launched every startup
${NSD_GetState} $ServerStartupCheckbox $ServerStartupState
${If} $ServerStartupState == ${BST_CHECKED}
; in case we added a shortcut in the global context, pull that now
SetShellVarContext all
Delete "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk"
; make a startup shortcut in this user's current context
SetShellVarContext current
CreateShortCut "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
; reset the shell var context back
SetShellVarContext all
!insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ YES
${Else}
!insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO
${EndIf}
${EndIf}
${If} @PR_BUILD@ == 1
; check if we need to copy settings/content from production for this PR build
${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState
${If} $CopyFromProductionState == ${BST_CHECKED}
SetShellVarContext current
StrCpy $0 "$APPDATA\@BUILD_ORGANIZATION@"
; we need to copy whatever is in the data folder for production build to the data folder for this build
CreateDirectory $0
ClearErrors
; copy the data from production build to this PR build
CopyFiles "$APPDATA\High Fidelity\*" $0
; handle an error in copying files
IfErrors 0 NoError
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground \
"There was a problem copying your production content and settings to $0 for this PR build.$\r$\n$\r$\nPlease copy them manually."
NoError:
SetShellVarContext all
${EndIf}
${EndIf}
; check if we need to launch an application post-install
${NSD_GetState} $LaunchNowCheckbox $LaunchNowState
${If} $LaunchNowState == ${BST_CHECKED}
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES
; both launches use the explorer trick in case the user has elevated permissions for the installer
; it won't be possible to use this approach if either application should be launched with a command line param
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"'
${Else}
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"'
${EndIf}
${Else}
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO
${EndIf}
FunctionEnd
!include nsProcess.nsh
!macro PromptForRunningApplication applicationName displayName action prompter

View file

@ -230,22 +230,40 @@
},
"columns": [
{
"name": "x_range",
"label": "X range",
"name": "x_min",
"label": "X start",
"can_set": true,
"placeholder": "0-16384"
"placeholder": "-16384.0"
},
{
"name": "y_range",
"label": "Y range",
"name": "x_max",
"label": "X end",
"can_set": true,
"placeholder": "0-16384"
"placeholder": "16384.0"
},
{
"name": "y_min",
"label": "Y start",
"can_set": true,
"placeholder": "-16384.0"
},
{
"name": "z_range",
"label": "Z range",
"name": "y_max",
"label": "Y end",
"can_set": true,
"placeholder": "0-16384"
"placeholder": "16384.0"
},
{
"name": "z_min",
"label": "Z start",
"can_set": true,
"placeholder": "-16384.0"
},
{
"name": "z_max",
"label": "Z end",
"can_set": true,
"placeholder": "16384.0"
}
]
},

View file

@ -778,7 +778,7 @@ function chooseFromHighFidelityDomains(clickedButton) {
function createTemporaryDomain() {
swal({
title: 'Create temporary place name',
text: "This will create a temporary place name and domain ID (valid for 30 days)"
text: "This will create a temporary place name and domain ID"
+ " so other users can easily connect to your domain.</br></br>"
+ "In order to make your domain reachable, this will also enable full automatic networking.",
showCancelButton: true,

View file

@ -331,7 +331,6 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
QCryptographicHash::Sha256);
if (rsaPublicKey) {
QByteArray decryptedArray(RSA_size(rsaPublicKey), 0);
int decryptResult = RSA_verify(NID_sha256,
reinterpret_cast<const unsigned char*>(usernameWithToken.constData()),
usernameWithToken.size(),

View file

@ -96,7 +96,7 @@ DomainServer::DomainServer(int argc, char* argv[]) :
// make sure we hear about newly connected nodes from our gatekeeper
connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode);
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth() && optionallySetupAssignmentPayment()) {
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) {
// we either read a certificate and private key or were not passed one
// and completed login or did not need to
@ -198,7 +198,6 @@ bool DomainServer::optionallySetupOAuth() {
}
AccountManager& accountManager = AccountManager::getInstance();
accountManager.disableSettingsFilePersistence();
accountManager.setAuthURL(_oauthProviderURL);
_oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString();
@ -372,20 +371,12 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) {
packetReceiver.registerListener(PacketType::ICEPing, &_gatekeeper, "processICEPingPacket");
packetReceiver.registerListener(PacketType::ICEPingReply, &_gatekeeper, "processICEPingReplyPacket");
packetReceiver.registerListener(PacketType::ICEServerPeerInformation, &_gatekeeper, "processICEPeerInformationPacket");
packetReceiver.registerListener(PacketType::ICEServerHeartbeatDenied, this, "processICEServerHeartbeatDenialPacket");
// add whatever static assignments that have been parsed to the queue
addStaticAssignmentsToQueue();
}
bool DomainServer::didSetupAccountManagerWithAccessToken() {
if (AccountManager::getInstance().hasValidAccessToken()) {
// we already gave the account manager a valid access token
return true;
}
return resetAccountManagerAccessToken();
}
const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token";
bool DomainServer::resetAccountManagerAccessToken() {
@ -401,9 +392,13 @@ bool DomainServer::resetAccountManagerAccessToken() {
if (accessTokenVariant && accessTokenVariant->canConvert(QMetaType::QString)) {
accessToken = accessTokenVariant->toString();
} else {
qDebug() << "A domain-server feature that requires authentication is enabled but no access token is present."
<< "Set an access token via the web interface, in your user or master config"
qDebug() << "A domain-server feature that requires authentication is enabled but no access token is present.";
qDebug() << "Set an access token via the web interface, in your user or master config"
<< "at keypath metaverse.access_token or in your ENV at key DOMAIN_SERVER_ACCESS_TOKEN";
// clear any existing access token from AccountManager
AccountManager::getInstance().setAccessTokenForCurrentAuthURL(QString());
return false;
}
} else {
@ -429,34 +424,6 @@ bool DomainServer::resetAccountManagerAccessToken() {
}
}
bool DomainServer::optionallySetupAssignmentPayment() {
const QString PAY_FOR_ASSIGNMENTS_OPTION = "pay-for-assignments";
const QVariantMap& settingsMap = _settingsManager.getSettingsMap();
if (settingsMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) &&
settingsMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool() &&
didSetupAccountManagerWithAccessToken()) {
qDebug() << "Assignments will be paid for via" << qPrintable(_oauthProviderURL.toString());
// assume that the fact we are authing against HF data server means we will pay for assignments
// setup a timer to send transactions to pay assigned nodes every 30 seconds
QTimer* creditSetupTimer = new QTimer(this);
connect(creditSetupTimer, &QTimer::timeout, this, &DomainServer::setupPendingAssignmentCredits);
const qint64 CREDIT_CHECK_INTERVAL_MSECS = 5 * 1000;
creditSetupTimer->start(CREDIT_CHECK_INTERVAL_MSECS);
QTimer* nodePaymentTimer = new QTimer(this);
connect(nodePaymentTimer, &QTimer::timeout, this, &DomainServer::sendPendingTransactionsToServer);
const qint64 TRANSACTION_SEND_INTERVAL_MSECS = 30 * 1000;
nodePaymentTimer->start(TRANSACTION_SEND_INTERVAL_MSECS);
}
return true;
}
void DomainServer::setupAutomaticNetworking() {
auto nodeList = DependencyManager::get<LimitedNodeList>();
@ -467,9 +434,9 @@ void DomainServer::setupAutomaticNetworking() {
setupICEHeartbeatForFullNetworking();
}
if (!didSetupAccountManagerWithAccessToken()) {
qDebug() << "Cannot send heartbeat to data server without an access token.";
qDebug() << "Add an access token to your config file or via the web interface.";
if (!resetAccountManagerAccessToken()) {
qDebug() << "Will not send heartbeat to Metaverse API without an access token.";
qDebug() << "If this is not a temporary domain add an access token to your config file or via the web interface.";
return;
}
@ -526,6 +493,19 @@ void DomainServer::setupICEHeartbeatForFullNetworking() {
// we need this DS to know what our public IP is - start trying to figure that out now
limitedNodeList->startSTUNPublicSocketUpdate();
// to send ICE heartbeats we'd better have a private key locally with an uploaded public key
auto& accountManager = AccountManager::getInstance();
auto domainID = accountManager.getAccountInfo().getDomainID();
// if we have an access token and we don't have a private key or the current domain ID has changed
// we should generate a new keypair
if (!accountManager.getAccountInfo().hasPrivateKey() || domainID != limitedNodeList->getSessionUUID()) {
accountManager.generateNewDomainKeypair(limitedNodeList->getSessionUUID());
}
// hookup to the signal from account manager that tells us when keypair is available
connect(&accountManager, &AccountManager::newKeypair, this, &DomainServer::handleKeypairChange);
if (!_iceHeartbeatTimer) {
// setup a timer to heartbeat with the ice-server every so often
_iceHeartbeatTimer = new QTimer { this };
@ -1082,11 +1062,76 @@ void DomainServer::sendHeartbeatToDataServer(const QString& networkAddress) {
domainUpdateJSON.toUtf8());
}
// TODO: have data-web respond with ice-server hostname to use
void DomainServer::sendHeartbeatToIceServer() {
if (!_iceServerSocket.getAddress().isNull()) {
DependencyManager::get<LimitedNodeList>()->sendHeartbeatToIceServer(_iceServerSocket);
auto& accountManager = AccountManager::getInstance();
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
if (!accountManager.getAccountInfo().hasPrivateKey()) {
qWarning() << "Cannot send an ice-server heartbeat without a private key for signature.";
qWarning() << "Waiting for keypair generation to complete before sending ICE heartbeat.";
if (!limitedNodeList->getSessionUUID().isNull()) {
accountManager.generateNewDomainKeypair(limitedNodeList->getSessionUUID());
} else {
qWarning() << "Attempting to send ICE server heartbeat with no domain ID. This is not supported";
}
return;
}
// NOTE: I'd love to specify the correct size for the packet here, but it's a little trickey with
// QDataStream and the possibility of IPv6 address for the sockets.
if (!_iceServerHeartbeatPacket) {
_iceServerHeartbeatPacket = NLPacket::create(PacketType::ICEServerHeartbeat);
}
bool shouldRecreatePacket = false;
if (_iceServerHeartbeatPacket->getPayloadSize() > 0) {
// if either of our sockets have changed we need to re-sign the heartbeat
// first read the sockets out from the current packet
_iceServerHeartbeatPacket->seek(0);
QDataStream heartbeatStream(_iceServerHeartbeatPacket.get());
QUuid senderUUID;
HifiSockAddr publicSocket, localSocket;
heartbeatStream >> senderUUID >> publicSocket >> localSocket;
if (senderUUID != limitedNodeList->getSessionUUID()
|| publicSocket != limitedNodeList->getPublicSockAddr()
|| localSocket != limitedNodeList->getLocalSockAddr()) {
shouldRecreatePacket = true;
}
} else {
shouldRecreatePacket = true;
}
if (shouldRecreatePacket) {
// either we don't have a heartbeat packet yet or some combination of sockets, ID and keypair have changed
// and we need to make a new one
// reset the position in the packet before writing
_iceServerHeartbeatPacket->reset();
// write our plaintext data to the packet
QDataStream heartbeatDataStream(_iceServerHeartbeatPacket.get());
heartbeatDataStream << limitedNodeList->getSessionUUID()
<< limitedNodeList->getPublicSockAddr() << limitedNodeList->getLocalSockAddr();
// setup a QByteArray that points to the plaintext data
auto plaintext = QByteArray::fromRawData(_iceServerHeartbeatPacket->getPayload(), _iceServerHeartbeatPacket->getPayloadSize());
// generate a signature for the plaintext data in the packet
auto signature = accountManager.getAccountInfo().signPlaintext(plaintext);
// pack the signature with the data
heartbeatDataStream << signature;
}
// send the heartbeat packet to the ice server now
limitedNodeList->sendUnreliablePacket(*_iceServerHeartbeatPacket, _iceServerSocket);
}
}
@ -1970,3 +2015,31 @@ void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMes
});
}
}
void DomainServer::processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message) {
static const int NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN = 3;
static int numHeartbeatDenials = 0;
if (++numHeartbeatDenials > NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN) {
qDebug() << "Received" << NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN << "heartbeat denials from ice-server"
<< "- re-generating keypair now";
// we've hit our threshold of heartbeat denials, trigger a keypair re-generation
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
AccountManager::getInstance().generateNewDomainKeypair(limitedNodeList->getSessionUUID());
// reset our number of heartbeat denials
numHeartbeatDenials = 0;
}
}
void DomainServer::handleKeypairChange() {
if (_iceServerHeartbeatPacket) {
// reset the payload size of the ice-server heartbeat packet - this causes the packet to be re-generated
// the next time we go to send an ice-server heartbeat
_iceServerHeartbeatPacket->setPayloadSize(0);
// send a heartbeat to the ice server immediately
sendHeartbeatToIceServer();
}
}

View file

@ -61,6 +61,7 @@ public slots:
void processNodeJSONStatsPacket(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer sendingNode);
void processPathQueryPacket(QSharedPointer<ReceivedMessage> packet);
void processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message);
private slots:
void aboutToQuit();
@ -78,16 +79,16 @@ private slots:
void handleTempDomainError(QNetworkReply& requestReply);
void queuedQuit(QString quitMessage, int exitCode);
void handleKeypairChange();
private:
void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid());
bool optionallySetupOAuth();
bool optionallyReadX509KeyAndCertificate();
bool optionallySetupAssignmentPayment();
void optionallyGetTemporaryName(const QStringList& arguments);
bool didSetupAccountManagerWithAccessToken();
bool resetAccountManagerAccessToken();
void setupAutomaticNetworking();
@ -153,6 +154,7 @@ private:
DomainServerSettingsManager _settingsManager;
HifiSockAddr _iceServerSocket;
std::unique_ptr<NLPacket> _iceServerHeartbeatPacket;
QTimer* _iceHeartbeatTimer { nullptr }; // this looks like it dangles when created but it's parented to the DomainServer

View file

@ -1,218 +1,260 @@
"use strict";
/*jslint nomen: true, plusplus: true, vars: true*/
/*global AvatarList, Entities, EntityViewer, Script, SoundCache, Audio, print, randFloat*/
//
// ACAudioSearchAndInject.js
// audio
//
// Created by Eric Levin 2/1/2016
// Created by Eric Levin and Howard Stearns 2/1/2016
// Copyright 2016 High Fidelity, Inc.
// This AC script searches for special sound entities nearby avatars and plays those sounds based off information specified in the entity's
// user data field ( see acAudioSearchAndCompatibilityEntitySpawner.js for an example)
//
// Keeps track of all sounds within QUERY_RADIUS of an avatar, where a "sound" is specified in entity userData.
// Inject as many as practical into the audio mixer.
// See acAudioSearchAndCompatibilityEntitySpawner.js.
//
// This implementation takes some precautions to scale well:
// - It doesn't hastle the entity server because it issues at most one octree query every UPDATE_TIME period, regardless of the number of avatars.
// - It does not load itself because it only gathers entities once every UPDATE_TIME period, and only
// checks entity properties for those small number of entities that are currently playing (plus a RECHECK_TIME period examination of all entities).
// This implementation tries to use all the available injectors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
Script.include("https://rawgit.com/highfidelity/hifi/master/examples/libraries/utils.js");
var SOUND_DATA_KEY = "soundKey";
var QUERY_RADIUS = 50;
EntityViewer.setKeyholeRadius(QUERY_RADIUS);
Entities.setPacketsPerSecond(6000);
Agent.isAvatar = true;
var MSEC_PER_SEC = 1000;
var SOUND_DATA_KEY = "io.highfidelity.soundKey"; // Sound data is specified in userData under this key.
var old_sound_data_key = "soundKey"; // For backwards compatibility.
var QUERY_RADIUS = 50; // meters
var UPDATE_TIME = 100; // ms. We'll update just one thing on this period.
var EXPIRATION_TIME = 5 * MSEC_PER_SEC; // ms. Remove sounds that have been out of range for this time.
var RECHECK_TIME = 10 * MSEC_PER_SEC; // ms. Check for new userData properties this often when not currently playing.
// (By not checking most of the time when not playing, we can efficiently go through all entities without getEntityProperties.)
var UPDATES_PER_STATS_LOG = RECHECK_TIME / UPDATE_TIME; // (It's nice to smooth out the results by straddling a recheck.)
var DEFAULT_SOUND_DATA = {
volume: 0.5,
loop: false,
playbackGap: 1000, // in ms
volume: 0.5, // userData cannot specify zero volume with our current method of defaulting.
loop: false, // Default must be false with our current method of defaulting, else there's no way to get a false value.
playbackGap: MSEC_PER_SEC, // in ms
playbackGapRange: 0 // in ms
};
var MIN_PLAYBACK_GAP = 0;
var UPDATE_TIME = 100;
var EXPIRATION_TIME = 5000;
Script.include("../../libraries/utils.js");
Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList.
function ignore() {}
function debug() { // Display the arguments not just [Object object].
//print.apply(null, [].map.call(arguments, JSON.stringify));
}
var soundEntityMap = {};
var soundUrls = {};
EntityViewer.setKeyholeRadius(QUERY_RADIUS);
var avatarPositions = [];
function update() {
var avatars = AvatarList.getAvatarIdentifiers();
for (var i = 0; i < avatars.length; i++) {
var avatar = AvatarList.getAvatar(avatars[i]);
var avatarPosition = avatar.position;
if (!avatarPosition) {
continue;
// ENTITY DATA CACHE
//
var entityCache = {}; // A dictionary of unexpired EntityData objects.
var examinationCount = 0;
function EntityDatum(entityIdentifier) { // Just the data of an entity that we need to know about.
// This data is only use for our sound injection. There is no need to store such info in the replicated entity on everyone's computer.
var that = this;
that.lastUserDataUpdate = 0; // new entity is in need of rechecking user data
// State Transitions:
// no data => no data | sound data | expired
// expired => stop => remove
// sound data => downloading
// downloading => downloading | waiting
// waiting => playing | waiting (if too many already playing)
// playing => update position etc | no data
that.stop = function stop() {
if (!that.sound) {
return;
}
EntityViewer.setPosition(avatarPosition);
EntityViewer.queryOctree();
avatarPositions.push(avatarPosition);
print("stopping sound", entityIdentifier, that.url);
delete that.sound;
delete that.url;
if (!that.injector) {
return;
}
that.injector.stop();
delete that.injector;
};
this.update = function stateTransitions(expirationCutoff, userDataCutoff, now) {
if (that.timestamp < expirationCutoff) { // EXPIRED => STOP => REMOVE
that.stop(); // Alternatively, we could fade out and then stop...
delete entityCache[entityIdentifier];
return;
}
var properties, soundData; // Latest data, pulled from local octree.
// getEntityProperties locks the tree, which competes with the asynchronous processing of queryOctree results.
// Most entity updates are fast and only a very few do getEntityProperties.
function ensureSoundData() { // We only getEntityProperities when we need to.
if (properties) {
return;
}
properties = Entities.getEntityProperties(entityIdentifier, ['userData', 'position']);
examinationCount++; // Collect statistics on how many getEntityProperties we do.
debug("updating", that, properties);
try {
var userData = properties.userData && JSON.parse(properties.userData);
soundData = userData && (userData[SOUND_DATA_KEY] || userData[old_sound_data_key]); // Don't store soundData yet. Let state changes compare.
that.lastUserDataUpdate = now; // But do update these ...
that.url = soundData && soundData.url;
that.playAfter = that.url && now;
} catch (err) {
print(err, properties.userData);
}
}
// Stumbling on big new pile of entities will do a lot of getEntityProperties. Once.
if (that.lastUserDataUpdate < userDataCutoff) { // NO DATA => SOUND DATA
ensureSoundData();
}
if (!that.url) { // NO DATA => NO DATA
return that.stop();
}
if (!that.sound) { // SOUND DATA => DOWNLOADING
that.sound = SoundCache.getSound(soundData.url); // SoundCache can manage duplicates better than we can.
}
if (!that.sound.downloaded) { // DOWNLOADING => DOWNLOADING
return;
}
if (that.playAfter > now) { // DOWNLOADING | WAITING => WAITING
return;
}
ensureSoundData(); // We'll try to play/setOptions and will need position, so we might as well get soundData, too.
if (soundData.url !== that.url) { // WAITING => NO DATA (update next time around)
return that.stop();
}
var options = {
position: properties.position,
loop: soundData.loop || DEFAULT_SOUND_DATA.loop,
volume: soundData.volume || DEFAULT_SOUND_DATA.volume
};
function repeat() {
return !options.loop && (soundData.playbackGap >= 0);
}
function randomizedNextPlay() { // time of next play or recheck, randomized to distribute the work
var range = soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange,
base = repeat() ? ((that.sound.duration * MSEC_PER_SEC) + (soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap)) : RECHECK_TIME;
return now + base + randFloat(-Math.min(base, range), range);
}
if (!that.injector) { // WAITING => PLAYING | WAITING
debug("starting", that, options);
that.injector = Audio.playSound(that.sound, options); // Might be null if at at injector limit. Will try again later.
if (that.injector) {
print("started", entityIdentifier, that.url);
} else { // Don't hammer ensureSoundData or injector manager.
that.playAfter = randomizedNextPlay();
}
return;
}
that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC
if (!that.injector.isPlaying) { // Subtle: a looping sound will not check playbackGap.
if (repeat()) { // WAITING => PLAYING
// Setup next play just once, now. Changes won't be looked at while we wait.
that.playAfter = randomizedNextPlay();
// Subtle: if the restart fails b/c we're at injector limit, we won't try again until next playAfter.
that.injector.restart();
} else { // PLAYING => NO DATA
that.playAfter = Infinity; // was one-shot and we're finished
}
}
};
}
function internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar) {
ignore(avatarPosition, avatar); // We could use avatars and/or avatarPositions to prioritize which ones to play.
var entitySound = entityCache[entityIdentifier];
if (!entitySound) {
entitySound = entityCache[entityIdentifier] = new EntityDatum(entityIdentifier);
}
Script.setTimeout(function() {
avatarPositions.forEach(function(avatarPosition) {
var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS);
handleFoundSoundEntities(entities);
entitySound.timestamp = timestamp; // Might be updated for multiple avatars. That's fine.
}
var nUpdates = UPDATES_PER_STATS_LOG, lastStats = Date.now();
function updateAllEntityData() { // A fast update of all entities we know about. A few make sounds.
var now = Date.now(),
expirationCutoff = now - EXPIRATION_TIME,
userDataRecheckCutoff = now - RECHECK_TIME;
Object.keys(entityCache).forEach(function (entityIdentifier) {
entityCache[entityIdentifier].update(expirationCutoff, userDataRecheckCutoff, now);
});
if (nUpdates-- <= 0) { // Report statistics.
// For example, with:
// injector-limit = 40 (in C++ code)
// N_SOUNDS = 1000 (from userData in, e.g., acAudioSearchCompatibleEntitySpawner.js)
// replay-period = 3 + 20 = 23 (seconds, ditto)
// stats-period = UPDATES_PER_STATS_LOG * UPDATE_TIME / MSEC_PER_SEC = 10 seconds
// The log should show between each stats report:
// "start" lines ~= injector-limit * P(finish) = injector-limit * stats-period/replay-period = 17 ?
// total attempts at starting ("start" lines + "could not thread" lines) ~= N_SOUNDS = 1000 ?
// entities > N_SOUNDS * (1+ N_SILENT_ENTITIES_PER_SOUND) = 11000 + whatever was in the scene before running spawner
// sounds = N_SOUNDS = 1000
// getEntityPropertiesPerUpdate ~= playing + failed-starts/UPDATES_PER_STATS_LOG + other-rechecks-each-update
// = injector-limit + (total attempts - "start" lines)/UPDATES_PER_STATS__LOG
// + (entities - playing - failed-starts/UPDATES_PER_STATS_LOG) * P(recheck-in-update)
// where failed-starts/UPDATES_PER_STATS_LOG = (1000-17)/100 = 10
// = 40 + 10 + (11000 - 40 - 10)*UPDATE_TIME/RECHECK_TIME
// = 40 + 10 + 10950*0.01 = 159 (mostly proportional to enties/RECHECK_TIME)
// millisecondsPerUpdate ~= UPDATE_TIME = 100 (+ some timer machinery time)
// this assignment client activity monitor < 100% cpu
var stats = {
entities: 0,
sounds: 0,
playing: 0,
getEntityPropertiesPerUpdate: examinationCount / UPDATES_PER_STATS_LOG,
millisecondsPerUpdate: (now - lastStats) / UPDATES_PER_STATS_LOG
};
nUpdates = UPDATES_PER_STATS_LOG;
lastStats = now;
examinationCount = 0;
Object.keys(entityCache).forEach(function (entityIdentifier) {
var datum = entityCache[entityIdentifier];
stats.entities++;
if (datum.url) {
stats.sounds++;
if (datum.injector && datum.injector.isPlaying) {
stats.playing++;
}
}
});
//Now wipe list for next query;
avatarPositions = [];
}, UPDATE_TIME);
handleActiveSoundEntities();
}
function handleActiveSoundEntities() {
// Go through all our sound entities, if they have passed expiration time, remove them from map
for (var potentialSoundEntity in soundEntityMap) {
if (!soundEntityMap.hasOwnProperty(potentialSoundEntity)) {
// The current property is not a direct property of soundEntityMap so ignore it
continue;
}
var soundEntity = potentialSoundEntity;
var soundProperties = soundEntityMap[soundEntity];
soundProperties.timeWithoutAvatarInRange += UPDATE_TIME;
if (soundProperties.timeWithoutAvatarInRange > EXPIRATION_TIME && soundProperties.soundInjector) {
// An avatar hasn't been within range of this sound entity recently, so remove it from map
soundProperties.soundInjector.stop();
delete soundEntityMap[soundEntity];
} else if (soundProperties.isDownloaded) {
// If this sound hasn't expired yet, we want to potentially play it!
if (soundProperties.readyToPlay) {
var newPosition = Entities.getEntityProperties(soundEntity, "position").position;
if (!soundProperties.soundInjector) {
soundProperties.soundInjector = Audio.playSound(soundProperties.sound, {
volume: soundProperties.volume,
position: newPosition,
loop: soundProperties.loop
});
} else {
soundProperties.soundInjector.restart();
}
soundProperties.readyToPlay = false;
} else if (soundProperties.sound && soundProperties.loop === false) {
// We need to check all of our entities that are not looping but have an interval associated with them
// to see if it's time for them to play again
soundProperties.timeSinceLastPlay += UPDATE_TIME;
if (soundProperties.timeSinceLastPlay > soundProperties.clipDuration + soundProperties.currentPlaybackGap) {
soundProperties.readyToPlay = true;
soundProperties.timeSinceLastPlay = 0;
// Now let's get our new current interval
soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange);
soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap);
}
}
}
print(JSON.stringify(stats));
}
}
function handleFoundSoundEntities(entities) {
entities.forEach(function(entity) {
var soundData = getEntityCustomData(SOUND_DATA_KEY, entity);
if (soundData && soundData.url) {
//check sound entities list- if it's not in, add it
if (!soundEntityMap[entity]) {
var soundProperties = {
url: soundData.url,
volume: soundData.volume || DEFAULT_SOUND_DATA.volume,
loop: soundData.loop || DEFAULT_SOUND_DATA.loop,
playbackGap: soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap,
playbackGapRange: soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange,
readyToPlay: false,
position: Entities.getEntityProperties(entity, "position").position,
timeSinceLastPlay: 0,
timeWithoutAvatarInRange: 0,
isDownloaded: false
};
soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange);
soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap);
soundEntityMap[entity] = soundProperties;
if (!soundUrls[soundData.url]) {
// We need to download sound before we add it to our map
var sound = SoundCache.getSound(soundData.url);
// Only add it to map once it's downloaded
soundUrls[soundData.url] = sound;
sound.ready.connect(function() {
soundProperties.sound = sound;
soundProperties.readyToPlay = true;
soundProperties.isDownloaded = true;
soundProperties.clipDuration = sound.duration * 1000;
soundEntityMap[entity] = soundProperties;
});
} else {
// We already have sound downloaded, so just add it to map right away
soundProperties.sound = soundUrls[soundData.url];
soundProperties.clipDuration = soundProperties.sound.duration * 1000;
soundProperties.readyToPlay = true;
soundProperties.isDownloaded = true;
soundEntityMap[entity] = soundProperties;
}
} else {
//If this sound is in our map already, we want to reset timeWithoutAvatarInRange
// Also we want to check to see if the entity has been updated with new sound data- if so we want to update!
soundEntityMap[entity].timeWithoutAvatarInRange = 0;
checkForSoundPropertyChanges(soundEntityMap[entity], soundData);
}
}
// Update the set of which EntityData we know about.
//
function updateEntiesForAvatar(avatarIdentifier) { // Just one piece of update work.
// This does at most:
// one queryOctree request of the entity server, and
// one findEntities geometry query of our own octree, and
// a quick internEntityDatum of each of what may be a large number of entityIdentifiers.
// The idea is that this is a nice bounded piece of work that should not be done too frequently.
// However, it means that we won't learn about new entities until, on average (nAvatars * UPDATE_TIME) + query round trip.
var avatar = AvatarList.getAvatar(avatarIdentifier), avatarPosition = avatar && avatar.position;
if (!avatarPosition) { // No longer here.
return;
}
var timestamp = Date.now();
EntityViewer.setPosition(avatarPosition);
EntityViewer.queryOctree(); // Requests an update, but there's no telling when we'll actually see different results.
var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS);
debug("found", entities.length, "entities near", avatar.name || "unknown", "at", avatarPosition);
entities.forEach(function (entityIdentifier) {
internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar);
});
}
function checkForSoundPropertyChanges(currentProps, newProps) {
var needsNewInjector = false;
if (currentProps.playbackGap !== newProps.playbackGap && !currentProps.loop) {
// playbackGap only applies to non looping sounds
currentProps.playbackGap = newProps.playbackGap;
currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange);
currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap);
currentProps.readyToPlay = true;
}
if (currentProps.playbackGapRange !== currentProps.playbackGapRange) {
currentProps.playbackGapRange = newProps.playbackGapRange;
currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange);
currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap);
currentProps.readyToPlay = true;
}
if (currentProps.volume !== newProps.volume) {
currentProps.volume = newProps.volume;
needsNewInjector = true;
}
if (currentProps.url !== newProps.url) {
currentProps.url = newProps.url;
currentProps.sound = null;
if (!soundUrls[currentProps.url]) {
var sound = SoundCache.getSound(currentProps.url);
currentProps.isDownloaded = false;
sound.ready.connect(function() {
currentProps.sound = sound;
currentProps.clipDuration = sound.duration * 1000;
currentProps.isDownloaded = true;
});
} else {
currentProps.sound = sound;
currentProps.clipDuration = sound.duration * 1000;
}
needsNewInjector = true;
}
if (currentProps.loop !== newProps.loop) {
currentProps.loop = newProps.loop;
needsNewInjector = true;
}
if (needsNewInjector) {
// If we were looping we need to stop that so new changes are applied
currentProps.soundInjector.stop();
currentProps.soundInjector = null;
currentProps.readyToPlay = true;
}
// Slowly update the set of data we have to work with.
//
var workQueue = [];
function updateWorkQueueForAvatarsPresent() { // when nothing else to do, fill queue with individual avatar updates
workQueue = AvatarList.getAvatarIdentifiers().map(function (avatarIdentifier) {
return function () {
updateEntiesForAvatar(avatarIdentifier);
};
});
}
Script.setInterval(update, UPDATE_TIME);
Script.setInterval(function () {
// There might be thousands of EntityData known to us, but only a few will require any work to update.
updateAllEntityData(); // i.e., this better be pretty fast.
// Each interval, we do no more than one updateEntitiesforAvatar.
if (!workQueue.length) {
workQueue = [updateWorkQueueForAvatarsPresent];
}
workQueue.pop()(); // There's always one
}, UPDATE_TIME);

View file

@ -1,4 +1,6 @@
//
"use strict";
/*jslint nomen: true, plusplus: true, vars: true*/
/*global Entities, Script, Quat, Vec3, Camera, MyAvatar, print, randFloat*/
// acAudioSearchCompatibleEntitySpawner.js
// audio/acAudioSearching
//
@ -13,6 +15,10 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var N_SOUNDS = 1000;
var N_SILENT_ENTITIES_PER_SOUND = 10;
var ADD_PERIOD = 50; // ms between adding 1 sound + N_SILENT_ENTITIES_PER_SOUND, to not overrun entity server.
var SPATIAL_DISTRIBUTION = 10; // meters spread over how far to randomly distribute enties.
Script.include("../../libraries/utils.js");
var orientation = Camera.getOrientation();
orientation = Quat.safeEulerAngles(orientation);
@ -20,20 +26,21 @@ orientation.x = 0;
orientation = Quat.fromVec3Degrees(orientation);
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation)));
// http://hifi-public.s3.amazonaws.com/ryan/demo/0619_Fireplace__Tree_B.L.wav
var SOUND_DATA_KEY = "soundKey";
var SOUND_DATA_KEY = "io.highfidelity.soundKey";
var userData = {
soundKey: {
url: "http://hifi-content.s3.amazonaws.com/DomainContent/Junkyard/Sounds/ClothSail/cloth_sail3.L.wav",
volume: 0.3,
loop: false,
playbackGap: 2000, // In ms - time to wait in between clip plays
playbackGapRange: 500 // In ms - the range to wait in between clip plays
playbackGap: 20000, // In ms - time to wait in between clip plays
playbackGapRange: 5000 // In ms - the range to wait in between clip plays
}
}
};
var userDataString = JSON.stringify(userData);
var entityProps = {
type: "Box",
position: center,
name: 'audioSearchEntity',
color: {
red: 200,
green: 10,
@ -43,15 +50,41 @@ var entityProps = {
x: 0.1,
y: 0.1,
z: 0.1
},
userData: JSON.stringify(userData)
}
};
var entities = [], nSounds = 0;
Script.include("../../libraries/utils.js");
function addOneSet() {
function randomizeDimension(coordinate) {
return coordinate + randFloat(-SPATIAL_DISTRIBUTION / 2, SPATIAL_DISTRIBUTION / 2);
}
function randomize() {
return {x: randomizeDimension(center.x), y: randomizeDimension(center.y), z: randomizeDimension(center.z)};
}
function addOne() {
entityProps.position = randomize();
entities.push(Entities.addEntity(entityProps));
}
var i;
entityProps.userData = userDataString;
entityProps.color.red = 200;
addOne();
delete entityProps.userData;
entityProps.color.red = 10;
for (i = 0; i < N_SILENT_ENTITIES_PER_SOUND; i++) {
addOne();
}
if (++nSounds < N_SOUNDS) {
Script.setTimeout(addOneSet, ADD_PERIOD);
}
}
var soundEntity = Entities.addEntity(entityProps);
addOneSet();
function cleanup() {
Entities.deleteEntity(soundEntity);
entities.forEach(Entities.deleteEntity);
}
// In console:
// Entities.findEntities(MyAvatar.position, 100).forEach(function (id) { if (Entities.getEntityProperties(id).name === 'audioSearchEntity') Entities.deleteEntity(id); })
Script.scriptEnding.connect(cleanup);
Script.scriptEnding.connect(cleanup);

View file

@ -73,9 +73,7 @@ var PICK_MAX_DISTANCE = 500; // max length of pick-ray
var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
var NEAR_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable.
var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected
var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things
var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object
var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed
var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size
@ -173,6 +171,10 @@ var STATE_WAITING_FOR_BUMPER_RELEASE = 15;
var COLLIDES_WITH_WHILE_GRABBED = "dynamic,otherAvatar";
var COLLIDES_WITH_WHILE_MULTI_GRABBED = "dynamic";
var HEART_BEAT_INTERVAL = 5; // seconds
var HEART_BEAT_TIMEOUT = 15;
function stateToName(state) {
switch (state) {
case STATE_OFF:
@ -1103,6 +1105,8 @@ function MyController(hand) {
return;
}
this.heartBeat(this.grabbedEntity);
var handPosition = this.getHandPosition();
var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition;
var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
@ -1438,6 +1442,8 @@ function MyController(hand) {
return;
}
this.heartBeat(this.grabbedEntity);
var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position"]);
if (props.parentID == MyAvatar.sessionUUID &&
Vec3.length(props.localPosition) > NEAR_PICK_MAX_DISTANCE * 2.0) {
@ -1644,7 +1650,7 @@ function MyController(hand) {
// this next line allowed both:
// (1) far-grab, pull to self, near grab, then throw
// (2) equip something physical and adjust it with a other-hand grab without the thing drifting
(!this.isInitialGrab && grabData.refCount > 1)) {
grabData.refCount > 1) {
noVelocity = true;
}
}
@ -1670,19 +1676,39 @@ function MyController(hand) {
Entities.deleteEntity(this.pointLight);
};
this.heartBeat = function(entityID) {
var now = Date.now();
if (now - this.lastHeartBeat > HEART_BEAT_INTERVAL) {
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
data["heartBeat"] = now;
setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data);
this.lastHeartBeat = now;
}
};
this.resetAbandonedGrab = function(entityID) {
print("cleaning up abandoned grab on " + entityID);
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
data["refCount"] = 1;
setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data);
this.deactivateEntity(entityID, false);
};
this.activateEntity = function(entityID, grabbedProperties, wasLoaded) {
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA);
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
data["activated"] = true;
data["avatarId"] = MyAvatar.sessionUUID;
var now = Date.now();
if (wasLoaded) {
data["refCount"] = 1;
data["avatarId"] = MyAvatar.sessionUUID;
} else {
data["refCount"] = data["refCount"] ? data["refCount"] + 1 : 1;
// zero gravity and set ignoreForCollisions in a way that lets us put them back, after all grabs are done
if (data["refCount"] == 1) {
data["heartBeat"] = now;
this.lastHeartBeat = now;
this.isInitialGrab = true;
data["gravity"] = grabbedProperties.gravity;
data["collidesWith"] = grabbedProperties.collidesWith;
@ -1698,12 +1724,21 @@ function MyController(hand) {
z: 0
},
// bummer, it isn't easy to do bitwise collisionMask operations like this:
//"collisionMask": COLLISION_MASK_WHILE_GRABBED | grabbedProperties.collisionMask
// "collisionMask": COLLISION_MASK_WHILE_GRABBED | grabbedProperties.collisionMask
// when using string values
"collidesWith": COLLIDES_WITH_WHILE_GRABBED
};
Entities.editEntity(entityID, whileHeldProperties);
} else if (data["refCount"] > 1) {
if (data["heartBeat"] === undefined ||
now - data["heartBeat"] > HEART_BEAT_TIMEOUT) {
// this entity has userData suggesting it is grabbed, but nobody is updating the hearbeat.
// deactivate it before grabbing.
this.resetAbandonedGrab(entityID);
grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES);
return this.activateEntity(entityID, grabbedProperties, wasLoaded);
}
this.isInitialGrab = false;
// if an object is being grabbed by more than one person (or the same person twice, but nevermind), switch
// the collision groups so that it wont collide with "other" avatars. This avoids a situation where two

View file

@ -72,10 +72,13 @@
};
}
function createEmitNumberPropertyUpdateFunction(propertyName) {
function createEmitNumberPropertyUpdateFunction(propertyName, decimals) {
decimals = decimals == undefined ? 4 : decimals;
return function() {
var value = parseFloat(this.value).toFixed(decimals);
EventBridge.emitWebEvent(
'{ "type":"update", "properties":{"' + propertyName + '":' + parseFloat(this.value).toFixed(4) + '}}'
'{ "type":"update", "properties":{"' + propertyName + '":' + value + '}}'
);
};
}
@ -323,6 +326,7 @@
var elLightColorBlue = document.getElementById("property-light-color-blue");
var elLightIntensity = document.getElementById("property-light-intensity");
var elLightFalloffRadius = document.getElementById("property-light-falloff-radius");
var elLightExponent = document.getElementById("property-light-exponent");
var elLightCutoff = document.getElementById("property-light-cutoff");
@ -604,9 +608,10 @@
elLightColorGreen.value = properties.color.green;
elLightColorBlue.value = properties.color.blue;
elLightIntensity.value = properties.intensity;
elLightExponent.value = properties.exponent;
elLightCutoff.value = properties.cutoff;
elLightIntensity.value = properties.intensity.toFixed(1);
elLightFalloffRadius.value = properties.falloffRadius.toFixed(1);
elLightExponent.value = properties.exponent.toFixed(2);
elLightCutoff.value = properties.cutoff.toFixed(2);
} else if (properties.type == "Zone") {
for (var i = 0; i < elZoneSections.length; i++) {
elZoneSections[i].style.display = 'block';
@ -795,9 +800,10 @@
}
})
elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity'));
elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent'));
elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff'));
elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity', 1));
elLightFalloffRadius.addEventListener('change', createEmitNumberPropertyUpdateFunction('falloffRadius', 1));
elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent', 2));
elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff', 2));
elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl'));
@ -1043,25 +1049,25 @@
<div class="text-section property">
<div class="label">Line Height</div>
<div class="value">
<input class="coord" type='number' id="property-text-line-height" min="0" step="0.005">
<input class="coord" type="number" id="property-text-line-height" min="0" step="0.005">
</div>
</div>
<div class="text-section property">
<div class="label">Text Color</div>
<div class="value">
<div class='color-picker' id="property-text-text-color"></div>
<div class="input-area">R <input class="coord" type='number' id="property-text-text-color-red"></div>
<div class="input-area">G <input class="coord" type='number' id="property-text-text-color-green"></div>
<div class="input-area">B <input class="coord" type='number' id="property-text-text-color-blue"></div>
<div class="input-area">R <input class="coord" type="number" id="property-text-text-color-red"></div>
<div class="input-area">G <input class="coord" type="number" id="property-text-text-color-green"></div>
<div class="input-area">B <input class="coord" type="number" id="property-text-text-color-blue"></div>
</div>
</div>
<div class="text-section property">
<div class="label">Background Color</div>
<div class="value">
<div class='color-picker' id="property-text-background-color"></div>
<div class="input-area">R <input class="coord" type='number' id="property-text-background-color-red"></div>
<div class="input-area">G <input class="coord" type='number' id="property-text-background-color-green"></div>
<div class="input-area">B <input class="coord" type='number' id="property-text-background-color-blue"></div>
<div class="input-area">R <input class="coord" type="number" id="property-text-background-color-red"></div>
<div class="input-area">G <input class="coord" type="number" id="property-text-background-color-green"></div>
<div class="input-area">B <input class="coord" type="number" id="property-text-background-color-blue"></div>
</div>
</div>
@ -1085,32 +1091,32 @@
<div class="label">Light Color</div>
<div class="value">
<div class='color-picker' id="property-zone-key-light-color"></div>
<div class="input-area">R <input class="coord" type='number' id="property-zone-key-light-color-red" min="0" max="255" step="1"></div>
<div class="input-area">G <input class="coord" type='number' id="property-zone-key-light-color-green" min="0" max="255" step="1"></div>
<div class="input-area">B <input class="coord" type='number' id="property-zone-key-light-color-blue" min="0" max="255" step="1"></div>
<div class="input-area">R <input class="coord" type="number" id="property-zone-key-light-color-red" min="0" max="255" step="1"></div>
<div class="input-area">G <input class="coord" type="number" id="property-zone-key-light-color-green" min="0" max="255" step="1"></div>
<div class="input-area">B <input class="coord" type="number" id="property-zone-key-light-color-blue" min="0" max="255" step="1"></div>
</div>
</div>
<div class="zone-section keyLight-section property">
<div class="label">Light Intensity</div>
<div class="value">
<input class="coord" type='number' id="property-zone-key-intensity" min="0" max="10" step="0.1">
<input class="coord" type="number" id="property-zone-key-intensity" min="0" max="10" step="0.1">
</div>
</div>
<div class="zone-section keyLight-section property">
<div class="label">Light Direction</div>
<div class="value">
<div class="input-area">Pitch <input class="coord" type='number' id="property-zone-key-light-direction-x"></div>
<div class="input-area">Yaw <input class="coord" type='number' id="property-zone-key-light-direction-y"></div>
<div class="input-area">Roll <input class="coord" type='number' id="property-zone-key-light-direction-z"></div>
<div class="input-area">Pitch <input class="coord" type="number" id="property-zone-key-light-direction-x"></div>
<div class="input-area">Yaw <input class="coord" type="number" id="property-zone-key-light-direction-y"></div>
<div class="input-area">Roll <input class="coord" type="number" id="property-zone-key-light-direction-z"></div>
</div>
</div>
<div class="zone-section keyLight-section property">
<div class="label">Ambient Intensity</div>
<div class="value">
<input class="coord" type='number' id="property-zone-key-ambient-intensity" min="0" max="10" step="0.1">
<input class="coord" type="number" id="property-zone-key-ambient-intensity" min="0" max="10" step="0.1">
</div>
</div>
@ -1129,19 +1135,19 @@
<div class="zone-section stage-section property">
<div class="label">Stage Latitude</div>
<div class="value">
<input class="coord" type='number' id="property-zone-stage-latitude" min="-90" max="90" step="1">
<input class="coord" type="number" id="property-zone-stage-latitude" min="-90" max="90" step="1">
</div>
</div>
<div class="zone-section stage-section property">
<div class="label">Stage Longitude</div>
<div class="value">
<input class="coord" type='number' id="property-zone-stage-longitude" min="-180" max="180" step="1">
<input class="coord" type="number" id="property-zone-stage-longitude" min="-180" max="180" step="1">
</div>
</div>
<div class="zone-section stage-section property">
<div class="label">Stage Altitude</div>
<div class="value">
<input class="coord" type='number' id="property-zone-stage-altitude" step="1">
<input class="coord" type="number" id="property-zone-stage-altitude" step="1">
</div>
</div>
@ -1155,13 +1161,13 @@
<div class="zone-section stage-section property">
<div class="label">Stage Day</div>
<div class="value">
<input class="coord" type='number' id="property-zone-stage-day" min="0" max="365" step="1">
<input class="coord" type="number" id="property-zone-stage-day" min="0" max="365" step="1">
</div>
</div>
<div class="zone-section stage-section property">
<div class="label">Stage Hour</div>
<div class="value">
<input class="coord" type='number' id="property-zone-stage-hour" min="0" max="24" step="0.5">
<input class="coord" type="number" id="property-zone-stage-hour" min="0" max="24" step="0.5">
</div>
</div>
@ -1186,9 +1192,9 @@
<div class="label">Skybox Color</div>
<div class="value">
<div class='color-picker' id="property-zone-skybox-color"></div>
<div class="input-area">R <input class="coord" type='number' id="property-zone-skybox-color-red"></div>
<div class="input-area">G <input class="coord" type='number' id="property-zone-skybox-color-green"></div>
<div class="input-area">B <input class="coord" type='number' id="property-zone-skybox-color-blue"></div>
<div class="input-area">R <input class="coord" type="number" id="property-zone-skybox-color-red"></div>
<div class="input-area">G <input class="coord" type="number" id="property-zone-skybox-color-green"></div>
<div class="input-area">B <input class="coord" type="number" id="property-zone-skybox-color-blue"></div>
</div>
</div>
@ -1264,23 +1270,23 @@
<div class="property">
<div class="label">Registration</div>
<div class="value">
<div class="input-area">X <input class="coord" type='number' id="property-reg-x"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type='number' id="property-reg-y"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type='number' id="property-reg-z"><div class="prop-z"></div></div>
<div class="input-area">X <input class="coord" type="number" id="property-reg-x"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type="number" id="property-reg-y"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type="number" id="property-reg-z"><div class="prop-z"></div></div>
</div>
</div>
<div class="property">
<div class="label">Dimensions</div>
<div class="value">
<div class="input-area">X <input class="coord" type='number' id="property-dim-x" step="0.1"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type='number' id="property-dim-y" step="0.1"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type='number' id="property-dim-z" step="0.1"><div class="prop-z"></div></div>
<div class="input-area">X <input class="coord" type="number" id="property-dim-x" step="0.1"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type="number" id="property-dim-y" step="0.1"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type="number" id="property-dim-z" step="0.1"><div class="prop-z"></div></div>
<div>
<input type="button" id="reset-to-natural-dimensions" value="Reset to Natural Dimensions">
</div>
<div class="input-area">
<input class="" type='number' id="dimension-rescale-pct" value=100>%
<input class="" type="number" id="dimension-rescale-pct" value=100>%
</div>
<span>
<input type="button" id="dimension-rescale-button" value="Rescale">
@ -1291,9 +1297,9 @@
<div class="poly-vox-section property">
<div class="label">Voxel Volume Size</div>
<div class="value">
<div class="input-area">X <br> <input class="coord" type='number' id="property-voxel-volume-size-x"></div>
<div class="input-area">Y <br><input class="coord" type='number' id="property-voxel-volume-size-y"></div>
<div class="input-area">Z <br><input class="coord" type='number' id="property-voxel-volume-size-z"></div>
<div class="input-area">X <br> <input class="coord" type="number" id="property-voxel-volume-size-x"></div>
<div class="input-area">Y <br><input class="coord" type="number" id="property-voxel-volume-size-y"></div>
<div class="input-area">Z <br><input class="coord" type="number" id="property-voxel-volume-size-z"></div>
</div>
<div class="label">Surface Extractor</div>
@ -1325,9 +1331,9 @@
<div class="property">
<div class="label">Rotation</div>
<div class="value">
<div class="input-area">Pitch <input class="coord" type='number' id="property-rot-x" step="0.1"></div>
<div class="input-area">Yaw <input class="coord" type='number' id="property-rot-y" step="0.1"></div>
<div class="input-area">Roll <input class="coord" type='number' id="property-rot-z"step="0.1"></div>
<div class="input-area">Pitch <input class="coord" type="number" id="property-rot-x" step="0.1"></div>
<div class="input-area">Yaw <input class="coord" type="number" id="property-rot-y" step="0.1"></div>
<div class="input-area">Roll <input class="coord" type="number" id="property-rot-z"step="0.1"></div>
</div>
</div>
@ -1339,66 +1345,66 @@
<div class="property">
<div class="label">Linear Velocity</div>
<div class="value">
<div class="input-area">X <input class="coord" type='number' id="property-lvel-x"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type='number' id="property-lvel-y"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type='number' id="property-lvel-z"><div class="prop-z"></div></div>
<div class="input-area">X <input class="coord" type="number" id="property-lvel-x"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type="number" id="property-lvel-y"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type="number" id="property-lvel-z"><div class="prop-z"></div></div>
</div>
</div>
<div class="property">
<div class="label">Linear Damping</div>
<div class="value">
<input class="coord" type='number' id="property-ldamping">
<input class="coord" type="number" id="property-ldamping">
</div>
</div>
<div class="property">
<div class="label">Angular Velocity</div>
<div class="value">
<div class="input-area">Pitch <input class="coord" type='number' id="property-avel-x"></div>
<div class="input-area">Yaw <input class="coord" type='number' id="property-avel-y"></div>
<div class="input-area">Roll <input class="coord" type='number' id="property-avel-z"></div>
<div class="input-area">Pitch <input class="coord" type="number" id="property-avel-x"></div>
<div class="input-area">Yaw <input class="coord" type="number" id="property-avel-y"></div>
<div class="input-area">Roll <input class="coord" type="number" id="property-avel-z"></div>
</div>
</div>
<div class="property">
<div class="label">Angular Damping</div>
<div class="value">
<input class="coord" type='number' id="property-adamping">
<input class="coord" type="number" id="property-adamping">
</div>
</div>
<div class="property">
<div class="label">Restitution</div>
<div class="value">
<input class="coord" type='number' id="property-restitution">
<input class="coord" type="number" id="property-restitution">
</div>
</div>
<div class="property">
<div class="label">Friction</div>
<div class="value">
<input class="coord" type='number' id="property-friction">
<input class="coord" type="number" id="property-friction">
</div>
</div>
<div class="property">
<div class="label">Gravity</div>
<div class="value">
<div class="input-area">X <input class="coord" type='number' id="property-grav-x"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type='number' id="property-grav-y"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type='number' id="property-grav-z"><div class="prop-z"></div></div>
<div class="input-area">X <input class="coord" type="number" id="property-grav-x"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type="number" id="property-grav-y"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type="number" id="property-grav-z"><div class="prop-z"></div></div>
</div>
</div>
<div class="property">
<div class="label">Acceleration</div>
<div class="value">
<div class="input-area">X <input class="coord" type='number' id="property-lacc-x"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type='number' id="property-lacc-y"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type='number' id="property-lacc-z"><div class="prop-z"></div></div>
<div class="input-area">X <input class="coord" type="number" id="property-lacc-x"><div class="prop-x"></div></div>
<div class="input-area">Y <input class="coord" type="number" id="property-lacc-y"><div class="prop-y"></div></div>
<div class="input-area">Z <input class="coord" type="number" id="property-lacc-z"><div class="prop-z"></div></div>
</div>
</div>
<div class="property">
<div class="label">Density</div>
<div>
<input type='number' id="property-density">
<input type="number" id="property-density">
</div>
</div>
@ -1406,9 +1412,9 @@
<div class="label">Color</div>
<div class="value">
<div id="property-color" class='color-picker'></div>
<div class="input-area">R <input class="coord" type='number' id="property-color-red"></div>
<div class="input-area">G <input class="coord" type='number' id="property-color-green"></div>
<div class="input-area">B <input class="coord" type='number' id="property-color-blue"></div>
<div class="input-area">R <input class="coord" type="number" id="property-color-red"></div>
<div class="input-area">G <input class="coord" type="number" id="property-color-green"></div>
<div class="input-area">B <input class="coord" type="number" id="property-color-blue"></div>
</div>
</div>
@ -1483,7 +1489,7 @@
<div class="property">
<div class="label">Lifetime</div>
<div class="value">
<input type='number' id="property-lifetime">
<input type="number" id="property-lifetime">
</div>
</div>
@ -1541,25 +1547,25 @@
<div class="model-section property">
<div class="label">Animation FPS</div>
<div class="value">
<input class="coord" type='number' id="property-model-animation-fps">
<input class="coord" type="number" id="property-model-animation-fps">
</div>
</div>
<div class="model-section property">
<div class="label">Animation Frame</div>
<div class="value">
<input class="coord" type='number' id="property-model-animation-frame">
<input class="coord" type="number" id="property-model-animation-frame">
</div>
</div>
<div class="model-section property">
<div class="label">Animation First Frame</div>
<div class="value">
<input class="coord" type='number' id="property-model-animation-first-frame">
<input class="coord" type="number" id="property-model-animation-first-frame">
</div>
</div>
<div class="model-section property">
<div class="label">Animation Last Frame</div>
<div class="value">
<input class="coord" type='number' id="property-model-animation-last-frame">
<input class="coord" type="number" id="property-model-animation-last-frame">
</div>
</div>
<div class="model-section property">
@ -1591,37 +1597,43 @@
<label>Light</label>
</div>
<div class="light-section property">
<div class="label">Color</div>
<div class="value">
<div class='color-picker' id="property-light-color"></div>
<div class="input-area">R <input class="coord" type="number" id="property-light-color-red"></div>
<div class="input-area">G <input class="coord" type="number" id="property-light-color-green"></div>
<div class="input-area">B <input class="coord" type="number" id="property-light-color-blue"></div>
</div>
</div>
<div class="light-section property">
<div class="label">Intensity</div>
<div class="value">
<input class="coord" type="number" id="property-light-intensity" min="0" step="0.1">
</div>
</div>
<div class="light-section property">
<div class="label">Falloff Radius</div>
<div class="value">
<input class="coord" type="number" id="property-light-falloff-radius" min="0" step="0.1">
</div>
</div>
<div class="light-section property">
<span class="label">Spot Light</span>
<span class="value">
<input type='checkbox' id="property-light-spot-light">
</span>
</div>
<div class="light-section property">
<div class="label">Color</div>
<div class="value">
<div class='color-picker' id="property-light-color"></div>
<div class="input-area">R <input class="coord" type='number' id="property-light-color-red"></div>
<div class="input-area">G <input class="coord" type='number' id="property-light-color-green"></div>
<div class="input-area">B <input class="coord" type='number' id="property-light-color-blue"></div>
</div>
</div>
<div class="light-section property">
<div class="label">Intensity</div>
<div class="value">
<input class="coord" type='number' id="property-light-intensity">
</div>
</div>
<div class="light-section property">
<div class="label">Spot Light Exponent</div>
<div class="value">
<input class="coord" type='number' id="property-light-exponent">
<input class="coord" type="number" id="property-light-exponent" step="0.01">
</div>
</div>
<div class="light-section property">
<div class="label">Spot Light Cutoff (degrees)</div>
<div class="value">
<input class="coord" type='number' id="property-light-cutoff">
<input class="coord" type="number" id="property-light-cutoff" step="0.01">
</div>
</div>
</div>

View file

@ -199,7 +199,7 @@ pointInExtents = function(point, minPoint, maxPoint) {
* @param Number l The lightness
* @return Array The RGB representation
*/
hslToRgb = function(hsl, hueOffset) {
hslToRgb = function(hsl) {
var r, g, b;
if (hsl.s == 0) {
r = g = b = hsl.l; // achromatic

View file

@ -43,10 +43,10 @@ var keysToAllow = [
'emitSpeed',
'speedSpread',
'emitOrientation',
'emitDimensios',
'emitRadiusStart',
'emitDimensions',
'polarStart',
'polarFinish',
'azimuthStart',
'azimuthFinish',
'emitAcceleration',
'accelerationSpread',

View file

@ -0,0 +1,229 @@
//
// fireworksLaunchEntityScript.js
// examples/playa/fireworks
//
// Created by Eric Levin on 2/24/16.
// Copyright 2016 High Fidelity, Inc.
//
// Run this script to spawn a big fireworks launch button that a user can press
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
(function() {
Script.include("../../libraries/utils.js");
var _this;
Fireworks = function() {
_this = this;
_this.launchSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/missle+launch.wav");
_this.explosionSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/fireworksExplosion.wav");
_this.timeToExplosionRange = {
min: 2500,
max: 4500
};
};
Fireworks.prototype = {
startNearTrigger: function() {
_this.shootFireworks();
},
startFarTrigger: function() {
_this.shootFireworks();
},
clickReleaseOnEntity: function() {
_this.shootFireworks();
},
shootFireworks: function() {
// Get launch position
var launchPosition = getEntityUserData(_this.entityID).launchPosition || _this.position;
var numMissles = randInt(1, 3);
for(var i = 0; i < numMissles; i++) {
_this.shootMissle(launchPosition);
}
},
shootMissle: function(launchPosition) {
Audio.playSound(_this.launchSound, {
position: launchPosition,
volume: 0.5
});
var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Rocket-2.fbx";
var missleDimensions = Vec3.multiply({
x: 0.24,
y: 0.7,
z: 0.24
}, randFloat(0.2, 1.5));
var missleRotation = Quat.fromPitchYawRollDegrees(randInt(-60, 60), 0, randInt(-60, 60));
var missleVelocity = Vec3.multiply(Quat.getUp(missleRotation), randFloat(2, 4));
var missleAcceleration = Vec3.multiply(Quat.getUp(missleRotation), randFloat(1, 3));
var missle = Entities.addEntity({
type: "Model",
modelURL: MODEL_URL,
position: launchPosition,
rotation: missleRotation,
dimensions: missleDimensions,
damping: 0,
dynamic: true,
lifetime: 20, // Just in case
velocity: missleVelocity,
acceleration: missleAcceleration,
angularVelocity: {
x: 0,
y: randInt(-1, 1),
z: 0
},
angularDamping: 0,
visible: false
});
var smokeTrailPosition = Vec3.sum(launchPosition, Vec3.multiply(-missleDimensions.y / 2 + 0.1, Quat.getUp(missleRotation)));
var smoke = Entities.addEntity({
type: "ParticleEffect",
position: smokeTrailPosition,
lifespan: 10,
lifetime: 20,
name: "Smoke Trail",
maxParticles: 3000,
emitRate: 80,
emitSpeed: 0,
speedSpread: 0,
dimensions: {
x: 1000,
y: 1000,
z: 1000
},
polarStart: 0,
polarFinish: 0,
azimuthStart: -3.14,
azimuthFinish: 3.14,
emitAcceleration: {
x: 0,
y: 0.01,
z: 0
},
accelerationSpread: {
x: 0.01,
y: 0,
z: 0.01
},
radiusSpread: 0.03,
particleRadius: 0.3,
radiusStart: 0.06,
radiusFinish: 0.9,
alpha: 0.1,
alphaSpread: 0,
alphaStart: 0.7,
alphaFinish: 0,
textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png",
emitterShouldTrail: true,
parentID: missle,
});
Script.setTimeout(function() {
Entities.editEntity(smoke, {
parentID: null,
isEmitting: false
});
var explodeBasePosition = Entities.getEntityProperties(missle, "position").position;
Entities.deleteEntity(missle);
// Explode 1 firework immediately
_this.explodeFirework(explodeBasePosition);
var numAdditionalFireworks = randInt(1, 5);
for (var i = 0; i < numAdditionalFireworks; i++) {
Script.setTimeout(function() {
var explodePosition = Vec3.sum(explodeBasePosition, {x: randFloat(-3, 3), y: randFloat(-3, 3), z: randFloat(-3, 3)});
_this.explodeFirework(explodePosition);
}, randInt(0, 1000))
}
}, randFloat(_this.timeToExplosionRange.min, _this.timeToExplosionRange.max));
},
explodeFirework: function(explodePosition) {
// We just exploded firework, so stop emitting its fire and smoke
Audio.playSound(_this.explosionSound, {
position: explodePosition
});
var firework = Entities.addEntity({
name: "fireworks emitter",
position: explodePosition,
type: "ParticleEffect",
colorStart: hslToRgb({
h: Math.random(),
s: 0.5,
l: 0.7
}),
color: hslToRgb({
h: Math.random(),
s: 0.5,
l: 0.5
}),
colorFinish: hslToRgb({
h: Math.random(),
s: 0.5,
l: 0.7
}),
maxParticles: 10000,
lifetime: 20,
lifespan: randFloat(1.5, 3),
emitRate: randInt(500, 5000),
emitSpeed: randFloat(0.5, 2),
speedSpread: 0.2,
emitOrientation: Quat.fromPitchYawRollDegrees(randInt(0, 360), randInt(0, 360), randInt(0, 360)),
polarStart: 1,
polarFinish: randFloat(1.2, 3),
azimuthStart: -Math.PI,
azimuthFinish: Math.PI,
emitAcceleration: {
x: 0,
y: randFloat(-1, -0.2),
z: 0
},
accelerationSpread: {
x: Math.random(),
y: 0,
z: Math.random()
},
particleRadius: randFloat(0.001, 0.1),
radiusSpread: Math.random() * 0.1,
radiusStart: randFloat(0.001, 0.1),
radiusFinish: randFloat(0.001, 0.1),
alpha: randFloat(0.8, 1.0),
alphaSpread: randFloat(0.1, 0.2),
alphaStart: randFloat(0.7, 1.0),
alphaFinish: randFloat(0.7, 1.0),
textures: "http://ericrius1.github.io/PlatosCave/assets/star.png",
});
Script.setTimeout(function() {
Entities.editEntity(firework, {
isEmitting: false
});
}, randInt(500, 1000));
},
preload: function(entityID) {
_this.entityID = entityID;
_this.position = Entities.getEntityProperties(_this.entityID, "position").position;
print("EBL RELOAD ENTITY SCRIPT!!!");
}
};
// entity scripts always need to return a newly constructed object of our type
return new Fireworks();
});

View file

@ -0,0 +1,46 @@
//
// fireworksLaunchButtonSpawner.js
// examples/playa/fireworks
//
// Created by Eric Levina on 2/24/16.
// Copyright 2015 High Fidelity, Inc.
//
// Run this script to spawn a big fireworks launch button that a user can press
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
var orientation = Camera.getOrientation();
orientation = Quat.safeEulerAngles(orientation);
orientation.x = 0;
orientation = Quat.fromVec3Degrees(orientation);
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation)));
// Math.random ensures no caching of script
var SCRIPT_URL = Script.resolvePath("fireworksLaunchButtonEntityScript.js");
var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Launch-Button.fbx";
var launchButton = Entities.addEntity({
type: "Model",
name: "launch pad",
modelURL: MODEL_URL,
position: center,
dimensions: {
x: 0.98,
y: 1.16,
z: 0.98
},
script: SCRIPT_URL,
userData: JSON.stringify({
launchPosition: {x: 1, y: 1.8, z: -20.9},
grabbableKey: {
wantsTrigger: true
}
})
})
function cleanup() {
Entities.deleteEntity(launchButton);
}
Script.scriptEnding.connect(cleanup);

View file

@ -6,3 +6,17 @@ setup_hifi_project(Network)
# link the shared hifi libraries
link_hifi_libraries(embedded-webserver networking shared)
package_libraries_for_deployment()
# find OpenSSL
find_package(OpenSSL REQUIRED)
if (APPLE AND ${OPENSSL_INCLUDE_DIR} STREQUAL "/usr/include")
# this is a user on OS X using system OpenSSL, which is going to throw warnings since they're deprecating for their common crypto
message(WARNING "The found version of OpenSSL is the OS X system version. This will produce deprecation warnings."
"\nWe recommend you install a newer version (at least 1.0.1h) in a different directory and set OPENSSL_ROOT_DIR in your env so Cmake can find it.")
endif ()
include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}")
# append OpenSSL to our list of libraries to link
target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES})

View file

@ -9,14 +9,21 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QTimer>
#include "IceServer.h"
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <QtCore/QJsonDocument>
#include <QtCore/QTimer>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
#include <LimitedNodeList.h>
#include <NetworkingConstants.h>
#include <udt/PacketHeaders.h>
#include <SharedUtil.h>
#include "IceServer.h"
const int CLEAR_INACTIVE_PEERS_INTERVAL_MSECS = 1 * 1000;
const int PEER_SILENCE_THRESHOLD_MSECS = 5 * 1000;
@ -45,7 +52,6 @@ IceServer::IceServer(int argc, char* argv[]) :
QTimer* inactivePeerTimer = new QTimer(this);
connect(inactivePeerTimer, &QTimer::timeout, this, &IceServer::clearInactivePeers);
inactivePeerTimer->start(CLEAR_INACTIVE_PEERS_INTERVAL_MSECS);
}
bool IceServer::packetVersionMatch(const udt::Packet& packet) {
@ -70,9 +76,14 @@ void IceServer::processPacket(std::unique_ptr<udt::Packet> packet) {
if (nlPacket->getType() == PacketType::ICEServerHeartbeat) {
SharedNetworkPeer peer = addOrUpdateHeartbeatingPeer(*nlPacket);
// so that we can send packets to the heartbeating peer when we need, we need to activate a socket now
peer->activateMatchingOrNewSymmetricSocket(nlPacket->getSenderSockAddr());
if (peer) {
// so that we can send packets to the heartbeating peer when we need, we need to activate a socket now
peer->activateMatchingOrNewSymmetricSocket(nlPacket->getSenderSockAddr());
} else {
// we couldn't verify this peer - respond back to them so they know they may need to perform keypair re-generation
static auto deniedPacket = NLPacket::create(PacketType::ICEServerHeartbeatDenied);
_serverSocket.writePacket(*deniedPacket, nlPacket->getSenderSockAddr());
}
} else if (nlPacket->getType() == PacketType::ICEServerQuery) {
QDataStream heartbeatStream(nlPacket.get());
@ -114,31 +125,135 @@ SharedNetworkPeer IceServer::addOrUpdateHeartbeatingPeer(NLPacket& packet) {
// pull the UUID, public and private sock addrs for this peer
QUuid senderUUID;
HifiSockAddr publicSocket, localSocket;
QByteArray signature;
QDataStream heartbeatStream(&packet);
heartbeatStream >> senderUUID;
heartbeatStream >> publicSocket >> localSocket;
heartbeatStream >> senderUUID >> publicSocket >> localSocket;
// make sure we have this sender in our peer hash
SharedNetworkPeer matchingPeer = _activePeers.value(senderUUID);
auto signedPlaintext = QByteArray::fromRawData(packet.getPayload(), heartbeatStream.device()->pos());
heartbeatStream >> signature;
if (!matchingPeer) {
// if we don't have this sender we need to create them now
matchingPeer = QSharedPointer<NetworkPeer>::create(senderUUID, publicSocket, localSocket);
_activePeers.insert(senderUUID, matchingPeer);
// make sure this is a verified heartbeat before performing any more processing
if (isVerifiedHeartbeat(senderUUID, signedPlaintext, signature)) {
// make sure we have this sender in our peer hash
SharedNetworkPeer matchingPeer = _activePeers.value(senderUUID);
qDebug() << "Added a new network peer" << *matchingPeer;
if (!matchingPeer) {
// if we don't have this sender we need to create them now
matchingPeer = QSharedPointer<NetworkPeer>::create(senderUUID, publicSocket, localSocket);
_activePeers.insert(senderUUID, matchingPeer);
qDebug() << "Added a new network peer" << *matchingPeer;
} else {
// we already had the peer so just potentially update their sockets
matchingPeer->setPublicSocket(publicSocket);
matchingPeer->setLocalSocket(localSocket);
}
// update our last heard microstamp for this network peer to now
matchingPeer->setLastHeardMicrostamp(usecTimestampNow());
return matchingPeer;
} else {
// we already had the peer so just potentially update their sockets
matchingPeer->setPublicSocket(publicSocket);
matchingPeer->setLocalSocket(localSocket);
// not verified, return the empty peer object
return SharedNetworkPeer();
}
}
bool IceServer::isVerifiedHeartbeat(const QUuid& domainID, const QByteArray& plaintext, const QByteArray& signature) {
// check if we have a private key for this domain ID - if we do not then fire off the request for it
auto it = _domainPublicKeys.find(domainID);
if (it != _domainPublicKeys.end()) {
// attempt to verify the signature for this heartbeat
const unsigned char* publicKeyData = reinterpret_cast<const unsigned char*>(it->second.constData());
// first load up the public key into an RSA struct
RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, it->second.size());
if (rsaPublicKey) {
auto hashedPlaintext = QCryptographicHash::hash(plaintext, QCryptographicHash::Sha256);
int verificationResult = RSA_verify(NID_sha256,
reinterpret_cast<const unsigned char*>(hashedPlaintext.constData()),
hashedPlaintext.size(),
reinterpret_cast<const unsigned char*>(signature.constData()),
signature.size(),
rsaPublicKey);
// free up the public key and remove connection token before we return
RSA_free(rsaPublicKey);
if (verificationResult == 1) {
// this is the only success case - we return true here to indicate that the heartbeat is verified
return true;
} else {
qDebug() << "Failed to verify heartbeat for" << domainID << "- re-requesting public key from API.";
}
} else {
// we can't let this user in since we couldn't convert their public key to an RSA key we could use
qWarning() << "Could not convert in-memory public key for" << domainID << "to usable RSA public key.";
qWarning() << "Re-requesting public key from API";
}
}
// update our last heard microstamp for this network peer to now
matchingPeer->setLastHeardMicrostamp(usecTimestampNow());
// we could not verify this heartbeat (missing public key, could not load public key, bad actor)
// ask the metaverse API for the right public key and return false to indicate that this is not verified
requestDomainPublicKey(domainID);
return matchingPeer;
return false;
}
void IceServer::requestDomainPublicKey(const QUuid& domainID) {
// send a request to the metaverse API for the public key for this domain
QNetworkAccessManager* manager = new QNetworkAccessManager { this };
connect(manager, &QNetworkAccessManager::finished, this, &IceServer::publicKeyReplyFinished);
QUrl publicKeyURL { NetworkingConstants::METAVERSE_SERVER_URL };
QString publicKeyPath = QString("/api/v1/domains/%1/public_key").arg(uuidStringWithoutCurlyBraces(domainID));
publicKeyURL.setPath(publicKeyPath);
QNetworkRequest publicKeyRequest { publicKeyURL };
publicKeyRequest.setAttribute(QNetworkRequest::User, domainID);
qDebug() << "Requesting public key for domain with ID" << domainID;
manager->get(publicKeyRequest);
}
void IceServer::publicKeyReplyFinished(QNetworkReply* reply) {
// get the domain ID from the QNetworkReply attribute
QUuid domainID = reply->request().attribute(QNetworkRequest::User).toUuid();
if (reply->error() == QNetworkReply::NoError) {
// pull out the public key and store it for this domain
// the response should be JSON
QJsonDocument responseDocument = QJsonDocument::fromJson(reply->readAll());
static const QString DATA_KEY = "data";
static const QString PUBLIC_KEY_KEY = "public_key";
static const QString STATUS_KEY = "status";
static const QString SUCCESS_VALUE = "success";
auto responseObject = responseDocument.object();
if (responseObject[STATUS_KEY].toString() == SUCCESS_VALUE) {
auto dataObject = responseObject[DATA_KEY].toObject();
if (dataObject.contains(PUBLIC_KEY_KEY)) {
_domainPublicKeys[domainID] = QByteArray::fromBase64(dataObject[PUBLIC_KEY_KEY].toString().toUtf8());
} else {
qWarning() << "There was no public key present in response for domain with ID" << domainID;
}
} else {
qWarning() << "The metaverse API did not return success for public key request for domain with ID" << domainID;
}
} else {
// there was a problem getting the public key for the domain
// log it since it will be re-requested on the next heartbeat
qWarning() << "Error retreiving public key for domain with ID" << domainID << "-" << reply->errorString();
}
}
void IceServer::sendPeerInformationPacket(const NetworkPeer& peer, const HifiSockAddr* destinationSockAddr) {

View file

@ -16,13 +16,15 @@
#include <QtCore/QSharedPointer>
#include <QUdpSocket>
#include <UUIDHasher.h>
#include <NetworkPeer.h>
#include <HTTPConnection.h>
#include <HTTPManager.h>
#include <NLPacket.h>
#include <udt/Socket.h>
typedef QHash<QUuid, SharedNetworkPeer> NetworkPeerHash;
class QNetworkReply;
class IceServer : public QCoreApplication, public HTTPRequestHandler {
Q_OBJECT
@ -31,6 +33,7 @@ public:
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false);
private slots:
void clearInactivePeers();
void publicKeyReplyFinished(QNetworkReply* reply);
private:
bool packetVersionMatch(const udt::Packet& packet);
void processPacket(std::unique_ptr<udt::Packet> packet);
@ -38,10 +41,19 @@ private:
SharedNetworkPeer addOrUpdateHeartbeatingPeer(NLPacket& incomingPacket);
void sendPeerInformationPacket(const NetworkPeer& peer, const HifiSockAddr* destinationSockAddr);
bool isVerifiedHeartbeat(const QUuid& domainID, const QByteArray& plaintext, const QByteArray& signature);
void requestDomainPublicKey(const QUuid& domainID);
QUuid _id;
udt::Socket _serverSocket;
using NetworkPeerHash = QHash<QUuid, SharedNetworkPeer>;
NetworkPeerHash _activePeers;
HTTPManager _httpManager;
using DomainPublicKeyHash = std::unordered_map<QUuid, QByteArray>;
DomainPublicKeyHash _domainPublicKeys;
};
#endif // hifi_IceServer_h

View file

@ -425,11 +425,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
_maxOctreePPS(maxOctreePacketsPerSecond.get()),
_lastFaceTrackerUpdate(0)
{
// FIXME this may be excessivly conservative. On the other hand
// FIXME this may be excessivly conservative. On the other hand
// maybe I'm used to having an 8-core machine
// Perhaps find the ideal thread count and subtract 2 or 3
// Perhaps find the ideal thread count and subtract 2 or 3
// (main thread, present thread, random OS load)
// More threads == faster concurrent loads, but also more concurrent
// More threads == faster concurrent loads, but also more concurrent
// load on the GPU until we can serialize GPU transfers (off the main thread)
QThreadPool::globalInstance()->setMaxThreadCount(2);
thread()->setPriority(QThread::HighPriority);
@ -553,14 +553,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails()));
connect(&domainHandler, &DomainHandler::settingsReceived, this, &Application::domainSettingsReceived);
// update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one
const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * 1000;
auto discoverabilityManager = DependencyManager::get<DiscoverabilityManager>();
connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation);
connect(&locationUpdateTimer, &QTimer::timeout,
connect(&locationUpdateTimer, &QTimer::timeout,
DependencyManager::get<AddressManager>().data(), &AddressManager::storeCurrentAddress);
locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS);
@ -590,6 +589,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(&accountManager, &AccountManager::usernameChanged, this, &Application::updateWindowTitle);
// set the account manager's root URL and trigger a login request if we don't have the access token
accountManager.setIsAgent(true);
accountManager.setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL);
UserActivityLogger::getInstance().launch(applicationVersion());
@ -604,7 +604,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle);
connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress);
// Save avatar location immediately after a teleport.
connect(getMyAvatar(), &MyAvatar::positionGoneTo,
DependencyManager::get<AddressManager>().data(), &AddressManager::storeCurrentAddress);
@ -625,7 +625,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
getEntities()->reloadEntityScripts();
}, Qt::QueuedConnection);
connect(scriptEngines, &ScriptEngines::scriptLoadError,
connect(scriptEngines, &ScriptEngines::scriptLoadError,
scriptEngines, [](const QString& filename, const QString& error){
OffscreenUi::warning(nullptr, "Error Loading Script", filename + " failed to load.");
}, Qt::QueuedConnection);
@ -905,9 +905,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
SpacemouseManager::getInstance().init();
#endif
auto& packetReceiver = nodeList->getPacketReceiver();
packetReceiver.registerListener(PacketType::DomainConnectionDenied, this, "handleDomainConnectionDeniedPacket");
// If the user clicks an an entity, we will check that it's an unlocked web entity, and if so, set the focus to it
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity,
@ -977,7 +974,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
disconnect(_idleTimer);
});
// Setting the interval to zero forces this to get called whenever there are no messages
// in the queue, which can be pretty damn frequent. Hence the idle function has a bunch
// in the queue, which can be pretty damn frequent. Hence the idle function has a bunch
// of logic to abort early if it's being called too often.
_idleTimer->start(0);
}
@ -1025,7 +1022,7 @@ void Application::cleanupBeforeQuit() {
getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts
DependencyManager::get<ScriptEngines>()->saveScripts();
DependencyManager::get<ScriptEngines>()->shutdownScripting(); // stop all currently running global scripts
DependencyManager::destroy<ScriptEngines>();
DependencyManager::destroy<ScriptEngines>();
// first stop all timers directly or by invokeMethod
// depending on what thread they run in
@ -1213,10 +1210,10 @@ void Application::initializeUi() {
setupPreferences();
// For some reason there is already an "Application" object in the QML context,
// For some reason there is already an "Application" object in the QML context,
// though I can't find it. Hence, "ApplicationInterface"
rootContext->setContextProperty("SnapshotUploader", new SnapshotUploader());
rootContext->setContextProperty("ApplicationInterface", this);
rootContext->setContextProperty("ApplicationInterface", this);
rootContext->setContextProperty("AnimationCache", DependencyManager::get<AnimationCache>().data());
rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance());
rootContext->setContextProperty("Controller", DependencyManager::get<controller::ScriptingInterface>().data());
@ -2448,7 +2445,6 @@ void Application::idle(uint64_t now) {
_overlayConductor.setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Overlays));
}
auto displayPlugin = getActiveDisplayPlugin();
// depending on whether we're throttling or not.
// Once rendering is off on another thread we should be able to have Application::idle run at start(0) in
@ -2472,7 +2468,7 @@ void Application::idle(uint64_t now) {
// Nested ifs are for clarity in the logic. Don't collapse them into a giant single if.
// Don't saturate the main thread with rendering, no paint calls until the last one is complete
if (!_pendingPaint) {
// Also no paint calls until the display plugin has increased by at least one frame
// Also no paint calls until the display plugin has increased by at least one frame
// (don't render at 90fps if the display plugin only goes at 60)
if (_renderedFrameIndex == INVALID_FRAME || presentCount > _renderedFrameIndex) {
// Record what present frame we're on
@ -2482,14 +2478,14 @@ void Application::idle(uint64_t now) {
// But when we DO request a paint, get to it as soon as possible: high priority
postEvent(this, new QEvent(static_cast<QEvent::Type>(Paint)), Qt::HighEventPriority);
}
}
// For the rest of idle, we want to cap at the max sim rate, so we might not call
}
// For the rest of idle, we want to cap at the max sim rate, so we might not call
// the remaining idle work every paint frame, or vice versa
// In theory this means we could call idle processing more often than painting,
// but in practice, when the paintGL calls aren't keeping up, there's no room left
// in the main thread to call idle more often than paint.
// This check is mostly to keep idle from burning up CPU cycles by running at
// This check is mostly to keep idle from burning up CPU cycles by running at
// hundreds of idles per second when the rendering is that fast
if ((timeSinceLastUpdateUs / USECS_PER_MSEC) < CAPPED_SIM_FRAME_PERIOD_MS) {
// No paint this round, but might be time for a new idle, otherwise return
@ -3445,7 +3441,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node
rootDetails.y * TREE_SCALE,
rootDetails.z * TREE_SCALE) - glm::vec3(HALF_TREE_SCALE),
rootDetails.s * TREE_SCALE);
ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds);
ViewFrustum::location serverFrustumLocation = _viewFrustum.computeCubeViewLocation(serverBounds);
if (serverFrustumLocation != ViewFrustum::OUTSIDE) {
inViewServers++;
@ -3513,7 +3509,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node
rootDetails.s * TREE_SCALE);
ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds);
ViewFrustum::location serverFrustumLocation = _viewFrustum.computeCubeViewLocation(serverBounds);
if (serverFrustumLocation != ViewFrustum::OUTSIDE) {
inView = true;
} else {
@ -3840,18 +3836,10 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se
});
}
// Setup the current Zone Entity lighting and skybox
// Setup the current Zone Entity lighting
{
// FIXME: Use a zone setting to determine the ambient light mode
DependencyManager::get<DeferredLightingEffect>()->setAmbientLightMode(-1);
auto skyStage = DependencyManager::get<SceneScriptingInterface>()->getSkyStage();
DependencyManager::get<DeferredLightingEffect>()->setGlobalLight(skyStage->getSunLight()->getDirection(), skyStage->getSunLight()->getColor(), skyStage->getSunLight()->getIntensity(), skyStage->getSunLight()->getAmbientIntensity());
auto skybox = model::SkyboxPointer();
if (skyStage->getBackgroundMode() == model::SunSkyStage::SKY_BOX) {
skybox = skyStage->getSkybox();
}
DependencyManager::get<DeferredLightingEffect>()->setGlobalSkybox(skybox);
auto stage = DependencyManager::get<SceneScriptingInterface>()->getSkyStage();
DependencyManager::get<DeferredLightingEffect>()->setGlobalLight(stage->getSunLight(), stage->getSkybox()->getCubemap());
}
{
@ -3987,30 +3975,10 @@ void Application::clearDomainOctreeDetails() {
void Application::domainChanged(const QString& domainHostname) {
updateWindowTitle();
clearDomainOctreeDetails();
_domainConnectionRefusals.clear();
// disable physics until we have enough information about our new location to not cause craziness.
_physicsEnabled = false;
}
void Application::handleDomainConnectionDeniedPacket(QSharedPointer<ReceivedMessage> message) {
// Read deny reason from packet
quint16 reasonSize;
message->readPrimitive(&reasonSize);
QString reason = QString::fromUtf8(message->readWithoutCopy(reasonSize));
// output to the log so the user knows they got a denied connection request
// and check and signal for an access token so that we can make sure they are logged in
qCDebug(interfaceapp) << "The domain-server denied a connection request: " << reason;
qCDebug(interfaceapp) << "You may need to re-log to generate a keypair so you can provide a username signature.";
if (!_domainConnectionRefusals.contains(reason)) {
_domainConnectionRefusals.append(reason);
emit domainConnectionRefused(reason);
}
AccountManager::getInstance().checkAndSignalForAccessToken();
}
void Application::connectedToDomain(const QString& hostname) {
AccountManager& accountManager = AccountManager::getInstance();
const QUuid& domainID = DependencyManager::get<NodeList>()->getDomainHandler().getUUID();
@ -4554,33 +4522,6 @@ void Application::openUrl(const QUrl& url) {
}
}
void Application::domainSettingsReceived(const QJsonObject& domainSettingsObject) {
// from the domain-handler, figure out the satoshi cost per voxel and per meter cubed
const QString VOXEL_SETTINGS_KEY = "voxels";
const QString PER_VOXEL_COST_KEY = "per-voxel-credits";
const QString PER_METER_CUBED_COST_KEY = "per-meter-cubed-credits";
const QString VOXEL_WALLET_UUID = "voxel-wallet";
const QJsonObject& voxelObject = domainSettingsObject[VOXEL_SETTINGS_KEY].toObject();
qint64 satoshisPerVoxel = 0;
qint64 satoshisPerMeterCubed = 0;
QUuid voxelWalletUUID;
if (!domainSettingsObject.isEmpty()) {
float perVoxelCredits = (float) voxelObject[PER_VOXEL_COST_KEY].toDouble();
float perMeterCubedCredits = (float) voxelObject[PER_METER_CUBED_COST_KEY].toDouble();
satoshisPerVoxel = (qint64) floorf(perVoxelCredits * SATOSHIS_PER_CREDIT);
satoshisPerMeterCubed = (qint64) floorf(perMeterCubedCredits * SATOSHIS_PER_CREDIT);
voxelWalletUUID = QUuid(voxelObject[VOXEL_WALLET_UUID].toString());
}
qCDebug(interfaceapp) << "Octree edits costs are" << satoshisPerVoxel << "per octree cell and" << satoshisPerMeterCubed << "per meter cubed";
qCDebug(interfaceapp) << "Destination wallet UUID for edit payments is" << voxelWalletUUID;
}
void Application::loadDialog() {
auto scriptEngines = DependencyManager::get<ScriptEngines>();
QString fileNameString = OffscreenUi::getOpenFileName(
@ -4782,7 +4723,7 @@ static void addDisplayPluginToMenu(DisplayPluginPointer displayPlugin, bool acti
groupingMenu = "Developer";
break;
default:
groupingMenu = "Standard";
groupingMenu = "Standard";
break;
}

View file

@ -225,7 +225,6 @@ signals:
void svoImportRequested(const QString& url);
void checkBackgroundDownloads();
void domainConnectionRefused(const QString& reason);
void fullAvatarURLChanged(const QString& newValue, const QString& modelName);
@ -295,9 +294,6 @@ private slots:
void activeChanged(Qt::ApplicationState state);
void domainSettingsReceived(const QJsonObject& domainSettingsObject);
void handleDomainConnectionDeniedPacket(QSharedPointer<ReceivedMessage> message);
void notifyPacketVersionMismatch();
void loadSettings();
@ -476,7 +472,6 @@ private:
typedef bool (Application::* AcceptURLMethod)(const QString &);
static const QHash<QString, AcceptURLMethod> _acceptedExtensions;
QList<QString> _domainConnectionRefusals;
glm::uvec2 _renderResolution;
int _maxOctreePPS = DEFAULT_MAX_OCTREE_PPS;

View file

@ -22,11 +22,8 @@
#include <QStandardPaths>
#include <QVBoxLayout>
#include "DataServerAccountInfo.h"
#include "Menu.h"
Q_DECLARE_METATYPE(DataServerAccountInfo)
static const QString RUNNING_MARKER_FILENAME = "Interface.running";
void CrashHandler::checkForAndHandleCrash() {
@ -57,7 +54,7 @@ CrashHandler::Action CrashHandler::promptUserForAction() {
layout->addWidget(label);
QRadioButton* option1 = new QRadioButton("Reset all my settings");
QRadioButton* option2 = new QRadioButton("Reset my settings but retain login and avatar info.");
QRadioButton* option2 = new QRadioButton("Reset my settings but retain avatar info.");
QRadioButton* option3 = new QRadioButton("Continue with my current settings");
option3->setChecked(true);
layout->addWidget(option1);
@ -79,7 +76,7 @@ CrashHandler::Action CrashHandler::promptUserForAction() {
return CrashHandler::DELETE_INTERFACE_INI;
}
if (option2->isChecked()) {
return CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO;
return CrashHandler::RETAIN_AVATAR_INFO;
}
}
@ -88,7 +85,7 @@ CrashHandler::Action CrashHandler::promptUserForAction() {
}
void CrashHandler::handleCrash(CrashHandler::Action action) {
if (action != CrashHandler::DELETE_INTERFACE_INI && action != CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) {
if (action != CrashHandler::DELETE_INTERFACE_INI && action != CrashHandler::RETAIN_AVATAR_INFO) {
// CrashHandler::DO_NOTHING or unexpected value
return;
}
@ -101,18 +98,13 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
const QString DISPLAY_NAME_KEY = "displayName";
const QString FULL_AVATAR_URL_KEY = "fullAvatarURL";
const QString FULL_AVATAR_MODEL_NAME_KEY = "fullAvatarModelName";
const QString ACCOUNTS_GROUP = "accounts";
QString displayName;
QUrl fullAvatarURL;
QString fullAvatarModelName;
QUrl address;
QMap<QString, DataServerAccountInfo> accounts;
if (action == CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) {
// Read login and avatar info
qRegisterMetaType<DataServerAccountInfo>("DataServerAccountInfo");
qRegisterMetaTypeStreamOperators<DataServerAccountInfo>("DataServerAccountInfo");
if (action == CrashHandler::RETAIN_AVATAR_INFO) {
// Read avatar info
// Location and orientation
settings.beginGroup(ADDRESS_MANAGER_GROUP);
@ -125,13 +117,6 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
fullAvatarURL = settings.value(FULL_AVATAR_URL_KEY).toUrl();
fullAvatarModelName = settings.value(FULL_AVATAR_MODEL_NAME_KEY).toString();
settings.endGroup();
// Accounts
settings.beginGroup(ACCOUNTS_GROUP);
foreach(const QString& key, settings.allKeys()) {
accounts.insert(key, settings.value(key).value<DataServerAccountInfo>());
}
settings.endGroup();
}
// Delete Interface.ini
@ -140,8 +125,8 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
settingsFile.remove();
}
if (action == CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) {
// Write login and avatar info
if (action == CrashHandler::RETAIN_AVATAR_INFO) {
// Write avatar info
// Location and orientation
settings.beginGroup(ADDRESS_MANAGER_GROUP);
@ -154,13 +139,6 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
settings.setValue(FULL_AVATAR_URL_KEY, fullAvatarURL);
settings.setValue(FULL_AVATAR_MODEL_NAME_KEY, fullAvatarModelName);
settings.endGroup();
// Accounts
settings.beginGroup(ACCOUNTS_GROUP);
foreach(const QString& key, accounts.keys()) {
settings.setValue(key, QVariant::fromValue(accounts.value(key)));
}
settings.endGroup();
}
}

View file

@ -25,7 +25,7 @@ public:
private:
enum Action {
DELETE_INTERFACE_INI,
RETAIN_LOGIN_AND_AVATAR_INFO,
RETAIN_AVATAR_INFO,
DO_NOTHING
};

View file

@ -70,17 +70,9 @@ Menu::Menu() {
dialogsManager.data(), &DialogsManager::toggleLoginDialog);
}
// File > Update -- FIXME: needs implementation
auto action = addActionToQMenuAndActionHash(fileMenu, "Update");
action->setDisabled(true);
// File > Help
addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp()));
// File > Crash Reporter...-- FIXME: needs implementation
auto crashReporterAction = addActionToQMenuAndActionHash(fileMenu, "Crash Reporter...");
crashReporterAction->setDisabled(true);
// File > About
addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole);
@ -167,7 +159,7 @@ Menu::Menu() {
QObject* avatar = avatarManager->getMyAvatar();
// Avatar > Attachments...
action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments);
auto action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments);
connect(action, &QAction::triggered, [] {
DependencyManager::get<OffscreenUi>()->show(QString("hifi/dialogs/AttachmentsDialog.qml"), "AttachmentsDialog");
});
@ -262,16 +254,10 @@ Menu::Menu() {
// Navigate menu ----------------------------------
MenuWrapper* navigateMenu = addMenu("Navigate");
// Navigate > Home -- FIXME: needs implementation
auto homeAction = addActionToQMenuAndActionHash(navigateMenu, "Home");
homeAction->setDisabled(true);
// Navigate > Show Address Bar
addActionToQMenuAndActionHash(navigateMenu, MenuOption::AddressBar, Qt::CTRL | Qt::Key_L,
dialogsManager.data(), SLOT(toggleAddressBar()));
// Navigate > Directory -- FIXME: needs implementation
addActionToQMenuAndActionHash(navigateMenu, "Directory");
// Navigate > Bookmark related menus -- Note: the Bookmark class adds its own submenus here.
qApp->getBookmarks()->setupMenus(this, navigateMenu);
@ -302,20 +288,19 @@ Menu::Menu() {
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), "GeneralPreferencesDialog");
});
// Settings > Avatar...-- FIXME: needs implementation
// Settings > Avatar...
action = addActionToQMenuAndActionHash(settingsMenu, "Avatar...");
connect(action, &QAction::triggered, [] {
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), "AvatarPreferencesDialog");
});
// Settings > Audio...-- FIXME: needs implementation
// Settings > Audio...
action = addActionToQMenuAndActionHash(settingsMenu, "Audio...");
connect(action, &QAction::triggered, [] {
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/AudioPreferencesDialog.qml"), "AudioPreferencesDialog");
});
// Settings > LOD...-- FIXME: needs implementation
// Settings > LOD...
action = addActionToQMenuAndActionHash(settingsMenu, "LOD...");
connect(action, &QAction::triggered, [] {
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/LodPreferencesDialog.qml"), "LodPreferencesDialog");

View file

@ -136,7 +136,7 @@ glm::quat Avatar::getWorldAlignedOrientation () const {
AABox Avatar::getBounds() const {
// Our skeleton models are rigged, and this method call safely produces the static bounds of the model.
// Except, that getPartBounds produces an infinite, uncentered bounding box when the model is not yet parsed,
// Except, that getPartBounds produces an infinite, uncentered bounding box when the model is not yet parsed,
// and we want a centered one. NOTE: There is code that may never try to render, and thus never load and get the
// real model bounds, if this is unrealistically small.
if (!_skeletonModel.isRenderable()) {
@ -188,7 +188,7 @@ void Avatar::simulate(float deltaTime) {
// simple frustum check
float boundingRadius = getBoundingRadius();
bool inViewFrustum = qApp->getViewFrustum()->sphereInFrustum(getPosition(), boundingRadius) !=
bool inViewFrustum = qApp->getViewFrustum()->computeSphereViewLocation(getPosition(), boundingRadius) !=
ViewFrustum::OUTSIDE;
{
@ -401,7 +401,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) {
frustum = qApp->getDisplayViewFrustum();
}
if (frustum->sphereInFrustum(getPosition(), boundingRadius) == ViewFrustum::OUTSIDE) {
if (frustum->computeSphereViewLocation(getPosition(), boundingRadius) == ViewFrustum::OUTSIDE) {
endRender();
return;
}
@ -430,6 +430,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) {
if (renderArgs->_renderMode != RenderArgs::SHADOW_RENDER_MODE) {
// add local lights
const float BASE_LIGHT_DISTANCE = 2.0f;
const float LIGHT_FALLOFF_RADIUS = 0.01f;
const float LIGHT_EXPONENT = 1.0f;
const float LIGHT_CUTOFF = glm::radians(80.0f);
float distance = BASE_LIGHT_DISTANCE * getUniformScale();
@ -438,7 +439,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) {
foreach (const AvatarManager::LocalLight& light, DependencyManager::get<AvatarManager>()->getLocalLights()) {
glm::vec3 direction = orientation * light.direction;
DependencyManager::get<DeferredLightingEffect>()->addSpotLight(position - direction * distance,
distance * 2.0f, light.color, 0.5f, orientation, LIGHT_EXPONENT, LIGHT_CUTOFF);
distance * 2.0f, light.color, 0.5f, LIGHT_FALLOFF_RADIUS, orientation, LIGHT_EXPONENT, LIGHT_CUTOFF);
}
}
@ -516,7 +517,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) {
auto& frustum = *renderArgs->_viewFrustum;
auto textPosition = getDisplayNamePosition();
if (frustum.pointInFrustum(textPosition, true) == ViewFrustum::INSIDE) {
if (frustum.computePointFrustumLocation(textPosition) == ViewFrustum::INSIDE) {
renderDisplayName(batch, frustum, textPosition);
}
}
@ -670,7 +671,7 @@ glm::vec3 Avatar::getDisplayNamePosition() const {
}
Transform Avatar::calculateDisplayNameTransform(const ViewFrustum& frustum, const glm::vec3& textPosition) const {
Q_ASSERT_X(frustum.pointInFrustum(textPosition, true) == ViewFrustum::INSIDE,
Q_ASSERT_X(frustum.computePointFrustumLocation(textPosition) == ViewFrustum::INSIDE,
"Avatar::calculateDisplayNameTransform", "Text not in viewfrustum.");
glm::vec3 toFrustum = frustum.getPosition() - textPosition;

View file

@ -656,7 +656,7 @@ void MyAvatar::saveData() {
settings.setValue("displayName", _displayName);
settings.setValue("collisionSoundURL", _collisionSoundURL);
settings.setValue("snapTurn", _useSnapTurn);
settings.setValue("useSnapTurn", _useSnapTurn);
settings.endGroup();
}
@ -750,7 +750,7 @@ void MyAvatar::loadData() {
setDisplayName(settings.value("displayName").toString());
setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString());
setSnapTurn(settings.value("snapTurn").toBool());
setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool());
settings.endGroup();

View file

@ -25,7 +25,7 @@
WindowScriptingInterface::WindowScriptingInterface() {
const DomainHandler& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
connect(&domainHandler, &DomainHandler::connectedToDomain, this, &WindowScriptingInterface::domainChanged);
connect(qApp, &Application::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused);
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused);
connect(qApp, &Application::svoImportRequested, [this](const QString& urlString) {
static const QMetaMethod svoImportRequestedSignal =

View file

@ -134,13 +134,14 @@ void EntityTreeRenderer::update() {
EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_tree);
tree->update();
// check to see if the avatar has moved and if we need to handle enter/leave entity logic
checkEnterLeaveEntities();
// Handle enter/leave entity logic
bool updated = checkEnterLeaveEntities();
// even if we haven't changed positions, if we previously attempted to set the skybox, but
// have a pending download of the skybox texture, then we should attempt to reapply to
// get the correct texture.
if (_pendingSkyboxTexture && _skyboxTexture && _skyboxTexture->isLoaded()) {
// If we haven't already updated and previously attempted to load a texture,
// check if the texture loaded and apply it
if (!updated && (
(_pendingSkyboxTexture && _skyboxTexture && _skyboxTexture->isLoaded()) ||
(_pendingAmbientTexture && _ambientTexture && _ambientTexture->isLoaded()))) {
applyZonePropertiesToScene(_bestZone);
}
@ -156,7 +157,7 @@ void EntityTreeRenderer::update() {
deleteReleasedModels();
}
void EntityTreeRenderer::checkEnterLeaveEntities() {
bool EntityTreeRenderer::checkEnterLeaveEntities() {
if (_tree && !_shuttingDown) {
glm::vec3 avatarPosition = _viewState->getAvatarPosition();
@ -171,7 +172,7 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
std::static_pointer_cast<EntityTree>(_tree)->findEntities(avatarPosition, radius, foundEntities);
// Whenever you're in an intersection between zones, we will always choose the smallest zone.
_bestZone = NULL; // NOTE: Is this what we want?
_bestZone = nullptr; // NOTE: Is this what we want?
_bestZoneVolume = std::numeric_limits<float>::max();
// create a list of entities that actually contain the avatar's position
@ -204,7 +205,6 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
}
applyZonePropertiesToScene(_bestZone);
});
// Note: at this point we don't need to worry about the tree being locked, because we only deal with
@ -228,8 +228,11 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
}
_currentEntitiesInside = entitiesContainingAvatar;
_lastAvatarPosition = avatarPosition;
return true;
}
}
return false;
}
void EntityTreeRenderer::leaveAllEntities() {
@ -253,6 +256,7 @@ void EntityTreeRenderer::forceRecheckEntities() {
void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityItem> zone) {
auto textureCache = DependencyManager::get<TextureCache>();
auto scene = DependencyManager::get<SceneScriptingInterface>();
auto sceneStage = scene->getStage();
auto skyStage = scene->getSkyStage();
@ -264,7 +268,11 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
_pendingSkyboxTexture = false;
_skyboxTexture.clear();
_pendingAmbientTexture = false;
_ambientTexture.clear();
if (_hasPreviousZone) {
sceneKeyLight->resetAmbientSphere();
sceneKeyLight->setColor(_previousKeyLightColor);
sceneKeyLight->setIntensity(_previousKeyLightIntensity);
sceneKeyLight->setAmbientIntensity(_previousKeyLightAmbientIntensity);
@ -274,6 +282,7 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
_previousStageAltitude);
sceneTime->setHour(_previousStageHour);
sceneTime->setDay(_previousStageDay);
_hasPreviousZone = false;
}
@ -306,6 +315,23 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
sceneTime->setHour(zone->getStageProperties().calculateHour());
sceneTime->setDay(zone->getStageProperties().calculateDay());
bool isAmbientTextureSet = false;
if (zone->getKeyLightProperties().getAmbientURL().isEmpty()) {
_pendingAmbientTexture = false;
_ambientTexture.clear();
} else {
_ambientTexture = textureCache->getTexture(zone->getKeyLightProperties().getAmbientURL(), CUBE_TEXTURE);
if (_ambientTexture->getGPUTexture()) {
_pendingAmbientTexture = false;
if (_ambientTexture->getGPUTexture()->getIrradiance()) {
sceneKeyLight->setAmbientSphere(_ambientTexture->getGPUTexture()->getIrradiance());
isAmbientTextureSet = true;
}
} else {
_pendingAmbientTexture = true;
}
}
switch (zone->getBackgroundMode()) {
case BACKGROUND_MODE_SKYBOX: {
auto skybox = std::dynamic_pointer_cast<ProceduralSkybox>(skyStage->getSkybox());
@ -326,12 +352,16 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
_skyboxTexture.clear();
} else {
// Update the Texture of the Skybox with the one pointed by this zone
auto textureCache = DependencyManager::get<TextureCache>();
_skyboxTexture = textureCache->getTexture(zone->getSkyboxProperties().getURL(), CUBE_TEXTURE);
if (_skyboxTexture->getGPUTexture()) {
skybox->setCubemap(_skyboxTexture->getGPUTexture());
auto texture = _skyboxTexture->getGPUTexture();
skybox->setCubemap(texture);
_pendingSkyboxTexture = false;
if (!isAmbientTextureSet && texture->getIrradiance()) {
sceneKeyLight->setAmbientSphere(texture->getIrradiance());
isAmbientTextureSet = true;
}
} else {
_pendingSkyboxTexture = true;
}
@ -348,6 +378,10 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
_skyboxTexture.clear();
break;
}
if (!isAmbientTextureSet) {
sceneKeyLight->resetAmbientSphere();
}
}
const FBXGeometry* EntityTreeRenderer::getGeometryForEntity(EntityItemPointer entityItem) {
@ -819,3 +853,19 @@ void EntityTreeRenderer::updateEntityRenderStatus(bool shouldRenderEntities) {
}
}
}
void EntityTreeRenderer::updateZone(const EntityItemID& id) {
if (!_bestZone) {
// Get in the zone!
auto zone = getTree()->findEntityByEntityItemID(id);
if (zone && zone->contains(_lastAvatarPosition)) {
_currentEntitiesInside << id;
emit enterEntity(id);
_entitiesScriptEngine->callEntityScriptMethod(id, "enterEntity");
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(zone);
}
}
if (_bestZone && _bestZone->getID() == id) {
applyZonePropertiesToScene(_bestZone);
}
}

View file

@ -109,6 +109,7 @@ public slots:
void entitySciptChanging(const EntityItemID& entityID, const bool reload);
void entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision);
void updateEntityRenderStatus(bool shouldRenderEntities);
void updateZone(const EntityItemID& id);
// optional slots that can be wired to menu items
void setDisplayModelBounds(bool value) { _displayModelBounds = value; }
@ -136,16 +137,19 @@ private:
EntityItemID _currentClickingOnEntityID;
QScriptValueList createEntityArgs(const EntityItemID& entityID);
void checkEnterLeaveEntities();
bool checkEnterLeaveEntities();
void leaveAllEntities();
void forceRecheckEntities();
glm::vec3 _lastAvatarPosition;
glm::vec3 _lastAvatarPosition { 0.0f };
QVector<EntityItemID> _currentEntitiesInside;
bool _pendingSkyboxTexture { false };
NetworkTexturePointer _skyboxTexture;
bool _pendingAmbientTexture { false };
NetworkTexturePointer _ambientTexture;
bool _wantScripts;
ScriptEngine* _entitiesScriptEngine;

View file

@ -36,15 +36,16 @@ void RenderableLightEntityItem::render(RenderArgs* args) {
glm::vec3 color = toGlm(getXColor());
float intensity = getIntensity();
float falloffRadius = getFalloffRadius();
float exponent = getExponent();
float cutoff = glm::radians(getCutoff());
if (_isSpotlight) {
DependencyManager::get<DeferredLightingEffect>()->addSpotLight(position, largestDiameter / 2.0f,
color, intensity, rotation, exponent, cutoff);
color, intensity, falloffRadius, rotation, exponent, cutoff);
} else {
DependencyManager::get<DeferredLightingEffect>()->addPointLight(position, largestDiameter / 2.0f,
color, intensity);
color, intensity, falloffRadius);
}
#ifdef WANT_DEBUG

View file

@ -18,6 +18,7 @@
#include <GeometryCache.h>
#include <PerfStat.h>
#include "EntityTreeRenderer.h"
#include "RenderableEntityItem.h"
// Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1
@ -62,6 +63,10 @@ bool RenderableZoneEntityItem::setProperties(const EntityItemProperties& propert
return somethingChanged;
}
void RenderableZoneEntityItem::somethingChangedNotification() {
DependencyManager::get<EntityTreeRenderer>()->updateZone(_id);
}
int RenderableZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead,
ReadBitstreamToTreeParams& args,
EntityPropertyFlags& propertyFlags, bool overwriteLocalData,

View file

@ -28,6 +28,8 @@ public:
{ }
virtual bool setProperties(const EntityItemProperties& properties);
virtual void somethingChangedNotification() override;
virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead,
ReadBitstreamToTreeParams& args,
EntityPropertyFlags& propertyFlags, bool overwriteLocalData,

View file

@ -245,6 +245,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
CHECK_PROPERTY_CHANGE(PROP_DYNAMIC, dynamic);
CHECK_PROPERTY_CHANGE(PROP_IS_SPOTLIGHT, isSpotlight);
CHECK_PROPERTY_CHANGE(PROP_INTENSITY, intensity);
CHECK_PROPERTY_CHANGE(PROP_FALLOFF_RADIUS, falloffRadius);
CHECK_PROPERTY_CHANGE(PROP_EXPONENT, exponent);
CHECK_PROPERTY_CHANGE(PROP_CUTOFF, cutoff);
CHECK_PROPERTY_CHANGE(PROP_LOCKED, locked);
@ -445,6 +446,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
if (_type == EntityTypes::Light) {
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IS_SPOTLIGHT, isSpotlight);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_INTENSITY, intensity);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FALLOFF_RADIUS, falloffRadius);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EXPONENT, exponent);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CUTOFF, cutoff);
}
@ -597,6 +599,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
COPY_PROPERTY_FROM_QSCRIPTVALUE(dynamic, bool, setDynamic);
COPY_PROPERTY_FROM_QSCRIPTVALUE(isSpotlight, bool, setIsSpotlight);
COPY_PROPERTY_FROM_QSCRIPTVALUE(intensity, float, setIntensity);
COPY_PROPERTY_FROM_QSCRIPTVALUE(falloffRadius, float, setFalloffRadius);
COPY_PROPERTY_FROM_QSCRIPTVALUE(exponent, float, setExponent);
COPY_PROPERTY_FROM_QSCRIPTVALUE(cutoff, float, setCutoff);
COPY_PROPERTY_FROM_QSCRIPTVALUE(locked, bool, setLocked);
@ -762,6 +765,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue
ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, dynamic, unused);
ADD_PROPERTY_TO_MAP(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool);
ADD_PROPERTY_TO_MAP(PROP_INTENSITY, Intensity, intensity, float);
ADD_PROPERTY_TO_MAP(PROP_FALLOFF_RADIUS, FalloffRadius, falloffRadius, float);
ADD_PROPERTY_TO_MAP(PROP_EXPONENT, Exponent, exponent, float);
ADD_PROPERTY_TO_MAP(PROP_CUTOFF, Cutoff, cutoff, float);
ADD_PROPERTY_TO_MAP(PROP_LOCKED, Locked, locked, bool);
@ -1043,6 +1047,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem
APPEND_ENTITY_PROPERTY(PROP_IS_SPOTLIGHT, properties.getIsSpotlight());
APPEND_ENTITY_PROPERTY(PROP_COLOR, properties.getColor());
APPEND_ENTITY_PROPERTY(PROP_INTENSITY, properties.getIntensity());
APPEND_ENTITY_PROPERTY(PROP_FALLOFF_RADIUS, properties.getFalloffRadius());
APPEND_ENTITY_PROPERTY(PROP_EXPONENT, properties.getExponent());
APPEND_ENTITY_PROPERTY(PROP_CUTOFF, properties.getCutoff());
}
@ -1332,6 +1337,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_IS_SPOTLIGHT, bool, setIsSpotlight);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLOR, xColor, setColor);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_INTENSITY, float, setIntensity);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FALLOFF_RADIUS, float, setFalloffRadius);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_EXPONENT, float, setExponent);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CUTOFF, float, setCutoff);
}
@ -1477,6 +1483,7 @@ void EntityItemProperties::markAllChanged() {
_dynamicChanged = true;
_intensityChanged = true;
_falloffRadiusChanged = true;
_exponentChanged = true;
_cutoffChanged = true;
_lockedChanged = true;
@ -1719,6 +1726,9 @@ QList<QString> EntityItemProperties::listChangedProperties() {
if (intensityChanged()) {
out += "intensity";
}
if (falloffRadiusChanged()) {
out += "falloffRadius";
}
if (exponentChanged()) {
out += "exponent";
}

View file

@ -35,6 +35,7 @@
#include "EntityItemPropertiesMacros.h"
#include "EntityTypes.h"
#include "EntityPropertyFlags.h"
#include "LightEntityItem.h"
#include "LineEntityItem.h"
#include "ParticleEffectEntityItem.h"
#include "PolyVoxEntityItem.h"
@ -129,10 +130,11 @@ public:
DEFINE_PROPERTY(PROP_COLLISIONLESS, Collisionless, collisionless, bool, ENTITY_ITEM_DEFAULT_COLLISIONLESS);
DEFINE_PROPERTY(PROP_COLLISION_MASK, CollisionMask, collisionMask, uint8_t, ENTITY_COLLISION_MASK_DEFAULT);
DEFINE_PROPERTY(PROP_DYNAMIC, Dynamic, dynamic, bool, ENTITY_ITEM_DEFAULT_DYNAMIC);
DEFINE_PROPERTY(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool, false);
DEFINE_PROPERTY(PROP_INTENSITY, Intensity, intensity, float, 1.0f);
DEFINE_PROPERTY(PROP_EXPONENT, Exponent, exponent, float, 0.0f);
DEFINE_PROPERTY(PROP_CUTOFF, Cutoff, cutoff, float, ENTITY_ITEM_DEFAULT_CUTOFF);
DEFINE_PROPERTY(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool, LightEntityItem::DEFAULT_IS_SPOTLIGHT);
DEFINE_PROPERTY(PROP_INTENSITY, Intensity, intensity, float, LightEntityItem::DEFAULT_INTENSITY);
DEFINE_PROPERTY(PROP_FALLOFF_RADIUS, FalloffRadius, falloffRadius, float, LightEntityItem::DEFAULT_FALLOFF_RADIUS);
DEFINE_PROPERTY(PROP_EXPONENT, Exponent, exponent, float, LightEntityItem::DEFAULT_EXPONENT);
DEFINE_PROPERTY(PROP_CUTOFF, Cutoff, cutoff, float, LightEntityItem::DEFAULT_CUTOFF);
DEFINE_PROPERTY(PROP_LOCKED, Locked, locked, bool, ENTITY_ITEM_DEFAULT_LOCKED);
DEFINE_PROPERTY_REF(PROP_TEXTURES, Textures, textures, QString, "");
DEFINE_PROPERTY_REF(PROP_USER_DATA, UserData, userData, QString, ENTITY_ITEM_DEFAULT_USER_DATA);
@ -359,6 +361,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) {
DEBUG_PROPERTY_IF_CHANGED(debug, properties, Dynamic, dynamic, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, IsSpotlight, isSpotlight, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, Intensity, intensity, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, FalloffRadius, falloffRadius, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, Exponent, exponent, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, Cutoff, cutoff, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, Locked, locked, "");

View file

@ -72,8 +72,6 @@ const bool ENTITY_ITEM_DEFAULT_COLLISIONLESS = false;
const bool ENTITY_ITEM_DEFAULT_DYNAMIC = false;
const bool ENTITY_ITEM_DEFAULT_BILLBOARDED = false;
const float ENTITY_ITEM_DEFAULT_CUTOFF = PI / 2;
const QString ENTITY_ITEM_DEFAULT_NAME = QString("");
#endif // hifi_EntityItemPropertiesDefaults_h

View file

@ -167,6 +167,8 @@ enum EntityPropertyList {
PROP_COLLISION_MASK, // one byte of collision group flags
PROP_FALLOFF_RADIUS, // for Light entity
////////////////////////////////////////////////////////////////////////////////////////////////////
// ATTENTION: add new properties to end of list just ABOVE this line
PROP_AFTER_LAST_ITEM,

View file

@ -85,7 +85,7 @@ void EntityTreeElement::initializeExtraEncodeData(EncodeBitstreamParams& params)
forEachEntity([&](EntityItemPointer entity) {
entityTreeElementExtraEncodeData->entities.insert(entity->getEntityItemID(), entity->getEntityProperties(params));
});
// TODO: some of these inserts might be redundant!!!
extraEncodeData->insert(this, entityTreeElementExtraEncodeData);
}
@ -96,39 +96,39 @@ bool EntityTreeElement::shouldIncludeChildData(int childIndex, EncodeBitstreamPa
assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes
if (extraEncodeData->contains(this)) {
EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData
EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData
= static_cast<EntityTreeElementExtraEncodeData*>(extraEncodeData->value(this));
bool childCompleted = entityTreeElementExtraEncodeData->childCompleted[childIndex];
// If we haven't completely sent the child yet, then we should include it
return !childCompleted;
}
// I'm not sure this should ever happen, since we should have the extra encode data if we're considering
// the child data for this element
assert(false);
return false;
}
bool EntityTreeElement::shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const {
bool EntityTreeElement::shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const {
EntityTreeElementPointer childElement = getChildAtIndex(childIndex);
if (childElement->alreadyFullyEncoded(params)) {
return false;
}
return true; // if we don't know otherwise than recurse!
}
bool EntityTreeElement::alreadyFullyEncoded(EncodeBitstreamParams& params) const {
bool EntityTreeElement::alreadyFullyEncoded(EncodeBitstreamParams& params) const {
OctreeElementExtraEncodeData* extraEncodeData = params.extraEncodeData;
assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes
if (extraEncodeData->contains(this)) {
EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData
EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData
= static_cast<EntityTreeElementExtraEncodeData*>(extraEncodeData->value(this));
// If we know that ALL subtrees below us have already been recursed, then we don't
// If we know that ALL subtrees below us have already been recursed, then we don't
// need to recurse this child.
return entityTreeElementExtraEncodeData->subtreeCompleted;
}
@ -139,7 +139,7 @@ void EntityTreeElement::updateEncodedData(int childIndex, AppendState childAppen
OctreeElementExtraEncodeData* extraEncodeData = params.extraEncodeData;
assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes
if (extraEncodeData->contains(this)) {
EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData
EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData
= static_cast<EntityTreeElementExtraEncodeData*>(extraEncodeData->value(this));
if (childAppendState == OctreeElement::COMPLETED) {
@ -155,7 +155,7 @@ void EntityTreeElement::updateEncodedData(int childIndex, AppendState childAppen
void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) const {
const bool wantDebug = false;
if (wantDebug) {
qCDebug(entities) << "EntityTreeElement::elementEncodeComplete() element:" << _cube;
}
@ -188,7 +188,7 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con
// If we've encoding this element before... but we're coming back a second time in an attempt to
// encoud our parent... this might happen.
if (extraEncodeData->contains(childElement.get())) {
EntityTreeElementExtraEncodeData* childExtraEncodeData
EntityTreeElementExtraEncodeData* childExtraEncodeData
= static_cast<EntityTreeElementExtraEncodeData*>(extraEncodeData->value(childElement.get()));
if (wantDebug) {
@ -197,7 +197,7 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con
qCDebug(entities) << " childExtraEncodeData->elementCompleted:" << childExtraEncodeData->elementCompleted;
qCDebug(entities) << " childExtraEncodeData->subtreeCompleted:" << childExtraEncodeData->subtreeCompleted;
}
if (childElement->isLeaf() && childExtraEncodeData->elementCompleted) {
if (wantDebug) {
qCDebug(entities) << " CHILD IS LEAF -- AND CHILD ELEMENT DATA COMPLETED!!!";
@ -217,24 +217,24 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con
qCDebug(entities) << " WAS elementCompleted:" << thisExtraEncodeData->elementCompleted;
qCDebug(entities) << " WAS subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted;
}
thisExtraEncodeData->subtreeCompleted = !someChildTreeNotComplete;
if (wantDebug) {
qCDebug(entities) << " NOW elementCompleted:" << thisExtraEncodeData->elementCompleted;
qCDebug(entities) << " NOW subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted;
if (thisExtraEncodeData->subtreeCompleted) {
qCDebug(entities) << " YEAH!!!!! >>>>>>>>>>>>>> NOW subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted;
}
}
}
OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData* packetData,
OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData* packetData,
EncodeBitstreamParams& params) const {
OctreeElement::AppendState appendElementState = OctreeElement::COMPLETED; // assume the best...
// first, check the params.extraEncodeData to see if there's any partial re-encode data for this element
OctreeElementExtraEncodeData* extraEncodeData = params.extraEncodeData;
EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData = NULL;
@ -280,7 +280,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData
QVector<uint16_t> indexesOfEntitiesToInclude;
// It's possible that our element has been previous completed. In this case we'll simply not include any of our
// entities for encoding. This is needed because we encode the element data at the "parent" level, and so we
// entities for encoding. This is needed because we encode the element data at the "parent" level, and so we
// need to handle the case where our sibling elements need encoding but we don't.
if (!entityTreeElementExtraEncodeData->elementCompleted) {
for (uint16_t i = 0; i < _entityItems.size(); i++) {
@ -304,7 +304,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData
// frustum culling on rendering.
bool success;
AACube entityCube = entity->getQueryAACube(success);
if (!success || params.viewFrustum->cubeInFrustum(entityCube) == ViewFrustum::OUTSIDE) {
if (!success || params.viewFrustum->computeCubeViewLocation(entityCube) == ViewFrustum::OUTSIDE) {
includeThisEntity = false; // out of view, don't include it
}
@ -397,7 +397,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData
// this octree element.
if (extraEncodeData && entityTreeElementExtraEncodeData) {
// After processing, if we are PARTIAL or COMPLETED then we need to re-include our extra data.
// After processing, if we are PARTIAL or COMPLETED then we need to re-include our extra data.
// Only our parent can remove our extra data in these cases and only after it knows that all of its
// children have been encoded.
// If we weren't able to encode ANY data about ourselves, then we go ahead and remove our element data
@ -412,7 +412,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData
extraEncodeData->insert(this, entityTreeElementExtraEncodeData);
}
} else {
// If we weren't previously completed, check to see if we are
if (!entityTreeElementExtraEncodeData->elementCompleted) {
// If all of our items have been encoded, then we are complete as an element.
@ -426,9 +426,9 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData
}
}
// Determine if no entities at all were able to fit
// Determine if no entities at all were able to fit
bool noEntitiesFit = (numberOfEntities > 0 && actualNumberOfEntities == 0);
// If we wrote fewer entities than we expected, update the number of entities in our packet
bool successUpdateEntityCount = true;
if (numberOfEntities != actualNumberOfEntities) {
@ -504,7 +504,7 @@ bool EntityTreeElement::bestFitBounds(const glm::vec3& minPoint, const glm::vec3
glm::vec3 clampedMax = glm::clamp(maxPoint, (float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE);
if (_cube.contains(clampedMin) && _cube.contains(clampedMax)) {
// If our child would be smaller than our smallest reasonable element, then we are the best fit.
float childScale = _cube.getScale() / 2.0f;
if (childScale <= SMALLEST_REASONABLE_OCTREE_ELEMENT_SCALE) {
@ -524,7 +524,7 @@ bool EntityTreeElement::bestFitBounds(const glm::vec3& minPoint, const glm::vec3
bool EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
bool& keepSearching, OctreeElementPointer& element, float& distance,
BoxFace& face, glm::vec3& surfaceNormal, const QVector<EntityItemID>& entityIdsToInclude,
BoxFace& face, glm::vec3& surfaceNormal, const QVector<EntityItemID>& entityIdsToInclude,
const QVector<EntityItemID>& entityIdsToDiscard, void** intersectedObject, bool precisionPicking) {
keepSearching = true; // assume that we will continue searching after this.
@ -607,7 +607,7 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con
// we can use the AABox's ray intersection by mapping our origin and direction into the entity frame
// and testing intersection there.
if (entityFrameBox.findRayIntersection(entityFrameOrigin, entityFrameDirection, localDistance,
if (entityFrameBox.findRayIntersection(entityFrameOrigin, entityFrameDirection, localDistance,
localFace, localSurfaceNormal)) {
if (localDistance < distance) {
// now ask the entity if we actually intersect
@ -862,12 +862,12 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int
if (this == _myTree->getRoot().get() && args.bitstreamVersion < VERSION_ROOT_ELEMENT_HAS_DATA) {
return 0;
}
const unsigned char* dataAt = data;
int bytesRead = 0;
uint16_t numberOfEntities = 0;
int expectedBytesPerEntity = EntityItem::expectedBytes();
args.elementsPerPacket++;
if (bytesLeftToRead >= (int)sizeof(numberOfEntities)) {
@ -947,7 +947,7 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int
entityItem->recordCreationTime();
}
} else {
qDebug() << "Recieved packet for previously deleted entity [" <<
qDebug() << "Recieved packet for previously deleted entity [" <<
entityItem->getID() << "] ignoring. (inside " << __FUNCTION__ << ")";
}
}
@ -959,7 +959,7 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int
}
}
}
return bytesRead;
}
@ -990,7 +990,7 @@ bool EntityTreeElement::pruneChildren() {
bool somethingPruned = false;
for (int childIndex = 0; childIndex < NUMBER_OF_CHILDREN; childIndex++) {
EntityTreeElementPointer child = getChildAtIndex(childIndex);
// if my child is a leaf, but has no entities, then it's safe to delete my child
if (child && child->isLeaf() && !child->hasEntities()) {
deleteChildAtIndex(childIndex);
@ -1040,4 +1040,4 @@ void EntityTreeElement::debugDump() {
}
});
}

View file

@ -30,7 +30,7 @@ void KeyLightPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desired
COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_INTENSITY, KeyLight, keyLight, Intensity, intensity);
COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_AMBIENT_INTENSITY, KeyLight, keyLight, AmbientIntensity, ambientIntensity);
COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_DIRECTION, KeyLight, keyLight, Direction, direction);
COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_AMBIENT_URL, KeyLight, keyLight, AmbientURL, ambientUrl);
COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_AMBIENT_URL, KeyLight, keyLight, AmbientURL, ambientURL);
}

View file

@ -21,6 +21,12 @@
#include "EntityTreeElement.h"
#include "LightEntityItem.h"
const bool LightEntityItem::DEFAULT_IS_SPOTLIGHT = false;
const float LightEntityItem::DEFAULT_INTENSITY = 1.0f;
const float LightEntityItem::DEFAULT_FALLOFF_RADIUS = 0.1f;
const float LightEntityItem::DEFAULT_EXPONENT = 0.0f;
const float LightEntityItem::DEFAULT_CUTOFF = PI / 2.0f;
bool LightEntityItem::_lightsArePickable = false;
EntityItemPointer LightEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
@ -32,12 +38,7 @@ EntityItemPointer LightEntityItem::factory(const EntityItemID& entityID, const E
// our non-pure virtual subclass for now...
LightEntityItem::LightEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) {
_type = EntityTypes::Light;
// default property values
_color[RED_INDEX] = _color[GREEN_INDEX] = _color[BLUE_INDEX] = 0;
_intensity = 1.0f;
_exponent = 0.0f;
_cutoff = PI;
}
void LightEntityItem::setDimensions(const glm::vec3& value) {
@ -62,10 +63,15 @@ EntityItemProperties LightEntityItem::getProperties(EntityPropertyFlags desiredP
COPY_ENTITY_PROPERTY_TO_PROPERTIES(intensity, getIntensity);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(exponent, getExponent);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(cutoff, getCutoff);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(falloffRadius, getFalloffRadius);
return properties;
}
void LightEntityItem::setFalloffRadius(float value) {
_falloffRadius = glm::max(value, 0.0f);
}
void LightEntityItem::setIsSpotlight(bool value) {
if (value != _isSpotlight) {
_isSpotlight = value;
@ -101,6 +107,7 @@ bool LightEntityItem::setProperties(const EntityItemProperties& properties) {
SET_ENTITY_PROPERTY_FROM_PROPERTIES(intensity, setIntensity);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(exponent, setExponent);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(cutoff, setCutoff);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(falloffRadius, setFalloffRadius);
if (somethingChanged) {
bool wantDebug = false;
@ -150,6 +157,7 @@ int LightEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
READ_ENTITY_PROPERTY(PROP_INTENSITY, float, setIntensity);
READ_ENTITY_PROPERTY(PROP_EXPONENT, float, setExponent);
READ_ENTITY_PROPERTY(PROP_CUTOFF, float, setCutoff);
READ_ENTITY_PROPERTY(PROP_FALLOFF_RADIUS, float, setFalloffRadius);
}
return bytesRead;
@ -164,6 +172,7 @@ EntityPropertyFlags LightEntityItem::getEntityProperties(EncodeBitstreamParams&
requestedProperties += PROP_INTENSITY;
requestedProperties += PROP_EXPONENT;
requestedProperties += PROP_CUTOFF;
requestedProperties += PROP_FALLOFF_RADIUS;
return requestedProperties;
}
@ -181,4 +190,5 @@ void LightEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit
APPEND_ENTITY_PROPERTY(PROP_INTENSITY, getIntensity());
APPEND_ENTITY_PROPERTY(PROP_EXPONENT, getExponent());
APPEND_ENTITY_PROPERTY(PROP_CUTOFF, getCutoff());
APPEND_ENTITY_PROPERTY(PROP_FALLOFF_RADIUS, getFalloffRadius());
}

View file

@ -16,6 +16,12 @@
class LightEntityItem : public EntityItem {
public:
static const bool DEFAULT_IS_SPOTLIGHT;
static const float DEFAULT_INTENSITY;
static const float DEFAULT_FALLOFF_RADIUS;
static const float DEFAULT_EXPONENT;
static const float DEFAULT_CUTOFF;
static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties);
LightEntityItem(const EntityItemID& entityItemID);
@ -65,6 +71,9 @@ public:
float getIntensity() const { return _intensity; }
void setIntensity(float value) { _intensity = value; }
float getFalloffRadius() const { return _falloffRadius; }
void setFalloffRadius(float value);
float getExponent() const { return _exponent; }
void setExponent(float value) { _exponent = value; }
@ -78,10 +87,11 @@ protected:
// properties of a light
rgbColor _color;
bool _isSpotlight;
float _intensity;
float _exponent;
float _cutoff;
bool _isSpotlight { DEFAULT_IS_SPOTLIGHT };
float _intensity { DEFAULT_INTENSITY };
float _falloffRadius { DEFAULT_FALLOFF_RADIUS };
float _exponent { DEFAULT_EXPONENT };
float _cutoff { DEFAULT_CUTOFF };
static bool _lightsArePickable;
};

View file

@ -21,6 +21,7 @@ Framebuffer::~Framebuffer() {
Framebuffer* Framebuffer::create() {
auto framebuffer = new Framebuffer();
framebuffer->_renderBuffers.resize(MAX_NUM_RENDER_BUFFERS);
framebuffer->_colorStamps.resize(MAX_NUM_RENDER_BUFFERS, 0);
return framebuffer;
}
@ -174,6 +175,8 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin
}
}
++_colorStamps[slot];
updateSize(texture);
// assign the new one
@ -190,6 +193,7 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin
}
void Framebuffer::removeRenderBuffers() {
if (isSwapchain()) {
return;
}
@ -230,6 +234,7 @@ uint32 Framebuffer::getRenderBufferSubresource(uint32 slot) const {
}
bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const Format& format, uint32 subresource) {
++_depthStamp;
if (isSwapchain()) {
return false;
}

View file

@ -135,10 +135,15 @@ public:
static uint32 getMaxNumRenderBuffers() { return MAX_NUM_RENDER_BUFFERS; }
const GPUObjectPointer gpuObject {};
Stamp getDepthStamp() const { return _depthStamp; }
const std::vector<Stamp>& getColorStamps() const { return _colorStamps; }
protected:
SwapchainPointer _swapchain;
Stamp _depthStamp { 0 };
std::vector<Stamp> _colorStamps;
TextureViews _renderBuffers;
TextureView _depthStencilBuffer;

View file

@ -173,6 +173,9 @@ public:
public:
GLuint _fbo = 0;
std::vector<GLenum> _colorBuffers;
Stamp _depthStamp { 0 };
std::vector<Stamp> _colorStamps;
GLFramebuffer();
~GLFramebuffer();

View file

@ -27,8 +27,15 @@ GLBackend::GLFramebuffer::~GLFramebuffer() {
GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffer) {
GLFramebuffer* object = Backend::getGPUObject<GLBackend::GLFramebuffer>(framebuffer);
bool needsUpate { false };
if (!object ||
framebuffer.getDepthStamp() != object->_depthStamp ||
framebuffer.getColorStamps() != object->_colorStamps) {
needsUpate = true;
}
// If GPU object already created and in sync
if (object) {
if (!needsUpate) {
return object;
} else if (framebuffer.isEmpty()) {
// NO framebuffer definition yet so let's avoid thinking
@ -37,94 +44,112 @@ GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffe
// need to have a gpu object?
if (!object) {
GLint currentFBO;
// All is green, assign the gpuobject to the Framebuffer
object = new GLFramebuffer();
Backend::setGPUObject(framebuffer, object);
glGenFramebuffers(1, &object->_fbo);
(void)CHECK_GL_ERROR();
}
if (needsUpate) {
GLint currentFBO = -1;
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &currentFBO);
GLuint fbo;
glGenFramebuffers(1, &fbo);
(void) CHECK_GL_ERROR();
glBindFramebuffer(GL_FRAMEBUFFER, object->_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
GLTexture* gltexture = nullptr;
TexturePointer surface;
if (framebuffer.getColorStamps() != object->_colorStamps) {
if (framebuffer.hasColor()) {
object->_colorBuffers.clear();
static const GLenum colorAttachments[] = {
GL_COLOR_ATTACHMENT0,
GL_COLOR_ATTACHMENT1,
GL_COLOR_ATTACHMENT2,
GL_COLOR_ATTACHMENT3,
GL_COLOR_ATTACHMENT4,
GL_COLOR_ATTACHMENT5,
GL_COLOR_ATTACHMENT6,
GL_COLOR_ATTACHMENT7,
GL_COLOR_ATTACHMENT8,
GL_COLOR_ATTACHMENT9,
GL_COLOR_ATTACHMENT10,
GL_COLOR_ATTACHMENT11,
GL_COLOR_ATTACHMENT12,
GL_COLOR_ATTACHMENT13,
GL_COLOR_ATTACHMENT14,
GL_COLOR_ATTACHMENT15 };
std::vector<GLenum> colorBuffers;
if (framebuffer.hasColor()) {
static const GLenum colorAttachments[] = {
GL_COLOR_ATTACHMENT0,
GL_COLOR_ATTACHMENT1,
GL_COLOR_ATTACHMENT2,
GL_COLOR_ATTACHMENT3,
GL_COLOR_ATTACHMENT4,
GL_COLOR_ATTACHMENT5,
GL_COLOR_ATTACHMENT6,
GL_COLOR_ATTACHMENT7,
GL_COLOR_ATTACHMENT8,
GL_COLOR_ATTACHMENT9,
GL_COLOR_ATTACHMENT10,
GL_COLOR_ATTACHMENT11,
GL_COLOR_ATTACHMENT12,
GL_COLOR_ATTACHMENT13,
GL_COLOR_ATTACHMENT14,
GL_COLOR_ATTACHMENT15 };
int unit = 0;
for (auto& b : framebuffer.getRenderBuffers()) {
surface = b._texture;
if (surface) {
gltexture = GLBackend::syncGPUObject(*surface);
} else {
gltexture = nullptr;
}
int unit = 0;
for (auto& b : framebuffer.getRenderBuffers()) {
auto surface = b._texture;
if (surface) {
auto gltexture = GLBackend::syncGPUObject(*surface);
if (gltexture) {
glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0);
object->_colorBuffers.push_back(colorAttachments[unit]);
} else {
glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, 0, 0);
}
colorBuffers.push_back(colorAttachments[unit]);
unit++;
}
}
}
#if (GPU_FEATURE_PROFILE == GPU_LEGACY)
// for reasons that i don't understand yet, it seems that on mac gl, a fbo must have a color buffer...
else {
GLuint renderBuffer = 0;
glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, framebuffer.getWidth(), framebuffer.getHeight());
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer);
(void) CHECK_GL_ERROR();
}
// glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
#endif
if (framebuffer.hasDepthStencil()) {
auto surface = framebuffer.getDepthStencilBuffer();
if (surface) {
auto gltexture = GLBackend::syncGPUObject(*surface);
if (gltexture) {
GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT;
if (!framebuffer.hasStencil()) {
attachement = GL_DEPTH_ATTACHMENT;
glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0);
} else if (!framebuffer.hasDepth()) {
attachement = GL_STENCIL_ATTACHMENT;
glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0);
} else {
attachement = GL_DEPTH_STENCIL_ATTACHMENT;
glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0);
}
(void) CHECK_GL_ERROR();
}
#if (GPU_FEATURE_PROFILE == GPU_LEGACY)
// for reasons that i don't understand yet, it seems that on mac gl, a fbo must have a color buffer...
else {
GLuint renderBuffer = 0;
glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, framebuffer.getWidth(), framebuffer.getHeight());
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer);
(void) CHECK_GL_ERROR();
}
// glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
#endif
object->_colorStamps = framebuffer.getColorStamps();
}
GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT;
if (!framebuffer.hasStencil()) {
attachement = GL_DEPTH_ATTACHMENT;
} else if (!framebuffer.hasDepth()) {
attachement = GL_STENCIL_ATTACHMENT;
}
if (framebuffer.getDepthStamp() != object->_depthStamp) {
auto surface = framebuffer.getDepthStencilBuffer();
if (framebuffer.hasDepthStencil() && surface) {
gltexture = GLBackend::syncGPUObject(*surface);
}
if (gltexture) {
glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0);
} else {
glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, 0, 0);
}
object->_depthStamp = framebuffer.getDepthStamp();
}
// Last but not least, define where we draw
if (!colorBuffers.empty()) {
glDrawBuffers((GLsizei)colorBuffers.size(), colorBuffers.data());
if (!object->_colorBuffers.empty()) {
glDrawBuffers((GLsizei)object->_colorBuffers.size(), object->_colorBuffers.data());
} else {
glDrawBuffer( GL_NONE );
}
// Now check for completness
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
// restore the current framebuffer
if (currentFBO != -1) {
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO);
}
bool result = false;
switch (status) {
case GL_FRAMEBUFFER_COMPLETE :
@ -147,20 +172,10 @@ GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffe
qCDebug(gpulogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED.";
break;
}
if (!result && fbo) {
glDeleteFramebuffers( 1, &fbo );
if (!result && object->_fbo) {
glDeleteFramebuffers(1, &object->_fbo);
return nullptr;
}
// All is green, assign the gpuobject to the Framebuffer
object = new GLFramebuffer();
object->_fbo = fbo;
object->_colorBuffers = colorBuffers;
Backend::setGPUObject(framebuffer, object);
// restore the current framebuffer
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO);
}
return object;

View file

@ -12,19 +12,18 @@
using namespace model;
Light::Light() :
_flags(0),
_schemaBuffer(),
_transform() {
Light::Light() {
// only if created from nothing shall we create the Buffer to store the properties
Schema schema;
_schemaBuffer = std::make_shared<gpu::Buffer>(sizeof(Schema), (const gpu::Byte*) &schema);
updateLightRadius();
}
Light::Light(const Light& light) :
_flags(light._flags),
_schemaBuffer(light._schemaBuffer),
_transform(light._transform) {
_transform(light._transform)
{
}
Light& Light::operator= (const Light& light) {
@ -70,18 +69,36 @@ void Light::setAmbientIntensity(float intensity) {
editSchema()._ambientIntensity = intensity;
}
void Light::setFalloffRadius(float radius) {
if (radius <= 0.0f) {
radius = 0.1f;
}
editSchema()._attenuation.x = radius;
updateLightRadius();
}
void Light::setMaximumRadius(float radius) {
if (radius <= 0.f) {
radius = 1.0f;
}
editSchema()._attenuation.w = radius;
editSchema()._attenuation.y = radius;
updateLightRadius();
}
void Light::updateLightRadius() {
float CutOffIntensityRatio = 0.05f;
float surfaceRadius = getMaximumRadius() / (sqrtf((getIntensity() * std::max(std::max(getColor().x, getColor().y), getColor().z)) / CutOffIntensityRatio) - 1.0f);
editSchema()._attenuation = Vec4(surfaceRadius, 1.0f/surfaceRadius, CutOffIntensityRatio, getMaximumRadius());
// This function relies on the attenuation equation:
// I = Li / (1 + (d + Lr)/Lr)^2
// where I = calculated intensity, Li = light intensity, Lr = light falloff radius, d = distance from surface
// see: https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/
// note that falloff radius replaces surface radius in linked example
// This equation is biased back by Lr so that all lights act as true points, regardless of surface radii
const float MIN_CUTOFF_INTENSITY = 0.001f;
// Get cutoff radius at minimum intensity
float intensity = getIntensity() * std::max(std::max(getColor().x, getColor().y), getColor().z);
float cutoffRadius = getFalloffRadius() * ((glm::sqrt(intensity / MIN_CUTOFF_INTENSITY) - 1) - 1);
// If it is less than max radius, store it to buffer to avoid extra shading
editSchema()._attenuation.z = std::min(getMaximumRadius(), cutoffRadius);
}
#include <math.h>

View file

@ -74,8 +74,17 @@ public:
bool isRanged() const { return (getType() == POINT) || (getType() == SPOT ); }
// FalloffRradius is the physical radius of the light sphere through which energy shines,
// expressed in meters. It is used only to calculate the falloff curve of the light.
// Actual rendered lights will all have surface radii approaching 0.
void setFalloffRadius(float radius);
float getFalloffRadius() const { return getSchema()._attenuation.x; }
// Maximum radius is the cutoff radius of the light energy, expressed in meters.
// It is used to bound light entities, and *will not* affect the falloff curve of the light.
// Setting it low will result in a noticeable cutoff.
void setMaximumRadius(float radius);
float getMaximumRadius() const { return getSchema()._attenuation.w; }
float getMaximumRadius() const { return getSchema()._attenuation.y; }
// Spot properties
bool isSpot() const { return getType() == SPOT; }
@ -107,7 +116,7 @@ public:
float _ambientIntensity{0.0f};
Color _color{1.0f};
float _intensity{1.0f};
Vec4 _attenuation{1.0f};
Vec4 _attenuation{0.1f, 1.0f, 0.0f, 0.0f};
Vec4 _spot{0.0f, 0.0f, 0.0f, 0.0f};
Vec4 _shadow{0.0f};
@ -120,7 +129,7 @@ public:
protected:
Flags _flags;
Flags _flags{ 0 };
UniformBufferView _schemaBuffer;
Transform _transform;

View file

@ -66,14 +66,6 @@ vec3 getLightColor(Light l) { return l._color.rgb; }
float getLightIntensity(Light l) { return l._color.w; }
float getLightAmbientIntensity(Light l) { return l._direction.w; }
float evalLightAttenuation(Light l, float r) {
float d = max(r - l._attenuation.x, 0.0);
float denom = d * l._attenuation.y + 1.0;
float attenuation = 1.0 / (denom * denom);
return max((attenuation - l._attenuation.z)/(1.0 - l._attenuation.z), 0.0);
// return clamp(1.0/(l._attenuation.x + l._attenuation.y * r + l._attenuation.z * r * r), 0.0, 1.0);
}
float getLightSpotAngleCos(Light l) {
return l._spot.x;
}
@ -86,22 +78,33 @@ float evalLightSpotAttenuation(Light l, float cosA) {
return pow(cosA, l._spot.w);
}
float getLightSquareRadius(Light l) {
return l._attenuation.w * l._attenuation.w;
}
float getLightRadius(Light l) {
return l._attenuation.w;
return l._attenuation.x;
}
float getLightAttenuationCutoff(Light l) {
float getLightSquareRadius(Light l) {
return getLightRadius(l) * getLightRadius(l);
}
float getLightCutoffRadius(Light l) {
return l._attenuation.z;
}
float getLightCutoffSquareRadius(Light l) {
return getLightCutoffRadius(l) * getLightCutoffRadius(l);
}
float getLightShowContour(Light l) {
return l._control.w;
}
float evalLightAttenuation(Light l, float d) {
float radius = getLightRadius(l);
float denom = d / radius + 1.0;
float attenuation = min(1.0, 1.0 / (denom * denom));
return attenuation;
}
SphericalHarmonics getLightAmbientSphere(Light l) {
return l._ambientSphere;
}

View file

@ -202,14 +202,13 @@ void SunSkyStage::setSunModelEnable(bool isEnabled) {
invalidate();
}
void SunSkyStage::setSunColor(const Vec3& color) {
_sunLight->setColor(color);
}
void SunSkyStage::setSunIntensity(float intensity) {
_sunLight->setIntensity(intensity);
}
void SunSkyStage::setSunAmbientIntensity(float intensity) {
_sunLight->setAmbientIntensity(intensity);
void SunSkyStage::setSunAmbientSphere(const gpu::SHPointer& sphere) {
if (sphere) {
_sunLight->setAmbientSphere(*sphere);
} else {
const gpu::SphericalHarmonics::Preset DEFAULT_AMBIENT_SPHERE = gpu::SphericalHarmonics::OLD_TOWN_SQUARE;
_sunLight->setAmbientSpherePreset(DEFAULT_AMBIENT_SPHERE);
}
}
void SunSkyStage::setSunDirection(const Vec3& direction) {

View file

@ -11,7 +11,7 @@
#ifndef hifi_model_Stage_h
#define hifi_model_Stage_h
#include "gpu/Pipeline.h"
#include <gpu/Pipeline.h>
#include "Light.h"
#include "Skybox.h"
@ -143,12 +143,13 @@ public:
bool isSunModelEnabled() const { return _sunModelEnable; }
// Sun properties
void setSunColor(const Vec3& color);
void setSunColor(const Vec3& color) { _sunLight->setColor(color); }
const Vec3& getSunColor() const { return getSunLight()->getColor(); }
void setSunIntensity(float intensity);
void setSunIntensity(float intensity) { _sunLight->setIntensity(intensity); }
float getSunIntensity() const { return getSunLight()->getIntensity(); }
void setSunAmbientIntensity(float intensity);
void setSunAmbientIntensity(float intensity) { _sunLight->setAmbientIntensity(intensity); }
float getSunAmbientIntensity() const { return getSunLight()->getAmbientIntensity(); }
void setSunAmbientSphere(const gpu::SHPointer& sphere);
// The sun direction is expressed in the world space
void setSunDirection(const Vec3& direction);

View file

@ -12,10 +12,12 @@
#include <memory>
#include <QtCore/QDataStream>
#include <QtCore/QFile>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QMap>
#include <QtCore/QStringList>
#include <QtCore/QStandardPaths>
#include <QtCore/QUrlQuery>
#include <QtNetwork/QHttpMultiPart>
#include <QtNetwork/QNetworkRequest>
@ -60,13 +62,12 @@ JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, co
updateReciever(updateReceiver),
updateSlot(updateSlot)
{
}
AccountManager::AccountManager() :
_authURL(),
_pendingCallbackMap(),
_accountInfo(),
_shouldPersistToSettingsFile(true)
_pendingCallbackMap()
{
qRegisterMetaType<OAuthAccessToken>("OAuthAccessToken");
qRegisterMetaTypeStreamOperators<OAuthAccessToken>("OAuthAccessToken");
@ -80,9 +81,6 @@ AccountManager::AccountManager() :
qRegisterMetaType<QHttpMultiPart*>("QHttpMultiPart*");
connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged);
// once we have a profile in account manager make sure we generate a new keypair
connect(this, &AccountManager::profileChanged, this, &AccountManager::generateNewKeypair);
}
const QString DOUBLE_SLASH_SUBSTITUTE = "slashslash";
@ -93,16 +91,9 @@ void AccountManager::logout() {
emit balanceChanged(0);
connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged);
if (_shouldPersistToSettingsFile) {
QString keyURLString(_authURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE));
QStringList path = QStringList() << ACCOUNTS_GROUP << keyURLString;
Setting::Handle<DataServerAccountInfo>(path).remove();
qCDebug(networking) << "Removed account info for" << _authURL << "from in-memory accounts and .ini file";
} else {
qCDebug(networking) << "Cleared data server account info in account manager.";
}
// remove this account from the account settings file
removeAccountFromFile();
emit logoutComplete();
// the username has changed to blank
@ -124,35 +115,83 @@ void AccountManager::accountInfoBalanceChanged(qint64 newBalance) {
emit balanceChanged(newBalance);
}
QString accountFilePath() {
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/AccountInfo.bin";
}
QVariantMap accountMapFromFile(bool& success) {
QFile accountFile { accountFilePath() };
if (accountFile.open(QIODevice::ReadOnly)) {
// grab the current QVariantMap from the settings file
QDataStream readStream(&accountFile);
QVariantMap accountMap;
readStream >> accountMap;
// close the file now that we have read the data
accountFile.close();
success = true;
return accountMap;
} else {
// failed to open file, return empty QVariantMap
// there was only an error if the account file existed when we tried to load it
success = !accountFile.exists();
return QVariantMap();
}
}
void AccountManager::setAuthURL(const QUrl& authURL) {
if (_authURL != authURL) {
_authURL = authURL;
qCDebug(networking) << "AccountManager URL for authenticated requests has been changed to" << qPrintable(_authURL.toString());
if (_shouldPersistToSettingsFile) {
// check if there are existing access tokens to load from settings
QFile accountsFile { accountFilePath() };
bool loadedMap = false;
auto accountsMap = accountMapFromFile(loadedMap);
if (accountsFile.exists() && loadedMap) {
// pull out the stored account info and store it in memory
_accountInfo = accountsMap[_authURL.toString()].value<DataServerAccountInfo>();
qCDebug(networking) << "Found metaverse API account information for" << qPrintable(_authURL.toString());
} else {
// we didn't have a file - see if we can migrate old settings and store them in the new file
// check if there are existing access tokens to load from settings
Settings settings;
settings.beginGroup(ACCOUNTS_GROUP);
foreach(const QString& key, settings.allKeys()) {
// take a key copy to perform the double slash replacement
QString keyCopy(key);
QUrl keyURL(keyCopy.replace(DOUBLE_SLASH_SUBSTITUTE, "//"));
if (keyURL == _authURL) {
// pull out the stored access token and store it in memory
_accountInfo = settings.value(key).value<DataServerAccountInfo>();
qCDebug(networking) << "Found a data-server access token for" << qPrintable(keyURL.toString());
// profile info isn't guaranteed to be saved too
if (_accountInfo.hasProfile()) {
emit profileChanged();
} else {
requestProfile();
}
qCDebug(networking) << "Migrated an access token for" << qPrintable(keyURL.toString())
<< "from previous settings file";
}
}
if (_accountInfo.getAccessToken().token.isEmpty()) {
qCWarning(networking) << "Unable to load account file. No existing account settings will be loaded.";
} else {
// persist the migrated settings to file
persistAccountToFile();
}
}
if (_isAgent && !_accountInfo.getAccessToken().token.isEmpty() && !_accountInfo.hasProfile()) {
// we are missing profile information, request it now
requestProfile();
}
// tell listeners that the auth endpoint has changed
@ -299,9 +338,11 @@ void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) {
} else {
if (VERBOSE_HTTP_REQUEST_DEBUGGING) {
qCDebug(networking) << "Received JSON response from data-server that has no matching callback.";
qCDebug(networking) << "Received JSON response from metaverse API that has no matching callback.";
qCDebug(networking) << QJsonDocument::fromJson(requestReply->readAll());
}
requestReply->deleteLater();
}
}
@ -317,22 +358,69 @@ void AccountManager::passErrorToCallback(QNetworkReply* requestReply) {
_pendingCallbackMap.remove(requestReply);
} else {
if (VERBOSE_HTTP_REQUEST_DEBUGGING) {
qCDebug(networking) << "Received error response from data-server that has no matching callback.";
qCDebug(networking) << "Received error response from metaverse API that has no matching callback.";
qCDebug(networking) << "Error" << requestReply->error() << "-" << requestReply->errorString();
qCDebug(networking) << requestReply->readAll();
}
requestReply->deleteLater();
}
}
void AccountManager::persistAccountToSettings() {
if (_shouldPersistToSettingsFile) {
// store this access token into the local settings
QString keyURLString(_authURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE));
QStringList path = QStringList() << ACCOUNTS_GROUP << keyURLString;
Setting::Handle<QVariant>(path).set(QVariant::fromValue(_accountInfo));
bool writeAccountMapToFile(const QVariantMap& accountMap) {
// re-open the file and truncate it
QFile accountFile { accountFilePath() };
if (accountFile.open(QIODevice::WriteOnly)) {
QDataStream writeStream(&accountFile);
// persist the updated account QVariantMap to file
writeStream << accountMap;
// close the file with the newly persisted settings
accountFile.close();
return true;
} else {
return false;
}
}
void AccountManager::persistAccountToFile() {
qCDebug(networking) << "Persisting AccountManager accounts to" << accountFilePath();
bool wasLoaded = false;
auto accountMap = accountMapFromFile(wasLoaded);
if (wasLoaded) {
// replace the current account information for this auth URL in the account map
accountMap[_authURL.toString()] = QVariant::fromValue(_accountInfo);
// re-open the file and truncate it
if (writeAccountMapToFile(accountMap)) {
return;
}
}
qCWarning(networking) << "Could not load accounts file - unable to persist account information to file.";
}
void AccountManager::removeAccountFromFile() {
bool wasLoaded = false;
auto accountMap = accountMapFromFile(wasLoaded);
if (wasLoaded) {
accountMap.remove(_authURL.toString());
if (writeAccountMapToFile(accountMap)) {
qCDebug(networking) << "Removed account info for" << _authURL << "from settings file.";
return;
}
}
qCWarning(networking) << "Count not load accounts file - unable to remove account information for" << _authURL
<< "from settings file.";
}
bool AccountManager::hasValidAccessToken() {
if (_accountInfo.getAccessToken().token.isEmpty() || _accountInfo.getAccessToken().isExpired()) {
@ -359,16 +447,19 @@ bool AccountManager::checkAndSignalForAccessToken() {
}
void AccountManager::setAccessTokenForCurrentAuthURL(const QString& accessToken) {
// clear our current DataServerAccountInfo
_accountInfo = DataServerAccountInfo();
// start the new account info with a new OAuthAccessToken
// replace the account info access token with a new OAuthAccessToken
OAuthAccessToken newOAuthToken;
newOAuthToken.token = accessToken;
qCDebug(networking) << "Setting new account manager access token. F2C:" << accessToken.left(2) << "L2C:" << accessToken.right(2);
if (!accessToken.isEmpty()) {
qCDebug(networking) << "Setting new AccountManager OAuth token. F2C:" << accessToken.left(2) << "L2C:" << accessToken.right(2);
} else if (!_accountInfo.getAccessToken().token.isEmpty()) {
qCDebug(networking) << "Clearing AccountManager OAuth token.";
}
_accountInfo.setAccessToken(newOAuthToken);
persistAccountToFile();
}
void AccountManager::requestAccessToken(const QString& login, const QString& password) {
@ -423,7 +514,7 @@ void AccountManager::requestAccessTokenFinished() {
emit loginComplete(rootURL);
persistAccountToSettings();
persistAccountToFile();
requestProfile();
}
@ -469,7 +560,7 @@ void AccountManager::requestProfileFinished() {
emit usernameChanged(_accountInfo.getUsername());
// store the whole profile into the local settings
persistAccountToSettings();
persistAccountToFile();
} else {
// TODO: error handling
@ -482,57 +573,141 @@ void AccountManager::requestProfileError(QNetworkReply::NetworkError error) {
qCDebug(networking) << "AccountManager requestProfileError - " << error;
}
void AccountManager::generateNewKeypair() {
// setup a new QThread to generate the keypair on, in case it takes a while
QThread* generateThread = new QThread(this);
generateThread->setObjectName("Account Manager Generator Thread");
// setup a keypair generator
RSAKeypairGenerator* keypairGenerator = new RSAKeypairGenerator();
connect(generateThread, &QThread::started, keypairGenerator, &RSAKeypairGenerator::generateKeypair);
connect(keypairGenerator, &RSAKeypairGenerator::generatedKeypair, this, &AccountManager::processGeneratedKeypair);
connect(keypairGenerator, &RSAKeypairGenerator::errorGeneratingKeypair,
this, &AccountManager::handleKeypairGenerationError);
connect(keypairGenerator, &QObject::destroyed, generateThread, &QThread::quit);
connect(generateThread, &QThread::finished, generateThread, &QThread::deleteLater);
keypairGenerator->moveToThread(generateThread);
qCDebug(networking) << "Starting worker thread to generate 2048-bit RSA key-pair.";
generateThread->start();
void AccountManager::generateNewKeypair(bool isUserKeypair, const QUuid& domainID) {
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, "generateNewKeypair", Q_ARG(bool, isUserKeypair), Q_ARG(QUuid, domainID));
return;
}
if (!isUserKeypair && domainID.isNull()) {
qCWarning(networking) << "AccountManager::generateNewKeypair called for domain keypair with no domain ID. Will not generate keypair.";
return;
}
// make sure we don't already have an outbound keypair generation request
if (!_isWaitingForKeypairResponse) {
_isWaitingForKeypairResponse = true;
// clear the current private key
qDebug() << "Clearing current private key in DataServerAccountInfo";
_accountInfo.setPrivateKey(QByteArray());
// setup a new QThread to generate the keypair on, in case it takes a while
QThread* generateThread = new QThread(this);
generateThread->setObjectName("Account Manager Generator Thread");
// setup a keypair generator
RSAKeypairGenerator* keypairGenerator = new RSAKeypairGenerator;
if (!isUserKeypair) {
keypairGenerator->setDomainID(domainID);
_accountInfo.setDomainID(domainID);
}
// start keypair generation when the thread starts
connect(generateThread, &QThread::started, keypairGenerator, &RSAKeypairGenerator::generateKeypair);
// handle success or failure of keypair generation
connect(keypairGenerator, &RSAKeypairGenerator::generatedKeypair, this, &AccountManager::processGeneratedKeypair);
connect(keypairGenerator, &RSAKeypairGenerator::errorGeneratingKeypair,
this, &AccountManager::handleKeypairGenerationError);
connect(keypairGenerator, &QObject::destroyed, generateThread, &QThread::quit);
connect(generateThread, &QThread::finished, generateThread, &QThread::deleteLater);
keypairGenerator->moveToThread(generateThread);
qCDebug(networking) << "Starting worker thread to generate 2048-bit RSA keypair.";
generateThread->start();
}
}
void AccountManager::processGeneratedKeypair(const QByteArray& publicKey, const QByteArray& privateKey) {
void AccountManager::processGeneratedKeypair() {
qCDebug(networking) << "Generated 2048-bit RSA key-pair. Storing private key and uploading public key.";
// set the private key on our data-server account info
_accountInfo.setPrivateKey(privateKey);
persistAccountToSettings();
// upload the public key so data-web has an up-to-date key
const QString PUBLIC_KEY_UPDATE_PATH = "api/v1/user/public_key";
// setup a multipart upload to send up the public key
QHttpMultiPart* requestMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart keyPart;
keyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream"));
keyPart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"public_key\"; filename=\"public_key\""));
keyPart.setBody(publicKey);
requestMultiPart->append(keyPart);
sendRequest(PUBLIC_KEY_UPDATE_PATH, AccountManagerAuth::Required, QNetworkAccessManager::PutOperation,
JSONCallbackParameters(), QByteArray(), requestMultiPart);
// get rid of the keypair generator now that we don't need it anymore
sender()->deleteLater();
qCDebug(networking) << "Generated 2048-bit RSA keypair. Uploading public key now.";
RSAKeypairGenerator* keypairGenerator = qobject_cast<RSAKeypairGenerator*>(sender());
if (keypairGenerator) {
// hold the private key to later set our metaverse API account info if upload succeeds
_pendingPrivateKey = keypairGenerator->getPrivateKey();
// upload the public key so data-web has an up-to-date key
const QString USER_PUBLIC_KEY_UPDATE_PATH = "api/v1/user/public_key";
const QString DOMAIN_PUBLIC_KEY_UPDATE_PATH = "api/v1/domains/%1/public_key";
QString uploadPath;
if (keypairGenerator->getDomainID().isNull()) {
uploadPath = USER_PUBLIC_KEY_UPDATE_PATH;
} else {
uploadPath = DOMAIN_PUBLIC_KEY_UPDATE_PATH.arg(uuidStringWithoutCurlyBraces(keypairGenerator->getDomainID()));
}
// setup a multipart upload to send up the public key
QHttpMultiPart* requestMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart keyPart;
keyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream"));
keyPart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"public_key\"; filename=\"public_key\""));
keyPart.setBody(keypairGenerator->getPublicKey());
requestMultiPart->append(keyPart);
// setup callback parameters so we know once the keypair upload has succeeded or failed
JSONCallbackParameters callbackParameters;
callbackParameters.jsonCallbackReceiver = this;
callbackParameters.jsonCallbackMethod = "publicKeyUploadSucceeded";
callbackParameters.errorCallbackReceiver = this;
callbackParameters.errorCallbackMethod = "publicKeyUploadFailed";
sendRequest(uploadPath, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation,
callbackParameters, QByteArray(), requestMultiPart);
keypairGenerator->deleteLater();
} else {
qCWarning(networking) << "Expected processGeneratedKeypair to be called by a live RSAKeypairGenerator"
<< "but the casted sender is NULL. Will not process generated keypair.";
}
}
void AccountManager::publicKeyUploadSucceeded(QNetworkReply& reply) {
qDebug() << "Uploaded public key to Metaverse API. RSA keypair generation is completed.";
// public key upload complete - store the matching private key and persist the account to settings
_accountInfo.setPrivateKey(_pendingPrivateKey);
_pendingPrivateKey.clear();
persistAccountToFile();
// clear our waiting state
_isWaitingForKeypairResponse = false;
emit newKeypair();
// delete the reply object now that we are done with it
reply.deleteLater();
}
void AccountManager::publicKeyUploadFailed(QNetworkReply& reply) {
// the public key upload has failed
qWarning() << "Public key upload failed from AccountManager" << reply.errorString();
// we aren't waiting for a response any longer
_isWaitingForKeypairResponse = false;
// clear our pending private key
_pendingPrivateKey.clear();
// delete the reply object now that we are done with it
reply.deleteLater();
}
void AccountManager::handleKeypairGenerationError() {
// for now there isn't anything we do with this except get the worker thread to clean up
qCritical() << "Error generating keypair - this is likely to cause authentication issues.";
// reset our waiting state for keypair response
_isWaitingForKeypairResponse = false;
sender()->deleteLater();
}

View file

@ -62,12 +62,12 @@ public:
QHttpMultiPart* dataMultiPart = NULL,
const QVariantMap& propertyMap = QVariantMap());
void setIsAgent(bool isAgent) { _isAgent = isAgent; }
const QUrl& getAuthURL() const { return _authURL; }
void setAuthURL(const QUrl& authURL);
bool hasAuthEndpoint() { return !_authURL.isEmpty(); }
void disableSettingsFilePersistence() { _shouldPersistToSettingsFile = false; }
bool isLoggedIn() { return !_authURL.isEmpty() && hasValidAccessToken(); }
bool hasValidAccessToken();
Q_INVOKABLE bool checkAndSignalForAccessToken();
@ -87,7 +87,9 @@ public slots:
void logout();
void updateBalance();
void accountInfoBalanceChanged(qint64 newBalance);
void generateNewKeypair();
void generateNewUserKeypair() { generateNewKeypair(); }
void generateNewDomainKeypair(const QUuid& domainID) { generateNewKeypair(false, domainID); }
signals:
void authRequired();
void authEndpointChanged();
@ -97,25 +99,36 @@ signals:
void loginFailed();
void logoutComplete();
void balanceChanged(qint64 newBalance);
void newKeypair();
private slots:
void processReply();
void handleKeypairGenerationError();
void processGeneratedKeypair(const QByteArray& publicKey, const QByteArray& privateKey);
void processGeneratedKeypair();
void publicKeyUploadSucceeded(QNetworkReply& reply);
void publicKeyUploadFailed(QNetworkReply& reply);
void generateNewKeypair(bool isUserKeypair = true, const QUuid& domainID = QUuid());
private:
AccountManager();
AccountManager(AccountManager const& other); // not implemented
void operator=(AccountManager const& other); // not implemented
AccountManager(AccountManager const& other) = delete;
void operator=(AccountManager const& other) = delete;
void persistAccountToSettings();
void persistAccountToFile();
void removeAccountFromFile();
void passSuccessToCallback(QNetworkReply* reply);
void passErrorToCallback(QNetworkReply* reply);
QUrl _authURL;
QMap<QNetworkReply*, JSONCallbackParameters> _pendingCallbackMap;
DataServerAccountInfo _accountInfo;
bool _shouldPersistToSettingsFile;
bool _isAgent { false };
bool _isWaitingForKeypairResponse { false };
QByteArray _pendingPrivateKey;
};
#endif // hifi_AccountManager_h

View file

@ -25,19 +25,6 @@
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
DataServerAccountInfo::DataServerAccountInfo() :
_accessToken(),
_username(),
_xmppPassword(),
_discourseApiKey(),
_walletID(),
_balance(0),
_hasBalance(false),
_privateKey()
{
}
DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherInfo) : QObject() {
_accessToken = otherInfo._accessToken;
_username = otherInfo._username;
@ -47,6 +34,7 @@ DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherI
_balance = otherInfo._balance;
_hasBalance = otherInfo._hasBalance;
_privateKey = otherInfo._privateKey;
_domainID = otherInfo._domainID;
}
DataServerAccountInfo& DataServerAccountInfo::operator=(const DataServerAccountInfo& otherInfo) {
@ -66,6 +54,7 @@ void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) {
swap(_balance, otherInfo._balance);
swap(_hasBalance, otherInfo._hasBalance);
swap(_privateKey, otherInfo._privateKey);
swap(_domainID, otherInfo._domainID);
}
void DataServerAccountInfo::setAccessTokenFromJSON(const QJsonObject& jsonObject) {
@ -128,59 +117,62 @@ void DataServerAccountInfo::setProfileInfoFromJSON(const QJsonObject& jsonObject
}
QByteArray DataServerAccountInfo::getUsernameSignature(const QUuid& connectionToken) {
if (!_privateKey.isEmpty()) {
const char* privateKeyData = _privateKey.constData();
RSA* rsaPrivateKey = d2i_RSAPrivateKey(NULL,
reinterpret_cast<const unsigned char**>(&privateKeyData),
_privateKey.size());
if (rsaPrivateKey) {
QByteArray lowercaseUsername = _username.toLower().toUtf8();
QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()),
QCryptographicHash::Sha256);
QByteArray usernameSignature(RSA_size(rsaPrivateKey), 0);
unsigned int usernameSignatureSize = 0;
int encryptReturn = RSA_sign(NID_sha256,
reinterpret_cast<const unsigned char*>(usernameWithToken.constData()),
usernameWithToken.size(),
reinterpret_cast<unsigned char*>(usernameSignature.data()),
&usernameSignatureSize,
rsaPrivateKey);
// free the private key RSA struct now that we are done with it
RSA_free(rsaPrivateKey);
auto lowercaseUsername = _username.toLower().toUtf8();
auto plaintext = lowercaseUsername.append(connectionToken.toRfc4122());
if (encryptReturn == -1) {
qCDebug(networking) << "Error encrypting username signature.";
qCDebug(networking) << "Will re-attempt on next domain-server check in.";
} else {
qDebug(networking) << "Returning username" << _username << "signed with connection UUID" << uuidStringWithoutCurlyBraces(connectionToken);
return usernameSignature;
}
} else {
qCDebug(networking) << "Could not create RSA struct from QByteArray private key.";
qCDebug(networking) << "Will re-attempt on next domain-server check in.";
}
}
return QByteArray();
auto signature = signPlaintext(plaintext);
if (!signature.isEmpty()) {
qDebug(networking) << "Returning username" << _username
<< "signed with connection UUID" << uuidStringWithoutCurlyBraces(connectionToken);
} else {
qCDebug(networking) << "Error signing username with connection token";
qCDebug(networking) << "Will re-attempt on next domain-server check in.";
}
return signature;
}
void DataServerAccountInfo::setPrivateKey(const QByteArray& privateKey) {
_privateKey = privateKey;
QByteArray DataServerAccountInfo::signPlaintext(const QByteArray& plaintext) {
if (!_privateKey.isEmpty()) {
const char* privateKeyData = _privateKey.constData();
RSA* rsaPrivateKey = d2i_RSAPrivateKey(NULL,
reinterpret_cast<const unsigned char**>(&privateKeyData),
_privateKey.size());
if (rsaPrivateKey) {
QByteArray signature(RSA_size(rsaPrivateKey), 0);
unsigned int signatureBytes = 0;
QByteArray hashedPlaintext = QCryptographicHash::hash(plaintext, QCryptographicHash::Sha256);
int encryptReturn = RSA_sign(NID_sha256,
reinterpret_cast<const unsigned char*>(hashedPlaintext.constData()),
hashedPlaintext.size(),
reinterpret_cast<unsigned char*>(signature.data()),
&signatureBytes,
rsaPrivateKey);
// free the private key RSA struct now that we are done with it
RSA_free(rsaPrivateKey);
if (encryptReturn != -1) {
return signature;
}
} else {
qCDebug(networking) << "Could not create RSA struct from QByteArray private key.";
}
}
return QByteArray();
}
QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info) {
out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey
<< info._walletID << info._privateKey;
<< info._walletID << info._privateKey << info._domainID;
return out;
}
QDataStream& operator>>(QDataStream &in, DataServerAccountInfo& info) {
in >> info._accessToken >> info._username >> info._xmppPassword >> info._discourseApiKey
>> info._walletID >> info._privateKey;
>> info._walletID >> info._privateKey >> info._domainID;
return in;
}

View file

@ -23,7 +23,7 @@ const float SATOSHIS_PER_CREDIT = 100000000.0f;
class DataServerAccountInfo : public QObject {
Q_OBJECT
public:
DataServerAccountInfo();
DataServerAccountInfo() {};
DataServerAccountInfo(const DataServerAccountInfo& otherInfo);
DataServerAccountInfo& operator=(const DataServerAccountInfo& otherInfo);
@ -42,10 +42,6 @@ public:
const QUuid& getWalletID() const { return _walletID; }
void setWalletID(const QUuid& walletID);
QByteArray getUsernameSignature(const QUuid& connectionToken);
bool hasPrivateKey() const { return !_privateKey.isEmpty(); }
void setPrivateKey(const QByteArray& privateKey);
qint64 getBalance() const { return _balance; }
float getBalanceInSatoshis() const { return _balance / SATOSHIS_PER_CREDIT; }
@ -54,6 +50,15 @@ public:
void setHasBalance(bool hasBalance) { _hasBalance = hasBalance; }
Q_INVOKABLE void setBalanceFromJSON(QNetworkReply& requestReply);
QByteArray getUsernameSignature(const QUuid& connectionToken);
bool hasPrivateKey() const { return !_privateKey.isEmpty(); }
void setPrivateKey(const QByteArray& privateKey) { _privateKey = privateKey; }
QByteArray signPlaintext(const QByteArray& plaintext);
void setDomainID(const QUuid& domainID) { _domainID = domainID; }
const QUuid& getDomainID() const { return _domainID; }
bool hasProfile() const;
void setProfileInfoFromJSON(const QJsonObject& jsonObject);
@ -70,8 +75,9 @@ private:
QString _xmppPassword;
QString _discourseApiKey;
QUuid _walletID;
qint64 _balance;
bool _hasBalance;
qint64 _balance { 0 };
bool _hasBalance { false };
QUuid _domainID; // if this holds account info for a domain, this holds the ID of that domain
QByteArray _privateKey;
};

View file

@ -92,7 +92,9 @@ void DomainHandler::softReset() {
disconnect();
clearSettings();
_connectionDenialsSinceKeypairRegen = 0;
// cancel the failure timeout for any pending requests for settings
QMetaObject::invokeMethod(&_settingsTimer, "stop");
}
@ -106,6 +108,9 @@ void DomainHandler::hardReset() {
_hostname = QString();
_sockAddr.clear();
_hasCheckedForAccessToken = false;
_domainConnectionRefusals.clear();
// clear any pending path we may have wanted to ask the previous DS about
_pendingPath.clear();
}
@ -347,3 +352,35 @@ void DomainHandler::processICEResponsePacket(QSharedPointer<ReceivedMessage> mes
emit icePeerSocketsReceived();
}
}
void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer<ReceivedMessage> message) {
// Read deny reason from packet
quint16 reasonSize;
message->readPrimitive(&reasonSize);
QString reason = QString::fromUtf8(message->readWithoutCopy(reasonSize));
// output to the log so the user knows they got a denied connection request
// and check and signal for an access token so that we can make sure they are logged in
qCWarning(networking) << "The domain-server denied a connection request: " << reason;
qCWarning(networking) << "Make sure you are logged in.";
if (!_domainConnectionRefusals.contains(reason)) {
_domainConnectionRefusals.append(reason);
emit domainConnectionRefused(reason);
}
auto& accountManager = AccountManager::getInstance();
if (!_hasCheckedForAccessToken) {
accountManager.checkAndSignalForAccessToken();
_hasCheckedForAccessToken = true;
}
static const int CONNECTION_DENIALS_FOR_KEYPAIR_REGEN = 3;
// force a re-generation of key-pair after CONNECTION_DENIALS_FOR_KEYPAIR_REGEN failed connection attempts
if (++_connectionDenialsSinceKeypairRegen >= CONNECTION_DENIALS_FOR_KEYPAIR_REGEN) {
accountManager.generateNewUserKeypair();
_connectionDenialsSinceKeypairRegen = 0;
}
}

View file

@ -92,6 +92,7 @@ public slots:
void processICEPingReplyPacket(QSharedPointer<ReceivedMessage> message);
void processDTLSRequirementPacket(QSharedPointer<ReceivedMessage> dtlsRequirementPacket);
void processICEResponsePacket(QSharedPointer<ReceivedMessage> icePacket);
void processDomainServerConnectionDeniedPacket(QSharedPointer<ReceivedMessage> message);
private slots:
void completedHostnameLookup(const QHostInfo& hostInfo);
@ -113,6 +114,8 @@ signals:
void settingsReceived(const QJsonObject& domainSettingsObject);
void settingsReceiveFail();
void domainConnectionRefused(QString reason);
private:
void sendDisconnectPacket();
void hardReset();
@ -130,6 +133,10 @@ private:
QJsonObject _settingsObject;
QString _pendingPath;
QTimer _settingsTimer;
QStringList _domainConnectionRefusals;
bool _hasCheckedForAccessToken { false };
int _connectionDenialsSinceKeypairRegen { 0 };
};
#endif // hifi_DomainHandler_h

View file

@ -902,10 +902,6 @@ void LimitedNodeList::updateLocalSockAddr() {
}
}
void LimitedNodeList::sendHeartbeatToIceServer(const HifiSockAddr& iceServerSockAddr) {
sendPacketToIceServer(PacketType::ICEServerHeartbeat, iceServerSockAddr, _sessionUUID);
}
void LimitedNodeList::sendPeerQueryToIceServer(const HifiSockAddr& iceServerSockAddr, const QUuid& clientID,
const QUuid& peerID) {
sendPacketToIceServer(PacketType::ICEServerQuery, iceServerSockAddr, clientID, peerID);

View file

@ -143,6 +143,7 @@ public:
bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; }
const HifiSockAddr& getLocalSockAddr() const { return _localSockAddr; }
const HifiSockAddr& getPublicSockAddr() const { return _publicSockAddr; }
const HifiSockAddr& getSTUNSockAddr() const { return _stunSockAddr; }
void processKillNode(ReceivedMessage& message);
@ -161,7 +162,6 @@ public:
std::unique_ptr<NLPacket> constructICEPingPacket(PingType_t pingType, const QUuid& iceID);
std::unique_ptr<NLPacket> constructICEPingReplyPacket(ReceivedMessage& message, const QUuid& iceID);
void sendHeartbeatToIceServer(const HifiSockAddr& iceServerSockAddr);
void sendPeerQueryToIceServer(const HifiSockAddr& iceServerSockAddr, const QUuid& clientID, const QUuid& peerID);
SharedNodePointer findNodeWithAddr(const HifiSockAddr& addr);

View file

@ -80,11 +80,16 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned
// send a ping punch immediately
connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer);
auto &accountManager = AccountManager::getInstance();
// assume that we may need to send a new DS check in anytime a new keypair is generated
connect(&accountManager, &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn);
// clear out NodeList when login is finished
connect(&AccountManager::getInstance(), &AccountManager::loginComplete , this, &NodeList::reset);
connect(&accountManager, &AccountManager::loginComplete , this, &NodeList::reset);
// clear our NodeList when logout is requested
connect(&AccountManager::getInstance(), &AccountManager::logoutComplete , this, &NodeList::reset);
connect(&accountManager, &AccountManager::logoutComplete , this, &NodeList::reset);
// anytime we get a new node we will want to attempt to punch to it
connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch);
@ -105,6 +110,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned
packetReceiver.registerListener(PacketType::ICEPing, this, "processICEPingPacket");
packetReceiver.registerListener(PacketType::DomainServerAddedNode, this, "processDomainServerAddedNode");
packetReceiver.registerListener(PacketType::DomainServerConnectionToken, this, "processDomainServerConnectionTokenPacket");
packetReceiver.registerListener(PacketType::DomainConnectionDenied, &_domainHandler, "processDomainServerConnectionDeniedPacket");
packetReceiver.registerListener(PacketType::DomainSettings, &_domainHandler, "processSettingsPacketList");
packetReceiver.registerListener(PacketType::ICEServerPeerInformation, &_domainHandler, "processICEResponsePacket");
packetReceiver.registerListener(PacketType::DomainServerRequireDTLS, &_domainHandler, "processDTLSRequirementPacket");
@ -265,6 +271,26 @@ void NodeList::sendDomainServerCheckIn() {
}
// check if we're missing a keypair we need to verify ourselves with the domain-server
auto& accountManager = AccountManager::getInstance();
const QUuid& connectionToken = _domainHandler.getConnectionToken();
// we assume that we're on the same box as the DS if it has the same local address and
// it didn't present us with a connection token to use for username signature
bool localhostDomain = _domainHandler.getSockAddr().getAddress() == QHostAddress::LocalHost
|| (_domainHandler.getSockAddr().getAddress() == _localSockAddr.getAddress() && connectionToken.isNull());
bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull() && !localhostDomain;
if (requiresUsernameSignature && !accountManager.getAccountInfo().hasPrivateKey()) {
qWarning() << "A keypair is required to present a username signature to the domain-server"
<< "but no keypair is present. Waiting for keypair generation to complete.";
accountManager.generateNewUserKeypair();
// don't send the check in packet - wait for the keypair first
return;
}
auto domainPacket = NLPacket::create(domainPacketType);
QDataStream packetStream(domainPacket.get());
@ -289,23 +315,15 @@ void NodeList::sendDomainServerCheckIn() {
// pack our data to send to the domain-server
packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList();
// if this is a connect request, and we can present a username signature, send it along
if (!_domainHandler.isConnected() ) {
DataServerAccountInfo& accountInfo = AccountManager::getInstance().getAccountInfo();
if (!_domainHandler.isConnected()) {
DataServerAccountInfo& accountInfo = accountManager.getAccountInfo();
packetStream << accountInfo.getUsername();
// get connection token from the domain-server
const QUuid& connectionToken = _domainHandler.getConnectionToken();
if (!connectionToken.isNull()) {
const QByteArray& usernameSignature = AccountManager::getInstance().getAccountInfo().getUsernameSignature(connectionToken);
if (!usernameSignature.isEmpty()) {
packetStream << usernameSignature;
}
// if this is a connect request, and we can present a username signature, send it along
if (requiresUsernameSignature && accountManager.getAccountInfo().hasPrivateKey()) {
const QByteArray& usernameSignature = accountManager.getAccountInfo().getUsernameSignature(connectionToken);
packetStream << usernameSignature;
}
}

View file

@ -85,12 +85,12 @@ void RSAKeypairGenerator::generateKeypair() {
// we can cleanup the RSA struct before we continue on
RSA_free(keyPair);
QByteArray publicKeyArray(reinterpret_cast<char*>(publicKeyDER), publicKeyLength);
QByteArray privateKeyArray(reinterpret_cast<char*>(privateKeyDER), privateKeyLength);
_publicKey = QByteArray { reinterpret_cast<char*>(publicKeyDER), publicKeyLength };
_privateKey = QByteArray { reinterpret_cast<char*>(privateKeyDER), privateKeyLength };
// cleanup the publicKeyDER and publicKeyDER data
OPENSSL_free(publicKeyDER);
OPENSSL_free(privateKeyDER);
emit generatedKeypair(publicKeyArray, privateKeyArray);
emit generatedKeypair();
}

View file

@ -12,17 +12,31 @@
#ifndef hifi_RSAKeypairGenerator_h
#define hifi_RSAKeypairGenerator_h
#include <qobject.h>
#include <QtCore/QObject>
#include <QtCore/QUuid>
class RSAKeypairGenerator : public QObject {
Q_OBJECT
public:
RSAKeypairGenerator(QObject* parent = 0);
void setDomainID(const QUuid& domainID) { _domainID = domainID; }
const QUuid& getDomainID() const { return _domainID; }
const QByteArray& getPublicKey() const { return _publicKey; }
const QByteArray& getPrivateKey() const { return _privateKey; }
public slots:
void generateKeypair();
signals:
void errorGeneratingKeypair();
void generatedKeypair(const QByteArray& publicKey, const QByteArray& privateKey);
void generatedKeypair();
private:
QUuid _domainID;
QByteArray _publicKey;
QByteArray _privateKey;
};
#endif // hifi_RSAKeypairGenerator_h
#endif // hifi_RSAKeypairGenerator_h

View file

@ -30,7 +30,7 @@ const QSet<PacketType> NON_SOURCED_PACKETS = QSet<PacketType>()
<< PacketType::DomainServerAddedNode << PacketType::DomainServerConnectionToken
<< PacketType::DomainSettingsRequest << PacketType::DomainSettings
<< PacketType::ICEServerPeerInformation << PacketType::ICEServerQuery << PacketType::ICEServerHeartbeat
<< PacketType::ICEPing << PacketType::ICEPingReply
<< PacketType::ICEPing << PacketType::ICEPingReply << PacketType::ICEServerHeartbeatDenied
<< PacketType::AssignmentClientStatus << PacketType::StopNode
<< PacketType::DomainServerRemovedNode;
@ -41,10 +41,12 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::EntityAdd:
case PacketType::EntityEdit:
case PacketType::EntityData:
return VERSION_ATMOSPHERE_REMOVED;
return VERSION_LIGHT_HAS_FALLOFF_RADIUS;
case PacketType::AvatarData:
case PacketType::BulkAvatarData:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::SoftAttachmentSupport);
case PacketType::ICEServerHeartbeat:
return 18; // ICE Server Heartbeat signing
default:
return 17;
}

View file

@ -90,7 +90,8 @@ public:
DomainServerRemovedNode,
MessagesData,
MessagesSubscribe,
MessagesUnsubscribe
MessagesUnsubscribe,
ICEServerHeartbeatDenied
};
};
@ -166,6 +167,7 @@ const PacketVersion VERSION_MODEL_ENTITIES_JOINTS_ON_WIRE = 53;
const PacketVersion VERSION_ENTITITES_HAVE_QUERY_BOX = 54;
const PacketVersion VERSION_ENTITITES_HAVE_COLLISION_MASK = 55;
const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56;
const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57;
enum class AvatarMixerPacketVersion : PacketVersion {
TranslationSupport = 17,

View file

@ -1034,7 +1034,7 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element,
// if we are INSIDE, INTERSECT, or OUTSIDE
if (parentLocationThisView != ViewFrustum::INSIDE) {
assert(parentLocationThisView != ViewFrustum::OUTSIDE); // we shouldn't be here if our parent was OUTSIDE!
nodeLocationThisView = element->inFrustum(*params.viewFrustum);
nodeLocationThisView = element->computeViewLocation(*params.viewFrustum);
}
// If we're at a element that is out of view, then we can return, because no nodes below us will be in view!
@ -1053,7 +1053,7 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element,
bool wasInView = false;
if (params.deltaViewFrustum && params.lastViewFrustum) {
ViewFrustum::location location = element->inFrustum(*params.lastViewFrustum);
ViewFrustum::location location = element->computeViewLocation(*params.lastViewFrustum);
// If we're a leaf, then either intersect or inside is considered "formerly in view"
if (element->isLeaf()) {
@ -1237,7 +1237,7 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element,
bool childWasInView = false;
if (childElement && params.deltaViewFrustum && params.lastViewFrustum) {
ViewFrustum::location location = childElement->inFrustum(*params.lastViewFrustum);
ViewFrustum::location location = childElement->computeViewLocation(*params.lastViewFrustum);
// If we're a leaf, then either intersect or inside is considered "formerly in view"
if (childElement->isLeaf()) {

View file

@ -458,8 +458,8 @@ float OctreeElement::getEnclosingRadius() const {
return getScale() * sqrtf(3.0f) / 2.0f;
}
ViewFrustum::location OctreeElement::inFrustum(const ViewFrustum& viewFrustum) const {
return viewFrustum.cubeInFrustum(_cube);
ViewFrustum::location OctreeElement::computeViewLocation(const ViewFrustum& viewFrustum) const {
return viewFrustum.computeCubeViewLocation(_cube);
}
// There are two types of nodes for which we want to "render"

View file

@ -49,20 +49,20 @@ protected:
OctreeElement();
virtual OctreeElementPointer createNewElement(unsigned char * octalCode = NULL) = 0;
public:
virtual void init(unsigned char * octalCode); /// Your subclass must call init on construction.
virtual ~OctreeElement();
// methods you can and should override to implement your tree functionality
/// Adds a child to the current element. Override this if there is additional child initialization your class needs.
virtual OctreeElementPointer addChildAtIndex(int childIndex);
/// Override this to implement LOD averaging on changes to the tree.
/// Override this to implement LOD averaging on changes to the tree.
virtual void calculateAverageFromChildren() { }
/// Override this to implement LOD collapsing and identical child pruning on changes to the tree.
/// Override this to implement LOD collapsing and identical child pruning on changes to the tree.
virtual bool collapseChildren() { return false; }
/// Should this element be considered to have content in it. This will be used in collision and ray casting methods.
@ -72,12 +72,12 @@ public:
/// Should this element be considered to have detailed content in it. Specifically should it be rendered.
/// By default we assume that only leaves have detailed content, but some octrees may have different semantics.
virtual bool hasDetailedContent() const { return isLeaf(); }
/// Override this to break up large octree elements when an edit operation is performed on a smaller octree element.
/// For example, if the octrees represent solid cubes and a delete of a smaller octree element is done then the
/// For example, if the octrees represent solid cubes and a delete of a smaller octree element is done then the
/// meaningful split would be to break the larger cube into smaller cubes of the same color/texture.
virtual void splitChildren() { }
/// Override to indicate that this element requires a split before editing lower elements in the octree
virtual bool requiresSplit() const { return false; }
@ -88,17 +88,17 @@ public:
virtual void initializeExtraEncodeData(EncodeBitstreamParams& params) { }
virtual bool shouldIncludeChildData(int childIndex, EncodeBitstreamParams& params) const { return true; }
virtual bool shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const { return true; }
virtual void updateEncodedData(int childIndex, AppendState childAppendState, EncodeBitstreamParams& params) const { }
virtual void elementEncodeComplete(EncodeBitstreamParams& params) const { }
/// Override to serialize the state of this element. This is used for persistance and for transmission across the network.
virtual AppendState appendElementData(OctreePacketData* packetData, EncodeBitstreamParams& params) const
virtual AppendState appendElementData(OctreePacketData* packetData, EncodeBitstreamParams& params) const
{ return COMPLETED; }
/// Override to deserialize the state of this element. This is used for loading from a persisted file or from reading
/// from the network.
virtual int readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args)
virtual int readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args)
{ return 0; }
/// Override to indicate that the item is currently rendered in the rendering engine. By default we assume that if
@ -106,7 +106,7 @@ public:
/// where an element is not actually rendering all should render elements. If the isRendered() state doesn't match the
/// shouldRender() state, the tree will remark elements as changed even in cases there the elements have not changed.
virtual bool isRendered() const { return getShouldRender(); }
virtual bool deleteApproved() const { return true; }
virtual bool canRayIntersect() const { return isLeaf(); }
@ -114,7 +114,7 @@ public:
/// \param radius radius of sphere in meters
/// \param[out] penetration pointing into cube from sphere
/// \param penetratedObject unused
virtual bool findSpherePenetration(const glm::vec3& center, float radius,
virtual bool findSpherePenetration(const glm::vec3& center, float radius,
glm::vec3& penetration, void** penetratedObject) const;
// Base class methods you don't need to implement
@ -125,7 +125,7 @@ public:
bool isParentOf(OctreeElementPointer possibleChild) const;
/// handles deletion of all descendants, returns false if delete not approved
bool safeDeepDeleteChildAtIndex(int childIndex, int recursionCount = 0);
bool safeDeepDeleteChildAtIndex(int childIndex, int recursionCount = 0);
const AACube& getAACube() const { return _cube; }
@ -134,8 +134,8 @@ public:
int getLevel() const { return numberOfThreeBitSectionsInCode(getOctalCode()) + 1; }
float getEnclosingRadius() const;
bool isInView(const ViewFrustum& viewFrustum) const { return inFrustum(viewFrustum) != ViewFrustum::OUTSIDE; }
ViewFrustum::location inFrustum(const ViewFrustum& viewFrustum) const;
bool isInView(const ViewFrustum& viewFrustum) const { return computeViewLocation(viewFrustum) != ViewFrustum::OUTSIDE; }
ViewFrustum::location computeViewLocation(const ViewFrustum& viewFrustum) const;
float distanceToCamera(const ViewFrustum& viewFrustum) const;
float furthestDistanceToCamera(const ViewFrustum& viewFrustum) const;
@ -257,7 +257,7 @@ protected:
static std::map<QString, uint16_t> _mapSourceUUIDsToKeys;
static std::map<uint16_t, QString> _mapKeysToSourceUUIDs;
unsigned char _childBitmask; // 1 byte
unsigned char _childBitmask; // 1 byte
bool _falseColored : 1, /// Client only, is this voxel false colored, 1 bit
_isDirty : 1, /// Client only, has this voxel changed since being rendered, 1 bit

View file

@ -27,9 +27,9 @@ void OctreeHeadlessViewer::init() {
void OctreeHeadlessViewer::queryOctree() {
char serverType = getMyNodeType();
PacketType packetType = getMyQueryMessageType();
NodeToJurisdictionMap& jurisdictions = *_jurisdictionListener->getJurisdictions();
bool wantExtraDebugging = false;
if (wantExtraDebugging) {
@ -77,7 +77,7 @@ void OctreeHeadlessViewer::queryOctree() {
if (jurisdictions.find(nodeUUID) == jurisdictions.end()) {
unknownJurisdictionServers++;
return;
}
}
const JurisdictionMap& map = (jurisdictions)[nodeUUID];
unsigned char* rootCode = map.getRootOctalCode();
@ -91,7 +91,7 @@ void OctreeHeadlessViewer::queryOctree() {
if (foundRootDetails) {
AACube serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s);
ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds);
ViewFrustum::location serverFrustumLocation = _viewFrustum.computeCubeViewLocation(serverBounds);
if (serverFrustumLocation != ViewFrustum::OUTSIDE) {
inViewServers++;
@ -165,7 +165,7 @@ void OctreeHeadlessViewer::queryOctree() {
if (foundRootDetails) {
AACube serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s);
ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds);
ViewFrustum::location serverFrustumLocation = _viewFrustum.computeCubeViewLocation(serverBounds);
if (serverFrustumLocation != ViewFrustum::OUTSIDE) {
inView = true;
} else {
@ -208,7 +208,7 @@ void OctreeHeadlessViewer::queryOctree() {
// setup the query packet
auto queryPacket = NLPacket::create(packetType);
// read the data to our packet and set the payload size to fit the query
int querySize = _octreeQuery.getBroadcastData(reinterpret_cast<unsigned char*>(queryPacket->getPayload()));
queryPacket->setPayloadSize(querySize);

View file

@ -241,30 +241,18 @@ ViewFrustum::location ViewFrustum::boxInKeyhole(const AABox& box) const {
return result;
}
ViewFrustum::location ViewFrustum::pointInFrustum(const glm::vec3& point, bool ignoreKeyhole) const {
ViewFrustum::location regularResult = INSIDE;
ViewFrustum::location keyholeResult = OUTSIDE;
// If we have a keyholeRadius, check that first, since it's cheaper
if (!ignoreKeyhole && _keyholeRadius >= 0.0f) {
keyholeResult = pointInKeyhole(point);
if (keyholeResult == INSIDE) {
return keyholeResult;
}
}
// If we're not known to be INSIDE the keyhole, then check the regular frustum
ViewFrustum::location ViewFrustum::computePointFrustumLocation(const glm::vec3& point) const {
// only checks against frustum, not sphere
for(int i = 0; i < 6; ++i) {
float distance = _planes[i].distance(point);
if (distance < 0) {
return keyholeResult; // escape early will be the value from checking the keyhole
if (distance < 0.0f) {
return OUTSIDE;
}
}
return regularResult;
return INSIDE;
}
ViewFrustum::location ViewFrustum::sphereInFrustum(const glm::vec3& center, float radius) const {
ViewFrustum::location ViewFrustum::computeSphereViewLocation(const glm::vec3& center, float radius) const {
ViewFrustum::location regularResult = INSIDE;
ViewFrustum::location keyholeResult = OUTSIDE;
@ -291,7 +279,7 @@ ViewFrustum::location ViewFrustum::sphereInFrustum(const glm::vec3& center, floa
}
ViewFrustum::location ViewFrustum::cubeInFrustum(const AACube& cube) const {
ViewFrustum::location ViewFrustum::computeCubeViewLocation(const AACube& cube) const {
ViewFrustum::location regularResult = INSIDE;
ViewFrustum::location keyholeResult = OUTSIDE;
@ -326,7 +314,7 @@ ViewFrustum::location ViewFrustum::cubeInFrustum(const AACube& cube) const {
return regularResult;
}
ViewFrustum::location ViewFrustum::boxInFrustum(const AABox& box) const {
ViewFrustum::location ViewFrustum::computeBoxViewLocation(const AABox& box) const {
ViewFrustum::location regularResult = INSIDE;
ViewFrustum::location keyholeResult = OUTSIDE;
@ -490,7 +478,7 @@ PickRay ViewFrustum::computePickRay(float x, float y) {
}
void ViewFrustum::computePickRay(float x, float y, glm::vec3& origin, glm::vec3& direction) const {
origin = _cornersWorld[TOP_LEFT_NEAR] + x * (_cornersWorld[TOP_RIGHT_NEAR] - _cornersWorld[TOP_LEFT_NEAR]) +
origin = _cornersWorld[TOP_LEFT_NEAR] + x * (_cornersWorld[TOP_RIGHT_NEAR] - _cornersWorld[TOP_LEFT_NEAR]) +
y * (_cornersWorld[BOTTOM_LEFT_NEAR] - _cornersWorld[TOP_LEFT_NEAR]);
direction = glm::normalize(origin - _position);
}
@ -804,7 +792,7 @@ float ViewFrustum::calculateRenderAccuracy(const AABox& bounds, float octreeSize
// FIXME - for now, it's either visible or not visible. We want to adjust this to eventually return
// a floating point for objects that have small angular size to indicate that they may be rendered
// with lower preciscion
return (distanceToCamera <= visibleDistanceAtClosestScale) ? 1.0f : 0.0f;
return (distanceToCamera <= visibleDistanceAtClosestScale) ? 1.0f : 0.0f;
}
float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeScale) {

View file

@ -91,10 +91,11 @@ public:
typedef enum {OUTSIDE, INTERSECT, INSIDE} location;
ViewFrustum::location pointInFrustum(const glm::vec3& point, bool ignoreKeyhole = false) const;
ViewFrustum::location sphereInFrustum(const glm::vec3& center, float radius) const;
ViewFrustum::location cubeInFrustum(const AACube& cube) const;
ViewFrustum::location boxInFrustum(const AABox& box) const;
ViewFrustum::location computePointFrustumLocation(const glm::vec3& point) const;
ViewFrustum::location computeSphereViewLocation(const glm::vec3& center, float radius) const;
ViewFrustum::location computeCubeViewLocation(const AACube& cube) const;
ViewFrustum::location computeBoxViewLocation(const AABox& box) const;
// some frustum comparisons
bool matches(const ViewFrustum& compareTo, bool debug = false) const;
@ -114,15 +115,15 @@ public:
glm::vec2 projectPoint(glm::vec3 point, bool& pointInView) const;
OctreeProjectedPolygon getProjectedPolygon(const AACube& box) const;
void getFurthestPointFromCamera(const AACube& box, glm::vec3& furthestPoint) const;
float distanceToCamera(const glm::vec3& point) const;
void evalProjectionMatrix(glm::mat4& proj) const;
void evalViewTransform(Transform& view) const;
/// renderAccuracy represents a floating point "visibility" of an object based on it's view from the camera. At a simple
/// level it returns 0.0f for things that are so small for the current settings that they could not be visible.
float calculateRenderAccuracy(const AABox& bounds, float octreeSizeScale = DEFAULT_OCTREE_SIZE_SCALE,
float calculateRenderAccuracy(const AABox& bounds, float octreeSizeScale = DEFAULT_OCTREE_SIZE_SCALE,
int boundaryLevelAdjust = 0) const;
float getAccuracyAngle(float octreeSizeScale = DEFAULT_OCTREE_SIZE_SCALE, int boundaryLevelAdjust = 0) const;

View file

@ -98,12 +98,12 @@ void DeferredLightingEffect::init() {
}
void DeferredLightingEffect::addPointLight(const glm::vec3& position, float radius, const glm::vec3& color,
float intensity) {
addSpotLight(position, radius, color, intensity);
float intensity, float falloffRadius) {
addSpotLight(position, radius, color, intensity, falloffRadius);
}
void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color,
float intensity, const glm::quat& orientation, float exponent, float cutoff) {
float intensity, float falloffRadius, const glm::quat& orientation, float exponent, float cutoff) {
unsigned int lightID = (unsigned int)(_pointLights.size() + _spotLights.size() + _globalLights.size());
if (lightID >= _allocatedLights.size()) {
@ -115,7 +115,7 @@ void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radiu
lp->setMaximumRadius(radius);
lp->setColor(color);
lp->setIntensity(intensity);
//lp->setShowContour(quadraticAttenuation);
lp->setFalloffRadius(falloffRadius);
if (exponent == 0.0f && cutoff == PI) {
lp->setType(model::Light::POINT);
@ -312,15 +312,13 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo
// First Global directional light and ambient pass
{
bool useSkyboxCubemap = (_skybox) && (_skybox->getCubemap());
auto& program = _shadowMapEnabled ? _directionalLightShadow : _directionalLight;
LightLocationsPtr locations = _shadowMapEnabled ? _directionalLightShadowLocations : _directionalLightLocations;
// Setup the global directional pass pipeline
{
if (_shadowMapEnabled) {
if (useSkyboxCubemap) {
if (_skyboxTexture) {
program = _directionalSkyboxLightShadow;
locations = _directionalSkyboxLightShadowLocations;
} else if (_ambientLightMode > -1) {
@ -328,7 +326,7 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo
locations = _directionalAmbientSphereLightShadowLocations;
}
} else {
if (useSkyboxCubemap) {
if (_skyboxTexture) {
program = _directionalSkyboxLight;
locations = _directionalSkyboxLightLocations;
} else if (_ambientLightMode > -1) {
@ -356,7 +354,7 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo
geometryCache->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color);
}
if (useSkyboxCubemap) {
if (_skyboxTexture) {
batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr);
}
}
@ -501,9 +499,8 @@ void DeferredLightingEffect::setupKeyLightBatch(gpu::Batch& batch, int lightBuff
batch.setUniformBuffer(lightBufferUnit, globalLight->getSchemaBuffer());
}
bool useSkyboxCubemap = (_skybox) && (_skybox->getCubemap());
if (useSkyboxCubemap && (skyboxCubemapUnit >= 0)) {
batch.setResourceTexture(skyboxCubemapUnit, _skybox->getCubemap());
if (_skyboxTexture && (skyboxCubemapUnit >= 0)) {
batch.setResourceTexture(skyboxCubemapUnit, _skyboxTexture);
}
}
@ -562,32 +559,9 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo
}
void DeferredLightingEffect::setAmbientLightMode(int preset) {
if ((preset >= 0) && (preset < gpu::SphericalHarmonics::NUM_PRESET)) {
_ambientLightMode = preset;
auto light = _allocatedLights.front();
light->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset(preset % gpu::SphericalHarmonics::NUM_PRESET));
} else {
// force to preset 0
setAmbientLightMode(0);
}
}
void DeferredLightingEffect::setGlobalLight(const glm::vec3& direction, const glm::vec3& diffuse, float intensity, float ambientIntensity) {
auto light = _allocatedLights.front();
light->setDirection(direction);
light->setColor(diffuse);
light->setIntensity(intensity);
light->setAmbientIntensity(ambientIntensity);
}
void DeferredLightingEffect::setGlobalSkybox(const model::SkyboxPointer& skybox) {
_skybox = skybox;
auto light = _allocatedLights.front();
if (_skybox && _skybox->getCubemap() && _skybox->getCubemap()->isDefined() && _skybox->getCubemap()->getIrradiance()) {
light->setAmbientSphere( (*_skybox->getCubemap()->getIrradiance()) );
}
void DeferredLightingEffect::setGlobalLight(const model::LightPointer& light, const gpu::TexturePointer& skyboxTexture) {
_allocatedLights.front() = light;
_skyboxTexture = skyboxTexture;
}
model::MeshPointer DeferredLightingEffect::getSpotLightMesh() {

View file

@ -18,7 +18,6 @@
#include <NumericalConstants.h>
#include "model/Light.h"
#include "model/Stage.h"
#include "model/Geometry.h"
#include "render/Context.h"
@ -37,11 +36,12 @@ public:
/// Adds a point light to render for the current frame.
void addPointLight(const glm::vec3& position, float radius, const glm::vec3& color = glm::vec3(0.0f, 0.0f, 0.0f),
float intensity = 0.5f);
float intensity = 0.5f, float falloffRadius = 0.01f);
/// Adds a spot light to render for the current frame.
void addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color = glm::vec3(1.0f, 1.0f, 1.0f),
float intensity = 0.5f, const glm::quat& orientation = glm::quat(), float exponent = 0.0f, float cutoff = PI);
float intensity = 0.5f, float falloffRadius = 0.01f,
const glm::quat& orientation = glm::quat(), float exponent = 0.0f, float cutoff = PI);
void prepare(RenderArgs* args);
void render(const render::RenderContextPointer& renderContext);
@ -49,9 +49,7 @@ public:
void setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int skyboxCubemapUnit);
// update global lighting
void setAmbientLightMode(int preset);
void setGlobalLight(const glm::vec3& direction, const glm::vec3& diffuse, float intensity, float ambientIntensity);
void setGlobalSkybox(const model::SkyboxPointer& skybox);
void setGlobalLight(const model::LightPointer& light, const gpu::TexturePointer& skyboxTexture);
const LightStage& getLightStage() { return _lightStage; }
void setShadowMapEnabled(bool enable) { _shadowMapEnabled = enable; };
@ -98,7 +96,7 @@ private:
std::vector<int> _spotLights;
int _ambientLightMode = 0;
model::SkyboxPointer _skybox;
gpu::TexturePointer _skyboxTexture;
// Class describing the uniform buffer with all the parameters common to the deferred shaders
class DeferredTransform {

View file

@ -413,9 +413,9 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const {
void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const {
if (!_isBlendShaped) {
batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0);
batch.setInputFormat((_drawMesh->getVertexFormat()));
batch.setInputStream(0, _drawMesh->getVertexStream());
} else {
batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0);
@ -426,7 +426,7 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const {
batch.setInputBuffer(1, _model->_blendedVertexBuffers[_meshIndex], _drawMesh->getNumVertices() * sizeof(glm::vec3), sizeof(glm::vec3));
batch.setInputStream(2, _drawMesh->getVertexStream().makeRangedStream(2));
}
// TODO: Get rid of that extra call
if (!_hasColorAttrib) {
batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
@ -474,8 +474,8 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
#ifdef DEBUG_BOUNDING_PARTS
{
AABox partBounds = getPartBounds(_meshIndex, partIndex);
bool inView = args->_viewFrustum->boxInFrustum(partBounds) != ViewFrustum::OUTSIDE;
bool inView = args->_viewFrustum->computeBoxViewLocation(partBounds) != ViewFrustum::OUTSIDE;
glm::vec4 cubeColor;
if (isSkinned) {
cubeColor = glm::vec4(0.0f, 1.0f, 1.0f, 1.0f);
@ -484,7 +484,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
} else {
cubeColor = glm::vec4(1.0f, 1.0f, 0.0f, 1.0f);
}
Transform transform;
transform.setTranslation(partBounds.calcCenter());
transform.setScale(partBounds.getDimensions());
@ -492,7 +492,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
DependencyManager::get<GeometryCache>()->renderWireCube(batch, 1.0f, cubeColor);
}
#endif //def DEBUG_BOUNDING_PARTS
auto locations = args->_pipeline->locations;
assert(locations);
@ -500,23 +500,23 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
bool canCauterize = args->_renderMode != RenderArgs::SHADOW_RENDER_MODE;
_model->updateClusterMatrices(_transform.getTranslation(), _transform.getRotation());
bindTransform(batch, locations, canCauterize);
//Bind the index buffer and vertex buffer and Blend shapes if needed
bindMesh(batch);
// apply material properties
bindMaterial(batch, locations);
if (args) {
args->_details._materialSwitches++;
}
// Draw!
{
PerformanceTimer perfTimer("batch.drawIndexed()");
drawCall(batch);
}
if (args) {
const int INDICES_PER_TRIANGLE = 3;
args->_details._trianglesRendered += _drawPart._numIndices / INDICES_PER_TRIANGLE;

View file

@ -48,7 +48,7 @@ void main(void) {
vec3 fragLightVec = getLightPosition(light) - fragPos.xyz;
// Kill if too far from the light center
if (dot(fragLightVec, fragLightVec) > getLightSquareRadius(light)) {
if (dot(fragLightVec, fragLightVec) > getLightCutoffSquareRadius(light)) {
discard;
}

View file

@ -47,7 +47,7 @@ void main(void) {
vec3 fragLightVec = getLightPosition(light) - fragPos.xyz;
// Kill if too far from the light center
if (dot(fragLightVec, fragLightVec) > getLightSquareRadius(light)) {
if (dot(fragLightVec, fragLightVec) > getLightCutoffSquareRadius(light)) {
discard;
}

View file

@ -29,7 +29,7 @@ void render::cullItems(const RenderContextPointer& renderContext, const CullFunc
ViewFrustum* frustum = args->_viewFrustum;
details._considered += (int)inItems.size();
// Culling / LOD
for (auto item : inItems) {
if (item.bound.isNull()) {
@ -41,8 +41,8 @@ void render::cullItems(const RenderContextPointer& renderContext, const CullFunc
// when they are outside of the view frustum...
bool outOfView;
{
PerformanceTimer perfTimer("boxInFrustum");
outOfView = frustum->boxInFrustum(item.bound) == ViewFrustum::OUTSIDE;
PerformanceTimer perfTimer("computeBoxViewLocation");
outOfView = frustum->computeBoxViewLocation(item.bound) == ViewFrustum::OUTSIDE;
}
if (!outOfView) {
bool bigEnoughToRender;
@ -88,10 +88,10 @@ struct BackToFrontSort {
void render::depthSortItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, bool frontToBack, const ItemBounds& inItems, ItemBounds& outItems) {
assert(renderContext->args);
assert(renderContext->args->_viewFrustum);
auto& scene = sceneContext->_scene;
RenderArgs* args = renderContext->args;
// Allocate and simply copy
outItems.clear();
@ -237,8 +237,8 @@ void CullSpatialSelection::run(const SceneContextPointer& sceneContext, const Re
*/
}
bool frustumTest(const AABox& bound) {
if (_args->_viewFrustum->boxInFrustum(bound) == ViewFrustum::OUTSIDE) {
bool viewTest(const AABox& bound) {
if (_args->_viewFrustum->computeBoxViewLocation(bound) == ViewFrustum::OUTSIDE) {
_renderDetails._outOfView++;
return false;
}
@ -302,7 +302,7 @@ void CullSpatialSelection::run(const SceneContextPointer& sceneContext, const Re
auto& item = scene->getItem(id);
if (_filter.test(item.getKey())) {
ItemBound itemBound(id, item.getBound());
if (test.frustumTest(itemBound.bound)) {
if (test.viewTest(itemBound.bound)) {
outItems.emplace_back(itemBound);
}
}
@ -316,7 +316,7 @@ void CullSpatialSelection::run(const SceneContextPointer& sceneContext, const Re
auto& item = scene->getItem(id);
if (_filter.test(item.getKey())) {
ItemBound itemBound(id, item.getBound());
if (test.frustumTest(itemBound.bound)) {
if (test.viewTest(itemBound.bound)) {
if (test.solidAngleTest(itemBound.bound)) {
outItems.emplace_back(itemBound);
}

View file

@ -77,6 +77,10 @@ void SceneScripting::KeyLight::setAmbientIntensity(float intensity) {
_skyStage->setSunAmbientIntensity(intensity);
}
void SceneScripting::KeyLight::setAmbientSphere(const gpu::SHPointer& sphere) {
_skyStage->setSunAmbientSphere(sphere);
}
glm::vec3 SceneScripting::KeyLight::getDirection() const {
return _skyStage->getSunDirection();
}

View file

@ -81,6 +81,10 @@ namespace SceneScripting {
// setDirection is only effective if stage Sun model is disabled
void setDirection(const glm::vec3& direction);
// AmbientTexture is unscriptable - it must be set through the zone entity
void setAmbientSphere(const gpu::SHPointer& sphere);
void resetAmbientSphere() { setAmbientSphere(nullptr); }
protected:
model::SunSkyStagePointer _skyStage;
};

View file

@ -26,9 +26,7 @@ LogHandler& LogHandler::getInstance() {
return staticInstance;
}
LogHandler::LogHandler() :
_shouldOutputProcessID(false),
_shouldOutputThreadID(false)
LogHandler::LogHandler()
{
// setup our timer to flush the verbose logs every 5 seconds
QTimer* logFlushTimer = new QTimer(this);
@ -62,6 +60,9 @@ const char* stringForLogType(LogMsgType msgType) {
// the following will produce 11/18 13:55:36
const QString DATE_STRING_FORMAT = "MM/dd hh:mm:ss";
// the following will produce 11/18 13:55:36.999
const QString DATE_STRING_FORMAT_WITH_MILLISECONDS = "MM/dd hh:mm:ss.zzz";
void LogHandler::flushRepeatedMessages() {
QMutexLocker locker(&_repeatedMessageLock);
QHash<QString, int>::iterator message = _repeatMessageCountHash.begin();
@ -132,7 +133,12 @@ QString LogHandler::printMessage(LogMsgType type, const QMessageLogContext& cont
// log prefix is in the following format
// [TIMESTAMP] [DEBUG] [PID] [TID] [TARGET] logged string
QString prefixString = QString("[%1]").arg(QDateTime::currentDateTime().toString(DATE_STRING_FORMAT));
const QString* dateFormatPtr = &DATE_STRING_FORMAT;
if (_shouldDisplayMilliseconds) {
dateFormatPtr = &DATE_STRING_FORMAT_WITH_MILLISECONDS;
}
QString prefixString = QString("[%1]").arg(QDateTime::currentDateTime().toString(*dateFormatPtr));
prefixString.append(QString(" [%1]").arg(stringForLogType(type)));

View file

@ -42,6 +42,7 @@ public:
void setShouldOutputProcessID(bool shouldOutputProcessID) { _shouldOutputProcessID = shouldOutputProcessID; }
void setShouldOutputThreadID(bool shouldOutputThreadID) { _shouldOutputThreadID = shouldOutputThreadID; }
void setShouldDisplayMilliseconds(bool shouldDisplayMilliseconds) { _shouldDisplayMilliseconds = shouldDisplayMilliseconds; }
QString printMessage(LogMsgType type, const QMessageLogContext& context, const QString &message);
@ -57,8 +58,9 @@ private:
void flushRepeatedMessages();
QString _targetName;
bool _shouldOutputProcessID;
bool _shouldOutputThreadID;
bool _shouldOutputProcessID { false };
bool _shouldOutputThreadID { false };
bool _shouldDisplayMilliseconds { false };
QSet<QString> _repeatedMessageRegexes;
QHash<QString, int> _repeatMessageCountHash;
QHash<QString, QString> _lastRepeatedMessage;