Files
aya/client/studio/src/RobloxGameExplorer.hpp
2025-12-17 16:47:48 +00:00

390 lines
13 KiB
C++

#pragma once
#include "Reflection/Type.hpp"
#include "CEvent.hpp"
#include "Script/LuaSourceContainer.hpp"
#include "Utility/ContentId.hpp"
#include "Utility/HttpAsync.hpp"
#include "DataModel/ContentProvider.hpp"
#include <QIcon>
#include <QPointer>
#include <QString>
#include <QKeyEvent>
#include <QLineEdit>
#include <QTreeView>
// https://stackoverflow.com/questions/22597948/using-boostfuture-with-then-continuations
#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
#define BOOST_THREAD_PROVIDES_FUTURE
#include <boost/function.hpp>
#include <boost/optional.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/future.hpp>
class QLabel;
class QPushButton;
class QStandardItem;
class QStandardItemModel;
enum EntityCategory
{
ENTITY_CATEGORY_Places, // the node that places are inserted under
ENTITY_CATEGORY_DeveloperProducts, // The node that dev products are inserted under
ENTITY_CATEGORY_Badges,
ENTITY_CATEGORY_NamedAssets,
ENTITY_CATEGORY_MAX
};
enum AliasType
{
ALIAS_TYPE_Asset = 1,
ALIAS_TYPE_DeveloperProduct = 2,
ALIAS_TYPE_AssetVersion = 3,
};
enum AssetTypeId
{
ASSET_TYPE_ID_Image = 1,
ASSET_TYPE_ID_Script = 5,
ASSET_TYPE_ID_Animation = 24
};
// Caller is responsible for thread safety when using setter methods
class EntityProperties
{
public:
EntityProperties();
void setFromJsonFuture(const boost::shared_future<std::string>& jsonFuture);
void setFromValueTable(const boost::shared_ptr<const Aya::Reflection::ValueTable>& valueTable);
void setFromEntitySettings(EntityProperties& other);
std::string asJson();
void waitUntilLoaded();
bool isLoadedAndParsed() const;
template<class T>
boost::optional<T> get(const std::string& fieldName)
{
boost::optional<Aya::Reflection::Variant> var = getVariant(fieldName);
if (var.is_initialized() && var->isType<T>())
{
return var->cast<T>();
}
return boost::optional<T>();
}
boost::optional<Aya::Reflection::Variant> getVariant(const std::string& fieldName);
// Caller is responsible for ensuring the thread safety of this call
template<class T>
void set(const std::string& fieldName, const T& value)
{
waitUntilLoaded();
data[fieldName] = value;
}
typedef std::vector<EntityProperties> EntityCollection;
void addChild(const EntityProperties& child)
{
children.push_back(child);
}
EntityCollection& getChildren()
{
return children;
}
private:
bool hasPendingFuture;
boost::shared_future<std::string> jsonFuture;
bool ableToGetFuture;
bool ableToParseJson;
Aya::Reflection::ValueTable data;
EntityCollection children;
};
class UniverseSettings
{
public:
void setFromJsonFuture(const boost::shared_future<std::string>& jsonFuture);
void waitUntilLoaded();
bool isLoadedAndParsed() const;
bool hasRootPlace();
int rootPlaceId();
std::string getName();
void setName(const std::string& newName);
bool currentUserHasAccess();
std::string asJson();
private:
EntityProperties properties;
};
struct CategoryWebSettings
{
std::string jsonItemGroupFieldName;
std::string jsonItemIdFieldName;
std::string perItemIdFieldName;
std::string fetchPageUrlFormatString;
std::string createFormatString;
std::string updateFormatString;
std::string removeFormatString;
std::string getContentFormatString;
std::string writeContentFormatString;
boost::function<void(int, EntityProperties*)> propertiesFixer;
};
class EntityPropertiesForCategory : public boost::enable_shared_from_this<EntityPropertiesForCategory>
{
public:
typedef boost::shared_ptr<EntityPropertiesForCategory> ref;
static ref loadFromWeb(const CategoryWebSettings& webSettings, int universeId, boost::function<void()> doneLoadingCallback);
bool isLoadedAndParsed();
template<class IdType>
void publishTo(ref target, const std::vector<std::string>& ignoredFields, boost::unordered_map<IdType, boost::function<IdType()>>* idRemaps);
template<class T>
void transform(boost::function<T(EntityProperties*)> transform, std::vector<T>* out)
{
waitUntilLoaded();
for (EntityCollection::iterator itr = data.begin(); itr != data.end(); ++itr)
{
out->push_back(transform(&(*itr)));
}
}
private:
EntityPropertiesForCategory(const CategoryWebSettings& webSettings, const int universeId);
const CategoryWebSettings& webSettings;
const int universeId;
Aya::CEvent doneEvent;
boost::function<void()> doneLoadingCallback;
typedef std::vector<EntityProperties> EntityCollection;
EntityCollection data;
void requestPage(int page);
void handlePage(int page, std::string* json, std::exception* error);
void waitUntilLoaded();
};
class AddImageDialog : public QObject
{
Q_OBJECT
public:
void runModal(QWidget* parent, bool* created, QString* newName);
private:
QDialog* dialog;
QLineEdit* nameEdit;
QLabel* nameErrorMessage;
QPushButton* fileNameEdit;
QLabel* fileNameLabel;
QLabel* fileNameErrorMessage;
QLabel* generalErrorMessage;
int currentGameId;
boost::optional<int> currentGameGroupId;
bool* created;
QString* newName;
std::vector<QString> usedNames;
void createImageAndNameThread();
private Q_SLOTS:
bool validateName();
bool validateImageFile();
void openFileSelector();
void createButtonClicked();
};
class AbortableLineEdit : public QLineEdit
{
Q_OBJECT
public:
AbortableLineEdit(boost::function<std::string()> getLastValue)
: QLineEdit("")
, getLastValue(getLastValue)
{
}
protected:
virtual void keyReleaseEvent(QKeyEvent* keyEvent)
{
if (keyEvent->key() == Qt::Key_Escape)
{
setText(QString::fromStdString(getLastValue()));
clearFocus();
}
if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return)
{
clearFocus();
}
QLineEdit::keyReleaseEvent(keyEvent);
}
private:
boost::function<std::string()> getLastValue;
};
class RobloxGameExplorer
: public QTreeView
, public Aya::AssetFetchMediator
{
Q_OBJECT
public:
RobloxGameExplorer(QWidget* parent);
// Asset management functions
virtual boost::optional<std::string> findCachedAssetOrEmpty(const Aya::ContentId& contentId, int universeId);
void updateAsset(const Aya::ContentId& contentId, const std::string& newContents);
void notifyIfUnpublishedChanges(int universeId);
void publishNamedAssetsToCurrentSlot();
static Aya::HttpFuture publishScriptAsset(
const std::string& scriptText, boost::optional<int> optionalAssetId, boost::optional<int> optionalGroupId);
// These methods must be run from main thread for concurrency protection:
void getListOfImages(std::vector<QString>* out);
void getListOfScripts(std::vector<QString>* out);
void getListOfAnimations(std::vector<QString>* out);
int getCurrentGameId() const
{
return currentGameId;
}
boost::optional<int> getCurrentGameGroupId()
{
return currentGameGroupId;
}
QString getAnimationsDataJson();
Q_SIGNALS:
void namedAssetsLoaded(int gameId);
void namedAssetModified(int gameId, const QString& assetName);
public Q_SLOTS:
void nonGameLoaded();
void openGameFromGameId(int gameId);
void openGameFromPlaceId(int placeId);
void onCloseIdeDoc();
void publishGameToNewSlot();
void publishGameToNewGroupSlot(int groupId);
void publishGameToExistingSlot(int gameId);
void reloadDataFromWeb();
private:
struct CategoryUiSettings
{
enum NameMode
{
FlatNames,
NameAsFolderPath
};
QString groupDisplayName;
QIcon icon;
boost::function<QString(EntityProperties*)> displayNameGetter;
boost::function<bool(EntityProperties*)> shouldBuildExplorerItemCallback;
boost::function<void(EntityProperties*)> doubleClickCallback;
// The QPoint provided will be global (ready to use for showing context menu)
boost::function<void(const QPoint&, EntityProperties*)> contextMenuCallback;
boost::function<void(const QPoint&)> groupContextMenuCallback;
boost::function<void(QStandardItem*)> nameChangedCallback;
NameMode nameMode;
};
struct NamedAssetCopyInfo
{
std::string assetName;
int aliasType;
bool needsTargetIdUpdate;
EntityProperties newAssetIdProperties;
boost::function<int()> newTargetIdGetter;
};
int currentGameId;
int currentPlaceId;
// This widget is stateful, and does a lot of async requests for data. Use
// a session identifier to throw out async requests that are stale (e.g.
// request places for a universe, then the widget is pointed to a different
// universe before the request returns).
int currentSessionId;
boost::optional<int> currentGameGroupId;
bool enoughDataLoadedToBeAbleToOpenStartPlace;
bool startPlaceOpenRequested;
boost::unordered_map<EntityCategory, CategoryUiSettings> uiSettings;
boost::unordered_map<EntityCategory, EntityPropertiesForCategory::ref> entitySettings;
UniverseSettings gameSettings;
QLabel* nameLabel;
AbortableLineEdit* gameNameEdit;
QLabel* idLabel;
QPushButton* reloadButton;
static void handleUniverseForPlaceResponse(
QPointer<RobloxGameExplorer> explorer, int originatingSessionId, int placeId, std::string* data, std::exception* error);
static void doneLoadingDataForCategory(QPointer<RobloxGameExplorer> explorer, int originatingSessionId, EntityCategory category);
static QStandardItem* buildItem(
const CategoryUiSettings& uiSettings, const CategoryWebSettings& webSettings, QStandardItem* root, EntityProperties* settings);
QStandardItemModel* getModel();
QStandardItem* findGroup(EntityCategory type);
QStandardItem* makeGroup(EntityCategory type);
void openAndFocusConfigureDoc(const std::string& url);
void setGroupLoadingStatus(EntityCategory category);
void placeDoubleClickCallback(EntityProperties* placeInfo);
void namedAssetsDoubleClickCallback(EntityProperties* assetInfo);
void openPlace(int placeId);
void updateItemForOpenedPlaceState(QStandardItem* placeItem);
void refreshOpenedPlaceIndicator();
void openStartPlaceIfRequestedAndNoPlaceOpenedAndDataReady();
void publishInternal(boost::function<int()> newUniverseFuture);
std::pair<int, boost::function<std::string()>> buildPlaceContentPair(EntityProperties* properties);
NamedAssetCopyInfo buildNamedContentInfo(EntityPropertiesForCategory::ref targetProperties, int sourceUnviverseId,
boost::optional<int> targetUniverseGroupId, boost::unordered_map<int, boost::function<int()>>* devProductIdRemap,
EntityProperties* properties);
void publishGameThread(boost::function<int()> newUniverseFuture, bool* publishSucceeded, int* targetGameId);
void addNewPlace();
void placeContextMenuHandler(const QPoint& point, EntityProperties* properties);
void developerProductContextMenuHandler(const QPoint& point, EntityProperties* properties);
void namedAssetsContextMenuHandler(const QPoint& point, EntityProperties* properties);
void badgesContextMenuHandler(const QPoint& point, EntityProperties* properties);
void badgesPlaceContextMenuHandler(const QPoint& point, EntityProperties* properties);
void placeGroupContextMenuHandler(const QPoint& point);
void developerProductGroupContextMenuHandler(const QPoint& point);
void namedAssetsGroupContextMenuHandler(const QPoint& point);
void handleRename(const QPoint& globalPoint);
void handleRemoveAssetName(const std::string& name, bool* needReload);
void bulkAddNewImageNames();
void insertNamedImage(EntityProperties* imageInfo);
void insertNamedScript(EntityProperties* scriptInfo, shared_ptr<Aya::LuaSourceContainer> container);
void checkRowForNameUpdate(EntityCategory category, QStandardItem* entityRow);
void afterNamedAssetsFinishedRecursive(QStandardItem* root, QStringList& imagesToReload);
bool namedAssetHasUnpublishedChanges(const std::string& name);
boost::shared_future<void> publishIfThereAreLocalModifications(EntityProperties* settings);
private Q_SLOTS:
void doOpenGameFromGameId(int gameId);
void failedToLoadSettings(int originatingSessionId);
void populateWithLoadedData(int originatingSessionId, int category);
void afterPlacesLoadedFinished(int originatingSessionId);
void afterNamedAssetsFinished(int originatingSessionId);
void refreshNamedScriptIcons(int originatingSessionId);
void afterBadgesFinished(int originatingSessionId);
void thumbnailLoadedForImage(int originatingSessionId, QModelIndex item, QVariant future);
void doubleClickHandler(const QModelIndex& modelIndex);
// The QPoint provided will be relative to the viewport
void contextMenuHandler(const QPoint& clickPoint);
void itemChangedHandler(QStandardItem* updatedItem);
void handleNameUpdates();
void doneEditingUniverseName();
};