diff --git a/distribution/changelog.txt b/distribution/changelog.txt index aa53808e96..5af4e5e813 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -8,6 +8,7 @@ - Feature: [#7660] Custom music objects that are distributed with the save. - Feature: [#8407] Ride platforms can be made invisible. - Feature: [#13858] Flatride bases can be made invisible. +- Feature: [#14676] [Plugin] Allow plugins to store data in .park files. - Feature: [#15367] Individual track elements can now be drawn as another ride type. - Feature: [#16029] [Plugin] Add TrackElement.rideType to API. - Feature: [#16097] The Looping Roller Coaster can now draw all elements from the LIM Launched Roller Coaster. diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index cf8ef865af..a5ee8c1103 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -185,6 +185,17 @@ declare global { */ sharedStorage: Configuration; + /** + * Shared generic storage for all plugins. Data is persisted for the current + * loaded park, and is stored inside the .park file. Any references to objects, + * or arrays are copied by reference. If these arrays, objects, or any other + * arrays, or objects that they reference change without a subsequent call to + * the `set` method, their new state will still be serialised. + * Keep in mind that all data here will be serialised every time the park is + * saved, including when the park is periodically saved automatically. + */ + parkStorage: Configuration; + /** * Render the current state of the map and save to disc. * Useful for server administration and timelapse creation. diff --git a/distribution/scripting.md b/distribution/scripting.md index 7304f56a57..715acffff9 100644 --- a/distribution/scripting.md +++ b/distribution/scripting.md @@ -172,6 +172,8 @@ if (!h) { All plugins have access to the same shared storage. +If you want to only store data specific to the current park that is loaded, use `context.parkStorage`. Any data stored here will be written to the .park file. + > Can plugins communicate with other processes, or the internet? There is a socket API (based on net.Server and net.Socket from node.js) available for listening and communicating across TCP streams. For security purposes, plugins can only listen and connect to localhost. If you want to extend the communication further, you will need to provide your own separate reverse proxy. What port you can listen on is subject to your operating system, and how elevated the OpenRCT2 process is. diff --git a/src/openrct2/GameState.cpp b/src/openrct2/GameState.cpp index 34f1bf8a81..3a9d96af52 100644 --- a/src/openrct2/GameState.cpp +++ b/src/openrct2/GameState.cpp @@ -86,6 +86,11 @@ void GameState::InitAll(const TileCoordsXY& mapSize) CheatsReset(); ClearRestrictedScenery(); + +#ifdef ENABLE_SCRIPTING + auto& scriptEngine = GetContext()->GetScriptEngine(); + scriptEngine.ClearParkStorage(); +#endif } /** diff --git a/src/openrct2/park/ParkFile.cpp b/src/openrct2/park/ParkFile.cpp index 3752e04eb2..8c05e99d2a 100644 --- a/src/openrct2/park/ParkFile.cpp +++ b/src/openrct2/park/ParkFile.cpp @@ -47,6 +47,7 @@ #include "../ride/Vehicle.h" #include "../scenario/Scenario.h" #include "../scenario/ScenarioRepository.h" +#include "../scripting/ScriptEngine.h" #include "../world/Climate.h" #include "../world/Entrance.h" #include "../world/Map.h" @@ -86,6 +87,7 @@ namespace OpenRCT2 // constexpr uint32_t STAFF = 0x35; constexpr uint32_t CHEATS = 0x36; constexpr uint32_t RESTRICTED_OBJECTS = 0x37; + constexpr uint32_t PLUGIN_STORAGE = 0x38; constexpr uint32_t PACKED_OBJECTS = 0x80; // clang-format on }; // namespace ParkFileChunkType @@ -134,6 +136,7 @@ namespace OpenRCT2 ReadWriteInterfaceChunk(os); ReadWriteCheatsChunk(os); ReadWriteRestrictedObjectsChunk(os); + ReadWritePluginStorageChunk(os); if (os.GetHeader().TargetVersion < 0x4) { UpdateTrackElementsRideType(); @@ -167,6 +170,7 @@ namespace OpenRCT2 ReadWriteInterfaceChunk(os); ReadWriteCheatsChunk(os); ReadWriteRestrictedObjectsChunk(os); + ReadWritePluginStorageChunk(os); ReadWritePackedObjectsChunk(os); } @@ -548,6 +552,35 @@ namespace OpenRCT2 }); } + void ReadWritePluginStorageChunk(OrcaStream& os) + { + auto& park = GetContext()->GetGameState()->GetPark(); + if (os.GetMode() == OrcaStream::Mode::WRITING) + { +#ifdef ENABLE_SCRIPTING + // Dump the plugin storage to JSON (stored in park) + auto& scriptEngine = GetContext()->GetScriptEngine(); + park.PluginStorage = scriptEngine.GetParkStorageAsJSON(); +#endif + if (park.PluginStorage.empty() || park.PluginStorage == "{}") + { + // Don't write the chunk if there is no plugin storage + return; + } + } + + os.ReadWriteChunk( + ParkFileChunkType::PLUGIN_STORAGE, [&park](OrcaStream::ChunkStream& cs) { cs.ReadWrite(park.PluginStorage); }); + + if (os.GetMode() == OrcaStream::Mode::READING) + { +#ifdef ENABLE_SCRIPTING + auto& scriptEngine = GetContext()->GetScriptEngine(); + scriptEngine.SetParkStorageFromJSON(park.PluginStorage); +#endif + } + } + void ReadWritePackedObjectsChunk(OrcaStream& os) { static constexpr uint8_t DESCRIPTOR_DAT = 0; diff --git a/src/openrct2/park/ParkFile.h b/src/openrct2/park/ParkFile.h index a3f7ba01e4..f7a2bc7b62 100644 --- a/src/openrct2/park/ParkFile.h +++ b/src/openrct2/park/ParkFile.h @@ -8,7 +8,7 @@ struct ObjectRepositoryItem; namespace OpenRCT2 { // Current version that is saved. - constexpr uint32_t PARK_FILE_CURRENT_VERSION = 0x9; + constexpr uint32_t PARK_FILE_CURRENT_VERSION = 0xA; // The minimum version that is forwards compatible with the current version. constexpr uint32_t PARK_FILE_MIN_VERSION = 0x9; diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 1b49413019..5ca156f6c8 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -439,6 +439,7 @@ void ScriptEngine::Initialise() _pluginsStarted = false; InitSharedStorage(); + ClearParkStorage(); } void ScriptEngine::LoadPlugins() @@ -1245,6 +1246,29 @@ void ScriptEngine::SaveSharedStorage() } } +void ScriptEngine::ClearParkStorage() +{ + duk_push_object(_context); + _parkStorage = std::move(DukValue::take_from_stack(_context)); +} + +std::string ScriptEngine::GetParkStorageAsJSON() +{ + _parkStorage.push(); + auto json = std::string(duk_json_encode(_context, -1)); + duk_pop(_context); + return json; +} + +void ScriptEngine::SetParkStorageFromJSON(std::string_view value) +{ + auto result = DuktapeTryParseJson(_context, value); + if (result) + { + _parkStorage = std::move(*result); + } +} + IntervalHandle ScriptEngine::AllocateHandle() { for (size_t i = 0; i < _intervals.size(); i++) diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index b7d8f81065..b1d7606dd7 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -46,7 +46,7 @@ namespace OpenRCT2 namespace OpenRCT2::Scripting { - static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 45; + static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 46; // Versions marking breaking changes. static constexpr int32_t API_VERSION_33_PEEP_DEPRECATION = 33; @@ -153,6 +153,7 @@ namespace OpenRCT2::Scripting HookEngine _hookEngine; ScriptExecutionInfo _execInfo; DukValue _sharedStorage; + DukValue _parkStorage; uint32_t _lastIntervalTimestamp{}; std::vector _intervals; @@ -195,11 +196,19 @@ namespace OpenRCT2::Scripting { return _sharedStorage; } + DukValue GetParkStorage() + { + return _parkStorage; + } std::vector>& GetPlugins() { return _plugins; } + void ClearParkStorage(); + std::string GetParkStorageAsJSON(); + void SetParkStorageFromJSON(std::string_view value); + void LoadPlugins(); void UnloadPlugins(); void Tick(); diff --git a/src/openrct2/scripting/bindings/game/ScContext.hpp b/src/openrct2/scripting/bindings/game/ScContext.hpp index 11ea877f37..654808dcf8 100644 --- a/src/openrct2/scripting/bindings/game/ScContext.hpp +++ b/src/openrct2/scripting/bindings/game/ScContext.hpp @@ -58,6 +58,12 @@ namespace OpenRCT2::Scripting return std::make_shared(scriptEngine.GetSharedStorage()); } + std::shared_ptr parkStorage_get() + { + auto& scriptEngine = GetContext()->GetScriptEngine(); + return std::make_shared(scriptEngine.GetParkStorage()); + } + void captureImage(const DukValue& options) { auto ctx = GetContext()->GetScriptEngine().GetContext(); @@ -381,6 +387,7 @@ namespace OpenRCT2::Scripting dukglue_register_property(ctx, &ScContext::apiVersion_get, nullptr, "apiVersion"); dukglue_register_property(ctx, &ScContext::configuration_get, nullptr, "configuration"); dukglue_register_property(ctx, &ScContext::sharedStorage_get, nullptr, "sharedStorage"); + dukglue_register_property(ctx, &ScContext::parkStorage_get, nullptr, "parkStorage"); dukglue_register_method(ctx, &ScContext::captureImage, "captureImage"); dukglue_register_method(ctx, &ScContext::getObject, "getObject"); dukglue_register_method(ctx, &ScContext::getAllObjects, "getAllObjects"); diff --git a/src/openrct2/world/Park.cpp b/src/openrct2/world/Park.cpp index ce4d613541..c201b94da5 100644 --- a/src/openrct2/world/Park.cpp +++ b/src/openrct2/world/Park.cpp @@ -258,6 +258,7 @@ money64 Park::GetCompanyValue() const void Park::Initialise() { Name = format_string(STR_UNNAMED_PARK, nullptr); + PluginStorage = {}; gStaffHandymanColour = COLOUR_BRIGHT_RED; gStaffMechanicColour = COLOUR_LIGHT_BLUE; gStaffSecurityColour = COLOUR_YELLOW; diff --git a/src/openrct2/world/Park.h b/src/openrct2/world/Park.h index c1701897f8..82a48c7fae 100644 --- a/src/openrct2/world/Park.h +++ b/src/openrct2/world/Park.h @@ -51,6 +51,7 @@ namespace OpenRCT2 { public: std::string Name; + std::string PluginStorage; Park() = default; Park(const Park&) = delete;