Ghostroles In Lobby Manifest (#21772)

This PR makes it so that player characters who are active in the
ghostrole list will show up on the Crew Manifest, but only if you the
player are not currently in-round. Any type of Observer, including
ghosts, aghosts, and actually being in the lobby, all allow the user to
view Off-ship characters in the manifest. If you are not any kind of
observer, the Crew Manifest will only show Horizon characters.


I have tested this PR. Behold.


https://github.com/user-attachments/assets/76ffb40a-bb67-4719-a2e7-df04f38b929e
This commit is contained in:
VMSolidus
2026-02-03 10:09:49 +00:00
committed by GitHub
parent 6ca48ee04c
commit 4d43d53c6f
7 changed files with 140 additions and 31 deletions
+39 -2
View File
@@ -7,7 +7,18 @@ SUBSYSTEM_DEF(ghostroles)
// -> type 1 -> spawnpoint 1
// -> spawnpoint 2
var/list/spawners = list() //List of the available spawner datums
/**
* List of all available spawner datums that exist in the code.
* This includes spawner datums that might not currently exist anywhere in-game.
*/
var/list/spawners = list()
/**
* The set of all ghostroles that have been taken by players.
* As a weakref list, these are not guaranteed to factually exist, only that these did at some point exist.
* The set of all mobs here should be retrieved via SSghostroles.get_ghostrole_mobs()
*/
var/list/datum/weakref/spawned_ghostrole_mobs = list()
// For special spawners that have mobile or object spawnpoints
var/list/spawn_types = list("Golems", "Borers")
@@ -189,7 +200,12 @@ SUBSYSTEM_DEF(ghostroles)
if(!S.post_spawn(M))
to_chat(usr, "Unable to spawn: post_spawn failed. Report this on GitHub")
return
LAZYADD(S.spawned_mobs, WEAKREF(M))
var/spawned_weakref = WEAKREF(M)
// Add the spawned mobs to this spawner's list.
LAZYADD(S.spawned_mobs, spawned_weakref)
// Then also add the spawned mob to the subsystem's list.
LAZYADD(spawned_ghostrole_mobs, spawned_weakref)
SSrecords.reset_manifest()
log_and_message_admins("joined as GhostRole: [S.name]", M)
SStgui.update_uis(src)
. = TRUE
@@ -269,3 +285,24 @@ SUBSYSTEM_DEF(ghostroles)
if(spawner_name in spawners)
return spawners[spawner_name]
return null
/**
* Returns a /list/mob containing all mobs that were created by ghostroles and currently exist.
* It will also delete any mob found to be nonexistent from the list for faster future lookups.
*/
/datum/controller/subsystem/ghostroles/proc/get_ghostrole_mobs()
var/list/mob/found_mobs = list()
for (var/datum/weakref/nullable_spawn in spawned_ghostrole_mobs)
var/possible_spawn = nullable_spawn.resolve()
if (!possible_spawn || !ismob(possible_spawn))
// Clear weakrefs from the list that are discovered to be nonexistent.
// So that future calls to this proc have fewer entries to enumerate over.
// Also do this if for whatever baffling reason a non-mob was shoved into the list.
qdel(nullable_spawn)
spawned_ghostrole_mobs.Remove(nullable_spawn)
continue
var/mob/found_spawn = possible_spawn
found_mobs.Add(found_spawn)
return found_mobs
+88 -28
View File
@@ -233,6 +233,7 @@ SUBSYSTEM_DEF(records)
var/list/data = list()
data["manifest"] = SSrecords.get_manifest_list()
data["allow_follow"] = isghost(user)
data["show_ooc_roles"] = isabstractmob(usr)
return data
/datum/controller/subsystem/records/proc/open_manifest_tgui(mob/user, datum/tgui/ui)
@@ -271,43 +272,102 @@ SUBSYSTEM_DEF(records)
if(!SSjobs)
log_world("ERROR: SSjobs not available, cannot build manifest")
return
// No pre-existing manifest, setup an empty list of all possible departments.
manifest = DEPARTMENTS_LIST_INIT
/* ----- START OF CASE FOR CREW ----- */
// Start with the "Crew", which are all of the IC manifest members.
for(var/datum/record/general/general_record in records)
var/name = sanitize(general_record.name, encode = FALSE)
var/rank = sanitize(general_record.rank, encode = FALSE)
var/real_rank = make_list_rank(general_record.real_rank)
var/datum/job/job = SSjobs.GetJob(real_rank)
var/activity_state = get_activity_state(general_record)
var/list/departments
if(istype(job) && job.departments.len > 0 && all_in_list(job.departments, manifest))
departments = job.departments
else // no department set or there's something weird
departments = list(DEPARTMENT_MISCELLANEOUS = JOBROLE_DEFAULT)
var/datum/job/job = SSjobs.GetJob(make_list_rank(general_record.real_rank))
var/list/departments = /* Jobs can be in more than one department. So we fact check that the departments are real. */\
(istype(job) && job.departments.len > 0 && all_in_list(job.departments, manifest))\
/* Accept the departments if they're real. */\
? job.departments \
/* Dump any jobs with invalid departments into a fallback holding pen. */\
: list(DEPARTMENT_MISCELLANEOUS = JOBROLE_DEFAULT)
for(var/department in departments) // add them to their departments
var/supervisor = departments[department] & JOBROLE_SUPERVISOR
manifest[department][++manifest[department].len] = list("name" = name, "rank" = rank, "active" = activity_state, "head" = supervisor)
manifest[department][++manifest[department].len] = list(\
"name" = general_record.name, \
"rank" = general_record.rank, \
"active" = get_activity_state(general_record), \
"head" = supervisor, \
"ooc_role" = FALSE)
if(supervisor) // they are a supervisor/head, put them on top
manifest[department].Swap(1, manifest[department].len)
// silicons are not in records, we need to add them manually
var/dept = DEPARTMENT_EQUIPMENT
for(var/mob/living/silicon/S in GLOB.player_list)
if(istype(S, /mob/living/silicon/robot))
var/mob/living/silicon/robot/R = S
if(R.scrambled_codes)
continue
var/selected_module = "Default Module"
if(R.module)
selected_module = capitalize_first_letters(R.module.name)
manifest[dept][++manifest[dept].len] = list("name" = sanitize(R.name), "rank" = selected_module, "active" = "Online", "head" = FALSE)
else if(istype(S, /mob/living/silicon/ai))
var/mob/living/silicon/ai/A = S
manifest[dept][++manifest[dept].len] = list("name" = sanitize(A.name), "rank" = "Vessel Intelligence", "active" = "Online", "head" = TRUE)
manifest[dept].Swap(1, manifest[dept].len)
/* ----- END OF CASE FOR CREW ----- */
/* ----- START OF CASE FOR SILICONS ----- */
for(var/mob/living/silicon/S in GLOB.player_list)
// Case for Cyborgs
if (isrobot(S))
var/mob/living/silicon/robot/R = S
manifest[DEPARTMENT_EQUIPMENT][++manifest[DEPARTMENT_EQUIPMENT].len] = list(\
"name" = R.name, \
"rank" = R.module \
/* Cyborg ranks on the manifest change whenever they pick a new module.
With a fallback for when they haven't decided yet. */
? capitalize_first_letters(R.module.name) \
: "Default Module", \
"active" = "Online", \
"head" = FALSE, \
/* Cyborgs uniquely can be either a Crew or OOC role.
Player-character borgs will typically show up on the IC manifest.
While ghost-role and event borgs show up on the OOC manifest. */
"ooc_role" = R.scrambled_codes)
continue // Skip to the next player since it's not possible for them to also be one of the other types.
// Case for Ship AIs
if (isAI(S))
manifest[DEPARTMENT_EQUIPMENT][++manifest[DEPARTMENT_EQUIPMENT].len] = list(\
"name" = S.name, \
"rank" = "Vessel Intelligence", \
"active" = "Online", \
"head" = TRUE, \
"ooc_role" = FALSE)
manifest[DEPARTMENT_EQUIPMENT].Swap(1, manifest[DEPARTMENT_EQUIPMENT].len)
continue // Skip to the next player since it's not possible for them to also be one of the other types.
// Strictly OOC listing for pAIs, which aren't typically caught by the ghostrole check.
if (ispAI(S))
manifest[DEPARTMENT_EQUIPMENT][++manifest[DEPARTMENT_EQUIPMENT].len] = list(\
"name" = S.name \
/* It's possible for a pAI to have no name,
so we fact check it here and provide a fallback if needed. */
? S.name \
: "Unknown", \
"rank" = "Personal AI Assistant", \
"active" = "Online", \
"head" = FALSE, \
"ooc_role" = TRUE)
continue
/* ----- END OF CASE FOR SILICONS ----- */
/* ----- START OF CASE FOR GHOSTROLES ----- */
// Build the list of off-ships too. These will be hidden for anyone in-game.
for (var/mob/ghostrole_mob in SSghostroles.get_ghostrole_mobs())
manifest[DEPARTMENT_OFFSHIP][++manifest[DEPARTMENT_OFFSHIP].len] = list(\
"name" = ghostrole_mob.name \
/* It's possible for a ghostrole to spawn with no name,
so we fact check it here and provide a fallback if needed. */
? ghostrole_mob.name \
: "Unknown",\
"rank" = ghostrole_mob.mind && ghostrole_mob.mind.assigned_role \
/* Use the mind's role if they have one. */
? ghostrole_mob.mind.assigned_role \
: ishuman(ghostrole_mob) \
/* Or a fallback if they don't. */
? "Independent Spacer" \
: "Non-Humanoid Role",\
"active" = ghostrole_mob.stat == DEAD ? "*Deceased*" : "Active",\
"head" = FALSE,\
"ooc_role" = TRUE)
/* ----- END OF CASE FOR GHOSTROLES ----- */
// Finally, trim all empty departments from the list.
for(var/department in manifest)
if(!length(manifest[department]))
manifest -= department