Merge pull request #11580 from IntelOrca/plugin/custom-tool

[Plugin] Implement custom tool API
This commit is contained in:
Tulio Leao 2020-05-02 08:28:59 -03:00 committed by GitHub
commit 1f7ef019fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 604 additions and 11 deletions

View File

@ -53,11 +53,19 @@ declare global {
dispose(): void;
}
/**
* A coordinate within the game's client screen in pixels.
*/
interface ScreenCoordsXY {
x: number;
y: number;
}
/**
* A coordinate within the game.
* Each in-game tile is a size of 32x32.
*/
interface Coord2 {
interface CoordsXY {
x: number;
y: number;
}
@ -67,10 +75,18 @@ declare global {
* Each in-game tile is a size of 32x32.
* The z-coordinate raises 16 per land increment. A full-height wall is 32 in height.
*/
interface Coord3 extends Coord2 {
interface CoordsXYZ extends CoordsXY {
z: number;
}
/**
* A rectangular area specified using two coordinates.
*/
interface MapRange {
leftTop: CoordsXY;
rightBottom: CoordsXY;
}
/**
* Represents information about the plugin such as type, name, author and version.
* It also includes the entry point.
@ -245,7 +261,7 @@ declare global {
error?: number;
errorTitle?: string;
errorMessage?: string;
position?: Coord3;
position?: CoordsXYZ;
cost?: number;
expenditureType?: ExpenditureType;
}
@ -328,7 +344,7 @@ declare global {
* APIs for the map.
*/
interface GameMap {
readonly size: Coord2;
readonly size: CoordsXY;
readonly numRides: number;
readonly numEntities: number;
readonly rides: Ride[];
@ -710,6 +726,8 @@ declare global {
readonly height: number;
readonly windows: number;
readonly mainViewport: Viewport;
readonly tileSelection: TileSelection;
readonly tool: Tool;
getWindow(id: number): Window;
getWindow(classification: string): Window;
@ -717,9 +735,79 @@ declare global {
closeWindows(classification: string, id?: number): void;
closeAllWindows(): void;
/**
* Begins a new tool session. The cursor will change to the style specified by the
* given tool descriptor and cursor events will be provided.
* @param tool The properties and event handlers for the tool.
*/
activateTool(tool: ToolDesc): void;
registerMenuItem(text: string, callback: () => void): void;
}
interface TileSelection {
range: MapRange;
tiles: CoordsXY[];
}
interface Tool {
id: string;
cursor: CursorType;
cancel: () => void;
}
interface ToolEventArgs {
readonly isDown: boolean;
readonly screenCoords: ScreenCoordsXY;
readonly mapCoords?: CoordsXYZ;
readonly tileElementIndex?: number;
readonly entityId?: number;
}
/**
* Describes the properties and event handlers for a custom tool.
*/
interface ToolDesc {
id: string;
cursor?: CursorType;
onStart: () => void;
onDown: (e: ToolEventArgs) => void;
onMove: (e: ToolEventArgs) => void;
onUp: (e: ToolEventArgs) => void;
onFinish: () => void;
}
type CursorType =
"arrow" |
"bench_down" |
"bin_down" |
"blank" |
"cross_hair" |
"diagonal_arrows" |
"dig_down" |
"entrance_down" |
"fence_down" |
"flower_down" |
"fountain_down" |
"hand_closed" |
"hand_open" |
"hand_point" |
"house_down" |
"lamppost_down" |
"paint_down" |
"path_down" |
"picker" |
"statue_down" |
"tree_down" |
"up_arrow" |
"up_down_arrow" |
"volcano_down" |
"walk_down" |
"water_down" |
"zzz";
/**
* Represents the type of a widget, e.g. button or label.
*/
@ -822,7 +910,7 @@ declare global {
frameBase: number;
frameCount?: number;
frameDuration?: number;
offset?: Coord2;
offset?: ScreenCoordsXY;
}
interface WindowTabDesc {
@ -839,8 +927,8 @@ declare global {
zoom: number;
visibilityFlags: number;
getCentrePosition(): Coord2;
moveTo(position: Coord2 | Coord3): void;
scrollTo(position: Coord2 | Coord3): void;
getCentrePosition(): CoordsXY;
moveTo(position: CoordsXY | CoordsXYZ): void;
scrollTo(position: CoordsXY | CoordsXYZ): void;
}
}

View File

@ -11,12 +11,72 @@
# include "CustomMenu.h"
# include <openrct2/Input.h>
# include <openrct2/world/Map.h>
# include <openrct2/world/Sprite.h>
namespace OpenRCT2::Scripting
{
std::optional<CustomTool> ActiveCustomTool;
std::vector<CustomToolbarMenuItem> CustomMenuItems;
static void RemoveMenuItems(std::shared_ptr<Plugin> owner)
static constexpr std::array<std::string_view, CURSOR_COUNT> CursorNames = {
"arrow", "blank", "up_arrow", "up_down_arrow", "hand_point", "zzz", "diagonal_arrows",
"picker", "tree_down", "fountain_down", "statue_down", "bench_down", "cross_hair", "bin_down",
"lamppost_down", "fence_down", "flower_down", "path_down", "dig_down", "water_down", "house_down",
"volcano_down", "walk_down", "paint_down", "entrance_down", "hand_open", "hand_closed",
};
template<> DukValue ToDuk(duk_context* ctx, const CURSOR_ID& value)
{
if (value >= 0 && static_cast<size_t>(value) < std::size(CursorNames))
{
auto str = CursorNames[value];
duk_push_lstring(ctx, str.data(), str.size());
DukValue::take_from_stack(ctx);
}
return {};
}
template<> CURSOR_ID FromDuk(const DukValue& s)
{
if (s.type() == DukValue::Type::STRING)
{
auto it = std::find(std::begin(CursorNames), std::end(CursorNames), s.as_c_string());
if (it != std::end(CursorNames))
{
return static_cast<CURSOR_ID>(std::distance(std::begin(CursorNames), it));
}
}
return CURSOR_UNDEFINED;
}
template<> DukValue ToDuk(duk_context* ctx, const CoordsXY& coords)
{
DukObject dukCoords(ctx);
dukCoords.Set("x", coords.x);
dukCoords.Set("y", coords.y);
return dukCoords.Take();
}
template<> DukValue ToDuk(duk_context* ctx, const ScreenCoordsXY& coords)
{
DukObject dukCoords(ctx);
dukCoords.Set("x", coords.x);
dukCoords.Set("y", coords.y);
return dukCoords.Take();
}
static void RemoveMenuItemsAndTool(std::shared_ptr<Plugin> owner)
{
if (ActiveCustomTool)
{
if (ActiveCustomTool->Owner == owner)
{
tool_cancel();
}
}
auto& menuItems = CustomMenuItems;
for (auto it = menuItems.begin(); it != menuItems.end();)
{
@ -33,8 +93,131 @@ namespace OpenRCT2::Scripting
void InitialiseCustomMenuItems(ScriptEngine& scriptEngine)
{
scriptEngine.SubscribeToPluginStoppedEvent([](std::shared_ptr<Plugin> plugin) -> void { RemoveMenuItems(plugin); });
scriptEngine.SubscribeToPluginStoppedEvent(
[](std::shared_ptr<Plugin> plugin) -> void { RemoveMenuItemsAndTool(plugin); });
}
void CustomTool::OnUpdate(const ScreenCoordsXY& screenCoords)
{
InvokeEventHandler(onMove, screenCoords);
}
void CustomTool::OnDown(const ScreenCoordsXY& screenCoords)
{
MouseDown = true;
InvokeEventHandler(onDown, screenCoords);
}
void CustomTool::OnDrag(const ScreenCoordsXY& screenCoords)
{
}
void CustomTool::Start()
{
auto& scriptEngine = GetContext()->GetScriptEngine();
scriptEngine.ExecutePluginCall(Owner, onStart, {}, false);
}
void CustomTool::OnUp(const ScreenCoordsXY& screenCoords)
{
MouseDown = false;
InvokeEventHandler(onUp, screenCoords);
}
void CustomTool::OnAbort()
{
auto& scriptEngine = GetContext()->GetScriptEngine();
scriptEngine.ExecutePluginCall(Owner, onFinish, {}, false);
}
void CustomTool::InvokeEventHandler(const DukValue& dukHandler, const ScreenCoordsXY& screenCoords)
{
if (dukHandler.is_function())
{
auto ctx = dukHandler.context();
auto flags = 0;
CoordsXY mapCoords{};
int32_t interactionType = 0;
TileElement* tileElement{};
get_map_coordinates_from_pos(screenCoords, flags, mapCoords, &interactionType, &tileElement, nullptr);
DukObject obj(dukHandler.context());
obj.Set("isDown", MouseDown);
obj.Set("screenCoords", ToDuk(ctx, screenCoords));
obj.Set("mapCoords", ToDuk(ctx, mapCoords));
if (interactionType == VIEWPORT_INTERACTION_ITEM_SPRITE && tileElement != nullptr)
{
// get_map_coordinates_from_pos returns the sprite using tileElement... ugh
auto sprite = reinterpret_cast<rct_sprite*>(tileElement);
obj.Set("entityId", sprite->generic.sprite_index);
}
else if (tileElement != nullptr)
{
int32_t index = 0;
auto el = map_get_first_element_at(mapCoords);
if (el != nullptr)
{
do
{
if (el == tileElement)
{
obj.Set("tileElementIndex", index);
break;
}
index++;
} while (!(el++)->IsLastForTile());
}
}
auto eventArgs = obj.Take();
auto& scriptEngine = GetContext()->GetScriptEngine();
std::vector<DukValue> args;
args.push_back(eventArgs);
scriptEngine.ExecutePluginCall(Owner, dukHandler, args, false);
}
}
void InitialiseCustomTool(ScriptEngine& scriptEngine, const DukValue& dukValue)
{
try
{
if (dukValue.type() == DukValue::Type::OBJECT)
{
CustomTool customTool;
customTool.Owner = scriptEngine.GetExecInfo().GetCurrentPlugin();
customTool.Id = dukValue["id"].as_string();
customTool.Cursor = FromDuk<CURSOR_ID>(dukValue["cursor"]);
if (customTool.Cursor == CURSOR_UNDEFINED)
{
customTool.Cursor = CURSOR_ARROW;
}
customTool.onStart = dukValue["onStart"];
customTool.onDown = dukValue["onDown"];
customTool.onMove = dukValue["onMove"];
customTool.onUp = dukValue["onUp"];
customTool.onFinish = dukValue["onFinish"];
auto toolbarWindow = window_find_by_class(WC_TOP_TOOLBAR);
if (toolbarWindow != nullptr)
{
// Use a widget that does not exist on top toolbar but also make sure it isn't -1 as that
// prevents abort from being called.
rct_widgetindex widgetIndex = -2;
tool_cancel();
tool_set(toolbarWindow, widgetIndex, static_cast<TOOL_IDX>(customTool.Cursor));
ActiveCustomTool = std::move(customTool);
ActiveCustomTool->Start();
}
}
}
catch (const DukException&)
{
duk_error(scriptEngine.GetContext(), DUK_ERR_ERROR, "Invalid parameters.");
}
}
} // namespace OpenRCT2::Scripting
#endif

View File

@ -13,6 +13,7 @@
# include <memory>
# include <openrct2/Context.h>
# include <openrct2/interface/Cursors.h>
# include <openrct2/scripting/Duktape.hpp>
# include <openrct2/scripting/ScriptEngine.h>
# include <string>
@ -41,9 +42,39 @@ namespace OpenRCT2::Scripting
}
};
struct CustomTool
{
std::shared_ptr<Plugin> Owner;
std::string Id;
CURSOR_ID Cursor{};
bool MouseDown{};
// Event handlers
DukValue onStart;
DukValue onDown;
DukValue onMove;
DukValue onUp;
DukValue onFinish;
void Start();
void OnUpdate(const ScreenCoordsXY& screenCoords);
void OnDown(const ScreenCoordsXY& screenCoords);
void OnDrag(const ScreenCoordsXY& screenCoords);
void OnUp(const ScreenCoordsXY& screenCoords);
void OnAbort();
private:
void InvokeEventHandler(const DukValue& dukHandler, const ScreenCoordsXY& screenCoords);
};
extern std::optional<CustomTool> ActiveCustomTool;
extern std::vector<CustomToolbarMenuItem> CustomMenuItems;
void InitialiseCustomMenuItems(ScriptEngine& scriptEngine);
void InitialiseCustomTool(ScriptEngine& scriptEngine, const DukValue& dukValue);
template<> DukValue ToDuk(duk_context* ctx, const CURSOR_ID& value);
template<> CURSOR_ID FromDuk(const DukValue& s);
} // namespace OpenRCT2::Scripting

View File

@ -0,0 +1,178 @@
/*****************************************************************************
* Copyright (c) 2014-2020 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#pragma once
#ifdef ENABLE_SCRIPTING
# include <openrct2/common.h>
# include <openrct2/scripting/Duktape.hpp>
# include <openrct2/world/Map.h>
namespace OpenRCT2::Scripting
{
class ScTileSelection
{
private:
duk_context* _ctx{};
public:
ScTileSelection(duk_context* ctx)
: _ctx(ctx)
{
}
DukValue range_get() const
{
if (gMapSelectFlags & MAP_SELECT_FLAG_ENABLE)
{
DukObject range(_ctx);
DukObject leftTop(_ctx);
leftTop.Set("x", gMapSelectPositionA.x);
leftTop.Set("y", gMapSelectPositionA.y);
range.Set("leftTop", leftTop.Take());
DukObject rightBottom(_ctx);
rightBottom.Set("x", gMapSelectPositionB.x);
rightBottom.Set("y", gMapSelectPositionB.y);
range.Set("rightBottom", rightBottom.Take());
return range.Take();
}
else
{
duk_push_null(_ctx);
return DukValue::take_from_stack(_ctx);
}
}
void range_set(DukValue value)
{
map_invalidate_selection_rect();
if (value.type() == DukValue::Type::OBJECT)
{
auto range = GetMapRange(value);
if (range)
{
gMapSelectPositionA.x = range->GetLeft();
gMapSelectPositionA.y = range->GetTop();
gMapSelectPositionB.x = range->GetRight();
gMapSelectPositionB.y = range->GetBottom();
gMapSelectType = MAP_SELECT_TYPE_FULL;
gMapSelectFlags |= MAP_SELECT_FLAG_ENABLE;
}
}
else
{
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE;
}
map_invalidate_selection_rect();
}
DukValue tiles_get() const
{
duk_push_array(_ctx);
if (gMapSelectFlags & MAP_SELECT_FLAG_ENABLE_CONSTRUCT)
{
duk_uarridx_t index = 0;
for (const auto& tile : gMapSelectionTiles)
{
duk_push_object(_ctx);
duk_push_int(_ctx, tile.x);
duk_put_prop_string(_ctx, -2, "x");
duk_push_int(_ctx, tile.y);
duk_put_prop_string(_ctx, -2, "y");
duk_put_prop_index(_ctx, -2, index);
index++;
}
}
return DukValue::take_from_stack(_ctx);
}
void tiles_set(DukValue value)
{
map_invalidate_map_selection_tiles();
gMapSelectionTiles.clear();
if (value.is_array())
{
value.push();
auto arrayLen = duk_get_length(_ctx, -1);
for (duk_uarridx_t i = 0; i < arrayLen; i++)
{
if (duk_get_prop_index(_ctx, -1, i))
{
auto dukElement = DukValue::take_from_stack(_ctx);
auto coords = GetCoordsXY(dukElement);
if (coords)
{
gMapSelectionTiles.push_back(*coords);
}
}
}
duk_pop(_ctx);
}
if (gMapSelectionTiles.empty())
{
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE_CONSTRUCT;
gMapSelectFlags &= ~MAP_SELECT_FLAG_GREEN;
}
else
{
gMapSelectFlags |= MAP_SELECT_FLAG_ENABLE_CONSTRUCT;
}
map_invalidate_map_selection_tiles();
}
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScTileSelection::range_get, &ScTileSelection::range_set, "range");
dukglue_register_property(ctx, &ScTileSelection::tiles_get, &ScTileSelection::tiles_set, "tiles");
}
private:
static std::optional<CoordsXY> GetCoordsXY(const DukValue& dukCoords)
{
std::optional<CoordsXY> result;
if (dukCoords.type() == DukValue::Type::OBJECT)
{
auto dukX = dukCoords["x"];
if (dukX.type() == DukValue::Type::NUMBER)
{
auto dukY = dukCoords["y"];
if (dukY.type() == DukValue::Type::NUMBER)
{
result = { dukX.as_int(), dukY.as_int() };
}
}
}
return result;
}
static std::optional<MapRange> GetMapRange(const DukValue& dukMapRange)
{
std::optional<MapRange> result;
if (dukMapRange.type() == DukValue::Type::OBJECT)
{
auto leftTop = GetCoordsXY(dukMapRange["leftTop"]);
if (leftTop.has_value())
{
auto rightBottom = GetCoordsXY(dukMapRange["rightBottom"]);
if (rightBottom.has_value())
{
result = MapRange(leftTop->x, leftTop->y, rightBottom->x, rightBottom->y);
}
}
}
return result;
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -12,11 +12,14 @@
#ifdef ENABLE_SCRIPTING
# include "CustomMenu.h"
# include "ScTileSelection.hpp"
# include "ScViewport.hpp"
# include "ScWindow.hpp"
# include <algorithm>
# include <memory>
# include <openrct2/Context.h>
# include <openrct2/Input.h>
# include <openrct2/common.h>
# include <openrct2/scripting/Duktape.hpp>
# include <openrct2/scripting/ScriptEngine.h>
@ -34,6 +37,41 @@ namespace OpenRCT2::Ui::Windows
namespace OpenRCT2::Scripting
{
class ScTool
{
private:
duk_context* _ctx{};
public:
ScTool(duk_context* ctx)
: _ctx(ctx)
{
}
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScTool::id_get, nullptr, "id");
dukglue_register_property(ctx, &ScTool::cursor_get, nullptr, "cursor");
dukglue_register_method(ctx, &ScTool::cancel, "cancel");
}
private:
std::string id_get() const
{
return ActiveCustomTool ? ActiveCustomTool->Id : "";
}
DukValue cursor_get() const
{
return ToDuk(_ctx, static_cast<CURSOR_ID>(gCurrentToolId));
}
void cancel()
{
tool_cancel();
}
};
class ScUi
{
private:
@ -64,6 +102,20 @@ namespace OpenRCT2::Scripting
return std::make_shared<ScViewport>(WC_MAIN_WINDOW);
}
std::shared_ptr<ScTileSelection> tileSelection_get() const
{
return std::make_shared<ScTileSelection>(_scriptEngine.GetContext());
}
std::shared_ptr<ScTool> tool_get() const
{
if (input_test_flag(INPUT_FLAG_TOOL_ACTIVE))
{
return std::make_shared<ScTool>(_scriptEngine.GetContext());
}
return {};
}
std::shared_ptr<ScWindow> openWindow(DukValue desc)
{
using namespace OpenRCT2::Ui::Windows;
@ -128,6 +180,11 @@ namespace OpenRCT2::Scripting
return {};
}
void activateTool(const DukValue& desc)
{
InitialiseCustomTool(_scriptEngine, desc);
}
void registerMenuItem(std::string text, DukValue callback)
{
auto& execInfo = _scriptEngine.GetExecInfo();
@ -142,10 +199,13 @@ namespace OpenRCT2::Scripting
dukglue_register_property(ctx, &ScUi::width_get, nullptr, "width");
dukglue_register_property(ctx, &ScUi::windows_get, nullptr, "windows");
dukglue_register_property(ctx, &ScUi::mainViewport_get, nullptr, "mainViewport");
dukglue_register_property(ctx, &ScUi::tileSelection_get, nullptr, "tileSelection");
dukglue_register_property(ctx, &ScUi::tool_get, nullptr, "tool");
dukglue_register_method(ctx, &ScUi::openWindow, "openWindow");
dukglue_register_method(ctx, &ScUi::closeWindows, "closeWindows");
dukglue_register_method(ctx, &ScUi::closeAllWindows, "closeAllWindows");
dukglue_register_method(ctx, &ScUi::getWindow, "getWindow");
dukglue_register_method(ctx, &ScUi::activateTool, "activateTool");
dukglue_register_method(ctx, &ScUi::registerMenuItem, "registerMenuItem");
}

View File

@ -12,6 +12,7 @@
# include "UiExtensions.h"
# include "CustomMenu.h"
# include "ScTileSelection.hpp"
# include "ScUi.hpp"
# include "ScWidget.hpp"
# include "ScWindow.hpp"
@ -26,6 +27,8 @@ void UiScriptExtensions::Extend(ScriptEngine& scriptEngine)
dukglue_register_global(ctx, std::make_shared<ScUi>(scriptEngine), "ui");
ScTileSelection::Register(ctx);
ScTool::Register(ctx);
ScUi::Register(ctx);
ScViewport::Register(ctx);
ScWidget::Register(ctx);

View File

@ -1,5 +1,5 @@
/*****************************************************************************
* Copyright (c) 2014-2019 OpenRCT2 developers
* Copyright (c) 2014-2020 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
@ -2961,6 +2961,15 @@ static void window_top_toolbar_tool_update(rct_window* w, rct_widgetindex widget
case WIDX_SCENERY:
top_toolbar_tool_update_scenery(screenCoords);
break;
#ifdef ENABLE_SCRIPTING
default:
auto& customTool = OpenRCT2::Scripting::ActiveCustomTool;
if (customTool)
{
customTool->OnUpdate(screenCoords);
}
break;
#endif
}
}
@ -3009,6 +3018,15 @@ static void window_top_toolbar_tool_down(rct_window* w, rct_widgetindex widgetIn
case WIDX_SCENERY:
window_top_toolbar_scenery_tool_down(screenCoords, w, widgetIndex);
break;
#ifdef ENABLE_SCRIPTING
default:
auto& customTool = OpenRCT2::Scripting::ActiveCustomTool;
if (customTool)
{
customTool->OnDown(screenCoords);
}
break;
#endif
}
}
@ -3227,6 +3245,15 @@ static void window_top_toolbar_tool_drag(rct_window* w, rct_widgetindex widgetIn
if (gWindowSceneryEyedropperEnabled)
window_top_toolbar_scenery_tool_down(screenCoords, w, widgetIndex);
break;
#ifdef ENABLE_SCRIPTING
default:
auto& customTool = OpenRCT2::Scripting::ActiveCustomTool;
if (customTool)
{
customTool->OnDrag(screenCoords);
}
break;
#endif
}
}
@ -3254,6 +3281,15 @@ static void window_top_toolbar_tool_up(rct_window* w, rct_widgetindex widgetInde
gMapSelectFlags &= ~MAP_SELECT_FLAG_ENABLE;
gCurrentToolId = TOOL_CROSSHAIR;
break;
#ifdef ENABLE_SCRIPTING
default:
auto& customTool = OpenRCT2::Scripting::ActiveCustomTool;
if (customTool)
{
customTool->OnUp(screenCoords);
}
break;
#endif
}
}
@ -3270,6 +3306,16 @@ static void window_top_toolbar_tool_abort(rct_window* w, rct_widgetindex widgetI
case WIDX_CLEAR_SCENERY:
hide_gridlines();
break;
#ifdef ENABLE_SCRIPTING
default:
auto& customTool = OpenRCT2::Scripting::ActiveCustomTool;
if (customTool)
{
customTool->OnAbort();
customTool = {};
}
break;
#endif
}
}

View File

@ -189,6 +189,10 @@ namespace OpenRCT2::Scripting
return std::nullopt;
}
}
template<typename T> DukValue ToDuk(duk_context* ctx, const T& value) = delete;
template<typename T> T FromDuk(const DukValue& s) = delete;
} // namespace OpenRCT2::Scripting
#endif