merge with master

This commit is contained in:
HifiExperiments 2019-11-02 01:08:56 -07:00
commit bae2ba7bae
165 changed files with 19367 additions and 3961 deletions

1
.gitignore vendored
View file

@ -92,6 +92,7 @@ npm-debug.log
# Resource binary file
interface/compiledResources
*.rcc
# GPUCache
interface/resources/GPUCache/*

View file

@ -376,7 +376,6 @@ void Agent::executeScript() {
// setup an Avatar for the script to use
auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
scriptedAvatar->setID(getSessionUUID());
scriptedAvatar->setForceFaceTrackerConnected(true);
// call model URL setters with empty URLs so our avatar, if user, will have the default models
scriptedAvatar->setSkeletonModelURL(QUrl());

View file

@ -32,24 +32,42 @@ MixerAvatar::MixerAvatar() {
_challengeTimer.setSingleShot(true);
_challengeTimer.setInterval(CHALLENGE_TIMEOUT_MS);
_challengeTimer.callOnTimeout(this, [this]() {
if (_verifyState == challengeClient) {
_pendingEvent = false;
_verifyState = verificationFailed;
_needsIdentityUpdate = true;
qCDebug(avatars) << "Dynamic verification TIMED-OUT for" << getDisplayName() << getSessionUUID();
} else {
qCDebug(avatars) << "Ignoring timeout of avatar challenge";
}
});
_challengeTimer.callOnTimeout(this, &MixerAvatar::challengeTimeout);
// QTimer::start is a set of overloaded functions.
connect(this, &MixerAvatar::startChallengeTimer, &_challengeTimer, static_cast<void(QTimer::*)()>(&QTimer::start));
}
const char* MixerAvatar::stateToName(VerifyState state) {
return QMetaEnum::fromType<VerifyState>().valueToKey(state);
}
void MixerAvatar::challengeTimeout() {
switch (_verifyState) {
case challengeClient:
_verifyState = staticValidation;
_pendingEvent = true;
if (++_numberChallenges < NUM_CHALLENGES_BEFORE_FAIL) {
qCDebug(avatars) << "Retrying (" << _numberChallenges << ") timed-out challenge for" << getDisplayName()
<< getSessionUUID();
} else {
_certifyFailed = true;
_needsIdentityUpdate = true;
qCWarning(avatars) << "ALERT: Dynamic verification TIMED-OUT for" << getDisplayName() << getSessionUUID();
}
break;
case verificationFailed:
qCDebug(avatars) << "Retrying failed challenge for" << getDisplayName() << getSessionUUID();
_verifyState = staticValidation;
_pendingEvent = true;
break;
default:
qCDebug(avatars) << "Ignoring timeout of avatar challenge";
break;
}
}
void MixerAvatar::fetchAvatarFST() {
if (_verifyState >= requestingFST && _verifyState <= challengeClient) {
qCDebug(avatars) << "WARNING: Avatar verification restarted; old state:" << stateToName(_verifyState);
@ -58,7 +76,7 @@ void MixerAvatar::fetchAvatarFST() {
_pendingEvent = false;
QUrl avatarURL = getSkeletonModelURL();
QUrl avatarURL = _skeletonModelURL;
if (avatarURL.isEmpty() || avatarURL.isLocalFile() || avatarURL.scheme() == "qrc") {
// Not network FST.
return;
@ -210,6 +228,23 @@ void MixerAvatar::ownerRequestComplete() {
networkReply->deleteLater();
}
void MixerAvatar::requestCurrentOwnership() {
// Get registered owner's public key from metaverse.
static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
requestURL.setPath(POP_MARKETPLACE_API);
networkRequest.setUrl(requestURL);
QJsonObject request;
request["certificate_id"] = _certificateIdFromFST;
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete);
}
void MixerAvatar::processCertifyEvents() {
if (!_pendingEvent) {
return;
@ -221,26 +256,15 @@ void MixerAvatar::processCertifyEvents() {
case receivedFST:
{
generateFSTHash();
_numberChallenges = 0;
if (_certificateIdFromFST.length() != 0) {
QString& marketplacePublicKey = EntityItem::_marketplacePublicKey;
bool staticVerification = validateFSTHash(marketplacePublicKey);
_verifyState = staticVerification ? staticValidation : verificationFailed;
if (_verifyState == staticValidation) {
static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
requestURL.setPath(POP_MARKETPLACE_API);
networkRequest.setUrl(requestURL);
QJsonObject request;
request["certificate_id"] = _certificateIdFromFST;
requestCurrentOwnership();
_verifyState = requestingOwner;
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete);
} else {
_needsIdentityUpdate = true;
_pendingEvent = false;
@ -248,11 +272,21 @@ void MixerAvatar::processCertifyEvents() {
}
} else { // FST doesn't have a certificate, so noncertified rather than failed:
_pendingEvent = false;
_certifyFailed = false;
_needsIdentityUpdate = true;
_verifyState = nonCertified;
qCDebug(avatars) << "Avatar " << getDisplayName() << "(" << getSessionUUID() << ") isn't certified";
}
break;
}
case staticValidation:
{
requestCurrentOwnership();
_verifyState = requestingOwner;
break;
}
case ownerResponse:
{
QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8());
@ -310,23 +344,28 @@ void MixerAvatar::processCertifyEvents() {
void MixerAvatar::sendOwnerChallenge() {
auto nodeList = DependencyManager::get<NodeList>();
QByteArray avatarID = ("{" + _marketplaceIdFromFST + "}").toUtf8();
QByteArray nonce = QUuid::createUuid().toByteArray();
if (_challengeNonce.isEmpty()) {
_challengeNonce = QUuid::createUuid().toByteArray();
QCryptographicHash nonceHash(QCryptographicHash::Sha256);
nonceHash.addData(_challengeNonce);
_challengeNonceHash = nonceHash.result();
}
auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership,
2 * sizeof(int) + nonce.length() + avatarID.length(), true);
2 * sizeof(int) + _challengeNonce.length() + avatarID.length(), true);
challengeOwnershipPacket->writePrimitive(avatarID.length());
challengeOwnershipPacket->writePrimitive(nonce.length());
challengeOwnershipPacket->writePrimitive(_challengeNonce.length());
challengeOwnershipPacket->write(avatarID);
challengeOwnershipPacket->write(nonce);
challengeOwnershipPacket->write(_challengeNonce);
nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(getSessionUUID())) );
QCryptographicHash nonceHash(QCryptographicHash::Sha256);
nonceHash.addData(nonce);
nonceHash.addData(_challengeNonce);
_challengeNonceHash = nonceHash.result();
_pendingEvent = false;
// QTimer::start is a set of overloaded functions.
QMetaObject::invokeMethod(&_challengeTimer, static_cast<void(QTimer::*)()>(&QTimer::start));
emit startChallengeTimer();
}
void MixerAvatar::processChallengeResponse(ReceivedMessage& response) {
@ -337,7 +376,7 @@ void MixerAvatar::processChallengeResponse(ReceivedMessage& response) {
QByteArray responseData = response.readAll();
if (responseData.length() < 8) {
_verifyState = error;
qCDebug(avatars) << "Avatar challenge response packet too small, length:" << responseData.length();
qCWarning(avatars) << "ALERT: Avatar challenge response packet too small, length:" << responseData.length();
return;
}
@ -354,11 +393,14 @@ void MixerAvatar::processChallengeResponse(ReceivedMessage& response) {
bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash,
QByteArray::fromBase64(signedNonce));
_verifyState = challengeResult ? verificationSucceeded : verificationFailed;
_certifyFailed = !challengeResult;
_needsIdentityUpdate = true;
if (_verifyState == verificationFailed) {
if (_certifyFailed) {
qCDebug(avatars) << "Dynamic verification FAILED for" << getDisplayName() << getSessionUUID();
emit startChallengeTimer();
} else {
qCDebug(avatars) << "Dynamic verification SUCCEEDED for" << getDisplayName() << getSessionUUID();
_challengeNonce.clear();
}
} else {

View file

@ -27,7 +27,7 @@ public:
void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; }
void fetchAvatarFST();
virtual bool isCertifyFailed() const override { return _verifyState == verificationFailed; }
virtual bool isCertifyFailed() const override { return _certifyFailed; }
bool needsIdentityUpdate() const { return _needsIdentityUpdate; }
void setNeedsIdentityUpdate(bool value = true) { _needsIdentityUpdate = value; }
@ -58,13 +58,18 @@ private:
QString _certificateIdFromFST;
QString _dynamicMarketResponse;
QString _ownerPublicKey;
QByteArray _challengeNonce;
QByteArray _challengeNonceHash;
QTimer _challengeTimer;
static constexpr int NUM_CHALLENGES_BEFORE_FAIL = 1;
int _numberChallenges { 0 };
bool _certifyFailed { false };
bool _needsIdentityUpdate { false };
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey) const;
QByteArray canonicalJson(const QString fstFile);
void requestCurrentOwnership();
void sendOwnerChallenge();
static const QString VERIFY_FAIL_MODEL;
@ -72,6 +77,10 @@ private:
private slots:
void fstRequestComplete();
void ownerRequestComplete();
void challengeTimeout();
signals:
void startChallengeTimer();
};
using MixerAvatarSharedPointer = std::shared_ptr<MixerAvatar>;

View file

@ -279,18 +279,6 @@ void ScriptableAvatar::setJointMappingsFromNetworkReply() {
networkReply->deleteLater();
}
void ScriptableAvatar::setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement) {
_headData->setHasProceduralBlinkFaceMovement(hasProceduralBlinkFaceMovement);
}
void ScriptableAvatar::setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement) {
_headData->setHasProceduralEyeFaceMovement(hasProceduralEyeFaceMovement);
}
void ScriptableAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) {
_headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement);
}
AvatarEntityMap ScriptableAvatar::getAvatarEntityData() const {
// DANGER: Now that we store the AvatarEntityData in packed format this call is potentially Very Expensive!
// Avoid calling this method if possible.

View file

@ -153,13 +153,6 @@ public:
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override;
void setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement);
bool getHasProceduralBlinkFaceMovement() const override { return _headData->getHasProceduralBlinkFaceMovement(); }
void setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement);
bool getHasProceduralEyeFaceMovement() const override { return _headData->getHasProceduralEyeFaceMovement(); }
void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement);
bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); }
/**jsdoc
* Gets details of all avatar entities.
* <p><strong>Warning:</strong> Potentially an expensive call. Do not use if possible.</p>

View file

@ -273,8 +273,10 @@ endif()
url = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/qt5-install-5.12.3-ubuntu-18.04.tar.gz'
else:
print('UNKNOWN LINUX VERSION!!!')
return;
else:
print('UNKNOWN OPERATING SYSTEM!!!')
return;
print('Extracting ' + url + ' to ' + dest)
hifi_utils.downloadAndExtract(url, dest)

View file

@ -211,10 +211,10 @@ endif()
link_hifi_libraries(
shared workload task octree ktx gpu gl procedural graphics graphics-scripting render
pointers recording hfm fbx networking material-networking
model-networking model-baker entities avatars trackers
model-networking model-baker entities avatars
audio audio-client animation script-engine physics
render-utils entities-renderer avatars-renderer ui qml auto-updater midi
controllers plugins image trackers platform
controllers plugins image platform
ui-plugins display-plugins input-plugins
# Platform specific GL libraries
${PLATFORM_GL_BACKEND}

File diff suppressed because it is too large Load diff

View file

@ -166,9 +166,70 @@
{ "from": "Standard.LeftEye", "to": "Actions.LeftEye" },
{ "from": "Standard.RightEye", "to": "Actions.RightEye" },
{ "from": "Standard.LeftEyeBlink", "to": "Actions.LeftEyeBlink" },
{ "from": "Standard.RightEyeBlink", "to": "Actions.RightEyeBlink" },
{ "from": "Standard.EyeBlink_L", "to": "Actions.EyeBlink_L" },
{ "from": "Standard.EyeBlink_R", "to": "Actions.EyeBlink_R" },
{ "from": "Standard.EyeSquint_L", "to": "Actions.EyeSquint_L" },
{ "from": "Standard.EyeSquint_R", "to": "Actions.EyeSquint_R" },
{ "from": "Standard.EyeDown_L", "to": "Actions.EyeDown_L" },
{ "from": "Standard.EyeDown_R", "to": "Actions.EyeDown_R" },
{ "from": "Standard.EyeIn_L", "to": "Actions.EyeIn_L" },
{ "from": "Standard.EyeIn_R", "to": "Actions.EyeIn_R" },
{ "from": "Standard.EyeOpen_L", "to": "Actions.EyeOpen_L" },
{ "from": "Standard.EyeOpen_R", "to": "Actions.EyeOpen_R" },
{ "from": "Standard.EyeOut_L", "to": "Actions.EyeOut_L" },
{ "from": "Standard.EyeOut_R", "to": "Actions.EyeOut_R" },
{ "from": "Standard.EyeUp_L", "to": "Actions.EyeUp_L" },
{ "from": "Standard.EyeUp_R", "to": "Actions.EyeUp_R" },
{ "from": "Standard.BrowsD_L", "to": "Actions.BrowsD_L" },
{ "from": "Standard.BrowsD_R", "to": "Actions.BrowsD_R" },
{ "from": "Standard.BrowsU_C", "to": "Actions.BrowsU_C" },
{ "from": "Standard.BrowsU_L", "to": "Actions.BrowsU_L" },
{ "from": "Standard.BrowsU_R", "to": "Actions.BrowsU_R" },
{ "from": "Standard.JawFwd", "to": "Actions.JawFwd" },
{ "from": "Standard.JawLeft", "to": "Actions.JawLeft" },
{ "from": "Standard.JawOpen", "to": "Actions.JawOpen" },
{ "from": "Standard.JawRight", "to": "Actions.JawRight" },
{ "from": "Standard.MouthLeft", "to": "Actions.MouthLeft" },
{ "from": "Standard.MouthRight", "to": "Actions.MouthRight" },
{ "from": "Standard.MouthFrown_L", "to": "Actions.MouthFrown_L" },
{ "from": "Standard.MouthFrown_R", "to": "Actions.MouthFrown_R" },
{ "from": "Standard.MouthSmile_L", "to": "Actions.MouthSmile_L" },
{ "from": "Standard.MouthSmile_R", "to": "Actions.MouthSmile_R" },
{ "from": "Standard.MouthDimple_L", "to": "Actions.MouthDimple_L" },
{ "from": "Standard.MouthDimple_R", "to": "Actions.MouthDimple_R" },
{ "from": "Standard.LipsStretch_L", "to": "Actions.LipsStretch_L" },
{ "from": "Standard.LipsStretch_R", "to": "Actions.LipsStretch_R" },
{ "from": "Standard.LipsUpperClose", "to": "Actions.LipsUpperClose" },
{ "from": "Standard.LipsLowerClose", "to": "Actions.LipsLowerClose" },
{ "from": "Standard.LipsUpperOpen", "to": "Actions.LipsUpperOpen" },
{ "from": "Standard.LipsLowerOpen", "to": "Actions.LipsLowerOpen" },
{ "from": "Standard.LipsFunnel", "to": "Actions.LipsFunnel" },
{ "from": "Standard.LipsPucker", "to": "Actions.LipsPucker" },
{ "from": "Standard.Puff", "to": "Actions.Puff" },
{ "from": "Standard.CheekSquint_L", "to": "Actions.CheekSquint_L" },
{ "from": "Standard.CheekSquint_R", "to": "Actions.CheekSquint_R" },
{ "from": "Standard.MouthClose", "to": "Actions.MouthClose" },
{ "from": "Standard.MouthUpperUp_L", "to": "Actions.MouthUpperUp_L" },
{ "from": "Standard.MouthUpperUp_R", "to": "Actions.MouthUpperUp_R" },
{ "from": "Standard.MouthLowerDown_L", "to": "Actions.MouthLowerDown_L" },
{ "from": "Standard.MouthLowerDown_R", "to": "Actions.MouthLowerDown_R" },
{ "from": "Standard.MouthPress_L", "to": "Actions.MouthPress_L" },
{ "from": "Standard.MouthPress_R", "to": "Actions.MouthPress_R" },
{ "from": "Standard.MouthShrugLower", "to": "Actions.MouthShrugLower" },
{ "from": "Standard.MouthShrugUpper", "to": "Actions.MouthShrugUpper" },
{ "from": "Standard.NoseSneer_L", "to": "Actions.NoseSneer_L" },
{ "from": "Standard.NoseSneer_R", "to": "Actions.NoseSneer_R" },
{ "from": "Standard.TongueOut", "to": "Actions.TongueOut" },
{ "from": "Standard.UserBlendshape0", "to": "Actions.UserBlendshape0" },
{ "from": "Standard.UserBlendshape1", "to": "Actions.UserBlendshape1" },
{ "from": "Standard.UserBlendshape2", "to": "Actions.UserBlendshape2" },
{ "from": "Standard.UserBlendshape3", "to": "Actions.UserBlendshape3" },
{ "from": "Standard.UserBlendshape4", "to": "Actions.UserBlendshape4" },
{ "from": "Standard.UserBlendshape5", "to": "Actions.UserBlendshape5" },
{ "from": "Standard.UserBlendshape6", "to": "Actions.UserBlendshape6" },
{ "from": "Standard.UserBlendshape7", "to": "Actions.UserBlendshape7" },
{ "from": "Standard.UserBlendshape8", "to": "Actions.UserBlendshape8" },
{ "from": "Standard.UserBlendshape9", "to": "Actions.UserBlendshape9" },
{ "from": "Standard.TrackedObject00", "to" : "Actions.TrackedObject00" },
{ "from": "Standard.TrackedObject01", "to" : "Actions.TrackedObject01" },

View file

@ -61,8 +61,70 @@
{ "from": "Standard.LeftEye", "to": "Actions.LeftEye" },
{ "from": "Standard.RightEye", "to": "Actions.RightEye" },
{ "from": "Standard.LeftEyeBlink", "to": "Actions.LeftEyeBlink" },
{ "from": "Standard.RightEyeBlink", "to": "Actions.RightEyeBlink" },
{ "from": "Standard.EyeBlink_L", "to": "Actions.EyeBlink_L" },
{ "from": "Standard.EyeBlink_R", "to": "Actions.EyeBlink_R" },
{ "from": "Standard.EyeSquint_L", "to": "Actions.EyeSquint_L" },
{ "from": "Standard.EyeSquint_R", "to": "Actions.EyeSquint_R" },
{ "from": "Standard.EyeDown_L", "to": "Actions.EyeDown_L" },
{ "from": "Standard.EyeDown_R", "to": "Actions.EyeDown_R" },
{ "from": "Standard.EyeIn_L", "to": "Actions.EyeIn_L" },
{ "from": "Standard.EyeIn_R", "to": "Actions.EyeIn_R" },
{ "from": "Standard.EyeOpen_L", "to": "Actions.EyeOpen_L" },
{ "from": "Standard.EyeOpen_R", "to": "Actions.EyeOpen_R" },
{ "from": "Standard.EyeOut_L", "to": "Actions.EyeOut_L" },
{ "from": "Standard.EyeOut_R", "to": "Actions.EyeOut_R" },
{ "from": "Standard.EyeUp_L", "to": "Actions.EyeUp_L" },
{ "from": "Standard.EyeUp_R", "to": "Actions.EyeUp_R" },
{ "from": "Standard.BrowsD_L", "to": "Actions.BrowsD_L" },
{ "from": "Standard.BrowsD_R", "to": "Actions.BrowsD_R" },
{ "from": "Standard.BrowsU_C", "to": "Actions.BrowsU_C" },
{ "from": "Standard.BrowsU_L", "to": "Actions.BrowsU_L" },
{ "from": "Standard.BrowsU_R", "to": "Actions.BrowsU_R" },
{ "from": "Standard.JawFwd", "to": "Actions.JawFwd" },
{ "from": "Standard.JawLeft", "to": "Actions.JawLeft" },
{ "from": "Standard.JawOpen", "to": "Actions.JawOpen" },
{ "from": "Standard.JawRight", "to": "Actions.JawRight" },
{ "from": "Standard.MouthLeft", "to": "Actions.MouthLeft" },
{ "from": "Standard.MouthRight", "to": "Actions.MouthRight" },
{ "from": "Standard.MouthFrown_L", "to": "Actions.MouthFrown_L" },
{ "from": "Standard.MouthFrown_R", "to": "Actions.MouthFrown_R" },
{ "from": "Standard.MouthSmile_L", "to": "Actions.MouthSmile_L" },
{ "from": "Standard.MouthSmile_R", "to": "Actions.MouthSmile_R" },
{ "from": "Standard.MouthDimple_L", "to": "Actions.MouthDimple_L" },
{ "from": "Standard.MouthDimple_R", "to": "Actions.MouthDimple_R" },
{ "from": "Standard.LipsStretch_L", "to": "Actions.LipsStretch_L" },
{ "from": "Standard.LipsStretch_R", "to": "Actions.LipsStretch_R" },
{ "from": "Standard.LipsUpperClose", "to": "Actions.LipsUpperClose" },
{ "from": "Standard.LipsLowerClose", "to": "Actions.LipsLowerClose" },
{ "from": "Standard.LipsUpperOpen", "to": "Actions.LipsUpperOpen" },
{ "from": "Standard.LipsLowerOpen", "to": "Actions.LipsLowerOpen" },
{ "from": "Standard.LipsFunnel", "to": "Actions.LipsFunnel" },
{ "from": "Standard.LipsPucker", "to": "Actions.LipsPucker" },
{ "from": "Standard.Puff", "to": "Actions.Puff" },
{ "from": "Standard.CheekSquint_L", "to": "Actions.CheekSquint_L" },
{ "from": "Standard.CheekSquint_R", "to": "Actions.CheekSquint_R" },
{ "from": "Standard.MouthClose", "to": "Actions.MouthClose" },
{ "from": "Standard.MouthUpperUp_L", "to": "Actions.MouthUpperUp_L" },
{ "from": "Standard.MouthUpperUp_R", "to": "Actions.MouthUpperUp_R" },
{ "from": "Standard.MouthLowerDown_L", "to": "Actions.MouthLowerDown_L" },
{ "from": "Standard.MouthLowerDown_R", "to": "Actions.MouthLowerDown_R" },
{ "from": "Standard.MouthPress_L", "to": "Actions.MouthPress_L" },
{ "from": "Standard.MouthPress_R", "to": "Actions.MouthPress_R" },
{ "from": "Standard.MouthShrugLower", "to": "Actions.MouthShrugLower" },
{ "from": "Standard.MouthShrugUpper", "to": "Actions.MouthShrugUpper" },
{ "from": "Standard.NoseSneer_L", "to": "Actions.NoseSneer_L" },
{ "from": "Standard.NoseSneer_R", "to": "Actions.NoseSneer_R" },
{ "from": "Standard.TongueOut", "to": "Actions.TongueOut" },
{ "from": "Standard.UserBlendshape0", "to": "Actions.UserBlendshape0" },
{ "from": "Standard.UserBlendshape1", "to": "Actions.UserBlendshape1" },
{ "from": "Standard.UserBlendshape2", "to": "Actions.UserBlendshape2" },
{ "from": "Standard.UserBlendshape3", "to": "Actions.UserBlendshape3" },
{ "from": "Standard.UserBlendshape4", "to": "Actions.UserBlendshape4" },
{ "from": "Standard.UserBlendshape5", "to": "Actions.UserBlendshape5" },
{ "from": "Standard.UserBlendshape6", "to": "Actions.UserBlendshape6" },
{ "from": "Standard.UserBlendshape7", "to": "Actions.UserBlendshape7" },
{ "from": "Standard.UserBlendshape8", "to": "Actions.UserBlendshape8" },
{ "from": "Standard.UserBlendshape9", "to": "Actions.UserBlendshape9" },
{ "from": "Standard.TrackedObject00", "to" : "Actions.TrackedObject00" },
{ "from": "Standard.TrackedObject01", "to" : "Actions.TrackedObject01" },

View file

@ -98,8 +98,9 @@
{ "from": "Vive.Head", "to" : "Standard.Head" },
{ "from": "Vive.LeftEye", "to" : "Standard.LeftEye" },
{ "from": "Vive.RightEye", "to" : "Standard.RightEye" },
{ "from": "Vive.LeftEyeBlink", "to" : "Standard.LeftEyeBlink" },
{ "from": "Vive.RightEyeBlink", "to" : "Standard.RightEyeBlink" },
{ "from": "Vive.EyeBlink_L", "to" : "Standard.EyeBlink_L" },
{ "from": "Vive.EyeBlink_R", "to" : "Standard.EyeBlink_R" },
{
"from": "Vive.LeftFoot", "to" : "Standard.LeftFoot",

View file

@ -65,6 +65,15 @@ Windows.Window {
}
});
}
Timer {
id: timer
interval: 500;
repeat: false;
onTriggered: {
updateContentParent();
}
}
function updateInteractiveWindowPositionForMode() {
if (presentationMode === Desktop.PresentationMode.VIRTUAL) {
@ -107,13 +116,11 @@ Windows.Window {
if (nativeWindow) {
nativeWindow.setVisible(false);
}
updateContentParent();
updateInteractiveWindowPositionForMode();
shown = interactiveWindowVisible;
} else if (presentationMode === Desktop.PresentationMode.NATIVE) {
shown = false;
if (nativeWindow) {
updateContentParent();
updateInteractiveWindowPositionForMode();
nativeWindow.setVisible(interactiveWindowVisible);
}
@ -123,10 +130,7 @@ Windows.Window {
}
Component.onCompleted: {
// Fix for parent loss on OSX:
parent.heightChanged.connect(updateContentParent);
parent.widthChanged.connect(updateContentParent);
x = interactiveWindowPosition.x;
y = interactiveWindowPosition.y;
width = interactiveWindowSize.width;
@ -140,6 +144,11 @@ Windows.Window {
id: root;
width: interactiveWindowSize.width
height: interactiveWindowSize.height
// fix for missing content on OSX initial startup with a non-maximized interface window. It seems that in this case, we cannot update
// the content parent during creation of the Window root. This added delay will update the parent after the root has finished loading.
Component.onCompleted: {
timer.start();
}
Rectangle {
color: hifi.colors.baseGray
@ -170,6 +179,7 @@ Windows.Window {
interactiveWindowPosition = Qt.point(nativeWindow.x, interactiveWindowPosition.y);
}
});
nativeWindow.yChanged.connect(function() {
if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) {
interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, nativeWindow.y);
@ -181,6 +191,7 @@ Windows.Window {
interactiveWindowSize = Qt.size(nativeWindow.width, interactiveWindowSize.height);
}
});
nativeWindow.heightChanged.connect(function() {
if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) {
interactiveWindowSize = Qt.size(interactiveWindowSize.width, nativeWindow.height);
@ -194,15 +205,11 @@ Windows.Window {
// finally set the initial window mode:
setupPresentationMode();
updateContentParent();
initialized = true;
}
Component.onDestruction: {
parent.heightChanged.disconnect(updateContentParent);
parent.widthChanged.disconnect(updateContentParent);
}
// Handle message traffic from the script that launched us to the loaded QML
function fromScript(message) {
if (root.dynamicContent && root.dynamicContent.fromScript) {
@ -232,8 +239,8 @@ Windows.Window {
interactiveWindowSize.width = newWidth;
updateInteractiveWindowSizeForMode();
}
function onRequestNewHeight(newWidth) {
interactiveWindowSize.width = newWidth;
function onRequestNewHeight(newHeight) {
interactiveWindowSize.height = newHeight;
updateInteractiveWindowSizeForMode();
}
@ -308,6 +315,7 @@ Windows.Window {
onPresentationModeChanged: {
if (initialized) {
setupPresentationMode();
updateContentParent();
}
}

View file

@ -375,14 +375,14 @@ Rectangle {
x: margins.paddings
interactive: false;
height: contentHeight;
spacing: 4;
clip: true;
model: AudioScriptingInterface.devices.input;
delegate: Item {
width: rightMostInputLevelPos - margins.paddings*2
height: margins.sizeCheckBox > checkBoxInput.implicitHeight ?
margins.sizeCheckBox : checkBoxInput.implicitHeight
height: ((type != "hmd" && bar.currentIndex === 0) || (type != "desktop" && bar.currentIndex === 1)) ?
(margins.sizeCheckBox > checkBoxInput.implicitHeight ? margins.sizeCheckBox + 4 : checkBoxInput.implicitHeight + 4) : 0
visible: (type != "hmd" && bar.currentIndex === 0) || (type != "desktop" && bar.currentIndex === 1)
AudioControls.CheckBox {
id: checkBoxInput
anchors.left: parent.left
@ -470,13 +470,13 @@ Rectangle {
height: contentHeight;
anchors.top: outputDeviceHeader.bottom;
anchors.topMargin: 10;
spacing: 4;
clip: true;
model: AudioScriptingInterface.devices.output;
delegate: Item {
width: rightMostInputLevelPos
height: margins.sizeCheckBox > checkBoxOutput.implicitHeight ?
margins.sizeCheckBox : checkBoxOutput.implicitHeight
height: ((type != "hmd" && bar.currentIndex === 0) || (type != "desktop" && bar.currentIndex === 1)) ?
(margins.sizeCheckBox > checkBoxOutput.implicitHeight ? margins.sizeCheckBox + 4 : checkBoxOutput.implicitHeight + 4) : 0
visible: (type != "hmd" && bar.currentIndex === 0) || (type != "desktop" && bar.currentIndex === 1)
AudioControls.CheckBox {
id: checkBoxOutput

View file

@ -133,15 +133,12 @@ Item {
ListElement {
text: "Low World Detail"
worldDetailQualityValue: 0.25
}
ListElement {
text: "Medium World Detail"
worldDetailQualityValue: 0.5
}
ListElement {
text: "Full World Detail"
worldDetailQualityValue: 0.75
}
}
@ -158,14 +155,7 @@ Item {
currentIndex: -1
function refreshWorldDetailDropdown() {
var currentWorldDetailQuality = LODManager.worldDetailQuality;
if (currentWorldDetailQuality <= 0.25) {
worldDetailDropdown.currentIndex = 0;
} else if (currentWorldDetailQuality <= 0.5) {
worldDetailDropdown.currentIndex = 1;
} else {
worldDetailDropdown.currentIndex = 2;
}
worldDetailDropdown.currentIndex = LODManager.worldDetailQuality;
}
Component.onCompleted: {
@ -173,7 +163,7 @@ Item {
}
onCurrentIndexChanged: {
LODManager.worldDetailQuality = model.get(currentIndex).worldDetailQualityValue;
LODManager.worldDetailQuality = currentIndex;
worldDetailDropdown.displayText = model.get(currentIndex).text;
}
}

View file

@ -258,13 +258,12 @@ Flickable {
Layout.preferredHeight: contentItem.height
Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin
interactive: false
spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons
clip: true
model: AudioScriptingInterface.devices.input
delegate: Item {
width: parent.width
height: inputDeviceCheckbox.height
width: parent.width
height: model.type != "hmd" ? inputDeviceCheckbox.height + simplifiedUI.margins.settings.spacingBetweenRadiobuttons : 0
visible: model.type != "hmd"
SimplifiedControls.RadioButton {
id: inputDeviceCheckbox
anchors.left: parent.left
@ -354,13 +353,12 @@ Flickable {
Layout.preferredHeight: contentItem.height
Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin
interactive: false
spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons
clip: true
model: AudioScriptingInterface.devices.output
delegate: Item {
width: parent.width
height: outputDeviceCheckbox.height
height: model.type != "hmd" ? outputDeviceCheckbox.height +simplifiedUI.margins.settings.spacingBetweenRadiobuttons : 0
visible: model.type != "hmd"
SimplifiedControls.RadioButton {
id: outputDeviceCheckbox
anchors.left: parent.left

View file

@ -259,13 +259,13 @@ Flickable {
Layout.preferredHeight: contentItem.height
Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin
interactive: false
spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons
clip: true
model: AudioScriptingInterface.devices.input
delegate: Item {
width: parent.width
height: inputDeviceCheckbox.height
width: parent.width
height: model.type != "desktop" ? inputDeviceCheckbox.height + simplifiedUI.margins.settings.spacingBetweenRadiobuttons : 0
visible: model.type != "desktop"
SimplifiedControls.RadioButton {
id: inputDeviceCheckbox
anchors.left: parent.left
@ -355,13 +355,12 @@ Flickable {
Layout.preferredHeight: contentItem.height
Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin
interactive: false
spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons
clip: true
model: AudioScriptingInterface.devices.output
delegate: Item {
width: parent.width
height: outputDeviceCheckbox.height
height: model.type != "desktop" ? outputDeviceCheckbox.height + simplifiedUI.margins.settings.spacingBetweenRadiobuttons : 0
visible: model.type != "desktop"
SimplifiedControls.RadioButton {
id: outputDeviceCheckbox
anchors.left: parent.left

View file

@ -171,7 +171,6 @@
#include "avatar/MyCharacterController.h"
#include "CrashRecoveryHandler.h"
#include "CrashHandler.h"
#include "devices/DdeFaceTracker.h"
#include "DiscoverabilityManager.h"
#include "GLCanvas.h"
#include "InterfaceDynamicFactory.h"
@ -889,11 +888,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<ScriptCache>();
DependencyManager::set<SoundCache>();
DependencyManager::set<SoundCacheScriptingInterface>();
#ifdef HAVE_DDE
DependencyManager::set<DdeFaceTracker>();
#endif
DependencyManager::set<AudioClient>();
DependencyManager::set<AudioScope>();
DependencyManager::set<DeferredLightingEffect>();
@ -1070,7 +1064,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_lastSendDownstreamAudioStats(usecTimestampNow()),
_notifiedPacketVersionMismatchThisDomain(false),
_maxOctreePPS(maxOctreePacketsPerSecond.get()),
_lastFaceTrackerUpdate(0),
_snapshotSound(nullptr),
_sampleSound(nullptr)
{
@ -2020,13 +2013,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
this->installEventFilter(this);
#ifdef HAVE_DDE
auto ddeTracker = DependencyManager::get<DdeFaceTracker>();
ddeTracker->init();
connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled);
#endif
// If launched from Steam, let it handle updates
const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater";
bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1;
@ -2770,9 +2756,6 @@ void Application::cleanupBeforeQuit() {
}
// Stop third party processes so that they're not left running in the event of a subsequent shutdown crash.
#ifdef HAVE_DDE
DependencyManager::get<DdeFaceTracker>()->setEnabled(false);
#endif
AnimDebugDraw::getInstance().shutdown();
// FIXME: once we move to shared pointer for the INputDevice we shoud remove this naked delete:
@ -2843,10 +2826,6 @@ void Application::cleanupBeforeQuit() {
_window->saveGeometry();
// Destroy third party processes after scripts have finished using them.
#ifdef HAVE_DDE
DependencyManager::destroy<DdeFaceTracker>();
#endif
DependencyManager::destroy<ContextOverlayInterface>(); // Must be destroyed before TabletScriptingInterface
// stop QML
@ -3481,9 +3460,6 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance());
surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface);
#ifdef HAVE_DDE
surfaceContext->setContextProperty("FaceTracker", DependencyManager::get<DdeFaceTracker>().data());
#endif
surfaceContext->setContextProperty("AvatarManager", DependencyManager::get<AvatarManager>().data());
surfaceContext->setContextProperty("LODManager", DependencyManager::get<LODManager>().data());
surfaceContext->setContextProperty("HMD", DependencyManager::get<HMDScriptingInterface>().data());
@ -3750,16 +3726,6 @@ void Application::runTests() {
runUnitTests();
}
void Application::faceTrackerMuteToggled() {
QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteFaceTracking);
Q_CHECK_PTR(muteAction);
bool isMuted = getSelectedFaceTracker()->isMuted();
muteAction->setChecked(isMuted);
getSelectedFaceTracker()->setEnabled(!isMuted);
Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setEnabled(!isMuted);
}
void Application::setFieldOfView(float fov) {
if (fov != _fieldOfView.get()) {
_fieldOfView.set(fov);
@ -5334,43 +5300,6 @@ ivec2 Application::getMouse() const {
return getApplicationCompositor().getReticlePosition();
}
FaceTracker* Application::getActiveFaceTracker() {
#ifdef HAVE_DDE
auto dde = DependencyManager::get<DdeFaceTracker>();
if (dde && dde->isActive()) {
return static_cast<FaceTracker*>(dde.data());
}
#endif
return nullptr;
}
FaceTracker* Application::getSelectedFaceTracker() {
FaceTracker* faceTracker = nullptr;
#ifdef HAVE_DDE
if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) {
faceTracker = DependencyManager::get<DdeFaceTracker>().data();
}
#endif
return faceTracker;
}
void Application::setActiveFaceTracker() const {
#ifdef HAVE_DDE
bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking);
bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera);
Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE);
Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE);
Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE);
Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE);
Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE);
auto ddeTracker = DependencyManager::get<DdeFaceTracker>();
ddeTracker->setIsMuted(isMuted);
ddeTracker->setEnabled(isUsingDDE && !isMuted);
#endif
}
bool Application::exportEntities(const QString& filename,
const QVector<QUuid>& entityIDs,
const glm::vec3* givenOffset) {
@ -5848,14 +5777,13 @@ void Application::pushPostUpdateLambda(void* key, const std::function<void()>& f
// to everyone.
// The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition().
// Note that it is called BEFORE we update position or joints based on sensors, etc.
void Application::updateMyAvatarLookAtPosition() {
void Application::updateMyAvatarLookAtPosition(float deltaTime) {
PerformanceTimer perfTimer("lookAt");
bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings);
PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()");
auto myAvatar = getMyAvatar();
FaceTracker* faceTracker = getActiveFaceTracker();
myAvatar->updateLookAtPosition(faceTracker, _myCamera);
myAvatar->updateEyesLookAtPosition(deltaTime);
}
void Application::updateThreads(float deltaTime) {
@ -6281,37 +6209,6 @@ void Application::update(float deltaTime) {
auto myAvatar = getMyAvatar();
{
PerformanceTimer perfTimer("devices");
FaceTracker* tracker = getSelectedFaceTracker();
if (tracker && Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking) != tracker->isMuted()) {
tracker->toggleMute();
}
tracker = getActiveFaceTracker();
if (tracker && !tracker->isMuted()) {
tracker->update(deltaTime);
// Auto-mute microphone after losing face tracking?
if (tracker->isTracking()) {
_lastFaceTrackerUpdate = usecTimestampNow();
} else {
const quint64 MUTE_MICROPHONE_AFTER_USECS = 5000000; //5 secs
Menu* menu = Menu::getInstance();
auto audioClient = DependencyManager::get<AudioClient>();
if (menu->isOptionChecked(MenuOption::AutoMuteAudio) && !audioClient->isMuted()) {
if (_lastFaceTrackerUpdate > 0
&& ((usecTimestampNow() - _lastFaceTrackerUpdate) > MUTE_MICROPHONE_AFTER_USECS)) {
audioClient->setMuted(true);
_lastFaceTrackerUpdate = 0;
}
} else {
_lastFaceTrackerUpdate = 0;
}
}
} else {
_lastFaceTrackerUpdate = 0;
}
auto userInputMapper = DependencyManager::get<UserInputMapper>();
controller::HmdAvatarAlignmentType hmdAvatarAlignmentType;
@ -6639,7 +6536,7 @@ void Application::update(float deltaTime) {
{
PROFILE_RANGE(simulation, "MyAvatar");
PerformanceTimer perfTimer("MyAvatar");
qApp->updateMyAvatarLookAtPosition();
qApp->updateMyAvatarLookAtPosition(deltaTime);
avatarManager->updateMyAvatar(deltaTime);
}
}
@ -7107,10 +7004,6 @@ void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const {
// feature. However, we still use this to reset face trackers, eye trackers, audio and to optionally re-load the avatar
// rig and animations from scratch.
void Application::resetSensors(bool andReload) {
#ifdef HAVE_DDE
DependencyManager::get<DdeFaceTracker>()->reset();
#endif
_overlayConductor.centerUI();
getActiveDisplayPlugin()->resetSensors();
getMyAvatar()->reset(true, andReload);
@ -7508,13 +7401,10 @@ void Application::registerScriptEngineWithApplicationServices(const ScriptEngine
scriptEngine->registerGlobalObject("AccountServices", AccountServicesScriptingInterface::getInstance());
qScriptRegisterMetaType(scriptEngine.data(), DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue);
#ifdef HAVE_DDE
scriptEngine->registerGlobalObject("FaceTracker", DependencyManager::get<DdeFaceTracker>().data());
#endif
scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get<AvatarManager>().data());
scriptEngine->registerGlobalObject("LODManager", DependencyManager::get<LODManager>().data());
qScriptRegisterMetaType(scriptEngine.data(), worldDetailQualityToScriptValue, worldDetailQualityFromScriptValue);
scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get<KeyboardScriptingInterface>().data());
scriptEngine->registerGlobalObject("Performance", new PerformanceScriptingInterface());

View file

@ -81,7 +81,6 @@
#include "VisionSqueeze.h"
class GLCanvas;
class FaceTracker;
class MainWindow;
class AssetUpload;
class CompositorHelper;
@ -191,9 +190,6 @@ public:
ivec2 getMouse() const;
FaceTracker* getActiveFaceTracker();
FaceTracker* getSelectedFaceTracker();
ApplicationOverlay& getApplicationOverlay() { return _applicationOverlay; }
const ApplicationOverlay& getApplicationOverlay() const { return _applicationOverlay; }
CompositorHelper& getApplicationCompositor() const;
@ -292,7 +288,7 @@ public:
virtual void pushPostUpdateLambda(void* key, const std::function<void()>& func) override;
void updateMyAvatarLookAtPosition();
void updateMyAvatarLookAtPosition(float deltaTime);
float getGameLoopRate() const { return _gameLoopCounter.rate(); }
@ -423,7 +419,6 @@ public slots:
static void packageModel();
void resetSensors(bool andReload = false);
void setActiveFaceTracker() const;
void hmdVisibleChanged(bool visible);
@ -497,8 +492,6 @@ private slots:
void resettingDomain();
void faceTrackerMuteToggled();
void activeChanged(Qt::ApplicationState state);
void windowMinimizedChanged(bool minimized);
@ -736,8 +729,6 @@ private:
PerformanceManager _performanceManager;
RefreshRateManager _refreshRateManager;
quint64 _lastFaceTrackerUpdate;
GameWorkload _gameWorkload;
GraphicsEngine _graphicsEngine;

View file

@ -212,22 +212,36 @@ void AvatarBookmarks::loadBookmark(const QString& bookmarkName) {
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
myAvatar->clearWornAvatarEntities();
// Once the skeleton URL has been loaded, add the Avatar Entities.
// We have to wait, because otherwise the avatar entities will try to get attached to the joints
// of the *current* avatar at first. But the current avatar might have a different joints scheme
// from the new avatar, and that would cause the entities to be attached to the wrong joints.
std::shared_ptr<QMetaObject::Connection> connection1 = std::make_shared<QMetaObject::Connection>();
*connection1 = connect(myAvatar.get(), &MyAvatar::onLoadComplete, [this, bookmark, bookmarkName, myAvatar, connection1]() {
qCDebug(interfaceapp) << "Finish loading avatar bookmark" << bookmarkName;
QObject::disconnect(*connection1);
myAvatar->clearWornAvatarEntities();
const float& qScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat();
myAvatar->setAvatarScale(qScale);
QList<QVariant> attachments = bookmark.value(ENTRY_AVATAR_ATTACHMENTS, QList<QVariant>()).toList();
myAvatar->setAttachmentsVariant(attachments);
QVariantList avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList();
addAvatarEntities(avatarEntities);
emit bookmarkLoaded(bookmarkName);
});
std::shared_ptr<QMetaObject::Connection> connection2 = std::make_shared<QMetaObject::Connection>();
*connection2 = connect(myAvatar.get(), &MyAvatar::onLoadFailed, [this, bookmarkName, connection2]() {
qCDebug(interfaceapp) << "Failed to load avatar bookmark" << bookmarkName;
QObject::disconnect(*connection2);
});
qCDebug(interfaceapp) << "Start loading avatar bookmark" << bookmarkName;
const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString();
myAvatar->useFullAvatarURL(avatarUrl);
qCDebug(interfaceapp) << "Avatar On";
const QList<QVariant>& attachments = bookmark.value(ENTRY_AVATAR_ATTACHMENTS, QList<QVariant>()).toList();
qCDebug(interfaceapp) << "Attach " << attachments;
myAvatar->setAttachmentsVariant(attachments);
const float& qScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat();
myAvatar->setAvatarScale(qScale);
const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList();
addAvatarEntities(avatarEntities);
emit bookmarkLoaded(bookmarkName);
}
}
}

View file

@ -19,11 +19,8 @@
#include "ui/DialogsManager.h"
#include "InterfaceLogging.h"
const float LODManager::DEFAULT_DESKTOP_LOD_DOWN_FPS = LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_DESKTOP_FPS;
const float LODManager::DEFAULT_HMD_LOD_DOWN_FPS = LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_HMD_FPS;
Setting::Handle<float> desktopLODDecreaseFPS("desktopLODDecreaseFPS", LODManager::DEFAULT_DESKTOP_LOD_DOWN_FPS);
Setting::Handle<float> hmdLODDecreaseFPS("hmdLODDecreaseFPS", LODManager::DEFAULT_HMD_LOD_DOWN_FPS);
Setting::Handle<int> desktopWorldDetailQuality("desktopWorldDetailQuality", (int)DEFAULT_WORLD_DETAIL_QUALITY);
Setting::Handle<int> hmdWorldDetailQuality("hmdWorldDetailQuality", (int)DEFAULT_WORLD_DETAIL_QUALITY);
LODManager::LODManager() {
}
@ -326,19 +323,21 @@ QString LODManager::getLODFeedbackText() {
}
void LODManager::loadSettings() {
setDesktopLODTargetFPS(desktopLODDecreaseFPS.get());
Setting::Handle<bool> firstRun { Settings::firstRun, true };
auto desktopQuality = static_cast<WorldDetailQuality>(desktopWorldDetailQuality.get());
auto hmdQuality = static_cast<WorldDetailQuality>(hmdWorldDetailQuality.get());
Setting::Handle<bool> firstRun{ Settings::firstRun, true };
if (qApp->property(hifi::properties::OCULUS_STORE).toBool() && firstRun.get()) {
const float LOD_HIGH_QUALITY_LEVEL = 0.75f;
setHMDLODTargetFPS(LOD_HIGH_QUALITY_LEVEL * LOD_MAX_LIKELY_HMD_FPS);
} else {
setHMDLODTargetFPS(hmdLODDecreaseFPS.get());
hmdQuality = WORLD_DETAIL_HIGH;
}
setWorldDetailQuality(desktopQuality, false);
setWorldDetailQuality(hmdQuality, true);
}
void LODManager::saveSettings() {
desktopLODDecreaseFPS.set(getDesktopLODTargetFPS());
hmdLODDecreaseFPS.set(getHMDLODTargetFPS());
desktopWorldDetailQuality.set((int)_desktopWorldDetailQuality);
hmdWorldDetailQuality.set((int)_hmdWorldDetailQuality);
}
const float MIN_DECREASE_FPS = 0.5f;
@ -393,54 +392,33 @@ float LODManager::getLODTargetFPS() const {
}
}
void LODManager::setWorldDetailQuality(float quality) {
static const float MIN_FPS = 10;
static const float LOW = 0.25f;
bool isLowestValue = quality == LOW;
bool isHMDMode = qApp->isHMDMode();
float maxFPS = isHMDMode ? LOD_MAX_LIKELY_HMD_FPS : LOD_MAX_LIKELY_DESKTOP_FPS;
float desiredFPS = maxFPS;
if (!isLowestValue) {
float calculatedFPS = (maxFPS - (maxFPS * quality));
desiredFPS = calculatedFPS < MIN_FPS ? MIN_FPS : calculatedFPS;
}
void LODManager::setWorldDetailQuality(WorldDetailQuality quality, bool isHMDMode) {
float desiredFPS = isHMDMode ? QUALITY_TO_FPS_HMD[quality] : QUALITY_TO_FPS_DESKTOP[quality];
if (isHMDMode) {
_hmdWorldDetailQuality = quality;
setHMDLODTargetFPS(desiredFPS);
} else {
_desktopWorldDetailQuality = quality;
setDesktopLODTargetFPS(desiredFPS);
}
}
void LODManager::setWorldDetailQuality(WorldDetailQuality quality) {
setWorldDetailQuality(quality, qApp->isHMDMode());
emit worldDetailQualityChanged();
}
float LODManager::getWorldDetailQuality() const {
WorldDetailQuality LODManager::getWorldDetailQuality() const {
return qApp->isHMDMode() ? _hmdWorldDetailQuality : _desktopWorldDetailQuality;
}
static const float LOW = 0.25f;
static const float MEDIUM = 0.5f;
static const float HIGH = 0.75f;
QScriptValue worldDetailQualityToScriptValue(QScriptEngine* engine, const WorldDetailQuality& worldDetailQuality) {
return worldDetailQuality;
}
bool inHMD = qApp->isHMDMode();
float targetFPS = 0.0f;
if (inHMD) {
targetFPS = getHMDLODTargetFPS();
} else {
targetFPS = getDesktopLODTargetFPS();
}
float maxFPS = inHMD ? LOD_MAX_LIKELY_HMD_FPS : LOD_MAX_LIKELY_DESKTOP_FPS;
float percentage = 1.0f - targetFPS / maxFPS;
if (percentage <= LOW) {
return LOW;
} else if (percentage <= MEDIUM) {
return MEDIUM;
}
return HIGH;
void worldDetailQualityFromScriptValue(const QScriptValue& object, WorldDetailQuality& worldDetailQuality) {
worldDetailQuality =
static_cast<WorldDetailQuality>(std::min(std::max(object.toInt32(), (int)WORLD_DETAIL_LOW), (int)WORLD_DETAIL_HIGH));
}
void LODManager::setLODQualityLevel(float quality) {

View file

@ -23,26 +23,52 @@
#include <render/Args.h>
/**jsdoc
* <p>The world detail quality rendered.</p>
* <table>
* <thead>
* <tr><th>Value</th><th>Description</th></tr>
* </thead>
* <tbody>
* <tr><td><code>0</code></td><td>Low world detail quality.</td></tr>
* <tr><td><code>1</code></td><td>Medium world detail quality.</td></tr>
* <tr><td><code>2</code></td><td>High world detail quality.</td></tr>
* </tbody>
* </table>
* @typedef {number} LODManager.WorldDetailQuality
*/
enum WorldDetailQuality {
WORLD_DETAIL_LOW = 0,
WORLD_DETAIL_MEDIUM,
WORLD_DETAIL_HIGH
};
Q_DECLARE_METATYPE(WorldDetailQuality);
#ifdef Q_OS_ANDROID
const float LOD_DEFAULT_QUALITY_LEVEL = 0.2f; // default quality level setting is High (lower framerate)
#else
const float LOD_DEFAULT_QUALITY_LEVEL = 0.5f; // default quality level setting is Mid
#endif
const float LOD_MAX_LIKELY_DESKTOP_FPS = 60.0f; // this is essentially, V-synch fps
#ifdef Q_OS_ANDROID
const float LOD_MAX_LIKELY_HMD_FPS = 36.0f; // this is essentially, V-synch fps
const WorldDetailQuality DEFAULT_WORLD_DETAIL_QUALITY = WORLD_DETAIL_LOW;
const std::vector<float> QUALITY_TO_FPS_DESKTOP = { 60.0f, 30.0f, 15.0f };
const std::vector<float> QUALITY_TO_FPS_HMD = { 25.0f, 16.0f, 10.0f };
#else
const float LOD_MAX_LIKELY_HMD_FPS = 90.0f; // this is essentially, V-synch fps
const WorldDetailQuality DEFAULT_WORLD_DETAIL_QUALITY = WORLD_DETAIL_MEDIUM;
const std::vector<float> QUALITY_TO_FPS_DESKTOP = { 60.0f, 30.0f, 15.0f };
const std::vector<float> QUALITY_TO_FPS_HMD = { 90.0f, 45.0f, 22.5f };
#endif
const float LOD_OFFSET_FPS = 5.0f; // offset of FPS to add for computing the target framerate
class AABox;
/**jsdoc
* The LOD class manages your Level of Detail functions within Interface.
* @namespace LODManager
*
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
@ -51,12 +77,12 @@ class AABox;
* @property {number} engineRunTime <em>Read-only.</em>
* @property {number} gpuTime <em>Read-only.</em>
*/
class LODManager : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
Q_PROPERTY(float worldDetailQuality READ getWorldDetailQuality WRITE setWorldDetailQuality NOTIFY worldDetailQualityChanged)
Q_PROPERTY(WorldDetailQuality worldDetailQuality READ getWorldDetailQuality WRITE setWorldDetailQuality
NOTIFY worldDetailQualityChanged)
Q_PROPERTY(float lodQualityLevel READ getLODQualityLevel WRITE setLODQualityLevel NOTIFY lodQualityLevelChanged)
@ -193,8 +219,8 @@ public:
float getSmoothRenderTime() const { return _smoothRenderTime; };
float getSmoothRenderFPS() const { return (_smoothRenderTime > 0.0f ? (float)MSECS_PER_SECOND / _smoothRenderTime : 0.0f); };
void setWorldDetailQuality(float quality);
float getWorldDetailQuality() const;
void setWorldDetailQuality(WorldDetailQuality quality);
WorldDetailQuality getWorldDetailQuality() const;
void setLODQualityLevel(float quality);
float getLODQualityLevel() const;
@ -220,9 +246,6 @@ public:
float getPidOd() const;
float getPidO() const;
static const float DEFAULT_DESKTOP_LOD_DOWN_FPS;
static const float DEFAULT_HMD_LOD_DOWN_FPS;
signals:
/**jsdoc
@ -244,6 +267,8 @@ signals:
private:
LODManager();
void setWorldDetailQuality(WorldDetailQuality quality, bool isHMDMode);
std::mutex _automaticLODLock;
bool _automaticLODAdjust = true;
@ -258,8 +283,11 @@ private:
float _lodQualityLevel{ LOD_DEFAULT_QUALITY_LEVEL };
float _desktopTargetFPS { LOD_OFFSET_FPS + LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_DESKTOP_FPS };
float _hmdTargetFPS { LOD_OFFSET_FPS + LOD_DEFAULT_QUALITY_LEVEL * LOD_MAX_LIKELY_HMD_FPS };
WorldDetailQuality _desktopWorldDetailQuality { DEFAULT_WORLD_DETAIL_QUALITY };
WorldDetailQuality _hmdWorldDetailQuality { DEFAULT_WORLD_DETAIL_QUALITY };
float _desktopTargetFPS { QUALITY_TO_FPS_DESKTOP[_desktopWorldDetailQuality] };
float _hmdTargetFPS { QUALITY_TO_FPS_HMD[_hmdWorldDetailQuality] };
float _lodHalfAngle = getHalfAngleFromVisibilityDistance(DEFAULT_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT);
int _boundaryLevelAdjust = 0;
@ -269,4 +297,7 @@ private:
glm::vec4 _pidOutputs{ 0.0f };
};
QScriptValue worldDetailQualityToScriptValue(QScriptEngine* engine, const WorldDetailQuality& worldDetailQuality);
void worldDetailQualityFromScriptValue(const QScriptValue& object, WorldDetailQuality& worldDetailQuality);
#endif // hifi_LODManager_h

View file

@ -37,7 +37,6 @@
#include "avatar/AvatarManager.h"
#include "avatar/AvatarPackager.h"
#include "AvatarBookmarks.h"
#include "devices/DdeFaceTracker.h"
#include "MainWindow.h"
#include "render/DrawStatus.h"
#include "scripting/MenuScriptingInterface.h"
@ -499,47 +498,6 @@ Menu::Menu() {
// Developer > Avatar >>>
MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar");
// Developer > Avatar > Face Tracking
MenuWrapper* faceTrackingMenu = avatarDebugMenu->addMenu("Face Tracking");
{
QActionGroup* faceTrackerGroup = new QActionGroup(avatarDebugMenu);
bool defaultNoFaceTracking = true;
#ifdef HAVE_DDE
defaultNoFaceTracking = false;
#endif
QAction* noFaceTracker = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::NoFaceTracking,
0, defaultNoFaceTracking,
qApp, SLOT(setActiveFaceTracker()));
faceTrackerGroup->addAction(noFaceTracker);
#ifdef HAVE_DDE
QAction* ddeFaceTracker = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::UseCamera,
0, true,
qApp, SLOT(setActiveFaceTracker()));
faceTrackerGroup->addAction(ddeFaceTracker);
#endif
}
#ifdef HAVE_DDE
faceTrackingMenu->addSeparator();
QAction* binaryEyelidControl = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::BinaryEyelidControl, 0, true);
binaryEyelidControl->setVisible(true); // DDE face tracking is on by default
QAction* coupleEyelids = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::CoupleEyelids, 0, true);
coupleEyelids->setVisible(true); // DDE face tracking is on by default
QAction* useAudioForMouth = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::UseAudioForMouth, 0, true);
useAudioForMouth->setVisible(true); // DDE face tracking is on by default
QAction* ddeFiltering = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::VelocityFilter, 0, true);
ddeFiltering->setVisible(true); // DDE face tracking is on by default
QAction* ddeCalibrate = addActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::CalibrateCamera, 0,
DependencyManager::get<DdeFaceTracker>().data(), SLOT(calibrate()));
ddeCalibrate->setVisible(true); // DDE face tracking is on by default
faceTrackingMenu->addSeparator();
addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::MuteFaceTracking,
[](bool mute) { FaceTracker::setIsMuted(mute); },
Qt::CTRL | Qt::SHIFT | Qt::Key_F, FaceTracker::isMuted());
addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::AutoMuteAudio, 0, false);
#endif
action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AvatarReceiveStats, 0, false);
connect(action, &QAction::triggered, [this]{ Avatar::setShowReceiveStats(isOptionChecked(MenuOption::AvatarReceiveStats)); });
action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowBoundingCollisionShapes, 0, false);

View file

@ -92,7 +92,7 @@ void PerformanceManager::applyPerformancePreset(PerformanceManager::PerformanceP
RenderScriptingInterface::getInstance()->setShadowsEnabled(true);
qApp->getRefreshRateManager().setRefreshRateProfile(RefreshRateManager::RefreshRateProfile::REALTIME);
DependencyManager::get<LODManager>()->setWorldDetailQuality(0.75f);
DependencyManager::get<LODManager>()->setWorldDetailQuality(WORLD_DETAIL_HIGH);
break;
case PerformancePreset::MID:
@ -104,7 +104,7 @@ void PerformanceManager::applyPerformancePreset(PerformanceManager::PerformanceP
RenderScriptingInterface::getInstance()->setShadowsEnabled(false);
qApp->getRefreshRateManager().setRefreshRateProfile(RefreshRateManager::RefreshRateProfile::INTERACTIVE);
DependencyManager::get<LODManager>()->setWorldDetailQuality(0.5f);
DependencyManager::get<LODManager>()->setWorldDetailQuality(WORLD_DETAIL_MEDIUM);
break;
case PerformancePreset::LOW:
@ -114,7 +114,7 @@ void PerformanceManager::applyPerformancePreset(PerformanceManager::PerformanceP
RenderScriptingInterface::getInstance()->setViewportResolutionScale(recommandedPpiScale);
DependencyManager::get<LODManager>()->setWorldDetailQuality(0.25f);
DependencyManager::get<LODManager>()->setWorldDetailQuality(WORLD_DETAIL_LOW);
break;
case PerformancePreset::UNKNOWN:

View file

@ -26,19 +26,22 @@ class AudioScope : public QObject, public Dependency {
SINGLETON_DEPENDENCY
/**jsdoc
* The AudioScope API helps control the Audio Scope features in Interface
* The <code>AudioScope</code> API provides facilities for an audio scope.
*
* @namespace AudioScope
*
* @deprecated This API doesn't work properly. It is deprecated and will be removed.
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
*
* @property {number} scopeInput <em>Read-only.</em>
* @property {number} scopeOutputLeft <em>Read-only.</em>
* @property {number} scopeOutputRight <em>Read-only.</em>
* @property {number} triggerInput <em>Read-only.</em>
* @property {number} triggerOutputLeft <em>Read-only.</em>
* @property {number} triggerOutputRight <em>Read-only.</em>
* @property {number[]} scopeInput - Scope input. <em>Read-only.</em>
* @property {number[]} scopeOutputLeft - Scope left output. <em>Read-only.</em>
* @property {number[]} scopeOutputRight - Scope right output. <em>Read-only.</em>
* @property {number[]} triggerInput - Trigger input. <em>Read-only.</em>
* @property {number[]} triggerOutputLeft - Trigger left output. <em>Read-only.</em>
* @property {number[]} triggerOutputRight - Trigger right output. <em>Read-only.</em>
*/
Q_PROPERTY(QVector<int> scopeInput READ getScopeInput)
@ -58,159 +61,186 @@ public:
public slots:
/**jsdoc
* Toggle.
* @function AudioScope.toggle
*/
void toggle() { setVisible(!_isEnabled); }
/**jsdoc
* Set visible.
* @function AudioScope.setVisible
* @param {boolean} visible
* @param {boolean} visible - Visible.
*/
void setVisible(bool visible);
/**jsdoc
* Get visible.
* @function AudioScope.getVisible
* @returns {boolean}
* @returns {boolean} Visible.
*/
bool getVisible() const { return _isEnabled; }
/**jsdoc
* Toggle pause.
* @function AudioScope.togglePause
*/
void togglePause() { setPause(!_isPaused); }
/**jsdoc
* Set pause.
* @function AudioScope.setPause
* @param {boolean} paused
* @param {boolean} pause - Pause.
*/
void setPause(bool paused) { _isPaused = paused; emit pauseChanged(); }
/**jsdoc
* Get pause.
* @function AudioScope.getPause
* @returns {boolean}
* @returns {boolean} Pause.
*/
bool getPause() { return _isPaused; }
/**jsdoc
* Toggle trigger.
* @function AudioScope.toggleTrigger
*/
void toggleTrigger() { _autoTrigger = !_autoTrigger; }
/**jsdoc
* Get auto trigger.
* @function AudioScope.getAutoTrigger
* @returns {boolean}
* @returns {boolean} Auto trigger.
*/
bool getAutoTrigger() { return _autoTrigger; }
/**jsdoc
* Set auto trigger.
* @function AudioScope.setAutoTrigger
* @param {boolean} autoTrigger
* @param {boolean} autoTrigger - Auto trigger.
*/
void setAutoTrigger(bool autoTrigger) { _isTriggered = false; _autoTrigger = autoTrigger; }
/**jsdoc
* Set trigger values.
* @function AudioScope.setTriggerValues
* @param {number} x
* @param {number} y
* @param {number} x - X.
* @param {number} y - Y.
*/
void setTriggerValues(int x, int y) { _triggerValues.x = x; _triggerValues.y = y; }
/**jsdoc
* Set triggered.
* @function AudioScope.setTriggered
* @param {boolean} triggered
* @param {boolean} triggered - Triggered.
*/
void setTriggered(bool triggered) { _isTriggered = triggered; }
/**jsdoc
* Get triggered.
* @function AudioScope.getTriggered
* @returns {boolean}
* @returns {boolean} Triggered.
*/
bool getTriggered() { return _isTriggered; }
/**jsdoc
* Get frames per second.
* @function AudioScope.getFramesPerSecond
* @returns {number}
* @returns {number} Frames per second.
*/
float getFramesPerSecond();
/**jsdoc
* Get frames per scope.
* @function AudioScope.getFramesPerScope
* @returns {number}
* @returns {number} Frames per scope.
*/
int getFramesPerScope() { return _framesPerScope; }
/**jsdoc
* Select five frames audio scope.
* @function AudioScope.selectAudioScopeFiveFrames
*/
void selectAudioScopeFiveFrames();
/**jsdoc
* Select twenty frames audio scope.
* @function AudioScope.selectAudioScopeTwentyFrames
*/
void selectAudioScopeTwentyFrames();
/**jsdoc
* Select fifty frames audio scope.
* @function AudioScope.selectAudioScopeFiftyFrames
*/
void selectAudioScopeFiftyFrames();
/**jsdoc
* Get scope input.
* @function AudioScope.getScopeInput
* @returns {number[]}
* @returns {number[]} Scope input.
*/
QVector<int> getScopeInput() { return _scopeInputData; };
/**jsdoc
* Get scope left output.
* @function AudioScope.getScopeOutputLeft
* @returns {number[]}
* @returns {number[]} Scope left output.
*/
QVector<int> getScopeOutputLeft() { return _scopeOutputLeftData; };
/**jsdoc
* Get scope right output.
* @function AudioScope.getScopeOutputRight
* @returns {number[]}
* @returns {number[]} Scope right output.
*/
QVector<int> getScopeOutputRight() { return _scopeOutputRightData; };
/**jsdoc
* Get trigger input.
* @function AudioScope.getTriggerInput
* @returns {number[]}
* @returns {number[]} Trigger input.
*/
QVector<int> getTriggerInput() { return _triggerInputData; };
/**jsdoc
* Get left trigger output.
* @function AudioScope.getTriggerOutputLeft
* @returns {number[]}
* @returns {number[]} Left trigger output.
*/
QVector<int> getTriggerOutputLeft() { return _triggerOutputLeftData; };
/**jsdoc
* Get right trigger output.
* @function AudioScope.getTriggerOutputRight
* @returns {number[]}
* @returns {number[]} Right trigger output.
*/
QVector<int> getTriggerOutputRight() { return _triggerOutputRightData; };
/**jsdoc
* Set local echo.
* @function AudioScope.setLocalEcho
* @parm {boolean} localEcho
* @parm {boolean} localEcho - Local echo.
*/
void setLocalEcho(bool localEcho);
/**jsdoc
* Set server echo.
* @function AudioScope.setServerEcho
* @parm {boolean} serverEcho
* @parm {boolean} serverEcho - Server echo.
*/
void setServerEcho(bool serverEcho);
signals:
/**jsdoc
* Triggered when pause changes.
* @function AudioScope.pauseChanged
* @returns {Signal}
*/
void pauseChanged();
/**jsdoc
* Triggered when scope is triggered.
* @function AudioScope.triggered
* @returns {Signal}
*/

