diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index d73baafa55..4990861d66 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -72,23 +72,25 @@ declare global { "interest"; interface GameActionResult { - error: string; + error?: string; errorTitle?: string; errorMessage?: string; - position: Coord3; - cost: number; - expenditureType: ExpenditureType; + position?: Coord3; + cost?: number; + expenditureType?: ExpenditureType; + } + + interface ActionEventArgs { + readonly player: number; + readonly type: string; + readonly isClientOnly: boolean; + result: GameActionResult; } interface NetworkEventArgs { readonly player: number; } - interface NetworkActionEventArgs extends NetworkEventArgs { - readonly type: string; - result: GameActionResult; - } - interface NetworkChatEventArgs extends NetworkEventArgs { message: string; } @@ -149,9 +151,10 @@ declare global { */ subscribe(hook: HookType, callback: Function): IDisposable; + subscribe(hook: "action.query", callback: (e: ActionEventArgs) => void): IDisposable; + subscribe(hook: "action.execute", callback: (e: ActionEventArgs) => void): IDisposable; subscribe(hook: "interval.tick", callback: () => void): IDisposable; subscribe(hook: "interval.day", callback: () => void): IDisposable; - subscribe(hook: "network.action", callback: (e: NetworkActionEventArgs) => void): IDisposable; subscribe(hook: "network.chat", callback: (e: NetworkChatEventArgs) => void): IDisposable; subscribe(hook: "network.join", callback: (e: NetworkEventArgs) => void): IDisposable; subscribe(hook: "network.leave", callback: (e: NetworkEventArgs) => void): IDisposable; diff --git a/src/openrct2/actions/GameAction.cpp b/src/openrct2/actions/GameAction.cpp index 03cb680b93..00ce2751a4 100644 --- a/src/openrct2/actions/GameAction.cpp +++ b/src/openrct2/actions/GameAction.cpp @@ -18,6 +18,7 @@ #include "../network/network.h" #include "../platform/platform.h" #include "../scenario/Scenario.h" +#include "../scripting/ScriptEngine.h" #include "../ui/UiContext.h" #include "../ui/WindowManager.h" #include "../world/Park.h" @@ -399,6 +400,15 @@ namespace GameActions } GameActionResult::Ptr result = QueryInternal(action, topLevel); +#ifdef __ENABLE_SCRIPTING__ + if (result->Error == GA_ERROR::OK + && ((network_get_mode() == NETWORK_MODE_NONE) || (flags & GAME_COMMAND_FLAG_NETWORKED))) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + scriptEngine.RunGameActionHooks(*action, result, false); + // Script hooks may now have changed the game action result... + } +#endif if (result->Error == GA_ERROR::OK) { if (topLevel) @@ -434,6 +444,14 @@ namespace GameActions // Execute the action, changing the game state result = action->Execute(); +#ifdef __ENABLE_SCRIPTING__ + if (result->Error == GA_ERROR::OK) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + scriptEngine.RunGameActionHooks(*action, result, true); + // Script hooks may now have changed the game action result... + } +#endif LogActionFinish(logContext, action, result); diff --git a/src/openrct2/scripting/HookEngine.cpp b/src/openrct2/scripting/HookEngine.cpp index fa3442f2ce..74295ec993 100644 --- a/src/openrct2/scripting/HookEngine.cpp +++ b/src/openrct2/scripting/HookEngine.cpp @@ -20,6 +20,8 @@ using namespace OpenRCT2::Scripting; HOOK_TYPE OpenRCT2::Scripting::GetHookType(const std::string& name) { static const std::unordered_map LookupTable({ + { "action.query", HOOK_TYPE::ACTION_QUERY }, + { "action.execute", HOOK_TYPE::ACTION_EXECUTE }, { "interval.tick", HOOK_TYPE::INTERVAL_TICK }, { "interval.day", HOOK_TYPE::INTERVAL_DAY }, { "network.chat", HOOK_TYPE::NETWORK_CHAT }, diff --git a/src/openrct2/scripting/HookEngine.h b/src/openrct2/scripting/HookEngine.h index 60ba158cfe..6547215866 100644 --- a/src/openrct2/scripting/HookEngine.h +++ b/src/openrct2/scripting/HookEngine.h @@ -28,6 +28,8 @@ namespace OpenRCT2::Scripting enum class HOOK_TYPE { + ACTION_QUERY, + ACTION_EXECUTE, INTERVAL_TICK, INTERVAL_DAY, NETWORK_CHAT, diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 5665298914..354c88c77b 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -12,6 +12,7 @@ # include "ScriptEngine.h" # include "../PlatformEnvironment.h" +# include "../actions/GameAction.h" # include "../config/Config.h" # include "../core/FileScanner.h" # include "../core/Path.hpp" @@ -739,6 +740,42 @@ std::unique_ptr ScriptEngine::DukToGameActionResult(const DukV return result; } +DukValue ScriptEngine::PositionToDuk(const CoordsXYZ& position) +{ + duk_context* ctx = _context; + auto obj = duk_push_object(ctx); + duk_push_int(ctx, position.x); + duk_put_prop_string(ctx, obj, "x"); + duk_push_int(ctx, position.y); + duk_put_prop_string(ctx, obj, "y"); + duk_push_int(ctx, position.z); + duk_put_prop_string(ctx, obj, "z"); + return DukValue::take_from_stack(ctx); +} + +DukValue ScriptEngine::GameActionResultToDuk(const GameAction& action, const std::unique_ptr& result) +{ + duk_context* ctx = _context; + auto obj = duk_push_object(ctx); + auto player = action.GetPlayer(); + if (player != -1) + { + duk_push_int(ctx, action.GetPlayer()); + duk_put_prop_string(ctx, obj, "player"); + } + if (result->Cost != MONEY32_UNDEFINED) + { + duk_push_int(ctx, result->Cost); + duk_put_prop_string(ctx, obj, "cost"); + } + if (!result->Position.isNull()) + { + PositionToDuk(result->Position).push(); + duk_put_prop_string(ctx, obj, "position"); + } + return DukValue::take_from_stack(ctx); +} + bool ScriptEngine::RegisterCustomAction( const std::shared_ptr& plugin, const std::string_view& action, const DukValue& query, const DukValue& execute) { @@ -772,6 +809,38 @@ void ScriptEngine::RemoveCustomGameActions(const std::shared_ptr& plugin } } +void ScriptEngine::RunGameActionHooks(const GameAction& action, std::unique_ptr& result, bool isExecute) +{ + auto hookType = isExecute ? HOOK_TYPE::ACTION_EXECUTE : HOOK_TYPE::ACTION_QUERY; + if (_hookEngine.HasSubscriptions(hookType)) + { + duk_context* ctx = _context; + auto obj = duk_push_object(ctx); + duk_push_uint(ctx, action.GetType()); + duk_put_prop_string(ctx, obj, "type"); + + auto flags = action.GetActionFlags(); + duk_push_boolean(ctx, (flags & GA_FLAGS::CLIENT_ONLY) != 0); + duk_put_prop_string(ctx, obj, "isClientOnly"); + + GameActionResultToDuk(action, result).push(); + duk_put_prop_string(ctx, obj, "result"); + auto dukEventArgs = DukValue::take_from_stack(ctx); + _hookEngine.Call(hookType, dukEventArgs, false); + + if (!isExecute) + { + auto error = AsOrDefault(dukEventArgs["error"]); + if (error != 0) + { + result->Error = static_cast(error); + result->ErrorTitle = AsOrDefault(dukEventArgs["errorTitle"]); + result->ErrorMessage = AsOrDefault(dukEventArgs["errorMessage"]); + } + } + } +} + std::string OpenRCT2::Scripting::Stringify(const DukValue& val) { return ExpressionStringifier::StringifyExpression(val); diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 871c40bea4..7ad4f91232 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -13,6 +13,7 @@ # include "../common.h" # include "../core/FileWatcher.h" +# include "../world/Location.hpp" # include "HookEngine.h" # include "Plugin.h" @@ -28,6 +29,7 @@ struct duk_hthread; typedef struct duk_hthread duk_context; +struct GameAction; class GameActionResult; class FileWatcher; class InteractiveConsole; @@ -172,6 +174,7 @@ namespace OpenRCT2::Scripting bool RegisterCustomAction( const std::shared_ptr& plugin, const std::string_view& action, const DukValue& query, const DukValue& execute); + void RunGameActionHooks(const GameAction& action, std::unique_ptr& result, bool isExecute); private: void Initialise(); @@ -187,6 +190,8 @@ namespace OpenRCT2::Scripting void ProcessREPL(); void RemoveCustomGameActions(const std::shared_ptr& plugin); std::unique_ptr DukToGameActionResult(const DukValue& d); + DukValue GameActionResultToDuk(const GameAction& action, const std::unique_ptr& result); + DukValue PositionToDuk(const CoordsXYZ& position); }; bool IsGameStateMutable();