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/__HELPERS/areas.dm b/code/__HELPERS/areas.dm
index 1f5b82f7bf..4b52187e13 100644
--- a/code/__HELPERS/areas.dm
+++ b/code/__HELPERS/areas.dm
@@ -43,6 +43,13 @@
var/static/blacklisted_areas = typecacheof(list(
/area/space,
))
+
+ if(creator)
+ if(creator.create_area_cooldown >= world.time)
+ to_chat(creator, "You're trying to create a new area a little too fast.")
+ return
+ creator.create_area_cooldown = world.time + 10
+
var/list/turfs = detect_room(get_turf(creator), area_or_turf_fail_types)
if(!turfs)
to_chat(creator, "The new area must be completely airtight and not a part of a shuttle.")
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/configuration.dm b/code/controllers/configuration/configuration.dm
index 14954524fa..730a3f17f4 100644
--- a/code/controllers/configuration/configuration.dm
+++ b/code/controllers/configuration/configuration.dm
@@ -236,7 +236,6 @@
if(!(M.config_tag in modes)) // ensure each mode is added only once
modes += M.config_tag
mode_names[M.config_tag] = M.name
- probabilities[M.config_tag] = M.probability
mode_reports[M.config_tag] = M.generate_report()
if(probabilities[M.config_tag]>0)
mode_false_report_weight[M.config_tag] = M.false_report_weight
@@ -342,6 +341,9 @@
if(probabilities[M.config_tag]<=0)
qdel(M)
continue
+ if(M.config_tag in SSvote.stored_modetier_results && SSvote.stored_modetier_results[M.config_tag] < Get(/datum/config_entry/number/dropped_modes))
+ qdel(M)
+ continue
if(min_pop[M.config_tag])
M.required_players = min_pop[M.config_tag]
if(max_pop[M.config_tag])
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/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index b95ef43d37..eea5789a0e 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -394,3 +394,9 @@
/datum/config_entry/flag/allow_clockwork_marauder_on_station
config_entry_value = TRUE
+
+/datum/config_entry/flag/modetier_voting
+ config_entry_value = TRUE
+
+/datum/config_entry/number/dropped_modes
+ config_entry_value = 3
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/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 45c8e3c8fe..0948e428ff 100755
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -221,6 +221,12 @@ SUBSYSTEM_DEF(ticker)
var/init_start = world.timeofday
//Create and announce mode
var/list/datum/game_mode/runnable_modes
+ if(SSvote.mode && (SSvote.mode == "roundtype" || SSvote.mode == "dynamic" || SSvote.mode == "mode tiers"))
+ SSvote.result()
+ SSpersistence.SaveSavedVotes()
+ for(var/client/C in SSvote.voting)
+ C << browse(null, "window=vote;can_close=0")
+ SSvote.reset()
if(GLOB.master_mode == "random" || GLOB.master_mode == "secret")
runnable_modes = config.get_runnable_modes()
@@ -484,9 +490,10 @@ SUBSYSTEM_DEF(ticker)
SSticker.modevoted = TRUE
var/dynamic = CONFIG_GET(flag/dynamic_voting)
if(dynamic)
- SSvote.initiate_vote("dynamic","server",hideresults=TRUE,votesystem=SCORE_VOTING,forced=TRUE,vote_time = 2 MINUTES)
+ SSvote.initiate_vote("dynamic","server",hideresults=TRUE,votesystem=SCORE_VOTING,forced=TRUE,vote_time = 20 MINUTES)
else
- SSvote.initiate_vote("roundtype","server",hideresults=TRUE,votesystem=PLURALITY_VOTING,forced=TRUE, vote_time = 2 MINUTES)
+ SSvote.initiate_vote("roundtype","server",hideresults=TRUE,votesystem=PLURALITY_VOTING,forced=TRUE, \
+ vote_time = (CONFIG_GET(flag/modetier_voting) ? 1 MINUTES : 20 MINUTES))
/datum/controller/subsystem/ticker/Recover()
current_state = SSticker.current_state
diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm
index 573c89a5af..f82954276e 100644
--- a/code/controllers/subsystem/vote.dm
+++ b/code/controllers/subsystem/vote.dm
@@ -15,6 +15,7 @@ SUBSYSTEM_DEF(vote)
var/vote_system = PLURALITY_VOTING
var/question = null
var/list/choices = list()
+ var/list/scores = list()
var/list/choice_descs = list() // optional descriptions
var/list/voted = list()
var/list/voting = list()
@@ -26,6 +27,8 @@ SUBSYSTEM_DEF(vote)
var/list/stored_gamemode_votes = list() //Basically the last voted gamemode is stored here for end-of-round use.
+ var/list/stored_modetier_results = list() // The aggregated tier list of the modes available in secret.
+
/datum/controller/subsystem/vote/fire() //called by master_controller
if(mode)
if(end_time < world.time)
@@ -33,7 +36,8 @@ SUBSYSTEM_DEF(vote)
SSpersistence.SaveSavedVotes()
for(var/client/C in voting)
C << browse(null, "window=vote;can_close=0")
- reset()
+ if(end_time < world.time) // result() can change this
+ reset()
else if(next_pop < world.time)
var/datum/browser/client_popup
for(var/client/C in voting)
@@ -54,6 +58,7 @@ SUBSYSTEM_DEF(vote)
choice_descs.Cut()
voted.Cut()
voting.Cut()
+ scores.Cut()
obfuscated = FALSE //CIT CHANGE - obfuscated votes
remove_action_buttons()
@@ -116,10 +121,8 @@ SUBSYSTEM_DEF(vote)
var/opposite_pref = d[j][i]
if(pref_number>opposite_pref)
p[i][j] = d[i][j]
- p[j][i] = 0
else
p[i][j] = 0
- p[j][i] = d[i][j]
for(var/i in 1 to choices.len)
for(var/j in 1 to choices.len)
if(i != j)
@@ -180,6 +183,22 @@ SUBSYSTEM_DEF(vote)
score.Cut(median_pos,median_pos+1)
choices[score_name]++
+/datum/controller/subsystem/vote/proc/calculate_scores(var/blackbox_text)
+ var/list/scores_by_choice = list()
+ for(var/choice in choices)
+ scores_by_choice[choice] = list()
+ for(var/ckey in voted)
+ var/list/this_vote = voted[ckey]
+ for(var/choice in this_vote)
+ sorted_insert(scores_by_choice[choice],this_vote[choice],/proc/cmp_numeric_asc)
+ var/middle_score = round(GLOB.vote_score_options.len/2,1)
+ for(var/score_name in scores_by_choice)
+ var/list/score = scores_by_choice[score_name]
+ for(var/S in score)
+ scores[score_name] += S-middle_score
+ SSblackbox.record_feedback("nested tally","voting",scores[score_name],list(blackbox_text,"Total scores",score_name))
+
+
/datum/controller/subsystem/vote/proc/announce_result()
var/vote_title_text
var/text
@@ -234,6 +253,8 @@ SUBSYSTEM_DEF(vote)
var/admintext = "Obfuscated results"
if(vote_system == RANKED_CHOICE_VOTING)
admintext += "\nIt should be noted that this is not a raw tally of votes (impossible in ranked choice) but the score determined by the schulze method of voting, so the numbers will look weird!"
+ else if(vote_system == SCORE_VOTING)
+ admintext += "\nIt should be noted that this is not a raw tally of votes but the number of runoffs done by majority judgement!"
for(var/i=1,i<=choices.len,i++)
var/votes = choices[choices[i]]
admintext += "\n[choices[i]]: [votes]"
@@ -252,6 +273,12 @@ SUBSYSTEM_DEF(vote)
SSticker.save_mode(.)
message_admins("The gamemode has been voted for, and has been changed to: [GLOB.master_mode]")
log_admin("Gamemode has been voted for and switched to: [GLOB.master_mode].")
+ if(CONFIG_GET(flag/modetier_voting))
+ reset()
+ started_time = 0
+ initiate_vote("mode tiers","server",hideresults=FALSE,votesystem=RANKED_CHOICE_VOTING,forced=TRUE, vote_time = 30 MINUTES)
+ to_chat(world,"The vote will end right as the round starts.")
+ return .
if("restart")
if(. == "Restart Round")
restart = 1
@@ -262,6 +289,8 @@ SUBSYSTEM_DEF(vote)
restart = 1
else
GLOB.master_mode = .
+ if("mode tiers")
+ stored_modetier_results = choices.Copy()
if("dynamic")
if(SSticker.current_state > GAME_STATE_PREGAME)//Don't change the mode if the round already started.
return message_admins("A vote has tried to change the gamemode, but the game has already started. Aborting.")
@@ -382,6 +411,13 @@ SUBSYSTEM_DEF(vote)
choices |= M
if("roundtype") //CIT CHANGE - adds the roundstart secret/extended vote
choices.Add("secret", "extended")
+ if("mode tiers")
+ var/list/modes_to_add = config.votable_modes
+ var/list/probabilities = CONFIG_GET(keyed_list/probability)
+ for(var/tag in modes_to_add)
+ if(probabilities[tag] <= 0)
+ modes_to_add -= tag
+ choices.Add(modes_to_add)
if("dynamic")
for(var/T in config.storyteller_cache)
var/datum/dynamic_storyteller/S = T
diff --git a/code/datums/status_effects/debuffs.dm b/code/datums/status_effects/debuffs.dm
index f5f012e7f9..c3f74ad44e 100644
--- a/code/datums/status_effects/debuffs.dm
+++ b/code/datums/status_effects/debuffs.dm
@@ -99,6 +99,12 @@
id = "Mesmerize"
alert_type = /obj/screen/alert/status_effect/mesmerized
+/datum/status_effect/no_combat_mode/mesmerize/on_apply()
+ ADD_TRAIT(owner, TRAIT_MUTE, "mesmerize")
+
+/datum/status_effect/no_combat_mode/mesmerize/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_MUTE, "mesmerize")
+
/obj/screen/alert/status_effect/mesmerized
name = "Mesmerized"
desc = "You cant tear your sight from who is in front of you...Their gaze is simply too enthralling.."
diff --git a/code/datums/traits/neutral.dm b/code/datums/traits/neutral.dm
index e712a38df1..8be3174f50 100644
--- a/code/datums/traits/neutral.dm
+++ b/code/datums/traits/neutral.dm
@@ -122,6 +122,15 @@
gain_text = "You feel like exposing yourself to the world."
lose_text = "Indecent exposure doesn't sound as charming to you anymore."
+/datum/quirk/coldblooded
+ name = "Cold-blooded"
+ desc = "Your body doesn't create its own internal heat, requiring external heat regulation."
+ value = 0
+ medical_record_text = "Patient is ectothermic."
+ mob_trait = TRAIT_COLDBLOODED
+ gain_text = "You feel cold-blooded."
+ lose_text = "You feel more warm-blooded."
+
/datum/quirk/alcohol_intolerance
name = "Alcohol Intolerance"
desc = "You take toxin damage from alcohol rather than getting drunk."
diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm
index 0c43d33a4b..e01b525e02 100644
--- a/code/datums/world_topic.dm
+++ b/code/datums/world_topic.dm
@@ -25,16 +25,16 @@
var/key_valid
var/require_comms_key = FALSE
-/datum/world_topic/proc/TryRun(list/input)
+/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)
+ . = Run(input, addr)
if(islist(.))
. = list2params(.)
-/datum/world_topic/proc/Run(list/input)
+/datum/world_topic/proc/Run(list/input, addr)
CRASH("Run() not implemented for [type]!")
// TOPICS
@@ -43,7 +43,7 @@
keyword = "ping"
log = FALSE
-/datum/world_topic/ping/Run(list/input)
+/datum/world_topic/ping/Run(list/input, addr)
. = 0
for (var/client/C in GLOB.clients)
++.
@@ -52,7 +52,7 @@
keyword = "playing"
log = FALSE
-/datum/world_topic/playing/Run(list/input)
+/datum/world_topic/playing/Run(list/input, addr)
return GLOB.player_list.len
/datum/world_topic/pr_announce
@@ -60,7 +60,7 @@
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)
+/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])
@@ -78,14 +78,14 @@
keyword = "Ahelp"
require_comms_key = TRUE
-/datum/world_topic/ahelp_relay/Run(list/input)
+/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)
+/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()
@@ -94,17 +94,17 @@
keyword = "News_Report"
require_comms_key = TRUE
-/datum/world_topic/news_report/Run(list/input)
+/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)
+/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)
+ if(O.client?.address == addr)
new /obj/screen/splash(O.client, TRUE)
break
@@ -112,14 +112,14 @@
keyword = "adminmsg"
require_comms_key = TRUE
-/datum/world_topic/adminmsg/Run(list/input)
+/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)
+/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
@@ -131,13 +131,13 @@
keyword = "adminwho"
require_comms_key = TRUE
-/datum/world_topic/adminwho/Run(list/input)
+/datum/world_topic/adminwho/Run(list/input, addr)
return ircadminwho()
/datum/world_topic/status
keyword = "status"
-/datum/world_topic/status/Run(list/input)
+/datum/world_topic/status/Run(list/input, addr)
. = list()
.["version"] = GLOB.game_version
.["mode"] = "hidden" //CIT CHANGE - hides the gamemode in topic() calls to prevent meta'ing the gamemode
diff --git a/code/game/gamemodes/bloodsucker/bloodsucker.dm b/code/game/gamemodes/bloodsucker/bloodsucker.dm
index e784fd836d..ca2ebfe00d 100644
--- a/code/game/gamemodes/bloodsucker/bloodsucker.dm
+++ b/code/game/gamemodes/bloodsucker/bloodsucker.dm
@@ -9,6 +9,8 @@
var/list/vassal_allowed_antags = list(/datum/antagonist/brother, /datum/antagonist/traitor, /datum/antagonist/traitor/internal_affairs, /datum/antagonist/survivalist, \
/datum/antagonist/rev, /datum/antagonist/nukeop, /datum/antagonist/pirate, /datum/antagonist/cult, /datum/antagonist/abductee)
// The antags you're allowed to be if turning Vassal.
+/proc/isvamp(mob/living/M)
+ return istype(M) && M.mind && M.mind.has_antag_datum(/datum/antagonist/bloodsucker)
/datum/game_mode/bloodsucker
name = "bloodsucker"
diff --git a/code/game/machinery/cloning.dm b/code/game/machinery/cloning.dm
index e683d67be2..97b260f3bf 100644
--- a/code/game/machinery/cloning.dm
+++ b/code/game/machinery/cloning.dm
@@ -155,6 +155,8 @@
mess = TRUE
update_icon()
return FALSE
+ if(isvamp(clonemind)) //If the mind is a bloodsucker
+ return FALSE
attempting = TRUE //One at a time!!
countdown.start()
diff --git a/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm b/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm
index aad68b2166..645d1e5d7a 100644
--- a/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm
+++ b/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm
@@ -22,7 +22,9 @@
if(opened || move_delay || user.stat || user.IsStun() || user.IsKnockdown() || user.IsUnconscious() || !isturf(loc) || !has_gravity(loc))
return
move_delay = TRUE
- if(step(src, direction))
+ var/oldloc = loc
+ step(src, direction)
+ if(oldloc != loc)
addtimer(CALLBACK(src, .proc/ResetMoveDelay), (use_mob_movespeed ? user.movement_delay() : CONFIG_GET(number/movedelay/walk_delay)) * move_speed_multiplier)
else
ResetMoveDelay()
diff --git a/code/game/objects/structures/lattice.dm b/code/game/objects/structures/lattice.dm
index a7eaea0d16..c41d6d32af 100644
--- a/code/game/objects/structures/lattice.dm
+++ b/code/game/objects/structures/lattice.dm
@@ -18,7 +18,7 @@
// flags = CONDUCT_1
/obj/structure/lattice/examine(mob/user)
- ..()
+ . = ..()
. += deconstruction_hints(user)
/obj/structure/lattice/proc/deconstruction_hints(mob/user)
diff --git a/code/game/turfs/simulated/floor.dm b/code/game/turfs/simulated/floor.dm
index 3f46994da9..f9153ff8bf 100644
--- a/code/game/turfs/simulated/floor.dm
+++ b/code/game/turfs/simulated/floor.dm
@@ -177,7 +177,7 @@
I.play_tool_sound(src, 80)
return remove_tile(user, silent)
-/turf/open/floor/proc/remove_tile(mob/user, silent = FALSE, make_tile = TRUE)
+/turf/open/floor/proc/remove_tile(mob/user, silent = FALSE, make_tile = TRUE, forced = FALSE)
if(broken || burnt)
broken = 0
burnt = 0
@@ -191,24 +191,20 @@
return make_plating()
/turf/open/floor/singularity_pull(S, current_size)
- ..()
- if(current_size == STAGE_THREE)
- if(prob(30))
+ . = ..()
+ switch(current_size)
+ if(STAGE_THREE)
+ if(floor_tile && prob(30))
+ remove_tile()
+ if(STAGE_FOUR)
+ if(floor_tile && prob(50))
+ remove_tile()
+ if(STAGE_FIVE to INFINITY)
if(floor_tile)
- new floor_tile(src)
- make_plating()
- else if(current_size == STAGE_FOUR)
- if(prob(50))
- if(floor_tile)
- new floor_tile(src)
- make_plating()
- else if(current_size >= STAGE_FIVE)
- if(floor_tile)
- if(prob(70))
- new floor_tile(src)
- make_plating()
- else if(prob(50))
- ReplaceWithLattice()
+ if(prob(70))
+ remove_tile()
+ else if(prob(50))
+ ReplaceWithLattice()
/turf/open/floor/narsie_act(force, ignore_mobs, probability = 20)
. = ..()
diff --git a/code/game/turfs/simulated/floor/fancy_floor.dm b/code/game/turfs/simulated/floor/fancy_floor.dm
index 82f1a88253..f38a8a3d9b 100644
--- a/code/game/turfs/simulated/floor/fancy_floor.dm
+++ b/code/game/turfs/simulated/floor/fancy_floor.dm
@@ -44,7 +44,7 @@
C.play_tool_sound(src, 80)
return remove_tile(user, silent, (C.tool_behaviour == TOOL_SCREWDRIVER))
-/turf/open/floor/wood/remove_tile(mob/user, silent = FALSE, make_tile = TRUE)
+/turf/open/floor/wood/remove_tile(mob/user, silent = FALSE, make_tile = TRUE, forced = FALSE)
if(broken || burnt)
broken = 0
burnt = 0
diff --git a/code/game/turfs/simulated/floor/reinf_floor.dm b/code/game/turfs/simulated/floor/reinf_floor.dm
index 28ffbbd1ff..7816341a0a 100644
--- a/code/game/turfs/simulated/floor/reinf_floor.dm
+++ b/code/game/turfs/simulated/floor/reinf_floor.dm
@@ -76,14 +76,16 @@
/turf/open/floor/engine/singularity_pull(S, current_size)
..()
- if(current_size >= STAGE_FIVE)
+ if(current_size >= STAGE_FIVE && prob(30))
if(floor_tile)
- if(prob(30))
- new floor_tile(src)
- make_plating()
- else if(prob(30))
+ remove_tile(forced = TRUE)
+ else
ReplaceWithLattice()
+/turf/open/floor/engine/remove_tile(mob/user, silent = FALSE, make_tile = TRUE, forced = FALSE)
+ if(forced)
+ return ..()
+
/turf/open/floor/engine/attack_paw(mob/user)
return attack_hand(user)
diff --git a/code/game/world.dm b/code/game/world.dm
index 4043f15f6f..034ecddfa2 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -112,6 +112,7 @@ GLOBAL_VAR(restart_counter)
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")
@@ -126,6 +127,7 @@ GLOBAL_VAR(restart_counter)
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))
@@ -143,6 +145,14 @@ GLOBAL_VAR(restart_counter)
/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)
@@ -159,7 +169,7 @@ GLOBAL_VAR(restart_counter)
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/antagonists/bloodsucker/bloodsucker_life.dm b/code/modules/antagonists/bloodsucker/bloodsucker_life.dm
index 9174692b49..bbb8aeca30 100644
--- a/code/modules/antagonists/bloodsucker/bloodsucker_life.dm
+++ b/code/modules/antagonists/bloodsucker/bloodsucker_life.dm
@@ -84,8 +84,8 @@
//It is called from your coffin on close (by you only)
if(poweron_masquerade == TRUE || owner.current.AmStaked())
return FALSE
- owner.current.adjustStaminaLoss(-2 + (regenRate * -10) * mult, 0) // Humans lose stamina damage really quickly. Vamps should heal more.
- owner.current.adjustCloneLoss(-1 * (regenRate * 4) * mult, 0)
+ owner.current.adjustStaminaLoss(-2 + (regenRate * 8) * mult, 0) // Humans lose stamina damage really quickly. Vamps should heal more.
+ owner.current.adjustCloneLoss(-0.1 * (regenRate * 2) * mult, 0)
owner.current.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1 * (regenRate * 4) * mult) //adjustBrainLoss(-1 * (regenRate * 4) * mult, 0)
// No Bleeding
if(ishuman(owner.current)) //NOTE Current bleeding is horrible, not to count the amount of blood ballistics delete.
@@ -97,7 +97,7 @@
var/fireheal = 0 // BURN: Heal in Coffin while Fakedeath, or when damage above maxhealth (you can never fully heal fire)
var/amInCoffinWhileTorpor = istype(C.loc, /obj/structure/closet/crate/coffin) && (mult == 0 || HAS_TRAIT(C, TRAIT_DEATHCOMA)) // Check for mult 0 OR death coma. (mult 0 means we're testing from coffin)
if(amInCoffinWhileTorpor)
- mult *= 5 // Increase multiplier if we're sleeping in a coffin.
+ mult *= 4 // Increase multiplier if we're sleeping in a coffin.
fireheal = min(C.getFireLoss_nonProsthetic(), regenRate) // NOTE: Burn damage ONLY heals in torpor.
costMult = 0.25
C.ExtinguishMob()
@@ -118,6 +118,8 @@
if(bruteheal + fireheal + toxinheal > 0) // Just a check? Don't heal/spend, and return.
if(mult == 0)
return TRUE
+ if(owner.current.stat >= UNCONSCIOUS) //Faster regeneration while unconcious, so you dont have to wait all day
+ mult *= 2
// We have damage. Let's heal (one time)
C.adjustBruteLoss(-bruteheal * mult, forced = TRUE)// Heal BRUTE / BURN in random portions throughout the body.
C.adjustFireLoss(-fireheal * mult, forced = TRUE)
@@ -187,19 +189,19 @@
/datum/antagonist/bloodsucker/proc/HandleDeath()
// FINAL DEATH
// Fire Damage? (above double health)
- if (owner.current.getFireLoss_nonProsthetic() >= owner.current.getMaxHealth() * 2)
+ if(owner.current.getFireLoss_nonProsthetic() >= owner.current.getMaxHealth() * 1.5)
FinalDeath()
return
// Staked while "Temp Death" or Asleep
- if (owner.current.StakeCanKillMe() && owner.current.AmStaked())
+ if(owner.current.StakeCanKillMe() && owner.current.AmStaked())
FinalDeath()
return
// Not "Alive"?
- if (!owner.current || !isliving(owner.current) || isbrain(owner.current) || !get_turf(owner.current))
+ if(!owner.current || !isliving(owner.current) || isbrain(owner.current) || !get_turf(owner.current))
FinalDeath()
return
// Missing Brain or Heart?
- if (!owner.current.HaveBloodsuckerBodyparts())
+ if(!owner.current.HaveBloodsuckerBodyparts())
FinalDeath()
return
// Disable Powers: Masquerade * NOTE * This should happen as a FLAW!
@@ -212,21 +214,21 @@
var/total_toxloss = owner.current.getToxLoss() //This is neater than just putting it in total_damage
var/total_damage = total_brute + total_burn + total_toxloss
// Died? Convert to Torpor (fake death)
- if (owner.current.stat >= DEAD)
+ if(owner.current.stat >= DEAD)
Torpor_Begin()
to_chat(owner, "Your immortal body will not yet relinquish your soul to the abyss. You enter Torpor.")
+ sleep(30) //To avoid spam
if (poweron_masquerade == TRUE)
to_chat(owner, "Your wounds will not heal until you disable the Masquerade power.")
// End Torpor:
else // No damage, OR toxin healed AND brute healed and NOT in coffin (since you cannot heal burn)
- if (total_damage <= 0 || total_toxloss <= 0 && total_brute <= 0 && !istype(owner.current.loc, /obj/structure/closet/crate/coffin))
+ if(total_damage <= 0 || total_toxloss <= 0 && total_brute <= 0 && !istype(owner.current.loc, /obj/structure/closet/crate/coffin))
// Not Daytime, Not in Torpor
- if (!SSticker.mode.is_daylight() && HAS_TRAIT_FROM(owner.current, TRAIT_DEATHCOMA, "bloodsucker"))
+ if(!SSticker.mode.is_daylight() && HAS_TRAIT_FROM(owner.current, TRAIT_DEATHCOMA, "bloodsucker"))
Torpor_End()
// Fake Unconscious
- if (poweron_masquerade == TRUE && total_damage >= owner.current.getMaxHealth() - HEALTH_THRESHOLD_FULLCRIT)
+ if(poweron_masquerade == TRUE && total_damage >= owner.current.getMaxHealth() - HEALTH_THRESHOLD_FULLCRIT)
owner.current.Unconscious(20,1)
-
//HEALTH_THRESHOLD_CRIT 0
//HEALTH_THRESHOLD_FULLCRIT -30
//HEALTH_THRESHOLD_DEAD -100
@@ -241,8 +243,8 @@
owner.current.update_sight()
owner.current.reload_fullscreen()
// Disable ALL Powers
- for (var/datum/action/bloodsucker/power in powers)
- if (power.active && !power.can_use_in_torpor)
+ for(var/datum/action/bloodsucker/power in powers)
+ if(power.active && !power.can_use_in_torpor)
power.DeactivatePower()
@@ -281,7 +283,7 @@
// Free my Vassals!
FreeAllVassals()
// Elders get Dusted
- if (vamplevel >= 4) // (vamptitle)
+ if(vamplevel >= 4) // (vamptitle)
owner.current.visible_message("[owner.current]'s skin crackles and dries, their skin and bones withering to dust. A hollow cry whips from what is now a sandy pile of remains.", \
"Your soul escapes your withering body as the abyss welcomes you to your Final Death.", \
"You hear a dry, crackling sound.")
@@ -306,7 +308,7 @@
if (!isliving(src))
return
var/mob/living/L = src
- if (!L.AmBloodsucker())
+ if(!L.AmBloodsucker())
return
// We're a vamp? Try to eat food...
var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
@@ -315,7 +317,7 @@
/datum/antagonist/bloodsucker/proc/handle_eat_human_food(var/food_nutrition) // Called from snacks.dm and drinks.dm
set waitfor = FALSE
- if (!owner.current || !iscarbon(owner.current))
+ if(!owner.current || !iscarbon(owner.current))
return
var/mob/living/carbon/C = owner.current
// Remove Nutrition, Give Bad Food
diff --git a/code/modules/antagonists/bloodsucker/datum_bloodsucker.dm b/code/modules/antagonists/bloodsucker/datum_bloodsucker.dm
index 97b4437298..844b523135 100644
--- a/code/modules/antagonists/bloodsucker/datum_bloodsucker.dm
+++ b/code/modules/antagonists/bloodsucker/datum_bloodsucker.dm
@@ -35,8 +35,8 @@
var/warn_sun_burn = FALSE // So we only get the sun burn message once per day.
var/had_toxlover = FALSE
// LISTS
- var/static/list/defaultTraits = list (TRAIT_STABLEHEART, TRAIT_NOBREATH, TRAIT_SLEEPIMMUNE, TRAIT_NOCRITDAMAGE, TRAIT_RESISTCOLD, TRAIT_RADIMMUNE, TRAIT_VIRUSIMMUNE, TRAIT_NIGHT_VISION, \
- TRAIT_NOSOFTCRIT, TRAIT_NOHARDCRIT, TRAIT_AGEUSIA, TRAIT_COLDBLOODED, TRAIT_NONATURALHEAL, TRAIT_NOMARROW, TRAIT_NOPULSE, TRAIT_NOCLONE)
+ var/static/list/defaultTraits = list (TRAIT_STABLEHEART, TRAIT_NOBREATH, TRAIT_SLEEPIMMUNE, TRAIT_NOCRITDAMAGE, TRAIT_RESISTCOLD, TRAIT_RADIMMUNE, TRAIT_NIGHT_VISION, \
+ TRAIT_NOSOFTCRIT, TRAIT_NOHARDCRIT, TRAIT_AGEUSIA, TRAIT_COLDBLOODED, TRAIT_NONATURALHEAL, TRAIT_NOMARROW, TRAIT_NOPULSE, TRAIT_VIRUSIMMUNE)
// NOTES: TRAIT_AGEUSIA <-- Doesn't like flavors.
// REMOVED: TRAIT_NODEATH
// TO ADD:
@@ -334,7 +334,7 @@ datum/antagonist/bloodsucker/proc/SpendRank()
// Assign True Reputation
if(vamplevel == 4)
SelectReputation(am_fledgling = FALSE, forced = TRUE)
- to_chat(owner.current, "You are now a rank [vamplevel] Bloodsucker. Your strength, resistence, health, feed rate, regen rate, and maximum blood have all increased!")
+ to_chat(owner.current, "You are now a rank [vamplevel] Bloodsucker. Your strength, health, feed rate, regen rate, and maximum blood have all increased!")
to_chat(owner.current, "Your existing powers have all ranked up as well!")
update_hud(TRUE)
owner.current.playsound_local(null, 'sound/effects/pope_entry.ogg', 25, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head.
diff --git a/code/modules/antagonists/bloodsucker/datum_vassal.dm b/code/modules/antagonists/bloodsucker/datum_vassal.dm
index 71ee0bcc1d..716b7ff223 100644
--- a/code/modules/antagonists/bloodsucker/datum_vassal.dm
+++ b/code/modules/antagonists/bloodsucker/datum_vassal.dm
@@ -56,11 +56,6 @@
var/obj/item/organ/eyes/vassal/E = new
E.Insert(owner.current)
-/obj/item/organ/eyes/vassal/
- lighting_alpha = 180 // LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE <--- This is too low a value at 128. We need to SEE what the darkness is so we can hide in it.
- see_in_dark = 12
- flash_protect = -1 //These eyes are weaker to flashes, but let you see in the dark
-
/datum/antagonist/vassal/proc/remove_thrall_eyes()
var/obj/item/organ/eyes/E = new
E.Insert(owner.current)
diff --git a/code/modules/antagonists/bloodsucker/items/bloodsucker_organs.dm b/code/modules/antagonists/bloodsucker/items/bloodsucker_organs.dm
index 5a905857c5..e4e26fe034 100644
--- a/code/modules/antagonists/bloodsucker/items/bloodsucker_organs.dm
+++ b/code/modules/antagonists/bloodsucker/items/bloodsucker_organs.dm
@@ -51,6 +51,11 @@
return "no" // Bloodsuckers don't have a heartbeat at all when stopped (default is "an unstable")
// EYES //
+/obj/item/organ/eyes/vassal/
+ lighting_alpha = 180 // LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE <--- This is too low a value at 128. We need to SEE what the darkness is so we can hide in it.
+ see_in_dark = 12
+ flash_protect = -1 //These eyes are weaker to flashes, but let you see in the dark
+
/obj/item/organ/eyes/vassal/bloodsucker
flash_protect = 2 //Eye healing isnt working properly
sight_flags = SEE_MOBS // Taken from augmented_eyesight.dm
diff --git a/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm b/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm
index 9db4cae1ff..b7c90523b6 100644
--- a/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm
+++ b/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm
@@ -78,7 +78,7 @@
var/mob/living/carbon/C = target
// Needs to be Down/Slipped in some way to Stake.
if(!C.can_be_staked() || target == user)
- to_chat(user, "You cant stake [target] when they are moving moving about! They have to be laying down!")
+ to_chat(user, "You can't stake [target] when they are moving about! They have to be laying down or grabbed by the neck!")
return
// Oops! Can't.
if(HAS_TRAIT(C, TRAIT_PIERCEIMMUNE))
@@ -113,7 +113,7 @@
// Can this target be staked? If someone stands up before this is complete, it fails. Best used on someone stationary.
/mob/living/carbon/proc/can_be_staked()
//return resting || IsKnockdown() || IsUnconscious() || (stat && (stat != SOFT_CRIT || pulledby)) || (has_trait(TRAIT_FAKEDEATH)) || resting || IsStun() || IsFrozen() || (pulledby && pulledby.grab_state >= GRAB_NECK)
- return (src.resting || src.lying)
+ return (resting || lying || IsUnconscious() || pulledby && pulledby.grab_state >= GRAB_NECK)
// ABOVE: Taken from update_mobility() in living.dm
/obj/item/stake/hardened
diff --git a/code/modules/antagonists/bloodsucker/objects/bloodsucker_crypt.dm b/code/modules/antagonists/bloodsucker/objects/bloodsucker_crypt.dm
index 3493622945..f66ce4a208 100644
--- a/code/modules/antagonists/bloodsucker/objects/bloodsucker_crypt.dm
+++ b/code/modules/antagonists/bloodsucker/objects/bloodsucker_crypt.dm
@@ -130,7 +130,7 @@
/obj/structure/bloodsucker/vassalrack/MouseDrop_T(atom/movable/O, mob/user)
if(!O.Adjacent(src) || O == user || !isliving(O) || !isliving(user) || useLock || has_buckled_mobs() || user.incapacitated())
return
- if(!anchored && user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
+ if(!anchored && isvamp(user))
to_chat(user, "Until this rack is secured in place, it cannot serve its purpose.")
return
// PULL TARGET: Remember if I was pullin this guy, so we can restore this
@@ -183,7 +183,7 @@
/obj/structure/bloodsucker/vassalrack/user_unbuckle_mob(mob/living/M, mob/user)
// Attempt Unbuckle
- if(!user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
+ if(!isvamp(user))
if(M == user)
M.visible_message("[user] tries to release themself from the rack!",\
"You attempt to release yourself from the rack!") // For sound if not seen --> "You hear a squishy wet noise.")
@@ -453,7 +453,7 @@
/obj/structure/bloodsucker/candelabrum/examine(mob/user)
. = ..()
- if((user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) || isobserver(user))
+ if((isvamp()) || isobserver(user))
. += {"This is a magical candle which drains at the sanity of mortals who are not under your command while it is active."}
. += {"You can alt click on it from any range to turn it on remotely, or simply be next to it and click on it to turn it on and off normally."}
/* if(user.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
@@ -461,15 +461,13 @@
You can turn it on and off by clicking on it while you are next to it"} */
/obj/structure/bloodsucker/candelabrum/attack_hand(mob/user)
- var/datum/antagonist/bloodsucker/V = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) //I wish there was a better way to do this
var/datum/antagonist/vassal/T = user.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
- if(istype(V) || istype(T))
+ if(isvamp(user) || istype(T))
toggle()
/obj/structure/bloodsucker/candelabrum/AltClick(mob/user)
- var/datum/antagonist/bloodsucker/V = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
// Bloodsuckers can turn their candles on from a distance. SPOOOOKY.
- if(istype(V))
+ if(isvamp(user))
toggle()
/obj/structure/bloodsucker/candelabrum/proc/toggle(mob/user)
@@ -486,8 +484,7 @@
if(lit)
for(var/mob/living/carbon/human/H in viewers(7, src))
var/datum/antagonist/vassal/T = H.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
- var/datum/antagonist/bloodsucker/V = H.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
- if(V || T) //We dont want vassals or vampires affected by this
+ if(isvamp(H) || T) //We dont want vassals or vampires affected by this
return
H.hallucination = 20
SEND_SIGNAL(H, COMSIG_ADD_MOOD_EVENT, "vampcandle", /datum/mood_event/vampcandle)
diff --git a/code/modules/antagonists/bloodsucker/powers/bs_cloak.dm b/code/modules/antagonists/bloodsucker/powers/bs_cloak.dm
index cea942e26d..9126638fea 100644
--- a/code/modules/antagonists/bloodsucker/powers/bs_cloak.dm
+++ b/code/modules/antagonists/bloodsucker/powers/bs_cloak.dm
@@ -9,8 +9,9 @@
bloodsucker_can_buy = TRUE
amToggle = TRUE
warn_constant_cost = TRUE
+ var/was_running
- var/light_min = 0.5 // If lum is above this, no good.
+ var/light_min = 0.2 // If lum is above this, no good.
/datum/action/bloodsucker/cloak/CheckCanUse(display_error)
. = ..()
@@ -26,18 +27,16 @@
/datum/action/bloodsucker/cloak/ActivatePower()
var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
var/mob/living/user = owner
- var/was_running = (user.m_intent == MOVE_INTENT_RUN)
+ was_running = (user.m_intent == MOVE_INTENT_RUN)
if(was_running)
user.toggle_move_intent()
ADD_TRAIT(user, TRAIT_NORUNNING, "cloak of darkness")
while(bloodsuckerdatum && ContinueActive(user) || user.m_intent == MOVE_INTENT_RUN)
// Pay Blood Toll (if awake)
- owner.alpha = max(0, owner.alpha - min(75, 20 + 15 * level_current))
+ owner.alpha = max(20, owner.alpha - min(75, 10 + 5 * level_current))
bloodsuckerdatum.AddBloodVolume(-0.2)
sleep(5) // Check every few ticks that we haven't disabled this power
// Return to Running (if you were before)
- if(was_running && user.m_intent != MOVE_INTENT_RUN)
- user.toggle_move_intent()
/datum/action/bloodsucker/cloak/ContinueActive(mob/living/user, mob/living/target)
if (!..())
@@ -55,3 +54,5 @@
..()
REMOVE_TRAIT(user, TRAIT_NORUNNING, "cloak of darkness")
user.alpha = 255
+ if(was_running && user.m_intent != MOVE_INTENT_RUN)
+ user.toggle_move_intent()
diff --git a/code/modules/antagonists/bloodsucker/powers/bs_masquerade.dm b/code/modules/antagonists/bloodsucker/powers/bs_masquerade.dm
index 6ee17b3014..0435ddccd5 100644
--- a/code/modules/antagonists/bloodsucker/powers/bs_masquerade.dm
+++ b/code/modules/antagonists/bloodsucker/powers/bs_masquerade.dm
@@ -51,14 +51,17 @@
REMOVE_TRAIT(user, TRAIT_COLDBLOODED, "bloodsucker")
REMOVE_TRAIT(user, TRAIT_NOHARDCRIT, "bloodsucker")
REMOVE_TRAIT(user, TRAIT_NOSOFTCRIT, "bloodsucker")
+ REMOVE_TRAIT(user, TRAIT_VIRUSIMMUNE, "bloodsucker")
var/obj/item/organ/heart/vampheart/H = user.getorganslot(ORGAN_SLOT_HEART)
-
+ var/obj/item/organ/eyes/vassal/bloodsucker/E = user.getorganslot(ORGAN_SLOT_EYES)
+ E.flash_protect = 0
+
// WE ARE ALIVE! //
bloodsuckerdatum.poweron_masquerade = TRUE
while(bloodsuckerdatum && ContinueActive(user))
// HEART
- if (istype(H))
+ if(istype(H))
H.FakeStart()
// PASSIVE (done from LIFE)
@@ -67,7 +70,7 @@
// Don't Heal
// Pay Blood Toll (if awake)
- if (user.stat == CONSCIOUS)
+ if(user.stat == CONSCIOUS)
bloodsuckerdatum.AddBloodVolume(-0.2)
sleep(20) // Check every few ticks that we haven't disabled this power
@@ -89,9 +92,13 @@
ADD_TRAIT(user, TRAIT_COLDBLOODED, "bloodsucker")
ADD_TRAIT(user, TRAIT_NOHARDCRIT, "bloodsucker")
ADD_TRAIT(user, TRAIT_NOSOFTCRIT, "bloodsucker")
+ ADD_TRAIT(user, TRAIT_VIRUSIMMUNE, "bloodsucker")
// HEART
var/obj/item/organ/heart/H = user.getorganslot(ORGAN_SLOT_HEART)
+ var/obj/item/organ/eyes/vassal/bloodsucker/E = user.getorganslot(ORGAN_SLOT_EYES)
H.Stop()
+ E.flash_protect = 2
+
to_chat(user, "Your heart beats one final time, while your skin dries out and your icy pallor returns.")
diff --git a/code/modules/antagonists/bloodsucker/powers/bs_mesmerize.dm b/code/modules/antagonists/bloodsucker/powers/bs_mesmerize.dm
index 94bc0e11d0..7962c7d403 100644
--- a/code/modules/antagonists/bloodsucker/powers/bs_mesmerize.dm
+++ b/code/modules/antagonists/bloodsucker/powers/bs_mesmerize.dm
@@ -89,17 +89,14 @@
if(istype(target))
target.Stun(40) //Utterly useless without this, its okay since there are so many checks to go through
- target.silent = 45 //Shhhh little lamb
target.apply_status_effect(STATUS_EFFECT_MESMERIZE, 45) //So you cant rotate with combat mode, plus fancy status alert
if(do_mob(user, target, 40, 0, TRUE, extra_checks=CALLBACK(src, .proc/ContinueActive, user, target)))
PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN!
var/power_time = 90 + level_current * 12
- target.silent = power_time + 20
- target.apply_status_effect(STATUS_EFFECT_MESMERIZE, 100 + level_current * 15)
+ target.apply_status_effect(STATUS_EFFECT_MESMERIZE, power_time + 80)
to_chat(user, "[target] is fixed in place by your hypnotic gaze.")
target.Stun(power_time)
- //target.silent += power_time / 10 // Silent isn't based on ticks.
target.next_move = world.time + power_time // <--- Use direct change instead. We want an unmodified delay to their next move // target.changeNext_move(power_time) // check click.dm
target.notransform = TRUE // <--- Fuck it. We tried using next_move, but they could STILL resist. We're just doing a hard freeze.
spawn(power_time)
diff --git a/code/modules/antagonists/bloodsucker/powers/bs_trespass.dm b/code/modules/antagonists/bloodsucker/powers/bs_trespass.dm
index c650a6af15..159c7b20b7 100644
--- a/code/modules/antagonists/bloodsucker/powers/bs_trespass.dm
+++ b/code/modules/antagonists/bloodsucker/powers/bs_trespass.dm
@@ -89,13 +89,7 @@
user.invisibility = INVISIBILITY_MAXIMUM
// LOSE CUFFS
- if(user.handcuffed)
- var/obj/O = user.handcuffed
- user.dropItemToGround(O)
- if(user.legcuffed)
- var/obj/O = user.legcuffed
- user.dropItemToGround(O)
-
+
// Wait...
sleep(mist_delay / 2)
diff --git a/code/modules/antagonists/revenant/revenant.dm b/code/modules/antagonists/revenant/revenant.dm
index d7d88a9b6a..b98fa04504 100644
--- a/code/modules/antagonists/revenant/revenant.dm
+++ b/code/modules/antagonists/revenant/revenant.dm
@@ -26,6 +26,7 @@
spacewalk = TRUE
sight = SEE_SELF
throwforce = 0
+ blood_volume = 0
see_in_dark = 8
lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
diff --git a/code/modules/assembly/playback.dm b/code/modules/assembly/playback.dm
new file mode 100644
index 0000000000..088f186adc
--- /dev/null
+++ b/code/modules/assembly/playback.dm
@@ -0,0 +1,50 @@
+/obj/item/assembly/playback
+ name = "playback device"
+ desc = "A small electronic device able to record a voice sample, and repeat that sample when it receive a signal."
+ icon_state = "radio"
+ materials = list(MAT_METAL=500, MAT_GLASS=50)
+ flags_1 = HEAR_1
+ attachable = TRUE
+ verb_say = "beeps"
+ verb_ask = "beeps"
+ verb_exclaim = "beeps"
+ var/listening = FALSE
+ var/recorded = "" //the activation message
+ var/languages
+
+/obj/item/assembly/playback/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, message_mode, atom/movable/source)
+ . = ..()
+ if(speaker == src)
+ return
+
+ if(listening && !radio_freq)
+ record_speech(speaker, raw_message, message_language)
+
+/obj/item/assembly/playback/proc/record_speech(atom/movable/speaker, raw_message, datum/language/message_language)
+ recorded = raw_message
+ listening = FALSE
+ languages = message_language
+ say("Activation message is '[recorded]'.", language = message_language)
+
+/obj/item/assembly/playback/activate()
+ if(recorded == "") // Why say anything when there isn't anything to say
+ return FALSE
+ say("[recorded]", language = languages) // Repeat the message in the language it was said in
+ return TRUE
+
+/obj/item/assembly/playback/proc/record()
+ if(!secured || holder)
+ return FALSE
+ listening = !listening
+ say("[listening ? "Now" : "No longer"] recording input.")
+ return TRUE
+
+/obj/item/assembly/playback/attack_self(mob/user)
+ if(!user)
+ return FALSE
+ record()
+ return TRUE
+
+/obj/item/assembly/playback/toggle_secure()
+ . = ..()
+ listening = FALSE
\ No newline at end of file
diff --git a/code/modules/assembly/voice.dm b/code/modules/assembly/voice.dm
index f72f726988..36a58b2a70 100644
--- a/code/modules/assembly/voice.dm
+++ b/code/modules/assembly/voice.dm
@@ -15,6 +15,7 @@
verb_exclaim = "beeps"
var/listening = FALSE
var/recorded = "" //the activation message
+ var/languages // The Message's language
var/mode = 1
var/static/list/modes = list("inclusive",
"exclusive",
@@ -33,23 +34,25 @@
if(listening && !radio_freq)
record_speech(speaker, raw_message, message_language)
else
- if(check_activation(speaker, raw_message))
- addtimer(CALLBACK(src, .proc/pulse, 0), 10)
+ if(message_language == languages) // If it isn't in the same language as the message, don't try to find the message
+ if(check_activation(speaker, raw_message))
+ addtimer(CALLBACK(src, .proc/pulse, 0), 10)
/obj/item/assembly/voice/proc/record_speech(atom/movable/speaker, raw_message, datum/language/message_language)
+ languages = message_language // Assign the message's language to a variable to use it elsewhere
switch(mode)
if(INCLUSIVE_MODE)
recorded = raw_message
listening = FALSE
- say("Activation message is '[recorded]'.", message_language)
+ say("Activation message is '[recorded]'.", language = languages) // Say the message in the language it was said in
if(EXCLUSIVE_MODE)
recorded = raw_message
listening = FALSE
- say("Activation message is '[recorded]'.", message_language)
+ say("Activation message is '[recorded]'.", language = languages)
if(RECOGNIZER_MODE)
recorded = speaker.GetVoice()
listening = FALSE
- say("Your voice pattern is saved.", message_language)
+ say("Your voice pattern is saved.", language = languages)
if(VOICE_SENSOR_MODE)
if(length(raw_message))
addtimer(CALLBACK(src, .proc/pulse, 0), 10)
diff --git a/code/modules/mining/equipment/regenerative_core.dm b/code/modules/mining/equipment/regenerative_core.dm
index 4429c8426f..1e4244165f 100644
--- a/code/modules/mining/equipment/regenerative_core.dm
+++ b/code/modules/mining/equipment/regenerative_core.dm
@@ -1,144 +1,147 @@
-/*********************Hivelord stabilizer****************/
-/obj/item/hivelordstabilizer
- name = "stabilizing serum"
- icon = 'icons/obj/chemical.dmi'
- icon_state = "bottle19"
- desc = "Inject certain types of monster organs with this stabilizer to preserve their healing powers indefinitely."
- w_class = WEIGHT_CLASS_TINY
-
-/obj/item/hivelordstabilizer/afterattack(obj/item/organ/M, mob/user)
- . = ..()
- var/obj/item/organ/regenerative_core/C = M
- if(!istype(C, /obj/item/organ/regenerative_core))
- to_chat(user, "The stabilizer only works on certain types of monster organs, generally regenerative in nature.")
- return ..()
-
- C.preserved()
- to_chat(user, "You inject the [M] with the stabilizer. It will no longer go inert.")
- qdel(src)
-
-/************************Hivelord core*******************/
-/obj/item/organ/regenerative_core
- name = "regenerative core"
- desc = "All that remains of a hivelord. It can be used to heal completely, but it will rapidly decay into uselessness."
- icon_state = "roro core 2"
- item_flags = NOBLUDGEON
- slot = "hivecore"
- force = 0
- actions_types = list(/datum/action/item_action/organ_action/use)
- var/inert = 0
- var/preserved = 0
-
-/obj/item/organ/regenerative_core/Initialize()
- . = ..()
- addtimer(CALLBACK(src, .proc/inert_check), 2400)
-
-/obj/item/organ/regenerative_core/proc/inert_check()
- if(!preserved)
- go_inert()
-
-/obj/item/organ/regenerative_core/proc/preserved(implanted = 0)
- inert = FALSE
- preserved = TRUE
- update_icon()
- desc = "All that remains of a hivelord. It is preserved, allowing you to use it to heal completely without danger of decay."
- if(implanted)
- SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "implanted"))
- else
- SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "stabilizer"))
-
-/obj/item/organ/regenerative_core/proc/go_inert()
- inert = TRUE
- name = "decayed regenerative core"
- desc = "All that remains of a hivelord. It has decayed, and is completely useless."
- SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "inert"))
- update_icon()
-
-/obj/item/organ/regenerative_core/ui_action_click()
- if(inert)
- to_chat(owner, "[src] breaks down as it tries to activate.")
- else
- owner.revive(full_heal = 1)
- owner.log_message("[owner] used an implanted [src] to heal themselves! Keep fighting, it's just a flesh wound!", LOG_ATTACK, color="green") //Logging for implanted legion core use
- qdel(src)
-
-/obj/item/organ/regenerative_core/on_life()
- ..()
- if(owner.health < owner.crit_threshold)
- ui_action_click()
-
-/obj/item/organ/regenerative_core/afterattack(atom/target, mob/user, proximity_flag)
- . = ..()
- if(proximity_flag && ishuman(target))
- var/mob/living/carbon/human/H = target
- if(inert)
- to_chat(user, "[src] has decayed and can no longer be used to heal.")
- return
- else
- if(H.stat == DEAD)
- to_chat(user, "[src] are useless on the dead.")
- return
- if(H != user)
- H.visible_message("[user] forces [H] to apply [src]... [H.p_they()] quickly regenerate all injuries!")
- SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "other"))
- else
- to_chat(user, "You start to smear [src] on yourself. It feels and smells disgusting, but you feel amazingly refreshed in mere moments.")
- SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "self"))
- H.revive(full_heal = 1)
- qdel(src)
- user.log_message("[user] used [src] to heal [H]! Wake the fuck up, Samurai!", LOG_ATTACK, color="green") //Logging for 'old' style legion core use, when clicking on a sprite of yourself or another.
-
-/obj/item/organ/regenerative_core/attack_self(mob/user) //Knouli's first hack! Allows for the use of the core in hand rather than needing to click on the target, yourself, to selfheal. Its a rip of the proc just above - but skips on distance check and only uses 'user' rather than 'target'
- if(ishuman(user)) //Check if user is human, no need for distance check as it's self heal
- var/mob/living/carbon/human/H = user //Set H to user rather than target
- if(inert) //Inert cores are useless
- to_chat(user, "[src] has decayed and can no longer be used to heal.")
- return
- else //Skip on check if the target to be healed is dead as, if you are dead, you're not going to be able to use it on yourself!
- to_chat(user, "You start to smear [src] on yourself. It feels and smells disgusting, but you feel amazingly refreshed in mere moments.")
- SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "self"))
- H.revive(full_heal = 1)
- qdel(src)
- H.log_message("[H] used [src] to heal themselves! Making use of Knouli's sexy and intelligent use-in-hand proc!", LOG_ATTACK, color="green") //Logging for 'new' style legion core use, when using the core in-hand.
-
-
-/obj/item/organ/regenerative_core/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE)
- . = ..()
- if(!preserved && !inert)
- preserved(TRUE)
- owner.visible_message("[src] stabilizes as it's inserted.")
-
-/obj/item/organ/regenerative_core/Remove(mob/living/carbon/M, special = 0)
- if(!inert && !special)
- owner.visible_message("[src] rapidly decays as it's removed.")
- go_inert()
- return ..()
-
-/obj/item/organ/regenerative_core/prepare_eat()
- return null
-
-/*************************Legion core********************/
-/obj/item/organ/regenerative_core/legion
- desc = "A strange rock that crackles with power. It can be used to heal completely, but it will rapidly decay into uselessness."
- icon_state = "legion_soul"
-
-/obj/item/organ/regenerative_core/legion/Initialize()
- . = ..()
- update_icon()
-
-/obj/item/organ/regenerative_core/update_icon()
- icon_state = inert ? "legion_soul_inert" : "legion_soul"
- cut_overlays()
- if(!inert && !preserved)
- add_overlay("legion_soul_crackle")
- for(var/X in actions)
- var/datum/action/A = X
- A.UpdateButtonIcon()
-
-/obj/item/organ/regenerative_core/legion/go_inert()
- ..()
- desc = "[src] has become inert. It has decayed, and is completely useless."
-
-/obj/item/organ/regenerative_core/legion/preserved(implanted = 0)
- ..()
- desc = "[src] has been stabilized. It is preserved, allowing you to use it to heal completely without danger of decay."
+/*********************Hivelord stabilizer****************/
+/obj/item/hivelordstabilizer
+ name = "stabilizing serum"
+ icon = 'icons/obj/chemical.dmi'
+ icon_state = "bottle19"
+ desc = "Inject certain types of monster organs with this stabilizer to preserve their healing powers indefinitely."
+ w_class = WEIGHT_CLASS_TINY
+
+/obj/item/hivelordstabilizer/afterattack(obj/item/organ/M, mob/user)
+ . = ..()
+ var/obj/item/organ/regenerative_core/C = M
+ if(!istype(C, /obj/item/organ/regenerative_core))
+ to_chat(user, "The stabilizer only works on certain types of monster organs, generally regenerative in nature.")
+ return ..()
+
+ C.preserved()
+ to_chat(user, "You inject the [M] with the stabilizer. It will no longer go inert.")
+ qdel(src)
+
+/************************Hivelord core*******************/
+/obj/item/organ/regenerative_core
+ name = "regenerative core"
+ desc = "All that remains of a hivelord. It can be used to heal completely, but it will rapidly decay into uselessness."
+ icon_state = "roro core 2"
+ item_flags = NOBLUDGEON
+ slot = "hivecore"
+ force = 0
+ actions_types = list(/datum/action/item_action/organ_action/use)
+ var/inert = 0
+ var/preserved = 0
+
+/obj/item/organ/regenerative_core/Initialize()
+ . = ..()
+ addtimer(CALLBACK(src, .proc/inert_check), 2400)
+
+/obj/item/organ/regenerative_core/proc/inert_check()
+ if(!preserved)
+ go_inert()
+
+/obj/item/organ/regenerative_core/proc/preserved(implanted = 0)
+ inert = FALSE
+ preserved = TRUE
+ update_icon()
+ desc = "All that remains of a hivelord. It is preserved, allowing you to use it to heal completely without danger of decay."
+ if(implanted)
+ SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "implanted"))
+ else
+ SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "stabilizer"))
+
+/obj/item/organ/regenerative_core/proc/go_inert()
+ inert = TRUE
+ name = "decayed regenerative core"
+ desc = "All that remains of a hivelord. It has decayed, and is completely useless."
+ SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "inert"))
+ update_icon()
+
+/obj/item/organ/regenerative_core/ui_action_click()
+ if(inert)
+ to_chat(owner, "[src] breaks down as it tries to activate.")
+ else
+ owner.revive(full_heal = 1)
+ owner.log_message("[owner] used an implanted [src] to heal themselves! Keep fighting, it's just a flesh wound!", LOG_ATTACK, color="green") //Logging for implanted legion core use
+ qdel(src)
+
+/obj/item/organ/regenerative_core/on_life()
+ ..()
+ if(owner.health < owner.crit_threshold)
+ ui_action_click()
+
+/obj/item/organ/regenerative_core/afterattack(atom/target, mob/user, proximity_flag)
+ . = ..()
+ if(proximity_flag && ishuman(target))
+ var/mob/living/carbon/human/H = target
+ if(inert)
+ to_chat(user, "[src] has decayed and can no longer be used to heal.")
+ return
+ if(isvamp(user))
+ to_chat(user, "[src] breaks down as it fails to heal your unholy self")
+ return
+ else
+ if(H.stat == DEAD)
+ to_chat(user, "[src] are useless on the dead.")
+ return
+ if(H != user)
+ H.visible_message("[user] forces [H] to apply [src]... [H.p_they()] quickly regenerate all injuries!")
+ SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "other"))
+ else
+ to_chat(user, "You start to smear [src] on yourself. It feels and smells disgusting, but you feel amazingly refreshed in mere moments.")
+ SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "self"))
+ H.revive(full_heal = 1)
+ qdel(src)
+ user.log_message("[user] used [src] to heal [H]! Wake the fuck up, Samurai!", LOG_ATTACK, color="green") //Logging for 'old' style legion core use, when clicking on a sprite of yourself or another.
+
+/obj/item/organ/regenerative_core/attack_self(mob/user) //Knouli's first hack! Allows for the use of the core in hand rather than needing to click on the target, yourself, to selfheal. Its a rip of the proc just above - but skips on distance check and only uses 'user' rather than 'target'
+ if(ishuman(user)) //Check if user is human, no need for distance check as it's self heal
+ var/mob/living/carbon/human/H = user //Set H to user rather than target
+ if(inert) //Inert cores are useless
+ to_chat(user, "[src] has decayed and can no longer be used to heal.")
+ return
+ else //Skip on check if the target to be healed is dead as, if you are dead, you're not going to be able to use it on yourself!
+ to_chat(user, "You start to smear [src] on yourself. It feels and smells disgusting, but you feel amazingly refreshed in mere moments.")
+ SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "self"))
+ H.revive(full_heal = 1)
+ qdel(src)
+ H.log_message("[H] used [src] to heal themselves! Making use of Knouli's sexy and intelligent use-in-hand proc!", LOG_ATTACK, color="green") //Logging for 'new' style legion core use, when using the core in-hand.
+
+
+/obj/item/organ/regenerative_core/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE)
+ . = ..()
+ if(!preserved && !inert)
+ preserved(TRUE)
+ owner.visible_message("[src] stabilizes as it's inserted.")
+
+/obj/item/organ/regenerative_core/Remove(mob/living/carbon/M, special = 0)
+ if(!inert && !special)
+ owner.visible_message("[src] rapidly decays as it's removed.")
+ go_inert()
+ return ..()
+
+/obj/item/organ/regenerative_core/prepare_eat()
+ return null
+
+/*************************Legion core********************/
+/obj/item/organ/regenerative_core/legion
+ desc = "A strange rock that crackles with power. It can be used to heal completely, but it will rapidly decay into uselessness."
+ icon_state = "legion_soul"
+
+/obj/item/organ/regenerative_core/legion/Initialize()
+ . = ..()
+ update_icon()
+
+/obj/item/organ/regenerative_core/update_icon()
+ icon_state = inert ? "legion_soul_inert" : "legion_soul"
+ cut_overlays()
+ if(!inert && !preserved)
+ add_overlay("legion_soul_crackle")
+ for(var/X in actions)
+ var/datum/action/A = X
+ A.UpdateButtonIcon()
+
+/obj/item/organ/regenerative_core/legion/go_inert()
+ ..()
+ desc = "[src] has become inert. It has decayed, and is completely useless."
+
+/obj/item/organ/regenerative_core/legion/preserved(implanted = 0)
+ ..()
+ desc = "[src] has been stabilized. It is preserved, allowing you to use it to heal completely without danger of decay."
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index c7365210ab..695084a794 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -29,6 +29,7 @@
var/obj/machinery/machine = null
var/next_move = null
+ var/create_area_cooldown
var/notransform = null //Carbon
var/eye_blind = 0 //Carbon
var/eye_blurry = 0 //Carbon
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index 64e693a326..8936866305 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -155,7 +155,7 @@
reagent_state = SOLID
color = "#FFFFFF" // rgb: 255, 255, 255
taste_mult = 1.5 // stop sugar drowning out other flavours
- nutriment_factor = 10 * REAGENTS_METABOLISM
+ nutriment_factor = 5 * REAGENTS_METABOLISM
metabolization_rate = 2 * REAGENTS_METABOLISM
overdose_threshold = 200 // Hyperglycaemic shock
taste_description = "sweetness"
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/code/modules/vending/assist.dm b/code/modules/vending/assist.dm
index 280bcb5ca2..0d0dd40b36 100644
--- a/code/modules/vending/assist.dm
+++ b/code/modules/vending/assist.dm
@@ -1,6 +1,7 @@
/obj/machinery/vending/assist
products = list(/obj/item/assembly/prox_sensor = 7,
/obj/item/assembly/igniter = 6,
+ /obj/item/assembly/playback = 4,
/obj/item/assembly/signaler = 6,
/obj/item/wirecutters = 3,
/obj/item/stock_parts/cell/crap = 6,
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/config/game_options.txt b/config/game_options.txt
index 34c8ca48fd..ef9296cd3b 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -578,4 +578,10 @@ DYNAMIC_VOTING
## Choose which Engine to start the round with, comment to remove an Engine from the rotation
BOX_RANDOM_ENGINE Engine SM
BOX_RANDOM_ENGINE Engine Tesla
-BOX_RANDOM_ENGINE Engine Singulo
\ No newline at end of file
+BOX_RANDOM_ENGINE Engine Singulo
+
+## Whether or not there's a mode tier list vote after the secret/extended vote.
+MODETIER_VOTING
+
+## Number of modes dropped by the modetier vote during mode selection, after vote.
+DROPPED_MODES 3
diff --git a/html/changelogs/AutoChangeLog-pr-10285.yml b/html/changelogs/AutoChangeLog-pr-10285.yml
new file mode 100644
index 0000000000..89b0bbae43
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10285.yml
@@ -0,0 +1,4 @@
+author: "Ghommie"
+delete-after: True
+changes:
+ - bugfix: "Fixed singularity pulls duping rods out of engine floors."
diff --git a/html/changelogs/AutoChangeLog-pr-10440.yml b/html/changelogs/AutoChangeLog-pr-10440.yml
new file mode 100644
index 0000000000..e942f4056f
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10440.yml
@@ -0,0 +1,5 @@
+author: "Putnam3145"
+delete-after: True
+changes:
+ - rscadd: "Added a sort of \"game mode ban\" by way of having people rank their game modes favorite to least favorite after the secret/extended vote."
+ - bugfix: "Turns out the schulze scoring was written wrong and it was setting things to 0 that shouldn't have been, so that's fixed."
diff --git a/html/changelogs/AutoChangeLog-pr-10442.yml b/html/changelogs/AutoChangeLog-pr-10442.yml
new file mode 100644
index 0000000000..f407ae9e15
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10442.yml
@@ -0,0 +1,4 @@
+author: "Trilbyspaceclone"
+delete-after: True
+changes:
+ - tweak: "Halfs the nutriments in sugar"
diff --git a/html/changelogs/AutoChangeLog-pr-10452.yml b/html/changelogs/AutoChangeLog-pr-10452.yml
new file mode 100644
index 0000000000..e51b9ff752
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10452.yml
@@ -0,0 +1,4 @@
+author: "Ghommie"
+delete-after: True
+changes:
+ - bugfix: "Lattices can be examined yet again."
diff --git a/html/changelogs/AutoChangeLog-pr-10457.yml b/html/changelogs/AutoChangeLog-pr-10457.yml
new file mode 100644
index 0000000000..203842b0b6
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10457.yml
@@ -0,0 +1,5 @@
+author: "r4d6"
+delete-after: True
+changes:
+ - rscadd: "Added a playback device"
+ - bugfix: "Made the Voice Analyzer actually care about languages"
diff --git a/html/changelogs/AutoChangeLog-pr-10459.yml b/html/changelogs/AutoChangeLog-pr-10459.yml
new file mode 100644
index 0000000000..77de8eb193
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10459.yml
@@ -0,0 +1,4 @@
+author: "Putnam"
+delete-after: True
+changes:
+ - rscadd: "Cold-blooded quirk"
diff --git a/html/changelogs/AutoChangeLog-pr-10467.yml b/html/changelogs/AutoChangeLog-pr-10467.yml
new file mode 100644
index 0000000000..7fa379f088
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10467.yml
@@ -0,0 +1,4 @@
+author: "Seris02"
+delete-after: True
+changes:
+ - bugfix: "cardboard box speed"
diff --git a/html/changelogs/AutoChangeLog-pr-10472.yml b/html/changelogs/AutoChangeLog-pr-10472.yml
new file mode 100644
index 0000000000..9404d70b7b
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-10472.yml
@@ -0,0 +1,4 @@
+author: "keronshb"
+delete-after: True
+changes:
+ - bugfix: "fixed the missing icons from Dermal Button nanites"
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/icons/mob/actions/actions_items.dmi b/icons/mob/actions/actions_items.dmi
index 39e2ea0584..43c9d20a93 100644
Binary files a/icons/mob/actions/actions_items.dmi and b/icons/mob/actions/actions_items.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 0bad708dc4..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"
@@ -1442,6 +1444,7 @@
#include "code\modules\assembly\igniter.dm"
#include "code\modules\assembly\infrared.dm"
#include "code\modules\assembly\mousetrap.dm"
+#include "code\modules\assembly\playback.dm"
#include "code\modules\assembly\proximity.dm"
#include "code\modules\assembly\shock_kit.dm"
#include "code\modules\assembly\signaler.dm"