Merge pull request #16685 from IntelOrca/plugin/park-storage

Add Plugin API for storing data in .park files
This commit is contained in:
Ted John 2022-02-25 14:23:10 +00:00 committed by GitHub
commit 18c4a4ea04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 198 additions and 24 deletions

View File

@ -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.

View File

@ -185,6 +185,20 @@ declare global {
*/
sharedStorage: Configuration;
/**
* Gets the storage for the current plugin if no name is specified.
* If a plugin name is specified, the storage for the plugin with that name will be returned.
* 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.
* @param pluginName The name of the plugin to get a store for. If undefined, the
* current plugin's name will be used. Plugin names are case sensitive.
*/
getParkStorage(pluginName?: string): Configuration;
/**
* Render the current state of the map and save to disc.
* Useful for server administration and timelapse creation.
@ -302,7 +316,7 @@ declare global {
}
interface Configuration {
getAll(namespace: string): { [name: string]: any };
getAll(namespace?: string): { [name: string]: any };
get<T>(key: string): T | undefined;
get<T>(key: string, defaultValue: T): T;
set<T>(key: string, value: T): void;

View File

@ -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.getParkStorage`. 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.

View File

@ -86,6 +86,11 @@ void GameState::InitAll(const TileCoordsXY& mapSize)
CheatsReset();
ClearRestrictedScenery();
#ifdef ENABLE_SCRIPTING
auto& scriptEngine = GetContext()->GetScriptEngine();
scriptEngine.ClearParkStorage();
#endif
}
/**

View File

@ -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;

View File

@ -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;

View File

@ -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++)

View File

@ -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<ScriptInterval> _intervals;
@ -195,11 +196,19 @@ namespace OpenRCT2::Scripting
{
return _sharedStorage;
}
DukValue GetParkStorage()
{
return _parkStorage;
}
std::vector<std::shared_ptr<Plugin>>& GetPlugins()
{
return _plugins;
}
void ClearParkStorage();
std::string GetParkStorageAsJSON();
void SetParkStorageFromJSON(std::string_view value);
void LoadPlugins();
void UnloadPlugins();
void Tick();

View File

@ -19,22 +19,30 @@
namespace OpenRCT2::Scripting
{
enum class ScConfigurationKind
{
User,
Shared,
Park
};
class ScConfiguration
{
private:
bool _isUserConfig{};
ScConfigurationKind _kind;
DukValue _backingObject;
public:
// context.configuration
ScConfiguration()
: _isUserConfig(true)
: _kind(ScConfigurationKind::User)
{
}
// context.sharedStorage
ScConfiguration(const DukValue& backingObject)
: _backingObject(backingObject)
// context.sharedStorage / context.getParkStorage
ScConfiguration(ScConfigurationKind kind, const DukValue& backingObject)
: _kind(kind)
, _backingObject(backingObject)
{
}
@ -68,15 +76,18 @@ namespace OpenRCT2::Scripting
std::optional<DukValue> GetNamespaceObject(std::string_view ns) const
{
auto store = _backingObject;
auto k = ns;
bool end;
do
if (!ns.empty())
{
auto [next, remainder] = GetNextNamespace(k);
store = store[next];
k = remainder;
end = store.type() == DukValue::Type::UNDEFINED || remainder.empty();
} while (!end);
auto k = ns;
bool end;
do
{
auto [next, remainder] = GetNextNamespace(k);
store = store[next];
k = remainder;
end = store.type() == DukValue::Type::UNDEFINED || remainder.empty();
} while (!end);
}
return store.type() == DukValue::OBJECT ? std::make_optional(store) : std::nullopt;
}
@ -112,17 +123,26 @@ namespace OpenRCT2::Scripting
bool IsValidNamespace(std::string_view ns) const
{
if (ns.empty() || ns[0] == '.' || ns[ns.size() - 1] == '.')
if (!ns.empty() && (ns[0] == '.' || ns[ns.size() - 1] == '.'))
{
return false;
}
for (size_t i = 1; i < ns.size() - 1; i++)
if (_kind != ScConfigurationKind::Park)
{
if (ns[i - 1] == '.' && ns[i] == '.')
if (ns.empty())
{
return false;
}
for (size_t i = 1; i < ns.size() - 1; i++)
{
if (ns[i - 1] == '.' && ns[i] == '.')
{
return false;
}
}
}
return true;
}
@ -131,13 +151,24 @@ namespace OpenRCT2::Scripting
return !key.empty() && key.find('.') == std::string_view::npos;
}
DukValue getAll(const std::string& ns) const
DukValue getAll(const DukValue& dukNamespace) const
{
DukValue result;
auto ctx = GetContext()->GetScriptEngine().GetContext();
std::string ns = "";
if (dukNamespace.type() == DukValue::Type::STRING)
{
ns = dukNamespace.as_string();
}
else if (dukNamespace.type() != DukValue::Type::UNDEFINED)
{
duk_error(ctx, DUK_ERR_ERROR, "Namespace was invalid.");
}
if (IsValidNamespace(ns))
{
if (_isUserConfig)
if (_kind == ScConfigurationKind::User)
{
DukObject obj(ctx);
if (ns == "general")
@ -163,7 +194,7 @@ namespace OpenRCT2::Scripting
DukValue get(const std::string& key, const DukValue& defaultValue) const
{
auto ctx = GetContext()->GetScriptEngine().GetContext();
if (_isUserConfig)
if (_kind == ScConfigurationKind::User)
{
if (key == "general.language")
{
@ -214,7 +245,7 @@ namespace OpenRCT2::Scripting
{
auto& scriptEngine = GetContext()->GetScriptEngine();
auto ctx = scriptEngine.GetContext();
if (_isUserConfig)
if (_kind == ScConfigurationKind::User)
{
try
{

View File

@ -55,7 +55,59 @@ namespace OpenRCT2::Scripting
std::shared_ptr<ScConfiguration> sharedStorage_get()
{
auto& scriptEngine = GetContext()->GetScriptEngine();
return std::make_shared<ScConfiguration>(scriptEngine.GetSharedStorage());
return std::make_shared<ScConfiguration>(ScConfigurationKind::Shared, scriptEngine.GetSharedStorage());
}
std::shared_ptr<ScConfiguration> GetParkStorageForPlugin(std::string_view pluginName)
{
auto& scriptEngine = GetContext()->GetScriptEngine();
auto parkStore = scriptEngine.GetParkStorage();
auto pluginStore = parkStore[pluginName];
// Create if it doesn't exist
if (pluginStore.type() != DukValue::Type::OBJECT)
{
auto* ctx = scriptEngine.GetContext();
parkStore.push();
duk_push_object(ctx);
duk_put_prop_lstring(ctx, -2, pluginName.data(), pluginName.size());
duk_pop(ctx);
pluginStore = parkStore[pluginName];
}
return std::make_shared<ScConfiguration>(ScConfigurationKind::Park, pluginStore);
}
std::shared_ptr<ScConfiguration> getParkStorage(const DukValue& dukPluginName)
{
auto& scriptEngine = GetContext()->GetScriptEngine();
std::shared_ptr<ScConfiguration> result;
if (dukPluginName.type() == DukValue::Type::STRING)
{
auto& pluginName = dukPluginName.as_string();
if (pluginName.empty())
{
duk_error(scriptEngine.GetContext(), DUK_ERR_ERROR, "Plugin name is empty");
}
result = GetParkStorageForPlugin(pluginName);
}
else if (dukPluginName.type() == DukValue::Type::UNDEFINED)
{
auto plugin = _execInfo.GetCurrentPlugin();
if (plugin == nullptr)
{
duk_error(
scriptEngine.GetContext(), DUK_ERR_ERROR, "Plugin name must be specified when used from console.");
}
result = GetParkStorageForPlugin(plugin->GetMetadata().Name);
}
else
{
duk_error(scriptEngine.GetContext(), DUK_ERR_ERROR, "Invalid plugin name.");
}
return result;
}
void captureImage(const DukValue& options)
@ -381,6 +433,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_method(ctx, &ScContext::getParkStorage, "getParkStorage");
dukglue_register_method(ctx, &ScContext::captureImage, "captureImage");
dukglue_register_method(ctx, &ScContext::getObject, "getObject");
dukglue_register_method(ctx, &ScContext::getAllObjects, "getAllObjects");

View File

@ -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;

View File

@ -51,6 +51,7 @@ namespace OpenRCT2
{
public:
std::string Name;
std::string PluginStorage;
Park() = default;
Park(const Park&) = delete;