/** * StudioUtilities.cpp * Copyright (c) 2013 ROBLOX Corp. All Rights Reserved. */ #include "stdafx.h" #include "StudioUtilities.h" // Qt Headers #include #include #include #include // 3rd Party Headers #include "boost/iostreams/copy.hpp" // Roblox Headers #include "network/api.h" #include "script/script.h" #include "script/ScriptContext.h" #include "util/FileSystem.h" #include "util/Hash.h" #include "util/RobloxGoogleAnalytics.h" #include "util/RunStateOwner.h" #include "util/Statistics.h" #include "v8datamodel/Camera.h" #include "v8datamodel/ContentProvider.h" #include "v8datamodel/ChangeHistory.h" #include "v8datamodel/DataModel.h" #include "v8datamodel/PartInstance.h" #include "v8datamodel/Tool.h" #include "v8datamodel/Workspace.h" #include "v8world/World.h" #include "v8datamodel/Visit.h" #include "v8xml/Serializer.h" #include "v8xml/XmlSerializer.h" #include "reflection/Type.h" #include "Client.h" #include "ClientReplicator.h" #include "Network/Players.h" #include "Marker.h" // Roblox Studio Headers #include "QtUtilities.h" #include "RobloxMainWindow.h" #include "UpdateUIManager.h" #include "MobileDevelopmentDeployer.h" #include "RobloxUser.h" #include "RobloxDocManager.h" #include "RobloxIDEDoc.h" #include "RobloxGameExplorer.h" #include "RobloxSettings.h" DYNAMIC_FASTFLAG(MaterialPropertiesEnabled) DYNAMIC_FASTFLAG(CloudEditGARespectsThrottling) FASTFLAGVARIABLE(TeamCreateOptimizeRemoteSelection, false) static const char* kCloudEditScriptUrlPrefix = "CloudEditPlace:"; namespace StudioUtilities { static bool sIsVideoUploading = false; static std::string sVideoFileName(""); static bool IsAvatarMode = false; static bool IsTestMode = false; static rbx::atomic sNumScreenShotUploads; static bool IsFirstTimeOpeningStudio = false; bool isFirstTimeOpeningStudio() { return IsFirstTimeOpeningStudio; } void setIsFirstTimeOpeningStudio(bool value) { IsFirstTimeOpeningStudio = value; } bool isConnectedToNetwork() { QList ifaces = QNetworkInterface::allInterfaces(); bool result = false; for (int i = 0; i < ifaces.count(); i++) { QNetworkInterface iface = ifaces.at(i); if ( iface.flags().testFlag(QNetworkInterface::IsUp) && !iface.flags().testFlag(QNetworkInterface::IsLoopBack) ) { #ifdef _DEBUG // details of connection qDebug() << "name:" << iface.name() << endl << "ip addresses:" << endl << "mac:" << iface.hardwareAddress() << endl; #endif // this loop is important for (int j=0; j 0; } void setVideoUploading(bool state) { sIsVideoUploading = state; } bool isVideoUploading() { return sIsVideoUploading; } void setVideoFileName(const std::string &fileName) { sVideoFileName = fileName; } std::string getVideoFileName() { return sVideoFileName; } bool containsEditScript(const QString& url) { return url.contains("edit.ashx", Qt::CaseInsensitive); } bool containsJoinScript(const QString& url) { return url.contains("join.ashx", Qt::CaseInsensitive); } bool containsVisitScript(const QString& url) { return url.contains("visit.ashx", Qt::CaseInsensitive); } bool containsGameServerScript(const QString& url) { return url.contains("gameserver.ashx", Qt::CaseInsensitive); } std::string authenticate(std::string &domain, std::string &url, std::string &ticket) { if(domain.empty() || url.empty() || ticket.empty()) return ""; try { // Post our ticket back to Roblox to suggest a re-authentication std::string result; { std::string compound = url; compound += "?suggest="; compound += ticket; RBX::Http http(compound.c_str()); #ifdef Q_WS_MAC http.setAuthDomain(::GetBaseURL()); #else http.additionalHeaders["RBXAuthenticationNegotiation:"] = ::GetBaseURL(); #endif //http.additionalHeaders["RBXAuthenticationNegotiation:"] = domain; http.get(result); } // The http content is the new ticket return result; } catch (std::exception& e) { //Report Error! RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, e.what()); return ""; } } RBX::ProtectedString fetchAndValidateScript(RBX::DataModel* pDataModel, const std::string& urlScript) { if(!pDataModel) return RBX::ProtectedString(); RBX::Security::Impersonator impersonate(RBX::Security::COM); std::ostringstream data; if (!RBX::ContentProvider::isUrl(urlScript)) return RBX::ProtectedString(); std::string dataString; { try { RBX::DataModel::LegacyLock lock(pDataModel, RBX::DataModelJob::Write); std::auto_ptr stream(RBX::ServiceProvider::create(pDataModel)->getContent(RBX::ContentId(urlScript.c_str()))); dataString = std::string(static_cast(std::stringstream() << stream->rdbuf()).str()); } catch(std::exception& e) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "ContentProvider while executing url script failed because %s", e.what()); } } RBX::ProtectedString verifiedSource; try { verifiedSource = RBX::ProtectedString::fromTrustedSource(dataString); RBX::ContentProvider::verifyScriptSignature(verifiedSource, true); } catch(std::bad_alloc&) { throw; } catch(std::exception& e) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "Script signature verification failed -- %s", e.what()); return RBX::ProtectedString(); } return verifiedSource; } void executeURLJoinScript(boost::shared_ptr pGame, std::string urlScript) { RBXASSERT(containsJoinScript(QString::fromStdString(urlScript))); boost::shared_ptr pDataModel = pGame->getDataModel(); if(!pDataModel) return; RBX::ProtectedString verifiedSource = fetchAndValidateScript(pDataModel.get(), urlScript); if (verifiedSource.empty()) return; try { RBX::DataModel::LegacyLock lock(pDataModel, RBX::DataModelJob::Write); if (pDataModel->isClosed()) return; std::string dataString = verifiedSource.getSource(); int firstNewLineIndex = dataString.find("\r\n"); if (dataString[firstNewLineIndex+2] == '{') { pGame->configurePlayer(RBX::Security::COM, dataString.substr(firstNewLineIndex+2)); if (RobloxIDEDoc* ideDoc = RobloxDocManager::Instance().getPlayDoc()) { QMetaObject::invokeMethod(ideDoc, "refreshDisplayName", Qt::QueuedConnection); } return; } boost::shared_ptr pContext = RBX::shared_from(pDataModel->create()); if (pContext) pContext->executeInNewThread(RBX::Security::COM, verifiedSource, "Start Game"); } catch (std::exception& exp) { std::string s(verifiedSource.getSource()); std::string msg = RBX::format("executeURLJoinScript() failed because %s. %s", exp.what(), s.substr(0,64).c_str()); RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, msg.c_str()); } } void executeURLScript(boost::shared_ptr pDataModel, std::string urlScript) { if(!pDataModel) return; RBX::ProtectedString verifiedSource = fetchAndValidateScript(pDataModel.get(), urlScript); if (verifiedSource.empty()) return; try { RBX::DataModel::LegacyLock lock(pDataModel, RBX::DataModelJob::Write); if (pDataModel->isClosed()) return; boost::shared_ptr pContext = RBX::shared_from(pDataModel->create()); if (pContext) pContext->executeInNewThread(RBX::Security::COM, verifiedSource, "Start Game"); } catch (std::exception& exp) { std::string s(verifiedSource.getSource()); std::string msg = RBX::format("executeURLScript() failed because %s. %s", exp.what(), s.substr(0,64).c_str()); RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, msg.c_str()); } } void reportCloudEditJoinEvent(const char* label) { if (DFFlag::CloudEditGARespectsThrottling) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_STUDIO, "CloudEdit", label); } else { RBX::RobloxGoogleAnalytics::trackEventWithoutThrottling(GA_CATEGORY_STUDIO, "CloudEdit", label); } } static void notifyCloudEditConnectionClosed(const char* category, std::string message) { std::string label = RBX::format("Connection Closed: %s | %s", category, message.c_str()); reportCloudEditJoinEvent(label.c_str()); QMetaObject::invokeMethod(&(UpdateUIManager::Instance().getMainWindow()), "notifyCloudEditConnectionClosed", Qt::QueuedConnection); } static void updateCameraLocation(RBX::DataModel* dm) { if (RBX::Network::Player* p = RBX::Network::Players::findLocalPlayer(dm)) { p->setCloudEditCameraCoordinateFrame(dm->getWorkspace()->getCamera()->getCameraCoordinateFrame()); } } static void doUpdateSelection(shared_ptr debouncer, RBX::DataModel* dm) { if (RBX::Network::Player* p = RBX::Network::Players::findLocalPlayer(dm)) { if (RBX::Selection* s = dm->find()) { shared_ptr va(new RBX::Reflection::ValueArray); for (auto& i : *s) { va->push_back(i); } RBX::Network::Player::event_cloudEditSelectionChanged.replicateEvent(p, shared_ptr(va)); } } if (FFlag::TeamCreateOptimizeRemoteSelection) { *debouncer = false; } } static void updateSelection(shared_ptr debouncer, RBX::DataModel* dm) { if (FFlag::TeamCreateOptimizeRemoteSelection) { if (*debouncer) return; *debouncer = true; dm->submitTask(boost::bind(&doUpdateSelection, debouncer, _1), RBX::DataModelJob::Write); } else { doUpdateSelection(debouncer, dm); } } static void onConnectionAccepted(int placeId, std::string url, shared_ptr replicator) { using namespace RBX; reportCloudEditJoinEvent("Connection Accepted"); try { boost::shared_ptr rep = Instance::fastSharedDynamicCast(replicator); rep->disconnectionSignal.connect(boost::bind(¬ifyCloudEditConnectionClosed, "ReplicatorDisconnect", _1)); } catch (RBX::base_exception& e) { StandardOut::singleton()->printf(MESSAGE_ERROR, "onConnectionAccepted failed: %s", e.what()); } } void joinCloudEditSession(shared_ptr pGame, shared_ptr config) { using namespace RBX; try { boost::shared_ptr dataModel = pGame->getDataModel(); if(!dataModel) return; DataModel::LegacyLock lock(dataModel, DataModelJob::Write); if (dataModel->isClosed()) return; reportCloudEditJoinEvent("Attempt To Connect"); Security::Impersonator impersonate(Security::COM); Http::gameID = config->get("GameId").get(); int userId = RobloxUser::singleton().getUserId(); RBX::Analytics::setUserId(userId); int placeId = config->get("PlaceId").get(); dataModel->setPlaceID(placeId, false); dataModel->setUniverseId(config->get("UniverseId").get()); ScriptContext::propScriptsDisabled.setValue(dataModel->create(), true); dataModel->create()->setThreadPool(16); Network::Players* players = dataModel->create(); // creating a visit helps suppress saving behaviors that don't work well in CloudEdit dataModel->create(); if (RobloxIDEDoc* ideDoc = RobloxDocManager::Instance().getPlayDoc()) { ideDoc->enableUndo(true); QMetaObject::invokeMethod(ideDoc, "updateFromPlaceID", Qt::QueuedConnection, Q_ARG(int, placeId)); QMetaObject::invokeMethod(&UpdateUIManager::Instance().getViewWidget(eDW_GAME_EXPLORER), "openGameFromPlaceId", Qt::QueuedConnection, Q_ARG(int, placeId)); } Network::Client* client = dataModel->create(); client->configureAsCloudEditClient(); client->connectionAcceptedSignal.connect(boost::bind(&onConnectionAccepted, placeId, _1, _2)); client->connectionRejectedSignal.connect(boost::bind(¬ifyCloudEditConnectionClosed, "Rejected", _1)); client->connectionFailedSignal.connect(boost::bind(¬ifyCloudEditConnectionClosed, "Failed", _3)); client->setTicket(config->get("ClientTicket").get()); client->setGameSessionID(config->get("SessionId").get()); shared_ptr player = Instance::fastSharedDynamicCast(client->playerConnect( userId, config->get("MachineAddress").get(), config->get("ServerPort").get(), 0, -1)); player->setName(config->get("UserName").get()); player->setCharacterAppearance(config->get("CharacterAppearance").get()); if (RunService* rs = dataModel->create()) { rs->heartbeatSignal.connect(boost::bind(&updateCameraLocation, dataModel.get())); } shared_ptr selectionUpdateDebouncer(new bool(false)); dataModel->create()->selectionChanged.connect(boost::bind(&updateSelection, selectionUpdateDebouncer, dataModel.get())); // creating a client will reset the mouse command to null, set it back to adv arrow here dataModel->getWorkspace()->setDefaultMouseCommand(); } catch (const std::exception& e) { StandardOut::singleton()->printf(MESSAGE_ERROR, "Error while joining CloudEdit: %s", e.what()); QtUtilities::RBXMessageBox failureBox; failureBox.setText("Exception while trying to join CloudEdit. See Output for details."); failureBox.exec(); } } void startMobileDevelopmentDeployer() { MobileDevelopmentDeployer::singleton().startPlaySessionWithRbxDevDevices(); } void stopMobileDevelopmentDeployer() { MobileDevelopmentDeployer::singleton().disconnectClientsAndShutDown(); } void convertPhysicalPropertiesIfNeeded(std::vector > instances, RBX::Workspace* workspace) { if (workspace->getUsingNewPhysicalProperties()) { for (size_t i = 0; i < instances.size(); ++i) { RBX::PartInstance::convertToNewPhysicalPropRecursive(instances[i].get()); } } else { for (size_t i = 0; i < instances.size(); i++) { if (RBX::PartInstance::instanceOrChildrenContainsNewCustomPhysics(instances[i].get())) { if (RobloxIDEDoc::displayAskConvertPlaceToNewMaterialsIfInsertNewModel()) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "PhysicalProperties: Insert - Conversion", "Workspace->PhysicalPropertiesMode_New"); workspace->setPhysicalPropertiesMode(RBX::PhysicalPropertiesMode_NewPartProperties); RBX::PartInstance::convertToNewPhysicalPropRecursive(RBX::DataModel::get(workspace)); } break; } } } } void insertModel(boost::shared_ptr pDataModel, QString fileName, bool insertInto) { if (fileName.isEmpty()) return; if (!pDataModel) throw std::runtime_error("Can't insert at this time"); QByteArray ba = fileName.toAscii(); const char *c_str = ba.constData(); std::ifstream stream(c_str, std::ios_base::in | std::ios_base::binary); // The following code happens BEFORE we do a lock because we can't display a message box within a lock RBX::Instances instances; Serializer().loadInstances(stream, instances); { // Lock the data model temporarily to make sure insertion is legal RBX::DataModel::LegacyLock lock(pDataModel, RBX::DataModelJob::Write); if (DFFlag::MaterialPropertiesEnabled) { convertPhysicalPropertiesIfNeeded(instances, pDataModel->getWorkspace()); } if (RBX::ContentProvider* cp = pDataModel->create()) { RBX::LuaSourceContainer::blockingLoadLinkedScriptsForInstances(cp, instances); } } RBX::PromptMode promptMode = RBX::SUPPRESS_PROMPTS; if (!insertInto){ if (instances.size() == 1) { RBX::Instance* single = instances[0].get(); if (dynamic_cast(single)) { if( QtUtilities::RBXMessageBox::question( &UpdateUIManager::Instance().getMainWindow(), "Insert Model", "Put this tool into the starter pack (otherwise drop into the 3d view)?", QMessageBox::Yes | QMessageBox::No, QMessageBox::No ) == QMessageBox::Yes ) promptMode = RBX::PUT_TOOL_IN_STARTERPACK; } } } // Now lock the datamodel (after MessageBox) RBX::DataModel::LegacyLock lock(pDataModel, RBX::DataModelJob::Write); shared_ptr spWorkspace = shared_from(pDataModel ? pDataModel->getWorkspace() : NULL); if(!spWorkspace) throw std::runtime_error("Can't insert at this time"); RBX::Selection* sel = RBX::ServiceProvider::create(spWorkspace.get()); RBX::Instance* requestedParent = (insertInto && (sel->size() == 1)) ? sel->front().get() : spWorkspace.get(); RBX::InsertMode insertMode = insertInto ? RBX::INSERT_TO_TREE : RBX::INSERT_TO_3D_VIEW; try { spWorkspace->insertInstances(instances, requestedParent, insertMode, promptMode); RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, insertInto ? "insertIntoFromFileComplete" : "insertModelComplete"); boost::shared_ptr runService = shared_from(pDataModel->create()); // If we're not playing a game then select the content if (runService->getRunState() == RBX::RS_STOPPED) { sel->setSelection(instances.begin(), instances.end()); } } catch (std::exception&) { std::for_each(instances.begin(), instances.end(), boost::bind(&RBX::Instance::setParent, _1, (RBX::Instance*) NULL)); throw; } } void insertScript(boost::shared_ptr pDataModel, const QString &fileName) { shared_ptr spWorkspace = shared_from(pDataModel ? pDataModel->getWorkspace() : NULL); if(!pDataModel || !spWorkspace) throw std::runtime_error("Script cannot be inserted at this time"); QFile file(fileName); if (!file.open(QFile::ReadOnly | QFile::Text)) throw std::runtime_error("Script file cannot be opened"); RBX::DataModel::LegacyLock lock(pDataModel, RBX::DataModelJob::Write); shared_ptr pScriptInstance = RBX::Creatable::create(); if (!pScriptInstance) throw std::runtime_error("Script instance cannot be created"); // set file name as script instance name pScriptInstance->setName(QFileInfo(fileName).completeBaseName().toStdString()); // read and set code from file into script instance QTextStream in(&file); pScriptInstance->setEmbeddedCode(RBX::ProtectedString::fromTrustedSource(in.readAll().toAscii().data())); // set appropriate parent RBX::Selection* pSelection = RBX::ServiceProvider::create(spWorkspace.get()); RBX::Instance* pRequestedParent = (pSelection && pSelection->size() == 1) ? pSelection->front().get() : spWorkspace.get(); pScriptInstance->setParent(pRequestedParent); // select the created script instance if (pSelection) pSelection->setSelection(pScriptInstance.get()); } QString getDebugInfoFile(const QString& fileName, const QString& debuggerFileExt) { QString debugInfoFile; if (!fileName.isEmpty()) { static QString filePath = QtUtilities::qstringFromStdString(RBX::FileSystem::getUserDirectory(true, RBX::DirAppData, "debugger").native()); // get hash of the file path unsigned int hash = RBX::Hash::hash(QFileInfo(fileName).absoluteFilePath().toStdString()); // create debug file path with generated hash debugInfoFile = filePath; debugInfoFile.append(QString::number(hash)); if (!debuggerFileExt.isEmpty()) debugInfoFile.append(debuggerFileExt); debugInfoFile.append(".rdbg"); } return debugInfoFile; } bool checkNetworkAndUserAuthentication(bool openStartPage) { if (!StudioUtilities::isConnectedToNetwork()) { // we don't have network connect UpdateUIManager::Instance().showErrorMessage(QObject::tr("Publish to Roblox"), QObject::tr("Feature not available in offline mode.\nPlease check internet connection and restart Roblox.")); return false; } if (RobloxUser::singleton().getUserId() <= 0) { UpdateUIManager::Instance().showErrorMessage(QObject::tr("Log in required"), QObject::tr("You must log in to perform this action!")); if (openStartPage) { UpdateUIManager::Instance().getMainWindow().openStartPage(true, "showlogin=True"); UpdateUIManager::Instance().getMainWindow().setFocus(); } return false; } return true; } int translateKeyModifiers(Qt::KeyboardModifiers state, const QString &text) { // Convert modifier combos to a single integer. int result = 0; if ((state & Qt::ShiftModifier) && (text.size() == 0 || !text.at(0).isPrint() || text.at(0).isLetter() || text.at(0).isSpace())) result |= Qt::SHIFT; if (state & Qt::ControlModifier) result |= Qt::CTRL; if (state & Qt::MetaModifier) result |= Qt::META; if (state & Qt::AltModifier) result |= Qt::ALT; return result; } }