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 (