From 9ccb9bddc4efd91f99c31a5242e860d19f931c86 Mon Sep 17 00:00:00 2001
From: AffectedArc07 <25063394+AffectedArc07@users.noreply.github.com>
Date: Sat, 17 Jun 2023 11:00:01 +0100
Subject: [PATCH] 2-way server heartbeats (plus bonus DMAPI update) (#21101)
* 2-way server heartbeats
* Upgrades people, upgrades
* DMAPI bump again
* How did this compile
* BALLS
---
code/__DEFINES/tgs.dm | 236 ++++++++++++++-----
code/controllers/subsystem/SSheartbeat.dm | 20 ++
code/datums/tgs_event_handler.dm | 3 +
code/modules/tgs/LICENSE | 2 +-
code/modules/tgs/core/tgs_datum.dm | 2 +
code/modules/tgs/includes.dm | 4 +
code/modules/tgs/v3210/README.md | 4 +-
code/modules/tgs/v4/README.md | 4 +-
code/modules/tgs/v4/v4_api.dm | 25 ++-
code/modules/tgs/v4/v4_commands.dm | 11 +-
code/modules/tgs/v5/README.md | 13 +-
code/modules/tgs/v5/_v5_defines.dm | 20 +-
code/modules/tgs/v5/v5_api.dm | 202 ++++-------------
code/modules/tgs/v5/v5_bridge.dm | 99 ++++++++
code/modules/tgs/v5/v5_chunking.dm | 43 ++++
code/modules/tgs/v5/v5_commands.dm | 38 +++-
code/modules/tgs/v5/v5_interop_version.dm | 2 +-
code/modules/tgs/v5/v5_serializers.dm | 59 +++++
code/modules/tgs/v5/v5_topic.dm | 261 ++++++++++++++++++++++
code/modules/tgs/v5/v5_undefs.dm | 18 +-
paradise.dme | 1 +
21 files changed, 811 insertions(+), 256 deletions(-)
create mode 100644 code/controllers/subsystem/SSheartbeat.dm
create mode 100644 code/modules/tgs/v5/v5_bridge.dm
create mode 100644 code/modules/tgs/v5/v5_chunking.dm
create mode 100644 code/modules/tgs/v5/v5_serializers.dm
create mode 100644 code/modules/tgs/v5/v5_topic.dm
diff --git a/code/__DEFINES/tgs.dm b/code/__DEFINES/tgs.dm
index 83e06658c57..89976c49842 100644
--- a/code/__DEFINES/tgs.dm
+++ b/code/__DEFINES/tgs.dm
@@ -1,6 +1,6 @@
// tgstation-server DMAPI
-#define TGS_DMAPI_VERSION "6.0.6"
+#define TGS_DMAPI_VERSION "6.5.0"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
@@ -12,8 +12,8 @@
// 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()()
+// 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):
@@ -52,44 +52,46 @@
// EVENT CODES
-/// Before a reboot mode change, extras parameters are the current and new reboot mode enums
+/// 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
+/// 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
+/// 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
+/// 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
+/// 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
+/// 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
+/// 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
+/// 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
+/// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path.
#define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4
-/// Before a BYOND install operation begins. Parameters: [/datum/tgs_version] of the installing BYOND
+/// Before a BYOND install operation begins. Parameters: [/datum/tgs_version] of the installing BYOND.
#define TGS_EVENT_BYOND_INSTALL_START 5
/// When a BYOND install operation fails. Parameters: Error message
#define TGS_EVENT_BYOND_INSTALL_FAIL 6
-/// When the active BYOND version changes. Parameters: (Nullable) [/datum/tgs_version] of the current BYOND, [/datum/tgs_version] of the new BYOND
+/// When the active BYOND version changes. Parameters: (Nullable) [/datum/tgs_version] of the current BYOND, [/datum/tgs_version] of the new BYOND.
#define TGS_EVENT_BYOND_ACTIVE_VERSION_CHANGE 7
-/// When the compiler starts running. Parameters: Game directory path, origin commit SHA
+/// 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
+/// 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
+/// 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
+/// 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
+/// 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
+/// 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
+/// 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
@@ -104,6 +106,12 @@
#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, byond version.
+#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
@@ -158,28 +166,28 @@
/datum/tgs_revision_information
/// Full SHA of the commit.
var/commit
- /// ISO 8601 timestamp of when the commit was created
+ /// 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
+ /// 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
+ // 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
+ /// The patch version number. null for wildcards.
var/patch
- /// Legacy version number. Generally null
+ /// Legacy version number. Generally null.
var/deprecated_patch
- /// Unparsed string value
+ /// Unparsed string value.
var/raw_parameter
- /// String value minus prefix
+ /// String value minus prefix.
var/deprefixed_parameter
/**
@@ -225,40 +233,49 @@
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
+ /// 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.
+ /// The user's display name.
var/friendly_name
- // The string to use to ping this user in a message.
+ /// The string to use to ping this user in a message.
var/mention
- /// The [/datum/tgs_chat_channel] the user was from
+ /// 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
+ * 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
+/// 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 ...`
+ /// 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
+ /// The help text displayed for this command.
var/help_text = ""
- /// If this command should be available to game administrators only
+ /// 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 string to respond to the issuer with.
+ * 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].
@@ -266,6 +283,107 @@
/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.
@@ -285,75 +403,73 @@
// 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.
+ * 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
+ * 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.
+ * Send a message to connected chats. This function may sleep!
*
- * message - The string to send.
+ * 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(message, admin_only = FALSE)
+/world/proc/TgsTargetedChatBroadcast(datum/tgs_message_content/message, admin_only = FALSE)
return
/**
- * Send a private message to a specific user.
+ * Send a private message to a specific user. This function may sleep!
*
- * message - The string to send.
+ * message - The [/datum/tgs_message_content] to send.
* user: The [/datum/tgs_chat_user] to PM.
*/
-/world/proc/TgsChatPrivateMessage(message, datum/tgs_chat_user/user)
+/world/proc/TgsChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user)
return
-// The following functions will sleep if a call to TgsNew() is sleeping
-
/**
- * Send a message to connected chats that are flagged as game-related in TGS.
+ * Send a message to connected chats that are flagged as game-related in TGS. This function may sleep!
*
- * message - The string to send.
+ * 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(message, list/channels = null)
+/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.
+/// 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 current [/datum/tgs_version] of the DMAPI being used if it was activated, null otherwise.
+/// 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.
+/// 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.
+/// 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.
+/// 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 a list of active [/datum/tgs_revision_information/test_merge]s if TGS is present, null otherwise.
+/// 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.
+/// 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 Jordan Brown
+Copyright (c) 2017-2023 Jordan Brown
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
diff --git a/code/controllers/subsystem/SSheartbeat.dm b/code/controllers/subsystem/SSheartbeat.dm
new file mode 100644
index 00000000000..83a2aa6419a
--- /dev/null
+++ b/code/controllers/subsystem/SSheartbeat.dm
@@ -0,0 +1,20 @@
+SUBSYSTEM_DEF(heartbeat)
+ name = "Heartbeat"
+ flags = SS_KEEP_TIMING
+ wait = 30 SECONDS
+ runlevels = RUNLEVEL_INIT | RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME // ALL THE THINGS
+ /// Last time we got a heartbeat from TGS
+ var/last_heartbeat = 0
+ /// Has a warning been sent this round?
+ var/warning_tripped = FALSE
+
+/datum/controller/subsystem/heartbeat/Initialize()
+ // Disable if we arent running on TGS
+ if(!world.TgsAvailable())
+ flags |= SS_NO_FIRE
+
+/datum/controller/subsystem/heartbeat/fire(resumed)
+ // If the last heartbeat is 0, we never got one this round
+ if(last_heartbeat != 0 && (last_heartbeat + 2 MINUTES < REALTIMEOFDAY) && !warning_tripped)
+ to_chat(GLOB.admins, "
--- CRITICAL ---
The server hasn't received an uptime check from the server daemon for over 2 minutes. Inform AA ASAP.
")
+ warning_tripped = TRUE
diff --git a/code/datums/tgs_event_handler.dm b/code/datums/tgs_event_handler.dm
index 584c932c951..0918a8ba9f2 100644
--- a/code/datums/tgs_event_handler.dm
+++ b/code/datums/tgs_event_handler.dm
@@ -10,6 +10,7 @@ GLOBAL_VAR_INIT(slower_restart, FALSE)
/datum/tgs_event_handler/impl
var/datum/timedevent/reattach_timer
+ receive_health_checks = TRUE
/datum/tgs_event_handler/impl/HandleEvent(event_code, ...)
switch(event_code)
@@ -48,6 +49,8 @@ GLOBAL_VAR_INIT(slower_restart, FALSE)
if(reattach_timer)
deltimer(reattach_timer)
reattach_timer = null
+ if(TGS_EVENT_HEALTH_CHECK)
+ SSheartbeat.last_heartbeat = REALTIMEOFDAY
/datum/tgs_event_handler/impl/proc/LateOnReattach()
server_announce_adminonly("\[Warning] TGS hasn't notified us of it coming back for a full minute! Is there a problem?")
diff --git a/code/modules/tgs/LICENSE b/code/modules/tgs/LICENSE
index 221f9e1deb2..2bedf9a63aa 100644
--- a/code/modules/tgs/LICENSE
+++ b/code/modules/tgs/LICENSE
@@ -1,6 +1,6 @@
The MIT License
-Copyright (c) 2017 Jordan Brown
+Copyright (c) 2017-2023 Jordan Brown
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
diff --git a/code/modules/tgs/core/tgs_datum.dm b/code/modules/tgs/core/tgs_datum.dm
index 4d37ed662d1..68b0330fe86 100644
--- a/code/modules/tgs/core/tgs_datum.dm
+++ b/code/modules/tgs/core/tgs_datum.dm
@@ -4,6 +4,8 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
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
diff --git a/code/modules/tgs/includes.dm b/code/modules/tgs/includes.dm
index 70f10c7ff76..29ee5886ac2 100644
--- a/code/modules/tgs/includes.dm
+++ b/code/modules/tgs/includes.dm
@@ -13,5 +13,9 @@
#include "v5\_v5_defines.dm"
#include "v5\v5_api.dm"
+#include "v5\v5_bridge.dm"
+#include "v5\v5_chunking.dm"
#include "v5\v5_commands.dm"
+#include "v5\v5_serializers.dm"
+#include "v5\v5_topic.dm"
#include "v5\v5_undefs.dm"
diff --git a/code/modules/tgs/v3210/README.md b/code/modules/tgs/v3210/README.md
index f96e7cf3b31..01ef44767ad 100644
--- a/code/modules/tgs/v3210/README.md
+++ b/code/modules/tgs/v3210/README.md
@@ -2,5 +2,5 @@
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.
+- [api.dm](./v3_api.dm) contains the bulk of the API code.
+- [commands.dm](./v3_commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
diff --git a/code/modules/tgs/v4/README.md b/code/modules/tgs/v4/README.md
index 78191447b27..c14d7d38872 100644
--- a/code/modules/tgs/v4/README.md
+++ b/code/modules/tgs/v4/README.md
@@ -2,5 +2,5 @@
This DMAPI implements bridge requests using file output which TGS monitors for. It has a safe mode restriction.
-- [api.dm](./api.dm) contains the bulk of the API code.
-- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
+- [api.dm](./v4_api.dm) contains the bulk of the API code.
+- [commands.dm](./v4_commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
diff --git a/code/modules/tgs/v4/v4_api.dm b/code/modules/tgs/v4/v4_api.dm
index 0e37a7aa24c..b9a75c4abb4 100644
--- a/code/modules/tgs/v4/v4_api.dm
+++ b/code/modules/tgs/v4/v4_api.dm
@@ -256,33 +256,46 @@
/datum/tgs_api/v4/Revision()
return cached_revision
-/datum/tgs_api/v4/ChatBroadcast(message, list/channels)
+/datum/tgs_api/v4/ChatBroadcast(datum/tgs_message_content/message, list/channels)
var/list/ids
if(length(channels))
ids = list()
for(var/I in channels)
var/datum/tgs_chat_channel/channel = I
ids += channel.id
- message = list("message" = message, "channelIds" = ids)
+
+ message = UpgradeDeprecatedChatMessage(message)
+
+ if (!length(channels))
+ return
+
+ message = list("message" = message.text, "channelIds" = ids)
if(intercepted_message_queue)
intercepted_message_queue += list(message)
else
Export(TGS4_COMM_CHAT, message)
-/datum/tgs_api/v4/ChatTargetedBroadcast(message, admin_only)
+/datum/tgs_api/v4/ChatTargetedBroadcast(datum/tgs_message_content/message, admin_only)
var/list/channels = list()
for(var/I in ChatChannelInfo())
var/datum/tgs_chat_channel/channel = I
if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only)))
channels += channel.id
- message = list("message" = message, "channelIds" = channels)
+
+ message = UpgradeDeprecatedChatMessage(message)
+
+ if (!length(channels))
+ return
+
+ message = list("message" = message.text, "channelIds" = channels)
if(intercepted_message_queue)
intercepted_message_queue += list(message)
else
Export(TGS4_COMM_CHAT, message)
-/datum/tgs_api/v4/ChatPrivateMessage(message, datum/tgs_chat_user/user)
- message = list("message" = message, "channelIds" = list(user.channel.id))
+/datum/tgs_api/v4/ChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user)
+ message = UpgradeDeprecatedChatMessage(message)
+ message = list("message" = message.text, "channelIds" = list(user.channel.id))
if(intercepted_message_queue)
intercepted_message_queue += list(message)
else
diff --git a/code/modules/tgs/v4/v4_commands.dm b/code/modules/tgs/v4/v4_commands.dm
index 4ca1500167b..d6d3d718d47 100644
--- a/code/modules/tgs/v4/v4_commands.dm
+++ b/code/modules/tgs/v4/v4_commands.dm
@@ -3,6 +3,9 @@
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!")
@@ -34,8 +37,8 @@
var/datum/tgs_chat_command/sc = custom_commands[command]
if(sc)
- var/result = sc.Run(u, params)
- if(result == null)
- result = ""
- return result
+ var/datum/tgs_message_content/result = sc.Run(u, params)
+ result = UpgradeDeprecatedCommandResponse(result, command)
+
+ return result?.text
return "Unknown command: [command]!"
diff --git a/code/modules/tgs/v5/README.md b/code/modules/tgs/v5/README.md
index 5b48d57a1f0..dd8da3bea43 100644
--- a/code/modules/tgs/v5/README.md
+++ b/code/modules/tgs/v5/README.md
@@ -2,7 +2,12 @@
This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no security restrictions.
-- [_defines.dm](./_defines.dm) contains constant definitions.
-- [api.dm](./api.dm) contains the bulk of the API code.
-- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
-- [undefs.dm](./undefs.dm) Undoes the work of `_defines.dm`.
+- [v5_interop_version.dm](./v5_interop_version.dm) contains the version of the API used between the DMAPI and TGS.
+- [_v5_defines.dm](./_v5_defines.dm) contains constant definitions.
+- [v5_api.dm](./v5_api.dm) contains the bulk of the API code.
+- [v5_bridge.dm](./v5_bridge.dm) contains functions related to making bridge requests.
+- [v5_chunking.dm](./v5_chunking.dm) contains common function for splitting large raw data sets into chunks BYOND can natively process.
+- [v5_commands.dm](./v5_commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
+- [v5_serializers.dm](./v5_serializers.dm) contains function to help convert interop `/datum`s into a JSON encodable `list()` format.
+- [v5_topic.dm](./v5_topic.dm) contains functions related to processing topic requests.
+- [v5_undefs.dm](./v5_undefs.dm) Undoes the work of `_v5_defines.dm`.
diff --git a/code/modules/tgs/v5/_v5_defines.dm b/code/modules/tgs/v5/_v5_defines.dm
index 10bc4cbe406..c7213cc2469 100644
--- a/code/modules/tgs/v5/_v5_defines.dm
+++ b/code/modules/tgs/v5/_v5_defines.dm
@@ -4,16 +4,29 @@
#define DMAPI5_BRIDGE_DATA "data"
#define DMAPI5_TOPIC_DATA "tgs_data"
+#define DMAPI5_BRIDGE_REQUEST_LIMIT 8198
+#define DMAPI5_TOPIC_REQUEST_LIMIT 65529
+#define DMAPI5_TOPIC_RESPONSE_LIMIT 65528
+
#define DMAPI5_BRIDGE_COMMAND_PORT_UPDATE 0
#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_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"
@@ -25,7 +38,6 @@
#define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort"
#define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation"
-#define DMAPI5_CHAT_MESSAGE_TEXT "text"
#define DMAPI5_CHAT_MESSAGE_CHANNEL_IDS "channelIds"
#define DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER "accessIdentifier"
@@ -63,8 +75,10 @@
#define DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED 4
#define DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE 5
#define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6
-#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 7
+#define DMAPI5_TOPIC_COMMAND_HEALTHCHECK 7
#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8
+#define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9
+#define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10
#define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType"
#define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand"
@@ -75,6 +89,7 @@
#define DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE "chatUpdate"
#define DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION "newServerVersion"
+#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE "commandResponse"
#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE "commandResponseMessage"
#define DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES "chatResponses"
@@ -93,6 +108,7 @@
#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"
diff --git a/code/modules/tgs/v5/v5_api.dm b/code/modules/tgs/v5/v5_api.dm
index 9e040d22754..60f1c00bdaf 100644
--- a/code/modules/tgs/v5/v5_api.dm
+++ b/code/modules/tgs/v5/v5_api.dm
@@ -17,6 +17,11 @@
var/initialized = FALSE
+ var/chunked_requests = 0
+ var/list/chunked_topics = list()
+
+ var/detached = FALSE
+
/datum/tgs_api/v5/ApiVersion()
return new /datum/tgs_version(
#include "v5_interop_version.dm"
@@ -97,172 +102,18 @@
/datum/tgs_api/v5/OnInitializationComplete()
Bridge(DMAPI5_BRIDGE_COMMAND_PRIME)
-/datum/tgs_api/v5/proc/TopicResponse(error_message = null)
- var/list/response = list()
- response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message
-
- return json_encode(response)
-
/datum/tgs_api/v5/OnTopic(T)
+ RequireInitialBridgeResponse()
var/list/params = params2list(T)
var/json = params[DMAPI5_TOPIC_DATA]
if(!json)
return FALSE // continue to /world/Topic
- var/list/topic_parameters = json_decode(json)
- if(!topic_parameters)
- return TopicResponse("Invalid topic parameters json!");
-
if(!initialized)
- TGS_WARNING_LOG("Missed topic due to not being initialized: [T]")
+ TGS_WARNING_LOG("Missed topic due to not being initialized: [json]")
return TRUE // too early to handle, but it's still our responsibility
- var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER]
- if(their_sCK != access_identifier)
- return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!");
-
- var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE]
- if(!isnum(command))
- return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] from: [json]!")
-
- switch(command)
- if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND)
- var/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND])
- if(!result)
- result = TopicResponse("Error running chat command!")
- return result
- if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION)
- intercepted_message_queue = list()
- var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]
- if(!istype(event_notification))
- return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!")
-
- var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE]
- if(!isnum(event_type))
- return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!")
-
- var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS]
- if(event_parameters && !istype(event_parameters))
- return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!")
-
- var/list/event_call = list(event_type)
- if(event_parameters)
- event_call += event_parameters
-
- if(event_handler != null)
- event_handler.HandleEvent(arglist(event_call))
-
- var/list/response = list()
- response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue
- intercepted_message_queue = null
- return json_encode(response)
- if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT)
- var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
- if (!isnum(new_port) || !(new_port > 0))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
-
- if(event_handler != null)
- event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)
-
- //the topic still completes, miraculously
- //I honestly didn't believe byond could do it without exploding
- if(!world.OpenPort(new_port))
- return TopicResponse("Port change failed!")
-
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE)
- var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]
- if(!isnum(new_reboot_mode))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!")
-
- if(event_handler != null)
- event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode)
-
- reboot_mode = new_reboot_mode
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED)
- var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]
- if(!istext(new_instance_name))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!")
-
- if(event_handler != null)
- event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name)
-
- instance_name = new_instance_name
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE)
- var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]
- if(!istype(chat_update_json))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!")
-
- DecodeChannels(chat_update_json)
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE)
- var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
- if (!isnum(new_port) || !(new_port > 0))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
-
- server_port = new_port
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_HEARTBEAT)
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH)
- var/new_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
-
- return json_encode(list(DMAPI5_RESPONSE_ERROR_MESSAGE = error_message, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands()))
-
- return TopicResponse("Unknown command: [command]")
-
-/datum/tgs_api/v5/proc/Bridge(command, list/data)
- if(!data)
- data = list()
-
- data[DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE] = command
- data[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] = access_identifier
-
- var/json = json_encode(data)
- var/encoded_json = url_encode(json)
-
- // This is an infinite sleep until we get a response
- var/export_response = world.Export("http://127.0.0.1:[server_port]/Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]")
- if(!export_response)
- TGS_ERROR_LOG("Failed export request: [json]")
- return
-
- var/response_json = file2text(export_response["CONTENT"])
- if(!response_json)
- TGS_ERROR_LOG("Failed export request, missing content!")
- return
-
- var/list/bridge_response = json_decode(response_json)
- if(!bridge_response)
- TGS_ERROR_LOG("Failed export request, bad json: [response_json]")
- return
-
- var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE]
- if(error)
- TGS_ERROR_LOG("Failed export request, bad request: [error]")
- return
-
- return bridge_response
+ return ProcessTopicJson(json, TRUE)
/datum/tgs_api/v5/OnReboot()
var/list/result = Bridge(DMAPI5_BRIDGE_COMMAND_REBOOT)
@@ -297,7 +148,15 @@
RequireInitialBridgeResponse()
return revision
-/datum/tgs_api/v5/ChatBroadcast(message, list/channels)
+// Common proc b/c it's used by the V3/V4 APIs
+/datum/tgs_api/proc/UpgradeDeprecatedChatMessage(datum/tgs_message_content/message)
+ if(!istext(message))
+ return message
+
+ TGS_WARNING_LOG("Received legacy string when a [/datum/tgs_message_content] was expected. Please audit all calls to TgsChatBroadcast, TgsChatTargetedBroadcast, and TgsChatPrivateMessage to ensure they use the new /datum.")
+ return new /datum/tgs_message_content(message)
+
+/datum/tgs_api/v5/ChatBroadcast(datum/tgs_message_content/message, list/channels)
if(!length(channels))
channels = ChatChannelInfo()
@@ -306,26 +165,41 @@
var/datum/tgs_chat_channel/channel = I
ids += channel.id
- message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = ids)
+ message = UpgradeDeprecatedChatMessage(message)
+
+ if (!length(channels))
+ return
+
+ message = message._interop_serialize()
+ message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = ids
if(intercepted_message_queue)
intercepted_message_queue += list(message)
else
Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message))
-/datum/tgs_api/v5/ChatTargetedBroadcast(message, admin_only)
+/datum/tgs_api/v5/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 = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = channels)
+
+ message = UpgradeDeprecatedChatMessage(message)
+
+ if (!length(channels))
+ return
+
+ message = message._interop_serialize()
+ message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = channels
if(intercepted_message_queue)
intercepted_message_queue += list(message)
else
Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message))
-/datum/tgs_api/v5/ChatPrivateMessage(message, datum/tgs_chat_user/user)
- message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = list(user.channel.id))
+/datum/tgs_api/v5/ChatPrivateMessage(datum/tgs_message_content/message, datum/tgs_chat_user/user)
+ message = UpgradeDeprecatedChatMessage(message)
+ message = message._interop_serialize()
+ message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = list(user.channel.id)
if(intercepted_message_queue)
intercepted_message_queue += list(message)
else
@@ -333,6 +207,7 @@
/datum/tgs_api/v5/ChatChannelInfo()
RequireInitialBridgeResponse()
+ WaitForReattach(TRUE)
return chat_channels.Copy()
/datum/tgs_api/v5/proc/DecodeChannels(chat_update_json)
@@ -354,6 +229,7 @@
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()
diff --git a/code/modules/tgs/v5/v5_bridge.dm b/code/modules/tgs/v5/v5_bridge.dm
new file mode 100644
index 00000000000..37f58bcdf63
--- /dev/null
+++ b/code/modules/tgs/v5/v5_bridge.dm
@@ -0,0 +1,99 @@
+/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/url = "http://127.0.0.1:[server_port]/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/response_json = file2text(export_response["CONTENT"])
+ if(!response_json)
+ TGS_ERROR_LOG("Failed bridge request, missing 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/v5_chunking.dm b/code/modules/tgs/v5/v5_chunking.dm
new file mode 100644
index 00000000000..cd5944d34fb
--- /dev/null
+++ b/code/modules/tgs/v5/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/v5_commands.dm b/code/modules/tgs/v5/v5_commands.dm
index 6d31dd3422d..c8f538db593 100644
--- a/code/modules/tgs/v5/v5_commands.dm
+++ b/code/modules/tgs/v5/v5_commands.dm
@@ -3,14 +3,17 @@
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_WARNING_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!")
+ TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!")
continue
if(results[command_name])
var/datum/other = custom_commands[command_name]
- TGS_WARNING_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!")
+ TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!")
continue
results += list(list(DMAPI5_CUSTOM_CHAT_COMMAND_NAME = command_name, DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT = stc.help_text, DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY = stc.admin_only))
custom_commands[command_name] = stc
@@ -30,11 +33,28 @@
var/datum/tgs_chat_command/sc = custom_commands[command]
if(sc)
- var/text_response = sc.Run(u, params)
- var/list/topic_response = list()
- if(!istext(text_response))
- TGS_ERROR_LOG("Custom command [command] should return a string! Got: \"[text_response]\"")
- text_response = null
- topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = text_response
- return json_encode(topic_response)
+ var/datum/tgs_message_content/response = sc.Run(u, params)
+ response = UpgradeDeprecatedCommandResponse(response, command)
+
+ var/list/topic_response = TopicResponse()
+ topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = response?.text
+ topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE] = response?._interop_serialize()
+ 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/v5_interop_version.dm b/code/modules/tgs/v5/v5_interop_version.dm
index c7bf62ecae3..5d3d491a736 100644
--- a/code/modules/tgs/v5/v5_interop_version.dm
+++ b/code/modules/tgs/v5/v5_interop_version.dm
@@ -1 +1 @@
-"5.3.0"
+"5.6.1"
diff --git a/code/modules/tgs/v5/v5_serializers.dm b/code/modules/tgs/v5/v5_serializers.dm
new file mode 100644
index 00000000000..3a6fbe6be34
--- /dev/null
+++ b/code/modules/tgs/v5/v5_serializers.dm
@@ -0,0 +1,59 @@
+/datum/tgs_message_content/proc/_interop_serialize()
+ return list("text" = text, "embed" = embed?._interop_serialize())
+
+/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(islist(fields))
+ 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" = image?._interop_serialize(),
+ "thumbnail" = thumbnail?._interop_serialize(),
+ "video" = video?._interop_serialize(),
+ "footer" = footer?._interop_serialize(),
+ "provider" = provider?._interop_serialize(),
+ "author" = author?._interop_serialize(),
+ "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/v5_topic.dm b/code/modules/tgs/v5/v5_topic.dm
new file mode 100644
index 00000000000..56c1824fd97
--- /dev/null
+++ b/code/modules/tgs/v5/v5_topic.dm
@@ -0,0 +1,261 @@
+/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)
+ 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)
+ var/list/topic_parameters = json_decode(json)
+ if(!topic_parameters)
+ return TopicResponse("Invalid topic parameters json: [json]!");
+
+ var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER]
+ if(check_access_identifier && their_sCK != access_identifier)
+ return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER]!")
+
+ var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE]
+ if(!isnum(command))
+ 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)
+ 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)
+ intercepted_message_queue = list()
+ var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]
+ if(!istype(event_notification))
+ return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!")
+
+ var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE]
+ if(!isnum(event_type))
+ return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!")
+
+ var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS]
+ if(event_parameters && !istype(event_parameters))
+ return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!")
+
+ var/list/event_call = list(event_type)
+ if (event_type == TGS_EVENT_WATCHDOG_DETACH)
+ detached = TRUE
+ chat_channels.Cut() // https://github.com/tgstation/tgstation-server/issues/1490
+
+ if(event_parameters)
+ event_call += event_parameters
+
+ if(event_handler != null)
+ event_handler.HandleEvent(arglist(event_call))
+
+ var/list/response = TopicResponse()
+ response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue
+ intercepted_message_queue = null
+ return response
+
+ if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT)
+ var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
+ if (!isnum(new_port) || !(new_port > 0))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
+
+ if(event_handler != null)
+ event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)
+
+ //the topic still completes, miraculously
+ //I honestly didn't believe byond could do it without exploding
+ if(!world.OpenPort(new_port))
+ return TopicResponse("Port change failed!")
+
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE)
+ var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]
+ if(!isnum(new_reboot_mode))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!")
+
+ if(event_handler != null)
+ event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode)
+
+ reboot_mode = new_reboot_mode
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED)
+ var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]
+ if(!istext(new_instance_name))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!")
+
+ if(event_handler != null)
+ event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name)
+
+ instance_name = new_instance_name
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE)
+ var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]
+ if(!istype(chat_update_json))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!")
+
+ DecodeChannels(chat_update_json)
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE)
+ var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
+ if (!isnum(new_port) || !(new_port > 0))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
+
+ server_port = new_port
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_HEALTHCHECK)
+ if(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()
+ 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
+
+ return TopicResponse("Unknown command: [command]")
diff --git a/code/modules/tgs/v5/v5_undefs.dm b/code/modules/tgs/v5/v5_undefs.dm
index 5885a60e75c..c679737dfc4 100644
--- a/code/modules/tgs/v5/v5_undefs.dm
+++ b/code/modules/tgs/v5/v5_undefs.dm
@@ -4,16 +4,29 @@
#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_PORT_UPDATE
#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_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
@@ -25,7 +38,6 @@
#undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT
#undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION
-#undef DMAPI5_CHAT_MESSAGE_TEXT
#undef DMAPI5_CHAT_MESSAGE_CHANNEL_IDS
#undef DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER
@@ -63,7 +75,7 @@
#undef DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED
#undef DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE
#undef DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE
-#undef DMAPI5_TOPIC_COMMAND_HEARTBEAT
+#undef DMAPI5_TOPIC_COMMAND_HEALTHCHECK
#undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH
#undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE
@@ -75,6 +87,7 @@
#undef DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE
#undef DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION
+#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE
#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE
#undef DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES
@@ -93,6 +106,7 @@
#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
diff --git a/paradise.dme b/paradise.dme
index 5d6210011b7..f71ab7dac2b 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -251,6 +251,7 @@
#include "code\controllers\subsystem\SSfires.dm"
#include "code\controllers\subsystem\SSgarbage.dm"
#include "code\controllers\subsystem\SSghost_spawns.dm"
+#include "code\controllers\subsystem\SSheartbeat.dm"
#include "code\controllers\subsystem\SShttp.dm"
#include "code\controllers\subsystem\SSicon_smooth.dm"
#include "code\controllers\subsystem\SSidlenpcpool.dm"