From bee97d0a50f36450a36bac0f3ef74608b222a205 Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 12:39:58 -0800
Subject: [PATCH 01/73] Update _automaton.dm
---
.../living/carbon/human/species_types/automatons/_automaton.dm | 1 +
1 file changed, 1 insertion(+)
diff --git a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
index a0e7907d1dc..d7a3b686dde 100644
--- a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
+++ b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
@@ -111,6 +111,7 @@
. = ..()
C.AddComponent(/datum/component/abberant_eater, list(/obj/item/ore/coal, /obj/item/grown/log/tree))
C.AddComponent(/datum/component/steam_life)
+ C.AddComponent(/datum/component/command_follower)
C.AddComponent(/datum/component/augmentable)
RegisterSignal(C, COMSIG_MOB_SAY, PROC_REF(handle_speech))
From abdde4c0ce00e3ef9b7dad5f3d32a0d7c1577c0d Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 14:14:34 -0800
Subject: [PATCH 02/73] adds in a ghost vessel component so that we can create
possessable things based on an item attack, adds in craftable automatons
---
code/datums/components/ghost_vessel.dm | 89 +++++++++++
.../crafting/quality_of_crafting/books.dm | 1 +
.../crafting/slapcrafting/orderless/_base.dm | 5 +-
.../species_types/automatons/_automaton.dm | 6 +-
.../species_types/automatons/assembly.dm | 145 ++++++++++++++++++
vanderlin.dme | 2 +
6 files changed, 244 insertions(+), 4 deletions(-)
create mode 100644 code/datums/components/ghost_vessel.dm
create mode 100644 code/modules/mob/living/carbon/human/species_types/automatons/assembly.dm
diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm
new file mode 100644
index 00000000000..d778e517abe
--- /dev/null
+++ b/code/datums/components/ghost_vessel.dm
@@ -0,0 +1,89 @@
+/datum/component/ghost_vessel
+ var/obj/item/vessel_item_type // The item type that "unlocks" the mob
+ var/mob/living/carbon/human/owner
+ var/being_offered = FALSE
+
+/datum/component/ghost_vessel/Initialize(obj/item/item_type)
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ owner = parent
+ vessel_item_type = item_type
+
+ ADD_TRAIT(owner, TRAIT_STASIS, REF(src))
+ ADD_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT)
+ ADD_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT)
+
+ RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby))
+ RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_deleted))
+
+/datum/component/ghost_vessel/Destroy()
+ if(owner)
+ REMOVE_TRAIT(owner, TRAIT_STASIS, REF(src))
+ REMOVE_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT)
+ REMOVE_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT)
+ owner = null
+ return ..()
+
+/datum/component/ghost_vessel/proc/on_parent_deleted(datum/source)
+ return
+
+/datum/component/ghost_vessel/proc/on_attackby(datum/source, obj/item/W, mob/living/user)
+ SIGNAL_HANDLER
+ if(!istype(W, vessel_item_type))
+ return
+ if(being_offered)
+ return
+ qdel(W)
+ INVOKE_ASYNC(src, PROC_REF(begin_ghost_offer))
+
+/datum/component/ghost_vessel/proc/begin_ghost_offer()
+ being_offered = TRUE
+
+ var/list/candidates = pollCandidatesForMob(
+ "A vessel at [owner.loc] awaits a soul. Do you wish to inhabit it?",
+ null,
+ null,
+ null,
+ 100,
+ parent,
+ POLL_IGNORE_GOLEM,
+ new_players = TRUE
+ )
+
+ if(length(candidates))
+ var/mob/dead/observer/chosen = candidates[1]
+ possess_vessel(chosen)
+ else
+ owner.balloon_alert_to_viewers("This vessel awaits a soul...")
+ add_ghost_verb()
+
+/datum/component/ghost_vessel/proc/possess_vessel(mob/dead/observer/ghost)
+ if(!ghost?.client)
+ return
+
+ being_offered = FALSE
+ REMOVE_TRAIT(owner, TRAIT_STASIS, REF(src))
+ REMOVE_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT)
+ REMOVE_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT)
+ owner.key = ghost.client.key
+ qdel(src)
+
+/datum/component/ghost_vessel/proc/add_ghost_verb()
+ RegisterSignal(owner, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine_by_ghost))
+
+/datum/component/ghost_vessel/proc/on_examine_by_ghost(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ if(!istype(user, /mob/dead/observer))
+ return
+ examine_list += span_notice("This vessel is empty. Inhabit it (Orbit Mob)?")
+ RegisterSignal(user, COMSIG_ATOM_ORBIT_BEGIN, PROC_REF(on_ghost_ctrl_click)) // alt: add a verb
+
+/datum/component/ghost_vessel/proc/on_ghost_ctrl_click(datum/source, mob/living/clicker)
+ var/option = browser_input_list(clicker, "Do you wish to possess this vessel??", "XYLIX", DEFAULT_INPUT_CHOICES)
+ if(!option)
+ return
+ if(!istype(clicker, /mob/dead/observer))
+ return
+ if(!being_offered)
+ return
+ INVOKE_ASYNC(src, PROC_REF(possess_vessel), clicker)
diff --git a/code/modules/crafting/quality_of_crafting/books.dm b/code/modules/crafting/quality_of_crafting/books.dm
index e6d32899204..6168518c44a 100644
--- a/code/modules/crafting/quality_of_crafting/books.dm
+++ b/code/modules/crafting/quality_of_crafting/books.dm
@@ -845,6 +845,7 @@
/datum/repeatable_crafting_recipe/engineering,
/datum/blueprint_recipe/engineering,
/datum/artificer_recipe,
+ /datum/orderless_slapcraft/automaton,
)
/obj/item/recipe_book/masonry
diff --git a/code/modules/crafting/slapcrafting/orderless/_base.dm b/code/modules/crafting/slapcrafting/orderless/_base.dm
index 62e02eb78cd..0a898aa25ca 100644
--- a/code/modules/crafting/slapcrafting/orderless/_base.dm
+++ b/code/modules/crafting/slapcrafting/orderless/_base.dm
@@ -16,6 +16,7 @@
var/datum/skill/related_skill
var/skill_xp_gained
var/action_time = 3 SECONDS
+ var/process_sound = 'sound/foley/dropsound/food_drop.ogg'
///list of atoms we pass to the output item
var/list/atoms_to_pass = list()
@@ -71,7 +72,7 @@
continue
if(!do_after(user, modified_action_time, hosted_source))
return
- playsound(user, 'sound/foley/dropsound/food_drop.ogg', 30, TRUE, -1)
+ playsound(user, process_sound, 30, TRUE, -1)
requirements[requirement]--
if(requirements[requirement] <= 0)
requirements -= list(requirement) // See Remove() behavior documentation
@@ -87,7 +88,7 @@
if(istype(attacking_item, requirement))
if(!do_after(user, modified_action_time, hosted_source))
return
- playsound(user, 'sound/foley/dropsound/food_drop.ogg', 30, TRUE, -1)
+ playsound(user, process_sound, 30, TRUE, -1)
requirements[requirement]--
if(requirements[requirement] <= 0)
requirements -= requirement
diff --git a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
index d7a3b686dde..60e5e8be763 100644
--- a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
+++ b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
@@ -2,6 +2,10 @@
race = /datum/species/automaton
footstep_type = FOOTSTEP_MOB_METAL
+/mob/living/carbon/human/species/automaton/vessel/LateInitialize()
+ . = ..()
+ AddComponent(/datum/component/ghost_vessel, /obj/item/reagent_containers/lux)
+
/datum/species/automaton
name = "Automaton"
id = SPEC_ID_AUTOMATON
@@ -174,12 +178,10 @@
/obj/item/organ/heart/automaton
name = "steam engine"
desc = "A miniature steam engine that powers the automaton's movements."
- icon_state = "steam_heart"
/obj/item/organ/eyes/automaton
name = "optical sensors"
desc = "Glowing lenses that allow the automaton to perceive the world."
- icon_state = "automaton_eyes"
/datum/blood_type/oil
name = "Lubricating Oil"
diff --git a/code/modules/mob/living/carbon/human/species_types/automatons/assembly.dm b/code/modules/mob/living/carbon/human/species_types/automatons/assembly.dm
new file mode 100644
index 00000000000..601fdab31cf
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/species_types/automatons/assembly.dm
@@ -0,0 +1,145 @@
+/obj/item/automaton_frame
+ name = "automaton frame"
+ desc = "An unfinished brass skeleton, waiting to be given purpose. The joints are hollow, the chest cavity empty."
+ icon = 'icons/roguetown/mob/bodies/m/automaton.dmi'
+ icon_state = "chest_s"
+ w_class = WEIGHT_CLASS_HUGE
+
+/datum/repeatable_crafting_recipe/engineering/automaton_frame
+ name = "automaton frame"
+ category = "Automatons"
+ requirements = list(
+ /obj/item/ingot/bronze = 8,
+ /obj/item/ingot/iron = 4,
+ )
+ tool_usage = list(
+ /obj/item/weapon/hammer = list(
+ span_notice("starts hammering a brass frame together"),
+ span_notice("start hammering a brass frame together"),
+ 'sound/items/bsmith2.ogg'
+ ),
+ )
+ attacked_atom = /obj/item/ingot/bronze
+ starting_atom = /obj/item/weapon/hammer
+ output = /obj/item/automaton_frame
+ craft_time = 20 SECONDS
+
+/datum/repeatable_crafting_recipe/engineering/automaton_heart
+ name = "automaton steam engine"
+ category = "Automatons"
+ requirements = list(
+ /obj/item/ingot/copper = 3,
+ /obj/item/ingot/iron = 1,
+ /obj/item/rotation_contraption/boiler = 1,
+ )
+ tool_usage = list(
+ /obj/item/weapon/hammer = list(
+ span_notice("starts assembling a miniature steam engine"),
+ span_notice("start assembling a miniature steam engine"),
+ 'sound/items/bsmith2.ogg'
+ ),
+ )
+ attacked_atom = /obj/item/rotation_contraption/boiler
+ starting_atom = /obj/item/weapon/hammer
+ output = /obj/item/organ/heart/automaton
+ craft_time = 12 SECONDS
+
+/datum/repeatable_crafting_recipe/engineering/automaton_eyes
+ name = "automaton optical sensors"
+ category = "Automatons"
+ requirements = list(
+ /obj/item/ingot/bronze = 1,
+ /obj/item/ingot/copper = 1,
+ )
+ tool_usage = list(
+ /obj/item/weapon/knife = list(
+ span_notice("starts grinding lenses for optical sensors"),
+ span_notice("start grinding lenses for optical sensors"),
+ 'sound/items/wood_sharpen.ogg'
+ ),
+ )
+ attacked_atom = /obj/item/ingot/bronze
+ starting_atom = /obj/item/weapon/knife
+ output = /obj/item/organ/eyes/automaton
+ craft_time = 10 SECONDS
+
+/datum/orderless_slapcraft/automaton
+ name = "Automaton Assembly"
+ category = "Automatons"
+ related_skill = /datum/skill/craft/engineering
+ skill_xp_gained = 50
+ action_time = 2 SECONDS
+ process_sound = 'sound/items/bsmith2.ogg'
+
+ starting_item = /obj/item/automaton_frame
+
+ requirements = list(
+ /obj/item/ingot/bronze = 5,
+ /obj/item/ingot/copper = 3,
+ /obj/item/ingot/iron = 2,
+ /obj/item/organ/eyes/automaton = 1,
+ /obj/item/gear/metal/bronze = 4,
+ )
+ finishing_item = /obj/item/organ/heart/automaton
+ output_item = null // We spawn a mob instead, handled in try_finish
+
+ var/list/installed_parts = list()
+ var/total_requirements = 0
+
+/datum/orderless_slapcraft/automaton/New(loc, _source)
+ . = ..()
+ for(var/type in requirements)
+ total_requirements += requirements[type]
+
+/datum/orderless_slapcraft/automaton/step_process(mob/user, obj/item/attacking_item)
+ if(istype(attacking_item, /obj/item/ingot/bronze))
+ user.visible_message(span_notice("[user] hammers brass plating onto the frame."), span_notice("You hammer the brass plating into place."))
+ else if(istype(attacking_item, /obj/item/ingot/copper))
+ user.visible_message(span_notice("[user] threads copper piping through the frame's chest."), span_notice("You thread copper piping through the chest cavity."))
+ else if(istype(attacking_item, /obj/item/ingot/iron))
+ user.visible_message(span_notice("[user] bolts iron joints into the frame."), span_notice("You bolt the iron joints firmly in place."))
+ else if(istype(attacking_item, /obj/item/organ/heart/automaton))
+ user.visible_message(span_notice("[user] carefully seats the steam engine into the frame's chest."), span_notice("You lower the steam engine into the chest cavity. It fits with a heavy clunk."))
+ else if(istype(attacking_item, /obj/item/organ/eyes/automaton))
+ user.visible_message(span_notice("[user] screws the optical sensors into the frame's skull."), span_notice("You screw the optical sensors into place. The lenses catch the light."))
+ else if(istype(attacking_item, /obj/item/gear/metal/bronze))
+ user.visible_message(span_notice("[user] clicks cogwheels into the frame's joints."), span_notice("You slot the cogwheels into the joint assemblies."))
+ update_frame_overlays()
+
+/datum/orderless_slapcraft/automaton/proc/update_frame_overlays()
+ var/remaining = 0
+ for(var/type in requirements)
+ remaining += requirements[type]
+
+ var/progress = (total_requirements - remaining) / total_requirements // 0.0 to 1.0 could do 0 to 100 but eh same thing really
+
+ var/list/stage_overlays = list(
+ "r_leg_s" = 0.15,
+ "l_leg_s" = 0.30,
+ "r_arm_s" = 0.50,
+ "l_arm_s" = 0.65,
+ "torso_s" = 0.80,
+ "head_s" = 0.95,
+ )
+
+ for(var/overlay in stage_overlays)
+ if(progress >= stage_overlays[overlay] && !(overlay in installed_parts))
+ installed_parts += overlay
+ hosted_source.add_overlay(mutable_appearance(hosted_source.icon, overlay))
+
+/datum/orderless_slapcraft/automaton/process_finishing_item(obj/item/attacking_item, mob/user)
+ user.visible_message(
+ span_notice("[user] presses the soul core into the automaton's chest. The runes flare — then go still."),
+ span_notice("You press the soul core into the chest cavity. The binding runes flare with cold light, then dim. Something stirs within the brass.")
+ )
+ return FALSE
+
+/datum/orderless_slapcraft/automaton/try_finish(mob/user)
+ var/turf/T = get_turf(hosted_source)
+ qdel(hosted_source)
+ new /mob/living/carbon/human/species/automaton/vessel(T)
+ user.adjust_experience(related_skill, skill_xp_gained)
+ to_chat(user, span_notice("The automaton stands complete. It awaits a soul."))
+
+/datum/orderless_slapcraft/automaton/handle_output_item(mob/user, obj/item/new_item)
+ return
diff --git a/vanderlin.dme b/vanderlin.dme
index b5b79f68b13..4da9f648af3 100644
--- a/vanderlin.dme
+++ b/vanderlin.dme
@@ -924,6 +924,7 @@
#include "code\datums\components\explodable.dm"
#include "code\datums\components\food_burner.dm"
#include "code\datums\components\generic_animal_hunger.dm"
+#include "code\datums\components\ghost_vessel.dm"
#include "code\datums\components\happiness.dm"
#include "code\datums\components\happiness_system.dm"
#include "code\datums\components\hideous_face.dm"
@@ -3226,6 +3227,7 @@
#include "code\modules\mob\living\carbon\human\npc\species_hostile\triton_hostile.dm"
#include "code\modules\mob\living\carbon\human\species_types\automatons\_automaton.dm"
#include "code\modules\mob\living\carbon\human\species_types\automatons\action.dm"
+#include "code\modules\mob\living\carbon\human\species_types\automatons\assembly.dm"
#include "code\modules\mob\living\carbon\human\species_types\automatons\voicelines.dm"
#include "code\modules\mob\living\carbon\human\species_types\demihumans\_demihuman.dm"
#include "code\modules\mob\living\carbon\human\species_types\dwarf\_dwarf.dm"
From 144644e68592663957b9db54f0ad20ba2f0eefcc Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:00:44 -0800
Subject: [PATCH 03/73] adds it to the latejoin menu
---
code/datums/components/ghost_vessel.dm | 39 +++++++------------
.../modules/mob/dead/new_player/new_player.dm | 24 ++++++++++++
2 files changed, 38 insertions(+), 25 deletions(-)
diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm
index d778e517abe..13d7692d836 100644
--- a/code/datums/components/ghost_vessel.dm
+++ b/code/datums/components/ghost_vessel.dm
@@ -1,22 +1,28 @@
+GLOBAL_LIST_EMPTY(active_ghost_vessels)
+
/datum/component/ghost_vessel
- var/obj/item/vessel_item_type // The item type that "unlocks" the mob
+ var/obj/item/vessel_item_type
var/mob/living/carbon/human/owner
var/being_offered = FALSE
+ var/vessel_id = "Automaton"
-/datum/component/ghost_vessel/Initialize(obj/item/item_type)
+/datum/component/ghost_vessel/Initialize(obj/item/item_type, id = "Automaton")
if(!isliving(parent))
return COMPONENT_INCOMPATIBLE
owner = parent
vessel_item_type = item_type
-
+ vessel_id = id
ADD_TRAIT(owner, TRAIT_STASIS, REF(src))
ADD_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT)
ADD_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT)
-
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby))
RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_deleted))
/datum/component/ghost_vessel/Destroy()
+ if(vessel_id && GLOB.active_ghost_vessels[vessel_id])
+ GLOB.active_ghost_vessels[vessel_id] -= owner
+ if(!length(GLOB.active_ghost_vessels[vessel_id]))
+ GLOB.active_ghost_vessels -= vessel_id // clean up empty keys
if(owner)
REMOVE_TRAIT(owner, TRAIT_STASIS, REF(src))
REMOVE_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT)
@@ -55,7 +61,10 @@
possess_vessel(chosen)
else
owner.balloon_alert_to_viewers("This vessel awaits a soul...")
- add_ghost_verb()
+ if(!GLOB.active_ghost_vessels[vessel_id])
+ GLOB.active_ghost_vessels[vessel_id] = list()
+ GLOB.active_ghost_vessels[vessel_id] += owner // store the mob, not the component
+
/datum/component/ghost_vessel/proc/possess_vessel(mob/dead/observer/ghost)
if(!ghost?.client)
@@ -67,23 +76,3 @@
REMOVE_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT)
owner.key = ghost.client.key
qdel(src)
-
-/datum/component/ghost_vessel/proc/add_ghost_verb()
- RegisterSignal(owner, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine_by_ghost))
-
-/datum/component/ghost_vessel/proc/on_examine_by_ghost(datum/source, mob/user, list/examine_list)
- SIGNAL_HANDLER
- if(!istype(user, /mob/dead/observer))
- return
- examine_list += span_notice("This vessel is empty. Inhabit it (Orbit Mob)?")
- RegisterSignal(user, COMSIG_ATOM_ORBIT_BEGIN, PROC_REF(on_ghost_ctrl_click)) // alt: add a verb
-
-/datum/component/ghost_vessel/proc/on_ghost_ctrl_click(datum/source, mob/living/clicker)
- var/option = browser_input_list(clicker, "Do you wish to possess this vessel??", "XYLIX", DEFAULT_INPUT_CHOICES)
- if(!option)
- return
- if(!istype(clicker, /mob/dead/observer))
- return
- if(!being_offered)
- return
- INVOKE_ASYNC(src, PROC_REF(possess_vessel), clicker)
diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm
index 007242194bf..2158714728b 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -180,6 +180,19 @@ GLOBAL_LIST_INIT(roleplay_readme, world.file2list("strings/rt/Lore_Primer.txt"))
to_chat(usr, span_boldwarning("You are in the migrant queue."))
return
+ if(href_list["PossessVessel"])
+ var/id = href_list["PossessVessel"]
+ var/list/group = GLOB.active_ghost_vessels[id]
+ if(!length(group))
+ to_chat(src, span_warning("No vessels of that type are available."))
+ return
+ var/mob/living/carbon/human/vessel_mob = pick(group)
+ var/datum/component/ghost_vessel/gc = vessel_mob.GetComponent(/datum/component/ghost_vessel)
+ if(!gc || !gc.being_offered)
+ to_chat(src, span_warning("That vessel is no longer available."))
+ return
+ gc.possess_vessel(src)
+
if(href_list["late_join"])
if(!SSticker?.IsRoundInProgress())
to_chat(usr, "The game is starting. You cannot join yet.")
@@ -570,6 +583,17 @@ GLOBAL_LIST_INIT(roleplay_readme, world.file2list("strings/rt/Lore_Primer.txt"))
column_counter++
if(column_counter > 0 && (column_counter % 4 == 0))
dat += "
"
+ if(length(GLOB.active_ghost_vessels))
+ dat += " "
+ column_counter++
+ if(column_counter > 0 && (column_counter % 4 == 0))
+ dat += " | "
+
dat += " | "
dat += ""
var/datum/browser/popup = new(src, "latechoices", "Choose Class", 720, 580)
From a08fe1dfdd3799181a2e97658052cbccc175a68e Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:02:53 -0800
Subject: [PATCH 04/73] Update ghost_vessel.dm
---
code/datums/components/ghost_vessel.dm | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm
index 13d7692d836..3cd43a13196 100644
--- a/code/datums/components/ghost_vessel.dm
+++ b/code/datums/components/ghost_vessel.dm
@@ -15,6 +15,10 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels)
ADD_TRAIT(owner, TRAIT_STASIS, REF(src))
ADD_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT)
ADD_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT)
+ if(!vessel_item_type)
+ INVOKE_ASYNC(src, PROC_REF(begin_ghost_offer))
+ return
+
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby))
RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_deleted))
From 2164fbda4aa2adcf1dbf09b04c26c28caff90bca Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:20:48 -0800
Subject: [PATCH 05/73] adds whitelist system that we can use for later adds it
to vessels
---
code/__DEFINES/whitelists.dm | 1 +
code/__HELPERS/game.dm | 22 +++
code/datums/components/ghost_vessel.dm | 9 +-
code/modules/admin/admin.dm | 1 +
code/modules/admin/granual_whitelist.dm | 135 ++++++++++++++++++
code/modules/admin/holder2.dm | 2 +
code/modules/admin/topic.dm | 6 +
.../modules/mob/dead/new_player/new_player.dm | 25 ++--
vanderlin.dme | 2 +
9 files changed, 191 insertions(+), 12 deletions(-)
create mode 100644 code/__DEFINES/whitelists.dm
create mode 100644 code/modules/admin/granual_whitelist.dm
diff --git a/code/__DEFINES/whitelists.dm b/code/__DEFINES/whitelists.dm
new file mode 100644
index 00000000000..2b7c222b83a
--- /dev/null
+++ b/code/__DEFINES/whitelists.dm
@@ -0,0 +1 @@
+#define WHITELIST_AUTOMATON "Automaton"
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index b0d4ebfaff0..b2b6642bc08 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -254,6 +254,22 @@
return pollCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, flashwindow, candidates)
+
+/proc/pollGhostCandidatesWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, ignore_category = null, flashwindow = TRUE, new_players = FALSE, whitelist_type)
+ var/list/candidates = list()
+
+ for(var/mob/dead/observer/G in GLOB.player_list)
+ if(G.client.is_whitelisted(whitelist_type))
+ candidates += G
+ if(new_players)
+ for(var/mob/dead/new_player/G as anything in GLOB.new_player_list)
+ if(!G.client)
+ continue
+ candidates += G
+
+ return pollCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, flashwindow, candidates)
+
+
/proc/pollCandidates(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, ignore_category = null, flashwindow = TRUE, list/group = null)
var/time_passed = world.time
if (!Question)
@@ -286,6 +302,12 @@
return list()
return L
+/proc/pollCandidatesForMobWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, mob/M, ignore_category = null, new_players = FALSE, whitelist_type)
+ var/list/L = pollGhostCandidatesWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, new_players = new_players, wnitelist_type = whitelist_type)
+ if(!M || QDELETED(M) || !M.loc)
+ return list()
+ return L
+
/proc/pollCandidatesForMobs(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, list/mobs, ignore_category = null)
var/list/L = pollGhostCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category)
var/i=1
diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm
index 3cd43a13196..af1ff09f74b 100644
--- a/code/datums/components/ghost_vessel.dm
+++ b/code/datums/components/ghost_vessel.dm
@@ -4,9 +4,9 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels)
var/obj/item/vessel_item_type
var/mob/living/carbon/human/owner
var/being_offered = FALSE
- var/vessel_id = "Automaton"
+ var/vessel_id = WHITELIST_AUTOMATON
-/datum/component/ghost_vessel/Initialize(obj/item/item_type, id = "Automaton")
+/datum/component/ghost_vessel/Initialize(obj/item/item_type, id = WHITELIST_AUTOMATON)
if(!isliving(parent))
return COMPONENT_INCOMPATIBLE
owner = parent
@@ -49,7 +49,7 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels)
/datum/component/ghost_vessel/proc/begin_ghost_offer()
being_offered = TRUE
- var/list/candidates = pollCandidatesForMob(
+ var/list/candidates = pollCandidatesForMobWhitelisted(
"A vessel at [owner.loc] awaits a soul. Do you wish to inhabit it?",
null,
null,
@@ -57,7 +57,8 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels)
100,
parent,
POLL_IGNORE_GOLEM,
- new_players = TRUE
+ new_players = TRUE,
+ whitelist_type = vessel_id,
)
if(length(candidates))
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index 326bd5005ed..343b37aa246 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -114,6 +114,7 @@
body += "\[Check Triumphs\] "
body += "
"
body += "\[Role Ban Panel\] "
+ body += "Whitelists - "
var/patron = ""
if(isliving(M))
diff --git a/code/modules/admin/granual_whitelist.dm b/code/modules/admin/granual_whitelist.dm
new file mode 100644
index 00000000000..80cae97d622
--- /dev/null
+++ b/code/modules/admin/granual_whitelist.dm
@@ -0,0 +1,135 @@
+/datum/whitelist_panel
+ var/datum/admins/holder
+ var/selected_ckey = null
+
+/datum/whitelist_panel/New(datum/admins/passed_holder)
+ holder = passed_holder
+ return ..()
+
+/datum/whitelist_panel/proc/show_ui(mob/user, forced_key)
+ if(forced_key)
+ selected_ckey = forced_key
+
+ var/list/dat = list()
+ dat += "Whitelist Panel
"
+ dat += "CKEY: [selected_ckey] Change
"
+
+ if(selected_ckey)
+ dat += "Current Whitelists for [selected_ckey]:
"
+ var/list/all_whitelists = get_all_whitelist_ids()
+ for(var/wl_id in all_whitelists)
+ var/datum/save_manager/SM = get_save_manager(selected_ckey)
+ var/data = SM ? SM.get_data("whitelists", wl_id, null) : null
+ var/has_wl = islist(data) && data["granted"]
+ dat += " - [wl_id]: [has_wl ? "Granted" : "Not Granted"]"
+ if(islist(data))
+ if(has_wl)
+ dat += " (by [data["granted_by"]] on [time2text(data["granted_on"], "DD/MM/YYYY")])"
+ else if(data["granted_by"])
+ dat += " (revoked by [data["granted_by"]] on [time2text(data["revoked_on"], "DD/MM/YYYY")])"
+ if(has_wl)
+ dat += " Remove"
+ else
+ dat += " Grant"
+ dat += "
"
+
+ dat += "
Add Custom Whitelist ID"
+
+ var/datum/browser/popup = new(user, "whitelist_panel", "Whitelist Panel", 400, 400)
+ popup.set_content(dat.Join())
+ popup.open()
+
+/datum/whitelist_panel/proc/get_all_whitelist_ids()
+ return list(
+ WHITELIST_AUTOMATON,
+ //as we add more we can fill it out here
+ )
+
+/datum/whitelist_panel/Topic(href, list/href_list)
+ . = ..()
+ if(!holder)
+ return
+ var/mob/user = usr
+ if(holder.owner != user.client)
+ return
+ if(!check_rights_for(user.client, R_ADMIN))
+ to_chat(user, span_boldwarning("No admin permission"))
+ return
+
+ switch(href_list["task"])
+ if("ckey")
+ var/chosen_ckey = input(user, "Enter ckey", "CKEY", selected_ckey) as text|null
+ if(!chosen_ckey)
+ return
+ selected_ckey = ckey(chosen_ckey)
+
+ if("add_whitelist")
+ if(!selected_ckey)
+ to_chat(user, span_boldwarning("No ckey selected."))
+ return
+ var/wl_id = href_list["wl_id"]
+ grant_whitelist(user, selected_ckey, wl_id)
+
+ if("remove_whitelist")
+ if(!selected_ckey)
+ to_chat(user, span_boldwarning("No ckey selected."))
+ return
+ var/wl_id = href_list["wl_id"]
+ revoke_whitelist(user, selected_ckey, wl_id)
+
+ if("add_custom")
+ if(!selected_ckey)
+ to_chat(user, span_boldwarning("No ckey selected."))
+ return
+ var/wl_id = input(user, "Enter custom whitelist ID (must match vessel_id exactly)", "Custom Whitelist", "") as text|null
+ if(!wl_id)
+ return
+ grant_whitelist(user, selected_ckey, wl_id)
+
+ show_ui(user)
+
+/datum/whitelist_panel/proc/grant_whitelist(mob/user, target_ckey, wl_id)
+ var/datum/save_manager/SM = get_save_manager(target_ckey)
+ if(!SM)
+ to_chat(user, span_boldwarning("Could not load save manager for [target_ckey]."))
+ return
+ SM.set_data("whitelists", wl_id, list(
+ "granted" = TRUE,
+ "granted_by" = ckey(user.ckey),
+ "granted_on" = world.realtime
+ ))
+ var/msg = "[key_name_admin(user)] granted whitelist '[wl_id]' to [target_ckey]"
+ message_admins(msg)
+ log_admin(msg)
+
+/datum/whitelist_panel/proc/revoke_whitelist(mob/user, target_ckey, wl_id)
+ var/datum/save_manager/SM = get_save_manager(target_ckey)
+ if(!SM)
+ to_chat(user, span_boldwarning("Could not load save manager for [target_ckey]."))
+ return
+ SM.set_data("whitelists", wl_id, list(
+ "granted" = FALSE,
+ "granted_by" = ckey(user.ckey),
+ "revoked_on" = world.realtime
+ ))
+ var/msg = "[key_name_admin(user)] revoked whitelist '[wl_id]' from [target_ckey]"
+ message_admins(msg)
+ log_admin(msg)
+
+/client/proc/is_whitelisted(whitelist_id)
+ var/datum/save_manager/SM = get_save_manager(ckey)
+ if(!SM)
+ return FALSE
+ var/data = SM.get_data("whitelists", whitelist_id, null)
+ if(!islist(data))
+ return FALSE
+ return data["granted"]
+
+/proc/is_whitelisted_for(target_ckey, whitelist_id)
+ var/datum/save_manager/SM = get_save_manager(target_ckey)
+ if(!SM)
+ return FALSE
+ var/data = SM.get_data("whitelists", whitelist_id, null)
+ if(!islist(data))
+ return FALSE
+ return data["granted"]
diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm
index df331b46a89..11b5d0134ab 100644
--- a/code/modules/admin/holder2.dm
+++ b/code/modules/admin/holder2.dm
@@ -25,6 +25,7 @@ GLOBAL_PROTECT(href_token)
var/deadmined
var/datum/role_ban_panel/role_ban_panel
+ var/datum/whitelist_panel/WP
var/datum/pathfind_debug/path_debug
var/datum/create_wave/create_wave
@@ -49,6 +50,7 @@ GLOBAL_PROTECT(href_token)
admin_signature = "Nanotrasen Officer #[rand(0,9)][rand(0,9)][rand(0,9)]"
href_token = GenerateToken()
role_ban_panel = new /datum/role_ban_panel(src)
+ WP = new /datum/whitelist_panel(src)
if(R.rights & R_DEBUG) //grant profile access
world.SetConfig("APP/admin", ckey, "role=admin")
//only admins with +ADMIN start admined
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index d099fb661a5..983862b307a 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -1329,6 +1329,12 @@
mind_of_mob.set_assigned_role(new_job)
+ else if(href_list["open_whitelist_panel"])
+ var/mob/M = locate(href_list["open_whitelist_panel"])
+ if(!M?.ckey)
+ return
+ WP.show_ui(usr, ckey(M.ckey))
+
else if(href_list["roleban"])
if(!check_rights(R_ADMIN))
return
diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm
index 2158714728b..5f3ea967431 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -182,6 +182,9 @@ GLOBAL_LIST_INIT(roleplay_readme, world.file2list("strings/rt/Lore_Primer.txt"))
if(href_list["PossessVessel"])
var/id = href_list["PossessVessel"]
+ if(!client.is_whitelisted(id))
+ to_chat(src, span_boldwarning("You are not whitelisted for [id]."))
+ return
var/list/group = GLOB.active_ghost_vessels[id]
if(!length(group))
to_chat(src, span_warning("No vessels of that type are available."))
@@ -584,15 +587,21 @@ GLOBAL_LIST_INIT(roleplay_readme, world.file2list("strings/rt/Lore_Primer.txt"))
if(column_counter > 0 && (column_counter % 4 == 0))
dat += ""
if(length(GLOB.active_ghost_vessels))
- dat += " "
- column_counter++
- if(column_counter > 0 && (column_counter % 4 == 0))
- dat += " | "
+ if(client.is_whitelisted(id))
+ available_vessel_ids += id
+
+ if(length(available_vessel_ids))
+ dat += " "
+ column_counter++
+ if(column_counter > 0 && (column_counter % 4 == 0))
+ dat += " | "
dat += " | "
dat += ""
diff --git a/vanderlin.dme b/vanderlin.dme
index 4da9f648af3..60bb29940be 100644
--- a/vanderlin.dme
+++ b/vanderlin.dme
@@ -195,6 +195,7 @@
#include "code\__DEFINES\weaponsounds.dm"
#include "code\__DEFINES\weights.dm"
#include "code\__DEFINES\werewolf.dm"
+#include "code\__DEFINES\whitelists.dm"
#include "code\__DEFINES\wires.dm"
#include "code\__DEFINES\ai\_ai.dm"
#include "code\__DEFINES\ai\hostile.dm"
@@ -2028,6 +2029,7 @@
#include "code\modules\admin\create_turf.dm"
#include "code\modules\admin\create_wave.dm"
#include "code\modules\admin\fun_balloon.dm"
+#include "code\modules\admin\granual_whitelist.dm"
#include "code\modules\admin\greyscale_modifiy_menu.dm"
#include "code\modules\admin\holder2.dm"
#include "code\modules\admin\IsBanned.dm"
From bba441d3118720ed5cf3ffc4675a859406d2ffbc Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:21:59 -0800
Subject: [PATCH 06/73] adds a prefilled variant of the vessel
---
.../carbon/human/species_types/automatons/_automaton.dm | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
index 60e5e8be763..d32a1dec844 100644
--- a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
+++ b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
@@ -6,6 +6,10 @@
. = ..()
AddComponent(/datum/component/ghost_vessel, /obj/item/reagent_containers/lux)
+/mob/living/carbon/human/species/automaton/prefilled_vessel/LateInitialize()
+ . = ..()
+ AddComponent(/datum/component/ghost_vessel)
+
/datum/species/automaton
name = "Automaton"
id = SPEC_ID_AUTOMATON
From c56e2c09855b09f79a9f5346884bd5ec7b5ca122 Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:23:25 -0800
Subject: [PATCH 07/73] Update ghost_vessel.dm
---
code/datums/components/ghost_vessel.dm | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm
index af1ff09f74b..c8107bb53f5 100644
--- a/code/datums/components/ghost_vessel.dm
+++ b/code/datums/components/ghost_vessel.dm
@@ -16,7 +16,11 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels)
ADD_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT)
ADD_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT)
if(!vessel_item_type)
- INVOKE_ASYNC(src, PROC_REF(begin_ghost_offer))
+ being_offered = TRUE
+ owner.balloon_alert_to_viewers("This vessel awaits a soul...")
+ if(!GLOB.active_ghost_vessels[vessel_id])
+ GLOB.active_ghost_vessels[vessel_id] = list()
+ GLOB.active_ghost_vessels[vessel_id] += owner // store the mob, not the component
return
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby))
From 9328b5e8e8ca9f7adfa7b714bcf7777f402a19fb Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:27:11 -0800
Subject: [PATCH 08/73] Update game.dm
---
code/__HELPERS/game.dm | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index b2b6642bc08..850855c0f1c 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -303,7 +303,7 @@
return L
/proc/pollCandidatesForMobWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, mob/M, ignore_category = null, new_players = FALSE, whitelist_type)
- var/list/L = pollGhostCandidatesWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, new_players = new_players, wnitelist_type = whitelist_type)
+ var/list/L = pollGhostCandidatesWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, new_players = new_players, whitelist_type = whitelist_type)
if(!M || QDELETED(M) || !M.loc)
return list()
return L
From 8eace221aa0d5b088a913fec1179d3f951837970 Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:34:19 -0800
Subject: [PATCH 09/73] Update vanderlin.dmm
---
_maps/map_files/vanderlin/vanderlin.dmm | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/_maps/map_files/vanderlin/vanderlin.dmm b/_maps/map_files/vanderlin/vanderlin.dmm
index 375113e0518..7cf87f294c0 100644
--- a/_maps/map_files/vanderlin/vanderlin.dmm
+++ b/_maps/map_files/vanderlin/vanderlin.dmm
@@ -9143,6 +9143,10 @@
},
/turf/open/floor/ruinedwood/spiral,
/area/indoors/town/theatre)
+"etk" = (
+/mob/living/carbon/human/species/automaton/prefilled_vessel,
+/turf/open/floor/blocks/stonered/tiny,
+/area/under/town/basement)
"etw" = (
/obj/machinery/light/fueled/firebowl,
/turf/open/floor/ruinedwood/darker,
@@ -29490,7 +29494,6 @@
/obj/structure/door/violet{
name = "Royal Guard's Storage"
},
-/obj/effect/mapping_helpers/access/locker,
/obj/effect/mapping_helpers/access/keyset/manor/atarms,
/turf/open/floor/cobble,
/area/under/town/basement)
@@ -76700,7 +76703,7 @@ qFx
qFx
dHC
vpQ
-dIz
+etk
jTO
oqo
jTO
From cfc3f2a67d105bcb21386e4d2c829447c1613de6 Mon Sep 17 00:00:00 2001
From: dwasint <82520990+dwasint@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:48:46 -0800
Subject: [PATCH 10/73] adds job boost and roundstart support to vessel targets
---
_maps/map_files/debug/roguetest.dmm | 6 +-
code/controllers/subsystem/ticker.dm | 63 ++++++-
code/modules/admin/granual_whitelist.dm | 4 +-
.../client/preferences/_preferences.dm | 173 +++++++++---------
code/modules/client/save_system/job_boosts.dm | 7 +
5 files changed, 163 insertions(+), 90 deletions(-)
diff --git a/_maps/map_files/debug/roguetest.dmm b/_maps/map_files/debug/roguetest.dmm
index d9504e83d6a..814d0b18a0c 100644
--- a/_maps/map_files/debug/roguetest.dmm
+++ b/_maps/map_files/debug/roguetest.dmm
@@ -1403,6 +1403,10 @@
/obj/structure/closet/crate/miningcar,
/turf/open/floor/grass/yel,
/area/outdoors)
+"ke" = (
+/mob/living/carbon/human/species/automaton/prefilled_vessel,
+/turf/open/floor/wood,
+/area/indoors/town/keep)
"ki" = (
/obj/structure/rotation_piece/cog{
dir = 4
@@ -16632,7 +16636,7 @@ QN
Wr
QN
zg
-GB
+ke
GB
vD
vD
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index eab98c59c2c..ce9dfa69d8d 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -318,16 +318,16 @@ SUBSYSTEM_DEF(ticker)
var/init_start = world.timeofday
CHECK_TICK
- //Configure mode and assign player to special mode stuff
- var/can_continue = 0
- CHECK_TICK
+ // Vessel assignment happens first, removes those players from the pool
+ assign_vessel_players()
- can_continue = SSgamemode.pre_setup()
+ CHECK_TICK
+ var/can_continue = SSgamemode.pre_setup()
CHECK_TICK
- can_continue = can_continue && SSjob.DivideOccupations(list()) //Distribute jobs
+ can_continue = can_continue && SSjob.DivideOccupations(list()) //Distribute jobs
CHECK_TICK
log_game("GAME SETUP: Divide Occupations success")
@@ -399,6 +399,59 @@ SUBSYSTEM_DEF(ticker)
return TRUE
+/datum/controller/subsystem/ticker/proc/assign_vessel_players()
+ var/list/vessel_candidates = list()
+
+ for(var/mob/dead/new_player/player in GLOB.new_player_list)
+ if(!player?.client)
+ continue
+ if(player.ready != PLAYER_READY_TO_PLAY)
+ continue
+ for(var/id in GLOB.vessel_ids)
+ if(!(id in player.client.prefs.be_special))
+ continue
+ if(!player.client.is_whitelisted(id))
+ continue
+ if(!vessel_candidates[id])
+ vessel_candidates[id] = list()
+ vessel_candidates[id] += player
+ break
+
+ for(var/id in vessel_candidates)
+ var/list/vessel_mobs = GLOB.active_ghost_vessels[id]
+ if(!length(vessel_mobs))
+ continue
+
+ // Build weighted list using boost system, respecting vessel_id
+ var/list/weighted_players = list()
+ for(var/mob/dead/new_player/player in vessel_candidates[id])
+ var/player_weight = 1
+ for(var/datum/job_priority_boost/boost in SSjob.get_player_boosts(player))
+ if(boost.can_boost_vessel(id))
+ player_weight += boost.boost_amount
+ weighted_players[player] = player_weight
+
+ while(length(weighted_players) && length(vessel_mobs))
+ var/mob/dead/new_player/player = pickweight(weighted_players)
+ weighted_players -= player
+
+ var/mob/living/carbon/human/vessel_mob = pick(vessel_mobs)
+ var/datum/component/ghost_vessel/gc = vessel_mob.GetComponent(/datum/component/ghost_vessel)
+ if(!gc)
+ vessel_mobs -= vessel_mob
+ continue
+
+ // Consume the first applicable boost, same as DO does
+ for(var/datum/job_priority_boost/boost in SSjob.get_player_boosts(player))
+ if(boost.can_boost_vessel(id))
+ boost.use_boost()
+ break
+
+ gc.possess_vessel(player)
+ vessel_mobs -= vessel_mob
+ GLOB.new_player_list -= player
+ log_game("Assigned [player.ckey] to vessel '[id]' ([vessel_mob.name])")
+
/datum/controller/subsystem/ticker/proc/PostSetup()
set waitfor = FALSE
diff --git a/code/modules/admin/granual_whitelist.dm b/code/modules/admin/granual_whitelist.dm
index 80cae97d622..1e352d4650c 100644
--- a/code/modules/admin/granual_whitelist.dm
+++ b/code/modules/admin/granual_whitelist.dm
@@ -1,3 +1,5 @@
+GLOBAL_LIST_INIT(vessel_ids, list(WHITELIST_AUTOMATON))
+
/datum/whitelist_panel
var/datum/admins/holder
var/selected_ckey = null
@@ -16,7 +18,7 @@
if(selected_ckey)
dat += "Current Whitelists for [selected_ckey]:
"
- var/list/all_whitelists = get_all_whitelist_ids()
+ var/list/all_whitelists = GLOB.vessel_ids
for(var/wl_id in all_whitelists)
var/datum/save_manager/SM = get_save_manager(selected_ckey)
var/data = SM ? SM.get_data("whitelists", wl_id, null) : null
diff --git a/code/modules/client/preferences/_preferences.dm b/code/modules/client/preferences/_preferences.dm
index cffa9b168ae..f15740bf2d4 100644
--- a/code/modules/client/preferences/_preferences.dm
+++ b/code/modules/client/preferences/_preferences.dm
@@ -439,8 +439,8 @@ GLOBAL_LIST_INIT(name_adjustments, list())
.v-color-box { top: 136px; left: 34px; width: 48px; height: 15px; background-image: url('voice_colour.png'); }
.v-blob { top: 4px; left: 35px; width: 8px; height: 7px;
- background-image: url('voice_colour_blob.png');
- background-blend-mode: multiply; }
+ background-image: url('voice_colour_blob.png');
+ background-blend-mode: multiply; }
.menu-keybinds {
top: 280px;
@@ -559,7 +559,7 @@ GLOBAL_LIST_INIT(name_adjustments, list())
var silhouette = document.getElementById('silhouette');
silhouette.style.backgroundImage = "url('features_bodytype_" + data.gender + ".png')";
if (data.gender === "F") silhouette.style.width = "15px";
- if (data.gender === "M") silhouette.style.width = "18px";
+ if (data.gender === "M") silhouette.style.width = "18px";
}
// Update voice color blob
@@ -1108,49 +1108,49 @@ GLOBAL_LIST_INIT(name_adjustments, list())
/datum/preferences/proc/update_job_preference(mob/user, role, desiredLvl)
- if(!SSjob || !length(SSjob.joinable_occupations))
- return
- var/datum/job/job = SSjob.GetJob(role)
- if(!job || !(job.job_flags & JOB_NEW_PLAYER_JOINABLE))
- user << browse(null, "window=mob_occupation")
- update_menu_data(user, list("job"))
- return
- if(!isnum(desiredLvl))
- to_chat(user, "update_job_preference - desired level was not a number. Please notify coders!")
- CRASH("update_job_preference called with desiredLvl value of [isnull(desiredLvl) ? "null" : desiredLvl]")
-
- var/jpval = null
- // desiredLvl comes from the links: 1=High, 2=Medium, 3=Low, 4=NEVER
- // JP constants: JP_LOW=1, JP_MEDIUM=2, JP_HIGH=3
- switch(desiredLvl)
- if(1)
- jpval = JP_HIGH // 3
- if(2)
- jpval = JP_MEDIUM // 2
- if(3)
- jpval = JP_LOW // 1
- if(4)
- jpval = null // NEVER
-
- var/was_high = (jpval == JP_HIGH)
- var/previous_high_job = null
-
- if(was_high)
- for(var/job_title in job_preferences)
- if(job_preferences[job_title] == JP_HIGH)
- previous_high_job = job_title
- break
-
- set_job_preference_level(job, jpval)
-
- // Send back the desiredLvl value directly since that's what JavaScript expects
- update_job_display(user, role, desiredLvl)
-
- if(was_high && previous_high_job && previous_high_job != role)
- update_job_display(user, previous_high_job, 2) // Medium
-
- update_menu_data(user, list("job"))
- return 1
+ if(!SSjob || !length(SSjob.joinable_occupations))
+ return
+ var/datum/job/job = SSjob.GetJob(role)
+ if(!job || !(job.job_flags & JOB_NEW_PLAYER_JOINABLE))
+ user << browse(null, "window=mob_occupation")
+ update_menu_data(user, list("job"))
+ return
+ if(!isnum(desiredLvl))
+ to_chat(user, "update_job_preference - desired level was not a number. Please notify coders!")
+ CRASH("update_job_preference called with desiredLvl value of [isnull(desiredLvl) ? "null" : desiredLvl]")
+
+ var/jpval = null
+ // desiredLvl comes from the links: 1=High, 2=Medium, 3=Low, 4=NEVER
+ // JP constants: JP_LOW=1, JP_MEDIUM=2, JP_HIGH=3
+ switch(desiredLvl)
+ if(1)
+ jpval = JP_HIGH // 3
+ if(2)
+ jpval = JP_MEDIUM // 2
+ if(3)
+ jpval = JP_LOW // 1
+ if(4)
+ jpval = null // NEVER
+
+ var/was_high = (jpval == JP_HIGH)
+ var/previous_high_job = null
+
+ if(was_high)
+ for(var/job_title in job_preferences)
+ if(job_preferences[job_title] == JP_HIGH)
+ previous_high_job = job_title
+ break
+
+ set_job_preference_level(job, jpval)
+
+ // Send back the desiredLvl value directly since that's what JavaScript expects
+ update_job_display(user, role, desiredLvl)
+
+ if(was_high && previous_high_job && previous_high_job != role)
+ update_job_display(user, previous_high_job, 2) // Medium
+
+ update_menu_data(user, list("job"))
+ return 1
/datum/preferences/proc/reset_jobs(mob/user, silent = FALSE)
job_preferences = list()
@@ -1270,31 +1270,38 @@ GLOBAL_LIST_INIT(name_adjustments, list())
/datum/preferences/proc/set_antag(mob/user)
var/list/dat = list()
-
dat += ""
dat += "Done"
dat += "Villains
"
-
if(is_total_antag_banned(user.ckey))
dat += "I am banned from antagonist roles.
"
src.be_special = list()
-
for (var/i in GLOB.special_roles_rogue)
if(is_antag_banned(user.ckey, i))
dat += "[capitalize(i)]: BANNED
"
else
var/days_remaining = null
- if(ispath(GLOB.special_roles_rogue[i]) && CONFIG_GET(flag/use_age_restriction_for_jobs)) //If it's a game mode antag, check if the player meets the minimum age
+ if(ispath(GLOB.special_roles_rogue[i]) && CONFIG_GET(flag/use_age_restriction_for_jobs))
days_remaining = get_remaining_days(user.client)
-
if(days_remaining)
- dat += "[capitalize(i)]: \[IN [days_remaining] DAYS\]
"
+ dat += "[capitalize(i)]: \[IN [days_remaining] DAYS__~~\]~~__
"
else
dat += "[capitalize(i)]: [(i in be_special) ? "Enabled" : "Disabled"]
"
- dat += ""
+ var/list/vessel_ids = GLOB.vessel_ids
+ var/list/available_vessel_ids = list()
+ for(var/id in vessel_ids)
+ if(user.client.is_whitelisted(id))
+ available_vessel_ids += id
+
+ if(length(available_vessel_ids))
+ dat += "Vessels
"
+ for(var/id in available_vessel_ids)
+ var/enabled = (id in be_special)
+ dat += "[id]: [enabled ? "Enabled" : "Disabled"]
"
- var/datum/browser/noclose/popup = new(user, "antag_setup", "Special Roles
", 265, 340) //no reason not to reuse the occupation window, as it's cleaner that way
+ dat += "", 265, 340)
popup.set_window_options(can_close = FALSE)
popup.set_content(dat.Join())
popup.open(FALSE)
@@ -2384,22 +2391,22 @@ GLOBAL_LIST_INIT(name_adjustments, list())