From 2ad37db8174b0b25acd86e8d863de0edf79032a5 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 2 Mar 2020 23:55:18 +0000 Subject: [PATCH] Implement registering game actions --- distribution/openrct2.d.ts | 18 ++--- src/openrct2-ui/windows/Error.cpp | 2 +- src/openrct2/scripting/ScContext.hpp | 79 ++++++++++++--------- src/openrct2/scripting/ScriptEngine.cpp | 92 ++++++++++++++++++++----- src/openrct2/scripting/ScriptEngine.h | 21 +++++- 5 files changed, 149 insertions(+), 63 deletions(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 188c7d16a1..d73baafa55 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -80,12 +80,6 @@ declare global { expenditureType: ExpenditureType; } - interface GameActionDesc { - id: string; - query: (args: object) => GameActionResult; - execute: (args: object) => GameActionResult; - } - interface NetworkEventArgs { readonly player: number; } @@ -122,8 +116,15 @@ declare global { /** * Registers a new game action that allows clients to interact with the game. + * @param action The unique name of the action. + * @param query Logic for validating and returning a price for an action. + * @param execute Logic for validating and executing the action. + * @throws An error if the action has already been registered by this or another plugin. */ - registerGameAction(desc: GameActionDesc): void; + registerGameAction( + action: string, + query: (args: object) => GameActionResult, + execute: (args: object) => GameActionResult): void; /** * Query the result of running a game action. This allows you to check the outcome and validity of @@ -156,8 +157,7 @@ declare global { subscribe(hook: "network.leave", callback: (e: NetworkEventArgs) => void): IDisposable; } - interface IntentDesc - { + interface IntentDesc { key: string; title?: string; shortcut?: string; diff --git a/src/openrct2-ui/windows/Error.cpp b/src/openrct2-ui/windows/Error.cpp index f7296cdb77..b7cad6960d 100644 --- a/src/openrct2-ui/windows/Error.cpp +++ b/src/openrct2-ui/windows/Error.cpp @@ -111,7 +111,7 @@ rct_window* window_error_open(rct_string_id title, rct_string_id message) gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM; width = gfx_get_string_width_new_lined(_window_error_text); - width = std::min(196, width); + width = std::clamp(width, 64, 196); gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM; gfx_wrap_string(_window_error_text, width + 1, &numLines, &fontHeight); diff --git a/src/openrct2/scripting/ScContext.hpp b/src/openrct2/scripting/ScContext.hpp index 8c7893ac1a..f6e72fbf1e 100644 --- a/src/openrct2/scripting/ScContext.hpp +++ b/src/openrct2/scripting/ScContext.hpp @@ -79,46 +79,31 @@ namespace OpenRCT2::Scripting { auto& scriptEngine = GetContext()->GetScriptEngine(); auto ctx = scriptEngine.GetContext(); - if (args.type() == DukValue::Type::OBJECT) + try { - if (callback.is_function()) + auto action = CreateGameAction(actionid, args); + if (action != nullptr) { - try + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + if (isExecute) { - auto action = CreateGameAction(actionid, args); - if (action != nullptr) - { - auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); - if (isExecute) - { - action->SetCallback( - [this, plugin, callback](const GameAction*, const GameActionResult* res) -> void { - HandleGameActionResult(plugin, *res, callback); - }); - GameActions::Execute(action.get()); - } - else - { - auto res = GameActions::Query(action.get()); - HandleGameActionResult(plugin, *res, callback); - } - } - else - { - duk_error(ctx, DUK_ERR_ERROR, "Unknown action."); - } + action->SetCallback([this, plugin, callback](const GameAction*, const GameActionResult* res) -> void { + HandleGameActionResult(plugin, *res, callback); + }); + GameActions::Execute(action.get()); } - catch (DukException&) + else { - duk_error(ctx, DUK_ERR_ERROR, "Invalid action parameters."); + auto res = GameActions::Query(action.get()); + HandleGameActionResult(plugin, *res, callback); } } else { - duk_error(ctx, DUK_ERR_ERROR, "Callback was not a function."); + duk_error(ctx, DUK_ERR_ERROR, "Unknown action."); } } - else + catch (DukException&) { duk_error(ctx, DUK_ERR_ERROR, "Invalid action parameters."); } @@ -148,7 +133,14 @@ namespace OpenRCT2::Scripting { // Serialise args to json so that it can be sent auto ctx = args.context(); - args.push(); + if (args.type() == DukValue::Type::OBJECT) + { + args.push(); + } + else + { + duk_push_object(ctx); + } auto jsonz = duk_json_encode(ctx, -1); auto json = std::string(jsonz); duk_pop(ctx); @@ -185,8 +177,30 @@ namespace OpenRCT2::Scripting auto args = DukValue::take_from_stack(ctx); - // Call the plugin callback and pass the result object - scriptEngine.ExecutePluginCall(plugin, callback, { args }, false); + if (callback.is_function()) + { + // Call the plugin callback and pass the result object + scriptEngine.ExecutePluginCall(plugin, callback, { args }, false); + } + } + + void registerGameAction(const std::string& action, const DukValue& query, const DukValue& execute) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + auto ctx = scriptEngine.GetContext(); + if (!query.is_function()) + { + duk_error(ctx, DUK_ERR_ERROR, "query was not a function."); + } + else if (!execute.is_function()) + { + duk_error(ctx, DUK_ERR_ERROR, "execute was not a function."); + } + else if (!scriptEngine.RegisterCustomAction(plugin, action, query, execute)) + { + duk_error(ctx, DUK_ERR_ERROR, "action has already been registered."); + } } public: @@ -196,6 +210,7 @@ namespace OpenRCT2::Scripting dukglue_register_method(ctx, &ScContext::subscribe, "subscribe"); dukglue_register_method(ctx, &ScContext::queryAction, "queryAction"); dukglue_register_method(ctx, &ScContext::executeAction, "executeAction"); + dukglue_register_method(ctx, &ScContext::registerGameAction, "registerGameAction"); } }; } // namespace OpenRCT2::Scripting diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 47792c4c42..95b5efc74d 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -459,6 +459,7 @@ void ScriptEngine::StopPlugin(std::shared_ptr plugin) { if (plugin->HasStarted()) { + RemoveCustomGameActions(plugin); _hookEngine.UnsubscribeAll(plugin); for (auto callback : _pluginStoppedSubscriptions) { @@ -653,7 +654,7 @@ std::future ScriptEngine::Eval(const std::string& s) return future; } -bool ScriptEngine::ExecutePluginCall( +DukValue ScriptEngine::ExecutePluginCall( const std::shared_ptr& plugin, const DukValue& func, const std::vector& args, bool isGameStateMutable) { if (func.is_function()) @@ -667,9 +668,7 @@ bool ScriptEngine::ExecutePluginCall( auto result = duk_pcall(_context, static_cast(args.size())); if (result == DUK_EXEC_SUCCESS) { - // TODO allow result to be returned as a DukValue - duk_pop(_context); - return true; + return DukValue::take_from_stack(_context); } else { @@ -678,7 +677,7 @@ bool ScriptEngine::ExecutePluginCall( duk_pop(_context); } } - return false; + return DukValue(); } void ScriptEngine::LogPluginInfo(const std::shared_ptr& plugin, const std::string_view& message) @@ -695,27 +694,82 @@ void ScriptEngine::AddNetworkPlugin(const std::string_view& code) } std::unique_ptr ScriptEngine::QueryOrExecuteCustomGameAction( - const std::string_view& id, - const std::string_view& args, - bool isExecute) + const std::string_view& id, const std::string_view& args, bool isExecute) { - // Deserialise the JSON args - std::string argsz(args); - duk_push_string(_context, argsz.c_str()); - duk_json_decode(_context, -1); - auto dukArgs = DukValue::take_from_stack(_context); - - // Ready to call plugin handler - if (isExecute) + std::string actionz = std::string(id); + auto kvp = _customActions.find(actionz); + if (kvp != _customActions.end()) { - std::printf("EXECUTE: %s(%s)\n", std::string(id).c_str(), std::string(args).c_str()); + const auto& customAction = kvp->second; + + // Deserialise the JSON args + std::string argsz(args); + duk_push_string(_context, argsz.c_str()); + duk_json_decode(_context, -1); + auto dukArgs = DukValue::take_from_stack(_context); + + // Ready to call plugin handler + DukValue dukResult; + if (!isExecute) + { + dukResult = ExecutePluginCall(customAction.Plugin, customAction.Query, { dukArgs }, false); + } + else + { + dukResult = ExecutePluginCall(customAction.Plugin, customAction.Execute, { dukArgs }, true); + } + return DukToGameActionResult(dukResult); } else { - std::printf("QUERY: %s(%s)\n", std::string(id).c_str(), std::string(args).c_str()); + auto action = std::make_unique(); + action->Error = GA_ERROR::UNKNOWN; + action->ErrorTitle = OBJECT_ERROR_UNKNOWN; + return action; + } +} + +std::unique_ptr ScriptEngine::DukToGameActionResult(const DukValue& d) +{ + auto result = std::make_unique(); + result->Error = d["error"].type() == DukValue::Type::NUMBER ? static_cast(d["error"].as_int()) : GA_ERROR::OK; + auto errorTitle = d["errorTitle"].type() == DukValue::Type::STRING ? d["errorTitle"].as_string() : std::string(); + auto errorMessage = d["errorMessage"].type() == DukValue::Type::STRING ? d["errorMessage"].as_string() : std::string(); + result->Cost = d["cost"].type() == DukValue::Type::NUMBER ? d["cost"].as_int() : 0; + return result; +} + +bool ScriptEngine::RegisterCustomAction( + const std::shared_ptr& plugin, const std::string_view& action, const DukValue& query, const DukValue& execute) +{ + std::string actionz = std::string(action); + if (_customActions.find(actionz) != _customActions.end()) + { + return false; } - return std::make_unique(); + CustomAction customAction; + customAction.Plugin = plugin; + customAction.Name = std::move(actionz); + customAction.Query = query; + customAction.Execute = execute; + _customActions[customAction.Name] = std::move(customAction); + return true; +} + +void ScriptEngine::RemoveCustomGameActions(const std::shared_ptr& plugin) +{ + for (auto it = _customActions.begin(); it != _customActions.end();) + { + if (it->second.Plugin == plugin) + { + it = _customActions.erase(it); + } + else + { + it++; + } + } } std::string OpenRCT2::Scripting::Stringify(const DukValue& val) diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index a309f1faa1..9a6981771c 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -21,6 +21,7 @@ # include # include # include +# include # include # include @@ -118,6 +119,16 @@ namespace OpenRCT2::Scripting std::mutex _changedPluginFilesMutex; std::vector)>> _pluginStoppedSubscriptions; + struct CustomAction + { + std::shared_ptr Plugin; + std::string Name; + DukValue Query; + DukValue Execute; + }; + + std::unordered_map _customActions; + public: ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env); ScriptEngine(ScriptEngine&) = delete; @@ -143,7 +154,7 @@ namespace OpenRCT2::Scripting void UnloadPlugins(); void Update(); std::future Eval(const std::string& s); - bool ExecutePluginCall( + DukValue ExecutePluginCall( const std::shared_ptr& plugin, const DukValue& func, const std::vector& args, bool isGameStateMutable); @@ -156,7 +167,11 @@ namespace OpenRCT2::Scripting void AddNetworkPlugin(const std::string_view& code); - std::unique_ptr QueryOrExecuteCustomGameAction(const std::string_view& id, const std::string_view& args, bool isExecute); + std::unique_ptr QueryOrExecuteCustomGameAction( + const std::string_view& id, const std::string_view& args, bool isExecute); + bool RegisterCustomAction( + const std::shared_ptr& plugin, const std::string_view& action, const DukValue& query, + const DukValue& execute); private: void Initialise(); @@ -170,6 +185,8 @@ namespace OpenRCT2::Scripting void SetupHotReloading(); void AutoReloadPlugins(); void ProcessREPL(); + void RemoveCustomGameActions(const std::shared_ptr& plugin); + std::unique_ptr DukToGameActionResult(const DukValue& d); }; bool IsGameStateMutable();