diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 64237d8dda..bc8b39adff 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -13,6 +13,7 @@ - Feature: [#13583] [Plugin] Add allowed_hosts to plugin section of config. - Feature: [#13593] [Plugin] Add ability to read and change the position of ride vehicles. - Feature: [#13614] Add terrain surfaces from RollerCoaster Tycoon 1. +- Feature: [#13675] [Plugin] Add context.setInterval and context.setTimeout. - Change: [#13346] [Plugin] Renamed FootpathScenery to FootpathAddition, fix typos. - Fix: [#12895] Mechanics are called to repair rides that have already been fixed. - Fix: [#13102] Underflow on height chart (Ride measurements). diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 092c3400e8..f361802e43 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -237,6 +237,34 @@ declare global { subscribe(hook: "network.leave", callback: (e: NetworkEventArgs) => void): IDisposable; subscribe(hook: "ride.ratings.calculate", callback: (e: RideRatingsCalculateArgs) => void): IDisposable; subscribe(hook: "action.location", callback: (e: ActionLocationArgs) => void): IDisposable; + + /** + * Registers a function to be called every so often in realtime, specified by the given delay. + * @param callback The function to call every time the delay has elapsed. + * @param delay The number of milliseconds to wait between each call to the given function. + */ + setInterval(callback: Function, delay: number): number; + + /** + * Like `setInterval`, except the callback will only execute once after the given delay. + * @param callback The function to call after the given delay has elapsed. + * @param delay The number of milliseconds to wait for before calling the given function. + */ + setTimeout(callback: Function, delay: number): number; + + /** + * Removes the registered interval specified by the numeric handle. The handles + * are shared with `setTimeout`. + * @param handle + */ + clearInterval(handle: number): void; + + /** + * Removes the registered timeout specified by the numeric handle. The handles + * are shared with `setInterval`. + * @param handle The numerical handle of the registered timeout to remove. + */ + clearTimeout(handle: number): void; } interface Configuration { diff --git a/src/openrct2/scripting/ScContext.hpp b/src/openrct2/scripting/ScContext.hpp index d565c82f70..51fecb37f9 100644 --- a/src/openrct2/scripting/ScContext.hpp +++ b/src/openrct2/scripting/ScContext.hpp @@ -324,6 +324,51 @@ namespace OpenRCT2::Scripting } } + int32_t SetIntervalOrTimeout(DukValue callback, int32_t delay, bool repeat) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto ctx = scriptEngine.GetContext(); + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + + int32_t handle = 0; + if (callback.is_function()) + { + handle = scriptEngine.AddInterval(plugin, delay, repeat, std::move(callback)); + } + else + { + duk_error(ctx, DUK_ERR_ERROR, "callback was not a function."); + } + return handle; + } + + void ClearIntervalOrTimeout(int32_t handle) + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin(); + scriptEngine.RemoveInterval(plugin, handle); + } + + int32_t setInterval(DukValue callback, int32_t delay) + { + return SetIntervalOrTimeout(callback, delay, true); + } + + int32_t setTimeout(DukValue callback, int32_t delay) + { + return SetIntervalOrTimeout(callback, delay, false); + } + + void clearInterval(int32_t handle) + { + ClearIntervalOrTimeout(handle); + } + + void clearTimeout(int32_t handle) + { + ClearIntervalOrTimeout(handle); + } + public: static void Register(duk_context* ctx) { @@ -338,6 +383,10 @@ namespace OpenRCT2::Scripting dukglue_register_method(ctx, &ScContext::queryAction, "queryAction"); dukglue_register_method(ctx, &ScContext::executeAction, "executeAction"); dukglue_register_method(ctx, &ScContext::registerAction, "registerAction"); + dukglue_register_method(ctx, &ScContext::setInterval, "setInterval"); + dukglue_register_method(ctx, &ScContext::setTimeout, "setTimeout"); + dukglue_register_method(ctx, &ScContext::clearInterval, "clearInterval"); + dukglue_register_method(ctx, &ScContext::clearTimeout, "clearTimeout"); } }; } // namespace OpenRCT2::Scripting diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index c86c81eeeb..f157e383a2 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -44,7 +44,7 @@ using namespace OpenRCT2; using namespace OpenRCT2::Scripting; -static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 16; +static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 17; struct ExpressionStringifier final { @@ -489,6 +489,7 @@ void ScriptEngine::StopPlugin(std::shared_ptr plugin) if (plugin->HasStarted()) { RemoveCustomGameActions(plugin); + RemoveIntervals(plugin); RemoveSockets(plugin); _hookEngine.UnsubscribeAll(plugin); for (auto callback : _pluginStoppedSubscriptions) @@ -651,6 +652,7 @@ void ScriptEngine::Update() } } + UpdateIntervals(); UpdateSockets(); ProcessREPL(); } @@ -1211,6 +1213,95 @@ void ScriptEngine::SaveSharedStorage() } } +IntervalHandle ScriptEngine::AllocateHandle() +{ + for (size_t i = 0; i < _intervals.size(); i++) + { + if (!_intervals[i].IsValid()) + { + return static_cast(i + 1); + } + } + _intervals.emplace_back(); + return static_cast(_intervals.size()); +} + +IntervalHandle ScriptEngine::AddInterval(const std::shared_ptr& plugin, int32_t delay, bool repeat, DukValue&& callback) +{ + auto handle = AllocateHandle(); + if (handle != 0) + { + auto& interval = _intervals[static_cast(handle) - 1]; + interval.Owner = plugin; + interval.Handle = handle; + interval.Delay = delay; + interval.LastTimestamp = _lastIntervalTimestamp; + interval.Callback = std::move(callback); + interval.Repeat = repeat; + } + return handle; +} + +void ScriptEngine::RemoveInterval(const std::shared_ptr& plugin, IntervalHandle handle) +{ + if (handle > 0 && static_cast(handle) <= _intervals.size()) + { + auto& interval = _intervals[static_cast(handle) - 1]; + + // Only allow owner or REPL (nullptr) to remove intervals + if (plugin == nullptr || interval.Owner == plugin) + { + interval = {}; + } + } +} + +void ScriptEngine::UpdateIntervals() +{ + uint32_t timestamp = platform_get_ticks(); + if (timestamp < _lastIntervalTimestamp) + { + // timestamp has wrapped, subtract all intervals by the remaining amount before wrap + auto delta = static_cast(std::numeric_limits::max() - _lastIntervalTimestamp); + for (auto& interval : _intervals) + { + if (interval.IsValid()) + { + interval.LastTimestamp = -delta; + } + } + } + _lastIntervalTimestamp = timestamp; + + for (auto& interval : _intervals) + { + if (interval.IsValid()) + { + if (timestamp >= interval.LastTimestamp + interval.Delay) + { + ExecutePluginCall(interval.Owner, interval.Callback, {}, false); + + interval.LastTimestamp = timestamp; + if (!interval.Repeat) + { + RemoveInterval(nullptr, interval.Handle); + } + } + } + } +} + +void ScriptEngine::RemoveIntervals(const std::shared_ptr& plugin) +{ + for (auto& interval : _intervals) + { + if (interval.Owner == plugin) + { + interval = {}; + } + } +} + # ifndef DISABLE_NETWORK void ScriptEngine::AddSocket(const std::shared_ptr& socket) { diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index cdfd5df147..78d9c7923c 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -117,6 +117,22 @@ namespace OpenRCT2::Scripting } }; + using IntervalHandle = int32_t; + struct ScriptInterval + { + std::shared_ptr Owner; + IntervalHandle Handle{}; + uint32_t Delay{}; + int64_t LastTimestamp{}; + DukValue Callback; + bool Repeat{}; + + bool IsValid() const + { + return Handle != 0; + } + }; + class ScriptEngine { private: @@ -133,6 +149,9 @@ namespace OpenRCT2::Scripting ScriptExecutionInfo _execInfo; DukValue _sharedStorage; + uint32_t _lastIntervalTimestamp{}; + std::vector _intervals; + std::unique_ptr _pluginFileWatcher; std::unordered_set _changedPluginFiles; std::mutex _changedPluginFilesMutex; @@ -203,6 +222,9 @@ namespace OpenRCT2::Scripting void SaveSharedStorage(); + IntervalHandle AddInterval(const std::shared_ptr& plugin, int32_t delay, bool repeat, DukValue&& callback); + void RemoveInterval(const std::shared_ptr& plugin, IntervalHandle handle); + # ifndef DISABLE_NETWORK void AddSocket(const std::shared_ptr& socket); # endif @@ -228,6 +250,10 @@ namespace OpenRCT2::Scripting void InitSharedStorage(); void LoadSharedStorage(); + IntervalHandle AllocateHandle(); + void UpdateIntervals(); + void RemoveIntervals(const std::shared_ptr& plugin); + void UpdateSockets(); void RemoveSockets(const std::shared_ptr& plugin); };