mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-04 20:57:49 +00:00
4619 lines
154 KiB
C++
4619 lines
154 KiB
C++
/**
|
|
* RobloxIDEDoc.cpp
|
|
* Copyright (c) 2013 ROBLOX Corp. All Rights Reserved.
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include "RobloxIDEDoc.h"
|
|
|
|
// Qt Headers
|
|
#include <QFileInfo>
|
|
#include <QDir>
|
|
#include <QDateTime>
|
|
#include <QMessageBox>
|
|
#include <QDesktopServices>
|
|
#include <QDialogButtonBox>
|
|
#include <QPushButton>
|
|
#include <QFileDialog>
|
|
#include <QDomDocument>
|
|
#include <QDomElement>
|
|
|
|
// 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<void(int)>);
|
|
Q_DECLARE_METATYPE(boost::function<void(std::string)>);
|
|
|
|
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<RBX::DataModel> dataModel = m_CurrentGame->m_Game->getDataModel();
|
|
|
|
dataModel->create<RBX::ContentProvider>()->setAssetFetchMediator(
|
|
&UpdateUIManager::Instance().getViewWidget<RobloxGameExplorer>(eDW_GAME_EXPLORER));
|
|
|
|
RBX::ServiceProvider::create<RBX::ServerScriptService>(dataModel.get());
|
|
RBX::ServiceProvider::create<RBX::ServerStorage>(dataModel.get());
|
|
RBX::ServiceProvider::create<RBX::ReplicatedStorage>(dataModel.get());
|
|
|
|
if (m_CurrentGame->m_Game != m_PlayGame.m_Game &&
|
|
!StudioUtilities::isAvatarMode() &&
|
|
!StudioUtilities::isTestMode())
|
|
dataModel->setIsStudio(true);
|
|
|
|
dataModel->create<RBX::LuaWebService>()->setCheckApiAccessBecauseInStudio();
|
|
dataModel->create<RBX::Network::Players>()->setNonSuperSafeChatForAllPlayersEnabled(true);
|
|
|
|
if (AuthoringSettings::singleton().onlyPlayFocusWindowAudio)
|
|
{
|
|
setMuteAllSounds(!QApplication::activeWindow());
|
|
if (m_pMainWindow)
|
|
{
|
|
if (QAction* audioToggleWidget = m_pMainWindow->findChild<QAction*>("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<RBX::Instance> instance = RBX::Creatable<RBX::Instance>::create<RBX::BasicPartInstance>();
|
|
RBXASSERT(instance);
|
|
|
|
// set up part instance
|
|
RBX::BaldPtr<RBX::PartInstance> partInstance = static_cast<RBX::PartInstance*>(instance.get());
|
|
partInstance->setName("BasePlate");
|
|
|
|
if (shared_ptr<RBX::BasicPartInstance> basicPartInstance = RBX::Instance::fastSharedDynamicCast<RBX::BasicPartInstance>(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<RBX::DataModel> 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<RBX::ChangeHistoryService>();
|
|
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<EntityProperties> 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<int> status = p.get<int>("status"))
|
|
{
|
|
// PlaceLauncher status code 2 == server is ready
|
|
if (status.get() == 2 && !*abort)
|
|
{
|
|
config->setFromValueTable(p.get<shared_ptr<const RBX::Reflection::ValueTable> >("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<int> 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<int>();
|
|
}
|
|
|
|
shared_ptr<EntityProperties> RobloxIDEDoc::cloudEditDetectionAndPlaceLaunch()
|
|
{
|
|
boost::optional<int> placeId;
|
|
if ((RobloxUser::singleton().getUserId() > 0) && StudioUtilities::containsEditScript(m_initializationScript))
|
|
{
|
|
placeId = parseUrlParameter("placeid", m_initializationScript);
|
|
RBXASSERT(placeId);
|
|
|
|
boost::optional<int> 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<int> placeUniverseId = p.get<int>("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<bool> enabled = p.get<bool>("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<EntityProperties> 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<EntityProperties> 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<QProgressBar> 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<RobloxExplorerWidget>(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<RobloxGameExplorer>(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<RBX::Network::Players>(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<CSGDictionaryService>(getDataModel());
|
|
NonReplicatedCSGDictionaryService* nrDictionaryService = ServiceProvider::create<NonReplicatedCSGDictionaryService>(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<StarterPlayerService>(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<RBX::Lighting>(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<RBX::MegaClusterInstance*>(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<RBX::UserInputService>();
|
|
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<RobloxGameExplorer>(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<QProgressBar> 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<RBX::Instance> item, const RBX::Reflection::PropertyDescriptor* descriptor)
|
|
{
|
|
if (descriptor == &RBX::PartOperation::prop_CollisionFidelity)
|
|
{
|
|
if (PartOperation* partOp = item->fastDynamicCast<PartOperation>())
|
|
{
|
|
if (CSGDictionaryService* dictionaryService = ServiceProvider::create<CSGDictionaryService>(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<RBX::ChangeHistoryService>();
|
|
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<RBX::DataModel>());
|
|
//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<RBX::DataModel> 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<RBX::DataModel>());
|
|
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<RobloxExplorerWidget>(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<RobloxGameExplorer>(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<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
RBX::DataModel::LegacyLock lock(dataModel,RBX::DataModelJob::Write);
|
|
QVector<RBX::Verb*>::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<RobloxExplorerWidget>(eDW_OBJECT_EXPLORER).setCurrentWidget(NULL);
|
|
}
|
|
|
|
// shut down UI and plugins
|
|
if (gameState.m_Game && gameState.m_Game->getDataModel())
|
|
{
|
|
shared_ptr<RBX::DataModel> 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<DataModel> 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<ContentProvider>(), 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<CSGDictionaryService>();
|
|
ChangeHistoryService* changeHistory = dm->create<ChangeHistoryService>();
|
|
|
|
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<PublishToRobloxAsVerb*>(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<RBX::DataModel> 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<RBX::RunService>())
|
|
return false;
|
|
|
|
RBX::RunState runState = m_PlayGame.m_Game->getDataModel()->create<RBX::RunService>()->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<RBX::Instance> instance)
|
|
{
|
|
if (RBX::Instance::fastSharedDynamicCast<RBX::Script>(instance) || RBX::Instance::fastSharedDynamicCast<RBX::ModuleScript>(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<RBX::Instance> playInstance = (*iter)->getCurrentScript().toInstance();
|
|
if (playInstance == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
shared_ptr<RBX::Instance> 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<bool> 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<RBX::Instance> backingInstance = (*iter)->getCurrentScript().toInstance();
|
|
if (backingInstance == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
std::vector<int> indexHierarchy;
|
|
std::string serviceName = getScriptIndexHierarchy(backingInstance, indexHierarchy);
|
|
shared_ptr<RBX::Instance> 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<RBX::Instance> 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<RBX::Game> 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<RBX::DataModel> 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<RobloxExplorerWidget>(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<RBX::DataModel>());
|
|
|
|
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<RBX::DataModel> dataModel = state ? m_CurrentGame->m_Game->getDataModel() : shared_ptr<RBX::DataModel>();
|
|
|
|
UpdateUIManager& uiManager = UpdateUIManager::Instance();
|
|
RobloxPropertyWidget& property_view = uiManager.getViewWidget<RobloxPropertyWidget>(eDW_PROPERTIES);
|
|
property_view.setDataModel(dataModel);
|
|
|
|
RobloxDiagnosticsView& diagnostics_view = uiManager.getViewWidget<RobloxDiagnosticsView>(eDW_DIAGNOSTICS);
|
|
diagnostics_view.setDataModel(dataModel);
|
|
|
|
RobloxScriptReview& script_view = uiManager.getViewWidget<RobloxScriptReview>(eDW_SCRIPT_REVIEW);
|
|
script_view.setDataModel(dataModel);
|
|
|
|
RobloxToolBox& toolbox_view = uiManager.getViewWidget<RobloxToolBox>(eDW_TOOLBOX);
|
|
toolbox_view.setDataModel(dataModel);
|
|
|
|
InsertObjectWidget& basic_view = uiManager.getViewWidget<InsertObjectWidget>(eDW_BASIC_OBJECTS);
|
|
basic_view.setDataModel(dataModel);
|
|
|
|
ScriptAnalysisWidget& analysis_view = uiManager.getViewWidget<ScriptAnalysisWidget>(eDW_SCRIPT_ANALYSIS);
|
|
analysis_view.setDataModel(dataModel);
|
|
|
|
if (!state)
|
|
{
|
|
RobloxFindWidget& find_view = uiManager.getViewWidget<RobloxFindWidget>(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<QKeySequence> 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<RBX::DataModel> 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<RobloxDiagnosticsView>(eDW_DIAGNOSTICS);
|
|
diagnostics_view.setDataModel(dataModel);
|
|
}
|
|
}
|
|
else if (actionID == "viewScriptPerformanceAction")
|
|
{
|
|
if (dataModel)
|
|
{
|
|
RobloxScriptReview& script_view = uiManager.getViewWidget<RobloxScriptReview>(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<RobloxToolBox>(eDW_TOOLBOX);
|
|
toolbox_view.setDataModel(dataModel);
|
|
}
|
|
}
|
|
else if (actionID == "viewBasicObjectsAction")
|
|
{
|
|
if (dataModel)
|
|
{
|
|
InsertObjectWidget& basic_view = uiManager.getViewWidget<InsertObjectWidget>(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<RobloxRibbonMainWindow*>(&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<RBX::IHostNotifier*>(pNotifier)->buttonClick(m_CurrentGame->m_Game->getDataModel().get(), pAction);
|
|
return true;
|
|
}
|
|
|
|
RBX::Reflection::Variant RobloxIDEDoc::evaluateCommandBarItem(const char* itemToEvaluate, shared_ptr<RBX::LuaSourceContainer> script)
|
|
{
|
|
shared_ptr<RBX::DataModel> dataModel = m_CurrentGame->m_Game->getDataModel();
|
|
RBX::DataModel::LegacyLock lock(dataModel,RBX::DataModelJob::Write);
|
|
|
|
boost::shared_ptr<RBX::ScriptContext> pScriptContext = RBX::shared_from(dataModel->create<RBX::ScriptContext>());
|
|
if(pScriptContext)
|
|
{
|
|
return pScriptContext->evaluateStudioCommandItem(itemToEvaluate, script);
|
|
}
|
|
return RBX::Reflection::Variant();
|
|
}
|
|
|
|
void RobloxIDEDoc::handleScriptCommand(const QString &execCommand)
|
|
{
|
|
if (execCommand.isEmpty())
|
|
return;
|
|
|
|
shared_ptr<RBX::DataModel> 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<std::istream> stream =
|
|
RBX::ServiceProvider::create<RBX::ContentProvider>(dataModel.get())->getContent(RBX::ContentId(command));
|
|
command = std::string(static_cast<std::stringstream const&>(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<RBX::ScriptContext> pScriptContext = RBX::shared_from(dataModel->create<RBX::ScriptContext>());
|
|
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<BreakpointsWidget>(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<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::CommonVerbs> commonVerbs = gameState.m_Game->commonVerbs;
|
|
shared_ptr<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::CommonVerbs> commonVerbs = gameState.m_Game->commonVerbs;
|
|
shared_ptr<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::CommonVerbs> commonVerbs = gameState.m_Game->commonVerbs;
|
|
shared_ptr<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::CommonVerbs> commonVerbs = gameState.m_Game->commonVerbs;
|
|
shared_ptr<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::CommonVerbs> commonVerbs = gameState.m_Game->commonVerbs;
|
|
shared_ptr<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::CommonVerbs> commonVerbs = gameState.m_Game->commonVerbs;
|
|
|
|
mapActionIDWithVerb(gameState,"moveUpBrickAction", &commonVerbs->moveUpBrickVerb);
|
|
mapActionIDWithVerb(gameState,"moveDownBrickAction", &commonVerbs->moveDownSelectionVerb);
|
|
}
|
|
|
|
void RobloxIDEDoc::mapAdvanceBuildToolbar(GameState& gameState)
|
|
{
|
|
boost::shared_ptr<RBX::CommonVerbs> commonVerbs = gameState.m_Game->commonVerbs;
|
|
shared_ptr<RBX::DataModel> dataModel = gameState.m_Game->getDataModel();
|
|
QVector<RBX::Verb*>& 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<RBX::DataModel> playDataModel = m_PlayGame.m_Game->getDataModel();
|
|
RBX::DataModel::LegacyLock lock(playDataModel,RBX::DataModelJob::Write);
|
|
RBX::RunService* pRS = playDataModel->create<RBX::RunService>();
|
|
pRS->pause();
|
|
}
|
|
else
|
|
{
|
|
shared_ptr<RBX::DataModel> dataModel = m_EditGame.m_Game->getDataModel();
|
|
RBX::DataModel::LegacyLock lock(dataModel,RBX::DataModelJob::Write);
|
|
RBX::RunService* pRS = dataModel->create<RBX::RunService>();
|
|
pRS->pause();
|
|
}
|
|
}
|
|
|
|
bool RobloxIDEDoc::isIdePauseEnabled()
|
|
{
|
|
if (m_PlayGame.m_Game)
|
|
{
|
|
shared_ptr<RBX::DataModel> playDataModel = m_PlayGame.m_Game->getDataModel();
|
|
RBX::RunService* pRS = playDataModel->create<RBX::RunService>();
|
|
RBX::RunState runState = pRS->getRunState();
|
|
return runState == RBX::RS_RUNNING;
|
|
}
|
|
|
|
shared_ptr<RBX::DataModel> dataModel = m_EditGame.m_Game->getDataModel();
|
|
RBX::RunService* pRS = dataModel->create<RBX::RunService>();
|
|
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<RBX::DataModel> dataModel = m_CurrentGame->m_Game->getDataModel();
|
|
std::string command = url.toStdString();
|
|
std::auto_ptr<std::istream> stream = RBX::ServiceProvider::create<RBX::ContentProvider>(dataModel.get())->getContent(RBX::ContentId(command));
|
|
std::string scriptStr(static_cast<std::stringstream const&>(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<RBX::DataModel> 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::RunService>();
|
|
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::RunService>())
|
|
{
|
|
{
|
|
RBX::DataModel::LegacyLock lock(m_PlayGame.m_Game->getDataModel(),RBX::DataModelJob::Write);
|
|
RBX::RunState runState = m_PlayGame.m_Game->getDataModel()->create<RBX::RunService>()->getRunState();
|
|
if (runState == RBX::RS_PAUSED)
|
|
{
|
|
RBX::ServiceProvider::create<RBX::RunService>(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::RunService>();
|
|
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<QProgressBar> 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<RBX::DataModel> 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<RobloxExplorerWidget>(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<RBX::UserInputService>();
|
|
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<RBX::DataModel> 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<ContentProvider>(), playDataModel.get());
|
|
|
|
}
|
|
|
|
tDebuggersMap playModelDebuggers;
|
|
for (tDebuggersMap::const_iterator iter = m_EditDebuggersMap.begin(); iter != m_EditDebuggersMap.end(); ++iter)
|
|
{
|
|
std::vector<int> indexHierarchy;
|
|
std::string serviceName = getScriptIndexHierarchy(iter->first, indexHierarchy);
|
|
shared_ptr<RBX::Instance> 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<RBX::RunService>(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<RBX::DataModel> 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<RBX::DataModel> 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<RBX::DataModel> playDataModel = m_PlayGame.m_Game->getDataModel();
|
|
RBX::RunService* pRS = playDataModel->create<RBX::RunService>();
|
|
|
|
return pRS->getRunState() != RBX::RS_RUNNING;
|
|
}
|
|
else
|
|
{
|
|
shared_ptr<RBX::DataModel> dataModel = m_EditGame.m_Game->getDataModel();
|
|
RBX::RunService* pRS = dataModel->create<RBX::RunService>();
|
|
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<RBX::DataModel> dataModel = m_EditGame.m_Game->getDataModel();
|
|
RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Write);
|
|
dataModel->create<RBX::RunService>()->stop();
|
|
dataModel->create<RBX::ChangeHistoryService>()->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<QTreeWidgetItem *> 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<RBX::Instance> instance, RBX::Scripting::DebuggerManager* pDebuggerManager)
|
|
{
|
|
if (!instance || instance->isDescendantOf(RBX::ServiceProvider::find<RBX::CoreGuiService>(instance.get())))
|
|
return;
|
|
|
|
shared_ptr<RBX::Script> pScript = RBX::Instance::fastSharedDynamicCast<RBX::Script>(instance);
|
|
if (pScript)
|
|
{
|
|
//RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Script Name: %s", pScript->getName().c_str());
|
|
pDebuggerManager->addDebugger(pScript.get());
|
|
}
|
|
else
|
|
{
|
|
shared_ptr<RBX::ModuleScript> pModuleScript = RBX::Instance::fastSharedDynamicCast<RBX::ModuleScript>(instance);
|
|
if (pModuleScript)
|
|
pDebuggerManager->addDebugger(pModuleScript.get());
|
|
}
|
|
}
|
|
|
|
void RobloxIDEDoc::initializeDebuggerData(const QString &debugFile, const shared_ptr<RBX::DataModel> 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<RBX::DataModel>());
|
|
|
|
//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<RobloxIDEDoc> 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<RobloxIDEDoc> 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<shared_ptr<const Reflection::ValueTable> >()->at("Name").cast<std::string>()));
|
|
}
|
|
}
|
|
|
|
|
|
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<StringValue>(defaultToolboxSearchInstance))
|
|
{
|
|
QMetaObject::invokeMethod(&UpdateUIManager::Instance().getViewWidget<RobloxToolBox>(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<CSGDictionaryService>(m_EditGame.m_Game->getDataModel().get());
|
|
ChangeHistoryService* changeHistory = ServiceProvider::create<ChangeHistoryService>(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<RobloxGameExplorer>(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<RBX::DataModel> 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<RBX::Visit>(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<RBX::Instance> 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<RBX::Instance> script, shared_ptr<RBX::Instance> 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<RBX::Instance> script, shared_ptr<RBX::DataModel> dataModel)
|
|
{
|
|
if (!script)
|
|
return;
|
|
|
|
std::vector<int> indexHierarchy;
|
|
std::string serviceName = getScriptIndexHierarchy(script, indexHierarchy);
|
|
|
|
shared_ptr<RBX::Instance> instance = getScriptByIndexHierarchy(dataModel, indexHierarchy, serviceName);
|
|
if (instance)
|
|
m_scriptMap[instance.get()] = script;
|
|
}
|
|
|
|
std::string RobloxIDEDoc::getScriptIndexHierarchy(shared_ptr<RBX::Instance> script, std::vector<int>& 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<const RBX::Service*>(pParent) &&
|
|
pParent->getParent() &&
|
|
pParent->getParent()->getParent() == NULL)
|
|
{
|
|
return pParent->getClassNameStr();
|
|
}
|
|
|
|
pChild = pParent;
|
|
pParent = pChild->getParent();
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
shared_ptr<RBX::Instance> RobloxIDEDoc::getScriptByIndexHierarchy(shared_ptr<RBX::DataModel> dataModel, const std::vector<int>& indexHierarchy, const std::string& serviceName)
|
|
{
|
|
if (serviceName == "")
|
|
return shared_ptr<RBX::Instance>();
|
|
|
|
shared_ptr<RBX::Instance> pParent = shared_from(dataModel->findFirstChildOfType(serviceName));
|
|
shared_ptr<RBX::Instance> pChild;
|
|
|
|
if (indexHierarchy.size() == 0)
|
|
return shared_ptr<RBX::Instance>();
|
|
|
|
for (int i = indexHierarchy.size() - 1; i >= 0; i--)
|
|
{
|
|
if (!pParent)
|
|
return shared_ptr<RBX::Instance>();
|
|
|
|
int childNum = indexHierarchy[i];
|
|
if (childNum >= (int)pParent->numChildren())
|
|
return shared_ptr<RBX::Instance>();
|
|
|
|
pChild = shared_from(pParent->getChild(childNum));
|
|
pParent = pChild;
|
|
}
|
|
|
|
if (pChild && (pChild->isA<RBX::Script>() || pChild->isA<RBX::ModuleScript>()))
|
|
return pChild;
|
|
return shared_ptr<RBX::Instance>();
|
|
}
|
|
|
|
boost::shared_ptr<RBX::Instance> RobloxIDEDoc::getEditScriptByPlayInstance(RBX::Instance* instance)
|
|
{
|
|
tScriptMap::iterator iter = m_scriptMap.find(instance);
|
|
|
|
if (iter != m_scriptMap.end())
|
|
return iter->second;
|
|
|
|
return boost::shared_ptr<RBX::Instance>();
|
|
}
|
|
|
|
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<RBX::Scripting::ScriptDebugger> debugger;
|
|
boost::shared_ptr<RBX::Scripting::DebuggerBreakpoint> breakpoint;
|
|
|
|
for (tDebuggersMap::const_iterator iter = debuggers.begin(); iter != debuggers.end(); ++iter)
|
|
{
|
|
debugger = debuggerManager.addDebugger(iter->first.get());
|
|
if (debugger)
|
|
{
|
|
const std::list<DebuggerData::BreakpointData>& breakpoints = iter->second.breakpoints;
|
|
for (std::list<DebuggerData::BreakpointData>::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<std::string>& watches = iter->second.watches;
|
|
for (std::list<std::string>::const_iterator w_iter = watches.begin(); w_iter != watches.end(); ++w_iter)
|
|
{
|
|
debugger->addWatch(*w_iter);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RobloxIDEDoc::addToDebuggersMap(shared_ptr<RBX::Instance> instance, tDebuggersMap& debuggersMap)
|
|
{
|
|
if (RBX::Instance::fastSharedDynamicCast<RBX::Script>(instance) || RBX::Instance::fastSharedDynamicCast<RBX::ModuleScript>(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<RBX::Instance> instance, tDebuggersMap& debuggersMap)
|
|
{
|
|
if (RBX::Instance::fastSharedDynamicCast<RBX::Script>(instance) || RBX::Instance::fastSharedDynamicCast<RBX::ModuleScript>(instance))
|
|
{
|
|
shared_ptr<RBX::Instance> 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<RBX::DataModel> 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<ContentProvider>())
|
|
{
|
|
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<RBX::DataModel>());
|
|
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<RBX::HttpRbxApiService>(getDataModel()))
|
|
{
|
|
m_bIsLocalDocument = false;
|
|
apiService->getAsync( RBX::format("marketplace/productinfo?assetId=%d", placeId),false, RBX::PRIORITY_DEFAULT,
|
|
boost::bind(&placeNameLoadedHelperSuccess, QPointer<RobloxIDEDoc>(this), _1),
|
|
boost::bind(&placeNameLoadedHelperError, QPointer<RobloxIDEDoc>(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<RBX::DataModel> 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<QAction*>("audioTurnOffAction");
|
|
if (pAction && pAction->isChecked())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (boost::shared_ptr<RBX::DataModel> dataModel = m_CurrentGame->m_Game->getDataModel())
|
|
{
|
|
if (RBX::Soundscape::SoundService* soundService = RBX::ServiceProvider::create<RBX::Soundscape::SoundService>(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<QWidget*>("Test"))
|
|
{
|
|
if (QWidget* audioGroup = testMenu->findChild<QWidget*>("Audio"))
|
|
{
|
|
audioGroup->setEnabled(false);
|
|
audioGroup->setToolTip(disabledAudioText);
|
|
audioGroup->setStatusTip(disabledAudioText);
|
|
if (QAction* audioToggleWidget = mainWindow.findChild<QAction*>("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<QAction*>("audioToggleAction"))
|
|
{
|
|
audioToggleWidget->setChecked(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RobloxIDEDoc::promptForExistingAssetId(const std::string& assetType, boost::function<void(int)> resumeFunction, boost::function<void(std::string)> 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<boost::function<void(int)> >(resumeFunction);
|
|
errorFunctionVar.setValue<boost::function<void(std::string)> >(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<void(std::string)> errorFunction = errorFunctionVar.value<boost::function<void(std::string)> >();
|
|
RBXASSERT(errorFunction);
|
|
errorFunction("User not logged in.");
|
|
return;
|
|
}
|
|
|
|
boost::function<void(int)> resumeFunction = resumeFunctionVar.value<boost::function<void(int)> >();
|
|
RBXASSERT(resumeFunction);
|
|
|
|
QString importUrl(QString("%1/studio/animations/import").arg(RobloxSettings::getBaseURL()));
|
|
|
|
if (FFlag::StudioEnableGameAnimationsTab)
|
|
{
|
|
RobloxGameExplorer& gameExplorer = UpdateUIManager::Instance().getViewWidget<RobloxGameExplorer>(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<RBX::Instance> 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<RBX::DataModel> dataModel;
|
|
boost::shared_ptr<PlayersDataManager> 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<RobloxTeamCreateWidget>(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<QWidget*>(pTeamCreateWidget.parent());
|
|
if (pParentWidget && pParentWidget->isVisible())
|
|
QMetaObject::invokeMethod(pParentWidget, "raise", Qt::QueuedConnection);
|
|
}
|
|
}
|
|
|
|
void RobloxIDEDoc::initCloudEditObjects()
|
|
{
|
|
if (!isCloudEditSession())
|
|
{
|
|
RBXASSERT(false);
|
|
return;
|
|
}
|
|
|
|
shared_ptr<RBX::DataModel> dataModel = m_EditGame.m_Game->getDataModel();
|
|
m_EditGame.m_PlayerDataManager.reset(new PlayersDataManager(this, dataModel));
|
|
|
|
shared_ptr<PlayersDataManager> 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("<a style='color: white;'>%2</a>");
|
|
toolTipLabel->setText(customTextHyperlinkFormat.arg(
|
|
DFString::WorkspaceMessageText.c_str()));
|
|
}
|
|
else
|
|
{
|
|
customTextHyperlinkFormat = QString("<a href=%1 style='color: white;'>%2</a>");
|
|
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());
|
|
}
|
|
|
|
|
|
|