From 4d43d53c6fbf1bb186da3619d2543afc62de6731 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 3 Feb 2026 05:09:49 -0500 Subject: [PATCH] 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 --- code/__DEFINES/subsystems.dm | 2 + code/controllers/subsystems/ghostroles.dm | 41 ++++++- code/controllers/subsystems/records.dm | 116 +++++++++++++----- .../modules/ghostroles/spawner/human/human.dm | 2 +- .../file_system/programs/generic/manifest.dm | 1 + .../hellfirejag-offships-manifest.yml | 5 + .../interfaces/common/ManifestSection.tsx | 4 + 7 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 html/changelogs/hellfirejag-offships-manifest.yml diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 0e3c1aef02e..ed8e1375c53 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -161,6 +161,7 @@ #define DEPARTMENT_CIVILIAN "Civilian" #define DEPARTMENT_EQUIPMENT "Equipment" #define DEPARTMENT_MISCELLANEOUS "Miscellaneous" +#define DEPARTMENT_OFFSHIP "Off-ship" #define DEPARTMENTS_LIST_INIT list(\ DEPARTMENT_COMMAND = list(),\ DEPARTMENT_COMMAND_SUPPORT = list(),\ @@ -173,6 +174,7 @@ DEPARTMENT_CIVILIAN = list(),\ DEPARTMENT_EQUIPMENT = list(),\ DEPARTMENT_MISCELLANEOUS = list(),\ + DEPARTMENT_OFFSHIP = list(),\ ) // job roles within departments diff --git a/code/controllers/subsystems/ghostroles.dm b/code/controllers/subsystems/ghostroles.dm index 8bd560ca40b..96fee090acc 100644 --- a/code/controllers/subsystems/ghostroles.dm +++ b/code/controllers/subsystems/ghostroles.dm @@ -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 diff --git a/code/controllers/subsystems/records.dm b/code/controllers/subsystems/records.dm index 7f9825852ce..ffbdf5d1ea3 100644 --- a/code/controllers/subsystems/records.dm +++ b/code/controllers/subsystems/records.dm @@ -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 diff --git a/code/modules/ghostroles/spawner/human/human.dm b/code/modules/ghostroles/spawner/human/human.dm index 07ee39020e9..13152e9e5dc 100644 --- a/code/modules/ghostroles/spawner/human/human.dm +++ b/code/modules/ghostroles/spawner/human/human.dm @@ -72,7 +72,7 @@ pick_message = "[pick_message] Auto Prefix: \"[mob_name_prefix]\" " if(mob_name_suffix) pick_message = "[pick_message] Auto Suffix: \"[mob_name_suffix]\" " - mname = sanitizeName(sanitize_readd_odd_symbols(sanitizeSafe(input(user, pick_message, "Name for a [species] (without prefix/suffix)")))) + mname = sanitizeName(tgui_input_text(user, pick_message, "Name for a [species] (without prefix/suffix)")) if(!length(mname)) if(mob_name_prefix || mob_name_suffix) diff --git a/code/modules/modular_computers/file_system/programs/generic/manifest.dm b/code/modules/modular_computers/file_system/programs/generic/manifest.dm index 02fded622dd..41d7333213c 100644 --- a/code/modules/modular_computers/file_system/programs/generic/manifest.dm +++ b/code/modules/modular_computers/file_system/programs/generic/manifest.dm @@ -19,4 +19,5 @@ var/list/data = list() data["manifest"] = SSrecords.get_manifest_list() data["allow_follow"] = isobserver(usr) + data["show_ooc_roles"] = isabstractmob(usr) return data diff --git a/html/changelogs/hellfirejag-offships-manifest.yml b/html/changelogs/hellfirejag-offships-manifest.yml new file mode 100644 index 00000000000..fd13d50fb3b --- /dev/null +++ b/html/changelogs/hellfirejag-offships-manifest.yml @@ -0,0 +1,5 @@ +author: Hellfirejag +delete-after: True +changes: + - rscadd: "Off-ship player characters can now be viewed from the crew manifest, so long as you are either an observer, or are in the lobby." + - bugfix: "Fixed player characters with apostrophes in their name showing up weirdly on the manifest." diff --git a/tgui/packages/tgui/interfaces/common/ManifestSection.tsx b/tgui/packages/tgui/interfaces/common/ManifestSection.tsx index 025040c99b4..6de97f58536 100644 --- a/tgui/packages/tgui/interfaces/common/ManifestSection.tsx +++ b/tgui/packages/tgui/interfaces/common/ManifestSection.tsx @@ -6,6 +6,7 @@ import { TableCell, TableRow } from '../../components/Table'; type ManifestData = { manifest: { department: Crew[] }; allow_follow: BooleanLike; + show_ooc_roles: BooleanLike; }; type Crew = { @@ -13,17 +14,20 @@ type Crew = { rank: string; active: string; head: BooleanLike; + ooc_role: BooleanLike; }; export const ManifestSection = (props, context) => { const { act, data } = useBackend(context); const manifest = data.manifest || {}; const allow_follow = data.allow_follow; + const show_ooc_roles = data.show_ooc_roles; return (
{Object.keys(manifest).length === 0 && 'There are no crew active.'} {Object.keys(manifest).map((dept) => { const deptCrew = manifest[dept]; + if (dept === 'Off-ship' && !show_ooc_roles) return; return (