View file

@ -48,7 +48,6 @@
#include <recording/Clip.h>
#include <recording/Frame.h>
#include <RecordingScriptingInterface.h>
#include <trackers/FaceTracker.h>
#include <RenderableModelEntityItem.h>
#include <VariantMapToScriptValue.h>
@ -101,7 +100,7 @@ static const QString USER_RECENTER_MODEL_DISABLE_HMD_LEAN = QStringLiteral("Disa
const QString HEAD_BLEND_DIRECTIONAL_ALPHA_NAME = "lookAroundAlpha";
const QString HEAD_BLEND_LINEAR_ALPHA_NAME = "lookBlendAlpha";
const float HEAD_ALPHA_BLENDING = 1.0f;
const QString SEATED_HEAD_BLEND_LINEAR_ALPHA_NAME = "seatedLookBlendAlpha";
const QString POINT_REACTION_NAME = "point";
const QString POINT_BLEND_DIRECTIONAL_ALPHA_NAME = "pointAroundAlpha";
@ -349,7 +348,8 @@ MyAvatar::MyAvatar(QThread* thread) :
}
});
connect(&(_skeletonModel->getRig()), SIGNAL(onLoadComplete()), this, SIGNAL(onLoadComplete()));
connect(&(_skeletonModel->getRig()), &Rig::onLoadComplete, this, &MyAvatar::onLoadComplete);
connect(&(_skeletonModel->getRig()), &Rig::onLoadFailed, this, &MyAvatar::onLoadFailed);
_characterController.setDensity(_density);
}
@ -733,7 +733,7 @@ void MyAvatar::update(float deltaTime) {
_physicsSafetyPending = getCollisionsEnabled();
_characterController.recomputeFlying(); // In case we've gone to into the sky.
}
if (_goToFeetAjustment && _skeletonModelLoaded) {
if (_goToFeetAjustment && _skeletonModel->isLoaded()) {
auto feetAjustment = getWorldPosition() - getWorldFeetPosition();
_goToPosition = getWorldPosition() + feetAjustment;
setWorldPosition(_goToPosition);
@ -749,7 +749,6 @@ void MyAvatar::update(float deltaTime) {
Head* head = getHead();
head->relax(deltaTime);
updateFromTrackers(deltaTime);
if (getIsInWalkingState() && glm::length(getControllerPoseInAvatarFrame(controller::Action::HEAD).getVelocity()) < DEFAULT_AVATAR_WALK_SPEED_THRESHOLD) {
setIsInWalkingState(false);
@ -782,18 +781,6 @@ void MyAvatar::update(float deltaTime) {
emit energyChanged(currentEnergy);
updateEyeContactTarget(deltaTime);
// if we're getting eye rotations from a tracker, disable observer-side procedural eye motions
auto userInputMapper = DependencyManager::get<UserInputMapper>();
bool eyesTracked =
userInputMapper->getPoseState(controller::Action::LEFT_EYE).valid &&
userInputMapper->getPoseState(controller::Action::RIGHT_EYE).valid;
int leftEyeJointIndex = getJointIndex("LeftEye");
int rightEyeJointIndex = getJointIndex("RightEye");
bool eyesAreOverridden = getIsJointOverridden(leftEyeJointIndex) || getIsJointOverridden(rightEyeJointIndex);
_headData->setHasProceduralEyeMovement(!(eyesTracked || eyesAreOverridden));
}
void MyAvatar::updateEyeContactTarget(float deltaTime) {
@ -1148,60 +1135,6 @@ void MyAvatar::updateSensorToWorldMatrix() {
}
// Update avatar head rotation with sensor data
void MyAvatar::updateFromTrackers(float deltaTime) {
glm::vec3 estimatedRotation;
bool hasHead = getControllerPoseInAvatarFrame(controller::Action::HEAD).isValid();
bool playing = DependencyManager::get<recording::Deck>()->isPlaying();
if (hasHead && playing) {
return;
}
FaceTracker* tracker = qApp->getActiveFaceTracker();
bool inFacetracker = tracker && !FaceTracker::isMuted();
if (inFacetracker) {
estimatedRotation = glm::degrees(safeEulerAngles(tracker->getHeadRotation()));
}
// Rotate the body if the head is turned beyond the screen
if (Menu::getInstance()->isOptionChecked(MenuOption::TurnWithHead)) {
const float TRACKER_YAW_TURN_SENSITIVITY = 0.5f;
const float TRACKER_MIN_YAW_TURN = 15.0f;
const float TRACKER_MAX_YAW_TURN = 50.0f;
if ( (fabs(estimatedRotation.y) > TRACKER_MIN_YAW_TURN) &&
(fabs(estimatedRotation.y) < TRACKER_MAX_YAW_TURN) ) {
if (estimatedRotation.y > 0.0f) {
_bodyYawDelta += (estimatedRotation.y - TRACKER_MIN_YAW_TURN) * TRACKER_YAW_TURN_SENSITIVITY;
} else {
_bodyYawDelta += (estimatedRotation.y + TRACKER_MIN_YAW_TURN) * TRACKER_YAW_TURN_SENSITIVITY;
}
}
}
// Set the rotation of the avatar's head (as seen by others, not affecting view frustum)
// to be scaled such that when the user's physical head is pointing at edge of screen, the
// avatar head is at the edge of the in-world view frustum. So while a real person may move
// their head only 30 degrees or so, this may correspond to a 90 degree field of view.
// Note that roll is magnified by a constant because it is not related to field of view.
Head* head = getHead();
if (hasHead || playing) {
head->setDeltaPitch(estimatedRotation.x);
head->setDeltaYaw(estimatedRotation.y);
head->setDeltaRoll(estimatedRotation.z);
} else {
ViewFrustum viewFrustum;
qApp->copyViewFrustum(viewFrustum);
float magnifyFieldOfView = viewFrustum.getFieldOfView() / _realWorldFieldOfView.get();
head->setDeltaPitch(estimatedRotation.x * magnifyFieldOfView);
head->setDeltaYaw(estimatedRotation.y * magnifyFieldOfView);
head->setDeltaRoll(estimatedRotation.z);
}
}
glm::vec3 MyAvatar::getLeftHandPosition() const {
auto pose = getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND);
return pose.isValid() ? pose.getTranslation() : glm::vec3(0.0f);
@ -2158,7 +2091,7 @@ static float lookAtCostFunction(const glm::vec3& myForward, const glm::vec3& myP
const float DISTANCE_FACTOR = 3.14f;
const float MY_ANGLE_FACTOR = 1.0f;
const float OTHER_ANGLE_FACTOR = 1.0f;
const float OTHER_IS_TALKING_TERM = otherIsTalking ? 1.0f : 0.0f;
const float OTHER_IS_TALKING_TERM = otherIsTalking ? -1.0f : 0.0f;
const float LOOKING_AT_OTHER_ALREADY_TERM = lookingAtOtherAlready ? -0.2f : 0.0f;
const float GREATEST_LOOKING_AT_DISTANCE = 10.0f; // meters
@ -2184,6 +2117,9 @@ static float lookAtCostFunction(const glm::vec3& myForward, const glm::vec3& myP
void MyAvatar::computeMyLookAtTarget(const AvatarHash& hash) {
glm::vec3 myForward = _lookAtYaw * IDENTITY_FORWARD;
if (_skeletonModel->isLoaded()) {
myForward = getHeadJointFrontVector();
}
glm::vec3 myPosition = getHead()->getEyePosition();
CameraMode mode = qApp->getCamera().getMode();
if (mode == CAMERA_MODE_FIRST_PERSON_LOOK_AT || mode == CAMERA_MODE_FIRST_PERSON) {
@ -2196,7 +2132,7 @@ void MyAvatar::computeMyLookAtTarget(const AvatarHash& hash) {
foreach (const AvatarSharedPointer& avatarData, hash) {
std::shared_ptr<Avatar> avatar = std::static_pointer_cast<Avatar>(avatarData);
if (!avatar->isMyAvatar() && avatar->isInitialized()) {
glm::vec3 otherForward = avatar->getHead()->getForwardDirection();
glm::vec3 otherForward = avatar->getHeadJointFrontVector();
glm::vec3 otherPosition = avatar->getHead()->getEyePosition();
const float TIME_WITHOUT_TALKING_THRESHOLD = 1.0f;
bool otherIsTalking = avatar->getHead()->getTimeWithoutTalking() <= TIME_WITHOUT_TALKING_THRESHOLD;
@ -2284,7 +2220,9 @@ void MyAvatar::updateLookAtTargetAvatar() {
AvatarHash hash = DependencyManager::get<AvatarManager>()->getHashCopy();
// determine what the best look at target for my avatar should be.
computeMyLookAtTarget(hash);
if (!_scriptControlsEyesLookAt) {
computeMyLookAtTarget(hash);
}
// snap look at position for avatars that are looking at me.
snapOtherAvatarLookAtTargetsToMe(hash);
@ -2496,7 +2434,6 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
_headBoneSet.clear();
_cauterizationNeedsUpdate = true;
_skeletonModelLoaded = false;
std::shared_ptr<QMetaObject::Connection> skeletonConnection = std::make_shared<QMetaObject::Connection>();
*skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, [this, skeletonModelChangeCount, skeletonConnection]() {
@ -2515,8 +2452,6 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
_fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl();
initAnimGraph();
initFlowFromFST();
_skeletonModelLoaded = true;
}
QObject::disconnect(*skeletonConnection);
});
@ -2633,6 +2568,8 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN
if (urlString.isEmpty() || (fullAvatarURL != getSkeletonModelURL())) {
setSkeletonModelURL(fullAvatarURL);
UserActivityLogger::getInstance().changedModel("skeleton", urlString);
} else {
emit onLoadComplete();
}
}
@ -3414,31 +3351,6 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const {
return !defaultMode || (!firstPerson && !insideHead) || (overrideAnim && !insideHead);
}
void MyAvatar::setHasScriptedBlendshapes(bool hasScriptedBlendshapes) {
if (hasScriptedBlendshapes == _hasScriptedBlendShapes) {
return;
}
if (!hasScriptedBlendshapes) {
// send a forced avatarData update to make sure the script can send neutal blendshapes on unload
// without having to wait for the update loop, make sure _hasScriptedBlendShapes is still true
// before sending the update, or else it won't send the neutal blendshapes to the receiving clients
sendAvatarDataPacket(true);
}
_hasScriptedBlendShapes = hasScriptedBlendshapes;
}
void MyAvatar::setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement) {
_headData->setHasProceduralBlinkFaceMovement(hasProceduralBlinkFaceMovement);
}
void MyAvatar::setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement) {
_headData->setHasProceduralEyeFaceMovement(hasProceduralEyeFaceMovement);
}
void MyAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) {
_headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement);
}
void MyAvatar::setRotationRecenterFilterLength(float length) {
const float MINIMUM_ROTATION_RECENTER_FILTER_LENGTH = 0.01f;
_rotationRecenterFilterLength = std::max(MINIMUM_ROTATION_RECENTER_FILTER_LENGTH, length);
@ -5212,10 +5124,9 @@ bool MyAvatar::isReadyForPhysics() const {
void MyAvatar::setSprintMode(bool sprint) {
if (qApp->isHMDMode()) {
_walkSpeedScalar = sprint ? AVATAR_DESKTOP_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR;
}
else {
_walkSpeedScalar = sprint ? AVATAR_HMD_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR;
} else {
_walkSpeedScalar = sprint ? AVATAR_DESKTOP_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR;
}
}
@ -6619,11 +6530,10 @@ bool MyAvatar::getIsJointOverridden(int jointIndex) const {
return _skeletonModel->getIsJointOverridden(jointIndex);
}
void MyAvatar::updateLookAtPosition(FaceTracker* faceTracker, Camera& myCamera) {
void MyAvatar::updateEyesLookAtPosition(float deltaTime) {
updateLookAtTargetAvatar();
bool isLookingAtSomeone = false;
glm::vec3 lookAtSpot;
const MyHead* myHead = getMyHead();
@ -6649,6 +6559,13 @@ void MyAvatar::updateLookAtPosition(FaceTracker* faceTracker, Camera& myCamera)
} else {
lookAtSpot = myHead->getEyePosition() + glm::normalize(leftVec) * 1000.0f;
}
} else if (_scriptControlsEyesLookAt) {
if (_scriptEyesControlTimer < MAX_LOOK_AT_TIME_SCRIPT_CONTROL) {
_scriptEyesControlTimer += deltaTime;
lookAtSpot = _eyesLookAtTarget.get();
} else {
_scriptControlsEyesLookAt = false;
}
} else {
controller::Pose leftEyePose = getControllerPoseInAvatarFrame(controller::Action::LEFT_EYE);
controller::Pose rightEyePose = getControllerPoseInAvatarFrame(controller::Action::RIGHT_EYE);
@ -6677,7 +6594,6 @@ void MyAvatar::updateLookAtPosition(FaceTracker* faceTracker, Camera& myCamera)
avatar && avatar->getLookAtSnappingEnabled() && getLookAtSnappingEnabled();
if (haveLookAtCandidate && mutualLookAtSnappingEnabled) {
// If I am looking at someone else, look directly at one of their eyes
isLookingAtSomeone = true;
auto lookingAtHead = avatar->getHead();
const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE;
@ -6711,28 +6627,14 @@ void MyAvatar::updateLookAtPosition(FaceTracker* faceTracker, Camera& myCamera)
if (headPose.isValid()) {
lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE));
} else {
lookAtSpot = myHead->getEyePosition() +
(getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE));
lookAtSpot = _shouldTurnToFaceCamera ?
myHead->getLookAtPosition() :
myHead->getEyePosition() + getHeadJointFrontVector() * (float)TREE_SCALE;
}
}
// Deflect the eyes a bit to match the detected gaze from the face tracker if active.
if (faceTracker && !faceTracker->isMuted()) {
float eyePitch = faceTracker->getEstimatedEyePitch();
float eyeYaw = faceTracker->getEstimatedEyeYaw();
const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f;
glm::vec3 origin = myHead->getEyePosition();
float deflection = faceTracker->getEyeDeflection();
if (isLookingAtSomeone) {
deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT;
}
lookAtSpot = origin + myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3(
eyePitch * deflection, eyeYaw * deflection, 0.0f))) *
glm::inverse(myCamera.getOrientation()) * (lookAtSpot - origin);
}
}
}
_eyesLookAtTarget.set(lookAtSpot);
getHead()->setLookAtPosition(lookAtSpot);
}
@ -6769,9 +6671,18 @@ glm::vec3 MyAvatar::aimToBlendValues(const glm::vec3& aimVector, const glm::quat
}
void MyAvatar::resetHeadLookAt() {
if (_skeletonModelLoaded) {
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, glm::vec3(),
HEAD_BLEND_LINEAR_ALPHA_NAME, HEAD_ALPHA_BLENDING);
if (_skeletonModel->isLoaded()) {
if (isSeated()) {
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, glm::vec3(),
HEAD_BLEND_LINEAR_ALPHA_NAME, 0.0f);
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, glm::vec3(),
SEATED_HEAD_BLEND_LINEAR_ALPHA_NAME, 1.0f);
} else {
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, glm::vec3(),
HEAD_BLEND_LINEAR_ALPHA_NAME, 1.0f);
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, glm::vec3(),
SEATED_HEAD_BLEND_LINEAR_ALPHA_NAME, 0.0f);
}
}
}
@ -6784,17 +6695,26 @@ void MyAvatar::resetLookAtRotation(const glm::vec3& avatarPosition, const glm::q
resetHeadLookAt();
}
void MyAvatar::updateHeadLookAt(float deltaTime) {
if (_skeletonModelLoaded) {
void MyAvatar::updateHeadLookAt(float deltaTime) {
if (_skeletonModel->isLoaded()) {
glm::vec3 lookAtTarget = _scriptControlsHeadLookAt ? _lookAtScriptTarget : _lookAtCameraTarget;
glm::vec3 aimVector = lookAtTarget - getDefaultEyePosition();
glm::vec3 lookAtBlend = MyAvatar::aimToBlendValues(aimVector, getWorldOrientation());
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, lookAtBlend,
HEAD_BLEND_LINEAR_ALPHA_NAME, HEAD_ALPHA_BLENDING);
if (isSeated()) {
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, lookAtBlend,
HEAD_BLEND_LINEAR_ALPHA_NAME, 0.0f);
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, lookAtBlend,
SEATED_HEAD_BLEND_LINEAR_ALPHA_NAME, 1.0f);
} else {
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, lookAtBlend,
HEAD_BLEND_LINEAR_ALPHA_NAME, 1.0f);
_skeletonModel->getRig().setDirectionalBlending(HEAD_BLEND_DIRECTIONAL_ALPHA_NAME, lookAtBlend,
SEATED_HEAD_BLEND_LINEAR_ALPHA_NAME, 0.0f);
}
if (_scriptControlsHeadLookAt) {
_scriptHeadControlTimer += deltaTime;
if (_scriptHeadControlTimer > MAX_LOOK_AT_TIME_SCRIPT_CONTROL) {
if (_scriptHeadControlTimer >= MAX_LOOK_AT_TIME_SCRIPT_CONTROL) {
_scriptHeadControlTimer = 0.0f;
_scriptControlsHeadLookAt = false;
_lookAtCameraTarget = _lookAtScriptTarget;
@ -6815,6 +6735,25 @@ void MyAvatar::setHeadLookAt(const glm::vec3& lookAtTarget) {
_lookAtScriptTarget = lookAtTarget;
}
void MyAvatar::setEyesLookAt(const glm::vec3& lookAtTarget) {
if (QThread::currentThread() != thread()) {
BLOCKING_INVOKE_METHOD(this, "setEyesLookAt",
Q_ARG(const glm::vec3&, lookAtTarget));
return;
}
_eyesLookAtTarget.set(lookAtTarget);
_scriptEyesControlTimer = 0.0f;
_scriptControlsEyesLookAt = true;
}
void MyAvatar::releaseHeadLookAtControl() {
_scriptHeadControlTimer = MAX_LOOK_AT_TIME_SCRIPT_CONTROL;
}
void MyAvatar::releaseEyesLookAtControl() {
_scriptEyesControlTimer = MAX_LOOK_AT_TIME_SCRIPT_CONTROL;
}
glm::vec3 MyAvatar::getLookAtPivotPoint() {
glm::vec3 avatarUp = getWorldOrientation() * Vectors::UP;
glm::vec3 yAxisEyePosition = getWorldPosition() + avatarUp * glm::dot(avatarUp, _skeletonModel->getDefaultEyeModelPosition());
@ -6888,7 +6827,7 @@ bool MyAvatar::setPointAt(const glm::vec3& pointAtTarget) {
Q_ARG(const glm::vec3&, pointAtTarget));
return result;
}
if (_skeletonModelLoaded && _pointAtActive) {
if (_skeletonModel->isLoaded() && _pointAtActive) {
glm::vec3 aimVector = pointAtTarget - getJointPosition(POINT_REF_JOINT_NAME);
_isPointTargetValid = glm::dot(aimVector, getWorldOrientation() * Vectors::FRONT) > 0.0f;
if (_isPointTargetValid) {
@ -6902,9 +6841,8 @@ bool MyAvatar::setPointAt(const glm::vec3& pointAtTarget) {
}
void MyAvatar::resetPointAt() {
if (_skeletonModelLoaded) {
if (_skeletonModel->isLoaded()) {
_skeletonModel->getRig().setDirectionalBlending(POINT_BLEND_DIRECTIONAL_ALPHA_NAME, glm::vec3(),
POINT_BLEND_LINEAR_ALPHA_NAME, POINT_ALPHA_BLENDING);
}
}

View file

@ -34,7 +34,6 @@
#include "AtRestDetector.h"
#include "MyCharacterController.h"
#include "RingBufferHistory.h"
#include "devices/DdeFaceTracker.h"
class AvatarActionHold;
class ModelItemID;
@ -184,12 +183,6 @@ class MyAvatar : public Avatar {
* property value is <code>audioListenerModeCustom</code>.
* @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the
* <code>audioListenerMode</code> property value is <code>audioListenerModeCustom</code>.
* @property {boolean} hasScriptedBlendshapes=false - <code>true</code> to transmit blendshapes over the network.
* <p><strong>Note:</strong> Currently doesn't work. Use {@link MyAvatar.setForceFaceTrackerConnected} instead.</p>
* @property {boolean} hasProceduralBlinkFaceMovement=true - <code>true</code> if procedural blinking is turned on.
* @property {boolean} hasProceduralEyeFaceMovement=true - <code>true</code> if procedural eye movement is turned on.
* @property {boolean} hasAudioEnabledFaceMovement=true - <code>true</code> to move the mouth blendshapes with voice audio
* when <code>MyAvatar.hasScriptedBlendshapes</code> is enabled.
* @property {number} rotationRecenterFilterLength - Configures how quickly the avatar root rotates to recenter its facing
* direction to match that of the user's torso based on head and hands orientation. A smaller value makes the
* recentering happen more quickly. The minimum value is <code>0.01</code>.
@ -275,7 +268,7 @@ class MyAvatar : public Avatar {
* @property {number} analogPlusSprintSpeed - The sprint (run) speed of your avatar for the "AnalogPlus" control scheme.
* @property {MyAvatar.SitStandModelType} userRecenterModel - Controls avatar leaning and recentering behavior.
* @property {number} isInSittingState - <code>true</code> if the user wearing the HMD is determined to be sitting
* (avatar leaning is disabled, recenntering is enabled), <code>false</code> if the user wearing the HMD is
* (avatar leaning is disabled, recentering is enabled), <code>false</code> if the user wearing the HMD is
* determined to be standing (avatar leaning is enabled, and avatar recenters if it leans too far).
* If <code>userRecenterModel == 2</code> (i.e., auto) the property value automatically updates as the user sits
* or stands, unless <code>isSitStandStateLocked == true</code>. Setting the property value overrides the current
@ -312,7 +305,10 @@ class MyAvatar : public Avatar {
* @borrows Avatar.setAttachmentsVariant as setAttachmentsVariant
* @borrows Avatar.updateAvatarEntity as updateAvatarEntity
* @borrows Avatar.clearAvatarEntity as clearAvatarEntity
* @borrows Avatar.setForceFaceTrackerConnected as setForceFaceTrackerConnected
* @borrows Avatar.hasScriptedBlendshapes as hasScriptedBlendshapes
* @borrows Avatar.hasProceduralBlinkFaceMovement as hasProceduralBlinkFaceMovement
* @borrows Avatar.hasProceduralEyeFaceMovement as hasProceduralEyeFaceMovement
* @borrows Avatar.hasAudioEnabledFaceMovement as hasAudioEnabledFaceMovement
* @borrows Avatar.setSkeletonModelURL as setSkeletonModelURL
* @borrows Avatar.getAttachmentData as getAttachmentData
* @borrows Avatar.setAttachmentData as setAttachmentData
@ -359,10 +355,6 @@ class MyAvatar : public Avatar {
Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom)
Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition)
Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation)
Q_PROPERTY(bool hasScriptedBlendshapes READ getHasScriptedBlendshapes WRITE setHasScriptedBlendshapes)
Q_PROPERTY(bool hasProceduralBlinkFaceMovement READ getHasProceduralBlinkFaceMovement WRITE setHasProceduralBlinkFaceMovement)
Q_PROPERTY(bool hasProceduralEyeFaceMovement READ getHasProceduralEyeFaceMovement WRITE setHasProceduralEyeFaceMovement)
Q_PROPERTY(bool hasAudioEnabledFaceMovement READ getHasAudioEnabledFaceMovement WRITE setHasAudioEnabledFaceMovement)
Q_PROPERTY(float rotationRecenterFilterLength READ getRotationRecenterFilterLength WRITE setRotationRecenterFilterLength)
Q_PROPERTY(float rotationThreshold READ getRotationThreshold WRITE setRotationThreshold)
Q_PROPERTY(bool enableStepResetRotation READ getEnableStepResetRotation WRITE setEnableStepResetRotation)
@ -1765,10 +1757,38 @@ public:
/**jsdoc
* Returns the current head look at target point in world coordinates.
* @function MyAvatar.getHeadLookAt
* @returns {Vec3} Default position between your avatar's eyes in world coordinates.
* @returns {Vec3} The head's look at target in world coordinates.
*/
Q_INVOKABLE glm::vec3 getHeadLookAt() { return _lookAtCameraTarget; }
/**jsdoc
* When this function is called the engine regains control of the head immediately.
* @function MyAvatar.releaseHeadLookAtControl
*/
Q_INVOKABLE void releaseHeadLookAtControl();
/**jsdoc
* Force the avatar's eyes to look to the specified location.
* Once this method is called, API calls will have full control of the eyes for a limited time.
* If this method is not called for two seconds, the engine will regain control of the eyes.
* @function MyAvatar.setEyesLookAt
* @param {Vec3} lookAtTarget - The target point in world coordinates.
*/
Q_INVOKABLE void setEyesLookAt(const glm::vec3& lookAtTarget);
/**jsdoc
* Returns the current eyes look at target point in world coordinates.
* @function MyAvatar.getEyesLookAt
* @returns {Vec3} The eyes's look at target in world coordinates.
*/
Q_INVOKABLE glm::vec3 getEyesLookAt() { return _eyesLookAtTarget.get(); }
/**jsdoc
* When this function is called the engine regains control of the eyes immediately.
* @function MyAvatar.releaseEyesLookAtControl
*/
Q_INVOKABLE void releaseEyesLookAtControl();
/**jsdoc
* Aims the pointing directional blending towards the provided target point.
* The "point" reaction should be triggered before using this method.
@ -1906,7 +1926,7 @@ public:
bool getFlowActive() const;
bool getNetworkGraphActive() const;
void updateLookAtPosition(FaceTracker* faceTracker, Camera& myCamera);
void updateEyesLookAtPosition(float deltaTime);
// sets the reaction enabled and triggered parameters of the passed in params
// also clears internal reaction triggers
@ -2435,10 +2455,17 @@ signals:
/**jsdoc
* Triggered when the avatar's model finishes loading.
* @function MyAvatar.onLoadComplete
* @returns {Signal}
* @returns {Signal}
*/
void onLoadComplete();
/**jsdoc
* Triggered when the avatar's model has failed to load.
* @function MyAvatar.onLoadFailed
* @returns {Signal}
*/
void onLoadFailed();
/**jsdoc
* Triggered when your avatar changes from being active to being away.
* @function MyAvatar.wentAway
@ -2551,20 +2578,11 @@ private:
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) override;
void simulate(float deltaTime, bool inView) override;
void updateFromTrackers(float deltaTime);
void saveAvatarUrl();
virtual void render(RenderArgs* renderArgs) override;
virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override;
void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); }
bool getShouldRenderLocally() const { return _shouldRender; }
void setHasScriptedBlendshapes(bool hasScriptedBlendshapes);
bool getHasScriptedBlendshapes() const override { return _hasScriptedBlendShapes; }
void setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement);
bool getHasProceduralBlinkFaceMovement() const override { return _headData->getHasProceduralBlinkFaceMovement(); }
void setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement);
bool getHasProceduralEyeFaceMovement() const override { return _headData->getHasProceduralEyeFaceMovement(); }
void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement);
bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); }
void setRotationRecenterFilterLength(float length);
float getRotationRecenterFilterLength() const { return _rotationRecenterFilterLength; }
void setRotationThreshold(float angleRadians);
@ -2666,6 +2684,9 @@ private:
eyeContactTarget _eyeContactTarget;
float _eyeContactTargetTimer { 0.0f };
ThreadSafeValueCache<glm::vec3> _eyesLookAtTarget { glm::vec3() };
bool _scriptControlsEyesLookAt{ false };
float _scriptEyesControlTimer{ 0.0f };
glm::vec3 _trackedHeadPosition;
@ -2899,7 +2920,6 @@ private:
bool _haveReceivedHeightLimitsFromDomain { false };
int _disableHandTouchCount { 0 };
bool _skeletonModelLoaded { false };
bool _reloadAvatarEntityDataFromSettings { true };
TimePoint _nextTraitsSendWindow;

View file

@ -14,15 +14,80 @@
#include <NodeList.h>
#include <recording/Deck.h>
#include <Rig.h>
#include <trackers/FaceTracker.h>
#include <FaceshiftConstants.h>
#include <BlendshapeConstants.h>
#include "devices/DdeFaceTracker.h"
#include "Application.h"
#include "MyAvatar.h"
using namespace std;
static controller::Action blendshapeActions[] = {
controller::Action::EYEBLINK_L,
controller::Action::EYEBLINK_R,
controller::Action::EYESQUINT_L,
controller::Action::EYESQUINT_R,
controller::Action::EYEDOWN_L,
controller::Action::EYEDOWN_R,
controller::Action::EYEIN_L,
controller::Action::EYEIN_R,
controller::Action::EYEOPEN_L,
controller::Action::EYEOPEN_R,
controller::Action::EYEOUT_L,
controller::Action::EYEOUT_R,
controller::Action::EYEUP_L,
controller::Action::EYEUP_R,
controller::Action::BROWSD_L,
controller::Action::BROWSD_R,
controller::Action::BROWSU_C,
controller::Action::BROWSU_L,
controller::Action::BROWSU_R,
controller::Action::JAWFWD,
controller::Action::JAWLEFT,
controller::Action::JAWOPEN,
controller::Action::JAWRIGHT,
controller::Action::MOUTHLEFT,
controller::Action::MOUTHRIGHT,
controller::Action::MOUTHFROWN_L,
controller::Action::MOUTHFROWN_R,
controller::Action::MOUTHSMILE_L,
controller::Action::MOUTHSMILE_R,
controller::Action::MOUTHDIMPLE_L,
controller::Action::MOUTHDIMPLE_R,
controller::Action::LIPSSTRETCH_L,
controller::Action::LIPSSTRETCH_R,
controller::Action::LIPSUPPERCLOSE,
controller::Action::LIPSLOWERCLOSE,
controller::Action::LIPSUPPEROPEN,
controller::Action::LIPSLOWEROPEN,
controller::Action::LIPSFUNNEL,
controller::Action::LIPSPUCKER,
controller::Action::PUFF,
controller::Action::CHEEKSQUINT_L,
controller::Action::CHEEKSQUINT_R,
controller::Action::MOUTHCLOSE,
controller::Action::MOUTHUPPERUP_L,
controller::Action::MOUTHUPPERUP_R,
controller::Action::MOUTHLOWERDOWN_L,
controller::Action::MOUTHLOWERDOWN_R,
controller::Action::MOUTHPRESS_L,
controller::Action::MOUTHPRESS_R,
controller::Action::MOUTHSHRUGLOWER,
controller::Action::MOUTHSHRUGUPPER,
controller::Action::NOSESNEER_L,
controller::Action::NOSESNEER_R,
controller::Action::TONGUEOUT,
controller::Action::USERBLENDSHAPE0,
controller::Action::USERBLENDSHAPE1,
controller::Action::USERBLENDSHAPE2,
controller::Action::USERBLENDSHAPE3,
controller::Action::USERBLENDSHAPE4,
controller::Action::USERBLENDSHAPE5,
controller::Action::USERBLENDSHAPE6,
controller::Action::USERBLENDSHAPE7,
controller::Action::USERBLENDSHAPE8,
controller::Action::USERBLENDSHAPE9
};
MyHead::MyHead(MyAvatar* owningAvatar) : Head(owningAvatar) {
}
@ -46,36 +111,57 @@ void MyHead::simulate(float deltaTime) {
auto player = DependencyManager::get<recording::Deck>();
// Only use face trackers when not playing back a recording.
if (!player->isPlaying()) {
// TODO -- finish removing face-tracker specific code. To do this, add input channels for
// each blendshape-coefficient and update the various json files to relay them in a useful way.
// After that, input plugins can be used to drive the avatar's face, and the various "DDE" files
// can be ported into the plugin and removed.
//
// auto faceTracker = qApp->getActiveFaceTracker();
// const bool hasActualFaceTrackerConnected = faceTracker && !faceTracker->isMuted();
// _isFaceTrackerConnected = hasActualFaceTrackerConnected || _owningAvatar->getHasScriptedBlendshapes();
// if (_isFaceTrackerConnected) {
// if (hasActualFaceTrackerConnected) {
// _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients();
// }
// }
auto userInputMapper = DependencyManager::get<UserInputMapper>();
// if input system has control over blink blendshapes
bool eyeLidsTracked =
userInputMapper->getActionStateValid(controller::Action::LEFT_EYE_BLINK) &&
userInputMapper->getActionStateValid(controller::Action::RIGHT_EYE_BLINK);
setFaceTrackerConnected(eyeLidsTracked);
if (eyeLidsTracked) {
float leftEyeBlink = userInputMapper->getActionState(controller::Action::LEFT_EYE_BLINK);
float rightEyeBlink = userInputMapper->getActionState(controller::Action::RIGHT_EYE_BLINK);
_blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2));
_blendshapeCoefficients[EYE_BLINK_INDICES[0]] = leftEyeBlink;
_blendshapeCoefficients[EYE_BLINK_INDICES[1]] = rightEyeBlink;
} else {
const float FULLY_OPEN = 0.0f;
_blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2));
_blendshapeCoefficients[EYE_BLINK_INDICES[0]] = FULLY_OPEN;
_blendshapeCoefficients[EYE_BLINK_INDICES[1]] = FULLY_OPEN;
userInputMapper->getActionStateValid(controller::Action::EYEBLINK_L) ||
userInputMapper->getActionStateValid(controller::Action::EYEBLINK_R);
// if input system has control over the brows.
bool browsTracked =
userInputMapper->getActionStateValid(controller::Action::BROWSD_L) ||
userInputMapper->getActionStateValid(controller::Action::BROWSD_R) ||
userInputMapper->getActionStateValid(controller::Action::BROWSU_L) ||
userInputMapper->getActionStateValid(controller::Action::BROWSU_R) ||
userInputMapper->getActionStateValid(controller::Action::BROWSU_C);
// if input system has control of mouth
bool mouthTracked =
userInputMapper->getActionStateValid(controller::Action::JAWOPEN) ||
userInputMapper->getActionStateValid(controller::Action::LIPSUPPERCLOSE) ||
userInputMapper->getActionStateValid(controller::Action::LIPSLOWERCLOSE) ||
userInputMapper->getActionStateValid(controller::Action::LIPSFUNNEL) ||
userInputMapper->getActionStateValid(controller::Action::MOUTHSMILE_L) ||
userInputMapper->getActionStateValid(controller::Action::MOUTHSMILE_R);
bool eyesTracked =
userInputMapper->getPoseState(controller::Action::LEFT_EYE).valid &&
userInputMapper->getPoseState(controller::Action::RIGHT_EYE).valid;
MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
int leftEyeJointIndex = myAvatar->getJointIndex("LeftEye");
int rightEyeJointIndex = myAvatar->getJointIndex("RightEye");
bool eyeJointsOverridden = myAvatar->getIsJointOverridden(leftEyeJointIndex) || myAvatar->getIsJointOverridden(rightEyeJointIndex);
bool anyInputTracked = false;
for (int i = 0; i < (int)Blendshapes::BlendshapeCount; i++) {
anyInputTracked = anyInputTracked || userInputMapper->getActionStateValid(blendshapeActions[i]);
}
setHasInputDrivenBlendshapes(anyInputTracked);
// suppress any procedural blendshape animation if they overlap with driven input.
setSuppressProceduralAnimationFlag(HeadData::BlinkProceduralBlendshapeAnimation, eyeLidsTracked);
setSuppressProceduralAnimationFlag(HeadData::LidAdjustmentProceduralBlendshapeAnimation, eyeLidsTracked || browsTracked);
setSuppressProceduralAnimationFlag(HeadData::AudioProceduralBlendshapeAnimation, mouthTracked);
setSuppressProceduralAnimationFlag(HeadData::SaccadeProceduralEyeJointAnimation, eyesTracked || eyeJointsOverridden);
if (anyInputTracked) {
for (int i = 0; i < (int)Blendshapes::BlendshapeCount; i++) {
_blendshapeCoefficients[i] = userInputMapper->getActionState(blendshapeActions[i]);
}
}
}
Parent::simulate(deltaTime);

View file

@ -112,9 +112,13 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) {
void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
const HFMModel& hfmModel = getHFMModel();
MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
assert(myAvatar);
Head* head = _owningAvatar->getHead();
bool eyePosesValid = !head->getHasProceduralEyeMovement();
bool eyePosesValid = (myAvatar->getControllerPoseInSensorFrame(controller::Action::LEFT_EYE).isValid() ||
myAvatar->getControllerPoseInSensorFrame(controller::Action::RIGHT_EYE).isValid());
glm::vec3 lookAt;
if (eyePosesValid) {
lookAt = head->getLookAtPosition(); // don't apply no-crosseyes code when eyes are being tracked
@ -122,9 +126,6 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
lookAt = avoidCrossedEyes(head->getLookAtPosition());
}
MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
assert(myAvatar);
Rig::ControllerParameters params;
AnimPose avatarToRigPose(glm::vec3(1.0f), Quaternions::Y_180, glm::vec3(0.0f));

View file

@ -267,6 +267,7 @@ void OtherAvatar::simulate(float deltaTime, bool inView) {
_skeletonModel->getRig().computeExternalPoses(rootTransform);
_jointDataSimulationRate.increment();
head->simulate(deltaTime);
_skeletonModel->simulate(deltaTime, true);
locationChanged(); // joints changed, so if there are any children, update them.
@ -277,9 +278,11 @@ void OtherAvatar::simulate(float deltaTime, bool inView) {
headPosition = getWorldPosition();
}
head->setPosition(headPosition);
} else {
head->simulate(deltaTime);
_skeletonModel->simulate(deltaTime, false);
}
head->setScale(getModelScale());
head->simulate(deltaTime);
relayJointDataToChildren();
} else {
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.

View file

@ -1,686 +0,0 @@
//
// DdeFaceTracker.cpp
//
//
// Created by Clement on 8/2/14.
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "DdeFaceTracker.h"
#include <SharedUtil.h>
#include <QtCore/QCoreApplication>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QTimer>
#include <GLMHelpers.h>
#include <NumericalConstants.h>
#include <FaceshiftConstants.h>
#include "Application.h"
#include "InterfaceLogging.h"
#include "Menu.h"
static const QHostAddress DDE_SERVER_ADDR("127.0.0.1");
static const quint16 DDE_SERVER_PORT = 64204;
static const quint16 DDE_CONTROL_PORT = 64205;
#if defined(Q_OS_WIN)
static const QString DDE_PROGRAM_PATH = "/dde/dde.exe";
#elif defined(Q_OS_MAC)
static const QString DDE_PROGRAM_PATH = "/dde.app/Contents/MacOS/dde";
#endif
static const QStringList DDE_ARGUMENTS = QStringList()
<< "--udp=" + DDE_SERVER_ADDR.toString() + ":" + QString::number(DDE_SERVER_PORT)
<< "--receiver=" + QString::number(DDE_CONTROL_PORT)
<< "--facedet_interval=500" // ms
<< "--headless";
static const int NUM_EXPRESSIONS = 46;
static const int MIN_PACKET_SIZE = (8 + NUM_EXPRESSIONS) * sizeof(float) + sizeof(int);
static const int MAX_NAME_SIZE = 31;
// There's almost but not quite a 1-1 correspondence between DDE's 46 and Faceshift 1.3's 48 packets.
// The best guess at mapping is to:
// - Swap L and R values
// - Skip two Faceshift values: JawChew (22) and LipsLowerDown (37)
static const int DDE_TO_FACESHIFT_MAPPING[] = {
1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10, 13, 12, 15, 14,
16,
18, 17,
19,
23,
21,
// Skip JawChew
20,
25, 24, 27, 26, 29, 28, 31, 30, 33, 32,
34, 35, 36,
// Skip LipsLowerDown
38, 39, 40, 41, 42, 43, 44, 45,
47, 46
};
// The DDE coefficients, overall, range from -0.2 to 1.5 or so. However, individual coefficients typically vary much
// less than this.
static const float DDE_COEFFICIENT_SCALES[] = {
1.0f, // EyeBlink_L
1.0f, // EyeBlink_R
1.0f, // EyeSquint_L
1.0f, // EyeSquint_R
1.0f, // EyeDown_L
1.0f, // EyeDown_R
1.0f, // EyeIn_L
1.0f, // EyeIn_R
1.0f, // EyeOpen_L
1.0f, // EyeOpen_R
1.0f, // EyeOut_L
1.0f, // EyeOut_R
1.0f, // EyeUp_L
1.0f, // EyeUp_R
3.0f, // BrowsD_L
3.0f, // BrowsD_R
3.0f, // BrowsU_C
3.0f, // BrowsU_L
3.0f, // BrowsU_R
1.0f, // JawFwd
2.0f, // JawLeft
1.8f, // JawOpen
1.0f, // JawChew
2.0f, // JawRight
1.5f, // MouthLeft
1.5f, // MouthRight
1.5f, // MouthFrown_L
1.5f, // MouthFrown_R
2.5f, // MouthSmile_L
2.5f, // MouthSmile_R
1.0f, // MouthDimple_L
1.0f, // MouthDimple_R
1.0f, // LipsStretch_L
1.0f, // LipsStretch_R
1.0f, // LipsUpperClose
1.0f, // LipsLowerClose
1.0f, // LipsUpperUp
1.0f, // LipsLowerDown
1.0f, // LipsUpperOpen
1.0f, // LipsLowerOpen
1.5f, // LipsFunnel
2.5f, // LipsPucker
1.5f, // ChinLowerRaise
1.5f, // ChinUpperRaise
1.0f, // Sneer
3.0f, // Puff
1.0f, // CheekSquint_L
1.0f // CheekSquint_R
};
struct DDEPacket {
//roughly in mm
float focal_length[1];
float translation[3];
//quaternion
float rotation[4];
// The DDE coefficients, overall, range from -0.2 to 1.5 or so. However, individual coefficients typically vary much
// less than this.
float expressions[NUM_EXPRESSIONS];
//avatar id selected on the UI
int avatar_id;
//client name, arbitrary length
char name[MAX_NAME_SIZE + 1];
};
static const float STARTING_DDE_MESSAGE_TIME = 0.033f;
static const float DEFAULT_DDE_EYE_CLOSING_THRESHOLD = 0.8f;
static const int CALIBRATION_SAMPLES = 150;
DdeFaceTracker::DdeFaceTracker() :
DdeFaceTracker(QHostAddress::Any, DDE_SERVER_PORT, DDE_CONTROL_PORT)
{
}
DdeFaceTracker::DdeFaceTracker(const QHostAddress& host, quint16 serverPort, quint16 controlPort) :
_ddeProcess(NULL),
_ddeStopping(false),
_host(host),
_serverPort(serverPort),
_controlPort(controlPort),
_lastReceiveTimestamp(0),
_reset(false),
_leftBlinkIndex(0), // see http://support.faceshift.com/support/articles/35129-export-of-blendshapes
_rightBlinkIndex(1),
_leftEyeDownIndex(4),
_rightEyeDownIndex(5),
_leftEyeInIndex(6),
_rightEyeInIndex(7),
_leftEyeOpenIndex(8),
_rightEyeOpenIndex(9),
_browDownLeftIndex(14),
_browDownRightIndex(15),
_browUpCenterIndex(16),
_browUpLeftIndex(17),
_browUpRightIndex(18),
_mouthSmileLeftIndex(28),
_mouthSmileRightIndex(29),
_jawOpenIndex(21),
_lastMessageReceived(0),
_averageMessageTime(STARTING_DDE_MESSAGE_TIME),
_lastHeadTranslation(glm::vec3(0.0f)),
_filteredHeadTranslation(glm::vec3(0.0f)),
_lastBrowUp(0.0f),
_filteredBrowUp(0.0f),
_eyePitch(0.0f),
_eyeYaw(0.0f),
_lastEyePitch(0.0f),
_lastEyeYaw(0.0f),
_filteredEyePitch(0.0f),
_filteredEyeYaw(0.0f),
_longTermAverageEyePitch(0.0f),
_longTermAverageEyeYaw(0.0f),
_lastEyeBlinks(),
_filteredEyeBlinks(),
_lastEyeCoefficients(),
_eyeClosingThreshold("ddeEyeClosingThreshold", DEFAULT_DDE_EYE_CLOSING_THRESHOLD),
_isCalibrating(false),
_calibrationCount(0),
_calibrationValues(),
_calibrationBillboard(NULL),
_calibrationMessage(QString()),
_isCalibrated(false)
{
_coefficients.resize(NUM_FACESHIFT_BLENDSHAPES);
_blendshapeCoefficients.resize(NUM_FACESHIFT_BLENDSHAPES);
_coefficientAverages.resize(NUM_FACESHIFT_BLENDSHAPES);
_calibrationValues.resize(NUM_FACESHIFT_BLENDSHAPES);
_eyeStates[0] = EYE_UNCONTROLLED;
_eyeStates[1] = EYE_UNCONTROLLED;
connect(&_udpSocket, SIGNAL(readyRead()), SLOT(readPendingDatagrams()));
connect(&_udpSocket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(socketErrorOccurred(QAbstractSocket::SocketError)));
connect(&_udpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)),
SLOT(socketStateChanged(QAbstractSocket::SocketState)));
}
DdeFaceTracker::~DdeFaceTracker() {
setEnabled(false);
if (_isCalibrating) {
cancelCalibration();
}
}
void DdeFaceTracker::init() {
FaceTracker::init();
setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::UseCamera) && !_isMuted);
Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setEnabled(!_isMuted);
}
void DdeFaceTracker::setEnabled(bool enabled) {
if (!_isInitialized) {
// Don't enable until have explicitly initialized
return;
}
#ifdef HAVE_DDE
if (_isCalibrating) {
cancelCalibration();
}
// isOpen() does not work as one might expect on QUdpSocket; don't test isOpen() before closing socket.
_udpSocket.close();
// Terminate any existing DDE process, perhaps left running after an Interface crash.
// Do this even if !enabled in case user reset their settings after crash.
const char* DDE_EXIT_COMMAND = "exit";
_udpSocket.bind(_host, _serverPort);
_udpSocket.writeDatagram(DDE_EXIT_COMMAND, DDE_SERVER_ADDR, _controlPort);
if (enabled && !_ddeProcess) {
_ddeStopping = false;
qCDebug(interfaceapp) << "DDE Face Tracker: Starting";
_ddeProcess = new QProcess(qApp);
connect(_ddeProcess, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(processFinished(int, QProcess::ExitStatus)));
_ddeProcess->start(QCoreApplication::applicationDirPath() + DDE_PROGRAM_PATH, DDE_ARGUMENTS);
}
if (!enabled && _ddeProcess) {
_ddeStopping = true;
qCDebug(interfaceapp) << "DDE Face Tracker: Stopping";
}
#endif
}
void DdeFaceTracker::processFinished(int exitCode, QProcess::ExitStatus exitStatus) {
if (_ddeProcess) {
if (_ddeStopping) {
qCDebug(interfaceapp) << "DDE Face Tracker: Stopped";
} else {
qCWarning(interfaceapp) << "DDE Face Tracker: Stopped unexpectedly";
Menu::getInstance()->setIsOptionChecked(MenuOption::NoFaceTracking, true);
}
_udpSocket.close();
delete _ddeProcess;
_ddeProcess = NULL;
}
}
void DdeFaceTracker::reset() {
if (_udpSocket.state() == QAbstractSocket::BoundState) {
_reset = true;
qCDebug(interfaceapp) << "DDE Face Tracker: Reset";
const char* DDE_RESET_COMMAND = "reset";
_udpSocket.writeDatagram(DDE_RESET_COMMAND, DDE_SERVER_ADDR, _controlPort);
FaceTracker::reset();
_reset = true;
}
}
void DdeFaceTracker::update(float deltaTime) {
if (!isActive()) {
return;
}
FaceTracker::update(deltaTime);
glm::vec3 headEulers = glm::degrees(glm::eulerAngles(_headRotation));
_estimatedEyePitch = _eyePitch - headEulers.x;
_estimatedEyeYaw = _eyeYaw - headEulers.y;
}
bool DdeFaceTracker::isActive() const {
return (_ddeProcess != NULL);
}
bool DdeFaceTracker::isTracking() const {
static const quint64 ACTIVE_TIMEOUT_USECS = 3000000; //3 secs
return (usecTimestampNow() - _lastReceiveTimestamp < ACTIVE_TIMEOUT_USECS);
}
//private slots and methods
void DdeFaceTracker::socketErrorOccurred(QAbstractSocket::SocketError socketError) {
qCWarning(interfaceapp) << "DDE Face Tracker: Socket error: " << _udpSocket.errorString();
}
void DdeFaceTracker::socketStateChanged(QAbstractSocket::SocketState socketState) {
QString state;
switch(socketState) {
case QAbstractSocket::BoundState:
state = "Bound";
break;
case QAbstractSocket::ClosingState:
state = "Closing";
break;
case QAbstractSocket::ConnectedState:
state = "Connected";
break;
case QAbstractSocket::ConnectingState:
state = "Connecting";
break;
case QAbstractSocket::HostLookupState:
state = "Host Lookup";
break;
case QAbstractSocket::ListeningState:
state = "Listening";
break;
case QAbstractSocket::UnconnectedState:
state = "Unconnected";
break;
}
qCDebug(interfaceapp) << "DDE Face Tracker: Socket: " << state;
}
void DdeFaceTracker::readPendingDatagrams() {
QByteArray buffer;
while (_udpSocket.hasPendingDatagrams()) {
buffer.resize(_udpSocket.pendingDatagramSize());
_udpSocket.readDatagram(buffer.data(), buffer.size());
}
decodePacket(buffer);
}
float DdeFaceTracker::getBlendshapeCoefficient(int index) const {
return (index >= 0 && index < (int)_blendshapeCoefficients.size()) ? _blendshapeCoefficients[index] : 0.0f;
}
void DdeFaceTracker::decodePacket(const QByteArray& buffer) {
_lastReceiveTimestamp = usecTimestampNow();
if (buffer.size() > MIN_PACKET_SIZE) {
if (!_isCalibrated) {
calibrate();
}
bool isFiltering = Menu::getInstance()->isOptionChecked(MenuOption::VelocityFilter);
DDEPacket packet;
int bytesToCopy = glm::min((int)sizeof(packet), buffer.size());
memset(&packet.name, '\n', MAX_NAME_SIZE + 1);
memcpy(&packet, buffer.data(), bytesToCopy);
glm::vec3 translation;
memcpy(&translation, packet.translation, sizeof(packet.translation));
glm::quat rotation;
memcpy(&rotation, &packet.rotation, sizeof(packet.rotation));
if (_reset || (_lastMessageReceived == 0)) {
memcpy(&_referenceTranslation, &translation, sizeof(glm::vec3));
memcpy(&_referenceRotation, &rotation, sizeof(glm::quat));
_reset = false;
}
// Compute relative translation
float LEAN_DAMPING_FACTOR = 75.0f;
translation -= _referenceTranslation;
translation /= LEAN_DAMPING_FACTOR;
translation.x *= -1;
if (isFiltering) {
glm::vec3 linearVelocity = (translation - _lastHeadTranslation) / _averageMessageTime;
const float LINEAR_VELOCITY_FILTER_STRENGTH = 0.3f;
float velocityFilter = glm::clamp(1.0f - glm::length(linearVelocity) *
LINEAR_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f);
_filteredHeadTranslation = velocityFilter * _filteredHeadTranslation + (1.0f - velocityFilter) * translation;
_lastHeadTranslation = translation;
_headTranslation = _filteredHeadTranslation;
} else {
_headTranslation = translation;
}
// Compute relative rotation
rotation = glm::inverse(_referenceRotation) * rotation;
if (isFiltering) {
glm::quat r = glm::normalize(rotation * glm::inverse(_headRotation));
float theta = 2 * acos(r.w);
glm::vec3 angularVelocity;
if (theta > EPSILON) {
float rMag = glm::length(glm::vec3(r.x, r.y, r.z));
angularVelocity = theta / _averageMessageTime * glm::vec3(r.x, r.y, r.z) / rMag;
} else {
angularVelocity = glm::vec3(0, 0, 0);
}
const float ANGULAR_VELOCITY_FILTER_STRENGTH = 0.3f;
_headRotation = safeMix(_headRotation, rotation, glm::clamp(glm::length(angularVelocity) *
ANGULAR_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f));
} else {
_headRotation = rotation;
}
// Translate DDE coefficients to Faceshift compatible coefficients
for (int i = 0; i < NUM_EXPRESSIONS; i++) {
_coefficients[DDE_TO_FACESHIFT_MAPPING[i]] = packet.expressions[i];
}
// Calibration
if (_isCalibrating) {
addCalibrationDatum();
}
for (int i = 0; i < NUM_FACESHIFT_BLENDSHAPES; i++) {
_coefficients[i] -= _coefficientAverages[i];
}
// Use BrowsU_C to control both brows' up and down
float browUp = _coefficients[_browUpCenterIndex];
if (isFiltering) {
const float BROW_VELOCITY_FILTER_STRENGTH = 0.5f;
float velocity = fabsf(browUp - _lastBrowUp) / _averageMessageTime;
float velocityFilter = glm::clamp(velocity * BROW_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f);
_filteredBrowUp = velocityFilter * browUp + (1.0f - velocityFilter) * _filteredBrowUp;
_lastBrowUp = browUp;
browUp = _filteredBrowUp;
_coefficients[_browUpCenterIndex] = browUp;
}
_coefficients[_browUpLeftIndex] = browUp;
_coefficients[_browUpRightIndex] = browUp;
_coefficients[_browDownLeftIndex] = -browUp;
_coefficients[_browDownRightIndex] = -browUp;
// Offset jaw open coefficient
static const float JAW_OPEN_THRESHOLD = 0.1f;
_coefficients[_jawOpenIndex] = _coefficients[_jawOpenIndex] - JAW_OPEN_THRESHOLD;
// Offset smile coefficients
static const float SMILE_THRESHOLD = 0.5f;
_coefficients[_mouthSmileLeftIndex] = _coefficients[_mouthSmileLeftIndex] - SMILE_THRESHOLD;
_coefficients[_mouthSmileRightIndex] = _coefficients[_mouthSmileRightIndex] - SMILE_THRESHOLD;
// Eye pitch and yaw
// EyeDown coefficients work better over both +ve and -ve values than EyeUp values.
// EyeIn coefficients work better over both +ve and -ve values than EyeOut values.
// Pitch and yaw values are relative to the screen.
const float EYE_PITCH_SCALE = -1500.0f; // Sign, scale, and average to be similar to Faceshift values.
_eyePitch = EYE_PITCH_SCALE * (_coefficients[_leftEyeDownIndex] + _coefficients[_rightEyeDownIndex]);
const float EYE_YAW_SCALE = 2000.0f; // Scale and average to be similar to Faceshift values.
_eyeYaw = EYE_YAW_SCALE * (_coefficients[_leftEyeInIndex] + _coefficients[_rightEyeInIndex]);
if (isFiltering) {
const float EYE_VELOCITY_FILTER_STRENGTH = 0.005f;
float pitchVelocity = fabsf(_eyePitch - _lastEyePitch) / _averageMessageTime;
float pitchVelocityFilter = glm::clamp(pitchVelocity * EYE_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f);
_filteredEyePitch = pitchVelocityFilter * _eyePitch + (1.0f - pitchVelocityFilter) * _filteredEyePitch;
_lastEyePitch = _eyePitch;
_eyePitch = _filteredEyePitch;
float yawVelocity = fabsf(_eyeYaw - _lastEyeYaw) / _averageMessageTime;
float yawVelocityFilter = glm::clamp(yawVelocity * EYE_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f);
_filteredEyeYaw = yawVelocityFilter * _eyeYaw + (1.0f - yawVelocityFilter) * _filteredEyeYaw;
_lastEyeYaw = _eyeYaw;
_eyeYaw = _filteredEyeYaw;
}
// Velocity filter EyeBlink values
const float DDE_EYEBLINK_SCALE = 3.0f;
float eyeBlinks[] = { DDE_EYEBLINK_SCALE * _coefficients[_leftBlinkIndex],
DDE_EYEBLINK_SCALE * _coefficients[_rightBlinkIndex] };
if (isFiltering) {
const float BLINK_VELOCITY_FILTER_STRENGTH = 0.3f;
for (int i = 0; i < 2; i++) {
float velocity = fabsf(eyeBlinks[i] - _lastEyeBlinks[i]) / _averageMessageTime;
float velocityFilter = glm::clamp(velocity * BLINK_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f);
_filteredEyeBlinks[i] = velocityFilter * eyeBlinks[i] + (1.0f - velocityFilter) * _filteredEyeBlinks[i];
_lastEyeBlinks[i] = eyeBlinks[i];
}
}
// Finesse EyeBlink values
float eyeCoefficients[2];
if (Menu::getInstance()->isOptionChecked(MenuOption::BinaryEyelidControl)) {
if (_eyeStates[0] == EYE_UNCONTROLLED) {
_eyeStates[0] = EYE_OPEN;
_eyeStates[1] = EYE_OPEN;
}
for (int i = 0; i < 2; i++) {
// Scale EyeBlink values so that they can be used to control both EyeBlink and EyeOpen
// -ve values control EyeOpen; +ve values control EyeBlink
static const float EYE_CONTROL_THRESHOLD = 0.5f; // Resting eye value
eyeCoefficients[i] = (_filteredEyeBlinks[i] - EYE_CONTROL_THRESHOLD) / (1.0f - EYE_CONTROL_THRESHOLD);
// Change to closing or opening states
const float EYE_CONTROL_HYSTERISIS = 0.25f;
float eyeClosingThreshold = getEyeClosingThreshold();
float eyeOpeningThreshold = eyeClosingThreshold - EYE_CONTROL_HYSTERISIS;
if ((_eyeStates[i] == EYE_OPEN || _eyeStates[i] == EYE_OPENING) && eyeCoefficients[i] > eyeClosingThreshold) {
_eyeStates[i] = EYE_CLOSING;
} else if ((_eyeStates[i] == EYE_CLOSED || _eyeStates[i] == EYE_CLOSING)
&& eyeCoefficients[i] < eyeOpeningThreshold) {
_eyeStates[i] = EYE_OPENING;
}
const float EYELID_MOVEMENT_RATE = 10.0f; // units/second
const float EYE_OPEN_SCALE = 0.2f;
if (_eyeStates[i] == EYE_CLOSING) {
// Close eyelid until it's fully closed
float closingValue = _lastEyeCoefficients[i] + EYELID_MOVEMENT_RATE * _averageMessageTime;
if (closingValue >= 1.0f) {
_eyeStates[i] = EYE_CLOSED;
eyeCoefficients[i] = 1.0f;
} else {
eyeCoefficients[i] = closingValue;
}
} else if (_eyeStates[i] == EYE_OPENING) {
// Open eyelid until it meets the current adjusted value
float openingValue = _lastEyeCoefficients[i] - EYELID_MOVEMENT_RATE * _averageMessageTime;
if (openingValue < eyeCoefficients[i] * EYE_OPEN_SCALE) {
_eyeStates[i] = EYE_OPEN;
eyeCoefficients[i] = eyeCoefficients[i] * EYE_OPEN_SCALE;
} else {
eyeCoefficients[i] = openingValue;
}
} else if (_eyeStates[i] == EYE_OPEN) {
// Reduce eyelid movement
eyeCoefficients[i] = eyeCoefficients[i] * EYE_OPEN_SCALE;
} else if (_eyeStates[i] == EYE_CLOSED) {
// Keep eyelid fully closed
eyeCoefficients[i] = 1.0;
}
}
if (_eyeStates[0] == EYE_OPEN && _eyeStates[1] == EYE_OPEN) {
// Couple eyelids
eyeCoefficients[0] = eyeCoefficients[1] = (eyeCoefficients[0] + eyeCoefficients[0]) / 2.0f;
}
_lastEyeCoefficients[0] = eyeCoefficients[0];
_lastEyeCoefficients[1] = eyeCoefficients[1];
} else {
_eyeStates[0] = EYE_UNCONTROLLED;
_eyeStates[1] = EYE_UNCONTROLLED;
eyeCoefficients[0] = _filteredEyeBlinks[0];
eyeCoefficients[1] = _filteredEyeBlinks[1];
}
// Couple eyelid values if configured - use the most "open" value for both
if (Menu::getInstance()->isOptionChecked(MenuOption::CoupleEyelids)) {
float eyeCoefficient = std::min(eyeCoefficients[0], eyeCoefficients[1]);
eyeCoefficients[0] = eyeCoefficient;
eyeCoefficients[1] = eyeCoefficient;
}
// Use EyeBlink values to control both EyeBlink and EyeOpen
if (eyeCoefficients[0] > 0) {
_coefficients[_leftBlinkIndex] = eyeCoefficients[0];
_coefficients[_leftEyeOpenIndex] = 0.0f;
} else {
_coefficients[_leftBlinkIndex] = 0.0f;
_coefficients[_leftEyeOpenIndex] = -eyeCoefficients[0];
}
if (eyeCoefficients[1] > 0) {
_coefficients[_rightBlinkIndex] = eyeCoefficients[1];
_coefficients[_rightEyeOpenIndex] = 0.0f;
} else {
_coefficients[_rightBlinkIndex] = 0.0f;
_coefficients[_rightEyeOpenIndex] = -eyeCoefficients[1];
}
// Scale all coefficients
for (int i = 0; i < NUM_EXPRESSIONS; i++) {
_blendshapeCoefficients[i]
= glm::clamp(DDE_COEFFICIENT_SCALES[i] * _coefficients[i], 0.0f, 1.0f);
}
// Calculate average frame time
const float FRAME_AVERAGING_FACTOR = 0.99f;
quint64 usecsNow = usecTimestampNow();
if (_lastMessageReceived != 0) {
_averageMessageTime = FRAME_AVERAGING_FACTOR * _averageMessageTime
+ (1.0f - FRAME_AVERAGING_FACTOR) * (float)(usecsNow - _lastMessageReceived) / 1000000.0f;
}
_lastMessageReceived = usecsNow;
FaceTracker::countFrame();
} else {
qCWarning(interfaceapp) << "DDE Face Tracker: Decode error";
}
if (_isCalibrating && _calibrationCount > CALIBRATION_SAMPLES) {
finishCalibration();
}
}
void DdeFaceTracker::setEyeClosingThreshold(float eyeClosingThreshold) {
_eyeClosingThreshold.set(eyeClosingThreshold);
}
static const int CALIBRATION_BILLBOARD_WIDTH = 300;
static const int CALIBRATION_BILLBOARD_HEIGHT = 120;
static QString CALIBRATION_INSTRUCTION_MESSAGE = "Hold still to calibrate camera";
void DdeFaceTracker::calibrate() {
if (!Menu::getInstance()->isOptionChecked(MenuOption::UseCamera) || _isMuted) {
return;
}
if (!_isCalibrating) {
qCDebug(interfaceapp) << "DDE Face Tracker: Calibration started";
_isCalibrating = true;
_calibrationCount = 0;
_calibrationMessage = CALIBRATION_INSTRUCTION_MESSAGE + "\n\n";
// FIXME: this overlay probably doesn't work anymore
_calibrationBillboard = new TextOverlay();
glm::vec2 viewport = qApp->getCanvasSize();
_calibrationBillboard->setX((viewport.x - CALIBRATION_BILLBOARD_WIDTH) / 2);
_calibrationBillboard->setY((viewport.y - CALIBRATION_BILLBOARD_HEIGHT) / 2);
_calibrationBillboard->setWidth(CALIBRATION_BILLBOARD_WIDTH);
_calibrationBillboard->setHeight(CALIBRATION_BILLBOARD_HEIGHT);
_calibrationBillboardID = qApp->getOverlays().addOverlay(_calibrationBillboard);
for (int i = 0; i < NUM_FACESHIFT_BLENDSHAPES; i++) {
_calibrationValues[i] = 0.0f;
}
}
}
void DdeFaceTracker::addCalibrationDatum() {
const int LARGE_TICK_INTERVAL = 30;
const int SMALL_TICK_INTERVAL = 6;
int samplesLeft = CALIBRATION_SAMPLES - _calibrationCount;
if (samplesLeft % LARGE_TICK_INTERVAL == 0) {
_calibrationMessage += QString::number(samplesLeft / LARGE_TICK_INTERVAL);
// FIXME: set overlay text
} else if (samplesLeft % SMALL_TICK_INTERVAL == 0) {
_calibrationMessage += ".";
// FIXME: set overlay text
}
for (int i = 0; i < NUM_FACESHIFT_BLENDSHAPES; i++) {
_calibrationValues[i] += _coefficients[i];
}
_calibrationCount += 1;
}
void DdeFaceTracker::cancelCalibration() {
qApp->getOverlays().deleteOverlay(_calibrationBillboardID);
_calibrationBillboard = NULL;
_isCalibrating = false;
qCDebug(interfaceapp) << "DDE Face Tracker: Calibration cancelled";
}
void DdeFaceTracker::finishCalibration() {
qApp->getOverlays().deleteOverlay(_calibrationBillboardID);
_calibrationBillboard = NULL;
_isCalibrating = false;
_isCalibrated = true;
for (int i = 0; i < NUM_FACESHIFT_BLENDSHAPES; i++) {
_coefficientAverages[i] = _calibrationValues[i] / (float)CALIBRATION_SAMPLES;
}
reset();
qCDebug(interfaceapp) << "DDE Face Tracker: Calibration finished";
}

View file

@ -1,181 +0,0 @@
//
// DdeFaceTracker.h
//
//
// Created by Clement on 8/2/14.
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_DdeFaceTracker_h
#define hifi_DdeFaceTracker_h
#include <QtCore/QtGlobal>
//Disabling dde due to random crashes with closing the socket on macos. all the accompanying code is wrapped with the ifdef HAVE_DDE. uncomment the define below to enable
#if defined(Q_OS_WIN) || defined(Q_OS_OSX)
//#define HAVE_DDE
#endif
#include <QProcess>
#include <QUdpSocket>
#include <DependencyManager.h>
#include <ui/overlays/TextOverlay.h>
#include <trackers/FaceTracker.h>
/**jsdoc
* The FaceTracker API helps manage facial tracking hardware.
* @namespace FaceTracker
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
*/
class DdeFaceTracker : public FaceTracker, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
public:
virtual void init() override;
virtual void reset() override;
virtual void update(float deltaTime) override;
virtual bool isActive() const override;
virtual bool isTracking() const override;
float getLeftBlink() const { return getBlendshapeCoefficient(_leftBlinkIndex); }
float getRightBlink() const { return getBlendshapeCoefficient(_rightBlinkIndex); }
float getLeftEyeOpen() const { return getBlendshapeCoefficient(_leftEyeOpenIndex); }
float getRightEyeOpen() const { return getBlendshapeCoefficient(_rightEyeOpenIndex); }
float getBrowDownLeft() const { return getBlendshapeCoefficient(_browDownLeftIndex); }
float getBrowDownRight() const { return getBlendshapeCoefficient(_browDownRightIndex); }
float getBrowUpCenter() const { return getBlendshapeCoefficient(_browUpCenterIndex); }
float getBrowUpLeft() const { return getBlendshapeCoefficient(_browUpLeftIndex); }
float getBrowUpRight() const { return getBlendshapeCoefficient(_browUpRightIndex); }
float getMouthSize() const { return getBlendshapeCoefficient(_jawOpenIndex); }
float getMouthSmileLeft() const { return getBlendshapeCoefficient(_mouthSmileLeftIndex); }
float getMouthSmileRight() const { return getBlendshapeCoefficient(_mouthSmileRightIndex); }
float getEyeClosingThreshold() { return _eyeClosingThreshold.get(); }
void setEyeClosingThreshold(float eyeClosingThreshold);
public slots:
/**jsdoc
* @function FaceTracker.setEnabled
* @param {boolean} enabled
*/
void setEnabled(bool enabled) override;
/**jsdoc
* @function FaceTracker.calibrate
*/
void calibrate();
private slots:
void processFinished(int exitCode, QProcess::ExitStatus exitStatus);
//sockets
void socketErrorOccurred(QAbstractSocket::SocketError socketError);
void readPendingDatagrams();
void socketStateChanged(QAbstractSocket::SocketState socketState);
private:
DdeFaceTracker();
DdeFaceTracker(const QHostAddress& host, quint16 serverPort, quint16 controlPort);
virtual ~DdeFaceTracker();
QProcess* _ddeProcess;
bool _ddeStopping;
QHostAddress _host;
quint16 _serverPort;
quint16 _controlPort;
float getBlendshapeCoefficient(int index) const;
void decodePacket(const QByteArray& buffer);
// sockets
QUdpSocket _udpSocket;
quint64 _lastReceiveTimestamp;
bool _reset;
glm::vec3 _referenceTranslation;
glm::quat _referenceRotation;
int _leftBlinkIndex;
int _rightBlinkIndex;
int _leftEyeDownIndex;
int _rightEyeDownIndex;
int _leftEyeInIndex;
int _rightEyeInIndex;
int _leftEyeOpenIndex;
int _rightEyeOpenIndex;
int _browDownLeftIndex;
int _browDownRightIndex;
int _browUpCenterIndex;
int _browUpLeftIndex;
int _browUpRightIndex;
int _mouthSmileLeftIndex;
int _mouthSmileRightIndex;
int _jawOpenIndex;
QVector<float> _coefficients;
quint64 _lastMessageReceived;
float _averageMessageTime;
glm::vec3 _lastHeadTranslation;
glm::vec3 _filteredHeadTranslation;
float _lastBrowUp;
float _filteredBrowUp;
float _eyePitch; // Degrees, relative to screen
float _eyeYaw;
float _lastEyePitch;
float _lastEyeYaw;
float _filteredEyePitch;
float _filteredEyeYaw;
float _longTermAverageEyePitch = 0.0f;
float _longTermAverageEyeYaw = 0.0f;
bool _longTermAverageInitialized = false;
enum EyeState {
EYE_UNCONTROLLED,
EYE_OPEN,
EYE_CLOSING,
EYE_CLOSED,
EYE_OPENING
};
EyeState _eyeStates[2];
float _lastEyeBlinks[2];
float _filteredEyeBlinks[2];
float _lastEyeCoefficients[2];
Setting::Handle<float> _eyeClosingThreshold;
QVector<float> _coefficientAverages;
bool _isCalibrating;
int _calibrationCount;
QVector<float> _calibrationValues;
TextOverlay* _calibrationBillboard;
QUuid _calibrationBillboardID;
QString _calibrationMessage;
bool _isCalibrated;
void addCalibrationDatum();
void cancelCalibration();
void finishCalibration();
};
#endif // hifi_DdeFaceTracker_h

View file

@ -25,10 +25,10 @@ class LaserPointerScriptingInterface : public QObject, public Dependency {
* represent objects for repeatedly calculating ray intersections with avatars, entities, and overlays. Ray pointers can also
* be configured to generate events on entities and overlays intersected.
*
* <p class="important">Deprecated: This API is deprecated. Use {@link Pointers} instead.
*
* @namespace LaserPointers
*
* @deprecated This API is deprecated and will be removed. Use {@link Pointers} instead.
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar

View file

@ -15,6 +15,7 @@
#include <algorithm>
#include <shared/QtHelpers.h>
#include <plugins/PluginManager.h>
#include <plugins/DisplayPlugin.h>
#include "Application.h"
@ -44,7 +45,8 @@ enum AudioDeviceRole {
SelectedDesktopRole,
SelectedHMDRole,
PeakRole,
InfoRole
InfoRole,
TypeRole
};
QHash<int, QByteArray> AudioDeviceList::_roles {
@ -52,7 +54,8 @@ QHash<int, QByteArray> AudioDeviceList::_roles {
{ SelectedDesktopRole, "selectedDesktop" },
{ SelectedHMDRole, "selectedHMD" },
{ PeakRole, "peak" },
{ InfoRole, "info" }
{ InfoRole, "info" },
{ TypeRole, "type"}
};
static QString getTargetDevice(bool hmd, QAudio::Mode mode) {
@ -60,18 +63,33 @@ static QString getTargetDevice(bool hmd, QAudio::Mode mode) {
auto& setting = getSetting(hmd, mode);
if (setting.isSet()) {
deviceName = setting.get();
} else if (hmd) {
if (mode == QAudio::AudioInput) {
deviceName = qApp->getActiveDisplayPlugin()->getPreferredAudioInDevice();
} else { // if (_mode == QAudio::AudioOutput)
deviceName = qApp->getActiveDisplayPlugin()->getPreferredAudioOutDevice();
}
} else {
deviceName = HifiAudioDeviceInfo::DEFAULT_DEVICE_NAME;
}
return deviceName;
}
static void checkHmdDefaultsChange(QAudio::Mode mode) {
QString name;
foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getAllDisplayPlugins()) {
if (displayPlugin && displayPlugin->isHmd()) {
if (mode == QAudio::AudioInput) {
name = displayPlugin->getPreferredAudioInDevice();
} else {
name = displayPlugin->getPreferredAudioOutDevice();
}
break;
}
}
if (!name.isEmpty()) {
auto client = DependencyManager::get<AudioClient>().data();
QMetaObject::invokeMethod(client, "setHmdAudioName",
Q_ARG(QAudio::Mode, mode),
Q_ARG(const QString&, name));
}
}
Qt::ItemFlags AudioDeviceList::_flags { Qt::ItemIsSelectable | Qt::ItemIsEnabled };
AudioDeviceList::AudioDeviceList(QAudio::Mode mode) : _mode(mode) {
@ -144,6 +162,8 @@ QVariant AudioDeviceList::data(const QModelIndex& index, int role) const {
return _devices.at(index.row())->selectedHMD;
} else if (role == InfoRole) {
return QVariant::fromValue<HifiAudioDeviceInfo>(_devices.at(index.row())->info);
} else if (role == TypeRole) {
return _devices.at(index.row())->type;
} else {
return QVariant();
}
@ -166,8 +186,8 @@ void AudioDeviceList::resetDevice(bool contextIsHMD) {
QString deviceName = getTargetDevice(contextIsHMD, _mode);
// FIXME can't use blocking connections here, so we can't determine whether the switch succeeded or not
// We need to have the AudioClient emit signals on switch success / failure
QMetaObject::invokeMethod(client, "switchAudioDevice",
Q_ARG(QAudio::Mode, _mode), Q_ARG(QString, deviceName));
QMetaObject::invokeMethod(client, "switchAudioDevice",
Q_ARG(QAudio::Mode, _mode), Q_ARG(QString, deviceName), Q_ARG(bool, contextIsHMD));
#if 0
bool switchResult = false;
@ -258,20 +278,28 @@ std::shared_ptr<scripting::AudioDevice> getSimilarDevice(const QString& deviceNa
return devices[minDistanceIndex];
}
void AudioDeviceList::onDevicesChanged(const QList<HifiAudioDeviceInfo>& devices) {
void AudioDeviceList::onDevicesChanged(QAudio::Mode mode, const QList<HifiAudioDeviceInfo>& devices) {
beginResetModel();
QList<std::shared_ptr<AudioDevice>> newDevices;
bool hmdIsSelected = false;
bool desktopIsSelected = false;
foreach(const HifiAudioDeviceInfo& deviceInfo, devices) {
for (bool isHMD : {false, true}) {
auto& backupSelectedDeviceName = isHMD ? _backupSelectedHMDDeviceName : _backupSelectedDesktopDeviceName;
if (deviceInfo.deviceName() == backupSelectedDeviceName) {
HifiAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
selectedDevice = deviceInfo;
backupSelectedDeviceName.clear();
checkHmdDefaultsChange(mode);
if (!_backupSelectedDesktopDeviceName.isEmpty() && !_backupSelectedHMDDeviceName.isEmpty()) {
foreach(const HifiAudioDeviceInfo& deviceInfo, devices) {
for (bool isHMD : {false, true}) {
auto& backupSelectedDeviceName = isHMD ? _backupSelectedHMDDeviceName : _backupSelectedDesktopDeviceName;
if (deviceInfo.deviceName() == backupSelectedDeviceName) {
if (isHMD && deviceInfo.getDeviceType() != HifiAudioDeviceInfo::desktop) {
_selectedHMDDevice= deviceInfo;
backupSelectedDeviceName.clear();
} else if (!isHMD && deviceInfo.getDeviceType() != HifiAudioDeviceInfo::hmd) {
_selectedDesktopDevice = deviceInfo;
backupSelectedDeviceName.clear();
}
}
}
}
}
@ -281,10 +309,18 @@ void AudioDeviceList::onDevicesChanged(const QList<HifiAudioDeviceInfo>& devices
device.info = deviceInfo;
if (deviceInfo.isDefault()) {
if (deviceInfo.getMode() == QAudio::AudioInput) {
device.display = "Computer's default microphone (recommended)";
} else {
device.display = "Computer's default audio (recommended)";
if (deviceInfo.getDeviceType() == HifiAudioDeviceInfo::desktop) {
if (deviceInfo.getMode() == QAudio::AudioInput) {
device.display = "Computer's default microphone (recommended)";
} else {
device.display = "Computer's default audio (recommended)";
}
} else if (deviceInfo.getDeviceType() == HifiAudioDeviceInfo::hmd) {
if (deviceInfo.getMode() == QAudio::AudioInput) {
device.display = "Headset's default mic (recommended)";
} else {
device.display = "Headset's default audio (recommended)";
}
}
} else {
device.display = device.info.deviceName()
@ -292,6 +328,19 @@ void AudioDeviceList::onDevicesChanged(const QList<HifiAudioDeviceInfo>& devices
.remove("Device")
.replace(" )", ")");
}
switch (deviceInfo.getDeviceType()) {
case HifiAudioDeviceInfo::hmd:
device.type = "hmd";
break;
case HifiAudioDeviceInfo::desktop:
device.type = "desktop";
break;
case HifiAudioDeviceInfo::both:
device.type = "both";
break;
}
for (bool isHMD : {false, true}) {
HifiAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
@ -302,8 +351,14 @@ void AudioDeviceList::onDevicesChanged(const QList<HifiAudioDeviceInfo>& devices
}
else {
//no selected device for context. fallback to saved
const QString& savedDeviceName = isHMD ? _hmdSavedDeviceName : _desktopSavedDeviceName;
isSelected = (device.info.deviceName() == savedDeviceName);
QString& savedDeviceName = isHMD ? _hmdSavedDeviceName : _desktopSavedDeviceName;
if (device.info.deviceName() == savedDeviceName) {
if ((isHMD && device.info.getDeviceType() != HifiAudioDeviceInfo::desktop) ||
(!isHMD && device.info.getDeviceType() != HifiAudioDeviceInfo::hmd)) {
isSelected = true;
}
}
}
if (isSelected) {
@ -385,6 +440,9 @@ AudioDevices::AudioDevices(bool& contextIsHMD) : _contextIsHMD(contextIsHMD) {
connect(client, &AudioClient::deviceChanged, this, &AudioDevices::onDeviceChanged, Qt::QueuedConnection);
connect(client, &AudioClient::devicesChanged, this, &AudioDevices::onDevicesChanged, Qt::QueuedConnection);
connect(client, &AudioClient::peakValueListChanged, &_inputs, &AudioInputDeviceList::onPeakValueListChanged, Qt::QueuedConnection);
checkHmdDefaultsChange(QAudio::AudioInput);
checkHmdDefaultsChange(QAudio::AudioOutput);
_inputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioInput), contextIsHMD);
_outputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioOutput), contextIsHMD);
@ -393,9 +451,11 @@ AudioDevices::AudioDevices(bool& contextIsHMD) : _contextIsHMD(contextIsHMD) {
const QList<HifiAudioDeviceInfo>& devicesInput = client->getAudioDevices(QAudio::AudioInput);
const QList<HifiAudioDeviceInfo>& devicesOutput = client->getAudioDevices(QAudio::AudioOutput);
//setup devices
_inputs.onDevicesChanged(devicesInput);
_outputs.onDevicesChanged(devicesOutput);
if (devicesInput.size() > 0 && devicesOutput.size() > 0) {
//setup devices
_inputs.onDevicesChanged(QAudio::AudioInput, devicesInput);
_outputs.onDevicesChanged(QAudio::AudioOutput, devicesOutput);
}
}
AudioDevices::~AudioDevices() {}
@ -494,14 +554,14 @@ void AudioDevices::onDevicesChanged(QAudio::Mode mode, const QList<HifiAudioDevi
//set devices for both contexts
if (mode == QAudio::AudioInput) {
_inputs.onDevicesChanged(devices);
_inputs.onDevicesChanged(mode, devices);
static std::once_flag onceAfterInputDevicesChanged;
std::call_once(onceAfterInputDevicesChanged, [&] { // we only want 'selectedDevicePlugged' signal to be handled after initial list of input devices was populated
connect(&_inputs, &AudioDeviceList::selectedDevicePlugged, this, &AudioDevices::chooseInputDevice);
});
} else { // if (mode == QAudio::AudioOutput)
_outputs.onDevicesChanged(devices);
_outputs.onDevicesChanged(mode, devices);
static std::once_flag onceAfterOutputDevicesChanged;
std::call_once(onceAfterOutputDevicesChanged, [&] { // we only want 'selectedDevicePlugged' signal to be handled after initial list of output devices was populated

View file

@ -29,6 +29,7 @@ public:
QString display;
bool selectedDesktop { false };
bool selectedHMD { false };
QString type;
};
class AudioDeviceList : public QAbstractListModel {
@ -57,7 +58,7 @@ signals:
protected slots:
void onDeviceChanged(const HifiAudioDeviceInfo& device, bool isHMD);
void onDevicesChanged(const QList<HifiAudioDeviceInfo>& devices);
void onDevicesChanged(QAudio::Mode mode, const QList<HifiAudioDeviceInfo>& devices);
protected:
friend class AudioDevices;

View file

@ -32,106 +32,187 @@ void MenuScriptingInterface::menuItemTriggered() {
}
void MenuScriptingInterface::addMenu(const QString& menu, const QString& grouping) {
QMetaObject::invokeMethod(Menu::getInstance(), "addMenu", Q_ARG(const QString&, menu), Q_ARG(const QString&, grouping));
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
QMetaObject::invokeMethod(menuInstance, "addMenu", Q_ARG(const QString&, menu), Q_ARG(const QString&, grouping));
}
void MenuScriptingInterface::removeMenu(const QString& menu) {
QMetaObject::invokeMethod(Menu::getInstance(), "removeMenu", Q_ARG(const QString&, menu));
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
QMetaObject::invokeMethod(menuInstance, "removeMenu", Q_ARG(const QString&, menu));
}
bool MenuScriptingInterface::menuExists(const QString& menu) {
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return false;
}
if (QThread::currentThread() == qApp->thread()) {
Menu* menuInstance = Menu::getInstance();
return menuInstance && menuInstance->menuExists(menu);
}
bool result { false };
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuExists",
BLOCKING_INVOKE_METHOD(menuInstance, "menuExists",
Q_RETURN_ARG(bool, result),
Q_ARG(const QString&, menu));
return result;
}
void MenuScriptingInterface::addSeparator(const QString& menuName, const QString& separatorName) {
QMetaObject::invokeMethod(Menu::getInstance(), "addSeparator",
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
QMetaObject::invokeMethod(menuInstance, "addSeparator",
Q_ARG(const QString&, menuName),
Q_ARG(const QString&, separatorName));
}
void MenuScriptingInterface::removeSeparator(const QString& menuName, const QString& separatorName) {
QMetaObject::invokeMethod(Menu::getInstance(), "removeSeparator",
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
QMetaObject::invokeMethod(menuInstance, "removeSeparator",
Q_ARG(const QString&, menuName),
Q_ARG(const QString&, separatorName));
}
void MenuScriptingInterface::addMenuItem(const MenuItemProperties& properties) {
QMetaObject::invokeMethod(Menu::getInstance(), "addMenuItem", Q_ARG(const MenuItemProperties&, properties));
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
QMetaObject::invokeMethod(menuInstance, "addMenuItem", Q_ARG(const MenuItemProperties&, properties));
}
void MenuScriptingInterface::addMenuItem(const QString& menu, const QString& menuitem, const QString& shortcutKey) {
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
MenuItemProperties properties(menu, menuitem, shortcutKey);
QMetaObject::invokeMethod(Menu::getInstance(), "addMenuItem", Q_ARG(const MenuItemProperties&, properties));
QMetaObject::invokeMethod(menuInstance, "addMenuItem", Q_ARG(const MenuItemProperties&, properties));
}
void MenuScriptingInterface::addMenuItem(const QString& menu, const QString& menuitem) {
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
MenuItemProperties properties(menu, menuitem);
QMetaObject::invokeMethod(Menu::getInstance(), "addMenuItem", Q_ARG(const MenuItemProperties&, properties));
QMetaObject::invokeMethod(menuInstance, "addMenuItem", Q_ARG(const MenuItemProperties&, properties));
}
void MenuScriptingInterface::removeMenuItem(const QString& menu, const QString& menuitem) {
QMetaObject::invokeMethod(Menu::getInstance(), "removeMenuItem",
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
QMetaObject::invokeMethod(menuInstance, "removeMenuItem",
Q_ARG(const QString&, menu),
Q_ARG(const QString&, menuitem));
};
bool MenuScriptingInterface::menuItemExists(const QString& menu, const QString& menuitem) {
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return false;
}
if (QThread::currentThread() == qApp->thread()) {
Menu* menuInstance = Menu::getInstance();
return menuInstance && menuInstance->menuItemExists(menu, menuitem);
}
bool result { false };
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuItemExists",
BLOCKING_INVOKE_METHOD(menuInstance, "menuItemExists",
Q_RETURN_ARG(bool, result),
Q_ARG(const QString&, menu),
Q_ARG(const QString&, menuitem));
return result;
}
bool MenuScriptingInterface::isOptionChecked(const QString& menuOption) {
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return false;
}
if (QThread::currentThread() == qApp->thread()) {
Menu* menuInstance = Menu::getInstance();
return menuInstance && menuInstance->isOptionChecked(menuOption);
}
bool result { false };
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isOptionChecked",
BLOCKING_INVOKE_METHOD(menuInstance, "isOptionChecked",
Q_RETURN_ARG(bool, result),
Q_ARG(const QString&, menuOption));
return result;
}
void MenuScriptingInterface::setIsOptionChecked(const QString& menuOption, bool isChecked) {
QMetaObject::invokeMethod(Menu::getInstance(), "setIsOptionChecked",
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
QMetaObject::invokeMethod(menuInstance, "setIsOptionChecked",
Q_ARG(const QString&, menuOption),
Q_ARG(bool, isChecked));
}
bool MenuScriptingInterface::isMenuEnabled(const QString& menuOption) {
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return false;
}
if (QThread::currentThread() == qApp->thread()) {
Menu* menuInstance = Menu::getInstance();
return menuInstance && menuInstance->isMenuEnabled(menuOption);
}
bool result { false };
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isMenuEnabled",
BLOCKING_INVOKE_METHOD(menuInstance, "isMenuEnabled",
Q_RETURN_ARG(bool, result),
Q_ARG(const QString&, menuOption));
return result;
}
void MenuScriptingInterface::setMenuEnabled(const QString& menuOption, bool isChecked) {
QMetaObject::invokeMethod(Menu::getInstance(), "setMenuEnabled",
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
QMetaObject::invokeMethod(menuInstance, "setMenuEnabled",
Q_ARG(const QString&, menuOption),
Q_ARG(bool, isChecked));
}
void MenuScriptingInterface::triggerOption(const QString& menuOption) {
QMetaObject::invokeMethod(Menu::getInstance(), "triggerOption", Q_ARG(const QString&, menuOption));
Menu* menuInstance = Menu::getInstance();
if (!menuInstance) {
return;
}
QMetaObject::invokeMethod(menuInstance, "triggerOption", Q_ARG(const QString&, menuOption));
}

View file

@ -11,7 +11,6 @@
#include <AudioClient.h>
#include <SettingHandle.h>
#include <trackers/FaceTracker.h>
#include <UsersScriptingInterface.h>
#include "Application.h"
@ -76,8 +75,6 @@ void AvatarInputs::update() {
return;
}
AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking));
AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking));
AI_UPDATE(isHMD, qApp->isHMDMode());
}
@ -103,13 +100,6 @@ bool AvatarInputs::getIgnoreRadiusEnabled() const {
return DependencyManager::get<NodeList>()->getIgnoreRadiusEnabled();
}
void AvatarInputs::toggleCameraMute() {
FaceTracker* faceTracker = qApp->getSelectedFaceTracker();
if (faceTracker) {
faceTracker->toggleMute();
}
}
void AvatarInputs::resetSensors() {
qApp->resetSensors();
}

View file

@ -35,11 +35,11 @@ class AvatarInputs : public QObject {
* @property {boolean} cameraEnabled - <code>true</code> if webcam face tracking is enabled, <code>false</code> if it is
* disabled.
* <em>Read-only.</em>
* <p class="important">Deprecated: This property is deprecated and will be removed.</p>
* <p class="important">Deprecated: This property is deprecated and has been removed.</p>
* @property {boolean} cameraMuted - <code>true</code> if webcam face tracking is muted (temporarily disabled),
* <code>false</code> it if isn't.
* <em>Read-only.</em>
* <p class="important">Deprecated: This property is deprecated and will be removed.</p>
* <p class="important">Deprecated: This property is deprecated and has been removed.</p>
* @property {boolean} ignoreRadiusEnabled - <code>true</code> if the privacy shield is enabled, <code>false</code> if it
* is disabled.
* <em>Read-only.</em>
@ -51,8 +51,6 @@ class AvatarInputs : public QObject {
* it is hidden.
*/
AI_PROPERTY(bool, cameraEnabled, false)
AI_PROPERTY(bool, cameraMuted, false)
AI_PROPERTY(bool, isHMD, false)
Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged)
@ -99,19 +97,17 @@ signals:
/**jsdoc
* Triggered when webcam face tracking is enabled or disabled.
* @deprecated This signal is deprecated and will be removed.
* @deprecated This signal is deprecated and has been removed.
* @function AvatarInputs.cameraEnabledChanged
* @returns {Signal}
*/
void cameraEnabledChanged();
/**jsdoc
* Triggered when webcam face tracking is muted (temporarily disabled) or unmuted.
* @deprecated This signal is deprecated and will be removed.
* @deprecated This signal is deprecated and has been removed.
* @function AvatarInputs.cameraMutedChanged
* @returns {Signal}
*/
void cameraMutedChanged();
/**jsdoc
* Triggered when the display mode changes between desktop and HMD.
@ -185,10 +181,9 @@ protected:
/**jsdoc
* Toggles the muting (temporary disablement) of webcam face tracking on/off.
* <p class="important">Deprecated: This function is deprecated and will be removed.</p>
* <p class="important">Deprecated: This function is deprecated and has been removed.</p>
* @function AvatarInputs.toggleCameraMute
*/
Q_INVOKABLE void toggleCameraMute();
private:
void onAvatarEnteredIgnoreRadius();

View file

@ -10,7 +10,6 @@
#include <AudioClient.h>
#include <avatar/AvatarManager.h>
#include <devices/DdeFaceTracker.h>
#include <ScriptEngines.h>
#include <OffscreenUi.h>
#include <Preferences.h>
@ -58,18 +57,19 @@ void setupPreferences() {
static const QString GRAPHICS_QUALITY { "Graphics Quality" };
{
auto getter = []()->float {
return DependencyManager::get<LODManager>()->getWorldDetailQuality();
return (int)DependencyManager::get<LODManager>()->getWorldDetailQuality();
};
auto setter = [](float value) {
DependencyManager::get<LODManager>()->setWorldDetailQuality(value);
auto setter = [](int value) {
DependencyManager::get<LODManager>()->setWorldDetailQuality(static_cast<WorldDetailQuality>(value));
};
auto wodSlider = new SliderPreference(GRAPHICS_QUALITY, "World Detail", getter, setter);
wodSlider->setMin(0.25f);
wodSlider->setMax(0.75f);
wodSlider->setStep(0.25f);
preferences->addPreference(wodSlider);
auto wodButtons = new RadioButtonsPreference(GRAPHICS_QUALITY, "World Detail", getter, setter);
QStringList items;
items << "Low World Detail" << "Medium World Detail" << "High World Detail";
wodButtons->setHeading("World Detail");
wodButtons->setItems(items);
preferences->addPreference(wodButtons);
auto getterShadow = []()->bool {
auto menu = Menu::getInstance();
@ -295,22 +295,6 @@ void setupPreferences() {
preferences->addPreference(preference);
}
static const QString FACE_TRACKING{ "Face Tracking" };
{
#ifdef HAVE_DDE
auto getter = []()->float { return DependencyManager::get<DdeFaceTracker>()->getEyeClosingThreshold(); };
auto setter = [](float value) { DependencyManager::get<DdeFaceTracker>()->setEyeClosingThreshold(value); };
preferences->addPreference(new SliderPreference(FACE_TRACKING, "Eye Closing Threshold", getter, setter));
#endif
}
{
auto getter = []()->float { return FaceTracker::getEyeDeflection(); };
auto setter = [](float value) { FaceTracker::setEyeDeflection(value); };
preferences->addPreference(new SliderPreference(FACE_TRACKING, "Eye Deflection", getter, setter));
}
static const QString VR_MOVEMENT{ "VR Movement" };
{
auto getter = [myAvatar]()->bool { return myAvatar->getAllowTeleporting(); };

View file

@ -227,6 +227,9 @@
<pointsize>-1</pointsize>
</font>
</property>
<property name="textColor">
<color>black</color>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>

View file

@ -12,7 +12,23 @@
}
@end
void redirectLogToDocuments()
{
NSString* filePath = [[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0]
stringByAppendingString:@"/Launcher/"];
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSError * error = nil;
[[NSFileManager defaultManager] createDirectoryAtPath:filePath withIntermediateDirectories:TRUE attributes:nil error:&error];
}
NSString *pathForLog = [filePath stringByAppendingPathComponent:@"log.txt"];
freopen([pathForLog cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);
}
int main(int argc, char const* argv[]) {
redirectLogToDocuments();
if (argc < 3) {
NSLog(@"Error: wrong number of arguments");
return 0;
@ -50,6 +66,7 @@ int main(int argc, char const* argv[]) {
options:NSWorkspaceLaunchNewInstance
configuration:configuration
error:&error];
if (error != nil) {
NSLog(@"couldn't start launcher: %@", error);
return 1;

290
launchers/qt/CMakeLists.txt Normal file
View file

@ -0,0 +1,290 @@
cmake_minimum_required(VERSION 3.0)
if (APPLE)
set(ENV{MACOSX_DEPLOYMENT_TARGET} 10.10)
endif()
project(HQLauncher)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake/Modules")
include("cmake/macros/SetPackagingParameters.cmake")
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
include("cmake/init.cmake")
include("cmake/macros/SetPackagingParameters.cmake")
if(MSVC)
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /MT")
set(CMAKE_CXX_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS_MINSIZEREL} /MT")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd")
endif()
function(set_from_env _RESULT_NAME _ENV_VAR_NAME _DEFAULT_VALUE)
if (NOT DEFINED ${_RESULT_NAME})
if ("$ENV{${_ENV_VAR_NAME}}" STREQUAL "")
set (${_RESULT_NAME} ${_DEFAULT_VALUE} PARENT_SCOPE)
else()
set (${_RESULT_NAME} $ENV{${_ENV_VAR_NAME}} PARENT_SCOPE)
endif()
endif()
endfunction()
include(ExternalProject)
if (APPLE)
set(CMAKE_EXE_LINKER_FLAGS "-framework Cocoa -framework CoreServices -framework Carbon -framework IOKit -framework Security -framework SystemConfiguration")
add_compile_options(-W -Wall -Wextra -Wpedantic)
endif()
if (WIN32)
ExternalProject_Add(
qtlite
URL "https://public.highfidelity.com/dependencies/qtlite/qt-lite-5.9.9-win-oct-15-2019.zip"
URL_HASH MD5=0176277bca37d219e83738caf3a698eb
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
ExternalProject_Get_Property(qtlite SOURCE_DIR)
ExternalProject_Get_Property(qtlite STAMP_DIR)
include("${STAMP_DIR}/download-qtlite.cmake")
include("${STAMP_DIR}/extract-qtlite.cmake")
include("${STAMP_DIR}/verify-qtlite.cmake")
message("${SOURCE_DIR}/lib/cmake")
list(APPEND CMAKE_PREFIX_PATH ${SOURCE_DIR}/lib/cmake)
set(SSL_DIR ${SOURCE_DIR}/ssl)
set(OPENSSL_ROOT_DIR ${SSL_DIR})
message("SSL dir is ${SSL_DIR}")
set(OPENSSL_USE_STATIC_LIBS TRUE)
find_package(OpenSSL REQUIRED)
message("-- Found OpenSSL Libs ${OPENSSL_LIBRARIES}")
endif ()
if (APPLE)
ExternalProject_Add(
qtlite
URL "https://public.highfidelity.com/dependencies/qtlite/qt-lite-5.9.9-mac.zip"
URL_HASH MD5=0cd78d40e5f539a7e314cf99b6cae0d0
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
ExternalProject_Get_Property(qtlite SOURCE_DIR)
ExternalProject_Get_Property(qtlite STAMP_DIR)
include("${STAMP_DIR}/download-qtlite.cmake")
include("${STAMP_DIR}/extract-qtlite.cmake")
include("${STAMP_DIR}/verify-qtlite.cmake")
message("${SOURCE_DIR}/lib/cmake")
list(APPEND CMAKE_PREFIX_PATH ${SOURCE_DIR}/lib/cmake)
set(SSL_DIR ${SOURCE_DIR}/ssl)
set(OPENSSL_ROOT_DIR ${SSL_DIR})
message("SSL dir is ${SSL_DIR}")
endif()
if (APPLE)
set(OPENSSL_USE_STATIC_LIBS TRUE)
find_package(OpenSSL REQUIRED)
endif()
find_package(Qt5 COMPONENTS Core Gui Qml Quick QuickControls2 Network REQUIRED)
find_package(OpenGL REQUIRED)
find_package(QtStaticDeps REQUIRED)
set(CUSTOM_LAUNCHER_QRC_PATHS "")
set(RESOURCES_QRC ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc)
set(RESOURCES_RCC ${CMAKE_CURRENT_SOURCE_DIR}/resources.rcc)
generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources CUSTOM_PATHS ${CUSTOM_LAUNCHER_QRC_PATHS} GLOBS *)
add_custom_command(
OUTPUT ${RESOURCES_RCC}
DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS}
COMMAND "${_qt5Core_install_prefix}/bin/rcc"
ARGS ${RESOURCES_QRC} -binary -o ${RESOURCES_RCC})
QT5_ADD_RESOURCES(RES_SOURCES ${RESOURCES_QRC})
list(APPEND GENERATE_QRC_DEPENDS ${RESOURCES_RCC})
add_custom_target(resources ALL DEPENDS ${GENERATE_QRC_DEPENDS})
foreach(plugin ${Qt5Gui_PLUGINS})
get_target_property(_loc ${plugin} LOCATION)
set(plugin_libs ${plugin_libs} ${_loc})
endforeach()
set(src_files
src/main.cpp
src/Launcher.h
src/Launcher.cpp
src/LauncherState.h
src/LauncherState.cpp
src/LauncherWindow.h
src/LauncherWindow.cpp
src/LoginRequest.h
src/LoginRequest.cpp
src/SignupRequest.h
src/SignupRequest.cpp
src/BuildsRequest.h
src/BuildsRequest.cpp
src/UserSettingsRequest.h
src/UserSettingsRequest.cpp
src/PathUtils.h
src/PathUtils.cpp
src/Unzipper.h
src/Unzipper.cpp
src/Helper.h
src/Helper.cpp
src/CommandlineOptions.h
src/CommandlineOptions.cpp
deps/miniz/miniz.h
deps/miniz/miniz.cpp
)
if (APPLE)
set(src_files ${src_files}
src/Helper_darwin.mm
src/NSTask+NSTaskExecveAdditions.h
src/NSTask+NSTaskExecveAdditions.m
)
endif()
if (WIN32)
set(src_files ${src_files}
src/Helper_windows.cpp
src/LauncherInstaller_windows.h
src/LauncherInstaller_windows.cpp
)
endif()
set(TARGET_NAME ${PROJECT_NAME})
set_packaging_parameters()
if (WIN32)
set(CONFIGURE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/resources/images/interface.ico")
message(${CONFIGURE_ICON_PATH})
set(CONFIGURE_ICON_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/Icon.rc")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/Icon.rc.in" ${CONFIGURE_ICON_RC_OUTPUT})
add_executable(${PROJECT_NAME} WIN32 ${src_files} ${RES_SOURCES} ${CONFIGURE_ICON_RC_OUTPUT})
elseif (APPLE)
set(APP_NAME "HQ Launcher")
set_target_properties(${this_target} PROPERTIES
MACOSX_BUNDLE_INFO_PLIST MacOSXBundleInfo.plist.in)
set(MACOSX_BUNDLE_ICON_FILE "interface.icns")
add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${src_files} ${RES_SOURCES})
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME ${APP_NAME}
MACOSX_BUNDLE_BUNDLE_NAME ${APP_NAME})
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${APP_NAME}.app/Contents/Resources"
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/resources/images "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${APP_NAME}.app/Contents/Resources/"
COMMAND ${CMAKE_COMMAND} -E chdir "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${APP_NAME}.app/Contents/MacOS/" ln -sf ./HQ\ Launcher updater
# Older versions of Launcher put updater in `/Contents/Resources/updater`.
COMMAND ${CMAKE_COMMAND} -E chdir "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${APP_NAME}.app/Contents/Resources" ln -sf ../MacOS/HQ\ Launcher updater
)
endif()
target_link_libraries(${PROJECT_NAME}
Qt5::Core
Qt5::Quick
Qt5::QuickControls2
Qt5::Qml
Qt5::Gui
Qt5::Network
${Qt_LIBRARIES}
${OPENGL_LIBRARIES}
${plugin_libs}
${QT_STATIC_LIBS}
)
if (WIN32)
target_link_libraries(${PROJECT_NAME}
wsock32 ws2_32 Winmm version imm32 dwmapi
Crypt32 Iphlpapi
#"${SSL_DIR}/lib/libeay32.lib"
#"${SSL_DIR}/lib/ssleay32.lib"
${OPENSSL_LIBRARIES}
"${_qt5Core_install_prefix}/qml/QtQuick.2/qtquick2plugin.lib"
"${_qt5Core_install_prefix}/qml/QtQuick/Controls.2/qtquickcontrols2plugin.lib"
"${_qt5Core_install_prefix}/qml/QtQuick/Templates.2/qtquicktemplates2plugin.lib")
elseif (APPLE)
target_link_libraries(${PROJECT_NAME}
${OPENSSL_LIBRARIES}
"${_qt5Core_install_prefix}/qml/QtQuick.2/libqtquick2plugin.a"
"${_qt5Core_install_prefix}/qml/QtQuick/Controls.2/libqtquickcontrols2plugin.a"
"${_qt5Core_install_prefix}/qml/QtQuick/Templates.2/libqtquicktemplates2plugin.a"
"${_qt5Core_install_prefix}/plugins/platforms/libqcocoa.a")
endif()
target_include_directories(${PROJECT_NAME} PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/deps/
${Qt5Core_INCLUDE_DIRS}
${Qt5Quick_INCLUDE_DIRS}
${Qt5Gui_INCLUDE_DIRS}
${Qt5Qml_INCLUDE_DIRS})
add_dependencies(${PROJECT_NAME} resources)
if (APPLE)
target_include_directories(${PROJECT_NAME} PUBLIC
${OPENSSL_INCLUDE_DIR})
endif()
if (LAUNCHER_SOURCE_TREE_RESOURCES)
target_compile_definitions(${PROJECT_NAME} PRIVATE RESOURCE_PREFIX_URL="${CMAKE_CURRENT_SOURCE_DIR}/resources/")
target_compile_definitions(${PROJECT_NAME} PRIVATE HIFI_USE_LOCAL_FILE)
message("Use source tree resources path: file://${CMAKE_CURRENT_SOURCE_DIR}/resources/")
else()
target_compile_definitions(${PROJECT_NAME} PRIVATE RESOURCE_PREFIX_URL="qrc:/")
message("Use resource.rcc path: qrc:/")
endif()
target_compile_definitions(${PROJECT_NAME} PRIVATE LAUNCHER_BUILD_VERSION="${BUILD_VERSION}")
if (APPLE)
install(
TARGETS HQLauncher
BUNDLE DESTINATION "."
COMPONENT applications)
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR})
include(CPackComponent)
set(CPACK_PACKAGE_NAME "HQ Launcher")
set(CPACK_PACKAGE_VENDOR "High Fidelity")
set(CPACK_PACKAGE_FILE_NAME "HQ Launcher")
set(CPACK_NSIS_DISPLAY_NAME ${_DISPLAY_NAME})
set(DMG_SUBFOLDER_NAME "High Fidelity")
set(ESCAPED_DMG_SUBFOLDER_NAME "")
set(DMG_SUBFOLDER_ICON "${CMAKE_SOURCE_DIR}/cmake/installer/install-folder.rsrc")
set(CPACK_GENERATOR "DragNDrop")
include(CPack)
endif()

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>high-fidelity.hifi</string>
</array>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,14 @@
# Hide automoc folders (for IDEs)
set(AUTOGEN_TARGETS_FOLDER "hidden/generated")
# Find includes in corresponding build directories
set(CMAKE_INCLUDE_CURRENT_DIR ON)
# CMAKE_CURRENT_SOURCE_DIR is the parent folder here
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/")
file(GLOB LAUNCHER_CUSTOM_MACROS "cmake/macros/*.cmake")
foreach(CUSTOM_MACRO ${LAUNCHER_CUSTOM_MACROS})
include(${CUSTOM_MACRO})
endforeach()
unset(LAUNCHER_CUSTOM_MACROS)

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View file

@ -0,0 +1,31 @@
function(GENERATE_QRC)
set(oneValueArgs OUTPUT PREFIX PATH)
set(multiValueArgs CUSTOM_PATHS GLOBS)
cmake_parse_arguments(GENERATE_QRC "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )
if ("${GENERATE_QRC_PREFIX}" STREQUAL "")
set(QRC_PREFIX_PATH /)
else()
set(QRC_PREFIX_PATH ${GENERATE_QRC_PREFIX})
endif()
foreach(GLOB ${GENERATE_QRC_GLOBS})
file(GLOB_RECURSE FOUND_FILES RELATIVE ${GENERATE_QRC_PATH} ${GLOB})
foreach(FILENAME ${FOUND_FILES})
if (${FILENAME} MATCHES "^\\.\\.")
continue()
endif()
list(APPEND ALL_FILES "${GENERATE_QRC_PATH}/${FILENAME}")
set(QRC_CONTENTS "${QRC_CONTENTS}<file alias=\"${FILENAME}\">${GENERATE_QRC_PATH}/${FILENAME}</file>\n")
endforeach()
endforeach()
foreach(CUSTOM_PATH ${GENERATE_QRC_CUSTOM_PATHS})
string(REPLACE "=" ";" CUSTOM_PATH ${CUSTOM_PATH})
list(GET CUSTOM_PATH 0 IMPORT_PATH)
list(GET CUSTOM_PATH 1 LOCAL_PATH)
set(QRC_CONTENTS "${QRC_CONTENTS}<file alias=\"${LOCAL_PATH}\">${IMPORT_PATH}</file>\n")
endforeach()
set(GENERATE_QRC_DEPENDS ${ALL_FILES} PARENT_SCOPE)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/resources.qrc.in" ${GENERATE_QRC_OUTPUT})
endfunction()

View file

@ -0,0 +1,45 @@
#
# SetPackagingParameters.cmake
# cmake/macros
#
# Created by Leonardo Murillo on 07/14/2015.
# Copyright 2015 High Fidelity, Inc.
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
# This macro checks some Jenkins defined environment variables to determine the origin of this build
# and decides how targets should be packaged.
macro(SET_PACKAGING_PARAMETERS)
set(PR_BUILD 0)
set(PRODUCTION_BUILD 0)
set(DEV_BUILD 0)
set(BUILD_NUMBER 0)
set_from_env(RELEASE_TYPE RELEASE_TYPE "DEV")
set_from_env(RELEASE_NUMBER RELEASE_NUMBER "")
set_from_env(STABLE_BUILD STABLE_BUILD 0)
message(STATUS "The RELEASE_TYPE variable is: ${RELEASE_TYPE}")
set(BUILD_NUMBER ${RELEASE_NUMBER})
if (RELEASE_TYPE STREQUAL "PRODUCTION")
set(PRODUCTION_BUILD 1)
set(BUILD_VERSION ${RELEASE_NUMBER})
# add definition for this release type
add_definitions(-DPRODUCTION_BUILD)
elseif (RELEASE_TYPE STREQUAL "PR")
set(PR_BUILD 1)
set(BUILD_VERSION "PR${RELEASE_NUMBER}")
# add definition for this release type
add_definitions(-DPR_BUILD)
else ()
set(DEV_BUILD 1)
set(BUILD_VERSION "dev")
endif ()
endmacro(SET_PACKAGING_PARAMETERS)

View file

@ -0,0 +1,33 @@
set(qt_static_lib_dependices
"qtpcre2"
"qtlibpng"
"qtfreetype"
"Qt5AccessibilitySupport"
"Qt5FbSupport"
"Qt5OpenGLExtensions"
"Qt5QuickTemplates2"
"Qt5FontDatabaseSupport"
"Qt5ThemeSupport"
"Qt5EventDispatcherSupport")
if (WIN32)
elseif(APPLE)
set(qt_static_lib_dependices
${qt_static_lib_dependices}
"Qt5GraphicsSupport"
"Qt5CglSupport"
"Qt5ClipboardSupport")
endif()
set(LIBS_PREFIX "${_qt5Core_install_prefix}/lib/")
foreach (_qt_static_dep ${qt_static_lib_dependices})
if (WIN32)
set(lib_path "${LIBS_PREFIX}${_qt_static_dep}.lib")
else()
set(lib_path "${LIBS_PREFIX}lib${_qt_static_dep}.a")
endif()
set(QT_STATIC_LIBS ${QT_STATIC_LIBS} ${lib_path})
endforeach()
unset(qt_static_lib_dependices)

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${APP_NAME}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key>
<string>com.highfidelity.launcher</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>NSMainNibFile</key>
<string>Window</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundleDisplayName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
</dict>
</plist>

View file

@ -0,0 +1 @@
IDI_ICON1 ICON DISCARDABLE "@CONFIGURE_ICON_PATH@"

View file

@ -0,0 +1,5 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="@QRC_PREFIX_PATH@">
@QRC_CONTENTS@
</qresource>
</RCC>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,13 @@
//
// Launcher.rc2 - resources Microsoft Visual C++ does not edit directly
//
#ifdef APSTUDIO_INVOKED
#error this file is not editable by Microsoft Visual C++
#endif //APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
// Add manually edited resources here...
/////////////////////////////////////////////////////////////////////////////

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,102 @@
import QtQuick 2.3
import QtQuick.Controls 2.1
import "HFControls"
Item {
id: root
anchors.fill: parent
Image {
anchors.centerIn: parent
width: parent.width
height: parent.height
mirror: true
source: PathUtils.resourcePath("images/hifi_window@2x.png");
transformOrigin: Item.Center
rotation: 180
}
Image {
id: logo
width: 150
height: 150
source: PathUtils.resourcePath("images/HiFi_Voxel.png");
anchors {
top: root.top
topMargin: 98
horizontalCenter: root.horizontalCenter
}
RotationAnimator {
target: logo;
loops: Animation.Infinite
from: 0;
to: 360;
duration: 5000
running: true
}
}
HFTextHeader {
id: firstText
text: "Setup will take a moment"
anchors {
top: logo.bottom
topMargin: 46
horizontalCenter: logo.horizontalCenter
}
}
HFTextRegular {
id: secondText
text: "We're getting everything set up for you."
anchors {
top: firstText.bottom
topMargin: 8
horizontalCenter: logo.horizontalCenter
}
}
ProgressBar {
id: progressBar
width: 394
height: 8
value: LauncherState.downloadProgress;
anchors {
top: secondText.bottom
topMargin: 30
horizontalCenter: logo.horizontalCenter
}
background: Rectangle {
implicitWidth: progressBar.width
implicitHeight: progressBar.height
radius: 8
color: "#252525"
}
contentItem: Item {
implicitWidth: progressBar.width
implicitHeight: progressBar.height * 0.85
Rectangle {
width: progressBar.visualPosition * parent.width
height: parent.height
radius: 6
color: "#01B2ED"
}
}
}
Component.onCompleted: {
root.parent.setBuildInfoState("left");
}
}

View file

@ -0,0 +1,61 @@
import QtQuick 2.3
import QtQuick.Controls 2.1
import "HFControls"
Item {
id: root
anchors.centerIn: parent
Image {
anchors.centerIn: parent
width: parent.width
height: parent.height
mirror: true
source: PathUtils.resourcePath("images/hifi_window@2x.png");
transformOrigin: Item.Center
rotation: 0
}
Image {
id: logo
width: 132
height: 134
source: PathUtils.resourcePath("images/HiFi_Voxel.png");
anchors {
top: root.top
topMargin: 144
horizontalCenter: root.horizontalCenter
}
}
HFTextHeader {
id: header
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: "You're all set!"
anchors {
top: logo.bottom
topMargin: 26
horizontalCenter: logo.horizontalCenter
}
}
HFTextRegular {
id: description
text: "We will see you in world."
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors {
top: header.bottom
topMargin: 8
horizontalCenter: header.horizontalCenter
}
}
Component.onCompleted: {
root.parent.setBuildInfoState("left");
}
}

View file

@ -0,0 +1,213 @@
import QtQuick 2.3
import QtQuick 2.1
import "../HFControls"
import HQLauncher 1.0
Item {
id: root
anchors.centerIn: parent
property string titleText: "Create Your Username and Password"
property string usernamePlaceholder: "Username"
property string passwordPlaceholder: "Set a password (must be at least 6 characters)"
property int marginLeft: root.width * 0.15
property bool enabled: LauncherState.applicationState == ApplicationState.WaitingForSignup
Image {
anchors.centerIn: parent
width: parent.width
height: parent.height
mirror: true
source: PathUtils.resourcePath("images/hifi_window@2x.png");
transformOrigin: Item.Center
rotation: 180
}
HFTextHeader {
id: title
width: 481
wrapMode: Text.WordWrap
lineHeight: 35
lineHeightMode: Text.FixedHeight
text: LauncherState.lastSignupErrorMessage.length == 0 ? root.titleText : "Uh oh"
anchors {
top: root.top
topMargin: 29
left: root.left
leftMargin: root.marginLeft
}
}
HFTextError {
id: error
width: 425
wrapMode: Text.Wrap
visible: LauncherState.lastSignupErrorMessage.length > 0
text: LauncherState.lastSignupErrorMessage
textFormat: Text.StyledText
onLinkActivated: {
if (link == "login") {
LauncherState.gotoLogin();
} else {
LauncherState.openURLInBrowser(link)
}
}
anchors {
left: root.left
leftMargin: root.marginLeft
top: title.bottom
topMargin: 18
}
}
HFTextField {
id: email
width: 430
enabled: root.enabled
placeholderText: "Verify Your Email"
seperatorColor: Qt.rgba(1, 1, 1, 0.3)
anchors {
top: error.visible ? error.bottom : title.bottom
left: root.left
leftMargin: root.marginLeft
topMargin: 18
}
}
HFTextField {
id: username
width: 430
enabled: root.enabled
placeholderText: root.usernamePlaceholder
seperatorColor: Qt.rgba(1, 1, 1, 0.3)
anchors {
top: email.bottom
left: root.left
leftMargin: root.marginLeft
topMargin: 18
}
}
HFTextField {
id: password
width: 430
enabled: root.enabled
placeholderText: root.passwordPlaceholder
seperatorColor: Qt.rgba(1, 1, 1, 0.3)
togglePasswordField: true
echoMode: TextInput.Password
anchors {
top: username.bottom
left: root.left
leftMargin: root.marginLeft
topMargin: 18
}
}
HFTextRegular {
id: displayNameText
text: "This is the display name other people see in High Fidelity. It can be changed at any time from your profile."
wrapMode: Text.Wrap
width: 430
anchors {
top: password.bottom
left: root.left
leftMargin: root.marginLeft
topMargin: 22
}
}
HFTextField {
id: displayName
width: 430
enabled: root.enabled
placeholderText: "Display Name"
seperatorColor: Qt.rgba(1, 1, 1, 0.3)
anchors {
top: displayNameText.bottom
left: root.left
leftMargin: root.marginLeft
topMargin: 4
}
onAccepted: {
if (root.enabled && email.text.length > 0 && username.text.length > 0 && password.text.length > 0 && displayName.text.length > 0) {
LauncherState.signup(email.text, username.text, password.text, displayName.text);
}
}
}
HFButton {
id: button
width: 134
enabled: root.enabled && email.text.length > 0 && username.text.length > 0 && password.text.length > 0 && displayName.text.length > 0
text: "NEXT"
anchors {
top: displayName.bottom
left: root.left
leftMargin: root.marginLeft
topMargin: 21
}
onClicked: LauncherState.signup(email.text, username.text, password.text, displayName.text)
}
Text {
text: "Already have an account?"
font.family: "Graphik"
font.pixelSize: 14
color: "#009EE0"
anchors {
top: button.bottom
topMargin: 16
left: button.left
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
LauncherState.gotoLogin();
}
}
}
HFTextLogo {
anchors {
bottom: root.bottom
bottomMargin: 46
right: displayName.right
}
}
Component.onCompleted: {
root.parent.setBuildInfoState("left");
}
}

View file

@ -0,0 +1,89 @@
import QtQuick 2.3
import QtQuick 2.1
import "../HFControls"
Item {
id: root
anchors.centerIn: parent
Image {
anchors.centerIn: parent
width: parent.width
height: parent.height
mirror: false
source: "qrc:/images/hifi_window@2x.png"
//fillMode: Image.PreserveAspectFit
transformOrigin: Item.Center
//rotation: 90
}
Image {
id: logo
width: 132
height: 134
source: "qrc:/images/HiFi_Voxel.png"
anchors {
top: root.top
topMargin: 98
horizontalCenter: root.horizontalCenter
}
}
HFTextHeader {
id: header
text: "Uh oh."
anchors {
top: logo.bottom
topMargin: 26
horizontalCenter: logo.horizontalCenter
}
}
HFTextRegular {
id: description
text: "We seem to have a problem."
anchors {
top: header.bottom
topMargin: 8
horizontalCenter: header.horizontalCenter
}
}
HFTextRegular {
text: "Please restart."
anchors {
top: description.bottom
topMargin: 1
horizontalCenter: header.horizontalCenter
}
}
HFButton {
id: button
width: 166
text: "RESTART"
anchors {
top: description.bottom
topMargin: 60
horizontalCenter: description.horizontalCenter
}
onClicked: {
LauncherState.restart();
}
}
Component.onCompleted: {
root.parent.setBuildInfoState("right");
}
}

View file

@ -0,0 +1,203 @@
import QtQuick 2.3
import QtQuick 2.1
import "../HFControls"
import HQLauncher 1.0
Item {
id: root
anchors.fill: parent
property bool enabled: LauncherState.applicationState == ApplicationState.WaitingForLogin
Image {
anchors.centerIn: parent
width: parent.width
height: parent.height
mirror: false
source: PathUtils.resourcePath("images/hifi_window@2x.png");
transformOrigin: Item.Center
rotation: 0
}
Item {
width: 430
height: root.height
anchors {
top: root.top
horizontalCenter: root.horizontalCenter
}
HFTextHeader {
id: title
lineHeight: 35
lineHeightMode: Text.FixedHeight
text: "Please Log in"
anchors {
top: parent.top
topMargin: 40
horizontalCenter: parent.horizontalCenter
}
}
HFTextRegular {
id: instruction
visible: LauncherState.lastLoginErrorMessage.length == 0
text: "Use the account credentials you created at sign-up"
anchors {
top: title.bottom
topMargin: 18
horizontalCenter: parent.horizontalCenter
}
}
HFTextError {
id: error
visible: LauncherState.lastLoginErrorMessage.length > 0
text: LauncherState.lastLoginErrorMessage
anchors {
top: title.bottom
topMargin: 18
horizontalCenter: parent.horizontalCenter
}
}
HFTextField {
id: username
enabled: root.enabled
width: 430
text: LauncherState.lastUsedUsername
placeholderText: "Username or Email address"
seperatorColor: Qt.rgba(1, 1, 1, 0.3)
anchors {
top: error.bottom
topMargin: 24
left: parent.left
right: parent.right;
}
}
HFTextField {
id: password
width: 430
enabled: root.enabled
placeholderText: "Password"
togglePasswordField: true
echoMode: TextInput.Password
seperatorColor: Qt.rgba(1, 1, 1, 0.3)
anchors {
top: username.bottom
topMargin: 25
left: parent.left
right: parent.right;
}
}
HFTextRegular {
id: displayText
text: "This is the display name other people see in High Fidelity. It can be changed at any time from your profile."
wrapMode: Text.Wrap
anchors {
top: password.bottom
topMargin: 50
left: parent.left
right: parent.right;
}
}
HFTextField {
id: displayName
width: 430
enabled: root.enabled
placeholderText: "Display name"
seperatorColor: Qt.rgba(1, 1, 1, 0.3)
anchors {
top: displayText.bottom
topMargin: 4
left: parent.left
right: parent.right;
}
onAccepted: {
if (root.enabled && username.text.length > 0 && password.text.length > 0 && displayName.text.length > 0) {
LauncherState.login(username.text, password.text, displayName.text);
}
}
}
HFButton {
id: button
width: 134
enabled: root.enabled && username.text.length > 0 && password.text.length > 0 && displayName.text.length > 0
text: "NEXT"
anchors {
top: displayName.bottom
topMargin: 25
left: parent.left
}
onClicked: LauncherState.login(username.text, password.text, displayName.text)
}
Text {
id: createAccountLink
text: "Sign up"
font.family: "Graphik"
font.pixelSize: 14
color: "#009EE0"
anchors {
top: button.bottom
topMargin: 16
left: parent.left
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
console.log("clicked");
LauncherState.gotoSignup();
}
}
}
HFTextLogo {
anchors {
bottom: createAccountLink.bottom
right: parent.right
}
}
}
Component.onCompleted: {
root.parent.setBuildInfoState("right");
}
}

View file

@ -0,0 +1,44 @@
import QtQuick 2.3
import QtQuick.Controls 2.1
Button {
id: control
height: 50
property string backgroundColor: "#00000000"
property string borderColor: enabled ? "#FFFFFF" : "#7e8c81"
property string textColor: borderColor
property int backgroundOpacity: 1
property int backgroundRadius: 1
property int backgroundWidth: 2
font.family: "Graphik Semibold"
font.pixelSize: 15
background: Rectangle {
implicitWidth: 100
implicitHeight: 40
color: control.backgroundColor
opacity: control.backgroundOpacity
border.color: control.borderColor
border.width: control.backgroundWidth
radius: control.backgroundRadius
}
contentItem: Text {
text: control.text
font: control.font
color: control.textColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
MouseArea {
id: mouseArea
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
}
}

View file

@ -0,0 +1,7 @@
import QtQuick 2.3
import QtQuick 2.1
HFTextRegular {
color: "#FF0014"
}

View file

@ -0,0 +1,90 @@
import QtQuick 2.5
import QtQuick.Controls 2.1
TextField {
id: control
height: 50
font.family: "Graphik Regular"
font.pixelSize: 14
color: (text.length == 0 || !enabled) ? "#7e8c81" : "#000000"
property bool togglePasswordField: false
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignLeft
placeholderText: "PlaceHolder"
property string seperatorColor: "#FFFFFF"
selectByMouse: true
background: Item {
anchors.fill: parent
Rectangle {
id: background
color: "#FFFFFF"
anchors.fill: parent
Image {
id: hide
visible: control.togglePasswordField
source: (control.echoMode == TextInput.Password) ? PathUtils.resourcePath("images/showPass.png") :
PathUtils.resourcePath("images/hidePass.png");
fillMode: Image.PreserveAspectFit
width: 24
smooth: true
anchors {
top: parent.top
topMargin: 18
bottom: parent.bottom
bottomMargin: 18
right: parent.right
rightMargin: 13
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (control.echoMode === TextInput.Password) {
control.echoMode = TextInput.Normal;
} else {
control.echoMode = TextInput.Password;
}
}
}
}
}
}
Keys.onPressed: {
event.accepted = false;
if (Platform === "MacOS") {
if (event.key == Qt.Key_Left) {
if (control.cursorPosition > 0) {
var index = control.cursorPosition - 1;
control.select(index, index);
}
event.accepted = true;
}
if (event.key == Qt.Key_Right) {
if (control.cursorPosition < control.text.length) {
var index = control.cursorPosition + 1;
control.select(index, index);
}
event.accepted = true;
}
if (event.modifiers & Qt.ControlModifier) {
if (event.key == Qt.Key_C) {
control.copy();
event.accepted = true;
} else if (event.key == Qt.Key_V) {
control.paste();
event.accepted = true;
}
}
}
}
}

View file

@ -0,0 +1,8 @@
import QtQuick 2.3
import QtQuick 2.1
Text {
font.family: "Graphik Semibold"
font.pixelSize: 32
color: "#ffffff"
}

View file

@ -0,0 +1,11 @@
import QtQuick 2.3
import QtQuick 2.1
Text {
text: "High Fidelity"
font.bold: true
font.family: "Graphik Semibold"
font.pixelSize: 17
font.letterSpacing: -1
color: "#FFFFFF"
}

View file

@ -0,0 +1,18 @@
import QtQuick 2.3
import QtQuick 2.1
Text {
id: root
font.family: "Graphik Regular"
font.pixelSize: 14
color: "#C4C4C4"
linkColor: color
MouseArea {
anchors.fill: root
cursorShape: root.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
}
}

View file

@ -0,0 +1,7 @@
// login
import "HFBase"
LoginBase {
anchors.fill: parent
}

View file

@ -0,0 +1,28 @@
import QtQuick 2.3
import QtQuick.Controls 2.1
Item {
id: root
anchors.fill: parent
Image {
anchors.centerIn: parent
width: parent.width
height: parent.height
mirror: false
source: PathUtils.resourcePath("images/hifi_window@2x.png");
transformOrigin: Item.Center
rotation: 0
}
Image {
anchors.centerIn: parent
width: 240
height: 180
source: PathUtils.resourcePath("images/hifi_logo_large@2x.png");
}
Component.onCompleted: {
root.parent.setBuildInfoState("right");
}
}

View file

@ -0,0 +1,67 @@
// root.qml
import QtQuick 2.3
import QtQuick.Controls 2.1
import HQLauncher 1.0
import "HFControls"
Item {
id: root
Loader {
anchors.fill: parent
id: loader
function setBuildInfoState(state) {
buildInfo.state = state;
}
}
Component.onCompleted: {
loader.source = "./SplashScreen.qml";
LauncherState.updateSourceUrl.connect(function(url) {
loader.source = url;
});
}
function loadPage(url) {
loader.source = url;
}
HFTextRegular {
id: buildInfo
anchors {
leftMargin: 10
rightMargin: 10
bottomMargin: 10
right: root.right
bottom: root.bottom
}
color: "#666"
text: "V." + LauncherState.buildVersion;
states: [
State {
name: "left"
AnchorChanges {
target: buildInfo
anchors.left: root.left
anchors.right: undefined
}
},
State {
name: "right"
AnchorChanges {
target: buildInfo
anchors.right: root.right
anchors.left: undefined
}
}
]
}
}

View file

@ -0,0 +1,115 @@
#include "BuildsRequest.h"
#include "Helper.h"
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QProcessEnvironment>
bool Builds::getBuild(QString tag, Build* outBuild) {
if (tag.isNull()) {
tag = defaultTag;
}
for (auto& build : builds) {
if (build.tag == tag) {
*outBuild = build;
return true;
}
}
return false;
}
void BuildsRequest::send(QNetworkAccessManager& nam) {
QString latestBuildRequestUrl { "https://thunder.highfidelity.com/builds/api/tags/latest/?format=json" };
QProcessEnvironment processEnvironment = QProcessEnvironment::systemEnvironment();
if (processEnvironment.contains("HQ_LAUNCHER_BUILDS_URL")) {
latestBuildRequestUrl = processEnvironment.value("HQ_LAUNCHER_BUILDS_URL");
}
qDebug() << latestBuildRequestUrl;
QNetworkRequest request{ QUrl(latestBuildRequestUrl) };
auto reply = nam.get(request);
QObject::connect(reply, &QNetworkReply::finished, this, &BuildsRequest::receivedResponse);
}
void BuildsRequest::receivedResponse() {
_state = State::Finished;
auto reply = static_cast<QNetworkReply*>(sender());
if (reply->error()) {
qDebug() << "Error getting builds from thunder: " << reply->errorString();
_error = Error::Unknown;
emit finished();
return;
} else {
qDebug() << "Builds reply has been received";
auto data = reply->readAll();
QJsonParseError parseError;
auto doc = QJsonDocument::fromJson(data, &parseError);
if (parseError.error) {
qDebug() << "Error parsing response from thunder: " << data;
_error = Error::Unknown;
} else {
auto root = doc.object();
if (!root.contains("default_tag")) {
_error = Error::MissingDefaultTag;
emit finished();
return;
}
_latestBuilds.defaultTag = root["default_tag"].toString();
auto results = root["results"];
if (!results.isArray()) {
_error = Error::MalformedResponse;
emit finished();
return;
}
for (auto result : results.toArray()) {
auto entry = result.toObject();
Build build;
build.tag = entry["name"].toString();
build.latestVersion = entry["latest_version"].toInt();
build.buildNumber = entry["build_number"].toInt();
#ifdef Q_OS_WIN
build.installerZipURL = entry["installers"].toObject()["windows"].toObject()["zip_url"].toString();
#elif defined(Q_OS_MACOS)
build.installerZipURL = entry["installers"].toObject()["mac"].toObject()["zip_url"].toString();
#else
#error "Launcher is only supported on Windows and Mac OS"
#endif
_latestBuilds.builds.push_back(build);
}
auto launcherResults = root["launcher"].toObject();
Build launcherBuild;
launcherBuild.latestVersion = launcherResults["version"].toInt();
#ifdef Q_OS_WIN
launcherBuild.installerZipURL = launcherResults["windows"].toObject()["url"].toString();
#elif defined(Q_OS_MACOS)
launcherBuild.installerZipURL = launcherResults["mac"].toObject()["url"].toString();
#else
#error "Launcher is only supported on Windows and Mac OS"
#endif
_latestBuilds.launcherBuild = launcherBuild;
}
}
emit finished();
}

View file

@ -0,0 +1,54 @@
#pragma once
#include <QObject>
#include <QNetworkAccessManager>
struct Build {
QString tag{ QString::null };
int latestVersion{ 0 };
int buildNumber{ 0 };
QString installerZipURL{ QString::null };
};
struct Builds {
bool getBuild(QString tag, Build* outBuild);
QString defaultTag;
std::vector<Build> builds;
Build launcherBuild;
};
class BuildsRequest : public QObject {
Q_OBJECT
public:
enum class State {
Unsent,
Sending,
Finished
};
enum class Error {
None = 0,
Unknown,
MalformedResponse,
MissingDefaultTag,
};
Q_ENUM(Error)
void send(QNetworkAccessManager& nam);
Error getError() const { return _error; }
const Builds& getLatestBuilds() const { return _latestBuilds; }
signals:
void finished();
private slots:
void receivedResponse();
private:
State _state { State::Unsent };
Error _error { Error::None };
Builds _latestBuilds;
};

View file

@ -0,0 +1,35 @@
#include "CommandlineOptions.h"
#include <algorithm>
#include <iostream>
#include <QDebug>
#include <QString>
bool isCommandlineOption(const std::string& option) {
if (option.rfind("--", 0) == 0 && option.at(2) != '-') {
return true;
}
return false;
}
bool CommandlineOptions::contains(const std::string& option) {
auto iter = std::find(_commandlineOptions.begin(), _commandlineOptions.end(), option);
return (iter != _commandlineOptions.end());
}
void CommandlineOptions::parse(const int argc, char** argv) {
for (int index = 1; index < argc; index++) {
std::string option = argv[index];
if (isCommandlineOption(option)) {
_commandlineOptions.push_back(option);
}
}
}
void CommandlineOptions::append(const std::string& command) {
_commandlineOptions.push_back(command);
}
CommandlineOptions* CommandlineOptions::getInstance() {
static CommandlineOptions commandlineOptions;
return &commandlineOptions;
}

View file

@ -0,0 +1,17 @@
#pragma once
#include <string>
#include <vector>
class CommandlineOptions {
public:
CommandlineOptions() = default;
~CommandlineOptions() = default;
void parse(const int argc, char** argv);
bool contains(const std::string& option);
void append(const std::string& command);
static CommandlineOptions* getInstance();
private:
std::vector<std::string> _commandlineOptions;
};

105
launchers/qt/src/Helper.cpp Normal file
View file

@ -0,0 +1,105 @@
#include "Helper.h"
#include "PathUtils.h"
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QProcessEnvironment>
#include <QDateTime>
#include <QTextStream>
#include <QStandardPaths>
#include <iostream>
QString getMetaverseAPIDomain() {
QProcessEnvironment processEnvironment = QProcessEnvironment::systemEnvironment();
if (processEnvironment.contains("HIFI_METAVERSE_URL")) {
return processEnvironment.value("HIFI_METAVERSE_URL");
}
return "https://metaverse.highfidelity.com";
}
void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) {
Q_UNUSED(context);
QString date = QDateTime::currentDateTime().toString("dd/MM/yyyy hh:mm:ss");
QString txt = QString("[%1] ").arg(date);
switch (type) {
case QtDebugMsg:
txt += QString("{Debug} \t\t %1").arg(message);
break;
case QtWarningMsg:
txt += QString("{Warning} \t %1").arg(message);
break;
case QtCriticalMsg:
txt += QString("{Critical} \t %1").arg(message);
break;
case QtFatalMsg:
txt += QString("{Fatal} \t\t %1").arg(message);
break;
case QtInfoMsg:
txt += QString("{Info} \t %1").arg(message);
break;
}
QDir logsDir = PathUtils::getLogsDirectory();
logsDir.mkpath(logsDir.absolutePath());
QString filename = logsDir.absoluteFilePath("Log.txt");
QFile outFile(filename);
outFile.open(QIODevice::WriteOnly | QIODevice::Append);
QTextStream textStream(&outFile);
std::cout << txt.toStdString() << "\n";
textStream << txt << "\n";
outFile.close();
}
bool swapLaunchers(const QString& oldLauncherPath, const QString& newLauncherPath) {
if (!(QFileInfo::exists(oldLauncherPath) && QFileInfo::exists(newLauncherPath))) {
qDebug() << "old launcher: " << oldLauncherPath << "new launcher: " << newLauncherPath << " file does not exist";
}
bool success = false;
#ifdef Q_OS_MAC
qDebug() << "replacing launchers -> old launcher: " << oldLauncherPath << " new launcher: " << newLauncherPath;
success = replaceDirectory(oldLauncherPath, newLauncherPath);
#endif
return success;
}
void cleanLogFile() {
QDir launcherDirectory = PathUtils::getLogsDirectory();
launcherDirectory.mkpath(launcherDirectory.absolutePath());
QString filename = launcherDirectory.absoluteFilePath("Log.txt");
QString tmpFilename = launcherDirectory.absoluteFilePath("Log-last.txt");
if (QFile::exists(filename)) {
if (QFile::exists(tmpFilename)) {
QFile::remove(tmpFilename);
}
QFile::rename(filename, tmpFilename);
QFile::remove(filename);
}
}
QString getHTTPUserAgent() {
#if defined(Q_OS_WIN)
return "HQLauncher/fixme (Windows)";
#elif defined(Q_OS_MACOS)
return "HQLauncher/fixme (MacOS)";
#else
#error Unsupported platform
#endif
}
const QString& getInterfaceSharedMemoryName() {
static const QString applicationName = "High Fidelity Interface - " + qgetenv("USERNAME");
return applicationName;
}

39
launchers/qt/src/Helper.h Normal file
View file

@ -0,0 +1,39 @@
#include <QString>
#include <string>
#ifdef Q_OS_WIN
#include "Windows.h"
#endif
QString getMetaverseAPIDomain();
void launchClient(const QString& clientPath, const QString& homePath, const QString& defaultScriptOverride,
const QString& displayName, const QString& contentCachePath, QString loginResponseToken = QString());
void launchAutoUpdater(const QString& autoUpdaterPath);
bool swapLaunchers(const QString& oldLauncherPath = QString(), const QString& newLauncherPath = QString());
void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message);
void cleanLogFile();
#ifdef Q_OS_MAC
bool replaceDirectory(const QString& orginalDirectory, const QString& newDirectory);
void closeInterfaceIfRunning();
void waitForInterfaceToClose();
bool isLauncherAlreadyRunning();
QString getBundlePath();
#endif
#ifdef Q_OS_WIN
HRESULT createSymbolicLink(LPCSTR lpszPathObj, LPCSTR lpszPathLink, LPCSTR lpszDesc, LPCSTR lpszArgs = (LPCSTR)"");
bool insertRegistryKey(const std::string& regPath, const std::string& name, const std::string& value);
bool insertRegistryKey(const std::string& regPath, const std::string& name, DWORD value);
bool deleteRegistryKey(const std::string& regPath);
BOOL isProcessRunning(const char* processName, int& processID);
BOOL shutdownProcess(DWORD dwProcessId, UINT uExitCode);
#endif
QString getHTTPUserAgent();
const QString& getInterfaceSharedMemoryName();

View file

@ -0,0 +1,150 @@
#include "Helper.h"
#import "NSTask+NSTaskExecveAdditions.h"
#import <Cocoa/Cocoa.h>
#include <QString>
#include <QDebug>
#include <QTimer>
#include <QCoreApplication>
void launchClient(const QString& clientPath, const QString& homePath, const QString& defaultScriptOverride,
const QString& displayName, const QString& contentCachePath, QString loginTokenResponse) {
NSString* homeBookmark = [[NSString stringWithFormat:@"hqhome="] stringByAppendingString:homePath.toNSString()];
NSArray* arguments;
if (!loginTokenResponse.isEmpty()) {
arguments = [NSArray arrayWithObjects:
@"--url" , homePath.toNSString(),
@"--tokens", loginTokenResponse.toNSString(),
@"--cache", contentCachePath.toNSString(),
@"--displayName", displayName.toNSString(),
@"--defaultScriptsOverride", defaultScriptOverride.toNSString(),
@"--setBookmark", homeBookmark,
@"--no-updater",
@"--no-launcher",
@"--suppress-settings-reset", nil];
} else {
arguments = [NSArray arrayWithObjects:
@"--url" , homePath.toNSString(),
@"--cache", contentCachePath.toNSString(),
@"--defaultScriptsOverride", defaultScriptOverride.toNSString(),
@"--setBookmark", homeBookmark,
@"--no-updater",
@"--no-launcher",
@"--suppress-settings-reset", nil];
}
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
NSURL *url = [NSURL fileURLWithPath:[workspace fullPathForApplication:clientPath.toNSString()]];
NSTask *task = [[NSTask alloc] init];
task.launchPath = [url path];
task.arguments = arguments;
[task replaceThisProcess];
}
QString getBundlePath() {
return QString::fromNSString([[NSBundle mainBundle] bundlePath]);
}
void launchAutoUpdater(const QString& autoUpdaterPath) {
NSException *exception;
bool launched = false;
// Older versions of Launcher put updater in `/Contents/Resources/updater`.
NSString* newLauncher = autoUpdaterPath.toNSString();
for (NSString *bundlePath in @[@"/Contents/MacOS/updater",
@"/Contents/Resources/updater",
]) {
NSTask* task = [[NSTask alloc] init];
task.launchPath = [newLauncher stringByAppendingString: bundlePath];
task.arguments = @[[[NSBundle mainBundle] bundlePath], newLauncher];
qDebug() << "launching updater: " << task.launchPath << task.arguments;
@try {
[task launch];
}
@catch (NSException *e) {
qDebug() << "couldn't launch updater: " << QString::fromNSString(e.name) << QString::fromNSString(e.reason);
exception = e;
continue;
}
launched = true;
break;
}
if (!launched) {
@throw exception;
}
QCoreApplication::instance()->quit();
}
@interface UpdaterHelper : NSObject
+(NSURL*) NSStringToNSURL: (NSString*) path;
@end
@implementation UpdaterHelper
+(NSURL*) NSStringToNSURL: (NSString*) path
{
return [NSURL URLWithString: [path stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]] relativeToURL: [NSURL URLWithString:@"file://"]];
}
@end
bool replaceDirectory(const QString& orginalDirectory, const QString& newDirectory) {
NSError *error = nil;
NSFileManager* fileManager = [NSFileManager defaultManager];
NSURL* destinationUrl = [UpdaterHelper NSStringToNSURL:newDirectory.toNSString()];
bool success = (bool) [fileManager replaceItemAtURL:[UpdaterHelper NSStringToNSURL:orginalDirectory.toNSString()] withItemAtURL:[UpdaterHelper NSStringToNSURL:newDirectory.toNSString()]
backupItemName:nil options:NSFileManagerItemReplacementUsingNewMetadataOnly resultingItemURL:&destinationUrl error:&error];
if (error != nil) {
qDebug() << "NSFileManager::replaceItemAtURL -> error: " << error;
}
return success;
}
void waitForInterfaceToClose() {
bool interfaceRunning = true;
while (interfaceRunning) {
interfaceRunning = false;
NSWorkspace* workspace = [NSWorkspace sharedWorkspace];
NSArray* apps = [workspace runningApplications];
for (NSRunningApplication* app in apps) {
if ([[app bundleIdentifier] isEqualToString:@"com.highfidelity.interface"] ||
[[app bundleIdentifier] isEqualToString:@"com.highfidelity.interface-pr"]) {
interfaceRunning = true;
break;
}
}
}
}
bool isLauncherAlreadyRunning() {
NSArray* apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.highfidelity.launcher"];
if ([apps count] > 1) {
qDebug() << "launcher is already running";
return true;
}
return false;
}
void closeInterfaceIfRunning() {
NSWorkspace* workspace = [NSWorkspace sharedWorkspace];
NSArray* apps = [workspace runningApplications];
for (NSRunningApplication* app in apps) {
if ([[app bundleIdentifier] isEqualToString:@"com.highfidelity.interface"] ||
[[app bundleIdentifier] isEqualToString:@"com.highfidelity.interface-pr"]) {
[app terminate];
}
}
}

View file

@ -0,0 +1,188 @@
#include "Helper.h"
#include <QCoreApplication>
#include "windows.h"
#include "winnls.h"
#include "shobjidl.h"
#include "objbase.h"
#include "objidl.h"
#include "shlguid.h"
#include <atlstr.h>
#include <tlhelp32.h>
#include <strsafe.h>
void launchClient(const QString& clientPath, const QString& homePath, const QString& defaultScriptsPath,
const QString& displayName, const QString& contentCachePath, QString loginResponseToken) {
// TODO Fix parameters
QString params = "\"" + clientPath + "\"" + " --url \"" + homePath + "\""
+ " --setBookmark \"hqhome=" + homePath + "\""
+ " --defaultScriptsOverride \"file:///" + defaultScriptsPath + "\""
+ " --cache \"" + contentCachePath + "\""
+ " --suppress-settings-reset --no-launcher --no-updater";
if (!displayName.isEmpty()) {
params += " --displayName \"" + displayName + "\"";
}
if (!loginResponseToken.isEmpty()) {
params += " --tokens \"" + loginResponseToken.replace("\"", "\\\"") + "\"";
}
STARTUPINFO si;
PROCESS_INFORMATION pi;
// set the size of the structures
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// start the program up
BOOL success = CreateProcess(
clientPath.toLatin1().data(),
params.toLatin1().data(),
nullptr, // Process handle not inheritable
nullptr, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
CREATE_NEW_CONSOLE, // Opens file in a separate console
nullptr, // Use parent's environment block
nullptr, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure
);
// Close process and thread handles.
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
void launchAutoUpdater(const QString& autoUpdaterPath) {
QString params = "\"" + QCoreApplication::applicationFilePath() + "\"" + " --restart --noUpdate";
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
BOOL success = CreateProcess(
autoUpdaterPath.toUtf8().data(),
params.toUtf8().data(),
nullptr, // Process handle not inheritable
nullptr, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
CREATE_NEW_CONSOLE, // Opens file in a separate console
nullptr, // Use parent's environment block
nullptr, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure
);
QCoreApplication::instance()->quit();
}
HRESULT createSymbolicLink(LPCSTR lpszPathObj, LPCSTR lpszPathLink, LPCSTR lpszDesc, LPCSTR lpszArgs) {
IShellLink* psl;
// Get a pointer to the IShellLink interface. It is assumed that CoInitialize
// has already been called.
CoInitialize(NULL);
HRESULT hres = E_INVALIDARG;
hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl);
if (SUCCEEDED(hres)) {
IPersistFile* ppf;
// Set the path to the shortcut target and add the description.
psl->SetPath(lpszPathObj);
psl->SetDescription(lpszDesc);
psl->SetArguments(lpszArgs);
// Query IShellLink for the IPersistFile interface, used for saving the
// shortcut in persistent storage.
hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
if (SUCCEEDED(hres)) {
WCHAR wsz[MAX_PATH];
// Ensure that the string is Unicode.
MultiByteToWideChar(CP_ACP, 0, lpszPathLink, -1, wsz, MAX_PATH);
// Add code here to check return value from MultiByteWideChar
// for success.
// Save the link by calling IPersistFile::Save.
hres = ppf->Save(wsz, TRUE);
ppf->Release();
}
psl->Release();
}
CoUninitialize();
return SUCCEEDED(hres);
}
bool insertRegistryKey(const std::string& regPath, const std::string& name, const std::string& value) {
HKEY key;
auto status = RegCreateKeyExA(HKEY_CURRENT_USER, regPath.c_str(), 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE | KEY_QUERY_VALUE, NULL, &key, NULL);
if (status == ERROR_SUCCESS) {
status = RegSetValueExA(key, name.c_str(), 0, REG_SZ, (const BYTE*)value.c_str(), (DWORD)(value.size() + 1));
return (bool) (status == ERROR_SUCCESS);
}
RegCloseKey(key);
return false;
}
bool insertRegistryKey(const std::string& regPath, const std::string& name, DWORD value) {
HKEY key;
auto status = RegCreateKeyExA(HKEY_CURRENT_USER, regPath.c_str(), 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE | KEY_QUERY_VALUE, NULL, &key, NULL);
if (status == ERROR_SUCCESS) {
status = RegSetValueExA(key, name.c_str(), 0, REG_DWORD, (const BYTE*)&value, sizeof(value));
return (bool) TRUE;
}
RegCloseKey(key);
return false;
}
bool deleteRegistryKey(const std::string& regPath) {
TCHAR szDelKey[MAX_PATH * 2];
StringCchCopy(szDelKey, MAX_PATH * 2, regPath.c_str());
auto status = RegDeleteKey(HKEY_CURRENT_USER, szDelKey);
if (status == ERROR_SUCCESS) {
return (bool) TRUE;
}
return false;
}
BOOL isProcessRunning(const char* processName, int& processID) {
bool exists = false;
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (Process32First(snapshot, &entry)) {
while (Process32Next(snapshot, &entry)) {
if (!_stricmp(entry.szExeFile, processName)) {
exists = true;
processID = entry.th32ProcessID;
break;
}
}
}
CloseHandle(snapshot);
return exists;
}
BOOL shutdownProcess(DWORD dwProcessId, UINT uExitCode) {
DWORD dwDesiredAccess = PROCESS_TERMINATE;
BOOL bInheritHandle = FALSE;
HANDLE hProcess = OpenProcess(dwDesiredAccess, bInheritHandle, dwProcessId);
if (hProcess == NULL) {
return FALSE;
}
BOOL result = TerminateProcess(hProcess, uExitCode);
CloseHandle(hProcess);
return result;
}

View file

@ -0,0 +1,42 @@
#include "Launcher.h"
#include <QResource>
#include <QFileInfo>
#include <QQmlContext>
#include <QFontDatabase>
#include "LauncherWindow.h"
#include "LauncherState.h"
#include "PathUtils.h"
Launcher::Launcher(int& argc, char**argv) : QGuiApplication(argc, argv) {
_launcherState = std::make_shared<LauncherState>();
QString platform;
#ifdef Q_OS_WIN
platform = "Windows";
#elif defined(Q_OS_MACOS)
platform = "MacOS";
#endif
_launcherWindow = std::make_unique<LauncherWindow>();
_launcherWindow->rootContext()->setContextProperty("LauncherState", _launcherState.get());
_launcherWindow->rootContext()->setContextProperty("PathUtils", new PathUtils());
_launcherWindow->rootContext()->setContextProperty("Platform", platform);
_launcherWindow->setTitle("High Fidelity");
_launcherWindow->setFlags(Qt::FramelessWindowHint | Qt::Window);
_launcherWindow->setLauncherStatePtr(_launcherState);
LauncherState::declareQML();
QFontDatabase::addApplicationFont(PathUtils::fontPath("Graphik-Regular.ttf"));
QFontDatabase::addApplicationFont(PathUtils::fontPath("Graphik-Medium.ttf"));
QFontDatabase::addApplicationFont(PathUtils::fontPath("Graphik-Semibold.ttf"));
_launcherWindow->setSource(QUrl(PathUtils::resourcePath("qml/root.qml")));
_launcherWindow->setHeight(540);
_launcherWindow->setWidth(627);
_launcherWindow->setResizeMode(QQuickView::SizeRootObjectToView);
_launcherWindow->show();
}
Launcher::~Launcher() {
}

View file

@ -0,0 +1,16 @@
#pragma once
#include <memory>
#include <QGuiApplication>
class LauncherWindow;
class LauncherState;
class Launcher : public QGuiApplication {
public:
Launcher(int& argc, char** argv);
~Launcher();
private:
std::unique_ptr<LauncherWindow> _launcherWindow;
std::shared_ptr<LauncherState> _launcherState;
};

View file

@ -0,0 +1,243 @@
#include "LauncherInstaller_windows.h"
#include "CommandlineOptions.h"
#include "Helper.h"
#include "PathUtils.h"
#include <string>
#include <chrono>
#include <iomanip>
#include <ctime>
#include <sstream>
#include <QStandardPaths>
#include <QFileInfo>
#include <QFile>
#include <QDebug>
LauncherInstaller::LauncherInstaller() {
_launcherInstallDir = PathUtils::getLauncherDirectory();
_launcherApplicationsDir = PathUtils::getApplicationsDirectory();
qDebug() << "Launcher install dir: " << _launcherInstallDir.absolutePath();
qDebug() << "Launcher Application dir: " << _launcherApplicationsDir.absolutePath();
_launcherInstallDir.mkpath(_launcherInstallDir.absolutePath());
_launcherApplicationsDir.mkpath(_launcherApplicationsDir.absolutePath());
char appPath[MAX_PATH];
GetModuleFileNameA(NULL, appPath, MAX_PATH);
QString applicationRunningPath = appPath;
QFileInfo fileInfo(applicationRunningPath);
_launcherRunningFilePath = fileInfo.absoluteFilePath();
_launcherRunningDirPath = fileInfo.absoluteDir().absolutePath();
qDebug() << "Launcher running file path: " << _launcherRunningFilePath;
qDebug() << "Launcher running dir path: " << _launcherRunningDirPath;
}
bool LauncherInstaller::runningOutsideOfInstallDir() {
return (QString::compare(_launcherInstallDir.absolutePath(), _launcherRunningDirPath) != 0);
}
void LauncherInstaller::install() {
if (runningOutsideOfInstallDir()) {
qDebug() << "Installing HQ Launcher....";
uninstallOldLauncher();
QString oldLauncherPath = PathUtils::getLauncherFilePath();
if (QFile::exists(oldLauncherPath)) {
bool didRemove = QFile::remove(oldLauncherPath);
qDebug() << "did remove file: " << didRemove;
}
qDebug() << "Current launcher location: " << _launcherRunningFilePath;
bool success = QFile::copy(_launcherRunningFilePath, oldLauncherPath);
if (success) {
qDebug() << "Launcher installed: " << oldLauncherPath;
} else {
qDebug() << "Failed to install: " << oldLauncherPath;
}
deleteShortcuts();
createShortcuts();
deleteApplicationRegistryKeys();
createApplicationRegistryKeys();
} else {
qDebug() << "Failed to install HQ Launcher";
}
}
void LauncherInstaller::createShortcuts() {
QString launcherPath = PathUtils::getLauncherFilePath();
QString uninstallLinkPath = _launcherInstallDir.absoluteFilePath("Uninstall HQ.lnk");
QDir desktopDir = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
QString appStartLinkPath = _launcherApplicationsDir.absoluteFilePath("HQ Launcher.lnk");
QString uninstallAppStartLinkPath = _launcherApplicationsDir.absoluteFilePath("Uninstall HQ.lnk");
QString desktopAppLinkPath = desktopDir.absoluteFilePath("HQ Launcher.lnk");
createSymbolicLink((LPCSTR)launcherPath.toStdString().c_str(), (LPCSTR)uninstallLinkPath.toStdString().c_str(),
(LPCSTR)("Click to Uninstall HQ"), (LPCSTR)("--uninstall"));
createSymbolicLink((LPCSTR)launcherPath.toStdString().c_str(), (LPCSTR)uninstallAppStartLinkPath.toStdString().c_str(),
(LPCSTR)("Click to Uninstall HQ"), (LPCSTR)("--uninstall"));
createSymbolicLink((LPCSTR)launcherPath.toStdString().c_str(), (LPCSTR)desktopAppLinkPath.toStdString().c_str(),
(LPCSTR)("Click to Setup and Launch HQ"));
createSymbolicLink((LPCSTR)launcherPath.toStdString().c_str(), (LPCSTR)appStartLinkPath.toStdString().c_str(),
(LPCSTR)("Click to Setup and Launch HQ"));
}
QString randomQtString() {
const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
const int randomStringLength = 5;
auto now = std::chrono::system_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
qsrand(duration.count());
QString randomString;
for(int i = 0; i < randomStringLength; i++)
{
int index = qrand() % possibleCharacters.length();
QChar nextChar = possibleCharacters.at(index);
randomString.append(nextChar);
}
return randomString;
}
void LauncherInstaller::uninstall() {
qDebug() << "Uninstall Launcher";
deleteShortcuts();
CommandlineOptions* options = CommandlineOptions::getInstance();
if (!options->contains("--resumeUninstall")) {
QDir tmpDirectory = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
QString destination = tmpDirectory.absolutePath() + "/" + randomQtString() + ".exe";
qDebug() << "temp file destination: " << destination;
bool copied = QFile::copy(_launcherRunningFilePath, destination);
if (copied) {
QString params = "\"" + _launcherRunningFilePath + "\"" + " --resumeUninstall";
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
BOOL success = CreateProcess(
destination.toUtf8().data(),
params.toUtf8().data(),
nullptr, // Process handle not inheritable
nullptr, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
CREATE_NEW_CONSOLE, // Opens file in a separate console
nullptr, // Use parent's environment block
nullptr, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure
);
} else {
qDebug() << "Failed to complete uninstall launcher";
}
return;
}
QString launcherPath = _launcherInstallDir.absoluteFilePath("HQ Launcher.exe");
if (QFile::exists(launcherPath)) {
bool removed = QFile::remove(launcherPath);
qDebug() << "Successfully removed " << launcherPath << ": " << removed;
}
deleteApplicationRegistryKeys();
}
void LauncherInstaller::deleteShortcuts() {
QDir desktopDir = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
QString applicationPath = _launcherApplicationsDir.absolutePath();
QString uninstallLinkPath = _launcherInstallDir.absoluteFilePath("Uninstall HQ.lnk");
if (QFile::exists(uninstallLinkPath)) {
QFile::remove(uninstallLinkPath);
}
QString appStartLinkPath = _launcherApplicationsDir.absoluteFilePath("HQ Launcher.lnk");
if (QFile::exists(appStartLinkPath)) {
QFile::remove(appStartLinkPath);
}
QString uninstallAppStartLinkPath = _launcherApplicationsDir.absoluteFilePath("Uninstall HQ.lnk");
if (QFile::exists(uninstallAppStartLinkPath)) {
QFile::remove(uninstallAppStartLinkPath);
}
QString desktopAppLinkPath = desktopDir.absoluteFilePath("HQ Launcher.lnk");
if (QFile::exists(desktopAppLinkPath)) {
QFile::remove(desktopAppLinkPath);
}
}
void LauncherInstaller::uninstallOldLauncher() {
QDir localAppDir = QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation).value(0) + "/../../HQ";
QDir startAppDir = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).value(0) + "/HQ";
QDir desktopDir = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
qDebug() << localAppDir.absolutePath();
qDebug() << startAppDir.absolutePath();
QString desktopAppLinkPath = desktopDir.absoluteFilePath("HQ Launcher.lnk");
if (QFile::exists(desktopAppLinkPath)) {
QFile::remove(desktopAppLinkPath);
}
QString uninstallLinkPath = localAppDir.absoluteFilePath("Uninstall HQ.lnk");
if (QFile::exists(uninstallLinkPath)) {
QFile::remove(uninstallLinkPath);
}
QString applicationPath = localAppDir.absoluteFilePath("HQ Launcher.exe");
if (QFile::exists(applicationPath)) {
QFile::remove(applicationPath);
}
QString appStartLinkPath = startAppDir.absoluteFilePath("HQ Launcher.lnk");
if (QFile::exists(appStartLinkPath)) {
QFile::remove(appStartLinkPath);
}
QString uninstallAppStartLinkPath = startAppDir.absoluteFilePath("Uninstall HQ.lnk");
if (QFile::exists(uninstallAppStartLinkPath)) {
QFile::remove(uninstallAppStartLinkPath);
}
}
void LauncherInstaller::createApplicationRegistryKeys() {
const std::string REGISTRY_PATH = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\HQ";
bool success = insertRegistryKey(REGISTRY_PATH, "DisplayName", "HQ");
std::string installPath = _launcherInstallDir.absolutePath().replace("/", "\\").toStdString();
success = insertRegistryKey(REGISTRY_PATH, "InstallLocation", installPath);
std::string applicationExe = installPath + "\\HQ Launcher.exe";
std::string uninstallPath = applicationExe + " --uninstall";
qDebug() << QString::fromStdString(applicationExe);
qDebug() << QString::fromStdString(uninstallPath);
success = insertRegistryKey(REGISTRY_PATH, "UninstallString", uninstallPath);
success = insertRegistryKey(REGISTRY_PATH, "DisplayVersion", std::string(LAUNCHER_BUILD_VERSION));
success = insertRegistryKey(REGISTRY_PATH, "DisplayIcon", applicationExe);
success = insertRegistryKey(REGISTRY_PATH, "Publisher", "High Fidelity");
auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::stringstream date;
date << std::put_time(std::localtime(&now), "%Y-%m-%d") ;
success = insertRegistryKey(REGISTRY_PATH, "InstallDate", date.str());
success = insertRegistryKey(REGISTRY_PATH, "EstimatedSize", (DWORD)14181);
success = insertRegistryKey(REGISTRY_PATH, "NoModify", (DWORD)1);
success = insertRegistryKey(REGISTRY_PATH, "NoRepair", (DWORD)1);
qDebug() << "Did succcessfully insertRegistyKeys: " << success;
}
void LauncherInstaller::deleteApplicationRegistryKeys() {
const std::string regPath= "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\HQ";
bool success = deleteRegistryKey(regPath.c_str());
qDebug() << "Did delete Application Registry Keys: " << success;
}

View file

@ -0,0 +1,23 @@
#pragma once
#include <QDir>
class LauncherInstaller {
public:
LauncherInstaller();
~LauncherInstaller() = default;
void install();
void uninstall();
bool runningOutsideOfInstallDir();
private:
void createShortcuts();
void uninstallOldLauncher();
void createApplicationRegistryKeys();
void deleteShortcuts();
void deleteApplicationRegistryKeys();
QDir _launcherInstallDir;
QDir _launcherApplicationsDir;
QString _launcherRunningFilePath;
QString _launcherRunningDirPath;
};

View file

@ -0,0 +1,797 @@
#include "LauncherState.h"
#include "CommandlineOptions.h"
#include "PathUtils.h"
#include "Unzipper.h"
#include "Helper.h"
#include <array>
#include <cstdlib>
#include <QProcess>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QUrlQuery>
#include <QDebug>
#include <QQmlEngine>
#include <QThreadPool>
#include <QEventLoop>
#include <qregularexpression.h>
#ifdef Q_OS_WIN
#include <shellapi.h>
#endif
//#define BREAK_ON_ERROR
//#define DEBUG_UI
const QString configHomeLocationKey { "homeLocation" };
const QString configLastLoginKey { "lastLogin" };
const QString configLoggedInKey{ "loggedIn" };
const QString configLauncherPathKey{ "launcherPath" };
Q_INVOKABLE void LauncherState::openURLInBrowser(QString url) {
#ifdef Q_OS_WIN
ShellExecute(0, 0, url.toLatin1(), 0, 0 , SW_SHOW);
#elif defined(Q_OS_DARWIN)
system("open \"" + url.toLatin1() + "\"");
#endif
}
void LauncherState::toggleDebugState() {
#ifdef DEBUG_UI
_isDebuggingScreens = !_isDebuggingScreens;
UIState updatedUIState = getUIState();
if (_uiState != updatedUIState) {
emit uiStateChanged();
emit updateSourceUrl(PathUtils::resourcePath(getCurrentUISource()));
_uiState = getUIState();
}
#endif
}
void LauncherState::gotoNextDebugScreen() {
#ifdef DEBUG_UI
if (_currentDebugScreen < (UIState::UI_STATE_NUM - 1)) {
_currentDebugScreen = (UIState)(_currentDebugScreen + 1);
emit uiStateChanged();
emit updateSourceUrl(PathUtils::resourcePath(getCurrentUISource()));
_uiState = getUIState();
}
#endif
}
void LauncherState::gotoPreviousDebugScreen() {
#ifdef DEBUG_UI
if (_currentDebugScreen > 0) {
_currentDebugScreen = (UIState)(_currentDebugScreen - 1);
emit uiStateChanged();
emit updateSourceUrl(PathUtils::resourcePath(getCurrentUISource()));
_uiState = getUIState();
}
#endif
}
bool LauncherState::shouldDownloadContentCache() const {
return !_contentCacheURL.isEmpty() && !QFile::exists(PathUtils::getContentCachePath());
}
void LauncherState::setLastSignupErrorMessage(const QString& msg) {
_lastSignupErrorMessage = msg;
emit lastSignupErrorMessageChanged();
}
void LauncherState::setLastLoginErrorMessage(const QString& msg) {
_lastLoginErrorMessage = msg;
emit lastLoginErrorMessageChanged();
}
static const std::array<QString, LauncherState::UIState::UI_STATE_NUM> QML_FILE_FOR_UI_STATE =
{ { "qml/SplashScreen.qml", "qml/HFBase/CreateAccountBase.qml", "qml/HFBase/LoginBase.qml",
"qml/Download.qml", "qml/DownloadFinished.qml", "qml/HFBase/Error.qml" } };
void LauncherState::ASSERT_STATE(ApplicationState state) {
if (_applicationState != state) {
qDebug() << "Unexpected state, current: " << _applicationState << ", expected: " << state;
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
}
}
void LauncherState::ASSERT_STATE(const std::vector<ApplicationState>& states) {
for (auto state : states) {
if (_applicationState == state) {
return;
}
}
qDebug() << "Unexpected state, current: " << _applicationState << ", expected: " << states;
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
}
LauncherState::LauncherState() {
_launcherDirectory = PathUtils::getLauncherDirectory();
qDebug() << "Launcher directory: " << _launcherDirectory.absolutePath();
_launcherDirectory.mkpath(_launcherDirectory.absolutePath());
_launcherDirectory.mkpath(PathUtils::getDownloadDirectory().absolutePath());
requestBuilds();
}
QString LauncherState::getCurrentUISource() const {
return QML_FILE_FOR_UI_STATE[getUIState()];
}
void LauncherState::declareQML() {
qmlRegisterType<LauncherState>("HQLauncher", 1, 0, "ApplicationState");
}
LauncherState::UIState LauncherState::getUIState() const {
if (_isDebuggingScreens) {
return _currentDebugScreen;
}
switch (_applicationState) {
case ApplicationState::Init:
case ApplicationState::RequestingBuilds:
case ApplicationState::GettingCurrentClientVersion:
return UIState::SplashScreen;
case ApplicationState::WaitingForLogin:
case ApplicationState::RequestingLogin:
return UIState::LoginScreen;
case ApplicationState::WaitingForSignup:
case ApplicationState::RequestingSignup:
case ApplicationState::RequestingLoginAfterSignup:
return UIState::SignupScreen;
case ApplicationState::DownloadingClient:
case ApplicationState::InstallingClient:
case ApplicationState::DownloadingContentCache:
case ApplicationState::InstallingContentCache:
return UIState::DownloadScreen;
case ApplicationState::LaunchingHighFidelity:
return UIState::DownloadFinishedScreen;
case ApplicationState::UnexpectedError:
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
return UIState::ErrorScreen;
default:
qDebug() << "FATAL: No UI for" << _applicationState;
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
return UIState::ErrorScreen;
}
}
void LauncherState::restart() {
setApplicationState(ApplicationState::Init);
requestBuilds();
}
void LauncherState::requestBuilds() {
ASSERT_STATE(ApplicationState::Init);
setApplicationState(ApplicationState::RequestingBuilds);
auto request = new BuildsRequest();
QObject::connect(request, &BuildsRequest::finished, this, [=] {
ASSERT_STATE(ApplicationState::RequestingBuilds);
if (request->getError() != BuildsRequest::Error::None) {
setApplicationStateError("Could not retrieve latest builds");
return;
}
_latestBuilds = request->getLatestBuilds();
CommandlineOptions* options = CommandlineOptions::getInstance();
qDebug() << "Latest version: " << _latestBuilds.launcherBuild.latestVersion
<< "Curretn version: " << getBuildVersion().toInt();
if (shouldDownloadLauncher() && !options->contains("--noUpdate")) {
downloadLauncher();
return;
}
getCurrentClientVersion();
});
request->send(_networkAccessManager);
}
QString LauncherState::getBuildVersion() {
QString buildVersion { LAUNCHER_BUILD_VERSION };
QProcessEnvironment processEnvironment = QProcessEnvironment::systemEnvironment();
if (processEnvironment.contains("HQ_LAUNCHER_BUILD_VERSION")) {
buildVersion = processEnvironment.value("HQ_LAUNCHER_BUILD_VERSION");
}
return buildVersion;
}
bool LauncherState::shouldDownloadLauncher() {
return _latestBuilds.launcherBuild.latestVersion != getBuildVersion().toInt();
}
void LauncherState::getCurrentClientVersion() {
ASSERT_STATE(ApplicationState::RequestingBuilds);
setApplicationState(ApplicationState::GettingCurrentClientVersion);
QProcess client;
QEventLoop loop;
connect(&client, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), &loop, &QEventLoop::exit, Qt::QueuedConnection);
connect(&client, &QProcess::errorOccurred, &loop, &QEventLoop::exit, Qt::QueuedConnection);
client.start(PathUtils::getClientExecutablePath(), { "--version" });
loop.exec();
// TODO Handle errors
auto output = client.readAllStandardOutput();
QRegularExpression regex { "Interface (?<version>\\d+)(-.*)?" };
auto match = regex.match(output);
if (match.hasMatch()) {
_currentClientVersion = match.captured("version");
} else {
_currentClientVersion = QString::null;
}
qDebug() << "Current client version is: " << _currentClientVersion;
{
auto path = PathUtils::getConfigFilePath();
QFile configFile{ path };
if (configFile.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(configFile.readAll());
auto root = doc.object();
_config.launcherPath = PathUtils::getLauncherFilePath();
_config.loggedIn = false;
if (root.contains(configLoggedInKey)) {
_config.loggedIn = root[configLoggedInKey].toBool();
}
if (root.contains(configLastLoginKey)) {
_config.lastLogin = root[configLastLoginKey].toString();
}
if (root.contains(configHomeLocationKey)) {
_config.homeLocation = root[configHomeLocationKey].toString();
}
if (root.contains(configLauncherPathKey)) {
_config.launcherPath = root[configLauncherPathKey].toString();
}
} else {
qDebug() << "Failed to open config.json";
}
}
qDebug() << "Is logged-in: " << _config.loggedIn;
if (_config.loggedIn) {
downloadClient();
} else {
if (_config.lastLogin.isEmpty()) {
setApplicationState(ApplicationState::WaitingForSignup);
} else {
_lastUsedUsername = _config.lastLogin;
setApplicationState(ApplicationState::WaitingForLogin);
}
}
}
void LauncherState::gotoSignup() {
if (_applicationState == ApplicationState::WaitingForLogin) {
setLastSignupErrorMessage("");
_lastLoginErrorMessage = "";
setApplicationState(ApplicationState::WaitingForSignup);
} else {
qDebug() << "Error, can't switch to signup page, current state is: " << _applicationState;
}
}
void LauncherState::gotoLogin() {
if (_applicationState == ApplicationState::WaitingForSignup) {
setLastLoginErrorMessage("");
setApplicationState(ApplicationState::WaitingForLogin);
} else {
qDebug() << "Error, can't switch to signup page, current state is: " << _applicationState;
}
}
void LauncherState::signup(QString email, QString username, QString password, QString displayName) {
ASSERT_STATE(ApplicationState::WaitingForSignup);
_username = username;
_password = password;
setApplicationState(ApplicationState::RequestingSignup);
auto signupRequest = new SignupRequest();
_displayName = displayName;
{
_lastSignupError = SignupRequest::Error::None;
emit lastSignupErrorChanged();
}
QObject::connect(signupRequest, &SignupRequest::finished, this, [this, signupRequest, username] {
signupRequest->deleteLater();
_lastSignupError = signupRequest->getError();
emit lastSignupErrorChanged();
auto err = signupRequest->getError();
if (err == SignupRequest::Error::ExistingUsername) {
setLastSignupErrorMessage(_username + " is already taken. Please try a different username.");
setApplicationState(ApplicationState::WaitingForSignup);
return;
} else if (err == SignupRequest::Error::BadPassword) {
setLastSignupErrorMessage("That's an invalid password. Must be at least 6 characters.");
setApplicationState(ApplicationState::WaitingForSignup);
return;
} else if (err == SignupRequest::Error::BadUsername) {
setLastSignupErrorMessage("That's an invalid username. Please try another username.");
setApplicationState(ApplicationState::WaitingForSignup);
return;
} else if (err == SignupRequest::Error::UserProfileAlreadyCompleted) {
setLastSignupErrorMessage("An account with this email already exists. Please <b><a href='login'>log in</a></b>.");
setApplicationState(ApplicationState::WaitingForSignup);
return;
} else if (err == SignupRequest::Error::NoSuchEmail) {
setLastSignupErrorMessage("That email isn't setup yet. <a href='https://www.highfidelity.com/hq-support'>Request access</a>.");
setApplicationState(ApplicationState::WaitingForSignup);
return;
} else if (err != SignupRequest::Error::None) {
setApplicationStateError("Failed to sign up. Please try again.");
return;
}
setApplicationState(ApplicationState::RequestingLoginAfterSignup);
// After successfully signing up, attempt to login
auto loginRequest = new LoginRequest();
_lastUsedUsername = username;
_config.lastLogin = username;
connect(loginRequest, &LoginRequest::finished, this, [this, loginRequest]() {
ASSERT_STATE(ApplicationState::RequestingLoginAfterSignup);
loginRequest->deleteLater();
auto err = loginRequest->getError();
if (err == LoginRequest::Error::BadUsernameOrPassword) {
setLastLoginErrorMessage("Invalid username or password.");
setApplicationState(ApplicationState::WaitingForLogin);
return;
} else if (err != LoginRequest::Error::None) {
setApplicationStateError("Failed to login. Please try again.");
return;
}
_config.loggedIn = true;
_loginResponse = loginRequest->getToken();
_loginTokenResponse = loginRequest->getRawToken();
requestSettings();
});
setApplicationState(ApplicationState::RequestingLoginAfterSignup);
loginRequest->send(_networkAccessManager, _username, _password);
});
signupRequest->send(_networkAccessManager, email, username, password);
}
void LauncherState::login(QString username, QString password, QString displayName) {
ASSERT_STATE(ApplicationState::WaitingForLogin);
setApplicationState(ApplicationState::RequestingLogin);
_displayName = displayName;
auto request = new LoginRequest();
connect(request, &LoginRequest::finished, this, [this, request, username]() {
ASSERT_STATE(ApplicationState::RequestingLogin);
request->deleteLater();
auto err = request->getError();
if (err == LoginRequest::Error::BadUsernameOrPassword) {
setLastLoginErrorMessage("Invalid username or password");
setApplicationState(ApplicationState::WaitingForLogin);
return;
} else if (err != LoginRequest::Error::None) {
setApplicationStateError("Failed to login. Please try again.");
return;
}
_lastUsedUsername = username;
_config.lastLogin = username;
_config.loggedIn = true;
_loginResponse = request->getToken();
_loginTokenResponse = request->getRawToken();
requestSettings();
});
request->send(_networkAccessManager, username, password);
}
void LauncherState::requestSettings() {
// TODO Request settings if already logged in
qDebug() << "Requesting settings";
auto request = new UserSettingsRequest();
connect(request, &UserSettingsRequest::finished, this, [this, request]() {
auto userSettings = request->getUserSettings();
if (userSettings.homeLocation.isEmpty()) {
_config.homeLocation = "file:///~/serverless/tutorial.json";
_contentCacheURL = "";
} else {
_config.homeLocation = userSettings.homeLocation;
auto host = QUrl(_config.homeLocation).host();
_contentCacheURL = "http://orgs.highfidelity.com/host-content-cache/" + host + ".zip";
qDebug() << "Content cache url: " << _contentCacheURL;
}
qDebug() << "Home location is: " << _config.homeLocation;
qDebug() << "Content cache url is: " << _contentCacheURL;
downloadClient();
});
request->send(_networkAccessManager, _loginResponse);
}
void LauncherState::downloadClient() {
ASSERT_STATE({ ApplicationState::RequestingLogin, ApplicationState::RequestingLoginAfterSignup });
Build build;
if (!_latestBuilds.getBuild(_buildTag, &build)) {
qDebug() << "Cannot determine latest build";
setApplicationState(ApplicationState::UnexpectedError);
return;
}
if (QString::number(build.latestVersion) == _currentClientVersion) {
qDebug() << "Existing client install is up-to-date.";
downloadContentCache();
return;
}
_interfaceDownloadProgress = 0;
setApplicationState(ApplicationState::DownloadingClient);
// Start client download
{
qDebug() << "Latest build: " << build.tag << build.buildNumber << build.latestVersion << build.installerZipURL;
auto request = new QNetworkRequest(QUrl(build.installerZipURL));
auto reply = _networkAccessManager.get(*request);
QDir downloadDir{ PathUtils::getDownloadDirectory() };
_clientZipFile.setFileName(downloadDir.absoluteFilePath("client.zip"));
qDebug() << "Opening " << _clientZipFile.fileName();
if (!_clientZipFile.open(QIODevice::WriteOnly)) {
setApplicationState(ApplicationState::UnexpectedError);
return;
}
connect(reply, &QNetworkReply::finished, this, &LauncherState::clientDownloadComplete);
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() {
char buf[4096];
while (reply->bytesAvailable() > 0) {
qint64 size;
size = reply->read(buf, (qint64)sizeof(buf));
if (size == 0) {
break;
}
_clientZipFile.write(buf, size);
}
});
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64 total) {
_interfaceDownloadProgress = (float)received / (float)total;
emit downloadProgressChanged();
});
}
}
void LauncherState::launcherDownloadComplete() {
_launcherZipFile.close();
#ifdef Q_OS_MAC
installLauncher();
#elif defined(Q_OS_WIN)
launchAutoUpdater(_launcherZipFile.fileName());
#endif
}
void LauncherState::clientDownloadComplete() {
ASSERT_STATE(ApplicationState::DownloadingClient);
_clientZipFile.close();
installClient();
}
float LauncherState::calculateDownloadProgress() const{
if (shouldDownloadContentCache()) {
return (_interfaceDownloadProgress * 0.40f) + (_interfaceInstallProgress * 0.10f) +
(_contentInstallProgress * 0.40f) + (_contentDownloadProgress * 0.10f);
}
return (_interfaceDownloadProgress * 0.80f) + (_interfaceInstallProgress * 0.20f);
}
void LauncherState::installClient() {
ASSERT_STATE(ApplicationState::DownloadingClient);
setApplicationState(ApplicationState::InstallingClient);
auto clientDir = PathUtils::getClientDirectory();
auto clientPath = clientDir.absolutePath();
_launcherDirectory.rmpath(clientPath);
_launcherDirectory.mkpath(clientPath);
_interfaceInstallProgress = 0;
qDebug() << "Unzipping " << _clientZipFile.fileName() << " to " << clientDir.absolutePath();
auto unzipper = new Unzipper(_clientZipFile.fileName(), clientDir);
unzipper->setAutoDelete(true);
connect(unzipper, &Unzipper::progress, this, [this](float progress) {
_interfaceInstallProgress = progress;
emit downloadProgressChanged();
});
connect(unzipper, &Unzipper::finished, this, [this](bool error, QString errorMessage) {
if (error) {
qDebug() << "Unzipper finished with error: " << errorMessage;
setApplicationState(ApplicationState::UnexpectedError);
} else {
qDebug() << "Unzipper finished without error";
downloadContentCache();
}
});
QThreadPool::globalInstance()->start(unzipper);
}
void LauncherState::downloadLauncher() {
auto request = new QNetworkRequest(QUrl(_latestBuilds.launcherBuild.installerZipURL));
auto reply = _networkAccessManager.get(*request);
#ifdef Q_OS_MAC
_launcherZipFile.setFileName(_launcherDirectory.absoluteFilePath("launcher.zip"));
#elif defined(Q_OS_WIN)
_launcherZipFile.setFileName(_launcherDirectory.absoluteFilePath("launcher.exe"));
#endif
qDebug() << "opening " << _launcherZipFile.fileName();
if (!_launcherZipFile.open(QIODevice::WriteOnly)) {
setApplicationState(ApplicationState::UnexpectedError);
return;
}
connect(reply, &QNetworkReply::finished, this, &LauncherState::launcherDownloadComplete);
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() {
char buf[4096];
while (reply->bytesAvailable() > 0) {
qint64 size;
size = reply->read(buf, (qint64)sizeof(buf));
if (size == 0) {
break;
}
_launcherZipFile.write(buf, size);
}
});
}
void LauncherState::installLauncher() {
_launcherDirectory.rmpath("launcher_install");
_launcherDirectory.mkpath("launcher_install");
auto installDir = _launcherDirectory.absoluteFilePath("launcher_install");
qDebug() << "Uzipping " << _launcherZipFile.fileName() << " to " << installDir;
auto unzipper = new Unzipper(_launcherZipFile.fileName(), QDir(installDir));
unzipper->setAutoDelete(true);
connect(unzipper, &Unzipper::finished, this, [this](bool error, QString errorMessage) {
if (error) {
qDebug() << "Unzipper finished with error: " << errorMessage;
} else {
qDebug() << "Unzipper finished without error";
QDir installDirectory = _launcherDirectory.filePath("launcher_install");
QString launcherPath;
#if defined(Q_OS_WIN)
launcherPath = installDirectory.absoluteFilePath("HQ Launcher.exe");
#elif defined(Q_OS_MACOS)
launcherPath = installDirectory.absoluteFilePath("HQ Launcher.app");
#endif
::launchAutoUpdater(launcherPath);
}
});
QThreadPool::globalInstance()->start(unzipper);
}
void LauncherState::downloadContentCache() {
ASSERT_STATE({ ApplicationState::RequestingLogin, ApplicationState::InstallingClient });
// Start content set cache download
if (shouldDownloadContentCache()) {
setApplicationState(ApplicationState::DownloadingContentCache);
_contentDownloadProgress = 0;
qDebug() << "Downloading content cache from: " << _contentCacheURL;
QNetworkRequest request{ QUrl(_contentCacheURL) };
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
auto reply = _networkAccessManager.get(request);
QDir downloadDir{ PathUtils::getDownloadDirectory() };
_contentZipFile.setFileName(downloadDir.absoluteFilePath("content_cache.zip"));
qDebug() << "Opening " << _contentZipFile.fileName();
if (!_contentZipFile.open(QIODevice::WriteOnly)) {
setApplicationState(ApplicationState::UnexpectedError);
return;
}
connect(reply, &QNetworkReply::finished, this, &LauncherState::contentCacheDownloadComplete);
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64 total) {
_contentDownloadProgress = (float)received / (float)total;
emit downloadProgressChanged();
});
} else {
launchClient();
}
}
void LauncherState::contentCacheDownloadComplete() {
ASSERT_STATE(ApplicationState::DownloadingContentCache);
auto reply = static_cast<QNetworkReply*>(sender());
if (reply->error()) {
qDebug() << "Error downloading content cache: " << reply->error() << reply->readAll();
qDebug() << "Continuing to launch client";
_contentDownloadProgress = 100.0f;
_contentInstallProgress = 100.0f;
launchClient();
return;
}
char buf[4096];
while (reply->bytesAvailable() > 0) {
qint64 size;
size = reply->read(buf, (qint64)sizeof(buf));
_contentZipFile.write(buf, size);
}
_contentZipFile.close();
installContentCache();
}
void LauncherState::installContentCache() {
ASSERT_STATE(ApplicationState::DownloadingContentCache);
setApplicationState(ApplicationState::InstallingContentCache);
auto installDir = PathUtils::getContentCachePath();
qDebug() << "Unzipping " << _contentZipFile.fileName() << " to " << installDir;
_contentInstallProgress = 0;
auto unzipper = new Unzipper(_contentZipFile.fileName(), QDir(installDir));
unzipper->setAutoDelete(true);
connect(unzipper, &Unzipper::progress, this, [this](float progress) {
qDebug() << "Unzipper progress (content cache): " << progress;
_contentInstallProgress = progress;
emit downloadProgressChanged();
});
connect(unzipper, &Unzipper::finished, this, [this](bool error, QString errorMessage) {
if (error) {
qDebug() << "Unzipper finished with error: " << errorMessage;
setApplicationState(ApplicationState::UnexpectedError);
} else {
qDebug() << "Unzipper finished without error";
launchClient();
}
});
QThreadPool::globalInstance()->start(unzipper);
}
#include <QTimer>
#include <QCoreApplication>
void LauncherState::launchClient() {
ASSERT_STATE({
ApplicationState::RequestingLogin,
ApplicationState::InstallingClient,
ApplicationState::InstallingContentCache
});
setApplicationState(ApplicationState::LaunchingHighFidelity);
QDir installDirectory = PathUtils::getClientDirectory();
QString clientPath = PathUtils::getClientExecutablePath();
auto path = PathUtils::getConfigFilePath();
QFile configFile{ path };
if (configFile.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
QJsonDocument doc = QJsonDocument::fromJson(configFile.readAll());
doc.setObject({
{ configHomeLocationKey, _config.homeLocation },
{ configLastLoginKey, _config.lastLogin },
{ configLoggedInKey, _config.loggedIn },
{ configLauncherPathKey, PathUtils::getLauncherFilePath() },
});
qint64 result = configFile.write(doc.toJson());
configFile.close();
qDebug() << "Wrote data to config data: " << result;
} else {
qDebug() << "Failed to open config file";
}
QString defaultScriptsPath;
#if defined(Q_OS_WIN)
defaultScriptsPath = installDirectory.filePath("scripts/simplifiedUIBootstrapper.js");
#elif defined(Q_OS_MACOS)
defaultScriptsPath = installDirectory.filePath("interface.app/Contents/Resources/scripts/simplifiedUIBootstrapper.js");
#endif
QString contentCachePath = _launcherDirectory.filePath("cache");
::launchClient(clientPath, _config.homeLocation, defaultScriptsPath, _displayName, contentCachePath, _loginTokenResponse);
QTimer::singleShot(3000, QCoreApplication::instance(), &QCoreApplication::quit);
}
void LauncherState::setApplicationStateError(QString errorMessage) {
_applicationErrorMessage = errorMessage;
setApplicationState(ApplicationState::UnexpectedError);
}
void LauncherState::setApplicationState(ApplicationState state) {
qDebug() << "Changing application state: " << _applicationState << " -> " << state;
if (state == ApplicationState::UnexpectedError) {
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
}
_applicationState = state;
UIState updatedUIState = getUIState();
if (_uiState != updatedUIState) {
emit uiStateChanged();
emit updateSourceUrl(PathUtils::resourcePath(getCurrentUISource()));
_uiState = getUIState();
}
emit applicationStateChanged();
}
LauncherState::ApplicationState LauncherState::getApplicationState() const {
return _applicationState;
}

View file

@ -0,0 +1,194 @@
#pragma once
#include <QDir>
#include <QObject>
#include <QString>
#include <QUrl>
#include <QNetworkAccessManager>
#include <QFile>
#include "LoginRequest.h"
#include "SignupRequest.h"
#include "UserSettingsRequest.h"
#include "BuildsRequest.h"
struct LauncherConfig {
QString lastLogin{ "" };
QString launcherPath{ "" };
bool loggedIn{ false };
QString homeLocation{ "" };
};
class LauncherState : public QObject {
Q_OBJECT
Q_PROPERTY(UIState uiState READ getUIState NOTIFY uiStateChanged)
Q_PROPERTY(ApplicationState applicationState READ getApplicationState NOTIFY applicationStateChanged)
Q_PROPERTY(float downloadProgress READ getDownloadProgress NOTIFY downloadProgressChanged)
Q_PROPERTY(QString lastLoginErrorMessage READ getLastLoginErrorMessage NOTIFY lastLoginErrorMessageChanged)
Q_PROPERTY(QString lastSignupErrorMessage READ getLastSignupErrorMessage NOTIFY lastSignupErrorMessageChanged)
Q_PROPERTY(QString buildVersion READ getBuildVersion)
Q_PROPERTY(QString lastUsedUsername READ getLastUsedUsername)
public:
LauncherState();
~LauncherState() = default;
enum UIState {
SplashScreen = 0,
SignupScreen,
LoginScreen,
DownloadScreen,
DownloadFinishedScreen,
ErrorScreen,
UI_STATE_NUM
};
enum class ApplicationState {
Init,
UnexpectedError,
RequestingBuilds,
GettingCurrentClientVersion,
WaitingForLogin,
RequestingLogin,
WaitingForSignup,
RequestingSignup,
RequestingLoginAfterSignup,
DownloadingClient,
DownloadingLauncher,
DownloadingContentCache,
InstallingClient,
InstallingLauncher,
InstallingContentCache,
LaunchingHighFidelity,
};
Q_ENUM(ApplicationState)
bool _isDebuggingScreens{ false };
UIState _currentDebugScreen{ UIState::SplashScreen };
void toggleDebugState();
void gotoNextDebugScreen();
void gotoPreviousDebugScreen();
Q_INVOKABLE QString getCurrentUISource() const;
void ASSERT_STATE(ApplicationState state);
void ASSERT_STATE(const std::vector<ApplicationState>& states);
static void declareQML();
UIState getUIState() const;
void setLastLoginErrorMessage(const QString& msg);
QString getLastLoginErrorMessage() const { return _lastLoginErrorMessage; }
void setLastSignupErrorMessage(const QString& msg);
QString getLastSignupErrorMessage() const { return _lastSignupErrorMessage; }
QString getBuildVersion();
QString getLastUsedUsername() const { return _lastUsedUsername; }
void setApplicationStateError(QString errorMessage);
void setApplicationState(ApplicationState state);
ApplicationState getApplicationState() const;
Q_INVOKABLE void gotoSignup();
Q_INVOKABLE void gotoLogin();
// Request builds
void requestBuilds();
// Signup
Q_INVOKABLE void signup(QString email, QString username, QString password, QString displayName);
// Login
Q_INVOKABLE void login(QString username, QString password, QString displayName);
// Request Settings
void requestSettings();
Q_INVOKABLE void restart();
// Launcher
void downloadLauncher();
void installLauncher();
// Client
void downloadClient();
void installClient();
// Content Cache
void downloadContentCache();
void installContentCache();
// Launching
void launchClient();
Q_INVOKABLE float getDownloadProgress() const { return calculateDownloadProgress(); }
Q_INVOKABLE void openURLInBrowser(QString url);
signals:
void updateSourceUrl(QUrl sourceUrl);
void uiStateChanged();
void applicationStateChanged();
void downloadProgressChanged();
void lastSignupErrorChanged();
void lastSignupErrorMessageChanged();
void lastLoginErrorMessageChanged();
private slots:
void clientDownloadComplete();
void contentCacheDownloadComplete();
void launcherDownloadComplete();
private:
bool shouldDownloadContentCache() const;
void getCurrentClientVersion();
float calculateDownloadProgress() const;
bool shouldDownloadLauncher();
QNetworkAccessManager _networkAccessManager;
Builds _latestBuilds;
QDir _launcherDirectory;
LauncherConfig _config;
// Application State
ApplicationState _applicationState { ApplicationState::Init };
UIState _uiState { UIState::SplashScreen };
LoginToken _loginResponse;
SignupRequest::Error _lastSignupError{ SignupRequest::Error::None };
QString _lastLoginErrorMessage{ "" };
QString _lastSignupErrorMessage{ "" };
QString _lastUsedUsername;
QString _displayName;
QString _applicationErrorMessage;
QString _currentClientVersion;
QString _buildTag { QString::null };
QString _contentCacheURL;
QString _loginTokenResponse;
QFile _clientZipFile;
QFile _launcherZipFile;
QFile _contentZipFile;
QString _username;
QString _password;
float _downloadProgress { 0 };
float _contentDownloadProgress { 0 };
float _contentInstallProgress { 0 };
float _interfaceDownloadProgress { 0 };
float _interfaceInstallProgress { 0 };
};

View file

@ -0,0 +1,73 @@
#include "LauncherWindow.h"
#include <iostream>
#include <QCursor>
#ifdef Q_OS_WIN
#include <shellapi.h>
#include <propsys.h>
#include <propkey.h>
#endif
LauncherWindow::LauncherWindow() {
#ifdef Q_OS_WIN
// On Windows, disable pinning of the launcher.
IPropertyStore* pps;
HWND id = (HWND)this->winId();
if (id == NULL) {
qDebug() << "Failed to disable pinning, window id is null";
} else {
HRESULT hr = SHGetPropertyStoreForWindow(id, IID_PPV_ARGS(&pps));
if (SUCCEEDED(hr)) {
PROPVARIANT var;
var.vt = VT_BOOL;
var.boolVal = VARIANT_TRUE;
hr = pps->SetValue(PKEY_AppUserModel_PreventPinning, var);
pps->Release();
}
}
#endif
}
void LauncherWindow::keyPressEvent(QKeyEvent* event) {
QQuickView::keyPressEvent(event);
if (!event->isAccepted()) {
if (event->key() == Qt::Key_Escape) {
exit(0);
} else if (event->key() == Qt::Key_Up) {
_launcherState->toggleDebugState();
} else if (event->key() == Qt::Key_Left) {
_launcherState->gotoPreviousDebugScreen();
} else if (event->key() == Qt::Key_Right) {
_launcherState->gotoNextDebugScreen();
}
}
}
void LauncherWindow::mousePressEvent(QMouseEvent* event) {
QQuickView::mousePressEvent(event);
if (!event->isAccepted()) {
if (event->button() == Qt::LeftButton) {
_drag = true;
_previousMousePos = QCursor::pos();
}
}
}
void LauncherWindow::mouseReleaseEvent(QMouseEvent* event) {
QQuickView::mouseReleaseEvent(event);
_drag = false;
}
void LauncherWindow::mouseMoveEvent(QMouseEvent* event) {
QQuickView::mouseMoveEvent(event);
if (!event->isAccepted()) {
if (_drag) {
QPoint cursorPos = QCursor::pos();
QPoint offset = _previousMousePos - cursorPos;
_previousMousePos = cursorPos;
setPosition(position() - offset);
}
}
}

Some files were not shown because too many files have changed in this diff Show more