From 24cd190989f109bafed393ee305eb74379d1706e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=91?=
<71145590+Evie7056@users.noreply.github.com>
Date: Mon, 1 May 2023 20:53:13 +0300
Subject: [PATCH 01/20] All the colors (#2)
* making them colorful
* fixing
---
cev_eris.dme | 6 +
code/__DEFINES/colors.dm | 3 +
code/game/objects/effects/chem/water.dm | 3 +-
proxima/code/game/atoms.dm | 10 +
proxima/code/game/objects/effects/misc.dm | 149 +++++
.../objects/items/devices/paint_sprayer.dm | 202 ++++++
.../code/game/objects/structures/window.dm | 8 +
.../game/turf/flooring/flooring_decals.dm | 617 ++++++++++++++++++
proxima/code/game/turf/simulated/walls.dm | 9 +
9 files changed, 1006 insertions(+), 1 deletion(-)
create mode 100644 proxima/code/game/atoms.dm
create mode 100644 proxima/code/game/objects/effects/misc.dm
create mode 100644 proxima/code/game/objects/items/devices/paint_sprayer.dm
create mode 100644 proxima/code/game/objects/structures/window.dm
create mode 100644 proxima/code/game/turf/flooring/flooring_decals.dm
create mode 100644 proxima/code/game/turf/simulated/walls.dm
diff --git a/cev_eris.dme b/cev_eris.dme
index b72dd7f049e..6b4fa151cb6 100644
--- a/cev_eris.dme
+++ b/cev_eris.dme
@@ -2836,4 +2836,10 @@
#include "maps\submaps\planetary_ruins\radshrine\radshrine.dm"
#include "maps\submaps\planetary_ruins\spider_nest\spider_nest.dm"
#include "maps\submaps\planetary_ruins\tar_anomaly\tar_anomaly.dm"
+#include "proxima\code\game\atoms.dm"
+#include "proxima\code\game\turf\simulated\walls.dm"
+#include "proxima\code\game\objects\structures\window.dm"
+#include "proxima\code\game\turf\flooring\flooring_decals.dm"
+#include "proxima\code\game\objects\effects\misc.dm"
+#include "proxima\code\game\objects\items\devices\paint_sprayer.dm"
// END_INCLUDE
diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm
index 2780c6da196..9577b3941dc 100644
--- a/code/__DEFINES/colors.dm
+++ b/code/__DEFINES/colors.dm
@@ -44,6 +44,7 @@
#define COLOR_BOTTLE_GREEN "#1f6b4f"
#define COLOR_PALE_BTL_GREEN "#57967f"
#define COLOR_GUNMETAL "#545c68"
+#define COLOR_WALL_GUNMETAL "#353a42"
#define COLOR_MUZZLE_FLASH "#ffffb2"
#define COLOR_CHESTNUT "#996633"
#define COLOR_BEASTY_BROWN "#663300"
@@ -52,6 +53,7 @@
#define COLOR_LIGHT_CYAN "#66ccff"
#define COLOR_PAKISTAN_GREEN "#006600"
#define COLOR_HULL "#436b8e"
+#define COLOR_HULL_BLUE "#436b8e"
#define COLOR_AMBER "#ffbf00"
#define COLOR_COMMAND_BLUE "#46698c"
#define COLOR_SKY_BLUE "#5ca1cc"
@@ -60,6 +62,7 @@
#define COLOR_TITANIUM "#d1e6e3"
#define COLOR_DARK_GUNMETAL "#4c535b"
#define COLOR_INDIGO "#4b0082"
+#define COLOR_ALUMINIUM "#bbbbbb"
#define COLOR_DARK_BLUE_GRAY "#3e4855"
#define COLOR_ASTEROID_ROCK "#735555"
#define COLOR_PALE_YELLOW "#c1bb7a"
diff --git a/code/game/objects/effects/chem/water.dm b/code/game/objects/effects/chem/water.dm
index 03fce10777a..47e26247944 100644
--- a/code/game/objects/effects/chem/water.dm
+++ b/code/game/objects/effects/chem/water.dm
@@ -11,7 +11,8 @@
if(src)
qdel(src)
-/obj/effect/effect/water/proc/set_color() // Call it after you move reagents to it
+// PRX - was a separate proc now moved to /atom/proc/set_color()
+/obj/effect/effect/water/set_color() // Call it after you move reagents to it
icon += reagents.get_color()
/obj/effect/effect/water/proc/set_up(var/turf/target, var/step_count = 5, var/delay = 5)
diff --git a/proxima/code/game/atoms.dm b/proxima/code/game/atoms.dm
new file mode 100644
index 00000000000..4c442ca531b
--- /dev/null
+++ b/proxima/code/game/atoms.dm
@@ -0,0 +1,10 @@
+/atom
+ var/paint_color
+ var/can_paint = FALSE
+
+/atom/proc/get_color()
+ return paint_color
+
+/atom/proc/set_color(var/color)
+ paint_color = color
+ update_icon()
diff --git a/proxima/code/game/objects/effects/misc.dm b/proxima/code/game/objects/effects/misc.dm
new file mode 100644
index 00000000000..51234565b67
--- /dev/null
+++ b/proxima/code/game/objects/effects/misc.dm
@@ -0,0 +1,149 @@
+//The effect when you wrap a dead body in gift wrap
+/obj/effect/spresent
+ name = "strange present"
+ desc = "It's a ... present?"
+ icon = 'icons/obj/items.dmi'
+ icon_state = "strangepresent"
+ density = TRUE
+ anchored = FALSE
+
+//Paints the wall it spawns on, then dies
+/obj/effect/paint
+ name = "coat of paint"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "wall_paint_effect"
+ layer = TURF_PLATING_DECAL_LAYER
+ blend_mode = BLEND_MULTIPLY
+
+/obj/effect/paint/Initialize()
+ . = ..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/effect/paint/LateInitialize()
+ var/turf/simulated/wall/W = get_turf(src)
+ if(istype(W))
+ W.paint_color = color
+ W.update_icon()
+ qdel(src)
+
+/obj/effect/paint/pink
+ color = COLOR_PINK
+
+/obj/effect/paint/sun
+ color = COLOR_SUN
+
+/obj/effect/paint/red
+ color = COLOR_RED
+
+/obj/effect/paint/silver
+ color = COLOR_SILVER
+
+/obj/effect/paint/black
+ color = COLOR_DARK_GRAY
+
+/obj/effect/paint/green
+ color = COLOR_GREEN_GRAY
+
+/obj/effect/paint/blue
+ color = COLOR_NAVY_BLUE
+
+/obj/effect/paint/ocean
+ color = COLOR_OCEAN
+
+/obj/effect/paint/palegreengray
+ color = COLOR_PALE_GREEN_GRAY
+
+/obj/effect/paint/brown
+ color = COLOR_DARK_BROWN
+
+/obj/effect/paint/gunmetal_dark
+ color = COLOR_WALL_GUNMETAL
+
+/obj/effect/paint/hull_blue
+ color = COLOR_HULL_BLUE
+
+//Stripes the wall it spawns on, then dies
+/obj/effect/paint_stripe
+ name = "stripe of paint"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "white"
+ layer = TURF_PLATING_DECAL_LAYER
+ blend_mode = BLEND_MULTIPLY
+
+/obj/effect/paint_stripe/Initialize()
+ . = ..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/effect/paint_stripe/LateInitialize()
+ var/turf/simulated/wall/W = get_turf(src)
+ if(istype(W))
+ W.stripe_color = color
+ W.update_icon()
+ qdel(src)
+
+/obj/effect/paint_stripe/green
+ color = COLOR_GREEN_GRAY
+
+/obj/effect/paint_stripe/red
+ color = COLOR_RED_GRAY
+
+/obj/effect/paint_stripe/paleblue
+ color = COLOR_PALE_BLUE_GRAY
+
+/obj/effect/paint_stripe/yellow
+ color = COLOR_BROWN
+
+/obj/effect/paint_stripe/blue
+ color = COLOR_BLUE_GRAY
+
+/obj/effect/paint_stripe/brown
+ color = COLOR_DARK_BROWN
+
+/obj/effect/paint_stripe/mauve
+ color = COLOR_PALE_PURPLE_GRAY
+
+/obj/effect/paint_stripe/white
+ color = COLOR_SILVER
+
+/obj/effect/paint_stripe/gunmetal
+ color = COLOR_GUNMETAL
+
+/obj/effect/paint_stripe/gunmetal_dark
+ color = COLOR_WALL_GUNMETAL
+
+/obj/effect/paint_stripe/hull_blue
+ color = COLOR_HULL_BLUE
+/*
+/obj/effect/gas_setup //cryogenic
+ icon = 'icons/mob/screen1.dmi'
+ icon_state = "x3"
+ var/tempurature = 70
+ var/pressure = 20* ONE_ATMOSPHERE
+
+/obj/effect/gas_setup/Initialize()
+ SHOULD_CALL_PARENT(FALSE)
+ atom_flags |= ATOM_FLAG_INITIALIZED
+ var/obj/machinery/atmospherics/pipe/P = locate() in loc
+ if(P && !P.air_temporary)
+ P.air_temporary = new(P.volume, tempurature)
+ var/datum/gas_mixture/G = P.air_temporary
+ G.adjust_gas(GAS_OXYGEN,((pressure*P.volume)/(R_IDEAL_GAS_EQUATION*temperature)))
+ return INITIALIZE_HINT_QDEL
+
+
+/obj/effect/heat
+ icon = 'icons/effects/fire.dmi'
+ icon_state = "3"
+ render_target = HEAT_EFFECT_TARGET
+ mouse_opacity = MOUSE_OPACITY_UNCLICKABLE
+
+/// Example of a warp filter
+/obj/effect/effect/warp
+ plane = WARP_EFFECT_PLANE
+ appearance_flags = PIXEL_SCALE
+ icon = 'icons/effects/352x352.dmi'
+ icon_state = "singularity_s11"
+ pixel_x = -176
+ pixel_y = -176
+ z_flags = ZMM_IGNORE
+*/
diff --git a/proxima/code/game/objects/items/devices/paint_sprayer.dm b/proxima/code/game/objects/items/devices/paint_sprayer.dm
new file mode 100644
index 00000000000..4a767bb2a92
--- /dev/null
+++ b/proxima/code/game/objects/items/devices/paint_sprayer.dm
@@ -0,0 +1,202 @@
+/obj/item/device/floor_painter
+ name = "wall painter"
+
+ decals = list(
+ "Quarter-turf" = list("path" = /obj/effect/floor_decal/corner, "precise" = 1, "colored" = 1),
+ "Monotile full" = list("path" = /obj/effect/floor_decal/corner/white/mono, "colored" = 1),
+ "Monotile halved" = list("path" = /obj/effect/floor_decal/corner/white/half, "colored" = 1),
+ "hazard stripes" = list("path" = /obj/effect/floor_decal/industrial/warning),
+ "corner, hazard" = list("path" = /obj/effect/floor_decal/industrial/warning/corner),
+ "hatched marking" = list("path" = /obj/effect/floor_decal/industrial/hatch, "coloured" = 1),
+ "dotted outline" = list("path" = /obj/effect/floor_decal/industrial/outline, "coloured" = 1),
+ "loading sign" = list("path" = /obj/effect/floor_decal/industrial/loading),
+ "mosaic, large" = list("path" = /obj/effect/floor_decal/chapel),
+ "1" = list("path" = /obj/effect/floor_decal/sign),
+ "2" = list("path" = /obj/effect/floor_decal/sign/two),
+ "A" = list("path" = /obj/effect/floor_decal/sign/a),
+ "B" = list("path" = /obj/effect/floor_decal/sign/b),
+ "C" = list("path" = /obj/effect/floor_decal/sign/c),
+ "D" = list("path" = /obj/effect/floor_decal/sign/d),
+ "M" = list("path" = /obj/effect/floor_decal/sign/m),
+ "V" = list("path" = /obj/effect/floor_decal/sign/v),
+ "CMO" = list("path" = /obj/effect/floor_decal/sign/cmo),
+ "Ex" = list("path" = /obj/effect/floor_decal/sign/ex),
+ "Psy" = list("path" = /obj/effect/floor_decal/sign/p),
+ "remove all decals" = list("path" = /obj/effect/floor_decal/reset)
+ )
+ paint_dirs = list(
+ "north" = NORTH,
+ "northwest" = NORTHWEST,
+ "west" = WEST,
+ "southwest" = SOUTHWEST,
+ "south" = SOUTH,
+ "southeast" = SOUTHEAST,
+ "east" = EAST,
+ "northeast" = NORTHEAST,
+ "precise" = 0
+ )
+ var/list/preset_colors = list(
+ "Beasty brown" = COLOR_BEASTY_BROWN,
+ "Blue" = COLOR_BLUE_GRAY,
+ "Civvie green" = COLOR_CIVIE_GREEN,
+ "Command blue" = COLOR_COMMAND_BLUE,
+ "Cyan" = COLOR_CYAN,
+ "Green" = COLOR_GREEN,
+ "Bottle green" = COLOR_PALE_BTL_GREEN,
+ "Nanotrasen red" = COLOR_NT_RED,
+ "Orange" = COLOR_ORANGE,
+ "Pale orange" = COLOR_PALE_ORANGE,
+ "Red" = COLOR_RED,
+ "Sky blue" = COLOR_DEEP_SKY_BLUE,
+ "Titanium" = COLOR_TITANIUM,
+ "Aluminium"= COLOR_ALUMINIUM,
+ "Violet" = COLOR_VIOLET,
+ "White" = COLOR_WHITE,
+ "Yellow" = COLOR_AMBER,
+ "Sol blue" = COLOR_HULL_BLUE,
+ "Bulkhead black" = COLOR_WALL_GUNMETAL,
+ "Remove all colors"=null
+ )
+
+/obj/item/device/floor_painter/afterattack(var/atom/A, var/mob/user, proximity, params)
+ if(!proximity)
+ return
+
+ if(istype(A, /turf/simulated/wall))
+ paint_wall()
+ return
+
+ if(istype(A, /turf/simulated/floor))
+ paint_floor()
+ return
+ return
+
+/obj/item/device/floor_painter/proc/paint_wall(/var/atom/A, var/mob/user)
+ if ((A.can_paint == TRUE) && paint_colour)
+ A.set_color(paint_color)
+ add_fingerprint(user)
+ . = TRUE
+
+/obj/item/device/floor_painter/proc/paint_floor(/var/turf/simulated/floor/F, var/mob/user, params)
+ if(!F.flooring || !F.flooring.can_paint || F.broken || F.burnt)
+ to_chat(user, SPAN_WARNING("\The [src] cannot paint broken or missing tiles."))
+ return
+
+ var/list/decal_data = decals[decal]
+ var/config_error
+ if(!islist(decal_data))
+ config_error = 1
+ var/painting_decal
+ if(!config_error)
+ painting_decal = decal_data["path"]
+ if(!ispath(painting_decal))
+ config_error = 1
+
+ if(config_error)
+ to_chat(user, SPAN_WARNING("\The [src] flashes an error light. You might need to reconfigure it."))
+ return
+
+ if(F.decals && F.decals.len > 5 && painting_decal != /obj/effect/floor_decal/reset)
+ to_chat(user, SPAN_WARNING("\The [F] has been painted too much; you need to clear it off."))
+ return
+ var/painting_dir = 0
+
+ if(paint_dir == "precise")
+ if(!decal_data["precise"])
+ painting_dir = user.dir
+ else
+ var/list/mouse_control = params2list(params)
+ var/mouse_x = text2num(mouse_control["icon-x"])
+ var/mouse_y = text2num(mouse_control["icon-y"])
+ if(isnum(mouse_x) && isnum(mouse_y))
+ if(mouse_x <= 16)
+ if(mouse_y <= 16)
+ painting_dir = WEST
+ else
+ painting_dir = NORTH
+ else
+ if(mouse_y <= 16)
+ painting_dir = SOUTH
+ else
+ painting_dir = EAST
+ else
+ painting_dir = user.dir
+ else if(paint_dirs[paint_dir])
+ painting_dir = paint_dirs[paint_dir]
+
+ var/painting_colour
+ if(decal_data["coloured"] && paint_colour)
+ painting_colour = paint_colour
+
+ new painting_decal(F, painting_dir, painting_colour)
+
+/obj/item/device/floor_painter/attack_self(var/mob/user)
+ var/choice = input("Do you wish to change the decal type, paint direction, or paint colour?") as null|anything in list("Decal","Direction", "Colour Selector", "Colour Presets")
+ if(choice == "Decal")
+ choose_decal()
+ else if(choice == "Direction")
+ choose_direction()
+ else if(choice == "Colour Selector")
+ choose_colour()
+ else if(choice == "Colour Presets")
+ choose_preset_color()
+
+/obj/item/device/floor_painter/examine(mob/user)
+ ..(user)
+ to_chat(user, "It is configured to produce the '[decal]' decal with a direction of '[paint_dir]' using [paint_colour] paint.")
+
+/obj/item/device/floor_painter/choose_colour()
+ set name = "Choose Colour"
+ set desc = "Choose a floor painter colour."
+ set category = "Object"
+ set src in usr
+
+ if(usr.incapacitated())
+ return
+ var/new_colour = input(usr, "Choose a colour.", "Floor painter", paint_colour) as color|null
+ if(new_colour && new_colour != paint_colour)
+ paint_colour = new_colour
+ to_chat(usr, SPAN_NOTICE("You set \the [src] to paint with a new colour."))
+
+/obj/item/device/floor_painter/choose_decal()
+ set name = "Choose Decal"
+ set desc = "Choose a floor painter decal."
+ set category = "Object"
+ set src in usr
+
+ if(usr.incapacitated())
+ return
+
+ var/new_decal = input("Select a decal.") as null|anything in decals
+ if(new_decal && !isnull(decals[new_decal]))
+ decal = new_decal
+ to_chat(usr, SPAN_NOTICE("You set \the [src] decal to '[decal]'."))
+
+/obj/item/device/floor_painter/choose_direction()
+ set name = "Choose Direction"
+ set desc = "Choose a floor painter direction."
+ set category = "Object"
+ set src in usr
+
+ if(usr.incapacitated())
+ return
+
+ var/new_dir = input("Select a direction.") as null|anything in paint_dirs
+ if(new_dir && !isnull(paint_dirs[new_dir]))
+ paint_dir = new_dir
+ to_chat(usr, SPAN_NOTICE("You set \the [src] direction to '[paint_dir]'."))
+
+/obj/item/device/floor_painter/verb/choose_preset_color()
+ set name = "Choose Preset color"
+ set desc = "Choose a preset color."
+ set category = "Object"
+ set src in usr
+
+ if(usr.incapacitated())
+ return
+ var/preset = input(usr, "Choose a color.", name, paint_color) as null|anything in preset_colors
+ if(usr.incapacitated())
+ return
+ if(preset && preset != paint_colour)
+ paint_colour = preset
+ to_chat(usr, SPAN_NOTICE("You set \the [src] to paint with a new colour."))
diff --git a/proxima/code/game/objects/structures/window.dm b/proxima/code/game/objects/structures/window.dm
new file mode 100644
index 00000000000..d9bb75b6ad5
--- /dev/null
+++ b/proxima/code/game/objects/structures/window.dm
@@ -0,0 +1,8 @@
+/obj/structure/window
+ can_paint = TRUE
+
+/turf/structure/window/examine(mob/user)
+ . = ..()
+
+ if(paint_color)
+ to_chat(user, "It has a coat of paint applied.")
diff --git a/proxima/code/game/turf/flooring/flooring_decals.dm b/proxima/code/game/turf/flooring/flooring_decals.dm
new file mode 100644
index 00000000000..a543edb4e62
--- /dev/null
+++ b/proxima/code/game/turf/flooring/flooring_decals.dm
@@ -0,0 +1,617 @@
+/obj/effect/floor_decal/corner
+ icon_state = "corner_white"
+ alpha = 229
+
+/obj/effect/floor_decal/corner/black
+ name = "black corner"
+ color = "#333333"
+
+/obj/effect/floor_decal/corner/black/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/black/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/black/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/black/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/black/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/black/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/black/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/black/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/black/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/black/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/blue
+ name = "blue corner"
+ color = COLOR_BLUE_GRAY
+
+/obj/effect/floor_decal/corner/blue/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/blue/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/blue/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/blue/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/blue/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/blue/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/blue/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/blue/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/blue/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/blue/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/paleblue
+ name = "pale blue corner"
+ color = COLOR_PALE_BLUE_GRAY
+
+/obj/effect/floor_decal/corner/paleblue/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/paleblue/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/paleblue/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/paleblue/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/paleblue/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/paleblue/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/paleblue/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/paleblue/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/paleblue/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/paleblue/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/green
+ name = "green corner"
+ color = COLOR_GREEN_GRAY
+
+/obj/effect/floor_decal/corner/green/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/green/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/green/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/green/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/green/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/green/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/green/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/green/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/green/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/green/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/lime
+ name = "lime corner"
+ color = COLOR_PALE_GREEN_GRAY
+
+/obj/effect/floor_decal/corner/lime/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/lime/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/lime/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/lime/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/lime/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/lime/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/lime/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/lime/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/lime/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/lime/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/yellow
+ name = "yellow corner"
+ color = COLOR_BROWN
+
+/obj/effect/floor_decal/corner/yellow/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/yellow/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/yellow/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/yellow/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/yellow/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/yellow/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/yellow/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/yellow/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/yellow/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/yellow/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/beige
+ name = "beige corner"
+ color = COLOR_BEIGE
+
+/obj/effect/floor_decal/corner/beige/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/beige/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/beige/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/beige/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/beige/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/beige/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/beige/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/beige/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/beige/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/beige/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/red
+ name = "red corner"
+ color = COLOR_RED_GRAY
+
+/obj/effect/floor_decal/corner/red/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/red/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/red/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/red/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/red/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/red/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/red/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/red/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/red/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/red/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/pink
+ name = "pink corner"
+ color = COLOR_PALE_RED_GRAY
+
+/obj/effect/floor_decal/corner/pink/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/pink/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/pink/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/pink/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/pink/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/pink/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/pink/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/pink/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/pink/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/pink/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/purple
+ name = "purple corner"
+ color = COLOR_PURPLE_GRAY
+
+/obj/effect/floor_decal/corner/purple/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/purple/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/purple/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/purple/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/purple/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/purple/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/purple/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/purple/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/purple/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/purple/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/mauve
+ name = "mauve corner"
+ color = COLOR_PALE_PURPLE_GRAY
+
+/obj/effect/floor_decal/corner/mauve/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/mauve/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/mauve/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/mauve/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/mauve/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/mauve/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/mauve/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/mauve/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/mauve/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/mauve/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/orange
+ name = "orange corner"
+ color = COLOR_DARK_ORANGE
+
+/obj/effect/floor_decal/corner/orange/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/orange/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/orange/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/orange/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/orange/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/orange/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/orange/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/orange/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/orange/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/orange/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/brown
+ name = "brown corner"
+ color = COLOR_DARK_BROWN
+
+/obj/effect/floor_decal/corner/brown/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/brown/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/brown/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/brown/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/brown/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/brown/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/brown/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/brown/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/brown/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/brown/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/white
+ name = "white corner"
+ icon_state = "corner_white"
+
+/obj/effect/floor_decal/corner/white/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/white/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/white/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/white/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/white/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/grey
+ name = "grey corner"
+ color = "#8d8c8c"
+
+/obj/effect/floor_decal/corner/grey/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/grey/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/grey/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/white/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/grey/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/grey/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/white/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/white/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/white/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/white/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/grey/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/grey/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/grey/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/grey/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/grey/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/grey/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/grey/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/lightgrey
+ name = "lightgrey corner"
+ color = "#a8b2b6"
+
+/obj/effect/floor_decal/corner/lightgrey/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/lightgrey/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/lightgrey/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/lightgrey/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/lightgrey/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/lightgrey/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/lightgrey/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/lightgrey/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/lightgrey/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/lightgrey/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/b_green
+ name = "bottle green corner"
+ color = COLOR_PALE_BTL_GREEN
+
+/obj/effect/floor_decal/corner/b_green/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/b_green/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/b_green/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/b_green/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/b_green/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/b_green/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/b_green/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/b_green/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/b_green/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/b_green/bordercee
+ icon_state = "bordercolorcee"
+
+/obj/effect/floor_decal/corner/research/diagonal
+ icon_state = "corner_white_diagonal"
+
+/obj/effect/floor_decal/corner/research/three_quarters
+ icon_state = "corner_white_three_quarters"
+
+/obj/effect/floor_decal/corner/research/full
+ icon_state = "corner_white_full"
+
+/obj/effect/floor_decal/corner/research/border
+ icon_state = "bordercolor"
+
+/obj/effect/floor_decal/corner/research/half
+ icon_state = "bordercolorhalf"
+
+/obj/effect/floor_decal/corner/research/mono
+ icon_state = "bordercolormonofull"
+
+/obj/effect/floor_decal/corner/research/bordercorner
+ icon_state = "bordercolorcorner"
+
+/obj/effect/floor_decal/corner/research/bordercorner2
+ icon_state = "bordercolorcorner2"
+
+/obj/effect/floor_decal/corner/research/borderfull
+ icon_state = "bordercolorfull"
+
+/obj/effect/floor_decal/corner/research/bordercee
+ icon_state = "bordercolorcee"
diff --git a/proxima/code/game/turf/simulated/walls.dm b/proxima/code/game/turf/simulated/walls.dm
new file mode 100644
index 00000000000..6d04297dfdb
--- /dev/null
+++ b/proxima/code/game/turf/simulated/walls.dm
@@ -0,0 +1,9 @@
+/turf/simulated/wall
+ var/stripe_color
+ can_paint = TRUE
+
+/turf/simulated/wall/examine(mob/user)
+ . = ..()
+
+ if(paint_color)
+ to_chat(user, "It has a coat of paint applied.")
From fecc7c6b28762991a218238f141e91304426d132 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=91?=
<71145590+Evie7056@users.noreply.github.com>
Date: Mon, 1 May 2023 21:00:18 +0300
Subject: [PATCH 02/20] All the colors round two (#5)
* making them colorful
* fixing
* let be commented for now
---
cev_eris.dme | 2 +-
proxima/code/game/objects/items/devices/paint_sprayer.dm | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/cev_eris.dme b/cev_eris.dme
index 6b4fa151cb6..657290fa22a 100644
--- a/cev_eris.dme
+++ b/cev_eris.dme
@@ -2841,5 +2841,5 @@
#include "proxima\code\game\objects\structures\window.dm"
#include "proxima\code\game\turf\flooring\flooring_decals.dm"
#include "proxima\code\game\objects\effects\misc.dm"
-#include "proxima\code\game\objects\items\devices\paint_sprayer.dm"
+// #include "proxima\code\game\objects\items\devices\paint_sprayer.dm"
// END_INCLUDE
diff --git a/proxima/code/game/objects/items/devices/paint_sprayer.dm b/proxima/code/game/objects/items/devices/paint_sprayer.dm
index 4a767bb2a92..b4d53db3b2c 100644
--- a/proxima/code/game/objects/items/devices/paint_sprayer.dm
+++ b/proxima/code/game/objects/items/devices/paint_sprayer.dm
@@ -71,9 +71,9 @@
return
return
-/obj/item/device/floor_painter/proc/paint_wall(/var/atom/A, var/mob/user)
- if ((A.can_paint == TRUE) && paint_colour)
- A.set_color(paint_color)
+/obj/item/device/floor_painter/proc/paint_wall(/turf/simulated/wall/W, var/mob/user)
+ if ((W.can_paint == TRUE) && paint_colour)
+ W.set_color(paint_color)
add_fingerprint(user)
. = TRUE
From f149150f1a9123810297fc4c398458fdd4bf92bc Mon Sep 17 00:00:00 2001
From: Archemagus <32466328+Archemagus@users.noreply.github.com>
Date: Mon, 1 May 2023 21:01:07 +0300
Subject: [PATCH 03/20] TGS integration (#1)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: евиё <71145590+Evie7056@users.noreply.github.com>
---
cev_eris.dme | 24 +
proxima/code/modules/TGS/TGDMAPI/tgs.dm | 483 ++++++++++++++++++
proxima/code/modules/TGS/TGDMAPI/tgs/LICENSE | 24 +
.../code/modules/TGS/TGDMAPI/tgs/README.md | 13 +
.../modules/TGS/TGDMAPI/tgs/core/README.md | 9 +
.../TGS/TGDMAPI/tgs/core/_definitions.dm | 2 +
.../code/modules/TGS/TGDMAPI/tgs/core/core.dm | 156 ++++++
.../modules/TGS/TGDMAPI/tgs/core/datum.dm | 59 +++
.../TGS/TGDMAPI/tgs/core/tgs_version.dm | 28 +
.../code/modules/TGS/TGDMAPI/tgs/includes.dm | 21 +
.../modules/TGS/TGDMAPI/tgs/v3210/README.md | 6 +
.../code/modules/TGS/TGDMAPI/tgs/v3210/api.dm | 244 +++++++++
.../modules/TGS/TGDMAPI/tgs/v3210/commands.dm | 58 +++
.../code/modules/TGS/TGDMAPI/tgs/v4/README.md | 6 +
.../code/modules/TGS/TGDMAPI/tgs/v4/api.dm | 312 +++++++++++
.../modules/TGS/TGDMAPI/tgs/v4/commands.dm | 44 ++
.../code/modules/TGS/TGDMAPI/tgs/v5/README.md | 13 +
.../TGS/TGDMAPI/tgs/v5/__interop_version.dm | 1 +
.../modules/TGS/TGDMAPI/tgs/v5/_defines.dm | 115 +++++
.../code/modules/TGS/TGDMAPI/tgs/v5/api.dm | 228 +++++++++
.../code/modules/TGS/TGDMAPI/tgs/v5/bridge.dm | 95 ++++
.../modules/TGS/TGDMAPI/tgs/v5/chunking.dm | 43 ++
.../modules/TGS/TGDMAPI/tgs/v5/commands.dm | 60 +++
.../modules/TGS/TGDMAPI/tgs/v5/serializers.dm | 59 +++
.../code/modules/TGS/TGDMAPI/tgs/v5/topic.dm | 258 ++++++++++
.../code/modules/TGS/TGDMAPI/tgs/v5/undefs.dm | 112 ++++
.../TGS/TGSIntegration/TGSChatCommands.dm | 469 +++++++++++++++++
.../TGS/TGSIntegration/TGSChatHelpers.dm | 171 +++++++
.../TGS/TGSIntegration/TGSChatHooks.dm | 164 ++++++
.../TGS/TGSIntegration/TGSIntegrationFiles.dm | 15 +
proxima/code/modules/TGS/_TGSDefines.dm | 40 ++
31 files changed, 3332 insertions(+)
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/LICENSE
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/README.md
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/core/README.md
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/core/_definitions.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/core/core.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/core/datum.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/core/tgs_version.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/includes.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v3210/README.md
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v3210/api.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v3210/commands.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v4/README.md
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v4/api.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v4/commands.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/README.md
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/__interop_version.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/_defines.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/api.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/bridge.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/chunking.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/commands.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/serializers.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/topic.dm
create mode 100644 proxima/code/modules/TGS/TGDMAPI/tgs/v5/undefs.dm
create mode 100644 proxima/code/modules/TGS/TGSIntegration/TGSChatCommands.dm
create mode 100644 proxima/code/modules/TGS/TGSIntegration/TGSChatHelpers.dm
create mode 100644 proxima/code/modules/TGS/TGSIntegration/TGSChatHooks.dm
create mode 100644 proxima/code/modules/TGS/TGSIntegration/TGSIntegrationFiles.dm
create mode 100644 proxima/code/modules/TGS/_TGSDefines.dm
diff --git a/cev_eris.dme b/cev_eris.dme
index 657290fa22a..42740f5e867 100644
--- a/cev_eris.dme
+++ b/cev_eris.dme
@@ -2836,6 +2836,30 @@
#include "maps\submaps\planetary_ruins\radshrine\radshrine.dm"
#include "maps\submaps\planetary_ruins\spider_nest\spider_nest.dm"
#include "maps\submaps\planetary_ruins\tar_anomaly\tar_anomaly.dm"
+#include "proxima\code\modules\TGS\_TGSDefines.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\includes.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\core\_definitions.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\core\core.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\core\datum.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\core\tgs_version.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v3210\api.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v3210\commands.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v4\api.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v4\commands.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v5\__interop_version.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v5\_defines.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v5\api.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v5\bridge.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v5\chunking.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v5\commands.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v5\serializers.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v5\topic.dm"
+#include "proxima\code\modules\TGS\TGDMAPI\tgs\v5\undefs.dm"
+#include "proxima\code\modules\TGS\TGSIntegration\TGSChatCommands.dm"
+#include "proxima\code\modules\TGS\TGSIntegration\TGSChatHelpers.dm"
+#include "proxima\code\modules\TGS\TGSIntegration\TGSChatHooks.dm"
+#include "proxima\code\modules\TGS\TGSIntegration\TGSIntegrationFiles.dm"
#include "proxima\code\game\atoms.dm"
#include "proxima\code\game\turf\simulated\walls.dm"
#include "proxima\code\game\objects\structures\window.dm"
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs.dm b/proxima/code/modules/TGS/TGDMAPI/tgs.dm
new file mode 100644
index 00000000000..855fdc42854
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs.dm
@@ -0,0 +1,483 @@
+// tgstation-server DMAPI
+
+#define TGS_DMAPI_VERSION "6.4.0"
+
+// 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 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 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
+#define TGS_EVENT_BYOND_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
+
+// 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
+
+/// 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
+
+//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].
+ */
+/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 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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
+
+// 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.
+ *
+ * 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.
+/world/proc/TgsVersion()
+ return
+
+/// Returns the current [/datum/tgs_version] of the DMAPI being used if it was activated, null otherwise.
+/world/proc/TgsApiVersion()
+ return
+
+/// Returns the name of the TGS instance running the game if TGS is present, null otherwise.
+/world/proc/TgsInstanceName()
+ return
+
+/// Return the current [/datum/tgs_revision_information] of the running server if TGS is present, null otherwise.
+/world/proc/TgsRevision()
+ return
+
+/// Returns the current BYOND security level as a TGS_SECURITY_ define if TGS is present, null otherwise.
+/world/proc/TgsSecurityLevel()
+ return
+
+/// Returns a list of active [/datum/tgs_revision_information/test_merge]s if TGS is present, null otherwise.
+/world/proc/TgsTestMerges()
+ return
+
+/// Returns a list of connected [/datum/tgs_chat_channel]s if TGS is present, null otherwise.
+/world/proc/TgsChatChannelInfo()
+ 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.
+*/
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/LICENSE b/proxima/code/modules/TGS/TGDMAPI/tgs/LICENSE
new file mode 100644
index 00000000000..221f9e1deb2
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/LICENSE
@@ -0,0 +1,24 @@
+The MIT License
+
+Copyright (c) 2017 Jordan Brown
+
+Permission is hereby granted, free of charge,
+to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to
+deal in the Software without restriction, including
+without limitation the rights to use, copy, modify,
+merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom
+the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/README.md b/proxima/code/modules/TGS/TGDMAPI/tgs/README.md
new file mode 100644
index 00000000000..6319028d810
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/README.md
@@ -0,0 +1,13 @@
+# DMAPI Internals
+
+This folder should be placed on it's own inside a codebase that wishes to use the TGS DMAPI. Warranty void if modified.
+
+- [includes.dm](./includes.dm) is the file that should be included by DM code, it handles including the rest.
+- The [core](./core) folder includes all code not directly part of any API version.
+- The other versioned folders contain code for the different DMAPI versions.
+ - [v3210](./v3210) contains the final TGS3 API.
+ - [v4](./v4) is the legacy DMAPI 4 (Used in TGS 4.0.X versions).
+ - [v5](./v5) is the current DMAPI version used by TGS >=4.1.
+- [LICENSE](./LICENSE) is the MIT license for the DMAPI.
+
+APIs communicate with TGS in two ways. All versions implement TGS -> DM communication using /world/Topic. DM -> TGS communication, called the bridge method, is different for each version.
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/core/README.md b/proxima/code/modules/TGS/TGDMAPI/tgs/core/README.md
new file mode 100644
index 00000000000..b82d8f49e29
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/core/README.md
@@ -0,0 +1,9 @@
+# Core DMAPI functions
+
+This folder contains all DMAPI code not directly involved in an API.
+
+- [_definitions.dm](./definitions.dm) contains defines needed across DMAPI internals.
+- [core.dm](./core.dm) contains the implementations of the `/world/proc/TgsXXX()` procs. Many map directly to the `/datum/tgs_api` functions. It also contains the /datum selection and setup code.
+- [datum.dm](./datum.dm) contains the `/datum/tgs_api` declarations that all APIs must implement.
+- [tgs_version.dm](./tgs_version.dm) contains the `/datum/tgs_version` definition
+-
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/core/_definitions.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/core/_definitions.dm
new file mode 100644
index 00000000000..ebf6d17c2a0
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/core/_definitions.dm
@@ -0,0 +1,2 @@
+#define TGS_UNIMPLEMENTED "___unimplemented"
+#define TGS_VERSION_PARAMETER "server_service_version"
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/core/core.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/core/core.dm
new file mode 100644
index 00000000000..41a04733945
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/core/core.dm
@@ -0,0 +1,156 @@
+/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 API version! Defaulting to latest. Update the DMAPI to fix this problem.")
+ api_datum = /datum/tgs_api/latest
+
+ if(!api_datum)
+ TGS_ERROR_LOG("Found unsupported API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.")
+ 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/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)
+ api.SecurityLevel()
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/core/datum.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/core/datum.dm
new file mode 100644
index 00000000000..68b0330fe86
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/core/datum.dm
@@ -0,0 +1,59 @@
+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/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
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/core/tgs_version.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/core/tgs_version.dm
new file mode 100644
index 00000000000..a5dae1241a3
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/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/proxima/code/modules/TGS/TGDMAPI/tgs/includes.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/includes.dm
new file mode 100644
index 00000000000..23b714f9d06
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/includes.dm
@@ -0,0 +1,21 @@
+#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/proxima/code/modules/TGS/TGDMAPI/tgs/v3210/README.md b/proxima/code/modules/TGS/TGDMAPI/tgs/v3210/README.md
new file mode 100644
index 00000000000..f96e7cf3b31
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/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/proxima/code/modules/TGS/TGDMAPI/tgs/v3210/api.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v3210/api.dm
new file mode 100644
index 00000000000..b881662d71c
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v3210/api.dm
@@ -0,0 +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"
+
+#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_ERROR_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/proxima/code/modules/TGS/TGDMAPI/tgs/v3210/commands.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v3210/commands.dm
new file mode 100644
index 00000000000..d9bd287465b
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v3210/commands.dm
@@ -0,0 +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_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?.text || TRUE
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/v4/README.md b/proxima/code/modules/TGS/TGDMAPI/tgs/v4/README.md
new file mode 100644
index 00000000000..78191447b27
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v4/README.md
@@ -0,0 +1,6 @@
+# DMAPI V4
+
+This DMAPI implements bridge requests using file output which TGS monitors for. It has a safe mode restriction.
+
+- [api.dm](./api.dm) contains the bulk of the API code.
+- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/v4/api.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v4/api.dm
new file mode 100644
index 00000000000..2f05c386338
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v4/api.dm
@@ -0,0 +1,312 @@
+#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]"))
+ del(world)
+
+ 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]")
+ del(world)
+
+ //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]")
+ del(world)
+
+ var/new_port = new_port_json[TGS4_PARAMETER_DATA]
+ if(!isnum(new_port) || new_port <= 0)
+ TGS_ERROR_LOG("Malformed new port json ([json_encode(new_port_json)])![TGS4_PORT_CRITFAIL_MESSAGE]")
+ del(world)
+
+ if(new_port != world.port && !world.OpenPort(new_port))
+ TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]")
+ del(world)
+ 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)
+ 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)
+ 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/proxima/code/modules/TGS/TGDMAPI/tgs/v4/commands.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v4/commands.dm
new file mode 100644
index 00000000000..d6d3d718d47
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/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?.text
+ return "Unknown command: [command]!"
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/v5/README.md b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/README.md
new file mode 100644
index 00000000000..a8a0c748e7b
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/README.md
@@ -0,0 +1,13 @@
+# DMAPI V5
+
+This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no security restrictions.
+
+- [__interop_version.dm](./__interop_version.dm) contains the version of the API used between the DMAPI and TGS.
+- [_defines.dm](./_defines.dm) contains constant definitions.
+- [api.dm](./api.dm) contains the bulk of the API code.
+- [bridge.dm](./bridge.dm) contains functions related to making bridge requests.
+- [chunking.dm](./chunking.dm) contains common function for splitting large raw data sets into chunks BYOND can natively process.
+- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
+- [serializers.dm](./serializers.dm) contains function to help convert interop `/datum`s into a JSON encodable `list()` format.
+- [topic.dm](./topic.dm) contains functions related to processing topic requests.
+- [undefs.dm](./undefs.dm) Undoes the work of `_defines.dm`.
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/v5/__interop_version.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/__interop_version.dm
new file mode 100644
index 00000000000..6ef7c86ef75
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/__interop_version.dm
@@ -0,0 +1 @@
+"5.6.0"
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/v5/_defines.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/_defines.dm
new file mode 100644
index 00000000000..a3f949081f1
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/_defines.dm
@@ -0,0 +1,115 @@
+#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 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"
+#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_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_HEARTBEAT 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"
+#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_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/proxima/code/modules/TGS/TGDMAPI/tgs/v5/api.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/api.dm
new file mode 100644
index 00000000000..517240f12f8
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/api.dm
@@ -0,0 +1,228 @@
+/datum/tgs_api/v5
+ var/server_port
+ var/access_identifier
+
+ var/instance_name
+ var/security_level
+
+ var/reboot_mode = TGS_REBOOT_MODE_NORMAL
+
+ var/list/intercepted_message_queue
+
+ var/list/custom_commands
+
+ var/list/test_merges
+ var/datum/tgs_revision_information/revision
+ var/list/chat_channels
+
+ 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 "__interop_version.dm"
+ )
+
+/datum/tgs_api/v5/OnWorldNew(minimum_required_security_level)
+ server_port = world.params[DMAPI5_PARAM_SERVER_PORT]
+ access_identifier = world.params[DMAPI5_PARAM_ACCESS_IDENTIFIER]
+
+ var/datum/tgs_version/api_version = ApiVersion()
+ version = null
+ var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands()))
+ if(!istype(bridge_response))
+ TGS_ERROR_LOG("Failed initial bridge request!")
+ return FALSE
+
+ var/list/runtime_information = bridge_response[DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION]
+ if(!istype(runtime_information))
+ TGS_ERROR_LOG("Failed to decode runtime information from bridge response: [json_encode(bridge_response)]!")
+ return FALSE
+
+ if(runtime_information[DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY])
+ TGS_INFO_LOG("DMAPI validation, exiting...")
+ del(world)
+
+ version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION])
+ security_level = runtime_information[DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL]
+ 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/RequireInitialBridgeResponse()
+ while(!version)
+ sleep(1)
+
+/datum/tgs_api/v5/OnInitializationComplete()
+ Bridge(DMAPI5_BRIDGE_COMMAND_PRIME)
+
+/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
+
+ 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/message, 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
+
+ message = UpgradeDeprecatedChatMessage(message)
+ 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(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)
+ 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(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
+ Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message))
+
+/datum/tgs_api/v5/ChatChannelInfo()
+ RequireInitialBridgeResponse()
+ return chat_channels.Copy()
+
+/datum/tgs_api/v5/proc/DecodeChannels(chat_update_json)
+ 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
diff --git a/proxima/code/modules/TGS/TGDMAPI/tgs/v5/bridge.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/bridge.dm
new file mode 100644
index 00000000000..b3cf7759397
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/bridge.dm
@@ -0,0 +1,95 @@
+/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/PerformBridgeRequest(bridge_request)
+ if(detached)
+ // Wait up to one minute
+ for(var/i in 1 to 600)
+ sleep(1)
+ if(!detached)
+ break
+
+ // dad went out for milk cigarettes 20 years ago...
+ if(i == 600)
+ detached = 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/proxima/code/modules/TGS/TGDMAPI/tgs/v5/chunking.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/chunking.dm
new file mode 100644
index 00000000000..af4cd6cc80b
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/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/proxima/code/modules/TGS/TGDMAPI/tgs/v5/commands.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/commands.dm
new file mode 100644
index 00000000000..a832c81f172
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/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?.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/proxima/code/modules/TGS/TGDMAPI/tgs/v5/serializers.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/serializers.dm
new file mode 100644
index 00000000000..7f9bc731b79
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/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/proxima/code/modules/TGS/TGDMAPI/tgs/v5/topic.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/topic.dm
new file mode 100644
index 00000000000..28fcc14aef8
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/topic.dm
@@ -0,0 +1,258 @@
+/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
+
+ 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_HEARTBEAT)
+ 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/proxima/code/modules/TGS/TGDMAPI/tgs/v5/undefs.dm b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/undefs.dm
new file mode 100644
index 00000000000..2e3b7ae7713
--- /dev/null
+++ b/proxima/code/modules/TGS/TGDMAPI/tgs/v5/undefs.dm
@@ -0,0 +1,112 @@
+#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_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_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
+#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_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_HEARTBEAT
+#undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH
+
+#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_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
diff --git a/proxima/code/modules/TGS/TGSIntegration/TGSChatCommands.dm b/proxima/code/modules/TGS/TGSIntegration/TGSChatCommands.dm
new file mode 100644
index 00000000000..12b95b0f551
--- /dev/null
+++ b/proxima/code/modules/TGS/TGSIntegration/TGSChatCommands.dm
@@ -0,0 +1,469 @@
+#define IRC_STATUS_THROTTLE 5
+#define NON_BYOND_URL "https://bay.proxima.fun/"
+
+/proc/Uptime(from_zero)
+ var/static/days = 0
+ var/static/result = 0
+ var/static/started = world.timeofday
+ var/static/last_time = started
+ var/time = world.timeofday
+ if (time == last_time)
+ return result
+ if (time < last_time)
+ ++days
+ last_time = time
+ result = time + days DAYS
+ if (from_zero)
+ result -= started
+ return result
+
+/datum/tgs_chat_embed/provider/author/glob
+ name = "Сервер 'PRX'"
+ url = NON_BYOND_URL
+ icon_url = "https://media.discordapp.net/attachments/1019304147671076975/1062958307096154172/Logo-Proxima-Mini.png"
+
+/datum/tgs_chat_command/ircstatus
+ name = "status"
+ help_text = "Показывает админов, кол-во игроков, игровой режим и настоящий игровой режим на сервере"
+ admin_only = TRUE
+ var/last_irc_status = 0
+
+/datum/tgs_chat_command/ircstatus/Run(datum/tgs_chat_user/sender, params)
+ var/rtod = Uptime()
+ if(rtod - last_irc_status < IRC_STATUS_THROTTLE)
+ return
+ last_irc_status = rtod
+ var/list/adm = get_admin_counts()
+ var/list/allmins = adm["total"]
+
+ var/datum/tgs_message_content/message = new ("")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+ var/datum/tgs_chat_embed/field/adminCount = new ("Админы", "[allmins.len]")
+ var/datum/tgs_chat_embed/field/afk = new ("АФК", "[english_list(adm["afk"])]")
+ var/datum/tgs_chat_embed/field/activeAdmins = new ("Активные админы", "[english_list(adm["present"])]")
+ var/datum/tgs_chat_embed/field/stealth = new ("Скрыты", "[english_list(adm["stealth"])]")
+ var/datum/tgs_chat_embed/field/useless = new ("Сотрудники без флага +BAN", "[english_list(adm["noflags"])]")
+ var/datum/tgs_chat_embed/field/players = new ("Игроки", "[clients.len]")
+ var/datum/tgs_chat_embed/field/activePlayers= new ("Активные игроки", "[get_active_player_count(0,1,0)]")
+ var/datum/tgs_chat_embed/field/modeReal = new ("Рассказчик", "||[GLOB.storyteller ? GLOB.storyteller.name : "Выбирается"]||")
+ adminCount.is_inline = TRUE
+ afk.is_inline = TRUE
+ stealth.is_inline = TRUE
+ players.is_inline = TRUE
+ activePlayers.is_inline = TRUE
+ //modePublic.is_inline = TRUE
+ modeReal.is_inline = TRUE
+ embed.fields = list(adminCount, activeAdmins, afk, stealth, useless, players, activePlayers, modeReal)
+ embed.colour = "#00ff8c"
+
+ embed.title = "Статус сервера Proxima"
+ embed.author = new /datum/tgs_chat_embed/provider/author/glob("Сервер 'PRX'")
+ //embed.footer = new /datum/tgs_chat_embed/footer("Сервер 'PRX'")
+ //embed.url = NON_BYOND_URL
+
+ return message
+
+/datum/tgs_chat_command/irccheck
+ name = "check"
+ help_text = "Показывает онлайн, текущий режим и адрес сервера"
+ var/last_irc_check = 0
+
+/datum/tgs_chat_command/irccheck/Run(datum/tgs_chat_user/sender, params)
+ var/rtod = Uptime()
+ if(rtod - last_irc_check < IRC_STATUS_THROTTLE)
+ return
+ last_irc_check = rtod
+
+ var/datum/tgs_message_content/message = new ("")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+ var/datum/tgs_chat_embed/field/round = new ("Раунд №", "[game_id ? "[game_id]" : "НЕТ АЙДИ"]")
+ var/datum/tgs_chat_embed/field/players = new ("Игроки", "[clients.len]")
+ var/datum/tgs_chat_embed/field/map = new ("Карта", "CEV ERIS")//"[GLOB.using_map.full_name]")
+ var/datum/tgs_chat_embed/field/modePublic = new ("Рассказчик", "||[GLOB.storyteller ? GLOB.storyteller.name : "Выбирается"]||")
+ var/datum/tgs_chat_embed/field/gameStatus = new ("Статус игры", "[Master.current_runlevel != RUNLEVEL_LOBBY ? (Master.current_runlevel != RUNLEVEL_POSTGAME ? "Активен" : "Заканчивается") : "Подготавливается"]")
+
+ round.is_inline = TRUE
+ players.is_inline = TRUE
+ //map.is_inline = TRUE
+ modePublic.is_inline = TRUE
+ gameStatus.is_inline = TRUE
+ embed.fields = list(round, players, map, modePublic, gameStatus)
+ embed.colour = "#ffae00"
+
+
+ embed.title = "Статус сервера Proxima"
+ embed.author = new /datum/tgs_chat_embed/provider/author/glob("Сервер 'PRX'")
+ //embed.footer = new /datum/tgs_chat_embed/footer("Сервер 'PRX'")
+ //embed.url = NON_BYOND_URL
+
+ return message
+
+/datum/tgs_chat_command/ircmanifest
+ name = "manifest"
+ help_text = "Показывает список членов экипажа с их должностями"
+ var/last_irc_check = 0
+
+/datum/tgs_chat_command/ircmanifest/Run(datum/tgs_chat_user/sender, params)
+ var/rtod = Uptime()
+ if(rtod - last_irc_check < IRC_STATUS_THROTTLE)
+ return
+ last_irc_check = rtod
+
+ if(Master.current_runlevel == RUNLEVEL_LOBBY)
+ return new /datum/tgs_message_content("Раунд еще подготавливается...")
+
+ var/datum/tgs_message_content/message = new ("")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+
+ embed.colour = "#ff003c"
+ embed.fields = list()
+
+ embed.title = "Манифест экипажа на сервере Proxima"
+ embed.author = new /datum/tgs_chat_embed/provider/author/glob("Сервер 'PRX'")
+ //embed.footer = new /datum/tgs_chat_embed/footer("Сервер 'PRX'")
+ //embed.url = NON_BYOND_URL
+
+ var/list/msg = list()
+ var/list/positions = list()
+ var/list/nano_crew_manifest = nano_crew_manifest()
+ // We rebuild the list in the format external tools expect
+ for(var/dept in nano_crew_manifest)
+ var/list/dept_list = nano_crew_manifest[dept]
+ if(dept_list.len > 0)
+ positions[dept] = list()
+ var/depString
+ switch(dept)
+ if ("heads") depString = "Командование"
+ if ("spt") depString = "Поддержка командования"
+ if ("sci") depString = "Научный отдел"
+ if ("sec") depString = "Отдел безопасности"
+ if ("eng") depString = "Инженерный отдел"
+ if ("med") depString = "Медицинский отдел"
+ if ("sup") depString = "Отдел снабжения"
+ if ("exp") depString = "Экспедиционный отдел"
+ if ("srv") depString = "Отдел обслуживания"
+ if ("bot") depString = "Синтетики"
+ if ("civ") depString = "Гражданские"
+ else depString = dept
+ for(var/list/person in dept_list)
+ //var/datum/mil_branch/branch_obj = GLOB.mil_branches.get_branch(person["branch"])
+ //var/datum/mil_rank/rank_obj = GLOB.mil_branches.get_rank(person["branch"], person["milrank"])
+ //msg += "*[person["rank"]]* - `[((branch_obj != null) && (branch_obj.name_short) && (branch_obj.name_short != "")) ? "[branch_obj.name_short] " : ""][((rank_obj != null) && (rank_obj.name_short) && (rank_obj.name_short != "")) ? "[rank_obj.name_short] " : ""][replacetext_char(person["name"], "'", "'")]`"
+ msg += "*[person["rank"]]* - `[replacetext_char(person["name"], "'", "'")]`"
+
+ var/datum/tgs_chat_embed/field/depEntry = new ("[depString]", jointext(msg, "\n"))
+ embed.fields += depEntry
+
+ msg = list()
+ return message
+
+/** -- Отвечать на тикеты из дискорда? Я подумаю над этим
+/datum/tgs_chat_command/ahelp
+ name = "ahelp"
+ help_text = ""
+ admin_only = TRUE
+
+/datum/tgs_chat_command/ahelp/Run(datum/tgs_chat_user/sender, params)
+ var/list/all_params = splittext(params, " ")
+ if(all_params.len < 2)
+ return "Insufficient parameters"
+ var/target = all_params[1]
+ all_params.Cut(1, 2)
+ var/id = text2num(target)
+ if(id != null)
+ var/datum/admin_help/AH = GLOB.ahelp_tickets.TicketByID(id)
+ if(AH)
+ target = AH.initiator_ckey
+ else
+ return "Ticket #[id] not found!"
+ var/res = IrcPm(target, all_params.Join(" "), sender.friendly_name)
+ if(res != "Message Successful")
+ return res
+**/
+
+/datum/tgs_chat_command/adminwho
+ name = "adminwho"
+ help_text = "Перечисляет администраторов, находящихся на сервере"
+
+/datum/tgs_chat_command/adminwho/Run(datum/tgs_chat_user/sender, params)
+ var/list/msg = list()
+ var/active_staff = 0
+ var/total_staff = 0
+ var/can_investigate = sender.channel.is_admin_channel
+
+ for(var/client/C in admins)
+ var/line = list()
+ if(!can_investigate && C.holder.fakekey)
+ continue
+ total_staff++
+ if(check_rights(R_ADMIN,0,C))
+ line += "*[C]* в ранге ***["\improper[C.holder.rank]"]***"
+ else
+ line += "*[C]* в ранге *["\improper[C.holder.rank]"]*"
+ if(!C.is_afk())
+ active_staff++
+ if(can_investigate)
+ if(C.is_afk())
+ line += " *(АФК - [C.inactivity2text()])*"
+ if(isghost(C.mob))
+ line += " - *Наблюдает*"
+ else if(istype(C.mob,/mob/new_player))
+ line += " - *В Лобби*"
+ else
+ line += " - *Играет*"
+ if(C.holder.fakekey)
+ line += " *(Скрыт)*"
+ line = jointext(line, null)
+ if(check_rights(R_ADMIN,0,C))
+ msg.Insert(1, line)
+ else
+ msg += line
+
+ var/datum/tgs_message_content/message = new ("")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+
+ embed.colour = "#ff0000"
+
+ embed.title = "Список администрации на сервере Proxima"
+ embed.author = new /datum/tgs_chat_embed/provider/author/glob("Сервер 'PRX'")
+ //embed.footer = new /datum/tgs_chat_embed/footer("Сервер 'PRX'")
+ //embed.url = NON_BYOND_URL
+
+ var/datum/tgs_chat_embed/field/adminCount = new ("Админы онлайн", "[can_investigate?"[active_staff]/[total_staff]":"[active_staff]"]")
+ var/datum/tgs_chat_embed/field/adminList = new ("Список администрации", jointext(msg, "\n"))
+ embed.fields = list(adminCount, adminList)
+
+ return message
+
+GLOBAL_LIST(round_end_notifiees)
+
+/datum/tgs_chat_command/notify
+ name = "notify"
+ help_text = "Уведомляет вызвавшего по окончанию раунда"
+ //admin_only = TRUE
+
+/datum/tgs_chat_command/notify/Run(datum/tgs_chat_user/sender, params)
+ if(Master.current_runlevel == RUNLEVEL_POSTGAME)
+ return new /datum/tgs_message_content("[sender.mention], раунд уже закончился!")
+ LAZYINITLIST(GLOB.round_end_notifiees)
+ GLOB.round_end_notifiees[sender.mention] = TRUE
+ return new /datum/tgs_message_content("Я уведомлю [sender.mention] когда раунд закончится.")
+/*
+/datum/tgs_chat_command/fax
+ name = "fax"
+ help_text = "Используется для просмотра, создания и овтетов на факсы. Используйте без параметров для помощи"
+ admin_only = TRUE
+
+/datum/tgs_chat_command/fax/Run(datum/tgs_chat_user/sender, params)
+ var/textTrue = copytext_char(params, findtext_char(params, "```"))
+ if (textTrue!=null && textTrue!="")
+ params = replacetext_char(params, textTrue, "")
+ textTrue = replacetext_char(textTrue, "```", "")
+ var/list/tmpList = splittext_char(params, " ")
+ var/command = tmpList.len ? tmpList[1] : null
+ if (command == null || command == "")
+ return "Дорогой [sender.mention], пожалуйста, ознакомься с тем как использовать эту команду.\n\nИспользование `fax комманда, аргументы(через запятую!!!)`\n\n__Команды:__\n\n1. **view \[ID\]** - без *ID* показывает список факсов полученных админами и посланных. С *ID* - показывает конкретный факс и его данные.\n\n2. **send \[\[FROM\] \[DESTINATION\] \[TITLE\] \[STAMP\] \[LANGUAGE\] \[HEADER_LOGO\] \[FOOTER\] \[PEN_MODE\] \[TEXT\]\]** - без аргументов - получить подсказку по написанию факсов, в том числе доступные __адреса для отправки__. С параметрами - написать факс все аргументы обязательны:\n*DESTINATION* - куда отправить факс (адрес должен существовать)\n*FROM* - от кого, ЦК, НаноТрейзн, Родная и любимая Бабушка - все что угодно\n*TITLE* - заголовок для факса\n*STAMP* - нужна ли печать (учтите печать принадлежит адресату то есть может быть 'Печать Квантового Реле Родной и любимой Бабушки'. Значения `TRUE`/`FALSE` или 1/0\n*LANGUAGE* - язык на котором написан факс, должен быть в списке\n*HEADER_LOGO* - логотип для заголовка. Так же может быть в значении `EMPTY` для заголовка без логотипа и `NULL` для факса без заголовка\n*FOOTER* - нужен ли мелкий текст (...уведомите отправителя и ЦК если хешключ не совпададает ля-ля...) значения `TRUE`/`FALSE` или 1/0\n*PEN_MODE* - зачем вы пишите факс мелком? Вы клоуны? Значения `PEN`/`CRAYON`\n*TEXT* - сообственно текст факса. Форматирование такое же как и в игре, НО перед текстом и после него добавьте ```\n\n*Использование тэга \[field\] недоступно.*"
+ else
+ params = copytext_char(params, length_char(command)+1)
+ var/list/parampampam = splittext_char(params, ", ")
+
+ // Копируем инфомрацию по логотипам из /obj/item/paper/admin
+ var/obj/item/paper/admin/adminPaper = new /obj/item/paper/admin( null )
+ var/list/logo_list = adminPaper.logo_list
+ logo_list = logo_list.Copy()
+ QDEL_IN(adminPaper, 100)
+
+ switch(command)
+ // Просмотр факсов
+ if("view")
+ // Без аргументов
+ if(parampampam.len == 0)
+ if(GLOB.adminfaxes && GLOB.adminfaxes.len)
+ var/list/msg = list()
+ msg += "**Вот доступные факсы:**"
+ for(var/i = 1, i <= GLOB.adminfaxes.len, i++)
+ var/obj/item/obj = GLOB.adminfaxes[i]
+ msg += "№ `[i]` | [obj.name]"
+ return jointext(msg, "\n")
+ else
+ return "В этом раунде факсов для админов не было"
+ // С айди, и айди существует
+ else if(text2num(parampampam[1]) != null && text2num(parampampam[1]) <= GLOB.adminfaxes.len)
+ var/obj/item/item = GLOB.adminfaxes[text2num(parampampam[1])]
+ if (istype(item, /obj/item/paper))
+ return paper2text(item)
+
+ else if (istype(item, /obj/item/photo))
+ return photo2text(item)
+
+ else if (istype(item, /obj/item/paper_bundle))
+ var/list/pack = bundle2text(item)
+ var/i = 0
+ for(var/string in pack)
+ i += 10
+ addtimer(CALLBACK(world, /world/proc/TgsChatPrivateMessage, string, sender), i)
+ return "Отправлено в личные сообщения [sender.mention]"
+
+ else return "Не удалось определить тип факса. Это баг сообщите куда подальше. Тип факса `[item.type]`"
+ // Нет факса с айди
+ else return "Не удалось найти факс под номером № `[parampampam[1]]`"
+
+ // Отправка факса
+ if("send")
+ // Без аргументов - подсказка
+ if(parampampam.len == 0)
+ var/list/msg = list()
+ msg += "__**Помощь по адресам и логотипам**__"
+ msg += "__*Доступные логотипы:*__ NULL | EMPTY | [jointext(logo_list, " | ")]\n"
+ var/list/selectable_languages = list()
+ for (var/key in global.all_languages)
+ var/datum/language/L = global.all_languages[key]
+ if (L.has_written_form)
+ selectable_languages += L.name
+ msg += "__*Доступные языки:*__ [jointext(selectable_languages, " | ")]\n"
+ msg += "__*Доступные адресаты:*__ [jointext(GLOB.alldepartments, " | ")]"
+ return jointext(msg, "\n")
+
+ // Не хватает аргументов
+ else if(parampampam.len < 8 || textTrue == null) return "Недостаточно кол-во аргументов. Требуется `8` (без send и текста) получено `[parampampam.len][textTrue == null?" и текст не найден":" и текст был найден"]`"
+ // Отправка факса
+ else
+ var/from = parampampam[1]
+ var/destination = parampampam[2]
+ var/title = parampampam[3]
+ var/stamp = parampampam[4]
+ var/language = parampampam[5]
+ var/headerLogo = parampampam[6]
+ var/footer = parampampam[7]
+ var/penMode = parampampam[8]
+ var/text = textTrue
+ // PARAMS CHECK START
+
+ // Destination
+ var/list/reciever = list()
+ for(var/obj/machinery/photocopier/faxmachine/sendto in GLOB.allfaxes)
+ if (sendto.department == destination)
+ reciever += sendto
+ if(reciever.len == 0)
+ return "Не удалось найти ни один факс по адресу `[destination]`"
+
+ // Language
+ var/datum/language/lang
+ lang = global.all_languages[language]
+ if(!lang)
+ return "Не удалось найти указанный язык `[language]`"
+ if(!lang.has_written_form)
+ return "У языка `[language]` нет письменной формы. Укажите другой язык."
+
+ // Header - Logo
+ if (!(headerLogo in logo_list))
+ if (!(headerLogo in list("NULL","EMPTY")))
+ return "Не удалось найти логотип `[headerLogo]`"
+
+ // Stamp
+ if(stamp == "TRUE" || stamp == "1" || stamp == TRUE)
+ stamp = TRUE
+ else if(stamp == "FALSE" || stamp == "0" || stamp == FALSE)
+ stamp = FALSE
+ else return "Неизвестное состояние штампа `[stamp]`"
+
+ // Footer
+ if(footer == "TRUE" || footer == "1" || footer == TRUE)
+ footer = TRUE
+ else if(footer == "FALSE" || footer == "0" || footer == FALSE)
+ footer = FALSE
+ else return "Неизвестное состояние нижнего текста `[footer]`"
+
+ // Pen Mode
+ if(penMode == "PEN")
+ penMode = TRUE
+ else if(penMode == "CRAYON")
+ penMode = FALSE
+ else return "Неизвестное состояние ручки или мелка `[penMode]`"
+
+ // Text
+ var/t = sanitize(text, MAX_PAPER_MESSAGE_LEN, extra = 0)
+ if(!t)
+ return "Ваш текст был уничтожен санитайзером. GGWP"
+ t = replacetext_char(t, "\n", "
")
+ t = replacetext_char(t, "\[field\]", "") // No fields sorry
+
+ // PARAMS CHECK FINISH
+
+ // Fax creation
+ var/obj/item/paper/admin/adminfax = new /obj/item/paper/admin( null )
+ adminfax.admindatum = null // May be it need to be reworked
+
+ adminfax.set_language(lang, TRUE) // Нужен только язык и Лого. Остальное, де факто - мусор
+ adminfax.logo = headerLogo == "NULL" ? "" : headerLogo == "EMPTY" ? "" : headerLogo
+ adminfax.origin = from
+ adminfax.isCrayon = !penMode
+ adminfax.headerOn = headerLogo != "NULL"
+ adminfax.footerOn = footer
+ adminfax.info = t
+
+ // Это делается при отправке так или иначе
+ adminfax.generateHeader()
+ adminfax.generateFooter()
+ adminfax.info = adminfax.parsepencode(adminfax.info, null, null, !penMode, null, TRUE) // Encode everything from pencode to html
+
+ if (adminfax.headerOn)
+ adminfax.info = adminfax.header + adminfax.info
+ if (adminfax.footerOn)
+ adminfax.info += adminfax.footer
+
+ adminfax.SetName("[adminfax.origin] - [title]")
+ adminfax.desc = "This is a paper titled '" + adminfax.name + "'."
+
+ // Я не знаю зачем так сложно с печатью
+ if (stamp)
+ adminfax.stamps += "
n1P?f@VvI3p6kmp6OI8Mzd&t6)kw zn8NK!oI@6s32TSb(@4-n2mGvT%TYdpx|baCKpD@!TiWxfx+!A~*Svz0*YGK7oeSmG zbptAO0k41jxBaw{!>DSCw46+?oIt?XjQd QALg7x@8 zzdW*sG&Q058m)J$#j`fG)I#T}WN<}ebv>HJRj_%2uT4+-9-)LITX#42%5{-PiEn?o z+q&|BCSm*>y2p8-mQjKm*{D@jPQOrzx1g%Qye;nc#F@c*#=J1y8eAP8|8zf!5px2P zbGjrM(r{|cVb!7h0Lf;+veN5em12V4I@}jgnN~(EaPMz;*w~@(k>ViGCr6dg4QZR? z$*u~!d>7`8wZmut%)QB)eHPQx1OcCqH`H3+GN)%Bb(FP)9BnjUFB%8cQCr9$Oo%Fe zg?z>Bo5Zw*NrrV-`zOm+w}wvBjPtLv@zS=jf48ir>$*Cwq!wh-K4`_#G$TSnjapKX z7TiYZ(-X4k1tC-SXgj23`;ZMYf#><-iP0kp3)QvppDRxbskC2>#6S#3VmLF~W!!B( z{@;r%N$ld*Y{@W{s%ZzB8#FLW{Y?Lj9-PjxDS>{&xebLN#ZO7L8UzP0d?b@KmPf%n zww`hZ8VNK&P_NyH`Y1> 0PV5F4*)R@ndYGGv-l722t zX2wCq8hKM<<+MJTyuP`x`B(Y9Uv-*M05Kiw9J(I5a$ku(h;7`Ua0X(!of9ljFV1 z2iHNQt4`1=yJql>33Y}za`em@wR2JvS1F@=#TK#~L!X!II-lYXYDC#By>__Ras_y= z*XWMXSG)f!JsWtpUW2uK4cI;V+jd+AguuSb7Lbe|mH`$12UVVZ8k(bs MN0XFqi~k^FH%dJ)(HUvF9B|stSP04z&8#@)uS6 zZonz6RjpI7088Eb*1)%_cvtT@V0bZ)6)o2s(2`ETl38s !$Uag>PR& h$=(}gf2ScrrxFl&@cscYTHzxJPt 1Z+)>NIijNa^M|4QE5q&-5^MgS})jc1^Q+)-gl1 zY^wv(xiy+T`xFGUipsG3WbcFNN$nAaQp_+N-y6nGbK8G)61NHso8bA39(K{Pg;N{0 zZpLx)_Z>t~L*=kizo$GJj$Q8rESubU*~g4LbDw`LJHn~re$L(aF4$*Ct`2Ei)G>bv z7@;~dtG1ma->>zqCD)4V^!|2J`yy2`d@J%&!d&JRnZJdS1(|>*Na +AX0-_N#9<%L4CZMz9C+hR8?fGzOq?n$zvk?D&(rZ0 zJP*t&cb$<{(eI9#rF)Ef5L;y)ymOk*L@X3jE3Kz0z(fTFm8gVc?aPFBg4vYj8o>t? zPh_8 >Haj1Rukt+h=9i*IpV+c81@(0AY6Nn)^xtiJu4 zUFo>yASJQN9r&OgK&@)#bqTEbaNFu?JaD6`z0oT#%{k%Y*W~4&N>v8E-&lGp&fY!5 zV+^x$p4`;mY=u)rogViLR&YceXH*}?R#cqT^$xj=6_%QQOiQrC*KgAa-e}IKj${V2 z{$tZ2H!5=1-kQ6AA5ceWvt-V(TRqF*-w;q1o%ykjer}of)WgC5)t}LF^aB6d4>pID z>~fJ`h| LC|y`(z~(sW+7?7Uo~OgiOk&2M^bhU7Vw9!~~!e%Nk) z;l9stRyJ38Cr~vrEU`$j-BL*ucU6(WeQr)-->fQZRi*m#890@OIvToO@@0ModWF*n zny~E_HAJ^^`7qGfcbe{3w8Z!0k+f^A5T2;vd4QI?p5=W52J%YutKIi}T+;=0j3D1F z*W5X8ReDT&LmR{HJ^&b$JOL|nj?%N&UE!i5{C&rCm)dA$W;}KJr%aD#5DnM52w60T zKS&fEbJUdg+huw@;C-ekn1!)NyO~D)E9{zMgx31k5RNvz=UrS>D^fOJj1|o|rT5Y= z`!tRY2(Qr}R$k^*N*?>uYPS2+*Oc`yIJpW)pLpRv-3+v(O=K7F $6 zq>w4;+NB6IuYTI$<=!iH1CB1LHbw ?uO)ES0(D!&C-WC?+E3M4ia6y&od05eBE~f zzuPo99bVohF%`si_X(r!7i_x|gIUTOQM#2KDfi}%0x-~;O34t`UY^>!rpLg;$$P;N zm_fzr|7PK9KjNUYwEN06jYizVX6yUE`PbYqzsS?&rQc3R-MuwigStNCBEa)0^a8UF z%+nhXO`F$ZIM&|8$_keROgdP7)RgZ<9 Cec^ku~PF% zz=siP;0s~6%8 ^d>Hnak1SHYkY2cgGdp^( z*1Hft&~V;K&cNbv!=TQM7CKVUm*+CB3(5&P3R0}?G;JFI6qGId<;2nX?};hrAW2aZ ze-BC%Ybq;8==xAq(3hi3L=z sb608`5W|C zMTcCu%36C=HTmtR(b>z<`dY%X#%u0|^}E#Z6}u-b`{VFVaIz(Cx|vl Dp)mAx93U5xxvH(vRK|Flp4#I1Q>;BcTbGC73baiMmAtWpfS=9*v-=#gyc z$WvMBr|zUGhR6 j8I&i)SP=5p%jo7WQB~o${fT zhupF!DeQ#vQ9$;q;&pAUWSTDx!<{kHh*Z;LzVF?qY6l{(ogLY&6Z`!id+tXbWq)g1 za@-p9X@ ygoECB&=k!rr%!5TfvZbbw{gfaL%F`uk@38H3pu-~7P1S}&z*m@RI8 zSKeT @0SxH`yc4GbAjLSKvt?WVne9?nT(qx9+6e3hDLxTDzx%>A)^L1Hlk9UXp z94$qoa5R1&sL*jtoiL`b*-3jY-XJnRgl^1gA*`!h87X)5!X1t&xX=yV?6`9|X~w`C zFcrb2A!6&dD|Jb~42G-pdc!b4v$y}b<-{)J*u6M5pZxpg!>-;tEnzC5*JJd+s!tqO zCNrb~qBvK2*{!XMe%(s>myl m>JFxzp~k*XZn{JuJipUzd y zuwnmpk6K{W?EMLq2l0j?8&B9nOD?r1Uudrb5-p0|V_Ic?*$gubR(}B}@T9a DiUO?ZKgYlL~xqwzd6A#Ww?lev_<}+Gch|&vH7t?mdXIvUzBO zI~dX9p5Ui_5?#8{H9p_U^wP{C+?#A^ENz>-F;9Jr%2N@dSPBzI&ux9!K^@fc-%THH z6UL2l!{pGX8gp=`Eop8p?@f!c1%LGo(H5J!kb{u6hopGwA6lmN2CV~Bdqy{}8lk@* z&;(0?Z4l;HAhwPyX)5a?HdcNOt)@sO@@vc{fMB{$?Fx?{rAto9p_d|RH=bs3 P zTgzsihV`G}M(lr5qW-s6{av0>`FZP^z-a@K7jtzmD-1+A?5&3mRkq;$fVuLg8y^+% zkh1=R;YB3R!+8BBHR2Bp1pD}ma1aI^t8sabBS0)eh{p4)MExGD2;Bt*B_@iNZM0A~ zVRUjtx^6I_{I!fsOX|)Rb-~kl&n2{KVPCTl%7Kq#bax;)?Oxl`^rC4NKnQhEJemtm zyxLjCi!)Y02Y%}yK9dL{MgoJ}{Bet~d1Mf=GmC!-97N9zcO(w8i1m_;9Bi@6uKdwK zOs5o$gG7H+OWA{e4 ZJmM?{_dQ;Q}%Rq?!bLahCznCP`?Jy{Y!JaD;D~=b}`Rr zsjd~bXwTJLmqqwg1MHp`ZF%C0{lzCQVUlFWD2kY03RewwqHv%3V2|iU<3JP`FYxPL zf8)l7pCX8M8%D(U>CIBa_uRz7JBP?!19`$g-|cES)9=AtdT~P^gaB&bwbbtocjr6r zf69fjnPt0@38CCIXm;j1!A1IWGoUN!Xr_Wliy&Ej2Ru?fDOgk?vrZ+Y->D7%^V2;f zCDvVUo>i$(T5g6I@DyrMO!+F){L$es*I2nCu6An&xzVKbHS1VD&yDG`kH=UEp;?U3 z;~&m^k6kUE_cZj-CWp;!sU`{JA>-~*l#sr)21PV;YE^#|1mANFJ;<=y+M!M*^dmk# zMbFRC3-l*vbUflV3)#{CwMvXQBlTJ;>P+%{9hdV6_bkAzlT)ktm99MQjZpZlG!HIy zfPgjbIZ=K~$IihfR5ZYy$5+s3@yGeJyV_vQ&3)K{twMcgl9BC;T6Th%_Q$GdX6d)x z!yt2j*<3jjd*|bwd@hNFO7ht2ecO3&sJaHL1hT#NZ^3LiX=WZ|iD`T&g~Z%>y}nex z6kHQ|C?3AVP-UgzL>Ha{(w4TGHXzu$F`#{lesX*s-Q)V?R6-p6;MIWeT`if}n9DW{ zZ!F7)!UVuJZD{$q*kL!`d5(aH`Em<~lJQTj+hcjz=G9MlyT#Z?*a1Lwbm}8~4b)_* zR?6_#eHfDyoe4M0NZnLRbQrt8qh8((O(hmsm~m;4OxDM!cV5S9;EU`W*^+aDp|ze; z96UyjN;C=b6|v}aciTxa8?yat!kjR4`DQ)$u|FAQdZk1HFw_+AyyTNf-J@*9c{28_ z&9s~PAI}cv^Xz-y_tPny=GY25*NT||LU9JWqz#Z^m?$1)(wO>-%z68Q4E$ul93b|# zZxgje`gl?(|K@zc+ImYShD`J$SRpfW7+6 @X%72No z>*|q4KnB|BCIE2YQnb+;fO7A;qcbOJ7=BU$q|Yq$dhhvJJX+m>AhAUL2$7w{4=9Hw z302M(DB@p5to$-702G%He>Zb9*Jc@9UM5a4Ja;;FY5eoB5x|CVx=C~FSMk@&qX60T zoL+XALuWp*(Sn~5oMgapdKzj$o|z4KY0z#&(9_HyFSY6BC|h!gXhzEmCsk!ZcScx0 zx8v9bvw9B@DUuS0@ej*`YQQVWTar4tKdT}Ju2Vw{SL_-;-kb3_%bn1nk{RaE-WXab zQ6U=|VagQKFu+01mg_(U%dR*z6-(iN{8!CQbdV>!|DL-U|HWtXXz(XncsoubmprqN zI8VKN@kjCdjib9@AfL@sK6Ohw7f=5J%%~~+hw6gffm^yn{6Jb8^v+`&$NT+E8vGm( zd$Oq1#QRte?1)nuD(zn5t8gI{MW?<>gEp=c{9f@Ji{K&R!vEs!<2c@Kfm=mKs!Ig5 z_U3MwO$XLcWNm{wwDRbe&HY~FflE-YlTas-lE9@(wI&Y~ugQ4(_`x`z{}US%KF`0h zyluMw)1#eya#_p99Kh9yKODgQadAG3QTK5E (YCo_+inB-ju5$s&y8RNZbDI^fn^41An~SD$GXKjrV!~CN zH2tDW A2`C!7a*F8dxFyW3Gf#GxpgkL2z z=0Wm0Y~g*xTto0Hf(o|kJdnQ_8ve>YW5-Bo>nz}AlOb+XwF_LOF2JfjCWCZRA^BVU zsB$d+MXouMqCE%4hZEZl44rc-=0}tBWB3Uhm6vx%V}NpEteUGa$By<^wcZx?1boWd zs$ieJ$2F(SQA^JAm=u*zw_D_gYwEZt-%(#Cf^-01BszQPs194D^b-DnlKSZ-lgfVe zgJ$oFL<(J#fp IC_XmZ#@qJU*5ez7taYB zM7B&HJ=+9hGbR>6v1@#7k8cBmkf?K{alZc#ywiZb0DATXzDLtt%kq{hP84^sdIAi9 zM&qy`^+AFb<$1yV+0c_eii !K95lQ4bv`z5Xp((S|f&os0xhwpkzyYA)!{-%S zOR%?p1dI?CfkA?ZYWIy;DETubX)F#trK6v8xnjCzDLnB9+S@VeEefPc06)9aiT2tn zO!q7m;ms0$I~|A 9a)mqxh?C^tMIE! g;mc&;`}jzx#{=Al`P49SDW z L!QNg8!!hg*@IP{EK_MyBDT6DUIhUs<+`O^GyNv zIa0)k^lknMwcm2(Pnv=JazqkF{DXAThS;hP>BcYtokuSp4_tNMJ-EEQAI;2vx|!fS z#{>R-a-6F};Bd2Pf6aqhAYnTLcwt&UCdM`(H&AReKwyBl>9ksWO}RT|2fnZD QG%tOLbkQFBwI_raH`(d(rT*M=r}GZOGn5U*owz@ zck=T8YU(=!nhKhBLyz>{AyiRmO7Ecx2!f#WUIjsVF99inw17zOO;G6|9YXINX-cm` z=rx2uZhY_iefR!Jc4z18IoXr5J2U&tbN1m2B5V;O@4AHpVhg=)5KtzaKp`bKY{8Ay z FV4wb z#qeiM?oIPF@^}BcU$5UdnhNTF6VM42I>+I3>TX5!-8&8Ei+fY_x_Z8RbNd0Wh8^Pb z;efP4g#{YwcVA?DTkUu~Xn=JTURlKP>SU{ -+{&DQhMSqXq6EBvhRm>Txfs$CfxA&4y08Y(C@dYWr0lX>w zU5N5fR;Iyx49dTRQOnE&sU@ew2gtz~-UWZz?(^oe&oDQZwM t&(3$oVu}6; zKj6SDk0oc&H~K>YKa1s-0~AtFuiuW>qx3{dOzwO3QB+T=0X7pMivD^`wHv6i`L^kM zjvfsIU}8?8x0Mf#+^6ExVxt?o(zPcDyVAcOav^^b*&}EyuNjv8x&jpB2|=6j6x?O! z8ZqI{hc1rK|6+`|`M0*-5B>yjN05J| xBSF{5a<8!N1PALK lqaH!X6-kWwkm;5Ag2QAZQIR0UNk@nwgc=N7m)C}A zmoL~w^0&$-Z~u17^Jig_`Mg11XPq<303ZTngkZ8{79&XbtFw?Vs4&Cc)GV%`!PGoOG$QL7bEE1}lHE zJ0J-UFkl;7T(9VHF5VKHe*$u+N31f%2mJ+x6#XIUJ*H93e{huTur(mB2_vI6k7h(7 zYI=lwl3dQ>l1VFaj$IEg`##We9ShE5Yf>p@3#x~DoNM!$%g-RJuo47BFM=?aGB0Ps z0t+tXH@sHI48!AbPaB0pIxy88KinsAaWPHP_*tz99+3^j{e8D aD`64M21jJ}Rr!lV^O9!5N?e`}qUa9YR6=%2$?v?|pZS96(0%$GlsCbC zUP_KA2P}LvDV>=ulTz*z{;fC^6C-Lw@Ov?h#1+cK8Sm-?gPw0_BoPJ{&{;;_(lhN* z6FlGBG*|99$$i>`_aze4Sq5wgKirA>UlknwJBg3*M`4vNl4V2@K*a;L2(Q@F>tJo- zcbQrvbOd7-enJs@;@Jyth2=W#N0{`N7XlR0y_W>DS$Ym+AMB27i05{U*O|{q3A*=O z>;+e&*dQZLOusoxy{d&TNYZ8zs=r!$_^P%mdfxfJ9nO*(SiL}zF}<^Y+5AKt{7}i# z7ta7JL^|@B=4`OnV*`iu(>ck98Jyb(l7?VG7&Bsm-KvA#@^CGU3go7i?Fuv%FU?~< za}Cx1mW%h17W5Y4t}uu9un#LJl6PyR ~ zc5sunhW&@#oW!6H&&&bKI0#jeg54|C9`iT@fJyD)K-K#p{y0J~a+c^lU+*9vg%!c5 zhCQSAe#G<2myv~V;v2~uMJp_2UbZJ)HF_UhHmV9qpY~fQsQz6zcdfCC?Md+>Fk9T6 zxaTM2fvPvA%{&?spabGqHFi(5@DEMcOdmSYf#FwNU`(SgTd9S3@1T?d$sGE|J{9Yc zM`%?N!7fp2XV%E@iQS2T9oBOcjZx^-xoa2p7Z)mmo^z*VaQ^)ZligkTy<+|(YPO5r zgyFI{GXvpzLuRtO4Zojqfq1zq wml=Jb*;QSBFM~1YM5k%r)rjVQM z;M;>b=pliA-^So_FL=XbwU&fkSZV=CxoLsq|3fac+4c?r6Ww@%DLVC6d7j(62r{}Z zCYYtFWsONLv=(y!=(D|z^J&)YcvdEpmX? Kg~F`&*Co=yV e(oHsG~3lyuO4>)`TTMEH*pl{JtAs`_4HJ0*^+2M1QKKZ7uNhj&Cv#CJkw z3d!&{ VH5Et_kof?+y}@ce5^M@~KOt#N9{A^8w1!#GG8zH5 zmfjCm&+R+#HG~HmI&3}Vf1h+>__>4vK|l)272 s>(l^u%CXu!3lgVDG&4 z=cFjf=PK;%5RFEDsmiZ@geaUe@MF@Y`DJ@I84T~i{fnDsf$FN-%nZ8Vq0h93b9HG= z`{`a-lk)`kA4c-12|oT{GID!wMTfYU(Z*mtLueuePh8DWE!$d(b7U&>Wk_ykkMG=j zJA*G=E$(#JsuC=qTaSkS9jI- KS H8lH9?%&AYx<-6msHKcQJ01a!RKT&bkP$J83}> zyw7}x#0PJ+&(S5^3(; G9LQT<2b^qFHvxIzsA%_QSi HS~=14?`ARb_ 1t;i4gHB~5 zEebY#;rX=5OQrTi1A~hGjPqxOCx?+GdDryOupkk*SN;rkWwWa?u+=PdQ-V++=0=;q zyxhGsbD4m9X>&Tp_XX*NCpd1;DBk+$&Ry3X=jML>Zg9hih1xYhd%khAOBP n>n zVJwx`@>k|O>iBBn cdy$ zx2KM(b@TX263re-y3u%hR5W^}dH~A#v+h9hZ4fOALv|B9bCR-v=H!_ympB|6N+) z$z9fm{6}B>ebuS`zzk`^_~GqQ6>0j#IbABr*YwXf1G!Fbj>4IC&78J}5%AlhzE3hY zRcVL{K=(VVu093HD!Kxqr-X7x7}_ZG-R-=JmD=F2Ha{1jT#V2_9|8m&ak4@3ZuX%> zrv^EY8^usAh?xY|<1lL2VjXQ(Df&0qmGOE=a5 $^(u_ z*tp&V%UB(ur+A8-npO|@(jq~LsUeg`MYe6c{oL3C||uH3I=RN_wmZ(fG=MNKmvX zc }z}u d(r(TTXDY`yZgrB&v#lfI%l`J3EArq?%sT z&TvS3zboFW*7w;9lXx8x_#Xpd9xEy)(6^4^Sq4hA<@M c8HvBQ`U?io<7}u`DC4xYRWxF z{Afs$$wCh*2`@TK9Uc)OL?y3q+P#%7{jH1~%UH2eKV7fr;q$GC>~A#P%J6a^SUE8@ zKQ?5>g@F6aAO!;XwB=3*4oyq9-4`bQbzSqJ*N&pU+ZhME-?|{bRvZ0$42tnH5}WwC zea`7#hUl)N|NPpTU2=m?#H6r%_SVzVzn3P1l 8VWwFU|kg2$G+L?rrnML)|$UM8%bT~y8k{sA;o2+b9;VlMzfh_p^z6z zX{4Zd;6Tw*X_szyK7zrVmL;QfF#;b?B)4g{NnZ7@a$}i=9bv-i9jLwbN=L{C_AFW} z633?TW<+Di#LUiKq{*9p*zvqrrX3lHBT g8vPmW7;+iA>1KKsV=YQc|9!pTxs?3pQK7mJ;2TKg3FBc=zuR+&qNh2_rHD zr>)y7v-Yhef0XCzDr1+V5{Md&oy5&19^PF3B?Ezbk0;QEcF$##azai|b`SLNF_O$b zbXo4utE$_?ewcL{|3PYG$fOBWwn5ux^b6L|pt`W0fg*N!@vodvyB`^}*=4KRpHMAS zUYNz|t3_=2&h9gxonMoetd;I*aYeN7P78i;&z!e46l6?BH3!0V+aTi1M%9{-M7U0N z!N+uNU7euIsARgLTU*B36G6#_j$d@;VBnenyg_|C>V!njIY;s;U*~EePau%JMNkkO zGXp;c{Of3RBmNtTT<(0obWASNz%JX>>O^1KWE%0olkVvPvCN3