Merge pull request #11320 from IntelOrca/epic/plug-in-4

Implement THE PLUGIN SYSTEM!
This commit is contained in:
Ted John 2020-04-26 22:50:30 +01:00 committed by GitHub
commit f9909e76f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 11951 additions and 129 deletions

View File

@ -71,7 +71,7 @@ jobs:
run: |
sudo su
mkdir bin && cd bin
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=../CMakeLists_mingw.txt -DCMAKE_BUILD_TYPE=MinSizeRel -DDISABLE_IPO=on -DFORCE32=on -DBUILD_SHARED_LIBS=ON
cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=../CMakeLists_mingw.txt -DCMAKE_BUILD_TYPE=MinSizeRel -DDISABLE_IPO=on -DFORCE32=on -DBUILD_SHARED_LIBS=ON -DENABLE_SCRIPTING=OFF
ninja -k0
macos:
name: macOS

View File

@ -56,6 +56,7 @@
"includePath": [
"${workspaceRoot}",
"${workspaceRoot}/src",
"${workspaceRoot}/src/thirdparty",
"${workspaceRoot}/lib/Win32/include",
"${workspaceRoot}/lib/x64/include",
"${workspaceRoot}/lib/googletest/googletest/include"
@ -63,7 +64,8 @@
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
"_UNICODE",
"__ENABLE_SCRIPTING__"
],
"intelliSenseMode": "msvc-x64",
"browse": {

View File

@ -49,6 +49,7 @@ option(DISABLE_HTTP_TWITCH "Disable HTTP and Twitch support.")
option(DISABLE_NETWORK "Disable multiplayer functionality. Mainly for testing.")
option(DISABLE_TTF "Disable support for TTF provided by freetype2.")
option(ENABLE_LIGHTFX "Enable lighting effects." ON)
option(ENABLE_SCRIPTING "Enable script / plugin support." ON)
option(DISABLE_GUI "Don't build GUI. (Headless only.)")
@ -121,6 +122,9 @@ endif ()
if (ENABLE_LIGHTFX)
add_definitions(-D__ENABLE_LIGHTFX__)
endif ()
if (ENABLE_SCRIPTING)
add_definitions(-DENABLE_SCRIPTING)
endif ()
if (NOT DISABLE_DISCORD_RPC)
if(EXISTS "${ROOT_DIR}/discord-rpc")
@ -287,7 +291,9 @@ endif ()
# Don't recurse, grab all *.txt and *.md files
file(GLOB DOC_FILES "${ROOT_DIR}/distribution/*.txt")
list(APPEND DOC_FILES "${ROOT_DIR}/contributors.md"
"${ROOT_DIR}/licence.txt")
"${ROOT_DIR}/licence.txt"
"${ROOT_DIR}/distribution/scripting.md"
"${ROOT_DIR}/distribution/openrct2.d.ts")
# CMake does not allow specifying a dependency chain which includes built-in
# targets, like `install`, so we have to trick it and execute dependency ourselves.

View File

@ -43,6 +43,9 @@
2ADE2F3622441960002598AF /* RideTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 2ADE2F352244195F002598AF /* RideTypes.h */; };
2ADE2F382244198B002598AF /* SpriteBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 2ADE2F372244198A002598AF /* SpriteBase.h */; };
304FE95023A2996600470197 /* SceneryScatter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 304FE94F23A2996600470197 /* SceneryScatter.cpp */; };
4C255958244A328B00CE7E45 /* CustomMenu.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C25594F244A328A00CE7E45 /* CustomMenu.cpp */; };
4C255959244A328B00CE7E45 /* UiExtensions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C255954244A328A00CE7E45 /* UiExtensions.cpp */; };
4C25595A244A328B00CE7E45 /* CustomWindow.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C255957244A328B00CE7E45 /* CustomWindow.cpp */; };
4C29DEB3218C6AE500E8707F /* RCT12.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C29DEB2218C6AE500E8707F /* RCT12.cpp */; };
4C358E5221C445F700ADE6BC /* ReplayManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C358E5021C445F700ADE6BC /* ReplayManager.cpp */; };
4C3B4236205914F7000C5BB7 /* InGameConsole.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C3B4234205914F7000C5BB7 /* InGameConsole.cpp */; };
@ -105,6 +108,27 @@
93CBA4CC20A7504500867D56 /* ImageImporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 93CBA4C820A7504500867D56 /* ImageImporter.h */; };
93DE9751209C3C1000FB1CC8 /* GameState.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93DE974E209C3C0F00FB1CC8 /* GameState.cpp */; };
93DE9753209C3C1000FB1CC8 /* GameState.h in Headers */ = {isa = PBXBuildFile; fileRef = 93DE974F209C3C0F00FB1CC8 /* GameState.h */; };
93DFD02E24521BA0001FCBAF /* FileWatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD02C24521B9F001FCBAF /* FileWatcher.h */; };
93DFD02F24521BA0001FCBAF /* FileWatcher.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93DFD02D24521BA0001FCBAF /* FileWatcher.cpp */; };
93DFD04424521C1A001FCBAF /* Plugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03124521C19001FCBAF /* Plugin.h */; };
93DFD04524521C1A001FCBAF /* ScObject.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03224521C19001FCBAF /* ScObject.hpp */; };
93DFD04624521C1A001FCBAF /* HookEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03324521C19001FCBAF /* HookEngine.h */; };
93DFD04724521C1A001FCBAF /* ScNetwork.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03424521C19001FCBAF /* ScNetwork.hpp */; };
93DFD04824521C1A001FCBAF /* HookEngine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93DFD03524521C19001FCBAF /* HookEngine.cpp */; };
93DFD04924521C1A001FCBAF /* ScTile.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03624521C19001FCBAF /* ScTile.hpp */; };
93DFD04A24521C1A001FCBAF /* ScConfiguration.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03724521C19001FCBAF /* ScConfiguration.hpp */; };
93DFD04B24521C1A001FCBAF /* ScriptEngine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93DFD03824521C19001FCBAF /* ScriptEngine.cpp */; };
93DFD04C24521C1A001FCBAF /* ScDisposable.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03924521C19001FCBAF /* ScDisposable.hpp */; };
93DFD04D24521C1A001FCBAF /* ScEntity.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03A24521C19001FCBAF /* ScEntity.hpp */; };
93DFD04E24521C1A001FCBAF /* Duktape.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03B24521C19001FCBAF /* Duktape.hpp */; };
93DFD04F24521C1A001FCBAF /* ScConsole.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03C24521C19001FCBAF /* ScConsole.hpp */; };
93DFD05024521C1A001FCBAF /* ScPark.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03D24521C19001FCBAF /* ScPark.hpp */; };
93DFD05124521C1A001FCBAF /* ScContext.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD03E24521C19001FCBAF /* ScContext.hpp */; };
93DFD05224521C1A001FCBAF /* Plugin.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93DFD03F24521C19001FCBAF /* Plugin.cpp */; };
93DFD05324521C1A001FCBAF /* ScRide.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD04024521C19001FCBAF /* ScRide.hpp */; };
93DFD05424521C1A001FCBAF /* ScDate.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD04124521C19001FCBAF /* ScDate.hpp */; };
93DFD05524521C1A001FCBAF /* ScMap.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD04224521C19001FCBAF /* ScMap.hpp */; };
93DFD05624521C1A001FCBAF /* ScriptEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 93DFD04324521C19001FCBAF /* ScriptEngine.h */; };
93F6004C213DD7DD00EEB83E /* TerrainSurfaceObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93F60049213DD7DC00EEB83E /* TerrainSurfaceObject.cpp */; };
93F6004D213DD7DD00EEB83E /* TerrainEdgeObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93F6004A213DD7DC00EEB83E /* TerrainEdgeObject.cpp */; };
93F60050213DD7E400EEB83E /* StationObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93F6004F213DD7E300EEB83E /* StationObject.cpp */; };
@ -126,6 +150,10 @@
93F9DA3920B46FB800D1BE92 /* ObjectJsonHelpers.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9AAAB1FDA7B14004093C6 /* ObjectJsonHelpers.cpp */; };
93F9DA3A20B46FCA00D1BE92 /* SceneryObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A53EC205FD19F000F8EF5 /* SceneryObject.cpp */; };
93F9DA3B20B4701100D1BE92 /* StdInOutConsole.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C3B423720591513000C5BB7 /* StdInOutConsole.cpp */; };
93FC08FF2418F3ED00CA3054 /* duktape.h in Headers */ = {isa = PBXBuildFile; fileRef = 93FC08FD2418F3ED00CA3054 /* duktape.h */; };
93FC09002418F3ED00CA3054 /* duk_config.h in Headers */ = {isa = PBXBuildFile; fileRef = 93FC08FE2418F3ED00CA3054 /* duk_config.h */; };
93FC09022418F3F500CA3054 /* libduktape.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 93FC09012418F3F500CA3054 /* libduktape.dylib */; };
93FC09032418F41700CA3054 /* libduktape.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 93FC09012418F3F500CA3054 /* libduktape.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C61ADB1F1FB6A0A70024F2EF /* TopToolbar.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C61ADB1E1FB6A0A60024F2EF /* TopToolbar.cpp */; };
C61ADB211FB7DC060024F2EF /* Scenery.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C61ADB201FB7DC060024F2EF /* Scenery.cpp */; };
C61ADB231FBBCB8B0024F2EF /* GameBottomToolbar.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C61ADB221FBBCB8A0024F2EF /* GameBottomToolbar.cpp */; };
@ -584,6 +612,7 @@
933F32EB24183CBB008376CE /* libicuuc.dylib in Embed Frameworks */,
C6E96E371E040E040076A04F /* libzip.dylib in Embed Frameworks */,
D45A39591CF300AF00659A24 /* libcrypto.dylib in Embed Frameworks */,
93FC09032418F41700CA3054 /* libduktape.dylib in Embed Frameworks */,
D45A395A1CF300AF00659A24 /* libfreetype.dylib in Embed Frameworks */,
D45A395B1CF300AF00659A24 /* libjansson.dylib in Embed Frameworks */,
D4A8B4B51DB4188D007A2F29 /* libpng16.dylib in Embed Frameworks */,
@ -648,8 +677,37 @@
2ADE2F352244195F002598AF /* RideTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RideTypes.h; sourceTree = "<group>"; };
2ADE2F372244198A002598AF /* SpriteBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SpriteBase.h; sourceTree = "<group>"; };
304FE94F23A2996600470197 /* SceneryScatter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SceneryScatter.cpp; sourceTree = "<group>"; };
4C04D69F2056AA9600F82EBA /* linenoise.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = linenoise.hpp; sourceTree = "<group>"; };
4C1A53EC205FD19F000F8EF5 /* SceneryObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SceneryObject.cpp; sourceTree = "<group>"; };
4C25594E244A328A00CE7E45 /* CustomWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CustomWindow.h; path = scripting/CustomWindow.h; sourceTree = "<group>"; };
4C25594F244A328A00CE7E45 /* CustomMenu.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CustomMenu.cpp; path = scripting/CustomMenu.cpp; sourceTree = "<group>"; };
4C255950244A328A00CE7E45 /* ScUi.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ScUi.hpp; path = scripting/ScUi.hpp; sourceTree = "<group>"; };
4C255951244A328A00CE7E45 /* ScWidget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ScWidget.hpp; path = scripting/ScWidget.hpp; sourceTree = "<group>"; };
4C255952244A328A00CE7E45 /* ScWindow.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ScWindow.hpp; path = scripting/ScWindow.hpp; sourceTree = "<group>"; };
4C255953244A328A00CE7E45 /* CustomMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CustomMenu.h; path = scripting/CustomMenu.h; sourceTree = "<group>"; };
4C255954244A328A00CE7E45 /* UiExtensions.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = UiExtensions.cpp; path = scripting/UiExtensions.cpp; sourceTree = "<group>"; };
4C255955244A328A00CE7E45 /* ScViewport.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ScViewport.hpp; path = scripting/ScViewport.hpp; sourceTree = "<group>"; };
4C255956244A328B00CE7E45 /* UiExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = UiExtensions.h; path = scripting/UiExtensions.h; sourceTree = "<group>"; };
4C255957244A328B00CE7E45 /* CustomWindow.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CustomWindow.cpp; path = scripting/CustomWindow.cpp; sourceTree = "<group>"; };
4C25595C244A32E400CE7E45 /* filesystem.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = filesystem.hpp; path = src/thirdparty/filesystem.hpp; sourceTree = SOURCE_ROOT; };
4C25595D244A32E400CE7E45 /* linenoise.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = linenoise.hpp; path = src/thirdparty/linenoise.hpp; sourceTree = SOURCE_ROOT; };
4C25595F244A330700CE7E45 /* detail_stack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_stack.h; path = src/thirdparty/dukglue/detail_stack.h; sourceTree = SOURCE_ROOT; };
4C255960244A330700CE7E45 /* register_class.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = register_class.h; path = src/thirdparty/dukglue/register_class.h; sourceTree = SOURCE_ROOT; };
4C255961244A330700CE7E45 /* dukvalue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = dukvalue.h; path = src/thirdparty/dukglue/dukvalue.h; sourceTree = SOURCE_ROOT; };
4C255962244A330700CE7E45 /* detail_primitive_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_primitive_types.h; path = src/thirdparty/dukglue/detail_primitive_types.h; sourceTree = SOURCE_ROOT; };
4C255963244A330700CE7E45 /* dukglue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = dukglue.h; path = src/thirdparty/dukglue/dukglue.h; sourceTree = SOURCE_ROOT; };
4C255964244A330700CE7E45 /* detail_class_proto.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_class_proto.h; path = src/thirdparty/dukglue/detail_class_proto.h; sourceTree = SOURCE_ROOT; };
4C255965244A330700CE7E45 /* detail_traits.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_traits.h; path = src/thirdparty/dukglue/detail_traits.h; sourceTree = SOURCE_ROOT; };
4C255966244A330700CE7E45 /* register_function.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = register_function.h; path = src/thirdparty/dukglue/register_function.h; sourceTree = SOURCE_ROOT; };
4C255967244A330700CE7E45 /* detail_function.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_function.h; path = src/thirdparty/dukglue/detail_function.h; sourceTree = SOURCE_ROOT; };
4C255968244A330700CE7E45 /* detail_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_types.h; path = src/thirdparty/dukglue/detail_types.h; sourceTree = SOURCE_ROOT; };
4C255969244A330800CE7E45 /* detail_constructor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_constructor.h; path = src/thirdparty/dukglue/detail_constructor.h; sourceTree = SOURCE_ROOT; };
4C25596A244A330800CE7E45 /* public_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = public_util.h; path = src/thirdparty/dukglue/public_util.h; sourceTree = SOURCE_ROOT; };
4C25596B244A330800CE7E45 /* detail_refs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_refs.h; path = src/thirdparty/dukglue/detail_refs.h; sourceTree = SOURCE_ROOT; };
4C25596C244A330800CE7E45 /* register_property.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = register_property.h; path = src/thirdparty/dukglue/register_property.h; sourceTree = SOURCE_ROOT; };
4C25596D244A330800CE7E45 /* detail_method.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_method.h; path = src/thirdparty/dukglue/detail_method.h; sourceTree = SOURCE_ROOT; };
4C25596E244A330800CE7E45 /* dukexception.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = dukexception.h; path = src/thirdparty/dukglue/dukexception.h; sourceTree = SOURCE_ROOT; };
4C25596F244A330800CE7E45 /* detail_typeinfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = detail_typeinfo.h; path = src/thirdparty/dukglue/detail_typeinfo.h; sourceTree = SOURCE_ROOT; };
4C255971244A342900CE7E45 /* CustomAction.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CustomAction.hpp; sourceTree = "<group>"; };
4C29DEB2218C6AE500E8707F /* RCT12.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = RCT12.cpp; sourceTree = "<group>"; };
4C358E5021C445F700ADE6BC /* ReplayManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ReplayManager.cpp; sourceTree = "<group>"; };
4C358E5121C445F700ADE6BC /* ReplayManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReplayManager.h; sourceTree = "<group>"; };
@ -1257,6 +1315,27 @@
93CBA4C820A7504500867D56 /* ImageImporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageImporter.h; sourceTree = "<group>"; };
93DE974E209C3C0F00FB1CC8 /* GameState.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GameState.cpp; sourceTree = "<group>"; };
93DE974F209C3C0F00FB1CC8 /* GameState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GameState.h; sourceTree = "<group>"; };
93DFD02C24521B9F001FCBAF /* FileWatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FileWatcher.h; sourceTree = "<group>"; };
93DFD02D24521BA0001FCBAF /* FileWatcher.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FileWatcher.cpp; sourceTree = "<group>"; };
93DFD03124521C19001FCBAF /* Plugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Plugin.h; sourceTree = "<group>"; };
93DFD03224521C19001FCBAF /* ScObject.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScObject.hpp; sourceTree = "<group>"; };
93DFD03324521C19001FCBAF /* HookEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HookEngine.h; sourceTree = "<group>"; };
93DFD03424521C19001FCBAF /* ScNetwork.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScNetwork.hpp; sourceTree = "<group>"; };
93DFD03524521C19001FCBAF /* HookEngine.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = HookEngine.cpp; sourceTree = "<group>"; };
93DFD03624521C19001FCBAF /* ScTile.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScTile.hpp; sourceTree = "<group>"; };
93DFD03724521C19001FCBAF /* ScConfiguration.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScConfiguration.hpp; sourceTree = "<group>"; };
93DFD03824521C19001FCBAF /* ScriptEngine.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ScriptEngine.cpp; sourceTree = "<group>"; };
93DFD03924521C19001FCBAF /* ScDisposable.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScDisposable.hpp; sourceTree = "<group>"; };
93DFD03A24521C19001FCBAF /* ScEntity.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScEntity.hpp; sourceTree = "<group>"; };
93DFD03B24521C19001FCBAF /* Duktape.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Duktape.hpp; sourceTree = "<group>"; };
93DFD03C24521C19001FCBAF /* ScConsole.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScConsole.hpp; sourceTree = "<group>"; };
93DFD03D24521C19001FCBAF /* ScPark.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScPark.hpp; sourceTree = "<group>"; };
93DFD03E24521C19001FCBAF /* ScContext.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScContext.hpp; sourceTree = "<group>"; };
93DFD03F24521C19001FCBAF /* Plugin.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Plugin.cpp; sourceTree = "<group>"; };
93DFD04024521C19001FCBAF /* ScRide.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScRide.hpp; sourceTree = "<group>"; };
93DFD04124521C19001FCBAF /* ScDate.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScDate.hpp; sourceTree = "<group>"; };
93DFD04224521C19001FCBAF /* ScMap.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ScMap.hpp; sourceTree = "<group>"; };
93DFD04324521C19001FCBAF /* ScriptEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScriptEngine.h; sourceTree = "<group>"; };
93F60048213DD7DC00EEB83E /* TerrainSurfaceObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TerrainSurfaceObject.h; sourceTree = "<group>"; };
93F60049213DD7DC00EEB83E /* TerrainSurfaceObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TerrainSurfaceObject.cpp; sourceTree = "<group>"; };
93F6004A213DD7DC00EEB83E /* TerrainEdgeObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TerrainEdgeObject.cpp; sourceTree = "<group>"; };
@ -1277,6 +1356,9 @@
93F76EFC20BFF77A00D4512C /* Paint.Banner.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Paint.Banner.cpp; sourceTree = "<group>"; };
93F76EFD20BFF77A00D4512C /* Paint.TileElement.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Paint.TileElement.cpp; sourceTree = "<group>"; };
93F76EFE20BFF77A00D4512C /* Paint.Entrance.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Paint.Entrance.cpp; sourceTree = "<group>"; };
93FC08FD2418F3ED00CA3054 /* duktape.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = duktape.h; sourceTree = "<group>"; };
93FC08FE2418F3ED00CA3054 /* duk_config.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = duk_config.h; sourceTree = "<group>"; };
93FC09012418F3F500CA3054 /* libduktape.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libduktape.dylib; sourceTree = "<group>"; };
C61ADB1E1FB6A0A60024F2EF /* TopToolbar.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TopToolbar.cpp; sourceTree = "<group>"; };
C61ADB201FB7DC060024F2EF /* Scenery.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Scenery.cpp; sourceTree = "<group>"; };
C61ADB221FBBCB8A0024F2EF /* GameBottomToolbar.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GameBottomToolbar.cpp; sourceTree = "<group>"; };
@ -1792,6 +1874,7 @@
D41B73EF1C2101890080A7B9 /* libcurl.tbd in Frameworks */,
D41B741D1C210A7A0080A7B9 /* libiconv.tbd in Frameworks */,
D45A38BC1CF3006400659A24 /* libcrypto.dylib in Frameworks */,
93FC09022418F3F500CA3054 /* libduktape.dylib in Frameworks */,
933F32EC24183CBB008376CE /* libicudata.dylib in Frameworks */,
D45A38BE1CF3006400659A24 /* libjansson.dylib in Frameworks */,
933F32EA24183CBB008376CE /* libicuuc.dylib in Frameworks */,
@ -1825,12 +1908,56 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
4C04D69E2056AA5C00F82EBA /* thirdparty */ = {
4C25594D244A326100CE7E45 /* scripting */ = {
isa = PBXGroup;
children = (
4C04D69F2056AA9600F82EBA /* linenoise.hpp */,
4C25594F244A328A00CE7E45 /* CustomMenu.cpp */,
4C255953244A328A00CE7E45 /* CustomMenu.h */,
4C255957244A328B00CE7E45 /* CustomWindow.cpp */,
4C25594E244A328A00CE7E45 /* CustomWindow.h */,
4C255950244A328A00CE7E45 /* ScUi.hpp */,
4C255955244A328A00CE7E45 /* ScViewport.hpp */,
4C255951244A328A00CE7E45 /* ScWidget.hpp */,
4C255952244A328A00CE7E45 /* ScWindow.hpp */,
4C255954244A328A00CE7E45 /* UiExtensions.cpp */,
4C255956244A328B00CE7E45 /* UiExtensions.h */,
);
path = thirdparty;
name = scripting;
sourceTree = "<group>";
};
4C25595B244A32A800CE7E45 /* thirdparty */ = {
isa = PBXGroup;
children = (
4C25595E244A32EA00CE7E45 /* dukglue */,
4C25595C244A32E400CE7E45 /* filesystem.hpp */,
4C25595D244A32E400CE7E45 /* linenoise.hpp */,
);
name = thirdparty;
path = openrct2;
sourceTree = "<group>";
};
4C25595E244A32EA00CE7E45 /* dukglue */ = {
isa = PBXGroup;
children = (
4C255964244A330700CE7E45 /* detail_class_proto.h */,
4C255969244A330800CE7E45 /* detail_constructor.h */,
4C255967244A330700CE7E45 /* detail_function.h */,
4C25596D244A330800CE7E45 /* detail_method.h */,
4C255962244A330700CE7E45 /* detail_primitive_types.h */,
4C25596B244A330800CE7E45 /* detail_refs.h */,
4C25595F244A330700CE7E45 /* detail_stack.h */,
4C255965244A330700CE7E45 /* detail_traits.h */,
4C25596F244A330800CE7E45 /* detail_typeinfo.h */,
4C255968244A330700CE7E45 /* detail_types.h */,
4C25596E244A330800CE7E45 /* dukexception.h */,
4C255963244A330700CE7E45 /* dukglue.h */,
4C255961244A330700CE7E45 /* dukvalue.h */,
4C25596A244A330800CE7E45 /* public_util.h */,
4C255960244A330700CE7E45 /* register_class.h */,
4C255966244A330700CE7E45 /* register_function.h */,
4C25596C244A330800CE7E45 /* register_property.h */,
);
name = dukglue;
sourceTree = "<group>";
};
933CBDB820CB1B3F00134678 /* title */ = {
@ -2100,6 +2227,32 @@
path = config;
sourceTree = "<group>";
};
93DFD03024521C19001FCBAF /* scripting */ = {
isa = PBXGroup;
children = (
93DFD03B24521C19001FCBAF /* Duktape.hpp */,
93DFD03524521C19001FCBAF /* HookEngine.cpp */,
93DFD03324521C19001FCBAF /* HookEngine.h */,
93DFD03F24521C19001FCBAF /* Plugin.cpp */,
93DFD03124521C19001FCBAF /* Plugin.h */,
93DFD03724521C19001FCBAF /* ScConfiguration.hpp */,
93DFD03C24521C19001FCBAF /* ScConsole.hpp */,
93DFD03E24521C19001FCBAF /* ScContext.hpp */,
93DFD04124521C19001FCBAF /* ScDate.hpp */,
93DFD03924521C19001FCBAF /* ScDisposable.hpp */,
93DFD03A24521C19001FCBAF /* ScEntity.hpp */,
93DFD04224521C19001FCBAF /* ScMap.hpp */,
93DFD03424521C19001FCBAF /* ScNetwork.hpp */,
93DFD03224521C19001FCBAF /* ScObject.hpp */,
93DFD03D24521C19001FCBAF /* ScPark.hpp */,
93DFD04024521C19001FCBAF /* ScRide.hpp */,
93DFD03824521C19001FCBAF /* ScriptEngine.cpp */,
93DFD04324521C19001FCBAF /* ScriptEngine.h */,
93DFD03624521C19001FCBAF /* ScTile.hpp */,
);
path = scripting;
sourceTree = "<group>";
};
C6352B871F477032006CCEE3 /* actions */ = {
isa = PBXGroup;
children = (
@ -2111,6 +2264,7 @@
932A211722D73CF900C57EDB /* BannerSetStyleAction.hpp */,
932A211622D73CF900C57EDB /* ClearAction.hpp */,
932A20DE22D73CF000C57EDB /* ClimateSetAction.hpp */,
4C255971244A342900CE7E45 /* CustomAction.hpp */,
932A20F722D73CF300C57EDB /* FootpathPlaceAction.hpp */,
932A20DB22D73CF000C57EDB /* FootpathPlaceFromTrackAction.hpp */,
932A20F322D73CF300C57EDB /* FootpathRemoveAction.hpp */,
@ -2225,6 +2379,7 @@
D41B72431C21015A0080A7B9 /* Sources */ = {
isa = PBXGroup;
children = (
4C25595B244A32A800CE7E45 /* thirdparty */,
F76C85801EC4E82600FA49E2 /* openrct2-ui */,
F76C857C1EC4E80E00FA49E2 /* openrct2-cli */,
F76C83551EC4E7CC00FA49E2 /* libopenrct2 */,
@ -2471,6 +2626,8 @@
D45A39161CF3007A00659A24 /* SDL2 */,
D45A39521CF3007A00659A24 /* speex */,
9350B44320B46E0800897BC5 /* unicode */,
93FC08FE2418F3ED00CA3054 /* duk_config.h */,
93FC08FD2418F3ED00CA3054 /* duktape.h */,
D45A38C41CF3007A00659A24 /* jansson_config.h */,
D45A38C51CF3007A00659A24 /* jansson.h */,
C6E96E331E0408A80076A04F /* zip.h */,
@ -2483,6 +2640,7 @@
isa = PBXGroup;
children = (
D45A38B31CF3006400659A24 /* libcrypto.dylib */,
93FC09012418F3F500CA3054 /* libduktape.dylib */,
D45A38B41CF3006400659A24 /* libfreetype.dylib */,
933F32E924183CBB008376CE /* libicudata.dylib */,
933F32E824183CBB008376CE /* libicuuc.dylib */,
@ -2527,7 +2685,7 @@
F76C846C1EC4E7CC00FA49E2 /* rct12 */,
F76C84831EC4E7CC00FA49E2 /* ride */,
F76C84F31EC4E7CD00FA49E2 /* scenario */,
4C04D69E2056AA5C00F82EBA /* thirdparty */,
93DFD03024521C19001FCBAF /* scripting */,
F76C84FB1EC4E7CD00FA49E2 /* title */,
F76C85041EC4E7CD00FA49E2 /* ui */,
F76C85061EC4E7CD00FA49E2 /* util */,
@ -2647,6 +2805,8 @@
F76C83811EC4E7CC00FA49E2 /* FileScanner.cpp */,
F76C83821EC4E7CC00FA49E2 /* FileScanner.h */,
F76C83831EC4E7CC00FA49E2 /* FileStream.hpp */,
93DFD02D24521BA0001FCBAF /* FileWatcher.cpp */,
93DFD02C24521B9F001FCBAF /* FileWatcher.h */,
F76C83841EC4E7CC00FA49E2 /* Guard.cpp */,
F76C83851EC4E7CC00FA49E2 /* Guard.hpp */,
4C8A6FF223EB5326001A8255 /* Http.cURL.cpp */,
@ -3295,6 +3455,7 @@
F76C858D1EC4E82600FA49E2 /* drawing */,
F7CB86451EEDA1200030C877 /* input */,
C68313CF1FDB4F4C006DB3D8 /* interface */,
4C25594D244A326100CE7E45 /* scripting */,
933CBDB820CB1B3F00134678 /* title */,
F7CB86401EEDA0E20030C877 /* windows */,
F76C858A1EC4E82600FA49E2 /* CursorData.cpp */,
@ -3486,34 +3647,53 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
93DFD05624521C1A001FCBAF /* ScriptEngine.h in Headers */,
2ADE2F3122441905002598AF /* DiscordService.h in Headers */,
C67B28172002D67A00109C93 /* Viewport.h in Headers */,
939A359C20C12FC800630B3F /* Paint.Sprite.h in Headers */,
93DFD05524521C1A001FCBAF /* ScMap.hpp in Headers */,
9308DA04209908090079EE96 /* TileElement.h in Headers */,
C67B28152002D67A00109C93 /* Widget.h in Headers */,
C6352B851F477022006CCEE3 /* DataSerialiserTraits.h in Headers */,
93DFD05124521C1A001FCBAF /* ScContext.hpp in Headers */,
939A359F20C12FDE00630B3F /* Paint.Surface.h in Headers */,
C67B28192002D7F200109C93 /* Window_internal.h in Headers */,
93DFD05024521C1A001FCBAF /* ScPark.hpp in Headers */,
93DFD02E24521BA0001FCBAF /* FileWatcher.h in Headers */,
2ADE2F28224418B2002598AF /* DataSerialiserTag.h in Headers */,
93DFD04C24521C1A001FCBAF /* ScDisposable.hpp in Headers */,
2ADE2F2E224418E7002598AF /* ConversionTables.h in Headers */,
933F2CBB20935668001B33FD /* LocalisationService.h in Headers */,
C6352B861F477022006CCEE3 /* Endianness.h in Headers */,
2ADE2F2C224418B2002598AF /* FileIndex.hpp in Headers */,
93DFD04A24521C1A001FCBAF /* ScConfiguration.hpp in Headers */,
93CBA4CC20A7504500867D56 /* ImageImporter.h in Headers */,
2ADE2F29224418B2002598AF /* Numerics.hpp in Headers */,
93DFD04924521C1A001FCBAF /* ScTile.hpp in Headers */,
93DFD04524521C1A001FCBAF /* ScObject.hpp in Headers */,
2ADE2F382244198B002598AF /* SpriteBase.h in Headers */,
C62D838B1FD36D6F008C04F1 /* EditorObjectSelectionSession.h in Headers */,
2ADE2F27224418B2002598AF /* Random.hpp in Headers */,
9344BEF920C1E6180047D165 /* Crypt.h in Headers */,
939A35A220C12FFD00630B3F /* InteractiveConsole.h in Headers */,
93CBA4C320A7502E00867D56 /* Imaging.h in Headers */,
93DFD04D24521C1A001FCBAF /* ScEntity.hpp in Headers */,
93DFD04E24521C1A001FCBAF /* Duktape.hpp in Headers */,
2ADE2F2B224418B2002598AF /* JobPool.hpp in Headers */,
2ADE2F3622441960002598AF /* RideTypes.h in Headers */,
93DFD05324521C1A001FCBAF /* ScRide.hpp in Headers */,
93DFD05424521C1A001FCBAF /* ScDate.hpp in Headers */,
93FC08FF2418F3ED00CA3054 /* duktape.h in Headers */,
93DFD04F24521C1A001FCBAF /* ScConsole.hpp in Headers */,
9308DA05209908090079EE96 /* Surface.h in Headers */,
93DE9753209C3C1000FB1CC8 /* GameState.h in Headers */,
2ADE2F2A224418B2002598AF /* Meta.hpp in Headers */,
93DFD04624521C1A001FCBAF /* HookEngine.h in Headers */,
93FC09002418F3ED00CA3054 /* duk_config.h in Headers */,
C6352B841F477022006CCEE3 /* DataSerialiser.h in Headers */,
939A35A020C12FDE00630B3F /* Paint.TileElement.h in Headers */,
93DFD04724521C1A001FCBAF /* ScNetwork.hpp in Headers */,
93DFD04424521C1A001FCBAF /* Plugin.h in Headers */,
C67B28162002D67A00109C93 /* Window.h in Headers */,
2ADE2F342244191E002598AF /* VirtualFloor.h in Headers */,
);
@ -3690,7 +3870,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "version=\"22\"\nzipname=\"openrct2-libs-v22-x64-macos-dylibs.zip\"\nliburl=\"https://github.com/OpenRCT2/Dependencies/releases/download/v$version/$zipname\"\n\n[[ ! -d \"${SRCROOT}/libxc\" || ! -e \"${SRCROOT}/libversion\" || $(head -n 1 \"${SRCROOT}/libversion\") != $version ]]\noutdated=$?\n\nif [[ $outdated -eq 0 ]]; then\nif [[ -d \"${SRCROOT}/libxc\" ]]; then rm -r \"${SRCROOT}/libxc\"; fi\nmkdir \"${SRCROOT}/libxc\"\n\ncurl -L -o \"${SRCROOT}/libxc/$zipname\" \"$liburl\"\nunzip -uaq -d \"${SRCROOT}/libxc\" \"${SRCROOT}/libxc/$zipname\"\nrm \"${SRCROOT}/libxc/$zipname\"\n\necho $version > \"${SRCROOT}/libversion\"\nfi\n";
shellScript = "version=\"23\"\nzipname=\"openrct2-libs-v23-x64-macos-dylibs.zip\"\nliburl=\"https://github.com/OpenRCT2/Dependencies/releases/download/v$version/$zipname\"\n\n[[ ! -d \"${SRCROOT}/libxc\" || ! -e \"${SRCROOT}/libversion\" || $(head -n 1 \"${SRCROOT}/libversion\") != $version ]]\noutdated=$?\n\nif [[ $outdated -eq 0 ]]; then\nif [[ -d \"${SRCROOT}/libxc\" ]]; then rm -r \"${SRCROOT}/libxc\"; fi\nmkdir \"${SRCROOT}/libxc\"\n\ncurl -L -o \"${SRCROOT}/libxc/$zipname\" \"$liburl\"\nunzip -uaq -d \"${SRCROOT}/libxc\" \"${SRCROOT}/libxc/$zipname\"\nrm \"${SRCROOT}/libxc/$zipname\"\n\necho $version > \"${SRCROOT}/libversion\"\nfi\n";
};
D42C09D21C254F4E00309751 /* Build g2.dat */ = {
isa = PBXShellScriptBuildPhase;
@ -3826,6 +4006,7 @@
9346F9DB208A191900C77D91 /* GuestPathfinding.cpp in Sources */,
C654DF361F69C0430040F43D /* Player.cpp in Sources */,
933F2CB720935653001B33FD /* LocalisationService.cpp in Sources */,
4C255958244A328B00CE7E45 /* CustomMenu.cpp in Sources */,
F76C88791EC5324E00FA49E2 /* AudioContext.cpp in Sources */,
C666EE7A1F37ACB10061AA04 /* Themes.cpp in Sources */,
C666EE7F1F37ACB10061AA04 /* Viewport.cpp in Sources */,
@ -3890,6 +4071,7 @@
C666ED761F33DBB20061AA04 /* ShortcutKeyChange.cpp in Sources */,
304FE95023A2996600470197 /* SceneryScatter.cpp in Sources */,
C685E51C1F8907850090598F /* Map.cpp in Sources */,
4C255959244A328B00CE7E45 /* UiExtensions.cpp in Sources */,
F7CB864A1EEDA1330030C877 /* KeyboardShortcuts.cpp in Sources */,
01C6F0C922FD51FC0057E2F7 /* T6Importer.cpp in Sources */,
4CC5258223A19C2900D4366D /* TrackDesignAction.cpp in Sources */,
@ -3921,6 +4103,7 @@
C654DF301F69C0430040F43D /* Finances.cpp in Sources */,
9308DA01209908090079EE96 /* Surface.cpp in Sources */,
933CBDBF20CB1BCA00134678 /* Window.cpp in Sources */,
4C25595A244A328B00CE7E45 /* CustomWindow.cpp in Sources */,
C68878C320289B710084B384 /* DrawRectShader.cpp in Sources */,
C666EE751F37ACB10061AA04 /* NewsOptions.cpp in Sources */,
C654DF311F69C0430040F43D /* GuestList.cpp in Sources */,
@ -4079,6 +4262,7 @@
F76C864D1EC4E88300FA49E2 /* NetworkGroup.cpp in Sources */,
F76C864F1EC4E88300FA49E2 /* NetworkKey.cpp in Sources */,
C688789620289B140084B384 /* Viewport.cpp in Sources */,
93DFD05224521C1A001FCBAF /* Plugin.cpp in Sources */,
C68878A520289B2A0084B384 /* Award.cpp in Sources */,
F76C86511EC4E88300FA49E2 /* NetworkPacket.cpp in Sources */,
F76C86531EC4E88300FA49E2 /* NetworkPlayer.cpp in Sources */,
@ -4095,6 +4279,7 @@
F76C86601EC4E88300FA49E2 /* BannerObject.cpp in Sources */,
C688792A20289B9B0084B384 /* Lift.cpp in Sources */,
F76C86621EC4E88300FA49E2 /* EntranceObject.cpp in Sources */,
93DFD04824521C1A001FCBAF /* HookEngine.cpp in Sources */,
C688792820289B9B0084B384 /* Twist.cpp in Sources */,
C688792D20289B9B0084B384 /* SuspendedMonorail.cpp in Sources */,
C688788620289ADE0084B384 /* TTF.cpp in Sources */,
@ -4106,6 +4291,7 @@
C688793420289B9B0084B384 /* WaterCoaster.cpp in Sources */,
F76C86681EC4E88300FA49E2 /* ImageTable.cpp in Sources */,
C68878E620289B9B0084B384 /* Platform.Linux.cpp in Sources */,
93DFD04B24521C1A001FCBAF /* ScriptEngine.cpp in Sources */,
C688785B20289A0A0084B384 /* Duck.cpp in Sources */,
F76C866A1EC4E88300FA49E2 /* LargeSceneryObject.cpp in Sources */,
C688788E20289AE70084B384 /* SSE41Drawing.cpp in Sources */,
@ -4192,6 +4378,7 @@
C688790A20289B9B0084B384 /* WoodenRollerCoaster.cpp in Sources */,
C688787220289A780084B384 /* MusicList.cpp in Sources */,
93F76F0220BFF77B00D4512C /* Paint.Surface.cpp in Sources */,
93DFD02F24521BA0001FCBAF /* FileWatcher.cpp in Sources */,
F76C871C1EC4E88400FA49E2 /* TrackDesignRepository.cpp in Sources */,
C68878FA20289B9B0084B384 /* LoopingRollerCoaster.cpp in Sources */,
C68878A720289B2A0084B384 /* Marketing.cpp in Sources */,
@ -4432,6 +4619,7 @@
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
__ENABLE_LIGHTFX__,
ENABLE_SCRIPTING,
);
GCC_WARN_64_TO_32_BIT_CONVERSION = NO;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
@ -4444,6 +4632,7 @@
"$(SRCROOT)/libxc/include/openssl",
"$(SRCROOT)/libxc/include/freetype2",
"$(SRCROOT)/src/",
"$(SRCROOT)/src/thirdparty/",
);
INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
@ -4454,6 +4643,7 @@
"$(inherited)",
"$(PROJECT_DIR)/libxc/lib",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = io.openrct2.OpenRCT2;
PRODUCT_NAME = "$(TARGET_NAME)";
};
@ -4475,6 +4665,7 @@
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
__ENABLE_LIGHTFX__,
ENABLE_SCRIPTING,
);
GCC_WARN_64_TO_32_BIT_CONVERSION = NO;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
@ -4487,6 +4678,7 @@
"$(SRCROOT)/libxc/include/openssl",
"$(SRCROOT)/libxc/include/freetype2",
"$(SRCROOT)/src/",
"$(SRCROOT)/src/thirdparty/",
);
INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
@ -4497,6 +4689,7 @@
"$(inherited)",
"$(PROJECT_DIR)/libxc/lib",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = io.openrct2.OpenRCT2;
PRODUCT_NAME = "$(TARGET_NAME)";
};
@ -4519,6 +4712,7 @@
"DEBUG=1",
"$(inherited)",
__ENABLE_LIGHTFX__,
ENABLE_SCRIPTING,
);
GCC_WARN_64_TO_32_BIT_CONVERSION = NO;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
@ -4530,12 +4724,14 @@
"$(SRCROOT)/libxc/include/libpng16",
"$(SRCROOT)/libxc/include/openssl",
"$(SRCROOT)/libxc/include/freetype2",
"$(SRCROOT)/src/",
"$(SRCROOT)/src/thirdparty/",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/libxc/lib",
);
MACOSX_DEPLOYMENT_TARGET = 10.8;
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRIVATE_HEADERS_FOLDER_PATH = "$(CONTENTS_FOLDER_PATH)/PrivateHeaders";
PRODUCT_NAME = "$(TARGET_NAME)";
PUBLIC_HEADERS_FOLDER_PATH = "$(CONTENTS_FOLDER_PATH)/Headers";
@ -4559,6 +4755,7 @@
OPENGL_NO_LINK,
"OPENRCT2_BUILD_INFO_HEADER=\"\\\"$(DERIVED_FILE_DIR)/gitversion.h\\\"\"",
__ENABLE_LIGHTFX__,
ENABLE_SCRIPTING,
);
GCC_WARN_64_TO_32_BIT_CONVERSION = NO;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
@ -4570,12 +4767,14 @@
"$(SRCROOT)/libxc/include/libpng16",
"$(SRCROOT)/libxc/include/openssl",
"$(SRCROOT)/libxc/include/freetype2",
"$(SRCROOT)/src/",
"$(SRCROOT)/src/thirdparty/",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/libxc/lib",
);
MACOSX_DEPLOYMENT_TARGET = 10.8;
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRIVATE_HEADERS_FOLDER_PATH = "$(CONTENTS_FOLDER_PATH)/PrivateHeaders";
PRODUCT_NAME = "$(TARGET_NAME)";
PUBLIC_HEADERS_FOLDER_PATH = "$(CONTENTS_FOLDER_PATH)/Headers";
@ -4597,6 +4796,7 @@
"DEBUG=1",
"$(inherited)",
__ENABLE_LIGHTFX__,
ENABLE_SCRIPTING,
);
GCC_WARN_64_TO_32_BIT_CONVERSION = NO;
HEADER_SEARCH_PATHS = (
@ -4605,12 +4805,13 @@
"$(SRCROOT)/libxc/include/libpng16",
"$(SRCROOT)/libxc/include/openssl",
"$(SRCROOT)/src/",
"$(SRCROOT)/src/thirdparty/",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/libxc/lib",
);
MACOSX_DEPLOYMENT_TARGET = 10.8;
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
@ -4630,6 +4831,7 @@
OPENGL_NO_LINK,
"OPENRCT2_BUILD_INFO_HEADER=\"\\\"$(DERIVED_FILE_DIR)/gitversion.h\\\"\"",
__ENABLE_LIGHTFX__,
ENABLE_SCRIPTING,
);
GCC_WARN_64_TO_32_BIT_CONVERSION = NO;
HEADER_SEARCH_PATHS = (
@ -4638,12 +4840,13 @@
"$(SRCROOT)/libxc/include/libpng16",
"$(SRCROOT)/libxc/include/openssl",
"$(SRCROOT)/src/",
"$(SRCROOT)/src/thirdparty/",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/libxc/lib",
);
MACOSX_DEPLOYMENT_TARGET = 10.8;
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;

2
debian/control vendored
View File

@ -4,7 +4,7 @@ Section: misc
Priority: optional
Standards-Version: 3.9.2
Multi-Arch: same
Build-Depends: debhelper (>= 9), cmake (>= 3.8), libsdl2-dev, g++ (>= 4:7), pkg-config, libjansson4 (>= 2.5), libjansson-dev (>= 2.3), libspeex-dev, libspeexdsp-dev, libcurl4-openssl-dev, libcrypto++-dev, libfontconfig1-dev, libfreetype6-dev, libpng-dev, libssl-dev, libzip-dev (>= 1.0.0), libicu-dev (>= 59.0)
Build-Depends: debhelper (>= 9), cmake (>= 3.8), duktape-dev, libsdl2-dev, g++ (>= 4:7), pkg-config, libjansson4 (>= 2.5), libjansson-dev (>= 2.3), libspeex-dev, libspeexdsp-dev, libcurl4-openssl-dev, libcrypto++-dev, libfontconfig1-dev, libfreetype6-dev, libpng-dev, libssl-dev, libzip-dev (>= 1.0.0), libicu-dev (>= 59.0)
Package: openrct2
Architecture: any

View File

@ -4,6 +4,7 @@
- Feature: [#10572] Cheat to allow building at invalid heights.
- Feature: [#11231] Change shortcut window list order to be more intuitive, and split it into logical sections.
- Feature: [#11306] Path additions are now kept when replacing the path.
- Feature: [#11320] Support for custom JavaScript plugins.
- Change: [#11209] Warn when user is running OpenRCT2 through Wine.
- Change: [#11358] Switch copy and paste button positions in tile inspector.
- Change: [#11449] Remove complete circuit requirement from Air Powered Vertical Coaster (for RCT1 parity).

746
distribution/openrct2.d.ts vendored Normal file
View File

@ -0,0 +1,746 @@
/*****************************************************************************
* 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.
*****************************************************************************/
// OpenRCT2 Scripting API definition file
// To enable IntelliSense for your scripts in Visual Studio or Visual Studio Code,
// add the following line to the top of your script and change the path appropriately.
//
// /// <reference path="/path/to/openrct2.d.ts" />
//
export type PluginType = "local" | "remote";
declare global {
/**
* Global context for accessing all other APIs.
*/
/** APIs for interacting with the stdout console. */
var console: Console;
/** Core APIs for plugins. */
var context: Context;
/** APIs for getting or setting the in-game date. */
var date: GameDate;
/** APIs for manipulating the map. */
var map: GameMap;
/** APIs for managing the server or interacting with the server or clients. */
var network: Network;
/** APIs for the park and management of it. */
var park: Park;
/**
* APIs for controlling the user interface.
* These will only be available to servers and clients that are not running headless mode.
* Plugin writers should check if ui is available using `typeof ui !== 'undefined'`.
*/
var ui: Ui;
/**
* Registers the plugin. This only only be called once.
* @param metadata Information about the plugin and the entry point.
*/
function registerPlugin(metadata: PluginMetadata): void;
/**
* Represents a JavaScript object that can or should be disposed when no longer needed.
*/
interface IDisposable {
dispose(): void;
}
/**
* A coordinate within the game.
* Each in-game tile is a size of 32x32.
*/
interface Coord2 {
x: number;
y: number;
}
/**
* A coordinate within the game.
* 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 {
z: number;
}
/**
* Represents information about the plugin such as type, name, author and version.
* It also includes the entry point.
*/
interface PluginMetadata {
name: string;
version: string;
authors: string | string[];
type: PluginType;
minApiVersion?: number;
main: () => void;
}
/**
* Console APIs
* Currently interact with stdout.
*/
interface Console {
clear(): void;
log(message?: any, ...optionalParams: any[]): void;
}
/**
* Core APIs for storage and subscriptions.
*/
interface Context {
/**
* The user's current configuration.
*/
configuration: Configuration;
/**
* Shared generic storage for all plugins. Data is persistant across instances
* of OpenRCT2 and is stored externally as a single JSON file in the OpenRCT2
* user directory. Internally it is a JavaScript object. Objects and arrays
* are only copied by reference. The external file is only written when using
* 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;
/**
* Gets the loaded object at the given index.
* @param type The object type.
* @param index The index.
*/
getObject(type: ObjectType, index: number): Object;
getObject(type: "ride", index: number): RideObject;
getAllObjects(type: ObjectType): Object[];
getAllObjects(type: "ride"): RideObject[];
/**
* Gets a random integer within the specified range using the game's pseudo-
* random number generator. This is part of the game state and shared across
* all clients, you therefore must be in a context that can mutate the game
* state. Use this to generate random numbers instead of Math.Random during
* game logic routines such as hooks and game actions.
* @param min The minimum value inclusive.
* @param max The maximum value exclusive.
*/
getRandom(min: number, max: number): number;
/**
* 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.
*/
registerAction(
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
* an action without actually executing it.
* @param action The name of the action.
* @param args The action parameters.
* @param callback The function to be called with the result of the action.
*/
queryAction(action: string, args: object, callback: (result: GameActionResult) => void): void;
/**
* Executes a game action. In a network game, this will send a request to the server and wait
* for the server to reply.
* @param action The name of the action.
* @param args The action parameters.
* @param callback The function to be called with the result of the action.
*/
executeAction(action: string, args: object, callback: (result: GameActionResult) => void): void;
/**
* Subscribes to the given hook.
*/
subscribe(hook: HookType, callback: Function): IDisposable;
subscribe(hook: "action.query", callback: (e: GameActionEventArgs) => void): IDisposable;
subscribe(hook: "action.execute", callback: (e: GameActionEventArgs) => void): IDisposable;
subscribe(hook: "interval.tick", callback: () => void): IDisposable;
subscribe(hook: "interval.day", callback: () => void): IDisposable;
subscribe(hook: "network.chat", callback: (e: NetworkChatEventArgs) => void): IDisposable;
subscribe(hook: "network.join", callback: (e: NetworkEventArgs) => void): IDisposable;
subscribe(hook: "network.leave", callback: (e: NetworkEventArgs) => void): IDisposable;
}
interface Configuration {
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;
has(key: string): boolean;
}
type ObjectType =
"ride" |
"small_scenery" |
"large_scenery" |
"wall" |
"banner" |
"footpath" |
"footpath_addition" |
"scenery_group" |
"park_entrance" |
"water" |
"terrain_surface" |
"terrain_edge" |
"station" |
"music";
type HookType =
"interval.tick" | "interval.day" |
"network.chat" | "network.action" | "network.join" | "network.leave";
type ExpenditureType =
"ride_construction" |
"ride_runningcosts" |
"land_purchase" |
"landscaping" |
"park_entrance_tickets" |
"park_ride_tickets" |
"shop_sales" |
"shop_stock" |
"food_drink_sales" |
"food_drink_stock" |
"wages" |
"marketing" |
"research" |
"interest";
interface GameActionEventArgs {
readonly player: number;
readonly type: string;
readonly isClientOnly: boolean;
result: GameActionResult;
}
interface GameActionResult {
error?: string;
errorTitle?: string;
errorMessage?: string;
position?: Coord3;
cost?: number;
expenditureType?: ExpenditureType;
}
interface RideCreateGameActionResult extends GameActionResult {
readonly ride: number;
}
interface RideCreateActionEventArgs extends GameActionEventArgs {
readonly rideType: number;
readonly rideObject: number;
result: RideCreateGameActionResult;
}
interface NetworkEventArgs {
readonly player: number;
}
interface NetworkChatEventArgs extends NetworkEventArgs {
message: string;
}
/**
* APIs for the in-game date.
*/
interface GameDate {
/**
* The total number of ticks that have elapsed since the beginning of the game / scenario. This
* should never reset.
*/
readonly ticksElapsed: number;
/**
* The total number of months that have elapsed. This will equate to 16 on 1st March, Year 2.
* Note: this represents the current date and may be reset by cheats or scripts.
*/
monthsElapsed: number;
/**
* The total number of years that have elapsed. This always equates to (monthsElapsed / 8).
*/
readonly yearsElapsed: number;
/**
* How far through the month we are between 0 and 65536. This is incremented by 4 each tick, so
* every month takes ~6.8 minutes to complete making a year take just under an hour.
*/
monthProgress: number;
/** The day of the month from 1 to 31. */
readonly day: number;
/** The current month of the year from 0 to 7, where 0 is March and 7 is October. */
readonly month: number;
/** The current year starting from 1. */
readonly year: number;
}
/**
* APIs for the map.
*/
interface GameMap {
readonly size: Coord2;
readonly numRides: number;
readonly numEntities: number;
readonly rides: Ride[];
getRide(id: number): Ride;
getTile(x: number, y: number): Tile;
getEntity(id: number): Entity;
getAllEntities(type: EntityType);
}
type TileElementType =
"surface" | "footpath" | "track" | "small_scenery" | "wall" | "entrance" | "large_scenery" | "banner";
interface BaseTileElement {
type: TileElementType;
baseHeight: number;
clearanceHeight: number;
}
interface SurfaceElement extends BaseTileElement {
slope: number;
surfaceStyle: number;
edgeStyle: number;
waterHeight: number;
grassLength: number;
ownership: number;
parkFences: number;
readonly hasOwnership: boolean;
readonly hasConstructionRights: boolean;
}
interface FootpathElement extends BaseTileElement {
footpathType: number;
edgesAndCorners: number;
slopeDirection: number | null;
isBlockedByVehicle: boolean;
isWide: boolean;
isQueue: boolean;
queueBannerDirection: number | null;
ride: number;
station: number;
addition: number | null;
isAdditionBroken: boolean;
}
interface TrackElement extends BaseTileElement {
trackType: number;
sequence: number;
ride: number;
station: number;
hasChainLift: boolean;
}
interface SmallSceneryElement extends BaseTileElement {
object: number;
primaryColour: number;
secondaryColour: number;
}
interface EntranceElement extends BaseTileElement {
object: number;
sequence: number;
ride: number;
station: number;
}
interface WallElement extends BaseTileElement {
object: number;
}
interface LargeSceneryElement extends BaseTileElement {
object: number;
primaryColour: number;
secondaryColour: number;
}
interface BannerElement extends BaseTileElement {
}
interface CorruptElement extends BaseTileElement {
}
type TileElement = SurfaceElement | FootpathElement | TrackElement;
/**
* Represents a tile containing tile elements on the map. This is a fixed handle
* for a given tile position. It can be re-used safely between game ticks.
*/
interface Tile {
/** The x position in tiles. */
readonly x: number;
/** The y position in tiles. */
readonly y: number;
/** Gets an array of all the tile elements on this tile. */
readonly elements: TileElement[];
/** Gets the number of tile elements on this tile. */
readonly numElements: number;
/**
* Gets or sets the raw data for this tile.
* This can provide more control and efficiency for tile manipulation but requires
* knowledge of tile element structures and may change between versions of OpenRCT2.
*/
data: Uint8Array;
/** Gets the tile element at the given index on this tile. */
getElement(index: number): TileElement;
/** Gets the tile element at the given index on this tile. */
getElement<T extends BaseTileElement>(index: number): T;
/** Inserts a new tile element at the given index on this tile. */
insertElement(index: number): TileElement;
/** Removes the tile element at the given index from this tile. */
removeElement(index: number): void;
}
/**
* Represents the definition of a loaded object (.DAT or .json) such a ride type or scenery item.
*/
interface Object {
/**
* The object type.
*/
readonly type: ObjectType;
/**
* The index of the loaded object for the object type.
*/
readonly index: number;
/**
* The unique name identifier of the object, e.g. "BURGB ".
* This may have trailing spaces if the name is shorter than 8 characters.
*/
readonly identifier: string;
/**
* The name in the user's current language.
*/
readonly name: string;
}
/**
* Represents the object definition of a ride or stall.
*/
interface RideObject extends Object {
/**
* The description of the ride / stall in the player's current language.
*/
readonly description: string;
/**
* A text description describing the capacity of the ride in the player's current language.
*/
readonly capacity: string;
}
/**
* Represents a ride or stall within the park.
*/
interface Ride {
/**
* The object metadata for this ride.
*/
readonly object: RideObject;
/**
* The unique ID / index of the ride.
*/
readonly id: number;
/**
* The type of the ride represented as the internal built-in ride type ID.
*/
type: number;
/**
* The generated or custom name of the ride.
*/
name: string;
/**
* The excitement metric of the ride represented as a 2 decimal point fixed integer.
* For example, `652` equates to `6.52`.
*/
excitement: number;
/**
* The intensity metric of the ride represented as a 2 decimal point fixed integer.
* For example, `652` equates to `6.52`.
*/
intensity: number;
/**
* The nausea metric of the ride represented as a 2 decimal point fixed integer.
* For example, `652` equates to `6.52`.
*/
nausea: number;
/**
* The total number of customers the ride has served since it was built.
*/
totalCustomers: number;
}
type EntityType =
"car" | "duck" | "peep";
/**
* Represents an object "entity" on the map that can typically moves and has a sub-tile coordinate.
*/
interface Entity {
/**
* The type of entity, e.g. car, duck, litter, or peep.
*/
readonly type: EntityType;
/**
* The x-coordinate of the entity in game units.
*/
x: number;
/**
* The y-coordinate of the entity in game units.
*/
y: number;
/**
* The z-coordinate of the entity in game units.
*/
z: number;
}
/**
* Represents a guest or staff member.
*/
interface Peep extends Entity {
/**
* Colour of the peep's t-shirt.
*/
tshirt: number;
/**
* Colour of the peep's trousers.
*/
trousers: number;
}
/**
* Network APIs
* Use `network.status` to determine whether the current game is a client, server or in single player mode.
*/
interface Network {
readonly mode: NetworkMode;
readonly groups: number;
readonly players: number;
defaultGroup: number;
getServerInfo(): ServerInfo;
getGroup(index: number): PlayerGroup;
setGroups(groups: PlayerGroup[]): void;
getPlayer(index: number): Player;
kickPlayer(index: number): void;
sendMessage(message: string): void;
sendMessage(message: string, players: number[]): void;
}
type NetworkMode = "none" | "server" | "client";
/**
* Represents a player within a network game.
*/
interface Player {
readonly id: number;
readonly name: string;
group: number;
readonly ping: number;
readonly commandsRan: number;
readonly moneySpent: number;
}
interface PlayerGroup {
readonly id: number;
name: string;
permissions: PermissionType[];
}
interface ServerInfo {
readonly name: string;
readonly description: string;
readonly greeting: string;
readonly providerName: string;
readonly providerEmail: string;
readonly providerWebsite: string;
}
type PermissionType =
"chat" |
"terraform" |
"set_water_level" |
"toggle_pause" |
"create_ride" |
"remove_ride" |
"build_ride" |
"ride_properties" |
"scenery" |
"path" |
"clear_landscape" |
"guest" |
"staff" |
"park_properties" |
"park_funding" |
"kick_player" |
"modify_groups" |
"set_player_group" |
"cheat" |
"toggle_scenery_cluster" |
"passwordless_login" |
"modify_tile" |
"edit_scenario_options";
/**
* Park APIs
*/
/**
* The type of park message, including icon and behaviour.
*/
type ParkMessageType =
"attraction" | "peep_on_attraction" | "peep" | "money" | "blank" | "research" | "guests" | "award" | "chart";
interface ParkMessage {
type: ParkMessageType;
text: string;
}
interface Park {
cash: number;
rating: number;
bankLoan: number;
maxBankLoan: number;
postMessage(message: string): void;
postMessage(message: ParkMessage): void;
}
/**
* User Interface APIs
* These will only be available to servers and clients that are not running headless mode.
* Plugin writers should check if ui is available using `typeof ui !== 'undefined'`.
*/
interface Ui {
readonly width: number;
readonly height: number;
readonly windows: number;
readonly mainViewport: Viewport;
getWindow(id: number): Window;
getWindow(classification: string): Window;
openWindow(desc: WindowDesc): Window;
closeWindows(classification: string, id?: number): void;
closeAllWindows(): void;
registerMenuItem(text: string, callback: () => void): void;
}
/**
* Represents the type of a widget, e.g. button or label.
*/
type WidgetType =
"button" | "checkbox" | "dropdown" | "groupbox" | "label" | "spinner" | "tabview" | "viewport";
interface Widget {
type: WidgetType;
x: number;
y: number;
width: number;
height: number;
name?: string;
isDisabled?: boolean;
}
interface ButtonWidget extends Widget {
image: number;
text: string;
onClick: () => void;
}
interface CheckboxWidget extends Widget {
text: string;
isChecked: boolean;
onChanged: (isChecked: boolean) => void;
}
interface DropdownWidget extends Widget {
items: string[];
selectedIndex: number;
onChanged: (index: number) => void;
}
interface LabelWidget extends Widget {
text: string;
onChanged: (index: number) => void;
}
interface SpinnerWidget extends Widget {
text: string;
onDecrement: () => void;
onIncrement: () => void;
}
interface ViewportWidget extends Widget {
viewport: Viewport
}
interface Window {
classification: number;
number: number;
x: number;
y: number;
width: number;
height: number;
isSticky: boolean;
colours: number[];
title: string;
widgets: Widget[];
close(): void;
bringToFront(): void;
findWidget<T extends Widget>(name: string): T;
}
interface WindowDesc {
classification: string;
x?: number;
y?: number;
width: number;
height: number;
title: string;
id?: number;
minWidth?: number;
minHeight?: number;
widgets?: Widget[];
colours?: number[];
onClose?: () => void;
}
interface Viewport {
left: number;
top: number;
right: number;
bottom: number;
rotation: number;
zoom: number;
visibilityFlags: number;
getCentrePosition(): Coord2;
moveTo(position: Coord2 | Coord3): void;
scrollTo(position: Coord2 | Coord3): void;
}
}

View File

@ -141,6 +141,8 @@ OpenRCT2 is licensed under the GNU General Public Licence version 3.0. For
the complete licence text, see the file 'licence.txt'. This licence applies
to all files in this distribution, except as noted below.
dukglue | MIT licence.
duktape | MIT licence.
Jansson | MIT licence.
libcURL | MIT (or Modified BSD-style) licence.
libicu | Unicode licence.

193
distribution/scripting.md Normal file
View File

@ -0,0 +1,193 @@
# Scripts for OpenRCT2
OpenRCT2 allows custom scripts (also known as plug-ins) to be written and executed in the game providing additional behaviour on top of the vanilla experience. This can range from extra windows providing information about the park to entire new multiplayer game modes.
Each script is a single physical javascript file within the `plugin` directory in your OpenRCT2 user directory. This is usually `C:\Users\YourName\Documents\OpenRCT2` on Windows, or `$XDG_CONFIG_HOME/OpenRCT2` or in its absence `$HOME/.config/OpenRCT2` on Linux. 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:
* Local
* Remote
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.
## Writing Scripts
Scripts are written in ECMAScript 5 compatible JavaScript. OpenRCT2 currently uses the [duktape](https://duktape.org) library to execute scripts. This however does not mean you need to write your plug-in in JavaScript, there are many transpilers that allow you to write in a language of your choice and then compile it to JavaScript allowing it to be executed by OpenRCT2. JavaScript or [TypeScript](https://www.typescriptlang.org) is recommended however, as that will allow you to utilise the type definition file we supply (`openrct2.d.ts`). If you would like to use ECMAScript 6 or later which contain features such as the `let` keyword or classes, then you will need to use a transpiler such as [Babel](https://babeljs.io) or [TypeScript](https://www.typescriptlang.org).
Official references for writing plug-ins are:
* The API: `openrct2.d.ts` distributed with OpenRCT2.
* Our collection of sample scripts: [OpenRCT2/plugin-samples](https://github.com/OpenRCT2/plugin-samples)
* A TypeScript plug-in comprised of multiple sources: [IntelOrca/OpenRCT2-ParkManager](https://github.com/IntelOrca/OpenRCT2-ParkManager)
Start by copying this template script into a new file in your `plugin` directory:
```js
function main() {
console.log("Your plug-in has started!");
}
registerPlugin({
name: 'Your Plugin',
version: '1.0',
authors: ['Your Name'],
type: 'remote',
main: main
});
```
This will log a message to the terminal screen (`stdout`) when you open any park. If you are on Windows, make sure to run `openrct2.com` instead of `openrct2.exe` so you can interact with the `stdin` / `stdout` console. The console is a JavaScript interpreter (REPL), this means you can write and test expressions similar to the console found in web browsers when you press `F12`. When you make changes to your script, you must exit your current game and open it again for the script to reload... unless you use the hot reload feature.
The hot reload feature can be enabled by editing your `config.ini` file and setting `enable_hot_reloading` to `true` under `[plugin]`. When this is enabled, the game will auto-reload the script in real-time whenever you save your JavaScript file. This allows rapid development of plug-ins as you can write code and quickly preview your changes, such as closing and opening a specific custom window on startup. A demonstration of this can be found on YouTube: [OpenRCT2 plugin hot-reload demo](https://www.youtube.com/watch?v=jmjWzEhmDjk)
## Frequently Asked Questions
> Why was JavaScript chosen instead of LUA or Python.
JavaScript is a very mature and flexible language with a large, if not the largest, resource base available. There are arguably more transpilers, tools, and libraries for JavaScript than any other language. That and also it using a familiar C-like syntax and 0-indexed arrays make it more suitable than LUA and Python. Of course if you would still like to use LUA or another language, there will likely be a JavaScript transpiler for it.
Another benefit of using JavaScript is that you get rich editor features such as completion and API documentation by using a TypeScript definition file which works for both TypeScript and JavaScript. [Visual Studio Code](https://code.visualstudio.com) is recommended, as that supports the workflow very well. See the [OpenRCT2 plugin hot-reload demo](https://www.youtube.com/watch?v=jmjWzEhmDjk) video for a demonstration of the editor functionality.
> How safe are scripts?
Scripts are executed in a sandbox container with no direct access to your computer. Scripts can only use the APIs we provide which only consist of interactions to OpenRCT2 and a limited API for storing data. It is technically possible for a script to freeze the game, or fill your disc up with data, but these aren't particularly severe issues and are noticed quite quickly.
The [duktape](https://duktape.org) library is used to execute scripts, it is a very mature library for executing scripts, but no library can promise 100% security. If any security vulnerabilities are found, they will likely be fixed promptly and OpenRCT2 can then be updated to use the new version of `duktape`.
> Is it possible for someone to run a bitcoin miner in my game?
Yes, but the performance would be so dire that it would be a waste of their time. This is probably the least of your worries.
> What are the limits?
Scripts can consist of any behaviour and have a large memory pool available to them. The speed will vary depending on the hardware and system executing them. The scripts are interpreted, so do not expect anywhere close to the performance of native code. In most scenarios this should be satisfactory, but a random map generator, or genetic algorithm for building roller coasters might struggle. Like any language, there will be tricks to optimising JavaScript and the use of the OpenRCT2 APIs.
The APIs for OpenRCT2 try to mimic the internal data structures as close as possible but we can only add so many at a time. The best way to grow the plug-in system is to add APIs on-demand. So if you find an API is missing, please raise an issue for it on GitHub and also feel free to submit a pull request afterwards.
> How do I debug my script?
Debugging has not yet been implemented, but is planned. In the meantime, you can use `console.log` to print useful information for diagnosing problems.
> 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.
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.
Whilst OpenRCT2 tries to prevent desynchronisation from happening, it still requires careful coding to ensure the behaviour is deterministic across all clients. Any attempt to use local specific or non-deterministic data to change the game state will cause a desync. This can be as easy as using `ui.windows`, a variable that can be different for every player.
> What are hooks?
Hooks allow a script to subscribe to certain events that occur in the game. For example if the script was to award cash to the park every day, it would subscribe to the `interval.day` event as follows:
```js
context.subscribe('interval.day', function() {
park.cash += 10000;
});
```
Other hooks include receiving a chat message in multiplayer, or a ride breaking-down.
> What are game actions?
Game actions allow you to define new actions (with a new permission slot) that players can invoke in games. Here is an example flow of a game action such as opening the park:
1. Player executes action (open park) via UI.
2. Game action (open park), query method is called.
3. If query succeeds, send action to server, else show error message.
4. Server receives action and query method is called on server.
5. If query or permission fails, send error message back to player, else:
6. Send action to all player to execute action at tick ###.
7. All players execute action at tick ###.
This sequence of actions ensures that every player execute the action exactly in the same way on the exact same game tick. It also allows the server to validate that the action is allowed and does not fail due to permission or another player executing a conflicting action just before it. This is why there is a noticeable delay when constructing in multiplayer games, as the client has to wait for the server to acknowledge the action and reply with the tick number to execute it on, since we do not yet have any rollback support.
> Can I run code specifically for servers or clients?
Yep, there is an API to get the current network mode.
```js
if (network.mode == "server") {
console.log("This is a server...");
} else if (network.mode == "client") {
console.log("This is a client...");
} else {
console.log("This is single player...");
}
```
> Can I run code only if the user interface is available?
Yes, it is good practice, particularly if writing scripts for servers to check if the game is running in headless mode before attempting to use any UI APIs. The `ui` namespace is not available in headless mode, so make sure you check it, otherwise an error will be thrown.
```js
if (typeof ui !== 'undefined') {
console.log("OpenRCT2 is not running in headless, UI is available!");
ui.registerMenuItem('My window', function() {
// ...
});
}
```
> Can I modify the widgets on built-in windows?
Not yet. A lot of the internal code for these windows relies on the widgets existing, and being in a specific order, as well as hard resizing them to certain positions. Modifying them would likely crash the game until the window system is improved to handle such changes.
> Can servers add additional user interface elements to players?
Yes, remote scripts are uploaded to every client and run as-is. Even if the server is running in headless mode, the script can still contain UI calls for the benefit of the players that are not running in headless mode. Be sure to check if the UI is available first before executing UI calls.
> Can plugins persist data across multiple OpenRCT2 instances?
Yes, use `context.sharedStorage` to read or write data to an external JSON file: `plugin.store.json` in the OpenRCT2 user directory. It is recommended that you namespace all your plugin's data under a common name.
```js
var h = context.sharedStorage.get('IntelOrca.MagicLift.Height');
if (!h) {
context.sharedStorage.set('IntelOrca.MagicLift.Height', 2);
}
```
All plugins have access to the same shared storage.
> Can I use third party JavaScript libraries?
Absolutely, just embed the library in your JavaScript file. There are a number of tools to help you do this.
> Can I share code across multiple scripts?
Yes, and there are two ways this can be done. If you are just sharing helper routines or libraries, the best thing is to embed it in each plug-in. There are a number of tools for JavaScript to help you do this. If you would like two plug-ins to communicate with each other, perhaps to share data, then you can do this by declaring a variable or function in global scope which is available to all plug-ins, or use the shared storage APIs.
> What do I do if there is no API for ...?
No problem, if there is something you need, post a new issue on the issue tracker for OpenRCT2 asking for the API you require: https://github.com/OpenRCT2/OpenRCT2/issues
Adding new APIs is most of time straight forward, so they can be added to new development builds quickly. Or if you are capable, make the change yourself to the OpenRCT2 source stream and create a pull request.
> How do I prevent my script from running on older versions of OpenRCT2 that do not support all the APIs I require?
When registering your plugin, you can specify a minimum required API version like below:
```js
registerPlugin({
name: 'Your Plugin',
version: '1.2', // Your plugin version
authors: ['Your Name'],
type: 'remote',
minApiVersion: 7, // OpenRCT2 plugin API v7 or higher is required to run your plugin
main: main
});
```
When new APIs are introduced, or the behaviour of current APIs change, a new version number will be issued which you can find in the OpenRCT2 source code or changelog.
> Where shall I keep the code for my script?
We recommend [GitHub](https://github.com) (where OpenRCT2 is hosted), or another source control host such as [BitBucket](https://bitbucket.org) or [GitLab](https://gitlab.com). All of them offer private repositories if you want to keep your code private, or public repositories which allow others to easily contribute to your script.
> What licence should I use for my script?
This is up to you. The OpenRCT2 licence does not enforce any licence requirement for content that is loaded into it. MIT is recommended as it is the most permissive. You also have to consider what licence any third party libraries you are using allow.
> Is there a good place to distribute my script to other players?
There is currently no official database for this. For now the recommendation is to upload releases of your script on GitHub alongside your source code (if public). Some people like to make a GitHub repository that just consists of a list of content (scripts in this case) which anyone can add to via pull requests.

View File

@ -170,6 +170,12 @@ Section "!OpenRCT2" Section1
File ..\..\contributors.md
Push "$INSTDIR\contributors.md"
Call unix2dos
File ..\scripting.md
Push "$INSTDIR\scripting.md"
Call unix2dos
File ..\openrct2.d.ts
Push "$INSTDIR\openrct2.d.ts"
Call unix2dos
; Copy executable
File /oname=${OPENRCT2_EXE} ${BINARY_DIR}\${OPENRCT2_EXE}
@ -240,6 +246,8 @@ Section "Uninstall"
Delete "$INSTDIR\licence.txt"
Delete "$INSTDIR\readme.txt"
Delete "$INSTDIR\contributors.md"
Delete "$INSTDIR\scripting.md"
Delete "$INSTDIR\openrct2.d.ts"
Delete "$INSTDIR\${OPENRCT2_EXE}"
Delete "$INSTDIR\${OPENRCT2_COM}"
Delete "$INSTDIR\INSTALL.LOG"

View File

@ -54,6 +54,7 @@
C4555: expression has no effect; expected expression with side-effect
-->
<PreprocessorDefinitions>__AVX2__;__SSE4_1__;OPENGL_NO_LINK;_CRT_SECURE_NO_WARNINGS;_USE_MATH_DEFINES;SDL_MAIN_HANDLED;_WINSOCK_DEPRECATED_NO_WARNINGS;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>ENABLE_SCRIPTING;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary Condition="'$(UseSharedLibs)'!='true'">MultiThreaded</RuntimeLibrary>
<RuntimeLibrary Condition="'$(UseSharedLibs)'=='true'">MultiThreadedDLL</RuntimeLibrary>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
@ -76,7 +77,7 @@
</ClCompile>
<Link>
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
<AdditionalDependencies>benchmarkd.lib;libbreakpadd.lib;libbreakpad_clientd.lib;bz2d.lib;discord-rpc.lib;freetyped.lib;jansson_d.lib;libpng16d.lib;libspeexdsp.lib;SDL2d.lib;zip.lib;zlibd.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>benchmarkd.lib;libbreakpadd.lib;libbreakpad_clientd.lib;bz2d.lib;discord-rpc.lib;duktape.lib;freetyped.lib;jansson_d.lib;libpng16d.lib;libspeexdsp.lib;SDL2d.lib;zip.lib;zlibd.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
@ -94,14 +95,14 @@
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>benchmark.lib;libbreakpad.lib;libbreakpad_client.lib;bz2.lib;discord-rpc.lib;freetype.lib;jansson.lib;libpng16.lib;libspeexdsp.lib;SDL2.lib;zip.lib;zlib.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>benchmark.lib;libbreakpad.lib;libbreakpad_client.lib;bz2.lib;discord-rpc.lib;duktape.lib;freetype.lib;jansson.lib;libpng16.lib;libspeexdsp.lib;SDL2.lib;zip.lib;zlib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup>
<IncludePath>$(SolutionDir)src;$(SolutionDir)lib\$(Platform)\include;$(SolutionDir)lib\$(Platform)\include\SDL2;$(IncludePath)</IncludePath>
<IncludePath>$(SolutionDir)src;$(SolutionDir)src\thirdparty;$(SolutionDir)lib\$(Platform)\include;$(SolutionDir)lib\$(Platform)\include\SDL2;$(IncludePath)</IncludePath>
<LibraryPath Condition="'$(Configuration)'=='Debug'">$(SolutionDir)lib\$(Platform)\debug\lib;$(LibraryPath)</LibraryPath>
<LibraryPath Condition="'$(Configuration)'!='Debug'">$(SolutionDir)lib\$(Platform)\lib;$(LibraryPath)</LibraryPath>
<LinkIncremental />

View File

@ -37,10 +37,10 @@
<!-- 3rd party libraries / dependencies -->
<PropertyGroup>
<DependenciesCheckFile>$(RootDir).dependencies</DependenciesCheckFile>
<LibsUrl Condition="'$(Platform)'=='Win32'">https://github.com/OpenRCT2/Dependencies/releases/download/v20/openrct2-libs-v20-x86-windows-static.zip</LibsUrl>
<LibsSha1 Condition="'$(Platform)'=='Win32'">2963fb45c23f5996638babd1e0bcd9661248ca02</LibsSha1>
<LibsUrl Condition="'$(Platform)'=='x64'">https://github.com/OpenRCT2/Dependencies/releases/download/v20/openrct2-libs-v20-x64-windows-static.zip</LibsUrl>
<LibsSha1 Condition="'$(Platform)'=='x64'">b7019e51bd78254cf19301d5668483a370a59eb0</LibsSha1>
<LibsUrl Condition="'$(Platform)'=='Win32'">https://github.com/OpenRCT2/Dependencies/releases/download/v21/openrct2-libs-v21-x86-windows-static.zip</LibsUrl>
<LibsSha1 Condition="'$(Platform)'=='Win32'">d5bc5b951c15880da0a413ccea27cff9b1b5ae5d</LibsSha1>
<LibsUrl Condition="'$(Platform)'=='x64'">https://github.com/OpenRCT2/Dependencies/releases/download/v21/openrct2-libs-v21-x64-windows-static.zip</LibsUrl>
<LibsSha1 Condition="'$(Platform)'=='x64'">91643da944a3682325f349ebe8dd6f1610e918a6</LibsSha1>
<GtestVersion>2fe3bd994b3189899d93f1d5a881e725e046fdc2</GtestVersion>
<GtestUrl>https://github.com/google/googletest/archive/$(GtestVersion).zip</GtestUrl>
<GtestSha1>058b9df80244c03f1633cb06e9f70471a29ebb8e</GtestSha1>

View File

@ -110,6 +110,7 @@ The program can also be built as a command line program using CMake. This type o
- icu (>= 59.0)
- zlib
- gl (commonly provided by Mesa or GPU vendors; only for UI client, can be disabled)
- duktape (unless scripting is disabled)
- cmake
---

View File

@ -23,7 +23,9 @@ if [[ "$OSTYPE" == "cygwin" || "$OSTYPE" == "msys" ]]; then
../contributors.md \
../licence.txt \
../distribution/changelog.txt \
../distribution/readme.txt
../distribution/readme.txt \
../distribution/scripting.md \
../distribution/openrct2.d.ts
destination=$(cygpath -w $(readlink -f $destination))
printf '\033[0;32m%s\033[0m\n' "${destination} created successfully"

View File

@ -5,4 +5,4 @@ set -e
basedir="$(readlink -f `dirname $0`/..)"
cd $basedir
scripts/run-clang-format.py -r src test --exclude src/openrct2/thirdparty
scripts/run-clang-format.py -r src test --exclude src/thirdparty

View File

@ -183,5 +183,7 @@ target_link_libraries(openrct2-ui openrct2 android stdc++ GLESv1_CM GLESv2 SDL2m
add_executable(openrct2-cli ${OPENRCT2_CLI_SOURCES})
target_link_libraries(openrct2-cli openrct2 android stdc++ GLESv1_CM GLESv2)
target_include_directories(openrct2 SYSTEM PRIVATE "${ORCT2_ROOT}/src/thirdparty")
target_include_directories(openrct2-ui PRIVATE "${ORCT2_ROOT}/src")
target_include_directories(openrct2-ui SYSTEM PRIVATE "${ORCT2_ROOT}/src/thirdparty")
target_include_directories(openrct2-cli PRIVATE "${ORCT2_ROOT}/src")

View File

@ -64,7 +64,8 @@ endif ()
# Includes
target_include_directories(${PROJECT} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/.."
${SPEEX_INCLUDE_DIRS})
target_include_directories(${PROJECT} SYSTEM PRIVATE ${SDL2_INCLUDE_DIRS})
target_include_directories(${PROJECT} SYSTEM PRIVATE ${SDL2_INCLUDE_DIRS}
"${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
# Compiler flags
if (WIN32)

View File

@ -17,6 +17,7 @@
#include "input/KeyboardShortcuts.h"
#include "interface/InGameConsole.h"
#include "interface/Theme.h"
#include "scripting/UiExtensions.h"
#include "title/TitleSequencePlayer.h"
#include <SDL.h>
@ -38,6 +39,7 @@
#include <openrct2/interface/InteractiveConsole.h>
#include <openrct2/localisation/StringIds.h>
#include <openrct2/platform/Platform2.h>
#include <openrct2/scripting/ScriptEngine.h>
#include <openrct2/title/TitleSequencePlayer.h>
#include <openrct2/ui/UiContext.h>
#include <openrct2/ui/WindowManager.h>
@ -46,6 +48,7 @@
using namespace OpenRCT2;
using namespace OpenRCT2::Drawing;
using namespace OpenRCT2::Input;
using namespace OpenRCT2::Scripting;
using namespace OpenRCT2::Ui;
#ifdef __MACOSX__
@ -115,6 +118,14 @@ public:
delete _platformUiContext;
}
void Initialise() override
{
#ifdef ENABLE_SCRIPTING
auto& scriptEngine = GetContext()->GetScriptEngine();
UiScriptExtensions::Extend(scriptEngine);
#endif
}
void Update() override
{
_inGameConsole.Update();

View File

@ -207,6 +207,11 @@ public:
return window_error_open(title, message);
}
rct_window* ShowError(const std::string_view& title, const std::string_view& message) override
{
return window_error_open(title, message);
}
rct_window* OpenIntent(Intent* intent) override
{
switch (intent->GetWindowClass())

View File

@ -361,8 +361,15 @@ static void widget_text_centred(rct_drawpixelinfo* dpi, rct_window* w, rct_widge
else
t = w->windowPos.y + widget->top;
auto stringId = widget->text;
void* formatArgs = gCommonFormatArgs;
if (widget->flags & WIDGET_FLAGS::TEXT_IS_STRING)
{
stringId = STR_STRING;
formatArgs = &widget->string;
}
gfx_draw_string_centred_clipped(
dpi, widget->text, gCommonFormatArgs, colour, (l + r + 1) / 2 - 1, t, widget->right - widget->left - 2);
dpi, stringId, formatArgs, colour, (l + r + 1) / 2 - 1, t, widget->right - widget->left - 2);
}
/**
@ -399,7 +406,14 @@ static void widget_text(rct_drawpixelinfo* dpi, rct_window* w, rct_widgetindex w
else
t = w->windowPos.y + widget->top;
gfx_draw_string_left_clipped(dpi, widget->text, gCommonFormatArgs, colour, l + 1, t, r - l);
auto stringId = widget->text;
void* formatArgs = gCommonFormatArgs;
if (widget->flags & WIDGET_FLAGS::TEXT_IS_STRING)
{
stringId = STR_STRING;
formatArgs = &widget->string;
}
gfx_draw_string_left_clipped(dpi, stringId, formatArgs, colour, l + 1, t, r - l);
}
/**
@ -424,6 +438,26 @@ static void widget_text_inset(rct_drawpixelinfo* dpi, rct_window* w, rct_widgeti
widget_text(dpi, w, widgetIndex);
}
static std::pair<rct_string_id, void*> widget_get_stringid_and_args(const rct_widget* widget)
{
auto stringId = widget->text;
void* formatArgs = gCommonFormatArgs;
if (widget->flags & WIDGET_FLAGS::TEXT_IS_STRING)
{
if (widget->string == nullptr || widget->string[0] == '\0')
{
stringId = STR_NONE;
formatArgs = nullptr;
}
else
{
stringId = STR_STRING;
formatArgs = (void*)&widget->string;
}
}
return std::make_pair(stringId, formatArgs);
}
/**
*
* rct2: 0x006EB535
@ -441,7 +475,8 @@ static void widget_groupbox_draw(rct_drawpixelinfo* dpi, rct_window* w, rct_widg
int32_t textRight = l;
// Text
if (widget->text != STR_NONE)
auto [stringId, formatArgs] = widget_get_stringid_and_args(widget);
if (stringId != STR_NONE)
{
uint8_t colour = w->colours[widget->colour] & 0x7F;
if (widget_is_disabled(w, widgetIndex))
@ -449,7 +484,7 @@ static void widget_groupbox_draw(rct_drawpixelinfo* dpi, rct_window* w, rct_widg
utf8 buffer[512] = { 0 };
uint8_t args[sizeof(uintptr_t)] = { 0 };
format_string(buffer, sizeof(buffer), widget->text, gCommonFormatArgs);
format_string(buffer, sizeof(buffer), stringId, formatArgs);
Formatter(args).Add<utf8*>(buffer);
gfx_draw_string_left(dpi, STR_STRING, args, colour, l, t);
textRight = l + gfx_get_string_width(buffer) + 1;
@ -610,7 +645,8 @@ static void widget_checkbox_draw(rct_drawpixelinfo* dpi, rct_window* w, rct_widg
if (widget->text == STR_NONE)
return;
gfx_draw_string_left_centred(dpi, widget->text, gCommonFormatArgs, colour, l + 14, yMid);
auto [stringId, formatArgs] = widget_get_stringid_and_args(widget);
gfx_draw_string_left_centred(dpi, stringId, formatArgs, colour, l + 14, yMid);
}
/**

View File

@ -0,0 +1,40 @@
/*****************************************************************************
* 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.
*****************************************************************************/
#ifdef ENABLE_SCRIPTING
# include "CustomMenu.h"
namespace OpenRCT2::Scripting
{
std::vector<CustomToolbarMenuItem> CustomMenuItems;
static void RemoveMenuItems(std::shared_ptr<Plugin> owner)
{
auto& menuItems = CustomMenuItems;
for (auto it = menuItems.begin(); it != menuItems.end();)
{
if (it->Owner == owner)
{
it = menuItems.erase(it);
}
else
{
it++;
}
}
}
void InitialiseCustomMenuItems(ScriptEngine& scriptEngine)
{
scriptEngine.SubscribeToPluginStoppedEvent([](std::shared_ptr<Plugin> plugin) -> void { RemoveMenuItems(plugin); });
}
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,50 @@
/*****************************************************************************
* 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 <memory>
# include <openrct2/Context.h>
# include <openrct2/scripting/Duktape.hpp>
# include <openrct2/scripting/ScriptEngine.h>
# include <string>
# include <vector>
namespace OpenRCT2::Scripting
{
class CustomToolbarMenuItem
{
public:
std::shared_ptr<Plugin> Owner;
std::string Text;
DukValue Callback;
CustomToolbarMenuItem(std::shared_ptr<Plugin> owner, const std::string& text, DukValue callback)
: Owner(owner)
, Text(text)
, Callback(callback)
{
}
void Invoke() const
{
auto& scriptEngine = GetContext()->GetScriptEngine();
scriptEngine.ExecutePluginCall(Owner, Callback, {}, false);
}
};
extern std::vector<CustomToolbarMenuItem> CustomMenuItems;
void InitialiseCustomMenuItems(ScriptEngine& scriptEngine);
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,804 @@
/*****************************************************************************
* Copyright (c) 2014-2018 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.
*****************************************************************************/
#ifdef ENABLE_SCRIPTING
# include "../interface/Dropdown.h"
# include "ScUi.hpp"
# include "ScWindow.hpp"
# include <limits>
# include <openrct2-ui/interface/Widget.h>
# include <openrct2-ui/windows/Window.h>
# include <openrct2/drawing/Drawing.h>
# include <openrct2/localisation/Language.h>
# include <openrct2/localisation/Localisation.h>
# include <openrct2/localisation/StringIds.h>
# include <openrct2/scripting/Plugin.h>
# include <openrct2/world/Sprite.h>
# include <optional>
# include <string>
# include <vector>
using namespace OpenRCT2;
using namespace OpenRCT2::Scripting;
namespace OpenRCT2::Ui::Windows
{
enum CUSTOM_WINDOW_WIDX
{
WIDX_BACKGROUND,
WIDX_TITLE,
WIDX_CLOSE,
WIDX_CONTENT_PANEL,
WIDX_CUSTOM_BEGIN,
};
static rct_widget CustomDefaultWidgets[] = {
{ WWT_FRAME, 0, 0, 0, 0, 0, 0xFFFFFFFF, STR_NONE }, // panel / background
{ WWT_CAPTION, 0, 1, 0, 1, 14, STR_STRING, STR_WINDOW_TITLE_TIP }, // title bar
{ WWT_CLOSEBOX, 0, 0, 0, 2, 13, STR_CLOSE_X, STR_CLOSE_WINDOW_TIP }, // close x button
{ WWT_RESIZE, 1, 0, 0, 14, 0, 0xFFFFFFFF, STR_NONE }, // content panel
};
static void window_custom_close(rct_window* w);
static void window_custom_mouseup(rct_window* w, rct_widgetindex widgetIndex);
static void window_custom_mousedown(rct_window* w, rct_widgetindex widgetIndex, rct_widget* widget);
static void window_custom_resize(rct_window* w);
static void window_custom_dropdown(rct_window* w, rct_widgetindex widgetIndex, int32_t dropdownIndex);
static void window_custom_invalidate(rct_window* w);
static void window_custom_paint(rct_window* w, rct_drawpixelinfo* dpi);
static void window_custom_update_viewport(rct_window* w);
static rct_window_event_list window_custom_events = { window_custom_close,
window_custom_mouseup,
window_custom_resize,
window_custom_mousedown,
window_custom_dropdown,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
window_custom_invalidate,
window_custom_paint,
nullptr };
struct CustomWidgetDesc
{
// Properties
std::string Type;
int32_t X{};
int32_t Y{};
int32_t Width{};
int32_t Height{};
std::string Name;
ImageId Image;
std::string Text;
std::vector<std::string> Items;
int32_t SelectedIndex{};
bool IsChecked{};
bool IsDisabled{};
// Event handlers
DukValue OnClick;
DukValue OnChange;
DukValue OnIncrement;
DukValue OnDecrement;
static std::string ProcessString(const DukValue& value)
{
if (value.type() == DukValue::Type::STRING)
return language_convert_string(value.as_string());
return {};
}
static CustomWidgetDesc FromDukValue(DukValue desc)
{
CustomWidgetDesc result;
result.Type = desc["type"].as_string();
result.X = desc["x"].as_int();
result.Y = desc["y"].as_int();
result.Width = desc["width"].as_int();
result.Height = desc["height"].as_int();
if (desc["isDisabled"].type() == DukValue::Type::BOOLEAN)
result.IsDisabled = desc["isDisabled"].as_bool();
if (desc["name"].type() == DukValue::Type::STRING)
{
result.Name = desc["name"].as_string();
}
if (result.Type == "button")
{
auto dukImage = desc["image"];
if (dukImage.type() == DukValue::Type::NUMBER)
{
auto img = dukImage.as_uint();
result.Image = ImageId::FromUInt32(img);
}
else
{
result.Text = ProcessString(desc["text"]);
}
result.OnClick = desc["onClick"];
}
else if (result.Type == "checkbox")
{
result.Text = ProcessString(desc["text"]);
auto dukIsChecked = desc["isChecked"];
if (dukIsChecked.type() == DukValue::Type::BOOLEAN)
{
result.IsChecked = dukIsChecked.as_bool();
}
result.OnChange = desc["onChange"];
}
else if (result.Type == "dropdown")
{
auto dukItems = desc["items"].as_array();
for (const auto& dukItem : dukItems)
{
result.Items.push_back(ProcessString(dukItem));
}
result.SelectedIndex = desc["selectedIndex"].as_int();
result.OnChange = desc["onChange"];
}
else if (result.Type == "groupbox" || result.Type == "label")
{
result.Text = ProcessString(desc["text"]);
}
else if (result.Type == "spinner")
{
result.Text = ProcessString(desc["text"]);
result.OnIncrement = desc["onIncrement"];
result.OnDecrement = desc["onDecrement"];
}
return result;
}
};
struct CustomWindowDesc
{
std::string Classification;
std::optional<int32_t> X;
std::optional<int32_t> Y;
int32_t Width{};
int32_t Height{};
std::optional<int32_t> MinWidth;
std::optional<int32_t> MinHeight;
std::optional<int32_t> MaxWidth;
std::optional<int32_t> MaxHeight;
std::string Title;
std::optional<int32_t> Id;
std::vector<CustomWidgetDesc> Widgets;
std::vector<colour_t> Colours;
// Event handlers
DukValue OnClose;
CustomWindowDesc() = default;
bool IsResizable() const
{
return MinWidth || MinHeight || MaxWidth || MaxHeight;
}
static CustomWindowDesc FromDukValue(DukValue desc)
{
CustomWindowDesc result;
result.Classification = desc["classification"].as_string();
result.X = GetOptionalInt(desc["x"]);
result.Y = GetOptionalInt(desc["y"]);
result.Width = desc["width"].as_int();
result.Height = desc["height"].as_int();
result.MinWidth = GetOptionalInt(desc["minWidth"]);
result.MaxWidth = GetOptionalInt(desc["maxWidth"]);
result.MinHeight = GetOptionalInt(desc["minHeight"]);
result.MaxHeight = GetOptionalInt(desc["maxHeight"]);
result.Title = language_convert_string(desc["title"].as_string());
result.Id = GetOptionalInt(desc["id"]);
if (desc["widgets"].is_array())
{
auto dukWidgets = desc["widgets"].as_array();
std::transform(dukWidgets.begin(), dukWidgets.end(), std::back_inserter(result.Widgets), [](const DukValue& w) {
return CustomWidgetDesc::FromDukValue(w);
});
}
if (desc["colours"].is_array())
{
auto dukColours = desc["colours"].as_array();
std::transform(dukColours.begin(), dukColours.end(), std::back_inserter(result.Colours), [](const DukValue& w) {
colour_t c = COLOUR_BLACK;
if (w.type() == DukValue::Type::NUMBER)
{
c = static_cast<colour_t>(std::clamp<int32_t>(w.as_int(), COLOUR_BLACK, COLOUR_COUNT - 1));
}
return c;
});
}
result.OnClose = desc["onClose"];
return result;
}
static std::optional<int32_t> GetOptionalInt(DukValue input)
{
return input.type() == DukValue::Type::NUMBER ? std::make_optional(input.as_int()) : std::nullopt;
}
};
class CustomWindowInfo
{
public:
std::shared_ptr<Plugin> Owner;
CustomWindowDesc Desc;
std::vector<rct_widget> Widgets;
std::vector<size_t> WidgetIndexMap;
CustomWindowInfo(std::shared_ptr<Plugin> owner, const CustomWindowDesc& desc)
: Owner(owner)
, Desc(desc)
{
}
CustomWindowInfo(const CustomWindowInfo&) = delete;
const CustomWidgetDesc* GetCustomWidgetDesc(size_t widgetIndex) const
{
if (widgetIndex < WidgetIndexMap.size())
{
auto widgetDescIndex = WidgetIndexMap[widgetIndex];
if (widgetDescIndex < Desc.Widgets.size())
{
return &Desc.Widgets[widgetDescIndex];
}
}
return nullptr;
}
CustomWidgetDesc* GetCustomWidgetDesc(size_t widgetIndex)
{
return const_cast<CustomWidgetDesc*>(std::as_const(*this).GetCustomWidgetDesc(widgetIndex));
}
};
static rct_windownumber _nextWindowNumber;
static CustomWindowInfo& GetInfo(rct_window* w);
static rct_windownumber GetNewWindowNumber();
static void RefreshWidgets(rct_window* w);
static void InvokeEventHandler(const std::shared_ptr<Plugin>& owner, const DukValue& dukHandler);
static void InvokeEventHandler(
const std::shared_ptr<Plugin>& owner, const DukValue& dukHandler, const std::vector<DukValue>& args);
rct_window* window_custom_open(std::shared_ptr<Plugin> owner, DukValue dukDesc)
{
auto desc = CustomWindowDesc::FromDukValue(dukDesc);
uint16_t windowFlags = 0;
if (desc.IsResizable())
{
windowFlags |= WF_RESIZABLE;
}
rct_window* window{};
if (desc.X && desc.Y)
{
window = window_create(
{ *desc.X, *desc.Y }, desc.Width, desc.Height, &window_custom_events, WC_CUSTOM, windowFlags);
}
else
{
window = window_create_auto_pos(desc.Width, desc.Height, &window_custom_events, WC_CUSTOM, windowFlags);
}
window->number = GetNewWindowNumber();
window->custom_info = new CustomWindowInfo(owner, desc);
window->enabled_widgets = (1 << WIDX_CLOSE);
// Set window colours
window->colours[0] = COLOUR_GREY;
window->colours[1] = COLOUR_GREY;
window->colours[2] = COLOUR_GREY;
auto numColours = std::min(std::size(window->colours), std::size(desc.Colours));
for (size_t i = 0; i < numColours; i++)
{
window->colours[i] = desc.Colours[i];
}
if (desc.IsResizable())
{
window->min_width = desc.MinWidth.value_or(0);
window->min_height = desc.MinHeight.value_or(0);
window->max_width = desc.MaxWidth.value_or(std::numeric_limits<uint16_t>::max());
window->max_height = desc.MaxHeight.value_or(std::numeric_limits<uint16_t>::max());
}
RefreshWidgets(window);
window_init_scroll_widgets(window);
window_custom_update_viewport(window);
return window;
}
static void window_custom_close(rct_window* w)
{
auto info = static_cast<CustomWindowInfo*>(w->custom_info);
if (info != nullptr)
{
InvokeEventHandler(info->Owner, info->Desc.OnClose);
delete info;
w->custom_info = nullptr;
}
}
static void window_custom_mouseup(rct_window* w, rct_widgetindex widgetIndex)
{
switch (widgetIndex)
{
case WIDX_CLOSE:
window_close(w);
break;
default:
{
const auto& info = GetInfo(w);
const auto widgetDesc = info.GetCustomWidgetDesc(widgetIndex);
if (widgetDesc != nullptr)
{
if (widgetDesc->Type == "button")
{
InvokeEventHandler(info.Owner, widgetDesc->OnClick);
}
else if (widgetDesc->Type == "checkbox")
{
auto& widget = w->widgets[widgetIndex];
widget.flags ^= WIDGET_FLAGS::IS_PRESSED;
bool isChecked = widget.flags & WIDGET_FLAGS::IS_PRESSED;
widget_set_checkbox_value(w, widgetIndex, isChecked);
std::vector<DukValue> args;
auto ctx = widgetDesc->OnChange.context();
duk_push_boolean(ctx, isChecked);
args.push_back(DukValue::take_from_stack(ctx));
InvokeEventHandler(info.Owner, widgetDesc->OnChange, args);
}
}
break;
}
}
}
static void window_custom_resize(rct_window* w)
{
const auto& desc = GetInfo(w).Desc;
if (desc.IsResizable())
{
if (w->width < w->min_width)
{
w->Invalidate();
w->width = w->min_width;
}
if (w->height < w->min_height)
{
w->Invalidate();
w->height = w->min_height;
}
}
window_custom_update_viewport(w);
}
static void window_custom_mousedown(rct_window* w, rct_widgetindex widgetIndex, rct_widget* widget)
{
const auto& info = GetInfo(w);
const auto widgetDesc = info.GetCustomWidgetDesc(widgetIndex);
if (widgetDesc != nullptr)
{
if (widgetDesc->Type == "dropdown")
{
widget--;
auto selectedIndex = widgetDesc->SelectedIndex;
const auto& items = widgetDesc->Items;
const auto numItems = std::min<size_t>(items.size(), DROPDOWN_ITEMS_MAX_SIZE);
for (size_t i = 0; i < numItems; i++)
{
gDropdownItemsFormat[i] = selectedIndex == static_cast<int32_t>(i) ? STR_OPTIONS_DROPDOWN_ITEM_SELECTED
: STR_OPTIONS_DROPDOWN_ITEM;
auto sz = items[i].c_str();
std::memcpy(&gDropdownItemsArgs[i], &sz, sizeof(const char*));
}
window_dropdown_show_text_custom_width(
w->windowPos.x + widget->left, w->windowPos.y + widget->top, widget->bottom - widget->top + 1,
w->colours[widget->colour], 0, DROPDOWN_FLAG_STAY_OPEN, numItems, widget->right - widget->left - 3);
}
else if (widgetDesc->Type == "spinner")
{
if (widget->text == STR_NUMERIC_DOWN)
{
InvokeEventHandler(info.Owner, widgetDesc->OnDecrement);
}
else if (widget->text == STR_NUMERIC_UP)
{
InvokeEventHandler(info.Owner, widgetDesc->OnIncrement);
}
}
}
}
static void window_custom_dropdown(rct_window* w, rct_widgetindex widgetIndex, int32_t dropdownIndex)
{
if (dropdownIndex == -1)
return;
auto& info = GetInfo(w);
auto widgetDesc = info.GetCustomWidgetDesc(widgetIndex);
if (widgetDesc != nullptr)
{
if (widgetDesc->Type == "dropdown")
{
if (dropdownIndex >= 0 && (size_t)dropdownIndex < widgetDesc->Items.size())
{
std::vector<DukValue> args;
auto ctx = widgetDesc->OnChange.context();
duk_push_int(ctx, dropdownIndex);
args.push_back(DukValue::take_from_stack(ctx));
InvokeEventHandler(info.Owner, widgetDesc->OnChange, args);
auto& widget = w->widgets[widgetIndex - 1];
widget.string = const_cast<utf8*>(widgetDesc->Items[dropdownIndex].c_str());
widgetDesc->SelectedIndex = dropdownIndex;
}
}
}
}
static void window_custom_invalidate(rct_window* w)
{
w->widgets[WIDX_BACKGROUND].right = w->width - 1;
w->widgets[WIDX_BACKGROUND].bottom = w->height - 1;
w->widgets[WIDX_TITLE].right = w->width - 2;
w->widgets[WIDX_CLOSE].left = w->width - 13;
w->widgets[WIDX_CLOSE].right = w->width - 3;
w->widgets[WIDX_CONTENT_PANEL].right = w->width - 1;
w->widgets[WIDX_CONTENT_PANEL].bottom = w->height - 1;
const auto& desc = GetInfo(w).Desc;
set_format_arg(0, void*, desc.Title.c_str());
}
static void window_custom_paint(rct_window* w, rct_drawpixelinfo* dpi)
{
window_draw_widgets(w, dpi);
if (w->viewport != nullptr)
{
window_draw_viewport(dpi, w);
}
}
static std::optional<rct_widgetindex> GetViewportWidgetIndex(rct_window* w)
{
rct_widgetindex widgetIndex = 0;
for (auto widget = w->widgets; widget->type != WWT_LAST; widget++)
{
if (widget->type == WWT_VIEWPORT)
{
return widgetIndex;
}
widgetIndex++;
}
return 0;
}
static void window_custom_update_viewport(rct_window* w)
{
auto viewportWidgetIndex = GetViewportWidgetIndex(w);
if (viewportWidgetIndex)
{
auto viewportWidget = &w->widgets[*viewportWidgetIndex];
auto& customInfo = GetInfo(w);
auto widgetInfo = customInfo.GetCustomWidgetDesc(*viewportWidgetIndex);
if (widgetInfo != nullptr)
{
if (w->viewport == nullptr)
{
auto left = w->windowPos.x + viewportWidget->left + 1;
auto top = w->windowPos.y + viewportWidget->top + 1;
auto width = (viewportWidget->right - viewportWidget->left) - 1;
auto height = (viewportWidget->bottom - viewportWidget->top) - 1;
auto mapX = 0;
auto mapY = 0;
auto mapZ = 0;
viewport_create(
w, { left, top }, width, height, 0, { mapX, mapY, mapZ }, VIEWPORT_FOCUS_TYPE_COORDINATE,
SPRITE_INDEX_NULL);
w->flags |= WF_NO_SCROLLING;
w->Invalidate();
}
}
}
}
static CustomWindowInfo& GetInfo(rct_window* w)
{
return *(static_cast<CustomWindowInfo*>(w->custom_info));
}
static rct_windownumber GetNewWindowNumber()
{
auto result = _nextWindowNumber++;
while (window_find_by_number(WC_CUSTOM, result) != nullptr)
{
result++;
}
return result;
}
static void CreateWidget(std::vector<rct_widget>& widgetList, const CustomWidgetDesc& desc)
{
rct_widget widget{};
widget.colour = 1;
widget.left = desc.X;
widget.top = desc.Y;
widget.right = desc.X + desc.Width;
widget.bottom = desc.Y + desc.Height;
widget.content = std::numeric_limits<uint32_t>::max();
widget.tooltip = STR_NONE;
widget.flags = WIDGET_FLAGS::IS_ENABLED;
if (desc.IsDisabled)
widget.flags |= WIDGET_FLAGS::IS_DISABLED;
if (desc.Type == "button")
{
if (desc.Image.HasValue())
{
widget.type = WWT_FLATBTN;
widget.image = desc.Image.ToUInt32();
}
else
{
widget.type = WWT_BUTTON;
widget.string = const_cast<utf8*>(desc.Text.c_str());
widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING;
}
widgetList.push_back(widget);
}
else if (desc.Type == "checkbox")
{
widget.type = WWT_CHECKBOX;
widget.string = const_cast<utf8*>(desc.Text.c_str());
widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING;
if (desc.IsChecked)
{
widget.flags |= WIDGET_FLAGS::IS_PRESSED;
}
widgetList.push_back(widget);
}
else if (desc.Type == "dropdown")
{
widget.type = WWT_DROPDOWN;
if (desc.SelectedIndex >= 0 && (size_t)desc.SelectedIndex < desc.Items.size())
{
widget.string = const_cast<utf8*>(desc.Items[desc.SelectedIndex].c_str());
}
widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING;
widgetList.push_back(widget);
// Add the dropdown button
widget = {};
widget.type = WWT_BUTTON;
widget.colour = 1;
widget.left = desc.X + desc.Width - 11;
widget.right = desc.X + desc.Width - 1;
widget.top = desc.Y + 1;
widget.bottom = desc.Y + desc.Height - 1;
widget.text = STR_DROPDOWN_GLYPH;
widget.tooltip = STR_NONE;
widget.flags |= WIDGET_FLAGS::IS_ENABLED;
widgetList.push_back(widget);
}
else if (desc.Type == "groupbox")
{
widget.type = WWT_GROUPBOX;
widget.string = const_cast<utf8*>(desc.Text.c_str());
widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING;
widgetList.push_back(widget);
}
else if (desc.Type == "label")
{
widget.type = WWT_LABEL;
widget.string = const_cast<utf8*>(desc.Text.c_str());
widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING;
widgetList.push_back(widget);
}
else if (desc.Type == "spinner")
{
widget.type = WWT_SPINNER;
widget.string = const_cast<utf8*>(desc.Text.c_str());
widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING;
widgetList.push_back(widget);
// Add the decrement button
widget = {};
widget.type = WWT_BUTTON;
widget.colour = 1;
widget.left = desc.X + desc.Width - 25;
widget.right = widget.left + 12;
widget.top = desc.Y + 1;
widget.bottom = desc.Y + desc.Height - 1;
widget.text = STR_NUMERIC_DOWN;
widget.tooltip = STR_NONE;
widget.flags |= WIDGET_FLAGS::IS_ENABLED;
widgetList.push_back(widget);
// Add the increment button
widget.left = desc.X + desc.Width - 12;
widget.right = widget.left + 11;
widget.text = STR_NUMERIC_UP;
widgetList.push_back(widget);
}
else if (desc.Type == "viewport")
{
widget.type = WWT_VIEWPORT;
widget.text = STR_NONE;
widgetList.push_back(widget);
}
}
static void RefreshWidgets(rct_window* w)
{
auto& info = GetInfo(w);
auto& widgets = info.Widgets;
widgets.clear();
// Add default widgets (window shim)
widgets.insert(widgets.begin(), std::begin(CustomDefaultWidgets), std::end(CustomDefaultWidgets));
for (size_t i = 0; i < widgets.size(); i++)
{
info.WidgetIndexMap.push_back(std::numeric_limits<size_t>::max());
}
// Add custom widgets
for (size_t widgetDescIndex = 0; widgetDescIndex < info.Desc.Widgets.size(); widgetDescIndex++)
{
const auto& widgetDesc = info.Desc.Widgets[widgetDescIndex];
auto preWidgetSize = widgets.size();
CreateWidget(widgets, widgetDesc);
auto numWidetsAdded = widgets.size() - preWidgetSize;
for (size_t i = 0; i < numWidetsAdded; i++)
{
info.WidgetIndexMap.push_back(widgetDescIndex);
}
}
widgets.push_back({ WIDGETS_END });
w->widgets = widgets.data();
// Enable widgets
w->enabled_widgets = 1ULL << WIDX_CLOSE;
for (size_t i = 0; i < std::min<size_t>(widgets.size(), 64); i++)
{
auto mask = 1ULL << i;
auto flags = widgets[i].flags;
if (flags & WIDGET_FLAGS::IS_ENABLED)
{
w->enabled_widgets |= mask;
}
if (flags & WIDGET_FLAGS::IS_PRESSED)
{
w->pressed_widgets |= mask;
}
if (flags & WIDGET_FLAGS::IS_DISABLED)
{
w->disabled_widgets |= mask;
}
}
}
static void InvokeEventHandler(const std::shared_ptr<Plugin>& owner, const DukValue& dukHandler)
{
std::vector<DukValue> args;
InvokeEventHandler(owner, dukHandler, args);
}
static void InvokeEventHandler(
const std::shared_ptr<Plugin>& owner, const DukValue& dukHandler, const std::vector<DukValue>& args)
{
auto& scriptEngine = GetContext()->GetScriptEngine();
scriptEngine.ExecutePluginCall(owner, dukHandler, args, false);
}
std::string GetWindowTitle(rct_window* w)
{
if (w->custom_info != nullptr)
{
auto& customInfo = GetInfo(w);
return customInfo.Desc.Title;
}
return {};
}
void UpdateWindowTitle(rct_window* w, const std::string_view& value)
{
if (w->custom_info != nullptr)
{
auto& customInfo = GetInfo(w);
customInfo.Desc.Title = value;
}
}
void UpdateWidgetText(rct_window* w, rct_widgetindex widgetIndex, const std::string_view& value)
{
if (w->custom_info != nullptr)
{
auto& customInfo = GetInfo(w);
auto customWidgetInfo = customInfo.GetCustomWidgetDesc(widgetIndex);
if (customWidgetInfo != nullptr)
{
customWidgetInfo->Text = language_convert_string(value);
w->widgets[widgetIndex].string = customWidgetInfo->Text.data();
widget_invalidate(w, widgetIndex);
}
}
}
rct_window* FindCustomWindowByClassification(const std::string_view& classification)
{
for (auto w : g_window_list)
{
if (w->classification == WC_CUSTOM)
{
auto& customInfo = GetInfo(w.get());
if (customInfo.Desc.Classification == classification)
{
return w.get();
}
}
}
return nullptr;
}
std::optional<rct_widgetindex> FindWidgetIndexByName(rct_window* w, const std::string_view& name)
{
if (w->custom_info != nullptr)
{
auto& customInfo = GetInfo(w);
for (size_t i = 0; i < customInfo.Widgets.size(); i++)
{
auto customWidgetInfo = customInfo.GetCustomWidgetDesc(i);
if (customWidgetInfo != nullptr)
{
if (customWidgetInfo->Name == name)
{
return i;
}
}
}
}
return {};
}
} // namespace OpenRCT2::Ui::Windows
#endif

View File

@ -0,0 +1,28 @@
/*****************************************************************************
* 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 "../interface/Window.h"
# include <optional>
# include <string_view>
namespace OpenRCT2::Ui::Windows
{
std::string GetWindowTitle(rct_window* w);
void UpdateWindowTitle(rct_window* w, const std::string_view& value);
void UpdateWidgetText(rct_window* w, rct_widgetindex widget, const std::string_view& string_view);
rct_window* FindCustomWindowByClassification(const std::string_view& classification);
std::optional<rct_widgetindex> FindWidgetIndexByName(rct_window* w, const std::string_view& name);
} // namespace OpenRCT2::Ui::Windows
#endif

View File

@ -0,0 +1,160 @@
/*****************************************************************************
* 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 "CustomMenu.h"
# include "ScViewport.hpp"
# include "ScWindow.hpp"
# include <memory>
# include <openrct2/Context.h>
# include <openrct2/common.h>
# include <openrct2/scripting/Duktape.hpp>
# include <openrct2/scripting/ScriptEngine.h>
# include <string>
namespace OpenRCT2::Scripting
{
class Plugin;
}
namespace OpenRCT2::Ui::Windows
{
rct_window* window_custom_open(std::shared_ptr<OpenRCT2::Scripting::Plugin> owner, DukValue dukDesc);
}
namespace OpenRCT2::Scripting
{
class ScUi
{
private:
ScriptEngine& _scriptEngine;
public:
ScUi(ScriptEngine& scriptEngine)
: _scriptEngine(scriptEngine)
{
}
private:
int32_t width_get() const
{
return context_get_width();
}
int32_t height_get() const
{
return context_get_height();
}
int32_t windows_get() const
{
return static_cast<int32_t>(g_window_list.size());
}
std::shared_ptr<ScViewport> mainViewport_get() const
{
return std::make_shared<ScViewport>(WC_MAIN_WINDOW);
}
std::shared_ptr<ScWindow> openWindow(DukValue desc)
{
using namespace OpenRCT2::Ui::Windows;
auto& execInfo = _scriptEngine.GetExecInfo();
auto owner = execInfo.GetCurrentPlugin();
std::shared_ptr<ScWindow> scWindow = nullptr;
auto w = window_custom_open(owner, desc);
if (w != nullptr)
{
scWindow = std::make_shared<ScWindow>(w);
}
return scWindow;
}
void closeWindows(std::string classification, DukValue id)
{
auto cls = GetClassification(classification);
if (cls != WC_NULL)
{
if (id.type() == DukValue::Type::NUMBER)
{
window_close_by_number(cls, id.as_int());
}
else
{
window_close_by_class(cls);
}
}
}
void closeAllWindows()
{
window_close_all();
}
std::shared_ptr<ScWindow> getWindow(DukValue a) const
{
if (a.type() == DukValue::Type::NUMBER)
{
auto index = a.as_int();
auto i = 0;
for (auto w : g_window_list)
{
if (i == index)
{
return std::make_shared<ScWindow>(w.get());
}
i++;
}
}
else if (a.type() == DukValue::Type::STRING)
{
auto classification = a.as_string();
auto w = FindCustomWindowByClassification(classification);
if (w != nullptr)
{
return std::make_shared<ScWindow>(w);
}
}
return {};
}
void registerMenuItem(std::string text, DukValue callback)
{
auto& execInfo = _scriptEngine.GetExecInfo();
auto owner = execInfo.GetCurrentPlugin();
CustomMenuItems.emplace_back(owner, text, callback);
}
public:
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScUi::height_get, nullptr, "height");
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_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::registerMenuItem, "registerMenuItem");
}
private:
rct_windowclass GetClassification(const std::string& key) const
{
return WC_NULL;
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,307 @@
/*****************************************************************************
* 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 "../interface/Window.h"
# include <memory>
# include <openrct2/Context.h>
# include <openrct2/common.h>
# include <openrct2/interface/Viewport.h>
# include <openrct2/scripting/Duktape.hpp>
# include <openrct2/scripting/ScriptEngine.h>
# include <openrct2/world/Map.h>
namespace OpenRCT2::Scripting
{
class ScViewport
{
private:
rct_windowclass _class{};
rct_windownumber _number{};
public:
ScViewport(rct_windowclass c, rct_windownumber n = 0)
: _class(c)
, _number(n)
{
}
private:
int32_t left_get() const
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
return viewport->viewPos.x;
}
return 0;
}
void left_set(int32_t value)
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
SetViewLeftTop(value, viewport->viewPos.y);
}
}
int32_t top_get() const
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
return viewport->viewPos.y;
}
return 0;
}
void top_set(int32_t value)
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
SetViewLeftTop(viewport->viewPos.x, value);
}
}
int32_t right_get() const
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
return viewport->viewPos.x + viewport->view_width;
}
return 0;
}
void right_set(int32_t value)
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
SetViewLeftTop(value - viewport->view_width, viewport->viewPos.y);
}
}
int32_t bottom_get() const
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
return viewport->viewPos.y + viewport->view_height;
}
return 0;
}
void bottom_set(int32_t value)
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
SetViewLeftTop(viewport->viewPos.x, value - viewport->view_height);
}
}
int32_t rotation_get() const
{
return get_current_rotation();
}
void rotation_set(int32_t value)
{
if (value >= 0 && value < 4)
{
auto w = GetWindow();
if (w != nullptr)
{
while (get_current_rotation() != value)
{
window_rotate_camera(w, 1);
}
}
}
}
int32_t zoom_get() const
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
return static_cast<int8_t>(viewport->zoom);
}
return 0;
}
void zoom_set(int32_t value)
{
auto w = GetWindow();
if (w != nullptr)
{
window_zoom_set(w, value, false);
}
}
uint32_t visibilityFlags_get() const
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
return viewport->flags;
}
return 0;
}
void visibilityFlags_set(uint32_t value)
{
auto w = GetWindow();
if (w != nullptr)
{
auto viewport = w->viewport;
if (viewport != nullptr)
{
if (viewport->flags != value)
{
viewport->flags = value;
w->Invalidate();
}
}
}
}
DukValue getCentrePosition() const
{
auto viewport = GetViewport();
if (viewport != nullptr)
{
auto centreX = viewport->viewPos.x + (viewport->view_width / 2);
auto centreY = viewport->viewPos.y + (viewport->view_height / 2);
auto coords = viewport_coord_to_map_coord(centreX, centreY, 24);
auto ctx = GetContext()->GetScriptEngine().GetContext();
auto obj = duk_push_object(ctx);
duk_push_number(ctx, coords.x);
duk_put_prop_string(ctx, obj, "x");
duk_push_number(ctx, coords.y);
duk_put_prop_string(ctx, obj, "y");
return DukValue::take_from_stack(ctx);
}
return {};
}
void moveTo(DukValue position)
{
auto w = GetWindow();
if (w != nullptr)
{
auto viewport = w->viewport;
if (viewport != nullptr)
{
auto coords = GetCoordsFromObject(position);
if (coords)
{
auto screenCoords = translate_3d_to_2d_with_z(get_current_rotation(), *coords);
auto left = screenCoords.x - (viewport->view_width / 2);
auto top = screenCoords.y - (viewport->view_height / 2);
SetViewLeftTop(left, top);
}
}
}
}
void scrollTo(DukValue position)
{
auto w = GetWindow();
if (w != nullptr)
{
auto coords = GetCoordsFromObject(position);
if (coords)
{
window_scroll_to_location(w, coords->x, coords->y, coords->z);
}
}
}
public:
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScViewport::left_get, &ScViewport::left_set, "left");
dukglue_register_property(ctx, &ScViewport::top_get, &ScViewport::top_set, "top");
dukglue_register_property(ctx, &ScViewport::right_get, &ScViewport::right_set, "right");
dukglue_register_property(ctx, &ScViewport::bottom_get, &ScViewport::bottom_set, "bottom");
dukglue_register_property(ctx, &ScViewport::rotation_get, &ScViewport::rotation_set, "rotation");
dukglue_register_property(ctx, &ScViewport::zoom_get, &ScViewport::zoom_set, "zoom");
dukglue_register_property(
ctx, &ScViewport::visibilityFlags_get, &ScViewport::visibilityFlags_set, "visibilityFlags");
dukglue_register_method(ctx, &ScViewport::getCentrePosition, "getCentrePosition");
dukglue_register_method(ctx, &ScViewport::moveTo, "moveTo");
dukglue_register_method(ctx, &ScViewport::scrollTo, "scrollTo");
}
private:
rct_window* GetWindow() const
{
if (_class == WC_MAIN_WINDOW)
return window_get_main();
else
return window_find_by_number(_class, _number);
}
rct_viewport* GetViewport() const
{
auto w = GetWindow();
if (w != nullptr)
{
return w->viewport;
}
return nullptr;
}
void SetViewLeftTop(int32_t left, int32_t top)
{
auto w = GetWindow();
if (w != nullptr)
{
auto viewport = w->viewport;
if (viewport != nullptr)
{
viewport->viewPos.x = left;
viewport->viewPos.y = top;
viewport->flags &= ~WF_SCROLLING_TO_LOCATION;
w->savedViewPos.x = viewport->viewPos.x;
w->savedViewPos.y = viewport->viewPos.y;
}
}
}
std::optional<CoordsXYZ> GetCoordsFromObject(DukValue position) const
{
if (position.type() == DukValue::Type::OBJECT)
{
auto dukX = position["x"];
auto dukY = position["y"];
auto dukZ = position["z"];
if (dukX.type() == DukValue::Type::NUMBER && dukY.type() == DukValue::Type::NUMBER)
{
auto x = dukX.as_int();
auto y = dukY.as_int();
if (dukZ.type() == DukValue::Type::NUMBER)
{
return CoordsXYZ(x, y, dukZ.as_int());
}
else
{
auto z = tile_element_height(CoordsXY(x, y));
return CoordsXYZ(x, y, z);
}
}
}
return {};
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,319 @@
/*****************************************************************************
* 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 "../interface/Widget.h"
# include "../interface/Window.h"
# include "CustomWindow.h"
# include "ScViewport.hpp"
# include <memory>
# include <openrct2/Context.h>
# include <openrct2/common.h>
# include <openrct2/scripting/Duktape.hpp>
# include <openrct2/scripting/ScriptEngine.h>
namespace OpenRCT2::Scripting
{
class ScWidget
{
private:
rct_windowclass _class{};
rct_windownumber _number{};
rct_widgetindex _widgetIndex{};
public:
ScWidget(rct_windowclass c, rct_windownumber n, rct_widgetindex widgetIndex)
: _class(c)
, _number(n)
, _widgetIndex(widgetIndex)
{
}
private:
std::string type_get() const
{
auto widget = GetWidget();
if (widget != nullptr)
{
switch (widget->type)
{
case WWT_FRAME:
return "frame";
case WWT_RESIZE:
return "resize";
case WWT_IMGBTN:
case WWT_COLOURBTN:
case WWT_TRNBTN:
case WWT_FLATBTN:
case WWT_BUTTON:
case WWT_CLOSEBOX:
return "button";
case WWT_TAB:
return "tab";
case WWT_LABEL_CENTRED:
case WWT_LABEL:
return "label";
case WWT_TABLE_HEADER:
return "table_header";
case WWT_SPINNER:
return "spinner";
case WWT_DROPDOWN:
return "dropdown";
case WWT_VIEWPORT:
return "viewport";
case WWT_GROUPBOX:
return "groupbox";
case WWT_CAPTION:
return "caption";
case WWT_SCROLL:
return "scroll_view";
case WWT_CHECKBOX:
return "checkbox";
case WWT_TEXT_BOX:
return "textbox";
}
}
return "unknown";
}
int32_t x_get() const
{
auto widget = GetWidget();
if (widget != nullptr)
{
return widget->left;
}
return 0;
}
void x_set(int32_t value)
{
auto widget = GetWidget();
if (widget != nullptr)
{
Invalidate();
widget->left = value;
Invalidate();
}
}
int32_t y_get() const
{
auto widget = GetWidget();
if (widget != nullptr)
{
return widget->top;
}
return 0;
}
void y_set(int32_t value)
{
auto widget = GetWidget();
if (widget != nullptr)
{
Invalidate();
widget->top = value;
Invalidate();
}
}
int32_t width_get() const
{
auto widget = GetWidget();
if (widget != nullptr)
{
return widget->right - widget->left;
}
return 0;
}
void width_set(int32_t value)
{
auto widget = GetWidget();
if (widget != nullptr)
{
Invalidate();
widget->right = widget->left + value;
Invalidate();
}
}
int32_t height_get() const
{
auto widget = GetWidget();
if (widget != nullptr)
{
return widget->bottom - widget->top;
}
return 0;
}
void height_set(int32_t value)
{
auto widget = GetWidget();
if (widget != nullptr)
{
Invalidate();
widget->bottom = widget->top + value;
Invalidate();
}
}
bool isDisabled_get() const
{
auto w = GetWindow();
if (w != nullptr)
{
return widget_is_disabled(w, _widgetIndex);
}
return false;
}
void isDisabled_set(bool value)
{
auto w = GetWindow();
if (w != nullptr)
{
auto mask = 1ULL << _widgetIndex;
if (value)
w->disabled_widgets |= mask;
else
w->disabled_widgets &= ~mask;
}
}
bool isChecked_get() const
{
auto w = GetWindow();
if (w != nullptr)
{
return widget_is_pressed(w, _widgetIndex);
}
return false;
}
void isChecked_set(bool value)
{
auto w = GetWindow();
if (w != nullptr)
{
widget_set_checkbox_value(w, _widgetIndex, value ? 1 : 0);
}
}
uint32_t image_get() const
{
if (IsCustomWindow())
{
auto widget = GetWidget();
if (widget != nullptr && widget->type == WWT_FLATBTN)
{
return widget->image;
}
}
return 0;
}
void image_set(uint32_t value)
{
auto widget = GetWidget();
if (widget != nullptr && widget->type == WWT_FLATBTN)
{
widget->image = value;
}
}
std::string text_get() const
{
if (IsCustomWindow())
{
auto widget = GetWidget();
if (widget != nullptr && (widget->flags & WIDGET_FLAGS::TEXT_IS_STRING) && widget->string != nullptr)
{
return language_convert_string_to_tokens(widget->string);
}
}
return "";
}
void text_set(std::string value)
{
auto w = GetWindow();
if (w != nullptr && IsCustomWindow())
{
OpenRCT2::Ui::Windows::UpdateWidgetText(w, _widgetIndex, value);
}
}
std::shared_ptr<ScViewport> viewport_get() const
{
auto w = GetWindow();
if (w != nullptr && IsCustomWindow())
{
auto widget = GetWidget();
if (widget != nullptr && widget->type == WWT_VIEWPORT)
{
return std::make_shared<ScViewport>(w->classification, w->number);
}
}
return {};
}
public:
static void Register(duk_context* ctx)
{
// Common
dukglue_register_property(ctx, &ScWidget::type_get, nullptr, "type");
dukglue_register_property(ctx, &ScWidget::x_get, &ScWidget::x_set, "x");
dukglue_register_property(ctx, &ScWidget::y_get, &ScWidget::y_set, "y");
dukglue_register_property(ctx, &ScWidget::width_get, &ScWidget::width_set, "width");
dukglue_register_property(ctx, &ScWidget::height_get, &ScWidget::height_set, "height");
dukglue_register_property(ctx, &ScWidget::isDisabled_get, &ScWidget::isDisabled_set, "isDisabled");
// No so common
dukglue_register_property(ctx, &ScWidget::image_get, &ScWidget::image_set, "image");
dukglue_register_property(ctx, &ScWidget::text_get, &ScWidget::text_set, "text");
dukglue_register_property(ctx, &ScWidget::isChecked_get, &ScWidget::isChecked_set, "isChecked");
dukglue_register_property(ctx, &ScWidget::viewport_get, nullptr, "viewport");
}
private:
rct_window* GetWindow() const
{
if (_class == WC_MAIN_WINDOW)
return window_get_main();
else
return window_find_by_number(_class, _number);
}
rct_widget* GetWidget() const
{
auto w = GetWindow();
if (w != nullptr)
{
return &w->widgets[_widgetIndex];
}
return nullptr;
}
bool IsCustomWindow() const
{
auto w = GetWindow();
if (w != nullptr)
{
return w->classification == WC_CUSTOM;
}
return false;
}
void Invalidate()
{
widget_invalidate_by_number(_class, _number, _widgetIndex);
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,206 @@
/*****************************************************************************
* 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 "ScWidget.hpp"
# include <openrct2/common.h>
# include <openrct2/interface/Window.h>
# include <openrct2/interface/Window_internal.h>
# include <openrct2/localisation/Language.h>
# include <openrct2/scripting/Duktape.hpp>
namespace OpenRCT2::Scripting
{
using namespace OpenRCT2::Ui::Windows;
class ScWindow
{
private:
rct_windowclass _class;
rct_windownumber _number;
public:
ScWindow(rct_window* w)
: ScWindow(w->classification, w->number)
{
}
ScWindow(rct_windowclass c, rct_windownumber n)
: _class(c)
, _number(n)
{
}
int32_t classification_get() const
{
return static_cast<int32_t>(_class);
}
int32_t number_get() const
{
return static_cast<int32_t>(_number);
}
int32_t x_get() const
{
return GetWindow()->windowPos.x;
}
void x_set(int32_t value)
{
auto w = GetWindow();
window_set_position(w, { value, w->windowPos.y });
}
int32_t y_get() const
{
return GetWindow()->windowPos.y;
}
void y_set(int32_t value)
{
auto w = GetWindow();
window_set_position(w, { w->windowPos.x, value });
}
int32_t width_get() const
{
return GetWindow()->width;
}
int32_t height_get() const
{
return GetWindow()->height;
}
bool isSticky_get() const
{
auto flags = GetWindow()->flags;
return (flags & (WF_STICK_TO_BACK | WF_STICK_TO_FRONT)) != 0;
}
std::vector<std::shared_ptr<ScWidget>> widgets_get() const
{
std::vector<std::shared_ptr<ScWidget>> result;
auto w = GetWindow();
if (w != nullptr)
{
rct_widgetindex widgetIndex = 0;
for (auto widget = w->widgets; widget->type != WWT_LAST; widget++)
{
result.push_back(std::make_shared<ScWidget>(_class, _number, widgetIndex));
widgetIndex++;
}
}
return result;
}
std::vector<int32_t> colours_get() const
{
std::vector<int32_t> result;
auto w = GetWindow();
if (w != nullptr)
{
result.reserve(std::size(w->colours));
for (auto c : w->colours)
{
result.push_back(c);
}
}
return result;
}
void colours_set(std::vector<int32_t> colours)
{
auto w = GetWindow();
if (w != nullptr)
{
for (size_t i = 0; i < std::size(w->colours); i++)
{
w->colours[i] = i < colours.size() ? std::clamp<int32_t>(colours[i], COLOUR_BLACK, COLOUR_COUNT - 1)
: COLOUR_BLACK;
}
}
}
std::string title_get() const
{
auto w = GetWindow();
if (w != nullptr && w->classification == WC_CUSTOM)
{
return language_convert_string_to_tokens(GetWindowTitle(w));
}
return {};
}
void title_set(std::string value)
{
auto w = GetWindow();
if (w != nullptr && w->classification == WC_CUSTOM)
{
UpdateWindowTitle(w, language_convert_string(value));
}
}
void close()
{
auto w = GetWindow();
if (w != nullptr)
{
window_close(w);
}
}
std::shared_ptr<ScWidget> findWidget(std::string name) const
{
auto w = GetWindow();
if (w != nullptr)
{
auto widgetIndex = FindWidgetIndexByName(w, name);
if (widgetIndex)
{
return std::make_shared<ScWidget>(_class, _number, *widgetIndex);
}
}
return {};
}
void bringToFront()
{
auto w = GetWindow();
if (w != nullptr)
{
window_bring_to_front(w);
w->flags |= WF_WHITE_BORDER_MASK;
}
}
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScWindow::classification_get, nullptr, "classification");
dukglue_register_property(ctx, &ScWindow::number_get, nullptr, "number");
dukglue_register_property(ctx, &ScWindow::x_get, &ScWindow::x_set, "x");
dukglue_register_property(ctx, &ScWindow::y_get, &ScWindow::y_set, "y");
dukglue_register_property(ctx, &ScWindow::width_get, nullptr, "width");
dukglue_register_property(ctx, &ScWindow::height_get, nullptr, "height");
dukglue_register_property(ctx, &ScWindow::isSticky_get, nullptr, "isSticky");
dukglue_register_property(ctx, &ScWindow::widgets_get, nullptr, "widgets");
dukglue_register_property(ctx, &ScWindow::colours_get, &ScWindow::colours_set, "colours");
dukglue_register_property(ctx, &ScWindow::title_get, &ScWindow::title_set, "title");
dukglue_register_method(ctx, &ScWindow::close, "close");
dukglue_register_method(ctx, &ScWindow::findWidget, "findWidget");
dukglue_register_method(ctx, &ScWindow::bringToFront, "bringToFront");
}
private:
rct_window* GetWindow() const
{
return window_find_by_number(_class, _number);
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,37 @@
/*****************************************************************************
* 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.
*****************************************************************************/
#ifdef ENABLE_SCRIPTING
# include "UiExtensions.h"
# include "CustomMenu.h"
# include "ScUi.hpp"
# include "ScWidget.hpp"
# include "ScWindow.hpp"
# include <openrct2/scripting/ScriptEngine.h>
using namespace OpenRCT2::Scripting;
void UiScriptExtensions::Extend(ScriptEngine& scriptEngine)
{
auto ctx = scriptEngine.GetContext();
dukglue_register_global(ctx, std::make_shared<ScUi>(scriptEngine), "ui");
ScUi::Register(ctx);
ScViewport::Register(ctx);
ScWidget::Register(ctx);
ScWindow::Register(ctx);
InitialiseCustomMenuItems(scriptEngine);
}
#endif

View File

@ -0,0 +1,25 @@
/*****************************************************************************
* 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
namespace OpenRCT2::Scripting
{
class ScriptEngine;
class UiScriptExtensions
{
public:
static void Extend(ScriptEngine& scriptEngine);
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -62,7 +62,7 @@ static rct_window_event_list window_error_events = {
};
// clang-format on
static char _window_error_text[512];
static std::string _window_error_text;
static uint16_t _window_error_num_lines;
/**
@ -74,30 +74,38 @@ static uint16_t _window_error_num_lines;
*/
rct_window* window_error_open(rct_string_id title, rct_string_id message)
{
utf8* dst;
auto titlez = format_string(title, gCommonFormatArgs);
auto messagez = format_string(message, gCommonFormatArgs);
return window_error_open(titlez, messagez);
}
rct_window* window_error_open(const std::string_view& title, const std::string_view& message)
{
int32_t numLines, fontHeight, width, height, maxY;
rct_window* w;
window_close_by_class(WC_ERROR);
dst = _window_error_text;
auto& buffer = _window_error_text;
buffer.clear();
// Format the title
dst = utf8_write_codepoint(dst, FORMAT_BLACK);
if (title != STR_NONE)
{
format_string(dst, 512 - (dst - _window_error_text), title, gCommonFormatArgs);
dst = get_string_end(dst);
char temp[8]{};
utf8_write_codepoint(temp, FORMAT_BLACK);
buffer.append(temp);
}
buffer.append(title);
// Format the message
if (message != STR_NONE)
if (!message.empty())
{
dst = utf8_write_codepoint(dst, FORMAT_NEWLINE);
format_string(dst, 512 - (dst - _window_error_text), message, gCommonFormatArgs);
dst = get_string_end(dst);
char temp[8]{};
utf8_write_codepoint(temp, FORMAT_NEWLINE);
buffer.append(temp);
buffer.append(message);
}
log_verbose("show error, %s", _window_error_text + 1);
log_verbose("show error, %s", buffer.c_str() + 1);
// Don't do unnecessary work in headless. Also saves checking if cursor state is null.
if (gOpenRCT2Headless)
@ -106,15 +114,15 @@ rct_window* window_error_open(rct_string_id title, rct_string_id message)
}
// Check if there is any text to display
if (dst == _window_error_text + 1)
if (buffer.size() <= 1)
return nullptr;
gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM;
width = gfx_get_string_width_new_lined(_window_error_text);
width = std::min(196, width);
width = gfx_get_string_width_new_lined(buffer.data());
width = std::clamp(width, 64, 196);
gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM;
gfx_wrap_string(_window_error_text, width + 1, &numLines, &fontHeight);
gfx_wrap_string(buffer.data(), width + 1, &numLines, &fontHeight);
_window_error_num_lines = numLines;
width = width + 3;
@ -186,5 +194,5 @@ static void window_error_paint(rct_window* w, rct_drawpixelinfo* dpi)
l = w->windowPos.x + (w->width + 1) / 2 - 1;
t = w->windowPos.y + 1;
draw_string_centred_raw(dpi, l, t, _window_error_num_lines, _window_error_text);
draw_string_centred_raw(dpi, l, t, _window_error_num_lines, _window_error_text.data());
}

View File

@ -1824,7 +1824,10 @@ static void window_ride_construction_construct(rct_window* w)
// Used by some functions
if (res->Error != GA_ERROR::OK)
{
gGameCommandErrorText = res->ErrorMessage;
if (auto error = res->ErrorMessage.AsStringId())
gGameCommandErrorText = *error;
else
gGameCommandErrorText = STR_NONE;
_trackPlaceCost = MONEY32_UNDEFINED;
}
else

View File

@ -123,6 +123,7 @@ rct_window* window_title_menu_open()
static void window_title_menu_scenarioselect_callback(const utf8* path)
{
context_load_park_from_file(path);
game_load_scripts();
}
static void window_title_menu_mouseup(rct_window* w, rct_widgetindex widgetIndex)

View File

@ -9,6 +9,7 @@
#include "../UiContext.h"
#include "../interface/InGameConsole.h"
#include "../scripting/CustomMenu.h"
#include <algorithm>
#include <iterator>
@ -305,6 +306,8 @@ static rct_window_event_list window_top_toolbar_events = {
static void top_toolbar_init_view_menu(rct_window* window, rct_widget* widget);
static void top_toolbar_view_menu_dropdown(int16_t dropdownIndex);
static void top_toolbar_init_map_menu(rct_window* window, rct_widget* widget);
static void top_toolbar_map_menu_dropdown(int16_t dropdownIndex);
static void top_toolbar_init_fastforward_menu(rct_window* window, rct_widget* widget);
static void top_toolbar_fastforward_menu_dropdown(int16_t dropdownIndex);
static void top_toolbar_init_rotate_menu(rct_window* window, rct_widget* widget);
@ -521,20 +524,7 @@ static void window_top_toolbar_mousedown(rct_window* w, rct_widgetindex widgetIn
top_toolbar_init_view_menu(w, widget);
break;
case WIDX_MAP:
gDropdownItemsFormat[0] = STR_SHORTCUT_SHOW_MAP;
gDropdownItemsFormat[1] = STR_EXTRA_VIEWPORT;
numItems = 2;
if ((gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && gS6Info.editor_step == EDITOR_STEP_LANDSCAPE_EDITOR)
{
gDropdownItemsFormat[2] = STR_MAPGEN_WINDOW_TITLE;
numItems++;
}
window_dropdown_show_text(
w->windowPos.x + widget->left, w->windowPos.y + widget->top, widget->bottom - widget->top + 1,
w->colours[1] | 0x80, 0, numItems);
gDropdownDefaultIndex = DDIDX_SHOW_MAP;
top_toolbar_init_map_menu(w, widget);
break;
case WIDX_FASTFORWARD:
top_toolbar_init_fastforward_menu(w, widget);
@ -651,18 +641,7 @@ static void window_top_toolbar_dropdown(rct_window* w, rct_widgetindex widgetInd
top_toolbar_view_menu_dropdown(dropdownIndex);
break;
case WIDX_MAP:
switch (dropdownIndex)
{
case 0:
context_open_window(WC_MAP);
break;
case 1:
context_open_window(WC_VIEWPORT);
break;
case 2:
context_open_window(WC_MAPGEN);
break;
}
top_toolbar_map_menu_dropdown(dropdownIndex);
break;
case WIDX_FASTFORWARD:
top_toolbar_fastforward_menu_dropdown(dropdownIndex);
@ -1968,9 +1947,12 @@ static void window_top_toolbar_scenery_tool_down(int16_t x, int16_t y, rct_windo
break;
}
if (res->ErrorMessage == STR_NOT_ENOUGH_CASH_REQUIRES || res->ErrorMessage == STR_CAN_ONLY_BUILD_THIS_ON_WATER)
if (auto message = res->ErrorMessage.AsStringId())
{
break;
if (*message == STR_NOT_ENOUGH_CASH_REQUIRES || *message == STR_CAN_ONLY_BUILD_THIS_ON_WATER)
{
break;
}
}
if (zAttemptRange != 1)
@ -2018,9 +2000,12 @@ static void window_top_toolbar_scenery_tool_down(int16_t x, int16_t y, rct_windo
break;
}
if (res->ErrorMessage == STR_NOT_ENOUGH_CASH_REQUIRES || res->ErrorMessage == STR_CAN_ONLY_BUILD_THIS_ON_WATER)
if (auto message = res->ErrorMessage.AsStringId())
{
break;
if (*message == STR_NOT_ENOUGH_CASH_REQUIRES || *message == STR_CAN_ONLY_BUILD_THIS_ON_WATER)
{
break;
}
}
if (zAttemptRange != 1)
@ -3293,6 +3278,73 @@ static void window_top_toolbar_tool_abort(rct_window* w, rct_widgetindex widgetI
}
}
static void top_toolbar_init_map_menu(rct_window* w, rct_widget* widget)
{
auto i = 0;
gDropdownItemsFormat[i++] = STR_SHORTCUT_SHOW_MAP;
gDropdownItemsFormat[i++] = STR_EXTRA_VIEWPORT;
if ((gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && gS6Info.editor_step == EDITOR_STEP_LANDSCAPE_EDITOR)
{
gDropdownItemsFormat[i++] = STR_MAPGEN_WINDOW_TITLE;
}
#ifdef ENABLE_SCRIPTING
const auto& customMenuItems = OpenRCT2::Scripting::CustomMenuItems;
if (!customMenuItems.empty())
{
gDropdownItemsFormat[i++] = STR_EMPTY;
for (const auto& item : customMenuItems)
{
gDropdownItemsFormat[i] = STR_STRING;
auto sz = item.Text.c_str();
std::memcpy(&gDropdownItemsArgs[i], &sz, sizeof(const char*));
i++;
}
}
#endif
window_dropdown_show_text(
w->windowPos.x + widget->left, w->windowPos.y + widget->top, widget->bottom - widget->top + 1, w->colours[1] | 0x80, 0,
i);
gDropdownDefaultIndex = DDIDX_SHOW_MAP;
}
static void top_toolbar_map_menu_dropdown(int16_t dropdownIndex)
{
int32_t customStartIndex = 3;
if ((gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && gS6Info.editor_step == EDITOR_STEP_LANDSCAPE_EDITOR)
{
customStartIndex++;
}
if (dropdownIndex < customStartIndex)
{
switch (dropdownIndex)
{
case 0:
context_open_window(WC_MAP);
break;
case 1:
context_open_window(WC_VIEWPORT);
break;
case 2:
context_open_window(WC_MAPGEN);
break;
}
}
else
{
#ifdef ENABLE_SCRIPTING
const auto& customMenuItems = OpenRCT2::Scripting::CustomMenuItems;
auto customIndex = static_cast<size_t>(dropdownIndex - customStartIndex);
if (customMenuItems.size() > customIndex)
{
customMenuItems[customIndex].Invoke();
}
#endif
}
}
static void top_toolbar_init_fastforward_menu(rct_window* w, rct_widget* widget)
{
int32_t num_items = 4;

View File

@ -23,11 +23,15 @@
#include <openrct2/ride/TrackDesign.h>
#include <openrct2/ride/TrackDesignRepository.h>
#include <openrct2/sprites.h>
#include <openrct2/ui/UiContext.h>
#include <openrct2/ui/WindowManager.h>
#include <openrct2/windows/Intent.h>
#include <openrct2/world/Park.h>
#include <openrct2/world/Surface.h>
#include <vector>
using namespace OpenRCT2;
constexpr int16_t TRACK_MINI_PREVIEW_WIDTH = 168;
constexpr int16_t TRACK_MINI_PREVIEW_HEIGHT = 78;
constexpr uint16_t TRACK_MINI_PREVIEW_SIZE = TRACK_MINI_PREVIEW_WIDTH * TRACK_MINI_PREVIEW_HEIGHT;
@ -391,8 +395,9 @@ static void window_track_place_tooldown(rct_window* w, rct_widgetindex widgetInd
// Unable to build track
audio_play_sound_at_location(SoundId::Error, trackLoc);
std::copy(res->ErrorMessageArgs.begin(), res->ErrorMessageArgs.end(), gCommonFormatArgs);
context_show_error(res->ErrorTitle, res->ErrorMessage);
auto windowManager = GetContext()->GetUiContext()->GetWindowManager();
windowManager->ShowError(res->GetErrorTitle(), res->GetErrorMessage());
}
/**

View File

@ -13,6 +13,7 @@
#include <openrct2/common.h>
#include <openrct2/ride/Ride.h>
#include <openrct2/windows/tile_inspector.h>
#include <string_view>
using loadsave_callback = void (*)(int32_t result, const utf8* path);
using scenarioselect_callback = void (*)(const utf8* path);
@ -105,6 +106,7 @@ void window_title_command_editor_open(struct TitleSequence* sequence, int32_t co
rct_window* window_scenarioselect_open(scenarioselect_callback callback, bool titleEditor);
rct_window* window_error_open(rct_string_id title, rct_string_id message);
rct_window* window_error_open(const std::string_view& title, const std::string_view& message);
struct TrackDesign;
rct_window* window_loadsave_open(int32_t type, const char* defaultName, loadsave_callback callback, TrackDesign* t6Exporter);
rct_window* window_track_place_open(const struct track_design_file_ref* tdFileRef);

View File

@ -100,6 +100,20 @@ if (NOT DISABLE_GOOGLE_BENCHMARK)
endif ()
endif ()
if (ENABLE_SCRIPTING)
if (MSVC)
find_package(duktape REQUIRED)
else ()
PKG_CHECK_MODULES(DUKTAPE REQUIRED duktape)
target_include_directories(${PROJECT_NAME} PRIVATE ${DUKTAPE_INCLUDE_DIRS})
if (STATIC)
target_link_libraries(${PROJECT_NAME} ${DUKTAPE_STATIC_LIBRARIES})
else ()
target_link_libraries(${PROJECT_NAME} ${DUKTAPE_LIBRARIES})
endif ()
endif ()
endif ()
# Third party libraries
if (MSVC)
find_package(jansson CONFIG REQUIRED)
@ -196,7 +210,8 @@ endif()
target_include_directories(${PROJECT_NAME} PRIVATE ${LIBZIP_INCLUDE_DIRS})
target_include_directories(${PROJECT_NAME} PUBLIC ${JANSSON_INCLUDE_DIRS})
target_include_directories(${PROJECT_NAME} PRIVATE ${PNG_INCLUDE_DIRS}
${ZLIB_INCLUDE_DIRS})
${ZLIB_INCLUDE_DIRS})
include_directories(${PROJECT_NAME} SYSTEM ${CMAKE_CURRENT_LIST_DIR}/../thirdparty)
# To avoid unnecessary rebuilds set the current branch and
# short sha1 only for the two files that use these

View File

@ -57,6 +57,8 @@
#include "ride/TrackDesignRepository.h"
#include "scenario/Scenario.h"
#include "scenario/ScenarioRepository.h"
#include "scripting/HookEngine.h"
#include "scripting/ScriptEngine.h"
#include "title/TitleScreen.h"
#include "title/TitleSequenceManager.h"
#include "ui/UiContext.h"
@ -76,6 +78,7 @@ using namespace OpenRCT2::Audio;
using namespace OpenRCT2::Drawing;
using namespace OpenRCT2::Localisation;
using namespace OpenRCT2::Paint;
using namespace OpenRCT2::Scripting;
using namespace OpenRCT2::Ui;
namespace OpenRCT2
@ -100,6 +103,9 @@ namespace OpenRCT2
std::unique_ptr<DiscordService> _discordService;
#endif
StdInOutConsole _stdInOutConsole;
#ifdef ENABLE_SCRIPTING
ScriptEngine _scriptEngine;
#endif
// Game states
std::unique_ptr<TitleScreen> _titleScreen;
@ -134,6 +140,9 @@ namespace OpenRCT2
, _audioContext(audioContext)
, _uiContext(uiContext)
, _localisationService(std::make_unique<LocalisationService>(env))
#ifdef ENABLE_SCRIPTING
, _scriptEngine(_stdInOutConsole, *env)
#endif
, _painter(std::make_unique<Painter>(uiContext))
{
// Can't have more than one context currently.
@ -176,6 +185,13 @@ namespace OpenRCT2
return _uiContext;
}
#ifdef ENABLE_SCRIPTING
Scripting::ScriptEngine& GetScriptEngine() override
{
return _scriptEngine;
}
#endif
GameState* GetGameState() override
{
return _gameState.get();
@ -454,6 +470,7 @@ namespace OpenRCT2
_gameState->InitAll(150);
_titleScreen = std::make_unique<TitleScreen>(*_gameState);
_uiContext->Initialise();
return true;
}
@ -799,7 +816,11 @@ namespace OpenRCT2
}
network_begin_server(gNetworkStartPort, gNetworkStartAddress);
}
else
#endif // DISABLE_NETWORK
{
game_load_scripts();
}
break;
}
case STARTUP_ACTION_EDIT:
@ -825,11 +846,7 @@ namespace OpenRCT2
}
#endif // DISABLE_NETWORK
// For now, only allow interactive console in headless mode
if (gOpenRCT2Headless)
{
_stdInOutConsole.Start();
}
_stdInOutConsole.Start();
RunGameLoop();
}
@ -1015,6 +1032,9 @@ namespace OpenRCT2
Twitch::Update();
chat_update();
#ifdef ENABLE_SCRIPTING
_scriptEngine.Update();
#endif
_stdInOutConsole.ProcessEvalQueue();
_uiContext->Update();
}
@ -1033,6 +1053,7 @@ namespace OpenRCT2
DIRID::TRACK,
DIRID::LANDSCAPE,
DIRID::HEIGHTMAP,
DIRID::PLUGIN,
DIRID::THEME,
DIRID::SEQUENCE,
DIRID::REPLAY,

View File

@ -85,6 +85,11 @@ namespace OpenRCT2
class LocalisationService;
}
namespace Scripting
{
class ScriptEngine;
}
namespace Ui
{
interface IUiContext;
@ -109,6 +114,9 @@ namespace OpenRCT2
virtual Localisation::LocalisationService& GetLocalisationService() abstract;
virtual IObjectManager& GetObjectManager() abstract;
virtual IObjectRepository& GetObjectRepository() abstract;
#ifdef ENABLE_SCRIPTING
virtual Scripting::ScriptEngine& GetScriptEngine() abstract;
#endif
virtual ITrackDesignRepository* GetTrackDesignRepository() abstract;
virtual IScenarioRepository* GetScenarioRepository() abstract;
virtual IReplayManager* GetReplayManager() abstract;

View File

@ -46,6 +46,7 @@
#include "ride/TrackDesign.h"
#include "ride/Vehicle.h"
#include "scenario/Scenario.h"
#include "scripting/ScriptEngine.h"
#include "title/TitleScreen.h"
#include "ui/UiContext.h"
#include "ui/WindowManager.h"
@ -589,6 +590,20 @@ void game_load_init()
gGameSpeed = 1;
}
void game_load_scripts()
{
#ifdef ENABLE_SCRIPTING
GetContext()->GetScriptEngine().LoadPlugins();
#endif
}
void game_unload_scripts()
{
#ifdef ENABLE_SCRIPTING
GetContext()->GetScriptEngine().UnloadPlugins();
#endif
}
/**
*
* rct2: 0x0069E9A7
@ -801,8 +816,10 @@ static void game_load_or_quit_no_save_prompt_callback(int32_t result, const utf8
{
if (result == MODAL_RESULT_OK)
{
game_unload_scripts();
window_close_by_class(WC_EDITOR_OBJECT_SELECTION);
context_load_park_from_file(path);
game_load_scripts();
}
}
@ -843,10 +860,12 @@ void game_load_or_quit_no_save_prompt()
}
gGameSpeed = 1;
gFirstTimeSaving = true;
game_unload_scripts();
title_load();
break;
}
default:
game_unload_scripts();
openrct2_finish();
break;
}

View File

@ -98,6 +98,7 @@ enum GAME_COMMAND
GAME_COMMAND_REMOVE_FOOTPATH_SCENERY, // GA
GAME_COMMAND_GUEST_SET_FLAGS, // GA
GAME_COMMAND_SET_DATE, // GA
GAME_COMMAND_CUSTOM, // GA
GAME_COMMAND_COUNT,
};
@ -156,6 +157,8 @@ void update_palette_effects();
void game_load_or_quit_no_save_prompt();
void load_from_sv6(const char* path);
void game_load_init();
void game_load_scripts();
void game_unload_scripts();
void pause_toggle();
bool game_is_paused();
bool game_is_not_paused();

View File

@ -26,6 +26,7 @@
#include "peep/Staff.h"
#include "platform/Platform2.h"
#include "scenario/Scenario.h"
#include "scripting/ScriptEngine.h"
#include "title/TitleScreen.h"
#include "title/TitleSequencePlayer.h"
#include "ui/UiContext.h"
@ -38,6 +39,7 @@
#include <algorithm>
using namespace OpenRCT2;
using namespace OpenRCT2::Scripting;
GameState::GameState()
{
@ -267,8 +269,22 @@ void GameState::UpdateLogic()
}
}
#ifdef ENABLE_SCRIPTING
auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine();
hookEngine.Call(HOOK_TYPE::INTERVAL_TICK, true);
auto day = _date.GetDay();
#endif
date_update();
_date = Date(gDateMonthTicks, gDateMonthTicks);
_date = Date(gDateMonthsElapsed, gDateMonthTicks);
#ifdef ENABLE_SCRIPTING
if (day != _date.GetDay())
{
hookEngine.Call(HOOK_TYPE::INTERVAL_DAY, true);
}
#endif
scenario_update();
climate_update();

View File

@ -194,6 +194,7 @@ const char * PlatformEnvironment::DirectoryNamesRCT2[] =
nullptr, // LOG_SERVER
nullptr, // NETWORK_KEY
"ObjData", // OBJECT
nullptr, // PLUGIN
"Saved Games", // SAVE
"Scenarios", // SCENARIO
nullptr, // SCREENSHOT
@ -212,6 +213,7 @@ const char * PlatformEnvironment::DirectoryNamesOpenRCT2[] =
"serverlogs", // LOG_SERVER
"keys", // NETWORK_KEY
"object", // OBJECT
"plugin", // PLUGIN
"save", // SAVE
"scenario", // SCENARIO
"screenshot", // SCREENSHOT
@ -238,6 +240,7 @@ const char * PlatformEnvironment::FileNames[] =
"highscores.dat", // SCORES
"scores.dat", // SCORES (LEGACY)
"Saved Games" PATH_SEPARATOR "scores.dat", // SCORES (RCT2)
"changelog.txt" // CHANGELOG
"changelog.txt", // CHANGELOG
"plugin.store.json" // PLUGIN_STORE
};
// clang-format on

View File

@ -38,6 +38,7 @@ namespace OpenRCT2
LOG_SERVER, // Contains server logs.
NETWORK_KEY, // Contains the user's public and private keys.
OBJECT, // Contains objects.
PLUGIN, // Contains plugins (.js).
SAVE, // Contains saved games (SV6).
SCENARIO, // Contains scenarios (SC6).
SCREENSHOT, // Contains screenshots.
@ -65,6 +66,7 @@ namespace OpenRCT2
SCORES_LEGACY, // Scenario scores, legacy (scores.dat).
SCORES_RCT2, // Scenario scores, rct2 (\Saved Games\scores.dat).
CHANGELOG, // Notable changes to the game between versions, distributed with the game.
PLUGIN_STORE, // Shared storage for plugins.
};
/**

View File

@ -0,0 +1,56 @@
/*****************************************************************************
* 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 "../Context.h"
# include "../scripting/ScriptEngine.h"
# include "GameAction.h"
DEFINE_GAME_ACTION(CustomAction, GAME_COMMAND_CUSTOM, GameActionResult)
{
private:
std::string _id;
std::string _json;
public:
CustomAction() = default;
CustomAction(const std::string& id, const std::string& json)
: _id(id)
, _json(json)
{
}
uint16_t GetActionFlags() const override
{
return GameAction::GetActionFlags() | GA_FLAGS::ALLOW_WHILE_PAUSED;
}
void Serialise(DataSerialiser & stream) override
{
GameAction::Serialise(stream);
stream << DS_TAG(_id) << DS_TAG(_json);
}
GameActionResult::Ptr Query() const override
{
auto& scriptingEngine = OpenRCT2::GetContext()->GetScriptEngine();
return scriptingEngine.QueryOrExecuteCustomGameAction(_id, _json, false);
}
GameActionResult::Ptr Execute() const override
{
auto& scriptingEngine = OpenRCT2::GetContext()->GetScriptEngine();
return scriptingEngine.QueryOrExecuteCustomGameAction(_id, _json, true);
}
};
#endif

View File

@ -18,12 +18,17 @@
#include "../network/network.h"
#include "../platform/platform.h"
#include "../scenario/Scenario.h"
#include "../scripting/ScriptEngine.h"
#include "../ui/UiContext.h"
#include "../ui/WindowManager.h"
#include "../world/Park.h"
#include "../world/Scenery.h"
#include <algorithm>
#include <iterator>
using namespace OpenRCT2;
GameActionResult::GameActionResult(GA_ERROR error, rct_string_id message)
{
Error = error;
@ -45,6 +50,34 @@ GameActionResult::GameActionResult(GA_ERROR error, rct_string_id title, rct_stri
std::copy_n(args, ErrorMessageArgs.size(), ErrorMessageArgs.begin());
}
std::string GameActionResult::GetErrorTitle() const
{
std::string title;
if (auto error = ErrorTitle.AsString())
{
title = *error;
}
else
{
title = format_string(ErrorTitle.GetStringId(), nullptr);
}
return title;
}
std::string GameActionResult::GetErrorMessage() const
{
std::string message;
if (auto error = ErrorMessage.AsString())
{
message = *error;
}
else
{
message = format_string(ErrorMessage.GetStringId(), ErrorMessageArgs.data());
}
return message;
}
namespace GameActions
{
struct QueuedGameAction
@ -367,6 +400,15 @@ namespace GameActions
}
GameActionResult::Ptr result = QueryInternal(action, topLevel);
#ifdef ENABLE_SCRIPTING
if (result->Error == GA_ERROR::OK
&& ((network_get_mode() == NETWORK_MODE_NONE) || (flags & GAME_COMMAND_FLAG_NETWORKED)))
{
auto& scriptEngine = GetContext()->GetScriptEngine();
scriptEngine.RunGameActionHooks(*action, result, false);
// Script hooks may now have changed the game action result...
}
#endif
if (result->Error == GA_ERROR::OK)
{
if (topLevel)
@ -402,6 +444,14 @@ namespace GameActions
// Execute the action, changing the game state
result = action->Execute();
#ifdef ENABLE_SCRIPTING
if (result->Error == GA_ERROR::OK)
{
auto& scriptEngine = GetContext()->GetScriptEngine();
scriptEngine.RunGameActionHooks(*action, result, true);
// Script hooks may now have changed the game action result...
}
#endif
LogActionFinish(logContext, action, result);
@ -486,9 +536,8 @@ namespace GameActions
if (result->Error != GA_ERROR::OK && shouldShowError)
{
// Show the error box
std::copy(result->ErrorMessageArgs.begin(), result->ErrorMessageArgs.end(), gCommonFormatArgs);
context_show_error(result->ErrorTitle, result->ErrorMessage);
auto windowManager = GetContext()->GetUiContext()->GetWindowManager();
windowManager->ShowError(result->GetErrorTitle(), result->GetErrorMessage());
}
return result;

View File

@ -60,6 +60,59 @@ namespace GA_FLAGS
# pragma GCC diagnostic ignored "-Wsuggest-final-types"
#endif
class StringVariant
{
private:
rct_string_id StringId = STR_NONE;
std::string String;
public:
StringVariant() = default;
StringVariant(rct_string_id stringId)
: StringId(stringId)
{
}
StringVariant(const std::string& s)
: String(s)
{
}
StringVariant(std::string&& s)
: String(s)
{
}
StringVariant(const char* s)
: String(s)
{
}
const std::string* AsString() const
{
if (!String.empty())
{
return &String;
}
return {};
}
const rct_string_id* AsStringId() const
{
if (String.empty())
{
return &StringId;
}
return {};
}
rct_string_id GetStringId() const
{
return String.empty() ? StringId : STR_NONE;
}
};
/**
* Represents the result of a game action query or execution.
*/
@ -69,8 +122,8 @@ public:
using Ptr = std::unique_ptr<GameActionResult>;
GA_ERROR Error = GA_ERROR::OK;
rct_string_id ErrorTitle = STR_NONE;
rct_string_id ErrorMessage = STR_NONE;
StringVariant ErrorTitle;
StringVariant ErrorMessage;
std::array<uint8_t, 32> ErrorMessageArgs;
CoordsXYZ Position = { LOCATION_NULL, LOCATION_NULL, LOCATION_NULL };
money32 Cost = 0;
@ -82,6 +135,9 @@ public:
GameActionResult(GA_ERROR error, rct_string_id title, rct_string_id message, uint8_t* args);
GameActionResult(const GameActionResult&) = delete;
virtual ~GameActionResult(){};
std::string GetErrorTitle() const;
std::string GetErrorMessage() const;
};
struct GameAction

View File

@ -140,8 +140,14 @@ money32 maze_set_track(
// NOTE: ride_construction_tooldown_construct requires them to be set.
// Refactor result type once theres no C code referencing this function.
gGameCommandErrorText = res->ErrorMessage;
gGameCommandErrorTitle = res->ErrorTitle;
if (auto title = res->ErrorTitle.AsStringId())
gGameCommandErrorTitle = *title;
else
gGameCommandErrorTitle = STR_NONE;
if (auto message = res->ErrorMessage.AsStringId())
gGameCommandErrorText = *message;
else
gGameCommandErrorText = STR_NONE;
if (res->Error != GA_ERROR::OK)
{

View File

@ -15,6 +15,7 @@
#include "BannerSetStyleAction.hpp"
#include "ClearAction.hpp"
#include "ClimateSetAction.hpp"
#include "CustomAction.hpp"
#include "FootpathPlaceAction.hpp"
#include "FootpathPlaceFromTrackAction.hpp"
#include "FootpathRemoveAction.hpp"
@ -171,5 +172,8 @@ namespace GameActions
Register<GuestSetFlagsAction>();
Register<ParkSetDateAction>();
Register<SetCheatAction>();
#ifdef ENABLE_SCRIPTING
Register<CustomAction>();
#endif
}
} // namespace GameActions

View File

@ -642,7 +642,7 @@ private:
}
default:
log_error("Invalid map selection %u", _selectionType);
return MakeResult(GA_ERROR::INVALID_PARAMETERS, res->ErrorTitle);
return MakeResult(GA_ERROR::INVALID_PARAMETERS, res->ErrorTitle.GetStringId());
} // switch selectionType
// Raise / lower the land tool selection area

View File

@ -93,7 +93,7 @@ public:
{ _loc.ToTileStart(), baseHeight, clearanceHeight }, &map_place_non_scenery_clear_func, { 0b1111, 0 },
GetFlags(), &clearCost, CREATE_CROSSING_MODE_NONE))
{
return MakeResult(GA_ERROR::NO_CLEARANCE, res->ErrorTitle, gGameCommandErrorText, gCommonFormatArgs);
return MakeResult(GA_ERROR::NO_CLEARANCE, res->ErrorTitle.GetStringId(), gGameCommandErrorText, gCommonFormatArgs);
}
if (gMapGroundFlags & ELEMENT_IS_UNDERWATER)
@ -162,7 +162,7 @@ public:
{ _loc.ToTileStart(), baseHeight, clearanceHeight }, &map_place_non_scenery_clear_func, { 0b1111, 0 },
GetFlags() | GAME_COMMAND_FLAG_APPLY, &clearCost, CREATE_CROSSING_MODE_NONE))
{
return MakeResult(GA_ERROR::NO_CLEARANCE, res->ErrorTitle, gGameCommandErrorText, gCommonFormatArgs);
return MakeResult(GA_ERROR::NO_CLEARANCE, res->ErrorTitle.GetStringId(), gGameCommandErrorText, gCommonFormatArgs);
}
money32 price = (((RideTrackCosts[ride->type].track_price * TrackPricing[TRACK_ELEM_MAZE]) >> 16));

View File

@ -136,7 +136,8 @@ public:
if (!map_can_construct_at({ _loc.ToTileStart(), baseHeight, clearanceHeight }, { 0b1111, 0 }))
{
return MakeResult(GA_ERROR::NO_CLEARANCE, res->ErrorTitle, gGameCommandErrorText, gCommonFormatArgs);
return MakeResult(
GA_ERROR::NO_CLEARANCE, res->ErrorTitle.GetStringId(), gGameCommandErrorText, gCommonFormatArgs);
}
if (gMapGroundFlags & ELEMENT_IS_UNDERWATER)

View File

@ -60,6 +60,16 @@ public:
{
}
int32_t GetRideType() const
{
return _rideType;
}
int32_t GetRideObject() const
{
return _subType;
}
uint16_t GetActionFlags() const override
{
return GameAction::GetActionFlags() | GA_FLAGS::ALLOW_WHILE_PAUSED;

View File

@ -549,6 +549,22 @@ namespace Config
writer->WriteInt32("hinting_threshold", model->hinting_threshold);
}
static void ReadPlugin(IIniReader* reader)
{
if (reader->ReadSection("plugin"))
{
auto model = &gConfigPlugin;
model->enable_hot_reloading = reader->GetBoolean("enable_hot_reloading", false);
}
}
static void WritePlugin(IIniWriter* writer)
{
auto model = &gConfigPlugin;
writer->WriteSection("plugin");
writer->WriteBoolean("enable_hot_reloading", model->enable_hot_reloading);
}
static bool SetDefaults()
{
try
@ -561,6 +577,7 @@ namespace Config
ReadNotifications(reader.get());
ReadTwitch(reader.get());
ReadFont(reader.get());
ReadPlugin(reader.get());
return true;
}
catch (const std::exception&)
@ -582,6 +599,7 @@ namespace Config
ReadNotifications(reader.get());
ReadTwitch(reader.get());
ReadFont(reader.get());
ReadPlugin(reader.get());
return true;
}
catch (const std::exception&)
@ -606,6 +624,7 @@ namespace Config
WriteNotifications(writer.get());
WriteTwitch(writer.get());
WriteFont(writer.get());
WritePlugin(writer.get());
return true;
}
catch (const std::exception& ex)
@ -721,6 +740,7 @@ TwitchConfiguration gConfigTwitch;
NetworkConfiguration gConfigNetwork;
NotificationConfiguration gConfigNotifications;
FontConfiguration gConfigFonts;
PluginConfiguration gConfigPlugin;
void config_set_defaults()
{

View File

@ -203,6 +203,11 @@ struct FontConfiguration
int32_t hinting_threshold;
};
struct PluginConfiguration
{
bool enable_hot_reloading;
};
enum SORT
{
SORT_NAME_ASCENDING,
@ -238,6 +243,7 @@ extern TwitchConfiguration gConfigTwitch;
extern NetworkConfiguration gConfigNetwork;
extern NotificationConfiguration gConfigNotifications;
extern FontConfiguration gConfigFonts;
extern PluginConfiguration gConfigPlugin;
bool config_open(const utf8* path);
bool config_save(const utf8* path);

View File

@ -35,7 +35,7 @@
# include <filesystem>
namespace fs = std::filesystem;
#else
# include "../thirdparty/filesystem.hpp"
# include <filesystem.hpp>
namespace fs = ghc::filesystem;
#endif

View File

@ -0,0 +1,199 @@
/*****************************************************************************
* 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.
*****************************************************************************/
#include <array>
#include <cstdio>
#include <stdexcept>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#elif defined(__linux__)
# include <fcntl.h>
# include <sys/inotify.h>
# include <sys/types.h>
# include <unistd.h>
#endif
#include "../core/String.hpp"
#include "FileSystem.hpp"
#include "FileWatcher.h"
#if defined(__linux__)
FileWatcher::FileDescriptor::~FileDescriptor()
{
Close();
}
void FileWatcher::FileDescriptor::Initialise()
{
int fd = inotify_init();
if (fd >= 0)
{
// Mark file as non-blocking
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
Fd = fd;
log_verbose("FileWatcher: inotify_init succeeded");
}
else
{
log_verbose("FileWatcher: inotify_init failed");
throw std::runtime_error("inotify_init failed");
}
}
void FileWatcher::FileDescriptor::Close()
{
if (Fd != -1)
{
close(Fd);
Fd = -1;
}
}
FileWatcher::WatchDescriptor::WatchDescriptor(int fd, const std::string& path)
: Fd(fd)
, Wd(inotify_add_watch(fd, path.c_str(), IN_CLOSE_WRITE))
, Path(path)
{
if (Wd >= 0)
{
log_verbose("FileWatcher: inotify watch added for %s", path.c_str());
}
else
{
log_verbose("FileWatcher: inotify_add_watch failed for %s", path.c_str());
throw std::runtime_error("inotify_add_watch failed for '" + path + "'");
}
}
FileWatcher::WatchDescriptor::~WatchDescriptor()
{
inotify_rm_watch(Fd, Wd);
log_verbose("FileWatcher: inotify watch removed");
}
#endif
FileWatcher::FileWatcher(const std::string& directoryPath)
{
#ifdef _WIN32
_path = directoryPath;
_directoryHandle = CreateFileA(
directoryPath.c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS,
nullptr);
if (_directoryHandle == INVALID_HANDLE_VALUE)
{
throw std::runtime_error("Unable to open directory '" + directoryPath + "'");
}
#elif defined(__linux__)
_fileDesc.Initialise();
_watchDescs.emplace_back(_fileDesc.Fd, directoryPath);
for (auto& p : fs::recursive_directory_iterator(directoryPath))
{
if (p.status().type() == fs::file_type::directory)
{
_watchDescs.emplace_back(_fileDesc.Fd, p.path().string());
}
}
#else
throw std::runtime_error("FileWatcher not supported on this platform.");
#endif
_watchThread = std::thread(std::bind(&FileWatcher::WatchDirectory, this));
}
FileWatcher::~FileWatcher()
{
#ifdef _WIN32
# ifdef __MINGW32__
// TODO CancelIo is documented as not working across a different thread but
// CancelIoEx is not available.
CancelIo(_directoryHandle);
# else
CancelIoEx(_directoryHandle, nullptr);
# endif
CloseHandle(_directoryHandle);
#elif defined(__linux__)
_finished = true;
_fileDesc.Close();
#else
return;
#endif
_watchThread.join();
}
void FileWatcher::WatchDirectory()
{
#if defined(_WIN32)
std::array<char, 1024> eventData;
DWORD bytesReturned;
while (ReadDirectoryChangesW(
_directoryHandle, eventData.data(), (DWORD)eventData.size(), TRUE, FILE_NOTIFY_CHANGE_LAST_WRITE, &bytesReturned,
nullptr, nullptr))
{
auto onFileChanged = OnFileChanged;
if (onFileChanged)
{
FILE_NOTIFY_INFORMATION* notifyInfo;
size_t offset = 0;
do
{
notifyInfo = (FILE_NOTIFY_INFORMATION*)(eventData.data() + offset);
offset += notifyInfo->NextEntryOffset;
std::wstring fileNameW(notifyInfo->FileName, notifyInfo->FileNameLength / sizeof(wchar_t));
auto fileName = String::ToUtf8(fileNameW);
auto path = fs::path(_path) / fs::path(fileName);
onFileChanged(path.u8string());
} while (notifyInfo->NextEntryOffset != 0);
}
}
#elif defined(__linux__)
log_verbose("FileWatcher: reading event data...");
std::array<char, 1024> eventData;
while (!_finished)
{
int length = read(_fileDesc.Fd, eventData.data(), eventData.size());
if (length >= 0)
{
log_verbose("FileWatcher: inotify event data received");
auto onFileChanged = OnFileChanged;
if (onFileChanged)
{
int offset = 0;
while (offset < length)
{
auto e = reinterpret_cast<inotify_event*>(eventData.data() + offset);
if ((e->mask & IN_CLOSE_WRITE) && !(e->mask & IN_ISDIR))
{
log_verbose("FileWatcher: inotify event received for %s", e->name);
// Find watch descriptor
int wd = e->wd;
auto findResult = std::find_if(
_watchDescs.begin(), _watchDescs.end(),
[wd](const WatchDescriptor& watchDesc) { return wd == watchDesc.Wd; });
if (findResult != _watchDescs.end())
{
auto directory = findResult->Path;
auto path = fs::path(directory) / fs::path(e->name);
onFileChanged(path);
}
}
offset += sizeof(inotify_event) + e->len;
}
}
}
// Sleep for 1/2 second
usleep(500000);
}
#endif
}

View File

@ -0,0 +1,65 @@
/*****************************************************************************
* 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
#include <functional>
#include <string>
#include <thread>
#include <vector>
#ifdef _WIN32
typedef void* HANDLE;
#endif
/**
* Creates a new thread that watches a directory tree for file modifications.
*/
class FileWatcher
{
private:
std::thread _watchThread;
#if defined(_WIN32)
std::string _path;
HANDLE _directoryHandle{};
#elif defined(__linux__)
struct FileDescriptor
{
int Fd = -1;
~FileDescriptor();
void Initialise();
void Close();
};
struct WatchDescriptor
{
int const Fd;
int const Wd;
std::string const Path;
WatchDescriptor(int fd, const std::string& path);
~WatchDescriptor();
};
FileDescriptor _fileDesc;
std::vector<WatchDescriptor> _watchDescs;
#endif
public:
std::function<void(const std::string& path)> OnFileChanged;
FileWatcher(const std::string& directoryPath);
~FileWatcher();
private:
bool _finished{};
void WatchDirectory();
};

View File

@ -12,11 +12,9 @@
#include "../common.h"
#include "../localisation/FormatCodes.h"
#include <deque>
#include <future>
#include <queue>
#include <string>
#include <tuple>
struct rct_drawpixelinfo;
struct TextInputSession;

View File

@ -7,11 +7,16 @@
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#include "../Context.h"
#include "../OpenRCT2.h"
#include "../platform/Platform2.h"
#include "../thirdparty/linenoise.hpp"
#include "../scripting/ScriptEngine.h"
#include "InteractiveConsole.h"
#include <linenoise.hpp>
using namespace OpenRCT2;
void StdInOutConsole::Start()
{
std::thread replThread([this]() -> void {
@ -51,16 +56,22 @@ void StdInOutConsole::Start()
std::future<void> StdInOutConsole::Eval(const std::string& s)
{
#ifdef ENABLE_SCRIPTING
auto& scriptEngine = GetContext()->GetScriptEngine();
return scriptEngine.Eval(s);
#else
// Push on-demand evaluations onto a queue so that it can be processed deterministically
// on the main thead at the right time.
std::promise<void> barrier;
auto future = barrier.get_future();
_evalQueue.emplace(std::move(barrier), s);
return future;
#endif
}
void StdInOutConsole::ProcessEvalQueue()
{
#ifndef ENABLE_SCRIPTING
while (_evalQueue.size() > 0)
{
auto item = std::move(_evalQueue.front());
@ -73,6 +84,7 @@ void StdInOutConsole::ProcessEvalQueue()
// Signal the promise so caller can continue
promise.set_value();
}
#endif
}
void StdInOutConsole::Clear()

View File

@ -1009,6 +1009,8 @@ void window_viewport_centre_tile_around_cursor(rct_window* w, int16_t map_x, int
void window_zoom_set(rct_window* w, ZoomLevel zoomLevel, bool atCursor)
{
rct_viewport* v = w->viewport;
if (v == nullptr)
return;
zoomLevel = std::clamp(zoomLevel, ZoomLevel::min(), ZoomLevel::max());
if (v->zoom == zoomLevel)

View File

@ -64,6 +64,15 @@ struct widget_identifier
extern widget_identifier gCurrentTextBox;
using WidgetFlags = uint32_t;
namespace WIDGET_FLAGS
{
const WidgetFlags TEXT_IS_STRING = 1 << 0;
const WidgetFlags IS_ENABLED = 1 << 1;
const WidgetFlags IS_PRESSED = 1 << 2;
const WidgetFlags IS_DISABLED = 1 << 3;
} // namespace WIDGET_FLAGS
/**
* Widget structure
* size: 0x10
@ -84,6 +93,9 @@ struct rct_widget
utf8* string;
};
rct_string_id tooltip; // 0x0E
// New properties
WidgetFlags flags{};
};
/**
@ -443,6 +455,7 @@ enum
WC_EDITOR_SCENARIO_BOTTOM_TOOLBAR = 222,
WC_CHAT = 223,
WC_CONSOLE = 224,
WC_CUSTOM = 225,
WC_NULL = 255,
};

View File

@ -55,6 +55,7 @@ struct rct_window
scenery_variables scenery;
track_list_variables track_list;
error_variables error;
void* custom_info;
};
int16_t page;
union

View File

@ -136,3 +136,113 @@ rct_string_id language_allocate_object_string(const std::string& target)
auto& localisationService = OpenRCT2::GetContext()->GetLocalisationService();
return localisationService.AllocateObjectString(target);
}
std::string language_convert_string_to_tokens(const std::string_view& s)
{
std::string result;
result.reserve(s.size() * 4);
std::string input = std::string(s);
auto readPtr = input.c_str();
while (true)
{
char32_t code = utf8_get_next(readPtr, (const utf8**)&readPtr);
if (code == 0)
{
break;
}
else if (code == '\n')
{
result.push_back('\n');
}
else if (utf8_is_format_code(code))
{
auto token = format_get_token(code);
result.push_back('{');
result.append(token);
result.push_back('}');
}
else
{
char buffer[8]{};
utf8_write_codepoint(buffer, code);
result.append(buffer);
}
}
result.shrink_to_fit();
return result;
}
std::string language_convert_string(const std::string_view& s)
{
enum class PARSE_STATE
{
DEFAULT,
CR,
TOKEN,
};
std::string result;
std::string token;
PARSE_STATE state{};
token.reserve(64);
result.reserve(s.size() * 2);
for (char c : s)
{
switch (state)
{
case PARSE_STATE::CR:
result.push_back(FORMAT_NEWLINE);
state = PARSE_STATE::DEFAULT;
[[fallthrough]];
case PARSE_STATE::DEFAULT:
switch (c)
{
case '\r':
state = PARSE_STATE::CR;
break;
case '\n':
result.push_back(FORMAT_NEWLINE);
break;
case '{':
token.clear();
state = PARSE_STATE::TOKEN;
break;
default:
if (c >= 32)
{
result.push_back(c);
}
break;
}
break;
case PARSE_STATE::TOKEN:
if (c == '}')
{
auto code = format_get_code(token.c_str());
if (code == 0)
{
int32_t number{};
if (sscanf(token.c_str(), "%d", &number) == 1)
{
auto b = static_cast<uint8_t>(std::clamp(number, 0, 255));
token.push_back(b);
}
}
else
{
char buffer[8]{};
utf8_write_codepoint(buffer, code);
result.append(buffer);
}
state = PARSE_STATE::DEFAULT;
}
else
{
token.push_back(c);
}
break;
}
}
result.shrink_to_fit();
return result;
}

View File

@ -107,6 +107,8 @@ bool language_get_localised_scenario_strings(const utf8* scenarioFilename, rct_s
void language_free_object_string(rct_string_id stringId);
rct_string_id language_get_object_override_string_id(const char* identifier, uint8_t index);
rct_string_id language_allocate_object_string(const std::string& target);
std::string language_convert_string_to_tokens(const std::string_view& s);
std::string language_convert_string(const std::string_view& s);
constexpr utf8* utf8_write_codepoint(utf8* dst, uint32_t codepoint)
{

View File

@ -19,6 +19,7 @@
#include "../actions/PeepPickupAction.hpp"
#include "../core/Guard.hpp"
#include "../platform/platform.h"
#include "../scripting/ScriptEngine.h"
#include "../ui/UiContext.h"
#include "../ui/WindowManager.h"
#include "../util/SawyerCoding.h"
@ -31,7 +32,7 @@
// This string specifies which version of network stream current build uses.
// It is used for making sure only compatible builds get connected, even within
// single OpenRCT2 version.
#define NETWORK_STREAM_VERSION "8"
#define NETWORK_STREAM_VERSION "9"
#define NETWORK_STREAM_ID OPENRCT2_VERSION "-" NETWORK_STREAM_VERSION
static Peep* _pickup_peep = nullptr;
@ -193,6 +194,7 @@ public:
void Client_Send_GAMEINFO();
void Client_Send_OBJECTS(const std::vector<std::string>& objects);
void Server_Send_OBJECTS(NetworkConnection& connection, const std::vector<const ObjectRepositoryItem*>& objects) const;
void Server_Send_SCRIPTS(NetworkConnection& connection) const;
NetworkStats_t GetStats() const;
json_t* GetServerInfoAsJson() const;
@ -308,6 +310,7 @@ private:
void Client_Handle_TOKEN(NetworkConnection& connection, NetworkPacket& packet);
void Server_Handle_TOKEN(NetworkConnection& connection, NetworkPacket& packet);
void Client_Handle_OBJECTS(NetworkConnection& connection, NetworkPacket& packet);
void Client_Handle_SCRIPTS(NetworkConnection& connection, NetworkPacket& packet);
void Client_Handle_GAMESTATE(NetworkConnection& connection, NetworkPacket& packet);
void Server_Handle_OBJECTS(NetworkConnection& connection, NetworkPacket& packet);
@ -343,6 +346,7 @@ Network::Network()
client_command_handlers[NETWORK_COMMAND_GAMEINFO] = &Network::Client_Handle_GAMEINFO;
client_command_handlers[NETWORK_COMMAND_TOKEN] = &Network::Client_Handle_TOKEN;
client_command_handlers[NETWORK_COMMAND_OBJECTS] = &Network::Client_Handle_OBJECTS;
client_command_handlers[NETWORK_COMMAND_SCRIPTS] = &Network::Client_Handle_SCRIPTS;
client_command_handlers[NETWORK_COMMAND_GAMESTATE] = &Network::Client_Handle_GAMESTATE;
server_command_handlers.resize(NETWORK_COMMAND_MAX, nullptr);
server_command_handlers[NETWORK_COMMAND_AUTH] = &Network::Server_Handle_AUTH;
@ -623,6 +627,8 @@ bool Network::BeginServer(uint16_t port, const std::string& address)
_serverState.gamestateSnapshotsEnabled = gConfigNetwork.desync_debugging;
_advertiser = CreateServerAdvertiser(listening_port);
game_load_scripts();
return true;
}
@ -1464,6 +1470,42 @@ void Network::Server_Send_OBJECTS(NetworkConnection& connection, const std::vect
connection.QueuePacket(std::move(packet));
}
void Network::Server_Send_SCRIPTS(NetworkConnection& connection) const
{
std::unique_ptr<NetworkPacket> packet(NetworkPacket::Allocate());
*packet << static_cast<uint32_t>(NETWORK_COMMAND_SCRIPTS);
# ifdef ENABLE_SCRIPTING
using namespace OpenRCT2::Scripting;
auto& scriptEngine = GetContext()->GetScriptEngine();
const auto& plugins = scriptEngine.GetPlugins();
std::vector<std::shared_ptr<Plugin>> pluginsToSend;
for (const auto& plugin : plugins)
{
const auto& metadata = plugin->GetMetadata();
if (metadata.Type == OpenRCT2::Scripting::PluginType::Remote)
{
pluginsToSend.push_back(plugin);
}
}
log_verbose("Server sends %u scripts", pluginsToSend.size());
*packet << static_cast<uint32_t>(pluginsToSend.size());
for (const auto& plugin : pluginsToSend)
{
const auto& metadata = plugin->GetMetadata();
log_verbose("Script %s", metadata.Name.c_str());
const auto& code = plugin->GetCode();
*packet << static_cast<uint32_t>(code.size());
packet->Write(reinterpret_cast<const uint8_t*>(code.c_str()), code.size());
}
# else
*packet << static_cast<uint32_t>(0);
# endif
connection.QueuePacket(std::move(packet));
}
NetworkStats_t Network::GetStats() const
{
NetworkStats_t stats = {};
@ -2338,6 +2380,7 @@ void Network::Server_Client_Joined(const char* name, const std::string& keyhash,
auto& objManager = context->GetObjectManager();
auto objects = objManager.GetPackableObjects();
Server_Send_OBJECTS(connection, objects);
Server_Send_SCRIPTS(connection);
// Log player joining event
std::string playerNameHash = player->Name + " (" + keyhash + ")";
@ -2397,6 +2440,29 @@ void Network::Client_Handle_OBJECTS(NetworkConnection& connection, NetworkPacket
Client_Send_OBJECTS(requested_objects);
}
void Network::Client_Handle_SCRIPTS(NetworkConnection& connection, NetworkPacket& packet)
{
uint32_t numScripts{};
packet >> numScripts;
# ifdef ENABLE_SCRIPTING
auto& scriptEngine = GetContext()->GetScriptEngine();
for (uint32_t i = 0; i < numScripts; i++)
{
uint32_t codeLength{};
packet >> codeLength;
auto code = std::string_view(reinterpret_cast<const char*>(packet.Read(codeLength)), codeLength);
scriptEngine.AddNetworkPlugin(code);
}
# else
if (numScripts > 0)
{
connection.SetLastDisconnectReason("The server requires plugin support.");
Close();
}
# endif
}
void Network::Client_Handle_GAMESTATE(NetworkConnection& connection, NetworkPacket& packet)
{
uint32_t tick;
@ -2682,6 +2748,7 @@ void Network::Client_Handle_MAP([[maybe_unused]] NetworkConnection& connection,
if (LoadMap(&ms))
{
game_load_init();
game_load_scripts();
_serverState.tick = gCurrentTicks;
// window_network_status_open("Loaded new map from network");
_serverState.state = NETWORK_SERVER_STATE_OK;
@ -2819,8 +2886,48 @@ void Network::Client_Handle_CHAT([[maybe_unused]] NetworkConnection& connection,
}
}
static bool ProcessChatMessagePluginHooks(const NetworkPlayer& player, std::string& text)
{
# ifdef ENABLE_SCRIPTING
auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine();
if (hookEngine.HasSubscriptions(OpenRCT2::Scripting::HOOK_TYPE::NETWORK_CHAT))
{
auto ctx = GetContext()->GetScriptEngine().GetContext();
// Create event args object
auto objIdx = duk_push_object(ctx);
duk_push_number(ctx, static_cast<int32_t>(player.Id));
duk_put_prop_string(ctx, objIdx, "player");
duk_push_string(ctx, text.c_str());
duk_put_prop_string(ctx, objIdx, "message");
auto e = DukValue::take_from_stack(ctx);
// Call the subscriptions
hookEngine.Call(OpenRCT2::Scripting::HOOK_TYPE::NETWORK_CHAT, e, false);
// Update text from object if subscriptions changed it
if (e["message"].type() != DukValue::Type::STRING)
{
// Subscription set text to non-string, do not relay message
return false;
}
text = e["message"].as_string();
if (text.empty())
{
// Subscription set text to empty string, do not relay message
return false;
}
}
# endif
return true;
}
void Network::Server_Handle_CHAT(NetworkConnection& connection, NetworkPacket& packet)
{
auto szText = packet.ReadString();
if (szText == nullptr || szText[0] == '\0')
return;
if (connection.Player)
{
NetworkGroup* group = GetGroupByID(connection.Player->Group);
@ -2829,13 +2936,20 @@ void Network::Server_Handle_CHAT(NetworkConnection& connection, NetworkPacket& p
return;
}
}
const char* text = packet.ReadString();
if (text)
std::string text = szText;
if (connection.Player != nullptr)
{
const char* formatted = FormatChat(connection.Player, text);
chat_history_add(formatted);
Server_Send_CHAT(formatted);
if (!ProcessChatMessagePluginHooks(*connection.Player, text))
{
// Message not to be relayed
return;
}
}
const char* formatted = FormatChat(connection.Player, text.c_str());
chat_history_add(formatted);
Server_Send_CHAT(formatted);
}
void Network::Client_Handle_GAME_ACTION([[maybe_unused]] NetworkConnection& connection, NetworkPacket& packet)
@ -2893,12 +3007,15 @@ void Network::Server_Handle_GAME_ACTION(NetworkConnection& connection, NetworkPa
return;
}
// Check if player's group permission allows command to run
NetworkGroup* group = GetGroupByID(connection.Player->Group);
if (group == nullptr || group->CanPerformCommand(actionType) == false)
if (actionType != GAME_COMMAND_CUSTOM)
{
Server_Send_SHOWERROR(connection, STR_CANT_DO_THIS, STR_PERMISSION_DENIED);
return;
// Check if player's group permission allows command to run
NetworkGroup* group = GetGroupByID(connection.Player->Group);
if (group == nullptr || group->CanPerformCommand(actionType) == false)
{
Server_Send_SHOWERROR(connection, STR_CANT_DO_THIS, STR_PERMISSION_DENIED);
return;
}
}
// Create and enqueue the action.
@ -3575,7 +3692,8 @@ GameActionResult::Ptr network_kick_player(NetworkPlayerId_t playerId, bool isExe
NetworkPlayer* player = gNetwork.GetPlayerByID(playerId);
if (player == nullptr)
{
// Player might be already removed by the PLAYERLIST command, need to refactor non-game commands executing too early.
// Player might be already removed by the PLAYERLIST command, need to refactor non-game commands executing too
// early.
return std::make_unique<GameActionResult>(GA_ERROR::UNKNOWN, STR_NONE);
}

View File

@ -68,6 +68,7 @@ enum NETWORK_COMMAND
NETWORK_COMMAND_PLAYERINFO,
NETWORK_COMMAND_REQUEST_GAMESTATE,
NETWORK_COMMAND_GAMESTATE,
NETWORK_COMMAND_SCRIPTS,
NETWORK_COMMAND_MAX,
NETWORK_COMMAND_INVALID = -1
};

View File

@ -0,0 +1,184 @@
/*****************************************************************************
* 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 <cstdio>
# include <dukglue/dukglue.h>
# include <duktape.h>
# include <optional>
# include <stdexcept>
namespace OpenRCT2::Scripting
{
template<typename T> DukValue GetObjectAsDukValue(duk_context* ctx, const std::shared_ptr<T>& value)
{
dukglue::types::DukType<std::shared_ptr<T>>::template push<T>(ctx, value);
return DukValue::take_from_stack(ctx);
}
template<typename T> T AsOrDefault(const DukValue& value, const T& defaultValue = {}) = delete;
template<> inline std::string AsOrDefault(const DukValue& value, const std::string& defaultValue)
{
return value.type() == DukValue::STRING ? value.as_string() : defaultValue;
}
template<> inline int32_t AsOrDefault(const DukValue& value, const int32_t& defaultValue)
{
return value.type() == DukValue::NUMBER ? value.as_int() : defaultValue;
}
/**
* Allows creation of an object on the duktape stack and setting properties on it before
* retrieving the DukValue instance of it.
*/
class DukObject
{
private:
duk_context* _ctx{};
duk_idx_t _idx = DUK_INVALID_INDEX;
public:
DukObject(duk_context* ctx)
: _ctx(ctx)
{
}
DukObject(const DukObject&) = delete;
DukObject(DukObject&& m) noexcept
{
_ctx = m._ctx;
_idx = m._idx;
m._ctx = {};
m._idx = {};
}
~DukObject()
{
PopObjectIfExists();
}
void Set(const char* name, bool value)
{
EnsureObjectPushed();
duk_push_boolean(_ctx, value);
duk_put_prop_string(_ctx, _idx, name);
}
void Set(const char* name, int32_t value)
{
EnsureObjectPushed();
duk_push_int(_ctx, value);
duk_put_prop_string(_ctx, _idx, name);
}
void Set(const char* name, uint32_t value)
{
EnsureObjectPushed();
duk_push_uint(_ctx, value);
duk_put_prop_string(_ctx, _idx, name);
}
void Set(const char* name, const std::string_view& value)
{
EnsureObjectPushed();
duk_push_lstring(_ctx, value.data(), value.size());
duk_put_prop_string(_ctx, _idx, name);
}
void Set(const char* name, const DukValue& value)
{
EnsureObjectPushed();
value.push();
duk_put_prop_string(_ctx, _idx, name);
}
DukValue Take()
{
EnsureObjectPushed();
auto result = DukValue::take_from_stack(_ctx, _idx);
_idx = DUK_INVALID_INDEX;
return result;
}
private:
void PopObjectIfExists()
{
if (_idx != DUK_INVALID_INDEX)
{
duk_remove(_ctx, _idx);
_idx = DUK_INVALID_INDEX;
}
}
void EnsureObjectPushed()
{
if (_idx == DUK_INVALID_INDEX)
{
_idx = duk_push_object(_ctx);
}
}
};
class DukStackFrame
{
private:
duk_context* _ctx{};
duk_idx_t _top;
public:
DukStackFrame(duk_context* ctx)
: _ctx(ctx)
{
_top = duk_get_top(ctx);
}
~DukStackFrame()
{
auto top = duk_get_top(_ctx);
if (top != _top)
{
duk_set_top(_ctx, _top);
_ctx = {};
std::fprintf(stderr, "duktape stack was not returned to original state!");
}
_ctx = {};
}
DukStackFrame(const DukStackFrame&) = delete;
DukStackFrame(DukStackFrame&&) = delete;
};
inline duk_ret_t duk_json_decode_wrapper(duk_context* ctx, void*)
{
duk_json_decode(ctx, -1);
return 1;
}
inline std::optional<DukValue> DuktapeTryParseJson(duk_context* ctx, const std::string_view& json)
{
duk_push_lstring(ctx, json.data(), json.size());
if (duk_safe_call(ctx, duk_json_decode_wrapper, nullptr, 1, 1) == DUK_EXEC_SUCCESS)
{
return DukValue::take_from_stack(ctx);
}
else
{
// Pop error off stack
duk_pop(ctx);
return std::nullopt;
}
}
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,156 @@
/*****************************************************************************
* 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.
*****************************************************************************/
#ifdef ENABLE_SCRIPTING
# include "HookEngine.h"
# include "ScriptEngine.h"
# include <unordered_map>
using namespace OpenRCT2::Scripting;
HOOK_TYPE OpenRCT2::Scripting::GetHookType(const std::string& name)
{
static const std::unordered_map<std::string, HOOK_TYPE> LookupTable({
{ "action.query", HOOK_TYPE::ACTION_QUERY },
{ "action.execute", HOOK_TYPE::ACTION_EXECUTE },
{ "interval.tick", HOOK_TYPE::INTERVAL_TICK },
{ "interval.day", HOOK_TYPE::INTERVAL_DAY },
{ "network.chat", HOOK_TYPE::NETWORK_CHAT },
});
auto result = LookupTable.find(name);
return (result != LookupTable.end()) ? result->second : HOOK_TYPE::UNDEFINED;
}
HookEngine::HookEngine(ScriptEngine& scriptEngine)
: _scriptEngine(scriptEngine)
{
_hookMap.resize(NUM_HOOK_TYPES);
for (size_t i = 0; i < NUM_HOOK_TYPES; i++)
{
_hookMap[i].Type = static_cast<HOOK_TYPE>(i);
}
}
uint32_t HookEngine::Subscribe(HOOK_TYPE type, std::shared_ptr<Plugin> owner, const DukValue& function)
{
auto& hookList = GetHookList(type);
auto cookie = _nextCookie++;
Hook hook(cookie, owner, function);
hookList.Hooks.push_back(hook);
return cookie;
}
void HookEngine::Unsubscribe(HOOK_TYPE type, uint32_t cookie)
{
auto& hookList = GetHookList(type);
auto& hooks = hookList.Hooks;
for (auto it = hooks.begin(); it != hooks.end(); it++)
{
if (it->Cookie == cookie)
{
hooks.erase(it);
break;
}
}
}
void HookEngine::UnsubscribeAll(std::shared_ptr<const Plugin> owner)
{
for (auto& hookList : _hookMap)
{
auto& hooks = hookList.Hooks;
auto isOwner = [&](auto& obj) { return obj.Owner == owner; };
hooks.erase(std::remove_if(hooks.begin(), hooks.end(), isOwner), hooks.end());
}
}
void HookEngine::UnsubscribeAll()
{
for (auto& hookList : _hookMap)
{
auto& hooks = hookList.Hooks;
hooks.clear();
}
}
bool HookEngine::HasSubscriptions(HOOK_TYPE type) const
{
auto& hookList = GetHookList(type);
return !hookList.Hooks.empty();
}
void HookEngine::Call(HOOK_TYPE type, bool isGameStateMutable)
{
auto& hookList = GetHookList(type);
for (auto& hook : hookList.Hooks)
{
_scriptEngine.ExecutePluginCall(hook.Owner, hook.Function, {}, isGameStateMutable);
}
}
void HookEngine::Call(HOOK_TYPE type, const DukValue& arg, bool isGameStateMutable)
{
auto& hookList = GetHookList(type);
for (auto& hook : hookList.Hooks)
{
_scriptEngine.ExecutePluginCall(hook.Owner, hook.Function, { arg }, isGameStateMutable);
}
}
void HookEngine::Call(
HOOK_TYPE type, const std::initializer_list<std::pair<std::string_view, std::any>>& args, bool isGameStateMutable)
{
auto& hookList = GetHookList(type);
for (auto& hook : hookList.Hooks)
{
auto ctx = _scriptEngine.GetContext();
// Convert key/value pairs into an object
auto objIdx = duk_push_object(ctx);
for (const auto& arg : args)
{
if (arg.second.type() == typeid(int32_t))
{
auto val = std::any_cast<int32_t>(arg.second);
duk_push_int(ctx, val);
}
else if (arg.second.type() == typeid(std::string))
{
const auto& val = std::any_cast<std::string>(arg.second);
duk_push_string(ctx, val.c_str());
}
else
{
throw std::runtime_error("Not implemented");
}
duk_put_prop_string(ctx, objIdx, arg.first.data());
}
std::vector<DukValue> dukArgs;
dukArgs.push_back(DukValue::take_from_stack(ctx));
_scriptEngine.ExecutePluginCall(hook.Owner, hook.Function, dukArgs, isGameStateMutable);
}
}
HookList& HookEngine::GetHookList(HOOK_TYPE type)
{
auto index = static_cast<size_t>(type);
return _hookMap[index];
}
const HookList& HookEngine::GetHookList(HOOK_TYPE type) const
{
auto index = static_cast<size_t>(type);
return _hookMap[index];
}
#endif

View File

@ -0,0 +1,93 @@
/*****************************************************************************
* 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 "../common.h"
# include "Duktape.hpp"
# include <any>
# include <memory>
# include <string>
# include <tuple>
# include <vector>
namespace OpenRCT2::Scripting
{
class ScriptEngine;
class ScriptExecutionInfo;
class Plugin;
enum class HOOK_TYPE
{
ACTION_QUERY,
ACTION_EXECUTE,
INTERVAL_TICK,
INTERVAL_DAY,
NETWORK_CHAT,
COUNT,
UNDEFINED = -1,
};
constexpr size_t NUM_HOOK_TYPES = static_cast<size_t>(HOOK_TYPE::COUNT);
HOOK_TYPE GetHookType(const std::string& name);
struct Hook
{
uint32_t Cookie;
std::shared_ptr<Plugin> Owner;
DukValue Function;
Hook() = default;
Hook(uint32_t cookie, std::shared_ptr<Plugin> owner, const DukValue& function)
: Cookie(cookie)
, Owner(owner)
, Function(function)
{
}
};
struct HookList
{
HOOK_TYPE Type{};
std::vector<Hook> Hooks;
HookList() = default;
HookList(const HookList&) = delete;
HookList(HookList&& src) = default;
};
class HookEngine
{
private:
ScriptEngine& _scriptEngine;
std::vector<HookList> _hookMap;
uint32_t _nextCookie = 1;
public:
HookEngine(ScriptEngine& scriptEngine);
HookEngine(const HookEngine&) = delete;
uint32_t Subscribe(HOOK_TYPE type, std::shared_ptr<Plugin> owner, const DukValue& function);
void Unsubscribe(HOOK_TYPE type, uint32_t cookie);
void UnsubscribeAll(std::shared_ptr<const Plugin> owner);
void UnsubscribeAll();
bool HasSubscriptions(HOOK_TYPE type) const;
void Call(HOOK_TYPE type, bool isGameStateMutable);
void Call(HOOK_TYPE type, const DukValue& arg, bool isGameStateMutable);
void Call(
HOOK_TYPE type, const std::initializer_list<std::pair<std::string_view, std::any>>& args, bool isGameStateMutable);
private:
HookList& GetHookList(HOOK_TYPE type);
const HookList& GetHookList(HOOK_TYPE type) const;
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,164 @@
/*****************************************************************************
* 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.
*****************************************************************************/
#ifdef ENABLE_SCRIPTING
# include "Plugin.h"
# include "../OpenRCT2.h"
# include "Duktape.hpp"
# include <algorithm>
# include <fstream>
# include <memory>
using namespace OpenRCT2::Scripting;
Plugin::Plugin(duk_context* context, const std::string& path)
: _context(context)
, _path(path)
{
}
void Plugin::SetCode(const std::string_view& code)
{
_code = code;
}
void Plugin::Load()
{
if (!_path.empty())
{
LoadCodeFromFile();
}
std::string projectedVariables = "console,context,date,map,network,park";
if (!gOpenRCT2Headless)
{
projectedVariables += ",ui";
}
// Wrap the script in a function and pass the global objects as variables
// so that if the script modifies them, they are not modified for other scripts.
// clang-format off
auto code = _code;
code =
" (function(" + projectedVariables + ") {"
" var __metadata__ = null;"
" var registerPlugin = function(m) { __metadata__ = m };"
" (function(__metadata__) {"
+ code +
" })();"
" return __metadata__;"
" })(" + projectedVariables + ");";
// clang-format on
auto flags = DUK_COMPILE_EVAL | DUK_COMPILE_SAFE | DUK_COMPILE_NOSOURCE | DUK_COMPILE_NOFILENAME;
auto result = duk_eval_raw(_context, code.c_str(), code.size(), flags);
if (result != DUK_ERR_NONE)
{
auto val = std::string(duk_safe_to_string(_context, -1));
duk_pop(_context);
throw std::runtime_error("Failed to load plug-in script: " + val);
}
_metadata = GetMetadata(DukValue::take_from_stack(_context));
}
void Plugin::Start()
{
const auto& mainFunc = _metadata.Main;
if (mainFunc.context() == nullptr)
{
throw std::runtime_error("No main function specified.");
}
mainFunc.push();
auto result = duk_pcall(_context, 0);
if (result != DUK_ERR_NONE)
{
auto val = std::string(duk_safe_to_string(_context, -1));
duk_pop(_context);
throw std::runtime_error("[" + _metadata.Name + "] " + val);
}
duk_pop(_context);
_hasStarted = true;
}
void Plugin::Stop()
{
_hasStarted = false;
}
void Plugin::LoadCodeFromFile()
{
std::string code;
std::ifstream fs(_path);
if (fs.is_open())
{
fs.seekg(0, std::ios::end);
code.reserve(fs.tellg());
fs.seekg(0, std::ios::beg);
code.assign(std::istreambuf_iterator<char>(fs), std::istreambuf_iterator<char>());
}
_code = std::move(code);
}
static std::string TryGetString(const DukValue& value, const std::string& message)
{
if (value.type() != DukValue::Type::STRING)
throw std::runtime_error(message);
return value.as_string();
}
PluginMetadata Plugin::GetMetadata(const DukValue& dukMetadata)
{
PluginMetadata metadata;
if (dukMetadata.type() == DukValue::Type::OBJECT)
{
metadata.Name = TryGetString(dukMetadata["name"], "Plugin name not specified.");
metadata.Version = TryGetString(dukMetadata["version"], "Plugin version not specified.");
metadata.Type = ParsePluginType(TryGetString(dukMetadata["type"], "Plugin type not specified."));
auto dukMinApiVersion = dukMetadata["minApiVersion"];
if (dukMinApiVersion.type() == DukValue::Type::NUMBER)
{
metadata.MinApiVersion = dukMinApiVersion.as_int();
}
auto dukAuthors = dukMetadata["authors"];
dukAuthors.push();
if (dukAuthors.is_array())
{
auto elements = dukAuthors.as_array();
std::transform(elements.begin(), elements.end(), std::back_inserter(metadata.Authors), [](const DukValue& v) {
return v.as_string();
});
}
else if (dukAuthors.type() == DukValue::Type::STRING)
{
metadata.Authors = { dukAuthors.as_string() };
}
metadata.Main = dukMetadata["main"];
}
return metadata;
}
PluginType Plugin::ParsePluginType(const std::string_view& type)
{
if (type == "local")
return PluginType::Local;
if (type == "remote")
return PluginType::Remote;
throw std::invalid_argument("Unknown plugin type.");
}
#endif

View File

@ -0,0 +1,101 @@
/*****************************************************************************
* 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 "Duktape.hpp"
# include <memory>
# include <string>
# include <string_view>
# include <vector>
namespace OpenRCT2::Scripting
{
enum class PluginType
{
/**
* Scripts that can run on servers or clients with no impact on the game state and will not
* be uploaded to clients.
*/
Local,
/**
* Scripts that can run on servers and will be uploaded to clients with ability to
* modify game state in certain contexts.
*/
Remote,
};
struct PluginMetadata
{
std::string Name;
std::string Version;
std::vector<std::string> Authors;
PluginType Type{};
int32_t MinApiVersion{};
DukValue Main;
};
class Plugin
{
private:
duk_context* _context{};
std::string _path;
PluginMetadata _metadata{};
std::string _code;
bool _hasStarted{};
public:
std::string GetPath() const
{
return _path;
};
bool HasPath() const
{
return !_path.empty();
}
const PluginMetadata& GetMetadata() const
{
return _metadata;
}
const std::string& GetCode() const
{
return _code;
}
bool HasStarted() const
{
return _hasStarted;
}
Plugin() = default;
Plugin(duk_context* context, const std::string& path);
Plugin(const Plugin&) = delete;
Plugin(Plugin&&) = delete;
void SetCode(const std::string_view& code);
void Load();
void Start();
void Stop();
private:
void LoadCodeFromFile();
static PluginMetadata GetMetadata(const DukValue& dukMetadata);
static PluginType ParsePluginType(const std::string_view& type);
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,257 @@
/*****************************************************************************
* 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 "../Context.h"
# include "../config/Config.h"
# include "Duktape.hpp"
# include "ScriptEngine.h"
namespace OpenRCT2::Scripting
{
class ScConfiguration
{
private:
bool _isUserConfig{ true };
DukValue _backingObject;
public:
ScConfiguration() = default;
ScConfiguration(const DukValue& backingObject)
: _backingObject(backingObject)
{
}
static void Register(duk_context* ctx)
{
dukglue_register_method(ctx, &ScConfiguration::getAll, "getAll");
dukglue_register_method(ctx, &ScConfiguration::get, "get");
dukglue_register_method(ctx, &ScConfiguration::set, "set");
dukglue_register_method(ctx, &ScConfiguration::has, "has");
}
private:
std::pair<std::string_view, std::string_view> GetNextNamespace(const std::string_view& input) const
{
auto pos = input.find('.');
if (pos == std::string_view::npos)
{
return std::make_pair(input, std::string_view());
}
else
{
return std::make_pair(input.substr(0, pos), input.substr(pos + 1));
}
}
std::pair<std::string_view, std::string_view> GetNamespaceAndKey(const std::string_view& input) const
{
auto pos = input.find_last_of('.');
return pos == std::string_view::npos ? std::make_pair(std::string_view(), input)
: std::make_pair(input.substr(0, pos), input.substr(pos + 1));
}
std::optional<DukValue> GetNamespaceObject(const std::string_view& ns) const
{
auto store = _backingObject;
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;
}
DukValue GetOrCreateNamespaceObject(duk_context* ctx, const std::string_view& ns) const
{
auto store = _backingObject;
if (!ns.empty())
{
std::string_view k = ns;
bool end;
do
{
auto [next, remainder] = GetNextNamespace(k);
auto subStore = store[next];
k = remainder;
if (subStore.type() == DukValue::Type::UNDEFINED)
{
store.push();
duk_push_object(ctx);
store = DukValue::copy_from_stack(ctx);
duk_put_prop_lstring(ctx, -2, next.data(), next.size());
duk_pop(ctx);
}
else
{
store = subStore;
}
end = remainder.empty();
} while (!end);
}
return store;
}
bool IsValidNamespace(const std::string_view& ns) const
{
if (ns.empty() || ns[0] == '.' || ns[ns.size() - 1] == '.')
{
return false;
}
for (size_t i = 1; i < ns.size() - 1; i++)
{
if (ns[i - 1] == '.' && ns[i] == '.')
{
return false;
}
}
return true;
}
bool IsValidKey(const std::string_view& key) const
{
return !key.empty() && key.find('.') == std::string_view::npos;
}
DukValue getAll(const std::string& ns) const
{
DukValue result;
auto ctx = GetContext()->GetScriptEngine().GetContext();
if (IsValidNamespace(ns))
{
if (_isUserConfig)
{
DukObject obj(ctx);
if (ns == "general")
{
obj.Set("general.showFps", gConfigGeneral.show_fps);
}
result = obj.Take();
}
else
{
auto obj = GetNamespaceObject(ns);
result = obj ? *obj : DukObject(ctx).Take();
}
}
else
{
duk_error(ctx, DUK_ERR_ERROR, "Namespace was invalid.");
}
return result;
}
DukValue get(const std::string& key, const DukValue& defaultValue) const
{
auto ctx = GetContext()->GetScriptEngine().GetContext();
if (_isUserConfig)
{
if (key == "general.showFps")
{
duk_push_boolean(ctx, gConfigGeneral.show_fps);
return DukValue::take_from_stack(ctx);
}
}
else
{
auto [ns, n] = GetNamespaceAndKey(key);
if (!IsValidNamespace(ns))
{
duk_error(ctx, DUK_ERR_ERROR, "Namespace was invalid.");
}
else if (!IsValidKey(n))
{
duk_error(ctx, DUK_ERR_ERROR, "Key was invalid.");
}
else
{
auto obj = GetNamespaceObject(ns);
if (obj)
{
auto val = (*obj)[n];
if (val.type() != DukValue::Type::UNDEFINED)
{
return val;
}
}
}
}
return defaultValue;
}
void set(const std::string& key, const DukValue& value) const
{
auto& scriptEngine = GetContext()->GetScriptEngine();
auto ctx = scriptEngine.GetContext();
if (_isUserConfig)
{
try
{
if (key == "general.showFps")
{
gConfigGeneral.show_fps = value.as_bool();
}
else
{
duk_error(ctx, DUK_ERR_ERROR, "Property does not exist.");
}
}
catch (const DukException&)
{
duk_error(ctx, DUK_ERR_ERROR, "Invalid value for this property.");
}
}
else
{
auto [ns, n] = GetNamespaceAndKey(key);
if (!IsValidNamespace(ns))
{
duk_error(ctx, DUK_ERR_ERROR, "Namespace was invalid.");
}
else if (!IsValidKey(n))
{
duk_error(ctx, DUK_ERR_ERROR, "Key was invalid.");
}
else
{
auto obj = GetOrCreateNamespaceObject(ctx, ns);
obj.push();
if (value.type() == DukValue::Type::UNDEFINED)
{
duk_del_prop_string(ctx, -1, key.c_str());
}
else
{
value.push();
duk_put_prop_lstring(ctx, -2, n.data(), n.size());
}
duk_pop(ctx);
scriptEngine.SaveSharedStorage();
}
}
}
bool has(const std::string& key) const
{
auto val = get(key, DukValue());
return val.type() != DukValue::Type::UNDEFINED;
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,62 @@
/*****************************************************************************
* 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 "../interface/InteractiveConsole.h"
# include "Duktape.hpp"
# include "ScriptEngine.h"
namespace OpenRCT2::Scripting
{
class ScConsole
{
private:
InteractiveConsole& _console;
public:
ScConsole(InteractiveConsole& console)
: _console(console)
{
}
void clear()
{
_console.Clear();
}
duk_ret_t log(duk_context* ctx)
{
std::string line;
auto nargs = duk_get_top(ctx);
for (duk_idx_t i = 0; i < nargs; i++)
{
auto arg = DukValue::copy_from_stack(ctx, i);
auto argsz = Stringify(arg);
if (i != 0)
{
line.push_back(' ');
}
line += argsz;
}
_console.WriteLine(line);
return 0;
}
static void Register(duk_context* ctx)
{
dukglue_register_method(ctx, &ScConsole::clear, "clear");
dukglue_register_method_varargs(ctx, &ScConsole::log, "log");
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,300 @@
/*****************************************************************************
* 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 "../actions/CustomAction.hpp"
# include "../actions/ParkSetNameAction.hpp"
# include "../actions/SmallSceneryPlaceAction.hpp"
# include "../object/ObjectManager.h"
# include "../scenario/Scenario.h"
# include "Duktape.hpp"
# include "HookEngine.h"
# include "ScConfiguration.hpp"
# include "ScDisposable.hpp"
# include "ScObject.hpp"
# include "ScriptEngine.h"
# include <cstdio>
# include <memory>
namespace OpenRCT2::Scripting
{
class ScContext
{
private:
ScriptExecutionInfo& _execInfo;
HookEngine& _hookEngine;
public:
ScContext(ScriptExecutionInfo& execInfo, HookEngine& hookEngine)
: _execInfo(execInfo)
, _hookEngine(hookEngine)
{
}
private:
std::shared_ptr<ScConfiguration> configuration_get()
{
return std::make_shared<ScConfiguration>();
}
std::shared_ptr<ScConfiguration> sharedStorage_get()
{
auto& scriptEngine = GetContext()->GetScriptEngine();
return std::make_shared<ScConfiguration>(scriptEngine.GetSharedStorage());
}
static DukValue CreateScObject(duk_context* ctx, uint8_t type, int32_t index)
{
switch (type)
{
case OBJECT_TYPE_RIDE:
return GetObjectAsDukValue(ctx, std::make_shared<ScRideObject>(type, index));
default:
return GetObjectAsDukValue(ctx, std::make_shared<ScObject>(type, index));
}
}
DukValue getObject(const std::string& typez, int32_t index) const
{
auto ctx = GetContext()->GetScriptEngine().GetContext();
auto& objManager = GetContext()->GetObjectManager();
auto type = ScObject::StringToObjectType(typez);
if (type)
{
auto obj = objManager.GetLoadedObject(*type, index);
if (obj != nullptr)
{
return CreateScObject(ctx, *type, index);
}
}
else
{
duk_error(ctx, DUK_ERR_ERROR, "Invalid object type.");
}
return {};
}
std::vector<DukValue> getAllObjects(const std::string& typez) const
{
auto ctx = GetContext()->GetScriptEngine().GetContext();
auto& objManager = GetContext()->GetObjectManager();
std::vector<DukValue> result;
auto type = ScObject::StringToObjectType(typez);
if (type)
{
auto count = object_entry_group_counts[*type];
for (int32_t i = 0; i < count; i++)
{
auto obj = objManager.GetLoadedObject(*type, i);
if (obj != nullptr)
{
result.push_back(CreateScObject(ctx, *type, i));
}
}
}
else
{
duk_error(ctx, DUK_ERR_ERROR, "Invalid object type.");
}
return result;
}
int32_t getRandom(int32_t min, int32_t max)
{
ThrowIfGameStateNotMutable();
if (min >= max)
return min;
int32_t range = max - min;
return min + scenario_rand_max(range);
}
std::shared_ptr<ScDisposable> subscribe(const std::string& hook, const DukValue& callback)
{
auto hookType = GetHookType(hook);
if (hookType == HOOK_TYPE::UNDEFINED)
{
throw DukException() << "Unknown hook type: " << hook;
}
if (!callback.is_function())
{
throw DukException() << "Expected function for callback";
}
auto owner = _execInfo.GetCurrentPlugin();
if (owner == nullptr)
{
throw DukException() << "Not in a plugin context";
}
auto cookie = _hookEngine.Subscribe(hookType, owner, callback);
return std::make_shared<ScDisposable>([this, hookType, cookie]() { _hookEngine.Unsubscribe(hookType, cookie); });
}
void queryAction(const std::string& action, const DukValue& args, const DukValue& callback)
{
QueryOrExecuteAction(action, args, callback, false);
}
void executeAction(const std::string& action, const DukValue& args, const DukValue& callback)
{
QueryOrExecuteAction(action, args, callback, true);
}
void QueryOrExecuteAction(const std::string& actionid, const DukValue& args, const DukValue& callback, bool isExecute)
{
auto& scriptEngine = GetContext()->GetScriptEngine();
auto ctx = scriptEngine.GetContext();
try
{
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.");
}
}
catch (DukException&)
{
duk_error(ctx, DUK_ERR_ERROR, "Invalid action parameters.");
}
}
std::unique_ptr<GameAction> CreateGameAction(const std::string& actionid, const DukValue& args)
{
if (actionid == "parksetname")
{
auto name = args["name"].as_string();
return std::make_unique<ParkSetNameAction>(name);
}
else if (actionid == "smallsceneryplace")
{
CoordsXYZD loc;
loc.x = args["x"].as_int();
loc.y = args["y"].as_int();
loc.z = args["z"].as_int();
loc.direction = args["direction"].as_int();
uint8_t quadrant = args["quadrant"].as_int();
uint8_t sceneryType = args["object"].as_int();
uint8_t primaryColour = args["primaryColour"].as_int();
uint8_t secondaryColour = args["secondaryColour"].as_int();
return std::make_unique<SmallSceneryPlaceAction>(loc, quadrant, sceneryType, primaryColour, secondaryColour);
}
else
{
// Serialise args to json so that it can be sent
auto ctx = args.context();
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);
return std::make_unique<CustomAction>(actionid, json);
}
}
void HandleGameActionResult(
const std::shared_ptr<Plugin>& plugin, const GameActionResult& res, const DukValue& callback)
{
// Construct result object
auto& scriptEngine = GetContext()->GetScriptEngine();
auto ctx = scriptEngine.GetContext();
auto objIdx = duk_push_object(ctx);
duk_push_int(ctx, static_cast<duk_int_t>(res.Error));
duk_put_prop_string(ctx, objIdx, "error");
if (res.Error != GA_ERROR::OK)
{
auto title = res.GetErrorTitle();
duk_push_string(ctx, title.c_str());
duk_put_prop_string(ctx, objIdx, "errorTitle");
auto message = res.GetErrorMessage();
duk_push_string(ctx, message.c_str());
duk_put_prop_string(ctx, objIdx, "errorMessage");
}
duk_push_int(ctx, static_cast<duk_int_t>(res.Cost));
duk_put_prop_string(ctx, objIdx, "cost");
duk_push_int(ctx, static_cast<duk_int_t>(res.Expenditure));
duk_put_prop_string(ctx, objIdx, "expenditureType");
auto args = DukValue::take_from_stack(ctx);
if (callback.is_function())
{
// Call the plugin callback and pass the result object
scriptEngine.ExecutePluginCall(plugin, callback, { args }, false);
}
}
void registerAction(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:
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScContext::configuration_get, nullptr, "configuration");
dukglue_register_property(ctx, &ScContext::sharedStorage_get, nullptr, "sharedStorage");
dukglue_register_method(ctx, &ScContext::getObject, "getObject");
dukglue_register_method(ctx, &ScContext::getAllObjects, "getAllObjects");
dukglue_register_method(ctx, &ScContext::getRandom, "getRandom");
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::registerAction, "registerAction");
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,102 @@
/*****************************************************************************
* 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 "../Context.h"
# include "../Date.h"
# include "../Game.h"
# include "../GameState.h"
# include "../common.h"
# include "../localisation/Date.h"
# include "Duktape.hpp"
# include "ScriptEngine.h"
namespace OpenRCT2::Scripting
{
class ScDate
{
public:
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScDate::monthsElapsed_get, &ScDate::monthsElapsed_set, "monthsElapsed");
dukglue_register_property(ctx, &ScDate::monthProgress_get, &ScDate::monthProgress_set, "monthProgress");
dukglue_register_property(ctx, &ScDate::yearsElapsed_get, nullptr, "yearsElapsed");
dukglue_register_property(ctx, &ScDate::ticksElapsed_get, nullptr, "ticksElapsed");
dukglue_register_property(ctx, &ScDate::day_get, nullptr, "day");
dukglue_register_property(ctx, &ScDate::month_get, nullptr, "month");
dukglue_register_property(ctx, &ScDate::year_get, nullptr, "year");
}
private:
uint32_t monthsElapsed_get() const
{
const auto& date = GetDate();
return date.GetMonthsElapsed();
}
void monthsElapsed_set(uint32_t value)
{
ThrowIfGameStateNotMutable();
gDateMonthsElapsed = value;
}
uint32_t monthProgress_get() const
{
const auto& date = GetDate();
return date.GetMonthTicks();
}
void monthProgress_set(int32_t value)
{
ThrowIfGameStateNotMutable();
gDateMonthTicks = value;
}
uint32_t yearsElapsed_get() const
{
const auto& date = GetDate();
return date.GetMonthsElapsed() / 8;
}
uint32_t ticksElapsed_get() const
{
return gCurrentTicks;
}
int32_t day_get() const
{
const auto& date = GetDate();
return date.GetDay() + 1;
}
int32_t month_get() const
{
const auto& date = GetDate();
return date.GetMonth();
}
int32_t year_get() const
{
const auto& date = GetDate();
return date.GetYear() + 1;
}
private:
const Date& GetDate() const
{
auto gameState = GetContext()->GetGameState();
return gameState->GetDate();
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,46 @@
/*****************************************************************************
* 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 "Duktape.hpp"
# include <functional>
namespace OpenRCT2::Scripting
{
class ScDisposable
{
private:
std::function<void()> _onDispose;
public:
ScDisposable(const std::function<void()>& onDispose)
: _onDispose(onDispose)
{
}
void dispose() const
{
if (_onDispose)
{
_onDispose();
}
}
static void Register(duk_context* ctx)
{
dukglue_register_method(ctx, &ScDisposable::dispose, "dispose");
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,177 @@
/*****************************************************************************
* 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 "../common.h"
# include "../world/Sprite.h"
# include "Duktape.hpp"
# include "ScriptEngine.h"
namespace OpenRCT2::Scripting
{
class ScEntity
{
protected:
uint16_t _id = SPRITE_INDEX_NULL;
public:
ScEntity(uint16_t id)
: _id(id)
{
}
private:
std::string type_get() const
{
auto entity = GetEntity();
if (entity != nullptr)
{
switch (entity->sprite_identifier)
{
case SPRITE_IDENTIFIER_VEHICLE:
return "car";
case SPRITE_IDENTIFIER_PEEP:
return "peep";
case SPRITE_IDENTIFIER_MISC:
switch (entity->type)
{
case SPRITE_MISC_BALLOON:
return "balloon";
case SPRITE_MISC_DUCK:
return "duck";
}
break;
case SPRITE_IDENTIFIER_LITTER:
return "litter";
}
}
return "unknown";
}
// x getter and setter
int32_t x_get() const
{
auto entity = GetEntity();
return entity != nullptr ? entity->x : 0;
}
void x_set(int32_t value)
{
ThrowIfGameStateNotMutable();
auto entity = GetEntity();
if (entity != nullptr)
{
sprite_move(value, entity->y, entity->z, entity);
}
}
// y getter and setter
int32_t y_get() const
{
auto entity = GetEntity();
return entity != nullptr ? entity->y : 0;
}
void y_set(int32_t value)
{
ThrowIfGameStateNotMutable();
auto entity = GetEntity();
if (entity != nullptr)
{
sprite_move(entity->x, value, entity->z, entity);
}
}
// z getter and setter
int16_t z_get() const
{
auto entity = GetEntity();
return entity != nullptr ? entity->z : 0;
}
void z_set(int16_t value)
{
ThrowIfGameStateNotMutable();
auto entity = GetEntity();
if (entity != nullptr)
{
sprite_move(entity->x, entity->y, value, entity);
}
}
SpriteBase* GetEntity() const
{
return &get_sprite(_id)->generic;
}
public:
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScEntity::type_get, nullptr, "type");
dukglue_register_property(ctx, &ScEntity::x_get, &ScEntity::x_set, "x");
dukglue_register_property(ctx, &ScEntity::y_get, &ScEntity::y_set, "y");
dukglue_register_property(ctx, &ScEntity::z_get, &ScEntity::z_set, "z");
}
};
class ScPeep : public ScEntity
{
public:
ScPeep(uint16_t id)
: ScEntity(id)
{
}
private:
uint8_t tshirtColour_get() const
{
auto peep = GetPeep();
return peep != nullptr ? peep->tshirt_colour : 0;
}
void tshirtColour_set(uint8_t value)
{
ThrowIfGameStateNotMutable();
auto peep = GetPeep();
if (peep != nullptr)
{
peep->tshirt_colour = value;
}
}
uint8_t trousersColour_get() const
{
auto peep = GetPeep();
return peep != nullptr ? peep->trousers_colour : 0;
}
void trousersColour_set(uint8_t value)
{
ThrowIfGameStateNotMutable();
auto peep = GetPeep();
if (peep != nullptr)
{
peep->trousers_colour = value;
}
}
Peep* GetPeep() const
{
return get_sprite(_id)->AsPeep();
}
public:
static void Register(duk_context* ctx)
{
dukglue_set_base_class<ScEntity, ScPeep>(ctx);
dukglue_register_property(ctx, &ScPeep::tshirtColour_get, &ScPeep::tshirtColour_set, "tshirtColour");
dukglue_register_property(ctx, &ScPeep::trousersColour_get, &ScPeep::trousersColour_set, "trousersColour");
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,206 @@
/*****************************************************************************
* 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 "../common.h"
# include "../ride/Ride.h"
# include "../world/Map.h"
# include "Duktape.hpp"
# include "ScEntity.hpp"
# include "ScRide.hpp"
# include "ScTile.hpp"
namespace OpenRCT2::Scripting
{
class ScMap
{
private:
duk_context* _context;
public:
ScMap(duk_context* ctx)
: _context(ctx)
{
}
DukValue size_get() const
{
auto ctx = _context;
auto objIdx = duk_push_object(ctx);
duk_push_number(ctx, gMapSize);
duk_put_prop_string(ctx, objIdx, "x");
duk_push_number(ctx, gMapSize);
duk_put_prop_string(ctx, objIdx, "y");
return DukValue::take_from_stack(ctx);
}
int32_t numRides_get() const
{
return static_cast<int32_t>(GetRideManager().size());
}
int32_t numEntities_get() const
{
return MAX_SPRITES;
}
std::vector<std::shared_ptr<ScRide>> rides_get() const
{
std::vector<std::shared_ptr<ScRide>> result;
auto rideManager = GetRideManager();
for (const auto& ride : rideManager)
{
result.push_back(std::make_shared<ScRide>(ride.id));
}
return result;
}
std::shared_ptr<ScRide> getRide(int32_t id) const
{
auto rideManager = GetRideManager();
if (id >= 0 && id < static_cast<int32_t>(rideManager.size()))
{
auto ride = rideManager[static_cast<ride_id_t>(id)];
if (ride != nullptr)
{
return std::make_shared<ScRide>(ride->id);
}
}
return {};
}
std::shared_ptr<ScTile> getTile(int32_t x, int32_t y) const
{
auto coords = TileCoordsXY(x, y).ToCoordsXY();
return std::make_shared<ScTile>(coords);
}
DukValue getEntity(int32_t id) const
{
if (id >= 0 && id < MAX_SPRITES)
{
auto spriteId = static_cast<uint16_t>(id);
auto sprite = get_sprite(spriteId);
if (sprite != nullptr && sprite->generic.sprite_identifier != SPRITE_IDENTIFIER_NULL)
{
return GetEntityAsDukValue(sprite);
}
}
duk_push_null(_context);
return DukValue::take_from_stack(_context);
}
std::vector<DukValue> getAllEntities(const std::string& type) const
{
SPRITE_LIST targetList{};
uint8_t targetType{};
if (type == "balloon")
{
targetList = SPRITE_LIST_MISC;
targetType = SPRITE_MISC_BALLOON;
}
if (type == "car")
{
targetList = SPRITE_LIST_TRAIN_HEAD;
}
else if (type == "litter")
{
targetList = SPRITE_LIST_LITTER;
}
else if (type == "duck")
{
targetList = SPRITE_LIST_MISC;
targetType = SPRITE_MISC_DUCK;
}
else if (type == "peep")
{
targetList = SPRITE_LIST_PEEP;
}
else
{
duk_error(_context, DUK_ERR_ERROR, "Invalid entity type.");
}
std::vector<DukValue> result;
auto spriteId = gSpriteListHead[targetList];
while (spriteId != SPRITE_INDEX_NULL)
{
auto sprite = get_sprite(spriteId);
if (sprite == nullptr)
{
break;
}
else
{
// Only the misc list checks the type property
if (targetList != SPRITE_LIST_MISC || sprite->generic.type == targetType)
{
if (targetList == SPRITE_LIST_PEEP)
{
result.push_back(GetObjectAsDukValue(_context, std::make_shared<ScPeep>(spriteId)));
}
else if (targetList == SPRITE_LIST_TRAIN_HEAD)
{
auto carId = spriteId;
while (carId != SPRITE_INDEX_NULL)
{
auto car = get_sprite(carId);
if (car == nullptr)
{
break;
}
else
{
result.push_back(GetObjectAsDukValue(_context, std::make_shared<ScEntity>(carId)));
carId = car->vehicle.next_vehicle_on_train;
}
}
}
else
{
result.push_back(GetObjectAsDukValue(_context, std::make_shared<ScEntity>(spriteId)));
}
}
spriteId = sprite->generic.next;
}
}
return result;
}
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScMap::size_get, nullptr, "size");
dukglue_register_property(ctx, &ScMap::numRides_get, nullptr, "numRides");
dukglue_register_property(ctx, &ScMap::numEntities_get, nullptr, "numEntities");
dukglue_register_property(ctx, &ScMap::rides_get, nullptr, "rides");
dukglue_register_method(ctx, &ScMap::getRide, "getRide");
dukglue_register_method(ctx, &ScMap::getTile, "getTile");
dukglue_register_method(ctx, &ScMap::getEntity, "getEntity");
dukglue_register_method(ctx, &ScMap::getAllEntities, "getAllEntities");
}
private:
DukValue GetEntityAsDukValue(const rct_sprite* sprite) const
{
auto spriteId = sprite->generic.sprite_index;
switch (sprite->generic.sprite_identifier)
{
case SPRITE_IDENTIFIER_PEEP:
return GetObjectAsDukValue(_context, std::make_shared<ScPeep>(spriteId));
default:
return GetObjectAsDukValue(_context, std::make_shared<ScEntity>(spriteId));
}
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,373 @@
/*****************************************************************************
* 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 "../actions/NetworkModifyGroupAction.hpp"
# include "../actions/PlayerKickAction.hpp"
# include "../actions/PlayerSetGroupAction.hpp"
# include "../network/NetworkAction.h"
# include "../network/network.h"
# include "Duktape.hpp"
namespace OpenRCT2::Scripting
{
class ScPlayerGroup
{
private:
int32_t _id;
public:
ScPlayerGroup(int32_t id)
: _id(id)
{
}
int32_t id_get()
{
return _id;
}
std::string name_get() const
{
# ifndef DISABLE_NETWORK
auto index = network_get_group_index(_id);
if (index == -1)
return {};
return network_get_group_name(index);
# else
return {};
# endif
}
void name_set(std::string value)
{
# ifndef DISABLE_NETWORK
auto action = NetworkModifyGroupAction(ModifyGroupType::SetName, _id, value);
GameActions::Execute(&action);
# endif
}
std::vector<std::string> permissions_get() const
{
# ifndef DISABLE_NETWORK
auto index = network_get_group_index(_id);
if (index == -1)
return {};
// Create array of permissions
std::vector<std::string> result;
auto permissionIndex = 0;
for (const auto& action : NetworkActions::Actions)
{
if (network_can_perform_action(index, permissionIndex))
{
auto p = TransformPermissionKeyToJS(action.PermissionName);
result.push_back(p);
}
permissionIndex++;
}
return result;
# else
return {};
# endif
}
void permissions_set(std::vector<std::string> value)
{
# ifndef DISABLE_NETWORK
auto groupIndex = network_get_group_index(_id);
if (groupIndex == -1)
return;
// First clear all permissions
auto networkAction = NetworkModifyGroupAction(
ModifyGroupType::SetPermissions, _id, "", 0, PermissionState::ClearAll);
GameActions::Execute(&networkAction);
std::vector<bool> enabledPermissions;
enabledPermissions.resize(NetworkActions::Actions.size());
for (const auto& p : value)
{
auto permissionName = TransformPermissionKeyToInternal(p);
auto permissionIndex = 0;
for (const auto& action : NetworkActions::Actions)
{
if (action.PermissionName == permissionName)
{
enabledPermissions[permissionIndex] = true;
}
permissionIndex++;
}
}
for (size_t i = 0; i < enabledPermissions.size(); i++)
{
auto toggle = (enabledPermissions[i] != (network_can_perform_action(groupIndex, (uint32_t)i) != 0));
if (toggle)
{
auto networkAction2 = NetworkModifyGroupAction(
ModifyGroupType::SetPermissions, _id, "", (uint32_t)i, PermissionState::Toggle);
GameActions::Execute(&networkAction2);
}
}
# endif
}
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScPlayerGroup::id_get, nullptr, "id");
dukglue_register_property(ctx, &ScPlayerGroup::name_get, &ScPlayerGroup::name_set, "name");
dukglue_register_property(ctx, &ScPlayerGroup::permissions_get, &ScPlayerGroup::permissions_set, "permissions");
}
private:
static std::string TransformPermissionKeyToJS(const std::string& s)
{
auto result = s.substr(sizeof("PERMISSION_") - 1);
for (auto& c : result)
{
c = std::tolower(c);
}
return result;
}
static std::string TransformPermissionKeyToInternal(const std::string& s)
{
auto result = "PERMISSION_" + s;
for (auto& c : result)
{
c = std::toupper(c);
}
return result;
}
};
class ScPlayer
{
private:
int32_t _id;
public:
ScPlayer(int32_t id)
: _id(id)
{
}
int32_t id_get() const
{
return _id;
}
std::string name_get() const
{
# ifndef DISABLE_NETWORK
auto index = network_get_player_index(_id);
if (index == -1)
return {};
return network_get_player_name(index);
# else
return {};
# endif
}
int32_t group_get() const
{
# ifndef DISABLE_NETWORK
auto index = network_get_player_index(_id);
if (index == -1)
return {};
return network_get_player_group(index);
# else
return 0;
# endif
}
void group_set(int32_t value)
{
# ifndef DISABLE_NETWORK
auto playerSetGroupAction = PlayerSetGroupAction(_id, value);
GameActions::Execute(&playerSetGroupAction);
# endif
}
int32_t ping_get() const
{
# ifndef DISABLE_NETWORK
auto index = network_get_player_index(_id);
if (index == -1)
return {};
return network_get_player_ping(index);
# else
return 0;
# endif
}
int32_t commandsRan_get() const
{
# ifndef DISABLE_NETWORK
auto index = network_get_player_index(_id);
if (index == -1)
return {};
return network_get_player_commands_ran(index);
# else
return 0;
# endif
}
int32_t moneySpent_get() const
{
# ifndef DISABLE_NETWORK
auto index = network_get_player_index(_id);
if (index == -1)
return {};
return network_get_player_money_spent(index);
# else
return 0;
# endif
}
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScPlayer::id_get, nullptr, "id");
dukglue_register_property(ctx, &ScPlayer::name_get, nullptr, "name");
dukglue_register_property(ctx, &ScPlayer::group_get, &ScPlayer::group_set, "group");
dukglue_register_property(ctx, &ScPlayer::ping_get, nullptr, "ping");
dukglue_register_property(ctx, &ScPlayer::commandsRan_get, nullptr, "commandsRan");
dukglue_register_property(ctx, &ScPlayer::moneySpent_get, nullptr, "moneySpent");
}
};
class ScNetwork
{
private:
# ifdef __clang__
[[maybe_unused]]
# endif
duk_context* _context;
public:
ScNetwork(duk_context* ctx)
: _context(ctx)
{
}
std::string mode_get() const
{
# ifndef DISABLE_NETWORK
switch (network_get_mode())
{
case NETWORK_MODE_SERVER:
return "server";
case NETWORK_MODE_CLIENT:
return "client";
}
# endif
return "none";
}
int32_t players_get() const
{
# ifndef DISABLE_NETWORK
return network_get_num_players();
# else
return 0;
# endif
}
int32_t groups_get() const
{
# ifndef DISABLE_NETWORK
return network_get_num_groups();
# else
return 0;
# endif
}
int32_t defaultGroup_get() const
{
# ifndef DISABLE_NETWORK
return network_get_default_group();
# else
return 0;
# endif
}
void defaultGroup_set(int32_t value)
{
# ifndef DISABLE_NETWORK
auto action = NetworkModifyGroupAction(ModifyGroupType::SetDefault, value);
GameActions::Execute(&action);
# endif
}
std::shared_ptr<ScPlayer> getPlayer(int32_t index) const
{
# ifndef DISABLE_NETWORK
auto numPlayers = network_get_num_players();
if (index < numPlayers)
{
auto playerId = network_get_player_id(index);
return std::make_shared<ScPlayer>(playerId);
}
# endif
return nullptr;
}
std::shared_ptr<ScPlayerGroup> getGroup(int32_t index) const
{
# ifndef DISABLE_NETWORK
auto numGroups = network_get_num_groups();
if (index < numGroups)
{
auto groupId = network_get_group_id(index);
return std::make_shared<ScPlayerGroup>(groupId);
}
# endif
return nullptr;
}
void kickPlayer(int32_t index)
{
# ifndef DISABLE_NETWORK
auto numPlayers = network_get_num_players();
if (index < numPlayers)
{
auto playerId = network_get_player_id(index);
auto kickPlayerAction = PlayerKickAction(playerId);
GameActions::Execute(&kickPlayerAction);
}
# endif
}
void sendMessage(std::string message, DukValue players)
{
# ifndef DISABLE_NETWORK
if (players.is_array())
{
duk_error(players.context(), DUK_ERR_ERROR, "Not yet supported");
}
else
{
network_send_chat(message.c_str());
}
# endif
}
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScNetwork::mode_get, nullptr, "mode");
dukglue_register_property(ctx, &ScNetwork::groups_get, nullptr, "groups");
dukglue_register_property(ctx, &ScNetwork::players_get, nullptr, "players");
dukglue_register_property(ctx, &ScNetwork::defaultGroup_get, &ScNetwork::defaultGroup_set, "defaultGroup");
dukglue_register_method(ctx, &ScNetwork::getGroup, "getGroup");
dukglue_register_method(ctx, &ScNetwork::getPlayer, "getPlayer");
dukglue_register_method(ctx, &ScNetwork::kickPlayer, "kickPlayer");
dukglue_register_method(ctx, &ScNetwork::sendMessage, "sendMessage");
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,152 @@
/*****************************************************************************
* 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 "../Context.h"
# include "../common.h"
# include "../object/ObjectManager.h"
# include "../object/RideObject.h"
# include "Duktape.hpp"
# include "ScriptEngine.h"
# include <optional>
namespace OpenRCT2::Scripting
{
class ScObject
{
private:
uint8_t _type{};
int32_t _index{};
public:
ScObject(uint8_t type, int32_t index)
: _type(type)
, _index(index)
{
}
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScObject::type_get, nullptr, "type");
dukglue_register_property(ctx, &ScObject::index_get, nullptr, "index");
dukglue_register_property(ctx, &ScObject::identifier_get, nullptr, "identifier");
dukglue_register_property(ctx, &ScObject::name_get, nullptr, "name");
}
static std::optional<uint8_t> StringToObjectType(const std::string_view& type)
{
for (uint8_t i = 0; i < OBJECT_TYPE_COUNT; i++)
{
auto s = ObjectTypeToString(i);
if (s == type)
{
return i;
}
}
return std::nullopt;
}
static std::string_view ObjectTypeToString(uint8_t type)
{
static const char* Types[] = { "ride", "small_scenery", "large_scenery", "wall", "banner",
"footpath", "footpath_addition", "scenery_group", "park_entrance", "water",
"stex", "terrain_surface", "terrain_edge", "station", "music" };
if (type >= std::size(Types))
return "unknown";
return Types[type];
}
private:
std::string type_get() const
{
return std::string(ObjectTypeToString(_type));
}
int32_t index_get() const
{
return _index;
}
std::string identifier_get() const
{
auto obj = GetObject();
if (obj != nullptr)
{
return obj->GetIdentifier();
}
return {};
}
std::string name_get() const
{
auto obj = GetObject();
if (obj != nullptr)
{
return obj->GetName();
}
return {};
}
protected:
Object* GetObject() const
{
auto& objManager = GetContext()->GetObjectManager();
return objManager.GetLoadedObject(_type, _index);
}
};
class ScRideObject : public ScObject
{
public:
ScRideObject(uint8_t type, int32_t index)
: ScObject(type, index)
{
}
static void Register(duk_context* ctx)
{
dukglue_set_base_class<ScObject, ScRideObject>(ctx);
dukglue_register_property(ctx, &ScRideObject::description_get, nullptr, "description");
dukglue_register_property(ctx, &ScRideObject::capacity_get, nullptr, "capacity");
}
private:
std::string description_get() const
{
auto obj = GetObject();
if (obj != nullptr)
{
return obj->GetDescription();
}
return {};
}
std::string capacity_get() const
{
auto obj = GetObject();
if (obj != nullptr)
{
return obj->GetCapacity();
}
return {};
}
protected:
RideObject* GetObject() const
{
return static_cast<RideObject*>(ScObject::GetObject());
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,130 @@
/*****************************************************************************
* Copyright (c) 2014-2018 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 "../Context.h"
# include "../common.h"
# include "../management/Finance.h"
# include "../management/NewsItem.h"
# include "../windows/Intent.h"
# include "../world/Park.h"
# include "Duktape.hpp"
# include "ScriptEngine.h"
# include <algorithm>
namespace OpenRCT2::Scripting
{
class ScPark
{
public:
money32 cash_get() const
{
return gCash;
}
void cash_set(money32 value)
{
ThrowIfGameStateNotMutable();
gCash = value;
auto intent = Intent(INTENT_ACTION_UPDATE_CASH);
context_broadcast_intent(&intent);
}
int32_t rating_get() const
{
return gParkRating;
}
void rating_set(int32_t value)
{
ThrowIfGameStateNotMutable();
gParkRating = std::min(std::max(0, value), 999);
auto intent = Intent(INTENT_ACTION_UPDATE_PARK_RATING);
context_broadcast_intent(&intent);
}
money32 bankLoan_get() const
{
return gBankLoan;
}
void bankLoan_set(money32 value)
{
ThrowIfGameStateNotMutable();
gBankLoan = value;
auto intent = Intent(INTENT_ACTION_UPDATE_CASH);
context_broadcast_intent(&intent);
}
money32 maxBankLoan_get() const
{
return gMaxBankLoan;
}
void maxBankLoan_set(money32 value)
{
ThrowIfGameStateNotMutable();
gMaxBankLoan = value;
auto intent = Intent(INTENT_ACTION_UPDATE_CASH);
context_broadcast_intent(&intent);
}
void postMessage(DukValue message)
{
ThrowIfGameStateNotMutable();
try
{
uint8_t type = NEWS_ITEM_BLANK;
std::string text;
if (message.type() == DukValue::Type::STRING)
{
text = message.as_string();
}
else
{
type = GetParkMessageType(message["type"].as_string());
text = message["text"].as_string();
}
news_item_add_to_queue_raw(type, text.c_str(), static_cast<uint32_t>(-1));
}
catch (const DukException&)
{
duk_error(message.context(), DUK_ERR_ERROR, "Invalid message argument.");
}
}
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScPark::cash_get, &ScPark::cash_set, "cash");
dukglue_register_property(ctx, &ScPark::rating_get, &ScPark::rating_set, "rating");
dukglue_register_property(ctx, &ScPark::bankLoan_get, &ScPark::bankLoan_set, "bankLoan");
dukglue_register_property(ctx, &ScPark::maxBankLoan_get, &ScPark::maxBankLoan_set, "maxBankLoan");
dukglue_register_method(ctx, &ScPark::postMessage, "postMessage");
}
private:
static uint8_t GetParkMessageType(const std::string& key)
{
static auto keys = { "attraction", "peep_on_attraction", "peep", "money", "blank", "research", "guests", "award",
"chart" };
uint8_t i = 0;
for (const auto& k : keys)
{
if (k == key)
{
return NEWS_ITEM_RIDE + i;
}
i++;
}
return NEWS_ITEM_BLANK;
}
};
} // namespace OpenRCT2::Scripting
#endif

View File

@ -0,0 +1,154 @@
/*****************************************************************************
* Copyright (c) 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 "../common.h"
# include "../ride/Ride.h"
# include "Duktape.hpp"
# include "ScObject.hpp"
# include "ScriptEngine.h"
namespace OpenRCT2::Scripting
{
class ScRide
{
private:
ride_id_t _rideId = RIDE_ID_NULL;
public:
ScRide(ride_id_t rideId)
: _rideId(rideId)
{
}
private:
int32_t id_get() const
{
return _rideId;
}
std::shared_ptr<ScRideObject> object_get()
{
auto ride = GetRide();
if (ride != nullptr)
{
auto rideObject = GetContext()->GetObjectManager().GetLoadedObject(OBJECT_TYPE_RIDE, ride->subtype);
if (rideObject != nullptr)
{
return std::make_shared<ScRideObject>(OBJECT_TYPE_RIDE, ride->subtype);
}
}
return nullptr;
}
int32_t type_get() const
{
auto ride = GetRide();
return ride != nullptr ? ride->type : 0;
}
std::string name_get() const
{
auto ride = GetRide();
return ride != nullptr ? ride->GetName() : std::string();
}
void name_set(std::string value)
{
ThrowIfGameStateNotMutable();
auto ride = GetRide();
if (ride != nullptr)
{
ride->custom_name = value;
}
}
int32_t excitement_get() const
{
auto ride = GetRide();
return ride != nullptr ? ride->excitement : 0;
}
void excitement_set(int32_t value)
{
ThrowIfGameStateNotMutable();
auto ride = GetRide();
if (ride != nullptr)
{
ride->excitement = value;
}
}
int32_t intensity_get() const
{
auto ride = GetRide();
return ride != nullptr ? ride->intensity : 0;
}
void intensity_set(int32_t value)
{
ThrowIfGameStateNotMutable();
auto ride = GetRide();
if (ride != nullptr)
{
ride->intensity = value;
}
}
int32_t nausea_get() const
{
auto ride = GetRide();
return ride != nullptr ? ride->nausea : 0;
}
void nausea_set(int32_t value)
{
ThrowIfGameStateNotMutable();
auto ride = GetRide();
if (ride != nullptr)
{
ride->nausea = value;
}
}
int32_t totalCustomers_get() const
{
auto ride = GetRide();
return ride != nullptr ? ride->total_customers : 0;
}
void totalCustomers_set(int32_t value)
{
ThrowIfGameStateNotMutable();
auto ride = GetRide();
if (ride != nullptr)
{
ride->total_customers = value;
}
}
Ride* GetRide() const
{
return get_ride(_rideId);
}
public:
static void Register(duk_context* ctx)
{
dukglue_register_property(ctx, &ScRide::id_get, nullptr, "id");
dukglue_register_property(ctx, &ScRide::object_get, nullptr, "object");
dukglue_register_property(ctx, &ScRide::type_get, nullptr, "type");
dukglue_register_property(ctx, &ScRide::name_get, &ScRide::name_set, "name");
dukglue_register_property(ctx, &ScRide::excitement_get, &ScRide::excitement_set, "excitement");
dukglue_register_property(ctx, &ScRide::intensity_get, &ScRide::intensity_set, "intensity");
dukglue_register_property(ctx, &ScRide::nausea_get, &ScRide::nausea_set, "nausea");
dukglue_register_property(ctx, &ScRide::totalCustomers_get, &ScRide::totalCustomers_set, "totalCustomers");
}
};
} // namespace OpenRCT2::Scripting
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,956 @@
/*****************************************************************************
* Copyright (c) 2014-2018 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.
*****************************************************************************/
#ifdef ENABLE_SCRIPTING
# include "ScriptEngine.h"
# include "../PlatformEnvironment.h"
# include "../actions/GameAction.h"
# include "../actions/RideCreateAction.hpp"
# include "../config/Config.h"
# include "../core/File.h"
# include "../core/FileScanner.h"
# include "../core/Path.hpp"
# include "../interface/InteractiveConsole.h"
# include "../platform/Platform2.h"
# include "Duktape.hpp"
# include "ScConsole.hpp"
# include "ScContext.hpp"
# include "ScDate.hpp"
# include "ScDisposable.hpp"
# include "ScEntity.hpp"
# include "ScMap.hpp"
# include "ScNetwork.hpp"
# include "ScObject.hpp"
# include "ScPark.hpp"
# include "ScRide.hpp"
# include "ScTile.hpp"
# include <iostream>
# include <stdexcept>
using namespace OpenRCT2;
using namespace OpenRCT2::Scripting;
static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 1;
struct ExpressionStringifier final
{
private:
std::stringstream _ss;
duk_context* _context{};
int32_t _indent{};
ExpressionStringifier(duk_context* ctx)
: _context(ctx)
{
}
void PushIndent(int32_t c = 1)
{
_indent += c;
}
void PopIndent(int32_t c = 1)
{
_indent -= c;
}
void LineFeed()
{
_ss << "\n" << std::string(_indent, ' ');
}
void Stringify(const DukValue& val, bool canStartWithNewLine)
{
switch (val.type())
{
case DukValue::Type::UNDEFINED:
_ss << "undefined";
break;
case DukValue::Type::NULLREF:
_ss << "null";
break;
case DukValue::Type::BOOLEAN:
StringifyBoolean(val);
break;
case DukValue::Type::NUMBER:
StringifyNumber(val);
break;
case DukValue::Type::STRING:
_ss << "'" << val.as_string() << "'";
break;
case DukValue::Type::OBJECT:
if (val.is_function())
{
StringifyFunction(val);
}
else if (val.is_array())
{
StringifyArray(val, canStartWithNewLine);
}
else
{
StringifyObject(val, canStartWithNewLine);
}
break;
case DukValue::Type::BUFFER:
_ss << "[Buffer]";
break;
case DukValue::Type::POINTER:
_ss << "[Pointer]";
break;
case DukValue::Type::LIGHTFUNC:
_ss << "[LightFunc]";
break;
}
}
void StringifyArray(const DukValue& val, bool canStartWithNewLine)
{
constexpr auto maxItemsToShow = 4;
val.push();
auto arrayLen = duk_get_length(_context, -1);
if (arrayLen == 0)
{
_ss << "[]";
}
else if (arrayLen == 1)
{
_ss << "[ ";
for (duk_uarridx_t i = 0; i < arrayLen; i++)
{
if (duk_get_prop_index(_context, -1, i))
{
if (i != 0)
{
_ss << ", ";
}
Stringify(DukValue::take_from_stack(_context), false);
}
}
_ss << " ]";
}
else
{
if (canStartWithNewLine)
{
PushIndent();
LineFeed();
}
_ss << "[ ";
PushIndent(2);
for (duk_uarridx_t i = 0; i < arrayLen; i++)
{
if (i != 0)
{
_ss << ",";
LineFeed();
}
if (i >= maxItemsToShow)
{
auto remainingItemsNotShown = arrayLen - maxItemsToShow;
if (remainingItemsNotShown == 1)
{
_ss << "... 1 more item";
}
else
{
_ss << "... " << std::to_string(remainingItemsNotShown) << " more items";
}
break;
}
else
{
if (duk_get_prop_index(_context, -1, i))
{
Stringify(DukValue::take_from_stack(_context), false);
}
}
}
_ss << " ]";
PopIndent(2);
if (canStartWithNewLine)
{
PopIndent();
}
}
duk_pop(_context);
}
void StringifyObject(const DukValue& val, bool canStartWithNewLine)
{
auto numEnumerables = GetNumEnumerablesOnObject(val);
if (numEnumerables == 0)
{
_ss << "{}";
}
else if (numEnumerables == 1)
{
_ss << "{ ";
val.push();
duk_enum(_context, -1, 0);
auto index = 0;
while (duk_next(_context, -1, 1))
{
if (index != 0)
{
_ss << ", ";
}
auto value = DukValue::take_from_stack(_context, -1);
auto key = DukValue::take_from_stack(_context, -1);
if (key.type() == DukValue::Type::STRING)
{
_ss << key.as_string() << ": ";
}
else
{
// For some reason the key was not a string
_ss << "?: ";
}
Stringify(value, true);
index++;
}
duk_pop_2(_context);
_ss << " }";
}
else
{
if (canStartWithNewLine)
{
PushIndent();
LineFeed();
}
_ss << "{ ";
PushIndent(2);
val.push();
duk_enum(_context, -1, 0);
auto index = 0;
while (duk_next(_context, -1, 1))
{
if (index != 0)
{
_ss << ",";
LineFeed();
}
auto value = DukValue::take_from_stack(_context, -1);
auto key = DukValue::take_from_stack(_context, -1);
if (key.type() == DukValue::Type::STRING)
{
_ss << key.as_string() << ": ";
}
else
{
// For some reason the key was not a string
_ss << "?: ";
}
Stringify(value, true);
index++;
}
duk_pop_2(_context);
PopIndent(2);
_ss << " }";
if (canStartWithNewLine)
{
PopIndent();
}
}
}
void StringifyFunction(const DukValue& val)
{
val.push();
if (duk_is_c_function(_context, -1))
{
_ss << "[Native Function]";
}
else if (duk_is_ecmascript_function(_context, -1))
{
_ss << "[ECMAScript Function]";
}
else
{
_ss << "[Function]";
}
duk_pop(_context);
}
void StringifyBoolean(const DukValue& val)
{
_ss << (val.as_bool() ? "true" : "false");
}
void StringifyNumber(const DukValue& val)
{
const auto d = val.as_double();
const duk_int_t i = val.as_int();
if (AlmostEqual<double>(d, i))
{
_ss << std::to_string(i);
}
else
{
_ss << std::to_string(d);
}
}
size_t GetNumEnumerablesOnObject(const DukValue& val)
{
size_t count = 0;
val.push();
duk_enum(_context, -1, 0);
while (duk_next(_context, -1, 0))
{
count++;
duk_pop(_context);
}
duk_pop_2(_context);
return count;
}
// Taken from http://en.cppreference.com/w/cpp/types/numeric_limits/epsilon
template<class T>
static typename std::enable_if<!std::numeric_limits<T>::is_integer, bool>::type AlmostEqual(T x, T y, int32_t ulp = 20)
{
// the machine epsilon has to be scaled to the magnitude of the values used
// and multiplied by the desired precision in ULPs (units in the last place)
return std::abs(x - y) <= std::numeric_limits<T>::epsilon() * std::abs(x + y) * ulp
// unless the result is subnormal
|| std::abs(x - y)
< (std::numeric_limits<T>::min)(); // TODO: Remove parentheses around min once the macro is removed
}
public:
static std::string StringifyExpression(const DukValue& val)
{
ExpressionStringifier instance(val.context());
instance.Stringify(val, false);
return instance._ss.str();
}
};
DukContext::DukContext()
{
_context = duk_create_heap_default();
if (_context == nullptr)
{
throw std::runtime_error("Unable to initialise duktape context.");
}
}
DukContext::~DukContext()
{
duk_destroy_heap(_context);
}
ScriptEngine::ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env)
: _console(console)
, _env(env)
, _hookEngine(*this)
{
}
void ScriptEngine::Initialise()
{
auto ctx = (duk_context*)_context;
ScConfiguration::Register(ctx);
ScConsole::Register(ctx);
ScContext::Register(ctx);
ScDate::Register(ctx);
ScDisposable::Register(ctx);
ScMap::Register(ctx);
ScNetwork::Register(ctx);
ScObject::Register(ctx);
ScPark::Register(ctx);
ScPlayer::Register(ctx);
ScPlayerGroup::Register(ctx);
ScRide::Register(ctx);
ScRideObject::Register(ctx);
ScTile::Register(ctx);
ScTileElement::Register(ctx);
ScEntity::Register(ctx);
ScPeep::Register(ctx);
dukglue_register_global(ctx, std::make_shared<ScConsole>(_console), "console");
dukglue_register_global(ctx, std::make_shared<ScContext>(_execInfo, _hookEngine), "context");
dukglue_register_global(ctx, std::make_shared<ScDate>(), "date");
dukglue_register_global(ctx, std::make_shared<ScMap>(ctx), "map");
dukglue_register_global(ctx, std::make_shared<ScNetwork>(ctx), "network");
dukglue_register_global(ctx, std::make_shared<ScPark>(), "park");
_initialised = true;
_pluginsLoaded = false;
_pluginsStarted = false;
InitSharedStorage();
}
void ScriptEngine::LoadPlugins()
{
if (!_initialised)
{
Initialise();
}
if (_pluginsLoaded)
{
UnloadPlugins();
}
auto base = _env.GetDirectoryPath(DIRBASE::USER, DIRID::PLUGIN);
if (Path::DirectoryExists(base))
{
auto pattern = Path::Combine(base, "*.js");
auto scanner = std::unique_ptr<IFileScanner>(Path::ScanDirectory(pattern, true));
while (scanner->Next())
{
auto path = std::string(scanner->GetPath());
if (ShouldLoadScript(path))
{
LoadPlugin(path);
}
}
if (gConfigPlugin.enable_hot_reloading && network_get_mode() == NETWORK_MODE_NONE)
{
SetupHotReloading();
}
}
_pluginsLoaded = true;
_pluginsStarted = false;
}
void ScriptEngine::LoadPlugin(const std::string& path)
{
auto plugin = std::make_shared<Plugin>(_context, path);
LoadPlugin(plugin);
}
void ScriptEngine::LoadPlugin(std::shared_ptr<Plugin>& plugin)
{
try
{
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
plugin->Load();
auto metadata = plugin->GetMetadata();
if (metadata.MinApiVersion <= OPENRCT2_PLUGIN_API_VERSION)
{
LogPluginInfo(plugin, "Loaded");
_plugins.push_back(std::move(plugin));
}
else
{
LogPluginInfo(plugin, "Requires newer API version: v" + std::to_string(metadata.MinApiVersion));
}
}
catch (const std::exception& e)
{
_console.WriteLineError(e.what());
}
}
void ScriptEngine::StopPlugin(std::shared_ptr<Plugin> plugin)
{
if (plugin->HasStarted())
{
RemoveCustomGameActions(plugin);
_hookEngine.UnsubscribeAll(plugin);
for (auto callback : _pluginStoppedSubscriptions)
{
callback(plugin);
}
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
try
{
plugin->Stop();
}
catch (const std::exception& e)
{
_console.WriteLineError(e.what());
}
}
}
bool ScriptEngine::ShouldLoadScript(const std::string& path)
{
// A lot of JavaScript is often found in a node_modules directory tree and is most likely unwanted, so ignore it
return path.find("/node_modules/") == std::string::npos && path.find("\\node_modules\\") == std::string::npos;
}
void ScriptEngine::SetupHotReloading()
{
try
{
auto base = _env.GetDirectoryPath(DIRBASE::USER, DIRID::PLUGIN);
_pluginFileWatcher = std::make_unique<FileWatcher>(base);
_pluginFileWatcher->OnFileChanged = [this](const std::string& path) {
std::lock_guard<std::mutex> guard(_changedPluginFilesMutex);
_changedPluginFiles.emplace(path);
};
}
catch (const std::exception& e)
{
std::fprintf(stderr, "Unable to enable hot reloading of plugins: %s\n", e.what());
}
}
void ScriptEngine::AutoReloadPlugins()
{
if (_changedPluginFiles.size() > 0)
{
std::lock_guard<std::mutex> guard(_changedPluginFilesMutex);
for (auto& path : _changedPluginFiles)
{
auto findResult = std::find_if(_plugins.begin(), _plugins.end(), [&path](const std::shared_ptr<Plugin>& plugin) {
return Path::Equals(path, plugin->GetPath());
});
if (findResult != _plugins.end())
{
auto& plugin = *findResult;
try
{
StopPlugin(plugin);
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
plugin->Load();
LogPluginInfo(plugin, "Reloaded");
plugin->Start();
}
catch (const std::exception& e)
{
_console.WriteLineError(e.what());
}
}
}
_changedPluginFiles.clear();
}
}
void ScriptEngine::UnloadPlugins()
{
StopPlugins();
for (auto& plugin : _plugins)
{
LogPluginInfo(plugin, "Unloaded");
}
_plugins.clear();
_pluginsLoaded = false;
_pluginsStarted = false;
}
void ScriptEngine::StartPlugins()
{
LoadSharedStorage();
for (auto& plugin : _plugins)
{
if (!plugin->HasStarted() && ShouldStartPlugin(plugin))
{
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
try
{
LogPluginInfo(plugin, "Started");
plugin->Start();
}
catch (const std::exception& e)
{
_console.WriteLineError(e.what());
}
}
}
_pluginsStarted = true;
}
bool ScriptEngine::ShouldStartPlugin(const std::shared_ptr<Plugin>& plugin)
{
auto networkMode = network_get_mode();
if (networkMode == NETWORK_MODE_CLIENT)
{
// Only client plugins and plugins downloaded from server should be started
const auto& metadata = plugin->GetMetadata();
if (metadata.Type == PluginType::Remote && plugin->HasPath())
{
LogPluginInfo(plugin, "Remote plugin not started");
return false;
}
}
return true;
}
void ScriptEngine::StopPlugins()
{
for (auto& plugin : _plugins)
{
if (plugin->HasStarted())
{
StopPlugin(plugin);
LogPluginInfo(plugin, "Stopped");
}
}
_pluginsStarted = false;
}
void ScriptEngine::Update()
{
if (!_initialised)
{
Initialise();
}
if (_pluginsLoaded)
{
if (!_pluginsStarted)
{
StartPlugins();
}
else
{
auto tick = Platform::GetTicks();
if (tick - _lastHotReloadCheckTick > 1000)
{
AutoReloadPlugins();
_lastHotReloadCheckTick = tick;
}
}
}
ProcessREPL();
}
void ScriptEngine::ProcessREPL()
{
while (_evalQueue.size() > 0)
{
auto item = std::move(_evalQueue.front());
_evalQueue.pop();
auto promise = std::move(std::get<0>(item));
auto command = std::move(std::get<1>(item));
if (duk_peval_string(_context, command.c_str()) != 0)
{
std::string result = std::string(duk_safe_to_string(_context, -1));
_console.WriteLineError(result);
}
else if (duk_get_type(_context, -1) != DUK_TYPE_UNDEFINED)
{
auto result = Stringify(DukValue::copy_from_stack(_context, -1));
_console.WriteLine(result);
}
duk_pop(_context);
// Signal the promise so caller can continue
promise.set_value();
}
}
std::future<void> ScriptEngine::Eval(const std::string& s)
{
std::promise<void> barrier;
auto future = barrier.get_future();
_evalQueue.emplace(std::move(barrier), s);
return future;
}
DukValue ScriptEngine::ExecutePluginCall(
const std::shared_ptr<Plugin>& plugin, const DukValue& func, const std::vector<DukValue>& args, bool isGameStateMutable)
{
DukStackFrame frame(_context);
if (func.is_function())
{
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, isGameStateMutable);
func.push();
for (const auto& arg : args)
{
arg.push();
}
auto result = duk_pcall(_context, static_cast<duk_idx_t>(args.size()));
if (result == DUK_EXEC_SUCCESS)
{
return DukValue::take_from_stack(_context);
}
else
{
auto message = duk_safe_to_string(_context, -1);
LogPluginInfo(plugin, message);
duk_pop(_context);
}
}
return DukValue();
}
void ScriptEngine::LogPluginInfo(const std::shared_ptr<Plugin>& plugin, const std::string_view& message)
{
const auto& pluginName = plugin->GetMetadata().Name;
_console.WriteLine("[" + pluginName + "] " + std::string(message));
}
void ScriptEngine::AddNetworkPlugin(const std::string_view& code)
{
auto plugin = std::make_shared<Plugin>(_context, std::string());
plugin->SetCode(code);
LoadPlugin(plugin);
}
std::unique_ptr<GameActionResult> ScriptEngine::QueryOrExecuteCustomGameAction(
const std::string_view& id, const std::string_view& args, bool isExecute)
{
std::string actionz = std::string(id);
auto kvp = _customActions.find(actionz);
if (kvp != _customActions.end())
{
const auto& customAction = kvp->second;
// Deserialise the JSON args
std::string argsz(args);
auto dukArgs = DuktapeTryParseJson(_context, argsz);
if (!dukArgs)
{
auto action = std::make_unique<GameActionResult>();
action->Error = GA_ERROR::INVALID_PARAMETERS;
action->ErrorTitle = "Invalid JSON";
return action;
}
// Ready to call plugin handler
DukValue dukResult;
if (!isExecute)
{
dukResult = ExecutePluginCall(customAction.Owner, customAction.Query, { *dukArgs }, false);
}
else
{
dukResult = ExecutePluginCall(customAction.Owner, customAction.Execute, { *dukArgs }, true);
}
return DukToGameActionResult(dukResult);
}
else
{
auto action = std::make_unique<GameActionResult>();
action->Error = GA_ERROR::UNKNOWN;
action->ErrorTitle = "Unknown custom action";
return action;
}
}
std::unique_ptr<GameActionResult> ScriptEngine::DukToGameActionResult(const DukValue& d)
{
auto result = std::make_unique<GameActionResult>();
result->Error = static_cast<GA_ERROR>(AsOrDefault<int32_t>(d["error"]));
result->ErrorTitle = AsOrDefault<std::string>(d["errorTitle"]);
result->ErrorMessage = AsOrDefault<std::string>(d["errorMessage"]);
result->Cost = AsOrDefault<int32_t>(d["cost"]);
return result;
}
DukValue ScriptEngine::PositionToDuk(const CoordsXYZ& position)
{
DukStackFrame frame(_context);
duk_context* ctx = _context;
auto obj = duk_push_object(ctx);
duk_push_int(ctx, position.x);
duk_put_prop_string(ctx, obj, "x");
duk_push_int(ctx, position.y);
duk_put_prop_string(ctx, obj, "y");
duk_push_int(ctx, position.z);
duk_put_prop_string(ctx, obj, "z");
return DukValue::take_from_stack(ctx);
}
DukValue ScriptEngine::GameActionResultToDuk(const GameAction& action, const std::unique_ptr<GameActionResult>& result)
{
DukStackFrame frame(_context);
DukObject obj(_context);
auto player = action.GetPlayer();
if (player != -1)
{
obj.Set("player", action.GetPlayer());
}
if (result->Cost != MONEY32_UNDEFINED)
{
obj.Set("cost", result->Cost);
}
if (!result->Position.isNull())
{
obj.Set("position", PositionToDuk(result->Position));
}
if (action.GetType() == GAME_COMMAND_CREATE_RIDE)
{
auto& rideCreateResult = static_cast<RideCreateGameActionResult&>(*result.get());
if (rideCreateResult.rideIndex != RIDE_ID_NULL)
{
obj.Set("ride", rideCreateResult.rideIndex);
}
}
return obj.Take();
}
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;
}
CustomAction customAction;
customAction.Owner = 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.Owner == plugin)
{
it = _customActions.erase(it);
}
else
{
it++;
}
}
}
void ScriptEngine::RunGameActionHooks(const GameAction& action, std::unique_ptr<GameActionResult>& result, bool isExecute)
{
DukStackFrame frame(_context);
auto hookType = isExecute ? HOOK_TYPE::ACTION_EXECUTE : HOOK_TYPE::ACTION_QUERY;
if (_hookEngine.HasSubscriptions(hookType))
{
DukObject obj(_context);
obj.Set("type", action.GetType());
auto flags = action.GetActionFlags();
obj.Set("isClientOnly", (flags & GA_FLAGS::CLIENT_ONLY) != 0);
if (action.GetType() == GAME_COMMAND_CREATE_RIDE)
{
auto& rideCreateAction = static_cast<const RideCreateAction&>(action);
obj.Set("rideType", rideCreateAction.GetRideType());
obj.Set("rideObject", rideCreateAction.GetRideObject());
}
obj.Set("result", GameActionResultToDuk(action, result));
auto dukEventArgs = obj.Take();
_hookEngine.Call(hookType, dukEventArgs, false);
if (!isExecute)
{
auto error = AsOrDefault<int32_t>(dukEventArgs["error"]);
if (error != 0)
{
result->Error = static_cast<GA_ERROR>(error);
result->ErrorTitle = AsOrDefault<std::string>(dukEventArgs["errorTitle"]);
result->ErrorMessage = AsOrDefault<std::string>(dukEventArgs["errorMessage"]);
}
}
}
}
void ScriptEngine::InitSharedStorage()
{
duk_push_object(_context);
_sharedStorage = std::move(DukValue::take_from_stack(_context));
}
void ScriptEngine::LoadSharedStorage()
{
InitSharedStorage();
auto path = _env.GetFilePath(PATHID::PLUGIN_STORE);
try
{
if (File::Exists(path))
{
auto data = File::ReadAllBytes(path);
auto result = DuktapeTryParseJson(_context, std::string_view((const char*)data.data(), data.size()));
if (result)
{
_sharedStorage = std::move(*result);
}
}
}
catch (const std::exception&)
{
fprintf(stderr, "Unable to read '%s'\n", path.c_str());
}
}
void ScriptEngine::SaveSharedStorage()
{
auto path = _env.GetFilePath(PATHID::PLUGIN_STORE);
try
{
_sharedStorage.push();
auto json = std::string(duk_json_encode(_context, -1));
duk_pop(_context);
File::WriteAllBytes(path, json.c_str(), json.size());
}
catch (const std::exception&)
{
fprintf(stderr, "Unable to write to '%s'\n", path.c_str());
}
}
std::string OpenRCT2::Scripting::Stringify(const DukValue& val)
{
return ExpressionStringifier::StringifyExpression(val);
}
bool OpenRCT2::Scripting::IsGameStateMutable()
{
// Allow single player to alter game state anywhere
if (network_get_mode() == NETWORK_MODE_NONE)
{
return true;
}
else
{
auto& scriptEngine = GetContext()->GetScriptEngine();
auto& execInfo = scriptEngine.GetExecInfo();
return execInfo.IsGameStateMutable();
}
}
void OpenRCT2::Scripting::ThrowIfGameStateNotMutable()
{
// Allow single player to alter game state anywhere
if (network_get_mode() != NETWORK_MODE_NONE)
{
auto& scriptEngine = GetContext()->GetScriptEngine();
auto& execInfo = scriptEngine.GetExecInfo();
if (!execInfo.IsGameStateMutable())
{
auto ctx = scriptEngine.GetContext();
duk_error(ctx, DUK_ERR_ERROR, "Game state is not mutable in this context.");
}
}
}
#endif

View File

@ -0,0 +1,214 @@
/*****************************************************************************
* Copyright (c) 2014-2018 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 "../common.h"
# include "../core/FileWatcher.h"
# include "../world/Location.hpp"
# include "HookEngine.h"
# include "Plugin.h"
# include <future>
# include <memory>
# include <mutex>
# include <queue>
# include <string>
# include <unordered_map>
# include <unordered_set>
# include <vector>
struct duk_hthread;
typedef struct duk_hthread duk_context;
struct GameAction;
class GameActionResult;
class FileWatcher;
class InteractiveConsole;
namespace OpenRCT2
{
interface IPlatformEnvironment;
}
namespace OpenRCT2::Scripting
{
class ScriptExecutionInfo
{
private:
std::shared_ptr<Plugin> _plugin;
bool _isGameStateMutable{};
public:
class PluginScope
{
private:
ScriptExecutionInfo& _execInfo;
std::shared_ptr<Plugin> _plugin;
public:
PluginScope(ScriptExecutionInfo& execInfo, std::shared_ptr<Plugin> plugin, bool isGameStateMutable)
: _execInfo(execInfo)
, _plugin(plugin)
{
_execInfo._plugin = plugin;
_execInfo._isGameStateMutable = isGameStateMutable;
}
PluginScope(const PluginScope&) = delete;
~PluginScope()
{
_execInfo._plugin = nullptr;
_execInfo._isGameStateMutable = false;
}
};
std::shared_ptr<Plugin> GetCurrentPlugin()
{
return _plugin;
}
bool IsGameStateMutable()
{
return _isGameStateMutable;
}
};
class DukContext
{
private:
duk_context* _context{};
public:
DukContext();
DukContext(DukContext&) = delete;
DukContext(DukContext&& src) noexcept
: _context(std::move(src._context))
{
src._context = {};
}
~DukContext();
operator duk_context*()
{
return _context;
}
};
class ScriptEngine
{
private:
InteractiveConsole& _console;
IPlatformEnvironment& _env;
DukContext _context;
bool _initialised{};
bool _pluginsLoaded{};
bool _pluginsStarted{};
std::queue<std::tuple<std::promise<void>, std::string>> _evalQueue;
std::vector<std::shared_ptr<Plugin>> _plugins;
uint32_t _lastHotReloadCheckTick{};
HookEngine _hookEngine;
ScriptExecutionInfo _execInfo;
DukValue _sharedStorage;
std::unique_ptr<FileWatcher> _pluginFileWatcher;
std::unordered_set<std::string> _changedPluginFiles;
std::mutex _changedPluginFilesMutex;
std::vector<std::function<void(std::shared_ptr<Plugin>)>> _pluginStoppedSubscriptions;
struct CustomAction
{
std::shared_ptr<Plugin> Owner;
std::string Name;
DukValue Query;
DukValue Execute;
};
std::unordered_map<std::string, CustomAction> _customActions;
public:
ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env);
ScriptEngine(ScriptEngine&) = delete;
duk_context* GetContext()
{
return _context;
}
HookEngine& GetHookEngine()
{
return _hookEngine;
}
ScriptExecutionInfo& GetExecInfo()
{
return _execInfo;
}
DukValue GetSharedStorage()
{
return _sharedStorage;
}
std::vector<std::shared_ptr<Plugin>>& GetPlugins()
{
return _plugins;
}
void LoadPlugins();
void UnloadPlugins();
void Update();
std::future<void> Eval(const std::string& s);
DukValue ExecutePluginCall(
const std::shared_ptr<Plugin>& plugin, const DukValue& func, const std::vector<DukValue>& args,
bool isGameStateMutable);
void LogPluginInfo(const std::shared_ptr<Plugin>& plugin, const std::string_view& message);
void SubscribeToPluginStoppedEvent(std::function<void(std::shared_ptr<Plugin>)> callback)
{
_pluginStoppedSubscriptions.push_back(callback);
}
void AddNetworkPlugin(const std::string_view& code);
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);
void RunGameActionHooks(const GameAction& action, std::unique_ptr<GameActionResult>& result, bool isExecute);
void SaveSharedStorage();
private:
void Initialise();
void StartPlugins();
void StopPlugins();
void LoadPlugin(const std::string& path);
void LoadPlugin(std::shared_ptr<Plugin>& plugin);
void StopPlugin(std::shared_ptr<Plugin> plugin);
bool ShouldLoadScript(const std::string& path);
bool ShouldStartPlugin(const std::shared_ptr<Plugin>& plugin);
void SetupHotReloading();
void AutoReloadPlugins();
void ProcessREPL();
void RemoveCustomGameActions(const std::shared_ptr<Plugin>& plugin);
std::unique_ptr<GameActionResult> DukToGameActionResult(const DukValue& d);
DukValue GameActionResultToDuk(const GameAction& action, const std::unique_ptr<GameActionResult>& result);
DukValue PositionToDuk(const CoordsXYZ& position);
void InitSharedStorage();
void LoadSharedStorage();
};
bool IsGameStateMutable();
void ThrowIfGameStateNotMutable();
std::string Stringify(const DukValue& value);
} // namespace OpenRCT2::Scripting
#endif

View File

@ -24,6 +24,9 @@ namespace OpenRCT2::Ui
IWindowManager* const _windowManager = CreateDummyWindowManager();
public:
void Initialise() override
{
}
void Update() override
{
}

View File

@ -30,6 +30,10 @@ namespace OpenRCT2::Ui
{
return nullptr;
}
rct_window* ShowError(const std::string_view& /*title*/, const std::string_view& /*message*/) override
{
return nullptr;
}
rct_window* OpenIntent(Intent* /*intent*/) override
{
return nullptr;

View File

@ -91,6 +91,7 @@ namespace OpenRCT2
{
virtual ~IUiContext() = default;
virtual void Initialise() abstract;
virtual void Update() abstract;
virtual void Draw(rct_drawpixelinfo * dpi) abstract;

View File

@ -30,6 +30,7 @@ namespace OpenRCT2::Ui
virtual rct_window* OpenIntent(Intent * intent) abstract;
virtual void BroadcastIntent(const Intent& intent) abstract;
virtual rct_window* ShowError(rct_string_id title, rct_string_id message) abstract;
virtual rct_window* ShowError(const std::string_view& title, const std::string_view& message) abstract;
virtual void ForceClose(rct_windowclass windowClass) abstract;
virtual void UpdateMapTooltip() abstract;
virtual void HandleInput() abstract;

View File

@ -1170,21 +1170,28 @@ TileElement* tile_element_insert(const CoordsXYZ& loc, int32_t occupiedQuadrants
// Set tile index pointer to point to new element block
gTileElementTilePointers[tileLoc.y * MAXIMUM_MAP_SIZE_TECHNICAL + tileLoc.x] = newTileElement;
// Copy all elements that are below the insert height
while (loc.z >= originalTileElement->GetBaseZ())
if (originalTileElement == nullptr)
{
// Copy over map element
*newTileElement = *originalTileElement;
originalTileElement->base_height = 255;
originalTileElement++;
newTileElement++;
if ((newTileElement - 1)->IsLastForTile())
isLastForTile = true;
}
else
{
// Copy all elements that are below the insert height
while (loc.z >= originalTileElement->GetBaseZ())
{
// No more elements above the insert element
(newTileElement - 1)->SetLastForTile(false);
isLastForTile = true;
break;
// Copy over map element
*newTileElement = *originalTileElement;
originalTileElement->base_height = 255;
originalTileElement++;
newTileElement++;
if ((newTileElement - 1)->IsLastForTile())
{
// No more elements above the insert element
(newTileElement - 1)->SetLastForTile(false);
isLastForTile = true;
break;
}
}
}
@ -1317,7 +1324,7 @@ static GameActionResult::Ptr map_can_construct_with_clear_at(
if (tileElement == nullptr)
{
res->Error = GA_ERROR::UNKNOWN;
res->ErrorMessage = 0;
res->ErrorMessage = STR_NONE;
return res;
}
do
@ -1474,7 +1481,10 @@ bool map_can_construct_with_clear_at(
uint8_t crossingMode)
{
GameActionResult::Ptr res = map_can_construct_with_clear_at(pos, clearFunc, quarterTile, flags, crossingMode);
gGameCommandErrorText = res->ErrorMessage;
if (auto message = res->ErrorMessage.AsStringId())
gGameCommandErrorText = *message;
else
gGameCommandErrorText = STR_NONE;
std::copy(res->ErrorMessageArgs.begin(), res->ErrorMessageArgs.end(), gCommonFormatArgs);
if (price != nullptr)
{

View File

@ -0,0 +1,201 @@
#pragma once
#include "detail_typeinfo.h"
#include <assert.h>
namespace dukglue {
namespace detail {
struct ProtoManager
{
public:
template <typename Cls>
static void push_prototype(duk_context* ctx)
{
push_prototype(ctx, TypeInfo(typeid(Cls)));
}
static void push_prototype(duk_context* ctx, const TypeInfo& check_info)
{
if (!find_and_push_prototype(ctx, check_info)) {
// nope, need to create our prototype object
duk_push_object(ctx);
// add reference to this class' info object so we can do type checking
// when trying to pass this object into method calls
typedef dukglue::detail::TypeInfo TypeInfo;
TypeInfo* info = new TypeInfo(check_info);
duk_push_pointer(ctx, info);
duk_put_prop_string(ctx, -2, "\xFF" "type_info");
// Clean up the TypeInfo object when this prototype is destroyed.
// We can't put a finalizer directly on this prototype, because it
// will be run whenever the wrapper for an object of this class is
// destroyed; instead, we make a dummy object and put the finalizer
// on that.
// If you're memory paranoid: this duplicates the type_info pointer
// once per registered class. If you don't care about freeing memory
// during shutdown, you can probably comment out this part.
duk_push_object(ctx);
duk_push_pointer(ctx, info);
duk_put_prop_string(ctx, -2, "\xFF" "type_info");
duk_push_c_function(ctx, type_info_finalizer, 1);
duk_set_finalizer(ctx, -2);
duk_put_prop_string(ctx, -2, "\xFF" "type_info_finalizer");
// register it in the stash
register_prototype(ctx, info);
}
}
template<typename Cls>
static void make_script_object(duk_context* ctx, Cls* obj)
{
assert(obj != NULL);
duk_push_object(ctx);
duk_push_pointer(ctx, obj);
duk_put_prop_string(ctx, -2, "\xFF" "obj_ptr");
// push the appropriate prototype
#ifdef DUKGLUE_INFER_BASE_CLASS
// In the "infer base class" case, we push the prototype
// corresponding to the compile-time class if no prototype
// for the run-time type has been defined. This allows us to
// skip calling dukglue_set_base_class() for every derived class,
// so long as we:
// (1) Always use the derived class as a pointer typed as the base class
// (2) Do not create a prototype for the derived class
// (i.e. do not register any functions on the derived class).
// For big projects with hundreds of derived classes, this is preferrable
// to registering each type's base class individually. However,
// registering a native method on a derived class will cause the
// base class's methods to disappear until dukglue_set_base_class() is
// also called (because registering the native method causes a prototype
// to be created for the run-time type). This behavior may be unexpected,
// and for "small" projects it is reasonable to require
// dukglue_set_base_class() to be called, so it is opt-in via an ifdef.
// does a prototype exist for the run-time type? if so, push it
if (!find_and_push_prototype(ctx, TypeInfo(typeid(*obj)))) {
// nope, find or create the prototype for the compile-time type
// and push that
push_prototype<Cls>(ctx);
}
#else
// always use the prototype for the run-time type
push_prototype(ctx, TypeInfo(typeid(*obj)));
#endif
duk_set_prototype(ctx, -2);
}
private:
static duk_ret_t type_info_finalizer(duk_context* ctx)
{
duk_get_prop_string(ctx, 0, "\xFF" "type_info");
dukglue::detail::TypeInfo* info = static_cast<dukglue::detail::TypeInfo*>(duk_require_pointer(ctx, -1));
delete info;
// set pointer to NULL in case this finalizer runs again
duk_push_pointer(ctx, NULL);
duk_put_prop_string(ctx, 0, "\xFF" "type_info");
return 0;
}
// puts heap_stash["dukglue_prototypes"] on the stack,
// or creates it if it doesn't exist
static void push_prototypes_array(duk_context* ctx)
{
static const char* DUKGLUE_PROTOTYPES = "dukglue_prototypes";
duk_push_heap_stash(ctx);
// does the prototype array already exist?
if (!duk_has_prop_string(ctx, -1, DUKGLUE_PROTOTYPES)) {
// nope, we need to create it
duk_push_array(ctx);
duk_put_prop_string(ctx, -2, DUKGLUE_PROTOTYPES);
}
duk_get_prop_string(ctx, -1, DUKGLUE_PROTOTYPES);
// remove the heap stash from the stack
duk_remove(ctx, -2);
}
// Stack: ... [proto] -> ... [proto]
static void register_prototype(duk_context* ctx, const TypeInfo* info) {
// 1. We assume info is not in the prototype array already
// 2. Duktape has no efficient "shift array indices" operation (at least publicly)
// 3. This method doesn't need to be fast, it's only called during registration
// Work from high to low in the prototypes array, shifting as we go,
// until we find the spot for info.
push_prototypes_array(ctx);
duk_size_t i = duk_get_length(ctx, -1);
while (i > 0) {
duk_get_prop_index(ctx, -1, i - 1);
duk_get_prop_string(ctx, -1, "\xFF" "type_info");
const TypeInfo* chk_info = static_cast<TypeInfo*>(duk_require_pointer(ctx, -1));
duk_pop(ctx); // pop type_info
if (*chk_info > *info) {
duk_put_prop_index(ctx, -2, i);
i--;
} else {
duk_pop(ctx); // pop prototypes_array[i]
break;
}
}
//std::cout << "Registering prototype for " << typeid(Cls).name() << " at " << i << std::endl;
duk_dup(ctx, -2); // copy proto to top
duk_put_prop_index(ctx, -2, i);
duk_pop(ctx); // pop prototypes_array
}
static bool find_and_push_prototype(duk_context* ctx, const TypeInfo& search_info) {
push_prototypes_array(ctx);
// these are ints and not duk_size_t to deal with negative indices
int min = 0;
int max = duk_get_length(ctx, -1) - 1;
while (min <= max) {
int mid = (max - min) / 2 + min;
duk_get_prop_index(ctx, -1, mid);
duk_get_prop_string(ctx, -1, "\xFF" "type_info");
TypeInfo* mid_info = static_cast<TypeInfo*>(duk_require_pointer(ctx, -1));
duk_pop(ctx); // pop type_info pointer
if (*mid_info == search_info) {
// found it
duk_remove(ctx, -2); // pop prototypes_array
return true;
}
else if (*mid_info < search_info) {
min = mid + 1;
}
else {
max = mid - 1;
}
duk_pop(ctx); // pop prototypes_array[mid]
}
duk_pop(ctx); // pop prototypes_array
return false;
}
};
}
}

View File

@ -0,0 +1,73 @@
#pragma once
#include "detail_stack.h"
#include "detail_traits.h"
namespace dukglue {
namespace detail {
template<bool managed, typename Cls, typename... Ts>
static duk_ret_t call_native_constructor(duk_context* ctx)
{
if (!duk_is_constructor_call(ctx)) {
duk_error(ctx, DUK_RET_TYPE_ERROR, "Constructor must be called with new T().");
return DUK_RET_TYPE_ERROR;
}
// construct the new instance
auto constructor_args = dukglue::detail::get_stack_values<Ts...>(ctx);
Cls* obj = dukglue::detail::apply_constructor<Cls>(std::move(constructor_args));
duk_push_this(ctx);
// make the new script object keep the pointer to the new object instance
duk_push_pointer(ctx, obj);
duk_put_prop_string(ctx, -2, "\xFF" "obj_ptr");
// register it
if (!managed)
dukglue::detail::RefManager::register_native_object(ctx, obj);
duk_pop(ctx); // pop this
return 0;
}
template <typename Cls>
static duk_ret_t managed_finalizer(duk_context* ctx)
{
duk_get_prop_string(ctx, 0, "\xFF" "obj_ptr");
Cls* obj = (Cls*) duk_require_pointer(ctx, -1);
duk_pop(ctx); // pop obj_ptr
if (obj != NULL) {
delete obj;
// for safety, set the pointer to undefined
duk_push_undefined(ctx);
duk_put_prop_string(ctx, 0, "\xFF" "obj_ptr");
}
return 0;
}
template<typename Cls>
static duk_ret_t call_native_deleter(duk_context* ctx)
{
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, "\xFF" "obj_ptr");
if (!duk_is_pointer(ctx, -1)) {
duk_error(ctx, DUK_RET_REFERENCE_ERROR, "Object has already been invalidated; cannot delete.");
return DUK_RET_REFERENCE_ERROR;
}
Cls* obj = static_cast<Cls*>(duk_require_pointer(ctx, -1));
dukglue_invalidate_object(ctx, obj);
delete obj;
duk_pop_2(ctx);
return 0;
}
}
}

102
src/thirdparty/dukglue/detail_function.h vendored Normal file
View File

@ -0,0 +1,102 @@
#pragma once
#include "detail_stack.h"
#include "detail_types.h"
#include "detail_primitive_types.h"
namespace dukglue
{
namespace detail
{
// This struct can be used to generate a Duktape C function that
// pulls the argument values off the stack (with type checking),
// calls the appropriate function with them, and puts the function's
// return value (if any) onto the stack.
template<typename RetType, typename... Ts>
struct FuncInfoHolder
{
typedef RetType(*FuncType)(Ts...);
template<FuncType funcToCall>
struct FuncCompiletime
{
// The function to call is embedded into call_native_function at
// compile-time through template magic.
// Performance is so similar to run-time function calls that
// this is not recommended due to the ugly syntax it requires.
static duk_ret_t call_native_function(duk_context* ctx)
{
auto bakedArgs = dukglue::detail::get_stack_values<Ts...>(ctx);
actually_call(ctx, bakedArgs);
return std::is_void<RetType>::value ? 0 : 1;
}
private:
// this mess is to support functions with void return values
template<typename Dummy = RetType, typename... BakedTs>
static typename std::enable_if<!std::is_void<Dummy>::value>::type actually_call(duk_context* ctx, const std::tuple<BakedTs...>& args)
{
// ArgStorage has some static_asserts in it that validate value types,
// so we typedef it to force ArgStorage<RetType> to compile and run the asserts
typedef typename dukglue::types::ArgStorage<RetType>::type ValidateReturnType;
RetType return_val = dukglue::detail::apply_fp(funcToCall, args);
using namespace dukglue::types;
DukType<typename Bare<RetType>::type>::template push<RetType>(ctx, std::move(return_val));
}
template<typename Dummy = RetType, typename... BakedTs>
static typename std::enable_if<std::is_void<Dummy>::value>::type actually_call(duk_context* ctx, const std::tuple<BakedTs...>& args)
{
dukglue::detail::apply_fp(funcToCall, args);
}
};
struct FuncRuntime
{
// Pull the address of the function to call from the
// Duktape function object at run time.
static duk_ret_t call_native_function(duk_context* ctx)
{
duk_push_current_function(ctx);
duk_get_prop_string(ctx, -1, "\xFF" "func_ptr");
void* fp_void = duk_require_pointer(ctx, -1);
if (fp_void == NULL) {
duk_error(ctx, DUK_RET_TYPE_ERROR, "what even");
return DUK_RET_TYPE_ERROR;
}
duk_pop_2(ctx);
static_assert(sizeof(RetType(*)(Ts...)) == sizeof(void*), "Function pointer and data pointer are different sizes");
RetType(*funcToCall)(Ts...) = reinterpret_cast<RetType(*)(Ts...)>(fp_void);
actually_call(ctx, funcToCall, dukglue::detail::get_stack_values<Ts...>(ctx));
return std::is_void<RetType>::value ? 0 : 1;
}
// this mess is to support functions with void return values
template<typename Dummy = RetType, typename... BakedTs>
static typename std::enable_if<!std::is_void<Dummy>::value>::type actually_call(duk_context* ctx, RetType(*funcToCall)(Ts...), const std::tuple<BakedTs...>& args)
{
// ArgStorage has some static_asserts in it that validate value types,
// so we typedef it to force ArgStorage<RetType> to compile and run the asserts
typedef typename dukglue::types::ArgStorage<RetType>::type ValidateReturnType;
RetType return_val = dukglue::detail::apply_fp(funcToCall, args);
using namespace dukglue::types;
DukType<typename Bare<RetType>::type>::template push<RetType>(ctx, std::move(return_val));
}
template<typename Dummy = RetType, typename... BakedTs>
static typename std::enable_if<std::is_void<Dummy>::value>::type actually_call(duk_context* ctx, RetType(*funcToCall)(Ts...), const std::tuple<BakedTs...>& args)
{
dukglue::detail::apply_fp(funcToCall, args);
}
};
};
}
}

183
src/thirdparty/dukglue/detail_method.h vendored Normal file
View File

@ -0,0 +1,183 @@
#pragma once
#include "detail_stack.h"
namespace dukglue
{
namespace detail
{
template<bool isConst, class Cls, typename RetType, typename... Ts>
struct MethodInfo
{
typedef typename std::conditional<isConst, RetType(Cls::*)(Ts...) const, RetType(Cls::*)(Ts...)>::type MethodType;
// The size of a method pointer is not guaranteed to be the same size as a function pointer.
// This means we can't just use duk_push_pointer(ctx, &MyClass::method) to store the method at run time.
// To get around this, we wrap the method pointer in a MethodHolder (on the heap), and push a pointer to
// that. The MethodHolder is cleaned up by the finalizer.
struct MethodHolder
{
MethodType method;
};
template<MethodType methodToCall>
struct MethodCompiletime
{
static duk_ret_t call_native_method(duk_context* ctx)
{
// get this.obj_ptr
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, "\xFF" "obj_ptr");
void* obj_void = duk_require_pointer(ctx, -1);
if (obj_void == nullptr) {
duk_error(ctx, DUK_RET_REFERENCE_ERROR, "Native object missing.");
return DUK_RET_REFERENCE_ERROR;
}
duk_pop_2(ctx);
// (should always be valid unless someone is intentionally messing with this.obj_ptr...)
Cls* obj = static_cast<Cls*>(obj_void);
// read arguments and call function
auto bakedArgs = dukglue::detail::get_stack_values<Ts...>(ctx);
actually_call(ctx, obj, bakedArgs);
return std::is_void<RetType>::value ? 0 : 1;
}
// this mess is to support functions with void return values
template<typename Dummy = RetType, typename... BakedTs>
static typename std::enable_if<!std::is_void<Dummy>::value>::type actually_call(duk_context* ctx, Cls* obj, const std::tuple<BakedTs...>& args)
{
// ArgStorage has some static_asserts in it that validate value types,
// so we typedef it to force ArgStorage<RetType> to compile and run the asserts
typedef typename dukglue::types::ArgStorage<RetType>::type ValidateReturnType;
RetType return_val = dukglue::detail::apply_method<Cls, RetType, Ts...>(methodToCall, obj, args);
using namespace dukglue::types;
DukType<typename Bare<RetType>::type>::template push<RetType>(ctx, std::move(return_val));
}
template<typename Dummy = RetType, typename... BakedTs>
static typename std::enable_if<std::is_void<Dummy>::value>::type actually_call(duk_context* ctx, Cls* obj, const std::tuple<BakedTs...>& args)
{
dukglue::detail::apply_method(methodToCall, obj, args);
}
};
struct MethodRuntime
{
static duk_ret_t finalize_method(duk_context* ctx)
{
// clean up the MethodHolder reference
duk_get_prop_string(ctx, 0, "\xFF" "method_holder");
void* method_holder_void = duk_require_pointer(ctx, -1);
MethodHolder* method_holder = static_cast<MethodHolder*>(method_holder_void);
delete method_holder;
return 0;
}
static duk_ret_t call_native_method(duk_context* ctx)
{
// get this.obj_ptr
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, "\xFF" "obj_ptr");
void* obj_void = duk_get_pointer(ctx, -1);
if (obj_void == nullptr) {
duk_error(ctx, DUK_RET_REFERENCE_ERROR, "Invalid native object for 'this'");
return DUK_RET_REFERENCE_ERROR;
}
duk_pop_2(ctx); // pop this.obj_ptr and this
// get current_function.method_info
duk_push_current_function(ctx);
duk_get_prop_string(ctx, -1, "\xFF" "method_holder");
void* method_holder_void = duk_require_pointer(ctx, -1);
if (method_holder_void == nullptr) {
duk_error(ctx, DUK_RET_TYPE_ERROR, "Method pointer missing?!");
return DUK_RET_TYPE_ERROR;
}
duk_pop_2(ctx);
// (should always be valid unless someone is intentionally messing with this.obj_ptr...)
Cls* obj = static_cast<Cls*>(obj_void);
MethodHolder* method_holder = static_cast<MethodHolder*>(method_holder_void);
// read arguments and call method
auto bakedArgs = dukglue::detail::get_stack_values<Ts...>(ctx);
actually_call(ctx, method_holder->method, obj, bakedArgs);
return std::is_void<RetType>::value ? 0 : 1;
}
// this mess is to support functions with void return values
template<typename Dummy = RetType, typename... BakedTs>
static typename std::enable_if<!std::is_void<Dummy>::value>::type actually_call(duk_context* ctx, MethodType method, Cls* obj, const std::tuple<BakedTs...>& args)
{
// ArgStorage has some static_asserts in it that validate value types,
// so we typedef it to force ArgStorage<RetType> to compile and run the asserts
typedef typename dukglue::types::ArgStorage<RetType>::type ValidateReturnType;
RetType return_val = dukglue::detail::apply_method<Cls, RetType, Ts...>(method, obj, args);
using namespace dukglue::types;
DukType<typename Bare<RetType>::type>::template push<RetType>(ctx, std::move(return_val));
}
template<typename Dummy = RetType, typename... BakedTs>
static typename std::enable_if<std::is_void<Dummy>::value>::type actually_call(duk_context* ctx, MethodType method, Cls* obj, const std::tuple<BakedTs...>& args)
{
dukglue::detail::apply_method(method, obj, args);
}
};
};
template <bool isConst, typename Cls>
struct MethodVariadicRuntime
{
typedef MethodInfo<isConst, Cls, duk_ret_t, duk_context*> MethodInfoVariadic;
typedef typename MethodInfoVariadic::MethodHolder MethodHolderVariadic;
static duk_ret_t finalize_method(duk_context* ctx)
{
return MethodInfoVariadic::MethodRuntime::finalize_method(ctx);
}
static duk_ret_t call_native_method(duk_context* ctx)
{
// get this.obj_ptr
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, "\xFF" "obj_ptr");
void* obj_void = duk_get_pointer(ctx, -1);
if (obj_void == nullptr) {
duk_error(ctx, DUK_RET_REFERENCE_ERROR, "Invalid native object for 'this'");
return DUK_RET_REFERENCE_ERROR;
}
duk_pop_2(ctx); // pop this.obj_ptr and this
// get current_function.method_info
duk_push_current_function(ctx);
duk_get_prop_string(ctx, -1, "\xFF" "method_holder");
void* method_holder_void = duk_require_pointer(ctx, -1);
if (method_holder_void == nullptr) {
duk_error(ctx, DUK_RET_TYPE_ERROR, "Method pointer missing?!");
return DUK_RET_TYPE_ERROR;
}
duk_pop_2(ctx);
// (should always be valid unless someone is intentionally messing with this.obj_ptr...)
Cls* obj = static_cast<Cls*>(obj_void);
MethodHolderVariadic* method_holder = static_cast<MethodHolderVariadic*>(method_holder_void);
return (*obj.*method_holder->method)(ctx);
}
};
}
}

View File

@ -0,0 +1,256 @@
#pragma once
#include "detail_types.h"
#include "detail_typeinfo.h"
#include "dukvalue.h"
#include <vector>
#include <stdint.h>
#include <memory> // for std::shared_ptr
namespace dukglue {
namespace types {
#define DUKGLUE_SIMPLE_VALUE_TYPE(TYPE, DUK_IS_FUNC, DUK_GET_FUNC, DUK_PUSH_FUNC, PUSH_VALUE) \
template<> \
struct DukType<TYPE> { \
typedef std::true_type IsValueType; \
\
template<typename FullT> \
static TYPE read(duk_context* ctx, duk_idx_t arg_idx) { \
if (DUK_IS_FUNC(ctx, arg_idx)) { \
return static_cast<TYPE>(DUK_GET_FUNC(ctx, arg_idx)); \
} else { \
duk_int_t type_idx = duk_get_type(ctx, arg_idx); \
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected " #TYPE ", got %s", arg_idx, detail::get_type_name(type_idx)); \
} \
} \
\
template<typename FullT> \
static void push(duk_context* ctx, TYPE value) { \
DUK_PUSH_FUNC(ctx, PUSH_VALUE); \
} \
};
DUKGLUE_SIMPLE_VALUE_TYPE(bool, duk_is_boolean, 0 != duk_get_boolean, duk_push_boolean, value)
DUKGLUE_SIMPLE_VALUE_TYPE(uint8_t, duk_is_number, duk_get_uint, duk_push_uint, value)
DUKGLUE_SIMPLE_VALUE_TYPE(uint16_t, duk_is_number, duk_get_uint, duk_push_uint, value)
DUKGLUE_SIMPLE_VALUE_TYPE(uint32_t, duk_is_number, duk_get_uint, duk_push_uint, value)
DUKGLUE_SIMPLE_VALUE_TYPE(uint64_t, duk_is_number, duk_get_number, duk_push_number, value) // have to cast to double
DUKGLUE_SIMPLE_VALUE_TYPE(int8_t, duk_is_number, duk_get_int, duk_push_int, value)
DUKGLUE_SIMPLE_VALUE_TYPE(int16_t, duk_is_number, duk_get_int, duk_push_int, value)
DUKGLUE_SIMPLE_VALUE_TYPE(int32_t, duk_is_number, duk_get_int, duk_push_int, value)
DUKGLUE_SIMPLE_VALUE_TYPE(int64_t, duk_is_number, duk_get_number, duk_push_number, value) // have to cast to double
// signed char and unsigned char are surprisingly *both* different from char, at least in MSVC
DUKGLUE_SIMPLE_VALUE_TYPE(char, duk_is_number, duk_get_int, duk_push_int, value)
DUKGLUE_SIMPLE_VALUE_TYPE(float, duk_is_number, duk_get_number, duk_push_number, value)
DUKGLUE_SIMPLE_VALUE_TYPE(double, duk_is_number, duk_get_number, duk_push_number, value)
DUKGLUE_SIMPLE_VALUE_TYPE(std::string, duk_is_string, duk_get_string, duk_push_string, value.c_str())
// We have to do some magic for const char* to work correctly.
// We override the "bare type" and "storage type" to both be const char*.
// char* is a bit tricky because its "bare type" should still be const char*, to differentiate it from just char
template<>
struct Bare<char*> {
typedef const char* type;
};
template<>
struct Bare<const char*> {
typedef const char* type;
};
// the storage type should also be const char* - if we don't do this, it will end up as just "char"
template<>
struct ArgStorage<const char*> {
typedef const char* type;
};
template<>
struct DukType<const char*> {
typedef std::true_type IsValueType;
template<typename FullT>
static const char* read(duk_context* ctx, duk_idx_t arg_idx) {
if (duk_is_string(ctx, arg_idx)) {
return duk_get_string(ctx, arg_idx);
} else {
duk_int_t type_idx = duk_get_type(ctx, arg_idx);
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected string, got %s", arg_idx, detail::get_type_name(type_idx));
}
}
template<typename FullT>
static void push(duk_context* ctx, const char* value) {
duk_push_string(ctx, value);
}
};
// DukValue
template<>
struct DukType<DukValue> {
typedef std::true_type IsValueType;
template <typename FullT>
static DukValue read(duk_context* ctx, duk_idx_t arg_idx) {
try {
return DukValue::copy_from_stack(ctx, arg_idx);
} catch (DukException& e) {
// only DukException can be thrown by DukValue::copy_from_stack
duk_error(ctx, DUK_ERR_ERROR, e.what());
}
}
template <typename FullT>
static void push(duk_context* ctx, const DukValue& value) {
if (value.context() == NULL) {
duk_error(ctx, DUK_ERR_ERROR, "DukValue is uninitialized");
return;
}
if (value.context() != ctx) {
duk_error(ctx, DUK_ERR_ERROR, "DukValue comes from a different context");
return;
}
try {
value.push();
} catch (DukException& e) {
// only DukException can be thrown by DukValue::copy_from_stack
duk_error(ctx, DUK_ERR_ERROR, e.what());
}
}
};
// std::vector (as value)
template<typename T>
struct DukType< std::vector<T> > {
typedef std::true_type IsValueType;
template <typename FullT>
static std::vector<T> read(duk_context* ctx, duk_idx_t arg_idx) {
if (!duk_is_array(ctx, arg_idx)) {
duk_int_t type_idx = duk_get_type(ctx, arg_idx);
duk_error(ctx, DUK_ERR_TYPE_ERROR, "Argument %d: expected array, got %s", arg_idx, detail::get_type_name(type_idx));
}
duk_size_t len = duk_get_length(ctx, arg_idx);
const duk_idx_t elem_idx = duk_get_top(ctx);
std::vector<T> vec;
vec.reserve(len);
for (duk_size_t i = 0; i < len; i++) {
duk_get_prop_index(ctx, arg_idx, i);
vec.push_back(DukType< typename Bare<T>::type >::template read<T>(ctx, elem_idx));
duk_pop(ctx);
}
return vec;
}
template <typename FullT>
static void push(duk_context* ctx, const std::vector<T>& value) {
duk_idx_t obj_idx = duk_push_array(ctx);
for (size_t i = 0; i < value.size(); i++) {
DukType< typename Bare<T>::type >::template push<T>(ctx, value[i]);
duk_put_prop_index(ctx, obj_idx, i);
}
}
};
// std::shared_ptr (as value)
template<typename T>
struct DukType< std::shared_ptr<T> > {
typedef std::true_type IsValueType;
static_assert(std::is_same<typename DukType<T>::IsValueType, std::false_type>::value, "Dukglue can only use std::shared_ptr to non-value types!");
template <typename FullT>
static std::shared_ptr<T> read(duk_context* ctx, duk_idx_t arg_idx) {
if (duk_is_null(ctx, arg_idx))
return nullptr;
if (!duk_is_object(ctx, arg_idx)) {
duk_int_t type_idx = duk_get_type(ctx, arg_idx);
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected shared_ptr object, got ", arg_idx, detail::get_type_name(type_idx));
}
duk_get_prop_string(ctx, arg_idx, "\xFF" "type_info");
if (!duk_is_pointer(ctx, -1)) // missing type_info, must not be a native object
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected shared_ptr object (missing type_info)", arg_idx);
// make sure this object can be safely returned as a T*
dukglue::detail::TypeInfo* info = static_cast<dukglue::detail::TypeInfo*>(duk_get_pointer(ctx, -1));
if (!info->can_cast<T>())
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: wrong type of shared_ptr object", arg_idx);
duk_pop(ctx); // pop type_info
duk_get_prop_string(ctx, arg_idx, "\xFF" "shared_ptr");
if (!duk_is_pointer(ctx, -1))
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: not a shared_ptr object (missing shared_ptr)", arg_idx);
void* ptr = duk_get_pointer(ctx, -1);
duk_pop(ctx); // pop pointer to shared_ptr
return *((std::shared_ptr<T>*) ptr);
}
static duk_ret_t shared_ptr_finalizer(duk_context* ctx)
{
duk_get_prop_string(ctx, 0, "\xFF" "shared_ptr");
std::shared_ptr<T>* ptr = (std::shared_ptr<T>*) duk_require_pointer(ctx, -1);
duk_pop(ctx); // pop shared_ptr ptr
if (ptr != NULL) {
delete ptr;
// for safety, set the pointer to undefined
// (finalizers can run multiple times)
duk_push_undefined(ctx);
duk_put_prop_string(ctx, 0, "\xFF" "shared_ptr");
}
return 0;
}
template <typename FullT>
static void push(duk_context* ctx, const std::shared_ptr<T>& value) {
if (value == nullptr) {
duk_push_null(ctx);
} else {
dukglue::detail::ProtoManager::make_script_object(ctx, value.get());
// create + set shared_ptr
duk_push_pointer(ctx, new std::shared_ptr<T>(value));
duk_put_prop_string(ctx, -2, "\xFF" "shared_ptr");
// set shared_ptr finalizer
duk_push_c_function(ctx, &shared_ptr_finalizer, 1);
duk_set_finalizer(ctx, -2);
}
}
};
// std::function
/*template <typename RetT, typename... ArgTs>
struct DukType< std::function<RetT(ArgTs...)> > {
typedef std::true_type IsValueType;
template<typename FullT>
static std::function<RetT(ArgTs...)> read(duk_context* ctx, duk_idx_t arg_idx) {
DukValue callable = DukValue::copy_from_stack(ctx, -1, DUK_TYPE_MASK_OBJECT);
return [ctx, callable] (ArgTs... args) -> RetT {
dukglue_call<RetT>(ctx, callable, args...);
};
}
template<typename FullT>
static void push(duk_context* ctx, std::function<RetT(ArgTs...)> value) {
static_assert(false, "Pushing an std::function has not been implemented yet. Sorry!");
}
};*/
}
}

199
src/thirdparty/dukglue/detail_refs.h vendored Normal file
View File

@ -0,0 +1,199 @@
#pragma once
#include <duktape.h>
#include <unordered_map>
namespace dukglue
{
namespace detail
{
// This class handles keeping a map of void* -> script object.
// It also prevents script objects from being GC'd until someone
// explicitly frees the underlying native object.
// Implemented by keeping an array of script objects in the heap stash.
// An std::unordered_map maps pointer -> array index.
// Thanks to std::unordered_map, lookup time is O(1) on average.
// Using std::unordered_map has some memory overhead (~32 bytes per object),
// which could be removed by using a different data structure:
// 1. Use no data structure. Blindly scan through the reference registry,
// checking \xFFobj_ptr on every object until you find yours.
// Performance when returning native objects from functions when a lot
// of native objects are registered will suffer.
// 2. Implement a self-balancing binary tree on top of a Duktape array
// for the registry. Still fast - O(log(N)) - and no memory overhead.
// 3. A sorted list would work too, though insertion speed might be worse
// than a binary tree.
struct RefManager
{
public:
// Find the script object corresponding to obj_ptr and push it.
// Returns true if successful, false if obj_ptr has not been registered.
// Stack: ... -> ... (if object has been registered before)
// ... -> ... [object] (if object has not been registered)
static bool find_and_push_native_object(duk_context* ctx, void* obj_ptr)
{
RefMap* ref_map = get_ref_map(ctx);
const auto it = ref_map->find(obj_ptr);
if (it == ref_map->end()) {
return false;
} else {
push_ref_array(ctx);
duk_get_prop_index(ctx, -1, it->second);
duk_remove(ctx, -2);
return true;
}
}
// Takes a script object and adds it to the registry, associating
// it with obj_ptr. unregistered_object is not modified.
// If obj_ptr has already been registered with another object,
// the old registry entry will be overidden.
// Does nothing if obj_ptr is NULL.
// Stack: ... [object] -> ... [object]
static void register_native_object(duk_context* ctx, void* obj_ptr)
{
if (obj_ptr == NULL)
return;
RefMap* ref_map = get_ref_map(ctx);
push_ref_array(ctx);
// find next free index
// free indices are kept in a linked list, starting at ref_array[0]
duk_get_prop_index(ctx, -1, 0);
duk_uarridx_t next_free_idx = duk_get_uint(ctx, -1);
duk_pop(ctx);
if (next_free_idx == 0) {
// no free spots in the array, make a new one at arr.length
next_free_idx = duk_get_length(ctx, -1);
} else {
// free spot found, need to remove it from the free list
// ref_array[0] = ref_array[next_free_idx]
duk_get_prop_index(ctx, -1, next_free_idx);
duk_put_prop_index(ctx, -2, 0);
}
// std::cout << "putting reference at ref_array[" << next_free_idx << "]" << std::endl;
(*ref_map)[obj_ptr] = next_free_idx;
duk_dup(ctx, -2); // put object on top
// ... [object] [ref_array] [object]
duk_put_prop_index(ctx, -2, next_free_idx);
duk_pop(ctx); // pop ref_array
}
// Remove the object associated with obj_ptr from the registry
// and invalidate the object's internal native pointer (by setting it to undefined).
// Does nothing if obj_ptr if object was never registered or obj_ptr is NULL.
// Does not affect the stack.
static void find_and_invalidate_native_object(duk_context* ctx, void* obj_ptr)
{
if (obj_ptr == NULL)
return;
RefMap* ref_map = get_ref_map(ctx);
auto it = ref_map->find(obj_ptr);
if (it == ref_map->end()) // was never registered
return;
push_ref_array(ctx);
duk_get_prop_index(ctx, -1, it->second);
// invalidate internal pointer
duk_push_undefined(ctx);
duk_put_prop_string(ctx, -2, "\xFF" "obj_ptr");
duk_pop(ctx); // pop object
// remove from references array and add the space it was in to free list
// (refs[0] -> tail) -> (refs[0] -> old_obj_idx -> tail)
// refs[old_obj_idx] = refs[0]
duk_get_prop_index(ctx, -1, 0);
duk_put_prop_index(ctx, -2, it->second);
// refs[0] = old_obj_idx
duk_push_uint(ctx, it->second);
duk_put_prop_index(ctx, -2, 0);
duk_pop(ctx); // pop ref_array
// also remove from map
// std::cout << "Freeing ref_array[" << it->second << "]" << std::endl;
ref_map->erase(it);
}
private:
typedef std::unordered_map<void*, duk_uarridx_t> RefMap;
static RefMap* get_ref_map(duk_context* ctx)
{
static const char* DUKGLUE_REF_MAP = "dukglue_ref_map";
static const char* PTR = "ptr";
duk_push_heap_stash(ctx);
if (!duk_has_prop_string(ctx, -1, DUKGLUE_REF_MAP)) {
// doesn't exist yet, need to create it
duk_push_object(ctx);
duk_push_pointer(ctx, new RefMap());
duk_put_prop_string(ctx, -2, PTR);
duk_push_c_function(ctx, ref_map_finalizer, 1);
duk_set_finalizer(ctx, -2);
duk_put_prop_string(ctx, -2, DUKGLUE_REF_MAP);
}
duk_get_prop_string(ctx, -1, DUKGLUE_REF_MAP);
duk_get_prop_string(ctx, -1, PTR);
RefMap* map = static_cast<RefMap*>(duk_require_pointer(ctx, -1));
duk_pop_3(ctx);
return map;
}
static duk_ret_t ref_map_finalizer(duk_context* ctx)
{
duk_get_prop_string(ctx, 0, "ptr");
RefMap* map = static_cast<RefMap*>(duk_require_pointer(ctx, -1));
delete map;
return 0;
}
static void push_ref_array(duk_context* ctx)
{
static const char* DUKGLUE_REF_ARRAY = "dukglue_ref_array";
duk_push_heap_stash(ctx);
if (!duk_has_prop_string(ctx, -1, DUKGLUE_REF_ARRAY)) {
duk_push_array(ctx);
// ref_array[0] = 0 (initialize free list as empty)
duk_push_int(ctx, 0);
duk_put_prop_index(ctx, -2, 0);
duk_put_prop_string(ctx, -2, DUKGLUE_REF_ARRAY);
}
duk_get_prop_string(ctx, -1, DUKGLUE_REF_ARRAY);
duk_remove(ctx, -2); // pop heap stash
}
};
}
}

52
src/thirdparty/dukglue/detail_stack.h vendored Normal file
View File

@ -0,0 +1,52 @@
#pragma once
#include <string>
#include "detail_primitive_types.h"
#include "detail_traits.h"
#include "detail_types.h"
#include <duktape.h>
namespace dukglue
{
namespace detail
{
// Helper to get the argument tuple type, with correct storage types.
template<typename... Args>
struct ArgsTuple {
typedef std::tuple<typename dukglue::types::ArgStorage<Args>::type...> type;
};
// Helper to get argument indices.
// Call read for every Ts[i], for matching argument index Index[i].
// The traits::index_tuple is used for type inference.
// A concrete example:
// get_values<int, bool>(duktape_context)
// get_values_helper<{int, bool}, {0, 1}>(ctx, ignored)
// std::make_tuple<int, bool>(read<int>(ctx, 0), read<bool>(ctx, 1))
template<typename... Args, size_t... Indexes>
typename ArgsTuple<Args...>::type get_stack_values_helper(duk_context* ctx, dukglue::detail::index_tuple<Indexes...>)
{
using namespace dukglue::types;
return std::forward_as_tuple(DukType<typename Bare<Args>::type>::template read<typename ArgStorage<Args>::type>(ctx, Indexes)...);
}
// Returns an std::tuple of the values asked for in the template parameters.
// Values will remain on the stack.
// Values are indexed from the bottom of the stack up (0, 1, ...).
// If a value does not exist or does not have the expected type, an error is thrown
// through Duktape (with duk_error(...)), and the function does not return
template<typename... Args>
typename ArgsTuple<Args...>::type get_stack_values(duk_context* ctx)
{
// We need the argument indices for read_value, and we need to be able
// to unpack them as a template argument to match Ts.
// So, we use traits::make_indexes<Ts...>, which returns a traits::index_tuple<0, 1, 2, ...> object.
// We pass that into a helper function so we can put a name to that <0, 1, ...> template argument.
// Here, the type of Args isn't important, the length of it is.
auto indices = typename dukglue::detail::make_indexes<Args...>::type();
return get_stack_values_helper<Args...>(ctx, indices);
}
}
}

122
src/thirdparty/dukglue/detail_traits.h vendored Normal file
View File

@ -0,0 +1,122 @@
#pragma once
#include <functional>
namespace dukglue
{
namespace detail
{
//////////////////////////////////////////////////////////////////////////////////////////////
// Credit to LuaState for this code:
// https://github.com/AdUki/LuaState/blob/master/include/Traits.h
template<size_t...> struct index_tuple {};
template<size_t I, typename IndexTuple, typename ... Types>
struct make_indexes_impl;
template<size_t I, size_t... Indexes, typename T, typename ... Types>
struct make_indexes_impl<I, index_tuple<Indexes...>, T, Types...>
{
typedef typename make_indexes_impl<I + 1, index_tuple<Indexes..., I>, Types...>::type type;
};
template<size_t I, size_t... Indexes>
struct make_indexes_impl<I, index_tuple<Indexes...> >
{
typedef index_tuple<Indexes...> type;
};
template<typename ... Types>
struct make_indexes : make_indexes_impl<0, index_tuple<>, Types...>
{};
//////////////////////////////////////////////////////////////////////////////////////////////
template <std::size_t... Is>
struct indexes {};
template <std::size_t N, std::size_t... Is>
struct indexes_builder : indexes_builder<N - 1, N - 1, Is...> {};
template <std::size_t... Is>
struct indexes_builder<0, Is...> {
typedef indexes<Is...> index;
};
//////////////////////////////////////////////////////////////////////////////////////////////
// This mess is used to use function arugments stored in an std::tuple to an
// std::function, function pointer, or method.
// std::function
template<class Ret, class... Args, size_t... Indexes >
Ret apply_helper(std::function<Ret(Args...)> pf, index_tuple< Indexes... >, std::tuple<Args...>&& tup)
{
return pf(std::forward<Args>(std::get<Indexes>(tup))...);
}
template<class Ret, class ... Args>
Ret apply(std::function<Ret(Args...)> pf, const std::tuple<Args...>& tup)
{
return apply_helper(pf, typename make_indexes<Args...>::type(), std::tuple<Args...>(tup));
}
// function pointer
template<class Ret, class... Args, class... BakedArgs, size_t... Indexes >
Ret apply_fp_helper(Ret(*pf)(Args...), index_tuple< Indexes... >, std::tuple<BakedArgs...>&& tup)
{
return pf(std::forward<Args>(std::get<Indexes>(tup))...);
}
template<class Ret, class ... Args, class ... BakedArgs>
Ret apply_fp(Ret(*pf)(Args...), const std::tuple<BakedArgs...>& tup)
{
return apply_fp_helper(pf, typename make_indexes<BakedArgs...>::type(), std::tuple<BakedArgs...>(tup));
}
// method pointer
template<class Cls, class Ret, class... Args, class... BakedArgs, size_t... Indexes >
Ret apply_method_helper(Ret(Cls::*pf)(Args...), index_tuple< Indexes... >, Cls* obj, std::tuple<BakedArgs...>&& tup)
{
return (*obj.*pf)(std::forward<Args>(std::get<Indexes>(tup))...);
}
template<class Cls, class Ret, class ... Args, class... BakedArgs>
Ret apply_method(Ret(Cls::*pf)(Args...), Cls* obj, const std::tuple<BakedArgs...>& tup)
{
return apply_method_helper(pf, typename make_indexes<Args...>::type(), obj, std::tuple<BakedArgs...>(tup));
}
// const method pointer
template<class Cls, class Ret, class... Args, class... BakedArgs, size_t... Indexes >
Ret apply_method_helper(Ret(Cls::*pf)(Args...) const, index_tuple< Indexes... >, Cls* obj, std::tuple<BakedArgs...>&& tup)
{
return (*obj.*pf)(std::forward<Args>(std::get<Indexes>(tup))...);
}
template<class Cls, class Ret, class ... Args, class... BakedArgs>
Ret apply_method(Ret(Cls::*pf)(Args...) const, Cls* obj, const std::tuple<BakedArgs...>& tup)
{
return apply_method_helper(pf, typename make_indexes<Args...>::type(), obj, std::tuple<BakedArgs...>(tup));
}
// constructor
template<class Cls, typename... Args, size_t... Indexes >
Cls* apply_constructor_helper(index_tuple< Indexes... >, std::tuple<Args...>&& tup)
{
return new Cls(std::forward<Args>(std::get<Indexes>(tup))...);
}
template<class Cls, typename... Args>
Cls* apply_constructor(const std::tuple<Args...>& tup)
{
return apply_constructor_helper<Cls>(typename make_indexes<Args...>::type(), std::tuple<Args...>(tup));
}
//////////////////////////////////////////////////////////////////////////////////////////////
}
}

View File

@ -0,0 +1,64 @@
#pragma once
#include <duktape.h>
#include <typeindex>
namespace dukglue
{
namespace detail
{
// same as duk_get_type_name, which is private for some reason *shakes fist*
static inline const char* get_type_name(duk_int_t type_idx) {
static const char* names[] = {
"none",
"undefined",
"null",
"boolean",
"number",
"string",
"object",
"buffer",
"pointer",
"lightfunc"
};
if (type_idx >= 0 && type_idx < static_cast<duk_int_t>(sizeof(names) / sizeof(names[0])))
return names[type_idx];
else
return "unknown";
}
class TypeInfo
{
public:
TypeInfo(std::type_index&& idx) : index_(idx), base_(nullptr) {}
TypeInfo(const TypeInfo& rhs) : index_(rhs.index_), base_(rhs.base_) {}
inline void set_base(TypeInfo* base) {
base_ = base;
}
template<typename T>
bool can_cast() const {
if (index_ == typeid(T))
return true;
if (base_)
return base_->can_cast<T>();
return false;
}
inline bool operator<(const TypeInfo& rhs) const { return index_ < rhs.index_; }
inline bool operator<=(const TypeInfo& rhs) const { return index_ <= rhs.index_; }
inline bool operator>(const TypeInfo& rhs) const { return index_ > rhs.index_; }
inline bool operator>=(const TypeInfo& rhs) const { return index_ >= rhs.index_; }
inline bool operator==(const TypeInfo& rhs) const { return index_ == rhs.index_; }
inline bool operator!=(const TypeInfo& rhs) const { return index_ != rhs.index_; }
private:
std::type_index index_;
TypeInfo* base_;
};
}
}

Some files were not shown because too many files have changed in this diff Show More