diff --git a/cev_eris.dme b/cev_eris.dme
index b72dd7f049e..b06d829c024 100644
--- a/cev_eris.dme
+++ b/cev_eris.dme
@@ -2836,4 +2836,61 @@
#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\objects\effects\misc.dm"
+#include "proxima\code\game\objects\structures\window.dm"
+#include "proxima\code\game\turf\flooring\flooring_decals.dm"
+#include "proxima\code\game\turf\simulated\walls.dm"
+#include "proxima\code\modules\factions\avalon\church.dm"
+#include "proxima\code\modules\factions\command\command.dm"
+#include "proxima\code\modules\factions\ftu\ftu.dm"
+#include "proxima\code\modules\factions\hephestus\engineering.dm"
+#include "proxima\code\modules\factions\lawson_arms\lawson_arms.dm"
+#include "proxima\code\modules\factions\mirania\mirania.dm"
+#include "proxima\code\modules\factions\nt\nanotrasens.dm"
+#include "proxima\code\modules\factions\pcrc\jobs.dm"
+#include "proxima\code\modules\factions\pcrc\pcrc.dm"
+#include "proxima\code\modules\factions\zeng-hu\medical.dm"
+#include "proxima\code\modules\factions\zeng-hu\science.dm"
+#include "proxima\code\modules\organs\external\subtypes\vey_med_limbs.dm"
+//#include "proxima\code\modules\power\fusion\fusion.dm"
+#include "proxima\code\modules\power\fusion\consoles\_consoles.dm"
+#include "proxima\code\modules\power\fusion\consoles\core_control.dm"
+#include "proxima\code\modules\power\fusion\consoles\gyrotron_control.dm"
+#include "proxima\code\modules\power\fusion\consoles\injector_control.dm"
+#include "proxima\code\modules\power\fusion\core\_core.dm"
+#include "proxima\code\modules\power\fusion\core\core_field.dm"
+#include "proxima\code\modules\power\fusion\fuel_assembly\fuel_assembly.dm"
+#include "proxima\code\modules\power\fusion\fuel_assembly\fuel_compressor.dm"
+#include "proxima\code\modules\power\fusion\fuel_assembly\fuel_injector.dm"
+#include "proxima\code\modules\power\fusion\gyrotron\gyrotron.dm"
+#include "proxima\code\modules\power\fusion\fusion_circuits.dm"
+#include "proxima\code\modules\power\fusion\fusion_particle_catcher.dm"
+#include "proxima\code\modules\power\fusion\fusion_reactions.dm"
+#include "proxima\code\modules\power\fusion\kinetic_harvester.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"
// END_INCLUDE
diff --git a/code/ATMOSPHERICS/components/trinary_devices/filter.dm b/code/ATMOSPHERICS/components/trinary_devices/filter.dm
index d9b5f59e3ec..500eac3009f 100644
--- a/code/ATMOSPHERICS/components/trinary_devices/filter.dm
+++ b/code/ATMOSPHERICS/components/trinary_devices/filter.dm
@@ -22,6 +22,7 @@
2: Nitrogen: Nitrogen ONLY
3: Carbon Dioxide: Carbon Dioxide ONLY
4: Sleeping Agent (N2O)
+ 5: Hydrogen
*/
var/filter_type = -1
var/list/filtered_out = list()
@@ -49,6 +50,8 @@
filtered_out = list("carbon_dioxide")
if(4)//removing N2O
filtered_out = list("sleeping_agent")
+ if(5)//removing h2
+ filtered_out = list("hydrogen")
air1.volume = ATMOS_DEFAULT_VOLUME_FILTER
air2.volume = ATMOS_DEFAULT_VOLUME_FILTER
@@ -168,6 +171,8 @@
current_filter_type = "Carbon Dioxide"
if(4)
current_filter_type = "Nitrous Oxide"
+ if(5)
+ current_filter_type = "Hydrogen"
if(-1)
current_filter_type = "Nothing"
else
@@ -182,6 +187,7 @@
Nitrogen
Carbon Dioxide
Nitrous Oxide
+ Hydrogen
Nothing
Set Flow Rate Limit:
@@ -213,6 +219,8 @@
filtered_out += "carbon_dioxide"
if(4)//removing N2O
filtered_out += "sleeping_agent"
+ if(5)//removing h2
+ filtered_out += "hydrogen"
if (href_list["temp"])
src.temp = null
diff --git a/code/ATMOSPHERICS/components/unary/vent_scrubber.dm b/code/ATMOSPHERICS/components/unary/vent_scrubber.dm
index 1babf0ffe77..b0febeb91b0 100644
--- a/code/ATMOSPHERICS/components/unary/vent_scrubber.dm
+++ b/code/ATMOSPHERICS/components/unary/vent_scrubber.dm
@@ -26,7 +26,7 @@
var/last_zas_update = null
var/scrubbing = SCRUBBING
- var/list/scrubbing_gas = list("carbon_dioxide","sleeping_agent","plasma")
+ var/list/scrubbing_gas = list("carbon_dioxide","sleeping_agent","plasma", "hydrogen")
var/expanded_range = FALSE
var/panic = FALSE //is this scrubber panicked?
@@ -108,6 +108,7 @@
"filter_n2" = ("nitrogen" in scrubbing_gas),
"filter_co2" = ("carbon_dioxide" in scrubbing_gas),
"filter_plasma" = ("plasma" in scrubbing_gas),
+ "filter_h2" = ("hydrogen" in scrubbing_gas),
"filter_n2o" = ("sleeping_agent" in scrubbing_gas),
"sigtype" = "status"
)
@@ -239,6 +240,11 @@
else if(signal.data["toggle_tox_scrub"])
toggle += "plasma"
+ if(!isnull(signal.data["h2_scrub"]) && text2num(signal.data["h2_scrub"]) != ("hydrogen" in scrubbing_gas))
+ toggle += "hydrogen"
+ else if(signal.data["toggle_h2_scrub"])
+ toggle += "hydrogen"
+
if(!isnull(signal.data["n2o_scrub"]) && text2num(signal.data["n2o_scrub"]) != ("sleeping_agent" in scrubbing_gas))
toggle += "sleeping_agent"
else if(signal.data["toggle_n2o_scrub"])
diff --git a/code/ATMOSPHERICS/pipes.dm b/code/ATMOSPHERICS/pipes.dm
index b8f44a4640a..60d541f72ef 100644
--- a/code/ATMOSPHERICS/pipes.dm
+++ b/code/ATMOSPHERICS/pipes.dm
@@ -1180,6 +1180,21 @@
..()
icon_state = "plasma"
+/obj/machinery/atmospherics/pipe/tank/hydrogen
+ name = "Pressure Tank (Hydrogen)"
+ description_antag = "Will make people suddenly combust"
+ icon_state = "h2_map"
+
+/obj/machinery/atmospherics/pipe/tank/hydrogen/New()
+ air_temporary = new
+ air_temporary.volume = volume
+ air_temporary.temperature = T20C
+
+ air_temporary.adjust_gas("hydrogen", (start_pressure)*(air_temporary.volume)/(R_IDEAL_GAS_EQUATION*air_temporary.temperature))
+
+ ..()
+ icon_state = "hydrogen"
+
/obj/machinery/atmospherics/pipe/tank/nitrous_oxide
name = "Pressure Tank (Nitrous Oxide)"
icon_state = "n2o_map"
diff --git a/code/ZAS/Controller.dm b/code/ZAS/Controller.dm
index c43dfb1327f..6af9db3fb4f 100644
--- a/code/ZAS/Controller.dm
+++ b/code/ZAS/Controller.dm
@@ -368,6 +368,7 @@ Total Unsimulated Turfs: [world.maxx*world.maxy*world.maxz - simulated_turf_coun
if(A.plasma != B.plasma) return 0
if(A.carbon_dioxide != B.carbon_dioxide) return 0
if(A.temperature != B.temperature) return 0
+ if(A.hydrogen != B.hydrogen) return 0
return 1
/datum/controller/air_system/proc/remove_edge(connection_edge/E)
diff --git a/code/ZAS/Turf.dm b/code/ZAS/Turf.dm
index b8accfa115f..660de0b047a 100644
--- a/code/ZAS/Turf.dm
+++ b/code/ZAS/Turf.dm
@@ -272,6 +272,7 @@
GM.gas["carbon_dioxide"] = (carbon_dioxide/sum)*amount
GM.gas["nitrogen"] = (nitrogen/sum)*amount
GM.gas["plasma"] = (plasma/sum)*amount
+ GM.gas["hydrogen"] = (hydrogen/sum)*amount
GM.temperature = temperature
GM.update_values()
diff --git a/code/__DEFINES/atmos.dm b/code/__DEFINES/atmos.dm
index ab4d32a5494..548b1f283b7 100644
--- a/code/__DEFINES/atmos.dm
+++ b/code/__DEFINES/atmos.dm
@@ -94,6 +94,7 @@
#define ATMOSTANK_OXYGEN 40000 // O2 is also important for airmix, but not as much as N2 as it's only 21% of it.
#define ATMOSTANK_CO2 25000 // CO2 and PH are not critically important for station, only for toxins and alternative coolants, no need to store a lot of those.
#define ATMOSTANK_PLASMA 25000
+#define ATMOSTANK_HYDROGEN 100000
#define ATMOSTANK_NITROUSOXIDE 10000 // N2O doesn't have a real useful use, i guess it's on station just to allow refilling of sec's riot control canisters?
#define R_IDEAL_GAS_EQUATION 8.31 // kPa*L/(K*mol).
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/__DEFINES/jobs.dm b/code/__DEFINES/jobs.dm
index b7920ea1a7d..b8611e40b69 100644
--- a/code/__DEFINES/jobs.dm
+++ b/code/__DEFINES/jobs.dm
@@ -1,28 +1,28 @@
#define ASSISTANT_TITLE "Vagabond"
//Jobs depatment lists for use in constant expressions
-#define JOBS_SECURITY "Ironhammer Commander","Ironhammer Gunnery Sergeant","Ironhammer Inspector","Ironhammer Medical Specialist","Ironhammer Operative"
-#define JOBS_COMMAND "Captain","First Officer","Ironhammer Commander","Guild Merchant","Technomancer Exultant","Moebius Biolab Officer","Moebius Expedition Overseer","NeoTheology Preacher"
-#define JOBS_ENGINEERING "Technomancer Exultant","Technomancer"
-#define JOBS_MEDICAL "Moebius Biolab Officer","Moebius Doctor","Moebius Psychiatrist","Moebius Chemist","Moebius Paramedic","Moebius Bio-Engineer"
-#define JOBS_SCIENCE "Moebius Expedition Overseer","Moebius Scientist","Moebius Roboticist"
-#define JOBS_MOEBIUS "Moebius Biolab Officer","Moebius Doctor","Moebius Psychiatrist","Moebius Chemist","Moebius Paramedic","Moebius Bio-Engineer","Moebius Expedition Overseer","Moebius Scientist","Moebius Roboticist"
-#define JOBS_CARGO "Guild Merchant","Guild Technician","Guild Miner",
+#define JOBS_SECURITY "PCRC Commander","PCRC Gunnery Sergeant","PCRC Inspector","PCRC Medical Specialist","PCRC Operative"
+#define JOBS_COMMAND "Captain","First Mate","PCRC Commander","FTU Merchant","Hephaestus Exultant","Zeng-Hu Biolab Officer","Zeng-Hu Expedition Overseer","NeoChristianity Preacher"
+#define JOBS_ENGINEERING "Hephaestus Foreman","Hephaestus Engineer"
+#define JOBS_MEDICAL "Zeng-Hu Biolab Officer","Zeng-Hu Doctor","Zeng-Hu Psychiatrist","Zeng-Hu Chemist","Zeng-Hu Paramedic","Zeng-Hu Bio-Engineer"
+#define JOBS_SCIENCE "Zeng-Hu Expedition Overseer","Zeng-Hu Scientist","Zeng-Hu Roboticist"
+#define JOBS_MOEBIUS "Zeng-Hu Biolab Officer","Zeng-Hu Doctor","Zeng-Hu Psychiatrist","Zeng-Hu Chemist","Zeng-Hu Paramedic","Zeng-Hu Bio-Engineer","Zeng-Hu Expedition Overseer","Zeng-Hu Scientist","Zeng-Hu Roboticist"
+#define JOBS_CARGO "FTU Merchant","FTU Technician","Guild Miner",
#define JOBS_CIVILIAN "Club Manager","Club Worker","Club Artist",ASSISTANT_TITLE
-#define JOBS_CHURCH "NeoTheology Preacher","NeoTheology Acolyte","NeoTheology Agrolyte","NeoTheology Custodian"
+#define JOBS_CHURCH "Neo-Christianity Preacher","Alanian Knight","Alanian Herbalist","Alanian Custodian"
#define JOBS_NONHUMAN "AI","Robot","pAI"
#define CREDITS "¢"
#define CREDS "¢"
#define DEPARTMENT_COMMAND "Command"
-#define DEPARTMENT_MEDICAL "Medical"
-#define DEPARTMENT_ENGINEERING "Engineering"
-#define DEPARTMENT_SCIENCE "Science"
-#define DEPARTMENT_SECURITY "Security"
-#define DEPARTMENT_GUILD "Guild"
+#define DEPARTMENT_MEDICAL "Zeng-Hu Medical"
+#define DEPARTMENT_ENGINEERING "Hephaestus Industries"
+#define DEPARTMENT_SCIENCE "Zeng-Hu Science"
+#define DEPARTMENT_SECURITY "Proxima Centauri Risk Control"
+#define DEPARTMENT_GUILD "Free Trade Union"
#define DEPARTMENT_CIVILIAN "Civilian"
-#define DEPARTMENT_CHURCH "Church"
+#define DEPARTMENT_CHURCH "Neo-Christianity"
#define DEPARTMENT_OFFSHIP "Offship"
#define DEPARTMENT_SILICON "Silicon"
diff --git a/code/__DEFINES/materials.dm b/code/__DEFINES/materials.dm
index 5e2e9e9ace7..e90acc11a7e 100644
--- a/code/__DEFINES/materials.dm
+++ b/code/__DEFINES/materials.dm
@@ -29,6 +29,7 @@
#define MATERIAL_CARPET "carpet"
#define MATERIAL_BIOMATTER "biomatter"
#define MATERIAL_COMPRESSED "compressed matter"
+#define MATERIAL_PLASTAN "plastan"
#define MATERIAL_LIST list(\
MATERIAL_STEEL,\
@@ -76,6 +77,7 @@
#define ORE_GOLD "o_gold"
#define ORE_PLATINUM "o_platinum"
#define ORE_HYDROGEN "o_hydrogen"
+#define ORE_TITANIUM "o_titanium"
#define ORE_LIST list(\
ORE_CARBON,\
@@ -86,4 +88,6 @@
ORE_DIAMOND,\
ORE_SILVER,\
ORE_GOLD,\
- ORE_PLATINUM)
+ ORE_PLATINUM,\
+ ORE_TITANIUM\
+ )
diff --git a/code/__DEFINES/species_languages.dm b/code/__DEFINES/species_languages.dm
index 3ae87d64c79..1e54ab6ceb6 100644
--- a/code/__DEFINES/species_languages.dm
+++ b/code/__DEFINES/species_languages.dm
@@ -23,13 +23,13 @@
#define HAS_HAIR_COLOR 0x20 // Hair colour selectable in chargen. (RGB)
// Languages.
-#define LANGUAGE_COMMON "English Common"
-#define LANGUAGE_CYRILLIC "Techno-Russian"
-#define LANGUAGE_SERBIAN "Serbian"
-#define LANGUAGE_JIVE "Jive"
-#define LANGUAGE_GERMAN "German"
-#define LANGUAGE_NEOHONGO "Neohongo"
-#define LANGUAGE_LATIN "Latin"
+#define LANGUAGE_COMMON "Спейсер"
+#define LANGUAGE_CYRILLIC "Пан-Славик"
+#define LANGUAGE_SERBIAN "Сербский"
+#define LANGUAGE_JIVE "Гаттер"
+#define LANGUAGE_GERMAN "Миранийский"
+#define LANGUAGE_NEOHONGO "Янгуй"
+#define LANGUAGE_LATIN "Алаин"
#define LANGUAGE_ROBOT "Robot Talk"
diff --git a/code/__HELPERS/manifest.dm b/code/__HELPERS/manifest.dm
index 334e07960d3..34276c673d3 100644
--- a/code/__HELPERS/manifest.dm
+++ b/code/__HELPERS/manifest.dm
@@ -16,14 +16,14 @@
var/list/dept_data = list(
list("names" = list(), "header" = "Command Staff", "flag" = COMMAND),
- list("names" = list(), "header" = "Ironhammer Security", "flag" = IRONHAMMER),
- list("names" = list(), "header" = "Moebius Medical", "flag" = MEDICAL),
- list("names" = list(), "header" = "Moebius Research", "flag" = SCIENCE),
- list("names" = list(), "header" = "Church of NeoTheology", "flag" = CHURCH),
- list("names" = list(), "header" = "Asters Guild", "flag" = GUILD),
+ list("names" = list(), "header" = "Proxima Centauri Risk Control", "flag" = IRONHAMMER),
+ list("names" = list(), "header" = "Zeng-Hu Medical", "flag" = MEDICAL),
+ list("names" = list(), "header" = "Zeng-Hu Research", "flag" = SCIENCE),
+ list("names" = list(), "header" = "Alanian Church of Neo-Christianity", "flag" = CHURCH),
+ list("names" = list(), "header" = "Free Trade Union", "flag" = GUILD),
list("names" = list(), "header" = "Civilian", "flag" = CIVILIAN),
list("names" = list(), "header" = "Service", "flag" = SERVICE),
- list("names" = list(), "header" = "Technomancer League", "flag" = ENGINEERING),
+ list("names" = list(), "header" = "Hephaestus Industries", "flag" = ENGINEERING),
list("names" = list(), "header" = "Miscellaneous", "flag" = MISC),
list("names" = list(), "header" = "Silicon")
)
@@ -151,4 +151,4 @@
/proc/flat_nano_crew_manifest()
. = list()
. += filtered_nano_crew_manifest(null, TRUE)
- . += silicon_nano_crew_manifest(nonhuman_positions)
\ No newline at end of file
+ . += silicon_nano_crew_manifest(nonhuman_positions)
diff --git a/code/controllers/subsystems/air.dm b/code/controllers/subsystems/air.dm
index e9060c90b25..eef48c790c9 100644
--- a/code/controllers/subsystems/air.dm
+++ b/code/controllers/subsystems/air.dm
@@ -517,6 +517,8 @@ SUBSYSTEM_DEF(air)
return FALSE
if(A.carbon_dioxide != B.carbon_dioxide)
return FALSE
+ if(A.hydrogen != B.hydrogen)
+ return FALSE
if(A.temperature != B.temperature)
return FALSE
diff --git a/code/controllers/subsystems/supply.dm b/code/controllers/subsystems/supply.dm
index b717e75e545..db3d03079a7 100644
--- a/code/controllers/subsystems/supply.dm
+++ b/code/controllers/subsystems/supply.dm
@@ -74,7 +74,7 @@ SUBSYSTEM_DEF(supply)
exports.Cut()
var/datum/money_account/GA = department_accounts[DEPARTMENT_GUILD]
- var/datum/transaction/T = new(points, "Asters Guild", "Exports", "Asters Automated Trading System")
+ var/datum/transaction/T = new(points, "FTU", "Exports", "Free Trade Union Trading System")
T.apply_to(GA)
centcom_message = msg
diff --git a/code/controllers/subsystems/trade.dm b/code/controllers/subsystems/trade.dm
index f7005ef5738..30e2fa22d86 100644
--- a/code/controllers/subsystems/trade.dm
+++ b/code/controllers/subsystems/trade.dm
@@ -1,4 +1,4 @@
-#define TRADE_SYSTEM_IC_NAME "Asters Automated Trading System"
+#define TRADE_SYSTEM_IC_NAME "Free Trade Union Trading System"
GLOBAL_LIST_EMPTY(price_cache)
SUBSYSTEM_DEF(trade)
name = "Trade"
@@ -249,7 +249,7 @@ SUBSYSTEM_DEF(trade)
if(!current_container.reagents) // If the previous check fails, we are looking for a container with reagents or a specific reagent
return FALSE // If the container is empty, fail
- for(var/datum/reagent/current_reagent in current_container.reagents?.reagent_list)
+ for(var/datum/reagent/current_reagent in current_container.reagents?.reagent_list)
if(current_reagent.volume >= target_volume && istype(current_reagent, target_reagent)) // Check volume and reagent type
return TRUE
@@ -531,7 +531,7 @@ SUBSYSTEM_DEF(trade)
// The max is a soft cap
if(export_count > EXPORT_COUNT_MAXIMUM)
break
-
+
senderBeacon.start_export()
var/datum/money_account/guild_account = department_accounts[DEPARTMENT_GUILD]
var/datum/transaction/T = new(cost, guild_account.get_name(), "Export", TRADE_SYSTEM_IC_NAME)
@@ -552,7 +552,7 @@ SUBSYSTEM_DEF(trade)
// Junk tags override hockable tags and offer types override both
if(target_hockable_tags.len)
- . = HOCKABLE
+ . = HOCKABLE
if(target_junk_tags.len)
. = JUNK
for(var/offer_type in offer_types)
diff --git a/code/game/gamemodes/scores.dm b/code/game/gamemodes/scores.dm
index 80097318c85..d3e4ba5781b 100644
--- a/code/game/gamemodes/scores.dm
+++ b/code/game/gamemodes/scores.dm
@@ -284,7 +284,7 @@ GLOBAL_VAR_INIT(score_technomancer_faction_item_loss, 0)
Antagonist contracts completed: [GLOB.completed_antag_contracts] ([to_score_color(GLOB.score_antag_contracts)] Points)
Antagonists killed or captured: [GLOB.captured_or_dead_antags] ([to_score_color(GLOB.captured_or_dead_antags_score)] Points)
Escaped Antagonists: [GLOB.ironhammer_escaped_antagonists] ([to_score_color(GLOB.ironhammer_escaped_antagonists_score)] Points)
- IH operatives killed: [GLOB.ironhammer_operative_dead] ([to_score_color(GLOB.ironhammer_operative_dead_score)] Points)
+ PCRC operatives killed: [GLOB.ironhammer_operative_dead] ([to_score_color(GLOB.ironhammer_operative_dead_score)] Points)
Final Ironhammer score: [get_color_score(GLOB.ironhammer_score, GLOB.ironhammer_score)] Points
"}
diff --git a/code/game/machinery/alarm.dm b/code/game/machinery/alarm.dm
index ac5f060a696..887502ff887 100644
--- a/code/game/machinery/alarm.dm
+++ b/code/game/machinery/alarm.dm
@@ -561,6 +561,7 @@
)
scrubbers[scrubbers.len]["filters"] += list(list("name" = "Oxygen", "command" = "o2_scrub", "val" = info["filter_o2"]))
scrubbers[scrubbers.len]["filters"] += list(list("name" = "Nitrogen", "command" = "n2_scrub", "val" = info["filter_n2"]))
+ scrubbers[scrubbers.len]["filters"] += list(list("name" = "Hydrogen", "command" = "h2_scrub", "val" = info["filter_h2"]))
scrubbers[scrubbers.len]["filters"] += list(list("name" = "Carbon Dioxide", "command" = "co2_scrub","val" = info["filter_co2"]))
scrubbers[scrubbers.len]["filters"] += list(list("name" = "Toxin" , "command" = "tox_scrub","val" = info["filter_plasma"]))
scrubbers[scrubbers.len]["filters"] += list(list("name" = "Nitrous Oxide", "command" = "n2o_scrub","val" = info["filter_n2o"]))
@@ -583,6 +584,7 @@
"oxygen" = "O2",
"carbon dioxide" = "CO2",
"plasma" = "Toxin",
+ "hydrogen" = "H2",
"other" = "Other")
for (var/g in gas_names)
thresholds[++thresholds.len] = list("name" = gas_names[g], "settings" = list())
@@ -690,6 +692,7 @@
"n2_scrub",
"co2_scrub",
"tox_scrub",
+ "h2_scrub",
"n2o_scrub",
"panic_siphon",
"scrubbing")
diff --git a/code/game/machinery/atmo_control.dm b/code/game/machinery/atmo_control.dm
index c9ad7fb1862..46fd580cf19 100644
--- a/code/game/machinery/atmo_control.dm
+++ b/code/game/machinery/atmo_control.dm
@@ -19,6 +19,7 @@
// 8 for plasma concentration
// 16 for nitrogen concentration
// 32 for carbon dioxide concentration
+ // 64 for hydrogen
var/datum/radio_frequency/radio_connection
@@ -50,11 +51,14 @@
signal.data["nitrogen"] = round(100*air_sample.gas["nitrogen"]/total_moles,0.1)
if(output&32)
signal.data["carbon_dioxide"] = round(100*air_sample.gas["carbon_dioxide"]/total_moles,0.1)
+ if(output&64)
+ signal.data["hydrogen"] = round(100*air_sample.gas["hydrogen"]/total_moles,0.1)
else
signal.data["oxygen"] = 0
signal.data["plasma"] = 0
signal.data["nitrogen"] = 0
signal.data["carbon_dioxide"] = 0
+ signal.data["hydrogen"] = 0
signal.data["sigtype"]="status"
radio_connection.post_signal(src, signal, filter = RADIO_ATMOSIA)
@@ -132,6 +136,8 @@ obj/machinery/computer/general_air_control/Destroy()
sensor_part += "[data["carbon_dioxide"]]% CO2; "
if(data["plasma"])
sensor_part += "[data["plasma"]]% TX; "
+ if(data["hydrogen"])
+ sensor_part += "[data["hydrogen"]]% H2; "
sensor_part += "
"
else
diff --git a/code/game/machinery/atmoalter/canister.dm b/code/game/machinery/atmoalter/canister.dm
index 7d4e7d4db2a..b2866fe3583 100644
--- a/code/game/machinery/atmoalter/canister.dm
+++ b/code/game/machinery/atmoalter/canister.dm
@@ -69,6 +69,13 @@
canister_color = "grey"
can_label = 0
+/obj/machinery/portable_atmospherics/canister/hydrogen
+ name = "Canister: \[N2]"
+ icon_state = "purple"
+ description_antag = "Causes people to suddenly combust. Very flamable"
+ canister_color = "purple"
+ can_label = 0
+
/obj/machinery/portable_atmospherics/canister/air/airlock
start_pressure = 3 * ONE_ATMOSPHERE
@@ -97,8 +104,10 @@
icon_state = "redws"
canister_color = "redws"
-
-
+/obj/machinery/portable_atmospherics/canister/empty/hydrogen
+ name = "Canister: \[N2]"
+ icon_state = "purple"
+ canister_color = "purple"
/obj/machinery/portable_atmospherics/canister/proc/check_change()
var/old_flag = update_flag
diff --git a/code/game/machinery/autolathe/nanoforge.dm b/code/game/machinery/autolathe/nanoforge.dm
index e24fc98d4a0..bb42b8a3475 100644
--- a/code/game/machinery/autolathe/nanoforge.dm
+++ b/code/game/machinery/autolathe/nanoforge.dm
@@ -30,7 +30,8 @@
MATERIAL_CARDBOARD = 0.1,
MATERIAL_RGLASS = 0.4,
MATERIAL_LEATHER = 0.1,
- MATERIAL_TITANIUM = 0.6)
+ MATERIAL_TITANIUM = 0.6,
+ MATERIAL_PLASTAN= 0.9)
mat_efficiency = 0.6 // 40% more efficient than normal autolathes
storage_capacity = 240
diff --git a/code/game/machinery/recycle_vendor.dm b/code/game/machinery/recycle_vendor.dm
index 86956ed6729..23ab6e73481 100644
--- a/code/game/machinery/recycle_vendor.dm
+++ b/code/game/machinery/recycle_vendor.dm
@@ -166,7 +166,7 @@
stored_item_value += round(S.get_item_cost(), 2) / 2
stored_item_fluff += "
[i] - [stored_item_materials[i]] units, worth [round(S.get_item_cost(), 2) / 2] credits."
else
- stored_item_fluff += "
Payouts for [i] suspended by Aster Guild representative."
+ stored_item_fluff += "
Payouts for [i] suspended by FTU representative."
else // Bay leftover materials
stored_item_fluff += "
[i] recycling is not supported."
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/code/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm
index f349d241754..e9eaca5d13e 100644
--- a/code/game/objects/items/devices/PDA/PDA.dm
+++ b/code/game/objects/items/devices/PDA/PDA.dm
@@ -442,6 +442,7 @@ var/global/list/obj/item/device/pda/PDAs = list()
var/o2_level = environment.gas["oxygen"]/total_moles
var/n2_level = environment.gas["nitrogen"]/total_moles
var/co2_level = environment.gas["carbon_dioxide"]/total_moles
+ var/h2_level = environment.gas["hydrogen"]/total_moles
var/plasma_level = environment.gas["plasma"]/total_moles
var/unknown_level = 1-(o2_level+n2_level+co2_level+plasma_level)
data["aircontents"] = list(\
@@ -450,6 +451,7 @@ var/global/list/obj/item/device/pda/PDAs = list()
"oxygen" = "[round(o2_level*100,0.1)]",\
"carbon_dioxide" = "[round(co2_level*100,0.1)]",\
"plasma" = "[round(plasma_level*100,0.01)]",\
+ "hydrogen" = "[round(h2_level*100,0.01)]",\
"other" = "[round(unknown_level, 0.01)]",\
"temp" = "[round(environment.temperature-T0C,0.1)]",\
"reading" = 1\
diff --git a/code/game/turfs/flooring/flooring_premade.dm b/code/game/turfs/flooring/flooring_premade.dm
index af600719d25..922fb34eab1 100644
--- a/code/game/turfs/flooring/flooring_premade.dm
+++ b/code/game/turfs/flooring/flooring_premade.dm
@@ -384,6 +384,12 @@
nitrogen = 0
plasma = ATMOSTANK_PLASMA
+/turf/simulated/floor/reinforced/hydrogen
+ oxygen = 0
+ nitrogen = 0
+ hydrogen = ATMOSTANK_HYDROGEN
+
+
/turf/simulated/floor/reinforced/carbon_dioxide
oxygen = 0
nitrogen = 0
diff --git a/code/game/turfs/simulated/wall_types.dm b/code/game/turfs/simulated/wall_types.dm
index 0be1a09a142..3a43c23d310 100644
--- a/code/game/turfs/simulated/wall_types.dm
+++ b/code/game/turfs/simulated/wall_types.dm
@@ -116,8 +116,19 @@
return
/turf/simulated/wall/titanium/New(var/newloc)
- ..(newloc, MATERIAL_VOXALLOY)
+ ..(newloc, MATERIAL_TITANIUM)
+
+/turf/simulated/wall/titanium/reinforced/New(var/newloc)
+ ..(newloc, MATERIAL_TITANIUM, MATERIAL_TITANIUM)
+
+/turf/simulated/wall/plastan/New(var/newloc)
+ ..(newloc, MATERIAL_PLASTAN)
+
+/turf/simulated/wall/plastan/reinforced/New(var/newloc)
+ ..(newloc, MATERIAL_PLASTAN, MATERIAL_PLASTAN)
+/turf/simulated/wall/plastan/reinforced/hull
+ paint_color = COLOR_HULL_BLUE
//Untinted walls have white color, all their coloring is built into their sprite and they should really not be given a tint, it'd look awful
/turf/simulated/wall/untinted
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 917eb945150..1f842d6b610 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -10,6 +10,7 @@
var/carbon_dioxide = 0
var/nitrogen = 0
var/plasma = 0
+ var/hydrogen = 0
var/list/initial_gas
diff --git a/code/modules/materials/material_sheets.dm b/code/modules/materials/material_sheets.dm
index 6fb3abeeb7c..efb0230cad9 100644
--- a/code/modules/materials/material_sheets.dm
+++ b/code/modules/materials/material_sheets.dm
@@ -425,3 +425,37 @@
item_state = "rcdammo"
default_type = MATERIAL_COMPRESSED
price_tag = 30
+
+/obj/item/stack/material/titanium
+ name = "titanium"
+ icon = 'proxima/icons/obj/stack/material_titanium.dmi'
+ icon_state = "sheet-titanium"
+ item_state = "sheet-metal"
+ price_tag = 25
+ default_type = MATERIAL_TITANIUM
+
+/obj/item/stack/material/titanium/full
+ amount = 120
+
+/obj/item/stack/material/titanium/random
+ rand_min = 3
+ rand_max = 20
+ spawn_tags = SPAWN_TAG_MATERIAL_BUILDING
+ rarity_value = 5
+
+/obj/item/stack/material/plastan
+ name = "plastan"
+ icon_state = "sheet-plasteel"
+ item_state = "sheet-metal"
+ color = COLOR_DARK_GUNMETAL
+ price_tag = 50
+ default_type = MATERIAL_PLASTAN
+
+/obj/item/stack/material/plastan/full
+ amount = 120
+
+/obj/item/stack/material/plastan/random
+ rand_min = 3
+ rand_max = 20
+ spawn_tags = SPAWN_TAG_MATERIAL_BUILDING
+ rarity_value = 5
diff --git a/code/modules/materials/materials.dm b/code/modules/materials/materials.dm
index 8e96fae6eff..283da9a2e13 100644
--- a/code/modules/materials/materials.dm
+++ b/code/modules/materials/materials.dm
@@ -375,12 +375,20 @@ var/list/name_to_material
hitsound = 'sound/weapons/genhit.ogg'
/material/plasteel/titanium
- name = "titanium"
- stack_type = null
- icon_base = "metal"
- door_icon_base = "metal"
- icon_colour = "#D1E6E3"
- icon_reinf = "reinf_metal"
+ name = MATERIAL_TITANIUM
+ stack_type = /obj/item/stack/material/titanium
+ icon_colour = COLOR_TITANIUM // "#6c7274" can be used as alternative, i dunno
+ weight = 10
+
+/material/plasteel/titanium/plastan
+ name = MATERIAL_PLASTAN
+ stack_type = /obj/item/stack/material/plastan
+ icon_colour = "#6c7274"
+ integrity = 1000
+ melting_point = 10000
+ hardness = 150
+ weight = 20
+ stack_origin_tech = list(TECH_MATERIAL = 5)
/material/glass
name = MATERIAL_GLASS
diff --git a/code/modules/mining/alloys.dm b/code/modules/mining/alloys.dm
index 997d556bde6..ceffd61c9f8 100644
--- a/code/modules/mining/alloys.dm
+++ b/code/modules/mining/alloys.dm
@@ -42,3 +42,15 @@
)
product = /obj/item/stack/material/glass/plasmaglass
+/datum/alloy/plastan
+ name = "Plastitanium"
+ metaltag = MATERIAL_PLASTAN
+ ore_input = 5
+ requires = list(
+ ORE_PLASMA = 1,
+ ORE_CARBON = 1,
+ ORE_IRON = 1,
+ ORE_TITANIUM = 2,
+ )
+ product = /obj/item/stack/material/plastan
+ product_mod = 0.5
diff --git a/code/modules/mining/ore.dm b/code/modules/mining/ore.dm
index 4db44988dfa..3832c268bed 100644
--- a/code/modules/mining/ore.dm
+++ b/code/modules/mining/ore.dm
@@ -93,6 +93,13 @@
rarity_value = 50
price_tag = 5
+/obj/item/ore/titanium
+ name = "raw titanium"
+ icon_state = "ore_titanium"
+ material = ORE_TITANIUM
+ rarity_value = 50
+ price_tag = 5
+
/obj/item/ore/hydrogen
name = "raw hydrogen"
icon_state = "ore_hydrogen"
diff --git a/code/modules/mining/ore_datum.dm b/code/modules/mining/ore_datum.dm
index 9baa97bd405..7170a90899e 100644
--- a/code/modules/mining/ore_datum.dm
+++ b/code/modules/mining/ore_datum.dm
@@ -126,6 +126,16 @@ var/global/list/ore_data = list()
ore = /obj/item/ore/osmium
scan_icon = "mineral_rare"
+/ore/titanium
+ name = ORE_TITANIUM
+ display_name = "raw titanium"
+ smelts_to = MATERIAL_TITANIUM
+ alloy = 1
+ result_amount = 5
+ spread_chance = 10
+ ore = /obj/item/ore/titanium
+ scan_icon = "mineral_rare"
+
/ore/hydrogen
name = ORE_HYDROGEN
display_name = "metallic hydrogen"
diff --git a/code/modules/mob/living/bot/cleanbot.dm b/code/modules/mob/living/bot/cleanbot.dm
index 699fb78ebd6..f4ce7637178 100644
--- a/code/modules/mob/living/bot/cleanbot.dm
+++ b/code/modules/mob/living/bot/cleanbot.dm
@@ -364,7 +364,7 @@
"Born to clean!",
"I HATE VAGABONDS I HATE VAGABONDS!!",
"It is always morally correct to perform field execution.",
- "But being as this is a RMB-A 2000, the most expensive robot in Frozen Star catalogue!",
+ "But being as this is a RMB-A 2000, the most expensive robot in PCRC catalogue!",
"Do I feel lucky? Well, do you, operative?",
"Those neotheologist fucks are up to something...",
"None of them know my true power!")
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index ef346f65e85..978e2f3b88e 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -238,15 +238,21 @@
//Trust me I'm an engineer
//I think we'll put this shit right here
var/list/rank_prefix = list(\
- "Ironhammer Operative" = "Operative",\
- "Ironhammer Inspector" = "Inspector",\
- "Ironhammer Medical Specialist" = "Specialist",\
- "Ironhammer Gunnery Sergeant" = "Sergeant",\
- "Ironhammer Commander" = "Lieutenant",\
- "NeoTheology Preacher" = "Reverend",\
- "Moebius Expedition Overseer" = "Overseer",\
- "Moebius Biolab Officer" = "Doctor",\
+ "PCRC Operative" = "Operative",\
+ "PCRC Inspector" = "Inspector",\
+ "PCRC Medical Specialist" = "Specialist",\
+ "PCRC Gunnery Sergeant" = "Sergeant",\
+ "PCRC Commander" = "Commander",\
+ "Neo-Christianity Preacher" = "Reverend",\
+ "Alanian Knight" = "Sir",\
+ "Zeng-Hu Expedition Overseer" = "Overseer",\
+ "Zeng-Hu Biolab Officer" = "Overseer",\
"Captain" = "Captain",\
+ "Fisrt Mate" = "Adjutant",\
+ "Zeng-Hu Doctor" = "Doctor",\
+ "Zeng-Hu Scientist" = "Researcher",\
+ "FTU Merchant" = "Merchant",\
+ "Hephaestus Foreman" = "Foreman"
)
/mob/living/carbon/human/proc/rank_prefix_name(name)
diff --git a/code/modules/organs/external/subtypes/robotic_types.dm b/code/modules/organs/external/subtypes/robotic_types.dm
index b919b4637e3..df9c8582ed5 100644
--- a/code/modules/organs/external/subtypes/robotic_types.dm
+++ b/code/modules/organs/external/subtypes/robotic_types.dm
@@ -41,8 +41,8 @@
default_description = /datum/organ_description/leg/right
/obj/item/organ/external/robotic/technomancer
- name = "Technomancer \"Homebrew\""
- desc = "Technomancer \"branded\" \"functional\" prosthesis."
+ name = "\"Hephaestus Industries\""
+ desc = "Hephaestus Industries prosthesis."
armor = list(melee = 2, bullet = 2, energy = 2, bomb = 10, bio = 100, rad = 100)
force_icon = 'icons/mob/human_races/cyberlimbs/technomancer.dmi'
model = "technomancer"
diff --git a/code/modules/projectiles/guns/projectile/modular/modular_AK.dm b/code/modules/projectiles/guns/projectile/modular/modular_AK.dm
index cf4df68a673..593b3771108 100644
--- a/code/modules/projectiles/guns/projectile/modular/modular_AK.dm
+++ b/code/modules/projectiles/guns/projectile/modular/modular_AK.dm
@@ -22,7 +22,7 @@
gun_tags = list(GUN_SILENCABLE)
- serial_type = "FS"
+ serial_type = "LA"
required_parts = list(/obj/item/part/gun/modular/mechanism/autorifle = 0, /obj/item/part/gun/modular/barrel = 0, /obj/item/part/gun/modular/grip = 0, /obj/item/part/gun/modular/stock = -1)
@@ -31,11 +31,11 @@
if(grip_type)
switch(grip_type)
if("wood")
- return "FS [stock_type] [caliber] \"Vipr\""
+ return "LA [stock_type] [caliber] \"Vipr\""
if("black")
return "BM [stock_type] [caliber] \"MPi-K\"" // Name of East-German AKs
if("rubber")
- return "FS [stock_type] [caliber] \"Venger\""
+ return "LA [stock_type] [caliber] \"Venger\""
if("excelsior")
return "Excelsior [stock_type] [caliber] \"Kalashnikov\""
if("serbian")
diff --git a/code/modules/projectiles/guns/projectile/modular/modular_wintermute.dm b/code/modules/projectiles/guns/projectile/modular/modular_wintermute.dm
index 15056acd935..5372308f9f4 100644
--- a/code/modules/projectiles/guns/projectile/modular/modular_wintermute.dm
+++ b/code/modules/projectiles/guns/projectile/modular/modular_wintermute.dm
@@ -33,11 +33,11 @@
if(grip_type)
switch(grip_type)
if("wood")
- return "FS AR [caliber] \"Fall\""
+ return "LA AR [caliber] \"Fall\""
if("black")
return "BM AR [caliber] \"Wintersun\""
if("rubber")
- return "FS AR [caliber] \"Wintermute\""
+ return "LA AR [caliber] \"Wintermute\""
if("excelsior")
return "Excelsior AR [caliber] \"Commute\""
if("serbian")
diff --git a/code/modules/trade/datums/trade_stations_presets/1-common/hellcat.dm b/code/modules/trade/datums/trade_stations_presets/1-common/hellcat.dm
index ff584de11b7..1dcb4723f19 100644
--- a/code/modules/trade/datums/trade_stations_presets/1-common/hellcat.dm
+++ b/code/modules/trade/datums/trade_stations_presets/1-common/hellcat.dm
@@ -21,8 +21,8 @@
stations_recommended = list("fs_ammo", "style")
inventory = list(
"Design Disks" = list(
- /obj/item/computer_hardware/hard_drive/portable/design/nonlethal_ammo = good_data("Frozen Star Nonlethal Magazines Pack", list(1, 10), 500),
- /obj/item/computer_hardware/hard_drive/portable/design/lethal_ammo = good_data("Frozen Star Lethal Magazines Pack", list(1, 10), 1000)
+ /obj/item/computer_hardware/hard_drive/portable/design/nonlethal_ammo = good_data("Lawson Arms Nonlethal Magazines Pack", list(1, 10), 500),
+ /obj/item/computer_hardware/hard_drive/portable/design/lethal_ammo = good_data("Lawson Arms Lethal Magazines Pack", list(1, 10), 1000)
),
"Enforce Equipment" = list(
/obj/item/handcuffs,
diff --git a/code/modules/trade/datums/trade_stations_presets/1-common/material_refine.dm b/code/modules/trade/datums/trade_stations_presets/1-common/material_refine.dm
index e03beeaeae7..7a4a3cfcc64 100644
--- a/code/modules/trade/datums/trade_stations_presets/1-common/material_refine.dm
+++ b/code/modules/trade/datums/trade_stations_presets/1-common/material_refine.dm
@@ -23,7 +23,8 @@
/obj/item/stack/material/plasteel/full = good_data("plasteel sheets (x120)", list(3, 5), null),
/obj/item/stack/material/wood/full = good_data("wood planks (x120)", list(3, 5), null),
/obj/item/stack/material/glass/full = good_data("glass sheets (x120)", list(3, 5), null),
- /obj/item/stack/material/plasma/full = good_data("plasma sheets (x120)", list(3, 5), null)
+ /obj/item/stack/material/plasma/full = good_data("plasma sheets (x120)", list(3, 5), null),
+ /obj/item/stack/material/titanium/full = good_data("titanium sheets (x120)", list(3,5), null)
)
)
hidden_inventory = list(
@@ -36,7 +37,8 @@
/obj/item/stack/material/osmium/full = good_data("osmium ingots (x120)", list(1, 2), null),
/obj/item/stack/material/mhydrogen/full = good_data("metallic hydrogen sheets (x120)", list(1, 2), null),
/obj/item/stack/material/tritium/full = good_data("tritium ingots (x120)", list(1, 2), null),
- /obj/item/stack/material/uranium/full = good_data("uranium sheets (x120)", list(1, 2), null)
+ /obj/item/stack/material/uranium/full = good_data("uranium sheets (x120)", list(1, 2), null),
+ /obj/item/stack/material/titanium/full = good_data("plastan sheets (x120)", list(1,3), null)
)
)
offer_types = list(
@@ -50,4 +52,5 @@
/obj/item/ore/diamond = offer_data("diamonds", 550, 0),
/obj/item/ore/osmium = offer_data("raw platinum", 330, 0),
/obj/item/ore/hydrogen = offer_data("raw hydrogen", 250, 0),
+ /obj/item/ore/titanium = offer_data("raw titanium", 300, 0)
)
diff --git a/code/modules/trade/datums/trade_stations_presets/1-common/suit_up.dm b/code/modules/trade/datums/trade_stations_presets/1-common/suit_up.dm
index dd74f254954..a0f56bba1e1 100644
--- a/code/modules/trade/datums/trade_stations_presets/1-common/suit_up.dm
+++ b/code/modules/trade/datums/trade_stations_presets/1-common/suit_up.dm
@@ -144,7 +144,7 @@
/obj/item/clothing/suit/space/void/security = custom_good_price(520),
/obj/item/clothing/suit/space/void/hazardsuit = custom_good_price(312)
),
- "Oberth Attire" = list(
+ "Miranian Attire" = list( //I fucking hate reinitialize lists
/obj/item/clothing/gloves/german,
/obj/item/clothing/head/beret/german,
/obj/item/clothing/mask/gas/german,
diff --git a/code/modules/trade/datums/trade_stations_presets/1-common/zarya.dm b/code/modules/trade/datums/trade_stations_presets/1-common/zarya.dm
index 3fcf1470a09..b48b2da038c 100644
--- a/code/modules/trade/datums/trade_stations_presets/1-common/zarya.dm
+++ b/code/modules/trade/datums/trade_stations_presets/1-common/zarya.dm
@@ -27,6 +27,8 @@
/obj/item/tank/plasma,
/obj/machinery/portable_atmospherics/canister/sleeping_agent = custom_good_price(800),
/obj/machinery/portable_atmospherics/canister/nitrogen = custom_good_price(400),
+ /obj/machinery/portable_atmospherics/canister/hydrogen = custom_good_price(400),
+ /obj/machinery/portable_atmospherics/canister/plasma = custom_good_price(2000),
/obj/machinery/portable_atmospherics/canister/oxygen = custom_good_price(400),
/obj/machinery/portable_atmospherics/canister/air = custom_good_price(400),
/obj/machinery/portable_atmospherics/canister/carbon_dioxide = custom_good_price(400)
diff --git a/code/modules/trade/datums/trade_stations_presets/2-uncommon/tb_cheapammofactorys.dm b/code/modules/trade/datums/trade_stations_presets/2-uncommon/tb_cheapammofactorys.dm
index 9d240f2770b..d9dbd1dc515 100644
--- a/code/modules/trade/datums/trade_stations_presets/2-uncommon/tb_cheapammofactorys.dm
+++ b/code/modules/trade/datums/trade_stations_presets/2-uncommon/tb_cheapammofactorys.dm
@@ -1,7 +1,7 @@
/datum/trade_station/tb_cheapammofactory
name_pool = list(
- "FSTB \'Zeus\'" = "Frozen Star Trade Beacon \'Zeus\': \"Cheap ammunition! Almost free! If we don\'t have it, that means it doesn't exists or it isn\'t legal enough!\"",
- "FSTB \'Hispa\'" = "Frozen Star Trade Beacon \'Hispa\': \"All ammunition in existence is here! Buy all calibers, all types! Cheap as breathing!\""
+ "LATB \'Zeus\'" = "Lawson Arms Trade Beacon \'Zeus\': \"Cheap ammunition! Almost free! If we don\'t have it, that means it doesn't exists or it isn\'t legal enough!\"",
+ "LATB \'Hispa\'" = "Lawson Arms Trade Beacon \'Hispa\': \"All ammunition in existence is here! Buy all calibers, all types! Cheap as breathing!\""
)
icon_states = list("htu_station", "station")
uid = "fs_ammo"
diff --git a/icons/atmos/tank.dmi b/icons/atmos/tank.dmi
index 8c49277ff85..94b65135837 100644
Binary files a/icons/atmos/tank.dmi and b/icons/atmos/tank.dmi differ
diff --git a/icons/obj/atmos.dmi b/icons/obj/atmos.dmi
index 7c19cd30e28..9bb82fe6126 100644
Binary files a/icons/obj/atmos.dmi and b/icons/obj/atmos.dmi differ
diff --git a/icons/obj/mining.dmi b/icons/obj/mining.dmi
index 28cfd53b612..f5212a27120 100644
Binary files a/icons/obj/mining.dmi and b/icons/obj/mining.dmi differ
diff --git a/proxima/code/defines/gases.dm b/proxima/code/defines/gases.dm
new file mode 100644
index 00000000000..99b829604d6
--- /dev/null
+++ b/proxima/code/defines/gases.dm
@@ -0,0 +1,7 @@
+/decl/xgm_gas/hydrogen
+ id = "hydrogen"
+ name = "Hydrogen"
+ specific_heat = 75 // J/(mol*K), I can't do math, sorry
+ molar_mass = 0.002 // kg/mol
+
+ flags = XGM_GAS_FUEL | XGM_GAS_CONTAMINANT
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..b4d53db3b2c
--- /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(/turf/simulated/wall/W, var/mob/user)
+ if ((W.can_paint == TRUE) && paint_colour)
+ W.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.")
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 += "
This paper has been stamped by the [adminfax.origin] Quantum Relay."
+
+ var/image/stampoverlay = image('icons/obj/bureaucracy.dmi')
+ var/x
+ var/y
+ x = rand(-2, 0)
+ y = rand(-1, 2)
+ adminfax.offset_x += x
+ adminfax.offset_y += y
+ stampoverlay.pixel_x = x
+ stampoverlay.pixel_y = y
+
+ if(!adminfax.ico)
+ adminfax.ico = new
+ adminfax.ico += "paper_stamp-boss"
+ stampoverlay.icon_state = "paper_stamp-boss"
+
+ if(!adminfax.stamped)
+ adminfax.stamped = new
+ adminfax.stamped += /obj/item/stamp/boss
+ adminfax.overlays += stampoverlay
+
+ // А вы знали, что когда админ печатает вам факс, то копия которая сохраняется в админских датумах так же жрет тоннер?
+ var/obj/item/rcvdcopy
+ var/obj/machinery/photocopier/faxmachine/cloner = pick(reciever)
+ rcvdcopy = cloner.copy(adminfax)
+ rcvdcopy.forceMove(null) //hopefully this shouldn't cause trouble
+ GLOB.adminfaxes += rcvdcopy
+
+ var/list/failure = list()
+ var/list/success = list()
+ for (var/obj/machinery/photocopier/faxmachine/machine in reciever)
+ if (machine.recievefax(adminfax))
+ success += machine.department
+ else
+ failure += machine.department
+
+ QDEL_IN(adminfax, 100 * reciever.len)
+
+ return "[success.len == 0 ? "Факс не удалось доставить до адресата (сломан/обесточен)" : failure.len == 0 ? "Факс успешно доставлен до всех адресатов" : "Факс был *доставлен* до: [jointext(success, ", ")]\nФакс *не был доставлен* до [jointext(failure, ", ")] - (сломан/обесточен)"]"
+ else
+ // Я могу только прочитать или написать факс
+ return "Не удалось распознать аргумент `[parampampam[1]]`"
+*/
+
+#undef PUBLIC_GAME_MODE
diff --git a/proxima/code/modules/TGS/TGSIntegration/TGSChatHelpers.dm b/proxima/code/modules/TGS/TGSIntegration/TGSChatHelpers.dm
new file mode 100644
index 00000000000..fdae7a6e0a3
--- /dev/null
+++ b/proxima/code/modules/TGS/TGSIntegration/TGSChatHelpers.dm
@@ -0,0 +1,171 @@
+/**
+sends a message to chat
+
+config_setting should be one of the following:
+
+- null - noop
+- empty string - use TgsTargetBroadcast with `admin_only = FALSE`
+- other string - use TgsChatBroadcast with the tag that matches config_setting, only works with TGS4, if using TGS3 the above method is used
+*/
+/proc/send2chat(datum/tgs_message_content/message, config_setting)
+ if(config_setting == null || !world.TgsAvailable())
+ return
+
+ var/datum/tgs_version/version = world.TgsVersion()
+ if(config_setting == "" || version.suite == 3)
+ world.TgsTargetedChatBroadcast(message, FALSE)
+ return
+
+ var/list/channels_to_use = list()
+ for(var/I in world.TgsChatChannelInfo())
+ var/datum/tgs_chat_channel/channel = I
+ if(channel.custom_tag == config_setting)
+ channels_to_use += channel
+
+ if(channels_to_use.len)
+ world.TgsChatBroadcast(message, channels_to_use)
+
+/proc/get_active_player_count(var/alive_check = 0, var/afk_check = 0, var/human_check = 0)
+ // Get active players who are playing in the round
+ var/active_players = 0
+ for(var/i = 1; i <= GLOB.player_list.len; i++)
+ var/mob/M = GLOB.player_list[i]
+ if(M && M.client)
+ if(alive_check && M.stat)
+ continue
+ else if(afk_check && M.client.is_afk())
+ continue
+ else if(human_check && !ishuman(M))
+ continue
+ else if(isnewplayer(M)) // exclude people in the lobby
+ continue
+ else if(isghost(M)) // Ghosts are fine if they were playing once (didn't start as observers)
+ var/mob/observer/ghost/O = M
+ if(O.started_as_observer) // Exclude people who started as observers
+ continue
+ active_players++
+ return active_players
+
+/proc/fax2TGS(var/o3, var/from3, var/to3, var/by3, var/intercepted3 = null)
+ var/list/admins = get_admin_counts()["present"]
+ var/obj/item/item = o3
+
+ var/datum/tgs_message_content/message = new("[admins.len = 0 ? "<@&984927384513953852> активных админов с Банхамером нет\n" : null]")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+ embed.title = "Перехват факса"
+ embed.colour = "#00d0ff"
+ 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
+ if (intercepted3)
+ embed.description = "***ФАКС БЫЛ ПЕРЕХВАЧЕН, ПОЛУЧАТЕЛЬ ЕГО __НЕ ВИДИТ__***"
+ var/datum/tgs_chat_embed/field/from = new ("ОТ", "[from3]")
+ var/datum/tgs_chat_embed/field/where = new ("КУДА", "[to3]")
+ var/datum/tgs_chat_embed/field/sentBy = new ("ОТПРАВИЛ", "[by3]")
+ from.is_inline = TRUE
+ where.is_inline = TRUE
+ sentBy.is_inline = TRUE
+ embed.fields = list(from, where, sentBy)
+
+ if(istype(item, /obj/item/paper))
+ var/obj/item/paper/paper = item
+ embed.fields += paper2embed(paper)
+
+ else if (istype(item, /obj/item/photo))
+ var/obj/item/photo/photo = item
+ embed.fields += photo2embed(photo)
+
+ else if (istype(item, /obj/item/paper_bundle))
+ var/obj/item/paper_bundle/bundle = item
+ var/list/pack = bundle2embed(bundle)
+ var/i = 10
+ for(var/subMessage in pack)
+ i += 10
+ addtimer(CALLBACK(world, /world/proc/TgsTargetedChatBroadcast, subMessage, TRUE), i)
+
+ world.TgsTargetedChatBroadcast(message, TRUE)
+ return TRUE
+
+// Костыль для превращения факса в эмбед
+/proc/paper2embed(var/obj/item/paper/paper)
+ . = list()
+
+ var/datum/tgs_chat_embed/field/paper_name = new ("Название бумаги", "*[paper.name]*")
+ //var/datum/tgs_chat_embed/field/paper_language = new ("Язык написания", "*[paper.language.name]*")
+ var/datum/tgs_chat_embed/field/paper_content = new ("Содержимое (чистый HTML)", "```html\n[paper.info]```")
+ var/datum/tgs_chat_embed/field/paper_stamps = new ("Стоят печати", "[replacetext_char(replacetext_char(replacetext_char(paper.stamps, "
", "\n"), "", "*"), "", "*")]")
+
+ . += paper_name
+ //. += paper_language
+ . += paper_content
+ if (paper.stamps)
+ . += paper_stamps
+
+// Костыль для превращения фото в эмбед
+/proc/photo2embed(var/obj/item/photo/photo)
+ . = list()
+
+ var/datum/tgs_chat_embed/field/photo_name = new ("Название фото", "*[photo.name]*")
+ var/datum/tgs_chat_embed/field/photo_scribble
+ if (photo.scribble)
+ photo_scribble = new ("Подпись с обратной стороны", "*[photo.scribble]*")
+ var/datum/tgs_chat_embed/field/photo_size = new ("Размер фото в (тайлах)", "*[photo.photo_size]X[photo.photo_size]*")
+ var/datum/tgs_chat_embed/field/photo_desc
+ if (photo.desc)
+ photo_desc = new ("Описание", "*[photo.desc]*")
+ . += photo_name
+ if (photo_scribble)
+ . += photo_scribble
+ . += photo_size
+ if (photo_desc)
+ . += photo_desc
+
+/proc/bundle2embed(var/obj/item/paper_bundle/bundle)
+ . = list()
+
+ for (var/page = 1, page <= bundle.pages.len, page++)
+ var/datum/tgs_message_content/message = new("")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+ embed.title = "===== Страница [page]/[bundle.pages.len] ====="
+ embed.colour = "#00d0ff"
+ var/obj/item/obj = bundle.pages[page]
+ embed.fields = istype(obj, /obj/item/paper) ? paper2embed(obj) : istype(obj, /obj/item/photo) ? photo2embed(obj) : list(new /datum/tgs_chat_embed/field("НЕИЗВЕСТНЫЙ ТИП БЮРОКРАТИЧЕСКОГО ОРУДИЯ ПЫТОК.", "[obj.type]"))
+ 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
+ . += message
+
+//
+// Костыль для превращения факса в текст
+/proc/paper2text(var/obj/item/paper/paper)
+ . = list()
+ . += "**Название:** [paper.name]"
+ //. += "**Язык написания:** [paper.language.name]"
+ // TODO пропарсить HTML в разметку, принимаемую дискордом?
+ . += "**Содержимое:**\n*Внимание - чистый HTML*\n```html\n[paper.info]```"
+ . = jointext(., "\n")
+
+// Костыль для превращения фото в текст
+/proc/photo2text(var/obj/item/photo/photo)
+ . = list()
+ . += "__*Просмотр фото не может быть имплементирован, но вот некоторые данные о нем*__"
+ . += "**Название:** [photo.name]"
+ if(photo.scribble)
+ . += "**Подпись с обратной стороны:** [photo.scribble]"
+ . += "**Размер фото (в тайлах):** [photo.photo_size]X[photo.photo_size]"
+ . += "**Описание:** [photo.desc == null ? "*Нет описания фото*" : photo.desc]"
+ . = jointext(., "\n")
+
+/proc/bundle2text(var/obj/item/paper_bundle/bundle)
+ . = list()
+ . += "__*Пачка документов*__"
+ for (var/page = 1, page <= bundle.pages.len, page++)
+ var/list/msg = list()
+ msg += "===== Страница № `[page]` ====="
+ var/obj/item/obj = bundle.pages[page]
+ msg += istype(obj, /obj/item/paper) ? paper2text(obj) : istype(obj, /obj/item/photo) ? photo2text(obj) : "НЕИЗВЕСТНЫЙ ТИП БЮРОКРАТИЧЕСКОГО ОРУДИЯ ПЫТОК. ТИП: [obj.type]"
+ msg += "--------------------------"
+ . += jointext(msg, "\n")
+ . += "__*Конец пачки документов*__"
diff --git a/proxima/code/modules/TGS/TGSIntegration/TGSChatHooks.dm b/proxima/code/modules/TGS/TGSIntegration/TGSChatHooks.dm
new file mode 100644
index 00000000000..7da8d33f0f6
--- /dev/null
+++ b/proxima/code/modules/TGS/TGSIntegration/TGSChatHooks.dm
@@ -0,0 +1,164 @@
+/proc/get_world_url()
+ . = "byond://"
+ if(config.serverurl)
+ . += config.serverurl
+ else if(config.server)
+ . += config.server
+ else
+ . += "[world.address]:[world.port]"
+
+// Server startup. Please proceed to the Lobby.
+/world/TgsInitializationComplete()
+ . = ..()
+ var/name = "CEV ERIS"//GLOB.using_map.full_name
+ var/datum/tgs_message_content/message = new ("**Дорогие, <@&839057002046947329>, заходите к нам.**\n\nУ нас эксперементальный запуск __**CEV-ERIS**__")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+ embed.title = "Начинается смена на [name]"
+ embed.description = "Вы можете кликнуть \[cюда\](<[get_world_url()]>) или на фразу \"Сервер 'PRX'\" любого сообщения от меня, чтобы зайти на сервер"
+ embed.colour = "#6590fe"
+ 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
+ send2chat(message, "launch-alert")
+
+// The round has been ended
+/hook/roundend/proc/SendTGSRoundEnd()
+ var/list/data = null//GLOB.using_map.roundend_statistics()
+ var/name = "CEV ERIS"//GLOB.using_map.full_name
+ var/datum/tgs_message_content/message = new ("")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+ embed.title = "Раунд на [name] завершен"
+ embed.colour = "#fffb00"
+ 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
+
+ if (data != null)
+ embed.description = "Статистика подготовлена"
+ var/datum/tgs_chat_embed/field/clients = new ("Всего игроков", "[data["clients"]]")
+ var/datum/tgs_chat_embed/field/survHuman = new ("Выжило экипажа", "[data["surviving_humans"]]")
+ var/datum/tgs_chat_embed/field/survTotal = new ("Выжило органиков", "[data["surviving_total"]]") //required field for roundend webhook!
+ var/datum/tgs_chat_embed/field/ghosts = new ("Наблюдателей", "[data["ghosts"]]") //required field for roundend webhook!
+ var/datum/tgs_chat_embed/field/evacHuman = new ("Эвакуировано экипажа", "[data["escaped_humans"]]")
+ var/datum/tgs_chat_embed/field/evacTotal = new ("Эвакуировано органиков", "[data["escaped_total"]]")
+ var/datum/tgs_chat_embed/field/left = new ("Брошены на произвол судьбы", "[data["left_behind_total"]]") //players who didnt escape and aren't on the station.
+ var/datum/tgs_chat_embed/field/survMisc = new ("Выжило не членов экипажа", "[data["offship_players"]]")
+ clients.is_inline = TRUE
+ survHuman.is_inline = TRUE
+ survTotal.is_inline = TRUE
+ ghosts.is_inline = TRUE
+ //evacHuman.is_inline = TRUE
+ evacTotal.is_inline = TRUE
+ //left.is_inline = TRUE
+ survMisc.is_inline = TRUE
+ embed.fields = list(survHuman, survTotal, evacHuman, evacTotal, left, survMisc, clients, ghosts)
+ else
+ embed.description = "Статистики нет"
+
+ send2chat(message, "launch-alert")
+
+ if (LAZYLEN(GLOB.round_end_notifiees))
+ send2chat(new /datum/tgs_message_content("*Раунд закончился, ребятки. Всем по слапу!*\n[GLOB.round_end_notifiees.Join(", ")]"), "bot-spam")
+ return TRUE
+
+/hook/banned/proc/SendTGSBan(bantype, admin, target, jobs, duration, reason)
+ var/datum/tgs_message_content/message = new ("")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+ var/datum/tgs_chat_embed/field/Fadmin = new ("Администратор", "[admin]")
+ var/datum/tgs_chat_embed/field/Ftarget = new ("Забаненный", "[target]")
+ var/datum/tgs_chat_embed/field/Freason = new ("Причина", "[reason]")
+ Fadmin.is_inline = TRUE
+ Ftarget.is_inline = TRUE
+ embed.fields = list(Ftarget, Fadmin)
+ embed.colour = "#ff0000"
+ 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
+
+ switch(bantype)
+ if (BANTYPE_JOB_PERMA)
+ embed.title = "ПЕРМАНЕНТНАЯ ВРЕМЕННАЯ БЛОКИРОВКА ПРОФЕССИЙ"
+ embed.description = "Пользователь потерял эти роли навсегда"
+ var/datum/tgs_chat_embed/field/Fjobs = new ("Заблокированные профессии", "[jobs]")
+ embed.fields += Fjobs
+ if (BANTYPE_JOB_TEMP)
+ embed.title = "ВРЕМЕННАЯ БЛОКИРОВКА ПРОФЕССИЙ"
+ embed.description = "Пользователь потерял эти роли на время"
+ var/datum/tgs_chat_embed/field/Ftime = new ("Длительность", "[duration]")
+ var/datum/tgs_chat_embed/field/Fjobs = new ("Заблокированные профессии", "[jobs]")
+ Ftime.is_inline = TRUE
+ embed.fields += Ftime
+ embed.fields += Fjobs
+ if (BANTYPE_PERMA)
+ embed.title = "ПЕРМАНЕНТНАЯ БЛОКИРОВКА"
+ embed.description = "Пользователь был забанен навсегда"
+ if (BANTYPE_TEMP)
+ embed.title = "ВРЕМЕННАЯ БЛОКИРОВКА"
+ embed.description = "Пользователь был забанен на время"
+ var/datum/tgs_chat_embed/field/Ftime = new ("Длительность", "[duration]")
+ Ftime.is_inline = TRUE
+ embed.fields += Ftime
+ else
+ embed.title = "Странный бан"
+ embed.description = "Забанил так забанил..."
+
+ embed.fields += Freason
+ send2chat(message, "notes-hub")
+ return TRUE
+
+/hook/unbanned/proc/SendTGSUnBan(admin, target)
+ var/datum/tgs_message_content/message = new ("")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+ var/datum/tgs_chat_embed/field/Fadmin = new ("Покровитель", "[admin]")
+ var/datum/tgs_chat_embed/field/Ftarget = new ("Помилованный", "[target]")
+ Fadmin.is_inline = TRUE
+ Ftarget.is_inline = TRUE
+ embed.fields = list(Ftarget, Fadmin)
+ embed.colour = "#00ff00"
+ embed.title = "Амнистия"
+ 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
+
+ send2chat(message, "notes-hub")
+ return TRUE
+
+/hook/playerNotes/proc/SendTGSNotes(admin, target, note)
+ if (findtext_char(note, "banned") || findtext_char(note, "(MANUAL BAN)"))
+ return TRUE // Это бан (или может быть анбан), нефиг дублировать
+
+ var/datum/tgs_message_content/message = new ("")
+ var/datum/tgs_chat_embed/structure/embed = new()
+ message.embed = embed
+ var/datum/tgs_chat_embed/field/Fadmin = new ("Доносчик", "[admin]")
+ var/datum/tgs_chat_embed/field/Ftarget = new ("Обвиняемый", "[target]")
+ var/datum/tgs_chat_embed/field/Freason = new ("Доносик", "[note]")
+ Fadmin.is_inline = TRUE
+ Ftarget.is_inline = TRUE
+ embed.fields = list(Ftarget, Fadmin, Freason)
+ embed.colour = "#e1ff00"
+ 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
+
+ send2chat(message, "notes-hub")
+ return TRUE
+
+/hook/oocMessage/proc/SendOOCMsg(ckey, message, admin_rank)
+ if (findtext_char(message, "@"))
+ var/mob/M = get_mob_by_key(ckey)
+ if(!M || !M.client || M.client.holder || M.client.deadmin_holder)
+ message_admins("Говно - [ckey] пытался сделать слап. Но я не могу его замутить")
+ return TRUE
+ if(!(M.client.prefs.muted & MUTE_OOC))
+ M.client.prefs.muted |= MUTE_OOC
+ message_admins("Кусок абузера на [ckey] пытался сделать слап. Теперь у него нет ООС")
+ return TRUE
+ send2chat(new /datum/tgs_message_content("**[admin_rank == null ? null : admin_rank][ckey]:** *[replacetext_char(replacetext_char(message, "'", "'"), " "", "\"")]*"), "ooc-chat")
+ return TRUE
+
+#undef NON_BYOND_URL
diff --git a/proxima/code/modules/TGS/TGSIntegration/TGSIntegrationFiles.dm b/proxima/code/modules/TGS/TGSIntegration/TGSIntegrationFiles.dm
new file mode 100644
index 00000000000..6b54375ad36
--- /dev/null
+++ b/proxima/code/modules/TGS/TGSIntegration/TGSIntegrationFiles.dm
@@ -0,0 +1,15 @@
+/hook/startup/proc/InitTgs()
+ world.TgsNew()
+ return TRUE
+
+/world/New()
+ . = ..()
+ world.TgsInitializationComplete()
+
+/world/Reboot()
+ TgsReboot()
+ ..()
+
+/world/Topic()
+ TGS_TOPIC
+ ..()
diff --git a/proxima/code/modules/TGS/_TGSDefines.dm b/proxima/code/modules/TGS/_TGSDefines.dm
new file mode 100644
index 00000000000..9f9ff4a8d3e
--- /dev/null
+++ b/proxima/code/modules/TGS/_TGSDefines.dm
@@ -0,0 +1,40 @@
+#define 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) var/global/_tgs_##Name = ##Value
+
+/// Read the value in the global variable `Name`.
+#define TGS_READ_GLOBAL(Name) global._tgs_##Name
+
+/// Set the value in the global variable `Name` to `Value`.
+#define TGS_WRITE_GLOBAL(Name, Value) global._tgs_##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) to_world(html_encode(message))
+
+/// Notify current in-game administrators of a string `event`.
+#define TGS_NOTIFY_ADMINS(event) message_admins(##event)
+
+/// Write an info `message` to a server log.
+#define TGS_INFO_LOG(message) log_debug("[##message]")
+
+/// Write an warning `message` to a server log.
+#define TGS_WARNING_LOG(message) error("TGS warning: [##message]")
+
+/// Write an error `message` to a server log.
+#define TGS_ERROR_LOG(message) error("TGS error: [##message]")
+
+/// Get the number of connected /clients.
+#define TGS_CLIENT_COUNT global.clients.len
diff --git a/proxima/code/modules/factions/avalon/church.dm b/proxima/code/modules/factions/avalon/church.dm
new file mode 100644
index 00000000000..5c161b37d76
--- /dev/null
+++ b/proxima/code/modules/factions/avalon/church.dm
@@ -0,0 +1,24 @@
+/datum/job/chaplain
+ title = "Neo-Christianity Preacher"
+
+/datum/job/acolyte
+ title = "Alanian Knight"
+
+/datum/job/hydro
+ title = "Alanian Herbalist"
+
+/datum/job/janitor
+ title = "Alanian Sentiel"
+
+/datum/category_item/setup_option/background/origin/new_rome
+ name = "Авалон"
+ desc = "Население этой планеты имеет простую жизнь, не сильно беспокоясь о передовых технологиях,\
+ существующих в других частях известной галактики. Еда здесь в большом количестве и, следовательно, дёшева,\
+ что позволяет населению поддерживать гораздо более крупные семьи. Большинство нелюдей, которые прилетают сюда,\
+ склонны легко адаптироваться к этому простому образу жизни, что позволяет обеспечить относительно более высокую степень стабильности и благополучия.\
+ \
+ Политический строй Авалонского Королевства восходит к временам раннего средневековья людей на Земле и представляет собой феодальную монархию. \
+ Возглавляет королевство король, который действует как глава государства на мировой арене, обладает правом собирать налоги с территорий королевства \
+ через своих вассалов, а также собирает ополчение и созывает армию в военное время, однако власть короля где-либо за пределами его феода максимально формальна.\
+ \
+ Авалон - родина Нео-Христианства. Вы получаете бонусный Язык Алаин"
diff --git a/proxima/code/modules/factions/command/command.dm b/proxima/code/modules/factions/command/command.dm
new file mode 100644
index 00000000000..6db64601263
--- /dev/null
+++ b/proxima/code/modules/factions/command/command.dm
@@ -0,0 +1,5 @@
+/datum/job/captain
+ title = "Captain"
+
+/datum/job/hop
+ title = "First Mate"
diff --git a/proxima/code/modules/factions/ftu/ftu.dm b/proxima/code/modules/factions/ftu/ftu.dm
new file mode 100644
index 00000000000..69793d35bf0
--- /dev/null
+++ b/proxima/code/modules/factions/ftu/ftu.dm
@@ -0,0 +1,14 @@
+/datum/job/merchant
+ title = "FTU Merchant"
+
+/datum/job/cargo_tech
+ title = "FTU Technician"
+
+/obj/landmark/join/start/cargo_tech
+ name = "FTU Technician"
+
+/datum/job/mining
+ title = "FTU Miner"
+
+/datum/department/guild
+ name = "Free Trade Union"
diff --git a/proxima/code/modules/factions/hephestus/engineering.dm b/proxima/code/modules/factions/hephestus/engineering.dm
new file mode 100644
index 00000000000..eb695f06d05
--- /dev/null
+++ b/proxima/code/modules/factions/hephestus/engineering.dm
@@ -0,0 +1,5 @@
+/datum/job/chief_engineer
+ title = "Hephaestus Foreman"
+
+/datum/job/technomancer
+ title = "Hephaestus Engineer"
diff --git a/proxima/code/modules/factions/include_factions.dm b/proxima/code/modules/factions/include_factions.dm
new file mode 100644
index 00000000000..362cf7a7cb0
--- /dev/null
+++ b/proxima/code/modules/factions/include_factions.dm
@@ -0,0 +1,11 @@
+#include "proxima\code\modules\factions\mirania\mirania.dm"
+#include "proxima\code\modules\factions\job_tittles.dm"
+#include "proxima\code\modules\factions\lawson_arms\lawson_arms.dm"
+#include "proxima\code\modules\factions\pcrc\pcrc.dm"
+#include "proxima\code\modules\factions\pcrc\jobs.dm"
+#include "proxima\code\modules\factions\zeng-hu\medical.dm"
+#include "proxima\code\modules\factions\zeng-hu\science.dm"
+#include "proxima\code\modules\factions\ftu\ftu.dm"
+#include "proxima\code\modules\factions\nt\nanotrasens.dm"
+#include "proxima\code\modules\factions\hephestus\engineering.dm"
+#include "proxima\code\modules\factions\avalon\church.dm"
diff --git a/proxima/code/modules/factions/lawson_arms/lawson_arms.dm b/proxima/code/modules/factions/lawson_arms/lawson_arms.dm
new file mode 100644
index 00000000000..d545d43cd66
--- /dev/null
+++ b/proxima/code/modules/factions/lawson_arms/lawson_arms.dm
@@ -0,0 +1,367 @@
+/obj/item/gun/projectile/boltgun/levergun
+ name = "LA BR .40 \"Svengali\""
+
+/datum/design/autolathe/ammo/fs_stinger
+ name = "LA sting shell"
+
+/datum/design/autolathe/gun/colt
+ name = "LA HG .35 Auto \"Colt M1911\""
+
+/datum/design/autolathe/gun/havelock
+ name = "LA REV .35 \"Havelock\""
+ build_path = /obj/item/gun/projectile/revolver/havelock
+
+// .40 handguns
+
+/datum/design/autolathe/gun/revolver
+ name = "LA REV .40 \"Miller\""
+
+/datum/design/autolathe/gun/revolver_consul
+ name = "LA REV .40 \"Consul\""
+
+/datum/design/autolathe/gun/revolver_deckard
+ name = "LA REV .40 \"Deckard\""
+
+/datum/design/autolathe/gun/revolver_mateba
+ name = "LA REV .40 Magnum \"Mateba\""
+
+/datum/design/autolathe/gun/lamia
+ name = "LA HG .40 \"Lamia\""
+
+/datum/design/autolathe/gun/avasarala
+ name = "LA HG .40 \"Avasarala\""
+
+/datum/design/autolathe/gun/pump_shotgun
+ name = "LA SG \"Kammerer\""
+
+/datum/design/autolathe/gun/gladstone
+ name = "LA SG \"Gladstone\""
+
+/datum/design/autolathe/gun/atreides
+ name = "LA SMG .35 \"Atreides\""
+
+/datum/design/autolathe/gun/straylight
+ name = "LA SMG .35 \"Straylight\""
+
+/datum/design/autolathe/gun/molly
+ name = "LA MP .35 \"Molly\""
+
+/datum/design/autolathe/gun/slaught_o_matic //alledgedly a handgun, but practically an SMG
+ name = "LA HG .35 \"Slaught-o-Matic\""
+
+/datum/design/autolathe/gun/wintermute
+ name = "LA AR .20 \"Wintermute\""
+
+/datum/design/autolathe/gun/boltgun_fs
+ name = "LA BR .20 \"Kadmin\""
+
+/datum/design/autolathe/gun/sol
+ name = "LA CAR .25 CS \"Sol\""
+
+/datum/design/autolathe/gun/ak47_fs
+ name = "LA AR .30 \"Vipr\""
+
+/datum/design/autolathe/gun/ak47_fs_ih
+ name = "LA AR .30 \"Venger\""
+
+/datum/design/autolathe/gun/lmg_tk
+ name = "LA LMG .25 \"Takeshi\""
+
+/datum/design/autolathe/gun/grenade_launcher_lenar
+ name = "LA GL \"Lenar\""
+
+/datum/design/autolathe/gun/energygun
+ name = "LA PDW E \"Spider Rose\""
+
+/datum/design/autolathe/gun/energygun_martin
+ name = "LA PDW E \"Martin\""
+
+/datum/design/autolathe/gun/plasma/cassad
+ name = "LA PR \"Cassad\""
+
+/datum/uplink_item/item/visible_weapons/revolver
+ name = "LA REV .40 Magnum \"Miller\" Revolver"
+
+/datum/uplink_item/item/visible_weapons/winchesterrifle
+ name = "LA BR .40 \"Svengali\""
+
+/datum/uplink_item/item/visible_weapons/lshotgun
+ name = "LA BR \"Sogekihei\""
+
+/obj/item/grenade/chem_grenade/incendiary
+ name = "LA IG \"River\""
+
+/obj/item/grenade/chem_grenade/teargas
+ name = "LA TGG \"Simon\""
+
+/obj/item/grenade/empgrenade
+ name = "LA EMPG \"Frye\""
+
+/obj/item/grenade/empgrenade/low_yield
+ name = "LA EMPG \"Frye\" - C"
+
+/obj/item/grenade/flashbang
+ name = "LA FBG \"Serra\""
+ desc = "A \"Lawson Arms\" flashbang grenade. If in any doubt - use it."
+
+/obj/item/grenade/frag/sting
+ name = "LA SG \"Hornet\""
+ desc = "A high-grade \"Lawson Arms\" sting grenade, for use against unruly crowds."
+
+/obj/item/grenade/heatwave
+ name = "LA HG \"Phoenix\""
+
+/obj/item/grenade/smokebomb
+ name = "LA SG \"Reynolds\""
+
+
+/obj/item/tool/hammer/sledgehammer/ironhammer //triple hammer!
+ name = "LA \"Ironhammer\" Breaching Hammer"
+ desc = "A modified sledgehammer produced by Lawson Arms for PMC forces. This tool can take down standard walls and if the user is strong enough, reinforced walls."
+
+/obj/item/gun/energy/chameleon
+ name = "LA HG .40 Magnum \"Avasarala\""
+
+/obj/item/clothing/gloves/stungloves
+ name = "LA Power Glove"
+
+/obj/item/ammo_casing/grenade
+ name = "LA SR \"Sasumata\""
+ desc = "A high-grade Lawson Arms sting round, for use against unruly crowds."
+
+/obj/item/gun/energy/gun
+ name = "LA PDW E \"Spider Rose\""
+
+/obj/item/gun/energy/gun/martin
+ name = "LA PDW E \"Martin\""
+ desc = "Martin is essentialy downscaled Spider Rose, made for PMC employees and civilians to use it as personal self defence weapon."
+
+/obj/item/gun/energy/plasma/cassad
+ name = "LA PR \"Cassad\""
+ desc = "\"Lawson Arms\" brand energy assault rifle, capable of prolonged combat. When surrender is not an option."
+
+/obj/item/gun/projectile/shotgun/pump/grenade/lenar
+ name = "LA GL \"Lenar\""
+
+/obj/item/gun/projectile/automatic/atreides
+ name = "LA SMG .35 Auto \"Atreides\""
+
+/obj/item/gun/projectile/automatic/lmg/tk
+ name = "LA LMG .25 CS Takeshi"
+ desc = "The \"Takeshi LMG\" is LA's answer to PMC's needs for mass supression and meat grinding, a fine oiled machine of war and death."
+
+/obj/item/gun/projectile/automatic/molly
+ name = "LA MP .35 Auto \"Molly\""
+
+/obj/item/gun/projectile/automatic/slaught_o_matic
+ name = "LA HG .35 Auto \"Slaught-o-Matic\""
+ desc = "This disposable plastic handgun is mass-produced by \"Lawson Arms\" for civilian use. It often is used by street urchin, thugs, or terrorists on a budget. For what it's worth, it's not an awful handgun - but you only get one magazine before the gun locks up and becomes useless."
+
+/obj/item/gun/projectile/automatic/sol
+ name = "LA CAR .25 CS \"Sol\""
+ desc = "A standard-issue weapon often used by PCRC operatives. Compact and reliable. Uses .25 Caseless rounds."
+
+/obj/item/gun/projectile/automatic/straylight
+ name = "LA SMG .35 Auto \"Straylight\""
+ desc = "A compact, lightweight and cheap rapid-firing submachine gun. In past was primarily used for testing ammunition and weapon modifications, \
+ nowadays mass produced for PMC or security forces. Suffers from poor recoil control and underperforming ballistic impact, \
+ but makes up for this through sheer firerate. Especially effective with rubber ammunition. Uses .35 Auto rounds."
+
+/obj/item/gun/projectile/boltgun/fs
+ name = "LA BR .20 \"Kadmin\""
+
+/obj/item/gun/projectile/boltgun/fs/civilian
+ name = "LA BR .20 \"Arasaka\""
+
+/obj/item/gun/projectile/boltgun/levergun
+ name = "LA BR .40 \"Svengali\""
+
+/obj/item/gun/projectile/boltgun/levergun/sawn
+ name = "sawn-off LA BR .40 \"Svengali\""
+
+/obj/item/gun/projectile/boltgun/levergun/shotgun
+ name = "LA BR \"Sogekihei\""
+
+/obj/item/gun/projectile/boltgun/levergun/shotgun/sawn
+ name = "sawn-off LA BR \"Sogekihei\""
+
+/obj/item/gun/projectile/giskard
+ name = "LA HG .35 Auto \"Giskard\""
+ desc = "A popular \"Lawson Arms\" brand pocket pistol chambered for the ubiquitous .35 auto round. Uses standard capacity magazines."
+
+/obj/item/gun/projectile/lamia
+ name = "LA HG .40 Magnum \"Lamia\""
+ desc = "LA HG .40 Magnum \"Lamia\", a heavy pistol of PMC enforcers. Uses 40 Magnum rounds."
+
+/obj/item/gun/projectile/olivaw
+ name = "LA MP .35 Auto \"Olivaw\""
+ desc = "A popular \"Lawson Arms\" machine pistol. This one has a two-round burst-fire mode and is chambered for .35 auto. It can use normal and high capacity magazines."
+
+/obj/item/gun/projectile/paco
+ name = "LA HG .35 Auto \"Paco\""
+ desc = "A modern and reliable sidearm for the soldier in the field. Commonly issued as a sidearm to PCRC Operatives. Uses standard .35 and high capacity magazines."
+
+/obj/item/gun/projectile/revolver/capgun
+ name = "LA REV .357 \"Miller\"" //for that epic clown robbery meme
+
+/obj/item/gun/projectile/revolver/consul
+ name = "LA REV .40 Magnum \"Consul\""
+
+/obj/item/gun/projectile/revolver/deckard
+ name = "LA REV .40 Magnum \"Deckard\""
+
+/obj/item/gun/projectile/revolver/havelock
+ name = "LA REV .35 Auto \"Havelock\""
+ desc = "A cheap \"Lawson Arms\" knock-off of a Smith & Wesson Model 10. Uses .35 Auto rounds."
+
+/obj/item/gun/projectile/revolver/mateba
+ name = "LA REV .40 Magnum \"Mateba\""
+
+/obj/item/gun/projectile/revolver
+ name = "LA REV .40 Magnum \"Miller\""
+ desc = "The \"Lawson Arms\" \"Miller\" is a revolver of choice when you absolutely, positively need to make a hole in someone. Uses .40 Magnum ammo."
+
+/obj/item/gun/projectile/shotgun/bull
+ name = "LA SG \"Bull\""
+ desc = "A \"Lawson Arms\" double-barreled pump-action shotgun. Marvel of engineering, this gun is often used by PMC tactical units. \
+ Due to shorter than usual barrels, damage are somewhat lower and recoil kicks slightly harder, but possibility to fire two barrels at once overshadows all bad design flaws. Can hold up to 7+2 shells."
+
+/obj/item/gun/projectile/shotgun/pump/gladstone
+ name = "LA SG \"Gladstone\""
+ desc = "It is a next-generation Lawson Arms shotgun intended as a cost-effective competitor to the aging NT \"Regulator 1000\". It has a semi-rifled lightweight full-length barrel which gives it exceptional projectile velocity and armor piercing capabilites with slugs, with a high-capacity magazine tube below it. Can hold up to 9+1 shells in a tube magazine."
+
+/obj/item/gun/projectile/shotgun/pump
+ name = "LA SG \"Kammerer\""
+
+/datum/design/autolathe/sec/watchman
+ name = "Lawson Arms \"Watchman\" scope"
+
+/datum/category_item/setup_option/background/origin/shimatengoku
+ name = "Тау Кита"
+ desc = "Кита Эпсилон - обитаемая планета с умеренным и тёплым климатом расположенная в системе Тау-Сети. \
+ Планета представляет собой большой урбанистический город под названием Верлиз, поделенный несколько отдельных районов. \
+ \"Кита\", как называют ее жители, является пристанищем для многочисленных корпоративных заводов, офисов и штаб квартир, \
+ а так же имеет при себе крупнейшее и самое престижное учебное заведение во всём ЦПСС - \"Технологический Университет Тау-Кита\".\
+ \
+ Дает бонусный доступ к языку Янгуй."
+
+/datum/uplink_item/item/visible_weapons/dna_trigger
+ name = "Lawson Arms \"DNA lock\" trigger"
+
+/obj/machinery/vending/weapon_machine
+ name = "Lawson Arms Guns&Ammo"
+ desc = "A self-defense equipment vending machine. When you need to take care of that clown."
+ product_slogans = "The best defense is good offense!;Buy for your whole family today!;Nobody can outsmart bullet!;God created man - Lawson Arms made them EQUAL!;Stupidity can be cured! By LEAD.;Dead kids can't bully your children!"
+
+/obj/item/computer_hardware/hard_drive/portable/design/nonlethal_ammo
+ disk_name = "Lawson Arms Nonlethal Magazines Pack"
+
+/obj/item/computer_hardware/hard_drive/portable/design/lethal_ammo
+ disk_name = "Lawson Arms Lethal Magazines Pack"
+
+/obj/item/computer_hardware/hard_drive/portable/design/ammo_boxes_smallarms
+ disk_name = "Lawson Arms .35 and .40 Ammunition"
+
+/obj/item/computer_hardware/hard_drive/portable/design/ammo_boxes_rifle
+ disk_name = "Lawson Arms Rifle Ammunition"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_cheap_guns
+ disk_name = "Lawson Arms Basic - .35 Civilian Pack"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_colt
+ disk_name = "Lawson Arms - .35 Colt 1911"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_revolver_miller
+ disk_name = "Lawson Arms- .40 Miller Revolver"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_revolver_consul
+ disk_name = "Lawson Arms - .40 Consul Revolver"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_revolver_deckard
+ disk_name = "Lawson Arms - .40 Deckard Revolver"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_revolver_mateba
+ disk_name = "Lawson Arms - .40 Mateba Revolver"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_lamia
+ disk_name = "Lawson Arms - .40 Lamia Handgun"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_deagle
+ disk_name = "Lawson Arms - .40 Avasarala Handgun"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_doublebarrel
+ disk_name = "Lawson Arms - .50 Double Barrel Shotgun"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_kammerer
+ disk_name = "Lawson Arms - .50 Kammerer Shotgun"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_gladstone
+ disk_name = "Lawson Arms - .50 Gladstone Shotgun"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_paco
+ disk_name = "Lawson Arms - .35 Paco HG"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_straylight
+ disk_name = "Lawson Arms - .35 Straylight SMG"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_molly
+ disk_name = "Lawson Arms - .35 Molly SMG"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_atreides
+ disk_name = "Lawson Arms - .35 Atreides SMG"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_slaught_o_matic
+ disk_name = "Lawson Arms - .35 Slaught-o-Matic HG"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_wintermute
+ disk_name = "Lawson Arms - .20 Wintermute Assault Rifle"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_sol
+ disk_name = "Lawson Arms - .25 Sol Caseless SMG Pack"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_kalashnikov
+ disk_name = "Lawson Arms - .30 Hunting Rifle Pack"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_kalashnikov_ih
+ disk_name = "Lawson Arms - .30 PD Rifle Pack"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_lenar
+ disk_name = "Lawson Arms - Lenar Grenade Launcher"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_spiderrose
+ disk_name = "Lawson Arms - Spider Rose PDW E"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_martin
+ disk_name = "Lawson Arms - Martin PDW E"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_cassad
+ disk_name = "Lawson Arms - Cassad Plasma Rifle"
+
+/obj/structure/sign/faction/frozenstar
+ name = "Lawson Arms"
+
+/datum/language/neohongo
+ name = LANGUAGE_NEOHONGO
+ desc = "The language widely used by Pan-Asian population"
+
+/obj/item/organ/external/robotic/frozen_star
+ name = "\"Lawson Arms\""
+ desc = "Tactical \"Lawson Arms\" blue and gray prosthesis for dangerous environment."
+
+/obj/item/gun_upgrade/trigger/dangerzone
+ name = "Lawson Arms \"Danger Zone\" Trigger"
+
+/obj/item/gun_upgrade/trigger/cop_block
+ name = "Lawson Arms \"Cop Block\" Trigger"
+
+/obj/item/gun_upgrade/trigger/dnalock
+ name = "Lawson Arms \"DNA lock\" Trigger"
+
+/obj/item/gun_upgrade/mechanism/overshooter
+ name = "Lawson Arms \"Overshooter\" internal magazine kit"
+
+/obj/item/gun_upgrade/mechanism/weintraub
+ name = "Lawson Arms \"Weintraub\" full auto kit"
+
+/obj/item/gun_upgrade/scope/watchman
+ name = "Lawson Arms \"Watchman\" scope"
diff --git a/proxima/code/modules/factions/mirania/mirania.dm b/proxima/code/modules/factions/mirania/mirania.dm
new file mode 100644
index 00000000000..6fef83bb517
--- /dev/null
+++ b/proxima/code/modules/factions/mirania/mirania.dm
@@ -0,0 +1,106 @@
+/datum/category_item/setup_option/background/origin/oberth
+ name = "Мирания"
+ desc = "Мираниане очень гордые. Их происхождение как аграрной и чрезвычайно изолированной колонии выработало \
+ у народа горделивый характер, поскольку всё на планете воспринимается как работа рук миранианов, которые сделали колонию \
+ великой. Новеньким здесь приходится несладко, и многих из иммигрантов направляют на “черные” рабочие места и неквалифицированный труд. \
+ Потомки первоначальных колонистов составляют класс “Старые Мираниане”, являющиеся богатыми лицами и занимающие непропорционально \
+ большое количество правительственных должностей.\
+ \
+ Дает бонусный доступ к Миранийскому языку."
+
+/obj/machinery/jammer
+ name = "Miranian Portable Signal Jammer"
+
+/obj/item/device/jammer
+ name = "Miranian Mobile Jammer"
+
+/obj/item/bluespace_harpoon/mounted/blitz
+ desc = "Reverse engineered version of harpoon developed by old Nanotrasen, remounted for robotic use only by Miranian Republic."
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/or_silenced
+ disk_name = "Miraian Republic - .25 Mandella"
+
+/obj/item/computer_hardware/hard_drive/portable/design/guns/fs_bulldog
+ disk_name = "Miraian Republic - .20 Bulldog Carabine"
+
+/datum/gear/gloves/german
+ display_name = "gloves, Mirania"
+
+/datum/gear/head/beret/oberth
+ display_name = "beret, Mirania" //Uberth
+
+/datum/gear/uniform/battledress_german
+ display_name = "battle dress uniform, Mirania"
+
+/obj/item/clothing/gloves/german
+ name = "Miranian Republic gloves"
+
+/obj/item/clothing/head/beret/german
+ name = "Miranian Republic beret"
+
+/obj/item/clothing/mask/gas/german
+ name = "Miranian Republic gas mask"
+
+/obj/item/clothing/shoes/jackboots/german
+ name = "Miranian Republic boots"
+
+/obj/item/clothing/suit/storage/greatcoat/german_overcoat
+ name = "Miranian Republic uniform overcoat"
+
+/obj/item/clothing/under/germansuit
+ name = "Miranian Republic Battle Dress Uniform"
+
+/datum/language/german
+ name = LANGUAGE_GERMAN
+ desc = "Language used by the inhabitants of Mirania."
+
+/obj/item/gun/energy/shrapnel
+ name = "MR ESG \"Shellshock\" energy shotgun"
+ desc = "An Miranian Republic Self Defence Force design, this mat-fab shotgun tends to burn through cells with use. The matter contained in empty cells can be converted directly into ammunition as well, if the safety bolts are loosened."
+
+/obj/item/gun_upgrade/barrel/blender
+ name = "MR \"Bullet Blender\" barrel"
+ desc = "A curious-looking barrel bearing the Mirania insignia. A small label reads \"No refunds for any collateral damage caused\"."
+
+/obj/item/gun/projectile/automatic/sts35
+ name = "MR SDF AR .30 \"STS-35\""
+ desc = "The rugged STS-35 is a durable automatic weapon, made by Miranian Republic Self Defence Force. \
+ Extremely efficient rifle design that was put in service right before collapse of the Republic, this weapon can be found almost anywhere in the galaxy by now. \
+ Uses .30 Rifle rounds."
+
+/obj/item/gun/projectile/automatic/z8
+ name = "MR CAR .20 \"Z8 Bulldog\""
+ desc = "The Z8 Bulldog is an older bullpup carbine model, made by \"Miranian Republic\". It includes an underbarrel grenade launcher which is compatible with most modern grenade types. Uses .20 Rifle rounds."
+
+/obj/item/gun/projectile/boltgun/levergun
+ desc = "A perfect modernization of an old earth classic hailing popular use with gun ho lawmen and bounty hunters. \
+ Marketed as the number one choice for crack shots on Miranian colonies and large vessels."
+
+/obj/item/gun/projectile/revolver/hornet
+ name = "MR REV .20 \"LBR-8 Hornet\""
+ desc = "An attempt by Mirania to replicate lost Terran Commonwealth tech. In order to achieve satisfactory ballistic \
+ performance, it sports an usually long barrel and overpressurized chamber. Uses .20 rifle rounds."
+
+/datum/design/autolathe/gun/mandella
+ name = "MR HG .25 CS \"Mandella\""
+
+/datum/design/autolathe/gun/z8
+ name = "MR CAR .20 \"Z8 Bulldog\""
+
+/datum/design/autolathe/gun/sts35
+ name = "MR SDF AR .30 \"STS-35\""
+
+/datum/uplink_item/item/visible_weapons/pistol
+ name = "MR HG .25 CS \"Mandella\" Silenced Handgun"
+
+/datum/uplink_item/item/visible_weapons/hornet
+ name = "MR REV .20 \"LBR-8 Hornet\" Revolver"
+
+/datum/uplink_item/item/visible_weapons/assaultrifle
+ name = "MR BR \"STS-35\" Battle Rifle"
+
+/datum/uplink_item/item/visible_weapons/blender
+ name = "MR \"Bullet Blender\" barrel"
+
+/obj/item/ammo_casing/grenade/blast
+ name = "MR OBR \"Puff\""
diff --git a/proxima/code/modules/factions/nt/nanotrasens.dm b/proxima/code/modules/factions/nt/nanotrasens.dm
new file mode 100644
index 00000000000..426feb4955d
--- /dev/null
+++ b/proxima/code/modules/factions/nt/nanotrasens.dm
@@ -0,0 +1,128 @@
+/datum/design/autolathe/cell/large
+ name = "NanoTrasen \"Robustcell 1000L\""
+
+/datum/design/autolathe/cell/large/high
+ name = "NanoTrasen \"Robustcell 5000L\""
+
+/datum/design/autolathe/cell/medium
+ name = "NanoTrasen \"Robustcell 600M\""
+ build_path = /obj/item/cell/medium
+
+/datum/design/autolathe/cell/medium/high
+ name = "NanoTrasen \"Robustcell 800M\""
+ build_path = /obj/item/cell/medium/high
+
+/datum/design/autolathe/cell/small
+ name = "NanoTrasen \"Robustcell 100S\""
+ build_path = /obj/item/cell/small
+
+/datum/design/autolathe/cell/small/high
+ name = "NanoTrasen \"Robustcell 200S\""
+ build_path = /obj/item/cell/small/high
+
+/datum/design/autolathe/part/laserguide
+ name = "NanoTrasen \"Guiding Light\" laser guide"
+ build_path = /obj/item/tool_upgrade/refinement/laserguide
+
+/datum/design/autolathe/part/diamondblade
+ name = "NanoTrasen \"Gleaming Edge\": Diamond blade"
+ build_path = /obj/item/tool_upgrade/productivity/diamond_blade
+
+/obj/machinery/vending/powermat
+ name = "Nanotrasen Power-Mat"
+
+/obj/machinery/vending/printomat
+ name = "Nanotrasen Print-o-Mat"
+
+/obj/machinery/vending/style
+ name = "Gilthari Exports Style-o-matic"
+ desc = "Gilthari Exports vendor selling, possibly stolen, most likely overpriced, stylish clothing."
+
+
+/obj/item/cell/large
+ name = "Nanotrasen \"Robustcell 1000L\""
+ desc = "Nanotrasen branded rechargeable L-standardized power cell. This one is the cheapest you can find."
+
+/obj/item/cell/large/high
+ name = "Nanotrasen \"Robustcell 5000L\""
+ desc = "Nanotrasen branded rechargeable L-standardized power cell. Popular and reliable version."
+
+/obj/item/cell/large/super
+ name = "Nanotrasen \"Robustcell 15000L\""
+ desc = "Nanotrasen branded rechargeable L-standardized power cell. This advanced version can store even more energy."
+
+/obj/item/cell/large/hyper
+ name = "Nanotrasen \"Robustcell-X 20000L\""
+ desc = "Nanotrasen branded rechargeable L-standardized power cell. Looks like this is a rare and powerful prototype."
+
+/obj/item/cell/medium
+ name = "Nanotrasen \"Robustcell 600M\""
+ desc = "Nanotrasen branded rechargeable M-standardized power cell. This one is the cheapest you can find."
+
+
+/obj/item/cell/medium/high
+ name = "Nanotrasen \"Robustcell 800M\""
+ desc = "Nanotrasen branded rechargeable M-standardized power cell. Popular and reliable version."
+
+
+/obj/item/cell/medium/super
+ name = "Nanotrasen \"Robustcell 1000M\""
+ desc = "Nanotrasen branded rechargeable M-standardized power cell. This advanced version can store even more energy."
+
+
+/obj/item/cell/medium/hyper
+ name = "Nanotrasen \"Robustcell-X 1500M\""
+ desc = "Nanotrasen branded rechargeable M-standardized power cell. Looks like this is a rare and powerful prototype."
+
+/obj/item/cell/small
+ name = "Nanotrasen \"Robustcell 100S\""
+ desc = "Nanotrasen branded rechargeable S-standardized power cell. This one is the cheapest you can find."
+
+/obj/item/cell/small/high
+ name = "Nanotrasen \"Robustcell 200S\""
+ desc = "Nanotrasen branded rechargeable S-standardized power cell. Popular and reliable version."
+
+
+/obj/item/cell/small/super
+ name = "Nanotrasen \"Robustcell 300S\""
+ desc = "Nanotrasen branded rechargeable S-standardized power cell. This advanced version can store even more energy."
+
+/obj/item/cell/small/hyper
+ name = "Nanotrasen \"Robustcell-X 500S\""
+ desc = "Nanotrasen branded rechargeable S-standardized power cell. Looks like this is a rare and powerful prototype."
+
+/obj/item/computer_hardware/hard_drive/portable/design/tools
+ disk_name = "Nanotrasen Basic Tool Pack"
+
+/obj/item/computer_hardware/hard_drive/portable/design/misc
+ disk_name = "Nanotrasen Miscellaneous Pack"
+
+/obj/item/computer_hardware/hard_drive/portable/design/devices
+ disk_name = "Nanotrasen Devices and Instruments"
+
+/obj/item/computer_hardware/hard_drive/portable/design/robustcells
+ disk_name = "Nanotrasen Robustcells"
+
+/obj/item/computer_hardware/hard_drive/portable/design/armor/asters
+ disk_name = "Nanotrasen Enforcement Armor Pack"
+
+/obj/item/grenade/chem_grenade/metalfoam
+ name = "Nanotrasen \"Stop-Space\""
+
+/obj/item/grenade/chem_grenade/antiweed
+ name = "Nanotrasen \"Flora Armageddon\""
+
+/obj/item/grenade/chem_grenade/cleaner
+ name = "Nanotrasen \"Shit-Be-Gone\""
+
+/obj/item/tool/omnitool
+ name = "Nanotrasen \"Munchkin 5000\""
+
+/obj/item/tool_upgrade/productivity/diamond_blade
+ name = "Nanotrasen \"Gleaming Edge\": Diamond blade"
+
+/obj/item/tool_upgrade/refinement/laserguide
+ name = "Nanotrasen \"Guiding Light\" laser guide"
+
+/obj/item/clothing/accessory/armband/cargo
+ name = "Free Trade Union armband"
diff --git a/proxima/code/modules/factions/pcrc/jobs.dm b/proxima/code/modules/factions/pcrc/jobs.dm
new file mode 100644
index 00000000000..e19e8dd2e63
--- /dev/null
+++ b/proxima/code/modules/factions/pcrc/jobs.dm
@@ -0,0 +1,110 @@
+/datum/job/ihc
+ title = "PCRC Commander"
+ description = "You are the commander of the local regiment of the Proxima Centauri Risk Control company, contracted to protect and serve aboard the IEV Hyperion. PCRC serves as both an internal security force, and as a guard for expeditions outwith the ship.
\
+
\
+ Your goal is to keep everyone aboard the ship as safe as possible, and to eliminate any threats to safety.
\
+ The Gunnery Sergeant is your second in command, and any of your duties can be delegated to him at your discretion"
+
+ duties = " Coordinate operatives in the field, assigning them to threats and distress calls as needed.
\
+ Allocate department funds for necessary supplies, equipment, armor, weapons, upgrades, etc. Spend your money as required to ensure your troops are at peak combat performance
\
+ Plan assaults on entrenched threats, ensure each operative knows their roles and carries them out precisely.
\
+ Oversee performance of the operatives under your command, and punish any that are insubordinate or incompetent
\
+ Advise the captain on threats to ship security, and counsel him towards choices that will minimise exposure to threats."
+
+ loyalties = " As commander, your first loyalty is to the safety of the troops under your command. They are elite professional soldiers, not cannon fodder. Do not allow them to be sent on suicide missions. Any killings of your men should be repaid in blood
\
+
\
+ Your second loyalty is to the name and reputation of the Proxima Centauri Risk Control company. You are often the captain's primary tool in keeping order and you must pride yourself on ensuring commands are carried out, threats extinguished and safety preserved. You may need to carry out unsavory orders like executions, and must balance your professional pride versus your conscience.
\
+
\
+ Your third loyalty is to the crew. As the strongest military force on the ship, any mutiny attempt is likely at your mercy, and if unjustified, it will fall to you to put it down. If the captain has gone mad and a mutiny is justified, your support will be the difference between a peaceful arrest and a bloody civil war in the halls. Without your guns, an insane captain will usually be forced to surrender."
+
+/obj/landmark/join/start/ihc
+ name = "PCRC Commander"
+
+/datum/job/gunserg
+ title = "PCRC Gunnery Sergeant"
+ supervisors = "the PCRC Commander"
+ description = "You are the Second-in-Command of the local Proxima Centauri Risk Control company regiment, and the defacto leader if the commander isn't around.
\
+ Within PCRC you largely hold a desk job, your duties will rarely take you outside of the PCRC wing, and you are not expected to interact with civilians. You have enough to deal with as is, and are probably the hardest working member of PCRC.
\
+
\
+ You have several core duties:
\
+ 1. As second in command, any of the commander's duties may be delegated to you, if they decide to do so. This means that at any time, you may be expected to handle funding, paperwork, disciplinary matters, planning combat tactics, or even carrying out executions. If there's no commander, these duties fall naturally to you. If there is a commander on site though, you shouldn't make these kind of decisions without consulting them.
\
+
\
+ 2. You serve as the PCRC quartermaster. And as such, it is your job to maintain the armoury, and stocks of other equipment. You should keep track of its contents, and who has what. Make sure weapons and equipment are returned at the end of a shift, and procure new armaments from the guild or from scavengers as necessary to keep supplies up and respond to new threat s.
\
+
\
+ 3. You are the defacto warden, and if there are any prisoners being kept in the PCRC brig, it is your responsibility to ensure they are fed, treated appropriately with regard to their legal rights, and ensure they have access to medical care. If necessary you may need to suppress riots or escape attempts within the brig too.
\
+
\
+ 4. In times of peace, prepare for war. To this end, you are also the onsite military instructor. If the ship is in a lull and there are no outstanding threats, you should take the initiative to order training drills. Allow junior operatives to train and learn with less conventional weapons and tactics, give lessons on aiming, trigger discipline, hand to hand combat. Conduct drills on threat response, squad tactics, and EVA manoeuvres.
\ "
+
+ loyalties = "You're a military man through and through. As such, your first loyalty is to the Commander, and thusly to the chain of command"
+
+/obj/landmark/join/start/gunserg
+ name = "PCRC Gunnery Sergeant"
+
+/datum/job/inspector
+ title = "PCRC Inspector"
+ supervisors = "the PCRC Commander"
+ description = "You are the ship's detective, here to take care of the cases that aren't always what they seem, and suspects that aren't always caught red handed or ready to confess.
\
+ The inspector's job is to interrogate suspects, gather witness statements, harvest evidence and reach a conclusion about the nature and culprit of a crime.
\
+
\
+ You are a higher ranking Proxima Centauri Risk Control officer, and you can give commands to operatives. But this doesn't mean you should be commanding assaults. You're not any kind of tactical commander
\
+
\
+ When there are no outstanding cases, your job is to go look for them. Mingle with civilians, interact and converse, sniff out leads about potential criminal activity. The PCRC budget can often include stipends to pay informers for any useful info"
+
+ duties = " Interview suspects and witnesses after a crime. Record important details of their statements, and look for inconsistencies.
\
+ Gather evidence and bring it back for processing
\
+ Send out operatives to bring suspects in for questioning
\
+ Interact with civilians and be on the lookout for criminal activity"
+
+ loyalties = " As a detective, your loyalty is firstly, to the truth and justice. Seek to uncover the true events of any crime.
\
+
\
+ Secondly, you are loyal to PCRC and to the commander. Follow the chain of command"
+
+/obj/landmark/join/start/inspector
+ name = "PCRC Inspector"
+
+/datum/job/medspec
+ title = "PCRC Medical Specialist"
+ supervisors = "the PCRC Commander"
+ description = "You are a highly trained specialist within Proxima Centauri Risk Control. You were probably a medical student or inexperienced doctor when you joined PCRC, and you thusly have a combination of medical and military training. You are not quite as knowledgeable as a civilian career doctor, not quite as much of a fighter as a dedicated PCRC operative, but strike a balance inbetween. Balance is the nature of your existence.
\
+
\
+ Within PCRC, you have three roles to undertake. All of your roles can be delegated to others when needed - Moebius Medical for roles 1 and 2, the PCRC Inspector for role 3. But you are often the best positioned to carry out these tasks, especially when time is short
\
+
\
+ 1. Field Medic.
\
+ You may be expected to serve on the backlines in a combat situation, treating and stabilising the wounded, making the call as to whether they can return to combat or leave by medivac. You may need to perform emergency trauma surgery in undesireable conditions.
\
+ You are allowed to be armed, but remember that saving lives, not taking them, is your first duty. Don't be afraid to send patients to moebius medical for proper specialist care.
\
+
\
+ 2. Prison Doctor.
\
+ During quiet times, when inmates are serving in the brig, you will often be required to treat prisoners, criminal suspects, and the condemned. Suicide attempts are common in prison, and you will often be treating a patient against their will, who is attempting to escape. When serving in this role, stay on guard, work closely with the gunnery sergeant, and keep control of the situation
\
+
\
+ 3. Forensic Specialist.
\
+ Solving crimes often requires scientific analysis, and expert rulings from a trusted source within PCRC. You will often be expected to analyze blood, chemicals and fingerprints, conduct autopsies, and submit your findings to help track down elusive culprits. In this task, you will work closely with the inspector, and if necessary, he often has the talents to perform these tasks. But his time is better spent questioning and interrogating people"
+
+/obj/landmark/join/start/medspec
+ name = "PCRC Medical Specialist"
+
+/datum/job/ihoper
+ title = "PCRC Operative"
+ description = "You are the boots on the ground, the rifle in the window, the long arm of the law. You are the hand of Proxima Centauri Risk Control, and the frontline against criminals, terrorists, and xenos.
\
+
\
+ You are a professional soldier and a hardened mercenary, no stranger to violence. You are required to employ your talents in order to bring an end to threats and conflict situations. As a consummate professional, you're often expected to put your pride aside, and work with others. Tactics and teamwork are vital.
\
+
\
+ You are paid to act, not to think. When in doubt, follow orders, and leave the hard choices to someone else. Trust in your chain of command. Remember that you are the lowest rank in PCRC, and you report to everyone else in your organisation. Inspector, medspec, gunnery sergeant and commander, are all your superior officers, their orders should be obeyed.
\
+
\
+ When there are no standing orders, your ongoing task is to patrol the ship and be on the lookout for threats. Check in at departments, ask if there are any concerns, break up fights and do your best to prevent trouble before it spirals out of control. Wipe out roaches and other dangerous creatures wherever you encounter them.
\
+
\
+ You have almost-total access to the ship in order to carry out your duties and reach threats quickly. Do not abuse this. It does not mean you can walk into anywhere you like, many areas are full of sensitive machinery and entering unnanounced can be harmful to your health. Do not steal from departments either. If it's not in the PCRC wing, it doesn't belong to you. Stealing from the Guild is a good way to get shot in the back"
+
+ duties = " Patrol the ship, provide a security presence, and look for trouble
\
+ Subdue and arrest criminals, terrorists, and other threats
\
+ Exterminate monsters, giant vermin and hostile xenos
\
+ Follow orders from the chain of command
\
+ Obey the law. You are not above it"
+
+ loyalties = " As a soldier, your first loyalty is to the chain of command, which ends with the PCRC Commander. Their orders are supreme over all, even if they're currently leading a mutiny against the captain.
\
+
\
+ Your second loyalty is to your fellow PCRC brothers in arms. As long as the company takes care of you, you should follow orders. But if you start being sent on suicide missions and treated as expendable fodder, that should change.
\
+
\
+ Your third loyalty is to humanity. You are still human under all that armour. If you're being ordered to slaughter civilians en masse, it may be time to start thinking for yourself."
+
+/obj/landmark/join/start/ihoper
+ name = "PCRC Operative"
diff --git a/proxima/code/modules/factions/pcrc/pcrc.dm b/proxima/code/modules/factions/pcrc/pcrc.dm
new file mode 100644
index 00000000000..1ec891ce2ab
--- /dev/null
+++ b/proxima/code/modules/factions/pcrc/pcrc.dm
@@ -0,0 +1,3 @@
+/datum/design/autolathe/container/ammocan_ih
+ name = "PCRC Ammo Can"
+ build_path = /obj/item/storage/hcases/ammo/ih
diff --git a/proxima/code/modules/factions/zeng-hu/medical.dm b/proxima/code/modules/factions/zeng-hu/medical.dm
new file mode 100644
index 00000000000..5f8200fc8c5
--- /dev/null
+++ b/proxima/code/modules/factions/zeng-hu/medical.dm
@@ -0,0 +1,18 @@
+/datum/job/cmo
+ title = "Zeng-Hu Biolab Officer"
+ supervisors = "the Zeng-Hu Expedition Overseer"
+
+/datum/job/doctor
+ title = "Zeng-Hu Doctor"
+
+/datum/job/chemist
+ title = "Zeng-Hu Pharmacist"
+
+/datum/job/paramedic
+ title = "Zeng-Hu Lifeline Technician"
+
+/datum/job/bioengineer
+ title = "Zeng-Hu Bio-Engineer"
+
+/obj/item/clothing/accessory/armband/medgreen
+ name = "Zeng-Hu medical armband"
diff --git a/proxima/code/modules/factions/zeng-hu/science.dm b/proxima/code/modules/factions/zeng-hu/science.dm
new file mode 100644
index 00000000000..092fee4f6ca
--- /dev/null
+++ b/proxima/code/modules/factions/zeng-hu/science.dm
@@ -0,0 +1,14 @@
+/datum/job/rd
+ title = "Zeng-Hu Expedition Overseer"
+
+/datum/job/scientist
+ title = "Zeng-Hu Scientist"
+
+/datum/job/roboticist
+ title = "Zeng-Hu Roboticist"
+
+/datum/job/psychiatrist
+ title = "Zeng-Hu Psychiatrist"
+
+/obj/item/clothing/accessory/armband/science
+ name = "Zeng-Hu research armband"
diff --git a/proxima/code/modules/organs/external/subtypes/vey_med_limbs.dm b/proxima/code/modules/organs/external/subtypes/vey_med_limbs.dm
new file mode 100644
index 00000000000..70498dc5687
--- /dev/null
+++ b/proxima/code/modules/organs/external/subtypes/vey_med_limbs.dm
@@ -0,0 +1,37 @@
+/obj/item/organ/external/robotic/veymed
+ name = "\"Vey-Med\""
+ desc = "Fragile, sophisticated, and hell of expensive."
+ armor = list(melee = -1, bullet = -1, energy = -1, bomb = -10, bio = 100, rad = 100)
+ force_icon = 'proxima/icons/mob/human_races/vey-med.dmi'
+ model = "veymed"
+ min_broken_damage = 20
+ min_malfunction_damage = 10
+ price_tag = 10000
+ bad_type = /obj/item/organ/external/robotic/veymed
+
+/obj/item/organ/external/robotic/veymed/l_arm
+ default_description = /datum/organ_description/arm/left
+
+/obj/item/organ/external/robotic/veymed/r_arm
+ default_description = /datum/organ_description/arm/right
+
+/obj/item/organ/external/robotic/veymed/l_leg
+ default_description = /datum/organ_description/leg/left
+
+/obj/item/organ/external/robotic/veymed/r_leg
+ default_description = /datum/organ_description/leg/right
+
+/obj/item/organ/external/robotic/veymed/groin
+ default_description = /datum/organ_description/groin
+
+/obj/item/organ/external/robotic/veymed/torso
+ default_description = /datum/organ_description/chest
+
+/obj/item/organ/external/robotic/veymed/head
+ default_description = /datum/organ_description/head
+
+/datum/body_modification/limb/prosthesis/veymed
+ id = "prosthesis_veymed"
+ replace_limb = /obj/item/organ/external/robotic/veymed
+ body_parts = list(BP_L_ARM, BP_R_ARM, BP_L_LEG, BP_R_LEG, BP_CHEST, BP_GROIN, BP_HEAD)
+ icon = 'proxima/icons/mob/human_races/vey-med.dmi'
diff --git a/proxima/code/modules/power/fusion/consoles/_consoles.dm b/proxima/code/modules/power/fusion/consoles/_consoles.dm
new file mode 100644
index 00000000000..2a6331c1c73
--- /dev/null
+++ b/proxima/code/modules/power/fusion/consoles/_consoles.dm
@@ -0,0 +1,49 @@
+/obj/machinery/computer/fusion
+ icon_keyboard = "power_key"
+ icon_screen = "rust_screen"
+ light_color = COLOR_ORANGE
+ idle_power_usage = 250
+ active_power_usage = 500
+ var/ui_template
+ var/initial_id_tag
+
+/obj/machinery/computer/fusion/Initialize()
+ set_extension(src, /datum/extension/local_network_member)
+ if(initial_id_tag)
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ fusion.set_tag(null, initial_id_tag)
+ . = ..()
+
+/obj/machinery/computer/fusion/proc/get_local_network()
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ return fusion.get_local_network()
+
+/obj/machinery/computer/fusion/attackby(var/obj/item/thing, var/mob/user)
+ if(isMultitool(thing))
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ fusion.get_new_tag(user)
+ return
+ else
+ return ..()
+
+/obj/machinery/computer/fusion/interface_interact(var/mob/user)
+ ui_interact(user)
+ return TRUE
+
+/obj/machinery/computer/fusion/proc/build_ui_data()
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ var/datum/local_network/lan = fusion.get_local_network()
+ var/list/data = list()
+ data["id"] = lan ? lan.id_tag : "unset"
+ data["name"] = name
+ . = data
+
+/obj/machinery/computer/fusion/ui_interact(var/mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1)
+ if(ui_template)
+ var/list/data = build_ui_data()
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, ui_template, name, 400, 600)
+ ui.set_initial_data(data)
+ ui.open()
+ ui.set_auto_update(1)
\ No newline at end of file
diff --git a/proxima/code/modules/power/fusion/consoles/core_control.dm b/proxima/code/modules/power/fusion/consoles/core_control.dm
new file mode 100644
index 00000000000..e56a76dc782
--- /dev/null
+++ b/proxima/code/modules/power/fusion/consoles/core_control.dm
@@ -0,0 +1,63 @@
+/obj/machinery/computer/fusion/core_control
+ name = "\improper R-UST Mk. 8 core control"
+ ui_template = "fusion_core_control.tmpl"
+
+/obj/machinery/computer/fusion/core_control/OnTopic(var/mob/user, var/href_list, var/datum/topic_state/state)
+
+ if(href_list["toggle_active"] || href_list["str"])
+ var/obj/machinery/power/fusion_core/C = locate(href_list["machine"])
+ if(!istype(C))
+ return TOPIC_NOACTION
+
+ var/datum/local_network/lan = get_local_network()
+ if(!lan || !lan.is_connected(C))
+ return TOPIC_NOACTION
+
+ if(!C.check_core_status())
+ return TOPIC_NOACTION
+
+ if(href_list["toggle_active"])
+ if(!C.Startup()) //Startup() whilst the device is active will return null.
+ if(!C.owned_field.is_shutdown_safe())
+ if(alert(user, "Shutting down this fusion core without proper safety procedures will cause serious damage, do you wish to continue?", "Shut Down?", "Yes", "No") == "No")
+ return TOPIC_NOACTION
+ C.Shutdown()
+ return TOPIC_REFRESH
+
+ if(href_list["str"] && C)
+ var/val = text2num(href_list["str"])
+ if(!val) //Value is 0, which is manual entering.
+ C.set_strength(input("Enter the new field power density (W.m^-3)", "Fusion Control", C.field_strength) as num)
+ else
+ C.set_strength(C.field_strength + val)
+ return TOPIC_REFRESH
+
+/obj/machinery/computer/fusion/core_control/build_ui_data()
+ . = ..()
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ var/datum/local_network/lan = fusion.get_local_network()
+ var/list/cores = list()
+ if(lan)
+ var/list/fusion_cores = lan.get_devices(/obj/machinery/power/fusion_core)
+ for(var/i = 1 to LAZYLEN(fusion_cores))
+ var/list/core = list()
+ var/obj/machinery/power/fusion_core/C = fusion_cores[i]
+ core["id"] = "#[i]"
+ core["ref"] = "\ref[C]"
+ core["field"] = !isnull(C.owned_field)
+ core["power"] = "[C.field_strength/10.0] tesla"
+ core["size"] = C.owned_field ? "[C.owned_field.size] meter\s" : "Field offline."
+ core["instability"] = C.owned_field ? "[C.owned_field.percent_unstable * 100]%" : "Field offline."
+ core["temperature"] = C.owned_field ? "[C.owned_field.plasma_temperature + 295]K" : "Field offline."
+ core["powerstatus"] = "[C.avail()]/[C.active_power_usage] W"
+ var/fuel_string = ""
+ if(C.owned_field && LAZYLEN(C.owned_field.reactants))
+ for(var/reactant in C.owned_field.reactants)
+ fuel_string += "| [reactant] | [C.owned_field.reactants[reactant]] |
"
+ else
+ fuel_string += "| Nothing. |
"
+ fuel_string += "
"
+ core["fuel"] = fuel_string
+
+ cores += list(core)
+ .["cores"] = cores
diff --git a/proxima/code/modules/power/fusion/consoles/gyrotron_control.dm b/proxima/code/modules/power/fusion/consoles/gyrotron_control.dm
new file mode 100644
index 00000000000..d27837b2b27
--- /dev/null
+++ b/proxima/code/modules/power/fusion/consoles/gyrotron_control.dm
@@ -0,0 +1,63 @@
+/obj/machinery/computer/fusion/gyrotron
+ name = "gyrotron control console"
+ icon_keyboard = "med_key"
+ icon_screen = "gyrotron_screen"
+ light_color = COLOR_BLUE
+ ui_template = "fusion_gyrotron_control.tmpl"
+
+/obj/machinery/computer/fusion/gyrotron/OnTopic(var/mob/user, var/href_list, var/datum/topic_state/state)
+
+ if(href_list["modifypower"] || href_list["modifyrate"] || href_list["toggle"])
+
+ var/obj/machinery/power/emitter/gyrotron/G = locate(href_list["machine"])
+ if(!istype(G))
+ return TOPIC_NOACTION
+
+ var/datum/local_network/lan = get_local_network()
+ var/list/gyrotrons = lan.get_devices(/obj/machinery/power/emitter/gyrotron)
+ if(!lan || !gyrotrons || !gyrotrons[G])
+ return TOPIC_NOACTION
+
+ if(href_list["modifypower"])
+ var/new_val = input("Enter new emission power level (1 - 50)", "Modifying power level", G.mega_energy) as num
+ if(!istype(G))
+ return TOPIC_NOACTION
+ if(!new_val)
+ to_chat(user, SPAN_WARNING("That's not a valid number."))
+ return TOPIC_NOACTION
+ G.mega_energy = clamp(new_val, 1, 50)
+ G.change_power_consumption(G.mega_energy * 1500, POWER_USE_ACTIVE)
+ return TOPIC_REFRESH
+
+ if(href_list["modifyrate"])
+ var/new_val = input("Enter new emission delay between 2 and 10 seconds.", "Modifying emission rate", G.rate) as num
+ if(!istype(G))
+ return TOPIC_NOACTION
+ if(!new_val)
+ to_chat(user, SPAN_WARNING("That's not a valid number."))
+ return TOPIC_NOACTION
+ G.rate = clamp(new_val, 2, 10)
+ return TOPIC_REFRESH
+
+ if(href_list["toggle"])
+ G.activate(user)
+ return TOPIC_REFRESH
+
+/obj/machinery/computer/fusion/gyrotron/build_ui_data()
+ . = ..()
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ var/datum/local_network/lan = fusion.get_local_network()
+ var/list/gyrotrons = list()
+ if(lan && gyrotrons)
+ var/list/lan_gyrotrons = lan.get_devices(/obj/machinery/power/emitter/gyrotron)
+ for(var/i = 1 to LAZYLEN(lan_gyrotrons))
+ var/list/gyrotron = list()
+ var/obj/machinery/power/emitter/gyrotron/G = lan_gyrotrons[i]
+ gyrotron["id"] = "#[i]"
+ gyrotron["ref"] = "\ref[G]"
+ gyrotron["active"] = G.active
+ gyrotron["firedelay"] = G.rate
+ gyrotron["energy"] = G.mega_energy
+ gyrotrons += list(gyrotron)
+ .["gyrotrons"] = gyrotrons
+
diff --git a/proxima/code/modules/power/fusion/consoles/injector_control.dm b/proxima/code/modules/power/fusion/consoles/injector_control.dm
new file mode 100644
index 00000000000..b8eb62e7f7e
--- /dev/null
+++ b/proxima/code/modules/power/fusion/consoles/injector_control.dm
@@ -0,0 +1,74 @@
+/obj/machinery/computer/fusion/fuel_control
+ name = "fuel injection control computer"
+ icon_keyboard = "rd_key"
+ icon_screen = "fuel_screen"
+ ui_template = "fusion_injector_control.tmpl"
+
+/obj/machinery/computer/fusion/fuel_control/OnTopic(var/mob/user, var/href_list, var/datum/topic_state/state)
+ var/datum/local_network/lan = get_local_network()
+ var/list/fuel_injectors = lan.get_devices(/obj/machinery/fusion_fuel_injector)
+
+ if(href_list["global_toggle"])
+ if(!lan || !fuel_injectors)
+ return TOPIC_NOACTION
+
+ for(var/obj/machinery/fusion_fuel_injector/F in fuel_injectors)
+ if(F.injecting)
+ F.StopInjecting()
+ else
+ F.BeginInjecting()
+ return TOPIC_REFRESH
+
+ if(href_list["global_rate"])
+ if(!lan || !fuel_injectors)
+ return TOPIC_NOACTION
+ var/new_injection_rate = input("Enter a new injection rate between 1 and 100. This will affect all injectors!", "Modifying injection rate") as null|num
+ var/new_injection_clamped = clamp(new_injection_rate, 1, 100) / 100
+ if(!new_injection_rate)
+ return TOPIC_NOACTION
+ if(!CanInteract(user,state))
+ return TOPIC_NOACTION
+ for(var/obj/machinery/fusion_fuel_injector/F as anything in fuel_injectors)
+ F.injection_rate = new_injection_clamped
+ return TOPIC_REFRESH
+
+ if(href_list["toggle_injecting"] || href_list["injection_rate"])
+ var/obj/machinery/fusion_fuel_injector/I = locate((href_list["toggle_injecting"] || href_list["machine"]))
+ if(!istype(I) || !lan || !fuel_injectors || !fuel_injectors[I])
+ return TOPIC_NOACTION
+
+ if(href_list["toggle_injecting"])
+ if(I.injecting)
+ I.StopInjecting()
+ else
+ I.BeginInjecting()
+
+ if(href_list["injection_rate"])
+ var/new_injection_rate = input("Enter a new injection rate between 1 and 100.", "Modifying injection rate", I.injection_rate) as null|num
+ if(!istype(I))
+ return TOPIC_NOACTION
+ if(!new_injection_rate)
+ return TOPIC_NOACTION
+ if(!CanInteract(user,state))
+ return TOPIC_NOACTION
+ I.injection_rate = clamp(new_injection_rate, 1, 100) / 100
+ return TOPIC_REFRESH
+
+/obj/machinery/computer/fusion/fuel_control/build_ui_data()
+ . = ..()
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ var/datum/local_network/lan = fusion.get_local_network()
+ var/list/injectors = list()
+ if(lan)
+ var/list/fuel_injectors = lan.get_devices(/obj/machinery/fusion_fuel_injector)
+ for(var/i = 1 to LAZYLEN(fuel_injectors))
+ var/list/injector = list()
+ var/obj/machinery/fusion_fuel_injector/I = fuel_injectors[i]
+ injector["id"] = "#[i]"
+ injector["ref"] = "\ref[I]"
+ injector["injecting"] = I.injecting
+ injector["fueltype"] = "[I.cur_assembly ? I.cur_assembly.fuel_type : "No Fuel Inserted"]"
+ injector["depletion"] = "[I.cur_assembly ? (I.cur_assembly.percent_depleted * 100) : 100]%"
+ injector["injection_rate"] = "[I.injection_rate * 100]%"
+ injectors += list(injector)
+ .["injectors"] = injectors
diff --git a/proxima/code/modules/power/fusion/core/_core.dm b/proxima/code/modules/power/fusion/core/_core.dm
new file mode 100644
index 00000000000..1031b4d39e5
--- /dev/null
+++ b/proxima/code/modules/power/fusion/core/_core.dm
@@ -0,0 +1,130 @@
+#define MAX_FIELD_STR 10000
+#define MIN_FIELD_STR 1
+
+/obj/machinery/power/fusion_core
+ name = "\improper R-UST Mk. 8 Tokamak core"
+ desc = "An enormous solenoid for generating extremely high power electromagnetic fields. It includes a kinetic energy harvester."
+ icon = 'icons/obj/machines/power/fusion_core.dmi'
+ icon_state = "core0"
+ layer = ABOVE_HUMAN_LAYER
+ density = TRUE
+ use_power = POWER_USE_IDLE
+ idle_power_usage = 50
+ active_power_usage = 500 //multiplied by field strength
+ anchored = FALSE
+ construct_state = /decl/machine_construction/default/panel_closed
+ uncreated_component_parts = null
+ stat_immune = 0
+ base_type = /obj/machinery/power/fusion_core
+
+ var/obj/effect/fusion_em_field/owned_field
+ var/field_strength = 1//0.01
+ var/initial_id_tag
+
+/obj/machinery/power/fusion_core/mapped
+ anchored = TRUE
+
+/obj/machinery/power/fusion_core/Initialize()
+ . = ..()
+ connect_to_network()
+ set_extension(src, /datum/extension/local_network_member)
+ if(initial_id_tag)
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ fusion.set_tag(null, initial_id_tag)
+
+/obj/machinery/power/fusion_core/Process()
+ if((stat & BROKEN) || !powernet || !owned_field)
+ Shutdown()
+
+/obj/machinery/power/fusion_core/Topic(href, href_list)
+ if(..())
+ return 1
+ if(href_list["str"])
+ var/dif = text2num(href_list["str"])
+ field_strength = min(max(field_strength + dif, MIN_FIELD_STR), MAX_FIELD_STR)
+ change_power_consumption(500 * field_strength, POWER_USE_ACTIVE)
+ if(owned_field)
+ owned_field.ChangeFieldStrength(field_strength)
+
+/obj/machinery/power/fusion_core/proc/Startup()
+ if(owned_field)
+ return
+ owned_field = new(loc, src)
+ owned_field.ChangeFieldStrength(field_strength)
+ icon_state = "core1"
+ update_use_power(POWER_USE_ACTIVE)
+ . = 1
+
+/obj/machinery/power/fusion_core/proc/Shutdown(var/force_rupture)
+ if(owned_field)
+ icon_state = "core0"
+ if(force_rupture || owned_field.plasma_temperature > 1000)
+ owned_field.Rupture()
+ else
+ owned_field.RadiateAll()
+ qdel(owned_field)
+ owned_field = null
+ update_use_power(POWER_USE_IDLE)
+
+/obj/machinery/power/fusion_core/proc/AddParticles(var/name, var/quantity = 1)
+ if(owned_field)
+ owned_field.AddParticles(name, quantity)
+ . = 1
+
+/obj/machinery/power/fusion_core/bullet_act(var/obj/item/projectile/Proj)
+ if(owned_field)
+ . = owned_field.bullet_act(Proj)
+
+/obj/machinery/power/fusion_core/proc/set_strength(var/value)
+ value = clamp(value, MIN_FIELD_STR, MAX_FIELD_STR)
+ field_strength = value
+ change_power_consumption(5 * value, POWER_USE_ACTIVE)
+ if(owned_field)
+ owned_field.ChangeFieldStrength(value)
+
+/obj/machinery/power/fusion_core/physical_attack_hand(var/mob/user)
+ visible_message("\The [user] hugs \the [src] to make it feel better!")
+ if(owned_field)
+ Shutdown()
+ return TRUE
+
+/obj/machinery/power/fusion_core/attackby(var/obj/item/W, var/mob/user)
+
+ if(owned_field)
+ to_chat(user,"Shut \the [src] off first!")
+ return
+
+ if(isMultitool(W))
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ fusion.get_new_tag(user)
+ return
+
+ else if(isWrench(W))
+ anchored = !anchored
+ playsound(src.loc, 'sound/items/Ratchet.ogg', 75, 1)
+ if(anchored)
+ user.visible_message("[user.name] secures [src.name] to the floor.", \
+ "You secure the [src.name] to the floor.", \
+ "You hear a ratchet")
+ else
+ user.visible_message("[user.name] unsecures [src.name] from the floor.", \
+ "You unsecure the [src.name] from the floor.", \
+ "You hear a ratchet")
+ return
+
+ return ..()
+
+/obj/machinery/power/fusion_core/proc/jumpstart(var/field_temperature)
+ field_strength = 501 // Generally a good size.
+ Startup()
+ if(!owned_field)
+ return FALSE
+ owned_field.plasma_temperature = field_temperature
+ return TRUE
+
+/obj/machinery/power/fusion_core/proc/check_core_status()
+ if(stat & BROKEN)
+ return FALSE
+ if(idle_power_usage > avail())
+ return FALSE
+ . = TRUE
diff --git a/proxima/code/modules/power/fusion/core/core_field.dm b/proxima/code/modules/power/fusion/core/core_field.dm
new file mode 100644
index 00000000000..c0377a79d9d
--- /dev/null
+++ b/proxima/code/modules/power/fusion/core/core_field.dm
@@ -0,0 +1,541 @@
+#define FUSION_ENERGY_PER_K 20
+#define FUSION_INSTABILITY_DIVISOR 50000
+#define FUSION_RUPTURE_THRESHOLD 10000
+#define FUSION_REACTANT_CAP 10000
+
+/obj/effect/fusion_em_field
+ name = "electromagnetic field"
+ desc = "A coruscating, barely visible field of energy. It is shaped like a slightly flattened torus."
+ alpha = 30
+ layer = 4
+ light_color = COLOR_RED
+
+ plane = EFFECTS_ABOVE_LIGHTING_PLANE
+
+ var/size = 1
+ var/energy = 0
+ var/plasma_temperature = 0
+ var/radiation = 0
+ var/field_strength = 0.01
+ var/tick_instability = 0
+ var/percent_unstable = 0
+
+ var/obj/machinery/power/fusion_core/owned_core
+ var/list/reactants = list()
+ var/list/particle_catchers = list()
+
+ var/list/ignore_types = list(
+ /obj/item/projectile,
+ /obj/effect,
+ /obj/structure/cable,
+ /obj/machinery/atmospherics,
+ /obj/machinery/air_sensor
+ )
+
+ var/light_min_range = 2
+ var/light_min_power = 0.2
+ var/light_max_range = 18
+ var/light_max_power = 1
+
+ var/last_range
+ var/last_power
+
+ var/last_reactants = 0
+
+ particles = new/particles/fusion
+
+ var/animating_ripple = FALSE
+
+/obj/effect/fusion_em_field/proc/UpdateVisuals()
+ //Take the particle system and edit it
+
+ //size
+ var/radius = ((size-1) / 2) * WORLD_ICON_SIZE
+
+ particles.position = generator("circle", radius - size, radius + size, NORMAL_RAND)
+
+ //Radiation affects drift
+ var/radiationfactor = clamp((radiation * 0.001), 0, 0.5)
+ particles.drift = generator("circle", (0.2 + radiationfactor), NORMAL_RAND)
+
+ particles.spawning = last_reactants * 0.9 + Interpolate(0, 200, clamp(plasma_temperature / 70000, 0, 1))
+
+
+/obj/effect/fusion_em_field/New(loc, var/obj/machinery/power/fusion_core/new_owned_core)
+ ..()
+
+ filters = list(filter(type = "ripple", size = 4, "radius" = 1, "falloff" = 1)
+ , filter(type="outline", size = 2, color = COLOR_RED)
+ , filter(type="bloom", size=3, offset = 0.5, alpha = 235))
+
+ set_light(light_min_power, light_min_range / 10, light_min_range)
+ last_range = light_min_range
+ last_power = light_min_power
+
+ owned_core = new_owned_core
+ if(!owned_core)
+ qdel(src)
+ return
+
+ particles.spawning = 0 //Turn off particles until something calls for it
+
+ //create the gimmicky things to handle field collisions
+ var/obj/effect/fusion_particle_catcher/catcher
+
+ catcher = new (locate(src.x,src.y,src.z))
+ catcher.parent = src
+ catcher.SetSize(1)
+ particle_catchers.Add(catcher)
+
+ for(var/iter=1,iter<=6,iter++)
+ catcher = new (locate(src.x-iter,src.y,src.z))
+ catcher.parent = src
+ catcher.SetSize((iter*2)+1)
+ particle_catchers.Add(catcher)
+
+ catcher = new (locate(src.x+iter,src.y,src.z))
+ catcher.parent = src
+ catcher.SetSize((iter*2)+1)
+ particle_catchers.Add(catcher)
+
+ catcher = new (locate(src.x,src.y+iter,src.z))
+ catcher.parent = src
+ catcher.SetSize((iter*2)+1)
+ particle_catchers.Add(catcher)
+
+ catcher = new (locate(src.x,src.y-iter,src.z))
+ catcher.parent = src
+ catcher.SetSize((iter*2)+1)
+ particle_catchers.Add(catcher)
+
+ START_PROCESSING(SSobj, src)
+
+/obj/effect/fusion_em_field/Initialize()
+ . = ..()
+ addtimer(CALLBACK(src, .proc/update_light_colors), 10 SECONDS, TIMER_LOOP)
+
+/obj/effect/fusion_em_field/proc/update_light_colors()
+ var/use_range
+ var/use_power
+ switch (plasma_temperature)
+ if (-INFINITY to 1000)
+ light_color = COLOR_RED
+ use_range = light_min_range
+ use_power = light_min_power
+ alpha = 30
+ if (100000 to INFINITY)
+ light_color = COLOR_VIOLET
+ use_range = light_max_range
+ use_power = light_max_power
+ alpha = 230
+ else
+ var/temp_mod = ((plasma_temperature-5000)/20000)
+ use_range = light_min_range + Ceil((light_max_range-light_min_range)*temp_mod)
+ use_power = light_min_power + Ceil((light_max_power-light_min_power)*temp_mod)
+ switch (plasma_temperature)
+ if (1000 to 6000)
+ light_color = COLOR_ORANGE
+ alpha = 50
+ if (6000 to 20000)
+ light_color = COLOR_YELLOW
+ alpha = 80
+ if (20000 to 50000)
+ light_color = COLOR_GREEN
+ alpha = 120
+ if (50000 to 70000)
+ light_color = COLOR_CYAN
+ alpha = 160
+ if (70000 to 100000)
+ light_color = COLOR_BLUE
+ alpha = 200
+
+ if (last_range != use_range || last_power != use_power || color != light_color)
+ set_light(min(use_power, 1), use_range / 6, use_range) //cap first arg at 1 to avoid breaking lighting stuff.
+ last_range = use_range
+ last_power = use_power
+ //Temperature based color
+ particles.gradient = list(0, COLOR_WHITE, 0.85, light_color)
+ UNLINT(var/dm_filter/outline = filters[2])
+ UNLINT(outline.color = light_color)
+ UNLINT(var/dm_filter/bloom = filters[3])
+ UNLINT(bloom.alpha = alpha)
+
+/obj/effect/fusion_em_field/Process()
+ //make sure the field generator is still intact
+ if(QDELETED(owned_core))
+ qdel(src)
+ return
+
+ // Take some gas up from our environment.
+ var/added_particles = FALSE
+ var/datum/gas_mixture/uptake_gas = owned_core.loc.return_air()
+ if(uptake_gas)
+ uptake_gas = uptake_gas.remove_by_flag(XGM_GAS_FUSION_FUEL, rand(50,100))
+ if(uptake_gas && uptake_gas.total_moles)
+ for(var/gasname in uptake_gas.gas)
+ if(uptake_gas.gas[gasname]*10 > reactants[gasname])
+ AddParticles(gasname, uptake_gas.gas[gasname]*10)
+ uptake_gas.adjust_gas(gasname, -(uptake_gas.gas[gasname]), update=FALSE)
+ added_particles = TRUE
+ if(added_particles)
+ uptake_gas.update_values()
+
+ //let the particles inside the field react
+ React()
+
+ // Dump power to our powernet.
+ owned_core.add_avail(FUSION_ENERGY_PER_K * plasma_temperature)
+
+ // Energy decay.
+ if(plasma_temperature >= 1)
+ var/lost = plasma_temperature*0.01
+ radiation += lost
+ plasma_temperature -= lost
+
+ //handle some reactants formatting
+ for(var/reactant in reactants)
+ var/amount = reactants[reactant]
+ if(amount < 1)
+ reactants.Remove(reactant)
+ else if(amount >= FUSION_REACTANT_CAP)
+ var/radiate = rand(3 * amount / 4, amount / 4)
+ reactants[reactant] -= radiate
+ radiation += radiate
+
+ check_instability()
+ Radiate()
+ if(radiation)
+ SSradiation.radiate(src, round(radiation*0.001))
+ return 1
+
+/obj/effect/fusion_em_field/proc/check_instability()
+ if(tick_instability > 0)
+ percent_unstable += (tick_instability*size)/FUSION_INSTABILITY_DIVISOR
+ tick_instability = 0
+ UpdateVisuals()
+ else
+ if(percent_unstable < 0)
+ percent_unstable = 0
+ else
+ if(percent_unstable > 1)
+ percent_unstable = 1
+ if(percent_unstable > 0)
+ percent_unstable = max(0, percent_unstable-rand(0.01,0.03))
+ UpdateVisuals()
+
+ if(percent_unstable >= 1)
+ owned_core.Shutdown(force_rupture=1)
+ else
+ if(percent_unstable > 0.5 && prob(percent_unstable*100))
+ var/ripple_radius = (((size-1) / 2) * WORLD_ICON_SIZE) + WORLD_ICON_SIZE
+ var/wave_size = 4
+ if(plasma_temperature < FUSION_RUPTURE_THRESHOLD)
+ visible_message("\The [src] ripples uneasily, like a disturbed pond.")
+ else
+ var/flare
+ var/fuel_loss
+ var/rupture
+ if(percent_unstable < 0.7)
+ visible_message("\The [src] ripples uneasily, like a disturbed pond.")
+ fuel_loss = prob(5)
+ else if(percent_unstable < 0.9)
+ visible_message("\The [src] undulates violently, shedding plumes of plasma!")
+ flare = prob(50)
+ fuel_loss = prob(20)
+ rupture = prob(5)
+ wave_size += 2
+ else
+ visible_message("\The [src] is wracked by a series of horrendous distortions, buckling and twisting like a living thing!")
+ flare = 1
+ fuel_loss = prob(50)
+ rupture = prob(25)
+ wave_size += 4
+
+ if(rupture)
+ owned_core.Shutdown(force_rupture=1)
+ else
+ var/lost_plasma = (plasma_temperature*percent_unstable)
+ radiation += lost_plasma
+ if(flare)
+ radiation += plasma_temperature/2
+ wave_size += 6
+ plasma_temperature -= lost_plasma
+
+ if(fuel_loss)
+ for(var/particle in reactants)
+ var/lost_fuel = reactants[particle]*percent_unstable
+ radiation += lost_fuel
+ reactants[particle] -= lost_fuel
+ if(reactants[particle] <= 0)
+ reactants.Remove(particle)
+ Radiate()
+ Ripple(wave_size, ripple_radius)
+ return
+
+/obj/effect/fusion_em_field/proc/Ripple(_size, _radius)
+ if(!animating_ripple)
+ UNLINT(var/dm_filter/ripple = filters[1])
+ UNLINT(ripple.size = _size)
+ animate(filters[1], time = 0, loop = 1, radius = 0, flags=ANIMATION_PARALLEL)
+ animate(time = 2 SECONDS, radius = _radius)
+ animating_ripple = TRUE
+ addtimer(CALLBACK(src, .proc/ResetRipple), 2 SECONDS, TIMER_CLIENT_TIME)
+
+/obj/effect/fusion_em_field/proc/ResetRipple()
+ animating_ripple = FALSE
+
+/obj/effect/fusion_em_field/proc/is_shutdown_safe()
+ return plasma_temperature < 1000
+
+/obj/effect/fusion_em_field/proc/Rupture()
+ visible_message("\The [src] shudders like a dying animal before flaring to eye-searing brightness and rupturing!")
+ set_light(1, 0.1, 15, 2, "#ccccff")
+ empulse(get_turf(src), Ceil(plasma_temperature/1000), Ceil(plasma_temperature/300))
+ sleep(5)
+ RadiateAll()
+ explosion(get_turf(owned_core),-1,-1,8,10) // Blow out all the windows.
+ return
+
+/obj/effect/fusion_em_field/proc/ChangeFieldStrength(var/new_strength)
+ var/calc_size = 1
+ if(new_strength <= 50)
+ calc_size = 1
+ else if(new_strength <= 200)
+ calc_size = 3
+ else if(new_strength <= 500)
+ calc_size = 5
+ else if(new_strength <= 1000)
+ calc_size = 7
+ else if(new_strength <= 2000)
+ calc_size = 9
+ else if(new_strength <= 5000)
+ calc_size = 11
+ else
+ calc_size = 13
+ field_strength = new_strength
+ change_size(calc_size)
+
+/obj/effect/fusion_em_field/proc/AddEnergy(var/a_energy, var/a_plasma_temperature)
+ energy += a_energy
+ plasma_temperature += a_plasma_temperature
+ if(a_energy && percent_unstable > 0)
+ percent_unstable -= a_energy/10000
+ if(percent_unstable < 0)
+ percent_unstable = 0
+ while(energy >= 100)
+ energy -= 100
+ plasma_temperature += 1
+ UpdateVisuals()
+
+/obj/effect/fusion_em_field/proc/AddParticles(var/name, var/quantity = 1)
+ if(name in reactants)
+ reactants[name] += quantity
+ else if(name != "proton" && name != "electron" && name != "neutron")
+ reactants.Add(name)
+ reactants[name] = quantity
+ UpdateVisuals()
+
+/obj/effect/fusion_em_field/proc/RadiateAll(var/ratio_lost = 1)
+
+ // Create our plasma field and dump it into our environment.
+ var/turf/T = get_turf(src)
+ if(istype(T))
+ var/datum/gas_mixture/plasma
+ for(var/reactant in reactants)
+ if(!gas_data.name[reactant])
+ continue
+ if(!plasma)
+ plasma = new
+ plasma.adjust_gas(reactant, max(1,round(reactants[reactant]*0.1)), 0) // *0.1 to compensate for *10 when uptaking gas.
+ if(!plasma)
+ return
+ plasma.temperature = (plasma_temperature/2)
+ plasma.update_values()
+ T.assume_air(plasma)
+ T.hotspot_expose(plasma_temperature)
+ plasma = null
+
+ // Radiate all our unspent fuel and energy.
+ for(var/particle in reactants)
+ radiation += reactants[particle]
+ reactants.Remove(particle)
+ radiation += plasma_temperature/2
+ plasma_temperature = 0
+
+ SSradiation.radiate(src, round(radiation*0.001))
+ Radiate()
+
+/obj/effect/fusion_em_field/proc/Radiate()
+ if(istype(loc, /turf))
+ var/empsev = max(1, min(3, Ceil(size/2)))
+ for(var/atom/movable/AM in range(max(1,Floor(size/2)), loc))
+
+ if(AM == src || AM == owned_core || !AM.simulated)
+ continue
+
+ var/skip_obstacle
+ for(var/ignore_path in ignore_types)
+ if(istype(AM, ignore_path))
+ skip_obstacle = TRUE
+ break
+ if(skip_obstacle)
+ continue
+
+ AM.visible_message("The field buckles visibly around \the [AM]!")
+ tick_instability += rand(30,50)
+ AM.emp_act(empsev)
+
+ if(owned_core && owned_core.loc)
+ var/datum/gas_mixture/environment = owned_core.loc.return_air()
+ if(environment && environment.temperature < (T0C+1000)) // Putting an upper bound on it to stop it being used in a TEG.
+ environment.add_thermal_energy(plasma_temperature*20000)
+ radiation = 0
+
+/obj/effect/fusion_em_field/proc/change_size(var/newsize = 1)
+ var/changed = 0
+ if( ((newsize-1)%2==0) && (newsize<=13) )
+ size = newsize
+ changed = newsize
+ UpdateVisuals()
+
+ for(var/obj/effect/fusion_particle_catcher/catcher in particle_catchers)
+ catcher.UpdateSize()
+ return changed
+
+//the !!fun!! part
+/obj/effect/fusion_em_field/proc/React()
+ //loop through the reactants in random order
+ var/list/react_pool = reactants.Copy()
+ last_reactants = 0
+
+ //cant have any reactions if there aren't any reactants present
+ if(react_pool.len)
+ //determine a random amount to actually react this cycle, and remove it from the standard pool
+ //this is a hack, and quite nonrealistic :(
+ for(var/reactant in react_pool)
+ react_pool[reactant] = rand(Floor(react_pool[reactant]/2),react_pool[reactant])
+ reactants[reactant] -= react_pool[reactant]
+ if(!react_pool[reactant])
+ react_pool -= reactant
+
+ //loop through all the reacting reagents, picking out random reactions for them
+ var/list/produced_reactants = new/list
+ var/list/p_react_pool = react_pool.Copy()
+ while(p_react_pool.len)
+ //pick one of the unprocessed reacting reagents randomly
+ var/cur_p_react = pick(p_react_pool)
+ p_react_pool.Remove(cur_p_react)
+
+ //grab all the possible reactants to have a reaction with
+ var/list/possible_s_reacts = react_pool.Copy()
+ //if there is only one of a particular reactant, then it can not react with itself so remove it
+ possible_s_reacts[cur_p_react] -= 1
+ if(possible_s_reacts[cur_p_react] < 1)
+ possible_s_reacts.Remove(cur_p_react)
+
+ //loop through and work out all the possible reactions
+ var/list/possible_reactions
+ for(var/cur_s_react in possible_s_reacts)
+ if(possible_s_reacts[cur_s_react] < 1)
+ continue
+ var/decl/fusion_reaction/cur_reaction = get_fusion_reaction(cur_p_react, cur_s_react)
+ if(cur_reaction && plasma_temperature >= cur_reaction.minimum_energy_level)
+ LAZYDISTINCTADD(possible_reactions, cur_reaction)
+
+ //if there are no possible reactions here, abandon this primary reactant and move on
+ if(!LAZYLEN(possible_reactions))
+ continue
+
+ /// Sort based on reaction priority to avoid deut-deut eating all the deut before deut-trit can run etc.
+ sortTim(possible_reactions, /proc/cmp_fusion_reaction_des)
+
+ //split up the reacting atoms between the possible reactions
+ while(possible_reactions.len)
+ var/decl/fusion_reaction/cur_reaction = possible_reactions[1]
+ possible_reactions.Remove(cur_reaction)
+
+ //set the randmax to be the lower of the two involved reactants
+ var/max_num_reactants = react_pool[cur_reaction.p_react] > react_pool[cur_reaction.s_react] ? \
+ react_pool[cur_reaction.s_react] : react_pool[cur_reaction.p_react]
+ if(max_num_reactants < 1)
+ continue
+
+ //make sure we have enough energy
+ if(plasma_temperature < cur_reaction.minimum_reaction_temperature)
+ continue
+
+ if(plasma_temperature < max_num_reactants * cur_reaction.energy_consumption)
+ max_num_reactants = round(plasma_temperature / cur_reaction.energy_consumption)
+ if(max_num_reactants < 1)
+ continue
+
+ //randomly determined amount to react
+ var/amount_reacting = rand(1, max_num_reactants)
+
+ //removing the reacting substances from the list of substances that are primed to react this cycle
+ //if there aren't enough of that substance (there should be) then modify the reactant amounts accordingly
+ if( react_pool[cur_reaction.p_react] - amount_reacting >= 0 )
+ react_pool[cur_reaction.p_react] -= amount_reacting
+ else
+ amount_reacting = react_pool[cur_reaction.p_react]
+ react_pool[cur_reaction.p_react] = 0
+ //same again for secondary reactant
+ if(react_pool[cur_reaction.s_react] - amount_reacting >= 0 )
+ react_pool[cur_reaction.s_react] -= amount_reacting
+ else
+ react_pool[cur_reaction.p_react] += amount_reacting - react_pool[cur_reaction.p_react]
+ amount_reacting = react_pool[cur_reaction.s_react]
+ react_pool[cur_reaction.s_react] = 0
+
+ plasma_temperature -= max_num_reactants * cur_reaction.energy_consumption // Remove the consumed energy.
+ plasma_temperature += max_num_reactants * cur_reaction.energy_production // Add any produced energy.
+ radiation += max_num_reactants * cur_reaction.radiation // Add any produced radiation.
+ tick_instability += max_num_reactants * cur_reaction.instability
+ last_reactants += amount_reacting
+
+ // Create the reaction products.
+ for(var/reactant in cur_reaction.products)
+ var/success = 0
+ for(var/check_reactant in produced_reactants)
+ if(check_reactant == reactant)
+ produced_reactants[reactant] += cur_reaction.products[reactant] * amount_reacting
+ success = 1
+ break
+ if(!success)
+ produced_reactants[reactant] = cur_reaction.products[reactant] * amount_reacting
+
+ // Handle anything special. If this proc returns true, abort the current reaction.
+ if(cur_reaction.handle_reaction_special(src))
+ return
+
+ // This reaction is done, and can't be repeated this sub-cycle.
+ possible_reactions.Remove(cur_reaction.s_react)
+
+ // Loop through the newly produced reactants and add them to the pool.
+ for(var/reactant in produced_reactants)
+ AddParticles(reactant, produced_reactants[reactant])
+
+ // Check whether there are reactants left, and add them back to the pool.
+ for(var/reactant in react_pool)
+ AddParticles(reactant, react_pool[reactant])
+
+ UpdateVisuals()
+
+/obj/effect/fusion_em_field/Destroy()
+ set_light(0)
+ RadiateAll()
+ QDEL_NULL_LIST(particle_catchers)
+ if(owned_core)
+ owned_core.owned_field = null
+ owned_core = null
+ STOP_PROCESSING(SSobj, src)
+ . = ..()
+
+/obj/effect/fusion_em_field/bullet_act(var/obj/item/projectile/Proj)
+ AddEnergy(Proj.damage)
+ update_icon()
+ return 0
+
+#undef FUSION_INSTABILITY_DIVISOR
+#undef FUSION_RUPTURE_THRESHOLD
+#undef FUSION_REACTANT_CAP
diff --git a/proxima/code/modules/power/fusion/fuel_assembly/fuel_assembly.dm b/proxima/code/modules/power/fusion/fuel_assembly/fuel_assembly.dm
new file mode 100644
index 00000000000..4116b8699df
--- /dev/null
+++ b/proxima/code/modules/power/fusion/fuel_assembly/fuel_assembly.dm
@@ -0,0 +1,77 @@
+/obj/item/fuel_assembly
+ name = "fuel rod assembly"
+ icon = 'icons/obj/machines/power/fusion.dmi'
+ icon_state = "fuel_assembly"
+ layer = 4
+
+ var/material_name
+ var/percent_depleted = 1
+ var/list/rod_quantities = list()
+ var/fuel_type = "composite"
+ var/fuel_colour
+ var/radioactivity = 0
+ var/initial_amount
+
+/obj/item/fuel_assembly/New(var/newloc, var/_material, var/_colour)
+ fuel_type = _material
+ fuel_colour = _colour
+ ..(newloc)
+
+/obj/item/fuel_assembly/Initialize()
+ . = ..()
+
+ if(ispath(fuel_type, /datum/reagent))
+ var/datum/reagent/R = fuel_type
+ fuel_type = lowertext(initial(R.name))
+ fuel_colour = initial(R.color)
+ initial_amount = 50000
+
+ var/material/material = SSmaterials.get_material_by_name(fuel_type)
+ if(istype(material))
+ initial_amount = material.units_per_sheet * 5 // Fuel compressor eats 5 sheets.
+ SetName("[material.use_name] fuel rod assembly")
+ desc = "A fuel rod for a fusion reactor. This one is made from [material.use_name]."
+ fuel_colour = material.icon_colour
+ fuel_type = material.use_name
+ if(material.radioactivity)
+ radioactivity = material.radioactivity
+ desc += " It is warm to the touch."
+ START_PROCESSING(SSobj, src)
+ if(material.luminescence)
+ set_light(material.luminescence, material.luminescence, material.icon_colour)
+ else
+ SetName("[fuel_type] fuel rod assembly")
+ desc = "A fuel rod for a fusion reactor. This one is made from [fuel_type]."
+
+ icon_state = "blank"
+ var/image/I = image(icon, "fuel_assembly")
+ I.color = fuel_colour
+ overlays += list(I, image(icon, "fuel_assembly_bracket"))
+ rod_quantities[fuel_type] = initial_amount
+
+/obj/item/fuel_assembly/Process()
+ if(!radioactivity)
+ return PROCESS_KILL
+
+ if(istype(loc, /turf))
+ SSradiation.radiate(src, max(1,Ceil(radioactivity/15)))
+
+/obj/item/fuel_assembly/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+// Mapper shorthand.
+/obj/item/fuel_assembly/deuterium/New(var/newloc)
+ ..(newloc, MATERIAL_DEUTERIUM)
+
+/obj/item/fuel_assembly/tritium/New(var/newloc)
+ ..(newloc, MATERIAL_TRITIUM)
+
+/obj/item/fuel_assembly/phoron/New(var/newloc)
+ ..(newloc, MATERIAL_PHORON)
+
+/obj/item/fuel_assembly/supermatter/New(var/newloc)
+ ..(newloc, MATERIAL_SUPERMATTER)
+
+/obj/item/fuel_assembly/hydrogen/New(var/newloc)
+ ..(newloc, MATERIAL_HYDROGEN)
\ No newline at end of file
diff --git a/proxima/code/modules/power/fusion/fuel_assembly/fuel_compressor.dm b/proxima/code/modules/power/fusion/fuel_assembly/fuel_compressor.dm
new file mode 100644
index 00000000000..1831eaf1108
--- /dev/null
+++ b/proxima/code/modules/power/fusion/fuel_assembly/fuel_compressor.dm
@@ -0,0 +1,56 @@
+// 5 sheets == ~12500 matter units == ~100u reagents
+// Try to avoid letting people produce more material
+// with the kinetic harvester than they put into the
+// field in the first place.
+
+/obj/machinery/fusion_fuel_compressor
+ name = "fuel compressor"
+ icon = 'icons/obj/machines/power/fusion.dmi'
+ icon_state = "fuel_compressor1"
+ density = TRUE
+ anchored = TRUE
+ layer = 4
+ construct_state = /decl/machine_construction/default/panel_closed
+
+/obj/machinery/fusion_fuel_compressor/MouseDrop_T(var/atom/movable/target, var/mob/user)
+ if(user.incapacitated() || !user.Adjacent(src))
+ return
+ return do_fuel_compression(target, user)
+
+/obj/machinery/fusion_fuel_compressor/attackby(var/obj/item/thing, var/mob/user)
+ return do_fuel_compression(thing, user) || ..()
+
+/obj/machinery/fusion_fuel_compressor/proc/do_fuel_compression(var/obj/item/thing, var/mob/user)
+ if(istype(thing) && thing.reagents && thing.reagents.total_volume && thing.is_open_container())
+ if(thing.reagents.reagent_list.len > 1)
+ to_chat(user, "The contents of \the [thing] are impure and cannot be used as fuel.")
+ return 1
+ if(thing.reagents.total_volume < 100)
+ to_chat(user, "You need at least one hundred units of material to form a fuel rod.")
+ return 1
+ var/datum/reagent/R = thing.reagents.reagent_list[1]
+ visible_message("\The [src] compresses the contents of \the [thing] into a new fuel assembly.")
+ var/obj/item/fuel_assembly/F = new(get_turf(src), R.type, R.color)
+ thing.reagents.remove_reagent(R.type, 100)
+ user.put_in_hands(F)
+ return 1
+ else if(istype(thing, /obj/machinery/power/supermatter/shard))
+ var/obj/item/fuel_assembly/F = new(get_turf(src), MATERIAL_SUPERMATTER)
+ visible_message("\The [src] compresses the \[thing] into a new fuel assembly.")
+ qdel(thing)
+ user.put_in_hands(F)
+ return 1
+ else if(istype(thing, /obj/item/stack/material))
+ var/obj/item/stack/material/M = thing
+ var/material/mat = M.get_material()
+ if(!mat.is_fusion_fuel)
+ to_chat(user, "It would be pointless to make a fuel rod out of [mat.use_name].")
+ return
+ if(!M.use(5))
+ to_chat(user, "You need at least five [mat.sheet_plural_name] to make a fuel rod.")
+ return
+ var/obj/item/fuel_assembly/F = new(get_turf(src), mat.name)
+ visible_message("\The [src] compresses the [mat.use_name] into a new fuel assembly.")
+ user.put_in_hands(F)
+ return 1
+ return 0
diff --git a/proxima/code/modules/power/fusion/fuel_assembly/fuel_injector.dm b/proxima/code/modules/power/fusion/fuel_assembly/fuel_injector.dm
new file mode 100644
index 00000000000..5b7746aac3a
--- /dev/null
+++ b/proxima/code/modules/power/fusion/fuel_assembly/fuel_injector.dm
@@ -0,0 +1,150 @@
+/obj/machinery/fusion_fuel_injector
+ name = "fuel injector"
+ icon = 'icons/obj/machines/power/fusion.dmi'
+ icon_state = "injector0"
+ density = TRUE
+ anchored = FALSE
+ req_access = list(access_engine)
+ idle_power_usage = 10
+ active_power_usage = 500
+ construct_state = /decl/machine_construction/default/panel_closed
+ uncreated_component_parts = null
+ stat_immune = 0
+ base_type = /obj/machinery/fusion_fuel_injector
+
+ var/fuel_usage = 0.001
+ var/initial_id_tag
+ var/injecting = 0
+ var/obj/item/fuel_assembly/cur_assembly
+ var/injection_rate = 1
+
+/obj/machinery/fusion_fuel_injector/Initialize()
+ set_extension(src, /datum/extension/local_network_member)
+ if(initial_id_tag)
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ fusion.set_tag(null, initial_id_tag)
+ . = ..()
+
+/obj/machinery/fusion_fuel_injector/Destroy()
+ if(cur_assembly)
+ cur_assembly.dropInto(loc)
+ cur_assembly = null
+ . = ..()
+
+/obj/machinery/fusion_fuel_injector/mapped
+ anchored = TRUE
+
+/obj/machinery/fusion_fuel_injector/Process()
+ if(injecting)
+ if(stat & (BROKEN|NOPOWER))
+ StopInjecting()
+ else
+ Inject()
+
+/obj/machinery/fusion_fuel_injector/attackby(obj/item/W, mob/user)
+
+ if(isMultitool(W))
+ var/datum/extension/local_network_member/lanm = get_extension(src, /datum/extension/local_network_member)
+ lanm.set_tag(null, initial_id_tag)
+ return
+
+ if(istype(W, /obj/item/fuel_assembly))
+
+ if(injecting)
+ to_chat(user, "Shut \the [src] off before playing with the fuel rod!")
+ return
+ if(!user.unEquip(W, src))
+ return
+ if(cur_assembly)
+ visible_message("\The [user] swaps \the [src]'s [cur_assembly] for \a [W].")
+ else
+ visible_message("\The [user] inserts \a [W] into \the [src].")
+ if(cur_assembly)
+ cur_assembly.dropInto(loc)
+ user.put_in_hands(cur_assembly)
+ cur_assembly = W
+ return
+
+ if(isWrench(W))
+ if(injecting)
+ to_chat(user, "Shut \the [src] off first!")
+ return
+ anchored = !anchored
+ playsound(src.loc, 'sound/items/Ratchet.ogg', 75, 1)
+ if(anchored)
+ user.visible_message("\The [user] secures \the [src] to the floor.")
+ else
+ user.visible_message("\The [user] unsecures \the [src] from the floor.")
+ return
+
+ return ..()
+
+/obj/machinery/fusion_fuel_injector/physical_attack_hand(mob/user)
+ if(injecting)
+ to_chat(user, "Shut \the [src] off before playing with the fuel rod!")
+ return TRUE
+
+ if(cur_assembly)
+ cur_assembly.dropInto(loc)
+ user.put_in_hands(cur_assembly)
+ visible_message("\The [user] removes \the [cur_assembly] from \the [src].")
+ cur_assembly = null
+ return TRUE
+ else
+ to_chat(user, "There is no fuel rod in \the [src].")
+ return TRUE
+
+/obj/machinery/fusion_fuel_injector/proc/BeginInjecting()
+ if(!injecting && cur_assembly)
+ icon_state = "injector1"
+ injecting = 1
+ update_use_power(POWER_USE_IDLE)
+
+/obj/machinery/fusion_fuel_injector/proc/StopInjecting()
+ if(injecting)
+ injecting = 0
+ icon_state = "injector0"
+ update_use_power(POWER_USE_OFF)
+
+/obj/machinery/fusion_fuel_injector/proc/Inject()
+ if(!injecting)
+ return
+ if(cur_assembly)
+ var/amount_left = 0
+ for(var/reagent in cur_assembly.rod_quantities)
+ if(cur_assembly.rod_quantities[reagent] > 0)
+ var/amount = cur_assembly.rod_quantities[reagent] * fuel_usage * injection_rate
+ if(amount < 1)
+ amount = 1
+ var/obj/effect/accelerated_particle/A = new/obj/effect/accelerated_particle(get_turf(src), dir)
+ A.particle_type = reagent
+ A.additional_particles = amount
+ A.move(1)
+ if(cur_assembly)
+ cur_assembly.rod_quantities[reagent] -= amount
+ amount_left += cur_assembly.rod_quantities[reagent]
+ if(cur_assembly)
+ cur_assembly.percent_depleted = amount_left / cur_assembly.initial_amount
+ flick("injector-emitting",src)
+ else
+ StopInjecting()
+
+/obj/machinery/fusion_fuel_injector/verb/rotate_clock()
+ set category = "Object"
+ set name = "Rotate Generator (Clockwise)"
+ set src in view(1)
+
+ if (usr.incapacitated() || usr.restrained() || anchored)
+ return
+
+ src.dir = turn(src.dir, -90)
+
+/obj/machinery/fusion_fuel_injector/verb/rotate_anticlock()
+ set category = "Object"
+ set name = "Rotate Generator (Counter-clockwise)"
+ set src in view(1)
+
+ if (usr.incapacitated() || usr.restrained() || anchored)
+ return
+
+ src.dir = turn(src.dir, 90)
\ No newline at end of file
diff --git a/proxima/code/modules/power/fusion/fusion.dm b/proxima/code/modules/power/fusion/fusion.dm
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/proxima/code/modules/power/fusion/fusion_circuits.dm b/proxima/code/modules/power/fusion/fusion_circuits.dm
new file mode 100644
index 00000000000..a1757383b4c
--- /dev/null
+++ b/proxima/code/modules/power/fusion/fusion_circuits.dm
@@ -0,0 +1,123 @@
+/obj/item/electronics/circuitboard/fusion/core_control
+ name = T_BOARD("fusion core controller")
+ build_path = /obj/machinery/computer/fusion/core_control
+ origin_tech = list(TECH_DATA = 4, TECH_ENGINEERING = 4)
+
+/obj/item/electronics/circuitboard/kinetic_harvester
+ name = T_BOARD("kinetic harvester")
+ build_path = /obj/machinery/kinetic_harvester
+ board_type = "machine"
+ origin_tech = list(TECH_DATA = 4, TECH_ENGINEERING = 4, TECH_MATERIAL = 4)
+ req_components = list(
+ /obj/item/stock_parts/manipulator/pico = 2,
+ /obj/item/stock_parts/matter_bin/super = 1,
+ /obj/item/stock_parts/console_screen = 1,
+ /obj/item/stack/cable_coil = 5
+ )
+
+/obj/item/electronics/circuitboard/fusion_fuel_compressor
+ name = T_BOARD("fusion fuel compressor")
+ build_path = /obj/machinery/fusion_fuel_compressor
+ board_type = "machine"
+ origin_tech = list(TECH_POWER = 3, TECH_ENGINEERING = 4, TECH_MATERIAL = 4)
+ req_components = list(
+ /obj/item/stock_parts/manipulator/pico = 2,
+ /obj/item/stock_parts/matter_bin/super = 2,
+ /obj/item/stock_parts/console_screen = 1,
+ /obj/item/stack/cable_coil = 5
+ )
+
+/obj/item/electronics/circuitboard/fusion_fuel_control
+ name = T_BOARD("fusion fuel controller")
+ build_path = /obj/machinery/computer/fusion/fuel_control
+ origin_tech = list(TECH_DATA = 4, TECH_ENGINEERING = 4)
+
+/obj/item/electronics/circuitboard/gyrotron_control
+ name = T_BOARD("gyrotron controller")
+ build_path = /obj/machinery/computer/fusion/gyrotron
+ origin_tech = list(TECH_DATA = 4, TECH_ENGINEERING = 4)
+
+/obj/item/electronics/circuitboard/fusion_core
+ name = T_BOARD("fusion core")
+ build_path = /obj/machinery/power/fusion_core
+ board_type = "machine"
+ origin_tech = list(TECH_BLUESPACE = 2, TECH_MAGNET = 4, TECH_POWER = 4)
+ req_components = list(
+ /obj/item/stock_parts/manipulator/pico = 2,
+ /obj/item/stock_parts/micro_laser/ultra = 1,
+ /obj/item/stock_parts/subspace/crystal = 1,
+ /obj/item/stock_parts/console_screen = 1,
+ /obj/item/stack/cable_coil = 5
+ )
+
+/obj/item/electronics/circuitboard/fusion_injector
+ name = T_BOARD("fusion fuel injector")
+ build_path = /obj/machinery/fusion_fuel_injector
+ board_type = "machine"
+ origin_tech = list(TECH_POWER = 3, TECH_ENGINEERING = 4, TECH_MATERIAL = 4)
+ req_components = list(
+ /obj/item/stock_parts/manipulator/pico = 2,
+ /obj/item/stock_parts/scanning_module/phasic = 1,
+ /obj/item/stock_parts/matter_bin/super = 1,
+ /obj/item/stock_parts/console_screen = 1,
+ /obj/item/stack/cable_coil = 5
+ )
+
+/obj/item/electronics/circuitboard/gyrotron
+ name = T_BOARD("gyrotron")
+ build_path = /obj/machinery/power/emitter/gyrotron
+ board_type = "machine"
+ origin_tech = list(TECH_POWER = 4, TECH_ENGINEERING = 4)
+ req_components = list(
+ /obj/item/stack/cable_coil = 20,
+ /obj/item/stock_parts/micro_laser/ultra = 2
+ )
+
+/datum/design/research/circuit/fusion
+ name = "fusion core control console"
+ id = "fusion_core_control"
+ build_path = /obj/item/electronics/circuitboard/fusion/core_control
+ sort_string = "LAAAD"
+ category = CAT_POWER
+
+/datum/design/research/circuit/fusion/fuel_compressor
+ name = "fusion fuel compressor"
+ id = "fusion_fuel_compressor"
+ build_path = /obj/item/electronics/circuitboard/fusion_fuel_compressor
+ sort_string = "LAAAE"
+
+/datum/design/research/circuit/fusion/fuel_control
+ name = "fusion fuel control console"
+ id = "fusion_fuel_control"
+ build_path = /obj/item/electronics/circuitboard/fusion_fuel_control
+ sort_string = "LAAAF"
+
+/datum/design/research/circuit/fusion/gyrotron_control
+ name = "gyrotron control console"
+ id = "gyrotron_control"
+ build_path = /obj/item/electronics/circuitboard/gyrotron_control
+ sort_string = "LAAAG"
+
+/datum/design/research/circuit/fusion/core
+ name = "fusion core"
+ id = "fusion_core"
+ build_path = /obj/item/electronics/circuitboard/fusion_core
+ sort_string = "LAAAH"
+
+/datum/design/research/circuit/fusion/injector
+ name = "fusion fuel injector"
+ id = "fusion_injector"
+ build_path = /obj/item/electronics/circuitboard/fusion_injector
+ sort_string = "LAAAI"
+
+/datum/design/research/circuit/fusion/kinetic_harvester
+ name = "fusion toroid kinetic harvester"
+ id = "fusion_kinetic_harvester"
+ build_path = /obj/item/electronics/circuitboard/kinetic_harvester
+ sort_string = "LAAAJ"
+
+/datum/design/research/circuit/fusion/gyrotron
+ name = "gyrotron"
+ id = "gyrotron"
+ build_path = /obj/item/electronics/circuitboard/gyrotron
+ sort_string = "LAAAK"
diff --git a/proxima/code/modules/power/fusion/fusion_particle_catcher.dm b/proxima/code/modules/power/fusion/fusion_particle_catcher.dm
new file mode 100644
index 00000000000..de4f3e2a112
--- /dev/null
+++ b/proxima/code/modules/power/fusion/fusion_particle_catcher.dm
@@ -0,0 +1,40 @@
+/obj/effect/fusion_particle_catcher
+ icon = 'icons/effects/effects.dmi'
+ density = TRUE
+ anchored = TRUE
+ invisibility = 101
+ light_color = COLOR_BLUE
+ var/obj/effect/fusion_em_field/parent
+ var/mysize = 0
+
+/obj/effect/fusion_particle_catcher/Destroy()
+ . =..()
+ parent.particle_catchers -= src
+ parent = null
+
+/obj/effect/fusion_particle_catcher/proc/SetSize(var/newsize)
+ name = "collector [newsize]"
+ mysize = newsize
+ UpdateSize()
+
+/obj/effect/fusion_particle_catcher/proc/AddParticles(var/name, var/quantity = 1)
+ if(parent && parent.size >= mysize)
+ parent.AddParticles(name, quantity)
+ return 1
+ return 0
+
+/obj/effect/fusion_particle_catcher/proc/UpdateSize()
+ if(parent.size >= mysize)
+ set_density(1)
+ SetName("collector [mysize] ON")
+ else
+ set_density(0)
+ SetName("collector [mysize] OFF")
+
+/obj/effect/fusion_particle_catcher/bullet_act(var/obj/item/projectile/Proj)
+ parent.AddEnergy(Proj.damage)
+ update_icon()
+ return 0
+
+/obj/effect/fusion_particle_catcher/CanPass(var/atom/movable/mover, var/turf/target, var/height=0, var/air_group=0)
+ return ismob(mover)
diff --git a/proxima/code/modules/power/fusion/fusion_reactions.dm b/proxima/code/modules/power/fusion/fusion_reactions.dm
new file mode 100644
index 00000000000..bb9b97d53a5
--- /dev/null
+++ b/proxima/code/modules/power/fusion/fusion_reactions.dm
@@ -0,0 +1,168 @@
+var/global/list/fusion_reactions
+
+/decl/fusion_reaction
+ var/p_react = "" // Primary reactant.
+ var/s_react = "" // Secondary reactant.
+ var/minimum_energy_level = 1
+ var/energy_consumption = 0
+ var/energy_production = 0
+ var/radiation = 0
+ var/instability = 0
+ var/list/products = list()
+ var/minimum_reaction_temperature = 100
+ var/priority = 100
+
+/decl/fusion_reaction/proc/handle_reaction_special(var/obj/effect/fusion_em_field/holder)
+ return 0
+
+/proc/get_fusion_reaction(var/p_react, var/s_react, var/m_energy)
+ if(!fusion_reactions)
+ fusion_reactions = list()
+ for(var/rtype in typesof(/decl/fusion_reaction) - /decl/fusion_reaction)
+ var/decl/fusion_reaction/cur_reaction = new rtype()
+ if(!fusion_reactions[cur_reaction.p_react])
+ fusion_reactions[cur_reaction.p_react] = list()
+ fusion_reactions[cur_reaction.p_react][cur_reaction.s_react] = cur_reaction
+ if(!fusion_reactions[cur_reaction.s_react])
+ fusion_reactions[cur_reaction.s_react] = list()
+ fusion_reactions[cur_reaction.s_react][cur_reaction.p_react] = cur_reaction
+
+ if(fusion_reactions.Find(p_react))
+ var/list/secondary_reactions = fusion_reactions[p_react]
+ if(secondary_reactions.Find(s_react))
+ return fusion_reactions[p_react][s_react]
+
+// Material fuels
+// deuterium
+// tritium
+// phoron
+// supermatter
+
+// Gaseous/reagent fuels
+// hydrogen
+// helium
+// lithium
+// boron
+
+// Basic power production reactions.
+// This is not necessarily realistic, but it makes a basic failure more spectacular.
+/decl/fusion_reaction/hydrogen_hydrogen
+ p_react = GAS_HYDROGEN
+ s_react = GAS_HYDROGEN
+ energy_consumption = 1
+ energy_production = 2
+ products = list(GAS_HELIUM = 1)
+ priority = 10
+
+/decl/fusion_reaction/deuterium_deuterium
+ p_react = GAS_DEUTERIUM
+ s_react = GAS_DEUTERIUM
+ energy_consumption = 1
+ energy_production = 2
+ priority = 0
+
+// Advanced production reactions (todo)
+/decl/fusion_reaction/deuterium_helium
+ p_react = GAS_DEUTERIUM
+ s_react = GAS_HELIUM
+ energy_consumption = 1
+ energy_production = 5
+ radiation = 2
+
+/decl/fusion_reaction/deuterium_tritium
+ p_react = GAS_DEUTERIUM
+ s_react = GAS_TRITIUM
+ energy_consumption = 1
+ energy_production = 1
+ products = list(GAS_HELIUM = 1)
+ instability = 0.5
+ radiation = 3
+
+/decl/fusion_reaction/deuterium_lithium
+ p_react = GAS_DEUTERIUM
+ s_react = "lithium"
+ energy_consumption = 2
+ energy_production = 0
+ radiation = 3
+ products = list(GAS_TRITIUM= 1)
+ instability = 1
+
+// Unideal/material production reactions
+/decl/fusion_reaction/oxygen_oxygen
+ p_react = GAS_OXYGEN
+ s_react = GAS_OXYGEN
+ energy_consumption = 10
+ energy_production = 0
+ instability = 5
+ radiation = 5
+ products = list("silicon"= 1)
+
+/decl/fusion_reaction/iron_iron
+ p_react = "iron"
+ s_react = "iron"
+ products = list("silver" = 10, "gold" = 10, "platinum" = 10) // Not realistic but w/e
+ energy_consumption = 10
+ energy_production = 0
+ instability = 2
+ minimum_reaction_temperature = 10000
+
+/decl/fusion_reaction/phoron_hydrogen
+ p_react = GAS_HYDROGEN
+ s_react = GAS_PHORON
+ energy_consumption = 10
+ energy_production = 0
+ instability = 5
+ products = list("mhydrogen" = 1)
+ minimum_reaction_temperature = 8000
+
+// VERY UNIDEAL REACTIONS.
+/decl/fusion_reaction/phoron_supermatter
+ p_react = "supermatter"
+ s_react = GAS_PHORON
+ energy_consumption = 0
+ energy_production = 5
+ radiation = 40
+ instability = 20
+
+/decl/fusion_reaction/phoron_supermatter/handle_reaction_special(var/obj/effect/fusion_em_field/holder)
+
+ wormhole_event(GetConnectedZlevels(holder))
+
+ var/turf/origin = get_turf(holder)
+ holder.Rupture()
+ qdel(holder)
+ var/radiation_level = rand(100, 200)
+
+ // Copied from the SM for proof of concept. //Not any more --Cirra //Use the whole z proc --Leshana
+ SSradiation.z_radiate(locate(1, 1, holder.z), radiation_level, 1)
+
+ for(var/mob/living/mob in GLOB.living_mob_list_)
+ var/turf/T = get_turf(mob)
+ if(T && (holder.z == T.z))
+ if(istype(mob, /mob/living/carbon/human))
+ var/mob/living/carbon/human/H = mob
+ H.hallucination(rand(100,150), 51)
+
+ for(var/obj/machinery/fusion_fuel_injector/I in range(world.view, origin))
+ if(I.cur_assembly && I.cur_assembly.fuel_type == MATERIAL_SUPERMATTER)
+ explosion(get_turf(I), 1, 2, 3)
+ spawn(5)
+ if(I && I.loc)
+ qdel(I)
+
+ sleep(5)
+ explosion(origin, 1, 2, 5)
+
+ return 1
+
+
+// High end reactions.
+/decl/fusion_reaction/boron_hydrogen
+ p_react = GAS_BORON
+ s_react = GAS_HYDROGEN
+ minimum_energy_level = 15000
+ energy_consumption = 3
+ energy_production = 12
+ radiation = 3
+ instability = 2.5
+ products = list(GAS_HELIUM = 1)
diff --git a/proxima/code/modules/power/fusion/gyrotron/gyrotron.dm b/proxima/code/modules/power/fusion/gyrotron/gyrotron.dm
new file mode 100644
index 00000000000..9d309c5f7b4
--- /dev/null
+++ b/proxima/code/modules/power/fusion/gyrotron/gyrotron.dm
@@ -0,0 +1,64 @@
+#define GYRO_POWER 25000
+
+/obj/machinery/power/emitter/gyrotron
+ name = "gyrotron"
+ icon = 'icons/obj/machines/power/fusion.dmi'
+ desc = "A heavy-duty, highly configurable industrial gyrotron suited for powering fusion reactors."
+ icon_state = "emitter-off"
+ req_lock_access = list(access_engine)
+ use_power = POWER_USE_IDLE
+ active_power_usage = GYRO_POWER
+
+ var/initial_id_tag
+ var/rate = 3
+ var/mega_energy = 1
+
+ construct_state = /decl/machine_construction/default/panel_closed
+ uncreated_component_parts = list(
+ /obj/item/stock_parts/radio/receiver,
+ )
+ stat_immune = 0
+ base_type = /obj/machinery/power/emitter/gyrotron
+
+/obj/machinery/power/emitter/gyrotron/anchored
+ anchored = TRUE
+ state = EMITTER_WELDED
+
+/obj/machinery/power/emitter/gyrotron/Initialize()
+ set_extension(src, /datum/extension/local_network_member)
+ if(initial_id_tag)
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ fusion.set_tag(null, initial_id_tag)
+ change_power_consumption(mega_energy * GYRO_POWER, POWER_USE_ACTIVE)
+ . = ..()
+
+/obj/machinery/power/emitter/gyrotron/Process()
+ change_power_consumption(mega_energy * GYRO_POWER, POWER_USE_ACTIVE)
+ . = ..()
+
+/obj/machinery/power/emitter/gyrotron/get_rand_burst_delay()
+ return rate * 10
+
+/obj/machinery/power/emitter/gyrotron/get_burst_delay()
+ return rate * 10
+
+/obj/machinery/power/emitter/gyrotron/get_emitter_beam()
+ var/obj/item/projectile/beam/emitter/E = ..()
+ E.damage = mega_energy * 50
+ return E
+
+/obj/machinery/power/emitter/gyrotron/on_update_icon()
+ if (active && powernet && avail(active_power_usage))
+ icon_state = "emitter-on"
+ else
+ icon_state = "emitter-off"
+
+/obj/machinery/power/emitter/gyrotron/attackby(var/obj/item/W, var/mob/user)
+ if(istype(W, /obj/item/tool/multitool))
+ W.set_buffer
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ fusion.get_new_tag(user)
+ return
+ return ..()
+
+#undef GYRO_POWER
diff --git a/proxima/code/modules/power/fusion/kinetic_harvester.dm b/proxima/code/modules/power/fusion/kinetic_harvester.dm
new file mode 100644
index 00000000000..e1b54870dfe
--- /dev/null
+++ b/proxima/code/modules/power/fusion/kinetic_harvester.dm
@@ -0,0 +1,131 @@
+/obj/machinery/kinetic_harvester
+ name = "kinetic harvester"
+ desc = "A complicated mechanism for harvesting rapidly moving particles from a fusion toroid and condensing them into a usable form."
+ density = TRUE
+ anchored = TRUE
+ use_power = POWER_USE_IDLE
+ icon = 'icons/obj/kinetic_harvester.dmi'
+ icon_state = "off"
+ var/initial_id_tag
+ var/list/stored = list()
+ var/list/harvesting = list()
+ var/obj/machinery/power/fusion_core/harvest_from
+ construct_state = /decl/machine_construction/default/panel_closed
+ uncreated_component_parts = null
+ stat_immune = 0
+
+/obj/machinery/kinetic_harvester/Initialize()
+ set_extension(src, /datum/extension/local_network_member)
+ if(initial_id_tag)
+ var/datum/extension/local_network_member/lanm = get_extension(src, /datum/extension/local_network_member)
+ lanm.set_tag(null, initial_id_tag)
+ find_core()
+ queue_icon_update()
+ . = ..()
+
+/obj/machinery/kinetic_harvester/interface_interact(mob/user)
+ ui_interact(user)
+ return TRUE
+
+/obj/machinery/kinetic_harvester/attackby(var/obj/item/thing, var/mob/user)
+ if(isMultitool(thing))
+ var/datum/extension/local_network_member/lanm = get_extension(src, /datum/extension/local_network_member)
+ if(lanm.get_new_tag(user))
+ find_core()
+ return
+ return ..()
+
+/obj/machinery/kinetic_harvester/proc/find_core()
+ harvest_from = null
+ var/datum/extension/local_network_member/lanm = get_extension(src, /datum/extension/local_network_member)
+ var/datum/local_network/lan = lanm.get_local_network()
+
+ if(lan)
+ var/list/fusion_cores = lan.get_devices(/obj/machinery/power/fusion_core)
+ if(fusion_cores && fusion_cores.len)
+ harvest_from = fusion_cores[1]
+ return harvest_from
+
+/obj/machinery/kinetic_harvester/ui_interact(var/mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1)
+
+ if(!harvest_from && !find_core())
+ to_chat(user, SPAN_WARNING("This machine cannot locate a fusion core. Please ensure the machine is correctly configured to share a fusion plant network."))
+ return
+
+ var/datum/extension/local_network_member/fusion = get_extension(src, /datum/extension/local_network_member)
+ var/datum/local_network/plant = fusion.get_local_network()
+ var/list/data = list()
+
+ data["id"] = plant ? plant.id_tag : "unset"
+ data["status"] = (use_power >= POWER_USE_ACTIVE)
+ data["materials"] = list()
+ for(var/mat in stored)
+ var/material/material = SSmaterials.get_material_by_name(mat)
+ if(material)
+ var/sheets = Floor(stored[mat]/(material.units_per_sheet * 1.5))
+ data["materials"] += list(list("material" = mat, "rawamount" = stored[mat], "amount" = sheets, "harvest" = harvesting[mat]))
+
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, "kinetic_harvester.tmpl", name, 400, 600)
+ ui.set_initial_data(data)
+ ui.open()
+ ui.set_auto_update(1)
+
+/obj/machinery/kinetic_harvester/Process()
+ if(harvest_from && get_dist(src, harvest_from) > 10)
+ harvest_from = null
+
+ if(use_power >= POWER_USE_ACTIVE)
+ if(harvest_from && harvest_from.owned_field)
+ for(var/mat in harvest_from.owned_field.reactants)
+ if(SSmaterials.materials_by_name[mat] && !stored[mat])
+ stored[mat] = 0
+ for(var/mat in harvesting)
+ if(!SSmaterials.materials_by_name[mat] || !harvest_from.owned_field.reactants[mat])
+ harvesting -= mat
+ else
+ var/harvest = min(harvest_from.owned_field.reactants[mat], rand(100,200))
+ harvest_from.owned_field.reactants[mat] -= harvest
+ if(harvest_from.owned_field.reactants[mat] <= 0)
+ harvest_from.owned_field.reactants -= mat
+ stored[mat] += harvest
+ else
+ harvesting.Cut()
+
+/obj/machinery/kinetic_harvester/on_update_icon()
+ if(stat & (BROKEN|NOPOWER))
+ icon_state = "broken"
+ else if(use_power >= POWER_USE_ACTIVE)
+ icon_state = "on"
+ else
+ icon_state = "off"
+
+/obj/machinery/kinetic_harvester/OnTopic(var/mob/user, var/href_list, var/datum/topic_state/state)
+ if(href_list["remove_mat"])
+ var/mat = href_list["remove_mat"]
+ var/material/material = SSmaterials.get_material_by_name(mat)
+ if(material)
+ var/sheet_cost = (material.units_per_sheet * 1.5)
+ var/sheets = Floor(stored[mat]/sheet_cost)
+ if(sheets > 0)
+ material.place_sheet(loc, sheets)
+ stored[mat] -= sheets * sheet_cost
+ if(stored[mat] <= 0)
+ stored -= mat
+ return TOPIC_REFRESH
+
+ if(href_list["toggle_power"])
+ use_power = (use_power >= POWER_USE_ACTIVE ? POWER_USE_IDLE : POWER_USE_ACTIVE)
+ queue_icon_update()
+ return TOPIC_REFRESH
+
+ if(href_list["toggle_harvest"])
+ var/mat = href_list["toggle_harvest"]
+ if(harvesting[mat])
+ harvesting -= mat
+ else
+ harvesting[mat] = TRUE
+ if(!(mat in stored))
+ stored[mat] = 0
+ return TOPIC_REFRESH
diff --git a/proxima/icons/mob/human_races/vey-med.dmi b/proxima/icons/mob/human_races/vey-med.dmi
new file mode 100644
index 00000000000..0ed8d080155
Binary files /dev/null and b/proxima/icons/mob/human_races/vey-med.dmi differ
diff --git a/proxima/icons/obj/stack/material_titanium.dmi b/proxima/icons/obj/stack/material_titanium.dmi
new file mode 100644
index 00000000000..5dde70c18b7
Binary files /dev/null and b/proxima/icons/obj/stack/material_titanium.dmi differ