/** * RobloxIDEDoc.cpp * Copyright (c) 2013 ROBLOX Corp. All Rights Reserved. */ #include "stdafx.h" #include "RobloxIDEDoc.h" // Qt Headers #include #include #include #include #include #include #include #include #include #include // 3rd Party Headers #include "boost/iostreams/copy.hpp" // Roblox Headers #include "v8world/World.h" #include "v8datamodel/PartOperation.h" #include "v8datamodel/Game.h" #include "v8datamodel/DataModel.h" #include "v8datamodel/Workspace.h" #include "v8datamodel/CommonVerbs.h" #include "v8datamodel/ContentProvider.h" #include "v8datamodel/PluginManager.h" #include "v8datamodel/GuiService.h" #include "v8datamodel/UserInputService.h" #include "v8datamodel/GameBasicSettings.h" #include "v8datamodel/Lighting.h" #include "v8datamodel/Light.h" #include "V8DataModel/NonReplicatedCSGDictionaryService.h" #include "v8datamodel/ServerScriptService.h" #include "v8datamodel/StarterPlayerService.h" #include "v8datamodel/ServerStorage.h" #include "v8datamodel/ReplicatedStorage.h" #include "v8datamodel/Visit.h" #include "v8xml/Serializer.h" #include "v8xml/SerializerBinary.h" #include "rbx/BaldPtr.h" #include "AppDraw/Draw.h" #include "script/ScriptContext.h" #include "script/LuaMemory.h" #include "script/DebuggerManager.h" #include "script/ModuleScript.h" #include "script/Script.h" #include "network/Players.h" #include "util/ProtectedString.h" #include "util/Statistics.h" #include "util/Profiling.h" #include "util/MD5Hasher.h" #include "Util/RobloxGoogleAnalytics.h" #include "util/ScopedAssign.h" #include "Util/LuaWebService.h" #include "Util/Hash.h" #include "GfxBase/FrameRateManager.h" #include "GfxBase/ViewBase.h" #include "GfxBase/IAdornableCollector.h" #include "RenderSettingsItem.h" #include "Tool/ToolsArrow.h" #include "V8Xml/WebParser.h" #include "v8datamodel/BasicPartInstance.h" #include "v8datamodel/HttpRbxApiService.h" #include "v8datamodel/MegaCluster.h" #include "StringConv.h" #include "script/ScriptAnalyzer.h" // Roblox Studio Headers #include "RobloxView.h" #include "RobloxTreeWidget.h" #include "ogrewidget.h" #include "RobloxMainWindow.h" #include "RobloxStudioVerbs.h" #include "RobloxPropertyWidget.h" #include "RobloxSettings.h" #include "StudioUtilities.h" #include "RobloxDiagnosticsView.h" #include "RobloxGameExplorer.h" #include "RobloxScriptReview.h" #include "CommonInsertWidget.h" #include "RobloxToolBox.h" #include "RobloxTutorials.h" #include "RobloxDocManager.h" #include "WebDialog.h" #include "IExternalHandler.h" #include "RobloxCustomWidgets.h" #include "QtUtilities.h" #include "RobloxScriptDoc.h" #include "UpdateUIManager.h" #include "AuthoringSettings.h" #include "StudioSerializerHelper.h" #include "InsertServiceDialog.h" #include "DebuggerClient.h" #include "RobloxPluginHost.h" #include "PluginAction.h" #include "RobloxUser.h" #include "DebuggerWidgets.h" #include "RobloxApplicationManager.h" #include "ScriptAnalysisWidget.h" #include "StudioDeviceEmulator.h" #include "KeepSavedChangesDialog.h" #include "AuthenticationHelper.h" #include "RobloxTextOutputWidget.h" #include "RobloxChatWidget.h" #include "PlayersDataManager.h" #include "RobloxTeamCreateWidget.h" #include "RobloxServicesTools.h" using namespace RBX; using namespace boost; LOGGROUP(AutoSave) FASTFLAG(LuaDebugger) FASTFLAG(StudioSeparateActionByActivationMethod) FASTFLAG(StudioShowTutorialsByDefault) FASTFLAG(StudioEmbeddedFindDialogEnabled) FASTFLAG(StudioCustomStatsEnabled) FASTFLAG(UserBetterInertialScrolling) FASTFLAG(StudioEnableGameAnimationsTab) DYNAMIC_FASTFLAG(ScriptExecutionContextApi) FASTFLAG(MaterialPropertiesEnabled) FASTFLAGVARIABLE(AnalyzerGroupUIEnabled, false) FASTFLAGVARIABLE(PrefetchResourcesEnabled, false) FASTFLAGVARIABLE(SearchToolboxByDataModelSearchString, false) FASTFLAGVARIABLE(EnablePhysicalPropertiesByDefault, false) FASTFLAGVARIABLE(PGSSolverDefaultOnNewPlaces, false) FASTFLAGVARIABLE(NoPromptOnAlreadySavedPlace, true) FASTFLAG(UseBuildGenericGameUrl) FASTFLAG(CSGPhysicsLevelOfDetailEnabled) DYNAMIC_FASTSTRINGVARIABLE(WorkspaceMessageText, std::string("Test Me - Please Remove")) DYNAMIC_FASTSTRINGVARIABLE(WorkspaceMessageLink, std::string("http://www.roblox.com")) DYNAMIC_FASTFLAGVARIABLE(WorkspaceNotificationMasterEnable, false) // Level 1 = Show it to people with DEFAULT Physical Properties settings // Level 2 = Show it to people with LEGACY and DEFAULT Physical properties settings DYNAMIC_FASTINTVARIABLE(StudioWorkspaceNotificationLevel, 0) FASTFLAGVARIABLE(CLI10590_FixConversionDialog, true) FASTFLAGVARIABLE(RetryWhenCloudEditEnabledEndpointFails, false) DYNAMIC_FASTFLAG(UseR15Character) Q_DECLARE_METATYPE(boost::function); Q_DECLARE_METATYPE(boost::function); static const char* DraggerGridModeSetting = "DraggerGridMode"; static const char* GridSizeSetting = "GridSize"; static const char* StatsVerb = "Stats"; static const char* RenderStatsVerb = "RenderStats"; static const char* PhysicsStatsVerb = "PhysicsStats"; static const char* NetworkStatsVerb = "NetworkStats"; static const char* SummaryStatsVerb = "SummaryStats"; static const char* CustomStatsVerb = "CustomStats"; static const char* kMaterialPropertiesConvertDialogDisabled = "MaterialPropertiesConvertDialogDisabled"; static const char* kMaterialPropertiesConvertByDefault = "MaterialPropertiesConvertByDefault"; static const char* PlaySoloDebugFileExt = "_NPS"; // extension to be added to rdbg file for New Play Solo mode static const char* kPublishedPlacePrefix = "Published as "; static const char* kCloudEditPlacePrefix = "Team Create: "; static const char* kNewPlaceWindowTitle = "Untitled"; #define RBX_MANUAL_JOINT_CREATION_KEY "RbxManualJointCreationMode" int RobloxIDEDoc::sIDEDocCount = 1; RobloxIDEDoc::RobloxIDEDoc(RobloxMainWindow* pMainWindow) : m_WrapperWidget(NULL) , m_pQOgreWidget(NULL) , m_pDiagViewWidget(NULL) , m_pMainWindow(pMainWindow) , m_pFileWatcher(NULL) , m_fileName("") , m_displayName(QString("Place%1").arg(sIDEDocCount)) , m_bIsLocalDocument(true) , m_initializationScript("") , m_IsAutoSaveLoad(false) , m_TestModeEnabled(true) , m_ReopenLastSavedPlace(true) , m_IOMutex(QMutex::NonRecursive) , m_RequiresSave(false) , m_CurrentGame(&m_EditGame) , m_initialDataModelHash("") , m_AnnouncementWidget(NULL) { #ifdef _WIN32 m_pRecordToggle = NULL; #endif InitializeDefaultsFromSettings(); } RobloxIDEDoc::~RobloxIDEDoc() { clearPlayModifiedScriptList(); } void RobloxIDEDoc::applyScriptChanges() { // Get all scripts and apply changes const tScriptDocs& openScriptDocs = RobloxDocManager::Instance().getOpenScriptDocs(); tScriptDocs::const_iterator iter = openScriptDocs.begin(); for (; iter != openScriptDocs.end() ; ++iter) (*iter)->applyEditChanges(); } void RobloxIDEDoc::InitializeDefaultsFromSettings() { RobloxSettings settings; // Set our default camera speeds RBX::Camera::CameraKeyMoveFactor = AuthoringSettings::singleton().getCameraKeyMoveSpeed(); RBX::Camera::CameraShiftKeyMoveFactor = AuthoringSettings::singleton().getCameraShiftMoveSpeed(); RBX::Camera::CameraMouseWheelMoveFactor = AuthoringSettings::singleton().getCameraMouseWheelMoveSpeed(); // Turn on or off the dragger grid RBX::ArrowToolBase::showDraggerGrid = AuthoringSettings::singleton().getShowDraggerGrid(); RBX::Draw::showHoverOver(AuthoringSettings::singleton().getShowHoverOver()); } // services that don't necessarily exist in a game, but a user may want to put into their game, so we show them in studio void RobloxIDEDoc::createOptionalDMServices() { boost::shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); dataModel->create()->setAssetFetchMediator( &UpdateUIManager::Instance().getViewWidget(eDW_GAME_EXPLORER)); RBX::ServiceProvider::create(dataModel.get()); RBX::ServiceProvider::create(dataModel.get()); RBX::ServiceProvider::create(dataModel.get()); if (m_CurrentGame->m_Game != m_PlayGame.m_Game && !StudioUtilities::isAvatarMode() && !StudioUtilities::isTestMode()) dataModel->setIsStudio(true); dataModel->create()->setCheckApiAccessBecauseInStudio(); dataModel->create()->setNonSuperSafeChatForAllPlayersEnabled(true); if (AuthoringSettings::singleton().onlyPlayFocusWindowAudio) { setMuteAllSounds(!QApplication::activeWindow()); if (m_pMainWindow) { if (QAction* audioToggleWidget = m_pMainWindow->findChild("audioToggleAction")) { audioToggleWidget->setChecked(!QApplication::activeWindow()); } } } dataModel->getWorkspace()->getAdornableCollector()->onRenderableDescendantAdded( &m_CurrentGame->m_SelectionHighlightAdornable); } void RobloxIDEDoc::displayWorkspaceMessage() { // Physical Properties Specific display of dialog. if (DFInt::StudioWorkspaceNotificationLevel && DFFlag::MaterialPropertiesEnabled ) { bool messageEnabled = false; RBX::PhysicalPropertiesMode currentMode = this->getDataModel()->getWorkspace()->getPhysicalPropertiesMode(); switch (DFInt::StudioWorkspaceNotificationLevel) { case 1: messageEnabled = currentMode == RBX::PhysicalPropertiesMode_Default; break; case 2: messageEnabled = currentMode <= RBX::PhysicalPropertiesMode_Default; break; default: break; } if ( messageEnabled) { // Should only display if Roblox user hasn't set his play to Legacy Mode // or hasn't upgraded to new mode. m_AnnouncementWidget->showText(); } else { m_AnnouncementWidget->hideText(); } } if (DFFlag::WorkspaceNotificationMasterEnable) { m_AnnouncementWidget->showText(); } } void RobloxIDEDoc::initializeNewPlace() { // disable studio mode. studio mode wants to do some fancy camera focus stuff // that causes a crash because we haven't initialized the network client yet bool oldStudioMode = RBX::GameBasicSettings::singleton().inStudioMode(); RBX::GameBasicSettings::singleton().setStudioMode(false); // create instance for base plate boost::shared_ptr instance = RBX::Creatable::create(); RBXASSERT(instance); // set up part instance RBX::BaldPtr partInstance = static_cast(instance.get()); partInstance->setName("BasePlate"); if (shared_ptr basicPartInstance = RBX::Instance::fastSharedDynamicCast(instance)) basicPartInstance->setFormFactorXml(RBX::PartInstance::SYMETRIC); partInstance->setPartSizeUi(RBX::Vector3(512, 20, 512)); partInstance->setTranslationUi(RBX::Vector3(0,-10,0)); partInstance->setAnchored(true); partInstance->setPartLocked(true); partInstance->setColor(RBX::BrickColor::brickDarkGray()); shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Write); if (DFFlag::MaterialPropertiesEnabled && FFlag::EnablePhysicalPropertiesByDefault) { // All new places will have Physical Material Properties enabled dataModel->getWorkspace()->setPhysicalPropertiesMode(PhysicalPropertiesMode_NewPartProperties); } if (DFFlag::MaterialPropertiesEnabled) { RBXASSERT(!m_workspacePropertyChangedConnection.connected()); m_workspacePropertyChangedConnection = getEditDataModel()->getWorkspace()->propertyChangedSignal.connect(boost::bind(&RobloxIDEDoc::onWorkspacePropertyChanged, this, _1)); } RBX::Instances instances; instances.push_back(instance); dataModel->getWorkspace()->insertInstances( instances, NULL, RBX::INSERT_TO_TREE, RBX::SUPPRESS_PROMPTS); RBX::GameBasicSettings::singleton().setStudioMode(oldStudioMode); // zoom to a good spot RBX::Vector3 boxSize(15,15,15); RBX::Extents extents(-boxSize,boxSize); RBX::Camera* camera = dataModel->getWorkspace()->getCamera(); camera->zoomExtents(extents,RBX::Camera::ZOOM_IN_OR_OUT); if(!FFlag::UserBetterInertialScrolling) { // adjust focus - zoom extents sets us in orbit cam RBX::CoordinateFrame frame = camera->getCameraCoordinateFrame(); RBX::Vector3 look = frame.lookVector(); camera->setCameraFocus(frame.translation + look * 2.0f); } // merge baseplate with change history baseline so we don't get an undo RBX::ChangeHistoryService* chs = dataModel->create(); chs->resetBaseWaypoint(); // In addition to setting the replicated flag "expSolverEnabled_Replicate", // this also sets "experimentalSolverEnabled" (serialized flag) to true if (FFlag::PGSSolverDefaultOnNewPlaces) dataModel->getWorkspace()->setExpSolverEnabled_Replicate(true); if (FFlag::CSGPhysicsLevelOfDetailEnabled) { RBXASSERT(!m_dataModelItemChangedConnection.connected()); m_dataModelItemChangedConnection = getEditDataModel()->itemChangedSignal.connect(boost::bind(&RobloxIDEDoc::onDMItemChanged, this, _1, _2)); } } void RobloxIDEDoc::initializeDataModeHash() { if (m_initialDataModelHash.isEmpty() && getDataModel()) { RBX::DataModel::LegacyLock lock(getDataModel(), RBX::DataModelJob::Read); m_initialDataModelHash = StudioSerializerHelper::getDataModelHash(getDataModel()); } } void RobloxIDEDoc::initializeRobloxView() { try { RBX::DataModel::LegacyLock lock(m_CurrentGame->m_Game->getDataModel(), RBX::DataModelJob::Write); m_GameView = RobloxView::createGameView(m_pQOgreWidget); m_CurrentGame->m_View = new RobloxView(m_pQOgreWidget,m_CurrentGame->m_Game, m_GameView); m_CurrentGame->m_View->buildGui(StudioUtilities::isAvatarMode() || isCloudEditSession()); // view has been created, initialize video recording initializeVideoRecording(*m_CurrentGame); UpdateUIManager::Instance().updateAction(*m_pMainWindow->toggleVideoRecordAction); } catch(CreatePopupException &e) { RBX::Log::current()->writeEntry(RBX::Log::Information,RBX::format("\tPopup Exception : %s",qPrintable(e.m_message)).c_str()); QtUtilities::RBXMessageBox msgBox; msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setDefaultButton(QMessageBox::Ok); msgBox.setText(e.m_message); msgBox.exec(); } catch (std::exception& e) { RBX::Log::current()->writeEntry(RBX::Log::Information,RBX::format("\tException : %s",e.what()).c_str()); RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, e.what()); } m_pMainWindow->onIDEDocViewInitialized(); } bool RobloxIDEDoc::open(RobloxMainWindow *pMainWindow, const QString &fileName) { m_pMainWindow = pMainWindow; return openFile(fileName, false); } bool RobloxIDEDoc::openFile(const QString& fileName, bool asNew) { if (!fileName.isEmpty()) { QByteArray ba = fileName.toUtf8(); std::string fileNameUtf8(ba.constData(), ba.size()); std::ifstream stream(RBX::utf8_decode(fileNameUtf8).c_str(), std::ios_base::in | std::ios_base::binary); if (!stream.is_open()) return false; return openStream(fileName, &stream, asNew); } return openStream(fileName, NULL, asNew); } static void doCloudEditFetch(const QString& scriptArg, int placeId, shared_ptr config, bool* abort, QDialog* dialog) { StudioUtilities::reportCloudEditJoinEvent("Hitting PlaceLauncher"); std::string url; if (FFlag::UseBuildGenericGameUrl) { url = BuildGenericGameUrl(RobloxSettings::getBaseURL().replace("http","https").toStdString(), QString("game/PlaceLauncher.ashx?request=CloudEdit&placeId=%1").arg(placeId).toStdString()); } else { url = (RobloxSettings::getBaseURL() .replace("http", "https") + QString("/game/PlaceLauncher.ashx?request=CloudEdit&placeId=%1").arg(placeId)).toStdString(); } RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Connecting to server..."); while (!*abort) { EntityProperties p; p.setFromJsonFuture(RBX::HttpAsync::get(url)); if (boost::optional status = p.get("status")) { // PlaceLauncher status code 2 == server is ready if (status.get() == 2 && !*abort) { config->setFromValueTable(p.get >("settings").get()); QMetaObject::invokeMethod(dialog, "done", Qt::QueuedConnection, Q_ARG(int, QDialog::Accepted)); break; } } boost::this_thread::sleep(boost::posix_time::milliseconds(500)); } } static boost::optional parseUrlParameter(QString parameterName, QString url) { QRegExp matcher(QString("%1=(\\d+)").arg(parameterName)); matcher.setCaseSensitivity(Qt::CaseInsensitive); if (matcher.indexIn(url) != -1) { return matcher.cap(1).toInt(); } return boost::optional(); } shared_ptr RobloxIDEDoc::cloudEditDetectionAndPlaceLaunch() { boost::optional placeId; if ((RobloxUser::singleton().getUserId() > 0) && StudioUtilities::containsEditScript(m_initializationScript)) { placeId = parseUrlParameter("placeid", m_initializationScript); RBXASSERT(placeId); boost::optional universeId = parseUrlParameter("universeid", m_initializationScript); if (placeId && !universeId) { EntityProperties p; std::string url = ReplaceTopSubdomain(RobloxSettings::getBaseURL().toStdString(), "api") + format("/universes/get-universe-containing-place?placeId=%d", placeId.get()); p.setFromJsonFuture(RBX::HttpAsync::get(url)); if (boost::optional placeUniverseId = p.get("UniverseId")) { universeId = placeUniverseId.get(); } } RBXASSERT(universeId); if (placeId && universeId) { std::string url = ReplaceTopSubdomain(RobloxSettings::getBaseURL().toStdString(), "api") + format("/universes/%d/cloudeditenabled", universeId.get()); EntityProperties p; bool done = false; while (!done) { p.setFromJsonFuture(RBX::HttpAsync::get(url)); if (boost::optional enabled = p.get("enabled")) { done = true; m_IsCloudEditSession = enabled.get(); } else { if (FFlag::RetryWhenCloudEditEnabledEndpointFails) { StudioUtilities::reportCloudEditJoinEvent("Failed universes/cloudeditenabled"); QtUtilities::RBXMessageBox warning; warning.setText("Failed to look up place's edit mode"); warning.addButton("Check Again", QMessageBox::ResetRole); warning.addButton("Edit Alone", QMessageBox::AcceptRole); warning.exec(); QMessageBox::ButtonRole role = warning.buttonRole(warning.clickedButton()); if (role == QMessageBox::AcceptRole) { done = true; m_IsCloudEditSession = false; } } else { done = true; m_IsCloudEditSession = false; } } } } else { StudioUtilities::reportCloudEditJoinEvent("Unable to read universeid/placeid from edit script"); StandardOut::singleton()->print(MESSAGE_ERROR, "Unable to determine place/universe, proceeding with Edit alone"); placeId = 0; universeId = 0; m_IsCloudEditSession = false; } } else { // if it isn't an edit script it can't be a cloud edit session m_IsCloudEditSession = false; } RBXASSERT(m_IsCloudEditSession.is_initialized()); shared_ptr cloudEditJoinConfig(new EntityProperties); if (isCloudEditSession()) { QtUtilities::RBXMessageBox connectingDialog; connectingDialog.setText("Requesting access..."); connectingDialog.setStandardButtons(QMessageBox::Cancel); bool abort = false; boost::thread httpThread(boost::bind(&doCloudEditFetch, boost::ref(m_initializationScript), placeId.get(), cloudEditJoinConfig, &abort, &connectingDialog)); int status = connectingDialog.exec(); if (status != QDialog::Accepted) { m_initializationScript = ""; abort = true; StudioUtilities::reportCloudEditJoinEvent("User Aborted PlaceLauncher"); httpThread.join(); } } return cloudEditJoinConfig; } bool RobloxIDEDoc::openStream(const QString& fileName, std::istream* stream, bool asNew) { RobloxMainWindow *pMainWindow = m_pMainWindow; clearOutputWidget(); shared_ptr cloudEditJoinConfig = cloudEditDetectionAndPlaceLaunch(); // Initialize our plugin host pMainWindow->getPluginHost()->init(); QMutexLocker locker(&m_IOMutex); RBX::Log::current()->writeEntry(RBX::Log::Information,"RobloxIDEDoc::open - start"); bool success = true; UpdateUIManager::Instance().setBusy(true); const static int ProgressStages = 7; RBX::BaldPtr progressBar = UpdateUIManager::Instance().showProgress("Opening Place",0,ProgressStages); bool isRemoteEditSession = false; try { // Stage 0 - initialize m_pMainWindow = pMainWindow; QString baseUrl = RobloxSettings::getBaseURL(); if (!baseUrl.endsWith("/")) { baseUrl = baseUrl + "/"; } #ifdef STUDIO_ADMIN_BUILD if (fileName.indexOf("Join.ashx") != -1) { m_EditGame.m_Game.reset(new RBX::SecurePlayerGame(NULL, baseUrl.toStdString().c_str(), false)); } else { m_EditGame.m_Game.reset(new RBX::UnsecuredStudioGame(NULL, baseUrl.toStdString().c_str(), StudioUtilities::isAvatarMode())); } #else m_EditGame.m_Game.reset(new RBX::UnsecuredStudioGame(NULL,baseUrl.toStdString().c_str(), true)); #endif RBX::Log::current()->writeEntry(RBX::Log::Information,"\tLocking DataModel"); RBX::DataModel::LegacyLock lock(m_EditGame.m_Game->getDataModel(), RBX::DataModelJob::Write); createOptionalDMServices(); // Stage 1 - initialize widgets/graphics/view UpdateUIManager::Instance().updateProgress(progressBar,1); // TODO - when opening up a place from the website, get the real place name for // the display name m_EditGame.m_Game->getDataModel()->setName(displayName().toStdString()); m_WrapperWidget = new QWidget; //hack to display Ogre window in a tab QWidget *pDocWidgetToAdd = NULL; RBX::CRenderSettings::GraphicsMode graphicsMode = CRenderSettingsItem::singleton().getLatchedGraphicsMode(); if (graphicsMode != RBX::CRenderSettings::NoGraphics) { RBX::Log::current()->writeEntry(RBX::Log::Information,"\tInitializing Ogre"); m_pQOgreWidget = new QOgreWidget("QOgreWidget", m_WrapperWidget); if (m_pMainWindow->getBuildMode() == BM_BASIC) m_WrapperWidget->setMinimumSize(QSize(800, 600)); else m_WrapperWidget->setMinimumSize(m_pQOgreWidget->minimumSize()); #ifdef _WIN32 //if the application is minimized then make sure m_WrapperWidget is shown (fix for DE5666) if (m_pMainWindow->windowState() & Qt::WindowMinimized) { m_WrapperWidget->move(m_pMainWindow->width(), m_pMainWindow->height()); m_WrapperWidget->setVisible(true); } #endif RBX::Log::current()->writeEntry(RBX::Log::Information,"\tInitializing View"); pDocWidgetToAdd = m_pQOgreWidget; } else { m_pDiagViewWidget = new RobloxDiagnosticsView(true); RBX::Profiling::setEnabled(true); pDocWidgetToAdd = m_pDiagViewWidget; } // Stage 2 - initialize controls, layout if (DFInt::StudioWorkspaceNotificationLevel || DFFlag::WorkspaceNotificationMasterEnable) { UpdateUIManager::Instance().updateProgress(progressBar,2); m_AnnouncementWidget = new WorkspaceAnnouncementTooltip(m_WrapperWidget); QVBoxLayout *renderWindowLayout = new QVBoxLayout(m_WrapperWidget); renderWindowLayout->setSpacing(0); renderWindowLayout->setContentsMargins(0,0,0,0); renderWindowLayout->addSpacerItem(new QSpacerItem(0,0,QSizePolicy::Maximum,QSizePolicy::Maximum)); renderWindowLayout->addWidget(m_AnnouncementWidget); renderWindowLayout->addSpacerItem(new QSpacerItem(0,0,QSizePolicy::Maximum,QSizePolicy::Maximum)); renderWindowLayout->addWidget(pDocWidgetToAdd); m_WrapperWidget->setLayout(renderWindowLayout); } else { UpdateUIManager::Instance().updateProgress(progressBar,2); QHBoxLayout *renderWindowLayout = new QHBoxLayout(m_WrapperWidget); renderWindowLayout->setSpacing(0); renderWindowLayout->setContentsMargins(0,0,0,0); renderWindowLayout->addSpacerItem(new QSpacerItem(0,0,QSizePolicy::Maximum,QSizePolicy::Maximum)); renderWindowLayout->addWidget(pDocWidgetToAdd); renderWindowLayout->addSpacerItem(new QSpacerItem(0,0,QSizePolicy::Maximum,QSizePolicy::Maximum)); m_WrapperWidget->setLayout(renderWindowLayout); } if (StudioUtilities::isAvatarMode()) StudioDeviceEmulator::Instance().activateCurrentDevice(); createActionIDVerbMap(m_EditGame); RBX::Log::current()->writeEntry(RBX::Log::Information,"\tInitializing Explorer"); m_EditGame.m_TreeWidget = new RobloxTreeWidget(m_EditGame.m_Game->getDataModel()); UpdateUIManager::Instance().getViewWidget(eDW_OBJECT_EXPLORER).setCurrentWidget(m_EditGame.m_TreeWidget); // Stage 3 - load file UpdateUIManager::Instance().updateProgress(progressBar,3); m_RequiresSave = false; RBX::Log::current()->writeEntry(RBX::Log::Information,RBX::format("\tLoading File : %s",qPrintable(fileName)).c_str()); if (m_initializationScript.isEmpty()) { QMetaObject::invokeMethod(&UpdateUIManager::Instance().getViewWidget(eDW_GAME_EXPLORER), "nonGameLoaded", Qt::QueuedConnection); } if (fileName.isEmpty() && m_initializationScript.isEmpty()) { //init debugger data initializeDebuggerData(fileName, m_EditGame.m_Game->getDataModel()); sIDEDocCount++; } else { if (!asNew) m_fileName = fileName; loadFromStream(stream); if (!m_initializationScript.isEmpty()) { sIDEDocCount++; m_bIsLocalDocument = false; if (m_initializationScript.contains("testmode=false")) m_TestModeEnabled = false; // make sure we are authenticated before loading initialization scripts AuthenticationHelper::Instance().waitForHttpAuthentication(); if (isCloudEditSession()) { // Reset synchronized flags, they should be set by the server FLog::ResetSynchronizedVariablesState(); boost::thread(boost::bind(&StudioUtilities::joinCloudEditSession, m_EditGame.m_Game, cloudEditJoinConfig)); initCloudEditObjects(); } else if (RBX::ContentProvider::isUrl(m_initializationScript.toStdString())) { if (StudioUtilities::containsEditScript(m_initializationScript)) { // if in EDIT mode, connect with workspaceLoadedSignal, so we can initialize debugger data m_contentDataLoadedConnection = m_EditGame.m_Game->getDataModel()->workspaceLoadedSignal.connect(boost::bind( &RobloxIDEDoc::onContentDataLoaded, this)); isRemoteEditSession = true; } if (StudioUtilities::containsJoinScript(m_initializationScript)) { // enable FRM in play mode CRenderSettingsItem::singleton().setEnableFRM(true); // Reset synchronized flags, they should be set by the server FLog::ResetSynchronizedVariablesState(); if (StudioUtilities::isTestMode()) { if (RBX::Network::Players* players = RBX::ServiceProvider::create(getDataModel())) { m_localPlayerConnection = players->onDemandWrite()->childAddedSignal.connect( boost::bind(&RobloxIDEDoc::onLocalPlayerAdded, this, _1) ); } } boost::thread(boost::bind( &StudioUtilities::executeURLJoinScript, m_EditGame.m_Game, m_initializationScript.toStdString())); } else { boost::thread(boost::bind( &StudioUtilities::executeURLScript, m_EditGame.m_Game->getDataModel(), m_initializationScript.toStdString())); } } else { handleScriptCommand(m_initializationScript); } } } CSGDictionaryService* dictionaryService = ServiceProvider::create(getDataModel()); NonReplicatedCSGDictionaryService* nrDictionaryService = ServiceProvider::create(getDataModel()); dictionaryService->refreshRefCount(); nrDictionaryService->refreshRefCount(); // Stage 4 - initialize terrain/undo UpdateUIManager::Instance().updateProgress(progressBar,4); // enable dock views (must be done before enabling undo/redo) setDockViewsEnabled(); setTeamCreateWidgetEnabled(true); if (m_bIsLocalDocument) { RBX::Log::current()->writeEntry(RBX::Log::Information,"\tInitializing Terrain"); m_EditGame.m_Game->getDataModel()->getWorkspace()->createTerrain(); // initialize PlayerScripts StarterPlayerService* starterPlayerService = ServiceProvider::find(m_EditGame.m_Game->getDataModel()->getWorkspace()); if (starterPlayerService) starterPlayerService->setupPlayerScripts(); RBX::Log::current()->writeEntry(RBX::Log::Information,"\tInitializing Undo"); enableUndo(m_pMainWindow->getBuildMode() == BM_ADVANCED); // enable dynamic lighting for new places if (fileName.isEmpty()) { if (RBX::Lighting* lighting = RBX::ServiceProvider::create(m_EditGame.m_Game->getDataModel().get())) { lighting->setGlobalShadows(true); lighting->setGlobalAmbient(G3D::Color3(0,0,0)); } } // enable smooth terrain for new places if (fileName.isEmpty()) { RBX::MegaClusterInstance* terrain = boost::polymorphic_downcast(m_EditGame.m_Game->getDataModel()->getWorkspace()->getTerrain()); terrain->convertToSmooth(); } } // Stage 5 - load plugins UpdateUIManager::Instance().updateProgress(progressBar,5); // finally load in plugins (we want these in local and online docs) RBX::Log::current()->writeEntry(RBX::Log::Information,"\tLoading Plugins"); RobloxPluginHost::loadPlugins(m_EditGame.m_Game->getDataModel().get()); // Stage 6 - finish initialization UpdateUIManager::Instance().updateProgress(progressBar,6); m_EditGame.m_Game->getDataModel()->setDirty(false); m_RequiresSave = false; m_pRSFHandler.reset(new RecentlySavedFilesHandler()); // restore in-game stats display RobloxSettings settings; setStatsEnabled(StatsVerb,settings.value(StatsVerb,false).toBool(), m_pMainWindow->testStatsAction); setStatsEnabled(RenderStatsVerb,settings.value(RenderStatsVerb,false).toBool(), m_pMainWindow->testRenderStatsAction); setStatsEnabled(PhysicsStatsVerb,settings.value(PhysicsStatsVerb,false).toBool(), m_pMainWindow->testPhysicsStatsAction); setStatsEnabled(NetworkStatsVerb,settings.value(NetworkStatsVerb,false).toBool(), m_pMainWindow->testNetworkStatsAction); setStatsEnabled(SummaryStatsVerb,settings.value(SummaryStatsVerb,false).toBool(), m_pMainWindow->testSummaryStatsAction); if (FFlag::StudioCustomStatsEnabled) { setStatsEnabled(CustomStatsVerb,settings.value(CustomStatsVerb,false).toBool(), m_pMainWindow->testCustomStatsAction); } // for now assume qt docs always have access to keyboard/mouse (we should handle this somewhere else in the future) RBX::UserInputService* userInputService = m_EditGame.m_Game->getDataModel()->find(); if (userInputService != NULL) { userInputService->setMouseEnabled(true); userInputService->setKeyboardEnabled(true); } enableDownloadPlaceCopy(isCloudEditSession()); QDateTime lastDateTimeOpen = settings.value("LastIDEDocOpen").toDateTime(); if (lastDateTimeOpen.isNull()) { UpdateUIManager::Instance().setDockVisibility(eDW_TOOLBOX, true); UpdateUIManager::Instance().setDockVisibility(eDW_TUTORIALS, FFlag::StudioShowTutorialsByDefault); } settings.setValue("LastIDEDocOpen", QDateTime::currentDateTime()); // Fire off pre-fetching if (FFlag::PrefetchResourcesEnabled) { RBX::Log::current()->writeEntry(RBX::Log::Information,"\tPrefetch Resources"); preFetchResources(); } RBXASSERT(!m_bIsLocalDocument || !isRemoteEditSession); if (m_bIsLocalDocument || isRemoteEditSession) { RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_STUDIO, "BeginEdit", m_bIsLocalDocument ? "LocalDocument" : "RemoteBackedDocument"); } if (m_AnnouncementWidget) { displayWorkspaceMessage(); } // Stage 7 - Done! UpdateUIManager::Instance().updateProgress(progressBar,7); RBX::Log::current()->writeEntry(RBX::Log::Information,"\tOpen Success"); } catch(CreatePopupException &e) { RBX::Log::current()->writeEntry(RBX::Log::Information,RBX::format("\tPopup Exception : %s",qPrintable(e.m_message)).c_str()); QtUtilities::RBXMessageBox msgBox; msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setDefaultButton(QMessageBox::Ok); msgBox.setText(e.m_message); msgBox.exec(); success = false; } catch (std::exception& e) { RBX::Log::current()->writeEntry(RBX::Log::Information,RBX::format("\tException : %s",e.what()).c_str()); RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, e.what()); success = false; } UpdateUIManager::Instance().hideProgress(progressBar); UpdateUIManager::Instance().setBusy(false); // Be sure not to do this while studio is busy, or the enabled state will get lost if (m_initializationScript.isEmpty()) { UpdateUIManager::Instance().getViewWidget(eDW_GAME_EXPLORER).nonGameLoaded(); } // make sure that we're not dirty, especially if we get any sort of error // if it's dirty, the user will be prompted to save m_RequiresSave = false; if (m_EditGame.m_Game && m_EditGame.m_Game->getDataModel()) m_EditGame.m_Game->getDataModel()->setDirty(false); if (!m_pFileWatcher && CRenderSettingsItem::singleton().getDebugReloadAssets()) { m_pFileWatcher = new FW::FileWatcher(); // assets m_pFileWatcher->addWatch(ContentProvider::assetFolder(), this, true); // platform dependent assets m_pFileWatcher->addWatch(ContentProvider::platformAssetFolder(), this, true); // and shaders, Oh yeah! std::string shaderFolder = QDir::cleanPath(QString((ContentProvider::assetFolder() + "/../shaders/source").c_str())).toStdString(); m_pFileWatcher->addWatch(shaderFolder, this, false); } RBX::Log::current()->writeEntry(RBX::Log::Information,"RobloxIDEDoc::open - end"); return success; } void RobloxIDEDoc::handleFileAction(FW::WatchID watchid, const FW::String& dir, const FW::String& filename, FW::Action action) { // we dont care about deleted file, we'll keep current data in that case if (action == FW::Actions::Delete) return; std::string simpleFilename = QDir::fromNativeSeparators(QString(filename.c_str())).toStdString(); std::string absoluteDir = QDir::fromNativeSeparators(QString(dir.c_str())).toStdString(); std::string contentFolder = QDir::fromNativeSeparators(QString(ContentProvider::assetFolder().c_str())).toStdString(); std::string platformContentFolder = QDir::fromNativeSeparators(QString(ContentProvider::platformAssetFolder().c_str())).toStdString(); std::string pathStr; if (absoluteDir.find(contentFolder) == 0 || absoluteDir.find(platformContentFolder) == 0) pathStr = "rbxasset://" + simpleFilename; else pathStr = simpleFilename; RBX::ViewBase* pView = (&m_CurrentGame->m_View->getViewBase()); pView->queueAssetReload(pathStr); } void RobloxIDEDoc::exportSelection(std::string filePath, ExporterSaveType exportType) { RBX::ViewBase* pView = (&m_CurrentGame->m_View->getViewBase()); QString fileName = filePath.c_str(); if (fileName.isEmpty()) fileName = QFileDialog::getSaveFileName(&UpdateUIManager::Instance().getMainWindow(), tr("Export Place"), "", "Object Model (*.obj)"); if (!fileName.isEmpty()) { QFileInfo fileInfo(fileName); if (fileInfo.suffix() == "obj") { QString exportAction = "Exporting Place"; if (exportType == ExporterSaveType_Selection) exportAction = "Exporting Selection"; UpdateUIManager::Instance().setBusy(true); RBX::BaldPtr progressBar = UpdateUIManager::Instance().showProgress(exportAction); UpdateUIManager::Instance().updateProgress(progressBar); pView->exportScene(fileInfo.absolutePath().toStdString() + "/" + fileInfo.baseName().toStdString(), exportType, ExporterFormat_Obj); UpdateUIManager::Instance().hideProgress(progressBar); UpdateUIManager::Instance().setBusy(false); } else { QMessageBox::critical( m_pMainWindow, "Export Failure", QString("Obj filetype expected. '%1' received.").arg(fileInfo.suffix())); } } } void RobloxIDEDoc::onDMItemChanged(shared_ptr item, const RBX::Reflection::PropertyDescriptor* descriptor) { if (descriptor == &RBX::PartOperation::prop_CollisionFidelity) { if (PartOperation* partOp = item->fastDynamicCast()) { if (CSGDictionaryService* dictionaryService = ServiceProvider::create(RBX::DataModel::get(partOp))) { if (!RBX::DataModel::get(partOp)->isRunMode()) { partOp->createPhysicsData(partOp->getMesh().get()); } } } } } void RobloxIDEDoc::enableUndo(bool enable) { //undo redo manager. RBX::ChangeHistoryService* chs = m_CurrentGame->m_Game->getDataModel()->create(); chs->setEnabled(true); if (enable) m_CurrentGame->m_ChangeHistoryConnection = chs->waypointChangedSignal.connect(boost::bind(&RobloxIDEDoc::onWaypointChanged,this)); else m_CurrentGame->m_ChangeHistoryConnection.disconnect(); } IRobloxDoc::RBXCloseRequest RobloxIDEDoc::requestClose() { RBX::DataModel::RequestShutdownResult shutdownRequestResult; { RBX::DataModel::LegacyLock lock(m_EditGame.m_Game->getDataModel(), RBX::DataModelJob::Write); // If we're in full screen build mode, call back to LUA for save as opposed to doing QT save dialog shutdownRequestResult = m_EditGame.m_Game->getDataModel()->requestShutdown(m_pMainWindow->getBuildMode() == BM_BASIC); } switch (shutdownRequestResult) { case RBX::DataModel::CLOSE_REQUEST_HANDLED: return IRobloxDoc::REQUEST_HANDLED; case RBX::DataModel::CLOSE_NOT_HANDLED: break; case RBX::DataModel::CLOSE_NO_SAVE_NEEDED: if (!m_TestModeEnabled) return IRobloxDoc::LOCAL_SAVE_NEEDED; return IRobloxDoc::NO_SAVE_NEEDED; case RBX::DataModel::CLOSE_LOCAL_SAVE: return IRobloxDoc::LOCAL_SAVE_NEEDED; } KeepSavedChangesDialog* keepSavedChangesDialog = new KeepSavedChangesDialog(m_pMainWindow); keepSavedChangesDialog->exec(); KeepSavedChangesDialog::DialogResult result = keepSavedChangesDialog->getResult(); delete keepSavedChangesDialog; switch (result) { case KeepSavedChangesDialog::Fail: { QtUtilities::RBXMessageBox msgBox; msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setDefaultButton(QMessageBox::Ok); msgBox.setText("Failed to save place online. Check your connection and try again."); msgBox.exec(); return IRobloxDoc::CLOSE_CANCELED; } case KeepSavedChangesDialog::Discard: case KeepSavedChangesDialog::Save: { return IRobloxDoc::NO_SAVE_NEEDED; } case KeepSavedChangesDialog::Cancel: default: { return IRobloxDoc::CLOSE_CANCELED; } } return IRobloxDoc::NO_SAVE_NEEDED; } bool RobloxIDEDoc::doClose() { MobileDevelopmentDeployer::singleton().disconnectClientsAndShutDown(); //reset debugger data if (FFlag::LuaDebugger) { RBX::Scripting::DebuggerManager::singleton().setDataModel(NULL); DebuggerClientManager::Instance().setDataModel(boost::shared_ptr()); //make sure we remove debugger file if we are in test mode if (StudioUtilities::isTestMode()) QFile::remove(getDebugInfoFile()); m_EditDebuggersMap.clear(); } if (m_pRSFHandler) { shared_ptr dataModel = getEditDataModel(); if (dataModel && dataModel->isDirty()) { QString fileToSave = getRecentSaveFile(); if (!fileToSave.isEmpty() && StudioSerializerHelper::saveAsIfModified(m_initialDataModelHash, dataModel.get(), fileToSave)) m_pRSFHandler->addFile(fileToSave); } } if (m_AnnouncementWidget) { m_AnnouncementWidget->deleteLater(); m_AnnouncementWidget = NULL; } if (m_pQOgreWidget) { m_pQOgreWidget->setRobloxView(NULL); m_pQOgreWidget->deleteLater(); m_pQOgreWidget = NULL; } if (m_pDiagViewWidget) { m_pDiagViewWidget->setDataModel(boost::shared_ptr()); m_pDiagViewWidget->deleteLater(); m_pDiagViewWidget = NULL; } if (m_WrapperWidget) { m_WrapperWidget->deleteLater(); m_WrapperWidget = NULL; } setTeamCreateWidgetEnabled(false); cleanupGameState(m_PlayGame); cleanupGameState(m_EditGame); UpdateUIManager::Instance().getViewWidget(eDW_OBJECT_EXPLORER).setCurrentWidget(NULL); // only delete autosave if we have an autosave and we did not load from an autosave // if we loaded from an autosave, we're going to keep using the same autosave but we're // not going to blow it away on close. FASTLOGS(FLog::AutoSave, "Autosave name: %s", m_AutoSaveName.toStdString()); FASTLOG1(FLog::AutoSave, "Is auto save loaded: %u", m_IsAutoSaveLoad); if (!m_AutoSaveName.isEmpty() && !m_IsAutoSaveLoad) { FASTLOG(FLog::AutoSave, "Removing auto save on successful exit"); QFile::remove(m_AutoSaveName); //also remove the auto save debug file if (FFlag::LuaDebugger) QFile::remove(getDebugInfoFile(true)); m_AutoSaveName.clear(); m_IsAutoSaveLoad = false; } enableDownloadPlaceCopy(false); UpdateUIManager::Instance().getViewWidget(eDW_GAME_EXPLORER).onCloseIdeDoc(); if (m_pFileWatcher) { delete m_pFileWatcher; m_pFileWatcher = NULL; } //self deletion (maybe we must use reference counting!) delete this; return true; } void RobloxIDEDoc::cleanupGameState(GameState& gameState) { m_CurrentGame = &gameState; gameState.m_ChangeHistoryConnection.disconnect(); if (gameState.m_Game && gameState.m_Game->getDataModel()) { // cleanup all dock views setDockViewsEnabled(false); // clean up verbs shared_ptr dataModel = gameState.m_Game->getDataModel(); RBX::DataModel::LegacyLock lock(dataModel,RBX::DataModelJob::Write); QVector::iterator iter = gameState.m_Verbs.begin(); while (iter != gameState.m_Verbs.end()) { delete *iter; ++iter; } gameState.m_Verbs.clear(); gameState.m_UndoVerb = NULL; gameState.m_RedoVerb = NULL; #ifdef _WIN32 gameState.m_RecordToggleVerb = NULL; #endif dataModel->getWorkspace()->getAdornableCollector()->onRenderableDescendantRemoving(&gameState.m_SelectionHighlightAdornable); } // remove tree view if (gameState.m_TreeWidget) { m_pMainWindow->treeWidgetStack().removeWidget(gameState.m_TreeWidget); gameState.m_TreeWidget->requestDelete(); gameState.m_TreeWidget = NULL; UpdateUIManager::Instance().getViewWidget(eDW_OBJECT_EXPLORER).setCurrentWidget(NULL); } // shut down UI and plugins if (gameState.m_Game && gameState.m_Game->getDataModel()) { shared_ptr dataModel = gameState.m_Game->getDataModel(); { RBX::DataModel::LegacyLock lock(dataModel,RBX::DataModelJob::Write); if (gameState.m_View) { delete gameState.m_View; gameState.m_View = NULL; } } gameState.m_Game->shutdown(); // Clean up plugins after the datamodel has closed (closing the datamodel ensures that // no more plugin scripts are running) RBX::PluginManager::singleton()->deletePlugins(dataModel.get()); gameState.m_Game.reset(); } } bool RobloxIDEDoc::loadFromStream(std::istream* stream) { if (!stream) return false; RBX::Security::Impersonator impersonate(RBX::Security::COM); shared_ptr dm = m_EditGame.m_Game->getDataModel(); RBX::DataModel::LegacyLock lock(dm, RBX::DataModelJob::Write); Serializer serializer; serializer.load(*stream, dm.get()); // For live games, the gameserver script sets the place id and universe id // before calling game:Load(placeid). loadFromStream() is taking the place of // game:Load(), and game loading needs to happen before gameserver.ashx finishes, // so as a workaround we use this attempt at url parsing to pull the place and // universe id out of the gameserver script request so that they are set in the // data model before we attempt load linked scripts. QRegExp universeIdInGameServer(".*gameserver\\.ashx.*\\((\\d+).*(\\d+)\\).*"); universeIdInGameServer.setCaseSensitivity(Qt::CaseInsensitive); if (universeIdInGameServer.indexIn(m_initializationScript) != -1) { int placeId = universeIdInGameServer.cap(1).toInt(); int universeId = universeIdInGameServer.cap(2).toInt(); dm->setPlaceID(placeId, false); dm->setUniverseId(universeId); } LuaSourceContainer::blockingLoadLinkedScripts(dm->create(), dm.get()); //init debugger data (in local document or play solo mode) if (m_initializationScript.isEmpty() || StudioUtilities::containsVisitScript(m_initializationScript)) initializeDebuggerData(getDebugInfoFile(), dm); dm->processAfterLoad(); if (DFFlag::MaterialPropertiesEnabled) { RBXASSERT(!m_workspacePropertyChangedConnection.connected()); m_workspacePropertyChangedConnection = getEditDataModel()->getWorkspace()->propertyChangedSignal.connect(boost::bind(&RobloxIDEDoc::onWorkspacePropertyChanged, this, _1)); } //Migrating CSG childData CSGDictionaryService* dictionaryService = dm->create(); ChangeHistoryService* changeHistory = dm->create(); if (FFlag::CSGPhysicsLevelOfDetailEnabled) { RBXASSERT(!m_dataModelItemChangedConnection.connected()); m_dataModelItemChangedConnection = getEditDataModel()->itemChangedSignal.connect(boost::bind(&RobloxIDEDoc::onDMItemChanged, this, _1, _2)); } dictionaryService->reparentAllChildData(); changeHistory->resetBaseWaypoint(); searchToolboxByDefaultSearchString(); return true; } void RobloxIDEDoc::preFetchResources() { RBX::Verb* verb = getVerb(m_EditGame,"publishToRobloxAsAction"); if(verb) { PublishToRobloxAsVerb* publishVerb = static_cast(verb); publishVerb->initDialog(); } } QString RobloxIDEDoc::displayName() const { if (m_fileName.isEmpty()) return m_displayName; if (StudioUtilities::isTestMode() && !StudioUtilities::isAvatarMode() && StudioUtilities::containsGameServerScript(m_initializationScript)) return "Server"; return QFileInfo(m_fileName).fileName(); } QString RobloxIDEDoc::windowTitle() const { if (m_windowTitlePrefix.isEmpty() && m_initializationScript.isEmpty()) { // local file if (!m_fileName.isEmpty()) return QDir::toNativeSeparators(m_fileName); // new file if (m_initializationScript.isEmpty()) return kNewPlaceWindowTitle; } QString title = displayName(); title.prepend(m_windowTitlePrefix); // remove appended time stamp if any from the title int index = title.lastIndexOf(" @ "); if (index > 0) title = title.left(index); return title; } QWidget* RobloxIDEDoc::getViewer() { return m_WrapperWidget; } /** * Checks if the data model is dirty or if we require a save. * The data model might not be dirty if we auto-saved. */ bool RobloxIDEDoc::isModified() { applyScriptChanges(); if (StudioUtilities::isTestMode()) return false; else if (m_RequiresSave) return true; // do not show save as dialog for cloud edit if (isCloudEditSession()) return false; shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); if (!dataModel) { RBXASSERT(false); return false; } RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Read); return dataModel->isDirty() && m_initialDataModelHash != StudioSerializerHelper::getDataModelHash(dataModel.get()); } /** * Checks if the game is currently stopped. * Paused is still considered "running". */ bool RobloxIDEDoc::isSimulating() { if (!m_PlayGame.m_Game || !m_PlayGame.m_Game->getDataModel() || !m_PlayGame.m_Game->getDataModel()->create()) return false; RBX::RunState runState = m_PlayGame.m_Game->getDataModel()->create()->getRunState(); return runState != RBX::RS_STOPPED; } bool RobloxIDEDoc::save() { bool result = saveAs(m_fileName); m_EditGame.m_Game->getDataModel()->setDirty(!result); return result; } void RobloxIDEDoc::createScriptMapping(shared_ptr instance) { if (RBX::Instance::fastSharedDynamicCast(instance) || RBX::Instance::fastSharedDynamicCast(instance)) { addScriptMapping(instance, m_PlayGame.m_Game->getDataModel()); } } void RobloxIDEDoc::restoreEditModelScripts() { RBX::DataModel::LegacyLock lock(m_EditGame.m_Game->getDataModel(), RBX::DataModelJob::Write); // Copy the open script docs list because each script doc will have its script // reset, and resetting the script changes the master script doc list (mutating the list // while iterating through it). const tScriptDocs openScriptDocs = RobloxDocManager::Instance().getOpenScriptDocs(); for (tScriptDocs::const_iterator iter = openScriptDocs.begin(); iter != openScriptDocs.end() ; ++iter) { shared_ptr playInstance = (*iter)->getCurrentScript().toInstance(); if (playInstance == NULL) { continue; } shared_ptr instance = getEditScriptByPlayInstance(playInstance.get()); if (instance && instance->isDescendantOf(m_EditGame.m_Game->getDataModel().get())) { LuaSourceBuffer buffer = LuaSourceBuffer::fromInstance(instance); (*iter)->setScript(m_EditGame.m_Game->getDataModel(), buffer); } else { // use RobloxDocManager::removeDoc for proper cleanup of documents if ((*iter) == RobloxDocManager::Instance().getCurrentDoc()) { // if script doc is current then make sure we ignore IDE doc activate while removing script doc // IDE doc will be activated in RobloxIDEDoc::closePlayDataModel RBX::ScopedAssign ignoreDocActivate(m_bActive, true); RobloxDocManager::Instance().removeDoc(**iter); } else { RobloxDocManager::Instance().removeDoc(**iter); } } } clearScriptMap(); } void RobloxIDEDoc::patchPlayModelScripts() { // Build a script mapping for each script in the Edit Data Model. RBX::DataModel* dm = m_EditGame.m_Game->getDataModel().get(); dm->visitDescendants(boost::bind(&RobloxIDEDoc::createScriptMapping, this, _1)); // Replace Edit scripts with Play scripts. // Copy the open script docs list because each script doc will have its script // reset, and resetting the script changes the master script doc list (mutating the list // while iterating through it). const tScriptDocs openScriptDocs = RobloxDocManager::Instance().getOpenScriptDocs(); for (tScriptDocs::const_iterator iter = openScriptDocs.begin(); iter != openScriptDocs.end() ; ++iter) { shared_ptr backingInstance = (*iter)->getCurrentScript().toInstance(); if (backingInstance == NULL) { continue; } std::vector indexHierarchy; std::string serviceName = getScriptIndexHierarchy(backingInstance, indexHierarchy); shared_ptr instance = getScriptByIndexHierarchy(m_PlayGame.m_Game->getDataModel(), indexHierarchy, serviceName); if (instance) { LuaSourceBuffer buffer = LuaSourceBuffer::fromInstance(instance); if (!buffer.empty()) (*iter)->setScript(m_PlayGame.m_Game->getDataModel(), buffer); } } } bool RobloxIDEDoc::saveScriptsChangedWhileTesting() { bool saveChanges = true; tPlayModifiedScriptList& playModifiedScriptList = getPlayModifiedScriptList(); if (playModifiedScriptList.size() > 0 && !isCloudEditSession()) { saveChanges = AuthoringSettings::singleton().alwaysSaveScriptChangesWhileRunning; if (!saveChanges) { UpdateUIManager::Instance().setBusy(true, false); QtUtilities::RBXMessageBox msgBox; msgBox.setIcon(QMessageBox::Question); msgBox.setText("You've made script changes while playing. Would you like to keep these changes?"); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::Yes); QPushButton *alwaysButton = msgBox.addButton(tr("Always Save"), QMessageBox::ActionRole); int ret = msgBox.exec(); UpdateUIManager::Instance().setBusy(false, false); if (msgBox.clickedButton() == alwaysButton) { AuthoringSettings::singleton().alwaysSaveScriptChangesWhileRunning = true; RBX::GlobalAdvancedSettings::singleton()->saveState(); RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_COUNTERS, "LiveCoding", "SaveDialogAlways", 1); saveChanges = true; } if (ret == QMessageBox::Yes) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_COUNTERS, "LiveCoding", "SaveDialogYes", 1); saveChanges = true; } } if (saveChanges) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_COUNTERS, "LiveCoding", "Saves", 1); applyScriptChanges(); RBX::DataModel::LegacyLock editLock(m_EditGame.m_Game->getDataModel(), RBX::DataModelJob::Write); for (tPlayModifiedScriptList::iterator iter = playModifiedScriptList.begin(); iter != playModifiedScriptList.end(); ++iter) { shared_ptr instance = getEditScriptByPlayInstance((*iter)->instance.get()); if (!instance) continue; LuaSourceBuffer sourceBuffer = LuaSourceBuffer::fromInstance((*iter)->instance); LuaSourceBuffer destBuffer = LuaSourceBuffer::fromInstance(instance); destBuffer.setScriptText(sourceBuffer.getScriptText()); } } clearPlayModifiedScriptList(); } return saveChanges; } bool RobloxIDEDoc::saveAs(const QString& fileName) { QMutexLocker locker(&m_IOMutex); saveScriptsChangedWhileTesting(); const bool useBinaryFormat = !(fileName.endsWith(".rbxlx", Qt::CaseInsensitive)); const bool isAutoSave = fileName == m_AutoSaveName; if(isAutoSave) { FASTLOGS(FLog::AutoSave, "Saving AutoSave, filename: %s", fileName.toStdString()); } shared_ptr game = m_EditGame.m_Game; QString errorMessage; bool result = StudioSerializerHelper::saveAs( fileName, isAutoSave ? "Auto-Saving" : "Saving", isAutoSave, useBinaryFormat, game->getDataModel().get(), errorMessage, !isAutoSave); if (result) { // modify dirty flag only for local document, we need to force saving in case of Build/Edit (DE4302) if (m_bIsLocalDocument) { if (game->getDataModel()->isDirty()) m_RequiresSave = (fileName == m_AutoSaveName); if (!FFlag::NoPromptOnAlreadySavedPlace) { if (game->getDataModel()->isDirty() && (fileName != m_AutoSaveName)) resetDirty(game->getDataModel().get()); } } if (FFlag::NoPromptOnAlreadySavedPlace) { if (game->getDataModel()->isDirty() && (fileName != m_AutoSaveName)) resetDirty(game->getDataModel().get()); } // if we changed file names and it's not the autosave file name if (fileName != m_fileName && fileName != m_AutoSaveName && !isCloudEditSession()) { FASTLOGS(FLog::AutoSave, "Filename changed, remove auto save. New file: %s", fileName.toStdString()); FASTLOGS(FLog::AutoSave, "Old file: %s", m_fileName.toStdString()); FASTLOGS(FLog::AutoSave, "Old autoSave file: %s", m_AutoSaveName.toStdString()); // if we have an autosave, we can delete it now if (!m_AutoSaveName.isEmpty()) { FASTLOG(FLog::AutoSave, "Removing old autosave"); QFile::remove(m_AutoSaveName); m_AutoSaveName.clear(); // if we loaded from an autosave, we have a real file name now and can delete the autosave at end m_IsAutoSaveLoad = false; } m_fileName = fileName; RobloxDocManager::Instance().setDocTitle(*this,displayName(),titleTooltip(),titleIcon()); } if (isAutoSave) m_pMainWindow->statusBar()->showMessage("Auto-Save Success",3000); else m_pMainWindow->statusBar()->showMessage("Save File Success",5000); if (isAutoSave && m_pRSFHandler) { QString recentSaveFile = getRecentSaveFile(); if (m_fileName == recentSaveFile) m_pRSFHandler->removeFile(recentSaveFile); } return true; } else { QMessageBox::critical( m_pMainWindow, "Save File Failure", errorMessage); m_pMainWindow->statusBar()->showMessage("Save File Failure",5000); return false; } } void RobloxIDEDoc::activate() { if (m_bActive) return; if (m_pQOgreWidget) m_pQOgreWidget->activate(); shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); if (m_pDiagViewWidget) m_pDiagViewWidget->setDataModel(dataModel); if ( !m_pMainWindow->isRibbonStyle() ) // We never hide the plugins on ribbon mode { RBX::PluginManager::singleton()->disableStudioUI(dataModel.get(), false); } RBX::PluginManager::singleton()->setCurrentDataModel(dataModel.get()); // Init Find Replace dialogs UpdateUIManager::Instance().getViewWidget(eDW_OBJECT_EXPLORER).setCurrentWidget(m_CurrentGame->m_TreeWidget); if (dataModel) { RBX::Workspace* workspace = dataModel->getWorkspace(); if (workspace) { workspace->setShowAxisWidget(UpdateUIManager::Instance().get3DAxisEnabled()); workspace->setShow3DGrid(UpdateUIManager::Instance().get3DGridEnabled()); } m_pMainWindow->insertServiceDialog().setDataModel(dataModel); m_pMainWindow->insertServiceAction->setEnabled(m_pMainWindow->insertServiceDialog().isAvailable()); } // load settings QSettings settings; // load the grid mode setting setDraggerGridMode((RBX::DRAG::DraggerGridMode)settings.value(DraggerGridModeSetting, RBX::DRAG::ONE_STUD).toInt()); setGridSize(settings.value(GridSizeSetting, 4).toInt()); //update toobars UpdateUIManager::Instance().updateToolBars(); updateUndoRedo(); m_bActive = true; } void RobloxIDEDoc::deActivate() { if (!m_bActive) return; #ifdef _WIN32 if (m_CurrentGame->m_RecordToggleVerb && m_CurrentGame->m_RecordToggleVerb->isRecording()) m_CurrentGame->m_RecordToggleVerb->stopRecording(false); #endif if (m_pQOgreWidget) m_pQOgreWidget->deActivate(); if (m_pDiagViewWidget) m_pDiagViewWidget->setDataModel(boost::shared_ptr()); if (!m_pMainWindow->isRibbonStyle()) // We never hide the plugins on ribbon mode { RBX::PluginManager::singleton()->disableStudioUI(m_CurrentGame->m_Game->getDataModel().get(),true); } if (FFlag::StudioEmbeddedFindDialogEnabled) { FindReplaceProvider::instance().getEmbeddedFindDialog()->hide(); } else { FindReplaceProvider::instance().getFindDialog()->hide(); FindReplaceProvider::instance().getReplaceDialog()->hide(); } // save settings QSettings settings; settings.setValue(DraggerGridModeSetting,RBX::AdvArrowTool::advGridMode); settings.setValue(GridSizeSetting, (int)m_CurrentGame->m_Game->getDataModel()->getWorkspace()->gridSizeModifier); // reset the tooltips for undo/redo m_pMainWindow->undoAction->setToolTip(tr("Undo")); QtUtilities::setActionShortcuts(*m_pMainWindow->undoAction,QKeySequence::keyBindings(QKeySequence::Undo)); m_pMainWindow->redoAction->setToolTip(tr("Redo")); QtUtilities::setActionShortcuts(*m_pMainWindow->redoAction,QKeySequence::keyBindings(QKeySequence::Redo)); m_bActive = false; } void RobloxIDEDoc::setDockViewsEnabled(bool state) { // update tree widget if (m_CurrentGame->m_TreeWidget) state ? m_CurrentGame->m_TreeWidget->activate() : m_CurrentGame->m_TreeWidget->deActivate(); // update rest of the views shared_ptr dataModel = state ? m_CurrentGame->m_Game->getDataModel() : shared_ptr(); UpdateUIManager& uiManager = UpdateUIManager::Instance(); RobloxPropertyWidget& property_view = uiManager.getViewWidget(eDW_PROPERTIES); property_view.setDataModel(dataModel); RobloxDiagnosticsView& diagnostics_view = uiManager.getViewWidget(eDW_DIAGNOSTICS); diagnostics_view.setDataModel(dataModel); RobloxScriptReview& script_view = uiManager.getViewWidget(eDW_SCRIPT_REVIEW); script_view.setDataModel(dataModel); RobloxToolBox& toolbox_view = uiManager.getViewWidget(eDW_TOOLBOX); toolbox_view.setDataModel(dataModel); InsertObjectWidget& basic_view = uiManager.getViewWidget(eDW_BASIC_OBJECTS); basic_view.setDataModel(dataModel); ScriptAnalysisWidget& analysis_view = uiManager.getViewWidget(eDW_SCRIPT_ANALYSIS); analysis_view.setDataModel(dataModel); if (!state) { RobloxFindWidget& find_view = uiManager.getViewWidget(eDW_FIND); find_view.dataModelClosing(); } } void RobloxIDEDoc::onWaypointChanged() { QMetaObject::invokeMethod(this,"updateUndoRedo",Qt::QueuedConnection); } void RobloxIDEDoc::updateUndoRedo() { RBXASSERT(m_CurrentGame->m_Game); RBXASSERT(m_CurrentGame->m_Game->getDataModel()); if (!m_CurrentGame->m_UndoVerb || !m_CurrentGame->m_RedoVerb) // Possible on closing down Play Doc return; m_pMainWindow->undoAction->setEnabled(m_CurrentGame->m_UndoVerb->isEnabled()); m_pMainWindow->redoAction->setEnabled(m_CurrentGame->m_RedoVerb->isEnabled()); QString tooltip; QList shortcuts; shortcuts = m_pMainWindow->undoAction->shortcuts(); tooltip = m_CurrentGame->m_UndoVerb->getText().c_str(); tooltip += " ("; for (int i = 0 ; i < shortcuts.size() ; ++i) { if (i > 0) tooltip += ", "; tooltip += shortcuts[i].toString(QKeySequence::NativeText); } tooltip += ")"; m_pMainWindow->undoAction->setToolTip(tooltip); shortcuts = m_pMainWindow->redoAction->shortcuts(); tooltip = m_CurrentGame->m_RedoVerb->getText().c_str(); tooltip += " ("; for (int i = 0 ; i < shortcuts.size() ; ++i) { if (i > 0) tooltip += ", "; tooltip += shortcuts[i].toString(QKeySequence::NativeText); } tooltip += ")"; m_pMainWindow->redoAction->setToolTip(tooltip); } void RobloxIDEDoc::mapActionIDWithVerb(GameState& gameState,const QString &actionID, const RBX::Name &verbName) { gameState.m_VerbMap[actionID] = &verbName; } void RobloxIDEDoc::mapActionIDWithVerb(GameState& gameState,const QString &actionID, const RBX::Verb *pVerb) { if (pVerb) mapActionIDWithVerb(gameState, actionID, pVerb->getName()); } void RobloxIDEDoc::mapActionIDWithVerb(GameState& gameState,const QString &actionID, const char *verbName) { if (verbName) mapActionIDWithVerb(gameState, actionID, RBX::Name::lookup(verbName)); } bool RobloxIDEDoc::handleDrop(const QString &fileName) { if (fileName.isEmpty()) return false; if (fileName.endsWith(".rbxm", Qt::CaseInsensitive) || fileName.endsWith(".rbxmx", Qt::CaseInsensitive)) { try { StudioUtilities::insertModel(m_CurrentGame->m_Game->getDataModel(), fileName, false); } catch (std::exception& e) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, e.what()); } } return true; } bool RobloxIDEDoc::doHandleAction(const QString& actionID, bool isChecked) { // TODO: Specific actions -- should this be moved to a separate function if more such actions? UpdateUIManager& uiManager = UpdateUIManager::Instance(); if (uiManager.isLongProcessInProgress()) return false; shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); if (actionID == "startPlayerAction" || actionID == "startServerAction" || actionID == "startServerAndPlayersAction" || actionID == "playSoloAction") { applyScriptChanges(); } else if (actionID == "viewDiagnosticsAction") { if (dataModel) { RobloxDiagnosticsView& diagnostics_view = uiManager.getViewWidget(eDW_DIAGNOSTICS); diagnostics_view.setDataModel(dataModel); } } else if (actionID == "viewScriptPerformanceAction") { if (dataModel) { RobloxScriptReview& script_view = uiManager.getViewWidget(eDW_SCRIPT_REVIEW); script_view.setDataModel(dataModel); } } else if (actionID == "actionUpdateStatusBar") { if (dataModel) { double time = dataModel->getGameTime(); QString timeLabel; timeLabel.sprintf("t %.4g", time); QString fpsLabel; if (m_CurrentGame->m_View && m_CurrentGame->m_View->getViewBase().getFrameRateManager()) { double fps = m_CurrentGame->m_View->getViewBase().getFrameRateManager()->GetFrameTimeAverage(); if (AuthoringSettings::singleton().renderThrottlePercentage >= 100 && !m_pQOgreWidget->hasApplicationFocus()) fpsLabel.sprintf("0fps"); else fpsLabel.sprintf("%.3gfps",1000.0 / fps); } UpdateUIManager::Instance().setStatusLabels(timeLabel, fpsLabel); } } else if (actionID == "viewToolboxAction") { if (dataModel) { RobloxToolBox& toolbox_view = uiManager.getViewWidget(eDW_TOOLBOX); toolbox_view.setDataModel(dataModel); } } else if (actionID == "viewBasicObjectsAction") { if (dataModel) { InsertObjectWidget& basic_view = uiManager.getViewWidget(eDW_BASIC_OBJECTS); basic_view.setDataModel(dataModel); } } else if (actionID == "testStatsAction") { toggleStats(StatsVerb); return true; } else if (actionID == "testRenderStatsAction") { toggleStats(RenderStatsVerb); return true; } else if (actionID == "testPhysicsStatsAction") { toggleStats(PhysicsStatsVerb); return true; } else if (actionID == "testNetworkStatsAction") { toggleStats(NetworkStatsVerb); return true; } else if (actionID == "testSummaryStatsAction") { toggleStats(SummaryStatsVerb); return true; } else if (actionID == "testCustomStatsAction" && FFlag::StudioCustomStatsEnabled) { toggleStats(CustomStatsVerb); return true; } else if (actionID == "testClearStatsAction") { setStatsEnabled(StatsVerb,false, m_pMainWindow->testStatsAction); setStatsEnabled(RenderStatsVerb,false, m_pMainWindow->testRenderStatsAction); setStatsEnabled(PhysicsStatsVerb,false, m_pMainWindow->testPhysicsStatsAction); setStatsEnabled(NetworkStatsVerb,false, m_pMainWindow->testNetworkStatsAction); setStatsEnabled(SummaryStatsVerb,false, m_pMainWindow->testSummaryStatsAction); setStatsEnabled(CustomStatsVerb,false, m_pMainWindow->testCustomStatsAction); } else if (FFlag::StudioSeparateActionByActivationMethod && actionID == "minimizeRibbonAction" && UpdateUIManager::Instance().getMainWindow().isRibbonStyle()) { if (RobloxRibbonMainWindow* mainWindow = static_cast(&UpdateUIManager::Instance().getMainWindow())) { mainWindow->toggleRibbonMinimize(); return true; } } else if (FFlag::StudioSeparateActionByActivationMethod && actionID == "quickInsertAction") { UpdateUIManager::Instance().onQuickInsertFocus(); return true; } else if (actionID == "materialActionAsTool") { StudioMaterialVerb::sMaterialActionActAsTool = isChecked; RobloxSettings().setValue("rbxMaterialActionActAsTool", isChecked); // reset mouse command if material tool is active if (!isChecked) resetMouseCommand("Material"); return true; } else if (actionID == "colorFillActionAsTool") { StudioColorVerb::sColorActionActAsTool = isChecked; RobloxSettings().setValue("rbxColorActionActAsTool", isChecked); // reset mouse command if fill tool is active if (!isChecked) resetMouseCommand("Fill"); return true; } else if (isCloudEditSession() && actionID == "downloadPlaceCopyAction") { downloadPlaceCopy(); return true; } QString actionIDToLookFor(actionID); //split the QString to check if it need preProcessVerb QStringList tokens = actionID.split("_"); //if it is a color change action, do the preProcessVerb to set rgb values, and change action to Fill automatically. if (tokens.length() > 1) { preProcessVerb(tokens); actionIDToLookFor = tokens[0]; } if (!m_CurrentGame->m_VerbMap.contains(actionIDToLookFor)) { IExternalHandler* pHandler = getExternalHandler(actionID); if (pHandler) { RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Write); pHandler->setDataModel(dataModel); return pHandler->handle(); } return false; } try { // DE2875: Doing a lock here leads to a hang with modal dialogs, if a lock is must then it has to be inside the verb! static QString noLockVerbs( "insertModelAction insertIntoFileAction selectionSaveToFileAction playSoloAction " "startServerAction startPlayerAction publishToRobloxAction publishToRobloxAsAction " "publishSelectionToRobloxAction saveToRobloxAction undoAction redoAction " "publishAsPluginAction openToolBoxWithOptions pairRbxDeviceAction pairRbxDevDeviceAction manageEmulationDeviceAction " "createNewLinkedSourceAction unionSelectionAction separateSelectionAction"); static QString noLockVerbsPlaySoloOptimization("simulationRunAction simulationPlayAction " "simulationRunSelectorSim simulationRunSelectorTest simulationRunSelectorDebug"); if (noLockVerbs.contains(actionIDToLookFor) || (noLockVerbsPlaySoloOptimization.contains(actionIDToLookFor)) || (actionIDToLookFor == "simulationResetAction")) { RBX::Verb *pVerb = dataModel->getWorkspace()->getVerb(*m_CurrentGame->m_VerbMap[actionIDToLookFor]); if (pVerb && pVerb->isEnabled()) pVerb->doIt(dataModel.get()); } else { RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Write); RBX::Verb *pVerb = dataModel->getWorkspace()->getVerb(*m_CurrentGame->m_VerbMap[actionIDToLookFor]); if (pVerb && pVerb->isEnabled()) pVerb->doIt(dataModel.get()); } } catch (std::exception& e) { QtUtilities::RBXMessageBox msgBox; msgBox.setIcon(QMessageBox::Warning); msgBox.setText(e.what()); msgBox.exec(); } if (actionID == "advanceJointCreationManualAction") { RobloxSettings settings; settings.setValue(RBX_MANUAL_JOINT_CREATION_KEY, RBX::AdvArrowTool::advManualJointMode); } return true; } void RobloxIDEDoc::preProcessVerb(const QStringList &tokens) { //if it is a color changes, get the last three tokens, which will be the rgb value respectily. check the name of the color actions for detail. if (tokens[0] == "actionFillColor") { QByteArray ba = tokens[tokens.length()-1].toAscii(); const char *c_str = ba.data(); RBX::FillTool::color.set(RBX::BrickColor::parse(c_str)); } //if it is a material change, the second token is the name of the material. check the material action for detail. else if (tokens[0] == "actionMaterial") { RBX::MaterialTool::material = RBX::MaterialVerb::parseMaterial(tokens[1].toStdString()); } } bool RobloxIDEDoc::handlePluginAction(void *pNotifier, void *pAction) { if (!pAction || !pNotifier) return false; static_cast(pNotifier)->buttonClick(m_CurrentGame->m_Game->getDataModel().get(), pAction); return true; } RBX::Reflection::Variant RobloxIDEDoc::evaluateCommandBarItem(const char* itemToEvaluate, shared_ptr script) { shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); RBX::DataModel::LegacyLock lock(dataModel,RBX::DataModelJob::Write); boost::shared_ptr pScriptContext = RBX::shared_from(dataModel->create()); if(pScriptContext) { return pScriptContext->evaluateStudioCommandItem(itemToEvaluate, script); } return RBX::Reflection::Variant(); } void RobloxIDEDoc::handleScriptCommand(const QString &execCommand) { if (execCommand.isEmpty()) return; shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); RBX::DataModel::LegacyLock lock(dataModel,RBX::DataModelJob::Write); try { std::string command = execCommand.toStdString(); bool isJoinScript = false; if (RBX::ContentProvider::isHttpUrl(command)) { if (StudioUtilities::containsJoinScript(execCommand)) isJoinScript = true; std::auto_ptr stream = RBX::ServiceProvider::create(dataModel.get())->getContent(RBX::ContentId(command)); command = std::string(static_cast(std::stringstream() << stream->rdbuf()).str()); } if (isJoinScript) { // enable FRM in play mode CRenderSettingsItem::singleton().setEnableFRM(true); // Reset synchronized flags, they should be set by the server FLog::ResetSynchronizedVariablesState(); int firstNewLineIndex = command.find("\r\n"); if (command[firstNewLineIndex+2] == '{') { m_CurrentGame->m_Game->configurePlayer(RBX::Security::CmdLine_, command.substr(firstNewLineIndex+2)); return; } } boost::shared_ptr pScriptContext = RBX::shared_from(dataModel->create()); if (pScriptContext) pScriptContext->executeInNewThread(RBX::Security::CmdLine_, RBX::ProtectedString::fromTrustedSource(command.c_str()), command.c_str()); } catch (std::exception& e) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, e.what()); } } bool RobloxIDEDoc::actionState(const QString &actionID, bool &enableState, bool &checkedState) { if (isCloudEditSession()) { // disable save and publish related actions static QString cloudEditDeactiveActions( "publishToRobloxAsAction fileSaveAction fileSaveAsAction publishToRobloxAction "); if (cloudEditDeactiveActions.contains(actionID)) { checkedState = false; enableState = false; return true; } static QString cloudEditActiveActions( "downloadPlaceCopyAction "); if (cloudEditActiveActions.contains(actionID)) { checkedState = false; enableState = true; return true; } } static QString defaultActiveActions( "fileSaveAction fileSaveAsAction fileCloseAction " "executeScriptAction publishToRobloxAsAction testClearStatsAction "); if (defaultActiveActions.contains(actionID)) { checkedState = false; enableState = true; return true; } if (m_pMainWindow->isRibbonStyle()) { static QString defaultActiveRibbonActions( "setVehiclesAction setNPCsAction setBuildingsAction setGearAction " "setWeaponsAction setMyModelsAction setSearchAction " "simulationScriptsEnabledAction simulationPlayerEnabledAction simulationPhysicsEnabledAction " "actionParticleEffects actionSelectMenu " "PartLargeAction SphereLargeAction WedgeLargeAction CylinderLargeAction startLocationAction lockMenu jointCreationMode " "ms_Plastic ms_Wood ms_Slate ms_Concrete ms_CorrodedMetal ms_DiamondPlate ms_Foil ms_Grass ms_Ice ms_Brick ms_Sand " "ms_Fabric ms_Granite ms_Marble ms_Pebble ms_SmoothPlastic ms_Neon ms_WoodPlanks ms_Cobblestone ms_Metal emulateDeviceAction " "placePartAction playGameAction ScriptLargeAction LocalScriptLargeAction ModuleScriptLargeAction gridGroup __qtn_Action_Option "); if ( defaultActiveRibbonActions.contains(actionID)) { QAction* action = UpdateUIManager::Instance().getAction(actionID); if (action) { checkedState = action->isChecked(); enableState = true; } return true; } static QString clientAndServerActions("playersMode startServerCB startServerAndPlayersAction cleanupServersAndPlayersAction pairRbxDevDeviceAction"); if (clientAndServerActions.contains(actionID)) { if (!m_CurrentGame || !m_CurrentGame->m_Game || !m_CurrentGame->m_Game->getDataModel() || ( !RobloxIDEDoc::isEditMode(m_CurrentGame->m_Game->getDataModel().get()) && !isCloudEditSession())) { enableState = false; return true; } else if (actionID == "pairRbxDevDeviceAction") { enableState = !RobloxApplicationManager::instance().hasChildProcesses(); return true; } else if (actionID == "cleanupServersAndPlayersAction") { enableState = RobloxApplicationManager::instance().hasChildProcesses(); return true; } else if (actionID == "playersMode") { enableState = true; return true; } } static QString defaultRibbonDeactiveAction("startServerAction addWatchAction "); if (defaultRibbonDeactiveAction.contains(actionID)) { checkedState = false; enableState = false; return true; } if (actionID == "actionSurface") { static QStringList surfaceActionsList = (QStringList() << "glueSurfaceAction" << "smoothSurfaceAction" << "studsAction" << "inletAction" << "universalsAction" << "weldSurfaceAction"); for (int ii = 0; ii < surfaceActionsList.size(); ++ii) { actionState(surfaceActionsList[ii], enableState, checkedState); if (checkedState) return true; } enableState = true; return true; } if (actionID == "materialActionAsTool") { checkedState = StudioMaterialVerb::sMaterialActionActAsTool; enableState = true; return true; } if (actionID == "colorFillActionAsTool") { checkedState = StudioColorVerb::sColorActionActAsTool; enableState = true; return true; } if (FFlag::LuaDebugger) { if (actionID == "toggleAllBreakpointsStateAction") { enableState = DebuggerViewsManager::Instance().getViewWidget(DebuggerViewsManager::eDV_BREAKPOINTS).numBreakpoints() > 0; return true; } } // get status from plugin host (this is required only for ribbon as we do not hide plugin page) if (m_pMainWindow->pluginHost().actionState(actionID, enableState, checkedState)) return true; } // TODO - fix this (see below) static QString disabledActions = "commentSelectionAction uncommentSelectionAction toggleCommentAction " "expandAllFoldsAction collapseAllFoldsAction replaceAction resetScriptZoomAction " "reloadScriptAction goToLineAction downloadPlaceCopyAction "; if (disabledActions.contains(actionID)) { enableState = false; return true; } if (UpdateUIManager::Instance().getDockActionNames().contains(actionID)) { enableState = true; checkedState = UpdateUIManager::Instance().getAction(actionID)->isChecked(); return true; } if (m_CurrentGame->m_VerbMap.contains(actionID)) { RBX::Verb* pVerb = m_CurrentGame->m_Game->getDataModel()->getWorkspace()->getVerb(*m_CurrentGame->m_VerbMap[actionID]); if (pVerb) { checkedState = pVerb->isChecked(); enableState = pVerb->isEnabled(); return true; } } // TODO - all the enabled/active states should be enabled otherwise default disabled // enabledState = false; return false; } void RobloxIDEDoc::createActionIDVerbMap(GameState& gameState) { shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; //define mapping - action name with the verb name! mapFilesMenu(gameState); mapEditMenu(gameState); mapViewMenu(gameState); mapFormatMenu(gameState); mapToolMenu(gameState); mapInsertMenu(gameState); mapAdvToolsToolbar(gameState); mapRunToolbar(gameState); mapCameraToolbar(gameState); mapStandardToolbar(gameState); mapAdvanceBuildToolbar(gameState); //cut, copy and paste verbs.append(new CutVerb(dataModel.get())); mapActionIDWithVerb(gameState,"cutAction",verbs.back()); verbs.append(new CopyVerb(dataModel.get())); mapActionIDWithVerb(gameState,"copyAction",verbs.back()); verbs.append(new PasteVerb(dataModel.get(), false)); mapActionIDWithVerb(gameState,"pasteAction",verbs.back()); verbs.append(new PasteVerb(dataModel.get(), true)); mapActionIDWithVerb(gameState,"pasteIntoAction",verbs.back()); verbs.append(new DuplicateSelectionVerb(dataModel.get())); mapActionIDWithVerb(gameState,"duplicateSelectionAction",verbs.back()); verbs.append(new RBX::BoundVerb(dataModel.get(), "renameObjectVerb", boost::bind(&RobloxIDEDoc::onObjectRename, this), boost::bind(&RobloxIDEDoc::isObjectRenameEnabled, this))); mapActionIDWithVerb(gameState,"renameObjectAction",verbs.back()); verbs.append(new InsertModelVerb(dataModel.get(), true)); mapActionIDWithVerb(gameState,"insertIntoFileAction",verbs.back()); verbs.push_back(new SelectionSaveToFileVerb(dataModel.get())); mapActionIDWithVerb(gameState, "selectionSaveToFileAction", verbs.back()); verbs.append(new ExportSelectionVerb(dataModel.get())); mapActionIDWithVerb(gameState, "exportSelectionAction", verbs.back()); if (m_pMainWindow->isRibbonStyle()) { verbs.push_back(new OpenToolBoxWithOptionsVerb(dataModel.get())); mapActionIDWithVerb(gameState, "openToolBoxWithOptions", verbs.back()); verbs.push_back(new InsertBasicObjectVerb(dataModel.get())); mapActionIDWithVerb(gameState, "insertBasicObject", verbs.back()); } // no mapping required verbs.push_back(new ShutdownClientVerb(dataModel.get(), this)); verbs.push_back(new ShutdownClientAndSaveVerb(dataModel.get(), this)); verbs.push_back(new LeaveGameVerb(dataModel.get(), this)); verbs.push_back(new ToggleFullscreenVerb(dataModel.get())); verbs.push_back(new ToggleLocalSpaceVerb(dataModel.get())); mapActionIDWithVerb(gameState, "toggleLocalSpaceAction", verbs.back()); verbs.push_back(new LaunchHelpForSelectionVerb(dataModel.get())); mapActionIDWithVerb(gameState, "launchHelpForSelectionAction", verbs.back()); verbs.push_back(new AnalyzePhysicsToggleVerb(dataModel.get())); mapActionIDWithVerb(gameState, "analyzePhysicsToggleAction", verbs.back()); } void RobloxIDEDoc::mapFilesMenu(GameState& gameState) { shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; verbs.append(new PublishToRobloxVerb(dataModel.get(), m_pMainWindow)); mapActionIDWithVerb(gameState,"publishToRobloxAction",verbs.back()); verbs.append(new PublishToRobloxAsVerb(dataModel.get(), m_pMainWindow)); mapActionIDWithVerb(gameState,"publishToRobloxAsAction",verbs.back()); verbs.append(new PublishSelectionToRobloxVerb(dataModel.get(), m_pMainWindow)); mapActionIDWithVerb(gameState,"publishSelectionToRobloxAction",verbs.back()); /// This goes into the Tree View mapActionIDWithVerb(gameState,"saveToRobloxAction",verbs.back()); verbs.push_back(new CreateNewLinkedSourceVerb(dataModel.get())); mapActionIDWithVerb(gameState, "createNewLinkedSourceAction", verbs.back()); verbs.push_back(new PublishAsPluginVerb(dataModel.get(), m_pMainWindow)); mapActionIDWithVerb(gameState, "publishAsPluginAction", verbs.back()); verbs.append(new ExportPlaceVerb(dataModel.get())); mapActionIDWithVerb(gameState, "exportPlaceAction", verbs.back()); } void RobloxIDEDoc::mapEditMenu(GameState& gameState) { boost::shared_ptr commonVerbs = gameState.m_Game->commonVerbs; shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; mapActionIDWithVerb(gameState,"selectAllAction", &commonVerbs->selectAllCommand); mapActionIDWithVerb(gameState,"deleteSelectedAction", &commonVerbs->deleteSelectionVerb); mapActionIDWithVerb(gameState,"lockAction", &commonVerbs->lockToolVerb); mapActionIDWithVerb(gameState,"unlockAllAction", &commonVerbs->unlockAllVerb); gameState.m_UndoVerb = new UndoVerb(dataModel.get()); verbs.append(gameState.m_UndoVerb); mapActionIDWithVerb(gameState,"undoAction",gameState.m_UndoVerb); gameState.m_RedoVerb = new RedoVerb(dataModel.get()); verbs.append(gameState.m_RedoVerb); mapActionIDWithVerb(gameState,"redoAction",gameState.m_RedoVerb); mapActionIDWithVerb(gameState,"rotateSelectionAction",&commonVerbs->rotateSelectionVerb); mapActionIDWithVerb(gameState,"tiltSelectionAction",&commonVerbs->tiltSelectionVerb); mapActionIDWithVerb(gameState,"moveUpBrickAction",&commonVerbs->moveUpBrickVerb); mapActionIDWithVerb(gameState,"moveDownBrickAction",&commonVerbs->moveDownSelectionVerb); } void RobloxIDEDoc::mapViewMenu(GameState& gameState) { shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; verbs.append(new ToggleAxisWidgetVerb(dataModel.get())); mapActionIDWithVerb(gameState,"toggleAxisWidgetAction",verbs.back()); verbs.append(new Toggle3DGridVerb(dataModel.get())); mapActionIDWithVerb(gameState,"toggle3DGridAction",verbs.back()); } void RobloxIDEDoc::mapFormatMenu(GameState& gameState) { boost::shared_ptr commonVerbs = gameState.m_Game->commonVerbs; shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; mapActionIDWithVerb(gameState,"rotateSelectionAction", &commonVerbs->rotateSelectionVerb); mapActionIDWithVerb(gameState,"tiltSelectionAction", &commonVerbs->tiltSelectionVerb); verbs.append(new GroupSelectionVerb(dataModel.get())); mapActionIDWithVerb(gameState,"groupSelectionAction", verbs.back()); verbs.append(new UngroupSelectionVerb(dataModel.get())); mapActionIDWithVerb(gameState,"ungroupSelectionAction", verbs.back()); verbs.append(new UnionSelectionVerb(dataModel.get())); mapActionIDWithVerb(gameState, "unionSelectionAction", verbs.back()); verbs.append(new NegateSelectionVerb(dataModel.get())); mapActionIDWithVerb(gameState, "negateSelectionAction", verbs.back()); verbs.append(new SeparateSelectionVerb(dataModel.get())); mapActionIDWithVerb(gameState, "separateSelectionAction", verbs.back()); mapActionIDWithVerb(gameState,"selectChildrenAction", &commonVerbs->selectChildrenVerb); } void RobloxIDEDoc::mapToolMenu(GameState& gameState) { boost::shared_ptr commonVerbs = gameState.m_Game->commonVerbs; shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; verbs.append(new PlaySoloVerb(dataModel.get())); mapActionIDWithVerb(gameState,"playSoloAction",verbs.back()); verbs.append(new StartServerVerb(dataModel.get())); mapActionIDWithVerb(gameState,"startServerAction",verbs.back()); verbs.append(new StartPlayerVerb(dataModel.get())); mapActionIDWithVerb(gameState,"startPlayerAction",verbs.back()); mapActionIDWithVerb(gameState,"testStatsAction", &commonVerbs->statsCommand); mapActionIDWithVerb(gameState,"testRenderStatsAction", &commonVerbs->renderStatsCommand); mapActionIDWithVerb(gameState,"testNetworkStatsAction", &commonVerbs->networkStatsCommand); mapActionIDWithVerb(gameState,"testPhysicsStatsAction", &commonVerbs->physicsStatsCommand); mapActionIDWithVerb(gameState,"testSummaryStatsAction", &commonVerbs->summaryStatsCommand); if (FFlag::StudioCustomStatsEnabled) { mapActionIDWithVerb(gameState,"testCustomStatsAction", &commonVerbs->customStatsCommand); } if (m_pQOgreWidget) { verbs.append(new ScreenshotVerb(dataModel.get())); mapActionIDWithVerb(gameState, "screenShotAction", verbs.back()); } if (m_pMainWindow->isRibbonStyle()) { verbs.push_back(new InsertAdvancedObjectViewVerb(dataModel.get())); mapActionIDWithVerb(gameState,"actionInsertAdvancedObject",verbs.back()); verbs.push_back(new CreatePluginVerb(dataModel.get())); mapActionIDWithVerb(gameState,"createPluginAction",verbs.back()); verbs.push_back(new StartServerAndPlayerVerb(dataModel.get())); mapActionIDWithVerb(gameState,"startServerAndPlayersAction",verbs.back()); verbs.push_back(new ServerPlayersStateInitVerb(dataModel.get())); mapActionIDWithVerb(gameState,"startServerCB",verbs.back()); verbs.push_back(new PairRbxDevVerb(dataModel.get(), NULL)); mapActionIDWithVerb(gameState,"pairRbxDevDeviceAction",verbs.back()); verbs.push_back(new ManageEmulationDevVerb(dataModel.get(), NULL)); mapActionIDWithVerb(gameState, "manageEmulationDeviceAction", verbs.back()); verbs.push_back(new AudioToggleVerb(dataModel.get())); mapActionIDWithVerb(gameState, "audioToggleAction", verbs.back()); //verbs.push_back(new LaunchInstancesVerb(dataModel.get())); //mapActionIDWithVerb(gameState,"launchInstancesAction",verbs.back()); } } void RobloxIDEDoc::mapInsertMenu(GameState& gameState) { shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; //insert verbs.append(new InsertModelVerb(dataModel.get(), false)); mapActionIDWithVerb(gameState,"insertModelAction",verbs.back()); } void RobloxIDEDoc::mapAdvToolsToolbar(GameState& gameState) { boost::shared_ptr commonVerbs = gameState.m_Game->commonVerbs; shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; mapActionIDWithVerb(gameState,"glueSurfaceAction", &commonVerbs->glueToolVerb); mapActionIDWithVerb(gameState,"smoothSurfaceAction", &commonVerbs->flatToolVerb); mapActionIDWithVerb(gameState,"weldSurfaceAction", &commonVerbs->weldToolVerb); mapActionIDWithVerb(gameState,"studsAction", &commonVerbs->studsToolVerb); mapActionIDWithVerb(gameState,"inletAction", &commonVerbs->inletToolVerb); mapActionIDWithVerb(gameState,"universalsAction", &commonVerbs->universalToolVerb); mapActionIDWithVerb(gameState,"hingeAction", &commonVerbs->hingeToolVerb); mapActionIDWithVerb(gameState,"smoothNoOutlinesAction", &commonVerbs->smoothNoOutlinesToolVerb); mapActionIDWithVerb(gameState,"motorRightAction", &commonVerbs->rightMotorToolVerb); if (m_pMainWindow->isRibbonStyle()) { // if we need to have sticky command then just remove these assignments RBX::RightMotorTool::isStickyVerb = false; RBX::HingeTool::isStickyVerb = false; mapActionIDWithVerb(gameState,"anchorAction", &commonVerbs->anchorVerb); verbs.push_back(new StudioColorVerb(dataModel.get())); mapActionIDWithVerb(gameState,"actionColorSelector", verbs.back()); verbs.push_back(new StudioMaterialVerb(dataModel.get())); mapActionIDWithVerb(gameState,"actionMaterialSelector", verbs.back()); } else { mapActionIDWithVerb(gameState,"anchorAction", &commonVerbs->anchorToolVerb); mapActionIDWithVerb(gameState,"actionFillColor", &commonVerbs->fillToolVerb); mapActionIDWithVerb(gameState,"actionMaterial", &commonVerbs->materialToolVerb); } mapActionIDWithVerb(gameState,"dropperAction", &commonVerbs->dropperToolVerb); mapActionIDWithVerb(gameState,"advArrowToolAction", &commonVerbs->advArrowToolVerb); } void RobloxIDEDoc::mapRunToolbar(GameState& gameState) { boost::shared_ptr commonVerbs = gameState.m_Game->commonVerbs; shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; //Run verbs.append(new RBX::BoundVerb( dataModel.get(), "IDE Run", boost::bind(&RobloxIDEDoc::onIdeRun, this, false), boost::bind(&RobloxIDEDoc::isIdeRunEnabled, this, false))); mapActionIDWithVerb(gameState,"simulationRunAction", verbs.back()); verbs.append(new RBX::BoundVerb( dataModel.get(), "IDE Play", boost::bind(&RobloxIDEDoc::onIdeRun, this, true), boost::bind(&RobloxIDEDoc::isIdeRunEnabled, this, true))); mapActionIDWithVerb(gameState,"simulationPlayAction", verbs.back()); verbs.append(new RBX::BoundVerb( dataModel.get(), "IDE Pause", boost::bind(&RobloxIDEDoc::onIdePause, this), boost::bind(&RobloxIDEDoc::isIdePauseEnabled, this))); mapActionIDWithVerb(gameState,"simulationStopAction", verbs.back()); verbs.append(new RBX::BoundVerb( dataModel.get(), "IDE Reset", boost::bind(&RobloxIDEDoc::onIdeReset, this), boost::bind(&RobloxIDEDoc::isIdeResetEnabled, this))); mapActionIDWithVerb(gameState,"simulationResetAction", verbs.back()); } void RobloxIDEDoc::mapCameraToolbar(GameState& gameState) { mapActionIDWithVerb(gameState,"zoomInAction", "CameraZoomIn"); mapActionIDWithVerb(gameState,"zoomOutAction", "CameraZoomOut"); // Camera Tilt Commands apparently use an inverted pitch. We don't want // to change anything that relies on that behavior so in Studio we're // going to switch them. Inverted pitch might make sense playing a // game but not when editing. mapActionIDWithVerb(gameState,"tiltUpAction", "CameraTiltDown"); mapActionIDWithVerb(gameState,"tiltDownAction", "CameraTiltUp"); mapActionIDWithVerb(gameState,"panRightAction", "CameraPanRight"); mapActionIDWithVerb(gameState,"panLeftAction", "CameraPanLeft"); mapActionIDWithVerb(gameState,"lookAtObjectAction", "CameraCenter"); mapActionIDWithVerb(gameState,"zoomExtentsAction", "CameraZoomExtents"); // Only used in context menu on part } void RobloxIDEDoc::mapStandardToolbar(GameState& gameState) { boost::shared_ptr commonVerbs = gameState.m_Game->commonVerbs; mapActionIDWithVerb(gameState,"moveUpBrickAction", &commonVerbs->moveUpBrickVerb); mapActionIDWithVerb(gameState,"moveDownBrickAction", &commonVerbs->moveDownSelectionVerb); } void RobloxIDEDoc::mapAdvanceBuildToolbar(GameState& gameState) { boost::shared_ptr commonVerbs = gameState.m_Game->commonVerbs; shared_ptr dataModel = gameState.m_Game->getDataModel(); QVector& verbs = gameState.m_Verbs; //Advance build relate if (!m_pMainWindow->isRibbonStyle()) { RobloxSettings settings; RBX::AdvArrowTool::advManualJointMode = settings.value(RBX_MANUAL_JOINT_CREATION_KEY, false).toBool(); mapActionIDWithVerb(gameState,"advanceJointCreationManualAction", &commonVerbs->turnOnManualJointCreationVerb); } else { verbs.push_back(new JointCreationModeVerb(dataModel.get())); mapActionIDWithVerb(gameState,"jointCreationMode", verbs.back()); mapActionIDWithVerb(gameState,"gridSizeToTwoAction", &commonVerbs->setGridSizeToTwoVerb); mapActionIDWithVerb(gameState,"gridSizeToFourAction", &commonVerbs->setGridSizeToFourVerb); mapActionIDWithVerb(gameState,"gridSizeToSixteenAction", &commonVerbs->setGridSizeToSixteenVerb); verbs.push_back(new JointToolHelpDialogVerb(dataModel.get())); mapActionIDWithVerb(gameState,"advancedJointCreationHelpAction",verbs.back()); } mapActionIDWithVerb(gameState, "gridToOneAction", &commonVerbs->setDragGridToOneVerb); mapActionIDWithVerb(gameState, "gridToOneFifthAction", &commonVerbs->setDragGridToOneFifthVerb); mapActionIDWithVerb(gameState, "gridToOffAction", &commonVerbs->setDragGridToOffVerb); mapActionIDWithVerb(gameState,"advTranslateAction", &commonVerbs->advMoveToolVerb); mapActionIDWithVerb(gameState,"advRotateAction", &commonVerbs->advRotateToolVerb); mapActionIDWithVerb(gameState,"resizeAction", &commonVerbs->resizeToolVerb); verbs.push_back(new ToggleCollisionCheckVerb(dataModel.get())); mapActionIDWithVerb(gameState,"toggleCollisionCheckAction", verbs.back()); } void RobloxIDEDoc::onIdePause() { if (m_PlayGame.m_Game) { shared_ptr playDataModel = m_PlayGame.m_Game->getDataModel(); RBX::DataModel::LegacyLock lock(playDataModel,RBX::DataModelJob::Write); RBX::RunService* pRS = playDataModel->create(); pRS->pause(); } else { shared_ptr dataModel = m_EditGame.m_Game->getDataModel(); RBX::DataModel::LegacyLock lock(dataModel,RBX::DataModelJob::Write); RBX::RunService* pRS = dataModel->create(); pRS->pause(); } } bool RobloxIDEDoc::isIdePauseEnabled() { if (m_PlayGame.m_Game) { shared_ptr playDataModel = m_PlayGame.m_Game->getDataModel(); RBX::RunService* pRS = playDataModel->create(); RBX::RunState runState = pRS->getRunState(); return runState == RBX::RS_RUNNING; } shared_ptr dataModel = m_EditGame.m_Game->getDataModel(); RBX::RunService* pRS = dataModel->create(); RBX::RunState runState = pRS->getRunState(); return (runState == RBX::RS_RUNNING); } void RobloxIDEDoc::teleportToURL(QString url, bool play) { if (StudioUtilities::isTestMode()) { QtUtilities::RBXMessageBox msgBox; msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setDefaultButton(QMessageBox::Ok); msgBox.setText("Teleports not yet supported in Test Server or Create Player."); msgBox.exec(); return; } closePlayDataModel(); // Check to see if the url fetched returns anything at all. { shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); std::string command = url.toStdString(); std::auto_ptr stream = RBX::ServiceProvider::create(dataModel.get())->getContent(RBX::ContentId(command)); std::string scriptStr(static_cast(std::stringstream() << stream->rdbuf()).str()); if (scriptStr.empty()) { QtUtilities::RBXMessageBox msgBox; msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setDefaultButton(QMessageBox::Ok); msgBox.setText("Could not teleport to place."); msgBox.exec(); return; } } loadPlayDataModel(url, play, false, true); } void RobloxIDEDoc::loadPlayDataModel(QString url, bool play, bool cloneDataModel, bool isTeleport) { shared_ptr dataModel = m_EditGame.m_Game->getDataModel(); if (!dataModel) return; { // this will be called in case of old play solo RBX::RunService* pRS = dataModel->create(); RBX::DataModel::LegacyLock editLock(dataModel,RBX::DataModelJob::Write); if (!pRS->isEditState()) { applyScriptChanges(); pRS->run(); return; } } // check if we're already running if (m_PlayGame.m_Game && m_PlayGame.m_Game->getDataModel() && m_PlayGame.m_Game->getDataModel()->create()) { { RBX::DataModel::LegacyLock lock(m_PlayGame.m_Game->getDataModel(),RBX::DataModelJob::Write); RBX::RunState runState = m_PlayGame.m_Game->getDataModel()->create()->getRunState(); if (runState == RBX::RS_PAUSED) { RBX::ServiceProvider::create(m_PlayGame.m_Game->getDataModel().get())->run(); return; } } } // If the Edit game has been run and is currently paused, then resume it. if (m_EditGame.m_Game && m_EditGame.m_Game->getDataModel()) { { RBX::RunService* pRunState = m_EditGame.m_Game->getDataModel()->find(); RBX::DataModel::LegacyLock lock(m_EditGame.m_Game->getDataModel(),RBX::DataModelJob::Write); if(pRunState && pRunState->getRunState() == RBX::RS_PAUSED) { pRunState->run(); return; } } } if (!isTeleport) { clearOutputWidget(); updateOnSimulationStateChange(true); } autoSave(true); UpdateUIManager::Instance().setBusy(true); static const int ProgressStages = 6; RBX::BaldPtr progressBar = UpdateUIManager::Instance().showProgress("Play Game",0,ProgressStages); // Stage 0 - initialize game UpdateUIManager::Instance().updateProgress(progressBar,0); deActivate(); // We never hide plugins in ribbon mode but it is requierd for NPS if (m_pMainWindow->isRibbonStyle()) { RBX::PluginManager::singleton()->hideStudioUI(m_EditGame.m_Game->getDataModel().get(),true); } if (m_EditGame.m_View) { // remove video recording verb since it's associated with view cleanupVideoRecording(m_EditGame); delete m_EditGame.m_View; m_EditGame.m_View = NULL; } QString debugDataFile = getDebugInfoFile(false, PlaySoloDebugFileExt); m_CurrentGame = &m_PlayGame; if (play) { StudioUtilities::setAvatarMode(true); } if (!m_PlayGame.m_Game) { if (FFlag::LuaDebugger) { // save edit model debugger data so it can be merged with play model changes later saveEditModelDebuggerData(); } QString baseUrl = RobloxSettings::getBaseURL() + "/"; m_PlayGame.m_Game.reset(new RBX::UnsecuredStudioGame(NULL,baseUrl.toStdString().c_str())); shared_ptr playDataModel = m_PlayGame.m_Game->getDataModel(); RBX::DataModel::LegacyLock lock(m_PlayGame.m_Game->getDataModel(), RBX::DataModelJob::Write); createOptionalDMServices(); playDataModel->setName(displayName().toStdString()); m_PlayGame.m_View = new RobloxView(m_pQOgreWidget,m_PlayGame.m_Game, m_GameView, /* vr = */ true); m_PlayGame.m_View->buildGui(StudioUtilities::isAvatarMode()); createActionIDVerbMap(m_PlayGame); // initialize video recording initializeVideoRecording(m_PlayGame); m_PlayGame.m_TreeWidget = new RobloxTreeWidget(playDataModel); UpdateUIManager::Instance().getViewWidget(eDW_OBJECT_EXPLORER).setCurrentWidget(m_PlayGame.m_TreeWidget); // restore in-game stats display QSettings settings; setStatsEnabled(StatsVerb,settings.value(StatsVerb,false).toBool(), m_pMainWindow->testStatsAction); setStatsEnabled(RenderStatsVerb,settings.value(RenderStatsVerb,false).toBool(), m_pMainWindow->testRenderStatsAction); setStatsEnabled(PhysicsStatsVerb,settings.value(PhysicsStatsVerb,false).toBool(), m_pMainWindow->testPhysicsStatsAction); setStatsEnabled(NetworkStatsVerb,settings.value(NetworkStatsVerb,false).toBool(), m_pMainWindow->testNetworkStatsAction); setStatsEnabled(SummaryStatsVerb,settings.value(SummaryStatsVerb,false).toBool(), m_pMainWindow->testSummaryStatsAction); if (FFlag::StudioCustomStatsEnabled) { setStatsEnabled(CustomStatsVerb,settings.value(CustomStatsVerb,false).toBool(), m_pMainWindow->testCustomStatsAction); } RBX::UserInputService* userInputService = playDataModel->find(); userInputService->setMouseEnabled(true); userInputService->setKeyboardEnabled(true); } m_pQOgreWidget->setRobloxView(m_PlayGame.m_View); setDockViewsEnabled(true); activate(); // Stage 1 - save edit world UpdateUIManager::Instance().updateProgress(progressBar, 1); std::stringstream ss; int placeId, universeId; if (cloneDataModel) { RBX::DataModel::LegacyLock editLock(dataModel,RBX::DataModelJob::Write); applyScriptChanges(); RBX::SerializerBinary::serialize(ss, dataModel.get()); placeId = dataModel->getPlaceID(); universeId = dataModel->getUniverseId(); } // Stage 2 - load play world UpdateUIManager::Instance().updateProgress(progressBar, 2); shared_ptr playDataModel = m_PlayGame.m_Game->getDataModel(); { RBX::DataModel::LegacyLock playLock(playDataModel,RBX::DataModelJob::Write); RBX::Security::Impersonator impersonate(RBX::Security::LocalGUI_); bool oldStudioMode = RBX::GameBasicSettings::singleton().inStudioMode(); RBX::GameBasicSettings::singleton().setStudioMode(false); if (cloneDataModel) { RBX::SerializerBinary::deserialize(ss, playDataModel.get()); playDataModel->processAfterLoad(); } // only need to load linked scripts here when we are cloning (not teleporting) if (cloneDataModel) { playDataModel->setPlaceID(placeId, false /*not a roblox place*/); playDataModel->setUniverseId(universeId); LuaSourceContainer::blockingLoadLinkedScripts( playDataModel->create(), playDataModel.get()); } tDebuggersMap playModelDebuggers; for (tDebuggersMap::const_iterator iter = m_EditDebuggersMap.begin(); iter != m_EditDebuggersMap.end(); ++iter) { std::vector indexHierarchy; std::string serviceName = getScriptIndexHierarchy(iter->first, indexHierarchy); shared_ptr instance = getScriptByIndexHierarchy(playDataModel, indexHierarchy, serviceName); if (instance) playModelDebuggers[instance] = iter->second; } initializeDebuggerData("", playDataModel, playModelDebuggers); RobloxPluginHost::loadPlugins(playDataModel.get()); RBX::GameBasicSettings::singleton().setStudioMode(oldStudioMode); } if (DFFlag::UseR15Character && playDataModel->getUniverseDataRequested()) { playDataModel->universeDataLoaded.get_future().wait(); } // make sure we patch scripts before calling RunService // otherwise it will create a 'Player1' instance inside Workspace, leading to a mismatch in index hierarchy patchPlayModelScripts(); // Stage 3 - set up local player UpdateUIManager::Instance().updateProgress(progressBar, 3); if (play) { RBXASSERT(m_CurrentGame == &m_PlayGame); if (!url.isEmpty()) { handleScriptCommand(url); } } else { RBX::DataModel::LegacyLock playLock(playDataModel,RBX::DataModelJob::Write); RBX::ServiceProvider::create(playDataModel.get())->run(); } UpdateUIManager::Instance().hideProgress(progressBar); UpdateUIManager::Instance().setBusy(false); m_pQOgreWidget->setVisible(true); RobloxDocManager::Instance().setCurrentDoc(this); UpdateUIManager::Instance().updateToolBars(); } void RobloxIDEDoc::closePlayDataModel() { if (!m_PlayGame.m_Game.get()) return; QString debugDataFile = getDebugInfoFile(false, PlaySoloDebugFileExt); applyScriptChanges(); if (saveScriptsChangedWhileTesting() && FFlag::LuaDebugger) updateEditModelDebuggerData(); restoreEditModelScripts(); cleanupVideoRecording(m_PlayGame); deActivate(); m_pQOgreWidget->setRobloxView(m_EditGame.m_View); if (FFlag::LuaDebugger) DebuggerClientManager::Instance().onRunTransition(RBX::RunTransition(RBX::RS_STOPPED, RBX::RS_STOPPED)); cleanupGameState(m_PlayGame); { RBX::DataModel::LegacyLock lock(m_EditGame.m_Game->getDataModel(), RBX::DataModelJob::Write); RBXASSERT(!m_EditGame.m_View); m_EditGame.m_View = new RobloxView(m_pQOgreWidget,m_EditGame.m_Game, m_GameView); //since we are creating a new view we will be required to reinitialize video recording initializeVideoRecording(m_EditGame); } m_CurrentGame = &m_EditGame; setDockViewsEnabled(true); activate(); // We never hide plugins in ribbon mode but it is requierd for NPS if (m_pMainWindow->isRibbonStyle()) { RBX::PluginManager::singleton()->hideStudioUI(m_CurrentGame->m_Game->getDataModel().get(), false); } m_CurrentGame->m_View->requestUpdateView(); if (isCloudEditSession()) { m_CurrentGame->m_TreeWidget->requestUpdate(); } StudioUtilities::setAvatarMode(false); { RBX::DataModel::LegacyLock lock(m_EditGame.m_Game->getDataModel(), RBX::DataModelJob::Write); RBX::GameBasicSettings::singleton().setStudioMode(true); // Turn off the frame rate manager when we're editing. CRenderSettingsItem::singleton().setEnableFRM(false); } // again reinitialize the data for original data model if (FFlag::LuaDebugger) { initializeDebuggerData("", m_EditGame.m_Game->getDataModel(), m_EditDebuggersMap); m_EditDebuggersMap.clear(); if (RobloxDocManager::Instance().getCurrentDoc() != this) RobloxDocManager::Instance().getCurrentDoc()->activate(); } } void RobloxIDEDoc::onIdeRun(bool play) { shared_ptr dataModel = m_EditGame.m_Game->getDataModel(); // We receive a crash down the stack with m_GameView equal to NULL. One explanation is somehow user pressed onIdeRun before first paint message made it // Lets try to catch it with assert RBXASSERT(m_GameView); if(m_GameView == NULL) return; QString scriptStr; if (FFlag::UseBuildGenericGameUrl) { QString pathStr = QString("game/visit.ashx?IsPlaySolo=1&UserID=%1&PlaceID=%2&universeId=%3") .arg(RobloxUser::singleton().getUserId()) .arg(m_EditGame.m_Game->getDataModel()->getPlaceID()) .arg(m_EditGame.m_Game->getDataModel()->getUniverseId()); scriptStr = QString("loadfile(\"%1\")()\n") .arg(QString::fromStdString(BuildGenericGameUrl(RobloxSettings::getBaseURL().toStdString(),pathStr.toStdString()))); } else { scriptStr = QString("loadfile(\"%1/game/visit.ashx?IsPlaySolo=1&UserID=%2&PlaceID=%3&universeId=%4\")()\n") .arg(RobloxSettings::getBaseURL()) .arg(RobloxUser::singleton().getUserId()) .arg(m_EditGame.m_Game->getDataModel()->getPlaceID()) .arg(m_EditGame.m_Game->getDataModel()->getUniverseId()); } loadPlayDataModel(scriptStr, play, true); if (DFFlag::ScriptExecutionContextApi) { if (!play) { if (shared_ptr playDataModel = m_PlayGame.m_Game->getDataModel()) { playDataModel->setIsRunMode(true); } } } } bool RobloxIDEDoc::isIdeRunEnabled(bool play) { if (m_PlayGame.m_Game) { if (play != StudioUtilities::isAvatarMode()) { return false; } shared_ptr playDataModel = m_PlayGame.m_Game->getDataModel(); RBX::RunService* pRS = playDataModel->create(); return pRS->getRunState() != RBX::RS_RUNNING; } else { shared_ptr dataModel = m_EditGame.m_Game->getDataModel(); RBX::RunService* pRS = dataModel->create(); bool clientIsPresent = RBX::Network::Players::clientIsPresent(dataModel.get()) && !isCloudEditSession(); return !clientIsPresent && (pRS->getRunState() != RBX::RS_RUNNING); } } void RobloxIDEDoc::onIdeReset() { if (m_PlayGame.m_Game) { closePlayDataModel(); } else { shared_ptr dataModel = m_EditGame.m_Game->getDataModel(); RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Write); dataModel->create()->stop(); dataModel->create()->reset(); } updateOnSimulationStateChange(false); } bool RobloxIDEDoc::isIdeResetEnabled() { return m_PlayGame.m_Game; } bool RobloxIDEDoc::isCloudEditSession() { return m_IsCloudEditSession.get(); } bool RobloxIDEDoc::getIsCloudEditSession() { if (RobloxIDEDoc* ideDoc = RobloxDocManager::Instance().getPlayDoc()) { return ideDoc->isCloudEditSession(); } return false; } bool RobloxIDEDoc::isObjectRenameEnabled() { QList selectedItems = m_CurrentGame->m_TreeWidget->selectedItems(); if (selectedItems.size() == 1) return true; return false; } void RobloxIDEDoc::onObjectRename() { if (m_CurrentGame->m_TreeWidget->currentItem()) m_CurrentGame->m_TreeWidget->editItem(m_CurrentGame->m_TreeWidget->currentItem()); } RBX::Verb* RobloxIDEDoc::getVerb(const QString& name) const { RBXASSERT(m_CurrentGame); return getVerb(*m_CurrentGame, name); } RBX::Verb* RobloxIDEDoc::getVerb(GameState& gameState,const QString& name) const { std::string stdname = name.toStdString(); for (int i = 0; i < gameState.m_Verbs.size() ; ++i) { if (gameState.m_Verbs[i]->getName().str == stdname) return gameState.m_Verbs[i]; } return NULL; } bool RobloxIDEDoc::autoSave(bool force) { FASTLOG1(FLog::AutoSave, "Auto-saving, force: %u", force); // don't save while we're running if (isSimulating()) return true; // don't save if running Play Solo, Start Server, Start Player if (StudioUtilities::isTestMode()) return true; // don't save unless we need to if (!m_EditGame.m_Game || !m_EditGame.m_Game->getDataModel() || !m_EditGame.m_Game->getDataModel()->isDirty()) { FASTLOG(FLog::AutoSave, "DM is not dirty, skipping"); return true; } if (force) { // force wait for a lock - this can cause a lockup if we're already loading or saving FASTLOG(FLog::AutoSave, "Locking mutex"); m_IOMutex.lock(); } else if (!force && !m_IOMutex.tryLock()) { // if we're saving or loading, just skip this auto-save for now FASTLOG(FLog::AutoSave, "Mutex is locked, skipping"); return true; } QByteArray currentDataModelHash; try { currentDataModelHash = StudioSerializerHelper::getDataModelHash(m_EditGame.m_Game->getDataModel().get()); if (m_autoSaveDataModelHash == currentDataModelHash) { FASTLOG(FLog::AutoSave, "DM hash is same, skipping"); m_IOMutex.unlock(); return true; } if (m_AutoSaveName.isEmpty()) { QString fileName; if (m_fileName.isEmpty() || !QFile::exists(m_fileName)) fileName = "Place"; else fileName = QFileInfo(m_fileName).baseName(); FASTLOGS(FLog::AutoSave, "AutoSave name is empty, constructing filename: %s", fileName.toStdString()); QDir dir; QString autoSavePath = AuthoringSettings::singleton().autoSaveDir.absolutePath(); if (!dir.mkpath(autoSavePath)) { FASTLOGS(FLog::AutoSave, "Can't make path: %s", autoSavePath.toStdString()); m_IOMutex.unlock(); return false; } QString fullFileName = QString("%1/%2_AutoSave_%3.rbxl"). arg(autoSavePath). arg(fileName); int count = 0; while (QFile::exists(fullFileName.arg(count)) && count < 100) ++count; FASTLOGS(FLog::AutoSave, "Full filename: %s", fullFileName.toStdString()); RBXASSERT(count < 100); m_AutoSaveName = fullFileName.arg(count); } m_IOMutex.unlock(); } catch(...) { FASTLOG(FLog::AutoSave, "Error on filename construction"); m_IOMutex.unlock(); return false; } try { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Auto-Saving..."); bool result = saveAs(m_AutoSaveName); if (result) m_autoSaveDataModelHash = currentDataModelHash; return result; } catch(...) { FASTLOG(FLog::AutoSave, "Error on AutoSave"); return false; } } /** * Set flag indicating that this place was loaded from an autosave file. * The autosave file will not be deleted at close unless the file is saved with a different * file name than the autosave name. */ void RobloxIDEDoc::setAutoSaveLoad() { FASTLOGS(FLog::AutoSave, "File was loaded from autosave, name: %s", m_fileName.toStdString()); m_AutoSaveName = m_fileName; m_fileName.clear(); m_IsAutoSaveLoad = true; } void RobloxIDEDoc::resetDirty(RBX::DataModel* dataModel) { if (!dataModel) return; m_initialDataModelHash = StudioSerializerHelper::getDataModelHash(dataModel); dataModel->setDirty(false); m_RequiresSave = false; } /** * Return the list of file filters for Open/Save dialogs */ const char* RobloxIDEDoc::getOpenFileFilters() { return "All Roblox Place Files (*rbxl *rbxlx);;Roblox Place Files (*.rbxl);;Roblox XML Place Files (*.rbxlx)"; } const char* RobloxIDEDoc::getSaveFileFilters() { return "Roblox Place Files (*.rbxl);;Roblox XML Place Files (*.rbxlx)"; } /** * Toggle in-game stats. * * @param stat stats verb name */ void RobloxIDEDoc::toggleStats(const QString& stats) { RBX::Verb* verb = m_CurrentGame->m_Game->getDataModel()->getVerb(stats.toStdString()); setStatsEnabled(stats,!verb->isChecked()); } /** * Enable/disable in-game stats displays. * * @param stat stats verb name * @param enabled true to show */ void RobloxIDEDoc::setStatsEnabled(const QString& stats, bool enabled, QAction* verbAction) { RBX::Verb* verb = m_CurrentGame->m_Game->getDataModel()->getVerb(stats.toStdString()); if (verb->isChecked() != enabled) { verb->doIt(m_CurrentGame->m_Game->getDataModel().get()); if (verbAction) verbAction->setChecked(verb->isChecked()); } QSettings settings; settings.setValue(stats,enabled); } bool RobloxIDEDoc::displayAskConvertPlaceToNewMaterialsIfInsertNewModel() { bool conversionDialogDisabled = RobloxSettings().value(kMaterialPropertiesConvertDialogDisabled, false).toBool(); if (!conversionDialogDisabled) { // Create Dialog Box std::string labelMessage = std::string("The model you are inserting was designed to work with the new Physical Properties system.") + "\n\n To make this model work with your place, would you like to convert the place to work with new Physical Properties?\n"; QtUtilities::RBXConfirmationMessageBox msgBox( QObject::tr(labelMessage.c_str()), QObject::tr("Convert Place"), QObject::tr("Do Not Convert") ); msgBox.setMinimumWidth(350); msgBox.setMinimumHeight(90); // Execute Dialog Box if (FFlag::CLI10590_FixConversionDialog) { UpdateUIManager::Instance().setBusy(true, false); QApplication::setOverrideCursor(Qt::ArrowCursor); msgBox.exec(); QApplication::restoreOverrideCursor(); UpdateUIManager::Instance().setBusy(false, false); } else { msgBox.exec(); } // Process Dialog Response bool convertResponse = false; if (msgBox.getClickedYes()) { convertResponse = true; } if (msgBox.getCheckBoxState()) { RobloxSettings().setValue(kMaterialPropertiesConvertDialogDisabled, true); RobloxSettings().setValue(kMaterialPropertiesConvertByDefault, convertResponse); } return convertResponse; } return RobloxSettings().value(kMaterialPropertiesConvertByDefault, false).toBool(); } void RobloxIDEDoc::onWorkspacePropertyChanged(const RBX::Reflection::PropertyDescriptor* pDescriptor) { if (pDescriptor->name == RBX::Workspace::prop_physicalPropertiesMode.name) { switch (getEditDataModel()->getWorkspace()->getPhysicalPropertiesMode()) { case PhysicalPropertiesMode_Legacy: RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "Physical Properties Mode", "Legacy"); break; case PhysicalPropertiesMode_Default: RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "Physical Properties Mode", "Default"); break; case PhysicalPropertiesMode_NewPartProperties: RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "Physical Properties Mode", "New"); break; } if (DFFlag::WorkspaceNotificationMasterEnable || DFInt::StudioWorkspaceNotificationLevel) displayWorkspaceMessage(); if (getEditDataModel()->getWorkspace()->getUsingNewPhysicalProperties()) { if (DataModel* dm = DataModel::get(getEditDataModel()->getWorkspace())) { RBX::PartInstance::convertToNewPhysicalPropRecursive(dm); } } } } static void createDebuggerClient(shared_ptr instance, RBX::Scripting::DebuggerManager* pDebuggerManager) { if (!instance || instance->isDescendantOf(RBX::ServiceProvider::find(instance.get()))) return; shared_ptr pScript = RBX::Instance::fastSharedDynamicCast(instance); if (pScript) { //RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Script Name: %s", pScript->getName().c_str()); pDebuggerManager->addDebugger(pScript.get()); } else { shared_ptr pModuleScript = RBX::Instance::fastSharedDynamicCast(instance); if (pModuleScript) pDebuggerManager->addDebugger(pModuleScript.get()); } } void RobloxIDEDoc::initializeDebuggerData(const QString &debugFile, const shared_ptr dm, const tDebuggersMap& debuggersMap) { if (!FFlag::LuaDebugger || (m_pMainWindow->getBuildMode() == BM_BASIC)) return; RBX::Scripting::DebuggerManager& debuggerManager = RBX::Scripting::DebuggerManager::singleton(); // before setting data model make sure we 'cleanup' client manager to avoid receiving unnecessary signals being called if (dm != DebuggerClientManager::Instance().getDataModel()) DebuggerClientManager::Instance().setDataModel(shared_ptr()); //set data model (so we can resolve references and save debugger data) debuggerManager.setDataModel(dm.get()); //connect with debugger client manager also DebuggerClientManager::Instance().setDataModel(dm); //load debugger data if (debugFile.isEmpty()) { updateDebuggerManager(debuggersMap); } else { QFileInfo debugFileInfo(debugFile); if (debugFileInfo.exists()) { //deserialize data std::ifstream stream(qPrintable(debugFile), std::ios_base::in | std::ios_base::binary); try { RBX::SerializerBinary::deserialize(stream, &debuggerManager); } catch (std::runtime_error const&) { stream.close(); QFile::remove(debugFile); //remove invalid debug info file //remove all children from debug manager and again set datamodel debuggerManager.setDataModel(NULL); debuggerManager.setDataModel(dm.get()); } } } dm->visitDescendants(boost::bind(&createDebuggerClient, _1, &debuggerManager)); } /** * Sets the current grid snap mode. * * @param gridMode new grid snap mode */ void RobloxIDEDoc::setDraggerGridMode(RBX::DRAG::DraggerGridMode gridMode) { if ( gridMode == RBX::DRAG::ONE_STUD ) m_pMainWindow->gridToOneAction->activate(QAction::Trigger); else if ( gridMode == RBX::DRAG::QUARTER_STUD ) m_pMainWindow->gridToOneFifthAction->activate(QAction::Trigger); else m_pMainWindow->gridToOffAction->activate(QAction::Trigger); } /** * Sets the current 3d grid size. * * @param gridSize new 3d grid size */ void RobloxIDEDoc::setGridSize(int gridSize) { if ( gridSize == 2 ) m_pMainWindow->gridSizeToTwoAction->activate(QAction::Trigger); else if ( gridSize == 16 ) m_pMainWindow->gridSizeToSixteenAction->activate(QAction::Trigger); else m_pMainWindow->gridSizeToFourAction->activate(QAction::Trigger); } static void placeNameLoadedHelperSuccess(QPointer doc, std::string json) { if (doc.isNull() || json.empty()) return; QMetaObject::invokeMethod(doc, "placeNameLoaded", Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(json))); } static void placeNameLoadedHelperError(QPointer doc, std::string error) { // didn't get name, just do nothing for now } void RobloxIDEDoc::placeNameLoaded(QString json) { RBX::Reflection::Variant v; if (RBX::WebParser::parseJSONObject(json.toStdString(), v)) { m_windowTitlePrefix = isCloudEditSession() ? kCloudEditPlacePrefix : kPublishedPlacePrefix; updateDisplayName(QString::fromStdString(v.cast >()->at("Name").cast())); } } void RobloxIDEDoc::searchToolboxByDefaultSearchString() { RBXASSERT(m_EditGame.m_Game->getDataModel()->write_requested); if (FFlag::SearchToolboxByDataModelSearchString) if (Instance* defaultToolboxSearchInstance = m_EditGame.m_Game->getDataModel()->findFirstChildByName("DefaultToolboxSearch")) if (StringValue* stringValue = RBX::Instance::fastDynamicCast(defaultToolboxSearchInstance)) { QMetaObject::invokeMethod(&UpdateUIManager::Instance().getViewWidget(eDW_TOOLBOX), "loadUrl", Qt::QueuedConnection, Q_ARG(QString, QString("%1/%2%3").arg(RobloxSettings::getBaseURL()).arg("ide/clienttoolbox?searchset=").arg(stringValue->getValue().c_str()))); } } /** * Slot called on emit of "workspaceLoadedSignal" signal */ void RobloxIDEDoc::onContentDataLoaded() { // required only at the time of loading m_contentDataLoadedConnection.disconnect(); if (DFFlag::MaterialPropertiesEnabled) { RBXASSERT(!m_workspacePropertyChangedConnection.connected()); m_workspacePropertyChangedConnection = getEditDataModel()->getWorkspace()->propertyChangedSignal.connect(boost::bind(&RobloxIDEDoc::onWorkspacePropertyChanged, this, _1)); } //Migrating CSG childData CSGDictionaryService* dictionaryService = ServiceProvider::create(m_EditGame.m_Game->getDataModel().get()); ChangeHistoryService* changeHistory = ServiceProvider::create(m_EditGame.m_Game->getDataModel().get()); dictionaryService->reparentAllChildData(); changeHistory->resetBaseWaypoint(); if (FFlag::CSGPhysicsLevelOfDetailEnabled) { RBXASSERT(!m_dataModelItemChangedConnection.connected()); m_dataModelItemChangedConnection = getEditDataModel()->itemChangedSignal.connect(boost::bind(&RobloxIDEDoc::onDMItemChanged, this, _1, _2)); } // delay initialization of debugger data // while loading of contentData "Message" instance is created and once loading is done is gets deleted, // but it's presence affects the child count in turn resulting in error on getting script from child number QTimer::singleShot(0, this, SLOT(onDelayedInitializeDebuggerData())); updateFromPlaceID(getDataModel()->getPlaceID()); QMetaObject::invokeMethod(&UpdateUIManager::Instance().getViewWidget(eDW_GAME_EXPLORER), "openGameFromPlaceId", Qt::QueuedConnection, Q_ARG(int, m_EditGame.m_Game->getDataModel()->getPlaceID())); searchToolboxByDefaultSearchString(); } /** * Slot called on emit of "saveFinishedSignal" signal */ void RobloxIDEDoc::onContentDataSaved() { // save debugger data StudioSerializerHelper::saveDebuggerData(getDebugInfoFile()); } /** * Delayed initialization of debugger data */ void RobloxIDEDoc::onDelayedInitializeDebuggerData() { shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); // initialize debugger data initializeDebuggerData(getDebugInfoFile(), dataModel); // connect with saved data signal, so we can save the debug data dataModel->saveFinishedSignal.connect(boost::bind(&RobloxIDEDoc::onContentDataSaved, this)); } /** * Gets upload URL for current datamodel */ QString RobloxIDEDoc::getUploadURL() { if (m_CurrentGame->m_Game && m_CurrentGame->m_Game->getDataModel()) { if(RBX::Visit* pVisit = RBX::ServiceProvider::find(m_CurrentGame->m_Game->getDataModel().get())) return pVisit->getUploadUrl().c_str(); } return QString(); } /** * Gets debug info file * * @param isAutoSaveFile true, if we need debug info file corresponding to auto save file * @param debuggerFileExt extension to be added with the file name */ QString RobloxIDEDoc::getDebugInfoFile(bool isAutoSaveFile, const QString& debuggerFileExt) { QString fileName; if (isAutoSaveFile) { // if we want to have autosave file name fileName = m_AutoSaveName; } else { QString uploadURL = getUploadURL(); if (!uploadURL.isEmpty()) { // get name based on the URL and assetID (used in EDIT mode) fileName = uploadURL; } else if (m_fileName.isEmpty()) { // if fileName is empty then get the display name fileName = displayName(); } else { // local file on disk fileName = m_fileName; } } return StudioUtilities::getDebugInfoFile(fileName, debuggerFileExt); } void RobloxIDEDoc::notifyScriptEdited(shared_ptr script) { if (script) { m_playModifiedScriptList.push_back(new PlayModifiedScriptItem()); PlayModifiedScriptItem& item = *m_playModifiedScriptList.back(); item.instance = script; item.ancestryChangedConnection = script->ancestryChangedSignal.connect( boost::bind(&RobloxIDEDoc::onPlayModifiedAncestryChanged, this, script, _2)); } } void RobloxIDEDoc::onPlayModifiedAncestryChanged(shared_ptr script, shared_ptr newParent) { if (newParent==NULL) { for (tPlayModifiedScriptList::iterator iter = m_playModifiedScriptList.begin(); iter != m_playModifiedScriptList.end(); ++iter) { if ((*iter)->instance.get() == script.get()) { delete (*iter); m_playModifiedScriptList.erase(iter); return; } } } } void RobloxIDEDoc::clearPlayModifiedScriptList() { for (tPlayModifiedScriptList::iterator iter = m_playModifiedScriptList.begin(); iter != m_playModifiedScriptList.end(); ++iter) { delete (*iter); } m_playModifiedScriptList.clear(); } void RobloxIDEDoc::addScriptMapping(shared_ptr script, shared_ptr dataModel) { if (!script) return; std::vector indexHierarchy; std::string serviceName = getScriptIndexHierarchy(script, indexHierarchy); shared_ptr instance = getScriptByIndexHierarchy(dataModel, indexHierarchy, serviceName); if (instance) m_scriptMap[instance.get()] = script; } std::string RobloxIDEDoc::getScriptIndexHierarchy(shared_ptr script, std::vector& indexHierarchy) { if (!script) return ""; indexHierarchy.clear(); const RBX::Instance* pParent = script->getParent(); const RBX::Instance* pChild = script.get(); while (pParent) { indexHierarchy.push_back(pParent->findChildIndex(pChild)); // if this parent is a service and a child of the DataModel. if (pParent && dynamic_cast(pParent) && pParent->getParent() && pParent->getParent()->getParent() == NULL) { return pParent->getClassNameStr(); } pChild = pParent; pParent = pChild->getParent(); } return ""; } shared_ptr RobloxIDEDoc::getScriptByIndexHierarchy(shared_ptr dataModel, const std::vector& indexHierarchy, const std::string& serviceName) { if (serviceName == "") return shared_ptr(); shared_ptr pParent = shared_from(dataModel->findFirstChildOfType(serviceName)); shared_ptr pChild; if (indexHierarchy.size() == 0) return shared_ptr(); for (int i = indexHierarchy.size() - 1; i >= 0; i--) { if (!pParent) return shared_ptr(); int childNum = indexHierarchy[i]; if (childNum >= (int)pParent->numChildren()) return shared_ptr(); pChild = shared_from(pParent->getChild(childNum)); pParent = pChild; } if (pChild && (pChild->isA() || pChild->isA())) return pChild; return shared_ptr(); } boost::shared_ptr RobloxIDEDoc::getEditScriptByPlayInstance(RBX::Instance* instance) { tScriptMap::iterator iter = m_scriptMap.find(instance); if (iter != m_scriptMap.end()) return iter->second; return boost::shared_ptr(); } void RobloxIDEDoc::initializeVideoRecording(GameState& gameState) { #ifdef _WIN32 if (gameState.m_View && m_pQOgreWidget) { RBXASSERT(!gameState.m_RecordToggleVerb); gameState.m_RecordToggleVerb = new RecordToggleVerb(gameState.m_Game->getDataModel().get(), &gameState.m_View->getViewBase()); gameState.m_Verbs.append(gameState.m_RecordToggleVerb); mapActionIDWithVerb(*m_CurrentGame, "toggleVideoRecordAction", gameState.m_RecordToggleVerb); } #endif } void RobloxIDEDoc::cleanupVideoRecording(GameState& gameState) { #ifdef _WIN32 if (gameState.m_RecordToggleVerb) { if (gameState.m_RecordToggleVerb->isRecording()) gameState.m_RecordToggleVerb->stopRecording(false); gameState.m_Verbs.remove(gameState.m_Verbs.indexOf(gameState.m_RecordToggleVerb.get())); delete gameState.m_RecordToggleVerb; gameState.m_RecordToggleVerb = NULL; } #endif } void RobloxIDEDoc::saveEditModelDebuggerData() { if (m_EditGame.m_Game && m_EditGame.m_Game->getDataModel()) { RBX::DataModel* dm = m_EditGame.m_Game->getDataModel().get(); dm->visitDescendants(boost::bind(&RobloxIDEDoc::addToDebuggersMap, this, _1, boost::ref(m_EditDebuggersMap))); } } void RobloxIDEDoc::updateEditModelDebuggerData() { if (m_PlayGame.m_Game && m_PlayGame.m_Game->getDataModel()) { RBX::DataModel* dm = m_PlayGame.m_Game->getDataModel().get(); dm->visitDescendants(boost::bind(&RobloxIDEDoc::updateDebuggersMap, this, _1, boost::ref(m_EditDebuggersMap))); } } void RobloxIDEDoc::updateDebuggerManager(const tDebuggersMap& debuggers) { // recreate all debuggers based upon the saved mapping RBX::Scripting::DebuggerManager& debuggerManager = RBX::Scripting::DebuggerManager::singleton(); boost::shared_ptr debugger; boost::shared_ptr breakpoint; for (tDebuggersMap::const_iterator iter = debuggers.begin(); iter != debuggers.end(); ++iter) { debugger = debuggerManager.addDebugger(iter->first.get()); if (debugger) { const std::list& breakpoints = iter->second.breakpoints; for (std::list::const_iterator bp_iter = breakpoints.begin(); bp_iter != breakpoints.end(); ++bp_iter) { breakpoint = debugger->setBreakpoint(bp_iter->line); if (breakpoint) RBX::Scripting::DebuggerBreakpoint::prop_Enabled.setValue(breakpoint.get(), bp_iter->enabled); } const std::list& watches = iter->second.watches; for (std::list::const_iterator w_iter = watches.begin(); w_iter != watches.end(); ++w_iter) { debugger->addWatch(*w_iter); } } } } void RobloxIDEDoc::addToDebuggersMap(shared_ptr instance, tDebuggersMap& debuggersMap) { if (RBX::Instance::fastSharedDynamicCast(instance) || RBX::Instance::fastSharedDynamicCast(instance)) { RBX::Scripting::DebuggerManager& debuggerManager = RBX::Scripting::DebuggerManager::singleton(); if (RBX::Scripting::ScriptDebugger* debugger = debuggerManager.findDebugger(instance.get())) debuggersMap[instance] = getDebuggerData(debugger); } } void RobloxIDEDoc::updateDebuggersMap(shared_ptr instance, tDebuggersMap& debuggersMap) { if (RBX::Instance::fastSharedDynamicCast(instance) || RBX::Instance::fastSharedDynamicCast(instance)) { shared_ptr editScript = getEditScriptByPlayInstance(instance.get()); if (editScript) { RBX::Scripting::DebuggerManager& debuggerManager = RBX::Scripting::DebuggerManager::singleton(); if (RBX::Scripting::ScriptDebugger* debugger = debuggerManager.findDebugger(instance.get())) debuggersMap[editScript] = getDebuggerData(debugger); } } } RobloxIDEDoc::DebuggerData RobloxIDEDoc::getDebuggerData(RBX::Scripting::ScriptDebugger* debugger) { DebuggerData data; const Scripting::ScriptDebugger::Breakpoints& breakpoints = debugger->getBreakpoints(); for (RBX::Scripting::ScriptDebugger::Breakpoints::const_iterator iter = breakpoints.begin(); iter != breakpoints.end(); ++iter) { DebuggerData::BreakpointData bp; bp.line = (iter->second->getLine()); bp.enabled = (iter->second->isEnabled()); data.breakpoints.push_back(bp); } data.breakpoints.sort(RobloxIDEDoc::DebuggerData::BreakpointLineNumberOrder()); const Scripting::ScriptDebugger::Watches& watches = debugger->getWatches(); for (RBX::Scripting::ScriptDebugger::Watches::const_iterator iter = watches.begin(); iter != watches.end(); ++iter) { data.watches.push_back((*iter)->getCondition()); } return data; } bool RobloxIDEDoc::isEditMode(const RBX::Instance* context) { // serverIsPresent can return 'true' as soon as query for 'NetworkServer' service happens // so explictly checking if we've actually launched Studio in server mode? if (RobloxApplicationManager::instance().hasLocalServer()) return false; return !RBX::Network::Players::clientIsPresent(context) && !RBX::Network::Players::findConstLocalPlayer(context); } void RobloxIDEDoc::forceViewSize(QSize viewSize) { if (!m_pQOgreWidget) return; if (!viewSize.isNull()) m_pQOgreWidget->setFixedSize(viewSize); else { m_pQOgreWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); m_pQOgreWidget->setMinimumSize(100, 100); m_pQOgreWidget->setMaximumSize(20000, 20000); } m_WrapperWidget->setMinimumSize(m_pQOgreWidget->minimumSize()); } void RobloxIDEDoc::updateDisplayName(const QString& newDisplayName) { if (m_displayName == newDisplayName) return; m_displayName = newDisplayName; refreshDisplayName(); } void RobloxIDEDoc::refreshDisplayName() { RobloxDocManager::Instance().setDocTitle(*this, displayName(), titleTooltip(), titleIcon()); } void RobloxIDEDoc::forceReloadImages(const QStringList& contentIds) { if (!m_CurrentGame->m_View || !m_CurrentGame->m_Game || !m_CurrentGame->m_Game->getDataModel()) return; RBX::ViewBase* pView = (&m_CurrentGame->m_View->getViewBase()); shared_ptr dm(m_CurrentGame->m_Game->getDataModel()); { RBX::DataModel::LegacyLock lock(dm, RBX::DataModelJob::Write); // Garbage collect calls clear content if the visual engine still has a ContentProvider, // but the visual engine's ContentProvider is cleared when the workspace is set to null. if (ContentProvider* cp = dm->find()) { for (QStringList::const_iterator itr = contentIds.begin(); itr != contentIds.end(); ++itr) { cp->invalidateCache(ContentId(itr->toStdString())); } } for (int i = 0; i < contentIds.size(); ++i) { std::string contentId = contentIds.at(i).toStdString(); pView->immediateAssetReload(contentId); } // This will trigger a full regeneration of geometry and refetching of textures. // Calling immediateAssetReload also causes the reload to retry failed textures. // Ideally immediateAssetReload could do a more targeted invalidation/regeneration eventually. pView->bindWorkspace(shared_ptr()); pView->garbageCollect(); pView->bindWorkspace(dm); } } const QIcon& RobloxIDEDoc::titleIcon() const { static QIcon icon; if (icon.isNull()) icon.addFile(QString::fromUtf8(":/images/onlinePlaceIcon.png"), QSize(), QIcon::Normal, QIcon::Off); if (DataModel* dm = getDataModel()) { if (dm->getPlaceID() != 0) { return icon; } } return m_emptyIcon; } void RobloxIDEDoc::updateFromPlaceID(int placeId) { if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find(getDataModel())) { m_bIsLocalDocument = false; apiService->getAsync( RBX::format("marketplace/productinfo?assetId=%d", placeId),false, RBX::PRIORITY_DEFAULT, boost::bind(&placeNameLoadedHelperSuccess, QPointer(this), _1), boost::bind(&placeNameLoadedHelperError, QPointer(this), _1)); } } const QString& RobloxIDEDoc::titleTooltip() const { if (!m_fileName.isEmpty()) return m_fileName; return m_displayName; } void RobloxIDEDoc::resetMouseCommand(const char* mouseCommandToReset) { shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel(); if (dataModel) { RBX::Workspace* workspace = dataModel->getWorkspace(); RBX::MouseCommand* mouseCommand = workspace->getCurrentMouseCommand(); if (mouseCommand) { if (strcmp(mouseCommand->getName().c_str(), mouseCommandToReset) == 0) workspace->setDefaultMouseCommand(); } } } void RobloxIDEDoc::setMuteAllSounds(bool mute) { if (!m_CurrentGame) { return; } if (!m_CurrentGame->m_Game) { return; } RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); QAction* pAction = mainWindow.findChild("audioTurnOffAction"); if (pAction && pAction->isChecked()) { return; } if (boost::shared_ptr dataModel = m_CurrentGame->m_Game->getDataModel()) { if (RBX::Soundscape::SoundService* soundService = RBX::ServiceProvider::create(dataModel.get())) { if (soundService->isMuted() != mute) { soundService->muteAllChannels(mute); if (pAction) { pAction->setChecked(mute); } } } } } void RobloxIDEDoc::initServerAudioBehavior() { if (AuthoringSettings::singleton().getTestServerAudioBehavior() == AuthoringSettings::Enabled) { return; } RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); // we want sound to behave like an online game, so completely disable sound // also make sure the radio buttons do not work if (AuthoringSettings::singleton().getTestServerAudioBehavior() == AuthoringSettings::OnlineGame) { RBX::Soundscape::SoundService::soundDisabled = true; const QString disabledAudioText = "Audio has been disabled. Please go to File->Settings->Studio and then Server Audio Behavior to change this."; if (QWidget* testMenu = mainWindow.findChild("Test")) { if (QWidget* audioGroup = testMenu->findChild("Audio")) { audioGroup->setEnabled(false); audioGroup->setToolTip(disabledAudioText); audioGroup->setStatusTip(disabledAudioText); if (QAction* audioToggleWidget = mainWindow.findChild("audioToggleAction")) { audioToggleWidget->setToolTip(disabledAudioText); audioToggleWidget->setStatusTip(disabledAudioText); } } } } // just mute the instance for now else if (AuthoringSettings::singleton().getTestServerAudioBehavior() == AuthoringSettings::Muted) { setMuteAllSounds(true); if (QAction* audioToggleWidget = mainWindow.findChild("audioToggleAction")) { audioToggleWidget->setChecked(true); } } } void RobloxIDEDoc::promptForExistingAssetId(const std::string& assetType, boost::function resumeFunction, boost::function errorFunction) { QString assetTypeStr(assetType.c_str()); if (assetTypeStr.toLower() != "animation") { errorFunction("Requested asset type is not supported."); return; } // make sure we launch dialog in Main Thread QVariant resumeFunctionVar, errorFunctionVar; resumeFunctionVar.setValue >(resumeFunction); errorFunctionVar.setValue >(errorFunction); QMetaObject::invokeMethod(this, "showImportAssetDialog", Qt::QueuedConnection, Q_ARG(QVariant, resumeFunctionVar), Q_ARG(QVariant, errorFunctionVar)); } void RobloxIDEDoc::showImportAssetDialog(QVariant resumeFunctionVar, QVariant errorFunctionVar) { if (!StudioUtilities::checkNetworkAndUserAuthentication()) { boost::function errorFunction = errorFunctionVar.value >(); RBXASSERT(errorFunction); errorFunction("User not logged in."); return; } boost::function resumeFunction = resumeFunctionVar.value >(); RBXASSERT(resumeFunction); QString importUrl(QString("%1/studio/animations/import").arg(RobloxSettings::getBaseURL())); if (FFlag::StudioEnableGameAnimationsTab) { RobloxGameExplorer& gameExplorer = UpdateUIManager::Instance().getViewWidget(eDW_GAME_EXPLORER); if (gameExplorer.getCurrentGameId() > 0) importUrl.append(QString("?universeId=%1").arg(gameExplorer.getCurrentGameId())); } // show import dialog ImportAssetDialog importDlg(m_pMainWindow, importUrl, getDataModel()); importDlg.exec(); // resume function returning the selected asset id resumeFunction(importDlg.getAssetId()); } QString RobloxIDEDoc::getRecentSaveFile() { QString fileToSave; QString recentSavesPath = AuthoringSettings::singleton().recentSavesDir.absolutePath(); if (QDir().mkpath(recentSavesPath)) { QFileInfo fileInfo(displayName()); QString baseName = fileInfo.baseName(); // make sure file name doesn't have invalid characters, replace them with '_' baseName.replace( QRegExp( "[" + QRegExp::escape( "\\/:*?\"<>|" ) + "]" ), QString( "_" ) ); // differentiate between files with same name // for local documents use hash created using file path // for online documents use place id if (m_bIsLocalDocument) { fileToSave = recentSavesPath+"/"+baseName+"_"+ QString::number(RBX::Hash::hash(QFileInfo(fileName()).absoluteFilePath().toStdString()))+".rbxl"; } else { fileToSave = recentSavesPath+"/"+baseName+"_"+QString::number(getDataModel()->getPlaceID())+".rbxl"; } } return fileToSave; } void RobloxIDEDoc::clearOutputWidget() { if (AuthoringSettings::singleton().clearOutputOnStart) m_pMainWindow->getOutputWidget()->clear(); } void RobloxIDEDoc::onLocalPlayerAdded(shared_ptr newPlayer) { m_localPlayerConnection = newPlayer->propertyChangedSignal.connect(boost::bind(&RobloxIDEDoc::onLocalPlayerNameChanged, this, _1)); } void RobloxIDEDoc::onLocalPlayerNameChanged(const RBX::Reflection::PropertyDescriptor* pDescriptor) { if (*pDescriptor == RBX::Instance::desc_Name) { QMetaObject::invokeMethod(this, "syncPlayerName", Qt::QueuedConnection); } } void RobloxIDEDoc::syncPlayerName() { RBX::DataModel* datamodel = getDataModel(); if (!datamodel) return; { RBX::DataModel::LegacyLock lock(datamodel, RBX::DataModelJob::Read); if (RBX::Network::Player* localPlayer = RBX::Network::Players::findLocalPlayer(datamodel)) m_displayName = localPlayer->getName().c_str(); } // update window title and tab title m_pMainWindow->updateWindowTitle(); refreshDisplayName(); } void RobloxIDEDoc::appendTimeStampToDisplayName() { QString updatedDisplayName = displayName(); updatedDisplayName.append(" @ "); updatedDisplayName.append(QDateTime::currentDateTime().toString("dd MMM yyyy hh:mm")); updateDisplayName(updatedDisplayName); } void RobloxIDEDoc::removeTimeStampFromDisplayName() { QString updatedDisplayName = displayName(); int index = updatedDisplayName.lastIndexOf(" @ "); if (index > 0) { updatedDisplayName = updatedDisplayName.left(index); updateDisplayName(updatedDisplayName); } } void RobloxIDEDoc::updateOnSimulationStateChange(bool run) { if (isCloudEditSession()) run ? appendTimeStampToDisplayName() : removeTimeStampFromDisplayName(); } void RobloxIDEDoc::setTeamCreateWidgetEnabled(bool enable) { boost::shared_ptr dataModel; boost::shared_ptr playersDataManager; // enable only for local doc and edit mode if (enable && !m_initializationScript.isEmpty()) enable = StudioUtilities::containsEditScript(m_initializationScript); if (enable) { dataModel = m_EditGame.m_Game->getDataModel(); playersDataManager = m_EditGame.m_PlayerDataManager; } RobloxTeamCreateWidget& pTeamCreateWidget = UpdateUIManager::Instance().getViewWidget(eDW_TEAM_CREATE); pTeamCreateWidget.setDataModel(dataModel, playersDataManager); if (enable) { // make sure we raise the dock widget to make sure it's on top if part of a tab QWidget* pParentWidget = qobject_cast(pTeamCreateWidget.parent()); if (pParentWidget && pParentWidget->isVisible()) QMetaObject::invokeMethod(pParentWidget, "raise", Qt::QueuedConnection); } } void RobloxIDEDoc::initCloudEditObjects() { if (!isCloudEditSession()) { RBXASSERT(false); return; } shared_ptr dataModel = m_EditGame.m_Game->getDataModel(); m_EditGame.m_PlayerDataManager.reset(new PlayersDataManager(this, dataModel)); shared_ptr playersDataManager = m_EditGame.m_PlayerDataManager; m_CloudEditAdornable.attach(dataModel, playersDataManager); // since we want the chat widget to be enabled in Play solo session also, widget is not part of game state QStackedWidget* pChatWidgetStack = UpdateUIManager::Instance().getChatWidgetStack(); if (pChatWidgetStack) { m_ChatWidget.reset(new RobloxChatWidget(pChatWidgetStack, dataModel, playersDataManager)); pChatWidgetStack->addWidget(m_ChatWidget.get()); pChatWidgetStack->setCurrentWidget(m_ChatWidget.get()); } m_EditGame.m_TreeWidget->setPlayersDataManager(playersDataManager); } QString RobloxIDEDoc::downloadPlaceCopy() { RobloxSettings settings; QString rbxmLastDir = settings.value("rbxm_last_directory").toString(); if (rbxmLastDir.isEmpty()) rbxmLastDir = RobloxMainWindow::getDefaultSavePath(); QString fileName = QFileDialog::getSaveFileName(&UpdateUIManager::Instance().getMainWindow(), "Download a Copy", rbxmLastDir, saveFileFilters()); if (!fileName.isEmpty()) { settings.setValue("rbxm_last_directory", QFileInfo(fileName).absolutePath()); bool result = saveAs(fileName); if (result) { QString message = QString("Successfully downloaded a copy to '%1'").arg(QDir::toNativeSeparators(fileName)); RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, qPrintable(message)); return fileName; } } return QString(); } void RobloxIDEDoc::enableDownloadPlaceCopy(bool state) { // show only downloadPlaceCopyAction, reset of the actions must be invisible for CloudEdit UpdateUIManager::Instance().getMainWindow().downloadPlaceCopyAction->setVisible(state); UpdateUIManager::Instance().getMainWindow().publishToRobloxAsAction->setVisible(!state); UpdateUIManager::Instance().getMainWindow().fileSaveAction->setVisible(!state); UpdateUIManager::Instance().getMainWindow().fileSaveAsAction->setVisible(!state); UpdateUIManager::Instance().getMainWindow().publishToRobloxAction->setVisible(!state); } static const int kMaxRecentlySavedFile = 5; static QString recentlySavedFilesFile() { QString xmlFilePath(AuthoringSettings::singleton().recentSavesDir.absolutePath()); xmlFilePath.append("/"); xmlFilePath.append("RecentSaves.xml"); return xmlFilePath; } RecentlySavedFilesHandler::RecentlySavedFilesHandler() { readFileList(); } RecentlySavedFilesHandler::~RecentlySavedFilesHandler() { saveFileList(); } void RecentlySavedFilesHandler::addFile(const QString& fileName) { removeFile(fileName); m_RecentlySavedFiles.prepend(fileName); } void RecentlySavedFilesHandler::removeFile(const QString& fileName) { if (m_RecentlySavedFiles.contains(fileName)) m_RecentlySavedFiles.removeAll(fileName); } void RecentlySavedFilesHandler::updateFileList() { while (m_RecentlySavedFiles.size() > kMaxRecentlySavedFile) { QFile::remove(m_RecentlySavedFiles.last()); m_RecentlySavedFiles.removeLast(); } } void RecentlySavedFilesHandler::saveFileList() { QFile file(recentlySavedFilesFile()); if (file.open(QFile::WriteOnly | QFile::Text)) { //update list updateFileList(); // create dom document QDomDocument doc; QDomElement root = doc.createElement("RecentSaves"); doc.appendChild(root); for (int ii = 0; ii < m_RecentlySavedFiles.size(); ii++) { QDomElement newElement = doc.createElement("file"); newElement.setAttribute("name", m_RecentlySavedFiles.at(ii)); root.appendChild(newElement); } // save dom document to file QTextStream out(&file); out << doc.toString(); file.close(); } } void RecentlySavedFilesHandler::readFileList() { m_RecentlySavedFiles.clear(); QFile file(recentlySavedFilesFile()); if (file.open(QIODevice::ReadOnly)) { QDomDocument doc; if (doc.setContent(&file)) { QDomElement domElement = doc.documentElement(); for (QDomElement iter = domElement.firstChildElement(); !iter.isNull(); iter = iter.nextSiblingElement()) { if (iter.tagName().toLower() == "file") { QString fileName = iter.attribute("name"); if (QFileInfo(fileName).exists()) m_RecentlySavedFiles.append(fileName); } } } } } const char* attentionMessageStyleSheet = "QFrame{background:rgb(246, 136, 2);}:"; static const char* kWorkspaceNotificationMessageAlreadyClosed = "WorkspaceNotificationMessageAlreadyClosed"; std::string getNotificationId() { return DFString::WorkspaceMessageText + boost::to_string(DFInt::StudioWorkspaceNotificationLevel); } WorkspaceAnnouncementTooltip::WorkspaceAnnouncementTooltip(QWidget* parent) { resize(QSize(1,1)); setAttribute(Qt::WA_ShowWithoutActivating); setFocusPolicy(Qt::NoFocus); setWindowFlags(Qt::WindowStaysOnTopHint | Qt::ToolTip | Qt::FramelessWindowHint); QSize parentSize = parent->size(); show(); setVisible(false); resize(parentSize.width(), 50); QHBoxLayout* mainLayout = new QHBoxLayout(); toolTipLabel = new QLabel(this); toolTipLabel->setGeometry(4, 4, width(), 20); toolTipLabel->setOpenExternalLinks(true); toolTipLabel->setWordWrap(true); toolTipLabel->setAlignment(Qt::AlignCenter); toolTipLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); toolTipLabel->setTextFormat(Qt::RichText); toolTipLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); closeButton = new QPushButton(this); closeButton->setIcon(QIcon(":/RibbonBar/images/Studio 2.0 icons/32x32/x_white_32.png")); closeButton->setIconSize(QSize(32, 32)); closeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); closeButton->setFlat(true); mainLayout->addWidget(toolTipLabel, 8); mainLayout->addWidget(closeButton, 1); setLayout(mainLayout); setMaximumHeight(50); setAutoFillBackground(true); setStyleSheet(attentionMessageStyleSheet); setParent(parent); setLastClosedMessage(RobloxSettings().value(kWorkspaceNotificationMessageAlreadyClosed, "").toString()); connect(closeButton, SIGNAL (clicked()), this, SLOT (closeText())); } void WorkspaceAnnouncementTooltip::showText() { if (getLastClosedMessage() != QString(getNotificationId().c_str())) { if (!isVisible()) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "WorkspaceNotification - ShowText", getNotificationId().c_str()); QString customTextHyperlinkFormat; QFont boldFont = toolTipLabel->font(); boldFont.setPointSize(10); boldFont.setBold(true); if (DFString::WorkspaceMessageLink == std::string("")) { // No Link customTextHyperlinkFormat = QString("%2"); toolTipLabel->setText(customTextHyperlinkFormat.arg( DFString::WorkspaceMessageText.c_str())); } else { customTextHyperlinkFormat = QString("%2"); toolTipLabel->setText(customTextHyperlinkFormat.arg( DFString::WorkspaceMessageLink.c_str(), DFString::WorkspaceMessageText.c_str())); } toolTipLabel->setFont(boldFont); } QPoint ownerPosition = parentWidget()->pos(); setGeometry(ownerPosition.x(), ownerPosition.y(), parentWidget()->size().width(), 50); setVisible(true); } } void WorkspaceAnnouncementTooltip::hideText() { if (isVisible()) { setVisible(false); } } void WorkspaceAnnouncementTooltip::closeText() { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "WorkspaceNotification - CloseText", getNotificationId().c_str()); setVisible(false); RobloxSettings().setValue(kWorkspaceNotificationMessageAlreadyClosed, getNotificationId().c_str()); setLastClosedMessage(getNotificationId().c_str()); }