From 355c8ca5f0652a1b86c5282d37b1816fb9ad866a Mon Sep 17 00:00:00 2001 From: Selis <12716288+ItsSelis@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:21:14 +0200 Subject: [PATCH] DMAPI Update (#17935) * DMAPI Update * Fix * More changes * InitTgs * . * css * urg * fix that * some linux fixes * . * . * . * update * . * pref fixing * . * those are already sent * . * . * . * fully off * fix that * New classes & Format * make this nicer * CSS Edits * . * eh use switch * . * Update client procs.dm * Hard restart counter * Improved Staffwho style --------- Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> --- .github/workflows/tgs_test.yml | 70 ++ .github/workflows/update_tgs_dmapi.yml | 62 ++ .tgs.yml | 25 + .vscode/tasks.json | 12 +- code/__defines/chat.dm | 9 + code/__defines/tgs.dm | 573 ++++++++++++---- .../configuration/entries/general.dm | 4 + .../configuration/entries/vorestation.dm | 6 +- code/datums/elements/footstep.dm | 2 +- code/datums/helper_datums/getrev.dm | 125 ++-- code/datums/tgs_event_handler.dm | 41 ++ code/game/world.dm | 50 +- code/modules/admin/permissionedit.dm | 8 + code/modules/client/client procs.dm | 7 +- code/modules/client/preferences.dm | 2 + code/modules/client/preferences_tgui.dm | 20 +- code/modules/client/verbs/who.dm | 10 +- code/modules/power/lighting.dm | 2 +- code/modules/tgs/LICENSE | 24 + code/modules/tgs/README.md | 13 + code/modules/tgs/core/README.md | 9 + code/modules/tgs/core/_definitions.dm | 8 + code/modules/tgs/core/byond_world_export.dm | 22 + code/modules/tgs/core/core.dm | 60 +- code/modules/tgs/core/datum.dm | 46 +- code/modules/tgs/core/tgs_version.dm | 1 + code/modules/tgs/includes.dm | 36 +- code/modules/tgs/v3210/README.md | 6 + code/modules/tgs/v3210/api.dm | 79 +-- code/modules/tgs/v3210/commands.dm | 51 +- code/modules/tgs/v4/README.md | 6 + code/modules/tgs/v4/api.dm | 97 ++- code/modules/tgs/v4/commands.dm | 41 +- code/modules/tgs/v5/README.md | 13 + code/modules/tgs/v5/__interop_version.dm | 1 + code/modules/tgs/v5/_defines.dm | 65 +- code/modules/tgs/v5/api.dm | 381 +++++------ code/modules/tgs/v5/api_vgs.dm | 90 --- code/modules/tgs/v5/bridge.dm | 106 +++ code/modules/tgs/v5/chunking.dm | 43 ++ code/modules/tgs/v5/commands.dm | 60 +- code/modules/tgs/v5/serializers.dm | 59 ++ code/modules/tgs/v5/topic.dm | 298 ++++++++ code/modules/tgs/v5/{undef.dm => undefs.dm} | 62 +- .../vorestation.dm} | 0 code/modules/tickets/procs.dm | 2 +- code/modules/tickets/tickets.dm | 37 +- code/modules/tickets/tickets_ui.dm | 6 +- config/example/config.txt | 9 +- dependencies.sh | 2 +- html/admin/panels.css | 64 +- html/browser/common.css | 642 ++++++++++-------- tgui/README.md | 18 +- .../tgui-panel/styles/tgchat/chat-dark.scss | 64 ++ .../tgui-panel/styles/tgchat/chat-light.scss | 41 ++ .../styles/tgchat/chat-vchatdark.scss | 64 ++ .../styles/tgchat/chat-vchatlight.scss | 41 ++ tools/bootstrap/javascript_.ps1 | 5 + tools/build/README.md | 4 +- tools/registration/.gitignore | 2 + tools/registration/config.json.example | 6 + tools/registration/index.js | 72 ++ tools/registration/package.json | 14 + tools/tgs_scripts/InstallDeps.sh | 52 ++ tools/tgs_scripts/PreCompile.bat | 6 + tools/tgs_scripts/PreCompile.sh | 59 ++ tools/tgs_scripts/WatchdogLaunch.sh | 6 + tools/tgs_test/Program.cs | 364 ++++++++++ tools/tgs_test/README.md | 11 + tools/tgs_test/StaticFile.cs | 5 + tools/tgs_test/TgsYml.cs | 15 + tools/tgs_test/Tgstation.TgsTest.csproj | 18 + tools/tgs_test/Tgstation.TgsTest.sln | 25 + vorestation.dme | 2 + 74 files changed, 3174 insertions(+), 1187 deletions(-) create mode 100644 .github/workflows/tgs_test.yml create mode 100644 .github/workflows/update_tgs_dmapi.yml create mode 100644 .tgs.yml create mode 100644 code/datums/tgs_event_handler.dm create mode 100644 code/modules/tgs/LICENSE create mode 100644 code/modules/tgs/README.md create mode 100644 code/modules/tgs/core/README.md create mode 100644 code/modules/tgs/core/byond_world_export.dm create mode 100644 code/modules/tgs/v3210/README.md create mode 100644 code/modules/tgs/v4/README.md create mode 100644 code/modules/tgs/v5/README.md create mode 100644 code/modules/tgs/v5/__interop_version.dm delete mode 100644 code/modules/tgs/v5/api_vgs.dm create mode 100644 code/modules/tgs/v5/bridge.dm create mode 100644 code/modules/tgs/v5/chunking.dm create mode 100644 code/modules/tgs/v5/serializers.dm create mode 100644 code/modules/tgs/v5/topic.dm rename code/modules/tgs/v5/{undef.dm => undefs.dm} (71%) rename code/modules/{tgs/v5/chat_commands.dm => tgs_commands/vorestation.dm} (100%) create mode 100644 tools/registration/.gitignore create mode 100644 tools/registration/config.json.example create mode 100644 tools/registration/index.js create mode 100644 tools/registration/package.json create mode 100644 tools/tgs_scripts/InstallDeps.sh create mode 100644 tools/tgs_scripts/PreCompile.bat create mode 100644 tools/tgs_scripts/PreCompile.sh create mode 100644 tools/tgs_scripts/WatchdogLaunch.sh create mode 100644 tools/tgs_test/Program.cs create mode 100644 tools/tgs_test/README.md create mode 100644 tools/tgs_test/StaticFile.cs create mode 100644 tools/tgs_test/TgsYml.cs create mode 100644 tools/tgs_test/Tgstation.TgsTest.csproj create mode 100644 tools/tgs_test/Tgstation.TgsTest.sln diff --git a/.github/workflows/tgs_test.yml b/.github/workflows/tgs_test.yml new file mode 100644 index 0000000000..eaaf9b307b --- /dev/null +++ b/.github/workflows/tgs_test.yml @@ -0,0 +1,70 @@ +name: TGS Test Suite +on: + push: + branches: + - master + - "project/**" + - "gh-readonly-queue/master/**" + - "gh-readonly-queue/project/**" + paths: + - ".tgs.yml" + - ".github/workflows/tgs_test.yml" + - "dependencies.sh" + - "code/__DEFINES/tgs.config.dm" + - "code/__DEFINES/tgs.dm" + - "code/game/world.dm" + - "code/modules/tgs/**" + - "tools/bootstrap/**" + - "tools/tgs_scripts/**" + - "tools/tgs_test/**" + pull_request: + branches: + - master + - "project/**" + paths: + - ".tgs.yml" + - ".github/workflows/tgs_test.yml" + - "dependencies.sh" + - "code/__DEFINES/tgs.config.dm" + - "code/__DEFINES/tgs.dm" + - "code/game/world.dm" + - "code/modules/tgs/**" + - "tools/bootstrap/**" + - "tools/tgs_scripts/**" + - "tools/tgs_test/**" + merge_group: + branches: + - master +env: + TGS_API_PORT: 5000 + PR_NUMBER: ${{ github.event.number }} +jobs: + test_tgs_docker: + if: ( !contains(github.event.head_commit.message, '[ci skip]') ) + name: Test TGS Docker + runs-on: ubuntu-24.04 + concurrency: + group: test_tgs_docker-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + services: + tgs: + image: tgstation/server + env: + Database__DatabaseType: Sqlite + Database__ConnectionString: Data Source=TGS_TGTest.sqlite3;Mode=ReadWriteCreate + General__ConfigVersion: 5.0.0 + General__ApiPort: ${{ env.TGS_API_PORT }} + General__SetupWizardMode: Never + ports: + - 5000:5000 #Can't use env here for some reason + steps: + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Test TGS Integration + run: dotnet run -c Release --project tools/tgs_test ${{ github.repository }} /tgs_instances/tgstation ${{ env.TGS_API_PORT }} ${{ github.event.pull_request.head.sha || github.sha }} ${{ secrets.GITHUB_TOKEN }} ${{ env.PR_NUMBER }} diff --git a/.github/workflows/update_tgs_dmapi.yml b/.github/workflows/update_tgs_dmapi.yml new file mode 100644 index 0000000000..285b393618 --- /dev/null +++ b/.github/workflows/update_tgs_dmapi.yml @@ -0,0 +1,62 @@ +name: Update TGS DMAPI + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + update-dmapi: + runs-on: ubuntu-24.04 + name: Update the TGS DMAPI + permissions: + contents: write + pull-requests: write + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: Branch + run: | + git branch -f tgs-dmapi-update + git checkout tgs-dmapi-update + git reset --hard master + + - name: Apply DMAPI update + uses: tgstation/tgs-dmapi-updater@v2 + id: dmapi-update + with: + header-path: "code/__defines/tgs.dm" + library-path: "code/modules/tgs" + + - name: Commit and Push + continue-on-error: true + run: | + git config user.name "tgstation-ci[bot]" + git config user.email "179393467+tgstation-ci[bot]@users.noreply.github.com" + git add . + git commit -m 'Update TGS DMAPI' + git push -f -u origin tgs-dmapi-update + + - name: Generate App Token + id: app-token-generation + uses: actions/create-github-app-token@v2 + if: env.APP_PRIVATE_KEY != '' && env.APP_ID != '' + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + env: + APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} + APP_ID: ${{ secrets.APP_ID }} + + - name: Create Pull Request + uses: repo-sync/pull-request@v2 + if: ${{ success() }} + with: + source_branch: "tgs-dmapi-update" + destination_branch: "master" + pr_title: "Automatic TGS DMAPI Update" + pr_body: "This pull request updates the TGS DMAPI to the latest version. Please note any changes that may be breaking or unimplemented in your codebase by checking what changes are in the definitions file: code/__DEFINES/tgs.dm before merging.\n\n${{ steps.dmapi-update.outputs.release-notes }}" + pr_label: "Tools" + pr_allow_empty: false + github_token: ${{ steps.app-token-generation.outputs.token || secrets.GITHUB_TOKEN }} diff --git a/.tgs.yml b/.tgs.yml new file mode 100644 index 0000000000..ae17d3b85c --- /dev/null +++ b/.tgs.yml @@ -0,0 +1,25 @@ +# This file is used by TGS (https://github.com/tgstation/tgstation-server) clients to quickly initialize a server instance for the codebase +# The format isn't documented anywhere but hopefully we never have to change it. If there are questions, contact the TGS maintainer Cyberboss/@Dominion#0444 +version: 1 +# The BYOND version to use (kept in sync with dependencies.sh by the "TGS Test Suite" CI job) +# Must be interpreted as a string, keep quoted +byond: "516.1664" +# Folders to create in "/Configuration/GameStaticFiles/" +static_files: + # Config directory should be static + - name: config + # This implies the folder should be pre-populated with contents from the repo + populate: true + # Data directory must be static + - name: data +# String dictionary. The value is the location of the file in the repo to upload to TGS. The key is the name of the file to upload to "/Configuration/EventScripts/" +# This one is for Linux hosted servers +linux_scripts: + PreCompile.sh: tools/tgs_scripts/PreCompile.sh + WatchdogLaunch.sh: tools/tgs_scripts/WatchdogLaunch.sh + InstallDeps.sh: tools/tgs_scripts/InstallDeps.sh +# Same as above for Windows hosted servers +windows_scripts: + PreCompile.bat: tools/tgs_scripts/PreCompile.bat +# The security level the game should be run at +security: Trusted diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e101d59165..5efaf21e8f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,7 +3,7 @@ "tasks": [ { "type": "process", - "command": "tools/build/build", + "command": "tools/build/build.sh", "windows": { "command": ".\\tools\\build\\build.bat" }, @@ -26,7 +26,7 @@ }, { "type": "process", - "command": "tools/build/build", + "command": "tools/build/build.sh", "args": ["-DLOWMEMORYMODE"], "windows": { "command": ".\\tools\\build\\build.bat", @@ -50,7 +50,7 @@ }, { "type": "process", - "command": "tools/build/build", + "command": "tools/build/build.sh", "args": ["-DLOWMEMORYMODE", "-DCITESTING", "-DUSE_MAP_TETHER"], "windows": { "command": ".\\tools\\build\\build.bat", @@ -74,7 +74,7 @@ }, { "type": "process", - "command": "tools/build/build", + "command": "tools/build/build.sh", "args": ["-DLOWMEMORYMODE", "-DCITESTING", "-DUSE_MAP_STELLARDELIGHT"], "windows": { "command": ".\\tools\\build\\build.bat", @@ -98,7 +98,7 @@ }, { "type": "process", - "command": "tools/build/build", + "command": "tools/build/build.sh", "args": ["-DLOWMEMORYMODE", "-DCITESTING", "-DUSE_MAP_GROUNDBASE"], "windows": { "command": ".\\tools\\build\\build.bat", @@ -122,7 +122,7 @@ }, { "type": "process", - "command": "tools/build/build", + "command": "tools/build/build.sh", "args": ["-DLOWMEMORYMODE", "-DCITESTING", "-DUSE_MAP_MINITEST"], "windows": { "command": ".\\tools\\build\\build.bat", diff --git a/code/__defines/chat.dm b/code/__defines/chat.dm index e427ef4c7d..1656b93ea5 100644 --- a/code/__defines/chat.dm +++ b/code/__defines/chat.dm @@ -35,3 +35,12 @@ #define MESSAGE_TYPE_ADMINLOG "adminlog" #define MESSAGE_TYPE_ATTACKLOG "attacklog" #define MESSAGE_TYPE_DEBUG "debug" + +/// Adds a generic box around whatever message you're sending in chat. Really makes things stand out. +#define boxed_message(str) ("
" + str + "
") +/// Adds a box around whatever message you're sending in chat. Can apply color and/or additional classes. Available colors: red, green, blue, purple. Use it like red_box +#define custom_boxed_message(classes, str) ("
" + str + "
") +/// Makes a fieldset with a neaty styled name. Can apply additional classes. +#define fieldset_block(title, content, classes) ("
" + title + "" + content + "
") +/// Makes a horizontal line with text in the middle +#define separator_hr(str) ("
" + str + "
") diff --git a/code/__defines/tgs.dm b/code/__defines/tgs.dm index e3ae002c58..b9b539c32e 100644 --- a/code/__defines/tgs.dm +++ b/code/__defines/tgs.dm @@ -1,261 +1,594 @@ -//tgstation-server DMAPI +// tgstation-server DMAPI +// The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in IETF RFC 2119. -#define TGS_DMAPI_VERSION "5.1.1" +#define TGS_DMAPI_VERSION "7.3.3" -//All functions and datums outside this document are subject to change with any version and should not be relied on +// All functions and datums outside this document are subject to change with any version and should not be relied on. -//CONFIGURATION +// CONFIGURATION -//create this define if you want to do configuration outside of this file +/// Consumers SHOULD create this define if you want to do TGS configuration outside of this file. #ifndef TGS_EXTERNAL_CONFIGURATION -//Comment this out once you've filled in the below +// Consumers MUST comment this out once you've filled in the below and are not using [TGS_EXTERNAL_CONFIGURATION]. #error TGS API unconfigured -//Uncomment this if you wish to allow the game to interact with TGS 3 -//This will raise the minimum required security level of your game to TGS_SECURITY_TRUSTED due to it utilizing call()() +// Consumers MUST uncomment this if you wish to allow the game to interact with TGS version 3. +// This will raise the minimum required security level of your game to TGS_SECURITY_TRUSTED due to it utilizing call()(). //#define TGS_V3_API -//Required interfaces (fill in with your codebase equivalent): +// Required interfaces (fill in with your codebase equivalent): -//create a global variable named `Name` and set it to `Value` +/// Create a global variable named `Name` and set it to `Value`. #define TGS_DEFINE_AND_SET_GLOBAL(Name, Value) -//Read the value in the global variable `Name` +/// Read the value in the global variable `Name`. #define TGS_READ_GLOBAL(Name) -//Set the value in the global variable `Name` to `Value` +/// Set the value in the global variable `Name` to `Value`. #define TGS_WRITE_GLOBAL(Name, Value) -//Disallow ANYONE from reflecting a given `path`, security measure to prevent in-game use of DD -> TGS capabilities +/// Disallow ANYONE from reflecting a given `path`, security measure to prevent in-game use of DD -> TGS capabilities. #define TGS_PROTECT_DATUM(Path) -//Display an announcement `message` from the server to all players +/// Display an announcement `message` from the server to all players. #define TGS_WORLD_ANNOUNCE(message) -//Notify current in-game administrators of a string `event` +/// Notify current in-game administrators of a string `event`. #define TGS_NOTIFY_ADMINS(event) -//Write an info `message` to a server log +/// Write an info `message` to a server log. #define TGS_INFO_LOG(message) -//Write an warning `message` to a server log +/// Write an warning `message` to a server log. #define TGS_WARNING_LOG(message) -//Write an error `message` to a server log +/// Write an error `message` to a server log. #define TGS_ERROR_LOG(message) -//Get the number of connected /clients +/// Get the number of connected /clients. #define TGS_CLIENT_COUNT #endif -//EVENT CODES +#ifndef TGS_FILE2TEXT_NATIVE +#ifdef file2text +#error Your codebase is re-defining the BYOND proc file2text. The DMAPI requires the native version to read the result of world.Export(). You SHOULD fix this by adding "#define TGS_FILE2TEXT_NATIVE file2text" before your override of file2text to allow the DMAPI to use the native version. This will only be used for world.Export(), not regular file accesses +#endif +#define TGS_FILE2TEXT_NATIVE file2text +#endif -#define TGS_EVENT_REBOOT_MODE_CHANGE -1 //Before a reboot mode change, extras parameters are the current and new reboot mode enums -#define TGS_EVENT_PORT_SWAP -2 //Before a port change is about to happen, extra parameters is new port -#define TGS_EVENT_INSTANCE_RENAMED -3 //Before the instance is renamed, extra parameter is the new name -#define TGS_EVENT_WATCHDOG_REATTACH -4 //After the watchdog reattaches to DD, extra parameter is the new /datum/tgs_version of the server +// SpacemanDMM compatibility +#ifndef CAN_BE_REDEFINED +#define CAN_BE_REDEFINED(X) +#endif -//See the descriptions for the parameters of these codes here: https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Host/Components/EventType.cs +// EVENT CODES + +/// Before a reboot mode change, extras parameters are the current and new reboot mode enums. +#define TGS_EVENT_REBOOT_MODE_CHANGE -1 +/// Before a port change is about to happen, extra parameters is new port. +#define TGS_EVENT_PORT_SWAP -2 +/// Before the instance is renamed, extra parameter is the new name. +#define TGS_EVENT_INSTANCE_RENAMED -3 +/// After the watchdog reattaches to DD, extra parameter is the new [/datum/tgs_version] of the server. +#define TGS_EVENT_WATCHDOG_REATTACH -4 +/// When the watchdog sends a health check to DD. No parameters. +#define TGS_EVENT_HEALTH_CHECK -5 + +/// When the repository is reset to its origin reference. Parameters: Reference name, Commit SHA. #define TGS_EVENT_REPO_RESET_ORIGIN 0 +/// When the repository performs a checkout. Parameters: Checkout git object. #define TGS_EVENT_REPO_CHECKOUT 1 +/// When the repository performs a fetch operation. No parameters. #define TGS_EVENT_REPO_FETCH 2 +/// When the repository test merges. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user. #define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3 +/// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path. #define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4 -#define TGS_EVENT_BYOND_INSTALL_START 5 -#define TGS_EVENT_BYOND_INSTALL_FAIL 6 -#define TGS_EVENT_BYOND_ACTIVE_VERSION_CHANGE 7 +/// Before a engine install operation begins. Parameters: Version string of the installing engine. +#define TGS_EVENT_ENGINE_INSTALL_START 5 +/// When a engine install operation fails. Parameters: Error message +#define TGS_EVENT_ENGINE_INSTALL_FAIL 6 +/// When the active engine version changes. Parameters: (Nullable) Version string of the current engine, version string of the new engine. +#define TGS_EVENT_ENGINE_ACTIVE_VERSION_CHANGE 7 +/// When the compiler starts running. Parameters: Game directory path, origin commit SHA. #define TGS_EVENT_COMPILE_START 8 +/// When a compile is cancelled. No parameters. #define TGS_EVENT_COMPILE_CANCELLED 9 +/// When a compile fails. Parameters: Game directory path, [TRUE]/[FALSE] based on if the cause for failure was DMAPI validation. #define TGS_EVENT_COMPILE_FAILURE 10 -#define TGS_EVENT_COMPILE_COMPLETE 11 // Note, this event fires before the new .dmb is loaded into the watchdog. Consider using the TGS_EVENT_DEPLOYMENT_COMPLETE instead +/// When a compile operation completes. Note, this event fires before the new .dmb is loaded into the watchdog. Consider using the [TGS_EVENT_DEPLOYMENT_COMPLETE] instead. Parameters: Game directory path. +#define TGS_EVENT_COMPILE_COMPLETE 11 +/// When an automatic update for the current instance begins. No parameters. #define TGS_EVENT_INSTANCE_AUTO_UPDATE_START 12 +/// When the repository encounters a merge conflict: Parameters: Base SHA, target SHA, base reference, target reference. #define TGS_EVENT_REPO_MERGE_CONFLICT 13 +/// When a deployment completes. No Parameters. #define TGS_EVENT_DEPLOYMENT_COMPLETE 14 +/// Before the watchdog shuts down. Not sent for graceful shutdowns. No parameters. #define TGS_EVENT_WATCHDOG_SHUTDOWN 15 +/// Before the watchdog detaches for a TGS update/restart. No parameters. #define TGS_EVENT_WATCHDOG_DETACH 16 +// We don't actually implement these 4 events as the DMAPI can never receive them. +// #define TGS_EVENT_WATCHDOG_LAUNCH 17 +// #define TGS_EVENT_WATCHDOG_CRASH 18 +// #define TGS_EVENT_WORLD_END_PROCESS 19 +// #define TGS_EVENT_WORLD_REBOOT 20 +/// Watchdog event when TgsInitializationComplete() is called. No parameters. +#define TGS_EVENT_WORLD_PRIME 21 +// DMAPI also doesnt implement this +// #define TGS_EVENT_DREAM_DAEMON_LAUNCH 22 +/// After a single submodule update is performed. Parameters: Updated submodule name. +#define TGS_EVENT_REPO_SUBMODULE_UPDATE 23 +/// After CodeModifications are applied, before DreamMaker is run. Parameters: Game directory path, origin commit sha, version string of the used engine. +#define TGS_EVENT_PRE_DREAM_MAKER 24 +/// Whenever a deployment folder is deleted from disk. Parameters: Game directory path. +#define TGS_EVENT_DEPLOYMENT_CLEANUP 25 -//OTHER ENUMS +// OTHER ENUMS +/// The server will reboot normally. #define TGS_REBOOT_MODE_NORMAL 0 +/// The server will stop running on reboot. #define TGS_REBOOT_MODE_SHUTDOWN 1 +/// The watchdog will restart on reboot. #define TGS_REBOOT_MODE_RESTART 2 +// Note that security levels are currently meaningless in OpenDream +/// DreamDaemon Trusted security level. #define TGS_SECURITY_TRUSTED 0 +/// DreamDaemon Safe security level. #define TGS_SECURITY_SAFE 1 +/// DreamDaemon Ultrasafe security level. #define TGS_SECURITY_ULTRASAFE 2 +/// DreamDaemon public visibility level. +#define TGS_VISIBILITY_PUBLIC 0 +/// DreamDaemon private visibility level. +#define TGS_VISIBILITY_PRIVATE 1 +/// DreamDaemon invisible visibility level. +#define TGS_VISIBILITY_INVISIBLE 2 + +/// The Build Your Own Net Dream engine. +#define TGS_ENGINE_TYPE_BYOND 0 +/// The OpenDream engine. +#define TGS_ENGINE_TYPE_OPENDREAM 1 + //REQUIRED HOOKS -//Call this somewhere in /world/New() that is always run -//IMPORTANT: This function may sleep! -//event_handler: optional user defined event handler. The default behaviour is to broadcast the event in english to all connected admin channels -//minimum_required_security_level: The minimum required security level to run the game in which the DMAPI is integrated -/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE) +/** + * Consumers MUST call this somewhere in [/world/proc/New] that is always run. This function may sleep! + * + * * event_handler - Optional user defined [/datum/tgs_event_handler]. + * * minimum_required_security_level: The minimum required security level to run the game in which the DMAPI is integrated. Can be one of [TGS_SECURITY_ULTRASAFE], [TGS_SECURITY_SAFE], or [TGS_SECURITY_TRUSTED]. + * * http_handler - Optional user defined [/datum/tgs_http_handler]. + */ +/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE, datum/tgs_http_handler/http_handler) + CAN_BE_REDEFINED(TRUE) return -//Call this when your initializations are complete and your game is ready to play before any player interactions happen -//This may use world.sleep_offline to make this happen so ensure no changes are made to it while this call is running -//Most importantly, before this point, note that any static files or directories may be in use by another server. Your code should account for this -//This function should not be called before ..() in /world/New() +/** + * Consumers MUST call this when world initializations are complete and the game is ready to play before any player interactions happen. + * + * This may use [/world/var/sleep_offline] to make this happen so ensure no changes are made to it while this call is running. + * Afterwards, consider explicitly setting it to what you want to avoid this BYOND bug: http://www.byond.com/forum/post/2575184 + * This function should not be called before ..() in [/world/proc/New]. + */ /world/proc/TgsInitializationComplete() + CAN_BE_REDEFINED(TRUE) return -//Put this at the start of /world/Topic() +/// Consumers MUST run this macro at the start of [/world/proc/Topic]. #define TGS_TOPIC var/tgs_topic_return = TgsTopic(args[1]); if(tgs_topic_return) return tgs_topic_return -#define VGS_TOPIC var/vgs_topic_return = VgsTopic(args[1]); if(vgs_topic_return) return vgs_topic_return // VOREStation Edit - VGS -//Call this at the beginning of world/Reboot(reason) +/// Consumers MUST call this as late as possible in [world/proc/Reboot] (BEFORE ..()). /world/proc/TgsReboot() + CAN_BE_REDEFINED(TRUE) return -//DATUM DEFINITIONS -//unless otherwise specified all datums defined here should be considered read-only, warranty void if written +// DATUM DEFINITIONS +// All datums defined here should be considered read-only -//represents git revision information about the current world build +/// Represents git revision information. /datum/tgs_revision_information - var/commit //full sha of compiled commit - var/origin_commit //full sha of last known remote commit. This may be null if the TGS repository is not currently tracking a remote branch + /// Full SHA of the commit. + var/commit + /// ISO 8601 timestamp of when the commit was created. + var/timestamp + /// Full sha of last known remote commit. This may be null if the TGS repository is not currently tracking a remote branch. + var/origin_commit -//represents a version of tgstation-server +/// Represents a version. /datum/tgs_version - var/suite //The suite/major version, can be >=3 + /// The suite/major version number. + var/suite - //this group of variables can be null to represent a wild card - var/minor //The minor version - var/patch //The patch version - var/deprecated_patch //The legacy version + // This group of variables can be null to represent a wild card. + /// The minor version number. null for wildcards. + var/minor + /// The patch version number. null for wildcards. + var/patch - var/raw_parameter //The unparsed parameter - var/deprefixed_parameter //The version only bit of raw_parameter + /// Legacy version number. Generally null. + var/deprecated_patch -//if the tgs_version is a wildcard version + /// Unparsed string value. + var/raw_parameter + /// String value minus prefix. + var/deprefixed_parameter + +/** + * Returns [TRUE]/[FALSE] based on if the [/datum/tgs_version] contains wildcards. + */ /datum/tgs_version/proc/Wildcard() + CAN_BE_REDEFINED(TRUE) return -//if the tgs_version equals some other_version +/** + * Returns [TRUE]/[FALSE] based on if the [/datum/tgs_version] equals some other version. + * + * other_version - The [/datum/tgs_version] to compare against. + */ /datum/tgs_version/proc/Equals(datum/tgs_version/other_version) + CAN_BE_REDEFINED(TRUE) return -//represents a merge of a GitHub pull request +/// Represents a merge of a GitHub pull request. /datum/tgs_revision_information/test_merge - var/number //pull request number - var/title //pull request title - var/body //pull request body - var/author //pull request github author - var/url //link to pull request html - var/pull_request_commit //commit of the pull request when it was merged - var/time_merged //timestamp of when the merge commit for the pull request was created - var/comment //optional comment left by the one who initiated the test merge + /// The test merge number. + var/number + /// The test merge source's title when it was merged. + var/title + /// The test merge source's body when it was merged. + var/body + /// The Username of the test merge source's author. + var/author + /// An http URL to the test merge source. + var/url + /// The SHA of the test merge when that was merged. + var/head_commit + /// Optional comment left by the TGS user who initiated the merge. + var/comment -//represents a connected chat channel +/// Represents a connected chat channel. /datum/tgs_chat_channel - var/id //internal channel representation - var/friendly_name //user friendly channel name - var/connection_name //the name of the configured chat connection - var/is_admin_channel //if the server operator has marked this channel for game admins only - var/is_private_channel //if this is a private chat channel - var/custom_tag //user defined string associated with channel + /// TGS internal channel ID. + var/id + /// User friendly name of the channel. + var/friendly_name + /// Name of the chat connection. This is the IRC server address or the Discord guild. + var/connection_name + /// [TRUE]/[FALSE] based on if the server operator has marked this channel for game admins only. + var/is_admin_channel + /// [TRUE]/[FALSE] if the channel is a private message channel for a [/datum/tgs_chat_user]. + var/is_private_channel + /// Tag string associated with the channel in TGS. + var/custom_tag + /// [TRUE]/[FALSE] if the channel supports embeds. + var/embeds_supported -//represents a chat user +// Represents a chat user /datum/tgs_chat_user - var/id //Internal user representation, requires channel to be unique - var/friendly_name //The user's public name - var/mention //The text to use to ping this user in a message - var/datum/tgs_chat_channel/channel //The /datum/tgs_chat_channel this user was from + /// TGS internal user ID. + var/id + /// The user's display name. + var/friendly_name + /// The string to use to ping this user in a message. + var/mention + /// The [/datum/tgs_chat_channel] the user was from. + var/datum/tgs_chat_channel/channel -//user definable callback for handling events -//extra parameters may be specified depending on the event +/// User definable handler for TGS events This abstract version SHOULD be overridden to be used. +/datum/tgs_event_handler + /// If the handler receieves [TGS_EVENT_HEALTH_CHECK] events. + var/receive_health_checks = FALSE + +/** + * User definable callback for handling TGS events. + * + * event_code - One of the TGS_EVENT_ defines. Extra parameters will be documented in each. + */ /datum/tgs_event_handler/proc/HandleEvent(event_code, ...) set waitfor = FALSE return -//user definable chat command -/datum/tgs_chat_command - var/name = "" //the string to trigger this command on a chat bot. e.g. TGS3_BOT: do_this_command - var/help_text = "" //help text for this command - var/admin_only = FALSE //set to TRUE if this command should only be usable by registered chat admins +/// User definable handler for HTTP calls. This abstract version MUST be overridden to be used. +/datum/tgs_http_handler -//override to implement command -//sender: The tgs_chat_user who send to command -//params: The trimmed string following the command name -//The return value will be stringified and sent to the appropriate chat +/** + * User definable callback for executing HTTP GET requests. + * MUST perform BYOND sleeps while the request is in flight. + * MUST return a [/datum/tgs_http_result]. + * SHOULD log its own errors + * + * url - The full URL to execute the GET request for including query parameters. + */ +/datum/tgs_http_handler/proc/PerformGet(url) + CRASH("[type]/PerformGet not implemented!") + +/// Result of a [/datum/tgs_http_handler] call. MUST NOT be overridden. +/datum/tgs_http_result + /// HTTP response as text + var/response_text + /// Boolean request success flag. Set for any 2XX response code. + var/success + +/** + * Create a [/datum/tgs_http_result]. + * + * * response_text - HTTP response as text. Must be provided in New(). + * * success - Boolean request success flag. Set for any 2XX response code. Must be provided in New(). + */ +/datum/tgs_http_result/New(response_text, success) + if(response_text && !istext(response_text)) + CRASH("response_text was not text!") + + src.response_text = response_text + src.success = success + +/// User definable chat command. This abstract version MUST be overridden to be used. +/datum/tgs_chat_command + /// The string to trigger this command on a chat bot. e.g `@bot name ...` or `!tgs name ...`. + var/name = "" + /// The help text displayed for this command. + var/help_text = "" + /// If this command should be available to game administrators only. + var/admin_only = FALSE + /// A subtype of [/datum/tgs_chat_command] that is ignored when enumerating available commands. Use this to create shared base /datums for commands. + var/ignore_type + +/** + * Process command activation. Should return a [/datum/tgs_message_content] to respond to the issuer with. + * MUST be implemented + * + * * sender - The [/datum/tgs_chat_user] who issued the command. + * * params - The trimmed string following the command `/datum/tgs_chat_command/var/name]. + */ /datum/tgs_chat_command/proc/Run(datum/tgs_chat_user/sender, params) CRASH("[type] has no implementation for Run()") -//FUNCTIONS +/// User definable chat message. MUST NOT be overridden. +/datum/tgs_message_content + /// The string content of the message. Must be provided in New(). + var/text -//Returns the respective supported /datum/tgs_version of the API -/world/proc/TgsMaximumAPIVersion() + /// The [/datum/tgs_chat_embed] to embed in the message. Not supported on all chat providers. + var/datum/tgs_chat_embed/structure/embed + +/** + * Create a [/datum/tgs_message_content]. + * + * * text - The string content of the message. + */ +/datum/tgs_message_content/New(text) + ..() + if(!istext(text)) + TGS_ERROR_LOG("[/datum/tgs_message_content] created with no text!") + text = null + + src.text = text + +/// User definable chat embed. Currently mirrors Discord chat embeds. See https://discord.com/developers/docs/resources/message#embed-object for details. +/datum/tgs_chat_embed/structure + var/title + var/description + var/url + + /// Timestamp must be encoded as: time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss"). Use the active timezone. + var/timestamp + + /// Colour must be #AARRGGBB or #RRGGBB hex string. + var/colour + + /// See https://discord.com/developers/docs/resources/message#embed-object-embed-image-structure for details. + var/datum/tgs_chat_embed/media/image + + /// See https://discord.com/developers/docs/resources/message#embed-object-embed-thumbnail-structure for details. + var/datum/tgs_chat_embed/media/thumbnail + + /// See https://discord.com/developers/docs/resources/message#embed-object-embed-video-structure for details. + var/datum/tgs_chat_embed/media/video + + var/datum/tgs_chat_embed/footer/footer + var/datum/tgs_chat_embed/provider/provider + var/datum/tgs_chat_embed/provider/author/author + + var/list/datum/tgs_chat_embed/field/fields + +/// Common datum for similar Discord embed medias. +/datum/tgs_chat_embed/media + /// Must be set in New(). + var/url + var/width + var/height + var/proxy_url + +/// Create a [/datum/tgs_chat_embed]. +/datum/tgs_chat_embed/media/New(url) + ..() + if(!istext(url)) + CRASH("[/datum/tgs_chat_embed/media] created with no url!") + + src.url = url + +/// See https://discord.com/developers/docs/resources/message#embed-object-embed-footer-structure for details. +/datum/tgs_chat_embed/footer + /// Must be set in New(). + var/text + var/icon_url + var/proxy_icon_url + +/// Create a [/datum/tgs_chat_embed/footer]. +/datum/tgs_chat_embed/footer/New(text) + ..() + if(!istext(text)) + CRASH("[/datum/tgs_chat_embed/footer] created with no text!") + + src.text = text + +/// See https://discord.com/developers/docs/resources/message#embed-object-embed-provider-structure for details. +/datum/tgs_chat_embed/provider + var/name + var/url + +/// See https://discord.com/developers/docs/resources/message#embed-object-embed-author-structure for details. Must have name set in New(). +/datum/tgs_chat_embed/provider/author + var/icon_url + var/proxy_icon_url + +/// Create a [/datum/tgs_chat_embed/footer]. +/datum/tgs_chat_embed/provider/author/New(name) + ..() + if(!istext(name)) + CRASH("[/datum/tgs_chat_embed/provider/author] created with no name!") + + src.name = name + +/// See https://discord.com/developers/docs/resources/message#embed-object-embed-field-structure for details. +/datum/tgs_chat_embed/field + /// Must be set in New(). + var/name + /// Must be set in New(). + var/value + var/is_inline + +/// Create a [/datum/tgs_chat_embed/field]. +/datum/tgs_chat_embed/field/New(name, value) + ..() + if(!istext(name)) + CRASH("[/datum/tgs_chat_embed/field] created with no name!") + + if(!istext(value)) + CRASH("[/datum/tgs_chat_embed/field] created with no value!") + + src.name = name + src.value = value + +// API FUNCTIONS + +/// Returns the maximum supported [/datum/tgs_version] of the DMAPI. +/world/proc/TgsMaximumApiVersion() + CAN_BE_REDEFINED(TRUE) return -/world/proc/TgsMinimumAPIVersion() +/// Returns the minimum supported [/datum/tgs_version] of the DMAPI. +/world/proc/TgsMinimumApiVersion() + CAN_BE_REDEFINED(TRUE) return -//Returns TRUE if the world was launched under the server tools and the API matches, FALSE otherwise -//No function below this succeeds if it returns FALSE or if TgsNew() has yet to be called +/** + * Returns [TRUE] if DreamDaemon was launched under TGS, the API matches, and was properly initialized. [FALSE] will be returned otherwise. + */ /world/proc/TgsAvailable() + CAN_BE_REDEFINED(TRUE) return -//Forces a hard reboot of BYOND by ending the process -//unlike del(world) clients will try to reconnect -//If the service has not requested a shutdown, the next server will take over +// No function below this succeeds if it TgsAvailable() returns FALSE or if TgsNew() has yet to be called. + +/** + * Forces a hard reboot of DreamDaemon by ending the process. This function may sleep! + * + * Unlike del(world) clients will try to reconnect. + * If TGS has not requested a [TGS_REBOOT_MODE_SHUTDOWN] DreamDaemon will be launched again. + */ /world/proc/TgsEndProcess() + CAN_BE_REDEFINED(TRUE) return -//Send a message to non-admin connected chats -//message: The message to send -//admin_only: If TRUE, message will instead be sent to only admin connected chats -/world/proc/TgsTargetedChatBroadcast(message, admin_only) +/** + * Send a message to connected chats. This function may sleep! + * If TGS is offline when called, the message may be placed in a queue to be sent and this function will return immediately. Your message will be sent when TGS reconnects to the game. + * + * message - The [/datum/tgs_message_content] to send. + * admin_only: If [TRUE], message will be sent to admin connected chats. Vice-versa applies. + */ +/world/proc/TgsTargetedChatBroadcast(datum/tgs_message_content/message, admin_only = FALSE) + CAN_BE_REDEFINED(TRUE) return -//Send a private message to a specific user -//message: The message to send -//user: The /datum/tgs_chat_user to send to -/world/proc/TgsChatPrivateMessage(message, datum/tgs_chat_user/user) +/** + * Send a private message to a specific user. This function may sleep! + * If TGS is offline when called, the message may be placed in a queue to be sent and this function will return immediately. Your message will be sent when TGS reconnects to the game. + * + * message - The [/datum/tgs_message_content] to send. + * user: The [/datum/tgs_chat_user] to PM. + */ +/world/proc/TgsChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user) + CAN_BE_REDEFINED(TRUE) return -//The following functions will sleep if a call to TgsNew() is sleeping - -//Sends a message to connected game chats -//message: The message to send -//channels: optional channels to limit the broadcast to -/world/proc/TgsChatBroadcast(message, list/channels) +/** + * Send a message to connected chats that are flagged as game-related in TGS. This function may sleep! + * If TGS is offline when called, the message may be placed in a queue to be sent and this function will return immediately. Your message will be sent when TGS reconnects to the game. + * + * message - The [/datum/tgs_message_content] to send. + * channels - Optional list of [/datum/tgs_chat_channel]s to restrict the message to. + */ +/world/proc/TgsChatBroadcast(datum/tgs_message_content/message, list/channels = null) + CAN_BE_REDEFINED(TRUE) return -//Gets the current /datum/tgs_version of the server tools running the server +/// Returns the current [/datum/tgs_version] of TGS if it is running the server, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! /world/proc/TgsVersion() + CAN_BE_REDEFINED(TRUE) return -//Gets the current /datum/tgs_version of the DMAPI being used +/// Returns the running engine type +/world/proc/TgsEngine() + CAN_BE_REDEFINED(TRUE) + return + +/// Returns the current [/datum/tgs_version] of the DMAPI being used if it was activated, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! /world/proc/TgsApiVersion() + CAN_BE_REDEFINED(TRUE) return -//Gets the name of the TGS instance running the game +/// Returns the name of the TGS instance running the game if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! /world/proc/TgsInstanceName() + CAN_BE_REDEFINED(TRUE) return -//Get the current `/datum/tgs_revision_information` +/// Return the current [/datum/tgs_revision_information] of the running server if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! /world/proc/TgsRevision() + CAN_BE_REDEFINED(TRUE) return -//Get the current BYOND security level +/// Returns the current BYOND security level as a TGS_SECURITY_ define if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! /world/proc/TgsSecurityLevel() + CAN_BE_REDEFINED(TRUE) return -//Gets a list of active `/datum/tgs_revision_information/test_merge`s +/// Returns the current BYOND visibility level as a TGS_VISIBILITY_ define if TGS is present, null otherwise. Requires TGS to be using interop API version 5 or higher otherwise the string "___unimplemented" wil be returned. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! +/world/proc/TgsVisibility() + CAN_BE_REDEFINED(TRUE) + return + +/// Returns a list of active [/datum/tgs_revision_information/test_merge]s if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! /world/proc/TgsTestMerges() + CAN_BE_REDEFINED(TRUE) return -//Gets a list of connected tgs_chat_channel +/// Returns a list of connected [/datum/tgs_chat_channel]s if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! /world/proc/TgsChatChannelInfo() + CAN_BE_REDEFINED(TRUE) + return + +/** + * Trigger an event in TGS. Requires TGS version >= 6.3.0. Returns [TRUE] if the event was triggered successfully, [FALSE] otherwise. This function may sleep! + * + * event_name - The name of the event to trigger + * parameters - Optional list of string parameters to pass as arguments to the event script. The first parameter passed to a script will always be the running game's directory followed by these parameters. + * wait_for_completion - If set, this function will not return until the event has run to completion. + */ +/world/proc/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE) + CAN_BE_REDEFINED(TRUE) return /* The MIT License -Copyright (c) 2017 Jordan Brown +Copyright (c) 2017-2024 Jordan Brown Permission is hereby granted, free of charge, to any person obtaining a copy of this software and diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index e17ef71d9a..6bc2597bbe 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -747,3 +747,7 @@ /// Turn this on if you want all admin-PMs to go to be sent to discord, and not only the first message of a ticket. /datum/config_entry/flag/discord_ahelps_all default = FALSE + +/datum/config_entry/number/rounds_until_hard_restart + default = -1 + min_val = 0 diff --git a/code/controllers/configuration/entries/vorestation.dm b/code/controllers/configuration/entries/vorestation.dm index 09f49497ec..8253d66e6c 100644 --- a/code/controllers/configuration/entries/vorestation.dm +++ b/code/controllers/configuration/entries/vorestation.dm @@ -2,11 +2,7 @@ /datum/config_entry/flag/items_survive_digestion default = TRUE -/datum/config_entry/string/vgs_access_identifier // VGS - default = null - protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN - -/datum/config_entry/number/vgs_server_port // VGS +/datum/config_entry/number/register_server_port default = null min_val = 0 max_val = 65535 diff --git a/code/datums/elements/footstep.dm b/code/datums/elements/footstep.dm index 59f8906ce7..4c570350d7 100644 --- a/code/datums/elements/footstep.dm +++ b/code/datums/elements/footstep.dm @@ -152,7 +152,7 @@ // we are wearing shoes var/obj/item/clothing/shoes/feet = source.shoes - if(feet.blocks_footsteps) + if(istype(feet) && feet.blocks_footsteps) var/shoestep_type = prepared_steps[FOOTSTEP_MOB_SHOE] if(!isnull(shoestep_type) && footstep_sounds[shoestep_type]) // shoestep type can be null playsound(source.loc, pick(footstep_sounds[shoestep_type][1]), diff --git a/code/datums/helper_datums/getrev.dm b/code/datums/helper_datums/getrev.dm index 854e6db9ce..1ecd7da806 100644 --- a/code/datums/helper_datums/getrev.dm +++ b/code/datums/helper_datums/getrev.dm @@ -1,82 +1,91 @@ /datum/getrev - var/branch - var/revision + var/commit // git rev-parse HEAD var/date - var/showinfo + var/originmastercommit // git rev-parse origin/master var/list/testmerge = list() /datum/getrev/New() - if(world.TgsAvailable()) // Try TGS maybe - testmerge = world.TgsTestMerges() - var/datum/tgs_revision_information/REV = world.TgsRevision() - if(REV) - revision = REV.origin_commit || REV.commit - branch = "-Using TGS-" // TGS doesn't provide branch info yet - date = "-Using TGS-" // Or date + commit = rustg_git_revparse("HEAD") + if(commit) + date = rustg_git_commit_date(commit) + originmastercommit = rustg_git_revparse("origin/master") - if(!revision) // File parse method - var/list/head_branch = file2list(".git/HEAD", "\n") - if(head_branch.len) - branch = copytext(head_branch[1], 17) +/datum/getrev/proc/load_tgs_info() + testmerge = world.TgsTestMerges() + var/datum/tgs_revision_information/revinfo = world.TgsRevision() + if(revinfo) + commit = revinfo.commit + originmastercommit = revinfo.origin_commit + date = revinfo.timestamp || rustg_git_commit_date(commit) - var/list/head_log = file2list(".git/logs/HEAD", "\n") - for(var/line=head_log.len, line>=1, line--) - if(head_log[line]) - var/list/last_entry = splittext(head_log[line], " ") - if(last_entry.len < 2) continue - revision = last_entry[2] - // Get date/time - if(last_entry.len >= 5) - var/unix_time = text2num(last_entry[5]) - if(unix_time) - date = unix2date(unix_time) - break + // goes to DD log and config_error.txt + log_world(get_log_message()) - to_world_log("-Revision Info-") - to_world_log("Branch: [branch]") - to_world_log("Date: [date]") - to_world_log("Revision: [revision]") +/datum/getrev/proc/get_log_message() + var/list/msg = list() + msg += "Running /tg/ revision: [date]" + if(originmastercommit) + msg += "origin/master: [originmastercommit]" + + for(var/line in testmerge) + var/datum/tgs_revision_information/test_merge/tm = line + msg += "Test merge active of PR #[tm.number] commit [tm.head_commit]" + //SSblackbox.record_feedback("associative", "testmerged_prs", 1, list("number" = "[tm.number]", "commit" = "[tm.head_commit]", "title" = "[tm.title]", "author" = "[tm.author]")) + + if(commit && commit != originmastercommit) + msg += "HEAD: [commit]" + else if(!originmastercommit) + msg += "No commit information" + + msg += "Running rust-g version [rustg_get_version()]" + + return msg.Join("\n") /datum/getrev/proc/GetTestMergeInfo(header = TRUE) - . = list() if(!testmerge.len) - return - if(header) - . += "The following pull requests are currently test merged:" - for(var/datum/tgs_revision_information/test_merge/tm as anything in testmerge) - var/cm = tm.pull_request_commit + return "" + . = header ? "The following pull requests are currently test merged:
" : "" + for(var/line in testmerge) + var/datum/tgs_revision_information/test_merge/tm = line + var/cm = tm.head_commit var/details = ": '" + html_encode(tm.title) + "' by " + html_encode(tm.author) + " at commit " + html_encode(copytext_char(cm, 1, 11)) - if(details && findtext(details, "\[s\]") && (!usr || !usr.client.holder)) - continue - . += "#[tm.number][details]" + . += "#[tm.number][details]
" /client/verb/showrevinfo() set category = "OOC.Game" set name = "Show Server Revision" set desc = "Check the current server code revision" - if(!GLOB.revdata) - to_chat(src, span_warning("Please wait until server initializations are complete.")) - return - var/list/msg = list() + // Round ID + if(GLOB.round_id) + msg += span_bold("Round ID") + ": [GLOB.round_id]" - if(GLOB.revdata.revision) - msg += span_bold("Server revision:") + " B:[GLOB.revdata.branch] D:[GLOB.revdata.date]" - if(CONFIG_GET(string/githuburl)) - msg += span_bold("Commit:") + " [GLOB.revdata.revision]" - else - msg += span_bold("Commit:") + " GLOB.revdata.revision" - else - msg += span_bold("Server revision:") + " Unknown" + msg += span_bold("BYOND Version") + ": [world.byond_version].[world.byond_build]" + if(DM_VERSION != world.byond_version || DM_BUILD != world.byond_build) + msg += span_bold("Compiled with BYOND Version") + ": [DM_VERSION].[DM_BUILD]" + // Revision information + var/datum/getrev/revdata = GLOB.revdata + msg += span_bold("Server revision compiled on") + ": [revdata.date]" + var/pc = revdata.originmastercommit + if(pc) + msg += span_bold("Master commit") + ": [pc]" + if(length(revdata.testmerge)) + msg += revdata.GetTestMergeInfo() + if(revdata.commit && revdata.commit != revdata.originmastercommit) + msg += span_bold("Local commit") + ": [revdata.commit]" + else if(!pc) + msg += "No commit information" if(world.TgsAvailable()) var/datum/tgs_version/version = world.TgsVersion() - msg += span_bold("TGS version:") + " [version.raw_parameter]" - var/datum/tgs_version/api_version = world.TgsApiVersion() - msg += span_bold("DMAPI version:") + " [api_version.raw_parameter]" + msg += span_bold("TGS version") + ": [version.raw_parameter]" + msg += span_bold("DMAPI version") + ": [TGS_DMAPI_VERSION]" - if(GLOB.revdata.testmerge.len) - msg += GLOB.revdata.GetTestMergeInfo() - - to_chat(src, msg.Join("
")) + // Game mode odds + //msg += "
Current Informational Settings:" + //msg += "Protect Authority Roles From Traitor: [CONFIG_GET(flag/protect_roles_from_antagonist) ? "Yes" : "No"]" + //msg += "Protect Assistant Role From Traitor: [CONFIG_GET(flag/protect_assistant_from_antagonist) ? "Yes" : "No"]" + //msg += "Enforce Human Authority: [CONFIG_GET(string/human_authority) ? "Yes" : "No"]" + //msg += "Allow Latejoin Antagonists: [CONFIG_GET(flag/allow_latejoin_antagonists) ? "Yes" : "No"]" + to_chat(src, fieldset_block("Server Revision Info", span_infoplain(jointext(msg, "
")), "boxed_message"), type = MESSAGE_TYPE_INFO) diff --git a/code/datums/tgs_event_handler.dm b/code/datums/tgs_event_handler.dm new file mode 100644 index 0000000000..6fd3d5b53a --- /dev/null +++ b/code/datums/tgs_event_handler.dm @@ -0,0 +1,41 @@ +/datum/tgs_event_handler/impl + var/datum/timedevent/reattach_timer + +/datum/tgs_event_handler/impl/HandleEvent(event_code, ...) + switch(event_code) + if(TGS_EVENT_REBOOT_MODE_CHANGE) + var/list/reboot_mode_lookup = list ("[TGS_REBOOT_MODE_NORMAL]" = "be normal", "[TGS_REBOOT_MODE_SHUTDOWN]" = "shutdown the server", "[TGS_REBOOT_MODE_RESTART]" = "hard restart the server") + var/old_reboot_mode = args[2] + var/new_reboot_mode = args[3] + message_admins("TGS: Reboot will no longer [reboot_mode_lookup["[old_reboot_mode]"]], it will instead [reboot_mode_lookup["[new_reboot_mode]"]]") + if(TGS_EVENT_PORT_SWAP) + message_admins("TGS: Changing port from [world.port] to [args[2]]") + if(TGS_EVENT_INSTANCE_RENAMED) + message_admins("TGS: Instance renamed to from [world.TgsInstanceName()] to [args[2]]") + if(TGS_EVENT_COMPILE_START) + message_admins("TGS: Deployment started, new game version incoming...") + if(TGS_EVENT_COMPILE_CANCELLED) + message_admins("TGS: Deployment cancelled!") + if(TGS_EVENT_COMPILE_FAILURE) + message_admins("TGS: Deployment failed!") + if(TGS_EVENT_DEPLOYMENT_COMPLETE) + message_admins("TGS: Deployment complete!") + to_chat(world, span_boldannounce("Server updated, changes will be applied on the next round...")) + if(TGS_EVENT_WATCHDOG_DETACH) + message_admins("TGS restarting...") + reattach_timer = addtimer(CALLBACK(src, PROC_REF(LateOnReattach)), 1 MINUTES, TIMER_STOPPABLE) + if(TGS_EVENT_WATCHDOG_REATTACH) + var/datum/tgs_version/old_version = world.TgsVersion() + var/datum/tgs_version/new_version = args[2] + if(!old_version.Equals(new_version)) + to_chat(world, span_boldannounce("TGS updated to v[new_version.deprefixed_parameter]")) + else + message_admins("TGS: Back online") + if(reattach_timer) + deltimer(reattach_timer) + reattach_timer = null + if(TGS_EVENT_WATCHDOG_SHUTDOWN) + to_chat_immediate(world, span_boldannounce("Server is shutting down!")) + +/datum/tgs_event_handler/impl/proc/LateOnReattach() + message_admins("Warning: TGS hasn't notified us of it coming back for a full minute! Is there a problem?") diff --git a/code/game/world.dm b/code/game/world.dm index eb9c167fc7..73b74a0365 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -1,3 +1,7 @@ +#define RESTART_COUNTER_PATH "data/round_counter.txt" + +GLOBAL_VAR(restart_counter) + #define RECOMMENDED_VERSION 513 /world/New() world_startup_time = world.timeofday @@ -21,7 +25,7 @@ if(byond_version < RECOMMENDED_VERSION) to_world_log("Your server's byond version does not meet the recommended requirements for this server. Please update BYOND") - TgsNew() + InitTgs() config.Load(params[OVERRIDE_CONFIG_DIRECTORY_PARAMETER]) @@ -29,7 +33,6 @@ ConfigLoaded() makeDatumRefLists() - VgsNew() var servername = CONFIG_GET(string/servername) if(config && servername != null && CONFIG_GET(flag/server_suffix) && world.port > 0) @@ -92,6 +95,11 @@ return +/// Initializes TGS and loads the returned revising info into GLOB.revdata +/world/proc/InitTgs() + TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_TRUSTED) + GLOB.revdata.load_tgs_info() + /// Runs after config is loaded but before Master is initialized /world/proc/ConfigLoaded() // Everything in here is prioritized in a very specific way. @@ -108,12 +116,15 @@ else //probably windows, if not this should work anyway CONFIG_SET(string/python_path, "python") + if(fexists(RESTART_COUNTER_PATH)) + GLOB.restart_counter = text2num(trim(file2text(RESTART_COUNTER_PATH))) + fdel(RESTART_COUNTER_PATH) + var/world_topic_spam_protect_ip = "0.0.0.0" var/world_topic_spam_protect_time = world.timeofday /world/Topic(T, addr, master, key) TGS_TOPIC - VGS_TOPIC // VOREStation Edit - VGS log_topic("\"[T]\", from:[addr], master:[master], key:[key]") if (T == "ping") @@ -249,8 +260,8 @@ var/world_topic_spam_protect_time = world.timeofday return list2params(positions) else if(T == "revision") - if(GLOB.revdata.revision) - return list2params(list(branch = GLOB.revdata.branch, date = GLOB.revdata.date, revision = GLOB.revdata.revision)) + if(GLOB.revdata.commit) + return list2params(list(testmerge = GLOB.revdata.testmerge, date = GLOB.revdata.date, commit = GLOB.revdata.commit, originmastercommit = GLOB.revdata.originmastercommit)) else return "unknown" @@ -431,6 +442,25 @@ var/world_topic_spam_protect_time = world.timeofday else return "Database connection failed or not set up" +/// Returns TRUE if the world should do a TGS hard reboot. +/world/proc/check_hard_reboot() + if(!TgsAvailable()) + return FALSE + // byond-tracy can't clean up itself, and thus we should always hard reboot if its enabled, to avoid an infinitely growing trace. + //if(Tracy?.enabled) + // return TRUE + var/ruhr = CONFIG_GET(number/rounds_until_hard_restart) + switch(ruhr) + if(-1) + return FALSE + if(0) + return TRUE + else + if(GLOB.restart_counter >= ruhr) + return TRUE + else + text2file("[++GLOB.restart_counter]", RESTART_COUNTER_PATH) + return FALSE /world/Reboot(reason = 0, fast_track = FALSE) /*spawn(0) @@ -451,6 +481,14 @@ var/world_topic_spam_protect_time = world.timeofday if(CONFIG_GET(string/server)) //if you set a server location in config.txt, it sends you there instead of trying to reconnect to the same world address. -- NeoFite C << link("byond://[CONFIG_GET(string/server)]") + if(check_hard_reboot()) + log_world("World hard rebooted at [time_stamp()]") + //shutdown_logging() // See comment below. + //QDEL_NULL(Tracy) + //QDEL_NULL(Debugger) + TgsEndProcess() + return ..() + TgsReboot() log_world("World rebooted at [time_stamp()]") ..() @@ -730,3 +768,5 @@ var/global/game_id = null if (debug_server) call_ext(debug_server, "auxtools_shutdown")() . = ..() + +#undef RESTART_COUNTER_PATH diff --git a/code/modules/admin/permissionedit.dm b/code/modules/admin/permissionedit.dm index ef61823dc7..7b0bad2904 100644 --- a/code/modules/admin/permissionedit.dm +++ b/code/modules/admin/permissionedit.dm @@ -455,7 +455,13 @@ ADMIN_VERB(edit_admin_permissions, R_PERMISSIONS, "Permissions Panel", "Edit adm #undef RANK_DONE +/// Changes, for this round only, the flags a particular admin gets to use /datum/admins/proc/change_admin_flags(admin_ckey, admin_key, datum/admins/admin_holder) + if(!check_rights(R_PERMISSIONS)) + return + if(IsAdminAdvancedProcCall()) + to_chat(usr, span_adminprefix("Rank Modification blocked: Advanced ProcCall detected."), confidential = TRUE) + return var/new_flags = input_bitfield( usr, "Admin rights
This will affect only the current admin [admin_key]", @@ -465,6 +471,8 @@ ADMIN_VERB(edit_admin_permissions, R_PERMISSIONS, "Permissions Panel", "Edit adm 590, allowed_edit_field = usr.client.holder.can_edit_rights_flags(), ) + if(isnull(new_flags)) + return admin_holder.disassociate() diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index 28d5018faa..9ecdad69b8 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -151,7 +151,10 @@ log_and_message_admins("[ckey] has registered their Discord ID to obtain the Crew Member role. Their Discord snowflake ID is: [their_id]", src) admin_chat_message(message = "[ckey] has registered their Discord ID to obtain the Crew Member role. Their Discord is: <@[their_id]>", color = "#4eff22") notes_add(ckey, "Discord ID: [their_id]") - world.VgsAddMemberRole(their_id) + var/port = CONFIG_GET(number/register_server_port) + if(port) + // Designed to be used with `tools/registration` + world.Export("http://127.0.0.1:[port]?member=[url_encode(json_encode(their_id))]") else to_chat(src, span_warning("There was an error registering your Discord ID in the database. Contact an administrator.")) log_and_message_admins("[ckey] failed to register their Discord ID. Their Discord snowflake ID is: [their_id]. Is the database connected?", src) @@ -348,6 +351,8 @@ alert = TRUE if(alert) for(var/client/X in GLOB.admins) + if(!check_rights_for(X, R_HOLDER)) + continue if(X.prefs?.read_preference(/datum/preference/toggle/holder/play_adminhelp_ping)) X << 'sound/effects/tones/newplayerping.ogg' window_flash(X) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index a72c66b60f..2f145cef5b 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -403,6 +403,8 @@ var/list/preferences_datums = list() sanitize_preferences() save_preferences() save_character() + load_preferences(TRUE) + load_character() attempt_vr(user.client?.prefs_vr,"load_vore","") ShowChoices(user) diff --git a/code/modules/client/preferences_tgui.dm b/code/modules/client/preferences_tgui.dm index 839a1107a7..d6234392de 100644 --- a/code/modules/client/preferences_tgui.dm +++ b/code/modules/client/preferences_tgui.dm @@ -136,8 +136,8 @@ switch(action) // Basic actions if("load") - if(!IsGuestKey(usr.key)) - open_load_dialog(usr) + if(!IsGuestKey(ui.user.key)) + open_load_dialog(ui.user) . = TRUE if("save") save_character() @@ -146,22 +146,22 @@ VARSET_IN(src, saved_notification, FALSE, 1 SECONDS) . = TRUE if("reload") - load_preferences() + load_preferences(TRUE) load_character() attempt_vr(client.prefs_vr,"load_vore","") //VOREStation Edit sanitize_preferences() . = TRUE if("resetslot") - if("Yes" != tgui_alert(usr, "This will reset the current slot. Continue?", "Reset current slot?", list("No", "Yes"))) + if("Yes" != tgui_alert(ui.user, "This will reset the current slot. Continue?", "Reset current slot?", list("No", "Yes"))) return - if("Yes" != tgui_alert(usr, "Are you completely sure that you want to reset this character slot?", "Reset current slot?", list("No", "Yes"))) + if("Yes" != tgui_alert(ui.user, "Are you completely sure that you want to reset this character slot?", "Reset current slot?", list("No", "Yes"))) return reset_slot() sanitize_preferences() . = TRUE if("copy") - if(!IsGuestKey(usr.key)) - open_copy_dialog(usr) + if(!IsGuestKey(ui.user.key)) + open_copy_dialog(ui.user) . = TRUE // More specific stuff if("switch_category") @@ -169,11 +169,11 @@ for(var/datum/category_group/player_setup_category/PS in player_setup.categories) if(PS.name == new_category) player_setup.selected_category = PS - update_tgui_static_data(usr, ui) + update_tgui_static_data(ui.user, ui) break . = TRUE if("game_prefs") - usr.client.game_options() + ui.user.client.game_options() . = TRUE if("refresh_character_preview") if(!COOLDOWN_FINISHED(src, ui_refresh_cooldown)) @@ -192,7 +192,7 @@ PMH.screen_loc = LAZYACCESS(preview_screen_locs, "PMH") /datum/preferences/tgui_close(mob/user) - save_character() + // save_character() save_preferences() /datum/preferences/proc/create_character_profiles() diff --git a/code/modules/client/verbs/who.dm b/code/modules/client/verbs/who.dm index 7825ff4b3e..82c22d4647 100644 --- a/code/modules/client/verbs/who.dm +++ b/code/modules/client/verbs/who.dm @@ -1,3 +1,5 @@ +#define NO_ADMINS_ONLINE_MESSAGE "Adminhelps are also sent through TGS to services like Discord. If no admins are available in game, sending an adminhelp might still be noticed and responded to." + /client/verb/who() set name = "Who" set category = "OOC.Resources" @@ -58,6 +60,8 @@ set category = "Admin" set name = "Staffwho" + var/header = GLOB.admins.len == 0 ? "No Admins Currently Online" : "Current Admins" + var/msg = "" var/modmsg = "" var/devmsg = "" @@ -132,6 +136,8 @@ if(CONFIG_GET(flag/show_mentors)) msg += "\n" + span_bold(" Current Mentors ([num_mentors_online]):") + "\n" + mentormsg - msg += "\n" + span_info("Adminhelps are also sent to Discord. If no admins are available in game try anyway and an admin on Discord may see it and respond.") + msg += "\n" + span_info(NO_ADMINS_ONLINE_MESSAGE) - to_chat(src,span_filter_notice("[jointext(msg, "
")]")) + to_chat(src, fieldset_block(span_bold(header), span_filter_notice("[jointext(msg, "
")]"), "boxed_message"), type = MESSAGE_TYPE_INFO) + +#undef NO_ADMINS_ONLINE_MESSAGE diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm index febe854c48..2f71507e4d 100644 --- a/code/modules/power/lighting.dm +++ b/code/modules/power/lighting.dm @@ -639,7 +639,7 @@ var/global/list/light_type_cache = list() playsound(src, W.usesound, 75, 1) user.visible_message("[user.name] opens [src]'s casing.", \ "You open [src]'s casing.", "You hear a noise.") - new construct_type(src.loc, fixture = src) + new construct_type(src.loc, src) qdel(src) return diff --git a/code/modules/tgs/LICENSE b/code/modules/tgs/LICENSE new file mode 100644 index 0000000000..324c48e993 --- /dev/null +++ b/code/modules/tgs/LICENSE @@ -0,0 +1,24 @@ +The MIT License + +Copyright (c) 2017-2024 Jordan Brown + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/code/modules/tgs/README.md b/code/modules/tgs/README.md new file mode 100644 index 0000000000..35ca73d7e9 --- /dev/null +++ b/code/modules/tgs/README.md @@ -0,0 +1,13 @@ +# DMAPI Internals + +This folder should be placed on its own inside a codebase that wishes to use the TGS DMAPI. Warranty void if modified. + +- [includes.dm](./includes.dm) is the file that should be included by DM code, it handles including the rest. +- The [core](./core) folder includes all code not directly part of any API version. +- The other versioned folders contain code for the different DMAPI versions. + - [v3210](./v3210) contains the final TGS3 API. + - [v4](./v4) is the legacy DMAPI 4 (Used in TGS 4.0.X versions). + - [v5](./v5) is the current DMAPI version used by TGS >=4.1. +- [LICENSE](./LICENSE) is the MIT license for the DMAPI. + +APIs communicate with TGS in two ways. All versions implement TGS -> DM communication using /world/Topic. DM -> TGS communication, called the bridge method, is different for each version. diff --git a/code/modules/tgs/core/README.md b/code/modules/tgs/core/README.md new file mode 100644 index 0000000000..965e21b549 --- /dev/null +++ b/code/modules/tgs/core/README.md @@ -0,0 +1,9 @@ +# Core DMAPI functions + +This folder contains all DMAPI code not directly involved in an API. + +- [_definitions.dm](./definitions.dm) contains defines needed across DMAPI internals. +- [byond_world_export.dm](./byond_world_export.dm) contains the default `/datum/tgs_http_handler` implementation which uses `world.Export()`. +- [core.dm](./core.dm) contains the implementations of the `/world/proc/TgsXXX()` procs. Many map directly to the `/datum/tgs_api` functions. It also contains the /datum selection and setup code. +- [datum.dm](./datum.dm) contains the `/datum/tgs_api` declarations that all APIs must implement. +- [tgs_version.dm](./tgs_version.dm) contains the `/datum/tgs_version` definition diff --git a/code/modules/tgs/core/_definitions.dm b/code/modules/tgs/core/_definitions.dm index ebf6d17c2a..fd98034eb7 100644 --- a/code/modules/tgs/core/_definitions.dm +++ b/code/modules/tgs/core/_definitions.dm @@ -1,2 +1,10 @@ +#if DM_VERSION < 510 +#error The TGS DMAPI does not support BYOND versions < 510! +#endif + #define TGS_UNIMPLEMENTED "___unimplemented" #define TGS_VERSION_PARAMETER "server_service_version" + +#ifndef TGS_DEBUG_LOG +#define TGS_DEBUG_LOG(message) +#endif diff --git a/code/modules/tgs/core/byond_world_export.dm b/code/modules/tgs/core/byond_world_export.dm new file mode 100644 index 0000000000..6ef8d841b8 --- /dev/null +++ b/code/modules/tgs/core/byond_world_export.dm @@ -0,0 +1,22 @@ +/datum/tgs_http_handler/byond_world_export + +/datum/tgs_http_handler/byond_world_export/PerformGet(url) + // This is an infinite sleep until we get a response + var/export_response = world.Export(url) + TGS_DEBUG_LOG("byond_world_export: Export complete") + + if(!export_response) + TGS_ERROR_LOG("byond_world_export: Failed request: [url]") + return new /datum/tgs_http_result(null, FALSE) + + var/content = export_response["CONTENT"] + if(!content) + TGS_ERROR_LOG("byond_world_export: Failed request, missing content!") + return new /datum/tgs_http_result(null, FALSE) + + var/response_json = TGS_FILE2TEXT_NATIVE(content) + if(!response_json) + TGS_ERROR_LOG("byond_world_export: Failed request, failed to load content!") + return new /datum/tgs_http_result(null, FALSE) + + return new /datum/tgs_http_result(response_json, TRUE) diff --git a/code/modules/tgs/core/core.dm b/code/modules/tgs/core/core.dm index a74c9bbab2..63cb5a2c35 100644 --- a/code/modules/tgs/core/core.dm +++ b/code/modules/tgs/core/core.dm @@ -1,4 +1,4 @@ -/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE) +/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE, datum/tgs_http_handler/http_handler = null) var/current_api = TGS_READ_GLOBAL(tgs) if(current_api) TGS_ERROR_LOG("API datum already set (\ref[current_api] ([current_api]))! Was TgsNew() called more than once?") @@ -40,13 +40,13 @@ if(5) api_datum = /datum/tgs_api/v5 - var/datum/tgs_version/max_api_version = TgsMaximumAPIVersion(); + var/datum/tgs_version/max_api_version = TgsMaximumApiVersion(); if(version.suite != null && version.minor != null && version.patch != null && version.deprecated_patch != null && version.deprefixed_parameter > max_api_version.deprefixed_parameter) - TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.") + TGS_ERROR_LOG("Detected unknown Interop API version! Defaulting to latest. Update the DMAPI to fix this problem.") api_datum = /datum/tgs_api/latest if(!api_datum) - TGS_ERROR_LOG("Found unsupported API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.") + TGS_ERROR_LOG("Found unsupported Interop API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.") return TGS_INFO_LOG("Activating API for version [version.deprefixed_parameter]") @@ -55,7 +55,10 @@ TGS_ERROR_LOG("Invalid parameter for event_handler: [event_handler]") event_handler = null - var/datum/tgs_api/new_api = new api_datum(event_handler, version) + if(!http_handler) + http_handler = new /datum/tgs_http_handler/byond_world_export + + var/datum/tgs_api/new_api = new api_datum(event_handler, version, http_handler) TGS_WRITE_GLOBAL(tgs, new_api) @@ -64,10 +67,10 @@ TGS_WRITE_GLOBAL(tgs, null) TGS_ERROR_LOG("Failed to activate API!") -/world/TgsMaximumAPIVersion() +/world/TgsMaximumApiVersion() return new /datum/tgs_version("5.x.x") -/world/TgsMinimumAPIVersion() +/world/TgsMinimumApiVersion() return new /datum/tgs_version("3.2.x") /world/TgsInitializationComplete() @@ -107,6 +110,13 @@ if(api) return api.ApiVersion() +/world/TgsEngine() +#ifdef OPENDREAM + return TGS_ENGINE_TYPE_OPENDREAM +#else + return TGS_ENGINE_TYPE_BYOND +#endif + /world/TgsInstanceName() var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) if(api) @@ -153,31 +163,17 @@ /world/TgsSecurityLevel() var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) if(api) - api.SecurityLevel() + return api.SecurityLevel() -/* -The MIT License +/world/TgsVisibility() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + return api.Visibility() -Copyright (c) 2017 Jordan Brown +/world/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE) + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + if(!istype(parameters, /list)) + parameters = list() -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ + return api.TriggerEvent(event_name, parameters, wait_for_completion) diff --git a/code/modules/tgs/core/datum.dm b/code/modules/tgs/core/datum.dm index ccc75fe344..3ca53e9bf7 100644 --- a/code/modules/tgs/core/datum.dm +++ b/code/modules/tgs/core/datum.dm @@ -4,11 +4,22 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null) var/datum/tgs_version/version var/datum/tgs_event_handler/event_handler -/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version) - . = ..() + var/list/warned_deprecated_command_runs + +/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version, datum/tgs_http_handler/http_handler) + ..() src.event_handler = event_handler src.version = version +/datum/tgs_api/proc/TerminateWorld() + while(TRUE) + TGS_DEBUG_LOG("About to terminate world. Tick: [world.time], sleep_offline: [world.sleep_offline]") + world.sleep_offline = FALSE // https://www.byond.com/forum/post/2894866 + del(world) + world.sleep_offline = FALSE // just in case, this is BYOND after all... + sleep(world.tick_lag) + TGS_DEBUG_LOG("BYOND DIDN'T TERMINATE THE WORLD!!! TICK IS: [world.time], sleep_offline: [world.sleep_offline]") + /datum/tgs_api/latest parent_type = /datum/tgs_api/v5 @@ -50,35 +61,14 @@ TGS_PROTECT_DATUM(/datum/tgs_api) /datum/tgs_api/proc/ChatTargetedBroadcast(message, admin_only) return TGS_UNIMPLEMENTED -/datum/tgs_api/proc/ChatPrivateMessage(message, admin_only) +/datum/tgs_api/proc/ChatPrivateMessage(message, datum/tgs_chat_user/user) return TGS_UNIMPLEMENTED /datum/tgs_api/proc/SecurityLevel() return TGS_UNIMPLEMENTED -/* -The MIT License +/datum/tgs_api/proc/Visibility() + return TGS_UNIMPLEMENTED -Copyright (c) 2017 Jordan Brown - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +/datum/tgs_api/proc/TriggerEvent(event_name, list/parameters, wait_for_completion) + return FALSE diff --git a/code/modules/tgs/core/tgs_version.dm b/code/modules/tgs/core/tgs_version.dm index a5dae1241a..bc561e6748 100644 --- a/code/modules/tgs/core/tgs_version.dm +++ b/code/modules/tgs/core/tgs_version.dm @@ -1,4 +1,5 @@ /datum/tgs_version/New(raw_parameter) + ..() src.raw_parameter = raw_parameter deprefixed_parameter = replacetext(raw_parameter, "/tg/station 13 Server v", "") var/list/version_bits = splittext(deprefixed_parameter, ".") diff --git a/code/modules/tgs/includes.dm b/code/modules/tgs/includes.dm index b9ef0f21eb..f5118ed55a 100644 --- a/code/modules/tgs/includes.dm +++ b/code/modules/tgs/includes.dm @@ -1,4 +1,5 @@ #include "core\_definitions.dm" +#include "core\byond_world_export.dm" #include "core\core.dm" #include "core\datum.dm" #include "core\tgs_version.dm" @@ -13,34 +14,9 @@ #include "v5\_defines.dm" #include "v5\api.dm" -#include "v5\api_vgs.dm" // VOREStation Edit - Include here so it has access to v5 defines +#include "v5\bridge.dm" +#include "v5\chunking.dm" #include "v5\commands.dm" -#include "v5\chat_commands.dm" -#include "v5\undef.dm" - -/* -The MIT License - -Copyright (c) 2017 Jordan Brown - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +#include "v5\serializers.dm" +#include "v5\topic.dm" +#include "v5\undefs.dm" diff --git a/code/modules/tgs/v3210/README.md b/code/modules/tgs/v3210/README.md new file mode 100644 index 0000000000..f96e7cf3b3 --- /dev/null +++ b/code/modules/tgs/v3210/README.md @@ -0,0 +1,6 @@ +# DMAPI V3 + +This DMAPI implements bridge using file output which TGS monitors for. + +- [api.dm](./api.dm) contains the bulk of the API code. +- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s. diff --git a/code/modules/tgs/v3210/api.dm b/code/modules/tgs/v3210/api.dm index 904a3d8518..666201a322 100644 --- a/code/modules/tgs/v3210/api.dm +++ b/code/modules/tgs/v3210/api.dm @@ -28,6 +28,8 @@ #define SERVICE_RETURN_SUCCESS "SUCCESS" +#define TGS_FILE2LIST(filename) (splittext(trim_left(trim_right(file2text(filename))), "\n")) + /datum/tgs_api/v3210 var/reboot_mode = REBOOT_MODE_NORMAL var/comms_key @@ -39,7 +41,7 @@ var/warned_custom_commands = FALSE /datum/tgs_api/v3210/ApiVersion() - return new /datum/tgs_version("3.2.1.0") + return new /datum/tgs_version("3.2.1.3") /datum/tgs_api/v3210/proc/trim_left(text) for (var/i = 1 to length(text)) @@ -53,24 +55,29 @@ return copytext(text, 1, i + 1) return "" -/datum/tgs_api/v3210/proc/file2list(filename) - return splittext(trim_left(trim_right(file2text(filename))), "\n") - /datum/tgs_api/v3210/OnWorldNew(minimum_required_security_level) . = FALSE comms_key = world.params[SERVICE_WORLD_PARAM] instance_name = world.params[SERVICE_INSTANCE_PARAM] if(!instance_name) - instance_name = "TG Station Server" //maybe just upgraded + instance_name = "TG Station Server" //maybe just upgraded - var/list/logs = file2list(".git/logs/HEAD") + var/list/logs = TGS_FILE2LIST(".git/logs/HEAD") if(logs.len) - logs = splittext(logs[logs.len - 1], " ") - commit = logs[2] - logs = file2list(".git/logs/refs/remotes/origin/master") + logs = splittext(logs[logs.len], " ") + if (logs.len >= 2) + commit = logs[2] + else + TGS_ERROR_LOG("Error parsing commit logs") + + logs = TGS_FILE2LIST(".git/logs/refs/remotes/origin/master") if(logs.len) - originmastercommit = splittext(logs[logs.len - 1], " ")[2] + logs = splittext(logs[logs.len], " ") + if (logs.len >= 2) + originmastercommit = logs[2] + else + TGS_ERROR_LOG("Error parsing origin commmit logs") if(world.system_type != MS_WINDOWS) TGS_ERROR_LOG("This API version is only supported on Windows. Not running on Windows. Aborting initialization!") @@ -92,14 +99,18 @@ if(skip_compat_check && !fexists(SERVICE_INTERFACE_DLL)) TGS_ERROR_LOG("Service parameter present but no interface DLL detected. This is symptomatic of running a service less than version 3.1! Please upgrade.") return - LIBCALL(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval + #if DM_VERSION >= 515 + call_ext(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval + #else + call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval + #endif return TRUE /datum/tgs_api/v3210/OnTopic(T) var/list/params = params2list(T) var/their_sCK = params[SERVICE_CMD_PARAM_KEY] if(!their_sCK) - return FALSE //continue world/Topic + return FALSE //continue world/Topic if(their_sCK != comms_key) return "Invalid comms key!"; @@ -160,7 +171,7 @@ var/datum/tgs_revision_information/test_merge/tm = new tm.number = text2num(I) var/list/entry = json[I] - tm.pull_request_commit = entry["commit"] + tm.head_commit = entry["commit"] tm.author = entry["author"] tm.title = entry["title"] . += tm @@ -168,7 +179,7 @@ /datum/tgs_api/v3210/Revision() if(!warned_revison) var/datum/tgs_version/api_version = ApiVersion() - TGS_ERROR_LOG("Use of TgsRevision on [api_version.deprefixed_parameter] origin_commit only points to master!") + TGS_WARNING_LOG("Use of TgsRevision on [api_version.deprefixed_parameter] origin_commit only points to master!") warned_revison = TRUE var/datum/tgs_revision_information/ri = new ri.commit = commit @@ -176,22 +187,25 @@ return ri /datum/tgs_api/v3210/EndProcess() - sleep(world.tick_lag) //flush the buffers + sleep(world.tick_lag) //flush the buffers ExportService(SERVICE_REQUEST_KILL_PROCESS) /datum/tgs_api/v3210/ChatChannelInfo() - return list() + return list() // :omegalul: -/datum/tgs_api/v3210/ChatBroadcast(message, list/channels) +/datum/tgs_api/v3210/ChatBroadcast(datum/tgs_message_content/message, list/channels) if(channels) return TGS_UNIMPLEMENTED + message = UpgradeDeprecatedChatMessage(message) ChatTargetedBroadcast(message, TRUE) ChatTargetedBroadcast(message, FALSE) -/datum/tgs_api/v3210/ChatTargetedBroadcast(message, admin_only) - ExportService("[admin_only ? SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE : SERVICE_REQUEST_IRC_BROADCAST] [message]") +/datum/tgs_api/v3210/ChatTargetedBroadcast(datum/tgs_message_content/message, admin_only) + message = UpgradeDeprecatedChatMessage(message) + ExportService("[admin_only ? SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE : SERVICE_REQUEST_IRC_BROADCAST] [message.text]") /datum/tgs_api/v3210/ChatPrivateMessage(message, datum/tgs_chat_user/user) + UpgradeDeprecatedChatMessage(message) return TGS_UNIMPLEMENTED /datum/tgs_api/v3210/SecurityLevel() @@ -227,29 +241,4 @@ #undef SERVICE_RETURN_SUCCESS -/* -The MIT License - -Copyright (c) 2017 Jordan Brown - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +#undef TGS_FILE2LIST diff --git a/code/modules/tgs/v3210/commands.dm b/code/modules/tgs/v3210/commands.dm index cfeacb3845..e65c816320 100644 --- a/code/modules/tgs/v3210/commands.dm +++ b/code/modules/tgs/v3210/commands.dm @@ -8,11 +8,14 @@ var/list/command_name_types = list() var/list/warned_command_names = warnings_only ? list() : null var/warned_about_the_dangers_of_robutussin = !warnings_only - for(var/I in subtypesof(/datum/tgs_chat_command)) + for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command) if(!warned_about_the_dangers_of_robutussin) - TGS_ERROR_LOG("Custom chat commands in [ApiVersion()] lacks the /datum/tgs_chat_user/sender.channel field!") + TGS_WARNING_LOG("Custom chat commands in [ApiVersion()] lacks the /datum/tgs_chat_user/sender.channel field!") warned_about_the_dangers_of_robutussin = TRUE var/datum/tgs_chat_command/stc = I + if(stc.ignore_type == I) + continue + var/command_name = initial(stc.name) if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\"")) if(warnings_only && !warned_command_names[command_name]) @@ -32,7 +35,8 @@ /datum/tgs_api/v3210/proc/HandleServiceCustomCommand(command, sender, params) if(!cached_custom_tgs_chat_commands) cached_custom_tgs_chat_commands = list() - for(var/datum/tgs_chat_command/stc as anything in subtypesof(/datum/tgs_chat_command)) + for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command) + var/datum/tgs_chat_command/stc = I cached_custom_tgs_chat_commands[lowertext(initial(stc.name))] = stc var/command_type = cached_custom_tgs_chat_commands[command] @@ -41,37 +45,14 @@ var/datum/tgs_chat_command/stc = new command_type var/datum/tgs_chat_user/user = new user.friendly_name = sender + + // Discord hack, fix the mention if it's only numbers (fuck you IRC trolls) + var/regex/discord_id_regex = regex("^\[0-9\]+$") + if(findtext(sender, discord_id_regex)) + sender = "<@[sender]>" + user.mention = sender - return stc.Run(user, params) || TRUE + var/datum/tgs_message_content/result = stc.Run(user, params) + result = UpgradeDeprecatedCommandResponse(result, command) -/* - -#undef SERVICE_JSON_PARAM_HELPTEXT -#undef SERVICE_JSON_PARAM_ADMINONLY -#undef SERVICE_JSON_PARAM_REQUIREDPARAMETERS - -The MIT License - -Copyright (c) 2017 Jordan Brown - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ + return result ? result.text : TRUE diff --git a/code/modules/tgs/v4/README.md b/code/modules/tgs/v4/README.md new file mode 100644 index 0000000000..78191447b2 --- /dev/null +++ b/code/modules/tgs/v4/README.md @@ -0,0 +1,6 @@ +# DMAPI V4 + +This DMAPI implements bridge requests using file output which TGS monitors for. It has a safe mode restriction. + +- [api.dm](./api.dm) contains the bulk of the API code. +- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s. diff --git a/code/modules/tgs/v4/api.dm b/code/modules/tgs/v4/api.dm index c0e71cf351..7c87922750 100644 --- a/code/modules/tgs/v4/api.dm +++ b/code/modules/tgs/v4/api.dm @@ -50,6 +50,10 @@ return new /datum/tgs_version("4.0.0.0") /datum/tgs_api/v4/OnWorldNew(minimum_required_security_level) + if(minimum_required_security_level == TGS_SECURITY_ULTRASAFE) + TGS_WARNING_LOG("V4 DMAPI requires safe security!") + minimum_required_security_level = TGS_SECURITY_SAFE + json_path = world.params[TGS4_PARAM_INFO_JSON] if(!json_path) TGS_ERROR_LOG("Missing [TGS4_PARAM_INFO_JSON] world parameter!") @@ -69,7 +73,7 @@ if(cached_json["apiValidateOnly"]) TGS_INFO_LOG("Validating API and exiting...") Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = "[minimum_required_security_level]")) - del(world) + TerminateWorld() security_level = cached_json["securityLevel"] chat_channels_json_path = cached_json["chatChannelsJson"] @@ -88,7 +92,7 @@ var/list/json = cached_json["testMerges"] for(var/entry in json) var/datum/tgs_revision_information/test_merge/tm = new - tm.time_merged = text2num(entry["timeMerged"]) + tm.timestamp = text2num(entry["timeMerged"]) var/list/revInfo = entry["revision"] if(revInfo) @@ -100,7 +104,7 @@ tm.url = entry["url"] tm.author = entry["author"] tm.number = entry["number"] - tm.pull_request_commit = entry["pullRequestRevision"] + tm.head_commit = entry["pullRequestRevision"] tm.comment = entry["comment"] cached_test_merges += tm @@ -110,18 +114,11 @@ /datum/tgs_api/v4/OnInitializationComplete() Export(TGS4_COMM_SERVER_PRIMED) - var/tgs4_secret_sleep_offline_sauce = 24051994 - var/old_sleep_offline = world.sleep_offline - world.sleep_offline = tgs4_secret_sleep_offline_sauce - sleep(1) - if(world.sleep_offline == tgs4_secret_sleep_offline_sauce) //if not someone changed it - world.sleep_offline = old_sleep_offline - /datum/tgs_api/v4/OnTopic(T) var/list/params = params2list(T) var/their_sCK = params[TGS4_INTEROP_ACCESS_IDENTIFIER] if(!their_sCK) - return FALSE //continue world/Topic + return FALSE //continue world/Topic if(their_sCK != access_identifier) return "Invalid comms key!"; @@ -184,35 +181,35 @@ var/json = json_encode(data) while(requesting_new_port && !override_requesting_new_port) - sleep(1) + sleep(world.tick_lag) //we need some port open at this point to facilitate return communication if(!world.port) requesting_new_port = TRUE if(!world.OpenPort(0)) //open any port TGS_ERROR_LOG("Unable to open random port to retrieve new port![TGS4_PORT_CRITFAIL_MESSAGE]") - del(world) + TerminateWorld() //request a new port export_lock = FALSE - var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose + var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose if(!new_port_json) TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]") - del(world) + TerminateWorld() var/new_port = new_port_json[TGS4_PARAMETER_DATA] if(!isnum(new_port) || new_port <= 0) TGS_ERROR_LOG("Malformed new port json ([json_encode(new_port_json)])![TGS4_PORT_CRITFAIL_MESSAGE]") - del(world) + TerminateWorld() if(new_port != world.port && !world.OpenPort(new_port)) TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]") - del(world) + TerminateWorld() requesting_new_port = FALSE while(export_lock) - sleep(1) + sleep(world.tick_lag) export_lock = TRUE last_interop_response = null @@ -220,7 +217,7 @@ text2file(json, server_commands_json_path) for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I) - sleep(1) + sleep(world.tick_lag) if(!last_interop_response) TGS_ERROR_LOG("Failed to get export result for: [json]") @@ -238,7 +235,7 @@ var/port = result[TGS4_PARAMETER_DATA] if(!isnum(port)) - return //this is valid, server may just want use to reboot + return //this is valid, server may just want use to reboot if(port == 0) //to byond 0 means any port and "none" means close vOv @@ -251,7 +248,7 @@ return instance_name /datum/tgs_api/v4/TestMerges() - return cached_test_merges + return cached_test_merges.Copy() /datum/tgs_api/v4/EndProcess() Export(TGS4_COMM_END_PROCESS) @@ -259,31 +256,46 @@ /datum/tgs_api/v4/Revision() return cached_revision -/datum/tgs_api/v4/ChatBroadcast(message, list/channels) +/datum/tgs_api/v4/ChatBroadcast(datum/tgs_message_content/message, list/channels) var/list/ids if(length(channels)) ids = list() - for(var/datum/tgs_chat_channel/channel as anything in channels) + for(var/I in channels) + var/datum/tgs_chat_channel/channel = I ids += channel.id - message = list("message" = message, "channelIds" = ids) + + message = UpgradeDeprecatedChatMessage(message) + + if (!length(channels)) + return + + message = list("message" = message.text, "channelIds" = ids) if(intercepted_message_queue) intercepted_message_queue += list(message) else Export(TGS4_COMM_CHAT, message) -/datum/tgs_api/v4/ChatTargetedBroadcast(message, admin_only) +/datum/tgs_api/v4/ChatTargetedBroadcast(datum/tgs_message_content/message, admin_only) var/list/channels = list() - for(var/datum/tgs_chat_channel/channel as anything in ChatChannelInfo()) + for(var/I in ChatChannelInfo()) + var/datum/tgs_chat_channel/channel = I if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only))) channels += channel.id - message = list("message" = message, "channelIds" = channels) + + message = UpgradeDeprecatedChatMessage(message) + + if (!length(channels)) + return + + message = list("message" = message.text, "channelIds" = channels) if(intercepted_message_queue) intercepted_message_queue += list(message) else Export(TGS4_COMM_CHAT, message) -/datum/tgs_api/v4/ChatPrivateMessage(message, datum/tgs_chat_user/user) - message = list("message" = message, "channelIds" = list(user.channel.id)) +/datum/tgs_api/v4/ChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user) + message = UpgradeDeprecatedChatMessage(message) + message = list("message" = message.text, "channelIds" = list(user.channel.id)) if(intercepted_message_queue) intercepted_message_queue += list(message) else @@ -308,30 +320,3 @@ /datum/tgs_api/v4/SecurityLevel() return security_level - -/* -The MIT License - -Copyright (c) 2017 Jordan Brown - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ diff --git a/code/modules/tgs/v4/commands.dm b/code/modules/tgs/v4/commands.dm index 63ee4e34e1..25dd6740e3 100644 --- a/code/modules/tgs/v4/commands.dm +++ b/code/modules/tgs/v4/commands.dm @@ -1,8 +1,11 @@ /datum/tgs_api/v4/proc/ListCustomCommands() var/results = list() custom_commands = list() - for(var/I in subtypesof(/datum/tgs_chat_command)) + for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command) var/datum/tgs_chat_command/stc = new I + if(stc.ignore_type == I) + continue + var/command_name = stc.name if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\"")) TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!") @@ -34,36 +37,8 @@ var/datum/tgs_chat_command/sc = custom_commands[command] if(sc) - var/result = sc.Run(u, params) - if(result == null) - result = "" - return result + var/datum/tgs_message_content/result = sc.Run(u, params) + result = UpgradeDeprecatedCommandResponse(result, command) + + return result ? result.text : TRUE return "Unknown command: [command]!" - -/* - -The MIT License - -Copyright (c) 2017 Jordan Brown - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ diff --git a/code/modules/tgs/v5/README.md b/code/modules/tgs/v5/README.md new file mode 100644 index 0000000000..a8a0c748e7 --- /dev/null +++ b/code/modules/tgs/v5/README.md @@ -0,0 +1,13 @@ +# DMAPI V5 + +This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no security restrictions. + +- [__interop_version.dm](./__interop_version.dm) contains the version of the API used between the DMAPI and TGS. +- [_defines.dm](./_defines.dm) contains constant definitions. +- [api.dm](./api.dm) contains the bulk of the API code. +- [bridge.dm](./bridge.dm) contains functions related to making bridge requests. +- [chunking.dm](./chunking.dm) contains common function for splitting large raw data sets into chunks BYOND can natively process. +- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s. +- [serializers.dm](./serializers.dm) contains function to help convert interop `/datum`s into a JSON encodable `list()` format. +- [topic.dm](./topic.dm) contains functions related to processing topic requests. +- [undefs.dm](./undefs.dm) Undoes the work of `_defines.dm`. diff --git a/code/modules/tgs/v5/__interop_version.dm b/code/modules/tgs/v5/__interop_version.dm new file mode 100644 index 0000000000..9589cbf31b --- /dev/null +++ b/code/modules/tgs/v5/__interop_version.dm @@ -0,0 +1 @@ +"5.10.1" diff --git a/code/modules/tgs/v5/_defines.dm b/code/modules/tgs/v5/_defines.dm index ce6f4b109d..a47bfd7800 100644 --- a/code/modules/tgs/v5/_defines.dm +++ b/code/modules/tgs/v5/_defines.dm @@ -4,29 +4,42 @@ #define DMAPI5_BRIDGE_DATA "data" #define DMAPI5_TOPIC_DATA "tgs_data" -#define DMAPI5_BRIDGE_COMMAND_PORT_UPDATE 0 +#define DMAPI5_BRIDGE_REQUEST_LIMIT 8198 +#define DMAPI5_TOPIC_REQUEST_LIMIT 65528 +#define DMAPI5_TOPIC_RESPONSE_LIMIT 65529 + #define DMAPI5_BRIDGE_COMMAND_STARTUP 1 #define DMAPI5_BRIDGE_COMMAND_PRIME 2 #define DMAPI5_BRIDGE_COMMAND_REBOOT 3 #define DMAPI5_BRIDGE_COMMAND_KILL 4 #define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5 -#define DMAPI5_BRIDGE_COMMAND_ADD_MEMBER_ROLE 6 // VOREStation Edit +#define DMAPI5_BRIDGE_COMMAND_CHUNK 6 +#define DMAPI5_BRIDGE_COMMAND_EVENT 7 #define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier" +#define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands" +#define DMAPI5_PARAMETER_TOPIC_PORT "topicPort" + +#define DMAPI5_CHUNK "chunk" +#define DMAPI5_CHUNK_PAYLOAD "payload" +#define DMAPI5_CHUNK_TOTAL "totalChunks" +#define DMAPI5_CHUNK_SEQUENCE_ID "sequenceId" +#define DMAPI5_CHUNK_PAYLOAD_ID "payloadId" + +#define DMAPI5_MISSING_CHUNKS "missingChunks" + #define DMAPI5_RESPONSE_ERROR_MESSAGE "errorMessage" #define DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE "commandType" #define DMAPI5_BRIDGE_PARAMETER_CURRENT_PORT "currentPort" #define DMAPI5_BRIDGE_PARAMETER_VERSION "version" #define DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE "chatMessage" -#define DMAPI5_BRIDGE_PARAMETER_CUSTOM_COMMANDS "customCommands" #define DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL "minimumSecurityLevel" -#define DMAPI5_BRIDGE_PARAMETER_CHAT_USER_ID "chatUserId" // VOREStation Edit +#define DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION "eventInvocation" #define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort" #define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation" -#define DMAPI5_CHAT_MESSAGE_TEXT "text" #define DMAPI5_CHAT_MESSAGE_CHANNEL_IDS "channelIds" #define DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER "accessIdentifier" @@ -37,6 +50,7 @@ #define DMAPI5_RUNTIME_INFORMATION_REVISION "revision" #define DMAPI5_RUNTIME_INFORMATION_TEST_MERGES "testMerges" #define DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL "securityLevel" +#define DMAPI5_RUNTIME_INFORMATION_VISIBILITY "visibility" #define DMAPI5_CHAT_UPDATE_CHANNELS "channels" @@ -64,9 +78,12 @@ #define DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED 4 #define DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE 5 #define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6 -#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 7 +#define DMAPI5_TOPIC_COMMAND_HEALTHCHECK 7 #define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8 -#define DMAPI5_TOPIC_COMMAND_GET_CHAT_COMMANDS 9 // VOREStation Edit +#define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9 +#define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10 +#define DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST 11 +#define DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT 12 #define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType" #define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand" @@ -76,11 +93,15 @@ #define DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME "newInstanceName" #define DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE "chatUpdate" #define DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION "newServerVersion" +#define DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE "broadcastMessage" +#define DMAPI5_TOPIC_RESPONSE_CLIENT_COUNT "clientCount" +#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE "commandResponse" #define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE "commandResponseMessage" #define DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES "chatResponses" #define DMAPI5_REVISION_INFORMATION_COMMIT_SHA "commitSha" +#define DMAPI5_REVISION_INFORMATION_TIMESTAMP "timestamp" #define DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA "originCommitSha" #define DMAPI5_CHAT_USER_ID "id" @@ -94,34 +115,14 @@ #define DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL "isAdminChannel" #define DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL "isPrivateChannel" #define DMAPI5_CHAT_CHANNEL_TAG "tag" +#define DMAPI5_CHAT_CHANNEL_EMBEDS_SUPPORTED "embedsSupported" #define DMAPI5_CUSTOM_CHAT_COMMAND_NAME "name" #define DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT "helpText" #define DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY "adminOnly" -/* -The MIT License +#define DMAPI5_EVENT_ID "eventId" -Copyright (c) 2020 Jordan Brown - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +#define DMAPI5_EVENT_INVOCATION_NAME "eventName" +#define DMAPI5_EVENT_INVOCATION_PARAMETERS "parameters" +#define DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION "notifyCompletion" diff --git a/code/modules/tgs/v5/api.dm b/code/modules/tgs/v5/api.dm index b3d2622d54..4ca9036b79 100644 --- a/code/modules/tgs/v5/api.dm +++ b/code/modules/tgs/v5/api.dm @@ -4,27 +4,60 @@ var/instance_name var/security_level + var/visibility var/reboot_mode = TGS_REBOOT_MODE_NORMAL + /// List of chat messages list()s that attempted to be sent during a topic call. To be bundled in the result of the call var/list/intercepted_message_queue + /// List of chat messages list()s that attempted to be sent during a topic call. To be bundled in the result of the call + var/list/offline_message_queue + var/list/custom_commands var/list/test_merges var/datum/tgs_revision_information/revision var/list/chat_channels + var/initialized = FALSE + var/initial_bridge_request_received = FALSE + var/datum/tgs_version/interop_version + + var/chunked_requests = 0 + var/list/chunked_topics = list() + + var/list/pending_events = list() + + var/detached = FALSE + + var/datum/tgs_http_handler/http_handler + +/datum/tgs_api/v5/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version, datum/tgs_http_handler/http_handler) + . = ..() + interop_version = version + src.http_handler = http_handler + TGS_DEBUG_LOG("V5 API created: [json_encode(args)]") + /datum/tgs_api/v5/ApiVersion() - return new /datum/tgs_version("5.1.1") + return new /datum/tgs_version( + #include "__interop_version.dm" + ) /datum/tgs_api/v5/OnWorldNew(minimum_required_security_level) + TGS_DEBUG_LOG("OnWorldNew()") server_port = world.params[DMAPI5_PARAM_SERVER_PORT] access_identifier = world.params[DMAPI5_PARAM_ACCESS_IDENTIFIER] var/datum/tgs_version/api_version = ApiVersion() - version = null - var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_BRIDGE_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands())) + version = null // we want this to be the TGS version, not the interop version + + // sleep once to prevent an issue where world.Export on the first tick can hang indefinitely + TGS_DEBUG_LOG("Starting Export bug prevention sleep tick. time:[world.time] sleep_offline:[world.sleep_offline]") + sleep(world.tick_lag) + TGS_DEBUG_LOG("Export bug prevention sleep complete") + + var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands(), DMAPI5_PARAMETER_TOPIC_PORT = GetTopicPort())) if(!istype(bridge_response)) TGS_ERROR_LOG("Failed initial bridge request!") return FALSE @@ -36,16 +69,19 @@ if(runtime_information[DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY]) TGS_INFO_LOG("DMAPI validation, exiting...") - del(world) + TerminateWorld() - version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION]) + initial_bridge_request_received = TRUE + version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION]) // reassigning this because it can change if TGS updates security_level = runtime_information[DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL] + visibility = runtime_information[DMAPI5_RUNTIME_INFORMATION_VISIBILITY] instance_name = runtime_information[DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME] var/list/revisionData = runtime_information[DMAPI5_RUNTIME_INFORMATION_REVISION] if(istype(revisionData)) revision = new revision.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA] + revision.timestamp = revisionData[DMAPI5_REVISION_INFORMATION_TIMESTAMP] revision.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA] else TGS_ERROR_LOG("Failed to decode [DMAPI5_RUNTIME_INFORMATION_REVISION] from runtime information!") @@ -61,15 +97,18 @@ if(revInfo) tm.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA] tm.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA] + tm.timestamp = revisionData[DMAPI5_REVISION_INFORMATION_TIMESTAMP] else TGS_WARNING_LOG("Failed to decode [DMAPI5_TEST_MERGE_REVISION] from test merge #[tm.number]!") - tm.time_merged = text2num(entry[DMAPI5_TEST_MERGE_TIME_MERGED]) + if(!tm.timestamp) + tm.timestamp = entry[DMAPI5_TEST_MERGE_TIME_MERGED] + tm.title = entry[DMAPI5_TEST_MERGE_TITLE_AT_MERGE] tm.body = entry[DMAPI5_TEST_MERGE_BODY_AT_MERGE] tm.url = entry[DMAPI5_TEST_MERGE_URL] tm.author = entry[DMAPI5_TEST_MERGE_AUTHOR] - tm.pull_request_commit = entry[DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION] + tm.head_commit = entry[DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION] tm.comment = entry[DMAPI5_TEST_MERGE_COMMENT] test_merges += tm @@ -79,190 +118,57 @@ chat_channels = list() DecodeChannels(runtime_information) + initialized = TRUE return TRUE +/datum/tgs_api/v5/proc/GetTopicPort() +#if defined(OPENDREAM) && defined(OPENDREAM_TOPIC_PORT_EXISTS) + return "[world.opendream_topic_port]" +#else + return null +#endif + /datum/tgs_api/v5/proc/RequireInitialBridgeResponse() - while(!version) - sleep(1) + TGS_DEBUG_LOG("RequireInitialBridgeResponse()") + var/logged = FALSE + while(!initial_bridge_request_received) + if(!logged) + TGS_DEBUG_LOG("RequireInitialBridgeResponse: Starting sleep") + logged = TRUE + + sleep(world.tick_lag) + + TGS_DEBUG_LOG("RequireInitialBridgeResponse: Passed") /datum/tgs_api/v5/OnInitializationComplete() Bridge(DMAPI5_BRIDGE_COMMAND_PRIME) - var/tgs4_secret_sleep_offline_sauce = 29051994 - var/old_sleep_offline = world.sleep_offline - world.sleep_offline = tgs4_secret_sleep_offline_sauce - sleep(1) - if(world.sleep_offline == tgs4_secret_sleep_offline_sauce) //if not someone changed it - world.sleep_offline = old_sleep_offline - -/datum/tgs_api/v5/proc/TopicResponse(error_message = null) - var/list/response = list() - response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message - - return json_encode(response) - /datum/tgs_api/v5/OnTopic(T) + TGS_DEBUG_LOG("OnTopic()") + RequireInitialBridgeResponse() + TGS_DEBUG_LOG("OnTopic passed bridge request gate") var/list/params = params2list(T) var/json = params[DMAPI5_TOPIC_DATA] if(!json) - return FALSE //continue world/Topic + TGS_DEBUG_LOG("No \"[DMAPI5_TOPIC_DATA]\" entry found, ignoring...") + return FALSE // continue to /world/Topic - var/list/topic_parameters = json_decode(json) - if(!topic_parameters) - return TopicResponse("Invalid topic parameters json!"); + if(!initialized) + TGS_WARNING_LOG("Missed topic due to not being initialized: [json]") + return TRUE // too early to handle, but it's still our responsibility - var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] - if(their_sCK != access_identifier) - return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!"); - - var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] - if(!isnum(command)) - return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] from: [json]!") - - switch(command) - if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND) - var/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND]) - if(!result) - result = TopicResponse("Error running chat command!") - return result - // VOREStation Edit Start - GetChatCommands command - if(DMAPI5_TOPIC_COMMAND_GET_CHAT_COMMANDS) - var/topic_response = list(DMAPI5_BRIDGE_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands()) - return json_encode(topic_response) - // VOREStation Edit - End - if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION) - intercepted_message_queue = list() - var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION] - if(!istype(event_notification)) - return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!") - - var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE] - if(!isnum(event_type)) - return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!") - - var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS] - if(event_parameters && !istype(event_parameters)) - return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!") - - var/list/event_call = list(event_type) - if(event_parameters) - event_call += event_parameters - - if(event_handler != null) - event_handler.HandleEvent(arglist(event_call)) - - var/list/response = list() - response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue - intercepted_message_queue = null - return json_encode(response) - if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) - var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] - if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") - - if(event_handler != null) - event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) - - //the topic still completes, miraculously - //I honestly didn't believe byond could do it without exploding - if(!world.OpenPort(new_port)) - return TopicResponse("Port change failed!") - - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE) - var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE] - if(!isnum(new_reboot_mode)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!") - - if(event_handler != null) - event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) - - reboot_mode = new_reboot_mode - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED) - var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME] - if(!istext(new_instance_name)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!") - - if(event_handler != null) - event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name) - - instance_name = new_instance_name - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE) - var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE] - if(!istype(chat_update_json)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!") - - DecodeChannels(chat_update_json) - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE) - var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] - if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") - - server_port = new_port - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_HEARTBEAT) - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH) - var/new_version_string = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION] - if (!istext(new_version_string)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]]") - - var/datum/tgs_version/new_version = new(new_version_string) - if (event_handler) - event_handler.HandleEvent(TGS_EVENT_WATCHDOG_REATTACH, new_version) - - version = new_version - - return TopicResponse() - - return TopicResponse("Unknown command: [command]") - -/datum/tgs_api/v5/proc/Bridge(command, list/data) - if(!data) - data = list() - - data[DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE] = command - data[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] = access_identifier - - var/json = json_encode(data) - var/encoded_json = url_encode(json) - - // This is an infinite sleep until we get a response - var/export_response = world.Export("http://127.0.0.1:[server_port]/Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]") - if(!export_response) - TGS_ERROR_LOG("Failed export request: [json]") - return - - var/response_json = file2text(export_response["CONTENT"]) - if(!response_json) - TGS_ERROR_LOG("Failed export request, missing content!") - return - - var/list/bridge_response = json_decode(response_json) - if(!bridge_response) - TGS_ERROR_LOG("Failed export request, bad json: [response_json]") - return - - var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE] - if(error) - TGS_ERROR_LOG("Failed export request, bad request: [error]") - return - - return bridge_response + return ProcessTopicJson(json, TRUE) /datum/tgs_api/v5/OnReboot() var/list/result = Bridge(DMAPI5_BRIDGE_COMMAND_REBOOT) if(!result) return - //okay so the standard TGS4 proceedure is: right before rebooting change the port to whatever was sent to us in the above json's data parameter + //okay so the standard TGS proceedure is: right before rebooting change the port to whatever was sent to us in the above json's data parameter var/port = result[DMAPI5_BRIDGE_RESPONSE_NEW_PORT] if(!isnum(port)) - return //this is valid, server may just want use to reboot + return //this is valid, server may just want use to reboot if(port == 0) //to byond 0 means any port and "none" means close vOv @@ -277,7 +183,7 @@ /datum/tgs_api/v5/TestMerges() RequireInitialBridgeResponse() - return test_merges + return test_merges.Copy() /datum/tgs_api/v5/EndProcess() Bridge(DMAPI5_BRIDGE_COMMAND_KILL) @@ -286,43 +192,110 @@ RequireInitialBridgeResponse() return revision -/datum/tgs_api/v5/ChatBroadcast(message, list/channels) +// Common proc b/c it's used by the V3/V4 APIs +/datum/tgs_api/proc/UpgradeDeprecatedChatMessage(datum/tgs_message_content/message) + if(!istext(message)) + return message + + TGS_WARNING_LOG("Received legacy string when a [/datum/tgs_message_content] was expected. Please audit all calls to TgsChatBroadcast, TgsChatTargetedBroadcast, and TgsChatPrivateMessage to ensure they use the new /datum.") + return new /datum/tgs_message_content(message) + +/datum/tgs_api/v5/ChatBroadcast(datum/tgs_message_content/message2, list/channels) if(!length(channels)) channels = ChatChannelInfo() var/list/ids = list() - for(var/datum/tgs_chat_channel/channel as anything in channels) + for(var/I in channels) + var/datum/tgs_chat_channel/channel = I ids += channel.id - message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = ids) - if(intercepted_message_queue) - intercepted_message_queue += list(message) - else - Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message)) + SendChatMessageRaw(message2, ids) -/datum/tgs_api/v5/ChatTargetedBroadcast(message, admin_only) +/datum/tgs_api/v5/ChatTargetedBroadcast(datum/tgs_message_content/message2, admin_only) var/list/channels = list() - for(var/datum/tgs_chat_channel/channel as anything in ChatChannelInfo()) + for(var/I in ChatChannelInfo()) + var/datum/tgs_chat_channel/channel = I if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only))) channels += channel.id - message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = channels) - if(intercepted_message_queue) - intercepted_message_queue += list(message) - else - Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message)) -/datum/tgs_api/v5/ChatPrivateMessage(message, datum/tgs_chat_user/user) - message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = list(user.channel.id)) + SendChatMessageRaw(message2, channels) + +/datum/tgs_api/v5/ChatPrivateMessage(datum/tgs_message_content/message2, datum/tgs_chat_user/user) + SendChatMessageRaw(message2, list(user.channel.id)) + +/datum/tgs_api/v5/proc/SendChatMessageRaw(datum/tgs_message_content/message2, list/channel_ids) + message2 = UpgradeDeprecatedChatMessage(message2) + + if (!length(channel_ids)) + return + + var/list/data = message2._interop_serialize() + data[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = channel_ids if(intercepted_message_queue) - intercepted_message_queue += list(message) + intercepted_message_queue += list(data) + return + + if(offline_message_queue) + offline_message_queue += list(data) + return + + if(detached) + offline_message_queue = list(data) + + WaitForReattach(FALSE) + + data = offline_message_queue + offline_message_queue = null + + for(var/queued_message in data) + SendChatDataRaw(queued_message) else - Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message)) + SendChatDataRaw(data) + +/datum/tgs_api/v5/proc/SendChatDataRaw(list/data) + Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = data)) /datum/tgs_api/v5/ChatChannelInfo() RequireInitialBridgeResponse() - return chat_channels + WaitForReattach(TRUE) + return chat_channels.Copy() + +/datum/tgs_api/v5/TriggerEvent(event_name, list/parameters, wait_for_completion) + RequireInitialBridgeResponse() + WaitForReattach(TRUE) + + if(interop_version.minor < 9) + TGS_WARNING_LOG("Interop version too low for custom events!") + return FALSE + + var/str_parameters = list() + for(var/i in parameters) + str_parameters += "[i]" + + var/list/response = Bridge(DMAPI5_BRIDGE_COMMAND_EVENT, list(DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION = list(DMAPI5_EVENT_INVOCATION_NAME = event_name, DMAPI5_EVENT_INVOCATION_PARAMETERS = str_parameters, DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION = wait_for_completion))) + if(!response) + return FALSE + + var/event_id = response[DMAPI5_EVENT_ID] + if(!event_id) + return FALSE + + TGS_DEBUG_LOG("Created event ID: [event_id]") + if(!wait_for_completion) + return TRUE + + TGS_DEBUG_LOG("Waiting for completion of event ID: [event_id]") + + while(!pending_events[event_id]) + sleep(world.tick_lag) + + TGS_DEBUG_LOG("Completed wait on event ID: [event_id]") + pending_events -= event_id + + return TRUE /datum/tgs_api/v5/proc/DecodeChannels(chat_update_json) + TGS_DEBUG_LOG("DecodeChannels()") var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS] if(istype(chat_channels_json)) chat_channels.Cut() @@ -341,35 +314,13 @@ channel.is_admin_channel = channel_json[DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL] channel.is_private_channel = channel_json[DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL] channel.custom_tag = channel_json[DMAPI5_CHAT_CHANNEL_TAG] + channel.embeds_supported = channel_json[DMAPI5_CHAT_CHANNEL_EMBEDS_SUPPORTED] return channel /datum/tgs_api/v5/SecurityLevel() RequireInitialBridgeResponse() return security_level -/* -The MIT License - -Copyright (c) 2020 Jordan Brown - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +/datum/tgs_api/v5/Visibility() + RequireInitialBridgeResponse() + return visibility diff --git a/code/modules/tgs/v5/api_vgs.dm b/code/modules/tgs/v5/api_vgs.dm deleted file mode 100644 index 0e9c8efe8e..0000000000 --- a/code/modules/tgs/v5/api_vgs.dm +++ /dev/null @@ -1,90 +0,0 @@ -// We currently isolate ourselves in a different variable so that only the specific APIs we choose will be active. -// Eventually it would be good to handle all the TGS Apis properly and we can use the same. -GLOBAL_DATUM(vgs, /datum/tgs_api) - -// Supply our own New functionality so we can read from config instead of world params -/world/proc/VgsNew(datum/tgs_event_handler/event_handler) - var/current_api = GLOB.vgs - if(current_api) - TGS_ERROR_LOG("API datum already set (\ref[current_api] ([current_api]))! Was TgsNew() called more than once?") - return - - // If we don't have a configured access identifier we aren't meant to use VGS - if(!CONFIG_GET(string/vgs_access_identifier)) - TGS_INFO_LOG("Skipping VGS: No access identifier configured") - return - - var/datum/tgs_api/api_datum = /datum/tgs_api/v5/vgs1 - TGS_INFO_LOG("Activating API for version [api_datum]") - - if(event_handler && !istype(event_handler)) - TGS_ERROR_LOG("Invalid parameter for event_handler: [event_handler]") - event_handler = null - - var/datum/tgs_api/new_api = new api_datum(event_handler) - GLOB.vgs = new_api - - var/result = new_api.OnWorldNew() - if(!result || result == TGS_UNIMPLEMENTED) - GLOB.vgs = null - TGS_ERROR_LOG("Failed to activate API!") - -/world/proc/VgsTopic(T) - var/datum/tgs_api/api = GLOB.vgs - if(api) - var/result = api.OnTopic(T) - if(result != TGS_UNIMPLEMENTED) - return result - -/world/TgsReboot() - var/datum/tgs_api/api = GLOB.vgs - if(api) - api.OnReboot() - else - return ..() - -/world/TgsInitializationComplete() - var/datum/tgs_api/api = GLOB.vgs - if(api) - api.OnInitializationComplete() - else - return ..() - -/world/proc/VgsAddMemberRole(chat_user_id) - var/datum/tgs_api/v5/vgs1/api = GLOB.vgs - if(api) - api.AddMemberRole(chat_user_id) - -/datum/tgs_api/v5/vgs1 - server_port = 8080 // Default port - -// Override to prevent error messages from the lack of revision/test_merge information, and to use config isntead of params. -/datum/tgs_api/v5/vgs1/OnWorldNew() - if(CONFIG_GET(number/vgs_server_port)) - server_port = CONFIG_GET(number/vgs_server_port) - access_identifier = CONFIG_GET(string/vgs_access_identifier) - - var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands())) - if(!istype(bridge_response)) - TGS_ERROR_LOG("Failed initial bridge request!") - return FALSE - - var/list/runtime_information = bridge_response[DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION] - if(!istype(runtime_information)) - TGS_ERROR_LOG("Failed to decode runtime information from bridge response: [json_encode(bridge_response)]!") - return FALSE - - version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION]) - instance_name = runtime_information[DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME] - - chat_channels = list() - DecodeChannels(runtime_information) - - return TRUE - -/datum/tgs_api/v5/vgs1/proc/AddMemberRole(chat_user_id) - Bridge(DMAPI5_BRIDGE_COMMAND_ADD_MEMBER_ROLE, list(DMAPI5_BRIDGE_PARAMETER_CHAT_USER_ID = chat_user_id)) - -// /datum/tgs_api/v5/vgs1/RequireInitialBridgeResponse() -// while(!instance_name) -// sleep(1) diff --git a/code/modules/tgs/v5/bridge.dm b/code/modules/tgs/v5/bridge.dm new file mode 100644 index 0000000000..62201fcc9e --- /dev/null +++ b/code/modules/tgs/v5/bridge.dm @@ -0,0 +1,106 @@ +/datum/tgs_api/v5/proc/Bridge(command, list/data) + if(!data) + data = list() + + var/single_bridge_request = CreateBridgeRequest(command, data) + if(length(single_bridge_request) <= DMAPI5_BRIDGE_REQUEST_LIMIT) + return PerformBridgeRequest(single_bridge_request) + + // chunking required + var/payload_id = ++chunked_requests + + var/raw_data = CreateBridgeData(command, data, FALSE) + + var/list/chunk_requests = GenerateChunks(raw_data, TRUE) + + var/list/response + for(var/bridge_request in chunk_requests) + response = PerformBridgeRequest(bridge_request) + if(!response) + // Abort + return + + var/list/missing_sequence_ids = response[DMAPI5_MISSING_CHUNKS] + if(length(missing_sequence_ids)) + do + TGS_WARNING_LOG("Server is still missing some chunks of bridge P[payload_id]! Sending missing chunks...") + if(!istype(missing_sequence_ids)) + TGS_ERROR_LOG("Did not receive a list() for [DMAPI5_MISSING_CHUNKS]!") + return + + for(var/missing_sequence_id in missing_sequence_ids) + if(!isnum(missing_sequence_id)) + TGS_ERROR_LOG("Did not receive a num in [DMAPI5_MISSING_CHUNKS]!") + return + + var/missing_chunk_request = chunk_requests[missing_sequence_id + 1] + response = PerformBridgeRequest(missing_chunk_request) + if(!response) + // Abort + return + + missing_sequence_ids = response[DMAPI5_MISSING_CHUNKS] + while(length(missing_sequence_ids)) + + return response + +/datum/tgs_api/v5/proc/CreateBridgeRequest(command, list/data) + var/json = CreateBridgeData(command, data, TRUE) + var/encoded_json = url_encode(json) + + var/api_prefix = interop_version.minor >= 8 ? "api/" : "" + + var/url = "http://127.0.0.1:[server_port]/[api_prefix]Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]" + return url + +/datum/tgs_api/v5/proc/CreateBridgeData(command, list/data, needs_auth) + data[DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE] = command + if(needs_auth) + data[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] = access_identifier + + var/json = json_encode(data) + return json + +/datum/tgs_api/v5/proc/WaitForReattach(require_channels = FALSE) + if(detached) + // Wait up to one minute + for(var/i in 1 to 600) + sleep(world.tick_lag) + if(!detached && (!require_channels || length(chat_channels))) + break + + // dad went out for milk and cigarettes 20 years ago... + // yes, this affects all other waiters, intentional + if(i == 600) + detached = FALSE + +/datum/tgs_api/v5/proc/PerformBridgeRequest(bridge_request) + WaitForReattach(FALSE) + + TGS_DEBUG_LOG("Bridge request start") + var/datum/tgs_http_result/result = http_handler.PerformGet(bridge_request) + TGS_DEBUG_LOG("Bridge request complete") + + if(isnull(result)) + TGS_ERROR_LOG("Failed bridge request, handler returned null!") + return + + if(!istype(result) || result.type != /datum/tgs_http_result) + TGS_ERROR_LOG("Failed bridge request, handler returned non-[/datum/tgs_http_result]!") + return + + if(!result.success) + TGS_DEBUG_LOG("Failed bridge request, HTTP request failed!") + return + + var/list/bridge_response = json_decode(result.response_text) + if(!bridge_response) + TGS_ERROR_LOG("Failed bridge request, bad json: [result.response_text]") + return + + var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE] + if(error) + TGS_ERROR_LOG("Failed bridge request, bad request: [error]") + return + + return bridge_response diff --git a/code/modules/tgs/v5/chunking.dm b/code/modules/tgs/v5/chunking.dm new file mode 100644 index 0000000000..cd5944d34f --- /dev/null +++ b/code/modules/tgs/v5/chunking.dm @@ -0,0 +1,43 @@ +/datum/tgs_api/v5/proc/GenerateChunks(payload, bridge) + var/limit = bridge ? DMAPI5_BRIDGE_REQUEST_LIMIT : DMAPI5_TOPIC_RESPONSE_LIMIT + + var/payload_id = ++chunked_requests + var/data_length = length(payload) + + var/chunk_count + var/list/chunk_requests + for(chunk_count = 2; !chunk_requests; ++chunk_count) + var/max_chunk_size = -round(-(data_length / chunk_count)) + if(max_chunk_size > limit) + continue + + chunk_requests = list() + for(var/i in 1 to chunk_count) + var/start_index = 1 + ((i - 1) * max_chunk_size) + if (start_index > data_length) + break + + var/end_index = min(1 + (i * max_chunk_size), data_length + 1) + + var/chunk_payload = copytext(payload, start_index, end_index) + + // sequence IDs in interop chunking are always zero indexed + var/list/chunk = list(DMAPI5_CHUNK_PAYLOAD_ID = payload_id, DMAPI5_CHUNK_SEQUENCE_ID = (i - 1), DMAPI5_CHUNK_TOTAL = chunk_count, DMAPI5_CHUNK_PAYLOAD = chunk_payload) + + var/chunk_request = list(DMAPI5_CHUNK = chunk) + var/chunk_length + if(bridge) + chunk_request = CreateBridgeRequest(DMAPI5_BRIDGE_COMMAND_CHUNK, chunk_request) + chunk_length = length(chunk_request) + else + chunk_request = list(chunk_request) // wrap for adding to list + chunk_length = length(json_encode(chunk_request)) + + if(chunk_length > limit) + // Screwed by encoding, no way to preempt it though + chunk_requests = null + break + + chunk_requests += chunk_request + + return chunk_requests diff --git a/code/modules/tgs/v5/commands.dm b/code/modules/tgs/v5/commands.dm index 70db4333dd..9557f8a08e 100644 --- a/code/modules/tgs/v5/commands.dm +++ b/code/modules/tgs/v5/commands.dm @@ -1,16 +1,19 @@ /datum/tgs_api/v5/proc/ListCustomCommands() var/results = list() custom_commands = list() - for(var/I in subtypesof(/datum/tgs_chat_command)) + for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command) var/datum/tgs_chat_command/stc = new I + if(stc.ignore_type == I) + continue + var/command_name = stc.name if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\"")) - TGS_WARNING_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!") + TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!") continue if(results[command_name]) var/datum/other = custom_commands[command_name] - TGS_WARNING_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!") + TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!") continue results += list(list(DMAPI5_CUSTOM_CHAT_COMMAND_NAME = command_name, DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT = stc.help_text, DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY = stc.admin_only)) custom_commands[command_name] = stc @@ -30,39 +33,28 @@ var/datum/tgs_chat_command/sc = custom_commands[command] if(sc) - var/text_response = sc.Run(u, params) - var/list/topic_response = list() - if(!istext(text_response)) - TGS_ERROR_LOG("Custom command [command] should return a string! Got: \"[text_response]\"") - text_response = null - topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = text_response - return json_encode(topic_response) + var/datum/tgs_message_content/response = sc.Run(u, params) + response = UpgradeDeprecatedCommandResponse(response, command) + + var/list/topic_response = TopicResponse() + topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = response ? response.text : null + topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE] = response ? response._interop_serialize() : null + return topic_response return TopicResponse("Unknown custom chat command: [command]!") -/* +// Common proc b/c it's used by the V3/V4 APIs +/datum/tgs_api/proc/UpgradeDeprecatedCommandResponse(datum/tgs_message_content/response, command) + // Backwards compatibility, used to return a string + if(istext(response)) + warned_deprecated_command_runs = warned_deprecated_command_runs || list() + if(!warned_deprecated_command_runs[command]) + TGS_WARNING_LOG("Custom chat command \"[command]\" is still returning a string. This behaviour is deprecated, please upgrade it to return a [/datum/tgs_message_content].") + warned_deprecated_command_runs[command] = TRUE -The MIT License + return new /datum/tgs_message_content(response) -Copyright (c) 2020 Jordan Brown + if(!istype(response)) + TGS_ERROR_LOG("Custom chat command \"[command]\" should return a [/datum/tgs_message_content]! Got: \"[response]\"") + return null -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ + return response diff --git a/code/modules/tgs/v5/serializers.dm b/code/modules/tgs/v5/serializers.dm new file mode 100644 index 0000000000..3a32848ad5 --- /dev/null +++ b/code/modules/tgs/v5/serializers.dm @@ -0,0 +1,59 @@ +/datum/tgs_message_content/proc/_interop_serialize() + return list("text" = text, "embed" = embed ? embed._interop_serialize() : null) + +/datum/tgs_chat_embed/proc/_interop_serialize() + CRASH("Base /proc/interop_serialize called on [type]!") + +/datum/tgs_chat_embed/structure/_interop_serialize() + var/list/serialized_fields + if(istype(fields, /list)) + serialized_fields = list() + for(var/datum/tgs_chat_embed/field/field as anything in fields) + serialized_fields += list(field._interop_serialize()) + return list( + "title" = title, + "description" = description, + "url" = url, + "timestamp" = timestamp, + "colour" = colour, + "image" = src.image ? src.image._interop_serialize() : null, + "thumbnail" = thumbnail ? thumbnail._interop_serialize() : null, + "video" = video ? video._interop_serialize() : null, + "footer" = footer ? footer._interop_serialize() : null, + "provider" = provider ? provider._interop_serialize() : null, + "author" = author ? author._interop_serialize() : null, + "fields" = serialized_fields + ) + +/datum/tgs_chat_embed/media/_interop_serialize() + return list( + "url" = url, + "width" = width, + "height" = height, + "proxyUrl" = proxy_url + ) + +/datum/tgs_chat_embed/provider/_interop_serialize() + return list( + "url" = url, + "name" = name + ) + +/datum/tgs_chat_embed/provider/author/_interop_serialize() + . = ..() + .["iconUrl"] = icon_url + .["proxyIconUrl"] = proxy_icon_url + +/datum/tgs_chat_embed/footer/_interop_serialize() + return list( + "text" = text, + "iconUrl" = icon_url, + "proxyIconUrl" = proxy_icon_url + ) + +/datum/tgs_chat_embed/field/_interop_serialize() + return list( + "name" = name, + "value" = value, + "isInline" = is_inline + ) diff --git a/code/modules/tgs/v5/topic.dm b/code/modules/tgs/v5/topic.dm new file mode 100644 index 0000000000..59e5e63e5c --- /dev/null +++ b/code/modules/tgs/v5/topic.dm @@ -0,0 +1,298 @@ +/datum/tgs_api/v5/proc/TopicResponse(error_message = null) + var/list/response = list() + if(error_message) + response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message + return response + +/datum/tgs_api/v5/proc/ProcessTopicJson(json, check_access_identifier) + TGS_DEBUG_LOG("ProcessTopicJson(..., [check_access_identifier])") + var/list/result = ProcessRawTopic(json, check_access_identifier) + if(!result) + result = TopicResponse("Runtime error!") + else if(!length(result)) + return "{}" // quirk of json_encode is an empty list returns "[]" + + var/response_json = json_encode(result) + if(length(response_json) > DMAPI5_TOPIC_RESPONSE_LIMIT) + // cache response chunks and send the first + var/list/chunks = GenerateChunks(response_json, FALSE) + var/payload_id = chunks[1][DMAPI5_CHUNK][DMAPI5_CHUNK_PAYLOAD_ID] + var/cache_key = ResponseTopicChunkCacheKey(payload_id) + + chunked_topics[cache_key] = chunks + + response_json = json_encode(chunks[1]) + + return response_json + +/datum/tgs_api/v5/proc/ProcessRawTopic(json, check_access_identifier) + TGS_DEBUG_LOG("ProcessRawTopic(..., [check_access_identifier])") + var/list/topic_parameters = json_decode(json) + if(!topic_parameters) + TGS_DEBUG_LOG("ProcessRawTopic: json_decode failed") + return TopicResponse("Invalid topic parameters json: [json]!"); + + var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] + if(check_access_identifier && their_sCK != access_identifier) + TGS_DEBUG_LOG("ProcessRawTopic: access identifier check failed") + return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] or it does not match!") + + var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] + if(!isnum(command)) + TGS_DEBUG_LOG("ProcessRawTopic: command type check failed") + return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE]!") + + return ProcessTopicCommand(command, topic_parameters) + +/datum/tgs_api/v5/proc/ResponseTopicChunkCacheKey(payload_id) + return "response[payload_id]" + +/datum/tgs_api/v5/proc/ProcessTopicCommand(command, list/topic_parameters) + TGS_DEBUG_LOG("ProcessTopicCommand([command], ...)") + switch(command) + + if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND) + intercepted_message_queue = list() + var/list/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND]) + if(!result) + result = TopicResponse("Error running chat command!") + result[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue + intercepted_message_queue = null + return result + + if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION) + var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION] + if(!istype(event_notification)) + return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!") + + var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE] + if(!isnum(event_type)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!") + + var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS] + if(event_parameters && !istype(event_parameters)) + . = TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!") + else + var/list/response = TopicResponse() + . = response + if(event_handler != null) + var/list/event_call = list(event_type) + if(event_parameters) + event_call += event_parameters + + intercepted_message_queue = list() + event_handler.HandleEvent(arglist(event_call)) + response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue + intercepted_message_queue = null + + if (event_type == TGS_EVENT_WATCHDOG_DETACH) + detached = TRUE + chat_channels.Cut() // https://github.com/tgstation/tgstation-server/issues/1490 + + return + + if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + if (!isnum(new_port) || !(new_port > 0)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) + + //the topic still completes, miraculously + //I honestly didn't believe byond could do it without exploding + if(!world.OpenPort(new_port)) + return TopicResponse("Port change failed!") + + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE) + var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE] + if(!isnum(new_reboot_mode)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) + + reboot_mode = new_reboot_mode + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED) + var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME] + if(!istext(new_instance_name)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name) + + instance_name = new_instance_name + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE) + TGS_DEBUG_LOG("ProcessTopicCommand: It's a chat update") + var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE] + if(!istype(chat_update_json)) + TGS_DEBUG_LOG("ProcessTopicCommand: failed \"[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]\" check") + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!") + + DecodeChannels(chat_update_json) + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE) + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + if (!isnum(new_port) || !(new_port > 0)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]") + + server_port = new_port + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_HEALTHCHECK) + if(event_handler && event_handler.receive_health_checks) + event_handler.HandleEvent(TGS_EVENT_HEALTH_CHECK) + var/list/health_check_response = TopicResponse() + health_check_response[DMAPI5_TOPIC_RESPONSE_CLIENT_COUNT] = TGS_CLIENT_COUNT + return health_check_response; + + if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH) + detached = FALSE + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + var/error_message = null + if (new_port != null) + if (!isnum(new_port) || !(new_port > 0)) + error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]" + else + server_port = new_port + + var/new_version_string = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION] + if (!istext(new_version_string)) + if(error_message != null) + error_message += ", " + error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]" + else + var/datum/tgs_version/new_version = new(new_version_string) + if (event_handler) + event_handler.HandleEvent(TGS_EVENT_WATCHDOG_REATTACH, new_version) + + version = new_version + + var/list/reattach_response = TopicResponse(error_message) + reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands() + reattach_response[DMAPI5_PARAMETER_TOPIC_PORT] = GetTopicPort() + + for(var/eventId in pending_events) + pending_events[eventId] = TRUE + + return reattach_response + + if(DMAPI5_TOPIC_COMMAND_SEND_CHUNK) + var/list/chunk = topic_parameters[DMAPI5_CHUNK] + if(!istype(chunk)) + return TopicResponse("Invalid [DMAPI5_CHUNK]!") + + var/payload_id = chunk[DMAPI5_CHUNK_PAYLOAD_ID] + if(!isnum(payload_id)) + return TopicResponse("[DMAPI5_CHUNK_PAYLOAD_ID] is not a number!") + + // Always updated the highest known payload ID + chunked_requests = max(chunked_requests, payload_id) + + var/sequence_id = chunk[DMAPI5_CHUNK_SEQUENCE_ID] + if(!isnum(sequence_id)) + return TopicResponse("[DMAPI5_CHUNK_SEQUENCE_ID] is not a number!") + + var/total_chunks = chunk[DMAPI5_CHUNK_TOTAL] + if(!isnum(total_chunks)) + return TopicResponse("[DMAPI5_CHUNK_TOTAL] is not a number!") + + if(total_chunks == 0) + return TopicResponse("[DMAPI5_CHUNK_TOTAL] is zero!") + + var/payload = chunk[DMAPI5_CHUNK_PAYLOAD] + if(!istext(payload)) + return TopicResponse("[DMAPI5_CHUNK_PAYLOAD] is not text!") + + var/cache_key = "request[payload_id]" + var/payloads = chunked_topics[cache_key] + + if(!payloads) + payloads = new /list(total_chunks) + chunked_topics[cache_key] = payloads + + if(total_chunks != length(payloads)) + chunked_topics -= cache_key + return TopicResponse("Received differing total chunks for same [DMAPI5_CHUNK_PAYLOAD_ID]! Invalidating [DMAPI5_CHUNK_PAYLOAD_ID]!") + + var/pre_existing_chunk = payloads[sequence_id + 1] + if(pre_existing_chunk && pre_existing_chunk != payload) + chunked_topics -= cache_key + return TopicResponse("Received differing payload for same [DMAPI5_CHUNK_SEQUENCE_ID]! Invalidating [DMAPI5_CHUNK_PAYLOAD_ID]!") + + payloads[sequence_id + 1] = payload + + var/list/missing_sequence_ids = list() + for(var/i in 1 to total_chunks) + if(!payloads[i]) + missing_sequence_ids += i - 1 + + if(length(missing_sequence_ids)) + return list(DMAPI5_MISSING_CHUNKS = missing_sequence_ids) + + chunked_topics -= cache_key + var/full_json = jointext(payloads, "") + + return ProcessRawTopic(full_json, FALSE) + + if(DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK) + var/payload_id = topic_parameters[DMAPI5_CHUNK_PAYLOAD_ID] + if(!isnum(payload_id)) + return TopicResponse("[DMAPI5_CHUNK_PAYLOAD_ID] is not a number!") + + // Always updated the highest known payload ID + chunked_requests = max(chunked_requests, payload_id) + + var/list/missing_chunks = topic_parameters[DMAPI5_MISSING_CHUNKS] + if(!istype(missing_chunks) || !length(missing_chunks)) + return TopicResponse("Missing or empty [DMAPI5_MISSING_CHUNKS]!") + + var/sequence_id_to_send = missing_chunks[1] + if(!isnum(sequence_id_to_send)) + return TopicResponse("[DMAPI5_MISSING_CHUNKS] contained a non-number!") + + var/cache_key = ResponseTopicChunkCacheKey(payload_id) + var/list/chunks = chunked_topics[cache_key] + if(!chunks) + return TopicResponse("Unknown response chunk set: P[payload_id]!") + + // sequence IDs in interop chunking are always zero indexed + var/chunk_to_send = chunks[sequence_id_to_send + 1] + if(!chunk_to_send) + return TopicResponse("Sequence ID [sequence_id_to_send] is not present in response chunk P[payload_id]!") + + if(length(missing_chunks) == 1) + // sending last chunk, purge the cache + chunked_topics -= cache_key + + return chunk_to_send + + if(DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST) + var/message = topic_parameters[DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE] + if (!istext(message)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE]") + + TGS_WORLD_ANNOUNCE(message) + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT) + var/event_id = topic_parameters[DMAPI5_EVENT_ID] + if (!istext(event_id)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_ID]") + + TGS_DEBUG_LOG("Completing event ID [event_id]...") + pending_events[event_id] = TRUE + return TopicResponse() + + return TopicResponse("Unknown command: [command]") + +/datum/tgs_api/v5/proc/WorldBroadcast(message) + set waitfor = FALSE + TGS_WORLD_ANNOUNCE(message) diff --git a/code/modules/tgs/v5/undef.dm b/code/modules/tgs/v5/undefs.dm similarity index 71% rename from code/modules/tgs/v5/undef.dm rename to code/modules/tgs/v5/undefs.dm index b27e3abe95..ca49e46cdf 100644 --- a/code/modules/tgs/v5/undef.dm +++ b/code/modules/tgs/v5/undefs.dm @@ -4,27 +4,42 @@ #undef DMAPI5_BRIDGE_DATA #undef DMAPI5_TOPIC_DATA -#undef DMAPI5_BRIDGE_COMMAND_PORT_UPDATE +#undef DMAPI5_BRIDGE_REQUEST_LIMIT +#undef DMAPI5_TOPIC_REQUEST_LIMIT +#undef DMAPI5_TOPIC_RESPONSE_LIMIT + #undef DMAPI5_BRIDGE_COMMAND_STARTUP #undef DMAPI5_BRIDGE_COMMAND_PRIME #undef DMAPI5_BRIDGE_COMMAND_REBOOT #undef DMAPI5_BRIDGE_COMMAND_KILL #undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND +#undef DMAPI5_BRIDGE_COMMAND_CHUNK +#undef DMAPI5_BRIDGE_COMMAND_EVENT #undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER +#undef DMAPI5_PARAMETER_CUSTOM_COMMANDS +#undef DMAPI5_PARAMETER_TOPIC_PORT + +#undef DMAPI5_CHUNK +#undef DMAPI5_CHUNK_PAYLOAD +#undef DMAPI5_CHUNK_TOTAL +#undef DMAPI5_CHUNK_SEQUENCE_ID +#undef DMAPI5_CHUNK_PAYLOAD_ID + +#undef DMAPI5_MISSING_CHUNKS + #undef DMAPI5_RESPONSE_ERROR_MESSAGE #undef DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE #undef DMAPI5_BRIDGE_PARAMETER_CURRENT_PORT #undef DMAPI5_BRIDGE_PARAMETER_VERSION #undef DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE -#undef DMAPI5_BRIDGE_PARAMETER_CUSTOM_COMMANDS #undef DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL +#undef DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION #undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT #undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION -#undef DMAPI5_CHAT_MESSAGE_TEXT #undef DMAPI5_CHAT_MESSAGE_CHANNEL_IDS #undef DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER @@ -35,6 +50,7 @@ #undef DMAPI5_RUNTIME_INFORMATION_REVISION #undef DMAPI5_RUNTIME_INFORMATION_TEST_MERGES #undef DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL +#undef DMAPI5_RUNTIME_INFORMATION_VISIBILITY #undef DMAPI5_CHAT_UPDATE_CHANNELS @@ -62,8 +78,12 @@ #undef DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED #undef DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE #undef DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE -#undef DMAPI5_TOPIC_COMMAND_HEARTBEAT +#undef DMAPI5_TOPIC_COMMAND_HEALTHCHECK #undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH +#undef DMAPI5_TOPIC_COMMAND_SEND_CHUNK +#undef DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK +#undef DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST +#undef DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT #undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE #undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND @@ -73,11 +93,15 @@ #undef DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME #undef DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE #undef DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION +#undef DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE +#undef DMAPI5_TOPIC_RESPONSE_CLIENT_COUNT +#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE #undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE #undef DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES #undef DMAPI5_REVISION_INFORMATION_COMMIT_SHA +#undef DMAPI5_REVISION_INFORMATION_TIMESTAMP #undef DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA #undef DMAPI5_CHAT_USER_ID @@ -91,34 +115,14 @@ #undef DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL #undef DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL #undef DMAPI5_CHAT_CHANNEL_TAG +#undef DMAPI5_CHAT_CHANNEL_EMBEDS_SUPPORTED #undef DMAPI5_CUSTOM_CHAT_COMMAND_NAME #undef DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT #undef DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY -/* -The MIT License +#undef DMAPI5_EVENT_ID -Copyright (c) 2020 Jordan Brown - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +#undef DMAPI5_EVENT_INVOCATION_NAME +#undef DMAPI5_EVENT_INVOCATION_PARAMETERS +#undef DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION diff --git a/code/modules/tgs/v5/chat_commands.dm b/code/modules/tgs_commands/vorestation.dm similarity index 100% rename from code/modules/tgs/v5/chat_commands.dm rename to code/modules/tgs_commands/vorestation.dm diff --git a/code/modules/tickets/procs.dm b/code/modules/tickets/procs.dm index 2cd19a236d..11255afb26 100644 --- a/code/modules/tickets/procs.dm +++ b/code/modules/tickets/procs.dm @@ -55,7 +55,7 @@ ADMIN_VERB(cmd_mentor_ticket_panel, (R_ADMIN|R_SERVER|R_MOD|R_MENTOR), "Mentor T GLOB.tickets.BrowseTickets(browse_to) /proc/message_mentors(var/msg) - msg = span_mentor_channel(span_prefix("Mentor:") + span_message("[msg]")) + msg = span_mentor_channel(span_prefix("Mentor: ") + span_message("[msg]")) for(var/client/C in GLOB.admins) to_chat(C, msg) diff --git a/code/modules/tickets/tickets.dm b/code/modules/tickets/tickets.dm index 34189fc23e..760aa86450 100644 --- a/code/modules/tickets/tickets.dm +++ b/code/modules/tickets/tickets.dm @@ -319,9 +319,9 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) var/list/activemins = adm["present"] var activeMins = activemins.len if(is_bwoink) - ahelp_discord_message("ADMINHELP: FROM: [key_name_admin(usr)] TO [initiator_ckey]/[initiator_key_name] - MSG: **[msg]** - Heard by [activeMins] NON-AFK staff members.") + ahelp_discord_message("[level == 0 ? "MENTORHELP" : "ADMINHELP"]: FROM: [key_name_admin(usr)] TO [initiator_ckey]/[initiator_key_name] - MSG: **[msg]** - Heard by [activeMins] NON-AFK staff members.") else - ahelp_discord_message("ADMINHELP: FROM: [initiator_ckey]/[initiator_key_name] - MSG: **[msg]** - Heard by [activeMins] NON-AFK staff members.") + ahelp_discord_message("[level == 0 ? "MENTORHELP" : "ADMINHELP"]: FROM: [initiator_ckey]/[initiator_key_name] - MSG: **[msg]** - Heard by [activeMins] NON-AFK staff members.") // Also send it to discord since that's the hip cool thing now. SSwebhooks.send( @@ -358,11 +358,11 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) tgui_interact(usr.client.mob) //private -/datum/ticket/proc/FullMonty(ref_src, admin = FALSE) +/datum/ticket/proc/FullMonty(ref_src, admin_commands = FALSE) if(!ref_src) ref_src = "\ref[src]" if(initiator && initiator.mob) - if(admin) + if(admin_commands) . = ADMIN_FULLMONTY_NONAME(initiator.mob) else . = "Initiator disconnected." @@ -404,22 +404,26 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) //won't bug irc /datum/ticket/proc/MessageNoRecipient(msg) var/ref_src = "\ref[src]" - var/chat_msg = span_admin_pm_notice(span_adminhelp("Ticket [TicketHref("#[id]", ref_src)]") + span_bold(": [LinkedReplyName(ref_src)] [FullMonty(ref_src)]:") + msg) AddInteraction(span_red("[LinkedReplyName(ref_src)]: [msg]")) //send this msg to all admins - if(level == 0) - for (var/client/C in GLOB.admins) - if (C.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) - C << 'sound/effects/mentorhelp.mp3' - message_mentors(chat_msg) - else if(level == 1) - for(var/client/X in GLOB.admins) - if(X.prefs?.read_preference(/datum/preference/toggle/holder/play_adminhelp_ping)) - X << 'sound/effects/adminhelp.ogg' - window_flash(X) - to_chat(X, chat_msg) + switch(level) + if(0) + for (var/client/C in GLOB.admins) + var/chat_msg = span_mentor_channel(span_admin_pm_notice(span_adminhelp("Ticket [TicketHref("#[id]", ref_src)]") + span_bold(" (Mentor): [LinkedReplyName(ref_src)] [FullMonty(ref_src, check_rights_for(C, (R_ADMIN|R_SERVER|R_MOD)))]:") + msg)) + if (C.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) + C << 'sound/effects/mentorhelp.mp3' + to_chat(C, chat_msg) + if(1) + for(var/client/X in GLOB.admins) + var/chat_msg = span_admin_pm_notice(span_adminhelp("Ticket [TicketHref("#[id]", ref_src)] (Admin)") + span_bold(": [LinkedReplyName(ref_src)] [FullMonty(ref_src, check_rights_for(X, (R_ADMIN|R_SERVER|R_MOD)))]:") + msg) + if(!check_rights_for(X, R_HOLDER)) + continue + if(X.prefs?.read_preference(/datum/preference/toggle/holder/play_adminhelp_ping)) + X << 'sound/effects/adminhelp.ogg' + window_flash(X) + to_chat(X, chat_msg) /* //Reopen a closed ticket @@ -650,6 +654,7 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) level = level + 1 + AddInteraction("[key_name_admin(usr)] escalated Ticket.") message_mentors("[usr.ckey] escalated Ticket [TicketHref("#[id]")]") log_admin("[key_name(usr)] escalated ticket [src.name]") to_chat(src.initiator, span_mentor("[usr.ckey] escalated your ticket to admins.")) diff --git a/code/modules/tickets/tickets_ui.dm b/code/modules/tickets/tickets_ui.dm index a41dd7398a..3bb42e44ef 100644 --- a/code/modules/tickets/tickets_ui.dm +++ b/code/modules/tickets/tickets_ui.dm @@ -45,7 +45,7 @@ "closed_at" = (world.time - T.closed_at), "opened_at_date" = gameTimestamp(wtime = T.opened_at), "closed_at_date" = gameTimestamp(wtime = T.closed_at), - "actions" = check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) ? T.FullMonty(,TRUE) : T.FullMonty(), + "actions" = T.FullMonty(null, check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD))), "log" = T._interactions, ) @@ -235,7 +235,7 @@ data["opened_at_date"] = gameTimestamp(wtime = opened_at) data["closed_at_date"] = gameTimestamp(wtime = closed_at) - data["actions"] = check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) ? FullMonty(ref_src, TRUE) : FullMonty(ref_src) + data["actions"] = FullMonty(ref_src, check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD))) data["log"] = _interactions @@ -296,7 +296,7 @@ dat += "
Closed at: [gameTimestamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)" dat += "

