From ef92a397ef5cbe34972add697d8b07a193526465 Mon Sep 17 00:00:00 2001
From: Aeris Bennet <156572979+vgcataeris@users.noreply.github.com>
Date: Tue, 6 Feb 2024 20:05:46 +0000
Subject: [PATCH 1/7] Retrofitting new DMAPI libs.
---
code/modules/tgs/core/_definitions.dm | 12 +-
code/modules/tgs/core/core.dm | 312 +++++++-------
code/modules/tgs/core/datum.dm | 145 ++++---
code/modules/tgs/core/tgs_version.dm | 28 ++
code/modules/tgs/includes.dm | 27 +-
code/modules/tgs/v3210/README.md | 6 +
code/modules/tgs/v3210/api.dm | 492 +++++++++++------------
code/modules/tgs/v3210/commands.dm | 136 +++----
code/modules/tgs/v4/api.dm | 322 +++++++++++++++
code/modules/tgs/v4/commands.dm | 44 ++
code/modules/tgs/v5/__interop_version.dm | 1 +
code/modules/tgs/v5/_defines.dm | 118 ++++++
code/modules/tgs/v5/api.dm | 281 +++++++++++++
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 | 283 +++++++++++++
code/modules/tgs/v5/undefs.dm | 118 ++++++
19 files changed, 2041 insertions(+), 552 deletions(-)
create mode 100644 code/modules/tgs/core/tgs_version.dm
create mode 100644 code/modules/tgs/v3210/README.md
create mode 100644 code/modules/tgs/v4/api.dm
create mode 100644 code/modules/tgs/v4/commands.dm
create mode 100644 code/modules/tgs/v5/__interop_version.dm
create mode 100644 code/modules/tgs/v5/_defines.dm
create mode 100644 code/modules/tgs/v5/api.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/commands.dm
create mode 100644 code/modules/tgs/v5/serializers.dm
create mode 100644 code/modules/tgs/v5/topic.dm
create mode 100644 code/modules/tgs/v5/undefs.dm
diff --git a/code/modules/tgs/core/_definitions.dm b/code/modules/tgs/core/_definitions.dm
index ebf6d17c..e8a041cd 100644
--- a/code/modules/tgs/core/_definitions.dm
+++ b/code/modules/tgs/core/_definitions.dm
@@ -1,2 +1,10 @@
-#define TGS_UNIMPLEMENTED "___unimplemented"
-#define TGS_VERSION_PARAMETER "server_service_version"
+#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/core.dm b/code/modules/tgs/core/core.dm
index 61f1e71d..b5029c57 100644
--- a/code/modules/tgs/core/core.dm
+++ b/code/modules/tgs/core/core.dm
@@ -1,144 +1,168 @@
-/world/TgsNew(datum/tgs_event_handler/event_handler)
- var/tgs_version = world.params[TGS_VERSION_PARAMETER]
- if(!tgs_version)
- return
-
- var/path = SelectTgsApi(tgs_version)
- if(!path)
- TGS_ERROR_LOG("Found unsupported API version: [tgs_version]. If this is a valid version please report this, backporting is done on demand.")
-
- TGS_INFO_LOG("Activating API for version [tgs_version]")
- var/datum/tgs_api/new_api = new path
-
- var/result = new_api.OnWorldNew(event_handler ? event_handler : new /datum/tgs_event_handler/tgs_default)
- if(result && result != TGS_UNIMPLEMENTED)
- TGS_WRITE_GLOBAL(tgs, new_api)
- else
- TGS_ERROR_LOG("Failed to activate API!")
-
-/world/proc/SelectTgsApi(tgs_version)
- //remove the old 3.0 header
- tgs_version = replacetext(tgs_version, "/tg/station 13 Server v", "")
-
- var/list/version_bits = splittext(tgs_version, ".")
-
- var/super = text2num(version_bits[1])
- var/major = text2num(version_bits[2])
- var/minor = text2num(version_bits[3])
- var/patch = text2num(version_bits[4])
-
- switch(super)
- if(3)
- switch(major)
- if(2)
- return /datum/tgs_api/v3210
-
- if(super != null && major != null && minor != null && patch != null && tgs_version > TgsMaximumAPIVersion())
- TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.")
- return /datum/tgs_api/latest
-
-/world/TgsMaximumAPIVersion()
- return "4.0.0.0"
-
-/world/TgsMinimumAPIVersion()
- return "3.2.0.0"
-
-/world/TgsInitializationComplete()
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- api.OnInitializationComplete()
-
-/world/proc/TgsTopic(T)
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- var/result = api.OnTopic(T)
- if(result != TGS_UNIMPLEMENTED)
- return result
-
-/world/TgsRevision()
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- var/result = api.Revision()
- if(result != TGS_UNIMPLEMENTED)
- return result
-
-/world/TgsReboot()
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- api.OnReboot()
-
-/world/TgsAvailable()
- return TGS_READ_GLOBAL(tgs) != null
-
-/world/TgsVersion()
- return world.params[TGS_VERSION_PARAMETER]
-
-/world/TgsInstanceName()
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- var/result = api.InstanceName()
- if(result != TGS_UNIMPLEMENTED)
- return result
-
-/world/TgsTestMerges()
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- var/result = api.TestMerges()
- if(result != TGS_UNIMPLEMENTED)
- return result
- return list()
-
-/world/TgsEndProcess()
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- api.EndProcess()
-
-/world/TgsChatChannelInfo()
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- var/result = api.ChatChannelInfo()
- if(result != TGS_UNIMPLEMENTED)
- return result
- return list()
-
-/world/TgsChatBroadcast(message, list/channels)
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- api.ChatBroadcast(message, channels)
-
-/world/TgsTargetedChatBroadcast(message, admin_only)
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- api.ChatTargetedBroadcast(message, admin_only)
-
-/world/TgsChatPrivateMessage(message, datum/tgs_chat_user/user)
- var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
- if(api)
- api.ChatPrivateMessage(message, user)
-
-/*
-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.
-*/
+/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE)
+ 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?")
+ return
+
+ if(!(minimum_required_security_level in list(TGS_SECURITY_ULTRASAFE, TGS_SECURITY_SAFE, TGS_SECURITY_TRUSTED)))
+ TGS_ERROR_LOG("Invalid minimum_required_security_level: [minimum_required_security_level]!")
+ return
+
+#ifdef TGS_V3_API
+ if(minimum_required_security_level != TGS_SECURITY_TRUSTED)
+ TGS_WARNING_LOG("V3 DMAPI requires trusted security!")
+ minimum_required_security_level = TGS_SECURITY_TRUSTED
+#endif
+ var/raw_parameter = world.params[TGS_VERSION_PARAMETER]
+ if(!raw_parameter)
+ return
+
+ var/datum/tgs_version/version = new(raw_parameter)
+ if(!version.Valid(FALSE))
+ TGS_ERROR_LOG("Failed to validate DMAPI version parameter: [raw_parameter]!")
+ return
+
+ var/api_datum
+ switch(version.suite)
+ if(3)
+#ifndef TGS_V3_API
+ TGS_ERROR_LOG("Detected V3 API but TGS_V3_API isn't defined!")
+ return
+#else
+ switch(version.minor)
+ if(2)
+ api_datum = /datum/tgs_api/v3210
+#endif
+ if(4)
+ switch(version.minor)
+ if(0)
+ api_datum = /datum/tgs_api/v4
+ if(5)
+ api_datum = /datum/tgs_api/v5
+
+ 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 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 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]")
+
+ 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, version)
+
+ TGS_WRITE_GLOBAL(tgs, new_api)
+
+ var/result = new_api.OnWorldNew(minimum_required_security_level)
+ if(!result || result == TGS_UNIMPLEMENTED)
+ TGS_WRITE_GLOBAL(tgs, null)
+ TGS_ERROR_LOG("Failed to activate API!")
+
+/world/TgsMaximumApiVersion()
+ return new /datum/tgs_version("5.x.x")
+
+/world/TgsMinimumApiVersion()
+ return new /datum/tgs_version("3.2.x")
+
+/world/TgsInitializationComplete()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ api.OnInitializationComplete()
+
+/world/proc/TgsTopic(T)
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ var/result = api.OnTopic(T)
+ if(result != TGS_UNIMPLEMENTED)
+ return result
+
+/world/TgsRevision()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ var/result = api.Revision()
+ if(result != TGS_UNIMPLEMENTED)
+ return result
+
+/world/TgsReboot()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ api.OnReboot()
+
+/world/TgsAvailable()
+ return TGS_READ_GLOBAL(tgs) != null
+
+/world/TgsVersion()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ return api.version
+
+/world/TgsApiVersion()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ 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)
+ var/result = api.InstanceName()
+ if(result != TGS_UNIMPLEMENTED)
+ return result
+
+/world/TgsTestMerges()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ var/result = api.TestMerges()
+ if(result != TGS_UNIMPLEMENTED)
+ return result
+ return list()
+
+/world/TgsEndProcess()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ api.EndProcess()
+
+/world/TgsChatChannelInfo()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ var/result = api.ChatChannelInfo()
+ if(result != TGS_UNIMPLEMENTED)
+ return result
+ return list()
+
+/world/TgsChatBroadcast(message, list/channels)
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ api.ChatBroadcast(message, channels)
+
+/world/TgsTargetedChatBroadcast(message, admin_only)
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ api.ChatTargetedBroadcast(message, admin_only)
+
+/world/TgsChatPrivateMessage(message, datum/tgs_chat_user/user)
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ api.ChatPrivateMessage(message, user)
+
+/world/TgsSecurityLevel()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ return api.SecurityLevel()
+
+/world/TgsVisibility()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ return api.Visibility()
diff --git a/code/modules/tgs/core/datum.dm b/code/modules/tgs/core/datum.dm
index 6af39a75..d2c7a6d3 100644
--- a/code/modules/tgs/core/datum.dm
+++ b/code/modules/tgs/core/datum.dm
@@ -1,74 +1,71 @@
-TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
-
-/datum/tgs_api
-
-/datum/tgs_api/latest
- parent_type = /datum/tgs_api/v3210
-
-TGS_PROTECT_DATUM(/datum/tgs_api)
-
-/datum/tgs_api/proc/ApiVersion()
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/OnWorldNew(datum/tgs_event_handler/event_handler)
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/OnInitializationComplete()
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/OnTopic(T)
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/OnReboot()
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/InstanceName()
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/TestMerges()
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/EndProcess()
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/Revision()
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/ChatChannelInfo()
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/ChatBroadcast(message, list/channels)
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/ChatTargetedBroadcast(message, admin_only)
- return TGS_UNIMPLEMENTED
-
-/datum/tgs_api/proc/ChatPrivateMessage(message, admin_only)
- return TGS_UNIMPLEMENTED
-
-/*
-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.
-*/
+TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
+
+/datum/tgs_api
+ var/datum/tgs_version/version
+ var/datum/tgs_event_handler/event_handler
+
+ var/list/warned_deprecated_command_runs
+
+/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version)
+ . = ..()
+ 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(1)
+ 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
+
+TGS_PROTECT_DATUM(/datum/tgs_api)
+
+/datum/tgs_api/proc/ApiVersion()
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/OnWorldNew(datum/tgs_event_handler/event_handler)
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/OnInitializationComplete()
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/OnTopic(T)
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/OnReboot()
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/InstanceName()
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/TestMerges()
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/EndProcess()
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/Revision()
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/ChatChannelInfo()
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/ChatBroadcast(message, list/channels)
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/ChatTargetedBroadcast(message, admin_only)
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/ChatPrivateMessage(message, datum/tgs_chat_user/user)
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/SecurityLevel()
+ return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/Visibility()
+ return TGS_UNIMPLEMENTED
diff --git a/code/modules/tgs/core/tgs_version.dm b/code/modules/tgs/core/tgs_version.dm
new file mode 100644
index 00000000..6d19cdb2
--- /dev/null
+++ b/code/modules/tgs/core/tgs_version.dm
@@ -0,0 +1,28 @@
+/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, ".")
+
+ suite = text2num(version_bits[1])
+ if(version_bits.len > 1)
+ minor = text2num(version_bits[2])
+ if(version_bits.len > 2)
+ patch = text2num(version_bits[3])
+ if(version_bits.len == 4)
+ deprecated_patch = text2num(version_bits[4])
+
+/datum/tgs_version/proc/Valid(allow_wildcards = FALSE)
+ if(suite == null)
+ return FALSE
+ if(allow_wildcards)
+ return TRUE
+ return !Wildcard()
+
+/datum/tgs_version/Wildcard()
+ return minor == null || patch == null
+
+/datum/tgs_version/Equals(datum/tgs_version/other_version)
+ if(!istype(other_version))
+ return FALSE
+
+ return suite == other_version.suite && minor == other_version.minor && patch == other_version.patch && deprecated_patch == other_version.deprecated_patch
diff --git a/code/modules/tgs/includes.dm b/code/modules/tgs/includes.dm
index 7ca906c8..a8c84bb7 100644
--- a/code/modules/tgs/includes.dm
+++ b/code/modules/tgs/includes.dm
@@ -1,6 +1,21 @@
-#include "core\_definitions.dm"
-#include "core\core.dm"
-#include "core\datum.dm"
-#include "core\default_event_handler.dm"
-#include "v3210\api.dm"
-#include "v3210\commands.dm"
+#include "core\_definitions.dm"
+#include "core\core.dm"
+#include "core\datum.dm"
+#include "core\tgs_version.dm"
+
+#ifdef TGS_V3_API
+#include "v3210\api.dm"
+#include "v3210\commands.dm"
+#endif
+
+#include "v4\api.dm"
+#include "v4\commands.dm"
+
+#include "v5\_defines.dm"
+#include "v5\api.dm"
+#include "v5\bridge.dm"
+#include "v5\chunking.dm"
+#include "v5\commands.dm"
+#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 00000000..c50faf75
--- /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 2a95f786..5f1cb320 100644
--- a/code/modules/tgs/v3210/api.dm
+++ b/code/modules/tgs/v3210/api.dm
@@ -1,248 +1,244 @@
-#define REBOOT_MODE_NORMAL 0
-#define REBOOT_MODE_HARD 1
-#define REBOOT_MODE_SHUTDOWN 2
-
-#define SERVICE_WORLD_PARAM "server_service"
-#define SERVICE_INSTANCE_PARAM "server_instance"
-#define SERVICE_PR_TEST_JSON "prtestjob.json"
-#define SERVICE_INTERFACE_DLL "TGDreamDaemonBridge.dll"
-#define SERVICE_INTERFACE_FUNCTION "DDEntryPoint"
-
-#define SERVICE_CMD_HARD_REBOOT "hard_reboot"
-#define SERVICE_CMD_GRACEFUL_SHUTDOWN "graceful_shutdown"
-#define SERVICE_CMD_WORLD_ANNOUNCE "world_announce"
-#define SERVICE_CMD_LIST_CUSTOM "list_custom_commands"
-#define SERVICE_CMD_API_COMPATIBLE "api_compat"
-#define SERVICE_CMD_PLAYER_COUNT "client_count"
-
-#define SERVICE_CMD_PARAM_KEY "serviceCommsKey"
-#define SERVICE_CMD_PARAM_COMMAND "command"
-#define SERVICE_CMD_PARAM_SENDER "sender"
-#define SERVICE_CMD_PARAM_CUSTOM "custom"
-
-#define SERVICE_REQUEST_KILL_PROCESS "killme"
-#define SERVICE_REQUEST_IRC_BROADCAST "irc"
-#define SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE "send2irc"
-#define SERVICE_REQUEST_WORLD_REBOOT "worldreboot"
-#define SERVICE_REQUEST_API_VERSION "api_ver"
-
-#define SERVICE_RETURN_SUCCESS "SUCCESS"
-
-/datum/tgs_api/v3210
- var/reboot_mode = REBOOT_MODE_NORMAL
- var/comms_key
- var/instance_name
- var/originmastercommit
- var/commit
- var/list/cached_custom_tgs_chat_commands
- var/warned_revison = FALSE
- var/warned_custom_commands = FALSE
-
-/datum/tgs_api/v3210/ApiVersion()
- return "3.2.1.0"
-
-/datum/tgs_api/v3210/proc/trim_left(text)
- for (var/i = 1 to length(text))
- if (text2ascii(text, i) > 32)
- return copytext(text, i)
- return ""
-
-/datum/tgs_api/v3210/proc/trim_right(text)
- for (var/i = length(text), i > 0, i--)
- if (text2ascii(text, i) > 32)
- 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(datum/tgs_event_handler/event_handler) //don't use event handling in this version
- . = 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
-
- var/list/logs = file2list(".git/logs/HEAD")
- if(logs.len)
- logs = splittext(logs[logs.len - 1], " ")
- commit = logs[2]
- logs = file2list(".git/logs/refs/remotes/origin/master")
- if(logs.len)
- originmastercommit = splittext(logs[logs.len - 1], " ")[2]
-
- if(world.system_type != MS_WINDOWS)
- TGS_ERROR_LOG("This API version is only supported on Windows. Not running on Windows. Aborting initialization!")
- return
- ListServiceCustomCommands(TRUE)
- ExportService("[SERVICE_REQUEST_API_VERSION] [ApiVersion()]", TRUE)
- return TRUE
-
-//nothing to do for v3
-/datum/tgs_api/v3210/OnInitializationComplete()
- return
-
-/datum/tgs_api/v3210/InstanceName()
- return world.params[SERVICE_INSTANCE_PARAM]
-
-/datum/tgs_api/v3210/proc/ExportService(command, skip_compat_check = FALSE)
- . = FALSE
- 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
- call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval
- 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
-
- if(their_sCK != comms_key)
- return "Invalid comms key!";
-
- var/command = params[SERVICE_CMD_PARAM_COMMAND]
- if(!command)
- return "No command!"
-
- switch(command)
- if(SERVICE_CMD_API_COMPATIBLE)
- return SERVICE_RETURN_SUCCESS
- if(SERVICE_CMD_HARD_REBOOT)
- if(reboot_mode != REBOOT_MODE_HARD)
- reboot_mode = REBOOT_MODE_HARD
- TGS_INFO_LOG("Hard reboot requested by service")
- TGS_NOTIFY_ADMINS("The world will hard reboot at the end of the game. Requested by TGS.")
- if(SERVICE_CMD_GRACEFUL_SHUTDOWN)
- if(reboot_mode != REBOOT_MODE_SHUTDOWN)
- reboot_mode = REBOOT_MODE_SHUTDOWN
- TGS_INFO_LOG("Shutdown requested by service")
- TGS_NOTIFY_ADMINS("The world will shutdown at the end of the game. Requested by TGS.")
- if(SERVICE_CMD_WORLD_ANNOUNCE)
- var/msg = params["message"]
- if(!istext(msg) || !msg)
- return "No message set!"
- TGS_WORLD_ANNOUNCE(msg)
- return SERVICE_RETURN_SUCCESS
- if(SERVICE_CMD_PLAYER_COUNT)
- return "[TGS_CLIENT_COUNT]"
- if(SERVICE_CMD_LIST_CUSTOM)
- return json_encode(ListServiceCustomCommands(FALSE))
- else
- var/custom_command_result = HandleServiceCustomCommand(lowertext(command), params[SERVICE_CMD_PARAM_SENDER], params[SERVICE_CMD_PARAM_CUSTOM])
- if(custom_command_result)
- return istext(custom_command_result) ? custom_command_result : SERVICE_RETURN_SUCCESS
- return "Unknown command: [command]"
-
-/datum/tgs_api/v3210/OnReboot()
- switch(reboot_mode)
- if(REBOOT_MODE_HARD)
- TGS_WORLD_ANNOUNCE("Hard reboot triggered, you will automatically reconnect...")
- EndProcess()
- if(REBOOT_MODE_SHUTDOWN)
- TGS_WORLD_ANNOUNCE("The server is shutting down...")
- EndProcess()
- else
- ExportService(SERVICE_REQUEST_WORLD_REBOOT) //just let em know
-
-/datum/tgs_api/v3210/TestMerges()
- //do the best we can here as the datum can't be completed using the v3 api
- . = list()
- if(!fexists(SERVICE_PR_TEST_JSON))
- return
- var/list/json = json_decode(file2text(SERVICE_PR_TEST_JSON))
- if(!json)
- return
- for(var/I in json)
- 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.author = entry["author"]
- tm.title = entry["title"]
- . += tm
-
-/datum/tgs_api/v3210/Revision()
- if(!warned_revison)
- TGS_ERROR_LOG("Use of TgsRevision on [ApiVersion()] origin_commit only points to master!")
- warned_revison = TRUE
- var/datum/tgs_revision_information/ri = new
- ri.commit = commit
- ri.origin_commit = originmastercommit
-
-/datum/tgs_api/v3210/EndProcess()
- sleep(world.tick_lag) //flush the buffers
- ExportService(SERVICE_REQUEST_KILL_PROCESS)
-
-/datum/tgs_api/v3210/ChatChannelInfo()
- return list()
-
-/datum/tgs_api/v3210/ChatBroadcast(message, list/channels)
- if(channels)
- return TGS_UNIMPLEMENTED
- 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/ChatPrivateMessage(message, admin_only)
- return TGS_UNIMPLEMENTED
-
-#undef REBOOT_MODE_NORMAL
-#undef REBOOT_MODE_HARD
-#undef REBOOT_MODE_SHUTDOWN
-
-#undef SERVICE_WORLD_PARAM
-#undef SERVICE_INSTANCE_PARAM
-#undef SERVICE_PR_TEST_JSON
-#undef SERVICE_INTERFACE_DLL
-#undef SERVICE_INTERFACE_FUNCTION
-
-#undef SERVICE_CMD_HARD_REBOOT
-#undef SERVICE_CMD_GRACEFUL_SHUTDOWN
-#undef SERVICE_CMD_WORLD_ANNOUNCE
-#undef SERVICE_CMD_LIST_CUSTOM
-#undef SERVICE_CMD_API_COMPATIBLE
-#undef SERVICE_CMD_PLAYER_COUNT
-
-#undef SERVICE_CMD_PARAM_KEY
-#undef SERVICE_CMD_PARAM_COMMAND
-#undef SERVICE_CMD_PARAM_SENDER
-#undef SERVICE_CMD_PARAM_CUSTOM
-
-#undef SERVICE_REQUEST_KILL_PROCESS
-#undef SERVICE_REQUEST_IRC_BROADCAST
-#undef SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE
-#undef SERVICE_REQUEST_WORLD_REBOOT
-#undef SERVICE_REQUEST_API_VERSION
-
-#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.
-*/
+#define REBOOT_MODE_NORMAL 0
+#define REBOOT_MODE_HARD 1
+#define REBOOT_MODE_SHUTDOWN 2
+
+#define SERVICE_WORLD_PARAM "server_service"
+#define SERVICE_INSTANCE_PARAM "server_instance"
+#define SERVICE_PR_TEST_JSON "prtestjob.json"
+#define SERVICE_INTERFACE_DLL "TGDreamDaemonBridge.dll"
+#define SERVICE_INTERFACE_FUNCTION "DDEntryPoint"
+
+#define SERVICE_CMD_HARD_REBOOT "hard_reboot"
+#define SERVICE_CMD_GRACEFUL_SHUTDOWN "graceful_shutdown"
+#define SERVICE_CMD_WORLD_ANNOUNCE "world_announce"
+#define SERVICE_CMD_LIST_CUSTOM "list_custom_commands"
+#define SERVICE_CMD_API_COMPATIBLE "api_compat"
+#define SERVICE_CMD_PLAYER_COUNT "client_count"
+
+#define SERVICE_CMD_PARAM_KEY "serviceCommsKey"
+#define SERVICE_CMD_PARAM_COMMAND "command"
+#define SERVICE_CMD_PARAM_SENDER "sender"
+#define SERVICE_CMD_PARAM_CUSTOM "custom"
+
+#define SERVICE_REQUEST_KILL_PROCESS "killme"
+#define SERVICE_REQUEST_IRC_BROADCAST "irc"
+#define SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE "send2irc"
+#define SERVICE_REQUEST_WORLD_REBOOT "worldreboot"
+#define SERVICE_REQUEST_API_VERSION "api_ver"
+
+#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
+ var/instance_name
+ var/originmastercommit
+ var/commit
+ var/list/cached_custom_tgs_chat_commands
+ var/warned_revison = FALSE
+ var/warned_custom_commands = FALSE
+
+/datum/tgs_api/v3210/ApiVersion()
+ return new /datum/tgs_version("3.2.1.3")
+
+/datum/tgs_api/v3210/proc/trim_left(text)
+ for (var/i = 1 to length(text))
+ if (text2ascii(text, i) > 32)
+ return copytext(text, i)
+ return ""
+
+/datum/tgs_api/v3210/proc/trim_right(text)
+ for (var/i = length(text), i > 0, i--)
+ if (text2ascii(text, i) > 32)
+ return copytext(text, 1, i + 1)
+ return ""
+
+/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
+
+ var/list/logs = TGS_FILE2LIST(".git/logs/HEAD")
+ if(logs.len)
+ 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)
+ 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!")
+ return
+ ListServiceCustomCommands(TRUE)
+ var/datum/tgs_version/api_version = ApiVersion()
+ ExportService("[SERVICE_REQUEST_API_VERSION] [api_version.deprefixed_parameter]", TRUE)
+ return TRUE
+
+//nothing to do for v3
+/datum/tgs_api/v3210/OnInitializationComplete()
+ return
+
+/datum/tgs_api/v3210/InstanceName()
+ return world.params[SERVICE_INSTANCE_PARAM]
+
+/datum/tgs_api/v3210/proc/ExportService(command, skip_compat_check = FALSE)
+ . = FALSE
+ 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
+ #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
+
+ if(their_sCK != comms_key)
+ return "Invalid comms key!";
+
+ var/command = params[SERVICE_CMD_PARAM_COMMAND]
+ if(!command)
+ return "No command!"
+
+ switch(command)
+ if(SERVICE_CMD_API_COMPATIBLE)
+ return SERVICE_RETURN_SUCCESS
+ if(SERVICE_CMD_HARD_REBOOT)
+ if(reboot_mode != REBOOT_MODE_HARD)
+ reboot_mode = REBOOT_MODE_HARD
+ TGS_INFO_LOG("Hard reboot requested by service")
+ TGS_NOTIFY_ADMINS("The world will hard reboot at the end of the game. Requested by TGS.")
+ if(SERVICE_CMD_GRACEFUL_SHUTDOWN)
+ if(reboot_mode != REBOOT_MODE_SHUTDOWN)
+ reboot_mode = REBOOT_MODE_SHUTDOWN
+ TGS_INFO_LOG("Shutdown requested by service")
+ TGS_NOTIFY_ADMINS("The world will shutdown at the end of the game. Requested by TGS.")
+ if(SERVICE_CMD_WORLD_ANNOUNCE)
+ var/msg = params["message"]
+ if(!istext(msg) || !msg)
+ return "No message set!"
+ TGS_WORLD_ANNOUNCE(msg)
+ return SERVICE_RETURN_SUCCESS
+ if(SERVICE_CMD_PLAYER_COUNT)
+ return "[TGS_CLIENT_COUNT]"
+ if(SERVICE_CMD_LIST_CUSTOM)
+ return json_encode(ListServiceCustomCommands(FALSE))
+ else
+ var/custom_command_result = HandleServiceCustomCommand(lowertext(command), params[SERVICE_CMD_PARAM_SENDER], params[SERVICE_CMD_PARAM_CUSTOM])
+ if(custom_command_result)
+ return istext(custom_command_result) ? custom_command_result : SERVICE_RETURN_SUCCESS
+ return "Unknown command: [command]"
+
+/datum/tgs_api/v3210/OnReboot()
+ switch(reboot_mode)
+ if(REBOOT_MODE_HARD)
+ TGS_WORLD_ANNOUNCE("Hard reboot triggered, you will automatically reconnect...")
+ EndProcess()
+ if(REBOOT_MODE_SHUTDOWN)
+ TGS_WORLD_ANNOUNCE("The server is shutting down...")
+ EndProcess()
+ else
+ ExportService(SERVICE_REQUEST_WORLD_REBOOT) //just let em know
+
+/datum/tgs_api/v3210/TestMerges()
+ //do the best we can here as the datum can't be completed using the v3 api
+ . = list()
+ if(!fexists(SERVICE_PR_TEST_JSON))
+ return
+ var/list/json = json_decode(file2text(SERVICE_PR_TEST_JSON))
+ if(!json)
+ return
+ for(var/I in json)
+ var/datum/tgs_revision_information/test_merge/tm = new
+ tm.number = text2num(I)
+ var/list/entry = json[I]
+ tm.head_commit = entry["commit"]
+ tm.author = entry["author"]
+ tm.title = entry["title"]
+ . += tm
+
+/datum/tgs_api/v3210/Revision()
+ if(!warned_revison)
+ var/datum/tgs_version/api_version = ApiVersion()
+ 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
+ ri.origin_commit = originmastercommit
+ return ri
+
+/datum/tgs_api/v3210/EndProcess()
+ sleep(world.tick_lag) //flush the buffers
+ ExportService(SERVICE_REQUEST_KILL_PROCESS)
+
+/datum/tgs_api/v3210/ChatChannelInfo()
+ return list() // :omegalul:
+
+/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(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()
+ return TGS_SECURITY_TRUSTED
+
+#undef REBOOT_MODE_NORMAL
+#undef REBOOT_MODE_HARD
+#undef REBOOT_MODE_SHUTDOWN
+
+#undef SERVICE_WORLD_PARAM
+#undef SERVICE_INSTANCE_PARAM
+#undef SERVICE_PR_TEST_JSON
+#undef SERVICE_INTERFACE_DLL
+#undef SERVICE_INTERFACE_FUNCTION
+
+#undef SERVICE_CMD_HARD_REBOOT
+#undef SERVICE_CMD_GRACEFUL_SHUTDOWN
+#undef SERVICE_CMD_WORLD_ANNOUNCE
+#undef SERVICE_CMD_LIST_CUSTOM
+#undef SERVICE_CMD_API_COMPATIBLE
+#undef SERVICE_CMD_PLAYER_COUNT
+
+#undef SERVICE_CMD_PARAM_KEY
+#undef SERVICE_CMD_PARAM_COMMAND
+#undef SERVICE_CMD_PARAM_SENDER
+#undef SERVICE_CMD_PARAM_CUSTOM
+
+#undef SERVICE_REQUEST_KILL_PROCESS
+#undef SERVICE_REQUEST_IRC_BROADCAST
+#undef SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE
+#undef SERVICE_REQUEST_WORLD_REBOOT
+#undef SERVICE_REQUEST_API_VERSION
+
+#undef SERVICE_RETURN_SUCCESS
+
+#undef TGS_FILE2LIST
diff --git a/code/modules/tgs/v3210/commands.dm b/code/modules/tgs/v3210/commands.dm
index 50466319..6961eea4 100644
--- a/code/modules/tgs/v3210/commands.dm
+++ b/code/modules/tgs/v3210/commands.dm
@@ -1,78 +1,58 @@
-#define SERVICE_JSON_PARAM_HELPTEXT "help_text"
-#define SERVICE_JSON_PARAM_ADMINONLY "admin_only"
-#define SERVICE_JSON_PARAM_REQUIREDPARAMETERS "required_parameters"
-
-/datum/tgs_api/v3210/proc/ListServiceCustomCommands(warnings_only)
- if(!warnings_only)
- . = list()
- 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 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!")
- warned_about_the_dangers_of_robutussin = TRUE
- var/datum/tgs_chat_command/stc = I
- 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])
- TGS_ERROR_LOG("Custom command [command_name] can't be used as it is empty or contains illegal characters!")
- warned_command_names[command_name] = TRUE
- continue
-
- if(command_name_types[command_name])
- if(warnings_only)
- TGS_ERROR_LOG("Custom commands [command_name_types[command_name]] and [stc] have the same name, only [command_name_types[command_name]] will be available!")
- continue
- command_name_types[stc] = command_name
-
- if(!warnings_only)
- .[command_name] = list(SERVICE_JSON_PARAM_HELPTEXT = initial(stc.help_text), SERVICE_JSON_PARAM_ADMINONLY = initial(stc.admin_only), SERVICE_JSON_PARAM_REQUIREDPARAMETERS = 0)
-
-/datum/tgs_api/v3210/proc/HandleServiceCustomCommand(command, sender, params)
- if(!cached_custom_tgs_chat_commands)
- cached_custom_tgs_chat_commands = list()
- 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]
- if(!command_type)
- return FALSE
- var/datum/tgs_chat_command/stc = new command_type
- var/datum/tgs_chat_user/user = new
- user.friendly_name = sender
- user.mention = sender
- return stc.Run(user, params) || TRUE
-
-/*
-
-#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.
-*/
+#define SERVICE_JSON_PARAM_HELPTEXT "help_text"
+#define SERVICE_JSON_PARAM_ADMINONLY "admin_only"
+#define SERVICE_JSON_PARAM_REQUIREDPARAMETERS "required_parameters"
+
+/datum/tgs_api/v3210/proc/ListServiceCustomCommands(warnings_only)
+ if(!warnings_only)
+ . = list()
+ 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 typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command)
+ if(!warned_about_the_dangers_of_robutussin)
+ 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])
+ TGS_ERROR_LOG("Custom command [command_name] can't be used as it is empty or contains illegal characters!")
+ warned_command_names[command_name] = TRUE
+ continue
+
+ if(command_name_types[command_name])
+ if(warnings_only)
+ TGS_ERROR_LOG("Custom commands [command_name_types[command_name]] and [stc] have the same name, only [command_name_types[command_name]] will be available!")
+ continue
+ command_name_types[stc] = command_name
+
+ if(!warnings_only)
+ .[command_name] = list(SERVICE_JSON_PARAM_HELPTEXT = initial(stc.help_text), SERVICE_JSON_PARAM_ADMINONLY = initial(stc.admin_only), SERVICE_JSON_PARAM_REQUIREDPARAMETERS = 0)
+
+/datum/tgs_api/v3210/proc/HandleServiceCustomCommand(command, sender, params)
+ if(!cached_custom_tgs_chat_commands)
+ cached_custom_tgs_chat_commands = list()
+ 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]
+ if(!command_type)
+ return FALSE
+ 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
+ var/datum/tgs_message_content/result = stc.Run(user, params)
+ result = UpgradeDeprecatedCommandResponse(result, command)
+
+ return result ? result.text : TRUE
diff --git a/code/modules/tgs/v4/api.dm b/code/modules/tgs/v4/api.dm
new file mode 100644
index 00000000..6dbb9a7d
--- /dev/null
+++ b/code/modules/tgs/v4/api.dm
@@ -0,0 +1,322 @@
+#define TGS4_PARAM_INFO_JSON "tgs_json"
+
+#define TGS4_INTEROP_ACCESS_IDENTIFIER "tgs_tok"
+
+#define TGS4_RESPONSE_SUCCESS "tgs_succ"
+
+#define TGS4_TOPIC_CHANGE_PORT "tgs_port"
+#define TGS4_TOPIC_CHANGE_REBOOT_MODE "tgs_rmode"
+#define TGS4_TOPIC_CHAT_COMMAND "tgs_chat_comm"
+#define TGS4_TOPIC_EVENT "tgs_event"
+#define TGS4_TOPIC_INTEROP_RESPONSE "tgs_interop"
+
+#define TGS4_COMM_NEW_PORT "tgs_new_port"
+#define TGS4_COMM_VALIDATE "tgs_validate"
+#define TGS4_COMM_SERVER_PRIMED "tgs_prime"
+#define TGS4_COMM_WORLD_REBOOT "tgs_reboot"
+#define TGS4_COMM_END_PROCESS "tgs_kill"
+#define TGS4_COMM_CHAT "tgs_chat_send"
+
+#define TGS4_PARAMETER_COMMAND "tgs_com"
+#define TGS4_PARAMETER_DATA "tgs_data"
+
+#define TGS4_PORT_CRITFAIL_MESSAGE " Must exit to let watchdog reboot..."
+
+#define EXPORT_TIMEOUT_DS 200
+
+/datum/tgs_api/v4
+ var/access_identifier
+ var/instance_name
+ var/json_path
+ var/chat_channels_json_path
+ var/chat_commands_json_path
+ var/server_commands_json_path
+ var/reboot_mode = TGS_REBOOT_MODE_NORMAL
+ var/security_level
+
+ var/requesting_new_port = FALSE
+
+ var/list/intercepted_message_queue
+
+ var/list/custom_commands
+
+ var/list/cached_test_merges
+ var/datum/tgs_revision_information/cached_revision
+
+ var/export_lock = FALSE
+ var/list/last_interop_response
+
+/datum/tgs_api/v4/ApiVersion()
+ 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!")
+ return
+ var/json_file = file2text(json_path)
+ if(!json_file)
+ TGS_ERROR_LOG("Missing specified json file: [json_path]")
+ return
+ var/cached_json = json_decode(json_file)
+ if(!cached_json)
+ TGS_ERROR_LOG("Failed to decode info json: [json_file]")
+ return
+
+ access_identifier = cached_json["accessIdentifier"]
+ server_commands_json_path = cached_json["serverCommandsJson"]
+
+ if(cached_json["apiValidateOnly"])
+ TGS_INFO_LOG("Validating API and exiting...")
+ Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = "[minimum_required_security_level]"))
+ TerminateWorld()
+
+ security_level = cached_json["securityLevel"]
+ chat_channels_json_path = cached_json["chatChannelsJson"]
+ chat_commands_json_path = cached_json["chatCommandsJson"]
+ instance_name = cached_json["instanceName"]
+
+ ListCustomCommands()
+
+ var/list/revisionData = cached_json["revision"]
+ if(revisionData)
+ cached_revision = new
+ cached_revision.commit = revisionData["commitSha"]
+ cached_revision.origin_commit = revisionData["originCommitSha"]
+
+ cached_test_merges = list()
+ var/list/json = cached_json["testMerges"]
+ for(var/entry in json)
+ var/datum/tgs_revision_information/test_merge/tm = new
+ tm.timestamp = text2num(entry["timeMerged"])
+
+ var/list/revInfo = entry["revision"]
+ if(revInfo)
+ tm.commit = revInfo["commitSha"]
+ tm.origin_commit = revInfo["originCommitSha"]
+
+ tm.title = entry["titleAtMerge"]
+ tm.body = entry["bodyAtMerge"]
+ tm.url = entry["url"]
+ tm.author = entry["author"]
+ tm.number = entry["number"]
+ tm.head_commit = entry["pullRequestRevision"]
+ tm.comment = entry["comment"]
+
+ cached_test_merges += tm
+
+ return TRUE
+
+/datum/tgs_api/v4/OnInitializationComplete()
+ Export(TGS4_COMM_SERVER_PRIMED)
+
+/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
+
+ if(their_sCK != access_identifier)
+ return "Invalid comms key!";
+
+ var/command = params[TGS4_PARAMETER_COMMAND]
+ if(!command)
+ return "No command!"
+
+ . = TGS4_RESPONSE_SUCCESS
+
+ switch(command)
+ if(TGS4_TOPIC_CHAT_COMMAND)
+ var/result = HandleCustomCommand(params[TGS4_PARAMETER_DATA])
+ if(result == null)
+ result = "Error running chat command!"
+ return result
+ if(TGS4_TOPIC_EVENT)
+ intercepted_message_queue = list()
+ var/list/event_notification = json_decode(params[TGS4_PARAMETER_DATA])
+ var/list/event_parameters = event_notification["Parameters"]
+
+ var/list/event_call = list(event_notification["Type"])
+ if(event_parameters)
+ event_call += event_parameters
+
+ if(event_handler != null)
+ event_handler.HandleEvent(arglist(event_call))
+
+ . = json_encode(intercepted_message_queue)
+ intercepted_message_queue = null
+ return
+ if(TGS4_TOPIC_INTEROP_RESPONSE)
+ last_interop_response = json_decode(params[TGS4_PARAMETER_DATA])
+ return
+ if(TGS4_TOPIC_CHANGE_PORT)
+ var/new_port = text2num(params[TGS4_PARAMETER_DATA])
+ if (!(new_port > 0))
+ return "Invalid port: [new_port]"
+
+ //the topic still completes, miraculously
+ //I honestly didn't believe byond could do it
+ if(event_handler != null)
+ event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)
+ if(!world.OpenPort(new_port))
+ return "Port change failed!"
+ return
+ if(TGS4_TOPIC_CHANGE_REBOOT_MODE)
+ var/new_reboot_mode = text2num(params[TGS4_PARAMETER_DATA])
+ if(event_handler != null)
+ event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode)
+ reboot_mode = new_reboot_mode
+ return
+
+ return "Unknown command: [command]"
+
+/datum/tgs_api/v4/proc/Export(command, list/data, override_requesting_new_port = FALSE)
+ if(!data)
+ data = list()
+ data[TGS4_PARAMETER_COMMAND] = command
+ var/json = json_encode(data)
+
+ while(requesting_new_port && !override_requesting_new_port)
+ sleep(1)
+
+ //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]")
+ 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
+
+ if(!new_port_json)
+ TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]")
+ 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]")
+ TerminateWorld()
+
+ if(new_port != world.port && !world.OpenPort(new_port))
+ TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]")
+ TerminateWorld()
+ requesting_new_port = FALSE
+
+ while(export_lock)
+ sleep(1)
+ export_lock = TRUE
+
+ last_interop_response = null
+ fdel(server_commands_json_path)
+ text2file(json, server_commands_json_path)
+
+ for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I)
+ sleep(1)
+
+ if(!last_interop_response)
+ TGS_ERROR_LOG("Failed to get export result for: [json]")
+ else
+ . = last_interop_response
+
+ export_lock = FALSE
+
+/datum/tgs_api/v4/OnReboot()
+ var/list/result = Export(TGS4_COMM_WORLD_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
+
+ var/port = result[TGS4_PARAMETER_DATA]
+ if(!isnum(port))
+ 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
+ port = "none"
+
+ if(!world.OpenPort(port))
+ TGS_ERROR_LOG("Unable to set port to [port]!")
+
+/datum/tgs_api/v4/InstanceName()
+ return instance_name
+
+/datum/tgs_api/v4/TestMerges()
+ return cached_test_merges.Copy()
+
+/datum/tgs_api/v4/EndProcess()
+ Export(TGS4_COMM_END_PROCESS)
+
+/datum/tgs_api/v4/Revision()
+ return cached_revision
+
+/datum/tgs_api/v4/ChatBroadcast(datum/tgs_message_content/message, list/channels)
+ var/list/ids
+ if(length(channels))
+ ids = list()
+ for(var/I in channels)
+ var/datum/tgs_chat_channel/channel = I
+ ids += channel.id
+
+ 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(datum/tgs_message_content/message, admin_only)
+ var/list/channels = list()
+ 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 = 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(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
+ Export(TGS4_COMM_CHAT, message)
+
+/datum/tgs_api/v4/ChatChannelInfo()
+ . = list()
+ //no caching cause tgs may change this
+ var/list/json = json_decode(file2text(chat_channels_json_path))
+ for(var/I in json)
+ . += DecodeChannel(I)
+
+/datum/tgs_api/v4/proc/DecodeChannel(channel_json)
+ var/datum/tgs_chat_channel/channel = new
+ channel.id = channel_json["id"]
+ channel.friendly_name = channel_json["friendlyName"]
+ channel.connection_name = channel_json["connectionName"]
+ channel.is_admin_channel = channel_json["isAdminChannel"]
+ channel.is_private_channel = channel_json["isPrivateChannel"]
+ channel.custom_tag = channel_json["tag"]
+ return channel
+
+/datum/tgs_api/v4/SecurityLevel()
+ return security_level
diff --git a/code/modules/tgs/v4/commands.dm b/code/modules/tgs/v4/commands.dm
new file mode 100644
index 00000000..e9ff27d4
--- /dev/null
+++ b/code/modules/tgs/v4/commands.dm
@@ -0,0 +1,44 @@
+/datum/tgs_api/v4/proc/ListCustomCommands()
+ var/results = list()
+ custom_commands = list()
+ 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!")
+ continue
+
+ if(results[command_name])
+ var/datum/other = custom_commands[command_name]
+ 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("name" = command_name, "help_text" = stc.help_text, "admin_only" = stc.admin_only))
+ custom_commands[command_name] = stc
+
+ var/commands_file = chat_commands_json_path
+ if(!commands_file)
+ return
+ text2file(json_encode(results), commands_file)
+
+/datum/tgs_api/v4/proc/HandleCustomCommand(command_json)
+ var/list/data = json_decode(command_json)
+ var/command = data["command"]
+ var/user = data["user"]
+ var/params = data["params"]
+
+ var/datum/tgs_chat_user/u = new
+ u.id = user["id"]
+ u.friendly_name = user["friendlyName"]
+ u.mention = user["mention"]
+ u.channel = DecodeChannel(user["channel"])
+
+ var/datum/tgs_chat_command/sc = custom_commands[command]
+ if(sc)
+ var/datum/tgs_message_content/result = sc.Run(u, params)
+ result = UpgradeDeprecatedCommandResponse(result, command)
+
+ return result ? result.text : TRUE
+ return "Unknown command: [command]!"
diff --git a/code/modules/tgs/v5/__interop_version.dm b/code/modules/tgs/v5/__interop_version.dm
new file mode 100644
index 00000000..cafe74bb
--- /dev/null
+++ b/code/modules/tgs/v5/__interop_version.dm
@@ -0,0 +1 @@
+"5.8.0"
diff --git a/code/modules/tgs/v5/_defines.dm b/code/modules/tgs/v5/_defines.dm
new file mode 100644
index 00000000..de8b1389
--- /dev/null
+++ b/code/modules/tgs/v5/_defines.dm
@@ -0,0 +1,118 @@
+#define DMAPI5_PARAM_SERVER_PORT "tgs_port"
+#define DMAPI5_PARAM_ACCESS_IDENTIFIER "tgs_key"
+
+#define DMAPI5_BRIDGE_DATA "data"
+#define DMAPI5_TOPIC_DATA "tgs_data"
+
+#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_CHUNK 6
+
+#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_MINIMUM_SECURITY_LEVEL "minimumSecurityLevel"
+
+#define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort"
+#define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation"
+
+#define DMAPI5_CHAT_MESSAGE_CHANNEL_IDS "channelIds"
+
+#define DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER "accessIdentifier"
+#define DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION "serverVersion"
+#define DMAPI5_RUNTIME_INFORMATION_SERVER_PORT "serverPort"
+#define DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY "apiValidateOnly"
+#define DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME "instanceName"
+#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"
+
+#define DMAPI5_TEST_MERGE_TIME_MERGED "timeMerged"
+#define DMAPI5_TEST_MERGE_REVISION "revision"
+#define DMAPI5_TEST_MERGE_TITLE_AT_MERGE "titleAtMerge"
+#define DMAPI5_TEST_MERGE_BODY_AT_MERGE "bodyAtMerge"
+#define DMAPI5_TEST_MERGE_URL "url"
+#define DMAPI5_TEST_MERGE_AUTHOR "author"
+#define DMAPI5_TEST_MERGE_NUMBER "number"
+#define DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION "pullRequestRevision"
+#define DMAPI5_TEST_MERGE_COMMENT "comment"
+
+#define DMAPI5_CHAT_COMMAND_NAME "name"
+#define DMAPI5_CHAT_COMMAND_PARAMS "params"
+#define DMAPI5_CHAT_COMMAND_USER "user"
+
+#define DMAPI5_EVENT_NOTIFICATION_TYPE "type"
+#define DMAPI5_EVENT_NOTIFICATION_PARAMETERS "parameters"
+
+#define DMAPI5_TOPIC_COMMAND_CHAT_COMMAND 0
+#define DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION 1
+#define DMAPI5_TOPIC_COMMAND_CHANGE_PORT 2
+#define DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE 3
+#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_HEALTHCHECK 7
+#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8
+#define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9
+#define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10
+#define DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST 11
+
+#define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType"
+#define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand"
+#define DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION "eventNotification"
+#define DMAPI5_TOPIC_PARAMETER_NEW_PORT "newPort"
+#define DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE "newRebootState"
+#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_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"
+#define DMAPI5_CHAT_USER_FRIENDLY_NAME "friendlyName"
+#define DMAPI5_CHAT_USER_MENTION "mention"
+#define DMAPI5_CHAT_USER_CHANNEL "channel"
+
+#define DMAPI5_CHAT_CHANNEL_ID "id"
+#define DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME "friendlyName"
+#define DMAPI5_CHAT_CHANNEL_CONNECTION_NAME "connectionName"
+#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"
diff --git a/code/modules/tgs/v5/api.dm b/code/modules/tgs/v5/api.dm
new file mode 100644
index 00000000..25e00aed
--- /dev/null
+++ b/code/modules/tgs/v5/api.dm
@@ -0,0 +1,281 @@
+/datum/tgs_api/v5
+ var/server_port
+ var/access_identifier
+
+ 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/detached = FALSE
+
+/datum/tgs_api/v5/New()
+ . = ..()
+ interop_version = version
+ TGS_DEBUG_LOG("V5 API created: [json_encode(args)]")
+
+/datum/tgs_api/v5/ApiVersion()
+ 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 // we want this to be the TGS version, not the interop version
+ 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
+
+ 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
+
+ if(runtime_information[DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY])
+ TGS_INFO_LOG("DMAPI validation, exiting...")
+ TerminateWorld()
+
+ 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!")
+
+ test_merges = list()
+ var/list/test_merge_json = runtime_information[DMAPI5_RUNTIME_INFORMATION_TEST_MERGES]
+ if(istype(test_merge_json))
+ for(var/entry in test_merge_json)
+ var/datum/tgs_revision_information/test_merge/tm = new
+ tm.number = entry[DMAPI5_TEST_MERGE_NUMBER]
+
+ var/list/revInfo = entry[DMAPI5_TEST_MERGE_REVISION]
+ if(revInfo)
+ tm.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA]
+ tm.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA]
+ tm.timestamp = entry[DMAPI5_REVISION_INFORMATION_TIMESTAMP]
+ else
+ TGS_WARNING_LOG("Failed to decode [DMAPI5_TEST_MERGE_REVISION] from test merge #[tm.number]!")
+
+ 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.head_commit = entry[DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION]
+ tm.comment = entry[DMAPI5_TEST_MERGE_COMMENT]
+
+ test_merges += tm
+ else
+ TGS_WARNING_LOG("Failed to decode [DMAPI5_RUNTIME_INFORMATION_TEST_MERGES] from runtime information!")
+
+ 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()
+ TGS_DEBUG_LOG("RequireInitialBridgeResponse()")
+ var/logged = FALSE
+ while(!initial_bridge_request_received)
+ if(!logged)
+ TGS_DEBUG_LOG("RequireInitialBridgeResponse: Starting sleep")
+ logged = TRUE
+
+ sleep(1)
+
+ TGS_DEBUG_LOG("RequireInitialBridgeResponse: Passed")
+
+/datum/tgs_api/v5/OnInitializationComplete()
+ Bridge(DMAPI5_BRIDGE_COMMAND_PRIME)
+
+/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)
+ TGS_DEBUG_LOG("No \"[DMAPI5_TOPIC_DATA]\" entry found, ignoring...")
+ return FALSE // continue to /world/Topic
+
+ 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
+
+ return ProcessTopicJson(json, TRUE)
+
+/datum/tgs_api/v5/OnReboot()
+ var/list/result = Bridge(DMAPI5_BRIDGE_COMMAND_REBOOT)
+ if(!result)
+ return
+
+ //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
+
+ if(port == 0)
+ //to byond 0 means any port and "none" means close vOv
+ port = "none"
+
+ if(!world.OpenPort(port))
+ TGS_ERROR_LOG("Unable to set port to [port]!")
+
+/datum/tgs_api/v5/InstanceName()
+ RequireInitialBridgeResponse()
+ return instance_name
+
+/datum/tgs_api/v5/TestMerges()
+ RequireInitialBridgeResponse()
+ return test_merges.Copy()
+
+/datum/tgs_api/v5/EndProcess()
+ Bridge(DMAPI5_BRIDGE_COMMAND_KILL)
+
+/datum/tgs_api/v5/Revision()
+ RequireInitialBridgeResponse()
+ return revision
+
+// 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/I in channels)
+ var/datum/tgs_chat_channel/channel = I
+ ids += channel.id
+
+ SendChatMessageRaw(message2, ids)
+
+/datum/tgs_api/v5/ChatTargetedBroadcast(datum/tgs_message_content/message2, admin_only)
+ var/list/channels = list()
+ 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
+
+ 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(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
+ 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()
+ WaitForReattach(TRUE)
+ return chat_channels.Copy()
+
+/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()
+ for(var/channel_json in chat_channels_json)
+ var/datum/tgs_chat_channel/channel = DecodeChannel(channel_json)
+ if(channel)
+ chat_channels += channel
+ else
+ TGS_WARNING_LOG("Failed to decode [DMAPI5_CHAT_UPDATE_CHANNELS] from channel update!")
+
+/datum/tgs_api/v5/proc/DecodeChannel(channel_json)
+ var/datum/tgs_chat_channel/channel = new
+ channel.id = channel_json[DMAPI5_CHAT_CHANNEL_ID]
+ channel.friendly_name = channel_json[DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME]
+ channel.connection_name = channel_json[DMAPI5_CHAT_CHANNEL_CONNECTION_NAME]
+ 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
+
+/datum/tgs_api/v5/Visibility()
+ RequireInitialBridgeResponse()
+ return visibility
diff --git a/code/modules/tgs/v5/bridge.dm b/code/modules/tgs/v5/bridge.dm
new file mode 100644
index 00000000..a261f8a0
--- /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(1)
+ 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)
+
+ // This is an infinite sleep until we get a response
+ var/export_response = world.Export(bridge_request)
+ if(!export_response)
+ TGS_ERROR_LOG("Failed bridge request: [bridge_request]")
+ return
+
+ var/content = export_response["CONTENT"]
+ if(!content)
+ TGS_ERROR_LOG("Failed bridge request, missing content!")
+ return
+
+ var/response_json = file2text(content)
+ if(!response_json)
+ TGS_ERROR_LOG("Failed bridge request, failed to load content!")
+ return
+
+ var/list/bridge_response = json_decode(response_json)
+ if(!bridge_response)
+ TGS_ERROR_LOG("Failed bridge request, bad json: [response_json]")
+ 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 00000000..167a124b
--- /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
new file mode 100644
index 00000000..c3ecf825
--- /dev/null
+++ b/code/modules/tgs/v5/commands.dm
@@ -0,0 +1,60 @@
+/datum/tgs_api/v5/proc/ListCustomCommands()
+ var/results = list()
+ custom_commands = list()
+ 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!")
+ continue
+
+ if(results[command_name])
+ var/datum/other = custom_commands[command_name]
+ 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
+
+ return results
+
+/datum/tgs_api/v5/proc/HandleCustomCommand(list/command_json)
+ var/command = command_json[DMAPI5_CHAT_COMMAND_NAME]
+ var/user = command_json[DMAPI5_CHAT_COMMAND_USER]
+ var/params = command_json[DMAPI5_CHAT_COMMAND_PARAMS]
+
+ var/datum/tgs_chat_user/u = new
+ u.id = user[DMAPI5_CHAT_USER_ID]
+ u.friendly_name = user[DMAPI5_CHAT_USER_FRIENDLY_NAME]
+ u.mention = user[DMAPI5_CHAT_USER_MENTION]
+ u.channel = DecodeChannel(user[DMAPI5_CHAT_USER_CHANNEL])
+
+ var/datum/tgs_chat_command/sc = custom_commands[command]
+ if(sc)
+ 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
+
+ return new /datum/tgs_message_content(response)
+
+ if(!istype(response))
+ TGS_ERROR_LOG("Custom chat command \"[command]\" should return a [/datum/tgs_message_content]! Got: \"[response]\"")
+ return null
+
+ return response
diff --git a/code/modules/tgs/v5/serializers.dm b/code/modules/tgs/v5/serializers.dm
new file mode 100644
index 00000000..6782bc96
--- /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 00000000..0a81adac
--- /dev/null
+++ b/code/modules/tgs/v5/topic.dm
@@ -0,0 +1,283 @@
+/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)
+ return TopicResponse()
+
+ 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()
+ 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()
+
+ 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/undefs.dm b/code/modules/tgs/v5/undefs.dm
new file mode 100644
index 00000000..94dcb2e4
--- /dev/null
+++ b/code/modules/tgs/v5/undefs.dm
@@ -0,0 +1,118 @@
+#undef DMAPI5_PARAM_SERVER_PORT
+#undef DMAPI5_PARAM_ACCESS_IDENTIFIER
+
+#undef DMAPI5_BRIDGE_DATA
+#undef DMAPI5_TOPIC_DATA
+
+#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_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_MINIMUM_SECURITY_LEVEL
+
+#undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT
+#undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION
+
+#undef DMAPI5_CHAT_MESSAGE_CHANNEL_IDS
+
+#undef DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER
+#undef DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION
+#undef DMAPI5_RUNTIME_INFORMATION_SERVER_PORT
+#undef DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY
+#undef DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME
+#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
+
+#undef DMAPI5_TEST_MERGE_TIME_MERGED
+#undef DMAPI5_TEST_MERGE_REVISION
+#undef DMAPI5_TEST_MERGE_TITLE_AT_MERGE
+#undef DMAPI5_TEST_MERGE_BODY_AT_MERGE
+#undef DMAPI5_TEST_MERGE_URL
+#undef DMAPI5_TEST_MERGE_AUTHOR
+#undef DMAPI5_TEST_MERGE_NUMBER
+#undef DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION
+#undef DMAPI5_TEST_MERGE_COMMENT
+
+#undef DMAPI5_CHAT_COMMAND_NAME
+#undef DMAPI5_CHAT_COMMAND_PARAMS
+#undef DMAPI5_CHAT_COMMAND_USER
+
+#undef DMAPI5_EVENT_NOTIFICATION_TYPE
+#undef DMAPI5_EVENT_NOTIFICATION_PARAMETERS
+
+#undef DMAPI5_TOPIC_COMMAND_CHAT_COMMAND
+#undef DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION
+#undef DMAPI5_TOPIC_COMMAND_CHANGE_PORT
+#undef DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE
+#undef DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED
+#undef DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE
+#undef DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE
+#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_PARAMETER_COMMAND_TYPE
+#undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND
+#undef DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION
+#undef DMAPI5_TOPIC_PARAMETER_NEW_PORT
+#undef DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE
+#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_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
+#undef DMAPI5_CHAT_USER_FRIENDLY_NAME
+#undef DMAPI5_CHAT_USER_MENTION
+#undef DMAPI5_CHAT_USER_CHANNEL
+
+#undef DMAPI5_CHAT_CHANNEL_ID
+#undef DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME
+#undef DMAPI5_CHAT_CHANNEL_CONNECTION_NAME
+#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
From 79948448d18d47e6e7d5d5d46453f93e2c975827 Mon Sep 17 00:00:00 2001
From: Aeris Bennet <156572979+vgcataeris@users.noreply.github.com>
Date: Tue, 6 Feb 2024 20:07:02 +0000
Subject: [PATCH 2/7] Updating the actual TGS file.
---
code/__DEFINES/tgs.dm | 760 ++++++++++++++++++++++++++++--------------
1 file changed, 518 insertions(+), 242 deletions(-)
diff --git a/code/__DEFINES/tgs.dm b/code/__DEFINES/tgs.dm
index db4f046e..bdd66a28 100644
--- a/code/__DEFINES/tgs.dm
+++ b/code/__DEFINES/tgs.dm
@@ -1,242 +1,518 @@
-//tgstation-server DMAPI
-
-//All functions and datums outside this document are subject to change with any version and should not be relied on
-
-//CONFIGURATION
-
-//create this define if you want to do configuration outside of this file
-#ifndef TGS_EXTERNAL_CONFIGURATION
-
-//Comment this out once you've filled in the below
-#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()()
-//#define TGS_V3_API
-
-//Required interfaces (fill in with your codebase equivalent):
-
-//create a global variable named `Name` and set it to `Value`
-//These globals must not be modifiable from anywhere outside of the server tools
-#define TGS_DEFINE_AND_SET_GLOBAL(Name, Value)
-
-//Read the value in the global variable `Name`
-#define TGS_READ_GLOBAL(Name)
-
-//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 priveledge escalation
-#define TGS_PROTECT_DATUM(Path)
-
-//display an announcement `message` from the server to all players
-#define TGS_WORLD_ANNOUNCE(message)
-
-//Notify current in-game administrators of a string `event`
-#define TGS_NOTIFY_ADMINS(event)
-
-//Write an info `message` to a server log
-#define TGS_INFO_LOG(message)
-
-//Write an error `message` to a server log
-#define TGS_ERROR_LOG(message)
-
-//Get the number of connected /clients
-#define TGS_CLIENT_COUNT
-
-#endif
-
-//EVENT CODES
-
-#define TGS_EVENT_PORT_SWAP -2 //before a port change is about to happen, extra parameter is new port
-#define TGS_EVENT_REBOOT_MODE_CHANGE -1 //before a reboot mode change, extras parameters are the current and new reboot mode enums
-
-//See the descriptions for these codes here: https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Host/Components/EventType.cs
-#define TGS_EVENT_REPO_RESET_ORIGIN 0
-#define TGS_EVENT_REPO_CHECKOUT 1
-#define TGS_EVENT_REPO_FETCH 2
-#define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3
-#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
-#define TGS_EVENT_COMPILE_START 8
-#define TGS_EVENT_COMPILE_CANCELLED 9
-#define TGS_EVENT_COMPILE_FAILURE 10
-#define TGS_EVENT_COMPILE_COMPLETE 11
-#define TGS_EVENT_INSTANCE_AUTO_UPDATE_START 12
-#define TGS_EVENT_REPO_MERGE_CONFLICT 13
-
-//OTHER ENUMS
-
-#define TGS_REBOOT_MODE_NORMAL 0
-#define TGS_REBOOT_MODE_SHUTDOWN 1
-#define TGS_REBOOT_MODE_RESTART 2
-
-#define TGS_SECURITY_TRUSTED 0
-#define TGS_SECURITY_SAFE 1
-#define TGS_SECURITY_ULTRASAFE 2
-
-//REQUIRED HOOKS
-
-//Call this somewhere in /world/New() that is always run
-//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)
- 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()
-/world/proc/TgsInitializationComplete()
- return
-
-//Put this at the start of /world/Topic()
-#define TGS_TOPIC var/tgs_topic_return = TgsTopic(args[1]); if(tgs_topic_return) return tgs_topic_return
-
-//Call this at the beginning of world/Reboot(reason)
-/world/proc/TgsReboot()
- return
-
-//DATUM DEFINITIONS
-//unless otherwise specified all datums defined here should be considered read-only, warranty void if written
-
-//represents git revision information about the current world build
-/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
-
-//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
-
-//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
-
-//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
-
-//user definable callback for handling events
-//extra parameters may be specified depending on the event
-/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
-
-//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
-/datum/tgs_chat_command/proc/Run(datum/tgs_chat_user/sender, params)
- CRASH("[type] has no implementation for Run()")
-
-//FUNCTIONS
-
-//Returns the respective string version of the API
-/world/proc/TgsMaximumAPIVersion()
- return
-
-/world/proc/TgsMinimumAPIVersion()
- return
-
-//Gets the current version of the server tools running the server
-/world/proc/TgsVersion()
- 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
-/world/proc/TgsAvailable()
- return
-
-/world/proc/TgsInstanceName()
- return
-
-//Get the current `/datum/tgs_revision_information`
-/world/proc/TgsRevision()
- return
-
-//Get the current BYOND security level
-/world/proc/TgsSecurityLevel()
- return
-
-//Gets a list of active `/datum/tgs_revision_information/test_merge`s
-/world/proc/TgsTestMerges()
- 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
-/world/proc/TgsEndProcess()
- return
-
-//Gets a list of connected tgs_chat_channel
-/world/proc/TgsChatChannelInfo()
- return
-
-//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)
- 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)
- 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)
- return
-
-/*
-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.
-*/
\ No newline at end of file
+// tgstation-server DMAPI
+
+#define TGS_DMAPI_VERSION "7.0.2"
+
+// All functions and datums outside this document are subject to change with any version and should not be relied on.
+
+// CONFIGURATION
+
+/// 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.
+#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()().
+//#define TGS_V3_API
+
+// Required interfaces (fill in with your codebase equivalent):
+
+/// 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`.
+#define TGS_READ_GLOBAL(Name)
+
+/// 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.
+#define TGS_PROTECT_DATUM(Path)
+
+/// Display an announcement `message` from the server to all players.
+#define TGS_WORLD_ANNOUNCE(message)
+
+/// Notify current in-game administrators of a string `event`.
+#define TGS_NOTIFY_ADMINS(event)
+
+/// Write an info `message` to a server log.
+#define TGS_INFO_LOG(message)
+
+/// Write an warning `message` to a server log.
+#define TGS_WARNING_LOG(message)
+
+/// Write an error `message` to a server log.
+#define TGS_ERROR_LOG(message)
+
+/// Get the number of connected /clients.
+#define TGS_CLIENT_COUNT
+
+#endif
+
+// 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
+/// 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
+/// 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
+
+/// 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/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].
+ */
+/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE)
+ return
+
+/**
+ * Call this when your initializations are complete and your 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()
+ return
+
+/// Put this 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
+
+/**
+ * Call this as late as possible in [world/proc/Reboot] (BEFORE ..()).
+ */
+/world/proc/TgsReboot()
+ return
+
+// DATUM DEFINITIONS
+// All datums defined here should be considered read-only
+
+/// Represents git revision information.
+/datum/tgs_revision_information
+ /// 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.
+/datum/tgs_version
+ /// The suite/major version number.
+ var/suite
+
+ // 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
+
+ /// Legacy version number. Generally null.
+ var/deprecated_patch
+
+ /// 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()
+ return
+
+/**
+ * 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)
+ return
+
+/// Represents a merge of a GitHub pull request.
+/datum/tgs_revision_information/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.
+/datum/tgs_chat_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
+/datum/tgs_chat_user
+ /// 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 handler for TGS events.
+/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
+ /// 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.
+ *
+ * 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()")
+
+/// User definable chat message.
+/datum/tgs_message_content
+ /// The tring content of the message. Must be provided in New().
+ var/text
+
+ /// The [/datum/tgs_chat_embed] to embed in the message. Not supported on all chat providers.
+ var/datum/tgs_chat_embed/structure/embed
+
+/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/channel#embed-object-embed-structure 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/channel#embed-object-embed-image-structure for details.
+ var/datum/tgs_chat_embed/media/image
+
+ /// See https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure for details.
+ var/datum/tgs_chat_embed/media/thumbnail
+
+ /// See https://discord.com/developers/docs/resources/channel#embed-object-embed-image-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
+
+/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/channel#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
+
+/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/channel#embed-object-embed-provider-structure for details.
+/datum/tgs_chat_embed/provider
+ var/name
+ var/url
+
+/// See https://discord.com/developers/docs/resources/channel#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
+
+/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/channel#embed-object-embed-field-structure for details. Must have name and value set in New().
+/datum/tgs_chat_embed/field
+ var/name
+ var/value
+ var/is_inline
+
+/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()
+ return
+
+/// Returns the minimum supported [/datum/tgs_version] of the DMAPI.
+/world/proc/TgsMinimumApiVersion()
+ return
+
+/**
+ * Returns [TRUE] if DreamDaemon was launched under TGS, the API matches, and was properly initialized. [FALSE] will be returned otherwise.
+ */
+/world/proc/TgsAvailable()
+ return
+
+// 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()
+ return
+
+/**
+ * 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)
+ return
+
+/**
+ * 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)
+ return
+
+/**
+ * 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)
+ return
+
+/// 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()
+ return
+
+/// Returns the running engine type
+/world/proc/TgsEngine()
+ 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()
+ return
+
+/// 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()
+ return
+
+/// 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()
+ return
+
+/// 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()
+ return
+
+/// 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()
+ 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()
+ return
+
+/// 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()
+ return
+
+/*
+The MIT License
+
+Copyright (c) 2017-2023 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.
+*/
From 7478a88d9f76115f948b5db64236720febbd0875 Mon Sep 17 00:00:00 2001
From: Aeris Bennet <156572979+vgcataeris@users.noreply.github.com>
Date: Tue, 6 Feb 2024 20:09:12 +0000
Subject: [PATCH 3/7] Upadting the TGS config to be in line with new updates
---
code/__DEFINES/tgs.config.dm | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/code/__DEFINES/tgs.config.dm b/code/__DEFINES/tgs.config.dm
index a40b5d46..af30d00a 100644
--- a/code/__DEFINES/tgs.config.dm
+++ b/code/__DEFINES/tgs.config.dm
@@ -1,10 +1,14 @@
#define TGS_EXTERNAL_CONFIGURATION
+#define TGS_V3_API
#define TGS_DEFINE_AND_SET_GLOBAL(Name, Value) GLOBAL_VAR_INIT(##Name, ##Value); GLOBAL_PROTECT(##Name)
#define TGS_READ_GLOBAL(Name) GLOB.##Name
#define TGS_WRITE_GLOBAL(Name, Value) GLOB.##Name = ##Value
#define TGS_WORLD_ANNOUNCE(message) to_chat(world, "[html_encode(##message)]")
#define TGS_INFO_LOG(message) log_world("TGS: Info: [##message]")
#define TGS_ERROR_LOG(message) log_world("TGS: Error: [##message]")
+#define TGS_INFO_LOG(message) log_world("TGS Info: [##message]")
+#define TGS_WARNING_LOG(message) log_world("TGS Warn: [##message]")
+#define TGS_ERROR_LOG(message) stack_trace("TGS Error: [##message]")
#define TGS_NOTIFY_ADMINS(event) message_admins(##event)
#define TGS_CLIENT_COUNT GLOB.clients.len
#define TGS_PROTECT_DATUM(Path) GENERAL_PROTECT_DATUM(##Path)
From 8d61277d218c346fe70b906b07dcec03e8afc9ba Mon Sep 17 00:00:00 2001
From: Aeris Bennet <156572979+vgcataeris@users.noreply.github.com>
Date: Tue, 6 Feb 2024 20:09:56 +0000
Subject: [PATCH 4/7] Wait.. why the fuck did it duplicate two lines?
---
code/__DEFINES/tgs.config.dm | 2 --
1 file changed, 2 deletions(-)
diff --git a/code/__DEFINES/tgs.config.dm b/code/__DEFINES/tgs.config.dm
index af30d00a..3a3727fb 100644
--- a/code/__DEFINES/tgs.config.dm
+++ b/code/__DEFINES/tgs.config.dm
@@ -4,8 +4,6 @@
#define TGS_READ_GLOBAL(Name) GLOB.##Name
#define TGS_WRITE_GLOBAL(Name, Value) GLOB.##Name = ##Value
#define TGS_WORLD_ANNOUNCE(message) to_chat(world, "[html_encode(##message)]")
-#define TGS_INFO_LOG(message) log_world("TGS: Info: [##message]")
-#define TGS_ERROR_LOG(message) log_world("TGS: Error: [##message]")
#define TGS_INFO_LOG(message) log_world("TGS Info: [##message]")
#define TGS_WARNING_LOG(message) log_world("TGS Warn: [##message]")
#define TGS_ERROR_LOG(message) stack_trace("TGS Error: [##message]")
From 78f79b8aa317baa0447850b1ebbe48865135e0f2 Mon Sep 17 00:00:00 2001
From: Aeris Bennet <156572979+vgcataeris@users.noreply.github.com>
Date: Tue, 6 Feb 2024 20:16:09 +0000
Subject: [PATCH 5/7] Patched the getrev file for compiling. This should be go
time!
---
code/datums/helper_datums/getrev.dm | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/code/datums/helper_datums/getrev.dm b/code/datums/helper_datums/getrev.dm
index 7d040a49..ca5ea28e 100644
--- a/code/datums/helper_datums/getrev.dm
+++ b/code/datums/helper_datums/getrev.dm
@@ -42,10 +42,8 @@
. = 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.pull_request_commit
+ 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]
"
/client/verb/showrevinfo()
@@ -119,4 +117,4 @@
if(probabilities[ctag] > 0)
var/percentage = round(probabilities[ctag] / sum * 100, 0.1)
msg += "[ctag] [percentage]%"
- to_chat(src, msg.Join("
"))
\ No newline at end of file
+ to_chat(src, msg.Join("
"))
From eec77a36fd31783a61c917b734f24b0b39dfe48e Mon Sep 17 00:00:00 2001
From: Aeris Bennet <156572979+vgcataeris@users.noreply.github.com>
Date: Tue, 6 Feb 2024 20:21:05 +0000
Subject: [PATCH 6/7] Time to add the config file to make the magic happem!
---
tgs.yml | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 tgs.yml
diff --git a/tgs.yml b/tgs.yml
new file mode 100644
index 00000000..8c837dc0
--- /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: "515.1630"
+# 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
From 8b469bc4e88cfb40f3198554e17538968a5f71eb Mon Sep 17 00:00:00 2001
From: Aeris Bennet <156572979+vgcataeris@users.noreply.github.com>
Date: Tue, 6 Feb 2024 20:22:08 +0000
Subject: [PATCH 7/7] Renaming the file and changing the version to current
stable.
515 won't compile this codebase. Ask me how I know this.
---
tgs.yml => .tgs.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename tgs.yml => .tgs.yml (96%)
diff --git a/tgs.yml b/.tgs.yml
similarity index 96%
rename from tgs.yml
rename to .tgs.yml
index 8c837dc0..90cf9730 100644
--- a/tgs.yml
+++ b/.tgs.yml
@@ -3,7 +3,7 @@
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: "515.1630"
+byond: "514.1589"
# Folders to create in "/Configuration/GameStaticFiles/"
static_files:
# Config directory should be static