From 057dbf13b12ef3ade954573f59f8cf77e3ab9687 Mon Sep 17 00:00:00 2001 From: Dani Glore Date: Sat, 3 May 2025 04:20:36 -0400 Subject: [PATCH] Add: Silicon Gender Preference (#90879) ## About The Pull Request This PR adds a secondary character preference which allows you to pick gender for silicon characters, and have the option of matching or separating that with their character's main gender. Silicon gender is displayed in their examine text as expected. ## Why It's Good For The Game People will have a way to identify the gender of their characters when playing as a cyborg and AI! ## Changelog :cl: A.C.M.O. add: Adds an option for picking silicon gender to secondary character preferences. Matches the main gender preference by default. add: Adds gender pronouns in cyborg/AI examine text. /:cl: --- code/game/objects/structures/ai_core.dm | 2 + .../client/preferences/silicon_gender.dm | 37 +++++++++++++++++++ code/modules/jobs/job_types/ai.dm | 2 + code/modules/jobs/job_types/cyborg.dm | 3 +- code/modules/mob/living/silicon/ai/ai.dm | 2 + code/modules/mob/living/silicon/ai/examine.dm | 18 ++++----- .../mob/living/silicon/robot/examine.dm | 32 ++++++++-------- .../modules/mob/living/silicon/robot/robot.dm | 5 +++ code/modules/mob/living/silicon/silicon.dm | 10 +++++ code/modules/mob/transform_procs.dm | 3 +- tgstation.dme | 1 + .../character_preferences/silicon_gender.tsx | 7 ++++ 12 files changed, 95 insertions(+), 27 deletions(-) create mode 100644 code/modules/client/preferences/silicon_gender.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/silicon_gender.tsx diff --git a/code/game/objects/structures/ai_core.dm b/code/game/objects/structures/ai_core.dm index 796654d2062..4e52090f848 100644 --- a/code/game/objects/structures/ai_core.dm +++ b/code/game/objects/structures/ai_core.dm @@ -388,6 +388,8 @@ if(malf_datum) malf_datum.add_law_zero() + if(!isnull(the_brainmob.client)) + ai_mob.set_gender(the_brainmob.client) if(core_mmi.force_replace_ai_name) ai_mob.fully_replace_character_name(ai_mob.name, core_mmi.replacement_ai_name()) ai_mob.posibrain_inside = core_mmi.braintype == "Android" diff --git a/code/modules/client/preferences/silicon_gender.dm b/code/modules/client/preferences/silicon_gender.dm new file mode 100644 index 00000000000..65d7b27b582 --- /dev/null +++ b/code/modules/client/preferences/silicon_gender.dm @@ -0,0 +1,37 @@ +#define SILICON_MALE "He/Him" +#define SILICON_FEMALE "She/Her" +#define SILICON_PLURAL "They/Them" +#define SILICON_NEUTER "It/Its" + +/datum/preference/choiced/silicon_gender + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "silicon_gender" + var/static/use_character_gender = "Use character gender" + ///Used to convert the read value of this preference into a gender + var/static/list/pronouns_to_genders = list( + "[SILICON_MALE]" = MALE, + "[SILICON_FEMALE]" = FEMALE, + "[SILICON_PLURAL]" = PLURAL, + "[SILICON_NEUTER]" = NEUTER, + ) + +/datum/preference/choiced/silicon_gender/init_possible_values() + return list( + use_character_gender, + SILICON_MALE, + SILICON_FEMALE, + SILICON_PLURAL, + SILICON_NEUTER, + ) + +/datum/preference/choiced/silicon_gender/create_default_value() + return SILICON_NEUTER + +/datum/preference/choiced/silicon_gender/apply_to_human(mob/living/carbon/human/target, value) + return + +#undef SILICON_MALE +#undef SILICON_FEMALE +#undef SILICON_PLURAL +#undef SILICON_NEUTER diff --git a/code/modules/jobs/job_types/ai.dm b/code/modules/jobs/job_types/ai.dm index 2264a914c1b..58774b5aedf 100644 --- a/code/modules/jobs/job_types/ai.dm +++ b/code/modules/jobs/job_types/ai.dm @@ -31,6 +31,8 @@ if(!R.connected_ai) R.TryConnectToAI() var/mob/living/silicon/ai/ai_spawn = spawned + if(player_client) + ai_spawn.set_gender(player_client) ai_spawn.log_current_laws() diff --git a/code/modules/jobs/job_types/cyborg.dm b/code/modules/jobs/job_types/cyborg.dm index 7b1e40fe1d6..99d370c2082 100644 --- a/code/modules/jobs/job_types/cyborg.dm +++ b/code/modules/jobs/job_types/cyborg.dm @@ -26,9 +26,10 @@ . = ..() if(!iscyborg(spawned)) return - spawned.gender = NEUTER var/mob/living/silicon/robot/robot_spawn = spawned robot_spawn.notify_ai(AI_NOTIFICATION_NEW_BORG) + if(player_client) + robot_spawn.set_gender(player_client) if(!robot_spawn.connected_ai) // Only log if there's no Master AI robot_spawn.log_current_laws() diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index 6a9b24a4b66..4e5e2dd3c96 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -51,6 +51,7 @@ if(client) INVOKE_ASYNC(src, PROC_REF(apply_pref_name), /datum/preference/name/ai, client) INVOKE_ASYNC(src, PROC_REF(apply_pref_hologram_display), client) + set_gender(client) INVOKE_ASYNC(src, PROC_REF(set_core_display_icon)) @@ -373,6 +374,7 @@ the_mmi.brainmob.name = src.real_name the_mmi.brainmob.real_name = src.real_name the_mmi.brainmob.container = the_mmi + the_mmi.brainmob.gender = src.gender var/has_suicided_trait = HAS_TRAIT(src, TRAIT_SUICIDED) the_mmi.brainmob.set_suicide(has_suicided_trait) diff --git a/code/modules/mob/living/silicon/ai/examine.dm b/code/modules/mob/living/silicon/ai/examine.dm index 13dc53ff2a8..9e58ee04d44 100644 --- a/code/modules/mob/living/silicon/ai/examine.dm +++ b/code/modules/mob/living/silicon/ai/examine.dm @@ -1,27 +1,27 @@ /mob/living/silicon/ai/examine(mob/user) . = list() if(stat == DEAD) - . += span_deadsay("It appears to be powered-down.") - . += span_notice("Its floor bolts are [is_anchored ? "tightened" : "loose"].") + . += span_deadsay("[p_They()] appear[p_s()] to be powered-down.") + . += span_notice("[p_Their()] floor bolts are [is_anchored ? "tightened" : "loose"].") if(is_anchored) if(!opened) if(!emagged) - . += span_notice("Its access panel is [stat == DEAD ? "damaged" : "closed and locked"], but could be pried open.") + . += span_notice("[p_Their()] access panel is [stat == DEAD ? "damaged" : "closed and locked"], but could be pried open.") else - . += span_warning("Its access panel lock is sparking, the cover can be pried open.") + . += span_warning("[p_Their()] access panel lock is sparking, the cover can be pried open.") else - . += span_notice("Its neural network connection could be cut, its access panel cover can be pried back into place.") + . += span_notice("[p_Their()] neural network connection could be cut, the access panel cover can be pried back into place.") if(stat != DEAD) if (getBruteLoss()) if (getBruteLoss() < 30) - . += span_warning("It looks slightly dented.") + . += span_warning("[p_They()] look[p_s()] slightly dented.") else - . += span_warning("It looks severely dented!") + . += span_warning("[p_They()] look[p_s()] severely dented!") if (getFireLoss()) if (getFireLoss() < 30) - . += span_warning("It looks slightly charred.") + . += span_warning("[p_They()] look[p_s()] slightly charred.") else - . += span_warning("Its casing is melted and heat-warped!") + . += span_warning("[p_Their()] casing is melted and heat-warped!") if(deployed_shell) . += "The wireless networking light is blinking." else if (!shunted && !client) diff --git a/code/modules/mob/living/silicon/robot/examine.dm b/code/modules/mob/living/silicon/robot/examine.dm index 3091881c56f..388835b7a9a 100644 --- a/code/modules/mob/living/silicon/robot/examine.dm +++ b/code/modules/mob/living/silicon/robot/examine.dm @@ -4,47 +4,47 @@ . += "[desc]" var/model_name = model ? "\improper [model.name]" : "\improper Default" - . += "It is currently \a [model_name]-type cyborg." + . += "[p_Theyre()] currently \a [model_name]-type cyborg." var/obj/act_module = get_active_held_item() if(act_module) - . += "It is holding [icon2html(act_module, user)] \a [act_module]." + . += "[p_Theyre()] holding [icon2html(act_module, user)] \a [act_module]." . += get_status_effect_examinations() if (getBruteLoss()) if (getBruteLoss() < maxHealth*0.5) - . += span_warning("It looks slightly dented.") + . += span_warning("[p_They()] look[p_s()] slightly dented.") else - . += span_boldwarning("It looks severely dented!") + . += span_boldwarning("[p_They()] look[p_s()] severely dented!") if (getFireLoss() || getToxLoss()) var/overall_fireloss = getFireLoss() + getToxLoss() if (overall_fireloss < maxHealth * 0.5) - . += span_warning("It looks slightly charred.") + . += span_warning("[p_They()] look[p_s()] slightly charred.") else - . += span_boldwarning("It looks severely burnt and heat-warped!") + . += span_boldwarning("[p_They()] look[p_s()] severely burnt and heat-warped!") if (health < -maxHealth*0.5) - . += span_warning("It looks barely operational.") + . += span_warning("[p_They()] look[p_s()] barely operational.") if (fire_stacks < 0) - . += span_warning("It's covered in water.") + . += span_warning("[p_Theyre()] covered in water.") else if (fire_stacks > 0) - . += span_warning("It's coated in something flammable.") + . += span_warning("[p_Theyre()] coated in something flammable.") if(opened) - . += span_warning("Its cover is open and the power cell is [cell ? "installed" : "missing"].") + . += span_warning("[p_Their()] cover is open and the power cell is [cell ? "installed" : "missing"].") else - . += "Its cover is closed[locked ? "" : ", and looks unlocked"]." + . += "[p_Their()] cover is closed[locked ? "" : ", and looks unlocked"]." if(cell && cell.charge <= 0) - . += span_warning("Its battery indicator is blinking red!") + . += span_warning("[p_Their()] battery indicator is blinking red!") switch(stat) if(CONSCIOUS) if(shell) - . += "It appears to be an [deployed ? "active" : "empty"] AI shell." + . += "[p_They()] appear[p_s()] to be an [deployed ? "active" : "empty"] AI shell." else if(!client) - . += "It appears to be in stand-by mode." //afk + . += "[p_They()] appear[p_s()] to be in stand-by mode." //afk if(SOFT_CRIT, UNCONSCIOUS, HARD_CRIT) - . += span_warning("It doesn't seem to be responding.") + . += span_warning("[p_They()] do[p_es()]n't seem to be responding.") if(DEAD) - . += span_deadsay("It looks like its system is corrupted and requires a reset.") + . += span_deadsay("[p_They()] look[p_s()] like its system is corrupted and requires a reset.") . += ..() diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 7b38a5df990..6fab744907e 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -75,6 +75,9 @@ mmi.update_appearance() setup_default_name() + if(mmi.brainmob) + gender = mmi.brainmob.gender + aicamera = new/obj/item/camera/siliconcam/robot_camera(src) toner = tonermax diag_hud_set_borgcell() @@ -887,6 +890,8 @@ builtInCamera.c_tag = real_name //update the camera name too mainframe = AI deployed = TRUE + if(AI.client) + set_gender(AI.client) set_connected_ai(mainframe) mainframe.connected_robots |= src lawupdate = TRUE diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm index 1937cd3d4a5..01ec2806a0f 100644 --- a/code/modules/mob/living/silicon/silicon.dm +++ b/code/modules/mob/living/silicon/silicon.dm @@ -94,6 +94,16 @@ GLOB.silicon_mobs -= src return ..() +///Sets cyborg gender from preferences. Expects a client. +/mob/living/silicon/proc/set_gender(client/player_client) + var/silicon_pronouns = player_client.prefs.read_preference(/datum/preference/choiced/silicon_gender) + if(silicon_pronouns == /datum/preference/choiced/silicon_gender::use_character_gender) + gender = player_client.prefs.read_preference(/datum/preference/choiced/gender) + return + var/silicon_gender = /datum/preference/choiced/silicon_gender::pronouns_to_genders[silicon_pronouns] + if(!isnull(silicon_gender)) + gender = silicon_gender + /mob/living/silicon/proc/on_silicon_shocked(datum/source, shock_damage, shock_source, siemens_coeff, flags) SIGNAL_HANDLER for(var/mob/living/living_mob in buckled_mobs) diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm index 2ac1dc56c1a..17da7290366 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -144,7 +144,8 @@ ADD_TRAIT(src, TRAIT_NO_TRANSFORM, PERMANENT_TRANSFORMATION_TRAIT) var/mob/living/silicon/robot/new_borg = new /mob/living/silicon/robot(loc) - new_borg.gender = gender + if(client) + new_borg.set_gender(client) new_borg.SetInvisibility(INVISIBILITY_NONE) if(client?.prefs.read_preference(/datum/preference/name/cyborg) != DEFAULT_CYBORG_NAME) diff --git a/tgstation.dme b/tgstation.dme index ce6faac8792..4ddfc80f51d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3912,6 +3912,7 @@ #include "code\modules\client\preferences\scarred_eye.dm" #include "code\modules\client\preferences\screentips.dm" #include "code\modules\client\preferences\security_department.dm" +#include "code\modules\client\preferences\silicon_gender.dm" #include "code\modules\client\preferences\skin_tone.dm" #include "code\modules\client\preferences\sounds.dm" #include "code\modules\client\preferences\species.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/silicon_gender.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/silicon_gender.tsx new file mode 100644 index 00000000000..c585bfc1c06 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/silicon_gender.tsx @@ -0,0 +1,7 @@ +import { FeatureChoiced } from '../base'; +import { FeatureDropdownInput } from '../dropdowns'; + +export const silicon_gender: FeatureChoiced = { + name: 'Gender (Silicon)', + component: FeatureDropdownInput, +};