diff --git a/.gitignore b/.gitignore index b20069596967..4783785ce09b 100644 --- a/.gitignore +++ b/.gitignore @@ -216,5 +216,4 @@ tools/MapAtmosFixer/MapAtmosFixer/bin/* /_maps/templates.dm #KDIFF3 files -*.orig -tgui/public/ \ No newline at end of file +*.orig \ No newline at end of file diff --git a/code/__DEFINES/DNA.dm b/code/__DEFINES/DNA.dm index 1b41f155696d..8d82f64f85bc 100644 --- a/code/__DEFINES/DNA.dm +++ b/code/__DEFINES/DNA.dm @@ -195,3 +195,19 @@ #define G_MALE 1 #define G_FEMALE 2 #define G_PLURAL 3 + +// Defines for used in creating "perks" for the species preference pages. +/// A key that designates UI icon displayed on the perk. +#define SPECIES_PERK_ICON "ui_icon" +/// A key that designates the name of the perk. +#define SPECIES_PERK_NAME "name" +/// A key that designates the description of the perk. +#define SPECIES_PERK_DESC "description" +/// A key that designates what type of perk it is (see below). +#define SPECIES_PERK_TYPE "perk_type" + +// The possible types each perk can be. +// Positive perks are shown in green, negative in red, and neutral in grey. +#define SPECIES_POSITIVE_PERK "positive" +#define SPECIES_NEGATIVE_PERK "negative" +#define SPECIES_NEUTRAL_PERK "neutral" diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index a66c4eba91af..6400d4231af6 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -45,6 +45,9 @@ #define BLOB_RANDOM_PLACEMENT 1 +/// The dimensions of the antagonist preview icon. Will be scaled to this size. +#define ANTAGONIST_PREVIEW_ICON_SIZE 96 + /// How many telecrystals a normal traitor starts with #define TELECRYSTALS_DEFAULT 20 /// How many telecrystals mapper/admin only "precharged" uplink implant diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 7beb454a22da..bfb5972c8901 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -14,6 +14,7 @@ #define COLOR_ALMOST_BLACK "#333333" #define COLOR_BLACK "#000000" #define COLOR_RED "#FF0000" +#define COLOR_MOSTLY_PURE_RED "#FF3300" #define COLOR_RED_LIGHT "#FF3333" #define COLOR_MAROON "#800000" #define COLOR_YELLOW "#FFFF00" @@ -29,6 +30,7 @@ #define COLOR_PINK "#FFC0CB" #define COLOR_MAGENTA "#FF00FF" #define COLOR_PURPLE "#800080" +#define COLOR_STRONG_VIOLET "#6927c5" #define COLOR_ORANGE "#FF9900" #define COLOR_PALE_ORANGE "#FFBE9D" #define COLOR_BEIGE "#CEB689" @@ -43,6 +45,7 @@ #define COLOR_PALE_RED_GRAY "#D59998" #define COLOR_PALE_PURPLE_GRAY "#CBB1CA" #define COLOR_PURPLE_GRAY "#AE8CA8" +#define COLOR_VIBRANT_LIME "#00FF00" //Color defines used by the assembly detailer. #define COLOR_ASSEMBLY_BLACK "#545454" @@ -60,3 +63,6 @@ #define COLOR_ASSEMBLY_LBLUE "#5D99BE" #define COLOR_ASSEMBLY_BLUE "#38559E" #define COLOR_ASSEMBLY_PURPLE "#6F6192" + +/// The default color for admin say, used as a fallback when the preference is not enabled +#define DEFAULT_ASAY_COLOR "#996600" diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/flags.dm index 3d05eb7ae6fc..e088f4b0cb6d 100644 --- a/code/__DEFINES/flags.dm +++ b/code/__DEFINES/flags.dm @@ -30,25 +30,23 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204 #define CONDUCT_1 (1<<5) /// For machines and structures that should not break into parts, eg, holodeck stuff #define NODECONSTRUCT_1 (1<<7) -/// atom queued to SSoverlay -#define OVERLAY_QUEUED_1 (1<<8) /// item has priority to check when entering or leaving -#define ON_BORDER_1 (1<<9) +#define ON_BORDER_1 (1<<8) /// Prevent clicking things below it on the same turf eg. doors/ fulltile windows -#define PREVENT_CLICK_UNDER_1 (1<<11) -#define HOLOGRAM_1 (1<<12) +#define PREVENT_CLICK_UNDER_1 (1<<9) +#define HOLOGRAM_1 (1<<10) /// TESLA_IGNORE grants immunity from being targeted by tesla-style electricity -#define TESLA_IGNORE_1 (1<<13) +#define TESLA_IGNORE_1 (1<<11) ///Whether /atom/Initialize() has already run for the object -#define INITIALIZED_1 (1<<14) +#define INITIALIZED_1 (1<<12) /// was this spawned by an admin? used for stat tracking stuff. -#define ADMIN_SPAWNED_1 (1<<15) +#define ADMIN_SPAWNED_1 (1<<13) /// should not get harmed if this gets caught by an explosion? -#define PREVENT_CONTENTS_EXPLOSION_1 (1<<16) +#define PREVENT_CONTENTS_EXPLOSION_1 (1<<14) /// should the contents of this atom be acted upon -#define RAD_PROTECT_CONTENTS_1 (1 << 17) +#define RAD_PROTECT_CONTENTS_1 (1 << 15) /// should this object be allowed to be contaminated -#define RAD_NO_CONTAMINATE_1 (1 << 18) +#define RAD_NO_CONTAMINATE_1 (1 << 16) //turf-only flags #define NOJAUNT_1 (1<<0) diff --git a/code/__DEFINES/food.dm b/code/__DEFINES/food.dm index ee5e0a9ce458..ed893bd712ca 100644 --- a/code/__DEFINES/food.dm +++ b/code/__DEFINES/food.dm @@ -20,6 +20,25 @@ #define MICE (1<<19) //disliked/liked by anything that dislikes/likes any of RAW, MEAT, or GROSS, except felinids #define NUTS (1<<20) +/// A list of food type names, in order of their flags +#define FOOD_FLAGS list( \ + "MEAT", \ + "VEGETABLES", \ + "RAW", \ + "JUNKFOOD", \ + "GRAIN", \ + "FRUIT", \ + "DAIRY", \ + "FRIED", \ + "ALCOHOL", \ + "SUGAR", \ + "GROSS", \ + "TOXIC", \ + "PINEAPPLE", \ + "BREAKFAST", \ + "CLOTH", \ +) + #define DRINK_NICE 1 #define DRINK_GOOD 2 #define DRINK_VERYGOOD 3 diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index a7ee31891ff2..af48d4c11a74 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -85,6 +85,7 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list( #define isipc(A) (is_species(A, /datum/species/ipc)) #define issnail(A) (is_species(A, /datum/species/snail)) #define isandroid(A) (is_species(A, /datum/species/android)) +#define isdummy(A) (istype(A, /mob/living/carbon/human/dummy)) //more carbon mobs #define ismonkey(A) (istype(A, /mob/living/carbon/monkey)) diff --git a/code/__DEFINES/jobs.dm b/code/__DEFINES/jobs.dm index 14f023b4693f..063d1507ee52 100644 --- a/code/__DEFINES/jobs.dm +++ b/code/__DEFINES/jobs.dm @@ -100,6 +100,27 @@ #define JOB_DISPLAY_ORDER_CLERK 39 #define JOB_DISPLAY_ORDER_CHAPLAIN 40 +#define DEPARTMENT_UNASSIGNED "No Department" +#define DEPARTMENT_BITFLAG_SECURITY (1<<0) +#define DEPARTMENT_SECURITY "Security" +#define DEPARTMENT_BITFLAG_COMMAND (1<<1) +#define DEPARTMENT_COMMAND "Command" +#define DEPARTMENT_BITFLAG_SERVICE (1<<2) +#define DEPARTMENT_SERVICE "Service" +#define DEPARTMENT_BITFLAG_CARGO (1<<3) +#define DEPARTMENT_CARGO "Cargo" +#define DEPARTMENT_BITFLAG_ENGINEERING (1<<4) +#define DEPARTMENT_ENGINEERING "Engineering" +#define DEPARTMENT_BITFLAG_SCIENCE (1<<5) +#define DEPARTMENT_SCIENCE "Science" +#define DEPARTMENT_BITFLAG_MEDICAL (1<<6) +#define DEPARTMENT_MEDICAL "Medical" +#define DEPARTMENT_BITFLAG_SILICON (1<<7) +#define DEPARTMENT_SILICON "Silicon" +#define DEPARTMENT_BITFLAG_ASSISTANT (1<<8) +#define DEPARTMENT_ASSISTANT "Assistant" +#define DEPARTMENT_BITFLAG_CAPTAIN (1<<9) +#define DEPARTMENT_CAPTAIN "Captain" /proc/find_job(target) //Get the job from the mind @@ -126,5 +147,3 @@ #define IS_SCIENCE(target) (find_job(target) in GLOB.science_positions) #define IS_CARGO(target) (find_job(target) in GLOB.supply_positions) #define IS_SECURITY(target) (find_job(target) in GLOB.security_positions) - -#define DEPARTMENT_UNASSIGNED "No Department" diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm index db17a3e21c7c..5dba720d72b1 100644 --- a/code/__DEFINES/misc.dm +++ b/code/__DEFINES/misc.dm @@ -192,42 +192,40 @@ GLOBAL_LIST_EMPTY(bloody_footprints_cache) #define GHOST_ORBIT_SQUARE "square" #define GHOST_ORBIT_PENTAGON "pentagon" -//Ghost showing preferences: -#define GHOST_ACCS_NONE 1 -#define GHOST_ACCS_DIR 50 -#define GHOST_ACCS_FULL 100 - -#define GHOST_ACCS_NONE_NAME "default sprites" -#define GHOST_ACCS_DIR_NAME "only directional sprites" -#define GHOST_ACCS_FULL_NAME "full accessories" +#define GHOST_ORBIT_DEFAULT_OPTION GHOST_ORBIT_CIRCLE -#define GHOST_ACCS_DEFAULT_OPTION GHOST_ACCS_FULL - -GLOBAL_LIST_INIT(ghost_accs_options, list(GHOST_ACCS_NONE, GHOST_ACCS_DIR, GHOST_ACCS_FULL)) //So save files can be sanitized properly. +//Ghost showing preferences: +#define GHOST_ACCS_NONE "Default sprites" +#define GHOST_ACCS_DIR "Only directional sprites" +#define GHOST_ACCS_FULL "Full accessories" -#define GHOST_OTHERS_SIMPLE 1 -#define GHOST_OTHERS_DEFAULT_SPRITE 50 -#define GHOST_OTHERS_THEIR_SETTING 100 +#define GHOST_ACCS_DEFAULT_OPTION GHOST_ACCS_FULL -#define GHOST_OTHERS_SIMPLE_NAME "white ghost" -#define GHOST_OTHERS_DEFAULT_SPRITE_NAME "default sprites" -#define GHOST_OTHERS_THEIR_SETTING_NAME "their setting" +#define GHOST_OTHERS_SIMPLE "White ghosts" +#define GHOST_OTHERS_DEFAULT_SPRITE "Default sprites" +#define GHOST_OTHERS_THEIR_SETTING "Their sprites" -#define GHOST_OTHERS_DEFAULT_OPTION GHOST_OTHERS_THEIR_SETTING +#define GHOST_OTHERS_DEFAULT_OPTION GHOST_OTHERS_THEIR_SETTING #define GHOST_MAX_VIEW_RANGE_DEFAULT 10 #define GHOST_MAX_VIEW_RANGE_MEMBER 14 -GLOBAL_LIST_INIT(ghost_others_options, list(GHOST_OTHERS_SIMPLE, GHOST_OTHERS_DEFAULT_SPRITE, GHOST_OTHERS_THEIR_SETTING)) //Same as ghost_accs_options. - //pda fonts -#define MONO "Monospaced" -#define VT "VT323" -#define ORBITRON "Orbitron" -#define SHARE "Share Tech Mono" +#define PDA_FONT_MONO "Monospaced" +#define PDA_FONT_VT "VT323" +#define PDA_FONT_ORBITRON "Orbitron" +#define PDA_FONT_SHARE "Share Tech Mono" + +GLOBAL_LIST_INIT(pda_styles, list(PDA_FONT_MONO, PDA_FONT_VT, PDA_FONT_ORBITRON, PDA_FONT_SHARE)) + +//pda colours +#define PDA_COLOR_NORMAL "Normal" +#define PDA_COLOR_TRANSPARENT "Transparent" +#define PDA_COLOR_PIPBOY "Pip Boy" +#define PDA_COLOR_RAINBOW "Rainbow" -GLOBAL_LIST_INIT(pda_styles, list(MONO, VT, ORBITRON, SHARE)) +GLOBAL_LIST_INIT(donor_pdas, list(PDA_COLOR_NORMAL, PDA_COLOR_TRANSPARENT, PDA_COLOR_PIPBOY, PDA_COLOR_RAINBOW)) ///////////////////////////////////// // atom.appearence_flags shortcuts // @@ -373,7 +371,6 @@ GLOBAL_LIST_INIT(pda_styles, list(MONO, VT, ORBITRON, SHARE)) #define SECURITY_TRUSTED 3 //Dummy mob reserve slots -#define DUMMY_HUMAN_SLOT_PREFERENCES "dummy_preference_preview" #define DUMMY_HUMAN_SLOT_ADMIN "admintools" #define DUMMY_HUMAN_SLOT_MANIFEST "dummy_manifest_generation" diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index d8d983ef20d8..ec37e27982dc 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -384,3 +384,5 @@ ///Swarmer flags #define SWARMER_LIGHT_ON (1<<0) + +#define ACCENT_NONE "None" diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index a217d9cbd90a..70af5fa33679 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -1,5 +1,7 @@ -//Preference toggles +// Legacy preference toggles. +// !!! DO NOT ADD ANY NEW ONES HERE !!! +// Use `/datum/preference/toggle` instead. #define SOUND_ADMINHELP (1<<0) #define SOUND_MIDI (1<<1) #define SOUND_AMBIENCE (1<<2) @@ -50,22 +52,16 @@ #define TOGGLES_DEFAULT_CHAT (CHAT_OOC|CHAT_DEAD|CHAT_GHOSTEARS|CHAT_GHOSTSIGHT|CHAT_PRAYER|CHAT_RADIO|CHAT_PULLR|CHAT_GHOSTWHISPER|CHAT_GHOSTPDA|CHAT_GHOSTRADIO|CHAT_BANKCARD) -#define PARALLAX_INSANE -1 //for show offs -#define PARALLAX_HIGH 0 //default. -#define PARALLAX_MED 1 -#define PARALLAX_LOW 2 -#define PARALLAX_DISABLE 3 //this option must be the highest number +#define PARALLAX_INSANE "Insane" +#define PARALLAX_HIGH "High" +#define PARALLAX_MED "Medium" +#define PARALLAX_LOW "Low" +#define PARALLAX_DISABLE "Disabled" #define PARALLAX_DELAY_DEFAULT world.tick_lag #define PARALLAX_DELAY_MED 1 #define PARALLAX_DELAY_LOW 2 -#define PIXEL_SCALING_AUTO 0 -#define PIXEL_SCALING_1X 1 -#define PIXEL_SCALING_1_2X 1.5 -#define PIXEL_SCALING_2X 2 -#define PIXEL_SCALING_3X 3 - #define SCALING_METHOD_NORMAL "normal" #define SCALING_METHOD_DISTORT "distort" #define SCALING_METHOD_BLUR "blur" @@ -112,3 +108,54 @@ #define JP_LOW 1 #define JP_MEDIUM 2 #define JP_HIGH 3 + + +//recommened client FPS +#define RECOMMENDED_FPS 100 + + +// randomise_appearance_prefs() and randomize_human_appearance() proc flags +#define RANDOMIZE_SPECIES (1<<0) +#define RANDOMIZE_NAME (1<<1) + + +//randomised elements +#define RANDOM_ANTAG_ONLY 1 +#define RANDOM_DISABLED 2 +#define RANDOM_ENABLED 3 + + +// Values for /datum/preference/savefile_identifier +/// This preference is character specific. +#define PREFERENCE_CHARACTER "character" +/// This preference is account specific. +#define PREFERENCE_PLAYER "player" + +// Values for /datum/preferences/current_tab +/// Open the character preference window +#define PREFERENCE_TAB_CHARACTER_PREFERENCES 0 + +/// Open the game preferences window +#define PREFERENCE_TAB_GAME_PREFERENCES 1 + +/// Open the keybindings window +#define PREFERENCE_TAB_KEYBINDINGS 2 + +/// These will be shown in the character sidebar, but at the bottom. +#define PREFERENCE_CATEGORY_FEATURES "features" + +/// Any preferences that will show to the sides of the character in the setup menu. +#define PREFERENCE_CATEGORY_CLOTHING "clothing" + +/// Preferences that will be put into the 3rd list, and are not contextual. +#define PREFERENCE_CATEGORY_NON_CONTEXTUAL "non_contextual" + +/// Will be put under the game preferences window. +#define PREFERENCE_CATEGORY_GAME_PREFERENCES "game_preferences" + +/// These will show in the list to the right of the character preview. +#define PREFERENCE_CATEGORY_SECONDARY_FEATURES "secondary_features" + +/// These are preferences that are supplementary for main features, +/// such as hair color being affixed to hair. +#define PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES "supplemental_features" diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index d206c5a16720..d835e5a0fea7 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -9,6 +9,7 @@ #define ROLE_SYNDICATE "Syndicate" #define ROLE_TRAITOR "Traitor" #define ROLE_OPERATIVE "Operative" +#define ROLE_CLOWNOP "Clown Operative" #define ROLE_CHANGELING "Changeling" #define ROLE_WIZARD "Wizard" #define ROLE_RAGINMAGES "Ragin Mages" @@ -31,7 +32,7 @@ #define ROLE_BRAINWASHED "Brainwashed Victim" #define ROLE_HIVE "Hivemind Host" #define ROLE_OBSESSED "Obsessed" -#define ROLE_SENTIENCE "Sentience Potion Spawn" +#define ROLE_SENTIENCE "Sentient Creature" #define ROLE_MOUSE "Mouse" #define ROLE_MIND_TRANSFER "Mind Transfer Potion" #define ROLE_POSIBRAIN "Posibrain" @@ -56,48 +57,58 @@ #define ROLE_GOLEM "Golem" #define ROLE_SINFULDEMON "Demon of Sin" #define ROLE_GHOSTBEACON "Ghost Beacon" +#define ROLE_NIGHTMARE "Nightmare" +#define ROLE_DISEASE "Disease" +#define ROLE_PIRATE "Pirate" + //Missing assignment means it's not a gamemode specific role, IT'S NOT A BUG OR ERROR. //The gamemode specific ones are just so the gamemodes can query whether a player is old enough //(in game days played) to play that role // check sql_ban_system.dm as well, that's where the job bans are located. GLOBAL_LIST_INIT(special_roles, list( - ROLE_TRAITOR = /datum/game_mode/traitor, - ROLE_BROTHER = /datum/game_mode/traitor/bros, - ROLE_OPERATIVE = /datum/game_mode/nuclear, - ROLE_CHANGELING = /datum/game_mode/changeling, - ROLE_WIZARD = /datum/game_mode/wizard, - ROLE_RAGINMAGES = /datum/game_mode/wizard, - ROLE_BULLSHITMAGES = /datum/game_mode/wizard, - ROLE_MALF, - ROLE_REV = /datum/game_mode/revolution, - ROLE_ALIEN, - ROLE_HORROR, - ROLE_PAI, - ROLE_CULTIST = /datum/game_mode/cult, - ROLE_BLOB, - ROLE_NINJA, - ROLE_OBSESSED, - ROLE_MONKEY = /datum/game_mode/monkey, - ROLE_REVENANT, - ROLE_ABDUCTOR, - ROLE_DEVIL = /datum/game_mode/devil, - ROLE_SERVANT_OF_RATVAR = /datum/game_mode/clockwork_cult, - ROLE_VAMPIRE = /datum/game_mode/vampire, // Yogs - ROLE_SHADOWLING = /datum/game_mode/shadowling, //yogs - ROLE_GANG = /datum/game_mode/gang, // yogs - ROLE_HERETIC = /datum/game_mode/heretics, - ROLE_HIVE = /datum/game_mode/hivemind, - ROLE_INFILTRATOR = /datum/game_mode/infiltration, // Yogs - ROLE_INTERNAL_AFFAIRS = /datum/game_mode/traitor/internal_affairs, - ROLE_DARKSPAWN = /datum/game_mode/darkspawn, - ROLE_SENTIENCE, - ROLE_ZOMBIE = /datum/game_mode/zombie, - ROLE_FUGITIVE, - ROLE_BLOODSUCKER = /datum/game_mode/bloodsucker, - ROLE_MONSTERHUNTER, - ROLE_SPACE_DRAGON, - ROLE_SINFULDEMON + ROLE_TRAITOR = /datum/antagonist/traitor, + ROLE_OPERATIVE = /datum/antagonist/nukeop, + ROLE_CLOWNOP = /datum/antagonist/nukeop/clownop, + ROLE_CHANGELING = /datum/antagonist/changeling, + ROLE_WIZARD = /datum/antagonist/wizard, + ROLE_RAGINMAGES = /datum/antagonist/wizard, + ROLE_BULLSHITMAGES = /datum/antagonist/wizard, + ROLE_MALF = /datum/antagonist/traitor/malf, + ROLE_REV_HEAD = /datum/antagonist/rev/head, + ROLE_ALIEN = /datum/antagonist/xeno, + ROLE_CULTIST = /datum/antagonist/cult, + ROLE_HERETIC = /datum/antagonist/heretic, + ROLE_BLOB = /datum/antagonist/blob, + ROLE_NINJA = /datum/antagonist/ninja, + ROLE_MONKEY = /datum/antagonist/monkey, + ROLE_ABDUCTOR = /datum/antagonist/abductor, + ROLE_REVENANT = /datum/antagonist/revenant, + ROLE_DEVIL = /datum/antagonist/devil, + ROLE_SERVANT_OF_RATVAR = /datum/antagonist/clockcult, + ROLE_BROTHER = /datum/antagonist/brother, + ROLE_BRAINWASHED = /datum/antagonist/brainwashed, + ROLE_OBSESSED = /datum/antagonist/obsessed, + ROLE_INTERNAL_AFFAIRS = /datum/antagonist/traitor/internal_affairs, + ROLE_FUGITIVE = /datum/antagonist/fugitive, + ROLE_SHADOWLING = /datum/antagonist/shadowling, // Yogs + ROLE_VAMPIRE = /datum/antagonist/vampire, // Yogs + ROLE_GANG = /datum/antagonist/gang, // Yogs + ROLE_DARKSPAWN = /datum/antagonist/darkspawn, // Yogs + ROLE_HOLOPARASITE = /datum/antagonist/guardian, // Yogs + ROLE_HORROR = /datum/antagonist/horror, // Yogs + ROLE_INFILTRATOR = /datum/antagonist/infiltrator, // Yogs + ROLE_ZOMBIE = /datum/antagonist/zombie, + ROLE_BLOODSUCKER = /datum/antagonist/bloodsucker, + ROLE_MONSTERHUNTER = /datum/antagonist/monsterhunter, + ROLE_SPACE_DRAGON = /datum/antagonist/space_dragon, + ROLE_GOLEM = /datum/antagonist/golem, + ROLE_SINFULDEMON = /datum/antagonist/sinfuldemon, + ROLE_NIGHTMARE = /datum/antagonist/nightmare, + ROLE_DISEASE = /datum/antagonist/disease, + ROLE_HIVE = /datum/antagonist/hivemind, + ROLE_PIRATE = /datum/antagonist/pirate, + ROLE_SENTIENCE = /datum/antagonist/sentient_creature )) //Job defines for what happens when you fail to qualify for any job during job selection diff --git a/code/__DEFINES/stat_tracking.dm b/code/__DEFINES/stat_tracking.dm index d7d207469d8d..f0635c95529e 100644 --- a/code/__DEFINES/stat_tracking.dm +++ b/code/__DEFINES/stat_tracking.dm @@ -8,10 +8,7 @@ #define STAT_LOG_ENTRY(entrylist, entryname) \ var/list/STAT_ENTRY = entrylist[entryname] || (entrylist[entryname] = new /list(STAT_ENTRY_LENGTH));\ STAT_ENTRY[STAT_ENTRY_TIME] += STAT_TIME;\ - var/STAT_INCR_AMOUNT = min(1, 2**round((STAT_ENTRY[STAT_ENTRY_COUNT] || 0)/SHORT_REAL_LIMIT));\ - if (STAT_INCR_AMOUNT == 1 || prob(100/STAT_INCR_AMOUNT)) {\ - STAT_ENTRY[STAT_ENTRY_COUNT] += STAT_INCR_AMOUNT;\ - };\ + STAT_ENTRY[STAT_ENTRY_COUNT] += 1; diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 5b5c122eb8a6..76ea46108b04 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -133,11 +133,12 @@ #define INIT_ORDER_MATERIALS 76 #define INIT_ORDER_RESEARCH 75 #define INIT_ORDER_STATION 74 +#define INIT_ORDER_QUIRKS 73 #define INIT_ORDER_EVENTS 70 -#define INIT_ORDER_MAPPING 65 -#define INIT_ORDER_JOBS 60 -#define INIT_ORDER_QUIRKS 55 +#define INIT_ORDER_JOBS 65 +#define INIT_ORDER_MAPPING 60 #define INIT_ORDER_TICKER 50 +#define INIT_ORDER_EARLY_ASSETS 48 #define INIT_ORDER_NETWORKS 45 #define INIT_ORDER_ECONOMY 40 #define INIT_ORDER_OUTPUTS 35 @@ -148,7 +149,8 @@ #define INIT_ORDER_TIMER 1 #define INIT_ORDER_DEFAULT 0 #define INIT_ORDER_AIR -1 -#define INIT_ORDER_PERSISTENCE -2 //before assets because some assets take data from SSPersistence +#define INIT_ORDER_PERSISTENCE -2 +#define INIT_ORDER_PERSISTENT_PAINTINGS -3 // Assets relies on this #define INIT_ORDER_ASSETS -4 #define INIT_ORDER_ICON_SMOOTHING -5 #define INIT_ORDER_OVERLAY -6 @@ -160,7 +162,7 @@ #define INIT_ORDER_PATH -50 #define INIT_ORDER_DISCORD -60 #define INIT_ORDER_EXPLOSIONS -69 -#define INIT_ORDER_STATPANELS -98 +#define INIT_ORDER_STATPANELS -98 #define INIT_ORDER_DEMO -99 // To avoid a bunch of changes related to initialization being written, do this last #define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init. @@ -188,6 +190,7 @@ #define FIRE_PRIORITY_PARALLAX 65 #define FIRE_PRIORITY_INSTRUMENTS 80 #define FIRE_PRIORITY_MOBS 100 +#define FIRE_PRIORITY_ASSETS 105 #define FIRE_PRIORITY_TGUI 110 #define FIRE_PRIORITY_TICKER 200 #define FIRE_PRIORITY_ATMOS_ADJACENCY 300 @@ -212,33 +215,22 @@ // Truly disgusting, TG. Truly disgusting. //! ## Overlays subsystem -///Compile all the overlays for an atom from the cache lists -#define COMPILE_OVERLAYS(A)\ - do {\ - var/list/ad = A.add_overlays;\ - var/list/rm = A.remove_overlays;\ - var/list/po = A.priority_overlays;\ - if(LAZYLEN(rm)){\ - A.overlays -= rm;\ - rm.Cut();\ - }\ - if(LAZYLEN(ad)){\ - A.overlays |= ad;\ - ad.Cut();\ - }\ - if(LAZYLEN(po)){\ - A.overlays |= po;\ - }\ - for(var/I in A.alternate_appearances){\ - var/datum/atom_hud/alternate_appearance/AA = A.alternate_appearances[I];\ +#define POST_OVERLAY_CHANGE(changed_on) \ + if(length(changed_on.overlays) >= MAX_ATOM_OVERLAYS) { \ + var/text_lays = overlays2text(changed_on.overlays); \ + stack_trace("Too many overlays on [changed_on.type] - [length(changed_on.overlays)], refusing to update and cutting.\ + \n What follows is a printout of all existing overlays at the time of the overflow \n[text_lays]"); \ + changed_on.overlays.Cut(); \ + changed_on.add_overlay(mutable_appearance('icons/Testing/greyscale_error.dmi')); \ + } \ + if(alternate_appearances) { \ + for(var/I in changed_on.alternate_appearances){\ + var/datum/atom_hud/alternate_appearance/AA = changed_on.alternate_appearances[I];\ if(AA.transfer_overlays){\ - AA.copy_overlays(A, TRUE);\ + AA.copy_overlays(changed_on, TRUE);\ }\ - }\ - A.flags_1 &= ~OVERLAY_QUEUED_1;\ - if(isturf(A)){SSdemo.mark_turf(A);}\ - if(isobj(A) || ismob(A)){SSdemo.mark_dirty(A);}\ - } while (FALSE) + } \ + } /** Create a new timer and add it to the queue. diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index b81681f5ca58..da19d570e6c4 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -233,6 +233,8 @@ /// This person is crying #define TRAIT_CRYING "crying" +/// This human wants to see the color of their glasses, for some reason +#define TRAIT_SEE_GLASS_COLORS "see_glass_colors" //non-mob traits /// Used for limb-based paralysis, where replacing the limb will fix it. @@ -299,6 +301,7 @@ #define INNATE_TRAIT "innate" #define STATION_TRAIT "station-trait" #define ATTACHMENT_TRAIT "attachment-trait" +#define GLASSES_TRAIT "glasses" // unique trait sources, still defines #define CLONING_POD_TRAIT "cloning-pod" diff --git a/code/__DEFINES/{yogs_defines}/preferences.dm b/code/__DEFINES/{yogs_defines}/preferences.dm index 8e5e630b7986..319d8afa1bbd 100644 --- a/code/__DEFINES/{yogs_defines}/preferences.dm +++ b/code/__DEFINES/{yogs_defines}/preferences.dm @@ -1,5 +1,8 @@ -#define DONOR_CHARACTER_SLOTS 3 +#define DONOR_BYOND (1<<0) +#define DONOR_YOGS (1<<1) +#define DONOR_BYOND_SLOTS 2 +#define DONOR_YOGS_SLOTS 3 #define CHAT_LOOC (1<<11) #define GHOST_CKEY (1<<12) @@ -11,7 +14,8 @@ //YOGS pref.yogstoggles enum's - +// !!! DO NOT ADD ANY NEW ONES HERE !!! +// Use `/datum/preference/toggle` instead. #define QUIET_ROUND (1<<0) //Donor features, quiet round; in /~yogs_defines/, as god intended #define PREF_MOOD (1<<1) //Toggles the use of the Mood feature. Defaults to off, thank god. diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index d4ec01d8833f..722e68d051e6 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -698,11 +698,12 @@ else L1[key] = other_value -/proc/assoc_list_strip_value(list/input) - var/list/ret = list() +/// Turns an associative list into a flat list of keys +/proc/assoc_to_keys(list/input) + var/list/keys = list() for(var/key in input) - ret += key - return ret + keys += key + return keys /proc/compare_list(list/l,list/d) if(!islist(l) || !islist(d)) diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index b9b4c4908fe4..a417b053462a 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -122,6 +122,9 @@ GLOBAL_VAR_INIT(cmp_field, "name") /proc/cmp_job_display_asc(datum/job/A, datum/job/B) return A.display_order - B.display_order +/proc/cmp_department_display_asc(datum/job_department/A, datum/job_department/B) + return A.display_order - B.display_order + /proc/cmp_reagents_asc(datum/reagent/a, datum/reagent/b) return sorttext(initial(b.name),initial(a.name)) diff --git a/code/__HELPERS/colors.dm b/code/__HELPERS/colors.dm new file mode 100644 index 000000000000..4a15f36ef6ff --- /dev/null +++ b/code/__HELPERS/colors.dm @@ -0,0 +1,18 @@ +/// Given a color in the format of "#RRGGBB", will return if the color +/// is dark. +/proc/is_color_dark(color, threshold = 25) + var/hsl = rgb2num(color, COLORSPACE_HSL) + return hsl[3] < threshold + +/// Given a 3 character color (no hash), converts it into #RRGGBB (with hash) +/proc/expand_three_digit_color(color) + if (length_char(color) != 3) + CRASH("Invalid 3 digit color: [color]") + + var/final_color = "#" + + for (var/digit = 1 to 3) + final_color += copytext(color, digit, digit + 1) + final_color += copytext(color, digit, digit + 1) + + return final_color diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index 79f71ee0761b..eddc887b20a5 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -642,7 +642,7 @@ var/mob/living/carbon/human/new_character = new//The mob being spawned. SSjob.SendToLateJoin(new_character) - G_found.client.prefs.copy_to(new_character) + G_found.client.prefs.apply_prefs_to(new_character) new_character.dna.update_dna_identity() new_character.key = G_found.key @@ -658,7 +658,7 @@ var/mob/M = C if(M.client) C = M.client - if(!C || (!C.prefs.windowflashing && !ignorepref)) + if(!C || (!C.prefs.read_preference(/datum/preference/toggle/window_flashing) && !ignorepref)) return winset(C, "mainwindow", "flash=5") diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm index 63cd3fdcc79a..9755c0db725f 100644 --- a/code/__HELPERS/global_lists.dm +++ b/code/__HELPERS/global_lists.dm @@ -29,7 +29,6 @@ init_sprite_accessory_subtypes(/datum/sprite_accessory/spines, GLOB.spines_list) init_sprite_accessory_subtypes(/datum/sprite_accessory/spines_animated, GLOB.animated_spines_list) init_sprite_accessory_subtypes(/datum/sprite_accessory/legs, GLOB.legs_list) - init_sprite_accessory_subtypes(/datum/sprite_accessory/wings, GLOB.r_wings_list,roundstart = TRUE) init_sprite_accessory_subtypes(/datum/sprite_accessory/caps, GLOB.caps_list) init_sprite_accessory_subtypes(/datum/sprite_accessory/moth_wings, GLOB.moth_wings_list) init_sprite_accessory_subtypes(/datum/sprite_accessory/moth_wingsopen, GLOB.moth_wingsopen_list) diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 2cd14840e06b..8e48ce3c35b9 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1052,7 +1052,7 @@ GLOBAL_LIST_EMPTY(friendly_animal_types) var/mob/living/carbon/human/dummy/body = generate_or_wait_for_human_dummy(dummy_key) if(prefs) - prefs.copy_to(body,TRUE,FALSE) + prefs.apply_prefs_to(body,TRUE) if(J) J.equip(body, TRUE, FALSE, outfit_override = outfit_override) else if (outfit_override) @@ -1062,7 +1062,6 @@ GLOBAL_LIST_EMPTY(friendly_animal_types) var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing") for(var/D in showDirs) body.setDir(D) - COMPILE_OVERLAYS(body) var/icon/partial = getFlatIcon(body) out_icon.Insert(partial,dir=D) diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index 249ecc768048..9a7f5e380122 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -214,7 +214,7 @@ /proc/random_skin_tone() return pick(GLOB.skin_tones) -GLOBAL_LIST_INIT(skin_tones, list( +GLOBAL_LIST_INIT(skin_tones, sortList(list( "albino", "caucasian1", "caucasian2", @@ -227,7 +227,22 @@ GLOBAL_LIST_INIT(skin_tones, list( "indian", "african1", "african2" - )) + ))) + +GLOBAL_LIST_INIT(skin_tone_names, list( + "african1" = "Medium brown", + "african2" = "Dark brown", + "albino" = "Albino", + "arab" = "Light brown", + "asian1" = "Ivory", + "asian2" = "Beige", + "caucasian1" = "Porcelain", + "caucasian2" = "Light peach", + "caucasian3" = "Peach", + "indian" = "Brown", + "latino" = "Light beige", + "mediterranean" = "Olive", +)) GLOBAL_LIST_EMPTY(species_list) diff --git a/code/__HELPERS/priority_announce.dm b/code/__HELPERS/priority_announce.dm index 84512b3f1b09..9caa886e50ce 100644 --- a/code/__HELPERS/priority_announce.dm +++ b/code/__HELPERS/priority_announce.dm @@ -65,7 +65,7 @@ else to_chat(M, announcement) if(M.client.prefs.toggles & SOUND_ANNOUNCEMENTS) - if(M.client.prefs.disable_alternative_announcers) + if(M.client.prefs.read_preference(/datum/preference/toggle/disable_alternative_announcers)) SEND_SOUND(M, default_s) else SEND_SOUND(M, s) diff --git a/code/__HELPERS/sanitize_values.dm b/code/__HELPERS/sanitize_values.dm index b91f99c142b3..e3403250baa5 100644 --- a/code/__HELPERS/sanitize_values.dm +++ b/code/__HELPERS/sanitize_values.dm @@ -6,6 +6,13 @@ return number return default +/proc/sanitize_float(number, min=0, max=1, accuracy=1, default=0) + if(isnum(number)) + number = round(number, accuracy) + if(min <= number && number <= max) + return number + return default + /proc/sanitize_text(text, default="") if(istext(text)) return text @@ -79,7 +86,7 @@ return crunch + . -/proc/sanitize_ooccolor(color) +/proc/sanitize_color(color) if(length(color) != length_char(color)) CRASH("Invalid characters in color '[color]'") var/list/HSL = rgb2hsl(hex2num(copytext(color, 2, 4)), hex2num(copytext(color, 4, 6)), hex2num(copytext(color, 6, 8))) diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index 91dc36234382..8df388f21658 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -841,3 +841,8 @@ GLOBAL_LIST_INIT(binary, list("0","1")) #define is_alpha(X) ((text2ascii(X) <= 122) && (text2ascii(X) >= 97)) #define is_digit(X) ((length(X) == 1) && (length(text2num(X)) == 1)) + +/// Removes all non-alphanumerics from the text, keep in mind this can lead to id conflicts +/proc/sanitize_css_class_name(name) + var/static/regex/regex = new(@"[^a-zA-Z0-9]","g") + return replacetext(name, regex, "") diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index fb2621081abf..edd5b6fd0470 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -186,7 +186,7 @@ Turf and target are separate in case you want to teleport some distance from a t return TRUE //Generalised helper proc for letting mobs rename themselves. Used to be clname() and ainame() -/mob/proc/apply_pref_name(role, client/C) +/mob/proc/apply_pref_name(preference_type, client/C) if(!C) C = client var/oldname = real_name @@ -197,20 +197,11 @@ Turf and target are separate in case you want to teleport some distance from a t var/banned = C ? is_banned_from(C.ckey, "Appearance") : null while(loop && safety < 5) - if(C && C.prefs.custom_names[role] && !safety && !banned) - newname = C.prefs.custom_names[role] + if(!safety && !banned) + newname = C?.prefs?.read_preference(preference_type) else - switch(role) - if("human") - newname = random_unique_name(gender) - if("clown") - newname = pick(GLOB.clown_names) - if("mime") - newname = pick(GLOB.mime_names) - if("ai") - newname = pick(GLOB.ai_names) - else - return FALSE + var/datum/preference/preference = GLOB.preference_entries[preference_type] + newname = preference.create_informed_default_value(C.prefs) for(var/mob/living/M in GLOB.player_list) if(M == src) diff --git a/code/_compile_options.dm b/code/_compile_options.dm index 275843733e99..d6abe473446f 100644 --- a/code/_compile_options.dm +++ b/code/_compile_options.dm @@ -20,6 +20,8 @@ //#define VISUALIZE_ACTIVE_TURFS //Highlights atmos active turfs in green #endif +// If defined, we will NOT defer asset generation till later in the game, and will instead do it all at once, during initiialize +//#define DO_NOT_DEFER_ASSETS //#define UNIT_TESTS //Enables unit tests via TEST_RUN_PARAMETER #ifndef PRELOAD_RSC //set to: @@ -44,6 +46,11 @@ #define UNIT_TESTS #endif +#if defined(UNIT_TESTS) +//Ensures all early assets can actually load early +#define DO_NOT_DEFER_ASSETS +#endif + #ifdef TRAVISTESTING #define TESTING #endif @@ -65,3 +72,7 @@ #warn In order to build, run BUILD.bat in the root directory. #warn Consider switching to VSCode editor instead, where you can press Ctrl+Shift+B to build. #endif + +// A reasonable number of maximum overlays an object needs +// If you think you need more, rethink it +#define MAX_ATOM_OVERLAYS 100 diff --git a/code/_globalvars/_regexes.dm b/code/_globalvars/_regexes.dm new file mode 100644 index 000000000000..59f468dcf022 --- /dev/null +++ b/code/_globalvars/_regexes.dm @@ -0,0 +1,3 @@ +//These are a bunch of regex datums for use /((any|every|no|some|head|foot)where(wolf)?\sand\s)+(\.[\.\s]+\s?where\?)?/i + +GLOBAL_DATUM_INIT(is_color, /regex, regex("^#\[0-9a-fA-F]{6}$")) diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm index 6c19e0fdbf20..230aa397f4d2 100644 --- a/code/_globalvars/bitfields.dm +++ b/code/_globalvars/bitfields.dm @@ -132,7 +132,6 @@ GLOBAL_LIST_INIT(bitfields, list( "CONDUCT_1" = CONDUCT_1, "NO_LAVA_GEN_1" = NO_LAVA_GEN_1, "NODECONSTRUCT_1" = NODECONSTRUCT_1, - "OVERLAY_QUEUED_1" = OVERLAY_QUEUED_1, "ON_BORDER_1" = ON_BORDER_1, "NO_RUINS_1" = NO_RUINS_1, "PREVENT_CLICK_UNDER_1" = PREVENT_CLICK_UNDER_1, diff --git a/code/_globalvars/lists/client.dm b/code/_globalvars/lists/client.dm index 5181d870c4f9..0e1ed245c2be 100644 --- a/code/_globalvars/lists/client.dm +++ b/code/_globalvars/lists/client.dm @@ -1,21 +1,2 @@ -GLOBAL_LIST_EMPTY(classic_keybinding_list_by_key) -GLOBAL_LIST_EMPTY(hotkey_keybinding_list_by_key) +GLOBAL_LIST_EMPTY(default_hotkeys) GLOBAL_LIST_EMPTY(keybindings_by_name) - -// This is a mapping from JS keys to Byond - ref: https://keycode.info/ -GLOBAL_LIST_INIT(_kbMap, list( - "UP" = "North", - "RIGHT" = "East", - "DOWN" = "South", - "LEFT" = "West", - "INSERT" = "Insert", - "HOME" = "Northwest", - "PAGEUP" = "Northeast", - "DEL" = "Delete", - "END" = "Southwest", - "PAGEDOWN" = "Southeast", - "SPACEBAR" = "Space", - "ALT" = "Alt", - "SHIFT" = "Shift", - "CONTROL" = "Ctrl" - )) diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm index 403c183156a1..cd34b409ce40 100644 --- a/code/_globalvars/lists/flavor_misc.dm +++ b/code/_globalvars/lists/flavor_misc.dm @@ -38,7 +38,6 @@ GLOBAL_LIST_EMPTY(animated_tails_list_human) GLOBAL_LIST_EMPTY(ears_list) GLOBAL_LIST_EMPTY(wings_list) GLOBAL_LIST_EMPTY(wings_open_list) -GLOBAL_LIST_EMPTY(r_wings_list) GLOBAL_LIST_EMPTY(moth_wings_list) GLOBAL_LIST_EMPTY(moth_wingsopen_list) GLOBAL_LIST_EMPTY(caps_list) @@ -100,18 +99,26 @@ GLOBAL_LIST_INIT(ai_core_display_screens, list( "Triumvirate-M", "Weird")) -/proc/resolve_ai_icon(input) +/// A form of resolve_ai_icon that is guaranteed to never sleep. +/// Not always accurate, but always synchronous. +/proc/resolve_ai_icon_sync(input) + SHOULD_NOT_SLEEP(TRUE) + if(!input || !(input in GLOB.ai_core_display_screens)) return "ai" else if(input == "Random") input = pick(GLOB.ai_core_display_screens - "Random") - if(input == "Portrait") - var/datum/portrait_picker/tgui = new(usr)//create the datum - tgui.ui_interact(usr)//datum has a tgui component, here we open the window - return "ai-portrait" //just take this until they decide return "ai-[lowertext(input)]" +/proc/resolve_ai_icon(input) + if (input == "Portrait") + var/datum/portrait_picker/tgui = new(usr)//create the datum + tgui.ui_interact(usr)//datum has a tgui component, here we open the window + return "ai-portrait" //just take this until they decide + + return resolve_ai_icon_sync(input) + GLOBAL_LIST_INIT(security_depts_prefs, list(SEC_DEPT_RANDOM, SEC_DEPT_NONE, SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, SEC_DEPT_SCIENCE, SEC_DEPT_SUPPLY, SEC_DEPT_SERVICE)) GLOBAL_LIST_INIT(engineering_depts_prefs, list(ENG_DEPT_RANDOM, ENG_DEPT_NONE, ENG_DEPT_MEDICAL, ENG_DEPT_SCIENCE, ENG_DEPT_SUPPLY, ENG_DEPT_SERVICE)) diff --git a/code/_globalvars/lists/keybindings.dm b/code/_globalvars/lists/keybindings.dm index 54a267d291c0..56256e912541 100644 --- a/code/_globalvars/lists/keybindings.dm +++ b/code/_globalvars/lists/keybindings.dm @@ -11,15 +11,13 @@ /proc/add_keybinding(datum/keybinding/instance) GLOB.keybindings_by_name[instance.name] = instance - // Classic - if(LAZYLEN(instance.classic_keys)) - for(var/bound_key in instance.classic_keys) - LAZYADD(GLOB.classic_keybinding_list_by_key[bound_key], list(instance.name)) - // Hotkey if(LAZYLEN(instance.hotkey_keys)) for(var/bound_key in instance.hotkey_keys) - LAZYADD(GLOB.hotkey_keybinding_list_by_key[bound_key], list(instance.name)) + if (bound_key == "Unbound") + LAZYADD(GLOB.default_hotkeys[instance.name], list()) + else + LAZYADD(GLOB.default_hotkeys[instance.name], list(bound_key)) /proc/init_emote_keybinds() for(var/i in subtypesof(/datum/emote)) diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm index 5c9b24e4efdd..f0b9240d33e0 100644 --- a/code/_globalvars/lists/mobs.dm +++ b/code/_globalvars/lists/mobs.dm @@ -39,8 +39,12 @@ GLOBAL_LIST_EMPTY(mob_config_movespeed_type_lookup) GLOBAL_LIST_EMPTY(emote_list) -GLOBAL_LIST_INIT(accents_name2file,strings("accents.json", "accent_file_names", directory = "strings/accents")) // Keys are the names of the accents, values are the name of their .json file. -GLOBAL_LIST_EMPTY(accents_name2regexes) // Holds some complex data regarding accents +/// Keys are the names of the accents, values are the name of their .json file. +GLOBAL_LIST_INIT(accents_name2file, strings("accents.json", "accent_file_names", directory = "strings/accents")) +/// List of all accents +GLOBAL_LIST_INIT(accents_names, list(ACCENT_NONE) + assoc_to_keys(GLOB.accents_name2file)) +/// Holds some complex data regarding accents +GLOBAL_LIST_EMPTY(accents_name2regexes) GLOBAL_LIST_EMPTY(walkingmushroom) diff --git a/code/_globalvars/misc.dm b/code/_globalvars/misc.dm index 7931d91acb50..b407cf78d19c 100644 --- a/code/_globalvars/misc.dm +++ b/code/_globalvars/misc.dm @@ -18,3 +18,15 @@ GLOBAL_LIST_EMPTY(powernets) GLOBAL_VAR_INIT(bsa_unlock, FALSE) //BSA unlocked by head ID swipes GLOBAL_LIST_EMPTY(player_details) // ckey -> /datum/player_details + +GLOBAL_LIST_INIT(preview_backgrounds, list( + "floor" = "Default Tile", + "white" = "Default White Tile", + "darkfull" = "Default Dark Tile", + "wood" = "Wood", + "rockvault" = "Rock Vault", + "grass4" = "Grass", + "black" = "Pure Black", + "grey" = "Pure Grey", + "pure_white" = "Pure White" +)) diff --git a/code/_onclick/hud/action_button.dm b/code/_onclick/hud/action_button.dm index 04202f0f0634..ebb0af4e0da2 100644 --- a/code/_onclick/hud/action_button.dm +++ b/code/_onclick/hud/action_button.dm @@ -96,6 +96,7 @@ usr.client.prefs.action_buttons_screen_locs["[name]_[id]"] = locked ? moved : null return TRUE if(modifiers["alt"]) + var/buttons_locked = usr.client.prefs.read_preference(/datum/preference/toggle/buttons_locked) for(var/V in usr.actions) var/datum/action/A = V if(A.owner != usr) @@ -104,8 +105,8 @@ B.moved = FALSE if(B.id && usr.client) usr.client.prefs.action_buttons_screen_locs["[B.name]_[B.id]"] = null - B.locked = usr.client.prefs.buttons_locked - locked = usr.client.prefs.buttons_locked + B.locked = buttons_locked + locked = buttons_locked moved = FALSE if(id && usr.client) usr.client.prefs.action_buttons_screen_locs["[name]_[id]"] = null diff --git a/code/_onclick/hud/ai.dm b/code/_onclick/hud/ai.dm index 9f038709a11f..1b13ac173001 100644 --- a/code/_onclick/hud/ai.dm +++ b/code/_onclick/hud/ai.dm @@ -189,9 +189,11 @@ ..() var/atom/movable/screen/using + var/widescreen = owner?.client?.prefs?.read_preference(/datum/preference/toggle/widescreen) + // Language menu using = new /atom/movable/screen/language_menu - if(owner?.client?.prefs?.widescreenpref) + if(widescreen) using.screen_loc = ui_ai_language_menu_widescreen else using.screen_loc = ui_ai_language_menu @@ -204,7 +206,7 @@ //Dashboard using = new /atom/movable/screen/ai/dashboard - if(owner?.client?.prefs?.widescreenpref) + if(widescreen) using.screen_loc = ui_ai_dashboard_widescreen else using.screen_loc = ui_ai_dashboard @@ -278,7 +280,7 @@ //Multicamera mode using = new /atom/movable/screen/ai/multicam() - if(owner?.client?.prefs?.widescreenpref) + if(widescreen) using.screen_loc = ui_ai_multicam_widescreen else using.screen_loc = ui_ai_multicam @@ -286,7 +288,7 @@ //Add multicamera camera using = new /atom/movable/screen/ai/add_multicam() - if(owner?.client?.prefs?.widescreenpref) + if(widescreen) using.screen_loc = ui_ai_add_multicam_widescreen else using.screen_loc = ui_ai_add_multicam diff --git a/code/_onclick/hud/credits.dm b/code/_onclick/hud/credits.dm index de3a4a58c425..15b48b83b453 100644 --- a/code/_onclick/hud/credits.dm +++ b/code/_onclick/hud/credits.dm @@ -14,7 +14,7 @@ GLOBAL_LIST(end_titles) GLOB.end_titles += "

Thanks for playing!

" for(var/client/C in GLOB.clients) - if(C.prefs.show_credits) + if(C.prefs.read_preference(/datum/preference/toggle/show_credits)) C.screen += new /atom/movable/screen/credit/title_card(null, null, SSticker.mode.title_icon) sleep(CREDIT_SPAWN_SPEED * 3) for(var/i in 1 to GLOB.end_titles.len) @@ -52,7 +52,7 @@ GLOBAL_LIST(end_titles) /atom/movable/screen/credit/proc/add_to_clients() for(var/client/C in GLOB.clients) - if(C.prefs.show_credits) + if(C.prefs?.read_preference(/datum/preference/toggle/show_credits)) C.screen += src /atom/movable/screen/credit/Destroy() diff --git a/code/_onclick/hud/ghost.dm b/code/_onclick/hud/ghost.dm index 90941e2b2d11..70984d1bd4c9 100644 --- a/code/_onclick/hud/ghost.dm +++ b/code/_onclick/hud/ghost.dm @@ -103,7 +103,7 @@ if(!.) return var/mob/screenmob = viewmob || mymob - if(!screenmob.client.prefs.ghost_hud) - screenmob.client.screen -= static_inventory - else + if(screenmob.client.prefs.read_preference(/datum/preference/toggle/ghost_hud)) screenmob.client.screen += static_inventory + else + screenmob.client.screen -= static_inventory diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm index 4c726bdfe09d..da66d0b0b3ee 100644 --- a/code/_onclick/hud/hud.dm +++ b/code/_onclick/hud/hud.dm @@ -71,12 +71,12 @@ GLOBAL_LIST_INIT(available_ui_styles, list( if (!ui_style) // will fall back to the default if any of these are null - ui_style = ui_style2icon(owner.client && owner.client.prefs && owner.client.prefs.UI_style) + ui_style = ui_style2icon(owner.client?.prefs?.read_preference(/datum/preference/choiced/ui_style)) hide_actions_toggle = new hide_actions_toggle.InitialiseIcon(src) if(mymob.client) - hide_actions_toggle.locked = mymob.client.prefs.buttons_locked + hide_actions_toggle.locked = mymob.client.prefs.read_preference(/datum/preference/toggle/buttons_locked) hand_slots = list() diff --git a/code/_onclick/hud/human.dm b/code/_onclick/hud/human.dm index aca43a389d8e..ea233cc41b85 100644 --- a/code/_onclick/hud/human.dm +++ b/code/_onclick/hud/human.dm @@ -110,7 +110,7 @@ owner.overlay_fullscreen("see_through_darkness", /atom/movable/screen/fullscreen/see_through_darkness) var/widescreen_layout = FALSE - if(owner.client?.prefs?.widescreenpref) + if(owner.client?.prefs?.read_preference(/datum/preference/toggle/widescreen)) widescreen_layout = TRUE var/atom/movable/screen/using diff --git a/code/_onclick/hud/parallax.dm b/code/_onclick/hud/parallax.dm index bf4a8f024e08..a8a85dc90523 100755 --- a/code/_onclick/hud/parallax.dm +++ b/code/_onclick/hud/parallax.dm @@ -60,10 +60,10 @@ var/mob/screenmob = viewmob || mymob var/client/C = screenmob.client if(C.prefs) - var/pref = C.prefs.parallax + var/pref = C.prefs.read_preference(/datum/preference/choiced/parallax) if (isnull(pref)) pref = PARALLAX_HIGH - switch(C.prefs.parallax) + switch(pref) if (PARALLAX_INSANE) C.parallax_throttle = FALSE C.parallax_layers_max = 5 diff --git a/code/_onclick/hud/plane_master.dm b/code/_onclick/hud/plane_master.dm index b283837a499a..8f0ca7843d5b 100644 --- a/code/_onclick/hud/plane_master.dm +++ b/code/_onclick/hud/plane_master.dm @@ -61,7 +61,7 @@ /atom/movable/screen/plane_master/game_world/backdrop(mob/mymob) filters = list() - if(istype(mymob) && mymob.client && mymob.client.prefs && mymob.client.prefs.ambientocclusion) + if(istype(mymob) && mymob.client?.prefs?.read_preference(/datum/preference/toggle/ambient_occlusion)) filters += AMBIENT_OCCLUSION if(istype(mymob) && mymob.eye_blurry) filters += GAUSSIAN_BLUR(clamp(mymob.eye_blurry*0.1,0.6,3)) @@ -154,7 +154,7 @@ /atom/movable/screen/plane_master/runechat/backdrop(mob/mymob) filters = list() - if(istype(mymob) && mymob.client?.prefs?.ambientocclusion) + if(istype(mymob) && mymob.client?.prefs?.read_preference(/datum/preference/toggle/ambient_occlusion)) filters += AMBIENT_OCCLUSION /atom/movable/screen/plane_master/o_light_visual diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm index 81e1c0f36911..abea6dc94a1a 100644 --- a/code/_onclick/observer.dm +++ b/code/_onclick/observer.dm @@ -61,7 +61,7 @@ return TRUE else if(IsAdminGhost(user)) attack_ai(user) - else if(user.client.prefs.inquisitive_ghost) + else if(user.client.prefs.read_preference(/datum/preference/toggle/inquisitive_ghost)) user.examinate(src) return FALSE diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index e11ac7ec7622..f8058ef4f874 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -171,8 +171,6 @@ key_mode = KEY_MODE_TEXT value_mode = VALUE_MODE_FLAG -/datum/config_entry/flag/join_with_mutant_humans //players can pick mutant bodyparts for humans before joining the game - /datum/config_entry/flag/no_intercept_report //Whether or not to send a communications intercept report roundstart. This may be overridden by gamemodes. /datum/config_entry/number/arrivals_shuttle_dock_window //Time from when a player late joins on the arrivals shuttle to when the shuttle docks on the station diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index 75899ad83198..5e6f1ba87a9f 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -532,5 +532,8 @@ /// logs all timers in buckets on automatic bucket reset (Useful for timer debugging) /datum/config_entry/flag/log_timers_on_bucket_reset +/datum/config_entry/flag/cache_assets + default = TRUE + /// Whether demos are written, if not set demo SS never initializes /datum/config_entry/flag/demos_enabled diff --git a/code/controllers/subsystem/asset_loading.dm b/code/controllers/subsystem/asset_loading.dm new file mode 100644 index 000000000000..646dd34726f8 --- /dev/null +++ b/code/controllers/subsystem/asset_loading.dm @@ -0,0 +1,34 @@ +/// Allows us to lazyload asset datums +/// Anything inserted here will fully load if directly gotten +/// So this just serves to remove the requirement to load assets fully during init +SUBSYSTEM_DEF(asset_loading) + name = "Asset Loading" + priority = FIRE_PRIORITY_ASSETS + flags = SS_NO_INIT + runlevels = RUNLEVEL_LOBBY|RUNLEVELS_DEFAULT + + var/list/datum/asset/generate_queue = list() + +/datum/controller/subsystem/asset_loading/stat_entry(msg) + msg = "Q:[length(generate_queue)]" + + return ..() + +/datum/controller/subsystem/asset_loading/fire(resumed) + while(length(generate_queue)) + var/datum/asset/to_load = generate_queue[generate_queue.len] + + to_load.queued_generation() + + if(MC_TICK_CHECK) + return + generate_queue.len-- + +/datum/controller/subsystem/asset_loading/proc/queue_asset(datum/asset/queue) +#ifdef DO_NOT_DEFER_ASSETS + stack_trace("We queued an instance of [queue.type] for lateloading despite not allowing it") +#endif + generate_queue += queue + +/datum/controller/subsystem/asset_loading/proc/dequeue_asset(datum/asset/queue) + generate_queue -= queue diff --git a/code/controllers/subsystem/assets.dm b/code/controllers/subsystem/assets.dm index 9626c9eda0f8..485433048e4e 100644 --- a/code/controllers/subsystem/assets.dm +++ b/code/controllers/subsystem/assets.dm @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(assets) loading_points = 3 SECONDS // Yogs -- loading times - var/list/cache = list() + var/list/datum/asset_cache_item/cache = list() var/list/preload = list() var/datum/asset_transport/transport = new() @@ -29,7 +29,7 @@ SUBSYSTEM_DEF(assets) for(var/type in typesof(/datum/asset)) var/datum/asset/A = type if (type != initial(A._abstract)) - get_asset_datum(type) + load_asset_datum(type) transport.Initialize(cache) diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm index 276554112cf7..625317c23513 100644 --- a/code/controllers/subsystem/atoms.dm +++ b/code/controllers/subsystem/atoms.dm @@ -19,6 +19,9 @@ SUBSYSTEM_DEF(atoms) var/init_start_time + /// Atoms that will be deleted once the subsystem is initialized + var/list/queued_deletions = list() + initialized = INITIALIZATION_INSSATOMS /datum/controller/subsystem/atoms/Initialize(timeofday) @@ -51,6 +54,12 @@ SUBSYSTEM_DEF(atoms) A.LateInitialize() testing("Late initialized [late_loaders.len] atoms") late_loaders.Cut() + + for (var/queued_deletion in queued_deletions) + qdel(queued_deletion) + + testing("[queued_deletions.len] atoms were queued for deletion.") + queued_deletions.Cut() /// Actually creates the list of atoms. Exists soley so a runtime in the creation logic doesn't cause initalized to totally break /datum/controller/subsystem/atoms/proc/CreateAtoms(list/atoms) @@ -191,6 +200,14 @@ SUBSYSTEM_DEF(atoms) if(fails & BAD_INIT_SLEPT) . += "- Slept during Initialize()\n" +/// Prepares an atom to be deleted once the atoms SS is initialized. +/datum/controller/subsystem/atoms/proc/prepare_deletion(atom/target) + if (initialized == INITIALIZATION_INNEW_REGULAR) + // Atoms SS has already completed, just kill it now. + qdel(target) + else + queued_deletions += WEAKREF(target) + /datum/controller/subsystem/atoms/Shutdown() var/initlog = InitLog() if(initlog) diff --git a/code/controllers/subsystem/early_assets.dm b/code/controllers/subsystem/early_assets.dm new file mode 100644 index 000000000000..5ce669bec83b --- /dev/null +++ b/code/controllers/subsystem/early_assets.dm @@ -0,0 +1,24 @@ +/// Initializes any assets that need to be loaded ASAP. +/// This houses preference menu assets, since they can be loaded at any time, +/// most dangerously before the atoms SS initializes. +/// Thus, we want it to fail consistently in CI as if it would've if a player +/// opened it up early. +SUBSYSTEM_DEF(early_assets) + name = "Early Assets" + init_order = INIT_ORDER_EARLY_ASSETS + flags = SS_NO_FIRE + +/datum/controller/subsystem/early_assets/Initialize() + for (var/datum/asset/asset_type as anything in subtypesof(/datum/asset)) + if (initial(asset_type._abstract) == asset_type) + continue + + if (!initial(asset_type.early)) + continue + + if (!load_asset_datum(asset_type)) + stack_trace("Could not initialize early asset [asset_type]!") + + CHECK_TICK + + return SS_INIT_SUCCESS diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm index f1cd88fa4210..945b0b11cb36 100644 --- a/code/controllers/subsystem/job.dm +++ b/code/controllers/subsystem/job.dm @@ -3,10 +3,19 @@ SUBSYSTEM_DEF(job) init_order = INIT_ORDER_JOBS flags = SS_NO_FIRE - var/list/occupations = list() //List of all jobs - var/list/datum/job/name_occupations = list() //Dict of jobs, keys are titles - var/list/datum/job/name_occupations_all = list() //Dict of ALL JOBS, EVEN DISABLED ONES, keys are titles + /// List of all jobs. + var/list/occupations = list() + /// List of jobs that can be joined through the starting menu. + var/list/datum/job/joinable_occupations = list() + /// Dictionary of all jobs, keys are titles. + var/list/name_occupations = list() + /// Dictionary of all jobs EVEN DISABLED, keys are types. + var/list/name_occupations_all = list() var/list/type_occupations = list() //Dict of all jobs, keys are types + /// List of all departments with joinable jobs. + var/list/datum/job_department/joinable_departments = list() + /// List of all joinable departments indexed by their typepath, sorted by their own display order. + var/list/datum/job_department/joinable_departments_by_type = list() var/list/unassigned = list() //Players who need jobs var/initial_players_to_assign = 0 //used for checking against population caps @@ -18,11 +27,10 @@ SUBSYSTEM_DEF(job) var/list/level_order = list(JP_HIGH,JP_MEDIUM,JP_LOW) /datum/controller/subsystem/job/Initialize(timeofday) - if(!occupations.len) + if(!length(occupations)) SetupOccupations() if(CONFIG_GET(flag/load_jobs_from_txt)) LoadJobs() - generate_selectable_species() set_overflow_role(CONFIG_GET(string/overflow_job)) return SS_INIT_SUCCESS @@ -46,6 +54,11 @@ SUBSYSTEM_DEF(job) if(!all_jobs.len) to_chat(world, span_boldannounce("Error setting up jobs, no job datums found")) return 0 + + var/list/new_occupations = list() + var/list/new_joinable_occupations = list() + var/list/new_joinable_departments = list() + var/list/new_joinable_departments_by_type = list() for(var/J in all_jobs) var/datum/job/job = new J() @@ -61,23 +74,62 @@ SUBSYSTEM_DEF(job) if(SEND_SIGNAL(job, SSmapping.config.map_name)) //Even though we initialize before mapping, this is fine because the config is loaded at new testing("Removed [job.type] due to map config") continue - occupations += job + + // All jobs are late joinable at the moment + + new_occupations += job + new_joinable_occupations += job name_occupations[job.title] = job type_occupations[J] = job - return 1 + if(!LAZYLEN(job.departments_list)) + var/datum/job_department/department = new_joinable_departments_by_type[/datum/job_department/undefined] + if(!department) + department = new /datum/job_department/undefined() + new_joinable_departments_by_type[/datum/job_department/undefined] = department + department.add_job(job) + continue + for(var/department_type in job.departments_list) + var/datum/job_department/department = new_joinable_departments_by_type[department_type] + if(!department) + department = new department_type() + new_joinable_departments_by_type[department_type] = department + department.add_job(job) + + sortTim(new_occupations, /proc/cmp_job_display_asc) + + sortTim(new_joinable_departments_by_type, /proc/cmp_department_display_asc, associative = TRUE) + for(var/department_type in new_joinable_departments_by_type) + var/datum/job_department/department = new_joinable_departments_by_type[department_type] + sortTim(department.department_jobs, /proc/cmp_job_display_asc) + new_joinable_departments += department + + occupations = new_occupations + joinable_occupations = sortTim(new_joinable_occupations, /proc/cmp_job_display_asc) + joinable_departments = new_joinable_departments + joinable_departments_by_type = new_joinable_departments_by_type + + return TRUE /datum/controller/subsystem/job/proc/GetJob(rank) - if(!occupations.len) + RETURN_TYPE(/datum/job) + if(!length(occupations)) SetupOccupations() return name_occupations[rank] /datum/controller/subsystem/job/proc/GetJobType(jobtype) - if(!occupations.len) + RETURN_TYPE(/datum/job) + if(!length(occupations)) SetupOccupations() return type_occupations[jobtype] +/datum/controller/subsystem/job/proc/get_department_type(department_type) + RETURN_TYPE(/datum/job_department) + if(!length(occupations)) + SetupOccupations() + return joinable_departments_by_type[department_type] + /datum/controller/subsystem/job/proc/GetPlayerAltTitle(mob/dead/new_player/player, rank) return player.client.prefs.GetPlayerAltTitle(GetJob(rank)) @@ -436,26 +488,32 @@ SUBSYSTEM_DEF(job) //We couldn't find a job from prefs for this guy. /datum/controller/subsystem/job/proc/HandleUnassigned(mob/dead/new_player/player) + var/jobless_role = player.client.prefs.read_preference(/datum/preference/choiced/jobless_role) + if(PopcapReached()) RejectPlayer(player) - else if(player.client.prefs.joblessrole == BEOVERFLOW) - var/allowed_to_be_a_loser = !is_banned_from(player.ckey, SSjob.overflow_role) - if(QDELETED(player) || !allowed_to_be_a_loser) - RejectPlayer(player) - else - if(!AssignRole(player, SSjob.overflow_role)) + return + + switch (jobless_role) + if (BEOVERFLOW) + var/allowed_to_be_a_loser = !is_banned_from(player.ckey, SSjob.overflow_role) + if(QDELETED(player) || !allowed_to_be_a_loser) RejectPlayer(player) - else if(player.client.prefs.joblessrole == BERANDOMJOB) - if(!GiveRandomJob(player)) + else + if(!AssignRole(player, SSjob.overflow_role)) + RejectPlayer(player) + if (BERANDOMJOB) + if(!GiveRandomJob(player)) + RejectPlayer(player) + if (RETURNTOLOBBY) RejectPlayer(player) - else if(player.client.prefs.joblessrole == RETURNTOLOBBY) - RejectPlayer(player) - else //Something gone wrong if we got here. - var/message = "DO: [player] fell through handling unassigned" - JobDebug(message) - log_game(message) - message_admins(message) - RejectPlayer(player) + else //Something gone wrong if we got here. + var/message = "DO: [player] fell through handling unassigned" + JobDebug(message) + log_game(message) + message_admins(message) + RejectPlayer(player) + //Gives the player the stuff he should have with his rank /datum/controller/subsystem/job/proc/EquipRank(mob/M, rank, joined_late = FALSE) var/mob/dead/new_player/newplayer @@ -539,7 +597,7 @@ SUBSYSTEM_DEF(job) job.give_map_flare(living_mob, M) var/obj/item/modular_computer/RPDA = locate(/obj/item/modular_computer/tablet) in living_mob.GetAllContents() if(istype(RPDA)) - RPDA.device_theme = GLOB.pda_themes[M.client.prefs.pda_theme] + RPDA.device_theme = GLOB.pda_themes[M.client?.prefs.read_preference(/datum/preference/choiced/pda_theme)] var/obj/item/computer_hardware/hard_drive/hard_drive = RPDA.all_components[MC_HDD] var/datum/computer_file/program/pdamessager/msgr = locate(/datum/computer_file/program/pdamessager) in hard_drive.stored_files if(msgr) @@ -565,7 +623,7 @@ SUBSYSTEM_DEF(job) var/list/player_box = list() for(var/mob/H in GLOB.player_list) if(H.client && H.client.prefs) // Prefs was null once and there was no bar - player_box += H.client.prefs.bar_choice + player_box += H.client.prefs.read_preference(/datum/preference/choiced/bar_choice) var/choice if(player_box.len == 0) diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm index 766d5af79758..1ce881982aa2 100644 --- a/code/controllers/subsystem/mapping.dm +++ b/code/controllers/subsystem/mapping.dm @@ -368,7 +368,7 @@ GLOBAL_LIST_EMPTY(the_station_areas) var/pmv = CONFIG_GET(flag/preference_map_voting) if(pmv) for (var/client/c in GLOB.clients) - var/vote = c.prefs.preferred_map + var/vote = c.prefs.read_preference(/datum/preference/choiced/preferred_map) if (!vote) if (global.config.defaultmap) mapvotes[global.config.defaultmap.map_name] += 1 diff --git a/code/controllers/subsystem/overlays.dm b/code/controllers/subsystem/overlays.dm index c75561480764..517ccf8f70b7 100644 --- a/code/controllers/subsystem/overlays.dm +++ b/code/controllers/subsystem/overlays.dm @@ -1,183 +1,83 @@ SUBSYSTEM_DEF(overlays) name = "Overlay" - flags = SS_TICKER - wait = 1 - priority = FIRE_PRIORITY_OVERLAYS - init_order = INIT_ORDER_OVERLAY + flags = SS_NO_FIRE|SS_NO_INIT - loading_points = 2.3 SECONDS // Yogs -- loading times - - var/list/queue var/list/stats - var/list/overlay_icon_state_caches - var/list/overlay_icon_cache /datum/controller/subsystem/overlays/PreInit() - overlay_icon_state_caches = list() - overlay_icon_cache = list() - queue = list() stats = list() -/datum/controller/subsystem/overlays/Initialize() - initialized = TRUE - fire(mc_check = FALSE) - return SS_INIT_SUCCESS - - -/datum/controller/subsystem/overlays/stat_entry(msg) - msg = "Ov:[length(queue)]" - return ..() - - /datum/controller/subsystem/overlays/Shutdown() text2file(render_stats(stats), "[GLOB.log_directory]/overlay.log") - /datum/controller/subsystem/overlays/Recover() - overlay_icon_state_caches = SSoverlays.overlay_icon_state_caches - overlay_icon_cache = SSoverlays.overlay_icon_cache - queue = SSoverlays.queue - - -/datum/controller/subsystem/overlays/fire(resumed = FALSE, mc_check = TRUE) - var/list/queue = src.queue - var/static/count = 0 - if (count) - var/c = count - count = 0 //so if we runtime on the Cut, we don't try again. - queue.Cut(1,c+1) - - for (var/thing in queue) - count++ - if(thing) - STAT_START_STOPWATCH - var/atom/A = thing - COMPILE_OVERLAYS(A) - STAT_STOP_STOPWATCH - STAT_LOG_ENTRY(stats, A.type) - if(mc_check) - if(MC_TICK_CHECK) - break - else - CHECK_TICK - - if (count) - queue.Cut(1,count+1) - count = 0 + stats = SSoverlays.stats + +/// Converts an overlay list into text for debug printing +/// Of note: overlays aren't actually mutable appearances, they're just appearances +/// Don't have access to that type tho, so this is the best you're gonna get +/proc/overlays2text(list/overlays) + var/list/unique_overlays = list() + // As anything because we're basically doing type coerrsion, rather then actually filtering for mutable apperances + for(var/mutable_appearance/overlay as anything in overlays) + var/key = "[overlay.icon]-[overlay.icon_state]-[overlay.dir]" + unique_overlays[key] += 1 + var/list/output_text = list() + for(var/key in unique_overlays) + output_text += "([key]) = [unique_overlays[key]]" + return output_text.Join("\n") /proc/iconstate2appearance(icon, iconstate) var/static/image/stringbro = new() - var/list/icon_states_cache = SSoverlays.overlay_icon_state_caches - var/list/cached_icon = icon_states_cache[icon] - if (cached_icon) - var/cached_appearance = cached_icon["[iconstate]"] - if (cached_appearance) - return cached_appearance stringbro.icon = icon stringbro.icon_state = iconstate - if (!cached_icon) //not using the macro to save an associated lookup - cached_icon = list() - icon_states_cache[icon] = cached_icon - var/cached_appearance = stringbro.appearance - cached_icon["[iconstate]"] = cached_appearance - return cached_appearance + return stringbro.appearance /proc/icon2appearance(icon) var/static/image/iconbro = new() - var/list/icon_cache = SSoverlays.overlay_icon_cache - . = icon_cache[icon] - if (!.) - iconbro.icon = icon - . = iconbro.appearance - icon_cache[icon] = . - -/atom/proc/build_appearance_list(old_overlays) - var/static/image/appearance_bro = new() - var/list/new_overlays = list() - if (!islist(old_overlays)) - old_overlays = list(old_overlays) - for (var/overlay in old_overlays) + iconbro.icon = icon + return iconbro.appearance + +/atom/proc/build_appearance_list(build_overlays) + if (!islist(build_overlays)) + build_overlays = list(build_overlays) + for (var/overlay in build_overlays) if(!overlay) + build_overlays -= overlay continue if (istext(overlay)) - new_overlays += iconstate2appearance(icon, overlay) + build_overlays -= overlay + build_overlays += iconstate2appearance(icon, overlay) else if(isicon(overlay)) - new_overlays += icon2appearance(overlay) - else - if(isloc(overlay)) - var/atom/A = overlay - if (A.flags_1 & OVERLAY_QUEUED_1) - COMPILE_OVERLAYS(A) - appearance_bro.appearance = overlay //this works for images and atoms too! - if(!ispath(overlay)) - var/image/I = overlay - appearance_bro.dir = I.dir - new_overlays += appearance_bro.appearance - return new_overlays - -#define NOT_QUEUED_ALREADY (!(flags_1 & OVERLAY_QUEUED_1)) -#define QUEUE_FOR_COMPILE flags_1 |= OVERLAY_QUEUED_1; SSoverlays.queue += src; -/atom/proc/cut_overlays(priority = FALSE) - LAZYINITLIST(priority_overlays) - LAZYINITLIST(remove_overlays) - LAZYINITLIST(add_overlays) - remove_overlays = overlays.Copy() - add_overlays.Cut() - - if(priority) - priority_overlays.Cut() - - //If not already queued for work and there are overlays to remove - if(NOT_QUEUED_ALREADY && remove_overlays.len) - QUEUE_FOR_COMPILE - -/atom/proc/cut_overlay(list/overlays, priority) + build_overlays -= overlay + build_overlays += icon2appearance(overlay) + return build_overlays + +/atom/proc/cut_overlays() + STAT_START_STOPWATCH + overlays = null + POST_OVERLAY_CHANGE(src) + STAT_STOP_STOPWATCH + STAT_LOG_ENTRY(SSoverlays.stats, type) + +/atom/proc/cut_overlay(list/remove_overlays) if(!overlays) return - overlays = build_appearance_list(overlays) - LAZYINITLIST(add_overlays) //always initialized after this point - LAZYINITLIST(priority_overlays) - LAZYINITLIST(remove_overlays) - var/a_len = add_overlays.len - var/r_len = remove_overlays.len - var/p_len = priority_overlays.len - remove_overlays += overlays - add_overlays -= overlays - - - if(priority) - var/list/cached_priority = priority_overlays - LAZYREMOVE(cached_priority, overlays) - - var/fa_len = add_overlays.len - var/fr_len = remove_overlays.len - var/fp_len = priority_overlays.len - - //If not already queued and there is work to be done - if(NOT_QUEUED_ALREADY && (fa_len != a_len || fr_len != r_len || fp_len != p_len)) - QUEUE_FOR_COMPILE - -/atom/proc/add_overlay(list/overlays, priority = FALSE) + STAT_START_STOPWATCH + overlays -= build_appearance_list(remove_overlays) + POST_OVERLAY_CHANGE(src) + STAT_STOP_STOPWATCH + STAT_LOG_ENTRY(SSoverlays.stats, type) + +/atom/proc/add_overlay(list/add_overlays) if(!overlays) return - overlays = build_appearance_list(overlays) - - LAZYINITLIST(add_overlays) //always initialized after this point - LAZYINITLIST(priority_overlays) - var/a_len = add_overlays.len - var/p_len = priority_overlays.len - - if(priority) - priority_overlays += overlays //or in the image. Can we use [image] = image? - var/fp_len = priority_overlays.len - if(NOT_QUEUED_ALREADY && fp_len != p_len) - QUEUE_FOR_COMPILE - else - add_overlays += overlays - var/fa_len = add_overlays.len - if(NOT_QUEUED_ALREADY && fa_len != a_len) - QUEUE_FOR_COMPILE + STAT_START_STOPWATCH + overlays += build_appearance_list(add_overlays) + POST_OVERLAY_CHANGE(src) + STAT_STOP_STOPWATCH + STAT_LOG_ENTRY(SSoverlays.stats, type) /atom/proc/copy_overlays(atom/other, cut_old) //copys our_overlays from another atom if(!other) @@ -185,18 +85,21 @@ SUBSYSTEM_DEF(overlays) cut_overlays() return + STAT_START_STOPWATCH var/list/cached_other = other.overlays.Copy() - if(cached_other) - if(cut_old || !LAZYLEN(overlays)) - remove_overlays = overlays - add_overlays = cached_other - if(NOT_QUEUED_ALREADY) - QUEUE_FOR_COMPILE - else if(cut_old) - cut_overlays() - -#undef NOT_QUEUED_ALREADY -#undef QUEUE_FOR_COMPILE + if(cut_old) + if(cached_other) + overlays = cached_other + else + overlays = null + POST_OVERLAY_CHANGE(src) + STAT_STOP_STOPWATCH + STAT_LOG_ENTRY(SSoverlays.stats, type) + else if(cached_other) + overlays += cached_other + POST_OVERLAY_CHANGE(src) + STAT_STOP_STOPWATCH + STAT_LOG_ENTRY(SSoverlays.stats, type) //TODO: Better solution for these? /image/proc/add_overlay(x) diff --git a/code/controllers/subsystem/persistence.dm b/code/controllers/subsystem/persistence.dm index 98724529cf94..26b0d121e661 100644 --- a/code/controllers/subsystem/persistence.dm +++ b/code/controllers/subsystem/persistence.dm @@ -14,8 +14,6 @@ SUBSYSTEM_DEF(persistence) var/list/picture_logging_information = list() var/list/obj/structure/sign/picture_frame/photo_frames = list() var/list/obj/item/storage/photo_album/photo_albums = list() - var/list/obj/structure/sign/painting/painting_frames = list() - var/list/paintings = list() /datum/controller/subsystem/persistence/Initialize() LoadPoly() @@ -26,7 +24,6 @@ SUBSYSTEM_DEF(persistence) if(CONFIG_GET(flag/use_antag_rep)) LoadAntagReputation() LoadRandomizedRecipes() - LoadPaintings() return SS_INIT_SUCCESS /datum/controller/subsystem/persistence/proc/LoadPoly() @@ -151,7 +148,6 @@ SUBSYSTEM_DEF(persistence) if(CONFIG_GET(flag/use_antag_rep)) CollectAntagReputation() SaveRandomizedRecipes() - SavePaintings() SaveScars() /datum/controller/subsystem/persistence/proc/GetPhotoAlbums() @@ -332,30 +328,18 @@ SUBSYSTEM_DEF(persistence) fdel(json_file) WRITE_FILE(json_file, json_encode(file_data)) -/datum/controller/subsystem/persistence/proc/LoadPaintings() - var/json_file = file("data/paintings.json") - if(fexists(json_file)) - paintings = json_decode(file2text(json_file)) - - for(var/obj/structure/sign/painting/P in painting_frames) - P.load_persistent() - -/datum/controller/subsystem/persistence/proc/SavePaintings() - for(var/obj/structure/sign/painting/P in painting_frames) - P.save_persistent() - - var/json_file = file("data/paintings.json") - fdel(json_file) - WRITE_FILE(json_file, json_encode(paintings)) - /datum/controller/subsystem/persistence/proc/SaveScars() for(var/i in GLOB.joined_player_list) var/mob/living/carbon/human/ending_human = get_mob_by_ckey(i) - if(!istype(ending_human) || !ending_human.mind?.original_character_slot_index || !ending_human.client || !ending_human.client.prefs || !ending_human.client.prefs.persistent_scars) + if(!istype(ending_human) || !ending_human.mind?.original_character_slot_index || !ending_human.client || !ending_human.client.prefs || !ending_human.client.prefs.read_preference(/datum/preference/toggle/persistent_scars)) continue var/mob/living/carbon/human/original_human = ending_human.mind.original_character - if(!original_human || original_human.stat == DEAD || !original_human.all_scars || original_human != ending_human) + + if(!original_human) + continue + + if(original_human.stat == DEAD || !original_human.all_scars || original_human != ending_human) original_human.save_persistent_scars(TRUE) else original_human.save_persistent_scars() diff --git a/code/controllers/subsystem/persistent_paintings.dm b/code/controllers/subsystem/persistent_paintings.dm new file mode 100644 index 000000000000..6e6c2b1c6279 --- /dev/null +++ b/code/controllers/subsystem/persistent_paintings.dm @@ -0,0 +1,29 @@ +SUBSYSTEM_DEF(persistent_paintings) + name = "Persistent Paintings" + init_order = INIT_ORDER_PERSISTENT_PAINTINGS + flags = SS_NO_FIRE + + /// A list of painting frames that this controls + var/list/obj/structure/sign/painting/painting_frames = list() + + /// A map of identifiers (such as library) to paintings from paintings.json + var/list/paintings = list() + +/datum/controller/subsystem/persistent_paintings/Initialize(start_timeofday) + var/json_file = file("data/paintings.json") + if(fexists(json_file)) + paintings = json_decode(file2text(json_file)) + + for(var/obj/structure/sign/painting/painting_frame as anything in painting_frames) + painting_frame.load_persistent() + + return SS_INIT_SUCCESS + +/// Saves all persistent paintings +/datum/controller/subsystem/persistent_paintings/proc/save_paintings() + for(var/obj/structure/sign/painting/painting_frame as anything in painting_frames) + painting_frame.save_persistent() + + var/json_file = file("data/paintings.json") + fdel(json_file) + WRITE_FILE(json_file, json_encode(paintings)) diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index b7c36e8dfafb..92d1241ebfc4 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -57,3 +57,73 @@ PROCESSING_SUBSYSTEM_DEF(quirks) badquirk = TRUE if(badquirk) cli.prefs.save_character() + +/// Takes a list of quirk names and returns a new list of quirks that would +/// be valid. +/// If no changes need to be made, will return the same list. +/// Expects all quirk names to be unique, but makes no other expectations. +/datum/controller/subsystem/processing/quirks/proc/filter_invalid_quirks(list/quirks, client/C) + var/list/new_quirks = list() + var/list/positive_quirks = list() + var/balance = 0 + + // If moods are globally enabled, or this guy does indeed have his mood pref set to Enabled + var/ismoody = (!CONFIG_GET(flag/disable_human_mood) || (C.prefs.yogtoggles & PREF_MOOD)) + + for (var/quirk_name in quirks) + var/datum/quirk/quirk = SSquirks.quirks[quirk_name] + if (isnull(quirk)) + continue + + if (initial(quirk.mood_quirk) && !ismoody) + continue + + // Returns error string, FALSE if quirk is okay to have + var/datum/quirk/quirk_obj = new quirk(no_init = TRUE) + if (quirk_obj?.check_quirk(C.prefs)) + continue + qdel(quirk_obj) + + var/blacklisted = FALSE + + for (var/list/blacklist as anything in quirk_blacklist) + if (!(quirk in blacklist)) + continue + + for (var/other_quirk in blacklist) + if (other_quirk in new_quirks) + blacklisted = TRUE + break + + if (blacklisted) + break + + if (blacklisted) + continue + + var/value = initial(quirk.value) + if (value > 0) + if (positive_quirks.len == MAX_QUIRKS) + continue + + positive_quirks[quirk_name] = value + + balance += value + new_quirks += quirk_name + + if (balance > 0) + var/balance_left_to_remove = balance + + for (var/positive_quirk in positive_quirks) + var/value = positive_quirks[positive_quirk] + balance_left_to_remove -= value + new_quirks -= positive_quirk + + if (balance_left_to_remove <= 0) + break + + // It is guaranteed that if no quirks are invalid, you can simply check through `==` + if (new_quirks.len == quirks.len) + return quirks + + return new_quirks diff --git a/code/controllers/subsystem/tgui.dm b/code/controllers/subsystem/tgui.dm index 51dfb15d59d2..92e787fa09f0 100644 --- a/code/controllers/subsystem/tgui.dm +++ b/code/controllers/subsystem/tgui.dm @@ -1,10 +1,13 @@ +/*! + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + /** * tgui subsystem * * Contains all tgui state and subsystem code. * - * Copyright (c) 2020 Aleksej Komarov - * SPDX-License-Identifier: MIT */ SUBSYSTEM_DEF(tgui) @@ -25,6 +28,10 @@ SUBSYSTEM_DEF(tgui) /datum/controller/subsystem/tgui/PreInit() basehtml = file2text('tgui/public/tgui.html') + // Inject inline polyfills + var/polyfill = file2text('tgui/public/tgui-polyfill.bundle.js') + polyfill = "" + basehtml = replacetextEx(basehtml, "", polyfill) /datum/controller/subsystem/tgui/Shutdown() close_all_uis() @@ -33,7 +40,7 @@ SUBSYSTEM_DEF(tgui) msg = "P:[length(open_uis)]" return ..() -/datum/controller/subsystem/tgui/fire(resumed = 0) +/datum/controller/subsystem/tgui/fire(resumed = FALSE) if(!resumed) src.current_run = open_uis.Copy() // Cache for sanic speed (lists are references anyways) @@ -42,7 +49,7 @@ SUBSYSTEM_DEF(tgui) var/datum/tgui/ui = current_run[current_run.len] current_run.len-- // TODO: Move user/src_object check to process() - if(ui && ui.user && ui.src_object) + if(ui?.user && ui.src_object) ui.process(wait * 0.1) else open_uis.Remove(ui) @@ -191,7 +198,7 @@ SUBSYSTEM_DEF(tgui) return count for(var/datum/tgui/ui in open_uis_by_src[key]) // Check if UI is valid. - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) + if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user)) ui.process(wait * 0.1, force = 1) count++ return count @@ -213,7 +220,7 @@ SUBSYSTEM_DEF(tgui) return count for(var/datum/tgui/ui in open_uis_by_src[key]) // Check if UI is valid. - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) + if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user)) ui.close() count++ return count @@ -230,7 +237,7 @@ SUBSYSTEM_DEF(tgui) for(var/key in open_uis_by_src) for(var/datum/tgui/ui in open_uis_by_src[key]) // Check if UI is valid. - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) + if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user)) ui.close() count++ return count diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 23a5e383d147..0c3d2230f595 100755 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -312,7 +312,7 @@ SUBSYSTEM_DEF(ticker) for(var/mob/P in GLOB.player_list) if(P.client && P.client.prefs) - if(P.client.prefs.disable_alternative_announcers) + if(P.client.prefs.read_preference(/datum/preference/toggle/disable_alternative_announcers)) SEND_SOUND(P, sound(default_sound)) continue SEND_SOUND(P, sound(random_sound)) diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index 35ab0c4671eb..d129066cb14d 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -69,7 +69,7 @@ SUBSYSTEM_DEF(vote) else if(mode == "map") for (var/non_voter_ckey in non_voters) var/client/C = non_voters[non_voter_ckey] - var/preferred_map = C.prefs.preferred_map + var/preferred_map = C.prefs.read_preference(/datum/preference/choiced/preferred_map) if(isnull(global.config.defaultmap)) continue if(!preferred_map) diff --git a/code/datums/action.dm b/code/datums/action.dm index dc237630e4a8..f75fbc71a9b1 100644 --- a/code/datums/action.dm +++ b/code/datums/action.dm @@ -72,7 +72,7 @@ M.actions += src if(M.client) M.client.screen += button - button.locked = M.client.prefs.buttons_locked || button.id ? M.client.prefs.action_buttons_screen_locs["[name]_[button.id]"] : FALSE //even if it's not defaultly locked we should remember we locked it before + button.locked = M.client.prefs.read_preference(/datum/preference/toggle/buttons_locked) || button.id ? M.client.prefs.action_buttons_screen_locs["[name]_[button.id]"] : FALSE //even if it's not defaultly locked we should remember we locked it before button.moved = button.id ? M.client.prefs.action_buttons_screen_locs["[name]_[button.id]"] : FALSE for(var/mob/dead/observer/O in M.observers) O?.client.screen += button diff --git a/code/datums/brain_damage/imaginary_friend.dm b/code/datums/brain_damage/imaginary_friend.dm index ca44ab1a4512..7b12b3e0cf1b 100644 --- a/code/datums/brain_damage/imaginary_friend.dm +++ b/code/datums/brain_damage/imaginary_friend.dm @@ -147,7 +147,7 @@ friend_talk(message) /mob/camera/imaginary_friend/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods = list()) - if (client?.prefs.chat_on_map && (client.prefs.see_chat_non_mob || ismob(speaker))) + if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && (client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker))) create_chat_message(speaker, message_language, raw_message, spans) to_chat(src, compose_message(speaker, message_language, raw_message, radio_freq, spans, message_mods)) diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm index 30c4fd461542..8dcf321a2990 100644 --- a/code/datums/chatmessage.dm +++ b/code/datums/chatmessage.dm @@ -109,7 +109,7 @@ text = replacetext(text, span_check, "") // Clip message - var/maxlen = owned_by.prefs.max_chat_length + var/maxlen = owned_by.prefs.read_preference(/datum/preference/numeric/max_chat_length) if (length_char(text) > maxlen) text = copytext_char(text, 1, maxlen + 1) + "..." // BYOND index moment diff --git a/code/datums/holocall.dm b/code/datums/holocall.dm index 551be3b68966..276e24d6d662 100644 --- a/code/datums/holocall.dm +++ b/code/datums/holocall.dm @@ -316,7 +316,6 @@ if(outfit_type) mannequin.equipOutfit(outfit_type,TRUE) mannequin.setDir(SOUTH) - COMPILE_OVERLAYS(mannequin) . = image(mannequin) unset_busy_human_dummy("HOLODISK_PRESET") diff --git a/code/datums/mind.dm b/code/datums/mind.dm index eef9bd2537d7..0b9d19a3b5d6 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -320,28 +320,28 @@ var/obj/item/uplink_loc var/implant = FALSE - if(traitor_mob.client && traitor_mob.client.prefs) - switch(traitor_mob.client.prefs.uplink_spawn_loc) - if(UPLINK_PDA) - uplink_loc = PDA - if(!uplink_loc) - uplink_loc = R - if(!uplink_loc) - uplink_loc = P - if(UPLINK_RADIO) + var/uplink_spawn_location = traitor_mob.client?.prefs?.read_preference(/datum/preference/choiced/uplink_location) + switch (uplink_spawn_location) + if(UPLINK_PDA) + uplink_loc = PDA + if(!uplink_loc) uplink_loc = R - if(!uplink_loc) - uplink_loc = PDA - if(!uplink_loc) - uplink_loc = P - if(UPLINK_PEN) + if(!uplink_loc) + uplink_loc = P + if(UPLINK_RADIO) + uplink_loc = R + if(!uplink_loc) + uplink_loc = PDA + if(!uplink_loc) uplink_loc = P - if(!uplink_loc) - uplink_loc = PDA - if(!uplink_loc) - uplink_loc = R - if(UPLINK_IMPLANT) - implant = TRUE + if(UPLINK_PEN) + uplink_loc = P + if(!uplink_loc) + uplink_loc = PDA + if(!uplink_loc) + uplink_loc = R + if(UPLINK_IMPLANT) + implant = TRUE if(!uplink_loc) // We've looked everywhere, let's just implant you implant = TRUE diff --git a/code/datums/mutations/olfaction.dm b/code/datums/mutations/olfaction.dm index c690b394ece8..2e3721c02365 100644 --- a/code/datums/mutations/olfaction.dm +++ b/code/datums/mutations/olfaction.dm @@ -80,10 +80,13 @@ /obj/effect/temp_visual/scent_trail/Destroy() UnregisterSignal(src, COMSIG_MOVABLE_CROSSED) animate(img, alpha = 0, time = 1 SECONDS, easing = EASE_OUT) //fade out + INVOKE_ASYNC(src, .proc/Fade) + return ..() + +/obj/effect/temp_visual/scent_trail/proc/Fade() sleep(1 SECONDS) sniffer.remove_alt_appearance("smelly") img = null - return ..() /datum/effect_system/trail_follow/scent effect_type = /obj/effect/temp_visual/scent_trail diff --git a/code/datums/traits/_quirk.dm b/code/datums/traits/_quirk.dm index 73312f8c58ee..f68fd9b695be 100644 --- a/code/datums/traits/_quirk.dm +++ b/code/datums/traits/_quirk.dm @@ -13,6 +13,9 @@ var/mob/living/quirk_holder var/not_init = FALSE // Yogs -- Allows quirks to be instantiated without all the song & dance below happening var/list/species_blacklist = list() + /// The icon to show in the preferences menu. + /// This references a tgui icon, so it can be FontAwesome or a tgfont (with a tg- prefix). + var/icon /datum/quirk/New(mob/living/quirk_mob, spawn_effects, no_init = FALSE) ..() diff --git a/code/datums/traits/good.dm b/code/datums/traits/good.dm index 531ee56bc670..4a40222e2511 100644 --- a/code/datums/traits/good.dm +++ b/code/datums/traits/good.dm @@ -4,6 +4,7 @@ /datum/quirk/no_taste name = "Ageusia" desc = "You can't taste anything! Toxic food will still poison you." + icon = "meh-blank" value = 2 mob_trait = TRAIT_AGEUSIA gain_text = span_notice("You can't taste anything!") @@ -11,13 +12,20 @@ medical_record_text = "Patient suffers from ageusia and is incapable of tasting food or reagents." /datum/quirk/no_taste/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant drink + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant drink + qdel(species) + + if(disallowed_trait) return "You don't have the ability to eat!" return FALSE /datum/quirk/alcohol_tolerance name = "Alcohol Tolerance" desc = "You become drunk more slowly and suffer fewer drawbacks from alcohol." + icon = "beer" value = 2 mob_trait = TRAIT_ALCOHOL_TOLERANCE gain_text = span_notice("You feel like you could drink a whole keg!") @@ -25,13 +33,20 @@ medical_record_text = "Patient demonstrates a high tolerance for alcohol." /datum/quirk/alcohol_tolerance/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant drink + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant drink + qdel(species) + + if(disallowed_trait) return "You don't have the ability to drink!" return FALSE /datum/quirk/apathetic name = "Apathetic" desc = "You just don't care as much as other people. That's nice to have in a place like this, I guess." + icon = "meh" value = 2 mood_quirk = TRUE medical_record_text = "Patient was administered the Apathy Evaluation Scale but did not bother to complete it." @@ -50,6 +65,7 @@ /datum/quirk/drunkhealing name = "Drunken Resilience" desc = "Nothing like a good drink to make you feel on top of the world. Whenever you're drunk, you slowly recover from injuries." + icon = "wine-bottle" value = 4 mob_trait = TRAIT_DRUNK_HEALING gain_text = span_notice("You feel like a drink would do you good.") @@ -57,13 +73,20 @@ medical_record_text = "Patient has unusually efficient liver metabolism and can slowly regenerate wounds by drinking alcoholic beverages." /datum/quirk/drunkhealing/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant drink + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant drink + qdel(species) + + if(disallowed_trait) // Cant drink return "You don't have the ability to drink!" return FALSE /datum/quirk/empath name = "Empath" desc = "Whether it's a sixth sense or careful study of body language, it only takes you a quick glance at someone to understand how they feel." + icon = "smile-beam" value = 1 mob_trait = TRAIT_EMPATH gain_text = span_notice("You feel in tune with those around you.") @@ -73,6 +96,7 @@ /datum/quirk/freerunning name = "Freerunning" desc = "You're great at quick moves! You can climb tables more quickly." + icon = "running" value = 4 mob_trait = TRAIT_FREERUNNING gain_text = span_notice("You feel lithe on your feet!") @@ -82,6 +106,7 @@ /datum/quirk/friendly name = "Friendly" desc = "You give the best hugs, especially when you're in the right mood." + icon = "hands-helping" value = 1 mob_trait = TRAIT_FRIENDLY gain_text = span_notice("You want to hug someone.") @@ -92,6 +117,7 @@ /datum/quirk/jolly name = "Jolly" desc = "You sometimes just feel happy, for no reason at all." + icon = "grin" value = 2 mob_trait = TRAIT_JOLLY mood_quirk = TRUE @@ -100,6 +126,7 @@ /datum/quirk/light_step name = "Light Step" desc = "You walk with a gentle step; stepping on sharp objects is quieter, less painful and you won't leave footprints behind you." + icon = "shoe-prints" value = 2 mob_trait = TRAIT_LIGHT_STEP gain_text = span_notice("You walk with a little more litheness.") @@ -109,6 +136,7 @@ /datum/quirk/musician name = "Musician" desc = "You can tune handheld musical instruments to play melodies that clear certain negative effects and soothe the soul." + icon = "guitar" value = 1 mob_trait = TRAIT_MUSICIAN gain_text = span_notice("You know everything about musical instruments.") @@ -127,6 +155,7 @@ /datum/quirk/night_vision name = "Night Vision" desc = "You can see slightly more clearly in full darkness than most people." + icon = "eye" value = 2 mob_trait = TRAIT_NIGHT_VISION gain_text = span_notice("The shadows seem a little less dark.") @@ -143,6 +172,7 @@ /datum/quirk/photographer name = "Photographer" desc = "You know how to handle a camera, shortening the delay between each shot." + icon = "camera" value = 1 mob_trait = TRAIT_PHOTOGRAPHER gain_text = span_notice("You know everything about photography.") @@ -159,6 +189,7 @@ /datum/quirk/selfaware name = "Self-Aware" desc = "You know your body well, and can accurately assess the extent of your wounds." + icon = "bone" value = 4 mob_trait = TRAIT_SELF_AWARE medical_record_text = "Patient demonstrates an uncanny knack for self-diagnosis." @@ -166,6 +197,7 @@ /datum/quirk/skittish name = "Skittish" desc = "You can conceal yourself in danger. Ctrl-shift-click a closed locker to jump into it, as long as you have access." + icon = "trash" value = 4 mob_trait = TRAIT_SKITTISH medical_record_text = "Patient demonstrates a high aversion to danger and has described hiding in containers out of fear." @@ -173,6 +205,7 @@ /datum/quirk/spiritual name = "Spiritual" desc = "You hold a spiritual belief, whether in God, nature or the arcane rules of the universe. You gain comfort from the presence of holy people, and believe that your prayers are more special than others." + icon = "bible" value = 1 mob_trait = TRAIT_SPIRITUAL gain_text = span_notice("You have faith in a higher power.") @@ -187,6 +220,7 @@ /datum/quirk/toxic_tastes name = "Toxic Tastes" desc = "You have a taste for normally dangerous foods." + icon = "face-grin-tongue" value = 2 gain_text = span_notice("Your stomach feels robust.") lose_text = span_notice("Your stomach feels normal again.") @@ -207,13 +241,20 @@ species.liked_food = initial(species.liked_food) /datum/quirk/toxic_tastes/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant eat + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant drink + qdel(species) + + if(disallowed_trait) // Cant eat return "You don't have the ability to eat!" return FALSE /datum/quirk/tagger name = "Tagger" desc = "You're an experienced artist. While drawing graffiti, you can get twice as many uses out of drawing supplies." + icon = "spray-can" value = 1 mob_trait = TRAIT_TAGGER gain_text = span_notice("You know how to tag walls efficiently.") @@ -230,6 +271,7 @@ /datum/quirk/voracious name = "Voracious" desc = "Nothing gets between you and your food. You eat faster and can binge on junk food! Being fat suits you just fine." + icon = "drumstick-bite" value = 1 mob_trait = TRAIT_VORACIOUS gain_text = span_notice("You feel HONGRY.") @@ -237,13 +279,20 @@ medical_record_text = "Patient demonstrates a disturbing capacity for eating." /datum/quirk/voracious/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant eat + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant drink + qdel(species) + + if(disallowed_trait) // Cant eat return "You don't have the ability to eat!" return FALSE /datum/quirk/efficient_metabolism //about 25% slower hunger name = "Efficient Metabolism" desc = "Your metabolism is unusually efficient, allowing you to better process your food and go longer periods without eating." + icon = "utensils" value = 1 mob_trait = TRAIT_EAT_LESS gain_text = span_notice("You don't feel very hungry.") @@ -253,6 +302,7 @@ /datum/quirk/crafty //about 25% faster crafting name = "Crafty" desc = "You're very good at making stuff, and can craft faster than others." + icon = "wrench" value = 2 mob_trait = TRAIT_CRAFTY gain_text = span_notice("You feel like crafting some stuff.") @@ -262,6 +312,7 @@ /datum/quirk/cyberorgan //random upgraded cybernetic organ name = "Cybernetic Organ" desc = "Due to a past incident you lost function of one of your organs, but now have a fancy upgraded cybernetic organ!" + icon = "building-ngo" value = 6 var/slot_string = "organ" medical_record_text = "During physical examination, patient was found to have an upgraded cybernetic organ." @@ -289,18 +340,27 @@ to_chat(quirk_holder, "Your [slot_string] has been replaced with an upgraded cybernetic variant.") /datum/quirk/cyberorgan/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && istype(prefs.pref_species, /datum/species/ipc)) // IPCs are already cybernetic + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + + if(species_type == /datum/species/ipc) // IPCs are already cybernetic return "You already have cybernetic organs!" return FALSE /datum/quirk/telomeres_long name = "Long Telomeres" desc = "You haven't been cloned much, if at all. Your DNA's telomeres are still largely unaffected by repeated cloning, enabling cloners to work faster." + icon = "magnifying-glass-plus" value = 2 mob_trait = TRAIT_LONG_TELOMERES medical_record_text = "DNA analysis indicates that the patient's DNA telomeres are still naturally long." /datum/quirk/telomeres_long/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NO_DNA_COPY in prefs.pref_species.species_traits)) //Can't pick if you have no DNA bruv. + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NO_DNA_COPY in species.species_traits) //Can't pick if you have no DNA bruv. + qdel(species) + + if(disallowed_trait) return "You have no DNA!" return FALSE diff --git a/code/datums/traits/negative.dm b/code/datums/traits/negative.dm index a5ac902f755c..f25a64636163 100644 --- a/code/datums/traits/negative.dm +++ b/code/datums/traits/negative.dm @@ -3,6 +3,7 @@ /datum/quirk/badback name = "Bad Back" desc = "Thanks to your poor posture, backpacks and other bags never sit right on your back. More evenly weighted objects are fine, though." + icon = "hiking" value = -4 mood_quirk = TRUE gain_text = span_danger("Your back REALLY hurts!") @@ -19,22 +20,35 @@ /datum/quirk/blooddeficiency name = "Blood Deficiency" desc = "Your body can't produce enough blood to sustain itself." + icon = "tint" value = -4 gain_text = span_danger("You feel your vigor slowly fading away.") lose_text = span_notice("You feel vigorous again.") medical_record_text = "Patient requires regular treatment for blood loss due to low production of blood." -/datum/quirk/blooddeficiency/on_process(delta_time) +/datum/quirk/blooddeficiency/check_quirk(datum/preferences/prefs) + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOBLOOD in species.species_traits) //can't lose blood if your species doesn't have any + qdel(species) + + if(disallowed_trait) + return "You don't have blood!" + return ..() + +/datum/quirk/blooddeficiency/on_process() var/mob/living/carbon/human/H = quirk_holder if(NOBLOOD in H.dna.species.species_traits) //can't lose blood if your species doesn't have any return else if (H.blood_volume > (BLOOD_VOLUME_SAFE(H) - 25)) // just barely survivable without treatment - H.blood_volume -= 0.275 * delta_time + H.blood_volume -= 0.275 /datum/quirk/blindness name = "Blind" desc = "You are completely blind, nothing can counteract this." + icon = "eye-slash" value = -9 gain_text = span_danger("You can't see anything.") lose_text = span_notice("You miraculously gain back your vision.") @@ -53,14 +67,15 @@ /datum/quirk/brainproblems name = "Brain Tumor" desc = "You have a little friend in your brain that is slowly destroying it. Better bring some mannitol!" + icon = "head-side-virus" value = -6 gain_text = span_danger("You feel smooth.") lose_text = span_notice("You feel wrinkled again.") medical_record_text = "Patient has a tumor in their brain that is slowly driving them to brain death." var/where = "at your feet" -/datum/quirk/brainproblems/on_process(delta_time) - quirk_holder.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.2 * delta_time) +/datum/quirk/brainproblems/on_process() + quirk_holder.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.2) /datum/quirk/brainproblems/on_spawn() var/mob/living/carbon/human/H = quirk_holder @@ -78,6 +93,7 @@ /datum/quirk/deafness name = "Deaf" desc = "You are incurably deaf." + icon = "deaf" value = -6 mob_trait = TRAIT_DEAF gain_text = span_danger("You can't hear anything.") @@ -87,6 +103,7 @@ /datum/quirk/depression name = "Depression" desc = "You sometimes just hate life." + icon = "frown" mob_trait = TRAIT_DEPRESSION value = -2 gain_text = span_danger("You start feeling depressed.") @@ -94,133 +111,10 @@ medical_record_text = "Patient has a mild mood disorder, causing them to experience episodes of depression." mood_quirk = TRUE -/datum/quirk/family_heirloom - name = "Family Heirloom" - desc = "You are the current owner of an heirloom, passed down for generations. You have to keep it safe!" - value = -2 - mood_quirk = TRUE - var/obj/item/heirloom - var/where - medical_record_text = "Patient demonstrates an unnatural attachment to a family heirloom." - -/datum/quirk/family_heirloom/on_spawn() - var/mob/living/carbon/human/H = quirk_holder - var/obj/item/heirloom_type - - if(is_species(H, /datum/species/moth) && prob(50)) - heirloom_type = /obj/item/flashlight/lantern/heirloom_moth - else if(iscatperson(H) && prob(50)) - heirloom_type = /obj/item/toy/cattoy - else - switch(quirk_holder.mind.assigned_role) - //Service jobs - if("Clown") - heirloom_type = /obj/item/bikehorn/golden - if("Mime") - heirloom_type = /obj/item/reagent_containers/food/snacks/baguette - if("Janitor") - heirloom_type = pick(/obj/item/mop, /obj/item/clothing/suit/caution, /obj/item/reagent_containers/glass/bucket/wooden) - if("Cook") - heirloom_type = pick(/obj/item/reagent_containers/food/condiment/saltshaker, /obj/item/kitchen/rollingpin, /obj/item/clothing/head/chefhat) - if("Clerk") - heirloom_type = pick(/obj/item/coin, /obj/item/coin/gold, /obj/item/coin/iron, /obj/item/coin/silver) - if("Botanist") - if(is_species(H, /datum/species/plasmaman)) - heirloom_type = pick(/obj/item/cultivator, /obj/item/shovel/spade, /obj/item/reagent_containers/glass/bucket/wooden, /obj/item/toy/plush/beeplushie) - else - heirloom_type = pick(/obj/item/cultivator, /obj/item/shovel/spade, /obj/item/reagent_containers/glass/bucket/wooden, /obj/item/toy/plush/beeplushie, /obj/item/clothing/mask/cigarette/pipe, /obj/item/clothing/mask/cigarette/pipe/cobpipe) - if("Bartender") - heirloom_type = pick(/obj/item/reagent_containers/glass/rag, /obj/item/clothing/head/that, /obj/item/reagent_containers/food/drinks/shaker) - if("Curator") - heirloom_type = pick(/obj/item/pen/fountain, /obj/item/storage/pill_bottle/dice) - if("Assistant") - heirloom_type = /obj/item/storage/toolbox/mechanical/old/heirloom - //Security/Command - if("Captain") - heirloom_type = /obj/item/reagent_containers/food/drinks/flask/gold - if("Head of Security") - heirloom_type = /obj/item/book/manual/wiki/security_space_law - if("Warden") - heirloom_type = /obj/item/book/manual/wiki/security_space_law - if("Security Officer") - heirloom_type = pick(/obj/item/book/manual/wiki/security_space_law, /obj/item/clothing/head/beret/sec) - if("Detective") - heirloom_type = pick(/obj/item/reagent_containers/food/drinks/bottle/whiskey, /obj/item/taperecorder/empty) - if("Lawyer") - heirloom_type = pick(/obj/item/gavelhammer, /obj/item/book/manual/wiki/security_space_law) - //RnD - if("Research Director") - heirloom_type = /obj/item/toy/plush/slimeplushie - if("Scientist") - heirloom_type = /obj/item/toy/plush/slimeplushie - if("Roboticist") - heirloom_type = pick(subtypesof(/obj/item/toy/prize)) //look at this nerd - //Medical - if("Chief Medical Officer") - heirloom_type = pick(/obj/item/clothing/neck/stethoscope, /obj/item/bodybag) - if("Medical Doctor") - heirloom_type = pick(/obj/item/clothing/neck/stethoscope, /obj/item/bodybag) - if("Chemist") - heirloom_type = pick(/obj/item/book/manual/wiki/chemistry, /obj/item/clothing/mask/vape) - if("Virologist") - heirloom_type = /obj/item/reagent_containers/syringe - //Engineering - if("Chief Engineer") - heirloom_type = pick(/obj/item/clothing/head/hardhat/white, /obj/item/screwdriver, /obj/item/wrench, /obj/item/weldingtool, /obj/item/crowbar, /obj/item/wirecutters) - if("Station Engineer") - heirloom_type = pick(/obj/item/clothing/head/hardhat, /obj/item/screwdriver, /obj/item/wrench, /obj/item/weldingtool, /obj/item/crowbar, /obj/item/wirecutters) - if("Atmospheric Technician") - heirloom_type = pick(/obj/item/lighter, /obj/item/lighter/greyscale, /obj/item/storage/box/matches) - //Supply - if("Quartermaster") - heirloom_type = pick(/obj/item/stamp, /obj/item/stamp/denied) - if("Cargo Technician") - heirloom_type = /obj/item/clipboard - if("Shaft Miner") - heirloom_type = pick(/obj/item/pickaxe/mini, /obj/item/shovel) - - if(!heirloom_type) - heirloom_type = pick( - /obj/item/toy/cards/deck, - /obj/item/lighter, - /obj/item/dice/d20) - heirloom = new heirloom_type(get_turf(quirk_holder)) - var/list/slots = list( - "in your left pocket" = SLOT_L_STORE, - "in your right pocket" = SLOT_R_STORE, - "in your backpack" = SLOT_IN_BACKPACK - ) - where = H.equip_in_one_of_slots(heirloom, slots, FALSE) || "at your feet" - -/datum/quirk/family_heirloom/post_add() - if(where == "in your backpack") - var/mob/living/carbon/human/H = quirk_holder - SEND_SIGNAL(H.back, COMSIG_TRY_STORAGE_SHOW, H) - - to_chat(quirk_holder, span_boldnotice("There is a precious family [heirloom.name] [where], passed down from generation to generation. Keep it safe!")) - - var/list/names = splittext(quirk_holder.real_name, " ") - var/family_name = names[names.len] - - heirloom.AddComponent(/datum/component/heirloom, quirk_holder.mind, family_name) - -/datum/quirk/family_heirloom/on_process() - if(heirloom in quirk_holder.GetAllContents()) - SEND_SIGNAL(quirk_holder, COMSIG_CLEAR_MOOD_EVENT, "family_heirloom_missing") - SEND_SIGNAL(quirk_holder, COMSIG_ADD_MOOD_EVENT, "family_heirloom", /datum/mood_event/family_heirloom) - else - SEND_SIGNAL(quirk_holder, COMSIG_CLEAR_MOOD_EVENT, "family_heirloom") - SEND_SIGNAL(quirk_holder, COMSIG_ADD_MOOD_EVENT, "family_heirloom_missing", /datum/mood_event/family_heirloom_missing) - -/datum/quirk/family_heirloom/clone_data() - return heirloom - -/datum/quirk/family_heirloom/on_clone(data) - heirloom = data - /datum/quirk/heavy_sleeper name = "Heavy Sleeper" desc = "You sleep like a rock! Whenever you're put to sleep or knocked unconscious, you take a little bit longer to wake up and cant see anything." + icon = "bed" value = -4 mob_trait = TRAIT_HEAVY_SLEEPER gain_text = span_danger("You feel sleepy.") @@ -230,6 +124,7 @@ /datum/quirk/hypersensitive name = "Hypersensitive" desc = "For better or worse, everything seems to affect your mood more than it should." + icon = "flushed" value = -2 gain_text = span_danger("You seem to make a big deal out of everything.") lose_text = span_notice("You don't seem to make a big deal out of everything anymore.") @@ -250,6 +145,7 @@ /datum/quirk/light_drinker name = "Light Drinker" desc = "You just can't handle your drinks and get drunk very quickly." + icon = "cocktail" value = -2 mob_trait = TRAIT_LIGHT_DRINKER gain_text = span_notice("Just the thought of drinking alcohol makes your head spin.") @@ -257,13 +153,20 @@ medical_record_text = "Patient demonstrates a low tolerance for alcohol. (Wimp)" /datum/quirk/light_drinker/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant drink + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant drink + qdel(species) + + if(disallowed_trait) return "You don't have the ability to drink!" return FALSE /datum/quirk/nearsighted //t. errorage name = "Nearsighted" desc = "You are nearsighted without prescription glasses, but spawn with a pair." + icon = "glasses" value = -2 gain_text = span_danger("Things far away from you start looking blurry.") lose_text = span_notice("You start seeing faraway things normally again.") @@ -282,6 +185,7 @@ /datum/quirk/nyctophobia name = "Nyctophobia" desc = "As far as you can remember, you've always been afraid of the dark. While in the dark without a light source, you instinctually act careful, and constantly feel a sense of dread." + icon = "lightbulb" value = -2 medical_record_text = "Patient demonstrates a fear of the dark. (Seriously?)" @@ -304,6 +208,7 @@ /datum/quirk/nonviolent name = "Pacifist" desc = "The thought of violence makes you sick. So much so, in fact, that you can't hurt anyone." + icon = "peace" value = -4 mob_trait = TRAIT_PACIFISM gain_text = span_danger("You feel repulsed by the thought of violence!") @@ -314,6 +219,7 @@ /datum/quirk/paraplegic name = "Paraplegic" desc = "Your legs do not function. Nothing will ever fix this. But hey, free wheelchair!" + icon = "wheelchair" value = -7 human_only = TRUE gain_text = null // Handled by trauma. @@ -349,6 +255,7 @@ /datum/quirk/poor_aim name = "Poor Aim" desc = "You're terrible with guns and can't line up a straight shot to save your life. Dual-wielding is right out." + icon = "bullseye" value = -2 mob_trait = TRAIT_POOR_AIM medical_record_text = "Patient possesses a strong tremor in both hands." @@ -364,6 +271,7 @@ /datum/quirk/prosopagnosia name = "Prosopagnosia" desc = "You have a mental disorder that prevents you from being able to recognize faces at all." + icon = "user-secret" value = -2 mob_trait = TRAIT_PROSOPAGNOSIA medical_record_text = "Patient suffers from prosopagnosia and cannot recognize faces." @@ -371,6 +279,7 @@ /datum/quirk/prosthetic_limb name = "Prosthetic Limb" desc = "An accident caused you to lose one of your limbs. Because of this, you now have a random prosthetic!" + icon = "tg-prosthetic-leg" value = -2 var/slot_string = "limb" var/specific = null @@ -433,7 +342,8 @@ /datum/quirk/insanity name = "Reality Dissociation Syndrome" - desc = "You suffer from a severe disorder that causes very vivid hallucinations. Mindbreaker toxin can suppress its effects, and you are immune to mindbreaker's hallucinogenic properties. This is not a license to grief." + desc = "You suffer from a severe disorder that causes very vivid hallucinations. Mindbreaker toxin can suppress its effects, and you are immune to mindbreaker's hallucinogenic properties. THIS IS NOT A LICENSE TO GRIEF." + icon = "grin-tongue-wink" value = -2 //no mob trait because it's handled uniquely gain_text = null //handled by trauma @@ -466,6 +376,7 @@ /datum/quirk/social_anxiety name = "Social Anxiety" desc = "Talking to people is very difficult for you, and you often stutter or even lock up." + icon = "comment-slash" value = -2 gain_text = span_danger("You start worrying about what you're saying.") lose_text = span_notice("You feel easier about talking again.") //if only it were that easy! @@ -574,6 +485,7 @@ /datum/quirk/junkie name = "Junkie" desc = "You can't get enough of hard drugs." + icon = "pills" value = -4 gain_text = span_danger("You suddenly feel the craving for drugs.") lose_text = span_notice("You feel like you should kick your drug habit.") @@ -655,13 +567,20 @@ H.reagents.addiction_list.Add(reagent_instance) /datum/quirk/junkie/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (prefs.pref_species.reagent_tag == PROCESS_SYNTHETIC)) //can't lose blood if your species doesn't have any + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = species.reagent_tag == PROCESS_SYNTHETIC //can't lose blood if your species doesn't have any + qdel(species) + + if(disallowed_trait) return "You don't process normal chemicals!" return FALSE /datum/quirk/junkie/smoker name = "Smoker" desc = "Sometimes you just really want a smoke. Probably not great for your lungs." + icon = "smoking" value = -2 mood_quirk = TRUE gain_text = span_danger("You could really go for a smoke right about now.") @@ -699,6 +618,7 @@ /datum/quirk/unstable name = "Unstable" desc = "Due to past troubles, you are unable to recover your sanity if you lose it. Be very careful managing your mood!" + icon = "angry" value = -4 mood_quirk = TRUE mob_trait = TRAIT_UNSTABLE @@ -709,6 +629,7 @@ /datum/quirk/sheltered name = "Sheltered" desc = "You never learned to speak galactic common." + icon = "comment-dots" value = -2 mob_trait = TRAIT_SHELTERED gain_text = span_danger("You do not speak galactic common.") @@ -730,6 +651,7 @@ /datum/quirk/allergic name = "Allergic Reaction" desc = "You have had an allergic reaction to medicine in the past. Better stay away from it!" + icon = "prescription-bottle" value = -2 mob_trait = TRAIT_ALLERGIC gain_text = span_danger("You remember your allergic reaction to a common medicine.") @@ -750,8 +672,14 @@ var/cooldown = FALSE /datum/quirk/allergic/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (TRAIT_MEDICALIGNORE in prefs.pref_species.inherent_traits)) - return "You don't benefit from the use of medicine as a [prefs.pref_species]." + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (TRAIT_MEDICALIGNORE in species.inherent_traits) + qdel(species) + + if(disallowed_trait) + return "You don't benefit from the use of medicine." return ..() /datum/quirk/allergic/on_spawn() @@ -770,13 +698,20 @@ addtimer(VARSET_CALLBACK(src, cooldown, FALSE), cooldown_time) /datum/quirk/allergic/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (prefs.pref_species.reagent_tag == PROCESS_SYNTHETIC)) //can't lose blood if your species doesn't have any + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = species.reagent_tag == PROCESS_SYNTHETIC //can't lose blood if your species doesn't have any + qdel(species) + + if(disallowed_trait) return "You don't process normal chemicals!" return FALSE /datum/quirk/kleptomaniac name = "Kleptomaniac" desc = "You have an uncontrollable urge to pick up things you see. Even things that don't belong to you." + icon = "hands-holding-circle" value = -2 mob_trait = TRAIT_KLEPTOMANIAC gain_text = span_danger("You have an unmistakeable urge to grab nearby objects.") @@ -800,6 +735,7 @@ /datum/quirk/ineloquent name = "Ineloquent" desc = "Thinking big words makes brain go hurt." + icon = "comments" value = -2 human_only = TRUE gain_text = "You feel your vocabulary slipping away." @@ -814,6 +750,7 @@ /datum/quirk/hemophilia //basically permanent heparin name = "Hemophiliac" desc = "You can't naturally clot bleeding wounds and bleed much more from them than most people, making even small cuts possibly life threatening." + icon = "droplet" value = -6 mob_trait = TRAIT_BLOODY_MESS gain_text = span_danger("You feel like your blood is thin.") @@ -821,13 +758,21 @@ medical_record_text = "Patient appears unable to naturally form blood clots." /datum/quirk/hemophilia/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (!(HAS_FLESH in prefs.pref_species.species_traits) || (NOBLOOD in prefs.pref_species.species_traits))) - return "You can't bleed as a [prefs.pref_species]." + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/has_flesh = (HAS_FLESH in species.species_traits) + var/no_blood = (NOBLOOD in species.species_traits) + qdel(species) + + if(has_flesh || no_blood) + return "You can't bleed." return ..() /datum/quirk/brain_damage name = "Brain Damage" desc = "The shuttle ride was a bit bumpy to the station." + icon = "brain" value = -7 gain_text = span_danger("Your head hurts.") lose_text = span_notice("Your head feels good again.") @@ -845,6 +790,7 @@ /datum/quirk/monochromatic name = "Monochromacy" desc = "You suffer from full colorblindness, and perceive nearly the entire world in blacks and whites." + icon = "palette" value = -2 medical_record_text = "Patient is afflicted with almost complete color blindness." @@ -863,18 +809,25 @@ /datum/quirk/nomail name = "Loser" desc = "You are a complete nobody, no one would ever send you anything worthwhile in the mail." + icon = "envelopes-bulk" value = -1 mob_trait = TRAIT_BADMAIL /datum/quirk/telomeres_short name = "Short Telomeres" desc = "Due to hundreds of cloning cycles, your DNA's telomeres are dangerously shortened. Your DNA can't support cloning without expensive DNA restructuring, and what's worse- you work for Nanotrasen." + icon = "magnifying-glass-minus" value = -2 mob_trait = TRAIT_SHORT_TELOMERES medical_record_text = "DNA analysis indicates that the patient's DNA telomeres are artificially shortened from previous cloner usage." /datum/quirk/telomeres_short/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NO_DNA_COPY in prefs.pref_species.species_traits)) //Can't pick if you have no DNA bruv. + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NO_DNA_COPY in species.species_traits) //Can't pick if you have no DNA bruv. + qdel(species) + + if(disallowed_trait) return "You have no DNA!" return FALSE - \ No newline at end of file diff --git a/code/datums/traits/neutral.dm b/code/datums/traits/neutral.dm index aa64d7baf7bf..d52ff1b5b4b6 100644 --- a/code/datums/traits/neutral.dm +++ b/code/datums/traits/neutral.dm @@ -4,6 +4,7 @@ /datum/quirk/vegetarian name = "Vegetarian" desc = "You find the idea of eating meat morally and physically repulsive." + icon = "carrot" value = 0 gain_text = span_notice("You feel repulsion at the idea of eating meat.") lose_text = span_notice("You feel like eating meat isn't that bad.") @@ -25,13 +26,20 @@ species.disliked_food &= ~MEAT /datum/quirk/vegetarian/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant eat + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant eat + qdel(species) + + if(disallowed_trait) return "You don't have the ability to eat!" return FALSE /datum/quirk/pineapple_liker name = "Ananas Affinity" desc = "You find yourself greatly enjoying fruits of the ananas genus. You can't seem to ever get enough of their sweet goodness!" + icon = "thumbs-up" value = 0 gain_text = span_notice("You feel an intense craving for pineapple.") lose_text = span_notice("Your feelings towards pineapples seem to return to a lukewarm state.") @@ -49,13 +57,20 @@ species.liked_food &= ~PINEAPPLE /datum/quirk/pineapple_liker/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant eat + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant eat + qdel(species) + + if(disallowed_trait) return "You don't have the ability to eat!" return FALSE /datum/quirk/pineapple_hater name = "Ananas Aversion" desc = "You find yourself greatly detesting fruits of the ananas genus. Serious, how the hell can anyone say these things are good? And what kind of madman would even dare putting it on a pizza!?" + icon = "thumbs-down" value = 0 gain_text = span_notice("You find yourself pondering what kind of idiot actually enjoys pineapples...") lose_text = span_notice("Your feelings towards pineapples seem to return to a lukewarm state.") @@ -73,13 +88,20 @@ species.disliked_food &= ~PINEAPPLE /datum/quirk/pineapple_hater/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant eat + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant eat + qdel(species) + + if(disallowed_trait) return "You don't have the ability to eat!" return FALSE /datum/quirk/deviant_tastes name = "Deviant Tastes" desc = "You dislike food that most people enjoy, and find delicious what they don't." + icon = "grin-tongue-squint" value = 0 gain_text = span_notice("You start craving something that tastes strange.") lose_text = span_notice("You feel like eating normal food again.") @@ -100,13 +122,20 @@ species.disliked_food = initial(species.disliked_food) /datum/quirk/deviant_tastes/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && (NOMOUTH in prefs.pref_species.species_traits)) // Cant eat + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (NOMOUTH in species.species_traits) // Cant eat + qdel(species) + + if(disallowed_trait) return "You don't have the ability to eat!" return FALSE /datum/quirk/shifty_eyes name = "Shifty Eyes" desc = "Your eyes tend to wander all over the place, whether you mean to or not, causing people to sometimes think you're looking directly at them when you aren't." + icon = "face-dizzy" value = 0 medical_record_text = "Fucking creep kept staring at me the whole damn checkup. I'm only diagnosing this because it's less awkward than thinking it was on purpose." mob_trait = TRAIT_SHIFTY_EYES @@ -114,6 +143,7 @@ /datum/quirk/random_accent name = "Randomized Accent" desc = "You have developed a random accent." + icon = "comment-dollar" value = 0 mob_trait = TRAIT_RANDOM_ACCENT gain_text = span_danger("You have developed an accent.") @@ -124,11 +154,12 @@ var/mob/living/carbon/human/H = quirk_holder if(!H.mind.accent_name) H.mind.RegisterSignal(H, COMSIG_MOB_SAY, /datum/mind/.proc/handle_speech) - H.mind.accent_name = pick(assoc_list_strip_value(GLOB.accents_name2file))// Right now this pick just picks a straight random one from all implemented. + H.mind.accent_name = pick(assoc_to_keys(GLOB.accents_name2file))// Right now this pick just picks a straight random one from all implemented. /datum/quirk/colorist name = "Colorist" desc = "You like carrying around a hair dye spray to quickly apply color patterns to your hair." + icon = "spray-can-sparkles" value = 0 medical_record_text = "Patient enjoys dyeing their hair with pretty colors." var/where @@ -151,6 +182,138 @@ to_chat(quirk_holder, span_boldnotice("Your bottle of hair dye spray is [where].")) /datum/quirk/colorist/check_quirk(datum/preferences/prefs) - if(prefs.pref_species && !(HAIR in prefs.pref_species.species_traits)) // No Hair + var/species_type = prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/disallowed_trait = (HAIR in species.species_traits) // No Hair + qdel(species) + + if(!disallowed_trait) return "You don't have hair!" return FALSE + +/datum/quirk/family_heirloom + name = "Family Heirloom" + desc = "You are the current owner of an heirloom, passed down for generations. You have to keep it safe!" + icon = "toolbox" + value = 0 + mood_quirk = FALSE + var/obj/item/heirloom + var/where + medical_record_text = "Patient demonstrates an unnatural attachment to a family heirloom." + +/datum/quirk/family_heirloom/on_spawn() + var/mob/living/carbon/human/H = quirk_holder + var/obj/item/heirloom_type + + if(is_species(H, /datum/species/moth) && prob(50)) + heirloom_type = /obj/item/flashlight/lantern/heirloom_moth + else if(iscatperson(H) && prob(50)) + heirloom_type = /obj/item/toy/cattoy + else + switch(quirk_holder.mind.assigned_role) + //Service jobs + if("Clown") + heirloom_type = /obj/item/bikehorn/golden + if("Mime") + heirloom_type = /obj/item/reagent_containers/food/snacks/baguette + if("Janitor") + heirloom_type = pick(/obj/item/mop, /obj/item/clothing/suit/caution, /obj/item/reagent_containers/glass/bucket/wooden) + if("Cook") + heirloom_type = pick(/obj/item/reagent_containers/food/condiment/saltshaker, /obj/item/kitchen/rollingpin, /obj/item/clothing/head/chefhat) + if("Clerk") + heirloom_type = pick(/obj/item/coin, /obj/item/coin/gold, /obj/item/coin/iron, /obj/item/coin/silver) + if("Botanist") + if(is_species(H, /datum/species/plasmaman)) + heirloom_type = pick(/obj/item/cultivator, /obj/item/shovel/spade, /obj/item/reagent_containers/glass/bucket/wooden, /obj/item/toy/plush/beeplushie) + else + heirloom_type = pick(/obj/item/cultivator, /obj/item/shovel/spade, /obj/item/reagent_containers/glass/bucket/wooden, /obj/item/toy/plush/beeplushie, /obj/item/clothing/mask/cigarette/pipe, /obj/item/clothing/mask/cigarette/pipe/cobpipe) + if("Bartender") + heirloom_type = pick(/obj/item/reagent_containers/glass/rag, /obj/item/clothing/head/that, /obj/item/reagent_containers/food/drinks/shaker) + if("Curator") + heirloom_type = pick(/obj/item/pen/fountain, /obj/item/storage/pill_bottle/dice) + if("Assistant") + heirloom_type = /obj/item/storage/toolbox/mechanical/old/heirloom + //Security/Command + if("Captain") + heirloom_type = /obj/item/reagent_containers/food/drinks/flask/gold + if("Head of Security") + heirloom_type = /obj/item/book/manual/wiki/security_space_law + if("Warden") + heirloom_type = /obj/item/book/manual/wiki/security_space_law + if("Security Officer") + heirloom_type = pick(/obj/item/book/manual/wiki/security_space_law, /obj/item/clothing/head/beret/sec) + if("Detective") + heirloom_type = pick(/obj/item/reagent_containers/food/drinks/bottle/whiskey, /obj/item/taperecorder/empty) + if("Lawyer") + heirloom_type = pick(/obj/item/gavelhammer, /obj/item/book/manual/wiki/security_space_law) + //RnD + if("Research Director") + heirloom_type = /obj/item/toy/plush/slimeplushie + if("Scientist") + heirloom_type = /obj/item/toy/plush/slimeplushie + if("Roboticist") + heirloom_type = pick(subtypesof(/obj/item/toy/prize)) //look at this nerd + //Medical + if("Chief Medical Officer") + heirloom_type = pick(/obj/item/clothing/neck/stethoscope, /obj/item/bodybag) + if("Medical Doctor") + heirloom_type = pick(/obj/item/clothing/neck/stethoscope, /obj/item/bodybag) + if("Chemist") + heirloom_type = pick(/obj/item/book/manual/wiki/chemistry, /obj/item/clothing/mask/vape) + if("Virologist") + heirloom_type = /obj/item/reagent_containers/syringe + //Engineering + if("Chief Engineer") + heirloom_type = pick(/obj/item/clothing/head/hardhat/white, /obj/item/screwdriver, /obj/item/wrench, /obj/item/weldingtool, /obj/item/crowbar, /obj/item/wirecutters) + if("Station Engineer") + heirloom_type = pick(/obj/item/clothing/head/hardhat, /obj/item/screwdriver, /obj/item/wrench, /obj/item/weldingtool, /obj/item/crowbar, /obj/item/wirecutters) + if("Atmospheric Technician") + heirloom_type = pick(/obj/item/lighter, /obj/item/lighter/greyscale, /obj/item/storage/box/matches) + //Supply + if("Quartermaster") + heirloom_type = pick(/obj/item/stamp, /obj/item/stamp/denied) + if("Cargo Technician") + heirloom_type = /obj/item/clipboard + if("Shaft Miner") + heirloom_type = pick(/obj/item/pickaxe/mini, /obj/item/shovel) + + if(!heirloom_type) + heirloom_type = pick( + /obj/item/toy/cards/deck, + /obj/item/lighter, + /obj/item/dice/d20) + heirloom = new heirloom_type(get_turf(quirk_holder)) + var/list/slots = list( + "in your left pocket" = SLOT_L_STORE, + "in your right pocket" = SLOT_R_STORE, + "in your backpack" = SLOT_IN_BACKPACK + ) + where = H.equip_in_one_of_slots(heirloom, slots, FALSE) || "at your feet" + +/datum/quirk/family_heirloom/post_add() + if(where == "in your backpack") + var/mob/living/carbon/human/H = quirk_holder + SEND_SIGNAL(H.back, COMSIG_TRY_STORAGE_SHOW, H) + + to_chat(quirk_holder, span_boldnotice("There is a precious family [heirloom.name] [where], passed down from generation to generation. Keep it safe!")) + + var/list/names = splittext(quirk_holder.real_name, " ") + var/family_name = names[names.len] + + heirloom.AddComponent(/datum/component/heirloom, quirk_holder.mind, family_name) + +/datum/quirk/family_heirloom/on_process() + if(heirloom in quirk_holder.GetAllContents()) + SEND_SIGNAL(quirk_holder, COMSIG_CLEAR_MOOD_EVENT, "family_heirloom_missing") + SEND_SIGNAL(quirk_holder, COMSIG_ADD_MOOD_EVENT, "family_heirloom", /datum/mood_event/family_heirloom) + else + SEND_SIGNAL(quirk_holder, COMSIG_CLEAR_MOOD_EVENT, "family_heirloom") + SEND_SIGNAL(quirk_holder, COMSIG_ADD_MOOD_EVENT, "family_heirloom_missing", /datum/mood_event/family_heirloom_missing) + +/datum/quirk/family_heirloom/clone_data() + return heirloom + +/datum/quirk/family_heirloom/on_clone(data) + heirloom = data + diff --git a/code/datums/view.dm b/code/datums/view.dm index 6bbe684c86f2..6600536de3e2 100644 --- a/code/datums/view.dm +++ b/code/datums/view.dm @@ -26,10 +26,10 @@ winset(chief, "mapwindow.map", "zoom=0") /datum/viewData/proc/resetFormat()//Cuck - winset(chief, "mapwindow.map", "zoom=[chief.prefs.pixel_size]") + winset(chief, "mapwindow.map", "zoom=[chief.prefs.read_preference(/datum/preference/numeric/pixel_size)]") /datum/viewData/proc/setZoomMode() - winset(chief, "mapwindow.map", "zoom-mode=[chief.prefs.scaling_method]") + winset(chief, "mapwindow.map", "zoom-mode=[chief.prefs.read_preference(/datum/preference/choiced/scaling_method)]") /datum/viewData/proc/isZooming() return (width || height) @@ -119,4 +119,4 @@ /proc/getScreenSize(widescreen) if(widescreen) return CONFIG_GET(string/default_view) - return CONFIG_GET(string/default_view_square) \ No newline at end of file + return CONFIG_GET(string/default_view_square) diff --git a/code/game/gamemodes/bloodsuckers/bloodsucker.dm b/code/game/gamemodes/bloodsuckers/bloodsucker.dm index 8b7a69892061..60b56bb1354d 100644 --- a/code/game/gamemodes/bloodsuckers/bloodsucker.dm +++ b/code/game/gamemodes/bloodsuckers/bloodsucker.dm @@ -39,7 +39,13 @@ break var/datum/mind/bloodsucker = antag_pick(antag_candidates) //Yogs start -- fixes plasmaman vampires - if(bloodsucker?.current?.client.prefs.pref_species && (NOBLOOD in bloodsucker.current.client.prefs.pref_species.species_traits)) + var/species_type = bloodsucker?.current?.client.prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/noblood = (NOBLOOD in species.species_traits) + qdel(species) + + if(noblood) antag_candidates -= bloodsucker // kinda need to do this to prevent some edge-case infinite loop or whatever i-- // to undo the imminent increment continue diff --git a/code/game/gamemodes/cult/cult.dm b/code/game/gamemodes/cult/cult.dm index f70b24cac79e..b3c46194cedc 100644 --- a/code/game/gamemodes/cult/cult.dm +++ b/code/game/gamemodes/cult/cult.dm @@ -8,6 +8,8 @@ var/bloodstone_cooldown = FALSE /proc/iscultist(mob/living/M) + if(istype(M, /mob/living/carbon/human/dummy)) + return TRUE return M?.mind?.has_antag_datum(/datum/antagonist/cult) /datum/team/cult/proc/is_sacrifice_target(datum/mind/mind) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm index 7e0f34527f05..9e4caccd1d63 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm @@ -98,7 +98,7 @@ ////////////////////////////////////////////// /datum/dynamic_ruleset/roundstart/changeling - name = "Changelings" + name = "Changeling" antag_flag = ROLE_CHANGELING antag_datum = /datum/antagonist/changeling protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Brig Physician") @@ -1052,7 +1052,13 @@ /datum/dynamic_ruleset/roundstart/bloodsucker/trim_candidates() . = ..() for(var/mob/player in candidates) - if(player?.client?.prefs.pref_species && (NOBLOOD in player.client.prefs.pref_species.species_traits)) + var/species_type = player?.client?.prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/noblood = (NOBLOOD in species.species_traits) + qdel(species) + + if(noblood) candidates.Remove(player) /datum/dynamic_ruleset/roundstart/bloodsucker/pre_execute(population) diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm index 81592cb4fbab..402648fc32fd 100644 --- a/code/game/gamemodes/game_mode.dm +++ b/code/game/gamemodes/game_mode.dm @@ -183,7 +183,7 @@ var/list/antag_candidates = list() for(var/mob/living/carbon/human/H in living_crew) - if(H.client && H.client.prefs.allow_midround_antag && !is_centcom_level(H.z)) + if(H.client && !is_centcom_level(H.z)) antag_candidates += H if(!antag_candidates) diff --git a/code/game/gamemodes/wizard/raginmages.dm b/code/game/gamemodes/wizard/raginmages.dm index 7c57d672bd4c..82e7dfe757af 100644 --- a/code/game/gamemodes/wizard/raginmages.dm +++ b/code/game/gamemodes/wizard/raginmages.dm @@ -104,7 +104,7 @@ var/mob/living/carbon/human/new_character = new //The mob being spawned. SSjob.SendToLateJoin(new_character) - G_found.client.prefs.copy_to(new_character) + G_found.client.prefs.apply_prefs_to(new_character) new_character.dna.update_dna_identity() new_character.key = G_found.key diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 3b152efd4880..08cf27d94a46 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -788,8 +788,8 @@ GLOBAL_DATUM_INIT(welding_sparks, /mutable_appearance, mutable_appearance('icons openToolTip(user,src,params,title = name,content = "[desc]
Force: [force_string]",theme = "") /obj/item/MouseEntered(location, control, params) - if((item_flags & IN_INVENTORY || item_flags & IN_STORAGE) && usr.client.prefs.enable_tips && !QDELETED(src)) - var/timedelay = usr.client.prefs.tip_delay/100 + if((item_flags & IN_INVENTORY || item_flags & IN_STORAGE) && usr.client.prefs.read_preference(/datum/preference/toggle/enable_tooltips) && !QDELETED(src)) + var/timedelay = usr.client.prefs.read_preference(/datum/preference/numeric/tooltip_delay) / 100 var/user = usr tip_timer = addtimer(CALLBACK(src, .proc/openTip, location, control, params, user), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it. diff --git a/code/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm index ad4d45cea0e4..5c24164cf374 100644 --- a/code/game/objects/items/devices/PDA/PDA.dm +++ b/code/game/objects/items/devices/PDA/PDA.dm @@ -146,18 +146,18 @@ GLOBAL_LIST_EMPTY(PDAs) . = ..() if(!equipped) if(user.client) - background_color = user.client.prefs.pda_color - switch(user.client.prefs.pda_style) - if(MONO) + background_color = user.client.prefs.read_preference(/datum/preference/color/pda_color) + switch(user.client.prefs.read_preference(/datum/preference/choiced/pda_style)) + if(PDA_FONT_MONO) font_index = MODE_MONO font_mode = FONT_MONO - if(SHARE) + if(PDA_FONT_SHARE) font_index = MODE_SHARE font_mode = FONT_SHARE - if(ORBITRON) + if(PDA_FONT_ORBITRON) font_index = MODE_ORBITRON font_mode = FONT_ORBITRON - if(VT) + if(PDA_FONT_VT) font_index = MODE_VT font_mode = FONT_VT else diff --git a/code/game/objects/structures/artstuff.dm b/code/game/objects/structures/artstuff.dm index 02bb9607c5b2..32a0f55ded1d 100644 --- a/code/game/objects/structures/artstuff.dm +++ b/code/game/objects/structures/artstuff.dm @@ -247,7 +247,7 @@ /obj/structure/sign/painting/Initialize(mapload, dir, building) . = ..() - SSpersistence.painting_frames += src + SSpersistent_paintings.painting_frames += src AddComponent(/datum/component/art, 20) if(dir) setDir(dir) @@ -257,7 +257,7 @@ /obj/structure/sign/painting/Destroy() . = ..() - SSpersistence.painting_frames -= src + SSpersistent_paintings.painting_frames -= src /obj/structure/sign/painting/attackby(obj/item/I, mob/user, params) if(!C && istype(I, /obj/item/canvas)) @@ -321,12 +321,12 @@ * Deleting paintings leaves their json, so this proc will remove the json and try again if it finds one of those. */ /obj/structure/sign/painting/proc/load_persistent() - if(!persistence_id || !SSpersistence.paintings || !SSpersistence.paintings[persistence_id]) + if(!persistence_id || !SSpersistent_paintings.paintings[persistence_id]) return - var/list/painting_category = SSpersistence.paintings[persistence_id] + var/list/painting_category = SSpersistent_paintings.paintings[persistence_id] var/list/painting while(!painting) - if(!length(SSpersistence.paintings[persistence_id])) + if(!length(SSpersistent_paintings.paintings[persistence_id])) return //aborts loading anything this category has no usable paintings var/list/chosen = pick(painting_category) if(!fexists("data/paintings/[persistence_id]/[chosen["md5"]].png")) //shitmin deleted this art, lets remove json entry to avoid errors @@ -368,7 +368,7 @@ C.painting_name = "Untitled Artwork" var/data = C.get_data_string() var/md5 = md5(lowertext(data)) - var/list/current = SSpersistence.paintings[persistence_id] + var/list/current = SSpersistent_paintings.paintings[persistence_id] if(!current) current = list() for(var/list/entry in current) @@ -380,7 +380,7 @@ if(result) CRASH("Error saving persistent painting: [result]") current += list(list("title" = C.painting_name , "md5" = md5, "ckey" = C.author_ckey)) - SSpersistence.paintings[persistence_id] = current + SSpersistent_paintings.paintings[persistence_id] = current /obj/item/canvas/proc/fill_grid_from_icon(icon/I) var/h = I.Height() + 1 @@ -410,15 +410,15 @@ return var/md5 = md5(C.get_data_string()) var/author = C.author_ckey - var/list/current = SSpersistence.paintings[persistence_id] + var/list/current = SSpersistent_paintings.paintings[persistence_id] if(current) for(var/list/entry in current) if(entry["md5"] == md5) current -= entry var/png = "data/paintings/[persistence_id]/[md5].png" fdel(png) - for(var/obj/structure/sign/painting/PA in SSpersistence.painting_frames) - if(PA.C && md5(PA.C.get_data_string()) == md5) - QDEL_NULL(PA.C) + for(var/obj/structure/sign/painting/painting in SSpersistent_paintings.painting_frames) + if(painting.C && md5(painting.C.get_data_string()) == md5) + QDEL_NULL(painting.C) log_admin("[key_name(user)] has deleted a persistent painting made by [author].") message_admins(span_notice("[key_name_admin(user)] has deleted persistent painting made by [author].")) diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index 11d6d5f2458a..98807fff386d 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -123,7 +123,6 @@ GLOBAL_LIST_EMPTY(lockers) is_animating_door = FALSE vis_contents -= door_obj update_icon() - COMPILE_OVERLAYS(src) /obj/structure/closet/proc/get_door_transform(angle) var/matrix/M = matrix() diff --git a/code/game/objects/structures/life_candle.dm b/code/game/objects/structures/life_candle.dm index c95ec00532d7..8a345c4252d3 100644 --- a/code/game/objects/structures/life_candle.dm +++ b/code/game/objects/structures/life_candle.dm @@ -81,7 +81,7 @@ body = new mob_type(T) var/mob/ghostie = mind.get_ghost(TRUE) if(ghostie.client && ghostie.client.prefs) - ghostie.client.prefs.copy_to(body) + ghostie.client.prefs.apply_prefs_to(body) mind.transfer_to(body) else body.forceMove(T) diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 1c0a482289ad..de6fee14eb33 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -117,7 +117,7 @@ body += "MENTORHELP | " body += "DEADCHAT\]" body += "(toggle all)" - body += "FREEZE" //yogs - adminfreezing + body += "FREEZE" //yogs - adminfreezing body += "

" body += "Jump to | " diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index be65edc8a07c..accce5184121 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -12,9 +12,6 @@ GLOBAL_PROTECT(admin_verbs_default) /client/verb/dsay, /*talk in deadchat using our ckey/fakekey*/ /client/proc/investigate_show, /*various admintools for investigation. Such as a singulo grief-log*/ /client/proc/secrets, - /client/proc/toggle_split_admin_tabs, - /client/proc/toggle_fast_mc_refresh, - /client/proc/toggle_hear_radio, /*allows admins to hide all radio output*/ /client/proc/reload_admins, /client/proc/reload_mentors, /client/proc/reestablish_db_connection, /*reattempt a connection to the database*/ @@ -78,12 +75,6 @@ GLOBAL_PROTECT(admin_verbs_admin) /client/proc/toggle_combo_hud, // toggle display of the combination pizza antag and taco sci/med/eng hud /client/proc/toggle_AI_interact, /*toggle admin ability to interact with machines as an AI*/ /datum/admins/proc/open_shuttlepanel, /* Opens shuttle manipulator UI */ - /client/proc/deadchat, - /client/proc/toggleprayers, - /client/proc/toggle_prayer_sound, - /client/proc/colorasay, - /client/proc/resetasaycolor, - /client/proc/toggleadminhelpsound, /client/proc/respawn_character, /client/proc/discord_id_manipulation, /datum/admins/proc/open_borgopanel, diff --git a/code/modules/admin/secrets.dm b/code/modules/admin/secrets.dm index fc77033780e1..e924b06c3f29 100644 --- a/code/modules/admin/secrets.dm +++ b/code/modules/admin/secrets.dm @@ -388,7 +388,7 @@ for(var/mob/living/carbon/human/H in GLOB.carbon_list) if(!get_turf(H)) continue - if(H.client?.prefs?.disable_alternative_announcers) + if(H.client?.prefs?.read_preference(/datum/preference/toggle/disable_alternative_announcers)) SEND_SOUND(H, sound(SSstation.default_announcer.event_sounds[ANNOUNCER_ANIMES])) else SEND_SOUND(H, sound(SSstation.announcer.event_sounds[ANNOUNCER_ANIMES])) diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index 10298e4d70c2..83fd45eeb708 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -134,7 +134,8 @@ panel_height = 240 var/datum/browser/panel = new(usr, "banpanel", "Banning Panel", 910, panel_height) panel.add_stylesheet("banpanelcss", 'html/admin/banpanel.css') - if(usr.client.prefs.tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements and DOM methods that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support + var/tgui_fancy = usr.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy) + if(tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements and DOM methods that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support panel.add_stylesheet("banpanelcss3", 'html/admin/banpanel_css3.css') panel.add_script("banpaneljs", 'html/admin/banpanel.js') var/list/output = list("
[HrefTokenFormField()]") @@ -240,14 +241,14 @@ banned_from += query_get_banned_roles.item[1] qdel(query_get_banned_roles) var/break_counter = 0 - output += "
" + output += "
" //all heads are listed twice so have a javascript call to toggle both their checkboxes when one is pressed //for simplicity this also includes the captain even though it doesn't do anything for(var/job in GLOB.original_command_positions) if(break_counter > 0 && (break_counter % 3 == 0)) output += "
" output += {" "} break_counter++ @@ -260,9 +261,9 @@ "Supply" = GLOB.original_supply_positions) for(var/department in job_lists) //the first element is the department head so they need the same javascript call as above - output += "
" + output += "
" output += {" "} break_counter = 1 @@ -279,7 +280,7 @@ var/list/headless_job_lists = list("Silicon" = GLOB.original_nonhuman_positions, "Abstract" = list("Appearance", "Emote", "OOC", "Voice Announcements", "DEAD")) for(var/department in headless_job_lists) - output += "
" + output += "
" break_counter = 0 for(var/job in headless_job_lists[department]) if(break_counter > 0 && (break_counter % 3 == 0)) @@ -301,7 +302,7 @@ ROLE_TRAITOR, ROLE_WIZARD, ROLE_GANG, ROLE_VAMPIRE, ROLE_SHADOWLING, ROLE_DARKSPAWN, ROLE_ZOMBIE, ROLE_HERETIC)) //ROLE_REV_HEAD is excluded from this because rev jobbans are handled by ROLE_REV for(var/department in long_job_lists) - output += "
" + output += "
" break_counter = 0 for(var/job in long_job_lists[department]) if(break_counter > 0 && (break_counter % 10 == 0)) diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 028544d4a5a7..10d7c5a9cf6c 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -28,10 +28,11 @@ var/mob/M = locate(href_list["afreeze"]) in GLOB.mob_list if(!M || !M.client) return + var/message - if(M.client.prefs.afreeze) + if(M.client.afreeze) to_chat(M, span_userdanger("You are no longer frozen.")) - M.client.prefs.afreeze = FALSE + M.client.afreeze = FALSE M.client.show_popup_menus = TRUE M.client.show_verb_panel = TRUE M.notransform = FALSE @@ -39,7 +40,7 @@ message = "[key_name(usr)] has unfrozen [key_name(M)]." else to_chat(M, span_userdanger("You have been frozen by an administrator.")) - M.client.prefs.afreeze = TRUE + M.client.afreeze = TRUE M.client.show_popup_menus = FALSE M.client.show_verb_panel = FALSE M.notransform = TRUE diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm index bb8bdb1c9b1d..f7d256dd3e59 100644 --- a/code/modules/admin/verbs/mapping.dm +++ b/code/modules/admin/verbs/mapping.dm @@ -350,7 +350,6 @@ GLOBAL_VAR_INIT(say_disabled, FALSE) qdel(I) randomize_human(D) JB.equip(D, TRUE, FALSE) - COMPILE_OVERLAYS(D) var/icon/I = icon(getFlatIcon(D), frame = 1) final.Insert(I, JB.title) qdel(D) diff --git a/code/modules/admin/verbs/one_click_antag.dm b/code/modules/admin/verbs/one_click_antag.dm index ba928e795a68..bc7b90fe28b4 100644 --- a/code/modules/admin/verbs/one_click_antag.dm +++ b/code/modules/admin/verbs/one_click_antag.dm @@ -352,7 +352,6 @@ equipAntagOnDummy(mannequin, ert) - COMPILE_OVERLAYS(mannequin) CHECK_TICK var/icon/preview_icon = icon('icons/effects/effects.dmi', "nothing") preview_icon.Scale(48+32, 16+32) @@ -451,7 +450,7 @@ //Spawn the body var/mob/living/carbon/human/ERTOperative = new ertemplate.mobtype(spawnloc) - chosen_candidate.client.prefs.copy_to(ERTOperative) + chosen_candidate.client.prefs.apply_prefs_to(ERTOperative) ERTOperative.key = chosen_candidate.key if(ertemplate.enforce_human || !(ERTOperative.dna.species.changesource_flags & ERT_SPAWN)) // Don't want any exploding plasmemes @@ -562,7 +561,7 @@ //Spawn the body var/mob/living/carbon/human/ERTOperative = new ertemplate.mobtype(spawnloc) - chosen_candidate.client.prefs.copy_to(ERTOperative) + chosen_candidate.client.prefs.apply_prefs_to(ERTOperative) ERTOperative.key = chosen_candidate.key if(ertemplate.enforce_human || !(ERTOperative.dna.species.changesource_flags & ERT_SPAWN)) // Don't want any exploding plasmemes diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index 6635a81b0995..7e02ea347b44 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -432,9 +432,9 @@ Traitors and the like can also be revived with the previous role mostly intact. new_character.age = record_found.fields["age"] new_character.hardset_dna(record_found.fields["identity"], record_found.fields["enzymes"], null, record_found.fields["name"], record_found.fields["blood_type"], new record_found.fields["species"], record_found.fields["features"]) else - var/datum/preferences/A = new() - A.copy_to(new_character) - A.real_name = G_found.real_name + new_character.randomize_human_appearance(~(RANDOMIZE_NAME|RANDOMIZE_SPECIES)) + new_character.name = G_found.real_name + new_character.real_name = G_found.real_name new_character.dna.update_dna_identity() new_character.name = new_character.real_name @@ -826,13 +826,13 @@ Traitors and the like can also be revived with the previous role mostly intact. newview = input("Enter custom view range:","FUCK YEEEEE") as num if(!newview) return - if(newview > 64) + if(newview >= 64) if(alert("Warning: Setting your view range to that large size may cause horrendous lag, visual bugs, and/or game crashes. Are you sure?",,"Yes","No") != "Yes") return view_size.setTo(newview) //yogs end else - view_size.resetToDefault(getScreenSize(prefs.widescreenpref)) + view_size.resetToDefault(getScreenSize(prefs.read_preference(/datum/preference/toggle/widescreen))) log_admin("[key_name(usr)] changed their view range to [view].") //message_admins("\blue [key_name_admin(usr)] changed their view range to [view].") //why? removed by order of XSI @@ -1509,8 +1509,7 @@ Traitors and the like can also be revived with the previous role mostly intact. var/mob/M = usr if(isobserver(M)) var/mob/living/carbon/human/H = new(T) - var/datum/preferences/A = new - A.copy_to(H) + H.randomize_human_appearance(~(RANDOMIZE_SPECIES)) H.dna.update_dna_identity() H.equipOutfit(/datum/outfit/centcom/official/nopda) @@ -1577,8 +1576,7 @@ Traitors and the like can also be revived with the previous role mostly intact. var/mob/M = usr if(isobserver(M)) var/mob/living/carbon/human/H = new(T) - var/datum/preferences/A = new - A.copy_to(H) + H.randomize_human_appearance(~(RANDOMIZE_SPECIES)) H.dna.update_dna_identity() var/datum/mind/Mind = new /datum/mind(M.key) // Reusing the mob's original mind actually breaks objectives for any antag who had this person as their target. diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index 1d10ee85a68a..bfed056bb21d 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -25,6 +25,8 @@ GLOBAL_LIST_EMPTY(antagonists) var/show_to_ghosts = FALSE // Should this antagonist be shown as antag to ghosts? Shouldn't be used for stealthy antagonists like traitors /// The corporation employing us var/datum/corporation/company + /// The typepath for the outfit to show in the preview for the preferences menu. + var/preview_outfit /datum/antagonist/New() @@ -220,6 +222,41 @@ GLOBAL_LIST_EMPTY(antagonists) return FALSE return TRUE +/// Creates an icon from the preview outfit. +/// Custom implementors of `get_preview_icon` should use this, as the +/// result of `get_preview_icon` is expected to be the completed version. +/datum/antagonist/proc/render_preview_outfit(datum/outfit/outfit, mob/living/carbon/human/dummy) + dummy = dummy || new /mob/living/carbon/human/dummy/consistent + dummy.equipOutfit(outfit, visualsOnly = TRUE) + var/icon = getFlatIcon(dummy) + + // We don't want to qdel the dummy right away, since its items haven't initialized yet. + SSatoms.prepare_deletion(dummy) + + return icon + +/// Given an icon, will crop it to be consistent of those in the preferences menu. +/// Not necessary, and in fact will look bad if it's anything other than a human. +/datum/antagonist/proc/finish_preview_icon(icon/icon) + // Zoom in on the top of the head and the chest + // I have no idea how to do this dynamically. + icon.Scale(115, 115) + + // This is probably better as a Crop, but I cannot figure it out. + icon.Shift(WEST, 8) + icon.Shift(SOUTH, 30) + + icon.Crop(1, 1, ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + + return icon + +/// Returns the icon to show on the preferences menu. +/datum/antagonist/proc/get_preview_icon() + if (isnull(preview_outfit)) + return null + + return finish_preview_icon(render_preview_outfit(preview_outfit)) + // List if ["Command"] = CALLBACK(), user will be appeneded to callback arguments on execution /datum/antagonist/proc/get_admin_commands() . = list() diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm index 629e087cbbbb..7d6a66f4a57e 100644 --- a/code/modules/antagonists/_common/antag_spawner.dm +++ b/code/modules/antagonists/_common/antag_spawner.dm @@ -88,7 +88,7 @@ /obj/item/antag_spawner/contract/spawn_antag(client/C, turf/T, kind ,datum/mind/user) new /obj/effect/particle_effect/smoke(T) var/mob/living/carbon/human/M = new/mob/living/carbon/human(T) - C.prefs.copy_to(M) + C.prefs.apply_prefs_to(M) M.key = C.key var/datum/mind/app_mind = M.mind @@ -151,7 +151,7 @@ /obj/item/antag_spawner/nuke_ops/spawn_antag(client/C, turf/T, kind, datum/mind/user) var/mob/living/carbon/human/M = new/mob/living/carbon/human(T) - C.prefs.copy_to(M) + C.prefs.apply_prefs_to(M) M.key = C.key var/datum/antagonist/nukeop/new_op = new() @@ -170,7 +170,7 @@ /obj/item/antag_spawner/nuke_ops/clown/spawn_antag(client/C, turf/T, kind, datum/mind/user) var/mob/living/carbon/human/M = new/mob/living/carbon/human(T) - C.prefs.copy_to(M) + C.prefs.apply_prefs_to(M) M.key = C.key var/datum/antagonist/nukeop/clownop/new_op = new /datum/antagonist/nukeop/clownop() diff --git a/code/modules/antagonists/abductor/abductor.dm b/code/modules/antagonists/abductor/abductor.dm index b13ded9656e4..6a4f8c482bac 100644 --- a/code/modules/antagonists/abductor/abductor.dm +++ b/code/modules/antagonists/abductor/abductor.dm @@ -13,6 +13,26 @@ var/landmark_type var/greet_text +/datum/antagonist/abductor/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/scientist = new + var/mob/living/carbon/human/dummy/consistent/agent = new + + scientist.set_species(/datum/species/abductor) + agent.set_species(/datum/species/abductor) + + var/icon/scientist_icon = render_preview_outfit(/datum/outfit/abductor/scientist, scientist) + scientist_icon.Shift(WEST, 8) + + var/icon/agent_icon = render_preview_outfit(/datum/outfit/abductor/agent, agent) + agent_icon.Shift(EAST, 8) + + var/icon/final_icon = scientist_icon + final_icon.Blend(agent_icon, ICON_OVERLAY) + + qdel(scientist) + qdel(agent) + + return finish_preview_icon(final_icon) /datum/antagonist/abductor/agent name = "Abductor Agent" diff --git a/code/modules/antagonists/blob/blob.dm b/code/modules/antagonists/blob/blob.dm index a7efe42ffa11..80fcaaf6cbb6 100644 --- a/code/modules/antagonists/blob/blob.dm +++ b/code/modules/antagonists/blob/blob.dm @@ -41,6 +41,16 @@ to_chat(owner.current, "You are no longer the Blob!") return ..() +/datum/antagonist/blob/get_preview_icon() + var/datum/blobstrain/reagent/reactive_spines/reactive_spines = /datum/blobstrain/reagent/reactive_spines + + var/icon/icon = icon('icons/mob/blob.dmi', "blob_core") + icon.Blend(initial(reactive_spines.color), ICON_MULTIPLY) + icon.Blend(icon('icons/mob/blob.dmi', "blob_core_overlay"), ICON_OVERLAY) + icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + + return icon + /datum/antagonist/blob/proc/create_objectives() var/datum/objective/blob_takeover/main = new main.owner = owner diff --git a/code/modules/antagonists/bloodsuckers/bloodsuckers.dm b/code/modules/antagonists/bloodsuckers/bloodsuckers.dm index 89a18d7a4e34..dcacce04380c 100644 --- a/code/modules/antagonists/bloodsuckers/bloodsuckers.dm +++ b/code/modules/antagonists/bloodsuckers/bloodsuckers.dm @@ -836,7 +836,7 @@ var/mob/living/carbon/human/user = convertee.current if(!(user.dna?.species) || !(user.mob_biotypes & MOB_ORGANIC)) user.set_species(/datum/species/human) - user.apply_pref_name("human", user.client) + user.apply_pref_name(/datum/preference/name/real_name, user.client) // Check for Fledgeling if(converter) message_admins("[convertee] has become a Bloodsucker, and was created by [converter].") @@ -934,3 +934,10 @@ var/datum/atom_hud/antag/vamphud = GLOB.huds[ANTAG_HUD_BLOODSUCKER] vamphud.leave_hud(owner.current) set_antag_hud(owner.current, null) + +/datum/antagonist/bloodsucker/get_preview_icon() + var/icon/bloodsucker_icon = icon('icons/mob/bloodsucker_mobs.dmi', "batform") + + bloodsucker_icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + + return bloodsucker_icon diff --git a/code/modules/antagonists/brother/brother.dm b/code/modules/antagonists/brother/brother.dm index 1fa72c2d1330..768ed4565efc 100644 --- a/code/modules/antagonists/brother/brother.dm +++ b/code/modules/antagonists/brother/brother.dm @@ -43,6 +43,34 @@ /datum/antagonist/brother/antag_panel_data() return "Conspirators : [get_brother_names()]" +/datum/antagonist/brother/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/brother1 = new + var/mob/living/carbon/human/dummy/consistent/brother2 = new + + brother1.dna.features["ethcolor"] = GLOB.color_list_ethereal["Faint Red"] + brother1.set_species(/datum/species/ethereal) + + brother2.dna.features["moth_antennae"] = "Plain" + brother2.dna.features["moth_markings"] = "None" + brother2.dna.features["moth_wings"] = "Plain" + brother2.set_species(/datum/species/moth) + + var/icon/brother1_icon = render_preview_outfit(/datum/outfit/job/quartermaster, brother1) + brother1_icon.Blend(icon('icons/effects/blood.dmi', "maskblood"), ICON_OVERLAY) + brother1_icon.Shift(WEST, 8) + + var/icon/brother2_icon = render_preview_outfit(/datum/outfit/job/scientist, brother2) + brother2_icon.Blend(icon('icons/effects/blood.dmi', "uniformblood"), ICON_OVERLAY) + brother2_icon.Shift(EAST, 8) + + var/icon/final_icon = brother1_icon + final_icon.Blend(brother2_icon, ICON_OVERLAY) + + qdel(brother1) + qdel(brother2) + + return finish_preview_icon(final_icon) + /datum/antagonist/brother/proc/get_brother_names() var/list/brothers = team.members - owner var/brother_text = "" diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm index d7099270836f..bf546dbd56fe 100644 --- a/code/modules/antagonists/changeling/changeling.dm +++ b/code/modules/antagonists/changeling/changeling.dm @@ -109,8 +109,8 @@ if(ishuman(C) && (NO_DNA_COPY in C.dna.species.species_traits || !C.has_dna())) to_chat(C, span_userdanger("You have been made a human, as your original race had incompatible DNA.")) C.set_species(/datum/species/human, TRUE, TRUE) - if(C.client?.prefs?.custom_names["human"] && !is_banned_from(C.client?.ckey, "Appearance")) - C.fully_replace_character_name(C.dna.real_name, C.client.prefs.custom_names["human"]) + if(C.client?.prefs?.read_preference(/datum/preference/name/real_name) && !is_banned_from(C.client?.ckey, "Appearance")) + C.fully_replace_character_name(C.dna.real_name, C.client.prefs.read_preference(/datum/preference/name/real_name)) else C.fully_replace_character_name(C.dna.real_name, random_unique_name(C.gender)) @@ -625,3 +625,24 @@ /datum/antagonist/changeling/xenobio/antag_listing_name() return ..() + "(Xenobio)" + +/datum/antagonist/changeling/get_preview_icon() + var/icon/final_icon = render_preview_outfit(/datum/outfit/changeling) + var/icon/split_icon = render_preview_outfit(/datum/outfit/job/engineer) + + final_icon.Shift(WEST, world.icon_size / 2) + final_icon.Shift(EAST, world.icon_size / 2) + + split_icon.Shift(EAST, world.icon_size / 2) + split_icon.Shift(WEST, world.icon_size / 2) + + final_icon.Blend(split_icon, ICON_OVERLAY) + + return finish_preview_icon(final_icon) + +/datum/outfit/changeling + name = "Changeling" + + head = /obj/item/clothing/head/helmet/changeling + suit = /obj/item/clothing/suit/armor/changeling + l_hand = /obj/item/melee/arm_blade diff --git a/code/modules/antagonists/changeling/powers/hivemind.dm b/code/modules/antagonists/changeling/powers/hivemind.dm index 9e63c555426b..6784dbc4f945 100644 --- a/code/modules/antagonists/changeling/powers/hivemind.dm +++ b/code/modules/antagonists/changeling/powers/hivemind.dm @@ -59,7 +59,7 @@ GLOBAL_LIST_EMPTY(hivemind_bank) to_chat(user, span_warning("The airwaves already have all of our DNA!")) return - var/chosen_name = input("Select a DNA to channel: ", "Channel DNA", null) as null|anything in sortList(names) + var/chosen_name = tgui_input_list(user, "Select a DNA to channel", "Channel DNA", sortList(names)) if(!chosen_name) return @@ -104,7 +104,7 @@ GLOBAL_LIST_EMPTY(hivemind_bank) to_chat(user, span_warning("There's no new DNA to absorb from the air!")) return - var/S = input("Select a DNA absorb from the air: ", "Absorb DNA", null) as null|anything in sortList(names) + var/S = tgui_input_list(user, "Select a DNA absorb from the air", "Absorb DNA", sortList(names)) if(!S) return var/datum/changelingprofile/chosen_prof = names[S] diff --git a/code/modules/antagonists/changeling/powers/humanform.dm b/code/modules/antagonists/changeling/powers/humanform.dm index 3f517494f860..6c8f870524c2 100644 --- a/code/modules/antagonists/changeling/powers/humanform.dm +++ b/code/modules/antagonists/changeling/powers/humanform.dm @@ -15,7 +15,7 @@ for(var/datum/changelingprofile/prof in changeling.stored_profiles) names += "[prof.name]" - var/chosen_name = input("Select the target DNA: ", "Target DNA", null) as null|anything in names + var/chosen_name = tgui_input_list("Select the target DNA", "Target DNA", sortList(names)) if(!chosen_name) return diff --git a/code/modules/antagonists/clockcult/clockcult.dm b/code/modules/antagonists/clockcult/clockcult.dm index 20ab3e2a8daa..862c4d2893b5 100644 --- a/code/modules/antagonists/clockcult/clockcult.dm +++ b/code/modules/antagonists/clockcult/clockcult.dm @@ -259,3 +259,10 @@ if(ratio >= SERVANT_HARDMODE_PERCENT) GLOB.clockwork_hardmode_active = TRUE hierophant_message("As the cult increases in size, the Ark's connection to the material plane weakens. Warping with camera consoles will take substantially more time unless the destination is a clockwork tile!") + +/datum/antagonist/clockcult/get_preview_icon() + var/icon/clockie_icon = icon('icons/effects/512x512.dmi', "ratvar") + + clockie_icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + + return clockie_icon diff --git a/code/modules/antagonists/creep/creep.dm b/code/modules/antagonists/creep/creep.dm index c59a29e94729..cc887dafe65c 100644 --- a/code/modules/antagonists/creep/creep.dm +++ b/code/modules/antagonists/creep/creep.dm @@ -2,6 +2,7 @@ name = "Obsessed" show_in_antagpanel = TRUE antagpanel_category = "Other" + preview_outfit = /datum/outfit/obsessed job_rank = ROLE_OBSESSED show_name_in_check_antagonists = TRUE roundend_category = "obsessed" @@ -21,6 +22,40 @@ //PRESTO FUCKIN MAJESTO C.gain_trauma(/datum/brain_trauma/special/obsessed)//ZAP +/datum/antagonist/obsessed/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/victim_dummy = new + victim_dummy.hair_color = "b96" // Brown + victim_dummy.hair_style = "Messy" + victim_dummy.update_hair() + + var/icon/obsessed_icon = render_preview_outfit(preview_outfit) + obsessed_icon.Blend(icon('icons/effects/blood.dmi', "uniformblood"), ICON_OVERLAY) + + var/icon/final_icon = finish_preview_icon(obsessed_icon) + + final_icon.Blend( + icon('icons/ui_icons/antags/obsessed.dmi', "obsession"), + ICON_OVERLAY, + ANTAGONIST_PREVIEW_ICON_SIZE - 30, + 20, + ) + + return final_icon + +/datum/outfit/obsessed + name = "Obsessed (Preview only)" + + uniform = /obj/item/clothing/under/yogs/redoveralls + gloves = /obj/item/clothing/gloves/color/latex + mask = /obj/item/clothing/mask/surgical + neck = /obj/item/camera + suit = /obj/item/clothing/suit/apron + +/datum/outfit/obsessed/post_equip(mob/living/carbon/human/H) + for(var/obj/item/carried_item in H.get_equipped_items(TRUE)) + carried_item.add_mob_blood(H)//Oh yes, there will be blood... + H.regenerate_icons() + /datum/antagonist/obsessed/greet() owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/creepalert.ogg', 100, FALSE, pressure_affected = FALSE) to_chat(owner, span_boldannounce("You are the Obsessed!")) diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm index d014109b7215..eac79a76e68c 100644 --- a/code/modules/antagonists/cult/cult.dm +++ b/code/modules/antagonists/cult/cult.dm @@ -11,6 +11,7 @@ var/datum/action/innate/cult/comm/communion = new var/datum/action/innate/cult/mastervote/vote = new var/datum/action/innate/cult/blood_magic/magic = new + preview_outfit = /datum/outfit/cultist job_rank = ROLE_CULTIST var/ignore_implant = FALSE var/give_equipment = FALSE @@ -83,6 +84,56 @@ if(cult_team.blood_target && cult_team.blood_target_image && current.client) current.client.images += cult_team.blood_target_image +/* +/datum/antagonist/cult/get_preview_icon() + var/icon/icon = render_preview_outfit(preview_outfit) + + // The longsword is 64x64, but getFlatIcon crunches to 32x32. + // So I'm just going to add it in post, screw it. + + // Center the dude, because item icon states start from the center. + // This makes the image 64x64. + icon.Crop(-15, -15, 48, 48) + + var/obj/item/melee/cultblade/longsword = new + icon.Blend(icon(longsword.lefthand_file, longsword.item_state), ICON_OVERLAY) + qdel(longsword) + + // Move the guy back to the bottom left, 32x32. + icon.Crop(17, 17, 48, 48) + + return finish_preview_icon(icon) +*/ +/datum/antagonist/cult/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/cult1 = new + var/mob/living/carbon/human/dummy/consistent/cult2 = new + + var/icon/final_icon = render_preview_outfit(/datum/outfit/cultist/leader, cult1) + var/icon/teammate = render_preview_outfit(/datum/outfit/cultist/follower, cult2) + teammate.Blend(rgb(128, 128, 128, 128), ICON_MULTIPLY) + + final_icon.Blend(teammate, ICON_OVERLAY, -world.icon_size / 4, 0) + final_icon.Blend(teammate, ICON_OVERLAY, world.icon_size / 4, 0) + + qdel(cult1) + qdel(cult2) + + return finish_preview_icon(final_icon) + +/datum/outfit/cultist/leader + suit = /obj/item/clothing/suit/hooded/cultrobes/berserker + shoes = /obj/item/clothing/shoes/cult/alt + head = /obj/item/clothing/head/hooded/berserkerhood + glasses = /obj/item/clothing/glasses/hud/health/night/cultblind + r_hand = /obj/item/melee/cultblade + l_hand = /obj/item/shield/mirror + +/datum/outfit/cultist/follower + suit = /obj/item/clothing/suit/cultrobes/alt + shoes = /obj/item/clothing/shoes/cult/alt + head = /obj/item/clothing/head/culthood/alt + glasses = /obj/item/clothing/glasses/hud/health/night/cultblind + r_hand = /obj/item/melee/cultblade /datum/antagonist/cult/proc/equip_cultist(metal=TRUE) var/mob/living/carbon/H = owner.current @@ -522,3 +573,20 @@ /datum/team/cult/is_gamemode_hero() return SSticker.mode.name == "cult" + +/datum/outfit/cultist + name = "Cultist (Preview only)" + + uniform = /obj/item/clothing/under/color/black + suit = /obj/item/clothing/suit/yogs/armor/sith_suit + shoes = /obj/item/clothing/shoes/cult/alt + r_hand = /obj/item/melee/blood_magic/stun + +/datum/outfit/cultist/post_equip(mob/living/carbon/human/H, visualsOnly) + H.eye_color = BLOODCULT_EYE + H.update_body() + + var/obj/item/clothing/suit/hooded/hooded = locate() in H + if(!isdummy(H)) + hooded.MakeHood() // This is usually created on Initialize, but we run before atoms + hooded.ToggleHood() diff --git a/code/modules/antagonists/demon/demons.dm b/code/modules/antagonists/demon/demons.dm index d1fccba1c73d..3e737527468a 100644 --- a/code/modules/antagonists/demon/demons.dm +++ b/code/modules/antagonists/demon/demons.dm @@ -180,3 +180,10 @@ #undef SIN_PRIDE #undef SIN_SLOTH #undef SIN_WRATH + +/datum/antagonist/sinfuldemon/get_preview_icon() + var/icon/sinfuldemon_icon = icon('icons/mob/mob.dmi', "lesserdaemon") + + sinfuldemon_icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + + return sinfuldemon_icon diff --git a/code/modules/antagonists/devil/devil.dm b/code/modules/antagonists/devil/devil.dm index d55a03b0fb41..d243aeff5fa5 100644 --- a/code/modules/antagonists/devil/devil.dm +++ b/code/modules/antagonists/devil/devil.dm @@ -214,13 +214,10 @@ GLOBAL_LIST_INIT(devil_suffix, list(" the Red", " the Soulless", " the Master", /datum/antagonist/devil/proc/regress_humanoid() to_chat(owner.current, span_warning("Your powers weaken, have more contracts be signed to regain power.")) if(ishuman(owner.current)) - var/species_to_be var/mob/living/carbon/human/H = owner.current - if(!isnull(owner.current.client.prefs.pref_species)) - species_to_be = owner.current.client.prefs.pref_species //fixes a really stupid bug where devils would turn into out of place looking humans after getting detransformed - else - species_to_be = /datum/species/human - H.set_species(species_to_be, 1) + + var/species_type = owner.current.client.prefs.read_preference(/datum/preference/choiced/species) + H.set_species(species_type, 1) H.regenerate_icons() give_appropriate_spells() if(istype(owner.current.loc, /obj/effect/dummy/phased_mob)) @@ -575,3 +572,10 @@ GLOBAL_LIST_INIT(devil_suffix, list(" the Red", " the Soulless", " the Master", ban = randomdevilban() banish = randomdevilbanish() ascendable = prob(25) + +/datum/antagonist/devil/get_preview_icon() + var/icon/devil_icon = icon('icons/effects/64x64.dmi', "devil") + + devil_icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + + return devil_icon diff --git a/code/modules/antagonists/disease/disease_datum.dm b/code/modules/antagonists/disease/disease_datum.dm index 7c4cbd9c3a26..fb18acc65d00 100644 --- a/code/modules/antagonists/disease/disease_datum.dm +++ b/code/modules/antagonists/disease/disease_datum.dm @@ -75,6 +75,11 @@ return result.Join("
") +/datum/antagonist/disease/get_preview_icon() + var/icon/icon = icon('icons/mob/hud.dmi', "virus_infected") + icon.Blend(COLOR_GREEN_GRAY, ICON_MULTIPLY) + icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + return icon /datum/objective/disease_infect explanation_text = "Survive and infect as many people as possible." @@ -102,3 +107,9 @@ if(L.onCentCom() || L.onSyndieBase()) return TRUE return FALSE + +/datum/antagonist/disease/get_preview_icon() + var/icon/disease_icon = icon('icons/mob/hud.dmi', "infected") + disease_icon.Blend(COLOR_GREEN_GRAY, ICON_MULTIPLY) + disease_icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + return disease_icon diff --git a/code/modules/antagonists/disease/disease_mob.dm b/code/modules/antagonists/disease/disease_mob.dm index 0ea7055431b4..13cf0085f31d 100644 --- a/code/modules/antagonists/disease/disease_mob.dm +++ b/code/modules/antagonists/disease/disease_mob.dm @@ -126,7 +126,7 @@ the new instance inside the host to be updated to the template's stats. else link = "" // Create map text prior to modifying message for goonchat - if (client?.prefs.chat_on_map && (client.prefs.see_chat_non_mob || ismob(speaker))) + if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && (client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker))) create_chat_message(speaker, message_language, raw_message, spans) // Recompose the message, because it's scrambled by default message = compose_message(speaker, message_language, raw_message, radio_freq, spans, message_mods) diff --git a/code/modules/antagonists/eldritch_cult/eldritch_antag.dm b/code/modules/antagonists/eldritch_cult/eldritch_antag.dm index 6a7e55e0f8e6..cf93ce107723 100644 --- a/code/modules/antagonists/eldritch_cult/eldritch_antag.dm +++ b/code/modules/antagonists/eldritch_cult/eldritch_antag.dm @@ -5,6 +5,7 @@ antag_moodlet = /datum/mood_event/heretics job_rank = ROLE_HERETIC can_hijack = HIJACK_HIJACKER + preview_outfit = /datum/outfit/heretic var/give_equipment = TRUE var/list/researched_knowledge = list() var/list/transmutations = list() @@ -41,6 +42,27 @@ You can find a basic guide at : https://wiki.yogstation.net/wiki/Heretic
\ If you need to quickly check your unlocked transmutation recipes, alt+click your Codex Cicatrix.") +/datum/antagonist/heretic/get_preview_icon() + var/icon/icon = render_preview_outfit(preview_outfit) + + // MOTHBLOCKS TOOD: Copied and pasted from cult, make this its own proc + + // The sickly blade is 64x64, but getFlatIcon crunches to 32x32. + // So I'm just going to add it in post, screw it. + + // Center the dude, because item icon states start from the center. + // This makes the image 64x64. + icon.Crop(-15, -15, 48, 48) + + var/obj/item/melee/sickly_blade/blade = new + icon.Blend(icon(blade.lefthand_file, blade.item_state), ICON_OVERLAY) + qdel(blade) + + // Move the guy back to the bottom left, 32x32. + icon.Crop(17, 17, 48, 48) + + return finish_preview_icon(icon) + /datum/antagonist/heretic/on_gain() var/mob/living/current = owner.current if(ishuman(current)) @@ -277,3 +299,14 @@ if(!cultie) return FALSE return cultie.total_sacrifices >= target_amount + +/datum/outfit/heretic + name = "Heretic (Preview only)" + + suit = /obj/item/clothing/suit/hooded/cultrobes/eldritch + r_hand = /obj/item/melee/touch_attack/mansus_fist + +/datum/outfit/heretic/post_equip(mob/living/carbon/human/H, visualsOnly) + var/obj/item/clothing/suit/hooded/hooded = locate() in H + hooded.MakeHood() // This is usually created on Initialize, but we run before atoms + hooded.ToggleHood() diff --git a/code/modules/antagonists/fugitive/fugitive.dm b/code/modules/antagonists/fugitive/fugitive.dm index cfc736bf9d8e..b228bd53dca9 100644 --- a/code/modules/antagonists/fugitive/fugitive.dm +++ b/code/modules/antagonists/fugitive/fugitive.dm @@ -7,6 +7,7 @@ var/datum/team/fugitive/fugitive_team var/is_captured = FALSE var/backstory = "error" + preview_outfit = /datum/outfit/spacepol /datum/antagonist/fugitive/apply_innate_effects(mob/living/mob_override) var/mob/living/M = mob_override || owner.current @@ -125,3 +126,4 @@ else if(M in GLOB.dead_mob_list) to_chat(M, "[FOLLOW_LINK(M, user)] [my_message]") user.log_talk(message, LOG_SAY, tag="Yalp Elor") + diff --git a/code/modules/antagonists/hivemind/hivemind.dm b/code/modules/antagonists/hivemind/hivemind.dm index bf32a0efeb0e..f6e134f73bc9 100644 --- a/code/modules/antagonists/hivemind/hivemind.dm +++ b/code/modules/antagonists/hivemind/hivemind.dm @@ -296,3 +296,10 @@ /datum/antagonist/hivemind/is_gamemode_hero() return SSticker.mode.name == "Assimilation" + +/datum/antagonist/hivemind/get_preview_icon() + var/icon/hivemind_icon = icon('icons/mob/hivebot.dmi', "basic") + + hivemind_icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + + return hivemind_icon diff --git a/code/modules/antagonists/horror/horror.dm b/code/modules/antagonists/horror/horror.dm index 745839b91348..70ec3d113a63 100644 --- a/code/modules/antagonists/horror/horror.dm +++ b/code/modules/antagonists/horror/horror.dm @@ -859,3 +859,4 @@ else RemoveInfestActions() GrantHorrorActions() + diff --git a/code/modules/antagonists/horror/horror_datums.dm b/code/modules/antagonists/horror/horror_datums.dm index 1d135f94d3fc..a6342c23738a 100644 --- a/code/modules/antagonists/horror/horror_datums.dm +++ b/code/modules/antagonists/horror/horror_datums.dm @@ -340,3 +340,10 @@ to_chat(src, span_userdanger("With an immense exertion of will, you regain control of your body!")) to_chat(H.victim, span_danger("You feel control of the host brain ripped from your grasp, and retract your probosci before the wild neural impulses can damage you.")) H.detatch() + +/datum/antagonist/horror/get_preview_icon() + var/icon/horror_icon = icon('icons/mob/animal.dmi', "horror_preview") + + horror_icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + + return horror_icon diff --git a/code/modules/antagonists/malf/malf.dm b/code/modules/antagonists/malf/malf.dm index 1d6a75c669cc..06f6f11c0be5 100644 --- a/code/modules/antagonists/malf/malf.dm +++ b/code/modules/antagonists/malf/malf.dm @@ -1,7 +1,7 @@ /datum/antagonist/traitor/malf //inheriting traitor antag datum since traitor AIs use it. malf = TRUE roundend_category = "malfunctioning AIs" - name = "Malf" + name = "Malfunctioning AI" show_to_ghosts = TRUE /datum/antagonist/traitor/malf/forge_ai_objectives() @@ -14,3 +14,13 @@ /datum/antagonist/traitor/malf/can_be_owned(datum/mind/new_owner) return istype(new_owner.current, /mob/living/silicon/ai) + +/datum/antagonist/traitor/malf/get_preview_icon() + var/icon/malf_ai_icon = icon('icons/mob/ai.dmi', "ai-red") + + // Crop out the borders of the AI, just the face + malf_ai_icon.Crop(5, 27, 28, 6) + + malf_ai_icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + + return malf_ai_icon diff --git a/code/modules/antagonists/monkey/monkey.dm b/code/modules/antagonists/monkey/monkey.dm index 91a8bf5fe89d..19c134750402 100644 --- a/code/modules/antagonists/monkey/monkey.dm +++ b/code/modules/antagonists/monkey/monkey.dm @@ -18,6 +18,16 @@ /datum/antagonist/monkey/get_team() return monkey_team +/datum/antagonist/monkey/get_preview_icon() + // Creating a *real* monkey is fairly involved before atoms init. + var/icon/icon = icon('icons/mob/monkey.dmi', "monkey1") + + icon.Crop(4, 9, 28, 33) + icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + icon.Shift(SOUTH, 10) + + return icon + /datum/antagonist/monkey/on_gain() . = ..() SSticker.mode.ape_infectees += owner diff --git a/code/modules/antagonists/monsterhunter/monsterhunter.dm b/code/modules/antagonists/monsterhunter/monsterhunter.dm index 8cd0a157236b..e73529574a35 100644 --- a/code/modules/antagonists/monsterhunter/monsterhunter.dm +++ b/code/modules/antagonists/monsterhunter/monsterhunter.dm @@ -8,6 +8,7 @@ roundend_category = "Monster Hunters" antagpanel_category = "Monster Hunter" job_rank = ROLE_MONSTERHUNTER + preview_outfit = /datum/outfit/monsterhunter var/list/datum/action/powers = list() var/datum/martial_art/hunterfu/my_kungfu = new var/give_objectives = TRUE @@ -154,4 +155,13 @@ /datum/status_effect/agent_pinpointer/hunter_edition/Destroy() if(scan_target) to_chat(owner, span_notice("You've lost the trail.")) - . = ..() \ No newline at end of file + . = ..() + +/datum/outfit/monsterhunter + name = "Monster Hunter" + + head = /obj/item/clothing/head/helmet/chaplain/witchunter_hat + uniform = /obj/item/clothing/under/rank/chaplain + suit = /obj/item/clothing/suit/armor/riot/chaplain/witchhunter + l_hand = /obj/item/stake + r_hand = /obj/item/stake/hardened/silver diff --git a/code/modules/antagonists/nightmare/nightmare.dm b/code/modules/antagonists/nightmare/nightmare.dm index dc950850c189..99ea5c6d97d9 100644 --- a/code/modules/antagonists/nightmare/nightmare.dm +++ b/code/modules/antagonists/nightmare/nightmare.dm @@ -3,6 +3,7 @@ show_in_antagpanel = FALSE show_name_in_check_antagonists = TRUE show_to_ghosts = TRUE + job_rank = ROLE_NIGHTMARE /datum/antagonist/nightmare/proc/forge_objectives() var/datum/objective/new_objective = new @@ -21,3 +22,11 @@ /datum/antagonist/nightmare/greet() owner.announce_objectives() SEND_SOUND(owner.current, sound('sound/magic/ethereal_exit.ogg')) + +/datum/antagonist/nightmare/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/nightmaredummy = new + nightmaredummy.set_species(/datum/species/shadow/nightmare) + var/icon/nightmare_icon = render_preview_outfit(null, nightmaredummy) + qdel(nightmaredummy) + + return finish_preview_icon(nightmare_icon) diff --git a/code/modules/antagonists/ninja/ninja.dm b/code/modules/antagonists/ninja/ninja.dm index 460a396e0f39..31b35cd0c7df 100644 --- a/code/modules/antagonists/ninja/ninja.dm +++ b/code/modules/antagonists/ninja/ninja.dm @@ -10,6 +10,7 @@ GLOBAL_LIST_EMPTY(ninja_capture) var/helping_station = FALSE var/give_objectives = TRUE var/give_equipment = TRUE + preview_outfit = /datum/outfit/ninja /datum/antagonist/ninja/New() if(helping_station) diff --git a/code/modules/antagonists/nukeop/clownop.dm b/code/modules/antagonists/nukeop/clownop.dm index fe2addd8dfaa..79b2593a243a 100644 --- a/code/modules/antagonists/nukeop/clownop.dm +++ b/code/modules/antagonists/nukeop/clownop.dm @@ -3,7 +3,9 @@ name = "Clown Operative" roundend_category = "clown operatives" antagpanel_category = "ClownOp" + job_rank = ROLE_CLOWNOP nukeop_outfit = /datum/outfit/syndicate/clownop + preview_outfit = /datum/outfit/syndicate/clownop /datum/antagonist/nukeop/clownop/greet() owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/hornin.ogg', 100, FALSE, pressure_affected = FALSE) diff --git a/code/modules/antagonists/nukeop/nukeop.dm b/code/modules/antagonists/nukeop/nukeop.dm index 6eb58d1f04cf..2230cf6a5c50 100644 --- a/code/modules/antagonists/nukeop/nukeop.dm +++ b/code/modules/antagonists/nukeop/nukeop.dm @@ -11,6 +11,11 @@ var/nukeop_outfit = /datum/outfit/syndicate can_hijack = HIJACK_HIJACKER //Alternative way to wipe out the station. + preview_outfit = /datum/outfit/nuclear_operative_elite + + /// In the preview icon, the nukies who are behind the leader + var/preview_outfit_behind = /datum/outfit/nuclear_operative + /datum/antagonist/nukeop/proc/update_synd_icons_added(mob/living/M) var/datum/atom_hud/antag/opshud = GLOB.huds[ANTAG_HUD_OPS] opshud.join_hud(M) @@ -157,11 +162,48 @@ else to_chat(admin, span_danger("No valid nuke found!")) +/datum/antagonist/nukeop/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/captain = new + var/icon/final_icon = render_preview_outfit(preview_outfit, captain) + final_icon.Blend(make_assistant_icon(), ICON_UNDERLAY, -8, 0) + final_icon.Blend(make_assistant_icon(), ICON_UNDERLAY, 8, 0) + + return finish_preview_icon(final_icon) + +/datum/antagonist/nukeop/proc/make_assistant_icon() + var/mob/living/carbon/human/dummy/assistant = new + var/icon/assistant_icon = render_preview_outfit(preview_outfit_behind, assistant) + assistant_icon.ChangeOpacity(0.5) + + return assistant_icon + +/datum/outfit/nuclear_operative + name = "Nuclear Operative (Preview only)" + mask = /obj/item/clothing/mask/gas/syndicate + uniform = /obj/item/clothing/under/syndicate + suit = /obj/item/clothing/suit/space/hardsuit/syndi + head = /obj/item/clothing/head/helmet/space/hardsuit/syndi + +/datum/outfit/nuclear_operative_elite + name = "Nuclear Operative (Elite, Preview only)" + mask = /obj/item/clothing/mask/gas/syndicate + uniform = /obj/item/clothing/under/syndicate + suit = /obj/item/clothing/suit/space/hardsuit/syndi/elite + head = /obj/item/clothing/head/helmet/space/hardsuit/syndi/elite + r_hand = /obj/item/shield/energy + +/datum/outfit/nuclear_operative_elite/post_equip(mob/living/carbon/human/H, visualsOnly) + var/obj/item/shield/energy/shield = locate() in H.held_items + shield.icon_state = "[shield.base_icon_state]1" + H.update_inv_hands() + /datum/antagonist/nukeop/leader name = "Nuclear Operative Leader" nukeop_outfit = /datum/outfit/syndicate/leader always_new_team = TRUE var/title + preview_outfit = /datum/outfit/nuclear_operative + preview_outfit_behind = null /datum/antagonist/nukeop/leader/memorize_code() ..() diff --git a/code/modules/antagonists/pirate/pirate.dm b/code/modules/antagonists/pirate/pirate.dm index 5e8e3d8d7b5c..2aeedf60d6fa 100644 --- a/code/modules/antagonists/pirate/pirate.dm +++ b/code/modules/antagonists/pirate/pirate.dm @@ -1,11 +1,14 @@ /datum/antagonist/pirate name = "Space Pirate" - job_rank = ROLE_TRAITOR + job_rank = ROLE_PIRATE roundend_category = "space pirates" antagpanel_category = "Pirate" show_to_ghosts = TRUE var/datum/team/pirate/crew + /// In the preview icon, the nukies who are behind the leader + var/preview_outfit_behind = /datum/outfit/pirate/space/gunner + /datum/antagonist/pirate/greet() to_chat(owner, span_boldannounce("You are a Space Pirate!")) to_chat(owner, "The station refused to pay for your protection, protect the ship, siphon the credits from the station and raid it for even more loot.[span_notice(" As a pirate, you are NOT authorized to murder the station's inhabitants without good reason.")]") @@ -108,3 +111,70 @@ parts += "The pirate crew has failed." return "
[parts.Join("
")]
" + +/datum/antagonist/pirate/get_preview_icon() + if (!preview_outfit) + return null + + var/icon/final_icon = render_preview_outfit(preview_outfit) + + if (!isnull(preview_outfit_behind)) + var/icon/teammate = render_preview_outfit(preview_outfit_behind) + teammate.Blend(rgb(128, 128, 128, 128), ICON_MULTIPLY) + + final_icon.Blend(teammate, ICON_OVERLAY, -world.icon_size / 4, 0) + final_icon.Blend(teammate, ICON_OVERLAY, world.icon_size / 4, 0) + + return finish_preview_icon(final_icon) + +/* +/datum/antagonist/pirate/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/zombiedummy = new + + zombiedummy.set_species(/datum/species/zombie) + + var/icon/zombie_icon = render_preview_outfit(null, zombiedummy) + + qdel(zombiedummy) + + return finish_preview_icon(zombie_icon) + + +/datum/antagonist/pirate/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/captain = new + var/mob/living/carbon/human/dummy/consistent/gunner = new + + captain.set_species(/datum/species/skeleton) + gunner.set_species(/datum/species/skeleton) + + var/icon/final_icon = render_preview_outfit(/datum/outfit/pirate/space/captain, captain) + var/icon/teammate = render_preview_outfit(/datum/outfit/pirate/space/gunner, gunner) + //teammate.Blend(rgb(128, 128, 128, 128), ICON_MULTIPLY) + + final_icon.Blend(teammate, ICON_OVERLAY, -world.icon_size / 4, 0) + final_icon.Blend(teammate, ICON_OVERLAY, world.icon_size / 4, 0) + + qdel(teammate) + qdel(captain) + + return finish_preview_icon(final_icon) + +*/ +/datum/antagonist/pirate/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/captain = new + captain.set_species(/datum/species/skeleton) + + var/icon/final_icon = render_preview_outfit(/datum/outfit/pirate/space/captain, captain) + final_icon.Blend(make_assistant_icon(), ICON_UNDERLAY, -8, 0) + final_icon.Blend(make_assistant_icon(), ICON_UNDERLAY, 8, 0) + + return finish_preview_icon(final_icon) + +/datum/antagonist/pirate/proc/make_assistant_icon() + var/mob/living/carbon/human/dummy/assistant = new + assistant.set_species(/datum/species/skeleton) + var/icon/assistant_icon = render_preview_outfit(/datum/outfit/pirate/space/gunner, assistant) + assistant_icon.ChangeOpacity(0.5) + + qdel(assistant) + return assistant_icon diff --git a/code/modules/antagonists/revenant/revenant_antag.dm b/code/modules/antagonists/revenant/revenant_antag.dm index 6eacc1697bec..d0e62e385a4c 100644 --- a/code/modules/antagonists/revenant/revenant_antag.dm +++ b/code/modules/antagonists/revenant/revenant_antag.dm @@ -19,3 +19,6 @@ /datum/antagonist/revenant/on_gain() forge_objectives() . = ..() + +/datum/antagonist/revenant/get_preview_icon() + return finish_preview_icon(icon('icons/mob/mob.dmi', "revenant_idle")) diff --git a/code/modules/antagonists/revolution/revolution.dm b/code/modules/antagonists/revolution/revolution.dm index 6c3702f67d39..3df49df63945 100644 --- a/code/modules/antagonists/revolution/revolution.dm +++ b/code/modules/antagonists/revolution/revolution.dm @@ -163,6 +163,7 @@ var/remove_clumsy = FALSE var/give_flash = TRUE var/give_hud = TRUE + preview_outfit = /datum/outfit/revolutionary /datum/antagonist/rev/head/antag_listing_name() return ..() + "(Leader)" @@ -172,6 +173,37 @@ equip_head() return ..() +/datum/antagonist/rev/head/get_preview_icon() + var/icon/final_icon = render_preview_outfit(preview_outfit) + + final_icon.Blend(make_assistant_icon("Business Hair"), ICON_UNDERLAY, -8, 0) + final_icon.Blend(make_assistant_icon("CIA"), ICON_UNDERLAY, 8, 0) + + // Apply the rev head HUD, but scale up the preview icon a bit beforehand. + // Otherwise, the R gets cut off. + final_icon.Scale(64, 64) + + var/icon/rev_head_icon = icon('icons/mob/hud.dmi', "rev_head") + rev_head_icon.Scale(48, 48) + rev_head_icon.Crop(1, 1, 64, 64) + rev_head_icon.Shift(EAST, 10) + rev_head_icon.Shift(NORTH, 16) + final_icon.Blend(rev_head_icon, ICON_OVERLAY) + + return finish_preview_icon(final_icon) + +/datum/antagonist/rev/head/proc/make_assistant_icon(hairstyle) + var/mob/living/carbon/human/dummy/consistent/assistant = new + assistant.hair_style = hairstyle + assistant.update_hair() + + var/icon/assistant_icon = render_preview_outfit(/datum/outfit/job/assistant/consistent, assistant) + assistant_icon.ChangeOpacity(0.5) + + qdel(assistant) + + return assistant_icon + /datum/antagonist/rev/head/proc/equip_head() var/obj/item/book/granter/crafting_recipe/weapons/W = new W.on_reading_finished(owner.current) @@ -439,3 +471,12 @@ /datum/team/revolution/is_gamemode_hero() return SSticker.mode.name == "revolution" + +/datum/outfit/revolutionary + name = "Revolutionary (Preview only)" + + uniform = /obj/item/clothing/under/yogs/soviet_dress_uniform + head = /obj/item/clothing/head/ushanka + gloves = /obj/item/clothing/gloves/color/black + l_hand = /obj/item/twohanded/spear + r_hand = /obj/item/assembly/flash diff --git a/code/modules/antagonists/sentient_creature.dm b/code/modules/antagonists/sentient_creature.dm new file mode 100644 index 000000000000..540533dfdea5 --- /dev/null +++ b/code/modules/antagonists/sentient_creature.dm @@ -0,0 +1,18 @@ +/datum/antagonist/sentient_creature + name = "\improper Sentient Creature" + show_in_antagpanel = FALSE + show_in_roundend = FALSE + +/datum/antagonist/sentient_creature/get_preview_icon() + var/icon/final_icon = icon('icons/mob/pets.dmi', "corgi") + + var/icon/broodmother = icon('icons/mob/lavaland/lavaland_elites.dmi', "broodmother") + broodmother.Blend(rgb(128, 128, 128, 128), ICON_MULTIPLY) + final_icon.Blend(broodmother, ICON_UNDERLAY, -world.icon_size / 4, 0) + + var/icon/rat = icon('icons/mob/animal.dmi', "regalrat") + rat.Blend(rgb(128, 128, 128, 128), ICON_MULTIPLY) + final_icon.Blend(rat, ICON_UNDERLAY, world.icon_size / 4, 0) + + final_icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + return final_icon diff --git a/code/modules/antagonists/space_dragon/space_dragon.dm b/code/modules/antagonists/space_dragon/space_dragon.dm index 502067004df8..0dfce5265a93 100644 --- a/code/modules/antagonists/space_dragon/space_dragon.dm +++ b/code/modules/antagonists/space_dragon/space_dragon.dm @@ -25,6 +25,19 @@ forge_objectives() . = ..() +/datum/antagonist/space_dragon/get_preview_icon() + var/icon/icon = icon('icons/mob/spacedragon.dmi', "spacedragon") + + icon.Blend(COLOR_STRONG_VIOLET, ICON_MULTIPLY) + icon.Blend(icon('icons/mob/spacedragon.dmi', "overlay_base"), ICON_OVERLAY) + + icon.Crop(10, 9, 54, 53) + icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE) + icon.Shift(EAST, 8) + icon.Shift(SOUTH, 6) + + return icon + /datum/objective/summon_carp var/datum/antagonist/space_dragon/dragon explanation_text = "Summon and protect the rifts to flood the station with carp." @@ -48,4 +61,4 @@ parts += "The [name] has failed!" parts += "The [name] was assisted by:" parts += printplayerlist(carp) - return "
[parts.Join("
")]
" \ No newline at end of file + return "
[parts.Join("
")]
" diff --git a/code/modules/antagonists/swarmer/swarmer.dm b/code/modules/antagonists/swarmer/swarmer.dm index caca1db27c7a..14beba879fa4 100644 --- a/code/modules/antagonists/swarmer/swarmer.dm +++ b/code/modules/antagonists/swarmer/swarmer.dm @@ -37,3 +37,8 @@ ..() if(!mind.has_antag_datum(/datum/antagonist/swarmer)) mind.add_antag_datum(/datum/antagonist/swarmer) + +/datum/antagonist/swarmer/get_preview_icon() + var/icon/swarmer_icon = icon('icons/mob/swarmer.dmi', "swarmer") + swarmer_icon.Shift(NORTH, 8) + return finish_preview_icon(swarmer_icon) diff --git a/code/modules/antagonists/traitor/IAA/internal_affairs.dm b/code/modules/antagonists/traitor/IAA/internal_affairs.dm index e9f3c6ba5108..7cb75a042677 100644 --- a/code/modules/antagonists/traitor/IAA/internal_affairs.dm +++ b/code/modules/antagonists/traitor/IAA/internal_affairs.dm @@ -13,6 +13,7 @@ var/last_man_standing = FALSE var/list/datum/mind/targets_stolen greentext_achieve = /datum/achievement/greentext/internal + preview_outfit = /datum/outfit/assassin /datum/antagonist/traitor/internal_affairs/proc/give_pinpointer() if(owner && owner.current) diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index fa5532a83750..3719b40c6ddb 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -7,6 +7,7 @@ antagpanel_category = "Traitor" job_rank = ROLE_TRAITOR antag_moodlet = /datum/mood_event/focused + preview_outfit = /datum/outfit/traitor var/special_role = ROLE_TRAITOR var/employer = "The Syndicate" var/give_objectives = TRUE @@ -452,6 +453,19 @@ return message - /datum/antagonist/traitor/is_gamemode_hero() return SSticker.mode.name == "traitor" + +/datum/outfit/traitor + name = "Traitor (Preview only)" + uniform = /obj/item/clothing/under/color/grey + suit = /obj/item/clothing/suit/armor/laserproof + gloves = /obj/item/clothing/gloves/color/yellow + mask = /obj/item/clothing/mask/gas + l_hand = /obj/item/melee/transforming/energy/sword + r_hand = /obj/item/gun/energy/kinetic_accelerator/crossbow + head = /obj/item/clothing/head/helmet + +/datum/outfit/traitor/post_equip(mob/living/carbon/human/H, visualsOnly) + var/obj/item/melee/transforming/energy/sword/sword = locate() in H.held_items + sword.transform_weapon(H) diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm index 0a347c49397a..1ec39021677c 100644 --- a/code/modules/antagonists/wizard/wizard.dm +++ b/code/modules/antagonists/wizard/wizard.dm @@ -14,6 +14,7 @@ var/wiz_age = WIZARD_AGE_MIN /* Wizards by nature cannot be too young. */ can_hijack = HIJACK_HIJACKER show_to_ghosts = TRUE + preview_outfit = /datum/outfit/wizard /datum/antagonist/wizard/on_gain() register() diff --git a/code/modules/antagonists/xeno/xeno.dm b/code/modules/antagonists/xeno/xeno.dm index a28f3751683b..d41713dbb4f1 100644 --- a/code/modules/antagonists/xeno/xeno.dm +++ b/code/modules/antagonists/xeno/xeno.dm @@ -36,4 +36,7 @@ /mob/living/carbon/alien/mind_initialize() ..() if(!mind.has_antag_datum(/datum/antagonist/xeno)) - mind.add_antag_datum(/datum/antagonist/xeno) \ No newline at end of file + mind.add_antag_datum(/datum/antagonist/xeno) + +/datum/antagonist/xeno/get_preview_icon() + return finish_preview_icon(icon('icons/mob/alien.dmi', "alienh")) diff --git a/code/modules/antagonists/zombie/zombie.dm b/code/modules/antagonists/zombie/zombie.dm index 4658872e6b38..beba12770422 100644 --- a/code/modules/antagonists/zombie/zombie.dm +++ b/code/modules/antagonists/zombie/zombie.dm @@ -430,3 +430,14 @@ ready = FALSE #undef TIER_2_TIME + +/datum/antagonist/zombie/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/zombiedummy = new + + zombiedummy.set_species(/datum/species/zombie) + + var/icon/zombie_icon = render_preview_outfit(null, zombiedummy) + + qdel(zombiedummy) + + return finish_preview_icon(zombie_icon) diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm index 221febbe14d4..b9b8f872ca32 100644 --- a/code/modules/asset_cache/asset_list.dm +++ b/code/modules/asset_cache/asset_list.dm @@ -1,3 +1,4 @@ +#define ASSET_CROSS_ROUND_CACHE_DIRECTORY "tmp/assets" //These datums are used to populate the asset cache, the proc "register()" does this. //Place any asset datums you create in asset_list_items.dm @@ -6,25 +7,60 @@ GLOBAL_LIST_EMPTY(asset_datums) //get an assetdatum or make a new one -/proc/get_asset_datum(type) +//does NOT ensure it's filled, if you want that use get_asset_datum() +/proc/load_asset_datum(type) return GLOB.asset_datums[type] || new type() +/proc/get_asset_datum(type) + var/datum/asset/loaded_asset = GLOB.asset_datums[type] || new type() + return loaded_asset.ensure_ready() + /datum/asset var/_abstract = /datum/asset + var/cached_serialized_url_mappings + var/cached_serialized_url_mappings_transport_type + + /// Whether or not this asset should be loaded in the "early assets" SS + var/early = FALSE + + /// Whether or not this asset can be cached across rounds of the same commit under the `CACHE_ASSETS` config. + /// This is not a *guarantee* the asset will be cached. Not all asset subtypes respect this field, and the + /// config can, of course, be disabled. + var/cross_round_cachable = FALSE /datum/asset/New() GLOB.asset_datums[type] = src register() +/// Stub that allows us to react to something trying to get us +/// Not useful here, more handy for sprite sheets +/datum/asset/proc/ensure_ready() + return src + +/// Stub to hook into if your asset is having its generation queued by SSasset_loading +/datum/asset/proc/queued_generation() + CRASH("[type] inserted into SSasset_loading despite not implementing /proc/queued_generation") + /datum/asset/proc/get_url_mappings() return list() +/// Returns a cached tgui message of URL mappings +/datum/asset/proc/get_serialized_url_mappings() + if (isnull(cached_serialized_url_mappings) || cached_serialized_url_mappings_transport_type != SSassets.transport.type) + cached_serialized_url_mappings = TGUI_CREATE_MESSAGE("asset/mappings", get_url_mappings()) + cached_serialized_url_mappings_transport_type = SSassets.transport.type + + return cached_serialized_url_mappings + /datum/asset/proc/register() return /datum/asset/proc/send(client) return +/// Returns whether or not the asset should attempt to read from cache +/datum/asset/proc/should_refresh() + return !cross_round_cachable || !CONFIG_GET(flag/cache_assets) /// If you don't need anything complicated. /datum/asset/simple @@ -67,7 +103,7 @@ GLOBAL_LIST_EMPTY(asset_datums) /datum/asset/group/register() for(var/type in children) - get_asset_datum(type) + load_asset_datum(type) /datum/asset/group/send(client/C) for(var/type in children) @@ -92,12 +128,68 @@ GLOBAL_LIST_EMPTY(asset_datums) /datum/asset/spritesheet _abstract = /datum/asset/spritesheet var/name + /// List of arguments to pass into queuedInsert + /// Exists so we can queue icon insertion, mostly for stuff like preferences + var/list/to_generate = list() var/list/sizes = list() // "32x32" -> list(10, icon/normal, icon/stripped) var/list/sprites = list() // "foo_bar" -> list("32x32", 5) + var/list/cached_spritesheets_needed + var/generating_cache = FALSE + var/fully_generated = FALSE + /// If this asset should be fully loaded on new + /// Defaults to false so we can process this stuff nicely + var/load_immediately = FALSE + +/datum/asset/spritesheet/proc/should_load_immediately() +#ifdef DO_NOT_DEFER_ASSETS + return TRUE +#else + return load_immediately +#endif + +/datum/asset/spritesheet/should_refresh() + if (..()) + return TRUE + + // Static so that the result is the same, even when the files are created, for this run + var/static/should_refresh = null + + if (isnull(should_refresh)) + // `fexists` seems to always fail on static-time + should_refresh = !fexists("[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css") + + return should_refresh /datum/asset/spritesheet/register() + SHOULD_NOT_OVERRIDE(TRUE) + if (!name) CRASH("spritesheet [type] cannot register without a name") + + if (!should_refresh() && read_from_cache()) + fully_generated = TRUE + return + + // If it's cached, may as well load it now, while the loading is cheap + if(CONFIG_GET(flag/cache_assets) && cross_round_cachable) + load_immediately = TRUE + + create_spritesheets() + if(should_load_immediately()) + realize_spritesheets(yield = FALSE) + else + SSasset_loading.queue_asset(src) + +/datum/asset/spritesheet/proc/realize_spritesheets(yield) + if(fully_generated) + return + while(length(to_generate)) + var/list/stored_args = to_generate[to_generate.len] + to_generate.len-- + queuedInsert(arglist(stored_args)) + if(yield && TICK_CHECK) + return + ensure_stripped() for(var/size_id in sizes) var/size = sizes[size_id] @@ -109,17 +201,39 @@ GLOBAL_LIST_EMPTY(asset_datums) SSassets.transport.register_asset(res_name, fcopy_rsc(fname)) fdel(fname) -/datum/asset/spritesheet/send(client/C) + if (CONFIG_GET(flag/cache_assets) && cross_round_cachable) + write_to_cache() + fully_generated = TRUE + // If we were ever in there, remove ourselves + SSasset_loading.dequeue_asset(src) + +/datum/asset/spritesheet/queued_generation() + realize_spritesheets(yield = TRUE) + +/datum/asset/spritesheet/ensure_ready() + if(!fully_generated) + realize_spritesheets(yield = FALSE) + return ..() + +/datum/asset/spritesheet/send(client/client) if (!name) return + + if (!should_refresh()) + return send_from_cache(client) + var/all = list("spritesheet_[name].css") for(var/size_id in sizes) all += "[name]_[size_id].png" - . = SSassets.transport.send_assets(C, all) + . = SSassets.transport.send_assets(client, all) /datum/asset/spritesheet/get_url_mappings() if (!name) return + + if (!should_refresh()) + return get_cached_url_mappings() + . = list("spritesheet_[name].css" = SSassets.transport.get_asset_url("spritesheet_[name].css")) for(var/size_id in sizes) .["[name]_[size_id].png"] = SSassets.transport.get_asset_url("[name]_[size_id].png") @@ -147,7 +261,7 @@ GLOBAL_LIST_EMPTY(asset_datums) for (var/size_id in sizes) var/size = sizes[size_id] var/icon/tiny = size[SPRSZ_ICON] - out += ".[name][size_id]{display:inline-block;width:[tiny.Width()]px;height:[tiny.Height()]px;background:url('[SSassets.transport.get_asset_url("[name]_[size_id].png")]') no-repeat;}" + out += ".[name][size_id]{display:inline-block;width:[tiny.Width()]px;height:[tiny.Height()]px;background:url('[get_background_url("[name]_[size_id].png")]') no-repeat;}" for (var/sprite_id in sprites) var/sprite = sprites[sprite_id] @@ -165,7 +279,75 @@ GLOBAL_LIST_EMPTY(asset_datums) return out.Join("\n") +/datum/asset/spritesheet/proc/read_from_cache() + var/replaced_css = file2text("[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css") + + var/regex/find_background_urls = regex(@"background:url\('%(.+?)%'\)", "g") + while (find_background_urls.Find(replaced_css)) + var/asset_id = find_background_urls.group[1] + var/asset_cache_item = SSassets.transport.register_asset(asset_id, "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[asset_id]") + var/asset_url = SSassets.transport.get_asset_url(asset_cache_item = asset_cache_item) + replaced_css = replacetext(replaced_css, find_background_urls.match, "background:url('[asset_url]')") + LAZYADD(cached_spritesheets_needed, asset_id) + + var/replaced_css_filename = "data/spritesheets/spritesheet_[name].css" + rustg_file_write(replaced_css, replaced_css_filename) + SSassets.transport.register_asset("spritesheet_[name].css", replaced_css_filename) + + fdel(replaced_css_filename) + + return TRUE + +/datum/asset/spritesheet/proc/send_from_cache(client/client) + if (isnull(cached_spritesheets_needed)) + stack_trace("cached_spritesheets_needed was null when sending assets from [type] from cache") + cached_spritesheets_needed = list() + + return SSassets.transport.send_assets(client, cached_spritesheets_needed + "spritesheet_[name].css") + +/// Returns the URL to put in the background:url of the CSS asset +/datum/asset/spritesheet/proc/get_background_url(asset) + if (generating_cache) + return "%[asset]%" + else + return SSassets.transport.get_asset_url(asset) + +/datum/asset/spritesheet/proc/write_to_cache() + for (var/size_id in sizes) + fcopy(SSassets.cache["[name]_[size_id].png"].resource, "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name]_[size_id].png") + + generating_cache = TRUE + var/mock_css = generate_css() + generating_cache = FALSE + + rustg_file_write(mock_css, "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css") + +/datum/asset/spritesheet/proc/get_cached_url_mappings() + var/list/mappings = list() + mappings["spritesheet_[name].css"] = SSassets.transport.get_asset_url("spritesheet_[name].css") + + for (var/asset_name in cached_spritesheets_needed) + mappings[asset_name] = SSassets.transport.get_asset_url(asset_name) + + return mappings + +/// Override this in order to start the creation of the spritehseet. +/// This is where all your Insert, InsertAll, etc calls should be inside. +/datum/asset/spritesheet/proc/create_spritesheets() + SHOULD_CALL_PARENT(FALSE) + CRASH("create_spritesheets() not implemented for [type]!") + /datum/asset/spritesheet/proc/Insert(sprite_name, icon/I, icon_state="", dir=SOUTH, frame=1, moving=FALSE) + if(should_load_immediately()) + queuedInsert(sprite_name, I, icon_state, dir, frame, moving) + else + to_generate += list(args.Copy()) + +// LEMON NOTE +// A GOON CODER SAYS BAD ICON ERRORS CAN BE THROWN BY THE "ICON CACHE" +// APPARENTLY IT MAKES ICONS IMMUTABLE +// LOOK INTO USING THE MUTABLE APPEARANCE PATTERN HERE +/datum/asset/spritesheet/proc/queuedInsert(sprite_name, icon/I, icon_state="", dir=SOUTH, frame=1, moving=FALSE) I = icon(I, icon_state=icon_state, dir=dir, frame=frame, moving=moving) if (!I || !length(icon_states(I))) // that direction or state doesn't exist return @@ -178,8 +360,10 @@ GLOBAL_LIST_EMPTY(asset_datums) if (size) var/position = size[SPRSZ_COUNT]++ var/icon/sheet = size[SPRSZ_ICON] + var/icon/sheet_copy = icon(sheet) size[SPRSZ_STRIPPED] = null - sheet.Insert(I, icon_state=sprite_name) + sheet_copy.Insert(I, icon_state=sprite_name) + size[SPRSZ_ICON] = sheet_copy sprites[sprite_name] = list(size_id, position) else sizes[size_id] = size = list(1, I, null) @@ -228,10 +412,9 @@ GLOBAL_LIST_EMPTY(asset_datums) _abstract = /datum/asset/spritesheet/simple var/list/assets -/datum/asset/spritesheet/simple/register() +/datum/asset/spritesheet/simple/create_spritesheets() for (var/key in assets) Insert(key, assets[key]) - ..() //Generates assets based on iconstates of a single icon /datum/asset/simple/icon_states @@ -315,3 +498,31 @@ GLOBAL_LIST_EMPTY(asset_datums) /datum/asset/simple/namespaced/proc/get_htmlloader(filename) return url2htmlloader(SSassets.transport.get_asset_url(filename, assets[filename])) + +/// A subtype to generate a JSON file from a list +/datum/asset/json + _abstract = /datum/asset/json + /// The filename, will be suffixed with ".json" + var/name + +/datum/asset/json/send(client) + return SSassets.transport.send_assets(client, "data/[name].json") + +/datum/asset/json/get_url_mappings() + return list( + "[name].json" = SSassets.transport.get_asset_url("data/[name].json"), + ) + +/datum/asset/json/register() + var/filename = "data/[name].json" + fdel(filename) + text2file(json_encode(generate()), filename) + SSassets.transport.register_asset(filename, fcopy_rsc(filename)) + fdel(filename) + +/// Returns the data that will be JSON encoded +/datum/asset/json/proc/generate() + SHOULD_CALL_PARENT(FALSE) + CRASH("generate() not implemented for [type]!") + +#undef ASSET_CROSS_ROUND_CACHE_DIRECTORY diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index c2c219e895f0..0d41cd60127f 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -161,7 +161,7 @@ "fa-regular-400.ttf" = 'html/font-awesome/webfonts/fa-regular-400.ttf', "fa-solid-900.ttf" = 'html/font-awesome/webfonts/fa-solid-900.ttf', "fa-v4compatibility.ttf" = 'html/font-awesome/webfonts/fa-v4compatibility.ttf', - "v4shim.css" = 'html/font-awesome/css/v4-shims.min.css' + "v4shim.css" = 'html/font-awesome/css/v4-shims.min.css', ) parents = list("font-awesome.css" = 'html/font-awesome/css/all.min.css') @@ -177,7 +177,7 @@ /datum/asset/spritesheet/chat name = "chat" -/datum/asset/spritesheet/chat/register() +/datum/asset/spritesheet/chat/create_spritesheets() InsertAll("emoji", 'icons/emoji.dmi') // pre-loading all lanugage icons also helps to avoid meta InsertAll("language", 'icons/misc/language.dmi') @@ -188,7 +188,6 @@ if (icon != 'icons/misc/language.dmi') var/icon_state = initial(L.icon_state) Insert("language-[icon_state]", icon, icon_state=icon_state) - ..() /datum/asset/simple/lobby assets = list( @@ -288,10 +287,9 @@ /datum/asset/spritesheet/pipes name = "pipes" -/datum/asset/spritesheet/pipes/register() +/datum/asset/spritesheet/pipes/create_spritesheets() for (var/each in list('icons/obj/atmospherics/pipes/pipe_item.dmi', 'icons/obj/atmospherics/pipes/disposal.dmi', 'icons/obj/atmospherics/pipes/transit_tube.dmi', 'icons/obj/plumbing/fluid_ducts.dmi')) InsertAll("", each, GLOB.alldirs) - ..() /datum/asset/simple/security_armaments assets = list( @@ -303,7 +301,7 @@ /datum/asset/spritesheet/research_designs name = "design" -/datum/asset/spritesheet/research_designs/register() +/datum/asset/spritesheet/research_designs/create_spritesheets() for (var/path in subtypesof(/datum/design)) var/datum/design/D = path @@ -314,9 +312,11 @@ if(initial(D.research_icon) && initial(D.research_icon_state)) //If the design has an icon replacement skip the rest icon_file = initial(D.research_icon) icon_state = initial(D.research_icon_state) + #ifdef UNIT_TESTS if(!(icon_state in icon_states(icon_file))) - warning("design [D] with icon '[icon_file]' missing state '[icon_state]'") + stack_trace("design [D] with icon '[icon_file]' missing state '[icon_state]'") continue + #endif I = icon(icon_file, icon_state, SOUTH) else @@ -338,10 +338,11 @@ icon_file = initial(item.icon) icon_state = initial(item.icon_state) - + #ifdef UNIT_TESTS if(!(icon_state in icon_states(icon_file))) - warning("design [D] with icon '[icon_file]' missing state '[icon_state]'") + stack_trace("design [D] with icon '[icon_file]' missing state '[icon_state]'") continue + #endif I = icon(icon_file, icon_state, SOUTH) // computers (and snowflakes) get their screen and keyboard sprites @@ -356,12 +357,11 @@ I.Blend(icon(icon_file, keyboard, SOUTH), ICON_OVERLAY) Insert(initial(D.id), I) - return ..() /datum/asset/spritesheet/vending name = "vending" -/datum/asset/spritesheet/vending/register() +/datum/asset/spritesheet/vending/create_spritesheets() for (var/k in GLOB.vending_products) var/atom/item = k if (!ispath(item, /atom)) @@ -373,33 +373,35 @@ var/obj/item/ammo_box/ammoitem = item if(initial(ammoitem.multiple_sprites)) icon_state = "[icon_state]-[initial(ammoitem.max_ammo)]" - var/icon/I + #ifdef UNIT_TESTS var/icon_states_list = icon_states(icon_file) - if(icon_state in icon_states_list) - I = icon(icon_file, icon_state, SOUTH) - var/c = initial(item.color) - if (!isnull(c) && c != "#FFFFFF") - I.Blend(c, ICON_MULTIPLY) - else + if (!(icon_state in icon_states_list)) var/icon_states_string for (var/an_icon_state in icon_states_list) if (!icon_states_string) icon_states_string = "[json_encode(an_icon_state)](\ref[an_icon_state])" else icon_states_string += ", [json_encode(an_icon_state)](\ref[an_icon_state])" + stack_trace("[item] does not have a valid icon state, icon=[icon_file], icon_state=[json_encode(icon_state)](\ref[icon_state]), icon_states=[icon_states_string]") - I = icon('icons/turf/floors.dmi', "", SOUTH) + continue + #endif + + var/icon/I = icon(icon_file, icon_state, SOUTH) + var/c = initial(item.color) + if (!isnull(c) && c != "#FFFFFF") + I.Blend(c, ICON_MULTIPLY) var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-") Insert(imgid, I) - return ..() /datum/asset/spritesheet/uplink name = "uplink" + load_immediately = TRUE // needed to prevent duplicates -/datum/asset/spritesheet/uplink/register() +/datum/asset/spritesheet/uplink/create_spritesheets() for(var/path in GLOB.uplink_items) var/datum/uplink_item/U = path if (!ispath(U, /datum/uplink_item)) @@ -415,15 +417,10 @@ var/obj/item/ammo_box/ammoitem = item if(initial(ammoitem.multiple_sprites)) icon_state = "[icon_state]-[initial(ammoitem.max_ammo)]" - var/icon/I + #ifdef UNIT_TESTS var/icon_states_list = icon_states(icon_file) - if(icon_state in icon_states_list) - I = icon(icon_file, icon_state, SOUTH) - var/c = initial(item.color) - if (!isnull(c) && c != "#FFFFFF") - I.Blend(c, ICON_MULTIPLY) - else + if (!(icon_state in icon_states_list)) var/icon_states_string for (var/an_icon_state in icon_states_list) if (!icon_states_string) @@ -431,13 +428,18 @@ else icon_states_string += ", [json_encode(an_icon_state)](\ref[an_icon_state])" stack_trace("[item] does not have a valid icon state, icon=[icon_file], icon_state=[json_encode(icon_state)](\ref[icon_state]), icon_states=[icon_states_string]") - I = icon('icons/turf/floors.dmi', "", SOUTH) + continue + #endif - var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-") + var/icon/I = icon(icon_file, icon_state, SOUTH) + var/c = initial(item.color) + if (!isnull(c) && c != "#FFFFFF") + I.Blend(c, ICON_MULTIPLY) + var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-") + if(!sprites[imgid]) Insert(imgid, I) - return ..() /datum/asset/simple/genetics assets = list( @@ -459,14 +461,13 @@ /datum/asset/spritesheet/sheetmaterials name = "sheetmaterials" -/datum/asset/spritesheet/sheetmaterials/register() +/datum/asset/spritesheet/sheetmaterials/create_spritesheets() InsertAll("", 'icons/obj/stack_objects.dmi') // Special case to handle Bluespace Crystals Insert("polycrystal", 'icons/obj/telescience.dmi', "polycrystal") Insert("dilithium_polycrystal", 'yogstation/icons/obj/telescience.dmi', "dilithium_polycrystal") //yogs: same as above but for dilithium - ..() /datum/asset/simple/portraits @@ -474,10 +475,10 @@ assets = list() /datum/asset/simple/portraits/New() - if(!SSpersistence.paintings || !SSpersistence.paintings[tab] || !length(SSpersistence.paintings[tab])) + if(!length(SSpersistent_paintings.paintings[tab])) return - for(var/p in SSpersistence.paintings[tab]) - var/list/portrait = p + + for(var/list/portrait as anything in SSpersistent_paintings.paintings[tab]) var/png = "data/paintings/[tab]/[portrait["md5"]].png" if(fexists(png)) var/asset_name = "[tab]_[portrait["md5"]]" @@ -490,7 +491,7 @@ /datum/asset/spritesheet/supplypods name = "supplypods" -/datum/asset/spritesheet/supplypods/register() +/datum/asset/spritesheet/supplypods/create_spritesheets() for (var/style in 1 to length(GLOB.podstyles)) var/icon_file = 'icons/obj/supplypods.dmi' if (style == STYLE_SEETHROUGH) @@ -515,4 +516,3 @@ glow = "pod_glow_[glow]" podIcon.Blend(icon(icon_file, glow), ICON_OVERLAY) Insert("pod_asset[style]", podIcon) - return ..() diff --git a/code/modules/asset_cache/transports/asset_transport.dm b/code/modules/asset_cache/transports/asset_transport.dm index f5a1af4f0573..555d96e76a6a 100644 --- a/code/modules/asset_cache/transports/asset_transport.dm +++ b/code/modules/asset_cache/transports/asset_transport.dm @@ -137,7 +137,7 @@ /// Precache files without clogging up the browse() queue, used for passively sending files on connection start. -/datum/asset_transport/proc/send_assets_slow(client/client, list/files, filerate = 3) +/datum/asset_transport/proc/send_assets_slow(client/client, list/files, filerate = 6) var/startingfilerate = filerate for (var/file in files) if (!client) diff --git a/code/modules/awaymissions/capture_the_flag.dm b/code/modules/awaymissions/capture_the_flag.dm index 58dfcfdd398a..3438d269e823 100644 --- a/code/modules/awaymissions/capture_the_flag.dm +++ b/code/modules/awaymissions/capture_the_flag.dm @@ -265,7 +265,7 @@ /obj/machinery/capture_the_flag/proc/spawn_team_member(client/new_team_member) var/mob/living/carbon/human/M = new/mob/living/carbon/human(get_turf(src)) - new_team_member.prefs.copy_to(M) + new_team_member.prefs.apply_prefs_to(M) M.set_species(/datum/species/synth) M.key = new_team_member.key M.faction += team diff --git a/code/modules/awaymissions/corpse.dm b/code/modules/awaymissions/corpse.dm index 769543dcae36..2e8c3a1bee82 100644 --- a/code/modules/awaymissions/corpse.dm +++ b/code/modules/awaymissions/corpse.dm @@ -320,7 +320,7 @@ /obj/effect/mob_spawn/human/doctor name = "Doctor" - outfit = /datum/outfit/job/doctor + outfit = /datum/outfit/job/doctor/dead /obj/effect/mob_spawn/human/doctor/alive diff --git a/code/modules/balloon_alert/balloon_alert.dm b/code/modules/balloon_alert/balloon_alert.dm index 43182617624d..c830fcda189e 100644 --- a/code/modules/balloon_alert/balloon_alert.dm +++ b/code/modules/balloon_alert/balloon_alert.dm @@ -19,7 +19,7 @@ /atom/proc/balloon_or_message(mob/viewer, alert, message) SHOULD_NOT_SLEEP(TRUE) - if(viewer.client.prefs.disable_balloon_alerts) + if(viewer.client.prefs.read_preference(/datum/preference/toggle/disable_balloon_alerts)) INVOKE_ASYNC(.proc/to_chat, viewer, message) else INVOKE_ASYNC(src, .proc/balloon_alert_perform, viewer, message ? message : alert) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 82a6c801326c..bc093d8e6658 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -99,8 +99,6 @@ ///world.timeofday they connected var/connection_timeofday - ///If the client is currently in player preferences - var/inprefs = FALSE ///Used for limiting the rate of topic sends by the client to avoid abuse var/list/topiclimiter ///Used for limiting the rate of clicks sends by the client to avoid abuse @@ -169,3 +167,6 @@ var/next_move_dir_add /// On next move, subtract this dir from the move that would otherwise be done var/next_move_dir_sub + + /// Whether or not this client has standard hotkeys enabled + var/hotkeys = TRUE diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 0161a78393f8..b322ce725494 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -38,7 +38,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(!usr || usr != mob) //stops us calling Topic for somebody else's client. Also helps prevent usr=null return - if(src.prefs && src.prefs.afreeze && !href_list["priv_msg"] && href_list["_src_"] != "chat" && !src.holder) //yogs start - afreeze + if(src.prefs && src.afreeze && !href_list["priv_msg"] && href_list["_src_"] != "chat" && !src.holder) //yogs start - afreeze to_chat(src, span_userdanger("You have been frozen by an administrator.")) return //yogs end @@ -140,13 +140,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( hsrc = mob if("mentor") // YOGS - Mentor stuff hsrc = mentor_datum // YOGS - Mentor stuff - if("prefs") - if (inprefs) - return - inprefs = TRUE - . = prefs.process_link(usr,href_list) - inprefs = FALSE - return if("vars") return view_var_Topic(href,href_list,hsrc) @@ -168,12 +161,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( ..() //redirect to hsrc.Topic() -/client/proc/is_content_unlocked() - if(!is_donator(src)) // yogs - changed this to is_donator so admins get donor perks - to_chat(src, "Become a BYOND member to access member-perks and features, as well as support the engine that makes this game possible. Only 10 bucks for 3 months! Click Here to find out more.") - return 0 - return 1 - /client/proc/handle_spam_prevention(message, mute_type) //Increment message count total_message_count += 1 @@ -246,7 +233,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( // Instantiate tgui panel tgui_panel = new(src) - tgui_panel.send_connected() + //tgui_panel.send_connected() GLOB.ahelp_tickets.ClientLogin(src) var/connecting_admin = GLOB.permissions.load_permissions_for(src) //because de-admined admins connecting should be treated like admins. @@ -267,12 +254,12 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( prefs = GLOB.preferences_datums[ckey] if(prefs) prefs.parent = src + prefs.apply_all_client_preferences() else prefs = new /datum/preferences(src) GLOB.preferences_datums[ckey] = prefs prefs.last_ip = address //these are gonna be used for banning prefs.last_id = computer_id //these are gonna be used for banning - fps = prefs.clientfps if(fexists(roundend_report_file())) add_verb(src, /client/proc/show_previous_roundend_report) @@ -311,20 +298,13 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( player_details.byond_version = full_version GLOB.player_details[ckey] = player_details - // yogs start - Donor stuff - if(ckey in GLOB.donators) - prefs.unlock_content |= 2 - else - prefs.unlock_content &= ~2 // is_donator relies on prefs.unlock_content - - if(is_donator(src)) + if (prefs.unlock_content & DONOR_YOGS) src.add_donator_verbs() else if(prefs.yogtoggles & QUIET_ROUND) prefs.yogtoggles &= ~QUIET_ROUND prefs.save_preferences() - - // yogs end + . = ..() //calls mob.Login() if (byond_version >= 512) @@ -479,11 +459,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if (verbpath.name[1] != "@") new child(src) - for (var/thing in prefs.menuoptions) - var/datum/verbs/menu/menuitem = GLOB.menulist[thing] - if (menuitem) - menuitem.Load_checked(src) - view_size = new(src, getScreenSize(prefs.widescreenpref)) + view_size = new(src, getScreenSize(prefs.read_preference(/datum/preference/toggle/widescreen))) view_size.resetFormat() view_size.setZoomMode() Master.UpdateTickRate() @@ -860,6 +836,10 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( ip_intel = res.intel /client/Click(atom/object, atom/location, control, params) + if(src.afreeze) + to_chat(src, span_userdanger("You have been frozen by an administrator.")) + return + var/ab = FALSE var/list/L = params2list(params) @@ -907,7 +887,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( to_chat(src, span_danger("Your previous click was ignored because you've done too many in a second")) return - if (prefs.hotkeys) + if (hotkeys) // If hotkey mode is enabled, then clicking the map will automatically // unfocus the text bar. This removes the red color from the text bar // so that the visual focus indicator matches reality. @@ -1004,8 +984,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(!D?.key_bindings) return movement_keys = list() - for(var/key in D.key_bindings) - for(var/kb_name in D.key_bindings[key]) + for(var/kb_name in D.key_bindings) + for(var/key in D.key_bindings[kb_name]) switch(kb_name) if("North") movement_keys[key] = NORTH @@ -1045,12 +1025,12 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( QDEL_NULL(mob.hud_used) mob.create_mob_hud() mob.hud_used.show_hud(mob.hud_used.hud_version) - mob.hud_used.update_ui_style(ui_style2icon(prefs.UI_style)) + mob.hud_used.update_ui_style(ui_style2icon(prefs.read_preference(/datum/preference/choiced/ui_style))) if (isliving(mob)) var/mob/living/M = mob M.update_damage_hud() - if (prefs.auto_fit_viewport) + if (prefs.read_preference(/datum/preference/toggle/auto_fit_viewport)) addtimer(CALLBACK(src,.verb/fit_viewport,10)) //Delayed to avoid wingets from Login calls. /client/proc/generate_clickcatcher() @@ -1067,26 +1047,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(prefs && prefs.chat_toggles & CHAT_PULLR) to_chat(src, announcement) -/client/proc/show_character_previews(mutable_appearance/MA) - var/pos = 0 - for(var/D in GLOB.cardinals) - pos++ - var/atom/movable/screen/O = LAZYACCESS(char_render_holders, "[D]") - if(!O) - O = new - LAZYSET(char_render_holders, "[D]", O) - screen |= O - O.appearance = MA - O.dir = D - O.screen_loc = "character_preview_map:0,[pos]" - -/client/proc/clear_character_previews() - for(var/index in char_render_holders) - var/atom/movable/screen/S = char_render_holders[index] - screen -= S - qdel(S) - char_render_holders = null - /// compiles a full list of verbs and sends it to the browser /client/proc/init_verbs() if(IsAdminAdvancedProcCall()) @@ -1114,3 +1074,11 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( return to_chat(src, span_userdanger("Statpanel failed to load, click here to reload the panel ")) tgui_panel.initialize() + +/client/verb/stop_client_sounds() + set name = "Stop Sounds" + set category = "OOC" + set desc = "Stop Current Sounds" + SEND_SOUND(usr, sound(null)) + tgui_panel?.stop_music() + SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Stop Self Sounds")) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 5b7ce54921d8..bf07326dd2b6 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -14,1326 +14,485 @@ GLOBAL_LIST_EMPTY(preferences_datums) //game-preferences var/lastchangelog = "" //Saved changlog filesize to detect if there was a change - var/ooccolor = "#c43b23" - var/asaycolor = null - var/enable_tips = TRUE - var/tip_delay = 500 //tip delay in milliseconds //Antag preferences var/list/be_special = list() //Special role selection - var/tmp/old_be_special = 0 //Bitflag version of be_special, used to update old savefiles and nothing more - //If it's 0, that's good, if it's anything but 0, the owner of this prefs file's antag choices were, - //autocorrected this round, not that you'd need to check that. - - var/UI_style = null - var/buttons_locked = FALSE - var/hotkeys = TRUE - // Custom Keybindings + + /// Custom keybindings. Map of keybind names to keyboard inputs. + /// For example, by default would have "swap_hands" -> list("X") var/list/key_bindings = list() - var/tgui_fancy = TRUE - var/tgui_lock = FALSE - var/windowflashing = TRUE + + /// Cached list of keybindings, mapping keys to actions. + /// For example, by default would have "X" -> list("swap_hands") + var/list/key_bindings_by_key = list() + var/toggles = TOGGLES_DEFAULT var/db_flags var/chat_toggles = TOGGLES_DEFAULT_CHAT var/extra_toggles = TOGGLES_DEFAULT_EXTRA + var/yogtoggles = YOGTOGGLES_DEFAULT var/ghost_form = "ghost" - var/ghost_orbit = GHOST_ORBIT_CIRCLE - var/ghost_accs = GHOST_ACCS_DEFAULT_OPTION - var/ghost_others = GHOST_OTHERS_DEFAULT_OPTION - var/ghost_hud = 1 - var/inquisitive_ghost = 1 - var/allow_midround_antag = 1 - var/preferred_map = null - var/pda_style = MONO - var/pda_color = "#808000" - var/pda_theme = PDA_THEME_TITLE_NTOS - var/id_in_pda = FALSE - var/show_credits = TRUE - var/uses_glasses_colour = 0 - - var/list/player_alt_titles = new() - - ///Whether emotes will be displayed on runechat. Requires chat_on_map to have effect. Boolean. - var/see_rc_emotes = TRUE - - - //character preferences - var/real_name //our character's name - var/be_random_name = 0 //whether we'll have a random name every round - var/be_random_body = 0 //whether we'll have a random body every round - var/gender = MALE //gender of character (well duh) - var/age = 30 //age of character - var/underwear = "Nude" //underwear type - var/undershirt = "Nude" //undershirt type - var/socks = "Nude" //socks type - var/backbag = DBACKPACK //backpack type - var/jumpsuit_style = PREF_SUIT //suit/skirt - var/hair_style = "Bald" //Hair type - var/hair_color = "000" //Hair color - var/facial_hair_style = "Shaved" //Face hair type - var/facial_hair_color = "000" //Facial hair color - var/skin_tone = "caucasian1" //Skin color - var/eye_color = "000" //Eye color - var/datum/species/pref_species = new /datum/species/human() //Mutant race - var/list/features = list("mcolor" = "FFF", "gradientstyle" = "None", "gradientcolor" = "000", "ethcolor" = "9c3030", "pretcolor" = "FFFFFF", "tail_lizard" = "Smooth", "tail_human" = "None", "snout" = "Round", "horns" = "None", "ears" = "None", "wings" = "None", "frills" = "None", "spines" = "None", "body_markings" = "None", "legs" = "Normal Legs", "moth_wings" = "Plain", "tail_polysmorph" = "Polys", "teeth" = "None", "dome" = "None", "dorsal_tubes" = "No", "ethereal_mark" = "None", "pod_hair" = "Cabbage", "pod_flower" = "Cabbage", "ipc_screen" = "Blue", "ipc_antenna" = "None", "ipc_chassis" = "Morpheus Cyberkinetics(Greyscale)","plasmaman_helmet" = "None") - var/list/genders = list(MALE, FEMALE, PLURAL) - var/list/friendlyGenders = list("Male" = "male", "Female" = "female", "Other" = "plural") - - var/list/random_locks = list() - - var/list/custom_names = list() - var/preferred_ai_core_display = "Blue" - var/prefered_security_department = SEC_DEPT_RANDOM - var/prefered_engineering_department = ENG_DEPT_RANDOM + + var/list/player_alt_titles = list() + + var/list/randomise = list() //Quirk list var/list/all_quirks = list() - var/mood_tail_wagging = TRUE - //Job preferences 2.0 - indexed by job title , no key or value implies never var/list/job_preferences = list() - // Want randomjob if preferences already filled - Donkie - var/joblessrole = BERANDOMJOB //defaults to 1 for fewer assistants - - // 0 = character settings, 1 = game preferences - var/current_tab = 0 + /// The current window, PREFERENCE_TAB_* in [`code/__DEFINES/preferences.dm`] + var/current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES var/unlock_content = 0 var/list/ignoring = list() - var/clientfps = 40 + var/list/exp = list() - var/parallax + var/action_buttons_screen_locs = list() - var/ambientocclusion = TRUE - ///Should we automatically fit the viewport? - var/auto_fit_viewport = TRUE - ///Should we be in the widescreen mode set by the config? - var/widescreenpref = TRUE - ///What size should pixels be displayed as? 0 is strech to fit - var/pixel_size = 0 - ///What scaling method should we use? - var/scaling_method = "normal" - var/uplink_spawn_loc = UPLINK_PDA + /// A preview of the current character + var/atom/movable/screen/character_preview_view/character_preview_view - var/skillcape = 1 /// Old skillcape value - var/skillcape_id = "None" /// Typepath of selected skillcape, null for none + /// Icon for the preview background + var/icon/background = "floor" - var/map = 1 - var/flare = 1 + /// A list of instantiated middleware + var/list/datum/preference_middleware/middleware = list() - var/bar_choice = "Random" + /// The savefile relating to core preferences, PREFERENCE_PLAYER + var/savefile/game_savefile - var/list/exp = list() - var/list/menuoptions + /// The savefile relating to character preferences, PREFERENCE_CHARACTER + var/savefile/character_savefile - var/action_buttons_screen_locs = list() + /// A list of keys that have been updated since the last save. + var/list/recently_updated_keys = list() - var/chat_on_map = TRUE - var/max_chat_length = CHAT_MESSAGE_MAX_LENGTH - var/see_chat_non_mob = TRUE - /// If we have persistent scars enabled - var/persistent_scars = TRUE + /// A cache of preference entries to values. + /// Used to avoid expensive READ_FILE every time a preference is retrieved. + var/value_cache = list() - var/disable_alternative_announcers = FALSE - var/icon/background = "floor" - var/list/background_options = list( - "floor" = "Default Tile", - "white" = "Default White Tile", - "darkfull" = "Default Dark Tile", - "wood" = "Wood", - "rockvault" = "Rock Vault", - "grass4" = "Grass", - "black" = "Pure Black", - "grey" = "Pure Grey", - "pure_white" = "Pure White" - ) + /// If set to TRUE, will update character_profiles on the next ui_data tick. + var/tainted_character_profiles = FALSE - var/disable_balloon_alerts = FALSE +/datum/preferences/Destroy(force, ...) + QDEL_NULL(character_preview_view) + QDEL_LIST(middleware) + value_cache = null + return ..() /datum/preferences/New(client/C) parent = C - for(var/custom_name_id in GLOB.preferences_custom_names) - custom_names[custom_name_id] = get_default_name(custom_name_id) + for (var/middleware_type in subtypesof(/datum/preference_middleware)) + middleware += new middleware_type(src) - UI_style = GLOB.available_ui_styles[1] if(istype(C)) if(!IsGuestKey(C.key)) load_path(C.ckey) - unlock_content |= C.IsByondMember() // yogs - Donor features - if(unlock_content) - max_save_slots += 2 - // yogs start - Donor features - if(is_donator(C) || (C.ckey in get_donators())) // the Latter handles race cases where the prefs are not fully loaded in, or GLOB.donators hasn't loaded in yet - max_save_slots += DONOR_CHARACTER_SLOTS - // yogs end + + if (C.IsByondMember()) + unlock_content |= DONOR_BYOND + + // the latter handles race cases where the prefs are not fully loaded in, or GLOB.donators hasn't loaded in yet + if(is_donator(C) || (C.ckey in get_donators())) + unlock_content |= DONOR_YOGS + + // give save slots to donors + if (unlock_content & DONOR_YOGS) + max_save_slots += DONOR_YOGS_SLOTS + DONOR_BYOND_SLOTS + else if (unlock_content & DONOR_BYOND) + max_save_slots += DONOR_BYOND_SLOTS + + // give them default keybinds and update their movement keys + key_bindings = deepCopyList(GLOB.default_hotkeys) + key_bindings_by_key = get_key_bindings_by_key(key_bindings) + randomise = get_default_randomization() + var/loaded_preferences_successfully = load_preferences() if(loaded_preferences_successfully) if(load_character()) return //we couldn't load character data so just randomize the character appearance + name - random_character() //let's create a random character then - rather than a fat, bald and naked man. - key_bindings = deepCopyList(GLOB.hotkey_keybinding_list_by_key) // give them default keybinds and update their movement keys - C?.set_macros() - real_name = pref_species.random_name(gender,1) + randomise_appearance_prefs() //let's create a random character then - rather than a fat, bald and naked man. + if(C) + apply_all_client_preferences() + C.set_macros() + if(!loaded_preferences_successfully) save_preferences() save_character() //let's save this new random character so it doesn't keep generating new ones. - menuoptions = list() - return - -#define APPEARANCE_CATEGORY_COLUMN "" -#define MAX_MUTANT_ROWS 4 -/datum/preferences/proc/ShowChoices(mob/user) - if(!user || !user.client) +/datum/preferences/ui_interact(mob/user, datum/tgui/ui) + if(!SSjob.initialized) + tgui_alert(user, "You cannot open the preferences menu before the job subsystem is initialized!") return - if(!SSjob || (SSjob.occupations.len <= 0)) - to_chat(user, span_notice("The job SSticker is not yet finished creating jobs, please try again later")) - return - - update_preview_icon() - var/list/dat = list("
") - - dat += "Character Settings" - dat += "Game Preferences" - dat += "OOC Preferences" - dat += "Donator Preferences" // yogs - Donor features - dat += "Keybindings" // yogs - Custom keybindings - - if(!path) - dat += "
Please create an account to save your preferences
" - - dat += "
" - - dat += "
" - - switch(current_tab) - if (0) // Character Settings# - if(path) - var/savefile/S = new /savefile(path) - if(S) - dat += "
" - var/name - var/unspaced_slots = 0 - for(var/i=1, i<=max_save_slots, i++) - unspaced_slots++ - if(unspaced_slots > 4) - dat += "
" - unspaced_slots = 0 - S.cd = "/character[i]" - S["real_name"] >> name - if(!name) - name = "Character[i]" - dat += "[name] " - dat += "
" - - dat += "

Occupation Choices

" - dat += "Set Occupation Preferences
" - if(CONFIG_GET(flag/roundstart_traits)) - dat += "

Quirk Setup

" - dat += "Configure Quirks
" - dat += "
Current Quirks: [all_quirks.len ? all_quirks.Join(", ") : "None"]
" - dat += "

Identity

" - dat += "" - - dat += "
" - if(is_banned_from(user.ckey, "Appearance")) - dat += "You are banned from using custom names and appearances. You can continue to adjust your characters, but you will be randomised once you join the game.
" - dat += "Random Name " - dat += "Always Random Name: [be_random_name ? "Yes" : "No"]
" - - dat += "Name: " - dat += "[real_name]
" - - if(FGENDER in pref_species.species_traits) //check for forced genders first like a smart person - gender = FEMALE - else if(AGENDER in pref_species.species_traits) - gender = PLURAL - else if(MGENDER in pref_species.species_traits) - gender = MALE - else - var/dispGender - if(gender == MALE) - dispGender = "Male" - else if(gender == FEMALE) - dispGender = "Female" - else - dispGender = "Other" - dat += "Gender: [dispGender]" - dat += "[random_locks["gender"] ? "Unlock" : "Lock"]
" - - dat += "Age: [age]
" - - dat += "Special Names:
" - var/old_group - for(var/custom_name_id in GLOB.preferences_custom_names) - var/namedata = GLOB.preferences_custom_names[custom_name_id] - if(!old_group) - old_group = namedata["group"] - else if(old_group != namedata["group"]) - old_group = namedata["group"] - dat += "
" - dat += "[namedata["pref_name"]]: [custom_names[custom_name_id]] " - dat += "

" - - dat += "Custom Job Preferences:
" - dat += "Preferred AI Core Display: [preferred_ai_core_display]
" - dat += "Preferred Security Department: [prefered_security_department]
" - dat += "Preferred Engineering Department: [prefered_engineering_department]
" - - - dat += "Language:
" - dat += "Accent: [accent ? accent : "None"]
" - - dat += "

Body

" - dat += "Random Body " - dat += "Always Random Body: [be_random_body ? "Yes" : "No"]" - dat += "Unlock all" - dat += "Lock all
" - dat += "Background: [background_options[background]]

" - - dat += "" - - var/use_skintones = pref_species.use_skintones - if(use_skintones) - - dat += APPEARANCE_CATEGORY_COLUMN + // If you leave and come back, re-register the character preview + if (!isnull(character_preview_view) && !(character_preview_view in user.client?.screen)) + user.client?.register_map_obj(character_preview_view) - dat += "

Skin Tone

" + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PreferencesMenu") + ui.set_autoupdate(FALSE) + ui.open() - dat += "[skin_tone]" - dat += "[random_locks["underwear"] ? "Unlock" : "Lock"]
" + // HACK: Without this the character starts out really tiny because of some BYOND bug. + // You can fix it by changing a preference, so let's just forcably update the body to emulate this. + addtimer(CALLBACK(character_preview_view, /atom/movable/screen/character_preview_view/proc/update_body), 1 SECONDS) - var/mutant_colors - if((((MUTCOLORS in pref_species.species_traits) && !(NOCOLORCHANGE in pref_species.species_traits))) || (MUTCOLORS_PARTSONLY in pref_species.species_traits)) +/datum/preferences/ui_state(mob/user) + return GLOB.always_state - if(!use_skintones) - dat += APPEARANCE_CATEGORY_COLUMN +// Without this, a hacker would be able to edit other people's preferences if +// they had the ref to Topic to. +/datum/preferences/ui_status(mob/user, datum/ui_state/state) + return user.client == parent ? UI_INTERACTIVE : UI_CLOSE - dat += "

Mutant Color

" +/datum/preferences/ui_data(mob/user) + var/list/data = list() - dat += "   " - dat += "Change[random_locks["mcolor"] ? "Unlock" : "Lock"]
" + if (isnull(character_preview_view)) + character_preview_view = create_character_preview_view(user) + else if (character_preview_view.client != parent) + // The client re-logged, and doing this when they log back in doesn't seem to properly + // carry emissives. + character_preview_view.register_to_client(parent) - mutant_colors = TRUE + if (tainted_character_profiles) + data["character_profiles"] = create_character_profiles() + tainted_character_profiles = FALSE - if(istype(pref_species, /datum/species/ethereal)) //not the best thing to do tbf but I dont know whats better. + data["character_preferences"] = compile_character_preferences(user) - if(!use_skintones) - dat += APPEARANCE_CATEGORY_COLUMN + data["active_slot"] = default_slot - dat += "

Ethereal Color

" + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + data += preference_middleware.get_ui_data(user) - dat += "   " - dat += "Change[random_locks["ethcolor"] ? "Unlock" : "Lock"]
" + return data +/datum/preferences/ui_static_data(mob/user) + var/list/data = list() - if(istype(pref_species, /datum/species/preternis)) //fuck, i know even less than you, i've just been copy pasting thus far. + data["character_profiles"] = create_character_profiles() - if(!use_skintones) - dat += APPEARANCE_CATEGORY_COLUMN + data["character_preview_view"] = character_preview_view.assigned_map + data["overflow_role"] = SSjob.GetJob(SSjob.overflow_role).title + data["window"] = current_window - dat += "

Preternis Color

" + data["content_unlocked"] = unlock_content + data["ckey"] = lowertext(user.client.ckey) - dat += "   " - dat += "Change[random_locks["pretcolor"] ? "Unlock" : "Lock"]
" + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + data += preference_middleware.get_ui_static_data(user) - if((EYECOLOR in pref_species.species_traits) || !(NOEYESPRITES in pref_species.species_traits)) + return data - if(!use_skintones && !mutant_colors) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Eye Color

" - - dat += "   " - dat += "Change[random_locks["eyes"] ? "Unlock" : "Lock"]
" - - dat += "" - else if(use_skintones || mutant_colors) - dat += "" - - if(HAIR in pref_species.species_traits) - - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Hair Style

" - - dat += "[hair_style]" - dat += "[random_locks["hair_style"] ? "Unlock" : "Lock"]
" - - dat += "<>
" - - dat += "   Change" - dat += "[random_locks["hair"] ? "Unlock" : "Lock"]
" - - dat += "

Facial Hair Style

" - - dat += "[facial_hair_style]" - dat += "[random_locks["facial_hair_style"] ? "Unlock" : "Lock"]
" - - dat += "<>
" - - dat += "   Change" - dat += "[random_locks["facial"] ? "Unlock" : "Lock"]
" - - dat += "

Hair Gradient

" - - dat += "[features["gradientstyle"]]" - dat += "[random_locks["gradientstyle"] ? "Unlock" : "Lock"]
" - - dat += "<>
" - - dat += "   Change" - dat += "[random_locks["gradientcolor"] ? "Unlock" : "Lock"]
" - - dat += "" - - //Mutant stuff - var/mutant_category = 0 - - if("tail_lizard" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Tail

" - - dat += "[features["tail_lizard"]]" - dat += "[random_locks["tail_lizard"] ? "Unlock" : "Lock"]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("tail_polysmorph" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Tail

" - - dat += "[features["tail_polysmorph"]]" - dat += "[random_locks["tail_polysmorph"] ? "Unlock" : "Lock"]
" +/datum/preferences/ui_assets(mob/user) + var/list/assets = list( + get_asset_datum(/datum/asset/spritesheet/preferences), + get_asset_datum(/datum/asset/json/preferences), + ) - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + assets += preference_middleware.get_ui_assets() - if("snout" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + return assets - dat += "

Snout

" +/datum/preferences/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if (.) + return - dat += "[features["snout"]]" - dat += "[random_locks["snout"] ? "Unlock" : "Lock"]
" + switch (action) + if ("change_slot") + // Save existing character + save_character() - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + // SAFETY: `load_character` performs sanitization the slot number + if (!load_character(params["slot"])) + tainted_character_profiles = TRUE + randomise_appearance_prefs() + save_character() - if("horns" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + preference_middleware.on_new_character(usr) - dat += "

Horns

" + character_preview_view.update_body() - dat += "[features["horns"]]" - dat += "[random_locks["horns"] ? "Unlock" : "Lock"]
" + return TRUE + if ("rotate") + character_preview_view.dir = turn(character_preview_view.dir, -90) - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + return TRUE + if ("cycle") + background = next_list_item(background, GLOB.preview_backgrounds) + character_preview_view.update_body() - if("frills" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + return TRUE + if ("set_preference") + var/requested_preference_key = params["preference"] + var/value = params["value"] - dat += "

Frills

" + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + if (preference_middleware.pre_set_preference(usr, requested_preference_key, value)) + return TRUE - dat += "[features["frills"]]" - dat += "[random_locks["frills"] ? "Unlock" : "Lock"]
" + var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key] + if (isnull(requested_preference)) + return FALSE - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + // SAFETY: `update_preference` performs validation checks + if (!update_preference(requested_preference, value)) + return FALSE - if("spines" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + if (istype(requested_preference, /datum/preference/name)) + tainted_character_profiles = TRUE - dat += "

Spines

" + return TRUE + if ("set_color_preference") + var/requested_preference_key = params["preference"] - dat += "[features["spines"]]" - dat += "[random_locks["spines"] ? "Unlock" : "Lock"]
" + var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key] + if (isnull(requested_preference)) + return FALSE - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + if (!istype(requested_preference, /datum/preference/color) \ + && !istype(requested_preference, /datum/preference/color_legacy) \ + ) + return FALSE - if("body_markings" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + var/default_value = read_preference(requested_preference.type) + if (istype(requested_preference, /datum/preference/color_legacy)) + default_value = expand_three_digit_color(default_value) - dat += "

Body Markings

" + // Yielding + var/new_color = input( + usr, + "Select new color", + null, + default_value || COLOR_WHITE, + ) as color | null - dat += "[features["body_markings"]]" - dat += "[random_locks["body_markings"] ? "Unlock" : "Lock"]
" + if (!new_color) + return FALSE - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + if (!update_preference(requested_preference, new_color)) + return FALSE - if(("legs" in pref_species.default_features) && !(DIGITIGRADE in pref_species.species_traits)) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + return TRUE - dat += "

Legs

" + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + var/delegation = preference_middleware.action_delegations[action] + if (!isnull(delegation)) + return call(preference_middleware, delegation)(params, usr) - dat += "[features["legs"]]" - dat += "[random_locks["legs"] ? "Unlock" : "Lock"]
" + return FALSE - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 +/datum/preferences/ui_close(mob/user) + save_character() + save_preferences() + QDEL_NULL(character_preview_view) - if("moth_wings" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN +/datum/preferences/Topic(href, list/href_list) + . = ..() + if (.) + return - dat += "

Moth wings

" + if (href_list["open_keybindings"]) + current_window = PREFERENCE_TAB_KEYBINDINGS + update_static_data(usr) + ui_interact(usr) + return TRUE - dat += "[features["moth_wings"]]" - dat += "[random_locks["moth_wings"] ? "Unlock" : "Lock"]
" +/datum/preferences/Topic(href, list/href_list) + . = ..() + if (.) + return - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + if (href_list["open_keybindings"]) + current_window = PREFERENCE_TAB_KEYBINDINGS + update_static_data(usr) + ui_interact(usr) + return TRUE - if("teeth" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN +/datum/preferences/proc/create_character_preview_view(mob/user) + character_preview_view = new(null, src, user.client) + character_preview_view.update_body() + character_preview_view.register_to_client(user.client) - dat += "

Teeth

" + return character_preview_view - dat += "[features["teeth"]]" - dat += "[random_locks["teeth"] ? "Unlock" : "Lock"]
" +/datum/preferences/proc/compile_character_preferences(mob/user) + var/list/preferences = list() - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (!preference.is_accessible(src)) + continue - if("dome" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + LAZYINITLIST(preferences[preference.category]) - dat += "

Dome

" + var/value = read_preference(preference.type) + var/data = preference.compile_ui_data(user, value) - dat += "[features["dome"]]" - dat += "[random_locks["dome"] ? "Unlock" : "Lock"]
" + preferences[preference.category][preference.savefile_key] = data - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + var/list/append_character_preferences = preference_middleware.get_character_preferences(user) + if (isnull(append_character_preferences)) + continue - if("dorsal_tubes" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + for (var/category in append_character_preferences) + if (category in preferences) + preferences[category] += append_character_preferences[category] + else + preferences[category] = append_character_preferences[category] - dat += "

Dorsal Tubes

" + return preferences - dat += "[features["dorsal_tubes"]]" - dat += "[random_locks["dorsal_tubes"] ? "Unlock" : "Lock"]
" +/// Applies all PREFERENCE_PLAYER preferences +/datum/preferences/proc/apply_all_client_preferences() + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (preference.savefile_identifier != PREFERENCE_PLAYER) + continue - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + value_cache -= preference.type + preference.apply_to_client(parent, read_preference(preference.type)) - if("ethereal_mark" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN +// This is necessary because you can open the set preferences menu before +// the atoms SS is done loading. +INITIALIZE_IMMEDIATE(/atom/movable/screen/character_preview_view) - dat += "

Ethereal Mark

" +/// A preview of a character for use in the preferences menu +/atom/movable/screen/character_preview_view + name = "character_preview" + del_on_map_removal = FALSE + layer = GAME_PLANE + plane = GAME_PLANE - dat += "[features["ethereal_mark"]]" - dat += "[random_locks["ethereal_mark"] ? "Unlock" : "Lock"]
" + /// The body that is displayed + var/mob/living/carbon/human/dummy/body - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + /// The preferences this refers to + var/datum/preferences/preferences - if("pod_hair" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + var/list/plane_masters = list() - dat += "

Head Vegitation Style

" - dat += "[features["pod_hair"]]" - dat += "[random_locks["pod_hair"] ? "Unlock" : "Lock"]
" - dat += "   Change" - dat += "[random_locks["hair"] ? "Unlock" : "Lock"]
" + /// The client that is watching this view + var/client/client - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 +/atom/movable/screen/character_preview_view/Initialize(mapload, datum/preferences/preferences, client/client) + . = ..() - if("pod_flower" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - dat += "

Head Flowers Color

" - dat += "   Change" - dat += "[random_locks["facial"] ? "Unlock" : "Lock"]
" + assigned_map = "character_preview_[REF(src)]" + set_position(1, 1) - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + src.preferences = preferences - if("tail_human" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN +/atom/movable/screen/character_preview_view/Destroy() + QDEL_NULL(body) - dat += "

Tail

" + for (var/plane_master in plane_masters) + client?.screen -= plane_master + qdel(plane_master) - dat += "[features["tail_human"]]" - dat += "[random_locks["tail_human"] ? "Unlock" : "Lock"]
" + client?.clear_map(assigned_map) + client?.screen -= src - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + preferences?.character_preview_view = null - if("ipc_screen" in pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + client = null + plane_masters = null + preferences = null - dat += "

Screen Style

" + return ..() - dat += "[features["ipc_screen"]]
" +/// Updates the currently displayed body +/atom/movable/screen/character_preview_view/proc/update_body() + if (isnull(body)) + create_body() + else + body.wipe_state() + appearance = preferences.render_new_preview_appearance(body) - dat += "   Change
" +/atom/movable/screen/character_preview_view/proc/create_body() + QDEL_NULL(body) - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 + body = new - if("ipc_antenna" in pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN + // Without this, it doesn't show up in the menu + body.appearance_flags &= ~KEEP_TOGETHER - dat += "

Antenna Style

" +/// Registers the relevant map objects to a client +/atom/movable/screen/character_preview_view/proc/register_to_client(client/client) + QDEL_LIST(plane_masters) - dat += "[features["ipc_antenna"]]
" + src.client = client - dat += "   Change
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("ipc_chassis" in pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Chassis Style

" - - dat += "[features["ipc_chassis"]]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("ears" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Ears

" - - dat += "[features["ears"]]" - dat += "[random_locks["ears"] ? "Unlock" : "Lock"]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("plasmaman_helmet" in pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Helmet Style

" - - dat += "[features["plasmaman_helmet"]]" - dat += "[random_locks["plasmaman_helmet"] ? "Unlock" : "Lock"]
" - - if(CONFIG_GET(flag/join_with_mutant_humans)) - - if("wings" in pref_species.default_features && GLOB.r_wings_list.len >1) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Wings

" - - dat += "[features["wings"]]" - dat += "[random_locks["wings"] ? "Unlock" : "Lock"]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if(mutant_category) - dat += "" - mutant_category = 0 - dat += "
" - - dat += "Species:
[pref_species.name]" - dat += "[random_locks["species"] ? "Unlock" : "Lock"]
" - - dat += "Underwear:
[underwear]" - dat += "[random_locks["underwear"] ? "Unlock" : "Lock"]
" - - dat += "Undershirt:
[undershirt]" - dat += "[random_locks["undershirt"] ? "Unlock" : "Lock"]
" - - dat += "Socks:
[socks]" - dat += "[random_locks["socks"] ? "Unlock" : "Lock"]
" - - dat += "Backpack:
[backbag]" - dat += "Jumpsuit:
[jumpsuit_style]
" - dat += "[random_locks["bag"] ? "Unlock" : "Lock"]
" - if((HAS_FLESH in pref_species.species_traits) || (HAS_BONE in pref_species.species_traits)) - dat += "
Temporal Scarring:
[(persistent_scars) ? "Enabled" : "Disabled"]" - dat += "Clear scar slots
" - dat += "Uplink Spawn Location:
[uplink_spawn_loc]
" - - - if (1) // Game Preferences - dat += "
" - dat += "

General Settings

" - dat += "UI Style: [UI_style]
" - dat += "tgui Window Mode: [(tgui_fancy) ? "Fancy (default)" : "Compatible (slower)"]
" - dat += "tgui Window Placement: [(tgui_lock) ? "Primary monitor" : "Free (default)"]
" - dat += "Show Runechat Chat Bubbles: [chat_on_map ? "Enabled" : "Disabled"]
" - dat += "Runechat message char limit: [max_chat_length]
" - dat += "See Runechat for non-mobs: [see_chat_non_mob ? "Enabled" : "Disabled"]
" - dat += "See Runechat emotes: [see_rc_emotes ? "Enabled" : "Disabled"]
" - dat += "Hear alternative station announcers: [disable_alternative_announcers ? "Disabled" : "Enabled"]
" - dat += "Balloon Alerts: [disable_balloon_alerts ? "Disabled" : "Enabled"]
" - dat += "
" - dat += "Action Buttons: [(buttons_locked) ? "Locked In Place" : "Unlocked"]
" - dat += "Hotkey mode: [(hotkeys) ? "Hotkeys" : "Default"]
" - dat += "
" - dat += "PDA Color:     Change
" - dat += "PDA Style: [pda_style]
" - dat += "PDA Theme: [pda_theme]
" - dat += "PDA Starts in ID Slot: [id_in_pda ? "Enabled" : "Disabled"]
" - dat += "Skillcape: [(skillcape_id != "None") ? "[GLOB.skillcapes[skillcape_id]]" : "None"]
" - dat += "Flare: [flare ? "Enabled" : "Disabled"]
" - dat += "Map: [map ? "Enabled" : "Disabled"]
" - dat += "Preferred Box Bar: [bar_choice]
" - dat += "
" - dat += "Ghost Ears: [(chat_toggles & CHAT_GHOSTEARS) ? "All Speech" : "Nearest Creatures"]
" - dat += "Ghost Radio: [(chat_toggles & CHAT_GHOSTRADIO) ? "All Messages":"No Messages"]
" - dat += "Ghost Sight: [(chat_toggles & CHAT_GHOSTSIGHT) ? "All Emotes" : "Nearest Creatures"]
" - dat += "Ghost Whispers: [(chat_toggles & CHAT_GHOSTWHISPER) ? "All Speech" : "Nearest Creatures"]
" - dat += "Ghost PDA: [(chat_toggles & CHAT_GHOSTPDA) ? "All Messages" : "Nearest Creatures"]
" - - if(unlock_content) - dat += "Ghost Form: [ghost_form]
" - dat += "Ghost Orbit: [ghost_orbit]
" - - var/button_name = "If you see this something went wrong." - switch(ghost_accs) - if(GHOST_ACCS_FULL) - button_name = GHOST_ACCS_FULL_NAME - if(GHOST_ACCS_DIR) - button_name = GHOST_ACCS_DIR_NAME - if(GHOST_ACCS_NONE) - button_name = GHOST_ACCS_NONE_NAME - - dat += "Ghost Accessories: [button_name]
" - - switch(ghost_others) - if(GHOST_OTHERS_THEIR_SETTING) - button_name = GHOST_OTHERS_THEIR_SETTING_NAME - if(GHOST_OTHERS_DEFAULT_SPRITE) - button_name = GHOST_OTHERS_DEFAULT_SPRITE_NAME - if(GHOST_OTHERS_SIMPLE) - button_name = GHOST_OTHERS_SIMPLE_NAME - - dat += "Ghosts of Others: [button_name]
" - dat += "
" - - dat += "Income Updates: [(chat_toggles & CHAT_BANKCARD) ? "Allowed" : "Muted"]
" - dat += "
" - - dat += "FPS: [clientfps]
" - - dat += "Parallax (Fancy Space): " - switch (parallax) - if (PARALLAX_LOW) - dat += "Low" - if (PARALLAX_MED) - dat += "Medium" - if (PARALLAX_INSANE) - dat += "Insane" - if (PARALLAX_DISABLE) - dat += "Disabled" - else - dat += "High" - dat += "
" - - dat += "Ambient Occlusion: [ambientocclusion ? "Enabled" : "Disabled"]
" - dat += "Fit Viewport: [auto_fit_viewport ? "Auto" : "Manual"]
" - if (CONFIG_GET(string/default_view) != CONFIG_GET(string/default_view_square)) - dat += "Widescreen: [widescreenpref ? "Enabled ([CONFIG_GET(string/default_view)])" : "Disabled ([CONFIG_GET(string/default_view_square)])"]
" - - button_name = pixel_size - dat += "Pixel Scaling: [(button_name) ? "Pixel Perfect [button_name]x" : "Stretch to fit"]
" - - switch(scaling_method) - if(SCALING_METHOD_NORMAL) - button_name = "Nearest Neighbor" - if(SCALING_METHOD_DISTORT) - button_name = "Point Sampling" - if(SCALING_METHOD_BLUR) - button_name = "Bilinear" - dat += "Scaling Method: [button_name]
" - - if (CONFIG_GET(flag/maprotation)) - var/p_map = preferred_map - if (!p_map) - p_map = "Default" - if (config.defaultmap) - p_map += " ([config.defaultmap.map_name])" - else - if (p_map in config.maplist) - var/datum/map_config/VM = config.maplist[p_map] - if (!VM) - p_map += " (No longer exists)" - else - p_map = VM.map_name - else - p_map += " (No longer exists)" - if(CONFIG_GET(flag/preference_map_voting)) - dat += "Preferred Map: [p_map]
" - //yogs start -- Mood preference toggling - if(CONFIG_GET(flag/disable_human_mood)) - dat += "Mood: [yogtoggles & PREF_MOOD ? "Enabled" : "Disabled"]
" - dat += "Mood Tail Wagging: [mood_tail_wagging ? "Enabled" : "Disabled"]
" - //yogs end - - dat += "
" - - dat += "

Special Role Settings

" - - if(is_banned_from(user.ckey, ROLE_SYNDICATE)) - dat += "You are banned from antagonist roles.
" - src.be_special = list() - - - for (var/i in GLOB.special_roles) - if(is_banned_from(user.ckey, i)) - dat += "Be [capitalize(i)]: BANNED
" - else - var/days_remaining = null - if(ispath(GLOB.special_roles[i]) && CONFIG_GET(flag/use_age_restriction_for_jobs)) //If it's a game mode antag, check if the player meets the minimum age - var/mode_path = GLOB.special_roles[i] - var/datum/game_mode/temp_mode = new mode_path - days_remaining = temp_mode.get_remaining_days(user.client) - - if(days_remaining) - dat += "Be [capitalize(i)]: \[IN [days_remaining] DAYS]
" - // yogs start - Donor features - else if(src.yogtoggles & QUIET_ROUND) - dat += "Be [capitalize(i)]: \[QUIET ROUND\]
" - // yogs end - else - dat += "Be [capitalize(i)]: [(i in be_special) ? "Enabled" : "Disabled"]
" - dat += "
" - dat += "Midround Antagonist: [(toggles & MIDROUND_ANTAG) ? "Enabled" : "Disabled"]
" - - // yogs start - Donor features - if(is_donator(user.client)) - dat += "Quiet round: [(src.yogtoggles & QUIET_ROUND) ? "Yes" : "No"]
" - // yogs end - dat += "
" - if(2) //OOC Preferences - dat += "" - - if(user.client.holder) - dat +="" - dat += "
" - dat += "

OOC Settings

" - dat += "Window Flashing: [(windowflashing) ? "Enabled":"Disabled"]
" - dat += "
" - dat += "Play Admin MIDIs: [(toggles & SOUND_MIDI) ? "Enabled":"Disabled"]
" - dat += "Play Lobby Music: [(toggles & SOUND_LOBBY) ? "Enabled":"Disabled"]
" - dat += "See Pull Requests: [(chat_toggles & CHAT_PULLR) ? "Enabled":"Disabled"]
" - dat += "
" - - - if(user.client) - if(unlock_content) - dat += "BYOND Membership Publicity: [(toggles & MEMBER_PUBLIC) ? "Public" : "Hidden"]
" - - if(unlock_content || check_rights_for(user.client, R_ADMIN)) - dat += "OOC Color:     Change
" - - dat += "
" - - dat += "

Admin Settings

" - - dat += "Adminhelp Sounds: [(toggles & SOUND_ADMINHELP)?"Enabled":"Disabled"]
" - dat += "Prayer Sounds: [(toggles & SOUND_PRAYERS)?"Enabled":"Disabled"]
" - dat += "Announce Login: [(toggles & ANNOUNCE_LOGIN)?"Enabled":"Disabled"]
" - dat += "
" - dat += "Combo HUD Lighting: [(toggles & COMBOHUD_LIGHTING)?"Full-bright":"No Change"]
" - dat += "
" - dat += "Hide Dead Chat: [(chat_toggles & CHAT_DEAD)?"Shown":"Hidden"]
" - dat += "Hide Radio Messages: [(chat_toggles & CHAT_RADIO)?"Shown":"Hidden"]
" - dat += "Hide Prayers: [(chat_toggles & CHAT_PRAYER)?"Shown":"Hidden"]
" - dat += "Split Admin Tabs: [(extra_toggles & SPLIT_ADMIN_TABS)?"Enabled":"Disabled"]
" - dat += "Fast MC Refresh: [(extra_toggles & FAST_MC_REFRESH)?"Enabled":"Disabled"]
" - if(CONFIG_GET(flag/allow_admin_asaycolor)) - dat += "
" - dat += "ASAY Color:     Change
" - - //deadmin - dat += "

Deadmin While Playing

" - if(CONFIG_GET(flag/auto_deadmin_players)) - dat += "Always Deadmin: FORCED
" - else - dat += "Always Deadmin: [(toggles & DEADMIN_ALWAYS)?"Enabled":"Disabled"]
" - if(!(toggles & DEADMIN_ALWAYS)) - dat += "
" - if(!CONFIG_GET(flag/auto_deadmin_antagonists)) - dat += "As Antag: [(toggles & DEADMIN_ANTAGONIST)?"Deadmin":"Keep Admin"]
" - else - dat += "As Antag: FORCED
" - - if(!CONFIG_GET(flag/auto_deadmin_heads)) - dat += "As Command: [(toggles & DEADMIN_POSITION_HEAD)?"Deadmin":"Keep Admin"]
" - else - dat += "As Command: FORCED
" - - if(!CONFIG_GET(flag/auto_deadmin_security)) - dat += "As Security: [(toggles & DEADMIN_POSITION_SECURITY)?"Deadmin":"Keep Admin"]
" - else - dat += "As Security: FORCED
" - - if(!CONFIG_GET(flag/auto_deadmin_silicons)) - dat += "As Silicon: [(toggles & DEADMIN_POSITION_SILICON)?"Deadmin":"Keep Admin"]
" - else - dat += "As Silicon: FORCED
" - - if(!CONFIG_GET(flag/auto_deadmin_critical)) - dat += "As Critical Roles: [(toggles & DEADMIN_POSITION_CRITICAL)?"Deadmin":"Keep Admin"]
" - else - dat += "As Critical Roles: FORCED
" - - dat += "
" - // yogs start - Donor features - if (3) //Donator preferences - dat += "
" - dat += "

Donator Preferences

" - if(is_donator(user.client)) - dat += "Quiet round: [(src.yogtoggles & QUIET_ROUND) ? "Yes" : "No"]
" - dat += "Wear fancy hat as borg: " - dat += "[borg_hat ? "Yes" : "No"]
" - dat += "Fancy Hat: " - ///This is the typepath of the donor's hat that they may choose to spawn with. - var/typehat = donor_hat - var/temp_hat = donor_hat ? (new typehat()) : "None selected" - dat += "Pick [temp_hat]
" - if(donor_hat) - qdel(temp_hat) - dat += "Fancy Item: " - ///Whatever item the donator has chosen to apply. - var/typeitem = donor_item - var/temp_item = donor_item ? (new typeitem()) : "None selected" - dat += "Pick [temp_item]
" - if(donor_item) - qdel(temp_item) - dat += "Fancy PDA: " - dat += "[GLOB.donor_pdas[donor_pda]]
" - dat += "Purrbation (Humans only) " - dat += "[purrbation ? "Yes" : "No"]
" - else - dat += "Donate here" - dat += "
" - // yogs end - if (4) // Keybindings - // Create an inverted list of keybindings -> key - var/list/user_binds = list() - for (var/key in key_bindings) - for(var/kb_name in key_bindings[key]) - user_binds[kb_name] += list(key) - - var/list/kb_categories = list() - // Group keybinds by category - for (var/name in GLOB.keybindings_by_name) - var/datum/keybinding/kb = GLOB.keybindings_by_name[name] - kb_categories[kb.category] += list(kb) - - dat += "" - - for (var/category in kb_categories) - dat += "

[category]

" - for (var/i in kb_categories[category]) - var/datum/keybinding/kb = i - if(!length(user_binds[kb.name]) || user_binds[kb.name][1] == "Unbound") - dat += " Unbound" - var/list/default_keys = hotkeys ? kb.hotkey_keys : kb.classic_keys - if(LAZYLEN(default_keys)) - dat += "| Default: [default_keys.Join(", ")]" - dat += "
" - else - var/bound_key = user_binds[kb.name][1] - dat += " [bound_key]" - for(var/bound_key_index in 2 to length(user_binds[kb.name])) - bound_key = user_binds[kb.name][bound_key_index] - dat += " | [bound_key]" - if(length(user_binds[kb.name]) < MAX_KEYS_PER_KEYBIND) - dat += "| Add Secondary" - var/list/default_keys = hotkeys ? kb.classic_keys : kb.hotkey_keys - if(LAZYLEN(default_keys)) - dat += "| Default: [default_keys.Join(", ")]" - dat += "
" - - dat += "

" - dat += "\[Reset to default\]" - dat += "" - dat += "
" - - if(!IsGuestKey(user.key)) - dat += "Undo " - dat += "Save Setup " - - dat += "Reset Setup" - dat += "
" - - winshow(user, "preferences_window", TRUE) - var/datum/browser/popup = new(user, "preferences_browser", "
Character Setup
", 640, 770) - popup.set_content(dat.Join()) - popup.open(FALSE) - onclose(user, "preferences_window", src) - -#undef APPEARANCE_CATEGORY_COLUMN -#undef MAX_MUTANT_ROWS - -/datum/preferences/proc/CaptureKeybinding(mob/user, datum/keybinding/kb, var/old_key) - var/HTML = {" -
Keybinding: [kb.full_name]
[kb.description]

Press any key to change
Press ESC to clear
- - "} - winshow(user, "capturekeypress", TRUE) - var/datum/browser/popup = new(user, "capturekeypress", "
Keybindings
", 350, 300) - popup.set_content(HTML) - popup.open(FALSE) - onclose(user, "capturekeypress", src) - -/datum/preferences/proc/SetChoices(mob/user, limit = 17, list/splitJobs = list("Research Director", "Head of Personnel"), widthPerColumn = 295, height = 620) - if(!SSjob) + if (!client) return - //limit - The amount of jobs allowed per column. Defaults to 17 to make it look nice. - //splitJobs - Allows you split the table by job. You can make different tables for each department by including their heads. Defaults to CE to make it look nice. - //widthPerColumn - Screen's width for every column. - //height - Screen's height. - - var/width = widthPerColumn + for (var/plane_master_type in subtypesof(/atom/movable/screen/plane_master)) + var/atom/movable/screen/plane_master/plane_master = new plane_master_type + plane_master.screen_loc = "[assigned_map]:CENTER" + client?.screen |= plane_master - var/HTML = "
" - if(SSjob.occupations.len <= 0) - HTML += "The job SSticker is not yet finished creating jobs, please try again later" - HTML += "
Done

" // Easier to press up here. - - else - HTML += "Choose occupation chances
" - HTML += "
Left-click to raise an occupation preference, right-click to lower it.
" - HTML += "
Done

" // Easier to press up here. - HTML += "" - HTML += "
" // Table within a table for alignment, also allows you to easily add more colomns. - HTML += "" - var/index = -1 - - //The job before the current job. I only use this to get the previous jobs color when I'm filling in blank rows. - var/datum/job/lastJob - - var/datum/job/overflow = SSjob.GetJob(SSjob.overflow_role) - - for(var/datum/job/job in sortList(SSjob.occupations, /proc/cmp_job_display_asc)) - - index += 1 - if((index >= limit) || (job.title in splitJobs)) - width += widthPerColumn - if((index < limit) && (lastJob != null)) - //If the cells were broken up by a job in the splitJob list then it will fill in the rest of the cells with - //the last job's selection color. Creating a rather nice effect. - for(var/i = 0, i < (limit - index), i += 1) - HTML += "" - HTML += "
  
" - index = 0 - - HTML += "" - continue - var/required_playtime_remaining = job.required_playtime_remaining(user.client) - if(required_playtime_remaining) - HTML += "[rank]" - continue - if(!job.player_old_enough(user.client)) - var/available_in_days = job.available_in_days(user.client) - HTML += "[rank]" - continue - if((job_preferences[overflow] == JP_LOW) && (rank != SSjob.overflow_role) && !is_banned_from(user.ckey, SSjob.overflow_role)) - HTML += "[rank]" - continue - // yogs start - Donor features, quiet round - if(((rank in GLOB.command_positions) || (rank in GLOB.nonhuman_positions)) && (src.yogtoggles & QUIET_ROUND)) - HTML += "[rank]" - continue - // yogs end - - var/rank_display - if(job.alt_titles) - rank_display = "[GetPlayerAltTitle(job)]" - else - rank_display = span_dark("[rank]") - - if((rank in GLOB.command_positions) || (rank == "AI"))//Bold head jobs - HTML += "[rank_display]" - else - HTML += rank_display - - HTML += "" - continue +/datum/preferences/proc/create_character_profiles() + var/list/profiles = list() - HTML += "[prefLevelLabel]" - HTML += "" + var/savefile/savefile = new(path) + for (var/index in 1 to max_save_slots) + // It won't be updated in the savefile yet, so just read the name directly + if (index == default_slot) + profiles += read_preference(/datum/preference/name/real_name) + continue - for(var/i = 1, i < (limit - index), i += 1) // Finish the column so it is even - HTML += "" + savefile.cd = "/character[index]" - HTML += "
" - var/rank = job.title - lastJob = job - if(is_banned_from(user.ckey, rank)) - HTML += "[rank] BANNED
\[ [get_exp_format(required_playtime_remaining)] as [job.get_exp_req_type()] \]
\[IN [(available_in_days)] DAYS\]
\[QUIET\]
" - - var/prefLevelLabel = "ERROR" - var/prefLevelColor = "pink" - var/prefUpperLevel = -1 // level to assign on left click - var/prefLowerLevel = -1 // level to assign on right click - - switch(job_preferences[job.title]) - if(JP_HIGH) - prefLevelLabel = "High" - prefLevelColor = "slateblue" - prefUpperLevel = 4 - prefLowerLevel = 2 - if(JP_MEDIUM) - prefLevelLabel = "Medium" - prefLevelColor = "green" - prefUpperLevel = 1 - prefLowerLevel = 3 - if(JP_LOW) - prefLevelLabel = "Low" - prefLevelColor = "orange" - prefUpperLevel = 2 - prefLowerLevel = 4 - else - prefLevelLabel = "NEVER" - prefLevelColor = "red" - prefUpperLevel = 3 - prefLowerLevel = 1 + plane_masters += plane_master - HTML += "" + client?.register_map_obj(src) - if(rank == SSjob.overflow_role)//Overflow is special - if(job_preferences[overflow.title] == JP_LOW) - HTML += "Yes" - else - HTML += "No" - HTML += "
  
" - HTML += "
" + var/name + READ_FILE(savefile["real_name"], name) - var/message = "Be an [SSjob.overflow_role] if preferences unavailable" - if(joblessrole == BERANDOMJOB) - message = "Get random job if preferences unavailable" - else if(joblessrole == RETURNTOLOBBY) - message = "Return to lobby if preferences unavailable" - HTML += "

[message]
" - HTML += "
Reset Preferences
" + if (isnull(name)) + profiles += null + continue - var/datum/browser/popup = new(user, "mob_occupation", "
Occupation Preferences
", width, height) - popup.set_window_options("can_close=0") - popup.set_content(HTML) - popup.open(FALSE) + profiles += name -/datum/preferences/proc/GetPlayerAltTitle(datum/job/job) - return player_alt_titles.Find(job.title) > 0 \ - ? player_alt_titles[job.title] \ - : job.title + return profiles -/datum/preferences/proc/SetPlayerAltTitle(datum/job/job, new_title) - // remove existing entry - if(player_alt_titles.Find(job.title)) - player_alt_titles -= job.title - // add one if it's not default - if(job.title != new_title) - player_alt_titles[job.title] = new_title - -/datum/preferences/proc/SetJobPreferenceLevel(datum/job/job, level) +/datum/preferences/proc/set_job_preference_level(datum/job/job, level) if (!job) return FALSE - if (level == JP_HIGH) // to high - //Set all other high to medium - for(var/j in job_preferences) - if(job_preferences[j] == JP_HIGH) - job_preferences[j] = JP_MEDIUM - //technically break here - - job_preferences[job.title] = level - return TRUE - -/datum/preferences/proc/UpdateJobPreference(mob/user, role, desiredLvl) - if(!SSjob || SSjob.occupations.len <= 0) - return - var/datum/job/job = SSjob.GetJob(role) - - if(!job) - user << browse(null, "window=mob_occupation") - ShowChoices(user) - return + if (level == JP_HIGH) + var/datum/job/overflow_role = SSjob.overflow_role + var/overflow_role_title = initial(overflow_role.title) - if (!isnum(desiredLvl)) - to_chat(user, span_danger("UpdateJobPreference - desired level was not a number. Please notify coders!")) - ShowChoices(user) - return - - var/jpval = null - switch(desiredLvl) - if(3) - jpval = JP_LOW - if(2) - jpval = JP_MEDIUM - if(1) - jpval = JP_HIGH - - if(role == SSjob.overflow_role) - if(job_preferences[job.title] == JP_LOW) - jpval = null - else - jpval = JP_LOW - - SetJobPreferenceLevel(job, jpval) - SetChoices(user) - - return 1 - - -/datum/preferences/proc/ResetJobs() - job_preferences = list() - -/datum/preferences/proc/SetQuirks(mob/user) - if(!SSquirks) - to_chat(user, span_danger("The quirk subsystem is still initializing! Try again in a minute.")) - return + for(var/other_job in job_preferences) + if(job_preferences[other_job] == JP_HIGH) + // Overflow role needs to go to NEVER, not medium! + if(other_job == overflow_role_title) + job_preferences[other_job] = null + else + job_preferences[other_job] = JP_MEDIUM - var/list/dat = list() - if(!SSquirks.quirks.len) - dat += "The quirk subsystem hasn't finished initializing, please hold..." - dat += "
Done

" + if(level == null) + job_preferences -= job.title else - dat += "
Choose quirk setup

" - dat += "
Left-click to add or remove quirks. You need negative quirks to have positive ones.
\ - Quirks are applied at roundstart and cannot normally be removed.
" - dat += "
Done
" - dat += "
" - dat += "
Current quirks: [all_quirks.len ? all_quirks.Join(", ") : "None"]
" - dat += "
[GetPositiveQuirkCount()] / [MAX_QUIRKS] max positive quirks
\ - Quirk balance remaining: [GetQuirkBalance()]

" - for(var/V in SSquirks.quirks) - var/datum/quirk/T = SSquirks.quirks[V] - var/quirk_name = initial(T.name) - var/has_quirk - var/quirk_cost = initial(T.value) * -1 - var/lock_reason = FALSE // Also marks whether this quirk ought to be locked at all; FALSE implies it's OK for this person to have this quirk - for(var/_V in all_quirks) - if(_V == quirk_name) - has_quirk = TRUE - if(initial(T.mood_quirk) && (CONFIG_GET(flag/disable_human_mood) && !(yogtoggles & PREF_MOOD)))//Yogs -- Adds mood to preferences - lock_reason = "Mood is disabled." - else - var/datum/quirk/t = new T(no_init = TRUE) - lock_reason = t.check_quirk(src) // Yogs -- allows for specific denial of quirks based on current preferences - qdel(t) - if(has_quirk) - if(lock_reason) - all_quirks -= quirk_name - has_quirk = FALSE - else - quirk_cost *= -1 //invert it back, since we'd be regaining this amount - if(quirk_cost > 0) - quirk_cost = "+[quirk_cost]" - var/font_color = "#AAAAFF" - if(initial(T.value) != 0) - font_color = initial(T.value) > 0 ? "#AAFFAA" : "#FFAAAA" - if(lock_reason) - dat += "[quirk_name] - [initial(T.desc)] \ - LOCKED: [lock_reason]
" - else - if(has_quirk) - dat += "[has_quirk ? "Remove" : "Take"] ([quirk_cost] pts.) \ - [quirk_name] - [initial(T.desc)]
" - else - dat += "[has_quirk ? "Remove" : "Take"] ([quirk_cost] pts.) \ - [quirk_name] - [initial(T.desc)]
" - dat += "
Reset Quirks
" + job_preferences[job.title] = level - var/datum/browser/popup = new(user, "mob_occupation", "
Quirk Preferences
", 900, 600) //no reason not to reuse the occupation window, as it's cleaner that way - popup.set_window_options("can_close=0") - popup.set_content(dat.Join()) - popup.open(FALSE) + return TRUE /datum/preferences/proc/GetQuirkBalance() var/bal = 0 @@ -1349,1020 +508,25 @@ GLOBAL_LIST_EMPTY(preferences_datums) sum++ return sum -/datum/preferences/Topic(href, href_list, hsrc) //yeah, gotta do this I guess.. - . = ..() - if(href_list["close"]) - var/client/C = usr.client - if(C) - C.clear_character_previews() - -/datum/preferences/proc/process_link(mob/user, list/href_list) - // yogs start - Donor features - if(href_list["preference"] == "donor") - if(is_donator(user)) - var/client/C = (istype(user, /client)) ? user : user.client - switch(href_list["task"]) - if("borghat") - borg_hat = !borg_hat - if("hat") - C.custom_donator_item() - if("item") - C.custom_donator_item() - if("quiet_round") - yogtoggles ^= QUIET_ROUND - if("pda") - donor_pda = (donor_pda % GLOB.donor_pdas.len) + 1 - if("purrbation") - purrbation = !purrbation - else - message_admins("EXPLOIT \[donor\]: [user] tried to access donor only functions (as a non-donor). Attempt made on \"[href_list["preference"]]\" -> \"[href_list["task"]]\".") - // yogs end - if(href_list["bancheck"]) - var/list/ban_details = is_banned_from_with_details(user.ckey, user.client.address, user.client.computer_id, href_list["bancheck"]) - var/admin = FALSE - if(GLOB.permissions.admin_datums[user.ckey] || GLOB.permissions.deadmins[user.ckey]) - admin = TRUE - for(var/i in ban_details) - if(admin && !text2num(i["applies_to_admins"])) - continue - ban_details = i - break //we only want to get the most recent ban's details - if(ban_details && ban_details.len) - var/expires = "This is a permanent ban." - if(ban_details["expiration_time"]) - expires = " The ban is for [DisplayTimeText(text2num(ban_details["duration"]) MINUTES)] and expires on [ban_details["expiration_time"]] (server time)." - to_chat(user, span_danger("You, or another user of this computer or connection ([ban_details["key"]]) is banned from playing [href_list["bancheck"]].
The ban reason is: [ban_details["reason"]]
This ban (BanID #[ban_details["id"]]) was applied by [ban_details["admin_key"]] on [ban_details["bantime"]] during round ID [ban_details["round_id"]].
[expires]")) - return - if(href_list["preference"] == "job") - switch(href_list["task"]) - if("close") - user << browse(null, "window=mob_occupation") - ShowChoices(user) - if("reset") - ResetJobs() - SetChoices(user) - if("random") - switch(joblessrole) - if(RETURNTOLOBBY) - if(is_banned_from(user.ckey, SSjob.overflow_role)) - joblessrole = BERANDOMJOB - else - joblessrole = BEOVERFLOW - if(BEOVERFLOW) - joblessrole = BERANDOMJOB - if(BERANDOMJOB) - joblessrole = RETURNTOLOBBY - SetChoices(user) - if ("alt_title") - var/datum/job/job = SSjob.GetJob(href_list["job"]) - if (job) - var/choices = list(job.title) + job.alt_titles - var/choice = input("Pick a title for [job.title].", "Character Generation", GetPlayerAltTitle(job)) as anything in choices | null - if(choice) - SetPlayerAltTitle(job, choice) - SetChoices(user) - if("setJobLevel") - UpdateJobPreference(user, href_list["text"], text2num(href_list["level"])) - else - SetChoices(user) - return 1 - - else if(href_list["preference"] == "trait") - switch(href_list["task"]) - if("close") - user << browse(null, "window=mob_occupation") - ShowChoices(user) - if("update") - var/quirk = href_list["trait"] - if(!SSquirks.quirks[quirk]) - return - for(var/V in SSquirks.quirk_blacklist) //V is a list - var/list/L = V - for(var/Q in all_quirks) - if((quirk in L) && (Q in L) && !(Q == quirk)) //two quirks have lined up in the list of the list of quirks that conflict with each other, so return (see quirks.dm for more details) - to_chat(user, span_danger("[quirk] is incompatible with [Q].")) - return - var/value = SSquirks.quirk_points[quirk] // The value of the chosen quirk. - var/balance = GetQuirkBalance() - if(quirk in all_quirks) - if(balance + value < 0) - to_chat(user, span_warning("Refunding this would cause you to go below your balance!")) - return - all_quirks -= quirk - else - var/positive_count = GetPositiveQuirkCount() // Yogs -- fixes weird behaviour when at max positive quirks - if(positive_count > MAX_QUIRKS || (positive_count == MAX_QUIRKS && value > 0)) // Yogs - to_chat(user, span_warning("You can't have more than [MAX_QUIRKS] positive quirks!")) - return - if(balance - value < 0) - to_chat(user, span_warning("You don't have enough balance to gain this quirk!")) - return - all_quirks += quirk - SetQuirks(user) - if("reset") - all_quirks = list() - SetQuirks(user) - else - SetQuirks(user) - return TRUE +/datum/preferences/proc/validate_quirks() + if(GetQuirkBalance() < 0) + all_quirks = list() - switch(href_list["task"]) - if("random") - switch(href_list["preference"]) - if("name") - real_name = pref_species.random_name(gender,1) - if("age") - age = rand(AGE_MIN, AGE_MAX) - if("hair") - hair_color = random_short_color() - if("hair_style") - hair_style = random_hair_style(gender) - if("facial") - facial_hair_color = random_short_color() - if("facial_hair_style") - facial_hair_style = random_facial_hair_style(gender) - if("underwear") - underwear = random_underwear(gender) - if("undershirt") - undershirt = random_undershirt(gender) - if("socks") - socks = random_socks() - if(BODY_ZONE_PRECISE_EYES) - eye_color = random_eye_color() - if("s_tone") - skin_tone = random_skin_tone() - if("bag") - backbag = pick(GLOB.backbaglist) - if("all") - random_character(gender) - if("lock") - switch(href_list["preference"]) - if("u_all") - for(var/i in random_locks) - random_locks[i] = 0; - if("l_all") - random_locks = list( - "gender" = gender, - "mcolor" = 1, - "ethcolor" = 1, - "pretcolor" = 1, - "tail_lizard" = 1, - "tail_human" = 1, - "wings" = 1, - "snout" = 1, - "horns" = 1, - "ears" = 1, - "frills" = 1, - "spines" = 1, - "body_markings" = 1, - "legs" = 1, - "caps" = 1, - "moth_wings" = 1, - "tail_polysmorph" = 1, - "teeth" = 1, - "dome" = 1, - "dorsal_tubes" = 1, - "ethereal_mark" = 1, - ) - if("gender") - random_locks["random_locks"] = gender - else - random_locks[href_list["preference"]] = !random_locks[href_list["preference"]] - - if("input") - - if(href_list["preference"] in GLOB.preferences_custom_names) - ask_for_custom_name(user,href_list["preference"]) - - - switch(href_list["preference"]) - if("ghostform") - if(unlock_content) - var/new_form = input(user, "Thanks for supporting BYOND - Choose your ghostly form:","Thanks for supporting BYOND",null) as null|anything in GLOB.ghost_forms - if(new_form) - ghost_form = new_form - if("ghostorbit") - if(unlock_content) - var/new_orbit = input(user, "Thanks for supporting BYOND - Choose your ghostly orbit:","Thanks for supporting BYOND", null) as null|anything in GLOB.ghost_orbits - if(new_orbit) - ghost_orbit = new_orbit - - if("ghostaccs") - var/new_ghost_accs = tgui_alert(usr,"Do you want your ghost to show full accessories where possible, hide accessories but still use the directional sprites where possible, or also ignore the directions and stick to the default sprites?",,list(GHOST_ACCS_FULL_NAME, GHOST_ACCS_DIR_NAME, GHOST_ACCS_NONE_NAME)) - switch(new_ghost_accs) - if(GHOST_ACCS_FULL_NAME) - ghost_accs = GHOST_ACCS_FULL - if(GHOST_ACCS_DIR_NAME) - ghost_accs = GHOST_ACCS_DIR - if(GHOST_ACCS_NONE_NAME) - ghost_accs = GHOST_ACCS_NONE - - if("ghostothers") - var/new_ghost_others = tgui_alert(usr,"Do you want the ghosts of others to show up as their own setting, as their default sprites or always as the default white ghost?",,list(GHOST_OTHERS_THEIR_SETTING_NAME, GHOST_OTHERS_DEFAULT_SPRITE_NAME, GHOST_OTHERS_SIMPLE_NAME)) - switch(new_ghost_others) - if(GHOST_OTHERS_THEIR_SETTING_NAME) - ghost_others = GHOST_OTHERS_THEIR_SETTING - if(GHOST_OTHERS_DEFAULT_SPRITE_NAME) - ghost_others = GHOST_OTHERS_DEFAULT_SPRITE - if(GHOST_OTHERS_SIMPLE_NAME) - ghost_others = GHOST_OTHERS_SIMPLE - - if("name") - var/new_name = input(user, "Choose your character's name:", "Character Preference") as text|null - if(new_name) - new_name = reject_bad_name(new_name, pref_species.allow_numbers_in_name) - if(new_name) - real_name = new_name - else - to_chat(user, "Invalid name. Your name should be at least 2 and at most [MAX_NAME_LEN] characters long. It may only contain the characters A-Z, a-z, -, ' and .") - - if("age") - var/new_age = input(user, "Choose your character's age:\n([AGE_MIN]-[AGE_MAX])", "Character Preference") as num|null - if(new_age) - age = max(min( round(text2num(new_age)), AGE_MAX),AGE_MIN) - - if("cycle_background") - background = next_list_item(background, background_options) - - if("hair") - var/new_hair = input(user, "Choose your character's hair colour:", "Character Preference","#"+hair_color) as color|null - if(new_hair) - hair_color = sanitize_hexcolor(new_hair) - - if("hair_style") - var/new_hair_style - if(gender == MALE) - new_hair_style = input(user, "Choose your character's hair style:", "Character Preference") as null|anything in GLOB.hair_styles_male_list - else if(gender == FEMALE) - new_hair_style = input(user, "Choose your character's hair style:", "Character Preference") as null|anything in GLOB.hair_styles_female_list - else - new_hair_style = input(user, "Choose your character's hair style:", "Character Preference") as null|anything in GLOB.hair_styles_list - if(new_hair_style) - hair_style = new_hair_style - - if("next_hair_style") - if (gender == MALE) - hair_style = next_list_item(hair_style, GLOB.hair_styles_male_list) - else if(gender == FEMALE) - hair_style = next_list_item(hair_style, GLOB.hair_styles_female_list) - else - hair_style = next_list_item(hair_style, GLOB.hair_styles_list) - - if("previous_hair_style") - if (gender == MALE) - hair_style = previous_list_item(hair_style, GLOB.hair_styles_male_list) - else if(gender == FEMALE) - hair_style = previous_list_item(hair_style, GLOB.hair_styles_female_list) - else - hair_style = previous_list_item(hair_style, GLOB.hair_styles_list) - - if("facial") - var/new_facial = input(user, "Choose your character's facial-hair colour:", "Character Preference","#"+facial_hair_color) as color|null - if(new_facial) - facial_hair_color = sanitize_hexcolor(new_facial) - - if("facial_hair_style") - var/new_facial_hair_style - if(gender == MALE) - new_facial_hair_style = input(user, "Choose your character's facial-hair style:", "Character Preference") as null|anything in GLOB.facial_hair_styles_male_list - else if(gender == FEMALE) - new_facial_hair_style = input(user, "Choose your character's facial-hair style:", "Character Preference") as null|anything in GLOB.facial_hair_styles_female_list - else - new_facial_hair_style = input(user, "Choose your character's facial-hair style:", "Character Preference") as null|anything in GLOB.facial_hair_styles_list - if(new_facial_hair_style) - facial_hair_style = new_facial_hair_style - - if("next_facehair_style") - if (gender == MALE) - facial_hair_style = next_list_item(facial_hair_style, GLOB.facial_hair_styles_male_list) - else if(gender == FEMALE) - facial_hair_style = next_list_item(facial_hair_style, GLOB.facial_hair_styles_female_list) - else - facial_hair_style = next_list_item(facial_hair_style, GLOB.facial_hair_styles_list) - - if("previous_facehair_style") - if (gender == MALE) - facial_hair_style = previous_list_item(facial_hair_style, GLOB.facial_hair_styles_male_list) - else if (gender == FEMALE) - facial_hair_style = previous_list_item(facial_hair_style, GLOB.facial_hair_styles_female_list) - else - facial_hair_style = previous_list_item(facial_hair_style, GLOB.facial_hair_styles_list) - - if("hair_gradient") - var/new_hair_gradient_color = input(user, "Choose your character's hair gradient colour:", "Character Preference","#"+features["gradientcolor"]) as color|null - if(new_hair_gradient_color) - features["gradientcolor"] = sanitize_hexcolor(new_hair_gradient_color) - - if("hair_gradient_style") - var/new_gradient_style - new_gradient_style = input(user, "Choose your character's hair gradient style:", "Character Preference") as null|anything in GLOB.hair_gradients_list - if(new_gradient_style) - features["gradientstyle"] = new_gradient_style - - if("next_hair_gradient_style") - features["gradientstyle"] = next_list_item(features["gradientstyle"], GLOB.hair_gradients_list) - - if("previous_hair_gradient_style") - features["gradientstyle"] = previous_list_item(features["gradientstyle"], GLOB.hair_gradients_list) - - if("underwear") - var/new_underwear - if(gender == MALE) - new_underwear = input(user, "Choose your character's underwear:", "Character Preference") as null|anything in GLOB.underwear_m - else if(gender == FEMALE) - new_underwear = input(user, "Choose your character's underwear:", "Character Preference") as null|anything in GLOB.underwear_f - else - new_underwear = input(user, "Choose your character's underwear:", "Character Preference") as null|anything in GLOB.underwear_list - if(new_underwear) - underwear = new_underwear - - if("undershirt") - var/new_undershirt - if(gender == MALE) - new_undershirt = input(user, "Choose your character's undershirt:", "Character Preference") as null|anything in GLOB.undershirt_m - else if(gender == FEMALE) - new_undershirt = input(user, "Choose your character's undershirt:", "Character Preference") as null|anything in GLOB.undershirt_f - else - new_undershirt = input(user, "Choose your character's undershirt:", "Character Preference") as null|anything in GLOB.undershirt_list - if(new_undershirt) - undershirt = new_undershirt - - if("socks") - var/new_socks - new_socks = input(user, "Choose your character's socks:", "Character Preference") as null|anything in GLOB.socks_list - if(new_socks) - socks = new_socks - - if(BODY_ZONE_PRECISE_EYES) - var/new_eyes = input(user, "Choose your character's eye colour:", "Character Preference","#"+eye_color) as color|null - if(new_eyes) - eye_color = sanitize_hexcolor(new_eyes) - - if("species") - - var/result = input(user, "Select a species", "Species Selection") as null|anything in (is_mentor(user) ? (GLOB.roundstart_races + GLOB.mentor_races) : GLOB.roundstart_races) - - if(result) - var/newtype = GLOB.species_list[result] - pref_species = new newtype() - //Now that we changed our species, we must verify that the mutant colour is still allowed. - var/temp_hsv = RGBtoHSV(features["mcolor"]) - if(features["mcolor"] == "#000" || (!(MUTCOLORS_PARTSONLY in pref_species.species_traits) && ReadHSV(temp_hsv)[3] < ReadHSV("#3a3a3a")[3])) - features["mcolor"] = pref_species.default_color - var/CQ - for(var/Q in all_quirks) - var/quirk_type = SSquirks.quirks[Q] - var/datum/quirk/quirk = new quirk_type(no_init = TRUE) - CQ = quirk.check_quirk(src) - if(CQ) - all_quirks -= Q - to_chat(user, span_danger(CQ)) - if(GetQuirkBalance() < 0) - to_chat(user, span_danger("Your quirk balance is now negative, and you will need to re-balance it or all quirks will be disabled.")) - - if("mcolor") - var/new_mutantcolor = input(user, "Choose your character's alien/mutant color:", "Character Preference","#"+features["mcolor"]) as color|null - if(new_mutantcolor) - var/temp_hsv = RGBtoHSV(new_mutantcolor) - if(new_mutantcolor == "#000000") - features["mcolor"] = pref_species.default_color - else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV("#3a3a3a")[3]) // mutantcolors must be bright, but only if they affect the skin - features["mcolor"] = sanitize_hexcolor(new_mutantcolor) - else - to_chat(user, span_danger("Invalid color. Your color is not bright enough.")) - - if("ethcolor") - var/new_etherealcolor = input(user, "Choose your ethereal color", "Character Preference") as null|anything in GLOB.color_list_ethereal - if(new_etherealcolor) - features["ethcolor"] = GLOB.color_list_ethereal[new_etherealcolor] - - if("pretcolor") - var/new_preterniscolor = input(user, "Choose your preternis color", "Character Preference") as null|anything in GLOB.color_list_preternis - if(new_preterniscolor) - features["pretcolor"] = GLOB.color_list_preternis[new_preterniscolor] - - if("tail_lizard") - var/new_tail - new_tail = input(user, "Choose your character's tail:", "Character Preference") as null|anything in GLOB.tails_list_lizard - if(new_tail) - features["tail_lizard"] = new_tail - - if("tail_polysmorph") - var/new_tail - new_tail = input(user, "Choose your character's tail:", "Character Preference") as null|anything in GLOB.tails_list_polysmorph - if(new_tail) - features["tail_polysmorph"] = new_tail - - if("tail_human") - var/new_tail - new_tail = input(user, "Choose your character's tail:", "Character Preference") as null|anything in GLOB.tails_list_human - if(new_tail) - features["tail_human"] = new_tail - - if("snout") - var/new_snout - new_snout = input(user, "Choose your character's snout:", "Character Preference") as null|anything in GLOB.snouts_list - if(new_snout) - features["snout"] = new_snout - - if("horns") - var/new_horns - new_horns = input(user, "Choose your character's horns:", "Character Preference") as null|anything in GLOB.horns_list - if(new_horns) - features["horns"] = new_horns - - if("ears") - var/new_ears - new_ears = input(user, "Choose your character's ears:", "Character Preference") as null|anything in GLOB.ears_list - if(new_ears) - features["ears"] = new_ears - - if("wings") - var/new_wings - new_wings = input(user, "Choose your character's wings:", "Character Preference") as null|anything in GLOB.r_wings_list - if(new_wings) - features["wings"] = new_wings - - if("frills") - var/new_frills - new_frills = input(user, "Choose your character's frills:", "Character Preference") as null|anything in GLOB.frills_list - if(new_frills) - features["frills"] = new_frills - - if("spines") - var/new_spines - new_spines = input(user, "Choose your character's spines:", "Character Preference") as null|anything in GLOB.spines_list - if(new_spines) - features["spines"] = new_spines - - if("body_markings") - var/new_body_markings - new_body_markings = input(user, "Choose your character's body markings:", "Character Preference") as null|anything in GLOB.body_markings_list - if(new_body_markings) - features["body_markings"] = new_body_markings - - if("legs") - var/new_legs - new_legs = input(user, "Choose your character's legs:", "Character Preference") as null|anything in GLOB.legs_list - if(new_legs) - features["legs"] = new_legs - - if("moth_wings") - var/new_moth_wings - new_moth_wings = input(user, "Choose your character's wings:", "Character Preference") as null|anything in GLOB.moth_wings_list - if(new_moth_wings) - features["moth_wings"] = new_moth_wings - - if("teeth") - var/new_teeth - new_teeth = input(user, "Choose your character's teeth:", "Character Preference") as null|anything in GLOB.teeth_list - if(new_teeth) - features["teeth"] = new_teeth - - if("dome") - var/new_dome - new_dome = input(user, "Choose your character's dome:", "Character Preference") as null|anything in GLOB.dome_list - if(new_dome) - features["dome"] = new_dome - - if("dorsal_tubes") - var/new_dorsal_tubes - new_dorsal_tubes = input(user, "Choose if your character has dorsal tubes:", "Character Preference") as null|anything in GLOB.dorsal_tubes_list - if(new_dorsal_tubes) - features["dorsal_tubes"] = new_dorsal_tubes - - if("ethereal_mark") - var/new_ethereal_mark - new_ethereal_mark = input(user, "Choose if your character has a facial mark", "Character Preference") as null|anything in GLOB.ethereal_mark_list - if(new_ethereal_mark) - features["ethereal_mark"] = new_ethereal_mark - - if("pod_hair") - var/new_pod_hair - new_pod_hair = input(user, "Choose the style of your head vegitation", "Character Preference") as null|anything in GLOB.pod_hair_list - if(new_pod_hair) - features["pod_hair"] = new_pod_hair - features["pod_flower"] = new_pod_hair - if("pod_hair_color") - var/new_hair = input(user, "Choose your character's \"hair\" colour:", "Character Preference","#"+hair_color) as color|null - if(new_hair) - var/temp_hsv = RGBtoHSV(new_hair) - if(new_hair == "#000000") - hair_color = pref_species.default_color - to_chat(user, span_danger("Invalid \"hair\" color. Your color is not bright enough.")) - else if(ReadHSV(temp_hsv)[3] >= ReadHSV("#3a3a3a")[3]) // mutantcolors must be bright, but only if they affect the skin - hair_color = sanitize_hexcolor(new_hair) - else - to_chat(user, span_danger("Invalid \"hair\" color. Your color is not bright enough.")) - if("pod_flower_color") - var/new_facial = input(user, "Choose your character's head flower colour:", "Character Preference","#"+facial_hair_color) as color|null - if(new_facial) - var/temp_hsv = RGBtoHSV(new_facial) - if(new_facial == "#000000") - facial_hair_color = pref_species.default_color - to_chat(user, span_danger("Invalid \"hair\" color. Your color is not bright enough.")) - else if(ReadHSV(temp_hsv)[3] >= ReadHSV("#3a3a3a")[3]) // mutantcolors must be bright, but only if they affect the skin - facial_hair_color = sanitize_hexcolor(new_facial) - else - to_chat(user, span_danger("Invalid head flower color. Your color is not bright enough.")) - if("ipc_screen") - var/new_ipc_screen - - new_ipc_screen = input(user, "Choose your character's screen:", "Character Preference") as null|anything in GLOB.ipc_screens_list - - if(new_ipc_screen) - features["ipc_screen"] = new_ipc_screen - - if("ipc_antenna") - var/new_ipc_antenna - - new_ipc_antenna = input(user, "Choose your character's antenna:", "Character Preference") as null|anything in GLOB.ipc_antennas_list - - if(new_ipc_antenna) - features["ipc_antenna"] = new_ipc_antenna - - if("ipc_chassis") - var/new_ipc_chassis - - new_ipc_chassis = input(user, "Choose your character's chassis:", "Character Preference") as null|anything in GLOB.ipc_chassis_list - - if(new_ipc_chassis) - features["ipc_chassis"] = new_ipc_chassis - - if("plasmaman_helmet") - var/new_plasmaman_helmet - - new_plasmaman_helmet = input(user, "Choose your character's plasmaman helmet style:", "Character Preference") as null|anything in GLOB.plasmaman_helmet_list - if(new_plasmaman_helmet) - features["plasmaman_helmet"] = new_plasmaman_helmet - - if("s_tone") - var/new_s_tone = input(user, "Choose your character's skin-tone:", "Character Preference") as null|anything in GLOB.skin_tones - if(new_s_tone) - skin_tone = new_s_tone - - if("ooccolor") - var/new_ooccolor = input(user, "Choose your OOC colour:", "Game Preference",ooccolor) as color|null - if(new_ooccolor) - ooccolor = new_ooccolor - - if("asaycolor") - var/new_asaycolor = input(user, "Choose your ASAY color:", "Game Preference",asaycolor) as color|null - if(new_asaycolor) - asaycolor = new_asaycolor - - if("bag") - var/new_backbag = input(user, "Choose your character's style of bag:", "Character Preference") as null|anything in GLOB.backbaglist - if(new_backbag) - backbag = new_backbag - - if("suit") - jumpsuit_style = jumpsuit_style == PREF_SUIT ? PREF_SKIRT : PREF_SUIT - - if("uplink_loc") - var/new_loc = input(user, "Choose your character's traitor uplink spawn location:", "Character Preference") as null|anything in GLOB.uplink_spawn_loc_list - if(new_loc) - uplink_spawn_loc = new_loc - - if("ai_core_icon") - var/ai_core_icon = input(user, "Choose your preferred AI core display screen:", "AI Core Display Screen Selection") as null|anything in GLOB.ai_core_display_screens - "Portrait" - if(ai_core_icon) - preferred_ai_core_display = ai_core_icon - - if("sec_dept") - var/department = input(user, "Choose your preferred security department:", "Security Departments") as null|anything in GLOB.security_depts_prefs - if(department) - prefered_security_department = department - - if("eng_dept") - var/department = input(user, "Choose your preferred engineering department:", "Engineering Departments") as null|anything in GLOB.engineering_depts_prefs - if(department) - prefered_engineering_department = department - - if("accent") - var/aksent = input(user,"Choose your accent:","Available Accents") as null|anything in (assoc_list_strip_value(strings("accents.json", "accent_file_names", directory = "strings/accents")) + "None") - if(aksent) - if(aksent == "None") - accent = initial(accent) - else - accent = aksent - if ("preferred_map") - var/maplist = list() - var/default = "Default" - if (config.defaultmap) - default += " ([config.defaultmap.map_name])" - for (var/M in config.maplist) - var/datum/map_config/VM = config.maplist[M] - if(!VM.votable) - continue - var/friendlyname = "[VM.map_name] " - if (VM.voteweight <= 0) - friendlyname += " (disabled)" - maplist[friendlyname] = VM.map_name - maplist[default] = null - var/pickedmap = input(user, "Choose your preferred map. This will be used to help weight random map selection.", "Character Preference") as null|anything in maplist - if (pickedmap) - preferred_map = maplist[pickedmap] - - if ("clientfps") - var/desiredfps = input(user, "Choose your desired fps. (0 = synced with server tick rate (currently:[world.fps]))", "Character Preference", clientfps) as null|num - if (!isnull(desiredfps)) - clientfps = desiredfps - parent.fps = desiredfps - if("ui") - var/pickedui = input(user, "Choose your UI style.", "Character Preference", UI_style) as null|anything in GLOB.available_ui_styles - if(pickedui) - UI_style = pickedui - if (parent && parent.mob && parent.mob.hud_used) - parent.mob.hud_used.update_ui_style(ui_style2icon(UI_style)) - if("pda_style") - var/pickedPDAStyle = input(user, "Choose your PDA style.", "Character Preference", pda_style) as null|anything in GLOB.pda_styles - if(pickedPDAStyle) - pda_style = pickedPDAStyle - if("pda_color") - var/pickedPDAColor = input(user, "Choose your PDA Interface color.", "Character Preference",pda_color) as color|null - if(pickedPDAColor) - pda_color = pickedPDAColor - if("pda_theme") - var/pickedPDATheme = input(user, "Choose your PDA Interface theme.", "Character Preference", pda_theme) as null|anything in GLOB.pda_themes - if(pickedPDATheme) - pda_theme = pickedPDATheme - if("id_in_pda") - id_in_pda = !id_in_pda - if("skillcape") - var/list/selectablecapes = list() - var/max_eligable = TRUE - for(var/id in GLOB.skillcapes) - var/datum/skillcape/A = GLOB.skillcapes[id] - if(!A.job) - continue - if(user.client.prefs.exp[A.job] >= A.minutes) - selectablecapes += A - else - max_eligable = FALSE - if(max_eligable) - selectablecapes += GLOB.skillcapes["max"] - - if(!selectablecapes.len) - to_chat(user, "You have no availiable skillcapes!") - return - var/pickedskillcape = input(user, "Choose your Skillcape.", "Character Preference") as null|anything in (list("None") + selectablecapes) - if(!pickedskillcape) - return - if(pickedskillcape == "None") - skillcape_id = "None" - else - var/datum/skillcape/cape = pickedskillcape - skillcape_id = cape.id - if("flare") - flare = !flare - if("map") - map = !map - if("bar_choice") - var/pickedbar = input(user, "Choose your bar.", "Character Preference", bar_choice) as null|anything in (GLOB.potential_box_bars|"Random") - if(!pickedbar) - return - bar_choice = pickedbar - if ("max_chat_length") - var/desiredlength = input(user, "Choose the max character length of shown Runechat messages. Valid range is 1 to [CHAT_MESSAGE_MAX_LENGTH] (default: [initial(max_chat_length)]))", "Character Preference", max_chat_length) as null|num - if (!isnull(desiredlength)) - max_chat_length = clamp(desiredlength, 1, CHAT_MESSAGE_MAX_LENGTH) - if("alternative_announcers") - disable_alternative_announcers = !disable_alternative_announcers - if("balloon_alerts") - disable_balloon_alerts = !disable_balloon_alerts - else - switch(href_list["preference"]) - if("publicity") - if(unlock_content) - toggles ^= MEMBER_PUBLIC - if("gender") - var/pickedGender = input(user, "Choose your gender.", "Character Preference", gender) as null|anything in friendlyGenders - if(pickedGender && friendlyGenders[pickedGender] != gender) - gender = friendlyGenders[pickedGender] - underwear = random_underwear(gender) - undershirt = random_undershirt(gender) - socks = random_socks() - facial_hair_style = random_facial_hair_style(gender) - hair_style = random_hair_style(gender) - - if("hotkeys") - hotkeys = !hotkeys - if(hotkeys) - winset(user, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED]") - else - winset(user, null, "input.focus=true input.background-color=[COLOR_INPUT_DISABLED]") - - if("keybindings_capture") - var/datum/keybinding/kb = GLOB.keybindings_by_name[href_list["keybinding"]] - var/old_key = href_list["old_key"] - CaptureKeybinding(user, kb, old_key) - return - - if("keybindings_set") - var/kb_name = href_list["keybinding"] - if(!kb_name) - user << browse(null, "window=capturekeypress") - ShowChoices(user) - return - - var/clear_key = text2num(href_list["clear_key"]) - var/old_key = href_list["old_key"] - if(clear_key) - if(key_bindings[old_key]) - key_bindings[old_key] -= kb_name - LAZYADD(key_bindings["Unbound"], kb_name) - if(!length(key_bindings[old_key])) - key_bindings -= old_key - user << browse(null, "window=capturekeypress") - user.client.set_macros() - save_preferences() - ShowChoices(user) - return - - var/new_key = uppertext(href_list["key"]) - var/AltMod = text2num(href_list["alt"]) ? "Alt" : "" - var/CtrlMod = text2num(href_list["ctrl"]) ? "Ctrl" : "" - var/ShiftMod = text2num(href_list["shift"]) ? "Shift" : "" - var/numpad = text2num(href_list["numpad"]) ? "Numpad" : "" - // var/key_code = text2num(href_list["key_code"]) - - if(GLOB._kbMap[new_key]) - new_key = GLOB._kbMap[new_key] - - var/full_key - switch(new_key) - if("Alt") - full_key = "[new_key][CtrlMod][ShiftMod]" - if("Ctrl") - full_key = "[AltMod][new_key][ShiftMod]" - if("Shift") - full_key = "[AltMod][CtrlMod][new_key]" - else - full_key = "[AltMod][CtrlMod][ShiftMod][numpad][new_key]" - if(kb_name in key_bindings[full_key]) //We pressed the same key combination that was already bound here, so let's remove to re-add and re-sort. - key_bindings[full_key] -= kb_name - if(key_bindings[old_key]) - key_bindings[old_key] -= kb_name - if(!length(key_bindings[old_key])) - key_bindings -= old_key - key_bindings[full_key] += list(kb_name) - key_bindings[full_key] = sortList(key_bindings[full_key]) - - user << browse(null, "window=capturekeypress") - user.client.set_macros() - save_preferences() - - if("keybindings_reset") - var/choice = tgalert(user, "Would you prefer 'hotkey' or 'classic' defaults?", "Setup keybindings", "Hotkey", "Classic", "Cancel") - if(choice == "Cancel") - ShowChoices(user) - return - hotkeys = (choice == "Hotkey") - key_bindings = (hotkeys) ? deepCopyList(GLOB.hotkey_keybinding_list_by_key) : deepCopyList(GLOB.classic_keybinding_list_by_key) - user.client.set_macros() - if("chat_on_map") - chat_on_map = !chat_on_map - if("see_chat_non_mob") - see_chat_non_mob = !see_chat_non_mob - if("see_rc_emotes") - see_rc_emotes = !see_rc_emotes - if("action_buttons") - buttons_locked = !buttons_locked - if("tgui_fancy") - tgui_fancy = !tgui_fancy - if("tgui_lock") - tgui_lock = !tgui_lock - if("winflash") - windowflashing = !windowflashing - - //here lies the badmins - if("hear_adminhelps") - user.client.toggleadminhelpsound() - if("hear_prayers") - user.client.toggle_prayer_sound() - if("announce_login") - user.client.toggleannouncelogin() - if("combohud_lighting") - toggles ^= COMBOHUD_LIGHTING - if("toggle_dead_chat") - user.client.deadchat() - if("toggle_radio_chatter") - user.client.toggle_hear_radio() - if("toggle_split_admin_tabs") - extra_toggles ^= SPLIT_ADMIN_TABS - if("toggle_fast_mc_refresh") - extra_toggles ^= FAST_MC_REFRESH - if("toggle_prayers") - user.client.toggleprayers() - if("toggle_deadmin_always") - toggles ^= DEADMIN_ALWAYS - if("toggle_deadmin_antag") - toggles ^= DEADMIN_ANTAGONIST - if("toggle_deadmin_head") - toggles ^= DEADMIN_POSITION_HEAD - if("toggle_deadmin_security") - toggles ^= DEADMIN_POSITION_SECURITY - if("toggle_deadmin_silicon") - toggles ^= DEADMIN_POSITION_SILICON - if("toggle_deadmin_critical") - toggles ^= DEADMIN_POSITION_CRITICAL - - - if("be_special") - var/be_special_type = href_list["be_special_type"] - if(be_special_type in be_special) - be_special -= be_special_type - else - be_special += be_special_type - - if("name") - be_random_name = !be_random_name - - if("all") - be_random_body = !be_random_body - - if("persistent_scars") - persistent_scars = !persistent_scars - - if("clear_scars") - var/path = "data/player_saves/[user.ckey[1]]/[user.ckey]/scars.sav" - fdel(path) - to_chat(user, span_notice("All scar slots cleared.")) - - if("hear_midis") - toggles ^= SOUND_MIDI - - if("lobby_music") - toggles ^= SOUND_LOBBY - if((toggles & SOUND_LOBBY) && user.client && isnewplayer(user)) - user.client.playtitlemusic() - else - user.stop_sound_channel(CHANNEL_LOBBYMUSIC) - - if("ghost_ears") - chat_toggles ^= CHAT_GHOSTEARS - - if("ghost_sight") - chat_toggles ^= CHAT_GHOSTSIGHT - - if("ghost_whispers") - chat_toggles ^= CHAT_GHOSTWHISPER - - if("ghost_radio") - chat_toggles ^= CHAT_GHOSTRADIO - - if("ghost_pda") - chat_toggles ^= CHAT_GHOSTPDA - - if("income_pings") - chat_toggles ^= CHAT_BANKCARD - - if("pull_requests") - chat_toggles ^= CHAT_PULLR - - if("allow_midround_antag") - toggles ^= MIDROUND_ANTAG - - if("parallaxup") - parallax = WRAP(parallax + 1, PARALLAX_INSANE, PARALLAX_DISABLE + 1) - if (parent && parent.mob && parent.mob.hud_used) - parent.mob.hud_used.update_parallax_pref(parent.mob) - - if("parallaxdown") - parallax = WRAP(parallax - 1, PARALLAX_INSANE, PARALLAX_DISABLE + 1) - if (parent && parent.mob && parent.mob.hud_used) - parent.mob.hud_used.update_parallax_pref(parent.mob) - - if("ambientocclusion") - ambientocclusion = !ambientocclusion - if(parent && parent.screen && parent.screen.len) - var/atom/movable/screen/plane_master/game_world/PM = locate(/atom/movable/screen/plane_master/game_world) in parent.screen - PM.backdrop(parent.mob) - - if("auto_fit_viewport") - auto_fit_viewport = !auto_fit_viewport - if(auto_fit_viewport && parent) - parent.fit_viewport() - - if("widescreenpref") - widescreenpref = !widescreenpref - user.client.view_size.setDefault(getScreenSize(widescreenpref)) - - if("pixel_size") - switch(pixel_size) - if(PIXEL_SCALING_AUTO) - pixel_size = PIXEL_SCALING_1X - if(PIXEL_SCALING_1X) - pixel_size = PIXEL_SCALING_1_2X - if(PIXEL_SCALING_1_2X) - pixel_size = PIXEL_SCALING_2X - if(PIXEL_SCALING_2X) - pixel_size = PIXEL_SCALING_3X - if(PIXEL_SCALING_3X) - pixel_size = PIXEL_SCALING_AUTO - user.client.view_size.apply() //Let's winset() it so it actually works - - if("scaling_method") - switch(scaling_method) - if(SCALING_METHOD_NORMAL) - scaling_method = SCALING_METHOD_DISTORT - if(SCALING_METHOD_DISTORT) - scaling_method = SCALING_METHOD_BLUR - if(SCALING_METHOD_BLUR) - scaling_method = SCALING_METHOD_NORMAL - user.client.view_size.setZoomMode() - - - if("save") - save_preferences() - save_character() - - if("load") - load_preferences() - load_character() - - if("changeslot") - if(!load_character(text2num(href_list["num"]))) - random_character() - real_name = random_unique_name(gender) - save_character() - - if("tab") - if (href_list["tab"]) - current_tab = text2num(href_list["tab"]) - - if("mood") - yogtoggles ^= PREF_MOOD - - if("moodtailwagging") - mood_tail_wagging = !mood_tail_wagging - // yogs end - - ShowChoices(user) - return 1 - -/datum/preferences/proc/copy_to(mob/living/carbon/human/character, icon_updates = 1, roundstart_checks = TRUE) - if(be_random_name) - real_name = pref_species.random_name(gender) - - if(be_random_body) - random_character(gender) - - if(roundstart_checks) - if(CONFIG_GET(flag/humans_need_surnames) && (pref_species.id == "human")) - var/firstspace = findtext(real_name, " ") - var/name_length = length(real_name) - if(!firstspace) //we need a surname - real_name += " [pick(GLOB.last_names)]" - else if(firstspace == name_length) - real_name += "[pick(GLOB.last_names)]" - - character.real_name = real_name - character.name = character.real_name - - character.gender = gender - character.age = age - - character.eye_color = eye_color - var/obj/item/organ/eyes/organ_eyes = character.getorgan(/obj/item/organ/eyes) - if(organ_eyes) - if(!initial(organ_eyes.eye_color)) - organ_eyes.eye_color = eye_color - organ_eyes.old_eye_color = eye_color - character.hair_color = hair_color - character.facial_hair_color = facial_hair_color - character.grad_color = features["gradientcolor"] - - character.skin_tone = skin_tone - character.hair_style = hair_style - character.facial_hair_style = facial_hair_style - character.grad_style = features["gradientstyle"] - character.underwear = underwear - character.undershirt = undershirt - character.socks = socks - - character.backbag = backbag - - character.jumpsuit_style = jumpsuit_style - character.id_in_pda = id_in_pda - - var/datum/species/chosen_species - chosen_species = pref_species.type - if(roundstart_checks && !(pref_species.id in GLOB.roundstart_races) && (!(pref_species.id in GLOB.mentor_races) && !is_mentor(character)) && !(pref_species.id in (CONFIG_GET(keyed_list/roundstart_no_hard_check)))) - chosen_species = /datum/species/human - pref_species = new /datum/species/human - save_character() - - character.dna.features = features.Copy() - character.set_species(chosen_species, icon_update = FALSE, pref_load = TRUE) - character.dna.real_name = character.real_name +/datum/preferences/proc/apply_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE) + character.dna.features = list() + + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (preference.savefile_identifier != PREFERENCE_CHARACTER) + continue - if("tail_lizard" in pref_species.default_features) - character.dna.species.mutant_bodyparts |= "tail_lizard" + // Dont apply if it's unique and we can't customize it + // This fixes pod hair color overwriting human hair color + if (preference.unique && !preference.can_apply(src)) + continue - if("tail_polysmorph" in pref_species.default_features) - character.dna.species.mutant_bodyparts |= "tail_polysmorph" + preference.apply_to_human(character, read_preference(preference.type)) + + character.dna.real_name = character.real_name if(icon_updates) character.icon_render_key = null //turns out if you don't set this to null update_body_parts does nothing, since it assumes the operation was cached @@ -2370,39 +534,39 @@ GLOBAL_LIST_EMPTY(preferences_datums) character.update_hair() character.update_body_parts() -/datum/preferences/proc/get_default_name(name_id) - switch(name_id) - if("human") - return random_unique_name() - if("ai") - return pick(GLOB.ai_names) - if("cyborg") - return DEFAULT_CYBORG_NAME - if("clown") - return pick(GLOB.clown_names) - if("mime") - return pick(GLOB.mime_names) - if("religion") - return DEFAULT_RELIGION - if("deity") - return DEFAULT_DEITY - return random_unique_name() - -/datum/preferences/proc/ask_for_custom_name(mob/user,name_id) - var/namedata = GLOB.preferences_custom_names[name_id] - if(!namedata) - return +/// Inverts the key_bindings list such that it can be used for key_bindings_by_key +/datum/preferences/proc/get_key_bindings_by_key(list/key_bindings) + var/list/output = list() - var/raw_name = input(user, "Choose your character's [namedata["qdesc"]]:","Character Preference") as text|null - if(!raw_name) - if(namedata["allow_null"]) - custom_names[name_id] = get_default_name(name_id) - else - return - else - var/sanitized_name = reject_bad_name(raw_name,namedata["allow_numbers"]) - if(!sanitized_name) - to_chat(user, "Invalid name. Your name should be at least 2 and at most [MAX_NAME_LEN] characters long. It may only contain the characters A-Z, a-z,[namedata["allow_numbers"] ? ",0-9," : ""] -, ' and .") - return - else - custom_names[name_id] = sanitized_name + for (var/action in key_bindings) + for (var/key in key_bindings[action]) + LAZYADD(output[key], action) + + return output + +/// Returns the default `randomise` variable ouptut +/datum/preferences/proc/get_default_randomization() + var/list/default_randomization = list() + + for (var/preference_key in GLOB.preference_entries_by_key) + var/datum/preference/preference = GLOB.preference_entries_by_key[preference_key] + if (preference.is_randomizable() && preference.randomize_by_default) + default_randomization[preference_key] = RANDOM_ENABLED + + return default_randomization + + +// yogs procs + +/datum/preferences/proc/GetPlayerAltTitle(datum/job/job) + return player_alt_titles.Find(job.title) > 0 \ + ? player_alt_titles[job.title] \ + : job.title + +/datum/preferences/proc/SetPlayerAltTitle(datum/job/job, new_title) + // remove existing entry + if(player_alt_titles.Find(job.title)) + player_alt_titles -= job.title + // add one if it's not default + if(job.title != new_title) + player_alt_titles[job.title] = new_title diff --git a/code/modules/client/preferences/README.md b/code/modules/client/preferences/README.md new file mode 100644 index 000000000000..45c62cc4198d --- /dev/null +++ b/code/modules/client/preferences/README.md @@ -0,0 +1,419 @@ +# Preferences (by Mothblocks) + +This does not contain all the information on specific values--you can find those as doc-comments in relevant paths, such as `/datum/preference`. Rather, this gives you an overview for creating *most* preferences, and getting your foot in the door to create more advanced ones. + +## Anatomy of a preference (A.K.A. how do I make one?) + +Most preferences consist of two parts: + +1. A `/datum/preference` type. +2. A tgui representation in a TypeScript file. + +Every `/datum/preference` requires these three values be set: +1. `category` - See [Categories](#Categories). +2. `savefile_key` - The value which will be saved in the savefile. This will also be the identifier for tgui. +3. `savefile_identifier` - Whether or not this is a character specific preference (`PREFERENCE_CHARACTER`) or one that affects the player (`PREFERENCE_PLAYER`). As an example: hair color is `PREFERENCE_CHARACTER` while your UI settings are `PREFERENCE_PLAYER`, since they do not change between characters. + +For the tgui representation, most preferences will create a `.tsx` file in `tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/`. If your preference is a character preference, make a new file in `character_preferences`. Otherwise, put it in `game_preferences`. The filename does not matter, and this file can hold multiple relevant preferences if you would like. + +From here, you will want to write code resembling: + +```ts +import { Feature } from "../base"; +export const savefile_key_here: Feature = { + name: "Preference Name Here", + component: Component, + // Necessary for game preferences, unused for others + category: "CATEGORY", + // Optional, only shown in game preferences + description: "This preference will blow your mind!", +} +``` + +`T` and `Component` depend on the type of preference you're making. Here are all common examples... + +## Numeric preferences + +Examples include age and FPS. + +A numeric preference derives from `/datum/preference/numeric`. + +```dm +/datum/preference/numeric/legs + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "legs" + minimum = 1 + maximum = 8 +``` + +You can optionally provide a `step` field. This value is 1 by default, meaning only integers are accepted. + +Your `.tsx` file would look like: + +```ts +import { Feature, FeatureNumberInput } from "../base"; +export const legs: Feature = { + name: "Legs", + component: FeatureNumberInput, +} +``` + +## Toggle preferences + +Examples include enabling tooltips. + +```dm +/datum/preference/toggle/enable_breathing + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "enable_breathing" + // Optional, TRUE by default + default_value = FALSE +``` + +Your `.tsx` file would look like: + +```ts +import { CheckboxInput, FeatureToggle } from "../base"; +export const enable_breathing: FeatureToggle = { + name: "Enable breathing", + component: CheckboxInput, +} +``` + +## Choiced preferences +A choiced preference is one where the only options are in a distinct few amount of choices. Examples include skin tone, shirt, and UI style. + +To create one, derive from `/datum/preference/choiced`. + +```dm +/datum/preference/choiced/favorite_drink + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "favorite_drink" +``` + +Now we need to tell the game what the choices are. We do this by overriding `init_possible_values()`. This will return a list of possible options. + +```dm +/datum/preference/choiced/favorite_drink/init_possible_values() + return list( + "Milk", + "Cola", + "Water", + ) +``` + +Your `.tsx` file would then look like: + +```tsx +import { FeatureChoiced, FeatureDropdownInput } from "../base"; +export const favorite_drink: FeatureChoiced = { + name: "Favorite drink", + component: FeatureDropdownInput, +}; +``` + +This will create a dropdown input for your preference. + +### Choiced preferences - Icons +Choiced preferences can generate icons. This is how the clothing/species preferences work, for instance. However, if we just want a basic dropdown input with icons, it would look like this: + +```dm +/datum/preference/choiced/favorite_drink + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "favorite_drink" + should_generate_icons = TRUE // NEW! This is necessary. +// Instead of returning a flat list, this now returns an assoc list +// of values to icons. +/datum/preference/choiced/favorite_drink/init_possible_values() + return list( + "Milk" = icon('drinks.dmi', "milk"), + "Cola" = icon('drinks.dmi', "cola"), + "Water" = icon('drinks.dmi', "water"), + ) +``` + +Then, change your `.tsx` file to look like: + +```tsx +import { FeatureChoiced, FeatureIconnedDropdownInput } from "../base"; +export const favorite_drink: FeatureChoiced = { + name: "Favorite drink", + component: FeatureIconnedDropdownInput, +}; +``` + +### Choiced preferences - Display names +Sometimes the values you want to save in code aren't the same as the ones you want to display. You can specify display names to change this. + +The only thing you will add is "compiled data". + +```dm +/datum/preference/choiced/favorite_drink/compile_constant_data() + var/list/data = ..() + // An assoc list of values to display names + data[CHOICED_PREFERENCE_DISPLAY_NAMES] = list( + "Milk" = "Delicious Milk", + "Cola" = "Crisp Cola", + "Water" = "Plain Ol' Water", + ) + return data +``` + +Your `.tsx` file does not change. The UI will figure it out for you! + +## Color preferences +These refer to colors, such as your OOC color. When read, these values will be given as 6 hex digits, *without* the pound sign. + +```dm +/datum/preference/color/eyeliner_color + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "eyeliner_color" +``` + +Your `.tsx` file would look like: + +```ts +import { FeatureColorInput, Feature } from "../base"; +export const eyeliner_color: Feature = { + name: "Eyeliner color", + component: FeatureColorInput, +}; +``` + +## Name preferences +These refer to an alternative name. Examples include AI names and backup human names. + +These exist in `code/modules/client/preferences/names.dm`. + +These do not need a `.ts` file, and will be created in the UI automatically. + +```dm +/datum/preference/name/doctor + savefile_key = "doctor_name" + // The name on the UI + explanation = "Doctor name" + // This groups together with anything else with the same group + group = "medicine" + // Optional, if specified the UI will show this name actively + // when the player is a medical doctor. + relevant_job = /datum/job/medical_doctor +``` + +## Making your preference do stuff + +There are a handful of procs preferences can use to act on their own: + +```dm +/// Apply this preference onto the given client. +/// Called when the savefile_identifier == PREFERENCE_PLAYER. +/datum/preference/proc/apply_to_client(client/client, value) +/// Fired when the preference is updated. +/// Calls apply_to_client by default, but can be overridden. +/datum/preference/proc/apply_to_client_updated(client/client, value) +/// Apply this preference onto the given human. +/// Must be overriden by subtypes. +/// Called when the savefile_identifier == PREFERENCE_CHARACTER. +/datum/preference/proc/apply_to_human(mob/living/carbon/human/target, value) +``` + +For example, `/datum/preference/numeric/age` contains: + +```dm +/datum/preference/numeric/age/apply_to_human(mob/living/carbon/human/target, value) + target.age = value +``` + +If your preference is `PREFERENCE_CHARACTER`, it MUST override `apply_to_human`, even if just to immediately `return`. + +You can also read preferences directly with `preferences.read_preference(/datum/preference/type/here)`, which will return the stored value. + +## Categories +Every preference needs to be in a `category`. These can be found in `code/__DEFINES/preferences.dm`. + +```dm +/// These will be shown in the character sidebar, but at the bottom. +#define PREFERENCE_CATEGORY_FEATURES "features" +/// Any preferences that will show to the sides of the character in the setup menu. +#define PREFERENCE_CATEGORY_CLOTHING "clothing" +/// Preferences that will be put into the 3rd list, and are not contextual. +#define PREFERENCE_CATEGORY_NON_CONTEXTUAL "non_contextual" +/// Will be put under the game preferences window. +#define PREFERENCE_CATEGORY_GAME_PREFERENCES "game_preferences" +/// These will show in the list to the right of the character preview. +#define PREFERENCE_CATEGORY_SECONDARY_FEATURES "secondary_features" +/// These are preferences that are supplementary for main features, +/// such as hair color being affixed to hair. +#define PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES "supplemental_features" +``` + +![Preference categories for the main page](https://raw.githubusercontent.com/tgstation/documentation-assets/main/preferences/preference_categories.png) + +> SECONDARY_FEATURES or NON_CONTEXTUAL? +Secondary features tend to be species specific. Non contextual features shouldn't change much from character to character. + +## Default values and randomization + +There are three procs to be aware of in regards to this topic: + +- `create_default_value()`. This is used when a value deserializes improperly or when a new character is created. +- `create_informed_default_value(datum/preferences/preferences)` - Used for more complicated default values, like how names require the gender. Will call `create_default_value()` by default. +- `create_random_value(datum/preferences/preferences)` - Explicitly used for random values, such as when a character is being randomized. + +`create_default_value()` in most preferences will create a random value. If this is a problem (like how default characters should always be human), you can override `create_default_value()`. By default (without overriding `create_random_value`), random values are just default values. + +## Advanced - Server data + +As previewed in [the display names implementation](#Choiced-preferences---Display-names), there exists a `compile_constant_data()` proc you can override. + +Compiled data is used wherever the server needs to give the client some value it can't figure out on its own. Skin tones use this to tell the client what colors they represent, for example. + +Compiled data is sent to the `serverData` field in the `FeatureValueProps`. + +## Advanced - Creating your own tgui component + +If you have good knowledge with tgui (especially TypeScript), you'll be able to create your own component to represent preferences. + +The `component` field in a feature accepts __any__ component that accepts `FeatureValueProps`. + +This will give you the fields: + +```ts +act: typeof sendAct, +featureId: string, +handleSetValue: (newValue: TSending) => void, +serverData: TServerData | undefined, +shrink?: boolean, +value: TReceiving, +``` + +`act` is the same as the one you get from `useBackend`. + +`featureId` is the savefile_key of the feature. + +`handleSetValue` is a function that, when called, will tell the server the new value, as well as changing the value immediately locally. + +`serverData` is the [server data](#Advanced---Server-data), if it has been fetched yet (and exists). + +`shrink` is whether or not the UI should appear smaller. This is only used for supplementary features. + +`value` is the current value, could be predicted (meaning that the value was changed locally, but has not yet reached the server). + +For a basic example of how this can look, observe `CheckboxInput`: + +```tsx +export const CheckboxInput = ( + props: FeatureValueProps +) => { + return ( { + props.handleSetValue(!props.value); + }} + />); +}; +``` + +## Advanced - Middleware +A `/datum/preference_middleware` is a way to inject your own data at specific points, as well as hijack actions. + +Middleware can hijack actions by specifying `action_delegations`: + +```dm +/datum/preference_middleware/congratulations + action_delegations = list( + "congratulate_me" = .proc/congratulate_me, + ) +/datum/preference_middleware/congratulations/proc/congratulate_me(list/params, mob/user) + to_chat(user, span_notice("Wow, you did a great job learning about middleware!")) + return TRUE +``` + +Middleware can inject its own data at several points, such as providing new UI assets, compiled data (used by middleware such as quirks to tell the client what quirks exist), etc. Look at `code/modules/client/preferences/middleware/_middleware.dm` for full information. + +--- + +## Antagonists + +In order to make an antagonist selectable, you must do a few things: + +1. Your antagonist needs an icon. +2. Your antagonist must be in a Dynamic ruleset. The ruleset must specify the antagonist as its `antag_flag`. +3. Your antagonist needs a file in `tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/filename.ts`. This file name MUST be the `antag_flag` of your ruleset, with nothing but letters remaining (e.g. "Nuclear Operative" -> `nuclearoperative`). +4. Add it to `special_roles`. + +## Creating icons + +If you are satisfied with your icon just being a dude with some clothes, then you can specify `preview_outfit` in your `/datum/antagonist`. + +Space Ninja, for example, looks like: + +```dm +/datum/antagonist/ninja + preview_outift = /datum/outfit/ninja +``` + +However, if you want to get creative, you can override `/get_preview_icon()`. This proc should return an icon of size `ANTAGONIST_PREVIEW_ICON_SIZE`x`ANTAGONIST_PREVIEW_ICON_SIZE`. + +There are some helper procs you can use as well. `render_preview_outfit(outfit_type)` will take an outfit and give you an icon of someone wearing those clothes. `finish_preview_outfit` will, given an icon, resize it appropriately and zoom in on the head. Note that this will look bad on anything that isn't a human, so if you have a non-human antagonist (such as sentient disease), just run `icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE)`. + +For inspiration, here is changeling's: + +```dm +/datum/antagonist/changeling/get_preview_icon() + var/icon/final_icon = render_preview_outfit(/datum/outfit/changeling) + var/icon/split_icon = render_preview_outfit(/datum/outfit/job/engineer) + final_icon.Shift(WEST, world.icon_size / 2) + final_icon.Shift(EAST, world.icon_size / 2) + split_icon.Shift(EAST, world.icon_size / 2) + split_icon.Shift(WEST, world.icon_size / 2) + final_icon.Blend(split_icon, ICON_OVERLAY) + return finish_preview_icon(final_icon) +``` + +...which creates: + +![Changeling icon](https://raw.githubusercontent.com/tgstation/documentation-assets/main/preferences/changeling.png) + +## Creating the tgui representation + +In the `.ts` file you created earlier, you must now give the information of your antagonist. For reference, this is the changeling's: + +```ts +import { Antagonist, Category } from "../base"; +import { multiline } from "common/string"; +const Changeling: Antagonist = { + key: "changeling", // This must be the same as your filename + name: "Changeling", + description: [ + multiline` + A highly intelligent alien predator that is capable of altering their + shape to flawlessly resemble a human. + `, + multiline` + Transform yourself or others into different identities, and buy from an + arsenal of biological weaponry with the DNA you collect. + `, + ], + category: Category.Roundstart, // Category.Roundstart, Category.Midround, or Category.Latejoin +}; +export default Changeling; +``` + +## Readying the Dynamic ruleset + +You already need to create a Dynamic ruleset, so in order to get your antagonist recognized, you just need to specify `antag_flag`. This must be unique per ruleset. + +Two other values to note are `antag_flag_override` and `antag_preference`. + +`antag_flag_override` exists for cases where you want the banned antagonist to be separate from `antag_flag`. As an example: roundstart, midround, and latejoin traitors have separate `antag_flag`, but all have `antag_flag_override = ROLE_TRAITOR`. This is because admins want to ban a player from Traitor altogether, not specific rulesets. + +If `antag_preference` is set, it will refer to that preference instead of `antag_flag`. This is used for clown operatives, which we want to be on the same preference as standard nuke ops, but must specify a unique `antag_flag` for. + +## Updating special_roles + +In `code/__DEFINES/role_preferences.dm` (the same place you'll need to make your ROLE_\* defined), simply add your antagonist to the `special_roles` assoc list. The key is your ROLE, the value is the number of days since your first game in order to play as that antagonist. diff --git a/code/modules/client/preferences/_preference.dm b/code/modules/client/preferences/_preference.dm new file mode 100644 index 000000000000..b90084afccc7 --- /dev/null +++ b/code/modules/client/preferences/_preference.dm @@ -0,0 +1,565 @@ +// Priorities must be in order! +/// The default priority level +#define PREFERENCE_PRIORITY_DEFAULT 1 + +/// The priority at which species runs, needed for external organs to apply properly. +#define PREFERENCE_PRIORITY_SPECIES 2 + +/// The priority at which gender is determined, needed for proper randomization. +#define PREFERENCE_PRIORITY_GENDER 3 + +/// The priority at which names are decided, needed for proper randomization. +#define PREFERENCE_PRIORITY_NAMES 4 + +/// The maximum preference priority, keep this updated, but don't use it for `priority`. +#define MAX_PREFERENCE_PRIORITY PREFERENCE_PRIORITY_NAMES + +/// For choiced preferences, this key will be used to set display names in constant data. +#define CHOICED_PREFERENCE_DISPLAY_NAMES "display_names" + +/// For choiced preferences, this key will be used to lock choices to a specific ckey. +#define CHOICED_PREFERENCE_KEY_LOCKED "key_locked" + +/// For main feature preferences, this key refers to a feature considered supplemental. +/// For instance, hair color being supplemental to hair. +#define SUPPLEMENTAL_FEATURE_KEY "supplemental_feature" + +/// An assoc list list of types to instantiated `/datum/preference` instances +GLOBAL_LIST_INIT(preference_entries, init_preference_entries()) + +/// An assoc list of preference entries by their `savefile_key` +GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key()) + +/proc/init_preference_entries() + var/list/output = list() + for (var/datum/preference/preference_type as anything in subtypesof(/datum/preference)) + if (initial(preference_type.abstract_type) == preference_type) + continue + output[preference_type] = new preference_type + return output + +/proc/init_preference_entries_by_key() + var/list/output = list() + for (var/datum/preference/preference_type as anything in subtypesof(/datum/preference)) + if (initial(preference_type.abstract_type) == preference_type) + continue + output[initial(preference_type.savefile_key)] = GLOB.preference_entries[preference_type] + return output + +/// Returns a flat list of preferences in order of their priority +/proc/get_preferences_in_priority_order() + var/list/preferences[MAX_PREFERENCE_PRIORITY] + + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + LAZYADD(preferences[preference.priority], preference) + + var/list/flattened = list() + for (var/index in 1 to MAX_PREFERENCE_PRIORITY) + flattened += preferences[index] + return flattened + +/// Represents an individual preference. +/datum/preference + /// The key inside the savefile to use. + /// This is also sent to the UI. + /// Once you pick this, don't change it. + var/savefile_key + + /// The category of preference, for use by the PreferencesMenu. + /// This isn't used for anything other than as a key for UI data. + /// It is up to the PreferencesMenu UI itself to interpret it. + var/category = "misc" + + /// Do not instantiate if type matches this. + var/abstract_type = /datum/preference + + /// What savefile should this preference be read from? + /// Valid values are PREFERENCE_CHARACTER and PREFERENCE_PLAYER. + /// See the documentation in [code/__DEFINES/preferences.dm]. + var/savefile_identifier + + /// The priority of when to apply this preference. + /// Used for when you need to rely on another preference. + var/priority = PREFERENCE_PRIORITY_DEFAULT + + /// If set, will be available to randomize, but only if the preference + /// is for PREFERENCE_CHARACTER. + var/can_randomize = TRUE + + /// If randomizable (PREFERENCE_CHARACTER and can_randomize), whether + /// or not to enable randomization by default. + /// This doesn't mean it'll always be random, but rather if a player + /// DOES have random body on, will this already be randomized? + var/randomize_by_default = TRUE + + /// If the selected species has this in its /datum/species/mutant_bodyparts, + /// will show the feature as selectable. + var/relevant_mutant_bodypart = null + + /// If the selected species has this in its /datum/species/species_traits, + /// will show the feature as selectable. + var/relevant_species_trait = null + + /// If the target should be checked upon applying the preference + /// For example, this is used for for podperson hair_color since it overwrites human hair_color + var/unique = FALSE + +/// Called on the saved input when retrieving. +/// Also called by the value sent from the user through UI. Do not trust it. +/// Input is the value inside the savefile, output is to tell other code +/// what the value is. +/// This is useful either for more optimal data saving or for migrating +/// older data. +/// Must be overridden by subtypes. +/// Can return null if no value was found. +/datum/preference/proc/deserialize(input, datum/preferences/preferences) + SHOULD_NOT_SLEEP(TRUE) + SHOULD_CALL_PARENT(FALSE) + CRASH("`deserialize()` was not implemented on [type]!") + +/// Called on the input while saving. +/// Input is the current value, output is what to save in the savefile. +/datum/preference/proc/serialize(input) + SHOULD_NOT_SLEEP(TRUE) + return input + +/// Produce a default, potentially random value for when no value for this +/// preference is found in the savefile. +/// Either this or create_informed_default_value must be overriden by subtypes. +/datum/preference/proc/create_default_value() + SHOULD_NOT_SLEEP(TRUE) + SHOULD_CALL_PARENT(FALSE) + CRASH("`create_default_value()` was not implemented on [type]!") + +/// Produce a default, potentially random value for when no value for this +/// preference is found in the savefile. +/// Unlike create_default_value(), will provide the preferences object if you +/// need to use it. +/// If not overriden, will call create_default_value() instead. +/datum/preference/proc/create_informed_default_value(datum/preferences/preferences) + return create_default_value() + +/// Produce a random value for the purposes of character randomization. +/// Will just create a default value by default. +/datum/preference/proc/create_random_value(datum/preferences/preferences) + return create_informed_default_value(preferences) + +/// Returns whether or not a preference can be randomized. +/datum/preference/proc/is_randomizable() + SHOULD_NOT_OVERRIDE(TRUE) + return savefile_identifier == PREFERENCE_CHARACTER && can_randomize + +/// Given a savefile, return either the saved data or an acceptable default. +/// This will write to the savefile if a value was not found with the new value. +/datum/preference/proc/read(savefile/savefile, datum/preferences/preferences) + SHOULD_NOT_OVERRIDE(TRUE) + + var/value + + if (!isnull(savefile)) + READ_FILE(savefile[savefile_key], value) + + if (isnull(value)) + return null + else + return deserialize(value, preferences) + +/// Given a savefile, writes the inputted value. +/// Returns TRUE for a successful application. +/// Return FALSE if it is invalid. +/datum/preference/proc/write(savefile/savefile, value) + SHOULD_NOT_OVERRIDE(TRUE) + + if (!is_valid(value)) + return FALSE + + if (!isnull(savefile)) + WRITE_FILE(savefile[savefile_key], serialize(value)) + + return TRUE + +/// Apply this preference onto the given client. +/// Called when the savefile_identifier == PREFERENCE_PLAYER. +/datum/preference/proc/apply_to_client(client/client, value) + //SHOULD_NOT_SLEEP(TRUE) // Broken because we have a bunch of sleeping shit + SHOULD_CALL_PARENT(FALSE) + return + +/// Fired when the preference is updated. +/// Calls apply_to_client by default, but can be overridden. +/datum/preference/proc/apply_to_client_updated(client/client, value) + SHOULD_NOT_SLEEP(TRUE) + apply_to_client(client, value) + +/// Apply this preference onto the given human. +/// Must be overriden by subtypes. +/// Called when the savefile_identifier == PREFERENCE_CHARACTER. +/datum/preference/proc/apply_to_human(mob/living/carbon/human/target, value) + //SHOULD_NOT_SLEEP(TRUE) // Broken because we have a bunch of sleeping shit + SHOULD_CALL_PARENT(FALSE) + CRASH("`apply_to_human()` was not implemented for [type]!") + +/// Returns which savefile to use for a given savefile identifier +/datum/preferences/proc/get_savefile_for_savefile_identifier(savefile_identifier) + RETURN_TYPE(/savefile) + + if (!parent) + return null + + // Both of these will cache savefiles, but only for a tick. + // This is because storing a savefile will lock it, causing later issues down the line. + // Do not change them to addtimer, since the timer SS might not be running at this time. + + switch (savefile_identifier) + if (PREFERENCE_CHARACTER) + if (!character_savefile) + character_savefile = new /savefile(path) + character_savefile.cd = "/character[default_slot]" + spawn (1) + character_savefile = null + return character_savefile + if (PREFERENCE_PLAYER) + if (!game_savefile) + game_savefile = new /savefile(path) + game_savefile.cd = "/" + spawn (1) + game_savefile = null + return game_savefile + else + CRASH("Unknown savefile identifier [savefile_identifier]") + +/// Read a /datum/preference type and return its value. +/// This will write to the savefile if a value was not found with the new value. +/datum/preferences/proc/read_preference(preference_type) + var/datum/preference/preference_entry = GLOB.preference_entries[preference_type] + if (isnull(preference_entry)) + var/extra_info = "" + + // Current initializing subsystem is important to know because it might be a problem with + // things running pre-assets-initialization. + if (!isnull(Master.current_initializing_subsystem)) + extra_info = "Info was attempted to be retrieved while [Master.current_initializing_subsystem] was initializing." + + CRASH("Preference type `[preference_type]` is invalid! [extra_info]") + + if (preference_type in value_cache) + return value_cache[preference_type] + + var/value = preference_entry.read(get_savefile_for_savefile_identifier(preference_entry.savefile_identifier), src) + if (isnull(value)) + value = preference_entry.create_informed_default_value(src) + if (write_preference(preference_entry, value)) + return value + else + CRASH("Couldn't write the default value for [preference_type] (received [value])") + value_cache[preference_type] = value + return value + +/// Set a /datum/preference entry. +/// Returns TRUE for a successful preference application. +/// Returns FALSE if it is invalid. +/datum/preferences/proc/write_preference(datum/preference/preference, preference_value) + var/savefile = get_savefile_for_savefile_identifier(preference.savefile_identifier) + var/new_value = preference.deserialize(preference_value, src) + var/success = preference.write(savefile, new_value) + if (success) + value_cache[preference.type] = new_value + return success + +/// Will perform an update on the preference, but not write to the savefile. +/// This will, for instance, update the character preference view. +/// Performs sanity checks. +/datum/preferences/proc/update_preference(datum/preference/preference, preference_value) + if (!preference.is_accessible(src)) + return FALSE + + var/new_value = preference.deserialize(preference_value, src) + var/success = preference.write(null, new_value) + + if (!success) + return FALSE + + recently_updated_keys |= preference.type + value_cache[preference.type] = new_value + + if (preference.savefile_identifier == PREFERENCE_PLAYER) + preference.apply_to_client_updated(parent, read_preference(preference.type)) + else + character_preview_view?.update_body() + + return TRUE + +/// Checks that a given value is valid. +/// Must be overriden by subtypes. +/// Any type can be passed through. +/datum/preference/proc/is_valid(value) + SHOULD_NOT_SLEEP(TRUE) + SHOULD_CALL_PARENT(FALSE) + CRASH("`is_valid()` was not implemented for [type]!") + +/// Returns data to be sent to users in the menu +/datum/preference/proc/compile_ui_data(mob/user, value) + SHOULD_NOT_SLEEP(TRUE) + + return serialize(value) + +/// Returns data compiled into the preferences JSON asset +/datum/preference/proc/compile_constant_data() + SHOULD_NOT_SLEEP(TRUE) + + return null + +/// Returns whether or not this preference is accessible. +/// If FALSE, will not show in the UI and will not be editable (by update_preference). +/datum/preference/proc/is_accessible(datum/preferences/preferences) + SHOULD_CALL_PARENT(TRUE) + SHOULD_NOT_SLEEP(TRUE) + + if (!isnull(relevant_mutant_bodypart) || !isnull(relevant_species_trait)) + var/species_type = preferences.read_preference(/datum/preference/choiced/species) + + var/datum/species/species = new species_type + if (!(savefile_key in species.get_features())) + return FALSE + + if (!should_show_on_page(preferences.current_window)) + return FALSE + + return TRUE + +/// Return whether or not we can apply this preference +/datum/preference/proc/can_apply(datum/preferences/preferences) + SHOULD_NOT_SLEEP(TRUE) + + if (!isnull(relevant_mutant_bodypart) || !isnull(relevant_species_trait)) + var/species_type = preferences.read_preference(/datum/preference/choiced/species) + + var/datum/species/species = new species_type + if (!(savefile_key in species.get_features())) + return FALSE + + return TRUE + +/// Returns whether or not, given the PREFERENCE_TAB_*, this preference should +/// appear. +/datum/preference/proc/should_show_on_page(preference_tab) + var/is_on_character_page = preference_tab == PREFERENCE_TAB_CHARACTER_PREFERENCES + var/is_character_preference = savefile_identifier == PREFERENCE_CHARACTER + return is_on_character_page == is_character_preference + +/// A preference that is a choice of one option among a fixed set. +/// Used for preferences such as clothing. +/datum/preference/choiced + /// If this is TRUE, icons will be generated. + /// This is necessary for if your `init_possible_values()` override + /// returns an assoc list of names to atoms/icons. + var/should_generate_icons = FALSE + + var/list/cached_values + + /// If the preference is a main feature (PREFERENCE_CATEGORY_FEATURES or PREFERENCE_CATEGORY_CLOTHING) + /// this is the name of the feature that will be presented. + var/main_feature_name + + abstract_type = /datum/preference/choiced + +/// Returns a list of every possible value. +/// The first time this is called, will run `init_values()`. +/// Return value can be in the form of: +/// - A flat list of raw values, such as list(MALE, FEMALE, PLURAL). +/// - An assoc list of raw values to atoms/icons. +/datum/preference/choiced/proc/get_choices() + // Override `init_values()` instead. + SHOULD_NOT_OVERRIDE(TRUE) + + if (isnull(cached_values)) + cached_values = init_possible_values() + ASSERT(cached_values.len) + + return cached_values + +/// Returns a list of every possible value, serialized. +/// Return value can be in the form of: +/// - A flat list of serialized values, such as list(MALE, FEMALE, PLURAL). +/// - An assoc list of serialized values to atoms/icons. +/datum/preference/choiced/proc/get_choices_serialized() + // Override `init_values()` instead. + SHOULD_NOT_OVERRIDE(TRUE) + + var/list/serialized_choices = list() + var/choices = get_choices() + + if (should_generate_icons) + for (var/choice in choices) + serialized_choices[serialize(choice)] = choices[choice] + else + for (var/choice in choices) + serialized_choices += serialize(choice) + + return serialized_choices + +/// Returns a list of every possible value. +/// This must be overriden by `/datum/preference/choiced` subtypes. +/// Return value can be in the form of: +/// - A flat list of raw values, such as list(MALE, FEMALE, PLURAL). +/// - An assoc list of raw values to atoms/icons, in which case +/// icons will be generated. +/datum/preference/choiced/proc/init_possible_values() + CRASH("`init_possible_values()` was not implemented for [type]!") + +/datum/preference/choiced/is_valid(value) + return value in get_choices() + +/datum/preference/choiced/deserialize(input, datum/preferences/preferences) + return sanitize_inlist(input, get_choices(), create_default_value()) + +/datum/preference/choiced/create_default_value() + return pick(get_choices()) + +/datum/preference/choiced/compile_constant_data() + var/list/data = list() + + var/list/choices = list() + + for (var/choice in get_choices()) + choices += choice + + data["choices"] = choices + + if (should_generate_icons) + var/list/icons = list() + + for (var/choice in choices) + icons[choice] = get_spritesheet_key(choice) + + data["icons"] = icons + + if (!isnull(main_feature_name)) + data["name"] = main_feature_name + + return data + +/// A preference that represents an RGB color of something, crunched down to 3 hex numbers. +/// Was used heavily in the past, but doesn't provide as much range and only barely conserves space. +/datum/preference/color_legacy + abstract_type = /datum/preference/color_legacy + +/datum/preference/color_legacy/deserialize(input, datum/preferences/preferences) + return sanitize_hexcolor(input) + +/datum/preference/color_legacy/create_default_value() + return random_short_color() + +/datum/preference/color_legacy/is_valid(value) + var/static/regex/is_legacy_color = regex(@"^[0-9a-fA-F]{3}$") + return findtext(value, is_legacy_color) + +/// A preference that represents an RGB color of something. +/// Will give the value as 6 hex digits, without a hash. +/datum/preference/color + abstract_type = /datum/preference/color + +/datum/preference/color/deserialize(input, datum/preferences/preferences) + return sanitize_color(input) + +/datum/preference/color/create_default_value() + return random_color() + +/datum/preference/color/is_valid(value) + return findtext(value, GLOB.is_color) + +/// Takes an assoc list of names to /datum/sprite_accessory and returns a value +/// fit for `/datum/preference/init_possible_values()` +/proc/possible_values_for_sprite_accessory_list(list/datum/sprite_accessory/sprite_accessories) + var/list/possible_values = list() + for (var/name in sprite_accessories) + var/datum/sprite_accessory/sprite_accessory = sprite_accessories[name] + if (istype(sprite_accessory)) + possible_values[name] = icon(sprite_accessory.icon, sprite_accessory.icon_state) + else + // This means it didn't have an icon state + possible_values[name] = icon('icons/mob/landmarks.dmi', "x") + return possible_values + +/// Takes an assoc list of names to /datum/sprite_accessory and returns a value +/// fit for `/datum/preference/init_possible_values()` +/// Different from `possible_values_for_sprite_accessory_list` in that it takes a list of layers +/// such as BEHIND, FRONT, and ADJ. +/// It also takes a "body part name", such as body_markings, moth_wings, etc +/// They are expected to be in order from lowest to top. +/proc/possible_values_for_sprite_accessory_list_for_body_part( + list/datum/sprite_accessory/sprite_accessories, + body_part, + list/layers, +) + var/list/possible_values = list() + + for (var/name in sprite_accessories) + var/datum/sprite_accessory/sprite_accessory = sprite_accessories[name] + if(sprite_accessory.locked) + continue + + var/icon/final_icon + + for (var/layer in layers) + var/icon/icon = icon(sprite_accessory.icon, "m_[body_part]_[sprite_accessory.icon_state]_[layer]") + + if (isnull(final_icon)) + final_icon = icon + else + final_icon.Blend(icon, ICON_OVERLAY) + + possible_values[name] = final_icon + + return possible_values + +/// A numeric preference with a minimum and maximum value +/datum/preference/numeric + /// The minimum value + var/minimum + + /// The maximum value + var/maximum + + /// The step of the number, such as 1 for integers or 0.5 for half-steps. + var/step = 1 + + abstract_type = /datum/preference/numeric + +/datum/preference/numeric/deserialize(input, datum/preferences/preferences) + if(istext(input)) // Sometimes TGUI will return a string instead of a number, so we take that into account. + input = text2num(input) // Worst case, it's null, it'll just use create_default_value() + return sanitize_float(input, minimum, maximum, step, create_default_value()) + +/datum/preference/numeric/serialize(input) + return sanitize_float(input, minimum, maximum, step, create_default_value()) + +/datum/preference/numeric/create_default_value() + return rand(minimum, maximum) + +/datum/preference/numeric/is_valid(value) + return isnum(value) && value >= minimum && value <= maximum + +/datum/preference/numeric/compile_constant_data() + return list( + "minimum" = minimum, + "maximum" = maximum, + "step" = step, + ) + +/// A prefernece whose value is always TRUE or FALSE +/datum/preference/toggle + abstract_type = /datum/preference/toggle + + /// The default value of the toggle, if create_default_value is not specified + var/default_value = TRUE + +/datum/preference/toggle/create_default_value() + return default_value + +/datum/preference/toggle/deserialize(input, datum/preferences/preferences) + return !!input + +/datum/preference/toggle/is_valid(value) + return value == TRUE || value == FALSE diff --git a/code/modules/client/preferences/accent.dm b/code/modules/client/preferences/accent.dm new file mode 100644 index 000000000000..7b23d66de44c --- /dev/null +++ b/code/modules/client/preferences/accent.dm @@ -0,0 +1,19 @@ +/datum/preference/choiced/accent + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "accent" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/accent/create_default_value() + return ACCENT_NONE + +/datum/preference/choiced/accent/init_possible_values() + return GLOB.accents_names + +/datum/preference/choiced/accent/deserialize(input, datum/preferences/preferences) + if (!(input in GLOB.accents_names)) + return ACCENT_NONE + + return ..(input, preferences) + +/datum/preference/choiced/accent/apply_to_human(mob/living/carbon/human/target, value) + return diff --git a/code/modules/client/preferences/admin.dm b/code/modules/client/preferences/admin.dm new file mode 100644 index 000000000000..7eaf041d4450 --- /dev/null +++ b/code/modules/client/preferences/admin.dm @@ -0,0 +1,14 @@ +/datum/preference/color/asay_color + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "asaycolor" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/color/asay_color/create_default_value() + return DEFAULT_ASAY_COLOR + +/datum/preference/color/asay_color/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return is_admin(preferences.parent) && CONFIG_GET(flag/allow_admin_asaycolor) + diff --git a/code/modules/client/preferences/age.dm b/code/modules/client/preferences/age.dm new file mode 100644 index 000000000000..cad9786ce1fe --- /dev/null +++ b/code/modules/client/preferences/age.dm @@ -0,0 +1,10 @@ +/datum/preference/numeric/age + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "age" + savefile_identifier = PREFERENCE_CHARACTER + + minimum = AGE_MIN + maximum = AGE_MAX + +/datum/preference/numeric/age/apply_to_human(mob/living/carbon/human/target, value) + target.age = value diff --git a/code/modules/client/preferences/ai_core_display.dm b/code/modules/client/preferences/ai_core_display.dm new file mode 100644 index 000000000000..08272254ddab --- /dev/null +++ b/code/modules/client/preferences/ai_core_display.dm @@ -0,0 +1,28 @@ +/// What to show on the AI screen +/datum/preference/choiced/ai_core_display + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "preferred_ai_core_display" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/choiced/ai_core_display/create_default_value() + return "Random" + +/datum/preference/choiced/ai_core_display/init_possible_values() + return GLOB.ai_core_display_screens - "Portrait" + +/datum/preference/choiced/ai_core_display/deserialize(input, datum/preferences/preferences) + if (!(input in GLOB.ai_core_display_screens)) + return "Random" + + return ..(input, preferences) + +/datum/preference/choiced/ai_core_display/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + // Job needs to be medium or high for the preference to show up + return preferences.job_preferences["AI"] >= JP_MEDIUM + +/datum/preference/choiced/ai_core_display/apply_to_human(mob/living/carbon/human/target, value) + return diff --git a/code/modules/client/preferences/ambient_occlusion.dm b/code/modules/client/preferences/ambient_occlusion.dm new file mode 100644 index 000000000000..a81efca00bd8 --- /dev/null +++ b/code/modules/client/preferences/ambient_occlusion.dm @@ -0,0 +1,12 @@ +/// Whether or not to toggle ambient occlusion, the shadows around people +/datum/preference/toggle/ambient_occlusion + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "ambientocclusion" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/ambient_occlusion/apply_to_client(client/client, value) + var/atom/movable/screen/plane_master/game_world/plane_master = locate() in client?.screen + if (!plane_master) + return + + plane_master.backdrop(client.mob) diff --git a/code/modules/client/preferences/announcer.dm b/code/modules/client/preferences/announcer.dm new file mode 100644 index 000000000000..92fbb887ecc8 --- /dev/null +++ b/code/modules/client/preferences/announcer.dm @@ -0,0 +1,5 @@ +/datum/preference/toggle/disable_alternative_announcers + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "disable_alternative_announcers" + savefile_identifier = PREFERENCE_PLAYER + default_value = FALSE diff --git a/code/modules/client/preferences/assets.dm b/code/modules/client/preferences/assets.dm new file mode 100644 index 000000000000..135cec77eac9 --- /dev/null +++ b/code/modules/client/preferences/assets.dm @@ -0,0 +1,66 @@ +/// Assets generated from `/datum/preference` icons +/datum/asset/spritesheet/preferences + name = "preferences" + early = TRUE + cross_round_cachable = TRUE + load_immediately = TRUE + +/datum/asset/spritesheet/preferences/create_spritesheets() + var/list/to_insert = list() + + for (var/preference_key in GLOB.preference_entries_by_key) + var/datum/preference/choiced/preference = GLOB.preference_entries_by_key[preference_key] + if (!istype(preference)) + continue + + if (!preference.should_generate_icons) + continue + + var/list/choices = preference.get_choices_serialized() + for (var/preference_value in choices) + var/create_icon_of = choices[preference_value] + + var/icon/icon + var/icon_state + + if (ispath(create_icon_of, /atom)) + var/atom/atom_icon_source = create_icon_of + icon = initial(atom_icon_source.icon) + icon_state = initial(atom_icon_source.icon_state) + else if (isicon(create_icon_of)) + icon = create_icon_of + else + CRASH("[create_icon_of] is an invalid preference value (from [preference_key]:[preference_value]).") + + to_insert[preference.get_spritesheet_key(preference_value)] = list(icon, icon_state) + + for (var/spritesheet_key in to_insert) + var/list/inserting = to_insert[spritesheet_key] + Insert(spritesheet_key, inserting[1], inserting[2]) + +/// Returns the key that will be used in the spritesheet for a given value. +/datum/preference/proc/get_spritesheet_key(value) + return "[savefile_key]___[sanitize_css_class_name(value)]" + +/// Sends information needed for shared details on individual preferences +/datum/asset/json/preferences + name = "preferences" + +/datum/asset/json/preferences/generate() + var/list/preference_data = list() + + for (var/middleware_type in subtypesof(/datum/preference_middleware)) + var/datum/preference_middleware/middleware = new middleware_type + var/data = middleware.get_constant_data() + if (!isnull(data)) + preference_data[middleware.key] = data + + qdel(middleware) + + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference_entry = GLOB.preference_entries[preference_type] + var/data = preference_entry.compile_constant_data() + if (!isnull(data)) + preference_data[preference_entry.savefile_key] = data + + return preference_data diff --git a/code/modules/client/preferences/auto_fit_viewport.dm b/code/modules/client/preferences/auto_fit_viewport.dm new file mode 100644 index 000000000000..3550af054549 --- /dev/null +++ b/code/modules/client/preferences/auto_fit_viewport.dm @@ -0,0 +1,7 @@ +/datum/preference/toggle/auto_fit_viewport + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "auto_fit_viewport" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/auto_fit_viewport/apply_to_client_updated(client/client, value) + INVOKE_ASYNC(client, /client/verb/fit_viewport) diff --git a/code/modules/client/preferences/balloon_alerts.dm b/code/modules/client/preferences/balloon_alerts.dm new file mode 100644 index 000000000000..b078d951603e --- /dev/null +++ b/code/modules/client/preferences/balloon_alerts.dm @@ -0,0 +1,5 @@ +/datum/preference/toggle/disable_balloon_alerts + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "disable_balloon_alerts" + savefile_identifier = PREFERENCE_PLAYER + default_value = FALSE diff --git a/code/modules/client/preferences/bar_choice.dm b/code/modules/client/preferences/bar_choice.dm new file mode 100644 index 000000000000..0721e77092fc --- /dev/null +++ b/code/modules/client/preferences/bar_choice.dm @@ -0,0 +1,23 @@ +/// Which bar to spawn on boxstation +/datum/preference/choiced/bar_choice + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "bar_choice" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/choiced/bar_choice/create_default_value() + return "Random" + +/datum/preference/choiced/bar_choice/init_possible_values() + return GLOB.potential_box_bars + "Random" + +/datum/preference/choiced/bar_choice/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + // Job needs to be medium or high for the preference to show up + return preferences.job_preferences["Bartender"] >= JP_MEDIUM + +/datum/preference/choiced/bar_choice/apply_to_human(mob/living/carbon/human/target, value) + return + diff --git a/code/modules/client/preferences/buttons_locked.dm b/code/modules/client/preferences/buttons_locked.dm new file mode 100644 index 000000000000..b4f54ff10f8f --- /dev/null +++ b/code/modules/client/preferences/buttons_locked.dm @@ -0,0 +1,5 @@ +/datum/preference/toggle/buttons_locked + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "buttons_locked" + savefile_identifier = PREFERENCE_PLAYER + default_value = FALSE diff --git a/code/modules/client/preferences/clothing.dm b/code/modules/client/preferences/clothing.dm new file mode 100644 index 000000000000..4b85cedd1f63 --- /dev/null +++ b/code/modules/client/preferences/clothing.dm @@ -0,0 +1,145 @@ +/proc/generate_values_for_underwear(icon_file, list/accessory_list, list/icons) + var/icon/lower_half = icon('icons/blanks/32x32.dmi', "nothing") + + for (var/icon in icons) + lower_half.Blend(icon('icons/mob/human_parts_greyscale.dmi', icon), ICON_OVERLAY) + + var/list/values = list() + + for (var/accessory_name in accessory_list) + var/icon/icon_with_socks = new(lower_half) + + if (accessory_name != "Nude") + var/datum/sprite_accessory/accessory = accessory_list[accessory_name] + + var/icon/accessory_icon = icon(icon_file, accessory.icon_state) + icon_with_socks.Blend(accessory_icon, ICON_OVERLAY) + + icon_with_socks.Crop(10, 1, 22, 13) + icon_with_socks.Scale(32, 32) + + values[accessory_name] = icon_with_socks + + return values + +/// Backpack preference +/datum/preference/choiced/backpack + savefile_key = "backbag" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Backpack" + category = PREFERENCE_CATEGORY_CLOTHING + should_generate_icons = TRUE + +/datum/preference/choiced/backpack/init_possible_values() + var/list/values = list() + + values[GBACKPACK] = /obj/item/storage/backpack + values[GSATCHEL] = /obj/item/storage/backpack/satchel + values[LSATCHEL] = /obj/item/storage/backpack/satchel/leather + values[GDUFFELBAG] = /obj/item/storage/backpack/duffelbag + + // In a perfect world, these would be your department's backpack. + // However, this doesn't factor in assistants, or no high slot, and would + // also increase the spritesheet size a lot. + // I play atmos, and so engi backpacks you get. + values[DBACKPACK] = /obj/item/storage/backpack/industrial + values[DSATCHEL] = /obj/item/storage/backpack/satchel/eng + values[DDUFFELBAG] = /obj/item/storage/backpack/duffelbag/engineering + + return values + +/datum/preference/choiced/backpack/create_default_value() + return GBACKPACK + +/datum/preference/choiced/backpack/apply_to_human(mob/living/carbon/human/target, value) + target.backbag = value + +/// Jumpsuit preference +/datum/preference/choiced/jumpsuit + savefile_key = "jumpsuit_style" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Jumpsuit" + category = PREFERENCE_CATEGORY_CLOTHING + should_generate_icons = TRUE + +/datum/preference/choiced/jumpsuit/init_possible_values() + var/list/values = list() + + values[PREF_SUIT] = /obj/item/clothing/under/color/grey + values[PREF_SKIRT] = /obj/item/clothing/under/skirt/color/grey + + return values + +/datum/preference/choiced/jumpsuit/apply_to_human(mob/living/carbon/human/target, value) + target.jumpsuit_style = value + +/// Socks preference +/datum/preference/choiced/socks + savefile_key = "socks" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Socks" + category = PREFERENCE_CATEGORY_CLOTHING + should_generate_icons = TRUE + +/datum/preference/choiced/socks/init_possible_values() + return generate_values_for_underwear('icons/mob/clothing/sprite_accessories/socks.dmi', GLOB.socks_list, list("human_r_leg", "human_l_leg")) + +/datum/preference/choiced/socks/apply_to_human(mob/living/carbon/human/target, value) + target.socks = value + +/// Undershirt preference +/datum/preference/choiced/undershirt + savefile_key = "undershirt" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Undershirt" + category = PREFERENCE_CATEGORY_CLOTHING + should_generate_icons = TRUE + +/datum/preference/choiced/undershirt/init_possible_values() + var/icon/body = icon('icons/mob/human_parts_greyscale.dmi', "human_r_leg") + body.Blend(icon('icons/mob/human_parts_greyscale.dmi', "human_l_leg"), ICON_OVERLAY) + body.Blend(icon('icons/mob/human_parts_greyscale.dmi', "human_r_arm"), ICON_OVERLAY) + body.Blend(icon('icons/mob/human_parts_greyscale.dmi', "human_l_arm"), ICON_OVERLAY) + body.Blend(icon('icons/mob/human_parts_greyscale.dmi', "human_r_hand"), ICON_OVERLAY) + body.Blend(icon('icons/mob/human_parts_greyscale.dmi', "human_l_hand"), ICON_OVERLAY) + body.Blend(icon('icons/mob/human_parts_greyscale.dmi', "human_chest_m"), ICON_OVERLAY) + + var/list/values = list() + + for (var/accessory_name in GLOB.undershirt_list) + var/icon/icon_with_undershirt = icon(body) + + if (accessory_name != "Nude") + var/datum/sprite_accessory/accessory = GLOB.undershirt_list[accessory_name] + icon_with_undershirt.Blend(icon('icons/mob/clothing/sprite_accessories/undershirt.dmi', accessory.icon_state), ICON_OVERLAY) + + icon_with_undershirt.Crop(9, 9, 23, 23) + icon_with_undershirt.Scale(32, 32) + values[accessory_name] = icon_with_undershirt + + return values + +/datum/preference/choiced/undershirt/apply_to_human(mob/living/carbon/human/target, value) + target.undershirt = value + +/// Underwear preference +/datum/preference/choiced/underwear + savefile_key = "underwear" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Underwear" + category = PREFERENCE_CATEGORY_CLOTHING + should_generate_icons = TRUE + +/datum/preference/choiced/underwear/init_possible_values() + return generate_values_for_underwear('icons/mob/clothing/sprite_accessories/underwear.dmi', GLOB.underwear_list, list("human_chest_m", "human_r_leg", "human_l_leg")) + +/datum/preference/choiced/underwear/apply_to_human(mob/living/carbon/human/target, value) + target.underwear = value + +/datum/preference/choiced/underwear/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + var/species_type = preferences.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + return !(NO_UNDERWEAR in species.species_traits) diff --git a/code/modules/client/preferences/credits.dm b/code/modules/client/preferences/credits.dm new file mode 100644 index 000000000000..f238c321d63b --- /dev/null +++ b/code/modules/client/preferences/credits.dm @@ -0,0 +1,4 @@ +/datum/preference/toggle/show_credits + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "show_credits" + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/donor.dm b/code/modules/client/preferences/donor.dm new file mode 100644 index 000000000000..53f396e4322b --- /dev/null +++ b/code/modules/client/preferences/donor.dm @@ -0,0 +1,144 @@ +/datum/preference/choiced/donor_hat + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "donor_hat" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/choiced/donor_hat/create_default_value() + return "None" + +/datum/preference/choiced/donor_hat/init_possible_values() + var/list/values = list() + + values += "None" + + for(var/datum/donator_gear/S as anything in GLOB.donator_gear.donor_items) + if(S.slot != SLOT_HEAD) + continue + + values += S.name + + return values + +/datum/preference/choiced/donor_hat/compile_constant_data() + var/list/data = ..() + + var/list/key_locked = list() + + for(var/datum/donator_gear/S as anything in GLOB.donator_gear.donor_items) + if(S.slot != SLOT_HEAD && !S.plush) + continue + + if (!S.ckey) + continue + + key_locked[S.name] = lowertext(S.ckey) + + data[CHOICED_PREFERENCE_KEY_LOCKED] = key_locked + + return data + + +/datum/preference/choiced/donor_item + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "donor_item" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/choiced/donor_item/create_default_value() + return "None" + +/datum/preference/choiced/donor_item/init_possible_values() + var/list/values = list() + + values += "None" + + for(var/datum/donator_gear/S as anything in GLOB.donator_gear.donor_items) + if(S.slot == SLOT_HEAD && !S.plush) + continue + + values += S.name + + return values + +/datum/preference/choiced/donor_item/compile_constant_data() + var/list/data = ..() + + var/list/key_locked = list() + + for(var/datum/donator_gear/S as anything in GLOB.donator_gear.donor_items) + if(S.slot == SLOT_HEAD) + continue + + if (!S.ckey) + continue + + key_locked[S.name] = lowertext(S.ckey) + + data[CHOICED_PREFERENCE_KEY_LOCKED] = key_locked + + return data + + +/datum/preference/choiced/donor_plush + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "donor_plush" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/choiced/donor_plush/create_default_value() + return "None" + +/datum/preference/choiced/donor_plush/init_possible_values() + var/list/values = list() + + values += "None" + + for(var/datum/donator_gear/S as anything in GLOB.donator_gear.donor_items) + if(S.plush) + values += S.name + + return values + +/datum/preference/choiced/donor_plush/compile_constant_data() + var/list/data = ..() + + var/list/key_locked = list() + + for(var/datum/donator_gear/S as anything in GLOB.donator_gear.donor_items) + if(S.slot == SLOT_HEAD) + continue + + if(!S.plush) + continue + + if (!S.ckey) + continue + + key_locked[S.name] = lowertext(S.ckey) + + data[CHOICED_PREFERENCE_KEY_LOCKED] = key_locked + + return data + + + +/datum/preference/toggle/borg_hat + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "borg_hat" + savefile_identifier = PREFERENCE_PLAYER + + +/datum/preference/choiced/donor_pda + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "donor_pda" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/choiced/donor_pda/create_default_value() + return PDA_COLOR_NORMAL + +/datum/preference/choiced/donor_pda/init_possible_values() + return GLOB.donor_pdas + + +/datum/preference/toggle/purrbation + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "purrbation" + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/engineering_department.dm b/code/modules/client/preferences/engineering_department.dm new file mode 100644 index 000000000000..08ea805bdc16 --- /dev/null +++ b/code/modules/client/preferences/engineering_department.dm @@ -0,0 +1,29 @@ +/// Which department to put station engineers in +/datum/preference/choiced/engineering_department + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "prefered_engineering_department" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/choiced/engineering_department/create_default_value() + return ENG_DEPT_NONE + +/datum/preference/choiced/engineering_department/init_possible_values() + return GLOB.engineering_depts_prefs + +/datum/preference/choiced/engineering_department/deserialize(input, datum/preferences/preferences) + if (!(input in GLOB.engineering_depts_prefs)) + return ENG_DEPT_NONE + + return ..(input, preferences) + +/datum/preference/choiced/engineering_department/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + // Job needs to be medium or high for the preference to show up + return preferences.job_preferences["Station Engineer"] >= JP_MEDIUM + +/datum/preference/choiced/engineering_department/apply_to_human(mob/living/carbon/human/target, value) + return + diff --git a/code/modules/client/preferences/fps.dm b/code/modules/client/preferences/fps.dm new file mode 100644 index 000000000000..f6cd8bf23bec --- /dev/null +++ b/code/modules/client/preferences/fps.dm @@ -0,0 +1,20 @@ +/datum/preference/numeric/fps + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "clientfps" + savefile_identifier = PREFERENCE_PLAYER + + minimum = -1 + maximum = 240 + +/datum/preference/numeric/fps/create_default_value() + return -1 + +/datum/preference/numeric/fps/apply_to_client(client/client, value) + client.fps = (value < 0) ? RECOMMENDED_FPS : value + +/datum/preference/numeric/fps/compile_constant_data() + var/list/data = ..() + + data["recommended_fps"] = RECOMMENDED_FPS + + return data diff --git a/code/modules/client/preferences/gender.dm b/code/modules/client/preferences/gender.dm new file mode 100644 index 000000000000..d24b1954c2c6 --- /dev/null +++ b/code/modules/client/preferences/gender.dm @@ -0,0 +1,13 @@ +/// Gender preference +/datum/preference/choiced/gender + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "gender" + priority = PREFERENCE_PRIORITY_GENDER + +/datum/preference/choiced/gender/init_possible_values() + return list(MALE, FEMALE, PLURAL) + +/datum/preference/choiced/gender/apply_to_human(mob/living/carbon/human/target, value) + if(!target.dna.species.sexes) + value = PLURAL //disregard gender preferences on this species + target.gender = value diff --git a/code/modules/client/preferences/ghost.dm b/code/modules/client/preferences/ghost.dm new file mode 100644 index 000000000000..734e7ed8a651 --- /dev/null +++ b/code/modules/client/preferences/ghost.dm @@ -0,0 +1,181 @@ +/// Determines what accessories your ghost will look like they have. +/datum/preference/choiced/ghost_accessories + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "ghost_accs" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/choiced/ghost_accessories/init_possible_values() + return list(GHOST_ACCS_NONE, GHOST_ACCS_DIR, GHOST_ACCS_FULL) + +/datum/preference/choiced/ghost_accessories/create_default_value() + return GHOST_ACCS_DEFAULT_OPTION + +/datum/preference/choiced/ghost_accessories/apply_to_client(client/client, value) + var/mob/dead/observer/ghost = client.mob + if (!istype(ghost)) + return + + ghost.ghost_accs = value + ghost.update_icon() + +/datum/preference/choiced/ghost_accessories/deserialize(input, datum/preferences/preferences) + // Old ghost preferences used to be 1/50/100. + // Whoever did that wasted an entire day of my time trying to get those sent + // properly, so I'm going to buck them. + if (isnum(input)) + switch (input) + if (1) + input = GHOST_ACCS_NONE + if (50) + input = GHOST_ACCS_DIR + if (100) + input = GHOST_ACCS_FULL + + return ..(input) + +/// Determines the appearance of your ghost to others, when you are a BYOND member +/datum/preference/choiced/ghost_form + savefile_key = "ghost_form" + savefile_identifier = PREFERENCE_PLAYER + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + should_generate_icons = TRUE + + var/static/list/ghost_forms = list( + "catghost" = "Cat", + "ghost" = "Default", + "ghost_black" = "Black", + "ghost_blazeit" = "Blaze it", + "ghost_blue" = "Blue", + "ghost_camo" = "Camo", + "ghost_cyan" = "Cyan", + "ghost_dblue" = "Dark blue", + "ghost_dcyan" = "Dark cyan", + "ghost_dgreen" = "Dark green", + "ghost_dpink" = "Dark pink", + "ghost_dred" = "Dark red", + "ghost_dyellow" = "Dark yellow", + "ghost_fire" = "Fire", + "ghost_funkypurp" = "Funky purple", + "ghost_green" = "Green", + "ghost_grey" = "Grey", + "ghost_mellow" = "Mellow", + "ghost_pink" = "Pink", + "ghost_pinksherbert" = "Pink Sherbert", + "ghost_purpleswirl" = "Purple Swirl", + "ghost_rainbow" = "Rainbow", + "ghost_red" = "Red", + "ghost_yellow" = "Yellow", + "ghostian2" = "Ian", + "ghostking" = "King", + "skeleghost" = "Skeleton", + ) + +/datum/preference/choiced/ghost_form/init_possible_values() + var/list/values = list() + + for (var/ghost_form in ghost_forms) + values[ghost_form] = icon('icons/mob/mob.dmi', ghost_form) + + return values + +/datum/preference/choiced/ghost_form/create_default_value() + return "ghost" + +/datum/preference/choiced/ghost_form/apply_to_client(client/client, value) + var/mob/dead/observer/ghost = client.mob + if (!istype(ghost)) + return + + if (!is_donator(client)) + return + + ghost.update_icon(value) + +/datum/preference/choiced/ghost_form/compile_constant_data() + var/list/data = ..() + + data[CHOICED_PREFERENCE_DISPLAY_NAMES] = ghost_forms + + return data + +/// Toggles the HUD for ghosts +/datum/preference/toggle/ghost_hud + savefile_key = "ghost_hud" + savefile_identifier = PREFERENCE_PLAYER + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + +/datum/preference/toggle/ghost_hud/apply_to_client(client/client, value) + if (isobserver(client?.mob)) + client?.mob.hud_used?.show_hud() + +/// Determines what ghosts orbiting look like to you. +/datum/preference/choiced/ghost_orbit + savefile_key = "ghost_orbit" + savefile_identifier = PREFERENCE_PLAYER + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + +/datum/preference/choiced/ghost_orbit/init_possible_values() + return list( + GHOST_ORBIT_CIRCLE, + GHOST_ORBIT_TRIANGLE, + GHOST_ORBIT_SQUARE, + GHOST_ORBIT_HEXAGON, + GHOST_ORBIT_PENTAGON, + ) + +/datum/preference/choiced/ghost_orbit/create_default_value() + return GHOST_ORBIT_DEFAULT_OPTION + +/datum/preference/choiced/ghost_orbit/apply_to_client(client/client, value) + var/mob/dead/observer/ghost = client.mob + if (!istype(ghost)) + return + + if (!is_donator(client)) + return + + ghost.ghost_orbit = value + +/// Determines how to show other ghosts +/datum/preference/choiced/ghost_others + savefile_key = "ghost_others" + savefile_identifier = PREFERENCE_PLAYER + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + +/datum/preference/choiced/ghost_others/init_possible_values() + return list( + GHOST_OTHERS_SIMPLE, + GHOST_OTHERS_DEFAULT_SPRITE, + GHOST_OTHERS_THEIR_SETTING, + ) + +/datum/preference/choiced/ghost_others/create_default_value() + return GHOST_OTHERS_DEFAULT_OPTION + +/datum/preference/choiced/ghost_others/apply_to_client(client/client, value) + var/mob/dead/observer/ghost = client.mob + if (!istype(ghost)) + return + + ghost.update_sight() + +/datum/preference/choiced/ghost_others/deserialize(input, datum/preferences/preferences) + // Old ghost preferences used to be 1/50/100. + // Whoever did that wasted an entire day of my time trying to get those sent + // properly, so I'm going to buck them. + if (isnum(input)) + switch (input) + if (1) + input = GHOST_OTHERS_SIMPLE + if (50) + input = GHOST_OTHERS_DEFAULT_SPRITE + if (100) + input = GHOST_OTHERS_THEIR_SETTING + + return ..(input, preferences) + +/// Whether or not ghosts can examine things by clicking on them. +/datum/preference/toggle/inquisitive_ghost + savefile_key = "inquisitive_ghost" + savefile_identifier = PREFERENCE_PLAYER + category = PREFERENCE_CATEGORY_GAME_PREFERENCES diff --git a/code/modules/client/preferences/hotkeys.dm b/code/modules/client/preferences/hotkeys.dm new file mode 100644 index 000000000000..b96b68286d60 --- /dev/null +++ b/code/modules/client/preferences/hotkeys.dm @@ -0,0 +1,7 @@ +/datum/preference/toggle/hotkeys + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "hotkeys" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/hotkeys/apply_to_client(client/client, value) + client.hotkeys = value diff --git a/code/modules/client/preferences/items.dm b/code/modules/client/preferences/items.dm new file mode 100644 index 000000000000..88ca0ff5179d --- /dev/null +++ b/code/modules/client/preferences/items.dm @@ -0,0 +1,9 @@ +/datum/preference/toggle/spawn_flare + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "flare" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/spawn_map + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "map" + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/jobless_role.dm b/code/modules/client/preferences/jobless_role.dm new file mode 100644 index 000000000000..4e368a251e0f --- /dev/null +++ b/code/modules/client/preferences/jobless_role.dm @@ -0,0 +1,15 @@ +/datum/preference/choiced/jobless_role + savefile_key = "joblessrole" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/jobless_role/create_default_value() + return BEOVERFLOW + +/datum/preference/choiced/jobless_role/init_possible_values() + return list(BEOVERFLOW, BERANDOMJOB, RETURNTOLOBBY) + +/datum/preference/choiced/jobless_role/should_show_on_page(preference_tab) + return preference_tab == PREFERENCE_TAB_CHARACTER_PREFERENCES + +/datum/preference/choiced/jobless_role/apply_to_human(mob/living/carbon/human/target, value) + return diff --git a/code/modules/client/preferences/middleware/_middleware.dm b/code/modules/client/preferences/middleware/_middleware.dm new file mode 100644 index 000000000000..8f47f73642c8 --- /dev/null +++ b/code/modules/client/preferences/middleware/_middleware.dm @@ -0,0 +1,52 @@ +/// Preference middleware is code that helps to decentralize complicated preference features. +/datum/preference_middleware + /// The preferences datum + var/datum/preferences/preferences + + /// The key that will be used for get_constant_data(). + /// If null, will use the typepath minus /datum/preference_middleware. + var/key = null + + /// Map of ui_act actions -> proc paths to call. + /// Signature is `(list/params, mob/user) -> TRUE/FALSE. + /// Return output is the same as ui_act--TRUE if it should update, FALSE if it should not + var/list/action_delegations = list() + +/datum/preference_middleware/New(datum/preferences) + src.preferences = preferences + + if (isnull(key)) + // + 2 coming from the off-by-one of copytext, and then another from the slash + key = copytext("[type]", length("[parent_type]") + 2) + +/datum/preference_middleware/Destroy() + preferences = null + return ..() + +/// Append all of these into ui_data +/datum/preference_middleware/proc/get_ui_data(mob/user) + return list() + +/// Append all of these into ui_static_data +/datum/preference_middleware/proc/get_ui_static_data(mob/user) + return list() + +/// Append all of these into ui_assets +/datum/preference_middleware/proc/get_ui_assets() + return list() + +/// Append all of these into /datum/asset/json/preferences. +/datum/preference_middleware/proc/get_constant_data() + return null + +/// Merge this into the result of compile_character_preferences. +/datum/preference_middleware/proc/get_character_preferences(mob/user) + return null + +/// Called every set_preference, returns TRUE if this handled it. +/datum/preference_middleware/proc/pre_set_preference(mob/user, preference, value) + return FALSE + +/// Called when a character is changed. +/datum/preference_middleware/proc/on_new_character(mob/user) + return diff --git a/code/modules/client/preferences/middleware/antags.dm b/code/modules/client/preferences/middleware/antags.dm new file mode 100644 index 000000000000..6deb9124f72b --- /dev/null +++ b/code/modules/client/preferences/middleware/antags.dm @@ -0,0 +1,164 @@ +/datum/preference_middleware/antags + action_delegations = list( + "set_antags" = .proc/set_antags, + ) + +/datum/preference_middleware/antags/get_ui_static_data(mob/user) + if (preferences.current_window != PREFERENCE_TAB_CHARACTER_PREFERENCES) + return list() + + var/list/data = list() + + var/list/selected_antags = list() + + for (var/antag in preferences.be_special) + selected_antags += serialize_antag_name(antag) + + data["selected_antags"] = selected_antags + + var/list/antag_bans = get_antag_bans() + if (antag_bans.len) + data["antag_bans"] = antag_bans + + var/list/antag_days_left = get_antag_days_left() + if (antag_days_left?.len) + data["antag_days_left"] = antag_days_left + + return data + +/datum/preference_middleware/antags/get_ui_assets() + return list( + get_asset_datum(/datum/asset/spritesheet/antagonists), + ) + +/datum/preference_middleware/antags/proc/set_antags(list/params, mob/user) + SHOULD_NOT_SLEEP(TRUE) + + var/sent_antags = params["antags"] + var/toggled = params["toggled"] + + var/antags = list() + + var/serialized_antags = get_serialized_antags() + + for (var/sent_antag in sent_antags) + var/special_role = serialized_antags[sent_antag] + if (!special_role) + continue + + antags += special_role + + if (toggled) + preferences.be_special |= antags + else + preferences.be_special -= antags + + // This is predicted on the client + return FALSE + +/datum/preference_middleware/antags/proc/get_antag_bans() + var/list/antag_bans = list() + + for (var/datum/dynamic_ruleset/dynamic_ruleset as anything in subtypesof(/datum/dynamic_ruleset)) + var/antag_flag = initial(dynamic_ruleset.antag_flag) + var/antag_flag_override = initial(dynamic_ruleset.antag_flag_override) + + if (isnull(antag_flag)) + continue + + if (is_banned_from(preferences.parent.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE))) + antag_bans += serialize_antag_name(antag_flag) + + return antag_bans + +/datum/preference_middleware/antags/proc/get_antag_days_left() + if (!CONFIG_GET(flag/use_age_restriction_for_jobs)) + return + + var/list/antag_days_left = list() + + for (var/datum/dynamic_ruleset/dynamic_ruleset as anything in subtypesof(/datum/dynamic_ruleset)) + var/antag_flag = initial(dynamic_ruleset.antag_flag) + //var/antag_flag_override = initial(dynamic_ruleset.antag_flag_override) + + if (isnull(antag_flag)) + continue + + /*var/days_needed = preferences.parent?.get_remaining_days( + GLOB.special_roles[antag_flag_override || antag_flag] + )*/ + + var/days_needed = 0 + + if (days_needed > 0) + antag_days_left[serialize_antag_name(antag_flag)] = days_needed + + return antag_days_left + +/datum/preference_middleware/antags/proc/get_serialized_antags() + var/list/serialized_antags + + if (isnull(serialized_antags)) + serialized_antags = list() + + for (var/special_role in GLOB.special_roles) + serialized_antags[serialize_antag_name(special_role)] = special_role + + return serialized_antags + +/// Sprites generated for the antagonists panel +/datum/asset/spritesheet/antagonists + name = "antagonists" + early = TRUE + cross_round_cachable = TRUE + +/datum/asset/spritesheet/antagonists/create_spritesheets() + // Antagonists that don't have a dynamic ruleset, but do have a preference + var/static/list/non_ruleset_antagonists = list( + ROLE_LONE_OPERATIVE = /datum/antagonist/nukeop/lone, + ) + + var/list/antagonists = GLOB.special_roles.Copy() + + /*for (var/datum/dynamic_ruleset/ruleset as anything in subtypesof(/datum/dynamic_ruleset)) + var/datum/antagonist/antagonist_type = initial(ruleset.antag_datum) + if (isnull(antagonist_type)) + continue + + // antag_flag is guaranteed to be unique by unit tests. + antagonists[initial(ruleset.antag_flag)] = antagonist_type + */ + var/list/generated_icons = list() + var/list/to_insert = list() + + for (var/antag_flag in antagonists) + var/datum/antagonist/antagonist_type = antagonists[antag_flag] + + // antag_flag is guaranteed to be unique by unit tests. + var/spritesheet_key = serialize_antag_name(antag_flag) + + if (!isnull(generated_icons[antagonist_type])) + to_insert[spritesheet_key] = generated_icons[antagonist_type] + continue + + var/datum/antagonist/antagonist = new antagonist_type + var/icon/preview_icon = antagonist.get_preview_icon() + + if (isnull(preview_icon)) + continue + + qdel(antagonist) + + // preview_icons are not scaled at this stage INTENTIONALLY. + // If an icon is not prepared to be scaled to that size, it looks really ugly, and this + // makes it harder to figure out what size it *actually* is. + generated_icons[antagonist_type] = preview_icon + to_insert[spritesheet_key] = preview_icon + + for (var/spritesheet_key in to_insert) + Insert(spritesheet_key, to_insert[spritesheet_key]) + +/// Serializes an antag name to be used for preferences UI +/proc/serialize_antag_name(antag_name) + // These are sent through CSS, so they need to be safe to use as class names. + return lowertext(sanitize_css_class_name(antag_name)) diff --git a/code/modules/client/preferences/middleware/jobs.dm b/code/modules/client/preferences/middleware/jobs.dm new file mode 100644 index 000000000000..3aa91aba0046 --- /dev/null +++ b/code/modules/client/preferences/middleware/jobs.dm @@ -0,0 +1,132 @@ +/datum/preference_middleware/jobs + action_delegations = list( + "set_job_preference" = .proc/set_job_preference, + "set_alt_title" = .proc/set_alt_title, + ) + +/datum/preference_middleware/jobs/proc/set_job_preference(list/params, mob/user) + var/job_title = params["job"] + var/level = params["level"] + + if (level != null && level != JP_LOW && level != JP_MEDIUM && level != JP_HIGH) + return FALSE + + var/datum/job/job = SSjob.GetJob(job_title) + + if (isnull(job)) + return FALSE + + //if (job.faction != FACTION_STATION) + // return FALSE + + if (!preferences.set_job_preference_level(job, level)) + return FALSE + + preferences.character_preview_view?.update_body() + + return TRUE + +/datum/preference_middleware/jobs/proc/set_alt_title(list/params, mob/user) + var/job_title = params["job"] + var/alt_title = params["alt_title"] + + var/datum/job/job = SSjob.GetJob(job_title) + + if (isnull(job)) + return FALSE + + preferences.SetPlayerAltTitle(job, alt_title) + + return TRUE + +/datum/preference_middleware/jobs/get_constant_data() + var/list/data = list() + + var/list/departments = list() + var/list/jobs = list() + + for (var/datum/job/job as anything in SSjob.joinable_occupations) + var/datum/job_department/department_type = job.department_for_prefs || job.departments_list?[1] + if (isnull(department_type)) + stack_trace("[job] does not have a department set, yet is a joinable occupation!") + continue + + if (isnull(job.description)) + stack_trace("[job] does not have a description set, yet is a joinable occupation!") + continue + + var/department_name = initial(department_type.department_name) + if (isnull(departments[department_name])) + var/datum/job/department_head_type = initial(department_type.department_head) + + departments[department_name] = list( + "head" = department_head_type && initial(department_head_type.title), + ) + + jobs[job.title] = list( + "description" = job.description, + "department" = department_name, + "alt_titles" = job.alt_titles + ) + + data["departments"] = departments + data["jobs"] = jobs + + return data + +/datum/preference_middleware/jobs/get_ui_data(mob/user) + var/list/data = list() + + data["job_preferences"] = preferences.job_preferences + data["job_alt_titles"] = preferences.player_alt_titles + + return data + +/datum/preference_middleware/jobs/get_ui_static_data(mob/user) + var/list/data = list() + + var/list/required_job_playtime = get_required_job_playtime(user) + if (!isnull(required_job_playtime)) + data += required_job_playtime + + var/list/job_bans = get_job_bans(user) + if (job_bans.len) + data["job_bans"] = job_bans + + return data.len > 0 ? data : null + +/datum/preference_middleware/jobs/proc/get_required_job_playtime(mob/user) + var/list/data = list() + + var/list/job_days_left = list() + var/list/job_required_experience = list() + + for (var/datum/job/job as anything in SSjob.occupations) + var/required_playtime_remaining = job.required_playtime_remaining(user.client) + if (required_playtime_remaining) + job_required_experience[job.title] = list( + "experience_type" = job.get_exp_req_type(), + "required_playtime" = required_playtime_remaining, + ) + + continue + + if (!job.player_old_enough(user.client)) + job_days_left[job.title] = job.available_in_days(user.client) + + if (job_days_left.len) + data["job_days_left"] = job_days_left + + if (job_required_experience) + data["job_required_experience"] = job_required_experience + + return data + +/datum/preference_middleware/jobs/proc/get_job_bans(mob/user) + var/list/data = list() + + for (var/datum/job/job as anything in SSjob.occupations) + if (is_banned_from(user.client?.ckey, job.title)) + data += job.title + + return data diff --git a/code/modules/client/preferences/middleware/keybindings.dm b/code/modules/client/preferences/middleware/keybindings.dm new file mode 100644 index 000000000000..bb771dc28f1c --- /dev/null +++ b/code/modules/client/preferences/middleware/keybindings.dm @@ -0,0 +1,101 @@ +#define MAX_HOTKEY_SLOTS 3 + +/// Middleware to handle keybindings +/datum/preference_middleware/keybindings + action_delegations = list( + "reset_all_keybinds" = .proc/reset_all_keybinds, + "reset_keybinds_to_defaults" = .proc/reset_keybinds_to_defaults, + "set_keybindings" = .proc/set_keybindings, + ) + +/datum/preference_middleware/keybindings/get_ui_static_data(mob/user) + if (preferences.current_window == PREFERENCE_TAB_CHARACTER_PREFERENCES) + return list() + + var/list/keybindings = preferences.key_bindings + + return list( + "keybindings" = keybindings, + ) + +/datum/preference_middleware/keybindings/get_ui_assets() + return list( + get_asset_datum(/datum/asset/json/keybindings) + ) + +/datum/preference_middleware/keybindings/proc/reset_all_keybinds(list/params, mob/user) + preferences.key_bindings = deepCopyList(GLOB.default_hotkeys) + preferences.key_bindings_by_key = preferences.get_key_bindings_by_key(preferences.key_bindings) + + preferences.update_static_data(user) + preferences.parent?.set_macros() + + return TRUE + +/datum/preference_middleware/keybindings/proc/reset_keybinds_to_defaults(list/params, mob/user) + var/keybind_name = params["keybind_name"] + var/datum/keybinding/keybinding = GLOB.keybindings_by_name[keybind_name] + + if (isnull(keybinding)) + return FALSE + + preferences.key_bindings[keybind_name] = preferences.parent.hotkeys ? keybinding.hotkey_keys : keybinding.classic_keys + preferences.key_bindings_by_key = preferences.get_key_bindings_by_key(preferences.key_bindings) + + preferences.update_static_data(user) + preferences.parent?.set_macros() + + return TRUE + +/datum/preference_middleware/keybindings/proc/set_keybindings(list/params) + var/keybind_name = params["keybind_name"] + + if (isnull(GLOB.keybindings_by_name[keybind_name])) + return FALSE + + var/list/raw_hotkeys = params["hotkeys"] + if (!istype(raw_hotkeys)) + return FALSE + + if (raw_hotkeys.len > MAX_HOTKEY_SLOTS) + return FALSE + + // There's no optimal, easy way to check if something is an array + // and not an object in BYOND, so just sanitize it to make sure. + var/list/hotkeys = list() + for (var/hotkey in raw_hotkeys) + if (!istext(hotkey)) + return FALSE + + // Fairly arbitrary number, it's just so you don't save enormous fake keybinds. + if (length(hotkey) > 100) + return FALSE + + hotkeys += hotkey + + preferences.key_bindings[keybind_name] = hotkeys + preferences.key_bindings_by_key = preferences.get_key_bindings_by_key(preferences.key_bindings) + preferences.parent?.set_macros() + + return TRUE + +/datum/asset/json/keybindings + name = "keybindings" + +/datum/asset/json/keybindings/generate() + var/list/keybindings = list() + + for (var/name in GLOB.keybindings_by_name) + var/datum/keybinding/keybinding = GLOB.keybindings_by_name[name] + + if (!(keybinding.category in keybindings)) + keybindings[keybinding.category] = list() + + keybindings[keybinding.category][keybinding.name] = list( + "name" = keybinding.full_name, + "description" = keybinding.description, + ) + + return keybindings + +#undef MAX_HOTKEY_SLOTS diff --git a/code/modules/client/preferences/middleware/legacy_toggles.dm b/code/modules/client/preferences/middleware/legacy_toggles.dm new file mode 100644 index 000000000000..757c1b83a409 --- /dev/null +++ b/code/modules/client/preferences/middleware/legacy_toggles.dm @@ -0,0 +1,166 @@ +/// In the before times, toggles were all stored in one bitfield. +/// In order to preserve this existing data (and code) without massive +/// migrations, this middleware attempts to handle this in a way +/// transparent to the preferences UI itself. +/// In the future, the existing toggles data should just be migrated to +/// individual `/datum/preference/toggle`s. +/datum/preference_middleware/legacy_toggles + // DO NOT ADD ANY NEW TOGGLES HERE! + // Use `/datum/preference/toggle` instead. + var/static/list/legacy_toggles = list( + "announce_login" = ANNOUNCE_LOGIN, + "combohud_lighting" = COMBOHUD_LIGHTING, + "deadmin_always" = DEADMIN_ALWAYS, + "deadmin_antagonist" = DEADMIN_ANTAGONIST, + "deadmin_position_head" = DEADMIN_POSITION_HEAD, + "deadmin_position_security" = DEADMIN_POSITION_SECURITY, + "deadmin_position_silicon" = DEADMIN_POSITION_SILICON, + "disable_arrivalrattle" = DISABLE_ARRIVALRATTLE, + "disable_deathrattle" = DISABLE_DEATHRATTLE, + "member_public" = MEMBER_PUBLIC, + "sound_adminhelp" = SOUND_ADMINHELP, + "sound_ambience" = SOUND_AMBIENCE, + "sound_announcements" = SOUND_ANNOUNCEMENTS, + "sound_instruments" = SOUND_INSTRUMENTS, + "sound_lobby" = SOUND_LOBBY, + "sound_midi" = SOUND_MIDI, + "sound_prayers" = SOUND_PRAYERS, + "sound_ship_ambience" = SOUND_SHIP_AMBIENCE, + ) + + var/static/list/legacy_extra_toggles = list( + "split_admin_tabs" = SPLIT_ADMIN_TABS, + "fast_mc_refresh" = FAST_MC_REFRESH, + ) + + var/static/list/legacy_chat_toggles = list( + "chat_bankcard" = CHAT_BANKCARD, + "chat_dead" = CHAT_DEAD, + "chat_ghostears" = CHAT_GHOSTEARS, + "chat_ghostpda" = CHAT_GHOSTPDA, + "chat_ghostradio" = CHAT_GHOSTRADIO, + "chat_ghostsight" = CHAT_GHOSTSIGHT, + "chat_ghostwhisper" = CHAT_GHOSTWHISPER, + "chat_ooc" = CHAT_OOC, + "chat_prayer" = CHAT_PRAYER, + "chat_pullr" = CHAT_PULLR, + ) + + var/static/list/legacy_yog_toggles = list( + "quiet_mode" = QUIET_ROUND, + "pref_mood" = PREF_MOOD, + ) + +/datum/preference_middleware/legacy_toggles/get_character_preferences(mob/user) + if (preferences.current_window != PREFERENCE_TAB_GAME_PREFERENCES) + return list() + + var/static/list/admin_only_legacy_toggles = list( + "admin_ignore_cult_ghost", + "announce_login", + "combohud_lighting", + "deadmin_always", + "deadmin_antagonist", + "deadmin_position_head", + "deadmin_position_security", + "deadmin_position_silicon", + "sound_adminhelp", + "sound_prayers", + ) + + var/static/list/admin_only_extra_toggles = list( + "split_admin_tabs", + "fast_mc_refresh", + ) + + var/static/list/admin_only_chat_toggles = list( + "chat_dead", + "chat_prayer", + ) + + var/static/list/donor_only_yog_toggles = list( + "quiet_mode", + ) + + var/static/list/deadmin_flags = list( + "deadmin_antagonist", + "deadmin_position_head", + "deadmin_position_security", + "deadmin_position_silicon", + ) + + var/list/new_game_preferences = list() + var/is_admin = is_admin(user.client) + var/is_donor = is_donator(user.client) + + for (var/toggle_name in legacy_toggles) + if (!is_admin && (toggle_name in admin_only_legacy_toggles)) + continue + + if (is_admin && (toggle_name in deadmin_flags) && (preferences.toggles & DEADMIN_ALWAYS)) + continue + + if (toggle_name == "member_public" && !preferences.unlock_content) + continue + + new_game_preferences[toggle_name] = (preferences.toggles & legacy_toggles[toggle_name]) != 0 + + for (var/toggle_name in legacy_extra_toggles) + if (!is_admin && (toggle_name in admin_only_extra_toggles)) + continue + + new_game_preferences[toggle_name] = (preferences.extra_toggles & legacy_extra_toggles[toggle_name]) != 0 + + for (var/toggle_name in legacy_chat_toggles) + if (!is_admin && (toggle_name in admin_only_chat_toggles)) + continue + + new_game_preferences[toggle_name] = (preferences.chat_toggles & legacy_chat_toggles[toggle_name]) != 0 + + for (var/toggle_name in legacy_yog_toggles) + if (!is_donor && (toggle_name in donor_only_yog_toggles)) + continue + + new_game_preferences[toggle_name] = (preferences.yogtoggles & legacy_yog_toggles[toggle_name]) != 0 + + return list( + PREFERENCE_CATEGORY_GAME_PREFERENCES = new_game_preferences, + ) + +/datum/preference_middleware/legacy_toggles/pre_set_preference(mob/user, preference, value) + var/legacy_flag = legacy_toggles[preference] + if (!isnull(legacy_flag)) + if (value) + preferences.toggles |= legacy_flag + else + preferences.toggles &= ~legacy_flag + + // I know this looks silly, but this is the only one that cares + // and NO NEW LEGACY TOGGLES should ever be added. + if (legacy_flag == SOUND_LOBBY) + if (value && isnewplayer(user)) + user.client?.playtitlemusic() + else + user.stop_sound_channel(CHANNEL_LOBBYMUSIC) + + return TRUE + + var/legacy_chat_flag = legacy_chat_toggles[preference] + if (!isnull(legacy_chat_flag)) + if (value) + preferences.chat_toggles |= legacy_chat_flag + else + preferences.chat_toggles &= ~legacy_chat_flag + + return TRUE + + var/legacy_yog_flag = legacy_yog_toggles[preference] + if (!isnull(legacy_yog_flag)) + if (value) + preferences.yogtoggles |= legacy_yog_flag + else + preferences.yogtoggles &= ~legacy_yog_flag + + return TRUE + + return FALSE diff --git a/code/modules/client/preferences/middleware/names.dm b/code/modules/client/preferences/middleware/names.dm new file mode 100644 index 000000000000..387d9129a578 --- /dev/null +++ b/code/modules/client/preferences/middleware/names.dm @@ -0,0 +1,56 @@ +/// Middleware that handles telling the UI which name to show, and what names +/// they have. +/datum/preference_middleware/names + action_delegations = list( + "randomize_name" = .proc/randomize_name, + ) + +/datum/preference_middleware/names/get_constant_data() + var/list/data = list() + + var/list/types = list() + + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/name/name_preference = GLOB.preference_entries[preference_type] + if (!istype(name_preference)) + continue + + types[name_preference.savefile_key] = list( + "can_randomize" = name_preference.is_randomizable(), + "explanation" = name_preference.explanation, + "group" = name_preference.group, + ) + + data["types"] = types + + return data + +/datum/preference_middleware/names/get_ui_data(mob/user) + var/list/data = list() + + data["name_to_use"] = get_name_to_use() + + return data + +/datum/preference_middleware/names/proc/get_name_to_use() + var/highest_priority_job = preferences.get_highest_priority_job() + + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/name/name_preference = GLOB.preference_entries[preference_type] + if (!istype(name_preference)) + continue + + if (isnull(name_preference.relevant_job)) + continue + + if (istype(highest_priority_job, name_preference.relevant_job)) + return name_preference.savefile_key + + return "real_name" + +/datum/preference_middleware/names/proc/randomize_name(list/params, mob/user) + var/datum/preference/name/name_preference = GLOB.preference_entries_by_key[params["preference"]] + if (!istype(name_preference)) + return FALSE + + return preferences.update_preference(name_preference, name_preference.create_random_value(preferences)) diff --git a/code/modules/client/preferences/middleware/quirks.dm b/code/modules/client/preferences/middleware/quirks.dm new file mode 100644 index 000000000000..95f56bc4c1b0 --- /dev/null +++ b/code/modules/client/preferences/middleware/quirks.dm @@ -0,0 +1,113 @@ +/// Middleware to handle quirks +/datum/preference_middleware/quirks + var/tainted = FALSE + + action_delegations = list( + "give_quirk" = .proc/give_quirk, + "remove_quirk" = .proc/remove_quirk, + ) + +/datum/preference_middleware/quirks/get_ui_static_data(mob/user) + if (preferences.current_window != PREFERENCE_TAB_CHARACTER_PREFERENCES) + return list() + + var/list/data = list() + + data["selected_quirks"] = get_selected_quirks() + data["locked_quirks"] = get_locked_quirks() + + // If moods are globally enabled, or this guy does indeed have his mood pref set to Enabled + var/ismoody = (!CONFIG_GET(flag/disable_human_mood) || (user.client?.prefs.yogtoggles & PREF_MOOD)) + data["mood_enabled"] = ismoody + + return data + +/datum/preference_middleware/quirks/get_ui_data(mob/user) + var/list/data = list() + + if (tainted) + tainted = FALSE + data["selected_quirks"] = get_selected_quirks() + data["locked_quirks"] = get_locked_quirks() + + // If moods are globally enabled, or this guy does indeed have his mood pref set to Enabled + var/ismoody = (!CONFIG_GET(flag/disable_human_mood) || (user.client?.prefs.yogtoggles & PREF_MOOD)) + data["mood_enabled"] = ismoody + + return data + +/datum/preference_middleware/quirks/get_constant_data() + var/list/quirk_info = list() + + for (var/quirk_name in SSquirks.quirks) + var/datum/quirk/quirk = SSquirks.quirks[quirk_name] + quirk_info[sanitize_css_class_name(quirk_name)] = list( + "description" = initial(quirk.desc), + "icon" = initial(quirk.icon), + "name" = quirk_name, + "value" = initial(quirk.value), + "mood" = initial(quirk.mood_quirk), + ) + + return list( + "max_positive_quirks" = MAX_QUIRKS, + "quirk_info" = quirk_info, + "quirk_blacklist" = SSquirks.quirk_blacklist, + ) + +/datum/preference_middleware/quirks/on_new_character(mob/user) + tainted = TRUE + +/datum/preference_middleware/quirks/proc/give_quirk(list/params, mob/user) + var/quirk_name = params["quirk"] + + var/list/new_quirks = preferences.all_quirks | quirk_name + if (SSquirks.filter_invalid_quirks(new_quirks, user.client) != new_quirks) + // If the client is sending an invalid give_quirk, that means that + // something went wrong with the client prediction, so we should + // catch it back up to speed. + preferences.update_static_data(user) + return TRUE + + preferences.all_quirks = new_quirks + + return TRUE + +/datum/preference_middleware/quirks/proc/remove_quirk(list/params, mob/user) + var/quirk_name = params["quirk"] + + var/list/new_quirks = preferences.all_quirks - quirk_name + if ( \ + !(quirk_name in preferences.all_quirks) \ + || SSquirks.filter_invalid_quirks(new_quirks, user.client) != new_quirks \ + ) + // If the client is sending an invalid remove_quirk, that means that + // something went wrong with the client prediction, so we should + // catch it back up to speed. + preferences.update_static_data(user) + return TRUE + + preferences.all_quirks = new_quirks + + return TRUE + +/datum/preference_middleware/quirks/proc/get_selected_quirks() + var/list/selected_quirks = list() + + for (var/quirk in preferences.all_quirks) + selected_quirks += sanitize_css_class_name(quirk) + + return selected_quirks + +/datum/preference_middleware/quirks/proc/get_locked_quirks() + var/list/locked_quirks = list() + + for (var/quirk_name in SSquirks.quirks) + var/datum/quirk/quirk_type = SSquirks.quirks[quirk_name] + var/datum/quirk/quirk = new quirk_type(no_init = TRUE) + var/lock_reason = quirk?.check_quirk(preferences) + if (lock_reason) + locked_quirks[sanitize_css_class_name(quirk_name)] = lock_reason + qdel(quirk) + + return locked_quirks diff --git a/code/modules/client/preferences/middleware/random.dm b/code/modules/client/preferences/middleware/random.dm new file mode 100644 index 000000000000..17bc259e2c25 --- /dev/null +++ b/code/modules/client/preferences/middleware/random.dm @@ -0,0 +1,84 @@ +/// Middleware for handling randomization preferences +/datum/preference_middleware/random + action_delegations = list( + "randomize_character" = .proc/randomize_character, + "set_random_preference" = .proc/set_random_preference, + ) + +/datum/preference_middleware/random/get_character_preferences(mob/user) + return list( + "randomization" = preferences.randomise, + ) + +/datum/preference_middleware/random/get_constant_data() + var/list/randomizable = list() + + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + if (!preference.is_randomizable()) + continue + + randomizable += preference.savefile_key + + return list( + "randomizable" = randomizable, + ) + +/datum/preference_middleware/random/proc/randomize_character() + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (preferences.should_randomize(preference)) + preferences.write_preference(preference, preference.create_random_value(src)) + + preferences.character_preview_view.update_body() + + return TRUE + +/datum/preference_middleware/random/proc/set_random_preference(list/params, mob/user) + var/requested_preference_key = params["preference"] + var/value = params["value"] + + var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key] + if (isnull(requested_preference)) + return FALSE + + if (!requested_preference.is_randomizable()) + return FALSE + + if (value == RANDOM_ANTAG_ONLY) + preferences.randomise[requested_preference_key] = RANDOM_ANTAG_ONLY + else if (value == RANDOM_ENABLED) + preferences.randomise[requested_preference_key] = RANDOM_ENABLED + else if (value == RANDOM_DISABLED) + preferences.randomise -= requested_preference_key + else + return FALSE + + return TRUE + +/// Returns if a preference should be randomized. +/datum/preferences/proc/should_randomize(datum/preference/preference, is_antag) + if (!preference.is_randomizable()) + return FALSE + + var/requested_randomization = randomise[preference.savefile_key] + + if (istype(preference, /datum/preference/name)) + requested_randomization = read_preference(/datum/preference/choiced/random_name) + + switch (requested_randomization) + if (RANDOM_ENABLED) + return TRUE + if (RANDOM_ANTAG_ONLY) + return is_antag + else + return FALSE + +/// Given randomization flags, will return whether or not this preference should be randomized. +/datum/preference/proc/included_in_randomization_flags(randomize_flags) + return TRUE + +/datum/preference/name/included_in_randomization_flags(randomize_flags) + return !!(randomize_flags & RANDOMIZE_NAME) + +/datum/preference/choiced/species/included_in_randomization_flags(randomize_flags) + return !!(randomize_flags & RANDOMIZE_SPECIES) diff --git a/code/modules/client/preferences/middleware/skillcapes.dm b/code/modules/client/preferences/middleware/skillcapes.dm new file mode 100644 index 000000000000..bf6d9aad9519 --- /dev/null +++ b/code/modules/client/preferences/middleware/skillcapes.dm @@ -0,0 +1,22 @@ +/// Middleware for skillcapes + +/// Generate list of valid skillcapes +/datum/preference_middleware/skillcape/get_ui_static_data() + . = list() + .["earned_skillcapes"] = list("None") + + var/max_earned = TRUE + for(var/id in GLOB.skillcapes) + var/datum/skillcape/cape_check = GLOB.skillcapes[id] + if(!cape_check.job) + continue + + if(preferences.exp[cape_check.job] < cape_check.minutes) + max_earned = FALSE + continue + + .["earned_skillcapes"] += id + + if(max_earned) + .["earned_skillcapes"] += "max" + diff --git a/code/modules/client/preferences/middleware/species.dm b/code/modules/client/preferences/middleware/species.dm new file mode 100644 index 000000000000..05fffbb32811 --- /dev/null +++ b/code/modules/client/preferences/middleware/species.dm @@ -0,0 +1,36 @@ +/// Handles the assets for species icons +/datum/preference_middleware/species + +/datum/preference_middleware/species/get_ui_assets() + return list( + get_asset_datum(/datum/asset/spritesheet/species), + ) + +/datum/asset/spritesheet/species + name = "species" + early = TRUE + cross_round_cachable = TRUE + +/datum/asset/spritesheet/species/create_spritesheets() + var/list/to_insert = list() + + for (var/species_id in get_selectable_species()) + var/datum/species/species_type = GLOB.species_list[species_id] + + var/mob/living/carbon/human/dummy/consistent/dummy = new + dummy.set_species(species_type) + dummy.equipOutfit(/datum/outfit/job/assistant/consistent, visualsOnly = TRUE) + dummy.dna.species.prepare_human_for_preview(dummy) + var/icon/dummy_icon = getFlatIcon(dummy) + if(ismoth(dummy)) + dummy_icon = null + dummy_icon = icon('icons/mob/human.dmi', "moth") + dummy_icon.Scale(64, 64) + dummy_icon.Crop(15, 64, 15 + 31, 64 - 31) + dummy_icon.Scale(64, 64) + to_insert[sanitize_css_class_name(initial(species_type.name))] = dummy_icon + + SSatoms.prepare_deletion(dummy) + + for (var/spritesheet_key in to_insert) + Insert(spritesheet_key, to_insert[spritesheet_key]) diff --git a/code/modules/client/preferences/mood.dm b/code/modules/client/preferences/mood.dm new file mode 100644 index 000000000000..b37274fb756d --- /dev/null +++ b/code/modules/client/preferences/mood.dm @@ -0,0 +1,4 @@ +/datum/preference/toggle/mood_tail_wagging + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "mood_tail_wagging" + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/names.dm b/code/modules/client/preferences/names.dm new file mode 100644 index 000000000000..52c865cb63c8 --- /dev/null +++ b/code/modules/client/preferences/names.dm @@ -0,0 +1,145 @@ +/// A preference for a name. Used not just for normal names, but also for clown names, etc. +/datum/preference/name + category = "names" + priority = PREFERENCE_PRIORITY_NAMES + savefile_identifier = PREFERENCE_CHARACTER + abstract_type = /datum/preference/name + + /// The display name when showing on the "other names" panel + var/explanation + + /// These will be grouped together on the preferences menu + var/group + + /// Whether or not to allow numbers in the person's name + var/allow_numbers = FALSE + + /// If the highest priority job matches this, will prioritize this name in the UI + var/relevant_job + +/datum/preference/name/apply_to_human(mob/living/carbon/human/target, value) + // Only real_name applies directly, everything else is applied by something else + return + +/datum/preference/name/deserialize(input, datum/preferences/preferences) + return reject_bad_name("[input]", allow_numbers) + +/datum/preference/name/serialize(input) + // `is_valid` should always be run before `serialize`, so it should not + // be possible for this to return `null`. + return reject_bad_name(input, allow_numbers) + +/datum/preference/name/is_valid(value) + return istext(value) && !isnull(reject_bad_name(value, allow_numbers)) + +/// A character's real name +/datum/preference/name/real_name + explanation = "Name" + // The `_` makes it first in ABC order. + group = "_real_name" + savefile_key = "real_name" + allow_numbers = TRUE //fucking ipcs + +/datum/preference/name/real_name/apply_to_human(mob/living/carbon/human/target, value) + target.real_name = value + target.name = value + +/datum/preference/name/real_name/create_informed_default_value(datum/preferences/preferences) + var/species_type = preferences.read_preference(/datum/preference/choiced/species) + var/gender = preferences.read_preference(/datum/preference/choiced/gender) + + var/datum/species/species = new species_type + + return species.random_name(gender, unique = TRUE) + +/datum/preference/name/real_name/deserialize(input, datum/preferences/preferences) + input = ..(input) + if (!input) + return input + + if (CONFIG_GET(flag/humans_need_surnames) && preferences.read_preference(/datum/preference/choiced/species) == /datum/species/human) + var/first_space = findtext(input, " ") + if(!first_space) //we need a surname + input += " [pick(GLOB.last_names)]" + else if(first_space == length(input)) + input += "[pick(GLOB.last_names)]" + + return reject_bad_name(input, allow_numbers) + +/// The name for a backup human, when nonhumans are made into head of staff +/datum/preference/name/backup_human + explanation = "Backup human name" + group = "backup_human" + savefile_key = "human_name" + +/datum/preference/name/backup_human/create_informed_default_value(datum/preferences/preferences) + var/gender = preferences.read_preference(/datum/preference/choiced/gender) + + return random_unique_name(gender) + +/datum/preference/name/clown + savefile_key = "clown_name" + + explanation = "Clown name" + group = "fun" + relevant_job = /datum/job/clown + +/datum/preference/name/clown/create_default_value() + return pick(GLOB.clown_names) + +/datum/preference/name/mime + savefile_key = "mime_name" + + explanation = "Mime name" + group = "fun" + relevant_job = /datum/job/mime + +/datum/preference/name/mime/create_default_value() + return pick(GLOB.mime_names) + +/datum/preference/name/cyborg + savefile_key = "cyborg_name" + + allow_numbers = TRUE + can_randomize = FALSE + + explanation = "Cyborg name" + group = "silicons" + relevant_job = /datum/job/cyborg + +/datum/preference/name/cyborg/create_default_value() + return DEFAULT_CYBORG_NAME + +/datum/preference/name/ai + savefile_key = "ai_name" + + allow_numbers = TRUE + explanation = "AI name" + group = "silicons" + relevant_job = /datum/job/ai + +/datum/preference/name/ai/create_default_value() + return pick(GLOB.ai_names) + +/datum/preference/name/religion + savefile_key = "religion_name" + + allow_numbers = TRUE + + explanation = "Religion name" + group = "religion" + +/datum/preference/name/religion/create_default_value() + return DEFAULT_RELIGION + +/datum/preference/name/deity + savefile_key = "deity_name" + + allow_numbers = TRUE + can_randomize = FALSE + + explanation = "Deity name" + group = "religion" + +/datum/preference/name/deity/create_default_value() + return DEFAULT_DEITY diff --git a/code/modules/client/preferences/ooc.dm b/code/modules/client/preferences/ooc.dm new file mode 100644 index 000000000000..de436391edc9 --- /dev/null +++ b/code/modules/client/preferences/ooc.dm @@ -0,0 +1,14 @@ +/// The color admins will speak in for OOC. +/datum/preference/color/ooc_color + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "ooccolor" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/color/ooc_color/create_default_value() + return "#c43b23" + +/datum/preference/color/ooc_color/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return is_admin(preferences.parent) || preferences.unlock_content diff --git a/code/modules/client/preferences/parallax.dm b/code/modules/client/preferences/parallax.dm new file mode 100644 index 000000000000..24cccce2da62 --- /dev/null +++ b/code/modules/client/preferences/parallax.dm @@ -0,0 +1,38 @@ +/// Determines parallax, "fancy space" +/datum/preference/choiced/parallax + savefile_key = "parallax" + savefile_identifier = PREFERENCE_PLAYER + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + +/datum/preference/choiced/parallax/init_possible_values() + return list( + PARALLAX_INSANE, + PARALLAX_HIGH, + PARALLAX_MED, + PARALLAX_LOW, + PARALLAX_DISABLE, + ) + +/datum/preference/choiced/parallax/create_default_value() + return PARALLAX_HIGH + +/datum/preference/choiced/parallax/apply_to_client(client/client, value) + client.mob?.hud_used?.update_parallax_pref(client?.mob) + +/datum/preference/choiced/parallax/deserialize(input, datum/preferences/preferences) + // Old preferences were numbers, which causes annoyances when + // sending over as lists that isn't worth dealing with. + if (isnum(input)) + switch (input) + if (-1) + input = PARALLAX_INSANE + if (0) + input = PARALLAX_HIGH + if (1) + input = PARALLAX_MED + if (2) + input = PARALLAX_LOW + if (3) + input = PARALLAX_DISABLE + + return ..(input) diff --git a/code/modules/client/preferences/pda.dm b/code/modules/client/preferences/pda.dm new file mode 100644 index 000000000000..2e05208a5b8e --- /dev/null +++ b/code/modules/client/preferences/pda.dm @@ -0,0 +1,33 @@ +/// The color of a PDA +/datum/preference/color/pda_color + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "pda_color" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/color/pda_color/create_default_value() + return COLOR_OLIVE + +/// The visual style of a PDA +/datum/preference/choiced/pda_style + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "pda_style" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/choiced/pda_style/init_possible_values() + return GLOB.pda_styles + +/// The visual theme of a PDA +/datum/preference/choiced/pda_theme + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "pda_theme" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/choiced/pda_theme/init_possible_values() + return GLOB.pda_themes + +/// Put ID into PDA when spawning +/datum/preference/toggle/id_in_pda + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "id_in_pda" + savefile_identifier = PREFERENCE_PLAYER + default_value = FALSE diff --git a/code/modules/client/preferences/persistent_scars.dm b/code/modules/client/preferences/persistent_scars.dm new file mode 100644 index 000000000000..e9777e2517d8 --- /dev/null +++ b/code/modules/client/preferences/persistent_scars.dm @@ -0,0 +1,10 @@ +/datum/preference/toggle/persistent_scars + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "persistent_scars" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/toggle/persistent_scars/apply_to_human(mob/living/carbon/human/target, value) + // This proc doesn't do anything, due to the nature of persistent scars, we ALWAYS need to have a client to be able to use them properly. Or at the very least, a ckey. + // So we don't need to store this anywhere else, we simply search the preference when we need it. + return diff --git a/code/modules/client/preferences/pixel_size.dm b/code/modules/client/preferences/pixel_size.dm new file mode 100644 index 000000000000..cb166e0139ac --- /dev/null +++ b/code/modules/client/preferences/pixel_size.dm @@ -0,0 +1,15 @@ +/datum/preference/numeric/pixel_size + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "pixel_size" + savefile_identifier = PREFERENCE_PLAYER + + minimum = 0 + maximum = 3 + + step = 0.5 + +/datum/preference/numeric/pixel_size/create_default_value() + return 0 + +/datum/preference/numeric/pixel_size/apply_to_client(client/client, value) + client?.view_size?.resetFormat() diff --git a/code/modules/client/preferences/preferred_map.dm b/code/modules/client/preferences/preferred_map.dm new file mode 100644 index 000000000000..8c9f87e82322 --- /dev/null +++ b/code/modules/client/preferences/preferred_map.dm @@ -0,0 +1,52 @@ +/// During map rotation, this will help determine the chosen map. +/datum/preference/choiced/preferred_map + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "preferred_map" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/choiced/preferred_map/init_possible_values() + var/list/maps = list() + maps += "" + + for (var/map in config.maplist) + var/datum/map_config/map_config = config.maplist[map] + if (!map_config.votable) + continue + + maps += map + + return maps + +/datum/preference/choiced/preferred_map/create_default_value() + return "" + +/datum/preference/choiced/preferred_map/compile_constant_data() + var/list/data = ..() + + var/display_names = list() + + if (config.defaultmap) + display_names[""] = "Default ([config.defaultmap.map_name])" + else + display_names[""] = "Default" + + for (var/choice in get_choices()) + if (choice == "") + continue + + var/datum/map_config/map_config = config.maplist[choice] + + var/map_name = map_config.map_name + if (map_config.voteweight <= 0) + map_name += " (disabled)" + display_names[choice] = map_name + + data[CHOICED_PREFERENCE_DISPLAY_NAMES] = display_names + + return data + +/datum/preference/choiced/preferred_map/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return CONFIG_GET(flag/preference_map_voting) diff --git a/code/modules/client/preferences/random.dm b/code/modules/client/preferences/random.dm new file mode 100644 index 000000000000..e8555e847487 --- /dev/null +++ b/code/modules/client/preferences/random.dm @@ -0,0 +1,53 @@ +/datum/preference/choiced/random_body + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "random_body" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/choiced/random_body/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/choiced/random_body/init_possible_values() + return list( + RANDOM_ANTAG_ONLY, + RANDOM_DISABLED, + RANDOM_ENABLED, + ) + +/datum/preference/choiced/random_body/create_default_value() + return RANDOM_DISABLED + +/*/datum/preference/toggle/random_hardcore + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "random_hardcore" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + default_value = FALSE + +/datum/preference/toggle/random_hardcore/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/toggle/random_hardcore/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return preferences.parent.get_exp_living(pure_numeric = TRUE) >= PLAYTIME_HARDCORE_RANDOM +*/ +/datum/preference/choiced/random_name + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "random_name" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/choiced/random_name/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/choiced/random_name/init_possible_values() + return list( + RANDOM_ANTAG_ONLY, + RANDOM_DISABLED, + RANDOM_ENABLED, + ) + +/datum/preference/choiced/random_name/create_default_value() + return RANDOM_DISABLED diff --git a/code/modules/client/preferences/runechat.dm b/code/modules/client/preferences/runechat.dm new file mode 100644 index 000000000000..83282fefe36c --- /dev/null +++ b/code/modules/client/preferences/runechat.dm @@ -0,0 +1,25 @@ +/datum/preference/toggle/enable_runechat + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "chat_on_map" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/enable_runechat_non_mobs + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "see_chat_non_mob" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/see_rc_emotes + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "see_rc_emotes" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/numeric/max_chat_length + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "max_chat_length" + savefile_identifier = PREFERENCE_PLAYER + + minimum = 1 + maximum = CHAT_MESSAGE_MAX_LENGTH + +/datum/preference/numeric/max_chat_length/create_default_value() + return CHAT_MESSAGE_MAX_LENGTH diff --git a/code/modules/client/preferences/scaling_method.dm b/code/modules/client/preferences/scaling_method.dm new file mode 100644 index 000000000000..63235abaf996 --- /dev/null +++ b/code/modules/client/preferences/scaling_method.dm @@ -0,0 +1,14 @@ +/// The scaling method to show the world in, e.g. nearest neighbor +/datum/preference/choiced/scaling_method + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "scaling_method" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/choiced/scaling_method/create_default_value() + return SCALING_METHOD_DISTORT + +/datum/preference/choiced/scaling_method/init_possible_values() + return list(SCALING_METHOD_DISTORT, SCALING_METHOD_BLUR, SCALING_METHOD_NORMAL) + +/datum/preference/choiced/scaling_method/apply_to_client(client/client, value) + client?.view_size?.setZoomMode() diff --git a/code/modules/client/preferences/security_department.dm b/code/modules/client/preferences/security_department.dm new file mode 100644 index 000000000000..ba07aaff9dda --- /dev/null +++ b/code/modules/client/preferences/security_department.dm @@ -0,0 +1,28 @@ +/// Which department to put security officers in +/datum/preference/choiced/security_department + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_key = "prefered_security_department" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/choiced/security_department/create_default_value() + return SEC_DEPT_NONE + +/datum/preference/choiced/security_department/init_possible_values() + return GLOB.security_depts_prefs + +/datum/preference/choiced/security_department/deserialize(input, datum/preferences/preferences) + if (!(input in GLOB.security_depts_prefs)) + return SEC_DEPT_NONE + + return ..(input, preferences) + +/datum/preference/choiced/security_department/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + // Job needs to be medium or high for the preference to show up + return preferences.job_preferences["Security Officer"] >= JP_MEDIUM + +/datum/preference/choiced/security_department/apply_to_human(mob/living/carbon/human/target, value) + return diff --git a/code/modules/client/preferences/skillcape.dm b/code/modules/client/preferences/skillcape.dm new file mode 100644 index 000000000000..37d64ad4d9e2 --- /dev/null +++ b/code/modules/client/preferences/skillcape.dm @@ -0,0 +1,30 @@ +/datum/preference/choiced/skillcape + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_identifier = PREFERENCE_PLAYER + savefile_key = "skillcape_id" + can_randomize = FALSE + +/datum/preference/choiced/skillcape/init_possible_values() + return list("None") + GLOB.skillcapes + +/datum/preference/choiced/skillcape/compile_constant_data() + var/list/data = ..() + + var/list/cape_names = list("None" = "None") + + for(var/cape_id in GLOB.skillcapes) + var/datum/skillcape/cape = GLOB.skillcapes[cape_id] + cape_names[cape.id] = cape.name + + data[CHOICED_PREFERENCE_DISPLAY_NAMES] = cape_names + + return data + +/datum/preference/choiced/skillcape/create_default_value() + return "None" + +/datum/preference/choiced/skillcape/deserialize(input, datum/preferences/preferences) + if (!(input in GLOB.skillcapes)) + return "None" + + return ..(input, preferences) diff --git a/code/modules/client/preferences/skin_tone.dm b/code/modules/client/preferences/skin_tone.dm new file mode 100644 index 000000000000..70aebc3521ec --- /dev/null +++ b/code/modules/client/preferences/skin_tone.dm @@ -0,0 +1,36 @@ +/datum/preference/choiced/skin_tone + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "skin_tone" + +/datum/preference/choiced/skin_tone/init_possible_values() + return GLOB.skin_tones + +/datum/preference/choiced/skin_tone/compile_constant_data() + var/list/data = ..() + + data[CHOICED_PREFERENCE_DISPLAY_NAMES] = GLOB.skin_tone_names + + var/list/to_hex = list() + for (var/choice in get_choices()) + var/hex_value = skintone2hex(choice) + var/list/hsl = rgb2num("#[hex_value]", COLORSPACE_HSL) + + to_hex[choice] = list( + "lightness" = hsl[3], + "value" = hex_value, + ) + + data["to_hex"] = to_hex + + return data + +/datum/preference/choiced/skin_tone/apply_to_human(mob/living/carbon/human/target, value) + target.skin_tone = value + +/datum/preference/choiced/skin_tone/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + var/datum/species/species_type = preferences.read_preference(/datum/preference/choiced/species) + return initial(species_type.use_skintones) diff --git a/code/modules/client/preferences/species.dm b/code/modules/client/preferences/species.dm new file mode 100644 index 000000000000..2ad93c3b14bf --- /dev/null +++ b/code/modules/client/preferences/species.dm @@ -0,0 +1,52 @@ +/// Species preference +/datum/preference/choiced/species + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "species" + priority = PREFERENCE_PRIORITY_SPECIES + randomize_by_default = FALSE + +/datum/preference/choiced/species/deserialize(input, datum/preferences/preferences) + return GLOB.species_list[sanitize_inlist(input, get_choices_serialized(), "human")] + +/datum/preference/choiced/species/serialize(input) + var/datum/species/species = input + return initial(species.id) + +/datum/preference/choiced/species/create_default_value() + return /datum/species/human + +/datum/preference/choiced/species/create_random_value(datum/preferences/preferences) + return pick(get_choices()) + +/datum/preference/choiced/species/init_possible_values() + var/list/values = list() + + for (var/species_id in get_selectable_species()) + values += GLOB.species_list[species_id] + + return values + +/datum/preference/choiced/species/apply_to_human(mob/living/carbon/human/target, value) + target.set_species(value, icon_update = FALSE, pref_load = TRUE) + +/datum/preference/choiced/species/compile_constant_data() + var/list/data = list() + + for (var/species_id in get_selectable_species()) + var/species_type = GLOB.species_list[species_id] + var/datum/species/species = new species_type() + + data[species_id] = list() + data[species_id]["name"] = species.name + data[species_id]["desc"] = species.get_species_description() + data[species_id]["lore"] = species.get_species_lore() + data[species_id]["icon"] = sanitize_css_class_name(species.name) + data[species_id]["use_skintones"] = species.use_skintones + data[species_id]["sexes"] = species.sexes + data[species_id]["enabled_features"] = species.get_features() + data[species_id]["perks"] = species.get_species_perks() + data[species_id]["diet"] = species.get_species_diet() + + qdel(species) + + return data diff --git a/code/modules/client/preferences/species_features/basic.dm b/code/modules/client/preferences/species_features/basic.dm new file mode 100644 index 000000000000..176116dd6440 --- /dev/null +++ b/code/modules/client/preferences/species_features/basic.dm @@ -0,0 +1,121 @@ +/proc/generate_possible_values_for_sprite_accessories_on_head(accessories) + var/list/values = possible_values_for_sprite_accessory_list(accessories) + + var/icon/head_icon = icon('icons/mob/human_parts_greyscale.dmi', "human_head_m") + head_icon.Blend("#[skintone2hex("caucasian1")]", ICON_MULTIPLY) + + for (var/name in values) + var/datum/sprite_accessory/accessory = accessories[name] + if (accessory == null || accessory.icon_state == null) + continue + + var/icon/final_icon = new(head_icon) + + var/icon/beard_icon = values[name] + beard_icon.Blend(COLOR_DARK_BROWN, ICON_MULTIPLY) + final_icon.Blend(beard_icon, ICON_OVERLAY) + + final_icon.Crop(10, 19, 22, 31) + final_icon.Scale(32, 32) + + values[name] = final_icon + + return values + + +/datum/preference/color_legacy/eye_color + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "eye_color" + savefile_identifier = PREFERENCE_CHARACTER + relevant_species_trait = EYECOLOR + unique = TRUE + +/datum/preference/color_legacy/eye_color/apply_to_human(mob/living/carbon/human/target, value) + target.eye_color = value + + var/obj/item/organ/eyes/eyes_organ = target.getorgan(/obj/item/organ/eyes) + if (istype(eyes_organ)) + if (!initial(eyes_organ.eye_color)) + eyes_organ.eye_color = value + eyes_organ.old_eye_color = value + +/datum/preference/color_legacy/eye_color/create_default_value() + return random_eye_color() + + +/datum/preference/choiced/hairstyle + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "hair_style_name" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Hair style" + should_generate_icons = TRUE + relevant_species_trait = HAIR + +/datum/preference/choiced/hairstyle/init_possible_values() + return generate_possible_values_for_sprite_accessories_on_head(GLOB.hair_styles_list) + +/datum/preference/choiced/hairstyle/apply_to_human(mob/living/carbon/human/target, value) + target.hair_style = value + + +/datum/preference/color_legacy/hair_color + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "hair_color" + savefile_identifier = PREFERENCE_CHARACTER + relevant_species_trait = HAIR + unique = TRUE + +/datum/preference/color_legacy/hair_color/apply_to_human(mob/living/carbon/human/target, value) + target.hair_color = value + + +/datum/preference/choiced/facial_hairstyle + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "facial_style_name" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Facial hair" + should_generate_icons = TRUE + relevant_species_trait = FACEHAIR + +/datum/preference/choiced/facial_hairstyle/init_possible_values() + return generate_possible_values_for_sprite_accessories_on_head(GLOB.facial_hair_styles_list) + +/datum/preference/choiced/facial_hairstyle/apply_to_human(mob/living/carbon/human/target, value) + target.facial_hair_style = value + + +/datum/preference/color_legacy/facial_hair_color + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "facial_hair_color" + savefile_identifier = PREFERENCE_CHARACTER + relevant_species_trait = FACEHAIR + unique = TRUE + +/datum/preference/color_legacy/facial_hair_color/apply_to_human(mob/living/carbon/human/target, value) + target.facial_hair_color = value + + +/datum/preference/choiced/hair_gradient + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_gradientstyle" + savefile_identifier = PREFERENCE_CHARACTER + relevant_species_trait = HAIR + +/datum/preference/choiced/hair_gradient/init_possible_values() + return assoc_to_keys(GLOB.hair_gradients_list) + +/datum/preference/choiced/hair_gradient/apply_to_human(mob/living/carbon/human/target, value) + target.grad_style = value + +/datum/preference/choiced/hair_gradient/create_default_value() + return "None" + + +/datum/preference/color_legacy/hair_gradient + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_gradientcolor" + savefile_identifier = PREFERENCE_CHARACTER + relevant_species_trait = HAIR + +/datum/preference/color_legacy/hair_gradient/apply_to_human(mob/living/carbon/human/target, value) + target.grad_color = value diff --git a/code/modules/client/preferences/species_features/ethereal.dm b/code/modules/client/preferences/species_features/ethereal.dm new file mode 100644 index 000000000000..cbdd949a8607 --- /dev/null +++ b/code/modules/client/preferences/species_features/ethereal.dm @@ -0,0 +1,46 @@ +/datum/preference/choiced/ethereal_color + savefile_key = "feature_ethcolor" + savefile_identifier = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Ethereal color" + should_generate_icons = TRUE + +/datum/preference/choiced/ethereal_color/init_possible_values() + var/list/values = list() + + var/icon/ethereal_base = icon('icons/mob/human_parts_greyscale.dmi', "ethereal_head_m") + ethereal_base.Blend(icon('icons/mob/human_parts_greyscale.dmi', "ethereal_chest_m"), ICON_OVERLAY) + ethereal_base.Blend(icon('icons/mob/human_parts_greyscale.dmi', "ethereal_l_arm"), ICON_OVERLAY) + ethereal_base.Blend(icon('icons/mob/human_parts_greyscale.dmi', "ethereal_r_arm"), ICON_OVERLAY) + + var/icon/eyes = icon('icons/mob/human_face.dmi', "eyes") + eyes.Blend(COLOR_BLACK, ICON_MULTIPLY) + ethereal_base.Blend(eyes, ICON_OVERLAY) + + ethereal_base.Scale(64, 64) + ethereal_base.Crop(15, 64, 15 + 31, 64 - 31) + + for (var/name in GLOB.color_list_ethereal) + var/color = GLOB.color_list_ethereal[name] + + var/icon/icon = new(ethereal_base) + icon.Blend("#[color]", ICON_MULTIPLY) + values[name] = icon + + return values + +/datum/preference/choiced/ethereal_color/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ethcolor"] = GLOB.color_list_ethereal[value] + + +/datum/preference/choiced/ethereal_mark + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_ethereal_mark" + savefile_identifier = PREFERENCE_CHARACTER + relevant_mutant_bodypart = "ethereal_mark" + +/datum/preference/choiced/ethereal_mark/init_possible_values() + return assoc_to_keys(GLOB.ethereal_mark_list) + +/datum/preference/choiced/ethereal_mark/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ethereal_mark"] = value diff --git a/code/modules/client/preferences/species_features/felinid.dm b/code/modules/client/preferences/species_features/felinid.dm new file mode 100644 index 000000000000..bc5445cd2fd0 --- /dev/null +++ b/code/modules/client/preferences/species_features/felinid.dm @@ -0,0 +1,33 @@ +/datum/preference/choiced/tail_human + savefile_key = "feature_human_tail" + savefile_identifier = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + can_randomize = FALSE + relevant_mutant_bodypart = "tail_human" + +/datum/preference/choiced/tail_human/init_possible_values() + return assoc_to_keys(GLOB.tails_list_human) + +/datum/preference/choiced/tail_human/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["tail_human"] = value + +/datum/preference/choiced/tail_human/create_default_value() + var/datum/sprite_accessory/tails/human/cat/tail = /datum/sprite_accessory/tails/human/cat + return initial(tail.name) + +/datum/preference/choiced/ears + savefile_key = "feature_human_ears" + savefile_identifier = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + can_randomize = FALSE + relevant_mutant_bodypart = "ears" + +/datum/preference/choiced/ears/init_possible_values() + return assoc_to_keys(GLOB.ears_list) + +/datum/preference/choiced/ears/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ears"] = value + +/datum/preference/choiced/ears/create_default_value() + var/datum/sprite_accessory/ears/cat/ears = /datum/sprite_accessory/ears/cat + return initial(ears.name) diff --git a/code/modules/client/preferences/species_features/ipc.dm b/code/modules/client/preferences/species_features/ipc.dm new file mode 100644 index 000000000000..d7b34cdf75f0 --- /dev/null +++ b/code/modules/client/preferences/species_features/ipc.dm @@ -0,0 +1,68 @@ +/datum/preference/choiced/ipc_screen + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_ipc_screen" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "IPC screen" + relevant_mutant_bodypart = "ipc_screen" + +/datum/preference/choiced/ipc_screen/init_possible_values() + return GLOB.ipc_screens_list + +/datum/preference/choiced/ipc_screen/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ipc_screen"] = value + + +/datum/preference/color_legacy/ipc_screen_color + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "eye_color" + savefile_identifier = PREFERENCE_CHARACTER + relevant_mutant_bodypart = "ipc_screen" + unique = TRUE + +/datum/preference/color_legacy/ipc_screen_color/apply_to_human(mob/living/carbon/human/target, value) + target.eye_color = value + + var/obj/item/organ/eyes/eyes_organ = target.getorgan(/obj/item/organ/eyes) + if (istype(eyes_organ)) + if (!initial(eyes_organ.eye_color)) + eyes_organ.eye_color = value + eyes_organ.old_eye_color = value + + +/datum/preference/choiced/ipc_antenna + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_ipc_antenna" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "IPC antenna" + relevant_mutant_bodypart = "ipc_antenna" + +/datum/preference/choiced/ipc_antenna/init_possible_values() + return GLOB.ipc_antennas_list + +/datum/preference/choiced/ipc_antenna/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ipc_antenna"] = value + + +/datum/preference/color_legacy/ipc_antenna_color + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "hair_color" + savefile_identifier = PREFERENCE_CHARACTER + relevant_mutant_bodypart = "ipc_antenna" + unique = TRUE + +/datum/preference/color_legacy/ipc_antenna_color/apply_to_human(mob/living/carbon/human/target, value) + target.hair_color = value + + +/datum/preference/choiced/ipc_chassis + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_ipc_chassis" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "IPC chassis" + relevant_mutant_bodypart = "ipc_chassis" + +/datum/preference/choiced/ipc_chassis/init_possible_values() + return GLOB.ipc_chassis_list + +/datum/preference/choiced/ipc_chassis/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ipc_chassis"] = value diff --git a/code/modules/client/preferences/species_features/lizard.dm b/code/modules/client/preferences/species_features/lizard.dm new file mode 100644 index 000000000000..ec7f86d57601 --- /dev/null +++ b/code/modules/client/preferences/species_features/lizard.dm @@ -0,0 +1,180 @@ +/proc/generate_lizard_side_shots(list/sprite_accessories, key, include_snout = TRUE) + var/list/values = list() + + var/icon/lizard = icon('icons/mob/human_parts_greyscale.dmi', "lizard_head_m", EAST) + + var/icon/eyes = icon('icons/mob/human_face.dmi', "eyes", EAST) + eyes.Blend(COLOR_GRAY, ICON_MULTIPLY) + lizard.Blend(eyes, ICON_OVERLAY) + + if (include_snout) + lizard.Blend(icon('icons/mob/mutant_bodyparts.dmi', "m_snout_round_ADJ", EAST), ICON_OVERLAY) + + for (var/name in sprite_accessories) + var/datum/sprite_accessory/sprite_accessory = sprite_accessories[name] + + var/icon/final_icon = icon(lizard) + + if (sprite_accessory.icon_state != "none") + var/icon/accessory_icon = icon(sprite_accessory.icon, "m_[key]_[sprite_accessory.icon_state]_ADJ", EAST) + final_icon.Blend(accessory_icon, ICON_OVERLAY) + + final_icon.Crop(11, 20, 23, 32) + final_icon.Scale(32, 32) + final_icon.Blend(COLOR_VIBRANT_LIME, ICON_MULTIPLY) + + values[name] = final_icon + + return values + + +/datum/preference/choiced/lizard_body_markings + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "feature_lizard_body_markings" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Body markings" + relevant_mutant_bodypart = "body_markings" + should_generate_icons = TRUE + +/datum/preference/choiced/lizard_body_markings/init_possible_values() + var/list/values = list() + + var/icon/lizard = icon('icons/mob/human_parts_greyscale.dmi', "lizard_chest_m") + + for (var/name in GLOB.body_markings_list) + var/datum/sprite_accessory/sprite_accessory = GLOB.body_markings_list[name] + + var/icon/final_icon = icon(lizard) + + if (sprite_accessory.icon_state != "none") + var/icon/body_markings_icon = icon( + 'icons/mob/mutant_bodyparts.dmi', + "m_body_markings_[sprite_accessory.icon_state]_ADJ", + ) + + final_icon.Blend(body_markings_icon, ICON_OVERLAY) + + final_icon.Blend(COLOR_VIBRANT_LIME, ICON_MULTIPLY) + final_icon.Crop(10, 8, 22, 23) + final_icon.Scale(26, 32) + final_icon.Crop(-2, 1, 29, 32) + + values[name] = final_icon + + return values + +/datum/preference/choiced/lizard_body_markings/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["body_markings"] = value + + +/datum/preference/choiced/lizard_frills + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "feature_lizard_frills" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Frills" + relevant_mutant_bodypart = "frills" + should_generate_icons = TRUE + +/datum/preference/choiced/lizard_frills/init_possible_values() + return generate_lizard_side_shots(GLOB.frills_list, "frills") + +/datum/preference/choiced/lizard_frills/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["frills"] = value + + +/datum/preference/choiced/lizard_horns + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "feature_lizard_horns" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Horns" + relevant_mutant_bodypart = "horns" + should_generate_icons = TRUE + +/datum/preference/choiced/lizard_horns/init_possible_values() + return generate_lizard_side_shots(GLOB.horns_list, "horns") + +/datum/preference/choiced/lizard_horns/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["horns"] = value + + +/datum/preference/choiced/lizard_legs + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_lizard_legs" + savefile_identifier = PREFERENCE_CHARACTER + relevant_mutant_bodypart = "legs" + +/datum/preference/choiced/lizard_legs/init_possible_values() + return assoc_to_keys(GLOB.legs_list) + +/datum/preference/choiced/lizard_legs/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["legs"] = value + + +/datum/preference/choiced/lizard_snout + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_lizard_snout" + savefile_identifier = PREFERENCE_CHARACTER + relevant_mutant_bodypart = "snout" + +/datum/preference/choiced/lizard_snout/init_possible_values() + return assoc_to_keys(GLOB.snouts_list) + +/datum/preference/choiced/lizard_snout/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["snout"] = value + + +/datum/preference/choiced/lizard_horns + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "feature_lizard_horns" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Horns" + relevant_mutant_bodypart = "horns" + should_generate_icons = TRUE + + +/datum/preference/choiced/lizard_horns/init_possible_values() + return generate_lizard_side_shots(GLOB.horns_list, "horns", include_snout = FALSE) + +/datum/preference/choiced/lizard_horns/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["horns"] = value + + +/datum/preference/choiced/lizard_frills + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "feature_lizard_frills" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Frills" + relevant_mutant_bodypart = "frills" + should_generate_icons = TRUE + +/datum/preference/choiced/lizard_frills/init_possible_values() + return generate_lizard_side_shots(GLOB.frills_list, "frills", include_snout = FALSE) + +/datum/preference/choiced/lizard_frills/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["frills"] = value + + +/datum/preference/choiced/lizard_spines + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_lizard_spines" + savefile_identifier = PREFERENCE_CHARACTER + relevant_mutant_bodypart = "spines" + +/datum/preference/choiced/lizard_spines/init_possible_values() + return assoc_to_keys(GLOB.spines_list) + +/datum/preference/choiced/lizard_spines/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["spines"] = value + + +/datum/preference/choiced/lizard_tail + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_lizard_tail" + savefile_identifier = PREFERENCE_CHARACTER + relevant_mutant_bodypart = "tail_lizard" + +/datum/preference/choiced/lizard_tail/init_possible_values() + return assoc_to_keys(GLOB.tails_list_lizard) + +/datum/preference/choiced/lizard_tail/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["tail_lizard"] = value diff --git a/code/modules/client/preferences/species_features/moth.dm b/code/modules/client/preferences/species_features/moth.dm new file mode 100644 index 000000000000..0a9708aeeb13 --- /dev/null +++ b/code/modules/client/preferences/species_features/moth.dm @@ -0,0 +1,24 @@ +/datum/preference/choiced/moth_wings + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "feature_moth_wings" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Moth wings" + relevant_mutant_bodypart = "moth_wings" + should_generate_icons = TRUE + +/datum/preference/choiced/moth_wings/init_possible_values() + var/list/icon/values = possible_values_for_sprite_accessory_list_for_body_part( + GLOB.moth_wings_list, + "moth_wings", + list("ADJ", "FRONT"), + ) + + // Moth wings are in a stupid dimension + for (var/name in values) + values[name].Crop(1, 1, 32, 32) + + return values + +/datum/preference/choiced/moth_wings/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["moth_wings"] = value + diff --git a/code/modules/client/preferences/species_features/mutants.dm b/code/modules/client/preferences/species_features/mutants.dm new file mode 100644 index 000000000000..0f8cfff73b68 --- /dev/null +++ b/code/modules/client/preferences/species_features/mutants.dm @@ -0,0 +1,20 @@ +/datum/preference/color_legacy/mutant_color + savefile_key = "feature_mcolor" + savefile_identifier = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + relevant_species_trait = MUTCOLORS + +/datum/preference/color_legacy/mutant_color/create_default_value() + return sanitize_hexcolor("[pick("7F", "FF")][pick("7F", "FF")][pick("7F", "FF")]") + +/datum/preference/color_legacy/mutant_color/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["mcolor"] = value + +/datum/preference/color_legacy/mutant_color/is_valid(value) + if (!..(value)) + return FALSE + + if (is_color_dark(expand_three_digit_color(value), 22)) + return FALSE + + return TRUE diff --git a/code/modules/client/preferences/species_features/plasmaman.dm b/code/modules/client/preferences/species_features/plasmaman.dm new file mode 100644 index 000000000000..9dfec16fb7d2 --- /dev/null +++ b/code/modules/client/preferences/species_features/plasmaman.dm @@ -0,0 +1,24 @@ +/datum/preference/choiced/plasmaman_helmet + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "feature_plasmaman_helmet" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Plasmaman helmet" + should_generate_icons = TRUE + relevant_mutant_bodypart = "plasmaman_helmet" + +/datum/preference/choiced/plasmaman_helmet/init_possible_values() + var/list/values = list() + + for (var/helmet_name in GLOB.plasmaman_helmet_list) + var/datum/sprite_accessory/helmet_icon_suffix = GLOB.plasmaman_helmet_list[helmet_name] + var/helmet_icon = "purple_envirohelm" + if (helmet_name != "None") + helmet_icon += "-[helmet_icon_suffix]" + + var/icon/icon = icon('icons/obj/clothing/hats.dmi', helmet_icon) + values[helmet_name] = icon + + return values + +/datum/preference/choiced/plasmaman_helmet/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["plasmaman_helmet"] = value diff --git a/code/modules/client/preferences/species_features/pod.dm b/code/modules/client/preferences/species_features/pod.dm new file mode 100644 index 000000000000..9747b9a2f6e3 --- /dev/null +++ b/code/modules/client/preferences/species_features/pod.dm @@ -0,0 +1,72 @@ +/datum/preference/choiced/pod_hair + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "feature_pod_hair" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Pod hair" + should_generate_icons = TRUE + relevant_mutant_bodypart = "pod_hair" + +/datum/preference/choiced/pod_hair/init_possible_values() + var/list/values = list() + + var/icon/pod_head = icon('icons/mob/human_parts_greyscale.dmi', "pod_head_m") + + for (var/pod_name in GLOB.pod_hair_list) + var/datum/sprite_accessory/pod_hair = GLOB.pod_hair_list[pod_name] + + var/icon/icon_with_hair = new(pod_head) + var/icon/icon = icon(pod_hair.icon, pod_hair.icon_state) + icon_with_hair.Blend(icon, ICON_OVERLAY) + icon_with_hair.Scale(64, 64) + icon_with_hair.Crop(15, 64, 15 + 31, 64 - 31) + icon_with_hair.Blend(COLOR_GREEN, ICON_MULTIPLY) + + values[pod_hair.name] = icon_with_hair + + return values + +/datum/preference/choiced/pod_hair/create_default_value() + return pick(assoc_to_keys(GLOB.pod_hair_list)) + +/datum/preference/choiced/pod_hair/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["pod_hair"] = value + target.dna.features["pod_flower"] = value + + +/datum/preference/color_legacy/pod_hair_color + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "hair_color" + savefile_identifier = PREFERENCE_CHARACTER + relevant_mutant_bodypart = "pod_hair" + unique = TRUE + +/datum/preference/color_legacy/pod_hair_color/apply_to_human(mob/living/carbon/human/target, value) + target.hair_color = value + +/datum/preference/color_legacy/pod_hair_color/is_valid(value) + if (!..(value)) + return FALSE + + if (is_color_dark(expand_three_digit_color(value), 22)) + return FALSE + + return TRUE + +/datum/preference/color_legacy/pod_flower_color + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "facial_hair_color" + savefile_identifier = PREFERENCE_CHARACTER + relevant_mutant_bodypart = "pod_flower" + unique = TRUE + +/datum/preference/color_legacy/pod_flower_color/apply_to_human(mob/living/carbon/human/target, value) + target.facial_hair_color = value + +/datum/preference/color_legacy/pod_flower_color/is_valid(value) + if (!..(value)) + return FALSE + + if (is_color_dark(expand_three_digit_color(value), 22)) + return FALSE + + return TRUE diff --git a/code/modules/client/preferences/species_features/polysmorph.dm b/code/modules/client/preferences/species_features/polysmorph.dm new file mode 100644 index 000000000000..385346d36a49 --- /dev/null +++ b/code/modules/client/preferences/species_features/polysmorph.dm @@ -0,0 +1,54 @@ +/datum/preference/choiced/polysmorph_tail + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_polysmorph_tail" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Polysmorph tail" + relevant_mutant_bodypart = "tail_polysmorph" + +/datum/preference/choiced/polysmorph_tail/init_possible_values() + return GLOB.tails_list_polysmorph + +/datum/preference/choiced/polysmorph_tail/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["tail_polysmorph"] = value + + +/datum/preference/choiced/polysmorph_teeth + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_polysmorph_teeth" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Polysmorph teeth" + relevant_mutant_bodypart = "teeth" + +/datum/preference/choiced/polysmorph_teeth/init_possible_values() + return GLOB.teeth_list + +/datum/preference/choiced/polysmorph_teeth/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["teeth"] = value + + +/datum/preference/choiced/polysmorph_dome + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_polysmorph_dome" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Polysmorph dome" + relevant_mutant_bodypart = "dome" + +/datum/preference/choiced/polysmorph_dome/init_possible_values() + return GLOB.dome_list + +/datum/preference/choiced/polysmorph_dome/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["dome"] = value + + +/datum/preference/choiced/polysmorph_dorsal_tubes + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "feature_polysmorph_dorsal_tubes" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Polysmorph dorsal tubes" + relevant_mutant_bodypart = "dorsal_tubes" + +/datum/preference/choiced/polysmorph_dorsal_tubes/init_possible_values() + return GLOB.dorsal_tubes_list + +/datum/preference/choiced/polysmorph_dorsal_tubes/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["dorsal_tubes"] = value diff --git a/code/modules/client/preferences/species_features/preternis.dm b/code/modules/client/preferences/species_features/preternis.dm new file mode 100644 index 000000000000..194661d4d656 --- /dev/null +++ b/code/modules/client/preferences/species_features/preternis.dm @@ -0,0 +1,33 @@ +/datum/preference/choiced/preternis_color + category = PREFERENCE_CATEGORY_FEATURES + savefile_key = "feature_pretcolor" + savefile_identifier = PREFERENCE_CHARACTER + main_feature_name = "Preternis color" + should_generate_icons = TRUE + +/datum/preference/choiced/preternis_color/init_possible_values() + var/list/values = list() + + var/icon/preternis_base = icon('icons/mob/human_parts_greyscale.dmi', "preternis_head_m") + preternis_base.Blend(icon('icons/mob/human_parts_greyscale.dmi', "preternis_chest_m"), ICON_OVERLAY) + preternis_base.Blend(icon('icons/mob/human_parts_greyscale.dmi', "preternis_l_arm"), ICON_OVERLAY) + preternis_base.Blend(icon('icons/mob/human_parts_greyscale.dmi', "preternis_r_arm"), ICON_OVERLAY) + + var/icon/eyes = icon('icons/mob/human_face.dmi', "eyes") + eyes.Blend(COLOR_RED, ICON_MULTIPLY) + preternis_base.Blend(eyes, ICON_OVERLAY) + + preternis_base.Scale(64, 64) + preternis_base.Crop(15, 64, 15 + 31, 64 - 31) + + for (var/name in GLOB.color_list_preternis) + var/color = GLOB.color_list_preternis[name] + + var/icon/icon = new(preternis_base) + icon.Blend("#[color]", ICON_MULTIPLY) + values[name] = icon + + return values + +/datum/preference/choiced/preternis_color/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["pretcolor"] = GLOB.color_list_preternis[value] diff --git a/code/modules/client/preferences/tgui.dm b/code/modules/client/preferences/tgui.dm new file mode 100644 index 000000000000..44a8666e642b --- /dev/null +++ b/code/modules/client/preferences/tgui.dm @@ -0,0 +1,20 @@ +/datum/preference/toggle/tgui_fancy + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "tgui_fancy" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/tgui_fancy/apply_to_client(client/client, value) + for (var/datum/tgui/tgui as anything in client.mob?.tgui_open_uis) + // Force it to reload either way + tgui.update_static_data(client.mob) + +/datum/preference/toggle/tgui_lock + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "tgui_lock" + savefile_identifier = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/tgui_lock/apply_to_client(client/client, value) + for (var/datum/tgui/tgui as anything in client.mob?.tgui_open_uis) + // Force it to reload either way + tgui.update_static_data(client.mob) diff --git a/code/modules/client/preferences/tgui_prefs_migration.dm b/code/modules/client/preferences/tgui_prefs_migration.dm new file mode 100644 index 000000000000..c7ea1f5836fb --- /dev/null +++ b/code/modules/client/preferences/tgui_prefs_migration.dm @@ -0,0 +1,30 @@ +/// Handle the migrations necessary from pre-tgui prefs to post-tgui prefs +/datum/preferences/proc/migrate_preferences_to_tgui_prefs_menu() + migrate_key_bindings() + +/// Handle the migrations necessary from pre-tgui prefs to post-tgui prefs, for characters +/datum/preferences/proc/migrate_character_to_tgui_prefs_menu() + //migrate_hair() + +// Key bindings used to be "key" -> list("action"), +// such as "X" -> list("swap_hands"). +// This made it impossible to determine any order, meaning placing a new +// hotkey would produce non-deterministic order. +// tgui prefs menu moves this over to "swap_hands" -> list("X"). +/datum/preferences/proc/migrate_key_bindings() + var/new_key_bindings = list() + + for (var/unbound_hotkey in key_bindings["Unbound"]) + new_key_bindings[unbound_hotkey] = list() + + for (var/hotkey in key_bindings) + if (hotkey == "Unbound") + continue + + for (var/keybind in key_bindings[hotkey]) + if (keybind in new_key_bindings) + new_key_bindings[keybind] |= hotkey + else + new_key_bindings[keybind] = list(hotkey) + + key_bindings = new_key_bindings diff --git a/code/modules/client/preferences/tooltips.dm b/code/modules/client/preferences/tooltips.dm new file mode 100644 index 000000000000..fbbea5085b10 --- /dev/null +++ b/code/modules/client/preferences/tooltips.dm @@ -0,0 +1,15 @@ +/datum/preference/toggle/enable_tooltips + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "enable_tips" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/numeric/tooltip_delay + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "tip_delay" + savefile_identifier = PREFERENCE_PLAYER + + minimum = 0 + maximum = 5000 + +/datum/preference/numeric/tooltip_delay/create_default_value() + return 500 // in ms diff --git a/code/modules/client/preferences/ui_style.dm b/code/modules/client/preferences/ui_style.dm new file mode 100644 index 000000000000..08f1af6c7dd5 --- /dev/null +++ b/code/modules/client/preferences/ui_style.dm @@ -0,0 +1,26 @@ +/// UI style preference +/datum/preference/choiced/ui_style + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_identifier = PREFERENCE_PLAYER + savefile_key = "UI_style" + should_generate_icons = TRUE + +/datum/preference/choiced/ui_style/init_possible_values() + var/list/values = list() + + for (var/style in GLOB.available_ui_styles) + var/icon/icons = GLOB.available_ui_styles[style] + + var/icon/icon = icon(icons, "hand_r") + icon.Crop(1, 1, world.icon_size * 2, world.icon_size) + icon.Blend(icon(icons, "hand_l"), ICON_OVERLAY, world.icon_size) + + values[style] = icon + + return values + +/datum/preference/choiced/ui_style/create_default_value() + return GLOB.available_ui_styles[1] + +/datum/preference/choiced/ui_style/apply_to_client(client/client, value) + client.mob?.hud_used?.update_ui_style(ui_style2icon(value)) diff --git a/code/modules/client/preferences/uplink_location.dm b/code/modules/client/preferences/uplink_location.dm new file mode 100644 index 000000000000..b2e6056cca58 --- /dev/null +++ b/code/modules/client/preferences/uplink_location.dm @@ -0,0 +1,26 @@ +/datum/preference/choiced/uplink_location + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "uplink_loc" + can_randomize = FALSE + +/datum/preference/choiced/uplink_location/create_default_value() + return UPLINK_PDA + +/datum/preference/choiced/uplink_location/init_possible_values() + return list(UPLINK_PDA, UPLINK_RADIO, UPLINK_PEN, UPLINK_IMPLANT) + +/datum/preference/choiced/uplink_location/compile_constant_data() + var/list/data = ..() + + data[CHOICED_PREFERENCE_DISPLAY_NAMES] = list( + UPLINK_PDA = "PDA", + UPLINK_RADIO = "Radio", + UPLINK_PEN = "Pen", + UPLINK_IMPLANT = "Implant ([UPLINK_IMPLANT_TELECRYSTAL_COST]TC)", + ) + + return data + +/datum/preference/choiced/uplink_location/apply_to_human(mob/living/carbon/human/target, value) + return diff --git a/code/modules/client/preferences/widescreen.dm b/code/modules/client/preferences/widescreen.dm new file mode 100644 index 000000000000..1041a4f6f272 --- /dev/null +++ b/code/modules/client/preferences/widescreen.dm @@ -0,0 +1,7 @@ +/datum/preference/toggle/widescreen + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "widescreenpref" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/widescreen/apply_to_client(client/client, value) + client.view_size?.setDefault(getScreenSize(value)) diff --git a/code/modules/client/preferences/window_flashing.dm b/code/modules/client/preferences/window_flashing.dm new file mode 100644 index 000000000000..687315387c63 --- /dev/null +++ b/code/modules/client/preferences/window_flashing.dm @@ -0,0 +1,5 @@ +/// Enables flashing the window in your task tray for important events +/datum/preference/toggle/window_flashing + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "windowflashing" + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences_menu.dm b/code/modules/client/preferences_menu.dm new file mode 100644 index 000000000000..0a8f1653bc0f --- /dev/null +++ b/code/modules/client/preferences_menu.dm @@ -0,0 +1,25 @@ +/datum/verbs/menu/Preferences/verb/open_character_preferences() + set category = "Preferences" + set name = "Open Character Preferences" + set desc = "Open Character Preferences" + + var/datum/preferences/preferences = usr?.client?.prefs + if (!preferences) + return + + preferences.current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES + preferences.update_static_data(usr) + preferences.ui_interact(usr) + +/datum/verbs/menu/Preferences/verb/open_game_preferences() + set category = "Preferences" + set name = "Open Game Preferences" + set desc = "Open Game Preferences" + + var/datum/preferences/preferences = usr?.client?.prefs + if (!preferences) + return + + preferences.current_window = PREFERENCE_TAB_GAME_PREFERENCES + preferences.update_static_data(usr) + preferences.ui_interact(usr) diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 814ba86be77a..d1a1258850f2 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -5,7 +5,7 @@ // You do not need to raise this if you are adding new values that have sane defaults. // Only raise this value when changing the meaning/format/name/layout of an existing value // where you would want the updater procs below to run -#define SAVEFILE_VERSION_MAX 39 +#define SAVEFILE_VERSION_MAX 40 /* SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn @@ -47,23 +47,25 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car if(LAZYFIND(be_special,"Ragin")) be_special -= "Ragin" be_special += "Ragin Mages" + if (current_version < 35) toggles |= SOUND_ALT + if (current_version < 37) chat_toggles |= CHAT_TYPING_INDICATOR + if (current_version < 39) - key_bindings = (hotkeys) ? deepCopyList(GLOB.hotkey_keybinding_list_by_key) : deepCopyList(GLOB.classic_keybinding_list_by_key) + write_preference(/datum/preference/toggle/hotkeys, TRUE) + key_bindings = deepCopyList(GLOB.default_hotkeys) + key_bindings_by_key = get_key_bindings_by_key(key_bindings) parent.set_macros() - to_chat(parent, "Empty keybindings, setting default to [hotkeys ? "Hotkey" : "Classic"] mode") - return + to_chat(parent, span_userdanger("Empty keybindings, setting default to Hotkey mode")) + + if (current_version < 40) + migrate_preferences_to_tgui_prefs_menu() + /datum/preferences/proc/update_character(current_version, savefile/S) - if(current_version < 19) - pda_style = "mono" - if(current_version < 20) - pda_color = "#808000" - if((current_version < 21) && features["ethcolor"] && (features["ethcolor"] == "#9c3030")) - features["ethcolor"] = "9c3030" if(current_version < 22) job_preferences = list() //It loaded null from nonexistant savefile field. var/job_civilian_high = 0 @@ -121,66 +123,47 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car all_quirks -= "Physically Obstructive" all_quirks -= "Neat" all_quirks -= "NEET" - if(current_version < 26) //The new donator hats system obsolesces the old one entirely, we need to update. - donor_hat = null - donor_item = null - if(current_version < 27) - map = TRUE - flare = TRUE if(current_version < 28) if(!job_preferences) job_preferences = list() - if(current_version < 29) - purrbation = FALSE - if(current_version < 30) //Someone doesn't know how to code and make savefiles get corrupted - if(!ispath(donor_hat)) - donor_hat = null - if(!ispath(donor_item)) - donor_item = null if(current_version < 31) //Someone doesn't know how to code and make jukebox and autodeadmin the same thing toggles &= ~DEADMIN_ALWAYS toggles &= ~DEADMIN_ANTAGONIST toggles &= ~DEADMIN_POSITION_HEAD toggles &= ~DEADMIN_POSITION_SECURITY toggles &= ~DEADMIN_POSITION_SILICON //This last one is technically a no-op but it looks cleaner and less like someone forgot - if(current_version < 32) // Changed skillcape storage - if(skillcape != 1) - var/path = subtypesof(/datum/skillcape)[skillcape] - var/datum/skillcape/cape = new path() - skillcape_id = cape.id - qdel(cape) - if(current_version < 33) //Reset map preference to no choice - if(preferred_map) - to_chat(parent, span_userdanger("Your preferred map has been reset to nothing. Please set it to the map you wish to play on.")) - preferred_map = null if(current_version < 34) // default to on toggles |= SOUND_VOX + + if (current_version < 40) + migrate_character_to_tgui_prefs_menu() /// checks through keybindings for outdated unbound keys and updates them /datum/preferences/proc/check_keybindings() if(!parent) return - var/list/user_binds = list() - for (var/key in key_bindings) - for(var/kb_name in key_bindings[key]) - user_binds[kb_name] += list(key) + var/list/binds_by_key = get_key_bindings_by_key(key_bindings) var/list/notadded = list() for (var/name in GLOB.keybindings_by_name) var/datum/keybinding/kb = GLOB.keybindings_by_name[name] - if(length(user_binds[kb.name])) + if(kb.name in key_bindings) continue // key is unbound and or bound to something + var/addedbind = FALSE - if(hotkeys) + key_bindings[kb.name] = list() + + if(parent.hotkeys) for(var/hotkeytobind in kb.hotkey_keys) - if(!length(key_bindings[hotkeytobind]) || hotkeytobind == "Unbound") //Only bind to the key if nothing else is bound expect for Unbound - LAZYADD(key_bindings[hotkeytobind], kb.name) + if(!length(binds_by_key[hotkeytobind]) && hotkeytobind != "Unbound") //Only bind to the key if nothing else is bound expect for Unbound + key_bindings[kb.name] |= hotkeytobind addedbind = TRUE else for(var/classickeytobind in kb.classic_keys) - if(!length(key_bindings[classickeytobind]) || classickeytobind == "Unbound") //Only bind to the key if nothing else is bound expect for Unbound - LAZYADD(key_bindings[classickeytobind], kb.name) + if(!length(binds_by_key[classickeytobind]) && classickeytobind != "Unbound") //Only bind to the key if nothing else is bound expect for Unbound + key_bindings[kb.name] |= classickeytobind addedbind = TRUE + if(!addedbind) notadded += kb save_preferences() //Save the players pref so that new keys that were set to Unbound as default are permanently stored @@ -189,7 +172,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car /datum/preferences/proc/announce_conflict(list/notadded) to_chat(parent, "Keybinding Conflict\n\ - There are new keybindings that default to keys you've already bound. The new ones will be unbound.") + There are new keybindings that default to keys you've already bound. The new ones will be unbound.") for(var/item in notadded) var/datum/keybinding/conflicted = item to_chat(parent, span_danger("[conflicted.category]: [conflicted.full_name] needs updating")) @@ -212,144 +195,42 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car var/needs_update = savefile_needs_update(S) if(needs_update == -2) //fatal, can't load any data + var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup + if (fexists(bacpath)) + fdel(bacpath) //only keep 1 version of backup + fcopy(S, bacpath) //byond helpfully lets you use a savefile for the first arg. return FALSE + + apply_all_client_preferences() //general preferences - READ_FILE(S["asaycolor"], asaycolor) - READ_FILE(S["ooccolor"], ooccolor) READ_FILE(S["lastchangelog"], lastchangelog) - READ_FILE(S["UI_style"], UI_style) - READ_FILE(S["hotkeys"], hotkeys) - READ_FILE(S["chat_on_map"], chat_on_map) - READ_FILE(S["max_chat_length"], max_chat_length) - READ_FILE(S["see_chat_non_mob"] , see_chat_non_mob) - READ_FILE(S["see_rc_emotes"] , see_rc_emotes) - READ_FILE(S["tgui_fancy"], tgui_fancy) - READ_FILE(S["tgui_lock"], tgui_lock) - READ_FILE(S["buttons_locked"], buttons_locked) - READ_FILE(S["windowflash"], windowflashing) READ_FILE(S["be_special"] , be_special) - READ_FILE(S["player_alt_titles"], player_alt_titles) - READ_FILE(S["default_slot"], default_slot) + + READ_FILE(S["toggles"], toggles) READ_FILE(S["chat_toggles"], chat_toggles) READ_FILE(S["extra_toggles"], extra_toggles) - READ_FILE(S["toggles"], toggles) - READ_FILE(S["ghost_form"], ghost_form) - READ_FILE(S["ghost_orbit"], ghost_orbit) - READ_FILE(S["ghost_accs"], ghost_accs) - READ_FILE(S["ghost_others"], ghost_others) - READ_FILE(S["preferred_map"], preferred_map) - READ_FILE(S["ignoring"], ignoring) - READ_FILE(S["ghost_hud"], ghost_hud) - READ_FILE(S["inquisitive_ghost"], inquisitive_ghost) - READ_FILE(S["uses_glasses_colour"], uses_glasses_colour) - READ_FILE(S["clientfps"], clientfps) - READ_FILE(S["parallax"], parallax) - READ_FILE(S["ambientocclusion"], ambientocclusion) - READ_FILE(S["auto_fit_viewport"], auto_fit_viewport) - READ_FILE(S["widescreenpref"], widescreenpref) - READ_FILE(S["pixel_size"], pixel_size) - READ_FILE(S["scaling_method"], scaling_method) - READ_FILE(S["menuoptions"], menuoptions) - READ_FILE(S["enable_tips"], enable_tips) - READ_FILE(S["tip_delay"], tip_delay) - READ_FILE(S["pda_style"], pda_style) - READ_FILE(S["pda_color"], pda_color) - READ_FILE(S["pda_theme"], pda_theme) - READ_FILE(S["id_in_pda"], id_in_pda) - - READ_FILE(S["skillcape"], skillcape) - READ_FILE(S["skillcape_id"], skillcape_id) - READ_FILE(S["map"], map) - READ_FILE(S["flare"], flare) - READ_FILE(S["bar_choice"], bar_choice) - READ_FILE(S["show_credits"], show_credits) - READ_FILE(S["alternative_announcers"], disable_alternative_announcers) - READ_FILE(S["balloon_alerts"], disable_balloon_alerts) - READ_FILE(S["key_bindings"], key_bindings) - - - // yogs start - Donor features - READ_FILE(S["donor_pda"], donor_pda) - READ_FILE(S["donor_hat"], donor_hat) - READ_FILE(S["borg_hat"], borg_hat) - READ_FILE(S["donor_item"], donor_item) - READ_FILE(S["purrbation"], purrbation) READ_FILE(S["yogtoggles"], yogtoggles) + + READ_FILE(S["ignoring"], ignoring) - READ_FILE(S["accent"], accent) // Accents, too! - - READ_FILE(S["mood_tail_wagging"], mood_tail_wagging) - // yogs end - check_keybindings() + READ_FILE(S["key_bindings"], key_bindings) //try to fix any outdated data if necessary if(needs_update >= 0) update_preferences(needs_update, S) //needs_update = savefile_version if we need an update (positive integer) + // this apparently fails every time and overwrites any unloaded prefs with the default values, so don't load anything after this line or it won't actually save + check_keybindings() + key_bindings_by_key = get_key_bindings_by_key(key_bindings) + //Sanitize - asaycolor = sanitize_ooccolor(sanitize_hexcolor(asaycolor, 6, 1, initial(asaycolor))) - ooccolor = sanitize_ooccolor(sanitize_hexcolor(ooccolor, 6, 1, initial(ooccolor))) - lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog)) - UI_style = sanitize_inlist(UI_style, GLOB.available_ui_styles, GLOB.available_ui_styles[1]) - hotkeys = sanitize_integer(hotkeys, FALSE, TRUE, initial(hotkeys)) - chat_on_map = sanitize_integer(chat_on_map, FALSE, TRUE, initial(chat_on_map)) - max_chat_length = sanitize_integer(max_chat_length, 1, CHAT_MESSAGE_MAX_LENGTH, initial(max_chat_length)) - see_chat_non_mob = sanitize_integer(see_chat_non_mob, FALSE, TRUE, initial(see_chat_non_mob)) - see_rc_emotes = sanitize_integer(see_rc_emotes, FALSE, TRUE, initial(see_rc_emotes)) - tgui_fancy = sanitize_integer(tgui_fancy, FALSE, TRUE, initial(tgui_fancy)) - tgui_lock = sanitize_integer(tgui_lock, FALSE, TRUE, initial(tgui_lock)) - buttons_locked = sanitize_integer(buttons_locked, FALSE, TRUE, initial(buttons_locked)) - windowflashing = sanitize_integer(windowflashing, FALSE, TRUE, initial(windowflashing)) - default_slot = sanitize_integer(default_slot, 1, max_save_slots, initial(default_slot)) - toggles = sanitize_integer(toggles, 0, ~0, initial(toggles)) // Yogs -- Fixes toggles not having >16 bits of flagspace - clientfps = sanitize_integer(clientfps, 0, 1000, 0) - parallax = sanitize_integer(parallax, PARALLAX_INSANE, PARALLAX_DISABLE, null) - ambientocclusion = sanitize_integer(ambientocclusion, FALSE, TRUE, initial(ambientocclusion)) - auto_fit_viewport = sanitize_integer(auto_fit_viewport, FALSE, TRUE, initial(auto_fit_viewport)) - widescreenpref = sanitize_integer(widescreenpref, FALSE, TRUE, initial(widescreenpref)) - pixel_size = sanitize_integer(pixel_size, PIXEL_SCALING_AUTO, PIXEL_SCALING_3X, initial(pixel_size)) - scaling_method = sanitize_text(scaling_method, initial(scaling_method)) - ghost_form = sanitize_inlist(ghost_form, GLOB.ghost_forms, initial(ghost_form)) - ghost_orbit = sanitize_inlist(ghost_orbit, GLOB.ghost_orbits, initial(ghost_orbit)) - ghost_accs = sanitize_inlist(ghost_accs, GLOB.ghost_accs_options, GHOST_ACCS_DEFAULT_OPTION) - ghost_others = sanitize_inlist(ghost_others, GLOB.ghost_others_options, GHOST_OTHERS_DEFAULT_OPTION) - menuoptions = SANITIZE_LIST(menuoptions) - be_special = SANITIZE_LIST(be_special) - pda_style = sanitize_inlist(pda_style, GLOB.pda_styles, initial(pda_style)) - pda_color = sanitize_hexcolor(pda_color, 6, 1, initial(pda_color)) - pda_theme = sanitize_inlist(pda_theme, GLOB.pda_themes, initial(pda_theme)) - skillcape = sanitize_integer(skillcape, 1, 82, initial(skillcape)) - skillcape_id = sanitize_text(skillcape_id, initial(skillcape_id)) - - if(skillcape_id != "None" && !(skillcape_id in GLOB.skillcapes)) - skillcape_id = "None" - - map = sanitize_integer(map, FALSE, TRUE, initial(map)) - flare = sanitize_integer(flare, FALSE, TRUE, initial(flare)) - bar_choice = sanitize_text(bar_choice, initial(bar_choice)) - disable_alternative_announcers = sanitize_integer(disable_alternative_announcers, FALSE, TRUE, initial(disable_alternative_announcers)) - disable_balloon_alerts = sanitize_integer(disable_balloon_alerts, FALSE, TRUE, initial(disable_balloon_alerts)) - key_bindings = sanitize_islist(key_bindings, list()) - - var/bar_sanitize = FALSE - for(var/A in GLOB.potential_box_bars) - if(bar_choice == A) - bar_sanitize = TRUE - break - if(!bar_sanitize) - bar_choice = "Random" - if(!player_alt_titles) player_alt_titles = new() - show_credits = sanitize_integer(show_credits, FALSE, TRUE, initial(show_credits)) - - // yogs start - Donor features & yogtoggles - yogtoggles = sanitize_integer(yogtoggles, 0, (1 << 23), initial(yogtoggles)) - donor_pda = sanitize_integer(donor_pda, 1, GLOB.donor_pdas.len, 1) - purrbation = sanitize_integer(purrbation, FALSE, TRUE, initial(purrbation)) - - accent = sanitize_text(accent, initial(accent)) // Can't use sanitize_inlist since it doesn't support falsely default values. - // yogs end + lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog)) + default_slot = sanitize_integer(default_slot, 1, max_save_slots, initial(default_slot)) + toggles = sanitize_integer(toggles, 0, ~0, initial(toggles)) // Yogs -- Fixes toggles not having >16 bits of flagspace + be_special = sanitize_be_special(SANITIZE_LIST(be_special)) + key_bindings = sanitize_keybindings(key_bindings) return TRUE @@ -363,79 +244,45 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car WRITE_FILE(S["version"] , SAVEFILE_VERSION_MAX) //updates (or failing that the sanity checks) will ensure data is not invalid at load. Assume up-to-date + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + if (preference.savefile_identifier != PREFERENCE_PLAYER) + continue + + if (!(preference.type in recently_updated_keys)) + continue + + recently_updated_keys -= preference.type + + if (preference_type in value_cache) + write_preference(preference, preference.serialize(value_cache[preference_type])) + //general preferences - WRITE_FILE(S["asaycolor"], asaycolor) - WRITE_FILE(S["ooccolor"], ooccolor) WRITE_FILE(S["lastchangelog"], lastchangelog) - WRITE_FILE(S["UI_style"], UI_style) - WRITE_FILE(S["hotkeys"], hotkeys) - WRITE_FILE(S["chat_on_map"], chat_on_map) - WRITE_FILE(S["max_chat_length"], max_chat_length) - WRITE_FILE(S["see_chat_non_mob"], see_chat_non_mob) - WRITE_FILE(S["see_rc_emotes"], see_rc_emotes) - WRITE_FILE(S["tgui_fancy"], tgui_fancy) - WRITE_FILE(S["tgui_lock"], tgui_lock) - WRITE_FILE(S["buttons_locked"], buttons_locked) - WRITE_FILE(S["windowflash"], windowflashing) WRITE_FILE(S["be_special"], be_special) - WRITE_FILE(S["player_alt_titles"], player_alt_titles) WRITE_FILE(S["default_slot"], default_slot) + WRITE_FILE(S["toggles"], toggles) WRITE_FILE(S["chat_toggles"], chat_toggles) WRITE_FILE(S["extra_toggles"], extra_toggles) - WRITE_FILE(S["ghost_form"], ghost_form) - WRITE_FILE(S["ghost_orbit"], ghost_orbit) - WRITE_FILE(S["ghost_accs"], ghost_accs) - WRITE_FILE(S["ghost_others"], ghost_others) - WRITE_FILE(S["preferred_map"], preferred_map) - WRITE_FILE(S["ignoring"], ignoring) - WRITE_FILE(S["ghost_hud"], ghost_hud) - WRITE_FILE(S["inquisitive_ghost"], inquisitive_ghost) - WRITE_FILE(S["uses_glasses_colour"], uses_glasses_colour) - WRITE_FILE(S["clientfps"], clientfps) - WRITE_FILE(S["parallax"], parallax) - WRITE_FILE(S["ambientocclusion"], ambientocclusion) - WRITE_FILE(S["auto_fit_viewport"], auto_fit_viewport) - WRITE_FILE(S["widescreenpref"], widescreenpref) - WRITE_FILE(S["pixel_size"], pixel_size) - WRITE_FILE(S["scaling_method"], scaling_method) - WRITE_FILE(S["menuoptions"], menuoptions) - WRITE_FILE(S["enable_tips"], enable_tips) - WRITE_FILE(S["tip_delay"], tip_delay) - WRITE_FILE(S["pda_style"], pda_style) - WRITE_FILE(S["pda_color"], pda_color) - WRITE_FILE(S["pda_theme"], pda_theme) - WRITE_FILE(S["id_in_pda"], id_in_pda) - WRITE_FILE(S["skillcape"], skillcape) - WRITE_FILE(S["skillcape_id"], skillcape_id) - WRITE_FILE(S["show_credits"], show_credits) - WRITE_FILE(S["map"], map) - WRITE_FILE(S["flare"], flare) - WRITE_FILE(S["bar_choice"], bar_choice) - WRITE_FILE(S["alternative_announcers"], disable_alternative_announcers) - WRITE_FILE(S["balloon_alerts"], disable_balloon_alerts) - WRITE_FILE(S["key_bindings"], key_bindings) - - // yogs start - Donor features & Yogstoggle WRITE_FILE(S["yogtoggles"], yogtoggles) - WRITE_FILE(S["donor_pda"], donor_pda) - WRITE_FILE(S["donor_hat"], donor_hat) - WRITE_FILE(S["borg_hat"], borg_hat) - WRITE_FILE(S["donor_item"], donor_item) - WRITE_FILE(S["purrbation"], purrbation) - WRITE_FILE(S["accent"], accent) // Accents, too! - - WRITE_FILE(S["mood_tail_wagging"], mood_tail_wagging) - // yogs end + WRITE_FILE(S["ignoring"], ignoring) + + WRITE_FILE(S["key_bindings"], key_bindings) return TRUE /datum/preferences/proc/load_character(slot) + SHOULD_NOT_SLEEP(TRUE) + if(!path) return FALSE if(!fexists(path)) return FALSE + + character_savefile = null + var/savefile/S = new /savefile(path) if(!S) return FALSE @@ -451,93 +298,22 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car var/needs_update = savefile_needs_update(S) if(needs_update == -2) //fatal, can't load any data return FALSE + + // Read everything into cache + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + if (preference.savefile_identifier != PREFERENCE_CHARACTER) + continue - //Species - var/species_id - READ_FILE(S["species"], species_id) - if(species_id) - var/newtype = GLOB.species_list[species_id] - if(newtype) - pref_species = new newtype - - if(!S["features["mcolor"]"] || S["features["mcolor"]"] == "#000") - WRITE_FILE(S["features["mcolor"]"] , "#FFF") - - if(!S["feature_ethcolor"] || S["feature_ethcolor"] == "#000") - WRITE_FILE(S["feature_ethcolor"] , "9c3030") - - if(!S["feature_pretcolor"] || S["feature_pretcolor"] == "#000") - WRITE_FILE(S["feature_pretcolor"] , "9c3030") + value_cache -= preference_type + read_preference(preference_type) //Character - READ_FILE(S["real_name"], real_name) - READ_FILE(S["name_is_always_random"], be_random_name) - READ_FILE(S["body_is_always_random"], be_random_body) - READ_FILE(S["gender"], gender) - READ_FILE(S["age"], age) - READ_FILE(S["hair_color"], hair_color) - READ_FILE(S["facial_hair_color"], facial_hair_color) - READ_FILE(S["eye_color"], eye_color) - READ_FILE(S["skin_tone"], skin_tone) - READ_FILE(S["hair_style_name"], hair_style) - READ_FILE(S["facial_style_name"], facial_hair_style) - READ_FILE(S["underwear"], underwear) - READ_FILE(S["undershirt"], undershirt) - READ_FILE(S["socks"], socks) - READ_FILE(S["backbag"], backbag) - READ_FILE(S["jumpsuit_style"], jumpsuit_style) - READ_FILE(S["uplink_loc"], uplink_spawn_loc) - READ_FILE(S["feature_mcolor"], features["mcolor"]) - READ_FILE(S["feature_gradientstyle"], features["gradientstyle"]) - READ_FILE(S["feature_gradientcolor"], features["gradientcolor"]) - READ_FILE(S["feature_ethcolor"], features["ethcolor"]) - READ_FILE(S["feature_pretcolor"], features["pretcolor"]) - READ_FILE(S["feature_lizard_tail"], features["tail_lizard"]) - READ_FILE(S["feature_lizard_snout"], features["snout"]) - READ_FILE(S["feature_lizard_horns"], features["horns"]) - READ_FILE(S["feature_lizard_frills"], features["frills"]) - READ_FILE(S["feature_lizard_spines"], features["spines"]) - READ_FILE(S["feature_lizard_body_markings"], features["body_markings"]) - READ_FILE(S["feature_lizard_legs"], features["legs"]) - READ_FILE(S["feature_moth_wings"], features["moth_wings"]) - READ_FILE(S["feature_polysmorph_tail"], features["tail_polysmorph"]) - READ_FILE(S["feature_polysmorph_teeth"], features["teeth"]) - READ_FILE(S["feature_polysmorph_dome"], features["dome"]) - READ_FILE(S["feature_polysmorph_dorsal_tubes"], features["dorsal_tubes"]) - READ_FILE(S["feature_ethereal_mark"], features["ethereal_mark"]) - READ_FILE(S["feature_pod_hair"], features["pod_hair"]) - READ_FILE(S["feature_pod_flower"], features["pod_flower"]) - READ_FILE(S["feature_ipc_screen"], features["ipc_screen"]) - READ_FILE(S["feature_ipc_antenna"], features["ipc_antenna"]) - READ_FILE(S["feature_ipc_chassis"], features["ipc_chassis"]) - READ_FILE(S["feature_plasmaman_helmet"], features["plasmaman_helmet"]) - - READ_FILE(S["persistent_scars"], persistent_scars) - if(!CONFIG_GET(flag/join_with_mutant_humans)) - features["tail_human"] = "none" - features["ears"] = "none" - else - READ_FILE(S["feature_human_tail"], features["tail_human"]) - READ_FILE(S["feature_human_ears"], features["ears"]) - - //Custom names - for(var/custom_name_id in GLOB.preferences_custom_names) - var/savefile_slot_name = custom_name_id + "_name" //TODO remove this - READ_FILE(S[savefile_slot_name], custom_names[custom_name_id]) - - READ_FILE(S["preferred_ai_core_display"], preferred_ai_core_display) - READ_FILE(S["prefered_security_department"], prefered_security_department) - READ_FILE(S["prefered_engineering_department"], prefered_engineering_department) - - //Jobs - READ_FILE(S["joblessrole"], joblessrole) - //Load prefs + READ_FILE(S["randomise"], randomise) + //Load prefs READ_FILE(S["job_preferences"], job_preferences) - if(!job_preferences) - job_preferences = list() - //Quirks READ_FILE(S["all_quirks"], all_quirks) @@ -546,95 +322,22 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car update_character(needs_update, S) //needs_update == savefile_version if we need an update (positive integer) //Sanitize + randomise = SANITIZE_LIST(randomise) + all_quirks = SANITIZE_LIST(all_quirks) - real_name = reject_bad_name(real_name, pref_species.allow_numbers_in_name) - gender = sanitize_gender(gender) - if(!real_name) - real_name = random_unique_name(gender) - - for(var/custom_name_id in GLOB.preferences_custom_names) - var/namedata = GLOB.preferences_custom_names[custom_name_id] - custom_names[custom_name_id] = reject_bad_name(custom_names[custom_name_id],namedata["allow_numbers"]) - if(!custom_names[custom_name_id]) - custom_names[custom_name_id] = get_default_name(custom_name_id) - - if(!features["mcolor"] || features["mcolor"] == "#000") - features["mcolor"] = pick("FFFFFF","7F7F7F", "7FFF7F", "7F7FFF", "FF7F7F", "7FFFFF", "FF7FFF", "FFFF7F") - - if(!features["ethcolor"] || features["ethcolor"] == "#000") - features["ethcolor"] = GLOB.color_list_ethereal[pick(GLOB.color_list_ethereal)] - - if(!features["pretcolor"] || features["pretcolor"] == "#000") - features["pretcolor"] = GLOB.color_list_preternis[pick(GLOB.color_list_preternis)] - - be_random_name = sanitize_integer(be_random_name, 0, 1, initial(be_random_name)) - be_random_body = sanitize_integer(be_random_body, 0, 1, initial(be_random_body)) - - if(gender == MALE) - hair_style = sanitize_inlist(hair_style, GLOB.hair_styles_male_list) - facial_hair_style = sanitize_inlist(facial_hair_style, GLOB.facial_hair_styles_male_list) - underwear = sanitize_inlist(underwear, GLOB.underwear_m) - undershirt = sanitize_inlist(undershirt, GLOB.undershirt_m) - else if(gender == FEMALE) - hair_style = sanitize_inlist(hair_style, GLOB.hair_styles_female_list) - facial_hair_style = sanitize_inlist(facial_hair_style, GLOB.facial_hair_styles_female_list) - underwear = sanitize_inlist(underwear, GLOB.underwear_f) - undershirt = sanitize_inlist(undershirt, GLOB.undershirt_f) - else - hair_style = sanitize_inlist(hair_style, GLOB.hair_styles_list) - facial_hair_style = sanitize_inlist(facial_hair_style, GLOB.facial_hair_styles_list) - underwear = sanitize_inlist(underwear, GLOB.underwear_list) - undershirt = sanitize_inlist(undershirt, GLOB.undershirt_list) - - - socks = sanitize_inlist(socks, GLOB.socks_list) - age = sanitize_integer(age, AGE_MIN, AGE_MAX, initial(age)) - hair_color = sanitize_hexcolor(hair_color, 3, 0) - facial_hair_color = sanitize_hexcolor(facial_hair_color, 3, 0) - eye_color = sanitize_hexcolor(eye_color, 3, 0) - skin_tone = sanitize_inlist(skin_tone, GLOB.skin_tones) - backbag = sanitize_inlist(backbag, GLOB.backbaglist, initial(backbag)) - jumpsuit_style = sanitize_inlist(jumpsuit_style, GLOB.jumpsuitlist, initial(jumpsuit_style)) - uplink_spawn_loc = sanitize_inlist(uplink_spawn_loc, GLOB.uplink_spawn_loc_list, initial(uplink_spawn_loc)) - features["mcolor"] = sanitize_hexcolor(features["mcolor"], 3, 0) - features["gradientstyle"] = sanitize_inlist(features["gradientstyle"], GLOB.hair_gradients_list) - features["gradientcolor"] = sanitize_hexcolor(features["gradientcolor"], 3, 0) - features["ethcolor"] = copytext_char(features["ethcolor"], 1, 7) - features["pretcolor"] = copytext_char(features["pretcolor"], 1, 7) - features["tail_lizard"] = sanitize_inlist(features["tail_lizard"], GLOB.tails_list_lizard) - features["tail_polysmorph"] = sanitize_inlist(features["tail_polysmorph"], GLOB.tails_list_polysmorph) - features["tail_human"] = sanitize_inlist(features["tail_human"], GLOB.tails_list_human, "None") - features["snout"] = sanitize_inlist(features["snout"], GLOB.snouts_list) - features["horns"] = sanitize_inlist(features["horns"], GLOB.horns_list) - features["ears"] = sanitize_inlist(features["ears"], GLOB.ears_list, "None") - features["frills"] = sanitize_inlist(features["frills"], GLOB.frills_list) - features["spines"] = sanitize_inlist(features["spines"], GLOB.spines_list) - features["body_markings"] = sanitize_inlist(features["body_markings"], GLOB.body_markings_list) - features["feature_lizard_legs"] = sanitize_inlist(features["legs"], GLOB.legs_list, "Normal Legs") - features["moth_wings"] = sanitize_inlist(features["moth_wings"], GLOB.moth_wings_list, "Plain") - features["teeth"] = sanitize_inlist(features["teeth"], GLOB.teeth_list) - features["dome"] = sanitize_inlist(features["dome"], GLOB.dome_list) - features["dorsal_tubes"] = sanitize_inlist(features["dorsal_tubes"], GLOB.dorsal_tubes_list) - features["ethereal_mark"] = sanitize_inlist(features["ethereal_mark"], GLOB.ethereal_mark_list) - features["pod_hair"] = sanitize_inlist(features["pod_hair"], GLOB.pod_hair_list) - features["pod_flower"] = sanitize_inlist(features["pod_flower"], GLOB.pod_flower_list) - features["ipc_screen"] = sanitize_inlist(features["ipc_screen"], GLOB.ipc_screens_list) - features["ipc_antenna"] = sanitize_inlist(features["ipc_antenna"], GLOB.ipc_antennas_list) - features["ipc_chassis"] = sanitize_inlist(features["ipc_chassis"], GLOB.ipc_chassis_list) - - persistent_scars = sanitize_integer(persistent_scars) - - joblessrole = sanitize_integer(joblessrole, 1, 3, initial(joblessrole)) //Validate job prefs for(var/j in job_preferences) if(job_preferences[j] != JP_LOW && job_preferences[j] != JP_MEDIUM && job_preferences[j] != JP_HIGH) job_preferences -= j - all_quirks = SANITIZE_LIST(all_quirks) + //all_quirks = SSquirks.filter_invalid_quirks(all_quirks, parent) + validate_quirks() return TRUE /datum/preferences/proc/save_character() + SHOULD_NOT_SLEEP(TRUE) + if(!path) return FALSE var/savefile/S = new /savefile(path) @@ -642,75 +345,48 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car return FALSE S.cd = "/character[default_slot]" - WRITE_FILE(S["version"] , SAVEFILE_VERSION_MAX) //load_character will sanitize any bad data, so assume up-to-date.) + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (preference.savefile_identifier != PREFERENCE_CHARACTER) + continue + + if (!(preference.type in recently_updated_keys)) + continue + + recently_updated_keys -= preference.type + + if (preference.type in value_cache) + write_preference(preference, preference.serialize(value_cache[preference.type])) + + WRITE_FILE(S["version"], SAVEFILE_VERSION_MAX) //load_character will sanitize any bad data, so assume up-to-date.) //Character - WRITE_FILE(S["real_name"] , real_name) - WRITE_FILE(S["name_is_always_random"] , be_random_name) - WRITE_FILE(S["body_is_always_random"] , be_random_body) - WRITE_FILE(S["gender"] , gender) - WRITE_FILE(S["age"] , age) - WRITE_FILE(S["hair_color"] , hair_color) - WRITE_FILE(S["facial_hair_color"] , facial_hair_color) - WRITE_FILE(S["eye_color"] , eye_color) - WRITE_FILE(S["skin_tone"] , skin_tone) - WRITE_FILE(S["hair_style_name"] , hair_style) - WRITE_FILE(S["facial_style_name"] , facial_hair_style) - WRITE_FILE(S["underwear"] , underwear) - WRITE_FILE(S["undershirt"] , undershirt) - WRITE_FILE(S["socks"] , socks) - WRITE_FILE(S["backbag"] , backbag) - WRITE_FILE(S["jumpsuit_style"] , jumpsuit_style) - WRITE_FILE(S["uplink_loc"] , uplink_spawn_loc) - WRITE_FILE(S["species"] , pref_species.id) - WRITE_FILE(S["feature_mcolor"] , features["mcolor"]) - WRITE_FILE(S["feature_gradientstyle"] , features["gradientstyle"]) - WRITE_FILE(S["feature_gradientcolor"] , features["gradientcolor"]) - WRITE_FILE(S["feature_ethcolor"] , features["ethcolor"]) - WRITE_FILE(S["feature_pretcolor"] , features["pretcolor"]) - WRITE_FILE(S["feature_lizard_tail"] , features["tail_lizard"]) - WRITE_FILE(S["feature_polysmorph_tail"] , features["tail_polysmorph"]) - WRITE_FILE(S["feature_human_tail"] , features["tail_human"]) - WRITE_FILE(S["feature_lizard_snout"] , features["snout"]) - WRITE_FILE(S["feature_lizard_horns"] , features["horns"]) - WRITE_FILE(S["feature_human_ears"] , features["ears"]) - WRITE_FILE(S["feature_lizard_frills"] , features["frills"]) - WRITE_FILE(S["feature_lizard_spines"] , features["spines"]) - WRITE_FILE(S["feature_lizard_body_markings"] , features["body_markings"]) - WRITE_FILE(S["feature_lizard_legs"] , features["legs"]) - WRITE_FILE(S["feature_moth_wings"] , features["moth_wings"]) - WRITE_FILE(S["feature_polysmorph_teeth"] , features["teeth"]) - WRITE_FILE(S["feature_polysmorph_dome"] , features["dome"]) - WRITE_FILE(S["feature_polysmorph_dorsal_tubes"] , features["dorsal_tubes"]) - WRITE_FILE(S["feature_ethereal_mark"] , features["ethereal_mark"]) - WRITE_FILE(S["feature_pod_hair"] , features["pod_hair"]) - WRITE_FILE(S["feature_pod_flower"] , features["pod_flower"]) - WRITE_FILE(S["persistent_scars"] , persistent_scars) - WRITE_FILE(S["feature_ipc_screen"] , features["ipc_screen"]) - WRITE_FILE(S["feature_ipc_antenna"] , features["ipc_antenna"]) - WRITE_FILE(S["feature_ipc_chassis"] , features["ipc_chassis"]) - WRITE_FILE(S["feature_plasmaman_helmet"] , features["plasmaman_helmet"]) - - //Custom names - for(var/custom_name_id in GLOB.preferences_custom_names) - var/savefile_slot_name = custom_name_id + "_name" //TODO remove this - WRITE_FILE(S[savefile_slot_name],custom_names[custom_name_id]) - - WRITE_FILE(S["preferred_ai_core_display"] , preferred_ai_core_display) - WRITE_FILE(S["prefered_security_department"] , prefered_security_department) - WRITE_FILE(S["prefered_engineering_department"] , prefered_engineering_department) - - //Jobs - WRITE_FILE(S["joblessrole"] , joblessrole) + WRITE_FILE(S["randomise"] , randomise) + //Write prefs WRITE_FILE(S["job_preferences"] , job_preferences) //Quirks - WRITE_FILE(S["all_quirks"] , all_quirks) + WRITE_FILE(S["all_quirks"] , all_quirks) return TRUE +/datum/preferences/proc/sanitize_be_special(list/input_be_special) + var/list/output = list() + + for (var/role in input_be_special) + if (role in GLOB.special_roles) + output += role + + return output.len == input_be_special.len ? input_be_special : output + +/proc/sanitize_keybindings(value) + var/list/base_bindings = sanitize_islist(value,list()) + for(var/keybind_name in base_bindings) + if (!(keybind_name in GLOB.keybindings_by_name)) + base_bindings -= keybind_name + return base_bindings + #undef SAVEFILE_VERSION_MAX #undef SAVEFILE_VERSION_MIN diff --git a/code/modules/client/preferences_toggles.dm b/code/modules/client/preferences_toggles.dm deleted file mode 100644 index 9c0568a4ff0b..000000000000 --- a/code/modules/client/preferences_toggles.dm +++ /dev/null @@ -1,521 +0,0 @@ -//this works as is to create a single checked item, but has no back end code for toggleing the check yet -#define TOGGLE_CHECKBOX(PARENT, CHILD) PARENT/CHILD/abstract = TRUE;PARENT/CHILD/checkbox = CHECKBOX_TOGGLE;PARENT/CHILD/verb/CHILD - -//Example usage TOGGLE_CHECKBOX(datum/verbs/menu/Settings/Ghost/chatterbox, toggle_ghost_ears)() - -//override because we don't want to save preferences twice. -/datum/verbs/menu/Settings/Set_checked(client/C, verbpath) - if (checkbox == CHECKBOX_GROUP) - C.prefs.menuoptions[type] = verbpath - else if (checkbox == CHECKBOX_TOGGLE) - var/checked = Get_checked(C) - C.prefs.menuoptions[type] = !checked - winset(C, "[verbpath]", "is-checked = [!checked]") - -/datum/verbs/menu/Settings/verb/setup_character() - set name = "Game Preferences" - set category = "Preferences" - set desc = "Open Game Preferences Window" - usr.client.prefs.current_tab = 1 - usr.client.prefs.ShowChoices(usr) - -//toggles -/datum/verbs/menu/Settings/Ghost/chatterbox - name = "Chat Box Spam" - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Ghost/chatterbox, toggle_ghost_ears)() - set name = "Show/Hide GhostEars" - set category = "Preferences" - set desc = "See All Speech" - usr.client.prefs.chat_toggles ^= CHAT_GHOSTEARS - to_chat(usr, "As a ghost, you will now [(usr.client.prefs.chat_toggles & CHAT_GHOSTEARS) ? "see all speech in the world" : "only see speech from nearby mobs"].") - usr.client.prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Ears", "[usr.client.prefs.chat_toggles & CHAT_GHOSTEARS ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Ghost/chatterbox/toggle_ghost_ears/Get_checked(client/C) - return C.prefs.chat_toggles & CHAT_GHOSTEARS - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Ghost/chatterbox, toggle_ghost_sight)() - set name = "Show/Hide GhostSight" - set category = "Preferences" - set desc = "See All Emotes" - usr.client.prefs.chat_toggles ^= CHAT_GHOSTSIGHT - to_chat(usr, "As a ghost, you will now [(usr.client.prefs.chat_toggles & CHAT_GHOSTSIGHT) ? "see all emotes in the world" : "only see emotes from nearby mobs"].") - usr.client.prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Sight", "[usr.client.prefs.chat_toggles & CHAT_GHOSTSIGHT ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Ghost/chatterbox/toggle_ghost_sight/Get_checked(client/C) - return C.prefs.chat_toggles & CHAT_GHOSTSIGHT - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Ghost/chatterbox, toggle_ghost_whispers)() - set name = "Show/Hide GhostWhispers" - set category = "Preferences" - set desc = "See All Whispers" - usr.client.prefs.chat_toggles ^= CHAT_GHOSTWHISPER - to_chat(usr, "As a ghost, you will now [(usr.client.prefs.chat_toggles & CHAT_GHOSTWHISPER) ? "see all whispers in the world" : "only see whispers from nearby mobs"].") - usr.client.prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Whispers", "[usr.client.prefs.chat_toggles & CHAT_GHOSTWHISPER ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Ghost/chatterbox/toggle_ghost_whispers/Get_checked(client/C) - return C.prefs.chat_toggles & CHAT_GHOSTWHISPER - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Ghost/chatterbox, toggle_ghost_radio)() - set name = "Show/Hide GhostRadio" - set category = "Preferences" - set desc = "See All Radio Chatter" - usr.client.prefs.chat_toggles ^= CHAT_GHOSTRADIO - to_chat(usr, "As a ghost, you will now [(usr.client.prefs.chat_toggles & CHAT_GHOSTRADIO) ? "see radio chatter" : "not see radio chatter"].") - usr.client.prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Radio", "[usr.client.prefs.chat_toggles & CHAT_GHOSTRADIO ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! //social experiment, increase the generation whenever you copypaste this shamelessly GENERATION 1 -/datum/verbs/menu/Settings/Ghost/chatterbox/toggle_ghost_radio/Get_checked(client/C) - return C.prefs.chat_toggles & CHAT_GHOSTRADIO - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Ghost/chatterbox, toggle_ghost_pda)() - set name = "Show/Hide GhostPDA" - set category = "Preferences" - set desc = "See All PDA Messages" - usr.client.prefs.chat_toggles ^= CHAT_GHOSTPDA - to_chat(usr, "As a ghost, you will now [(usr.client.prefs.chat_toggles & CHAT_GHOSTPDA) ? "see all pda messages in the world" : "only see pda messages from nearby mobs"].") - usr.client.prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost PDA", "[usr.client.prefs.chat_toggles & CHAT_GHOSTPDA ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Ghost/chatterbox/toggle_ghost_pda/Get_checked(client/C) - return C.prefs.chat_toggles & CHAT_GHOSTPDA - -/datum/verbs/menu/Settings/Ghost/chatterbox/Events - name = "Events" - -//please be aware that the following two verbs have inverted stat output, so that "Toggle Deathrattle|1" still means you activated it -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Ghost/chatterbox/Events, toggle_deathrattle)() - set name = "Toggle Deathrattle" - set category = "Preferences" - set desc = "Death" - usr.client.prefs.toggles ^= DISABLE_DEATHRATTLE - usr.client.prefs.save_preferences() - to_chat(usr, "You will [(usr.client.prefs.toggles & DISABLE_DEATHRATTLE) ? "no longer" : "now"] get messages when a sentient mob dies.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Deathrattle", "[!(usr.client.prefs.toggles & DISABLE_DEATHRATTLE) ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, maybe you should spend some time reading the comments. -/datum/verbs/menu/Settings/Ghost/chatterbox/Events/toggle_deathrattle/Get_checked(client/C) - return !(C.prefs.toggles & DISABLE_DEATHRATTLE) - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Ghost/chatterbox/Events, toggle_arrivalrattle)() - set name = "Toggle Arrivalrattle" - set category = "Preferences" - set desc = "New Player Arrival" - usr.client.prefs.toggles ^= DISABLE_ARRIVALRATTLE - to_chat(usr, "You will [(usr.client.prefs.toggles & DISABLE_ARRIVALRATTLE) ? "no longer" : "now"] get messages when someone joins the station.") - usr.client.prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Arrivalrattle", "[!(usr.client.prefs.toggles & DISABLE_ARRIVALRATTLE) ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, maybe you should rethink where your life went so wrong. -/datum/verbs/menu/Settings/Ghost/chatterbox/Events/toggle_arrivalrattle/Get_checked(client/C) - return !(C.prefs.toggles & DISABLE_ARRIVALRATTLE) - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Ghost, togglemidroundantag)() - set name = "Toggle Midround Antagonist" - set category = "Preferences" - set desc = "Midround Antagonist" - usr.client.prefs.toggles ^= MIDROUND_ANTAG - usr.client.prefs.save_preferences() - to_chat(usr, "You will [(usr.client.prefs.toggles & MIDROUND_ANTAG) ? "now" : "no longer"] be considered for midround antagonist positions.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Midround Antag", "[usr.client.prefs.toggles & MIDROUND_ANTAG ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Ghost/togglemidroundantag/Get_checked(client/C) - return C.prefs.toggles & MIDROUND_ANTAG - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, toggletitlemusic)() - set name = "Hear/Silence Lobby Music" - set category = "Preferences" - set desc = "Hear Music In Lobby" - usr.client.prefs.toggles ^= SOUND_LOBBY - usr.client.prefs.save_preferences() - if(usr.client.prefs.toggles & SOUND_LOBBY) - to_chat(usr, "You will now hear music in the game lobby.") - if(isnewplayer(usr)) - usr.client.playtitlemusic() - else - to_chat(usr, "You will no longer hear music in the game lobby.") - usr.stop_sound_channel(CHANNEL_LOBBYMUSIC) - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Lobby Music", "[usr.client.prefs.toggles & SOUND_LOBBY ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Sound/toggletitlemusic/Get_checked(client/C) - return C.prefs.toggles & SOUND_LOBBY - - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, togglemidis)() - set name = "Hear/Silence Midis" - set category = "Preferences" - set desc = "Hear Admin Triggered Sounds (Midis)" - usr.client.prefs.toggles ^= SOUND_MIDI - usr.client.prefs.save_preferences() - if(usr.client.prefs.toggles & SOUND_MIDI) - to_chat(usr, "You will now hear any sounds uploaded by admins.") - else - to_chat(usr, "You will no longer hear sounds uploaded by admins") - usr.stop_sound_channel(CHANNEL_ADMIN) - var/client/C = usr.client - C?.tgui_panel?.stop_music() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Hearing Midis", "[usr.client.prefs.toggles & SOUND_MIDI ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Sound/togglemidis/Get_checked(client/C) - return C.prefs.toggles & SOUND_MIDI - - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, toggle_instruments)() - set name = "Hear/Silence Instruments" - set category = "Preferences" - set desc = "Hear In-game Instruments" - usr.client.prefs.toggles ^= SOUND_INSTRUMENTS - usr.client.prefs.save_preferences() - if(usr.client.prefs.toggles & SOUND_INSTRUMENTS) - to_chat(usr, "You will now hear people playing musical instruments.") - else - to_chat(usr, "You will no longer hear musical instruments.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Instruments", "[usr.client.prefs.toggles & SOUND_INSTRUMENTS ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Sound/toggle_instruments/Get_checked(client/C) - return C.prefs.toggles & SOUND_INSTRUMENTS - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, toggle_jukebox)() - set name = "Hear/Silence Jukeboxes" - set category = "Preferences" - set desc = "Hear In-game Jukeboxes" - usr.client.prefs.toggles ^= SOUND_JUKEBOX - usr.client.prefs.save_preferences() - if(usr.client.prefs.toggles & SOUND_JUKEBOX) - to_chat(usr, "You will now hear jukeboxes.") - else - to_chat(usr, "You will no longer hear jukeboxes.") - usr.client.update_playing_music() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Jukeboxes", "[usr.client.prefs.toggles & SOUND_JUKEBOX ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Sound/toggle_jukebox/Get_checked(client/C) - return C.prefs.toggles & SOUND_JUKEBOX - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, Toggle_Soundscape)() - set name = "Hear/Silence Ambience" - set category = "Preferences" - set desc = "Hear Ambient Sound Effects" - usr.client.prefs.toggles ^= SOUND_AMBIENCE - usr.client.prefs.save_preferences() - if(usr.client.prefs.toggles & SOUND_AMBIENCE) - to_chat(usr, "You will now hear ambient sounds.") - else - to_chat(usr, "You will no longer hear ambient sounds.") - usr.stop_sound_channel(CHANNEL_AMBIENCE) - usr.stop_sound_channel(CHANNEL_BUZZ) - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ambience", "[usr.client.prefs.toggles & SOUND_AMBIENCE ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Sound/Toggle_Soundscape/Get_checked(client/C) - return C.prefs.toggles & SOUND_AMBIENCE - - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, toggle_ship_ambience)() - set name = "Hear/Silence Ship Ambience" - set category = "Preferences" - set desc = "Hear Ship Ambience Roar" - usr.client.prefs.toggles ^= SOUND_SHIP_AMBIENCE - usr.client.prefs.save_preferences() - if(usr.client.prefs.toggles & SOUND_SHIP_AMBIENCE) - to_chat(usr, "You will now hear ship ambience.") - else - to_chat(usr, "You will no longer hear ship ambience.") - usr.stop_sound_channel(CHANNEL_BUZZ) - usr.client.ambience_playing = 0 - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ship Ambience", "[usr.client.prefs.toggles & SOUND_SHIP_AMBIENCE ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, I bet you read this comment expecting to see the same thing :^) -/datum/verbs/menu/Settings/Sound/toggle_ship_ambience/Get_checked(client/C) - return C.prefs.toggles & SOUND_SHIP_AMBIENCE - - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, toggle_announcement_sound)() - set name = "Hear/Silence Announcements" - set category = "Preferences" - set desc = "Hear Announcement Sound" - usr.client.prefs.toggles ^= SOUND_ANNOUNCEMENTS - to_chat(usr, "You will now [(usr.client.prefs.toggles & SOUND_ANNOUNCEMENTS) ? "hear announcement sounds" : "no longer hear announcements"].") - usr.client.prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Announcement Sound", "[usr.client.prefs.toggles & SOUND_ANNOUNCEMENTS ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Sound/toggle_announcement_sound/Get_checked(client/C) - return C.prefs.toggles & SOUND_ANNOUNCEMENTS - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, toggle_vox)() - set name = "Hear/Silence VOX" - set category = "Preferences" - set desc = "Hear VOX Announcements" - usr.client.prefs.toggles ^= SOUND_VOX - to_chat(usr, "You will now [(usr.client.prefs.toggles & SOUND_VOX) ? "hear VOX announcements" : "no longer hear VOX announcements"].") - usr.client.prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle VOX", "[usr.client.prefs.toggles & SOUND_VOX ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Sound/toggle_vox/Get_checked(client/C) - return C.prefs.toggles & SOUND_VOX - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, toggle_alt)() - set name = "Hear/Silence Alternative Sounds" - set category = "Preferences" - set desc = "Hear potentially annoying \"alternative\" sounds" - usr.client.prefs.toggles ^= SOUND_ALT - to_chat(usr, "You will now [(usr.client.prefs.toggles & SOUND_ALT) ? "hear alternative sounds" : "no longer hear alternative sounds"].") - usr.client.prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Alternative Sounds", "[usr.client.prefs.toggles & SOUND_ALT ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/Sound/toggle_alt/Get_checked(client/C) - return C.prefs.toggles & SOUND_ALT - -/datum/verbs/menu/Settings/Sound/verb/stop_client_sounds() - set name = "Stop Sounds" - set category = "Preferences" - set desc = "Stop Current Sounds" - SEND_SOUND(usr, sound(null)) - var/client/C = usr.client - C?.tgui_panel?.stop_music() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Stop Self Sounds")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings, listen_ooc)() - set name = "Show/Hide OOC" - set category = "Preferences" - set desc = "Show OOC Chat" - usr.client.prefs.chat_toggles ^= CHAT_OOC - usr.client.prefs.save_preferences() - to_chat(usr, "You will [(usr.client.prefs.chat_toggles & CHAT_OOC) ? "now" : "no longer"] see messages on the OOC channel.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Seeing OOC", "[usr.client.prefs.chat_toggles & CHAT_OOC ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/datum/verbs/menu/Settings/listen_ooc/Get_checked(client/C) - return C.prefs.chat_toggles & CHAT_OOC - -TOGGLE_CHECKBOX(/datum/verbs/menu/Settings, listen_bank_card)() - set name = "Show/Hide Income Updates" - set category = "Preferences" - set desc = "Show or hide updates to your income" - usr.client.prefs.chat_toggles ^= CHAT_BANKCARD - usr.client.prefs.save_preferences() - to_chat(usr, "You will [(usr.client.prefs.chat_toggles & CHAT_BANKCARD) ? "now" : "no longer"] be notified when you get paid.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Income Notifications", "[(usr.client.prefs.chat_toggles & CHAT_BANKCARD) ? "Enabled" : "Disabled"]")) -/datum/verbs/menu/Settings/listen_bank_card/Get_checked(client/C) - return C.prefs.chat_toggles & CHAT_BANKCARD - - -GLOBAL_LIST_INIT(ghost_forms, list("ghost","ghostking","ghostian2","skeleghost","ghost_red","ghost_black", \ - "ghost_blue","ghost_yellow","ghost_green","ghost_pink", \ - "ghost_cyan","ghost_dblue","ghost_dred","ghost_dgreen", \ - "ghost_dcyan","ghost_grey","ghost_dyellow","ghost_dpink", "ghost_purpleswirl","ghost_funkypurp","ghost_pinksherbert","ghost_blazeit",\ - "ghost_mellow","ghost_rainbow","ghost_camo","ghost_fire", "catghost")) -/client/proc/pick_form() - if(!is_content_unlocked()) - tgui_alert(usr,"This setting is for accounts with BYOND premium only.") - return - var/new_form = input(src, "Thanks for supporting BYOND - Choose your ghostly form:","Thanks for supporting BYOND",null) as null|anything in GLOB.ghost_forms - if(new_form) - prefs.ghost_form = new_form - prefs.save_preferences() - if(isobserver(mob)) - var/mob/dead/observer/O = mob - O.update_icon(new_form) - -GLOBAL_LIST_INIT(ghost_orbits, list(GHOST_ORBIT_CIRCLE,GHOST_ORBIT_TRIANGLE,GHOST_ORBIT_SQUARE,GHOST_ORBIT_HEXAGON,GHOST_ORBIT_PENTAGON)) - -/client/proc/pick_ghost_orbit() - if(!is_content_unlocked()) - tgui_alert(usr,"This setting is for accounts with BYOND premium only.") - return - var/new_orbit = input(src, "Thanks for supporting BYOND - Choose your ghostly orbit:","Thanks for supporting BYOND",null) as null|anything in GLOB.ghost_orbits - if(new_orbit) - prefs.ghost_orbit = new_orbit - prefs.save_preferences() - if(isobserver(mob)) - var/mob/dead/observer/O = mob - O.ghost_orbit = new_orbit - -/client/proc/pick_ghost_accs() - var/new_ghost_accs = tgui_alert(usr,"Do you want your ghost to show full accessories where possible, hide accessories but still use the directional sprites where possible, or also ignore the directions and stick to the default sprites?",,list("full accessories", "only directional sprites", "default sprites")) - if(new_ghost_accs) - switch(new_ghost_accs) - if("full accessories") - prefs.ghost_accs = GHOST_ACCS_FULL - if("only directional sprites") - prefs.ghost_accs = GHOST_ACCS_DIR - if("default sprites") - prefs.ghost_accs = GHOST_ACCS_NONE - prefs.save_preferences() - if(isobserver(mob)) - var/mob/dead/observer/O = mob - O.update_icon() - -/client/verb/pick_ghost_customization() - set name = "Ghost Customization" - set category = "Preferences" - set desc = "Customize your ghastly appearance." - if(is_content_unlocked()) - switch(tgui_alert(usr,"Which setting do you want to change?",,list("Ghost Form","Ghost Orbit","Ghost Accessories"))) - if("Ghost Form") - pick_form() - if("Ghost Orbit") - pick_ghost_orbit() - if("Ghost Accessories") - pick_ghost_accs() - else - pick_ghost_accs() - -/client/verb/pick_ghost_others() - set name = "Ghosts of Others" - set category = "Preferences" - set desc = "Change display settings for the ghosts of other players." - var/new_ghost_others = tgui_alert(usr,"Do you want the ghosts of others to show up as their own setting, as their default sprites or always as the default white ghost?",,list("Their Setting", "Default Sprites", "White Ghost")) - if(new_ghost_others) - switch(new_ghost_others) - if("Their Setting") - prefs.ghost_others = GHOST_OTHERS_THEIR_SETTING - if("Default Sprites") - prefs.ghost_others = GHOST_OTHERS_DEFAULT_SPRITE - if("White Ghost") - prefs.ghost_others = GHOST_OTHERS_SIMPLE - prefs.save_preferences() - if(isobserver(mob)) - var/mob/dead/observer/O = mob - O.update_sight() - -/client/verb/toggle_intent_style() - set name = "Toggle Intent Selection Style" - set category = "Preferences" - set desc = "Toggle between directly clicking the desired intent or clicking to rotate through." - prefs.toggles ^= INTENT_STYLE - to_chat(src, "[(prefs.toggles & INTENT_STYLE) ? "Clicking directly on intents selects them." : "Clicking on intents rotates selection clockwise."]") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Intent Selection", "[prefs.toggles & INTENT_STYLE ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_hud_pref() - set name = "Toggle Ghost HUD" - set category = "Preferences" - set desc = "Hide/Show Ghost HUD" - - prefs.ghost_hud = !prefs.ghost_hud - to_chat(src, "Ghost HUD will now be [prefs.ghost_hud ? "visible" : "hidden"].") - prefs.save_preferences() - if(isobserver(mob)) - mob.hud_used.show_hud() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost HUD", "[prefs.ghost_hud ? "Enabled" : "Disabled"]")) - -/client/verb/toggle_show_credits() - set name = "Toggle Credits" - set category = "Preferences" - set desc = "Hide/Show Credits" - - prefs.show_credits = !prefs.show_credits - to_chat(src, "Credits will now be [prefs.show_credits ? "visible" : "hidden"].") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Credits", "[prefs.show_credits ? "Enabled" : "Disabled"]")) - -/client/verb/toggle_inquisition() // warning: unexpected inquisition - set name = "Toggle Inquisitiveness" - set desc = "Sets whether your ghost examines everything on click by default" - set category = "Preferences" - - prefs.inquisitive_ghost = !prefs.inquisitive_ghost - prefs.save_preferences() - if(prefs.inquisitive_ghost) - to_chat(src, span_notice("You will now examine everything you click on.")) - else - to_chat(src, span_notice("You will no longer examine things you click on.")) - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Inquisitiveness", "[prefs.inquisitive_ghost ? "Enabled" : "Disabled"]")) - -//Admin Preferences -/client/proc/toggleadminhelpsound() - set name = "Hear/Silence Adminhelps" - set category = "Preferences.Admin" - set desc = "Toggle hearing a notification when admin PMs are received" - if(!holder) - return - prefs.toggles ^= SOUND_ADMINHELP - prefs.save_preferences() - to_chat(usr, "You will [(prefs.toggles & SOUND_ADMINHELP) ? "now" : "no longer"] hear a sound when adminhelps arrive.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Adminhelp Sound", "[prefs.toggles & SOUND_ADMINHELP ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggleannouncelogin() - set name = "Do/Don't Announce Login" - set category = "Preferences.Admin" - set desc = "Toggle if you want an announcement to admins when you login during a round" - if(!holder) - return - prefs.toggles ^= ANNOUNCE_LOGIN - prefs.save_preferences() - to_chat(usr, "You will [(prefs.toggles & ANNOUNCE_LOGIN) ? "now" : "no longer"] have an announcement to other admins when you login.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Login Announcement", "[prefs.toggles & ANNOUNCE_LOGIN ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggle_hear_radio() - set name = "Show/Hide Radio Chatter" - set category = "Preferences.Admin" - set desc = "Toggle seeing radiochatter from nearby radios and speakers" - if(!holder) - return - prefs.chat_toggles ^= CHAT_RADIO - prefs.save_preferences() - to_chat(usr, "You will [(prefs.chat_toggles & CHAT_RADIO) ? "now" : "no longer"] see radio chatter from nearby radios or speakers") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Radio Chatter", "[prefs.chat_toggles & CHAT_RADIO ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/deadchat() - set name = "Show/Hide Deadchat" - set category = "Preferences.Admin" - set desc ="Toggles seeing deadchat" - if(!holder) - return - prefs.chat_toggles ^= CHAT_DEAD - prefs.save_preferences() - to_chat(src, "You will [(prefs.chat_toggles & CHAT_DEAD) ? "now" : "no longer"] see deadchat.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Deadchat Visibility", "[prefs.chat_toggles & CHAT_DEAD ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggleprayers() - set name = "Show/Hide Prayers" - set category = "Preferences.Admin" - set desc = "Toggles seeing prayers" - if(!holder) - return - prefs.chat_toggles ^= CHAT_PRAYER - prefs.save_preferences() - to_chat(src, "You will [(prefs.chat_toggles & CHAT_PRAYER) ? "now" : "no longer"] see prayerchat.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Prayer Visibility", "[prefs.chat_toggles & CHAT_PRAYER ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggle_prayer_sound() - set name = "Hear/Silence Prayer Sounds" - set category = "Preferences.Admin" - set desc = "Hear Prayer Sounds" - if(!holder) - return - prefs.toggles ^= SOUND_PRAYERS - prefs.save_preferences() - to_chat(usr, "You will [(prefs.toggles & SOUND_PRAYERS) ? "now" : "no longer"] hear a sound when prayers arrive.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Prayer Sounds", "[usr.client.prefs.toggles & SOUND_PRAYERS ? "Enabled" : "Disabled"]")) - -/client/proc/colorasay() - set name = "Set Admin Say Color" - set category = "Preferences.Admin" - set desc = "Set the color of your ASAY messages" - if(!holder) - return - if(!CONFIG_GET(flag/allow_admin_asaycolor)) - to_chat(src, "Custom Asay color is currently disabled by the server.") - return - var/new_asaycolor = input(src, "Please select your ASAY color.", "ASAY color", prefs.asaycolor) as color|null - if(new_asaycolor) - prefs.asaycolor = sanitize_ooccolor(new_asaycolor) - prefs.save_preferences() - SSblackbox.record_feedback("tally", "admin_verb", 1, "Set ASAY Color") - return - -/client/proc/resetasaycolor() - set name = "Reset your Admin Say Color" - set desc = "Returns your ASAY Color to default" - set category = "Preferences.Admin" - if(!holder) - return - if(!CONFIG_GET(flag/allow_admin_asaycolor)) - to_chat(src, "Custom Asay color is currently disabled by the server.") - return - prefs.asaycolor = initial(prefs.asaycolor) - prefs.save_preferences() - -/client/proc/toggle_split_admin_tabs() - set name = "Toggle Split Admin Tabs" - set category = "Preferences.Admin" - set desc = "Toggle the admin tab being split into separate tabs instead of being merged into one" - if(!holder) - return - prefs.extra_toggles ^= SPLIT_ADMIN_TABS - prefs.save_preferences() - to_chat(src, "Admin tabs will now [(prefs.extra_toggles & SPLIT_ADMIN_TABS) ? "be" : "not be"] split.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Split Admin Tabs", "[prefs.extra_toggles & SPLIT_ADMIN_TABS ? "Enabled" : "Disabled"]")) - -/client/proc/toggle_fast_mc_refresh() - set name = "Toggle Fast MC Refresh" - set category = "Preferences.Admin" - set desc = "Toggle the speed of which the MC refreshes." - if(!holder) - return - prefs.extra_toggles ^= FAST_MC_REFRESH - prefs.save_preferences() - to_chat(src, "MC will now [(prefs.extra_toggles & FAST_MC_REFRESH) ? "not be" : "be"] fast updating.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Fast MC Refresh", "[prefs.extra_toggles & FAST_MC_REFRESH ? "Enabled" : "Disabled"]")) diff --git a/code/modules/client/verbs/etips.dm b/code/modules/client/verbs/etips.dm deleted file mode 100644 index 5c53deb9a3de..000000000000 --- a/code/modules/client/verbs/etips.dm +++ /dev/null @@ -1,20 +0,0 @@ -/client/verb/toggle_tips() - set name = "Toggle Examine Tooltips" - set desc = "Toggles examine hover-over tooltips" - set category = "Preferences" - - prefs.enable_tips = !prefs.enable_tips - prefs.save_preferences() - to_chat(usr, span_danger("Examine tooltips [prefs.enable_tips ? "en" : "dis"]abled.")) - -/client/verb/change_tip_delay() - set name = "Set Examine Tooltip Delay" - set desc = "Sets the delay in milliseconds before examine tooltips appear" - set category = "Preferences" - - var/indelay = stripped_input(usr, "Enter the tooltip delay in milliseconds (default: 500)", "Enter tooltip delay", "", 10) - indelay = text2num(indelay) - if(usr)//is this what you mean? - prefs.tip_delay = indelay - prefs.save_preferences() - to_chat(usr, span_danger("Tooltip delay set to [indelay] milliseconds.")) diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index be70c608d03f..ab69f1e9c4fe 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -69,7 +69,7 @@ GLOBAL_VAR_INIT(mentor_ooc_colour, YOGS_MENTOR_OOC_COLOUR) // yogs - mentor ooc var/keyname = key if(prefs.unlock_content) if(prefs.toggles & MEMBER_PUBLIC) - keyname = "[icon2html('icons/member_content.dmi', world, "blag")][keyname]" + keyname = "[icon2html('icons/member_content.dmi', world, "blag")][keyname]" //YOG START - Yog OOC //PINGS @@ -100,7 +100,8 @@ GLOBAL_VAR_INIT(mentor_ooc_colour, YOGS_MENTOR_OOC_COLOUR) // yogs - mentor ooc var/oocmsg_toadmins = FALSE; // The message sent to admins. if(holder) // If the speaker is an admin or something if(check_rights_for(src, R_ADMIN)) // If they're supposed to have their own admin OOC colour - oocmsg += "[(CONFIG_GET(flag/allow_admin_ooccolor) && prefs.ooccolor) ? "" :"" ][find_admin_rank(src)]" // The header for an Admin's OOC. + var/ooc_color = prefs.read_preference(/datum/preference/color/ooc_color) + oocmsg += "[(CONFIG_GET(flag/allow_admin_ooccolor) && ooc_color) ? "" :"" ][find_admin_rank(src)]" // The header for an Admin's OOC. else // Else if they're an AdminObserver oocmsg += "[find_admin_rank(src)]" // The header for an AO's OOC. //Check yogstation\code\module\client\verbs\ooc for the find_admin_rank definition. @@ -119,7 +120,7 @@ GLOBAL_VAR_INIT(mentor_ooc_colour, YOGS_MENTOR_OOC_COLOUR) // yogs - mentor ooc mposition = src.mentor_datum?.position oocmsg = "\[" oocmsg += "[mposition]" - oocmsg += "]" + oocmsg += "]" else oocmsg = "[(is_donator(src) && !CONFIG_GET(flag/everyone_is_donator)) ? "(Donator)" : ""]" oocmsg += "" @@ -176,7 +177,7 @@ GLOBAL_VAR_INIT(mentor_ooc_colour, YOGS_MENTOR_OOC_COLOUR) // yogs - mentor ooc set name = "Set Player OOC Color" set desc = "Modifies player OOC Color" set category = "Server" - GLOB.OOC_COLOR = sanitize_ooccolor(newColor) + GLOB.OOC_COLOR = sanitize_color(newColor) /client/proc/reset_ooc() set name = "Reset Player OOC Color" @@ -194,33 +195,6 @@ GLOBAL_VAR_INIT(mentor_ooc_colour, YOGS_MENTOR_OOC_COLOUR) // yogs - mentor ooc log_admin("[key_name_admin(usr)] has reset player ooc color.") GLOB.OOC_COLOR = null -/client/verb/colorooc() - set name = "Set Your OOC Color" - set category = "Preferences" - - if(!holder || !check_rights_for(src, R_ADMIN)) - if(!is_content_unlocked()) - return - - var/new_ooccolor = input(src, "Please select your OOC color.", "OOC color", prefs.ooccolor) as color|null - if(new_ooccolor) - prefs.ooccolor = sanitize_ooccolor(new_ooccolor) - prefs.save_preferences() - SSblackbox.record_feedback("tally", "admin_verb", 1, "Set OOC Color") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - return - -/client/verb/resetcolorooc() - set name = "Reset Your OOC Color" - set desc = "Returns your OOC Color to default" - set category = "Preferences" - - if(!holder || !check_rights_for(src, R_ADMIN)) - if(!is_content_unlocked()) - return - - prefs.ooccolor = initial(prefs.ooccolor) - prefs.save_preferences() - //Checks admin notice /client/verb/admin_notice() set name = "Adminnotice" diff --git a/code/modules/clothing/glasses/_glasses.dm b/code/modules/clothing/glasses/_glasses.dm index 9e4fb396d564..7789a74dfd3d 100644 --- a/code/modules/clothing/glasses/_glasses.dm +++ b/code/modules/clothing/glasses/_glasses.dm @@ -545,16 +545,18 @@ /obj/item/clothing/glasses/AltClick(mob/user) if(glass_colour_type && ishuman(user)) - var/mob/living/carbon/human/H = user - if(H.client) - if(H.client.prefs) - if(src == H.glasses) - H.client.prefs.uses_glasses_colour = !H.client.prefs.uses_glasses_colour - if(H.client.prefs.uses_glasses_colour) - to_chat(H, "You will now see glasses colors.") - else - to_chat(H, "You will no longer see glasses colors.") - H.update_glasses_color(src, 1) + var/mob/living/carbon/human/human_user = user + + if (human_user.glasses != src) + return ..() + + if (HAS_TRAIT_FROM(human_user, TRAIT_SEE_GLASS_COLORS, GLASSES_TRAIT)) + REMOVE_TRAIT(human_user, TRAIT_SEE_GLASS_COLORS, GLASSES_TRAIT) + to_chat(human_user, span_notice("You will now see glasses colors.")) + else + ADD_TRAIT(human_user, TRAIT_SEE_GLASS_COLORS, GLASSES_TRAIT) + to_chat(human_user, span_notice("You will no longer see glasses colors.")) + human_user.update_glasses_color(src, TRUE) else return ..() @@ -570,7 +572,7 @@ /mob/living/carbon/human/proc/update_glasses_color(obj/item/clothing/glasses/G, glasses_equipped) - if(client && client.prefs.uses_glasses_colour && glasses_equipped) + if (HAS_TRAIT(src, TRAIT_SEE_GLASS_COLORS) && glasses_equipped) add_client_colour(G.glass_colour_type) else remove_client_colour(G.glass_colour_type) diff --git a/code/modules/clothing/outfits/standard.dm b/code/modules/clothing/outfits/standard.dm index 70feb1afced8..aa5b1b7f5887 100644 --- a/code/modules/clothing/outfits/standard.dm +++ b/code/modules/clothing/outfits/standard.dm @@ -90,8 +90,6 @@ uniform = /obj/item/clothing/under/pirate/space suit = /obj/item/clothing/suit/space/pirate head = /obj/item/clothing/head/helmet/space/pirate/bandana - mask = /obj/item/clothing/mask/breath - suit_store = /obj/item/tank/internals/oxygen ears = /obj/item/radio/headset/syndicate id = /obj/item/card/id diff --git a/code/modules/clothing/spacesuits/hardsuit.dm b/code/modules/clothing/spacesuits/hardsuit.dm index 4c0e2ec2bfd6..6e04b7388d46 100644 --- a/code/modules/clothing/spacesuits/hardsuit.dm +++ b/code/modules/clothing/spacesuits/hardsuit.dm @@ -61,6 +61,8 @@ else qdel(src) else + if(isdummy(user)) + return soundloop.start(user) /obj/item/clothing/head/helmet/space/hardsuit/proc/display_visor_message(var/msg) @@ -148,7 +150,7 @@ /obj/item/clothing/suit/space/hardsuit/equipped(mob/user, slot) ..() - if(jetpack) + if(jetpack && istype(jetpack)) if(slot == SLOT_WEAR_SUIT) for(var/X in jetpack.actions) var/datum/action/A = X @@ -156,7 +158,7 @@ /obj/item/clothing/suit/space/hardsuit/dropped(mob/user) ..() - if(jetpack) + if(jetpack && istype(jetpack)) for(var/X in jetpack.actions) var/datum/action/A = X A.Remove(user) @@ -384,7 +386,7 @@ name = "elite syndicate hardsuit helmet" desc = "An elite version of the syndicate helmet, with improved armour and fireproofing. It is in EVA mode. Property of Gorlex Marauders." alt_desc = "An elite version of the syndicate helmet, with improved armour and fireproofing. It is in combat mode. Property of Gorlex Marauders." - icon_state = "hardsuit0-syndielite" + icon_state = "hardsuit1-syndielite" hardsuit_type = "syndielite" armor = list(MELEE = 60, BULLET = 60, LASER = 50, ENERGY = 35, BOMB = 90, BIO = 100, RAD = 70, FIRE = 100, ACID = 100, WOUND = 25) heat_protection = HEAD @@ -395,7 +397,7 @@ name = "elite syndicate hardsuit" desc = "An elite version of the syndicate hardsuit, with improved armour and fireproofing. It is in travel mode." alt_desc = "An elite version of the syndicate hardsuit, with improved armour and fireproofing. It is in combat mode." - icon_state = "hardsuit0-syndielite" + icon_state = "hardsuit1-syndielite" hardsuit_type = "syndielite" helmettype = /obj/item/clothing/head/helmet/space/hardsuit/syndi/elite armor = list(MELEE = 60, BULLET = 60, LASER = 50, ENERGY = 25, BOMB = 90, BIO = 100, RAD = 70, FIRE = 100, ACID = 100, WOUND = 25) diff --git a/code/modules/clothing/spacesuits/plasmamen.dm b/code/modules/clothing/spacesuits/plasmamen.dm index a45d14f6d2a3..e5661390a09c 100644 --- a/code/modules/clothing/spacesuits/plasmamen.dm +++ b/code/modules/clothing/spacesuits/plasmamen.dm @@ -78,11 +78,14 @@ if(!ishuman(user)) return var/style = user.dna?.features["plasmaman_helmet"] + var/suffix = "" if(style && (style in GLOB.plasmaman_helmet_list) && style != "None") - icon_state = initial(icon_state) + "-[GLOB.plasmaman_helmet_list[style]]" - item_state = icon_state - base_icon_state = icon_state - user.update_inv_head() + suffix = "-[GLOB.plasmaman_helmet_list[style]]" + + icon_state = initial(icon_state) + suffix + item_state = icon_state + base_icon_state = icon_state + user.update_inv_head() /obj/item/clothing/head/helmet/space/plasmaman/security name = "security envirosuit helmet" diff --git a/code/modules/clothing/suits/toggles.dm b/code/modules/clothing/suits/toggles.dm index 306f4909e026..9eca99872dc3 100644 --- a/code/modules/clothing/suits/toggles.dm +++ b/code/modules/clothing/suits/toggles.dm @@ -133,7 +133,8 @@ if(helmet) helmet.suit = null qdel(helmet) - qdel(jetpack) + if(jetpack && istype(jetpack)) + qdel(jetpack) return ..() /obj/item/clothing/head/helmet/space/hardsuit/Destroy() diff --git a/code/modules/events/devil.dm b/code/modules/events/devil.dm index 3760cbe05d9a..ae01b40b8726 100644 --- a/code/modules/events/devil.dm +++ b/code/modules/events/devil.dm @@ -46,8 +46,7 @@ var/mob/living/carbon/human/new_devil = new(spawn_loc) if(!spawn_loc) SSjob.SendToLateJoin(new_devil) - var/datum/preferences/A = new() //Randomize appearance for the devil. - A.copy_to(new_devil) + new_devil.randomize_human_appearance(~(RANDOMIZE_SPECIES)) new_devil.dna.update_dna_identity() return new_devil diff --git a/code/modules/events/operative.dm b/code/modules/events/operative.dm index b5bba1902945..6120bccab6e6 100644 --- a/code/modules/events/operative.dm +++ b/code/modules/events/operative.dm @@ -24,8 +24,7 @@ return MAP_ERROR var/mob/living/carbon/human/operative = new(pick(spawn_locs)) - var/datum/preferences/A = new - A.copy_to(operative) + operative.randomize_human_appearance(~(RANDOMIZE_SPECIES)) operative.dna.update_dna_identity() var/datum/mind/Mind = new /datum/mind(selected.key) Mind.assigned_role = "Lone Operative" diff --git a/code/modules/events/sinfuldemon.dm b/code/modules/events/sinfuldemon.dm index 9a916fb03e37..f1b798816f8b 100644 --- a/code/modules/events/sinfuldemon.dm +++ b/code/modules/events/sinfuldemon.dm @@ -58,8 +58,7 @@ var/mob/living/carbon/human/new_sinfuldemon = new(spawn_loc) if(!spawn_loc) SSjob.SendToLateJoin(new_sinfuldemon) - var/datum/preferences/A = new() //Randomize appearance for the demon. - A.copy_to(new_sinfuldemon) + new_sinfuldemon.randomize_human_appearance(~(RANDOMIZE_SPECIES)) new_sinfuldemon.dna.update_dna_identity() return new_sinfuldemon diff --git a/code/modules/events/tzimisce.dm b/code/modules/events/tzimisce.dm index 1922d8031c20..1a2f5e60c560 100644 --- a/code/modules/events/tzimisce.dm +++ b/code/modules/events/tzimisce.dm @@ -84,8 +84,7 @@ /datum/round_event/ghost_role/tzimisce/proc/spawn_event_tzimisce() var/mob/living/carbon/human/new_tzimisce = new() SSjob.SendToLateJoin(new_tzimisce) - var/datum/preferences/A = new() //Randomize appearance. - A.copy_to(new_tzimisce) + new_tzimisce.randomize_human_appearance(~(RANDOMIZE_SPECIES)) new_tzimisce.dna.update_dna_identity() return new_tzimisce diff --git a/code/modules/flufftext/Hallucination.dm b/code/modules/flufftext/Hallucination.dm index 7098ae845e64..cf1944184103 100644 --- a/code/modules/flufftext/Hallucination.dm +++ b/code/modules/flufftext/Hallucination.dm @@ -709,10 +709,10 @@ GLOBAL_LIST_INIT(hallucination_list, list( feedback_details += "Type: [is_radio ? "Radio" : "Talk"], Source: [person.real_name], Message: [message]" // Display message - if (!is_radio && !target.client?.prefs.chat_on_map) + if (!is_radio && !target.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)) var/image/speech_overlay = image('icons/mob/talk.dmi', person, "default0", layer = ABOVE_MOB_LAYER) INVOKE_ASYNC(GLOBAL_PROC, /proc/flick_overlay, speech_overlay, list(target.client), 30) - if (target.client?.prefs.chat_on_map) + if (target.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)) target.create_chat_message(person, understood_language, chosen, spans) to_chat(target, message) qdel(src) @@ -884,7 +884,7 @@ GLOBAL_LIST_INIT(hallucination_list, list( if("blob alert") to_chat(target, "

Biohazard Alert

") to_chat(target, "
[span_alert("Confirmed outbreak of level 5 biohazard aboard [station_name()]. All personnel must contain the outbreak.")]
") - if(target.client.prefs.disable_alternative_announcers) + if(target.client.prefs.read_preference(/datum/preference/toggle/disable_alternative_announcers)) SEND_SOUND(target, SSstation.default_announcer.event_sounds[ANNOUNCER_OUTBREAK5]) else SEND_SOUND(target, SSstation.announcer.event_sounds[ANNOUNCER_OUTBREAK5]) @@ -896,21 +896,21 @@ GLOBAL_LIST_INIT(hallucination_list, list( if("shuttle dock") to_chat(target, "

Priority Announcement

") to_chat(target, "
[span_alert("The Emergency Shuttle has docked with the station. You have 3 minutes to board the Emergency Shuttle.")]
") - if(target.client.prefs.disable_alternative_announcers) + if(target.client.prefs.read_preference(/datum/preference/toggle/disable_alternative_announcers)) SEND_SOUND(target, SSstation.default_announcer.event_sounds[ANNOUNCER_SHUTTLEDOCK]) else SEND_SOUND(target, SSstation.announcer.event_sounds[ANNOUNCER_SHUTTLEDOCK]) if("malf ai") //AI is doomsdaying! to_chat(target, "

Anomaly Alert

") to_chat(target, "
[span_alert("Hostile runtimes detected in all station systems, please deactivate your AI to prevent possible damage to its morality core.")]
") - if(target.client.prefs.disable_alternative_announcers) + if(target.client.prefs.read_preference(/datum/preference/toggle/disable_alternative_announcers)) SEND_SOUND(target, SSstation.default_announcer.event_sounds[ANNOUNCER_AIMALF]) else SEND_SOUND(target, SSstation.announcer.event_sounds[ANNOUNCER_AIMALF]) if("meteors") //Meteors inbound! to_chat(target, "

Meteor Alert

") to_chat(target, "
[span_alert("Meteors have been detected on collision course with the station.")]
") - if(target.client.prefs.disable_alternative_announcers) + if(target.client.prefs.read_preference(/datum/preference/toggle/disable_alternative_announcers)) SEND_SOUND(target, SSstation.default_announcer.event_sounds[ANNOUNCER_OUTBREAK5]) else SEND_SOUND(target, SSstation.announcer.event_sounds[ANNOUNCER_OUTBREAK5]) diff --git a/code/modules/jobs/departments/departments.dm b/code/modules/jobs/departments/departments.dm new file mode 100644 index 000000000000..a629c2eaab62 --- /dev/null +++ b/code/modules/jobs/departments/departments.dm @@ -0,0 +1,121 @@ +/// Singleton representing a category of jobs forming a department. +/// NOTICE: This is NOT fully implemented everywhere. Currently only used in: Preferences menu +/datum/job_department + /// Department as displayed on different menus. + var/department_name = DEPARTMENT_UNASSIGNED + /// Bitflags associated to the specific department. + var/department_bitflags = NONE + /// Typepath of the job datum leading this department. + var/datum/job/department_head = null + /// Experience granted by playing in a job of this department. + var/department_experience_type = null + /// The order in which this department appears on menus, in relation to other departments. + var/display_order = 0 + /// The header color to be displayed in the ban panel, classes defined in banpanel.css + var/label_class = "undefineddepartment" + /// The color used in TGUI or similar menus. + var/ui_color = "#9689db" + /// Job singleton datums associated to this department. Populated on job initialization. + var/list/department_jobs = list() + + +/// Handles adding jobs to the department and setting up the job bitflags. +/datum/job_department/proc/add_job(datum/job/job) + department_jobs += job + job.departments_bitflags |= department_bitflags + +/// A special assistant only department, primarily for use by the preferences menu +/datum/job_department/assistant + department_name = DEPARTMENT_ASSISTANT + department_bitflags = DEPARTMENT_BITFLAG_ASSISTANT + // Don't add department_head! Assistants names should not be in bold. + +/// A special captain only department, for use by the preferences menu +/datum/job_department/captain + department_name = DEPARTMENT_CAPTAIN + department_bitflags = DEPARTMENT_BITFLAG_CAPTAIN + department_head = /datum/job/captain + +/datum/job_department/command + department_name = DEPARTMENT_COMMAND + department_bitflags = DEPARTMENT_BITFLAG_COMMAND + department_head = /datum/job/captain + department_experience_type = EXP_TYPE_COMMAND + display_order = 1 + label_class = "command" + ui_color = "#ccccff" + + +/datum/job_department/security + department_name = DEPARTMENT_SECURITY + department_bitflags = DEPARTMENT_BITFLAG_SECURITY + department_head = /datum/job/hos + department_experience_type = EXP_TYPE_SECURITY + display_order = 2 + label_class = "security" + ui_color = "#ffbbbb" + + +/datum/job_department/engineering + department_name = DEPARTMENT_ENGINEERING + department_bitflags = DEPARTMENT_BITFLAG_ENGINEERING + department_head = /datum/job/chief_engineer + department_experience_type = EXP_TYPE_ENGINEERING + display_order = 3 + label_class = "engineering" + ui_color = "#ffeeaa" + + +/datum/job_department/medical + department_name = DEPARTMENT_MEDICAL + department_bitflags = DEPARTMENT_BITFLAG_MEDICAL + department_head = /datum/job/cmo + department_experience_type = EXP_TYPE_MEDICAL + display_order = 4 + label_class = "medical" + ui_color = "#c1e1ec" + + +/datum/job_department/science + department_name = DEPARTMENT_SCIENCE + department_bitflags = DEPARTMENT_BITFLAG_SCIENCE + department_head = /datum/job/rd + department_experience_type = EXP_TYPE_SCIENCE + display_order = 5 + label_class = "science" + ui_color = "#ffddff" + + +/datum/job_department/cargo + department_name = DEPARTMENT_CARGO + department_bitflags = DEPARTMENT_BITFLAG_CARGO + department_head = /datum/job/qm + department_experience_type = EXP_TYPE_SUPPLY + display_order = 6 + label_class = "supply" + ui_color = "#d7b088" + + +/datum/job_department/service + department_name = DEPARTMENT_SERVICE + department_bitflags = DEPARTMENT_BITFLAG_SERVICE + department_head = /datum/job/hop + department_experience_type = EXP_TYPE_SERVICE + display_order = 7 + label_class = "service" + ui_color = "#ddddff" + + +/datum/job_department/silicon + department_name = DEPARTMENT_SILICON + department_bitflags = DEPARTMENT_BITFLAG_SILICON + department_head = /datum/job/ai + department_experience_type = EXP_TYPE_SILICON + display_order = 8 + label_class = "silicon" + ui_color = "#ccffcc" + + +/// Catch-all department for undefined jobs. +/datum/job_department/undefined + display_order = 10 diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index 938451ceed6e..225f75c1cbc6 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -1,20 +1,42 @@ /datum/job /// The name of the job used for preferences, bans, etc. var/title = "NOPE" + /// The description of the job, used for preferences menu. /// Keep it short and useful. Avoid in-jokes, these are for new players. var/description + /// This job comes with these accesses by default var/list/base_access = list() + /// Additional accesses for the job if config.jobs_have_minimal_access is set to false var/list/added_access = list() + /// Who is responsible for demoting them var/department_head = list() + /// Tells the given channels that the given mob is the new department head. See communications.dm for valid channels. var/list/head_announce = null + // Used for something in preferences_savefile.dm + // NOTE: currently unused var/department_flag = NONE + + /// Bitfield of departments this job belongs to. These get setup when adding the job into the department, on job datum creation. + var/departments_bitflags = NONE + + /// If specified, this department will be used for the preferences menu. + var/datum/job_department/department_for_prefs = null + + /// Lazy list with the departments this job belongs to. + /// Required to be set for playable jobs. + /// The first department will be used in the preferences menu, + /// unless department_for_prefs is set. + /// TODO: Currently not used so will always be empty! Change this to department datums + var/list/departments_list = null + var/flag = NONE //Deprecated + /// Automatic deadmin for a job. Usually head/security positions var/auto_deadmin_role_flags = NONE // Players will be allowed to spawn in as jobs that are set to "Station" @@ -45,7 +67,7 @@ var/exp_requirements = 0 /// Which type of XP is required see `EXP_TYPE_` in __DEFINES/preferences.dm var/exp_type = "" - /// Department XP required + /// Department XP required YOGS THIS IS NOT FUCKING SET FOR EVERY JOB I HATE WHOEVER DID THIS var/exp_type_department = "" /// How much antag rep this job gets increase antag chances next round unless its overriden in antag_rep.txt var/antag_rep = 10 @@ -137,7 +159,7 @@ if(CONFIG_GET(keyed_list/job_species_whitelist)[type] && !splittext(CONFIG_GET(keyed_list/job_species_whitelist)[type], ",").Find(H.dna.species.id)) if(H.dna.species.id != "human") H.set_species(/datum/species/human) - H.apply_pref_name("human", preference_source) + H.apply_pref_name(/datum/preference/name/backup_human, preference_source) if(!visualsOnly) var/datum/bank_account/bank_account = new(H.real_name, src, H.dna.species.payday_modifier) @@ -168,6 +190,13 @@ if(CONFIG_GET(flag/everyone_has_maint_access)) //Config has global maint access set . |= list(ACCESS_MAINT_TUNNELS) +/mob/living/proc/dress_up_as_job(datum/job/equipping, visual_only = FALSE) + return + +/mob/living/carbon/human/dress_up_as_job(datum/job/equipping, visual_only = FALSE) + dna.species.before_equip_job(equipping, src, visual_only) + equipOutfit(equipping.outfit, visual_only) + /datum/job/proc/announce_head(var/mob/living/carbon/human/H, var/channels) //tells the given channel that the given mob is the new department head. See communications.dm for valid channels. if(H && GLOB.announcement_systems.len) //timer because these should come after the captain announcement @@ -287,7 +316,7 @@ var/obj/item/modular_computer/PDA = new pda_type() if(istype(PDA)) - if (H.id_in_pda) + if (H.client?.prefs.read_preference(/datum/preference/toggle/id_in_pda)) PDA.InsertID(C) H.equip_to_slot_if_possible(PDA, SLOT_WEAR_ID) else // just in case you hate change diff --git a/code/modules/jobs/job_types/ai.dm b/code/modules/jobs/job_types/ai.dm index 1aff3b9daa8b..e80a36327745 100644 --- a/code/modules/jobs/job_types/ai.dm +++ b/code/modules/jobs/job_types/ai.dm @@ -18,6 +18,10 @@ display_order = JOB_DISPLAY_ORDER_AI var/do_special_check = TRUE + departments_list = list( + /datum/job_department/silicon, + ) + alt_titles = list("Station Central Processor", "Central Silicon Intelligence", "Cyborg Overlord") //this should never be seen because of the way olfaction works but just in case @@ -41,7 +45,7 @@ GLOB.ai_os.set_cpu(AI, total_available_cpu) GLOB.ai_os.add_ram(AI, total_available_ram) - AI.apply_pref_name("ai", M.client) //If this runtimes oh well jobcode is fucked. + AI.apply_pref_name(/datum/preference/name/ai, M.client) //If this runtimes oh well jobcode is fucked. AI.set_core_display_icon(null, M.client) //we may have been created after our borg diff --git a/code/modules/jobs/job_types/artist.dm b/code/modules/jobs/job_types/artist.dm index 1c01b4f49868..9cdf8d3354f7 100644 --- a/code/modules/jobs/job_types/artist.dm +++ b/code/modules/jobs/job_types/artist.dm @@ -21,6 +21,10 @@ display_order = JOB_DISPLAY_ORDER_ARTIST minimal_character_age = 18 //Young folks can be crazy crazy artists, something talented that can be self-taught feasibly + departments_list = list( + /datum/job_department/service, + ) + smells_like = "pain-t" /datum/outfit/job/artist diff --git a/code/modules/jobs/job_types/assistant.dm b/code/modules/jobs/job_types/assistant.dm index c5b2d294cd0c..e14e86d5ba39 100644 --- a/code/modules/jobs/job_types/assistant.dm +++ b/code/modules/jobs/job_types/assistant.dm @@ -21,6 +21,8 @@ Assistant display_order = JOB_DISPLAY_ORDER_ASSISTANT minimal_character_age = 18 //Would make it even younger if I could because this role turns men into little brat boys and likewise for the other genders + department_for_prefs = /datum/job_department/assistant + mail_goodies = list( /obj/item/reagent_containers/food/snacks/donkpocket = 10, /obj/item/clothing/mask/gas = 10, @@ -49,3 +51,19 @@ Assistant uniform = /obj/item/clothing/under/color/random uniform_skirt = /obj/item/clothing/under/skirt/color/random return ..() + + +/datum/outfit/job/assistant/consistent + name = "Assistant - Consistent" + +/datum/outfit/job/assistant/consistent/pre_equip(mob/living/carbon/human/target) + ..() + uniform = /obj/item/clothing/under/color/grey + +/datum/outfit/job/assistant/consistent/post_equip(mob/living/carbon/human/H, visualsOnly) + ..() + + // This outfit is used by the assets SS, which is ran before the atoms SS + if (SSatoms.initialized == INITIALIZATION_INSSATOMS) + // H.w_uniform?.update_greyscale() + H.update_inv_w_uniform() diff --git a/code/modules/jobs/job_types/atmospheric_technician.dm b/code/modules/jobs/job_types/atmospheric_technician.dm index a4d8d7833359..694e2a7a7882 100644 --- a/code/modules/jobs/job_types/atmospheric_technician.dm +++ b/code/modules/jobs/job_types/atmospheric_technician.dm @@ -23,6 +23,10 @@ display_order = JOB_DISPLAY_ORDER_ATMOSPHERIC_TECHNICIAN minimal_character_age = 24 //Intense understanding of thermodynamics, gas law, gas interaction, construction and safe containment of gases, creation of new ones, math beyond your wildest imagination + departments_list = list( + /datum/job_department/engineering, + ) + mail_goodies = list( ///obj/item/rpd_upgrade/unwrench = 30, /obj/item/grenade/gas_crystal/crystal_foam = 10, diff --git a/code/modules/jobs/job_types/bartender.dm b/code/modules/jobs/job_types/bartender.dm index ba8fd46b585f..d94592cbd9eb 100644 --- a/code/modules/jobs/job_types/bartender.dm +++ b/code/modules/jobs/job_types/bartender.dm @@ -23,6 +23,10 @@ display_order = JOB_DISPLAY_ORDER_BARTENDER minimal_character_age = 21 //I shouldn't have to explain this one + departments_list = list( + /datum/job_department/service, + ) + mail_goodies = list( /obj/item/storage/box/rubbershot = 30, /obj/item/reagent_containers/glass/bottle/clownstears = 10, diff --git a/code/modules/jobs/job_types/botanist.dm b/code/modules/jobs/job_types/botanist.dm index 639d62b037e8..80b97070a164 100644 --- a/code/modules/jobs/job_types/botanist.dm +++ b/code/modules/jobs/job_types/botanist.dm @@ -22,6 +22,10 @@ display_order = JOB_DISPLAY_ORDER_BOTANIST minimal_character_age = 22 //Biological understanding of plants and how to manipulate their DNAs and produces relatively "safely". Not just something that comes to you without education + departments_list = list( + /datum/job_department/service, + ) + mail_goodies = list( /obj/item/reagent_containers/glass/bottle/mutagen = 20, /obj/item/reagent_containers/glass/bottle/saltpetre = 20, diff --git a/code/modules/jobs/job_types/captain.dm b/code/modules/jobs/job_types/captain.dm index 570240202dae..4f3f1ffe62ef 100644 --- a/code/modules/jobs/job_types/captain.dm +++ b/code/modules/jobs/job_types/captain.dm @@ -28,6 +28,11 @@ paycheck = PAYCHECK_COMMAND paycheck_department = ACCOUNT_SEC + department_for_prefs = /datum/job_department/captain + departments_list = list( + /datum/job_department/command, + ) + mind_traits = list(TRAIT_DISK_VERIFIER) mail_goodies = list( diff --git a/code/modules/jobs/job_types/cargo_technician.dm b/code/modules/jobs/job_types/cargo_technician.dm index 8eeee2317477..b7af99cd58fb 100644 --- a/code/modules/jobs/job_types/cargo_technician.dm +++ b/code/modules/jobs/job_types/cargo_technician.dm @@ -25,6 +25,10 @@ display_order = JOB_DISPLAY_ORDER_CARGO_TECHNICIAN minimal_character_age = 18 //We love manual labor and exploiting the young for our corporate purposes + departments_list = list( + /datum/job_department/cargo, + ) + mail_goodies = list( /obj/item/pizzabox = 10, /obj/item/stack/sheet/mineral/gold = 5, diff --git a/code/modules/jobs/job_types/chaplain.dm b/code/modules/jobs/job_types/chaplain.dm index d6de815d0f0e..56f1ee102aa4 100644 --- a/code/modules/jobs/job_types/chaplain.dm +++ b/code/modules/jobs/job_types/chaplain.dm @@ -24,6 +24,10 @@ display_order = JOB_DISPLAY_ORDER_CHAPLAIN minimal_character_age = 18 //My guy you are literally just a priest + departments_list = list( + /datum/job_department/service, + ) + mail_goodies = list( /obj/item/reagent_containers/food/drinks/bottle/holywater = 30, /obj/item/toy/plush/awakenedplushie = 10, @@ -62,17 +66,11 @@ if(H.mind) H.mind.holy_role = HOLY_ROLE_HIGHPRIEST - var/new_religion = DEFAULT_RELIGION - if(M.client && M.client.prefs.custom_names["religion"]) - new_religion = M.client.prefs.custom_names["religion"] - - var/new_deity = DEFAULT_DEITY - if(M.client && M.client.prefs.custom_names["deity"]) - new_deity = M.client.prefs.custom_names["deity"] + var/new_religion = M.client?.prefs?.read_preference(/datum/preference/name/religion) || DEFAULT_RELIGION + var/new_deity = M.client?.prefs?.read_preference(/datum/preference/name/deity) || DEFAULT_DEITY B.deity_name = new_deity - switch(lowertext(new_religion)) if("christianity") // DEFAULT_RELIGION B.name = pick("The Holy Bible","The Dead Sea Scrolls") diff --git a/code/modules/jobs/job_types/chemist.dm b/code/modules/jobs/job_types/chemist.dm index bed0b3645cec..bb28706314c0 100644 --- a/code/modules/jobs/job_types/chemist.dm +++ b/code/modules/jobs/job_types/chemist.dm @@ -27,6 +27,10 @@ display_order = JOB_DISPLAY_ORDER_CHEMIST minimal_character_age = 24 //A lot of experimental drugs plus understanding the facilitation and purpose of several subtances; what treats what and how to safely manufacture it + departments_list = list( + /datum/job_department/medical, + ) + changed_maps = list("OmegaStation", "EclipseStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/chief_engineer.dm b/code/modules/jobs/job_types/chief_engineer.dm index 249d2d36303b..3492249e7046 100644 --- a/code/modules/jobs/job_types/chief_engineer.dm +++ b/code/modules/jobs/job_types/chief_engineer.dm @@ -33,6 +33,11 @@ display_order = JOB_DISPLAY_ORDER_CHIEF_ENGINEER minimal_character_age = 30 //Combine all the jobs together; that's a lot of physics, mechanical, electrical, and power-based knowledge + departments_list = list( + /datum/job_department/engineering, + /datum/job_department/command, + ) + mail_goodies = list( /obj/item/reagent_containers/food/snacks/cracker = 25, //you know. for poly /obj/item/stack/sheet/mineral/diamond = 15, diff --git a/code/modules/jobs/job_types/chief_medical_officer.dm b/code/modules/jobs/job_types/chief_medical_officer.dm index 2ac71f4d6ae0..45030c7bc6bb 100644 --- a/code/modules/jobs/job_types/chief_medical_officer.dm +++ b/code/modules/jobs/job_types/chief_medical_officer.dm @@ -32,6 +32,11 @@ display_order = JOB_DISPLAY_ORDER_CHIEF_MEDICAL_OFFICER minimal_character_age = 30 //Do you knoW HOW MANY JOBS YOU HAVE TO KNOW TO DO?? This should really be like 35 or something + departments_list = list( + /datum/job_department/medical, + /datum/job_department/command, + ) + changed_maps = list("OmegaStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/clown.dm b/code/modules/jobs/job_types/clown.dm index d5f38adf7a2f..7d4928fddeec 100644 --- a/code/modules/jobs/job_types/clown.dm +++ b/code/modules/jobs/job_types/clown.dm @@ -22,6 +22,10 @@ display_order = JOB_DISPLAY_ORDER_CLOWN minimal_character_age = 18 //Honk + + departments_list = list( + /datum/job_department/service, + ) mail_goodies = list( /obj/item/reagent_containers/food/snacks/grown/banana = 100, @@ -36,7 +40,7 @@ /datum/job/clown/after_spawn(mob/living/carbon/human/H, mob/M) . = ..() - H.apply_pref_name("clown", M.client) + H.apply_pref_name(/datum/preference/name/clown, M.client) /datum/outfit/job/clown name = "Clown" diff --git a/code/modules/jobs/job_types/cook.dm b/code/modules/jobs/job_types/cook.dm index 2027e47be70d..25b34fe82512 100644 --- a/code/modules/jobs/job_types/cook.dm +++ b/code/modules/jobs/job_types/cook.dm @@ -24,6 +24,10 @@ display_order = JOB_DISPLAY_ORDER_COOK minimal_character_age = 18 //My guy they just a cook + departments_list = list( + /datum/job_department/service, + ) + changed_maps = list("OmegaStation", "EclipseStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/curator.dm b/code/modules/jobs/job_types/curator.dm index 760df3f8a236..d4140d1e805c 100644 --- a/code/modules/jobs/job_types/curator.dm +++ b/code/modules/jobs/job_types/curator.dm @@ -24,6 +24,10 @@ display_order = JOB_DISPLAY_ORDER_CURATOR minimal_character_age = 18 //Don't need to be some aged-ass fellow to know how to care for things, possessions could easily have come from parents and the like. Bloodsucker knowledge is another thing, though that's likely mostly consulted by the book + departments_list = list( + /datum/job_department/service, + ) + smells_like = "musty paper" /datum/outfit/job/curator diff --git a/code/modules/jobs/job_types/cyborg.dm b/code/modules/jobs/job_types/cyborg.dm index 9d4689dd12e7..37b89d9a698c 100644 --- a/code/modules/jobs/job_types/cyborg.dm +++ b/code/modules/jobs/job_types/cyborg.dm @@ -17,6 +17,10 @@ display_order = JOB_DISPLAY_ORDER_CYBORG + departments_list = list( + /datum/job_department/silicon, + ) + changed_maps = list("EclipseStation", "OmegaStation") smells_like = "inorganic indifference" @@ -52,8 +56,8 @@ if(!is_donator(C)) return - if(C.prefs.donor_hat && C.prefs.borg_hat) - var/type = C.prefs.donor_hat + if(C.prefs.read_preference(/datum/preference/toggle/borg_hat)) + var/type = GLOB.donator_gear.item_names[C.prefs.read_preference(/datum/preference/choiced/donor_hat)] if(type) var/obj/item/hat = new type() if(istype(hat) && hat.slot_flags & ITEM_SLOT_HEAD && H.hat_offset != INFINITY && !is_type_in_typecache(hat, H.blacklisted_hats)) diff --git a/code/modules/jobs/job_types/detective.dm b/code/modules/jobs/job_types/detective.dm index 84b66d787dca..623c7c2ae422 100644 --- a/code/modules/jobs/job_types/detective.dm +++ b/code/modules/jobs/job_types/detective.dm @@ -29,6 +29,10 @@ display_order = JOB_DISPLAY_ORDER_DETECTIVE minimal_character_age = 22 //Understanding of forensics, crime analysis, and theory. Less of a grunt officer and more of an intellectual, theoretically, despite how this is never reflected in-game + departments_list = list( + /datum/job_department/security, + ) + mail_goodies = list( ///obj/item/storage/fancy/cigarettes = 25, /obj/item/ammo_box/c38 = 25, diff --git a/code/modules/jobs/job_types/geneticist.dm b/code/modules/jobs/job_types/geneticist.dm index 426e228cbc4b..5b32ed194f02 100644 --- a/code/modules/jobs/job_types/geneticist.dm +++ b/code/modules/jobs/job_types/geneticist.dm @@ -24,6 +24,10 @@ display_order = JOB_DISPLAY_ORDER_GENETICIST minimal_character_age = 24 //Genetics would likely require more education than your average position due to the sheer number of alien physiologies and experimental nature of the field + departments_list = list( + /datum/job_department/medical, + ) + mail_goodies = list( /obj/item/storage/box/monkeycubes = 10 ) diff --git a/code/modules/jobs/job_types/head_of_personnel.dm b/code/modules/jobs/job_types/head_of_personnel.dm index bbc1ec396613..394bb5702529 100644 --- a/code/modules/jobs/job_types/head_of_personnel.dm +++ b/code/modules/jobs/job_types/head_of_personnel.dm @@ -36,6 +36,11 @@ display_order = JOB_DISPLAY_ORDER_HEAD_OF_PERSONNEL minimal_character_age = 26 //Baseline age requirement and competency, as well as ability to assume leadership in shite situations + departments_list = list( + /datum/job_department/service, + /datum/job_department/command, + ) + changed_maps = list("OmegaStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/head_of_security.dm b/code/modules/jobs/job_types/head_of_security.dm index c2915f4b364e..43c4d36bf2af 100644 --- a/code/modules/jobs/job_types/head_of_security.dm +++ b/code/modules/jobs/job_types/head_of_security.dm @@ -34,6 +34,11 @@ display_order = JOB_DISPLAY_ORDER_HEAD_OF_SECURITY minimal_character_age = 28 //You need some experience on your belt and a little gruffiness; you're still a foot soldier, not quite a tactician commander back at base + departments_list = list( + /datum/job_department/security, + /datum/job_department/command, + ) + changed_maps = list("YogsPubby") smells_like = "deadly authority" diff --git a/code/modules/jobs/job_types/janitor.dm b/code/modules/jobs/job_types/janitor.dm index 8b6941a746d0..460ab8082f36 100644 --- a/code/modules/jobs/job_types/janitor.dm +++ b/code/modules/jobs/job_types/janitor.dm @@ -23,6 +23,10 @@ display_order = JOB_DISPLAY_ORDER_JANITOR minimal_character_age = 20 //Theoretically janitors do actually need training and certifications in handling of certain hazardous materials as well as cleaning substances, but nothing absurd, I'd assume + departments_list = list( + /datum/job_department/service, + ) + changed_maps = list("OmegaStation", "EclipseStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/lawyer.dm b/code/modules/jobs/job_types/lawyer.dm index df657a12ba3d..66588759d2f5 100644 --- a/code/modules/jobs/job_types/lawyer.dm +++ b/code/modules/jobs/job_types/lawyer.dm @@ -25,6 +25,10 @@ display_order = JOB_DISPLAY_ORDER_LAWYER minimal_character_age = 24 //Law is already absurd, never mind the wacky-ass shit that is space law + departments_list = list( + /datum/job_department/service, + ) + changed_maps = list("OmegaStation") smells_like = "legal lies" diff --git a/code/modules/jobs/job_types/medical_doctor.dm b/code/modules/jobs/job_types/medical_doctor.dm index 38a22a19b31d..070ebb3b5ef6 100644 --- a/code/modules/jobs/job_types/medical_doctor.dm +++ b/code/modules/jobs/job_types/medical_doctor.dm @@ -25,6 +25,10 @@ display_order = JOB_DISPLAY_ORDER_MEDICAL_DOCTOR minimal_character_age = 26 //Barely acceptable considering the theoretically absurd knowledge they have, but fine + departments_list = list( + /datum/job_department/medical, + ) + changed_maps = list("EclipseStation", "OmegaStation") mail_goodies = list( @@ -54,10 +58,8 @@ /datum/outfit/job/doctor name = "Medical Doctor" jobtype = /datum/job/doctor - - pda_type= /obj/item/modular_computer/tablet/pda/preset/medical - ears = /obj/item/radio/headset/headset_med + pda_type = /obj/item/modular_computer/tablet/pda/preset/medical uniform = /obj/item/clothing/under/rank/medical uniform_skirt = /obj/item/clothing/under/rank/medical/skirt shoes = /obj/item/clothing/shoes/sneakers/white @@ -68,5 +70,15 @@ backpack = /obj/item/storage/backpack/medic satchel = /obj/item/storage/backpack/satchel/med duffelbag = /obj/item/storage/backpack/duffelbag/med - chameleon_extras = /obj/item/gun/syringe +/datum/outfit/job/doctor/dead + name = "Medical Doctor" + jobtype = /datum/job/doctor + ears = /obj/item/radio/headset/headset_med + uniform = /obj/item/clothing/under/rank/medical + shoes = /obj/item/clothing/shoes/sneakers/white + suit = /obj/item/clothing/suit/toggle/labcoat/md + l_hand = /obj/item/storage/firstaid/medical + suit_store = /obj/item/flashlight/pen + gloves = /obj/item/clothing/gloves/color/latex/nitrile + pda_type = /obj/item/pda/medical diff --git a/code/modules/jobs/job_types/mime.dm b/code/modules/jobs/job_types/mime.dm index 7c624b5a7b16..0871df5b1ce4 100644 --- a/code/modules/jobs/job_types/mime.dm +++ b/code/modules/jobs/job_types/mime.dm @@ -23,6 +23,10 @@ display_order = JOB_DISPLAY_ORDER_MIME minimal_character_age = 18 //Mime?? Might increase this a LOT depending on how mime lore turns out + departments_list = list( + /datum/job_department/service, + ) + mail_goodies = list( /obj/item/reagent_containers/food/snacks/baguette = 15, /obj/item/reagent_containers/food/snacks/store/cheesewheel = 10, @@ -33,7 +37,7 @@ smells_like = "complete nothingness" /datum/job/mime/after_spawn(mob/living/carbon/human/H, mob/M) - H.apply_pref_name("mime", M.client) + H.apply_pref_name(/datum/preference/name/mime, M.client) /datum/outfit/job/mime name = "Mime" diff --git a/code/modules/jobs/job_types/quartermaster.dm b/code/modules/jobs/job_types/quartermaster.dm index 9142fbd4d807..b96ac4868e2c 100644 --- a/code/modules/jobs/job_types/quartermaster.dm +++ b/code/modules/jobs/job_types/quartermaster.dm @@ -20,6 +20,10 @@ display_order = JOB_DISPLAY_ORDER_QUARTERMASTER minimal_character_age = 20 //Probably just needs some baseline experience with bureaucracy, enough trust to land the position + departments_list = list( + /datum/job_department/cargo, + ) + changed_maps = list("OmegaStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/research_director.dm b/code/modules/jobs/job_types/research_director.dm index 01ddd7feb833..00944c7315c2 100644 --- a/code/modules/jobs/job_types/research_director.dm +++ b/code/modules/jobs/job_types/research_director.dm @@ -36,6 +36,11 @@ display_order = JOB_DISPLAY_ORDER_RESEARCH_DIRECTOR minimal_character_age = 26 //Barely knows more than actual scientists, just responsibility and AI things + departments_list = list( + /datum/job_department/science, + /datum/job_department/command, + ) + changed_maps = list("OmegaStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/roboticist.dm b/code/modules/jobs/job_types/roboticist.dm index a70486c50c36..cc30dd364d43 100644 --- a/code/modules/jobs/job_types/roboticist.dm +++ b/code/modules/jobs/job_types/roboticist.dm @@ -24,6 +24,10 @@ display_order = JOB_DISPLAY_ORDER_ROBOTICIST minimal_character_age = 22 //Engineering, AI theory, robotic knowledge and the like + departments_list = list( + /datum/job_department/science, + ) + changed_maps = list("OmegaStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/scientist.dm b/code/modules/jobs/job_types/scientist.dm index a8c13a8a5a05..8aa8852b30a8 100644 --- a/code/modules/jobs/job_types/scientist.dm +++ b/code/modules/jobs/job_types/scientist.dm @@ -23,6 +23,10 @@ display_order = JOB_DISPLAY_ORDER_SCIENTIST minimal_character_age = 24 //Consider the level of knowledge that spans xenobio, nanites, and toxins + departments_list = list( + /datum/job_department/science, + ) + changed_maps = list("EclipseStation", "OmegaStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index 39500e703b60..9a4d1a9db8e4 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -29,6 +29,10 @@ display_order = JOB_DISPLAY_ORDER_SECURITY_OFFICER minimal_character_age = 18 //Just a few months of boot camp, not a whole year + departments_list = list( + /datum/job_department/security, + ) + changed_maps = list("EclipseStation", "YogsPubby", "OmegaStation") mail_goodies = list( @@ -67,7 +71,7 @@ GLOBAL_LIST_INIT(available_depts_sec, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICA // Assign department security var/department if(M && M.client && M.client.prefs) - department = M.client.prefs.prefered_security_department + department = M.client?.prefs?.read_preference(/datum/preference/choiced/security_department) if(!LAZYLEN(GLOB.available_depts_sec) || department == "None") return else if(department in GLOB.available_depts_sec) diff --git a/code/modules/jobs/job_types/shaft_miner.dm b/code/modules/jobs/job_types/shaft_miner.dm index 81d69aed467d..e76a93cbb256 100644 --- a/code/modules/jobs/job_types/shaft_miner.dm +++ b/code/modules/jobs/job_types/shaft_miner.dm @@ -23,6 +23,10 @@ display_order = JOB_DISPLAY_ORDER_SHAFT_MINER minimal_character_age = 18 //Young and fresh bodies for a high mortality job, what more could you ask for + departments_list = list( + /datum/job_department/cargo, + ) + changed_maps = list("EclipseStation", "OmegaStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/station_engineer.dm b/code/modules/jobs/job_types/station_engineer.dm index ae72933e4d78..59efae7b9eca 100644 --- a/code/modules/jobs/job_types/station_engineer.dm +++ b/code/modules/jobs/job_types/station_engineer.dm @@ -26,6 +26,10 @@ display_order = JOB_DISPLAY_ORDER_STATION_ENGINEER minimal_character_age = 22 //You need to know a lot of complicated stuff about engines, could theoretically just have a traditional bachelor's + departments_list = list( + /datum/job_department/engineering, + ) + changed_maps = list("EclipseStation", "OmegaStation") mail_goodies = list( @@ -56,7 +60,7 @@ GLOBAL_LIST_INIT(available_depts_eng, list(ENG_DEPT_MEDICAL, ENG_DEPT_SCIENCE, E // Assign department engineering var/department if(M && M.client && M.client.prefs) - department = M.client.prefs.prefered_engineering_department + department = M.client.prefs.read_preference(/datum/preference/choiced/engineering_department) if(!LAZYLEN(GLOB.available_depts_eng) || department == "None") return else if(department in GLOB.available_depts_eng) diff --git a/code/modules/jobs/job_types/virologist.dm b/code/modules/jobs/job_types/virologist.dm index fa6aca62c1ef..5bbeee15d52a 100644 --- a/code/modules/jobs/job_types/virologist.dm +++ b/code/modules/jobs/job_types/virologist.dm @@ -28,6 +28,10 @@ display_order = JOB_DISPLAY_ORDER_VIROLOGIST minimal_character_age = 24 //Requires understanding of microbes, biology, infection, and all the like, as well as being able to understand how to interface the machines. Epidemiology is no joke of a field + departments_list = list( + /datum/job_department/medical, + ) + changed_maps = list("OmegaStation") mail_goodies = list( diff --git a/code/modules/jobs/job_types/warden.dm b/code/modules/jobs/job_types/warden.dm index 8b8552620f4b..927b09d77a3c 100644 --- a/code/modules/jobs/job_types/warden.dm +++ b/code/modules/jobs/job_types/warden.dm @@ -31,6 +31,10 @@ display_order = JOB_DISPLAY_ORDER_WARDEN minimal_character_age = 20 //You're a sergeant, probably has some experience in the field + departments_list = list( + /datum/job_department/security, + ) + changed_maps = list("YogsPubby", "OmegaStation") mail_goodies = list( diff --git a/code/modules/jobs/jobs.dm b/code/modules/jobs/jobs.dm index 77099b366b55..3841c9f28b5d 100644 --- a/code/modules/jobs/jobs.dm +++ b/code/modules/jobs/jobs.dm @@ -58,7 +58,7 @@ GLOBAL_LIST_INIT(original_security_positions, list( GLOBAL_LIST_INIT(original_nonhuman_positions, list( "AI", "Cyborg", - ROLE_PAI)) + "pAI")) GLOBAL_LIST_INIT(alt_command_positions, list( "Station Commander", "Facility Director", "Chief Executive Officer", diff --git a/code/modules/keybindings/bindings_client.dm b/code/modules/keybindings/bindings_client.dm index 1168219acbd6..b5c8c9424944 100644 --- a/code/modules/keybindings/bindings_client.dm +++ b/code/modules/keybindings/bindings_client.dm @@ -6,7 +6,7 @@ set hidden = TRUE //Focus Chat failsafe. Overrides movement checks to prevent WASD. - if(!prefs.hotkeys && length(_key) == 1 && _key != "Alt" && _key != "Ctrl" && _key != "Shift") + if(!hotkeys && length(_key) == 1 && _key != "Alt" && _key != "Ctrl" && _key != "Shift") winset(src, null, "input.focus=true ; input.text=[url_encode(_key)]") return @@ -36,7 +36,7 @@ full_key = _key var/keycount = 0 - for(var/kb_name in prefs.key_bindings[full_key]) + for(var/kb_name in prefs.key_bindings_by_key[full_key]) keycount++ var/datum/keybinding/kb = GLOB.keybindings_by_name[kb_name] if(kb.can_use(src) && kb.down(src) && keycount >= MAX_COMMANDS_PER_KEY) @@ -66,7 +66,7 @@ // We don't do full key for release, because for mod keys you // can hold different keys and releasing any should be handled by the key binding specifically - for (var/kb_name in prefs.key_bindings[_key]) + for (var/kb_name in prefs.key_bindings_by_key[_key]) var/datum/keybinding/kb = GLOB.keybindings_by_name[kb_name] if (!kb) stack_trace("Invalid keybind found in keyUp: _key=[_key]; kb_name=[kb_name]") diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index ab03c172efd9..5e98192e9262 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -32,7 +32,7 @@ var/command = macro_set[key] winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[command]") - if(prefs?.hotkeys) + if(hotkeys) winset(src, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED]") else winset(src, null, "input.focus=true input.background-color=[COLOR_INPUT_DISABLED]") diff --git a/code/modules/language/language_holder.dm b/code/modules/language/language_holder.dm index 4e3945179672..663556c866d1 100644 --- a/code/modules/language/language_holder.dm +++ b/code/modules/language/language_holder.dm @@ -59,7 +59,10 @@ Key procs var/datum/mind/M = owner if(M.current) update_atom_languages(M.current) - get_selected_language() + + // If we have an owner, we'll set a default selected language + if(owner) + get_selected_language() /datum/language_holder/Destroy() QDEL_NULL(language_menu) diff --git a/code/modules/mob/dead/new_player/latejoin_menu.dm b/code/modules/mob/dead/new_player/latejoin_menu.dm index e58eda66ae6d..8d942601ec20 100644 --- a/code/modules/mob/dead/new_player/latejoin_menu.dm +++ b/code/modules/mob/dead/new_player/latejoin_menu.dm @@ -17,6 +17,20 @@ GLOBAL_DATUM_INIT(latejoin_menu, /datum/latejoin_menu, new) user.AttemptLateSpawn(input_contents) +/datum/latejoin_menu/verb/open_fallback_ui() + set category = "Preferences" + set name = "Open fallback latejoin menu" + set desc = "Open fallback latejoin menu" + + if (!istype(usr, /mob/dead/new_player)) + to_chat(usr, span_notice("You cannot do this at this time!")) + return + + if (!GLOB.latejoin_menu.check_latejoin_eligibility(usr, use_chat = TRUE)) + return + + GLOB.latejoin_menu.fallback_ui(usr) + /datum/latejoin_menu/ui_close(mob/dead/new_player/user) . = ..() if(istype(user)) @@ -36,7 +50,7 @@ GLOBAL_DATUM_INIT(latejoin_menu, /datum/latejoin_menu, new) /datum/latejoin_menu/proc/scream_at_player(mob/dead/new_player/player) if(!player.jobs_menu_mounted) - to_chat(player, span_notice("If the late join menu isn't showing, hold CTRL while clicking the join button!")) + to_chat(player, span_notice("If the late join menu isn't showing, you can open the fallback menu using the verb in the Preferences tab!")) /datum/latejoin_menu/ui_data(mob/user) var/mob/dead/new_player/owner = user @@ -58,20 +72,15 @@ GLOBAL_DATUM_INIT(latejoin_menu, /datum/latejoin_menu, new) if(prioritized_job.current_positions >= prioritized_job.total_positions) SSjob.prioritized_jobs -= prioritized_job - for(var/list/category in list(GLOB.command_positions) + list(GLOB.engineering_positions) + list(GLOB.supply_positions) + list(GLOB.nonhuman_positions - "pAI") + list(GLOB.civilian_positions) + list(GLOB.science_positions) + list(GLOB.security_positions) + list(GLOB.medical_positions) ) - var/cat_name = SSjob.name_occupations_all[category[1]].exp_type_department - + for(var/datum/job_department/department as anything in SSjob.joinable_departments) var/list/department_jobs = list() var/list/department_data = list( "jobs" = department_jobs, "open_slots" = 0, ) - departments[cat_name] = department_data - for(var/job in category) - var/datum/job/job_datum = SSjob.name_occupations[job] - if (!job_datum) - continue + departments[department.department_name] = department_data + for(var/datum/job/job_datum as anything in department.department_jobs) var/job_availability = owner.IsJobUnavailable(job_datum.title, latejoin = TRUE) var/list/job_data = list( @@ -97,26 +106,17 @@ GLOBAL_DATUM_INIT(latejoin_menu, /datum/latejoin_menu, new) /datum/latejoin_menu/ui_static_data(mob/user) var/list/departments = list() - for(var/list/category in list(GLOB.command_positions) + list(GLOB.engineering_positions) + list(GLOB.supply_positions) + list(GLOB.nonhuman_positions - "pAI") + list(GLOB.civilian_positions) + list(GLOB.science_positions) + list(GLOB.security_positions) + list(GLOB.medical_positions) ) - var/cat_color = SSjob.name_occupations_all[category[1]].selection_color - var/cat_name = SSjob.name_occupations_all[category[1]].exp_type_department - + for(var/datum/job_department/department as anything in SSjob.joinable_departments) var/list/department_jobs = list() var/list/department_data = list( "jobs" = department_jobs, - "color" = cat_color, + "color" = department.ui_color, ) - departments[cat_name] = department_data - - for(var/job in category) - var/datum/job/job_datum = SSjob.name_occupations[job] - if (!job_datum) - continue - - var/is_command = (job in GLOB.command_positions) + departments[department.department_name] = department_data + for(var/datum/job/job_datum as anything in department.department_jobs) var/list/job_data = list( - "command" = is_command, + "command" = !!(job_datum.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND), "description" = job_datum.description, "icon" = job_datum.orbit_icon, ) @@ -149,33 +149,50 @@ GLOBAL_DATUM_INIT(latejoin_menu, /datum/latejoin_menu, new) params["job"] = job - if(!SSticker?.IsRoundInProgress()) - tgui_alert(owner, "The round is either not ready, or has already finished...", "Oh No!") + if (!check_latejoin_eligibility(owner)) return TRUE - if(!GLOB.enter_allowed || SSticker.late_join_disabled) - tgui_alert(owner, "There is an administrative lock on entering the game for non-observers!", "Oh No!") - return TRUE - - //Determines Relevent Population Cap - var/relevant_cap - var/hard_popcap = CONFIG_GET(number/hard_popcap) - var/extreme_popcap = CONFIG_GET(number/extreme_popcap) - if(hard_popcap && extreme_popcap) - relevant_cap = min(hard_popcap, extreme_popcap) - else - relevant_cap = max(hard_popcap, extreme_popcap) - - if(SSticker.queued_players.len) - if((living_player_count() >= relevant_cap) || (owner != SSticker.queued_players[1])) - tgui_alert(owner, "The server is full!", "Oh No!") - return TRUE - + remove_verb(owner, /datum/latejoin_menu/verb/open_fallback_ui) // SAFETY: AttemptLateSpawn has it's own sanity checks. This is perfectly safe. owner.AttemptLateSpawn(params["job"]) return TRUE + +/datum/latejoin_menu/proc/check_latejoin_eligibility(mob/dead/new_player/owner, var/use_chat = FALSE) + if(!SSticker?.IsRoundInProgress()) + if (use_chat) + to_chat(owner, span_notice("The round is either not ready, or has already finished...")) + else + tgui_alert(owner, "The round is either not ready, or has already finished...", "Oh No!") + return FALSE + + if(!GLOB.enter_allowed || SSticker.late_join_disabled) + if (use_chat) + to_chat(owner, span_notice("There is an administrative lock on entering the game for non-observers!")) + else + tgui_alert(owner, "There is an administrative lock on entering the game for non-observers!", "Oh No!") + return FALSE + + //Determines Relevent Population Cap + var/relevant_cap + var/hard_popcap = CONFIG_GET(number/hard_popcap) + var/extreme_popcap = CONFIG_GET(number/extreme_popcap) + if(hard_popcap && extreme_popcap) + relevant_cap = min(hard_popcap, extreme_popcap) + else + relevant_cap = max(hard_popcap, extreme_popcap) + + if(SSticker.queued_players.len) + if((living_player_count() >= relevant_cap) || (owner != SSticker.queued_players[1])) + if (use_chat) + to_chat(owner, span_notice("The server is full!")) + else + tgui_alert(owner, "The server is full!", "Oh No!") + return FALSE + + return TRUE + /// Gives the user a random job that they can join as, and prompts them if they'd actually like to keep it, rerolling if not. Cancellable by the user. /// WARNING: BLOCKS THREAD! /datum/latejoin_menu/proc/get_random_job(mob/dead/new_player/owner) diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 8d0e81b51aa2..19d90f266f4a 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -31,6 +31,8 @@ ComponentInitialize() + add_verb(usr, /datum/latejoin_menu/verb/open_fallback_ui) + . = ..() GLOB.new_player_list += src @@ -46,6 +48,7 @@ var/datum/asset/asset_datum = get_asset_datum(/datum/asset/simple/lobby) asset_datum.send(client) var/output = "

Setup Character

" + output += "

Game Options

" if(SSticker.current_state <= GAME_STATE_PREGAME) switch(ready) @@ -117,8 +120,18 @@ relevant_cap = max(hpc, epc) if(href_list["show_preferences"]) - client.prefs.ShowChoices(src) - return 1 + var/datum/preferences/preferences = client.prefs + preferences.current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES + preferences.update_static_data(usr) + preferences.ui_interact(usr) + return TRUE + + if(href_list["show_gameoptions"]) + var/datum/preferences/preferences = client.prefs + preferences.current_window = PREFERENCE_TAB_GAME_PREFERENCES + preferences.update_static_data(usr) + preferences.ui_interact(usr) + return TRUE if(href_list["ready"]) var/tready = text2num(href_list["ready"]) @@ -142,10 +155,6 @@ to_chat(usr, span_danger("The round is either not ready, or has already finished...")) return - if(href_list["late_join"] == "override") - GLOB.latejoin_menu.ui_interact(src) - return - if(SSticker.queued_players.len || (relevant_cap && living_player_count() >= relevant_cap && !(ckey(key) in GLOB.permissions.admin_datums))) //yogs start -- donors bypassing the queue if(ckey(key) in get_donators()) @@ -165,10 +174,8 @@ to_chat(usr, span_notice("You have been added to the queue to join the game. Your position in queue is [SSticker.queued_players.len].")) return - // TODO: Fallback menu GLOB.latejoin_menu.ui_interact(usr) - if(href_list["manifest"]) ViewManifest() @@ -289,7 +296,7 @@ observer.client = client observer.set_ghost_appearance() if(observer.client && observer.client.prefs) - observer.real_name = observer.client.prefs.real_name + observer.real_name = observer.client.prefs.read_preference(/datum/preference/name/real_name) observer.name = observer.real_name observer.client.init_verbs() observer.update_icon() @@ -445,13 +452,11 @@ if(QDELETED(src)) return if(frn) - client.prefs.random_character() - client.prefs.accent = null - client.prefs.real_name = client.prefs.pref_species.random_name(gender,1) - client.prefs.copy_to(H) + client.prefs.randomise_appearance_prefs() - client.prefs.copy_to(H) + client.prefs.apply_prefs_to(H) H.dna.update_dna_identity() + if(mind) if(mind.assigned_role) var/datum/job/J = SSjob.GetJob(mind.assigned_role) @@ -462,8 +467,11 @@ mind.late_joiner = TRUE mind.active = FALSE //we wish to transfer the key manually mind.original_character_slot_index = client.prefs.default_slot - if(!HAS_TRAIT(H,TRAIT_RANDOM_ACCENT)) - mind.accent_name = client.prefs.accent + if(!HAS_TRAIT(H, TRAIT_RANDOM_ACCENT)) + var/accent_name = client.prefs.read_preference(/datum/preference/choiced/accent) + if (accent_name == ACCENT_NONE) + accent_name = null + mind.accent_name = accent_name mind.transfer_to(H) //won't transfer key since the mind is not active mind.original_character = H @@ -512,7 +520,7 @@ /mob/dead/new_player/proc/check_preferences() if(!client) return FALSE //Not sure how this would get run without the mob having a client, but let's just be safe. - if(client.prefs.joblessrole != RETURNTOLOBBY) + if(client.prefs.read_preference(/datum/preference/choiced/jobless_role) != RETURNTOLOBBY) return TRUE // If they have antags enabled, they're potentially doing this on purpose instead of by accident. Notify admins if so. var/has_antags = FALSE diff --git a/code/modules/mob/dead/new_player/preferences_setup.dm b/code/modules/mob/dead/new_player/preferences_setup.dm index 7703b6c0f551..eefa3e385051 100644 --- a/code/modules/mob/dead/new_player/preferences_setup.dm +++ b/code/modules/mob/dead/new_player/preferences_setup.dm @@ -1,65 +1,55 @@ - //The mob should have a gender you want before running this proc. Will run fine without H -/datum/preferences/proc/random_character(gender_override) - if(gender_override) - gender = gender_override - else - gender = pick(MALE,FEMALE) - if(!random_locks["underwear"]) - underwear = random_underwear(gender) - if(!random_locks["undershirt"]) - undershirt = random_undershirt(gender) - if(!random_locks["socks"]) - socks = random_socks() - if(!random_locks["skin_tone"]) - skin_tone = random_skin_tone() - if(!random_locks["hair_style"]) - hair_style = random_hair_style(gender) - if(!random_locks["facial_hair_style"]) - facial_hair_style = random_facial_hair_style(gender) - if(!random_locks["hair"]) - hair_color = random_short_color() - if(!random_locks["facial"]) - facial_hair_color = hair_color - if(!random_locks["eye_color"]) - eye_color = random_eye_color() - if(!pref_species) - var/rando_race = pick(GLOB.roundstart_races) - pref_species = new rando_race() - var/temp_features = random_features() - for(var/i in temp_features) - if(random_locks[i]) - i = features[i] - features = temp_features - age = rand(AGE_MIN,AGE_MAX) +/// Fully randomizes everything in the character. +/datum/preferences/proc/randomise_appearance_prefs(randomize_flags = ALL) + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (!preference.included_in_randomization_flags(randomize_flags)) + continue -/datum/preferences/proc/update_preview_icon() - // Determine what job is marked as 'High' priority, and dress them up as such. - var/datum/job/previewJob + if (preference.is_randomizable()) + write_preference(preference, preference.create_random_value(src)) + +/// Randomizes the character according to preferences. +/datum/preferences/proc/apply_character_randomization_prefs(antag_override = FALSE) + switch (read_preference(/datum/preference/choiced/random_body)) + if (RANDOM_ANTAG_ONLY) + if (!antag_override) + return + + if (RANDOM_DISABLED) + return + + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (should_randomize(preference, antag_override)) + write_preference(preference, preference.create_random_value(src)) + +/// Returns what job is marked as highest +/datum/preferences/proc/get_highest_priority_job() + var/datum/job/preview_job var/highest_pref = 0 + for(var/job in job_preferences) if(job_preferences[job] > highest_pref) - previewJob = SSjob.GetJob(job) + preview_job = SSjob.GetJob(job) highest_pref = job_preferences[job] - if(previewJob) + return preview_job + +/datum/preferences/proc/render_new_preview_appearance(mob/living/carbon/human/dummy/mannequin) + var/datum/job/preview_job = get_highest_priority_job() + + if(preview_job) // Silicons only need a very basic preview since there is no customization for them. - if(istype(previewJob,/datum/job/ai)) - parent.show_character_previews(image('icons/mob/ai.dmi', icon_state = resolve_ai_icon(preferred_ai_core_display), dir = SOUTH)) - return - if(istype(previewJob,/datum/job/cyborg)) - parent.show_character_previews(image('icons/mob/robots.dmi', icon_state = "robot", dir = SOUTH)) - return + if (istype(preview_job, /datum/job/ai)) + return image('icons/mob/ai.dmi', icon_state = resolve_ai_icon(read_preference(/datum/preference/choiced/ai_core_display)), dir = SOUTH) + if (istype(preview_job, /datum/job/cyborg)) + return image('icons/mob/robots.dmi', icon_state = "robot", dir = SOUTH) // Set up the dummy for its photoshoot - var/mob/living/carbon/human/dummy/mannequin = generate_or_wait_for_human_dummy(DUMMY_HUMAN_SLOT_PREFERENCES) mannequin.add_overlay(mutable_appearance('icons/turf/floors.dmi', background, layer = SPACE_LAYER)) - copy_to(mannequin) + apply_prefs_to(mannequin, TRUE) - if(previewJob) - mannequin.job = previewJob.title - previewJob.equip(mannequin, TRUE, preference_source = parent) + if(preview_job) + mannequin.job = preview_job.title + mannequin.dress_up_as_job(preview_job, TRUE) - COMPILE_OVERLAYS(mannequin) - parent.show_character_previews(new /mutable_appearance(mannequin)) - unset_busy_human_dummy(DUMMY_HUMAN_SLOT_PREFERENCES) + return mannequin.appearance diff --git a/code/modules/mob/dead/observer/login.dm b/code/modules/mob/dead/observer/login.dm index 1b328dbc697c..e39207ffde59 100644 --- a/code/modules/mob/dead/observer/login.dm +++ b/code/modules/mob/dead/observer/login.dm @@ -1,16 +1,16 @@ /mob/dead/observer/Login() ..() - ghost_accs = client.prefs.ghost_accs - ghost_others = client.prefs.ghost_others + ghost_accs = client.prefs.read_preference(/datum/preference/choiced/ghost_accessories) + ghost_others = client.prefs.read_preference(/datum/preference/choiced/ghost_others) var/preferred_form = null if(IsAdminGhost(src)) has_unlimited_silicon_privilege = 1 - if(client.prefs.unlock_content) - preferred_form = client.prefs.ghost_form - ghost_orbit = client.prefs.ghost_orbit + if(is_donator(client)) + preferred_form = client.prefs.read_preference(/datum/preference/choiced/ghost_form) + ghost_orbit = client.prefs.read_preference(/datum/preference/choiced/ghost_orbit) var/turf/T = get_turf(src) if (isturf(T)) diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 5ff31f655d36..00b8128aec36 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -185,8 +185,8 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) */ /mob/dead/observer/proc/update_icon(new_form) if(client) //We update our preferences in case they changed right before update_icon was called. - ghost_accs = client.prefs.ghost_accs - ghost_others = client.prefs.ghost_others + ghost_accs = client.prefs.read_preference(/datum/preference/choiced/ghost_accessories) + ghost_others = client.prefs.read_preference(/datum/preference/choiced/ghost_others) if(hair_overlay) cut_overlay(hair_overlay) @@ -204,7 +204,7 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) else ghostimage_default.icon_state = new_form - if(ghost_accs >= GHOST_ACCS_DIR && (icon_state in GLOB.ghost_forms_with_directions_list)) //if this icon has dirs AND the client wants to show them, we make sure we update the dir on movement + if((ghost_accs == GHOST_ACCS_DIR || ghost_accs == GHOST_ACCS_FULL) && (icon_state in GLOB.ghost_forms_with_directions_list)) //if this icon has dirs AND the client wants to show them, we make sure we update the dir on movement updatedir = 1 else updatedir = 0 //stop updating the dir in case we want to show accessories with dirs on a ghost sprite without dirs @@ -355,7 +355,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp if(mind.current.key && mind.current.key[1] != "@") //makes sure we don't accidentally kick any clients to_chat(usr, span_warning("Another consciousness is in your body...It is resisting you.")) return - client.view_size.setDefault(getScreenSize(client.prefs.widescreenpref))//Let's reset so people can't become allseeing gods + client.view_size.setDefault(getScreenSize(client.prefs.read_preference(/datum/preference/toggle/widescreen)))//Let's reset so people can't become allseeing gods SStgui.on_transfer(src, mind.current) // Transfer NanoUIs. mind.current.key = key mind.current.oobe_client = null //yogs @@ -387,8 +387,9 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp if(source) var/atom/movable/screen/alert/A = throw_alert("[REF(source)]_notify_cloning", /atom/movable/screen/alert/notify_cloning) if(A) - if(client && client.prefs && client.prefs.UI_style) - A.icon = ui_style2icon(client.prefs.UI_style) + var/ui_style = client?.prefs?.read_preference(/datum/preference/choiced/ui_style) + if(ui_style) + A.icon = ui_style2icon(ui_style) A.desc = message var/old_layer = source.layer var/old_plane = source.plane @@ -578,7 +579,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp /mob/dead/observer/update_sight() if(client) - ghost_others = client.prefs.ghost_others //A quick update just in case this setting was changed right before calling the proc + ghost_others = client.prefs.read_preference(/datum/preference/choiced/ghost_others) //A quick update just in case this setting was changed right before calling the proc if (!ghostvision) see_invisible = SEE_INVISIBLE_LIVING @@ -606,11 +607,11 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp client.images -= GLOB.ghost_images_default if(GHOST_OTHERS_SIMPLE) client.images -= GLOB.ghost_images_simple - lastsetting = client.prefs.ghost_others + lastsetting = client.prefs.read_preference(/datum/preference/choiced/ghost_others) if(!ghostvision) return - if(client.prefs.ghost_others != GHOST_OTHERS_THEIR_SETTING) - switch(client.prefs.ghost_others) + if(lastsetting != GHOST_OTHERS_THEIR_SETTING) + switch(lastsetting) if(GHOST_OTHERS_DEFAULT_SPRITE) client.images |= (GLOB.ghost_images_default-ghostimage_default) if(GHOST_OTHERS_SIMPLE) @@ -779,26 +780,31 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp set category = "Ghost" set_ghost_appearance() - if(client && client.prefs) - deadchat_name = client.prefs.real_name + if(client?.prefs) + var/real_name = client.prefs.read_preference(/datum/preference/name/real_name) + deadchat_name = real_name if(mind) - mind.name = client.prefs.real_name + mind.name = real_name + name = real_name /mob/dead/observer/proc/set_ghost_appearance() if((!client) || (!client.prefs)) return - if(client.prefs.be_random_name) - client.prefs.real_name = random_unique_name(gender) - if(client.prefs.be_random_body) - client.prefs.random_character(gender) - - if(HAIR in client.prefs.pref_species.species_traits) - hair_style = client.prefs.hair_style - hair_color = brighten_color(client.prefs.hair_color) - if(FACEHAIR in client.prefs.pref_species.species_traits) - facial_hair_style = client.prefs.facial_hair_style - facial_hair_color = brighten_color(client.prefs.facial_hair_color) + client.prefs.apply_character_randomization_prefs() + + var/species_type = client.prefs.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + if(HAIR in species.species_traits) + hair_style = client.prefs.read_preference(/datum/preference/choiced/hairstyle) + hair_color = brighten_color(client.prefs.read_preference(/datum/preference/color_legacy/hair_color)) + + if(FACEHAIR in species.species_traits) + facial_hair_style = client.prefs.read_preference(/datum/preference/choiced/facial_hairstyle) + facial_hair_color = brighten_color(client.prefs.read_preference(/datum/preference/color_legacy/facial_hair_color)) + + qdel(species) update_icon() diff --git a/code/modules/mob/dead/observer/say.dm b/code/modules/mob/dead/observer/say.dm index d441bb33def1..0e3dc94844e8 100644 --- a/code/modules/mob/dead/observer/say.dm +++ b/code/modules/mob/dead/observer/say.dm @@ -43,7 +43,7 @@ to_follow = V.source var/link = FOLLOW_LINK(src, to_follow) // Create map text prior to modifying message for goonchat - if (client?.prefs.chat_on_map && (client.prefs.see_chat_non_mob || ismob(speaker))) + if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && (client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker))) create_chat_message(speaker, message_language, raw_message, spans) // Recompose the message, because it's scrambled by default message = compose_message(speaker, message_language, raw_message, radio_freq, spans, message_mods) diff --git a/code/modules/mob/living/carbon/human/dummy.dm b/code/modules/mob/living/carbon/human/dummy.dm index ce9b1e64d9af..8391acb58f06 100644 --- a/code/modules/mob/living/carbon/human/dummy.dm +++ b/code/modules/mob/living/carbon/human/dummy.dm @@ -69,10 +69,47 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy) cut_overlays(TRUE) /mob/living/carbon/human/dummy/setup_human_dna() - create_dna(src) + create_dna() randomize_human(src) dna.initialize_dna(skip_index = TRUE) //Skip stuff that requires full round init. +/proc/create_consistent_human_dna(mob/living/carbon/human/target) + target.dna.initialize_dna(skip_index = TRUE) + target.dna.features["body_markings"] = "None" + target.dna.features["ears"] = "None" + target.dna.features["ethcolor"] = "EEEEEE" // white-ish + target.dna.features["frills"] = "None" + target.dna.features["horns"] = "None" + target.dna.features["mcolor"] = COLOR_VIBRANT_LIME + target.dna.features["moth_antennae"] = "Plain" + target.dna.features["moth_markings"] = "None" + target.dna.features["moth_wings"] = "Plain" + target.dna.features["snout"] = "Round" + target.dna.features["spines"] = "None" + target.dna.features["tail_cat"] = "None" + target.dna.features["tail_lizard"] = "Smooth" + target.dna.features["pod_hair"] = "Ivy" + +/// Provides a dummy that is consistently bald, white, naked, etc. +/mob/living/carbon/human/dummy/consistent + +/mob/living/carbon/human/dummy/consistent/setup_human_dna() + create_dna() + create_consistent_human_dna(src) + +/// Provides a dummy for unit_tests that functions like a normal human, but with a standardized appearance +/// Copies the stock dna setup from the dummy/consistent type +/mob/living/carbon/human/consistent + +/mob/living/carbon/human/consistent/setup_human_dna() + create_dna() + create_consistent_human_dna(src) + +/mob/living/carbon/human/consistent/update_body(is_creating) + ..() + if(is_creating) + fully_replace_character_name(real_name, "John Doe") + //Inefficient pooling/caching way. GLOBAL_LIST_EMPTY(human_dummy_list) GLOBAL_LIST_EMPTY(dummy_mob_list) diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm index 3051debb2b19..8c986f207a5d 100644 --- a/code/modules/mob/living/carbon/human/human_defines.dm +++ b/code/modules/mob/living/carbon/human/human_defines.dm @@ -35,7 +35,6 @@ var/socks = "Nude" //Which socks the player wants var/backbag = DBACKPACK //Which backpack type the player has chosen. var/jumpsuit_style = PREF_SUIT //suit/skirt - var/id_in_pda = FALSE //Whether the player wants their ID to start in their PDA //Equipment slots var/obj/item/clothing/wear_suit = null diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm index 3f6ddf10adf6..832525a00adc 100644 --- a/code/modules/mob/living/carbon/human/human_helpers.dm +++ b/code/modules/mob/living/carbon/human/human_helpers.dm @@ -184,7 +184,7 @@ /// When we're joining the game in [/mob/dead/new_player/proc/create_character], we increment our scar slot then store the slot in our mind datum. /mob/living/carbon/human/proc/increment_scar_slot() var/check_ckey = ckey || client?.ckey - if(!check_ckey || !mind || !client?.prefs.persistent_scars) + if(!check_ckey || !mind || !client?.prefs.read_preference(/datum/preference/toggle/persistent_scars)) return var/path = "data/player_saves/[check_ckey[1]]/[check_ckey]/scars.sav" @@ -229,7 +229,7 @@ /// Read all the scars we have for the designated character/scar slots, verify they're good/dump them if they're old/wrong format, create them on the user, and write the scars that passed muster back to the file /mob/living/carbon/human/proc/load_persistent_scars() - if(!ckey || !mind?.original_character_slot_index || !client?.prefs.persistent_scars) + if(!ckey || !mind?.original_character_slot_index || !client?.prefs.read_preference(/datum/preference/toggle/persistent_scars)) return var/path = "data/player_saves/[ckey[1]]/[ckey]/scars.sav" @@ -255,7 +255,7 @@ /// Save any scars we have to our designated slot, then write our current slot so that the next time we call [/mob/living/carbon/human/proc/increment_scar_slot] (the next round we join), we'll be there /mob/living/carbon/human/proc/save_persistent_scars(nuke=FALSE) - if(!ckey || !mind?.original_character_slot_index || !client?.prefs.persistent_scars) + if(!ckey || !mind?.original_character_slot_index || !client?.prefs.read_preference(/datum/preference/toggle/persistent_scars)) return var/path = "data/player_saves/[ckey[1]]/[ckey]/scars.sav" @@ -286,3 +286,14 @@ /mob/living/carbon/human/proc/get_punchstunthreshold() //Gets the total punch damage needed to knock down someone return dna.species.punchstunthreshold + physiology.punchstunthreshold_bonus + +/// Fully randomizes everything according to the given flags. +/mob/living/carbon/human/proc/randomize_human_appearance(randomize_flags = ALL) + var/datum/preferences/preferences = new + + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (!preference.included_in_randomization_flags(randomize_flags)) + continue + + if (preference.is_randomizable()) + preferences.write_preference(preference, preference.create_random_value(preferences)) diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm index ea66c0d76044..edfa33ebc12f 100644 --- a/code/modules/mob/living/carbon/human/inventory.dm +++ b/code/modules/mob/living/carbon/human/inventory.dm @@ -257,9 +257,9 @@ else O = outfit if(!istype(O)) - return 0 + return FALSE if(!O) - return 0 + return FALSE return O.equip(src, visualsOnly) diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm index 71effd47c12d..945a0cf50292 100644 --- a/code/modules/mob/living/carbon/human/species.dm +++ b/code/modules/mob/living/carbon/human/species.dm @@ -3,6 +3,9 @@ GLOBAL_LIST_EMPTY(roundstart_races) GLOBAL_LIST_EMPTY(mentor_races) +/// An assoc list of species types to their features (from get_features()) +GLOBAL_LIST_EMPTY(features_by_species) + /datum/species /// if the game needs to manually check your race to do something not included in a proc here, it will use this var/id @@ -10,6 +13,9 @@ GLOBAL_LIST_EMPTY(mentor_races) var/limbs_id /// this is the fluff name. these will be left generic (such as 'Lizardperson' for the lizard race) so servers can change them to whatever var/name + /// The formatting of the name of the species in plural context. Defaults to "[name]\s" if unset. + /// Ex "[Plasmamen] are weak", "[Mothmen] are strong", "[Lizardpeople] don't like", "[Golems] hate" + var/plural_form /// if alien colors are disabled, this is the color that will be used by that race var/default_color = "#FFF" /// whether or not the race has sexual characteristics. at the moment this is only FALSE for skeletons and shadows @@ -31,7 +37,7 @@ GLOBAL_LIST_EMPTY(mentor_races) var/use_skintones = FALSE /// If your race wants to bleed something other than bog standard blood, change this to reagent id. - var/exotic_blood = "" + var/datum/reagent/exotic_blood ///If your race uses a non standard bloodtype (A+, O-, AB-, etc) var/exotic_bloodtype = "" ///What the species drops on gibbing @@ -196,24 +202,49 @@ GLOBAL_LIST_EMPTY(mentor_races) /datum/species/New() - if(!limbs_id) //if we havent set a limbs id to use, just use our own id limbs_id = id - ..() + + if(!plural_form) + plural_form = "[name]\s" + + return ..() + +/// Gets a list of all species available to choose in roundstart. +/proc/get_selectable_species() + RETURN_TYPE(/list) + + if (!GLOB.roundstart_races.len) + GLOB.roundstart_races = generate_selectable_species() + return GLOB.roundstart_races +/** + * Generates species available to choose in character setup at roundstart + * + * This proc generates which species are available to pick from in character setup. + * If there are no available roundstart species, defaults to human. + */ /proc/generate_selectable_species() - for(var/I in subtypesof(/datum/species)) - var/datum/species/S = new I - if(S.check_roundstart_eligible()) - GLOB.roundstart_races += S.id - qdel(S) - else if(S.check_mentor()) - GLOB.mentor_races += S.id - qdel(S) - if(!GLOB.roundstart_races.len) - GLOB.roundstart_races += "human" + var/list/selectable_species = list() + + for(var/species_type in subtypesof(/datum/species)) + var/datum/species/species = new species_type + if(species.check_roundstart_eligible()) + selectable_species += species.id + qdel(species) + + if(!selectable_species.len) + selectable_species += "human" + + return selectable_species +/** + * Checks if a species is eligible to be picked at roundstart. + * + * Checks the config to see if this species is allowed to be picked in the character setup menu. + * Used by [/proc/generate_selectable_species]. + */ /datum/species/proc/check_roundstart_eligible() if(id in (CONFIG_GET(keyed_list/roundstart_races))) return TRUE @@ -2328,3 +2359,402 @@ GLOBAL_LIST_EMPTY(mentor_races) to_store += mutanttail //We don't cache mutant hands because it's not constrained enough, too high a potential for failure return to_store + +/// Returns a list of strings representing features this species has. +/// Used by the preferences UI to know what buttons to show. +/datum/species/proc/get_features() + var/cached_features = GLOB.features_by_species[type] + if (!isnull(cached_features)) + return cached_features + + var/list/features = list() + + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + + if ( \ + (preference.relevant_mutant_bodypart in mutant_bodyparts) \ + || (preference.relevant_species_trait in species_traits) \ + ) + features += preference.savefile_key + + /*for (var/obj/item/organ/external/organ_type as anything in external_organs) + var/preference = initial(organ_type.preference) + if (!isnull(preference)) + features += preference*/ + + GLOB.features_by_species[type] = features + + return features + +/// Given a human, will adjust it before taking a picture for the preferences UI. +/// This should create a CONSISTENT result, so the icons don't randomly change. +/datum/species/proc/prepare_human_for_preview(mob/living/carbon/human/human) + return + +/** + * Gets a short description for the specices. Should be relatively succinct. + * Used in the preference menu. + * + * Returns a string. + */ +/datum/species/proc/get_species_description() + SHOULD_CALL_PARENT(FALSE) + + stack_trace("Species [name] ([type]) did not have a description set, and is a selectable roundstart race! Override get_species_description.") + return "No species description set, file a bug report!" + +/** + * Gets the lore behind the type of species. Can be long. + * Used in the preference menu. + * + * Returns a list of strings. + * Between each entry in the list, a newline will be inserted, for formatting. + */ +/datum/species/proc/get_species_lore() + SHOULD_CALL_PARENT(FALSE) + RETURN_TYPE(/list) + + stack_trace("Species [name] ([type]) did not have lore set, and is a selectable roundstart race! Override get_species_lore.") + return list("No species lore set, file a bug report!") + +/** + * Translate the species liked foods from bitfields into strings + * and returns it in the form of an associated list. + * + * Returns a list, or null if they have no diet. + */ +/datum/species/proc/get_species_diet() + if(TRAIT_NOHUNGER in inherent_traits) + return null + + var/list/food_flags = FOOD_FLAGS + + return list( + "liked_food" = bitfield_to_list(liked_food, food_flags), + "disliked_food" = bitfield_to_list(disliked_food, food_flags), + "toxic_food" = bitfield_to_list(toxic_food, food_flags), + ) + +/** + * Generates a list of "perks" related to this species + * (Postives, neutrals, and negatives) + * in the format of a list of lists. + * Used in the preference menu. + * + * "Perk" format is as followed: + * list( + * SPECIES_PERK_TYPE = type of perk (postiive, negative, neutral - use the defines) + * SPECIES_PERK_ICON = icon shown within the UI + * SPECIES_PERK_NAME = name of the perk on hover + * SPECIES_PERK_DESC = description of the perk on hover + * ) + * + * Returns a list of lists. + * The outer list is an assoc list of [perk type]s to a list of perks. + * The innter list is a list of perks. Can be empty, but won't be null. + */ +/datum/species/proc/get_species_perks() + var/list/species_perks = list() + + // Let us get every perk we can concieve of in one big list. + // The order these are called (kind of) matters. + // Species unique perks first, as they're more important than genetic perks, + // and language perk last, as it comes at the end of the perks list + species_perks += create_pref_unique_perks() + species_perks += create_pref_blood_perks() + species_perks += create_pref_combat_perks() + species_perks += create_pref_damage_perks() + species_perks += create_pref_temperature_perks() + species_perks += create_pref_traits_perks() + species_perks += create_pref_biotypes_perks() + species_perks += create_pref_language_perk() + + // Some overrides may return `null`, prevent those from jamming up the list. + listclearnulls(species_perks) + + // Now let's sort them out for cleanliness and sanity + var/list/perks_to_return = list( + SPECIES_POSITIVE_PERK = list(), + SPECIES_NEUTRAL_PERK = list(), + SPECIES_NEGATIVE_PERK = list(), + ) + + for(var/list/perk as anything in species_perks) + var/perk_type = perk[SPECIES_PERK_TYPE] + // If we find a perk that isn't postiive, negative, or neutral, + // it's a bad entry - don't add it to our list. Throw a stack trace and skip it instead. + if(isnull(perks_to_return[perk_type])) + stack_trace("Invalid species perk ([perk[SPECIES_PERK_NAME]]) found for species [name]. \ + The type should be positive, negative, or neutral. (Got: [perk_type])") + continue + + perks_to_return[perk_type] += list(perk) + + return perks_to_return + +/** + * Used to add any species specific perks to the perk list. + * + * Returns null by default. When overriding, return a list of perks. + */ +/datum/species/proc/create_pref_unique_perks() + return null + +/** + * Adds adds any perks related to combat. + * For example, the damage type of their punches. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_combat_perks() + var/list/to_add = list() + + if(attack_type != BRUTE) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "fist-raised", + SPECIES_PERK_NAME = "Elemental Attacker", + SPECIES_PERK_DESC = "[plural_form] deal [attack_type] damage with their punches instead of brute.", + )) + + return to_add + +/** + * Adds adds any perks related to sustaining damage. + * For example, brute damage vulnerability, or fire damage resistance. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_damage_perks() + var/list/to_add = list() + + // Brute related + if(brutemod > 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "band-aid", + SPECIES_PERK_NAME = "Brutal Weakness", + SPECIES_PERK_DESC = "[plural_form] are weak to brute damage.", + )) + + if(brutemod < 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "shield-alt", + SPECIES_PERK_NAME = "Brutal Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to bruising and brute damage.", + )) + + // Burn related + if(burnmod > 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "burn", + SPECIES_PERK_NAME = "Fire Weakness", + SPECIES_PERK_DESC = "[plural_form] are weak to fire and burn damage.", + )) + + if(burnmod < 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "shield-alt", + SPECIES_PERK_NAME = "Fire Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to flames, and burn damage.", + )) + + // Shock damage + if(siemens_coeff > 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "bolt", + SPECIES_PERK_NAME = "Shock Vulnerability", + SPECIES_PERK_DESC = "[plural_form] are vulnerable to being shocked.", + )) + + if(siemens_coeff < 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "shield-alt", + SPECIES_PERK_NAME = "Shock Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to being shocked.", + )) + + return to_add + +/** + * Adds adds any perks related to how the species deals with temperature. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_temperature_perks() + var/list/to_add = list() + + // Hot temperature tolerance + if(heatmod > 1/* || bodytemp_heat_damage_limit < BODYTEMP_HEAT_DAMAGE_LIMIT*/) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "temperature-high", + SPECIES_PERK_NAME = "Heat Vulnerability", + SPECIES_PERK_DESC = "[plural_form] are vulnerable to high temperatures.", + )) + + if(heatmod < 1/* || bodytemp_heat_damage_limit > BODYTEMP_HEAT_DAMAGE_LIMIT*/) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "thermometer-empty", + SPECIES_PERK_NAME = "Heat Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to hotter environments.", + )) + + // Cold temperature tolerance + if(coldmod > 1/* || bodytemp_cold_damage_limit > BODYTEMP_COLD_DAMAGE_LIMIT*/) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "temperature-low", + SPECIES_PERK_NAME = "Cold Vulnerability", + SPECIES_PERK_DESC = "[plural_form] are vulnerable to cold temperatures.", + )) + + if(coldmod < 1/* || bodytemp_cold_damage_limit < BODYTEMP_COLD_DAMAGE_LIMIT*/) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "thermometer-empty", + SPECIES_PERK_NAME = "Cold Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to colder environments.", + )) + + return to_add + +/** + * Adds adds any perks related to the species' blood (or lack thereof). + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_blood_perks() + var/list/to_add = list() + + // NOBLOOD takes priority by default + if(NOBLOOD in species_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "tint-slash", + SPECIES_PERK_NAME = "Bloodletted", + SPECIES_PERK_DESC = "[plural_form] do not have blood.", + )) + + // Otherwise, check if their exotic blood is a valid typepath + else if(ispath(exotic_blood)) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "tint", + SPECIES_PERK_NAME = initial(exotic_blood.name), + SPECIES_PERK_DESC = "[name] blood is [initial(exotic_blood.name)], which can make recieving medical treatment harder.", + )) + + // Otherwise otherwise, see if they have an exotic bloodtype set + else if(exotic_bloodtype) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "tint", + SPECIES_PERK_NAME = "Exotic Blood", + SPECIES_PERK_DESC = "[plural_form] have \"[exotic_bloodtype]\" type blood, which can make recieving medical treatment harder.", + )) + + return to_add + +/** + * Adds adds any perks related to the species' inherent_traits list. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_traits_perks() + var/list/to_add = list() + + if(TRAIT_LIMBATTACHMENT in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "user-plus", + SPECIES_PERK_NAME = "Limbs Easily Reattached", + SPECIES_PERK_DESC = "[plural_form] limbs are easily readded, and as such do not \ + require surgery to restore. Simply pick it up and pop it back in, champ!", + )) + + if(TRAIT_EASYDISMEMBER in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "user-times", + SPECIES_PERK_NAME = "Limbs Easily Dismembered", + SPECIES_PERK_DESC = "[plural_form] limbs are not secured well, and as such they are easily dismembered.", + )) + + if(TRAIT_EASILY_WOUNDED in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "user-times", + SPECIES_PERK_NAME = "Easily Wounded", + SPECIES_PERK_DESC = "[plural_form] skin is very weak and fragile. They are much easier to apply serious wounds to.", + )) + + if(TRAIT_TOXINLOVER in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "syringe", + SPECIES_PERK_NAME = "Toxins Lover", + SPECIES_PERK_DESC = "Toxins damage dealt to [plural_form] are reversed - healing toxins will instead cause harm, and \ + causing toxins will instead cause healing. Be careful around purging chemicals!", + )) + + return to_add + +/** + * Adds adds any perks related to the species' inherent_biotypes flags. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_biotypes_perks() + var/list/to_add = list() + + if(MOB_UNDEAD in inherent_biotypes) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "skull", + SPECIES_PERK_NAME = "Undead", + SPECIES_PERK_DESC = "[plural_form] are of the undead! The undead do not have the need to eat or breathe, and \ + most viruses will not be able to infect a walking corpse. Their worries mostly stop at remaining in one piece, really.", + )) + + return to_add + +/** + * Adds in a language perk based on all the languages the species + * can speak by default (according to their language holder). + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_language_perk() + var/list/to_add = list() + + // Grab galactic common as a path, for comparisons + var/datum/language/common_language = /datum/language/common + + // Now let's find all the languages they can speak that aren't common + var/list/bonus_languages = list() + var/datum/language_holder/temp_holder = new species_language_holder() + for(var/datum/language/language_type as anything in temp_holder.spoken_languages) + if(ispath(language_type, common_language)) + continue + bonus_languages += initial(language_type.name) + + // If we have any languages we can speak: create a perk for them all + if(length(bonus_languages)) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "comment", + SPECIES_PERK_NAME = "Native Speaker", + SPECIES_PERK_DESC = "Alongside [initial(common_language.name)], [plural_form] gain the ability to speak [english_list(bonus_languages)].", + )) + + qdel(temp_holder) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/IPC.dm b/code/modules/mob/living/carbon/human/species_types/IPC.dm index 62eda7a37c9b..5a761f5cb804 100644 --- a/code/modules/mob/living/carbon/human/species_types/IPC.dm +++ b/code/modules/mob/living/carbon/human/species_types/IPC.dm @@ -96,6 +96,30 @@ datum/species/ipc/on_species_loss(mob/living/carbon/C) C.dna.features["ipc_screen"] = null //Turns off screen on death C.update_body() +/datum/species/ipc/get_species_description() + return /*"IPCs, or Integrated Posibrain Chassis, are a series of constructed bipedal humanoids which vaguely represent humans in their figure. \ + IPCs were made by several human corporations after the second generation of cyborg units was created."*/ + +/datum/species/ipc/get_species_lore() + return list("TBD",/* + "The development and creation of IPCs was a natural occurrence after Sol Interplanetary Coalition explorers, flying a Martian flag, uncovered MMI technology in 2419. \ + It was massively hoped by scientists, explorers, and opportunists that this discovery would lead to a breakthrough in humanity’s ability to access and understand much of the derelict technology left behind." + */) + +/datum/species/ipc/create_pref_unique_perks() + var/list/to_add = list() + + // TODO + + return to_add + +/datum/species/ipc/create_pref_biotypes_perks() + var/list/to_add = list() + + // TODO + + return to_add + /datum/action/innate/change_screen name = "Change Display" check_flags = AB_CHECK_CONSCIOUS diff --git a/code/modules/mob/living/carbon/human/species_types/dullahan.dm b/code/modules/mob/living/carbon/human/species_types/dullahan.dm index 4bc1a41367db..44debbb969c5 100644 --- a/code/modules/mob/living/carbon/human/species_types/dullahan.dm +++ b/code/modules/mob/living/carbon/human/species_types/dullahan.dm @@ -62,6 +62,49 @@ else H.reset_perspective(myhead) +/datum/species/dullahan/get_species_description() + return "An angry spirit, hanging onto the land of the living for \ + unfinished business. Or that's what the books say. They're quite nice \ + when you get to know them." + +/datum/species/dullahan/get_species_lore() + return list( + "\"No wonder they're all so grumpy! Their hands are always full! I used to think, \ + \"Wouldn't this be cool?\" but after watching these creatures suffer from their head \ + getting dunked down disposals for the nth time, I think I'm good.\" - Captain Larry Dodd" + ) + +/datum/species/dullahan/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "horse-head", + SPECIES_PERK_NAME = "Headless and Horseless", + SPECIES_PERK_DESC = "Dullahans must lug their head around in their arms. While \ + many creative uses can come out of your head being independent of your \ + body, Dullahans will find it mostly a pain.", + )) + + return to_add + +// There isn't a "Minor Undead" biotype, so we have to explain it in an override (see: vampires) +/datum/species/dullahan/create_pref_biotypes_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "skull", + SPECIES_PERK_NAME = "Minor Undead", + SPECIES_PERK_DESC = "[name] are minor undead. \ + Minor undead enjoy some of the perks of being dead, like \ + not needing to breathe or eat, but do not get many of the \ + environmental immunities involved with being fully undead.", + )) + + return to_add + + /obj/item/organ/brain/dullahan decoy_override = TRUE organ_flags = 0 diff --git a/code/modules/mob/living/carbon/human/species_types/ethereal.dm b/code/modules/mob/living/carbon/human/species_types/ethereal.dm index e2ef4a778655..98b45f988178 100644 --- a/code/modules/mob/living/carbon/human/species_types/ethereal.dm +++ b/code/modules/mob/living/carbon/human/species_types/ethereal.dm @@ -34,6 +34,7 @@ hair_color = "fixedmutcolor" hair_alpha = 140 swimming_component = /datum/component/swimming/ethereal + var/current_color var/EMPeffect = FALSE var/emageffect = FALSE @@ -60,6 +61,7 @@ . = ..() if(!ishuman(C)) return + var/mob/living/carbon/human/ethereal = C default_color = "#[ethereal.dna.features["ethcolor"]]" r1 = GETREDPART(default_color) @@ -196,3 +198,48 @@ if(istype(stomach)) return stomach.crystal_charge return ETHEREAL_CHARGE_NONE + +/datum/species/ethereal/get_features() + var/list/features = ..() + + features += "feature_ethcolor" + + return features + +/datum/species/ethereal/get_species_description() + return /*"Coming from the planet of Sprout, the theocratic ethereals are \ + separated socially by caste, and espouse a dogma of aiding the weak and \ + downtrodden."*/ + +/datum/species/ethereal/get_species_lore() + return list("TBD",/* + "Ethereals are a species native to the planet Sprout. \ + When they were originally discovered, they were at a medieval level of technological progression, \ + but due to their natural acclimation with electricity, they felt easy among the large NanoTrasen installations.", + */) + +/datum/species/ethereal/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bolt", + SPECIES_PERK_NAME = "Shockingly Tasty", + SPECIES_PERK_DESC = "Ethereals can feed on electricity from APCs, and do not otherwise need to eat.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "lightbulb", + SPECIES_PERK_NAME = "Disco Ball", + SPECIES_PERK_DESC = "Ethereals passively generate their own light.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "biohazard", + SPECIES_PERK_NAME = "Starving Artist", + SPECIES_PERK_DESC = "Ethereals take toxin damage while starving.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/felinid.dm b/code/modules/mob/living/carbon/human/species_types/felinid.dm index 276e05f02576..ae52c1e5b82c 100644 --- a/code/modules/mob/living/carbon/human/species_types/felinid.dm +++ b/code/modules/mob/living/carbon/human/species_types/felinid.dm @@ -171,7 +171,7 @@ /datum/species/human/felinid/spec_life(mob/living/carbon/human/H) . = ..() - if((H.client && H.client.prefs.mood_tail_wagging) && !is_wagging_tail() && H.mood_enabled) + if((H.client && H.client.prefs.read_preference(/datum/preference/toggle/mood_tail_wagging)) && !is_wagging_tail() && H.mood_enabled) var/datum/component/mood/mood = H.GetComponent(/datum/component/mood) if(!istype(mood) || !(mood.shown_mood >= MOOD_LEVEL_HAPPY2)) return @@ -193,3 +193,32 @@ H.emote("wag") if(-1) stop_wagging_tail(H) + +/datum/species/human/felinid/prepare_human_for_preview(mob/living/carbon/human/human) + human.hair_style = "Hime Cut" + human.hair_color = "fcc" // pink + human.update_hair() + + var/obj/item/organ/ears/cat/cat_ears = human.getorgan(/obj/item/organ/ears/cat) + if (cat_ears) + cat_ears.color = human.hair_color + human.update_body() + +/datum/species/human/felinid/get_species_description() + return "Felinids are one of the many types of bespoke genetic \ + modifications to come of humanity's mastery of genetic science, and are \ + also one of the most common. Meow?" + +/datum/species/human/felinid/get_species_lore() + return list( + "Bio-engineering at its felinest, Felinids are the peak example of humanity's mastery of genetic code. \ + One of many \"Animalid\" variants, Felinids are the most popular and common, as well as one of the \ + biggest points of contention in genetic-modification.", + + "Body modders were eager to splice human and feline DNA in search of the holy trifecta: ears, eyes, and tail. \ + These traits were in high demand, with the corresponding side effects of vocal and neurochemical changes being seen as a minor inconvenience.", + + "Sadly for the Felinids, they were not minor inconveniences. Shunned as subhuman and monstrous by many, Felinids (and other Animalids) \ + sought their greener pastures out in the colonies, cloistering in communities of their own kind. \ + As a result, outer Human space has a high Animalid population.", + ) diff --git a/code/modules/mob/living/carbon/human/species_types/flypeople.dm b/code/modules/mob/living/carbon/human/species_types/flypeople.dm index 4ccd49169019..100698775049 100644 --- a/code/modules/mob/living/carbon/human/species_types/flypeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/flypeople.dm @@ -1,5 +1,6 @@ /datum/species/fly name = "Flyperson" + plural_form = "Flypeople" id = "fly" say_mod = "buzzes" species_traits = list(NOEYESPRITES, HAS_FLESH, HAS_BONE) @@ -31,3 +32,51 @@ if(istype(weapon, /obj/item/melee/flyswatter)) return 29 //Flyswatters deal 30x damage to flypeople. return 0 + +/datum/species/fly/get_species_description() + return "With no official documentation or knowledge of the origin of \ + this species, they remain a mystery to most. Any and all rumours among \ + Nanotrasen staff regarding flypeople are often quickly silenced by high \ + ranking staff or officials." + +/datum/species/fly/get_species_lore() + return list( + "Flypeople are a curious species with a striking resemblance to the insect order of Diptera, \ + commonly known as flies. With no publically known origin, flypeople are rumored to be a side effect of bluespace travel, \ + despite statements from Nanotrasen officials.", + + "Little is known about the origins of this race, \ + however they posess the ability to communicate with giant spiders, originally discovered in the Australicus sector \ + and now a common occurence in black markets as a result of a breakthrough in syndicate bioweapon research.", + + "Flypeople are often feared or avoided among other species, their appearance often described as unclean or frightening in some cases, \ + and their eating habits even more so with an insufferable accent to top it off.", + ) + +/datum/species/fly/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "grin-tongue", + SPECIES_PERK_NAME = "Uncanny Digestive System", + SPECIES_PERK_DESC = "Flypeople regurgitate their stomach contents and drink it \ + off the floor to eat and drink with little care for taste, favoring gross foods.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "fist-raised", + SPECIES_PERK_NAME = "Insectoid Biology", + SPECIES_PERK_DESC = "Fly swatters will deal significantly higher amounts of damage to a Flyperson.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "briefcase-medical", + SPECIES_PERK_NAME = "Weird Organs", + SPECIES_PERK_DESC = "Flypeople take specialized medical knowledge to be \ + treated. Their organs are disfigured and organ manipulation can be interesting...", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/golems.dm b/code/modules/mob/living/carbon/human/species_types/golems.dm index ccf8569f0e58..d3f413cbdc0f 100644 --- a/code/modules/mob/living/carbon/human/species_types/golems.dm +++ b/code/modules/mob/living/carbon/human/species_types/golems.dm @@ -44,6 +44,21 @@ var/golem_name = "[prefix] [golem_surname]" return golem_name +/datum/species/golem/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "gem", + SPECIES_PERK_NAME = "Lithoid", + SPECIES_PERK_DESC = "Lithoids are creatures made out of elements instead of \ + blood and flesh. Because of this, they're generally stronger, slower, \ + and mostly immune to environmental dangers and dangers to their health, \ + such as viruses and dismemberment.", + )) + + return to_add + /datum/species/golem/random name = "Random Golem" changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC | RACE_SWAP | ERT_SPAWN @@ -1548,3 +1563,46 @@ /obj/item/melee/supermatter_sword/hand/Initialize(mapload,silent,synthetic) . = ..() ADD_TRAIT(src, TRAIT_NODROP, INNATE_TRAIT) + + +/datum/species/golem/cloth/get_species_description() + return "A wrapped up Mummy! They descend upon Space Station Thirteen every year to spook the crew! \"Return the slab!\"" + +/datum/species/golem/cloth/get_species_lore() + return list( + "Mummies are very self conscious. They're shaped weird, they walk slow, and worst of all, \ + they're considered the laziest halloween costume. But that's not even true, they say.", + + "Making a mummy costume may be easy, but making a CONVINCING mummy costume requires \ + things like proper fabric and purposeful staining to achieve the look. Which is FAR from easy. Gosh.", + ) + +// Calls parent, as Golems have a species-wide perk we care about. +/datum/species/golem/cloth/create_pref_unique_perks() + var/list/to_add = ..() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "recycle", + SPECIES_PERK_NAME = "Reformation", + SPECIES_PERK_DESC = "Mummies collapse into \ + a pile of bandages after they die. If left alone, they will reform back \ + into themselves. The bandages themselves are very vulnerable to fire.", + )) + + return to_add + +// Override to add a perk elaborating on just how dangerous fire is. +/datum/species/golem/cloth/create_pref_temperature_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "fire-alt", + SPECIES_PERK_NAME = "Incredibly Flammable", + SPECIES_PERK_DESC = "Mummies are made entirely of cloth, which makes them \ + very vulnerable to fire. They will not reform if they die while on \ + fire, and they will easily catch alight. If your bandages burn to ash, you're toast!", + )) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/humans.dm b/code/modules/mob/living/carbon/human/species_types/humans.dm index 069c9192552a..32f0823d2055 100644 --- a/code/modules/mob/living/carbon/human/species_types/humans.dm +++ b/code/modules/mob/living/carbon/human/species_types/humans.dm @@ -29,3 +29,54 @@ if(prob(1)) return 'sound/voice/human/wilhelm_scream.ogg' return pick(male_screams) + +/datum/species/human/prepare_human_for_preview(mob/living/carbon/human/human) + human.hair_style = "Business Hair" + human.hair_color = "b96" // brown + human.update_hair() + +/datum/species/human/get_species_description() + return /*"Humans are the dominant species in the known galaxy. \ + Their kind extend from old Earth to the edges of known space."*/ + +/datum/species/human/get_species_lore() + return list("TBD",/* + "These primate-descended creatures, originating from the mostly harmless Earth, \ + have long-since outgrown their home and semi-benign designation. \ + The space age has taken humans out of their solar system and into the galaxy-at-large.", + + "In traditional human fashion, this near-record pace from terra firma to the final frontier spat \ + in the face of other races they now shared a stage with. \ + This included the lizards - if anyone was offended by these upstarts, it was certainly lizardkind.", + + "Humanity never managed to find the kind of peace to fully unite under one banner like other species. \ + The pencil and paper pushing of the UN bureaucrat lives on in the mosaic that is TerraGov; \ + a composite of the nation-states that still live on in human society.", + + "The human spirit of opportunity and enterprise continues on in its peak form: \ + the hypercorporation. Acting outside of TerraGov's influence, literally and figuratively, \ + hypercorporations buy the senate votes they need and establish territory far past the Earth Government's reach. \ + In hypercorporation territory company policy is law, giving new meaning to \"employee termination\".", + */) + +/datum/species/human/create_pref_unique_perks() + var/list/to_add = list() + + if(CONFIG_GET(number/default_laws) == 0) // Default lawset is set to Asimov + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "robot", + SPECIES_PERK_NAME = "Asimov Superiority", + SPECIES_PERK_DESC = "The AI and their cyborgs are, by default, subservient only \ + to humans. As a human, silicons are required to both protect and obey you.", + )) + + if(CONFIG_GET(flag/enforce_human_authority)) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bullhorn", + SPECIES_PERK_NAME = "Chain of Command", + SPECIES_PERK_DESC = "Nanotrasen only recognizes humans for Captain and Head of Personel. In addition to this, humans get more than other species.", + )) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm index a48e4a53db86..71391ce6c4da 100644 --- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm @@ -1,6 +1,7 @@ /datum/species/jelly // Entirely alien beings that seem to be made entirely out of gel. They have three eyes and a skeleton visible within them. name = "Jellyperson" + plural_form = "Jellypeople" id = "jelly" default_color = "00FF90" say_mod = "chirps" @@ -67,6 +68,21 @@ qdel(consumed_limb) H.blood_volume += 20 +// Slimes have both NOBLOOD and an exotic bloodtype set, so they need to be handled uniquely here. +// They may not be roundstart but in the unlikely event they become one might as well not leave a glaring issue open. +/datum/species/jelly/create_pref_blood_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "tint", + SPECIES_PERK_NAME = "Jelly Blood", + SPECIES_PERK_DESC = "[plural_form] don't have blood, but instead have toxic [initial(exotic_blood.name)]! This means they will heal from toxin damage. \ + Jelly is extremely important, as losing it will cause you to lose limbs. Having low jelly will make medical treatment very difficult.", + )) + + return to_add + /datum/action/innate/regenerate_limbs name = "Regenerate Limbs" check_flags = AB_CHECK_CONSCIOUS @@ -112,6 +128,7 @@ /datum/species/jelly/slime name = "Slimeperson" + plural_form = "Slimepeople" id = "slime" default_color = "00FFFF" species_traits = list(MUTCOLORS,EYECOLOR,HAIR,FACEHAIR,NOBLOOD) @@ -384,6 +401,7 @@ /datum/species/jelly/luminescent name = "Luminescent" + plural_form = null id = "lum" say_mod = "says" var/glow_intensity = LUMINESCENT_DEFAULT_GLOW @@ -553,6 +571,7 @@ /datum/species/jelly/stargazer name = "Stargazer" + plural_form = null id = "stargazer" var/datum/action/innate/project_thought/project_thought var/datum/action/innate/link_minds/link_minds diff --git a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm index e99ced8f28b8..c71e102b3a0e 100644 --- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm @@ -1,6 +1,7 @@ /datum/species/lizard // Reptilian humanoids with scaled skin and tails. name = "Lizardperson" + plural_form = "Lizardfolk" id = "lizard" say_mod = "hisses" default_color = "00FF00" @@ -66,7 +67,7 @@ /datum/species/lizard/spec_life(mob/living/carbon/human/H) . = ..() - if((H.client && H.client.prefs.mood_tail_wagging) && !is_wagging_tail() && H.mood_enabled) + if((H.client && H.client.prefs.read_preference(/datum/preference/toggle/mood_tail_wagging)) && !is_wagging_tail() && H.mood_enabled) var/datum/component/mood/mood = H.GetComponent(/datum/component/mood) if(!istype(mood) || !(mood.shown_mood >= MOOD_LEVEL_HAPPY2)) return @@ -89,6 +90,46 @@ if(-1) stop_wagging_tail(H) +/datum/species/lizard/get_species_description() + return /*"The militaristic Lizardpeople hail originally from Tizira, but have grown \ + throughout their centuries in the stars to possess a large spacefaring \ + empire: though now they must contend with their younger, more \ + technologically advanced Human neighbours."*/ + +/datum/species/lizard/get_species_lore() + return list( + "TBD",/* + "The face of conspiracy theory was changed forever the day mankind met the lizards.", + + "Hailing from the arid world of Tizira, lizards were travelling the stars back when mankind was first discovering how neat trains could be. \ + However, much like the space-fable of the space-tortoise and space-hare, lizards have rejected their kin's motto of \"slow and steady\" \ + in favor of resting on their laurels and getting completely surpassed by 'bald apes', due in no small part to their lack of access to plasma.", + + "The history between lizards and humans has resulted in many conflicts that lizards ended on the losing side of, \ + with the finale being an explosive remodeling of their moon. Today's lizard-human relations are seeing the continuance of a record period of peace.", + + "Lizard culture is inherently militaristic, though the influence the military has on lizard culture \ + begins to lessen the further colonies lie from their homeworld - \ + with some distanced colonies finding themselves subsumed by the cultural practices of other species nearby.", + + "On their homeworld, lizards celebrate their 16th birthday by enrolling in a mandatory 5 year military tour of duty. \ + Roles range from combat to civil service and everything in between. As the old slogan goes: \"Your place will be found!\"", + */) + +// Override for the default temperature perks, so we can give our specific "cold blooded" perk. +/datum/species/lizard/create_pref_temperature_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "thermometer-empty", + SPECIES_PERK_NAME = "Cold-blooded", + SPECIES_PERK_DESC = "Lizardpeople have higher tolerance for hot temperatures, but lower \ + tolerance for cold temperatures. Additionally, they cannot self-regulate their body temperature - \ + they are as cold or as warm as the environment around them is. Stay warm!", + )) + + return to_add /* Lizard subspecies: ASHWALKERS diff --git a/code/modules/mob/living/carbon/human/species_types/mothmen.dm b/code/modules/mob/living/carbon/human/species_types/mothmen.dm index f3efed0db1d6..8d4dedb9c9ff 100644 --- a/code/modules/mob/living/carbon/human/species_types/mothmen.dm +++ b/code/modules/mob/living/carbon/human/species_types/mothmen.dm @@ -1,5 +1,6 @@ /datum/species/moth name = "Mothperson" + plural_form = "Mothpeople" id = "moth" say_mod = "flutters" default_color = "00FF00" @@ -71,3 +72,57 @@ var/datum/gas_mixture/current = H.loc.return_air() if(current && (current.return_pressure() >= ONE_ATMOSPHERE*0.85)) //as long as there's reasonable pressure and no gravity, flight is possible return TRUE + +/datum/species/moth/get_species_description() + return /*"Hailing from a planet that was lost long ago, the moths travel \ + the galaxy as a nomadic people aboard a colossal fleet of ships, seeking a new homeland."*/ + +/datum/species/moth/get_species_lore() + return list(/* + "Their homeworld lost to the ages, the moths live aboard the Grand Nomad Fleet. \ + Made up of what could be found, bartered, repaired, or stolen the armada is a colossal patchwork \ + built on a history of politely flagging travelers down and taking their things. Occasionally a moth \ + will decide to leave the fleet, usually to strike out for fortunes to send back home.", + + "Nomadic life produces a tight-knit culture, with moths valuing their friends, family, and vessels highly. \ + Moths are gregarious by nature and do best in communal spaces. This has served them well on the galactic stage, \ + maintaining a friendly and personable reputation even in the face of hostile encounters. \ + It seems that the galaxy has come to accept these former pirates.", + + "Surprisingly, living together in a giant fleet hasn't flattened variance in dialect and culture. \ + These differences are welcomed and encouraged within the fleet for the variety that they bring.", + */) + +/datum/species/moth/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "feather-alt", + SPECIES_PERK_NAME = "Precious Wings", + SPECIES_PERK_DESC = "Moths can fly in pressurized, zero-g environments and safely land short falls using their wings.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "tshirt", + SPECIES_PERK_NAME = "Meal Plan", + SPECIES_PERK_DESC = "Moths can eat clothes for nourishment.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "fire", + SPECIES_PERK_NAME = "Ablazed Wings", + SPECIES_PERK_DESC = "Moth wings are fragile, and can be easily burnt off.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "sun", + SPECIES_PERK_NAME = "Bright Lights", + SPECIES_PERK_DESC = "Moths need an extra layer of flash protection to protect \ + themselves, such as against security officers or when welding. Welding \ + masks will work.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/mushpeople.dm b/code/modules/mob/living/carbon/human/species_types/mushpeople.dm index 13761a2d60b5..3087a3c397e6 100644 --- a/code/modules/mob/living/carbon/human/species_types/mushpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/mushpeople.dm @@ -1,5 +1,6 @@ /datum/species/mush //mush mush codecuck name = "Mushroomperson" + plural_form = "Mushroompeople" id = "mush" mutant_bodyparts = list("caps") default_features = list("caps" = "Round") diff --git a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm index 412554b7c997..aa8a5e99dc6d 100644 --- a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm +++ b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm @@ -1,5 +1,6 @@ /datum/species/plasmaman name = "Plasmaman" + plural_form = "Plasmamen" id = "plasmaman" say_mod = "rattles" sexes = FALSE @@ -8,7 +9,8 @@ // plasmemes get hard to wound since they only need a severe bone wound to dismember, but unlike skellies, they can't pop their bones back into place. inherent_traits = list(TRAIT_RESISTCOLD,TRAIT_RADIMMUNE,TRAIT_GENELESS,TRAIT_NOHUNGER,TRAIT_CALCIUM_HEALER,TRAIT_ALWAYS_CLEAN,TRAIT_HARDLY_WOUNDED) inherent_biotypes = list(MOB_UNDEAD, MOB_HUMANOID) - default_features = list("plasmaman_helmet") + mutant_bodyparts = list("plasmaman_helmet") + default_features = list("plasmaman_helmet" = "None") mutantlungs = /obj/item/organ/lungs/plasmaman mutanttongue = /obj/item/organ/tongue/bone/plasmaman mutantliver = /obj/item/organ/liver/plasmaman @@ -22,7 +24,6 @@ payday_modifier = 1 //Former humans, employment restrictions arise from psychological and practical concerns; they won't be able to be some head positions, but they get human pay and NT can't weasel out of it breathid = "tox" damage_overlay_type = ""//let's not show bloody wounds or burns over bones. - var/internal_fire = FALSE //If the bones themselves are burning clothes won't help you much disliked_food = NONE liked_food = DAIRY changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC @@ -30,6 +31,9 @@ smells_like = "plasma-caked calcium" + /// If the bones themselves are burning clothes won't help you much + var/internal_fire = FALSE + /datum/species/plasmaman/spec_life(mob/living/carbon/human/H) var/datum/gas_mixture/environment = H.loc.return_air() var/atmos_sealed = FALSE @@ -175,8 +179,6 @@ var/obj/item/clothing/head/helmet/space/plasmaman/plasmeme_helmet = H.head plasmeme_helmet.set_design(H) - return 0 - /datum/species/plasmaman/random_name(gender,unique,lastname) if(unique) return random_unique_plasmaman_name() @@ -187,3 +189,80 @@ randname += " [lastname]" return randname + +/datum/species/plasmaman/get_species_description() + return /*"Found on the Icemoon of Freyja, plasmamen consist of colonial \ + fungal organisms which together form a sentient being. In human space, \ + they're usually attached to skeletons to afford a human touch."*/ + +/datum/species/plasmaman/get_species_lore() + return list( + "TBD",/* + "A confusing species, plasmamen are truly \"a fungus among us\". \ + What appears to be a singular being is actually a colony of millions of organisms \ + surrounding a found (or provided) skeletal structure.", + + "Originally discovered by NT when a researcher \ + fell into an open tank of liquid plasma, the previously unnoticed fungal colony overtook the body creating \ + the first \"true\" plasmaman. The process has since been streamlined via generous donations of convict corpses and plasmamen \ + have been deployed en masse throughout NT to bolster the workforce.", + + "New to the galactic stage, plasmamen are a blank slate. \ + Their appearance, generally regarded as \"ghoulish\", inspires a lot of apprehension in their crewmates. \ + It might be the whole \"flammable purple skeleton\" thing.", + + "The colonids that make up plasmamen require the plasma-rich atmosphere they evolved in. \ + Their psuedo-nervous system runs with externalized electrical impulses that immediately ignite their plasma-based bodies when oxygen is present.", + */) + +/datum/species/plasmaman/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "user-shield", + SPECIES_PERK_NAME = "Protected", + SPECIES_PERK_DESC = "Plasmamen are immune to radiation, poisons, and most diseases.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bone", + SPECIES_PERK_NAME = "Wound Resistance", + SPECIES_PERK_DESC = "Plasmamen have higher tolerance for damage that would wound others.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "wind", + SPECIES_PERK_NAME = "Plasma Healing", + SPECIES_PERK_DESC = "Plasmamen can heal wounds by consuming plasma.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "hard-hat", + SPECIES_PERK_NAME = "Protective Helmet", + SPECIES_PERK_DESC = "Plasmamen's helmets provide them shielding from the flashes of welding, as well as an inbuilt flashlight.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "fire", + SPECIES_PERK_NAME = "Living Torch", + SPECIES_PERK_DESC = "Plasmamen instantly ignite when their body makes contact with oxygen.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "wind", + SPECIES_PERK_NAME = "Plasma Breathing", + SPECIES_PERK_DESC = "Plasmamen must breathe plasma to survive. You receive a tank when you arrive.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "briefcase-medical", + SPECIES_PERK_NAME = "Complex Biology", + SPECIES_PERK_DESC = "Plasmamen take specialized medical knowledge to be \ + treated. Do not expect speedy revival, if you are lucky enough to get \ + one at all.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/podpeople.dm b/code/modules/mob/living/carbon/human/species_types/podpeople.dm index 8dd82d3d2d8e..4b1810dc2b71 100644 --- a/code/modules/mob/living/carbon/human/species_types/podpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/podpeople.dm @@ -7,6 +7,7 @@ DISREGUARD THIS FILE IF YOU'RE INTENDING TO CHANGE ASPECTS OF PLAYER CONTROLLED /datum/species/pod // A mutation caused by a human being ressurected in a revival pod. These regain health in light, and begin to wither in darkness. name = "Podperson" + plural_form = "Podpeople" id = "pod" default_color = "59CE00" species_traits = list(MUTCOLORS,EYECOLOR) diff --git a/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm b/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm index 2ea0e1fc6620..9447592a23e4 100644 --- a/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm +++ b/code/modules/mob/living/carbon/human/species_types/polysmorphs.dm @@ -52,3 +52,25 @@ .=..() if(C.physiology) C.physiology.armor.wound -= 10 + +/datum/species/polysmorph/get_species_description() + return ""//"TODO: This is polysmorph description" + +/datum/species/polysmorph/get_species_lore() + return list( + ""//"TODO: This is polysmorph lore" + ) + +/datum/species/polysmorph/create_pref_unique_perks() + var/list/to_add = list() + + // TODO + + return to_add + +/datum/species/polysmorph/create_pref_biotypes_perks() + var/list/to_add = list() + + // TODO + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm index eb9aaae2f961..0b68ce850ecc 100644 --- a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm @@ -4,6 +4,7 @@ /datum/species/shadow // Humans cursed to stay in the darkness, lest their life forces drain. They regain health in shadow and die in light. name = "???" + plural_form = "???" id = "shadow" sexes = FALSE ignored_by = list(/mob/living/simple_animal/hostile/faithless) @@ -30,8 +31,58 @@ return TRUE return ..() +/datum/species/shadow/get_species_description() + return "Victims of a long extinct space alien. Their flesh is a sickly \ + seethrough filament, their tangled insides in clear view. Their form \ + is a mockery of life, leaving them mostly unable to work with others under \ + normal circumstances." + +/datum/species/shadow/get_species_lore() + return list( + "Long ago, the Spinward Sector used to be inhabited by terrifying aliens aptly named \"Shadowlings\" \ + after their control over darkness, and tendancy to kidnap victims into the dark maintenance shafts. \ + Around 2558, the long campaign Nanotrasen waged against the space terrors ended with the full extinction of the Shadowlings.", + + "Victims of their kidnappings would become brainless thralls, and via surgery they could be freed from the Shadowling's control. \ + Those more unlucky would have their entire body transformed by the Shadowlings to better serve in kidnappings. \ + Unlike the brain tumors of lesser control, these greater thralls could not be reverted.", + + "With Shadowlings long gone, their will is their own again. But their bodies have not reverted, burning in exposure to light. \ + Nanotrasen has assured the victims that they are searching for a cure. No further information has been given, even years later. \ + Most shadowpeople now assume Nanotrasen has long since shelfed the project.", + ) + +/datum/species/shadow/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "moon", + SPECIES_PERK_NAME = "Shadowborn", + SPECIES_PERK_DESC = "Their skin blooms in the darkness. All kinds of damage, \ + no matter how extreme, will heal over time as long as there is no light.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "eye", + SPECIES_PERK_NAME = "Nightvision", + SPECIES_PERK_DESC = "Their eyes are adapted to the night, and can see in the dark with no problems.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "sun", + SPECIES_PERK_NAME = "Lightburn", + SPECIES_PERK_DESC = "Their flesh withers in the light. Any exposure to light is \ + incredibly painful for the shadowperson, charring their skin.", + ), + ) + + return to_add + /datum/species/shadow/nightmare name = "Nightmare" + plural_form = null id = "nightmare" limbs_id = "shadow" burnmod = 1.5 diff --git a/code/modules/mob/living/carbon/human/species_types/skeletons.dm b/code/modules/mob/living/carbon/human/species_types/skeletons.dm index 85cd0cce1b33..6bf2eb266c27 100644 --- a/code/modules/mob/living/carbon/human/species_types/skeletons.dm +++ b/code/modules/mob/living/carbon/human/species_types/skeletons.dm @@ -28,3 +28,15 @@ if(SSevents.holidays && SSevents.holidays[HALLOWEEN]) return TRUE return ..() + +/datum/species/skeleton/get_species_description() + return "A rattling skeleton! They descend upon Space Station 13 \ + Every year to spook the crew! \"I've got a BONE to pick with you!\"" + +/datum/species/skeleton/get_species_lore() + return list( + "Skeletons want to be feared again! Their presence in media has been destroyed, \ + or at least that's what they firmly believe. They're always the first thing fought in an RPG, \ + they're Flanderized into pun rolling JOKES, and it's really starting to get to them. \ + You could say they're deeply RATTLED. Hah." + ) diff --git a/code/modules/mob/living/carbon/human/species_types/snail.dm b/code/modules/mob/living/carbon/human/species_types/snail.dm index c53f045b9e7c..af55a09bcdb0 100644 --- a/code/modules/mob/living/carbon/human/species_types/snail.dm +++ b/code/modules/mob/living/carbon/human/species_types/snail.dm @@ -1,5 +1,6 @@ /datum/species/snail name = "Snailperson" + plural_form = "Snailpeople" id = "snail" offset_features = list(OFFSET_UNIFORM = list(0,0), OFFSET_ID = list(0,0), OFFSET_GLOVES = list(0,0), OFFSET_GLASSES = list(0,4), OFFSET_EARS = list(0,0), OFFSET_SHOES = list(0,0), OFFSET_S_STORE = list(0,0), OFFSET_FACEMASK = list(0,0), OFFSET_HEAD = list(0,0), OFFSET_FACE = list(0,0), OFFSET_BELT = list(0,0), OFFSET_BACK = list(0,0), OFFSET_SUIT = list(0,0), OFFSET_NECK = list(0,0)) default_color = "336600" //vomit green diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm index a16bad18939e..5a5f6c407ba5 100644 --- a/code/modules/mob/living/carbon/human/species_types/vampire.dm +++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm @@ -60,8 +60,80 @@ /datum/species/vampire/check_species_weakness(obj/item/weapon, mob/living/attacker) if(istype(weapon, /obj/item/nullrod/whip)) - return 1 //Whips deal 2x damage to vampires. Vampire killer. - return 0 + return TRUE //Whips deal 2x damage to vampires. Vampire killer. + + return FALSE + +/datum/species/vampire/get_species_description() + return "A classy Vampire! They descend upon Space Station Thirteen Every year to spook the crew! \"Bleeg!!\"" + +/datum/species/vampire/get_species_lore() + return list( + "Vampires are unholy beings blessed and cursed with The Thirst. \ + The Thirst requires them to feast on blood to stay alive, and in return it gives them many bonuses. \ + Because of this, Vampires have split into two clans, one that embraces their powers as a blessing and one that rejects it.", + ) + +/datum/species/vampire/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bed", + SPECIES_PERK_NAME = "Coffin Brooding", + SPECIES_PERK_DESC = "Vampires can delay The Thirst and heal by resting in a coffin. So THAT'S why they do that!", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "book-dead", + SPECIES_PERK_NAME = "Vampire Clans", + SPECIES_PERK_DESC = "Vampires belong to one of two clans - the Inoculated, and the Outcast. The Outcast \ + don't follow many vampiric traditions, while the Inoculated are given unique names and flavor.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "cross", + SPECIES_PERK_NAME = "Against God and Nature", + SPECIES_PERK_DESC = "Almost all higher powers are disgusted by the existence of \ + Vampires, and entering the Chapel is essentially suicide. Do not do it!", + ), + ) + + return to_add + +// Vampire blood is special, so it needs to be handled with its own entry. +/datum/species/vampire/create_pref_blood_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "tint", + SPECIES_PERK_NAME = "The Thirst", + SPECIES_PERK_DESC = "In place of eating, Vampires suffer from The Thirst. \ + Thirst of what? Blood! Their tongue allows them to grab people and drink \ + their blood, and they will die if they run out. As a note, it doesn't \ + matter whose blood you drink, it will all be converted into your blood \ + type when consumed.", + )) + + return to_add + +// There isn't a "Minor Undead" biotype, so we have to explain it in an override (see: dullahans) +/datum/species/vampire/create_pref_biotypes_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "skull", + SPECIES_PERK_NAME = "Minor Undead", + SPECIES_PERK_DESC = "[name] are minor undead. \ + Minor undead enjoy some of the perks of being dead, like \ + not needing to breathe or eat, but do not get many of the \ + environmental immunities involved with being fully undead.", + )) + + return to_add /obj/item/organ/tongue/vampire name = "vampire tongue" diff --git a/code/modules/mob/living/carbon/human/species_types/zombies.dm b/code/modules/mob/living/carbon/human/species_types/zombies.dm index 15a89a60aa37..bc7ac579f7b3 100644 --- a/code/modules/mob/living/carbon/human/species_types/zombies.dm +++ b/code/modules/mob/living/carbon/human/species_types/zombies.dm @@ -21,6 +21,28 @@ return TRUE return ..() +/datum/species/zombie/get_species_description() + return "A rotting zombie! They descend upon Space Station Thirteen Every year to spook the crew! \"Sincerely, the Zombies!\"" + +/datum/species/zombie/get_species_lore() + return list("Zombies have long lasting beef with Botanists. Their last incident involving a lawn with defensive plants has left them very unhinged.") + +// Override for the default temperature perks, so we can establish that they don't care about temperature very much +/datum/species/zombie/create_pref_temperature_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "thermometer-half", + SPECIES_PERK_NAME = "No Body Temperature", + SPECIES_PERK_DESC = "Having long since departed, Zombies do not have anything \ + regulating their body temperature anymore. This means that \ + the environment decides their body temperature - which they don't mind at \ + all, until it gets a bit too hot.", + )) + + return to_add + /datum/species/zombie/infectious name = "Infectious Zombie" id = "memezombies" diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index baea19d42527..c17577de1d63 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -266,7 +266,7 @@ GLOBAL_LIST_INIT(special_radio_keys, list( deaf_message = span_notice("You can't hear yourself!") deaf_type = 2 // Since you should be able to hear yourself without looking // Create map text prior to modifying message for goonchat - if (client?.prefs.chat_on_map && stat != UNCONSCIOUS && (client.prefs.see_chat_non_mob || ismob(speaker)) && can_hear()) + if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && stat != UNCONSCIOUS && (client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker)) && can_hear()) create_chat_message(speaker, message_language, raw_message, spans) diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index dcf454152e6e..1da9dc9e8f35 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -166,7 +166,7 @@ create_eye() if(client) - apply_pref_name("ai",client) + INVOKE_ASYNC(src, .proc/apply_pref_name, /datum/preference/name/ai, client) INVOKE_ASYNC(src, .proc/set_core_display_icon) @@ -243,13 +243,13 @@ /mob/living/silicon/ai/proc/set_core_display_icon(input, client/C) if(client && !C) C = client - if(!input && !C?.prefs?.preferred_ai_core_display) + if(!input && !C?.prefs?.read_preference(/datum/preference/choiced/ai_core_display)) for (var/each in GLOB.ai_core_displays) //change status of displays var/obj/machinery/status_display/ai_core/M = each M.set_ai(initial(icon_state)) M.update() else - var/preferred_icon = input ? input : C.prefs.preferred_ai_core_display + var/preferred_icon = input ? input : C.prefs.read_preference(/datum/preference/choiced/ai_core_display) icon = initial(icon) //yogs for (var/each in GLOB.ai_core_displays) //change status of displays @@ -974,7 +974,7 @@ jobpart = "Unknown" var/rendered = "[start][hrefpart][namepart] ([jobpart]) [span_message("[treated_message]")]" - if (client?.prefs.chat_on_map && (client.prefs.see_chat_non_mob || ismob(speaker))) + if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && (client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker))) create_chat_message(speaker, message_language, raw_message, spans) show_message(rendered, 2) diff --git a/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm b/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm index 9700d463b499..a38a47131fb6 100644 --- a/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm +++ b/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm @@ -34,7 +34,7 @@ /datum/portrait_picker/ui_data(mob/user) var/list/data = list() - data["public_paintings"] = SSpersistence.paintings["public"] ? SSpersistence.paintings["public"] : 0 + data["public_paintings"] = SSpersistent_paintings.paintings["public"] ? SSpersistent_paintings.paintings["public"] : 0 return data /datum/portrait_picker/ui_act(action, params) @@ -45,7 +45,7 @@ if("select") var/list/tab2key = list(TAB_PUBLIC = "public") var/folder = tab2key[params["tab"]] - var/list/current_list = SSpersistence.paintings[folder] + var/list/current_list = SSpersistent_paintings.paintings[folder] var/list/chosen_portrait = current_list[params["selected"]] var/png = "data/paintings/[folder]/[chosen_portrait["md5"]].png" var/icon/portrait_icon = new(png) diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 0749c123207d..23e71385c2e1 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -261,9 +261,9 @@ var/changed_name = "" if(custom_name) changed_name = custom_name - if(changed_name == "" && C && C.prefs.custom_names["cyborg"] != DEFAULT_CYBORG_NAME) - if(apply_pref_name("cyborg", C)) - return //built in camera handled in proc + if(changed_name == "" && C && C.prefs.read_preference(/datum/preference/name/cyborg) != DEFAULT_CYBORG_NAME) + apply_pref_name(/datum/preference/name/cyborg, C) + return if(!changed_name) changed_name = get_standard_name() diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm index 145259aa7c52..d6931bfc28a0 100644 --- a/code/modules/mob/living/silicon/silicon.dm +++ b/code/modules/mob/living/silicon/silicon.dm @@ -471,7 +471,7 @@ var/datum/mind/mega = usr.mind if(!istype(mega)) return - var/aksent = input(usr, "Choose your accent:","Available Accents") as null|anything in (assoc_list_strip_value(strings("accents.json", "accent_file_names", directory = "strings/accents")) + "None") + var/aksent = input(usr, "Choose your accent:","Available Accents") as null|anything in (assoc_to_keys(strings("accents.json", "accent_file_names", directory = "strings/accents")) + "None") if(aksent) // Accents were an accidents why the fuck do I have to do mind.RegisterSignal(mob, COMSIG_MOB_SAY) if(aksent == "None") mega.accent_name = null diff --git a/code/modules/mob/living/simple_animal/hostile/zombie.dm b/code/modules/mob/living/simple_animal/hostile/zombie.dm index 3871aabee6f9..171935935021 100644 --- a/code/modules/mob/living/simple_animal/hostile/zombie.dm +++ b/code/modules/mob/living/simple_animal/hostile/zombie.dm @@ -30,21 +30,20 @@ setup_visuals() /mob/living/simple_animal/hostile/zombie/proc/setup_visuals() - var/datum/preferences/dummy_prefs = new - dummy_prefs.pref_species = new /datum/species/zombie - dummy_prefs.be_random_body = TRUE - var/datum/job/J = SSjob.GetJob(zombiejob) - var/datum/outfit/O - if(J.outfit) - O = new J.outfit - //They have claws now. - O.r_hand = null - O.l_hand = null - - var/icon/P = get_flat_human_icon("zombie_[zombiejob]", J , dummy_prefs, "zombie", outfit_override = O) - icon = P + var/datum/job/job = SSjob.GetJob(zombiejob) + + var/datum/outfit/outfit = new job.outfit + outfit.l_hand = null + outfit.r_hand = null + + var/mob/living/carbon/human/dummy/dummy = new + dummy.equipOutfit(outfit) + dummy.set_species(/datum/species/zombie) + icon = getFlatIcon(dummy) + qdel(dummy) + corpse = new(src) - corpse.outfit = O + corpse.outfit = outfit corpse.mob_species = /datum/species/zombie corpse.mob_name = name @@ -59,4 +58,4 @@ corpse.create() /mob/living/simple_animal/hostile/zombie/mostlyinfection //yogs 25% infection zombie - infection_chance = 25 \ No newline at end of file + infection_chance = 25 diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 2f40f21dcdba..670d303561d1 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -36,7 +36,7 @@ create_mob_hud() if(hud_used) hud_used.show_hud(hud_used.hud_version) - hud_used.update_ui_style(ui_style2icon(client.prefs.UI_style)) + hud_used.update_ui_style(ui_style2icon(client.prefs?.read_preference(/datum/preference/choiced/ui_style))) next_move = 1 @@ -73,7 +73,7 @@ update_client_colour() update_mouse_pointer() if(client) - client.change_view(getScreenSize(client.prefs.widescreenpref)) + client.change_view(getScreenSize(client.prefs.read_preference(/datum/preference/toggle/widescreen))) if(client.player_details.player_actions.len) for(var/datum/action/A in client.player_details.player_actions) A.Grant(src) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index ae507e2cf336..64eb236d53fb 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -255,16 +255,18 @@ ///Returns the client runechat visible messages preference according to the message type. /atom/proc/runechat_prefs_check(mob/target, visible_message_flags = NONE) - if(!target.client?.prefs.chat_on_map || !target.client.prefs.see_chat_non_mob) + if(!target.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)) return FALSE - if(visible_message_flags & EMOTE_MESSAGE && !target.client.prefs.see_rc_emotes) + if (!target.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs)) + return FALSE + if(visible_message_flags & EMOTE_MESSAGE && !target.client.prefs.read_preference(/datum/preference/toggle/see_rc_emotes)) return FALSE return TRUE /mob/runechat_prefs_check(mob/target, visible_message_flags = NONE) - if(!target.client?.prefs.chat_on_map) + if(!target.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)) return FALSE - if(visible_message_flags & EMOTE_MESSAGE && !target.client.prefs.see_rc_emotes) + if(visible_message_flags & EMOTE_MESSAGE && !target.client.prefs.read_preference(/datum/preference/toggle/see_rc_emotes)) return FALSE return TRUE diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index d75b4b3c9bf4..28a437a7692f 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -411,8 +411,9 @@ if(source) var/atom/movable/screen/alert/notify_action/A = O.throw_alert("[REF(source)]_notify_action", /atom/movable/screen/alert/notify_action) if(A) - if(O.client.prefs && O.client.prefs.UI_style) - A.icon = ui_style2icon(O.client.prefs.UI_style) + var/ui_style = O.client?.prefs?.read_preference(/datum/preference/choiced/ui_style) + if(ui_style) + A.icon = ui_style2icon(ui_style) if (header) A.name = header A.desc = message diff --git a/code/modules/mob/mob_transformation_simple.dm b/code/modules/mob/mob_transformation_simple.dm index 1217b49a9e5a..84979ea01903 100644 --- a/code/modules/mob/mob_transformation_simple.dm +++ b/code/modules/mob/mob_transformation_simple.dm @@ -47,7 +47,7 @@ D.updateappearance(mutcolor_update=1, mutations_overlay_update=1) else if(ishuman(M)) var/mob/living/carbon/human/H = M - client.prefs.copy_to(H) + client.prefs.apply_prefs_to(H, TRUE) H.dna.update_dna_identity() if(mind && isliving(M)) diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm index 926e3c20d4b5..11f82fd7841e 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -390,7 +390,7 @@ if(preference_source) - apply_pref_name("ai",preference_source) + apply_pref_name(/datum/preference/name/ai, preference_source) qdel(src) diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index 92498ad82fae..3e13cf7a4590 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -102,13 +102,10 @@ /obj/item/modular_computer/Destroy() kill_program(forced = TRUE) STOP_PROCESSING(SSobj, src) - for(var/H in all_components) - var/obj/item/computer_hardware/CH = all_components[H] - if(CH.holder == src) - CH.on_remove(src) - CH.holder = null - all_components.Remove(CH.device_type) - qdel(CH) + for(var/port in all_components) + var/obj/item/computer_hardware/component = all_components[port] + qdel(component) + all_components.Cut() //Die demon die physical = null return ..() diff --git a/code/modules/modular_computers/computers/item/computer_components.dm b/code/modules/modular_computers/computers/item/computer_components.dm index b328e6701e27..8020167959a6 100644 --- a/code/modules/modular_computers/computers/item/computer_components.dm +++ b/code/modules/modular_computers/computers/item/computer_components.dm @@ -1,67 +1,71 @@ -/obj/item/modular_computer/proc/can_install_component(obj/item/computer_hardware/H, mob/living/user = null) - if(!H.can_install(src, user)) +/obj/item/modular_computer/proc/can_install_component(obj/item/computer_hardware/try_install, mob/living/user = null) + if(!try_install.can_install(src, user)) return FALSE - if(H.w_class > max_hardware_size) + if(try_install.w_class > max_hardware_size) to_chat(user, span_warning("This component is too large for \the [src]!")) return FALSE - if(H.expansion_hw) + if(try_install.expansion_hw) if(LAZYLEN(expansion_bays) >= max_bays) to_chat(user, "All of the computer's expansion bays are filled.") return FALSE - if(LAZYACCESS(expansion_bays, H.device_type)) - to_chat(user, "The computer immediately ejects /the [H] and flashes an error: \"Hardware Address Conflict\".") + if(LAZYACCESS(expansion_bays, try_install.device_type)) + to_chat(user, "The computer immediately ejects /the [try_install] and flashes an error: \"Hardware Address Conflict\".") return FALSE - if(all_components[H.device_type]) - to_chat(user, span_warning("This computer's hardware slot is already occupied by \the [all_components[H.device_type]].")) + if(all_components[try_install.device_type]) + to_chat(user, span_warning("This computer's hardware slot is already occupied by \the [all_components[try_install.device_type]].")) return FALSE return TRUE -// Installs component. -/obj/item/modular_computer/proc/install_component(obj/item/computer_hardware/H, mob/living/user = null) - if(!can_install_component(H, user)) +/// Installs component. +/obj/item/modular_computer/proc/install_component(obj/item/computer_hardware/install, mob/living/user = null) + if(!can_install_component(install, user)) return FALSE - if(user && !user.transferItemToLoc(H, src)) + if(user && !user.transferItemToLoc(install, src)) return FALSE - if(H.expansion_hw) - LAZYSET(expansion_bays, H.device_type, H) - all_components[H.device_type] = H + if(install.expansion_hw) + LAZYSET(expansion_bays, install.device_type, install) + all_components[install.device_type] = install - to_chat(user, span_notice("You install \the [H] into \the [src].")) - H.holder = src - H.forceMove(src) - H.on_install(src, user) + to_chat(user, span_notice("You install \the [install] into \the [src].")) + install.holder = src + install.forceMove(src) + install.on_install(src, user) -// Uninstalls component. -/obj/item/modular_computer/proc/uninstall_component(obj/item/computer_hardware/H, mob/living/user = null) - if(H.holder != src) // Not our component at all. +/// Uninstalls component. +/obj/item/modular_computer/proc/uninstall_component(obj/item/computer_hardware/yeet, mob/living/user = null) + if(yeet.holder != src) // Not our component at all. return FALSE - if(H.expansion_hw) - LAZYREMOVE(expansion_bays, H.device_type) - all_components.Remove(H.device_type) + to_chat(user, span_notice("You remove \the [yeet] from \the [src].")) - to_chat(user, span_notice("You remove \the [H] from \the [src].")) - - H.forceMove(get_turf(src)) - H.holder = null - H.on_remove(src, user) + yeet.forceMove(get_turf(src)) + forget_component(yeet) + yeet.on_remove(src, user) if(enabled && !use_power()) shutdown_computer() update_icon() return TRUE +/// This isn't the "uninstall fully" proc, it just makes the computer lose all its references to the component +/obj/item/modular_computer/proc/forget_component(obj/item/computer_hardware/wipe_memory) + if(wipe_memory.holder != src) + return FALSE + if(wipe_memory.expansion_hw) + LAZYREMOVE(expansion_bays, wipe_memory.device_type) + all_components.Remove(wipe_memory.device_type) + wipe_memory.holder = null -// Checks all hardware pieces to determine if name matches, if yes, returns the hardware piece, otherwise returns null +/// Checks all hardware pieces to determine if name matches, if yes, returns the hardware piece, otherwise returns null /obj/item/modular_computer/proc/find_hardware_by_name(name) for(var/i in all_components) - var/obj/O = all_components[i] - if(O.name == name) - return O + var/obj/component = all_components[i] + if(component.name == name) + return component return null diff --git a/code/modules/modular_computers/file_system/programs/portrait_printer.dm b/code/modules/modular_computers/file_system/programs/portrait_printer.dm index e898fffa7acc..912bf4030dcd 100644 --- a/code/modules/modular_computers/file_system/programs/portrait_printer.dm +++ b/code/modules/modular_computers/file_system/programs/portrait_printer.dm @@ -22,7 +22,7 @@ /datum/computer_file/program/portrait_printer/ui_data(mob/user) var/list/data = list() - data["public_paintings"] = SSpersistence.paintings["public"] ? SSpersistence.paintings["public"] : 0 + data["public_paintings"] = SSpersistent_paintings.paintings["public"] ? SSpersistent_paintings.paintings["public"] : 0 return data /datum/computer_file/program/portrait_printer/ui_assets(mob/user) @@ -50,7 +50,7 @@ //canvas printing! var/list/tab2key = list(TAB_PUBLIC = "public") var/folder = tab2key[params["tab"]] - var/list/current_list = SSpersistence.paintings[folder] + var/list/current_list = SSpersistent_paintings.paintings[folder] var/list/chosen_portrait = current_list[params["selected"]] var/author = chosen_portrait["author"] var/title = chosen_portrait["title"] diff --git a/code/modules/modular_computers/hardware/CPU.dm b/code/modules/modular_computers/hardware/CPU.dm index addc4fb20151..67e45480f604 100644 --- a/code/modules/modular_computers/hardware/CPU.dm +++ b/code/modules/modular_computers/hardware/CPU.dm @@ -13,8 +13,8 @@ var/single_purpose = FALSE // If you can switch to other programs or only use the initial program device_type = MC_CPU -/obj/item/computer_hardware/processor_unit/on_remove(obj/item/modular_computer/MC, mob/user) - MC.shutdown_computer() +/obj/item/computer_hardware/processor_unit/on_remove(obj/item/modular_computer/remove_from, mob/user) + remove_from.shutdown_computer() /obj/item/computer_hardware/processor_unit/small name = "microprocessor" diff --git a/code/modules/modular_computers/hardware/_hardware.dm b/code/modules/modular_computers/hardware/_hardware.dm index 29da37e09948..8cb9b430951b 100644 --- a/code/modules/modular_computers/hardware/_hardware.dm +++ b/code/modules/modular_computers/hardware/_hardware.dm @@ -9,17 +9,29 @@ var/obj/item/modular_computer/holder = null // Computer that holds this hardware, if any. - var/power_usage = 0 // If the hardware uses extra power, change this. - var/enabled = TRUE // If the hardware is turned off set this to 0. - var/critical = FALSE // Prevent disabling for important component, like the CPU. - var/can_install = TRUE // Prevents direct installation of removable media. - var/expansion_hw = FALSE // Hardware that fits into expansion bays. - var/removable = TRUE // Whether the hardware is removable or not. - var/damage = 0 // Current damage level - var/max_damage = 100 // Maximal damage level. - var/damage_malfunction = 20 // "Malfunction" threshold. When damage exceeds this value the hardware piece will semi-randomly fail and do !!FUN!! things - var/damage_failure = 50 // "Failure" threshold. When damage exceeds this value the hardware piece will not work at all. - var/malfunction_probability = 10// Chance of malfunction when the component is damaged + /// If the hardware uses extra power, change this. + var/power_usage = 0 + /// If the hardware is turned off set this to 0. + var/enabled = TRUE + /// Prevent disabling for important component, like the CPU. + var/critical = FALSE + /// Prevents direct installation of removable media. + var/can_install = TRUE + /// Hardware that fits into expansion bays. + var/expansion_hw = FALSE + /// Whether the hardware is removable or not. + var/removable = TRUE + /// Current damage level + var/damage = 0 + /// Maximal damage level. + var/max_damage = 100 + /// "Malfunction" threshold. When damage exceeds this value the hardware piece will semi-randomly fail and do !!FUN!! things + var/damage_malfunction = 20 + /// "Failure" threshold. When damage exceeds this value the hardware piece will not work at all. + var/damage_failure = 50 + /// Chance of malfunction when the component is damaged + var/malfunction_probability = 10 + /// What define is used to qualify this piece of hardware? Important for upgraded versions of the same hardware. var/device_type /obj/item/computer_hardware/New(var/obj/L) @@ -29,7 +41,7 @@ /obj/item/computer_hardware/Destroy() if(holder) - holder.uninstall_component(src) + holder.forget_component(src) return ..() @@ -56,11 +68,11 @@ to_chat(user, "******************************") return TRUE -// Called on multitool click, prints diagnostic information to the user. +/// Called on multitool click, prints diagnostic information to the user. /obj/item/computer_hardware/proc/diagnostics(var/mob/user) to_chat(user, "Hardware Integrity Test... (Corruption: [damage]/[max_damage]) [damage > damage_failure ? "FAIL" : damage > damage_malfunction ? "WARN" : "PASS"]") -// Handles damage checks +/// Handles damage checks /obj/item/computer_hardware/proc/check_functionality() if(!enabled) // Disabled. return FALSE @@ -83,22 +95,23 @@ else if(damage) . += span_notice("It seems to be slightly damaged.") -// Component-side compatibility check. -/obj/item/computer_hardware/proc/can_install(obj/item/modular_computer/M, mob/living/user = null) +/// Component-side compatibility check. +/obj/item/computer_hardware/proc/can_install(obj/item/modular_computer/install_into, mob/living/user = null) return can_install -// Called when component is installed into PC. -/obj/item/computer_hardware/proc/on_install(obj/item/modular_computer/M, mob/living/user = null) +/// Called when component is installed into PC. +/obj/item/computer_hardware/proc/on_install(obj/item/modular_computer/install_into, mob/living/user = null) return -// Called when component is removed from PC. -/obj/item/computer_hardware/proc/on_remove(obj/item/modular_computer/M, mob/living/user = null) - try_eject(forced = 1) +/// Called when component is removed from PC. +/obj/item/computer_hardware/proc/on_remove(obj/item/modular_computer/remove_from, mob/living/user) + if(remove_from.physical && !QDELETED(remove_from) && !QDELETED(src)) + try_eject(forced = TRUE) -// Called when someone tries to insert something in it - paper in printer, card in card reader, etc. +/// Called when someone tries to insert something in it - paper in printer, card in card reader, etc. /obj/item/computer_hardware/proc/try_insert(obj/item/I, mob/living/user = null) return FALSE -// Called when someone tries to eject something from it - card from card reader, etc. +/// Called when someone tries to eject something from it - card from card reader, etc. /obj/item/computer_hardware/proc/try_eject(slot=0, mob/living/user = null, forced = 0) return FALSE diff --git a/code/modules/modular_computers/hardware/ai_slot.dm b/code/modules/modular_computers/hardware/ai_slot.dm index 280bb7a69b70..15a458b8b503 100644 --- a/code/modules/modular_computers/hardware/ai_slot.dm +++ b/code/modules/modular_computers/hardware/ai_slot.dm @@ -7,13 +7,14 @@ device_type = MC_AI expansion_hw = TRUE - var/obj/item/aicard/stored_card = null + var/obj/item/aicard/stored_card var/locked = FALSE -/obj/item/computer_hardware/ai_slot/handle_atom_del(atom/A) +///What happens when the intellicard is removed (or deleted) from the module, through try_eject() or not. +/obj/item/computer_hardware/ai_slot/Exited(atom/A, atom/newloc) if(A == stored_card) - try_eject(0, null, TRUE) - . = ..() + stored_card = null + return ..() /obj/item/computer_hardware/ai_slot/examine(mob/user) . = ..() @@ -55,7 +56,6 @@ user.put_in_hands(stored_card) else stored_card.forceMove(drop_location()) - stored_card = null return TRUE return FALSE diff --git a/code/modules/modular_computers/hardware/battery_module.dm b/code/modules/modular_computers/hardware/battery_module.dm index f30e598a134b..2350a74ca83d 100644 --- a/code/modules/modular_computers/hardware/battery_module.dm +++ b/code/modules/modular_computers/hardware/battery_module.dm @@ -4,22 +4,26 @@ icon_state = "cell_con" critical = 1 malfunction_probability = 1 - var/obj/item/stock_parts/cell/battery = null + var/obj/item/stock_parts/cell/battery device_type = MC_CELL -/obj/item/computer_hardware/battery/New(loc, battery_type = null) +/obj/item/computer_hardware/battery/Initialize(mapload, battery_type) + . = ..() if(battery_type) battery = new battery_type(src) - ..() /obj/item/computer_hardware/battery/Destroy() - . = ..() - QDEL_NULL(battery) + if(battery) + QDEL_NULL(battery) + return ..() -/obj/item/computer_hardware/battery/handle_atom_del(atom/A) +///What happens when the battery is removed (or deleted) from the module, through try_eject() or not. +/obj/item/computer_hardware/battery/Exited(atom/A, atom/newloc) if(A == battery) - try_eject(forced = TRUE) - . = ..() + battery = null + if(holder?.enabled && !holder.use_power()) + holder.shutdown_computer() + return ..() /obj/item/computer_hardware/battery/try_insert(obj/item/I, mob/living/user = null) if(!holder) @@ -44,31 +48,20 @@ return TRUE - -/obj/item/computer_hardware/battery/try_eject(mob/living/user = null, forced = FALSE) +/obj/item/computer_hardware/battery/try_eject(mob/living/user, forced = FALSE) if(!battery) to_chat(user, span_warning("There is no power cell connected to \the [src].")) return FALSE else if(user) user.put_in_hands(battery) + to_chat(user, span_notice("You detach \the [battery] from \the [src].")) else battery.forceMove(drop_location()) - to_chat(user, span_notice("You detach \the [battery] from \the [src].")) - battery = null - - if(holder) - if(holder.enabled && !holder.use_power()) - holder.shutdown_computer() - + return TRUE - - - - - /obj/item/stock_parts/cell/computer name = "standard battery" desc = "A standard power cell, commonly seen in high-end portable microcomputers or low-end laptops." diff --git a/code/modules/modular_computers/hardware/card_slot.dm b/code/modules/modular_computers/hardware/card_slot.dm index fcef177fed03..f0d10f749525 100644 --- a/code/modules/modular_computers/hardware/card_slot.dm +++ b/code/modules/modular_computers/hardware/card_slot.dm @@ -6,16 +6,30 @@ w_class = WEIGHT_CLASS_TINY device_type = MC_CARD - var/obj/item/card/id/stored_card = null + var/obj/item/card/id/stored_card -/obj/item/computer_hardware/card_slot/handle_atom_del(atom/A) +///What happens when the ID card is removed (or deleted) from the module, through try_eject() or not. +/obj/item/computer_hardware/card_slot/Exited(atom/A, atom/newloc) if(A == stored_card) - try_eject(null, TRUE) - holder.update_label() - . = ..() + stored_card = null + if(holder) + if(holder.active_program) + holder.active_program.event_idremoved(0) + for(var/p in holder.idle_threads) + var/datum/computer_file/program/computer_program = p + computer_program.event_idremoved(1) + + holder.update_slot_icon() + + if(ishuman(holder.loc)) + var/mob/living/carbon/human/human_wearer = holder.loc + if(human_wearer.wear_id == holder) + human_wearer.sec_hud_set_ID() + return ..() /obj/item/computer_hardware/card_slot/Destroy() - try_eject() + if(stored_card) //If you didn't expect this behavior for some dumb reason, do something different instead of directly destroying the slot + QDEL_NULL(stored_card) return ..() /obj/item/computer_hardware/card_slot/GetAccess() @@ -76,19 +90,7 @@ user.put_in_hands(stored_card) else stored_card.forceMove(drop_location()) - stored_card = null - holder.update_label() - - if(holder) - if(holder.active_program) - holder.active_program.event_idremoved(0) - for(var/p in holder.idle_threads) - var/datum/computer_file/program/computer_program = p - computer_program.event_idremoved(1) - if(ishuman(user)) - var/mob/living/carbon/human/human_user = user - human_user.sec_hud_set_ID() to_chat(user, "You remove the card from \the [src].") playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, FALSE) return TRUE diff --git a/code/modules/modular_computers/hardware/hard_drive.dm b/code/modules/modular_computers/hardware/hard_drive.dm index b5a438785d19..7646bf7e6c82 100644 --- a/code/modules/modular_computers/hardware/hard_drive.dm +++ b/code/modules/modular_computers/hardware/hard_drive.dm @@ -10,8 +10,8 @@ var/used_capacity = 0 var/list/stored_files = list() // List of stored files on this drive. DO NOT MODIFY DIRECTLY! -/obj/item/computer_hardware/hard_drive/on_remove(obj/item/modular_computer/MC, mob/user) - MC.shutdown_computer() +/obj/item/computer_hardware/hard_drive/on_remove(obj/item/modular_computer/remove_from, mob/user) + remove_from.shutdown_computer() /obj/item/computer_hardware/hard_drive/proc/install_default_programs() store_file(new/datum/computer_file/program/computerconfig(src)) // Computer configuration utility, allows hardware control and displays more info than status bar diff --git a/code/modules/modular_computers/hardware/portable_disk.dm b/code/modules/modular_computers/hardware/portable_disk.dm index 5c0eb6706dd7..61220b49365d 100644 --- a/code/modules/modular_computers/hardware/portable_disk.dm +++ b/code/modules/modular_computers/hardware/portable_disk.dm @@ -12,7 +12,7 @@ . = ..() . += span_notice("Insert this disk into a modular computer and open the File Manager program to interact with it.") -/obj/item/computer_hardware/hard_drive/portable/on_remove(obj/item/modular_computer/MC, mob/user) +/obj/item/computer_hardware/hard_drive/portable/on_remove(obj/item/modular_computer/remove_from, mob/user) return //this is a floppy disk, let's not shut the computer down when it gets pulled out. /obj/item/computer_hardware/hard_drive/portable/install_default_programs() diff --git a/code/modules/modular_computers/hardware/recharger.dm b/code/modules/modular_computers/hardware/recharger.dm index 37a86e5b5947..9a2fe5e81b16 100644 --- a/code/modules/modular_computers/hardware/recharger.dm +++ b/code/modules/modular_computers/hardware/recharger.dm @@ -52,8 +52,8 @@ icon_state = "charger_wire" w_class = WEIGHT_CLASS_BULKY -/obj/item/computer_hardware/recharger/wired/can_install(obj/item/modular_computer/M, mob/living/user = null) - if(ismachinery(M.physical) && M.physical.anchored) +/obj/item/computer_hardware/recharger/wired/can_install(obj/item/modular_computer/install_into, mob/living/user = null) + if(ismachinery(install_into.physical) && install_into.physical.anchored) return ..() to_chat(user, span_warning("\The [src] is incompatible with portable computers!")) return FALSE diff --git a/code/modules/ninja/ninja_event.dm b/code/modules/ninja/ninja_event.dm index 88f67f2a54a2..6098693a310a 100644 --- a/code/modules/ninja/ninja_event.dm +++ b/code/modules/ninja/ninja_event.dm @@ -87,8 +87,9 @@ Contents: /proc/create_space_ninja(spawn_loc) var/mob/living/carbon/human/new_ninja = new(spawn_loc) - var/datum/preferences/A = new()//Randomize appearance for the ninja. - A.real_name = "[pick(GLOB.ninja_titles)] [pick(GLOB.ninja_names)]" - A.copy_to(new_ninja) + new_ninja.randomize_human_appearance(~(RANDOMIZE_NAME|RANDOMIZE_SPECIES)) + var/new_name = "[pick(GLOB.ninja_titles)] [pick(GLOB.ninja_names)]" + new_ninja.name = new_name + new_ninja.real_name = new_name new_ninja.dna.update_dna_identity() return new_ninja diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm index d1c2a5321c84..5e246cd7afb1 100644 --- a/code/modules/projectiles/projectile/magic.dm +++ b/code/modules/projectiles/projectile/magic.dm @@ -245,10 +245,8 @@ if(chooseable_races.len) new_mob.set_species(pick(chooseable_races)) - var/datum/preferences/A = new() //Randomize appearance for the human - A.copy_to(new_mob, icon_updates=0) - var/mob/living/carbon/human/H = new_mob + H.randomize_human_appearance(~(RANDOMIZE_SPECIES)) H.update_body() H.update_hair() H.update_body_parts() diff --git a/code/modules/reagents/chemistry/machinery/chem_master.dm b/code/modules/reagents/chemistry/machinery/chem_master.dm index 117b4ae50b2b..abf36adeb536 100644 --- a/code/modules/reagents/chemistry/machinery/chem_master.dm +++ b/code/modules/reagents/chemistry/machinery/chem_master.dm @@ -25,15 +25,6 @@ /obj/machinery/chem_master/Initialize() create_reagents(100) - //Calculate the span tags and ids fo all the available pill icons - var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/pills) - pillStyles = list() - for (var/x in 1 to PILL_STYLE_COUNT) - var/list/SL = list() - SL["id"] = x - SL["className"] = assets.icon_class_name("pill[x]") - pillStyles += list(SL) - . = ..() /obj/machinery/chem_master/Destroy() @@ -151,6 +142,16 @@ bottle = null return ..() +/obj/machinery/chem_master/proc/load_styles() + //Calculate the span tags and ids fo all the available pill icons + var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/pills) + pillStyles = list() + for (var/x in 1 to PILL_STYLE_COUNT) + var/list/SL = list() + SL["id"] = x + SL["className"] = assets.icon_class_name("pill[x]") + pillStyles += list(SL) + /obj/machinery/chem_master/ui_assets(mob/user) return list( get_asset_datum(/datum/asset/spritesheet/simple/pills), @@ -190,7 +191,9 @@ bufferContents.Add(list(list("name" = N.name, "id" = ckey(N.name), "volume" = N.volume))) // ^ data["bufferContents"] = bufferContents - //Calculated at init time as it never changes + //Calculated once since it'll never change + if(!pillStyles) + load_styles() data["pillStyles"] = pillStyles return data diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm index 7945eacf8843..5bf1cff4a57c 100644 --- a/code/modules/research/techweb/_techweb_node.dm +++ b/code/modules/research/techweb/_techweb_node.dm @@ -42,9 +42,9 @@ VARSET_TO_LIST(., display_name) VARSET_TO_LIST(., hidden) VARSET_TO_LIST(., starting_node) - VARSET_TO_LIST(., assoc_list_strip_value(prereq_ids)) - VARSET_TO_LIST(., assoc_list_strip_value(design_ids)) - VARSET_TO_LIST(., assoc_list_strip_value(unlock_ids)) + VARSET_TO_LIST(., assoc_to_keys(prereq_ids)) + VARSET_TO_LIST(., assoc_to_keys(design_ids)) + VARSET_TO_LIST(., assoc_to_keys(unlock_ids)) VARSET_TO_LIST(., boost_item_paths) VARSET_TO_LIST(., autounlock_by_boost) VARSET_TO_LIST(., research_costs) diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index fd3940052a51..0022f70a258b 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -733,6 +733,7 @@ SM.key = C.key SM.sentience_act() SM.mind.enslave_mind_to_creator(user) + SM.mind.add_antag_datum(/datum/antagonist/sentient_creature) to_chat(SM, span_warning("All at once it makes sense: you know what you are and who you are! Self awareness is yours!")) to_chat(SM, span_userdanger("You are grateful to be self aware and owe [user.real_name] a great debt. Serve [user.real_name], and assist [user.p_them()] in completing [user.p_their()] goals at any cost.")) if(SM.flags_1 & HOLOGRAM_1) //Check to see if it's a holodeck creature diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index d7b697deb7d7..b73842544b4b 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -1,4 +1,4 @@ -/** +/*! * Copyright (c) 2020 Aleksej Komarov * SPDX-License-Identifier: MIT */ @@ -54,7 +54,7 @@ */ /datum/tgui/New(mob/user, datum/src_object, interface, title, ui_x, ui_y) log_tgui(user, - "new [interface] fancy [user.client.prefs.tgui_fancy]", + "new [interface] fancy [user?.client?.prefs.read_preference(/datum/preference/toggle/tgui_fancy)]", src_object = src_object) src.user = user src.src_object = src_object @@ -62,11 +62,16 @@ src.interface = interface if(title) src.title = title - src.state = src_object.ui_state() + src.state = src_object.ui_state(user) // Deprecated if(ui_x && ui_y) src.window_size = list(ui_x, ui_y) +/datum/tgui/Destroy() + user = null + src_object = null + return ..() + /** * public * @@ -89,8 +94,8 @@ window.acquire_lock(src) if(!window.is_ready()) window.initialize( - fancy = user.client.prefs.tgui_fancy, - inline_assets = list( + fancy = user.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy), + assets = list( get_asset_datum(/datum/asset/simple/tgui), )) else @@ -224,8 +229,8 @@ "window" = list( "key" = window_key, "size" = window_size, - "fancy" = user.client.prefs.tgui_fancy, - "locked" = user.client.prefs.tgui_lock, + "fancy" = user.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy), + "locked" = user.client.prefs.read_preference(/datum/preference/toggle/tgui_lock), ), "client" = list( "ckey" = user.client.ckey, @@ -308,7 +313,7 @@ return FALSE switch(type) if("ready") - // Send a full update when the user manually refreshes the UI + // Send a full update when the user manually refreshes the UI if(initialized) send_full_update() initialized = TRUE diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm index cfc788768c97..18e87468325d 100644 --- a/code/modules/tgui/tgui_window.dm +++ b/code/modules/tgui/tgui_window.dm @@ -1,4 +1,4 @@ -/** +/*! * Copyright (c) 2020 Aleksej Komarov * SPDX-License-Identifier: MIT */ @@ -18,8 +18,11 @@ var/message_queue var/sent_assets = list() // Vars passed to initialize proc (and saved for later) - var/inline_assets - var/fancy + var/initial_fancy + var/initial_assets + var/initial_inline_html + var/initial_inline_js + var/initial_inline_css /** * public @@ -44,21 +47,26 @@ * state. You can begin sending messages right after initializing. Messages * will be put into the queue until the window finishes loading. * - * optional inline_assets list List of assets to inline into the html. + * optional assets list List of assets to inline into the html. * optional inline_html string Custom HTML to inject. * optional fancy bool If TRUE, will hide the window titlebar. */ /datum/tgui_window/proc/initialize( - inline_assets = list(), + fancy = FALSE, + assets = list(), inline_html = "", - fancy = FALSE) + inline_js = "", + inline_css = "") log_tgui(client, context = "[id]/initialize", window = src) if(!client) return - src.inline_assets = inline_assets - src.fancy = fancy + src.initial_fancy = fancy + src.initial_assets = assets + src.initial_inline_html = inline_html + src.initial_inline_js = inline_js + src.initial_inline_css = inline_css status = TGUI_WINDOW_LOADING fatally_errored = FALSE // Build window options @@ -71,9 +79,9 @@ // Generate page html var/html = SStgui.basehtml html = replacetextEx(html, "\[tgui:windowId]", id) - // Inject inline assets + // Inject assets var/inline_assets_str = "" - for(var/datum/asset/asset in inline_assets) + for(var/datum/asset/asset in assets) var/mappings = asset.get_url_mappings() for(var/name in mappings) var/url = mappings[name] @@ -86,8 +94,17 @@ if(length(inline_assets_str)) inline_assets_str = "\n" html = replacetextEx(html, "\n", inline_assets_str) - // Inject custom HTML - html = replacetextEx(html, "\n", inline_html) + // Inject inline HTML + if (inline_html) + html = replacetextEx(html, "", inline_html) + // Inject inline JS + if (inline_js) + inline_js = "" + html = replacetextEx(html, "", inline_js) + // Inject inline CSS + if (inline_css) + inline_css = "" + html = replacetextEx(html, "", inline_css) // Open the window client << browse(html, "window=[id];[options]") // Detect whether the control is a browser @@ -258,7 +275,7 @@ if(istype(asset, /datum/asset/spritesheet)) var/datum/asset/spritesheet/spritesheet = asset send_message("asset/stylesheet", spritesheet.css_filename()) - send_message("asset/mappings", asset.get_url_mappings()) + send_raw_message(asset.get_serialized_url_mappings()) /** * private @@ -317,7 +334,15 @@ client << link(href_list["url"]) if("cacheReloaded") // Reinitialize - initialize(inline_assets = inline_assets, fancy = fancy) + initialize( + fancy = initial_fancy, + assets = initial_assets, + inline_html = initial_inline_html, + inline_js = initial_inline_js, + inline_css = initial_inline_css) // Resend the assets for(var/asset in sent_assets) send_asset(asset) + +/datum/tgui_window/vv_edit_var(var_name, var_value) + return var_name != NAMEOF(src, id) && ..() diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm index a75ce59f0173..b98fbfbbdf82 100644 --- a/code/modules/tgui_panel/tgui_panel.dm +++ b/code/modules/tgui_panel/tgui_panel.dm @@ -1,4 +1,4 @@ -/** +/*! * Copyright (c) 2020 Aleksej Komarov * SPDX-License-Identifier: MIT */ @@ -12,7 +12,6 @@ var/datum/tgui_window/window var/broken = FALSE var/initialized_at - var/retries = 0 /datum/tgui_panel/New(client/client) src.client = client @@ -40,53 +39,27 @@ /datum/tgui_panel/proc/initialize(force = FALSE) set waitfor = FALSE // Minimal sleep to defer initialization to after client constructor - sleep(0.1 SECONDS) + sleep(1) initialized_at = world.time // Perform a clean initialization - window.initialize(inline_assets = list( + window.initialize(assets = list( get_asset_datum(/datum/asset/simple/tgui_panel), )) window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome)) window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/tgfont)) window.send_asset(get_asset_datum(/datum/asset/spritesheet/chat)) - // Preload assets for /datum/tgui - var/datum/asset/asset_tgui = get_asset_datum(/datum/asset/simple/tgui) - var/flush_queue = asset_tgui.send(src.client) - if(flush_queue) - src.client.browse_queue_flush() // Other setup request_telemetry() - if(!telemetry_connections && retries < 6) - addtimer(CALLBACK(src, .proc/check_telemetry), 2 SECONDS) - addtimer(CALLBACK(src, .proc/on_initialize_timed_out), 2 SECONDS) - -/datum/tgui_panel/proc/check_telemetry() - if(!telemetry_connections) /// Somethings fucked lets try again. - if(retries > 2) - if(client && istype(client)) - winset(client, null, "command=.reconnect") /// Kitchen Sink - qdel(client) - if(retries > 3) - qdel(client) - if(retries > 5) - return // I give up - if(retries < 6) - retries++ - src << browse(file('html/statbrowser.html'), "window=statbrowser") /// Reloads the statpanel as well - initialize() /// Lets just start again - var/mob/dead/new_player/M = client?.mob - if(istype(M)) - M.Login() + addtimer(CALLBACK(src, .proc/on_initialize_timed_out), 5 SECONDS) /** * private * * Called when initialization has timed out. */ - /datum/tgui_panel/proc/on_initialize_timed_out() // Currently does nothing but sending a message to old chat. - SEND_TEXT(client, span_userdanger("Failed to load fancy chat, click HERE to attempt to reload it.")) + SEND_TEXT(client, "Failed to load fancy chat, click HERE to attempt to reload it.") /** * private @@ -124,6 +97,3 @@ */ /datum/tgui_panel/proc/send_roundrestart() window.send_message("roundrestart") - -/datum/tgui_panel/proc/send_connected() - window.send_message("reconnected") diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm index 31e570cd9431..e610479084af 100644 --- a/code/modules/tooltip/tooltip.dm +++ b/code/modules/tooltip/tooltip.dm @@ -108,8 +108,9 @@ Notes: /proc/openToolTip(mob/user = null, atom/movable/tip_src = null, params = null,title = "",content = "",theme = "") if(istype(user)) if(user.client && user.client.tooltips) - if(!theme && user.client.prefs && user.client.prefs.UI_style) - theme = lowertext(user.client.prefs.UI_style) + var/ui_style = user.client?.prefs?.read_preference(/datum/preference/choiced/ui_style) + if(!theme && ui_style) + theme = lowertext(ui_style) if(!theme) theme = "default" user.client.tooltips.show(tip_src, params,title,content,theme) diff --git a/config/admins.txt b/config/admins.txt index f15744c4b491..1b2010bc989d 100644 --- a/config/admins.txt +++ b/config/admins.txt @@ -16,3 +16,4 @@ baiomu = Council Member ajhchenry = Council Member jamied12 = Head Developer +theos = Head Developer diff --git a/config/config.txt b/config/config.txt index 56b826017c56..c7d1b7fb6dee 100644 --- a/config/config.txt +++ b/config/config.txt @@ -454,3 +454,10 @@ MAX_SHUTTLE_SIZE 300 ## Enable/disable roundstart station traits #STATION_TRAITS + +## Assets can opt-in to caching their results into `tmp`. +## This is important, as preferences assets take upwards of 30 seconds (without sleeps) to collect. +## The cache is assumed to be cleared by TGS recompiling, which deletes `tmp`. +## This should be disabled (through `CACHE_ASSETS 0`) on development, +## but enabled on production (the default). +CACHE_ASSETS 1 diff --git a/html/changelogs/AutoChangelog-pr-17368.yml b/html/changelogs/AutoChangelog-pr-17368.yml new file mode 100644 index 000000000000..37b3d756b484 --- /dev/null +++ b/html/changelogs/AutoChangelog-pr-17368.yml @@ -0,0 +1,6 @@ +author: "Mothblocks & a lot of people from /tg/" +delete-after: true +changes: + - rscadd: "The preferences menu has been completely rewritten in tgui." + - tweak: "The \"Preferences\" tab has been removed, the menu verbs have been moved to the OOC tab" + - tweak: "The \"Stop Sounds\" verb has been moved to OOC." diff --git a/html/font-awesome/webfonts/fa-regular-400.eot b/html/font-awesome/webfonts/fa-regular-400.eot deleted file mode 100644 index d62be2fad885..000000000000 Binary files a/html/font-awesome/webfonts/fa-regular-400.eot and /dev/null differ diff --git a/html/font-awesome/webfonts/fa-regular-400.woff b/html/font-awesome/webfonts/fa-regular-400.woff deleted file mode 100644 index 43b1a9ae49db..000000000000 Binary files a/html/font-awesome/webfonts/fa-regular-400.woff and /dev/null differ diff --git a/html/font-awesome/webfonts/fa-solid-900.eot b/html/font-awesome/webfonts/fa-solid-900.eot deleted file mode 100644 index c77baa8d46ab..000000000000 Binary files a/html/font-awesome/webfonts/fa-solid-900.eot and /dev/null differ diff --git a/html/font-awesome/webfonts/fa-solid-900.woff b/html/font-awesome/webfonts/fa-solid-900.woff deleted file mode 100644 index 77c1786227f5..000000000000 Binary files a/html/font-awesome/webfonts/fa-solid-900.woff and /dev/null differ diff --git a/icons/Testing/greyscale_error.dmi b/icons/Testing/greyscale_error.dmi new file mode 100644 index 000000000000..6c781a70ad19 Binary files /dev/null and b/icons/Testing/greyscale_error.dmi differ diff --git a/icons/UI_Icons/antags/obsessed.dmi b/icons/UI_Icons/antags/obsessed.dmi new file mode 100644 index 000000000000..219a6e594132 Binary files /dev/null and b/icons/UI_Icons/antags/obsessed.dmi differ diff --git a/icons/blanks/32x32.dmi b/icons/blanks/32x32.dmi new file mode 100644 index 000000000000..6c4f2b33e0fe Binary files /dev/null and b/icons/blanks/32x32.dmi differ diff --git a/icons/blanks/96x96.dmi b/icons/blanks/96x96.dmi new file mode 100644 index 000000000000..d79e60c111ab Binary files /dev/null and b/icons/blanks/96x96.dmi differ diff --git a/icons/blanks/blank_title.png b/icons/blanks/blank_title.png new file mode 100644 index 000000000000..387a21f03a01 Binary files /dev/null and b/icons/blanks/blank_title.png differ diff --git a/icons/effects/64x64.dmi b/icons/effects/64x64.dmi index 6b0cf1efc98e..7f18c6166a3e 100644 Binary files a/icons/effects/64x64.dmi and b/icons/effects/64x64.dmi differ diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi index bcea2fb4e5a0..4b5ec0990bf2 100644 Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ diff --git a/icons/mob/hud.dmi b/icons/mob/hud.dmi index b56dfc2f3448..6f626fe09f85 100644 Binary files a/icons/mob/hud.dmi and b/icons/mob/hud.dmi differ diff --git a/icons/mob/human.dmi b/icons/mob/human.dmi index e0fe6b4d9889..63284a0012f2 100644 Binary files a/icons/mob/human.dmi and b/icons/mob/human.dmi differ diff --git a/interface/menu.dm b/interface/menu.dm index c670a58a5203..ec9719e82365 100644 --- a/interface/menu.dm +++ b/interface/menu.dm @@ -8,7 +8,6 @@ GLOBAL_LIST_EMPTY(menulist) /datum/verbs/menu - var/checkbox = CHECKBOX_NONE //checkbox type. var/default //default checked type. //Set to true to append our children to our parent, //Rather then add us as a node (used for having more then one checkgroups in the same menu) @@ -17,48 +16,4 @@ GLOBAL_LIST_EMPTY(menulist) return GLOB.menulist /datum/verbs/menu/HandleVerb(list/entry, verbpath, client/C) - var/datum/verbs/menu/verb_true_parent = GLOB.menulist[verblist[verbpath]] - var/true_checkbox = verb_true_parent.checkbox - if (true_checkbox != CHECKBOX_NONE) - var/checkedverb = verb_true_parent.Get_checked(C) - if (true_checkbox == CHECKBOX_GROUP) - if (verbpath == checkedverb) - entry["is-checked"] = TRUE - else - entry["is-checked"] = FALSE - else if (true_checkbox == CHECKBOX_TOGGLE) - entry["is-checked"] = checkedverb - - entry["command"] = ".updatemenuchecked \"[verb_true_parent.type]\" \"[verbpath]\"\n[entry["command"]]" - entry["can-check"] = TRUE - entry["group"] = "[verb_true_parent.type]" return list2params(entry) - -/datum/verbs/menu/proc/Get_checked(client/C) - return C.prefs.menuoptions[type] || default || FALSE - -/datum/verbs/menu/proc/Load_checked(client/C) //Loads the checked menu item into a new client. Used by icon menus to invoke the checked item. - return - -/datum/verbs/menu/proc/Set_checked(client/C, verbpath) - if (checkbox == CHECKBOX_GROUP) - C.prefs.menuoptions[type] = verbpath - C.prefs.save_preferences() - else if (checkbox == CHECKBOX_TOGGLE) - var/checked = Get_checked(C) - C.prefs.menuoptions[type] = !checked - C.prefs.save_preferences() - winset(C, "[verbpath]", "is-checked = [!checked]") - -/client/verb/updatemenuchecked(menutype as text, verbpath as text) - set name = ".updatemenuchecked" - menutype = text2path(menutype) - verbpath = text2path(verbpath) - if (!menutype || !verbpath) - return - var/datum/verbs/menu/M = GLOB.menulist[menutype] - if (!M) - return - if (!(verbpath in typesof("[menutype]/verb"))) - return - M.Set_checked(src, verbpath) \ No newline at end of file diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml index 97529e27e7fb..3cc709385bae 100644 --- a/tgui/.eslintrc.yml +++ b/tgui/.eslintrc.yml @@ -1,4 +1,5 @@ root: true +##extends: prettier parser: '@typescript-eslint/parser' parserOptions: ecmaVersion: 2020 @@ -12,6 +13,7 @@ env: plugins: - radar - react +# - unused-imports settings: react: version: '16.10' @@ -641,7 +643,7 @@ rules: ## Prevent usage of unsafe lifecycle methods react/no-unsafe: error ## Prevent definitions of unused prop types - react/no-unused-prop-types: error + ##react/no-unused-prop-types: error ## Prevent definitions of unused state properties react/no-unused-state: error ## Prevent usage of setState in componentWillUpdate @@ -761,3 +763,4 @@ rules: react/jsx-wrap-multilines: error ## Prevents the use of unused imports. ## This could be done by enabling no-unused-vars, but we're doing this for now + ##unused-imports/no-unused-imports: error diff --git a/tgui/.gitignore b/tgui/.gitignore index 4d0dd666d88e..bb34d61b3ce8 100644 --- a/tgui/.gitignore +++ b/tgui/.gitignore @@ -16,9 +16,8 @@ package-lock.json /public/.tmp/**/* /public/**/* !/public/*.html -!/public/tgui-polyfill.min.js +!/public/tgui-polyfill.bundle.js /coverage ## Previously ignored locations that are kept to avoid confusing git ## while transitioning to a new project structure. -/packages/tgui/public/** diff --git a/tgui/.yarnrc.yml b/tgui/.yarnrc.yml index 65f95873521c..b6387e8e46e8 100644 --- a/tgui/.yarnrc.yml +++ b/tgui/.yarnrc.yml @@ -1,3 +1,5 @@ +enableScripts: false + logFilters: - code: YN0004 level: discard @@ -8,6 +10,8 @@ plugins: - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs spec: "@yarnpkg/plugin-interactive-tools" +pnpEnableEsmLoader: false + preferAggregateCacheInfo: true preferInteractive: true diff --git a/tgui/global.d.ts b/tgui/global.d.ts index 7f2cc8f7282c..348cb9c5165c 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -4,155 +4,179 @@ * @license MIT */ -declare global { - // Webpack asset modules. - // Should match extensions used in webpack config. - declare module '*.png' { - const content: string; - export default content; - } - - declare module '*.jpg' { - const content: string; - export default content; - } - - declare module '*.svg' { - const content: string; - export default content; - } - - type ByondType = { - /** - * True if javascript is running in BYOND. - */ - IS_BYOND: boolean; - - /** - * True if browser is IE8 or lower. - */ - IS_LTE_IE8: boolean; - - /** - * True if browser is IE9 or lower. - */ - IS_LTE_IE9: boolean; - - /** - * True if browser is IE10 or lower. - */ - IS_LTE_IE10: boolean; - - /** - * True if browser is IE11 or lower. - */ - IS_LTE_IE11: boolean; - - /** - * Makes a BYOND call. - * - * If path is empty, this will trigger a Topic call. - * You can reference a specific object by setting the "src" parameter. - * - * See: https://secure.byond.com/docs/ref/skinparams.html - */ - call(path: string, params: object): void; - - /** - * Makes an asynchronous BYOND call. Returns a promise. - */ - callAsync(path: string, params: object): Promise; - - /** - * Makes a Topic call. - * - * You can reference a specific object by setting the "src" parameter. - */ - topic(params: object): void; - - /** - * Runs a command or a verb. - */ - command(command: string): void; - - /** - * Retrieves all properties of the BYOND skin element. - * - * Returns a promise with a key-value object containing all properties. - */ - winget(id: string): Promise; - - /** - * Retrieves all properties of the BYOND skin element. - * - * Returns a promise with a key-value object containing all properties. - */ - winget(id: string, propName: '*'): Promise; - - /** - * Retrieves an exactly one property of the BYOND skin element, - * as defined in `propName`. - * - * Returns a promise with the value of that property. - */ - winget(id: string, propName: string): Promise; - - /** - * Retrieves multiple properties of the BYOND skin element, - * as defined in the `propNames` array. - * - * Returns a promise with a key-value object containing listed properties. - */ - winget(id: string, propNames: string[]): Promise; - - /** - * Assigns properties to BYOND skin elements. - */ - winset(props: object): void; - - /** - * Assigns properties to the BYOND skin element. - */ - winset(id: string, props: object): void; - - /** - * Sets a property on the BYOND skin element to a certain value. - */ - winset(id: string, propName: string, propValue: any): void; - - /** - * Parses BYOND JSON. - * - * Uses a special encoding to preverse Infinity and NaN. - */ - parseJson(text: string): any; - - /** - * Loads a stylesheet into the document. - */ - loadCss(url: string): void; - - /** - * Loads a script into the document. - */ - loadJs(url: string): void; - }; - - /** - * Object that provides access to Byond Skin API and is available in - * any tgui application. - */ - const Byond: ByondType; - - interface Window { - /** - * ID of the Byond window this script is running on. - * Should be used as a parameter to winget/winset. - */ - __windowId__: string; - Byond: ByondType; - } +// Webpack asset modules. +// Should match extensions used in webpack config. +declare module '*.png' { + const content: string; + export default content; +} + +declare module '*.jpg' { + const content: string; + export default content; +} +declare module '*.svg' { + const content: string; + export default content; } -export {}; +type TguiMessage = { + type: string; + payload?: any; + [key: string]: any; +}; + +type ByondType = { + /** + * ID of the Byond window this script is running on. + * Can be used as a parameter to winget/winset. + */ + windowId: string; + + /** + * True if javascript is running in BYOND. + */ + IS_BYOND: boolean; + + /** + * Version of Trident engine of Internet Explorer. Null if N/A. + */ + TRIDENT: number | null; + + /** + * True if browser is IE8 or lower. + */ + IS_LTE_IE8: boolean; + + /** + * True if browser is IE9 or lower. + */ + IS_LTE_IE9: boolean; + + /** + * True if browser is IE10 or lower. + */ + IS_LTE_IE10: boolean; + + /** + * True if browser is IE11 or lower. + */ + IS_LTE_IE11: boolean; + + /** + * Makes a BYOND call. + * + * If path is empty, this will trigger a Topic call. + * You can reference a specific object by setting the "src" parameter. + * + * See: https://secure.byond.com/docs/ref/skinparams.html + */ + call(path: string, params: object): void; + + /** + * Makes an asynchronous BYOND call. Returns a promise. + */ + callAsync(path: string, params: object): Promise; + + /** + * Makes a Topic call. + * + * You can reference a specific object by setting the "src" parameter. + */ + topic(params: object): void; + + /** + * Runs a command or a verb. + */ + command(command: string): void; + + /** + * Retrieves all properties of the BYOND skin element. + * + * Returns a promise with a key-value object containing all properties. + */ + winget(id: string | null): Promise; + + /** + * Retrieves all properties of the BYOND skin element. + * + * Returns a promise with a key-value object containing all properties. + */ + winget(id: string | null, propName: '*'): Promise; + + /** + * Retrieves an exactly one property of the BYOND skin element, + * as defined in `propName`. + * + * Returns a promise with the value of that property. + */ + winget(id: string | null, propName: string): Promise; + + /** + * Retrieves multiple properties of the BYOND skin element, + * as defined in the `propNames` array. + * + * Returns a promise with a key-value object containing listed properties. + */ + winget(id: string | null, propNames: string[]): Promise; + + /** + * Assigns properties to BYOND skin elements in bulk. + */ + winset(props: object): void; + + /** + * Assigns properties to the BYOND skin element. + */ + winset(id: string | null, props: object): void; + + /** + * Sets a property on the BYOND skin element to a certain value. + */ + winset(id: string | null, propName: string, propValue: any): void; + + /** + * Parses BYOND JSON. + * + * Uses a special encoding to preserve `Infinity` and `NaN`. + */ + parseJson(text: string): any; + + /** + * Sends a message to `/datum/tgui_window` which hosts this window instance. + */ + sendMessage(type: string, payload?: any): void; + sendMessage(message: TguiMessage): void; + + /** + * Subscribe to incoming messages that were sent from `/datum/tgui_window`. + */ + subscribe(listener: (type: string, payload: any) => void): void; + + /** + * Subscribe to incoming messages *of some specific type* + * that were sent from `/datum/tgui_window`. + */ + subscribeTo(type: string, listener: (payload: any) => void): void; + + /** + * Loads a stylesheet into the document. + */ + loadCss(url: string): void; + + /** + * Loads a script into the document. + */ + loadJs(url: string): void; +}; + +/** + * Object that provides access to Byond Skin API and is available in + * any tgui application. + */ +const Byond: ByondType; + +interface Window { + Byond: ByondType; +} diff --git a/tgui/package.json b/tgui/package.json index 0f65150c4381..3bfdc3a412ad 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -2,21 +2,22 @@ "private": true, "name": "tgui-workspace", "version": "4.3.0", + "packageManager": "yarn@3.1.1", "workspaces": [ "packages/*" ], "scripts": { "tgui:analyze": "webpack --analyze", - "tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js", "tgui:build": "BROWSERSLIST_IGNORE_OLD_DATA=true webpack", "tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js", "tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx", "tgui:prettier": "", "tgui:sonar": "eslint packages --ext .js,.cjs,.ts,.tsx -c .eslintrc-sonar.yml", + "tgui:tsc": "tsc", "tgui:test": "jest --watch", "tgui:test-simple": "CI=true jest --color", "tgui:test-ci": "CI=true jest --color --collect-coverage", - "tgui:tsc": "tsc" + "tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js" }, "dependencies": { "@babel/core": "^7.15.0", @@ -28,6 +29,8 @@ "@types/jest": "^27.0.1", "@types/jsdom": "^16.2.13", "@types/node": "^14.17.9", + "@types/webpack": "^5.28.0", + "@types/webpack-env": "^1.16.2", "@typescript-eslint/parser": "^4.29.1", "babel-jest": "^27.0.6", "babel-loader": "^8.2.2", @@ -38,6 +41,7 @@ "eslint": "^7.32.0", "eslint-plugin-radar": "^0.2.1", "eslint-plugin-react": "^7.24.0", + "eslint-plugin-unused-imports": "^1.1.4", "file-loader": "^6.2.0", "inferno": "^7.4.8", "jest": "^27.0.6", @@ -53,6 +57,5 @@ "webpack": "^5.50.0", "webpack-bundle-analyzer": "^4.4.2", "webpack-cli": "^4.7.2" - }, - "packageManager": "yarn@3.0.1" + } } diff --git a/tgui/packages/common/collections.js b/tgui/packages/common/collections.js deleted file mode 100644 index 7b4540e730ab..000000000000 --- a/tgui/packages/common/collections.js +++ /dev/null @@ -1,271 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -/** - * Converts a given collection to an array. - * - * - Arrays are returned unmodified; - * - If object was provided, keys will be discarded; - * - Everything else will result in an empty array. - * - * @returns {any[]} - */ -export const toArray = collection => { - if (Array.isArray(collection)) { - return collection; - } - if (typeof collection === 'object') { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const result = []; - for (let i in collection) { - if (hasOwnProperty.call(collection, i)) { - result.push(collection[i]); - } - } - return result; - } - return []; -}; - -/** - * Converts a given object to an array, and appends a key to every - * object inside of that array. - * - * Example input (object): - * ``` - * { - * 'Foo': { info: 'Hello world!' }, - * 'Bar': { info: 'Hello world!' }, - * } - * ``` - * - * Example output (array): - * ``` - * [ - * { key: 'Foo', info: 'Hello world!' }, - * { key: 'Bar', info: 'Hello world!' }, - * ] - * ``` - * - * @template T - * @param {{ [key: string]: T }} obj Object, or in DM terms, an assoc array - * @param {string} keyProp Property, to which key will be assigned - * @returns {T[]} Array of keyed objects - */ -export const toKeyedArray = (obj, keyProp = 'key') => { - return map((item, key) => ({ - [keyProp]: key, - ...item, - }))(obj); -}; - -/** - * Iterates over elements of collection, returning an array of all elements - * iteratee returns truthy for. The predicate is invoked with three - * arguments: (value, index|key, collection). - * - * If collection is 'null' or 'undefined', it will be returned "as is" - * without emitting any errors (which can be useful in some cases). - * - * @returns {any[]} - */ -export const filter = iterateeFn => collection => { - if (collection === null || collection === undefined) { - return collection; - } - if (Array.isArray(collection)) { - const result = []; - for (let i = 0; i < collection.length; i++) { - const item = collection[i]; - if (iterateeFn(item, i, collection)) { - result.push(item); - } - } - return result; - } - throw new Error(`filter() can't iterate on type ${typeof collection}`); -}; - -/** - * Creates an array of values by running each element in collection - * thru an iteratee function. The iteratee is invoked with three - * arguments: (value, index|key, collection). - * - * If collection is 'null' or 'undefined', it will be returned "as is" - * without emitting any errors (which can be useful in some cases). - * - * @returns {any[]} - */ -export const map = iterateeFn => collection => { - if (collection === null || collection === undefined) { - return collection; - } - if (Array.isArray(collection)) { - const result = []; - for (let i = 0; i < collection.length; i++) { - result.push(iterateeFn(collection[i], i, collection)); - } - return result; - } - if (typeof collection === 'object') { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const result = []; - for (let i in collection) { - if (hasOwnProperty.call(collection, i)) { - result.push(iterateeFn(collection[i], i, collection)); - } - } - return result; - } - throw new Error(`map() can't iterate on type ${typeof collection}`); -}; - -const COMPARATOR = (objA, objB) => { - const criteriaA = objA.criteria; - const criteriaB = objB.criteria; - const length = criteriaA.length; - for (let i = 0; i < length; i++) { - const a = criteriaA[i]; - const b = criteriaB[i]; - if (a < b) { - return -1; - } - if (a > b) { - return 1; - } - } - return 0; -}; - -/** - * Creates an array of elements, sorted in ascending order by the results - * of running each element in a collection thru each iteratee. - * - * Iteratees are called with one argument (value). - * - * @returns {any[]} - */ -export const sortBy = (...iterateeFns) => array => { - if (!Array.isArray(array)) { - return array; - } - let length = array.length; - // Iterate over the array to collect criteria to sort it by - let mappedArray = []; - for (let i = 0; i < length; i++) { - const value = array[i]; - mappedArray.push({ - criteria: iterateeFns.map(fn => fn(value)), - value, - }); - } - // Sort criteria using the base comparator - mappedArray.sort(COMPARATOR); - // Unwrap values - while (length--) { - mappedArray[length] = mappedArray[length].value; - } - return mappedArray; -}; - -/** - * A fast implementation of reduce. - */ -export const reduce = (reducerFn, initialValue) => array => { - const length = array.length; - let i; - let result; - if (initialValue === undefined) { - i = 1; - result = array[0]; - } - else { - i = 0; - result = initialValue; - } - for (; i < length; i++) { - result = reducerFn(result, array[i], i, array); - } - return result; -}; - -/** - * Creates a duplicate-free version of an array, using SameValueZero for - * equality comparisons, in which only the first occurrence of each element - * is kept. The order of result values is determined by the order they occur - * in the array. - * - * It accepts iteratee which is invoked for each element in array to generate - * the criterion by which uniqueness is computed. The order of result values - * is determined by the order they occur in the array. The iteratee is - * invoked with one argument: value. - */ -export const uniqBy = iterateeFn => array => { - const { length } = array; - const result = []; - const seen = iterateeFn ? [] : result; - let index = -1; - outer: - while (++index < length) { - let value = array[index]; - const computed = iterateeFn ? iterateeFn(value) : value; - value = value !== 0 ? value : 0; - if (computed === computed) { - let seenIndex = seen.length; - while (seenIndex--) { - if (seen[seenIndex] === computed) { - continue outer; - } - } - if (iterateeFn) { - seen.push(computed); - } - result.push(value); - } - else if (!seen.includes(computed)) { - if (seen !== result) { - seen.push(computed); - } - result.push(value); - } - } - return result; -}; - -/** - * Creates an array of grouped elements, the first of which contains - * the first elements of the given arrays, the second of which contains - * the second elements of the given arrays, and so on. - * - * @returns {any[]} - */ -export const zip = (...arrays) => { - if (arrays.length === 0) { - return; - } - const numArrays = arrays.length; - const numValues = arrays[0].length; - const result = []; - for (let valueIndex = 0; valueIndex < numValues; valueIndex++) { - const entry = []; - for (let arrayIndex = 0; arrayIndex < numArrays; arrayIndex++) { - entry.push(arrays[arrayIndex][valueIndex]); - } - result.push(entry); - } - return result; -}; - -/** - * This method is like "zip" except that it accepts iteratee to - * specify how grouped values should be combined. The iteratee is - * invoked with the elements of each group. - * - * @returns {any[]} - */ -export const zipWith = iterateeFn => (...arrays) => { - return map(values => iterateeFn(...values))(zip(...arrays)); -}; diff --git a/tgui/packages/common/collections.spec.ts b/tgui/packages/common/collections.spec.ts new file mode 100644 index 000000000000..58eff7f354c9 --- /dev/null +++ b/tgui/packages/common/collections.spec.ts @@ -0,0 +1,20 @@ +import { range, zip } from "./collections"; + +// Type assertions, these will lint if the types are wrong. +const _zip1: [string, number] = zip(["a"], [1])[0]; + +describe("range", () => { + test("range(0, 5)", () => { + expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]); + }); +}); + +describe("zip", () => { + test("zip(['a', 'b', 'c'], [1, 2, 3, 4])", () => { + expect(zip(["a", "b", "c"], [1, 2, 3, 4])).toEqual([ + ["a", 1], + ["b", 2], + ["c", 3], + ]); + }); +}); diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts index 40f610b36d6f..f0419253e242 100644 --- a/tgui/packages/common/collections.ts +++ b/tgui/packages/common/collections.ts @@ -4,64 +4,6 @@ * @license MIT */ -/** - * Converts a given collection to an array. - * - * - Arrays are returned unmodified; - * - If object was provided, keys will be discarded; - * - Everything else will result in an empty array. - * - * @returns {any[]} - */ -export const toArray = collection => { - if (Array.isArray(collection)) { - return collection; - } - if (typeof collection === 'object') { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const result = []; - for (let i in collection) { - if (hasOwnProperty.call(collection, i)) { - result.push(collection[i]); - } - } - return result; - } - return []; -}; - -/** - * Converts a given object to an array, and appends a key to every - * object inside of that array. - * - * Example input (object): - * ``` - * { - * 'Foo': { info: 'Hello world!' }, - * 'Bar': { info: 'Hello world!' }, - * } - * ``` - * - * Example output (array): - * ``` - * [ - * { key: 'Foo', info: 'Hello world!' }, - * { key: 'Bar', info: 'Hello world!' }, - * ] - * ``` - * - * @template T - * @param {{ [key: string]: T }} obj Object, or in DM terms, an assoc array - * @param {string} keyProp Property, to which key will be assigned - * @returns {T[]} Array of keyed objects - */ -export const toKeyedArray = (obj, keyProp = 'key') => { - return map((item, key) => ({ - [keyProp]: key, - ...item, - }))(obj); -}; - /** * Iterates over elements of collection, returning an array of all elements * iteratee returns truthy for. The predicate is invoked with three @@ -72,21 +14,40 @@ export const toKeyedArray = (obj, keyProp = 'key') => { * * @returns {any[]} */ -export const filter = iterateeFn => collection => { - if (collection === null && collection === undefined) { - return collection; - } - if (Array.isArray(collection)) { - const result = []; - for (let i = 0; i < collection.length; i++) { - const item = collection[i]; - if (iterateeFn(item, i, collection)) { - result.push(item); +export const filter = (iterateeFn: ( + input: T, + index: number, + collection: T[], +) => boolean) => + (collection: T[]): T[] => { + if (collection === null || collection === undefined) { + return collection; } - } - return result; - } - throw new Error(`filter() can't iterate on type ${typeof collection}`); + if (Array.isArray(collection)) { + const result: T[] = []; + for (let i = 0; i < collection.length; i++) { + const item = collection[i]; + if (iterateeFn(item, i, collection)) { + result.push(item); + } + } + return result; + } + throw new Error(`filter() can't iterate on type ${typeof collection}`); + }; + +type MapFunction = { + (iterateeFn: ( + value: T, + index: number, + collection: T[], + ) => U): (collection: T[]) => U[]; + + (iterateeFn: ( + value: T, + index: K, + collection: Record, + ) => U): (collection: Record) => U[]; }; /** @@ -96,31 +57,43 @@ export const filter = iterateeFn => collection => { * * If collection is 'null' or 'undefined', it will be returned "as is" * without emitting any errors (which can be useful in some cases). - * - * @returns {any[]} */ -export const map = iterateeFn => collection => { - if (collection === null && collection === undefined) { - return collection; - } - if (Array.isArray(collection)) { - const result = []; - for (let i = 0; i < collection.length; i++) { - result.push(iterateeFn(collection[i], i, collection)); +export const map: MapFunction = (iterateeFn) => + (collection: T[]): U[] => { + if (collection === null || collection === undefined) { + return collection; } - return result; - } - if (typeof collection === 'object') { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const result = []; - for (let i in collection) { - if (hasOwnProperty.call(collection, i)) { - result.push(iterateeFn(collection[i], i, collection)); - } + + if (Array.isArray(collection)) { + return collection.map(iterateeFn); + } + + if (typeof collection === 'object') { + return Object.entries(collection).map(([key, value]) => { + return iterateeFn(value, key, collection); + }); + } + + throw new Error(`map() can't iterate on type ${typeof collection}`); + }; + +/** + * Given a collection, will run each element through an iteratee function. + * Will then filter out undefined values. + */ +export const filterMap = (collection: T[], iterateeFn: ( + value: T +) => U | undefined): U[] => { + const finalCollection: U[] = []; + + for (const value of collection) { + const output = iterateeFn(value); + if (output !== undefined) { + finalCollection.push(output); } - return result; } - throw new Error(`map() can't iterate on type ${typeof collection}`); + + return finalCollection; }; const COMPARATOR = (objA, objB) => { @@ -145,31 +118,47 @@ const COMPARATOR = (objA, objB) => { * of running each element in a collection thru each iteratee. * * Iteratees are called with one argument (value). - * - * @returns {any[]} */ -export const sortBy = (...iterateeFns) => array => { - if (!Array.isArray(array)) { - return array; - } - let length = array.length; - // Iterate over the array to collect criteria to sort it by - let mappedArray = []; - for (let i = 0; i < length; i++) { - const value = array[i]; - mappedArray.push({ - criteria: iterateeFns.map(fn => fn(value)), - value, - }); - } - // Sort criteria using the base comparator - mappedArray.sort(COMPARATOR); - // Unwrap values - while (length--) { - mappedArray[length] = mappedArray[length].value; - } - return mappedArray; -}; +export const sortBy = ( + ...iterateeFns: ((input: T) => unknown)[] +) => (array: T[]): T[] => { + if (!Array.isArray(array)) { + return array; + } + let length = array.length; + // Iterate over the array to collect criteria to sort it by + let mappedArray: { + criteria: unknown[], + value: T, + }[] = []; + for (let i = 0; i < length; i++) { + const value = array[i]; + mappedArray.push({ + criteria: iterateeFns.map(fn => fn(value)), + value, + }); + } + // Sort criteria using the base comparator + mappedArray.sort(COMPARATOR); + + // Unwrap values + const values: T[] = []; + while (length--) { + values[length] = mappedArray[length].value; + } + return values; + }; + +export const sort = sortBy(); + +export const sortStrings = sortBy(); + +/** + * Returns a range of numbers from start to end, exclusively. + * For example, range(0, 5) will return [0, 1, 2, 3, 4]. + */ +export const range = (start: number, end: number): number[] => + new Array(end - start).fill(null).map((_, index) => index + start); /** * A fast implementation of reduce. @@ -203,58 +192,66 @@ export const reduce = (reducerFn, initialValue) => array => { * is determined by the order they occur in the array. The iteratee is * invoked with one argument: value. */ -export const uniqBy = iterateeFn => array => { - const { length } = array; - const result = []; - const seen = iterateeFn ? [] : result; - let index = -1; - outer: - while (++index < length) { - let value = array[index]; - const computed = iterateeFn ? iterateeFn(value) : value; - value = value !== 0 ? value : 0; - if (computed === computed) { - let seenIndex = seen.length; - while (seenIndex--) { - if (seen[seenIndex] === computed) { - continue outer; +export const uniqBy = ( + iterateeFn?: (value: T) => unknown +) => (array: T[]): T[] => { + const { length } = array; + const result: T[] = []; + const seen: unknown[] = iterateeFn ? [] : result; + let index = -1; + outer: + while (++index < length) { + let value: T | 0 = array[index]; + const computed = iterateeFn ? iterateeFn(value) : value; + if (computed === computed) { + let seenIndex = seen.length; + while (seenIndex--) { + if (seen[seenIndex] === computed) { + continue outer; + } } + if (iterateeFn) { + seen.push(computed); + } + result.push(value); } - if (iterateeFn) { - seen.push(computed); - } - result.push(value); - } - else if (!seen.includes(computed)) { - if (seen !== result) { - seen.push(computed); + else if (!seen.includes(computed)) { + if (seen !== result) { + seen.push(computed); + } + result.push(value); } - result.push(value); } - } - return result; -}; + return result; + }; +/* eslint-enable indent */ + +export const uniq = uniqBy(); + +type Zip = { + [I in keyof T]: T[I] extends (infer U)[] ? U : never; +}[]; /** * Creates an array of grouped elements, the first of which contains * the first elements of the given arrays, the second of which contains * the second elements of the given arrays, and so on. - * - * @returns {any[]} */ -export const zip = (...arrays) => { +export const zip = (...arrays: T): Zip => { if (arrays.length === 0) { - return; + return []; } const numArrays = arrays.length; const numValues = arrays[0].length; - const result = []; + const result: Zip = []; for (let valueIndex = 0; valueIndex < numValues; valueIndex++) { - const entry = []; + const entry: unknown[] = []; for (let arrayIndex = 0; arrayIndex < numArrays; arrayIndex++) { entry.push(arrays[arrayIndex][valueIndex]); } - result.push(entry); + + // I tried everything to remove this any, and have no idea how to do it. + result.push(entry as any); } return result; }; @@ -263,13 +260,56 @@ export const zip = (...arrays) => { * This method is like "zip" except that it accepts iteratee to * specify how grouped values should be combined. The iteratee is * invoked with the elements of each group. - * - * @returns {any[]} */ -export const zipWith = iterateeFn => (...arrays) => { - return map(values => iterateeFn(...values))(zip(...arrays)); +export const zipWith = (iterateeFn: (...values: T[]) => U) => + (...arrays: T[][]): U[] => { + return map((values: T[]) => iterateeFn(...values))(zip(...arrays)); + }; + +const binarySearch = ( + getKey: (value: T) => U, + collection: readonly T[], + inserting: T, +): number => { + if (collection.length === 0) { + return 0; + } + + const insertingKey = getKey(inserting); + + let [low, high] = [0, collection.length]; + + // Because we have checked if the collection is empty, it's impossible + // for this to be used before assignment. + let compare: U = undefined as unknown as U; + let middle = 0; + + while (low < high) { + middle = (low + high) >> 1; + + compare = getKey(collection[middle]); + + if (compare < insertingKey) { + low = middle + 1; + } else if (compare === insertingKey) { + return middle; + } else { + high = middle; + } + } + + return compare > insertingKey ? middle : middle + 1; }; +export const binaryInsertWith = (getKey: (value: T) => U): + ((collection: readonly T[], value: T) => T[]) => +{ + return (collection, value) => { + const copy = [...collection]; + copy.splice(binarySearch(getKey, collection, value), 0, value); + return copy; + }; +}; const isObject = (obj: unknown) => typeof obj === 'object' && obj !== null; diff --git a/tgui/packages/common/color.js b/tgui/packages/common/color.js index 4a62b97b8491..ba26187326d0 100644 --- a/tgui/packages/common/color.js +++ b/tgui/packages/common/color.js @@ -15,7 +15,12 @@ export class Color { } toString() { - return `rgba(${this.r | 0}, ${this.g | 0}, ${this.b | 0}, ${this.a | 0})`; + // Alpha component needs to permit fractional values, so cannot use | + let alpha = parseFloat(this.a); + if (isNaN(alpha)) { + alpha = 1; + } + return `rgba(${this.r | 0}, ${this.g | 0}, ${this.b | 0}, ${alpha})`; } // Darkens a color by a given percent. Returns a color, which can have toString called to get it's rgba() css value. diff --git a/tgui/packages/common/exhaustive.ts b/tgui/packages/common/exhaustive.ts new file mode 100644 index 000000000000..bc41757515b0 --- /dev/null +++ b/tgui/packages/common/exhaustive.ts @@ -0,0 +1,19 @@ +/** + * Throws an error such that a non-exhaustive check will error at compile time + * when using TypeScript, rather than at runtime. + * + * For example: + * enum Color { Red, Green, Blue } + * switch (color) { + * case Color.Red: + * return "red"; + * case Color.Green: + * return "green"; + * default: + * // This will error at compile time that we forgot blue. + * exhaustiveCheck(color); + * } + */ +export const exhaustiveCheck = (input: never) => { + throw new Error(`Unhandled case: ${input}`); +}; diff --git a/tgui/packages/common/math.ts b/tgui/packages/common/math.ts new file mode 100644 index 000000000000..97e6b60b2ed4 --- /dev/null +++ b/tgui/packages/common/math.ts @@ -0,0 +1,100 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +/** + * Limits a number to the range between 'min' and 'max'. + */ +export const clamp = (value, min, max) => { + return value < min ? min : value > max ? max : value; +}; + +/** + * Limits a number between 0 and 1. + */ +export const clamp01 = value => { + return value < 0 ? 0 : value > 1 ? 1 : value; +}; + +/** + * Scales a number to fit into the range between min and max. + */ +export const scale = (value, min, max) => { + return (value - min) / (max - min); +}; + +/** + * Robust number rounding. + * + * Adapted from Locutus, see: http://locutus.io/php/math/round/ + * + * @param {number} value + * @param {number} precision + * @return {number} + */ +export const round = (value, precision) => { + if (!value || isNaN(value)) { + return value; + } + // helper variables + let m, f, isHalf, sgn; + // making sure precision is integer + precision |= 0; + m = Math.pow(10, precision); + value *= m; + // sign of the number + sgn = +(value > 0) | -(value < 0); + // isHalf = value % 1 === 0.5 * sgn; + isHalf = Math.abs(value % 1) >= 0.4999999999854481; + f = Math.floor(value); + if (isHalf) { + // rounds .5 away from zero + value = f + (sgn > 0); + } + return (isHalf ? value : Math.round(value)) / m; +}; + +/** + * Returns a string representing a number in fixed point notation. + */ +export const toFixed = (value, fractionDigits = 0) => { + return Number(value).toFixed(Math.max(fractionDigits, 0)); +}; + +/** + * Checks whether a value is within the provided range. + * + * Range is an array of two numbers, for example: [0, 15]. + */ +export const inRange = (value, range) => { + return range + && value >= range[0] + && value <= range[1]; +}; + +/** + * Walks over the object with ranges, comparing value against every range, + * and returns the key of the first matching range. + * + * Range is an array of two numbers, for example: [0, 15]. + */ +export const keyOfMatchingRange = (value, ranges) => { + for (let rangeName of Object.keys(ranges)) { + const range = ranges[rangeName]; + if (inRange(value, range)) { + return rangeName; + } + } +}; + +/** + * Get number of digits following the decimal point in a number + */ +export const numberOfDecimalDigits = value => { + if (Math.floor(value) !== value) { + return value.toString().split('.')[1].length || 0; + } + return 0; +}; diff --git a/tgui/packages/common/types.ts b/tgui/packages/common/types.ts index e219bd3b7e12..a92ac122d9fe 100644 --- a/tgui/packages/common/types.ts +++ b/tgui/packages/common/types.ts @@ -1,6 +1,5 @@ /** * Returns the arguments of a function F as an array. */ -// prettier-ignore export type ArgumentsOf = F extends (...args: infer A) => unknown ? A : never; diff --git a/tgui/packages/tgfont/dist/tgfont.css b/tgui/packages/tgfont/dist/tgfont.css index 7de15a0d7cf8..b30dd992813d 100644 --- a/tgui/packages/tgfont/dist/tgfont.css +++ b/tgui/packages/tgfont/dist/tgfont.css @@ -1,7 +1,7 @@ @font-face { font-family: "tgfont"; - src: url("./tgfont.woff2?befe80cc7d69939d2283d48b84f181ef") format("woff2"), -url("./tgfont.eot?befe80cc7d69939d2283d48b84f181ef#iefix") format("embedded-opentype"); + src: url("./tgfont.woff2?bb59482417f65cea822ea95f849b2acf") format("woff2"), +url("./tgfont.eot?bb59482417f65cea822ea95f849b2acf#iefix") format("embedded-opentype"); } i[class^="tg-"]:before, i[class*=" tg-"]:before { @@ -15,9 +15,39 @@ i[class^="tg-"]:before, i[class*=" tg-"]:before { -moz-osx-font-smoothing: grayscale; } -.tg-nanotrasen_logo:before { +.tg-air-tank-slash:before { content: "\f101"; } -.tg-syndicate_logo:before { +.tg-air-tank:before { content: "\f102"; } +.tg-bad-touch:before { + content: "\f103"; +} +.tg-image-minus:before { + content: "\f104"; +} +.tg-image-plus:before { + content: "\f105"; +} +.tg-nanotrasen-logo:before { + content: "\f106"; +} +.tg-non-binary:before { + content: "\f107"; +} +.tg-prosthetic-full:before { + content: "\f108"; +} +.tg-prosthetic-leg:before { + content: "\f109"; +} +.tg-sound-minus:before { + content: "\f10a"; +} +.tg-sound-plus:before { + content: "\f10b"; +} +.tg-syndicate-logo:before { + content: "\f10c"; +} diff --git a/tgui/packages/tgfont/dist/tgfont.eot b/tgui/packages/tgfont/dist/tgfont.eot index a6c3a57332b4..c8e092a4d3d1 100644 Binary files a/tgui/packages/tgfont/dist/tgfont.eot and b/tgui/packages/tgfont/dist/tgfont.eot differ diff --git a/tgui/packages/tgfont/dist/tgfont.woff2 b/tgui/packages/tgfont/dist/tgfont.woff2 index 715dd3e572c8..87df9b8efb4c 100644 Binary files a/tgui/packages/tgfont/dist/tgfont.woff2 and b/tgui/packages/tgfont/dist/tgfont.woff2 differ diff --git a/tgui/packages/tgfont/icons/ATTRIBUTIONS.md b/tgui/packages/tgfont/icons/ATTRIBUTIONS.md new file mode 100644 index 000000000000..2f218388d364 --- /dev/null +++ b/tgui/packages/tgfont/icons/ATTRIBUTIONS.md @@ -0,0 +1,6 @@ +bad-touch.svg contains: +- hug by Phạm Thanh Lộc from the Noun Project +- Fight by Rudez Studio from the Noun Project + +prosthetic-leg.svg contains: +- prosthetic leg by Gan Khoon Lay from the Noun Project diff --git a/tgui/packages/tgfont/icons/air-tank-slash.svg b/tgui/packages/tgfont/icons/air-tank-slash.svg new file mode 100644 index 000000000000..37ffcb5b8809 --- /dev/null +++ b/tgui/packages/tgfont/icons/air-tank-slash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tgui/packages/tgfont/icons/air-tank.svg b/tgui/packages/tgfont/icons/air-tank.svg new file mode 100644 index 000000000000..7d1e07747197 --- /dev/null +++ b/tgui/packages/tgfont/icons/air-tank.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tgui/packages/tgfont/icons/bad-touch.svg b/tgui/packages/tgfont/icons/bad-touch.svg new file mode 100644 index 000000000000..6dc3c9a718a7 --- /dev/null +++ b/tgui/packages/tgfont/icons/bad-touch.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/tgui/packages/tgfont/icons/image-minus.svg b/tgui/packages/tgfont/icons/image-minus.svg new file mode 100644 index 000000000000..8c3231917ff9 --- /dev/null +++ b/tgui/packages/tgfont/icons/image-minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tgui/packages/tgfont/icons/image-plus.svg b/tgui/packages/tgfont/icons/image-plus.svg new file mode 100644 index 000000000000..1658509429e3 --- /dev/null +++ b/tgui/packages/tgfont/icons/image-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tgui/packages/tgfont/icons/nanotrasen-logo.svg b/tgui/packages/tgfont/icons/nanotrasen-logo.svg new file mode 100644 index 000000000000..b74a415d4d2b --- /dev/null +++ b/tgui/packages/tgfont/icons/nanotrasen-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/tgui/packages/tgfont/icons/nanotrasen_logo.svg b/tgui/packages/tgfont/icons/nanotrasen_logo.svg deleted file mode 100644 index d21b9f0a2a0f..000000000000 --- a/tgui/packages/tgfont/icons/nanotrasen_logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/tgui/packages/tgfont/icons/non-binary.svg b/tgui/packages/tgfont/icons/non-binary.svg new file mode 100644 index 000000000000..9aaec674bbbc --- /dev/null +++ b/tgui/packages/tgfont/icons/non-binary.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/tgui/packages/tgfont/icons/prosthetic-full.svg b/tgui/packages/tgfont/icons/prosthetic-full.svg new file mode 100644 index 000000000000..7d221244edcc --- /dev/null +++ b/tgui/packages/tgfont/icons/prosthetic-full.svg @@ -0,0 +1,27 @@ + +image/svg+xml + + diff --git a/tgui/packages/tgfont/icons/prosthetic-leg.svg b/tgui/packages/tgfont/icons/prosthetic-leg.svg new file mode 100644 index 000000000000..c1f6ceee3fc3 --- /dev/null +++ b/tgui/packages/tgfont/icons/prosthetic-leg.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/tgui/packages/tgfont/icons/sound-minus.svg b/tgui/packages/tgfont/icons/sound-minus.svg new file mode 100644 index 000000000000..df51179d4b53 --- /dev/null +++ b/tgui/packages/tgfont/icons/sound-minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tgui/packages/tgfont/icons/sound-plus.svg b/tgui/packages/tgfont/icons/sound-plus.svg new file mode 100644 index 000000000000..c5f40d53b560 --- /dev/null +++ b/tgui/packages/tgfont/icons/sound-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tgui/packages/tgfont/icons/syndicate-logo.svg b/tgui/packages/tgfont/icons/syndicate-logo.svg new file mode 100644 index 000000000000..eda92f9b3082 --- /dev/null +++ b/tgui/packages/tgfont/icons/syndicate-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/tgui/packages/tgfont/icons/syndicate_logo.svg b/tgui/packages/tgfont/icons/syndicate_logo.svg deleted file mode 100644 index c2863b790df3..000000000000 --- a/tgui/packages/tgfont/icons/syndicate_logo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/tgui/packages/tgui-panel/Panel.js b/tgui/packages/tgui-panel/Panel.js index 927d793b17ab..d6cc9eb7efc8 100644 --- a/tgui/packages/tgui-panel/Panel.js +++ b/tgui/packages/tgui-panel/Panel.js @@ -4,11 +4,11 @@ * @license MIT */ -import { Button, Flex, Section } from 'tgui/components'; +import { Button, Section, Stack } from 'tgui/components'; import { Pane } from 'tgui/layouts'; import { NowPlayingWidget, useAudio } from './audio'; import { ChatPanel, ChatTabs } from './chat'; -import { gameReducer, useGame } from './game'; +import { useGame } from './game'; import { Notifications } from './Notifications'; import { PingIndicator } from './ping'; import { SettingsPanel, useSettings } from './settings'; @@ -34,19 +34,17 @@ export const Panel = (props, context) => { } return ( - - + +
- - + + - - + + - - + +
-
+ {audio.visible && ( - +
-
+ )} {settings.visible && ( - + - + )} - +
@@ -96,21 +94,19 @@ export const Panel = (props, context) => { )}> You are either AFK, experiencing lag or the connection - has closed. If the server has been nuked, you - are just lagging, you should be fine in a moment. + has closed. )} - {game.reconnectTimer > 0 && ( + {game.roundRestartedAt && ( The connection has been closed because the server is - restarting. Please wait while you are automatically reconnected - in {game.reconnectTimer} Seconds. + restarting. Please wait while you automatically reconnect. )}
-
-
+ +
); }; @@ -132,9 +128,7 @@ const HoboPanel = (props, context) => { Settings {settings.visible && ( - - - + ) || ( )} diff --git a/tgui/packages/tgui-panel/chat/ChatPageSettings.js b/tgui/packages/tgui-panel/chat/ChatPageSettings.js index 278fbab8f2f2..ba045983d2f3 100644 --- a/tgui/packages/tgui-panel/chat/ChatPageSettings.js +++ b/tgui/packages/tgui-panel/chat/ChatPageSettings.js @@ -5,7 +5,7 @@ */ import { useDispatch, useSelector } from 'common/redux'; -import { Button, Collapsible, Divider, Stack, Input, Section } from 'tgui/components'; +import { Button, Collapsible, Divider, Input, Section, Stack } from 'tgui/components'; import { removeChatPage, toggleAcceptedType, updateChatPage } from './actions'; import { MESSAGE_TYPES } from './constants'; import { selectCurrentChatPage } from './selectors'; diff --git a/tgui/packages/tgui-panel/chat/middleware.js b/tgui/packages/tgui-panel/chat/middleware.js index bb903f018aca..6cfc72bd2994 100644 --- a/tgui/packages/tgui-panel/chat/middleware.js +++ b/tgui/packages/tgui-panel/chat/middleware.js @@ -4,6 +4,7 @@ * @license MIT */ +import DOMPurify from 'dompurify'; import { storage } from 'common/storage'; import { loadSettings, updateSettings } from '../settings/actions'; import { selectSettings } from '../settings/selectors'; @@ -13,6 +14,14 @@ import { createMessage, serializeMessage } from './model'; import { chatRenderer } from './renderer'; import { selectChat, selectCurrentChatPage } from './selectors'; +// List of blacklisted tags +const FORBID_TAGS = [ + 'a', + 'iframe', + 'link', + 'video', +]; + const saveChatToStorage = async store => { const state = selectChat(store.getState()); const fromIndex = Math.max(0, @@ -30,11 +39,18 @@ const loadChatFromStorage = async store => { storage.get('chat-messages'), ]); // Discard incompatible versions - if (state && state.version <= 5) { + if (state && state.version <= 4) { store.dispatch(loadChat()); return; } if (messages) { + for (let message of messages) { + if (message.html) { + message.html = DOMPurify.sanitize(message.html, { + FORBID_TAGS, + }); + } + } const batch = [ ...messages, createMessage({ diff --git a/tgui/packages/tgui-panel/chat/model.js b/tgui/packages/tgui-panel/chat/model.js index 035bd0cf2781..7400d5e13cd3 100644 --- a/tgui/packages/tgui-panel/chat/model.js +++ b/tgui/packages/tgui-panel/chat/model.js @@ -11,14 +11,22 @@ export const canPageAcceptType = (page, type) => ( type.startsWith(MESSAGE_TYPE_INTERNAL) || page.acceptedTypes[type] ); -export const createPage = obj => ({ - id: createUuid(), - name: 'New Tab', - acceptedTypes: {}, - unreadCount: 0, - createdAt: Date.now(), - ...obj, -}); +export const createPage = obj => { + let acceptedTypes = {}; + + for (let typeDef of MESSAGE_TYPES) { + acceptedTypes[typeDef.type] = !!typeDef.important; + } + + return { + id: createUuid(), + name: 'New Tab', + acceptedTypes: acceptedTypes, + unreadCount: 0, + createdAt: Date.now(), + ...obj, + }; +}; export const createMainPage = () => { const acceptedTypes = {}; diff --git a/tgui/packages/tgui-panel/chat/reducer.js b/tgui/packages/tgui-panel/chat/reducer.js index d4b7b809689c..0188b1fabe8b 100644 --- a/tgui/packages/tgui-panel/chat/reducer.js +++ b/tgui/packages/tgui-panel/chat/reducer.js @@ -10,7 +10,7 @@ import { canPageAcceptType, createMainPage } from './model'; const mainPage = createMainPage(); export const initialState = { - version: 6, + version: 5, currentPageId: mainPage.id, scrollTracking: true, pages: [ @@ -28,6 +28,19 @@ export const chatReducer = (state = initialState, action) => { if (payload?.version !== state.version) { return state; } + // Enable any filters that are not explicitly set, that are + // enabled by default on the main page. + // NOTE: This mutates acceptedTypes on the state. + for (let id of Object.keys(payload.pageById)) { + const page = payload.pageById[id]; + const filters = page.acceptedTypes; + const defaultFilters = mainPage.acceptedTypes; + for (let type of Object.keys(defaultFilters)) { + if (filters[type] === undefined) { + filters[type] = defaultFilters[type]; + } + } + } // Reset page message counts // NOTE: We are mutably changing the payload on the assumption // that it is a copy that comes straight from the web storage. diff --git a/tgui/packages/tgui-panel/chat/renderer.js b/tgui/packages/tgui-panel/chat/renderer.js index 3825e94c846b..b95066116053 100644 --- a/tgui/packages/tgui-panel/chat/renderer.js +++ b/tgui/packages/tgui-panel/chat/renderer.js @@ -8,8 +8,10 @@ import { EventEmitter } from 'common/events'; import { classes } from 'common/react'; import { createLogger } from 'tgui/logging'; import { COMBINE_MAX_MESSAGES, COMBINE_MAX_TIME_WINDOW, IMAGE_RETRY_DELAY, IMAGE_RETRY_LIMIT, IMAGE_RETRY_MESSAGE_AGE, MAX_PERSISTED_MESSAGES, MAX_VISIBLE_MESSAGES, MESSAGE_PRUNE_INTERVAL, MESSAGE_TYPES, MESSAGE_TYPE_INTERNAL, MESSAGE_TYPE_UNKNOWN } from './constants'; +import { render } from 'inferno'; import { canPageAcceptType, createMessage, isSameMessage } from './model'; import { highlightNode, linkifyNode } from './replaceInTextNode'; +import { Tooltip } from "../../tgui/components"; const logger = createLogger('chatRenderer'); @@ -17,6 +19,18 @@ const logger = createLogger('chatRenderer'); // that is still trackable. const SCROLL_TRACKING_TOLERANCE = 24; +// List of injectable component names to the actual type +export const TGUI_CHAT_COMPONENTS = { + Tooltip, +}; + +// List of injectable attibute names mapped to their proper prop +// We need this because attibutes don't support lowercase names +export const TGUI_CHAT_ATTRIBUTES_TO_PROPS = { + "position": "position", + "content": "content", +}; + const findNearestScrollableParent = startingNode => { const body = document.body; let node = startingNode; @@ -176,7 +190,6 @@ class ChatRenderer { this.highlightColor = null; return; } - // const allowedRegex = /^[a-z0-9_-\s]+$/ig; const lines = String(text) .split(',') // eslint-disable-next-line no-useless-escape @@ -302,6 +315,51 @@ class ChatRenderer { else { logger.error('Error: message is missing text payload', message); } + // Get all nodes in this message that want to be rendered like jsx + const nodes = node.querySelectorAll("[data-component]"); + for (let i = 0; i < nodes.length; i++) { + const childNode = nodes[i]; + const targetName = childNode.getAttribute("data-component"); + // Let's pull out the attibute info we need + let outputProps = {}; + for (let j = 0; j < childNode.attributes.length; j++) { + const attribute = childNode.attributes[j]; + + let working_value = attribute.nodeValue; + // We can't do the "if it has no value it's truthy" trick + // Because getAttribute returns "", not null. Hate IE + if (working_value === "$true") { + working_value = true; + } + else if (working_value === "$false") { + working_value = false; + } + else if (!isNaN(working_value)) { + const parsed_float = parseFloat(working_value); + if (!isNaN(parsed_float)) { + working_value = parsed_float; + } + } + + let canon_name = attribute.nodeName.replace("data-", ""); + // html attributes don't support upper case chars, so we need to map + canon_name = TGUI_CHAT_ATTRIBUTES_TO_PROPS[canon_name]; + outputProps[canon_name] = working_value; + } + const oldHtml = { __html: childNode.innerHTML }; + while (childNode.firstChild) { + childNode.removeChild(childNode.firstChild); + } + const Element = TGUI_CHAT_COMPONENTS[targetName]; + /* eslint-disable react/no-danger */ + render( + + + , childNode); + /* eslint-enable react/no-danger */ + + } + // Highlight text if (!message.avoidHighlighting && this.highlightRegex) { const highlighted = highlightNode(node, @@ -450,7 +508,7 @@ class ChatRenderer { cssText += 'body, html { background-color: #141414 }\n'; // Compile chat log as HTML text let messagesHtml = ''; - for (let message of this.messages) { + for (let message of this.visibleMessages) { if (message.node) { messagesHtml += message.node.outerHTML + '\n'; } diff --git a/tgui/packages/tgui-panel/game/actions.js b/tgui/packages/tgui-panel/game/actions.js index 4fb425267de2..e40014c44b04 100644 --- a/tgui/packages/tgui-panel/game/actions.js +++ b/tgui/packages/tgui-panel/game/actions.js @@ -7,6 +7,5 @@ import { createAction } from 'common/redux'; export const roundRestarted = createAction('roundrestart'); -export const reconnected = createAction('reconnected'); export const connectionLost = createAction('game/connectionLost'); export const connectionRestored = createAction('game/connectionRestored'); diff --git a/tgui/packages/tgui-panel/game/reducer.js b/tgui/packages/tgui-panel/game/reducer.js index b3853bb2dfb5..97535524c560 100644 --- a/tgui/packages/tgui-panel/game/reducer.js +++ b/tgui/packages/tgui-panel/game/reducer.js @@ -6,7 +6,6 @@ import { connectionLost } from './actions'; import { connectionRestored } from './actions'; -import { reconnected } from './actions'; const initialState = { // TODO: This is where round info should be. @@ -14,9 +13,6 @@ const initialState = { roundTime: null, roundRestartedAt: null, connectionLostAt: null, - rebooting: false, - reconnectTimer: 0, - reconnected: false, }; export const gameReducer = (state = initialState, action) => { @@ -25,23 +21,8 @@ export const gameReducer = (state = initialState, action) => { return { ...state, roundRestartedAt: meta.now, - rebooting: true, - reconnectTimer: 14, - reconnected: false, - tryingtoreconnect: true, }; } - if (type === 'reconnected') { - return { - ...state, - reconnected: true, - rebooting: false, - }; - } - if (state.rebooting === true && state.tryingtoreconnect === true) { - setInterval(() => { reconnectplease(); }, 10000); - state.tryingtoreconnect = false; - } if (type === connectionLost.type) { return { ...state, @@ -54,10 +35,5 @@ export const gameReducer = (state = initialState, action) => { connectionLostAt: null, }; } - let reconnectplease = function () { - if (state.reconnected === false) { - Byond.command('.reconnect'); - } - }; return state; }; diff --git a/tgui/packages/tgui-panel/index.js b/tgui/packages/tgui-panel/index.js index 020b7b59b329..721fa97fb50c 100644 --- a/tgui/packages/tgui-panel/index.js +++ b/tgui/packages/tgui-panel/index.js @@ -68,20 +68,11 @@ const setupApp = () => { setupPanelFocusHacks(); captureExternalLinks(); - // Subscribe for Redux state updates + // Re-render UI on store updates store.subscribe(renderApp); - // Subscribe for bankend updates - window.update = msg => store.dispatch(Byond.parseJson(msg)); - - // Process the early update queue - while (true) { - const msg = window.__updateQueue__.shift(); - if (!msg) { - break; - } - window.update(msg); - } + // Dispatch incoming messages as store actions + Byond.subscribe((type, payload) => store.dispatch({ type, payload })); // Unhide the panel Byond.winset('output', { @@ -94,6 +85,13 @@ const setupApp = () => { 'size': '0x0', }); + // Resize the panel to match the non-browser output + Byond.winget('output').then(output => { + Byond.winset('browseroutput', { + 'size': output.size, + }); + }); + // Enable hot module reloading if (module.hot) { setupHotReloading(); diff --git a/tgui/packages/tgui-panel/ping/middleware.js b/tgui/packages/tgui-panel/ping/middleware.js index fdd841095719..2d607c841740 100644 --- a/tgui/packages/tgui-panel/ping/middleware.js +++ b/tgui/packages/tgui-panel/ping/middleware.js @@ -4,13 +4,13 @@ * @license MIT */ -import { sendMessage } from 'tgui/backend'; import { pingFail, pingSuccess } from './actions'; import { PING_INTERVAL, PING_QUEUE_SIZE, PING_TIMEOUT } from './constants'; export const pingMiddleware = store => { let initialized = false; let index = 0; + let interval; const pings = []; const sendPing = () => { for (let i = 0; i < PING_QUEUE_SIZE; i++) { @@ -22,19 +22,22 @@ export const pingMiddleware = store => { } const ping = { index, sentAt: Date.now() }; pings[index] = ping; - sendMessage({ - type: 'ping', - payload: { index }, - }); + Byond.sendMessage('ping', { index }); index = (index + 1) % PING_QUEUE_SIZE; }; return next => action => { const { type, payload } = action; if (!initialized) { initialized = true; - setInterval(sendPing, PING_INTERVAL); + interval = setInterval(sendPing, PING_INTERVAL); sendPing(); } + if (type === 'roundrestart') { + // Stop pinging because dreamseeker is currently reconnecting. + // Topic calls in the middle of reconnect will crash the connection. + clearInterval(interval); + return next(action); + } if (type === 'pingReply') { const { index } = payload; const ping = pings[index]; diff --git a/tgui/packages/tgui-panel/settings/SettingsPanel.js b/tgui/packages/tgui-panel/settings/SettingsPanel.js index d449fd3e7a68..01df419ce21f 100644 --- a/tgui/packages/tgui-panel/settings/SettingsPanel.js +++ b/tgui/packages/tgui-panel/settings/SettingsPanel.js @@ -23,17 +23,13 @@ export const SettingsPanel = (props, context) => {
- {SETTINGS_TABS.map((tab) => ( + {SETTINGS_TABS.map(tab => ( - dispatch( - changeSettingsTab({ - tabId: tab.id, - }) - ) - }> + onClick={() => dispatch(changeSettingsTab({ + tabId: tab.id, + }))}> {tab.name} ))} @@ -41,8 +37,12 @@ export const SettingsPanel = (props, context) => {
- {activeTab === 'general' && } - {activeTab === 'chatPage' && } + {activeTab === 'general' && ( + + )} + {activeTab === 'chatPage' && ( + + )} ); @@ -60,7 +60,7 @@ export const SettingsGeneral = (props, context) => { matchCase, } = useSelector(context, selectSettings); const dispatch = useDispatch(context); - const [freeFont, setFreeFont] = useLocalState(context, 'freeFont', false); + const [freeFont, setFreeFont] = useLocalState(context, "freeFont", false); return (
@@ -68,48 +68,34 @@ export const SettingsGeneral = (props, context) => { - dispatch( - updateSettings({ - theme: value, - }) - ) - } - /> + onSelected={value => dispatch(updateSettings({ + theme: value, + }))} /> - {(!freeFont && ( + {!freeFont && ( - dispatch( - updateSettings({ - fontFamily: value, - }) - ) - } - /> - )) || ( + onSelected={value => dispatch(updateSettings({ + fontFamily: value, + }))} /> + ) || ( - dispatch( - updateSettings({ - fontFamily: value, - }) - ) - } + onChange={(e, value) => dispatch(updateSettings({ + fontFamily: value, + }))} /> )}