mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-23 06:44:38 +01:00
Implement registering game actions
This commit is contained in:
18
distribution/openrct2.d.ts
vendored
18
distribution/openrct2.d.ts
vendored
@@ -80,12 +80,6 @@ declare global {
|
||||
expenditureType: ExpenditureType;
|
||||
}
|
||||
|
||||
interface GameActionDesc {
|
||||
id: string;
|
||||
query: (args: object) => GameActionResult;
|
||||
execute: (args: object) => GameActionResult;
|
||||
}
|
||||
|
||||
interface NetworkEventArgs {
|
||||
readonly player: number;
|
||||
}
|
||||
@@ -122,8 +116,15 @@ declare global {
|
||||
|
||||
/**
|
||||
* Registers a new game action that allows clients to interact with the game.
|
||||
* @param action The unique name of the action.
|
||||
* @param query Logic for validating and returning a price for an action.
|
||||
* @param execute Logic for validating and executing the action.
|
||||
* @throws An error if the action has already been registered by this or another plugin.
|
||||
*/
|
||||
registerGameAction(desc: GameActionDesc): void;
|
||||
registerGameAction(
|
||||
action: string,
|
||||
query: (args: object) => GameActionResult,
|
||||
execute: (args: object) => GameActionResult): void;
|
||||
|
||||
/**
|
||||
* Query the result of running a game action. This allows you to check the outcome and validity of
|
||||
@@ -156,8 +157,7 @@ declare global {
|
||||
subscribe(hook: "network.leave", callback: (e: NetworkEventArgs) => void): IDisposable;
|
||||
}
|
||||
|
||||
interface IntentDesc
|
||||
{
|
||||
interface IntentDesc {
|
||||
key: string;
|
||||
title?: string;
|
||||
shortcut?: string;
|
||||
|
||||
@@ -111,7 +111,7 @@ rct_window* window_error_open(rct_string_id title, rct_string_id message)
|
||||
|
||||
gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM;
|
||||
width = gfx_get_string_width_new_lined(_window_error_text);
|
||||
width = std::min(196, width);
|
||||
width = std::clamp(width, 64, 196);
|
||||
|
||||
gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM;
|
||||
gfx_wrap_string(_window_error_text, width + 1, &numLines, &fontHeight);
|
||||
|
||||
@@ -79,46 +79,31 @@ namespace OpenRCT2::Scripting
|
||||
{
|
||||
auto& scriptEngine = GetContext()->GetScriptEngine();
|
||||
auto ctx = scriptEngine.GetContext();
|
||||
if (args.type() == DukValue::Type::OBJECT)
|
||||
try
|
||||
{
|
||||
if (callback.is_function())
|
||||
auto action = CreateGameAction(actionid, args);
|
||||
if (action != nullptr)
|
||||
{
|
||||
try
|
||||
auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin();
|
||||
if (isExecute)
|
||||
{
|
||||
auto action = CreateGameAction(actionid, args);
|
||||
if (action != nullptr)
|
||||
{
|
||||
auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin();
|
||||
if (isExecute)
|
||||
{
|
||||
action->SetCallback(
|
||||
[this, plugin, callback](const GameAction*, const GameActionResult* res) -> void {
|
||||
HandleGameActionResult(plugin, *res, callback);
|
||||
});
|
||||
GameActions::Execute(action.get());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto res = GameActions::Query(action.get());
|
||||
HandleGameActionResult(plugin, *res, callback);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Unknown action.");
|
||||
}
|
||||
action->SetCallback([this, plugin, callback](const GameAction*, const GameActionResult* res) -> void {
|
||||
HandleGameActionResult(plugin, *res, callback);
|
||||
});
|
||||
GameActions::Execute(action.get());
|
||||
}
|
||||
catch (DukException&)
|
||||
else
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Invalid action parameters.");
|
||||
auto res = GameActions::Query(action.get());
|
||||
HandleGameActionResult(plugin, *res, callback);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Callback was not a function.");
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Unknown action.");
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (DukException&)
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "Invalid action parameters.");
|
||||
}
|
||||
@@ -148,7 +133,14 @@ namespace OpenRCT2::Scripting
|
||||
{
|
||||
// Serialise args to json so that it can be sent
|
||||
auto ctx = args.context();
|
||||
args.push();
|
||||
if (args.type() == DukValue::Type::OBJECT)
|
||||
{
|
||||
args.push();
|
||||
}
|
||||
else
|
||||
{
|
||||
duk_push_object(ctx);
|
||||
}
|
||||
auto jsonz = duk_json_encode(ctx, -1);
|
||||
auto json = std::string(jsonz);
|
||||
duk_pop(ctx);
|
||||
@@ -185,8 +177,30 @@ namespace OpenRCT2::Scripting
|
||||
|
||||
auto args = DukValue::take_from_stack(ctx);
|
||||
|
||||
// Call the plugin callback and pass the result object
|
||||
scriptEngine.ExecutePluginCall(plugin, callback, { args }, false);
|
||||
if (callback.is_function())
|
||||
{
|
||||
// Call the plugin callback and pass the result object
|
||||
scriptEngine.ExecutePluginCall(plugin, callback, { args }, false);
|
||||
}
|
||||
}
|
||||
|
||||
void registerGameAction(const std::string& action, const DukValue& query, const DukValue& execute)
|
||||
{
|
||||
auto& scriptEngine = GetContext()->GetScriptEngine();
|
||||
auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin();
|
||||
auto ctx = scriptEngine.GetContext();
|
||||
if (!query.is_function())
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "query was not a function.");
|
||||
}
|
||||
else if (!execute.is_function())
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "execute was not a function.");
|
||||
}
|
||||
else if (!scriptEngine.RegisterCustomAction(plugin, action, query, execute))
|
||||
{
|
||||
duk_error(ctx, DUK_ERR_ERROR, "action has already been registered.");
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -196,6 +210,7 @@ namespace OpenRCT2::Scripting
|
||||
dukglue_register_method(ctx, &ScContext::subscribe, "subscribe");
|
||||
dukglue_register_method(ctx, &ScContext::queryAction, "queryAction");
|
||||
dukglue_register_method(ctx, &ScContext::executeAction, "executeAction");
|
||||
dukglue_register_method(ctx, &ScContext::registerGameAction, "registerGameAction");
|
||||
}
|
||||
};
|
||||
} // namespace OpenRCT2::Scripting
|
||||
|
||||
@@ -459,6 +459,7 @@ void ScriptEngine::StopPlugin(std::shared_ptr<Plugin> plugin)
|
||||
{
|
||||
if (plugin->HasStarted())
|
||||
{
|
||||
RemoveCustomGameActions(plugin);
|
||||
_hookEngine.UnsubscribeAll(plugin);
|
||||
for (auto callback : _pluginStoppedSubscriptions)
|
||||
{
|
||||
@@ -653,7 +654,7 @@ std::future<void> ScriptEngine::Eval(const std::string& s)
|
||||
return future;
|
||||
}
|
||||
|
||||
bool ScriptEngine::ExecutePluginCall(
|
||||
DukValue ScriptEngine::ExecutePluginCall(
|
||||
const std::shared_ptr<Plugin>& plugin, const DukValue& func, const std::vector<DukValue>& args, bool isGameStateMutable)
|
||||
{
|
||||
if (func.is_function())
|
||||
@@ -667,9 +668,7 @@ bool ScriptEngine::ExecutePluginCall(
|
||||
auto result = duk_pcall(_context, static_cast<duk_idx_t>(args.size()));
|
||||
if (result == DUK_EXEC_SUCCESS)
|
||||
{
|
||||
// TODO allow result to be returned as a DukValue
|
||||
duk_pop(_context);
|
||||
return true;
|
||||
return DukValue::take_from_stack(_context);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -678,7 +677,7 @@ bool ScriptEngine::ExecutePluginCall(
|
||||
duk_pop(_context);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return DukValue();
|
||||
}
|
||||
|
||||
void ScriptEngine::LogPluginInfo(const std::shared_ptr<Plugin>& plugin, const std::string_view& message)
|
||||
@@ -695,27 +694,82 @@ void ScriptEngine::AddNetworkPlugin(const std::string_view& code)
|
||||
}
|
||||
|
||||
std::unique_ptr<GameActionResult> ScriptEngine::QueryOrExecuteCustomGameAction(
|
||||
const std::string_view& id,
|
||||
const std::string_view& args,
|
||||
bool isExecute)
|
||||
const std::string_view& id, const std::string_view& args, bool isExecute)
|
||||
{
|
||||
// Deserialise the JSON args
|
||||
std::string argsz(args);
|
||||
duk_push_string(_context, argsz.c_str());
|
||||
duk_json_decode(_context, -1);
|
||||
auto dukArgs = DukValue::take_from_stack(_context);
|
||||
|
||||
// Ready to call plugin handler
|
||||
if (isExecute)
|
||||
std::string actionz = std::string(id);
|
||||
auto kvp = _customActions.find(actionz);
|
||||
if (kvp != _customActions.end())
|
||||
{
|
||||
std::printf("EXECUTE: %s(%s)\n", std::string(id).c_str(), std::string(args).c_str());
|
||||
const auto& customAction = kvp->second;
|
||||
|
||||
// Deserialise the JSON args
|
||||
std::string argsz(args);
|
||||
duk_push_string(_context, argsz.c_str());
|
||||
duk_json_decode(_context, -1);
|
||||
auto dukArgs = DukValue::take_from_stack(_context);
|
||||
|
||||
// Ready to call plugin handler
|
||||
DukValue dukResult;
|
||||
if (!isExecute)
|
||||
{
|
||||
dukResult = ExecutePluginCall(customAction.Plugin, customAction.Query, { dukArgs }, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
dukResult = ExecutePluginCall(customAction.Plugin, customAction.Execute, { dukArgs }, true);
|
||||
}
|
||||
return DukToGameActionResult(dukResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::printf("QUERY: %s(%s)\n", std::string(id).c_str(), std::string(args).c_str());
|
||||
auto action = std::make_unique<GameActionResult>();
|
||||
action->Error = GA_ERROR::UNKNOWN;
|
||||
action->ErrorTitle = OBJECT_ERROR_UNKNOWN;
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<GameActionResult> ScriptEngine::DukToGameActionResult(const DukValue& d)
|
||||
{
|
||||
auto result = std::make_unique<GameActionResult>();
|
||||
result->Error = d["error"].type() == DukValue::Type::NUMBER ? static_cast<GA_ERROR>(d["error"].as_int()) : GA_ERROR::OK;
|
||||
auto errorTitle = d["errorTitle"].type() == DukValue::Type::STRING ? d["errorTitle"].as_string() : std::string();
|
||||
auto errorMessage = d["errorMessage"].type() == DukValue::Type::STRING ? d["errorMessage"].as_string() : std::string();
|
||||
result->Cost = d["cost"].type() == DukValue::Type::NUMBER ? d["cost"].as_int() : 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ScriptEngine::RegisterCustomAction(
|
||||
const std::shared_ptr<Plugin>& plugin, const std::string_view& action, const DukValue& query, const DukValue& execute)
|
||||
{
|
||||
std::string actionz = std::string(action);
|
||||
if (_customActions.find(actionz) != _customActions.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::make_unique<GameActionResult>();
|
||||
CustomAction customAction;
|
||||
customAction.Plugin = plugin;
|
||||
customAction.Name = std::move(actionz);
|
||||
customAction.Query = query;
|
||||
customAction.Execute = execute;
|
||||
_customActions[customAction.Name] = std::move(customAction);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScriptEngine::RemoveCustomGameActions(const std::shared_ptr<Plugin>& plugin)
|
||||
{
|
||||
for (auto it = _customActions.begin(); it != _customActions.end();)
|
||||
{
|
||||
if (it->second.Plugin == plugin)
|
||||
{
|
||||
it = _customActions.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string OpenRCT2::Scripting::Stringify(const DukValue& val)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
# include <mutex>
|
||||
# include <queue>
|
||||
# include <string>
|
||||
# include <unordered_map>
|
||||
# include <unordered_set>
|
||||
# include <vector>
|
||||
|
||||
@@ -118,6 +119,16 @@ namespace OpenRCT2::Scripting
|
||||
std::mutex _changedPluginFilesMutex;
|
||||
std::vector<std::function<void(std::shared_ptr<Plugin>)>> _pluginStoppedSubscriptions;
|
||||
|
||||
struct CustomAction
|
||||
{
|
||||
std::shared_ptr<Plugin> Plugin;
|
||||
std::string Name;
|
||||
DukValue Query;
|
||||
DukValue Execute;
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, CustomAction> _customActions;
|
||||
|
||||
public:
|
||||
ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env);
|
||||
ScriptEngine(ScriptEngine&) = delete;
|
||||
@@ -143,7 +154,7 @@ namespace OpenRCT2::Scripting
|
||||
void UnloadPlugins();
|
||||
void Update();
|
||||
std::future<void> Eval(const std::string& s);
|
||||
bool ExecutePluginCall(
|
||||
DukValue ExecutePluginCall(
|
||||
const std::shared_ptr<Plugin>& plugin, const DukValue& func, const std::vector<DukValue>& args,
|
||||
bool isGameStateMutable);
|
||||
|
||||
@@ -156,7 +167,11 @@ namespace OpenRCT2::Scripting
|
||||
|
||||
void AddNetworkPlugin(const std::string_view& code);
|
||||
|
||||
std::unique_ptr<GameActionResult> QueryOrExecuteCustomGameAction(const std::string_view& id, const std::string_view& args, bool isExecute);
|
||||
std::unique_ptr<GameActionResult> QueryOrExecuteCustomGameAction(
|
||||
const std::string_view& id, const std::string_view& args, bool isExecute);
|
||||
bool RegisterCustomAction(
|
||||
const std::shared_ptr<Plugin>& plugin, const std::string_view& action, const DukValue& query,
|
||||
const DukValue& execute);
|
||||
|
||||
private:
|
||||
void Initialise();
|
||||
@@ -170,6 +185,8 @@ namespace OpenRCT2::Scripting
|
||||
void SetupHotReloading();
|
||||
void AutoReloadPlugins();
|
||||
void ProcessREPL();
|
||||
void RemoveCustomGameActions(const std::shared_ptr<Plugin>& plugin);
|
||||
std::unique_ptr<GameActionResult> DukToGameActionResult(const DukValue& d);
|
||||
};
|
||||
|
||||
bool IsGameStateMutable();
|
||||
|
||||
Reference in New Issue
Block a user