/** * RobloxPluginHost.cpp * Copyright (c) 2013 ROBLOX Corp. All rights reserved. */ #include "stdafx.h" #include "RobloxPluginHost.h" // Qt Headers #include #include #include #include #include #include #include // Roblox Headers #include "GfxBase/ViewBase.h" #include "Script/ScriptContext.h" #include "Util/ProtectedString.h" #include "V8DataModel/DataModel.h" #include "V8DataModel/PluginManager.h" #include "V8DataModel/Stats.h" #include "V8DataModel/Workspace.h" #include "v8datamodel/ContentProvider.h" #include "V8Xml/Serializer.h" #include "V8Xml/WebParser.h" #include "V8Xml/XmlSerializer.h" // Roblox Studio Headers #include "AuthoringSettings.h" #include "RobloxCustomWidgets.h" #include "RobloxDocManager.h" #include "RobloxIDEDoc.h" #include "RobloxMainWindow.h" #include "RobloxSettings.h" #include "RobloxUser.h" #include "UpdateUIManager.h" #include "RobloxQuickAccessConfig.h" #include "RobloxContextualHelp.h" #include "IRobloxDoc.h" #include "QtUtilities.h" #include "Util/RobloxGoogleAnalytics.h" #include "PluginAction.h" #include "CSGOperations.h" #include "v8datamodel/PartOperation.h" #include "V8DataModel/NonReplicatedCSGDictionaryService.h" #include "V8DataModel/ChangeHistory.h" #include "V8Xml/SerializerBinary.h" #include "V8Tree/Service.h" #include "rbx/make_shared.h" Q_DECLARE_METATYPE(QList); FASTFLAG(StudioEnableGameAnimationsTab) FASTFLAGVARIABLE(EnableCSGScripting, true) FASTFLAGVARIABLE(StudioDisableToolbarUpdateOnButtonActive, false) static const QString PluginToolbarName = "Plugins"; static const QString TerrainToolbarName = "Terrain"; static const QString TransformToolbarName = "Transform"; static const QString SmoothTerrainToolbarName = "Smooth Terrain"; int RobloxPluginHost::m_NextID = 1; static const char* kFailedToLoadPluginIconIcon = "/textures/ui/CloseButton_dn.png"; FASTFLAG(GoogleAnalyticsTrackingEnabled) static const char* kPluginFileName = "Plugin.rbxm"; static const char* kAssetVersionJsonKey = "AssetVersion"; static const char* kEnabledJsonKey = "Enabled"; static const char* kPluginSettingsFileName = "settings.json"; static const char* kIsChecked = "isChecked"; static const char* kIsVisible = "isVisible"; RobloxPluginHost::RobloxPluginHost(QObject* pParent) : QObject(pParent), m_PluginsToolbar(NULL), m_TerrainToolbar(NULL), m_ConvertToSmoothID(0) { RBX::PluginManager::singleton()->setStudioHost(this); installEventFilter(this); } RobloxPluginHost::~RobloxPluginHost() { } void RobloxPluginHost::setNotifier(RBX::IHostNotifier *notifier) { m_pNotifier = notifier; } void* RobloxPluginHost::createToolbar(const std::string& name) { RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if (mainWindow.isRibbonStyle()) { if (name == TerrainToolbarName.toStdString()) return (void*)m_TerrainToolbarID; else if (name == TransformToolbarName.toStdString()) return (void*)m_TransformToolbarID; } int toolbarID = m_NextID++; QMetaObject::invokeMethod( this, "onCreateToolbar", Qt::QueuedConnection, Q_ARG(int,toolbarID), Q_ARG(QString,name.c_str()) ); return (void*)toolbarID; } void* RobloxPluginHost::createButton( void* tbId, const std::string& text, const std::string& tooltip, const std::string& iconFilePath ) { int toolbarID = (int)tbId; RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if (mainWindow.isRibbonStyle() && m_TextActionMap.contains(text.c_str())) { if (toolbarID == m_TerrainToolbarID) return (void*)m_TextActionMap[text.c_str()]; else if(toolbarID == m_TransformToolbarID) return (void*)m_TextActionMap[text.c_str()]; } int actionID = m_NextID++; QMetaObject::invokeMethod( this, "onCreateButton", Qt::QueuedConnection, Q_ARG(int,toolbarID), Q_ARG(int,actionID), Q_ARG(QString,text.c_str()), Q_ARG(QString,tooltip.c_str()), Q_ARG(QString,QString::fromUtf8(iconFilePath.c_str())) ); return (void*)actionID; } void RobloxPluginHost::setButtonActive(void *bId, bool active) { QMetaObject::invokeMethod(this, "onSetButtonActive", Qt::QueuedConnection, Q_ARG(int, (int)(bId)), Q_ARG(bool, active)); } void RobloxPluginHost::setButtonIcon_deprecated(void* buttonId, const std::string& iconFilePath) { QMetaObject::invokeMethod(this, "setButtonIconInternal_deprecated", Qt::QueuedConnection, Q_ARG(int, (int)(buttonId)), Q_ARG(QString, QString(iconFilePath.c_str()))); } void RobloxPluginHost::setButtonIcon(void* buttonId, const std::string& imageContent) { QImage image; image.loadFromData((const uchar*)imageContent.c_str(), imageContent.size()); if (image.isNull()) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, "Unable to load plugin icon. Image may have an invalid or unknown format."); buttonIconFailedToLoad(buttonId); } else { QMetaObject::invokeMethod(this, "setButtonIconInternal", Qt::QueuedConnection, Q_ARG(int, (int)(buttonId)), Q_ARG(QImage, image)); } } void RobloxPluginHost::buttonIconFailedToLoad(void* buttonId) { QImage image; image.load(QString(AppSettings::instance().contentFolder() + kFailedToLoadPluginIconIcon)); QMetaObject::invokeMethod(this, "setButtonIconInternal", Qt::QueuedConnection, Q_ARG(int, (int)(buttonId)), Q_ARG(QImage, image)); } void RobloxPluginHost::setButtonIconInternal_deprecated(int actionId, QString iconFilePath) { if (!m_ActionMap.contains(actionId)) { // This can happen if it takes a long time to load an icon return; } QPixmap iconPixmap; if (!iconPixmap.load(iconFilePath)) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, "Unable to load plugin icon. Image may have an invalid or unknown format."); iconPixmap.load(AppSettings::instance().contentFolder() + kFailedToLoadPluginIconIcon); } QAction* action = m_ActionMap[actionId]; QSize scale(16, 16); // default size of icons for SystemMenu UI RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if ( mainWindow.isRibbonStyle() ) { // max size ribbongroup can support is 32 x 32 so scale accordingly if (iconPixmap.width() >= 32 || iconPixmap.height() >= 32) { scale = QSize(32, 32); } else { int min = qMin(iconPixmap.width(), iconPixmap.height()); scale = QSize(min, min); } } QIcon icon(iconPixmap.scaled(scale)); action->setIcon(icon); } void RobloxPluginHost::setButtonIconInternal(int actionId, QImage image) { if (!m_ActionMap.contains(actionId)) { // This can happen if it takes a long time to load an icon return; } QAction* action = m_ActionMap[actionId]; QSize scale(16, 16); // default size of icons for SystemMenu UI RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if ( mainWindow.isRibbonStyle() ) { // max size ribbongroup can support is 32 x 32 so scale accordingly if (image.width() >= 32 || image.height() >= 32) { scale = QSize(32, 32); } else { int min = qMin(image.width(), image.height()); scale = QSize(min, min); } } QIcon icon(QPixmap::fromImage(image).scaled(scale)); action->setIcon(icon); } void RobloxPluginHost::hideToolbars(const std::vector& toolbars, bool hide) { RobloxMainWindow& main_window = UpdateUIManager::Instance().getMainWindow(); if ( !hide && main_window.getBuildMode() == BM_BASIC ) return; if (toolbars.empty()) return; QList toolbarIds; for ( size_t i = 0 ; i < toolbars.size( ); ++i ) toolbarIds.push_back((int)toolbars[i]); QVariant toolbarsVariant; toolbarsVariant.setValue(toolbarIds); QMetaObject::invokeMethod( this, "onShowToolbars", Qt::QueuedConnection, Q_ARG(QVariant,toolbarsVariant), Q_ARG(bool,!hide) ); } void RobloxPluginHost::disableToolbars(const std::vector& toolbars, bool disable) { RobloxMainWindow& main_window = UpdateUIManager::Instance().getMainWindow(); if ( !disable && main_window.getBuildMode() == BM_BASIC ) return; for ( size_t i = 0 ; i < toolbars.size( ); ++i ) { int toolbarID = (int)toolbars[i]; QMetaObject::invokeMethod( this, "onEnableToolbar", Qt::QueuedConnection, Q_ARG(int,toolbarID), Q_ARG(bool,!disable) ); } } void RobloxPluginHost::onShowToolbar(int toolbarID, bool show) { if ( !m_ToolBarMap.contains(toolbarID) && !m_RibbonGroupMap.contains(toolbarID) ) { // only output an error if we're showing a toolbar that is supposed to exist if ( show ) RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to show toolbar that does not exist - %i",toolbarID); return; } RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if ( mainWindow.isRibbonStyle() ) { RibbonGroup* group = m_RibbonGroupMap[toolbarID]; group->setVisible(show); // make sure we set the property so it can be used to correctly update the status in ribbon bar QList actions = group->actions(); for (QList::iterator iter = actions.begin(); iter != actions.end(); iter++) (*iter)->setProperty(kIsVisible, show); } else { QToolBar* toolbar = m_ToolBarMap[toolbarID]; toolbar->setVisible(show); } } void RobloxPluginHost::onShowToolbars(QVariant toolbarsVariant, bool show) { QList toolbars = toolbarsVariant.value >(); QVector shows(toolbars.size(), show); if (show) { bool hasSmoothTerrainToolbar = false; for (int i = 0; i < toolbars.size(); ++i) if (getToolbarName(toolbars[i]) == SmoothTerrainToolbarName) hasSmoothTerrainToolbar = true; for (int i = 0; i < toolbars.size(); ++i) if (hasSmoothTerrainToolbar && getToolbarName(toolbars[i]) == TerrainToolbarName) shows[i] = false; } for (int i = 0; i < toolbars.size(); ++i) { onShowToolbar(toolbars[i], shows[i]); } } void RobloxPluginHost::onEnableToolbar(int toolbarID, bool enable) { if ( !m_ToolBarMap.contains(toolbarID) && !m_RibbonGroupMap.contains(toolbarID) ) { // only output an error if we're showing a toolbar that is supposed to exist if ( enable ) RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to disable/enable toolbar that does not exist - %i",toolbarID); return; } RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if ( mainWindow.isRibbonStyle() ) { RibbonGroup* group = m_RibbonGroupMap[toolbarID]; group->setEnabled(enable); // make sure we set the property so it can be used to correctly update the status in ribbon bar QList actions = group->actions(); for (QList::iterator iter = actions.begin(); iter != actions.end(); iter++) (*iter)->setEnabled(enable); } else { QToolBar* toolbar = m_ToolBarMap[toolbarID]; toolbar->setEnabled(enable); } } void RobloxPluginHost::deleteToolbars(const std::vector& toolbars) { RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); for ( size_t i = 0 ; i < toolbars.size() ; ++i ) { int toolbarID = (int)toolbars[i]; if ( mainWindow.isRibbonStyle() && toolbarID == m_TerrainToolbarID ) { handleTerrainRibbonGroupDeletion(); continue; } QObject* parent = NULL; if ( mainWindow.isRibbonStyle() ) { if ( !m_RibbonGroupMap.contains(toolbarID) ) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to delete toolbar that does not exist - %i",toolbarID); continue; } RibbonGroup* group = m_RibbonGroupMap[toolbarID]; parent = group; } else { if ( !m_ToolBarMap.contains(toolbarID) ) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to delete toolbar that does not exist - %i",toolbarID); continue; } QToolBar* toolbar = m_ToolBarMap[toolbarID]; parent = toolbar; } // delete all actions in the map that are parented to this toolbar removeActionsFromMap(parent); if ( mainWindow.isRibbonStyle() ) { if (toolbarID == m_TransformToolbarID) continue; RibbonGroup* group = m_RibbonGroupMap[toolbarID]; m_RibbonGroupMap.remove(toolbarID); qobject_cast(group->parentWidget())->removeGroup(group); } else { QToolBar* toolbar = m_ToolBarMap[toolbarID]; m_ToolBarMap.remove(toolbarID); QMetaObject::invokeMethod(toolbar, "deleteLater", Qt::QueuedConnection); } } } void RobloxPluginHost::setSetting(int assetId, const std::string& key, const RBX::Reflection::Variant& value) { shared_ptr values(new RBX::Reflection::ValueTable()); readPluginSettings(assetId, values); (*values)[key] = value; writePluginSettings(assetId, values); } void RobloxPluginHost::getSetting(int assetId, const std::string& key, RBX::Reflection::Variant* result) { shared_ptr values(new RBX::Reflection::ValueTable()); readPluginSettings(assetId, values); (*result) = (*values)[key]; } bool RobloxPluginHost::getLoggedInUserId(int* userIdOut) { int userId = RobloxUser::singleton().getUserId(); if (userId > 0) { *userIdOut = userId; return true; } else { return false; } } void RobloxPluginHost::handlePluginAction() { QAction* pAction = qobject_cast(sender()); if ( !pAction ) return; if (RobloxIDEDoc::getIsCloudEditSession() && pAction->data().toInt() == m_ConvertToSmoothID) { QtUtilities::RBXMessageBox warning; warning.setText("Cannot conver to smooth during Cloud Edit session"); warning.setStandardButtons(QMessageBox::Ok); warning.exec(); return; } // revert the Qt's checked status (it will be taken care by the script code by calling "setButtonActive" pAction->setChecked(!pAction->isChecked()); // action to be handled by the relevant document if ( RobloxDocManager::Instance().getCurrentDoc() ) { int actionID = pAction->data().toInt(); RobloxDocManager::Instance().getCurrentDoc()->handlePluginAction(m_pNotifier,(void*)(actionID)); } // update toolbar status UpdateUIManager::Instance().updateToolBars(); } void RobloxPluginHost::onCreateToolbar(int toolbarID,QString name) { RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if ( mainWindow.isRibbonStyle() ) { RBXASSERT(!m_RibbonGroupMap.contains(toolbarID)); RibbonPage* page = (name == SmoothTerrainToolbarName) ? m_TerrainToolbar : m_PluginsToolbar; RibbonGroup* group = page->addGroup(name); group->setObjectName(name); group->setTitle(name); group->setVisible(false); m_RibbonGroupMap[toolbarID] = group; } else { RBXASSERT(!m_ToolBarMap.contains(toolbarID)); RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); QToolBar* pToolbar = mainWindow.addToolBar(name); RBXASSERT(pToolbar); pToolbar->setObjectName(name); pToolbar->setIconSize(QSize(16,16)); pToolbar->setVisible(false); m_ToolBarMap[toolbarID] = pToolbar; } } void RobloxPluginHost::uiAction(std::string uiActionName) { QMetaObject::invokeMethod( this, "onUIAction", Qt::QueuedConnection, Q_ARG(QString,uiActionName.c_str()) ); } void RobloxPluginHost::onUIAction(QString uiActionName) { QObjectList objects = UpdateUIManager::Instance().getMainWindow().children(); for (QObjectList::iterator iter = objects.begin(); iter != objects.end(); ++iter) { RBX::BaldPtr action = dynamic_cast(*iter); if (action && action->objectName() == uiActionName) { action->trigger(); return; } } } void RobloxPluginHost::onCreateButton(int toolbarID,int actionID,QString text,QString tooltip,QString iconName) { if ( !m_RibbonGroupMap.contains(toolbarID) && !m_ToolBarMap.contains(toolbarID) ) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"Plugin attempting to add button to toolbar that does not exist - %i, %i",toolbarID,actionID); return; } QAction* action = NULL; if ( text.isEmpty() ) text = tooltip; RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if ( mainWindow.isRibbonStyle() ) { Qtitan::RibbonGroup* pGroup = m_RibbonGroupMap[toolbarID]; action = new PluginAction(text,pGroup); if (mainWindow.ribbonBar()) action->setFont(mainWindow.ribbonBar()->font()); // by default an action is visible (unless it is explicitly set to invisible) action->setProperty(kIsVisible, true); pGroup->addAction(action,Qt::ToolButtonTextUnderIcon); } else { QToolBar* pToolbar = m_ToolBarMap[toolbarID]; action = new PluginAction(text,pToolbar); pToolbar->addAction(action); } action->setObjectName(text); connect(action,SIGNAL(triggered()),this,SLOT(handlePluginAction())); if ( text.isEmpty() ) action->setPriority(QAction::LowPriority); else action->setText(text); action->setToolTip(tooltip); action->setCheckable(true); action->setData(actionID); m_ActionMap[actionID] = action; if ( !iconName.isEmpty() ) { #ifdef Q_WS_MAC iconName.replace('\\',QDir::separator()); #endif // set icon for action (safe to call this method as we should be in MainThread) setButtonIconInternal_deprecated(actionID, iconName); } // make sure we enable plugin actions for shortcut customization mainWindow.addAction(action); // for ribbon bar mode, check if we need to add created action in quick access bar if (mainWindow.isRibbonStyle()) { RobloxQuickAccessConfig& quickAccessConfig = RobloxQuickAccessConfig::singleton(); if (QAction* mappedAction = quickAccessConfig.mapProxyAction(action)) quickAccessConfig.addToQuickAccessBar(mappedAction, quickAccessConfig.visibleInQuickAccessBar(action)); // QTitanRibbon doesn't recalculate group width, make sure we force the width recalculation Qtitan::RibbonGroup* pGroup = m_RibbonGroupMap[toolbarID]; if (pGroup) { pGroup->recalcWidths(); QEvent evt(QEvent::LayoutRequest); QApplication::sendEvent(pGroup->parentWidget(), &evt); } } } void RobloxPluginHost::onSetButtonActive(int buttonID, bool active) { if ( !m_ActionMap.contains(buttonID) ) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Plugin attempting to set active state for missing button ID - %i", buttonID); return; } QAction *pAction = m_ActionMap[buttonID]; pAction->setChecked(active); //make sure we set the property so it can be used to correctly status in ribbon bar pAction->setProperty(kIsChecked, active); if (!FFlag::StudioDisableToolbarUpdateOnButtonActive) QTimer::singleShot(1, &UpdateUIManager::Instance(), SLOT(updateToolBars())); } bool RobloxPluginHost::event(QEvent* evt) { if ( evt->type() != PLUGIN_LOAD ) return QObject::event(evt); RobloxCustomEventWithArg *pCustomEvent = dynamic_cast(evt); if ( pCustomEvent ) { boost::function* pFunctionObj = pCustomEvent->m_pEventArg; if ( pFunctionObj ) (*pFunctionObj)(); } return true; } void RobloxPluginHost::loadPlugins(RBX::DataModel* dataModel) { if ( !dataModel ) return; // Old style plugins loadPluginsFromPath(dataModel,RobloxPluginHost::builtInPluginPath()); loadPluginsFromPath(dataModel,RobloxPluginHost::userPluginPath()); findAndLoadModelPlugins(dataModel, builtInPluginPath()); loadInstalledModelPlugins(dataModel); findAndLoadModelPlugins(dataModel, userPluginPath()); RBX::PluginManager::singleton()->startModelPluginScripts(dataModel); } void RobloxPluginHost::loadPluginsFromPath(RBX::DataModel* dataModel,const QString &srcPath) { if ( !dataModel || srcPath.isEmpty() ) return; UpdateUIManager::Instance().getMainWindow().pluginHost().loadPluginsInternal(dataModel,srcPath); } void RobloxPluginHost::loadPluginsInternal(RBX::DataModel* dataModel,const QString& srcPath) { QDir srcDir(srcPath); QFileInfoList fileList = srcDir.entryInfoList(QStringList("*.lua"),QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot); QFileInfo fileInfo; for ( int ii = 0 ; ii < fileList.size() ; ++ii ) { fileInfo = fileList.at(ii); if ( fileInfo.isSymLink() ) continue; if ( fileInfo.isDir() ) loadPluginsInternal(dataModel, fileInfo.absoluteFilePath()); else QApplication::postEvent(this, new RobloxCustomEventWithArg(PLUGIN_LOAD,boost::bind(&RobloxPluginHost::doLoadPlugin,dataModel,fileInfo.absoluteFilePath()))); } } void RobloxPluginHost::doLoadPlugin(RBX::DataModel* dataModel, QString filename) { boost::mutex::scoped_lock lock(RBX::PluginManager::singleton()->mutex); QFile file(filename); if ( !file.open(QIODevice::ReadOnly | QIODevice::Text) ) return; QTextStream inStream(&file); QString toExecute = inStream.readAll(); if ( toExecute.isEmpty() ) return; QByteArray baToExe = toExecute.toAscii(); const char* c_strToExe = baToExe.data(); QByteArray baFileName = filename.toAscii(); const char* c_strFileName = baFileName.data(); RBX::PluginManager::singleton()->setLastPath(QFileInfo(filename).absoluteDir().absolutePath().toStdString()); try { RBX::DataModel::LegacyLock legacylock(dataModel, RBX::DataModelJob::Write); boost::shared_ptr pContext = RBX::shared_from(dataModel->create()); if (pContext) pContext->executeInNewThread(RBX::Security::StudioPlugin, RBX::ProtectedString::fromTrustedSource(c_strToExe), c_strFileName); } catch (std::exception &e) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR,e.what()); } } void RobloxPluginHost::findAndLoadModelPlugins(RBX::DataModel* dataModel, const QString& srcPath) { QStringList extensions; extensions += "*.rbxm"; extensions += "*.rbxmx"; QDir srcDir(srcPath); QFileInfoList fileList = srcDir.entryInfoList(extensions, QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks); QFileInfo fileInfo; for (int ii = 0 ; ii < fileList.size() ; ++ii) { fileInfo = fileList.at(ii); if (!fileInfo.isDir()) { doLoadModelPlugin(dataModel, fileInfo.absoluteFilePath(), -1); } } } void RobloxPluginHost::loadInstalledModelPlugins(RBX::DataModel* dataModel) { shared_ptr values(new RBX::Reflection::ValueTable); QString pluginMetadataString = getPluginMetadataJson(); std::stringstream jsonStream(pluginMetadataString.toUtf8().constData()); if (!pluginMetadataString.isEmpty() && !RBX::WebParser::parseJSONTable(pluginMetadataString.toUtf8().constData(), values)) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, qPrintable(tr("Unable to load metadata for installed plugins. " "Installed plugins will not be loaded."))); return; } QDir basePluginFolder(modelPluginsPath()); for (RBX::Reflection::ValueTable::const_iterator itr = values->begin(); itr != values->end(); ++itr) { // the key in the map should be asset id QString assetId = QString::fromStdString(itr->first); bool intConversionWorked = true; int intAssetId = assetId.toInt(&intConversionWorked); if (!intConversionWorked) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_WARNING, qPrintable(tr("Error parsing plugin asset id for plugin: %s")), qPrintable(assetId)); continue; } // skip the plugin if it marked as not enabled shared_ptr valueTable = itr->second.cast >(); RBX::Reflection::ValueTable::const_iterator enabledData = valueTable->find(kEnabledJsonKey); if (enabledData != valueTable->end() && enabledData->second.cast() == false) { continue; } QDir subFolder = basePluginFolder; QString assetFile = QString(kPluginFileName); if (!subFolder.cd(assetId) || !subFolder.exists(assetFile)) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_WARNING, qPrintable(tr("Unable to find plugin with the following id: %s")), qPrintable(assetId)); continue; } // everything is good! load the plugin doLoadModelPlugin(dataModel, subFolder.absoluteFilePath(assetFile), intAssetId); } } void RobloxPluginHost::doLoadModelPlugin(RBX::DataModel* dataModel, const QString& filename, int assetId) { std::ifstream stream(qPrintable(filename), std::ios_base::in | std::ios_base::binary); if (!stream.is_open()) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, qPrintable(tr("Error while trying to open plugin file: %s")), qPrintable(filename)); return; } try { // Read the items RBX::Instances instances; Serializer().loadInstances(stream, instances); if (RBX::ContentProvider* cp = dataModel->create()) { RBX::LuaSourceContainer::blockingLoadLinkedScriptsForInstances(cp, instances); } RBX::PluginManager::singleton()->addModelPlugin(dataModel, instances, assetId); } catch (const std::runtime_error&) // xml parse can throw { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, qPrintable(tr("Unable to open plugin file (bad file format): "))); RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "%s", qPrintable(filename)); } } QString RobloxPluginHost::builtInPluginPath() { return RobloxSettings::getBuiltInPluginsFolder(); } QString RobloxPluginHost::userPluginPath() { return AuthoringSettings::singleton().pluginsDir.absolutePath(); } QString RobloxPluginHost::modelPluginsPath() { return AuthoringSettings::singleton().modelPluginsDir.absolutePath(); } void RobloxPluginHost::processInstallPluginFromWebsite(int assetId, int assetVersion, std::string* serialized, std::exception* error, boost::function resultCallback) { QString assetIdString = QString("%1").arg(assetId); // Track an event for each attempt to install a plugin if (FFlag::GoogleAnalyticsTrackingEnabled) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "PluginInstall", assetIdString.toStdString().c_str()); } if (error || !serialized || serialized->empty() || userPluginPath().isEmpty()) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, qPrintable(tr("Unable to download plugin at this time. Try again later."))); if (FFlag::GoogleAnalyticsTrackingEnabled) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ERROR, "PluginInstallFailed", assetIdString.toStdString().c_str()); } resultCallback(false); return; } QDir pluginFolder = getSpecificPluginFolder(assetId); QFile pluginFile(pluginFolder.absoluteFilePath(kPluginFileName)); if (!pluginFile.open(QIODevice::Truncate | QIODevice::WriteOnly) || !pluginFile.write(QByteArray(serialized->c_str(), serialized->size()))) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, qPrintable(tr("Unable to create plugin file."))); if (FFlag::GoogleAnalyticsTrackingEnabled) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ERROR, "PluginInstallFailed", assetIdString.toStdString().c_str()); } resultCallback(false); return; } pluginFile.flush(); // Update Metadata { shared_ptr metadata(new RBX::Reflection::ValueTable); if (readPluginMetadata(metadata)) { std::string assetIdStdString(qPrintable(assetIdString)); RBX::Reflection::ValueTable::iterator pluginData = metadata->find(assetIdStdString); shared_ptr newData; if (pluginData == metadata->end()) { newData.reset(new RBX::Reflection::ValueTable); (*newData)[kEnabledJsonKey] = true; } else { if (FFlag::GoogleAnalyticsTrackingEnabled) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "PluginReInstall", assetIdString.toStdString().c_str()); } newData.reset(new RBX::Reflection::ValueTable( *(pluginData->second.cast >()))); } (*newData)[kAssetVersionJsonKey] = assetVersion; (*metadata)[assetIdStdString] = shared_ptr(newData); writePluginMetadata(metadata); } } RBX::StandardOut::singleton()->print(RBX::MESSAGE_INFO, qPrintable(tr("Finished installing plugin. Open a new window to start using it."))); resultCallback(true); } QString RobloxPluginHost::getPluginMetadataJson() { QFile pluginMetadataFile(getPluginMetadataFilePath()); if (!pluginMetadataFile.open(QIODevice::ReadOnly | QIODevice::Text)) { return QString(); } return QTextStream(&pluginMetadataFile).readAll(); } void RobloxPluginHost::setPluginEnabled(int assetId, bool enabled) { std::string assetIdString(qPrintable(QString("%1").arg(assetId))); if (FFlag::GoogleAnalyticsTrackingEnabled) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, enabled ? "EnablePlugin" : "DisablePlugin", QString::fromUtf8(assetIdString.c_str()).toStdString().c_str()); } shared_ptr metadata(new RBX::Reflection::ValueTable); if (readPluginMetadata(metadata)) { RBX::Reflection::ValueTable::iterator itr = metadata->find(assetIdString); if (itr == metadata->end()) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, qPrintable(tr("Unable to set enabled/disabled state for plugin: %d")), assetId); return; } shared_ptr newData( new RBX::Reflection::ValueTable( *(itr->second.cast >()))); (*newData)[kEnabledJsonKey] = enabled; (*metadata)[assetIdString] = shared_ptr(newData); writePluginMetadata(metadata); } } void RobloxPluginHost::uninstallPlugin(int assetId) { if (modelPluginsPath().isEmpty()) { // If we don't know where the model plugins should be, we can't delete. return; } QString assetIdString = QString("%1").arg(assetId); std::string assetIdStdString(qPrintable(assetIdString)); if (FFlag::GoogleAnalyticsTrackingEnabled) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ACTION, "UninstallPlugin", assetIdString.toStdString().c_str()); } { shared_ptr metadata(new RBX::Reflection::ValueTable); if (readPluginMetadata(metadata)) { metadata->erase(assetIdStdString); writePluginMetadata(metadata); } } QDir removingPluginPath(getSpecificPluginFolder(assetId)); bool cleanPluginFileSucceeded = !removingPluginPath.exists(kPluginFileName) || removingPluginPath.remove(kPluginFileName); bool cleanPluginSettingsSucceeded = !removingPluginPath.exists(kPluginSettingsFileName) || removingPluginPath.remove(kPluginSettingsFileName); bool removeDirectorySucceeded = false; removeDirectorySucceeded = !removingPluginPath.exists() || (removingPluginPath.cdUp() && removingPluginPath.rmdir(assetIdString)); if (!cleanPluginFileSucceeded || !cleanPluginSettingsSucceeded || !removeDirectorySucceeded) { RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, qPrintable(tr("Unable to remove all of the plugin files in directory %s. " "Please clean this location manually")), qPrintable(removingPluginPath.absolutePath())); } } QDir RobloxPluginHost::getSpecificPluginFolder(int assetId) { QString assetIdString = QString("%1").arg(assetId); QDir result(modelPluginsPath()); result.mkpath(QString("%1").arg(assetIdString)); result.cd(assetIdString); return result; } QString RobloxPluginHost::getPluginMetadataFilePath() { static const char* kPluginMetadataFilename = "PluginMetadata.json"; return QDir(modelPluginsPath()).absoluteFilePath(kPluginMetadataFilename); } bool RobloxPluginHost::readPluginMetadata(shared_ptr& values) { if (userPluginPath().isEmpty()) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_WARNING, qPrintable(tr("Warning: unable to add new plugin to metadata file."))); return false; } QString pluginMetadataString = getPluginMetadataJson(); std::stringstream jsonStream(pluginMetadataString.toUtf8().constData()); if (!pluginMetadataString.isEmpty()) { shared_ptr readValues; if (!RBX::WebParser::parseJSONTable(pluginMetadataString.toUtf8().constData(), readValues) ) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_WARNING, qPrintable(tr("Plugin metadata file is corrupted. " "Version information for your plugins may have been lost."))); return false; } *values = *readValues; } return true; } void RobloxPluginHost::writePluginMetadata(const shared_ptr& values) { std::stringstream outputBuffer; bool needComma = false; outputBuffer << "{"; RBX::Stats::JsonWriter writer(outputBuffer, needComma); writer.writeTableEntries(*values); outputBuffer << "}"; QFile pluginMetadataFile(getPluginMetadataFilePath()); if (!pluginMetadataFile.open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text) || !pluginMetadataFile.write(outputBuffer.str().c_str())) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, qPrintable(tr("Unable to write plugin metadata. " "Version information for your plugins may be lost."))); } } void RobloxPluginHost::readPluginSettings(int assetId, boost::shared_ptr& settings) { QDir pluginFolder = getSpecificPluginFolder(assetId); QString settingsFileContents; { QFile settingsFile(pluginFolder.absoluteFilePath(kPluginSettingsFileName)); if (settingsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { settingsFileContents = settingsFile.readAll(); } } // initialize readValues to an empty table, in case the settings cannot be read from file. shared_ptr readValues = rbx::make_shared(); if (!settingsFileContents.isEmpty()) { std::stringstream jsonStream(settingsFileContents.toStdString()); if ( RBX::WebParser::parseJSONTable(settingsFileContents.toStdString(), readValues) ) { *settings = *readValues; } else { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, qPrintable(tr("Unable to parse settings file for plugin"))); if (FFlag::GoogleAnalyticsTrackingEnabled) { RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ERROR, "ParsePluginSettingsFailed", QString("%1").arg(assetId).toStdString().c_str()); } } } } void RobloxPluginHost::writePluginSettings(int assetId, const boost::shared_ptr& settings) { std::stringstream stream; bool needComma = false; stream << "{"; RBX::Stats::JsonWriter writer(stream, needComma); writer.writeTableEntries(*settings.get()); stream << "}"; QDir pluginFolder = getSpecificPluginFolder(assetId); QFile settingsFile(pluginFolder.absoluteFilePath(kPluginSettingsFileName)); if (settingsFile.open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)) { settingsFile.write(stream.str().c_str()); } } void RobloxPluginHost::init() { // All must be done on the main thread // Create our Add-ons and Terrain special pages for Ribbon RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if ( mainWindow.isRibbonStyle() ) { RobloxQuickAccessConfig::singleton().setPluginHostInitialized(true); if ( !m_PluginsToolbar ) m_PluginsToolbar = mainWindow.ribbonBar()->findChild(PluginToolbarName); if ( !m_TerrainToolbar ) { const int pagesCount = mainWindow.ribbonBar()->getPageCount(); for ( int i = 0 ; i < pagesCount ; ++i ) { RibbonPage* page = mainWindow.ribbonBar()->getPage(i); if ( page->objectName() != TerrainToolbarName ) continue; m_TerrainToolbar = page; const QList ribbonGroups = mainWindow.ribbonBar()->getPage(i)->findChildren(); for (int j = 0; j < ribbonGroups.count(); j++) { RibbonGroup* group = ribbonGroups[j]; if ( group->objectName() != TerrainToolbarName ) continue; m_TerrainToolbarID = m_NextID++; m_RibbonGroupMap[m_TerrainToolbarID] = group; const QList& actions = group->actions(); for ( int k = 0 ; k < actions.size() ; ++k ) { QAction* action = actions[k]; const int actionID = m_NextID++; m_ActionMap[actionID] = action; m_TextActionMap[action->text()] = actionID; action->setCheckable(true); action->setData(actionID); if (action->text() == "Convert To Smooth") { m_ConvertToSmoothID = actionID; } connect(action,SIGNAL(triggered()),this,SLOT(handlePluginAction())); } } } if (RibbonPage* page = mainWindow.ribbonBar()->findChild("Model")) { if (RibbonGroup* group = page->findChild("Tools")) { m_TransformToolbarID = m_NextID++; m_RibbonGroupMap[m_TransformToolbarID] = group; const QList& actions = group->actions(); for (int k = 0; k < actions.size(); ++k) { QAction* action = actions[k]; if (action->objectName() != TransformToolbarName) continue; const int actionID = m_NextID++; m_ActionMap[actionID] = action; m_TextActionMap[action->text()] = actionID; action->setCheckable(true); action->setData(actionID); connect(action, SIGNAL(triggered()),this,SLOT(handlePluginAction())); break; } } } } } } void RobloxPluginHost::removeActionsFromMap(QObject* parent) { QMap::iterator iter = m_ActionMap.begin(); while ( iter != m_ActionMap.end() ) { if ( iter.value()->parent() == parent ) iter = m_ActionMap.erase(iter); else ++iter; } } void RobloxPluginHost::handleTerrainRibbonGroupDeletion() { RibbonGroup* group = m_RibbonGroupMap[m_TerrainToolbarID]; if (!group) return; const QList& actions = group->actions(); for ( int k = 0 ; k < actions.size() ; ++k ) { if (!m_TextActionMap.contains(actions.at(k)->text())) group->removeAction(actions.at(k)); } removeActionsFromMap(group); group->setVisible(true); } QString RobloxPluginHost::getToolbarName(int id) const { RobloxMainWindow& mainWindow = UpdateUIManager::Instance().getMainWindow(); if (mainWindow.isRibbonStyle()) { RibbonGroup* group = m_RibbonGroupMap[id]; if (group) return group->objectName(); } else { QToolBar* toolbar = m_ToolBarMap[id]; if (toolbar) return toolbar->objectName(); } return QString(); } bool RobloxPluginHost::actionState(const QString &actionID, bool &enableState, bool &checkedState) { QAction* pAction = NULL; QMapIterator iter(m_ActionMap); while (iter.hasNext()) { iter.next(); pAction = iter.value(); if (pAction && ((pAction->objectName() == actionID) || (pAction->objectName() + "_proxyAct" == actionID)) && pAction->property(kIsVisible).toBool()) { enableState = true; checkedState = pAction->property(kIsChecked).toBool(); return true; } } return false; } void RobloxPluginHost::openScriptDoc(shared_ptr instance, int lineNumber) { if (RBX::Instance::fastSharedDynamicCast(instance) || RBX::Instance::fastSharedDynamicCast(instance)) QMetaObject::invokeMethod(&RobloxDocManager::Instance(), "openDoc", Qt::QueuedConnection, Q_ARG(shared_ptr, instance), Q_ARG(int, lineNumber)); } void RobloxPluginHost::openWikiPage(const std::string& url) { UpdateUIManager::Instance().setDockVisibility(eDW_CONTEXTUAL_HELP, true); QMetaObject::invokeMethod(&UpdateUIManager::Instance().getViewWidget(eDW_CONTEXTUAL_HELP), "onHelpTopicChanged", Q_ARG(QString, QString::fromStdString(url))); } void RobloxPluginHost::exportPlace(std::string filePath, RBX::ExporterSaveType exportType) { RobloxIDEDoc* playDoc = RobloxDocManager::Instance().getPlayDoc(); if (!playDoc) return; playDoc->exportSelection(filePath, exportType); } void RobloxPluginHost::promptForExistingAssetId(const std::string& assetType, boost::function resumeFunction, boost::function errorFunction) { if (FFlag::StudioEnableGameAnimationsTab) { RobloxIDEDoc* playDoc = RobloxDocManager::Instance().getPlayDoc(); if (playDoc) playDoc->promptForExistingAssetId(assetType, resumeFunction, errorFunction); } else { errorFunction("Requested functionality is not available."); } } void operationFailed(const char* title, const char* msg) { throw RBX::runtime_error("%s %s", title, msg); } shared_ptr RobloxPluginHost::csgUnion(shared_ptr instances, RBX::DataModel* dataModel) { if (!FFlag::EnableCSGScripting) throw std::runtime_error("Scripted CSG operations are disabled"); if (instances->size() == 0) throw std::runtime_error("Union function requires instances; no instances passed."); RBX::CSGOperations csgOps( dataModel, operationFailed ); shared_ptr partOperation; csgOps.doUnion( instances->begin(), instances->end(), partOperation ); return partOperation; } shared_ptr RobloxPluginHost::csgNegate(shared_ptr instances, RBX::DataModel* dataModel) { if (!FFlag::EnableCSGScripting) throw std::runtime_error("Scripted CSG operations are disabled"); if (instances->size() == 0) throw std::runtime_error("Negate function requires instances; no instances passed."); RBX::CSGOperations csgOps( dataModel, operationFailed ); std::vector > toSelect; csgOps.doNegate( instances->begin(), instances->end(), toSelect ); shared_ptr result = rbx::make_shared(toSelect.size()); int i = 0; for ( std::vector >::const_iterator iter = toSelect.begin(); iter != toSelect.end(); iter++, i++ ) (*result)[i] = (*iter); return result; } shared_ptr RobloxPluginHost::csgSeparate(shared_ptr instances, RBX::DataModel* dataModel) { if (!FFlag::EnableCSGScripting) throw std::runtime_error("Scripted CSG operations are disabled"); if (instances->size() == 0) throw std::runtime_error("Separate function requires instances; no instances passed."); RBX::CSGOperations csgOps( dataModel, operationFailed ); std::vector > ungroupedItems; csgOps.doSeparate( instances->begin(), instances->end(), ungroupedItems ); shared_ptr result = rbx::make_shared(ungroupedItems.size()); int i = 0; for ( std::vector >::const_iterator iter = ungroupedItems.begin(); iter != ungroupedItems.end(); iter++, i++ ) (*result)[i] = (*iter); return result; } void RobloxPluginHost::handleDragEnterEvent(RBX::DataModel* dataModel, shared_ptr instances, RBX::Vector2 location) { if (!m_pNotifier) return; m_pNotifier->fireDragEnterEvent(dataModel, instances, location); }