diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 540d01e2de..4c96804d33 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -36,7 +36,11 @@ export interface Context { /** * Subscribes to the given hook. */ - subscribe: (hook: string, callback: Function) => void; + subscribe(hook: string, callback: Function): IDisposable; +} + +export interface IDisposable { + dispose(): void; } export interface Ride { diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index 22f611c03d..63597337a9 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -57,6 +57,7 @@ #include "ride/TrackDesignRepository.h" #include "scenario/Scenario.h" #include "scenario/ScenarioRepository.h" +#include "scripting/HookEngine.h" #include "scripting/ScriptEngine.h" #include "title/TitleScreen.h" #include "title/TitleSequenceManager.h" diff --git a/src/openrct2/GameState.cpp b/src/openrct2/GameState.cpp index 4a195010b6..bdeeaff041 100644 --- a/src/openrct2/GameState.cpp +++ b/src/openrct2/GameState.cpp @@ -26,6 +26,7 @@ #include "peep/Staff.h" #include "platform/Platform2.h" #include "scenario/Scenario.h" +#include "scripting/ScriptEngine.h" #include "title/TitleScreen.h" #include "title/TitleSequencePlayer.h" #include "ui/UiContext.h" @@ -38,6 +39,7 @@ #include using namespace OpenRCT2; +using namespace OpenRCT2::Scripting; GameState::GameState() { @@ -267,6 +269,9 @@ void GameState::UpdateLogic() } } + auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine(); + hookEngine.Call(HOOK_TYPE::INTERVAL_TICK); + date_update(); _date = Date(gDateMonthTicks, gDateMonthTicks); diff --git a/src/openrct2/scripting/HookEngine.cpp b/src/openrct2/scripting/HookEngine.cpp new file mode 100644 index 0000000000..4b649b7332 --- /dev/null +++ b/src/openrct2/scripting/HookEngine.cpp @@ -0,0 +1,99 @@ +/***************************************************************************** + * Copyright (c) 2014-2018 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include +#include "HookEngine.h" +#include "ScriptEngine.h" + +using namespace OpenRCT2::Scripting; + +HOOK_TYPE OpenRCT2::Scripting::GetHookType(const std::string &name) +{ + static const std::unordered_map LookupTable({ + { "interval.tick", HOOK_TYPE::INTERVAL_TICK }, + { "interval.day", HOOK_TYPE::INTERVAL_DAY }, + }); + auto result = LookupTable.find(name); + return (result != LookupTable.end()) ? + result->second : + HOOK_TYPE::UNDEFINED; +} + +HookEngine::HookEngine(ScriptExecutionInfo& execInfo) : + _execInfo(execInfo) +{ + _hookMap.resize(NUM_HOOK_TYPES); + for (size_t i = 0; i < NUM_HOOK_TYPES; i++) + { + _hookMap[i].Type = static_cast(i); + } +} + +uint32 HookEngine::Subscribe(HOOK_TYPE type, Plugin& owner, const DukValue &function) +{ + auto& hookList = GetHookList(type); + auto cookie = _nextCookie++; + Hook hook(cookie, owner, function); + hookList.Hooks.push_back(hook); + return cookie; +} + +void HookEngine::Unsubscribe(HOOK_TYPE type, uint32 cookie) +{ + auto& hookList = GetHookList(type); + auto& hooks = hookList.Hooks; + for (auto it = hooks.begin(); it != hooks.end(); it++) + { + if (it->Cookie == cookie) + { + hooks.erase(it); + break; + } + } +} + +void HookEngine::UnsubscribeAll(const Plugin& owner) +{ + for (auto& hookList : _hookMap) + { + auto& hooks = hookList.Hooks; + for (auto it = hooks.begin(); it != hooks.end();) + { + if (it->Owner == &owner) + { + it = hooks.erase(it); + } + else + { + it++; + } + } + } +} + +void HookEngine::Call(HOOK_TYPE type) +{ + auto& hookList = GetHookList(type); + for (auto& hook : hookList.Hooks) + { + _execInfo.SetCurrentPlugin(hook.Owner); + + const auto& function = hook.Function; + function.push(); + duk_pcall(function.context(), 0); + duk_pop(function.context()); + } + _execInfo.SetCurrentPlugin(nullptr); +} + +HookList& HookEngine::GetHookList(HOOK_TYPE type) +{ + auto index = static_cast(type); + return _hookMap[index]; +} diff --git a/src/openrct2/scripting/HookEngine.h b/src/openrct2/scripting/HookEngine.h new file mode 100644 index 0000000000..12058aea2a --- /dev/null +++ b/src/openrct2/scripting/HookEngine.h @@ -0,0 +1,81 @@ +/***************************************************************************** + * Copyright (c) 2014-2018 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#pragma once + +#include "../common.h" +#include +#include +#include +#include + +namespace OpenRCT2::Scripting +{ + class ScriptExecutionInfo; + class Plugin; + + enum class HOOK_TYPE + { + INTERVAL_TICK, + INTERVAL_DAY, + COUNT, + UNDEFINED = -1, + }; + constexpr size_t NUM_HOOK_TYPES = static_cast(HOOK_TYPE::COUNT); + HOOK_TYPE GetHookType(const std::string &name); + + struct Hook + { + uint32 Cookie; + Plugin * Owner; + DukValue Function; + + Hook(); + Hook(uint32 cookie, Plugin& owner, const DukValue &function) + : Cookie(cookie), + Owner(&owner), + Function(function) + { + } + }; + + struct HookList + { + HOOK_TYPE Type; + std::vector Hooks; + + HookList() {} + HookList(const HookList&) = delete; + HookList(HookList&& src) + : Type(std::move(src.Type)), + Hooks(std::move(src.Hooks)) + { + } + }; + + class HookEngine + { + private: + ScriptExecutionInfo& _execInfo; + std::vector _hookMap; + size_t _numHooks{}; + uint32_t _nextCookie = 1; + + public: + HookEngine(ScriptExecutionInfo& execInfo); + HookEngine(const HookEngine&) = delete; + uint32 Subscribe(HOOK_TYPE type, Plugin& owner, const DukValue &function); + void Unsubscribe(HOOK_TYPE type, uint32 cookie); + void UnsubscribeAll(const Plugin& owner); + void Call(HOOK_TYPE type); + + private: + HookList& GetHookList(HOOK_TYPE type); + }; +} diff --git a/src/openrct2/scripting/ScConsole.hpp b/src/openrct2/scripting/ScConsole.hpp index 9d293bacbc..10f5916de9 100644 --- a/src/openrct2/scripting/ScConsole.hpp +++ b/src/openrct2/scripting/ScConsole.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include "../interface/Console.h" diff --git a/src/openrct2/scripting/ScContext.hpp b/src/openrct2/scripting/ScContext.hpp new file mode 100644 index 0000000000..c533cab3ed --- /dev/null +++ b/src/openrct2/scripting/ScContext.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include "HookEngine.h" +#include "ScDisposable.hpp" +#include "ScriptEngine.h" +#include +#include + +namespace OpenRCT2::Scripting +{ + class ScContext + { + private: + ScriptExecutionInfo& _execInfo; + HookEngine& _hookEngine; + + public: + ScContext(ScriptExecutionInfo& execInfo, HookEngine& hookEngine) + : _execInfo(execInfo), + _hookEngine(hookEngine) + { + } + + std::shared_ptr subscribe(const std::string &hook, const DukValue &callback) + { + auto hookType = GetHookType(hook); + if (hookType == HOOK_TYPE::UNDEFINED) + { + throw DukException() << "Unknown hook type: " << hook; + } + + if (!callback.is_function()) + { + throw DukException() << "Expected function for callback"; + } + + auto owner = _execInfo.GetCurrentPlugin(); + if (owner == nullptr) + { + throw DukException() << "Not in a plugin context"; + } + + auto cookie = _hookEngine.Subscribe(hookType, *owner, callback); + return std::make_shared( + [this, hookType, cookie]() + { + _hookEngine.Unsubscribe(hookType, cookie); + }); + } + + static void Register(duk_context * ctx) + { + dukglue_register_method(ctx, &ScContext::subscribe, "subscribe"); + } + }; +} diff --git a/src/openrct2/scripting/ScDisposable.hpp b/src/openrct2/scripting/ScDisposable.hpp new file mode 100644 index 0000000000..eb3b58782a --- /dev/null +++ b/src/openrct2/scripting/ScDisposable.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace OpenRCT2::Scripting +{ + class ScDisposable + { + private: + std::function _onDispose; + + public: + ScDisposable(std::function onDispose) : + _onDispose(onDispose) + { + } + + void dispose() + { + if (_onDispose) + { + _onDispose(); + } + } + + static void Register(duk_context * ctx) + { + dukglue_register_method(ctx, &ScDisposable::dispose, "dispose"); + } + }; +} diff --git a/src/openrct2/scripting/ScPark.hpp b/src/openrct2/scripting/ScPark.hpp index b8512d122b..3bbab2db7d 100644 --- a/src/openrct2/scripting/ScPark.hpp +++ b/src/openrct2/scripting/ScPark.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include #include "../common.h" diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 3e940fe70f..d5f854107c 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -19,6 +19,8 @@ #include #include "ScConsole.hpp" +#include "ScContext.hpp" +#include "ScDisposable.hpp" #include "ScPark.hpp" using namespace OpenRCT2; @@ -28,7 +30,8 @@ static std::string Stringify(duk_context * ctx, duk_idx_t idx); ScriptEngine::ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env) : _console(console), - _env(env) + _env(env), + _hookEngine(_execInfo) { } @@ -47,9 +50,12 @@ void ScriptEngine::Initialise() auto ctx = _context; ScConsole::Register(ctx); + ScContext::Register(ctx); + ScDisposable::Register(ctx); ScPark::Register(ctx); dukglue_register_global(ctx, std::make_shared(_console), "console"); + dukglue_register_global(ctx, std::make_shared(_execInfo, _hookEngine), "context"); dukglue_register_global(ctx, std::make_shared(), "park"); LoadPlugins(); @@ -67,6 +73,7 @@ void ScriptEngine::LoadPlugins() try { Plugin p(_context, path); + _execInfo.SetCurrentPlugin(&p); p.Load(); p.EnableHotReload(); _plugins.push_back(std::move(p)); @@ -76,6 +83,7 @@ void ScriptEngine::LoadPlugins() _console.WriteLineError(e.what()); } } + _execInfo.SetCurrentPlugin(nullptr); } void ScriptEngine::AutoReloadPlugins() @@ -86,6 +94,8 @@ void ScriptEngine::AutoReloadPlugins() { try { + _hookEngine.UnsubscribeAll(plugin); + _execInfo.SetCurrentPlugin(&plugin); plugin.Load(); plugin.Start(); } @@ -95,14 +105,24 @@ void ScriptEngine::AutoReloadPlugins() } } } + _execInfo.SetCurrentPlugin(nullptr); } void ScriptEngine::StartPlugins() { for (auto& plugin : _plugins) { - plugin.Start(); + _execInfo.SetCurrentPlugin(&plugin); + try + { + plugin.Start(); + } + catch (const std::exception &e) + { + _console.WriteLineError(e.what()); + } } + _execInfo.SetCurrentPlugin(nullptr); } void ScriptEngine::Update() diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 2aaca3195a..d8f81b089c 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -10,6 +10,7 @@ #pragma once #include "../common.h" +#include "HookEngine.h" #include "Plugin.h" #include #include @@ -27,6 +28,16 @@ namespace OpenRCT2 namespace OpenRCT2::Scripting { + class ScriptExecutionInfo + { + private: + Plugin * _plugin; + + public: + Plugin * GetCurrentPlugin() { return _plugin; } + void SetCurrentPlugin(Plugin * value) { _plugin = value; } + }; + class ScriptEngine { private: @@ -37,12 +48,16 @@ namespace OpenRCT2::Scripting std::queue, std::string>> _evalQueue; std::vector _plugins; uint32 _lastHotReloadCheckTick{}; + HookEngine _hookEngine; + ScriptExecutionInfo _execInfo; public: ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env); ScriptEngine(ScriptEngine&) = delete; ~ScriptEngine(); + HookEngine& GetHookEngine() { return _hookEngine; } + void Update(); std::future Eval(const std::string &s); diff --git a/src/openrct2/thirdparty/dukglue/dukvalue.h b/src/openrct2/thirdparty/dukglue/dukvalue.h index 982bad1496..4e505cce05 100644 --- a/src/openrct2/thirdparty/dukglue/dukvalue.h +++ b/src/openrct2/thirdparty/dukglue/dukvalue.h @@ -625,4 +625,12 @@ public: duk_pop(mContext); return result; } + + bool is_function() const + { + push(); + bool result = duk_is_function(mContext, -1); + duk_pop(mContext); + return result; + } }; \ No newline at end of file