diff --git a/.gitattributes b/.gitattributes index 4efc7667f9..406780d20a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -25,3 +25,4 @@ *.svg binary *.ttf binary *.wav binary +*.rsrc binary diff --git a/.gitignore b/.gitignore index 0a2bcc6689..4a1c1c227b 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,7 @@ interface/external/*/* # Ignore interfaceCache for Linux users interface/interfaceCache/ -# ignore audio-client externals +# ignore audio-client externals libraries/audio-client/external/*/* !libraries/audio-client/external/*/readme.txt @@ -46,3 +46,7 @@ gvr-interface/libs/* # ignore files for various dev environments TAGS *.swp + +# ignore node files for the console +node_modules +npm-debug.log diff --git a/CMakeLists.txt b/CMakeLists.txt index 0da8dc7309..0922779bc6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ endif () if (POLICY CMP0043) cmake_policy(SET CMP0043 OLD) -endif () +endif () if (POLICY CMP0042) cmake_policy(SET CMP0042 OLD) @@ -26,24 +26,32 @@ set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "CMakeTargets") project(hifi) add_definitions(-DGLM_FORCE_RADIANS) -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") if (WIN32) add_definitions(-DNOMINMAX -D_CRT_SECURE_NO_WARNINGS) - # set path for Microsoft SDKs - # if get build error about missing 'glu32' this path is likely wrong - # Uncomment the line with 8.1 if running Windows 8.1 + + if (NOT WINDOW_SDK_PATH) + set(DEBUG_DISCOVERED_SDK_PATH TRUE) + endif() + + # sets path for Microsoft SDKs + # if you get build error about missing 'glu32' this path is likely wrong if (MSVC10) - set(WINDOW_SDK_PATH "C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1 ") + set(WINDOW_SDK_PATH "C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1 " CACHE PATH "Windows SDK PATH") elseif (MSVC12) if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") - set(WINDOW_SDK_FOLDER "x64") + set(WINDOW_SDK_FOLDER "x64") else() - set(WINDOW_SDK_FOLDER "x86") + set(WINDOW_SDK_FOLDER "x86") endif() - set(WINDOW_SDK_PATH "C:\\Program Files (x86)\\Windows Kits\\8.1\\Lib\\winv6.3\\um\\${WINDOW_SDK_FOLDER}") + set(WINDOW_SDK_PATH "C:\\Program Files (x86)\\Windows Kits\\8.1\\Lib\\winv6.3\\um\\${WINDOW_SDK_FOLDER}" CACHE PATH "Windows SDK PATH") endif () - message (WINDOW_SDK_PATH= ${WINDOW_SDK_PATH}) + + if (DEBUG_DISCOVERED_SDK_PATH) + message(STATUS "The discovered Windows SDK path is ${WINDOW_SDK_PATH}") + endif () + set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${WINDOW_SDK_PATH}) # /wd4351 disables warning C4351: new behavior: elements of array will be default initialized set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP /wd4351") @@ -75,7 +83,7 @@ if ((NOT MSVC12) AND (NOT MSVC14)) endif() endif () -if (APPLE) +if (APPLE) set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++0x") set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --stdlib=libc++") @@ -85,16 +93,16 @@ if (NOT ANDROID_LIB_DIR) set(ANDROID_LIB_DIR $ENV{ANDROID_LIB_DIR}) endif () -if (ANDROID) +if (ANDROID) if (NOT ANDROID_QT_CMAKE_PREFIX_PATH) set(QT_CMAKE_PREFIX_PATH ${ANDROID_LIB_DIR}/Qt/5.4/android_armv7/lib/cmake) else () set(QT_CMAKE_PREFIX_PATH ${ANDROID_QT_CMAKE_PREFIX_PATH}) endif () - + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) - + if (ANDROID_LIB_DIR) list(APPEND CMAKE_FIND_ROOT_PATH ${ANDROID_LIB_DIR}) endif () @@ -107,6 +115,8 @@ else () endif () endif () +set(QT_DIR $ENV{QT_DIR}) + if (WIN32) if (NOT EXISTS ${QT_CMAKE_PREFIX_PATH}) message(FATAL_ERROR "Could not determine QT_CMAKE_PREFIX_PATH.") @@ -119,34 +129,34 @@ get_filename_component(QT_DIR "${QT_CMAKE_PREFIX_PATH}/../../" ABSOLUTE) set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${QT_CMAKE_PREFIX_PATH}) if (APPLE) - + exec_program(sw_vers ARGS -productVersion OUTPUT_VARIABLE OSX_VERSION) string(REGEX MATCH "^[0-9]+\\.[0-9]+" OSX_VERSION ${OSX_VERSION}) message(STATUS "Detected OS X version = ${OSX_VERSION}") - + set(OSX_SDK "${OSX_VERSION}" CACHE String "OS X SDK version to look for inside Xcode bundle or at OSX_SDK_PATH") - + # set our OS X deployment target set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8) - + # find the SDK path for the desired SDK find_path( - _OSX_DESIRED_SDK_PATH - NAME MacOSX${OSX_SDK}.sdk + _OSX_DESIRED_SDK_PATH + NAME MacOSX${OSX_SDK}.sdk HINTS ${OSX_SDK_PATH} PATHS /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/ /Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/ ) - + if (NOT _OSX_DESIRED_SDK_PATH) message(STATUS "Could not find OS X ${OSX_SDK} SDK. Will fall back to default. If you want a specific SDK, please pass OSX_SDK and optionally OSX_SDK_PATH to CMake.") else () message(STATUS "Found OS X ${OSX_SDK} SDK at ${_OSX_DESIRED_SDK_PATH}/MacOSX${OSX_SDK}.sdk") - + # set that as the SDK to use set(CMAKE_OSX_SYSROOT ${_OSX_DESIRED_SDK_PATH}/MacOSX${OSX_SDK}.sdk) endif () - + endif () # Hide automoc folders (for IDEs) @@ -170,9 +180,9 @@ else () set(UPPER_CMAKE_BUILD_TYPE DEBUG) endif () -set(HIFI_CMAKE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -set(MACRO_DIR "${HIFI_CMAKE_DIR}/macros") -set(EXTERNAL_PROJECT_DIR "${HIFI_CMAKE_DIR}/externals") +set(HF_CMAKE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +set(MACRO_DIR "${HF_CMAKE_DIR}/macros") +set(EXTERNAL_PROJECT_DIR "${HF_CMAKE_DIR}/externals") file(GLOB HIFI_CUSTOM_MACROS "cmake/macros/*.cmake") foreach(CUSTOM_MACRO ${HIFI_CUSTOM_MACROS}) @@ -198,9 +208,11 @@ if (WIN32) endif () if (NOT DEFINED SERVER_ONLY) - set(SERVER_ONLY 0) + set(SERVER_ONLY 0) endif() +set_packaging_parameters() + # add subdirectories for all targets if (NOT ANDROID) add_subdirectory(assignment-client) @@ -209,8 +221,7 @@ if (NOT ANDROID) set_target_properties(domain-server PROPERTIES FOLDER "Apps") add_subdirectory(ice-server) set_target_properties(ice-server PROPERTIES FOLDER "Apps") - add_subdirectory(stack-manager) - set_target_properties(stack-manager PROPERTIES FOLDER "Apps") + add_subdirectory(server-console) if (NOT SERVER_ONLY) add_subdirectory(interface) set_target_properties(interface PROPERTIES FOLDER "Apps") @@ -233,5 +244,4 @@ if (HIFI_MEMORY_DEBUGGING) endif (UNIX) endif () -include_application_version() -generate_installers() \ No newline at end of file +generate_installers() diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000000..701752f6af --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,25 @@ +Follow the [build guide](BUILD.md) to figure out how to build High Fidelity for your platform. + +During generation, CMake should produce an `install` target and a `package` target. + +###Install + +The `install` target will copy the High Fidelity targets and their dependencies to your `CMAKE_INSTALL_PREFIX`. + +###Packaging + +To produce an installer, run the `package` target. + +####Windows + +To produce an executable installer on Windows, the following are required: + +- [Nullsoft Scriptable Install System](http://nsis.sourceforge.net/Download) - 3.0b3 +- [UAC Plug-in for Nullsoft](http://nsis.sourceforge.net/UAC_plug-in) - 0.2.4c +- [nsProcess Plug-in for Nullsoft](http://nsis.sourceforge.net/NsProcess_plugin) - 1.6 + +Run the `package` target to create an executable installer using the Nullsoft Scriptable Install System. + +####OS X + +Run the `package` target to create an Apple Disk Image (.dmg). diff --git a/LICENSE b/LICENSE index 93c9a953d4..53c5ccf39a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,12 @@ -Copyright 2014 High Fidelity, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file +Copyright (c) 2013-2016, High Fidelity, Inc. +All rights reserved. +licensing@highfidelity.io + +Licensed under the Apache License version 2.0 (the "License"); +You may not use this software except in compliance with the License. +You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 + +This software includes third-party software. +Please see each individual software license for additional details. + +This software is distributed "as-is" without any warranties, conditions, or representations whether express or implied, including without limitation the implied warranties and conditions of merchantability, merchantable quality, fitness for a particular purpose, performance, durability, title, non-infringement, and those arising from statute or from custom or usage of trade or course of dealing. diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 3064e97d70..3c102e1f02 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -3,12 +3,14 @@ set(TARGET_NAME assignment-client) setup_hifi_project(Core Gui Network Script Quick Widgets WebSockets) # link in the shared libraries -link_hifi_libraries( - audio avatars octree environment gpu model fbx entities +link_hifi_libraries( + audio avatars octree environment gpu model fbx entities networking animation recording shared script-engine embedded-webserver controllers physics ) -include_application_version() -package_libraries_for_deployment() -consolidate_stack_components() \ No newline at end of file +if (WIN32) + package_libraries_for_deployment() +endif() + +install_beside_console() diff --git a/assignment-client/src/AssignmentClientApp.cpp b/assignment-client/src/AssignmentClientApp.cpp index cddce5de7d..8a36d99c75 100644 --- a/assignment-client/src/AssignmentClientApp.cpp +++ b/assignment-client/src/AssignmentClientApp.cpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include #include #include @@ -22,6 +22,8 @@ #include "AssignmentClient.h" #include "AssignmentClientMonitor.h" #include "AssignmentClientApp.h" +#include +#include AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : @@ -30,7 +32,7 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : // to work around the Qt constant wireless scanning, set the env for polling interval very high const QByteArray EXTREME_BEARER_POLL_TIMEOUT = QString::number(INT_MAX).toLocal8Bit(); qputenv("QT_BEARER_POLL_TIMEOUT", EXTREME_BEARER_POLL_TIMEOUT); - + # ifndef WIN32 setvbuf(stdout, NULL, _IOLBF, 0); # endif @@ -42,10 +44,10 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : ShutdownEventListener::getInstance(); # endif - setOrganizationName("High Fidelity"); + setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); setOrganizationDomain("highfidelity.io"); setApplicationName("assignment-client"); - setApplicationName(BUILD_VERSION); + setApplicationVersion(BuildInfo::VERSION); // use the verbose message handler in Logging qInstallMessageHandler(LogHandler::verboseMessageHandler); @@ -63,7 +65,7 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : const QCommandLineOption poolOption(ASSIGNMENT_POOL_OPTION, "set assignment pool", "pool-name"); parser.addOption(poolOption); - + const QCommandLineOption portOption(ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION, "UDP port for this assignment client (or monitor)", "port"); parser.addOption(portOption); @@ -92,6 +94,12 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : const QCommandLineOption monitorPortOption(ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION, "assignment-client monitor port", "port"); parser.addOption(monitorPortOption); + const QCommandLineOption httpStatusPortOption(ASSIGNMENT_HTTP_STATUS_PORT, "http status server port", "http-status-port"); + parser.addOption(httpStatusPortOption); + + const QCommandLineOption logDirectoryOption(ASSIGNMENT_LOG_DIRECTORY, "directory to store logs", "log-directory"); + parser.addOption(logDirectoryOption); + if (!parser.parse(QCoreApplication::arguments())) { qCritical() << parser.errorText() << endl; parser.showHelp(); @@ -130,6 +138,18 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : numForks = minForks; } + quint16 httpStatusPort { 0 }; + if (parser.isSet(httpStatusPortOption)) { + httpStatusPort = parser.value(httpStatusPortOption).toUShort(); + } + + QDir logDirectory { "." }; + if (parser.isSet(logDirectoryOption)) { + logDirectory = parser.value(logDirectoryOption); + } else { + logDirectory = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + } + Assignment::Type requestAssignmentType = Assignment::AllTypes; if (argumentVariantMap.contains(ASSIGNMENT_TYPE_OVERRIDE_OPTION)) { requestAssignmentType = (Assignment::Type) argumentVariantMap.value(ASSIGNMENT_TYPE_OVERRIDE_OPTION).toInt(); @@ -168,11 +188,11 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : if (argumentVariantMap.contains(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION)) { assignmentServerPort = argumentVariantMap.value(CUSTOM_ASSIGNMENT_SERVER_PORT_OPTION).toUInt(); } - + if (parser.isSet(assignmentServerPortOption)) { assignmentServerPort = parser.value(assignmentServerPortOption).toInt(); } - + // check for an overidden listen port quint16 listenPort = 0; if (argumentVariantMap.contains(ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION)) { @@ -200,7 +220,7 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : AssignmentClientMonitor* monitor = new AssignmentClientMonitor(numForks, minForks, maxForks, requestAssignmentType, assignmentPool, listenPort, walletUUID, assignmentServerHostname, - assignmentServerPort); + assignmentServerPort, httpStatusPort, logDirectory); monitor->setParent(this); connect(this, &QCoreApplication::aboutToQuit, monitor, &AssignmentClientMonitor::aboutToQuit); } else { diff --git a/assignment-client/src/AssignmentClientApp.h b/assignment-client/src/AssignmentClientApp.h index c4705e30eb..37d3b9cc1d 100644 --- a/assignment-client/src/AssignmentClientApp.h +++ b/assignment-client/src/AssignmentClientApp.h @@ -25,6 +25,8 @@ const QString ASSIGNMENT_NUM_FORKS_OPTION = "n"; const QString ASSIGNMENT_MIN_FORKS_OPTION = "min"; const QString ASSIGNMENT_MAX_FORKS_OPTION = "max"; const QString ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION = "monitor-port"; +const QString ASSIGNMENT_HTTP_STATUS_PORT = "http-status-port"; +const QString ASSIGNMENT_LOG_DIRECTORY = "log-directory"; class AssignmentClientApp : public QCoreApplication { Q_OBJECT diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index 2d27962071..f76434d9c7 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -12,6 +12,9 @@ #include #include +#include +#include + #include #include #include @@ -20,6 +23,7 @@ #include "AssignmentClientApp.h" #include "AssignmentClientChildData.h" #include "SharedUtil.h" +#include const QString ASSIGNMENT_CLIENT_MONITOR_TARGET_NAME = "assignment-client-monitor"; const int WAIT_FOR_CHILD_MSECS = 1000; @@ -29,7 +33,9 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen const unsigned int maxAssignmentClientForks, Assignment::Type requestAssignmentType, QString assignmentPool, quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname, - quint16 assignmentServerPort) : + quint16 assignmentServerPort, quint16 httpStatusServerPort, QDir logDirectory) : + _logDirectory(logDirectory), + _httpManager(QHostAddress::LocalHost, httpStatusServerPort, "", this), _numAssignmentClientForks(numAssignmentClientForks), _minAssignmentClientForks(minAssignmentClientForks), _maxAssignmentClientForks(maxAssignmentClientForks), @@ -38,6 +44,7 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen _walletUUID(walletUUID), _assignmentServerHostname(assignmentServerHostname), _assignmentServerPort(assignmentServerPort) + { qDebug() << "_requestAssignmentType =" << _requestAssignmentType; @@ -80,24 +87,21 @@ void AssignmentClientMonitor::simultaneousWaitOnChildren(int waitMsecs) { } } -void AssignmentClientMonitor::childProcessFinished() { - QProcess* childProcess = qobject_cast(sender()); - qint64 processID = _childProcesses.key(childProcess); - - if (processID > 0) { - qDebug() << "Child process" << processID << "has finished. Removing from internal map."; - _childProcesses.remove(processID); +void AssignmentClientMonitor::childProcessFinished(qint64 pid) { + if (_childProcesses.remove(pid)) { + qDebug() << "Child process" << pid << "has finished. Removed from internal map."; } } void AssignmentClientMonitor::stopChildProcesses() { + qDebug() << "Stopping child processes"; auto nodeList = DependencyManager::get(); // ask child processes to terminate - foreach(QProcess* childProcess, _childProcesses) { - if (childProcess->processId() > 0) { - qDebug() << "Attempting to terminate child process" << childProcess->processId(); - childProcess->terminate(); + for (auto& ac : _childProcesses) { + if (ac.process->processId() > 0) { + qDebug() << "Attempting to terminate child process" << ac.process->processId(); + ac.process->terminate(); } } @@ -105,10 +109,10 @@ void AssignmentClientMonitor::stopChildProcesses() { if (_childProcesses.size() > 0) { // ask even more firmly - foreach(QProcess* childProcess, _childProcesses) { - if (childProcess->processId() > 0) { - qDebug() << "Attempting to kill child process" << childProcess->processId(); - childProcess->kill(); + for (auto& ac : _childProcesses) { + if (ac.process->processId() > 0) { + qDebug() << "Attempting to kill child process" << ac.process->processId(); + ac.process->kill(); } } @@ -155,18 +159,61 @@ void AssignmentClientMonitor::spawnChildClient() { _childArguments.append("--" + ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION); _childArguments.append(QString::number(DependencyManager::get()->getLocalSockAddr().getPort())); + // Setup log files + const QString DATETIME_FORMAT = "yyyyMMdd.hh.mm.ss.zzz"; + + if (!_logDirectory.exists()) { + qDebug() << "Log directory (" << _logDirectory.absolutePath() << ") does not exist, creating."; + _logDirectory.mkpath(_logDirectory.absolutePath()); + } + + auto nowString = QDateTime::currentDateTime().toString(DATETIME_FORMAT); + auto stdoutFilenameTemp = QString("ac-%1-stdout.txt").arg(nowString); + auto stderrFilenameTemp = QString("ac-%1-stderr.txt").arg(nowString); + QString stdoutPathTemp = _logDirectory.absoluteFilePath(stdoutFilenameTemp); + QString stderrPathTemp = _logDirectory.absoluteFilePath(stderrFilenameTemp); + + // reset our output and error files + assignmentClient->setStandardOutputFile(stdoutPathTemp); + assignmentClient->setStandardErrorFile(stderrPathTemp); + // make sure that the output from the child process appears in our output assignmentClient->setProcessChannelMode(QProcess::ForwardedChannels); assignmentClient->start(QCoreApplication::applicationFilePath(), _childArguments); - + + // Update log path to use PID in filename + auto stdoutFilename = QString("ac-%1_%2-stdout.txt").arg(nowString).arg(assignmentClient->processId()); + auto stderrFilename = QString("ac-%1_%2-stderr.txt").arg(nowString).arg(assignmentClient->processId()); + QString stdoutPath = _logDirectory.absoluteFilePath(stdoutFilename); + QString stderrPath = _logDirectory.absoluteFilePath(stderrFilename); + + qDebug() << "Renaming " << stdoutPathTemp << " to " << stdoutPath; + if (!_logDirectory.rename(stdoutFilenameTemp, stdoutFilename)) { + qDebug() << "Failed to rename " << stdoutFilenameTemp; + stdoutPath = stdoutPathTemp; + stdoutFilename = stdoutFilenameTemp; + } + + qDebug() << "Renaming " << stderrPathTemp << " to " << stderrPath; + if (!QFile::rename(stderrPathTemp, stderrPath)) { + qDebug() << "Failed to rename " << stderrFilenameTemp; + stderrPath = stderrPathTemp; + stderrFilename = stderrFilenameTemp; + } + + qDebug() << "Child stdout being written to: " << stdoutFilename; + qDebug() << "Child stderr being written to: " << stderrFilename; + if (assignmentClient->processId() > 0) { + auto pid = assignmentClient->processId(); // make sure we hear that this process has finished when it does - connect(assignmentClient, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(childProcessFinished())); - + connect(assignmentClient, static_cast(&QProcess::finished), + this, [this, pid]() { childProcessFinished(pid); }); + qDebug() << "Spawned a child client with PID" << assignmentClient->processId(); - _childProcesses.insert(assignmentClient->processId(), assignmentClient); - } + _childProcesses.insert(assignmentClient->processId(), { assignmentClient, stdoutPath, stderrPath }); + } } void AssignmentClientMonitor::checkSpares() { @@ -222,12 +269,12 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer()->addOrUpdateNode (senderID, NodeType::Unassigned, senderSockAddr, senderSockAddr, false, false); - + auto childData = std::unique_ptr { new AssignmentClientChildData(Assignment::Type::AllTypes) }; matchingNode->setLinkedData(std::move(childData)); @@ -252,10 +299,39 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointerreadPrimitive(&assignmentType); - - childData->setChildType((Assignment::Type) assignmentType); - + + childData->setChildType(Assignment::Type(assignmentType)); + // note when this child talked matchingNode->setLastHeardMicrostamp(usecTimestampNow()); } } + +bool AssignmentClientMonitor::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { + if (url.path() == "/status") { + QByteArray response; + + QJsonObject status; + QJsonObject servers; + for (auto& ac : _childProcesses) { + QJsonObject server; + + server["pid"] = ac.process->processId(); + server["logStdout"] = ac.logStdoutPath; + server["logStderr"] = ac.logStderrPath; + + servers[QString::number(ac.process->processId())] = server; + } + + status["servers"] = servers; + + QJsonDocument document { status }; + + connection->respond(HTTPConnection::StatusCode200, document.toJson()); + } else { + connection->respond(HTTPConnection::StatusCode404); + } + + + return true; +} diff --git a/assignment-client/src/AssignmentClientMonitor.h b/assignment-client/src/AssignmentClientMonitor.h index 7ea7c3e274..a5ae3cd1af 100644 --- a/assignment-client/src/AssignmentClientMonitor.h +++ b/assignment-client/src/AssignmentClientMonitor.h @@ -16,28 +16,39 @@ #include #include #include +#include #include #include "AssignmentClientChildData.h" +#include +#include extern const char* NUM_FORKS_PARAMETER; -class AssignmentClientMonitor : public QObject { +struct ACProcess { + QProcess* process; // looks like a dangling pointer, but is parented by the AssignmentClientMonitor + QString logStdoutPath; + QString logStderrPath; +}; + +class AssignmentClientMonitor : public QObject, public HTTPRequestHandler { Q_OBJECT public: AssignmentClientMonitor(const unsigned int numAssignmentClientForks, const unsigned int minAssignmentClientForks, const unsigned int maxAssignmentClientForks, Assignment::Type requestAssignmentType, QString assignmentPool, quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname, - quint16 assignmentServerPort); + quint16 assignmentServerPort, quint16 httpStatusServerPort, QDir logDirectory); ~AssignmentClientMonitor(); void stopChildProcesses(); private slots: void checkSpares(); - void childProcessFinished(); + void childProcessFinished(qint64 pid); void handleChildStatusPacket(QSharedPointer message); - + + bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; + public slots: void aboutToQuit(); @@ -47,6 +58,10 @@ private: QTimer _checkSparesTimer; // every few seconds see if it need fewer or more spare children + QDir _logDirectory; + + HTTPManager _httpManager; + const unsigned int _numAssignmentClientForks; const unsigned int _minAssignmentClientForks; const unsigned int _maxAssignmentClientForks; @@ -57,7 +72,7 @@ private: QString _assignmentServerHostname; quint16 _assignmentServerPort; - QMap _childProcesses; + QMap _childProcesses; }; #endif // hifi_AssignmentClientMonitor_h diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 2f9e7ee1e6..c0862e3f63 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -24,6 +24,7 @@ #include "NodeType.h" #include "SendAssetTask.h" #include "UploadAssetTask.h" +#include const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server"; @@ -49,8 +50,27 @@ void AssetServer::run() { auto nodeList = DependencyManager::get(); nodeList->addNodeTypeToInterestSet(NodeType::Agent); - _resourcesDirectory = QDir(QCoreApplication::applicationDirPath()).filePath("resources/assets"); + const QString RESOURCES_PATH = "assets"; + + _resourcesDirectory = QDir(ServerPathUtils::getDataDirectory()).filePath(RESOURCES_PATH); if (!_resourcesDirectory.exists()) { + qDebug() << "Asset resources directory not found, searching for existing asset resources"; + QString oldDataDirectory = QCoreApplication::applicationDirPath(); + auto oldResourcesDirectory = QDir(oldDataDirectory).filePath(RESOURCES_PATH); + + if (QDir(oldResourcesDirectory).exists()) { + qDebug() << "Existing assets found in " << oldResourcesDirectory << ", copying to " << _resourcesDirectory; + + + QDir resourcesParentDirectory = _resourcesDirectory.filePath(".."); + if (!resourcesParentDirectory.exists()) { + qDebug() << "Creating data directory " << resourcesParentDirectory.absolutePath(); + resourcesParentDirectory.mkpath("."); + } + + QFile::copy(oldResourcesDirectory, _resourcesDirectory.absolutePath()); + } + qDebug() << "Creating resources directory"; _resourcesDirectory.mkpath("."); } diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 410aa922dc..331a6cbe5b 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -27,6 +27,9 @@ #include "OctreeQueryNode.h" #include "OctreeServerConsts.h" +#include +#include +#include int OctreeServer::_clientCount = 0; const int MOVING_AVERAGE_SAMPLE_COUNTS = 1000000; @@ -280,10 +283,10 @@ OctreeServer::~OctreeServer() { void OctreeServer::initHTTPManager(int port) { // setup the embedded web server - QString documentRoot = QString("%1/resources/web").arg(QCoreApplication::applicationDirPath()); + QString documentRoot = QString("%1/web").arg(ServerPathUtils::getDataDirectory()); // setup an httpManager with us as the request handler and the parent - _httpManager = new HTTPManager(port, documentRoot, this, this); + _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); } bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { @@ -1031,6 +1034,7 @@ void OctreeServer::readConfiguration() { if (!readOptionString(QString("persistFilename"), settingsSectionObject, persistFilename)) { persistFilename = getMyDefaultPersistFilename(); } + strcpy(_persistFilename, qPrintable(persistFilename)); qDebug("persistFilename=%s", _persistFilename); @@ -1096,7 +1100,7 @@ void OctreeServer::run() { _tree->setIsServer(true); qDebug() << "Waiting for connection to domain to request settings from domain-server."; - + // wait until we have the domain-server settings, otherwise we bail DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::settingsReceived, this, &OctreeServer::domainSettingsRequestComplete); @@ -1139,9 +1143,30 @@ void OctreeServer::domainSettingsRequestComplete() { // if we want Persistence, set up the local file and persist thread if (_wantPersist) { + // If persist filename does not exist, let's see if there is one beside the application binary + // If there is, let's copy it over to our target persist directory + QString oldResourcesDirectory = QCoreApplication::applicationDirPath(); + auto oldPersistPath = QDir(oldResourcesDirectory).absoluteFilePath(_persistFilename); + auto persistPath = ServerPathUtils::getDataFilePath("entities/" + QString(_persistFilename)); + if (oldPersistPath != persistPath && !QFile::exists(persistPath)) { + qDebug() << "Persist file does not exist, checking for existence of persist file next to application"; + if (QFile::exists(oldPersistPath)) { + qDebug() << "Old persist file found, copying from " << oldPersistPath << " to " << persistPath; + + QDir persistFileDirectory = QDir(persistPath).filePath(".."); + + if (!persistFileDirectory.exists()) { + qDebug() << "Creating data directory " << persistFileDirectory.path(); + persistFileDirectory.mkpath("."); + } + QFile::copy(oldPersistPath, persistPath); + } else { + qDebug() << "No existing persist file found"; + } + } // now set up PersistThread - _persistThread = new OctreePersistThread(_tree, _persistFilename, _persistInterval, + _persistThread = new OctreePersistThread(_tree, persistPath, _persistInterval, _wantBackup, _settings, _debugTimestampNow, _persistAsFileType); _persistThread->initialize(true); } diff --git a/cmake/externals/faceshift/CMakeLists.txt b/cmake/externals/faceshift/CMakeLists.txt index b18b861912..28fbffec34 100644 --- a/cmake/externals/faceshift/CMakeLists.txt +++ b/cmake/externals/faceshift/CMakeLists.txt @@ -21,10 +21,23 @@ ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE FILEPATH "Path to Faceshift include directory") +set(LIBRARY_DEBUG_PATH "lib/Debug") +set(LIBRARY_RELEASE_PATH "lib/Release") + if (WIN32) - set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/Debug/faceshift.lib CACHE FILEPATH "Faceshift libraries") - set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/Release/faceshift.lib CACHE FILEPATH "Faceshift libraries") + set(LIBRARY_PREFIX "") + set(LIBRARY_EXT "lib") elseif (APPLE) - set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/Debug/libfaceshift.a CACHE FILEPATH "Faceshift libraries") - set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/Release/libfaceshift.a CACHE FILEPATH "Faceshift libraries") + set(LIBRARY_EXT "a") + set(LIBRARY_PREFIX "lib") + + if (CMAKE_GENERATOR STREQUAL "Unix Makefiles") + set(LIBRARY_DEBUG_PATH "build") + set(LIBRARY_RELEASE_PATH "build") + endif () endif() + +set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG + ${INSTALL_DIR}/${LIBRARY_DEBUG_PATH}/${LIBRARY_PREFIX}faceshift.${LIBRARY_EXT} CACHE FILEPATH "Faceshift libraries") +set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE +${INSTALL_DIR}/${LIBRARY_RELEASE_PATH}/${LIBRARY_PREFIX}faceshift.${LIBRARY_EXT} CACHE FILEPATH "Faceshift libraries") diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt deleted file mode 100644 index ac19e7d680..0000000000 --- a/cmake/externals/quazip/CMakeLists.txt +++ /dev/null @@ -1,52 +0,0 @@ -set(EXTERNAL_NAME quazip) -string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -cmake_policy(SET CMP0046 OLD) - -include(ExternalProject) - -if (WIN32) - # windows shell does not like backslashes expanded on the command line, - # so convert all backslashes in the QT path to forward slashes - string(REPLACE \\ / QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH}) -elseif ($ENV{QT_CMAKE_PREFIX_PATH}) - set(QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH}) -endif () - -ExternalProject_Add( - ${EXTERNAL_NAME} - URL http://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.6.2.zip - URL_MD5 514851970f1a14d815bdc3ad6267af4d - BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build - CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=${QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=/lib -DZLIB_ROOT=${ZLIB_ROOT} - LOG_DOWNLOAD 1 - LOG_CONFIGURE 1 - LOG_BUILD 1 -) - -add_dependencies(quazip zlib) - -# Hide this external target (for ide users) -set_target_properties(${EXTERNAL_NAME} PROPERTIES - FOLDER "hidden/externals" - INSTALL_NAME_DIR ${INSTALL_DIR}/lib - BUILD_WITH_INSTALL_RPATH True) - -ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) -set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIR ${INSTALL_DIR}/include CACHE PATH "List of QuaZip include directories") -set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${${EXTERNAL_NAME_UPPER}_INCLUDE_DIR} CACHE PATH "List of QuaZip include directories") -set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${INSTALL_DIR}/lib CACHE FILEPATH "Location of QuaZip DLL") - -if (APPLE) - set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library") -elseif (WIN32) - set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/quazip.lib CACHE FILEPATH "Location of QuaZip release library") -else () - set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip.so CACHE FILEPATH "Location of QuaZip release library") -endif () - -include(SelectLibraryConfigurations) -select_library_configurations(${EXTERNAL_NAME_UPPER}) - -# Force selected libraries into the cache -set(${EXTERNAL_NAME_UPPER}_LIBRARY ${${EXTERNAL_NAME_UPPER}_LIBRARY} CACHE FILEPATH "Location of QuaZip libraries") -set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE FILEPATH "Location of QuaZip libraries") diff --git a/cmake/installer/install-folder.rsrc b/cmake/installer/install-folder.rsrc new file mode 100644 index 0000000000..8ce12afd3f --- /dev/null +++ b/cmake/installer/install-folder.rsrc @@ -0,0 +1,1634 @@ +data 'icns' (-16455) { + $"6963 6E73 0000 65EB 544F 4320 0000 0018" /* icns..eTOC .... */ + $"6974 3332 0000 25C3 7438 6D6B 0000 4008" /* it32..%t8mk..@. */ + $"6974 3332 0000 25C3 0000 0000 FF00 FF00" /* it32..%...... */ + $"FF00 FF00 FF00 FF00 FF00 FF00 FF00 FF00" /* ........ */ + $"FF00 FF00 EE00 0005 A008 0007 D500 0309" /* .......... */ + $"549A BBA0 C102 C0A3 48D2 0003 2FB2 E4E3" /* T.H../ */ + $"A2DF 03E3 E176 01CF 0002 30C9 DDA5 D502" /* .v...0ݥ. */ + $"D7DE 61CE 0002 0EB1 D6A7 D002 D2CC 2BCD" /* a...֧.+ */ + $"0001 5BD4 A9CA 02D1 9A05 CB00 0202 9ACA" /* ..[ԩ.њ.... */ + $"AAC5 01CC 4FCB 0002 0EAF C1AA BF02 C2AF" /* .O....¯ */ + $"16CA 0002 14AE BCAA BA03 B9C1 7F02 C900" /* ........ */ + $"0213 A9B6 ACB4 02BA 6503 C800 0212 A4B1" /* ...e.... */ + $"ADAF 03B4 803B 26BA 2502 2415 0186 0002" /* .;&%.$.... */ + $"129F ABAE A902 AEAE AAB9 A905 AAA9 A184" /* ... */ + $"4003 8400 0211 9AA5 EFA4 04A5 A9AA 740E" /* @.....t. */ + $"8300 0211 949F 819E EC9D 049E 9EA6 7607" /* ....v. */ + $"8200 0710 8F9A 9997 9698 9CEA 9F05 9E9A" /* .... */ + $"9797 A04A 8200 080F 8A94 919C B5CE DDE4" /* J... */ + $"E9E5 06E2 D5BA 9F94 840B 8100 050F 848F" /* .պ.... */ + $"B2E1 F9EE FF04 F6D3 A78F 2881 0003 0E80" /* .ӧ(... */ + $"BEF6 F1FF 03FD DBA3 3881 0002 0BA1 F5F3" /* .ۣ8... */ + $"FF02 FECD 3C81 0001 16D1 F5FF 01ED 4781" /* .<....G */ + $"0001 36F1 F5FF 01F9 5281 0001 43F7 F5FF" /* ..6.R..C */ + $"01FB 5A81 0002 43F6 FFF3 FE02 FFFB 5F81" /* .Z..C._ */ + $"0002 43F5 FFF3 FD02 FEFA 5F81 0002 43F5" /* ..C._..C */ + $"FEB5 FC06 FDFE FEFF FEFE FDB4 FC02 FDF9" /* .. */ + $"5F81 0002 43F5 FEB0 FC00 FD81 FF06 FAF2" /* _..C.. */ + $"EDEB EEF4 FD81 FF00 FDAF FC02 FDF9 5F81" /* .._ */ + $"0002 43F4 FDAD FB16 FCFF FFF6 D1A0 704B" /* ..C.ѠpK */ + $"3222 1A18 1B26 3854 7CAD DDFD FFFF FCAC" /* 2"...&8T| */ + $"FB02 FCF8 5F81 0002 43F3 FCAB FA06 FCFF" /* ._..C. */ + $"FAC4 732E 068A 0006 0E3E 89D7 FFFF FBAA" /* s.....> */ + $"FA02 FBF8 5F81 0002 43F3 FBA9 F905 FAFF" /* ._..C. */ + $"F3A1 3C02 8300 0608 131B 1D19 1005 8300" /* <........... */ + $"040C 55BC FDFF A9F9 02FA F75F 8100 0243" /* ..U._..C */ + $"F3FB A7F9 04F8 FEFB A831 8200 0E26 5C91" /* .1..&\ */ + $"B9D4 E3EA ECE8 DFCE B083 4D19 8100 0501" /* ΰM.... */ + $"4DC7 FFFB F8A6 F902 FAF7 5F81 0002 43F2" /* M._..C */ + $"FAA7 F802 FFD5 4E81 0003 1D6E BFEF 80FF" /* .N...n */ + $"01FC FB80 FA08 FBFD FFFF FEE6 AC56 0D80" /* ..V. */ + $"0004 0973 EDFF F7A5 F802 F9F6 5F81 0002" /* ..s._.. */ + $"43F1 F8A4 F704 F6F8 FE9F 1580 0005 238F" /* C....# */ + $"E6FF FDF8 8AF7 05F9 FFFE D570 0F80 0002" /* .p... */ + $"31C6 FFA5 F702 F8F5 5F81 0002 43F0 F7A4" /* 1._..C */ + $"F602 F9F7 7280 0004 0C7C E8FF F990 F603" /* .r...|. */ + $"FBFF D357 8000 030F A1FF F7A3 F602 F7F5" /* W.... */ + $"5F81 0002 43F0 F7A3 F602 F9F2 5880 0003" /* _..C.X.. */ + $"37C8 FFF9 94F6 09FB FCA4 1900 0003 8AFE" /* 7.... */ + $"F7A2 F602 F7F5 5F81 0002 43F0 F6A2 F502" /* ._..C. */ + $"F8F2 5180 0002 63EE FC82 F501 F6F6 91F5" /* Q..c. */ + $"02FF D539 8000 0186 FDA2 F502 F6F4 5F81" /* .9..._ */ + $"0002 43EF F5A1 F408 F5F7 5D00 0001 7FFA" /* ..C.].... */ + $"F781 F405 F5FD F3F4 FDF5 90F4 07FB EA4F" /* ..O */ + $"0000 0194 FEA1 F402 F5F3 5E81 0002 43EE" /* ....^..C */ + $"F4A1 F301 FC7D 8000 0286 FCF4 81F3 07F5" /* .}... */ + $"EF81 3335 88F2 F490 F307 F7F0 5200 0008" /* 35.R... */ + $"B4FC A0F3 02F4 F25E 8100 0243 EEF3 A0F2" /* .^..C */ + $"07FB AE04 0000 77FB F382 F201 F86A 8100" /* ....w.j. */ + $"0177 F991 F207 F6ED 4200 001D DBF7 9FF2" /* .w.B... */ + $"02F3 F25E 8100 0243 EDF2 9FF1 07F5 DF20" /* .^..C. */ + $"0000 53F5 F382 F102 F5D8 0D81 0002 15E0" /* ..S..... */ + $"F486 F101 F2F2 86F1 07F7 DD25 0000 50F5" /* ..%..P */ + $"F29E F102 F2F1 5E81 0002 43EC F19F F006" /* .^..C. */ + $"F864 0000 25E1 F483 F002 F5CA 0481 0002" /* d..%.... */ + $"0AD4 F484 F005 F1F9 F4F5 F8F1 85F0 06F9" /* .. */ + $"B808 0000 A0F9 9EF0 02F1 F05E 8100 0243" /* ....^..C */ + $"EBF0 9EEF 06F6 BE06 0004 AEF8 84EF 02F1" /* ..... */ + $"E92C 8100 0237 EEF0 83EF 07F1 ED88 3E43" /* ,..7.>C */ + $"99F2 F085 EF06 FA74 0000 25E5 F19D EF02" /* .t..%. */ + $"F0EF 5E81 0002 43EB F09E EF05 F54B 0000" /* ^..C.K.. */ + $"59F7 86EF 07F5 BA20 0000 26C3 F584 EF01" /* Y. ..&. */ + $"F569 8100 0188 F785 EF06 F1E5 2500 0088" /* i...%.. */ + $"F99D EF02 F0EF 5E81 0002 43EB F09D EF06" /* .^..C. */ + $"F5BE 0500 0DCC F486 EF07 EEFA B400 13D3" /* ...... */ + $"F6EE 83EF 02F2 D20A 8100 021E E5F0 84EF" /* .... */ + $"07EE F898 0000 23E5 F09C EF02 F0EF 5E81" /* ...#.^ */ + $"0002 43EB EF9D EE06 F760 0000 61F7 ED87" /* ..C.`..a */ + $"EE05 F4BD 0018 DFEF 84EE 01F3 BF82 0002" /* ...... */ + $"0FD7 F085 EE07 EFE9 2A00 009D F6ED 9BEE" /* ..*.. */ + $"02EF EF5E 8100 0243 EAEE 9CED 06EF DB16" /* .^..C.. */ + $"0006 C0F3 88ED 05F3 BB00 17DD EF84 ED02" /* ...... */ + $"EFE0 1E81 0000 3988 ED05 F788 0000 46F2" /* ...9...F */ + $"9CED 02EE EE5E 8100 0243 E9ED 9CEC 05F4" /* .^..C. */ + $"9D00 003B EE89 EC05 F2BB 0017 DCEE 85EC" /* ..;... */ + $"07F3 A715 0000 20BF F187 EC06 EFD3 1000" /* .... ... */ + $"0DD0 EF9B EC02 EDED 5E81 0002 43E9 EC9C" /* ..^..C */ + $"EB05 F459 0000 86F5 89EB 05F1 BA00 17DB" /* .Y..... */ + $"ED86 EB05 F7A4 0016 CFF2 89EB 05F1 4900" /* ....I. */ + $"0096 F49B EB02 ECEC 5E81 0002 43E8 EB9B" /* ..^..C */ + $"EA06 EBE4 2500 05C2 EF89 EA05 F0B9 0017" /* .%..... */ + $"DAEC 86EA 05F1 B300 1DE1 EC89 EA05 F48B" /* .... */ + $"0000 5AF3 9BEA 02EB EC5E 8100 0243 E7EA" /* ..Z.^..C */ + $"9BE9 06ED C607 0024 E3EA 89E9 05EF B800" /* ...$.. */ + $"18E1 ED86 E905 F0B0 001C DFEB 89E9 06EE" /* ..... */ + $"BE04 002D E7EA 9AE9 02EA EB5E 8100 0243" /* ..-.^..C */ + $"E7EA 9BE9 05F1 A400 004B F08A E907 EFB8" /* ...K. */ + $"000A 8AE0 F3EC 84E9 05F0 B000 1CDF EB89" /* .Š... */ + $"E906 EBDC 1A00 11D3 EC9A E902 EAEB 5E81" /* .....^ */ + $"0002 43E7 E99B E805 F284 0000 6FF2 8AE8" /* ..C...o */ + $"01EE B780 0004 2481 D6F2 EC82 E805 EFAF" /* ...$. */ + $"001C DEEA 89E8 06E9 EA35 0002 BCEE 9AE8" /* ...5.. */ + $"02E9 EA5E 8100 0243 E6E8 9BE7 05F1 6B00" /* .^..C.k. */ + $"008A F18A E70B EDB7 0003 0200 0019 70CB" /* ........p */ + $"F0ED 80E7 05EE AF00 1CDD E98A E705 EF4E" /* ....N */ + $"0000 A7EF 9AE7 02E8 E95E 8100 0243 E5E7" /* ...^..C */ + $"9BE6 05EF 5B00 009B EF8A E614 ECB6 0017" /* .[..... */ + $"A959 0B00 0010 60BF EDED E7ED AE00 1CDC" /* Y....`.. */ + $"E88A E605 F05F 0000 97EF 9AE6 02E7 E95E" /* ._...^ */ + $"8100 0243 E4E6 9BE5 05ED 5300 00A3 ED8A" /* ..C.S.. */ + $"E514 EBB5 0016 DCEF C367 1400 0008 50B1" /* ...g....P */ + $"E9F4 AE00 1CDB E78A E505 EF68 0000 8FEE" /* ...h.. */ + $"9AE5 02E6 E85E 8100 0243 E4E5 9BE4 05EC" /* .^..C. */ + $"5200 00A3 EC8A E414 EAB4 0016 D5E7 E9EE" /* R..... */ + $"CD76 1D00 0002 42A9 A900 1CDA E68A E405" /* v....B... */ + $"EF68 0000 8EEE 9AE4 02E5 E75E 8100 0243" /* h...^..C */ + $"E3E4 9BE3 05EC 5900 009A EC8A E30C E9B3" /* .Y... */ + $"0016 D4E5 E3E3 E7ED D585 2880 0004 1E00" /* ..Յ(.... */ + $"1CD9 E58A E305 ED5F 0000 95EC 9AE3 02E4" /* .._... */ + $"E65E 8100 0243 E3E4 9BE3 05ED 6800 0089" /* ^..C.h.. */ + $"EC8A E305 E9B3 0016 D4E5 81E3 04E5 EDDC" /* .... */ + $"9435 8000 021C D9E5 8AE3 05EB 4E00 00A3" /* 5....N.. */ + $"EA9A E302 E4E6 5E81 0002 43E3 E49B E305" /* .^..C. */ + $"EC80 0000 6FED 8AE3 05E9 B300 16D4 E583" /* ..o... */ + $"E308 E4EC E2A4 3B00 1CD9 E58A E305 E536" /* .;...6 */ + $"0002 B6E8 9AE3 02E4 E65E 8100 0243 E2E3" /* ...^..C */ + $"9BE2 05EA 9D00 004B E98A E205 E8B3 0016" /* ...K... */ + $"D3E4 85E2 06E3 EFAF 001B D8E4 89E2 06E4" /* .... */ + $"D61A 000F CCE5 9AE2 02E3 E65D 8100 0243" /* ....]..C */ + $"E2E2 9BE1 06E5 BE06 0025 DCE2 89E1 05E7" /* ...%. */ + $"B200 16D2 E386 E105 E7AA 001B D7E3 89E1" /* ..... */ + $"06E6 B904 002A DEE2 9AE1 02E2 E55D 8100" /* ...*.]. */ + $"0243 E1E1 9BE0 06E1 D921 0006 BCE4 89E0" /* .C.!.. */ + $"05E6 B200 16D3 E286 E005 E6A9 001B D6E2" /* ...... */ + $"89E0 05E9 8800 0054 E89B E002 E1E4 5D81" /* ...T.] */ + $"0002 43E0 E09C DF05 E751 0000 83E9 89DF" /* ..C.Q.. */ + $"05EA A900 12C6 E686 DF05 E5A8 001B D5E1" /* ...... */ + $"89DF 05E6 4900 008B E89B DF02 E0E3 5D81" /* .I...] */ + $"0002 43DF DF9C DE05 E691 0000 3BE2 88DE" /* ..Cߜ...; */ + $"07E4 AF1F 0000 25B7 E385 DE05 E4A8 001B" /* ....%... */ + $"D4E0 88DE 06E1 C911 000A C1E2 9BDE 02DF" /* .... */ + $"E35D 8100 0243 DFDE 9CDD 06E0 CA12 0007" /* ]..Cޜ.... */ + $"B7E2 86DD 02DE D82A 8100 0234 DCDE 84DD" /* .*..4ބ */ + $"05E3 A700 1BD3 DF88 DD05 E783 0000 3DE1" /* ...߈...= */ + $"9DDD 01E2 5D81 0002 43DE DD9D DC05 E454" /* .]..Cݝ.T */ + $"0000 5FE5 86DC 02E1 B904 8100 0209 C3E0" /* .._.... */ + $"84DC 05E2 A700 1BD3 DE87 DC06 DDDA 2B00" /* ...އ.+. */ + $"008D E59D DC01 E15D 8100 0243 DEDD 9DDC" /* ..]..Cݝ */ + $"06E2 AB03 000E C0E0 85DC 02DF C40C 8100" /* ....... */ + $"0213 CCDE 84DC 05E7 A300 18CE E287 DC06" /* ..ބ.... */ + $"E592 0000 1DD1 DE9D DC01 E15D 8100 0243" /* ...ޝ.]..C */ + $"DDDC 9DDB 07DC DF40 0000 57E4 DC84 DB02" /* ܝ.@..W܄. */ + $"DCE1 5E81 0001 6AE2 84DB 07E1 AF23 0001" /* ^..j.#.. */ + $"32C1 DF85 DB06 DDD5 2700 0077 E49E DB01" /* 2߅.'..w. */ + $"E05D 8100 0243 DDDC 9DDB 08DA E1A9 0400" /* ]..Cܝ... */ + $"05A5 E2DA 83DB 09DA DCD6 712C 2E78 D9DC" /* .ڃq,.x */ + $"DA82 DB02 DCD4 2881 0001 42DE 84DB 07DA" /* ڂ.(..Bބ. */ + $"E471 0000 1DCE DD9E DB01 E05D 8100 0243" /* q...ݞ.]..C */ + $"DDDB 9FDA 07E0 5400 0027 CFDD D983 DA07" /* ۟.T..'ك. */ + $"D9DB E1D8 D9E1 DBD9 83DA 01DE B182 0002" /* ك.ޱ.. */ + $"0FC7 DC82 DA08 D9E1 AD0A 0000 8AE2 D99E" /* .܂...ٞ */ + $"DA01 E05D 8100 0144 DCA0 D907 DCC4 1800" /* .]..Dܠ... */ + $"0052 DEDA 91D9 02DC B905 8100 0216 CCDA" /* .Rڑ.ܹ.... */ + $"81D9 07D8 DDCB 2600 0041 DBA0 D901 DF5D" /* .&..A۠.] */ + $"8100 0144 DBA0 D807 D7E0 9402 0000 72E0" /* ..D۠....r */ + $"92D8 01DA 4C81 0001 68DE 82D8 07DA D642" /* .L..hނ.B */ + $"0000 16BE DCA0 D801 DE5D 8100 0144 DBA2" /* ...ܠ.]..Dۢ */ + $"D707 DE66 0000 027F E0D8 90D7 07D9 CC5E" /* .f....ؐ.^ */ + $"2024 6ED4 D881 D707 DAD8 5100 0005 98DF" /* $n؁.Q... */ + $"A1D7 01DD 5D81 0001 44DA A2D6 08D8 D74A" /* .]..Dڢ.J */ + $"0000 0279 DDD8 90D6 05D8 DDCF D1DD D781" /* ...yؐ.ׁ */ + $"D602 DBD2 4E80 0001 7ADE A2D6 01DC 5D81" /* .N..zޢ.] */ + $"0001 44DA A3D6 02D9 D03F 8000 0260 D4DB" /* ..Dڣ.?..` */ + $"91D6 01D7 D782 D602 DEC1 3A80 0002 6CDC" /* .ׂ.:..l */ + $"D7A2 D601 DC5D 8100 0144 D9A4 D502 D9CE" /* ע.]..D٤. */ + $"4480 0003 38B5 DFD7 94D5 03D9 DC98 1C80" /* D..8ה.ܘ. */ + $"0002 6FDB D6A3 D501 DC5D 8100 0144 D8A5" /* ..o֣.]..Dإ */ + $"D402 D8D2 5980 0004 0F75 CDDD D690 D40A" /* .Y...u֐ */ + $"D7DD BD55 0200 0009 82DB D5A4 D401 DB5D" /* ݽU...Ƃդ.] */ + $"8100 0144 D8A6 D303 D5D8 7F0D 8000 0526" /* ..Dئ.....& */ + $"85CA DCD8 D48A D305 D5DA DBBD 6A13 8000" /* Ԋ.۽j.. */ + $"0323 A2DC D4A5 D301 DA5D 8100 0144 D7A7" /* .#ԥ.]..Dק */ + $"D203 D3DB AE39 8100 141F 68AB D0DB DAD8" /* .ۮ9...h */ + $"D5D4 D4D3 D4D4 D6D8 DBD9 C99C 5411 8000" /* ɜT.. */ + $"0303 57C5 D9A7 D201 D95D 8100 0144 D6A9" /* ..W٧.]..D֩ */ + $"D103 D7D1 8422 8100 0F04 2859 85A5 B9C5" /* .ф"...(Y */ + $"CACB C9C3 B59D 7A4B 1C82 0003 389F D8D4" /* õzK...8 */ + $"A8D1 01D9 5D81 0001 44D6 AAD0 04D1 D9C7" /* .]..D֪. */ + $"7D2A 8400 060B 161E 201C 1408 8300 0506" /* }*..... ...... */ + $"3D95 D1D7 D1A9 D001 D85C 8100 0144 D6AC" /* =ѩ.\..D֬ */ + $"D006 D2D9 CD9B 551F 018A 0006 072A 68AB" /* .͛U.....*h */ + $"D4D7 D1AB D001 D85C 8100 0144 D6AD D016" /* ѫ.\..D֭. */ + $"CFD1 D6D8 C8A5 7A52 3521 150F 0D10 1825" /* ȥzR5!.....% */ + $"3B5C 85AF CED8 D4AE D001 D85C 8100 0144" /* ;\Ԯ.\..D */ + $"D5B1 CF0F D0D4 D7D7 D1C9 C1BC BABD C3CC" /* ձ. */ + $"D3D7 D7D3 B1CF 01D7 5C81 0001 44D4 B6CE" /* ӱ.\..DԶ */ + $"00CF 81D0 01CF CFB5 CE01 D65C 8100 0144" /* .ρ.ϵ.\..D */ + $"D3F5 CD01 D65C 8100 0144 D3F5 CC01 D55C" /* .\..D.\ */ + $"8100 0144 D2F5 CB01 D45C 8100 0144 D1F5" /* ..D.\..D */ + $"CA01 D35C 8100 0144 D1F5 CA01 D35C 8100" /* .\..D.\. */ + $"0144 D1F5 C901 D35C 8100 0144 D0F5 C801" /* .D.\..D. */ + $"D25C 8100 0144 CFF5 C801 D15C 8100 0244" /* \..D.\..D */ + $"C7BA F3BB 02BA C85B 8100 0144 C6F5 BA01" /* Ǻ.[..D. */ + $"C75B 8100 0144 CAF5 C001 CC5C 8100 0144" /* [..D.\..D */ + $"CEF5 C501 CF5C 8100 0144 D1F5 CA01 D35C" /* .\..D.\ */ + $"8100 0144 D5F5 CF01 D75C 8100 0144 D9F5" /* ..D.\..D */ + $"D401 DB5D 8100 0146 DEF5 DA01 E05E 8100" /* .]..F.^. */ + $"022A B5B8 F4B7 01BF 5382 0000 07F5 0801" /* .*.S..... */ + $"0904 FF00 FF00 FF00 FF00 FF00 FF00 FF00" /* ........ */ + $"FF00 FF00 FF00 FF00 FF00 E700 FF00 FF00" /* ........ */ + $"FF00 FF00 FF00 FF00 FF00 FF00 FF00 FF00" /* ........ */ + $"FF00 FF00 EE00 0004 A008 0007 D500 0409" /* .......... */ + $"5399 BAC1 A0C0 01A2 48D2 0004 2FB1 E3E2" /* S.H../ */ + $"DFA1 DE03 E2E0 7601 CF00 0230 C9DC A4D5" /* ߡ.v...0ܤ */ + $"03D4 D6DD 60CE 0002 0EB0 D5A7 CF02 D1CB" /* .`...է. */ + $"2BCD 0001 5BD3 A9C9 02D0 9905 CB00 0202" /* +..[ө.Й.... */ + $"99C9 AAC4 01CB 4FCB 0002 0EAE C0AA BE02" /* ɪ.O.... */ + $"C1AE 16CA 0002 13AD BAAB B802 C07F 02C9" /* ....... */ + $"0002 13A8 B5AC B302 B964 03C8 0002 12A2" /* ....d.... */ + $"AFAC AD04 AEB2 7F3B 26BA 2502 2415 0186" /* ..;&%.$.. */ + $"0002 129D AAAE A801 ADAD BCA8 03A0 8240" /* .....@ */ + $"0384 0002 1198 A3EF A204 A3A7 A973 0D83" /* .....s. */ + $"0002 1192 9D81 9CEC 9B04 9C9C A474 0782" /* ....t. */ + $"0007 108D 9897 9594 969A EA9D 059C 9895" /* .... */ + $"959E 4982 0008 0F88 928F 9AB4 CDDD E4E9" /* I... */ + $"E506 E2D5 B99E 9282 0B81 0005 0F82 8DB1" /* .չ.... */ + $"E1F9 EEFF 04F6 D2A6 8D28 8100 030D 7EBD" /* .Ҧ(...~ */ + $"F6F1 FF03 FDDB A137 8100 020A A0F6 F3FF" /* .ۡ7..  */ + $"02FE CD3B 8100 0116 D1F5 FF01 ED46 8100" /* .;....F. */ + $"0136 F1F5 FF01 F852 8100 0143 F7F5 FF01" /* .6.R..C. */ + $"FB5A 8100 0243 F6FF F3FE 02FF FB5F 8100" /* Z..C._. */ + $"0243 F5FF F3FD 02FE FA5F 8100 0243 F5FE" /* .C._..C */ + $"B6FC 82FD B5FC 02FD F95F 8100 0243 F4FD" /* ._..C */ + $"B0FB 0FFC FDFF FFFE FBF7 F5F5 F6F8 FBFE" /* . */ + $"FFFF FDB0 FB02 FCF8 5F81 0002 43F4 FDAE" /* ._..C */ + $"FB14 FDFF F9EB D8C6 B7AE A8A5 A4A5 A9B0" /* .Ʒ */ + $"BBCA DDEF FCFF FDAD FB02 FCF8 5F81 0002" /* ._.. */ + $"43F3 FCAB FA08 FBFE FAE6 C6AC 9D97 9785" /* C.Ƭ */ + $"9809 9797 98A0 B2CF EDFD FDFB AAFA 02FB" /* Ɨ. */ + $"F85F 8100 0243 F3FB A9F9 1DFA FDF7 D8B1" /* _..C.ر */ + $"9B97 9999 9797 9A9E A2A5 A6A5 A19D 9997" /* */ + $"9899 9897 9FBA E3FB FCA9 F902 FAF7 5F81" /* ._ */ + $"0002 43F3 FBA7 F922 F8FB FADA AC98 9899" /* ..C"ڬ */ + $"979B A9BD D1E1 EBF0 F3F4 F2EF E8DD CCB7" /* ̷ */ + $"A499 989A 979A B7E6 FCFA F8A6 F902 FAF7" /* . */ + $"5F81 0002 43F2 FAA7 F802 FBEB B780 991C" /* _..C.뷀. */ + $"98A5 C4E2 F5FB FCFA F9F9 F8F8 F9F9 FAFB" /* */ + $"FCFA F1DB BB9F 979A 979D C6F4 FAA6 F802" /* ۻ. */ + $"F9F6 5F81 0002 43F1 F8A6 F70A FAD6 A197" /* _..C֡ */ + $"9A97 A7D0 F1FB F98B F70B F8FA FAEA C4A0" /* .Ġ */ + $"989A 97AC E5FA A5F7 02F8 F55F 8100 0243" /* ._..C */ + $"F0F7 A4F6 0AF7 F6C5 999A 989E C8F1 FAF7" /* ř */ + $"90F6 09F8 FAE9 BB99 9998 9FD6 F9A4 F602" /* 黙. */ + $"F7F5 5F81 0002 43F0 F6A3 F509 F6F3 BB97" /* _..C */ + $"9A97 AEE4 F9F6 94F5 08F7 F7D7 A398 9A9A" /* .ף */ + $"CEF8 A3F5 02F6 F45F 8100 0243 EFF5 A2F4" /* ._..C */ + $"08F5 F3B8 979A 98BF F2F7 98F4 07F8 E9AF" /* .󸗚. */ + $"989A 99CC F7A2 F402 F5F3 5E81 0002 43EF" /* .^..C */ + $"F5A2 F407 F5BD 979A 99C9 F6F5 82F4 03F7" /* .. */ + $"F4F4 F791 F407 F6F1 B797 9A99 D1F7 A1F4" /* .񷗚 */ + $"02F5 F35E 8100 0243 EEF4 A1F3 07F6 C897" /* .^..C.ȗ */ + $"9A99 CBF6 F481 F305 F4F2 CAAC ADCC 92F3" /* .ʬ̒ */ + $"07F5 F2B8 989A 9CDC F6A0 F302 F4F2 5E81" /* .򸘚.^ */ + $"0002 43EE F3A0 F206 F5DA 9B9A 98C6 F583" /* ..C.ڛ */ + $"F207 F4C1 9699 9996 C6F5 91F2 07F3 F0B3" /* .. */ + $"9899 A5EA F49F F202 F3F2 5E81 0002 43ED" /* .^..C */ + $"F29F F107 F2EB A699 98B9 F3F2 82F1 09F2" /* .릙 */ + $"E8A0 9A9B 9B9A A2EB F291 F106 F3EA A799" /* 蠚.꧙ */ + $"97B8 F39F F102 F2F1 5E81 0002 43EC F19F" /* .^..C */ + $"F006 F3BF 9799 A8EB F283 F002 F2E3 9C80" /* .󿗙.㜀 */ + $"9B03 9A9F E6F1 84F0 04F1 F3F1 F2F3 86F0" /* .. */ + $"06F3 DC9C 9A99 D4F4 9EF0 02F1 F05E 8100" /* .ܜ.^. */ + $"0243 EBF0 9EEF 06F2 DE9C 9A9B D8F3 84EF" /* .C.ޜ */ + $"09F0 EDAA 989B 9B97 AEEF F083 EF07 F0EF" /* . */ + $"CBB0 B2D1 F0F0 85EF 06F3 C498 99A8 ECF0" /* ˰.Ę */ + $"9DEF 02F0 EF5E 8100 0243 EBEF 9EEE 05F0" /* .^..C. */ + $"B598 98BA F186 EE07 F1DC A59A 9AA7 DFF0" /* .ܥ */ + $"84EE 07F0 C096 9898 97CB F185 EE06 EFEB" /* .. */ + $"A899 98CB F29D EE02 EFEF 5E81 0002 43EB" /* .^..C */ + $"EF9D EE07 F0DD 9C9A 9FE2 EFED 85EE 07ED" /* .ݜ. */ + $"F1DA 9AA1 E4F0 ED83 EE08 EFE3 9E9A 9B9B" /* ښ.㞚 */ + $"99A6 EA85 EE06 EDF1 D099 99A7 EA9D EE02" /* .Й. */ + $"EFEF 5E81 0002 43EA EE9D ED05 F0BC 9798" /* ^..C.𼗘 */ + $"BDF0 88ED 05EF DD9A A3E8 EE84 ED01 EFDD" /* .ݚ. */ + $"819B 039A A0E5 EE86 ED05 EBAA 9998 D1F0" /* ..몙 */ + $"9CED 02EE EE5E 8100 0243 EAEE 9CED 06EE" /* .^..C. */ + $"E6A3 9A9C DDEF 88ED 05EF DC9A A3E7 EE85" /* 棚.ܚ */ + $"ED06 E8A6 989B 9B97 AF88 ED05 F0CA 9898" /* .覘.ʘ */ + $"B3EE 9CED 02EE EE5E 8100 0243 E9ED 9CEC" /* .^..C */ + $"05EF D199 99AF ED89 EC05 EEDB 9AA3 E7ED" /* .љ.ۚ */ + $"85EC 07EE D4A1 9A9A A5DC EE87 EC06 EDE3" /* .ԡ. */ + $"A09A 9FE2 ED9B EC02 EDED 5E81 0002 43E9" /* .^..C */ + $"EC9C EB05 EEB9 9898 C9EE 89EB 05ED DA9A" /* ..ښ */ + $"A3E6 EC86 EB05 EFD2 99A2 E1EE 89EB 05ED" /* .ҙ. */ + $"B498 98CE EE9B EB02 ECEC 5E81 0002 43E8" /* .^..C */ + $"EB9C EA05 E8A7 999C DCEC 89EA 05EC D99A" /* .觙.ٚ */ + $"A3E5 EB86 EA05 ECD8 99A5 E7EB 89EA 05ED" /* .ؙ. */ + $"CA98 98BA ED9B EA02 EBEC 5E81 0002 43E7" /* ʘ.^..C */ + $"EA9B E905 EADD 9D99 A7E7 8AE9 05EB D99A" /* .ݝ.ٚ */ + $"A3E7 EB86 E905 EBD6 99A5 E6EA 89E9 05EB" /* .֙. */ + $"DA9C 99AA E89B E902 EAEB 5E81 0002 43E7" /* ڜ.^..C */ + $"E99B E805 EBD1 9998 B4EA 8AE8 07EA D89B" /* .љ.؛ */ + $"9DC8 E5EB E984 E805 EAD5 99A4 E5E9 89E8" /* .ՙ */ + $"06E9 E4A4 99A1 E1E9 9AE8 02E9 EA5E 8100" /* .䤙.^. */ + $"0243 E6E8 9BE7 05EA C698 98C0 EA8A E709" /* .C.Ƙ */ + $"E9D7 9B9A 98A6 C5E1 EAE8 82E7 05E9 D499" /* כ.ԙ */ + $"A4E4 E88A E705 E8AC 999C D9E9 9AE7 02E8" /* .謙. */ + $"E95E 8100 0243 E5E7 9BE6 05E9 BE98 98C8" /* ^..C.龘 */ + $"E98A E60B E8D6 9B9C 9C97 97A2 BFDD E9E8" /* .֛ */ + $"80E6 05E8 D499 A4E3 E78A E605 E9B4 9899" /* .ԙ.鴘 */ + $"D1E9 9AE6 02E7 E95E 8100 0243 E5E7 9BE6" /* .^..C */ + $"05E9 B998 99CE E98A E614 E8D6 9AA2 D2B8" /* .鹘.֚Ҹ */ + $"9E97 979F BAD9 E8E8 E6E8 D499 A4E3 E78A" /* ԙ */ + $"E605 E9BA 9898 CCE9 9AE6 02E7 E95E 8100" /* .麘.^. */ + $"0243 E4E6 9BE5 05E8 B698 99D0 E88A E514" /* .C.趘. */ + $"E7D5 9AA2 E2E9 DABC A197 979D B5D4 E7EA" /* ՚ڼ */ + $"D499 A4E2 E68A E505 E8BD 9898 C9E8 9AE5" /* ԙ.轘 */ + $"02E6 E85E 8100 0243 E4E5 9BE4 05E7 B698" /* .^..C.綘 */ + $"99CF E78A E414 E6D5 9AA2 DFE6 E6E8 DDC1" /* .՚ */ + $"A398 969B AFD1 D299 A4E1 E58A E405 E7BC" /* ҙ. */ + $"9898 C9E7 9AE4 02E5 E75E 8100 0243 E4E4" /* .^..C */ + $"9BE3 05E6 B798 99CC E68A E314 E5D5 9AA2" /* .淘.՚ */ + $"DEE4 E3E3 E5E7 DFC5 A799 979A A49A A4E0" /* ŧ */ + $"E48A E305 E7B9 9899 CAE6 9AE3 02E4 E65E" /* .繘.^ */ + $"8100 0243 E3E4 9BE3 05E6 BC98 98C6 E68A" /* ..C.漘 */ + $"E305 E5D4 9AA2 DEE4 82E3 09E6 E0C9 AB99" /* .Ԛɫ */ + $"989A A4E0 E48A E305 E5B4 9899 CEE5 9AE3" /* .崘 */ + $"02E4 E65E 8100 0243 E2E3 9BE2 05E5 C398" /* .^..C.Ø */ + $"98BD E58A E205 E4D3 9AA2 DDE3 84E2 07E4" /* .Ӛ. */ + $"E2CE AD99 A4DF E38B E204 AC99 9BD4 E39A" /* έ. */ + $"E202 E3E6 5D81 0002 43E2 E29B E105 E3CC" /* .]..C. */ + $"9998 B2E3 8AE1 05E3 D29A A2DC E286 E105" /* .Қ. */ + $"E5D1 99A3 DEE2 8AE1 05DD A399 A0DA E29A" /* љ.ݣ */ + $"E102 E2E5 5D81 0002 43E1 E19B E005 E1D5" /* .]..C. */ + $"9D99 A6DE 8AE0 05E2 D29A A2DB E186 E004" /* ފ.Қ. */ + $"E2CF 99A3 DD8A E005 E1D4 9C99 A8DF 9BE0" /* ϙ݊.Ԝߛ */ + $"02E1 E45D 8100 0243 E0E0 9CDF 05DD A599" /* .]..C.ݥ */ + $"9DD4 E089 DF05 E1D1 9AA2 DBE0 86DF 04E1" /* .њ. */ + $"CE99 A3DC 8ADF 05E2 C498 98B4 E19B DF02" /* Ι܊.Ę. */ + $"E0E3 5D81 0002 43DF DF9C DE05 E0B3 9898" /* ]..Cߜ.೘ */ + $"C2E1 89DE 05E1 CE9A A0D7 E086 DE04 E0CE" /* .Κ. */ + $"99A3 DB8A DE05 E0B1 9999 C5E1 9BDE 02DF" /* ۊ.ౙ. */ + $"E35D 8100 0243 DFDF 9CDE 05E1 C799 99AD" /* ]..Cߜ.Ǚ */ + $"DF88 DE07 E0D0 A39A 9AA5 D2DF 85DE 04E0" /* ߈.У߅. */ + $"CE99 A3DB 89DE 06DF D8A0 9A9E D5DF 9BDE" /* Ιۉ.ؠߛ */ + $"02DF E35D 8100 0243 DFDE 9CDD 06DE D7A0" /* .]..Cޜ.נ */ + $"9A9C D2DF 86DD 07DE DCA7 989B 9B98 AA86" /* ߆.ܧ */ + $"DD04 DFCD 99A3 DA89 DD05 E0C2 9899 ADDE" /* .͙ډ.˜ */ + $"9DDD 01E2 5D81 0002 43DE DD9D DC05 DFB4" /* .]..Cݝ.ߴ */ + $"9898 B7DF 86DC 02DD D29C 809B 039A 9ED5" /* ߆.Ҝ. */ + $"DD84 DC04 DECC 99A3 DA88 DC06 DDDB A899" /* ݄.̙ڈ.ۨ */ + $"99C5 DF9D DC01 E15D 8100 0243 DDDC 9DDB" /* ߝ.]..Cܝ */ + $"06DD CD9B 9A9F D3DD 85DB 09DC D49E 9A9B" /* .͛݅Ԟ */ + $"9B9A A1D7 DC84 DB05 DFCA 99A2 D7DD 87DB" /* ܄.ʙ݇ */ + $"06DE C699 9AA3 D8DC 9DDB 01E0 5D81 0002" /* .ƙܝ.].. */ + $"43DD DB9E DA05 DCAE 9998 B4DD 86DA 07DC" /* C۞.ܮ݆. */ + $"B697 9999 97BA DC84 DA07 DCCE A49A 9BA9" /* ܄.Τ */ + $"D3DC 85DA 06DB D8A6 9A98 BEDD 9EDA 01E0" /* ܅.ئݞ. */ + $"5D81 0002 43DD DB9E DA07 DBCB 9B9A 9BCA" /* ]..C۞.˛ */ + $"DCD9 85DA 05D8 BBA7 A7BD D985 DA07 D8A6" /* م.ػم.ئ */ + $"989B 9B98 AEDB 84DA 06D9 DCBB 989A A3D6" /* ۄ.ܻ */ + $"9FDA 01E0 5D81 0001 44DB A0D9 07DA B398" /* .]..D۠.ڳ */ + $"99A6 D6D9 D883 D907 D8D9 DBD8 D8DB D9D8" /* ؃. */ + $"83D9 01DA CD81 9B02 9A9F D383 D908 D8DB" /* .́.Ӄ. */ + $"CC9D 9A99 C2DB D89E D901 DF5D 8100 0144" /* ̝؞.]..D */ + $"DBA0 D806 D9D2 A29A 98B2 D992 D802 D9CF" /* ۠.Ңْ. */ + $"9C80 9B02 9AA1 D483 D805 D9D4 A599 99AD" /* .ԃ.ԥ */ + $"A1D8 01DE 5D81 0001 44DB A1D7 06D9 C49A" /* .]..Dۡ.Ě */ + $"9B99 BBD9 92D7 07D8 B097 9A9A 97B8 D982" /* ْ.ذق */ + $"D707 D8D7 AD99 9AA1 D0D8 A0D7 01DD 5D81" /* .׭ؠ.] */ + $"0001 44DB A2D7 06D9 B799 9B9A BED9 91D7" /* ..Dۢ.ٷّ */ + $"06D8 D4B5 A3A4 B9D6 82D7 07D8 D7B1 989A" /* .Եւ.ױ */ + $"9BC5 D9A1 D701 DD5D 8100 0144 DAA4 D605" /* ١.]..Dڤ. */ + $"AF98 9A9B BCD8 92D6 03D8 D4D4 D882 D607" /* ؒ.؂. */ + $"D7D5 B099 9B99 BCD8 A2D6 01DC 5D81 0001" /* հآ.].. */ + $"44D9 A3D5 08D6 D3AC 999A 9AB5 D4D6 98D5" /* D٣.Ӭ֘ */ + $"07D7 CFAA 999B 99B8 D7A3 D501 DC5D 8100" /* .Ϫף.]. */ + $"0144 D8A4 D409 D5D2 AD99 9B99 AACB D6D5" /* .Dؤҭ */ + $"94D4 08D5 D6C4 A299 9A9A B9D5 A4D4 01DB" /* .Ģդ. */ + $"5D81 0001 44D8 A5D3 0AD4 D2B2 999A 999E" /* ]..DإҲ */ + $"BAD1 D5D4 90D3 09D4 D5CD B19B 9A9A 9CBD" /* Ԑͱ */ + $"D5A5 D301 DA5D 8100 0144 D7A6 D20B D3D4" /* ե.]..Dצ. */ + $"BC9E 999A 99A4 BED0 D5D3 8BD2 0BD3 D4D4" /* Ӌ. */ + $"CCB6 9F99 9A99 A3C5 D4A6 D201 D95D 8100" /* ̶Ԧ.]. */ + $"0144 D6A8 D10A D3C8 A999 9A9A 99A3 B6C7" /* .D֨ȩ */ + $"D180 D384 D280 D30A CFC3 B09F 999A 999B" /* рӄҀð */ + $"B1CE D3A7 D101 D95D 8100 0144 D6A9 D020" /* ӧ.]..D֩ */ + $"D2D0 BCA3 999A 9A99 9CA5 B1BD C5CA CDCE" /* м */ + $"CFCE CDC9 C3BA AEA2 9A99 9A99 9AA9 C3D2" /* ú */ + $"D1A8 D001 D85C 8100 0144 D6AB D01C D2CE" /* Ѩ.\..D֫. */ + $"BBA5 9A99 9A9A 9999 9B9E A1A2 A3A2 A09D" /* */ + $"9A99 999A 9A99 9CAA C1D0 D2AA D001 D85C" /* Ҫ.\ */ + $"8100 0144 D5AC CF06 D0D2 CFC2 B0A2 9B80" /* ..Dլ.° */ + $"9984 9A80 9906 9CA5 B5C6 D0D1 D0AB CF01" /* .Ы. */ + $"D75C 8100 0144 D5AF CF14 D0D1 CDC4 B9AF" /* \..Dկ.Ĺ */ + $"A8A3 A09F 9E9F A1A4 A9B1 BCC7 CED1 D0AE" /* Ю */ + $"CF01 D65C 8100 0144 D4B2 CE05 CFD0 D0CE" /* .\..DԲ. */ + $"CCCA 80C9 05CB CDCF D0D0 CFB1 CE01 D65C" /* ʀ.ϱ.\ */ + $"8100 0144 D3F5 CD01 D65C 8100 0144 D3F5" /* ..D.\..D */ + $"CC01 D55C 8100 0144 D2F5 CB01 D45C 8100" /* .\..D.\. */ + $"0144 D1F5 CA01 D35C 8100 0144 D1F5 C901" /* .D.\..D. */ + $"D35C 8100 0144 D1F5 C901 D35C 8100 0144" /* \..D.\..D */ + $"D0F5 C801 D25C 8100 0144 CFF5 C701 D15C" /* .\..D.\ */ + $"8100 0144 CFF5 C701 D05C 8100 0244 C6B8" /* ..D.\..DƸ */ + $"F3B9 02B8 C75B 8100 0244 C5B8 F3B9 02B8" /* .[..DŸ. */ + $"C65B 8100 0144 C9F5 BF01 CB5C 8100 0144" /* [..D.\..D */ + $"CDF5 C401 CF5C 8100 0144 D1F5 C901 D35C" /* .\..D.\ */ + $"8100 0144 D4F5 CE01 D65C 8100 0144 D8F5" /* ..D.\..D */ + $"D301 DA5D 8100 0146 DEF5 D901 E05E 8100" /* .]..F.^. */ + $"022A B5B7 F4B6 01BE 5382 0000 07F5 0801" /* .*.S..... */ + $"0904 FF00 FF00 FF00 FF00 FF00 FF00 FF00" /* ........ */ + $"FF00 FF00 FF00 FF00 FF00 E700 FF00 FF00" /* ........ */ + $"FF00 FF00 FF00 FF00 FF00 FF00 FF00 FF00" /* ........ */ + $"FF00 FF00 EE00 0004 A008 0007 D500 0409" /* .......... */ + $"549A BAC1 9EC0 03C1 C0A3 48D2 0004 2FB2" /* T.H../ */ + $"E3E3 DFA1 DE03 E2E1 7601 CF00 0230 C9DC" /* ߡ.v...0 */ + $"A5D5 02D6 DD60 CE00 020E B0D6 A7CF 02D1" /* .`...֧. */ + $"CB2B CD00 025B D3C9 A7CA 03C9 D09A 05CB" /* +..[ɧ.К. */ + $"0002 0299 CAAA C401 CB4F CB00 020E AEC1" /* ...ʪ.O... */ + $"AABE 02C1 AE16 CA00 0213 ADBB ABB9 02C0" /* ...... */ + $"7F02 C900 0213 A8B5 ACB3 02B9 6503 C800" /* ......e.. */ + $"0212 A3B0 ADAE 03B3 803B 26BA 2502 2415" /* ...;&%.$. */ + $"0186 0002 129E AAAE A802 ADAD A9BB A803" /* ...... */ + $"A083 4003 8400 0211 98A4 AFA2 BDA3 04A4" /* @..... */ + $"A7A9 730E 8300 0211 939E 819D ED9C 039D" /* s..... */ + $"A575 0782 0007 108E 9897 9694 969B EA9E" /* u.... */ + $"059D 9995 959F 4A82 0008 0F89 928F 9AB4" /* .J... */ + $"CDDD E4E9 E506 E2D5 B99E 9283 0B81 0005" /* .չ... */ + $"0F82 8EB1 E1F9 EEFF 04F6 D2A7 8E28 8100" /* ..ҧ(. */ + $"030D 7FBD F6F1 FF03 FDDB A237 8100 020A" /* ....ۢ7.. */ + $"A0F6 F3FF 02FE CD3B 8100 0116 D1F5 FF01" /* .;.... */ + $"ED46 8100 0136 F1F5 FF01 F852 8100 0143" /* F..6.R..C */ + $"F7F5 FF01 FB5A 8100 0243 F6FF F3FE 02FF" /* .Z..C. */ + $"FB5F 8100 0243 F5FF F3FD 02FE FA5F 8100" /* _..C._. */ + $"0243 F5FE B6FC 81FD B6FC 02FD F95F 8100" /* .C._. */ + $"0243 F5FE B1FC 05FD FEFF FEFC FA80 F805" /* .C.. */ + $"FAFC FEFF FEFD B0FC 02FD F95F 8100 0243" /* ._..C */ + $"F4FD ADFB 15FC FDFE FAF1 E5D9 D0CA C6C4" /* . */ + $"C4C5 C7CB D2DC E8F4 FCFE FCAD FB02 FCF8" /* . */ + $"5F81 0002 43F3 FCAB FA08 FBFD FAED DAC9" /* _..C. */ + $"BFBC BB86 BC07 BBBC C1CD DFF2 FCFC ABFA" /* . */ + $"02FB F85F 8100 0243 F3FB A9F9 1DFA FCF8" /* ._..C. */ + $"E5CC BEBB BCBD BCBC BDC0 C2C4 C5C4 C2BF" /* ̾¿ */ + $"BDBC BCBD BCBB C1D2 EBFA FBA9 F902 FAF7" /* . */ + $"5F81 0002 43F3 FBA7 F90F F8FA F9E5 C9BC" /* _..C.ɼ */ + $"BCBD BCBE C7D3 E0EA F0F3 80F5 0FF3 EEE7" /* . */ + $"DDD0 C4BD BCBD BCBE D0ED FBF9 F8A6 F902" /* Ľ. */ + $"FAF7 5F81 0002 43F2 FAA7 F80E FAF0 D0BC" /* _..C.м */ + $"BCBD BCC4 D7EA F6FA FAF9 F982 F80E F9FA" /* . */ + $"FAF9 F4E6 D2C1 BCBD BCBF D9F5 F9A6 F802" /* . */ + $"F9F6 5F81 0002 43F1 F8A6 F70A F8E3 C2BC" /* _..C¼ */ + $"BDBC C5DE F3F9 F88C F70A F9F9 EFD7 C1BC" /* */ + $"BDBB C9EC F9A5 F702 F8F5 5F81 0002 43F0" /* ._..C */ + $"F7A6 F607 D8BD BDBC C0DA F3F8 91F6 09F7" /* .ؽ */ + $"F8EE D1BD BDBC C1E3 F8A4 F602 F7F5 5F81" /* ѽ._ */ + $"0002 43F0 F6A3 F509 F6F4 D2BC BEBC CAEB" /* ..CҼ */ + $"F7F6 94F5 08F6 F7E3 C3BC BDBE DDF7 A3F5" /* .ü */ + $"02F6 F45F 8100 0243 F0F6 A3F5 07F4 D0BC" /* ._..C.м */ + $"BEBC D4F3 F698 F507 F7EE CABC BDBD DCF7" /* .ʼ */ + $"A2F5 02F6 F45F 8100 0243 EFF5 A2F4 07F5" /* ._..C. */ + $"D2BC BEBD DAF5 F582 F403 F6F4 F4F6 91F4" /* Ҽ. */ + $"07F5 F2CF BCBE BDDF F6A1 F402 F5F3 5E81" /* .ϼ.^ */ + $"0002 43EE F4A1 F306 F5D9 BCBE BDDB F583" /* ..C.ټ */ + $"F304 F2DA C9C9 DC92 F307 F4F2 CFBC BDBF" /* .ܒ.ϼ */ + $"E5F5 A0F3 02F4 F25E 8100 0243 EEF3 A0F2" /* .^..C */ + $"06F4 E4BE BEBC D8F4 83F2 07F3 D5BB BDBC" /* .侾.ջ */ + $"BBD8 F491 F207 F3F1 CCBC BDC4 EDF3 9FF2" /* .̼ */ + $"02F3 F25E 8100 0243 EDF2 9FF1 07F2 EDC5" /* .^..C. */ + $"BDBC D0F2 F282 F109 F2EC C1BD BEBE BDC2" /* */ + $"EEF2 91F1 06F2 EDC5 BDBC CFF2 9FF1 02F2" /* .Ž. */ + $"F15E 8100 0243 ECF1 9FF0 06F2 D3BC BDC6" /* ^..C.Ӽ */ + $"EDF1 83F0 02F1 E8BF 81BE 02C0 EAF1 85F0" /* .迁. */ + $"03F2 F1F1 F286 F006 F2E4 BFBE BDDF F29E" /* ..俾 */ + $"F002 F1F0 5E81 0002 43EB F09E EF06 F1E5" /* .^..C. */ + $"BFBE BEE2 F184 EF07 F0EE C7BC BEBE BCC9" /* .Ǽ */ + $"85EF 06F0 EFDA CACB DEF0 86EF 06F2 D6BC" /* ..ּ */ + $"BDC6 EDF0 9DEF 02F0 EF5E 8100 0243 EBEF" /* .^..C */ + $"9EEE 05F0 CDBC BCD0 F086 EE07 F0E4 C4BE" /* .ͼ.ľ */ + $"BDC5 E5EF 84EE 02F0 D3BB 80BC 01DA F085" /* .ӻ. */ + $"EE06 EFEC C5BD BCDA F09D EE02 EFEF 5E81" /* .Ž.^ */ + $"0002 43EB EF9D EE06 EFE4 BEBD C0E7 EF87" /* ..C.侽 */ + $"EE05 F0E2 BDC2 E9EF 84EE 02EF E8C0 80BE" /* .. */ + $"02BD C4EC 86EE 05F0 DDBC BDC5 EC9D EE02" /* ..ݼ. */ + $"EFEF 5E81 0002 43EB EF9D EE05 EFD1 BCBC" /* ^..C.Ѽ */ + $"D1F0 88EE 04EF E4BD C3EB 85EE 01EF E482" /* .. */ + $"BE01 C1E9 87EE 05ED C6BD BCDD EF9C EE02" /* ..ƽ. */ + $"EFEF 5E81 0002 43EA EE9D ED05 E9C2 BDBF" /* ^..C.½ */ + $"E4EE 88ED 04EE E3BD C3EA 86ED 06EA C4BD" /* ..Ľ */ + $"BEBE BCC9 88ED 05EF D9BC BCCC EE9C ED02" /* Ɉ.ټ. */ + $"EEEE 5E81 0002 43E9 ED9C EC04 EEDC BDBD" /* ^..C.ܽ */ + $"C98A EC04 EDE2 BDC2 E986 EC07 EDDE C1BE" /* Ɋ.. */ + $"BEC4 E3ED 88EC 04E7 C1BD C1E6 9CEC 02ED" /* .. */ + $"ED5E 8100 0243 E9EC 9CEB 05ED CFBC BCD8" /* ^..C.ϼ */ + $"ED89 EB04 ECE2 BDC2 E887 EB05 EDDD BDC2" /* ..ݽ */ + $"E6EC 89EB 05EC CCBC BCDB ED9B EB02 ECEC" /* .̼. */ + $"5E81 0002 43E8 EB9C EA05 E9C5 BDBF E2EB" /* ^..C.Ž */ + $"89EA 04EB E1BD C2E7 87EA 04EB DFBD C3E8" /* ..߽ */ + $"8AEA 05EC D8BC BCCF EC9B EA02 EBEC 5E81" /* .ؼ.^ */ + $"0002 43E7 EA9B E905 EAE3 BFBD C5E8 8AE9" /* ..C.㿽 */ + $"04EA E0BD C2E7 87E9 04EA DEBD C3E7 8AE9" /* ..޽ */ + $"04EA E1BF BDC6 9CE9 02EA EB5E 8100 0243" /* .´Ɯ.^..C */ + $"E7E9 9BE8 05E9 DCBD BCCC E98A E807 E9DF" /* .ܽ. */ + $"BEBF D7E6 EAE9 84E8 04E9 DEBD C3E6 8BE8" /* .޽ */ + $"05E6 C3BD C1E4 E99A E802 E9EA 5E81 0002" /* .ý.^.. */ + $"43E6 E89B E705 E9D5 BCBC D2E9 8AE7 09E8" /* C.ռ */ + $"DEBE BEBD C4D5 E4E9 E882 E704 E8DD BDC3" /* ޾.ݽ */ + $"E58C E704 C8BD BEDF E89A E702 E8E9 5E81" /* .Ƚ.^ */ + $"0002 43E6 E89B E705 E9D1 BCBC D7E9 8AE7" /* ..C.Ѽ */ + $"0BE8 DEBE BFBE BCBC C2D2 E2E9 E880 E704" /* .޾. */ + $"E8DD BDC3 E58B E705 E8CC BCBD DCE8 9AE7" /* ݽ.̼ */ + $"02E8 E95E 8100 0243 E5E7 9BE6 05E8 CEBC" /* .^..C.μ */ + $"BDD9 E88A E613 E7DE BDC2 DBCD C0BC BCC0" /* .޽ */ + $"CFDF E7E7 E6E7 DCBD C3E4 8BE6 05E8 CFBC" /* ܽ.ϼ */ + $"BDD9 E89A E602 E7E9 5E81 0002 43E4 E69B" /* .^..C */ + $"E505 E6CC BCBD DAE6 8AE5 13E6 DDBD C2E3" /* .̼.ݽ */ + $"E7DF CFC1 BCBC BFCC DCE6 E8DC BDC3 E38B" /* ܽ */ + $"E505 E7D0 BCBD D6E7 9AE5 02E6 E85E 8100" /* .м.^. */ + $"0243 E4E5 9BE4 05E5 CCBC BDD9 E58A E413" /* .C.̼. */ + $"E5DC BDC2 E1E4 E5E6 E1D2 C3BC BCBE C9DB" /* ܽü */ + $"DABD C3E2 8BE4 05E7 D0BC BDD6 E69A E402" /* ڽ.м. */ + $"E5E7 5E81 0002 43E4 E49B E305 E5CD BCBD" /* ^..C.ͼ */ + $"D7E4 8AE3 04E4 DBBD C2E1 80E3 0BE4 E5E1" /* .۽. */ + $"D4C4 BDBC BEC3 BDC3 E18B E305 E5CE BCBD" /* Ľý.μ */ + $"D7E4 9AE3 02E4 E65E 8100 0243 E3E4 9BE3" /* .^..C */ + $"05E5 CFBC BDD4 E48A E304 E4DB BDC2 E183" /* .ϼ.۽ */ + $"E308 E4E2 D6C6 BDBC BDC3 E18B E305 E4CB" /* .ƽ. */ + $"BDBD D8E4 9AE3 02E4 E65E 8100 0243 E2E3" /* .^..C */ + $"9BE2 05E3 D2BC BCCF E48A E204 E3DA BDC1" /* .Ҽ.ڽ */ + $"E085 E206 E3E2 D8C7 BDC2 E08C E204 C6BD" /* .ǽ.ƽ */ + $"BEDB E39A E202 E3E6 5D81 0002 43E2 E29B" /* .]..C */ + $"E105 E2D6 BDBD CAE2 8AE1 04E2 DABE C1DF" /* .ֽ.ھ */ + $"87E1 04E3 D9BD C2DF 8BE1 04DF C2BD C0DD" /* .ٽߋ.½ */ + $"9BE1 02E2 E55D 8100 0243 E1E1 9BE0 05E1" /* .]..C. */ + $"DBBF BDC4 DF8A E004 E1D9 BEC1 DE87 E004" /* ۿߊ.پއ. */ + $"E1D8 BDC2 DE8A E004 E1DA BEBD C49C E002" /* ؽފ.ھĜ. */ + $"E1E4 5D81 0002 43E1 E19C E005 DFC3 BDBF" /* ]..C.ý */ + $"DAE1 89E0 04E1 D9BE C1DE 87E0 04E1 D8BD" /* .پއ.ؽ */ + $"C2DE 8AE0 05E1 D3BD BDCB E19B E002 E1E4" /* ފ.ӽ. */ + $"5D81 0002 43E0 E09C DF05 E0CA BDBD D1E0" /* ]..C.ʽ */ + $"89DF 05E1 D7BE C0DC E086 DF04 E0D7 BDC2" /* .׾.׽ */ + $"DE8A DF05 E0C9 BDBD D3E0 9BDF 02E0 E35D" /* ފ.ɽ.] */ + $"8100 0243 DFDF 9CDE 05DF D3BD BDC6 DF88" /* ..Cߜ.ӽ߈ */ + $"DE07 DFD7 C2BE BEC3 D8DF 85DE 04DF D6BD" /* .¾߅.ֽ */ + $"C2DD 8ADE 05DB C0BD BFDA DF9B DE02 DFE3" /* ݊.ߛ. */ + $"5D81 0002 43DF DE9D DD05 DAC0 BDBF D8DE" /* ]..Cޝ. */ + $"88DD 05C4 BDBE BEBD C586 DD04 DED5 BDC2" /* .Ľņ.ս */ + $"DC89 DD05 DED1 BDBD C6DE 9DDD 01E2 5D81" /* ܉.ѽޝ.] */ + $"0002 43DE DD9D DC05 DDC9 BDBD CBDD 86DC" /* ..Cݝ.ɽ݆ */ + $"02DD D7BF 81BE 01BF D985 DC04 DDD5 BDC2" /* .׿.م.ս */ + $"DB8A DC04 C4BD BDD1 DD9D DC01 E15D 8100" /* ۊ.Ľݝ.]. */ + $"0243 DDDC 9DDB 06DC D5BE BEC0 D8DC 85DB" /* .Cܝ.վ܅ */ + $"02DC D8C0 81BE 01C0 D985 DB05 DDD4 BDC1" /* ..م.Խ */ + $"DADC 87DB 05DC D1BD BEC2 DA9E DB01 E05D" /* ܇.ѽڞ.] */ + $"8100 0243 DDDB 9EDA 05DB C6BD BDC9 DB86" /* ..C۞.ƽۆ */ + $"DA07 DBCA BCBD BDBC CCDB 84DA 07DB D5C2" /* .ʼۄ. */ + $"BEBE C4D7 DB85 DA06 DBDA C3BD BDCD DC9E" /* ۅ.ýܞ */ + $"DA01 E05D 8100 0243 DDDA 9ED9 01DA D380" /* .]..Cڞ.Ӏ */ + $"BE01 D3DA 85D9 07DA D9CC C3C3 CDD9 DA85" /* .څ.څ */ + $"D906 C3BD BEBE BCC6 DA85 D906 DBCC BDBD" /* .ýڅ.̽ */ + $"C2D8 DA9E D901 E05D 8100 0244 DCDA 9FD9" /* ڞ.]..Dڟ */ + $"05DA C8BD BDC3 D887 D903 DAD9 D9DA 85D9" /* .Ƚ؇.څ */ + $"01DA D482 BE01 C0D7 84D9 06DA D3BF BEBD" /* .Ԃ.ׄ.ӿ */ + $"CFDA 9FD9 01DF 5D81 0001 44DC A1D9 04D6" /* ڟ.]..Dܡ. */ + $"C1BE BDC8 94D9 01D5 BF81 BE01 C1D7 84D9" /* Ȕ.տ.ׄ */ + $"04D7 C3BD BDC6 A1D9 01DF 5D81 0001 44DB" /* .ýơ.]..D */ + $"A1D8 06D9 D0BE BEBD CCD9 93D8 06C7 BCBE" /* .оٓ.Ǽ */ + $"BDBC CAD9 83D8 05D7 C6BD BEC0 D5A1 D801" /* ك.ƽա. */ + $"DE5D 8100 0144 DBA2 D706 D8CA BDBE BECD" /* ]..Dۢ.ʽ */ + $"D892 D704 D6C9 C1C2 CB85 D705 C7BD BEBE" /* ؒ.˅.ǽ */ + $"D0D8 A1D7 01DD 5D81 0001 44DA A4D6 05C6" /* ء.]..Dڤ. */ + $"BDBE BECC D792 D603 D7D5 D5D7 84D6 05C7" /* ג.ׄ. */ + $"BDBE BDCC D7A2 D601 DC5D 8100 0144 D9A4" /* ע.]..D٤ */ + $"D505 D4C5 BDBE BDC8 9AD5 07D6 D3C4 BDBE" /* .ŽȚ.Ľ */ + $"BDCA D6A3 D501 DC5D 8100 0144 D8A5 D407" /* ֣.]..Dإ. */ + $"D3C5 BDBE BDC4 D1D5 96D4 07D5 CEC1 BDBE" /* ŽՖ. */ + $"BDC9 D5A4 D401 DB5D 8100 0144 D8A7 D307" /* դ.]..Dا. */ + $"C7BD BEBD BFCA D2D4 92D3 08D4 D1C6 BEBE" /* ǽԒ.ƾ */ + $"BDBE CBD4 A5D3 01DA 5D81 0001 44D7 A7D2" /* ԥ.]..Dק */ + $"0AD3 CABF BDBE BDC1 CBD1 D3D3 8CD2 0AD3" /* ʿӌ */ + $"D3D0 C8C0 BDBE BDC1 CDD3 A6D2 01D9 5D81" /* Ӧ.] */ + $"0001 44D7 A8D2 0CD3 CEC3 BDBE BEBD C1C8" /* ..Dר.ý */ + $"CED2 D3D3 85D2 80D3 0AD1 CDC6 BFBD BEBD" /* ӅҀƿ */ + $"BEC6 D1D3 A7D2 01D9 5D81 0001 44D6 A9D1" /* ӧ.]..D֩ */ + $"0ED2 D1CA C1BD BEBE BDBE C2C6 CACD CFD0" /* . */ + $"80D1 0ED0 CFCD C9C5 C1BE BDBE BDBE C3CD" /* . */ + $"D2D2 A8D1 01D9 5D81 0001 44D6 AAD0 07D1" /* Ҩ.]..D֪. */ + $"D1D0 C9C2 BEBD BE80 BD02 BEBF C080 C10C" /* ¾.. */ + $"C0BF BEBD BDBE BEBD BEC3 CBD1 D1AA D001" /* Ѫ. */ + $"D85C 8100 0144 D5AC CF06 D0D0 CFCB C5C0" /* \..Dլ. */ + $"BE80 BD80 BE00 BD80 BE80 BD05 BFC1 C7CC" /* .. */ + $"D0D0 ACCF 01D7 5C81 0001 44D5 B1CF 06CE" /* Ь.\..Dձ. */ + $"CBC7 C4C2 C1C0 80BF 06C0 C1C3 C5C8 CCCE" /* . */ + $"B0CF 01D6 5C81 0001 44D4 B6CE 01CD CD80" /* .\..DԶ.̀ */ + $"CC01 CDCD B5CE 01D6 5C81 0001 44D3 F5CD" /* .͵.\..D */ + $"01D6 5C81 0001 44D3 F5CC 01D5 5C81 0001" /* .\..D.\.. */ + $"44D2 F5CB 01D4 5C81 0001 44D2 F5CB 01D4" /* D.\..D. */ + $"5C81 0001 44D1 F5CA 01D3 5C81 0001 44D1" /* \..D.\..D */ + $"F5C9 01D3 5C81 0001 44D0 F5C8 01D2 5C81" /* .\..D.\ */ + $"0001 44CF F5C7 01D1 5C81 0001 44CF F5C7" /* ..D.\..D */ + $"01D0 5C81 0002 44C6 B9F3 BA02 B9C7 5B81" /* .\..Dƹ.[ */ + $"0001 44C5 F5B9 01C7 5B81 0001 44C9 F5BF" /* ..D.[..D */ + $"01CB 5C81 0001 44CD F5C4 01CF 5C81 0001" /* .\..D.\.. */ + $"44D1 F5C9 01D3 5C81 0001 44D4 F5CE 01D7" /* D.\..D. */ + $"5C81 0001 44D8 F5D4 01DB 5D81 0002 46DE" /* \..D.]..F */ + $"DAF4 D901 E05E 8100 012B B5F5 B701 BF53" /* .^..+.S */ + $"8200 0007 F508 0109 04FF 00FF 00FF 00FF" /* ......... */ + $"00FF 00FF 00FF 00FF 00FF 00FF 00FF 00FF" /* ........ */ + $"00E7 0074 386D 6B00 0040 0800 0000 0000" /* ..t8mk..@...... */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0508 0808 0808 0808 0808 0808" /* ................ */ + $"0808 0808 0808 0808 0808 0808 0808 0808" /* ................ */ + $"0808 0808 0808 0808 0700 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"000B 5FAD D3DB DBDB DBDB DBDB DBDB DBDB" /* .._ */ + $"DBDB DBDB DBDB DBDB DBDB DBDB DBDB DBDB" /* */ + $"DBDB DBDB DBDB DBDB D9B7 5300 0000 0000" /* ٷS..... */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"37CC FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* 7 */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FF8A 0100 0000" /* .... */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 003A" /* ...............: */ + $"EDFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF 7400 0000" /* t... */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 11D8" /* ............... */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF F636 0000" /* 6.. */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 73FF" /* ..............s */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFC3 0600" /* .. */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0003 C7FF" /* .............. */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF 6800" /* h. */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0013 E9FF" /* .............. */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF E71D" /* . */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 001B EFFF" /* .............. */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFAF" /* */ + $"0300 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 001B EFFF" /* .............. */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"8F04 0000 0000 0000 0000 0000 0000 0000" /* ............... */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 001B EFFF" /* .............. */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFBB 5738 3737 3737 3737 3737 3737 3737" /* W8777777777777 */ + $"3737 3737 3737 3737 3737 3737 3737 3737" /* 7777777777777777 */ + $"3737 3737 3737 3737 3737 3737 3737 3737" /* 7777777777777777 */ + $"3737 3737 3737 3737 3737 3737 3737 3737" /* 7777777777777777 */ + $"3734 1F01 0000 0000 0000 0000 001B EFFF" /* 74............ */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFE F2C4 6104 0000 0000 0000 001B EFFF" /* a......... */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFB3 1500 0000 0000 001B EFFF" /* ........ */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF BE0B 0000 0000 001B EFFF" /* ....... */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FF7D 0000 0000 001B EFFF" /* }...... */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFE5 1300 0000 001B EFFF" /* ...... */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 4900 0000 001B EFFF" /* I..... */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 6D00 0000 001A EFFF" /* m..... */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7400 0000 0021 F4FF" /* t....! */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0046 FFFF" /* s....F */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0056 FFFF" /* s....V */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0055 FFFF" /* s....U */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7300 0000 0058 FFFF" /* s....X */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF" /* */ + $"FFFF FFFF FFFF FFFF 7500 0000 0034 D6DB" /* u....4 */ + $"DBDB DBDB DBDB DBDB DBDB DBDB DBDB DBDB" /* */ + $"DBDB DBDB DBDB DBDB DBDB DBDB DBDB DBDB" /* */ + $"DBDB DBDB DBDB DBDB DBDB DBDB DBDB DBDB" /* */ + $"DBDB DBDB DBDB DBDB DBDB DBDB DBDB DBDB" /* */ + $"DBDB DBDB DBDB DBDB DBDB DBDB DBDB DBDB" /* */ + $"DBDB DBDB DBDB DBDB DBDB DBDB DBDB DBDB" /* */ + $"DBDB DBDB DBDB DBDB DBDB DBDB DBDB DBDB" /* */ + $"DBDB DBDB DBDB DBDB 6300 0000 0000 0708" /* c....... */ + $"0808 0808 0808 0808 0808 0808 0808 0808" /* ................ */ + $"0808 0808 0808 0808 0808 0808 0808 0808" /* ................ */ + $"0808 0808 0808 0808 0808 0808 0808 0808" /* ................ */ + $"0808 0808 0808 0808 0808 0808 0808 0808" /* ................ */ + $"0808 0808 0808 0808 0808 0808 0808 0808" /* ................ */ + $"0808 0808 0808 0808 0808 0808 0808 0808" /* ................ */ + $"0808 0808 0808 0808 0808 0808 0808 0808" /* ................ */ + $"0808 0808 0808 0809 0400 0000 0000 0000" /* ............... */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 0000 0000 0000" /* ................ */ + $"0000 0000 0000 0000 0000 00" /* ........... */ +}; + diff --git a/cmake/installer/installer-header.bmp b/cmake/installer/installer-header.bmp new file mode 100644 index 0000000000..7588a338b1 Binary files /dev/null and b/cmake/installer/installer-header.bmp differ diff --git a/cmake/installer/installer.ico b/cmake/installer/installer.ico new file mode 100644 index 0000000000..f6ec3c7108 Binary files /dev/null and b/cmake/installer/installer.ico differ diff --git a/cmake/installer/uninstaller-header.bmp b/cmake/installer/uninstaller-header.bmp new file mode 100644 index 0000000000..d43166ded3 Binary files /dev/null and b/cmake/installer/uninstaller-header.bmp differ diff --git a/cmake/macros/ConsolidateStackComponents.cmake b/cmake/macros/ConsolidateStackComponents.cmake deleted file mode 100644 index ca272f6485..0000000000 --- a/cmake/macros/ConsolidateStackComponents.cmake +++ /dev/null @@ -1,28 +0,0 @@ -macro(CONSOLIDATE_STACK_COMPONENTS) - - if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE) - if (WIN32) - # Copy all the output for this target into the common deployment location - add_custom_command( - TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory $ ${CMAKE_BINARY_DIR}/full-stack-deployment - ) - - # Copy icon files for interface and stack manager - if (TARGET_NAME STREQUAL "interface" OR TARGET_NAME STREQUAL "stack-manager") - if (TARGET_NAME STREQUAL "interface") - set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/icon/interface.ico") - set (ICON_DESTINATION_NAME "interface.ico") - elseif (TARGET_NAME STREQUAL "stack-manager") - set (ICON_FILE_PATH "${PROJECT_SOURCE_DIR}/assets/icon.ico") - set (ICON_DESTINATION_NAME "stack-manager.ico") - endif () - add_custom_command( - TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy ${ICON_FILE_PATH} ${CMAKE_BINARY_DIR}/full-stack-deployment/${ICON_DESTINATION_NAME} - ) - endif () - endif () - endif () - -endmacro() \ No newline at end of file diff --git a/cmake/macros/FixPathForNSIS.cmake b/cmake/macros/FixPathForNSIS.cmake new file mode 100644 index 0000000000..22ee538ed7 --- /dev/null +++ b/cmake/macros/FixPathForNSIS.cmake @@ -0,0 +1,16 @@ +# +# FixPathForNSIS.cmake +# +# Created by Sam Gateau on 1/14/16. +# Copyright 2016 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 +# + +macro(fix_path_for_nsis INITIAL_PATH RESULTING_PATH) + # replace forward slash with backslash + string(REPLACE "/" "\\\\" _BACKSLASHED_PATH ${INITIAL_PATH}) + # set the resulting path variable + set(${RESULTING_PATH} ${_BACKSLASHED_PATH}) +endmacro() diff --git a/cmake/macros/FixupInterface.cmake b/cmake/macros/FixupInterface.cmake new file mode 100644 index 0000000000..3e5ea7a3e2 --- /dev/null +++ b/cmake/macros/FixupInterface.cmake @@ -0,0 +1,58 @@ +# +# FixupInterface.cmake +# cmake/macros +# +# Copyright 2016 High Fidelity, Inc. +# Created by Stephen Birarda on January 6th, 2016 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +macro(fixup_interface) + if (APPLE) + + string(REPLACE " " "\\ " ESCAPED_BUNDLE_NAME ${INTERFACE_BUNDLE_NAME}) + string(REPLACE " " "\\ " ESCAPED_INSTALL_PATH ${INTERFACE_INSTALL_DIR}) + set(_INTERFACE_INSTALL_PATH "${ESCAPED_INSTALL_PATH}/${ESCAPED_BUNDLE_NAME}.app") + + # install QtWebProcess from Qt to the application bundle + # since it is missed by macdeployqt + # https://bugreports.qt.io/browse/QTBUG-35211 + set(LIBEXEC_PATH "${_INTERFACE_INSTALL_PATH}/Contents/libexec") + install( + PROGRAMS "${QT_DIR}/libexec/QtWebProcess" + DESTINATION ${LIBEXEC_PATH} + COMPONENT ${CLIENT_COMPONENT} + ) + + set(QTWEBPROCESS_PATH "\${CMAKE_INSTALL_PREFIX}/${LIBEXEC_PATH}") + + # we also need a qt.conf in the directory of QtWebProcess + install(CODE " + file(WRITE ${QTWEBPROCESS_PATH}/qt.conf + \"[Paths]\nPlugins = ../PlugIns\nImports = ../Resources/qml\nQml2Imports = ../Resources/qml\" + )" + COMPONENT ${CLIENT_COMPONENT} + ) + + find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS "${QT_DIR}/bin" NO_DEFAULT_PATH) + + if (NOT MACDEPLOYQT_COMMAND AND (PRODUCTION_BUILD OR PR_BUILD)) + message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin.\ + It is required to produce an relocatable interface application.\ + Check that the environment variable QT_DIR points to your Qt installation.\ + ") + endif () + + install(CODE " + execute_process(COMMAND ${MACDEPLOYQT_COMMAND}\ + \${CMAKE_INSTALL_PREFIX}/${_INTERFACE_INSTALL_PATH}/\ + -verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/\ + -executable=\${CMAKE_INSTALL_PREFIX}/${_INTERFACE_INSTALL_PATH}/Contents/libexec/QtWebProcess\ + )" + COMPONENT ${CLIENT_COMPONENT} + ) + + endif () +endmacro() diff --git a/cmake/macros/GenerateInstallers.cmake b/cmake/macros/GenerateInstallers.cmake index 570e24332b..4d7119d737 100644 --- a/cmake/macros/GenerateInstallers.cmake +++ b/cmake/macros/GenerateInstallers.cmake @@ -1,30 +1,78 @@ -# -# GenerateInstallers.cmake -# cmake/macros -# -# Created by Leonardo Murillo on 12/16/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 -# - -macro(GENERATE_INSTALLERS) - if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE AND WIN32) - file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/full-stack-deployment") - find_program(MAKENSIS_COMMAND makensis PATHS [HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\NSIS]) - if (NOT MAKENSIS_COMMAND) - message(FATAL_ERROR "The Nullsoft Scriptable Install Systems is required for generating packaged installers on Windows (http://nsis.sourceforge.net/)") - endif () - add_custom_target( - build-package ALL - DEPENDS interface assignment-client domain-server stack-manager - COMMAND set INSTALLER_SOURCE_DIR=${CMAKE_BINARY_DIR}/full-stack-deployment - COMMAND set INSTALLER_NAME=${CMAKE_BINARY_DIR}/${INSTALLER_NAME} - COMMAND set INSTALLER_SCRIPTS_DIR=${CMAKE_SOURCE_DIR}/examples - COMMAND set INSTALLER_COMPANY=${INSTALLER_COMPANY} - COMMAND set INSTALLER_DIRECTORY=${INSTALLER_DIRECTORY} - COMMAND CMD /C "\"${MAKENSIS_COMMAND}\" ${CMAKE_SOURCE_DIR}/tools/nsis/release.nsi" - ) - endif () -endmacro() \ No newline at end of file +# +# GenerateInstallers.cmake +# cmake/macros +# +# Created by Leonardo Murillo on 12/16/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 +# + +macro(GENERATE_INSTALLERS) + include(CPackComponent) + + set(CPACK_MODULE_PATH ${CPACK_MODULE_PATH} "${HF_CMAKE_DIR}/templates") + + set(_DISPLAY_NAME ${BUILD_ORGANIZATION}) + + set(CPACK_PACKAGE_NAME ${_DISPLAY_NAME}) + set(CPACK_PACKAGE_VENDOR "High Fidelity") + set(CPACK_PACKAGE_VERSION ${BUILD_VERSION}) + set(CPACK_PACKAGE_FILE_NAME "HighFidelity-Alpha-${BUILD_VERSION}") + set(CPACK_NSIS_DISPLAY_NAME ${_DISPLAY_NAME}) + set(CPACK_NSIS_PACKAGE_NAME ${_DISPLAY_NAME}) + set(CPACK_PACKAGE_INSTALL_DIRECTORY ${_DISPLAY_NAME}) + + if (WIN32) + set(CPACK_NSIS_MUI_ICON "${HF_CMAKE_DIR}/installer/installer.ico") + + # install and reference the Add/Remove icon + set(ADD_REMOVE_ICON_NAME "installer.ico") + set(ADD_REMOVE_ICON_BAD_PATH "${HF_CMAKE_DIR}/installer/${ADD_REMOVE_ICON_NAME}") + fix_path_for_nsis(${ADD_REMOVE_ICON_BAD_PATH} ADD_REMOVE_ICON_PATH) + set(CPACK_NSIS_INSTALLED_ICON_NAME ${ADD_REMOVE_ICON_NAME}) + + # use macro to put backslashes in header image path since nsis requires them + set(_INSTALLER_HEADER_BAD_PATH "${HF_CMAKE_DIR}/installer/installer-header.bmp") + set(INSTALLER_HEADER_IMAGE "") + fix_path_for_nsis(${_INSTALLER_HEADER_BAD_PATH} INSTALLER_HEADER_IMAGE) + + set(_UNINSTALLER_HEADER_BAD_PATH "${HF_CMAKE_DIR}/installer/uninstaller-header.bmp") + set(UNINSTALLER_HEADER_IMAGE "") + fix_path_for_nsis(${_UNINSTALLER_HEADER_BAD_PATH} UNINSTALLER_HEADER_IMAGE) + elseif (APPLE) + # produce a drag and drop DMG on OS X + set(CPACK_GENERATOR "DragNDrop") + + set(CPACK_PACKAGE_INSTALL_DIRECTORY "/") + set(CPACK_PACKAGING_INSTALL_PREFIX /) + set(CPACK_OSX_PACKAGE_VERSION ${CMAKE_OSX_DEPLOYMENT_TARGET}) + + # make sure a High Fidelity directory exists, in case this hits prior to other installs + install(CODE "file(MAKE_DIRECTORY \"\${CMAKE_INSTALL_PREFIX}/${DMG_SUBFOLDER_NAME}\")") + + # add the resource file to the Icon file inside the folder + install(CODE + "execute_process(COMMAND Rez -append ${DMG_SUBFOLDER_ICON} -o \${CMAKE_INSTALL_PREFIX}/${ESCAPED_DMG_SUBFOLDER_NAME}/Icon\\r)" + ) + + # modify the folder to use that custom icon + install(CODE "execute_process(COMMAND SetFile -a C \${CMAKE_INSTALL_PREFIX}/${ESCAPED_DMG_SUBFOLDER_NAME})") + + # hide the special Icon? file + install(CODE "execute_process(COMMAND SetFile -a V \${CMAKE_INSTALL_PREFIX}/${ESCAPED_DMG_SUBFOLDER_NAME}/Icon\\r)") + endif () + + # configure a cpack properties file for custom variables in template + set(CPACK_CONFIGURED_PROP_FILE "${CMAKE_CURRENT_BINARY_DIR}/CPackCustomProperties.cmake") + configure_file("${HF_CMAKE_DIR}/templates/CPackProperties.cmake.in" ${CPACK_CONFIGURED_PROP_FILE}) + set(CPACK_PROPERTIES_FILE ${CPACK_CONFIGURED_PROP_FILE}) + + set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE") + + cpack_add_component(${CLIENT_COMPONENT} DISPLAY_NAME "High Fidelity Client") + cpack_add_component(${SERVER_COMPONENT} DISPLAY_NAME "High Fidelity Server") + + include(CPack) +endmacro() diff --git a/cmake/macros/IncludeApplicationVersion.cmake b/cmake/macros/IncludeApplicationVersion.cmake deleted file mode 100644 index 753a12a01c..0000000000 --- a/cmake/macros/IncludeApplicationVersion.cmake +++ /dev/null @@ -1,36 +0,0 @@ -# -# IncludeApplicationVersion.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 -# - -macro(INCLUDE_APPLICATION_VERSION) - # - # We are relying on Jenkins defined environment variables to determine the origin of this build - # and will only package if this is a PR or Release build - if (DEFINED ENV{JOB_ID}) - set(DEPLOY_PACKAGE 1) - set(BUILD_SEQ $ENV{JOB_ID}) - set(INSTALLER_COMPANY "High Fidelity") - set(INSTALLER_DIRECTORY "${INSTALLER_COMPANY}") - set(INSTALLER_NAME "interface-win64-${BUILD_SEQ}.exe") - elseif (DEFINED ENV{ghprbPullId}) - set(DEPLOY_PACKAGE 1) - set(BUILD_SEQ "PR-$ENV{ghprbPullId}") - set(INSTALLER_COMPANY "High Fidelity - PR") - set(INSTALLER_DIRECTORY "${INSTALLER_COMPANY}\\${BUILD_SEQ}") - set(INSTALLER_NAME "pr-interface-win64-${BUILD_SEQ}.exe") - else () - set(BUILD_SEQ "dev") - set(INSTALLER_COMPANY "High Fidelity - Dev") - set(INSTALLER_DIRECTORY "${INSTALLER_COMPANY}") - set(INSTALLER_NAME "dev-interface-win64.exe") - endif () - configure_file("${MACRO_DIR}/ApplicationVersion.h.in" "${PROJECT_BINARY_DIR}/includes/ApplicationVersion.h") - include_directories("${PROJECT_BINARY_DIR}/includes") -endmacro(INCLUDE_APPLICATION_VERSION) diff --git a/cmake/macros/InstallBesideConsole.cmake b/cmake/macros/InstallBesideConsole.cmake new file mode 100644 index 0000000000..e30a25a0f8 --- /dev/null +++ b/cmake/macros/InstallBesideConsole.cmake @@ -0,0 +1,72 @@ +# +# InstallBesideConsole.cmake +# cmake/macros +# +# Copyright 2016 High Fidelity, Inc. +# Created by Stephen Birarda on January 5th, 2016 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +macro(install_beside_console) + if (WIN32 OR APPLE) + # install this component beside the installed server-console executable + if (APPLE) + set(CONSOLE_APP_CONTENTS "${CONSOLE_INSTALL_APP_PATH}/Contents") + set(COMPONENT_DESTINATION "${CONSOLE_APP_CONTENTS}/MacOS/Components.app/Contents/MacOS") + else () + set(COMPONENT_DESTINATION ${CONSOLE_INSTALL_DIR}) + endif () + + if (APPLE) + install( + TARGETS ${TARGET_NAME} + RUNTIME DESTINATION ${COMPONENT_DESTINATION} + COMPONENT ${SERVER_COMPONENT} + ) + else () + # setup install of executable and things copied by fixup/windeployqt + install( + FILES "$/" + DESTINATION ${COMPONENT_DESTINATION} + COMPONENT ${SERVER_COMPONENT} + ) + + # on windows for PR and production builds, sign the executable + set(EXECUTABLE_COMPONENT ${SERVER_COMPONENT}) + optional_win_executable_signing() + endif () + + if (TARGET_NAME STREQUAL domain-server) + if (APPLE) + set(RESOURCES_DESTINATION ${COMPONENT_DESTINATION}) + else () + set(RESOURCES_DESTINATION ${CONSOLE_INSTALL_DIR}) + endif () + + # install the resources folder for the domain-server where its executable will be + install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/resources + DESTINATION ${RESOURCES_DESTINATION} + USE_SOURCE_PERMISSIONS + COMPONENT ${SERVER_COMPONENT} + ) + endif () + + if (APPLE) + # during the install phase, call fixup to drop the shared libraries for these components in the right place + set(EXECUTABLE_NEEDING_FIXUP "\${CMAKE_INSTALL_PREFIX}/${COMPONENT_DESTINATION}/${TARGET_NAME}") + install(CODE " + include(BundleUtilities) + fixup_bundle(\"${EXECUTABLE_NEEDING_FIXUP}\" \"\" \"${FIXUP_LIBS}\") + " COMPONENT ${SERVER_COMPONENT}) + endif () + endif () + + # set variables used by manual ssleay library copy + set(TARGET_INSTALL_DIR ${COMPONENT_DESTINATION}) + set(TARGET_INSTALL_COMPONENT ${SERVER_COMPONENT}) + manually_install_ssl_eay() + +endmacro() diff --git a/cmake/macros/ManuallyInstallSSLEay.cmake b/cmake/macros/ManuallyInstallSSLEay.cmake new file mode 100644 index 0000000000..41e7e9eaf3 --- /dev/null +++ b/cmake/macros/ManuallyInstallSSLEay.cmake @@ -0,0 +1,28 @@ +# +# ManuallyInstallSSLEay.cmake +# +# Created by Stephen Birarda on 1/15/16. +# 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 +# + +macro(manually_install_ssl_eay) + + # on windows we have had issues with targets missing ssleay library + # not convinced we actually need it (I assume it would show up in the dependency tree) + # but to be safe we install it manually beside the current target + if (WIN32) + # we need to find SSL_EAY_LIBRARY_* so we can install it beside this target + # so we have to call find_package(OpenSSL) here even though this target may not specifically need it + find_package(OpenSSL REQUIRED) + + install( + FILES "${OPENSSL_DLL_PATH}/ssleay32.dll" + DESTINATION ${TARGET_INSTALL_DIR} + COMPONENT ${TARGET_INSTALL_COMPONENT} + ) + endif() + +endmacro() diff --git a/cmake/macros/OptionalWinExecutableSigning.cmake b/cmake/macros/OptionalWinExecutableSigning.cmake new file mode 100644 index 0000000000..784aae716f --- /dev/null +++ b/cmake/macros/OptionalWinExecutableSigning.cmake @@ -0,0 +1,34 @@ +# +# OptionalWinExecutableSigning.cmake +# cmake/macros +# +# Copyright 2016 High Fidelity, Inc. +# Created by Stephen Birarda on January 12, 2016 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +macro(optional_win_executable_signing) + if (WIN32 AND PRODUCTION_BUILD) + if (DEFINED ENV{HF_PFX_FILE}) + if (DEFINED ENV{HF_PFX_PASSPHRASE}) + message(STATUS "Executable for ${TARGET_NAME} will be signed with SignTool.") + + if (NOT EXECUTABLE_PATH) + set(EXECUTABLE_PATH "$") + endif () + + # setup a post build command to sign the executable + add_custom_command( + TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${SIGNTOOL_EXECUTABLE} sign /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 ${EXECUTABLE_PATH} + ) + else () + message(FATAL_ERROR "HF_PFX_PASSPHRASE must be set for executables to be signed.") + endif () + else () + message(WARNING "Creating a production build but not code signing since HF_PFX_FILE is not set.") + endif () + endif () +endmacro() diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index 17b5d5f49d..050cea9fe1 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -1,25 +1,24 @@ -# +# # PackageLibrariesForDeployment.cmake # cmake/macros -# +# # Copyright 2015 High Fidelity, Inc. # Created by Stephen Birarda on February 17, 2014 # # Distributed under the Apache License, Version 2.0. # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -# +# macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) - if (WIN32) configure_file( - ${HIFI_CMAKE_DIR}/templates/FixupBundlePostBuild.cmake.in + ${HF_CMAKE_DIR}/templates/FixupBundlePostBuild.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/FixupBundlePostBuild.cmake @ONLY ) - + set(PLUGIN_PATH "plugins") - + # add a post-build command to copy DLLs beside the executable add_custom_command( TARGET ${TARGET_NAME} @@ -29,31 +28,18 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) -DBUNDLE_PLUGIN_DIR=$/${PLUGIN_PATH} -P ${CMAKE_CURRENT_BINARY_DIR}/FixupBundlePostBuild.cmake ) - + find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) - + if (NOT WINDEPLOYQT_COMMAND) message(FATAL_ERROR "Could not find windeployqt at ${QT_DIR}/bin. windeployqt is required.") endif () - + # add a post-build command to call windeployqt to copy Qt plugins add_custom_command( TARGET ${TARGET_NAME} POST_BUILD COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" ) - elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) - find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) - - if (NOT MACDEPLOYQT_COMMAND) - message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin. macdeployqt is required.") - endif () - - # add a post-build command to call macdeployqt to copy Qt plugins - add_custom_command( - TARGET ${TARGET_NAME} - POST_BUILD - COMMAND ${MACDEPLOYQT_COMMAND} ${CMAKE_CURRENT_BINARY_DIR}/\${CONFIGURATION}/${TARGET_NAME}.app -verbose 0 - ) endif () endmacro() diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake new file mode 100644 index 0000000000..5bfbb0794d --- /dev/null +++ b/cmake/macros/SetPackagingParameters.cmake @@ -0,0 +1,109 @@ +# +# 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(RELEASE_TYPE $ENV{RELEASE_TYPE}) + set(RELEASE_NUMBER $ENV{RELEASE_NUMBER}) + + if (RELEASE_TYPE STREQUAL "PRODUCTION") + set(DEPLOY_PACKAGE TRUE) + set(PRODUCTION_BUILD 1) + set(BUILD_VERSION ${RELEASE_NUMBER}) + set(BUILD_ORGANIZATION "High Fidelity") + set(HIGH_FIDELITY_PROTOCOL "hifi") + set(INTERFACE_BUNDLE_NAME "High Fidelity") + set(INTERFACE_ICON_PREFIX "interface") + elseif (RELEASE_TYPE STREQUAL "PR") + set(DEPLOY_PACKAGE TRUE) + set(PR_BUILD 1) + set(BUILD_VERSION "PR${RELEASE_NUMBER}") + set(BUILD_ORGANIZATION "High Fidelity - ${BUILD_VERSION}") + set(INTERFACE_BUNDLE_NAME "High Fidelity") + set(INTERFACE_ICON_PREFIX "interface-beta") + else () + set(DEV_BUILD 1) + set(BUILD_VERSION "dev") + set(BUILD_ORGANIZATION "High Fidelity - ${BUILD_VERSION}") + set(INTERFACE_BUNDLE_NAME "Interface") + set(INTERFACE_ICON_PREFIX "interface-beta") + endif () + + if (APPLE) + set(DMG_SUBFOLDER_NAME "${BUILD_ORGANIZATION}") + + set(ESCAPED_DMG_SUBFOLDER_NAME "") + string(REPLACE " " "\\ " ESCAPED_DMG_SUBFOLDER_NAME ${DMG_SUBFOLDER_NAME}) + + set(DMG_SUBFOLDER_ICON "${HF_CMAKE_DIR}/installer/install-folder.rsrc") + + set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME}) + set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME}) + else () + set(CONSOLE_INSTALL_DIR ".") + set(INTERFACE_INSTALL_DIR ".") + endif () + + if (WIN32) + set(INTERFACE_EXEC_PREFIX "interface") + set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.ico") + + set(CONSOLE_EXEC_NAME "server-console.exe") + + set(DS_EXEC_NAME "domain-server.exe") + set(AC_EXEC_NAME "assignment-client.exe") + + # start menu shortcuts + set(INTERFACE_SM_SHORTCUT_NAME "High Fidelity") + set(CONSOLE_SM_SHORTCUT_NAME "Server Console") + + # check if we need to find signtool + if (PRODUCTION_BUILD OR PR_BUILD) + find_program(SIGNTOOL_EXECUTABLE signtool PATHS "C:/Program Files (x86)/Windows Kits/8.1" PATH_SUFFIXES "bin/x64") + + if (NOT SIGNTOOL_EXECUTABLE) + message(FATAL_ERROR "Code signing of executables was requested but signtool.exe could not be found.") + endif () + endif () + + set(GENERATED_UNINSTALLER_EXEC_NAME "Uninstall.exe") + set(REGISTRY_HKLM_INSTALL_ROOT "Software") + set(POST_INSTALL_OPTIONS_REG_GROUP "PostInstallOptions") + set(CLIENT_DESKTOP_SHORTCUT_REG_KEY "ClientDesktopShortcut") + set(CONSOLE_DESKTOP_SHORTCUT_REG_KEY "ConsoleDesktopShortcut") + set(CONSOLE_STARTUP_REG_KEY "ConsoleStartupShortcut") + set(LAUNCH_NOW_REG_KEY "LaunchAfterInstall") + endif () + + if (APPLE) + + set(CONSOLE_EXEC_NAME "Server Console.app") + set(CONSOLE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${CONSOLE_EXEC_NAME}") + + set(INTERFACE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${INTERFACE_BUNDLE_NAME}.app") + set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.icns") + endif() + + # setup component categories for installer + set(DDE_COMPONENT dde) + set(CLIENT_COMPONENT client) + set(SERVER_COMPONENT server) + + # create a header file our targets can use to find out the application version + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/includes") + configure_file("${HF_CMAKE_DIR}/templates/BuildInfo.h.in" "${CMAKE_BINARY_DIR}/includes/BuildInfo.h") + +endmacro(SET_PACKAGING_PARAMETERS) diff --git a/cmake/macros/SetupHifiPlugin.cmake b/cmake/macros/SetupHifiPlugin.cmake index b9fc4490d7..e9c8688590 100644 --- a/cmake/macros/SetupHifiPlugin.cmake +++ b/cmake/macros/SetupHifiPlugin.cmake @@ -12,12 +12,12 @@ macro(SETUP_HIFI_PLUGIN) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Plugins") if (APPLE) - set(PLUGIN_PATH "interface.app/Contents/MacOS/plugins") + set(PLUGIN_PATH "${INTERFACE_BUNDLE_NAME}.app/Contents/PlugIns") else() set(PLUGIN_PATH "plugins") endif() - IF(${CMAKE_SYSTEM_NAME} MATCHES "Linux") + if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_GENERATOR STREQUAL "Unix Makefiles") set(PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/${PLUGIN_PATH}/") else() set(PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/$/${PLUGIN_PATH}/") @@ -35,5 +35,4 @@ macro(SETUP_HIFI_PLUGIN) "$" ${PLUGIN_FULL_PATH} ) - endmacro() diff --git a/cmake/macros/SetupHifiProject.cmake b/cmake/macros/SetupHifiProject.cmake index 77df280d1a..6c150b6c8d 100644 --- a/cmake/macros/SetupHifiProject.cmake +++ b/cmake/macros/SetupHifiProject.cmake @@ -1,36 +1,35 @@ -# +# # SetupHifiProject.cmake -# +# # Copyright 2013 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 -# +# macro(SETUP_HIFI_PROJECT) project(${TARGET_NAME}) - + # grab the implemenation and header files file(GLOB TARGET_SRCS src/*) - + file(GLOB SRC_SUBDIRS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/*) - + foreach(DIR ${SRC_SUBDIRS}) if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/${DIR}") file(GLOB DIR_CONTENTS "src/${DIR}/*") set(TARGET_SRCS ${TARGET_SRCS} "${DIR_CONTENTS}") endif () endforeach() - - if (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) - add_executable(${TARGET_NAME} MACOSX_BUNDLE ${TARGET_SRCS} ${AUTOMTC_SRC} ${AUTOSCRIBE_SHADER_LIB_SRC}) - else () - add_executable(${TARGET_NAME} ${TARGET_SRCS} ${AUTOMTC_SRC} ${AUTOSCRIBE_SHADER_LIB_SRC}) - endif() + + add_executable(${TARGET_NAME} ${TARGET_SRCS} ${AUTOMTC_SRC} ${AUTOSCRIBE_SHADER_LIB_SRC}) + + # include the generated application version header + target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/includes") set(${TARGET_NAME}_DEPENDENCY_QT_MODULES ${ARGN}) list(APPEND ${TARGET_NAME}_DEPENDENCY_QT_MODULES Core) - + # find these Qt modules and link them to our own target find_package(Qt5 COMPONENTS ${${TARGET_NAME}_DEPENDENCY_QT_MODULES} REQUIRED) @@ -44,7 +43,7 @@ macro(SETUP_HIFI_PROJECT) foreach(QT_MODULE ${${TARGET_NAME}_DEPENDENCY_QT_MODULES}) target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) endforeach() - + target_glm() - + endmacro() diff --git a/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake b/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake index fe6f5b671e..37a7a9caa0 100644 --- a/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake +++ b/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake @@ -1,5 +1,5 @@ # -# CopyDirectoryBesideTarget.cmake +# CopyDirectoryBesideTarget.cmake # cmake/macros # # Created by Stephen Birarda on 04/30/15. @@ -10,22 +10,22 @@ # macro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY _DESTINATION) - + # remove the current directory add_custom_command( TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E remove_directory $/${_DESTINATION} ) - + if (${_SHOULD_SYMLINK}) - + # first create the destination add_custom_command( TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E make_directory $/${_DESTINATION} ) - + # the caller wants a symlink, so just add a command to symlink all contents of _DIRECTORY # in the destination - we can't simply create a symlink for _DESTINATION # because the remove_directory call above would delete the original files @@ -35,20 +35,20 @@ macro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY _DESTIN foreach(_ITEM ${_DIR_ITEMS}) # get the filename for this item get_filename_component(_ITEM_FILENAME ${_ITEM} NAME) - + # add the command to symlink this item add_custom_command( TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E create_symlink ${_ITEM} $/${_DESTINATION}/${_ITEM_FILENAME} - ) - endforeach() + ) + endforeach() else () # copy the directory add_custom_command( - TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory ${_DIRECTORY} + TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${_DIRECTORY} $/${_DESTINATION} ) endif () diff --git a/cmake/macros/TargetNsight.cmake b/cmake/macros/TargetNsight.cmake index 4b7e87e9d3..09b056d07a 100644 --- a/cmake/macros/TargetNsight.cmake +++ b/cmake/macros/TargetNsight.cmake @@ -1,20 +1,28 @@ -# +# # Copyright 2015 High Fidelity, Inc. # Created by Bradley Austin Davis on 2015/10/10 # # Distributed under the Apache License, Version 2.0. # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -# +# macro(TARGET_NSIGHT) - if (WIN32) - if (USE_NSIGHT) + if (WIN32 AND USE_NSIGHT) + + # grab the global CHECKED_FOR_NSIGHT_ONCE property + get_property(NSIGHT_CHECKED GLOBAL PROPERTY CHECKED_FOR_NSIGHT_ONCE) + + if (NOT NSIGHT_CHECKED) # try to find the Nsight package and add it to the build if we find it find_package(NSIGHT) - if (NSIGHT_FOUND) - include_directories(${NSIGHT_INCLUDE_DIRS}) - add_definitions(-DNSIGHT_FOUND) - target_link_libraries(${TARGET_NAME} "${NSIGHT_LIBRARIES}") - endif () - endif() - endif (WIN32) -endmacro() \ No newline at end of file + + # set the global CHECKED_FOR_NSIGHT_ONCE property so that we only debug that we couldn't find it once + set_property(GLOBAL PROPERTY CHECKED_FOR_NSIGHT_ONCE TRUE) + endif () + + if (NSIGHT_FOUND) + include_directories(${NSIGHT_INCLUDE_DIRS}) + add_definitions(-DNSIGHT_FOUND) + target_link_libraries(${TARGET_NAME} "${NSIGHT_LIBRARIES}") + endif () + endif () +endmacro() diff --git a/cmake/macros/TargetQuazip.cmake b/cmake/macros/TargetQuazip.cmake deleted file mode 100644 index 0536a1a9d8..0000000000 --- a/cmake/macros/TargetQuazip.cmake +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2015 High Fidelity, Inc. -# Created by Leonardo Murillo on 2015/11/20 -# -# Distributed under the Apache License, Version 2.0. -# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -# -macro(TARGET_QUAZIP) - add_dependency_external_projects(quazip) - find_package(QuaZip REQUIRED) - target_include_directories(${TARGET_NAME} PUBLIC ${QUAZIP_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) - if (WIN32) - add_paths_to_fixup_libs(${QUAZIP_DLL_PATH}) - endif () -endmacro() diff --git a/cmake/macros/TargetSixense.cmake b/cmake/macros/TargetSixense.cmake index b52af9cdd2..6fd9cede1f 100644 --- a/cmake/macros/TargetSixense.cmake +++ b/cmake/macros/TargetSixense.cmake @@ -1,14 +1,14 @@ -# +# # Copyright 2015 High Fidelity, Inc. # Created by Bradley Austin Davis on 2015/10/10 # # Distributed under the Apache License, Version 2.0. # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -# +# macro(TARGET_SIXENSE) add_dependency_external_projects(sixense) find_package(Sixense REQUIRED) target_include_directories(${TARGET_NAME} PRIVATE ${SIXENSE_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${SIXENSE_LIBRARIES}) add_definitions(-DHAVE_SIXENSE) -endmacro() \ No newline at end of file +endmacro() diff --git a/cmake/modules/FindOpenSSL.cmake b/cmake/modules/FindOpenSSL.cmake index 9d9557ad9e..5cc48d2598 100644 --- a/cmake/modules/FindOpenSSL.cmake +++ b/cmake/modules/FindOpenSSL.cmake @@ -8,11 +8,11 @@ # OPENSSL_INCLUDE_DIR - the OpenSSL include directory # OPENSSL_LIBRARIES - The libraries needed to use OpenSSL # OPENSSL_VERSION - This is set to $major.$minor.$revision$path (eg. 0.9.8s) -# +# # Modified on 7/16/2014 by Stephen Birarda # This is an adapted version of the FindOpenSSL.cmake module distributed with Cmake 2.8.12.2 # The original license for that file is displayed below. -# +# #============================================================================= # Copyright 2006-2009 Kitware, Inc. # Copyright 2006 Alexander Neundorf @@ -50,18 +50,18 @@ if (WIN32) ) set(_OPENSSL_ROOT_PATHS "${_programfiles}/OpenSSL" "${_programfiles}/OpenSSL-Win32" "C:/OpenSSL/" "C:/OpenSSL-Win32/") endif() - + unset(_programfiles) set(_OPENSSL_ROOT_HINTS_AND_PATHS HINTS ${_OPENSSL_ROOT_HINTS} PATHS ${_OPENSSL_ROOT_PATHS}) - + else () include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") hifi_library_search_hints("openssl") - + set(_OPENSSL_ROOT_HINTS_AND_PATHS ${OPENSSL_SEARCH_DIRS}) endif () -find_path(OPENSSL_INCLUDE_DIR NAMES openssl/ssl.h HINTS ${_OPENSSL_ROOT_HINTS_AND_PATHS} ${_OPENSSL_INCLUDEDIR} +find_path(OPENSSL_INCLUDE_DIR NAMES openssl/ssl.h HINTS ${_OPENSSL_ROOT_HINTS_AND_PATHS} ${_OPENSSL_INCLUDEDIR} PATH_SUFFIXES include ) @@ -81,15 +81,15 @@ if (WIN32 AND NOT CYGWIN) # We are using the libraries located in the VC subdir instead of the parent directory eventhough : # libeay32MD.lib is identical to ../libeay32.lib, and # ssleay32MD.lib is identical to ../ssleay32.lib - find_library(LIB_EAY_DEBUG NAMES libeay32MDd libeay32d + find_library(LIB_EAY_DEBUG NAMES libeay32MDd libeay32d ${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "VC" "lib/VC" ) - find_library(LIB_EAY_RELEASE NAMES libeay32MD libeay32 + find_library(LIB_EAY_RELEASE NAMES libeay32MD libeay32 ${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "VC" "lib/VC" ) - find_library(SSL_EAY_DEBUG NAMES ssleay32MDd ssleay32d + find_library(SSL_EAY_DEBUG NAMES ssleay32MDd ssleay32d ${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "VC" "lib/VC" ) @@ -109,22 +109,22 @@ if (WIN32 AND NOT CYGWIN) set(OPENSSL_LIBRARIES ${SSL_EAY_LIBRARY} ${LIB_EAY_LIBRARY}) find_path(OPENSSL_DLL_PATH NAMES ssleay32.dll PATH_SUFFIXES "bin" ${_OPENSSL_ROOT_HINTS_AND_PATHS}) - + elseif (MINGW) # same player, for MinGW set(LIB_EAY_NAMES libeay32) set(SSL_EAY_NAMES ssleay32) - + if (CMAKE_CROSSCOMPILING) list(APPEND LIB_EAY_NAMES crypto) list(APPEND SSL_EAY_NAMES ssl) endif () - + find_library(LIB_EAY NAMES ${LIB_EAY_NAMES} ${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "lib/MinGW" ) - find_library(SSL_EAY NAMES ${SSL_EAY_NAMES} + find_library(SSL_EAY NAMES ${SSL_EAY_NAMES} ${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "lib/MinGW" ) @@ -147,7 +147,7 @@ else() PATH_SUFFIXES lib ) - find_library(OPENSSL_CRYPTO_LIBRARY NAMES crypto HINTS ${_OPENSSL_ROOT_HINTS_AND_PATHS} ${_OPENSSL_LIBDIR} + find_library(OPENSSL_CRYPTO_LIBRARY NAMES crypto HINTS ${_OPENSSL_ROOT_HINTS_AND_PATHS} ${_OPENSSL_LIBDIR} PATH_SUFFIXES lib ) @@ -196,7 +196,7 @@ if (OPENSSL_INCLUDE_DIR) if(OPENSSL_INCLUDE_DIR AND EXISTS "${OPENSSL_INCLUDE_DIR}/openssl/opensslv.h") file(STRINGS "${OPENSSL_INCLUDE_DIR}/openssl/opensslv.h" openssl_version_str REGEX "^#[ ]?define[\t ]+OPENSSL_VERSION_NUMBER[\t ]+0x([0-9a-fA-F])+.*") - + # The version number is encoded as 0xMNNFFPPS: major minor fix patch status # The status gives if this is a developer or prerelease and is ignored here. # Major, minor, and fix directly translate into the version numbers shown in @@ -252,15 +252,6 @@ endif () if (WIN32) add_paths_to_fixup_libs(${OPENSSL_DLL_PATH}) - # - # For some reason fixup misses the following DLL and only copies libeay32. There's gotta be a better way to handle this - # but for now resorting to the following interm solution - if (DEFINED DEPLOY_PACKAGE AND DEPLOY_PACKAGE) - add_custom_command( - TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy ${OPENSSL_DLL_PATH}/ssleay32.dll ${CMAKE_BINARY_DIR}/full-stack-deployment/ - ) - endif () endif () mark_as_advanced(OPENSSL_INCLUDE_DIR OPENSSL_LIBRARIES OPENSSL_SEARCH_DIRS) diff --git a/cmake/modules/FindQuaZip.cmake b/cmake/modules/FindQuaZip.cmake deleted file mode 100644 index 110f239c68..0000000000 --- a/cmake/modules/FindQuaZip.cmake +++ /dev/null @@ -1,29 +0,0 @@ -# -# FindQuaZip.h -# StackManagerQt/cmake/modules -# -# Created by Mohammed Nafees. -# Copyright (c) 2014 High Fidelity. All rights reserved. -# - -# QUAZIP_FOUND - QuaZip library was found -# QUAZIP_INCLUDE_DIR - Path to QuaZip include dir -# QUAZIP_INCLUDE_DIRS - Path to QuaZip and zlib include dir (combined from QUAZIP_INCLUDE_DIR + ZLIB_INCLUDE_DIR) -# QUAZIP_LIBRARIES - List of QuaZip libraries -# QUAZIP_ZLIB_INCLUDE_DIR - The include dir of zlib headers - -include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") -hifi_library_search_hints("quazip") - -if (WIN32) - find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) -elseif (APPLE) - find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) -else () - find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES quazip HINTS ${QUAZIP_SEARCH_DIRS}) -endif () - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_INCLUDE_DIRS) - -mark_as_advanced(QUAZIP_INCLUDE_DIRS QUAZIP_SEARCH_DIRS) diff --git a/cmake/macros/ApplicationVersion.h.in b/cmake/templates/BuildInfo.h.in similarity index 51% rename from cmake/macros/ApplicationVersion.h.in rename to cmake/templates/BuildInfo.h.in index 736d00726c..3a462feb1a 100644 --- a/cmake/macros/ApplicationVersion.h.in +++ b/cmake/templates/BuildInfo.h.in @@ -1,12 +1,15 @@ // -// ApplicationVersion.h.in +// BuildInfo.h.in // cmake/macros // -// Created by Leonardo Murillo on 8/13/15. +// Created by Stephen Birarda on 1/14/16. // 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 // -const QString BUILD_VERSION = "@BUILD_SEQ@"; +namespace BuildInfo { + const QString MODIFIED_ORGANIZATION = "@BUILD_ORGANIZATION@"; + const QString VERSION = "@BUILD_VERSION@"; +} diff --git a/cmake/templates/CPackProperties.cmake.in b/cmake/templates/CPackProperties.cmake.in new file mode 100644 index 0000000000..7b3d0ae53e --- /dev/null +++ b/cmake/templates/CPackProperties.cmake.in @@ -0,0 +1,35 @@ +# +# CPackProperties.cmake.in +# cmake/templates +# +# Copyright 2016 High Fidelity, Inc. +# Created by Stephen Birarda on January 11, 2016 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +set(INTERFACE_SHORTCUT_NAME "@INTERFACE_SM_SHORTCUT_NAME@") +set(INTERFACE_WIN_EXEC_NAME "@INTERFACE_EXEC_PREFIX@.exe") +set(CONSOLE_SHORTCUT_NAME "@CONSOLE_SM_SHORTCUT_NAME@") +set(CONSOLE_WIN_EXEC_NAME "@CONSOLE_EXEC_NAME@") +set(DS_EXEC_NAME "@DS_EXEC_NAME@") +set(AC_EXEC_NAME "@AC_EXEC_NAME@") +set(HIGH_FIDELITY_PROTOCOL "@HIGH_FIDELITY_PROTOCOL@") +set(PRODUCTION_BUILD "@PRODUCTION_BUILD@") +set(PR_BUILD "@PR_BUILD@") +set(BUILD_ORGANIZATION "@BUILD_ORGANIZATION@") +set(POST_INSTALL_OPTIONS_PATH "@POST_INSTALL_OPTIONS_PATH@") +set(CLIENT_COMPONENT_NAME "@CLIENT_COMPONENT@") +set(SERVER_COMPONENT_NAME "@SERVER_COMPONENT@") +set(SIGNTOOL_EXECUTABLE "@SIGNTOOL_EXECUTABLE@") +set(UNINSTALLER_NAME "@GENERATED_UNINSTALLER_EXEC_NAME@") +set(REGISTRY_HKLM_INSTALL_ROOT "@REGISTRY_HKLM_INSTALL_ROOT@") +set(POST_INSTALL_OPTIONS_REG_GROUP "@POST_INSTALL_OPTIONS_REG_GROUP@") +set(CLIENT_DESKTOP_SHORTCUT_REG_KEY "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@") +set(CONSOLE_DESKTOP_SHORTCUT_REG_KEY "@CONSOLE_DESKTOP_SHORTCUT_REG_KEY@") +set(CONSOLE_STARTUP_REG_KEY "@CONSOLE_STARTUP_REG_KEY@") +set(LAUNCH_NOW_REG_KEY "@LAUNCH_NOW_REG_KEY@") +set(INSTALLER_HEADER_IMAGE "@INSTALLER_HEADER_IMAGE@") +set(UNINSTALLER_HEADER_IMAGE "@UNINSTALLER_HEADER_IMAGE@") +set(ADD_REMOVE_ICON_PATH "@ADD_REMOVE_ICON_PATH@") diff --git a/cmake/templates/FixupBundlePostBuild.cmake.in b/cmake/templates/FixupBundlePostBuild.cmake.in index 6e3ad9723b..57d1fd787f 100644 --- a/cmake/templates/FixupBundlePostBuild.cmake.in +++ b/cmake/templates/FixupBundlePostBuild.cmake.in @@ -1,56 +1,54 @@ -# +# # FixupBundlePostBuild.cmake.in # cmake/templates -# +# # Copyright 2015 High Fidelity, Inc. # Created by Stephen Birarda on February 13, 2014 # # Distributed under the Apache License, Version 2.0. # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -# +# include(BundleUtilities) - -# replace copy_resolved_item_into_bundle +# replace copy_resolved_item_into_bundle # # The official version of copy_resolved_item_into_bundle will print out a "warning:" when # the resolved item matches the resolved embedded item. This not not really an issue that # should rise to the level of a "warning" so we replace this message with a "status:" # function(copy_resolved_item_into_bundle resolved_item resolved_embedded_item) - if(WIN32) - # ignore case on Windows - string(TOLOWER "${resolved_item}" resolved_item_compare) - string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) - else() - set(resolved_item_compare "${resolved_item}") - set(resolved_embedded_item_compare "${resolved_embedded_item}") - endif() + if (WIN32) + # ignore case on Windows + string(TOLOWER "${resolved_item}" resolved_item_compare) + string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) + else() + set(resolved_item_compare "${resolved_item}") + set(resolved_embedded_item_compare "${resolved_embedded_item}") + endif() - if("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") - # this is our only change from the original version - message(STATUS "status: resolved_item == resolved_embedded_item - not copying...") - else() - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") - if(UNIX AND NOT APPLE) - file(RPATH_REMOVE FILE "${resolved_embedded_item}") - endif() + if ("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") + # this is our only change from the original version + message(STATUS "status: resolved_item == resolved_embedded_item - not copying...") + else() + #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") + execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") + if(UNIX AND NOT APPLE) + file(RPATH_REMOVE FILE "${resolved_embedded_item}") endif() + endif() endfunction() message(STATUS "FIXUP_LIBS for fixup_bundle called for bundle ${BUNDLE_EXECUTABLE} are @FIXUP_LIBS@") - message(STATUS "Scanning for plugins from ${BUNDLE_PLUGIN_DIR}") if (APPLE) - set(PLUGIN_EXTENSION "dylib") -elseif (WIN32) - set(PLUGIN_EXTENSION "dll") -else() - set(PLUGIN_EXTENSION "so") + set(PLUGIN_EXTENSION "dylib") +elseif (WIN32) + set(PLUGIN_EXTENSION "dll") +else() + set(PLUGIN_EXTENSION "so") endif() -file(GLOB RUNTIME_PLUGINS "${BUNDLE_PLUGIN_DIR}/*.${PLUGIN_EXTENSION}") -fixup_bundle("${BUNDLE_EXECUTABLE}" "${RUNTIME_PLUGINS}" "@FIXUP_LIBS@") +file(GLOB EXTRA_PLUGINS "${BUNDLE_PLUGIN_DIR}/*.${PLUGIN_EXTENSION}") +fixup_bundle("${BUNDLE_EXECUTABLE}" "${EXTRA_PLUGINS}" "@FIXUP_LIBS@") diff --git a/cmake/templates/Icon.rc.in b/cmake/templates/Icon.rc.in new file mode 100644 index 0000000000..d5f6ca74d6 --- /dev/null +++ b/cmake/templates/Icon.rc.in @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "@CONFIGURE_ICON_PATH@" diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in new file mode 100644 index 0000000000..075b3df38b --- /dev/null +++ b/cmake/templates/NSIS.template.in @@ -0,0 +1,1364 @@ +; CPack install script designed for a nmake build + +;-------------------------------- +; You must define these values + + !define VERSION "@CPACK_PACKAGE_VERSION@" + !define PATCH "@CPACK_PACKAGE_VERSION_PATCH@" + !define INST_DIR "@CPACK_TEMPORARY_DIRECTORY@" + +;-------------------------------- +;Variables + + Var MUI_TEMP + Var STARTMENU_FOLDER + Var SV_ALLUSERS + Var START_MENU + Var DO_NOT_ADD_TO_PATH + Var ADD_TO_PATH_ALL_USERS + Var ADD_TO_PATH_CURRENT_USER + Var INSTALL_DESKTOP + Var IS_DEFAULT_INSTALLDIR + +;-------------------------------- +;Include Modern UI + + !include "MUI2.nsh" + !include "InstallOptions.nsh" + + ;Default installation folder + InstallDir "@CPACK_NSIS_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_DIRECTORY@" + +;-------------------------------- +;General + ; leverage the UAC NSIS plugin to promote uninstaller to elevated privileges + !include UAC.nsh + + ; Set name prior to inner loop so uninstaller has correct values + Name "@CPACK_NSIS_PACKAGE_NAME@" + BrandingText " " + ManifestDPIAware true + + !ifdef INNER + !echo "Inner invocation" ; just to see what's going on + + ; Require user only for temp installer + RequestExecutionLevel user + + OutFile "$%TEMP%\tempinstaller.exe" ; not really important where this is + SetCompress off ; for speed + !else + !echo "Outer invocation" + + ; Require administrator access + RequestExecutionLevel admin + + ; Call makensis again, defining INNER. This writes an installer for us which, when + ; it is invoked, will just write the uninstaller to some location, and then exit. + ; Be sure to substitute the name of this script here. + + !system "$\"${NSISDIR}\makensis$\" /DINNER project.nsi" = 0 + + ; Require administrator access + RequestExecutionLevel admin + + ; So now run that installer we just created as %TEMP%\tempinstaller.exe. Since it + ; calls quit the return value isn't zero. + + !system "$%TEMP%\tempinstaller.exe" = 2 + + ; The Inner invocation has written an uninstaller binary for us. + ; We need to sign it if it's a production or PR build. + !if @PRODUCTION_BUILD@ == 1 + !system '"@SIGNTOOL_EXECUTABLE@" sign /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 $%TEMP%\@UNINSTALLER_NAME@' = 0 + !endif + + ; Good. Now we can carry on writing the real installer. + + ;Output file + OutFile "@CPACK_TOPLEVEL_DIRECTORY@/@CPACK_OUTPUT_FILE_NAME@" + + ;Set compression + SetCompressor @CPACK_NSIS_COMPRESSOR@ + !endif + +@CPACK_NSIS_DEFINES@ + + !include Sections.nsh + +;--- Component support macros: --- +; The code for the add/remove functionality is from: +; http://nsis.sourceforge.net/Add/Remove_Functionality +; It has been modified slightly and extended to provide +; inter-component dependencies. +Var AR_SecFlags +Var AR_RegFlags +@CPACK_NSIS_SECTION_SELECTED_VARS@ + +; Loads the "selected" flag for the section named SecName into the +; variable VarName. +!macro LoadSectionSelectedIntoVar SecName VarName + SectionGetFlags ${${SecName}} $${VarName} + IntOp $${VarName} $${VarName} & ${SF_SELECTED} ;Turn off all other bits +!macroend + +; Loads the value of a variable... can we get around this? +!macro LoadVar VarName + IntOp $R0 0 + $${VarName} +!macroend + +; Sets the value of a variable +!macro StoreVar VarName IntValue + IntOp $${VarName} 0 + ${IntValue} +!macroend + +!macro InitSection SecName + ; This macro reads component installed flag from the registry and + ;changes checked state of the section on the components page. + ;Input: section index constant name specified in Section command. + + ClearErrors + ;Reading component status from registry + ReadRegDWORD $AR_RegFlags HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\Components\${SecName}" "Installed" + IfErrors "default_${SecName}" + ;Status will stay default if registry value not found + ;(component was never installed) + IntOp $AR_RegFlags $AR_RegFlags & ${SF_SELECTED} ;Turn off all other bits + SectionGetFlags ${${SecName}} $AR_SecFlags ;Reading default section flags + IntOp $AR_SecFlags $AR_SecFlags & 0xFFFE ;Turn lowest (enabled) bit off + IntOp $AR_SecFlags $AR_RegFlags | $AR_SecFlags ;Change lowest bit + + ; Note whether this component was installed before + !insertmacro StoreVar ${SecName}_was_installed $AR_RegFlags + IntOp $R0 $AR_RegFlags & $AR_RegFlags + + ;Writing modified flags + SectionSetFlags ${${SecName}} $AR_SecFlags + + "default_${SecName}:" + !insertmacro LoadSectionSelectedIntoVar ${SecName} ${SecName}_selected +!macroend + +!macro FinishSection SecName + ; This macro reads section flag set by user and removes the section + ;if it is not selected. + ;Then it writes component installed flag to registry + ;Input: section index constant name specified in Section command. + + SectionGetFlags ${${SecName}} $AR_SecFlags ;Reading section flags + ;Checking lowest bit: + IntOp $AR_SecFlags $AR_SecFlags & ${SF_SELECTED} + IntCmp $AR_SecFlags 1 "leave_${SecName}" + ;Section is not selected: + + ; NOTE: The default CPack template calls Section uninstall macro and writes zero installed flag + ; We do not do that here because it would absolutely cause issues with shared dependencies + + ;!insertmacro "Remove_${${SecName}}" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\Components\${SecName}" \ + "Installed" 0 + + Goto "exit_${SecName}" + + "leave_${SecName}:" + ;Section is selected: + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\Components\${SecName}" \ + "Installed" 1 + + "exit_${SecName}:" +!macroend + +!macro RemoveSection_CPack SecName + ; This macro is used to call section's Remove_... macro + ;from the uninstaller. + ;Input: section index constant name specified in Section command. + + !insertmacro "Remove_${${SecName}}" +!macroend + +; Determine whether the selection of SecName changed +!macro MaybeSelectionChanged SecName + !insertmacro LoadVar ${SecName}_selected + SectionGetFlags ${${SecName}} $R1 + IntOp $R1 $R1 & ${SF_SELECTED} ;Turn off all other bits + + ; See if the status has changed: + IntCmp $R0 $R1 "${SecName}_unchanged" + !insertmacro LoadSectionSelectedIntoVar ${SecName} ${SecName}_selected + + IntCmp $R1 ${SF_SELECTED} "${SecName}_was_selected" + !insertmacro "Deselect_required_by_${SecName}" + goto "${SecName}_unchanged" + + "${SecName}_was_selected:" + !insertmacro "Select_${SecName}_depends" + + "${SecName}_unchanged:" +!macroend +;--- End of Add/Remove macros --- + +;-------------------------------- +;Interface Settings + + !define MUI_HEADERIMAGE + !define MUI_HEADERIMAGE_BITMAP "@INSTALLER_HEADER_IMAGE@" + !define MUI_HEADERIMAGE_UNBITMAP "@UNINSTALLER_HEADER_IMAGE@" + !define MUI_ABORTWARNING + +;-------------------------------- +; path functions + +!verbose 3 +!include "WinMessages.NSH" +!verbose 4 + +;---------------------------------------- +; based upon a script of "Written by KiCHiK 2003-01-18 05:57:02" +;---------------------------------------- +!verbose 3 +!include "WinMessages.NSH" +!verbose 4 +;==================================================== +; get_NT_environment +; Returns: the selected environment +; Output : head of the stack +;==================================================== +!macro select_NT_profile UN +Function ${UN}select_NT_profile + StrCmp $ADD_TO_PATH_ALL_USERS "1" 0 environment_single + DetailPrint "Selected environment for all users" + Push "all" + Return + environment_single: + DetailPrint "Selected environment for current user only." + Push "current" + Return +FunctionEnd +!macroend +!insertmacro select_NT_profile "" +!insertmacro select_NT_profile "un." +;---------------------------------------------------- +!define NT_current_env 'HKCU "Environment"' +!define NT_all_env 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' + +!ifndef WriteEnvStr_RegKey + !ifdef ALL_USERS + !define WriteEnvStr_RegKey \ + 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' + !else + !define WriteEnvStr_RegKey 'HKCU "Environment"' + !endif +!endif + +; AddToPath - Adds the given dir to the search path. +; Input - head of the stack +; Note - Win9x systems requires reboot + +Function AddToPath + Exch $0 + Push $1 + Push $2 + Push $3 + + # don't add if the path doesn't exist + IfFileExists "$0\*.*" "" AddToPath_done + + ReadEnvStr $1 PATH + ; if the path is too long for a NSIS variable NSIS will return a 0 + ; length string. If we find that, then warn and skip any path + ; modification as it will trash the existing path. + StrLen $2 $1 + IntCmp $2 0 CheckPathLength_ShowPathWarning CheckPathLength_Done CheckPathLength_Done + CheckPathLength_ShowPathWarning: + Messagebox MB_OK|MB_ICONEXCLAMATION "Warning! PATH too long installer unable to modify PATH!" + Goto AddToPath_done + CheckPathLength_Done: + Push "$1;" + Push "$0;" + Call StrStr + Pop $2 + StrCmp $2 "" "" AddToPath_done + Push "$1;" + Push "$0\;" + Call StrStr + Pop $2 + StrCmp $2 "" "" AddToPath_done + GetFullPathName /SHORT $3 $0 + Push "$1;" + Push "$3;" + Call StrStr + Pop $2 + StrCmp $2 "" "" AddToPath_done + Push "$1;" + Push "$3\;" + Call StrStr + Pop $2 + StrCmp $2 "" "" AddToPath_done + + Call IsNT + Pop $1 + StrCmp $1 1 AddToPath_NT + ; Not on NT + StrCpy $1 $WINDIR 2 + FileOpen $1 "$1\autoexec.bat" a + FileSeek $1 -1 END + FileReadByte $1 $2 + IntCmp $2 26 0 +2 +2 # DOS EOF + FileSeek $1 -1 END # write over EOF + FileWrite $1 "$\r$\nSET PATH=%PATH%;$3$\r$\n" + FileClose $1 + SetRebootFlag true + Goto AddToPath_done + + AddToPath_NT: + StrCmp $ADD_TO_PATH_ALL_USERS "1" ReadAllKey + ReadRegStr $1 ${NT_current_env} "PATH" + Goto DoTrim + ReadAllKey: + ReadRegStr $1 ${NT_all_env} "PATH" + DoTrim: + StrCmp $1 "" AddToPath_NTdoIt + Push $1 + Call Trim + Pop $1 + StrCpy $0 "$1;$0" + AddToPath_NTdoIt: + StrCmp $ADD_TO_PATH_ALL_USERS "1" WriteAllKey + WriteRegExpandStr ${NT_current_env} "PATH" $0 + Goto DoSend + WriteAllKey: + WriteRegExpandStr ${NT_all_env} "PATH" $0 + DoSend: + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + + AddToPath_done: + Pop $3 + Pop $2 + Pop $1 + Pop $0 +FunctionEnd + + +; RemoveFromPath - Remove a given dir from the path +; Input: head of the stack + +Function un.RemoveFromPath + Exch $0 + Push $1 + Push $2 + Push $3 + Push $4 + Push $5 + Push $6 + + IntFmt $6 "%c" 26 # DOS EOF + + Call un.IsNT + Pop $1 + StrCmp $1 1 unRemoveFromPath_NT + ; Not on NT + StrCpy $1 $WINDIR 2 + FileOpen $1 "$1\autoexec.bat" r + GetTempFileName $4 + FileOpen $2 $4 w + GetFullPathName /SHORT $0 $0 + StrCpy $0 "SET PATH=%PATH%;$0" + Goto unRemoveFromPath_dosLoop + + unRemoveFromPath_dosLoop: + FileRead $1 $3 + StrCpy $5 $3 1 -1 # read last char + StrCmp $5 $6 0 +2 # if DOS EOF + StrCpy $3 $3 -1 # remove DOS EOF so we can compare + StrCmp $3 "$0$\r$\n" unRemoveFromPath_dosLoopRemoveLine + StrCmp $3 "$0$\n" unRemoveFromPath_dosLoopRemoveLine + StrCmp $3 "$0" unRemoveFromPath_dosLoopRemoveLine + StrCmp $3 "" unRemoveFromPath_dosLoopEnd + FileWrite $2 $3 + Goto unRemoveFromPath_dosLoop + unRemoveFromPath_dosLoopRemoveLine: + SetRebootFlag true + Goto unRemoveFromPath_dosLoop + + unRemoveFromPath_dosLoopEnd: + FileClose $2 + FileClose $1 + StrCpy $1 $WINDIR 2 + Delete "$1\autoexec.bat" + CopyFiles /SILENT $4 "$1\autoexec.bat" + Delete $4 + Goto unRemoveFromPath_done + + unRemoveFromPath_NT: + StrCmp $ADD_TO_PATH_ALL_USERS "1" unReadAllKey + ReadRegStr $1 ${NT_current_env} "PATH" + Goto unDoTrim + unReadAllKey: + ReadRegStr $1 ${NT_all_env} "PATH" + unDoTrim: + StrCpy $5 $1 1 -1 # copy last char + StrCmp $5 ";" +2 # if last char != ; + StrCpy $1 "$1;" # append ; + Push $1 + Push "$0;" + Call un.StrStr ; Find `$0;` in $1 + Pop $2 ; pos of our dir + StrCmp $2 "" unRemoveFromPath_done + ; else, it is in path + # $0 - path to add + # $1 - path var + StrLen $3 "$0;" + StrLen $4 $2 + StrCpy $5 $1 -$4 # $5 is now the part before the path to remove + StrCpy $6 $2 "" $3 # $6 is now the part after the path to remove + StrCpy $3 $5$6 + + StrCpy $5 $3 1 -1 # copy last char + StrCmp $5 ";" 0 +2 # if last char == ; + StrCpy $3 $3 -1 # remove last char + + StrCmp $ADD_TO_PATH_ALL_USERS "1" unWriteAllKey + WriteRegExpandStr ${NT_current_env} "PATH" $3 + Goto unDoSend + unWriteAllKey: + WriteRegExpandStr ${NT_all_env} "PATH" $3 + unDoSend: + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + + unRemoveFromPath_done: + Pop $6 + Pop $5 + Pop $4 + Pop $3 + Pop $2 + Pop $1 + Pop $0 +FunctionEnd + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Uninstall sutff +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +########################################### +# Utility Functions # +########################################### + +;==================================================== +; IsNT - Returns 1 if the current system is NT, 0 +; otherwise. +; Output: head of the stack +;==================================================== +; IsNT +; no input +; output, top of the stack = 1 if NT or 0 if not +; +; Usage: +; Call IsNT +; Pop $R0 +; ($R0 at this point is 1 or 0) + +!macro IsNT un +Function ${un}IsNT + Push $0 + ReadRegStr $0 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion" CurrentVersion + StrCmp $0 "" 0 IsNT_yes + ; we are not NT. + Pop $0 + Push 0 + Return + + IsNT_yes: + ; NT!!! + Pop $0 + Push 1 +FunctionEnd +!macroend +!insertmacro IsNT "" +!insertmacro IsNT "un." + +; StrStr +; input, top of stack = string to search for +; top of stack-1 = string to search in +; output, top of stack (replaces with the portion of the string remaining) +; modifies no other variables. +; +; Usage: +; Push "this is a long ass string" +; Push "ass" +; Call StrStr +; Pop $R0 +; ($R0 at this point is "ass string") + +!macro StrStr un +Function ${un}StrStr +Exch $R1 ; st=haystack,old$R1, $R1=needle + Exch ; st=old$R1,haystack + Exch $R2 ; st=old$R1,old$R2, $R2=haystack + Push $R3 + Push $R4 + Push $R5 + StrLen $R3 $R1 + StrCpy $R4 0 + ; $R1=needle + ; $R2=haystack + ; $R3=len(needle) + ; $R4=cnt + ; $R5=tmp + loop: + StrCpy $R5 $R2 $R3 $R4 + StrCmp $R5 $R1 done + StrCmp $R5 "" done + IntOp $R4 $R4 + 1 + Goto loop +done: + StrCpy $R1 $R2 "" $R4 + Pop $R5 + Pop $R4 + Pop $R3 + Pop $R2 + Exch $R1 +FunctionEnd +!macroend +!insertmacro StrStr "" +!insertmacro StrStr "un." + +Function Trim ; Added by Pelaca + Exch $R1 + Push $R2 +Loop: + StrCpy $R2 "$R1" 1 -1 + StrCmp "$R2" " " RTrim + StrCmp "$R2" "$\n" RTrim + StrCmp "$R2" "$\r" RTrim + StrCmp "$R2" ";" RTrim + GoTo Done +RTrim: + StrCpy $R1 "$R1" -1 + Goto Loop +Done: + Pop $R2 + Exch $R1 +FunctionEnd + +Function ConditionalAddToRegisty + Pop $0 + Pop $1 + StrCmp "$0" "" ConditionalAddToRegisty_EmptyString + WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" \ + "$1" "$0" + ;MessageBox MB_OK "Set Registry: '$1' to '$0'" + DetailPrint "Set install registry entry: '$1' to '$0'" + ConditionalAddToRegisty_EmptyString: +FunctionEnd + +;-------------------------------- + +!ifdef CPACK_USES_DOWNLOAD +Function DownloadFile + IfFileExists $INSTDIR\* +2 + CreateDirectory $INSTDIR + Pop $0 + + ; Skip if already downloaded + IfFileExists $INSTDIR\$0 0 +2 + Return + + StrCpy $1 "@CPACK_DOWNLOAD_SITE@" + + try_again: + NSISdl::download "$1/$0" "$INSTDIR\$0" + + Pop $1 + StrCmp $1 "success" success + StrCmp $1 "Cancelled" cancel + MessageBox MB_OK "Download failed: $1" + cancel: + Return + success: +FunctionEnd +!endif + +;-------------------------------- +; Installation types +@CPACK_NSIS_INSTALLATION_TYPES@ + +;-------------------------------- +; Component sections +@CPACK_NSIS_COMPONENT_SECTIONS@ + +;-------------------------------- +; Define some macro setting for the gui +@CPACK_NSIS_INSTALLER_MUI_ICON_CODE@ +@CPACK_NSIS_INSTALLER_ICON_CODE@ +@CPACK_NSIS_INSTALLER_MUI_COMPONENTS_DESC@ +@CPACK_NSIS_INSTALLER_MUI_FINISHPAGE_RUN_CODE@ + +;-------------------------------- +;Pages + !insertmacro MUI_PAGE_WELCOME + + !insertmacro MUI_PAGE_LICENSE "@CPACK_RESOURCE_FILE_LICENSE@" + Page custom InstallOptionsPage + !insertmacro MUI_PAGE_DIRECTORY + + ;Start Menu Folder Page Configuration + !define MUI_STARTMENUPAGE_REGISTRY_ROOT "SHCTX" + !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" + !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" + !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER + + @CPACK_NSIS_PAGE_COMPONENTS@ + + !insertmacro MUI_PAGE_INSTFILES + Page custom PostInstallOptionsPage HandlePostInstallOptions + + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" ;first language is the default language + !insertmacro MUI_LANGUAGE "Albanian" + !insertmacro MUI_LANGUAGE "Arabic" + !insertmacro MUI_LANGUAGE "Basque" + !insertmacro MUI_LANGUAGE "Belarusian" + !insertmacro MUI_LANGUAGE "Bosnian" + !insertmacro MUI_LANGUAGE "Breton" + !insertmacro MUI_LANGUAGE "Bulgarian" + !insertmacro MUI_LANGUAGE "Croatian" + !insertmacro MUI_LANGUAGE "Czech" + !insertmacro MUI_LANGUAGE "Danish" + !insertmacro MUI_LANGUAGE "Dutch" + !insertmacro MUI_LANGUAGE "Estonian" + !insertmacro MUI_LANGUAGE "Farsi" + !insertmacro MUI_LANGUAGE "Finnish" + !insertmacro MUI_LANGUAGE "French" + !insertmacro MUI_LANGUAGE "German" + !insertmacro MUI_LANGUAGE "Greek" + !insertmacro MUI_LANGUAGE "Hebrew" + !insertmacro MUI_LANGUAGE "Hungarian" + !insertmacro MUI_LANGUAGE "Icelandic" + !insertmacro MUI_LANGUAGE "Indonesian" + !insertmacro MUI_LANGUAGE "Irish" + !insertmacro MUI_LANGUAGE "Italian" + !insertmacro MUI_LANGUAGE "Japanese" + !insertmacro MUI_LANGUAGE "Korean" + !insertmacro MUI_LANGUAGE "Kurdish" + !insertmacro MUI_LANGUAGE "Latvian" + !insertmacro MUI_LANGUAGE "Lithuanian" + !insertmacro MUI_LANGUAGE "Luxembourgish" + !insertmacro MUI_LANGUAGE "Macedonian" + !insertmacro MUI_LANGUAGE "Malay" + !insertmacro MUI_LANGUAGE "Mongolian" + !insertmacro MUI_LANGUAGE "Norwegian" + !insertmacro MUI_LANGUAGE "Polish" + !insertmacro MUI_LANGUAGE "Portuguese" + !insertmacro MUI_LANGUAGE "PortugueseBR" + !insertmacro MUI_LANGUAGE "Romanian" + !insertmacro MUI_LANGUAGE "Russian" + !insertmacro MUI_LANGUAGE "Serbian" + !insertmacro MUI_LANGUAGE "SerbianLatin" + !insertmacro MUI_LANGUAGE "SimpChinese" + !insertmacro MUI_LANGUAGE "Slovak" + !insertmacro MUI_LANGUAGE "Slovenian" + !insertmacro MUI_LANGUAGE "Spanish" + !insertmacro MUI_LANGUAGE "Swedish" + !insertmacro MUI_LANGUAGE "Thai" + !insertmacro MUI_LANGUAGE "TradChinese" + !insertmacro MUI_LANGUAGE "Turkish" + !insertmacro MUI_LANGUAGE "Ukrainian" + !insertmacro MUI_LANGUAGE "Welsh" + + +;-------------------------------- +;Reserve Files + + ;These files should be inserted before other files in the data block + ;Keep these lines before any File command + ;Only for solid compression (by default, solid compression is enabled for BZIP2 and LZMA) + + ReserveFile "NSIS.InstallOptions.ini" + ReserveFile "@POST_INSTALL_OPTIONS_PATH@" + +;-------------------------------- +;Installer Sections + +Section "-Core installation" + ;Use the entire tree produced by the INSTALL target. Keep the + ;list of directories here in sync with the RMDir commands below. + SetOutPath "$INSTDIR" + @CPACK_NSIS_EXTRA_PREINSTALL_COMMANDS@ + @CPACK_NSIS_FULL_INSTALL@ + + ;Store installation folder + WriteRegStr SHCTX "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "" $INSTDIR + + ;Package the signed uninstaller produced by the inner loop + !ifndef INNER + ; this packages the signed uninstaller + File $%TEMP%\@UNINSTALLER_NAME@ + !endif + + Push "DisplayName" + Push "@CPACK_NSIS_DISPLAY_NAME@" + Call ConditionalAddToRegisty + Push "DisplayVersion" + Push "@CPACK_PACKAGE_VERSION@" + Call ConditionalAddToRegisty + Push "Publisher" + Push "@CPACK_PACKAGE_VENDOR@" + Call ConditionalAddToRegisty + Push "UninstallString" + Push "$INSTDIR\@UNINSTALLER_NAME@" + Call ConditionalAddToRegisty + Push "NoRepair" + Push "1" + Call ConditionalAddToRegisty + + !ifdef CPACK_NSIS_ADD_REMOVE + ;Create add/remove functionality + Push "ModifyPath" + Push "$INSTDIR\AddRemove.exe" + Call ConditionalAddToRegisty + !else + Push "NoModify" + Push "1" + Call ConditionalAddToRegisty + !endif + + ; Package the add/remove icon file + File "@ADD_REMOVE_ICON_PATH@" + + ; Optional registration + Push "DisplayIcon" + Push "$INSTDIR\@CPACK_NSIS_INSTALLED_ICON_NAME@" + Call ConditionalAddToRegisty + Push "HelpLink" + Push "@CPACK_NSIS_HELP_LINK@" + Call ConditionalAddToRegisty + Push "URLInfoAbout" + Push "@CPACK_NSIS_URL_INFO_ABOUT@" + Call ConditionalAddToRegisty + Push "Contact" + Push "@CPACK_NSIS_CONTACT@" + Call ConditionalAddToRegisty + !insertmacro INSTALLOPTIONS_READ $INSTALL_DESKTOP "NSIS.InstallOptions.ini" "Field 5" "State" + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + + ;Create shortcuts + CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" +@CPACK_NSIS_CREATE_ICONS@ +@CPACK_NSIS_CREATE_ICONS_EXTRA@ + + ; Conditional handling for Interface specific options + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\@INTERFACE_SHORTCUT_NAME@.lnk" \ + "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@" + + ${If} "@PRODUCTION_BUILD@" == "1" + ; hifi:// protocol handler registry entries + WriteRegStr HKCR '@HIGH_FIDELITY_PROTOCOL@' '' 'URL:High Fidelity Protocol' + WriteRegStr HKCR '@HIGH_FIDELITY_PROTOCOL@' 'URL Protocol' '' + WriteRegStr HKCR '@HIGH_FIDELITY_PROTOCOL@\DefaultIcon' '' '$INSTDIR\@INTERFACE_WIN_EXEC_NAME@,1' + WriteRegStr HKCR '@HIGH_FIDELITY_PROTOCOL@\shell\open\command' '' '$INSTDIR\@INTERFACE_WIN_EXEC_NAME@ --url "%1"' + ${EndIf} + + ${EndIf} + + ; Conditional handling for server console shortcut + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\@CONSOLE_SHORTCUT_NAME@.lnk" \ + "$INSTDIR\@CONSOLE_WIN_EXEC_NAME@" + ${EndIf} + + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "$INSTDIR\@UNINSTALLER_NAME@" + + ;Read a value from an InstallOptions INI file + !insertmacro INSTALLOPTIONS_READ $DO_NOT_ADD_TO_PATH "NSIS.InstallOptions.ini" "Field 2" "State" + !insertmacro INSTALLOPTIONS_READ $ADD_TO_PATH_ALL_USERS "NSIS.InstallOptions.ini" "Field 3" "State" + !insertmacro INSTALLOPTIONS_READ $ADD_TO_PATH_CURRENT_USER "NSIS.InstallOptions.ini" "Field 4" "State" + + ; Write special uninstall registry entries + Push "StartMenu" + Push "$STARTMENU_FOLDER" + Call ConditionalAddToRegisty + Push "DoNotAddToPath" + Push "$DO_NOT_ADD_TO_PATH" + Call ConditionalAddToRegisty + Push "AddToPathAllUsers" + Push "$ADD_TO_PATH_ALL_USERS" + Call ConditionalAddToRegisty + Push "AddToPathCurrentUser" + Push "$ADD_TO_PATH_CURRENT_USER" + Call ConditionalAddToRegisty + Push "InstallToDesktop" + Push "$INSTALL_DESKTOP" + Call ConditionalAddToRegisty + + !insertmacro MUI_STARTMENU_WRITE_END + +@CPACK_NSIS_EXTRA_INSTALL_COMMANDS@ + +SectionEnd + +Section "-Add to path" + Push $INSTDIR\bin + StrCmp "@CPACK_NSIS_MODIFY_PATH@" "ON" 0 doNotAddToPath + StrCmp $DO_NOT_ADD_TO_PATH "1" doNotAddToPath 0 + Call AddToPath + doNotAddToPath: +SectionEnd + +;-------------------------------- +; Create custom pages +Function InstallOptionsPage + !insertmacro MUI_HEADER_TEXT "Install Options" "Choose options for installing @CPACK_NSIS_PACKAGE_NAME@" + !insertmacro INSTALLOPTIONS_DISPLAY "NSIS.InstallOptions.ini" + +FunctionEnd + +; Make sure nsDialogs is included before we use it +!include "nsdialogs.nsh" + +Var PostInstallDialog +Var OptionsLabel +Var DesktopClientCheckbox +Var DesktopServerCheckbox +Var ServerStartupCheckbox +Var LaunchNowCheckbox +Var CurrentOffset +Var OffsetUnits +Var CopyFromProductionCheckbox + +!macro SetPostInstallOption Checkbox OptionName Default + ; reads the value for the given post install option to the registry + ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" + + ${If} $0 == "NO" + ; the value in the registry says it should not be checked + ${NSD_SetState} ${Checkbox} ${BST_UNCHECKED} + ${ElseIf} $0 == "YES" + ; the value in the registry says it should be checked + ${NSD_SetState} ${Checkbox} ${BST_CHECKED} + ${Else} + ; the value in the registry was not in the expected format, use default + ${NSD_SetState} ${Checkbox} ${Default} + ${EndIf} +!macroend + +Function PostInstallOptionsPage + ; Set the text on the dialog button to match finish, hide the back and cancel buttons + GetDlgItem $R1 $hwndparent 1 + SendMessage $R1 ${WM_SETTEXT} 0 "STR:&Finish" + + GetDlgItem $R3 $hwndparent 3 + ShowWindow $R3 0 + + nsDialogs::Create 1018 + Pop $PostInstallDialog + + ${If} $PostInstallDialog == error + Abort + ${EndIf} + + ${NSD_CreateLabel} 0 0 100% 12u "Setup Options" + Pop $OptionsLabel + + ; Set label to bold + CreateFont $R2 "Arial" 10 700 + SendMessage $OptionsLabel ${WM_SETFONT} $R2 0 + + ; Force label redraw + ShowWindow $OptionsLabel ${SW_HIDE} + ShowWindow $OptionsLabel ${SW_SHOW} + + StrCpy $CurrentOffset 15 + StrCpy $OffsetUnits u + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_SHORTCUT_NAME@" + Pop $DesktopClientCheckbox + IntOp $CurrentOffset $CurrentOffset + 15 + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED} + + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for High Fidelity @CONSOLE_SHORTCUT_NAME@" + Pop $DesktopServerCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED} + + IntOp $CurrentOffset $CurrentOffset + 15 + + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity @CONSOLE_SHORTCUT_NAME@ on startup" + Pop $ServerStartupCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} + + IntOp $CurrentOffset $CurrentOffset + 15 + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Server Console Now" + ${Else} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Now" + ${EndIf} + + Pop $LaunchNowCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} + + ${If} @PR_BUILD@ == 1 + ; push the offset + IntOp $CurrentOffset $CurrentOffset + 15 + + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install" + Pop $CopyFromProductionCheckbox + + ${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED} + ${EndIf} + + nsDialogs::Show +FunctionEnd + +!macro WritePostInstallOption OptionName Option + ; writes the value for the given post install option to the registry + WriteRegStr HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" ${Option} +!macroend + +Var DesktopClientState +Var DesktopServerState +Var ServerStartupState +Var LaunchNowState +Var CopyFromProductionState + +Function HandlePostInstallOptions + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to High Fidelity + ${NSD_GetState} $DesktopClientCheckbox $DesktopClientState + + ${If} $DesktopClientState == ${BST_CHECKED} + CreateShortCut "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@" + !insertmacro WritePostInstallOption "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@" YES + ${Else} + !insertmacro WritePostInstallOption @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ NO + ${EndIf} + + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to Server Console + ${NSD_GetState} $DesktopServerCheckbox $DesktopServerState + + ${If} $DesktopServerState == ${BST_CHECKED} + CreateShortCut "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_WIN_EXEC_NAME@" + !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ YES + ${Else} + !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO + ${EndIf} + + ; check if the user asked to have Server Console launched every startup + ${NSD_GetState} $ServerStartupCheckbox $ServerStartupState + + ${If} $ServerStartupState == ${BST_CHECKED} + CreateShortCut "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_WIN_EXEC_NAME@" + + !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ YES + ${Else} + !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO + ${EndIf} + ${EndIf} + + ${If} @PR_BUILD@ == 1 + ; check if we need to copy settings/content from production for this PR build + ${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState + + ${If} $CopyFromProductionState == ${BST_CHECKED} + SetShellVarContext current + + StrCpy $0 "$APPDATA\@BUILD_ORGANIZATION@" + + ; we need to copy whatever is in the data folder for production build to the data folder for this build + CreateDirectory $0 + + ClearErrors + + ; copy the data from production build to this PR build + CopyFiles "$APPDATA\High Fidelity\*" $0 + + ; handle an error in copying files + IfErrors 0 NoError + + MessageBox mb_IconStop|mb_TopMost|mb_SetForeground \ + "There was a problem copying your production content and settings to $0 for this PR build.$\r$\n$\r$\nPlease copy them manually." + + NoError: + ${EndIf} + ${EndIf} + + ; check if we need to launch an application post-install + ${NSD_GetState} $LaunchNowCheckbox $LaunchNowState + + ${If} $LaunchNowState == ${BST_CHECKED} + !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES + + ; both launches use the explorer trick in case the user has elevated permissions for the installer + ; it won't be possible to use this approach if either application should be launched with a command line param + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_WIN_EXEC_NAME@"' + ${Else} + Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"' + ${EndIf} + ${Else} + !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO + ${EndIf} + +FunctionEnd + +!include nsProcess.nsh + +!macro PromptForRunningApplication applicationName displayName action prompter + ${nsProcess::FindProcess} ${applicationName} $R0 + + ${If} $R0 == 0 + ; the process is running, ask the user if they want us to close it + MessageBox MB_OK|MB_ICONEXCLAMATION \ + "${displayName} cannot be ${action} while ${displayName} is running.$\r$\nPlease close it and try again." + Abort + ${EndIf} +!macroend + +!macro CheckForRunningApplications action prompter + !insertmacro PromptForRunningApplication "@INTERFACE_WIN_EXEC_NAME@" "@INTERFACE_SHORTCUT_NAME@" ${action} ${prompter} + !insertmacro PromptForRunningApplication "@CONSOLE_WIN_EXEC_NAME@" "@CONSOLE_SHORTCUT_NAME@" ${action} ${prompter} + !insertmacro PromptForRunningApplication "@DS_EXEC_NAME@" "Domain Server" ${action} ${prompter} + !insertmacro PromptForRunningApplication "@AC_EXEC_NAME@" "Assignment Client" ${action} ${prompter} +!macroend + +;-------------------------------- +; determine admin versus local install +Function un.onInit + + ; In order for the uninstaller to be able to remove itself, we have to do some trickery here. + ; If the $EXEPATH does not contain the $TEMP dir, this instance is not the copied one + ; so we move it to the $TEMP dir and then execute the copied uninstaller. + + ${If} $EXEDIR != $TEMP + CopyFiles /SILENT $EXEPATH $TEMP\@UNINSTALLER_NAME@ + ExecWait '"$Temp\@UNINSTALLER_NAME@" _?=$INSTDIR' $0 + SetErrorLevel $0 + Quit + ${EndIf} + + ; make sure none of the installed applications are still running + !insertmacro CheckForRunningApplications "uninstalled" "Uninstaller" + ${nsProcess::Unload} + + ; attempt to elevate the uninstaller to admin status + uac_tryagain: + !insertmacro UAC_RunElevated + ${Switch} $0 + ${Case} 0 + ${IfThen} $1 = 1 ${|} Quit ${|} ;we are the outer process, the inner process has done its work, we are done + ${IfThen} $3 <> 0 ${|} ${Break} ${|} ;we are admin, let the show go on + ${If} $1 = 3 ;RunAs completed successfully, but with a non-admin user + MessageBox mb_YesNo|mb_IconExclamation|mb_TopMost|mb_SetForeground "The uninstaller requires admin privileges, try again" /SD IDNO IDYES uac_tryagain IDNO 0 + ${EndIf} + ;fall-through and die + + ${Case} 1223 + MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "The uninstaller requires admin privileges, aborting!" + Quit + ${Case} 1062 + MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Logon service not running, aborting!" + Quit + ${Default} + MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0" + Quit + ${EndSwitch} + + ClearErrors + UserInfo::GetName + IfErrors noLM + Pop $0 + UserInfo::GetAccountType + Pop $1 + StrCmp $1 "Admin" 0 +3 + SetShellVarContext all + ;MessageBox MB_OK 'User "$0" is in the Admin group' + Goto done + StrCmp $1 "Power" 0 +3 + SetShellVarContext all + ;MessageBox MB_OK 'User "$0" is in the Power Users group' + Goto done + + noLM: + ;Get installation folder from registry if available + + done: + +FunctionEnd + +;--- Add/Remove callback functions: --- +!macro SectionList MacroName + ;This macro used to perform operation on multiple sections. + ;List all of your components in following manner here. +@CPACK_NSIS_COMPONENT_SECTION_LIST@ +!macroend + +Section -FinishComponents + ;Removes unselected components and writes component status to registry + !insertmacro SectionList "FinishSection" + +!ifdef CPACK_NSIS_ADD_REMOVE + ; Get the name of the installer executable + System::Call 'kernel32::GetModuleFileNameA(i 0, t .R0, i 1024) i r1' + StrCpy $R3 $R0 + + ; Strip off the last 13 characters, to see if we have AddRemove.exe + StrLen $R1 $R0 + IntOp $R1 $R0 - 13 + StrCpy $R2 $R0 13 $R1 + StrCmp $R2 "AddRemove.exe" addremove_installed + + ; We're not running AddRemove.exe, so install it + CopyFiles $R3 $INSTDIR\AddRemove.exe + + addremove_installed: +!endif +SectionEnd +;--- End of Add/Remove callback functions --- + +;-------------------------------- +; Component dependencies +Function .onSelChange + !insertmacro SectionList MaybeSelectionChanged + + ; if neither component is selected, disable the install button + ${IfNot} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${AndIfNot} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + GetDlgItem $0 $HWNDPARENT 1 + EnableWindow $0 0 + ${Else} + GetDlgItem $0 $HWNDPARENT 1 + EnableWindow $0 1 + ${EndIf} +FunctionEnd + +;-------------------------------- +;Uninstaller Section + +; the normal uninstaller section is only defined for inner (they're not needed in the "outer" +; installer and will just cause warnings because there is no WriteInstaller command) + +!ifdef INNER +Section "Uninstall" + + ReadRegStr $START_MENU SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "StartMenu" + ;MessageBox MB_OK "Start menu is in: $START_MENU" + ReadRegStr $DO_NOT_ADD_TO_PATH SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "DoNotAddToPath" + ReadRegStr $ADD_TO_PATH_ALL_USERS SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "AddToPathAllUsers" + ReadRegStr $ADD_TO_PATH_CURRENT_USER SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "AddToPathCurrentUser" + ;MessageBox MB_OK "Add to path: $DO_NOT_ADD_TO_PATH all users: $ADD_TO_PATH_ALL_USERS" + ReadRegStr $INSTALL_DESKTOP SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "InstallToDesktop" + ;MessageBox MB_OK "Install to desktop: $INSTALL_DESKTOP " + +@CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS@ + + ;Remove files we installed. + ;Keep the list of directories here in sync with the File commands above. +@CPACK_NSIS_DELETE_FILES@ +@CPACK_NSIS_DELETE_DIRECTORIES@ + +!ifdef CPACK_NSIS_ADD_REMOVE + ;Remove the add/remove program + Delete "$INSTDIR\AddRemove.exe" +!endif + + ;Remove the Add/Remove icon + Delete "$INSTDIR\@CPACK_NSIS_INSTALLED_ICON_NAME@" + + ;Remove the uninstaller itself. + Delete "$INSTDIR\@UNINSTALLER_NAME@" + DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" + + ;Remove the installation directory if it is empty. + RMDir "$INSTDIR" + + ; Remove the registry entries. + DeleteRegKey SHCTX "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" + + ; Removes all optional components + !insertmacro SectionList "RemoveSection_CPack" + + !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP + + Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk" + Delete "$SMPROGRAMS\$MUI_TEMP\@INTERFACE_SHORTCUT_NAME@.lnk" + Delete "$SMPROGRAMS\$MUI_TEMP\@CONSOLE_SHORTCUT_NAME@.lnk" + Delete "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk" + Delete "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk" + Delete "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" +@CPACK_NSIS_DELETE_ICONS@ +@CPACK_NSIS_DELETE_ICONS_EXTRA@ + + ;Delete High Fidelity protocol handling, if installed + ${If} "@PRODUCTION_BUILD@" == "1" + DeleteRegKey HKCR '@HIGH_FIDELITY_PROTOCOL@' + ${EndIf} + + ;Delete post-install option information from registry + DeleteRegKey HKLM '@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@' + + ;Delete empty start menu parent diretories + StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" + + startMenuDeleteLoop: + ClearErrors + RMDir $MUI_TEMP + GetFullPathName $MUI_TEMP "$MUI_TEMP\.." + + IfErrors startMenuDeleteLoopDone + + StrCmp "$MUI_TEMP" "$SMPROGRAMS" startMenuDeleteLoopDone startMenuDeleteLoop + startMenuDeleteLoopDone: + + ; If the user changed the shortcut, then untinstall may not work. This should + ; try to fix it. + StrCpy $MUI_TEMP "$START_MENU" + Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk" +@CPACK_NSIS_DELETE_ICONS_EXTRA@ + + ;Delete empty start menu parent diretories + StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" + + secondStartMenuDeleteLoop: + ClearErrors + RMDir $MUI_TEMP + GetFullPathName $MUI_TEMP "$MUI_TEMP\.." + + IfErrors secondStartMenuDeleteLoopDone + + StrCmp "$MUI_TEMP" "$SMPROGRAMS" secondStartMenuDeleteLoopDone secondStartMenuDeleteLoop + secondStartMenuDeleteLoopDone: + + DeleteRegKey /ifempty SHCTX "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" + + Push $INSTDIR\bin + StrCmp $DO_NOT_ADD_TO_PATH_ "1" doNotRemoveFromPath 0 + Call un.RemoveFromPath + doNotRemoveFromPath: +SectionEnd +!endif + +;-------------------------------- +; determine admin versus local install +; Is install for "AllUsers" or "JustMe"? +; Default to "JustMe" - set to "AllUsers" if admin or on Win9x +; This function is used for the very first "custom page" of the installer. +; This custom page does not show up visibly, but it executes prior to the +; first visible page and sets up $INSTDIR properly... +; Choose different default installation folder based on SV_ALLUSERS... +; "Program Files" for AllUsers, "My Documents" for JustMe... + +Function .onInit + + !ifdef INNER + ; If INNER is defined, then we aren't supposed to do anything except write out + ; the installer. This is better than processing a command line option as it means + ; this entire code path is not present in the final (real) installer. + + WriteUninstaller "$%TEMP%\@UNINSTALLER_NAME@" + + ; just bail out quickly when running the "inner" installer + Quit + !endif + + ; make sure none of the installed applications are still running + !insertmacro CheckForRunningApplications "installed" "Installer" + ${nsProcess::Unload} + + StrCmp "@CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL@" "ON" 0 inst + + ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "UninstallString" + StrCmp $0 "" inst + + MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION \ + "@CPACK_NSIS_PACKAGE_NAME@ is already installed. $\n$\nDo you want to uninstall the old version before installing the new one?" \ + IDYES uninst IDNO inst + Abort + +;Run the uninstaller +uninst: + ClearErrors + StrLen $2 "\Uninstall.exe" + StrCpy $3 $0 -$2 # remove "\Uninstall.exe" from UninstallString to get path + ExecWait '$0 _?=$3' ;Do not copy the uninstaller to a temp file + + IfErrors uninst_failed inst +uninst_failed: + MessageBox MB_OK|MB_ICONSTOP "Uninstall failed." + Abort + + +inst: + ; Reads components status for registry + !insertmacro SectionList "InitSection" + + ; check to see if /D has been used to change + ; the install directory by comparing it to the + ; install directory that is expected to be the + ; default + StrCpy $IS_DEFAULT_INSTALLDIR 0 + StrCmp "$INSTDIR" "@CPACK_NSIS_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_DIRECTORY@" 0 +2 + StrCpy $IS_DEFAULT_INSTALLDIR 1 + + StrCpy $SV_ALLUSERS "JustMe" + ; if default install dir then change the default + ; if it is installed for JustMe + StrCmp "$IS_DEFAULT_INSTALLDIR" "1" 0 +2 + StrCpy $INSTDIR "$DOCUMENTS\@CPACK_PACKAGE_INSTALL_DIRECTORY@" + + ClearErrors + UserInfo::GetName + IfErrors noLM + Pop $0 + UserInfo::GetAccountType + Pop $1 + StrCmp $1 "Admin" 0 +4 + SetShellVarContext all + ;MessageBox MB_OK 'User "$0" is in the Admin group' + StrCpy $SV_ALLUSERS "AllUsers" + Goto done + StrCmp $1 "Power" 0 +4 + SetShellVarContext all + ;MessageBox MB_OK 'User "$0" is in the Power Users group' + StrCpy $SV_ALLUSERS "AllUsers" + Goto done + + noLM: + StrCpy $SV_ALLUSERS "AllUsers" + ;Get installation folder from registry if available + + done: + StrCmp $SV_ALLUSERS "AllUsers" 0 +3 + StrCmp "$IS_DEFAULT_INSTALLDIR" "1" 0 +2 + StrCpy $INSTDIR "@CPACK_NSIS_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_DIRECTORY@" + + StrCmp "@CPACK_NSIS_MODIFY_PATH@" "ON" 0 noOptionsPage + !insertmacro INSTALLOPTIONS_EXTRACT "NSIS.InstallOptions.ini" + + noOptionsPage: +FunctionEnd diff --git a/cmake/templates/console-build-info.json.in b/cmake/templates/console-build-info.json.in new file mode 100644 index 0000000000..c1ef010e08 --- /dev/null +++ b/cmake/templates/console-build-info.json.in @@ -0,0 +1,4 @@ +{ + "releaseType": "@RELEASE_TYPE@", + "buildIdentifier": "@BUILD_VERSION@" +} diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index 2200916765..b7eb8c0138 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -34,8 +34,10 @@ target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES}) # libcrypto uses dlopen in libdl if (UNIX) target_link_libraries(${TARGET_NAME} ${CMAKE_DL_LIBS}) -endif (UNIX) +endif () -include_application_version() -package_libraries_for_deployment() -consolidate_stack_components() \ No newline at end of file +if (WIN32) + package_libraries_for_deployment() +endif () + +install_beside_console() diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 98f063a6f5..3116b072d8 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -386,8 +386,8 @@ "name": "persistFilename", "label": "Entities Filename", "help": "the path to the file entities are stored in. Make sure the path exists.", - "placeholder": "resources/models.json.gz", - "default": "resources/models.json.gz", + "placeholder": "models.json.gz", + "default": "models.json.gz", "advanced": true }, { diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index c733256fe3..7dc94421be 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -126,6 +126,20 @@ var viewHelpers = { } } +var qs = (function(a) { + if (a == "") return {}; + var b = {}; + for (var i = 0; i < a.length; ++i) + { + var p=a[i].split('=', 2); + if (p.length == 1) + b[p[0]] = ""; + else + b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " ")); + } + return b; +})(window.location.search.substr(1).split('&')); + $(document).ready(function(){ /* * Clamped-width. @@ -272,9 +286,80 @@ $(document).ready(function(){ // $('body').scrollspy({ target: '#setup-sidebar'}) - reloadSettings(); + reloadSettings(function(success){ + if (success) { + handleAction(); + } else { + swal({ + title: '', + type: 'error', + text: "There was a problem loading the domain settings.\nPlease refresh the page to try again.", + }); + } + }); }); +function handleAction() { + // check if we were passed an action to handle + var action = qs["action"]; + + if (action == "share") { + // figure out if we already have a stored domain ID + if (Settings.data.values.metaverse.id.length > 0) { + // we need to ask the API what a shareable name for this domain is + getDomainFromAPI(function(data){ + // check if we have owner_places (for a real domain) or a name (for a temporary domain) + if (data && data.status == "success") { + var shareName; + if (data.domain.owner_places) { + shareName = data.domain.owner_places[0].name + } else if (data.domain.name) { + shareName = data.domain.name; + } + + var shareLink = "hifi://" + shareName; + + console.log(shareLink); + + // show a dialog with a copiable share URL + swal({ + title: "Share", + type: "input", + inputPlaceholder: shareLink, + inputValue: shareLink, + text: "Copy this URL to invite friends to your domain.", + closeOnConfirm: true + }); + + $('.sweet-alert input').select(); + + } else { + // show an error alert + swal({ + title: '', + type: 'error', + text: "There was a problem retreiving domain information from High Fidelity API.", + confirmButtonText: 'Try again', + showCancelButton: true, + closeOnConfirm: false + }, function(isConfirm){ + if (isConfirm) { + // they want to try getting domain share info again + showSpinnerAlert("Requesting domain information...") + handleAction(); + } else { + swal.close(); + } + }); + } + }); + } else { + // no domain ID present, just show the share dialog + createTemporaryDomain(); + } + } +} + function dynamicButton(button_id, text) { return $(""); } @@ -577,29 +662,33 @@ function placeTableRowForPlaceObject(place) { return placeTableRow(place.name, placePathOrIndex, false); } -function reloadPlacesOrTemporaryName() { +function getDomainFromAPI(callback) { // we only need to do this if we have a current domain ID var domainID = Settings.data.values.metaverse.id; if (domainID.length > 0) { var domainURL = Settings.METAVERSE_URL + "/api/v1/domains/" + domainID; - $.getJSON(domainURL, function(data){ - // check if we have owner_places (for a real domain) or a name (for a temporary domain) - if (data.status == "success") { - if (data.domain.owner_places) { - // add a table row for each of these names - _.each(data.domain.owner_places, function(place){ - $('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRowForPlaceObject(place)); - }); - } else if (data.domain.name) { - // add a table row for this temporary domain name - $('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRow(data.domain.name, '/', true)); - } - } - }); + $.getJSON(domainURL, callback).fail(callback); } } +function reloadPlacesOrTemporaryName() { + getDomainFromAPI(function(data){ + // check if we have owner_places (for a real domain) or a name (for a temporary domain) + if (data.status == "success") { + if (data.domain.owner_places) { + // add a table row for each of these names + _.each(data.domain.owner_places, function(place){ + $('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRowForPlaceObject(place)); + }); + } else if (data.domain.name) { + // add a table row for this temporary domain name + $('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRow(data.domain.name, '/', true)); + } + } + }) +} + function appendDomainIDButtons() { var domainIDInput = $(Settings.DOMAIN_ID_SELECTOR); @@ -728,7 +817,7 @@ function createTemporaryDomain() { }); } -function reloadSettings() { +function reloadSettings(callback) { $.getJSON('/settings.json', function(data){ _.extend(data, viewHelpers) @@ -757,6 +846,12 @@ function reloadSettings() { placement: 'right', title: 'This setting is in the master config file and cannot be changed' }); + + // call the callback now that settings are loaded + callback(true); + }).fail(function() { + // call the failure object since settings load faild + callback(false) }); } diff --git a/domain-server/resources/web/settings/js/sweetalert.min.js b/domain-server/resources/web/settings/js/sweetalert.min.js index d81d1e906b..5c029c2532 100755 --- a/domain-server/resources/web/settings/js/sweetalert.min.js +++ b/domain-server/resources/web/settings/js/sweetalert.min.js @@ -1 +1 @@ -!function(e,t,n){"use strict";!function o(e,t,n){function a(s,l){if(!t[s]){if(!e[s]){var i="function"==typeof require&&require;if(!l&&i)return i(s,!0);if(r)return r(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var c=t[s]={exports:{}};e[s][0].call(c.exports,function(t){var n=e[s][1][t];return a(n?n:t)},c,c.exports,o,e,t,n)}return t[s].exports}for(var r="function"==typeof require&&require,s=0;s=0;)n=n.replace(" "+t+" "," ");e.className=n.replace(/^\s+|\s+$/g,"")}},i=function(e){var n=t.createElement("div");return n.appendChild(t.createTextNode(e)),n.innerHTML},u=function(e){e.style.opacity="",e.style.display="block"},c=function(e){if(e&&!e.length)return u(e);for(var t=0;t0?setTimeout(o,t):e.style.display="none"});o()},h=function(n){if("function"==typeof MouseEvent){var o=new MouseEvent("click",{view:e,bubbles:!1,cancelable:!0});n.dispatchEvent(o)}else if(t.createEvent){var a=t.createEvent("MouseEvents");a.initEvent("click",!1,!1),n.dispatchEvent(a)}else t.createEventObject?n.fireEvent("onclick"):"function"==typeof n.onclick&&n.onclick()},g=function(t){"function"==typeof t.stopPropagation?(t.stopPropagation(),t.preventDefault()):e.event&&e.event.hasOwnProperty("cancelBubble")&&(e.event.cancelBubble=!0)};a.hasClass=r,a.addClass=s,a.removeClass=l,a.escapeHtml=i,a._show=u,a.show=c,a._hide=d,a.hide=f,a.isDescendant=p,a.getTopMargin=m,a.fadeIn=v,a.fadeOut=y,a.fireClick=h,a.stopEventPropagation=g},{}],5:[function(t,o,a){Object.defineProperty(a,"__esModule",{value:!0});var r=t("./handle-dom"),s=t("./handle-swal-dom"),l=function(t,o,a){var l=t||e.event,i=l.keyCode||l.which,u=a.querySelector("button.confirm"),c=a.querySelector("button.cancel"),d=a.querySelectorAll("button[tabindex]");if(-1!==[9,13,32,27].indexOf(i)){for(var f=l.target||l.srcElement,p=-1,m=0;m"),i.innerHTML=e.html?e.text:s.escapeHtml(e.text||"").split("\n").join("
"),e.text&&s.show(i),e.customClass)s.addClass(t,e.customClass),t.setAttribute("data-custom-class",e.customClass);else{var d=t.getAttribute("data-custom-class");s.removeClass(t,d),t.setAttribute("data-custom-class","")}if(s.hide(t.querySelectorAll(".sa-icon")),e.type&&!a.isIE8()){var f=function(){for(var o=!1,a=0;ao;o++)n=parseInt(e.substr(2*o,2),16),n=Math.round(Math.min(Math.max(0,n+n*t),255)).toString(16),a+=("00"+n).substr(n.length);return a};o.extend=a,o.hexToRgb=r,o.isIE8=s,o.logStr=l,o.colorLuminance=i},{}]},{},[1])}(window,document); \ No newline at end of file +!function(e,t,n){"use strict";!function o(e,t,n){function a(s,l){if(!t[s]){if(!e[s]){var i="function"==typeof require&&require;if(!l&&i)return i(s,!0);if(r)return r(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var c=t[s]={exports:{}};e[s][0].call(c.exports,function(t){var n=e[s][1][t];return a(n?n:t)},c,c.exports,o,e,t,n)}return t[s].exports}for(var r="function"==typeof require&&require,s=0;s=0;)n=n.replace(" "+t+" "," ");e.className=n.replace(/^\s+|\s+$/g,"")}},i=function(e){var n=t.createElement("div");return n.appendChild(t.createTextNode(e)),n.innerHTML},u=function(e){e.style.opacity="",e.style.display="block"},c=function(e){if(e&&!e.length)return u(e);for(var t=0;t0?setTimeout(o,t):e.style.display="none"});o()},h=function(n){if("function"==typeof MouseEvent){var o=new MouseEvent("click",{view:e,bubbles:!1,cancelable:!0});n.dispatchEvent(o)}else if(t.createEvent){var a=t.createEvent("MouseEvents");a.initEvent("click",!1,!1),n.dispatchEvent(a)}else t.createEventObject?n.fireEvent("onclick"):"function"==typeof n.onclick&&n.onclick()},b=function(t){"function"==typeof t.stopPropagation?(t.stopPropagation(),t.preventDefault()):e.event&&e.event.hasOwnProperty("cancelBubble")&&(e.event.cancelBubble=!0)};a.hasClass=r,a.addClass=s,a.removeClass=l,a.escapeHtml=i,a._show=u,a.show=c,a._hide=d,a.hide=f,a.isDescendant=p,a.getTopMargin=m,a.fadeIn=v,a.fadeOut=y,a.fireClick=h,a.stopEventPropagation=b},{}],5:[function(t,o,a){Object.defineProperty(a,"__esModule",{value:!0});var r=t("./handle-dom"),s=t("./handle-swal-dom"),l=function(t,o,a){var l=t||e.event,i=l.keyCode||l.which,u=a.querySelector("button.confirm"),c=a.querySelector("button.cancel"),d=a.querySelectorAll("button[tabindex]");if(-1!==[9,13,32,27].indexOf(i)){for(var f=l.target||l.srcElement,p=-1,m=0;m"),i.innerHTML=e.html?e.text:s.escapeHtml(e.text||"").split("\n").join("
"),e.text&&s.show(i),e.customClass)s.addClass(t,e.customClass),t.setAttribute("data-custom-class",e.customClass);else{var d=t.getAttribute("data-custom-class");s.removeClass(t,d),t.setAttribute("data-custom-class","")}if(s.hide(t.querySelectorAll(".sa-icon")),e.type&&!a.isIE8()){var f=function(){for(var o=!1,a=0;ao;o++)n=parseInt(e.substr(2*o,2),16),n=Math.round(Math.min(Math.max(0,n+n*t),255)).toString(16),a+=("00"+n).substr(n.length);return a};o.extend=a,o.hexToRgb=r,o.isIE8=s,o.logStr=l,o.colorLuminance=i},{}]},{},[1]),"function"==typeof define&&define.amd?define(function(){return sweetAlert}):"undefined"!=typeof module&&module.exports&&(module.exports=sweetAlert)}(window,document); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index e219b47571..6d49b39d10 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include #include #include #include @@ -35,6 +35,7 @@ #include #include #include +#include #include "DomainServerNodeData.h" #include "NodeConnectionData.h" @@ -46,7 +47,7 @@ const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.highfidelity.io"; DomainServer::DomainServer(int argc, char* argv[]) : QCoreApplication(argc, argv), _gatekeeper(this), - _httpManager(DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this), + _httpManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this), _httpsManager(NULL), _allAssignments(), _unfulfilledAssignments(), @@ -71,17 +72,19 @@ DomainServer::DomainServer(int argc, char* argv[]) : connect(this, &QCoreApplication::aboutToQuit, this, &DomainServer::aboutToQuit); - setOrganizationName("High Fidelity"); + setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); setOrganizationDomain("highfidelity.io"); setApplicationName("domain-server"); - setApplicationVersion(BUILD_VERSION); + setApplicationVersion(BuildInfo::VERSION); QSettings::setDefaultFormat(QSettings::IniFormat); // make sure we have a fresh AccountManager instance // (need this since domain-server can restart itself and maintain static variables) AccountManager::getInstance(true); - _settingsManager.setupConfigMap(arguments()); + auto args = arguments(); + + _settingsManager.setupConfigMap(args); // setup a shutdown event listener to handle SIGTERM or WM_CLOSE for us #ifdef _WIN32 @@ -110,6 +113,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : // preload some user public keys so they can connect on first request _gatekeeper.preloadAllowedUserPublicKeys(); + + optionallyGetTemporaryName(args); } } @@ -157,7 +162,7 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { QSslCertificate sslCertificate(&certFile); QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); - _httpsManager = new HTTPSManager(DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this, this); + _httpsManager = new HTTPSManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this, this); qDebug() << "TCP server listening for HTTPS connections on" << DOMAIN_SERVER_HTTPS_PORT; @@ -209,6 +214,80 @@ bool DomainServer::optionallySetupOAuth() { return true; } +static const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id"; + +void DomainServer::optionallyGetTemporaryName(const QStringList& arguments) { + // check for the temporary name parameter + const QString GET_TEMPORARY_NAME_SWITCH = "--get-temp-name"; + + if (arguments.contains(GET_TEMPORARY_NAME_SWITCH)) { + + // make sure we don't already have a domain ID + const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH); + if (idValueVariant) { + qWarning() << "Temporary domain name requested but a domain ID is already present in domain-server settings." + << "Will not request temporary name."; + return; + } + + // we've been asked to grab a temporary name from the API + // so fire off that request now + auto& accountManager = AccountManager::getInstance(); + + // ask our auth endpoint for our balance + JSONCallbackParameters callbackParameters; + callbackParameters.jsonCallbackReceiver = this; + callbackParameters.jsonCallbackMethod = "handleTempDomainSuccess"; + callbackParameters.errorCallbackReceiver = this; + callbackParameters.errorCallbackMethod = "handleTempDomainError"; + + accountManager.sendRequest("/api/v1/domains/temporary", AccountManagerAuth::None, + QNetworkAccessManager::PostOperation, callbackParameters); + } +} + +void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + + // grab the information for the new domain + static const QString DATA_KEY = "data"; + static const QString DOMAIN_KEY = "domain"; + static const QString ID_KEY = "id"; + static const QString NAME_KEY = "name"; + + auto domainObject = jsonObject[DATA_KEY].toObject()[DOMAIN_KEY].toObject(); + if (!domainObject.isEmpty()) { + auto id = domainObject[ID_KEY].toString(); + auto name = domainObject[NAME_KEY].toString(); + + qInfo() << "Received new temporary domain name" << name; + qDebug() << "The temporary domain ID is" << id; + + // store the new domain ID and auto network setting immediately + QString newSettingsJSON = QString("{\"metaverse\": { \"id\": \"%1\", \"automatic_networking\": \"full\"}}").arg(id); + auto settingsDocument = QJsonDocument::fromJson(newSettingsJSON.toUtf8()); + _settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object()); + + // store the new ID and auto networking setting on disk + _settingsManager.persistToFile(); + + // change our domain ID immediately + DependencyManager::get()->setSessionUUID(QUuid { id }); + + // change our automatic networking settings so that we're communicating with the ICE server + setupICEHeartbeatForFullNetworking(); + + } else { + qWarning() << "There were problems parsing the API response containing a temporary domain name. Please try again" + << "via domain-server relaunch or from the domain-server settings."; + } +} + +void DomainServer::handleTempDomainError(QNetworkReply& requestReply) { + qWarning() << "A temporary name was requested but there was an error creating one. Please try again via domain-server relaunch" + << "or from the domain-server settings."; +} + const QString DOMAIN_CONFIG_ID_KEY = "id"; const QString METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH = "metaverse.automatic_networking"; @@ -259,7 +338,6 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { // set our LimitedNodeList UUID to match the UUID from our config // nodes will currently use this to add resources to data-web that relate to our domain - const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id"; const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); if (idValueVariant) { nodeList->setSessionUUID(idValueVariant->toString()); @@ -372,19 +450,7 @@ void DomainServer::setupAutomaticNetworking() { _settingsManager.valueOrDefaultValueForKeyPath(METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH).toString(); if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { - // call our sendHeartbeatToIceServer immediately anytime a local or public socket changes - connect(nodeList.data(), &LimitedNodeList::localSockAddrChanged, - this, &DomainServer::sendHeartbeatToIceServer); - connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged, - this, &DomainServer::sendHeartbeatToIceServer); - - // we need this DS to know what our public IP is - start trying to figure that out now - nodeList->startSTUNPublicSocketUpdate(); - - // setup a timer to heartbeat with the ice-server every so often - QTimer* iceHeartbeatTimer = new QTimer(this); - connect(iceHeartbeatTimer, &QTimer::timeout, this, &DomainServer::sendHeartbeatToIceServer); - iceHeartbeatTimer->start(ICE_HEARBEAT_INTERVAL_MSECS); + setupICEHeartbeatForFullNetworking(); } if (!didSetupAccountManagerWithAccessToken()) { @@ -434,6 +500,26 @@ void DomainServer::setupAutomaticNetworking() { dataHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); } +void DomainServer::setupICEHeartbeatForFullNetworking() { + auto limitedNodeList = DependencyManager::get(); + + // call our sendHeartbeatToIceServer immediately anytime a local or public socket changes + connect(limitedNodeList.data(), &LimitedNodeList::localSockAddrChanged, + this, &DomainServer::sendHeartbeatToIceServer); + connect(limitedNodeList.data(), &LimitedNodeList::publicSockAddrChanged, + this, &DomainServer::sendHeartbeatToIceServer); + + // we need this DS to know what our public IP is - start trying to figure that out now + limitedNodeList->startSTUNPublicSocketUpdate(); + + if (!_iceHeartbeatTimer) { + // setup a timer to heartbeat with the ice-server every so often + _iceHeartbeatTimer = new QTimer { this }; + connect(_iceHeartbeatTimer, &QTimer::timeout, this, &DomainServer::sendHeartbeatToIceServer); + _iceHeartbeatTimer->start(ICE_HEARBEAT_INTERVAL_MSECS); + } +} + void DomainServer::loginFailed() { qDebug() << "Login to data server has failed. domain-server will now quit"; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); @@ -1068,7 +1154,7 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { const char ASSIGNMENT_SCRIPT_HOST_LOCATION[] = "resources/web/assignment"; QString pathForAssignmentScript(const QUuid& assignmentUUID) { - QString newPath(ASSIGNMENT_SCRIPT_HOST_LOCATION); + QString newPath { ServerPathUtils::getDataDirectory() + "/" + QString(ASSIGNMENT_SCRIPT_HOST_LOCATION) }; newPath += "/scripts/"; // append the UUID for this script as the new filename, remove the curly braces newPath += uuidStringWithoutCurlyBraces(assignmentUUID); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index f5d5ff8f40..a4892b446b 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -74,6 +74,9 @@ private slots: void sendHeartbeatToIceServer(); void handleConnectedNode(SharedNodePointer newNode); + + void handleTempDomainSuccess(QNetworkReply& requestReply); + void handleTempDomainError(QNetworkReply& requestReply); private: void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid()); @@ -81,10 +84,13 @@ private: bool optionallyReadX509KeyAndCertificate(); bool optionallySetupAssignmentPayment(); + void optionallyGetTemporaryName(const QStringList& arguments); + bool didSetupAccountManagerWithAccessToken(); bool resetAccountManagerAccessToken(); void setupAutomaticNetworking(); + void setupICEHeartbeatForFullNetworking(); void sendHeartbeatToDataServer(const QString& networkAddress); unsigned int countConnectedUsers(); @@ -144,6 +150,8 @@ private: DomainServerSettingsManager _settingsManager; HifiSockAddr _iceServerSocket; + + QTimer* _iceHeartbeatTimer { nullptr }; // this looks like it dangles when created but it's parented to the DomainServer friend class DomainGatekeeper; }; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 888df219dd..e10007784f 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -193,7 +193,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection qDebug() << "DomainServerSettingsManager postedObject -" << postedObject; // we recurse one level deep below each group for the appropriate setting - recurseJSONObjectAndOverwriteSettings(postedObject, _configMap.getUserConfig()); + recurseJSONObjectAndOverwriteSettings(postedObject); // store whatever the current _settingsMap is to file persistToFile(); @@ -407,8 +407,9 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson return QJsonObject(); } -void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, - QVariantMap& settingsVariant) { +void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) { + auto& settingsVariant = _configMap.getUserConfig(); + // Iterate on the setting groups foreach(const QString& rootKey, postedObject.keys()) { QJsonValue rootValue = postedObject[rootKey]; @@ -481,6 +482,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ settingsVariant.remove(rootKey); } } + + // re-merge the user and master configs after a settings change + _configMap.mergeMasterAndUserConfigs(); } void DomainServerSettingsManager::persistToFile() { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 7f71a89da5..d6dd5070a9 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -46,7 +46,7 @@ private slots: private: QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false); - void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant); + void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject); void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription); @@ -56,6 +56,8 @@ private: double _descriptionVersion; QJsonArray _descriptionArray; HifiConfigVariantMap _configMap; + + friend class DomainServer; }; #endif // hifi_DomainServerSettingsManager_h diff --git a/ice-server/src/IceServer.cpp b/ice-server/src/IceServer.cpp index f65ff4a8cf..2baa7a13a7 100644 --- a/ice-server/src/IceServer.cpp +++ b/ice-server/src/IceServer.cpp @@ -27,7 +27,7 @@ IceServer::IceServer(int argc, char* argv[]) : _id(QUuid::createUuid()), _serverSocket(), _activePeers(), - _httpManager(ICE_SERVER_MONITORING_PORT, QString("%1/web/").arg(QCoreApplication::applicationDirPath()), this) + _httpManager(QHostAddress::AnyIPv4, ICE_SERVER_MONITORING_PORT, QString("%1/web/").arg(QCoreApplication::applicationDirPath()), this) { // start the ice-server socket qDebug() << "ice-server socket is listening on" << ICE_SERVER_DEFAULT_PORT; diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 5d96b95624..7b0545c277 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -20,12 +20,10 @@ find_package(Qt5LinguistTools REQUIRED) find_package(Qt5LinguistToolsMacros) if (WIN32) - add_definitions(-D_USE_MATH_DEFINES) # apparently needed to get M_PI and other defines from cmath/math.h - add_definitions(-DWINDOWS_LEAN_AND_MEAN) # needed to make sure windows doesn't go to crazy with its defines + add_definitions(-D_USE_MATH_DEFINES) # apparently needed to get M_PI and other defines from cmath/math.h + add_definitions(-DWINDOWS_LEAN_AND_MEAN) # needed to make sure windows doesn't go to crazy with its defines endif() -include_application_version() - # grab the implementation and header files from src dirs file(GLOB_RECURSE INTERFACE_SRCS "src/*.cpp" "src/*.h") GroupSources("src") @@ -45,8 +43,8 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () -find_package(Qt5 COMPONENTS - Gui Multimedia Network OpenGL Qml Quick Script Svg +find_package(Qt5 COMPONENTS + Gui Multimedia Network OpenGL Qml Quick Script Svg WebChannel WebEngine WebEngineWidgets WebKitWidgets WebSockets) # grab the ui files in resources/ui @@ -65,23 +63,24 @@ set(INTERFACE_SRCS ${INTERFACE_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") if (APPLE) # configure CMake to use a custom Info.plist - SET_TARGET_PROPERTIES( ${this_target} PROPERTIES MACOSX_BUNDLE_INFO_PLIST MacOSXBundleInfo.plist.in ) + set_target_properties(${this_target} PROPERTIES MACOSX_BUNDLE_INFO_PLIST MacOSXBundleInfo.plist.in) - set(MACOSX_BUNDLE_BUNDLE_NAME Interface) - set(MACOSX_BUNDLE_GUI_IDENTIFIER io.highfidelity.Interface) - - if (UPPER_CMAKE_BUILD_TYPE MATCHES RELEASE OR UPPER_CMAKE_BUILD_TYPE MATCHES RELWITHDEBINFO) - set(ICON_FILENAME "interface.icns") + if (PRODUCTION_BUILD) + set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.interface) else () - set(ICON_FILENAME "interface-beta.icns") + if (DEV_BUILD) + set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.interface-dev) + elseif (PR_BUILD) + set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.interface-pr) + endif () endif () - + # set how the icon shows up in the Info.plist file - SET(MACOSX_BUNDLE_ICON_FILE "${ICON_FILENAME}") + set(MACOSX_BUNDLE_ICON_FILE "${INTERFACE_ICON_FILENAME}") # set where in the bundle to put the resources file - SET_SOURCE_FILES_PROPERTIES(${CMAKE_CURRENT_SOURCE_DIR}/icon/${ICON_FILENAME} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/icon/${INTERFACE_ICON_FILENAME} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + set(DISCOVERED_RESOURCES "") # use the add_resources_to_os_x_bundle macro to recurse into resources @@ -89,28 +88,37 @@ if (APPLE) # append the discovered resources to our list of interface sources list(APPEND INTERFACE_SRCS ${DISCOVERED_RESOURCES}) - - set(INTERFACE_SRCS ${INTERFACE_SRCS} "${CMAKE_CURRENT_SOURCE_DIR}/icon/${ICON_FILENAME}") + + set(INTERFACE_SRCS ${INTERFACE_SRCS} "${CMAKE_CURRENT_SOURCE_DIR}/icon/${INTERFACE_ICON_FILENAME}") endif() # create the executable, make it a bundle on OS X if (APPLE) add_executable(${TARGET_NAME} MACOSX_BUNDLE ${INTERFACE_SRCS} ${QM}) + + # make sure the output name for the .app bundle is correct + set_target_properties(${TARGET_NAME} PROPERTIES OUTPUT_NAME ${INTERFACE_BUNDLE_NAME}) elseif(WIN32) - add_executable(${TARGET_NAME} WIN32 ${INTERFACE_SRCS} ${QM}) + # configure an rc file for the chosen icon + set(CONFIGURE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/icon/${INTERFACE_ICON_FILENAME}") + set(CONFIGURE_ICON_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/Icon.rc") + configure_file("${HF_CMAKE_DIR}/templates/Icon.rc.in" ${CONFIGURE_ICON_RC_OUTPUT}) + + # add an executable that also has the icon itself and the configured rc file as resources + add_executable(${TARGET_NAME} WIN32 ${INTERFACE_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT}) else() add_executable(${TARGET_NAME} ${INTERFACE_SRCS} ${QM}) endif() -# These are external plugins, but we need to do the 'add dependency' here so that their -# binary directories get added to the fixup path -add_dependency_external_projects(sixense) -add_dependency_external_projects(sdl2) +target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/includes") + if (WIN32) - add_dependency_external_projects(OpenVR) -endif() -if(WIN32 OR APPLE) - add_dependency_external_projects(neuron) + # These are external plugins, but we need to do the 'add dependency' here so that their + # binary directories get added to the fixup path + add_dependency_external_projects(sixense) + add_dependency_external_projects(sdl2) + add_dependency_external_projects(OpenVR) + add_dependency_external_projects(neuron) endif() # disable /OPT:REF and /OPT:ICF for the Debug builds @@ -121,11 +129,14 @@ if (WIN32) endif() # link required hifi libraries -link_hifi_libraries(shared octree environment gpu gl procedural model render - recording fbx networking model-networking entities avatars - audio audio-client animation script-engine physics +link_hifi_libraries(shared octree environment gpu gl procedural model render + recording fbx networking model-networking entities avatars + audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater - controllers plugins display-plugins input-plugins ) + controllers plugins display-plugins input-plugins) + +# include the binary directory of render-utils for shader includes +target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries/render-utils") #fixme find a way to express faceshift as a plugin target_bullet() @@ -133,39 +144,39 @@ target_glew() target_opengl() if (WIN32 OR APPLE) - target_faceshift() + target_faceshift() endif() # perform standard include and linking for found externals foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) - + if (${${EXTERNAL}_UPPERCASE}_REQUIRED) find_package(${EXTERNAL} REQUIRED) else () find_package(${EXTERNAL}) endif () - + if (${${EXTERNAL}_UPPERCASE}_FOUND AND NOT DISABLE_${${EXTERNAL}_UPPERCASE}) add_definitions(-DHAVE_${${EXTERNAL}_UPPERCASE}) - + # include the library directories (ignoring warnings) if (NOT ${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS) set(${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIR}) endif () - + include_directories(SYSTEM ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) - + # perform the system include hack for OS X to ignore warnings if (APPLE) foreach(EXTERNAL_INCLUDE_DIR ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}") endforeach() endif () - + if (NOT ${${EXTERNAL}_UPPERCASE}_LIBRARIES) set(${${EXTERNAL}_UPPERCASE}_LIBRARIES ${${${EXTERNAL}_UPPERCASE}_LIBRARY}) endif () - + if (NOT APPLE OR NOT ${${EXTERNAL}_UPPERCASE} MATCHES "SIXENSE") target_link_libraries(${TARGET_NAME} ${${${EXTERNAL}_UPPERCASE}_LIBRARIES}) elseif (APPLE AND NOT INSTALLER_BUILD) @@ -179,15 +190,15 @@ include_directories("${PROJECT_SOURCE_DIR}/src") target_link_libraries( ${TARGET_NAME} - Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL - Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg + Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL + Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg Qt5::WebChannel Qt5::WebEngine Qt5::WebEngineWidgets Qt5::WebKitWidgets ) -# Issue causes build failure unless we add this directory. -# See https://bugreports.qt.io/browse/QTBUG-43351 +# Issue causes build failure unless we add this directory. +# See https://bugreports.qt.io/browse/QTBUG-43351 if (WIN32) - add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) + add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) endif() # assume we are using a Qt build without bearer management @@ -199,31 +210,63 @@ if (APPLE) find_library(AppKit AppKit) target_link_libraries(${TARGET_NAME} ${OpenGL} ${AppKit}) - - # install command for OS X bundle - INSTALL(TARGETS ${TARGET_NAME} - BUNDLE DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/install" COMPONENT Runtime - RUNTIME DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/install" COMPONENT Runtime + + # setup install of OS X interface bundle + install(TARGETS ${TARGET_NAME} + BUNDLE DESTINATION ${INTERFACE_INSTALL_DIR} + COMPONENT ${CLIENT_COMPONENT} ) + + set(SCRIPTS_INSTALL_DIR "${INTERFACE_INSTALL_APP_PATH}/Contents/Resources") + + # call the fixup_interface macro to add required bundling commands for installation + fixup_interface() + else (APPLE) # copy the resources files beside the executable add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PROJECT_SOURCE_DIR}/resources" $/resources + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${CMAKE_SOURCE_DIR}/examples" + $/scripts ) # link target to external libraries if (WIN32) target_link_libraries(${TARGET_NAME} wsock32.lib Winmm.lib) - else (WIN32) - # Nothing else required on linux apparently + + # setup install of executable and things copied by fixup/windeployqt + install( + FILES "$/" + DESTINATION ${INTERFACE_INSTALL_DIR} + COMPONENT ${CLIENT_COMPONENT} + ) + + set(SCRIPTS_INSTALL_DIR "${INTERFACE_INSTALL_DIR}") + + set(EXECUTABLE_COMPONENT ${CLIENT_COMPONENT}) + + optional_win_executable_signing() endif() endif (APPLE) -if (WIN32) - set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") +if (SCRIPTS_INSTALL_DIR) + # setup install of scripts beside interface executable + install( + DIRECTORY "${CMAKE_SOURCE_DIR}/examples/" + DESTINATION ${SCRIPTS_INSTALL_DIR}/scripts + COMPONENT ${CLIENT_COMPONENT} + ) endif() -package_libraries_for_deployment() -consolidate_stack_components() +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") + + set(TARGET_INSTALL_DIR ${INTERFACE_INSTALL_DIR}) + set(TARGET_INSTALL_COMPONENT ${CLIENT_COMPONENT}) + manually_install_ssl_eay() + + package_libraries_for_deployment() +endif() diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a56e7bd938..a6d396400e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -36,6 +36,9 @@ #include #include +#include +#include + #include #include #include @@ -57,7 +60,7 @@ #include #include #include -#include +#include #include #include #include @@ -302,7 +305,7 @@ bool setupEssentials(int& argc, char** argv) { listenPort = atoi(portStr); } // Set build version - QCoreApplication::setApplicationVersion(BUILD_VERSION); + QCoreApplication::setApplicationVersion(BuildInfo::VERSION); Setting::preInit(); @@ -5175,3 +5178,30 @@ void Application::setActiveDisplayPlugin(const QString& pluginName) { } updateDisplayMode(); } + +void Application::handleLocalServerConnection() { + auto server = qobject_cast(sender()); + + qDebug() << "Got connection on local server from additional instance - waiting for parameters"; + + auto socket = server->nextPendingConnection(); + + connect(socket, &QLocalSocket::readyRead, this, &Application::readArgumentsFromLocalSocket); + + qApp->getWindow()->raise(); + qApp->getWindow()->activateWindow(); +} + +void Application::readArgumentsFromLocalSocket() { + auto socket = qobject_cast(sender()); + + auto message = socket->readAll(); + socket->deleteLater(); + + qDebug() << "Read from connection: " << message; + + // If we received a message, try to open it as a URL + if (message.length() > 0) { + qApp->openUrl(QString::fromUtf8(message)); + } +} diff --git a/interface/src/Application.h b/interface/src/Application.h index 30cda33051..efcefa0c6e 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -89,7 +89,7 @@ class Application; class Application : public QApplication, public AbstractViewStateInterface, public AbstractScriptingServicesInterface, public AbstractUriHandler { Q_OBJECT - + // TODO? Get rid of those friend class OctreePacketProcessor; friend class PluginContainerProxy; @@ -102,15 +102,15 @@ public: Application(int& argc, char** argv, QElapsedTimer& startup_time); ~Application(); - void postLambdaEvent(std::function f); + void postLambdaEvent(std::function f) override; void initializeGL(); void initializeUi(); void paintGL(); void resizeGL(); - bool event(QEvent* event); - bool eventFilter(QObject* object, QEvent* event); + bool event(QEvent* event) override; + bool eventFilter(QObject* object, QEvent* event) override; glm::uvec2 getCanvasSize() const; glm::uvec2 getUiSize() const; @@ -129,7 +129,7 @@ public: // passes, mirror window passes, etc ViewFrustum* getDisplayViewFrustum(); const ViewFrustum* getDisplayViewFrustum() const; - ViewFrustum* getShadowViewFrustum() { return &_shadowViewFrustum; } + ViewFrustum* getShadowViewFrustum() override { return &_shadowViewFrustum; } const OctreePacketProcessor& getOctreePacketProcessor() const { return _octreeProcessor; } EntityTreeRenderer* getEntities() { return DependencyManager::get().data(); } QUndoStack* getUndoStack() { return &_undoStack; } @@ -151,7 +151,7 @@ public: Overlays& getOverlays() { return _overlays; } bool isForeground() const { return _isForeground; } - + uint32_t getFrameCount() { return _frameCount; } float getFps() const { return _fps; } float getTargetFrameRate(); // frames/second @@ -164,7 +164,7 @@ public: NodeToOctreeSceneStats* getOcteeSceneStats() { return &_octreeServerSceneStats; } virtual controller::ScriptingInterface* getControllerScriptingInterface() { return _controllerScriptingInterface; } - virtual void registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine); + virtual void registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine) override; QImage renderAvatarBillboard(RenderArgs* renderArgs); @@ -208,9 +208,9 @@ public: void setMaxOctreePacketsPerSecond(int maxOctreePPS); int getMaxOctreePacketsPerSecond(); - render::ScenePointer getMain3DScene() { return _main3DScene; } + render::ScenePointer getMain3DScene() override { return _main3DScene; } render::ScenePointer getMain3DScene() const { return _main3DScene; } - render::EnginePointer getRenderEngine() { return _renderEngine; } + render::EnginePointer getRenderEngine() override { return _renderEngine; } gpu::ContextPointer getGPUContext() const { return _gpuContext; } const QRect& getMirrorViewRect() const { return _mirrorViewRect; } @@ -245,6 +245,9 @@ public slots: void toggleLogDialog(); void toggleRunningScriptsWidget(); + void handleLocalServerConnection(); + void readArgumentsFromLocalSocket(); + void showFriendsWindow(); void packageModel(); @@ -258,7 +261,7 @@ public slots: void resetSensors(bool andReload = false); void setActiveFaceTracker(); - + #ifdef HAVE_IVIEWHMD void setActiveEyeTracker(); void calibrateEyeTracker1Point(); @@ -275,11 +278,11 @@ public slots: void reloadResourceCaches(); void crashApplication(); - + void rotationModeChanged(); - + void runTests(); - + private slots: void clearDomainOctreeDetails(); void idle(uint64_t now); @@ -291,12 +294,12 @@ private slots: void faceTrackerMuteToggled(); void activeChanged(Qt::ApplicationState state); - + void domainSettingsReceived(const QJsonObject& domainSettingsObject); void handleDomainConnectionDeniedPacket(QSharedPointer message); - + void notifyPacketVersionMismatch(); - + void loadSettings(); void saveSettings(); @@ -318,13 +321,13 @@ private slots: void packetSent(quint64 length); void updateDisplayMode(); void updateInputModes(); - + private: void initDisplay(); void init(); void cleanupBeforeQuit(); - + void emptyLocalCache(); void update(float deltaTime); @@ -344,45 +347,45 @@ private: void renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool billboard = false); int sendNackPackets(); - + void takeSnapshot(); - + MyAvatar* getMyAvatar() const; - + void checkSkeleton(); - + void initializeAcceptedFiles(); int getRenderAmbientLight() const; - + void displaySide(RenderArgs* renderArgs, Camera& whichCamera, bool selfAvatarOnly = false, bool billboard = false); - + bool importSVOFromURL(const QString& urlString); - + bool nearbyEntitiesAreReadyForPhysics(); int processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode); void trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket); - + void resizeEvent(QResizeEvent* size); - + void keyPressEvent(QKeyEvent* event); void keyReleaseEvent(QKeyEvent* event); - + void focusOutEvent(QFocusEvent* event); void focusInEvent(QFocusEvent* event); - + void mouseMoveEvent(QMouseEvent* event, unsigned int deviceID = 0); void mousePressEvent(QMouseEvent* event, unsigned int deviceID = 0); void mouseDoublePressEvent(QMouseEvent* event, unsigned int deviceID = 0); void mouseReleaseEvent(QMouseEvent* event, unsigned int deviceID = 0); - + void touchBeginEvent(QTouchEvent* event); void touchEndEvent(QTouchEvent* event); void touchUpdateEvent(QTouchEvent* event); - + void wheelEvent(QWheelEvent* event); void dropEvent(QDropEvent* event); void dragEnterEvent(QDragEnterEvent* event); - + bool _dependencyManagerIsSetup; @@ -461,7 +464,7 @@ private: quint64 _lastNackTime; quint64 _lastSendDownstreamAudioStats; - + bool _aboutToQuit; Bookmarks* _bookmarks; @@ -470,9 +473,9 @@ private: QThread _settingsThread; QTimer _settingsTimer; - + GLCanvas* _glWidget{ nullptr }; - + typedef bool (Application::* AcceptURLMethod)(const QString &); static const QHash _acceptedExtensions; diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp index c8fd5188e2..3305cc4f3b 100644 --- a/interface/src/Stars.cpp +++ b/interface/src/Stars.cpp @@ -22,11 +22,11 @@ #include #include -#include "../../libraries/render-utils/stars_vert.h" -#include "../../libraries/render-utils/stars_frag.h" +#include +#include -#include "../../libraries/render-utils/standardTransformPNTC_vert.h" -#include "../../libraries/render-utils/starsGrid_frag.h" +#include +#include //static const float TILT = 0.23f; static const float TILT = 0.0f; diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index b97ec59780..7646f87238 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -30,7 +30,7 @@ public: virtual void updateActionWorker(float deltaTimeStep) override; - QByteArray serialize() const; + QByteArray serialize() const override; virtual void deserialize(QByteArray serializedArguments) override; virtual bool shouldSuppressLocationEdits() override { return _active && !_ownerEntity.expired(); } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 1de8f9224c..89511a4d9b 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -11,7 +11,10 @@ #include #include #include +#include +#include #include +#include #include #include @@ -19,71 +22,71 @@ #include "AddressManager.h" #include "Application.h" #include "InterfaceLogging.h" - -#ifdef Q_OS_WIN -static BOOL CALLBACK enumWindowsCallback(HWND hWnd, LPARAM lParam) { - const UINT TIMEOUT = 200; // ms - DWORD_PTR response; - LRESULT result = SendMessageTimeout(hWnd, UWM_IDENTIFY_INSTANCES, 0, 0, SMTO_BLOCK | SMTO_ABORTIFHUNG, TIMEOUT, &response); - if (result == 0) { // Timeout; continue search. - return TRUE; - } - if (response == UWM_IDENTIFY_INSTANCES) { - HWND* target = (HWND*)lParam; - *target = hWnd; - return FALSE; // Found; terminate search. - } - return TRUE; // Not found; continue search. -} -#endif +#include "MainWindow.h" int main(int argc, const char* argv[]) { + QString applicationName = "High Fidelity Interface - " + qgetenv("USERNAME"); + + bool instanceMightBeRunning = true; + #ifdef Q_OS_WIN - // Run only one instance of Interface at a time. - HANDLE mutex = CreateMutex(NULL, FALSE, "High Fidelity Interface - " + qgetenv("USERNAME")); - DWORD result = GetLastError(); - if (result == ERROR_ALREADY_EXISTS || result == ERROR_ACCESS_DENIED) { - // Interface is already running. - HWND otherInstance = NULL; - EnumWindows(enumWindowsCallback, (LPARAM)&otherInstance); - if (otherInstance) { - // Show other instance. - SendMessage(otherInstance, UWM_SHOW_APPLICATION, 0, 0); + // Try to create a shared memory block - if it can't be created, there is an instance of + // interface already running. We only do this on Windows for now because of the potential + // for crashed instances to leave behind shared memory instances on unix. + QSharedMemory sharedMemory { applicationName }; + instanceMightBeRunning = !sharedMemory.create(1, QSharedMemory::ReadOnly); +#endif - // Send command line --url value to other instance. - if (argc >= 3) { - QStringList arguments; - for (int i = 0; i < argc; i += 1) { - arguments << argv[i]; - } + if (instanceMightBeRunning) { + // Try to connect and send message to existing interface instance + QLocalSocket socket; - QCommandLineParser parser; - QCommandLineOption urlOption("url", "", "value"); - parser.addOption(urlOption); - parser.process(arguments); + socket.connectToServer(applicationName); - if (parser.isSet(urlOption)) { - QUrl url = QUrl(parser.value(urlOption)); - if (url.isValid() && url.scheme() == HIFI_URL_SCHEME) { - QByteArray urlBytes = url.toString().toLatin1(); - const char* urlChars = urlBytes.data(); - COPYDATASTRUCT cds; - cds.cbData = urlBytes.length() + 1; - cds.lpData = (PVOID)urlChars; - SendMessage(otherInstance, WM_COPYDATA, 0, (LPARAM)&cds); + static const int LOCAL_SERVER_TIMEOUT_MS = 500; + + // Try to connect - if we can't connect, interface has probably just gone down + if (socket.waitForConnected(LOCAL_SERVER_TIMEOUT_MS)) { + + QStringList arguments; + for (int i = 0; i < argc; ++i) { + arguments << argv[i]; + } + + QCommandLineParser parser; + QCommandLineOption urlOption("url", "", "value"); + parser.addOption(urlOption); + parser.process(arguments); + + if (parser.isSet(urlOption)) { + QUrl url = QUrl(parser.value(urlOption)); + if (url.isValid() && url.scheme() == HIFI_URL_SCHEME) { + qDebug() << "Writing URL to local socket"; + socket.write(url.toString().toUtf8()); + if (!socket.waitForBytesWritten(5000)) { + qDebug() << "Error writing URL to local socket"; } } } + + socket.close(); + + qDebug() << "Interface instance appears to be running, exiting"; + + return EXIT_SUCCESS; } - return 0; - } + +#ifdef Q_OS_WIN + return EXIT_SUCCESS; #endif + } + QElapsedTimer startupTime; startupTime.start(); - - // Debug option to demonstrate that the client's local time does not - // need to be in sync with any other network node. This forces clock + + // Debug option to demonstrate that the client's local time does not + // need to be in sync with any other network node. This forces clock // skew for the individual client const char* CLOCK_SKEW = "--clockSkew"; const char* clockSkewOption = getCmdOption(argc, argv, CLOCK_SKEW); @@ -102,19 +105,26 @@ int main(int argc, const char* argv[]) { QSettings::setDefaultFormat(QSettings::IniFormat); Application app(argc, const_cast(argv), startupTime); + // Setup local server + QLocalServer server { &app }; + + // We failed to connect to a local server, so we remove any existing servers. + server.removeServer(applicationName); + server.listen(applicationName); + + QObject::connect(&server, &QLocalServer::newConnection, &app, &Application::handleLocalServerConnection); + QTranslator translator; translator.load("i18n/interface_en"); app.installTranslator(&translator); - + qCDebug(interfaceapp, "Created QT Application."); exitCode = app.exec(); + server.close(); } Application::shutdownPlugins(); -#ifdef Q_OS_WIN - ReleaseMutex(mutex); -#endif qCDebug(interfaceapp, "Normal exit."); return exitCode; -} +} diff --git a/libraries/embedded-webserver/src/HTTPManager.cpp b/libraries/embedded-webserver/src/HTTPManager.cpp index 4c804f2e7b..7cbdf01bbc 100644 --- a/libraries/embedded-webserver/src/HTTPManager.cpp +++ b/libraries/embedded-webserver/src/HTTPManager.cpp @@ -23,8 +23,9 @@ const int SOCKET_ERROR_EXIT_CODE = 2; const int SOCKET_CHECK_INTERVAL_IN_MS = 30000; -HTTPManager::HTTPManager(quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler, QObject* parent) : +HTTPManager::HTTPManager(const QHostAddress& listenAddress, quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler, QObject* parent) : QTcpServer(parent), + _listenAddress(listenAddress), _documentRoot(documentRoot), _requestHandler(requestHandler), _port(port) @@ -178,7 +179,7 @@ void HTTPManager::isTcpServerListening() { bool HTTPManager::bindSocket() { qCDebug(embeddedwebserver) << "Attempting to bind TCP socket on port " << QString::number(_port); - if (listen(QHostAddress::AnyIPv4, _port)) { + if (listen(_listenAddress, _port)) { qCDebug(embeddedwebserver) << "TCP socket is listening on" << serverAddress() << "and port" << serverPort(); return true; diff --git a/libraries/embedded-webserver/src/HTTPManager.h b/libraries/embedded-webserver/src/HTTPManager.h index 03498fbe8d..90a896ccf5 100644 --- a/libraries/embedded-webserver/src/HTTPManager.h +++ b/libraries/embedded-webserver/src/HTTPManager.h @@ -33,7 +33,7 @@ class HTTPManager : public QTcpServer, public HTTPRequestHandler { Q_OBJECT public: /// Initializes the manager. - HTTPManager(quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler = NULL, QObject* parent = 0); + HTTPManager(const QHostAddress& listenAddress, quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler = NULL, QObject* parent = 0); bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false); @@ -49,6 +49,7 @@ protected: virtual void incomingConnection(qintptr socketDescriptor); virtual bool requestHandledByRequestHandler(HTTPConnection* connection, const QUrl& url); + QHostAddress _listenAddress; QString _documentRoot; HTTPRequestHandler* _requestHandler; QTimer* _isListeningTimer; diff --git a/libraries/embedded-webserver/src/HTTPSManager.cpp b/libraries/embedded-webserver/src/HTTPSManager.cpp index a745d7605e..ee61f15457 100644 --- a/libraries/embedded-webserver/src/HTTPSManager.cpp +++ b/libraries/embedded-webserver/src/HTTPSManager.cpp @@ -15,9 +15,9 @@ #include "HTTPSManager.h" -HTTPSManager::HTTPSManager(quint16 port, const QSslCertificate& certificate, const QSslKey& privateKey, +HTTPSManager::HTTPSManager(QHostAddress listenAddress, quint16 port, const QSslCertificate& certificate, const QSslKey& privateKey, const QString& documentRoot, HTTPSRequestHandler* requestHandler, QObject* parent) : - HTTPManager(port, documentRoot, requestHandler, parent), + HTTPManager(listenAddress, port, documentRoot, requestHandler, parent), _certificate(certificate), _privateKey(privateKey), _sslRequestHandler(requestHandler) diff --git a/libraries/embedded-webserver/src/HTTPSManager.h b/libraries/embedded-webserver/src/HTTPSManager.h index 66c0c76d0b..2d3cc9ed62 100644 --- a/libraries/embedded-webserver/src/HTTPSManager.h +++ b/libraries/embedded-webserver/src/HTTPSManager.h @@ -26,7 +26,8 @@ public: class HTTPSManager : public HTTPManager, public HTTPSRequestHandler { Q_OBJECT public: - HTTPSManager(quint16 port, + HTTPSManager(QHostAddress listenAddress, + quint16 port, const QSslCertificate& certificate, const QSslKey& privateKey, const QString& documentRoot, @@ -35,12 +36,12 @@ public: void setCertificate(const QSslCertificate& certificate) { _certificate = certificate; } void setPrivateKey(const QSslKey& privateKey) { _privateKey = privateKey; } - bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false); - bool handleHTTPSRequest(HTTPSConnection* connection, const QUrl& url, bool skipSubHandler = false); + bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; + bool handleHTTPSRequest(HTTPSConnection* connection, const QUrl& url, bool skipSubHandler = false) override; protected: - void incomingConnection(qintptr socketDescriptor); - bool requestHandledByRequestHandler(HTTPConnection* connection, const QUrl& url); + void incomingConnection(qintptr socketDescriptor) override; + bool requestHandledByRequestHandler(HTTPConnection* connection, const QUrl& url) override; private: QSslCertificate _certificate; QSslKey _privateKey; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 212b71759f..d7d8d65e3a 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -39,7 +39,7 @@ public: }; namespace render { - template <> const ItemKey payloadGetKey(const RenderableEntityItemProxy::Pointer& payload); + template <> const ItemKey payloadGetKey(const RenderableEntityItemProxy::Pointer& payload); template <> const Item::Bound payloadGetBound(const RenderableEntityItemProxy::Pointer& payload); template <> void payloadRender(const RenderableEntityItemProxy::Pointer& payload, RenderArgs* args); } @@ -73,8 +73,8 @@ private: #define SIMPLE_RENDERABLE() \ public: \ - virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) { return _renderHelper.addToScene(self, scene, pendingChanges); } \ - virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) { _renderHelper.removeFromScene(self, scene, pendingChanges); } \ + virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override { return _renderHelper.addToScene(self, scene, pendingChanges); } \ + virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override { _renderHelper.removeFromScene(self, scene, pendingChanges); } \ private: \ SimpleRenderableEntityItem _renderHelper; diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index d320610d83..dfde97c407 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -27,19 +27,19 @@ public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); static void createPipeline(); RenderablePolyLineEntityItem(const EntityItemID& entityItemID); - - virtual void render(RenderArgs* args); + + virtual void render(RenderArgs* args) override; virtual void update(const quint64& now) override; - virtual bool needsToCallUpdate() const { return true; }; - + virtual bool needsToCallUpdate() const override { return true; }; + SIMPLE_RENDERABLE(); - + NetworkTexturePointer _texture; static gpu::PipelinePointer _pipeline; static gpu::Stream::FormatPointer _format; static int32_t PAINTSTROKE_GPU_SLOT; - + protected: void updateGeometry(); void updateVertices(); diff --git a/libraries/networking/CMakeLists.txt b/libraries/networking/CMakeLists.txt index 470c9145df..53ebb86b83 100644 --- a/libraries/networking/CMakeLists.txt +++ b/libraries/networking/CMakeLists.txt @@ -2,6 +2,8 @@ set(TARGET_NAME networking) setup_hifi_library(Network) link_hifi_libraries(shared) +target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/includes") + if (WIN32) # we need ws2_32.lib on windows, but it's static so we don't bubble it up target_link_libraries(${TARGET_NAME} ws2_32.lib) @@ -31,4 +33,3 @@ endif (UNIX) # append tbb includes to our list of includes to bubble target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${TBB_INCLUDE_DIRS}) -include_application_version() diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index e6163c776b..0114bcb708 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -12,11 +12,13 @@ #include "udt/PacketHeaders.h" #include "SharedUtil.h" #include "UUID.h" +#include "ServerPathUtils.h" #include -#include +#include #include "Assignment.h" +#include Assignment::Type Assignment::typeForNodeType(NodeType_t nodeType) { switch (nodeType) { @@ -64,7 +66,7 @@ Assignment::Assignment(Assignment::Command command, Assignment::Type type, const // this is a newly created assignment, generate a random UUID _uuid = QUuid::createUuid(); } else if (_command == Assignment::RequestCommand) { - _nodeVersion = BUILD_VERSION; + _nodeVersion = BuildInfo::VERSION; } } diff --git a/libraries/networking/src/HifiSockAddr.cpp b/libraries/networking/src/HifiSockAddr.cpp index e6c2f2985a..81a58df730 100644 --- a/libraries/networking/src/HifiSockAddr.cpp +++ b/libraries/networking/src/HifiSockAddr.cpp @@ -95,16 +95,16 @@ void HifiSockAddr::handleLookupResult(const QHostInfo& hostInfo) { if (hostInfo.error() != QHostInfo::NoError) { qCDebug(networking) << "Lookup failed for" << hostInfo.lookupId() << ":" << hostInfo.errorString(); emit lookupFailed(); - } - - foreach(const QHostAddress& address, hostInfo.addresses()) { - // just take the first IPv4 address - if (address.protocol() == QAbstractSocket::IPv4Protocol) { - _address = address; - qCDebug(networking) << "QHostInfo lookup result for" - << hostInfo.hostName() << "with lookup ID" << hostInfo.lookupId() << "is" << address.toString(); - emit lookupCompleted(); - break; + } else { + foreach(const QHostAddress& address, hostInfo.addresses()) { + // just take the first IPv4 address + if (address.protocol() == QAbstractSocket::IPv4Protocol) { + _address = address; + qCDebug(networking) << "QHostInfo lookup result for" + << hostInfo.hostName() << "with lookup ID" << hostInfo.lookupId() << "is" << address.toString(); + emit lookupCompleted(); + break; + } } } } diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 0b194b017e..485100da0a 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -61,7 +61,7 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short firstCall = false; } - + qRegisterMetaType("ConnectionStep"); _nodeSocket.bind(QHostAddress::AnyIPv4, socketListenPort); @@ -87,7 +87,7 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short // check the local socket right now updateLocalSockAddr(); - + // set &PacketReceiver::handleVerifiedPacket as the verified packet callback for the udt::Socket _nodeSocket.setPacketHandler( [this](std::unique_ptr packet) { @@ -108,8 +108,16 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short // set our isPacketVerified method as the verify operator for the udt::Socket using std::placeholders::_1; _nodeSocket.setPacketFilterOperator(std::bind(&LimitedNodeList::isPacketVerified, this, _1)); - + _packetStatTimer.start(); + + if (_stunSockAddr.getAddress().isNull()) { + // we don't know the stun server socket yet, add it to unfiltered once known + connect(&_stunSockAddr, &HifiSockAddr::lookupCompleted, this, &LimitedNodeList::addSTUNHandlerToUnfiltered); + } else { + // we know the stun server socket, add it to unfiltered now + addSTUNHandlerToUnfiltered(); + } } void LimitedNodeList::setSessionUUID(const QUuid& sessionUUID) { @@ -161,42 +169,42 @@ bool LimitedNodeList::isPacketVerified(const udt::Packet& packet) { bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { PacketType headerType = NLPacket::typeInHeader(packet); PacketVersion headerVersion = NLPacket::versionInHeader(packet); - + if (headerVersion != versionForPacketType(headerType)) { - + static QMultiHash sourcedVersionDebugSuppressMap; static QMultiHash versionDebugSuppressMap; - + bool hasBeenOutput = false; QString senderString; - + if (NON_SOURCED_PACKETS.contains(headerType)) { const HifiSockAddr& senderSockAddr = packet.getSenderSockAddr(); hasBeenOutput = versionDebugSuppressMap.contains(senderSockAddr, headerType); - + if (!hasBeenOutput) { versionDebugSuppressMap.insert(senderSockAddr, headerType); senderString = QString("%1:%2").arg(senderSockAddr.getAddress().toString()).arg(senderSockAddr.getPort()); } } else { QUuid sourceID = NLPacket::sourceIDInHeader(packet); - + hasBeenOutput = sourcedVersionDebugSuppressMap.contains(sourceID, headerType); - + if (!hasBeenOutput) { sourcedVersionDebugSuppressMap.insert(sourceID, headerType); senderString = uuidStringWithoutCurlyBraces(sourceID.toString()); } } - + if (!hasBeenOutput) { qCDebug(networking) << "Packet version mismatch on" << headerType << "- Sender" << senderString << "sent" << qPrintable(QString::number(headerVersion)) << "but" << qPrintable(QString::number(versionForPacketType(headerType))) << "expected."; - + emit packetVersionMismatch(headerType); } - + return false; } else { return true; @@ -204,27 +212,27 @@ bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { } bool LimitedNodeList::packetSourceAndHashMatch(const udt::Packet& packet) { - + PacketType headerType = NLPacket::typeInHeader(packet); - + if (NON_SOURCED_PACKETS.contains(headerType)) { return true; } else { QUuid sourceID = NLPacket::sourceIDInHeader(packet); - + // figure out which node this is from SharedNodePointer matchingNode = nodeWithUUID(sourceID); - + if (matchingNode) { if (!NON_VERIFIED_PACKETS.contains(headerType)) { - + QByteArray packetHeaderHash = NLPacket::verificationHashInHeader(packet); QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, matchingNode->getConnectionSecret()); - + // check if the md5 hash in the header matches the hash we would expect if (packetHeaderHash != expectedHash) { static QMultiMap hashDebugSuppressMap; - + if (!hashDebugSuppressMap.contains(sourceID, headerType)) { qCDebug(networking) << "Packet hash mismatch on" << headerType << "- Sender" << sourceID; @@ -238,15 +246,15 @@ bool LimitedNodeList::packetSourceAndHashMatch(const udt::Packet& packet) { // No matter if this packet is handled or not, we update the timestamp for the last time we heard // from this sending node matchingNode->setLastHeardMicrostamp(usecTimestampNow()); - + return true; - + } else { static const QString UNKNOWN_REGEX = "Packet of type \\d+ \\([\\sa-zA-Z:]+\\) received from unknown node with UUID"; static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex(UNKNOWN_REGEX); - qCDebug(networking) << "Packet of type" << headerType + qCDebug(networking) << "Packet of type" << headerType << "received from unknown node with UUID" << qPrintable(uuidStringWithoutCurlyBraces(sourceID)); } } @@ -264,7 +272,7 @@ void LimitedNodeList::fillPacketHeader(const NLPacket& packet, const QUuid& conn if (!NON_SOURCED_PACKETS.contains(packet.getType())) { packet.writeSourceID(getSessionUUID()); } - + if (!connectionSecret.isNull() && !NON_SOURCED_PACKETS.contains(packet.getType()) && !NON_VERIFIED_PACKETS.contains(packet.getType())) { @@ -274,14 +282,14 @@ void LimitedNodeList::fillPacketHeader(const NLPacket& packet, const QUuid& conn qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const Node& destinationNode) { Q_ASSERT(!packet.isPartOfMessage()); - + if (!destinationNode.getActiveSocket()) { return 0; } - + emit dataSent(destinationNode.getType(), packet.getDataSize()); destinationNode.recordBytesSent(packet.getDataSize()); - + return sendUnreliablePacket(packet, *destinationNode.getActiveSocket(), destinationNode.getConnectionSecret()); } @@ -290,21 +298,21 @@ qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const HifiS Q_ASSERT(!packet.isPartOfMessage()); Q_ASSERT_X(!packet.isReliable(), "LimitedNodeList::sendUnreliablePacket", "Trying to send a reliable packet unreliably."); - + collectPacketStats(packet); fillPacketHeader(packet, connectionSecret); - + return _nodeSocket.writePacket(packet, sockAddr); } qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& destinationNode) { Q_ASSERT(!packet->isPartOfMessage()); auto activeSocket = destinationNode.getActiveSocket(); - + if (activeSocket) { emit dataSent(destinationNode.getType(), packet->getDataSize()); destinationNode.recordBytesSent(packet->getDataSize()); - + return sendPacket(std::move(packet), *activeSocket, destinationNode.getConnectionSecret()); } else { qDebug() << "LimitedNodeList::sendPacket called without active socket for node" << destinationNode << "- not sending"; @@ -318,10 +326,10 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const HifiS if (packet->isReliable()) { collectPacketStats(*packet); fillPacketHeader(*packet, connectionSecret); - + auto size = packet->getDataSize(); _nodeSocket.writePacket(std::move(packet), sockAddr); - + return size; } else { return sendUnreliablePacket(*packet, sockAddr, connectionSecret); @@ -330,18 +338,18 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const HifiS qint64 LimitedNodeList::sendPacketList(NLPacketList& packetList, const Node& destinationNode) { auto activeSocket = destinationNode.getActiveSocket(); - + if (activeSocket) { qint64 bytesSent = 0; auto connectionSecret = destinationNode.getConnectionSecret(); - + // close the last packet in the list packetList.closeCurrentPacket(); - + while (!packetList._packets.empty()) { bytesSent += sendPacket(packetList.takeFront(), *activeSocket, connectionSecret); } - + emit dataSent(destinationNode.getType(), bytesSent); return bytesSent; } else { @@ -354,21 +362,21 @@ qint64 LimitedNodeList::sendPacketList(NLPacketList& packetList, const Node& des qint64 LimitedNodeList::sendPacketList(NLPacketList& packetList, const HifiSockAddr& sockAddr, const QUuid& connectionSecret) { qint64 bytesSent = 0; - + // close the last packet in the list packetList.closeCurrentPacket(); - + while (!packetList._packets.empty()) { bytesSent += sendPacket(packetList.takeFront(), sockAddr, connectionSecret); } - + return bytesSent; } qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList, const HifiSockAddr& sockAddr) { // close the last packet in the list packetList->closeCurrentPacket(); - + for (std::unique_ptr& packet : packetList->_packets) { NLPacket* nlPacket = static_cast(packet.get()); collectPacketStats(*nlPacket); @@ -383,13 +391,13 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList, if (activeSocket) { // close the last packet in the list packetList->closeCurrentPacket(); - + for (std::unique_ptr& packet : packetList->_packets) { NLPacket* nlPacket = static_cast(packet.get()); collectPacketStats(*nlPacket); fillPacketHeader(*nlPacket, destinationNode.getConnectionSecret()); } - + return _nodeSocket.writePacketList(std::move(packetList), *activeSocket); } else { qCDebug(networking) << "LimitedNodeList::sendPacketList called without active socket for node. Not sending."; @@ -403,11 +411,11 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& qCDebug(networking) << "LimitedNodeList::sendPacket called without active socket for node. Not sending."; return 0; } - + // use the node's active socket as the destination socket if there is no overriden socket address auto& destinationSockAddr = (overridenSockAddr.isNull()) ? *destinationNode.getActiveSocket() : overridenSockAddr; - + return sendPacket(std::move(packet), destinationSockAddr, destinationNode.getConnectionSecret()); } @@ -423,7 +431,7 @@ int LimitedNodeList::updateNodeWithDataFromPacket(QSharedPointergetMutex()); return linkedData->parseData(*message); } - + return 0; } @@ -453,7 +461,7 @@ void LimitedNodeList::eraseAllNodes() { } } } - + foreach(const SharedNodePointer& killedNode, killedNodes) { handleNodeKill(killedNode); } @@ -461,7 +469,7 @@ void LimitedNodeList::eraseAllNodes() { void LimitedNodeList::reset() { eraseAllNodes(); - + // we need to make sure any socket connections are gone so wait on that here _nodeSocket.clearConnections(); } @@ -474,16 +482,16 @@ bool LimitedNodeList::killNodeWithUUID(const QUuid& nodeUUID) { SharedNodePointer matchingNode = it->second; readLocker.unlock(); - + { QWriteLocker writeLocker(&_nodeMutex); _nodeHash.unsafe_erase(it); } - + handleNodeKill(matchingNode); return true; } - + return false; } @@ -499,7 +507,7 @@ void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) { qCDebug(networking) << "Killed" << *node; node->stopPingTimer(); emit nodeKilled(node); - + if (auto activeSocket = node->getActiveSocket()) { _nodeSocket.cleanupConnection(*activeSocket); } @@ -553,9 +561,9 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t std::unique_ptr LimitedNodeList::constructPingPacket(PingType_t pingType) { int packetSize = sizeof(PingType_t) + sizeof(quint64); - + auto pingPacket = NLPacket::create(PacketType::Ping, packetSize); - + pingPacket->writePrimitive(pingType); pingPacket->writePrimitive(usecTimestampNow()); @@ -605,14 +613,14 @@ std::unique_ptr LimitedNodeList::constructICEPingReplyPacket(ReceivedM unsigned int LimitedNodeList::broadcastToNodes(std::unique_ptr packet, const NodeSet& destinationNodeTypes) { unsigned int n = 0; - + eachNode([&](const SharedNodePointer& node){ if (node && destinationNodeTypes.contains(node->getType())) { sendUnreliablePacket(*packet, *node); ++n; } }); - + return n; } @@ -664,46 +672,48 @@ const int NUM_BYTES_STUN_HEADER = 20; void LimitedNodeList::sendSTUNRequest() { - const int NUM_INITIAL_STUN_REQUESTS_BEFORE_FAIL = 10; + if (!_stunSockAddr.isNull()) { + const int NUM_INITIAL_STUN_REQUESTS_BEFORE_FAIL = 10; - if (!_hasCompletedInitialSTUN) { - qCDebug(networking) << "Sending intial stun request to" << STUN_SERVER_HOSTNAME; + if (!_hasCompletedInitialSTUN) { + qCDebug(networking) << "Sending intial stun request to" << STUN_SERVER_HOSTNAME; - if (_numInitialSTUNRequests > NUM_INITIAL_STUN_REQUESTS_BEFORE_FAIL) { - // we're still trying to do our initial STUN we're over the fail threshold - stopInitialSTUNUpdate(false); + if (_numInitialSTUNRequests > NUM_INITIAL_STUN_REQUESTS_BEFORE_FAIL) { + // we're still trying to do our initial STUN we're over the fail threshold + stopInitialSTUNUpdate(false); + } + + ++_numInitialSTUNRequests; } - ++_numInitialSTUNRequests; + char stunRequestPacket[NUM_BYTES_STUN_HEADER]; + + int packetIndex = 0; + + const uint32_t RFC_5389_MAGIC_COOKIE_NETWORK_ORDER = htonl(RFC_5389_MAGIC_COOKIE); + + // leading zeros + message type + const uint16_t REQUEST_MESSAGE_TYPE = htons(0x0001); + memcpy(stunRequestPacket + packetIndex, &REQUEST_MESSAGE_TYPE, sizeof(REQUEST_MESSAGE_TYPE)); + packetIndex += sizeof(REQUEST_MESSAGE_TYPE); + + // message length (no additional attributes are included) + uint16_t messageLength = 0; + memcpy(stunRequestPacket + packetIndex, &messageLength, sizeof(messageLength)); + packetIndex += sizeof(messageLength); + + memcpy(stunRequestPacket + packetIndex, &RFC_5389_MAGIC_COOKIE_NETWORK_ORDER, sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER)); + packetIndex += sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER); + + // transaction ID (random 12-byte unsigned integer) + const uint NUM_TRANSACTION_ID_BYTES = 12; + QUuid randomUUID = QUuid::createUuid(); + memcpy(stunRequestPacket + packetIndex, randomUUID.toRfc4122().data(), NUM_TRANSACTION_ID_BYTES); + + flagTimeForConnectionStep(ConnectionStep::SendSTUNRequest); + + _nodeSocket.writeDatagram(stunRequestPacket, sizeof(stunRequestPacket), _stunSockAddr); } - - char stunRequestPacket[NUM_BYTES_STUN_HEADER]; - - int packetIndex = 0; - - const uint32_t RFC_5389_MAGIC_COOKIE_NETWORK_ORDER = htonl(RFC_5389_MAGIC_COOKIE); - - // leading zeros + message type - const uint16_t REQUEST_MESSAGE_TYPE = htons(0x0001); - memcpy(stunRequestPacket + packetIndex, &REQUEST_MESSAGE_TYPE, sizeof(REQUEST_MESSAGE_TYPE)); - packetIndex += sizeof(REQUEST_MESSAGE_TYPE); - - // message length (no additional attributes are included) - uint16_t messageLength = 0; - memcpy(stunRequestPacket + packetIndex, &messageLength, sizeof(messageLength)); - packetIndex += sizeof(messageLength); - - memcpy(stunRequestPacket + packetIndex, &RFC_5389_MAGIC_COOKIE_NETWORK_ORDER, sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER)); - packetIndex += sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER); - - // transaction ID (random 12-byte unsigned integer) - const uint NUM_TRANSACTION_ID_BYTES = 12; - QUuid randomUUID = QUuid::createUuid(); - memcpy(stunRequestPacket + packetIndex, randomUUID.toRfc4122().data(), NUM_TRANSACTION_ID_BYTES); - - flagTimeForConnectionStep(ConnectionStep::SendSTUNRequest); - - _nodeSocket.writeDatagram(stunRequestPacket, sizeof(stunRequestPacket), _stunSockAddr); } void LimitedNodeList::processSTUNResponse(std::unique_ptr packet) { @@ -722,7 +732,7 @@ void LimitedNodeList::processSTUNResponse(std::unique_ptr packe // enumerate the attributes to find XOR_MAPPED_ADDRESS_TYPE while (attributeStartIndex < packet->getDataSize()) { - + if (memcmp(packet->getData() + attributeStartIndex, &XOR_MAPPED_ADDRESS_TYPE, sizeof(XOR_MAPPED_ADDRESS_TYPE)) == 0) { const int NUM_BYTES_STUN_ATTR_TYPE_AND_LENGTH = 4; const int NUM_BYTES_FAMILY_ALIGN = 1; @@ -751,7 +761,7 @@ void LimitedNodeList::processSTUNResponse(std::unique_ptr packe uint32_t stunAddress = ntohl(xorMappedAddress) ^ ntohl(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER); QHostAddress newPublicAddress(stunAddress); - + if (newPublicAddress != _publicSockAddr.getAddress() || newPublicPort != _publicSockAddr.getPort()) { _publicSockAddr = HifiSockAddr(newPublicAddress, newPublicPort); @@ -768,7 +778,7 @@ void LimitedNodeList::processSTUNResponse(std::unique_ptr packe flagTimeForConnectionStep(ConnectionStep::SetPublicSocketFromSTUN); } - + // we're done reading the packet so we can return now return; } @@ -788,38 +798,50 @@ void LimitedNodeList::processSTUNResponse(std::unique_ptr packe } void LimitedNodeList::startSTUNPublicSocketUpdate() { - assert(!_initialSTUNTimer); + if (!_initialSTUNTimer ) { + // setup our initial STUN timer here so we can quickly find out our public IP address + _initialSTUNTimer = new QTimer { this }; - if (!_initialSTUNTimer) { - // if we don't know the STUN IP yet we need to have ourselves be called once it is known + connect(_initialSTUNTimer.data(), &QTimer::timeout, this, &LimitedNodeList::sendSTUNRequest); + + const int STUN_INITIAL_UPDATE_INTERVAL_MSECS = 250; + _initialSTUNTimer->setInterval(STUN_INITIAL_UPDATE_INTERVAL_MSECS); + + // if we don't know the STUN IP yet we need to wait until it is known to start STUN requests if (_stunSockAddr.getAddress().isNull()) { - connect(&_stunSockAddr, &HifiSockAddr::lookupCompleted, this, &LimitedNodeList::startSTUNPublicSocketUpdate); - connect(&_stunSockAddr, &HifiSockAddr::lookupCompleted, this, &LimitedNodeList::addSTUNHandlerToUnfiltered); - // in case we just completely fail to lookup the stun socket - add a 10s timeout that will trigger the fail case + // if we fail to lookup the socket then timeout the STUN address lookup + connect(&_stunSockAddr, &HifiSockAddr::lookupFailed, this, &LimitedNodeList::possiblyTimeoutSTUNAddressLookup); + + // immediately send a STUN request once we know the socket + connect(&_stunSockAddr, &HifiSockAddr::lookupCompleted, this, &LimitedNodeList::sendSTUNRequest); + + // start the initial STUN timer once we know the socket + connect(&_stunSockAddr, SIGNAL(lookupCompleted()), _initialSTUNTimer, SLOT(start())); + + // in case we just completely fail to lookup the stun socket - add a 10s single shot that will trigger the fail case const quint64 STUN_DNS_LOOKUP_TIMEOUT_MSECS = 10 * 1000; - QTimer* stunLookupFailTimer = new QTimer(this); - connect(stunLookupFailTimer, &QTimer::timeout, this, &LimitedNodeList::possiblyTimeoutSTUNAddressLookup); - stunLookupFailTimer->start(STUN_DNS_LOOKUP_TIMEOUT_MSECS); + QTimer* lookupTimeoutTimer = new QTimer { this }; + lookupTimeoutTimer->setSingleShot(true); + connect(lookupTimeoutTimer, &QTimer::timeout, this, &LimitedNodeList::possiblyTimeoutSTUNAddressLookup); + + // delete the lookup timeout timer once it has fired + connect(lookupTimeoutTimer, &QTimer::timeout, lookupTimeoutTimer, &QTimer::deleteLater); + + lookupTimeoutTimer->start(STUN_DNS_LOOKUP_TIMEOUT_MSECS); } else { - // setup our initial STUN timer here so we can quickly find out our public IP address - _initialSTUNTimer = new QTimer(this); + _initialSTUNTimer->start(); - connect(_initialSTUNTimer.data(), &QTimer::timeout, this, &LimitedNodeList::sendSTUNRequest); - - const int STUN_INITIAL_UPDATE_INTERVAL_MSECS = 250; - _initialSTUNTimer->start(STUN_INITIAL_UPDATE_INTERVAL_MSECS); - - // send an initial STUN request right away - sendSTUNRequest(); + // send an initial STUN request right away + sendSTUNRequest(); } } } void LimitedNodeList::possiblyTimeoutSTUNAddressLookup() { - if (_stunSockAddr.getAddress().isNull()) { + if (_stunSockAddr.isNull()) { // our stun address is still NULL, but we've been waiting for long enough - time to force a fail stopInitialSTUNUpdate(false); } @@ -859,7 +881,7 @@ void LimitedNodeList::stopInitialSTUNUpdate(bool success) { // Or, if we failed - if will check if we can eventually get a public socket const int STUN_IP_ADDRESS_CHECK_INTERVAL_MSECS = 30 * 1000; - QTimer* stunOccasionalTimer = new QTimer(this); + QTimer* stunOccasionalTimer = new QTimer { this }; connect(stunOccasionalTimer, &QTimer::timeout, this, &LimitedNodeList::sendSTUNRequest); stunOccasionalTimer->start(STUN_IP_ADDRESS_CHECK_INTERVAL_MSECS); @@ -904,7 +926,7 @@ void LimitedNodeList::sendPacketToIceServer(PacketType packetType, const HifiSoc QDataStream iceDataStream(icePacket.get()); iceDataStream << clientID << _publicSockAddr << _localSockAddr; - + if (packetType == PacketType::ICEServerQuery) { assert(!peerID.isNull()); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 87ed12ac66..c9785d240b 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -293,6 +293,7 @@ protected: bool _thisNodeCanRez; QPointer _initialSTUNTimer; + int _numInitialSTUNRequests = 0; bool _hasCompletedInitialSTUN = false; quint64 _firstSTUNTime = 0; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 5af2bbe7c7..7870e61723 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include diff --git a/libraries/physics/src/ObjectAction.h b/libraries/physics/src/ObjectAction.h index efab75b802..4e3390b386 100644 --- a/libraries/physics/src/ObjectAction.h +++ b/libraries/physics/src/ObjectAction.h @@ -40,8 +40,8 @@ public: virtual void updateActionWorker(float deltaTimeStep) = 0; // these are from btActionInterface - virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTimeStep); - virtual void debugDraw(btIDebugDraw* debugDrawer); + virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTimeStep) override; + virtual void debugDraw(btIDebugDraw* debugDrawer) override; virtual QByteArray serialize() const override = 0; virtual void deserialize(QByteArray serializedArguments) override = 0; diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index 4f9a741aad..5d21473fd8 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -9,8 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "HifiConfigVariantMap.h" + #include #include +#include #include #include #include @@ -18,8 +21,8 @@ #include #include +#include "ServerPathUtils.h" #include "SharedLogging.h" -#include "HifiConfigVariantMap.h" QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringList& argumentList) { @@ -110,18 +113,54 @@ void HifiConfigVariantMap::loadMasterAndUserConfig(const QStringList& argumentLi // load the user config const QString USER_CONFIG_FILE_OPTION = "--user-config"; + static const QString USER_CONFIG_FILE_NAME = "config.json"; int userConfigIndex = argumentList.indexOf(USER_CONFIG_FILE_OPTION); if (userConfigIndex != -1) { _userConfigFilename = argumentList[userConfigIndex + 1]; } else { - _userConfigFilename = QString("%1/%2/%3/config.json").arg(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation), - QCoreApplication::organizationName(), - QCoreApplication::applicationName()); + // we weren't passed a user config path + _userConfigFilename = ServerPathUtils::getDataFilePath(USER_CONFIG_FILE_NAME); + + // as of 1/19/2016 this path was moved so we attempt a migration for first run post migration here + + // figure out what the old path was + auto oldConfigFilename = QString("%1/%2/%3/%4").arg(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation), + QCoreApplication::organizationName(), + QCoreApplication::applicationName(), + USER_CONFIG_FILE_NAME); + + // check if there's already a config file at the new path + QFile newConfigFile { _userConfigFilename }; + if (!newConfigFile.exists()) { + QFile oldConfigFile { oldConfigFilename }; + + if (oldConfigFile.exists()) { + // we have the old file and not the new file - time to copy the file + + // make the destination directory if it doesn't exist + auto dataDirectory = ServerPathUtils::getDataDirectory(); + if (QDir().mkpath(dataDirectory)) { + if (oldConfigFile.copy(_userConfigFilename)) { + qDebug() << "Migrated config file from" << oldConfigFilename << "to" << _userConfigFilename; + } else { + qWarning() << "Could not copy previous config file from" << oldConfigFilename << "to" << _userConfigFilename + << "- please try to copy manually and restart."; + } + } else { + qWarning() << "Could not create application data directory" << dataDirectory << "- unable to migrate previous config file."; + } + } + } + } loadMapFromJSONFile(_userConfig, _userConfigFilename); + mergeMasterAndUserConfigs(); +} + +void HifiConfigVariantMap::mergeMasterAndUserConfigs() { // the merged config is initially matched to the master config _mergedConfig = _masterConfig; diff --git a/libraries/shared/src/HifiConfigVariantMap.h b/libraries/shared/src/HifiConfigVariantMap.h index 75b8be1984..e92561cff5 100644 --- a/libraries/shared/src/HifiConfigVariantMap.h +++ b/libraries/shared/src/HifiConfigVariantMap.h @@ -13,6 +13,7 @@ #define hifi_HifiConfigVariantMap_h #include +#include class HifiConfigVariantMap { public: @@ -25,6 +26,8 @@ public: QVariantMap& getUserConfig() { return _userConfig; } QVariantMap& getMergedConfig() { return _mergedConfig; } + void mergeMasterAndUserConfigs(); + const QString& getUserConfigFilename() const { return _userConfigFilename; } private: QString _userConfigFilename; diff --git a/libraries/shared/src/LogHandler.cpp b/libraries/shared/src/LogHandler.cpp index 7dd1aceb3d..c1cf969f36 100644 --- a/libraries/shared/src/LogHandler.cpp +++ b/libraries/shared/src/LogHandler.cpp @@ -42,6 +42,8 @@ LogHandler::LogHandler() : const char* stringForLogType(LogMsgType msgType) { switch (msgType) { + case LogInfo: + return "INFO"; case LogDebug: return "DEBUG"; case LogWarning: diff --git a/libraries/shared/src/LogHandler.h b/libraries/shared/src/LogHandler.h index a74a6287d7..ee8e426c34 100644 --- a/libraries/shared/src/LogHandler.h +++ b/libraries/shared/src/LogHandler.h @@ -22,11 +22,12 @@ const int VERBOSE_LOG_INTERVAL_SECONDS = 5; enum LogMsgType { + LogInfo = QtInfoMsg, LogDebug = QtDebugMsg, LogWarning = QtWarningMsg, LogCritical = QtCriticalMsg, LogFatal = QtFatalMsg, - LogSuppressed + LogSuppressed = 100 }; /// Handles custom message handling and sending of stats/logs to Logstash instance diff --git a/libraries/shared/src/ServerPathUtils.cpp b/libraries/shared/src/ServerPathUtils.cpp new file mode 100644 index 0000000000..ca87a28610 --- /dev/null +++ b/libraries/shared/src/ServerPathUtils.cpp @@ -0,0 +1,37 @@ +// +// ServerPathUtils.cpp +// libraries/shared/src +// +// Created by Ryan Huffman on 01/12/16. +// Copyright 2016 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 "ServerPathUtils.h" + +#include +#include +#include +#include + +QString ServerPathUtils::getDataDirectory() { + auto dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + +#ifdef Q_OS_WIN + dataPath += "/AppData/Roaming/"; +#elif defined(Q_OS_OSX) + dataPath += "/Library/Application Support/"; +#else + dataPath += "/.local/share/"; +#endif + + dataPath += qApp->organizationName() + "/" + qApp->applicationName(); + + return QDir::cleanPath(dataPath); +} + +QString ServerPathUtils::getDataFilePath(QString filename) { + return QDir(getDataDirectory()).absoluteFilePath(filename); +} + diff --git a/libraries/shared/src/ServerPathUtils.h b/libraries/shared/src/ServerPathUtils.h new file mode 100644 index 0000000000..28a9a71f0d --- /dev/null +++ b/libraries/shared/src/ServerPathUtils.h @@ -0,0 +1,22 @@ +// +// ServerPathUtils.h +// libraries/shared/src +// +// Created by Ryan Huffman on 01/12/16. +// Copyright 2016 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_ServerPathUtils_h +#define hifi_ServerPathUtils_h + +#include + +namespace ServerPathUtils { + QString getDataDirectory(); + QString getDataFilePath(QString filename); +} + +#endif // hifi_ServerPathUtils_h \ No newline at end of file diff --git a/server-console/.gitignore b/server-console/.gitignore new file mode 100644 index 0000000000..1624823e80 --- /dev/null +++ b/server-console/.gitignore @@ -0,0 +1,5 @@ +Server\ Console-*/ +server-console-*/ +electron-packager/ +npm-debug.log +logs/ diff --git a/server-console/CMakeLists.txt b/server-console/CMakeLists.txt new file mode 100644 index 0000000000..b77bab4dd4 --- /dev/null +++ b/server-console/CMakeLists.txt @@ -0,0 +1,71 @@ +set(TARGET_NAME packaged-server-console) + +if (PRODUCTION_BUILD) + set(PRODUCTION_OPTION "--production") +endif() + +# add a target that will package the console +add_custom_target(${TARGET_NAME}-npm-install + COMMAND npm install + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) +add_custom_target(${TARGET_NAME} + COMMAND npm run packager -- --out ${CMAKE_CURRENT_BINARY_DIR} ${PRODUCTION_OPTION} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + DEPENDS ${TARGET_NAME}-npm-install +) + +set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Server Console") +set_target_properties(${TARGET_NAME}-npm-install PROPERTIES FOLDER "hidden/Server Console") + +# add a dependency from the package target to the server components +add_dependencies(${TARGET_NAME} assignment-client domain-server) + +# set the packaged console folder depending on platform, so we can copy it +if (APPLE) + set(PACKAGED_CONSOLE_FOLDER "Server\\ Console-darwin-x64/${CONSOLE_EXEC_NAME}") +elseif (WIN32) + set(PACKAGED_CONSOLE_FOLDER "server-console-win32-x64") +elseif (UNIX) + set(PACKAGED_CONSOLE_FOLDER "server-console-linux-x64") +endif () + +# install the packaged Server Console in our install directory +if (APPLE) + install( + PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_CONSOLE_FOLDER}" + DESTINATION ${CONSOLE_INSTALL_DIR} + COMPONENT ${SERVER_COMPONENT} + ) +elseif (WIN32) + set(CONSOLE_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_CONSOLE_FOLDER}") + + install( + DIRECTORY "${CONSOLE_DESTINATION}/" + DESTINATION ${CONSOLE_INSTALL_DIR} + COMPONENT ${SERVER_COMPONENT} + ) + + # sign the copied server console executable after install + set(EXECUTABLE_PATH "${CONSOLE_DESTINATION}/${CONSOLE_EXEC_NAME}") + optional_win_executable_signing() +endif() + +if (PR_BUILD OR PRODUCTION_BUILD) + set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL FALSE EXCLUDE_FROM_DEFAULT_BUILD FALSE) + + if (APPLE) + set(BESIDE_CONSOLE_DIR "${CONSOLE_INSTALL_APP_PATH}/Contents/Resources") + else () + set(BESIDE_CONSOLE_DIR ${CONSOLE_INSTALL_DIR}) + endif () + + # configure our build info json file and install it beside the console + set(CONSOLE_BUILD_INFO_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/build-info.json") + configure_file("${HF_CMAKE_DIR}/templates/console-build-info.json.in" ${CONSOLE_BUILD_INFO_OUTPUT}) + install( + FILES ${CONSOLE_BUILD_INFO_OUTPUT} + DESTINATION ${BESIDE_CONSOLE_DIR} + COMPONENT ${SERVER_COMPONENT} + ) +endif () diff --git a/server-console/README.md b/server-console/README.md new file mode 100644 index 0000000000..b4b6c3533b --- /dev/null +++ b/server-console/README.md @@ -0,0 +1,25 @@ +### Console + +The High Fidelity Server Console, made with [Electron](http://electron.atom.io/). + +### Running Locally + +Make sure you have [Node.js](https://nodejs.org/en/) installed. Use the latest stable version. + +``` +npm install +npm start +``` + +To run, the console needs to find a build of Interface, domain-server, and assignment-client. + +The command `npm start` tells the console to look for builds of those binaries in a build folder beside this console folder. + +On platforms with separate build folders for release and debug libraries `npm start` will choose the debug binaries. On those platforms if you prefer to use local release builds you'll want `npm run local-release`. + +### Packaging + +CMake produces a target `packaged-server-console` that will bundle up everything you need for the Server Console on your platform. +It ensures that there are available builds for the domain-server and assignment-client. Then it produces an executable for the Server Console. + +The install target will copy all of the produced executables to a directory, ready for testing or packaging for deployment. diff --git a/server-console/package.json b/server-console/package.json new file mode 100644 index 0000000000..44f7b3d05e --- /dev/null +++ b/server-console/package.json @@ -0,0 +1,38 @@ +{ + "name": "hf-console", + "description": "High Fidelity Console", + "author": "High Fidelity", + "license": "Apache-2.0", + "version": "1.0.0", + "keywords": [ + "" + ], + "devDependencies": { + "electron-compilers": "^1.0.1", + "electron-packager": "^5.2.1", + "electron-prebuilt": "0.35.4" + }, + "repository": { + "type": "git", + "url": "https://github.com/highfidelity/hifi.git" + }, + "main": "src/main.js", + "scripts": { + "start": "electron . --binary-type local-debug --debug", + "local-release": "electron . --binary-type local-release --debug", + "local-release-no-debug": "electron . --binary-type local-release", + "packager": "node packager.js" + }, + "dependencies": { + "always-tail": "0.2.0", + "cheerio": "^0.19.0", + "extend": "^3.0.0", + "fs-extra": "^0.26.4", + "node-notifier": "^4.4.0", + "os-homedir": "^1.0.1", + "request": "2.67.0", + "request-progress": "1.0.2", + "unzip": "0.1.11", + "yargs": "^3.30.0" + } +} diff --git a/server-console/packager.js b/server-console/packager.js new file mode 100644 index 0000000000..bf8ddd68d4 --- /dev/null +++ b/server-console/packager.js @@ -0,0 +1,59 @@ +var packager = require('electron-packager') +var osType = require('os').type(); + +var platform = null; +if (osType == "Darwin" || osType == "Linux") { + platform = osType.toLowerCase(); +} else if (osType == "Windows_NT") { + platform = "win32" +} + +var argv = require('yargs').argv; + +// check which icon we should use, beta or regular +var iconName = argv.production ? "console" : "console-beta"; + +// setup the common options for the packager +var options = { + dir: __dirname, + name: "server-console", + version: "0.35.4", + overwrite: true, + prune: true, + arch: "x64", + platform: platform, + icon: "resources/" + iconName, + ignore: "logs|(S|s)erver(\\s|-)(C|c)onsole-\\S+|electron-packager|README.md|CMakeLists.txt|packager.js|.gitignore" +} + +const EXEC_NAME = "server-console"; +const SHORT_NAME = "Server Console"; +const FULL_NAME = "High Fidelity Server Console"; + +// setup per OS options +if (osType == "Darwin") { + options["app-bundle-id"] = "com.highfidelity.server-console" + (argv.production ? "" : "-dev") + options["name"] = SHORT_NAME +} else if (osType == "Windows_NT") { + options["version-string"] = { + CompanyName: "High Fidelity, Inc.", + FileDescription: SHORT_NAME, + ProductName: FULL_NAME, + OriginalFilename: EXEC_NAME + ".exe" + } +} + +// check if we were passed a custom out directory, pass it along if so +if (argv.out) { + options.out = argv.out +} + +// call the packager to produce the executable +packager(options, function(error, appPath) { + if (error) { + console.error("There was an error writing the packaged console: " + error.message); + process.exit(1); + } else { + console.log("Wrote new app to " + appPath); + } +}); diff --git a/server-console/resources/console-beta.icns b/server-console/resources/console-beta.icns new file mode 100644 index 0000000000..a0c2af881b Binary files /dev/null and b/server-console/resources/console-beta.icns differ diff --git a/server-console/resources/console-beta.ico b/server-console/resources/console-beta.ico new file mode 100644 index 0000000000..90962c4871 Binary files /dev/null and b/server-console/resources/console-beta.ico differ diff --git a/server-console/resources/console-beta.png b/server-console/resources/console-beta.png new file mode 100644 index 0000000000..4fa8e1ce80 Binary files /dev/null and b/server-console/resources/console-beta.png differ diff --git a/server-console/resources/console-notification.png b/server-console/resources/console-notification.png new file mode 100644 index 0000000000..0fd22b8900 Binary files /dev/null and b/server-console/resources/console-notification.png differ diff --git a/server-console/resources/console-tray-Template.png b/server-console/resources/console-tray-Template.png new file mode 100644 index 0000000000..11dbbbc38f Binary files /dev/null and b/server-console/resources/console-tray-Template.png differ diff --git a/server-console/resources/console-tray-Template@2x.png b/server-console/resources/console-tray-Template@2x.png new file mode 100644 index 0000000000..8acd094606 Binary files /dev/null and b/server-console/resources/console-tray-Template@2x.png differ diff --git a/server-console/resources/console-tray.ico b/server-console/resources/console-tray.ico new file mode 100644 index 0000000000..becc1b8e8b Binary files /dev/null and b/server-console/resources/console-tray.ico differ diff --git a/server-console/resources/console-tray.png b/server-console/resources/console-tray.png new file mode 100644 index 0000000000..6ff889828c Binary files /dev/null and b/server-console/resources/console-tray.png differ diff --git a/server-console/resources/console-tray@2x.png b/server-console/resources/console-tray@2x.png new file mode 100644 index 0000000000..7295647c20 Binary files /dev/null and b/server-console/resources/console-tray@2x.png differ diff --git a/server-console/resources/console.icns b/server-console/resources/console.icns new file mode 100644 index 0000000000..45dd334a3e Binary files /dev/null and b/server-console/resources/console.icns differ diff --git a/server-console/resources/console.ico b/server-console/resources/console.ico new file mode 100644 index 0000000000..872a4a7a68 Binary files /dev/null and b/server-console/resources/console.ico differ diff --git a/server-console/resources/console.png b/server-console/resources/console.png new file mode 100644 index 0000000000..a8260f9687 Binary files /dev/null and b/server-console/resources/console.png differ diff --git a/server-console/src/downloader.css b/server-console/src/downloader.css new file mode 100644 index 0000000000..6c30d9e3fe --- /dev/null +++ b/server-console/src/downloader.css @@ -0,0 +1,145 @@ +@font-face { + font-family: 'Raleway'; + src: url('vendor/Raleway/Raleway-Regular.ttf'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'Raleway'; + src: url('vendor/Raleway/Raleway-ExtraLight.ttf'); + font-weight: 200; + font-style: normal; +} +@font-face { + font-family: 'Raleway'; + src: url('vendor/Raleway/Raleway-SemiBold.ttf'); + font-weight: bold; + font-style: normal; +} + + +* { + font-family: "Raleway", "Open Sans", Arial, Helvetica, sans-serif; + line-height: 130%; +} + +body { + margin: 0; + padding: 0; + color: #808785; + margin: 0 auto; + text-align: center; + font-size: 13.5pt; + -webkit-touch-callout: none; -webkit-user-select: none; + cursor: default; + overflow: hidden; + font-variant-numeric: lining-nums; + -moz-font-feature-settings: "lnum"; + -webkit-font-feature-settings: "lnum"; + font-feature-settings: "lnum"; +} + +#error-message { + background-color: #F7F8F8; + color: #EB4C5F; + padding: 20px; + margin: 20px 120px 0 120px; +} + +.selectable { + -webkit-touch-callout: text; + -webkit-user-select: text; + cursor: text; +} + +progress { + height: 1px; + width: 300px; + -webkit-appearance: none; +} + +progress[value]::-webkit-progress-bar { + background-color: #DADADA; + /* border-radius: 2px; */ +} + +progress[value]::-webkit-progress-value { + background-color: #21B7D4; + /* border-radius: 2px; */ +} + +#progress-bytes { + font-weight: lighter; + color: #BBBBBB; + margin-top: 10px; +} + +#download-summary { + margin-top: 80px; +} + +.state { + padding-top: 100px; +} + +h1 { + font-size: 29pt; + font-weight: normal; +} + +#cancel-area { + margin-top: 100px; +} + +a:link, +a:visited, +a:hover, +a:active { + color: #B4B4B4; +} + +a:hover { + color: #2D88A4; +} + +.one { + opacity: 0; + animation: dot 2.3s infinite; + animation-delay: 0.0s; +} + +.two { + opacity: 0; + animation: dot 2.3s infinite; + animation-delay: 0.2s; +} + +.three { + opacity: 0; + animation: dot 2.3s infinite; + animation-delay: 0.3s; +} + +@-webkit-keyframes dot { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@keyframes dot { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } +} \ No newline at end of file diff --git a/server-console/src/downloader.html b/server-console/src/downloader.html new file mode 100644 index 0000000000..a7ba7bd7b7 --- /dev/null +++ b/server-console/src/downloader.html @@ -0,0 +1,34 @@ + + + + High Fidelity + + + + +
+

Downloading...

+ +
10 MB / 4.0 MB
+
+ Sit tight, we are downloading content for your home +
+
+ +
+

Installing...

+ Just a moment +
+ +
+

Success!

+ You're all set. +
+ +
+

Houston...

+ An unfortunate error has occurred: +
+
+ + diff --git a/server-console/src/downloader.js b/server-console/src/downloader.js new file mode 100644 index 0000000000..f7e67f03ce --- /dev/null +++ b/server-console/src/downloader.js @@ -0,0 +1,52 @@ +function ready() { + console.log("Ready"); + + const electron = require('electron'); + const remote = require('remote'); + window.$ = require('./vendor/jquery/jquery-2.1.4.min.js'); + + $(".state").hide(); + + var currentState = null; + + function updateState(state, args) { + if (state == 'downloading') { + function formatBytes(size) { + return (size / 1000000).toFixed('2'); + } + $('#download-progress').attr('value', args.percentage * 100); + if (args.size !== null && args.size.transferred !== null && args.size.total !== null) { + var progressString = formatBytes(args.size.transferred) + "MB / " + formatBytes(args.size.total) + "MB"; + $('#progress-bytes').html(progressString); + } else { + $('#progress-bytes').html("Retrieving resources..."); + } + } else if (state == 'installing') { + } else if (state == 'complete') { + setTimeout(function() { + remote.getCurrentWindow().close(); + }, 2000); + } else if (state == 'error') { + $('#error-message').html(args.message); + } + + if (currentState != state) { + if (currentState) { + $('#state-' + currentState).hide(); + } + $('#state-' + state).show(); + currentState = state; + } + } + + electron.ipcRenderer.on('update', function(event, message) { + updateState(message.state, message.args); + }); + + // updateState('error', { message: "This is an error message", progress: 0.5 }); + // updateState('complete', { progress: 0 }); + // updateState('downloading', { percentage: 0.5, size: { total: 83040400, transferred: 500308} }); + + updateState('downloading', { percentage: 0, size: null }); + electron.ipcRenderer.send('ready'); +} diff --git a/server-console/src/images/console-hf-logo-2x.png b/server-console/src/images/console-hf-logo-2x.png new file mode 100644 index 0000000000..4c0afd6777 Binary files /dev/null and b/server-console/src/images/console-hf-logo-2x.png differ diff --git a/server-console/src/images/console-menubar-osx-2x.png b/server-console/src/images/console-menubar-osx-2x.png new file mode 100644 index 0000000000..e8b3c11e29 Binary files /dev/null and b/server-console/src/images/console-menubar-osx-2x.png differ diff --git a/server-console/src/images/console-menubar-windows-2x.png b/server-console/src/images/console-menubar-windows-2x.png new file mode 100644 index 0000000000..5057da17d4 Binary files /dev/null and b/server-console/src/images/console-menubar-windows-2x.png differ diff --git a/server-console/src/log.css b/server-console/src/log.css new file mode 100644 index 0000000000..d13a48b5a0 --- /dev/null +++ b/server-console/src/log.css @@ -0,0 +1,87 @@ +@font-face { + font-family: 'Raleway'; + src: url('vendor/Raleway/Raleway-Regular.ttf'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'Raleway'; + src: url('vendor/Raleway/Raleway-ExtraLight.ttf'); + font-weight: 200; + font-style: normal; +} +@font-face { + font-family: 'Raleway'; + src: url('vendor/Raleway/Raleway-SemiBold.ttf'); + font-weight: bold; + font-style: normal; +} + +body { + font-family: "Raleway", "Open Sans", Arial, Helvetica, sans-serif; + font-size: 13.5pt; + line-height: 1.6; + margin: 0; + padding: 0; + font-variant-numeric: lining-nums; + -moz-font-feature-settings: "lnum"; + -webkit-font-feature-settings: "lnum"; + font-feature-settings: "lnum"; +} + +ul.tabs { + margin: 0; + padding: 0px; + list-style: none; +} + +ul.tabs li { + background: none; + color: #222; + display: inline-block; + padding: 10px 15px; + cursor: pointer; +} + +ul.tabs li.current { + background-color: #ededed; + color: #222; +} + +.tab-pane { + display: none; + background-color: #ededed; + width: 100%; + overflow: auto; + margin: 0; + padding: 0; + left: 0; +} + +.tab-pane.current { + display: inherit; +} + +.tab-content { + overflow: auto; + padding: 10px; + background-color: #ededed; +} + +.top { + height: 45px; +} + +.bottom { + position: absolute; + left: 0; + top: 45px; + bottom: 0; + right: 0; +} + +.search { + display: none; + float: right; + margin: 10px; +} \ No newline at end of file diff --git a/server-console/src/log.html b/server-console/src/log.html new file mode 100644 index 0000000000..f704f3e35f --- /dev/null +++ b/server-console/src/log.html @@ -0,0 +1,23 @@ + + + + Log + + + + + + +
    + + +
+ +
+
+
+
+ + diff --git a/server-console/src/log.js b/server-console/src/log.js new file mode 100644 index 0000000000..d7aae8a432 --- /dev/null +++ b/server-console/src/log.js @@ -0,0 +1,205 @@ +var remote = require('electron').remote; +var os = require('os'); +var Tail = require('always-tail'); + +function cleanPath(path) { + if (os.type() == "Windows_NT") { + // Fix path on Windows + while (path.indexOf('\\') >= 0) { + path = path.replace('\\', '/', 'g'); + } + } + return path; +} + +// a: array, b: array +// Returns: { add: [...], subtract: [...] } +// add: array of items in b but not a +// subtract: array of items in a but not b +function difference(a, b) { + var add = []; + var subtract = []; + for (var k of a) { + if (b.indexOf(k) == -1) { + // In a, but not in b + subtract.push(k); + } + } + for (k of b) { + if (a.indexOf(k) == -1) { + // In a, but not in b + add.push(k); + } + } + return { + add: add, + subtract: subtract + }; +} + +ready = function() { + window.$ = require('./vendor/jquery/jquery-2.1.4.min.js'); + + var domainServer = remote.getGlobal('domainServer'); + var acMonitor = remote.getGlobal('acMonitor'); + + var logWatchers = { + 'ds': { + }, + 'ac': { + } + }; + + function updateLogWatchers(stream, watchList, newLogFilesByPID) { + // Consolidate into a list of the log paths + var newLogFilePaths = []; + for (var pid in newLogFilesByPID) { + newLogFilePaths.push(newLogFilesByPID[pid].stdout); + newLogFilePaths.push(newLogFilesByPID[pid].stderr); + } + + var oldLogFilePaths = Object.keys(watchList); + var diff = difference(oldLogFilePaths, newLogFilePaths); + console.log('diff', diff); + // For each removed file, remove it from our watch list + diff.subtract.forEach(function(removedLogFilePath) { + watchList[removedLogFilePath].unwatch(); + delete watchList[removedLogFilePath]; + }); + diff.add.forEach(function(addedLogFilePath) { + var cleanFilePath = cleanPath(addedLogFilePath); + + var logTail = new Tail(cleanFilePath, '\n', { start: 0, interval: 500 }); + + logTail.on('line', function(msg) { + appendLogMessage(0, msg, stream); + }); + + logTail.on('error', function(error) { + console.log("ERROR:", error); + }); + + logTail.watch(); + + watchList[addedLogFilePath] = logTail; + console.log("Watching", cleanFilePath); + }); + } + + function updateLogFiles() { + // Get ds and ac logs from main application + var dsLogs = domainServer.getLogs(); + var acLogs = acMonitor.getLogs(); + + updateLogWatchers('ds', logWatchers.ds, dsLogs); + updateLogWatchers('ac', logWatchers.ac, acLogs); + } + + window.onbeforeunload = function(e) { + domainServer.removeListener('logs-updated', updateLogFiles); + acMonitor.removeListener('logs-updated', updateLogFiles); + }; + + domainServer.on('logs-updated', updateLogFiles); + acMonitor.on('logs-updated', updateLogFiles); + + + const maxLogLines = 2500; + const ipcRenderer = require('electron').ipcRenderer; + + var currentTab = 'domain-server'; + var tabStates = { + 'domain-server': { + atBottom: true, + size: 0 + }, + 'assignment-client': { + atBottom: true, + size: 0 + } + }; + function setCurrentTab(tabId) { + if (currentTab == tabId) { + return; + } + + var padding = 15; + $currentTab = $('#' + currentTab); + tabStates[currentTab].atBottom = $currentTab[0].scrollTop >= ($currentTab[0].scrollHeight - $currentTab.height() - (2 * padding)); + + currentTab = tabId; + $('ul.tabs li').removeClass('current'); + $('.tab-pane').removeClass('current'); + + $('li[data-tab=' + tabId + ']').addClass('current'); + var $pidLog = $("#" + tabId); + $pidLog.addClass('current'); + + if (tabStates[tabId].atBottom) { + $pidLog.scrollTop($pidLog[0].scrollHeight); + } + } + + $('ul.tabs li').click(function(){ + setCurrentTab($(this).attr('data-tab')); + }); + + setCurrentTab('domain-server'); + setCurrentTab('assignment-client'); + + var filter = ""; + + function shouldDisplayLogMessage(message) { + return !filter || message.toLowerCase().indexOf(filter) >= 0; + } + + function appendLogMessage(pid, msg, name) { + console.log(pid, msg, name); + var id = "pid-" + pid; + id = name == "ds" ? "domain-server" : "assignment-client"; + var $pidLog = $('#' + id); + + var size = ++tabStates[id].size; + if (size > maxLogLines) { + $pidLog.find('div.log-line:first').remove(); + removed = true; + } + + var wasAtBottom = false; + if (currentTab == id) { + var padding = 15; + wasAtBottom = $pidLog[0].scrollTop >= ($pidLog[0].scrollHeight - $pidLog.height() - (2 * padding)); + } + + var $logLine = $('
').text(msg); + if (!shouldDisplayLogMessage(msg)) { + $logLine.hide(); + } + + $pidLog.append($logLine); + + if (wasAtBottom) { + $pidLog.scrollTop($pidLog[0].scrollHeight); + } + + } + + // handle filtering of table rows on input change + $('#search-input').on('input', function() { + filter = $(this).val().toLowerCase(); + if (filter == "") { + $('.log-line').show(); + } else { + $('.log-line').each(function(){ + // decide to hide or show the row if it matches the filter + if (filter && $(this).text().toLowerCase().indexOf(filter) == -1) { + $(this).hide(); + } else { + $(this).show(); + } + }); + } + }); + + updateLogFiles(); +}; diff --git a/server-console/src/main.js b/server-console/src/main.js new file mode 100644 index 0000000000..4524c69644 --- /dev/null +++ b/server-console/src/main.js @@ -0,0 +1,808 @@ +'use strict'; + +const electron = require('electron'); +const app = electron.app; // Module to control application life. +const BrowserWindow = electron.BrowserWindow; + +const notifier = require('node-notifier'); +const util = require('util'); +const dialog = electron.dialog; +const Menu = require('menu'); +const Tray = require('tray'); +const shell = require('shell'); +const os = require('os'); +const childProcess = require('child_process'); +const path = require('path'); +const fs = require('fs-extra'); +const Tail = require('always-tail'); +const http = require('http'); +const unzip = require('unzip'); + +const request = require('request'); +const progress = require('request-progress'); +const osHomeDir = require('os-homedir'); + +const updater = require('./modules/hf-updater.js'); + +const Config = require('./modules/config').Config; + +const hfprocess = require('./modules/hf-process.js'); +const Process = hfprocess.Process; +const ACMonitorProcess = hfprocess.ACMonitorProcess; +const ProcessStates = hfprocess.ProcessStates; +const ProcessGroup = hfprocess.ProcessGroup; +const ProcessGroupStates = hfprocess.ProcessGroupStates; + +const osType = os.type(); + +const appIcon = path.join(__dirname, '../resources/console.png'); + +const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days +const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; + +function getBuildInfo() { + var buildInfoPath = null; + + if (osType == 'Windows_NT') { + buildInfoPath = path.join(path.dirname(process.execPath), 'build-info.json'); + } else if (osType == 'Darwin') { + var contentPath = ".app/Contents/"; + var contentEndIndex = __dirname.indexOf(contentPath); + + if (contentEndIndex != -1) { + // this is an app bundle + var appPath = __dirname.substring(0, contentEndIndex) + ".app"; + buildInfoPath = path.join(appPath, "/Contents/Resources/build-info.json"); + } + } + + const DEFAULT_BUILD_INFO = { releaseType: "", buildIdentifier: "dev" }; + var buildInfo = DEFAULT_BUILD_INFO; + + if (buildInfoPath) { + console.log('Build info path:', buildInfoPath); + try { + buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); + } catch (e) { + buildInfo = DEFAULT_BUILD_INFO; + } + } + + return buildInfo; +} + +const buildInfo = getBuildInfo(); + +console.log("build info", buildInfo); + +function getRootHifiDataDirectory() { + var organization = "High Fidelity"; + if (buildInfo.releaseType != "PRODUCTION") { + organization += ' - ' + buildInfo.buildIdentifier; + } + if (osType == 'Windows_NT') { + return path.resolve(osHomeDir(), 'AppData/Roaming', organization); + } else if (osType == 'Darwin') { + return path.resolve(osHomeDir(), 'Library/Application Support', organization); + } else { + return path.resolve(osHomeDir(), '.local/share/', organization); + } +} + +function getAssignmentClientResourcesDirectory() { + return path.join(getRootHifiDataDirectory(), '/assignment-client'); +} + +function getApplicationDataDirectory() { + return path.join(getRootHifiDataDirectory(), '/Server Console'); +} + +console.log("Root hifi directory is: ", getRootHifiDataDirectory()); + +const ipcMain = electron.ipcMain; + + +var isShuttingDown = false; +function shutdown() { + if (!isShuttingDown) { + var idx = 0; + + // if the home server is running, show a prompt before quit to ask if the user is sure + if (homeServer.state == ProcessGroupStates.STARTED) { + idx = dialog.showMessageBox({ + type: 'question', + buttons: ['Yes', 'No'], + title: 'Are you sure?', + message: 'Quitting will stop your Server Console and your Home domain will no longer be running.' + }); + } + + if (idx == 0) { + isShuttingDown = true; + + userConfig.save(configPath); + + if (logWindow) { + logWindow.close(); + } + if (homeServer) { + homeServer.stop(); + } + + updateTrayMenu(null); + + if (homeServer.state == ProcessGroupStates.STOPPED) { + // if the home server is already down, take down the server console now + app.quit(); + } else { + // if the home server is still running, wait until we get a state change or timeout + // before quitting the app + var timeoutID = setTimeout(app.quit, 5000); + homeServer.on('state-update', function(processGroup) { + if (processGroup.state == ProcessGroupStates.STOPPED) { + clearTimeout(timeoutID); + app.quit(); + } + }); + } + } + } +} + +function deleteOldFiles(directoryPath, maxAgeInSeconds, filenameRegex) { + console.log("Deleting old log files in " + directoryPath); + + var filenames = []; + try { + filenames = fs.readdirSync(directoryPath); + } catch (e) { + console.warn("Error reading contents of log file directory", e); + return; + } + + for (const filename of filenames) { + console.log("Checking", filename); + const absolutePath = path.join(directoryPath, filename); + var stat = null; + try { + stat = fs.statSync(absolutePath); + } catch (e) { + console.log("Error stat'ing file", absolutePath, e); + continue; + } + const curTime = Date.now(); + if (stat.isFile() && filename.search(filenameRegex) >= 0) { + const ageInSeconds = (curTime - stat.mtime.getTime()) / 1000.0; + if (ageInSeconds >= maxAgeInSeconds) { + console.log("\tDeleting:", filename, ageInSeconds); + try { + fs.unlinkSync(absolutePath); + } catch (e) { + if (e.code != 'EBUSY') { + console.warn("\tError deleting:", e); + } + } + } + } + } +} + +var logPath = path.join(getApplicationDataDirectory(), '/logs'); + +console.log("Log directory:", logPath); +console.log("Data directory:", getRootHifiDataDirectory()); + +const configPath = path.join(getApplicationDataDirectory(), 'config.json'); +var userConfig = new Config(); +userConfig.load(configPath); + +// print out uncaught exceptions in the console +process.on('uncaughtException', function(err) { + console.error(err); + console.error(err.stack); +}); + +var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) { + // Someone tried to run a second instance, focus the window (if there is one) + return true; +}); + +if (shouldQuit) { + console.warn("Another instance of the Server Console is already running - this instance will quit."); + app.quit(); + return; +} + +// Check command line arguments to see how to find binaries +var argv = require('yargs').argv; +var pathFinder = require('./modules/path-finder.js'); + +var interfacePath = null; +var dsPath = null; +var acPath = null; + +var debug = argv.debug; + +var binaryType = argv.binaryType; + +interfacePath = pathFinder.discoveredPath("Interface", binaryType, buildInfo.releaseType); +dsPath = pathFinder.discoveredPath("domain-server", binaryType, buildInfo.releaseType); +acPath = pathFinder.discoveredPath("assignment-client", binaryType, buildInfo.releaseType); + +function binaryMissingMessage(displayName, executableName, required) { + var message = "The " + displayName + " executable was not found.\n"; + + if (required) { + message += "It is required for the Server Console to run.\n\n"; + } else { + message += "\n"; + } + + if (debug) { + message += "Please ensure there is a compiled " + displayName + " in a folder named build in this checkout.\n\n"; + message += "It was expected to be found at one of the following paths:\n"; + + var paths = pathFinder.searchPaths(executableName, argv.localReleaseBuilds); + message += paths.join("\n"); + } else { + message += "It is expected to be found beside this executable.\n"; + message += "You may need to re-install the Server Console."; + } + + return message; +} + +// if at this point any of the paths are null, we're missing something we wanted to find + +if (!dsPath) { + dialog.showErrorBox("Domain Server Not Found", binaryMissingMessage("domain-server", "domain-server", true)); + app.quit(); +} + +if (!acPath) { + dialog.showErrorBox("Assignment Client Not Found", binaryMissingMessage("assignment-client", "assignment-client", true)); + app.quit(); +} + +function openFileBrowser(path) { + // Add quotes around path + path = '"' + path + '"'; + if (osType == "Windows_NT") { + childProcess.exec('start "" ' + path); + } else if (osType == "Darwin") { + childProcess.exec('open ' + path); + } else if (osType == "Linux") { + childProcess.exec('xdg-open ' + path); + } +} + +// NOTE: this looks like it does nothing, but it's very important. +// Without it the default behaviour is to quit the app once all windows closed +// which is absolutely not what we want for a taskbar application. +app.on('window-all-closed', function() { +}); + +function startInterface(url) { + var argArray = []; + + // check if we have a url parameter to include + if (url) { + argArray = ["--url", url]; + } + + // create a new Interface instance - Interface makes sure only one is running at a time + var pInterface = new Process('interface', interfacePath, argArray); + pInterface.start(); +} + +var tray = null; +global.homeServer = null; +global.domainServer = null; +global.acMonitor = null; +global.userConfig = userConfig; + +var LogWindow = function(ac, ds) { + this.ac = ac; + this.ds = ds; + this.window = null; + this.acMonitor = null; + this.dsMonitor = null; +} +LogWindow.prototype = { + open: function() { + if (this.window) { + this.window.show(); + this.window.restore(); + return; + } + // Create the browser window. + this.window = new BrowserWindow({ width: 700, height: 500, icon: appIcon }); + this.window.loadURL('file://' + __dirname + '/log.html'); + + if (!debug) { + this.window.setMenu(null); + } + + this.window.on('closed', function() { + this.window = null; + }.bind(this)); + }, + close: function() { + if (this.window) { + this.window.close(); + } + } +}; + +function goHomeClicked() { + if (interfacePath) { + startInterface('hifi://localhost/7.91966,23.2104,20.7416/0,-0.476144,0,0.879367'); + } else { + // show an error to say that we can't go home without an interface instance + dialog.showErrorBox("Client Not Found", binaryMissingMessage("High Fidelity client", "Interface", false)); + } +} + +function stackManagerBasePath() { + var dataPath = 'High Fidelity/Stack Manager/resources'; + + if (process.platform == "win32") { + return path.resolve(osHomeDir(), 'AppData/Local', dataPath); + } else if (process.platform == "darwin") { + return path.resolve(osHomeDir(), 'Library/Application Support', dataPath); + } else { + return "" + } +} + +function isStackManagerContentPresent() { + var modelsPath = path.resolve(stackManagerBasePath(), 'models.json.gz'); + + try { + var stats = fs.lstatSync(modelsPath); + + if (stats.isFile()) { + console.log("Stack Manager entities file discovered at " + modelsPath) + // we found a content file + return true; + } + } catch (e) { + console.log("Stack Manager entities file not found at " + modelsPath); + } +} + +function promptToMigrateContent() { + dialog.showMessageBox({ + type: 'question', + buttons: ['Yes', 'No'], + title: 'Migrate Content', + message: 'Are you sure?\n\nThis will stop your home server and replace everything in your home with your content from Stack Manager.' + }, function(index) { + if (index == 0) { + if (homeServer.state != ProcessGroupStates.STOPPED) { + var stopThenMigrateCallback = function(processGroup) { + if (isShuttingDown) { + homeServer.removeListener('state-update', stopThenMigrateCallback); + } else if (processGroup.state == ProcessGroupStates.STOPPED) { + performContentMigration(); + + homeServer.removeListener('state-update', stopThenMigrateCallback); + } + }; + + homeServer.on('state-update', stopThenMigrateCallback); + homeServer.stop(); + + } else { + performContentMigration(); + } + } + }); +} + +function performContentMigration() { + // check if there is a models file to migrate + var modelsPath = path.resolve(stackManagerBasePath(), 'models.json.gz'); + + try { + var stats = fs.lstatSync(modelsPath); + } catch (e) { + // no entities file + dialog.showMessageBox({ + type: 'info', + buttons: ['OK'], + title: 'Models File Not Found', + message: 'There is no models file at ' + modelsPath + '\n\nStack Manager content migration can not proceed.' + }, null); + + return; + } + + function showMigrationCompletionDialog(copyError) { + if (!copyError) { + // show message for successful migration + dialog.showMessageBox({ + type: 'info', + buttons: ['OK'], + title: 'Migration Complete', + message: 'Your Stack Manager content has been migrated.\n\nYour home server will now be restarted.' + }, null); + } else { + // show error message for copy fail + dialog.showMessageBox({ + type: 'info', + buttons: ['OK'], + title: 'Migration Failed', + message: 'There was an error copying your Stack Manager content: ' + copyError + '\n\nPlease try again.' + }, null); + } + } + + // we have a models file, try and copy it + var newModelsPath = path.resolve(getAssignmentClientResourcesDirectory(), 'entities/models.json.gz') + console.log("Copying Stack Manager entity file from " + modelsPath + " to " + newModelsPath); + + try { + fs.copySync(modelsPath, newModelsPath); + + // check if there are any assets to copy + var oldAssetsPath = path.resolve(stackManagerBasePath(), 'assets'); + + var assets = fs.readdirSync(oldAssetsPath); + + if (assets.length > 0) { + // assume this means the directory is not empty + // and that we should copy it + var newAssetsPath = path.resolve(getAssignmentClientResourcesDirectory(), 'assets'); + + console.log("Copying Stack Manager assets from " + oldAssetsPath + " to " + newAssetsPath); + + // attempt to copy the assets folder + fs.copySync(oldAssetsPath, newAssetsPath, { + preserveTimestamps: true + }); + } + + showMigrationCompletionDialog(null); + } catch (error) { + showMigrationCompletionDialog(error); + } + + homeServer.start(); +} + +var logWindow = null; + +function buildMenuArray(serverState) { + var menuArray = null; + + if (isShuttingDown) { + menuArray = [ + { + label: "Shutting down...", + enabled: false + } + ]; + } else { + menuArray = [ + { + label: 'Server - Stopped', + enabled: false + }, + { + type: 'separator' + }, + { + label: 'Go Home', + click: goHomeClicked, + enabled: false + }, + { + type: 'separator' + }, + { + label: 'Start Server', + click: function() { homeServer.restart(); } + }, + { + label: 'Stop Server', + visible: false, + click: function() { homeServer.stop(); } + }, + { + label: 'Settings', + click: function() { shell.openExternal('http://localhost:40100/settings'); }, + enabled: false + }, + { + label: 'View Logs', + click: function() { logWindow.open(); } + }, + { + type: 'separator' + }, + { + label: 'Share', + click: function() { shell.openExternal('http://localhost:40100/settings/?action=share') } + }, + { + type: 'separator' + }, + { + label: 'Quit', + accelerator: 'Command+Q', + click: function() { shutdown(); } + } + ]; + + var foundStackManagerContent = isStackManagerContentPresent(); + if (foundStackManagerContent) { + // add a separator and the stack manager content migration option + menuArray.splice(menuArray.length - 1, 0, { + label: 'Migrate Stack Manager Content', + click: function() { promptToMigrateContent(); } + }, { + type: 'separator' + }); + } + + updateMenuArray(menuArray, serverState); + } + + return menuArray; +} + +const GO_HOME_INDEX = 2; +const SERVER_LABEL_INDEX = 0; +const RESTART_INDEX = 4; +const STOP_INDEX = 5; +const SETTINGS_INDEX = 6; + +function updateMenuArray(menuArray, serverState) { + // update the tray menu state + var running = serverState == ProcessGroupStates.STARTED; + + var serverLabelItem = menuArray[SERVER_LABEL_INDEX]; + var restartItem = menuArray[RESTART_INDEX]; + + // Go Home is only enabled if running + menuArray[GO_HOME_INDEX].enabled = running; + + // Stop is only visible if running + menuArray[STOP_INDEX].visible = running; + + // Settings is only visible if running + menuArray[SETTINGS_INDEX].enabled = running; + + if (serverState == ProcessGroupStates.STARTED) { + serverLabelItem.label = "Server - Started"; + restartItem.label = "Restart Server"; + } else if (serverState == ProcessGroupStates.STOPPED) { + serverLabelItem.label = "Server - Stopped"; + restartItem.label = "Start Server"; + } else if (serverState == ProcessGroupStates.STOPPING) { + serverLabelItem.label = "Server - Stopping"; + + restartItem.label = "Restart Server"; + restartItem.enabled = false; + } +} + +function updateTrayMenu(serverState) { + if (tray) { + var menuArray = buildMenuArray(serverState); + tray.setContextMenu(Menu.buildFromTemplate(menuArray)); + if (isShuttingDown) { + tray.setToolTip('High Fidelity - Shutting Down'); + } + } +} + +const httpStatusPort = 60332; + +function maybeInstallDefaultContentSet(onComplete) { + // Check for existing AC data + const acResourceDirectory = getAssignmentClientResourcesDirectory(); + console.log("Checking for existence of " + acResourceDirectory); + var userHasExistingServerData = true; + try { + fs.accessSync(acResourceDirectory); + } catch (e) { + console.log(e); + userHasExistingServerData = false; + } + + if (userHasExistingServerData) { + console.log("User has existing data, suppressing downloader"); + onComplete(); + return; + } + + // Show popup + var window = new BrowserWindow({ + icon: appIcon, + width: 640, + height: 480, + center: true, + frame: true, + useContentSize: true, + resizable: false + }); + window.loadURL('file://' + __dirname + '/downloader.html'); + if (!debug) { + window.setMenu(null); + } + window.show(); + + window.on('closed', onComplete); + + electron.ipcMain.on('ready', function() { + console.log("got ready"); + function sendStateUpdate(state, args) { + // console.log(state, window, args); + window.webContents.send('update', { state: state, args: args }); + } + + var aborted = false; + + // Start downloading content set + var req = progress(request.get({ + url: "https://s3.amazonaws.com/hifi-public/homeset/ContentSet-Lounge.zip" + }, function(error, responseMessage, responseData) { + if (aborted) { + return; + } else if (error || responseMessage.statusCode != 200) { + var message = ''; + if (error) { + message = "Error contacting resource server."; + } else { + message = "Error downloading resources from server."; + } + sendStateUpdate('error', { + message: message + }); + } else { + sendStateUpdate('installing'); + } + }), { throttle: 250 }).on('progress', function(state) { + if (!aborted) { + // Update progress popup + sendStateUpdate('downloading', state); + } + }); + var unzipper = unzip.Extract({ + path: acResourceDirectory, + verbose: true + }); + unzipper.on('close', function() { + console.log("Done", arguments); + sendStateUpdate('complete'); + }); + unzipper.on('error', function (err) { + console.log("aborting"); + aborted = true; + req.abort(); + console.log("ERROR"); + sendStateUpdate('error', { + message: "Error installing resources." + }); + }); + req.pipe(unzipper); + + window.on('closed', function() { + if (currentState == 'downloading') { + req.abort(); + } + }); + + userConfig.set('hasRun', true); + }); +} + +function maybeShowSplash() { + var suppressSplash = userConfig.get('doNotShowSplash', false); + + if (!suppressSplash) { + const zoomFactor = 0.8; + var window = new BrowserWindow({ + icon: appIcon, + width: 1600 * zoomFactor, + height: 737 * zoomFactor, + center: true, + frame: true, + useContentSize: true, + zoomFactor: zoomFactor, + resizable: false + }); + window.loadURL('file://' + __dirname + '/splash.html'); + if (!debug) { + window.setMenu(null); + } + window.show(); + + window.webContents.on('new-window', function(e, url) { + e.preventDefault(); + require('shell').openExternal(url); + }); + } +} + +const trayFilename = (osType == "Darwin" ? "console-tray-Template.png" : "console-tray.png"); +const trayIcon = path.join(__dirname, '../resources/' + trayFilename); + +const notificationIcon = path.join(__dirname, '../resources/console-notification.png'); + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +app.on('ready', function() { + + if (app.dock) { + // hide the dock icon on OS X + app.dock.hide(); + } + + // Create tray icon + tray = new Tray(trayIcon); + tray.setToolTip('High Fidelity Server Console'); + + tray.on('click', function() { + tray.popUpContextMenu(tray.menu); + }); + + updateTrayMenu(ProcessGroupStates.STOPPED); + + maybeInstallDefaultContentSet(function() { + maybeShowSplash(); + + if (buildInfo.releaseType == 'PRODUCTION') { + var currentVersion = null; + try { + currentVersion = parseInt(buildInfo.buildIdentifier); + } catch (e) { + } + + if (currentVersion !== null) { + const CHECK_FOR_UPDATES_INTERVAL_SECONDS = 60 * 30; + var hasShownUpdateNotification = false; + const updateChecker = new updater.UpdateChecker(currentVersion, CHECK_FOR_UPDATES_INTERVAL_SECONDS); + updateChecker.on('update-available', function(latestVersion, url) { + if (!hasShownUpdateNotification) { + notifier.notify({ + icon: notificationIcon, + title: 'An update is available!', + message: 'High Fidelity version ' + latestVersion + ' is available', + wait: true, + url: url + }); + hasShownUpdateNotification = true; + } + }); + notifier.on('click', function(notifierObject, options) { + console.log("Got click", options.url); + shell.openExternal(options.url); + }); + } + } + + deleteOldFiles(logPath, DELETE_LOG_FILES_OLDER_THAN_X_SECONDS, LOG_FILE_REGEX); + + if (dsPath && acPath) { + domainServer = new Process('domain-server', dsPath, ["--get-temp-name"], logPath); + acMonitor = new ACMonitorProcess('ac-monitor', acPath, ['-n6', + '--log-directory', logPath, + '--http-status-port', httpStatusPort], httpStatusPort, logPath); + homeServer = new ProcessGroup('home', [domainServer, acMonitor]); + logWindow = new LogWindow(acMonitor, domainServer); + + var processes = { + home: homeServer + }; + + // handle process updates + homeServer.on('state-update', function(processGroup) { updateTrayMenu(processGroup.state); }); + + // start the home server + homeServer.start(); + } + }); +}); diff --git a/server-console/src/modules/config.js b/server-console/src/modules/config.js new file mode 100644 index 0000000000..df44dcfafe --- /dev/null +++ b/server-console/src/modules/config.js @@ -0,0 +1,45 @@ +var fs = require('fs'); +var extend = require('extend'); + +function Config() { + this.data = {}; +} +Config.prototype = { + load: function(filePath) { + var rawData = null; + try { + rawData = fs.readFileSync(filePath); + } catch(e) { + console.log("Config file not found"); + } + var configData = {}; + + try { + if (rawData) { + configData = JSON.parse(rawData); + } else { + configData = {}; + } + } catch(e) { + console.error("Error parsing config file", filePath) + } + + this.data = {}; + extend(true, this.data, configData); + }, + save: function(filePath) { + fs.writeFileSync(filePath, JSON.stringify(this.data)); + }, + get: function(key, defaultValue) { + if (this.data.hasOwnProperty(key)) { + return this.data[key]; + } + return defaultValue; + }, + set: function(key, value) { + console.log("Setting", key, "to", value); + this.data[key] = value; + } +}; + +exports.Config = Config; diff --git a/server-console/src/modules/hf-process.js b/server-console/src/modules/hf-process.js new file mode 100755 index 0000000000..c7f21ebbe5 --- /dev/null +++ b/server-console/src/modules/hf-process.js @@ -0,0 +1,352 @@ +'use strict' + +const request = require('request'); +const extend = require('extend'); +const util = require('util'); +const events = require('events'); +const childProcess = require('child_process'); +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); + +const ProcessGroupStates = { + STOPPED: 'stopped', + STARTED: 'started', + STOPPING: 'stopping' +}; + +const ProcessStates = { + STOPPED: 'stopped', + STARTED: 'started', + STOPPING: 'stopping' +}; + + + +function ProcessGroup(name, processes) { + events.EventEmitter.call(this); + + this.name = name; + this.state = ProcessGroupStates.STOPPED; + this.processes = []; + this.restarting = false; + + for (let process of processes) { + this.addProcess(process); + } +}; +util.inherits(ProcessGroup, events.EventEmitter); +ProcessGroup.prototype = extend(ProcessGroup.prototype, { + addProcess: function(process) { + this.processes.push(process); + process.on('state-update', this.onProcessStateUpdate.bind(this)); + }, + start: function() { + if (this.state != ProcessGroupStates.STOPPED) { + console.warn("Can't start process group that is not stopped."); + return; + } + + for (let process of this.processes) { + process.start(); + } + + this.state = ProcessGroupStates.STARTED; + this.emit('state-update', this); + }, + stop: function() { + if (this.state != ProcessGroupStates.STARTED) { + console.warn("Can't stop process group that is not started."); + return; + } + for (let process of this.processes) { + process.stop(); + } + this.state = ProcessGroupStates.STOPPING; + this.emit('state-update', this); + }, + restart: function() { + if (this.state == ProcessGroupStates.STOPPED) { + // start the group, we were already stopped + this.start(); + } else { + // set our restart flag so the group will restart once stopped + this.restarting = true; + + // call stop, that will put them in the stopping state + this.stop(); + } + }, + + // Event handlers + onProcessStateUpdate: function(process) { + var processesStillRunning = false; + for (let process of this.processes) { + if (process.state != ProcessStates.STOPPED) { + processesStillRunning = true; + break; + } + } + if (!processesStillRunning) { + this.state = ProcessGroupStates.STOPPED; + this.emit('state-update', this); + + // if we we're supposed to restart, call start now and reset the flag + if (this.restarting) { + this.start(); + this.restarting = false; + } + } + this.emit('process-update', process); + } +}); + +var ID = 0; +function Process(name, command, commandArgs, logDirectory) { + events.EventEmitter.call(this); + + this.id = ++ID; + this.name = name; + this.command = command; + this.commandArgs = commandArgs ? commandArgs : []; + this.child = null; + this.logDirectory = logDirectory; + this.logStdout = null; + this.logStderr = null; + + this.state = ProcessStates.STOPPED; +}; +util.inherits(Process, events.EventEmitter); +Process.prototype = extend(Process.prototype, { + start: function() { + if (this.state != ProcessStates.STOPPED) { + console.warn("Can't start process that is not stopped."); + return; + } + console.log("Starting " + this.command + " " + this.commandArgs.join(' ')); + + var logStdout = 'ignore', + logStderr = 'ignore'; + + if (this.logDirectory) { + var logDirectoryCreated = false; + + try { + fs.mkdirsSync(this.logDirectory); + logDirectoryCreated = true; + } catch (e) { + if (e.code == 'EEXIST') { + logDirectoryCreated = true; + } else { + console.error("Error creating log directory"); + } + } + + if (logDirectoryCreated) { + // Create a temporary file with the current time + var time = (new Date).getTime(); + var tmpLogStdout = path.resolve(this.logDirectory + '/' + this.name + '-' + time + '-stdout.txt'); + var tmpLogStderr = path.resolve(this.logDirectory + '/' + this.name + '-' + time + '-stderr.txt'); + + try { + logStdout = fs.openSync(tmpLogStdout, 'ax'); + } catch(e) { + console.log("Error creating stdout log file", e); + logStdout = 'ignore'; + } + try { + logStderr = fs.openSync(tmpLogStderr, 'ax'); + } catch(e) { + console.log("Error creating stderr log file", e); + logStderr = 'ignore'; + } + } + } + + try { + this.child = childProcess.spawn(this.command, this.commandArgs, { + detached: false, + stdio: ['ignore', logStdout, logStderr] + }); + } catch (e) { + console.log("Got error starting child process for " + this.name, e); + this.child = null; + this.updateState(ProcessStates.STOPPED); + return; + } + + if (logStdout != 'ignore') { + var pidLogStdout = path.resolve(this.logDirectory + '/' + this.name + "-" + this.child.pid + "-" + time + "-stdout.txt"); + fs.rename(tmpLogStdout, pidLogStdout, function(e) { + if (e !== null) { + console.log("Error renaming log file from " + tmpLogStdout + " to " + pidLogStdout, e); + } + }); + this.logStdout = pidLogStdout; + fs.closeSync(logStdout); + } + + if (logStderr != 'ignore') { + var pidLogStderr = path.resolve(this.logDirectory + '/' + this.name + "-" + this.child.pid + "-" + time + "-stderr.txt"); + fs.rename(tmpLogStderr, pidLogStderr, function(e) { + if (e !== null) { + console.log("Error renaming log file from " + tmpLogStdout + " to " + pidLogStdout, e); + } + }); + this.logStderr = pidLogStderr; + + fs.closeSync(logStderr); + } + + this.child.on('error', this.onChildStartError.bind(this)); + this.child.on('close', this.onChildClose.bind(this)); + + console.log("Child process started"); + this.updateState(ProcessStates.STARTED); + this.emit('logs-updated'); + }, + stop: function(force) { + if (this.state == ProcessStates.STOPPED) { + console.warn("Can't stop process that is not started or stopping."); + return; + } + if (os.type() == "Windows_NT") { + var command = "taskkill /pid " + this.child.pid; + if (force) { + command += " /f /t"; + } + childProcess.exec(command, {}, function(error) { + if (error) { + console.error('Error executing taskkill:', error); + } + }); + } else { + var signal = force ? 'SIGKILL' : null; + this.child.kill(signal); + } + + console.log("Stopping child process:", this.child.pid, this.name); + + if (!force) { + this.stoppingTimeoutID = setTimeout(function() { + if (this.state == ProcessStates.STOPPING) { + console.log("Force killling", this.name, this.child.pid); + this.stop(true); + } + }.bind(this), 2500); + } + + this.updateState(ProcessStates.STOPPING); + }, + updateState: function(newState) { + if (this.state != newState) { + this.state = newState; + this.emit('state-update', this); + return true; + } + return false; + }, + getLogs: function() { + var logs = {}; + logs[this.child.pid] = { + stdout: this.logStdout == 'ignore' ? null : this.logStdout, + stderr: this.logStderr == 'ignore' ? null : this.logStderr + }; + return logs; + }, + + // Events + onChildStartError: function(error) { + console.log("Child process error ", error); + this.updateState(ProcessStates.STOPPED); + }, + onChildClose: function(code) { + console.log("Child process closed with code ", code, this.name); + if (this.stoppingTimeoutID) { + clearTimeout(this.stoppingTimeoutID); + this.stoppingTimeoutID = null; + } + this.updateState(ProcessStates.STOPPED); + } +}); + +// ACMonitorProcess is an extension of Process that keeps track of the AC Montior's +// children status and log locations. +const CHECK_AC_STATUS_INTERVAL = 5000; +function ACMonitorProcess(name, path, args, httpStatusPort, logPath) { + Process.call(this, name, path, args, logPath); + + this.httpStatusPort = httpStatusPort; + + this.requestTimeoutID = null; + this.pendingRequest = null; + this.childServers = {}; +}; +util.inherits(ACMonitorProcess, Process); +ACMonitorProcess.prototype = extend(ACMonitorProcess.prototype, { + updateState: function(newState) { + if (ACMonitorProcess.super_.prototype.updateState.call(this, newState)) { + if (this.state == ProcessStates.STARTED) { + this._updateACMonitorStatus(); + } else { + if (this.requestTimeoutID) { + clearTimeout(this.requestTimeoutID); + this.requestTimeoutID = null; + } + if (this.pendingRequest) { + this.pendingRequest.destroy(); + this.pendingRequest = null; + } + } + } + }, + getLogs: function() { + var logs = {}; + logs[this.child.pid] = { + stdout: this.logStdout == 'ignore' ? null : this.logStdout, + stderr: this.logStderr == 'ignore' ? null : this.logStderr + }; + for (var pid in this.childServers) { + logs[pid] = { + stdout: this.childServers[pid].logStdout, + stderr: this.childServers[pid].logStderr + } + } + console.log(logs); + return logs; + }, + _updateACMonitorStatus: function() { + if (this.state != ProcessStates.STARTED) { + return; + } + + // If there is a pending request, return + if (this.pendingRequest) { + return; + } + + console.log("Checking AC Monitor status"); + var options = { + url: "http://localhost:" + this.httpStatusPort + "/status", + json: true + }; + this.pendingRequest = request(options, function(error, response, body) { + if (error) { + console.error('ERROR Getting AC Monitor status', error); + } else { + this.childServers = body.servers; + } + console.log(body); + + this.emit('logs-updated'); + + this.requestTimeoutID = setTimeout(this._updateACMonitorStatus.bind(this), CHECK_AC_STATUS_INTERVAL); + }.bind(this)); + } +}); + +module.exports.Process = Process; +module.exports.ACMonitorProcess = ACMonitorProcess; +module.exports.ProcessGroup = ProcessGroup; +module.exports.ProcessGroupStates = ProcessGroupStates; +module.exports.ProcessStates = ProcessStates; diff --git a/server-console/src/modules/hf-updater.js b/server-console/src/modules/hf-updater.js new file mode 100644 index 0000000000..38e409b3a2 --- /dev/null +++ b/server-console/src/modules/hf-updater.js @@ -0,0 +1,46 @@ +const request = require('request'); +const extend = require('extend'); +const util = require('util'); +const events = require('events'); +const cheerio = require('cheerio'); +const os = require('os'); + +const platform = os.type() == 'Windows_NT' ? 'windows' : 'mac'; + +const BUILDS_URL = 'https://highfidelity.com/builds.xml'; + +function UpdateChecker(currentVersion, checkForUpdatesEveryXSeconds) { + this.currentVersion = currentVersion; + console.log('cur', currentVersion); + + setInterval(this.checkForUpdates.bind(this), checkForUpdatesEveryXSeconds * 1000); + this.checkForUpdates(); +}; +util.inherits(UpdateChecker, events.EventEmitter); +UpdateChecker.prototype = extend(UpdateChecker.prototype, { + checkForUpdates: function() { + console.log("Checking for updates"); + request(BUILDS_URL, (error, response, body) => { + if (error) { + console.log("Error", error); + return; + } + if (response.statusCode == 200) { + try { + var $ = cheerio.load(body, { xmlMode: true }); + const latestBuild = $('project[name="interface"] platform[name="' + platform + '"]').children().first(); + const latestVersion = parseInt(latestBuild.find('version').text()); + console.log("Latest version is:", latestVersion, this.currentVersion); + if (latestVersion > this.currentVersion) { + const url = latestBuild.find('url').text(); + this.emit('update-available', latestVersion, url); + } + } catch (e) { + console.warn("Error when checking for updates", e); + } + } + }); + } +}); + +exports.UpdateChecker = UpdateChecker; diff --git a/server-console/src/modules/path-finder.js b/server-console/src/modules/path-finder.js new file mode 100644 index 0000000000..d4cb9aa277 --- /dev/null +++ b/server-console/src/modules/path-finder.js @@ -0,0 +1,85 @@ +var fs = require('fs'); +var path = require('path'); + +exports.searchPaths = function(name, binaryType, releaseType) { + function platformExtension(name) { + if (name == "Interface" || name == "High Fidelity") { + if (process.platform == "darwin") { + return ".app/Contents/MacOS/" + name + } else if (process.platform == "win32") { + return ".exe" + } else { + return "" + } + } else { + return process.platform == "win32" ? ".exe" : "" + } + } + + var extension = platformExtension(name); + var devBasePath = "../build/" + name + "/"; + + var paths = []; + + if (!releaseType) { + // check in the developer build tree for binaries + paths = [ + devBasePath + name + extension, + devBasePath + (binaryType == "local-release" ? "Release/" : "Debug/") + name + extension + ] + } else { + // check directly beside the binary + paths = [ + path.join(path.dirname(process.execPath), name + extension) + ]; + + // assume we're inside an app bundle on OS X + if (process.platform == "darwin") { + var contentPath = ".app/Contents/"; + var contentEndIndex = __dirname.indexOf(contentPath); + + if (contentEndIndex != -1) { + // this is an app bundle + var appPath = __dirname.substring(0, contentEndIndex) + ".app"; + + // check in Contents/MacOS for the binaries + var componentsPath = appPath + "/Contents/MacOS/Components.app/Contents/MacOS/"; + paths.push(componentsPath + name + extension); + + // check beside the app bundle for the binaries + paths.push(path.join(path.dirname(appPath), name + extension)); + } + } + } + + return paths; +} + +exports.discoveredPath = function (name, binaryType, releaseType) { + function binaryFromPaths(name, paths) { + for (var i = 0; i < paths.length; i++) { + var testPath = paths[i]; + + try { + var stats = fs.lstatSync(testPath); + + if (stats.isFile() || (stats.isDirectory() && extension == ".app")) { + console.log("Found " + name + " at " + testPath); + return testPath; + } + } catch (e) { + console.log("Executable with name " + name + " not found at path " + testPath); + } + } + + return null; + } + + // for a released server console on OS X, assume the name of the interface executable is "High Fidelity" + if (releaseType && process.platform == "darwin" && name == "Interface") { + name = "High Fidelity"; + } + + // attempt to find a binary at the usual paths, return null if it doesn't exist + return binaryFromPaths(name, this.searchPaths(name, binaryType, releaseType)); +} diff --git a/server-console/src/splash.css b/server-console/src/splash.css new file mode 100644 index 0000000000..bd3a06cbe3 --- /dev/null +++ b/server-console/src/splash.css @@ -0,0 +1,146 @@ +@font-face { + font-family: 'Raleway'; + src: url('vendor/Raleway/Raleway-Regular.ttf'); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: 'Raleway'; + src: url('vendor/Raleway/Raleway-ExtraLight.ttf'); + font-weight: 200; + font-style: normal; +} +@font-face { + font-family: 'Raleway'; + src: url('vendor/Raleway/Raleway-SemiBold.ttf'); + font-weight: bold; + font-style: normal; +} + +body { + height: 100%; + width: 100%; + margin: 0; + padding: 0; + color: #414141; + -webkit-touch-callout: none; + -webkit-user-select: none; + cursor: default; + font-variant-numeric: lining-nums; + -moz-font-feature-settings: "lnum"; + -webkit-font-feature-settings: "lnum"; + font-feature-settings: "lnum"; +} + +* { + font-family: "Raleway", "Open Sans", Arial, Helvetica, sans-serif; + font-size: 13.5pt; + line-height: 130%; +} + +a:link, +a:visited, +a:hover, +a:active { + font-weight: bold; + color: #2D88A4; +} + +a:hover { + font-weight: bold; + color: #00B4EF; +} + +h1 { + font-weight: normal; + color: #6D7472; + font-size: 30pt; + margin: 0; + padding: 0; +} + +h2 { + color: #6D7472; + font-weight: bold; + margin: 0; + padding: 0; +} + +.header-title { + padding-top: 80px; +} + +.content { + margin: 0 110px; +} + +.column { + display: inline-block; + float: left; + padding-top: 26px; +} + +.column.left { + width: 415px; + padding-right: 120px; +} + +.column.center { + width: 565px; +} + +.column.right { + z-index: -1; + float: right; + padding-top: 16px; + padding-left: 60px; + position: absolute; + right: -30px; +} + +.top { + height: 168px; + border-bottom: 2px solid #F5F6F6; +} + +.middle { + width: 1364px; + /* 1584 - (110 * 2) */ + position: absolute; + top: 168px; + bottom: 98px; +} + +.bottom { + width: 100%; + position: absolute; + height: 98px; + background-color: #F5F6F6; + bottom: 0; + /* padding-top: 34px; */ +} + +#main-content { + height: 350px; + border-bottom: 2px solid #F5F6F6; +} + +#existing-resources-area { + padding-top: 20px; +} + +.footer { + padding-top: 34px; +} + +.header-right { + /* float: right; */ + position: absolute; + top: 63px; + right: 114px; +} + +input[type="checkbox"] { + -webkit-transform: scale(1.4); + display: inline-block; +} \ No newline at end of file diff --git a/server-console/src/splash.html b/server-console/src/splash.html new file mode 100644 index 0000000000..30868ebb6f --- /dev/null +++ b/server-console/src/splash.html @@ -0,0 +1,86 @@ + + + + High Fidelity + + + + +
+
+

Hello Worlds!

+
+
+ +
+
+
+
+
+

+

What now?

+High Fidelity is now installed and your Home domain is ready for you to explore. To start you off, we've put a few things in your home to play around with and learn the ropes. +

+ +

+You can make your home yours by uploading your own models and scripts, and adding items from the Market. + +

+ +

+ To see how, check out 'The Basics' +

+
+
+

+

How do I use it?

+ You can manage your server by clicking on the High Fidelity icon in your + + In the menu that opens, you can: +

+ +

+

    +
  • go to your 'Home,' automatically launching High Fidelity
  • +
  • administer basic function like starting and stopping your server
  • +
  • access your server's settings and logs
  • +
+

+ +

+ For more information on managing your server, visit our documentation +

+
+
+ +
+
+
+

+

Your existing Stack Manager content is safe.

+ Server Console comes with demo content but does not overwrite your data. See our guide to importing content previously managed with Stack Manager. +

+
+
+
+ +
+ + diff --git a/server-console/src/splash.js b/server-console/src/splash.js new file mode 100644 index 0000000000..3d9bd81bde --- /dev/null +++ b/server-console/src/splash.js @@ -0,0 +1,11 @@ +var remote = require('electron').remote; + +ready = function() { + window.$ = require('./vendor/jquery/jquery-2.1.4.min.js'); + + var userConfig = remote.getGlobal('userConfig'); + $('#suppress-splash').change(function() { + console.log("updating"); + userConfig.set('doNotShowSplash', $(this).is(':checked')); + }); +} diff --git a/server-console/src/vendor/Raleway/OFL.txt b/server-console/src/vendor/Raleway/OFL.txt new file mode 100644 index 0000000000..1c9779ddcd --- /dev/null +++ b/server-console/src/vendor/Raleway/OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2010, Matt McInerney (matt@pixelspread.com), +Copyright (c) 2011, Pablo Impallari (www.impallari.com|impallari@gmail.com), +Copyright (c) 2011, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com), with Reserved Font Name Raleway +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/server-console/src/vendor/Raleway/Raleway-ExtraLight.ttf b/server-console/src/vendor/Raleway/Raleway-ExtraLight.ttf new file mode 100644 index 0000000000..281a001983 Binary files /dev/null and b/server-console/src/vendor/Raleway/Raleway-ExtraLight.ttf differ diff --git a/server-console/src/vendor/Raleway/Raleway-Regular.ttf b/server-console/src/vendor/Raleway/Raleway-Regular.ttf new file mode 100644 index 0000000000..252cad14a6 Binary files /dev/null and b/server-console/src/vendor/Raleway/Raleway-Regular.ttf differ diff --git a/server-console/src/vendor/Raleway/Raleway-SemiBold.ttf b/server-console/src/vendor/Raleway/Raleway-SemiBold.ttf new file mode 100644 index 0000000000..64b54a77a3 Binary files /dev/null and b/server-console/src/vendor/Raleway/Raleway-SemiBold.ttf differ diff --git a/server-console/src/vendor/jquery/jquery-2.1.4.min.js b/server-console/src/vendor/jquery/jquery-2.1.4.min.js new file mode 100644 index 0000000000..49990d6e14 --- /dev/null +++ b/server-console/src/vendor/jquery/jquery-2.1.4.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.4 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b="length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){ +return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,ba=/<([\w:]+)/,ca=/<|&#?\w+;/,da=/<(?:script|style|link)/i,ea=/checked\s*(?:[^=]|=\s*.checked.)/i,fa=/^$|\/(?:java|ecma)script/i,ga=/^true\/(.*)/,ha=/^\s*\s*$/g,ia={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ia.optgroup=ia.option,ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead,ia.th=ia.td;function ja(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function ka(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function la(a){var b=ga.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function ma(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function na(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function oa(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pa(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=oa(h),f=oa(a),d=0,e=f.length;e>d;d++)pa(f[d],g[d]);if(b)if(c)for(f=f||oa(a),g=g||oa(h),d=0,e=f.length;e>d;d++)na(f[d],g[d]);else na(a,h);return g=oa(h,"script"),g.length>0&&ma(g,!i&&oa(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(ca.test(e)){f=f||k.appendChild(b.createElement("div")),g=(ba.exec(e)||["",""])[1].toLowerCase(),h=ia[g]||ia._default,f.innerHTML=h[1]+e.replace(aa,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=oa(k.appendChild(e),"script"),i&&ma(f),c)){j=0;while(e=f[j++])fa.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(oa(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&ma(oa(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(oa(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!da.test(a)&&!ia[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(aa,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(oa(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(oa(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&ea.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(oa(c,"script"),ka),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,oa(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,la),j=0;g>j;j++)h=f[j],fa.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(ha,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qa,ra={};function sa(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function ta(a){var b=l,c=ra[a];return c||(c=sa(a,b),"none"!==c&&c||(qa=(qa||n("