Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement intransient plugins #16707

Merged
merged 13 commits into from
Mar 22, 2022
2 changes: 2 additions & 0 deletions distribution/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
- Feature: [#16097] The Looping Roller Coaster can now draw all elements from the LIM Launched Roller Coaster.
- Feature: [#16132, #16389] The Corkscrew, Twister and Vertical Drop Roller Coasters can now draw inline twists.
- Feature: [#16144] [Plugin] Add ImageManager to API.
- Feature: [#16707] [Plugin] Implement intransient plugins.
- Feature: [#16707] [Plugin] New API for current mode, map.change hook and toolbox menu items on title screen.
- Feature: [#16731] [Plugin] New API for fetching and manipulating a staff member's patrol area.
- Feature: [#16800] [Plugin] Add lift hill speed properties to API.
- Feature: [#16806] Parkobj can load sprites from RCT image archives.
Expand Down
35 changes: 31 additions & 4 deletions distribution/openrct2.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// /// <reference path="/path/to/openrct2.d.ts" />
//

export type PluginType = "local" | "remote";
export type PluginType = "local" | "remote" | "intransient";

declare global {
/**
Expand Down Expand Up @@ -173,7 +173,7 @@ declare global {
/**
* The user's current configuration.
*/
configuration: Configuration;
readonly configuration: Configuration;

/**
* Shared generic storage for all plugins. Data is persistent across instances
Expand All @@ -183,7 +183,7 @@ declare global {
* the `set` method, do not rely on the file being saved by modifying your own
* objects. Functions and other internal structures will not be persisted.
*/
sharedStorage: Configuration;
readonly sharedStorage: Configuration;

/**
* Gets the storage for the current plugin if no name is specified.
Expand All @@ -199,6 +199,12 @@ declare global {
*/
getParkStorage(pluginName?: string): Configuration;

/**
* The current mode / screen the game is in. Can be used for example to check
* whether the game is currently on the title screen or in the scenario editor.
*/
readonly mode: GameMode;

/**
* Render the current state of the map and save to disc.
* Useful for server administration and timelapse creation.
Expand Down Expand Up @@ -286,6 +292,12 @@ declare global {
subscribe(hook: "guest.generation", callback: (e: GuestGenerationArgs) => void): IDisposable;
subscribe(hook: "vehicle.crash", callback: (e: VehicleCrashArgs) => void): IDisposable;
subscribe(hook: "map.save", callback: () => void): IDisposable;
subscribe(hook: "map.change", callback: () => void): IDisposable;

/**
* Can only be used in intransient plugins.
*/
subscribe(hook: "map.changed", callback: () => void): IDisposable;

/**
* Registers a function to be called every so often in realtime, specified by the given delay.
Expand Down Expand Up @@ -365,6 +377,13 @@ declare global {
transparent?: boolean;
}

type GameMode =
"normal" |
"title" |
"scenario_editor" |
"track_designer" |
"track_manager";

type ObjectType =
"ride" |
"small_scenery" |
Expand All @@ -387,7 +406,7 @@ declare global {
"interval.tick" | "interval.day" |
"network.chat" | "network.action" | "network.join" | "network.leave" |
"ride.ratings.calculate" | "action.location" | "vehicle.crash" |
"map.save";
"map.change" | "map.changed" | "map.save";

type ExpenditureType =
"ride_construction" |
Expand Down Expand Up @@ -2106,6 +2125,14 @@ declare global {

registerMenuItem(text: string, callback: () => void): void;

/**
* Registers a new item in the toolbox menu on the title screen.
* Only available to intransient plugins.
* @param text The menu item text.
* @param callback The function to call when the menu item is clicked.
*/
registerToolboxMenuItem(text: string, callback: () => void): void;

registerShortcut(desc: ShortcutDesc): void;
}

Expand Down
7 changes: 5 additions & 2 deletions distribution/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ Each script is a single physical javascript file within the `plugin` directory i

OpenRCT2 will load every single file with the extension `.js` in this directory recursively. So if you want to prevent a plug-in from being used, you must move it outside this directory, or rename it so the filename does not end with `.js`.

There are two types of scripts:
There are three types of scripts:
* Local
* Remote
* Intransient

Local scripts can **not** alter the game state. This allows each player to enable any local script for their own game without other players needing to also enable the same script. These scripts tend to provide extra tools for productivity, or new windows containing information.

Remote scripts on the other hand can alter the game state in certain contexts, thus must be enabled for every player in a multiplayer game. Players **cannot** enable or disable remote scripts for multiplayer servers they join. Instead the server will upload any remote scripts that have been enabled on the server to each player. This allows servers to enable scripts without players needing to manually download or enable the same script on their end.

Intransient scripts are similar to local scripts, in that they can **not** alter the game state. However they are loaded at the very start of launching OpenRCT2 and remain loaded until shutdown. This allows the plugin to provide functionality across different screens such as the title screen.

The authors must also define a licence for the plug-in, making it clear to the community whether that plug-in can be altered, copied, etc. A good reference material is listed on [ChooseALlicense](https://choosealicense.com/appendix/), try to pick one of them and use its corresponding identifier, as listed on [SPDX](https://spdx.org/licenses/).

## Writing Scripts
Expand Down Expand Up @@ -92,7 +95,7 @@ Debugging has not yet been implemented, but is planned. In the meantime, you can

> What does the error 'Game state is not mutable in this context' mean?

This means you are attempting to modify the game state (e.g. change the park, map or guests etc.) in a context where you should not be doing so. This might be because your script is defined as `local`, meaning it must work independently of other players, not having the script enabled, or a remote script attempting to modify the game in the main function or a user interface event.
This means you are attempting to modify the game state (e.g. change the park, map or guests etc.) in a context where you should not be doing so. This might be because your script is defined as `local` or `intransient`, meaning it must work independently of other players, not having the script enabled, or a remote script attempting to modify the game in the main function or a user interface event.

Any changes to the game state must be synchronised across all players so that the same changes happen on the same tick for every player. This prevents the game going out of sync. To do this you must only change game state in a compatible hook such as `interval.day` or in the execute method of a game action. Game actions allow players to make specific changes to the game providing they have the correct permissions and the server allows it.

Expand Down
1 change: 0 additions & 1 deletion src/openrct2-ui/scripting/CustomMenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ namespace OpenRCT2::Scripting
duk_error(scriptEngine.GetContext(), DUK_ERR_ERROR, "Invalid parameters.");
}
}

} // namespace OpenRCT2::Scripting

#endif
11 changes: 10 additions & 1 deletion src/openrct2-ui/scripting/CustomMenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,24 @@ enum class CursorID : uint8_t;

namespace OpenRCT2::Scripting
{
enum class CustomToolbarMenuItemKind
{
Standard,
Toolbox,
};

class CustomToolbarMenuItem
{
public:
std::shared_ptr<Plugin> Owner;
CustomToolbarMenuItemKind Kind;
std::string Text;
DukValue Callback;

CustomToolbarMenuItem(std::shared_ptr<Plugin> owner, const std::string& text, DukValue callback)
CustomToolbarMenuItem(
std::shared_ptr<Plugin> owner, CustomToolbarMenuItemKind kind, const std::string& text, DukValue callback)
: Owner(owner)
, Kind(kind)
, Text(text)
, Callback(callback)
{
Expand Down
9 changes: 8 additions & 1 deletion src/openrct2-ui/scripting/ScTitleSequence.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,14 @@ namespace OpenRCT2::Scripting
parkImporter->Import();

auto old = gLoadKeepWindowsOpen;
gLoadKeepWindowsOpen = true;

// Unless we are already in the game, we have to re-create the windows
// so that the game toolbars are created.
if (gScreenFlags == SCREEN_FLAGS_PLAYING)
{
gLoadKeepWindowsOpen = true;
}

if (isScenario)
scenario_begin();
else
Expand Down
17 changes: 16 additions & 1 deletion src/openrct2-ui/scripting/ScUi.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,21 @@ namespace OpenRCT2::Scripting
{
auto& execInfo = _scriptEngine.GetExecInfo();
auto owner = execInfo.GetCurrentPlugin();
CustomMenuItems.emplace_back(owner, text, callback);
CustomMenuItems.emplace_back(owner, CustomToolbarMenuItemKind::Standard, text, callback);
}

void registerToolboxMenuItem(const std::string& text, DukValue callback)
{
auto& execInfo = _scriptEngine.GetExecInfo();
auto owner = execInfo.GetCurrentPlugin();
if (owner->GetMetadata().Type == PluginType::Intransient)
{
CustomMenuItems.emplace_back(owner, CustomToolbarMenuItemKind::Toolbox, text, callback);
}
else
{
duk_error(_scriptEngine.GetContext(), DUK_ERR_ERROR, "Plugin must be intransient.");
}
}

void registerShortcut(DukValue desc)
Expand Down Expand Up @@ -364,6 +378,7 @@ namespace OpenRCT2::Scripting
dukglue_register_method(ctx, &ScUi::showScenarioSelect, "showScenarioSelect");
dukglue_register_method(ctx, &ScUi::activateTool, "activateTool");
dukglue_register_method(ctx, &ScUi::registerMenuItem, "registerMenuItem");
dukglue_register_method(ctx, &ScUi::registerToolboxMenuItem, "registerToolboxMenuItem");
dukglue_register_method(ctx, &ScUi::registerShortcut, "registerShortcut");
}

Expand Down
14 changes: 12 additions & 2 deletions src/openrct2-ui/title/TitleSequencePlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,14 @@ class TitleSequencePlayer final : public ITitleSequencePlayer
auto parkHandle = TitleSequenceGetParkHandle(*_sequence, saveIndex);
if (parkHandle != nullptr)
{
game_notify_map_change();
loadSuccess = LoadParkFromStream(parkHandle->Stream.get(), parkHandle->HintPath);
}
if (!loadSuccess)
if (loadSuccess)
{
game_notify_map_changed();
}
else
{
if (_sequence->Saves.size() > saveIndex)
{
Expand All @@ -305,9 +310,14 @@ class TitleSequencePlayer final : public ITitleSequencePlayer
auto scenario = GetScenarioRepository()->GetByInternalName(command.Scenario);
if (scenario != nullptr)
{
game_notify_map_change();
loadSuccess = LoadParkFromFile(scenario->path);
}
if (!loadSuccess)
if (loadSuccess)
{
game_notify_map_changed();
}
else
{
Console::Error::WriteLine("Failed to load: \"%s\" for the title sequence.", command.Scenario);
return false;
Expand Down
1 change: 1 addition & 0 deletions src/openrct2-ui/windows/EditorObjectSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ class EditorObjectSelectionWindow final : public Window
}
if (gScreenFlags & SCREEN_FLAGS_TRACK_MANAGER)
{
game_notify_map_change();
game_unload_scripts();
title_load();
}
Expand Down
7 changes: 5 additions & 2 deletions src/openrct2-ui/windows/ServerStart.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ static void WindowServerStartClose(rct_window* w)

static void WindowServerStartScenarioselectCallback(const utf8* path)
{
network_set_password(_password);
game_notify_map_change();
if (context_load_park_from_file(path))
{
network_begin_server(gConfigNetwork.default_port, gConfigNetwork.listen_address.c_str());
Expand All @@ -135,8 +135,10 @@ static void WindowServerStartScenarioselectCallback(const utf8* path)

static void WindowServerStartLoadsaveCallback(int32_t result, const utf8* path)
{
if (result == MODAL_RESULT_OK && context_load_park_from_file(path))
if (result == MODAL_RESULT_OK)
{
game_notify_map_change();
context_load_park_from_file(path);
network_begin_server(gConfigNetwork.default_port, gConfigNetwork.listen_address.c_str());
}
}
Expand Down Expand Up @@ -185,6 +187,7 @@ static void WindowServerStartMouseup(rct_window* w, rct_widgetindex widgetIndex)
w->Invalidate();
break;
case WIDX_START_SERVER:
network_set_password(_password);
WindowScenarioselectOpen(WindowServerStartScenarioselectCallback, false);
break;
case WIDX_LOAD_SERVER:
Expand Down