diff --git a/code/_helpers/lists.dm b/code/_helpers/lists.dm
index fb4cd3874eb..c8d12c2c2d4 100644
--- a/code/_helpers/lists.dm
+++ b/code/_helpers/lists.dm
@@ -71,11 +71,17 @@ proc/isemptylist(list/list)
return 1
return 0
-//Empties the list by setting the length to 0. Hopefully the elements get garbage collected
-proc/clearlist(list/list)
- if(istype(list))
- list.len = 0
- return
+/proc/instances_of_type_in_list(var/atom/A, var/list/L)
+ var/instances = 0
+ for(var/type in L)
+ if(istype(A, type))
+ instances++
+ return instances
+
+//Empties the list by .Cut(). Setting lenght = 0 has been confirmed to leak references.
+proc/clearlist(var/list/L)
+ if(islist(L))
+ L.Cut()
//Removes any null entries from the list
proc/listclearnulls(list/list)
diff --git a/code/game/antagonist/antagonist.dm b/code/game/antagonist/antagonist.dm
index faf7526a9cb..011ae5ebe6c 100644
--- a/code/game/antagonist/antagonist.dm
+++ b/code/game/antagonist/antagonist.dm
@@ -86,7 +86,8 @@
/datum/antagonist/proc/tick()
return 1
-/datum/antagonist/proc/get_candidates(var/ghosts_only)
+// Get the raw list of potential players.
+/datum/antagonist/proc/build_candidate_list(var/ghosts_only)
candidates = list() // Clear.
// Prune restricted status. Broke it up for readability.
@@ -108,46 +109,57 @@
return candidates
/datum/antagonist/proc/attempt_random_spawn()
+ update_current_antag_max()
build_candidate_list(flags & (ANTAG_OVERRIDE_MOB|ANTAG_OVERRIDE_JOB))
attempt_spawn()
finalize_spawn()
-/datum/antagonist/proc/attempt_late_spawn(var/datum/mind/player)
+/datum/antagonist/proc/attempt_auto_spawn()
if(!can_late_spawn())
return 0
- if(!istype(player))
- var/list/players = get_candidates(is_latejoin_template())
- if(players && players.len)
- player = pick(players)
- if(!istype(player))
- message_admins("AUTO[uppertext(ticker.mode.name)]: Failed to find a candidate for [role_text].")
- return 0
- player.current << "You have been selected this round as an antagonist!"
- message_admins("AUTO[uppertext(ticker.mode.name)]: Selected [player] as a [role_text].")
- if(istype(player.current, /mob/dead))
- create_default(player.current)
- else
- add_antagonist(player,0,0,0,1,1)
- return 1
-/datum/antagonist/proc/build_candidate_list(var/ghosts_only)
- // Get the raw list of potential players.
update_current_antag_max()
- candidates = get_candidates(ghosts_only)
+ var/active_antags = get_active_antag_count()
+ log_debug("[uppertext(id)]: Found [active_antags]/[cur_max] active [role_text_plural].")
+
+ if(active_antags >= cur_max)
+ log_debug("Could not auto-spawn a [role_text], active antag limit reached.")
+ return 0
+
+ build_candidate_list(flags & (ANTAG_OVERRIDE_MOB|ANTAG_OVERRIDE_JOB))
+ if(!candidates.len)
+ log_debug("Could not auto-spawn a [role_text], no candidates found.")
+ return 0
+
+ attempt_spawn(1) //auto-spawn antags one at a time
+ if(!pending_antagonists.len)
+ log_debug("Could not auto-spawn a [role_text], none of the available candidates could be selected.")
+ return 0
+
+ var/datum/mind/player = pending_antagonists[1]
+ if(!add_antagonist(player,0,0,0,1,1))
+ log_debug("Could not auto-spawn a [role_text], failed to add antagonist.")
+ return 0
+
+ reset_antag_selection()
+
+ return 1
//Selects players that will be spawned in the antagonist role from the potential candidates
//Selected players are added to the pending_antagonists lists.
//Attempting to spawn an antag role with ANTAG_OVERRIDE_JOB should be done before jobs are assigned,
//so that they do not occupy regular job slots. All other antag roles should be spawned after jobs are
//assigned, so that job restrictions can be respected.
-/datum/antagonist/proc/attempt_spawn(var/rebuild_candidates = 1)
+/datum/antagonist/proc/attempt_spawn(var/spawn_target = null)
+ if(spawn_target == null)
+ spawn_target = initial_spawn_target
// Update our boundaries.
if(!candidates.len)
return 0
//Grab candidates randomly until we have enough.
- while(candidates.len && pending_antagonists.len < initial_spawn_target)
+ while(candidates.len && pending_antagonists.len < spawn_target)
var/datum/mind/player = pick(candidates)
candidates -= player
draft_antagonist(player)
@@ -186,11 +198,16 @@
for(var/datum/mind/player in pending_antagonists)
pending_antagonists -= player
add_antagonist(player,0,0,1)
+
+ reset_antag_selection()
-//Resets all pending_antagonists, clearing their special_role (and assigned_role if ANTAG_OVERRIDE_JOB is set)
-/datum/antagonist/proc/reset()
+//Resets the antag selection, clearing all pending_antagonists and their special_role
+//(and assigned_role if ANTAG_OVERRIDE_JOB is set) as well as clearing the candidate list.
+//Existing antagonists are left untouched.
+/datum/antagonist/proc/reset_antag_selection()
for(var/datum/mind/player in pending_antagonists)
if(flags & ANTAG_OVERRIDE_JOB)
player.assigned_role = null
player.special_role = null
pending_antagonists.Cut()
+ candidates.Cut()
diff --git a/code/game/antagonist/antagonist_create.dm b/code/game/antagonist/antagonist_create.dm
index 416a774541a..1b8b82210a7 100644
--- a/code/game/antagonist/antagonist_create.dm
+++ b/code/game/antagonist/antagonist_create.dm
@@ -16,7 +16,8 @@
create_objectives(target)
update_icons_added(target)
greet(target)
- announce_antagonist_spawn()
+ if(!gag_announcement)
+ announce_antagonist_spawn()
/datum/antagonist/proc/create_default(var/mob/source)
var/mob/living/M
diff --git a/code/game/antagonist/antagonist_helpers.dm b/code/game/antagonist/antagonist_helpers.dm
index 056fbd18651..adc706734fd 100644
--- a/code/game/antagonist/antagonist_helpers.dm
+++ b/code/game/antagonist/antagonist_helpers.dm
@@ -20,6 +20,17 @@
/datum/antagonist/proc/get_antag_count()
return current_antagonists ? current_antagonists.len : 0
+/datum/antagonist/proc/get_active_antag_count()
+ var/active_antags = 0
+ for(var/datum/mind/player in current_antagonists)
+ var/mob/living/L = player.current
+ if(!L || L.stat == DEAD)
+ continue //no mob or dead
+ if(!L.client && !L.teleop)
+ continue //SSD
+ active_antags++
+ return active_antags
+
/datum/antagonist/proc/is_antagonist(var/datum/mind/player)
if(player in current_antagonists)
return 1
@@ -33,9 +44,6 @@
return (flags & ANTAG_VOTABLE)
/datum/antagonist/proc/can_late_spawn()
- update_current_antag_max()
- if(get_antag_count() >= cur_max)
- return 0
return 1
/datum/antagonist/proc/is_latejoin_template()
diff --git a/code/game/antagonist/station/rogue_ai.dm b/code/game/antagonist/station/rogue_ai.dm
index c17b6c0f86b..4240708cc22 100644
--- a/code/game/antagonist/station/rogue_ai.dm
+++ b/code/game/antagonist/station/rogue_ai.dm
@@ -22,13 +22,11 @@ var/datum/antagonist/rogue_ai/malf
malf = src
-/datum/antagonist/rogue_ai/get_candidates()
+/datum/antagonist/rogue_ai/build_candidate_list()
..()
for(var/datum/mind/player in candidates)
if(player.assigned_role && player.assigned_role != "AI")
candidates -= player
- if(!candidates.len)
- return list()
return candidates
diff --git a/code/game/gamemodes/calamity/calamity.dm b/code/game/gamemodes/calamity/calamity.dm
index 4f7b9ab49de..3104e624e34 100644
--- a/code/game/gamemodes/calamity/calamity.dm
+++ b/code/game/gamemodes/calamity/calamity.dm
@@ -12,14 +12,15 @@
/datum/game_mode/calamity/create_antagonists()
- shuffle(all_antag_types) // This is probably the only instance in the game where the order will be important.
- var/i = 1
+ //Let's not modify global lists for trivial reasons, even if it seems harmless right now.
+ var/list/antag_candidates = all_antag_types.Copy()
+
var/grab_antags = round(num_players()/ANTAG_TYPE_RATIO)+1
- for(var/antag_id in all_antag_types)
- if(i > grab_antags)
- break
+ while(antag_candidates.len && antag_tags.len < grab_antags)
+ var/antag_id = pick(antag_candidates)
+ antag_candidates -= antag_id
antag_tags |= antag_id
- i++
+
..()
/datum/game_mode/calamity/check_victory()
diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm
index dab27d898d4..a256a41edd6 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -21,7 +21,6 @@ var/global/list/additional_antag_types = list()
var/list/antag_tags = list() // Core antag templates to spawn.
var/list/antag_templates // Extra antagonist types to include.
- var/list/latejoin_templates = list()
var/round_autoantag = 0 // Will this round attempt to periodically spawn more antagonists?
var/antag_scaling_coeff = 5 // Coefficient for scaling max antagonists to player count.
var/require_all_templates = 0 // Will only start if all templates are checked and can spawn.
@@ -183,6 +182,7 @@ var/global/list/additional_antag_types = list()
/datum/game_mode/proc/pre_setup()
for(var/datum/antagonist/antag in antag_templates)
+ antag.update_current_antag_max()
antag.build_candidate_list() //compile a list of all eligible candidates
//antag roles that replace jobs need to be assigned before the job controller hands out jobs.
@@ -207,8 +207,6 @@ var/global/list/additional_antag_types = list()
if(!(antag.flags & ANTAG_OVERRIDE_JOB))
antag.attempt_spawn() //select antags to be spawned
antag.finalize_spawn() //actually spawn antags
- if(antag.is_latejoin_template())
- latejoin_templates |= antag
if(emergency_shuttle && auto_recall_shuttle)
emergency_shuttle.auto_recall = 1
@@ -221,7 +219,7 @@ var/global/list/additional_antag_types = list()
/datum/game_mode/proc/fail_setup()
for(var/datum/antagonist/antag in antag_templates)
- antag.reset()
+ antag.reset_antag_selection()
/datum/game_mode/proc/announce_ert_disabled()
if(!ert_disabled)
@@ -491,6 +489,7 @@ var/global/list/additional_antag_types = list()
if(antag)
antag_templates |= antag
+ shuffle(antag_templates) //In the case of multiple antag types
newscaster_announcements = pick(newscaster_standard_feeds)
/datum/game_mode/proc/check_victory()
diff --git a/code/game/gamemodes/game_mode_latespawn.dm b/code/game/gamemodes/game_mode_latespawn.dm
index 038cf351ab8..9c79a689251 100644
--- a/code/game/gamemodes/game_mode_latespawn.dm
+++ b/code/game/gamemodes/game_mode_latespawn.dm
@@ -3,55 +3,39 @@
/datum/game_mode/var/max_autotraitor_delay = 12000 // Approx 20 minutes.
/datum/game_mode/var/process_count = 0
-/datum/game_mode/proc/get_usable_templates(var/list/supplied_templates)
- var/list/usable_templates = list()
- for(var/datum/antagonist/A in supplied_templates)
- if(A.can_late_spawn())
- message_admins("AUTO[uppertext(name)]: [A.id] selected for spawn attempt.")
- usable_templates |= A
- return usable_templates
-
///process()
///Called by the gameticker
/datum/game_mode/proc/process()
- // Slow this down a bit so latejoiners have a chance of being antags.
- process_count++
- if(process_count >= 10)
- process_count = 0
- try_latespawn()
+ if(round_autoantag && world.time < next_spawn && !emergency_shuttle.departed)
+ process_autoantag()
-/datum/game_mode/proc/latespawn(var/mob/living/carbon/human/character)
- if(!character.mind)
- return
- try_latespawn(character.mind)
+//This can be overriden in case a game mode needs to do stuff when a player latejoins
+/datum/game_mode/proc/handle_latejoin(var/mob/living/carbon/human/character)
return 0
-/datum/game_mode/proc/try_latespawn(var/datum/mind/player, var/latejoin_only)
+/datum/game_mode/proc/process_autoantag()
+ message_admins("[uppertext(name)]: Attempting autospawn.")
- if(emergency_shuttle.departed || !round_autoantag)
- return
-
- if(world.time < next_spawn)
- return
-
- message_admins("AUTO[uppertext(name)]: Attempting spawn.")
-
- var/list/usable_templates
- if(latejoin_only && latejoin_templates.len)
- usable_templates = get_usable_templates(latejoin_templates)
- else if (antag_templates && antag_templates.len)
- usable_templates = get_usable_templates(antag_templates)
- else
- message_admins("AUTO[uppertext(name)]: Failed to find configured mode spawn templates, please disable auto-antagonists until one is added.")
+ var/list/usable_templates = list()
+ for(var/datum/antagonist/A in antag_templates)
+ if(A.can_late_spawn())
+ message_admins("[uppertext(name)]: [A.id] selected for spawn attempt.")
+ usable_templates |= A
+
+ if(!usable_templates.len)
+ message_admins("[uppertext(name)]: Failed to find configured mode spawn templates, please re-enable auto-antagonists after one is added.")
round_autoantag = 0
return
-
+
while(usable_templates.len)
var/datum/antagonist/spawn_antag = pick(usable_templates)
usable_templates -= spawn_antag
- if(spawn_antag.attempt_late_spawn(player))
- message_admins("AUTO[uppertext(name)]: Attempting to latespawn [spawn_antag.id]. ([spawn_antag.get_antag_count()]/[spawn_antag.cur_max])")
+
+ if(spawn_antag.attempt_auto_spawn())
+ message_admins("[uppertext(name)]: Auto-added a new [spawn_antag.role_text].")
+ message_admins("There are now [spawn_antag.get_active_antag_count()]/[spawn_antag.cur_max] active [spawn_antag.role_text_plural].")
next_spawn = world.time + rand(min_autotraitor_delay, max_autotraitor_delay)
return
- message_admins("AUTO[uppertext(name)]: Failed to proc a viable spawn template.")
- next_spawn = world.time + rand(min_autotraitor_delay, max_autotraitor_delay)
+
+ message_admins("[uppertext(name)]: Failed to proc a viable spawn template.")
+ next_spawn = world.time + min_autotraitor_delay //recheck again in the miniumum time
diff --git a/code/game/machinery/machinery.dm b/code/game/machinery/machinery.dm
index 709f4b46263..8a524f7dc3f 100644
--- a/code/game/machinery/machinery.dm
+++ b/code/game/machinery/machinery.dm
@@ -195,7 +195,10 @@ Class Procs:
return (stat & (NOPOWER|BROKEN|additional_flags))
/obj/machinery/CanUseTopic(var/mob/user)
- if(!interact_offline && (stat & (NOPOWER|BROKEN)))
+ if(stat & BROKEN)
+ return STATUS_CLOSE
+
+ if(!interact_offline && (stat & NOPOWER))
return STATUS_CLOSE
return ..()
diff --git a/code/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm
index 953b04345d1..ce2e5ad1501 100644
--- a/code/game/objects/items/devices/PDA/PDA.dm
+++ b/code/game/objects/items/devices/PDA/PDA.dm
@@ -721,7 +721,8 @@ var/global/list/obj/item/device/pda/PDAs = list()
if("Message")
var/obj/item/device/pda/P = locate(href_list["target"])
- src.create_message(U, P, !href_list["notap"])
+ var/tap = istype(U, /mob/living/carbon)
+ src.create_message(U, P, tap)
if(mode == 2)
if(href_list["target"] in conversations) // Need to make sure the message went through, if not welp.
active_conversation = href_list["target"]
@@ -1040,7 +1041,7 @@ var/global/list/obj/item/device/pda/PDAs = list()
new_message(sending_device, sending_device.owner, sending_device.ownjob, message)
/obj/item/device/pda/proc/new_message(var/sending_unit, var/sender, var/sender_job, var/message)
- var/reception_message = "\icon[src] Message from [sender] ([sender_job]), \"[message]\" (Reply)"
+ var/reception_message = "\icon[src] Message from [sender] ([sender_job]), \"[message]\" (Reply)"
new_info(message_silent, ttone, reception_message)
log_pda("[usr] (PDA: [sending_unit]) sent \"[message]\" to [name]")
@@ -1052,7 +1053,7 @@ var/global/list/obj/item/device/pda/PDAs = list()
if(ismob(sending_unit.loc) && isAI(loc))
track = "(Follow)"
- var/reception_message = "\icon[src] Message from [sender] ([sender_job]), \"[message]\" (Reply) [track]"
+ var/reception_message = "\icon[src] Message from [sender] ([sender_job]), \"[message]\" (Reply) [track]"
new_info(message_silent, newstone, reception_message)
log_pda("[usr] (PDA: [sending_unit]) sent \"[message]\" to [name]")
diff --git a/code/game/objects/items/weapons/storage/boxes.dm b/code/game/objects/items/weapons/storage/boxes.dm
index 95c41916222..00c58e404a5 100644
--- a/code/game/objects/items/weapons/storage/boxes.dm
+++ b/code/game/objects/items/weapons/storage/boxes.dm
@@ -598,35 +598,36 @@
icon_state = "light"
desc = "This box is shaped on the inside so that only light tubes and bulbs fit."
item_state = "syringe_kit"
- storage_slots=21
- can_hold = list(/obj/item/weapon/light/tube, /obj/item/weapon/light/bulb)
- max_storage_space = 42 //holds 21 items of w_class 2
use_to_pickup = 1 // for picking up broken bulbs, not that most people will try
+
+/obj/item/weapon/storage/box/lights/New()
+ ..()
+ make_exact_fit()
/obj/item/weapon/storage/box/lights/bulbs/New()
- ..()
for(var/i = 0; i < 21; i++)
new /obj/item/weapon/light/bulb(src)
+ ..()
/obj/item/weapon/storage/box/lights/tubes
name = "box of replacement tubes"
icon_state = "lighttube"
/obj/item/weapon/storage/box/lights/tubes/New()
- ..()
for(var/i = 0; i < 21; i++)
new /obj/item/weapon/light/tube(src)
+ ..()
/obj/item/weapon/storage/box/lights/mixed
name = "box of replacement lights"
icon_state = "lightmixed"
/obj/item/weapon/storage/box/lights/mixed/New()
- ..()
for(var/i = 0; i < 14; i++)
new /obj/item/weapon/light/tube(src)
for(var/i = 0; i < 7; i++)
new /obj/item/weapon/light/bulb(src)
+ ..()
/obj/item/weapon/storage/box/freezer
name = "portable freezer"
diff --git a/code/game/objects/items/weapons/storage/firstaid.dm b/code/game/objects/items/weapons/storage/firstaid.dm
index c23dfcdb516..a76fb4f1a1f 100644
--- a/code/game/objects/items/weapons/storage/firstaid.dm
+++ b/code/game/objects/items/weapons/storage/firstaid.dm
@@ -130,8 +130,7 @@
/obj/item/weapon/storage/firstaid/surgery
name = "surgery kit"
- desc = "Contains tools for surgery."
- storage_slots = 10
+ desc = "Contains tools for surgery. Has precise foam fitting for safe transport."
/obj/item/weapon/storage/firstaid/surgery/New()
..()
@@ -146,7 +145,8 @@
new /obj/item/weapon/bonegel(src)
new /obj/item/weapon/FixOVein(src)
new /obj/item/stack/medical/advanced/bruise_pack(src)
- return
+
+ make_exact_fit()
/*
* Pill Bottles
diff --git a/code/game/objects/items/weapons/storage/storage.dm b/code/game/objects/items/weapons/storage/storage.dm
index 131347ac29a..aa9a54b2240 100644
--- a/code/game/objects/items/weapons/storage/storage.dm
+++ b/code/game/objects/items/weapons/storage/storage.dm
@@ -232,12 +232,16 @@
usr << "[src] is full, make some space."
return 0 //Storage item is full
- if(can_hold.len && !is_type_in_list(W, can_hold))
- if(!stop_messages)
- if (istype(W, /obj/item/weapon/hand_labeler))
- return 0
- usr << "[src] cannot hold [W]."
- return 0
+ if(can_hold.len)
+ if(!is_type_in_list(W, can_hold))
+ if(!stop_messages && ! istype(W, /obj/item/weapon/hand_labeler))
+ usr << "[src] cannot hold \the [W]."
+ return 0
+ var/max_instances = can_hold[W.type]
+ if(max_instances && instances_of_type_in_list(W, contents) >= max_instances)
+ if(!stop_messages && !istype(W, /obj/item/weapon/hand_labeler))
+ usr << "[src] has no more space specifically for \the [W]."
+ return 0
if(cant_hold.len && is_type_in_list(W, cant_hold))
if(!stop_messages)
@@ -443,6 +447,17 @@
src.quick_empty()
return 1
+/obj/item/weapon/storage/proc/make_exact_fit()
+ storage_slots = contents.len
+
+ can_hold.Cut()
+ max_w_class = 0
+ max_storage_space = 0
+ for(var/obj/item/I in src)
+ can_hold[I.type]++
+ max_w_class = max(I.w_class, max_w_class)
+ max_storage_space += I.get_storage_cost()
+
//Returns the storage depth of an atom. This is the number of storage items the atom is contained in before reaching toplevel (the area).
//Returns -1 if the atom was not found on container.
/atom/proc/storage_depth(atom/container)
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index bac665035ec..aaf184604a3 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -612,7 +612,7 @@ proc/admin_notice(var/message, var/rights)
Bombing List
Show current traitors and objectives
Show last [length(lastsignalers)] signalers
- Show last [length(lawchanges)] law changes
+ Show last [lawchanges.len] law change\s
Show AI Laws
Show Game Mode
Show Crew Manifest
@@ -1386,7 +1386,7 @@ proc/admin_notice(var/message, var/rights)
var/datum/antagonist/antag = all_antag_types[antag_type]
message_admins("[key_name(usr)] attempting to force latespawn with template [antag.id].")
- antag.attempt_late_spawn()
+ antag.attempt_auto_spawn()
/datum/admins/proc/force_mode_latespawn()
set category = "Admin"
@@ -1403,6 +1403,5 @@ proc/admin_notice(var/message, var/rights)
usr << "Mode has not started."
return
- message_admins("[key_name(usr)] attempting to force mode latespawn.")
- ticker.mode.next_spawn = 0
- ticker.mode.try_latespawn()
+ message_admins("[key_name(usr)] attempting to force mode autospawn.")
+ ticker.mode.process_autoantag()
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 9c4397106f2..4dcb78e65b6 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -2300,7 +2300,7 @@
dat += "[sig]
"
usr << browse(dat, "window=lastsignalers;size=800x500")
if("list_lawchanges")
- var/dat = "Showing last [length(lawchanges)] law changes.