diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index c194e578c9..5c54843df2 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -47,6 +47,7 @@
// Subsystems shutdown in the reverse of the order they initialize in
// The numbers just define the ordering, they are meaningless otherwise.
+#define INIT_ORDER_FAIL2TOPIC 22
#define INIT_ORDER_TITLE 20
#define INIT_ORDER_GARBAGE 19
#define INIT_ORDER_DBCORE 18
diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm
index 3ee77d3edc..4dd590d9a1 100644
--- a/code/__HELPERS/_logging.dm
+++ b/code/__HELPERS/_logging.dm
@@ -78,7 +78,6 @@
if (CONFIG_GET(flag/log_manifest))
WRITE_LOG(GLOB.world_manifest_log, "[ckey] \\ [body.real_name] \\ [mind.assigned_role] \\ [mind.special_role ? mind.special_role : "NONE"] \\ [latejoin ? "LATEJOIN":"ROUNDSTART"]")
-
/proc/log_say(text)
if (CONFIG_GET(flag/log_say))
WRITE_LOG(GLOB.world_game_log, "SAY: [text]")
@@ -121,7 +120,6 @@
if (CONFIG_GET(flag/log_vote))
WRITE_LOG(GLOB.world_game_log, "VOTE: [text]")
-
/proc/log_topic(text)
WRITE_LOG(GLOB.world_game_log, "TOPIC: [text]")
@@ -141,6 +139,9 @@
if (CONFIG_GET(flag/log_job_debug))
WRITE_LOG(GLOB.world_job_debug_log, "JOB: [text]")
+/proc/log_subsystem(subsystem, text)
+ WRITE_LOG(GLOB.subsystem_log, "[subsystem]: [text]")
+
/* Log to both DD and the logfile. */
/proc/log_world(text)
#ifdef USE_CUSTOM_ERROR_HANDLER
diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm
index 5ca3513e66..01d5051dba 100644
--- a/code/_globalvars/logging.dm
+++ b/code/_globalvars/logging.dm
@@ -30,6 +30,8 @@ GLOBAL_VAR(world_virus_log)
GLOBAL_PROTECT(world_virus_log)
GLOBAL_VAR(world_map_error_log)
GLOBAL_PROTECT(world_map_error_log)
+GLOBAL_VAR(subsystem_log)
+GLOBAL_PROTECT(subsystem_log)
GLOBAL_LIST_EMPTY(bombers)
GLOBAL_PROTECT(bombers)
diff --git a/code/controllers/configuration/entries/fail2topic.dm b/code/controllers/configuration/entries/fail2topic.dm
new file mode 100644
index 0000000000..7ed09b378a
--- /dev/null
+++ b/code/controllers/configuration/entries/fail2topic.dm
@@ -0,0 +1,19 @@
+/datum/config_entry/number/fail2topic_rate_limit
+ config_entry_value = 10 //deciseconds
+
+/datum/config_entry/number/fail2topic_max_fails
+ config_entry_value = 5
+
+/datum/config_entry/string/fail2topic_rule_name
+ config_entry_value = "_dd_fail2topic"
+ protection = CONFIG_ENTRY_LOCKED //affects physical server configuration, no touchies!!
+
+/datum/config_entry/flag/fail2topic_enabled
+ config_entry_value = TRUE
+
+/datum/config_entry/number/topic_max_size
+ config_entry_value = 8192
+
+/datum/config_entry/keyed_list/topic_rate_limit_whitelist
+ key_mode = KEY_MODE_TEXT
+ value_mode = VALUE_MODE_FLAG
diff --git a/code/controllers/master.dm b/code/controllers/master.dm
index 125da84a30..7244212630 100644
--- a/code/controllers/master.dm
+++ b/code/controllers/master.dm
@@ -54,7 +54,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/static/restart_clear = 0
var/static/restart_timeout = 0
var/static/restart_count = 0
-
+
var/static/random_seed
//current tick limit, assigned before running a subsystem.
@@ -69,7 +69,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
if(!random_seed)
random_seed = (TEST_RUN_PARAMETER in world.params) ? 29051994 : rand(1, 1e9)
rand_seed(random_seed)
-
+
var/list/_subsystems = list()
subsystems = _subsystems
if (Master != src)
diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm
index 4fe0812c56..3be4f36270 100644
--- a/code/controllers/subsystem.dm
+++ b/code/controllers/subsystem.dm
@@ -155,6 +155,8 @@
if(SS_SLEEPING)
state = SS_PAUSING
+/datum/controller/subsystem/proc/subsystem_log(msg)
+ return log_subsystem(name, msg)
//used to initialize the subsystem AFTER the map has loaded
/datum/controller/subsystem/Initialize(start_timeofday)
@@ -162,7 +164,7 @@
var/time = (REALTIMEOFDAY - start_timeofday) / 10
var/msg = "Initialized [name] subsystem within [time] second[time == 1 ? "" : "s"]!"
to_chat(world, "[msg]")
- log_world(msg)
+ log_subsystem("INIT", msg)
return time
//hook for printing stats to the "MC" statuspanel for admins to see performance and related stats etc.
diff --git a/code/controllers/subsystem/fail2topic.dm b/code/controllers/subsystem/fail2topic.dm
new file mode 100644
index 0000000000..a589ae2462
--- /dev/null
+++ b/code/controllers/subsystem/fail2topic.dm
@@ -0,0 +1,113 @@
+SUBSYSTEM_DEF(fail2topic)
+ name = "Fail2Topic"
+ init_order = INIT_ORDER_FAIL2TOPIC
+ flags = SS_BACKGROUND
+ runlevels = ALL
+
+ var/list/rate_limiting = list()
+ var/list/fail_counts = list()
+ var/list/active_bans = list()
+
+ var/rate_limit
+ var/max_fails
+ var/rule_name
+ var/enabled = FALSE
+
+/datum/controller/subsystem/fail2topic/Initialize(timeofday)
+ rate_limit = CONFIG_GET(number/fail2topic_rate_limit)
+ max_fails = CONFIG_GET(number/fail2topic_max_fails)
+ rule_name = CONFIG_GET(string/fail2topic_rule_name)
+ enabled = CONFIG_GET(flag/fail2topic_enabled)
+
+ DropFirewallRule() // Clear the old bans if any still remain
+
+ if (world.system_type == UNIX && enabled)
+ enabled = FALSE
+ subsystem_log("DISABLED - UNIX systems are not supported.")
+ if(!enabled)
+ flags |= SS_NO_FIRE
+ can_fire = FALSE
+
+ return ..()
+
+/datum/controller/subsystem/fail2topic/fire()
+ while (rate_limiting.len)
+ var/ip = rate_limiting[1]
+ var/last_attempt = rate_limiting[ip]
+
+ if (world.time - last_attempt > rate_limit)
+ rate_limiting -= ip
+ fail_counts -= ip
+
+ if (MC_TICK_CHECK)
+ return
+
+/datum/controller/subsystem/fail2topic/Shutdown()
+ DropFirewallRule()
+
+/datum/controller/subsystem/fail2topic/proc/IsRateLimited(ip)
+ var/last_attempt = rate_limiting[ip]
+
+ var/static/datum/config_entry/keyed_list/topic_rate_limit_whitelist/cached_whitelist_entry
+ if(!istype(cached_whitelist_entry))
+ cached_whitelist_entry = CONFIG_GET(keyed_list/topic_rate_limit_whitelist)
+
+ if(istype(cached_whitelist_entry))
+ if(cached_whitelist_entry.config_entry_value[ip])
+ return FALSE
+
+ if (active_bans[ip])
+ return TRUE
+
+ rate_limiting[ip] = world.time
+
+ if (isnull(last_attempt))
+ return FALSE
+
+ if (world.time - last_attempt > rate_limit)
+ fail_counts -= ip
+ return FALSE
+ else
+ var/failures = fail_counts[ip]
+
+ if (isnull(failures))
+ fail_counts[ip] = 1
+ return TRUE
+ else if (failures > max_fails)
+ BanFromFirewall(ip)
+ return TRUE
+ else
+ fail_counts[ip] = failures + 1
+ return TRUE
+
+/datum/controller/subsystem/fail2topic/proc/BanFromFirewall(ip)
+ if (!enabled)
+ return
+
+ active_bans[ip] = world.time
+ fail_counts -= ip
+ rate_limiting -= ip
+
+ . = shell("netsh advfirewall firewall add rule name=\"[rule_name]\" dir=in interface=any action=block remoteip=[ip]")
+
+ if (.)
+ subsystem_log("Failed to ban [ip]. Exit code: [.].")
+ else if (isnull(.))
+ subsystem_log("Failed to invoke shell to ban [ip].")
+ else
+ subsystem_log("Banned [ip].")
+
+/datum/controller/subsystem/fail2topic/proc/DropFirewallRule()
+ if (!enabled)
+ return
+
+ active_bans = list()
+
+ . = shell("netsh advfirewall firewall delete rule name=\"[rule_name]\"")
+
+ if (.)
+ subsystem_log("Failed to drop firewall rule. Exit code: [.].")
+ else if (isnull(.))
+ subsystem_log("Failed to invoke shell for firewall rule drop.")
+ else
+ subsystem_log("Firewall rule dropped.")
diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm
index 612c86bc11..9e2efd1d4e 100644
--- a/code/datums/world_topic.dm
+++ b/code/datums/world_topic.dm
@@ -1,189 +1,189 @@
-// 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"
- 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"
- require_comms_key = TRUE
-
-/datum/world_topic/adminmsg/Run(list/input)
- return IrcPm(input[keyword], input["msg"], input["sender"])
-
-/datum/world_topic/namecheck
- keyword = "namecheck"
- require_comms_key = TRUE
-
-/datum/world_topic/namecheck/Run(list/input)
- //Oh this is a hack, someone refactor the functionality out of the chat command PLS
- var/datum/tgs_chat_command/namecheck/NC = new
- var/datum/tgs_chat_user/user = new
- user.friendly_name = input["sender"]
- user.mention = user.friendly_name
- return NC.Run(user, input["namecheck"])
-
-/datum/world_topic/adminwho
- keyword = "adminwho"
- require_comms_key = TRUE
-
-/datum/world_topic/adminwho/Run(list/input)
- return ircadminwho()
-
-/datum/world_topic/status
- keyword = "status"
-
-/datum/world_topic/status/Run(list/input)
- if(!key_valid) //If we have a key, then it's safe to trust that this isn't a malicious packet. Also prevents the extra info from leaking
- if(GLOB.topic_status_lastcache <= world.time + 5)
- return GLOB.topic_status_cache
- GLOB.topic_status_lastcache = world.time
- . = list()
- .["version"] = GLOB.game_version
- .["mode"] = "hidden" //CIT CHANGE - hides the gamemode in topic() calls to prevent meta'ing the gamemode
- .["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
- .["round_id"] = GLOB.round_id
- .["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 || "Loading..."
-
- 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
-
- //Time dilation stats.
- .["time_dilation_current"] = SStime_track.time_dilation_current
- .["time_dilation_avg"] = SStime_track.time_dilation_avg
- .["time_dilation_avg_slow"] = SStime_track.time_dilation_avg_slow
- .["time_dilation_avg_fast"] = SStime_track.time_dilation_avg_fast
-
- if(SSshuttle && SSshuttle.emergency)
- .["shuttle_mode"] = SSshuttle.emergency.mode
- // Shuttle status, see /__DEFINES/stat.dm
- .["shuttle_timer"] = SSshuttle.emergency.timeLeft()
- // Shuttle timer, in seconds
-
- if(!key_valid)
- GLOB.topic_status_cache = .
+// 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, addr)
+ key_valid = config && (CONFIG_GET(string/comms_key) == input["key"])
+ if(require_comms_key && !key_valid)
+ return "Bad Key"
+ input -= "key"
+ . = Run(input, addr)
+ if(islist(.))
+ . = list2params(.)
+
+/datum/world_topic/proc/Run(list/input, addr)
+ CRASH("Run() not implemented for [type]!")
+
+// TOPICS
+
+/datum/world_topic/ping
+ keyword = "ping"
+ log = FALSE
+
+/datum/world_topic/ping/Run(list/input, addr)
+ . = 0
+ for (var/client/C in GLOB.clients)
+ ++.
+
+/datum/world_topic/playing
+ keyword = "playing"
+ log = FALSE
+
+/datum/world_topic/playing/Run(list/input, addr)
+ 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, addr)
+ 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"
+ require_comms_key = TRUE
+
+/datum/world_topic/ahelp_relay/Run(list/input, addr)
+ 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, addr)
+ 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, addr)
+ 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, addr)
+ var/expected_key = input[keyword]
+ for(var/mob/dead/observer/O in GLOB.player_list)
+ if(O.key == expected_key)
+ if(O.client?.address == addr)
+ new /obj/screen/splash(O.client, TRUE)
+ break
+
+/datum/world_topic/adminmsg
+ keyword = "adminmsg"
+ require_comms_key = TRUE
+
+/datum/world_topic/adminmsg/Run(list/input, addr)
+ return IrcPm(input[keyword], input["msg"], input["sender"])
+
+/datum/world_topic/namecheck
+ keyword = "namecheck"
+ require_comms_key = TRUE
+
+/datum/world_topic/namecheck/Run(list/input, addr)
+ //Oh this is a hack, someone refactor the functionality out of the chat command PLS
+ var/datum/tgs_chat_command/namecheck/NC = new
+ var/datum/tgs_chat_user/user = new
+ user.friendly_name = input["sender"]
+ user.mention = user.friendly_name
+ return NC.Run(user, input["namecheck"])
+
+/datum/world_topic/adminwho
+ keyword = "adminwho"
+ require_comms_key = TRUE
+
+/datum/world_topic/adminwho/Run(list/input, addr)
+ return ircadminwho()
+
+/datum/world_topic/status
+ keyword = "status"
+
+/datum/world_topic/status/Run(list/input, addr)
+ if(!key_valid) //If we have a key, then it's safe to trust that this isn't a malicious packet. Also prevents the extra info from leaking
+ if(GLOB.topic_status_lastcache <= world.time + 5)
+ return GLOB.topic_status_cache
+ GLOB.topic_status_lastcache = world.time
+ . = list()
+ .["version"] = GLOB.game_version
+ .["mode"] = "hidden" //CIT CHANGE - hides the gamemode in topic() calls to prevent meta'ing the gamemode
+ .["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
+ .["round_id"] = GLOB.round_id
+ .["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 || "Loading..."
+
+ 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
+
+ //Time dilation stats.
+ .["time_dilation_current"] = SStime_track.time_dilation_current
+ .["time_dilation_avg"] = SStime_track.time_dilation_avg
+ .["time_dilation_avg_slow"] = SStime_track.time_dilation_avg_slow
+ .["time_dilation_avg_fast"] = SStime_track.time_dilation_avg_fast
+
+ if(SSshuttle && SSshuttle.emergency)
+ .["shuttle_mode"] = SSshuttle.emergency.mode
+ // Shuttle status, see /__DEFINES/stat.dm
+ .["shuttle_timer"] = SSshuttle.emergency.timeLeft()
+ // Shuttle timer, in seconds
+
+ if(!key_valid)
+ GLOB.topic_status_cache = .
diff --git a/code/game/world.dm b/code/game/world.dm
index 691a60ce18..e68372d768 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -115,6 +115,7 @@ GLOBAL_LIST(topic_status_cache)
GLOB.world_runtime_log = "[GLOB.log_directory]/runtime.log"
GLOB.query_debug_log = "[GLOB.log_directory]/query_debug.log"
GLOB.world_job_debug_log = "[GLOB.log_directory]/job_debug.log"
+ GLOB.subsystem_log = "[GLOB.log_directory]/subsystem.log"
#ifdef UNIT_TESTS
GLOB.test_log = file("[GLOB.log_directory]/tests.log")
@@ -129,6 +130,7 @@ GLOBAL_LIST(topic_status_cache)
start_log(GLOB.world_qdel_log)
start_log(GLOB.world_runtime_log)
start_log(GLOB.world_job_debug_log)
+ start_log(GLOB.subsystem_log)
GLOB.changelog_hash = md5('html/changelog.html') //for telling if the changelog has changed recently
if(fexists(GLOB.config_error_log))
@@ -146,6 +148,14 @@ GLOBAL_LIST(topic_status_cache)
/world/Topic(T, addr, master, key)
TGS_TOPIC //redirect to server tools if necessary
+ if(!SSfail2topic)
+ return "Server not initialized."
+ else if(SSfail2topic.IsRateLimited(addr))
+ return "Rate limited."
+
+ if(length(T) > CONFIG_GET(number/topic_max_size))
+ return "Payload too large!"
+
var/static/list/topic_handlers = TopicHandlers()
var/list/input = params2list(T)
@@ -162,7 +172,7 @@ GLOBAL_LIST(topic_status_cache)
return
handler = new handler()
- return handler.TryRun(input)
+ return handler.TryRun(input, addr)
/world/proc/AnnouncePR(announcement, list/payload)
var/static/list/PRcounts = list() //PR id -> number of times announced this round
diff --git a/code/modules/surgery/bodyparts/bodyparts.dm b/code/modules/surgery/bodyparts/bodyparts.dm
index a1b74942e0..63a392bf06 100644
--- a/code/modules/surgery/bodyparts/bodyparts.dm
+++ b/code/modules/surgery/bodyparts/bodyparts.dm
@@ -172,9 +172,8 @@
var/total_damage = brute + burn
if(total_damage > can_inflict)
- var/excess = total_damage - can_inflict
- brute = round(brute * (excess / total_damage),DAMAGE_PRECISION)
- burn = round(burn * (excess / total_damage),DAMAGE_PRECISION)
+ brute = round(brute * (max_damage / total_damage),DAMAGE_PRECISION)
+ burn = round(burn * (max_damage / total_damage),DAMAGE_PRECISION)
brute_dam += brute
burn_dam += burn
diff --git a/code/modules/tgs/v3210/commands.dm b/code/modules/tgs/v3210/commands.dm
index 71d7e32366..e674fd4e78 100644
--- a/code/modules/tgs/v3210/commands.dm
+++ b/code/modules/tgs/v3210/commands.dm
@@ -19,7 +19,7 @@
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!")
@@ -55,24 +55,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,
+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
+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
+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/config/config.txt b/config/config.txt
index e71c2587b7..fafb3e5791 100644
--- a/config/config.txt
+++ b/config/config.txt
@@ -473,3 +473,15 @@ DISABLE_HIGH_POP_MC_MODE_AMOUNT 60
## For reference, Goonstation uses a resolution of 21x15 for it's widescreen mode.
## Do note that changing this value will affect the title screen. The title screen will have to be updated manually if this is changed.
DEFAULT_VIEW 21x15
+
+### FAIL2TOPIC:
+### Automated IP bans for world/Topic() spammers
+## Enabled
+FAIL2TOPIC_ENABLED
+## Minimum wait time in deciseconds between valid requests
+FAIL2TOPIC_RATE_LIMIT 10
+## Number of requests after breaching rate limit that triggers a ban
+FAIL2TOPIC_MAX_FAILS 5
+## Firewall rule name used on physical server
+FAIL2TOPIC_RULE_NAME _dd_fail2topic
+
diff --git a/html/changelogs/AutoChangeLog-pr-10473.yml b/html/changelogs/AutoChangeLog-pr-10473.yml
new file mode 100644
index 0000000000..f1821a3cce
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10473.yml
@@ -0,0 +1,4 @@
+author: "Bhijn"
+delete-after: True
+changes:
+ - bugfix: "server_hop can no longer be used to remotely lobotomize a spaceman"
diff --git a/html/changelogs/AutoChangeLog-pr-10483.yml b/html/changelogs/AutoChangeLog-pr-10483.yml
new file mode 100644
index 0000000000..1f016b1591
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10483.yml
@@ -0,0 +1,4 @@
+author: "Putnam3145"
+delete-after: True
+changes:
+ - bugfix: "Limb damage works now"
diff --git a/tgstation.dme b/tgstation.dme
index a167c130b0..f52892fce3 100755
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -229,6 +229,7 @@
#include "code\controllers\configuration\entries\dbconfig.dm"
#include "code\controllers\configuration\entries\donator.dm"
#include "code\controllers\configuration\entries\dynamic.dm"
+#include "code\controllers\configuration\entries\fail2topic.dm"
#include "code\controllers\configuration\entries\game_options.dm"
#include "code\controllers\configuration\entries\general.dm"
#include "code\controllers\subsystem\acid.dm"
@@ -245,6 +246,7 @@
#include "code\controllers\subsystem\dcs.dm"
#include "code\controllers\subsystem\disease.dm"
#include "code\controllers\subsystem\events.dm"
+#include "code\controllers\subsystem\fail2topic.dm"
#include "code\controllers\subsystem\fire_burning.dm"
#include "code\controllers\subsystem\garbage.dm"
#include "code\controllers\subsystem\icon_smooth.dm"
@@ -470,8 +472,8 @@
#include "code\datums\elements\_element.dm"
#include "code\datums\elements\cleaning.dm"
#include "code\datums\elements\earhealing.dm"
-#include "code\datums\elements\wuv.dm"
#include "code\datums\elements\ghost_role_eligibility.dm"
+#include "code\datums\elements\wuv.dm"
#include "code\datums\helper_datums\events.dm"
#include "code\datums\helper_datums\getrev.dm"
#include "code\datums\helper_datums\icon_snapshot.dm"