diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index bdb9ad35a9..b83fc3dd39 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -466,3 +466,6 @@ GLOBAL_LIST_INIT(ghost_others_options, list(GHOST_OTHERS_SIMPLE, GHOST_OTHERS_DE
//Dummy mob reserve slots
#define DUMMY_HUMAN_SLOT_PREFERENCES "dummy_preference_preview"
#define DUMMY_HUMAN_SLOT_MANIFEST "dummy_manifest_generation"
+
+#define PR_ANNOUNCEMENTS_PER_ROUND 5 //The number of unique PR announcements allowed per round
+ //This makes sure that a single person can only spam 3 reopens and 3 closes before being ignored
\ No newline at end of file
diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm
new file mode 100644
index 0000000000..02cb4e0c31
--- /dev/null
+++ b/code/datums/world_topic.dm
@@ -0,0 +1,168 @@
+// SETUP
+
+/proc/TopicHandlers()
+ . = list()
+ var/list/all_handlers = subtypesof(/datum/world_topic)
+ for(var/I in all_handlers)
+ var/datum/world_topic/WT = I
+ var/keyword = initial(WT.keyword)
+ if(!keyword)
+ warning("[WT] has no keyword! Ignoring...")
+ continue
+ var/existing_path = .[keyword]
+ if(existing_path)
+ warning("[existing_path] and [WT] have the same keyword! Ignoring [WT]...")
+ else if(keyword == "key")
+ warning("[WT] has keyword 'key'! Ignoring...")
+ else
+ .[keyword] = WT
+
+// DATUM
+
+/datum/world_topic
+ var/keyword
+ var/log = TRUE
+ var/key_valid
+ var/require_comms_key = FALSE
+
+/datum/world_topic/proc/TryRun(list/input)
+ key_valid = !config || CONFIG_GET(string/comms_key) != input["key"]
+ if(require_comms_key && !key_valid)
+ return "Bad Key"
+ input -= "key"
+ . = Run(input)
+ if(islist(.))
+ . = list2params(.)
+
+/datum/world_topic/proc/Run(list/input)
+ CRASH("Run() not implemented for [type]!")
+
+// TOPICS
+
+/datum/world_topic/ping
+ keyword = "ping"
+ log = FALSE
+
+/datum/world_topic/ping/Run(list/input)
+ . = 0
+ for (var/client/C in GLOB.clients)
+ ++.
+
+/datum/world_topic/playing
+ keyword = "playing"
+ log = FALSE
+
+/datum/world_topic/playing/Run(list/input)
+ return GLOB.player_list.len
+
+/datum/world_topic/pr_announce
+ keyword = "announce"
+ require_comms_key = TRUE
+ var/static/list/PRcounts = list() //PR id -> number of times announced this round
+
+/datum/world_topic/pr_announce/Run(list/input)
+ var/list/payload = json_decode(input["payload"])
+ var/id = "[payload["pull_request"]["id"]]"
+ if(!PRcounts[id])
+ PRcounts[id] = 1
+ else
+ ++PRcounts[id]
+ if(PRcounts[id] > PR_ANNOUNCEMENTS_PER_ROUND)
+ return
+
+ var/final_composed = "PR: [input[keyword]]"
+ for(var/client/C in GLOB.clients)
+ C.AnnouncePR(final_composed)
+
+/datum/world_topic/ahelp_relay
+ keyword = "ahelp_relay"
+ require_comms_key = TRUE
+
+/datum/world_topic/ahelp_relay/Run(list/input)
+ relay_msg_admins("HELP: [input["source"]] [input["message_sender"]]: [input["message"]]")
+
+/datum/world_topic/comms_console
+ keyword = "comms_console"
+ require_comms_key = TRUE
+
+/datum/world_topic/comms_console/Run(list/input)
+ minor_announce(input["message"], "Incoming message from [input["message_sender"]]")
+ for(var/obj/machinery/computer/communications/CM in GLOB.machines)
+ CM.overrideCooldown()
+
+/datum/world_topic/news_report
+ keyword = "news_report"
+ require_comms_key = TRUE
+
+/datum/world_topic/news_report/Run(list/input)
+ minor_announce(input["message"], "Breaking Update From [input["message_sender"]]")
+
+/datum/world_topic/server_hop
+ keyword = "server_hop"
+
+/datum/world_topic/server_hop/Run(list/input)
+ var/expected_key = input[keyword]
+ for(var/mob/dead/observer/O in GLOB.player_list)
+ if(O.key == expected_key)
+ if(O.client)
+ new /obj/screen/splash(O.client, TRUE)
+ break
+
+/datum/world_topic/adminmsg
+ keyword = "adminmsg"
+
+/datum/world_topic/adminmsg/Run(list/input)
+ return IrcPm(input[keyword], input["msg"], input["sender"])
+
+/datum/world_topic/namecheck
+ keyword = "namecheck"
+
+/datum/world_topic/namecheck/Run(list/input)
+ var/datum/server_tools_command/namecheck/NC = new
+ return NC.Run(input["sender"], input["namecheck"])
+
+/datum/world_topic/adminwho
+ keyword = "namecheck"
+
+/datum/world_topic/adminwho/Run(list/input)
+ return ircadminwho()
+
+/datum/world_topic/status
+ keyword = "status"
+
+/datum/world_topic/status/Run(list/input)
+ . = list()
+ .["version"] = GLOB.game_version
+ .["mode"] = GLOB.master_mode
+ .["respawn"] = config ? !CONFIG_GET(flag/norespawn) : FALSE
+ .["enter"] = GLOB.enter_allowed
+ .["vote"] = CONFIG_GET(flag/allow_vote_mode)
+ .["ai"] = CONFIG_GET(flag/allow_ai)
+ .["host"] = world.host ? world.host : null
+ .["players"] = GLOB.clients.len
+ .["revision"] = GLOB.revdata.commit
+ .["revision_date"] = GLOB.revdata.date
+
+ var/list/adm = get_admin_counts()
+ var/list/presentmins = adm["present"]
+ var/list/afkmins = adm["afk"]
+ .["admins"] = presentmins.len + afkmins.len //equivalent to the info gotten from adminwho
+ .["gamestate"] = SSticker.current_state
+
+ .["map_name"] = SSmapping.config.map_name
+
+ if(key_valid)
+ .["active_players"] = get_active_player_count()
+ if(SSticker.HasRoundStarted())
+ .["real_mode"] = SSticker.mode.name
+ // Key-authed callers may know the truth behind the "secret"
+
+ .["security_level"] = get_security_level()
+ .["round_duration"] = SSticker ? round((world.time-SSticker.round_start_time)/10) : 0
+ // Amount of world's ticks in seconds, useful for calculating round duration
+
+ if(SSshuttle && SSshuttle.emergency)
+ .["shuttle_mode"] = SSshuttle.emergency.mode
+ // Shuttle status, see /__DEFINES/stat.dm
+ .["shuttle_timer"] = SSshuttle.emergency.timeLeft()
+ // Shuttle timer, in seconds
diff --git a/code/game/world.dm b/code/game/world.dm
index bd04008ea3..5fe674cb7f 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -1,6 +1,3 @@
-#define PR_ANNOUNCEMENTS_PER_ROUND 5 //The number of unique PR announcements allowed per round
- //This makes sure that a single person can only spam 3 reopens and 3 closes before being ignored
-
GLOBAL_VAR(security_mode)
GLOBAL_PROTECT(security_mode)
@@ -118,91 +115,25 @@ GLOBAL_PROTECT(security_mode)
warning("/tg/station 13 uses many file operations, a few shell()s, and some external call()s. Trusted mode is recommended. You can download our source code for your own browsing and compilation at https://github.com/tgstation/tgstation")
/world/Topic(T, addr, master, key)
+ var/static/list/topic_handlers = TopicHandlers()
+
var/list/input = params2list(T)
-
- var/pinging = ("ping" in input)
- var/playing = ("players" in input)
-
- if(!pinging && !playing && config && CONFIG_GET(flag/log_world_topic))
+ var/datum/world_topic/handler
+ for(var/I in topic_handlers)
+ if(input[I])
+ handler = I
+ break
+
+ if((!handler || initial(handler.log)) && config && CONFIG_GET(flag/log_world_topic))
WRITE_FILE(GLOB.world_game_log, "TOPIC: \"[T]\", from:[addr], master:[master], key:[key]")
SERVER_TOOLS_ON_TOPIC //redirect to server tools if necessary
- var/comms_key = CONFIG_GET(string/comms_key)
- var/key_valid = (comms_key && input["key"] == comms_key)
+ if(!handler)
+ return
- if(pinging)
- var/x = 1
- for (var/client/C in GLOB.clients)
- x++
- return x
-
- else if(playing)
- var/n = 0
- for(var/mob/M in GLOB.player_list)
- if(M.client)
- n++
- return n
-
- else if("status" in input)
- var/list/s = list()
- s["version"] = GLOB.game_version
- s["mode"] = GLOB.master_mode
- s["respawn"] = config ? !CONFIG_GET(flag/norespawn) : FALSE
- s["enter"] = GLOB.enter_allowed
- s["vote"] = CONFIG_GET(flag/allow_vote_mode)
- s["ai"] = CONFIG_GET(flag/allow_ai)
- s["host"] = host ? host : null
- s["active_players"] = get_active_player_count()
- s["players"] = GLOB.clients.len
- s["revision"] = GLOB.revdata.commit
- s["revision_date"] = GLOB.revdata.date
-
- var/list/adm = get_admin_counts()
- var/list/presentmins = adm["present"]
- var/list/afkmins = adm["afk"]
- s["admins"] = presentmins.len + afkmins.len //equivalent to the info gotten from adminwho
- s["gamestate"] = SSticker.current_state
-
- s["map_name"] = SSmapping.config.map_name
-
- if(key_valid && SSticker.HasRoundStarted())
- s["real_mode"] = SSticker.mode.name
- // Key-authed callers may know the truth behind the "secret"
-
- s["security_level"] = get_security_level()
- s["round_duration"] = SSticker ? round((world.time-SSticker.round_start_time)/10) : 0
- // Amount of world's ticks in seconds, useful for calculating round duration
-
- if(SSshuttle && SSshuttle.emergency)
- s["shuttle_mode"] = SSshuttle.emergency.mode
- // Shuttle status, see /__DEFINES/stat.dm
- s["shuttle_timer"] = SSshuttle.emergency.timeLeft()
- // Shuttle timer, in seconds
-
- return list2params(s)
-
- else if("announce" in input)
- if(!key_valid)
- return "Bad Key"
- else
- AnnouncePR(input["announce"], json_decode(input["payload"]))
-
- else if("crossmessage" in input)
- if(!key_valid)
- return
- else
- if(input["crossmessage"] == "Ahelp")
- relay_msg_admins("HELP: [input["source"]] [input["message_sender"]]: [input["message"]]")
- if(input["crossmessage"] == "Comms_Console")
- minor_announce(input["message"], "Incoming message from [input["message_sender"]]")
- for(var/obj/machinery/computer/communications/CM in GLOB.machines)
- CM.overrideCooldown()
- if(input["crossmessage"] == "News_Report")
- minor_announce(input["message"], "Breaking Update From [input["message_sender"]]")
-
- else if("server_hop" in input)
- show_server_hop_transfer_screen(input["server_hop"])
+ handler = new handler()
+ return handler.Run(input)
/world/proc/AnnouncePR(announcement, list/payload)
var/static/list/PRcounts = list() //PR id -> number of times announced this round
diff --git a/code/modules/admin/chat_commands.dm b/code/modules/admin/chat_commands.dm
index f24e03aabc..ec79aaca07 100644
--- a/code/modules/admin/chat_commands.dm
+++ b/code/modules/admin/chat_commands.dm
@@ -49,8 +49,8 @@
admin_only = TRUE
/datum/server_tools_command/namecheck/Run(sender, params)
- log_admin("IRC Name Check: [sender] on [params]")
- message_admins("IRC name checking on [params] from [sender]")
+ log_admin("Chat Name Check: [sender] on [params]")
+ message_admins("Name checking [params] from [sender]")
return keywords_lookup(params, 1)
/datum/server_tools_command/adminwho
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 1022719805..4c6628a53b 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -597,14 +597,6 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
target.faction = list("neutral")
return 1
-/proc/show_server_hop_transfer_screen(expected_key)
- //only show it to incoming ghosts
- for(var/mob/dead/observer/O in GLOB.player_list)
- if(O.key == expected_key)
- if(O.client)
- new /obj/screen/splash(O.client, TRUE)
- break
-
//this is a mob verb instead of atom for performance reasons
//see /mob/verb/examinate() in mob.dm for more info
//overriden here and in /mob/living for different point span classes and sanity checks
diff --git a/tgstation.dme b/tgstation.dme
index c63b893b69..581c882e41 100755
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -291,6 +291,7 @@
#include "code\datums\soullink.dm"
#include "code\datums\spawners_menu.dm"
#include "code\datums\verbs.dm"
+#include "code\datums\world_topic.dm"
#include "code\datums\antagonists\antag_datum.dm"
#include "code\datums\antagonists\datum_abductor.dm"
#include "code\datums\antagonists\datum_brother.dm"