diff --git a/_maps/templates/shelter_1.dmm b/_maps/templates/shelter_1.dmm
index f5b2e141f4..7f95fba10f 100644
--- a/_maps/templates/shelter_1.dmm
+++ b/_maps/templates/shelter_1.dmm
@@ -50,9 +50,7 @@
/area/survivalpod)
"l" = (
/obj/structure/tubes,
-/obj/structure/chair/comfy/black{
- dir = 8
- },
+/obj/machinery/recharge_station,
/turf/open/floor/pod,
/area/survivalpod)
"m" = (
diff --git a/_maps/templates/shelter_2.dmm b/_maps/templates/shelter_2.dmm
index 80de4438da..825cb26e3a 100644
--- a/_maps/templates/shelter_2.dmm
+++ b/_maps/templates/shelter_2.dmm
@@ -67,8 +67,7 @@
layer = 3
},
/obj/machinery/door/window/survival_pod{
- dir = 1;
- icon_state = "windoor"
+ dir = 1
},
/turf/open/floor/carpet/black,
/area/survivalpod)
@@ -133,7 +132,6 @@
/area/survivalpod)
"u" = (
/obj/machinery/door/window/survival_pod{
- icon_state = "windoor";
dir = 1
},
/turf/open/floor/carpet/black,
@@ -169,10 +167,10 @@
/area/survivalpod)
"y" = (
/obj/structure/sink/kitchen{
- icon_state = "sink_alt";
dir = 4;
pixel_x = -13
},
+/obj/machinery/recharge_station/upgraded,
/turf/open/floor/carpet/black,
/area/survivalpod)
"z" = (
diff --git a/_maps/templates/shelter_3.dmm b/_maps/templates/shelter_3.dmm
index b71da1fba0..bb400f29ac 100644
--- a/_maps/templates/shelter_3.dmm
+++ b/_maps/templates/shelter_3.dmm
@@ -268,6 +268,10 @@
/obj/structure/fans/tiny,
/turf/open/floor/carpet/black,
/area/survivalpod)
+"T" = (
+/obj/machinery/recharge_station/fullupgrade,
+/turf/open/floor/carpet/black,
+/area/survivalpod)
(1,1,1) = {"
a
@@ -287,7 +291,7 @@ b
f
q
s
-x
+T
y
D
F
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 5a0eb0c65f..8b6bbe83b4 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/elements/wuv.dm b/code/datums/elements/wuv.dm
new file mode 100644
index 0000000000..84f327500f
--- /dev/null
+++ b/code/datums/elements/wuv.dm
@@ -0,0 +1,60 @@
+
+/datum/element/wuv //D'awwwww
+ element_flags = ELEMENT_BESPOKE
+ id_arg_index = 2
+ //the for the me emote proc call when petted.
+ var/pet_emote
+ //whether the emote is visible or audible
+ var/pet_type
+ //same as above, except when harmed. "You are going into orbit, you stupid mutt!"
+ var/punt_emote
+ //same as pet_type
+ var/punt_type
+ //mood typepath for the moodlet signal when petted.
+ var/pet_moodlet
+ //same as above but for harm
+ var/punt_moodlet
+
+/datum/element/wuv/Attach(datum/target, pet, pet_t, pet_mood, punt, punt_t, punt_mood)
+ . = ..()
+
+ if(!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+
+ pet_emote = pet
+ pet_type = pet_t
+ punt_emote = punt
+ punt_type = punt_t
+ pet_moodlet = pet_mood
+ punt_moodlet = punt_mood
+
+ RegisterSignal(target, COMSIG_MOB_ATTACK_HAND, .proc/on_attack_hand)
+
+/datum/element/wuv/proc/on_attack_hand(datum/source, mob/user)
+ var/mob/living/L = source
+
+ if(L.stat == DEAD)
+ return
+ //we want to delay the effect to be displayed after the mob is petted, not before.
+ switch(user.a_intent)
+ if(INTENT_HARM, INTENT_DISARM)
+ addtimer(CALLBACK(src, .proc/kick_the_dog, source, user), 1)
+ if(INTENT_HELP)
+ addtimer(CALLBACK(src, .proc/pet_the_dog, source, user), 1)
+
+/datum/element/wuv/proc/pet_the_dog(mob/target, mob/user)
+ if(!QDELETED(target) || !QDELETED(user) || target.stat != CONSCIOUS)
+ return
+ new /obj/effect/temp_visual/heart(target.loc)
+ if(pet_emote)
+ target.emote("me", pet_type, pet_emote)
+ if(pet_moodlet && !CHECK_BITFIELD(target.flags_1, HOLOGRAM_1)) //prevents unlimited happiness petting park exploit.
+ SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, target, pet_moodlet, target)
+
+/datum/element/wuv/proc/kick_the_dog(mob/target, mob/user)
+ if(!QDELETED(target) || !QDELETED(user) || target.stat != CONSCIOUS)
+ return
+ if(punt_emote)
+ target.emote("me", punt_type, punt_emote)
+ if(punt_moodlet && !CHECK_BITFIELD(target.flags_1, HOLOGRAM_1))
+ SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, target, punt_moodlet, target)
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 5c92c83fb5..8ae45ff720 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -525,7 +525,7 @@
if(!objective)
to_chat(usr,"Invalid objective.")
return
- //qdel(objective) Needs cleaning objective destroys
+ qdel(objective) //TODO: Needs cleaning objective destroys (whatever that means)
message_admins("[key_name_admin(usr)] removed an objective for [current]: [objective.explanation_text]")
log_admin("[key_name(usr)] removed an objective for [current]: [objective.explanation_text]")
diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm
index 94fd08535f..98a8eade59 100644
--- a/code/datums/mood_events/generic_positive_events.dm
+++ b/code/datums/mood_events/generic_positive_events.dm
@@ -23,10 +23,13 @@
mood_change = 3
timeout = 3000
-/datum/mood_event/pet_corgi
- description = "Corgis are adorable! I can't stop petting them!\n"
- mood_change = 3
- timeout = 3000
+/datum/mood_event/pet_animal
+ description = "Animals are adorable! I can't stop petting them!\n"
+ mood_change = 2
+ timeout = 5 MINUTES
+
+/datum/mood_event/pet_animal/add_effects(mob/animal)
+ description = "\The [animal.name] is adorable! I can't stop petting [animal.p_them()]!\n"
/datum/mood_event/honk
description = "Maybe clowns aren't so bad after all. Honk!\n"
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/Sleeper.dm b/code/game/machinery/Sleeper.dm
index 520721560a..b0c7299b34 100644
--- a/code/game/machinery/Sleeper.dm
+++ b/code/game/machinery/Sleeper.dm
@@ -14,7 +14,7 @@
circuit = /obj/item/circuitboard/machine/sleeper
req_access = list(ACCESS_CMO) //Used for reagent deletion and addition of non medicines
var/efficiency = 1
- var/min_health = -25
+ var/min_health = 30
var/list/available_chems
var/controls_inside = FALSE
var/list/possible_chems = list(
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/machinery/doppler_array.dm b/code/game/machinery/doppler_array.dm
index 1c25c7b242..65f7602215 100644
--- a/code/game/machinery/doppler_array.dm
+++ b/code/game/machinery/doppler_array.dm
@@ -7,26 +7,51 @@ GLOBAL_LIST_EMPTY(doppler_arrays)
icon_state = "tdoppler"
density = TRUE
var/integrated = FALSE
+ var/list_limit = 100
+ var/cooldown = 10
+ var/next_announce = 0
var/max_dist = 150
verb_say = "states coldly"
+ var/list/message_log = list()
/obj/machinery/doppler_array/Initialize()
. = ..()
GLOB.doppler_arrays += src
/obj/machinery/doppler_array/ComponentInitialize()
+ . = ..()
AddComponent(/datum/component/simple_rotation,ROTATION_ALTCLICK | ROTATION_CLOCKWISE,null,null,CALLBACK(src,.proc/rot_message))
/obj/machinery/doppler_array/Destroy()
GLOB.doppler_arrays -= src
return ..()
-/obj/machinery/doppler_array/examine(mob/user)
+/obj/machinery/doppler_array/ui_interact(mob/user)
. = ..()
- . += "Its dish is facing to the [dir2text(dir)]."
+ if(stat)
+ return FALSE
-/obj/machinery/doppler_array/process()
- return PROCESS_KILL
+ var/list/dat = list()
+ for(var/i in 1 to LAZYLEN(message_log))
+ dat += "Log recording #[i]: [message_log[i]]
"
+ dat += "Delete logs
"
+ dat += "