diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 35a8f65c06..9245c58760 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -11,6 +11,9 @@ #include "Application.h" +#include +#include + #include #include #include @@ -144,6 +147,7 @@ #include "InterfaceLogging.h" #include "LODManager.h" #include "ModelPackager.h" +#include "networking/CloseEventSender.h" #include "networking/HFWebEngineProfile.h" #include "networking/HFTabletWebEngineProfile.h" #include "networking/FileTypeProfile.h" @@ -534,6 +538,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -1570,6 +1575,14 @@ void Application::aboutToQuit() { getActiveDisplayPlugin()->deactivate(); + // use the CloseEventSender via a QThread to send an event that says the user asked for the app to close + auto closeEventSender = DependencyManager::get(); + QThread* closureEventThread = new QThread(this); + closeEventSender->moveToThread(closureEventThread); + // sendQuitEventAsync will bail immediately if the UserActivityLogger is not enabled + connect(closureEventThread, &QThread::started, closeEventSender.data(), &CloseEventSender::sendQuitEventAsync); + closureEventThread->start(); + // Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown. DependencyManager::get()->hide("RunningScripts"); @@ -1738,6 +1751,15 @@ Application::~Application() { _window->deleteLater(); + // make sure that the quit event has finished sending before we take the application down + auto closeEventSender = DependencyManager::get(); + while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) { + // sleep a little so we're not spinning at 100% + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + // quit the thread used by the closure event sender + closeEventSender->thread()->quit(); + // Can't log to file passed this point, FileLogger about to be deleted qInstallMessageHandler(LogHandler::verboseMessageHandler); } @@ -2388,15 +2410,16 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { // Check HMD use (may be technically available without being in use) bool hasHMD = PluginUtils::isHMDAvailable(); - bool isUsingHMD = hasHMD && hasHandControllers && _displayPlugin->isHmd(); + bool isUsingHMD = _displayPlugin->isHmd(); + bool isUsingHMDAndHandControllers = hasHMD && hasHandControllers && isUsingHMD; Setting::Handle tutorialComplete{ "tutorialComplete", false }; Setting::Handle firstRun{ Settings::firstRun, true }; bool isTutorialComplete = tutorialComplete.get(); - bool shouldGoToTutorial = isUsingHMD && hasTutorialContent && !isTutorialComplete; + bool shouldGoToTutorial = isUsingHMDAndHandControllers && hasTutorialContent && !isTutorialComplete; - qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMD; + qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers; qCDebug(interfaceapp) << "Tutorial version:" << contentVersion << ", sufficient:" << hasTutorialContent << ", complete:" << isTutorialComplete << ", should go:" << shouldGoToTutorial; @@ -2410,10 +2433,18 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { const QString TUTORIAL_PATH = "/tutorial_begin"; + static const QString SENT_TO_TUTORIAL = "tutorial"; + static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; + static const QString SENT_TO_ENTRY = "entry"; + static const QString SENT_TO_SANDBOX = "sandbox"; + + QString sentTo; + if (shouldGoToTutorial) { if (sandboxIsRunning) { qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; DependencyManager::get()->goToLocalSandbox(TUTORIAL_PATH); + sentTo = SENT_TO_TUTORIAL; } else { qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; if (firstRun.get()) { @@ -2421,8 +2452,10 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { } if (addressLookupString.isEmpty()) { DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; } else { DependencyManager::get()->loadSettings(addressLookupString); + sentTo = SENT_TO_PREVIOUS_LOCATION; } } } else { @@ -2435,23 +2468,40 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { // If this is a first run we short-circuit the address passed in if (isFirstRun) { - if (isUsingHMD) { + if (isUsingHMDAndHandControllers) { if (sandboxIsRunning) { qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; DependencyManager::get()->goToLocalSandbox(); + sentTo = SENT_TO_SANDBOX; } else { qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry."; DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; } } else { DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; } } else { qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString); DependencyManager::get()->loadSettings(addressLookupString); + sentTo = SENT_TO_PREVIOUS_LOCATION; } } + UserActivityLogger::getInstance().logAction("startup_sent_to", { + { "sent_to", sentTo }, + { "sandbox_is_running", sandboxIsRunning }, + { "has_hmd", hasHMD }, + { "has_hand_controllers", hasHandControllers }, + { "is_using_hmd", isUsingHMD }, + { "is_using_hmd_and_hand_controllers", isUsingHMDAndHandControllers }, + { "content_version", contentVersion }, + { "is_tutorial_complete", isTutorialComplete }, + { "has_tutorial_content", hasTutorialContent }, + { "should_go_to_tutorial", shouldGoToTutorial } + }); + _connectionMonitor.init(); // After all of the constructor is completed, then set firstRun to false. diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 4138798484..463069430d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -407,6 +407,12 @@ Menu::Menu() { #endif + { + auto action = addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::RenderClearKtxCache); + connect(action, &QAction::triggered, []{ + Setting::Handle(KTXCache::SETTING_VERSION_NAME, KTXCache::INVALID_VERSION).set(KTXCache::INVALID_VERSION); + }); + } // Developer > Render > LOD Tools addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, 0, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index b6d72f5446..41bf89a20a 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -145,6 +145,7 @@ namespace MenuOption { const QString Quit = "Quit"; const QString ReloadAllScripts = "Reload All Scripts"; const QString ReloadContent = "Reload Content (Clears all caches)"; + const QString RenderClearKtxCache = "Clear KTX Cache (requires restart)"; const QString RenderMaxTextureMemory = "Maximum Texture Memory"; const QString RenderMaxTextureAutomatic = "Automatic Texture Memory"; const QString RenderMaxTexture4MB = "4 MB"; diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 49517eb38e..63738d2d91 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -24,7 +24,6 @@ #include #include - #include "AddressManager.h" #include "Application.h" #include "InterfaceLogging.h" diff --git a/interface/src/networking/CloseEventSender.cpp b/interface/src/networking/CloseEventSender.cpp new file mode 100644 index 0000000000..8c3d6ae888 --- /dev/null +++ b/interface/src/networking/CloseEventSender.cpp @@ -0,0 +1,90 @@ +// +// CloseEventSender.cpp +// interface/src/networking +// +// Created by Stephen Birarda on 5/31/17. +// Copyright 2017 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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "CloseEventSender.h" + +QNetworkRequest createNetworkRequest() { + + QNetworkRequest request; + + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + requestURL.setPath(USER_ACTIVITY_URL); + + request.setUrl(requestURL); + + auto accountManager = DependencyManager::get(); + + if (accountManager->hasValidAccessToken()) { + request.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, + accountManager->getAccountInfo().getAccessToken().authorizationHeaderValue()); + } + + request.setRawHeader(METAVERSE_SESSION_ID_HEADER, + uuidStringWithoutCurlyBraces(accountManager->getSessionID()).toLocal8Bit()); + + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + request.setPriority(QNetworkRequest::HighPriority); + + return request; +} + +QByteArray postDataForAction(QString action) { + return QString("{\"action_name\": \"" + action + "\"}").toUtf8(); +} + +QNetworkReply* replyForAction(QString action) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + return networkAccessManager.post(createNetworkRequest(), postDataForAction(action)); +} + +void CloseEventSender::sendQuitEventAsync() { + if (UserActivityLogger::getInstance().isEnabled()) { + QNetworkReply* reply = replyForAction("quit"); + connect(reply, &QNetworkReply::finished, this, &CloseEventSender::handleQuitEventFinished); + _quitEventStartTimestamp = QDateTime::currentMSecsSinceEpoch(); + } else { + _hasFinishedQuitEvent = true; + } +} + +void CloseEventSender::handleQuitEventFinished() { + _hasFinishedQuitEvent = true; + + auto reply = qobject_cast(sender()); + if (reply->error() == QNetworkReply::NoError) { + qCDebug(networking) << "Quit event sent successfully"; + } else { + qCDebug(networking) << "Failed to send quit event -" << reply->errorString(); + } + + reply->deleteLater(); +} + +bool CloseEventSender::hasTimedOutQuitEvent() { + const int CLOSURE_EVENT_TIMEOUT_MS = 5000; + return _quitEventStartTimestamp != 0 + && QDateTime::currentMSecsSinceEpoch() - _quitEventStartTimestamp > CLOSURE_EVENT_TIMEOUT_MS; +} + + diff --git a/interface/src/networking/CloseEventSender.h b/interface/src/networking/CloseEventSender.h new file mode 100644 index 0000000000..05e6f81ad4 --- /dev/null +++ b/interface/src/networking/CloseEventSender.h @@ -0,0 +1,41 @@ +// +// CloseEventSender.h +// interface/src/networking +// +// Created by Stephen Birarda on 5/31/17. +// Copyright 2017 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_CloseEventSender_h +#define hifi_CloseEventSender_h + +#include + +#include +#include + +#include + +class CloseEventSender : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + bool hasTimedOutQuitEvent(); + bool hasFinishedQuitEvent() { return _hasFinishedQuitEvent; } + +public slots: + void sendQuitEventAsync(); + +private slots: + void handleQuitEventFinished(); + +private: + std::atomic _hasFinishedQuitEvent { false }; + std::atomic _quitEventStartTimestamp; +}; + +#endif // hifi_CloseEventSender_h diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/model-networking/src/model-networking/KTXCache.cpp index e0447af8e6..6443748920 100644 --- a/libraries/model-networking/src/model-networking/KTXCache.cpp +++ b/libraries/model-networking/src/model-networking/KTXCache.cpp @@ -11,14 +11,28 @@ #include "KTXCache.h" +#include #include using File = cache::File; using FilePointer = cache::FilePointer; +// Whenever a change is made to the serialized format for the KTX cache that isn't backward compatible, +// this value should be incremented. This will force the KTX cache to be wiped +const int KTXCache::CURRENT_VERSION = 0x01; +const int KTXCache::INVALID_VERSION = 0x00; +const char* KTXCache::SETTING_VERSION_NAME = "hifi.ktx.cache_version"; + KTXCache::KTXCache(const std::string& dir, const std::string& ext) : FileCache(dir, ext) { initialize(); + + Setting::Handle cacheVersionHandle(SETTING_VERSION_NAME, INVALID_VERSION); + auto cacheVersion = cacheVersionHandle.get(); + if (cacheVersion != CURRENT_VERSION) { + wipe(); + cacheVersionHandle.set(CURRENT_VERSION); + } } KTXFilePointer KTXCache::writeFile(const char* data, Metadata&& metadata) { diff --git a/libraries/model-networking/src/model-networking/KTXCache.h b/libraries/model-networking/src/model-networking/KTXCache.h index bbf7ceadea..5617019c52 100644 --- a/libraries/model-networking/src/model-networking/KTXCache.h +++ b/libraries/model-networking/src/model-networking/KTXCache.h @@ -27,6 +27,12 @@ class KTXCache : public cache::FileCache { Q_OBJECT public: + // Whenever a change is made to the serialized format for the KTX cache that isn't backward compatible, + // this value should be incremented. This will force the KTX cache to be wiped + static const int CURRENT_VERSION; + static const int INVALID_VERSION; + static const char* SETTING_VERSION_NAME; + KTXCache(const std::string& dir, const std::string& ext); KTXFilePointer writeFile(const char* data, Metadata&& metadata); diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index c6fffbfdbd..fa6b49597d 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -45,7 +45,6 @@ Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; -static const auto METAVERSE_SESSION_ID_HEADER = QString("HFM-SessionID").toLocal8Bit(); JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, const QString& jsonCallbackMethod, QObject* errorCallbackReceiver, const QString& errorCallbackMethod, diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 9a456ca7e8..b37846ec1b 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -52,6 +52,7 @@ namespace AccountManagerAuth { Q_DECLARE_METATYPE(AccountManagerAuth::Type); const QByteArray ACCESS_TOKEN_AUTHORIZATION_HEADER = "Authorization"; +const auto METAVERSE_SESSION_ID_HEADER = QString("HFM-SessionID").toLocal8Bit(); using UserAgentGetter = std::function; diff --git a/libraries/networking/src/FileCache.cpp b/libraries/networking/src/FileCache.cpp index 6cb8cd8f7c..95304e3866 100644 --- a/libraries/networking/src/FileCache.cpp +++ b/libraries/networking/src/FileCache.cpp @@ -236,6 +236,28 @@ namespace cache { }; } +void FileCache::eject(const FilePointer& file) { + file->_cache = nullptr; + const auto& length = file->getLength(); + const auto& key = file->getKey(); + + { + Lock lock(_filesMutex); + if (0 != _files.erase(key)) { + _numTotalFiles -= 1; + _totalFilesSize -= length; + } + } + + { + Lock unusedLock(_unusedFilesMutex); + if (0 != _unusedFiles.erase(file)) { + _numUnusedFiles -= 1; + _unusedFilesSize -= length; + } + } +} + void FileCache::clean() { size_t overbudgetAmount = getOverbudgetAmount(); @@ -250,28 +272,23 @@ void FileCache::clean() { for (const auto& file : _unusedFiles) { queue.push(file); } + while (!queue.empty() && overbudgetAmount > 0) { auto file = queue.top(); queue.pop(); + eject(file); auto length = file->getLength(); - - unusedLock.unlock(); - { - file->_cache = nullptr; - Lock lock(_filesMutex); - _files.erase(file->getKey()); - } - unusedLock.lock(); - - _unusedFiles.erase(file); - _numTotalFiles -= 1; - _numUnusedFiles -= 1; - _totalFilesSize -= length; - _unusedFilesSize -= length; overbudgetAmount -= std::min(length, overbudgetAmount); } } +void FileCache::wipe() { + Lock unusedFilesLock(_unusedFilesMutex); + while (!_unusedFiles.empty()) { + eject(*_unusedFiles.begin()); + } +} + void FileCache::clear() { // Eliminate any overbudget files clean(); diff --git a/libraries/networking/src/FileCache.h b/libraries/networking/src/FileCache.h index 040e1ab592..f29d75f779 100644 --- a/libraries/networking/src/FileCache.h +++ b/libraries/networking/src/FileCache.h @@ -46,6 +46,9 @@ public: FileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr); virtual ~FileCache(); + // Remove all unlocked items from the cache + void wipe(); + size_t getNumTotalFiles() const { return _numTotalFiles; } size_t getNumCachedFiles() const { return _numUnusedFiles; } size_t getSizeTotalFiles() const { return _totalFilesSize; } @@ -95,6 +98,9 @@ public: private: using Mutex = std::recursive_mutex; using Lock = std::unique_lock; + using Map = std::unordered_map>; + using Set = std::unordered_set; + using KeySet = std::unordered_set; friend class File; @@ -105,6 +111,8 @@ private: void removeUnusedFile(const FilePointer& file); void clean(); void clear(); + // Remove a file from the cache + void eject(const FilePointer& file); size_t getOverbudgetAmount() const; @@ -122,10 +130,10 @@ private: std::string _dirpath; bool _initialized { false }; - std::unordered_map> _files; + Map _files; Mutex _filesMutex; - std::unordered_set _unusedFiles; + Set _unusedFiles; Mutex _unusedFilesMutex; }; @@ -136,8 +144,8 @@ public: using Key = FileCache::Key; using Metadata = FileCache::Metadata; - Key getKey() const { return _key; } - size_t getLength() const { return _length; } + const Key& getKey() const { return _key; } + const size_t& getLength() const { return _length; } std::string getFilepath() const { return _filepath; } virtual ~File(); diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index e2dd110cfd..28117c0933 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -20,8 +20,6 @@ #include #include "AddressManager.h" -static const QString USER_ACTIVITY_URL = "/api/v1/user_activities"; - UserActivityLogger& UserActivityLogger::getInstance() { static UserActivityLogger sharedInstance; return sharedInstance; diff --git a/libraries/networking/src/UserActivityLogger.h b/libraries/networking/src/UserActivityLogger.h index b41960a8ad..9fad498b86 100644 --- a/libraries/networking/src/UserActivityLogger.h +++ b/libraries/networking/src/UserActivityLogger.h @@ -22,6 +22,8 @@ #include #include "AddressManager.h" +const QString USER_ACTIVITY_URL = "/api/v1/user_activities"; + class UserActivityLogger : public QObject { Q_OBJECT diff --git a/tests/networking/src/FileCacheTests.cpp b/tests/networking/src/FileCacheTests.cpp index 0813d05a54..79fe9dee54 100644 --- a/tests/networking/src/FileCacheTests.cpp +++ b/tests/networking/src/FileCacheTests.cpp @@ -113,18 +113,21 @@ void FileCacheTests::testUnusedFiles() { QVERIFY(!file.get()); } - QThread::msleep(1000); // Test files 90 to 99 are present for (int i = 90; i < 100; ++i) { std::string key = getFileKey(i); auto file = cache->getFile(key); QVERIFY(file.get()); inUseFiles.push_back(file); - // Each access touches the file, so we need to sleep here to ensure that the files are - // spaced out in numeric order, otherwise later tests can't reliably determine the order - // for cache ejection - QThread::msleep(1000); + + if (i == 94) { + // Each access touches the file, so we need to sleep here to ensure that the the last 5 files + // have later times for cache ejection priority, otherwise the test runs too fast to reliably + // differentiate + QThread::msleep(1000); + } } + QCOMPARE(cache->getNumCachedFiles(), (size_t)0); QCOMPARE(cache->getNumTotalFiles(), (size_t)10); inUseFiles.clear(); @@ -165,6 +168,20 @@ void FileCacheTests::testFreeSpacePreservation() { } } +void FileCacheTests::testWipe() { + // Reset the cache + auto cache = makeFileCache(_testDir.path()); + QCOMPARE(cache->getNumCachedFiles(), (size_t)5); + QCOMPARE(cache->getNumTotalFiles(), (size_t)5); + cache->wipe(); + QCOMPARE(cache->getNumCachedFiles(), (size_t)0); + QCOMPARE(cache->getNumTotalFiles(), (size_t)0); + QVERIFY(getCacheDirectorySize() > 0); + forceDeletes(); + QCOMPARE(getCacheDirectorySize(), (size_t)0); +} + + void FileCacheTests::cleanupTestCase() { } diff --git a/tests/networking/src/FileCacheTests.h b/tests/networking/src/FileCacheTests.h index 838c15afb8..b34b384855 100644 --- a/tests/networking/src/FileCacheTests.h +++ b/tests/networking/src/FileCacheTests.h @@ -20,6 +20,7 @@ private slots: void testUnusedFiles(); void testFreeSpacePreservation(); void cleanupTestCase(); + void testWipe(); private: size_t getFreeSpace() const; diff --git a/unpublishedScripts/marketplace/stopwatch/models/transparent-box.fbx b/unpublishedScripts/marketplace/stopwatch/models/transparent-box.fbx new file mode 100644 index 0000000000..b1df7d962c Binary files /dev/null and b/unpublishedScripts/marketplace/stopwatch/models/transparent-box.fbx differ diff --git a/unpublishedScripts/marketplace/stopwatch/spawnStopwatch.js b/unpublishedScripts/marketplace/stopwatch/spawnStopwatch.js index e72f949163..3a0a8a506b 100644 --- a/unpublishedScripts/marketplace/stopwatch/spawnStopwatch.js +++ b/unpublishedScripts/marketplace/stopwatch/spawnStopwatch.js @@ -43,13 +43,47 @@ var minuteHandID = Entities.addEntity({ modelURL: Script.resolvePath("models/Stopwatch-min-hand.fbx"), }); +var startStopButtonID = Entities.addEntity({ + type: "Model", + name: "stopwatch/startStop", + parentID: stopwatchID, + dimensions: Vec3.multiply(scale, { x: 0.8, y: 0.8, z: 1.0 }), + localPosition: Vec3.multiply(scale, { x: 0, y: -0.1, z: -2.06 }), + modelURL: Script.resolvePath("models/transparent-box.fbx") +}); + +var resetButtonID = Entities.addEntity({ + type: "Model", + name: "stopwatch/startStop", + parentID: stopwatchID, + dimensions: Vec3.multiply(scale, { x: 0.6, y: 0.6, z: 0.8 }), + localPosition: Vec3.multiply(scale, { x: -1.5, y: -0.1, z: -1.2 }), + localRotation: Quat.fromVec3Degrees({ x: 0, y: 36, z: 0 }), + modelURL: Script.resolvePath("models/transparent-box.fbx") +}); + Entities.editEntity(stopwatchID, { userData: JSON.stringify({ secondHandID: secondHandID, - minuteHandID: minuteHandID, + minuteHandID: minuteHandID }), - script: Script.resolvePath("stopwatchClient.js"), serverScripts: Script.resolvePath("stopwatchServer.js") }); +Entities.editEntity(startStopButtonID, { + userData: JSON.stringify({ + stopwatchID: stopwatchID, + grabbableKey: { wantsTrigger: true } + }), + script: Script.resolvePath("stopwatchStartStop.js") +}); + +Entities.editEntity(resetButtonID, { + userData: JSON.stringify({ + stopwatchID: stopwatchID, + grabbableKey: { wantsTrigger: true } + }), + script: Script.resolvePath("stopwatchReset.js") +}); + Script.stop() diff --git a/unpublishedScripts/marketplace/stopwatch/stopwatchReset.js b/unpublishedScripts/marketplace/stopwatch/stopwatchReset.js new file mode 100644 index 0000000000..b65c1e7340 --- /dev/null +++ b/unpublishedScripts/marketplace/stopwatch/stopwatchReset.js @@ -0,0 +1,22 @@ +// +// stopwatchReset.js +// +// Created by David Rowe on 26 May 2017. +// Copyright 2017 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 +// + +(function () { + this.preload = function (entityID) { + var properties = Entities.getEntityProperties(entityID, "userData"); + this.messageChannel = "STOPWATCH-" + JSON.parse(properties.userData).stopwatchID; + }; + function click() { + Messages.sendMessage(this.messageChannel, "reset"); + } + this.startNearTrigger = click; + this.startFarTrigger = click; + this.clickDownOnEntity = click; +}); diff --git a/unpublishedScripts/marketplace/stopwatch/stopwatchServer.js b/unpublishedScripts/marketplace/stopwatch/stopwatchServer.js index 925db565c3..6ae1b69087 100644 --- a/unpublishedScripts/marketplace/stopwatch/stopwatchServer.js +++ b/unpublishedScripts/marketplace/stopwatch/stopwatchServer.js @@ -13,6 +13,7 @@ self.equipped = false; self.isActive = false; + self.seconds = 0; self.secondHandID = null; self.minuteHandID = null; @@ -46,11 +47,19 @@ }; self.messageReceived = function(channel, message, sender) { print("Message received", channel, sender, message); - if (channel === self.messageChannel && message === 'click') { - if (self.isActive) { - self.resetTimer(); - } else { - self.startTimer(); + if (channel === self.messageChannel) { + switch (message) { + case "startStop": + if (self.isActive) { + self.stopTimer(); + } else { + self.startTimer(); + } + break; + case "reset": + self.stopTimer(); + self.resetTimer(); + break; } } }; @@ -58,14 +67,7 @@ return Entities.getEntityProperties(self.entityID, "position").position; }; self.resetTimer = function() { - print("Stopping stopwatch"); - if (self.tickInjector) { - self.tickInjector.stop(); - } - if (self.tickIntervalID !== null) { - Script.clearInterval(self.tickIntervalID); - self.tickIntervalID = null; - } + print("Resetting stopwatch"); Entities.editEntity(self.secondHandID, { localRotation: Quat.fromPitchYawRollDegrees(0, 0, 0), angularVelocity: { x: 0, y: 0, z: 0 }, @@ -74,7 +76,7 @@ localRotation: Quat.fromPitchYawRollDegrees(0, 0, 0), angularVelocity: { x: 0, y: 0, z: 0 }, }); - self.isActive = false; + self.seconds = 0; }; self.startTimer = function() { print("Starting stopwatch"); @@ -88,7 +90,6 @@ self.tickInjector.restart(); } - var seconds = 0; self.tickIntervalID = Script.setInterval(function() { if (self.tickInjector) { self.tickInjector.setOptions({ @@ -97,15 +98,15 @@ loop: true }); } - seconds++; + self.seconds++; const degreesPerTick = -360 / 60; Entities.editEntity(self.secondHandID, { - localRotation: Quat.fromPitchYawRollDegrees(0, seconds * degreesPerTick, 0), + localRotation: Quat.fromPitchYawRollDegrees(0, self.seconds * degreesPerTick, 0), }); - if (seconds % 60 == 0) { + if (self.seconds % 60 == 0) { Entities.editEntity(self.minuteHandID, { - localRotation: Quat.fromPitchYawRollDegrees(0, (seconds / 60) * degreesPerTick, 0), + localRotation: Quat.fromPitchYawRollDegrees(0, (self.seconds / 60) * degreesPerTick, 0), }); Audio.playSound(self.chimeSound, { position: self.getStopwatchPosition(), @@ -117,4 +118,15 @@ self.isActive = true; }; + self.stopTimer = function () { + print("Stopping stopwatch"); + if (self.tickInjector) { + self.tickInjector.stop(); + } + if (self.tickIntervalID !== null) { + Script.clearInterval(self.tickIntervalID); + self.tickIntervalID = null; + } + self.isActive = false; + }; }); diff --git a/unpublishedScripts/marketplace/stopwatch/stopwatchClient.js b/unpublishedScripts/marketplace/stopwatch/stopwatchStartStop.js similarity index 50% rename from unpublishedScripts/marketplace/stopwatch/stopwatchClient.js rename to unpublishedScripts/marketplace/stopwatch/stopwatchStartStop.js index 6284b86102..88c037ee36 100644 --- a/unpublishedScripts/marketplace/stopwatch/stopwatchClient.js +++ b/unpublishedScripts/marketplace/stopwatch/stopwatchStartStop.js @@ -1,20 +1,21 @@ // -// stopwatchServer.js +// stopwatchStartStop.js // -// Created by Ryan Huffman on 1/20/17. +// Created by David Rowe on 26 May 2017. // Copyright 2017 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 // -(function() { +(function () { var messageChannel; - this.preload = function(entityID) { - this.messageChannel = "STOPWATCH-" + entityID; + this.preload = function (entityID) { + var properties = Entities.getEntityProperties(entityID, "userData"); + this.messageChannel = "STOPWATCH-" + JSON.parse(properties.userData).stopwatchID; }; function click() { - Messages.sendMessage(this.messageChannel, 'click'); + Messages.sendMessage(this.messageChannel, "startStop"); } this.startNearTrigger = click; this.startFarTrigger = click;