" if(initiator) - dat += span_bold("Actions:") + " [check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)) ? FullMonty(ref_src, TRUE) : FullMonty(ref_src)]
" + dat += span_bold("Actions:") + " [FullMonty(ref_src, check_rights_for(user.client, (R_ADMIN|R_SERVER|R_MOD)))]
" else dat += span_bold("DISCONNECTED") + "[GLOB.TAB][ClosureLinks(ref_src)]
" dat += "
Log:

" diff --git a/config/example/config.txt b/config/example/config.txt index a21581316c..e110289d92 100644 --- a/config/example/config.txt +++ b/config/example/config.txt @@ -451,10 +451,8 @@ ITEMS_SURVIVE_DIGESTION # Path to the folder that BYOND should export faxes into so they are readable on the web. #FAX_EXPORT_DIR data/faxes -# TCP port on which to connect to the VGS instance. -#VGS_SERVER_PORT 8888 -# Secret pre-shared-key to authenticate to VGS. -#VGS_ACCESS_IDENTIFIER some_password_here +# Port for helper script to add member roles in discord (see tools/registration) +#REGISTER_SERVER_PORT 6969 MULTI_Z_EXPLOSION_SCALAR 0.35 @@ -601,3 +599,6 @@ JUKEBOX_TRACK_FILES config/jukebox.json #Turn this on if you want all admin-PMs to go to be sent to discord, and not only the first message of a ticket. #DISCORD_AHELPS_ALL + +## Uncomment to set the number of /world/Reboot()s before the DreamDaemon restarts itself. 0 means restart every round. Requires tgstation server tools. +#ROUNDS_UNTIL_HARD_RESTART 10 diff --git a/dependencies.sh b/dependencies.sh index 20b97c7d2f..d2b937981f 100644 --- a/dependencies.sh +++ b/dependencies.sh @@ -8,7 +8,7 @@ export BYOND_MAJOR=516 export BYOND_MINOR=1664 # Macro Count -export MACRO_COUNT=8 +export MACRO_COUNT=9 #rust_g git tag export RUST_G_VERSION=3.11.0 diff --git a/html/admin/panels.css b/html/admin/panels.css index 22373ee0ca..c1699b7843 100644 --- a/html/admin/panels.css +++ b/html/admin/panels.css @@ -1,10 +1,54 @@ -body {padding:0px;margin:0px;} -#top {position:fixed;top:5px;left:10%;width:80%;text-align:center;background-color:#fff;border:2px solid #ccc;} -#main {position:relative;top:10px;left:3%;width:96%;text-align:center;z-index:0;} -#searchable {table-layout:fixed;width:100%;text-align:center;"#f4f4f4";} -tr.norm {background-color:#f4f4f4;} -tr.title {background-color:#ccc;} -tr.alt {background-color:#e7e7e7;} -.small {font-size:80%;} -a {text-decoration:none;} -a:hover {color:#d3d;} +body { + padding: 0px; + margin: 0px; +} + +#top { + position: fixed; + top: 5px; + left: 10%; + width: 80%; + text-align: center; + background-color: #fff; + border: 2px solid #ccc; +} + +#main { + position: relative; + top: 10px; + left: 3%; + width: 96%; + text-align: center; + z-index: 0; +} + +#searchable { + table-layout: fixed; + width: 100%; + text-align: center; + background-color: "#f4f4f4"; +} + +tr.norm { + background-color: #f4f4f4; +} + +tr.title { + background-color: #ccc; +} + +tr.alt { + background-color: #e7e7e7; +} + +.small { + font-size: 80%; +} + +a { + text-decoration: none; +} + +a:hover { + color: #d3d; +} diff --git a/html/browser/common.css b/html/browser/common.css index 671df60ed3..06f29333fe 100644 --- a/html/browser/common.css +++ b/html/browser/common.css @@ -1,422 +1,518 @@ -body -{ - padding: 0; - margin: 0; - background-color: #272727; - font-size: 12px; - color: #ffffff; - line-height: 170%; +body { + padding: 0; + margin: 0; + background-color: #272727; + font-size: 12px; + color: #ffffff; + line-height: 170%; } -hr -{ - background-color: #40628a; - height: 1px; +hr { + background-color: #40628a; + height: 1px; } -a, a:link, a:visited, a:active, .linkOn, .linkOff -{ - color: #ffffff; - text-decoration: none; - background: #40628a; - border: 1px solid #161616; - padding: 1px 4px 1px 4px; - margin: 0 2px 0 0; - cursor:default; - white-space:nowrap; +a, +button, +a:link, +a:visited, +a:active, +.linkOn, +.linkOff { + color: #ffffff; + text-decoration: none; + background: #40628a; + border: 1px solid #161616; + padding: 1px 4px 1px 4px; + margin: 0 2px 0 0; + cursor: default; } -a:hover -{ - color: #40628a; - background: #ffffff; +a:hover { + color: #40628a; + background: #ffffff; } -a.white, a.white:link, a.white:visited, a.white:active -{ - color: #40628a; - text-decoration: none; - background: #ffffff; - border: 1px solid #161616; - padding: 1px 4px 1px 4px; - margin: 0 2px 0 0; - cursor:default; +a.white, +a.white:link, +a.white:visited, +a.white:active { + color: #40628a; + text-decoration: none; + background: #ffffff; + border: 1px solid #161616; + padding: 1px 4px 1px 4px; + margin: 0 2px 0 0; + cursor: default; } -a.white:hover -{ - color: #ffffff; - background: #40628a; +a.white:hover { + color: #ffffff; + background: #40628a; } -.linkOn, a.linkOn:link, a.linkOn:visited, a.linkOn:active, a.linkOn:hover -{ - color: #ffffff; - background: #2f943c; - border-color: #24722e; +.linkOn, +a.linkOn:link, +a.linkOn:visited, +a.linkOn:active, +a.linkOn:hover { + color: #ffffff; + background: #2f943c; + border-color: #24722e; } -.linkOff, a.linkOff:link, a.linkOff:visited, a.linkOff:active, a.linkOff:hover -{ - color: #ffffff; - background: #999999; - border-color: #666666; +.linkOff, +a.linkOff:link, +a.linkOff:visited, +a.linkOff:active, +a.linkOff:hover { + color: #ffffff; + background: #999999; + border-color: #666666; } -a.icon, .linkOn.icon, .linkOff.icon -{ - position: relative; - padding: 1px 4px 2px 20px; +a.icon, +.linkOn.icon, +.linkOff.icon { + position: relative; + padding: 1px 4px 2px 20px; } -a.icon img, .linkOn.icon img -{ - position: absolute; - top: 0; - left: 0; - width: 18px; - height: 18px; +a.icon img, +.linkOn.icon img { + position: absolute; + top: 0; + left: 0; + width: 18px; + height: 18px; } -a.icon64, .linkOn.icon64, .linkOff.icon64 -{ - position: relative; - padding: 1px 4px 2px 68px; - font-size: 55px; +a.icon64, +.linkOn.icon64, +.linkOff.icon64 { + position: relative; + padding: 1px 4px 2px 68px; + font-size: 55px; } -a.icon64 img, .linkOn.icon64 img -{ - position: absolute; - top: 0; - left: 0; - width: 66px; - height: 66px; +a.icon64 img, +.linkOn.icon64 img { + position: absolute; + top: 0; + left: 0; + width: 66px; + height: 66px; } -ul -{ - padding: 4px 0 0 10px; - margin: 0; - list-style-type: none; +ul { + padding: 4px 0 0 10px; + margin: 0; + list-style-type: none; } -li -{ - padding: 0 0 2px 0; +li { + padding: 0 0 2px 0; } -img, a img -{ - border-style:none; +img, +a img { + border-style: none; } -h1, h2, h3, h4, h5, h6 -{ - margin: 0; - padding: 16px 0 8px 0; - color: #517087; +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + padding: 16px 0 8px 0; + color: #517087; } -h1 -{ - font-size: 15px; +h1 { + font-size: 15px; } -h2 -{ - font-size: 14px; +h2 { + font-size: 14px; } -h3 -{ - font-size: 13px; +h3 { + font-size: 13px; } -h4 -{ - font-size: 12px; +h4 { + font-size: 12px; } -.uiWrapper -{ - - width: 100%; - height: 100%; - padding-top:32px; +.uiWrapper { + width: 100%; + height: 100%; } -.uiTitle -{ - clear: both; - padding: 6px 8px 6px 8px; - border-bottom: 2px solid #161616; - background: #383838; - color: #98B0C3; - font-size: 16px; +.uiTitle { + clear: both; + padding: 6px 8px 6px 8px; + border-bottom: 2px solid #161616; + background: #383838; + color: #98b0c3; + font-size: 16px; } -.uiTitleWrapper -{ - position:fixed; - top:0px; - left:0px; - right:0px; - z-index: 10 +.uiTitle.icon { + padding: 6px 8px 6px 42px; + background-position: 2px 50%; + background-repeat: no-repeat; } -.uiTitleButtons -{ - position:fixed; - top:0px; - right:0px; - height:32px; - z-index:11; +.uiContent { + clear: both; + padding: 8px; + font-family: Verdana, Geneva, sans-serif; } -.uiTitle.icon -{ - padding: 6px 8px 6px 42px; - background-position: 2px 50%; - background-repeat: no-repeat; +.good, +.green { + color: #00ff00; } -.uiContent -{ - clear: both; - padding: 8px; - font-family: Verdana, Geneva, sans-serif; +.average, +.orange { + color: #d09000; } -.good, .green -{ - color: #00ff00; +.bad, +.red { + color: #ff0000; } -.average, .orange -{ - color: #d09000; +.highlight { + color: #8BA5C4; } -.bad, .red -{ - color: #ff0000; -} - -.highlight -{ - color: #8BA5C4; -} - -.dark -{ - color: #272727; +.dark { + color: #272727; } .darkgreen { - color: #008000; + color: #008000; } .grey { - color: #838383; + color: #838383; } - .crimson { - color: #dc143c; + color: #dc143c; } .maroon { - color: #800000; + color: #800000; } .brown { - color: #8d4925; + color: #8d4925; } .blue { - color: #0000ff; + color: #0000ff; } .black { - color: #000000; + color: #000000; } .white { - color: #000000; + color: #000000; } .darkgray { - color: #808080; + color: #808080; } .gray { - color: #a9a9a9; + color: #a9a9a9; } .yellow { - color: #ffcc00; + color: #ffcc00; } .pink { - color: #ffc0cb; + color: #ffc0cb; } .cyan { - color: #00ffff; + color: #00ffff; } .nicegreen { - color: #14a833; + color: #14a833; } -.notice -{ - position: relative; - background: #E9C183; - color: #15345A; - font-size: 10px; - font-style: italic; - padding: 2px 4px 0 4px; - margin: 4px; +.notice { + position: relative; + background: #e9c183; + color: #15345a; + font-size: 10px; + font-style: italic; + padding: 2px 4px 0 4px; + margin: 4px; } -.boldnotice -{ - position: relative; - background: #E9C183; - color: #15345A; - font-weight: bold; - font-size: 10px; - font-style: italic; - padding: 2px 4px 0 4px; - margin: 4px; +.boldnotice { + position: relative; + background: #E9C183; + color: #15345A; + font-weight: bold; + font-size: 10px; + font-style: italic; + padding: 2px 4px 0 4px; + margin: 4px; } -.notice.icon -{ - padding: 2px 4px 0 20px; +.notice.icon { + padding: 2px 4px 0 20px; } -.notice img -{ - position: absolute; - top: 0; - left: 0; - width: 16px; - height: 16px; +.notice img { + position: absolute; + top: 0; + left: 0; + width: 16px; + height: 16px; } -div.notice -{ - clear: both; +div.notice { + clear: both; } -.statusDisplay -{ - background: #000000; - color: #ffffff; - border: 1px solid #40628a; - padding: 4px; - margin: 3px 0; +.statusDisplay { + background: #000000; + color: #ffffff; + border: 1px solid #40628a; + padding: 4px; + margin: 3px 0; } -.block -{ - padding: 8px; - margin: 10px 4px 4px 4px; - border: 1px solid #40628a; - background-color: #202020; +.statusLabel { + width: 138px; + float: left; + overflow: hidden; + color: #98b0c3; } -.block h3 -{ - padding: 0; +.statusValue { + float: left; } -.progressBar -{ - width: 240px; - height: 14px; - border: 1px solid #666666; - float: left; - margin: 0 5px; - overflow: hidden; +.block { + padding: 8px; + margin: 10px 4px 4px 4px; + border: 1px solid #40628a; + background-color: #202020; } -.progressFill -{ - width: 100%; - height: 100%; - background: #40628a; - overflow: hidden; +.block h3 { + padding: 0; } -.progressFill.good -{ - color: #ffffff; - background: #00ff00; +.progressBar { + width: 240px; + height: 14px; + border: 1px solid #666666; + float: left; + margin: 0 5px; + overflow: hidden; } -.progressFill.average -{ - color: #ffffff; - background: #d09000; +.progressFill { + width: 100%; + height: 100%; + background: #40628a; + overflow: hidden; } -.progressFill.bad -{ - color: #ffffff; - background: #ff0000; +.progressFill.good { + color: #ffffff; + background: #00ff00; } -.progressFill.highlight -{ - color: #ffffff; - background: #8BA5C4; +.progressFill.average { + color: #ffffff; + background: #d09000; } -.clearBoth -{ - clear: both; +.progressFill.bad { + color: #ffffff; + background: #ff0000; } -.clearLeft -{ - clear: left; +.progressFill.highlight { + color: #ffffff; + background: #8ba5c4; } -.clearRight -{ - clear: right; +.clearBoth { + clear: both; } -.line -{ - width: 100%; - clear: both; +.clearLeft { + clear: left; +} + +.clearRight { + clear: right; +} + +.line { + width: 100%; + clear: both; } .italic, .italics { - font-style: italic; + font-style: italic; } .bold { - font-weight: bold; + font-weight: bold; } .underline { - text-decoration: underline; + text-decoration: underline; } -.spoiler{ - background-color: gray; - color: transparent; - user-select: none; +.spoiler { + background-color: gray; + color: transparent; + user-select: none; } -.spoiler:hover{ - background-color: inherit; - color: inherit; +.spoiler:hover { + background-color: inherit; + color: inherit; +} + +.switch { + position: relative; + display: inline-block; + width: 50px; + height: 26px; +} + +.switch input { + display: none; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #383838; + transition: 0.4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 4px; + bottom: 4px; + background-color: #98b0c3; + transition: 0.4s; +} + +.slider.red:before { + background-color: #d6858b; +} + +.slider.locked:before { + content: url("padlock.png"); + background-color: #b4b4b4; +} + +input:checked+.slider { + background-color: #40628a; +} + +input:checked+.slider.red { + background-color: #a92621; +} + +input:checked+.slider.locked { + background-color: #707070; +} + +input:focus+.slider { + box-shadow: 0 0 1px #2196f3; +} + +input:focus+.slider.red { + box-shadow: 0 0 1px #f3212d; +} + +input:focus+.slider.locked { + box-shadow: 0 0 1px #979797; +} + +input:checked+.slider:before { + transform: translateX(24px); +} + +.switch span { + display: inline-block; + position: relative; + width: 60px; + margin-left: 60px; +} + +ul.sparse { + padding-bottom: 20px; +} + +.sparse li { + margin-top: 2px; +} + +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} + +.severity { + margin: 0px; + padding: 1px 8px 1px 8px; + border-radius: 25px; + border: 1px solid #161616; + background: #40628a; + color: #ffffff; +} + +.severity img { + display: inline-block; + vertical-align: middle; +} + +.code { + padding: 6px 8px; + border: 1px solid #161616; + background: #383838; + color: #ffffff; + font-size: 12px; + display: block; + margin: 4px 0; + font-family: "Courier New", Courier, monospace; +} + +.user-select { + user-select: all; } diff --git a/tgui/README.md b/tgui/README.md index 15c3ca2aed..a1f7584b2c 100644 --- a/tgui/README.md +++ b/tgui/README.md @@ -68,20 +68,20 @@ will need these: **Via Juke Build (cross-platform)**: -- `tools/build/build tgui` - Build tgui in production mode. -- `tools/build/build tgui-dev` - Build tgui in production mode. - - `tools/build/build tgui-dev --reload` - Reload byond cache once. - - `tools/build/build tgui-dev --debug` - Run server with debug logging +- `tools/build/build.sh tgui` - Build tgui in production mode. +- `tools/build/build.sh tgui-dev` - Build tgui in production mode. + - `tools/build/build.sh tgui-dev --reload` - Reload byond cache once. + - `tools/build/build.sh tgui-dev --debug` - Run server with debug logging enabled. -- `tools/build/build tgui-lint` - Show (and auto-fix) problems with the code. -- `tools/build/build tgui-test` - Run unit and integration tests. -- `tools/build/build tgui-analyze` - Run a bundle analyzer. -- `tools/build/build tgui-clean` - Clean up tgui folder. +- `tools/build/build.sh tgui-lint` - Show (and auto-fix) problems with the code. +- `tools/build/build.sh tgui-test` - Run unit and integration tests. +- `tools/build/build.sh tgui-analyze` - Run a bundle analyzer. +- `tools/build/build.sh tgui-clean` - Clean up tgui folder. > With Juke Build, you can run multiple targets together, e.g.: > > ``` -> tools/build/build tgui tgui-lint tgui-tsc tgui-test +> tools/build/build.sh tgui tgui-lint tgui-tsc tgui-test > ``` **Via Bun (cross-platform)**: diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss index d29fa79546..b2c920b8d8 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss @@ -1126,6 +1126,70 @@ img.icon.bigicon { border-bottom: 1px dashed hsl(0, 0%, 100%); } +.fieldset_legend { + position: relative; + max-width: 95%; + font-size: 120%; + background-color: var(--color-section); + padding: var(--space-s) var(--space-m); + border-color: inherit; + z-index: 0; + + &:before { + content: ''; + position: absolute; + inset: 0; + border: var(--border-thickness-tiny) solid; + border-color: inherit; + border-radius: var(--border-radius-medium); + mask-image: linear-gradient(to top, black 50%, transparent 50%); + z-index: -1; + } +} + +.boxed_message { + background: hsl(220, 10%, 10%); + border: var(--border-thickness-tiny) solid; + border-left: var(--border-thickness-large) solid; + border-color: hsla(220, 40%, 75%, 0.25); + margin: var(--space-m) 0; + padding: var(--space-m) var(--space-l); + border-radius: var(--border-radius-medium); + + &.red_box { + background: hsl(0, 20%, 10%); + border-color: hsla(0, 100%, 50%, 0.5); + } + + &.green_box { + background: hsl(140, 20%, 10%); + border-color: hsla(120, 100%, 50%, 0.5); + } + + &.blue_box { + background: hsl(220, 20%, 10%); + border-color: hsla(225, 90%, 65%, 0.5); + } + + &.purple_box { + background: hsl(260, 25%, 12.5%); + border-color: hsla(260, 100%, 75%, 0.5); + } + + hr { + margin: var(--space-m) -0.75em; + border-color: inherit; + } +} + +// Provides a horizontal bar with text in the middle +// I got this off stackoverflow +.separator { + display: flex; + align-items: center; + text-align: center; +} + $alert-stripe-colors: ( 'default': hsl(199, 100%, 11%), 'green': hsl(120, 100%, 12%), diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss index 43d3676148..962d411804 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss @@ -1148,6 +1148,47 @@ h2.alert { border-bottom: 1px dashed hsl(0, 0%, 0%); } +.fieldset_legend { + background: hsl(0, 0%, 100%); // Chat background color + + &:before { + background: hsl(0, 0%, 100%); // Chat background color + } +} + +.boxed_message { + background: hsl(220, 100%, 97.5%); + border-color: hsla(220, 75%, 25%, 0.5); + + &.red_box { + background: hsl(0, 100%, 97.5%); + border-color: hsla(0, 100%, 50%, 0.5); + } + + &.green_box { + background: hsl(140, 100%, 97.5%); + border-color: hsl(120, 100%, 33%, 0.5); + } + + &.blue_box { + background: hsl(220, 100%, 97.5%); + border-color: hsla(225, 100%, 50%, 0.5); + } + + &.purple_box { + background: hsl(260, 100%, 97.5%); + border-color: hsla(260, 100%, 50%, 0.5); + } +} + +// Provides a horizontal bar with text in the middle +// I got this off stackoverflow +.separator { + display: flex; + align-items: center; + text-align: center; +} + $alert-stripe-colors: ( 'default': hsl(231, 100%, 85%), 'green': hsl(120, 100%, 84%), diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-vchatdark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-vchatdark.scss index d131f209f1..7504926401 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-vchatdark.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-vchatdark.scss @@ -1127,6 +1127,70 @@ img.icon.bigicon { border-bottom: 1px dashed hsl(0, 0%, 100%); } +.fieldset_legend { + position: relative; + max-width: 95%; + font-size: 120%; + background-color: var(--color-section); + padding: var(--space-s) var(--space-m); + border-color: inherit; + z-index: 0; + + &:before { + content: ''; + position: absolute; + inset: 0; + border: var(--border-thickness-tiny) solid; + border-color: inherit; + border-radius: var(--border-radius-medium); + mask-image: linear-gradient(to top, black 50%, transparent 50%); + z-index: -1; + } +} + +.boxed_message { + background: hsl(220, 10%, 10%); + border: var(--border-thickness-tiny) solid; + border-left: var(--border-thickness-large) solid; + border-color: hsla(220, 40%, 75%, 0.25); + margin: var(--space-m) 0; + padding: var(--space-m) var(--space-l); + border-radius: var(--border-radius-medium); + + &.red_box { + background: hsl(0, 20%, 10%); + border-color: hsla(0, 100%, 50%, 0.5); + } + + &.green_box { + background: hsl(140, 20%, 10%); + border-color: hsla(120, 100%, 50%, 0.5); + } + + &.blue_box { + background: hsl(220, 20%, 10%); + border-color: hsla(225, 90%, 65%, 0.5); + } + + &.purple_box { + background: hsl(260, 25%, 12.5%); + border-color: hsla(260, 100%, 75%, 0.5); + } + + hr { + margin: var(--space-m) -0.75em; + border-color: inherit; + } +} + +// Provides a horizontal bar with text in the middle +// I got this off stackoverflow +.separator { + display: flex; + align-items: center; + text-align: center; +} + $alert-stripe-colors: ( 'default': hsl(199, 100%, 11%), 'green': hsl(120, 100%, 12%), diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-vchatlight.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-vchatlight.scss index 0f901de4bb..111cad899f 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-vchatlight.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-vchatlight.scss @@ -1146,6 +1146,47 @@ h2.alert { border-bottom: 1px dashed hsl(0, 0%, 0%); } +.fieldset_legend { + background: hsl(0, 0%, 100%); // Chat background color + + &:before { + background: hsl(0, 0%, 100%); // Chat background color + } +} + +.boxed_message { + background: hsl(220, 100%, 97.5%); + border-color: hsla(220, 75%, 25%, 0.5); + + &.red_box { + background: hsl(0, 100%, 97.5%); + border-color: hsla(0, 100%, 50%, 0.5); + } + + &.green_box { + background: hsl(140, 100%, 97.5%); + border-color: hsl(120, 100%, 33%, 0.5); + } + + &.blue_box { + background: hsl(220, 100%, 97.5%); + border-color: hsla(225, 100%, 50%, 0.5); + } + + &.purple_box { + background: hsl(260, 100%, 97.5%); + border-color: hsla(260, 100%, 50%, 0.5); + } +} + +// Provides a horizontal bar with text in the middle +// I got this off stackoverflow +.separator { + display: flex; + align-items: center; + text-align: center; +} + $alert-stripe-colors: ( 'default': hsl(231, 100%, 85%), 'green': hsl(120, 100%, 84%), diff --git a/tools/bootstrap/javascript_.ps1 b/tools/bootstrap/javascript_.ps1 index 5f286dd4d4..dbf9a7a9b8 100644 --- a/tools/bootstrap/javascript_.ps1 +++ b/tools/bootstrap/javascript_.ps1 @@ -37,6 +37,11 @@ function Get-Bun { $BunTag = " (baseline)" } + if (Test-Path $BunTargetDir -PathType Container) { + Write-Output "Bun target directory exists but bun.exe is missing. Re-downloading." + Remove-Item $BunTargetDir -Recurse -Force + } + $BunSource = "https://github.com/oven-sh/bun/releases/download/bun-v$BunVersion/$BunRelease.zip" Write-Output "Downloading Bun v$BunVersion$BunTag" diff --git a/tools/build/README.md b/tools/build/README.md index 195c9eff83..ce63f21487 100644 --- a/tools/build/README.md +++ b/tools/build/README.md @@ -9,7 +9,7 @@ This build script is the recommended way to compile the game, including not only a) Double-click `BUILD.bat` in the repository root to build (will wait for a key press before it closes). b) Double-click `tools/build/build.bat` to build (will exit as soon as it finishes building). - Linux: - a) Run `tools/build/build` from the repository root. + a) Run `tools/build/build.sh` from the repository root. The script will skip build steps whose inputs have not changed since the last run. @@ -18,7 +18,7 @@ The script will skip build steps whose inputs have not changed since the last ru You can get a list of all targets that you can build by running the following command: ``` -tools/build/build --help +tools/build/build.sh --help ``` ## Dependencies diff --git a/tools/registration/.gitignore b/tools/registration/.gitignore new file mode 100644 index 0000000000..1687d79e66 --- /dev/null +++ b/tools/registration/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +config.json diff --git a/tools/registration/config.json.example b/tools/registration/config.json.example new file mode 100644 index 0000000000..a95663c923 --- /dev/null +++ b/tools/registration/config.json.example @@ -0,0 +1,6 @@ +{ + "port": 6969, + "token": "your-token-goes-here", + "guildId": "your-guild-snowflake-goes-here", + "roleId": "your-role-snowflake-goes-here" +} diff --git a/tools/registration/index.js b/tools/registration/index.js new file mode 100644 index 0000000000..af35488282 --- /dev/null +++ b/tools/registration/index.js @@ -0,0 +1,72 @@ +const http = require("node:http"); +const { Client, GatewayIntentBits } = require("discord.js"); +const { port, token, guildId, roleId } = require("./config.json"); + +async function main() { + const client = new Client({ intents: [] }); + await client.login(token); + + const guild = await client.guilds.fetch(guildId); + if (!guild) { + console.error("ERROR in config, unable to resolve guild " + guildId); + return; + } + + const role = await guild.roles.fetch(roleId); + if (!role) { + console.error("ERROR in config, invalid role ID ", roleId); + return; + } + + const server = http.createServer({}, (req, res) => { + const url = req.url; + if (!url) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ data: "error parsing URL" })) + return; + } + + let urlObj; + try { + urlObj = new URL(`http://localhost${url}`); + } catch (err) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ data: "error parsing URL: " + err })) + return; + } + if (!urlObj.search) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ data: "error parsing URL" })) + return; + } + + const params = urlObj.searchParams; + const member_id = params.get("member"); + + if (!member_id) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ data: "error parsing URL" })) + return; + } + + let memberIdRaw; + try { + memberIdRaw = JSON.parse(member_id); + } catch(err) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ data: "error parsing URL: " + err })) + return; + } + + guild.members.addRole({ user: memberIdRaw, role, reason: "SS13 Registration" }) + console.log("Successfully registered ", memberIdRaw); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ data: "success" })) + }); + + server.listen(port); + console.log("Server running on port", port); +} + +main(); diff --git a/tools/registration/package.json b/tools/registration/package.json new file mode 100644 index 0000000000..e2b1bd7fe1 --- /dev/null +++ b/tools/registration/package.json @@ -0,0 +1,14 @@ +{ + "name": "registration", + "version": "1.0.0", + "description": "Small discord.js bot to handle adding member roles on discord registration", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "tigercat2000", + "license": "MIT", + "dependencies": { + "discord.js": "^14.21.0" + } +} diff --git a/tools/tgs_scripts/InstallDeps.sh b/tools/tgs_scripts/InstallDeps.sh new file mode 100644 index 0000000000..b4212890e7 --- /dev/null +++ b/tools/tgs_scripts/InstallDeps.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +#find out what we have (+e is important for this) +set +e +has_git="$(command -v git)" +has_curl="$(command -v curl)" +has_cargo="$(command -v ~/.cargo/bin/cargo)" +has_sudo="$(command -v sudo)" +has_ytdlp="$(command -v yt-dlp)" +has_pip3="$(command -v pip3)" +has_unzip="$(command -v unzip)" +set -e +set -x + +# apt packages, libssl needed by rust-g but not included in TGS barebones install +if ! ( [ -x "$has_git" ] && [ -x "$has_curl" ] && [ -x "$has_pip3" ] && [ -x "$has_unzip" ] && [ -f "/usr/lib/i386-linux-gnu/libssl.so" ] ); then + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + echo "!!! HEY YOU THERE, READING THE TGS LOGS READ THIS!!!" + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + echo "We are about to try installing native dependencies, we will use 'sudo' if possible for this, but it may fail because the tgstation-server user doesn't have passwordless sudo." + echo "WE DO NOT RECOMMEND GRANTING PASSWORDLESS SUDO!!! Instead, install all the dependencies yourself with the following command:" + echo ".................................................................................................................................................." + echo "sudo apt-get install -y lib32z1 git pkg-config libssl-dev:i386 libssl-dev zlib1g-dev:i386 curl libclang-dev g++-multilib python3 python3-pip unzip" + echo ".................................................................................................................................................." + echo "Attempting to install apt dependencies..." + if ! [ -x "$has_sudo" ]; then + dpkg --add-architecture i386 + apt-get update + apt-get install -y lib32z1 git pkg-config libssl-dev:i386 libssl-dev zlib1g-dev:i386 curl libclang-dev g++-multilib python3 python3-pip unzip + else + sudo dpkg --add-architecture i386 + sudo apt-get update + sudo apt-get install -y lib32z1 git pkg-config libssl-dev:i386 libssl-dev zlib1g-dev:i386 curl libclang-dev g++-multilib python3 python3-pip unzip + fi +fi + +# install cargo if needed +if ! [ -x "$has_cargo" ]; then + echo "Installing rust..." + curl https://sh.rustup.rs -sSf | sh -s -- -y + . ~/.profile +fi + +# install or update yt-dlp when not present, or if it is present with pip3, +# which we assume was used to install it +if ! [ -x "$has_ytdlp" ]; then + echo "Installing yt-dlp with pip3..." + pip3 install yt-dlp --break-system-packages +else + echo "Ensuring yt-dlp is up-to-date with pip3..." + pip3 install yt-dlp -U --break-system-packages +fi diff --git a/tools/tgs_scripts/PreCompile.bat b/tools/tgs_scripts/PreCompile.bat new file mode 100644 index 0000000000..43d3066ae8 --- /dev/null +++ b/tools/tgs_scripts/PreCompile.bat @@ -0,0 +1,6 @@ +@echo off +cd /D "%~dp0" +set TG_BOOTSTRAP_CACHE=%cd% +cd %1 +set CBT_BUILD_MODE=TGS +tools\build\build diff --git a/tools/tgs_scripts/PreCompile.sh b/tools/tgs_scripts/PreCompile.sh new file mode 100644 index 0000000000..eea5f0958c --- /dev/null +++ b/tools/tgs_scripts/PreCompile.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +./InstallDeps.sh + +set -e +set -x + +#load dep exports +#need to switch to game dir for Dockerfile weirdness +original_dir=$PWD +cd "$1" +. dependencies.sh +cd "$original_dir" + + +# update rust-g +if [ ! -d "rust-g" ]; then + echo "Cloning rust-g..." + git clone https://github.com/tgstation/rust-g + cd rust-g + ~/.cargo/bin/rustup target add i686-unknown-linux-gnu +else + echo "Fetching rust-g..." + cd rust-g + git fetch + ~/.cargo/bin/rustup target add i686-unknown-linux-gnu +fi + +echo "Deploying rust-g..." +git checkout "$RUST_G_VERSION" +env PKG_CONFIG_ALLOW_CROSS=1 ~/.cargo/bin/cargo build --ignore-rust-version --release --target=i686-unknown-linux-gnu +mv target/i686-unknown-linux-gnu/release/librust_g.so "$1/librust_g.so" +cd .. + +# +cd "$original_dir" +# update dreamluau +if [ ! -d "dreamluau" ]; then + echo "Cloning dreamluau..." + git clone https://github.com/tgstation/dreamluau + cd dreamluau + ~/.cargo/bin/rustup target add i686-unknown-linux-gnu +else + echo "Fetching dreamlaua..." + cd dreamluau + git fetch + ~/.cargo/bin/rustup target add i686-unknown-linux-gnu +fi + +echo "Deploying Dreamlaua..." +git checkout "$DREAMLUAU_VERSION" +env PKG_CONFIG_ALLOW_CROSS=1 ~/.cargo/bin/cargo build --ignore-rust-version --release --target=i686-unknown-linux-gnu +mv target/i686-unknown-linux-gnu/release/libdreamluau.so "$1/libdreamluau.so" +cd .. + +# compile tgui +echo "Compiling tgui..." +cd "$1" +env TG_BOOTSTRAP_CACHE="$original_dir" CBT_BUILD_MODE="TGS" tools/bootstrap/javascript.sh tools/build/build.js diff --git a/tools/tgs_scripts/WatchdogLaunch.sh b/tools/tgs_scripts/WatchdogLaunch.sh new file mode 100644 index 0000000000..cd400cbb96 --- /dev/null +++ b/tools/tgs_scripts/WatchdogLaunch.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Special file to ensure all dependencies still exist between server launches. +# Mainly for use by people who abuse docker by modifying the container's system. + +./InstallDeps.sh diff --git a/tools/tgs_test/Program.cs b/tools/tgs_test/Program.cs new file mode 100644 index 0000000000..8bc77e9e8e --- /dev/null +++ b/tools/tgs_test/Program.cs @@ -0,0 +1,364 @@ +// Simple app meant to test tgstation's TGS integration given a fresh TGS install with the default account +// +// Args: Repository Owner/Name, TGS instance path, TGS API port, Pushed commit hash (For .tgs.yml access), GitHub Token, (OPTIONAL) PR Number + +using System.Reflection; +using System.Text; + +using Octokit; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models.Request; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Client; +using Tgstation.Server.Common.Extensions; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization; + +Console.WriteLine("Parsing args..."); + +if (args.Length < 5 || args.Length > 6) +{ + Console.WriteLine($"Incorrect number of args: {args.Length}. Expected 5-6"); + return 1; +} + +var repoSlug = args[0]; +var instancePath = args[1]; +var tgsApiPortString = args[2]; +var pushedCommitHash = args[3]; +var gitHubToken = args[4]; + +int? pullRequest = default; +if(args.Length == 6) +{ + if (!Int32.TryParse(args[5], out int prNumber)) + { + Console.WriteLine($"Invalid repo slug: {repoSlug}"); + return 10; + } + + pullRequest = prNumber; +} + +var repoSlugSplits = repoSlug.Split('/', StringSplitOptions.RemoveEmptyEntries); +if(repoSlugSplits.Length != 2) +{ + Console.WriteLine($"Invalid repo slug: {repoSlug}"); + return 2; +} + +var repoOwner = repoSlugSplits[0]; +var repoName = repoSlugSplits[1]; + +if (!ushort.TryParse(tgsApiPortString, out var tgsApiPort)) +{ + Console.WriteLine($"Invalid port: {tgsApiPortString}"); + return 3; +} + +try +{ + Console.WriteLine($"Retrieving .tgs.yml (@{pushedCommitHash})..."); + var assemblyName = Assembly.GetExecutingAssembly().GetName(); + var gitHubClient = new GitHubClient( + new ProductHeaderValue( + assemblyName.Name, + assemblyName.Version!.Semver().ToString())) + { + Credentials = new Credentials(gitHubToken) + }; + + var tgsYmlContent = await gitHubClient.Repository.Content.GetRawContentByRef(repoOwner, repoName, ".tgs.yml", pushedCommitHash); + var tgsYmlString = Encoding.UTF8.GetString(tgsYmlContent); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(new UnderscoredNamingConvention()) + .Build(); + + var tgsYml = deserializer.Deserialize(tgsYmlString); + + const int SupportedTgsYmlVersion = 1; + if (tgsYml.Version != SupportedTgsYmlVersion) + { + Console.WriteLine($"Unsupported .tgs.yml version: {tgsYml.Version}. Expected {SupportedTgsYmlVersion}"); + return 4; + } + + var targetByondVersion = Version.Parse(tgsYml.Byond); + + Console.WriteLine($".tgs.yml Security level: {tgsYml.Security}"); + + Console.WriteLine("Downloading and checking BYOND version in dependencies.sh..."); + var dependenciesShContent = await gitHubClient.Repository.Content.GetRawContentByRef(repoOwner, repoName, "dependencies.sh", pushedCommitHash); + var dependenciesSh = Encoding.UTF8.GetString(dependenciesShContent); + var dependenciesShLines = dependenciesSh.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + int dependenciesShByondMajor = 0; + int dependenciesShByondMinor = 0; + foreach(var dependenciesShLine in dependenciesShLines) + { + var trimmedLine = dependenciesShLine.Trim(); + var lineSplit = trimmedLine.Split('=', StringSplitOptions.RemoveEmptyEntries); + if (lineSplit.Length != 2) + continue; + + if (lineSplit[0].EndsWith("BYOND_MAJOR")) + dependenciesShByondMajor = Int32.Parse(lineSplit[1]); + else if (lineSplit[0].EndsWith("BYOND_MINOR")) + dependenciesShByondMinor = Int32.Parse(lineSplit[1]); + } + + var dependenciesByondVersion = new Version(dependenciesShByondMajor, dependenciesShByondMinor); + if(dependenciesByondVersion != targetByondVersion) + { + Console.WriteLine($".tgs.yml BYOND version does not match dependencies.sh! Expected {dependenciesByondVersion} got {targetByondVersion}!"); + return 5; + } + + // Connect to TGS + var clientFactory = new ServerClientFactory( + new System.Net.Http.Headers.ProductHeaderValue( + assemblyName.Name!, + assemblyName.Version!.Semver().ToString())); + + var tgsApiUrl = new Uri($"http://127.0.0.1:{tgsApiPort}"); + var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(2); + IServerClient client; + for (var I = 1; ; ++I) + { + try + { + Console.WriteLine($"TGS Connection Attempt {I}..."); + client = await clientFactory.CreateFromLogin( + tgsApiUrl, + DefaultCredentials.AdminUserName, + DefaultCredentials.DefaultAdminUserPassword); + break; + } + catch (HttpRequestException) + { + //migrating, to be expected + if (DateTimeOffset.UtcNow > giveUpAt) + throw; + await Task.Delay(TimeSpan.FromSeconds(1)); + } + catch (ServiceUnavailableException) + { + // migrating, to be expected + if (DateTimeOffset.UtcNow > giveUpAt) + throw; + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + + + Console.WriteLine("Getting TGS information..."); + + var tgsInfo = await client.ServerInformation(default); + + var scriptDictionaryToUse = tgsInfo.WindowsHost ? tgsYml.WindowsScripts : tgsYml.LinuxScripts; + Console.WriteLine($"Downloading {scriptDictionaryToUse.Count} EventScripts..."); + + var scriptDownloadTasks = new Dictionary>(); + foreach (var scriptKvp in scriptDictionaryToUse) + { + scriptDownloadTasks.Add( + scriptKvp.Key, + gitHubClient.Repository.Content.GetRawContentByRef(repoOwner, repoName, scriptKvp.Value, pushedCommitHash)); + } + + await Task.WhenAll(scriptDownloadTasks.Values); + + Console.WriteLine("Setting up TGS instance..."); + + var instance = await client.Instances.CreateOrAttach( + new InstanceCreateRequest + { + ConfigurationType = ConfigurationType.HostWrite, + Name = "tgstation", + Path = instancePath + }, + default); + + instance = await client.Instances.Update( + new InstanceUpdateRequest + { + Id = instance.Id, + Online = true + }, + default); + + var instanceClient = client.Instances.CreateClient(instance); + + Console.WriteLine("Cloning main branch of repo..."); + var repoCloneJob = await instanceClient.Repository.Clone( + new RepositoryCreateRequest + { + Origin = new Uri($"http://github.com/{repoSlug}"), + UpdateSubmodules = true, + }, + default); + + Console.WriteLine("Installing BYOND..."); + var byondInstallJob = await instanceClient.Engine.SetActiveVersion( + new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Version = targetByondVersion, + Engine = EngineType.Byond, + } + }, + null, + default); + + Console.WriteLine("Updating server/compiler settings..."); + await instanceClient.DreamMaker.Update( + new DreamMakerRequest + { + ApiValidationSecurityLevel = tgsYml.Security + }, + default); + + await instanceClient.DreamDaemon.Update( + new DreamDaemonRequest + { + SecurityLevel = tgsYml.Security, + Visibility = DreamDaemonVisibility.Invisible + }, + default); + + Console.WriteLine("Uploading EventScripts..."); + foreach (var scriptDownloadKvp in scriptDownloadTasks) + { + var scriptContent = await scriptDownloadKvp.Value; + + var memoryStream = new MemoryStream(scriptContent); + await instanceClient.Configuration.Write( + new ConfigurationFileRequest + { + Path = $"EventScripts/{scriptDownloadKvp.Key}" + }, + memoryStream, + default); + } + + Console.WriteLine("Creating GameStaticFiles structure..."); + var staticFileDownloadTasks = new Dictionary>>(); + foreach (var staticFile in tgsYml.StaticFiles) + { + if (!staticFile.Populate) + { + Console.WriteLine($"Creating empty directory GameStaticFiles/{staticFile.Name}..."); + await instanceClient.Configuration.CreateDirectory(new ConfigurationFileRequest + { + Path = $"GameStaticFiles/{staticFile.Name}" + }, + default); + } + else + { + // not by ref here as we are relying on master being not broken + Console.WriteLine($"Enumerating repo path {staticFile.Name}..."); + var repositoryFilesToUpload = new Queue(await gitHubClient.Repository.Content.GetAllContents(repoOwner, repoName, staticFile.Name)); + while (repositoryFilesToUpload.Count != 0) + { + var repositoryFileToUpload = repositoryFilesToUpload.Dequeue(); + if (repositoryFileToUpload.Type == ContentType.File) + { + // serial because easier to track errors + Console.WriteLine($"Transferring {repositoryFileToUpload.Path}..."); + var fileContent = await gitHubClient.Repository.Content.GetRawContent(repoOwner, repoName, repositoryFileToUpload.Path); + using var memoryStream = new MemoryStream(fileContent); + await instanceClient.Configuration.Write(new ConfigurationFileRequest + { + Path = $"GameStaticFiles/{repositoryFileToUpload.Path}" + }, + memoryStream, + default); + } + else + { + Console.WriteLine($"Enumerating repo path {repositoryFileToUpload.Path}..."); + var additionalFiles = await gitHubClient.Repository.Content.GetAllContents(repoOwner, repoName, repositoryFileToUpload.Path); + foreach (var additionalFile in additionalFiles) + repositoryFilesToUpload.Enqueue(additionalFile); + } + } + } + } + + async Task WaitForJob(JobResponse originalJob, int timeout) + { + Console.WriteLine($"Waiting for job \"{originalJob.Description}\"..."); + var job = originalJob; + var previousProgress = job.Progress; + do + { + if (job.Progress != previousProgress) + Console.WriteLine($"Progress: {previousProgress = job.Progress}"); + + await Task.Delay(TimeSpan.FromSeconds(1)); + job = await instanceClient!.Jobs.GetId(job, default); + --timeout; + } + while (!job.StoppedAt.HasValue && timeout > 0); + + if (!job.StoppedAt.HasValue) + { + await instanceClient!.Jobs.Cancel(job, default); + Console.WriteLine($"Timed out!"); + return false; + } + else if (job.ExceptionDetails != null) + { + Console.WriteLine($"Error: {job.ExceptionDetails}"); + return false; + } + + return true; + } + + if (!await WaitForJob(byondInstallJob.InstallJob!, 120)) + return 6; + + if (!await WaitForJob(repoCloneJob.ActiveJob!, 600)) + return 7; + + if (pullRequest.HasValue) + { + Console.WriteLine($"Applying test merge #{pullRequest}..."); + var testMergeJob = await instanceClient.Repository.Update(new RepositoryUpdateRequest + { + NewTestMerges = new List + { + new TestMergeParameters + { + Comment = "Active Pull Request", + Number = pullRequest.Value, + TargetCommitSha = pushedCommitHash + } + } + }, default); + if (!await WaitForJob(testMergeJob.ActiveJob!, 60)) + return 11; + } + + Console.WriteLine("Deploying..."); + var deploymentJob = await instanceClient.DreamMaker.Compile(default); + if (!await WaitForJob(deploymentJob, 1800)) + return 8; + + Console.WriteLine("Launching..."); + var launchJob = await instanceClient.DreamDaemon.Start(default); + if (!await WaitForJob(launchJob, 300)) + return 9; + + return 0; +} +catch (Exception ex) +{ + Console.WriteLine(ex); + return 4; +} diff --git a/tools/tgs_test/README.md b/tools/tgs_test/README.md new file mode 100644 index 0000000000..436f21d894 --- /dev/null +++ b/tools/tgs_test/README.md @@ -0,0 +1,11 @@ +# TGS Test Script + +This is a simple app that does a few things + +- Downloads .tgs.yml information from a specific commit of a given repository. +- Checks that the BYOND version in the .tgs.yml file matches the dependencies.sh version. +- Connects to a TGS instance via command line parameters. +- Uses the .tgs.yml information to automatically set up a TGS instance. +- Runs a TGS deploy/launch and validates that they succeeded. + +Look for its invocation in the GitHub workflows diff --git a/tools/tgs_test/StaticFile.cs b/tools/tgs_test/StaticFile.cs new file mode 100644 index 0000000000..135f159ad6 --- /dev/null +++ b/tools/tgs_test/StaticFile.cs @@ -0,0 +1,5 @@ +sealed class StaticFile +{ + public string Name { get; set; } = String.Empty; + public bool Populate { get; set; } +} diff --git a/tools/tgs_test/TgsYml.cs b/tools/tgs_test/TgsYml.cs new file mode 100644 index 0000000000..c738ac4fde --- /dev/null +++ b/tools/tgs_test/TgsYml.cs @@ -0,0 +1,15 @@ +using Tgstation.Server.Api.Models; + +sealed class TgsYml +{ + public int Version { get; set; } + + public string Byond { get; set; } = String.Empty; + + public List StaticFiles { get; set; } = new List(); + + public Dictionary WindowsScripts { get; set; } = new Dictionary(); + public Dictionary LinuxScripts { get; set; } = new Dictionary(); + + public DreamDaemonSecurity Security { get; set; } +} diff --git a/tools/tgs_test/Tgstation.TgsTest.csproj b/tools/tgs_test/Tgstation.TgsTest.csproj new file mode 100644 index 0000000000..967fbd4295 --- /dev/null +++ b/tools/tgs_test/Tgstation.TgsTest.csproj @@ -0,0 +1,18 @@ + + + + 2.0.0 + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/tools/tgs_test/Tgstation.TgsTest.sln b/tools/tgs_test/Tgstation.TgsTest.sln new file mode 100644 index 0000000000..37a7a119a8 --- /dev/null +++ b/tools/tgs_test/Tgstation.TgsTest.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.TgsTest", "Tgstation.TgsTest.csproj", "{3146D745-AAE5-4205-8FF2-0CE471B47B4E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3146D745-AAE5-4205-8FF2-0CE471B47B4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3146D745-AAE5-4205-8FF2-0CE471B47B4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3146D745-AAE5-4205-8FF2-0CE471B47B4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3146D745-AAE5-4205-8FF2-0CE471B47B4E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {001721B4-7740-419D-837E-26CE9DABCAB8} + EndGlobalSection +EndGlobal diff --git a/vorestation.dme b/vorestation.dme index 50d57a3c40..94d297b7ef 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -488,6 +488,7 @@ #include "code\datums\signals.dm" #include "code\datums\soul_link.dm" #include "code\datums\sun.dm" +#include "code\datums\tgs_event_handler.dm" #include "code\datums\verb_callbacks.dm" #include "code\datums\weakrefs.dm" #include "code\datums\autolathe\arms.dm" @@ -4283,6 +4284,7 @@ #include "code\modules\tgchat\message.dm" #include "code\modules\tgchat\to_chat.dm" #include "code\modules\tgs\includes.dm" +#include "code\modules\tgs_commands\vorestation.dm" #include "code\modules\tgui\external.dm" #include "code\modules\tgui\modal.dm" #include "code\modules\tgui\states.dm"