forked from aya/aya
Initial commit
This commit is contained in:
389
client/studio/src/RobloxGameExplorer.hpp
Normal file
389
client/studio/src/RobloxGameExplorer.hpp
Normal file
@@ -0,0 +1,389 @@
|
||||
#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();
|
||||
};
|
||||
Reference in New Issue
Block a user