diff --git a/code/__DEFINES/DNA.dm b/code/__DEFINES/DNA.dm
index 51186e53023..e1db1f07d85 100644
--- a/code/__DEFINES/DNA.dm
+++ b/code/__DEFINES/DNA.dm
@@ -137,31 +137,29 @@
#define LIPS 5
#define NOBLOOD 6
#define NOTRANSSTING 7
-/// Used if we want the mutant colour to be only used by mutant bodyparts. Don't combine this with MUTCOLORS, or it will be useless.
-#define MUTCOLORS_PARTSONLY 8
-#define NOZOMBIE 9
+#define NOZOMBIE 8
/// Uses weird leg sprites. Optional for Lizards, required for ashwalkers. Don't give it to other races unless you make sprites for this (see human_parts_greyscale.dmi)
-#define DIGITIGRADE 10
-#define NO_UNDERWEAR 11
-#define NOSTOMACH 12
-#define NO_DNA_COPY 13
-#define DRINKSBLOOD 14
+#define DIGITIGRADE 9
+#define NO_UNDERWEAR 10
+#define NOSTOMACH 11
+#define NO_DNA_COPY 12
+#define DRINKSBLOOD 13
/// Use this if you want to change the race's color without the player being able to pick their own color. AKA special color shifting
-#define DYNCOLORS 15
-#define AGENDER 16
+#define DYNCOLORS 14
+#define AGENDER 15
/// Do not draw eyes or eyeless overlay
-#define NOEYESPRITES 17
+#define NOEYESPRITES 16
/// Used for determining which wounds are applicable to this species.
/// if we have flesh (can suffer slash/piercing/burn wounds, requires they don't have NOBLOOD)
-#define HAS_FLESH 18
+#define HAS_FLESH 17
/// if we have bones (can suffer bone wounds)
-#define HAS_BONE 19
+#define HAS_BONE 18
///If we have a limb-specific overlay sprite
-#define HAS_MARKINGS 20
+#define HAS_MARKINGS 19
/// Do not draw blood overlay
-#define NOBLOODOVERLAY 21
+#define NOBLOODOVERLAY 20
///No augments, for monkeys in specific because they will turn into fucking freakazoids https://cdn.discordapp.com/attachments/326831214667235328/791313258912153640/102707682-fa7cad80-4294-11eb-8f13-8c689468aeb0.png
-#define NOAUGMENTS 22
+#define NOAUGMENTS 21
//organ slots
#define ORGAN_SLOT_ADAMANTINE_RESONATOR "adamantine_resonator"
diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm
index 4fda5876be8..790106349fe 100644
--- a/code/__DEFINES/admin.dm
+++ b/code/__DEFINES/admin.dm
@@ -103,8 +103,6 @@
#define MAX_KEYPRESS_COMMANDLENGTH 16
///Maximum keys that can be bound to one button
#define MAX_COMMANDS_PER_KEY 5
-///Maximum keys per keybind
-#define MAX_KEYS_PER_KEYBIND 3
///Max amount of keypress messages per second over two seconds before client is autokicked
#define MAX_KEYPRESS_AUTOKICK 50
///Length of held key buffer
diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm
index aaa0ae4d368..c5bcd2eac2c 100644
--- a/code/__DEFINES/antagonists.dm
+++ b/code/__DEFINES/antagonists.dm
@@ -189,3 +189,6 @@ GLOBAL_LIST_INIT(normal_employers, list(
/// Checks if the given mob is a head revolutionary.
#define IS_HEAD_REVOLUTIONARY(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/rev/head))
+
+/// The dimensions of the antagonist preview icon. Will be scaled to this size.
+#define ANTAGONIST_PREVIEW_ICON_SIZE 96
diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm
index 664b8c0c0d8..bd20ba610c1 100644
--- a/code/__DEFINES/colors.dm
+++ b/code/__DEFINES/colors.dm
@@ -195,3 +195,6 @@
#define COLOR_PRIDE_GREEN "#41FC66"
#define COLOR_PRIDE_BLUE "#42FFF2"
#define COLOR_PRIDE_PURPLE "#5D5DFC"
+
+/// The default color for admin say, used as a fallback when the preference is not enabled
+#define DEFAULT_ASAY_COLOR COLOR_MOSTLY_PURE_RED
diff --git a/code/__DEFINES/food.dm b/code/__DEFINES/food.dm
index a9f95b0772a..a877c968685 100644
--- a/code/__DEFINES/food.dm
+++ b/code/__DEFINES/food.dm
@@ -15,6 +15,25 @@
#define CLOTH (1<<14)
#define NUTS (1<<15)
+/// 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/menu.dm b/code/__DEFINES/menu.dm
deleted file mode 100644
index 60a7a2379c1..00000000000
--- a/code/__DEFINES/menu.dm
+++ /dev/null
@@ -1,3 +0,0 @@
-#define CHECKBOX_NONE 0
-#define CHECKBOX_GROUP 1
-#define CHECKBOX_TOGGLE 2
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 3ea5029c5f5..6d79862cb24 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -217,34 +217,21 @@ GLOBAL_LIST_EMPTY(bloody_footprints_cache)
#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_ACCS_NONE "Default sprites"
+#define GHOST_ACCS_DIR "Only directional sprites"
+#define GHOST_ACCS_FULL "Full accessories"
#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.
-
-#define GHOST_OTHERS_SIMPLE 1
-#define GHOST_OTHERS_DEFAULT_SPRITE 50
-#define GHOST_OTHERS_THEIR_SETTING 100
-
-#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_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"
@@ -398,7 +385,6 @@ GLOBAL_LIST_INIT(pda_styles, sortList(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"
#define DUMMY_HUMAN_SLOT_CTF "dummy_ctf_preview_generation"
diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm
index 9a24f9b3d10..fa17b408598 100644
--- a/code/__DEFINES/preferences.dm
+++ b/code/__DEFINES/preferences.dm
@@ -1,12 +1,12 @@
-//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)
#define SOUND_LOBBY (1<<3)
#define MEMBER_PUBLIC (1<<4)
-// #define INTENT_STYLE (1<<5) - intents no longer exist
-#define MIDROUND_ANTAG (1<<6)
#define SOUND_INSTRUMENTS (1<<7)
#define SOUND_SHIP_AMBIENCE (1<<8)
#define SOUND_PRAYERS (1<<9)
@@ -25,15 +25,16 @@
#define SOUND_COMBATMODE (1<<22)
#define SPLIT_ADMIN_TABS (1<<23)
-#define TOGGLES_DEFAULT (SOUND_ADMINHELP|SOUND_MIDI|SOUND_AMBIENCE|SOUND_LOBBY|SOUND_ENDOFROUND|MEMBER_PUBLIC|MIDROUND_ANTAG|SOUND_INSTRUMENTS|SOUND_SHIP_AMBIENCE|SOUND_PRAYERS|SOUND_ANNOUNCEMENTS|SOUND_COMBATMODE)
+#define TOGGLES_DEFAULT (SOUND_ADMINHELP|SOUND_MIDI|SOUND_AMBIENCE|SOUND_LOBBY|SOUND_ENDOFROUND|MEMBER_PUBLIC|SOUND_INSTRUMENTS|SOUND_SHIP_AMBIENCE|SOUND_PRAYERS|SOUND_ANNOUNCEMENTS|SOUND_COMBATMODE)
-//Chat toggles
+// Legacy chat toggles.
+// !!! DO NOT ADD ANY NEW ONES HERE !!!
+// Use `/datum/preference/toggle` instead.
#define CHAT_OOC (1<<0)
#define CHAT_DEAD (1<<1)
#define CHAT_GHOSTEARS (1<<2)
#define CHAT_GHOSTSIGHT (1<<3)
#define CHAT_PRAYER (1<<4)
-#define CHAT_RADIO (1<<5)
#define CHAT_PULLR (1<<6)
#define CHAT_GHOSTWHISPER (1<<7)
#define CHAT_GHOSTPDA (1<<8)
@@ -42,19 +43,13 @@
#define CHAT_GHOSTLAWS (1<<11)
#define CHAT_LOGIN_LOGOUT (1<<12)
-#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|CHAT_GHOSTLAWS|CHAT_LOGIN_LOGOUT)
+#define TOGGLES_DEFAULT_CHAT (CHAT_OOC|CHAT_DEAD|CHAT_GHOSTEARS|CHAT_GHOSTSIGHT|CHAT_PRAYER|CHAT_PULLR|CHAT_GHOSTWHISPER|CHAT_GHOSTPDA|CHAT_GHOSTRADIO|CHAT_BANKCARD|CHAT_GHOSTLAWS|CHAT_LOGIN_LOGOUT)
-#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 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 PARALLAX_INSANE "Insane"
+#define PARALLAX_HIGH "High"
+#define PARALLAX_MED "Medium"
+#define PARALLAX_LOW "Low"
+#define PARALLAX_DISABLE "Disabled"
#define SCALING_METHOD_NORMAL "normal"
#define SCALING_METHOD_DISTORT "distort"
@@ -105,47 +100,48 @@
#define JP_HIGH 3
//randomised elements
-#define RANDOM_HARDCORE "random_hardcore"
-#define RANDOM_NAME "random_name"
-#define RANDOM_NAME_ANTAG "random_name_antag"
-#define RANDOM_BODY "random_body"
-#define RANDOM_BODY_ANTAG "random_body_antag"
-#define RANDOM_SPECIES "random_species"
-#define RANDOM_GENDER "random_gender"
-#define RANDOM_GENDER_ANTAG "random_gender_antag"
-#define RANDOM_AGE "random_age"
-#define RANDOM_AGE_ANTAG "random_age_antag"
-#define RANDOM_UNDERWEAR "random_underwear"
-#define RANDOM_UNDERWEAR_COLOR "random_underwear_color"
-#define RANDOM_UNDERSHIRT "random_undershirt"
-#define RANDOM_SOCKS "random_socks"
-#define RANDOM_BACKPACK "random_backpack"
-#define RANDOM_JUMPSUIT_STYLE "random_jumpsuit_style"
-#define RANDOM_HAIRSTYLE "random_hairstyle"
-#define RANDOM_HAIR_COLOR "random_hair_color"
-#define RANDOM_FACIAL_HAIR_COLOR "random_facial_hair_color"
-#define RANDOM_FACIAL_HAIRSTYLE "random_facial_hairstyle"
-#define RANDOM_SKIN_TONE "random_skin_tone"
-#define RANDOM_EYE_COLOR "random_eye_color"
+#define RANDOM_ANTAG_ONLY 1
+#define RANDOM_DISABLED 2
+#define RANDOM_ENABLED 3
//recommened client FPS
#define RECOMMENDED_FPS 40
// randomise_appearance_prefs() and randomize_human_appearance() proc flags
-#define RANDOMIZE_GENDER (1<<0)
-#define RANDOMIZE_SPECIES (1<<1)
-#define RANDOMIZE_NAME (1<<2)
-#define RANDOMIZE_AGE (1<<3)
-#define RANDOMIZE_UNDERWEAR (1<<4)
-#define RANDOMIZE_UNDERWEAR_COLOR (1<<5)
-#define RANDOMIZE_UNDERSHIRT (1<<6)
-#define RANDOMIZE_SOCKS (1<<7)
-#define RANDOMIZE_BACKPACK (1<<8)
-#define RANDOMIZE_JUMPSUIT_STYLE (1<<9)
-#define RANDOMIZE_HAIRSTYLE (1<<10)
-#define RANDOMIZE_FACIAL_HAIRSTYLE (1<<11)
-#define RANDOMIZE_HAIR_COLOR (1<<12)
-#define RANDOMIZE_FACIAL_HAIR_COLOR (1<<13)
-#define RANDOMIZE_SKIN_TONE (1<<14)
-#define RANDOMIZE_EYE_COLOR (1<<15)
-#define RANDOMIZE_FEATURES (1<<16)
+#define RANDOMIZE_SPECIES (1<<0)
+#define RANDOMIZE_NAME (1<<1)
+
+// 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 e1344c904cb..a9a4db01f63 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -6,31 +6,54 @@
//These are synced with the Database, if you change the values of the defines
//then you MUST update the database!
-#define ROLE_SYNDICATE "Syndicate"
-#define ROLE_TRAITOR "Traitor"
-#define ROLE_OPERATIVE "Operative"
-#define ROLE_CHANGELING "Changeling"
-#define ROLE_WIZARD "Wizard"
-#define ROLE_MALF "Malf AI"
-#define ROLE_REV "Revolutionary"
-#define ROLE_REV_HEAD "Head Revolutionary"
-#define ROLE_REV_SUCCESSFUL "Victorious Revolutionary"
-#define ROLE_ALIEN "Xenomorph"
-#define ROLE_PAI "pAI"
-#define ROLE_CULTIST "Cultist"
-#define ROLE_HERETIC "Heretic"
-#define ROLE_BLOB "Blob"
-#define ROLE_NINJA "Space Ninja"
-#define ROLE_MONKEY "Monkey"
-#define ROLE_MONKEY_HELMET "Monkey Mind Magnification Helmet"
-#define ROLE_ABDUCTOR "Abductor"
-#define ROLE_REVENANT "Revenant"
+
+// Roundstart roles
#define ROLE_BROTHER "Blood Brother"
+#define ROLE_CHANGELING "Changeling"
+#define ROLE_CULTIST "Cultist"
+#define ROLE_FAMILIES "Gangster"
+#define ROLE_HERETIC "Heretic"
+#define ROLE_MALF "Malf AI"
+#define ROLE_MONKEY "Monkey"
+#define ROLE_OPERATIVE "Operative"
+#define ROLE_TRAITOR "Traitor"
+#define ROLE_WIZARD "Wizard"
+
+// Midround roles
+#define ROLE_ABDUCTOR "Abductor"
+#define ROLE_ALIEN "Xenomorph"
+#define ROLE_BLOB "Blob"
+#define ROLE_BLOB_INFECTION "Blob Infection"
+#define ROLE_FAMILY_HEAD_ASPIRANT "Family Head Aspirant"
+#define ROLE_LONE_OPERATIVE "Lone Operative"
+#define ROLE_MALF_MIDROUND "Malf AI (Midround)"
+#define ROLE_NIGHTMARE "Nightmare"
+#define ROLE_NINJA "Space Ninja"
+#define ROLE_REV_HEAD "Head Revolutionary"
+#define ROLE_OBSESSED "Obsessed"
+#define ROLE_OPERATIVE_MIDROUND "Operative (Midround)"
+#define ROLE_SENTIENT_DISEASE "Sentient Disease"
+#define ROLE_SLEEPER_AGENT "Syndicate Sleeper Agent"
+#define ROLE_SPACE_DRAGON "Space Dragon"
+#define ROLE_SPIDER "Spider"
+#define ROLE_SWARMER "Swarmer"
+#define ROLE_WIZARD_MIDROUND "Wizard (Midround)"
+
+// Latejoin roles
+#define ROLE_HERETIC_SMUGGLER "Heretic Smuggler"
+#define ROLE_PROVOCATEUR "Provocateur"
+#define ROLE_SYNDICATE_INFILTRATOR "Syndicate Infiltrator"
+
+// Other roles
+#define ROLE_SYNDICATE "Syndicate"
+#define ROLE_REV "Revolutionary"
+#define ROLE_REV_SUCCESSFUL "Victorious Revolutionary"
+#define ROLE_PAI "pAI"
+#define ROLE_MONKEY_HELMET "Monkey Mind Magnification Helmet"
+#define ROLE_REVENANT "Revenant"
#define ROLE_BRAINWASHED "Brainwashed Victim"
#define ROLE_OVERTHROW "Syndicate Mutineer" //Role removed, left here for safety.
#define ROLE_HIVE "Hivemind Host" //Role removed, left here for safety.
-#define ROLE_OBSESSED "Obsessed"
-#define ROLE_SPACE_DRAGON "Space Dragon"
#define ROLE_SENTIENCE "Sentience Potion Spawn"
#define ROLE_PYROCLASTIC_SLIME "Pyroclastic Anomaly Slime"
#define ROLE_MIND_TRANSFER "Mind Transfer Potion"
@@ -39,25 +62,20 @@
#define ROLE_DEATHSQUAD "Deathsquad"
#define ROLE_LAVALAND "Lavaland"
#define ROLE_INTERNAL_AFFAIRS "Internal Affairs Agent"
-#define ROLE_FAMILIES "Familes Antagonists"
#define ROLE_POSITRONIC_BRAIN "Positronic Brain"
#define ROLE_FREE_GOLEM "Free Golem"
#define ROLE_SERVANT_GOLEM "Servant Golem"
#define ROLE_NUCLEAR_OPERATIVE "Nuclear Operative"
#define ROLE_CLOWN_OPERATIVE "Clown Operative"
-#define ROLE_LONE_OPERATIVE "Lone Operative"
-#define ROLE_NIGHTMARE "Nightmare"
#define ROLE_WIZARD_APPRENTICE "apprentice"
#define ROLE_SLAUGHTER_DEMON "Slaughter Demon"
-#define ROLE_SENTIENT_DISEASE "Sentient Disease"
#define ROLE_MORPH "Morph"
#define ROLE_SANTA "Santa"
#define ROLE_FUGITIVE "Fugitive"
//Spawner roles
#define ROLE_GHOST_ROLE "Ghost Role"
-#define ROLE_SPIDER "Spider"
#define ROLE_EXILE "Exile"
#define ROLE_FUGITIVE_HUNTER "Fugitive Hunter"
#define ROLE_ESCAPED_PRISONER "Escaped Prisoner"
@@ -85,27 +103,48 @@
/// Keys are the antagonist, values are the number of days since the player's
/// first connection in order to play.
GLOBAL_LIST_INIT(special_roles, list(
- ROLE_TRAITOR = 0,
+ // Roundstart
ROLE_BROTHER = 0,
- ROLE_OPERATIVE = 14,
ROLE_CHANGELING = 0,
- ROLE_WIZARD = 14,
- ROLE_MALF = 0,
- ROLE_REV = 14,
- ROLE_ALIEN = 0,
- ROLE_PAI = 0,
+ ROLE_CLOWN_OPERATIVE = 14,
ROLE_CULTIST = 14,
- ROLE_BLOB = 0,
- ROLE_NINJA = 0,
- ROLE_OBSESSED = 0,
- ROLE_SPACE_DRAGON = 0,
- ROLE_MONKEY = 0,
- ROLE_REVENANT = 0,
- ROLE_ABDUCTOR = 0,
- ROLE_INTERNAL_AFFAIRS = 0,
- ROLE_SENTIENCE = 0,
ROLE_FAMILIES = 0,
ROLE_HERETIC = 0,
+ ROLE_MALF = 0,
+ ROLE_MONKEY = 0,
+ ROLE_OPERATIVE = 14,
+ ROLE_REV_HEAD = 14,
+ ROLE_TRAITOR = 0,
+ ROLE_WIZARD = 14,
+
+ // Midround
+ ROLE_ABDUCTOR = 0,
+ ROLE_ALIEN = 0,
+ ROLE_BLOB = 0,
+ ROLE_BLOB_INFECTION = 0,
+ ROLE_FAMILY_HEAD_ASPIRANT = 0,
+ ROLE_LONE_OPERATIVE = 14,
+ ROLE_MALF_MIDROUND = 0,
+ ROLE_NINJA = 0,
+ ROLE_OBSESSED = 0,
+ ROLE_OPERATIVE_MIDROUND = 14,
+ ROLE_REVENANT = 0,
+ ROLE_SENTIENT_DISEASE = 0,
+ ROLE_SLEEPER_AGENT = 0,
+ ROLE_SPACE_DRAGON = 0,
+ ROLE_SPIDER = 0,
+ ROLE_SWARMER = 0,
+
+ // Latejoin
+ ROLE_HERETIC_SMUGGLER = 0,
+ ROLE_PROVOCATEUR = 14,
+ ROLE_SYNDICATE_INFILTRATOR = 0,
+
+ // I'm not too sure why these are here, but they're not moving.
+ ROLE_REV = 14,
+ ROLE_PAI = 0,
+ ROLE_INTERNAL_AFFAIRS = 0,
+ ROLE_SENTIENCE = 0,
))
//Job defines for what happens when you fail to qualify for any job during job selection
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 005833a3905..c7fd8fab66b 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -119,16 +119,17 @@
#define INIT_ORDER_ACHIEVEMENTS 77
#define INIT_ORDER_RESEARCH 75
#define INIT_ORDER_STATION 74 //This is high priority because it manipulates a lot of the subsystems that will initialize after it.
+#define INIT_ORDER_QUIRKS 73
+#define INIT_ORDER_REAGENTS 72 //HAS to be before mapping and assets - both create objects, which creates reagents, which relies on lists made in this subsystem
#define INIT_ORDER_EVENTS 70
#define INIT_ORDER_IDACCESS 66
#define INIT_ORDER_JOBS 65 // Must init before atoms, to set up properly the dynamic job lists.
-#define INIT_ORDER_QUIRKS 60
#define INIT_ORDER_AI_MOVEMENT 56 //We need the movement setup
#define INIT_ORDER_AI_CONTROLLERS 55 //So the controller can get the ref
#define INIT_ORDER_TICKER 55
#define INIT_ORDER_TCG 55
-#define INIT_ORDER_REAGENTS 55 //HAS to be before mapping - mapping creates objects, which creates reagents, which relies on lists made in this subsystem
#define INIT_ORDER_MAPPING 50
+#define INIT_ORDER_EARLY_ASSETS 48
#define INIT_ORDER_TIMETRACK 47
#define INIT_ORDER_NETWORKS 45
#define INIT_ORDER_ECONOMY 40
@@ -142,7 +143,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
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 900e8b51f85..e5b85207422 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -737,3 +737,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_MAGNETIC_ID_CARD "magnetic_id_card"
/// Traits granted to items due to their chameleon properties.
#define CHAMELEON_ITEM_TRAIT "chameleon_item_trait"
+
+/// This human wants to see the color of their glasses, for some reason
+#define TRAIT_SEE_GLASS_COLORS "see_glass_colors"
diff --git a/modular_skyrat/modules/customization/__DEFINES/augment.dm b/code/__DEFINES/~skyrat_defines/augment.dm
similarity index 100%
rename from modular_skyrat/modules/customization/__DEFINES/augment.dm
rename to code/__DEFINES/~skyrat_defines/augment.dm
diff --git a/code/__DEFINES/~skyrat_defines/chat.dm b/code/__DEFINES/~skyrat_defines/chat.dm
new file mode 100644
index 00000000000..307d1b798f3
--- /dev/null
+++ b/code/__DEFINES/~skyrat_defines/chat.dm
@@ -0,0 +1,5 @@
+
+#define MESSAGE_TYPE_MENTOR "mentor"
+
+/// Adds a generic box around whatever message you're sending in chat. Really makes things stand out.
+#define examine_block(str) ("
" + str + "
")
diff --git a/code/__DEFINES/~skyrat_defines/loadouts.dm b/code/__DEFINES/~skyrat_defines/loadouts.dm
new file mode 100644
index 00000000000..1d9aa2ca9f4
--- /dev/null
+++ b/code/__DEFINES/~skyrat_defines/loadouts.dm
@@ -0,0 +1,28 @@
+/// Defines for what loadout slot a corresponding item belongs to.
+#define LOADOUT_ITEM_BELT "belt"
+#define LOADOUT_ITEM_EARS "ears"
+#define LOADOUT_ITEM_GLASSES "glasses"
+#define LOADOUT_ITEM_GLOVES "gloves"
+#define LOADOUT_ITEM_HEAD "head"
+#define LOADOUT_ITEM_MASK "mask"
+#define LOADOUT_ITEM_NECK "neck"
+#define LOADOUT_ITEM_SHOES "shoes"
+#define LOADOUT_ITEM_SUIT "suit"
+#define LOADOUT_ITEM_UNIFORM "under"
+#define LOADOUT_ITEM_ACCESSORY "accessory"
+#define LOADOUT_ITEM_INHAND "inhand_items"
+#define LOADOUT_ITEM_MISC "pocket_items"
+#define LOADOUT_ITEM_TOYS "toys"
+
+#define INFO_GREYSCALE "greyscale"
+#define INFO_NAMED "name"
+
+/// Max amonut of misc / backpack items that are allowed.
+#define MAX_ALLOWED_MISC_ITEMS 3
+
+/// Defines for extra info blurbs, for loadout items.
+#define TOOLTIP_NO_ARMOR "ARMORLESS - This item has no armor and is entirely cosmetic."
+#define TOOLTIP_NO_DAMAGE "CEREMONIAL - This item has very low force and is cosmetic."
+#define TOOLTIP_RANDOM_COLOR "RANDOM COLOR -This item has a random color and will change every round."
+#define TOOLTIP_GREYSCALE "GREYSCALED - This item can be customized via the greyscale modification UI."
+#define TOOLTIP_RENAMABLE "RENAMABLE - This item can be given a custom name."
diff --git a/code/__DEFINES/~skyrat_defines/preferences.dm b/code/__DEFINES/~skyrat_defines/preferences.dm
deleted file mode 100644
index 203dad844c9..00000000000
--- a/code/__DEFINES/~skyrat_defines/preferences.dm
+++ /dev/null
@@ -1,16 +0,0 @@
-//Skyrat toggles
-#define CHAT_LOOC (1<<0) //UNUSED BIT SLOT, could still be in savefiles. Feel free to replace it when you add a new one
-#define CHAT_LOOC_ADMIN (1<<1)
-#define APHRO_PREF (1<<2)
-#define CUMFACE_PREF (1<<3)
-#define ADMINDEL_ZAP_PREF (1<<4)
-
-#define TOGGLES_DEFAULT_SKYRAT (CHAT_LOOC_ADMIN|APHRO_PREF|CUMFACE_PREF)
-
-//SKYRAT ADDITION - ERP UPDATE
-#define BREAST_ENLARGEMENT (2<<3)
-#define PENIS_ENLARGEMENT (2<<4)
-#define FORCED_FEM (2<<5)
-#define FORCED_MALE (2<<6)
-#define BIMBO_PREF (2<<7)
-//SKYRAT ADDITION END
diff --git a/code/__DEFINES/~skyrat_defines/role_preferences.dm b/code/__DEFINES/~skyrat_defines/role_preferences.dm
new file mode 100644
index 00000000000..faf440262f0
--- /dev/null
+++ b/code/__DEFINES/~skyrat_defines/role_preferences.dm
@@ -0,0 +1 @@
+#define ROLE_LONE_INFILTRATOR "Lone Infiltrator"
diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm
index 329b422a1b8..36c873e87c8 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -554,11 +554,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/admin.dm b/code/__HELPERS/admin.dm
new file mode 100644
index 00000000000..94f61ef0152
--- /dev/null
+++ b/code/__HELPERS/admin.dm
@@ -0,0 +1,3 @@
+/// Returns if the given client is an admin, REGARDLESS of if they're deadminned or not.
+/proc/is_admin(client/client)
+ return !isnull(GLOB.admin_datums[client.ckey]) || !isnull(GLOB.deadmins[client.ckey])
diff --git a/code/__HELPERS/colors.dm b/code/__HELPERS/colors.dm
new file mode 100644
index 00000000000..5847c30aff5
--- /dev/null
+++ b/code/__HELPERS/colors.dm
@@ -0,0 +1,19 @@
+/// Given a color in the format of "#RRGGBB", will return if the color
+/// is dark.
+/proc/is_color_dark(color, threshold = 0.25)
+ var/list/rgb = hex2rgb(color)
+ var/list/hsl = rgb2hsl(rgb[1], rgb[2], rgb[3])
+ 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 afdac907dd0..9195a071b31 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -512,7 +512,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")
@@ -528,7 +528,7 @@
A = A.loc
return A.loc
-/* - SKYRAT EDIT MOVAL - MOVED TO ALTTITLEPREFS
+
/proc/AnnounceArrival(mob/living/carbon/human/character, rank)
if(!SSticker.IsRoundInProgress() || QDELETED(character))
return
@@ -543,7 +543,7 @@
var/obj/machinery/announcement_system/announcer = pick(GLOB.announcement_systems)
announcer.announce("ARRIVAL", character.real_name, rank, list()) //make the list empty to make it announce it in common
-*/
+
/proc/lavaland_equipment_pressure_check(turf/T)
. = FALSE
if(!istype(T))
diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm
index e979556e747..280bbf50da1 100644
--- a/code/__HELPERS/global_lists.dm
+++ b/code/__HELPERS/global_lists.dm
@@ -30,7 +30,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_antennae, GLOB.moth_antennae_list)
diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm
index ef7520bc68c..d10dc1b5b1a 100644
--- a/code/__HELPERS/icons.dm
+++ b/code/__HELPERS/icons.dm
@@ -991,7 +991,7 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
if(job)
body.dna.species.pre_equip_species_outfit(job, body, TRUE)
if(outfit)
- body.equipOutfit(outfit, TRUE)
+ body.equip_outfit_and_loadout(outfit, prefs, TRUE) //SKYRAT EDIT CHANGE
var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing")
COMPILE_OVERLAYS(body)
@@ -1053,12 +1053,7 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
dummySave = null
fdel("tmp/dummySave.sav") //if you get the idea to try and make this more optimized, make sure to still call unlock on the savefile after every write to unlock it.
-/proc/icon2html(thing, target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null, override_skyrat = FALSE) /// SKYRAT EDIT - icon2html override instead of fully disabling it - Original: /proc/icon2html(thing, target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null)
- // SKYRAT EDIT START - icon2html override instead of fully disabling it
- if(!override_skyrat)
- return "" //SKYRAT EDIT DISABLE - ICON2HTML
- // SKYRAT EDIT END
-
+/proc/icon2html(thing, target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null)
if (!thing)
return
if(SSlag_switch.measures[DISABLE_USR_ICON2HTML] && usr && !HAS_TRAIT(usr, TRAIT_BYPASS_MEASURES))
@@ -1163,8 +1158,6 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
//Costlier version of icon2html() that uses getFlatIcon() to account for overlays, underlays, etc. Use with extreme moderation, ESPECIALLY on mobs.
/proc/costly_icon2html(thing, target, sourceonly = FALSE)
- return ""
- /* SKYRAT EDIT REMOVAL
if (!thing)
return
if(SSlag_switch.measures[DISABLE_USR_ICON2HTML] && usr && !HAS_TRAIT(usr, TRAIT_BYPASS_MEASURES))
@@ -1175,7 +1168,6 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
var/icon/I = getFlatIcon(thing)
return icon2html(I, target, sourceonly = sourceonly)
-*/
GLOBAL_LIST_EMPTY(transformation_animation_objects)
diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm
index a44c895c8f9..0dd6b6fddaf 100644
--- a/code/__HELPERS/mobs.dm
+++ b/code/__HELPERS/mobs.dm
@@ -159,6 +159,22 @@ GLOBAL_LIST_INIT(skin_tones, sortList(list(
"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",
+))
+
+/// An assoc list of species IDs to type paths
GLOBAL_LIST_EMPTY(species_list)
/proc/age2agedescription(age)
diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm
index f54cb24ded6..35a27b76d24 100644
--- a/code/__HELPERS/roundend.dm
+++ b/code/__HELPERS/roundend.dm
@@ -278,8 +278,10 @@
CHECK_TICK
SSdbcore.SetRoundEnd()
+
//Collects persistence features
SSpersistence.collect_data()
+ SSpersistent_paintings.save_paintings()
//stop collecting feedback during grifftime
SSblackbox.Seal()
diff --git a/code/__HELPERS/sanitize_values.dm b/code/__HELPERS/sanitize_values.dm
index 7cc24570a36..c7758e1f801 100644
--- a/code/__HELPERS/sanitize_values.dm
+++ b/code/__HELPERS/sanitize_values.dm
@@ -99,5 +99,5 @@
return default ? default : crunch + repeat_string(desired_format, "0")
/// Makes sure the input color is text with a # at the start followed by 6 hexadecimal characters. Examples: "#ff1234", "#A38321", COLOR_GREEN_GRAY
-/proc/sanitize_ooccolor(color)
+/proc/sanitize_color(color)
return findtext(color, GLOB.is_color) ? color : GLOB.normal_ooc_colour
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index ad76c7f5a0a..fb3b5dee06d 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -191,7 +191,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
@@ -202,20 +202,11 @@ Turf and target are separate in case you want to teleport some distance from a t
var/random = CONFIG_GET(flag/force_random_names) || (C ? is_banned_from(C.ckey, "Appearance") : FALSE)
while(loop && safety < 5)
- if(!safety && !random && C?.prefs.custom_names[role])
- newname = C.prefs.custom_names[role]
+ if(!safety && !random)
+ newname = C?.prefs?.read_preference(preference_type)
else
- switch(role)
- if(SPECIES_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/_globalvars/lists/client.dm b/code/_globalvars/lists/client.dm
index 5181d870c4f..0e1ed245c2b 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 1d096c65bed..ac038b0ae45 100644
--- a/code/_globalvars/lists/flavor_misc.dm
+++ b/code/_globalvars/lists/flavor_misc.dm
@@ -36,7 +36,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_antennae_list)
GLOBAL_LIST_EMPTY(moth_markings_list)
@@ -164,18 +163,26 @@ GLOBAL_LIST_INIT(ai_core_display_screens, sortList(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, sortList(list(
SEC_DEPT_NONE,
SEC_DEPT_ENGINEERING,
@@ -197,18 +204,12 @@ GLOBAL_LIST_INIT(backpacklist, list(DBACKPACK, DSATCHEL, DDUFFELBAG, GBACKPACK,
//Suit/Skirt
#define PREF_SUIT "Jumpsuit"
#define PREF_SKIRT "Jumpskirt"
-GLOBAL_LIST_INIT(jumpsuitlist, list(PREF_SUIT, PREF_SKIRT))
//Uplink spawn loc
#define UPLINK_PDA "PDA"
#define UPLINK_RADIO "Radio"
#define UPLINK_PEN "Pen" //like a real spy!
#define UPLINK_IMPLANT "Implant"
-#define UPLINK_IMPLANT_WITH_PRICE "[UPLINK_IMPLANT] (-[UPLINK_IMPLANT_TELECRYSTAL_COST] TC)"
-// What we show to the user
-GLOBAL_LIST_INIT(uplink_spawn_loc_list, list(UPLINK_PDA, UPLINK_RADIO, UPLINK_PEN, UPLINK_IMPLANT_WITH_PRICE))
-// What is actually saved; if the uplink implant price changes, it won't affect save files then
-GLOBAL_LIST_INIT(uplink_spawn_loc_list_save, list(UPLINK_PDA, UPLINK_RADIO, UPLINK_PEN, UPLINK_IMPLANT))
//Female Uniforms
GLOBAL_LIST_EMPTY(female_clothing_icons)
diff --git a/code/_globalvars/lists/keybindings.dm b/code/_globalvars/lists/keybindings.dm
index 8e4ce26f37b..882b171ea26 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/names.dm b/code/_globalvars/lists/names.dm
index 3eb87aeca87..5fd4b59b1c3 100644
--- a/code/_globalvars/lists/names.dm
+++ b/code/_globalvars/lists/names.dm
@@ -34,24 +34,3 @@ GLOBAL_LIST_INIT(hive_names, world.file2list("strings/names/hive_names.txt"))
GLOBAL_LIST_INIT(dream_strings, world.file2list("strings/dreamstrings.txt"))
//loaded on startup because of "
//would include in rsc if ' was used
-
-/*
-List of configurable names in preferences and their metadata
-"id" = list(
- "pref_name" = "name", //pref label
- "qdesc" = "name", //popup question text
- "allow_numbers" = FALSE, // numbers allowed in the name
- "group" = "whatever", // group (these will be grouped together on pref ui ,order still follows the list so they need to be concurrent to be grouped)
- "allow_null" = FALSE // if empty name is entered it's replaced with default value
- ),
-*/
-GLOBAL_LIST_INIT(preferences_custom_names, list(
- "human" = list("pref_name" = "Backup Human", "qdesc" = "backup human name, used in the event you are assigned a command role as another species (leave empty to pick random name)", "allow_numbers" = FALSE , "group" = "backup_human", "allow_null" = TRUE),
- "clown" = list("pref_name" = "Clown" , "qdesc" = "clown name (leave empty to pick random name)", "allow_numbers" = FALSE , "group" = "fun", "allow_null" = TRUE),
- "mime" = list("pref_name" = "Mime", "qdesc" = "mime name (leave empty to pick random name)" , "allow_numbers" = FALSE , "group" = "fun", "allow_null" = TRUE),
- "cyborg" = list("pref_name" = "Cyborg", "qdesc" = "cyborg name (leave empty to use default naming scheme)", "allow_numbers" = TRUE , "group" = "silicons", "allow_null" = TRUE),
- "ai" = list("pref_name" = "AI", "qdesc" = "AI name (leave empty to pick random name)", "allow_numbers" = TRUE , "group" = "silicons", "allow_null" = TRUE),
- "religion" = list("pref_name" = "Chaplain religion", "qdesc" = "religion (leave empty to pick random religion)" , "allow_numbers" = TRUE , "group" = "chaplain", "allow_null" = TRUE),
- "deity" = list("pref_name" = "Chaplain deity", "qdesc" = "deity (leave empty to pick default name)", "allow_numbers" = TRUE , "group" = "chaplain", "allow_null" = TRUE),
- "bible" = list("pref_name" = "Chaplain bible name", "qdesc" = "bible name (leave empty to use default naming scheme)", "allow_numbers" = TRUE , "group" = "chaplain", "allow_null" = TRUE)
- ))
diff --git a/code/_globalvars/lists/objects.dm b/code/_globalvars/lists/objects.dm
index c8c69880bef..b0ce422d2b6 100644
--- a/code/_globalvars/lists/objects.dm
+++ b/code/_globalvars/lists/objects.dm
@@ -20,7 +20,7 @@ GLOBAL_LIST_EMPTY(mechpad_list) //list of all /obj/machinery/mechpad
GLOBAL_LIST(chemical_reactions_list) //list of all /datum/chemical_reaction datums indexed by their typepath. Use this for general lookup stuff
GLOBAL_LIST(chemical_reactions_list_reactant_index) //list of all /datum/chemical_reaction datums. Used during chemical reactions. Indexed by REACTANT types
GLOBAL_LIST(chemical_reactions_list_product_index) //list of all /datum/chemical_reaction datums. Used for the reaction lookup UI. Indexed by PRODUCT type
-GLOBAL_LIST(chemical_reagents_list) //list of all /datum/reagent datums indexed by reagent id. Used by chemistry stuff
+GLOBAL_LIST_INIT(chemical_reagents_list, init_chemical_reagent_list()) //list of all /datum/reagent datums indexed by reagent id. Used by chemistry stuff
GLOBAL_LIST(chemical_reactions_results_lookup_list) //List of all reactions with their associated product and result ids. Used for reaction lookups
GLOBAL_LIST(fake_reagent_blacklist) //List of all reagents that are parent types used to define a bunch of children - but aren't used themselves as anything.
GLOBAL_LIST_EMPTY(tech_list) //list of all /datum/tech datums indexed by id.
diff --git a/code/_globalvars/traumas.dm b/code/_globalvars/traumas.dm
new file mode 100644
index 00000000000..8344c2c476f
--- /dev/null
+++ b/code/_globalvars/traumas.dm
@@ -0,0 +1,285 @@
+#define PHOBIA_FILE "phobia.json"
+
+/// Phobia types that can be pulled randomly for brain traumas.
+/// Also determines what phobias you can choose as your preference with the quirk.
+GLOBAL_LIST_INIT(phobia_types, sortList(list(
+ "aliens",
+ "anime",
+ "authority",
+ "birds",
+ "clowns",
+ "doctors",
+ "falling",
+ "greytide",
+ "guns",
+ "insects",
+ "lizards",
+ "robots",
+ "security",
+ "skeletons",
+ "snakes",
+ "space",
+ "spiders",
+ "strangers",
+ "the supernatural",
+)))
+
+GLOBAL_LIST_INIT(phobia_regexes, list(
+ "aliens" = construct_phobia_regex("aliens"),
+ "anime" = construct_phobia_regex("anime"),
+ "authority" = construct_phobia_regex("authority"),
+ "birds" = construct_phobia_regex("birds"),
+ "clowns" = construct_phobia_regex("clowns"),
+ "conspiracies" = construct_phobia_regex("conspiracies"),
+ "doctors" = construct_phobia_regex("doctors"),
+ "falling" = construct_phobia_regex("falling"),
+ "greytide" = construct_phobia_regex("greytide"),
+ "guns" = construct_phobia_regex("guns"),
+ "insects" = construct_phobia_regex("insects"),
+ "lizards" = construct_phobia_regex("lizards"),
+ "ocky icky" = construct_phobia_regex("ocky icky"),
+ "robots" = construct_phobia_regex("robots"),
+ "security" = construct_phobia_regex("security"),
+ "skeletons" = construct_phobia_regex("skeletons"),
+ "snakes" = construct_phobia_regex("snakes"),
+ "space" = construct_phobia_regex("space"),
+ "spiders" = construct_phobia_regex("spiders"),
+ "strangers" = construct_phobia_regex("strangers"),
+ "the supernatural" = construct_phobia_regex("the supernatural"),
+))
+
+GLOBAL_LIST_INIT(phobia_mobs, list(
+ "spiders" = typecacheof(list(/mob/living/simple_animal/hostile/giant_spider)),
+ "security" = typecacheof(list(/mob/living/simple_animal/bot/secbot)),
+ "lizards" = typecacheof(list(/mob/living/simple_animal/hostile/lizard)),
+ "skeletons" = typecacheof(list(/mob/living/simple_animal/hostile/skeleton)),
+ "snakes" = typecacheof(list(/mob/living/simple_animal/hostile/retaliate/snake)),
+ "robots" = typecacheof(list(
+ /mob/living/silicon/ai,
+ /mob/living/silicon/robot,
+ /mob/living/simple_animal/bot,
+ /mob/living/simple_animal/drone,
+ /mob/living/simple_animal/hostile/swarmer,
+ )),
+ "doctors" = typecacheof(list(/mob/living/simple_animal/bot/medbot)),
+ "the supernatural" = typecacheof(list(
+ /mob/living/simple_animal/hostile/construct,
+ /mob/living/simple_animal/revenant,
+ /mob/living/simple_animal/shade,
+ /mob/living/simple_animal/hostile/skeleton,
+ /mob/living/simple_animal/hostile/retaliate/ghost,
+ /mob/living/simple_animal/hostile/imp,
+ /mob/dead/observer,
+ /mob/living/simple_animal/bot/mulebot/paranormal,
+ /mob/living/simple_animal/hostile/retaliate/bat,
+ /mob/living/simple_animal/hostile/dark_wizard,
+ /mob/living/simple_animal/hostile/faithless,
+ /mob/living/simple_animal/hostile/wizard,
+ /mob/living/simple_animal/hostile/zombie,
+ )),
+ "aliens" = typecacheof(list(
+ /mob/living/carbon/alien,
+ /mob/living/simple_animal/slime,
+ )),
+ "conspiracies" = typecacheof(list(
+ /mob/living/simple_animal/bot/secbot,
+ /mob/living/simple_animal/drone,
+ /mob/living/simple_animal/pet/penguin,
+ )),
+ "birds" = typecacheof(list(
+ /mob/living/simple_animal/chick,
+ /mob/living/simple_animal/chicken,
+ /mob/living/simple_animal/parrot,
+ /mob/living/simple_animal/pet/penguin,
+ )),
+ "anime" = typecacheof(list(/mob/living/simple_animal/hostile/guardian)),
+ "insects" = typecacheof(list(
+ /mob/living/basic/cockroach,
+ /mob/living/simple_animal/hostile/bee,
+ )),
+))
+
+GLOBAL_LIST_INIT(phobia_objs, list(
+ "snakes" = typecacheof(list(/obj/item/rod_of_asclepius, /obj/item/toy/plush/snakeplushie)),
+ "spiders" = typecacheof(list(/obj/structure/spider)),
+ "security" = typecacheof(list(
+ /obj/item/clothing/under/rank/security/officer, /obj/item/clothing/under/rank/security/warden,
+ /obj/item/clothing/under/rank/security/head_of_security, /obj/item/clothing/under/rank/security/detective,
+ /obj/item/melee/baton, /obj/item/gun/energy/taser, /obj/item/restraints/handcuffs,
+ /obj/machinery/door/airlock/security, /obj/effect/hallucination/simple/securitron,
+ /obj/item/stamp/hos, /obj/item/megaphone/sec, /obj/item/toy/figure/hos, /obj/item/toy/figure/secofficer,
+ /obj/structure/statue/silver/sec, /obj/structure/statue/silver/secborg, /obj/structure/statue/gold/hos
+ )),
+
+ "clowns" = typecacheof(list(
+ /obj/item/clothing/under/rank/civilian/clown, /obj/item/clothing/shoes/clown_shoes,
+ /obj/item/clothing/mask/gas/clown_hat, /obj/item/instrument/bikehorn,
+ /obj/item/pda/clown, /obj/item/grown/bananapeel, /obj/item/food/cheesiehonkers,
+ /obj/item/trash/cheesie, /obj/item/stack/ore/bananium, /obj/item/stack/tile/mineral/bananium,
+ /obj/structure/falsewall/bananium, /obj/item/stack/ore/bananium, /obj/machinery/door/airlock/bananium,
+ /obj/structure/statue/bananium, /obj/item/toy/crayon/rainbow, /obj/vehicle/sealed/mecha/combat/honker,
+ /obj/item/toy/mecha/honk, /obj/item/toy/mecha/darkhonk,
+ /obj/structure/mecha_wreckage/honker, /obj/item/megaphone/clown, /obj/item/stamp/clown,
+ /obj/item/storage/backpack/clown, /obj/item/storage/backpack/ert/clown, /obj/item/storage/backpack/duffelbag/clown,
+ /obj/item/food/pie/cream, /obj/item/pneumatic_cannon/pie, /obj/item/bedsheet/clown, /obj/vehicle/sealed/car/clowncar,
+ /obj/item/gun/magic/staff/honk, /obj/item/clothing/suit/chaplainsuit/clownpriest, /obj/item/clothing/head/clownmitre,
+ /obj/item/clothing/under/plasmaman/clown
+ )),
+
+ "greytide" = typecacheof(list(/obj/item/clothing/under/color/grey, /obj/item/melee/baton/security/cattleprod,
+ /obj/item/spear, /obj/item/clothing/mask/gas, /obj/item/toy/figure/assistant,
+ /obj/structure/statue/sandstone/assistant
+ )),
+
+ "lizards" = typecacheof(list(/obj/item/toy/plush/lizard_plushie, /obj/item/food/kebab/tail, /obj/item/organ/tail/lizard,
+ /obj/item/reagent_containers/food/drinks/bottle/lizardwine, /obj/item/clothing/head/lizard, /obj/item/clothing/shoes/cowboy/lizard,
+ )),
+
+ "skeletons" = typecacheof(list(/obj/item/organ/tongue/bone, /obj/item/clothing/suit/armor/bone, /obj/item/stack/sheet/bone,
+ /obj/item/food/meat/slab/human/mutant/skeleton,
+ /obj/effect/decal/remains/human,
+ )),
+
+ "conspiracies" = typecacheof(list(/obj/item/clothing/under/rank/captain, /obj/item/clothing/under/rank/security/head_of_security,
+ /obj/item/clothing/under/rank/engineering/chief_engineer, /obj/item/clothing/under/rank/medical/chief_medical_officer,
+ /obj/item/clothing/under/rank/civilian/head_of_personnel, /obj/item/clothing/under/rank/rnd/research_director,
+ /obj/item/clothing/under/rank/security/head_of_security/grey, /obj/item/clothing/under/rank/security/head_of_security/alt,
+ /obj/item/clothing/under/rank/rnd/research_director/alt, /obj/item/clothing/under/rank/rnd/research_director/turtleneck,
+ /obj/item/clothing/under/rank/captain/parade, /obj/item/clothing/under/rank/security/head_of_security/parade, /obj/item/clothing/under/rank/security/head_of_security/parade/female,
+ /obj/item/clothing/head/helmet/abductor, /obj/item/clothing/suit/armor/abductor/vest, /obj/item/melee/baton/abductor,
+ /obj/item/storage/belt/military/abductor, /obj/item/gun/energy/alien, /obj/item/abductor/silencer,
+ /obj/item/abductor/gizmo, /obj/item/clothing/under/rank/centcom/officer,
+ /obj/item/clothing/suit/space/hardsuit/ert, /obj/item/clothing/suit/space/hardsuit/ert/sec,
+ /obj/item/clothing/suit/space/hardsuit/ert/engi, /obj/item/clothing/suit/space/hardsuit/ert/med,
+ /obj/item/clothing/suit/space/hardsuit/deathsquad, /obj/item/clothing/head/helmet/space/hardsuit/deathsquad,
+ /obj/machinery/door/airlock/centcom, /obj/machinery/atmospherics/miner, /obj/item/toy/figure/captain, /obj/item/toy/figure/ce,
+ /obj/item/toy/figure/dsquad, /obj/item/toy/figure/hop, /obj/item/toy/figure/hos, /obj/item/toy/figure/rd,
+ /obj/item/toy/figure/cmo, /obj/item/stamp/captain, /obj/item/stamp/hop, /obj/item/stamp/hos, /obj/item/stamp/ce,
+ /obj/item/stamp/rd, /obj/item/stamp/cmo, /obj/item/stamp/centcom, /obj/item/megaphone/command
+ )),
+
+ "robots" = typecacheof(list(/obj/machinery/computer/upload, /obj/item/ai_module, /obj/machinery/recharge_station,
+ /obj/item/aicard, /obj/structure/swarmer_beacon, /obj/item/toy/figure/borg, /obj/item/toy/talking/ai,
+ /obj/structure/statue/silver/medborg, /obj/structure/statue/silver/secborg, /obj/structure/statue/diamond/ai1,
+ /obj/structure/statue/diamond/ai2,
+ )),
+
+ "doctors" = typecacheof(list(/obj/item/clothing/under/rank/medical,
+ /obj/item/reagent_containers/syringe, /obj/item/reagent_containers/pill/, /obj/item/reagent_containers/hypospray,
+ /obj/item/storage/firstaid, /obj/item/storage/pill_bottle, /obj/item/healthanalyzer,
+ /obj/structure/sign/departments/medbay, /obj/machinery/door/airlock/medical, /obj/machinery/sleeper, /obj/machinery/stasis,
+ /obj/machinery/dna_scannernew, /obj/machinery/atmospherics/components/unary/cryo_cell, /obj/item/surgical_drapes,
+ /obj/item/retractor, /obj/item/hemostat, /obj/item/cautery, /obj/item/surgicaldrill, /obj/item/scalpel, /obj/item/circular_saw,
+ /obj/item/clothing/suit/bio_suit/plaguedoctorsuit, /obj/item/clothing/head/plaguedoctorhat, /obj/item/clothing/mask/gas/plaguedoctor,
+ )),
+
+ "authority" = typecacheof(list(/obj/item/clothing/under/rank/captain, /obj/item/clothing/under/rank/civilian/head_of_personnel,
+ /obj/item/clothing/under/rank/security/head_of_security, /obj/item/clothing/under/rank/rnd/research_director,
+ /obj/item/clothing/under/rank/medical/chief_medical_officer, /obj/item/clothing/under/rank/engineering/chief_engineer,
+ /obj/item/clothing/under/rank/centcom/officer, /obj/item/clothing/under/rank/centcom/commander,
+ /obj/item/melee/baton/telescopic, /obj/item/card/id/advanced/silver, /obj/item/card/id/advanced/gold,
+ /obj/item/card/id/advanced/centcom, /obj/machinery/door/airlock/command, /obj/item/card/id/advanced,
+ /obj/item/toy/figure/captain, /obj/item/toy/figure/ce,
+ /obj/item/toy/figure/dsquad, /obj/item/toy/figure/hop, /obj/item/toy/figure/hos, /obj/item/toy/figure/rd,
+ /obj/item/toy/figure/cmo, /obj/item/stamp/captain, /obj/item/stamp/hop, /obj/item/stamp/hos, /obj/item/stamp/ce,
+ /obj/item/stamp/rd, /obj/item/stamp/cmo, /obj/item/stamp/centcom, /obj/item/megaphone/command,
+ /obj/structure/statue/gold/hos, /obj/structure/statue/gold/ce, /obj/structure/statue/gold/cmo, /obj/structure/statue/gold/rd,
+ /obj/structure/statue/gold/hop, /obj/structure/statue/diamond/captain
+ )),
+
+ "the supernatural" = typecacheof(list(/obj/structure/destructible/cult, /obj/item/tome,
+ /obj/item/melee/cultblade, /obj/item/cult_bastard,
+ /obj/item/restraints/legcuffs/bola/cult, /obj/item/clothing/suit/space/hardsuit/cult,
+ /obj/item/clothing/suit/hooded/cultrobes, /obj/item/clothing/head/hooded/cult_hoodie, /obj/effect/rune,
+ /obj/item/stack/sheet/runed_metal, /obj/machinery/door/airlock/cult, /obj/narsie,
+ /obj/item/soulstone,
+ /obj/item/clothing/suit/wizrobe, /obj/item/clothing/head/wizard, /obj/item/spellbook, /obj/item/staff,
+ /obj/item/clothing/suit/space/hardsuit/shielded/wizard, /obj/item/clothing/suit/space/hardsuit/wizard,
+ /obj/item/gun/magic/staff, /obj/item/gun/magic/wand,
+ /obj/item/nullrod, /obj/item/clothing/under/rank/civilian/chaplain,
+ /obj/item/gun/magic/staff, /obj/item/gun/magic/wand, /obj/item/scrying, /obj/item/necromantic_stone,
+ /obj/item/warpwhistle, /obj/item/nullrod, /obj/item/clothing/under/rank/civilian/chaplain,
+ /obj/structure/spirit_board, /obj/item/toy/eightball/haunted, /obj/item/storage/toolbox/haunted,
+ /obj/item/stack/sheet/hauntium,
+ )),
+
+ "aliens" = typecacheof(list(/obj/item/clothing/mask/facehugger, /obj/item/organ/body_egg/alien_embryo,
+ /obj/structure/alien, /obj/item/toy/toy_xeno,
+ /obj/item/clothing/suit/armor/abductor, /obj/item/abductor, /obj/item/gun/energy/alien,
+ /obj/item/melee/baton/abductor, /obj/item/radio/headset/abductor, /obj/item/scalpel/alien, /obj/item/hemostat/alien,
+ /obj/item/retractor/alien, /obj/item/circular_saw/alien, /obj/item/surgicaldrill/alien, /obj/item/cautery/alien,
+ /obj/item/clothing/head/helmet/abductor, /obj/structure/bed/abductor, /obj/structure/table_frame/abductor,
+ /obj/structure/table/abductor, /obj/structure/table/optable/abductor, /obj/structure/closet/abductor, /obj/item/organ/heart/gland,
+ /obj/machinery/abductor, /obj/item/crowbar/abductor, /obj/item/screwdriver/abductor, /obj/item/weldingtool/abductor,
+ /obj/item/wirecutters/abductor, /obj/item/wrench/abductor, /obj/item/stack/sheet/mineral/abductor, /obj/item/toy/toy_xeno,
+ /obj/structure/statue/plasma/xeno
+ )),
+
+ "birds" = typecacheof(list(/obj/item/clothing/mask/gas/plaguedoctor, /obj/item/food/cracker,
+ /obj/item/clothing/suit/chickensuit, /obj/item/clothing/head/chicken,
+ /obj/item/clothing/suit/toggle/owlwings, /obj/item/clothing/under/costume/owl, /obj/item/clothing/mask/gas/owl_mask,
+ /obj/item/clothing/under/costume/griffin, /obj/item/clothing/shoes/griffin, /obj/item/clothing/head/griffin,
+ /obj/item/clothing/head/helmet/space/freedom, /obj/item/clothing/suit/space/freedom,
+ )),
+
+ "anime" = typecacheof(list(/obj/item/clothing/under/costume/schoolgirl, /obj/item/katana, /obj/item/food/sashimi, /obj/item/food/chawanmushi,
+ /obj/item/reagent_containers/food/drinks/bottle/sake, /obj/item/throwing_star, /obj/item/clothing/head/kitty/genuine, /obj/item/clothing/suit/space/space_ninja,
+ /obj/item/clothing/mask/gas/space_ninja, /obj/item/clothing/shoes/space_ninja, /obj/item/clothing/gloves/space_ninja, /obj/item/vibro_weapon,
+ /obj/item/nullrod/scythe/vibro, /obj/item/energy_katana, /obj/item/toy/katana, /obj/item/nullrod/claymore/katana, /obj/structure/window/paperframe, /obj/structure/mineral_door/paperframe)),
+
+ "birds" = typecacheof(list(/obj/item/clothing/mask/gas/plaguedoctor, /obj/item/food/cracker,
+ /obj/item/clothing/suit/chickensuit, /obj/item/clothing/head/chicken,
+ /obj/item/clothing/suit/toggle/owlwings, /obj/item/clothing/under/costume/owl, /obj/item/clothing/mask/gas/owl_mask,
+ /obj/item/clothing/under/costume/griffin, /obj/item/clothing/shoes/griffin, /obj/item/clothing/head/griffin,
+ /obj/item/clothing/head/helmet/space/freedom, /obj/item/clothing/suit/space/freedom
+ )),
+
+ "guns" = typecacheof(list(/obj/item/gun/ballistic, /obj/item/gun/energy, /obj/item/gun/syringe, /obj/item/gun/chem,
+ /obj/item/gun/blastcannon, /obj/item/gun/grenadelauncher, /obj/machinery/porta_turret, /obj/machinery/power/emitter,
+ /obj/item/ammo_casing, /obj/item/storage/belt/bandolier, /obj/item/storage/belt/holster, /obj/item/ammo_box,
+ /obj/item/mecha_ammo, /obj/item/mecha_parts/mecha_equipment/weapon/ballistic, /obj/item/mecha_parts/mecha_equipment/weapon/energy
+ )),
+
+ "insects" = typecacheof(list(/obj/item/toy/plush/moth, /obj/item/toy/plush/beeplushie, /obj/item/clothing/mask/animal/rat/bee, /obj/item/clothing/suit/hooded/bee_costume, /obj/structure/beebox)),
+
+ "anime" = typecacheof(list(/obj/item/clothing/under/costume/schoolgirl, /obj/item/katana, /obj/item/food/sashimi, /obj/item/food/chawanmushi,
+ /obj/item/reagent_containers/food/drinks/bottle/sake, /obj/item/throwing_star, /obj/item/clothing/head/kitty/genuine, /obj/item/clothing/suit/space/space_ninja,
+ /obj/item/clothing/mask/gas/space_ninja, /obj/item/clothing/shoes/space_ninja, /obj/item/clothing/gloves/space_ninja, /obj/item/vibro_weapon,
+ /obj/item/nullrod/scythe/vibro, /obj/item/energy_katana, /obj/item/toy/katana, /obj/item/nullrod/claymore/katana, /obj/structure/window/paperframe, /obj/structure/mineral_door/paperframe
+ )),
+))
+
+GLOBAL_LIST_INIT(phobia_turfs, list(
+ "space" = typecacheof(list(/turf/open/space, /turf/open/floor/holofloor/space, /turf/open/floor/fakespace)),
+ "the supernatural" = typecacheof(list(/turf/open/floor/cult, /turf/closed/wall/mineral/cult)),
+ "aliens" = typecacheof(list(/turf/open/floor/plating/abductor, /turf/open/floor/plating/abductor2,
+ /turf/open/floor/mineral/abductor, /turf/closed/wall/mineral/abductor
+ )),
+ "falling" = typecacheof(list(/turf/open/chasm, /turf/open/floor/fakepit, /turf/open/openspace)),
+))
+
+GLOBAL_LIST_INIT(phobia_species, list(
+ "aliens" = typecacheof(list(/datum/species/abductor, /datum/species/jelly, /datum/species/pod,/datum/species/shadow)),
+ "anime" = typecacheof(list(/datum/species/human/felinid)),
+ "conspiracies" = typecacheof(list(/datum/species/abductor, /datum/species/lizard, /datum/species/synth)),
+ "insects" = typecacheof(list(/datum/species/fly, /datum/species/moth)),
+ "lizards" = typecacheof(list(/datum/species/lizard)),
+ "robots" = typecacheof(list(/datum/species/android)),
+ "skeletons" = typecacheof(list(/datum/species/skeleton, /datum/species/plasmaman)),
+ "the supernatural" = typecacheof(list(/datum/species/golem/runic)),
+))
+
+/// Creates a regular expression to match against the given phobia
+/// Capture group 2 = the scary word
+/// Capture group 3 = an optional suffix on the scary word
+/proc/construct_phobia_regex(list/name)
+ var/list/words = strings(PHOBIA_FILE, name)
+ if(!length(words))
+ CRASH("phobia [name] has no entries")
+ var/words_match = ""
+ for(var/word in words)
+ words_match += "[REGEX_QUOTE(word)]|"
+ words_match = copytext(words_match, 1, -1)
+ return regex("(\\b|\\A)([words_match])('?s*)(\\b|\\|)", "ig")
+
+#undef PHOBIA_FILE
diff --git a/code/_onclick/hud/action_button.dm b/code/_onclick/hud/action_button.dm
index 02deb859551..3b917f3a194 100644
--- a/code/_onclick/hud/action_button.dm
+++ b/code/_onclick/hud/action_button.dm
@@ -109,14 +109,15 @@
usr.client.prefs.action_buttons_screen_locs["[name]_[id]"] = locked ? moved : null
return TRUE
if(LAZYACCESS(modifiers, ALT_CLICK))
+ var/buttons_locked = usr.client.prefs.read_preference(/datum/preference/toggle/buttons_locked)
for(var/V in usr.actions)
var/datum/action/A = V
var/atom/movable/screen/movable/action_button/B = A.button
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/ghost.dm b/code/_onclick/hud/ghost.dm
index e506cf059a2..7efab81cc42 100644
--- a/code/_onclick/hud/ghost.dm
+++ b/code/_onclick/hud/ghost.dm
@@ -104,10 +104,10 @@
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
//We should only see observed mob alerts.
/datum/hud/ghost/reorganize_alerts(mob/viewmob)
diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm
index ea72ebdd9a4..4856ec0676a 100644
--- a/code/_onclick/hud/hud.dm
+++ b/code/_onclick/hud/hud.dm
@@ -60,6 +60,18 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
///UI for screentips that appear when you mouse over things
var/atom/movable/screen/screentip/screentip_text
+ /// Whether or not screentips are enabled.
+ /// This is updated by the preference for cheaper reads than would be
+ /// had with a proc call, especially on one of the hottest procs in the
+ /// game (MouseEntered).
+ var/screentips_enabled = TRUE
+
+ /// The color to use for the screentips.
+ /// This is updated by the preference for cheaper reads than would be
+ /// had with a proc call, especially on one of the hottest procs in the
+ /// game (MouseEntered).
+ var/screentip_color
+
var/atom/movable/screen/movable/action_button/hide_toggle/hide_actions_toggle
var/action_buttons_hidden = FALSE
@@ -76,12 +88,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()
@@ -90,6 +102,9 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
plane_masters["[instance.plane]"] = instance
instance.backdrop(mymob)
+ var/datum/preferences/preferences = owner?.client?.prefs
+ screentip_color = preferences?.read_preference(/datum/preference/color/screentip_color)
+ screentips_enabled = preferences?.read_preference(/datum/preference/toggle/enable_screentips)
screentip_text = new(null, src)
static_inventory += screentip_text
@@ -173,7 +188,7 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
if(toggleable_inventory.len && screenmob.hud_used && screenmob.hud_used.inventory_shown)
screenmob.client.screen += toggleable_inventory
//SKYRAT EDIT ADDITION BEGIN - ERP_SLOT_SYSTEM
- if(ERP_toggleable_inventory.len && screenmob.hud_used && screenmob.hud_used.ERP_inventory_shown && screenmob.client?.prefs.sextoys_pref == "Yes")
+ if(ERP_toggleable_inventory.len && screenmob.hud_used && screenmob.hud_used.ERP_inventory_shown && screenmob.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
screenmob.client.screen += ERP_toggleable_inventory
//SKYRAT EDIT ADDITION END
if(hotkeybuttons.len && !hotkey_ui_hidden)
@@ -193,7 +208,7 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
if(toggleable_inventory.len)
screenmob.client.screen -= toggleable_inventory
//SKYRAT EDIT ADDITION BEGIN - ERP_SLOT_SYSTEM
- if(ERP_toggleable_inventory.len && screenmob.hud_used && screenmob.hud_used.ERP_inventory_shown && screenmob.client?.prefs.sextoys_pref == "Yes")
+ if(ERP_toggleable_inventory.len && screenmob.hud_used && screenmob.hud_used.ERP_inventory_shown && screenmob.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
screenmob.client.screen -= ERP_toggleable_inventory
//SKYRAT EDIT ADDITION END
if(hotkeybuttons.len)
@@ -217,7 +232,7 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
if(toggleable_inventory.len)
screenmob.client.screen -= toggleable_inventory
//SKYRAT EDIT ADDITION BEGIN - ERP_SLOT_SYSTEM
- if(toggleable_inventory.len && screenmob.hud_used && screenmob.hud_used.ERP_inventory_shown && screenmob.client?.prefs.sextoys_pref == "Yes")
+ if(toggleable_inventory.len && screenmob.hud_used && screenmob.hud_used.ERP_inventory_shown && screenmob.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
screenmob.client.screen -= ERP_toggleable_inventory
//SKYRAT EDIT ADDITION END
if(hotkeybuttons.len)
diff --git a/code/_onclick/hud/human.dm b/code/_onclick/hud/human.dm
index da0b76f8eb0..50663c35a6e 100644
--- a/code/_onclick/hud/human.dm
+++ b/code/_onclick/hud/human.dm
@@ -239,7 +239,7 @@
using.screen_loc = ui_erp_inventory
using.hud = src
// When creating a character, we will check if the ERP is enabled on the client, if not, then the ERP button is immediately invisible
- if(owner.client?.prefs.sextoys_pref != "Yes")
+ if(owner.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
using.invisibility = 100
static_inventory += using
//SKYRAT EDIT ADDITION END
@@ -470,7 +470,7 @@
if(H.head) screenmob.client.screen -= H.head
//SKYRAT EDIT ADDITION BEGIN - ERP_SLOT_SYSTEM
- if(screenmob.hud_used.ERP_inventory_shown && screenmob.hud_used.hud_shown && H.client.prefs?.sextoys_pref == "Yes")
+ if(screenmob.hud_used.ERP_inventory_shown && screenmob.hud_used.hud_shown && H.client.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(H.vagina)
// This shity code need for hanlde an moving UI stuff when default inventory expand/collapse
if(screenmob.hud_used.inventory_shown && screenmob.hud_used)
@@ -535,7 +535,7 @@
screenmob.client.screen += H.r_store
//SKYRAT EDIT ADDITION BEGIN - ERP_SLOT_SYSTEM
- if(H.client.prefs?.sextoys_pref == "Yes")
+ if(H.client.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(H.vagina)
H.vagina.screen_loc = ui_vagina
screenmob.client.screen += H.vagina
diff --git a/code/_onclick/hud/new_player.dm b/code/_onclick/hud/new_player.dm
index 44ed119b95f..c0f0a694f0e 100644
--- a/code/_onclick/hud/new_player.dm
+++ b/code/_onclick/hud/new_player.dm
@@ -74,7 +74,11 @@
. = ..()
if(!.)
return
- hud.mymob.client.prefs.ShowChoices(hud.mymob)
+
+ var/datum/preferences/preferences = hud.mymob.client.prefs
+ preferences.current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES
+ preferences.update_static_data(usr)
+ preferences.ui_interact(usr)
///Button that appears before the game has started
/atom/movable/screen/lobby/button/ready
@@ -190,21 +194,21 @@
set_button_status(TRUE)
UnregisterSignal(SSticker, COMSIG_TICKER_ENTER_PREGAME, .proc/enable_observing)
-
-/* This is here for a future settings menu that will come with the prefs rework, if this is not in by 2022 kill mothblocks.
/atom/movable/screen/lobby/button/settings
icon = 'icons/hud/lobby/bottom_buttons.dmi'
icon_state = "settings"
base_icon_state = "settings"
- screen_loc = "TOP:-122,CENTER:+58"
+ screen_loc = "TOP:-122,CENTER:+30"
/atom/movable/screen/lobby/button/settings/Click(location, control, params)
. = ..()
if(!.)
return
- hud.mymob.client.prefs.ShowChoices(hud.mymob)
-*/
+ var/datum/preferences/preferences = hud.mymob.client.prefs
+ preferences.current_window = PREFERENCE_TAB_GAME_PREFERENCES
+ preferences.update_static_data(usr)
+ preferences.ui_interact(usr)
/atom/movable/screen/lobby/button/changelog_button
icon = 'icons/hud/lobby/bottom_buttons.dmi'
@@ -217,7 +221,7 @@
icon = 'icons/hud/lobby/bottom_buttons.dmi'
icon_state = "crew_manifest"
base_icon_state = "crew_manifest"
- screen_loc = "TOP:-122,CENTER:+30"
+ screen_loc = "TOP:-122,CENTER:+2"
/atom/movable/screen/lobby/button/crew_manifest/Click(location, control, params)
. = ..()
@@ -234,7 +238,7 @@
icon = 'icons/hud/lobby/bottom_buttons.dmi'
icon_state = "poll"
base_icon_state = "poll"
- screen_loc = "TOP:-122,CENTER:+2"
+ screen_loc = "TOP:-122,CENTER:-26"
var/new_poll = FALSE
diff --git a/code/_onclick/hud/parallax.dm b/code/_onclick/hud/parallax.dm
index 7b56c1f0489..1d09819f986 100755
--- a/code/_onclick/hud/parallax.dm
+++ b/code/_onclick/hud/parallax.dm
@@ -48,10 +48,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 4efd190bf5b..0c06fa5a258 100644
--- a/code/_onclick/hud/plane_master.dm
+++ b/code/_onclick/hud/plane_master.dm
@@ -58,7 +58,8 @@
blend_mode = BLEND_OVERLAY
/atom/movable/screen/plane_master/game_world/backdrop(mob/mymob)
- if(istype(mymob) && mymob.client && mymob.client.prefs && mymob.client.prefs.ambientocclusion)
+ remove_filter("AO")
+ if(istype(mymob) && mymob.client?.prefs?.read_preference(/datum/preference/toggle/ambient_occlusion))
add_filter("AO", 1, drop_shadow_filter(x = 0, y = -2, size = 4, color = "#04080FAA"))
@@ -184,6 +185,6 @@
blend_mode = BLEND_OVERLAY
/atom/movable/screen/plane_master/runechat/backdrop(mob/mymob)
- filters = list()
- if(istype(mymob) && mymob.client?.prefs?.ambientocclusion)
+ remove_filter("AO")
+ if(istype(mymob) && mymob.client?.prefs?.read_preference(/datum/preference/toggle/ambient_occlusion))
add_filter("AO", 1, drop_shadow_filter(x = 0, y = -2, size = 4, color = "#04080FAA"))
diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm
index 1ca37d04882..c54c130ee2d 100644
--- a/code/_onclick/observer.dm
+++ b/code/_onclick/observer.dm
@@ -55,7 +55,7 @@
return TRUE
else if(isAdminGhostAI(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/configuration.dm b/code/controllers/configuration/configuration.dm
index d8bd050c5ef..70e416276f9 100644
--- a/code/controllers/configuration/configuration.dm
+++ b/code/controllers/configuration/configuration.dm
@@ -235,6 +235,7 @@
var/tm_info = GLOB.revdata.GetTestMergeInfo()
if(motd || tm_info)
motd = motd ? "[motd] [tm_info]" : tm_info
+
/*
Policy file should be a json file with a single object.
Value is raw html.
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index 5cbbc1e7ae7..674e9308680 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -110,8 +110,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_summon_guns //No
/datum/config_entry/flag/no_summon_magic //Fun
diff --git a/code/controllers/master.dm b/code/controllers/master.dm
index e7ddca62933..e375fde21b3 100644
--- a/code/controllers/master.dm
+++ b/code/controllers/master.dm
@@ -64,6 +64,10 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/current_runlevel //!for scheduling different subsystems for different stages of the round
var/sleep_offline_after_initializations = TRUE
+ /// During initialization, will be the instanced subsytem that is currently initializing.
+ /// Outside of initialization, returns null.
+ var/current_initializing_subsystem = null
+
var/static/restart_clear = 0
var/static/restart_timeout = 0
var/static/restart_count = 0
@@ -214,8 +218,10 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
for (var/datum/controller/subsystem/SS in subsystems)
if (SS.flags & SS_NO_INIT || SS.initialized) //Don't init SSs with the correspondig flag or if they already are initialzized
continue
+ current_initializing_subsystem = SS
SS.Initialize(REALTIMEOFDAY)
CHECK_TICK
+ current_initializing_subsystem = null
current_ticklimit = TICK_LIMIT_RUNNING
var/time = (REALTIMEOFDAY - start_timeofday) / 10
diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm
index 4f92c35dba0..87eaf03a5f7 100644
--- a/code/controllers/subsystem/atoms.dm
+++ b/code/controllers/subsystem/atoms.dm
@@ -17,6 +17,9 @@ SUBSYSTEM_DEF(atoms)
///initAtom() adds the atom its creating to this list iff InitializeAtoms() has been given a list to populate as an argument
var/list/created_atoms
+ /// Atoms that will be deleted once the subsystem is initialized
+ var/list/queued_deletions = list()
+
initialized = INITIALIZATION_INSSATOMS
/datum/controller/subsystem/atoms/Initialize(timeofday)
@@ -75,6 +78,12 @@ SUBSYSTEM_DEF(atoms)
atoms_to_return += created_atoms
created_atoms = null
+ for (var/queued_deletion in queued_deletions)
+ qdel(queued_deletion)
+
+ testing("[queued_deletions.len] atoms were queued for deletion.")
+ queued_deletions.Cut()
+
/// Init this specific atom
/datum/controller/subsystem/atoms/proc/InitAtom(atom/A, from_template = FALSE, list/arguments)
var/the_type = A.type
@@ -169,6 +178,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 00000000000..db1ffb13333
--- /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(start_timeofday)
+ 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 (!get_asset_datum(asset_type))
+ stack_trace("Could not initialize early asset [asset_type]!")
+
+ CHECK_TICK
+
+ return ..()
diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm
index 42f8bd696f9..5e9b090b949 100644
--- a/code/controllers/subsystem/job.dm
+++ b/code/controllers/subsystem/job.dm
@@ -74,7 +74,6 @@ SUBSYSTEM_DEF(job)
SetupOccupations()
if(CONFIG_GET(flag/load_jobs_from_txt))
LoadJobs()
- generate_selectable_species()
set_overflow_role(CONFIG_GET(string/overflow_job))
return ..()
@@ -175,6 +174,7 @@ SUBSYSTEM_DEF(job)
return name_occupations[rank]
/datum/controller/subsystem/job/proc/GetJobType(jobtype)
+ RETURN_TYPE(/datum/job)
if(!length(all_occupations))
SetupOccupations()
return type_occupations[jobtype]
@@ -529,27 +529,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/datum/job/overflow_role_datum = GetJobType(overflow_role)
- var/allowed_to_be_a_loser = !is_banned_from(player.ckey, overflow_role_datum.title)
- if(QDELETED(player) || !allowed_to_be_a_loser)
- RejectPlayer(player)
- else
- if(!AssignRole(player, overflow_role_datum))
+ return
+
+ switch (jobless_role)
+ if (BEOVERFLOW)
+ var/datum/job/overflow_role_datum = GetJobType(overflow_role)
+ var/allowed_to_be_a_loser = !is_banned_from(player.ckey, overflow_role_datum.title)
+ if(QDELETED(player) || !allowed_to_be_a_loser)
RejectPlayer(player)
- else if(player.client.prefs.joblessrole == BERANDOMJOB)
- if(!GiveRandomJob(player))
+ else
+ if(!AssignRole(player, overflow_role_datum))
+ RejectPlayer(player)
+ if (BERANDOMJOB)
+ if(!GiveRandomJob(player))
+ RejectPlayer(player)
+ if (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 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)
//Gives the player the stuff he should have with his rank
@@ -559,18 +564,10 @@ SUBSYSTEM_DEF(job)
SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job)
equipping.mind?.set_assigned_role(job)
- //SKYRAT EDIT ADD - ALTERNATE JOB TITLES
- var/display_rank = job.title
- if(player_client && player_client.prefs && player_client.prefs.alt_titles_preferences[job.title])
- display_rank = player_client.prefs.alt_titles_preferences[job.title]
if(player_client)
- to_chat(player_client, "You are the [display_rank]. ")
- /* SKYRAT EDIT ORIGINAL
- if(player_client)
- to_chat(player_client, "You are the [job.title]. ")
- */ // SKYRAT EDIT END
+ to_chat(player_client, span_infoplain("You are the [job.title]."))
- equipping.on_job_equipping(job)
+ equipping.on_job_equipping(job, player_client?.prefs) //SKYRAT EDIT CHANGE
job.announce_job(equipping)
@@ -581,15 +578,8 @@ SUBSYSTEM_DEF(job)
handle_auto_deadmin_roles(player_client, job.title)
- var/list/packed_items //SKYRAT EDIT ADD - CUSTOMISATION
- if(job)
- if (player_client && job.no_dresscode && job.loadout)
- packed_items = player_client.prefs.equip_preference_loadout(equipping,FALSE,job,blacklist=job.blacklist_dresscode_slots,initial=TRUE)
- //SKYRAT EDIT ADDITION END
-
if(player_client)
- to_chat(player_client, "As the [display_rank] you answer directly to [job.supervisors]. Special circumstances may change this. Your role is that of a [job.title]. Regardless of what your job title may be, please work to fulfil that role. ") //SKYRAT EDIT -- ALT TITLES
- // to_chat(player_client, "As the [job.title] you answer directly to [job.supervisors]. Special circumstances may change this. " // SKYRAT EDIT ORIGINAL
+ to_chat(player_client, span_infoplain("As the [job.title] you answer directly to [job.supervisors]. Special circumstances may change this.")) // SKYRAT EDIT ORIGINAL
job.radio_help_message(equipping)
@@ -610,14 +600,6 @@ SUBSYSTEM_DEF(job)
job.after_spawn(equipping, player_client)
- //SKYRAT CHANGE ADDITION BEGIN - CUSTOMIZATION
- if(!job.no_dresscode && job.loadout)
- if(player_client)
- packed_items = player_client.prefs.equip_preference_loadout(equipping, FALSE, job,initial=TRUE)
- if(packed_items)
- player_client.prefs.add_packed_items(equipping, packed_items)
- //SKYRAT CHANGE ADDITION END
-
/datum/controller/subsystem/job/proc/handle_auto_deadmin_roles(client/C, rank)
if(!C?.holder)
return TRUE
diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm
index 8dd240c7f7b..586c6cac441 100644
--- a/code/controllers/subsystem/mapping.dm
+++ b/code/controllers/subsystem/mapping.dm
@@ -378,7 +378,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/persistence.dm b/code/controllers/subsystem/persistence.dm
index abd15644359..7d7a98049aa 100644
--- a/code/controllers/subsystem/persistence.dm
+++ b/code/controllers/subsystem/persistence.dm
@@ -21,8 +21,6 @@ SUBSYSTEM_DEF(persistence)
var/list/picture_logging_information = list()
var/list/obj/structure/sign/picture_frame/photo_frames
var/list/obj/item/storage/photo_album/photo_albums
- var/list/obj/structure/sign/painting/painting_frames = list()
- var/list/paintings = list()
/datum/controller/subsystem/persistence/Initialize()
@@ -33,7 +31,6 @@ SUBSYSTEM_DEF(persistence)
LoadRecentMaps()
LoadPhotoPersistence()
LoadRandomizedRecipes()
- LoadPaintings()
LoadPanicBunker() //SKYRAT EDIT ADDITION - PANICBUNKER
load_custom_outfits()
@@ -48,7 +45,6 @@ SUBSYSTEM_DEF(persistence)
SavePhotoPersistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION.
SaveRandomizedRecipes()
SavePanicBunker()//SKYRAT EDIT ADDITION - PANICBUNKER
- SavePaintings()
SaveScars()
save_custom_outfits()
@@ -413,22 +409,6 @@ 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)
diff --git a/code/controllers/subsystem/persistent_paintings.dm b/code/controllers/subsystem/persistent_paintings.dm
new file mode 100644
index 00000000000..ccf14a37269
--- /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 ..()
+
+/// 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 581087348bc..5edbe026ee7 100644
--- a/code/controllers/subsystem/processing/quirks.dm
+++ b/code/controllers/subsystem/processing/quirks.dm
@@ -69,21 +69,6 @@ PROCESSING_SUBSYSTEM_DEF(quirks)
var/datum/quirk/wayfinder = /datum/quirk/item_quirk/needswayfinder
if(user.add_quirk(wayfinder))
SSblackbox.record_feedback("nested tally", "quirks_taken", 1, list(initial(wayfinder.name)))
-
-
- // SKYRAT EDIT ADDITION START - Customization (food prefs)
- // This was done in this proc on old skyrat and i cba to find a better way
- var/mob/living/carbon/human/H = user
- if(istype(H))
- if(cli.prefs.foodlikes.len)
- H.dna.species.liked_food = 0
- for(var/V in cli.prefs.foodlikes)
- H.dna.species.liked_food |= cli.prefs.foodlikes[V]
- if(cli.prefs.fooddislikes.len)
- H.dna.species.disliked_food = 0
- for(var/V in cli.prefs.fooddislikes)
- H.dna.species.disliked_food |= cli.prefs.fooddislikes[V]
- // SKYRAT EDIT ADDITION END
/*
*Randomises the quirks for a specified mob
*/
@@ -147,5 +132,66 @@ PROCESSING_SUBSYSTEM_DEF(quirks)
for(var/datum/quirk/quirk as anything in quirks_to_add)
user.add_quirk(quirks[quirk]) //these are typepaths converted from string
+/// 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)
+ var/list/new_quirks = list()
+ var/list/positive_quirks = list()
+ var/balance = 0
+
+ for (var/quirk_name in quirks)
+ var/datum/quirk/quirk = SSquirks.quirks[quirk_name]
+ if (isnull(quirk))
+ continue
+
+ if (initial(quirk.mood_quirk) && CONFIG_GET(flag/disable_human_mood))
+ continue
+
+ 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
+
#undef RANDOM_QUIRK_BONUS
#undef MINIMUM_RANDOM_QUIRKS
diff --git a/code/controllers/subsystem/processing/reagents.dm b/code/controllers/subsystem/processing/reagents.dm
index 1a129ace80c..25e50c21b4c 100644
--- a/code/controllers/subsystem/processing/reagents.dm
+++ b/code/controllers/subsystem/processing/reagents.dm
@@ -17,7 +17,6 @@ PROCESSING_SUBSYSTEM_DEF(reagents)
///Blacklists these reagents from being added to the master list. the exact type only. Children are not blacklisted.
GLOB.fake_reagent_blacklist = list(/datum/reagent/medicine/c2, /datum/reagent/medicine, /datum/reagent/reaction_agent)
//Build GLOB lists - see holder.dm
- build_chemical_reagent_list()
build_chemical_reactions_lists()
return
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 8829e0972a6..de6c2e2438f 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -414,6 +414,12 @@ SUBSYSTEM_DEF(ticker)
if(new_player_mob.client?.prefs?.should_be_random_hardcore(player_assigned_role, new_player_living.mind))
new_player_mob.client.prefs.hardcore_random_setup(new_player_living)
SSquirks.AssignQuirks(new_player_living, new_player_mob.client)
+
+ //SKYRAT EDIT ADDITION
+ if(ishuman(new_player_living))
+ for(var/datum/loadout_item/item as anything in loadout_list_to_datums(new_player_mob.client?.prefs?.loadout_list))
+ item.post_equip_item(new_player_mob.client?.prefs, new_player_living)
+ //SKYRAT EDIT END
CHECK_TICK
if(captainless)
@@ -437,7 +443,7 @@ SUBSYSTEM_DEF(ticker)
officer_mobs += character
var/datum/client_interface/client = GET_CLIENT(new_player_mob)
- var/preference = client?.prefs?.prefered_security_department || SEC_DEPT_NONE
+ var/preference = client?.prefs?.read_preference(/datum/preference/choiced/security_department)
officer_preferences += preference
var/distribution = get_officer_departments(officer_preferences, departments)
@@ -483,7 +489,7 @@ SUBSYSTEM_DEF(ticker)
m = pick(memetips)
if(m)
- to_chat(world, span_purple("Tip of the round: [html_encode(m)] "))
+ to_chat(world, span_purple(examine_block("Tip of the round: [html_encode(m)] "))) //SKYRAT EDIT CHAGNE
/datum/controller/subsystem/ticker/proc/check_queue()
if(!queued_players.len)
diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm
index a0511526500..0ecaad692e0 100644
--- a/code/controllers/subsystem/vote.dm
+++ b/code/controllers/subsystem/vote.dm
@@ -64,7 +64,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 eb516b5c215..6789ce79da2 100644
--- a/code/datums/action.dm
+++ b/code/datums/action.dm
@@ -67,7 +67,7 @@
LAZYADD(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
M.update_action_buttons()
else
diff --git a/code/datums/brain_damage/imaginary_friend.dm b/code/datums/brain_damage/imaginary_friend.dm
index 737f6ac94c9..0a2196b4df3 100644
--- a/code/datums/brain_damage/imaginary_friend.dm
+++ b/code/datums/brain_damage/imaginary_friend.dm
@@ -125,7 +125,7 @@
setup_friend()
return
- real_name = appearance_from_prefs.real_name
+ real_name = appearance_from_prefs.read_preference(/datum/preference/name/real_name)
name = real_name
// Determine what job is marked as 'High' priority.
@@ -141,7 +141,7 @@
appearance_job = SSjob.GetJob("Assistant")
if(istype(appearance_job, /datum/job/ai))
- human_image = icon('icons/mob/ai.dmi', icon_state = resolve_ai_icon(appearance_from_prefs.preferred_ai_core_display), dir = SOUTH)
+ human_image = icon('icons/mob/ai.dmi', icon_state = resolve_ai_icon(appearance_from_prefs.read_preference(/datum/preference/choiced/ai_core_display)), dir = SOUTH)
return
if(istype(appearance_job, /datum/job/cyborg))
@@ -194,7 +194,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/brain_damage/phobia.dm b/code/datums/brain_damage/phobia.dm
index 4a95bf0ecfa..62abea4c688 100644
--- a/code/datums/brain_damage/phobia.dm
+++ b/code/datums/brain_damage/phobia.dm
@@ -22,16 +22,16 @@
phobia_type = new_phobia_type
if(!phobia_type)
- phobia_type = pick(SStraumas.phobia_types)
+ phobia_type = pick(GLOB.phobia_types)
gain_text = "You start finding [phobia_type] very unnerving... "
lose_text = "You no longer feel afraid of [phobia_type]. "
scan_desc += " of [phobia_type]"
- trigger_regex = SStraumas.phobia_regexes[phobia_type]
- trigger_mobs = SStraumas.phobia_mobs[phobia_type]
- trigger_objs = SStraumas.phobia_objs[phobia_type]
- trigger_turfs = SStraumas.phobia_turfs[phobia_type]
- trigger_species = SStraumas.phobia_species[phobia_type]
+ trigger_regex = GLOB.phobia_regexes[phobia_type]
+ trigger_mobs = GLOB.phobia_mobs[phobia_type]
+ trigger_objs = GLOB.phobia_objs[phobia_type]
+ trigger_turfs = GLOB.phobia_turfs[phobia_type]
+ trigger_species = GLOB.phobia_species[phobia_type]
..()
/datum/brain_trauma/mild/phobia/on_life(delta_time, times_fired)
diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm
index 9e4af75593a..d590a33a130 100644
--- a/code/datums/chatmessage.dm
+++ b/code/datums/chatmessage.dm
@@ -115,7 +115,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/components/mood.dm b/code/datums/components/mood.dm
index 8741e1c0fc3..6a07eea0ed1 100644
--- a/code/datums/components/mood.dm
+++ b/code/datums/components/mood.dm
@@ -139,7 +139,7 @@
msg += event.description
else
msg += "[span_nicegreen("I don't have much of a reaction to anything right now.")]\n"
- to_chat(user, msg)
+ to_chat(user, examine_block(msg)) //SKYRAT EDIT CHANGE
///Called after moodevent/s have been added/removed.
/datum/component/mood/proc/update_mood()
diff --git a/code/datums/datacore.dm b/code/datums/datacore.dm
index 371f5ee9283..516160a2e86 100644
--- a/code/datums/datacore.dm
+++ b/code/datums/datacore.dm
@@ -141,7 +141,7 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
var/datum/data/record/foundrecord = find_record("name", name, GLOB.data_core.general)
if(foundrecord)
foundrecord.fields["rank"] = assignment
- foundrecord.fields["truerank"] = assignment // SKYRAT EDIT - ALT TITLES
+
/datum/datacore/proc/get_manifest()
// First we build up the order in which we want the departments to appear in.
@@ -154,14 +154,12 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
for(var/datum/data/record/record as anything in GLOB.data_core.general)
var/name = record.fields["name"]
var/rank = record.fields["rank"]
- var/truerank = record.fields["truerank"] // SKYRAT EDIT ADD - ALT TITLES
- var/datum/job/job = SSjob.GetJob(truerank) // SKYRAT EDIT - ORIGINAL CALLED GetJob(rank)
+ var/datum/job/job = SSjob.GetJob(rank)
if(!job || !(job.job_flags & JOB_CREW_MANIFEST) || !LAZYLEN(job.departments_list)) // In case an unlawful custom rank is added.
var/list/misc_list = manifest_out[DEPARTMENT_UNASSIGNED]
misc_list[++misc_list.len] = list(
"name" = name,
"rank" = rank,
- "truerank" = truerank,
)
continue
for(var/department_type as anything in job.departments_list)
@@ -172,7 +170,6 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
var/list/entry = list(
"name" = name,
"rank" = rank,
- "truerank" = truerank,
)
var/list/department_list = manifest_out[department.department_name]
if(istype(job, department.department_head))
@@ -223,13 +220,7 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
var/static/list/show_directions = list(SOUTH, WEST)
if(H.mind?.assigned_role.job_flags & JOB_CREW_MANIFEST)
var/assignment = H.mind.assigned_role.title
- //SKYRAT EDIT ADD - ALTERNATE JOB TITLES
- var/true_assignment = assignment
- if(H.client && H.client.prefs && H.client.prefs.alt_titles_preferences[true_assignment]) // latejoin
- assignment = H.client.prefs.alt_titles_preferences[true_assignment]
- else if(C && C.prefs && C.prefs.alt_titles_preferences[true_assignment]) // roundstart - yes both do separate things i don't fucking know why but they do and if they're not both there then they don't fucking work leave me ALONE
- assignment = C.prefs.alt_titles_preferences[true_assignment]
- //SKYRAT EDIT ADD END
+
var/static/record_id_num = 1001
var/id = num2hex(record_id_num++,6)
if(!C)
@@ -252,7 +243,6 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
G.fields["id"] = id
G.fields["name"] = H.real_name
G.fields["rank"] = assignment
- G.fields["truerank"] = true_assignment // SKYRAT EDIT ADD - ALT TITLES
G.fields["age"] = H.age
G.fields["species"] = H.dna.species.name
G.fields["fingerprint"] = md5(H.dna.unique_identity)
@@ -267,10 +257,6 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
G.fields["gender"] = "Other"
G.fields["photo_front"] = photo_front
G.fields["photo_side"] = photo_side
- if(C && C.prefs && C.prefs.general_record) // SKYRAT EDIT ADD - RP RECORDS
- G.fields["past_records"] = C.prefs.general_record
- else
- G.fields["past_records"] = "" // SKYRAT EDIT END
general += G
//Medical Record
@@ -287,10 +273,6 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
M.fields["cdi_d"] = "No diseases have been diagnosed at the moment."
M.fields["notes"] = H.get_quirk_string(!medical, CAT_QUIRK_NOTES)
M.fields["notes_d"] = H.get_quirk_string(medical, CAT_QUIRK_NOTES)
- if(C && C.prefs && C.prefs.general_record) // SKYRAT EDIT ADD - RP RECORDS
- M.fields["past_records"] = C.prefs.medical_record
- else
- M.fields["past_records"] = "" // SKYRAT EDIT END
medical += M
//Security Record
@@ -301,10 +283,6 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
S.fields["citation"] = list()
S.fields["crim"] = list()
S.fields["notes"] = "No notes."
- if(C && C.prefs && C.prefs.general_record) // SKYRAT EDIT ADD - RP RECORDS
- S.fields["past_records"] = C.prefs.security_record
- else
- S.fields["past_records"] = "" // SKYRAT EDIT END
security += S
//Locked Record
@@ -312,7 +290,6 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
L.fields["id"] = md5("[H.real_name][assignment]") //surely this should just be id, like the others?
L.fields["name"] = H.real_name
L.fields["rank"] = assignment
- L.fields["truerank"] = true_assignment // SKYRAT EDIT ADD - ALT TITLES
L.fields["age"] = H.age
L.fields["gender"] = H.gender
if(H.gender == "male")
@@ -328,12 +305,6 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)
L.fields["features"] = H.dna.features
L.fields["image"] = image
L.fields["mindref"] = H.mind
- // SKYRAT EDIT ADD BEGIN - EXAMINE RECORDS
- if(C && C.prefs && C.prefs.general_record)
- L.fields["exp_records"] = C.prefs.exploitable_info //TODO: somebody with less coding anxiety please add a field that stores C.prefs.background_info
- else
- L.fields["exp_records"] = ""
- // SKYRAT EDIT END
locked += L
return
diff --git a/code/datums/dna.dm b/code/datums/dna.dm
index 48b58fef695..ec51cd8efe3 100644
--- a/code/datums/dna.dm
+++ b/code/datums/dna.dm
@@ -430,7 +430,7 @@
update_atom_languages()
*/
//SKYRAT EDIT REMOVAL BEGIN
-
+/*
/mob/living/carbon/human/set_species(datum/species/mrace, icon_update = TRUE, pref_load = FALSE)
..()
if(icon_update)
@@ -438,7 +438,7 @@
update_hair()
update_body_parts()
update_mutations_overlay()// no lizard with human hulk overlay please.
-
+*/
/mob/proc/has_dna()
return
@@ -492,7 +492,7 @@
/mob/living/carbon/proc/create_dna()
dna = new /datum/dna(src)
if(!dna.species)
- var/rando_race = GLOB.species_list[pick(GLOB.roundstart_races)]
+ var/rando_race = pick(get_selectable_species())
dna.species = new rando_race()
//proc used to update the mob's appearance after its dna UI has been changed
diff --git a/code/datums/elements/weapon_description.dm b/code/datums/elements/weapon_description.dm
index 6c5c90d8e70..690b5fe9a21 100644
--- a/code/datums/elements/weapon_description.dm
+++ b/code/datums/elements/weapon_description.dm
@@ -62,7 +62,7 @@
SIGNAL_HANDLER
if(href_list["examine"])
- to_chat(user, span_notice("[build_label_text(source)]"))
+ to_chat(user, span_notice(examine_block("[build_label_text(source)]"))) //SKYRAT EDIT CHANGE
/**
*
diff --git a/code/datums/keybinding/emote.dm b/code/datums/keybinding/emote.dm
index 157a45af51f..342c4009a68 100644
--- a/code/datums/keybinding/emote.dm
+++ b/code/datums/keybinding/emote.dm
@@ -10,7 +10,6 @@
emote_key = initial(faketype.key)
name = initial(faketype.key)
full_name = capitalize(initial(faketype.key))
- description = "Do the emote '*[emote_key]'"
/datum/keybinding/emote/down(client/user)
. = ..()
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index ae74cecfc4c..5ab75b481c0 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -261,12 +261,12 @@
if(!length(shown_skills))
to_chat(user, span_notice("You don't seem to have any particularly outstanding skills."))
return
- var/msg = "[span_info("*---------*\nYour skills ")]\n"
+ var/msg = "[span_info("Your skills ")]\n " //SKYRAT EDIT CHANGE
for(var/i in shown_skills)
var/datum/skill/the_skill = i
msg += "[initial(the_skill.name)] - [get_skill_level_name(the_skill)]\n"
msg += " "
- to_chat(user, msg)
+ to_chat(user, examine_block(msg)) //SKYRAT EDIT CHANGE
/datum/mind/proc/set_death_time()
SIGNAL_HANDLER
@@ -416,24 +416,24 @@
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_IMPLANT)
- implant = TRUE
+ if(UPLINK_RADIO)
+ uplink_loc = R
+ if(!uplink_loc)
+ uplink_loc = PDA
+ if(!uplink_loc)
+ uplink_loc = P
+ if(UPLINK_PEN)
+ uplink_loc = P
+ if(UPLINK_IMPLANT)
+ implant = TRUE
if(!uplink_loc) // We've looked everywhere, let's just implant you
implant = TRUE
diff --git a/code/datums/quirks/_quirk.dm b/code/datums/quirks/_quirk.dm
index 2ed7920c07b..3d69a030dbd 100644
--- a/code/datums/quirks/_quirk.dm
+++ b/code/datums/quirks/_quirk.dm
@@ -17,6 +17,9 @@
var/processing_quirk = FALSE
/// When making an abstract quirk (in OOP terms), don't forget to set this var to the type path for that abstract quirk.
var/abstract_parent_type = /datum/quirk
+ /// 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 = "bug" //SKYRAT EDIT CHANGE
/datum/quirk/Destroy()
if(quirk_holder)
diff --git a/code/datums/quirks/good.dm b/code/datums/quirks/good.dm
index 0242e10ca43..f391d9c8bea 100644
--- a/code/datums/quirks/good.dm
+++ b/code/datums/quirks/good.dm
@@ -4,6 +4,7 @@
/datum/quirk/alcohol_tolerance
name = "Alcohol Tolerance"
desc = "You become drunk more slowly and suffer fewer drawbacks from alcohol."
+ icon = "beer"
value = 4
mob_trait = TRAIT_ALCOHOL_TOLERANCE
gain_text = "You feel like you could drink a whole keg! "
@@ -13,6 +14,7 @@
/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 = 4
mood_quirk = TRUE
medical_record_text = "Patient was administered the Apathy Evaluation Scale but did not bother to complete it."
@@ -30,6 +32,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 = 8
gain_text = "You feel like a drink would do you good. "
lose_text = "You no longer feel like drinking would ease your pain. "
@@ -53,6 +56,7 @@
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."
value = 6 /// SKYRAT EDIT - Quirk Rebalance - Original: value = 8
+ icon = "smile-beam"
mob_trait = TRAIT_EMPATH
gain_text = "You feel in tune with those around you. "
lose_text = "You feel isolated from others. "
@@ -61,6 +65,7 @@
/datum/quirk/item_quirk/fan_clown
name = "Clown Fan"
desc = "You enjoy clown antics and get a mood boost from wearing your clown pin."
+ icon = "map-pin"
value = 2
mob_trait = TRAIT_FAN_CLOWN
gain_text = "You are a big fan of clowns. "
@@ -77,6 +82,7 @@
/datum/quirk/item_quirk/fan_mime
name = "Mime Fan"
desc = "You enjoy mime antics and get a mood boost from wearing your mime pin."
+ icon = "thumbtack"
value = 2
mob_trait = TRAIT_FAN_MIME
gain_text = "You are a big fan of the Mime. "
@@ -93,6 +99,7 @@
/datum/quirk/freerunning
name = "Freerunning"
desc = "You're great at quick moves! You can climb tables more quickly and take no damage from short falls."
+ icon = "running"
value = 8
mob_trait = TRAIT_FREERUNNING
gain_text = "You feel lithe on your feet! "
@@ -102,6 +109,7 @@
/datum/quirk/friendly
name = "Friendly"
desc = "You give the best hugs, especially when you're in the right mood."
+ icon = "hands-helping"
value = 2
mob_trait = TRAIT_FRIENDLY
gain_text = "You want to hug someone. "
@@ -112,6 +120,7 @@
/datum/quirk/jolly
name = "Jolly"
desc = "You sometimes just feel happy, for no reason at all."
+ icon = "grin"
value = 4
mob_trait = TRAIT_JOLLY
mood_quirk = TRUE
@@ -120,6 +129,7 @@
/datum/quirk/light_step
name = "Light Step"
desc = "You walk with a gentle step; footsteps and stepping on sharp objects is quieter and less painful. Also, your hands and clothes will not get messed in case of stepping in blood."
+ icon = "shoe-prints"
value = 4
mob_trait = TRAIT_LIGHT_STEP
gain_text = "You walk with a little more litheness. "
@@ -129,6 +139,7 @@
/datum/quirk/item_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 = 2
mob_trait = TRAIT_MUSICIAN
gain_text = "You know everything about musical instruments. "
@@ -141,6 +152,7 @@
/datum/quirk/night_vision
name = "Night Vision"
desc = "You can see slightly more clearly in full darkness than most people."
+ icon = "eye"
value = 4
mob_trait = TRAIT_NIGHT_VISION
gain_text = "The shadows seem a little less dark. "
@@ -164,6 +176,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 = 8
mob_trait = TRAIT_SELF_AWARE
medical_record_text = "Patient demonstrates an uncanny knack for self-diagnosis."
@@ -171,6 +184,7 @@
/datum/quirk/skittish
name = "Skittish"
desc = "You're easy to startle, and hide frequently. Run into a closed locker to jump into it, as long as you have access. You can walk to avoid this."
+ icon = "trash"
value = 8
mob_trait = TRAIT_SKITTISH
medical_record_text = "Patient demonstrates a high aversion to danger and has described hiding in containers out of fear."
@@ -179,6 +193,7 @@
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. Being in the chapel makes you happy."
value = 2 /// SKYRAT EDIT - Quirk Rebalance - Original: value = 4
+ icon = "bible"
mob_trait = TRAIT_SPIRITUAL
gain_text = "You have faith in a higher power. "
lose_text = "You lose faith! "
@@ -191,6 +206,7 @@
/datum/quirk/item_quirk/tagger
name = "Tagger"
desc = "You're an experienced artist. People will actually be impressed by your graffiti, and you can get twice as many uses out of drawing supplies."
+ icon = "spray-can"
value = 4
mob_trait = TRAIT_TAGGER
gain_text = "You know how to tag walls efficiently. "
@@ -203,6 +219,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 = 4
mob_trait = TRAIT_VORACIOUS
gain_text = "You feel HONGRY. "
diff --git a/code/datums/quirks/negative.dm b/code/datums/quirks/negative.dm
index ecfe58ebc03..e51e62bc0b0 100644
--- a/code/datums/quirks/negative.dm
+++ b/code/datums/quirks/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 evently weighted objects are fine, though."
+ icon = "hiking"
value = -8
mood_quirk = TRUE
gain_text = "Your back REALLY hurts! "
@@ -52,6 +53,7 @@
/datum/quirk/blooddeficiency
name = "Blood Deficiency"
desc = "Your body can't produce enough blood to sustain itself."
+ icon = "tint"
value = -8
gain_text = "You feel your vigor slowly fading away. "
lose_text = "You feel vigorous again. "
@@ -73,6 +75,7 @@
/datum/quirk/item_quirk/blindness
name = "Blind"
desc = "You are completely blind, nothing can counteract this."
+ icon = "eye-slash"
value = -16
gain_text = "You can't see anything. "
lose_text = "You miraculously gain back your vision. "
@@ -96,6 +99,7 @@
/datum/quirk/item_quirk/brainproblems
name = "Brain Tumor"
desc = "You have a little friend in your brain that is slowly destroying it. Better bring some mannitol!"
+ icon = "brain"
value = -12
gain_text = "You feel smooth. "
lose_text = "You feel wrinkled again. "
@@ -127,6 +131,7 @@
/datum/quirk/deafness
name = "Deaf"
desc = "You are incurably deaf."
+ icon = "deaf"
value = -8
mob_trait = TRAIT_DEAF
gain_text = "You can't hear anything. "
@@ -137,6 +142,7 @@
/datum/quirk/depression
name = "Depression"
desc = "You sometimes just hate life."
+ icon = "frown"
mob_trait = TRAIT_DEPRESSION
value = -3
gain_text = "You start feeling depressed. "
@@ -148,6 +154,7 @@
/datum/quirk/item_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 = -2
mood_quirk = TRUE
medical_record_text = "Patient demonstrates an unnatural attachment to a family heirloom."
@@ -221,6 +228,7 @@
/datum/quirk/frail
name = "Frail"
desc = "You have skin of paper and bones of glass! You suffer wounds much more easily than most."
+ icon = "skull"
value = -6
mob_trait = TRAIT_EASILY_WOUNDED
gain_text = "You feel frail. "
@@ -231,6 +239,7 @@
/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."
+ icon = "bed"
value = -2
mob_trait = TRAIT_HEAVY_SLEEPER
gain_text = "You feel sleepy. "
@@ -241,6 +250,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 = "You seem to make a big deal out of everything. "
lose_text = "You don't seem to make a big deal out of everything anymore. "
@@ -260,6 +270,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 = "Just the thought of drinking alcohol makes your head spin. "
@@ -270,6 +281,7 @@
/datum/quirk/item_quirk/nearsighted
name = "Nearsighted"
desc = "You are nearsighted without prescription glasses, but spawn with a pair."
+ icon = "glasses"
value = -4
gain_text = "Things far away from you start looking blurry. "
lose_text = "You start seeing faraway things normally again. "
@@ -288,6 +300,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 = -3
medical_record_text = "Patient demonstrates a fear of the dark. (Seriously?)"
hardcore_value = 5
@@ -327,6 +340,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 = -8
mob_trait = TRAIT_PACIFISM
gain_text = "You feel repulsed by the thought of violence! "
@@ -337,6 +351,7 @@
/datum/quirk/paraplegic
name = "Paraplegic"
desc = "Your legs do not function. Nothing will ever fix this. But hey, free wheelchair!"
+ icon = "wheelchair"
value = -12
human_only = TRUE
gain_text = null // Handled by trauma.
@@ -378,6 +393,7 @@
/datum/quirk/poor_aim
name = "Stormtrooper Aim"
desc = "You've never hit anything you were aiming for in your life."
+ icon = "bullseye"
value = -4
mob_trait = TRAIT_POOR_AIM
medical_record_text = "Patient possesses a strong tremor in both hands."
@@ -386,6 +402,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 = -4
mob_trait = TRAIT_PROSOPAGNOSIA
medical_record_text = "Patient suffers from prosopagnosia and cannot recognize faces."
@@ -394,6 +411,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 = -4
var/slot_string = "limb"
medical_record_text = "During physical examination, patient was found to have a prosthetic limb."
@@ -428,6 +446,7 @@
/datum/quirk/pushover
name = "Pushover"
desc = "Your first instinct is always to let people push you around. Resisting out of grabs will take conscious effort."
+ icon = "handshake"
value = -8
mob_trait = TRAIT_GRABWEAKNESS
gain_text = "You feel like a pushover. "
@@ -437,7 +456,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 = -8
mob_trait = TRAIT_INSANITY
gain_text = "... "
@@ -462,6 +482,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 = -3
gain_text = "You start worrying about what you're saying. "
lose_text = "You feel easier about talking again. " //if only it were that easy!
@@ -588,6 +609,7 @@
/datum/quirk/item_quirk/junkie
name = "Junkie"
desc = "You can't get enough of hard drugs."
+ icon = "pills"
value = -6
gain_text = "You suddenly feel the craving for drugs. "
medical_record_text = "Patient has a history of hard drugs."
@@ -676,6 +698,7 @@
/datum/quirk/item_quirk/junkie/smoker
name = "Smoker"
desc = "Sometimes you just really want a smoke. Probably not great for your lungs."
+ icon = "smoking"
value = -4
gain_text = "You could really go for a smoke right about now. "
medical_record_text = "Patient is a current smoker."
@@ -713,6 +736,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 = -10
mob_trait = TRAIT_UNSTABLE
gain_text = "There's a lot on your mind right now. "
@@ -723,6 +747,7 @@
/datum/quirk/item_quirk/allergic
name = "Extreme Medicine Allergy"
desc = "Ever since you were a kid, you've been allergic to certain chemicals..."
+ icon = "prescription-bottle"
value = -6
gain_text = "You feel your immune system shift. "
lose_text = "You feel your immune system phase back into perfect shape. "
@@ -781,6 +806,7 @@
/datum/quirk/bad_touch
name = "Bad Touch"
desc = "You don't like hugs. You'd really prefer if people just left you alone."
+ icon = "tg-bad-touch"
mob_trait = TRAIT_BADTOUCH
value = -1
gain_text = "You just want people to leave you alone. "
diff --git a/code/datums/quirks/neutral.dm b/code/datums/quirks/neutral.dm
index ad5bf3b1d70..8de07ddfbb9 100644
--- a/code/datums/quirks/neutral.dm
+++ b/code/datums/quirks/neutral.dm
@@ -4,6 +4,7 @@
/datum/quirk/extrovert
name = "Extrovert"
desc = "You are energized by talking to others, and enjoy spending your free time in the bar."
+ icon = "users"
value = 0
mob_trait = TRAIT_EXTROVERT
gain_text = "You feel like hanging out with other people. "
@@ -13,6 +14,7 @@
/datum/quirk/introvert
name = "Introvert"
desc = "You are energized by having time to yourself, and enjoy spending your free time in the library."
+ icon = "book-reader"
value = 0
mob_trait = TRAIT_INTROVERT
gain_text = "You feel like reading a good book quietly. "
@@ -22,6 +24,7 @@
/datum/quirk/no_taste
name = "Ageusia"
desc = "You can't taste anything! Toxic food will still poison you."
+ icon = "meh-blank"
value = 0
mob_trait = TRAIT_AGEUSIA
gain_text = "You can't taste anything! "
@@ -31,6 +34,7 @@
/datum/quirk/foreigner
name = "Foreigner"
desc = "You're not from around here. You don't know Galactic Common!"
+ icon = "language"
value = 0
gain_text = "The words being spoken around you don't make any sense."
lose_text = "You've developed fluency in Galactic Common."
@@ -51,6 +55,7 @@
/datum/quirk/vegetarian
name = "Vegetarian"
desc = "You find the idea of eating meat morally and physically repulsive."
+ icon = "carrot"
value = 0
gain_text = "You feel repulsion at the idea of eating meat. "
lose_text = "You feel like eating meat isn't that bad. "
@@ -80,6 +85,7 @@
/datum/quirk/snob
name = "Snob"
desc = "You care about the finer things, if a room doesn't look nice its just not really worth it, is it?"
+ icon = "user-tie"
value = 0
gain_text = "You feel like you understand what things should look like. "
lose_text = "Well who cares about deco anyways? "
@@ -89,6 +95,7 @@
/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 = "You feel an intense craving for pineapple. "
lose_text = "Your feelings towards pineapples seem to return to a lukewarm state. "
@@ -112,6 +119,7 @@
/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 = "You find yourself pondering what kind of idiot actually enjoys pineapples... "
lose_text = "Your feelings towards pineapples seem to return to a lukewarm state. "
@@ -135,6 +143,7 @@
/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 = "You start craving something that tastes strange. "
lose_text = "You feel like eating normal food again. "
@@ -163,6 +172,7 @@
/datum/quirk/monochromatic
name = "Monochromacy"
desc = "You suffer from full colorblindness, and perceive nearly the entire world in blacks and whites."
+ icon = "adjust"
value = 0
medical_record_text = "Patient is afflicted with almost complete color blindness."
@@ -180,13 +190,13 @@
/datum/quirk/phobia
name = "Phobia"
desc = "You are irrationally afraid of something."
+ icon = "spider"
value = 0
medical_record_text = "Patient has an irrational fear of something."
var/phobia
/datum/quirk/phobia/add()
- if(!phobia && quirk_holder.client?.prefs.phobia)
- phobia = quirk_holder.client?.prefs.phobia
+ phobia = phobia || quirk_holder.client?.prefs?.read_preference(/datum/preference/choiced/phobia)
if(phobia)
var/mob/living/carbon/human/human_holder = quirk_holder
@@ -195,7 +205,7 @@
/datum/quirk/phobia/post_add()
if(!phobia)
var/mob/living/carbon/human/human_holder = quirk_holder
- phobia = human_holder.client.prefs.phobia
+ phobia = human_holder.client.prefs.read_preference(/datum/preference/choiced/phobia)
human_holder.gain_trauma(new /datum/brain_trauma/mild/phobia(phobia), TRAUMA_RESILIENCE_ABSOLUTE)
/datum/quirk/phobia/remove()
@@ -205,6 +215,7 @@
/datum/quirk/item_quirk/needswayfinder
name = "Navigationally Challenged"
desc = "Lacking familiarity with certain stations, you start with a wayfinding pinpointer where available."
+ icon = "route"
value = 0
medical_record_text = "Patient demonstrates a keen ability to get lost."
@@ -223,6 +234,7 @@
/datum/quirk/item_quirk/bald
name = "Smooth-Headed"
desc = "You have no hair and are quite insecure about it! Keep your wig on, or at least your head covered up."
+ icon = "egg"
value = 0
mob_trait = TRAIT_BALD
gain_text = "Your head is as smooth as can be, it's terrible. "
@@ -277,6 +289,7 @@
/datum/quirk/item_quirk/tongue_tied
name = "Tongue Tied"
desc = "Due to a past incident, your ability to communicate has been relegated to your hands."
+ icon = "sign-language"
value = 0
medical_record_text = "During physical examination, patient's tongue was found to be uniquely damaged."
@@ -297,6 +310,7 @@
/datum/quirk/item_quirk/photographer
name = "Photographer"
desc = "You carry your camera and personal photo album everywhere you go, and your scrapbooks are legendary among your coworkers."
+ icon = "camera"
value = 0
mob_trait = TRAIT_PHOTOGRAPHER
gain_text = "You know everything about photography. "
@@ -325,6 +339,7 @@
/datum/quirk/item_quirk/colorist
name = "Colorist"
desc = "You like carrying around a hair dye spray to quickly apply color patterns to your hair."
+ icon = "fill-drip"
value = 0
medical_record_text = "Patient enjoys dyeing their hair with pretty colors."
diff --git a/code/datums/view.dm b/code/datums/view.dm
index 65eb0953e5c..860ccb8bf1f 100644
--- a/code/datums/view.dm
+++ b/code/datums/view.dm
@@ -26,10 +26,10 @@
winset(chief, "mapwindow.map", "zoom=0")
/datum/view_data/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/view_data/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/view_data/proc/isZooming()
return (width || height)
diff --git a/code/game/atom_defense.dm b/code/game/atom_defense.dm
index 2cf9fb2cca6..d7b621edd82 100644
--- a/code/game/atom_defense.dm
+++ b/code/game/atom_defense.dm
@@ -81,7 +81,12 @@
switch(damage_type)
if(BRUTE)
if(damage_amount)
- playsound(src, 'sound/weapons/smash.ogg', 50, TRUE)
+ //SKYRAT EDIT ADDITION - CREDITS TO WHITEDREAM(valtos)
+ playsound(src, pick('modular_skyrat/master_files/sound/effects/metalblock1.wav', 'modular_skyrat/master_files/sound/effects/metalblock2.wav', \
+ 'modular_skyrat/master_files/sound/effects/metalblock3.wav', 'modular_skyrat/master_files/sound/effects/metalblock4.wav', \
+ 'modular_skyrat/master_files/sound/effects/metalblock5.wav', 'modular_skyrat/master_files/sound/effects/metalblock6.wav', \
+ 'modular_skyrat/master_files/sound/effects/metalblock7.wav', 'modular_skyrat/master_files/sound/effects/metalblock8.wav'), 50, TRUE)
+ //SKYRAT EDIT END
else
playsound(src, 'sound/weapons/tap.ogg', 50, TRUE)
if(BURN)
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 54a591c9aa7..751ae1eb36a 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -633,19 +633,21 @@
* Produces a signal [COMSIG_PARENT_EXAMINE]
*/
/atom/proc/examine(mob/user)
- . = list("[get_examine_string(user, TRUE)].")
+ . = list("[get_examine_string(user, TRUE)]. ")
. += get_name_chaser(user)
if(desc)
. += desc
if(custom_materials)
+ . += " " //SKYRAT EDIT ADDITION
var/list/materials_list = list()
for(var/i in custom_materials)
var/datum/material/M = i
materials_list += "[M.name]"
. += "It is made out of [english_list(materials_list)] ."
if(reagents)
+ . += " " //SKYRAT EDIT ADDITION
if(reagents.flags & TRANSPARENT)
. += "It contains:"
if(length(reagents.reagent_list))
@@ -2127,10 +2129,10 @@
status_bar_set_text(usr, name)
// Screentips
if(usr?.hud_used)
- if(!usr.client?.prefs.screentip_pref || (flags_1 & NO_SCREENTIPS_1))
+ if(!usr.hud_used.screentips_enabled || (flags_1 & NO_SCREENTIPS_1))
usr.hud_used.screentip_text.maptext = ""
else
- usr.hud_used.screentip_text.maptext = MAPTEXT("[name] ")
+ usr.hud_used.screentip_text.maptext = MAPTEXT("[name] ")
/// Gets a merger datum representing the connected blob of objects in the allowed_types argument
/atom/proc/GetMergeGroup(id, list/allowed_types)
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
index a5f9af6e66a..91297884e82 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
@@ -18,7 +18,7 @@
var/list/mob/candidates = list()
/// List of players that were selected for this rule
var/list/datum/mind/assigned = list()
- /// Preferences flag such as ROLE_WIZARD that need to be turned on for players to be antag
+ /// Preferences flag such as ROLE_WIZARD that need to be turned on for players to be antag.
var/antag_flag = null
/// The antagonist datum that is assigned to the mobs mind on ruleset execution.
var/datum/antagonist/antag_datum = null
@@ -57,6 +57,8 @@
var/datum/game_mode/dynamic/mode = null
/// If a role is to be considered another for the purpose of banning.
var/antag_flag_override = null
+ /// If set, will check this preference instead of antag_flag.
+ var/antag_preference = null
/// If a ruleset type which is in this list has been executed, then the ruleset will not be executed.
var/list/blocking_rules = list()
/// The minimum amount of players required for the rule to be considered.
@@ -201,7 +203,7 @@
continue
//SKYRAT EDIT ADDITION
- if(!candidate_client.prefs?.be_antag)
+ if(!candidate_client.prefs?.read_preference(/datum/preference/toggle/be_antag))
candidates.Remove(candidate_player)
continue
//SKYRAT EDIT END
@@ -214,14 +216,13 @@
candidates.Remove(candidate_player)
continue
- if(antag_flag_override)
- if(!(antag_flag_override in candidate_client.prefs.be_special) || is_banned_from(candidate_player.ckey, list(antag_flag_override, ROLE_SYNDICATE)))
- candidates.Remove(candidate_player)
- continue
- else
- if(!(antag_flag in candidate_client.prefs.be_special) || is_banned_from(candidate_player.ckey, list(antag_flag, ROLE_SYNDICATE)))
- candidates.Remove(candidate_player)
- continue
+ if (!((antag_preference || antag_flag) in candidate_client.prefs.be_special))
+ candidates.Remove(candidate_player)
+ continue
+
+ if (is_banned_from(candidate_player.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE)))
+ candidates.Remove(candidate_player)
+ continue
// If this ruleset has exclusive_roles set, we want to only consider players who have those
// job prefs enabled and are eligible to play that job. Otherwise, continue as before.
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
index 4321a3997c8..c1b4f7ee547 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
@@ -14,12 +14,8 @@
candidates.Remove(P)
else if((exclusive_roles.len > 0) && !(P.mind.assigned_role.title in exclusive_roles)) // Is the rule exclusive to their job?
candidates.Remove(P)
- else if(antag_flag_override)
- if(!(antag_flag_override in P.client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag_override, ROLE_SYNDICATE)))
- candidates.Remove(P)
- else
- if(!(antag_flag in P.client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag, ROLE_SYNDICATE)))
- candidates.Remove(P)
+ else if (!((antag_preference || antag_flag) in P.client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE)))
+ candidates.Remove(P)
/datum/dynamic_ruleset/latejoin/ready(forced = 0)
if (!forced)
@@ -52,7 +48,8 @@
/datum/dynamic_ruleset/latejoin/infiltrator
name = "Syndicate Infiltrator"
antag_datum = /datum/antagonist/traitor
- antag_flag = ROLE_TRAITOR
+ antag_flag = ROLE_SYNDICATE_INFILTRATOR
+ antag_flag_override = ROLE_TRAITOR
protected_roles = list("Prisoner", "Blueshield", "Corrections Officer", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Head of Security", "Captain", "Nanotrasen Representative", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Vanguard Operative") //SKYRAT EDIT - SEC_HAUL
restricted_roles = list("AI","Cyborg")
required_candidates = 1
@@ -71,7 +68,7 @@
name = "Provocateur"
persistent = TRUE
antag_datum = /datum/antagonist/rev/head
- antag_flag = ROLE_REV_HEAD
+ antag_flag = ROLE_PROVOCATEUR
antag_flag_override = ROLE_REV
restricted_roles = list("AI", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Corrections Officer", "Chief Medical Officer", "Research Director", "Vanguard Operative", "Nanotrasen Representative", "Blueshield", "Civil Disputes Officer", "Security Sergeant") //SKYRAT EDIT - Sec_haul
enemy_roles = list("AI", "Cyborg", "Security Officer","Detective","Head of Security", "Captain", "Warden")
@@ -147,7 +144,8 @@
/datum/dynamic_ruleset/latejoin/heretic_smuggler
name = "Heretic Smuggler"
antag_datum = /datum/antagonist/heretic
- antag_flag = ROLE_HERETIC
+ antag_flag = ROLE_HERETIC_SMUGGLER
+ antag_flag_override = ROLE_HERETIC
protected_roles = list("Prisoner", "Blueshield", "Corrections Officer", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Head of Security", "Captain", "Nanotrasen Representative", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Vanguard Operative") //SKYRAT EDIT - SEC_HAUL
restricted_roles = list("AI","Cyborg")
required_candidates = 1
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 8b972410e04..724d106ff26 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -46,14 +46,12 @@
if(M.client.get_remaining_days(minimum_required_age) > 0)
trimmed_list.Remove(M)
continue
- if(antag_flag_override)
- if(!(antag_flag_override in M.client.prefs.be_special) || is_banned_from(M.ckey, list(antag_flag_override, ROLE_SYNDICATE)))
- trimmed_list.Remove(M)
- continue
- else
- if(!(antag_flag in M.client.prefs.be_special) || is_banned_from(M.ckey, list(antag_flag, ROLE_SYNDICATE)))
- trimmed_list.Remove(M)
- continue
+ if (!((antag_preference || antag_flag) in M.client.prefs.be_special))
+ trimmed_list.Remove(M)
+ continue
+ if (is_banned_from(M.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE)))
+ trimmed_list.Remove(M)
+ continue
if (M.mind)
if (restrict_ghost_roles && (M.mind.assigned_role.title in GLOB.exp_specialmap[EXP_TYPE_SPECIAL])) // Are they playing a ghost role?
trimmed_list.Remove(M)
@@ -104,7 +102,7 @@
message_admins("Polling [possible_volunteers.len] players to apply for the [name] ruleset.")
log_game("DYNAMIC: Polling [possible_volunteers.len] players to apply for the [name] ruleset.")
- candidates = pollGhostCandidates("The mode is looking for volunteers to become [antag_flag] for [name]", antag_flag, antag_flag_override ? antag_flag_override : antag_flag, poll_time = 300)
+ candidates = pollGhostCandidates("The mode is looking for volunteers to become [antag_flag] for [name]", antag_flag_override, antag_flag || antag_flag_override, poll_time = 300)
if(!candidates || candidates.len <= 0)
mode.dynamic_log("The ruleset [name] received no applications.")
@@ -184,7 +182,8 @@
/datum/dynamic_ruleset/midround/autotraitor
name = "Syndicate Sleeper Agent"
antag_datum = /datum/antagonist/traitor
- antag_flag = ROLE_TRAITOR
+ antag_flag = ROLE_SLEEPER_AGENT
+ antag_flag_override = ROLE_TRAITOR
protected_roles = list("Prisoner", "Blueshield", "Corrections Officer", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Head of Security", "Captain", "Nanotrasen Representative", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Vanguard Operative") //SKYRAT EDIT - SEC_HAUL
restricted_roles = list("Cyborg", "AI", "Positronic Brain")
required_candidates = 1
@@ -248,7 +247,9 @@
/datum/dynamic_ruleset/midround/families
name = "Family Head Aspirants"
persistent = TRUE
- antag_flag = ROLE_FAMILIES
+ antag_datum = /datum/antagonist/gang
+ antag_flag = ROLE_FAMILY_HEAD_ASPIRANT
+ antag_flag_override = ROLE_FAMILIES
protected_roles = list("Prisoner", "Head of Personnel")
restricted_roles = list("Cyborg", "AI", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Corrections Officer", "Vanguard Operative", "Nanotrasen Representative", "Blueshield", "Civil Disputes Officer", "Security Sergeant") //SKYRAT EDIT - Sec_haul
required_candidates = 6 // gotta have 'em ALL
@@ -317,7 +318,8 @@
/datum/dynamic_ruleset/midround/malf
name = "Malfunctioning AI"
antag_datum = /datum/antagonist/malf_ai
- antag_flag = ROLE_MALF
+ antag_flag = ROLE_MALF_MIDROUND
+ antag_flag_override = ROLE_MALF
enemy_roles = list("Security Officer", "Warden","Detective","Head of Security", "Captain", "Scientist", "Chemist", "Research Director", "Chief Engineer", "Blueshield")
exclusive_roles = list("AI")
required_enemies = list(4,4,4,4,4,4,2,2,2,0)
@@ -367,7 +369,8 @@
/datum/dynamic_ruleset/midround/from_ghosts/wizard
name = "Wizard"
antag_datum = /datum/antagonist/wizard
- antag_flag = ROLE_WIZARD
+ antag_flag = ROLE_WIZARD_MIDROUND
+ antag_flag_override = ROLE_WIZARD
enemy_roles = list("Security Officer","Detective","Head of Security", "Captain")
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
@@ -397,7 +400,8 @@
/datum/dynamic_ruleset/midround/from_ghosts/nuclear
name = "Nuclear Assault"
- antag_flag = ROLE_OPERATIVE
+ antag_flag = ROLE_OPERATIVE_MIDROUND
+ antag_flag_override = ROLE_OPERATIVE
antag_datum = /datum/antagonist/nukeop
enemy_roles = list("AI", "Cyborg", "Security Officer", "Warden","Detective","Head of Security", "Captain")
required_enemies = list(3,3,3,3,3,2,1,1,0,0)
@@ -456,8 +460,9 @@
/// Infects a random player, making them explode into a blob.
/datum/dynamic_ruleset/midround/blob_infection
name = "Blob Infection"
- antag_datum = /datum/antagonist/blob
- antag_flag = ROLE_BLOB
+ antag_datum = /datum/antagonist/blob/infection
+ antag_flag = ROLE_BLOB_INFECTION
+ antag_flag_override = ROLE_BLOB
protected_roles = list("Prisoner", "Blueshield", "Corrections Officer", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Head of Security", "Captain", "Nanotrasen Representative", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Vanguard Operative") //SKYRAT EDIT - SEC_HAUL
restricted_roles = list("Cyborg", "AI", "Positronic Brain")
enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain")
@@ -633,8 +638,8 @@
/datum/dynamic_ruleset/midround/from_ghosts/abductors
name = "Abductors"
- antag_flag = "Abductor"
- antag_flag_override = ROLE_ABDUCTOR
+ antag_datum = /datum/antagonist/abductor
+ antag_flag = ROLE_ABDUCTOR
enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain")
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 2
@@ -672,8 +677,7 @@
/datum/dynamic_ruleset/midround/swarmers
name = "Swarmers"
antag_datum = /datum/antagonist/swarmer
- antag_flag = "Swarmer"
- antag_flag_override = ROLE_ALIEN
+ antag_flag = ROLE_SWARMER
required_type = /mob/dead/observer
enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain")
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
@@ -707,8 +711,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/space_ninja
name = "Space Ninja"
antag_datum = /datum/antagonist/ninja
- antag_flag = "Space Ninja"
- antag_flag_override = ROLE_NINJA
+ antag_flag = ROLE_NINJA
enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain")
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
@@ -747,8 +750,7 @@
/datum/dynamic_ruleset/midround/spiders
name = "Spiders"
- antag_flag = "Spider"
- antag_flag_override = ROLE_ALIEN
+ antag_flag = ROLE_SPIDER
required_type = /mob/dead/observer
enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain")
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
@@ -767,8 +769,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/revenant
name = "Revenant"
antag_datum = /datum/antagonist/revenant
- antag_flag = "Revenant"
- antag_flag_override = ROLE_REVENANT
+ antag_flag = ROLE_REVENANT
enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain")
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
required_candidates = 1
@@ -814,8 +815,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/sentient_disease
name = "Sentient Disease"
antag_datum = /datum/antagonist/disease
- antag_flag = "Sentient Disease"
- antag_flag_override = ROLE_ALIEN
+ antag_flag = ROLE_SENTIENT_DISEASE
required_candidates = 1
weight = 4
cost = 10
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
index cbc2fb67375..3c7574bd314 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
@@ -11,7 +11,7 @@
antag_flag = ROLE_TRAITOR
antag_datum = /datum/antagonist/traitor
minimum_required_age = 0
- protected_roles = list("Prisoner", "Blueshield", "Corrections Officer", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Head of Security", "Captain", "Nanotrasen Representative", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Vanguard Operative") //SKYRAT EDIT - SEC_HAUL
+ protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain")
restricted_roles = list("AI", "Cyborg")
required_candidates = 1
weight = 5
@@ -92,8 +92,8 @@
/datum/dynamic_ruleset/roundstart/traitorbro
name = "Blood Brothers"
antag_flag = ROLE_BROTHER
- antag_datum = /datum/antagonist/brother/
- protected_roles = list("Prisoner", "Blueshield", "Corrections Officer", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Head of Security", "Captain", "Nanotrasen Representative", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Vanguard Operative") //SKYRAT EDIT - SEC_HAUL
+ antag_datum = /datum/antagonist/brother
+ protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain")
restricted_roles = list("Cyborg", "AI")
required_candidates = 2
weight = 4
@@ -142,7 +142,7 @@
name = "Changelings"
antag_flag = ROLE_CHANGELING
antag_datum = /datum/antagonist/changeling
- protected_roles = list("Prisoner", "Blueshield", "Corrections Officer", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Head of Security", "Captain", "Nanotrasen Representative", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Vanguard Operative") //SKYRAT EDIT - SEC_HAUL
+ protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain")
restricted_roles = list("AI", "Cyborg")
required_candidates = 1
weight = 3
@@ -179,7 +179,7 @@
name = "Heretics"
antag_flag = ROLE_HERETIC
antag_datum = /datum/antagonist/heretic
- protected_roles = list("Prisoner", "Blueshield", "Corrections Officer", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Head of Security", "Captain", "Nanotrasen Representative", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster", "Vanguard Operative") //SKYRAT EDIT - SEC_HAUL
+ protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain")
restricted_roles = list("AI", "Cyborg")
required_candidates = 1
weight = 3
@@ -268,7 +268,7 @@
antag_flag = ROLE_CULTIST
antag_datum = /datum/antagonist/cult
minimum_required_age = 14
- restricted_roles = list("AI", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Chaplain", "Head of Personnel", "Corrections Officer")
+ restricted_roles = list("AI", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Chaplain", "Head of Personnel")
required_candidates = 2
weight = 3
cost = 20
@@ -411,7 +411,7 @@
antag_flag_override = ROLE_REV
antag_datum = /datum/antagonist/rev/head
minimum_required_age = 14
- restricted_roles = list("AI", "Corrections Officer", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director")
+ restricted_roles = list("AI", "Cyborg", "Prisoner", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director")
required_candidates = 3
weight = 3
delay = 7 MINUTES
@@ -492,9 +492,10 @@
/datum/dynamic_ruleset/roundstart/families
name = "Families"
persistent = TRUE
+ antag_datum = /datum/antagonist/gang
antag_flag = ROLE_FAMILIES
protected_roles = list("Prisoner", "Head of Personnel")
- restricted_roles = list("Cyborg", "AI", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Corrections Officer", "Vanguard Operative", "Nanotrasen Representative", "Blueshield", "Civil Disputes Officer", "Security Sergeant") //SKYRAT EDIT - SEC_HAUL
+ restricted_roles = list("Cyborg", "AI", "Security Officer", "Warden", "Detective", "Head of Security", "Captain")
required_candidates = 6 // gotta have 'em ALL
weight = 2
cost = 30
@@ -561,22 +562,25 @@
//////////////////////////////////////////////
/datum/dynamic_ruleset/roundstart/nuclear/clown_ops
- name = "Clown Ops"
+ name = "Clown Operatives"
antag_datum = /datum/antagonist/nukeop/clownop
+ antag_flag = ROLE_CLOWN_OPERATIVE
+ antag_flag_override = ROLE_OPERATIVE
antag_leader_datum = /datum/antagonist/nukeop/leader/clownop
requirements = list(101,101,101,101,101,101,101,101,101,101)
/datum/dynamic_ruleset/roundstart/nuclear/clown_ops/pre_execute()
. = ..()
if(.)
- for(var/obj/machinery/nuclearbomb/syndicate/S in GLOB.nuke_list)
- var/turf/T = get_turf(S)
- if(T)
- qdel(S)
- new /obj/machinery/nuclearbomb/syndicate/bananium(T)
- for(var/datum/mind/V in assigned)
- V.set_assigned_role(SSjob.GetJobType(/datum/job/clown_operative))
- V.special_role = ROLE_CLOWN_OPERATIVE
+ var/obj/machinery/nuclearbomb/syndicate/syndicate_nuke = locate() in GLOB.nuke_list
+ if(syndicate_nuke)
+ var/turf/nuke_turf = get_turf(syndicate_nuke)
+ if(nuke_turf)
+ new /obj/machinery/nuclearbomb/syndicate/bananium(nuke_turf)
+ qdel(syndicate_nuke)
+ for(var/datum/mind/clowns in assigned)
+ clowns.set_assigned_role(SSjob.GetJobType(/datum/job/clown_operative))
+ clowns.special_role = ROLE_CLOWN_OPERATIVE
//////////////////////////////////////////////
diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm
index 6a085b3584a..227995af6c2 100644
--- a/code/game/machinery/computer/crew.dm
+++ b/code/game/machinery/computer/crew.dm
@@ -235,7 +235,7 @@ GLOBAL_DATUM_INIT(crewmonitor, /datum/crewmonitor, new)
if (id_card)
entry["name"] = id_card.registered_name
entry["assignment"] = id_card.assignment
- entry["ijob"] = jobs[id_card.real_title || id_card.assignment] //SKYRAT EDIT - ALTERNATE JOB TITLES
+ entry["ijob"] = jobs[id_card.assignment] //SKYRAT EDIT - ALTERNATE JOB TITLES
// Binary living/dead status
if (sensor_mode >= SENSOR_LIVING)
diff --git a/code/game/machinery/computer/security.dm b/code/game/machinery/computer/security.dm
index f19bff0ec1d..20c2c4b1fa4 100644
--- a/code/game/machinery/computer/security.dm
+++ b/code/game/machinery/computer/security.dm
@@ -802,7 +802,7 @@ What a mess.*/
active1.fields["age"] = t1
if("species")
if(istype(active1, /datum/data/record))
- var/t1 = input("Select a species", "Species Selection") as null|anything in GLOB.roundstart_races
+ var/t1 = input("Select a species", "Species Selection") as null|anything in get_selectable_species()
if(!canUseSecurityRecordsConsole(usr, t1, a1))
return
active1.fields["species"] = t1
@@ -1042,7 +1042,7 @@ What a mess.*/
if(6)
R.fields["m_stat"] = pick("*Insane*", "*Unstable*", "*Watch*", "Stable")
if(7)
- R.fields["species"] = pick(GLOB.roundstart_races)
+ R.fields["species"] = pick(get_selectable_species())
if(8)
var/datum/data/record/G = pick(GLOB.data_core.general)
R.fields["photo_front"] = G.fields["photo_front"]
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 2aeb82d33b7..8aab783c33c 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -284,7 +284,12 @@
if(glass)
playsound(loc, 'sound/effects/glasshit.ogg', 90, TRUE)
else if(damage_amount)
- playsound(loc, 'sound/weapons/smash.ogg', 50, TRUE)
+ //SKYRAT EDIT ADDITION - CREDITS TO WHITEDREAM(valtos)
+ playsound(src, pick('modular_skyrat/master_files/sound/effects/metalblock1.wav', 'modular_skyrat/master_files/sound/effects/metalblock2.wav', \
+ 'modular_skyrat/master_files/sound/effects/metalblock3.wav', 'modular_skyrat/master_files/sound/effects/metalblock4.wav', \
+ 'modular_skyrat/master_files/sound/effects/metalblock5.wav', 'modular_skyrat/master_files/sound/effects/metalblock6.wav', \
+ 'modular_skyrat/master_files/sound/effects/metalblock7.wav', 'modular_skyrat/master_files/sound/effects/metalblock8.wav'), 50, TRUE)
+ //SKYRAT EDIT END
else
playsound(src, 'sound/weapons/tap.ogg', 50, TRUE)
if(BURN)
diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm
index b5879420ee6..adf96ba3239 100644
--- a/code/game/machinery/hologram.dm
+++ b/code/game/machinery/hologram.dm
@@ -216,15 +216,6 @@ Possible to do for anyone motivated enough:
AI = null
else
aiPlayer = AI
- if(aiPlayer)
- if(aiPlayer.client)
- if(length(aiPlayer.client.prefs.features["silicon_flavor_text"]))
- var/message = aiPlayer.client.prefs.features["silicon_flavor_text"]
- if(length_char(message) <= 40)
- line = "[message] "
- else
- line = "[copytext_char(message, 1, 37)]... More... "
- line += " \[OOC\] "
if(LAZYLEN(masters))
if(holo.Impersonation)
. += holo.Impersonation.examine(user)
@@ -510,7 +501,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/
for(var/I in holo_calls)
var/datum/holocall/HC = I
if(HC.connected_holopad == src)
- if(speaker == HC.hologram && HC.user.client?.prefs.chat_on_map)
+ if(speaker == HC.hologram && HC.user.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat))
HC.user.create_chat_message(speaker, message_language, raw_message, spans)
else
HC.user.Hear(message, speaker, message_language, raw_message, radio_freq, spans, message_mods)
diff --git a/code/game/machinery/telecomms/broadcasting.dm b/code/game/machinery/telecomms/broadcasting.dm
index 636868de3e5..aa2bf8cc736 100644
--- a/code/game/machinery/telecomms/broadcasting.dm
+++ b/code/game/machinery/telecomms/broadcasting.dm
@@ -170,11 +170,6 @@
// From the list of radios, find all mobs who can hear those.
var/list/receive = get_mobs_in_radio_ranges(radios)
- // Cut out mobs with clients who are admins and have radio chatter disabled.
- for(var/mob/R in receive)
- if (R.client && R.client.holder && !(R.client.prefs.chat_toggles & CHAT_RADIO))
- receive -= R
-
// Add observers who have ghost radio enabled.
for(var/mob/dead/observer/M in GLOB.player_list)
if(M.client.prefs.chat_toggles & CHAT_GHOSTRADIO)
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 0ec8e082303..e817fc4b50b 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -879,10 +879,10 @@ attack_basic_mob
. = ..()
if(get(src, /mob) == usr && !QDELETED(src))
var/mob/living/L = usr
- if(usr.client.prefs.enable_tips)
- var/timedelay = usr.client.prefs.tip_delay/100
+ if(usr.client.prefs.read_preference(/datum/preference/toggle/enable_tooltips))
+ var/timedelay = usr.client.prefs.read_preference(/datum/preference/numeric/tooltip_delay) / 100
tip_timer = addtimer(CALLBACK(src, .proc/openTip, location, control, params, usr), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it.
- if(usr.client.prefs.itemoutline_pref)
+ if(usr.client.prefs.read_preference(/datum/preference/toggle/item_outlines))
if(istype(L) && L.incapacitated())
apply_outline(COLOR_RED_GRAY) //if they're dead or handcuffed, let's show the outline as red to indicate that they can't interact with that right now
else
@@ -900,7 +900,7 @@ attack_basic_mob
/obj/item/proc/apply_outline(outline_color = null)
if(get(src, /mob) != usr || QDELETED(src) || isobserver(usr)) //cancel if the item isn't in an inventory, is being deleted, or if the person hovering is a ghost (so that people spectating you don't randomly make your items glow)
return
- var/theme = lowertext(usr.client.prefs.UI_style)
+ var/theme = lowertext(usr.client?.prefs?.read_preference(/datum/preference/choiced/ui_style))
if(!outline_color) //if we weren't provided with a color, take the theme's color
switch(theme) //yeah it kinda has to be this way
if("midnight")
diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm
index 195f70430d5..21c3672112b 100644
--- a/code/game/objects/items/crayons.dm
+++ b/code/game/objects/items/crayons.dm
@@ -1,5 +1,3 @@
-#define DARK_COLOR_LIGHTNESS_THRESHOLD 0.25
-
#define RANDOM_GRAFFITI "Random Graffiti"
#define RANDOM_LETTER "Random Letter"
#define RANDOM_PUNCTUATION "Random Punctuation"
@@ -762,9 +760,7 @@
if(isobj(target) && !(target.flags_1 & UNPAINTABLE_1))
if(actually_paints)
- var/list/rgb = hex2rgb(paint_color)
- var/list/hsl = rgb2hsl(rgb[1], rgb[2], rgb[3])
- var/color_is_dark = hsl[3] < DARK_COLOR_LIGHTNESS_THRESHOLD
+ var/color_is_dark = is_color_dark(paint_color)
if (color_is_dark && !(target.flags_1 & ALLOW_DARK_PAINTS_1))
to_chat(user, span_warning("A color that dark on an object like this? Surely not..."))
@@ -865,7 +861,6 @@
charges = -1
desc = "Now with 30% more bluespace technology."
-#undef DARK_COLOR_LIGHTNESS_THRESHOLD
#undef RANDOM_GRAFFITI
#undef RANDOM_LETTER
#undef RANDOM_PUNCTUATION
diff --git a/code/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm
index fcb4fa914b3..f6435b65335 100644
--- a/code/game/objects/items/devices/PDA/PDA.dm
+++ b/code/game/objects/items/devices/PDA/PDA.dm
@@ -129,8 +129,8 @@ GLOBAL_LIST_EMPTY(PDAs)
. = ..()
if(!equipped)
if(user.client)
- background_color = user.client.prefs.pda_color
- switch(user.client.prefs.pda_style)
+ background_color = user.client.prefs.read_preference(/datum/preference/color/pda_color)
+ switch(user.client.prefs.read_preference(/datum/preference/choiced/pda_style))
if(MONO)
font_index = MODE_MONO
font_mode = FONT_MONO
@@ -146,9 +146,6 @@ GLOBAL_LIST_EMPTY(PDAs)
else
font_index = MODE_MONO
font_mode = FONT_MONO
- //SKYRAT EDIT ADDITION BEGIN - CUSTOMIZATION
- ttone = user.client.prefs.pda_ringer
- //SKYRAT EDIT ADDITION END
equipped = TRUE
/obj/item/pda/proc/update_label()
diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm
index 4b455a497f2..8e81dddbdca 100644
--- a/code/game/objects/items/devices/scanners.dm
+++ b/code/game/objects/items/devices/scanners.dm
@@ -429,7 +429,7 @@ GENE SCANNER
render_list += "Detected cybernetic modifications: \n"
render_list += "[cyberimp_detect] \n"
- to_chat(user, jointext(render_list, ""), trailing_newline = FALSE) // we handled the last so we don't need handholding
+ to_chat(user, examine_block(jointext(render_list, "")), trailing_newline = FALSE) // we handled the last so we don't need handholding //SKYRAT EDIT CHANGE
/proc/chemscan(mob/living/user, mob/living/M)
if(user.incapacitated())
@@ -477,7 +477,7 @@ GENE SCANNER
if(M.has_status_effect(/datum/status_effect/eigenstasium))
render_list += "Subject is temporally unstable. Stabilising agent is recommended to reduce disturbances. \n"
- to_chat(user, jointext(render_list, ""), trailing_newline = FALSE) // we handled the last so we don't need handholding
+ to_chat(user, examine_block(jointext(render_list, "")), trailing_newline = FALSE) // we handled the last so we don't need handholding //SKYRAT EDIT CHANGE
/obj/item/healthanalyzer/verb/toggle_mode()
set name = "Switch Verbosity"
@@ -522,7 +522,7 @@ GENE SCANNER
else
to_chat(user, "No wounds detected in subject. ")
else
- to_chat(user, jointext(render_list, ""))
+ to_chat(user, examine_block(jointext(render_list, ""))) //SKYRAT EDIT CHANGE
/obj/item/healthanalyzer/wound
name = "first aid analyzer"
@@ -642,7 +642,7 @@ GENE SCANNER
var/gas_concentration = env_gases[id][MOLES]/total_moles
render_list += "[span_alert("[env_gases[id][GAS_META][META_GAS_NAME]]: [round(gas_concentration*100, 0.01)] % ([round(env_gases[id][MOLES], 0.01)] mol)")]\n"
render_list += "[span_info("Temperature: [round(environment.temperature-T0C, 0.01)] °C ([round(environment.temperature, 0.01)] K)")]\n"
- to_chat(user, jointext(render_list, ""), trailing_newline = FALSE) // we handled the last so we don't need handholding
+ to_chat(user, examine_block(jointext(render_list, "")), trailing_newline = FALSE) // we handled the last so we don't need handholding //SKYRAT EDIT CHANGE
/obj/item/analyzer/AltClick(mob/user) //Barometer output for measuring when the next storm happens
..()
@@ -750,7 +750,7 @@ GENE SCANNER
render_list += "[span_boldnotice("Large amounts of free neutrons detected in the air indicate that a fusion reaction took place.")]\
\n[span_notice("Instability of the last fusion reaction: [round(cached_scan_results["fusion"], 0.01)].")]"
- to_chat(user, jointext(render_list, "\n")) // we let the join apply newlines so we do need handholding
+ to_chat(user, examine_block(jointext(render_list, "\n"))) // we let the join apply newlines so we do need handholding //SKYRAT EDIT CHANGE
return TRUE
//slime scanner
@@ -820,7 +820,7 @@ GENE SCANNER
if(T.effectmod)
to_render += "\n[span_notice("Core mutation in progress: [T.effectmod]")]\
\n[span_notice("Progress in core mutation: [T.applied] / [SLIME_EXTRACT_CROSSING_REQUIRED]")]"
- to_chat(user, to_render + "\n========================")
+ to_chat(user, examine_block(to_render + "\n========================")) //SKYRAT EDIT CHANGE
/obj/item/sequence_scanner//SKYRAT EDIT - ICON OVERRIDEN BY AESTHETICS - SEE MODUL
name = "genetic sequence scanner"
diff --git a/code/game/objects/items/inspector.dm b/code/game/objects/items/inspector.dm
index a68e8203c59..333beaea6e9 100644
--- a/code/game/objects/items/inspector.dm
+++ b/code/game/objects/items/inspector.dm
@@ -37,7 +37,7 @@
// Clean up the cell on destroy
/obj/item/clothing/suit/space/Destroy()
- if(cell)
+ if(isatom(cell))
QDEL_NULL(cell)
return ..()
diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm
index 535aa80f4d9..96432aa750d 100644
--- a/code/game/objects/structures/mirror.dm
+++ b/code/game/objects/structures/mirror.dm
@@ -149,7 +149,7 @@
..()
/obj/structure/mirror/magic/lesser/New()
- choosable_races = GLOB.roundstart_races.Copy()
+ choosable_races = get_selectable_species().Copy()
..()
/obj/structure/mirror/magic/badmin/New()
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 3be7a7bd4f7..8fa6dbbaa47 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -12,8 +12,6 @@ GLOBAL_PROTECT(admin_verbs_default)
/client/proc/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_hear_radio, /*allows admins to hide all radio output*/
- /client/proc/toggle_split_admin_tabs,
/client/proc/reload_admins,
/client/proc/reestablish_db_connection, /*reattempt a connection to the database*/
/client/proc/cmd_admin_pm_context, /*right-click adminPM interface*/
@@ -77,13 +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/toggle_admin_looc_global, //SKYRAT EDIT ADDITION - LOOC
- /client/proc/colorasay,
- /client/proc/resetasaycolor,
- /client/proc/toggleadminhelpsound,
/client/proc/respawn_character,
/client/proc/admin_open_event_spawners_menu, //SKYRAT EDIT ADDITION - EVENTS
/datum/admins/proc/toggleaooc, //SKYRAT EDIT ADDITION - ADMIN
diff --git a/code/modules/admin/greyscale_modify_menu.dm b/code/modules/admin/greyscale_modify_menu.dm
index aae202b4a32..7efd3f0d044 100644
--- a/code/modules/admin/greyscale_modify_menu.dm
+++ b/code/modules/admin/greyscale_modify_menu.dm
@@ -280,15 +280,15 @@ This is highly likely to cause massive amounts of lag as every object in the gam
// SKYRAT EDIT BEGIN - Bringing back the GAGS coloring menu
steps += list(
list(
- "layer"=icon2html(layer, user, dir=sprite_dir, sourceonly=TRUE, override_skyrat = TRUE),
- "result"=icon2html(result, user, dir=sprite_dir, sourceonly=TRUE, override_skyrat = TRUE),
+ "layer"=icon2html(layer, user, dir=sprite_dir, sourceonly=TRUE),
+ "result"=icon2html(result, user, dir=sprite_dir, sourceonly=TRUE),
"config_name"=step["config_name"]
)
)
// SKYRAT EDIT END
sprite_data["time_spent"] = TICK_DELTA_TO_MS(time_spent)
- sprite_data["finished"] = icon2html(finished, user, dir=sprite_dir, sourceonly=TRUE, override_skyrat = TRUE)
+ sprite_data["finished"] = icon2html(finished, user, dir=sprite_dir, sourceonly=TRUE)
refreshing = FALSE
/datum/greyscale_modify_menu/proc/Unlock()
diff --git a/code/modules/admin/poll_management.dm b/code/modules/admin/poll_management.dm
index 7b6c4bddd6d..86f075d3113 100644
--- a/code/modules/admin/poll_management.dm
+++ b/code/modules/admin/poll_management.dm
@@ -229,7 +229,7 @@
output += " "
var/datum/browser/panel = new(usr, "pmpanel", "Poll Management Panel", 780, 640)
panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css')
- if(usr.client.prefs.tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements 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
+ if(usr.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy)) //some browsers (IE8) have trouble with unsupported css3 elements 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("admin_panelscss3", 'html/admin/admin_panels_css3.css')
panel.set_content(jointext(output, ""))
panel.open()
@@ -527,7 +527,7 @@
panel_height = 320
var/datum/browser/panel = new(usr, "popanel", "Poll Option Panel", 370, panel_height)
panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css')
- if(usr.client.prefs.tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements 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
+ if(usr.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy)) //some browsers (IE8) have trouble with unsupported css3 elements 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("admin_panelscss3", 'html/admin/admin_panels_css3.css')
panel.set_content(jointext(output, ""))
panel.open()
diff --git a/code/modules/admin/smites/imaginary_friend_special.dm b/code/modules/admin/smites/imaginary_friend_special.dm
index 60ab1254417..5490a2ad108 100644
--- a/code/modules/admin/smites/imaginary_friend_special.dm
+++ b/code/modules/admin/smites/imaginary_friend_special.dm
@@ -28,7 +28,7 @@
return FALSE
if(friend_candidate_client.prefs)
- random_appearance = tgui_alert(user, "Do you want the imaginary friend to look like and be named after [friend_candidate_client]'s current preferences ([friend_candidate_client.prefs.real_name])?", "Imaginary Friend Appearance?", list("Look-a-like", "Random")) == "Random"
+ random_appearance = tgui_alert(user, "Do you want the imaginary friend to look like and be named after [friend_candidate_client]'s current preferences ([friend_candidate_client.prefs.read_preference(/datum/preference/name/real_name)])?", "Imaginary Friend Appearance?", list("Look-a-like", "Random")) == "Random"
else
random_appearance = TRUE
diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm
index 1a06672a857..2d15b1bda2a 100644
--- a/code/modules/admin/sql_ban_system.dm
+++ b/code/modules/admin/sql_ban_system.dm
@@ -139,7 +139,7 @@
var/datum/browser/panel = new(usr, "banpanel", "Banning Panel", 910, panel_height)
panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css')
panel.add_stylesheet("banpanelcss", 'html/admin/banpanel.css')
- var/tgui_fancy = usr.client.prefs.tgui_fancy
+ 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("admin_panelscss3", 'html/admin/admin_panels_css3.css')
panel.add_script("banpaneljs", 'html/admin/banpanel.js')
@@ -348,6 +348,9 @@
ROLE_REV,
ROLE_REVENANT,
ROLE_REV_HEAD,
+ ROLE_SENTIENT_DISEASE,
+ ROLE_SPIDER,
+ ROLE_SWARMER,
ROLE_SYNDICATE,
ROLE_TRAITOR,
ROLE_WIZARD,
diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm
index 1b136d4ef68..c5d48d113b9 100644
--- a/code/modules/admin/verbs/admingame.dm
+++ b/code/modules/admin/verbs/admingame.dm
@@ -386,7 +386,7 @@ Traitors and the like can also be revived with the previous role mostly intact.
if(view_size.getView() == view_size.default)
view_size.setTo(input("Select view range:", "FUCK YE", 7) in list(1,2,3,4,5,6,7,8,9,10,11,12,13,14,37) - 7)
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
diff --git a/code/modules/admin/verbs/adminsay.dm b/code/modules/admin/verbs/adminsay.dm
index b9c7da1f8a0..d92f3f105ae 100644
--- a/code/modules/admin/verbs/adminsay.dm
+++ b/code/modules/admin/verbs/adminsay.dm
@@ -11,7 +11,8 @@
mob.log_talk(msg, LOG_ASAY)
msg = keywords_lookup(msg)
- var/custom_asay_color = (CONFIG_GET(flag/allow_admin_asaycolor) && prefs.asaycolor) ? "" : ""
+ var/asay_color = prefs.read_preference(/datum/preference/color/asay_color)
+ var/custom_asay_color = (CONFIG_GET(flag/allow_admin_asaycolor) && asay_color) ? "" : ""
msg = "[span_adminsay("[span_prefix("ADMIN:")] [key_name(usr, 1)] [ADMIN_FLW(mob)]: [custom_asay_color][msg]")] [custom_asay_color ? " ":null]"
to_chat(GLOB.admins,
type = MESSAGE_TYPE_ADMINCHAT,
diff --git a/code/modules/admin/verbs/anonymousnames.dm b/code/modules/admin/verbs/anonymousnames.dm
index 434f71a0569..c9a87e1399d 100644
--- a/code/modules/admin/verbs/anonymousnames.dm
+++ b/code/modules/admin/verbs/anonymousnames.dm
@@ -113,9 +113,8 @@ GLOBAL_DATUM(current_anonymous_theme, /datum/anonymous_theme)
continue
var/old_name = player.real_name //before restoration
if(issilicon(player))
- player.apply_pref_name("[isAI(player) ? "ai" : "cyborg"]", player.client)
+ player.apply_pref_name("[isAI(player) ? /datum/preference/name/ai : /datum/preference/name/cyborg]", player.client)
else
- player.client.prefs.sanitize_chosen_prefs() // Just in case they changed unlawfully.
player.client.prefs.apply_prefs_to(player) // This is not sound logic, as the prefs may have changed since then.
player.fully_replace_character_name(old_name, player.real_name) //this changes IDs and PDAs and whatnot
@@ -131,7 +130,9 @@ GLOBAL_DATUM(current_anonymous_theme, /datum/anonymous_theme)
* * target - mob for preferences and gender
*/
/datum/anonymous_theme/proc/anonymous_name(mob/target)
- return target.client.prefs.pref_species.random_name(target.gender,1)
+ var/species_type = target.client.prefs.read_preference(/datum/preference/choiced/species)
+ var/datum/species/species = new species_type
+ return species.random_name(target.gender,1)
/**
* anonymous_ai_name: generates a random name, based off of whatever the round's anonymousnames is set to (but for sillycones).
diff --git a/code/modules/admin/verbs/ert.dm b/code/modules/admin/verbs/ert.dm
index 3b805834212..01b45ae0bb1 100644
--- a/code/modules/admin/verbs/ert.dm
+++ b/code/modules/admin/verbs/ert.dm
@@ -121,7 +121,7 @@
if(ertemplate.spawn_admin)
if(isobserver(usr))
var/mob/living/carbon/human/admin_officer = new (spawnpoints[1])
- var/chosen_outfit = usr.client?.prefs?.brief_outfit
+ var/chosen_outfit = usr.client?.prefs?.read_preference(/datum/preference/choiced/brief_outfit)
usr.client.prefs.safe_transfer_prefs_to(admin_officer, is_antag = TRUE)
admin_officer.equipOutfit(chosen_outfit)
admin_officer.key = usr.key
diff --git a/code/modules/admin/verbs/highlander_datum.dm b/code/modules/admin/verbs/highlander_datum.dm
index c4c5308a2d2..92366fc6d9d 100644
--- a/code/modules/admin/verbs/highlander_datum.dm
+++ b/code/modules/admin/verbs/highlander_datum.dm
@@ -56,7 +56,7 @@ GLOBAL_DATUM(highlander_controller, /datum/highlander_controller)
* * setup_list: list of all the datum setups (fancy list of roles) that would work for the game
* * ready_players: list of filtered, sane players (so not playing or disconnected) for the game to put into roles
*/
-/datum/highlander_controller/proc/new_highlander(mob/living/carbon/human/new_crewmember, rank)
+/datum/highlander_controller/proc/new_highlander(datum/source, mob/living/new_crewmember, rank)
SIGNAL_HANDLER
to_chat(new_crewmember, span_userdanger("THERE CAN BE ONLY ONE!!! "))
@@ -90,8 +90,11 @@ GLOBAL_DATUM(highlander_controller, /datum/highlander_controller)
log_admin("[key_name(usr)] used delayed THERE CAN BE ONLY ONE.")
addtimer(CALLBACK(src, .proc/only_one, TRUE), 42 SECONDS)
-/mob/living/carbon/human/proc/make_scottish()
+/mob/living/proc/make_scottish()
+ return
+
+/mob/living/carbon/human/make_scottish()
mind.add_antag_datum(/datum/antagonist/highlander)
-/mob/living/silicon/robot/proc/make_scottish()
+/mob/living/silicon/robot/make_scottish()
mind.add_antag_datum(/datum/antagonist/highlander/robot)
diff --git a/code/modules/admin/verbs/secrets.dm b/code/modules/admin/verbs/secrets.dm
index 3a5da3b062a..cfd8559d99a 100644
--- a/code/modules/admin/verbs/secrets.dm
+++ b/code/modules/admin/verbs/secrets.dm
@@ -1,3 +1,5 @@
+GLOBAL_DATUM(everyone_a_traitor, /datum/everyone_is_a_traitor_controller)
+
/client/proc/secrets() //Creates a verb for admins to open up the ui
set name = "Secrets"
set desc = "Abuse harder than you ever have before with this handy dandy semi-misc stuff menu"
@@ -448,24 +450,16 @@
if(!SSticker.HasRoundStarted())
tgui_alert(usr,"The game hasn't started yet!")
return
+ if(GLOB.everyone_a_traitor)
+ tgui_alert(usr, "The everyone is a traitor secret has already been triggered")
+ return
var/objective = stripped_input(holder, "Enter an objective")
if(!objective)
return
+ GLOB.everyone_a_traitor = new /datum/everyone_is_a_traitor_controller(objective)
SSblackbox.record_feedback("nested tally", "admin_secrets_fun_used", 1, list("Traitor All", "[objective]"))
for(var/mob/living/player in GLOB.player_list)
- if(!(ishuman(player)||istype(player, /mob/living/silicon/)))
- continue
- if(player.stat == DEAD || !player.mind || ispAI(player))
- continue
- if(is_special_character(player))
- continue
- var/datum/antagonist/traitor/traitor_datum = new()
- traitor_datum.give_objectives = FALSE
- var/datum/objective/new_objective = new
- new_objective.owner = player
- new_objective.explanation_text = objective
- traitor_datum.objectives += new_objective
- player.mind.add_antag_datum(traitor_datum)
+ GLOB.everyone_a_traitor.make_traitor(null, player)
message_admins(span_adminnotice("[key_name_admin(holder)] used everyone is a traitor secret. Objective is [objective]"))
log_admin("[key_name(holder)] used everyone is a traitor secret. Objective is [objective]")
if("massbraindamage")
@@ -612,3 +606,31 @@
var/turf/T = get_step(loc, SOUTHWEST)
flick_overlay_static(portal_appearance, T, 15)
playsound(T, 'sound/magic/lightningbolt.ogg', rand(80, 100), TRUE)
+
+///Makes sure latejoining crewmembers also become traitors.
+/datum/everyone_is_a_traitor_controller
+ var/objective = ""
+
+/datum/everyone_is_a_traitor_controller/New(objective)
+ src.objective = objective
+ RegisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED, .proc/make_traitor)
+
+/datum/everyone_is_a_traitor_controller/Destroy()
+ UnregisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED)
+ return ..()
+
+/datum/everyone_is_a_traitor_controller/proc/make_traitor(datum/source, mob/living/player)
+ SIGNAL_HANDLER
+ if(player.stat == DEAD || !player.mind)
+ return
+ if(!(ishuman(player) || issilicon(player)) || ispAI(player))
+ return
+ if(is_special_character(player))
+ return
+ var/datum/antagonist/traitor/traitor_datum = new()
+ traitor_datum.give_objectives = FALSE
+ var/datum/objective/new_objective = new
+ new_objective.owner = player
+ new_objective.explanation_text = objective
+ traitor_datum.objectives += new_objective
+ player.mind.add_antag_datum(traitor_datum)
diff --git a/code/modules/admin/view_variables/admin_delete.dm b/code/modules/admin/view_variables/admin_delete.dm
index 9f5f0151a67..60cf222bd11 100644
--- a/code/modules/admin/view_variables/admin_delete.dm
+++ b/code/modules/admin/view_variables/admin_delete.dm
@@ -24,7 +24,7 @@
if(!QDELETED(D))
vv_update_display(D, "deleted", "")
// Skyrat edit addition start -- optional bluespace sparks on delete
- if(T && prefs.skyrat_toggles & ADMINDEL_ZAP_PREF)
+ if(T && prefs.read_preference(/datum/preference/toggle/admin/delete_sparks))
playsound(T, 'sound/magic/Repulse.ogg', 100, 1)
var/datum/effect_system/spark_spread/quantum/sparks = new
sparks.set_up(10, 1, T)
diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm
index 0c381a93123..f11cf0c0a71 100644
--- a/code/modules/antagonists/_common/antag_datum.dm
+++ b/code/modules/antagonists/_common/antag_datum.dm
@@ -48,6 +48,8 @@ GLOBAL_LIST_EMPTY(antagonists)
var/show_name_in_check_antagonists = FALSE
/// Should this antagonist be shown as antag to ghosts? Shouldn't be used for stealthy antagonists like traitors
var/show_to_ghosts = FALSE
+ /// The typepath for the outfit to show in the preview for the preferences menu.
+ var/preview_outfit
//ANTAG UI
@@ -62,9 +64,7 @@ GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist/Destroy()
GLOB.antagonists -= src
- if(!owner)
- stack_trace("Destroy()ing antagonist datum when it has no owner.")
- else
+ if(owner)
LAZYREMOVE(owner.antag_datums, src)
owner = null
return ..()
@@ -341,6 +341,42 @@ GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist/proc/get_admin_commands()
. = list()
+/// 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)
+ COMPILE_OVERLAYS(dummy)
+ 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))
+
/datum/antagonist/Topic(href,href_list)
if(!check_rights(R_ADMIN))
return
diff --git a/code/modules/antagonists/abductor/abductor.dm b/code/modules/antagonists/abductor/abductor.dm
index 2f792e77f40..994db584e81 100644
--- a/code/modules/antagonists/abductor/abductor.dm
+++ b/code/modules/antagonists/abductor/abductor.dm
@@ -19,6 +19,26 @@ GLOBAL_LIST_INIT(possible_abductor_names, list("Alpha","Beta","Gamma","Delta","E
/// Type path for the associated job datum.
var/role_job = /datum/job/abductor_agent
+/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 a8c54453f68..40977fff0d6 100644
--- a/code/modules/antagonists/blob/blob.dm
+++ b/code/modules/antagonists/blob/blob.dm
@@ -38,6 +38,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
@@ -107,3 +117,22 @@
var/mob/camera/blob/blob_cam = owner.current
if(istype(blob_cam))
. += "(Progress: [length(blob_cam.blobs_legit)]/[blob_cam.blobwincount])"
+
+/// A subtype of blob meant to represent the infective version.
+/datum/antagonist/blob/infection
+ job_rank = ROLE_BLOB_INFECTION
+
+/datum/antagonist/blob/infection/get_preview_icon()
+ var/icon/blob_icon = ..()
+
+ var/datum/blobstrain/reagent/reactive_spines/reactive_spines = /datum/blobstrain/reagent/reactive_spines
+ var/icon/blob_head = icon('icons/mob/blob.dmi', "blob_head")
+ blob_head.Blend(initial(reactive_spines.complementary_color), ICON_MULTIPLY)
+
+ var/icon/human_icon = render_preview_outfit(/datum/outfit/job/miner)
+ human_icon.Blend(blob_head, ICON_OVERLAY)
+ human_icon.ChangeOpacity(0.7)
+
+ blob_icon.Blend(finish_preview_icon(human_icon), ICON_OVERLAY)
+
+ return blob_icon
diff --git a/code/modules/antagonists/brother/brother.dm b/code/modules/antagonists/brother/brother.dm
index 8d30d13e435..bbadfa70b90 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 3772ba319d5..d743cb5ed68 100644
--- a/code/modules/antagonists/changeling/changeling.dm
+++ b/code/modules/antagonists/changeling/changeling.dm
@@ -701,6 +701,20 @@
return parts.Join(" ")
+/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/antagonist/changeling/ui_data(mob/user)
var/list/data = list()
var/list/memories = list()
@@ -744,3 +758,10 @@
to_chat(owner, span_boldannounce("You are a fresh changeling birthed from a headslug! You aren't as strong as a normal changeling, as you are newly born."))
if(policy)
to_chat(owner, policy)
+
+/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/creep/creep.dm b/code/modules/antagonists/creep/creep.dm
index 951345472f2..672724e31a3 100644
--- a/code/modules/antagonists/creep/creep.dm
+++ b/code/modules/antagonists/creep/creep.dm
@@ -9,6 +9,7 @@
roundend_category = "obsessed"
silent = TRUE //not actually silent, because greet will be called by the trauma anyway.
suicide_cry = "FOR MY LOVE!!"
+ preview_outfit = /datum/outfit/obsessed
var/datum/brain_trauma/special/obsessed/trauma
/datum/antagonist/obsessed/admin_add(datum/mind/new_owner,mob/admin)
@@ -44,6 +45,40 @@
var/mob/living/M = mob_override || owner.current
remove_antag_hud(antag_hud_type, M)
+/datum/antagonist/obsessed/get_preview_icon()
+ var/mob/living/carbon/human/dummy/consistent/victim_dummy = new
+ victim_dummy.hair_color = "b96" // Brown
+ victim_dummy.hairstyle = "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/misc/overalls
+ 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/proc/forge_objectives(datum/mind/obsessionmind)
var/list/objectives_left = list("spendtime", "polaroid", "hug")
var/datum/objective/assassinate/obsessed/kill = new
diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm
index 5ceb0fd6117..0b57b25e3f1 100644
--- a/code/modules/antagonists/cult/cult.dm
+++ b/code/modules/antagonists/cult/cult.dm
@@ -9,6 +9,7 @@
antagpanel_category = "Cult"
antag_moodlet = /datum/mood_event/cult
suicide_cry = "FOR NAR'SIE!!"
+ preview_outfit = /datum/outfit/cultist
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
@@ -68,6 +69,24 @@
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.inhand_icon_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/proc/equip_cultist(metal=TRUE)
var/mob/living/carbon/H = owner.current
@@ -434,3 +453,19 @@
if(HAS_TRAIT(M, TRAIT_MINDSHIELD) || issilicon(M) || isbot(M) || isdrone(M) || !M.client)
return FALSE //can't convert machines, shielded, or braindead
return TRUE
+
+/datum/outfit/cultist
+ name = "Cultist (Preview only)"
+
+ uniform = /obj/item/clothing/under/color/black
+ suit = /obj/item/clothing/suit/hooded/cultrobes/alt
+ 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
+ hooded.MakeHood() // This is usually created on Initialize, but we run before atoms
+ hooded.ToggleHood()
diff --git a/code/modules/antagonists/disease/disease_datum.dm b/code/modules/antagonists/disease/disease_datum.dm
index 145ccd48610..b7cc9a79348 100644
--- a/code/modules/antagonists/disease/disease_datum.dm
+++ b/code/modules/antagonists/disease/disease_datum.dm
@@ -73,6 +73,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."
diff --git a/code/modules/antagonists/disease/disease_mob.dm b/code/modules/antagonists/disease/disease_mob.dm
index 26b204deb6d..d696e1ceb2e 100644
--- a/code/modules/antagonists/disease/disease_mob.dm
+++ b/code/modules/antagonists/disease/disease_mob.dm
@@ -146,7 +146,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 1f1109205f1..4c8470d11fa 100644
--- a/code/modules/antagonists/eldritch_cult/eldritch_antag.dm
+++ b/code/modules/antagonists/eldritch_cult/eldritch_antag.dm
@@ -9,6 +9,7 @@
antag_hud_name = "heretic"
hijack_speed = 0.5
suicide_cry = "THE MANSUS SMILES UPON ME!!"
+ preview_outfit = /datum/outfit/heretic
var/give_equipment = TRUE
var/list/researched_knowledge = list()
var/total_sacrifices = 0
@@ -40,6 +41,27 @@
to_chat(owner.current, span_userdanger("Your mind begins to flare as the otherwordly knowledge escapes your grasp!"))
owner.announce_objectives()
+/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.inhand_icon_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))
@@ -261,3 +283,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/gang/gang.dm b/code/modules/antagonists/gang/gang.dm
index 800c2f8fcb0..58476132c3f 100644
--- a/code/modules/antagonists/gang/gang.dm
+++ b/code/modules/antagonists/gang/gang.dm
@@ -6,6 +6,7 @@
antagpanel_category = "Family"
show_in_antagpanel = FALSE // i don't *think* this base class is buggy but it's too worthless to test
suicide_cry = "FOR THE FAMILY!!"
+ preview_outfit = /datum/outfit/gangster
/// The overarching family that the owner of this datum is a part of. Family teams are generic and imprinted upon by the per-person antagonist datums.
var/datum/team/gang/my_gang
/// The name of the family corresponding to this family member datum.
@@ -251,6 +252,12 @@
return FALSE
return TRUE
+/datum/outfit/gangster
+ name = "Gangster (Preview only)"
+
+ uniform = /obj/item/clothing/under/suit/henchmen
+ back = /obj/item/storage/backpack/henchmen
+
/datum/antagonist/gang/purple
show_in_antagpanel = TRUE
name = "Ballas"
diff --git a/code/modules/antagonists/malf_ai/datum_malf_ai.dm b/code/modules/antagonists/malf_ai/datum_malf_ai.dm
index 905fe322c5b..71bb53d50ea 100644
--- a/code/modules/antagonists/malf_ai/datum_malf_ai.dm
+++ b/code/modules/antagonists/malf_ai/datum_malf_ai.dm
@@ -187,4 +187,14 @@
return result.Join(" ")
+/datum/antagonist/malf_ai/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
+
#undef PROB_SPECIAL
diff --git a/code/modules/antagonists/monkey/monkey.dm b/code/modules/antagonists/monkey/monkey.dm
index 0f7e0e1d353..6132ab4d202 100644
--- a/code/modules/antagonists/monkey/monkey.dm
+++ b/code/modules/antagonists/monkey/monkey.dm
@@ -16,6 +16,16 @@
/datum/antagonist/monkey/can_be_owned(datum/mind/new_owner)
return ..() && (!monkey_only || ismonkey(new_owner.current))
+/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/get_team()
return monkey_team
diff --git a/code/modules/antagonists/nukeop/clownop.dm b/code/modules/antagonists/nukeop/clownop.dm
index 32beaa42f7d..df9530fb9e4 100644
--- a/code/modules/antagonists/nukeop/clownop.dm
+++ b/code/modules/antagonists/nukeop/clownop.dm
@@ -1,11 +1,15 @@
/datum/antagonist/nukeop/clownop
- name = "Clown Operative"
+ name = ROLE_CLOWN_OPERATIVE
roundend_category = "clown operatives"
antagpanel_category = "ClownOp"
nukeop_outfit = /datum/outfit/syndicate/clownop
suicide_cry = "HAPPY BIRTHDAY!!"
+ preview_outfit = /datum/outfit/clown_operative_elite
+ preview_outfit_behind = /datum/outfit/clown_operative
+ nuke_icon_state = "bananiumbomb_base"
+
/datum/antagonist/nukeop/clownop/admin_add(datum/mind/new_owner,mob/admin)
new_owner.set_assigned_role(SSjob.GetJobType(/datum/job/clown_operative))
new_owner.add_antag_datum(src)
@@ -60,3 +64,17 @@
var/obj/item/organ/liver/liver = L.getorganslot(ORGAN_SLOT_LIVER)
if(liver)
ADD_TRAIT(liver, TRAIT_COMEDY_METABOLISM, CLOWNOP_TRAIT)
+
+/datum/outfit/clown_operative
+ name = "Clown Operative (Preview only)"
+
+ suit = /obj/item/clothing/suit/space/hardsuit/syndi
+ gloves = /obj/item/clothing/gloves/color/black
+ mask = /obj/item/clothing/mask/gas/clown_hat
+
+/datum/outfit/clown_operative_elite
+ name = "Clown Operative (Elite, Preview only)"
+
+ suit = /obj/item/clothing/suit/space/hardsuit/syndi/elite
+ gloves = /obj/item/clothing/gloves/color/black
+ mask = /obj/item/clothing/mask/gas/clown_hat
diff --git a/code/modules/antagonists/nukeop/nukeop.dm b/code/modules/antagonists/nukeop/nukeop.dm
index 536aa47c2f7..991e876c670 100644
--- a/code/modules/antagonists/nukeop/nukeop.dm
+++ b/code/modules/antagonists/nukeop/nukeop.dm
@@ -14,6 +14,12 @@
var/send_to_spawnpoint = TRUE //Should the user be moved to default spawnpoint.
var/nukeop_outfit = /datum/outfit/syndicate
+ 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
+ /// In the preview icon, a nuclear fission explosive device, only appearing if there's an icon state for it.
+ var/nuke_icon_state = "nuclearbomb_base"
/datum/antagonist/nukeop/apply_innate_effects(mob/living/mob_override)
var/mob/living/M = mob_override || owner.current
@@ -87,7 +93,7 @@
/datum/antagonist/nukeop/proc/memorize_code()
if(nuke_team && nuke_team.tracked_nuke && nuke_team.memorized_code)
antag_memory += "[nuke_team.tracked_nuke] Code : [nuke_team.memorized_code] "
- owner.add_memory(MEMORY_NUKECODE, list(DETAIL_NUKE_CODE = nuke_team.memorized_code, DETAIL_PROTAGONIST = owner.current), story_value = STORY_VALUE_AMAZING, memory_flags = MEMORY_FLAG_NOLOCATION | MEMORY_FLAG_NOMOOD | MEMORY_FLAG_NOPERSISTENCE)
+ owner.add_memory(MEMORY_NUKECODE, list(DETAIL_NUKE_CODE = nuke_team.memorized_code, DETAIL_PROTAGONIST = owner.current), story_value = STORY_VALUE_AMAZING, memory_flags = MEMORY_FLAG_NOLOCATION | MEMORY_FLAG_NOMOOD | MEMORY_FLAG_NOPERSISTENCE)
to_chat(owner, "The nuclear authorization code is: [nuke_team.memorized_code] ")
else
to_chat(owner, "Unfortunately the syndicate was unable to provide you with nuclear authorization code.")
@@ -149,6 +155,45 @@
else
to_chat(admin, span_danger("No valid nuke found!"))
+/datum/antagonist/nukeop/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_UNDERLAY, -world.icon_size / 4, 0)
+ final_icon.Blend(teammate, ICON_UNDERLAY, world.icon_size / 4, 0)
+
+ if (!isnull(nuke_icon_state))
+ var/icon/nuke = icon('icons/obj/machines/nuke.dmi', nuke_icon_state)
+ nuke.Shift(SOUTH, 6)
+ final_icon.Blend(nuke, ICON_OVERLAY)
+
+ return finish_preview_icon(final_icon)
+
+/datum/outfit/nuclear_operative
+ name = "Nuclear Operative (Preview only)"
+
+ 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)"
+
+ suit = /obj/item/clothing/suit/space/hardsuit/syndi/elite
+ head = /obj/item/clothing/head/helmet/space/hardsuit/syndi/elite
+ l_hand = /obj/item/modular_computer/tablet/nukeops
+ 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
@@ -231,6 +276,9 @@
always_new_team = TRUE
send_to_spawnpoint = FALSE //Handled by event
nukeop_outfit = /datum/outfit/syndicate/full
+ preview_outfit = /datum/outfit/nuclear_operative
+ preview_outfit_behind = null
+ nuke_icon_state = null
/datum/antagonist/nukeop/lone/assign_nuke()
if(nuke_team && !nuke_team.tracked_nuke)
diff --git a/code/modules/antagonists/revenant/revenant_antag.dm b/code/modules/antagonists/revenant/revenant_antag.dm
index d6d5bb9da89..c00a4dcee2e 100644
--- a/code/modules/antagonists/revenant/revenant_antag.dm
+++ b/code/modules/antagonists/revenant/revenant_antag.dm
@@ -18,3 +18,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 a82f1a24162..722178363c6 100644
--- a/code/modules/antagonists/revolution/revolution.dm
+++ b/code/modules/antagonists/revolution/revolution.dm
@@ -163,6 +163,9 @@
/datum/antagonist/rev/head
name = "Head Revolutionary"
antag_hud_name = "rev_head"
+
+ preview_outfit = /datum/outfit/revolutionary
+
var/remove_clumsy = FALSE
var/give_flash = FALSE
var/give_hud = TRUE
@@ -181,6 +184,37 @@
/datum/antagonist/rev/head/antag_listing_name()
return ..() + "(Leader)"
+/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.hairstyle = 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/proc/can_be_converted(mob/living/candidate)
if(!candidate.mind)
return FALSE
@@ -559,5 +593,14 @@
heads_report += ""
return common_part + heads_report
+/datum/outfit/revolutionary
+ name = "Revolutionary (Preview only)"
+
+ uniform = /obj/item/clothing/under/costume/soviet
+ head = /obj/item/clothing/head/ushanka
+ gloves = /obj/item/clothing/gloves/color/black
+ l_hand = /obj/item/spear
+ r_hand = /obj/item/assembly/flash
+
#undef DECONVERTER_STATION_WIN
#undef DECONVERTER_REVS_WIN
diff --git a/code/modules/antagonists/space_dragon/space_dragon.dm b/code/modules/antagonists/space_dragon/space_dragon.dm
index cc8322c0a9d..62d3c808c5e 100644
--- a/code/modules/antagonists/space_dragon/space_dragon.dm
+++ b/code/modules/antagonists/space_dragon/space_dragon.dm
@@ -26,6 +26,17 @@
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)
+
+ 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."
diff --git a/code/modules/antagonists/space_ninja/space_ninja.dm b/code/modules/antagonists/space_ninja/space_ninja.dm
index 22eaad4750d..f4c93a27bb5 100644
--- a/code/modules/antagonists/space_ninja/space_ninja.dm
+++ b/code/modules/antagonists/space_ninja/space_ninja.dm
@@ -9,6 +9,7 @@
show_to_ghosts = TRUE
antag_moodlet = /datum/mood_event/focused
suicide_cry = "FOR THE SPIDER CLAN!!"
+ preview_outfit = /datum/outfit/ninja
///Whether or not this ninja will obtain objectives
var/give_objectives = TRUE
///Whether or not this ninja receives the standard equipment
diff --git a/code/modules/antagonists/swarmer/swarmer.dm b/code/modules/antagonists/swarmer/swarmer.dm
index 7e1e8c8da79..dcaa42b0929 100644
--- a/code/modules/antagonists/swarmer/swarmer.dm
+++ b/code/modules/antagonists/swarmer/swarmer.dm
@@ -10,7 +10,7 @@
/datum/antagonist/swarmer
name = "Swarmer"
- job_rank = ROLE_ALIEN
+ job_rank = ROLE_SWARMER
show_to_ghosts = TRUE
show_in_antagpanel = FALSE
prevent_roundtype_conversion = FALSE
@@ -32,6 +32,11 @@
/datum/antagonist/swarmer/get_team()
return swarmer_team
+/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)
+
//SWARMER
/mob/living/simple_animal/hostile/swarmer/mind_initialize()
..()
diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm
index 6e4b8df6205..7f6d661388b 100644
--- a/code/modules/antagonists/traitor/datum_traitor.dm
+++ b/code/modules/antagonists/traitor/datum_traitor.dm
@@ -26,6 +26,7 @@
hijack_speed = 0.5 //10 seconds per hijack stage by default
ui_name = "AntagInfoTraitor"
suicide_cry = "FOR THE SYNDICATE!!"
+ preview_outfit = /datum/outfit/traitor
var/give_objectives = TRUE
var/should_give_codewords = TRUE
var/should_equip = TRUE
@@ -350,6 +351,23 @@
return message
+/datum/outfit/traitor
+ name = "Traitor (Preview only)"
+
+ uniform = /obj/item/clothing/under/color/grey
+ suit = /obj/item/clothing/suit/hooded/ablative
+ gloves = /obj/item/clothing/gloves/color/yellow
+ mask = /obj/item/clothing/mask/gas
+ l_hand = /obj/item/melee/energy/sword
+ r_hand = /obj/item/gun/energy/kinetic_accelerator/crossbow
+
+/datum/outfit/traitor/post_equip(mob/living/carbon/human/H, visualsOnly)
+ var/obj/item/melee/energy/sword/sword = locate() in H.held_items
+ sword.icon_state = "e_sword_on_red"
+ sword.worn_icon_state = "e_sword_on_red"
+
+ H.update_inv_hands()
+
#undef HIJACK_PROB
#undef HIJACK_MIN_PLAYERS
#undef MARTYR_PROB
diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm
index 8b727ee84b8..a7cdbbf6f6f 100644
--- a/code/modules/antagonists/wizard/wizard.dm
+++ b/code/modules/antagonists/wizard/wizard.dm
@@ -9,6 +9,7 @@
hijack_speed = 0.5
ui_name = "AntagInfoWizard"
suicide_cry = "FOR THE FEDERATION!!"
+ preview_outfit = /datum/outfit/wizard
var/give_objectives = TRUE
var/strip = TRUE //strip before equipping
var/allow_rename = TRUE
diff --git a/code/modules/antagonists/xeno/xeno.dm b/code/modules/antagonists/xeno/xeno.dm
index b04f489dc31..eef1b989fac 100644
--- a/code/modules/antagonists/xeno/xeno.dm
+++ b/code/modules/antagonists/xeno/xeno.dm
@@ -32,6 +32,9 @@
/datum/antagonist/xeno/get_team()
return xeno_team
+/datum/antagonist/xeno/get_preview_icon()
+ return finish_preview_icon(icon('icons/mob/alien.dmi', "alienh"))
+
//XENO
/mob/living/carbon/alien/mind_initialize()
..()
diff --git a/code/modules/art/paintings.dm b/code/modules/art/paintings.dm
index 289e9131b4e..23b2ca86a59 100644
--- a/code/modules/art/paintings.dm
+++ b/code/modules/art/paintings.dm
@@ -256,7 +256,7 @@
/obj/structure/sign/painting/Initialize(mapload, dir, building)
. = ..()
- SSpersistence.painting_frames += src
+ SSpersistent_paintings.painting_frames += src
if(dir)
setDir(dir)
if(building)
@@ -265,7 +265,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(!current_canvas && istype(I, /obj/item/canvas))
@@ -336,12 +336,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
@@ -383,7 +383,7 @@
current_canvas.painting_name = "Untitled Artwork"
var/data = current_canvas.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)
@@ -395,7 +395,7 @@
if(result)
CRASH("Error saving persistent painting: [result]")
current += list(list("title" = current_canvas.painting_name , "md5" = md5, "ckey" = current_canvas.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
@@ -437,16 +437,16 @@
return
var/md5 = md5(lowertext(current_canvas.get_data_string()))
var/author = current_canvas.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/P in SSpersistence.painting_frames)
- if(P.current_canvas && md5(P.current_canvas.get_data_string()) == md5)
- QDEL_NULL(P.current_canvas)
- P.update_appearance()
+ for(var/obj/structure/sign/painting/painting as anything in SSpersistent_paintings.painting_frames)
+ if(painting.current_canvas && md5(painting.current_canvas.get_data_string()) == md5)
+ QDEL_NULL(painting.current_canvas)
+ painting.update_appearance()
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/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm
index d3692190e86..86d858189e2 100644
--- a/code/modules/asset_cache/asset_list.dm
+++ b/code/modules/asset_cache/asset_list.dm
@@ -13,6 +13,9 @@ GLOBAL_LIST_EMPTY(asset_datums)
var/_abstract = /datum/asset
var/cached_url_mappings
+ /// Whether or not this asset should be loaded in the "early assets" SS
+ var/early = FALSE
+
/datum/asset/New()
GLOB.asset_datums[type] = src
register()
@@ -364,3 +367,28 @@ 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]!")
diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm
index c72eabe3b16..bd6bf1ff086 100644
--- a/code/modules/asset_cache/asset_list_items.dm
+++ b/code/modules/asset_cache/asset_list_items.dm
@@ -476,10 +476,9 @@
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"]]"
diff --git a/code/modules/awaymissions/corpse.dm b/code/modules/awaymissions/corpse.dm
index 33247cfca0e..16a938e9902 100644
--- a/code/modules/awaymissions/corpse.dm
+++ b/code/modules/awaymissions/corpse.dm
@@ -241,7 +241,7 @@
var/initial_string = "Would you like to spawn as a randomly created character, or use the one currently selected in your preferences?"
var/action = tgui_alert(user, initial_string, "", list("Use Random Character", "Use Character From Preferences"))
if(action && action == "Use Character From Preferences")
- var/warning_string = "WARNING: This spawner will use your currently selected character in prefs ([user.client.prefs.real_name])\nMake sure that the character is not used as a station crew, or would have a good reason to be this role.(ie. intern in Space Hotel)\nUSING STATION CHARACTERS FOR SYNDICATE OR HOSTILE ROLES IS PROHIBITED WILL GET YOU BANNED!\nConsider making a character dedicated to the role.\nDo you wanna proceed?"
+ var/warning_string = "WARNING: This spawner will use your currently selected character in prefs ([user.client.prefs?.read_preference(/datum/preference/name/real_name)])\nMake sure that the character is not used as a station crew, or would have a good reason to be this role.(ie. intern in Space Hotel)\nUSING STATION CHARACTERS FOR SYNDICATE OR HOSTILE ROLES IS PROHIBITED WILL GET YOU BANNED!\nConsider making a character dedicated to the role.\nDo you wanna proceed?"
var/action2 = tgui_alert(user, warning_string, "", list("Yes", "No"))
if(action2 && action2 == "Yes")
is_pref_char = TRUE
@@ -257,7 +257,8 @@
chosen_alias = msg
if(is_pref_char)
- if(!any_station_species && user.client.prefs.pref_species.type != mob_species)
+ var/species_type = user.client.prefs.read_preference(/datum/preference/choiced/species)
+ if(!any_station_species && species_type != mob_species)
alert(user, "Sorry, This spawner is limited to those species: [mob_species]. Please switch your character.", "", "Ok")
return FALSE
@@ -281,8 +282,8 @@
H.dna.species.pre_equip_species_outfit(null, H)
H.regenerate_icons()
SSquirks.AssignQuirks(H, user.client, TRUE, TRUE, null, FALSE, H)
- user.client.prefs.equip_preference_loadout(H, FALSE, blacklist = list(ITEM_SLOT_EARS,ITEM_SLOT_BELT,ITEM_SLOT_ID,ITEM_SLOT_BACK,ITEM_SLOT_ICLOTHING,ITEM_SLOT_BACK,ITEM_SLOT_OCLOTHING,ITEM_SLOT_GLOVES,ITEM_SLOT_FEET,ITEM_SLOT_HEAD,ITEM_SLOT_MASK,ITEM_SLOT_NECK,ITEM_SLOT_EYES,ITEM_SLOT_SUITSTORE,ITEM_SLOT_LPOCKET,ITEM_SLOT_RPOCKET)) //There has to be a better way to do this, this is utter bloat.
- user.client.prefs.add_packed_items(H, null, FALSE)
+ for(var/datum/loadout_item/item as anything in loadout_list_to_datums(H?.client?.prefs?.loadout_list))
+ item.post_equip_item(H.client?.prefs, H)
else
if(!random || newname)
if(newname)
diff --git a/code/modules/buildmode/submodes/advanced.dm b/code/modules/buildmode/submodes/advanced.dm
index 253cb09920c..2209ecd33c7 100644
--- a/code/modules/buildmode/submodes/advanced.dm
+++ b/code/modules/buildmode/submodes/advanced.dm
@@ -63,7 +63,7 @@
// SKYRAT EDIT -- BS delete sparks. Original was just qdel(object)
var/turf/T = get_turf(object)
qdel(object)
- if(T && c.prefs.skyrat_toggles & ADMINDEL_ZAP_PREF)
+ if(T && c.prefs.read_preference(/datum/preference/toggle/admin/delete_sparks))
playsound(T, 'sound/magic/Repulse.ogg', 100, 1)
var/datum/effect_system/spark_spread/quantum/sparks = new
sparks.set_up(10, 1, T)
diff --git a/code/modules/buildmode/submodes/basic.dm b/code/modules/buildmode/submodes/basic.dm
index e002ce4fe4e..89666f5e351 100644
--- a/code/modules/buildmode/submodes/basic.dm
+++ b/code/modules/buildmode/submodes/basic.dm
@@ -41,7 +41,7 @@
// SKYRAT EDIT -- BS delete sparks. Original was just qdel(object)
var/turf/T = get_turf(object)
qdel(object)
- if(T && c.prefs.skyrat_toggles & ADMINDEL_ZAP_PREF)
+ if(T && c.prefs.read_preference(/datum/preference/toggle/admin/delete_sparks))
playsound(T, 'sound/magic/Repulse.ogg', 100, 1)
var/datum/effect_system/spark_spread/quantum/sparks = new
sparks.set_up(10, 1, T)
diff --git a/code/modules/buildmode/submodes/delete.dm b/code/modules/buildmode/submodes/delete.dm
index f241ad99479..9c87640bd33 100644
--- a/code/modules/buildmode/submodes/delete.dm
+++ b/code/modules/buildmode/submodes/delete.dm
@@ -18,7 +18,7 @@
// SKYRAT EDIT -- BS delete sparks. Original was just qdel(object)
var/turf/T = get_turf(object)
qdel(object)
- if(T && c.prefs.skyrat_toggles & ADMINDEL_ZAP_PREF)
+ if(T && c.prefs.read_preference(/datum/preference/toggle/admin/delete_sparks))
playsound(T, 'sound/magic/Repulse.ogg', 100, 1)
var/datum/effect_system/spark_spread/quantum/sparks = new
sparks.set_up(10, 1, T)
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index b32b1936975..40ab427126e 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -211,3 +211,6 @@
/// If the client is currently under the restrictions of the interview system
var/interviewee = FALSE
+
+ /// 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 d876eee8ec4..d5e6c3b5fe9 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -110,13 +110,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
hsrc = holder
if("usr")
hsrc = mob
- 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)
@@ -249,12 +242,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 < 0) ? RECOMMENDED_FPS : prefs.clientfps
if(fexists(roundend_report_file()))
add_verb(src, /client/proc/show_previous_roundend_report)
@@ -413,7 +406,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
adminGreet()
if (mob && reconnecting)
var/stealth_admin = mob.client?.holder?.fakekey
- var/announce_leave = mob.client?.prefs?.broadcast_login_logout
+ var/announce_leave = mob.client?.prefs?.read_preference(/datum/preference/toggle/broadcast_login_logout)
if (!stealth_admin)
deadchat_broadcast(" has reconnected.", "[mob][mob.get_realname_string()] ", follow_target = mob, turf_target = get_turf(mob), message_type = DEADCHAT_LOGIN_LOGOUT, admin_only=!announce_leave)
add_verbs_from_config()
@@ -472,7 +465,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if (!interviewee)
initialize_menus()
- 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()
@@ -490,7 +483,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
/client/Destroy()
if(mob)
var/stealth_admin = mob.client?.holder?.fakekey
- var/announce_join = mob.client?.prefs?.broadcast_login_logout
+ var/announce_join = mob.client?.prefs?.read_preference(/datum/preference/toggle/broadcast_login_logout)
if (!stealth_admin)
deadchat_broadcast(" has disconnected.", "[mob][mob.get_realname_string()] ", follow_target = mob, turf_target = get_turf(mob), message_type = DEADCHAT_LOGIN_LOGOUT, admin_only=!announce_join)
@@ -534,6 +527,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
QDEL_NULL(view_size)
QDEL_NULL(void)
QDEL_NULL(tooltips)
+ QDEL_NULL(open_loadout_ui) //SKYRAT EDIT ADDITION
seen_messages = null
Master.UpdateTickRate()
..() //Even though we're going to be hard deleted there are still some things that want to know the destroy is happening
@@ -918,7 +912,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.
@@ -1010,8 +1004,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,7 +1039,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
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()
@@ -1062,35 +1056,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/source)
- LAZYINITLIST(char_render_holders)
- if(!LAZYLEN(char_render_holders))
- for(var/plane_master_path as anything in subtypesof(/atom/movable/screen/plane_master))
- var/atom/movable/screen/plane_master/plane_master = new plane_master_path()
- char_render_holders["plane_master-[plane_master.plane]"] = plane_master
- plane_master.backdrop(mob)
- screen |= plane_master
- plane_master.screen_loc = "character_preview_map:0,CENTER"
-
- var/pos = 0
- for(var/dir in GLOB.cardinals)
- pos++
- var/atom/movable/screen/preview = char_render_holders["preview-[dir]"]
- if(!preview)
- preview = new
- char_render_holders["preview-[dir]"] = preview
- screen |= preview
- preview.appearance = source
- preview.dir = dir
- preview.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
-
///Redirect proc that makes it easier to call the unlock achievement proc. Achievement type is the typepath to the award, user is the mob getting the award, and value is an optional variable used for leaderboard value increments
/client/proc/give_award(achievement_type, mob/user, value = 1)
return player_details.achievements.unlock(achievement_type, user, value)
@@ -1158,11 +1123,6 @@ 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)
-
/client/proc/open_filter_editor(atom/in_atom)
if(holder)
holder.filteriffic = new /datum/filter_editor(in_atom)
@@ -1215,3 +1175,11 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
var/mob/dead/observer/observer = mob
observer.ManualFollow(target)
+
+/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 845e1b91977..7108545afc1 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -1,4 +1,3 @@
-/* SKYRAT EDIT REMOVAL - MOVED TO MODULAR
GLOBAL_LIST_EMPTY(preferences_datums)
/datum/preferences
@@ -6,7 +5,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
//doohickeys for savefiles
var/path
var/default_slot = 1 //Holder so it doesn't default to slot 1, rather the last one used
- var/max_save_slots = 3
+ var/max_save_slots = 30 //SKYRAT EDIT CHANGE
//non-preference stuff
var/muted = 0
@@ -15,92 +14,27 @@ GLOBAL_LIST_EMPTY(preferences_datums)
//game-preferences
var/lastchangelog = "" //Saved changlog filesize to detect if there was a change
- var/ooccolor = "#c43b23"
- var/asaycolor = "#ff4500" //This won't change the color for current admins, only incoming ones.
- /// If we spawn an ERT as an admin and choose to spawn as the briefing officer, we'll be given this outfit
- var/brief_outfit = /datum/outfit/centcom/commander
- 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
-
- ///Runechat preference. If true, certain messages will be displayed on the map, not ust on the chat area. Boolean.
- var/chat_on_map = TRUE
- ///Limit preference on the size of the message. Requires chat_on_map to have effect.
- var/max_chat_length = CHAT_MESSAGE_MAX_LENGTH
- ///Whether non-mob messages will be displayed, such as machine vendor announcements. Requires chat_on_map to have effect. Boolean.
- var/see_chat_non_mob = TRUE
- ///Whether emotes will be displayed on runechat. Requires chat_on_map to have effect. Boolean.
- var/see_rc_emotes = 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/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/uses_glasses_colour = 0
//character preferences
var/slot_randomized //keeps track of round-to-round randomization of the character slot, prevents overwriting
- var/real_name //our character's name
- var/gender = MALE //gender of character (well duh)
- var/age = 30 //age of character
- var/underwear = "Nude" //underwear type
- var/underwear_color = "000" //underwear color
- var/undershirt = "Nude" //undershirt type
- var/socks = "Nude" //socks type
- var/backpack = DBACKPACK //backpack type
- var/jumpsuit_style = PREF_SUIT //suit/skirt
- var/hairstyle = "Bald" //Hair type
- var/hair_color = "000" //Hair color
- var/facial_hairstyle = "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", "ethcolor" = "9c3030", "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", "moth_antennae" = "Plain", "moth_markings" = "None")
- var/list/randomise = list(
- RANDOM_UNDERWEAR = TRUE,
- RANDOM_UNDERWEAR_COLOR = TRUE,
- RANDOM_UNDERSHIRT = TRUE,
- RANDOM_SOCKS = TRUE,
- RANDOM_BACKPACK = TRUE,
- RANDOM_JUMPSUIT_STYLE = TRUE,
- RANDOM_HAIRSTYLE = TRUE,
- RANDOM_HAIR_COLOR = TRUE,
- RANDOM_FACIAL_HAIRSTYLE = TRUE,
- RANDOM_FACIAL_HAIR_COLOR = TRUE,
- RANDOM_SKIN_TONE = TRUE,
- RANDOM_EYE_COLOR = TRUE,
- )
- var/phobia = "spiders"
- var/list/custom_names = list()
- var/preferred_ai_core_display = "Blue"
- var/prefered_security_department = SEC_DEPT_NONE
+ var/list/randomise = list()
//Quirk list
var/list/all_quirks = list()
@@ -108,42 +42,14 @@ GLOBAL_LIST_EMPTY(preferences_datums)
//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 = -1
-
- var/parallax
-
- ///Do we show screentips, if so, how big?
- var/screentip_pref = TRUE
- ///Color of screentips at top of screen
- var/screentip_color = "#ffd391"
- ///Do we show item hover outlines?
- var/itemoutline_pref = TRUE
-
- var/ambientocclusion = TRUE
- ///Should we automatically fit the viewport?
- var/auto_fit_viewport = FALSE
- ///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? Distort means nearest neighbor
- var/scaling_method = SCALING_METHOD_DISTORT
- var/uplink_spawn_loc = UPLINK_PDA
- ///The playtime_reward_cloak variable can be set to TRUE from the prefs menu only once the user has gained over 5K playtime hours. If true, it allows the user to get a cool looking roundstart cloak.
- var/playtime_reward_cloak = FALSE
-
var/list/exp = list()
- var/list/menuoptions
var/action_buttons_screen_locs = list()
@@ -151,985 +57,453 @@ GLOBAL_LIST_EMPTY(preferences_datums)
var/hearted
///If we have a hearted commendations, we honor it every time the player loads preferences until this time has been passed
var/hearted_until
- /// Agendered spessmen can choose whether to have a male or female bodytype
- var/body_type
/// If we have persistent scars enabled
var/persistent_scars = TRUE
- ///If we want to broadcast deadchat connect/disconnect messages
- var/broadcast_login_logout = TRUE
///What outfit typepaths we've favorited in the SelectEquipment menu
var/list/favorite_outfits = list()
- ///If TRUE, we replace the flash effect from flashes with a solid black screen
- var/darkened_flash = FALSE
+
+ /// A preview of the current character
+ var/atom/movable/screen/character_preview_view/character_preview_view
+
+ /// A list of instantiated middleware
+ var/list/datum/preference_middleware/middleware = list()
+
+ /// The savefile relating to core preferences, PREFERENCE_PLAYER
+ var/savefile/game_savefile
+
+ /// The savefile relating to character preferences, PREFERENCE_CHARACTER
+ var/savefile/character_savefile
+
+ /// A list of keys that have been updated since the last save.
+ var/list/recently_updated_keys = list()
+
+ /// A cache of preference entries to values.
+ /// Used to avoid expensive READ_FILE every time a preference is retrieved.
+ var/value_cache = list()
+
+ /// If set to TRUE, will update character_profiles on the next ui_data tick.
+ var/tainted_character_profiles = FALSE
+
+/datum/preferences/Destroy(force, ...)
+ QDEL_NULL(character_preview_view)
+ QDEL_LIST(middleware)
+ value_cache = null
+ //SKYRAT EDIT ADDITION
+ if(pref_species)
+ QDEL_NULL(pref_species)
+ //SKYRAT EDIT END
+ 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()
+ unlock_content = !!C.IsByondMember()
if(unlock_content)
- max_save_slots = 8
+ max_save_slots = 40 //SKYRAT EDIT CHANGE
+
+ // 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
randomise_appearance_prefs() //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)
+
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/ui_interact(mob/user, datum/tgui/ui)
+ // 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)
-/datum/preferences/proc/ShowChoices(mob/user)
- if(!user || !user.client)
- return
- if(slot_randomized)
- load_character(default_slot) // Reloads the character slot. Prevents random features from overwriting the slot if saved.
- slot_randomized = FALSE
- update_preview_icon()
- var/list/dat = list("")
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "PreferencesMenu")
+ ui.set_autoupdate(FALSE)
+ ui.open()
- dat += "Character Settings "
- dat += "Game Preferences "
- dat += "OOC Preferences "
- dat += "Custom Keybindings "
+ // 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)
- if(!path)
- dat += "Please create an account to save your preferences
"
+/datum/preferences/ui_state(mob/user)
+ return GLOB.always_state
- dat += " "
+// 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 += " "
+/datum/preferences/ui_data(mob/user)
+ var/list/data = list()
- 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 += " "
+ 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)
- 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 += ""
+/datum/preferences/ui_static_data(mob/user)
+ var/list/data = list()
- dat += "Body "
- dat += "Random Body "
- dat += "Always Random Body: [(randomise[RANDOM_BODY]) ? "Yes" : "No"] "
- dat += "When Antagonist: [(randomise[RANDOM_BODY_ANTAG]) ? "Yes" : "No"] "
+ data["character_profiles"] = create_character_profiles()
- dat += ""
+ data["character_preview_view"] = character_preview_view.assigned_map
+ data["overflow_role"] = SSjob.GetJobType(SSjob.overflow_role).title
+ data["window"] = current_window
- dat += "Species: [pref_species.name] "
- dat += "Random Species "
- dat += "Always Random Species: [(randomise[RANDOM_SPECIES]) ? "Yes" : "No"] "
+ data["content_unlocked"] = unlock_content
- dat += "Underwear: [underwear] "
- dat += "[(randomise[RANDOM_UNDERWEAR]) ? "Lock" : "Unlock"] "
+ for (var/datum/preference_middleware/preference_middleware as anything in middleware)
+ data += preference_middleware.get_ui_static_data(user)
- dat += "Underwear Color: Change "
- dat += "[(randomise[RANDOM_UNDERWEAR_COLOR]) ? "Lock" : "Unlock"] "
+ return data
- dat += "Undershirt: [undershirt] "
- dat += "[(randomise[RANDOM_UNDERSHIRT]) ? "Lock" : "Unlock"] "
+/datum/preferences/ui_assets(mob/user)
+ var/list/assets = list(
+ get_asset_datum(/datum/asset/spritesheet/preferences),
+ get_asset_datum(/datum/asset/json/preferences),
+ )
+ for (var/datum/preference_middleware/preference_middleware as anything in middleware)
+ assets += preference_middleware.get_ui_assets()
- dat += "Socks: [socks] "
- dat += "[(randomise[RANDOM_SOCKS]) ? "Lock" : "Unlock"] "
+ return assets
-
- dat += "Jumpsuit Style: [jumpsuit_style] "
- dat += "[(randomise[RANDOM_JUMPSUIT_STYLE]) ? "Lock" : "Unlock"] "
-
- dat += "Backpack: [backpack] "
- dat += "[(randomise[RANDOM_BACKPACK]) ? "Lock" : "Unlock"] "
-
- 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 == UPLINK_IMPLANT ? UPLINK_IMPLANT_WITH_PRICE : uplink_spawn_loc] "
- if (user.client.get_exp_living(TRUE) >= PLAYTIME_VETERAN)
- dat += "Don The Ultimate Gamer Cloak?: [(playtime_reward_cloak) ? "Enabled" : "Disabled"] "
- var/use_skintones = pref_species.use_skintones
- if(use_skintones)
-
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Skin Tone "
-
- dat += "[skin_tone] "
- dat += "[(randomise[RANDOM_SKIN_TONE]) ? "Lock" : "Unlock"] "
- dat += " "
-
- var/mutant_colors
- if((MUTCOLORS in pref_species.species_traits) || (MUTCOLORS_PARTSONLY in pref_species.species_traits))
-
- if(!use_skintones)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Mutant Color "
-
- dat += " Change "
-
- mutant_colors = TRUE
-
- if(istype(pref_species, /datum/species/ethereal)) //not the best thing to do tbf but I dont know whats better.
-
- if(!use_skintones)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Ethereal Color "
-
- dat += " Change "
-
-
- if((EYECOLOR in pref_species.species_traits) && !(NOEYESPRITES in pref_species.species_traits))
-
- if(!use_skintones && !mutant_colors)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Eye Color "
- dat += " Change "
- dat += "[(randomise[RANDOM_EYE_COLOR]) ? "Lock" : "Unlock"] "
-
- dat += " "
- else if(use_skintones || mutant_colors)
- dat += ""
-
- if(HAIR in pref_species.species_traits)
-
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Hairstyle "
-
- dat += "[hairstyle] "
- dat += "< > "
- dat += "[(randomise[RANDOM_HAIRSTYLE]) ? "Lock" : "Unlock"] "
-
- dat += " Change "
- dat += "[(randomise[RANDOM_HAIR_COLOR]) ? "Lock" : "Unlock"] "
-
- dat += "Facial Hairstyle "
-
- dat += "[facial_hairstyle] "
- dat += "< > "
- dat += "[(randomise[RANDOM_FACIAL_HAIRSTYLE]) ? "Lock" : "Unlock"] "
-
- dat += " Change "
- dat += "[(randomise[RANDOM_FACIAL_HAIR_COLOR]) ? "Lock" : "Unlock"] "
- dat += " "
-
- //Mutant stuff
- var/mutant_category = 0
-
- if(pref_species.mutant_bodyparts["tail_lizard"])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Tail "
-
- dat += "[features["tail_lizard"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.external_organs[/obj/item/organ/external/snout])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Snout "
-
- dat += "[features["snout"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.external_organs[/obj/item/organ/external/horns])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Horns "
-
- dat += "[features["horns"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.external_organs[/obj/item/organ/external/frills])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Frills "
-
- dat += "[features["frills"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.mutant_bodyparts["spines"])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Spines "
-
- dat += "[features["spines"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.mutant_bodyparts["body_markings"])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Body Markings "
-
- dat += "[features["body_markings"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.mutant_bodyparts["legs"])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
-<<<<<<< HEAD
- dat += "Legs "
-=======
- client?.clear_map(assigned_map)
->>>>>>> a08f698230e (Harddeletes: Accident edition (#61562))
-
- dat += "[features["legs"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.external_organs[/obj/item/organ/external/wings/moth])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Moth wings "
-
- dat += "[features["moth_wings"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.external_organs[/obj/item/organ/external/antennae])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Moth antennae "
-
- dat += "[features["moth_antennae"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.mutant_bodyparts["moth_markings"])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Moth markings "
-
- dat += "[features["moth_markings"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.mutant_bodyparts["tail_human"])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Tail "
-
- dat += "[features["tail_human"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(pref_species.mutant_bodyparts["ears"])
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Ears "
-
- dat += "[features["ears"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- //Adds a thing to select which phobia because I can't be assed to put that in the quirks window
- if("Phobia" in all_quirks)
- dat += "Phobia "
-
- dat += "[phobia] "
-
- if(CONFIG_GET(flag/join_with_mutant_humans))
-
- if(pref_species.mutant_bodyparts["wings"] && GLOB.r_wings_list.len >1)
- if(!mutant_category)
- dat += APPEARANCE_CATEGORY_COLUMN
-
- dat += "Wings "
-
- dat += "[features["wings"]] "
-
- mutant_category++
- if(mutant_category >= MAX_MUTANT_ROWS)
- dat += ""
- mutant_category = 0
-
- if(mutant_category)
- dat += ""
- mutant_category = 0
- dat += "
"
-
-
- if (1) // Game Preferences
- dat += ""
- if(2) //OOC Preferences
- dat += ""
- if(3) // Custom 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 += "[kb.full_name] 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 += "[kb.full_name] [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, 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 = 15, widthPerColumn = 295, height = 620)
- if(!SSjob)
+/datum/preferences/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if (.)
return
- //limit - The amount of jobs allowed per column. Defaults to 17 to make it look nice.
- //widthPerColumn - Screen's width for every column.
- //height - Screen's height.
+ switch (action)
+ if ("change_slot")
+ // Save existing character
+ save_character()
- var/width = widthPerColumn
+ // SAFETY: `load_character` performs sanitization the slot number
+ if (!load_character(params["slot"]))
+ tainted_character_profiles = TRUE
+ randomise_appearance_prefs()
+ save_character()
- var/HTML = ""
- if(length(SSjob.joinable_occupations) <= 0)
- HTML += "The job SSticker is not yet finished creating jobs, please try again later"
- HTML += "Done " // Easier to press up here.
+ for (var/datum/preference_middleware/preference_middleware as anything in middleware)
+ preference_middleware.on_new_character(usr)
- 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
+ character_preview_view.update_body()
- //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_role = SSjob.GetJobType(SSjob.overflow_role)
+ return TRUE
+ if ("rotate")
+ character_preview_view.dir = turn(character_preview_view.dir, -90)
- for(var/datum/job/job as anything in SSjob.joinable_occupations)
+ return TRUE
+ if ("set_preference")
+ var/requested_preference_key = params["preference"]
+ var/value = params["value"]
- index += 1
- if(index >= limit)
- width += widthPerColumn
- if((index < limit) && (lastJob != null))
- // Fills the rest of the cells with the last job's selection color.
- for(var/i = 0, i < (limit - index), i += 1)
- HTML += "    "
- HTML += "
"
- index = 0
+ for (var/datum/preference_middleware/preference_middleware as anything in middleware)
+ if (preference_middleware.pre_set_preference(usr, requested_preference_key, value))
+ return TRUE
- HTML += ""
- var/rank = job.title
- lastJob = job
- if(is_banned_from(user.ckey, rank))
- HTML += "[rank] BANNED "
- continue
- var/required_playtime_remaining = job.required_playtime_remaining(user.client)
- if(required_playtime_remaining)
- HTML += "[rank] \[ [get_exp_format(required_playtime_remaining)] as [job.get_exp_req_type()] \] "
- continue
- if(!job.player_old_enough(user.client))
- var/available_in_days = job.available_in_days(user.client)
- HTML += "[rank] \[IN [(available_in_days)] DAYS\] "
- continue
- if((job_preferences[overflow_role.title] == JP_LOW) && (rank != overflow_role.title) && !is_banned_from(user.ckey, overflow_role.title))
- HTML += "[rank] "
- continue
- if(job.job_flags & JOB_BOLD_SELECT_TEXT)//Bold head jobs
- HTML += "[rank] "
+ var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key]
+ if (isnull(requested_preference))
+ return FALSE
+
+ // SAFETY: `update_preference` performs validation checks
+ if (!update_preference(requested_preference, value))
+ return FALSE
+
+ if (istype(requested_preference, /datum/preference/name))
+ tainted_character_profiles = TRUE
+
+ return TRUE
+ if ("set_color_preference")
+ var/requested_preference_key = params["preference"]
+
+ var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key]
+ if (isnull(requested_preference))
+ return FALSE
+
+ if (!istype(requested_preference, /datum/preference/color) \
+ && !istype(requested_preference, /datum/preference/color_legacy) \
+ )
+ return FALSE
+
+ var/default_value = read_preference(requested_preference.type)
+ if (istype(requested_preference, /datum/preference/color_legacy))
+ default_value = expand_three_digit_color(default_value)
+
+ // Yielding
+ var/new_color = input(
+ usr,
+ "Select new color",
+ null,
+ default_value || COLOR_WHITE,
+ ) as color | null
+
+ if (!new_color)
+ return FALSE
+
+ if (!update_preference(requested_preference, new_color))
+ return FALSE
+
+ return TRUE
+
+ //SKYRAT EDIT ADDITION
+ if ("open_advanced_prefs")
+ show_advanced_prefs(usr)
+ return TRUE
+
+ if("update_preview")
+ preview_pref = params["updated_preview"]
+ character_preview_view.update_body()
+ return TRUE
+
+ if ("open_loadout")
+ if(parent.open_loadout_ui)
+ parent.open_loadout_ui.ui_interact(usr)
else
- HTML += "[rank] "
+ var/datum/loadout_manager/tgui = new(usr)
+ tgui.ui_interact(usr)
+ return TRUE
+ //SKYRAT EDIT END
- HTML += ""
- var/prefLevelLabel = "ERROR"
- var/prefLevelColor = "pink"
- var/prefUpperLevel = -1 // level to assign on left click
- var/prefLowerLevel = -1 // level to assign on right click
+ 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)
- 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
+ return FALSE
- HTML += ""
+/datum/preferences/ui_close(mob/user)
+ save_character()
+ save_preferences()
+ QDEL_NULL(character_preview_view)
- if(rank == overflow_role.title)//Overflow is special
- if(job_preferences[overflow_role.title] == JP_LOW)
- HTML += "Yes "
- else
- HTML += "No "
- HTML += " "
- continue
+/datum/preferences/Topic(href, list/href_list)
+ . = ..()
+ if (.)
+ return
+ //SKYRAT EDIT ADDITION
+ if(href_list["preference"] || href_list["task"])
+ SkyratTopic(href, href_list, usr)
+ return TRUE
+ //SKYRAT EDIT END
- HTML += "[prefLevelLabel] "
- HTML += ""
+ if (href_list["open_keybindings"])
+ current_window = PREFERENCE_TAB_KEYBINDINGS
+ update_static_data(usr)
+ ui_interact(usr)
+ return TRUE
- for(var/i = 1, i < (limit - index), i += 1) // Finish the column so it is even
- HTML += "    "
+/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)
- HTML += "
"
- HTML += "
"
+ return character_preview_view
- var/message = "Be an [overflow_role.title] 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 "
+/datum/preferences/proc/compile_character_preferences(mob/user)
+ var/list/preferences = list()
- 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)
+ for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
+ if (!preference.is_accessible(src))
+ continue
-/datum/preferences/proc/SetJobPreferenceLevel(datum/job/job, level)
+ LAZYINITLIST(preferences[preference.category])
+
+ var/value = read_preference(preference.type)
+ var/data = preference.compile_ui_data(user, value)
+
+ preferences[preference.category][preference.savefile_key] = data
+
+ 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
+
+ for (var/category in append_character_preferences)
+ if (category in preferences)
+ preferences[category] += append_character_preferences[category]
+ else
+ preferences[category] = append_character_preferences[category]
+
+ return preferences
+
+/// 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
+
+ value_cache -= preference.type
+ preference.apply_to_client(parent, read_preference(preference.type))
+
+// 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)
+
+/// 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
+
+ /// The body that is displayed
+ var/mob/living/carbon/human/dummy/body
+
+ /// The preferences this refers to
+ var/datum/preferences/preferences
+
+ var/list/plane_masters = list()
+
+ /// The client that is watching this view
+ var/client/client
+
+ var/linked_to_prefs = TRUE //SKYRAT EDIT ADDITION
+
+/atom/movable/screen/character_preview_view/Initialize(mapload, datum/preferences/preferences, client/client, prefs_link = TRUE) //SKYRAT EDIT CHANGE
+ . = ..()
+
+ linked_to_prefs = prefs_link //SKYRAT EDIT ADDITION
+
+ assigned_map = "character_preview_[REF(src)]"
+ set_position(1, 1)
+
+ src.preferences = preferences
+
+/atom/movable/screen/character_preview_view/Destroy()
+ QDEL_NULL(body)
+
+ for (var/plane_master in plane_masters)
+ client?.screen -= plane_master
+ qdel(plane_master)
+
+ client?.clear_map(assigned_map)
+
+ if(linked_to_prefs) //SKYRAT EDIT ADDITION
+ preferences?.character_preview_view = null
+
+ client = null
+ plane_masters = null
+ preferences = null
+
+ return ..()
+
+/// 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)
+
+/atom/movable/screen/character_preview_view/proc/create_body()
+ QDEL_NULL(body)
+
+ body = new
+
+ // Without this, it doesn't show up in the menu
+ body.appearance_flags &= ~KEEP_TOGETHER
+
+/// Registers the relevant map objects to a client
+/atom/movable/screen/character_preview_view/proc/register_to_client(client/client)
+ QDEL_LIST(plane_masters)
+
+ src.client = client
+
+ if (!client)
+ return
+
+ 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
+
+ plane_masters += plane_master
+
+ client?.register_map_obj(src)
+
+/datum/preferences/proc/create_character_profiles()
+ var/list/profiles = list()
+
+ 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
+
+ savefile.cd = "/character[index]"
+
+ var/name
+ READ_FILE(savefile["real_name"], name)
+
+ if (isnull(name))
+ profiles += null
+ continue
+
+ profiles += name
+
+ return profiles
+
+/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
+ if (level == JP_HIGH)
+ for(var/other_job in job_preferences)
+ if(job_preferences[other_job] == JP_HIGH)
+ job_preferences[other_job] = JP_MEDIUM
job_preferences[job.title] = level
+
return TRUE
-/datum/preferences/proc/UpdateJobPreference(mob/user, role, desiredLvl)
- if(!SSjob || length(SSjob.joinable_occupations) <= 0)
- return
- var/datum/job/job = SSjob.GetJob(role)
-
- if(!job || !(job.job_flags & JOB_NEW_PLAYER_JOINABLE))
- user << browse(null, "window=mob_occupation")
- ShowChoices(user)
- return
-
- if (!isnum(desiredLvl))
- to_chat(user, span_danger("UpdateJobPreference - desired level was not a number. Please notify coders!"))
- ShowChoices(user)
- CRASH("UpdateJobPreference called with desiredLvl value of [isnull(desiredLvl) ? "null" : desiredLvl]")
-
- var/jpval = null
- switch(desiredLvl)
- if(3)
- jpval = JP_LOW
- if(2)
- jpval = JP_MEDIUM
- if(1)
- jpval = JP_HIGH
-
- if(job.type == 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
-
- var/list/dat = list()
- if(!SSquirks.quirks.len)
- dat += "The quirk subsystem hasn't finished initializing, please hold..."
- dat += "Done "
- 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 = "This trait is unavailable."
- var/quirk_conflict = FALSE
- for(var/_V in all_quirks)
- if(_V == quirk_name)
- has_quirk = TRUE
- if(initial(T.mood_quirk) && CONFIG_GET(flag/disable_human_mood))
- lock_reason = "Mood is disabled."
- quirk_conflict = TRUE
- if(has_quirk)
- if(quirk_conflict)
- 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(quirk_conflict)
- 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 "
-
- 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)
-
/datum/preferences/proc/GetQuirkBalance()
var/bal = 0
for(var/V in all_quirks)
var/datum/quirk/T = SSquirks.quirks[V]
bal -= initial(T.value)
+ //SKYRAT EDIT ADDITION
+ for(var/key in augments)
+ var/datum/augment_item/aug = GLOB.augment_items[augments[key]]
+ bal -= aug.cost
+ //SKYRAT EDIT END
return bal
/datum/preferences/proc/GetPositiveQuirkCount()
@@ -1142,823 +516,23 @@ GLOBAL_LIST_EMPTY(preferences_datums)
if(GetQuirkBalance() < 0)
all_quirks = list()
-/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)
- 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.admin_datums[user.ckey] || GLOB.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?.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)
- var/datum/job/overflow_role = SSjob.GetJobType(SSjob.overflow_role)
- if(is_banned_from(user.ckey, overflow_role.title))
- joblessrole = BERANDOMJOB
- else
- joblessrole = BEOVERFLOW
- if(BEOVERFLOW)
- joblessrole = BERANDOMJOB
- if(BERANDOMJOB)
- joblessrole = RETURNTOLOBBY
- 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
- if(!(quirk in L))
- continue
- for(var/Q in all_quirks)
- if((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]
- 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/is_positive_quirk = SSquirks.quirk_points[quirk] > 0
- if(is_positive_quirk && GetPositiveQuirkCount() >= MAX_QUIRKS)
- 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
-
- 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("hairstyle")
- hairstyle = random_hairstyle(gender)
- if("facial")
- facial_hair_color = random_short_color()
- if("facial_hairstyle")
- facial_hairstyle = random_facial_hairstyle(gender)
- if("underwear")
- underwear = random_underwear(gender)
- if("underwear_color")
- underwear_color = random_short_color()
- 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("species")
- random_species()
- if("bag")
- backpack = pick(GLOB.backpacklist)
- if("suit")
- jumpsuit_style = pick(GLOB.jumpsuitlist)
- if("all")
- apply_character_randomization_prefs()
-
- 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)
- 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 . It must not contain any words restricted by IC chat and name filters. ")
-
- 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("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("hairstyle")
- var/new_hairstyle = input(user, "Choose your character's hairstyle:", "Character Preference") as null|anything in GLOB.hairstyles_list
- if(new_hairstyle)
- hairstyle = new_hairstyle
-
- if("next_hairstyle")
- hairstyle = next_list_item(hairstyle, GLOB.hairstyles_list)
-
- if("previous_hairstyle")
- hairstyle = previous_list_item(hairstyle, GLOB.hairstyles_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_hairstyle")
- var/new_facial_hairstyle = input(user, "Choose your character's facial-hairstyle:", "Character Preference") as null|anything in GLOB.facial_hairstyles_list
- if(new_facial_hairstyle)
- facial_hairstyle = new_facial_hairstyle
-
- if("next_facehairstyle")
- facial_hairstyle = next_list_item(facial_hairstyle, GLOB.facial_hairstyles_list)
-
- if("previous_facehairstyle")
- facial_hairstyle = previous_list_item(facial_hairstyle, GLOB.facial_hairstyles_list)
-
- if("underwear")
- var/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("underwear_color")
- var/new_underwear_color = input(user, "Choose your character's underwear color:", "Character Preference","#"+underwear_color) as color|null
- if(new_underwear_color)
- underwear_color = sanitize_hexcolor(new_underwear_color)
-
- if("undershirt")
- var/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("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 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("#7F7F7F")[3]))
- features["mcolor"] = pref_species.default_color
- if(randomise[RANDOM_NAME])
- real_name = pref_species.random_name(gender)
-
- if("mutant_color")
- 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("#7F7F7F")[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("color_ethereal")
- 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("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_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("moth_antennae")
- var/new_moth_antennae
- new_moth_antennae = input(user, "Choose your character's antennae:", "Character Preference") as null|anything in GLOB.moth_antennae_list
- if(new_moth_antennae)
- features["moth_antennae"] = new_moth_antennae
-
- if("moth_markings")
- var/new_moth_markings
- new_moth_markings = input(user, "Choose your character's markings:", "Character Preference") as null|anything in GLOB.moth_markings_list
- if(new_moth_markings)
- features["moth_markings"] = new_moth_markings
-
- 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 = sanitize_ooccolor(new_ooccolor)
-
- if("asaycolor")
- var/new_asaycolor = input(user, "Choose your ASAY color:", "Game Preference",asaycolor) as color|null
- if(new_asaycolor)
- asaycolor = sanitize_ooccolor(new_asaycolor)
-
- if("briefoutfit")
- var/list/valid_paths = list()
- for(var/datum/outfit/outfit_path as anything in subtypesof(/datum/outfit))
- valid_paths[initial(outfit_path.name)] = outfit_path
- var/new_outfit = input(user, "Choose your briefing officer outfit:", "Game Preference") as null|anything in valid_paths
- new_outfit = valid_paths[new_outfit]
- if(new_outfit)
- brief_outfit = new_outfit
-
- if("bag")
- var/new_backpack = input(user, "Choose your character's style of bag:", "Character Preference") as null|anything in GLOB.backpacklist
- if(new_backpack)
- backpack = new_backpack
-
- if("suit")
- if(jumpsuit_style == PREF_SUIT)
- jumpsuit_style = PREF_SKIRT
- else
- jumpsuit_style = 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)
- // This is done to prevent affecting saves
- uplink_spawn_loc = new_loc == UPLINK_IMPLANT_WITH_PRICE ? UPLINK_IMPLANT : new_loc
-
- if("playtime_reward_cloak")
- if (user.client.get_exp_living(TRUE) >= PLAYTIME_VETERAN)
- playtime_reward_cloak = !playtime_reward_cloak
-
- 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 ("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 sortList(maplist)
- if (pickedmap)
- preferred_map = maplist[pickedmap]
-
- if ("clientfps")
- var/desiredfps = input(user, "Choose your desired fps.\n-1 means recommended value (currently:[RECOMMENDED_FPS])\n0 means world fps (currently:[world.fps])", "Character Preference", clientfps) as null|num
- if (!isnull(desiredfps))
- clientfps = sanitize_integer(desiredfps, -1, 1000, clientfps)
- parent.fps = (clientfps < 0) ? RECOMMENDED_FPS : clientfps
- if("ui")
- var/pickedui = input(user, "Choose your UI style.", "Character Preference", UI_style) as null|anything in sortList(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("phobia")
- var/phobiaType = input(user, "What are you scared of?", "Character Preference", phobia) as null|anything in SStraumas.phobia_types
- if(phobiaType)
- phobia = phobiaType
-
- 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)
-
- else
- switch(href_list["preference"])
- if("publicity")
- if(unlock_content)
- toggles ^= MEMBER_PUBLIC
- if("gender")
- var/list/friendlyGenders = list("Male" = "male", "Female" = "female", "Other" = "plural")
- 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_hairstyle = random_facial_hairstyle(gender)
- hairstyle = random_hairstyle(gender)
- if("body_type")
- if(body_type == MALE)
- body_type = FEMALE
- else
- body_type = MALE
- 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 = tgui_alert(user, "Would you prefer 'hotkey' or 'classic' defaults?", "Setup keybindings", list("Hotkey", "Classic", "Cancel"), 60 SECONDS)
- 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")
- toggles ^= SPLIT_ADMIN_TABS
- 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_ignore_cult_ghost")
- toggles ^= ADMIN_IGNORE_CULT_GHOST
-
-
- 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("toggle_random")
- var/random_type = href_list["random_type"]
- if(randomise[random_type])
- randomise -= random_type
- else
- randomise[random_type] = TRUE
-
- 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("endofround_sounds")
- toggles ^= SOUND_ENDOFROUND
-
- if("combat_mode_sound")
- toggles ^= SOUND_COMBATMODE
-
- 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("ghost_laws")
- chat_toggles ^= CHAT_GHOSTLAWS
-
- if("hear_login_logout")
- chat_toggles ^= CHAT_LOGIN_LOGOUT
-
- if("broadcast_login_logout")
- broadcast_login_logout = !broadcast_login_logout
-
- 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("screentipmode")
- screentip_pref = !screentip_pref
-
- if("screentipcolor")
- var/new_screentipcolor = input(user, "Choose your screentip color:", "Character Preference", screentip_color) as color|null
- if(new_screentipcolor)
- screentip_color = sanitize_ooccolor(new_screentipcolor)
-
- if("itemoutline_pref")
- itemoutline_pref = !itemoutline_pref
-
- if("ambientocclusion")
- ambientocclusion = !ambientocclusion
- if(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("darkened_flash")
- darkened_flash = !darkened_flash
-
- if("save")
- save_preferences()
- save_character()
-
- if("load")
- load_preferences()
- load_character()
-
- if("changeslot")
- if(!load_character(text2num(href_list["num"])))
- randomise_appearance_prefs()
- save_character()
-
- if("tab")
- if (href_list["tab"])
- current_tab = text2num(href_list["tab"])
-
- if("clear_heart")
- hearted = FALSE
- hearted_until = null
- to_chat(user, span_notice("OOC Commendation Heart disabled"))
- save_preferences()
-
- ShowChoices(user)
- return 1
-
-
-/// Sanitization checks to be performed before using these preferences.
-/datum/preferences/proc/sanitize_chosen_prefs()
- if(!(pref_species.id in GLOB.roundstart_races) && !(pref_species.id in (CONFIG_GET(keyed_list/roundstart_no_hard_check))))
- pref_species = new /datum/species/human
- save_character()
-
- if(CONFIG_GET(flag/humans_need_surnames) && (pref_species.id == SPECIES_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)]"
-
-
/// Sanitizes the preferences, applies the randomization prefs, and then applies the preference to the human mob.
/datum/preferences/proc/safe_transfer_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE, is_antag = FALSE)
apply_character_randomization_prefs(is_antag)
- sanitize_chosen_prefs()
apply_prefs_to(character, icon_updates)
-
/// Applies the given preferences to a human mob.
/datum/preferences/proc/apply_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE)
- character.real_name = real_name
- character.name = real_name
+ character.dna.features = MANDATORY_FEATURE_LIST //SKYRAT EDIT CHANGE - We need to instansiate the list with the basic features.
- character.gender = gender
- character.age = age
- if(gender == MALE || gender == FEMALE)
- character.body_type = gender
- else
- character.body_type = body_type
+ for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
+ if (preference.savefile_identifier != PREFERENCE_CHARACTER)
+ continue
- 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.skin_tone = skin_tone
- character.hairstyle = hairstyle
- character.facial_hairstyle = facial_hairstyle
- character.underwear = underwear
- character.underwear_color = underwear_color
- character.undershirt = undershirt
- character.socks = socks
+ preference.apply_to_human(character, read_preference(preference.type), src) //SKYRAT EDIT CHANGE
- character.backpack = backpack
-
- character.jumpsuit_style = jumpsuit_style
-
- character.dna.features = features.Copy()
- character.set_species(pref_species.type, icon_update = FALSE, pref_load = TRUE)
character.dna.real_name = character.real_name
- if(pref_species.mutant_bodyparts["tail_lizard"])
- character.dna.species.mutant_bodyparts["tail_lizard"] = pref_species.mutant_bodyparts["tail_lizard"]
- if(pref_species.mutant_bodyparts["spines"])
- character.dna.species.mutant_bodyparts["spines"] = pref_species.mutant_bodyparts["spines"]
-
if(icon_updates)
character.update_body()
character.update_hair()
@@ -1967,7 +541,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
/// Returns whether the parent mob should have the random hardcore settings enabled. Assumes it has a mind.
/datum/preferences/proc/should_be_random_hardcore(datum/job/job, datum/mind/mind)
- if(!randomise[RANDOM_HARDCORE])
+ if(!read_preference(/datum/preference/toggle/random_hardcore))
return FALSE
if(job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) //No command staff
return FALSE
@@ -1976,115 +550,23 @@ GLOBAL_LIST_EMPTY(preferences_datums)
return FALSE
return TRUE
+/// 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()
-/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 pick(GLOB.religion_names)
- if("deity")
- return DEFAULT_DEITY
- if("bible")
- return DEFAULT_BIBLE
- return random_unique_name()
+ for (var/action in key_bindings)
+ for (var/key in key_bindings[action])
+ LAZYADD(output[key], action)
-/datum/preferences/proc/ask_for_custom_name(mob/user,name_id)
- var/namedata = GLOB.preferences_custom_names[name_id]
- if(!namedata)
- return
+ return output
- 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 . It must not contain any words restricted by IC chat and name filters. ")
- return
- else
- custom_names[name_id] = sanitized_name
+/// Returns the default `randomise` variable ouptut
+/datum/preferences/proc/get_default_randomization()
+ var/list/default_randomization = list()
- if(name_id == "religion")
- update_bible_and_deity_name(custom_names[name_id])
+ 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
-/datum/preferences/proc/update_bible_and_deity_name(religion)
- switch(lowertext(religion))
- if("christianity") // DEFAULT_RELIGION
- custom_names["bible"] = pick("The Holy Bible","The Dead Sea Scrolls")
- custom_names["deity"] = "Space Jesus"
- if("buddhism")
- custom_names["bible"] = "The Sutras"
- custom_names["deity"] = "Buddha"
- if("clownism","honkmother","honk","honkism","comedy")
- custom_names["bible"] = pick("The Holy Joke Book", "Just a Prank", "Hymns to the Honkmother")
- custom_names["deity"] = "The Honkmother"
- if("chaos")
- custom_names["bible"] = "The Book of Lorgar"
- custom_names["deity"] = pick("Chaos Gods", "Dark Gods", "Ruinous Powers")
- if("cthulhu")
- custom_names["bible"] = "The Necronomicon"
- custom_names["deity"] = pick("Great Old Ones", "Old Ones")
- if("hinduism")
- custom_names["bible"] = "The Vedas"
- custom_names["deity"] = pick("Brahma", "Vishnu", "Shiva")
- if("imperium")
- custom_names["bible"] = "Uplifting Primer"
- custom_names["deity"] = "Astra Militarum"
- if("islam")
- custom_names["bible"] = "Quran"
- custom_names["deity"] = "Allah"
- if("judaism")
- custom_names["bible"] = "The Torah"
- custom_names["deity"] = "Yahweh"
- if("lampism")
- custom_names["bible"] = "Fluorescent Incandescence"
- custom_names["deity"] = "Lamp"
- if("monkeyism","apism","gorillism","primatism")
- custom_names["bible"] = pick("Going Bananas", "Bananas Out For Harambe")
- custom_names["deity"] = pick("Harambe", "monky")
- if("mormonism")
- custom_names["bible"] = "The Book of Mormon"
- custom_names["deity"] = pick("God", "Elohim", "Godhead")
- if("pastafarianism")
- custom_names["bible"] = "The Gospel of the Flying Spaghetti Monster"
- custom_names["deity"] = "Flying Spaghetti Monster"
- if("rastafarianism","rasta")
- custom_names["bible"] = "The Holy Piby"
- custom_names["deity"] = "Haile Selassie I"
- if("satanism")
- custom_names["bible"] = "The Unholy Bible"
- custom_names["deity"] = "Satan"
- if("sikhism")
- custom_names["bible"] = "Guru Granth Sahib"
- custom_names["deity"] = "Waheguru"
- if("science")
- custom_names["bible"] = pick("Principle of Relativity", "Quantum Enigma: Physics Encounters Consciousness", "Programming the Universe", "Quantum Physics and Theology", "String Theory for Dummies", "How To: Build Your Own Warp Drive", "The Mysteries of Bluespace", "Playing God: Collector's Edition")
- custom_names["deity"] = pick("Albert Einstein", "Stephen Hawking", "Neil deGrasse Tyson", "Carl Sagan", "Richard Dawkins")
- if("scientology")
- custom_names["bible"] = pick("The Biography of L. Ron Hubbard","Dianetics")
- custom_names["deity"] = pick("Money", "Power", "Xenu", "Tom Cruise", "L. Ron Hubbard", "David Miscavige", "John Travolta")
- if("servicianism", "partying")
- custom_names["bible"] = "The Tenets of Servicia"
- custom_names["deity"] = pick("Servicia", "Space Bacchus", "Space Dionysus")
- if("subgenius")
- custom_names["bible"] = "Book of the SubGenius"
- custom_names["deity"] = pick("Jehovah 1", "J. R. \"Bob\" Dobbs")
- if("toolboxia","greytide")
- custom_names["bible"] = pick("Toolbox Manifesto","iGlove Assistants")
- custom_names["deity"] = "Maintenance"
- else
- if(custom_names["bible"] == DEFAULT_BIBLE)
- custom_names["bible"] = "The Holy Book of [religion]"
-*/
+ return default_randomization
diff --git a/code/modules/client/preferences/README.md b/code/modules/client/preferences/README.md
new file mode 100644
index 00000000000..c2d238f34db
--- /dev/null
+++ b/code/modules/client/preferences/README.md
@@ -0,0 +1,476 @@
+# 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"
+```
+
+
+
+> 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.
+
+---
+
+## Jobs
+
+Every job must have an associated `.ts` file so that the UI knows where to place it.
+
+Create a file in `tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/`.
+
+This will specify the description and department of the job. Here is the details for the medical doctor:
+
+```ts
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const Cook: Job = {
+ name: "Cook",
+ // If you need more room, use `multiline`
+ description: "Serve food, cook meat, keep the crew fed.",
+ department: Service,
+};
+
+export default Cook;
+```
+
+---
+
+## 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:
+
+
+
+## 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 00000000000..df1c2dc9f02
--- /dev/null
+++ b/code/modules/client/preferences/_preference.dm
@@ -0,0 +1,545 @@
+// 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 body type is decided, applied after gender so we can
+/// make sure they're non-binary.
+#define PREFERENCE_PRIORITY_BODY_TYPE 4
+
+/// The priority at which names are decided, needed for proper randomization.
+#define PREFERENCE_PRIORITY_NAMES 5
+
+/// 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 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
+
+/// 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)
+ 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, datum/preferences/preferecnes) //SKYRAT EDIT CHANGE
+ SHOULD_NOT_SLEEP(TRUE)
+ 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)
+
+ // 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.
+
+ if (!parent)
+ return null
+
+ 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
+
+/// 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]
+
+ 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)
+ 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/admin.dm b/code/modules/client/preferences/admin.dm
new file mode 100644
index 00000000000..9cc1026d473
--- /dev/null
+++ b/code/modules/client/preferences/admin.dm
@@ -0,0 +1,41 @@
+/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)
+
+/// What outfit to equip when spawning as a briefing officer for an ERT
+/datum/preference/choiced/brief_outfit
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "brief_outfit"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/choiced/brief_outfit/deserialize(input, datum/preferences/preferences)
+ var/path = text2path(input)
+ if (!ispath(path, /datum/outfit))
+ return create_default_value()
+
+ return path
+
+/datum/preference/choiced/brief_outfit/serialize(input)
+ return "[input]"
+
+/datum/preference/choiced/brief_outfit/create_default_value()
+ return /datum/outfit/centcom/commander
+
+/datum/preference/choiced/brief_outfit/init_possible_values()
+ return subtypesof(/datum/outfit)
+
+/datum/preference/choiced/brief_outfit/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return is_admin(preferences.parent)
diff --git a/code/modules/client/preferences/age.dm b/code/modules/client/preferences/age.dm
new file mode 100644
index 00000000000..cad9786ce1f
--- /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 00000000000..dab61f2b323
--- /dev/null
+++ b/code/modules/client/preferences/ai_core_display.dm
@@ -0,0 +1,25 @@
+/// What to show on the AI screen
+/datum/preference/choiced/ai_core_display
+ category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
+ savefile_identifier = PREFERENCE_CHARACTER
+ savefile_key = "preferred_ai_core_display"
+ should_generate_icons = TRUE
+
+/datum/preference/choiced/ai_core_display/init_possible_values()
+ var/list/values = list()
+
+ values["Random"] = icon('icons/mob/ai.dmi', "ai-empty")
+
+ for (var/screen in GLOB.ai_core_display_screens - "Portrait" - "Random")
+ values[screen] = icon('icons/mob/ai.dmi', resolve_ai_icon_sync(screen))
+
+ return values
+
+/datum/preference/choiced/ai_core_display/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return istype(preferences.get_highest_priority_job(), /datum/job/ai)
+
+/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 00000000000..a81efca00bd
--- /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/assets.dm b/code/modules/client/preferences/assets.dm
new file mode 100644
index 00000000000..88d4313cae7
--- /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
+
+/datum/asset/spritesheet/preferences/register()
+ 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])
+
+ return ..()
+
+/// 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 00000000000..3550af05454
--- /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/body_type.dm b/code/modules/client/preferences/body_type.dm
new file mode 100644
index 00000000000..81c6b3e9baa
--- /dev/null
+++ b/code/modules/client/preferences/body_type.dm
@@ -0,0 +1,21 @@
+/datum/preference/choiced/body_type
+ category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
+ priority = PREFERENCE_PRIORITY_BODY_TYPE
+ savefile_key = "body_type"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/body_type/init_possible_values()
+ return list(MALE, FEMALE)
+
+/datum/preference/choiced/body_type/apply_to_human(mob/living/carbon/human/target, value)
+ if (target.gender != MALE && target.gender != FEMALE)
+ target.body_type = value
+ else
+ target.body_type = target.gender
+
+/datum/preference/choiced/body_type/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ var/gender = preferences.read_preference(/datum/preference/choiced/gender)
+ return gender != MALE && gender != FEMALE
diff --git a/code/modules/client/preferences/broadcast_login_logout.dm b/code/modules/client/preferences/broadcast_login_logout.dm
new file mode 100644
index 00000000000..90250118d2a
--- /dev/null
+++ b/code/modules/client/preferences/broadcast_login_logout.dm
@@ -0,0 +1,5 @@
+/// Whether or not to announce when the player logs in or out.
+/datum/preference/toggle/broadcast_login_logout
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "broadcast_login_logout"
+ savefile_identifier = PREFERENCE_PLAYER
diff --git a/code/modules/client/preferences/buttons_locked.dm b/code/modules/client/preferences/buttons_locked.dm
new file mode 100644
index 00000000000..b4f54ff10f8
--- /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 00000000000..16ab726b565
--- /dev/null
+++ b/code/modules/client/preferences/clothing.dm
@@ -0,0 +1,153 @@
+/proc/generate_values_for_underwear(list/accessory_list, list/icons, color)
+ 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(accessory.icon, accessory.icon_state) //SKYRAT EDIT CHANGE
+ if (color && !accessory.use_static)
+ accessory_icon.Blend(color, ICON_MULTIPLY)
+ 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 = "backpack"
+ 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 medical doctor, and so medical doctor you get.
+ values[DBACKPACK] = /obj/item/storage/backpack/medic
+ values[DSATCHEL] = /obj/item/storage/backpack/satchel/med
+ values[DDUFFELBAG] = /obj/item/storage/backpack/duffelbag/med
+
+ return values
+
+/datum/preference/choiced/backpack/apply_to_human(mob/living/carbon/human/target, value)
+ target.backpack = 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/color/jumpskirt/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(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/underwear.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
+
+/* SKYRAT EDIT TODO - FOR WHATEVER REASON, THIS IS BROKEN
+/// 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(GLOB.underwear_list, list("human_chest_m", "human_r_leg", "human_l_leg"), COLOR_ALMOST_BLACK)
+
+/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)
+
+/datum/preference/choiced/underwear/compile_constant_data()
+ var/list/data = ..()
+
+ data[SUPPLEMENTAL_FEATURE_KEY] = "underwear_color"
+
+ return data
+*/
diff --git a/code/modules/client/preferences/darkened_flash.dm b/code/modules/client/preferences/darkened_flash.dm
new file mode 100644
index 00000000000..ef89467bf35
--- /dev/null
+++ b/code/modules/client/preferences/darkened_flash.dm
@@ -0,0 +1,6 @@
+/// When toggled, being flashed will show a dark screen rather than a light one.
+/datum/preference/toggle/darkened_flash
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ default_value = FALSE
+ savefile_key = "darkened_flash"
+ savefile_identifier = PREFERENCE_PLAYER
diff --git a/code/modules/client/preferences/fps.dm b/code/modules/client/preferences/fps.dm
new file mode 100644
index 00000000000..8bd1310932b
--- /dev/null
+++ b/code/modules/client/preferences/fps.dm
@@ -0,0 +1,17 @@
+/datum/preference/numeric/fps
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "clientfps"
+ savefile_identifier = PREFERENCE_PLAYER
+
+ minimum = -1
+ maximum = 240
+
+/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 00000000000..a9400465b67
--- /dev/null
+++ b/code/modules/client/preferences/gender.dm
@@ -0,0 +1,11 @@
+/// 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)
+ target.gender = value
diff --git a/code/modules/client/preferences/ghost.dm b/code/modules/client/preferences/ghost.dm
new file mode 100644
index 00000000000..ce8e05235ee
--- /dev/null
+++ b/code/modules/client/preferences/ghost.dm
@@ -0,0 +1,178 @@
+/// Determines what accessories your ghost will look like they have.
+/datum/preference/choiced/ghost_accessories
+ savefile_key = "ghost_accs"
+ savefile_identifier = PREFERENCE_PLAYER
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+
+/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_appearance()
+
+/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 (!client.is_content_unlocked())
+ return
+
+ ghost.update_icon(ALL, 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/apply_to_client(client/client, value)
+ var/mob/dead/observer/ghost = client.mob
+ if (!istype(ghost))
+ return
+
+ if (!client.is_content_unlocked())
+ 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 00000000000..b96b68286d6
--- /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/item_outlines.dm b/code/modules/client/preferences/item_outlines.dm
new file mode 100644
index 00000000000..74b89966173
--- /dev/null
+++ b/code/modules/client/preferences/item_outlines.dm
@@ -0,0 +1,4 @@
+/datum/preference/toggle/item_outlines
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "itemoutline_pref"
+ 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 00000000000..8514451fb71
--- /dev/null
+++ b/code/modules/client/preferences/jobless_role.dm
@@ -0,0 +1,12 @@
+/datum/preference/choiced/jobless_role
+ savefile_key = "joblessrole"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/choiced/jobless_role/create_default_value()
+ return BERANDOMJOB
+
+/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
diff --git a/code/modules/client/preferences/middleware/_middleware.dm b/code/modules/client/preferences/middleware/_middleware.dm
new file mode 100644
index 00000000000..8f47f73642c
--- /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 00000000000..1e5bf6e0054
--- /dev/null
+++ b/code/modules/client/preferences/middleware/antags.dm
@@ -0,0 +1,163 @@
+/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]
+ )
+
+ 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
+
+/datum/asset/spritesheet/antagonists/register()
+ // 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 = non_ruleset_antagonists.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])
+
+ return ..()
+
+/// 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 00000000000..50e8b4b0d45
--- /dev/null
+++ b/code/modules/client/preferences/middleware/jobs.dm
@@ -0,0 +1,82 @@
+/datum/preference_middleware/jobs
+ action_delegations = list(
+ "set_job_preference" = .proc/set_job_preference,
+ )
+
+/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/get_ui_data(mob/user)
+ var/list/data = list()
+
+ data["job_preferences"] = preferences.job_preferences
+
+ 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.all_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.all_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 00000000000..e51852b408b
--- /dev/null
+++ b/code/modules/client/preferences/middleware/keybindings.dm
@@ -0,0 +1,89 @@
+#define MAX_HOTKEY_SLOTS 3
+
+/// Middleware to handle keybindings
+/datum/preference_middleware/keybindings
+ action_delegations = list(
+ "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_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)
+
+ 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)
+
+ 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 00000000000..e35832e5434
--- /dev/null
+++ b/code/modules/client/preferences/middleware/legacy_toggles.dm
@@ -0,0 +1,132 @@
+/// 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(
+ "admin_ignore_cult_ghost" = ADMIN_IGNORE_CULT_GHOST,
+ "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_combatmode" = SOUND_COMBATMODE,
+ "sound_endofround" = SOUND_ENDOFROUND,
+ "sound_instruments" = SOUND_INSTRUMENTS,
+ "sound_lobby" = SOUND_LOBBY,
+ "sound_midi" = SOUND_MIDI,
+ "sound_prayers" = SOUND_PRAYERS,
+ "sound_ship_ambience" = SOUND_SHIP_AMBIENCE,
+ "split_admin_tabs" = SPLIT_ADMIN_TABS,
+ )
+
+ var/list/legacy_chat_toggles = list(
+ "chat_bankcard" = CHAT_BANKCARD,
+ "chat_dead" = CHAT_DEAD,
+ "chat_ghostears" = CHAT_GHOSTEARS,
+ "chat_ghostlaws" = CHAT_GHOSTLAWS,
+ "chat_ghostpda" = CHAT_GHOSTPDA,
+ "chat_ghostradio" = CHAT_GHOSTRADIO,
+ "chat_ghostsight" = CHAT_GHOSTSIGHT,
+ "chat_ghostwhisper" = CHAT_GHOSTWHISPER,
+ "chat_login_logout" = CHAT_LOGIN_LOGOUT,
+ "chat_ooc" = CHAT_OOC,
+ "chat_prayer" = CHAT_PRAYER,
+ "chat_pullr" = CHAT_PULLR,
+ )
+
+/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",
+ "split_admin_tabs",
+ )
+
+ var/static/list/admin_only_chat_toggles = list(
+ "chat_dead",
+ "chat_prayer",
+ )
+
+ 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)
+
+ 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_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
+
+ 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
+
+ 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 00000000000..34e97f0f727
--- /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 waht 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 00000000000..f508c204ab5
--- /dev/null
+++ b/code/modules/client/preferences/middleware/quirks.dm
@@ -0,0 +1,89 @@
+/// 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()
+
+ 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()
+
+ 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),
+ )
+
+ 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) != 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) != 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
diff --git a/code/modules/client/preferences/middleware/random.dm b/code/modules/client/preferences/middleware/random.dm
new file mode 100644
index 00000000000..17bc259e2c2
--- /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/species.dm b/code/modules/client/preferences/middleware/species.dm
new file mode 100644
index 00000000000..7962980739e
--- /dev/null
+++ b/code/modules/client/preferences/middleware/species.dm
@@ -0,0 +1,43 @@
+/// Handles the assets for species icons
+/datum/preference_middleware/species
+
+//SKYRAT EDIT ADDITION
+/datum/preference_middleware/species/pre_set_preference(mob/user, preference, value)
+ var/datum/species/species_type = GLOB.species_list[value]
+ preferences.species_updated(species_type)
+ return ..()
+//SKYRAT EDIT END
+
+/datum/preference_middleware/species/get_ui_assets()
+ return list(
+ get_asset_datum(/datum/asset/spritesheet/species),
+ )
+
+/datum/asset/spritesheet/species
+ name = "species"
+ early = TRUE
+
+/datum/asset/spritesheet/species/register()
+ 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)
+ COMPILE_OVERLAYS(dummy)
+
+ var/icon/dummy_icon = getFlatIcon(dummy)
+ 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])
+
+ return ..()
diff --git a/code/modules/client/preferences/names.dm b/code/modules/client/preferences/names.dm
new file mode 100644
index 00000000000..7c6dbdd2165
--- /dev/null
+++ b/code/modules/client/preferences/names.dm
@@ -0,0 +1,156 @@
+/// 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"
+
+/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 pick(GLOB.religion_names)
+
+/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
+
+/datum/preference/name/bible
+ savefile_key = "bible_name"
+
+ allow_numbers = TRUE
+ can_randomize = FALSE
+
+ explanation = "Bible name"
+ group = "religion"
+
+/datum/preference/name/bible/create_default_value()
+ return DEFAULT_BIBLE
diff --git a/code/modules/client/preferences/ooc.dm b/code/modules/client/preferences/ooc.dm
new file mode 100644
index 00000000000..de436391edc
--- /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 00000000000..24cccce2da6
--- /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 00000000000..fcbf4f1ad4b
--- /dev/null
+++ b/code/modules/client/preferences/pda.dm
@@ -0,0 +1,17 @@
+/// 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
diff --git a/code/modules/client/preferences/phobia.dm b/code/modules/client/preferences/phobia.dm
new file mode 100644
index 00000000000..2a5caa5ba0f
--- /dev/null
+++ b/code/modules/client/preferences/phobia.dm
@@ -0,0 +1,16 @@
+/datum/preference/choiced/phobia
+ category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
+ savefile_key = "phobia"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/phobia/init_possible_values()
+ return GLOB.phobia_types
+
+/datum/preference/choiced/phobia/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return "Phobia" in preferences.all_quirks
+
+/datum/preference/choiced/phobia/apply_to_human(mob/living/carbon/human/target, value)
+ return
diff --git a/code/modules/client/preferences/pixel_size.dm b/code/modules/client/preferences/pixel_size.dm
new file mode 100644
index 00000000000..cb166e0139a
--- /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/playtime_reward_cloak.dm b/code/modules/client/preferences/playtime_reward_cloak.dm
new file mode 100644
index 00000000000..5eb1c89e9b2
--- /dev/null
+++ b/code/modules/client/preferences/playtime_reward_cloak.dm
@@ -0,0 +1,20 @@
+/// This can be set to TRUE from the prefs menu only once the user has
+/// gained over 5K playtime hours.
+/// If true, it allows the user to get a cool looking roundstart cloak.
+/datum/preference/toggle/playtime_reward_cloak
+ category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
+ savefile_identifier = PREFERENCE_CHARACTER
+ savefile_key = "playtime_reward_cloak"
+
+/datum/preference/toggle/playtime_reward_cloak/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return preferences.parent?.is_veteran()
+
+/datum/preference/toggle/playtime_reward_cloak/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/// Returns whether the client should receive the gamer cloak
+/client/proc/is_veteran()
+ return get_exp_living(pure_numeric = TRUE) >= PLAYTIME_VETERAN
diff --git a/code/modules/client/preferences/preferred_map.dm b/code/modules/client/preferences/preferred_map.dm
new file mode 100644
index 00000000000..8c9f87e8232
--- /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 00000000000..6ad675fcda2
--- /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 00000000000..83282fefe36
--- /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 00000000000..63235abaf99
--- /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/screentips.dm b/code/modules/client/preferences/screentips.dm
new file mode 100644
index 00000000000..d446575ac7b
--- /dev/null
+++ b/code/modules/client/preferences/screentips.dm
@@ -0,0 +1,18 @@
+/datum/preference/toggle/enable_screentips
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "screentip_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/enable_screentips/apply_to_client(client/client, value)
+ client.mob?.hud_used?.screentips_enabled = value
+
+/datum/preference/color/screentip_color
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "screentip_color"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/color/screentip_color/apply_to_client(client/client, value)
+ client.mob?.hud_used?.screentip_color = value
+
+/datum/preference/color/screentip_color/create_default_value()
+ return "#ffd391"
diff --git a/code/modules/client/preferences/security_department.dm b/code/modules/client/preferences/security_department.dm
new file mode 100644
index 00000000000..d3a637275ef
--- /dev/null
+++ b/code/modules/client/preferences/security_department.dm
@@ -0,0 +1,27 @@
+/// Which department to put security officers in, when the config is enabled
+/datum/preference/choiced/security_department
+ category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
+ can_randomize = FALSE
+ savefile_identifier = PREFERENCE_CHARACTER
+ savefile_key = "prefered_security_department"
+
+// This is what that #warn wants you to remove :)
+/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/init_possible_values()
+ return GLOB.security_depts_prefs
+
+/datum/preference/choiced/security_department/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/datum/preference/choiced/security_department/create_default_value()
+ return SEC_DEPT_NONE
+
+/datum/preference/choiced/security_department/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return !CONFIG_GET(flag/sec_start_brig)
diff --git a/code/modules/client/preferences/skin_tone.dm b/code/modules/client/preferences/skin_tone.dm
new file mode 100644
index 00000000000..60e71da430a
--- /dev/null
+++ b/code/modules/client/preferences/skin_tone.dm
@@ -0,0 +1,37 @@
+/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/rgb = hex2rgb("#[hex_value]")
+ var/list/hsl = rgb2hsl(rgb[1], rgb[2], rgb[3])
+
+ 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 00000000000..f6d971b50b1
--- /dev/null
+++ b/code/modules/client/preferences/species.dm
@@ -0,0 +1,75 @@
+/// 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, datum/preferences/prefs)
+ target.set_species(value, FALSE, prefs?.features.Copy(), prefs?.mutant_bodyparts.Copy(), prefs?.body_markings.Copy())
+
+//SKYRAT EDIT ADDITION
+ target.dna.update_body_size()
+
+ for(var/organ_key in list(ORGAN_SLOT_VAGINA, ORGAN_SLOT_PENIS, ORGAN_SLOT_BREASTS))
+ var/obj/item/organ/genital/gent = target.getorganslot(organ_key)
+ if(gent)
+ gent.aroused = prefs.arousal_preview
+ gent.update_sprite_suffix()
+
+ if(length(prefs.augments))
+ for(var/key in prefs.augments)
+ var/datum/augment_item/aug = GLOB.augment_items[prefs.augments[key]]
+ aug.apply(target, prefs = prefs)
+//SKYRAT EDIT END
+
+/datum/preference/choiced/species/compile_constant_data()
+ var/list/data = list()
+
+ var/list/food_flags = FOOD_FLAGS
+
+ for (var/species_id in get_selectable_species())
+ var/species_type = GLOB.species_list[species_id]
+ var/datum/species/species = new species_type
+
+ var/list/diet = list()
+
+ if (!(TRAIT_NOHUNGER in species.inherent_traits))
+ diet = list(
+ "liked_food" = bitfield2list(species.liked_food, food_flags),
+ "disliked_food" = bitfield2list(species.disliked_food, food_flags),
+ "toxic_food" = bitfield2list(species.toxic_food, food_flags),
+ )
+
+ data[species_id] = list(
+ "name" = species.name,
+ "icon" = sanitize_css_class_name(species.name),
+
+ "use_skintones" = species.use_skintones,
+ "sexes" = species.sexes,
+
+ "enabled_features" = species.get_features(),
+ ) + diet
+
+ 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 00000000000..bf57c5fd1b5
--- /dev/null
+++ b/code/modules/client/preferences/species_features/basic.dm
@@ -0,0 +1,101 @@
+/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
+ savefile_key = "eye_color"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
+ relevant_species_trait = EYECOLOR
+
+/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/facial_hairstyle
+ savefile_key = "facial_style_name"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_FEATURES
+ 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_hairstyles_list)
+
+/datum/preference/choiced/facial_hairstyle/apply_to_human(mob/living/carbon/human/target, value)
+ target.facial_hairstyle = value
+
+/datum/preference/choiced/facial_hairstyle/compile_constant_data()
+ var/list/data = ..()
+
+ data[SUPPLEMENTAL_FEATURE_KEY] = "facial_hair_color"
+
+ return data
+
+/datum/preference/color_legacy/facial_hair_color
+ savefile_key = "facial_hair_color"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES
+ relevant_species_trait = FACEHAIR
+
+/datum/preference/color_legacy/facial_hair_color/apply_to_human(mob/living/carbon/human/target, value)
+ target.facial_hair_color = value
+
+/datum/preference/color_legacy/hair_color
+ savefile_key = "hair_color"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES
+ relevant_species_trait = HAIR
+
+/datum/preference/color_legacy/hair_color/apply_to_human(mob/living/carbon/human/target, value)
+ target.hair_color = value
+
+/datum/preference/choiced/hairstyle
+ savefile_key = "hairstyle_name"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_FEATURES
+ main_feature_name = "Hairstyle"
+ 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.hairstyles_list)
+
+/datum/preference/choiced/hairstyle/apply_to_human(mob/living/carbon/human/target, value)
+ target.hairstyle = value
+
+/datum/preference/choiced/hairstyle/compile_constant_data()
+ var/list/data = ..()
+
+ data[SUPPLEMENTAL_FEATURE_KEY] = "hair_color"
+
+ return data
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 00000000000..5a0ac1e5be5
--- /dev/null
+++ b/code/modules/client/preferences/species_features/ethereal.dm
@@ -0,0 +1,33 @@
+/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]
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 00000000000..2dbe915bf5b
--- /dev/null
+++ b/code/modules/client/preferences/species_features/felinid.dm
@@ -0,0 +1,35 @@
+/* SKYRAT EDIT REMOVAL
+/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.sprite_accessories["tail"])
+
+/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 GLOB.sprite_accessories["ears"]
+
+/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/lizard.dm b/code/modules/client/preferences/species_features/lizard.dm
new file mode 100644
index 00000000000..43860174b89
--- /dev/null
+++ b/code/modules/client/preferences/species_features/lizard.dm
@@ -0,0 +1,163 @@
+/*
+/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
+ savefile_key = "feature_lizard_body_markings"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_FEATURES
+ main_feature_name = "Body markings"
+ should_generate_icons = TRUE
+ relevant_mutant_bodypart = "body_markings"
+
+/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.sprite_accessories["body_markings"]) //SKYRAT EDIT
+ var/datum/sprite_accessory/sprite_accessory = GLOB.sprite_accessories["body_markings"][name] //SKYRAT EDIT
+
+ 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)
+ if(!target.dna.mutant_bodyparts["body_markings"])
+ target.dna.mutant_bodyparts["body_markings"] = list()
+ target.dna.mutant_bodyparts["body_markings"][MUTANT_INDEX_NAME] = value
+ target.dna.mutant_bodyparts["body_markings"][MUTANT_INDEX_COLOR_LIST] = list("333", "222", "444")
+
+/datum/preference/choiced/lizard_frills
+ savefile_key = "feature_lizard_frills"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_FEATURES
+ main_feature_name = "Frills"
+ should_generate_icons = TRUE
+
+/datum/preference/choiced/lizard_frills/init_possible_values()
+ return generate_lizard_side_shots(GLOB.sprite_accessories["frills"], "frills") //SKYRAT EDIT
+
+/datum/preference/choiced/lizard_frills/apply_to_human(mob/living/carbon/human/target, value)
+ if(!target.dna.species.mutant_bodyparts["frills"])
+ target.dna.species.mutant_bodyparts["frills"] = list()
+ target.dna.species.mutant_bodyparts["frills"][MUTANT_INDEX_NAME] = value
+ target.dna.species.mutant_bodyparts["frills"][MUTANT_INDEX_COLOR_LIST] = list("333", "222", "444")
+
+/datum/preference/choiced/lizard_horns
+ savefile_key = "feature_lizard_horns"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_FEATURES
+ main_feature_name = "Horns"
+ should_generate_icons = TRUE
+
+/datum/preference/choiced/lizard_horns/init_possible_values()
+ return generate_lizard_side_shots(GLOB.sprite_accessories["horns"], "horns") //SKYRAT EDIT
+
+/datum/preference/choiced/lizard_horns/apply_to_human(mob/living/carbon/human/target, value)
+ if(!target.dna.species.mutant_bodyparts["horns"])
+ target.dna.species.mutant_bodyparts["horns"] = list()
+ target.dna.species.mutant_bodyparts["horns"][MUTANT_INDEX_NAME] = value
+ target.dna.species.mutant_bodyparts["horns"][MUTANT_INDEX_COLOR_LIST] = list("333", "222", "444")
+
+/* SKYRAT EDIT REMOVAL
+/datum/preference/choiced/lizard_legs
+ savefile_key = "feature_lizard_legs"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
+ relevant_mutant_bodypart = "legs"
+
+/datum/preference/choiced/lizard_legs/init_possible_values()
+ return GLOB.sprite_accessories["legs"] //SKYRAT EDIT CHANGE
+
+/datum/preference/choiced/lizard_legs/apply_to_human(mob/living/carbon/human/target, value)
+ target.dna.features["legs"] = value
+*/
+
+/datum/preference/choiced/lizard_snout
+ savefile_key = "feature_lizrd_snout"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_FEATURES
+ main_feature_name = "Snout"
+ should_generate_icons = TRUE
+
+/datum/preference/choiced/lizard_snout/init_possible_values()
+ return generate_lizard_side_shots(GLOB.sprite_accessories["snout"], "snout", include_snout = FALSE) //SKYRAT EDIT
+
+/datum/preference/choiced/lizard_snout/apply_to_human(mob/living/carbon/human/target, value)
+ if(!target.dna.species.mutant_bodyparts["snout"])
+ target.dna.species.mutant_bodyparts["snout"] = list()
+ target.dna.species.mutant_bodyparts["snout"][MUTANT_INDEX_NAME] = value
+ target.dna.species.mutant_bodyparts["snout"][MUTANT_INDEX_COLOR_LIST] = list("333", "222", "444")
+
+/datum/preference/choiced/lizard_spines
+ savefile_key = "feature_lizard_spines"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
+ relevant_mutant_bodypart = "spines"
+
+/datum/preference/choiced/lizard_spines/init_possible_values()
+ return GLOB.sprite_accessories["spines"] //SKYRAT EDIT
+
+/datum/preference/choiced/lizard_spines/apply_to_human(mob/living/carbon/human/target, value)
+ if(!target.dna.species.mutant_bodyparts["spines"])
+ target.dna.species.mutant_bodyparts["spines"] = list()
+ target.dna.species.mutant_bodyparts["spines"][MUTANT_INDEX_NAME] = value
+ target.dna.species.mutant_bodyparts["spines"][MUTANT_INDEX_COLOR_LIST] = list("333", "222", "444")
+
+/datum/preference/choiced/lizard_tail
+ savefile_key = "feature_lizard_tail"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
+ relevant_mutant_bodypart = "tail_lizard"
+
+/datum/preference/choiced/lizard_tail/init_possible_values()
+ return GLOB.sprite_accessories["tail"] //SKYRAT EDIT
+
+/datum/preference/choiced/lizard_tail/apply_to_human(mob/living/carbon/human/target, value)
+ if(!target.dna.species.mutant_bodyparts["tail"])
+ target.dna.species.mutant_bodyparts["tail"] = list()
+ target.dna.species.mutant_bodyparts["tail"][MUTANT_INDEX_NAME] = value
+ target.dna.species.mutant_bodyparts["tail"][MUTANT_INDEX_COLOR_LIST] = list("333", "222", "444")
+*/
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 00000000000..1ddc14c1adb
--- /dev/null
+++ b/code/modules/client/preferences/species_features/moth.dm
@@ -0,0 +1,101 @@
+/* SKYRAT EDIT REMOVAL
+/datum/preference/choiced/moth_antennae
+ savefile_key = "feature_moth_antennae"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_FEATURES
+ main_feature_name = "Antennae"
+ should_generate_icons = TRUE
+
+/datum/preference/choiced/moth_antennae/init_possible_values()
+ var/list/values = list()
+
+ var/icon/moth_head = icon('icons/mob/human_parts.dmi', "moth_head_m")
+ moth_head.Blend(icon('icons/mob/human_face.dmi', "motheyes"), ICON_OVERLAY)
+
+ for (var/antennae_name in GLOB.sprite_accessories["moth_antennae"]) //SKYRAT EDIT
+ var/datum/sprite_accessory/antennae = GLOB.sprite_accessories["moth_antennae"][antennae_name] //SKYRAT EDIT
+
+ var/icon/icon_with_antennae = new(moth_head)
+ icon_with_antennae.Blend(icon('icons/mob/moth_antennae.dmi', "m_moth_antennae_[antennae.icon_state]_FRONT"), ICON_OVERLAY)
+ icon_with_antennae.Scale(64, 64)
+ icon_with_antennae.Crop(15, 64, 15 + 31, 64 - 31)
+
+ values[antennae.name] = icon_with_antennae
+
+ return values
+
+/datum/preference/choiced/moth_antennae/apply_to_human(mob/living/carbon/human/target, value)
+ target.dna.features["moth_antennae"] = value
+
+/datum/preference/choiced/moth_markings
+ savefile_key = "feature_moth_markings"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_FEATURES
+ main_feature_name = "Body markings"
+ should_generate_icons = TRUE
+ relevant_mutant_bodypart = "moth_markings"
+
+/datum/preference/choiced/moth_markings/init_possible_values()
+ var/list/values = list()
+
+ var/icon/moth_body = icon('icons/blanks/32x32.dmi', "nothing")
+
+ moth_body.Blend(icon('icons/mob/moth_wings.dmi', "m_moth_wings_plain_BEHIND"), ICON_OVERLAY)
+
+ var/list/body_parts = list(
+ BODY_ZONE_HEAD,
+ BODY_ZONE_CHEST,
+ BODY_ZONE_L_ARM,
+ BODY_ZONE_R_ARM,
+ )
+
+ for (var/body_part in body_parts)
+ var/gender = (body_part == "chest" || body_part == "head") ? "_m" : ""
+ moth_body.Blend(icon('icons/mob/human_parts.dmi', "moth_[body_part][gender]"), ICON_OVERLAY)
+
+ moth_body.Blend(icon('icons/mob/human_face.dmi', "motheyes"), ICON_OVERLAY)
+
+ for (var/markings_name in GLOB.sprite_accessories["moth_markings"]) //SKYRAT EDIT
+ var/datum/sprite_accessory/markings = GLOB.sprite_accessories["moth_markings"][markings_name] //SKYRAT EDIT
+ var/icon/icon_with_markings = new(moth_body)
+
+ if (markings_name != "None")
+ for (var/body_part in body_parts)
+ var/icon/body_part_icon = icon(markings.icon, "[markings.icon_state]_[body_part]")
+ body_part_icon.Crop(1, 1, 32, 32)
+ icon_with_markings.Blend(body_part_icon, ICON_OVERLAY)
+
+ icon_with_markings.Blend(icon('icons/mob/moth_wings.dmi', "m_moth_wings_plain_FRONT"), ICON_OVERLAY)
+ icon_with_markings.Blend(icon('icons/mob/moth_antennae.dmi', "m_moth_antennae_plain_FRONT"), ICON_OVERLAY)
+
+ // Zoom in on the top of the head and the chest
+ icon_with_markings.Scale(64, 64)
+ icon_with_markings.Crop(15, 64, 15 + 31, 64 - 31)
+
+ values[markings.name] = icon_with_markings
+
+ return values
+
+/datum/preference/choiced/moth_markings/apply_to_human(mob/living/carbon/human/target, value)
+ target.dna.features["moth_markings"] = value
+
+
+/datum/preference/choiced/moth_wings
+ savefile_key = "feature_moth_wings"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_FEATURES
+ main_feature_name = "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.sprite_accessories["moth_wings"]) //SKYRAT EDIT
+
+ // 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 00000000000..955e19ad6f1
--- /dev/null
+++ b/code/modules/client/preferences/species_features/mutants.dm
@@ -0,0 +1,22 @@
+/* SKYRAT EDIT REMOVAL
+/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)))
+ return FALSE
+
+ return TRUE
+*/
diff --git a/code/modules/client/preferences/tgui.dm b/code/modules/client/preferences/tgui.dm
new file mode 100644
index 00000000000..44a8666e642
--- /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 00000000000..c6492dfa60a
--- /dev/null
+++ b/code/modules/client/preferences/tgui_prefs_migration.dm
@@ -0,0 +1,116 @@
+/// Handle the migrations necessary from pre-tgui prefs to post-tgui prefs
+/datum/preferences/proc/migrate_preferences_to_tgui_prefs_menu()
+ migrate_antagonists()
+ 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_randomization()
+
+// 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
+
+// Before tgui preferences menu, "traitor" would handle both roundstart, midround, and latejoin.
+// These were split apart.
+/datum/preferences/proc/migrate_antagonists()
+ migrate_antagonist(ROLE_HERETIC, list(ROLE_HERETIC_SMUGGLER))
+ migrate_antagonist(ROLE_MALF, list(ROLE_MALF_MIDROUND))
+ migrate_antagonist(ROLE_OPERATIVE, list(ROLE_OPERATIVE_MIDROUND, ROLE_LONE_OPERATIVE))
+ migrate_antagonist(ROLE_REV_HEAD, list(ROLE_PROVOCATEUR))
+ migrate_antagonist(ROLE_TRAITOR, list(ROLE_SYNDICATE_INFILTRATOR, ROLE_SLEEPER_AGENT))
+ migrate_antagonist(ROLE_WIZARD, list(ROLE_WIZARD_MIDROUND))
+
+ // "Familes [sic] Antagonists" was the old name of the catch-all.
+ migrate_antagonist("Familes Antagonists", list(ROLE_FAMILIES, ROLE_FAMILY_HEAD_ASPIRANT))
+
+/datum/preferences/proc/migrate_antagonist(will_exist, list/to_add)
+ if (will_exist in be_special)
+ for (var/add in to_add)
+ be_special += add
+
+// Randomization used to be an assoc list of fields to TRUE.
+// Antagonist randomization was not even available to all options.
+// tgui prefs menu changes from list("random_socks" = TRUE, "random_name_antag" = TRUE)
+// to list("socks" = "enabled", "name" = "antag")
+// as well as removing anything that was set to FALSE, as this can be extrapolated.
+/datum/preferences/proc/migrate_randomization()
+ var/static/list/random_settings = list(
+ "random_age" = "age",
+ "random_backpack" = "backpack",
+ "random_eye_color" = "eye_color",
+ "random_facial_hair_color" = "facial_hair_color",
+ "random_facial_hairstyle" = "facial_hairstyle",
+ "random_gender" = "gender",
+ "random_hair_color" = "hair_color",
+ "random_hairstyle" = "hairstyle",
+ "random_jumpsuit_style" = "jumpsuit_style",
+ "random_skin_tone" = "skin_tone",
+ "random_socks" = "socks",
+ "random_species" = "species",
+ "random_undershirt" = "undershirt",
+ "random_underwear" = "underwear",
+ "random_underwear_color" = "underwear_color",
+ )
+
+ var/static/list/random_antag_settings = list(
+ "random_age_antag" = "age",
+ "random_gender_antag" = "gender",
+ "random_name_antag" = "name",
+ )
+
+ var/list/new_randomise = list()
+
+ for (var/old_setting in random_settings)
+ if (randomise[old_setting])
+ new_randomise[random_settings[old_setting]] = RANDOM_ENABLED
+
+ for (var/old_antag_setting in random_antag_settings)
+ if (randomise[old_antag_setting])
+ new_randomise[random_settings[old_antag_setting]] = RANDOM_ANTAG_ONLY
+
+ migrate_randomization_to_new_pref(
+ /datum/preference/choiced/random_body,
+ "random_body",
+ "random_body_antag",
+ )
+
+ migrate_randomization_to_new_pref(
+ /datum/preference/choiced/random_name,
+ "random_name",
+ "random_name_antag",
+ )
+
+ if (randomise["random_hardcore"])
+ write_preference(GLOB.preference_entries[/datum/preference/toggle/random_hardcore], TRUE)
+
+ randomise = new_randomise
+
+/datum/preferences/proc/migrate_randomization_to_new_pref(
+ preference_type,
+ key,
+ key_antag,
+)
+ if (randomise[key_antag])
+ write_preference(GLOB.preference_entries[preference_type], RANDOM_ANTAG_ONLY)
+ else if (randomise[key])
+ write_preference(GLOB.preference_entries[preference_type], RANDOM_ENABLED)
diff --git a/code/modules/client/preferences/tooltips.dm b/code/modules/client/preferences/tooltips.dm
new file mode 100644
index 00000000000..1d7f5587838
--- /dev/null
+++ b/code/modules/client/preferences/tooltips.dm
@@ -0,0 +1,15 @@
+/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
+
+/datum/preference/toggle/enable_tooltips
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "enable_tips"
+ savefile_identifier = PREFERENCE_PLAYER
diff --git a/code/modules/client/preferences/ui_style.dm b/code/modules/client/preferences/ui_style.dm
new file mode 100644
index 00000000000..08f1af6c7dd
--- /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/underwear_color.dm b/code/modules/client/preferences/underwear_color.dm
new file mode 100644
index 00000000000..cb0aa0c9bda
--- /dev/null
+++ b/code/modules/client/preferences/underwear_color.dm
@@ -0,0 +1,15 @@
+/datum/preference/color_legacy/underwear_color
+ savefile_key = "underwear_color"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES
+
+/datum/preference/color_legacy/underwear_color/apply_to_human(mob/living/carbon/human/target, value)
+ target.underwear_color = value
+
+/datum/preference/color_legacy/underwear_color/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/uplink_location.dm b/code/modules/client/preferences/uplink_location.dm
new file mode 100644
index 00000000000..8ae892bb402
--- /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/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/create_default_value()
+ return UPLINK_PDA
+
+/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 00000000000..1041a4f6f27
--- /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 00000000000..687315387c6
--- /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 00000000000..887816a0a0a
--- /dev/null
+++ b/code/modules/client/preferences_menu.dm
@@ -0,0 +1,26 @@
+/datum/verbs/menu/Preferences/verb/open_character_preferences()
+ set category = "OOC"
+ 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 = "OOC"
+ 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 a5d54dd3735..7fd69fa38a3 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -1,13 +1,11 @@
//This is the lowest supported version, anything below this is completely obsolete and the entire savefile will be wiped.
-//SKYRAT EDIT REMOVAL BEGIN - CUSTOMIZATION (moved to modular)
-/*
#define SAVEFILE_VERSION_MIN 32
//This is the current version, anything below this will attempt to update (if it's not obsolete)
// 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 40
+#define SAVEFILE_VERSION_MAX 41
/*
SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn
@@ -48,7 +46,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
toggles |= SOUND_ENDOFROUND
if(current_version < 34)
- auto_fit_viewport = TRUE
+ write_preference(/datum/preference/toggle/auto_fit_viewport, TRUE)
if(current_version < 35) //makes old keybinds compatible with #52040, sets the new default
var/newkey = FALSE
@@ -72,8 +70,8 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
key_bindings["ShiftQ"] = list("quick_equip_suit_storage")
if(current_version < 37)
- if(clientfps == 0)
- clientfps = -1
+ if(read_preference(/datum/preference/numeric/fps) == 0)
+ write_preference(GLOB.preference_entries[/datum/preference/numeric/fps], -1)
if (current_version < 38)
var/found_block_movement = FALSE
@@ -95,33 +93,38 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
if (current_version < 40)
LAZYADD(key_bindings["Space"], "hold_throw_mode")
-/datum/preferences/proc/update_character(current_version, savefile/S)
- return
+ if (current_version < 41)
+ migrate_preferences_to_tgui_prefs_menu()
+
+/datum/preferences/proc/update_character(current_version, savefile/savefile)
+ if (current_version < 41)
+ 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
@@ -130,13 +133,10 @@ 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"))
- LAZYADD(key_bindings["Unbound"], conflicted.name) // set it to unbound to prevent this from opening up again in the future
- save_preferences()
-
/datum/preferences/proc/load_path(ckey,filename="preferences.sav")
@@ -163,54 +163,18 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
fcopy(S, bacpath) //byond helpfully lets you use a savefile for the first arg.
return FALSE
- //general preferences
- READ_FILE(S["asaycolor"], asaycolor)
- READ_FILE(S["brief_outfit"], brief_outfit)
- READ_FILE(S["ooccolor"], ooccolor)
- READ_FILE(S["screentip_color"], screentip_color)
- 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["broadcast_login_logout"] , broadcast_login_logout)
+ apply_all_client_preferences()
+
+ //general preferences
+ READ_FILE(S["lastchangelog"], lastchangelog)
- 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["default_slot"], default_slot)
READ_FILE(S["chat_toggles"], chat_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["screentip_pref"], screentip_pref)
- READ_FILE(S["itemoutline_pref"], itemoutline_pref)
- 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["darkened_flash"], darkened_flash)
// OOC commendations
READ_FILE(S["hearted_until"], hearted_until)
@@ -228,7 +192,6 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
// Custom hotkeys
READ_FILE(S["key_bindings"], key_bindings)
- check_keybindings() // 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
//try to fix any outdated data if necessary
if(needs_update >= 0)
@@ -238,47 +201,16 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
fcopy(S, bacpath) //byond helpfully lets you use a savefile for the first arg.
update_preferences(needs_update, S) //needs_update = savefile_version if we need an update (positive integer)
-
+ check_keybindings() // 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
+ 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)))
- screentip_color = sanitize_ooccolor(sanitize_hexcolor(screentip_color, 6, 1, initial(screentip_color)))
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))
- broadcast_login_logout = sanitize_integer(broadcast_login_logout, FALSE, TRUE, initial(broadcast_login_logout))
- 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, (2**24)-1, initial(toggles))
- clientfps = sanitize_integer(clientfps, -1, 1000, 0)
- parallax = sanitize_integer(parallax, PARALLAX_INSANE, PARALLAX_DISABLE, null)
- ambientocclusion = sanitize_integer(ambientocclusion, FALSE, TRUE, initial(ambientocclusion))
- screentip_pref = sanitize_integer(screentip_pref, FALSE, TRUE, initial(screentip_pref))
- itemoutline_pref = sanitize_integer(itemoutline_pref, FALSE, TRUE, initial(itemoutline_pref))
- 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_float(pixel_size, PIXEL_SCALING_AUTO, PIXEL_SCALING_3X, 0.5, 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)
- brief_outfit = sanitize_inlist(brief_outfit, subtypesof(/datum/outfit), null)
- pda_style = sanitize_inlist(pda_style, GLOB.pda_styles, initial(pda_style))
- pda_color = sanitize_hexcolor(pda_color, 6, 1, initial(pda_color))
key_bindings = sanitize_keybindings(key_bindings)
favorite_outfits = SANITIZE_LIST(favorite_outfits)
- darkened_flash = sanitize_integer(darkened_flash, FALSE, TRUE, initial(darkened_flash))
if(needs_update >= 0) //save the updated version
var/old_default_slot = default_slot
@@ -310,61 +242,39 @@ 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["brief_outfit"], brief_outfit)
- WRITE_FILE(S["ooccolor"], ooccolor)
- WRITE_FILE(S["screentip_color"], screentip_color)
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["broadcast_login_logout"], broadcast_login_logout)
- 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["default_slot"], default_slot)
WRITE_FILE(S["toggles"], toggles)
WRITE_FILE(S["chat_toggles"], chat_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["screentip_pref"], screentip_pref)
- WRITE_FILE(S["itemoutline_pref"], itemoutline_pref)
- 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["key_bindings"], key_bindings)
WRITE_FILE(S["hearted_until"], (hearted_until > world.realtime ? hearted_until : null))
WRITE_FILE(S["favorite_outfits"], favorite_outfits)
- WRITE_FILE(S["darkened_flash"], darkened_flash)
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
@@ -381,75 +291,19 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
if(needs_update == -2) //fatal, can't load any data
return FALSE
- //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
+ // 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
+ value_cache -= preference_type
+ read_preference(preference_type)
//Character
- READ_FILE(S["real_name"], real_name)
- READ_FILE(S["gender"], gender)
- READ_FILE(S["body_type"], body_type)
- 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["hairstyle_name"], hairstyle)
- READ_FILE(S["facial_style_name"], facial_hairstyle)
- READ_FILE(S["underwear"], underwear)
- READ_FILE(S["underwear_color"], underwear_color)
- READ_FILE(S["undershirt"], undershirt)
- READ_FILE(S["socks"], socks)
- READ_FILE(S["backpack"], backpack)
- READ_FILE(S["jumpsuit_style"], jumpsuit_style)
- READ_FILE(S["uplink_loc"], uplink_spawn_loc)
- READ_FILE(S["playtime_reward_cloak"], playtime_reward_cloak)
- READ_FILE(S["phobia"], phobia)
READ_FILE(S["randomise"], randomise)
- READ_FILE(S["feature_mcolor"], features["mcolor"])
- READ_FILE(S["feature_ethcolor"], features["ethcolor"])
- 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_moth_antennae"], features["moth_antennae"])
- READ_FILE(S["feature_moth_markings"], features["moth_markings"])
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)
-
- // This is the version when the random security department was removed.
- // When the minimum is higher than that version, it's impossible for someone to have the "Random" department.
- #if SAVEFILE_VERSION_MIN > 40
- #warn The prefered_security_department check in preferences_savefile.dm is no longer necessary.
- #endif
-
- if (!(prefered_security_department in GLOB.security_depts_prefs))
- prefered_security_department = SEC_DEPT_NONE
-
- //Jobs
- READ_FILE(S["joblessrole"], joblessrole)
//Load prefs
READ_FILE(S["job_preferences"], job_preferences)
@@ -462,59 +316,12 @@ 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
- real_name = reject_bad_name(real_name)
- gender = sanitize_gender(gender)
- body_type = sanitize_gender(body_type, FALSE, FALSE, 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)]
-
randomise = SANITIZE_LIST(randomise)
- hairstyle = sanitize_inlist(hairstyle, GLOB.hairstyles_list)
- facial_hairstyle = sanitize_inlist(facial_hairstyle, GLOB.facial_hairstyles_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)
- underwear_color = sanitize_hexcolor(underwear_color, 3, 0)
- eye_color = sanitize_hexcolor(eye_color, 3, 0)
- skin_tone = sanitize_inlist(skin_tone, GLOB.skin_tones)
- backpack = sanitize_inlist(backpack, GLOB.backpacklist, initial(backpack))
- jumpsuit_style = sanitize_inlist(jumpsuit_style, GLOB.jumpsuitlist, initial(jumpsuit_style))
- uplink_spawn_loc = sanitize_inlist(uplink_spawn_loc, GLOB.uplink_spawn_loc_list_save, initial(uplink_spawn_loc))
- playtime_reward_cloak = sanitize_integer(playtime_reward_cloak)
- features["mcolor"] = sanitize_hexcolor(features["mcolor"], 3, 0)
- features["ethcolor"] = copytext_char(features["ethcolor"], 1, 7)
- features["tail_lizard"] = sanitize_inlist(features["tail_lizard"], GLOB.tails_list_lizard)
- 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["moth_antennae"] = sanitize_inlist(features["moth_antennae"], GLOB.moth_antennae_list, "Plain")
- features["moth_markings"] = sanitize_inlist(features["moth_markings"], GLOB.moth_markings_list, "None")
-
persistent_scars = sanitize_integer(persistent_scars)
- joblessrole = sanitize_integer(joblessrole, 1, 3, initial(joblessrole))
+ loadout_list = sanitize_loadout_list(update_loadout_list(loadout_list)) //SKYRAT EDIT ADDITION
+
//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)
@@ -523,9 +330,13 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
all_quirks = SANITIZE_LIST(all_quirks)
validate_quirks()
+ load_character_skyrat(S) //SKYRAT EDIT ADDITION
+
return TRUE
/datum/preferences/proc/save_character()
+ SHOULD_NOT_SLEEP(TRUE)
+
if(!path)
return FALSE
var/savefile/S = new /savefile(path)
@@ -533,71 +344,46 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
return FALSE
S.cd = "/character[default_slot]"
+ 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.)
+ // This is the version when the random security department was removed.
+ // When the minimum is higher than that version, it's impossible for someone to have the "Random" department.
+ #if SAVEFILE_VERSION_MIN > 40
+ #warn The prefered_security_department check in code/modules/client/preferences/security_department.dm is no longer necessary.
+ #endif
+
//Character
- WRITE_FILE(S["real_name"] , real_name)
- WRITE_FILE(S["gender"] , gender)
- WRITE_FILE(S["body_type"] , body_type)
- 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["hairstyle_name"] , hairstyle)
- WRITE_FILE(S["facial_style_name"] , facial_hairstyle)
- WRITE_FILE(S["underwear"] , underwear)
- WRITE_FILE(S["underwear_color"] , underwear_color)
- WRITE_FILE(S["undershirt"] , undershirt)
- WRITE_FILE(S["socks"] , socks)
- WRITE_FILE(S["backpack"] , backpack)
- WRITE_FILE(S["jumpsuit_style"] , jumpsuit_style)
- WRITE_FILE(S["uplink_loc"] , uplink_spawn_loc)
- WRITE_FILE(S["playtime_reward_cloak"] , playtime_reward_cloak)
WRITE_FILE(S["randomise"] , randomise)
- WRITE_FILE(S["species"] , pref_species.id)
- WRITE_FILE(S["phobia"], phobia)
- WRITE_FILE(S["feature_mcolor"] , features["mcolor"])
- WRITE_FILE(S["feature_ethcolor"] , features["ethcolor"])
- WRITE_FILE(S["feature_lizard_tail"] , features["tail_lizard"])
- 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_moth_antennae"] , features["moth_antennae"])
- WRITE_FILE(S["feature_moth_markings"] , features["moth_markings"])
WRITE_FILE(S["persistent_scars"] , persistent_scars)
- //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)
-
- //Jobs
- WRITE_FILE(S["joblessrole"] , joblessrole)
//Write prefs
WRITE_FILE(S["job_preferences"] , job_preferences)
//Quirks
WRITE_FILE(S["all_quirks"] , all_quirks)
+ save_character_skyrat(S) //SKYRAT EDIT ADDITION
+
return TRUE
/proc/sanitize_keybindings(value)
var/list/base_bindings = sanitize_islist(value,list())
- for(var/key in base_bindings)
- base_bindings[key] = base_bindings[key] & GLOB.keybindings_by_name
- if(!length(base_bindings[key]))
- base_bindings -= key
+ 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
@@ -616,5 +402,3 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
S.ImportText("/",file("[path].txt"))
#endif
-*/
-//SKYRAT EDIT REMOVAL END
diff --git a/code/modules/client/verbs/etips.dm b/code/modules/client/verbs/etips.dm
deleted file mode 100644
index 5c53deb9a3d..00000000000
--- 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 885e76ba951..837e8ed7325 100644
--- a/code/modules/client/verbs/ooc.dm
+++ b/code/modules/client/verbs/ooc.dm
@@ -58,7 +58,7 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8")
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] "
if(prefs.hearted)
var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat)
keyname = "[sheet.icon_tag("emoji-heart")][keyname]"
@@ -74,7 +74,8 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8")
if(holder)
if(!holder.fakekey || receiver.holder)
if(check_rights_for(src, R_ADMIN))
- to_chat(receiver, span_adminooc("[CONFIG_GET(flag/allow_admin_ooccolor) && prefs.ooccolor ? "" :"" ][span_prefix("OOC:")] [keyname][holder.fakekey ? "/([holder.fakekey])" : ""]: [msg] "), avoid_highlighting = avoid_highlight)
+ var/ooc_color = prefs.read_preference(/datum/preference/color/ooc_color)
+ to_chat(receiver, span_adminooc("[CONFIG_GET(flag/allow_admin_ooccolor) && ooc_color ? "" :"" ][span_prefix("OOC:")] [keyname][holder.fakekey ? "/([holder.fakekey])" : ""]: [msg] "), avoid_highlighting = avoid_highlight)
else
to_chat(receiver, span_adminobserverooc(span_prefix("OOC: [keyname][holder.fakekey ? "/([holder.fakekey])" : ""]: [msg]")), avoid_highlighting = avoid_highlight)
else
@@ -123,7 +124,7 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8")
message_admins("[usr.key] has attempted to use the Set Player OOC Color verb!")
log_admin("[key_name(usr)] tried to set player ooc color without authorization.")
return
- var/new_color = sanitize_ooccolor(newColor)
+ var/new_color = sanitize_color(newColor)
message_admins("[key_name_admin(usr)] has set the players' ooc color to [new_color].")
log_admin("[key_name_admin(usr)] has set the player ooc color to [new_color].")
GLOB.OOC_COLOR = new_color
@@ -145,36 +146,6 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8")
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(isnull(new_ooccolor))
- return
- new_ooccolor = sanitize_ooccolor(new_ooccolor)
- prefs.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!
-
-
-/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 28f0387cfe6..8990b795230 100644
--- a/code/modules/clothing/glasses/_glasses.dm
+++ b/code/modules/clothing/glasses/_glasses.dm
@@ -56,16 +56,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, span_notice("You will now see glasses colors."))
- else
- to_chat(H, span_notice("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 ..()
@@ -81,7 +83,7 @@
/mob/living/carbon/human/proc/update_glasses_color(obj/item/clothing/glasses/G, glasses_equipped)
- if(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/spacesuits/_spacesuits.dm b/code/modules/clothing/spacesuits/_spacesuits.dm
index 5d367019fcf..1bf346c87a4 100644
--- a/code/modules/clothing/spacesuits/_spacesuits.dm
+++ b/code/modules/clothing/spacesuits/_spacesuits.dm
@@ -102,7 +102,7 @@
// Clean up the cell on destroy
/obj/item/clothing/suit/space/Destroy()
- if(cell)
+ if(isatom(cell))
QDEL_NULL(cell)
var/mob/living/carbon/human/human = src.loc
if(istype(human))
diff --git a/code/modules/clothing/spacesuits/hardsuit.dm b/code/modules/clothing/spacesuits/hardsuit.dm
index e8c4109e2c2..5a49645f636 100644
--- a/code/modules/clothing/spacesuits/hardsuit.dm
+++ b/code/modules/clothing/spacesuits/hardsuit.dm
@@ -184,7 +184,7 @@
/obj/item/clothing/suit/space/hardsuit/dropped(mob/user)
..()
- if(jetpack)
+ if(isatom(jetpack))
for(var/X in jetpack.actions)
var/datum/action/A = X
A.Remove(user)
diff --git a/code/modules/clothing/suits/cloaks.dm b/code/modules/clothing/suits/cloaks.dm
index 81e05ff76be..8c0a051514b 100644
--- a/code/modules/clothing/suits/cloaks.dm
+++ b/code/modules/clothing/suits/cloaks.dm
@@ -241,4 +241,4 @@
icon_state = "playercloak"
/obj/item/clothing/neck/cloak/skill_reward/playing/check_wearable(mob/user)
- return user.client?.get_exp_living(TRUE) >= PLAYTIME_VETERAN
+ return user.client?.is_veteran()
diff --git a/code/modules/clothing/suits/toggles.dm b/code/modules/clothing/suits/toggles.dm
index bb79374a457..8d6810e9d19 100644
--- a/code/modules/clothing/suits/toggles.dm
+++ b/code/modules/clothing/suits/toggles.dm
@@ -144,7 +144,8 @@
helmet.suit = null
qdel(helmet)
helmet = null
- QDEL_NULL(jetpack)
+ if (isatom(jetpack))
+ QDEL_NULL(jetpack)
return ..()
/obj/item/clothing/head/helmet/space/hardsuit/Destroy()
diff --git a/code/modules/events/operative.dm b/code/modules/events/operative.dm
index cd02ff2dee9..79540ffb803 100644
--- a/code/modules/events/operative.dm
+++ b/code/modules/events/operative.dm
@@ -10,7 +10,7 @@
fakeable = FALSE
/datum/round_event/ghost_role/operative/spawn_role()
- var/list/candidates = get_candidates(ROLE_OPERATIVE, ROLE_OPERATIVE)
+ var/list/candidates = get_candidates(ROLE_OPERATIVE, ROLE_LONE_OPERATIVE)
if(!candidates.len)
return NOT_ENOUGH_PLAYERS
diff --git a/code/modules/flufftext/Hallucination.dm b/code/modules/flufftext/Hallucination.dm
index 81d3a80f752..07d9633779f 100644
--- a/code/modules/flufftext/Hallucination.dm
+++ b/code/modules/flufftext/Hallucination.dm
@@ -862,10 +862,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)
diff --git a/code/modules/hydroponics/hydroitemdefines.dm b/code/modules/hydroponics/hydroitemdefines.dm
index d370e7baa86..ea9398ae4d5 100644
--- a/code/modules/hydroponics/hydroitemdefines.dm
+++ b/code/modules/hydroponics/hydroitemdefines.dm
@@ -42,19 +42,19 @@
*/
/obj/item/plant_analyzer/proc/do_plant_stats_scan(atom/scan_target, mob/user)
if(istype(scan_target, /obj/machinery/hydroponics))
- to_chat(user, scan_tray_stats(scan_target))
+ to_chat(user, examine_block(scan_tray_stats(scan_target))) //SKYRAT EDIT CHANGE
return TRUE
if(istype(scan_target, /obj/structure/glowshroom))
var/obj/structure/glowshroom/shroom_plant = scan_target
- to_chat(user, scan_plant_stats(shroom_plant.myseed))
+ to_chat(user, examine_block(scan_plant_stats(shroom_plant.myseed))) //SKYRAT EDIT CHANGE
return TRUE
if(istype(scan_target, /obj/item/graft))
- to_chat(user, get_graft_text(scan_target))
+ to_chat(user, examine_block(get_graft_text(scan_target))) //SKYRAT EDIT CHANGE
return TRUE
if(isitem(scan_target))
var/obj/item/scanned_object = scan_target
if(scanned_object.get_plant_seed() || istype(scanned_object, /obj/item/seeds))
- to_chat(user, scan_plant_stats(scanned_object))
+ to_chat(user, examine_block(scan_plant_stats(scanned_object))) //SKYRAT EDIT CHANGE
return TRUE
if(isliving(scan_target))
var/mob/living/L = scan_target
@@ -75,19 +75,19 @@
*/
/obj/item/plant_analyzer/proc/do_plant_chem_scan(atom/scan_target, mob/user)
if(istype(scan_target, /obj/machinery/hydroponics))
- to_chat(user, scan_tray_chems(scan_target))
+ to_chat(user, examine_block(scan_tray_chems(scan_target))) //SKYRAT EDIT CHANGE
return TRUE
if(istype(scan_target, /obj/structure/glowshroom))
var/obj/structure/glowshroom/shroom_plant = scan_target
- to_chat(user, scan_plant_chems(shroom_plant.myseed))
+ to_chat(user, examine_block(scan_plant_chems(shroom_plant.myseed))) //SKYRAT EDIT CHANGE
return TRUE
if(istype(scan_target, /obj/item/graft))
- to_chat(user, get_graft_text(scan_target))
+ to_chat(user, examine_block(get_graft_text(scan_target))) //SKYRAT EDIT CHANGE
return TRUE
if(isitem(scan_target))
var/obj/item/scanned_object = scan_target
if(scanned_object.get_plant_seed() || istype(scanned_object, /obj/item/seeds))
- to_chat(user, scan_plant_chems(scanned_object))
+ to_chat(user, examine_block(scan_plant_chems(scanned_object))) //SKYRAT EDIT CHANGE
return TRUE
if(isliving(scan_target))
var/mob/living/L = scan_target
diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm
index 339715ec7a0..34a65c52513 100644
--- a/code/modules/jobs/job_types/_job.dm
+++ b/code/modules/jobs/job_types/_job.dm
@@ -102,10 +102,6 @@
/// String. If set to a non-empty one, it will be the key for the policy text value to show this role on spawn.
var/policy_index = ""
- //SKYRAT EDIT ADDITION
- ///Is this job veteran only? If so, then this job requires the player to be in the veteran_players.txt
- var/veteran_only = FALSE
-
/datum/job/New()
. = ..()
@@ -174,29 +170,25 @@
/mob/living/proc/on_job_equipping(datum/job/equipping)
return
-/mob/living/carbon/human/on_job_equipping(datum/job/equipping)
+/mob/living/carbon/human/on_job_equipping(datum/job/equipping, datum/preferences/used_pref) //SKYRAT EDIT CHANGE
var/datum/bank_account/bank_account = new(real_name, equipping, dna.species.payday_modifier)
bank_account.payday(STARTING_PAYCHECKS, TRUE)
account_id = bank_account.account_id
- dress_up_as_job(equipping)
+ dress_up_as_job(equipping, FALSE, used_pref) //SKYRAT EDIT CHANGE
/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)
+/mob/living/carbon/human/dress_up_as_job(datum/job/equipping, visual_only = FALSE, datum/preferences/used_pref) //SKYRAT EDIT CHANGE
dna.species.pre_equip_species_outfit(equipping, src, visual_only)
- equipOutfit(equipping.outfit, visual_only)
-
-
-/* SKYRAT EDIT MOVAL - MOVED TO ALTTITLEPREFS
+ equip_outfit_and_loadout(equipping.outfit, used_pref, visual_only, equipping) //SKYRAT EDIT CHANGE
/datum/job/proc/announce_head(mob/living/carbon/human/H, 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
SSticker.OnRoundstart(CALLBACK(GLOBAL_PROC, .proc/_addtimer, CALLBACK(pick(GLOB.announcement_systems), /obj/machinery/announcement_system/proc/announce, "NEWHEAD", H.real_name, H.job, channels), 1))
-*/
//If the configuration option is set to require players to be logged as old enough to play certain jobs, then this proc checks that they are, otherwise it just returns 1
/datum/job/proc/player_old_enough(client/C)
@@ -275,6 +267,11 @@
holder = "[uniform]"
uniform = text2path(holder)
+ var/client/client = GLOB.directory[ckey(H.mind?.key)]
+
+ if(client?.is_veteran() && client?.prefs.read_preference(/datum/preference/toggle/playtime_reward_cloak))
+ neck = /obj/item/clothing/neck/cloak/skill_reward/playing
+
/datum/outfit/job/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE)
if(visualsOnly)
return
@@ -289,9 +286,6 @@
C.registered_name = H.real_name
if(H.age)
C.registered_age = H.age
- if(H.alt_title_holder) // SKYRAT EDIT ADD -- ALT TITLES
- C.real_title = J.title
- C.assignment = H.alt_title_holder // SKYRAT EDIT ADD END
C.update_label()
C.update_icon()
var/datum/bank_account/B = SSeconomy.bank_accounts_by_id["[H.account_id]"]
@@ -304,13 +298,8 @@
if(istype(PDA))
PDA.owner = H.real_name
PDA.ownjob = J.title
- if(H.alt_title_holder) // SKYRAT EDIT ADD -- ALT TITLES
- PDA.ownjob = H.alt_title_holder // SKYRAT EDIT ADD END
PDA.update_label()
- if(H.client?.prefs.playtime_reward_cloak)
- neck = /obj/item/clothing/neck/cloak/skill_reward/playing
-
/datum/outfit/job/get_chameleon_disguise_info()
var/list/types = ..()
@@ -404,60 +393,62 @@
var/fully_randomize = GLOB.current_anonymous_theme || player_client.prefs.should_be_random_hardcore(job, player_client.mob.mind) || is_banned_from(player_client.ckey, "Appearance")
if(!player_client)
return // Disconnected while checking for the appearance ban.
+
+ var/require_human = CONFIG_GET(flag/enforce_human_authority) && (job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
+
if(fully_randomize)
- if(CONFIG_GET(flag/enforce_human_authority) && (job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND))
- if(player_client.prefs.pref_species.id != SPECIES_HUMAN) //SKYRAT EDIT CHANGE _ WARNING YOU MUST CHANGE THIS WHEN SPECIES DEFINES ARE COMPLETED!!!
- player_client.prefs.pref_species = new /datum/species/human
+ if(require_human)
player_client.prefs.randomise_appearance_prefs(~RANDOMIZE_SPECIES)
else
player_client.prefs.randomise_appearance_prefs()
+
player_client.prefs.apply_prefs_to(src)
+
+ if (require_human)
+ set_species(/datum/species/human)
+
if(GLOB.current_anonymous_theme)
fully_replace_character_name(null, GLOB.current_anonymous_theme.anonymous_name(src))
else
var/is_antag = (player_client.mob.mind in GLOB.pre_setup_antags)
- if(CONFIG_GET(flag/enforce_human_authority) && (job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND))
- player_client.prefs.randomise[RANDOM_SPECIES] = FALSE
- if(player_client.prefs.pref_species.id != SPECIES_HUMAN) //SKYRAT EDIT CHANGE _ WARNING YOU MUST CHANGE THIS WHEN SPECIES DEFINES ARE COMPLETED!!!
- player_client.prefs.pref_species = new /datum/species/human
+ if(require_human)
+ player_client.prefs.randomise["species"] = FALSE
player_client.prefs.safe_transfer_prefs_to(src, TRUE, is_antag)
+ if (require_human)
+ set_species(/datum/species/human)
if(CONFIG_GET(flag/force_random_names))
- player_client.prefs.real_name = player_client.prefs.pref_species.random_name(player_client.prefs.gender, TRUE)
- dna.update_dna_identity()
+ var/species_type = player_client.prefs.read_preference(/datum/preference/choiced/species)
+ var/datum/species/species = new species_type
- //SKYRAT EDIT ADD -- ALT TITLES
- if(player_client && player_client.prefs && player_client.prefs.alt_titles_preferences[job.title])
- alt_title_holder = player_client.prefs.alt_titles_preferences[job.title]
- //SKYRAT EDIT ADD END
+ var/gender = player_client.prefs.read_preference(/datum/preference/choiced/gender)
+ real_name = species.random_name(gender, TRUE)
+ dna.update_dna_identity()
/mob/living/silicon/ai/apply_prefs_job(client/player_client, datum/job/job)
if(GLOB.current_anonymous_theme)
fully_replace_character_name(real_name, GLOB.current_anonymous_theme.anonymous_ai_name(TRUE))
return
- apply_pref_name("ai", player_client) // This proc already checks if the player is appearance banned.
+ apply_pref_name(/datum/preference/name/ai, player_client) // This proc already checks if the player is appearance banned.
set_core_display_icon(null, player_client)
- //SKYRAT EDIT ADD -- ALT TITLES
- if(player_client && player_client.prefs && player_client.prefs.alt_titles_preferences[job.title])
- alt_title_holder = player_client.prefs.alt_titles_preferences[job.title]
- if(aiPDA && alt_title_holder)
- aiPDA.ownjob = alt_title_holder
- //SKYRAT EDIT ADD END
/mob/living/silicon/robot/apply_prefs_job(client/player_client, datum/job/job)
if(mmi)
var/organic_name
if(GLOB.current_anonymous_theme)
organic_name = GLOB.current_anonymous_theme.anonymous_name(src)
- else if(player_client.prefs.randomise[RANDOM_NAME] || CONFIG_GET(flag/force_random_names) || is_banned_from(player_client.ckey, "Appearance"))
+ else if(player_client.prefs.read_preference(/datum/preference/choiced/random_name) == RANDOM_ENABLED || CONFIG_GET(flag/force_random_names) || is_banned_from(player_client.ckey, "Appearance"))
if(!player_client)
return // Disconnected while checking the appearance ban.
- organic_name = player_client.prefs.pref_species.random_name(player_client.prefs.gender, TRUE)
+
+ var/species_type = player_client.prefs.read_preference(/datum/preference/choiced/species)
+ var/datum/species/species = new species_type
+ organic_name = species.random_name(player_client.prefs.read_preference(/datum/preference/choiced/gender), TRUE)
else
if(!player_client)
return // Disconnected while checking the appearance ban.
- organic_name = player_client.prefs.real_name
+ organic_name = player_client.prefs.read_preference(/datum/preference/name/real_name)
mmi.name = "[initial(mmi.name)]: [organic_name]"
if(mmi.brain)
@@ -466,15 +457,8 @@
mmi.brainmob.real_name = organic_name //the name of the brain inside the cyborg is the robotized human's name.
mmi.brainmob.name = organic_name
// If this checks fails, then the name will have been handled during initialization.
- if(!GLOB.current_anonymous_theme && player_client.prefs.custom_names["cyborg"] != DEFAULT_CYBORG_NAME)
- apply_pref_name("cyborg", player_client)
-
- //SKYRAT EDIT ADD -- ALT TITLES
- if(player_client && player_client.prefs && player_client.prefs.alt_titles_preferences[job.title])
- alt_title_holder = player_client.prefs.alt_titles_preferences[job.title]
- if(aiPDA && alt_title_holder)
- aiPDA.ownjob = alt_title_holder
- //SKYRAT EDIT ADD END
+ if(!GLOB.current_anonymous_theme && player_client.prefs.read_preference(/datum/preference/name/cyborg) != DEFAULT_CYBORG_NAME)
+ apply_pref_name(/datum/preference/name/cyborg, player_client)
/**
* Called after a successful roundstart spawn.
diff --git a/code/modules/jobs/job_types/assistant.dm b/code/modules/jobs/job_types/assistant.dm
index 057250b1b62..f9dfaae3d46 100644
--- a/code/modules/jobs/job_types/assistant.dm
+++ b/code/modules/jobs/job_types/assistant.dm
@@ -40,12 +40,29 @@ Assistant
/datum/outfit/job/assistant/pre_equip(mob/living/carbon/human/H)
..()
if (CONFIG_GET(flag/grey_assistants))
- if(H.jumpsuit_style == PREF_SUIT)
- uniform = /obj/item/clothing/under/color/grey
- else
- uniform = /obj/item/clothing/under/color/jumpskirt/grey
+ give_grey_suit(H)
else
if(H.jumpsuit_style == PREF_SUIT)
uniform = /obj/item/clothing/under/color/random
else
uniform = /obj/item/clothing/under/color/jumpskirt/random
+
+/datum/outfit/job/assistant/proc/give_grey_suit(mob/living/carbon/human/target)
+ if (target.jumpsuit_style == PREF_SUIT)
+ uniform = /obj/item/clothing/under/color/grey
+ else
+ uniform = /obj/item/clothing/under/color/jumpskirt/grey
+
+/datum/outfit/job/assistant/consistent
+ name = "Assistant - Consistent"
+
+/datum/outfit/job/assistant/consistent/pre_equip(mob/living/carbon/human/H)
+ ..()
+ give_grey_suit(H)
+
+/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()
diff --git a/code/modules/jobs/job_types/chaplain.dm b/code/modules/jobs/job_types/chaplain.dm
index bbe8cc83ddd..ef4053654d0 100644
--- a/code/modules/jobs/job_types/chaplain.dm
+++ b/code/modules/jobs/job_types/chaplain.dm
@@ -62,20 +62,12 @@
if(H.mind)
H.mind.holy_role = HOLY_ROLE_HIGHPRIEST
- var/new_religion = DEFAULT_RELIGION
- if(player_client?.prefs.custom_names["religion"])
- new_religion = player_client.prefs.custom_names["religion"]
-
- var/new_deity = DEFAULT_DEITY
- if(player_client?.prefs.custom_names["deity"])
- new_deity = player_client.prefs.custom_names["deity"]
+ var/new_religion = player_client?.prefs?.read_preference(/datum/preference/name/religion) || DEFAULT_RELIGION
+ var/new_deity = player_client?.prefs?.read_preference(/datum/preference/name/deity) || DEFAULT_DEITY
+ var/new_bible = player_client?.prefs?.read_preference(/datum/preference/name/bible) || DEFAULT_BIBLE
B.deity_name = new_deity
- var/new_bible = DEFAULT_BIBLE
- if(player_client?.prefs.custom_names["bible"])
- new_bible = player_client.prefs.custom_names["bible"]
-
switch(lowertext(new_religion))
if("homosexuality", "gay", "penis", "ass", "cock", "cocks")
new_bible = pick("Guys Gone Wild","Coming Out of The Closet","War of Cocks")
diff --git a/code/modules/jobs/job_types/clown.dm b/code/modules/jobs/job_types/clown.dm
index 0cd02db2ab1..4a60c1e87c7 100644
--- a/code/modules/jobs/job_types/clown.dm
+++ b/code/modules/jobs/job_types/clown.dm
@@ -40,7 +40,7 @@
. = ..()
if(!ishuman(spawned))
return
- spawned.apply_pref_name("clown", player_client)
+ spawned.apply_pref_name(/datum/preference/name/clown, player_client)
/datum/outfit/job/clown
diff --git a/code/modules/jobs/job_types/mime.dm b/code/modules/jobs/job_types/mime.dm
index 512499fd37f..3ead86904e9 100644
--- a/code/modules/jobs/job_types/mime.dm
+++ b/code/modules/jobs/job_types/mime.dm
@@ -38,7 +38,7 @@
. = ..()
if(!ishuman(spawned))
return
- spawned.apply_pref_name("mime", player_client)
+ spawned.apply_pref_name(/datum/preference/name/mime, player_client)
/datum/outfit/job/mime
diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm
index 47adce5771e..26b8f6987a8 100644
--- a/code/modules/jobs/job_types/security_officer.dm
+++ b/code/modules/jobs/job_types/security_officer.dm
@@ -67,7 +67,7 @@ GLOBAL_LIST_EMPTY(security_officer_distribution)
/// Returns the department this mob was assigned to, if any.
/datum/job/security_officer/proc/setup_department(mob/living/carbon/human/spawning, client/player_client)
- var/department = player_client?.prefs?.prefered_security_department
+ var/department = player_client?.prefs?.read_preference(/datum/preference/choiced/security_department)
if (!isnull(department))
department = get_my_department(spawning, department)
@@ -115,11 +115,6 @@ GLOBAL_LIST_EMPTY(security_officer_distribution)
var/obj/item/card/id/worn_id = spawning.wear_id
SSid_access.apply_trim_to_card(worn_id, dep_trim)
spawning.sec_hud_set_ID()
- //SKYRAT EDIT ADDITION -- ALT TITLES
- if(player_client && player_client.prefs && player_client.prefs.alt_titles_preferences[title])
- spawning.alt_title_holder = player_client.prefs.alt_titles_preferences[title]
- worn_id.assignment = "[spawning.alt_title_holder] ([department])"
- //SKYRAT EDIT END
var/spawn_point = pick(LAZYACCESS(GLOB.department_security_spawns, department))
diff --git a/code/modules/keybindings/bindings_client.dm b/code/modules/keybindings/bindings_client.dm
index 38164487d49..9c3a7e0ac80 100644
--- a/code/modules/keybindings/bindings_client.dm
+++ b/code/modules/keybindings/bindings_client.dm
@@ -37,7 +37,7 @@
return
//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
@@ -67,7 +67,7 @@
else
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)
@@ -99,7 +99,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.can_use(src) && kb.up(src))
break
diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm
index d020e9e030a..de07c952c72 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/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm
index 8a2584d8de2..5761c6dc992 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -1,4 +1,3 @@
-/* SKYRAT EDIT REMOVAL - MOVED TO MODULAR
/mob/dead/new_player
flags_1 = NONE
invisibility = INVISIBILITY_ABSTRACT
@@ -94,6 +93,7 @@
var/datum/poll_question/poll = locate(href_list["votepollref"]) in GLOB.polls
vote_on_poll_handler(poll, href_list)
+
//When you cop out of the round (NB: this HAS A SLEEP FOR PLAYER INPUT IN IT)
/mob/dead/new_player/proc/make_me_an_observer()
if(QDELETED(src) || !src.client)
@@ -126,7 +126,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_appearance()
@@ -150,6 +150,10 @@
return "Your account is not old enough for [jobtitle]."
if(JOB_UNAVAILABLE_SLOTFULL)
return "[jobtitle] is already filled to capacity."
+ //SKYRAT EDIT ADDITION
+ if(JOB_NOT_VETERAN)
+ return "You need to be veteran to join as [jobtitle]."
+ //SKYRAT EDIT END
return "Error: Unknown job availability."
/mob/dead/new_player/proc/IsJobUnavailable(rank, latejoin = FALSE)
@@ -175,8 +179,13 @@
return JOB_UNAVAILABLE_PLAYTIME
if(latejoin && !job.special_check_latejoin(client))
return JOB_UNAVAILABLE_GENERIC
+ //SKYRAT EDIT ADDITION
+ if(job.veteran_only && !is_veteran_player(client))
+ return JOB_NOT_VETERAN
+ //SKYRAT EDIT END
return JOB_AVAILABLE
+
/mob/dead/new_player/proc/AttemptLateSpawn(rank)
var/error = IsJobUnavailable(rank)
if(error != JOB_AVAILABLE)
@@ -249,8 +258,6 @@
if(GLOB.curse_of_madness_triggered)
give_madness(humanc, GLOB.curse_of_madness_triggered)
- SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CREWMEMBER_JOINED, humanc, rank)
-
GLOB.joined_player_list += character.ckey
if(CONFIG_GET(flag/allow_latejoin_antagonists) && humanc) //Borgs aren't allowed to be antags. Will need to be tweaked if we get true latejoin ais.
@@ -267,6 +274,14 @@
log_manifest(character.mind.key,character.mind,character,latejoin = TRUE)
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CREWMEMBER_JOINED, character, rank)
+
+ //SKYRAT EDIT ADDITION
+ if(humanc)
+ for(var/datum/loadout_item/item as anything in loadout_list_to_datums(humanc?.client?.prefs?.loadout_list))
+ item.post_equip_item(humanc.client?.prefs, humanc)
+ //SKYRAT EDIT END
+
/mob/dead/new_player/proc/AddEmploymentContract(mob/living/carbon/human/employee)
//TODO: figure out a way to exclude wizards/nukeops/demons from this.
for(var/C in GLOB.employmentCabinets)
@@ -280,6 +295,7 @@
if(SSlag_switch.measures[DISABLE_NON_OBSJOBS])
dat += "Only Observers may join at this time.
"
dat += "Round Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]
"
+ dat += "Alert Level: [capitalize(num2seclevel(SSsecurity_level.current_level))]
"
if(SSshuttle.emergency)
switch(SSshuttle.emergency.mode)
if(SHUTTLE_ESCAPE)
@@ -301,6 +317,7 @@
for(var/datum/job/job_datum as anything in department.department_jobs)
if(IsJobUnavailable(job_datum.title, TRUE) != JOB_AVAILABLE)
continue
+
var/command_bold = ""
if(job_datum.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
command_bold = " command"
@@ -308,6 +325,9 @@
dept_data += "[job_datum.title] ([job_datum.current_positions]) "
else
dept_data += "[job_datum.title] ([job_datum.current_positions]) "
+
+
+
if(!length(dept_data))
dept_data += "No positions open. "
dat += dept_data.Join()
@@ -332,6 +352,9 @@
var/mob/living/spawning_mob = mind.assigned_role.get_spawn_mob(client, destination)
if(QDELETED(src) || !client)
return // Disconnected while checking for the appearance ban.
+
+
+
if(!isAI(spawning_mob)) // Unfortunately there's still snowflake AI code out there.
mind.original_character_slot_index = client.prefs.default_slot
mind.transfer_to(spawning_mob) //won't transfer key since the mind is not active
@@ -364,6 +387,7 @@
if(!GLOB.crew_manifest_tgui)
GLOB.crew_manifest_tgui = new /datum/crew_manifest(src)
+
GLOB.crew_manifest_tgui.ui_interact(src)
/mob/dead/new_player/Move()
@@ -386,7 +410,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
@@ -429,4 +453,3 @@
// Add verb for re-opening the interview panel, and re-init the verbs for the stat panel
add_verb(src, /mob/dead/new_player/proc/open_interview)
-*/
diff --git a/code/modules/mob/dead/new_player/preferences_setup.dm b/code/modules/mob/dead/new_player/preferences_setup.dm
index 16790ef8671..059a1b187f5 100644
--- a/code/modules/mob/dead/new_player/preferences_setup.dm
+++ b/code/modules/mob/dead/new_player/preferences_setup.dm
@@ -1,98 +1,25 @@
-/* SKYRAT EDIT REMOVAL - MOVED TO MODULAR
/// Fully randomizes everything in the character.
-/datum/preferences/proc/randomise_appearance_prefs(randomise_flags = ALL)
- if(randomise_flags & RANDOMIZE_GENDER)
- gender = pick(MALE, FEMALE, PLURAL)
- switch(gender)
- if(MALE, FEMALE)
- body_type = gender
- else
- body_type = pick(MALE, FEMALE)
- if(randomise_flags & RANDOMIZE_SPECIES)
- var/rando_race = GLOB.species_list[pick(GLOB.roundstart_races)]
- pref_species = new rando_race()
- if(randomise_flags & RANDOMIZE_NAME)
- real_name = pref_species.random_name(gender, TRUE)
- if(randomise_flags & RANDOMIZE_AGE)
- age = rand(AGE_MIN, AGE_MAX)
- if(randomise_flags & RANDOMIZE_UNDERWEAR)
- underwear = random_underwear(gender)
- if(randomise_flags & RANDOMIZE_UNDERWEAR_COLOR)
- underwear_color = random_short_color()
- if(randomise_flags & RANDOMIZE_UNDERSHIRT)
- undershirt = random_undershirt(gender)
- if(randomise_flags & RANDOMIZE_SOCKS)
- socks = random_socks()
- if(randomise_flags & RANDOMIZE_BACKPACK)
- backpack = random_backpack()
- if(randomise_flags & RANDOMIZE_JUMPSUIT_STYLE)
- jumpsuit_style = pick(GLOB.jumpsuitlist)
- if(randomise_flags & RANDOMIZE_HAIRSTYLE)
- hairstyle = random_hairstyle(gender)
- if(randomise_flags & RANDOMIZE_FACIAL_HAIRSTYLE)
- facial_hairstyle = random_facial_hairstyle(gender)
- if(randomise_flags & RANDOMIZE_HAIR_COLOR)
- hair_color = random_short_color()
- if(randomise_flags & RANDOMIZE_FACIAL_HAIR_COLOR)
- facial_hair_color = random_short_color()
- if(randomise_flags & RANDOMIZE_SKIN_TONE)
- skin_tone = random_skin_tone()
- if(randomise_flags & RANDOMIZE_EYE_COLOR)
- eye_color = random_eye_color()
- if(randomise_flags & RANDOMIZE_FEATURES)
- features = random_features()
+/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
+ 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)
- if(!randomise[RANDOM_BODY] && !(antag_override && randomise[RANDOM_BODY_ANTAG]))
- return // Prefs say "no, thank you"
- if(randomise[RANDOM_GENDER] || antag_override && randomise[RANDOM_GENDER_ANTAG])
- gender = pick(MALE, FEMALE, PLURAL)
- switch(gender)
- if(MALE, FEMALE)
- body_type = gender
- else
- body_type = pick(MALE, FEMALE)
- if(randomise[RANDOM_SPECIES])
- random_species()
- else if(randomise[RANDOM_NAME] || antag_override && randomise[RANDOM_NAME_ANTAG])
- real_name = pref_species.random_name(gender, TRUE)
- if(randomise[RANDOM_AGE] || antag_override && randomise[RANDOM_AGE_ANTAG])
- age = rand(AGE_MIN, AGE_MAX)
- if(randomise[RANDOM_UNDERWEAR])
- underwear = random_underwear(gender)
- if(randomise[RANDOM_UNDERWEAR_COLOR])
- underwear_color = random_short_color()
- if(randomise[RANDOM_UNDERSHIRT])
- undershirt = random_undershirt(gender)
- if(randomise[RANDOM_SOCKS])
- socks = random_socks()
- if(randomise[RANDOM_BACKPACK])
- backpack = random_backpack()
- if(randomise[RANDOM_JUMPSUIT_STYLE])
- jumpsuit_style = pick(GLOB.jumpsuitlist)
- if(randomise[RANDOM_HAIRSTYLE])
- hairstyle = random_hairstyle(gender)
- if(randomise[RANDOM_FACIAL_HAIRSTYLE])
- facial_hairstyle = random_facial_hairstyle(gender)
- if(randomise[RANDOM_HAIR_COLOR])
- hair_color = random_short_color()
- if(randomise[RANDOM_FACIAL_HAIR_COLOR])
- facial_hair_color = random_short_color()
- if(randomise[RANDOM_SKIN_TONE])
- skin_tone = random_skin_tone()
- if(randomise[RANDOM_EYE_COLOR])
- eye_color = random_eye_color()
- features = random_features()
+ switch (read_preference(/datum/preference/choiced/random_body))
+ if (RANDOM_ANTAG_ONLY)
+ if (!antag_override)
+ return
+ if (RANDOM_DISABLED)
+ return
-/datum/preferences/proc/random_species()
- var/random_species_type = GLOB.species_list[pick(GLOB.roundstart_races)]
- pref_species = new random_species_type
- if(randomise[RANDOM_NAME])
- real_name = pref_species.random_name(gender,1)
-
+ 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))
///Setup the random hardcore quirks and give the character the new score prize.
/datum/preferences/proc/hardcore_random_setup(mob/living/carbon/human/character)
@@ -149,34 +76,37 @@
. += available_hardcore_quirks[picked_quirk]
available_hardcore_quirks -= picked_quirk
-
-/datum/preferences/proc/update_preview_icon()
- // Determine what job is marked as 'High' priority, and dress them up as such.
- var/datum/job/previewJob
+/// 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)
- // 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
+ 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()
+
+ switch(preview_pref)
+ if(PREVIEW_PREF_JOB)
+ if(preview_job) //SKYRAT EDIT CHANGE
+ // Silicons only need a very basic preview since there is no customization for them.
+ 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)
+ mannequin.job = preview_job.title
+ mannequin.equip_outfit_and_loadout(preview_job.outfit, src, TRUE)
+ if(PREVIEW_PREF_LOADOUT)
+ var/default_outfit = new /datum/outfit()
+ mannequin.equip_outfit_and_loadout(default_outfit, src, TRUE)
// Set up the dummy for its photoshoot
- var/mob/living/carbon/human/dummy/mannequin = generate_or_wait_for_human_dummy(DUMMY_HUMAN_SLOT_PREFERENCES)
apply_prefs_to(mannequin, TRUE)
- if(previewJob)
- mannequin.job = previewJob.title
- mannequin.dress_up_as_job(previewJob, 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 497be6db9c1..4befca223cf 100644
--- a/code/modules/mob/dead/observer/login.dm
+++ b/code/modules/mob/dead/observer/login.dm
@@ -3,16 +3,16 @@
if(!. || !client)
return FALSE
- 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(isAdminGhostAI(src))
has_unlimited_silicon_privilege = 1
if(client.prefs.unlock_content)
- preferred_form = client.prefs.ghost_form
- ghost_orbit = client.prefs.ghost_orbit
+ 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 32bf070f6c7..eb5449b3549 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -191,8 +191,8 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER)
. = ..()
if(client) //We update our preferences in case they changed right before update_appearance 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)
@@ -210,7 +210,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
@@ -375,7 +375,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.
if(mind.current.stat == DEAD && SSlag_switch.measures[DISABLE_DEAD_KEYLOOP])
to_chat(src, span_warning("To leave your body again use the Ghost verb."))
@@ -414,8 +414,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
@@ -601,7 +602,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
@@ -629,11 +630,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)
@@ -798,10 +799,11 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
set_ghost_appearance()
if(client?.prefs)
- deadchat_name = client.prefs.real_name
+ var/real_name = client.prefs.read_preference(/datum/preference/name/real_name)
+ deadchat_name = real_name
if(mind)
- mind.ghostname = client.prefs.real_name
- name = client.prefs.real_name
+ mind.ghostname = real_name
+ name = real_name
/mob/dead/observer/proc/set_ghost_appearance()
if(!client?.prefs)
@@ -809,12 +811,18 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
client.prefs.apply_character_randomization_prefs()
- if(HAIR in client.prefs.pref_species.species_traits)
- hairstyle = client.prefs.hairstyle
- hair_color = brighten_color(client.prefs.hair_color)
- if(FACEHAIR in client.prefs.pref_species.species_traits)
- facial_hairstyle = client.prefs.facial_hairstyle
- facial_hair_color = brighten_color(client.prefs.facial_hair_color)
+ var/species_type = client.prefs.read_preference(/datum/preference/choiced/species)
+ var/datum/species/species = new species_type
+
+ if(HAIR in species.species_traits)
+ hairstyle = 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_hairstyle = 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_appearance()
diff --git a/code/modules/mob/dead/observer/observer_say.dm b/code/modules/mob/dead/observer/observer_say.dm
index 32ca318c5b7..d70f122a1c5 100644
--- a/code/modules/mob/dead/observer/observer_say.dm
+++ b/code/modules/mob/dead/observer/observer_say.dm
@@ -46,7 +46,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/brain/brain.dm b/code/modules/mob/living/brain/brain.dm
index 8133052e3f6..37f1ec10bda 100644
--- a/code/modules/mob/living/brain/brain.dm
+++ b/code/modules/mob/living/brain/brain.dm
@@ -23,7 +23,7 @@
/mob/living/brain/proc/create_dna()
stored_dna = new /datum/dna/stored(src)
if(!stored_dna.species)
- var/rando_race = GLOB.species_list[pick(GLOB.roundstart_races)]
+ var/rando_race = pick(get_selectable_species())
stored_dna.species = new rando_race()
/mob/living/brain/Destroy()
diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm
index 4eb53b9742f..4a82310b5d0 100644
--- a/code/modules/mob/living/carbon/examine.dm
+++ b/code/modules/mob/living/carbon/examine.dm
@@ -6,7 +6,7 @@
var/t_has = p_have()
var/t_is = p_are()
- . = list("*---------*\nThis is [icon2html(src, user)] \a [src] !")
+ . = list("This is [icon2html(src, user)] \a [src] ! ")
var/obscured = check_obscured_slots()
if (handcuffed)
@@ -150,7 +150,7 @@
. += "[t_He] look[p_s()] very happy."
if(MOOD_LEVEL_HAPPY4 to INFINITY)
. += "[t_He] look[p_s()] ecstatic."
- . += "*---------* "
+ //. += "*---------* " SKYRAT EDIT REMOVAL
SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE, user, .)
diff --git a/code/modules/mob/living/carbon/human/dummy.dm b/code/modules/mob/living/carbon/human/dummy.dm
index fa1f5208906..8fa2bd93e9c 100644
--- a/code/modules/mob/living/carbon/human/dummy.dm
+++ b/code/modules/mob/living/carbon/human/dummy.dm
@@ -29,6 +29,26 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy)
randomize_human(src)
dna.initialize_dna(skip_index = TRUE) //Skip stuff that requires full round init.
+/// 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(src)
+ dna.initialize_dna(skip_index = TRUE)
+ dna.features["body_markings"] = "None"
+ dna.features["ears"] = "Cat"
+ dna.features["ethcolor"] = COLOR_WHITE
+ dna.features["frills"] = "None"
+ dna.features["horns"] = "None"
+ dna.features["mcolor"] = COLOR_VIBRANT_LIME
+ dna.features["moth_antennae"] = "Plain"
+ dna.features["moth_markings"] = "None"
+ dna.features["moth_wings"] = "Plain"
+ dna.features["snout"] = "Round"
+ dna.features["spines"] = "None"
+ dna.features["tail_human"] = "Cat"
+ dna.features["tail_lizard"] = "Smooth"
+
//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/examine.dm b/code/modules/mob/living/carbon/human/examine.dm
index 0975034908d..d7542f0669b 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -31,7 +31,7 @@
else
species_name_string = ", [prefix_a_or_an(dna.species.name)] [dna.species.name] !"
- . = list("*---------*\nThis is [!obscure_name ? name : "Unknown"] [species_name_string]")
+ . = list("This is [!obscure_name ? name : "Unknown"] [species_name_string] ") //SKYRAT EDIT CHANGE
if(species_visible) //If they have a custom species shown, show the real one too
if(dna.features["custom_species"])
. += "[t_He] [t_is] [prefix_a_or_an(dna.species.name)] [dna.species.name]!"
@@ -477,7 +477,12 @@
else
line = "[copytext_char(message, 1, 37)]... More... "
if(client)
- line += " \[OOC\] "
+ var/erp_pref = client.prefs.read_preference(/datum/preference/toggle/erp)
+ var/noncon_pref = client.prefs.read_preference(/datum/preference/toggle/erp/noncon)
+ if(erp_pref)
+ line += span_notice(" ERP: [erp_pref ? "YES" : "NO"]")
+ if(noncon_pref)
+ line += span_notice(" NON-CON: [noncon_pref ? "YES" : "NO"]")
if(line)
. += line
//Temporary flavor text addition:
@@ -486,33 +491,7 @@
. += "[temporary_flavor_text] "
else
. += "[copytext_char(temporary_flavor_text, 1, 37)]... More... "
-
- //SKYRAT EDIT ADDITION BEGIN - EXAMINE RECORDS
- var/datum/data/record/isinworld = find_record("name", perpname, GLOB.data_core.locked) //i dont know of a better way to do this-it doesnt work on off-station roles. please fix this someone better at coding.
- for(var/datum/antagonist/antag_datum in user?.mind?.antag_datums)
- if ((is_special_character(user)) && isinworld && (antag_datum.view_exploitables))
- . += "\[View exploitable info\] "
- //SKYRAT EDIT END
-
- //RP RECORDS FOR OBSERVERS
- if(client && user.client.holder && isobserver(user))
- var/line = ""
- if(!(client.prefs.general_record == ""))
- line += "\[GEN\] "
- if(!(client.prefs.security_record == ""))
- line += "\[SEC\] "
- if(!(client.prefs.medical_record == ""))
- line += "\[MED\] "
- if(!(client.prefs.background_info == ""))
- line += "\[BG\] "
- if(!(client.prefs.exploitable_info == ""))
- line += "\[EXP\] "
-
- if(!(line == ""))
- . += "*---------*"
- . += line
- //END OF SKYRAT EDIT
- . += "*---------* "
+ //. += "*---------* " SKYRAT EDIT REMOVAL
SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE, user, .)
/mob/living/proc/status_effect_examines(pronoun_replacement) //You can include this in any mob's examine() to show the examine texts of status effects!
@@ -536,7 +515,7 @@
switch(age)
if(-INFINITY to 17) // SKYRAT EDIT ADD START -- AGE EXAMINE
age_text = "too young to be here"
- if(18 to 25)
+ if(18 to 25)
age_text = "a young adult" // SKYRAT EDIT END
if(26 to 35)
age_text = "of adult age"
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index a983fa43522..6f6d0b5ec2b 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -726,10 +726,11 @@
/mob/living/carbon/human/check_self_for_injuries()
if(stat >= UNCONSCIOUS)
return
- var/list/combined_msg = list()
+ var/list/combined_msg = list("") //SKYRAT EDIT CHANGE
- visible_message(span_notice("[src] examines [p_them()]self."), \
- span_notice("You check yourself for injuries."))
+ visible_message(span_notice("[src] examines [p_them()]self.")) //SKYRAT EDIT CHANGE
+
+ combined_msg += span_boldnotice("You check yourself for injuries.
")//SKYRAT EDIT ADDITION
var/list/missing = list(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm
index 44e513c042a..407debc860d 100644
--- a/code/modules/mob/living/carbon/human/human_helpers.dm
+++ b/code/modules/mob/living/carbon/human/human_helpers.dm
@@ -241,50 +241,12 @@
/// Fully randomizes everything according to the given flags.
-/mob/living/carbon/human/proc/randomize_human_appearance(randomise_flags = ALL)
- if(randomise_flags & RANDOMIZE_GENDER)
- gender = pick(MALE, FEMALE, PLURAL)
- switch(gender)
- if(MALE, FEMALE)
- body_type = gender
- else
- body_type = pick(MALE, FEMALE)
- if(randomise_flags & RANDOMIZE_SPECIES)
- set_species(GLOB.species_list[pick(GLOB.roundstart_races)], FALSE)
- if(randomise_flags & RANDOMIZE_NAME)
- var/new_name = dna.species.random_name(gender, TRUE)
- name = new_name
- real_name = new_name
- if(randomise_flags & RANDOMIZE_AGE)
- age = rand(AGE_MIN, AGE_MAX)
- if(randomise_flags & RANDOMIZE_UNDERWEAR)
- underwear = random_underwear(gender)
- if(randomise_flags & RANDOMIZE_UNDERWEAR_COLOR)
- underwear_color = random_short_color()
- if(randomise_flags & RANDOMIZE_UNDERSHIRT)
- undershirt = random_undershirt(gender)
- if(randomise_flags & RANDOMIZE_SOCKS)
- socks = random_socks()
- if(randomise_flags & RANDOMIZE_BACKPACK)
- backpack = random_backpack()
- if(randomise_flags & RANDOMIZE_JUMPSUIT_STYLE)
- jumpsuit_style = pick(GLOB.jumpsuitlist)
- if(randomise_flags & RANDOMIZE_HAIRSTYLE)
- hairstyle = random_hairstyle(gender)
- if(randomise_flags & RANDOMIZE_FACIAL_HAIRSTYLE)
- facial_hairstyle = random_facial_hairstyle(gender)
- if(randomise_flags & RANDOMIZE_HAIR_COLOR)
- hair_color = random_short_color()
- if(randomise_flags & RANDOMIZE_FACIAL_HAIR_COLOR)
- facial_hair_color = random_short_color()
- if(randomise_flags & RANDOMIZE_SKIN_TONE)
- skin_tone = random_skin_tone()
- if(randomise_flags & RANDOMIZE_EYE_COLOR)
- eye_color = random_eye_color()
- var/obj/item/organ/eyes/organ_eyes = 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
- if(randomise_flags & RANDOMIZE_FEATURES)
- dna.features = random_features()
+/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/species.dm b/code/modules/mob/living/carbon/human/species.dm
index a8b3725d9ff..c746229d7a0 100644
--- a/code/modules/mob/living/carbon/human/species.dm
+++ b/code/modules/mob/living/carbon/human/species.dm
@@ -1,5 +1,8 @@
GLOBAL_LIST_EMPTY(roundstart_races)
+/// An assoc list of species types to their features (from get_features())
+GLOBAL_LIST_EMPTY(features_by_species)
+
/**
* # species datum
*
@@ -210,8 +213,6 @@ GLOBAL_LIST_EMPTY(roundstart_races)
///List of visual overlays created by handle_body()
var/list/body_vis_overlays = list()
-
-
///////////
// PROCS //
///////////
@@ -224,24 +225,34 @@ GLOBAL_LIST_EMPTY(roundstart_races)
wings_icons = string_list(wings_icons)
..()
+/// 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.
*/
-//SKYRAT EDIT REMOVAL BEGIN - MOVED - CUSTOMIZATION
-/*
/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)
- if(!GLOB.roundstart_races.len)
- GLOB.roundstart_races += "human"
-*/
-//SKYRAT EDIT REMOVAL END
+ 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 += SPECIES_HUMAN
+
+ return selectable_species
/**
* Checks if a species is eligible to be picked at roundstart.
@@ -414,7 +425,7 @@ GLOBAL_LIST_EMPTY(roundstart_races)
if(istype(I))
C.dropItemToGround(I)
else //Entries in the list should only ever be items or null, so if it's not an item, we can assume it's an empty hand
- C.put_in_hands(new mutanthands())
+ INVOKE_ASYNC(C, /mob/proc/put_in_hands, new mutanthands)
for(var/X in inherent_traits)
ADD_TRAIT(C, X, SPECIES_TRAIT)
@@ -2205,3 +2216,36 @@ GLOBAL_LIST_EMPTY(roundstart_races)
continue
current_part.change_bodypart(species_part)
+
+/* SKYRAT EDIT REMOVAL - MOVED TO MODULAR
+/// 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
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 9aed6253f94..82ae396f47c 100644
--- a/code/modules/mob/living/carbon/human/species_types/ethereal.dm
+++ b/code/modules/mob/living/carbon/human/species_types/ethereal.dm
@@ -139,3 +139,10 @@
emageffect = FALSE
spec_updatehealth(H)
H.visible_message(span_danger("[H] stops flickering and goes back to their normal state!"))
+
+/datum/species/ethereal/get_features()
+ var/list/features = ..()
+
+ features += "feature_ethcolor"
+
+ return features
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 e336b5ebdeb..3d6c1ce0787 100644
--- a/code/modules/mob/living/carbon/human/species_types/felinid.dm
+++ b/code/modules/mob/living/carbon/human/species_types/felinid.dm
@@ -128,3 +128,13 @@
new_ears.Insert(H, TRUE, FALSE)
if(!silent)
to_chat(H, span_boldnotice("You are no longer a cat."))
+
+/datum/species/human/felinid/prepare_human_for_preview(mob/living/carbon/human/human)
+ human.hairstyle = "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()
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 a12c2cfc9e1..cb7d84dc0d5 100644
--- a/code/modules/mob/living/carbon/human/species_types/humans.dm
+++ b/code/modules/mob/living/carbon/human/species_types/humans.dm
@@ -15,3 +15,8 @@
liked_food = JUNKFOOD | FRIED
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_MAGIC | MIRROR_PRIDE | ERT_SPAWN | RACE_SWAP | SLIME_EXTRACT
payday_modifier = 1
+
+/datum/species/human/prepare_human_for_preview(mob/living/carbon/human/human)
+ human.hairstyle = "Business Hair"
+ human.hair_color = "b96" // brown
+ human.update_hair()
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 b417131b5a6..a7c56c29aa6 100644
--- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
@@ -3,7 +3,7 @@
name = "Lizardperson"
id = SPECIES_LIZARD
say_mod = "hisses"
- default_color = "00FF00"
+ default_color = COLOR_VIBRANT_LIME
species_traits = list(MUTCOLORS, EYECOLOR, LIPS, HAS_FLESH, HAS_BONE)
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
@@ -16,7 +16,7 @@
/obj/item/organ/external/frills = "None",
/obj/item/organ/external/snout = "Round")
mutanttongue = /obj/item/organ/tongue/lizard
- //mutant_organs = list(/obj/item/organ/tail/lizard) //SKYRAT EDIT REMOVAL - CUSTOMIZATION
+ mutant_organs = list(/obj/item/organ/tail/lizard)
coldmod = 1.5
heatmod = 0.67
payday_modifier = 0.75
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 72ddd604477..0a58f00f5ec 100644
--- a/code/modules/mob/living/carbon/human/species_types/mothmen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/mothmen.dm
@@ -11,7 +11,7 @@
)
inherent_biotypes = MOB_ORGANIC|MOB_HUMANOID|MOB_BUG
mutant_bodyparts = list("moth_markings" = "None")
- //external_organs = list(/obj/item/organ/external/wings/moth = "Plain", /obj/item/organ/external/antennae = "Plain") SKYRAT EDIT REMOVAL
+ external_organs = list(/obj/item/organ/external/wings/moth = "Plain", /obj/item/organ/external/antennae = "Plain")
attack_verb = "slash"
attack_effect = ATTACK_EFFECT_CLAW
attack_sound = 'sound/weapons/slash.ogg'
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 1d887799241..82b01c9c9ff 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -458,7 +458,7 @@
// this forces any kind of flash (namely normal and static) to use a black screen for photosensitive players
// it absolutely isn't an ideal solution since sudden flashes to black can apparently still trigger epilepsy, but byond apparently doesn't let you freeze screens
// and this is apparently at least less likely to trigger issues than a full white/static flash
- if(client?.prefs?.darkened_flash)
+ if(client?.prefs?.read_preference(/datum/preference/toggle/darkened_flash))
type = /atom/movable/screen/fullscreen/flash/black
overlay_fullscreen("flash", type)
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index 2df81aa0d5f..4e2aa7c0b5b 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -274,7 +274,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
deaf_type = 2
// Create map text prior to modifying message for goonchat, sign lang edition
- if (client?.prefs.chat_on_map && !(stat == UNCONSCIOUS || stat == HARD_CRIT || is_blind(src)) && (client.prefs.see_chat_non_mob || ismob(speaker)))
+ if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && !(stat == UNCONSCIOUS || stat == HARD_CRIT || is_blind(src)) && (client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker)))
create_chat_message(speaker, message_language, raw_message, spans)
if(is_blind(src))
@@ -295,7 +295,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
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 || stat == HARD_CRIT) && (client.prefs.see_chat_non_mob || ismob(speaker)) && can_hear())
+ if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && !(stat == UNCONSCIOUS || stat == HARD_CRIT) && (client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker)) && can_hear())
create_chat_message(speaker, message_language, raw_message, spans)
// Recompose message for AI hrefs, language incomprehension.
@@ -360,7 +360,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
//speech bubble
var/list/speech_bubble_recipients = list()
for(var/mob/M in listening)
- if(M.client && (!M.client.prefs.chat_on_map || (SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES))))
+ if(M.client && (!M.client.prefs.read_preference(/datum/preference/toggle/enable_runechat) || (SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES))))
speech_bubble_recipients.Add(M.client)
var/image/I = image('icons/mob/talk.dmi', src, "[bubble_type][say_test(message)]", FLY_LAYER)
I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 2be0c419a9f..bb02dda68c1 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -141,7 +141,7 @@
create_eye()
if(client)
- INVOKE_ASYNC(src, .proc/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)
@@ -238,10 +238,10 @@
/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))
icon_state = initial(icon_state)
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_state = resolve_ai_icon(preferred_icon)
/mob/living/silicon/ai/verb/pick_icon()
@@ -788,7 +788,7 @@
var/rendered = "
[start][span_name("[hrefpart][namepart] ([jobpart]) ")][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 61a2336a370..d792ff82bb0 100644
--- a/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm
+++ b/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm
@@ -37,9 +37,9 @@
/datum/portrait_picker/ui_data(mob/user)
var/list/data = list()
- data["library"] = SSpersistence.paintings["library"] ? SSpersistence.paintings["library"] : 0
- data["library_secure"] = SSpersistence.paintings["library_secure"] ? SSpersistence.paintings["library_secure"] : 0
- data["library_private"] = SSpersistence.paintings["library_private"] ? SSpersistence.paintings["library_private"] : 0 //i'm gonna regret this, won't i?
+ data["library"] = SSpersistent_paintings.paintings["library"] ? SSpersistent_paintings.paintings["library"] : 0
+ data["library_secure"] = SSpersistent_paintings.paintings["library_secure"] ? SSpersistent_paintings.paintings["library_secure"] : 0
+ data["library_private"] = SSpersistent_paintings.paintings["library_private"] ? SSpersistent_paintings.paintings["library_private"] : 0 //i'm gonna regret this, won't i?
return data
/datum/portrait_picker/ui_act(action, params)
@@ -50,7 +50,7 @@
if("select")
var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private")
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/ai/examine.dm b/code/modules/mob/living/silicon/ai/examine.dm
index 3202372a897..43ff2cda679 100644
--- a/code/modules/mob/living/silicon/ai/examine.dm
+++ b/code/modules/mob/living/silicon/ai/examine.dm
@@ -1,5 +1,5 @@
/mob/living/silicon/ai/examine(mob/user)
- . = list("
*---------*\nThis is [icon2html(src, user)] [src] !")
+ . = list("This is [icon2html(src, user)] [src] ! ") //SKYRAT EDIT CHANGE
if (stat == DEAD)
. += span_deadsay("It appears to be powered-down.")
else
@@ -17,6 +17,6 @@
. += "The wireless networking light is blinking.\n"
else if (!shunted && !client)
. += "[src]Core.exe has stopped responding! NTOS is searching for a solution to the problem...\n"
- . += "*---------* "
+ //. += "*---------* " SKYRAT EDIT REMOVAL
. += ..()
diff --git a/code/modules/mob/living/silicon/examine.dm b/code/modules/mob/living/silicon/examine.dm
deleted file mode 100644
index 0b997dc5cca..00000000000
--- a/code/modules/mob/living/silicon/examine.dm
+++ /dev/null
@@ -1,19 +0,0 @@
-/mob/living/silicon/examine(mob/user) //Displays a silicon's laws to ghosts
- . = ..()
- if(laws && isobserver(user))
- . += "
[src] has the following laws: "
- for(var/law in laws.get_law_list(include_zeroth = TRUE))
- . += law
-
- //SKYRAT EDIT ADDITION BEGIN - CUSTOMIZATION
- if(client)
- var/line
- if(length(client.prefs.features["silicon_flavor_text"]))
- var/message = client.prefs.features["silicon_flavor_text"]
- if(length_char(message) <= 40)
- line = "
[message] "
- else
- line = "
[copytext_char(message, 1, 37)]... More... "
- line += "
\[OOC\] "
- . += line
- //SKYRAT EDIT ADDITION END
diff --git a/code/modules/mob/living/silicon/robot/examine.dm b/code/modules/mob/living/silicon/robot/examine.dm
index 8618e883c55..4c633d9b209 100644
--- a/code/modules/mob/living/silicon/robot/examine.dm
+++ b/code/modules/mob/living/silicon/robot/examine.dm
@@ -1,5 +1,5 @@
/mob/living/silicon/robot/examine(mob/user)
- . = list("
*---------*\nThis is [icon2html(src, user)] \a [src] !")
+ . = list("This is [icon2html(src, user)] \a [src] !") //SKYRAT EDIT CHANGE
if(desc)
. += "[desc]"
@@ -50,6 +50,6 @@
else
. += "[copytext_char(temporary_flavor_text, 1, 37)]... More... "
//SKYRAT EDIT ADDITION END
- . += "*---------* "
+ //. += "*---------* "
. += ..()
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index e0b967632c1..a237dbbd127 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -199,8 +199,8 @@
changed_name = GLOB.current_anonymous_theme.anonymous_ai_name(FALSE)
else if(custom_name)
changed_name = custom_name
- else if(pref_source && pref_source.prefs.custom_names["cyborg"] != DEFAULT_CYBORG_NAME)
- apply_pref_name("cyborg", pref_source)
+ else if(pref_source && pref_source.prefs.read_preference(/datum/preference/name/cyborg) != DEFAULT_CYBORG_NAME)
+ apply_pref_name(/datum/preference/name/cyborg, pref_source)
return //built in camera handled in proc
else
changed_name = get_standard_name()
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index a2c30affd8f..6d2780523f2 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -181,25 +181,6 @@
return
to_chat(usr, href_list["printlawtext"])
- //SKYRAT EDIT ADDITION BEGIN - CUSTOMIZATION
- if(href_list["lookup_info"])
- switch(href_list["lookup_info"])
- if("ooc_prefs")
- if(client)
- var/str = "[src]'s OOC Notes :
ERP : [client.prefs.erp_pref]
| Non-Con : [client.prefs.noncon_pref]
| Vore : [client.prefs.vore_pref]"
- str += "
[html_encode(client.prefs.ooc_prefs)]"
- var/datum/browser/popup = new(usr, "[name]'s ooc info", "[name]'s OOC Information", 500, 200)
- popup.set_content(text("
[] [] ", "[name]'s OOC information", replacetext(str, "\n", "
")))
- popup.open()
- return
-
- if("silicon_flavor_text")
- if(client && length(client.prefs.features["silicon_flavor_text"]))
- var/datum/browser/popup = new(usr, "[name]'s flavor text", "[name]'s Flavor Text", 500, 200)
- popup.set_content(text("
[] [] ", "[name]'s flavor text", replacetext(client.prefs.features["silicon_flavor_text"], "\n", "
")))
- popup.open()
- return
- //SKYRAT EDIT ADDITION END
return
/mob/living/silicon/proc/statelaws(force = 0)
diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm
index ac7cbeb93c9..d588b8ec1ae 100644
--- a/code/modules/mob/living/simple_animal/constructs.dm
+++ b/code/modules/mob/living/simple_animal/constructs.dm
@@ -90,13 +90,13 @@
text_span = "purple"
if(THEME_HOLY)
text_span = "blue"
- . = list("
*---------*\nThis is [icon2html(src, user)] \a [src] !\n[desc]")
+ . = list("This is [icon2html(src, user)] \a [src] !\n[desc] ") //SKYRAT EDIT CHANGE
if(health < maxHealth)
if(health >= maxHealth/2)
. += span_warning("[t_He] look[t_s] slightly dented.")
else
. += span_warning("[t_He] look[t_s] severely dented! ")
- . += "*---------* "
+ //. += "*---------* " SKYRAT EDIT REMOVAL
/mob/living/simple_animal/hostile/construct/attack_animal(mob/living/simple_animal/user, list/modifiers)
if(isconstruct(user)) //is it a construct?
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
index 0fcb4e8fae2..de4addedcaf 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
+++ b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
@@ -243,7 +243,7 @@
dust()
/mob/living/simple_animal/drone/examine(mob/user)
- . = list("
*---------*\nThis is [icon2html(src, user)] \a [src] !")
+ . = list("This is [icon2html(src, user)] \a [src] ! ") //SKYRAT EDIT CHANGE
//Hands
for(var/obj/item/I in held_items)
@@ -279,7 +279,7 @@
. += span_deadsay("A message repeatedly flashes on its display: \"REBOOT -- REQUIRED\".")
else
. += span_deadsay("A message repeatedly flashes on its display: \"ERROR -- OFFLINE\".")
- . += "*---------* "
+ //. += "*---------* " SKYRAT EDIT REMOVAL
/mob/living/simple_animal/drone/assess_threat(judgement_criteria, lasercolor = "", datum/callback/weaponcheck=null) //Secbots won't hunt maintenance drones.
diff --git a/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm b/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
index 70db007d955..92d246915bb 100644
--- a/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
+++ b/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
@@ -19,13 +19,13 @@
/mob/living/simple_animal/hostile/guardian/dextrous/examine(mob/user)
if(dextrous)
- . = list("
*---------*\nThis is [icon2html(src)] \a [src] !\n[desc]")
+ . = list("This is [icon2html(src)] \a [src] !\n[desc] ") //SKYRAT EDIT CHANGE
for(var/obj/item/I in held_items)
if(!(I.item_flags & ABSTRACT))
. += "It has [I.get_examine_string(user)] in its [get_held_index_name(get_held_index_of_item(I))]."
if(internal_storage && !(internal_storage.item_flags & ABSTRACT))
. += "It is holding [internal_storage.get_examine_string(user)] in its internal storage."
- . += "*---------* "
+ //. += "*---------* " SKYRAT EDIT REMOVAL
else
return ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/zombie.dm b/code/modules/mob/living/simple_animal/hostile/zombie.dm
index a0f39b4e3b6..8c71525e745 100644
--- a/code/modules/mob/living/simple_animal/hostile/zombie.dm
+++ b/code/modules/mob/living/simple_animal/hostile/zombie.dm
@@ -32,21 +32,21 @@
/* SKYRAT EDIT REMOVAL - MOVED TO MODULAR ZOMBIE.DM
/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.randomise[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/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)
+ COMPILE_OVERLAYS(dummy)
+ icon = getFlatIcon(dummy)
+ qdel(dummy)
- var/icon/P = get_flat_human_icon("zombie_[zombiejob]", J , dummy_prefs, "zombie", outfit_override = O)
- icon = P
corpse = new(src)
- corpse.outfit = O
+ corpse.outfit = outfit
corpse.mob_species = /datum/species/zombie
corpse.mob_name = name
*/
diff --git a/code/modules/mob/living/simple_animal/slime/slime.dm b/code/modules/mob/living/simple_animal/slime/slime.dm
index bb704d392de..0a5127a45b9 100644
--- a/code/modules/mob/living/simple_animal/slime/slime.dm
+++ b/code/modules/mob/living/simple_animal/slime/slime.dm
@@ -437,7 +437,7 @@
return
/mob/living/simple_animal/slime/examine(mob/user)
- . = list("
*---------*\nThis is [icon2html(src, user)] \a [src] !")
+ . = list("This is [icon2html(src, user)] \a [src] ! ") //SKYRAT EDIT CHANGE
if (stat == DEAD)
. += span_deadsay("It is limp and unresponsive.")
else
@@ -464,7 +464,7 @@
if(10)
. += span_warning("It is radiating with massive levels of electrical activity! ")
- . += "*---------* "
+ //. += "*---------* " SKYRAT EDIT REMOVAL
/mob/living/simple_animal/slime/proc/discipline_slime(mob/user)
if(stat)
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index 0a42c927795..605e20f57ae 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -39,7 +39,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
@@ -82,7 +82,7 @@
if(client.view_size)
client.view_size.resetToDefault() // Resets the client.view in case it was changed.
else
- 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)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index ac85a9b3cb3..823fa29a8f4 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -277,16 +277,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
@@ -481,7 +483,14 @@
else
result = examinify.examine(src) // if a tree is examined but no client is there to see it, did the tree ever really exist?
- to_chat(src, "
[result.Join("\n")] ")
+ //SKYRAT EDIT ADDITION
+ if(result.len)
+ for(var/i = 1, i <= result.len, i++)
+ if(!findtext(result[i], "
"))
+ result[i] += "\n"
+ //SKYRAT EDIT END
+
+ to_chat(src, "
[result.Join()]
") //SKYRAT EDIT CHANGE
SEND_SIGNAL(src, COMSIG_MOB_EXAMINATE, examinify)
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 992af96eb40..fd49e6bf252 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -351,8 +351,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/transform_procs.dm b/code/modules/mob/transform_procs.dm
index ee047c75173..50e43d3f977 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -109,7 +109,7 @@
. = new /mob/living/silicon/ai(pick(landmark_loc), null, src)
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/file_system/programs/card.dm b/code/modules/modular_computers/file_system/programs/card.dm
index b4757a12f03..5d65015567c 100644
--- a/code/modules/modular_computers/file_system/programs/card.dm
+++ b/code/modules/modular_computers/file_system/programs/card.dm
@@ -219,7 +219,6 @@
return TRUE
var/new_asignment = sanitize(params["assignment"])
target_id_card.assignment = new_asignment
- target_id_card.real_title = new_asignment // SKYRAT EDIT ADD -- ALT TITLES
playsound(computer, "terminal_type", 50, FALSE)
target_id_card.update_label()
return TRUE
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 12a2187550e..69d40a363f9 100644
--- a/code/modules/modular_computers/file_system/programs/portrait_printer.dm
+++ b/code/modules/modular_computers/file_system/programs/portrait_printer.dm
@@ -23,9 +23,9 @@
/datum/computer_file/program/portrait_printer/ui_data(mob/user)
var/list/data = list()
- data["library"] = SSpersistence.paintings["library"] ? SSpersistence.paintings["library"] : 0
- data["library_secure"] = SSpersistence.paintings["library_secure"] ? SSpersistence.paintings["library_secure"] : 0
- data["library_private"] = SSpersistence.paintings["library_private"] ? SSpersistence.paintings["library_private"] : 0 //i'm gonna regret this, won't i?
+ data["library"] = SSpersistent_paintings.paintings["library"] ? SSpersistent_paintings.paintings["library"] : 0
+ data["library_secure"] = SSpersistent_paintings.paintings["library_secure"] ? SSpersistent_paintings.paintings["library_secure"] : 0
+ data["library_private"] = SSpersistent_paintings.paintings["library_private"] ? SSpersistent_paintings.paintings["library_private"] : 0 //i'm gonna regret this, won't i?
return data
/datum/computer_file/program/portrait_printer/ui_assets(mob/user)
@@ -55,7 +55,7 @@
//canvas printing!
var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private")
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/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm
index 74d48e07644..0601eb9c112 100644
--- a/code/modules/reagents/chemistry/holder.dm
+++ b/code/modules/reagents/chemistry/holder.dm
@@ -5,22 +5,20 @@
#define REAGENT_TRANSFER_AMOUNT "amount"
#define REAGENT_PURITY "purity"
-/////////////These are used in the reagents subsystem init() and the reagent_id_typos.dm////////
-/proc/build_chemical_reagent_list()
- //Chemical Reagents - Initialises all /datum/reagent into a list indexed by reagent id
-
- if(GLOB.chemical_reagents_list)
- return
+/// Initialises all /datum/reagent into a list indexed by reagent id
+/proc/init_chemical_reagent_list()
+ var/list/reagent_list = list()
var/paths = subtypesof(/datum/reagent)
- GLOB.chemical_reagents_list = list()
for(var/path in paths)
if(path in GLOB.fake_reagent_blacklist)
continue
var/datum/reagent/D = new path()
D.mass = rand(10, 800) //This is terrible and should be removed ASAP!
- GLOB.chemical_reagents_list[path] = D
+ reagent_list[path] = D
+
+ return reagent_list
/proc/build_chemical_reactions_lists()
//Chemical Reactions - Initialises all /datum/chemical_reaction into a list
diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm
index d24f6fbaa0d..1effc539ed2 100644
--- a/code/modules/research/techweb/_techweb_node.dm
+++ b/code/modules/research/techweb/_techweb_node.dm
@@ -63,9 +63,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/spells/spell_types/rightandwrong.dm b/code/modules/spells/spell_types/rightandwrong.dm
index 960a1e7a305..ffd84018dac 100644
--- a/code/modules/spells/spell_types/rightandwrong.dm
+++ b/code/modules/spells/spell_types/rightandwrong.dm
@@ -63,9 +63,10 @@ GLOBAL_LIST_INIT(summoned_magic, list(
/obj/item/book/granter/spell/barnyard,
/obj/item/book/granter/spell/charge,
/obj/item/book/granter/spell/summonitem,
- /obj/item/gun/magic/wand/nothing, //SKYEDIT EDIT - Removes wand of polymorph
+ /obj/item/gun/magic/wand/nothing,
/obj/item/gun/magic/wand/death,
/obj/item/gun/magic/wand/resurrection,
+ /obj/item/gun/magic/wand/polymorph,
/obj/item/gun/magic/wand/teleport,
/obj/item/gun/magic/wand/door,
/obj/item/gun/magic/wand/fireball,
@@ -77,7 +78,8 @@ GLOBAL_LIST_INIT(summoned_magic, list(
/obj/item/immortality_talisman,
/obj/item/melee/ghost_sword))
-GLOBAL_LIST_INIT(summoned_special_magic, list( //SKYEDIT EDIT - Removes wand of polymorph
+GLOBAL_LIST_INIT(summoned_special_magic, list(
+ /obj/item/gun/magic/staff/change,
/obj/item/gun/magic/staff/animate,
/obj/item/storage/belt/wands/full,
/obj/item/antag_spawner/contract,
@@ -202,9 +204,10 @@ GLOBAL_LIST_INIT(summoned_magic_objectives, list(
UnregisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED)
///signal proc to give magic to new crewmembers
-/proc/magic_up_new_crew(mob/living/carbon/human/new_crewmember, rank)
+/datum/summon_magic_controller/proc/magic_up_new_crew(datum/source, mob/living/new_crewmember, rank)
SIGNAL_HANDLER
- INVOKE_ASYNC(GLOB.summon_magic, .proc/give_magic, new_crewmember)
+ if(ishuman(new_crewmember))
+ INVOKE_ASYNC(GLOB.summon_magic, .proc/give_magic, new_crewmember)
/**
* The guns controller handles the summon guns event.
@@ -230,7 +233,8 @@ GLOBAL_LIST_INIT(summoned_magic_objectives, list(
UnregisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED)
///signal proc to give guns to new crewmembers
-/proc/arm_up_new_crew(mob/living/carbon/human/new_crewmember, rank)
+/datum/summon_guns_controller/proc/arm_up_new_crew(datum/source, mob/living/new_crewmember, rank)
SIGNAL_HANDLER
- INVOKE_ASYNC(GLOB.summon_guns, .proc/give_guns, new_crewmember)
+ if(ishuman(new_crewmember))
+ INVOKE_ASYNC(GLOB.summon_guns, .proc/give_guns, new_crewmember)
diff --git a/code/modules/surgery/organs/external/_external_organs.dm b/code/modules/surgery/organs/external/_external_organs.dm
index 1a70abb69c0..ca70e5a18d8 100644
--- a/code/modules/surgery/organs/external/_external_organs.dm
+++ b/code/modules/surgery/organs/external/_external_organs.dm
@@ -15,8 +15,12 @@
///Convert the bitflag define into the actual layer define
var/static/list/all_layers = list(EXTERNAL_FRONT, EXTERNAL_ADJACENT, EXTERNAL_BEHIND)
- ///Defines what kind of 'organ' we're looking at. Sprites have names like 'm_firemoth_mothwings'. 'mothwings' would then be preference
- var/preference = ""
+ ///Defines what kind of 'organ' we're looking at. Sprites have names like 'm_firemoth_mothwings'. 'mothwings' would then be feature_key
+ var/feature_key = ""
+
+ /// The savefile_key of the preference this relates to. Used for the preferences UI.
+ var/preference
+
///Sprite datum we use to draw on the bodypart
var/datum/sprite_accessory/sprite_datum
///Key of the icon states of all the sprite_datums for easy caching
@@ -80,7 +84,7 @@
return
var/gender = (body_type == FEMALE) ? "f" : "m"
- var/finished_icon_state = (sprite_datum.gender_specific ? gender : "m") + "_" + preference + "_" + sprite_datum.icon_state + mutant_bodyparts_layertext(image_layer)
+ var/finished_icon_state = (sprite_datum.gender_specific ? gender : "m") + "_" + feature_key + "_" + sprite_datum.icon_state + mutant_bodyparts_layertext(image_layer)
var/mutable_appearance/appearance = mutable_appearance(sprite_datum.icon, finished_icon_state, layer = -image_layer)
appearance.dir = image_dir
@@ -93,14 +97,16 @@
overlay_list += appearance
/obj/item/organ/external/proc/set_sprite(sprite_name)
+ return
+ /* SKYRAT EDIT REMOVAL
sprite_datum = get_sprite_datum(sprite_name)
cache_key = generate_icon_cache()
+ */
///Generate a unique key based on our sprites. So that if we've aleady drawn these sprites, they can be found in the cache and wont have to be drawn again (blessing and curse)
/obj/item/organ/external/proc/generate_icon_cache()
- if(sprite_datum)
- return "[sprite_datum.icon_state]_[preference]"
- return
+ return ""
+ //return "[sprite_datum.icon_state]_[feature_key]" SKYRAT EDIT REMOVAL
/**This exists so sprite accessories can still be per-layer without having to include that layer's
* number in their sprite name, which causes issues when those numbers change.
@@ -153,7 +159,8 @@
slot = ORGAN_SLOT_EXTERNAL_HORNS
layers = EXTERNAL_ADJACENT
- preference = "horns"
+ feature_key = "horns"
+ preference = "feature_lizard_horns"
/obj/item/organ/external/horns/can_draw_on_bodypart(mob/living/carbon/human/human)
return TRUE
@@ -168,7 +175,8 @@
slot = ORGAN_SLOT_EXTERNAL_FRILLS
layers = EXTERNAL_ADJACENT
- preference = "frills"
+ feature_key = "frills"
+ preference = "feature_lizard_frills"
/obj/item/organ/external/frills/can_draw_on_bodypart(mob/living/carbon/human/human)
if(!(human.head?.flags_inv & HIDEEARS))
@@ -186,7 +194,8 @@
slot = ORGAN_SLOT_EXTERNAL_SNOUT
layers = EXTERNAL_ADJACENT
- preference = "snout"
+ feature_key = "snout"
+ preference = "feature_lizard_snout"
/obj/item/organ/external/snout/can_draw_on_bodypart(mob/living/carbon/human/human)
if(!(human.wear_mask?.flags_inv & HIDESNOUT) && !(human.head?.flags_inv & HIDESNOUT))
@@ -203,7 +212,8 @@
slot = ORGAN_SLOT_EXTERNAL_ANTENNAE
layers = EXTERNAL_FRONT | EXTERNAL_BEHIND
- preference = "moth_antennae"
+ feature_key = "moth_antennae"
+ preference = "feature_moth_antennae"
///Are we burned?
var/burnt = FALSE
diff --git a/code/modules/surgery/organs/external/wings.dm b/code/modules/surgery/organs/external/wings.dm
index e4f511e9093..a04a1e6a963 100644
--- a/code/modules/surgery/organs/external/wings.dm
+++ b/code/modules/surgery/organs/external/wings.dm
@@ -4,7 +4,7 @@
slot = ORGAN_SLOT_EXTERNAL_WINGS
layers = EXTERNAL_BEHIND | EXTERNAL_ADJACENT | EXTERNAL_FRONT
- preference = "wings"
+ feature_key = "wings"
/obj/item/organ/external/wings/can_draw_on_bodypart(mob/living/carbon/human/human)
if(!human.wear_suit)
@@ -21,9 +21,9 @@
var/datum/action/innate/flight/fly
///The preference type for opened wings
- var/wings_open_preference = "wingsopen"
+ var/wings_open_feature_key = "wingsopen"
///The preference type for closed wings
- var/wings_closed_preference = "wings"
+ var/wings_closed_feature_key = "wings"
///Are our wings open or closed?
var/wings_open = FALSE
@@ -118,7 +118,7 @@
///SPREAD OUR WINGS AND FLLLLLYYYYYY
/obj/item/organ/external/wings/functional/proc/open_wings()
- preference = wings_open_preference
+ feature_key = wings_open_feature_key
wings_open = TRUE
cache_key = generate_icon_cache() //we've changed preference to open, so we only need to update the key and ask for an update to change our sprite
@@ -126,7 +126,7 @@
///close our wings
/obj/item/organ/external/wings/functional/proc/close_wings()
- preference = wings_closed_preference
+ feature_key = wings_closed_feature_key
wings_open = FALSE
cache_key = generate_icon_cache()
@@ -155,7 +155,8 @@
///Moth wings! They can flutter in low-grav and burn off in heat
/obj/item/organ/external/wings/moth
- preference = "moth_wings"
+ feature_key = "moth_wings"
+ preference = "feature_moth_wings"
layers = EXTERNAL_BEHIND | EXTERNAL_FRONT
//dna_block = DNA_MOTH_WINGS_BLOCK SKYRAT EDIT REMOVAL
diff --git a/code/modules/surgery/organs/organ_internal.dm b/code/modules/surgery/organs/organ_internal.dm
index cf251c74d67..c463ff64104 100644
--- a/code/modules/surgery/organs/organ_internal.dm
+++ b/code/modules/surgery/organs/organ_internal.dm
@@ -34,6 +34,12 @@
var/failure_time = 0
+// Players can look at prefs before atoms SS init, and without this
+// they would not be able to see external organs, such as moth wings.
+// This is also necessary because assets SS is before atoms, and so
+// any nonhumans created in that time would experience the same effect.
+INITIALIZE_IMMEDIATE(/obj/item/organ)
+
/obj/item/organ/Initialize()
. = ..()
if(organ_flags & ORGAN_EDIBLE)
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index 27344e76712..a863028b3e8 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -50,7 +50,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
@@ -90,7 +90,7 @@
window.acquire_lock(src)
if(!window.is_ready())
window.initialize(
- fancy = user.client.prefs.tgui_fancy,
+ fancy = user.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy),
inline_assets = list(
get_asset_datum(/datum/asset/simple/tgui),
))
@@ -216,8 +216,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,
diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm
index 3b77894f679..11ab9bb803b 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/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index 969e32f06c9..f9ffdb18c84 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -75,7 +75,9 @@
#include "outfit_sanity.dm"
#include "pills.dm"
#include "plantgrowth_tests.dm"
+#include "preferences.dm"
#include "projectiles.dm"
+#include "quirks.dm"
#include "rcd.dm"
#include "reagent_id_typos.dm"
#include "reagent_mod_expose.dm"
diff --git a/code/modules/unit_tests/dynamic_ruleset_sanity.dm b/code/modules/unit_tests/dynamic_ruleset_sanity.dm
index 9fff5daf9b2..837e0b235cc 100644
--- a/code/modules/unit_tests/dynamic_ruleset_sanity.dm
+++ b/code/modules/unit_tests/dynamic_ruleset_sanity.dm
@@ -12,3 +12,25 @@
Fail("[ruleset] has a scaling_cost, but is also a lone/highlander ruleset.")
else if (!has_scaling_cost && !is_lone)
Fail("[ruleset] has no scaling cost, but is also not a lone/highlander ruleset.")
+
+/// Verifies that dynamic rulesets have unique antag_flag.
+/datum/unit_test/dynamic_unique_antag_flags
+
+/datum/unit_test/dynamic_unique_antag_flags/Run()
+ var/list/known_antag_flags = list()
+
+ for (var/datum/dynamic_ruleset/ruleset as anything in subtypesof(/datum/dynamic_ruleset))
+ if (isnull(initial(ruleset.antag_datum)))
+ continue
+
+ var/antag_flag = initial(ruleset.antag_flag)
+
+ if (isnull(antag_flag))
+ Fail("[ruleset] has a null antag_flag!")
+ continue
+
+ if (antag_flag in known_antag_flags)
+ Fail("[ruleset] has a non-unique antag_flag [antag_flag] (used by [known_antag_flags[antag_flag]])!")
+ continue
+
+ known_antag_flags[antag_flag] = ruleset
diff --git a/code/modules/unit_tests/preferences.dm b/code/modules/unit_tests/preferences.dm
new file mode 100644
index 00000000000..e3d6afbe533
--- /dev/null
+++ b/code/modules/unit_tests/preferences.dm
@@ -0,0 +1,51 @@
+/// Requires all preferences to implement required methods.
+/datum/unit_test/preferences_implement_everything
+
+/datum/unit_test/preferences_implement_everything/Run()
+ var/datum/preferences/preferences = new
+ var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)
+
+ for (var/preference_type in GLOB.preference_entries)
+ var/datum/preference/preference = GLOB.preference_entries[preference_type]
+ if (preference.savefile_identifier == PREFERENCE_CHARACTER)
+ preference.apply_to_human(human, preference.create_informed_default_value(preferences))
+
+ if (istype(preference, /datum/preference/choiced))
+ var/datum/preference/choiced/choiced_preference = preference
+ choiced_preference.init_possible_values()
+
+ // Smoke-test is_valid
+ preference.is_valid(TRUE)
+ preference.is_valid("string")
+ preference.is_valid(100)
+ preference.is_valid(list(1, 2, 3))
+
+/// Requires all preferences to have a valid, unique savefile_identifier.
+/datum/unit_test/preferences_valid_savefile_key
+
+/datum/unit_test/preferences_valid_savefile_key/Run()
+ var/list/known_savefile_keys = list()
+
+ for (var/preference_type in GLOB.preference_entries)
+ var/datum/preference/preference = GLOB.preference_entries[preference_type]
+ if (!istext(preference.savefile_key))
+ Fail("[preference_type] has an invalid savefile_key.")
+
+ if (preference.savefile_key in known_savefile_keys)
+ Fail("[preference_type] has a non-unique savefile_key `[preference.savefile_key]`!")
+
+ known_savefile_keys += preference.savefile_key
+
+/// Requires all main features have a main_feature_name
+/datum/unit_test/preferences_valid_main_feature_name
+
+/datum/unit_test/preferences_valid_main_feature_name/Run()
+ for (var/preference_type in GLOB.preference_entries)
+ var/datum/preference/choiced/preference = GLOB.preference_entries[preference_type]
+ if (!istype(preference))
+ continue
+
+ if (preference.category != PREFERENCE_CATEGORY_FEATURES && preference.category != PREFERENCE_CATEGORY_CLOTHING)
+ continue
+
+ TEST_ASSERT(!isnull(preference.main_feature_name), "Preference [preference_type] does not have a main_feature_name set!")
diff --git a/code/modules/unit_tests/quirks.dm b/code/modules/unit_tests/quirks.dm
new file mode 100644
index 00000000000..ad0ca261c8b
--- /dev/null
+++ b/code/modules/unit_tests/quirks.dm
@@ -0,0 +1,21 @@
+/// Ensure every quirk has a unique icon
+/datum/unit_test/quirk_icons
+
+/datum/unit_test/quirk_icons/Run()
+ var/list/used_icons = list()
+
+ for (var/datum/quirk/quirk_type as anything in subtypesof(/datum/quirk))
+ if (initial(quirk_type.abstract_parent_type) == quirk_type)
+ continue
+
+ var/icon = initial(quirk_type.icon)
+
+ if (isnull(icon))
+ Fail("[quirk_type] has no icon!")
+ continue
+
+ if (icon in used_icons)
+ Fail("[icon] used in both [quirk_type] and [used_icons[icon]]!")
+ continue
+
+ used_icons[icon] = quirk_type
diff --git a/code/modules/unit_tests/reagent_id_typos.dm b/code/modules/unit_tests/reagent_id_typos.dm
index 60da167f645..a0ced472927 100644
--- a/code/modules/unit_tests/reagent_id_typos.dm
+++ b/code/modules/unit_tests/reagent_id_typos.dm
@@ -4,7 +4,6 @@
/datum/unit_test/reagent_id_typos/Run()
build_chemical_reactions_lists()
- build_chemical_reagent_list()
for(var/I in GLOB.chemical_reactions_list_reactant_index)
for(var/V in GLOB.chemical_reactions_list_reactant_index[I])
diff --git a/code/modules/unit_tests/security_officer_distribution.dm b/code/modules/unit_tests/security_officer_distribution.dm
index c5906feea6a..42c13fbabf3 100644
--- a/code/modules/unit_tests/security_officer_distribution.dm
+++ b/code/modules/unit_tests/security_officer_distribution.dm
@@ -1,4 +1,10 @@
#define SECURITY_OFFICER_DEPARTMENTS list("a", "b", "c", "d")
+#define SECURITY_OFFICER_DEPARTMENTS_TO_NAMES (list( \
+ "a" = SEC_DEPT_ENGINEERING, \
+ "b" = SEC_DEPT_MEDICAL, \
+ "c" = SEC_DEPT_SCIENCE, \
+ "d" = SEC_DEPT_SUPPLY, \
+))
/// Test that security officers with specific distributions get their departments.
/datum/unit_test/security_officer_roundstart_distribution
@@ -45,17 +51,22 @@
SECURITY_OFFICER_DEPARTMENTS,
)
- TEST_ASSERT_EQUAL(outcome[REF(officer_a.new_character)], "a", "Officer A's department outcome was incorrect.")
- TEST_ASSERT_EQUAL(outcome[REF(officer_b.new_character)], "b", "Officer B's department outcome was incorrect.")
- TEST_ASSERT_EQUAL(outcome[REF(officer_c.new_character)], "b", "Officer C's department outcome was incorrect.")
- TEST_ASSERT_EQUAL(outcome[REF(officer_d.new_character)], "a", "Officer D's department outcome was incorrect.")
+ TEST_ASSERT_EQUAL(outcome[REF(officer_a.new_character)], SECURITY_OFFICER_DEPARTMENTS_TO_NAMES["a"], "Officer A's department outcome was incorrect.")
+ TEST_ASSERT_EQUAL(outcome[REF(officer_b.new_character)], SECURITY_OFFICER_DEPARTMENTS_TO_NAMES["b"], "Officer B's department outcome was incorrect.")
+ TEST_ASSERT_EQUAL(outcome[REF(officer_c.new_character)], SECURITY_OFFICER_DEPARTMENTS_TO_NAMES["b"], "Officer C's department outcome was incorrect.")
+ TEST_ASSERT_EQUAL(outcome[REF(officer_d.new_character)], SECURITY_OFFICER_DEPARTMENTS_TO_NAMES["a"], "Officer D's department outcome was incorrect.")
/datum/unit_test/security_officer_roundstart_distribution/proc/create_officer(preference)
var/mob/dead/new_player/new_player = allocate(/mob/dead/new_player)
var/datum/client_interface/mock_client = new
mock_client.prefs = new
- mock_client.prefs.prefered_security_department = preference
+ var/write_success = mock_client.prefs.write_preference(
+ GLOB.preference_entries[/datum/preference/choiced/security_department],
+ SECURITY_OFFICER_DEPARTMENTS_TO_NAMES[preference],
+ )
+
+ TEST_ASSERT(write_success, "Couldn't write department [SECURITY_OFFICER_DEPARTMENTS_TO_NAMES[preference]]")
var/mob/living/carbon/human/new_character = allocate(/mob/living/carbon/human)
new_character.mind_initialize()
diff --git a/config/game_options.txt b/config/game_options.txt
index 43c59a7e460..69a5c6a53cc 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -312,6 +312,7 @@ SILICON_MAX_LAW_AMOUNT 12
ROUNDSTART_RACES human
## Races that are strictly worse than humans that could probably be turned on without balance concerns
+ROUNDSTART_RACES felinid
ROUNDSTART_RACES lizard
ROUNDSTART_RACES felinid
#ROUNDSTART_RACES fly
@@ -367,9 +368,6 @@ ROUNDSTART_RACES ghoul
## Races defined here will not cause existing characters to be reset to human if they currently have a non-roundstart species defined.
#ROUNDSTART_NO_HARD_CHECK felinid
-## Uncomment to give players the choice of joining as a human with mutant bodyparts before they join the game
-#JOIN_WITH_MUTANT_HUMANS
-
##Overflow job. Default is assistant
OVERFLOW_JOB Assistant
diff --git a/config/skyrat/donators.txt b/config/skyrat/donators.txt
index d21c78ccf13..dd297dd3c7e 100644
--- a/config/skyrat/donators.txt
+++ b/config/skyrat/donators.txt
@@ -9,3 +9,4 @@
#That one penguin who became an admin
Azalis
Xyelgazel
+Gandalf2k15
diff --git a/config/skyrat/skyrat_config.txt b/config/skyrat/skyrat_config.txt
index 2ba6479850d..6b640f7c960 100644
--- a/config/skyrat/skyrat_config.txt
+++ b/config/skyrat/skyrat_config.txt
@@ -68,3 +68,6 @@ PC_MOB_TEXT As a player controlled mob you are expected to play the role to the
## Toggles off SSDecay when uncommented, HIGHLY recommended for map test-merges, to avoid giving it a bad first impression.
#SSDECAY_DISABLED
+
+## Disables any ERP preferences for codebases that don't want it.
+#DISABLE_ERP_PREFERENCES
\ No newline at end of file
diff --git a/icons/ui_icons/antags/obsessed.dmi b/icons/ui_icons/antags/obsessed.dmi
new file mode 100644
index 00000000000..219a6e59413
Binary files /dev/null and b/icons/ui_icons/antags/obsessed.dmi differ
diff --git a/interface/menu.dm b/interface/menu.dm
index 2923910713d..ec9719e8236 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)
diff --git a/modular_skyrat/master_files/code/__DEFINES/chat.dm b/modular_skyrat/master_files/code/__DEFINES/chat.dm
deleted file mode 100644
index 32dbda1b23b..00000000000
--- a/modular_skyrat/master_files/code/__DEFINES/chat.dm
+++ /dev/null
@@ -1,2 +0,0 @@
-
-#define MESSAGE_TYPE_MENTOR "mentor"
diff --git a/modular_skyrat/master_files/code/_onclick/hud/new_player.dm b/modular_skyrat/master_files/code/_onclick/hud/new_player.dm
index bc837eb6d55..1f5aa439a30 100644
--- a/modular_skyrat/master_files/code/_onclick/hud/new_player.dm
+++ b/modular_skyrat/master_files/code/_onclick/hud/new_player.dm
@@ -9,10 +9,12 @@
if(!.)
return
var/mob/dead/new_player/new_player = hud.mymob
- new_player.client.prefs.be_antag = !new_player.client.prefs.be_antag
- base_icon_state = "be_antag_[new_player.client.prefs.be_antag ? "on" : "off"]"
+ var/current_pref = new_player.client.prefs?.read_preference(/datum/preference/toggle/be_antag)
+ current_pref = !current_pref
+ new_player.client.prefs?.write_preference(/datum/preference/toggle/be_antag, current_pref)
+ base_icon_state = "be_antag_[current_pref ? "on" : "off"]"
update_appearance(UPDATE_ICON)
- to_chat(new_player, span_notice("You will now [new_player.client.prefs.be_antag ? "be considered" : "not be considered"] for any antagonist positions set in your preferences."))
+ to_chat(new_player, span_notice("You will now [current_pref ? "be considered" : "not be considered"] for any antagonist positions set in your preferences."))
/atom/movable/screen/lobby/button/antag_toggle/Initialize(mapload)
. = ..()
diff --git a/modular_skyrat/master_files/code/datums/traits/good.dm b/modular_skyrat/master_files/code/datums/traits/good.dm
index 101766ecd24..e6673c78242 100644
--- a/modular_skyrat/master_files/code/datums/traits/good.dm
+++ b/modular_skyrat/master_files/code/datums/traits/good.dm
@@ -8,6 +8,7 @@
gain_text = "
The ground doesn't feel so rough on your feet anymore. "
lose_text = "
You start feeling the ridges and imperfections on the ground. "
medical_record_text = "Patient's feet are more resilient against traction."
+ icon = "boot"
/datum/quirk/linguist
name = "Linguist"
@@ -17,6 +18,7 @@
gain_text = span_notice("Your brain seems more equipped to handle different modes of conversation.")
lose_text = span_danger("Your grasp of the finer points of Draconic idioms fades away.")
medical_record_text = "Patient demonstrates a high brain plasticity in regards to language learning."
+ icon = "globe"
//AdditionalEmotes *turf quirks
/datum/quirk/water_aspect
@@ -27,6 +29,7 @@
gain_text = "
You feel like you can control water. "
lose_text = "
Somehow, you've lost your ability to control water! "
medical_record_text = "Patient holds a collection of nanobots designed to synthesize H2O."
+ icon = "water"
/datum/quirk/webbing_aspect
name = "Webbing aspect (Emotes)"
@@ -36,6 +39,7 @@
gain_text = "
You could easily spin a web. "
lose_text = "
Somehow, you've lost your ability to weave. "
medical_record_text = "Patient has the ability to weave webs with naturally synthesized silk."
+ icon = "spider-web"
/datum/quirk/floral_aspect
name = "Floral aspect (Emotes)"
@@ -45,6 +49,7 @@
gain_text = "
You feel like you can grow vines. "
lose_text = "
Somehow, you've lost your ability to rapidly photosynthesize. "
medical_record_text = "Patient can rapidly photosynthesize to grow vines."
+ icon = "seedling"
/datum/quirk/ash_aspect
name = "Ash aspect (Emotes)"
@@ -54,6 +59,7 @@
gain_text = "
There is a forge smouldering inside of you. "
lose_text = "
Somehow, you've lost your ability to breathe fire. "
medical_record_text = "Patients possess a fire breathing gland commonly found in lizard folk."
+ icon = "fire"
/datum/quirk/sparkle_aspect
name = "Sparkle aspect (Emotes)"
@@ -63,3 +69,4 @@
gain_text = "
You're covered in sparkling dust! "
lose_text = "
Somehow, you've completely cleaned yourself of glitter.. "
medical_record_text = "Patient seems to be looking fabulous."
+ icon = "hand-sparkles"
diff --git a/modular_skyrat/master_files/code/datums/traits/negative.dm b/modular_skyrat/master_files/code/datums/traits/negative.dm
index 862e76011dc..266029ad993 100644
--- a/modular_skyrat/master_files/code/datums/traits/negative.dm
+++ b/modular_skyrat/master_files/code/datums/traits/negative.dm
@@ -6,6 +6,7 @@
value = -4
mob_trait = TRAIT_MOOD_NOEXAMINE
medical_record_text = "Patient is incapable of communicating their emotions."
+ icon = "meh"
/datum/quirk/sensitivesnout
name = "Sensitive Snout"
@@ -15,6 +16,7 @@
medical_record_text = "Patient's nose seems to have a cluster of nerves in the tip, would advise against direct contact."
value = -2
mob_trait = TRAIT_SENSITIVESNOUT
+ icon = "fingerprint"
/datum/quirk/monophobia
name = "Monophobia"
@@ -23,6 +25,7 @@
gain_text = "
You feel really lonely... "
lose_text = "
You feel like you could be safe on your own. "
medical_record_text = "Patient feels sick and distressed when not around other people, leading to potentially lethal levels of stress."
+ icon = "people-arrows"
/datum/quirk/monophobia/post_add()
. = ..()
@@ -42,3 +45,4 @@
medical_record_text = "Patient's digits are thick and lack the dexterity for operating some small devices, computers and non-adapted firearms."
value = -8
mob_trait = TRAIT_CHUNKYFINGERS
+ icon = "hand-middle-finger"
diff --git a/modular_skyrat/master_files/code/datums/traits/neutral.dm b/modular_skyrat/master_files/code/datums/traits/neutral.dm
index 695ec530221..d55c98ca88f 100644
--- a/modular_skyrat/master_files/code/datums/traits/neutral.dm
+++ b/modular_skyrat/master_files/code/datums/traits/neutral.dm
@@ -9,6 +9,7 @@
medical_record_text = "Patient seems to get excited easily."
value = 0
mob_trait = TRAIT_EXCITABLE
+ icon = "laugh-beam"
/datum/quirk/ironass
name = "Iron Ass"
@@ -18,6 +19,7 @@
medical_record_text = "Patient's ass seems incredibly solid."
value = 0
mob_trait = TRAIT_IRONASS
+ icon = "hand-paper"
/datum/quirk/dnc
name = "Do Not Clone"
@@ -27,6 +29,7 @@
medical_record_text = "Patient's anatomy is incompatible with conventional cloning techniques."
value = 0
mob_trait = TRAIT_DNC
+ icon = "users-slash"
/datum/quirk/dnr
name = "Do Not Revive"
@@ -36,6 +39,7 @@
medical_record_text = "Patient is a DNR, and cannot be revived in any way."
value = 0
mob_trait = TRAIT_DNR
+ icon = "skull-crossbones"
//uncontrollable laughter
/datum/quirk/item_quirk/joker
@@ -45,6 +49,7 @@
medical_record_text = "Patient suffers with sudden and uncontrollable bursts of laughter."
var/pcooldown = 0
var/pcooldown_time = 60 SECONDS
+ icon = "grin-squint-tears"
/datum/quirk/item_quirk/joker/add_unique()
give_item_to_holder(/obj/item/paper/joker, list(LOCATION_BACKPACK = ITEM_SLOT_BACKPACK, LOCATION_HANDS = ITEM_SLOT_HANDS))
@@ -123,3 +128,4 @@
medical_record_text = "Patient seems to possess behavior much like a feline."
value = 0
mob_trait = TRAIT_FELINE
+ icon = "cat"
diff --git a/modular_skyrat/master_files/code/modules/client/preferences.dm b/modular_skyrat/master_files/code/modules/client/preferences.dm
new file mode 100644
index 00000000000..2dc138c1462
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences.dm
@@ -0,0 +1,1127 @@
+#define CHAR_TAB_APPEARANCE 1
+#define CHAR_TAB_MARKINGS 2
+#define CHAR_TAB_BACKGROUND 3
+#define CHAR_TAB_AUGMENTS 4
+
+#define APPEARANCE_CATEGORY_COLUMN "
"
+#define MAX_MUTANT_ROWS 4
+
+/datum/preferences
+ /// Loadout prefs. Assoc list of [typepaths] to [associated list of item info].
+ var/list/loadout_list
+ /// Associative list, keyed by language typepath, pointing to LANGUAGE_UNDERSTOOD, or LANGUAGE_SPOKEN, for whether we understand or speak the language
+ var/list/languages = list()
+ /// List of chosen augmentations. It's an associative list with key name of the slot, pointing to a typepath of an augment define
+ var/augments = list()
+ /// List of chosen preferred styles for limb replacements
+ var/augment_limb_styles = list()
+ /// Which augment slot we currently have chosen, this is for UI display
+ var/chosen_augment_slot
+ /// Has to include all information that extra organs from mutant bodyparts would need. (so far only genitals now)
+ var/list/features = MANDATORY_FEATURE_LIST
+ /// A list containing all of our mutant bodparts
+ var/list/list/mutant_bodyparts = list()
+ /// A list of all bodymarkings
+ var/list/list/body_markings = list()
+ /// Current tab of html advanced prefs
+ var/character_settings_tab = CHAR_TAB_APPEARANCE
+
+ /// Will the person see accessories not meant for their species to choose from
+ var/mismatched_customization = FALSE
+
+ /// Allows the user to freely color his body markings and mutant parts.
+ var/allow_advanced_colors = FALSE
+
+ /// Preference of how the preview should show the character.
+ var/preview_pref = PREVIEW_PREF_JOB
+
+ var/needs_update = TRUE
+
+ var/arousal_preview = AROUSAL_NONE
+
+ var/datum/species/pref_species
+
+ /// Chosen cultural informations
+ var/pref_culture = /datum/cultural_info/culture/generic
+ var/pref_location = /datum/cultural_info/location/generic
+ var/pref_faction = /datum/cultural_info/faction/none
+ /// Whether someone wishes to see more information regarding either of those
+ var/culture_more_info = FALSE
+ var/location_more_info = FALSE
+ var/faction_more_info = FALSE
+
+ //BACKGROUND STUFF
+ var/general_record = ""
+ var/security_record = ""
+ var/medical_record = ""
+
+ var/background_info = ""
+ var/exploitable_info = ""
+
+ ///Whether the user wants to see body size being shown in the preview
+ var/show_body_size = FALSE
+
+/datum/preferences/proc/species_updated(species_type)
+ if(!update_pref_species(species_type))
+ return
+ var/list/new_features = pref_species.get_random_features() //We do this to keep flavor text, genital sizes etc.
+ for(var/key in new_features)
+ features[key] = new_features[key]
+ mutant_bodyparts = pref_species.get_random_mutant_bodyparts(features)
+ body_markings = pref_species.get_random_body_markings(features)
+ if(pref_species.use_skintones)
+ features["uses_skintones"] = TRUE
+ //We reset the quirk-based stuff
+ augments = list()
+ all_quirks = list()
+ //Reset cultural stuff
+ pref_culture = pref_species.cultures[1]
+ pref_location = pref_species.locations[1]
+ pref_faction = pref_species.factions[1]
+ try_get_common_language()
+ validate_languages()
+ save_character()
+
+/datum/preferences/proc/update_pref_species(passed_type)
+ var/species_type = passed_type || read_preference(/datum/preference/choiced/species)
+ if(pref_species)
+ if(pref_species.type == species_type)
+ return FALSE
+ QDEL_NULL(pref_species)
+ pref_species = new species_type()
+ return TRUE
+
+/datum/preferences/proc/SkyratTopic(href, list/href_list, mob/user)
+ if(update_pref_species())
+ show_advanced_prefs(user)
+ return
+ if(href_list["task"])
+ switch(href_list["task"])
+ if("augment_style")
+ needs_update = TRUE
+ var/slot_name = href_list["slot"]
+ var/new_style = input(user, "Choose your character's [slot_name] augmentation style:", "Character Preference") as null|anything in GLOB.robotic_styles_list
+ if(new_style)
+ if(new_style == "None")
+ if(augment_limb_styles[slot_name])
+ augment_limb_styles -= slot_name
+ else
+ augment_limb_styles[slot_name] = new_style
+ if("set_augment")
+ if(pref_species.can_augment)
+ needs_update = TRUE
+ var/typed_path = text2path(href_list["type"])
+ var/datum/augment_item/target_aug = GLOB.augment_items[typed_path]
+ var/datum/augment_item/current
+ if(augments[target_aug.slot])
+ current = GLOB.augment_items[augments[target_aug.slot]]
+ if(current == target_aug)
+ augments -= target_aug.slot
+ else if(CanBuyAugment(target_aug, current))
+ augments[target_aug.slot] = typed_path
+ if("augment_slot")
+ var/slot_name = href_list["slot"]
+ chosen_augment_slot = slot_name
+
+ if("close_language")
+ user << browse(null, "window=culture_lang")
+ show_advanced_prefs(user)
+
+ if("cultural")
+ switch(href_list["preference"])
+ if("general_record")
+ var/msg = input(usr, "Set your general record. This is more or less public information, available from security, medical and command consoles", "General Record", general_record) as message|null
+ if(!isnull(msg))
+ general_record = STRIP_HTML_SIMPLE(msg, MAX_FLAVOR_LEN)
+
+ if("medical_record")
+ var/msg = input(usr, "Set your medical record. ", "Medical Record", medical_record) as message|null
+ if(!isnull(msg))
+ medical_record = STRIP_HTML_SIMPLE(msg, MAX_FLAVOR_LEN)
+
+ if("security_record")
+ var/msg = input(usr, "Set your security record. ", "Medical Record", security_record) as message|null
+ if(!isnull(msg))
+ security_record = STRIP_HTML_SIMPLE(msg, MAX_FLAVOR_LEN)
+
+ if("background_info")
+ var/msg = input(usr, "Set your background information. (Where you come from, which culture were you raised in and why you are working here etc.)", "Background Info", background_info) as message|null
+ if(!isnull(msg))
+ background_info = STRIP_HTML_SIMPLE(msg, MAX_FLAVOR_LEN)
+
+ if("exploitable_info")
+ var/msg = input(usr, "Set your exploitable information. This is sensitive informations that antagonists may get to see, recommended for better roleplay experience", "Exploitable Info", exploitable_info) as message|null
+ if(!isnull(msg))
+ exploitable_info = STRIP_HTML_SIMPLE(msg, MAX_FLAVOR_LEN)
+
+ if("cultural_info_change")
+ var/thing = href_list["info"]
+ var/list/choice_list = list()
+ var/list/iteration_list
+ var/list/siphon_list
+ switch(thing)
+ if(CULTURE_CULTURE)
+ iteration_list = pref_species.cultures
+ siphon_list = GLOB.culture_cultures
+ if(CULTURE_FACTION)
+ iteration_list = pref_species.factions
+ siphon_list = GLOB.culture_factions
+ if(CULTURE_LOCATION)
+ iteration_list = pref_species.locations
+ siphon_list = GLOB.culture_locations
+ for(var/cultural_entity in iteration_list)
+ var/datum/cultural_info/CINFO = siphon_list[cultural_entity]
+ choice_list[CINFO.name] = cultural_entity
+ var/new_cultural_thing = input(user, "Choose your character's [thing]:", "Character Preference") as null|anything in choice_list
+ if(new_cultural_thing)
+ switch(thing)
+ if(CULTURE_CULTURE)
+ pref_culture = choice_list[new_cultural_thing]
+ if(CULTURE_FACTION)
+ pref_faction = choice_list[new_cultural_thing]
+ if(CULTURE_LOCATION)
+ pref_location = choice_list[new_cultural_thing]
+ validate_languages()
+
+ if("cultural_info_toggle")
+ var/thing = href_list["info"]
+ switch(thing)
+ if(CULTURE_CULTURE)
+ culture_more_info = !culture_more_info
+ if(CULTURE_FACTION)
+ faction_more_info = !faction_more_info
+ if(CULTURE_LOCATION)
+ location_more_info = !location_more_info
+
+ if("language")
+ var/target_lang = text2path(href_list["lang"])
+ var/level = text2num(href_list["level"])
+ var/required_lang = get_required_languages()
+ if(required_lang[target_lang]) //Can't do anything to a required language
+ return TRUE
+ var/opt_langs = get_optional_languages()
+ if(!opt_langs[target_lang])
+ return TRUE
+ if(!level)
+ languages -= target_lang
+ else if(can_buy_language(target_lang, level))
+ languages[target_lang] = level
+ ShowLangMenu(user)
+ return TRUE
+
+ if("language_button")
+ ShowLangMenu(user)
+ return TRUE
+ if("mutant_color")
+ needs_update = TRUE
+ var/new_mutantcolor = input(user, "Choose your character's primary color:", "Character Preference","#"+features["mcolor"]) as color|null
+ if(new_mutantcolor)
+ if(new_mutantcolor == "#000000")
+ features["mcolor"] = pref_species.default_color
+ else
+ features["mcolor"] = sanitize_hexcolor(new_mutantcolor)
+ if(!allow_advanced_colors)
+ reset_colors()
+
+ if("mutant_color2")
+ needs_update = TRUE
+ var/new_mutantcolor = input(user, "Choose your character's secondary color:", "Character Preference","#"+features["mcolor2"]) as color|null
+ if(new_mutantcolor)
+ if(new_mutantcolor == "#000000")
+ features["mcolor2"] = pref_species.default_color
+ else
+ features["mcolor2"] = sanitize_hexcolor(new_mutantcolor)
+ if(!allow_advanced_colors)
+ reset_colors()
+
+ if("mutant_color3")
+ needs_update = TRUE
+ var/new_mutantcolor = input(user, "Choose your character's tertiary color:", "Character Preference","#"+features["mcolor3"]) as color|null
+ if(new_mutantcolor)
+ if(new_mutantcolor == "#000000")
+ features["mcolor3"] = pref_species.default_color
+ else
+ features["mcolor3"] = sanitize_hexcolor(new_mutantcolor)
+ if(!allow_advanced_colors)
+ reset_colors()
+
+ if("change_marking")
+ needs_update = TRUE
+ switch(href_list["preference"])
+ if("use_preset")
+ var/action = tgui_alert(user, "Are you sure you want to use a preset (This will clear your existing markings)?", "", list("Yes", "No"))
+ if(action && action == "Yes")
+ var/list/candidates = GLOB.body_marking_sets.Copy()
+ if(!mismatched_customization)
+ for(var/name in candidates)
+ var/datum/body_marking_set/BMS = GLOB.body_marking_sets[name]
+ if(BMS.recommended_species && !(pref_species.id in BMS.recommended_species))
+ candidates -= name
+ if(length(candidates) == 0)
+ return
+ var/desired_set = input(user, "Choose your new body markings:", "Character Preference") as null|anything in candidates
+ if(desired_set)
+ var/datum/body_marking_set/BMS = GLOB.body_marking_sets[desired_set]
+ body_markings = assemble_body_markings_from_set(BMS, features, pref_species)
+
+ if("reset_color")
+ var/zone = href_list["key"]
+ var/name = href_list["name"]
+ if(!body_markings[zone] || !body_markings[zone][name])
+ return
+ var/datum/body_marking/BM = GLOB.body_markings[name]
+ body_markings[zone][name] = BM.get_default_color(features, pref_species)
+ if("change_color")
+ var/zone = href_list["key"]
+ var/name = href_list["name"]
+ if(!body_markings[zone] || !body_markings[zone][name])
+ return
+ var/color = body_markings[zone][name]
+ var/new_color = input(user, "Choose your markings color:", "Character Preference","#[color]") as color|null
+ if(new_color)
+ if(!body_markings[zone] || !body_markings[zone][name])
+ return
+ body_markings[zone][name] = sanitize_hexcolor(new_color)
+ if("marking_move_up")
+ var/zone = href_list["key"]
+ var/name = href_list["name"]
+ var/list/marking_list = LAZYACCESS(body_markings, zone)
+ var/current_index = LAZYFIND(marking_list, name)
+ if(!current_index || --current_index < 1)
+ return
+ var/marking_content = marking_list[name]
+ marking_list -= name
+ marking_list.Insert(current_index, name)
+ marking_list[name] = marking_content
+ if("marking_move_down")
+ var/zone = href_list["key"]
+ var/name = href_list["name"]
+ var/list/marking_list = LAZYACCESS(body_markings, zone)
+ var/current_index = LAZYFIND(marking_list, name)
+ if(!current_index || ++current_index > length(marking_list))
+ return
+ var/marking_content = marking_list[name]
+ marking_list -= name
+ marking_list.Insert(current_index, name)
+ marking_list[name] = marking_content
+ if("add_marking")
+ var/zone = href_list["key"]
+ if(!GLOB.body_markings_per_limb[zone])
+ return
+ var/list/possible_candidates = GLOB.body_markings_per_limb[zone].Copy()
+ if(body_markings[zone])
+ //To prevent exploiting hrefs to bypass the marking limit
+ if(body_markings[zone].len >= MAXIMUM_MARKINGS_PER_LIMB)
+ return
+ //Remove already used markings from the candidates
+ for(var/list/this_list in body_markings[zone])
+ possible_candidates -= this_list[MUTANT_INDEX_NAME]
+ if(!mismatched_customization)
+ for(var/name in possible_candidates)
+ var/datum/body_marking/BD = GLOB.body_markings[name]
+ if((BD.recommended_species && !(pref_species.id in BD.recommended_species)))
+ possible_candidates -= name
+
+ if(possible_candidates.len == 0)
+ return
+ var/desired_marking = input(user, "Choose your new marking to add:", "Character Preference") as null|anything in possible_candidates
+ if(desired_marking)
+ var/datum/body_marking/BD = GLOB.body_markings[desired_marking]
+ if(!body_markings[zone])
+ body_markings[zone] = list()
+ body_markings[zone][BD.name] = BD.get_default_color(features, pref_species)
+
+ if("remove_marking")
+ var/zone = href_list["key"]
+ var/name = href_list["name"]
+ if(!body_markings[zone] || !body_markings[zone][name])
+ return
+ body_markings[zone] -= name
+ if(body_markings[zone].len == 0)
+ body_markings -= zone
+ if("change_marking")
+ var/zone = href_list["key"]
+ var/changing_name = href_list["name"]
+
+ var/list/possible_candidates = GLOB.body_markings_per_limb[zone].Copy()
+ if(body_markings[zone])
+ //Remove already used markings from the candidates
+ for(var/keyed_name in body_markings[zone])
+ possible_candidates -= keyed_name
+ if(!mismatched_customization)
+ for(var/name in possible_candidates)
+ var/datum/body_marking/BD = GLOB.body_markings[name]
+ if(BD.recommended_species && !(pref_species.id in BD.recommended_species))
+ possible_candidates -= name
+ if(possible_candidates.len == 0)
+ return
+ var/desired_marking = input(user, "Choose a marking to change the current one to:", "Character Preference") as null|anything in possible_candidates
+ if(desired_marking)
+ if(!body_markings[zone] || !body_markings[zone][changing_name])
+ return
+ var/held_index = LAZYFIND(body_markings[zone], changing_name)
+ var/datum/body_marking/BD = GLOB.body_markings[desired_marking]
+ var/marking_content
+ if(allow_advanced_colors)
+ marking_content = body_markings[zone][changing_name]
+ else
+ marking_content = BD.get_default_color(features, pref_species)
+ body_markings[zone] -= changing_name
+ body_markings[zone].Insert(held_index, desired_marking)
+ body_markings[zone][desired_marking] = marking_content
+ if("uses_skintones")
+ needs_update = TRUE
+ features["uses_skintones"] = !features["uses_skintones"]
+ if("change_arousal_preview")
+ var/list/gen_arous_trans = list("Not aroused" = AROUSAL_NONE,
+ "Partly aroused" = AROUSAL_PARTIAL,
+ "Very aroused" = AROUSAL_FULL
+ )
+ var/new_arousal = input(user, "Choose your character's arousal:", "Character Preference") as null|anything in gen_arous_trans
+ if(new_arousal)
+ arousal_preview = gen_arous_trans[new_arousal]
+ needs_update = TRUE
+ if("change_genitals")
+ needs_update = TRUE
+ switch(href_list["preference"])
+ if("breasts_size")
+ var/new_size = input(user, "Choose your character's breasts size:", "Character Preference") as null|anything in GLOB.preference_breast_sizes
+ if(new_size)
+ features["breasts_size"] = breasts_cup_to_size(new_size)
+ if("breasts_lactation")
+ features["breasts_lactation"] = !features["breasts_lactation"]
+ if("penis_taur_mode")
+ features["penis_taur_mode"] = !features["penis_taur_mode"]
+ if("penis_size")
+ var/new_length = input(user, "Choose your penis length:\n([PENIS_MIN_LENGTH]-[PENIS_MAX_LENGTH] in inches)", "Character Preference") as num|null
+ if(new_length)
+ features["penis_size"] = clamp(round(new_length, 1), PENIS_MIN_LENGTH, PENIS_MAX_LENGTH)
+ if(features["penis_girth"] >= new_length)
+ features["penis_girth"] = new_length - 1
+ if("penis_sheath")
+ var/new_sheath = input(user, "Choose your penis sheath", "Character Preference") as null|anything in SHEATH_MODES
+ if(new_sheath)
+ features["penis_sheath"] = new_sheath
+ if("penis_girth")
+ var/max_girth = PENIS_MAX_GIRTH
+ if(features["penis_size"] >= max_girth)
+ max_girth = features["penis_size"]
+ var/new_girth = input(user, "Choose your penis girth:\n(1-[max_girth] (based on length) in inches)", "Character Preference") as num|null
+ if(new_girth)
+ features["penis_girth"] = clamp(round(new_girth, 1), 1, max_girth)
+ if("balls_size")
+ var/new_size = input(user, "Choose your character's balls size:", "Character Preference") as null|anything in GLOB.preference_balls_sizes
+ if(new_size)
+ features["balls_size"] = balls_description_to_size(new_size)
+ if("change_bodypart")
+ needs_update = TRUE
+ switch(href_list["preference"])
+ if("change_name")
+ var/key = href_list["key"]
+ if(!mutant_bodyparts[key])
+ return
+ var/new_name
+ if(mismatched_customization)
+ new_name = input(user, "Choose your character's [key]:", "Character Preference") as null|anything in accessory_list_of_key_for_species(key, pref_species, TRUE, parent.ckey)
+ else
+ new_name = input(user, "Choose your character's [key]:", "Character Preference") as null|anything in accessory_list_of_key_for_species(key, pref_species, FALSE, parent.ckey)
+ if(new_name && mutant_bodyparts[key])
+ mutant_bodyparts[key][MUTANT_INDEX_NAME] = new_name
+ validate_color_keys_for_part(key)
+ if(!allow_advanced_colors)
+ var/datum/sprite_accessory/SA = GLOB.sprite_accessories[key][new_name]
+ mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST] = SA.get_default_color(features, pref_species)
+ if("change_color")
+ var/key = href_list["key"]
+ if(!mutant_bodyparts[key])
+ return
+ var/list/colorlist = mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST]
+ var/index = text2num(href_list["color_index"])
+ if(colorlist.len < index)
+ return
+ var/new_color = input(user, "Choose your character's [key] color:", "Character Preference","#[colorlist[index]]") as color|null
+ if(new_color)
+ colorlist[index] = sanitize_hexcolor(new_color)
+ if("reset_color")
+ var/key = href_list["key"]
+ if(!mutant_bodyparts[key])
+ return
+ var/datum/sprite_accessory/SA = GLOB.sprite_accessories[key][mutant_bodyparts[key][MUTANT_INDEX_NAME]]
+ mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST] = SA.get_default_color(features, pref_species)
+ if("reset_all_colors")
+ var/action = tgui_alert(user, "Are you sure you want to reset all colors?", "", list("Yes", "No"))
+ if(action == "Yes")
+ reset_colors()
+ else
+ switch(href_list["preference"])
+ if("custom_species")
+ var/new_name = input(user, "Choose your character's species name:", "Character Preference") as text|null
+ if(new_name)
+ if(new_name == "")
+ features["custom_species"] = null
+ else
+ features["custom_species"] = reject_bad_name(new_name)
+ else
+ features["custom_species"] = null
+ if("flavor_text")
+ var/msg = input(usr, "Set the flavor text in your 'examine' verb. This is for describing what people can tell by looking at your character.", "Flavor Text", features["flavor_text"]) as message|null //Skyrat edit, removed stripped_multiline_input()
+ if(!isnull(msg))
+ features["flavor_text"] = STRIP_HTML_SIMPLE(msg, MAX_FLAVOR_LEN)
+ if("silicon_flavor_text")
+ var/msg = input(usr, "Set the flavor text in your 'examine' verb. This is for describing what people can tell by looking at your character.", "Silicon Flavor Text", features["silicon_flavor_text"]) as message|null
+ if(!isnull(msg))
+ features["silicon_flavor_text"] = STRIP_HTML_SIMPLE(msg, MAX_FLAVOR_LEN)
+ if("show_body_size")
+ needs_update = TRUE
+ show_body_size = !show_body_size
+
+ if("body_size")
+ needs_update = TRUE
+ var/new_body_size = input(user, "Choose your desired sprite size:\n([BODY_SIZE_MIN*100]%-[BODY_SIZE_MAX*100]%), Warning: May make your character look distorted", "Character Preference", features["body_size"]*100) as num|null
+ if(new_body_size)
+ new_body_size = clamp(new_body_size * 0.01, BODY_SIZE_MIN, BODY_SIZE_MAX)
+ features["body_size"] = new_body_size
+ if("character_tab")
+ character_settings_tab = text2num(href_list["tab"])
+ if("adv_colors")
+ if(allow_advanced_colors)
+ var/action = tgui_alert(user, "Are you sure you want to disable advanced colors (This will reset your colors back to default)?", "", list("Yes", "No"))
+ if(action && action != "Yes")
+ return
+ allow_advanced_colors = !allow_advanced_colors
+ if(!allow_advanced_colors)
+ reset_colors()
+ if("mismatch")
+ mismatched_customization = !mismatched_customization
+ if("reset_all_colors")
+ var/action = tgui_alert(user, "Are you sure you want to reset all colors?", "", list("Yes", "No"))
+ if(action == "Yes")
+ reset_colors()
+ if("save")
+ save_character()
+ if("load")
+ load_character()
+ show_advanced_prefs(user)
+
+/datum/preferences/proc/show_advanced_prefs(mob/user)
+ update_pref_species()
+ if(needs_update)
+ character_preview_view?.update_body()
+ needs_update = FALSE
+ var/list/dat = list()
+ dat += ""
+ dat += ""
+ dat += "Appearance "
+ dat += "Markings "
+ dat += "Background "
+ dat += "Augmentation "
+ dat += " "
+ dat += " "
+ dat += ""
+ dat += ""
+ dat += " "
+ dat += " "
+ switch(character_settings_tab)
+ if(CHAR_TAB_APPEARANCE)
+ //Mutant stuff
+ dat += ""
+ dat += APPEARANCE_CATEGORY_COLUMN
+ dat += "Sprite body size: [(features["body_size"] * 100)]% [show_body_size ? "Hide preview" : "Show preview"] "
+ dat += "Species Naming: [(features["custom_species"]) ? features["custom_species"] : "Default"] "
+ dat += "Flavor Text "
+ // Carbon flavor text
+ dat += "Set Examine Text "
+ if(length(features["flavor_text"]) <= 40)
+ if(!length(features["flavor_text"]))
+ dat += "\[...\]"
+ else
+ dat += "[html_encode(features["flavor_text"])]"
+ else
+ dat += "[copytext(html_encode(features["flavor_text"]), 1, 40)]..."
+ dat += " "
+ // Silicon flavor text
+ dat += "Set Silicon Examine Text "
+ if(length(features["silicon_flavor_text"]) <= 40)
+ if(!length(features["silicon_flavor_text"]))
+ dat += "\[...\]"
+ else
+ dat += "[html_encode(features["silicon_flavor_text"])]"
+ else
+ dat += "[copytext(html_encode(features["silicon_flavor_text"]), 1, 40)]..."
+ dat += ""
+ var/mutant_category = 0
+ var/list/generic_cache = GLOB.generic_accessories
+ for(var/key in mutant_bodyparts)
+ if(!generic_cache[key]) //This means that we have a mutant bodypart that shouldnt be bundled here (genitals)
+ continue
+ if(!mutant_category)
+ dat += APPEARANCE_CATEGORY_COLUMN
+ dat += "[generic_cache[key]] "
+ dat += print_bodypart_change_line(key)
+ dat += " "
+ mutant_category++
+ if(mutant_category >= MAX_MUTANT_ROWS)
+ dat += ""
+ mutant_category = 0
+ if(mutant_category)
+ dat += ""
+ mutant_category = 0
+ dat += "
"
+ dat += ""
+ if(pref_species.can_have_genitals)
+ dat += APPEARANCE_CATEGORY_COLUMN
+ dat += "Change arousal preview "
+ dat += "Penis "
+ var/penis_name = mutant_bodyparts["penis"][MUTANT_INDEX_NAME]
+ dat += print_bodypart_change_line("penis")
+ if(penis_name != "None")
+ dat += "Length: [features["penis_size"]] inches."
+ dat += "Girth: [features["penis_girth"]] inches circumference"
+ dat += "Sheath: [features["penis_sheath"]] "
+ dat += "Testicles "
+ var/balls_name = mutant_bodyparts["testicles"][MUTANT_INDEX_NAME]
+ dat += print_bodypart_change_line("testicles")
+ if(balls_name != "None")
+ var/named_size = balls_size_to_description(features["balls_size"])
+ dat += "Size: [named_size] "
+ if(mutant_bodyparts["taur"])
+ var/datum/sprite_accessory/taur/TSP = GLOB.sprite_accessories["taur"][mutant_bodyparts["taur"][MUTANT_INDEX_NAME]]
+ if(TSP.factual && !(TSP.taur_mode & STYLE_TAUR_SNAKE))
+ var/text_string = (features["penis_taur_mode"]) ? "Yes" : "No"
+ dat += "Taur Mode: [text_string] "
+ dat += ""
+ dat += ""
+ dat += APPEARANCE_CATEGORY_COLUMN
+ dat += "Uses skintones: [(features["uses_skintones"]) ? "Yes" : "No"] "
+ dat += "Vagina "
+ dat += print_bodypart_change_line("vagina")
+ dat += "Womb "
+ dat += print_bodypart_change_line("womb")
+ dat += ""
+ dat += APPEARANCE_CATEGORY_COLUMN
+ dat += " "
+ dat += "Breasts "
+ var/breasts_name = mutant_bodyparts["breasts"][MUTANT_INDEX_NAME]
+ dat += print_bodypart_change_line("breasts")
+ if(breasts_name != "None")
+ var/named_size = breasts_size_to_cup(features["breasts_size"])
+ var/named_lactation = (features["breasts_lactation"]) ? "Yes" : "No"
+ dat += "Size: [named_size] "
+ dat += "Can Lactate: [named_lactation] "
+ dat += ""
+ dat += "
"
+
+ if(CHAR_TAB_MARKINGS)
+ dat += "Use a markings preset : Choose "
+ dat += ""
+ dat += " Primary: Change "
+ dat += " Secondary: Change "
+ dat += " Tertiary: Change "
+ dat += "
"
+ dat += ""
+ dat += ""
+ var/iterated_markings = 0
+ for(var/zone in GLOB.marking_zones)
+ var/named_zone = " "
+ switch(zone)
+ if(BODY_ZONE_R_ARM)
+ named_zone = "Right Arm"
+ if(BODY_ZONE_L_ARM)
+ named_zone = "Left Arm"
+ if(BODY_ZONE_HEAD)
+ named_zone = "Head"
+ if(BODY_ZONE_CHEST)
+ named_zone = "Chest"
+ if(BODY_ZONE_R_LEG)
+ named_zone = "Right Leg"
+ if(BODY_ZONE_L_LEG)
+ named_zone = "Left Leg"
+ if(BODY_ZONE_PRECISE_R_HAND)
+ named_zone = "Right Hand"
+ if(BODY_ZONE_PRECISE_L_HAND)
+ named_zone = "Left Hand"
+ dat += "[named_zone] "
+ dat += ""
+ dat += ""
+ dat += " "
+ dat += " "
+ dat += " "
+ dat += " "
+ dat += " "
+ dat += " "
+ if(body_markings[zone])
+ for(var/key in body_markings[zone])
+ var/datum/body_marking/BD = GLOB.body_markings[key]
+ var/can_move_up = " "
+ var/can_move_down = " "
+ var/color_line = " "
+ var/current_index = LAZYFIND(body_markings[zone], key)
+ if(BD.always_color_customizable || allow_advanced_colors)
+ var/color = body_markings[zone][key]
+ color_line = "R "
+ color_line += " "
+ if(current_index < length(body_markings[zone]))
+ can_move_down = "Down "
+ if(current_index > 1)
+ can_move_up = "Up "
+ dat += ""
+ dat += "[can_move_up] "
+ dat += "[can_move_down] "
+ dat += "[key] "
+ dat += "[color_line] "
+ dat += "Remove "
+ dat += " "
+ if(!(body_markings[zone]) || body_markings[zone].len < MAXIMUM_MARKINGS_PER_LIMB)
+ dat += ""
+ dat += " "
+ dat += " "
+ dat += " "
+ dat += " "
+ dat += "Add "
+ dat += " "
+ dat += "
"
+ iterated_markings += 1
+ if(iterated_markings >= 4)
+ dat += " "
+ iterated_markings = 0
+ dat += "
"
+ if(CHAR_TAB_BACKGROUND)
+ dat += "THE ENTRIES AND THEIR DESCRIPTIONS ARE PLACEHOLDERS AND ARE NOT FINAL. "
+ dat += ""
+ dat += ""
+ dat += " "
+ dat += " "
+ dat += " "
+ dat += " "
+ var/even = FALSE
+ for(var/cultural_thing in list(CULTURE_CULTURE, CULTURE_LOCATION, CULTURE_FACTION))
+ even = !even
+ var/datum/cultural_info/cult
+ var/prefix
+ var/more = FALSE
+ switch(cultural_thing)
+ if(CULTURE_CULTURE)
+ cult = GLOB.culture_cultures[pref_culture]
+ prefix = "Culture"
+ more = culture_more_info
+ if(CULTURE_LOCATION)
+ cult = GLOB.culture_locations[pref_location]
+ prefix = "Location"
+ more = location_more_info
+ if(CULTURE_FACTION)
+ cult = GLOB.culture_factions[pref_faction]
+ prefix = "Faction"
+ more = faction_more_info
+ var/cult_desc
+ if(more || length(cult.description) <= 160)
+ cult_desc = cult.description
+ else
+ cult_desc = "[copytext(cult.description, 1, 160)]..."
+ dat += ""
+ dat += "[prefix]: [cult.name] [cult.get_extra_desc(more)] "
+ dat += "[cult_desc] "
+ dat += "[more ? "Show Less" : "Show More"] "
+ dat += " "
+ dat += "
"
+ dat += ""
+ dat += ""
+ dat += "Languages "
+ dat += "Linguistic points: [get_linguistic_points()] "
+ for(var/language_path in languages)
+ var/datum/language/lang_datum = language_path
+ dat += " [initial(lang_datum.name)] - [languages[language_path] == LANGUAGE_SPOKEN ? "Spoken" : "Understood" ]"
+ dat += "Change Languages... "
+ dat += " "
+ dat += ""
+ dat += "Records "
+ dat += "General "
+ dat += "Set general record "
+ if(length(general_record) <= 40)
+ if(!length(general_record))
+ dat += "\[...\]"
+ else
+ dat += "[html_encode(general_record)]"
+ else
+ dat += "[copytext(html_encode(general_record), 1, 40)]..."
+ dat += " "
+
+
+ dat += "Medical "
+ dat += "Set medical record "
+ if(length(medical_record) <= 40)
+ if(!length(medical_record))
+ dat += "\[...\]"
+ else
+ dat += "[html_encode(medical_record)]"
+ else
+ dat += "[copytext(html_encode(medical_record), 1, 40)]..."
+ dat += " "
+
+
+ dat += "Security "
+ dat += "Set security record "
+ if(length(security_record) <= 40)
+ if(!length(security_record))
+ dat += "\[...\]"
+ else
+ dat += "[html_encode(security_record)]"
+ else
+ dat += "[copytext(html_encode(security_record), 1, 40)]..."
+ dat += " "
+ dat += " "
+
+
+ dat += ""
+ dat += "Information "
+ dat += "Background "
+ dat += "Set background information "
+ if(length(background_info) <= 40)
+ if(!length(background_info))
+ dat += "\[...\]"
+ else
+ dat += "[html_encode(background_info)]"
+ else
+ dat += "[copytext(html_encode(background_info), 1, 40)]..."
+ dat += "Exploitable "
+ dat += "Set exploitable information "
+ if(length(exploitable_info) <= 40)
+ if(!length(exploitable_info))
+ dat += "\[...\]"
+ else
+ dat += "[html_encode(exploitable_info)]"
+ else
+ dat += "[copytext(html_encode(exploitable_info), 1, 40)]..."
+ dat += " "
+ dat += ""
+ //Empty column for future stuff here
+ dat += " "
+ dat += "
"
+ if(CHAR_TAB_AUGMENTS)
+ if(!pref_species.can_augment)
+ dat += "Sorry, but your species doesn't support augmentations"
+ else if(!SSquirks || !SSquirks.quirks.len)
+ dat += "The quirk subsystem is still initializing! Try again in a minute."
+ else
+ dat += ""
+ for(var/category_name in GLOB.augment_categories_to_slots)
+ dat += ""
+ dat += "[category_name]: "
+ var/list/slot_list = GLOB.augment_categories_to_slots[category_name]
+ for(var/slot_name in slot_list)
+ var/link = "href='?src=[REF(src)];task=augment_slot;slot=[slot_name]'"
+ var/datum/augment_item/chosen_item
+ if(augments[slot_name])
+ chosen_item = GLOB.augment_items[augments[slot_name]]
+ if(chosen_augment_slot && chosen_augment_slot == slot_name)
+ link = "class='linkOn'"
+ var/print_name = ""
+ if(chosen_item)
+ print_name = chosen_item.name
+ var/font_color = "#AAAAFF"
+ if(chosen_item.cost != 0)
+ font_color = chosen_item.cost > 0 ? "#AAFFAA" : "#FFAAAA"
+ print_name = "[print_name] "
+ dat += ""
+ dat += "[slot_name] : [print_name] "
+ if(category_name == AUGMENT_CATEGORY_LIMBS && chosen_item)
+ var/datum/augment_item/limb/chosen_limb = chosen_item
+ var/print_style = "None "
+ if(augment_limb_styles[slot_name])
+ print_style = augment_limb_styles[slot_name]
+ if(chosen_limb.uses_robotic_styles)
+ dat += "Style : [print_style] "
+ dat += "[chosen_item ? "[chosen_item.description] " : ""] "
+ dat += "
"
+ dat += " "
+ dat += ""
+ if(chosen_augment_slot)
+ var/list/augment_list = GLOB.augment_slot_to_items[chosen_augment_slot]
+ if(augment_list)
+ dat += ""
+ dat += "[chosen_augment_slot] "
+ dat += ""
+ dat += "Name "
+ dat += "Cost "
+ dat += "Description "
+ dat += " "
+ var/even = FALSE
+ for(var/type_thing in augment_list)
+ var/datum/augment_item/aug_datum = GLOB.augment_items[type_thing]
+ var/datum/augment_item/current
+ even = !even
+ if(augments[chosen_augment_slot])
+ current = GLOB.augment_items[augments[chosen_augment_slot]]
+ var/aug_link = "class='linkOff'"
+ var/name_print = aug_datum.name
+ if (current == aug_datum)
+ aug_link = "class='linkOn' href='?src=[REF(src)];task=set_augment;type=[type_thing]'"
+ name_print = "[name_print] (Remove)"
+ else if(CanBuyAugment(aug_datum, current))
+ aug_link = "href='?src=[REF(src)];task=set_augment;type=[type_thing]'"
+ dat += ""
+ dat += "[name_print] "
+ dat += "[aug_datum.cost] "
+ dat += "[aug_datum.description] "
+ dat += " "
+ dat += "
"
+ dat += "
"
+ dat += ""
+ dat += "Undo "
+ dat += "Save Setup "
+ dat += " "
+
+ var/datum/browser/popup = new(user, "character_setup", "Character Setup
", 840, 770)
+ popup.set_content(dat.Join())
+ popup.open(FALSE)
+
+/datum/preferences/proc/print_bodypart_change_line(key)
+ var/acc_name = mutant_bodyparts[key][MUTANT_INDEX_NAME]
+ var/shown_colors = 0
+ var/datum/sprite_accessory/SA = GLOB.sprite_accessories[key][acc_name]
+ var/dat = ""
+ if(SA.color_src == USE_MATRIXED_COLORS)
+ shown_colors = 3
+ else if (SA.color_src == USE_ONE_COLOR)
+ shown_colors = 1
+ if((allow_advanced_colors || SA.always_color_customizable) && shown_colors)
+ dat += "R "
+ dat += "[acc_name] "
+ if(allow_advanced_colors || SA.always_color_customizable)
+ if(shown_colors)
+ dat += " "
+ var/list/colorlist = mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST]
+ for(var/i in 1 to shown_colors)
+ dat += " "
+ return dat
+
+/datum/preferences/proc/reset_colors()
+ for(var/key in mutant_bodyparts)
+ var/datum/sprite_accessory/SA = GLOB.sprite_accessories[key][mutant_bodyparts[key][MUTANT_INDEX_NAME]]
+ if(SA.always_color_customizable)
+ continue
+ mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST] = SA.get_default_color(features, pref_species)
+
+ for(var/zone in body_markings)
+ var/list/bml = body_markings[zone]
+ for(var/key in bml)
+ var/datum/body_marking/BM = GLOB.body_markings[key]
+ bml[key] = BM.get_default_color(features, pref_species)
+
+/datum/preferences/proc/get_linguistic_points()
+ var/points
+ points = (QUIRK_LINGUIST in all_quirks) ? LINGUISTIC_POINTS_LINGUIST : LINGUISTIC_POINTS_DEFAULT
+ for(var/langpath in languages)
+ points -= languages[langpath]
+ return points
+
+/datum/preferences/proc/get_required_languages()
+ var/list/lang_list = list()
+ for(var/cultural_thing in list(CULTURE_CULTURE, CULTURE_LOCATION, CULTURE_FACTION))
+ var/datum/cultural_info/cult
+ switch(cultural_thing)
+ if(CULTURE_CULTURE)
+ cult = GLOB.culture_cultures[pref_culture]
+ if(CULTURE_LOCATION)
+ cult = GLOB.culture_locations[pref_location]
+ if(CULTURE_FACTION)
+ cult = GLOB.culture_factions[pref_faction]
+ if(cult.required_lang)
+ lang_list[cult.required_lang] = TRUE
+ return lang_list
+
+/datum/preferences/proc/get_optional_languages()
+ var/list/lang_list = list()
+ for(var/lang in pref_species.learnable_languages)
+ lang_list[lang] = TRUE
+ for(var/cultural_thing in list(CULTURE_CULTURE, CULTURE_LOCATION, CULTURE_FACTION))
+ var/datum/cultural_info/cult
+ switch(cultural_thing)
+ if(CULTURE_CULTURE)
+ cult = GLOB.culture_cultures[pref_culture]
+ if(CULTURE_LOCATION)
+ cult = GLOB.culture_locations[pref_location]
+ if(CULTURE_FACTION)
+ cult = GLOB.culture_factions[pref_faction]
+ if(cult.additional_langs)
+ for(var/langtype in cult.additional_langs)
+ lang_list[langtype] = TRUE
+ return lang_list
+
+/datum/preferences/proc/get_available_languages()
+ var/list/lang_list = get_required_languages()
+ for(var/lang_key in get_optional_languages())
+ lang_list[lang_key] = TRUE
+ return lang_list
+
+/datum/preferences/proc/validate_languages()
+ var/list/opt_langs = get_optional_languages()
+ var/list/req_langs = get_required_languages()
+ for(var/langkey in languages)
+ if(!opt_langs[langkey] && !req_langs[langkey])
+ languages -= langkey
+ for(var/req_lang in req_langs)
+ if(!languages[req_lang])
+ languages[req_lang] = LANGUAGE_SPOKEN
+ var/left_points = get_linguistic_points()
+ //If we're below 0 points somehow, remove all optional languages
+ if(left_points < 0)
+ for(var/lang in languages)
+ if(!req_langs[lang])
+ languages -= lang
+
+/datum/preferences/proc/can_buy_language(language_path, level)
+ var/points = get_linguistic_points()
+ if(languages[language_path])
+ points += languages[language_path]
+ if(points < level)
+ return FALSE
+ return TRUE
+
+//Whenever we switch a species, we'll try to get common if we can to not confuse anyone
+/datum/preferences/proc/try_get_common_language()
+ var/list/langs = get_available_languages()
+ if(langs[/datum/language/common])
+ languages[/datum/language/common] = LANGUAGE_SPOKEN
+
+/datum/preferences/proc/ShowLangMenu(mob/user)
+ var/list/dat = list()
+ dat += "Choose your languages: "
+ dat += "Availability of the languages to choose from depends on your background. If you can't unlearn one, it means it is required for your background."
+ dat += "Done "
+ dat += " "
+ var/current_ling_points = get_linguistic_points()
+ dat += "Linguistic Points remaining: [current_ling_points] "
+ dat += ""
+ dat += " "
+ dat += " "
+ dat += " "
+ dat += " "
+ dat += " "
+ dat += " "
+ var/list/avail_langs = get_available_languages()
+ var/list/req_langs = get_required_languages()
+ var/even = TRUE
+ var/background_cl
+ for(var/lang_path in avail_langs)
+ even = !even
+ var/datum/language/lang_datum = lang_path
+ var/required = (req_langs[lang_path] ? TRUE : FALSE)
+ if(even)
+ background_cl = (required ? "#7A5A00" : "#17191C")
+ else
+ background_cl = (required ? "#856200" : "#23273C")
+ var/language_skill = 0
+ if(languages[lang_path])
+ language_skill = languages[lang_path]
+ var/unlearn_button
+ if(language_skill && !required)
+ unlearn_button = "Unlearn "
+ else
+ unlearn_button = "Unlearn "
+ var/understood_button
+ if(languages[lang_path])
+ //Has a href in case you want to downgrade from spoken to understood
+ understood_button = "Understood "
+ else if(can_buy_language(lang_path, LANGUAGE_UNDERSTOOD))
+ understood_button = "Understood "
+ else
+ understood_button = "Understood "
+ var/spoken_button
+ if(languages[lang_path] >= LANGUAGE_SPOKEN)
+ spoken_button = "Spoken "
+ else if(can_buy_language(lang_path, LANGUAGE_SPOKEN))
+ spoken_button = "Spoken "
+ else
+ spoken_button = "Spoken "
+ dat += ""
+ dat += "[initial(lang_datum.name)] "
+ dat += "[initial(lang_datum.desc)] "
+ dat += "[unlearn_button] "
+ dat += "[understood_button] "
+ dat += "[spoken_button] "
+ dat += " "
+ dat += ""
+ var/datum/browser/popup = new(user, "culture_lang", "Language Choice
", 900, 600)
+ popup.set_window_options("can_close=0")
+ popup.set_content(dat.Join())
+ popup.open(FALSE)
+
+/datum/preferences/proc/validate_species_parts()
+ update_pref_species()
+
+ var/list/target_bodyparts = pref_species.default_mutant_bodyparts.Copy()
+
+ //Remove all "extra" accessories
+ for(var/key in mutant_bodyparts)
+ if(!GLOB.sprite_accessories[key]) //That accessory no longer exists, remove it
+ mutant_bodyparts -= key
+ continue
+ if(!pref_species.default_mutant_bodyparts[key])
+ mutant_bodyparts -= key
+ continue
+ if(!GLOB.sprite_accessories[key][mutant_bodyparts[key][MUTANT_INDEX_NAME]]) //The individual accessory no longer exists
+ mutant_bodyparts[key][MUTANT_INDEX_NAME] = pref_species.default_mutant_bodyparts[key]
+ validate_color_keys_for_part(key) //Validate the color count of each accessory that wasnt removed
+
+ //Add any missing accessories
+ for(var/key in target_bodyparts)
+ if(!mutant_bodyparts[key])
+ var/datum/sprite_accessory/SA
+ if(target_bodyparts[key] == ACC_RANDOM)
+ SA = random_accessory_of_key_for_species(key, pref_species)
+ else
+ SA = GLOB.sprite_accessories[key][target_bodyparts[key]]
+ var/final_list = list()
+ final_list[MUTANT_INDEX_NAME] = SA.name
+ final_list[MUTANT_INDEX_COLOR_LIST] = SA.get_default_color(features, pref_species)
+ mutant_bodyparts[key] = final_list
+
+ if(!allow_advanced_colors)
+ reset_colors()
+
+/datum/preferences/proc/validate_color_keys_for_part(key)
+ var/datum/sprite_accessory/SA = GLOB.sprite_accessories[key][mutant_bodyparts[key][MUTANT_INDEX_NAME]]
+ var/list/colorlist = mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST]
+ if(SA.color_src == USE_MATRIXED_COLORS && colorlist.len != 3)
+ mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST] = SA.get_default_color(features, pref_species)
+ else if (SA.color_src == USE_ONE_COLOR && colorlist.len != 1)
+ mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST] = SA.get_default_color(features, pref_species)
+
+/datum/preferences/proc/CanBuyAugment(datum/augment_item/target_aug, datum/augment_item/current_aug)
+ //Check biotypes
+ if(!(pref_species.inherent_biotypes & target_aug.allowed_biotypes))
+ return
+ var/quirk_points = GetQuirkBalance()
+ var/leverage = 0
+ if(current_aug)
+ leverage += current_aug.cost
+ if((quirk_points+leverage)>= target_aug.cost)
+ return TRUE
+ else
+ return FALSE
+
+#undef CHAR_TAB_APPEARANCE
+#undef CHAR_TAB_MARKINGS
+#undef CHAR_TAB_BACKGROUND
+#undef CHAR_TAB_AUGMENTS
+
+#undef APPEARANCE_CATEGORY_COLUMN
+#undef MAX_MUTANT_ROWS
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/_admin.dm b/modular_skyrat/master_files/code/modules/client/preferences/_admin.dm
new file mode 100644
index 00000000000..3720993e1c5
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/_admin.dm
@@ -0,0 +1,8 @@
+/datum/preference/toggle/admin
+ abstract_type = /datum/preference/toggle/admin
+
+/datum/preference/toggle/admin/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return is_admin(preferences.parent)
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/auto_dementor.dm b/modular_skyrat/master_files/code/modules/client/preferences/auto_dementor.dm
new file mode 100644
index 00000000000..7bd29c560aa
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/auto_dementor.dm
@@ -0,0 +1,5 @@
+/datum/preference/toggle/admin/auto_dementor
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "auto_dementor_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/be_antag.dm b/modular_skyrat/master_files/code/modules/client/preferences/be_antag.dm
new file mode 100644
index 00000000000..e865921bb3a
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/be_antag.dm
@@ -0,0 +1,5 @@
+/datum/preference/toggle/be_antag
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ default_value = FALSE
+ savefile_key = "be_antag_pref"
+ savefile_identifier = PREFERENCE_PLAYER
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/delete_sparks.dm b/modular_skyrat/master_files/code/modules/client/preferences/delete_sparks.dm
new file mode 100644
index 00000000000..5725d618c46
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/delete_sparks.dm
@@ -0,0 +1,5 @@
+/datum/preference/toggle/admin/delete_sparks
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ default_value = FALSE
+ savefile_key = "delete_sparks_pref"
+ savefile_identifier = PREFERENCE_PLAYER
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/erp_preferences.dm b/modular_skyrat/master_files/code/modules/client/preferences/erp_preferences.dm
new file mode 100644
index 00000000000..ce085d165df
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/erp_preferences.dm
@@ -0,0 +1,76 @@
+/datum/config_entry/flag/disable_erp_preferences
+ default = FALSE
+
+/datum/preference/toggle/master_erp_preferences
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "master_erp_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/master_erp_preferences/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ if(CONFIG_GET(flag/disable_erp_preferences))
+ return FALSE
+
+ return TRUE
+
+/datum/preference/toggle/erp
+ default_value = FALSE
+ abstract_type = /datum/preference/toggle/erp
+
+/datum/preference/toggle/erp/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ if(CONFIG_GET(flag/disable_erp_preferences))
+ return FALSE
+
+ return preferences.read_preference(/datum/preference/toggle/master_erp_preferences)
+
+/datum/preference/toggle/erp/deserialize(input, datum/preferences/preferences)
+ if(CONFIG_GET(flag/disable_erp_preferences))
+ return FALSE
+ if(!preferences.read_preference(/datum/preference/toggle/master_erp_preferences))
+ return FALSE
+ . = ..()
+
+/datum/preference/toggle/erp/cum_face
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "cum_face_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/erp/sex_toy
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "sextoy_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/erp/noncon
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "noncon_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/erp/bimbofication
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "bimbofication_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/erp/aphro
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "aphro_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/erp/breast_enlargement
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "breast_enlargement_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/erp/penis_enlargement
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "penis_enlargement_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/erp/gender_change
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "gender_change_pref"
+ savefile_identifier = PREFERENCE_PLAYER
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/flavor_text.dm b/modular_skyrat/master_files/code/modules/client/preferences/flavor_text.dm
new file mode 100644
index 00000000000..a29dbdb5d65
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/flavor_text.dm
@@ -0,0 +1 @@
+/datum/preference
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/looc.dm b/modular_skyrat/master_files/code/modules/client/preferences/looc.dm
new file mode 100644
index 00000000000..8b49dd13b29
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/looc.dm
@@ -0,0 +1,11 @@
+/datum/preference/toggle/see_looc
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ default_value = TRUE
+ savefile_key = "looc_pref"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/admin/see_looc
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ default_value = TRUE
+ savefile_key = "looc_admin_pref"
+ savefile_identifier = PREFERENCE_PLAYER
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/species_features/generate_side_shots.dm b/modular_skyrat/master_files/code/modules/client/preferences/species_features/generate_side_shots.dm
new file mode 100644
index 00000000000..91d1331880c
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/species_features/generate_side_shots.dm
@@ -0,0 +1,27 @@
+/proc/generate_mutant_accessory_icons(list/sprite_accessories, key, direction = EAST, layer = BODY_FRONT_LAYER)
+ var/list/values = list()
+
+ for (var/name in sprite_accessories)
+ var/datum/sprite_accessory/sprite_accessory = sprite_accessories[name]
+
+ var/icon/final_icon
+
+ if(sprite_accessory.icon_state != "none")
+ if(!(layer in sprite_accessory.relevent_layers))
+ layer = pick(sprite_accessory.relevent_layers)
+ switch(layer)
+ if(BODY_FRONT_LAYER)
+ layer = "FRONT"
+ if(BODY_ADJ_LAYER)
+ layer = "ADJ"
+ else
+ layer = "BEHIND"
+ final_icon = icon(sprite_accessory.icon,"m_[key]_[sprite_accessory.icon_state]_[layer]", direction)
+ else
+ final_icon = icon('icons/mob/human_parts_greyscale.dmi', "lizard_chest_m", direction)
+
+ final_icon.Blend(COLOR_VIBRANT_LIME)
+
+ values[name] = final_icon
+
+ return values
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/species_features/mutant_colors.dm b/modular_skyrat/master_files/code/modules/client/preferences/species_features/mutant_colors.dm
new file mode 100644
index 00000000000..c96d8b4dc8e
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/species_features/mutant_colors.dm
@@ -0,0 +1,43 @@
+/*
+/datum/preference/color_legacy/mutant_color_two
+ savefile_key = "feature_mcolor2"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
+ relevant_species_trait = MUTCOLORS
+
+/datum/preference/color_legacy/mutant_color_two/create_default_value()
+ return sanitize_hexcolor("[pick("7F", "FF")][pick("7F", "FF")][pick("7F", "FF")]")
+
+/datum/preference/color_legacy/mutant_color_two/apply_to_human(mob/living/carbon/human/target, value)
+ target.dna.features["mcolor2"] = value
+
+/datum/preference/color_legacy/mutant_color_two/is_valid(value)
+ if (!..(value))
+ return FALSE
+
+ if (is_color_dark(expand_three_digit_color(value)))
+ return FALSE
+
+ return TRUE
+
+/datum/preference/color_legacy/mutant_color_three
+ savefile_key = "feature_mcolor3"
+ savefile_identifier = PREFERENCE_CHARACTER
+ category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
+ relevant_species_trait = MUTCOLORS
+
+/datum/preference/color_legacy/mutant_color_three/create_default_value()
+ return sanitize_hexcolor("[pick("7F", "FF")][pick("7F", "FF")][pick("7F", "FF")]")
+
+/datum/preference/color_legacy/mutant_color_three/apply_to_human(mob/living/carbon/human/target, value)
+ target.dna.features["mcolor3"] = value
+
+/datum/preference/color_legacy/mutant_color_three/is_valid(value)
+ if (!..(value))
+ return FALSE
+
+ if (is_color_dark(expand_three_digit_color(value)))
+ return FALSE
+
+ return TRUE
+*/
diff --git a/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm b/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm
new file mode 100644
index 00000000000..5d4d4b8f383
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm
@@ -0,0 +1,77 @@
+/datum/preferences/proc/load_character_skyrat(savefile/S)
+
+ READ_FILE(S["loadout_list"], loadout_list)
+
+ READ_FILE(S["augments"] , augments)
+ READ_FILE(S["augment_limb_styles"] , augment_limb_styles)
+
+ augments = SANITIZE_LIST(augments)
+ //validating augments
+ for(var/aug_slot in augments)
+ var/datum/augment_item/aug = GLOB.augment_items[augments[aug_slot]]
+ if(!aug)
+ augments -= aug_slot
+ augment_limb_styles = SANITIZE_LIST(augment_limb_styles)
+ //validating limb styles
+ for(var/key in augment_limb_styles)
+ if(!GLOB.robotic_styles_list[key])
+ augment_limb_styles -= key
+
+ READ_FILE(S["features"], features)
+ READ_FILE(S["mutant_bodyparts"], mutant_bodyparts)
+ READ_FILE(S["body_markings"], body_markings)
+
+ READ_FILE(S["mismatched_customization"], mismatched_customization)
+ READ_FILE(S["allow_advanced_colors"], allow_advanced_colors)
+
+ READ_FILE(S["pref_culture"] , pref_culture)
+ READ_FILE(S["pref_location"] , pref_location)
+ READ_FILE(S["pref_faction"] , pref_faction)
+
+ READ_FILE(S["general_record"], general_record)
+ READ_FILE(S["security_record"], security_record)
+ READ_FILE(S["medical_record"], medical_record)
+ READ_FILE(S["background_info"], background_info)
+ READ_FILE(S["exploitable_info"], exploitable_info)
+
+ general_record = sanitize_text(general_record)
+ security_record = sanitize_text(security_record)
+ medical_record = sanitize_text(medical_record)
+ background_info = sanitize_text(background_info)
+ exploitable_info = sanitize_text(exploitable_info)
+
+ READ_FILE(S["languages"] , languages)
+ languages = SANITIZE_LIST(languages)
+
+ update_pref_species()
+
+ if(!pref_culture || !GLOB.culture_cultures[pref_culture])
+ pref_culture = pref_species.cultures[1]
+ if(!pref_location || !GLOB.culture_locations[pref_location])
+ pref_location = pref_species.locations[1]
+ if(!pref_faction || !GLOB.culture_factions[pref_faction])
+ pref_faction = pref_species.factions[1]
+
+
+ validate_languages()
+
+ validate_species_parts()
+
+/datum/preferences/proc/save_character_skyrat(savefile/S)
+
+ WRITE_FILE(S["loadout_list"], loadout_list)
+ WRITE_FILE(S["augments"] , augments)
+ WRITE_FILE(S["augment_limb_styles"] , augment_limb_styles)
+ WRITE_FILE(S["features"] , features)
+ WRITE_FILE(S["mutant_bodyparts"] , mutant_bodyparts)
+ WRITE_FILE(S["body_markings"] , body_markings)
+
+ WRITE_FILE(S["mismatched_customization"], mismatched_customization)
+ WRITE_FILE(S["allow_advanced_colors"], allow_advanced_colors)
+
+ WRITE_FILE(S["general_record"] , general_record)
+ WRITE_FILE(S["security_record"] , security_record)
+ WRITE_FILE(S["medical_record"] , medical_record)
+ WRITE_FILE(S["background_info"] , background_info)
+ WRITE_FILE(S["exploitable_info"] , exploitable_info)
+
diff --git a/modular_skyrat/master_files/code/modules/mob/living/dead/new_player/new_player.dm b/modular_skyrat/master_files/code/modules/mob/living/dead/new_player/new_player.dm
deleted file mode 100644
index cbf37eee2bd..00000000000
--- a/modular_skyrat/master_files/code/modules/mob/living/dead/new_player/new_player.dm
+++ /dev/null
@@ -1,462 +0,0 @@
-/mob/dead/new_player
- flags_1 = NONE
- invisibility = INVISIBILITY_ABSTRACT
- density = FALSE
- stat = DEAD
- hud_type = /datum/hud/new_player
- hud_possible = list()
-
- var/ready = FALSE
- /// Referenced when you want to delete the new_player later on in the code.
- var/spawning = FALSE
- /// For instant transfer once the round is set up
- var/mob/living/new_character
- ///Used to make sure someone doesn't get spammed with messages if they're ineligible for roles.
- var/ineligible_for_roles = FALSE
-
-
-
-/mob/dead/new_player/Initialize()
- if(client && SSticker.state == GAME_STATE_STARTUP)
- var/atom/movable/screen/splash/S = new(client, TRUE, TRUE)
- S.Fade(TRUE)
-
- if(length(GLOB.newplayer_start))
- forceMove(pick(GLOB.newplayer_start))
- else
- forceMove(locate(1,1,1))
-
- ComponentInitialize()
-
- . = ..()
-
- GLOB.new_player_list += src
-
-/mob/dead/new_player/Destroy()
- GLOB.new_player_list -= src
-
- return ..()
-
-/mob/dead/new_player/prepare_huds()
- return
-
-/mob/dead/new_player/Topic(href, href_list[])
- if(src != usr)
- return
-
- if(!client)
- return
-
- if(client.interviewee)
- return FALSE
-
- if(href_list["late_join"]) //This still exists for queue messages in chat
- if(!SSticker?.IsRoundInProgress())
- to_chat(usr, span_boldwarning("The round is either not ready, or has already finished..."))
- return
- LateChoices()
- return
-
- if(href_list["SelectedJob"])
- if(!SSticker?.IsRoundInProgress())
- to_chat(usr, span_danger("The round is either not ready, or has already finished..."))
- return
-
- if(SSlag_switch.measures[DISABLE_NON_OBSJOBS])
- to_chat(usr, span_notice("There is an administrative lock on entering the game!"))
- return
-
- //Determines Relevent Population Cap
- var/relevant_cap
- var/hpc = CONFIG_GET(number/hard_popcap)
- var/epc = CONFIG_GET(number/extreme_popcap)
- if(hpc && epc)
- relevant_cap = min(hpc, epc)
- else
- relevant_cap = max(hpc, epc)
-
-
-
- if(SSticker.queued_players.len && !(ckey(key) in GLOB.admin_datums))
- if((living_player_count() >= relevant_cap) || (src != SSticker.queued_players[1]))
- to_chat(usr, span_warning("Server is full."))
- return
-
- AttemptLateSpawn(href_list["SelectedJob"])
- return
-
- if(href_list["viewpoll"])
- var/datum/poll_question/poll = locate(href_list["viewpoll"]) in GLOB.polls
- poll_player(poll)
-
- if(href_list["votepollref"])
- var/datum/poll_question/poll = locate(href_list["votepollref"]) in GLOB.polls
- vote_on_poll_handler(poll, href_list)
-
-
-//When you cop out of the round (NB: this HAS A SLEEP FOR PLAYER INPUT IN IT)
-/mob/dead/new_player/proc/make_me_an_observer()
- if(QDELETED(src) || !src.client)
- ready = PLAYER_NOT_READY
- return FALSE
-
- var/less_input_message
- if(SSlag_switch.measures[DISABLE_DEAD_KEYLOOP])
- less_input_message = " - Notice: Observer freelook is currently disabled."
- var/this_is_like_playing_right = tgui_alert(usr, "Are you sure you wish to observe? You will not be able to play this round![less_input_message]","Player Setup",list("Yes","No"))
-
- if(QDELETED(src) || !src.client || this_is_like_playing_right != "Yes")
- ready = PLAYER_NOT_READY
- src << browse(null, "window=playersetup") //closes the player setup window
- return FALSE
-
- var/mob/dead/observer/observer = new()
- spawning = TRUE
-
- observer.started_as_observer = TRUE
- close_spawn_windows()
- var/obj/effect/landmark/observer_start/O = locate(/obj/effect/landmark/observer_start) in GLOB.landmarks_list
- to_chat(src, span_notice("Now teleporting."))
- if (O)
- observer.forceMove(O.loc)
- else
- to_chat(src, span_notice("Teleporting failed. Ahelp an admin please"))
- stack_trace("There's no freaking observer landmark available on this map or you're making observers before the map is initialised")
- observer.key = key
- observer.client = client
- observer.set_ghost_appearance()
- if(observer.client && observer.client.prefs)
- observer.real_name = observer.client.prefs.real_name
- observer.name = observer.real_name
- observer.client.init_verbs()
- observer.update_appearance()
- observer.stop_sound_channel(CHANNEL_LOBBYMUSIC)
- deadchat_broadcast(" has observed.", "[observer.real_name] ", follow_target = observer, turf_target = get_turf(observer), message_type = DEADCHAT_DEATHRATTLE)
- QDEL_NULL(mind)
- qdel(src)
- return TRUE
-
-/proc/get_job_unavailable_error_message(retval, jobtitle)
- switch(retval)
- if(JOB_AVAILABLE)
- return "[jobtitle] is available."
- if(JOB_UNAVAILABLE_GENERIC)
- return "[jobtitle] is unavailable."
- if(JOB_UNAVAILABLE_BANNED)
- return "You are currently banned from [jobtitle]."
- if(JOB_NOT_VETERAN)
- return "You need to be veteran to join as [jobtitle]."
- if(JOB_UNAVAILABLE_PLAYTIME)
- return "You do not have enough relevant playtime for [jobtitle]."
- if(JOB_UNAVAILABLE_ACCOUNTAGE)
- return "Your account is not old enough for [jobtitle]."
- if(JOB_UNAVAILABLE_SLOTFULL)
- return "[jobtitle] is already filled to capacity."
- if(JOB_UNAVAILABLE_QUIRK)
- return "[jobtitle] is restricted from your quirks."
- if(JOB_UNAVAILABLE_SPECIES)
- return "[jobtitle] is restricted from your species."
- if(JOB_UNAVAILABLE_LANGUAGE)
- return "You don't have the required languages for [jobtitle]."
- return "Error: Unknown job availability."
-
-/mob/dead/new_player/proc/IsJobUnavailable(rank, latejoin = FALSE)
- var/datum/job/job = SSjob.GetJob(rank)
- if(!(job.job_flags & JOB_NEW_PLAYER_JOINABLE))
- return JOB_UNAVAILABLE_GENERIC
- if((job.current_positions >= job.total_positions) && job.total_positions != -1)
- if(is_assistant_job(job))
- if(isnum(client.player_age) && client.player_age <= 14) //Newbies can always be assistants
- return JOB_AVAILABLE
- for(var/datum/job/other_job as anything in SSjob.joinable_occupations)
- if(other_job.current_positions < other_job.total_positions && other_job != job)
- return JOB_UNAVAILABLE_SLOTFULL
- else
- return JOB_UNAVAILABLE_SLOTFULL
- if(is_banned_from(ckey, rank))
- return JOB_UNAVAILABLE_BANNED
- if(QDELETED(src))
- return JOB_UNAVAILABLE_GENERIC
- if(!job.player_old_enough(client))
- return JOB_UNAVAILABLE_ACCOUNTAGE
- if(job.required_playtime_remaining(client))
- return JOB_UNAVAILABLE_PLAYTIME
- if(job.has_banned_quirk(client.prefs))
- return JOB_UNAVAILABLE_QUIRK
- if(job.has_banned_species(client.prefs))
- return JOB_UNAVAILABLE_SPECIES
- if(latejoin && !job.special_check_latejoin(client))
- return JOB_UNAVAILABLE_GENERIC
- if(job.has_banned_quirk(client.prefs))
- return JOB_UNAVAILABLE_QUIRK
- if(job.has_banned_species(client.prefs))
- return JOB_UNAVAILABLE_SPECIES
- if(!job.has_required_languages(client.prefs))
- return JOB_UNAVAILABLE_LANGUAGE
- if(job.veteran_only && !is_veteran_player(client))
- return JOB_NOT_VETERAN
- return JOB_AVAILABLE
-
-/mob/dead/new_player/proc/AttemptLateSpawn(rank)
- var/error = IsJobUnavailable(rank)
- if(error != JOB_AVAILABLE)
- tgui_alert(usr, get_job_unavailable_error_message(error, rank))
- return FALSE
-
- if(SSshuttle.arrivals)
- close_spawn_windows() //In case we get held up
- if(SSshuttle.arrivals.damaged && CONFIG_GET(flag/arrivals_shuttle_require_safe_latejoin))
- src << tgui_alert(usr,"The arrivals shuttle is currently malfunctioning! You cannot join.")
- return FALSE
-
- if(CONFIG_GET(flag/arrivals_shuttle_require_undocked))
- SSshuttle.arrivals.RequireUndocked(src)
-
- //Remove the player from the join queue if he was in one and reset the timer
- SSticker.queued_players -= src
- SSticker.queue_delay = 4
-
- var/datum/job/job = SSjob.GetJob(rank)
-
- SSjob.AssignRole(src, job, TRUE)
-
- mind.late_joiner = TRUE
- var/atom/destination = mind.assigned_role.get_latejoin_spawn_point()
- if(!destination)
- CRASH("Failed to find a latejoin spawn point.")
- var/mob/living/character = create_character(destination)
- if(!character)
- CRASH("Failed to create a character for latejoin.")
- transfer_character()
-
- SSjob.EquipRank(character, job, character.client)
- job.after_latejoin_spawn(character)
-
- #define IS_NOT_CAPTAIN 0
- #define IS_ACTING_CAPTAIN 1
- #define IS_FULL_CAPTAIN 2
- var/is_captain = IS_NOT_CAPTAIN
- // If we already have a captain, are they a "Captain" rank and are we allowing multiple of them to be assigned?
- if(is_captain_job(job))
- is_captain = IS_FULL_CAPTAIN
- // If we don't have an assigned cap yet, check if this person qualifies for some from of captaincy.
- else if(!SSjob.assigned_captain && ishuman(character) && SSjob.chain_of_command[rank] && !is_banned_from(ckey, list("Captain")))
- is_captain = IS_ACTING_CAPTAIN
- if(is_captain != IS_NOT_CAPTAIN)
- minor_announce(job.get_captaincy_announcement(character))
- SSjob.promote_to_captain(character, is_captain == IS_ACTING_CAPTAIN)
- #undef IS_NOT_CAPTAIN
- #undef IS_ACTING_CAPTAIN
- #undef IS_FULL_CAPTAIN
-
- SSticker.minds += character.mind
- character.client.init_verbs() // init verbs for the late join
- var/mob/living/carbon/human/humanc
- if(ishuman(character))
- humanc = character //Let's retypecast the var to be human,
-
- if(humanc) //These procs all expect humans
- GLOB.data_core.manifest_inject(humanc)
- if(SSshuttle.arrivals)
- SSshuttle.arrivals.QueueAnnounce(humanc, rank)
- else
- AnnounceArrival(humanc, rank)
- AddEmploymentContract(humanc)
-
- humanc.increment_scar_slot()
- humanc.load_persistent_scars()
-
- if(GLOB.curse_of_madness_triggered)
- give_madness(humanc, GLOB.curse_of_madness_triggered)
-
- SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CREWMEMBER_JOINED, humanc, rank)
-
- GLOB.joined_player_list += character.ckey
-
- if(CONFIG_GET(flag/allow_latejoin_antagonists) && humanc) //Borgs aren't allowed to be antags. Will need to be tweaked if we get true latejoin ais.
- if(SSshuttle.emergency)
- switch(SSshuttle.emergency.mode)
- if(SHUTTLE_RECALL, SHUTTLE_IDLE)
- SSticker.mode.make_antag_chance(humanc)
- if(SHUTTLE_CALL)
- if(SSshuttle.emergency.timeLeft(1) > initial(SSshuttle.emergencyCallTime)*0.5)
- SSticker.mode.make_antag_chance(humanc)
-
- if(humanc && CONFIG_GET(flag/roundstart_traits))
- SSquirks.AssignQuirks(humanc, humanc.client)
-
- log_manifest(character.mind.key,character.mind,character,latejoin = TRUE)
-
-/mob/dead/new_player/proc/AddEmploymentContract(mob/living/carbon/human/employee)
- //TODO: figure out a way to exclude wizards/nukeops/demons from this.
- for(var/C in GLOB.employmentCabinets)
- var/obj/structure/filingcabinet/employment/employmentCabinet = C
- if(!employmentCabinet.virgin)
- employmentCabinet.addFile(employee)
-
-
-/mob/dead/new_player/proc/LateChoices()
- var/list/dat = list()
- if(SSlag_switch.measures[DISABLE_NON_OBSJOBS])
- dat += "Only Observers may join at this time.
"
- dat += "Round Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]
"
- dat += "Alert Level: [capitalize(num2seclevel(SSsecurity_level.current_level))]
"
- if(SSshuttle.emergency)
- switch(SSshuttle.emergency.mode)
- if(SHUTTLE_ESCAPE)
- dat += "The station has been evacuated.
"
- if(SHUTTLE_CALL)
- if(!SSshuttle.canRecall())
- dat += "The station is currently undergoing evacuation procedures.
"
- for(var/datum/job/prioritized_job in SSjob.prioritized_jobs)
- if(prioritized_job.current_positions >= prioritized_job.total_positions)
- SSjob.prioritized_jobs -= prioritized_job
- dat += ""
- var/column_counter = 0
-
- for(var/datum/job_department/department as anything in SSjob.joinable_departments)
- var/department_color = department.latejoin_color
- dat += ""
- dat += "[department.department_name] "
- var/list/dept_data = list()
- for(var/datum/job/job_datum as anything in department.department_jobs)
- if(IsJobUnavailable(job_datum.title, TRUE) != JOB_AVAILABLE)
- continue
- var/jobline = ""
- var/command_bold = ""
- if((job_datum.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) || (job_datum.departments_bitflags & DEPARTMENT_BITFLAG_NANOTRASEN_FLEET_COMMAND))
- command_bold = " command"
- if(job_datum in SSjob.prioritized_jobs)
- jobline = "[job_datum.title] ([job_datum.current_positions]) "
- else
- jobline = "[job_datum.title] ([job_datum.current_positions]) "
-
- if(client && client.prefs && client.prefs.alt_titles_preferences[job_datum.title])
- jobline += "(as [client.prefs.alt_titles_preferences[job_datum.title]]) "
- dept_data += jobline
-
- if(!length(dept_data))
- dept_data += "No positions open. "
- dat += dept_data.Join()
- dat += " "
- column_counter++
- if(column_counter > 0 && (column_counter % 3 == 0))
- dat += " "
- dat += "
"
- dat += ""
- var/datum/browser/popup = new(src, "latechoices", "Choose Profession", 1080, 680)
- popup.add_stylesheet("playeroptions", 'html/browser/playeroptions.css')
- popup.set_content(jointext(dat, ""))
- popup.open(FALSE) // 0 is passed to open so that it doesn't use the onclose() proc
-
-
-/// Creates, assigns and returns the new_character to spawn as. Assumes a valid mind.assigned_role exists.
-/mob/dead/new_player/proc/create_character(atom/destination)
- spawning = TRUE
- close_spawn_windows()
-
- mind.active = FALSE //we wish to transfer the key manually
- var/mob/living/spawning_mob = mind.assigned_role.get_spawn_mob(client, destination)
- if(QDELETED(src) || !client)
- return // Disconnected while checking for the appearance ban.
- if(iscyborg(spawning_mob))
- var/mob/living/silicon/robot/cyborg = spawning_mob
- cyborg.latejoin_find_parent_ai(destination.z)
- if(!isAI(spawning_mob)) // Unfortunately there's still snowflake AI code out there.
- mind.original_character_slot_index = client.prefs.default_slot
- mind.transfer_to(spawning_mob) //won't transfer key since the mind is not active
- mind.set_original_character(spawning_mob)
- client.init_verbs()
- . = spawning_mob
- new_character = .
-
-
-/mob/dead/new_player/proc/transfer_character()
- . = new_character
- if(!.)
- return
- new_character.key = key //Manually transfer the key to log them in,
- new_character.stop_sound_channel(CHANNEL_LOBBYMUSIC)
- var/area/joined_area = get_area(new_character.loc)
- if(joined_area)
- joined_area.on_joining_game(new_character)
- new_character = null
- qdel(src)
-
-
-/mob/dead/new_player/proc/ViewManifest()
- if(!client)
- return
- if(world.time < client.crew_manifest_delay)
- return
- client.crew_manifest_delay = world.time + (1 SECONDS)
-
- if(!GLOB.crew_manifest_tgui)
- GLOB.crew_manifest_tgui = new /datum/crew_manifest(src)
-
-
- GLOB.crew_manifest_tgui.ui_interact(src)
-
-/mob/dead/new_player/Move()
- return 0
-
-
-/mob/dead/new_player/proc/close_spawn_windows()
-
- src << browse(null, "window=latechoices") //closes late choices window
- src << browse(null, "window=playersetup") //closes the player setup window
- src << browse(null, "window=preferences") //closes job selection
- src << browse(null, "window=mob_occupation")
- src << browse(null, "window=latechoices") //closes late job selection
-
-// Used to make sure that a player has a valid job preference setup, used to knock players out of eligibility for anything if their prefs don't make sense.
-// A "valid job preference setup" in this situation means at least having one job set to low, or not having "return to lobby" enabled
-// Prevents "antag rolling" by setting antag prefs on, all jobs to never, and "return to lobby if preferences not available"
-// Doing so would previously allow you to roll for antag, then send you back to lobby if you didn't get an antag role
-// This also does some admin notification and logging as well, as well as some extra logic to make sure things don't go wrong
-/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)
- 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
- if(client.prefs.be_special.len > 0)
- has_antags = TRUE
- if(client.prefs.job_preferences.len == 0)
- if(!ineligible_for_roles)
- to_chat(src, span_danger("You have no jobs enabled, along with return to lobby if job is unavailable. This makes you ineligible for any round start role, please update your job preferences."))
- ineligible_for_roles = TRUE
- ready = PLAYER_NOT_READY
- if(has_antags)
- log_admin("[src.ckey] has no jobs enabled, return to lobby if job is unavailable enabled and [client.prefs.be_special.len] antag preferences enabled. The player has been forcefully returned to the lobby.")
- message_admins("[src.ckey] has no jobs enabled, return to lobby if job is unavailable enabled and [client.prefs.be_special.len] antag preferences enabled. This is an old antag rolling technique. The player has been asked to update their job preferences and has been forcefully returned to the lobby.")
- return FALSE //This is the only case someone should actually be completely blocked from antag rolling as well
- return TRUE
-
-/**
- * Prepares a client for the interview system, and provides them with a new interview
- *
- * This proc will both prepare the user by removing all verbs from them, as well as
- * giving them the interview form and forcing it to appear.
- */
-/mob/dead/new_player/proc/register_for_interview()
- // First we detain them by removing all the verbs they have on client
- for (var/v in client.verbs)
- var/procpath/verb_path = v
- if (!(verb_path in GLOB.stat_panel_verbs))
- remove_verb(client, verb_path)
-
- // Then remove those on their mob as well
- for (var/v in verbs)
- var/procpath/verb_path = v
- if (!(verb_path in GLOB.stat_panel_verbs))
- remove_verb(src, verb_path)
-
- // Then we create the interview form and show it to the client
- var/datum/interview/I = GLOB.interviews.interview_for_client(client)
- if (I)
- I.ui_interact(src)
-
- // Add verb for re-opening the interview panel, and re-init the verbs for the stat panel
- add_verb(src, /mob/dead/new_player/proc/open_interview)
diff --git a/modular_skyrat/master_files/code/modules/mob/living/human/species.dm b/modular_skyrat/master_files/code/modules/mob/living/human/species.dm
new file mode 100644
index 00000000000..58f6b66f197
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/mob/living/human/species.dm
@@ -0,0 +1,22 @@
+/// 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/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 default_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
diff --git a/modular_skyrat/master_files/code/modules/mob/living/simple_animal/hostile/zombie.dm b/modular_skyrat/master_files/code/modules/mob/living/simple_animal/hostile/zombie.dm
index 8fc687592d2..b3b33b2ff26 100644
--- a/modular_skyrat/master_files/code/modules/mob/living/simple_animal/hostile/zombie.dm
+++ b/modular_skyrat/master_files/code/modules/mob/living/simple_animal/hostile/zombie.dm
@@ -30,7 +30,7 @@
//They have claws now.
O.r_hand = null
O.l_hand = null
-
+/*
var/icon/P = get_flat_human_icon_skyrat("zombie_[picked_job]", J, /datum/species/zombie/infectious, SPECIES_ZOMBIE_HALLOWEEN, outfit_override = O)
icon = P
if(!no_corpse)
@@ -38,3 +38,4 @@
corpse.outfit = O
corpse.mob_species = /datum/species/zombie
corpse.mob_name = name
+*/
diff --git a/modular_skyrat/master_files/sound/effects/metalblock1.wav b/modular_skyrat/master_files/sound/effects/metalblock1.wav
new file mode 100644
index 00000000000..85a7302d323
Binary files /dev/null and b/modular_skyrat/master_files/sound/effects/metalblock1.wav differ
diff --git a/modular_skyrat/master_files/sound/effects/metalblock2.wav b/modular_skyrat/master_files/sound/effects/metalblock2.wav
new file mode 100644
index 00000000000..2af9c33c6fc
Binary files /dev/null and b/modular_skyrat/master_files/sound/effects/metalblock2.wav differ
diff --git a/modular_skyrat/master_files/sound/effects/metalblock3.wav b/modular_skyrat/master_files/sound/effects/metalblock3.wav
new file mode 100644
index 00000000000..b1f9f51eae1
Binary files /dev/null and b/modular_skyrat/master_files/sound/effects/metalblock3.wav differ
diff --git a/modular_skyrat/master_files/sound/effects/metalblock4.wav b/modular_skyrat/master_files/sound/effects/metalblock4.wav
new file mode 100644
index 00000000000..39ea0a93e4a
Binary files /dev/null and b/modular_skyrat/master_files/sound/effects/metalblock4.wav differ
diff --git a/modular_skyrat/master_files/sound/effects/metalblock5.wav b/modular_skyrat/master_files/sound/effects/metalblock5.wav
new file mode 100644
index 00000000000..4bc6bf3e993
Binary files /dev/null and b/modular_skyrat/master_files/sound/effects/metalblock5.wav differ
diff --git a/modular_skyrat/master_files/sound/effects/metalblock6.wav b/modular_skyrat/master_files/sound/effects/metalblock6.wav
new file mode 100644
index 00000000000..d4eaccbeb13
Binary files /dev/null and b/modular_skyrat/master_files/sound/effects/metalblock6.wav differ
diff --git a/modular_skyrat/master_files/sound/effects/metalblock7.wav b/modular_skyrat/master_files/sound/effects/metalblock7.wav
new file mode 100644
index 00000000000..49b524469cb
Binary files /dev/null and b/modular_skyrat/master_files/sound/effects/metalblock7.wav differ
diff --git a/modular_skyrat/master_files/sound/effects/metalblock8.wav b/modular_skyrat/master_files/sound/effects/metalblock8.wav
new file mode 100644
index 00000000000..99feed72fc4
Binary files /dev/null and b/modular_skyrat/master_files/sound/effects/metalblock8.wav differ
diff --git a/modular_skyrat/modules/Midroundtraitor/event.dm b/modular_skyrat/modules/Midroundtraitor/event.dm
index 87558d549c2..591190d7e88 100644
--- a/modular_skyrat/modules/Midroundtraitor/event.dm
+++ b/modular_skyrat/modules/Midroundtraitor/event.dm
@@ -1,7 +1,7 @@
/datum/dynamic_ruleset/midround/from_ghosts/lone_infiltrator
name = "Lone Infiltrator"
antag_datum = /datum/antagonist/traitor/infiltrator
- antag_flag = ROLE_TRAITOR
+ antag_flag = ROLE_LONE_INFILTRATOR
restricted_roles = list("Cyborg", "AI", "Positronic Brain")
required_candidates = 1
weight = 5 //Slightly less common than normal midround traitors.
diff --git a/modular_skyrat/modules/admin/code/aooc.dm b/modular_skyrat/modules/admin/code/aooc.dm
index f4948c3d31d..0de764331d3 100644
--- a/modular_skyrat/modules/admin/code/aooc.dm
+++ b/modular_skyrat/modules/admin/code/aooc.dm
@@ -73,7 +73,7 @@ GLOBAL_LIST_EMPTY(ckey_to_aooc_name)
for(var/c in listeners)
var/client/C = c
var/mode = listeners[c]
- var/color = (!anon && CONFIG_GET(flag/allow_admin_ooccolor) && C.prefs.ooccolor) ? C.prefs.ooccolor : GLOB.AOOC_COLOR
+ var/color = (!anon && CONFIG_GET(flag/allow_admin_ooccolor) && C.prefs?.read_preference(/datum/preference/color/ooc_color)) ? C.prefs?.read_preference(/datum/preference/color/ooc_color) : GLOB.AOOC_COLOR
var/name = (mode == AOOC_LISTEN_ADMIN && anon) ? "([key])[keyname]" : keyname
to_chat(C, "AOOC: [name]: [msg] ")
diff --git a/modular_skyrat/modules/admin/code/loud_say.dm b/modular_skyrat/modules/admin/code/loud_say.dm
index e58f213398f..1746533903f 100644
--- a/modular_skyrat/modules/admin/code/loud_say.dm
+++ b/modular_skyrat/modules/admin/code/loud_say.dm
@@ -13,7 +13,7 @@
mob.log_talk(msg, LOG_ASAY)
msg = keywords_lookup(msg)
- var/custom_asay_color = (CONFIG_GET(flag/allow_admin_asaycolor) && prefs.asaycolor) ? "" : ""
+ var/custom_asay_color = (CONFIG_GET(flag/allow_admin_asaycolor) && prefs?.read_preference(/datum/preference/color/asay_color)) ? "" : ""
msg = " ADMIN: [key_name(usr, 1)] [ADMIN_FLW(mob)]: [custom_asay_color][msg] [custom_asay_color ? " ":null]"
to_chat(GLOB.admins,
type = MESSAGE_TYPE_ADMINCHAT,
diff --git a/modular_skyrat/modules/admin/code/sooc.dm b/modular_skyrat/modules/admin/code/sooc.dm
index a4b0fc9c5a2..39f259abbef 100644
--- a/modular_skyrat/modules/admin/code/sooc.dm
+++ b/modular_skyrat/modules/admin/code/sooc.dm
@@ -74,7 +74,7 @@ GLOBAL_LIST_EMPTY(ckey_to_sooc_name)
for(var/c in listeners)
var/client/C = c
var/mode = listeners[c]
- var/color = (!anon && CONFIG_GET(flag/allow_admin_ooccolor) && C.prefs.ooccolor) ? C.prefs.ooccolor : GLOB.SOOC_COLOR
+ var/color = (!anon && CONFIG_GET(flag/allow_admin_ooccolor) && C.prefs?.read_preference(/datum/preference/color/ooc_color)) ? C.prefs?.read_preference(/datum/preference/color/ooc_color) : GLOB.SOOC_COLOR
var/name = (mode == SOOC_LISTEN_ADMIN && anon) ? "([key])[keyname]" : keyname
to_chat(C, "SOOC: [name]: [msg] ")
diff --git a/modular_skyrat/modules/alerts/code/set_security_level.dm b/modular_skyrat/modules/alerts/code/set_security_level.dm
index 31986d4c358..d2b34edb818 100644
--- a/modular_skyrat/modules/alerts/code/set_security_level.dm
+++ b/modular_skyrat/modules/alerts/code/set_security_level.dm
@@ -175,9 +175,9 @@ GLOBAL_VAR_INIT(sec_level_cooldown, FALSE)
else
var/area/A = get_area(M)
if(is_type_in_typecache(A, quiet_areas)) //These areas don't hear it as loudly
- M.playsound_local(get_turf(M), S, min(10, M.client.prefs.announcement_volume), FALSE)
+ M.playsound_local(get_turf(M), S, 10, FALSE)
else
- M.playsound_local(get_turf(M), S, M.client.prefs.announcement_volume, FALSE)
+ M.playsound_local(get_turf(M), S, 70, FALSE)
#undef GAMMA_LOOP_LENGTH
#undef SET_SEC_LEVEL_COOLDOWN
diff --git a/modular_skyrat/modules/altjobtitles/code/_job.dm b/modular_skyrat/modules/altjobtitles/code/_job.dm
deleted file mode 100644
index dfd2250610a..00000000000
--- a/modular_skyrat/modules/altjobtitles/code/_job.dm
+++ /dev/null
@@ -1,150 +0,0 @@
-/mob/living
- /// used to hold alt titles because apparently a round-start player applies post-equip before there's a goddamn client in the mob AAAAAAAAAAAAAAAAA
- var/alt_title_holder
-
-/datum/job/proc/announce_head(mob/living/carbon/human/H, 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)
- if(H.alt_title_holder)
- //timer because these should come after the captain announcement
- SSticker.OnRoundstart(CALLBACK(GLOBAL_PROC, .proc/_addtimer, CALLBACK(pick(GLOB.announcement_systems), /obj/machinery/announcement_system/proc/announce, "NEWHEAD", H.real_name, H.alt_title_holder, channels), 1))
- else
- SSticker.OnRoundstart(CALLBACK(GLOBAL_PROC, .proc/_addtimer, CALLBACK(pick(GLOB.announcement_systems), /obj/machinery/announcement_system/proc/announce, "NEWHEAD", H.real_name, H.job, channels), 1))
-
-//Central Command
-/datum/job/admiral
- alt_titles = list("Fleet Admiral", "Fleet Commander")
-
-/datum/job/nanotrasen_representative
- alt_titles = list("Nanotrasen Diplomat", "Central Command Representative")
-
-/datum/job/blueshield
- alt_titles = list("Command Bodyguard", "Executive Protection Agent", "Personal Protection Specialist", "Asset Retention Agent")
-
-//Command
-/datum/job/captain
- alt_titles = list("Station Commander", "Commanding Officer", "Site Manager")
-
-/datum/job/head_of_personnel
- alt_titles = list("Executive Officer", "Employment Officer", "Crew Supervisor")
-
-/datum/job/head_of_security
- alt_titles = list("Security Commander", "Chief Constable", "Chief of Security", "Sheriff")
-
-/datum/job/chief_engineer
- alt_titles = list("Engineering Foreman")
-
-/datum/job/research_director
- alt_titles = list("Silicon Administrator", "Lead Researcher", "Biorobotics Director", "Research Supervisor", "Chief Science Officer")
-
-/datum/job/quartermaster
- alt_titles = list("Deck Chief", "Cargo Foreman")
-
-/datum/job/chief_medical_officer
- alt_titles = list("Medical Director")
-
-//Engineering
-/datum/job/station_engineer
- alt_titles = list("Emergency Damage Control Technician", "Electrician", "Engine Technician", "EVA Technician", "Engineer")
-
-/datum/job/atmospheric_technician
- alt_titles = list("Life Support Technician", "Emergency Fire Technician")
-
-//Medical
-/datum/job/doctor
- alt_titles = list("Surgeon", "Nurse")
-
-/datum/job/paramedic
- alt_titles = list("Emergency Medical Technician", "Search and Rescue Technician")
-
-/datum/job/virologist
- alt_titles = list("Pathologist")
-
-/datum/job/chemist
- alt_titles = list("Pharmacist", "Pharmacologist")
-
-//Science
-/datum/job/scientist
- alt_titles = list("Circuitry Designer", "Xenobiologist", "Cytologist", "Plasma Researcher", "Anomalist", "Lab Technician", "Xenoarchaeologist")
-
-/datum/job/roboticist
- alt_titles = list("Biomechanical Engineer", "Mechatronic Engineer")
-
-/datum/job/geneticist
- alt_titles = list("Mutation Researcher")
-
-/datum/job/expeditionary_trooper // taken from chesify's classes update. miss 'em
- alt_titles = list("Vanguard Medic", "Vanguard Technician", "Vanguard Pointman", "Vanguard Marksman")
-
-//Cargo
-/datum/job/cargo_technician
- alt_titles = list("Deck Worker", "Mailman")
-
-/datum/job/shaft_miner
- alt_titles = list("Excavator")
-
-//Service
-/datum/job/bartender
- alt_titles = list("Mixologist", "Barkeeper")
-
-/datum/job/cook
- alt_titles = list("Chef", "Butcher", "Culinary Artist", "Sous-Chef")
-
-/datum/job/janitor
- alt_titles = list("Custodian", "Custodial Technicial", "Sanitation Technician", "Maid")
-
-/datum/job/curator
- alt_titles = list("Librarian", "Journalist", "Archivist")
-
-/datum/job/psychologist
- alt_titles = list("Psychiatrist", "Therapist", "Counsellor")
-
-/datum/job/lawyer
- alt_titles = list("Internal Affairs Agent", "Human Resources Agent", "Defence Attorney", "Public Defender", "Barrister", "Prosecutor")
-
-/datum/job/chaplain
- alt_titles = list("Priest", "Preacher")
-
-/datum/job/mime
- alt_titles = list("Pantomimist")
-
-/datum/job/clown
- alt_titles = list("Jester")
-
-/datum/job/botanist
- alt_titles = list("Hydroponicist", "Gardener", "Botanical Researcher", "Herbalist")
-
-//Security
-/datum/job/warden
- alt_titles = list("Brig Sergeant", "Dispatch Officer", "Brig Governor", "Jailer")
-
-/datum/job/detective
- alt_titles = list("Forensic Technician", "Private Investigator", "Forensic Scientist")
-
-/datum/job/security_officer
- alt_titles = list("Security Operative", "Peacekeeper")
-
-/datum/job/security_sergeant
- alt_titles = list("Security Squad Leader", "Security Task Force Leader", "Security Fireteam Leader", "Security Enforcer")
-
-/datum/job/security_medic
- alt_titles = list("Field Medic", "Security Corpsman", "Brig Physician")
-
-/datum/job/junior_officer
- alt_titles = list("Station Police", "Civil Protection Officer", "Parole Officer")
-
-/datum/job/brigoff
- alt_titles = list("Brig Officer", "Prison Guard")
-
-//Civillian
-/datum/job/prisoner
- alt_titles = list("Low Risk Prisoner", "High Risk Prisoner", "Extreme Risk Prisoner", "Protective Custody Prisoner")
-
-/datum/job/assistant
- alt_titles = list("Civilian", "Tourist", "Businessman", "Trader", "Entertainer", "Off-Duty Staff", "Freelancer")
-
-//Silicon
-/datum/job/ai
- alt_titles = list("Station Intelligence", "Automated Overseer")
-
-/datum/job/cyborg
- alt_titles = list("Robot", "Android")
diff --git a/modular_skyrat/modules/altjobtitles/code/cards_ids.dm b/modular_skyrat/modules/altjobtitles/code/cards_ids.dm
deleted file mode 100644
index cb27fd5c6d9..00000000000
--- a/modular_skyrat/modules/altjobtitles/code/cards_ids.dm
+++ /dev/null
@@ -1,2 +0,0 @@
-/obj/item/card/id
- var/real_title
diff --git a/modular_skyrat/modules/altjobtitles/code/game.dm b/modular_skyrat/modules/altjobtitles/code/game.dm
deleted file mode 100644
index 04af22d6de7..00000000000
--- a/modular_skyrat/modules/altjobtitles/code/game.dm
+++ /dev/null
@@ -1,17 +0,0 @@
-/proc/AnnounceArrival(mob/living/carbon/human/character, rank)
- if(!SSticker.IsRoundInProgress() || QDELETED(character))
- return
- var/area/A = get_area(character)
- deadchat_broadcast(" has arrived at the station at [A.name] . ", "[character.real_name] ([rank]) ", follow_target = character, message_type=DEADCHAT_ARRIVALRATTLE)
- if(!character.mind)
- return
- if(!GLOB.announcement_systems.len)
- return
- if((character.mind.assigned_role == "Cyborg") || (character.mind.assigned_role == character.mind.special_role))
- return
-
- var/displayed_rank = rank
- if(character.client && character.client.prefs && character.client.prefs.alt_titles_preferences[rank])
- displayed_rank = character.client.prefs.alt_titles_preferences[rank]
- var/obj/machinery/announcement_system/announcer = pick(GLOB.announcement_systems)
- announcer.announce("ARRIVAL", character.real_name, displayed_rank, list()) //make the list empty to make it announce it in common
diff --git a/modular_skyrat/modules/altjobtitles/readme.md b/modular_skyrat/modules/altjobtitles/readme.md
deleted file mode 100644
index c3c88b1e9db..00000000000
--- a/modular_skyrat/modules/altjobtitles/readme.md
+++ /dev/null
@@ -1,41 +0,0 @@
-https://github.com/Skyrat-SS13/Skyrat-tg/pull/5150
-
-## Title: Alternative Job Titles
-
-MODULE ID: ALTJOBTITLES
-
-### Description:
-
-Ports alternative job titles from oldbase, making the necessary changes to make it actually work.
-
-### TG Proc Changes:
-
-- /proc/AnnounceArrival() - (code/_HELPERS/game.dm) - Moved to modular
-- /datum/controller/subsystem/job/proc/EquipRank() - (code/controllers/subsystem/job.dm)
-- /datum/datacore/proc/get_manifest() - (code/datums/datacore.dm) - Moved to modular
-- /datum/datacore/proc/manifest_inject() - (code/datums/datacore.dm)
-- /datum/crewmonitor/proc/update_data() - (code/game/machinery/computer/crew.dm)
-- /datum/job/proc/equip() - (code/modules/jobs/job_types/_job.dm)
-- /datum/outfit/job/post_equip() - (code/modules/jobs/job_types/_job.dm)
-- /datum/job/proc/announce_head() - (code/modules/jobs/job_types/_job.dm) - Moved to modular
-- /datum/job/security_officer/after_spawn() - (code/modules/jobs/job_types/security_officer.dm)
-
-### Defines:
-(i don't know if these count but)
-- Global lists for each department's alt title (command_altttles, security_alttitles etc.)
-
-### Master file additions
-
-- N/A
-
-### Edited files that are not contained in this module:
-
-- modular_skyrat/master_files/code/modules/mob/living/dead/new_player/new_player.dm - Updated latejoin job selection
-- modular_skyrat/modules/customization/modules/client/preferences.dm - Title selection
-- modular_skyrat/modules/customization/modules/client/preferences_savefile.dm - Saving of title selection
-
-### Credits:
-FlamingLily - Porting and Refactoring
-
-Azarak - OG code - https://github.com/Skyrat-SS13/Skyrat13/pull/1887
-
diff --git a/modular_skyrat/modules/customization/__HELPERS/global_lists.dm b/modular_skyrat/modules/customization/__HELPERS/global_lists.dm
index e17f955fbe4..368f3fcf0d9 100644
--- a/modular_skyrat/modules/customization/__HELPERS/global_lists.dm
+++ b/modular_skyrat/modules/customization/__HELPERS/global_lists.dm
@@ -3,7 +3,6 @@
make_body_marking_references()
make_body_marking_set_references()
make_body_marking_dna_block_references()
- make_loadout_references()
make_augment_references()
make_culture_references()
//We're loading donators here because it's the least intrusive way modularly
@@ -78,19 +77,6 @@
GLOB.dna_body_marking_blocks[marking_zone] = GLOB.dna_total_feature_blocks+1
GLOB.dna_total_feature_blocks += DNA_BLOCKS_PER_MARKING_ZONE
-/proc/make_loadout_references()
- // Here we build the global loadout lists
- for(var/path in subtypesof(/datum/loadout_item))
- var/datum/loadout_item/L = path
- if(initial(L.path))
- L = new path()
- GLOB.loadout_items[L.path] = L
- if(!GLOB.loadout_category_to_subcategory_to_items[L.category])
- GLOB.loadout_category_to_subcategory_to_items[L.category] = list()
- if(!GLOB.loadout_category_to_subcategory_to_items[L.category][L.subcategory])
- GLOB.loadout_category_to_subcategory_to_items[L.category][L.subcategory] = list()
- GLOB.loadout_category_to_subcategory_to_items[L.category][L.subcategory] += L.path
-
/proc/make_augment_references()
// Here we build the global loadout lists
for(var/path in subtypesof(/datum/augment_item))
diff --git a/modular_skyrat/modules/customization/datums/dna.dm b/modular_skyrat/modules/customization/datums/dna.dm
index 310f6619dd9..7a092049dfe 100644
--- a/modular_skyrat/modules/customization/datums/dna.dm
+++ b/modular_skyrat/modules/customization/datums/dna.dm
@@ -132,7 +132,7 @@
holder.transform = holder.transform.Translate(0, translate)
current_body_size = features["body_size"]
-/mob/living/carbon/set_species(datum/species/mrace, icon_update = TRUE, var/datum/preferences/pref_load, var/list/override_features, var/list/override_mutantparts, var/list/override_markings, retain_features = FALSE, retain_mutantparts = FALSE)
+/mob/living/carbon/set_species(datum/species/mrace, icon_update = TRUE, var/list/override_features, var/list/override_mutantparts, var/list/override_markings, retain_features = FALSE, retain_mutantparts = FALSE)
if(QDELETED(src))
CRASH("You're trying to change your species post deletion, this is a recipe for madness")
if(mrace && has_dna())
@@ -144,48 +144,35 @@
else
return
deathsound = new_race.deathsound
- dna.species.on_species_loss(src, new_race, pref_load)
+ dna.species.on_species_loss(src, new_race)
var/datum/species/old_species = dna.species
dna.species = new_race
- //BODYPARTS AND FEATURES
- if(pref_load)
- dna.features = pref_load.features.Copy()
- dna.real_name = pref_load.real_name
- dna.mutant_bodyparts = pref_load.mutant_bodyparts.Copy()
- dna.body_markings = pref_load.body_markings.Copy()
- dna.species.body_markings = pref_load.body_markings.Copy()
- else
- if(!retain_features)
- dna.features = override_features || new_race.get_random_features()
- if(retain_mutantparts)
- var/list/list/new_list = new_race.get_random_mutant_bodyparts(dna.features)
- var/list/compiled_list = list()
- for(var/key in new_list)
- if(dna.mutant_bodyparts[key])
- compiled_list[key] = dna.mutant_bodyparts[key].Copy()
- else
- compiled_list[key] = new_list[key].Copy()
- else
- dna.mutant_bodyparts = override_mutantparts || new_race.get_random_mutant_bodyparts(dna.features)
- dna.body_markings = override_markings || new_race.get_random_body_markings(dna.features)
- dna.species.body_markings = dna.body_markings.Copy()
- copy_mutant_parts_to_species()
+ //BODYPARTS AND FEATURES - We need to instantiate the list with compatible mutant parts so we don't break things
+ dna.mutant_bodyparts = override_mutantparts || new_race.get_random_mutant_bodyparts(dna.features)
+ dna.body_markings = override_markings || new_race.get_random_body_markings(dna.features)
+ dna.features = override_features || new_race.get_random_features(dna.features)
+ //END OF BODYPARTS AND FEATURES
+
+ apply_customizable_dna_features_to_species()
dna.unique_features = dna.generate_unique_features()
dna.update_body_size()
- dna.species.on_species_gain(src, old_species, pref_load)
- //END OF BODYPARTS AND FEATURES
+ dna.species.on_species_gain(src, old_species)
+
+
if(ishuman(src))
qdel(language_holder)
var/species_holder = initial(mrace.species_language_holder)
- language_holder = new species_holder(src, pref_load)
+ language_holder = new species_holder(src)
update_atom_languages()
-/mob/living/carbon/proc/copy_mutant_parts_to_species()
+
+/mob/living/carbon/proc/apply_customizable_dna_features_to_species()
if(!has_dna())
CRASH("[src] does not have DNA")
+ dna.species.body_markings = dna.body_markings.Copy()
var/list/bodyparts_to_add = dna.mutant_bodyparts.Copy()
for(var/key in bodyparts_to_add)
var/datum/sprite_accessory/SP = GLOB.sprite_accessories[key][bodyparts_to_add[key][MUTANT_INDEX_NAME]]
@@ -214,36 +201,6 @@
dna.features["ethcolor"] = sanitize_hexcolor(getblock(features, DNA_ETHEREAL_COLOR_BLOCK))
if(dna.features["skin_color"])
dna.features["skin_color"] = sanitize_hexcolor(getblock(features, DNA_SKIN_COLOR_BLOCK))
- for(var/key in GLOB.genetic_accessories)
- if(dna.mutant_bodyparts[key] && (dna.mutant_bodyparts[key][MUTANT_INDEX_NAME] in GLOB.genetic_accessories[key]))
- var/bodypart_block = GLOB.dna_mutant_bodypart_blocks[key]
- var/list/accessories_for_key = GLOB.sprite_accessories[key]
- var/list/possible_accessories = GLOB.genetic_accessories[key]
- var/accessory_name = GLOB.genetic_accessories[key][deconstruct_block(getblock(features, bodypart_block), possible_accessories.len)]
- dna.mutant_bodyparts[key][MUTANT_INDEX_NAME] = accessory_name
- var/datum/sprite_accessory/accessory_to_apply = accessories_for_key[accessory_name]
- if(accessory_to_apply.factual)
- switch(accessory_to_apply.color_src)
- if(USE_ONE_COLOR)
- dna.mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST] = list(sanitize_hexcolor(getblock(features,bodypart_block+1)))
- if(USE_MATRIXED_COLORS)
- dna.mutant_bodyparts[key][MUTANT_INDEX_COLOR_LIST] = list(sanitize_hexcolor(getblock(features,bodypart_block+1)), sanitize_hexcolor(getblock(features,bodypart_block+2)), sanitize_hexcolor(getblock(features,bodypart_block+3)))
- for(var/zone in GLOB.marking_zones)
- var/marking_count_block = GLOB.dna_body_marking_blocks[zone]
- var/first_marking_block = marking_count_block+1
- var/marking_count = deconstruct_block(getblock(features, marking_count_block), 4)-1
- if(!marking_count && dna.body_markings[zone])
- dna.body_markings -= zone
- if(marking_count)
- if(!dna.body_markings[zone])
- dna.body_markings[zone] = list()
- dna.body_markings[zone].len = marking_count
- for(var/i in 1 to marking_count)
- var/marking = GLOB.body_markings_per_limb[zone][deconstruct_block(getblock(features, first_marking_block+(i-1)*DNA_BLOCKS_PER_MARKING), GLOB.body_markings_per_limb[zone].len)]
- dna.body_markings[zone][i] = marking
- dna.body_markings[zone][marking] = sanitize_hexcolor(getblock(features, first_marking_block+(i-1)*DNA_BLOCKS_PER_MARKING+1))
- dna.species.body_markings = dna.body_markings.Copy()
- copy_mutant_parts_to_species()
if(icon_update)
dna.species.handle_body(src) // We want 'update_body_parts()' to be called only if mutcolor_update is TRUE, so no 'update_body()' here.
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/_loadout.dm b/modular_skyrat/modules/customization/modules/client/loadout/_loadout.dm
deleted file mode 100644
index 4aa8d1643e0..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/_loadout.dm
+++ /dev/null
@@ -1,82 +0,0 @@
-/datum/loadout_item
- var/name
- ///Category in which the item belongs to LOADOUT_CATEGORY_UNIFORM, LOADOUT_CATEGORY_BACKPACK etc.
- var/category = LOADOUT_CATEGORY_NONE
- ///Subcategory in which the item belongs to
- var/subcategory = LOADOUT_SUBCATEGORY_MISC
- ///Description of the loadout item, automatically set by New() if null
- var/description
- ///Typepath to the item being spawned
- var/path
- ///How much loadout points does it cost?
- var/cost = 1
- ///If set, it's a list containing ckeys which only can get the item
- var/list/ckeywhitelist
- ///If set, is a list of job names of which can get the loadout item
- var/list/restricted_roles
- ///Descriptive explanation of the restricted roles, if empty will automatically generate on New() if nessecary
- var/restricted_desc
- ///Extra information which the user can set. (LOADOUT_INFO_NONE, LOADOUT_INFO_STYLE, LOADOUT_INFO_ONE_COLOR, LOADOUT_INFO_THREE_COLORS)
- var/extra_info
- ///Whether the item is restricted to supporters
- var/donator_only
-
-/datum/loadout_item/New()
- if(!description && path)
- var/obj/O = path
- description = initial(O.desc)
- if(restricted_roles && !restricted_desc)
- var/passed_first = FALSE
- restricted_desc = ""
- for(var/job_name in restricted_roles)
- if(!passed_first)
- passed_first = TRUE
- else
- restricted_desc += ", "
- restricted_desc += job_name
-
-/datum/loadout_item/proc/get_spawned_item(customization) //Pass the value from the associative list
- var/obj/item/spawned = new path()
- if(customization != "None")
- customize(spawned, customization)
- return spawned
-
-//Proc designed to be overwritten by invidivual loadout items. has support for a one color feed, and poly colors
-/datum/loadout_item/proc/customize(var/obj/item/spawned, customization)
- switch(extra_info)
- if(LOADOUT_INFO_ONE_COLOR)
- spawned.color = "#[customization]"
- if(LOADOUT_INFO_THREE_COLORS)
- var/list/color_list = splittext(customization, "|")
- var/list/finished_list = list()
- finished_list += ReadRGB("[color_list[1]]0")
- finished_list += ReadRGB("[color_list[2]]0")
- finished_list += ReadRGB("[color_list[3]]0")
- finished_list += list(0,0,0,255)
- for(var/index in 1 to finished_list.len)
- finished_list[index] /= 255
- spawned.color = finished_list
-
-/datum/loadout_item/proc/default_customization()
- switch(extra_info)
- if(LOADOUT_INFO_ONE_COLOR)
- return "FFF"
- if(LOADOUT_INFO_THREE_COLORS)
- return "FFF|FFF|FFF"
- else
- return "None"
-
-//Used to validate extra information from loadout on savefiles
-/datum/loadout_item/proc/get_valid_information(passed_string)
- switch(extra_info)
- if(LOADOUT_INFO_ONE_COLOR)
- return sanitize_hexcolor(passed_string)
- if(LOADOUT_INFO_THREE_COLORS)
- //No way I need to sanitize colors themselves in this thing, right. Like it's too specific to fail this check by chance
- var/list/color_list = splittext(passed_string, "|")
- if(length(color_list) == 3)
- return passed_string
- else
- return default_customization()
- else
- return "None"
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/backpack.dm b/modular_skyrat/modules/customization/modules/client/loadout/backpack.dm
deleted file mode 100644
index 6b83bc5c14b..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/backpack.dm
+++ /dev/null
@@ -1,392 +0,0 @@
-/datum/loadout_item/backpack
- category = LOADOUT_CATEGORY_BACKPACK
-
-//MISC
-
-/datum/loadout_item/backpack/mailbelt
- name = "Messenger Belt Bag"
- path = /obj/item/storage/belt/mailbelt
- cost = 2
-
-/datum/loadout_item/backpack/mailbelt_white
- name = "White Belt Bag"
- path = /obj/item/storage/belt/mailbelt/white
- cost = 2
-
-/datum/loadout_item/backpack/mailbelt_leather
- name = "Leather Belt Bag"
- path = /obj/item/storage/belt/mailbelt/leather
- cost = 2
-
-/datum/loadout_item/backpack/matches
- name = "Matchbox"
- path = /obj/item/storage/box/matches
-
-/datum/loadout_item/backpack/cheaplighter
- name = "Cheap lighter"
- path = /obj/item/lighter/greyscale
-
-/datum/loadout_item/backpack/zippolighter
- name = "Zippo Lighter"
- path = /obj/item/lighter
- cost = 2
-
-/datum/loadout_item/backpack/ttsdevice
- name = "Text-to-Speech Device"
- path = /obj/item/ttsdevice
- cost = 7
-
-/datum/loadout_item/backpack/paicard
- name = "Personal AI device"
- description = "A device, that let you browse and download various AIs."
- path = /obj/item/paicard
- cost = 2
-
-/datum/loadout_item/backpack/cigar
- name = "Cigar"
- path = /obj/item/clothing/mask/cigarette/cigar
- cost = 4 //smoking is bad mkay
-
-/datum/loadout_item/backpack/cigarettes
- name = "Cigarette pack"
- path = /obj/item/storage/fancy/cigarettes
-
-/datum/loadout_item/backpack/wallet
- name = "Wallet"
- path = /obj/item/storage/wallet
-
-/datum/loadout_item/backpack/flask
- name = "Flask"
- path = /obj/item/reagent_containers/food/drinks/flask
- cost = 2
-
-/datum/loadout_item/backpack/skub
- name = "Skub"
- path = /obj/item/skub
-
-/datum/loadout_item/backpack/multipen
- name = "A multicolored pen"
- path = /obj/item/pen/fourcolor
-
-/datum/loadout_item/backpack/fountainpen
- name = "A fancy pen"
- path = /obj/item/pen/fountain
- cost = 2
-
-/datum/loadout_item/backpack/modular_tablet
- name = "A modular tablet"
- path = /obj/item/modular_computer/tablet/preset/cheap/
- cost = 4
-
-/datum/loadout_item/backpack/modular_laptop
- name = "A modular laptop"
- path = /obj/item/modular_computer/laptop/preset/civilian
- cost = 7
-
-/datum/loadout_item/backpack/ringbox_gold
- name = "A gold ring box"
- path = /obj/item/storage/fancy/ringbox
- cost = 3
-
-/datum/loadout_item/backpack/ringbox_silver
- name = "A silver ring box"
- path = /obj/item/storage/fancy/ringbox/silver
- cost = 3
-
-/datum/loadout_item/backpack/ringbox_diamond
- name = "A diamond ring box"
- path = /obj/item/storage/fancy/ringbox/diamond
- cost = 5
-
-/datum/loadout_item/backpack/tapeplayer
- name = "Taperecorder"
- path = /obj/item/taperecorder
-
-/datum/loadout_item/backpack/tape
- name = "Spare cassette tape"
- path = /obj/item/tape/random
-
-/datum/loadout_item/backpack/newspaper
- name = "Newspaper"
- path = /obj/item/newspaper
-
-/datum/loadout_item/backpack/hhmirror
- name = "Handheld Mirror"
- path = /obj/item/hhmirror
- cost = 3
-
-//TOYS
-/datum/loadout_item/backpack/toy
- subcategory = LOADOUT_SUBCATEGORY_BACKPACK_TOYS
-
-/datum/loadout_item/backpack/toy/dice
- name = "Dice bag"
- path = /obj/item/storage/dice
-
-/datum/loadout_item/backpack/toy/eightball
- name = "Magic eightball"
- path = /obj/item/toy/eightball
-
-/datum/loadout_item/backpack/toy/cards
- name = "Playing cards"
- path = /obj/item/toy/cards/deck
-
-/datum/loadout_item/backpack/toy/tennis
- name = "Classic Tennis Ball"
- path = /obj/item/toy/tennis
-
-/datum/loadout_item/backpack/toy/tennisred
- name = "Red Tennis Ball"
- path = /obj/item/toy/tennis/red
-
-/datum/loadout_item/backpack/toy/tennisyellow
- name = "Yellow Tennis Ball"
- path = /obj/item/toy/tennis/yellow
-
-/datum/loadout_item/backpack/toy/tennisgreen
- name = "Green Tennis Ball"
- path = /obj/item/toy/tennis/green
-
-/datum/loadout_item/backpack/toy/tenniscyan
- name = "Cyan Tennis Ball"
- path = /obj/item/toy/tennis/cyan
-
-/datum/loadout_item/backpack/toy/tennisblue
- name = "Blue Tennis Ball"
- path = /obj/item/toy/tennis/blue
-
-/datum/loadout_item/backpack/toy/tennispurple
- name = "Purple Tennis Ball"
- path = /obj/item/toy/tennis/purple
-
-/datum/loadout_item/backpack/toy/toykatana
- name = "Toy Katana"
- path = /obj/item/toy/katana
- cost = 3
-
-/datum/loadout_item/backpack/toy/crayons
- name = "Box of crayons"
- path = /obj/item/storage/crayons
-
-/datum/loadout_item/backpack/cross
- name = "Ornate Cross"
- path = /obj/item/crucifix
- cost = 2
- restricted_roles = list("Chaplain")
-
-
-//Plushies
-/datum/loadout_item/backpack/plushies
- subcategory = LOADOUT_SUBCATEGORY_BACKPACK_PLUSHIES
-
-/datum/loadout_item/backpack/plushies/plushcarp
- name = "Space carp plushie"
- path = /obj/item/toy/plush/carpplushie
-
-/datum/loadout_item/backpack/plushies/plushliz
- name = "Lizard plushie"
- path = /obj/item/toy/plush/lizard_plushie/green
-
-/datum/loadout_item/backpack/plushies/plushsnek
- name = "Snake plushie"
- path = /obj/item/toy/plush/snakeplushie
-
-/datum/loadout_item/backpack/plushies/plushslime
- name = "Slime plushie"
- path = /obj/item/toy/plush/slimeplushie
-
-/datum/loadout_item/backpack/plushies/bubbleplush
- name = "Bubblegum plushie"
- path = /obj/item/toy/plush/bubbleplush
-
-/datum/loadout_item/backpack/plushies/nukeplushie
- name = "Operative plushie"
- path = /obj/item/toy/plush/nukeplushie
-
-/datum/loadout_item/backpack/plushies/plasmamanplushie
- name = "Plasmaman plushie"
- path = /obj/item/toy/plush/plasmamanplushie
-
-/datum/loadout_item/backpack/plushies/beeplushie //the best one
- name = "Bee plushie"
- path = /obj/item/toy/plush/beeplushie
-
-/datum/loadout_item/backpack/plushies/goatplushie
- name = "Strange Goat plushie"
- path = /obj/item/toy/plush/goatplushie
-
-/datum/loadout_item/backpack/plushies/moth
- name = "Moth plushie"
- path = /obj/item/toy/plush/moth
-
-/datum/loadout_item/backpack/plushies/pkplush
- name = "Peacekeeper plushie"
- path = /obj/item/toy/plush/pkplush
-
-/datum/loadout_item/backpack/plushies/sechound
- name = "Sechound plushie"
- path = /obj/item/toy/plush/sechound
-
-/datum/loadout_item/backpack/plushies/medihound
- name = "Medihound plushie"
- path = /obj/item/toy/plush/medihound
-
-/datum/loadout_item/backpack/plushies/scrubpuppy
- name = "Scrubpuppy plushie"
- path = /obj/item/toy/plush/scrubpuppy
-
-/datum/loadout_item/backpack/plushies/meddrake
- name = "MediDrake Plushie"
- path = /obj/item/toy/plush/meddrake
-
-/datum/loadout_item/backpack/plushies/secdrake
- name = "SecDrake Plushie"
- path = /obj/item/toy/plush/secdrake
-
-/datum/loadout_item/backpack/plushies/borbplushie
- name = "Borb plushie"
- path = /obj/item/toy/plush/borbplushie
-
-/datum/loadout_item/backpack/plushies/deer
- name = "Deer plushie"
- path = /obj/item/toy/plush/deer
-
-/datum/loadout_item/backpack/plushies/fermis
- name = "Medcat plushie"
- path = /obj/item/toy/plush/fermis
-
-/datum/loadout_item/backpack/plushies/chen
- name = "Securicat plushie"
- path = /obj/item/toy/plush/fermis/chen
-
-/datum/loadout_item/backpack/plushies/fox
- name = "Fox plushie"
- path = /obj/item/toy/plush/fox
-
-/datum/loadout_item/backpack/plushies/duffmoff
- name = "Suspicious moth plushie"
- path = /obj/item/toy/plush/duffmoth
-
-/datum/loadout_item/backpack/plushies/musicalduffy
- name = "Suspicious musical moth"
- path = /obj/item/instrument/musicalduffy
-
-/datum/loadout_item/backpack/plushies/leaplush
- name = "Suspicious deer plushie"
- path = /obj/item/toy/plush/leaplush
-
-/datum/loadout_item/backpack/plushies/sarmie
- name = "Cosplayer plushie"
- path = /obj/item/toy/plush/sarmieplush
-
-/datum/loadout_item/backpack/plushies/arcplush
- name = "Familiar Lizard plushie"
- path = /obj/item/toy/plush/arcplush
-
-/datum/loadout_item/backpack/plushies/sharknet
- name = "Gluttonous Shark plushie"
- path = /obj/item/toy/plush/sharknet
-
-/datum/loadout_item/backpack/plushies/pintaplush
- name = "Smaller Deer plushie"
- path = /obj/item/toy/plush/pintaplush
-
-/datum/loadout_item/backpack/plushies/oleplush
- name = "Irritable Goat plushie"
- path = /obj/item/toy/plush/oleplush
-
-/datum/loadout_item/backpack/plushies/szaplush
- name = "Suspicious spider plushie"
- path = /obj/item/toy/plush/szaplush
-
-/datum/loadout_item/backpack/plushies/riffplush
- name = "Valid plushie"
- path = /obj/item/toy/plush/riffplush
-
-/datum/loadout_item/backpack/plushies/ianbastardman
- name = "Ian plushie"
- path = /obj/item/toy/plush/ian
-
-/datum/loadout_item/backpack/plushies/corgiman
- name = "Corgi plushie"
- path = /obj/item/toy/plush/ian/small
-
-/datum/loadout_item/backpack/plushies/corgiwoman
- name = "Girly Corgi plushie"
- path = /obj/item/toy/plush/ian/lisa
-
-/datum/loadout_item/backpack/plushies/cat
- name = "Cat plushie"
- path = /obj/item/toy/plush/cat
-
-/datum/loadout_item/backpack/plushies/tuxcat
- name = "Tux Cat plushie"
- path = /obj/item/toy/plush/cat/tux
-
-/datum/loadout_item/backpack/plushies/whitecat
- name = "White Cat plushie"
- path = /obj/item/toy/plush/cat/white
-
-/datum/loadout_item/backpack/plushies/narplush
- name = "Narsie Plushie"
- path = /obj/item/toy/plush/narplush
- cost = 5
- restricted_roles = list("Chaplain")
-
-/datum/loadout_item/backpack/plushies/ratplush
- name = "Ratvar Plushie"
- path = /obj/item/toy/plush/ratplush
- cost = 5
- restricted_roles = list("Chaplain")
-
-/datum/loadout_item/backpack/plushies/rouny
- name = "Runner Plushie"
- path = /obj/item/toy/plush/rouny
-
-/datum/loadout_item/backpack/plushies/seaduplush
- name = "Sneed plushie"
- path = /obj/item/toy/plush/seaduplush
-
-/datum/loadout_item/backpack/fragrance
- subcategory = LOADOUT_SUBCATEGORY_BACKPACK_FRAGRANCE
- cost = 2
-
-/datum/loadout_item/backpack/fragrance/cologne
- name = "Cologne Bottle"
- path = /obj/item/perfume/cologne
-
-/datum/loadout_item/backpack/fragrance/wood
- name = "Wood Perfume"
- path = /obj/item/perfume/wood
-
-/datum/loadout_item/backpack/fragrance/rose
- name = "Rose Perfume"
- path = /obj/item/perfume/rose
-
-/datum/loadout_item/backpack/fragrance/jasmine
- name = "Jasmine Perfume"
- path = /obj/item/perfume/jasmine
-
-/datum/loadout_item/backpack/fragrance/mint
- name = "Mint Perfume"
- path = /obj/item/perfume/mint
-
-/datum/loadout_item/backpack/fragrance/vanilla
- name = "Vanilla Perfume"
- path = /obj/item/perfume/vanilla
-
-/datum/loadout_item/backpack/fragrance/pear
- name = "Pear Perfume"
- path = /obj/item/perfume/pear
-
-/datum/loadout_item/backpack/fragrance/strawberry
- name = "Strawberry Perfume"
- path = /obj/item/perfume/strawberry
-
-/datum/loadout_item/backpack/fragrance/cherry
- name = "Cherry Perfume"
- path = /obj/item/perfume/cherry
-
-/datum/loadout_item/backpack/fragrance/amber
- name = "Amber Perfume"
- path = /obj/item/perfume/amber
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/donator/_donator.dm b/modular_skyrat/modules/customization/modules/client/loadout/donator/_donator.dm
deleted file mode 100644
index 0933e43d90e..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/donator/_donator.dm
+++ /dev/null
@@ -1,55 +0,0 @@
-/datum/loadout_item/uniform/donator
- category = LOADOUT_CATEGORY_UNIFORM
- subcategory = LOADOUT_SUBCATEGORY_DONATOR
-
-/datum/loadout_item/uniform/donator/public
- donator_only = TRUE
-
-/datum/loadout_item/suit/donator
- category = LOADOUT_CATEGORY_SUIT
- subcategory = LOADOUT_SUBCATEGORY_DONATOR
-
-/datum/loadout_item/suit/donator/public
- donator_only = TRUE
-
-/datum/loadout_item/gloves/donator
- category = LOADOUT_CATEGORY_GLOVES
- subcategory = LOADOUT_SUBCATEGORY_DONATOR
-
-/datum/loadout_item/gloves/donator/public
- donator_only = TRUE
-
-/datum/loadout_item/neck/donator
- category = LOADOUT_CATEGORY_NECK
- subcategory = LOADOUT_SUBCATEGORY_DONATOR
-
-/datum/loadout_item/neck/donator/public
- donator_only = TRUE
-
-/datum/loadout_item/shoes/donator
- category = LOADOUT_CATEGORY_SHOES
- subcategory = LOADOUT_SUBCATEGORY_DONATOR
-
-/datum/loadout_item/shoes/donator/public
- donator_only = TRUE
-
-/datum/loadout_item/head/donator
- category = LOADOUT_CATEGORY_HEAD
- subcategory = LOADOUT_SUBCATEGORY_DONATOR
-
-/datum/loadout_item/head/donator/public
- donator_only = TRUE
-
-/datum/loadout_item/mask/donator
- category = LOADOUT_CATEGORY_MASK
- subcategory = LOADOUT_SUBCATEGORY_DONATOR
-
-/datum/loadout_item/mask/donator/public
- donator_only = TRUE
-
-/datum/loadout_item/backpack/donator
- category = LOADOUT_CATEGORY_BACKPACK
- subcategory = LOADOUT_SUBCATEGORY_DONATOR
-
-/datum/loadout_item/backpack/donator/public
- donator_only = TRUE
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/donator/backpack.dm b/modular_skyrat/modules/customization/modules/client/loadout/donator/backpack.dm
deleted file mode 100644
index d401b7d2465..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/donator/backpack.dm
+++ /dev/null
@@ -1,12 +0,0 @@
-/datum/loadout_item/backpack/donator/public/coin
- name = "Iron Coin"
- path = /obj/item/coin/iron
-
-/datum/loadout_item/backpack/donator/public/havana_cigar_case
- name = "Havanian Cigars"
- path = /obj/item/storage/fancy/cigarettes/cigars/havana
- cost = 3
-
-/datum/loadout_item/backpack/donator/public/vape
- name = "E-Cigarette"
- path = /obj/item/clothing/mask/vape
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/donator/feet.dm b/modular_skyrat/modules/customization/modules/client/loadout/donator/feet.dm
deleted file mode 100644
index df2dff4fa84..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/donator/feet.dm
+++ /dev/null
@@ -1,7 +0,0 @@
-/datum/loadout_item/shoes/donator/public/blackjackboots
- name = "Black Jackboots"
- path = /obj/item/clothing/shoes/jackboots/black
-
-/datum/loadout_item/shoes/donator/public/rainbow
- name = "Rainbow Converse"
- path = /obj/item/clothing/shoes/sneakers/rainbow
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/donator/hands.dm b/modular_skyrat/modules/customization/modules/client/loadout/donator/hands.dm
deleted file mode 100644
index e85fc2f150e..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/donator/hands.dm
+++ /dev/null
@@ -1,3 +0,0 @@
-/datum/loadout_item/gloves/donator/public/military
- name = "Military Gloves"
- path = /obj/item/clothing/gloves/military
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/donator/head.dm b/modular_skyrat/modules/customization/modules/client/loadout/donator/head.dm
deleted file mode 100644
index 2a3d31aa87b..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/donator/head.dm
+++ /dev/null
@@ -1,35 +0,0 @@
-/datum/loadout_item/head/donator/public/poppy
- name = "Poppy Flower"
- path = /obj/item/food/grown/poppy
-
-/datum/loadout_item/head/donator/public/lily
- name = "Lily Flower"
- path = /obj/item/food/grown/poppy/lily
-
-/datum/loadout_item/head/donator/public/geranium
- name = "Geranium Flower"
- path = /obj/item/food/grown/poppy/geranium
-
-/datum/loadout_item/head/donator/public/fraxinella
- name = "Fraxinella Flower"
- path = /obj/item/food/grown/poppy/geranium/fraxinella
-
-/datum/loadout_item/head/donator/public/harebell
- name = "Harebell Flower"
- path = /obj/item/food/grown/harebell
-
-/datum/loadout_item/head/donator/public/rose
- name = "Rose Flower"
- path = /obj/item/food/grown/rose
-
-/datum/loadout_item/head/donator/public/carbon_rose
- name = "Carbon Rose Flower"
- path = /obj/item/grown/carbon_rose
-
-/datum/loadout_item/head/donator/public/enclave
- name = "Enclave Cap"
- path = /obj/item/clothing/head/soft/enclave
-
-/datum/loadout_item/head/donator/public/enclaveo
- name = "Enclave Cap - officer"
- path = /obj/item/clothing/head/soft/enclaveo
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/donator/neck.dm b/modular_skyrat/modules/customization/modules/client/loadout/donator/neck.dm
deleted file mode 100644
index 72d9e785ee1..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/donator/neck.dm
+++ /dev/null
@@ -1,26 +0,0 @@
-/datum/loadout_item/neck/donator/public/rscloak
- name = "Black Cape"
- path = /obj/item/clothing/neck/cloak/rscloak
-
-/datum/loadout_item/neck/donator/public/rscloakcross
- name = "Black Cross Cape"
- path = /obj/item/clothing/neck/cloak/rscloak_cross
-
-/datum/loadout_item/neck/donator/public/rscloakchampion
- name = "Champion Cape"
- path = /obj/item/clothing/neck/cloak/rscloak_champion
-
-/datum/loadout_item/neck/donator/public/rscloak_poly
- name = "Polychromic Cape"
- path = /obj/item/clothing/neck/cloak/polychromic/rscloak
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/donator/public/rscloakcross_poly
- name = "Polychromic Cape - Cross"
- path = /obj/item/clothing/neck/cloak/polychromic/rscloak_cross
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/donator/mantle/regal
- name = "Regal Mantle"
- path = /obj/item/clothing/neck/mantle/regal
-
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/donator/personal/donator_personal.dm b/modular_skyrat/modules/customization/modules/client/loadout/donator/personal/donator_personal.dm
deleted file mode 100644
index 1115001729c..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/donator/personal/donator_personal.dm
+++ /dev/null
@@ -1,287 +0,0 @@
-/datum/loadout_item/backpack/donator/miafoxplush
- name = "Mia’s fox plushie"
- path = /obj/item/toy/plush/fox/mia
- ckeywhitelist = list("fuzlet")
-
-/datum/loadout_item/backpack/donator/teasefoxplush
- name = "Teasable fox plushie"
- path = /obj/item/toy/plush/fox/kailyn
- ckeywhitelist = list("ratraus")
-
-/datum/loadout_item/backpack/donator/drawingtablet
- name = "Drawing Tablet"
- path = /obj/item/canvas/drawingtablet
- ckeywhitelist = list("thedragmeme")
-
-/datum/loadout_item/shoes/donator/heeled_jackboots
- name = "High-heel Jackboots"
- path = /obj/item/clothing/shoes/jackboots/heel
-// ckeywhitelist = list("thedragmeme")
-//As they requested, it's properly public now.
-
-/datum/loadout_item/suit/donator/furcoat
- name = "Leather coat with fur"
- path = /obj/item/clothing/suit/furcoat
- ckeywhitelist = list("thedragmeme")
-
-/datum/loadout_item/uniform/donator/black_turtleneck
- name = "Black turtleneck"
- path = /obj/item/clothing/under/syndicate/tacticool/black
- ckeywhitelist = list("thedragmeme")
-
-/datum/loadout_item/suit/donator/ryddid
- name = "The Ryddid"
- path = /obj/item/clothing/suit/jacket/ryddid
- ckeywhitelist = list("grunnyyy")
-
-/datum/loadout_item/uniform/donator/draculass
- name = "Draculass Dress"
- path = /obj/item/clothing/under/costume/draculass
- ckeywhitelist = list("grunnyyy")
-
-/datum/loadout_item/neck/donator/grunnyyycloak
- name = "Black and Red cloak"
- path = /obj/item/clothing/neck/cloak/grunnyyy
- ckeywhitelist = list("grunnyyy")
-
-/datum/loadout_item/gloves/donator/hypnoring_coffee
- name = "Hypnodemon's Ring"
- path = /obj/item/clothing/gloves/ring/hypno/coffeepot
- ckeywhitelist = list("coffeepot")
-
-/datum/loadout_item/gloves/donator/hypnoring_bippy
- name = "Hypnodemon's Ring"
- path = /obj/item/clothing/gloves/ring/hypno/bippys
- ckeywhitelist = list("bippys")
-
-/datum/loadout_item/suit/donator/kimjacket
- name = "Aerostatic Bomber Jacket"
- path = /obj/item/clothing/suit/kimjacket
- ckeywhitelist = list("raxraus")
-
-/datum/loadout_item/uniform/donator/rax_turtleneck
- name = "Black Turtleneck"
- path = /obj/item/clothing/under/rax_turtleneck
- ckeywhitelist = list("raxraus")
-
-/datum/loadout_item/shoes/donator/rax_armadyne_boots
- name = "Tactical Boots"
- path = /obj/item/clothing/shoes/combat/rax
- ckeywhitelist = list("raxraus")
-
-/datum/loadout_item/suit/donator/rax_peacekeeper_jacket
- name = "Peacekeeper jacket"
- path = /obj/item/clothing/suit/armor/vest/warden/rax
- ckeywhitelist = list("raxraus")
- restricted_roles = list("Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
-
-/datum/loadout_item/suit/donator/rax_peacekeeper_turtleneck
- name = "Peacekeeper turtleneck"
- path = /obj/item/clothing/under/rank/security/blueshieldturtleneck/rax
- ckeywhitelist = list("raxraus")
- restricted_roles = list("Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
-
-/datum/loadout_item/suit/donator/rax_aerostatic_jacket
- name = "Navy Aerostatic Jacket"
- path = /obj/item/clothing/suit/jacket/rax
- ckeywhitelist = list("raxraus")
-
-/datum/loadout_item/uniform/donator/rax_gray_turtleneck
- name = "Gray Turtleneck"
- path = /obj/item/clothing/under/rax_turtleneck_gray
- ckeywhitelist = list("raxraus")
-
-/datum/loadout_item/glasses/donator/zentaiglasses
- name = "Demonic Sunglasses"
- path = /obj/item/clothing/glasses/zentai
- ckeywhitelist = list("candlejax")
-
-/datum/loadout_item/head/donator/hephelmet
- name = "HepUnit Standard Helmet"
- path = /obj/item/clothing/head/helmet/sec/peacekeeper/jax
- ckeywhitelist = list("candlejax")
- restricted_roles = list("Vanguard Operative","Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
-
-/datum/loadout_item/head/donator/emissionhelm
- name = "Emission's Helmet"
- path = /obj/item/clothing/head/helmet/space/plasmaman/candlejax
- ckeywhitelist = list("candlejax")
- restricted_roles = list("Shaft Miner","Vanguard Operative","Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
-
-/datum/loadout_item/uniform/donator/emissionsuit
- name = "Emission's Suit"
- path = /obj/item/clothing/under/plasmaman/security/candlejax
- ckeywhitelist = list("candlejax")
- restricted_roles = list("Shaft Miner","Vanguard Operative","Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
-
-/datum/loadout_item/head/donator/avipilot
- name = "Smuggler's Flying Cap"
- path = /obj/item/clothing/head/avipilot
- ckeywhitelist = list("slippyjoe")
-
-/datum/loadout_item/shoes/donator/britches_shoes
- name = "Britches' shoes"
- path = /obj/item/clothing/shoes/clown_shoes/britches
- ckeywhitelist = list("bloodrite")
-
-/datum/loadout_item/uniform/donator/britches_dress
- name = "Britches' dress"
- path = /obj/item/clothing/under/rank/civilian/clown/britches
- ckeywhitelist = list("bloodrite")
-
-/datum/loadout_item/mask/donator/britches_mask
- name = "Britches' mask"
- path = /obj/item/clothing/mask/gas/britches
- ckeywhitelist = list("bloodrite")
-
-/datum/loadout_item/mask/donator/luchador_mask
- name = "Mask of El Red Templar"
- path = /obj/item/clothing/mask/luchador/enzo
- ckeywhitelist = list("enzoman")
-
-/datum/loadout_item/mask/donator/nightlight_mask
- name = "FAR-13 SRU"
- path = /obj/item/clothing/mask/gas/nightlight
- ckeywhitelist = list("farsightednightlight")
-
-/datum/loadout_item/mask/donator/kindle_mask
- name = "Kindle's mask"
- path = /obj/item/clothing/mask/kindle
- ckeywhitelist = list("theooz")
-
-/datum/loadout_item/head/donator/drake_skull
- name = "Skull of an ashdrake"
- path = /obj/item/clothing/head/drake_skull
- ckeywhitelist = list("random516")
-
-/datum/loadout_item/gloves/donator/blutigen_wraps
- name = "Blutigen Wraps"
- path = /obj/item/clothing/gloves/fingerless/blutigen_wraps
- ckeywhitelist = list("random516")
-
-/datum/loadout_item/suit/donator/blutigen_kimono
- name = "Blutigen Kimono"
- path = /obj/item/clothing/suit/blutigen_kimono
- ckeywhitelist = list("random516")
-
-/datum/loadout_item/uniform/donator/blutigen_undergarment
- name = "Dragon Undergarment"
- path = /obj/item/clothing/under/custom/blutigen_undergarment
- ckeywhitelist = list("random516")
-
-/datum/loadout_item/uniform/donator/captain_dress
- name = "Captain's Dress"
- path = /obj/item/clothing/under/rank/captain/dress
- ckeywhitelist = list("netrakyram")
- restricted_roles = list("Captain")
-
-/datum/loadout_item/uniform/donator/kilano_suit
- name = "black and gold dress uniform"
- path = /obj/item/clothing/under/custom/kilano
- ckeywhitelist = list("netrakyram")
-
-/datum/loadout_item/gloves/donator/kilano_gloves
- name = "black and gold gloves"
- path = /obj/item/clothing/gloves/kilano
- ckeywhitelist = list("netrakyram")
-
-/datum/loadout_item/shoes/donator/kilano_boots
- name = "black and gold boots"
- path = /obj/item/clothing/shoes/winterboots/kilano
- ckeywhitelist = list("netrakyram")
-
-/datum/loadout_item/neck/donator/kiaracloak
- name = "Kiara's cloak"
- path = /obj/item/clothing/neck/cloak/inferno
- ckeywhitelist = list("inferno707")
-
-/datum/loadout_item/neck/donator/kiaracollar
- name = "Kiara's collar"
- path = /obj/item/clothing/neck/human_petcollar/inferno
- ckeywhitelist = list("inferno707")
-
-/datum/loadout_item/backpack/donator/kiaramedal
- name = "Insignia of Steele"
- path = /obj/item/clothing/accessory/medal/steele
- ckeywhitelist = list("inferno707")
-
-/datum/loadout_item/mask/donator/hheart
- name = "The Hollow Heart"
- path = /obj/item/clothing/mask/hheart
- ckeywhitelist = list("inferno707")
-
-/datum/loadout_item/backpack/donator/darksabre
- name = "Dark Sabre"
- path = /obj/item/toy/darksabre
- ckeywhitelist = list("inferno707")
-
-/datum/loadout_item/backpack/donator/darksabresheath
- name = "Dark Sabre Sheath"
- path = /obj/item/storage/belt/sabre/darksabre
- ckeywhitelist = list("inferno707")
-
-/datum/loadout_item/backpack/donator/darkarmor
- name = "Dark Armor"
- path = /obj/item/clothing/suit/armor/vest/darkcarapace
- ckeywhitelist = list("inferno707")
-
-/datum/loadout_item/neck/donator/zuliecloak
- name = "Project: Zul-E"
- path = /obj/item/clothing/suit/hooded/cloak/zuliecloak
- ckeywhitelist = list("asky")
-
-/datum/loadout_item/backpack/donator/brightcosmos
- name = "Bright Cosmos cigar"
- path = /obj/item/clothing/mask/holocigarette/cigar
- ckeywhitelist = list("lyricalpaws")
-
-/datum/loadout_item/uniform/donator/lannese
- name = "Lannese Dress"
- path = /obj/item/clothing/under/custom/lannese
- ckeywhitelist = list("kathrinbailey")
-
-/datum/loadout_item/uniform/donator/lannese/vambrace
- name = "Lannese Dress w/ Vambraces"
- path = /obj/item/clothing/under/custom/lannese/vambrace
- ckeywhitelist = list("kathrinbailey")
-
-/datum/loadout_item/suit/donator/scraparmour
- name = "Scrap Armor"
- path = /obj/item/clothing/suit/scraparmour
- ckeywhitelist = list("hackertdog")
-
-/datum/loadout_item/uniform/donator/mechanic
- name = "Mechanic's Overalls"
- path = /obj/item/clothing/under/misc/mechanic
- ckeywhitelist = list("cypressb")
-
-/datum/loadout_item/uniform/donator/mikubikini
- name = "starlight singer bikini"
- path = /obj/item/clothing/under/mikubikini
- ckeywhitelist = list("grandvegeta")
-
-/datum/loadout_item/suit/donator/mikujacket
- name = "starlight singer jacket"
- path = /obj/item/clothing/suit/mikujacket
- ckeywhitelist = list("grandvegeta")
-
-/datum/loadout_item/head/donator/mikuhair
- name = "starlight singer hair"
- path = /obj/item/clothing/head/mikuhair
- ckeywhitelist = list("grandvegeta")
-
-/datum/loadout_item/gloves/donator/mikugloves
- name = "starlight singer gloves"
- path = /obj/item/clothing/gloves/mikugloves
- ckeywhitelist = list("grandvegeta")
-
-/datum/loadout_item/shoes/donator/mikulegging
- name = "starlight singer bikini"
- path = /obj/item/clothing/shoes/sneakers/mikuleggings
- ckeywhitelist = list("grandvegeta")
-
-//FOR DEV INTERNS!
-/datum/loadout_item/backpack/donator/intern_hat
- name = "Intern Hat"
- path = /obj/item/clothing/head/intern/developer
- ckeywhitelist = list("gandalf2k15", "yawet330", "junglerat", "theooz", "tf4", "cheshify", "swiftfeather", "spcr", "tygertac", "useroth", "slippyjoe")
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/donator/suit.dm b/modular_skyrat/modules/customization/modules/client/loadout/donator/suit.dm
deleted file mode 100644
index d61848c2bda..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/donator/suit.dm
+++ /dev/null
@@ -1,23 +0,0 @@
-/datum/loadout_item/suit/donator/public/furredjacket
- name = "Furred Jacket"
- path = /obj/item/clothing/suit/brownfurrich/public
-
-/datum/loadout_item/suit/donator/public/whitefurredjacket
- name = "White Furred Jacket"
- path = /obj/item/clothing/suit/brownfurrich/white
-
-/datum/loadout_item/suit/donator/public/creamfurredjacket
- name = "Cream Furred Jacket"
- path = /obj/item/clothing/suit/brownfurrich/cream
-
-/datum/loadout_item/suit/donator/public/british_officer
- name = "British Officers Coat"
- path = /obj/item/clothing/suit/british_officer
-
-/datum/loadout_item/suit/donator/public/british_officer
- name = "Modern Winter Coat"
- path = /obj/item/clothing/suit/modern_winter
-
-/datum/loadout_item/suit/donator/public/blondie
- name = "Cowboy Vest"
- path = /obj/item/clothing/suit/cowboyvest
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/donator/uniform.dm b/modular_skyrat/modules/customization/modules/client/loadout/donator/uniform.dm
deleted file mode 100644
index d43a59d1889..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/donator/uniform.dm
+++ /dev/null
@@ -1,16 +0,0 @@
-
-/datum/loadout_item/uniform/donator/public/enclavesergeant
- name = "Enclave - Sergeant"
- path = /obj/item/clothing/under/enclave
-
-/datum/loadout_item/uniform/donator/public/enclaveofficer
- name = "Enclave - Officer"
- path = /obj/item/clothing/under/enclaveo
-
-/datum/loadout_item/uniform/donator/public/fashionableskirt
- name = "Fashionable Skirt"
- path = /obj/item/clothing/under/rank/civilian/skirt
-
-/datum/loadout_item/uniform/donator/public/blondie
- name = "Blonde Cowboy Uniform"
- path = /obj/item/clothing/under/rank/security/detective/cowboy/armorless
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/glasses.dm b/modular_skyrat/modules/customization/modules/client/loadout/glasses.dm
deleted file mode 100644
index 6304da512be..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/glasses.dm
+++ /dev/null
@@ -1,95 +0,0 @@
-/datum/loadout_item/glasses
- category = LOADOUT_CATEGORY_GLASSES
-
-//MISC
-/datum/loadout_item/glasses/blindfold
- name = "Blindfold"
- path = /obj/item/clothing/glasses/blindfold
-
-/datum/loadout_item/glasses/fakeblindfold
- name = "Fake Blindfold"
- path = /obj/item/clothing/glasses/trickblindfold
-
-/datum/loadout_item/glasses/cold
- name = "Cold goggles"
- path = /obj/item/clothing/glasses/cold
-
-/datum/loadout_item/glasses/eyepatch
- name = "Eyepatch"
- path = /obj/item/clothing/glasses/eyepatch
-
-/datum/loadout_item/glasses/heat
- name = "Heat goggles"
- path = /obj/item/clothing/glasses/heat
-
-/datum/loadout_item/glasses/hipster
- name = "Hipster glasses"
- path = /obj/item/clothing/glasses/regular/hipster
-
-/datum/loadout_item/glasses/jamjar
- name = "Jamjar glasses"
- path = /obj/item/clothing/glasses/regular/jamjar
-
-/datum/loadout_item/glasses/monocle
- name = "Monocle"
- path = /obj/item/clothing/glasses/monocle
-
-/datum/loadout_item/glasses/orange
- name = "Orange glasses"
- path = /obj/item/clothing/glasses/orange
-
-/datum/loadout_item/glasses/red
- name = "Red Glasses"
- path = /obj/item/clothing/glasses/red
-
-/datum/loadout_item/glasses/prescription
- name = "Prescription glasses"
- path = /obj/item/clothing/glasses/regular
-
-/datum/loadout_item/glasses/thin
- name = "Thin Glasses"
- path = /obj/item/clothing/glasses/thin
-
-/datum/loadout_item/glasses/better
- name = "Modern Glasses"
- path = /obj/item/clothing/glasses/betterunshit
-
-/datum/loadout_item/glasses/eyewrap
- name = "Eyepatch Wrap"
- path = /obj/item/clothing/glasses/eyepatch/wrap
-
-/datum/loadout_item/glasses/whiteeyepatch
- name = "White Eyepatch"
- path = /obj/item/clothing/glasses/eyepatch/white
-
-/datum/loadout_item/glasses/biker
- name = "Biker Goggles"
- path = /obj/item/clothing/glasses/biker
-
-/datum/loadout_item/glasses/medicpatch
- name = "Medical Eyepatch"
- path = /obj/item/clothing/glasses/hud/eyepatch/med
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Paramedic")
- restricted_desc = "Medical"
-
-/datum/loadout_item/glasses/robopatch
- name = "Diagnostic Eyepatch"
- path = /obj/item/clothing/glasses/hud/eyepatch/diagnostic
- restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Vanguard Operative")
- restricted_desc = "Science"
-
-/datum/loadout_item/glasses/scipatch
- name = "Science Eyepatch"
- path = /obj/item/clothing/glasses/hud/eyepatch/sci
- restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Chemist", "Vanguard Operative")
- restricted_desc = "Science and Chemists"
-
-/datum/loadout_item/glasses/sechud
- name = "Security Hud"
- path = /obj/item/clothing/glasses/hud/security
- restricted_roles = list("Security Officer", "Security Sergeant", "Warden", "Head of Security", "Civil Disputes Officer")
-
-/datum/loadout_item/glasses/secpatch
- name = "Security Eyepatch Hud"
- path = /obj/item/clothing/glasses/hud/eyepatch/sec
- restricted_roles = list("Security Officer", "Security Sergeant", "Warden", "Head of Security", "Civil Disputes Officer")
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/gloves.dm b/modular_skyrat/modules/customization/modules/client/loadout/gloves.dm
deleted file mode 100644
index 12e804d385a..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/gloves.dm
+++ /dev/null
@@ -1,27 +0,0 @@
-/datum/loadout_item/gloves
- category = LOADOUT_CATEGORY_GLOVES
-
-//MISC
-/datum/loadout_item/gloves/fingerless
- name = "Fingerless Gloves"
- path = /obj/item/clothing/gloves/fingerless
-
-/datum/loadout_item/gloves/evening
- name = "Evening Gloves"
- path = /obj/item/clothing/gloves/evening
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/gloves/goldring
- name = "A gold ring"
- path = /obj/item/clothing/gloves/ring
- cost = 2
-
-/datum/loadout_item/gloves/silverring
- name = "A silver ring"
- path = /obj/item/clothing/gloves/ring/silver
- cost = 2
-
-/datum/loadout_item/gloves/diamondring
- name = "A diamond ring"
- path = /obj/item/clothing/gloves/ring/diamond
- cost = 4
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/hands.dm b/modular_skyrat/modules/customization/modules/client/loadout/hands.dm
deleted file mode 100644
index 16434b4920c..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/hands.dm
+++ /dev/null
@@ -1,12 +0,0 @@
-/datum/loadout_item/hands
- category = LOADOUT_CATEGORY_HANDS
-
-//MISC
-/datum/loadout_item/hands/cane
- name = "Cane"
- path = /obj/item/cane
-
-/datum/loadout_item/hands/briefcase
- name = "Briefcase"
- path = /obj/item/storage/briefcase
- cost = 2 //Authentic leather
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/head.dm b/modular_skyrat/modules/customization/modules/client/loadout/head.dm
deleted file mode 100644
index 6b5b11ccef8..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/head.dm
+++ /dev/null
@@ -1,450 +0,0 @@
-/datum/loadout_item/head
- category = LOADOUT_CATEGORY_HEAD
-
-//MISC
-/datum/loadout_item/head/baseball
- name = "Ballcap"
- path = /obj/item/clothing/head/soft/mime
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/head/beanie
- name = "Beanie"
- path = /obj/item/clothing/head/beanie
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-// /datum/loadout_item/head/beret_white
-// name = "Beret"
-// path = /obj/item/clothing/head/beret/white
-// extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/head/beret
- name = "Black beret"
- path = /obj/item/clothing/head/beret/black
-
-/datum/loadout_item/head/flatcap
- name = "Flat cap"
- path = /obj/item/clothing/head/flatcap
-
-/datum/loadout_item/head/pflatcap
- name = "Poly Flat cap"
- path = /obj/item/clothing/head/polyflatc
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/head/pirate
- name = "Pirate hat"
- path = /obj/item/clothing/head/pirate
-
-/datum/loadout_item/head/flowerpin
- name = "Flower Pin"
- path = /obj/item/clothing/head/flowerpin
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/head/rice_hat
- name = "Rice hat"
- path = /obj/item/clothing/head/rice_hat
-
-/datum/loadout_item/head/ushanka
- name = "Ushanka"
- path = /obj/item/clothing/head/ushanka
-
-/datum/loadout_item/head/wrussian
- name = "Black Papakha"
- path = /obj/item/clothing/head/whiterussian
-
-/datum/loadout_item/head/wrussianw
- name = "White Papakha"
- path = /obj/item/clothing/head/whiterussian/white
-
-/datum/loadout_item/head/wrussianb
- name = "Black and Red Papakha"
- path = /obj/item/clothing/head/whiterussian/black
-
-/datum/loadout_item/head/slime
- name = "Slime hat"
- path = /obj/item/clothing/head/collectable/slime
-
-/datum/loadout_item/head/fedora
- name = "Fedora"
- path = /obj/item/clothing/head/fedora
-
-/datum/loadout_item/head/that
- name = "Top Hat"
- path = /obj/item/clothing/head/that
-
-/datum/loadout_item/head/flakhelm
- name = "Flak Helmet"
- path = /obj/item/clothing/head/flakhelm
- cost = 2
-
-/datum/loadout_item/head/bunnyears
- name = "Bunny Ears"
- path = /obj/item/clothing/head/rabbitears
-
-/datum/loadout_item/head/mailmanhat
- name = "Mailman's Hat"
- path = /obj/item/clothing/head/mailman
-
-/datum/loadout_item/head/whitekepi
- name = "White Kepi"
- path = /obj/item/clothing/head/kepi
-
-/datum/loadout_item/head/whitekepiold
- name = "White Kepi, Old"
- path = /obj/item/clothing/head/kepi/old
-
-/datum/loadout_item/head/hijab
- name = "Hijab"
- path = /obj/item/clothing/head/hijab
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/head/turban
- name = "Turban"
- path = /obj/item/clothing/head/turb
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/head/keff
- name = "Keffiyeh"
- path = /obj/item/clothing/head/keffiyeh
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/head/maidhead
- name = "Maid Headband"
- path = /obj/item/clothing/head/maid
-
-/datum/loadout_item/head/widehat
- name = "Wide Black Hat"
- path = /obj/item/clothing/head/costume/widehat
-
-/datum/loadout_item/head/widehat_red
- name = "Wide Red Hat"
- path = /obj/item/clothing/head/costume/widehat/red
-
-/datum/loadout_item/head/cardboard
- name = "Cardboard Helmet"
- path = /obj/item/clothing/head/cardborg
- cost = 2
-
-/datum/loadout_item/head/wig
- name = "Wig"
- path = /obj/item/clothing/head/wig
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/head/wignatural
- name = "Natural Wig"
- path = /obj/item/clothing/head/wig/natural
-
-//Cowboy Stuff
-/datum/loadout_item/head/cowboyhat
- name = "Cowboy Hat, Brown"
- path = /obj/item/clothing/head/cowboyhat
-
-/datum/loadout_item/head/cowboyhat/black
- name = "Cowboy Hat, Black"
- path = /obj/item/clothing/head/cowboyhat/black
-
-/datum/loadout_item/head/cowboyhat/blackwide
- name = "Wide Cowboy Hat, Black"
- path = /obj/item/clothing/head/cowboyhat/blackwide
-
-/datum/loadout_item/head/cowboyhat/wide
- name = "Wide Cowboy Hat, Brown"
- path = /obj/item/clothing/head/cowboyhat/wide
-
-/datum/loadout_item/head/cowboyhat/white
- name = "Cowboy Hat, White"
- path = /obj/item/clothing/head/cowboyhat/white
-
-/datum/loadout_item/head/cowboyhat/pink
- name = "Cowboy Hat, Pink"
- path = /obj/item/clothing/head/cowboyhat/pink
-
-/datum/loadout_item/head/cowboyhat/winter
- name = "Winter Cowboy Hat"
- path = /obj/item/clothing/head/cowboyhat/sheriff
-
-/datum/loadout_item/head/cowboyhat/sheriff
- name = "Sheriff Hat"
- path = /obj/item/clothing/head/cowboyhat/sheriff/alt
-
-/datum/loadout_item/head/cowboyhat/deputy
- name = "Deputy Hat"
- path = /obj/item/clothing/head/cowboyhat/deputy
-
-//trek fancy Hats!
-/datum/loadout_item/head/trek/trekcap
- name = "Federation Officer's Cap (White)"
- path = /obj/item/clothing/head/caphat/formal/fedcover
- restricted_roles = list("Captain","Head of Personnel")
-
-/datum/loadout_item/head/trek/trekcapcap
- name = "Federation Officer's Cap (Black)"
- path = /obj/item/clothing/head/caphat/formal/fedcover/black
- restricted_roles = list("Captain","Head of Personnel")
-
-/datum/loadout_item/head/trek/trekcapmedisci
- name = "Federation Officer's Cap (Blue)"
- path = /obj/item/clothing/head/caphat/formal/fedcover/medsci
- restricted_desc = "Medical and Science"
- restricted_roles = list("Chief Medical Officer","Medical Doctor","Security Medic","Paramedic","Chemist","Virologist","Psychologist","Geneticist","Research Director","Scientist","Roboticist","Vanguard Operative")
-
-/datum/loadout_item/head/trek/trekcapeng
- name = "Federation Officer's Cap (Yellow)"
- path = /obj/item/clothing/head/caphat/formal/fedcover/eng
- restricted_desc = "Engineering, Security, and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Security Medic","Security Sergeant","Warden","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-/datum/loadout_item/head/trek/trekcapsec
- name = "Federation Officer's Cap (Red)"
- path = /obj/item/clothing/head/caphat/formal/fedcover/sec
- restricted_desc = "Engineering, Security, and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Security Medic","Security Sergeant","Warden","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-/*Commenting out Until next Christmas or made automatic
-/datum/gear/santahatr
- name = "Red Santa Hat"
- category = SLOT_HEAD
- path = /obj/item/clothing/head/christmashat
-/datum/gear/santahatg
- name = "Green Santa Hat"
- category = SLOT_HEAD
- path = /obj/item/clothing/head/christmashatg
-*/
-
-//JOB
-/datum/loadout_item/head/job
- subcategory = LOADOUT_SUBCATEGORY_JOB
-
-/datum/loadout_item/head/job/captain/imperial
- name = "Captain's Naval Cap"
- path = /obj/item/clothing/head/imperial/cap
- restricted_roles = list("Captain", "Nanotrasen Representative")
-
-/datum/loadout_item/head/job/hop/imperial
- name = "Head of Personnel's Naval Cap"
- path = /obj/item/clothing/head/imperial/hop
- restricted_roles = list("Head of Personnel", "Nanotrasen Representative")
-
-/datum/loadout_item/head/job/ce/imperial
- name = "Chief Engineer's blast helmet."
- path = /obj/item/clothing/head/imperial/ce
- restricted_roles = list("Chief Engineer")
-
-/datum/loadout_item/head/job/cowboyhat/sec
- name = "Cowboy Hat, Security"
- path = /obj/item/clothing/head/cowboyhat/sec
- restricted_desc = "Security"
- restricted_roles = list("Warden","Detective","Security Medic","Security Sergeant","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer")
-
-/datum/loadout_item/head/job/cowboyhat/secwide
- name = "Wide Cowboy Hat, Security"
- path = /obj/item/clothing/head/cowboyhat/widesec
- restricted_desc = "Security"
- restricted_roles = list("Warden","Detective","Security Medic","Security Sergeant","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer")
-
-/datum/loadout_item/head/job/sec/ushanka
- name = "Security Ushanka"
- path = /obj/item/clothing/head/ushankasec
- restricted_desc = "Security"
- restricted_roles = list("Warden","Detective","Security Medic","Security Sergeant","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer")
-
-/datum/loadout_item/head/job/blasthelmet
- name = "General's Helmet"
- path = /obj/item/clothing/head/imperialhelmet
- restricted_desc = "Security and Command"
- restricted_roles = list("Warden","Detective","Security Medic","Security Sergeant","Security Officer","Civil Disputes Officer","Corrections Officer","Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer")
-
-/datum/loadout_item/head/job/navybluehoscap
- name = "Head of Security's Naval Cap"
- path = /obj/item/clothing/head/imperial/hos
- restricted_roles = list("Head of Security")
-
-/datum/loadout_item/head/job/navyblueofficerberet
- name = "Security officer's Navyblue beret"
- path = /obj/item/clothing/head/beret/sec/navyofficer
- restricted_roles = list("Security Officer","Security Medic","Security Sergeant",)
-
-/datum/loadout_item/head/job/solofficercap
- name = "Security officer's Sol Cap"
- path = /obj/item/clothing/head/sec/peacekeeper/sol
- restricted_roles = list("Security Officer","Security Medic","Security Sergeant",)
-
-/datum/loadout_item/head/job/soltrafficoff
- name = "Traffic Officer Cap"
- path = /obj/item/clothing/head/soltraffic
- restricted_roles = list("Security Officer","Security Medic","Security Sergeant","Civil Disputes Officer")
-
-/datum/loadout_item/head/job/navybluewardenberet
- name = "Warden's navyblue beret"
- path = /obj/item/clothing/head/beret/sec/navywarden
- restricted_roles = list("Warden")
-
-/datum/loadout_item/head/job/cybergoggles //Cyberpunk-P.I. Outfit
- name = "Type-34C Forensics Headwear"
- path = /obj/item/clothing/head/fedora/det_hat/cybergoggles
- restricted_roles = list("Detective")
- restricted_desc = "Detective"
-
-/datum/loadout_item/head/job/nursehat
- name = "Nurse Hat"
- path = /obj/item/clothing/head/nursehat
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist","Security Medic")
- restricted_desc = "Medical"
-
-/datum/loadout_item/head/job/imperial
- name = "Naval Officer Cap"
- path = /obj/item/clothing/head/imperial
- restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
- restricted_desc = "Command Staff"
-
-/datum/loadout_item/head/job/impgrey
- name = "Grey Naval Officer Cap"
- path = /obj/item/clothing/head/imperial/grey
- restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
- restricted_desc = "Command Staff"
-
-/datum/loadout_item/head/job/impred
- name = "Red Naval Officer Cap"
- path = /obj/item/clothing/head/imperial/red
- restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer")
- restricted_desc = "Command Staff"
-
-// JOB - Berets
-/datum/loadout_item/head/job/atmos_beret
- name = "Atmospherics Beret"
- path = /obj/item/clothing/head/beret/atmos
- restricted_roles = list("Station Engineer", "Atmospheric Technician", "Chief Engineer")
- restricted_desc = "Engineering"
-
-/datum/loadout_item/head/job/engi_beret
- name = "Engineering Beret"
- path = /obj/item/clothing/head/beret/engi
- restricted_roles = list("Station Engineer", "Atmospheric Technician", "Chief Engineer")
- restricted_desc = "Engineering"
-
-// SKYRAT EDIT REMOVAL BEGIN - MOVED TO COMMAND CLOTHING VENDOR
-/*
-/datum/loadout_item/head/job/CE_beret
- name = "Chief Engineer's Beret"
- path = /obj/item/clothing/head/beret/engi/ce
- restricted_roles = list("Chief Engineer")
- restricted_desc = "Chief Engineer"
-
-/datum/loadout_item/head/job/CE_beret_alt
- name = "Chief Engineer's White Beret"
- path = /obj/item/clothing/head/beret/engi/ce/alt
- restricted_roles = list("Chief Engineer")
- restricted_desc = "Chief Engineer"
-*/
-// SKYRAT EDIT REMOVAL END
-/datum/loadout_item/head/job/med
- name = "Medical Beret"
- path = /obj/item/clothing/head/beret/medical
- restricted_roles = list("Medical Doctor","Virologist", "Chemist", "Chief Medical Officer","Security Medic")
- restricted_desc = "Medical"
-
-/datum/loadout_item/head/job/med
- name = "Paramedic Beret"
- path = /obj/item/clothing/head/beret/medical/paramedic
- restricted_roles = list("Paramedic", "Chief Medical Officer")
- restricted_desc = "Paramedic"
-
-/datum/loadout_item/head/job/med_viro
- name = "Virologist Beret"
- path = /obj/item/clothing/head/beret/medical/virologist
- restricted_roles = list("Virologist", "Chief Medical Officer")
- restricted_desc = "Virology"
-
-/datum/loadout_item/head/job/med_chem
- name = "Chemist Beret"
- path = /obj/item/clothing/head/beret/medical/chemist
- restricted_roles = list("Chemist", "Chief Medical Officer")
- restricted_desc = "Chemistry"
-
-// SKYRAT EDIT REMOVAL BEGIN - MOVED TO COMMAND CLOTHING VENDOR
-/*
-/datum/loadout_item/head/job/CMO_beret_alt
- name = "Chief Medical Officer's Blue Beret"
- path = /obj/item/clothing/head/beret/medical/cmo
- restricted_roles = list("Chief Medical Officer")
- restricted_desc = "Chief Medical Officer"
-
-/datum/loadout_item/head/job/CMO_beret
- name = "Chief Medical Officer's Beret"
- path = /obj/item/clothing/head/beret/medical/cmo/alt
- restricted_roles = list("Chief Medical Officer")
- restricted_desc = "Chief Medical Officer"
-*/
-// SKYRAT EDIT REMOVAL END
-/datum/loadout_item/head/job/sci
- name = "Scientist's Beret"
- path = /obj/item/clothing/head/beret/science
- restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Vanguard Operative")
- restricted_desc = "Science"
-
-/datum/loadout_item/head/job/robo
- name = "Roboticist's Beret"
- path = /obj/item/clothing/head/beret/science/fancy/robo
- restricted_roles = list("Roboticist", "Research Director")
- restricted_desc = "Robotics"
-
-// SKYRAT EDIT REMOVAL BEGIN - MOVED TO COMMAND CLOTHING VENDOR
-/*
-/datum/loadout_item/head/job/RD_beret
- name = "Research Director's Beret"
- path = /obj/item/clothing/head/beret/science/fancy/rd
- restricted_roles = list("Research Director")
- restricted_desc = "Research Director"
-
-/datum/loadout_item/head/job/RD_beret_alt
- name = "Research Director's White Beret"
- path = /obj/item/clothing/head/beret/science/fancy/rd
- restricted_roles = list("Research Director")
- restricted_desc = "Research Director"
-
-/datum/loadout_item/head/job/QM_beret
- name = "Quartermaster's Beret"
- path = /obj/item/clothing/head/beret/cargo/qm
- restricted_roles = list("Quartermaster")
- restricted_desc = "Quartermaster"
-
-/datum/loadout_item/head/job/QM_beret
- name = "Quartermaster's White Beret"
- path = /obj/item/clothing/head/beret/cargo/qm/alt
- restricted_roles = list("Quartermaster")
- restricted_desc = "Quartermaster"
-
-/datum/loadout_item/head/job/Cap_beret
- name = "Captain's Beret"
- path = /obj/item/clothing/head/caphat/beret
- restricted_roles = list("Captain")
- restricted_desc = "Captain"
-
-/datum/loadout_item/head/job/Cap_beret_alt
- name = "Captain's White Beret"
- path = /obj/item/clothing/head/caphat/beret/alt
- restricted_roles = list("Captain")
- restricted_desc = "Captain"
-
-/datum/loadout_item/head/job/HOP_beret
- name = "Head of Personnel's Beret"
- path = /obj/item/clothing/head/hopcap/beret
- restricted_roles = list("Head of Personnel")
- restricted_desc = "Head of Personnel"
-
-/datum/loadout_item/head/job/HOP_beret_alt
- name = "Head of Personnel's White Beret"
- path = /obj/item/clothing/head/hopcap/beret/alt
- restricted_roles = list("Head of Personnel")
- restricted_desc = "Head of Personnel"
-*/
-// SKYRAT EDIT REMOVAL END
-
-/datum/loadout_item/head/brfed
- name = "Brown Fedora"
- path = /obj/item/clothing/head/fedora/fedbrown
- cost = 2
-
-/datum/loadout_item/head/blfed
- name = "Black Fedora"
- path = /obj/item/clothing/head/fedora/fedblack
- cost = 2
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/mask.dm b/modular_skyrat/modules/customization/modules/client/loadout/mask.dm
deleted file mode 100644
index 5a8492ea76f..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/mask.dm
+++ /dev/null
@@ -1,87 +0,0 @@
-/datum/loadout_item/mask
- category = LOADOUT_CATEGORY_MASK
-
-//MISC
-/datum/loadout_item/mask/balaclava
- name = "Balaclava"
- path = /obj/item/clothing/mask/balaclava
-
-/datum/loadout_item/mask/balaclavaadj
- name = "Adjustable Balaclava"
- path = /obj/item/clothing/mask/balaclavaadjust
-
-/datum/loadout_item/mask/balaclavathree
- name = "Three Hole Balaclava"
- path = /obj/item/clothing/mask/balaclava/threehole
-
-/datum/loadout_item/mask/balaclavagreen
- name = "Three Hole Green Balaclava"
- path = /obj/item/clothing/mask/balaclava/threehole/green
-
-
-/datum/loadout_item/mask/moustache
- name = "Fake moustache"
- path = /obj/item/clothing/mask/fakemoustache
-
-/datum/loadout_item/mask/bandana_red
- name = "Red Bandana"
- path = /obj/item/clothing/mask/bandana/red
-
-/datum/loadout_item/mask/bandana_blue
- name = "Blue Bandana"
- path = /obj/item/clothing/mask/bandana/blue
-
-/datum/loadout_item/mask/bandana_green
- name = "Green Bandana"
- path = /obj/item/clothing/mask/bandana/green
-
-/datum/loadout_item/mask/bandana_gold
- name = "Gold Bandana"
- path = /obj/item/clothing/mask/bandana/gold
-
-/datum/loadout_item/mask/bandana_black
- name = "Black Bandana"
- path = /obj/item/clothing/mask/bandana/black
-
-/datum/loadout_item/mask/bandana_skull
- name = "Skull Bandana"
- path = /obj/item/clothing/mask/bandana/skull
-
-/datum/loadout_item/mask/bandana_redft
- name = "Skin Tight Red Bandana"
- path = /obj/item/clothing/mask/bandana/red/ft
-
-/datum/loadout_item/mask/bandana_blueft
- name = "Skin Tight Blue Bandana"
- path = /obj/item/clothing/mask/bandana/blue/ft
-
-/datum/loadout_item/mask/bandana_greenft
- name = "Skin Tight Green Bandana"
- path = /obj/item/clothing/mask/bandana/green/ft
-
-/datum/loadout_item/mask/bandana_goldft
- name = "Skin Tight Gold Bandana"
- path = /obj/item/clothing/mask/bandana/gold/ft
-
-/datum/loadout_item/mask/bandana_blackft
- name = "Skin Tight Black Bandana"
- path = /obj/item/clothing/mask/bandana/black/ft
-
-/datum/loadout_item/mask/bandana_skullft
- name = "Skin Tight Skull Bandana"
- path = /obj/item/clothing/mask/bandana/skull/ft
-
-
-/datum/loadout_item/mask/gas_glass
- name = "Glass Gas Mask"
- path = /obj/item/clothing/mask/gas/glass
-
-//JOB RELATED
-/datum/loadout_item/mask/job
- subcategory = LOADOUT_SUBCATEGORY_JOB
-
-/datum/loadout_item/mask/job/surgical
- name = "Sterile Mask"
- path = /obj/item/clothing/mask/surgical
- restricted_roles = list("Chief Medical Officer", "Medical Doctor", "Virologist", "Chemist", "Geneticist", "Paramedic", "Psychologist","Security Medic")
- restricted_desc = "Medical"
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/neck.dm b/modular_skyrat/modules/customization/modules/client/loadout/neck.dm
deleted file mode 100644
index f5da78e67d4..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/neck.dm
+++ /dev/null
@@ -1,199 +0,0 @@
-/datum/loadout_item/neck
- category = LOADOUT_CATEGORY_NECK
-
-//MISC
-/datum/loadout_item/neck/headphones
- name = "Headphones"
- path = /obj/item/clothing/ears/headphones
-
-/datum/loadout_item/neck/choker
- name = "Choker"
- path = /obj/item/clothing/neck/human_petcollar/choker
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/neck/collar
- name = "Collar"
- path = /obj/item/clothing/neck/human_petcollar
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/leathercollar
- name = "Leather collar"
- path = /obj/item/clothing/neck/human_petcollar/leather
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/cbellcollar
- name = "Cowbell collar"
- path = /obj/item/clothing/neck/human_petcollar/locked/cowcollar
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/bellcollar
- name = "Bell collar"
- path = /obj/item/clothing/neck/human_petcollar/locked/bellcollar
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/spikecollar
- name = "Spike collar"
- path = /obj/item/clothing/neck/human_petcollar/locked/spikecollar
-
-/datum/loadout_item/neck/hcollar
- name = "Holocollar"
- path = /obj/item/clothing/neck/human_petcollar/locked/holocollar
-
-/datum/loadout_item/neck/ponchocowboy
- name = "Green cowboy poncho"
- path = /obj/item/clothing/neck/cowboylea
-
-/datum/loadout_item/neck/brownponchocowboy
- name = "Brown cowboy poncho"
- path = /obj/item/clothing/neck/ponchoranger
-
-/datum/loadout_item/neck/crosscollar
- name = "Cross collar"
- path = /obj/item/clothing/neck/human_petcollar/locked/cross
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/poly_cloak
- name = "Polychromic Cloak"
- path = /obj/item/clothing/neck/cloak/polychromic
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/poly_veil
- name = "Polychromic Veil"
- path = /obj/item/clothing/neck/cloak/polychromic/veil
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/poly_shroud
- name = "Polychromic Shroud"
- path = /obj/item/clothing/neck/cloak/polychromic/shroud
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/poly_boat
- name = "Polychromic Boatcloak"
- path = /obj/item/clothing/neck/cloak/polychromic/boat
- extra_info = LOADOUT_INFO_THREE_COLORS
-
-/datum/loadout_item/neck/stethoscope
- name = "Stethoscope"
- path = /obj/item/clothing/neck/stethoscope
- restricted_roles = list("Medical Doctor", "Chief Medical Officer","Security Medic")
-
-//SCARVES
-/datum/loadout_item/neck/scarf
- subcategory = LOADOUT_SUBCATEGORY_NECK_SCARVES
-
-/datum/loadout_item/neck/scarf/scarf
- name = "White Scarf"
- path = /obj/item/clothing/neck/scarf
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/neck/scarf/blackscarf
- name = "Black scarf"
- path = /obj/item/clothing/neck/scarf/black
-
-/datum/loadout_item/neck/scarf/redscarf
- name = "Red scarf"
- path = /obj/item/clothing/neck/scarf/red
-
-/datum/loadout_item/neck/scarf/greenscarf
- name = "Green scarf"
- path = /obj/item/clothing/neck/scarf/green
-
-/datum/loadout_item/neck/scarf/darkbluescarf
- name = "Dark blue scarf"
- path = /obj/item/clothing/neck/scarf/darkblue
-
-/datum/loadout_item/neck/scarf/purplescarf
- name = "Purple scarf"
- path = /obj/item/clothing/neck/scarf/purple
-
-/datum/loadout_item/neck/scarf/yellowscarf
- name = "Yellow scarf"
- path = /obj/item/clothing/neck/scarf/yellow
-
-/datum/loadout_item/neck/scarf/orangescarf
- name = "Orange scarf"
- path = /obj/item/clothing/neck/scarf/orange
-
-/datum/loadout_item/neck/scarf/cyanscarf
- name = "Cyan scarf"
- path = /obj/item/clothing/neck/scarf/cyan
-
-/datum/loadout_item/neck/scarf/stripedredscarf
- name = "Striped red scarf"
- path = /obj/item/clothing/neck/stripedredscarf
-
-/datum/loadout_item/neck/scarf/stripedbluescarf
- name = "Striped blue scarf"
- path = /obj/item/clothing/neck/stripedbluescarf
-
-/datum/loadout_item/neck/scarf/stripedgreenscarf
- name = "Striped green scarf"
- path = /obj/item/clothing/neck/stripedgreenscarf
-
-//TIES
-/datum/loadout_item/neck/tie
- subcategory = LOADOUT_SUBCATEGORY_NECK_TIE
-
-/datum/loadout_item/neck/tie/bluetie
- name = "Blue tie"
- path = /obj/item/clothing/neck/tie/blue
-
-/datum/loadout_item/neck/tie/redtie
- name = "Red tie"
- path = /obj/item/clothing/neck/tie/red
-
-/datum/loadout_item/neck/tie/blacktie
- name = "Black tie"
- path = /obj/item/clothing/neck/tie/black
-
-/datum/loadout_item/neck/tie/disco
- name = "Yellow Necktie"
- path = /obj/item/clothing/neck/tie/horrible
-
-/datum/loadout_item/neck/tie/discoproper
- name = "Horrible Necktie"
- path = /obj/item/clothing/neck/tie/disco
- restricted_roles = list("Detective")
- restricted_desc = "Superstar Detectives"
-
-//MANTLES
-
-/datum/loadout_item/neck/mantle
- name = "Mantle"
- path = /obj/item/clothing/neck/mantle
-
-/datum/loadout_item/neck/mantle/hopmantle
- name = "Head of Personnel's Mantle"
- path = /obj/item/clothing/neck/mantle/hopmantle
- restricted_roles = list("Head of Personnel")
-
-/datum/loadout_item/neck/mantle/cmomantle
- name = "Chief Medical Officer's Mantle"
- path = /obj/item/clothing/neck/mantle/cmomantle
- restricted_roles = list("Chief Medical Officer")
-
-/datum/loadout_item/neck/mantle/rdmantle
- name = "Research Director's Mantle"
- path = /obj/item/clothing/neck/mantle/rdmantle
- restricted_roles = list("Research Director")
-
-/datum/loadout_item/neck/mantle/cemantle
- name = "Chief Engineer's Mantle"
- path = /obj/item/clothing/neck/mantle/cemantle
- restricted_roles = list("Chief Engineer")
-
-/datum/loadout_item/neck/mantle/hosmantle
- name = "Head of Security's Mantle"
- path = /obj/item/clothing/neck/mantle/hosmantle
- restricted_roles = list("Head of Security")
-
-/datum/loadout_item/neck/mantle/bsmantle
- name = "Blueshield's Mantle"
- path = /obj/item/clothing/neck/mantle/bsmantle
- restricted_roles = list("Blueshield")
-
-/datum/loadout_item/neck/mantle/capmantle
- name = "Captain's Mantle"
- path = /obj/item/clothing/neck/mantle/capmantle
- restricted_roles = list("Captain")
-
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/shoes.dm b/modular_skyrat/modules/customization/modules/client/loadout/shoes.dm
deleted file mode 100644
index 494d6f3a73b..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/shoes.dm
+++ /dev/null
@@ -1,123 +0,0 @@
-/datum/loadout_item/shoes
- category = LOADOUT_CATEGORY_SHOES
-
-//MISC
-/datum/loadout_item/shoes/laceup
- name = "Laceup shoes"
- path = /obj/item/clothing/shoes/laceup
-
-/datum/loadout_item/shoes/workboots
- name = "Work boots"
- path = /obj/item/clothing/shoes/workboots
-
-/datum/loadout_item/shoes/jackboots
- name = "Jackboots"
- path = /obj/item/clothing/shoes/jackboots
-
-// Thedragmeme's donator reward, they've decided to make them available to everybody.
-/datum/loadout_item/shoes/jackboots/heel
- name = "High-heel Jackboots"
- path = /obj/item/clothing/shoes/jackboots/heel
-
-/datum/loadout_item/shoes/winterboots
- name = "Winter boots"
- path = /obj/item/clothing/shoes/winterboots
-
-/datum/loadout_item/shoes/sandals
- name = "Sandals"
- path = /obj/item/clothing/shoes/sandal
-
-/datum/loadout_item/shoes/blackshoes
- name = "Black shoes"
- path = /obj/item/clothing/shoes/sneakers/black
-
-/datum/loadout_item/shoes/brownshoes
- name = "Brown shoes"
- path = /obj/item/clothing/shoes/sneakers/brown
-
-/datum/loadout_item/shoes/whiteshoes
- name = "White shoes"
- path = /obj/item/clothing/shoes/sneakers/white
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/shoes/sportshoes
- name = "Sport Shoes"
- path = /obj/item/clothing/shoes/sports
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/shoes/gildedcuffs
- name = "Gilded leg wraps"
- path= /obj/item/clothing/shoes/wraps
-
-/datum/loadout_item/shoes/silvercuffs
- name = "Silver leg wraps"
- path= /obj/item/clothing/shoes/wraps/silver
-
-/datum/loadout_item/shoes/redcuffs
- name = "Red leg wraps"
- path= /obj/item/clothing/shoes/wraps/red
-
-/datum/loadout_item/shoes/bluecuffs
- name = "Blue leg wraps"
- path= /obj/item/clothing/shoes/wraps/blue
-
-/datum/loadout_item/shoes/clothwrap
- name = "Colourable Cloth Wraps"
- path = /obj/item/clothing/shoes/wraps/poly
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/shoes/high_heels
- name = "High Heels"
- path = /obj/item/clothing/shoes/high_heels
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/*/datum/loadout_item/shoes/christmasbootsr - it aint christmas!
- name = "Red Christmas Boots"
- category = SLOT_SHOES
- path= /obj/item/clothing/shoes/winterboots/christmasbootsr
-
-/datum/loadout_item/shoes/christmasbootsg
- name = "Green Christmas Boots"
- category = SLOT_SHOES
- path= /obj/item/clothing/shoes/winterboots/christmasbootsg
-
-/datum/loadout_item/shoes/santaboots
- name = "Santa Boots"
- category = SLOT_SHOES
- path= /obj/item/clothing/shoes/winterboots/santaboots*/
-
-/datum/loadout_item/shoes/cowboyboots
- name = "Cowboy Boots, Brown"
- path = /obj/item/clothing/shoes/cowboyboots
-
-/datum/loadout_item/shoes/cowboyboots/black
- name = "Cowboy Boots, Black"
- path = /obj/item/clothing/shoes/cowboyboots/black
-
-/datum/loadout_item/shoes/cowboyboots/blackproper
- name = "Cowboy Boots, Hangman Black"
- path = /obj/item/clothing/shoes/cowboy/black
-
-/datum/loadout_item/shoes/junhle
- name = "Brown Jackboots"
- path = /obj/item/clothing/shoes/jungleboots
-
-/datum/loadout_item/shoes/thighboot
- name = "Thigh Boots"
- path = /obj/item/clothing/shoes/jackboots/thigh
-
-/datum/loadout_item/shoes/kneeboot
- name = "Knee Boots"
- path = /obj/item/clothing/shoes/jackboots/knee
-
-/datum/loadout_item/shoes/timbs
- name = "Fashionable Boots"
- path = /obj/item/clothing/shoes/jackboots/timbs
-
-/datum/loadout_item/shoes/disco
- name = "Green Snakeskin Shoes"
- path = /obj/item/clothing/shoes/discoshoes
-
-
-
-
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/suit.dm b/modular_skyrat/modules/customization/modules/client/loadout/suit.dm
deleted file mode 100644
index 7d0177f1ccd..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/suit.dm
+++ /dev/null
@@ -1,525 +0,0 @@
-/datum/loadout_item/suit
- category = LOADOUT_CATEGORY_SUIT
-
-//MISC
-/datum/loadout_item/suit/poncho
- name = "Poncho"
- path = /obj/item/clothing/suit/poncho
-
-/datum/loadout_item/suit/ponchogreen
- name = "Green poncho"
- path = /obj/item/clothing/suit/poncho/green
-
-/datum/loadout_item/suit/ponchored
- name = "Red poncho"
- path = /obj/item/clothing/suit/poncho/red
-
-/datum/loadout_item/suit/redhood
- name = "Red cloak"
- path = /obj/item/clothing/suit/hooded/cloak/david
- cost = 3
-
-/datum/loadout_item/suit/ianshirt
- name = "Ian Shirt"
- path = /obj/item/clothing/suit/ianshirt
-
-/datum/loadout_item/suit/wornshirt
- name = "Worn Shirt"
- path = /obj/item/clothing/suit/wornshirt
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/suit/tailcoat
- name = "Tailcoat"
- path = /obj/item/clothing/suit/costume/tailcoat
-
-/datum/loadout_item/suit/duster
- name = "Colorable Duster"
- path = /obj/item/clothing/suit/duster/colorable
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/suit/peacoat
- name = "Colorable Peacoat"
- path = /obj/item/clothing/suit/toggle/peacoat
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/suit/dresscoat
- name = "Black Dresscoat"
- path = /obj/item/clothing/suit/costume/vic_dresscoat
-
-/datum/loadout_item/suit/dresscoat_red
- name = "Red Dresscoat"
- path = /obj/item/clothing/suit/costume/vic_dresscoat/red
-
-/datum/loadout_item/suit/trackjacket
- name = "Track Jacket"
- path = /obj/item/clothing/suit/toggle/trackjacket
-
-/*Flannels go inside Misc*/
-/datum/loadout_item/suit/flannel_black
- name = "Black Flannel"
- path = /obj/item/clothing/suit/toggle/jacket/flannel
-
-/datum/loadout_item/suit/flannel_red
- name = "Red Flannel"
- path = /obj/item/clothing/suit/toggle/jacket/flannel/red
-
-/datum/loadout_item/suit/flannel_aqua
- name = "Aqua Flannel"
- path = /obj/item/clothing/suit/toggle/jacket/flannel/aqua
-
-/datum/loadout_item/suit/flannel_brown
- name = "Brown Flannel"
- path = /obj/item/clothing/suit/toggle/jacket/flannel/brown
-
-/*Hawaiian Shirts*/
-/datum/loadout_item/suit/hawaiian_blue
- name = "Blue Hawaiian Shirt"
- path = /obj/item/clothing/suit/hawaiian_blue
-
-/datum/loadout_item/suit/hawaiian_orange
- name = "Orange Hawaiian Shirt"
- path = /obj/item/clothing/suit/hawaiian_orange
-
-/datum/loadout_item/suit/hawaiian_purple
- name = "Purple Hawaiian Shirt"
- path = /obj/item/clothing/suit/hawaiian_purple
-
-/datum/loadout_item/suit/hawaiian_green
- name = "Green Hawaiian Shirt"
- path = /obj/item/clothing/suit/hawaiian_green
-
-/datum/loadout_item/suit/frenchtrench
- name = "Blue Trenchcoat"
- path = /obj/item/clothing/suit/frenchtrench
-
-/datum/loadout_item/suit/cossak
- name = "Ukrainian Coat"
- path = /obj/item/clothing/suit/cossack
-
-//COATS
-/datum/loadout_item/suit/coat
- subcategory = LOADOUT_SUBCATEGORY_SUIT_COATS
-
-/datum/loadout_item/suit/coat/normal
- name = "Winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat
-
-/datum/loadout_item/suit/coat/aformal
- name = "Assistant's formal winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/aformal
-
-/datum/loadout_item/suit/coat/runed
- name = "Runed winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/narsie/fake
-
-/datum/loadout_item/suit/coat/brass
- name = "Brass winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/ratvar/fake
-
-/datum/loadout_item/suit/coat/korea
- name = "Eastern winter coat"
- path = /obj/item/clothing/suit/koreacoat
-
-/datum/loadout_item/suit/coat/czech
- name = "Modern winter coat"
- path = /obj/item/clothing/suit/modernwintercoatthing
-
-/datum/loadout_item/suit/coat/parka
- name = "Falls Parka"
- path = /obj/item/clothing/suit/fallsparka
-
-/datum/loadout_item/suit/coat/poly
- name = "Polychromic Wintercoat"
- path = /obj/item/clothing/suit/hooded/wintercoat/polychromic
- extra_info = LOADOUT_INFO_THREE_COLORS
- cost = 3
-
-/datum/loadout_item/suit/coat/urban
- name = "Urban Coat"
- path = /obj/item/clothing/suit/urban/polychromic
- extra_info = LOADOUT_INFO_THREE_COLORS
- cost = 3
-
-/datum/loadout_item/suit/coat/maxson
- name = "Fancy Brown Coat"
- path = /obj/item/clothing/suit/brownbattlecoat
- cost = 3
-
-/datum/loadout_item/suit/coat/bossu
- name = "Fancy Black Coat"
- path = /obj/item/clothing/suit/blackfurrich
- cost = 3
-
-/datum/loadout_item/suit/coat/custom
- name = "Alternate Winter Coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/custom
- cost= 2
-
-
-//JACKETS
-/datum/loadout_item/suit/jacket
- subcategory = LOADOUT_SUBCATEGORY_SUIT_JACKETS
-
-/datum/loadout_item/suit/jacket/dutchjacket
- name = "Dutch Jacket"
- path = /obj/item/clothing/suit/dutchjacketsr
-
-/datum/loadout_item/suit/jacket/caretaker
- name = "Caretaker Jacket"
- path = /obj/item/clothing/suit/victoriantailcoatbutler
-
-/datum/loadout_item/suit/jacket/yakuzajacket
- name = "Asian Jacket"
- path = /obj/item/clothing/suit/yakuza
-
-/datum/loadout_item/suit/jacket/suitblue
- name = "Blue Suit Jacket"
- path = /obj/item/clothing/suit/toggle/lawyer
-
-/datum/loadout_item/suit/jacket/suitpurple
- name = "Purple Suit Jacket"
- path = /obj/item/clothing/suit/toggle/lawyer/purple
-
-/datum/loadout_item/suit/jacket/suitblack
- name = "Black Suit Jacket"
- path = /obj/item/clothing/suit/toggle/lawyer/black
-
-/datum/loadout_item/suit/jacket/suitblackbetter
- name = "Light Black Suit Jacket"
- path = /obj/item/clothing/suit/toggle/lawyer/black/better
-
-/datum/loadout_item/suit/jacket/suitwhite
- name = "White Suit Jacket"
- path = /obj/item/clothing/suit/texas
-
-/datum/loadout_item/suit/jacket/jacketbomber
- name = "Bomber jacket"
- path = /obj/item/clothing/suit/jacket
-
-/datum/loadout_item/suit/jacket/jacketbomber_alt
- name = "Bomber Jacket w/ Zipper"
- path = /obj/item/clothing/suit/toggle/jacket
-
-/datum/loadout_item/suit/jacket/jacketleather
- name = "Leather jacket"
- path = /obj/item/clothing/suit/jacket/leather
-
-/datum/loadout_item/suit/jacket/overcoatleather
- name = "Leather overcoat"
- path = /obj/item/clothing/suit/jacket/leather/overcoat
-
-/datum/loadout_item/suit/jacket/polyjacketleather
- name = "Colorable leather jacket"
- path = /obj/item/clothing/suit/jacket/leather/polychromic
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/suit/jacket/woolcoat
- name = "Leather overcoat"
- path = /obj/item/clothing/suit/woolcoat
-
-/datum/loadout_item/suit/jacket/jacketpuffer
- name = "Puffer jacket"
- path = /obj/item/clothing/suit/jacket/puffer
-
-/datum/loadout_item/suit/jacket/vestpuffer
- name = "Puffer vest"
- path = /obj/item/clothing/suit/jacket/puffer/vest
-
-/datum/loadout_item/suit/jacket/jacketlettermanbrown
- name = "Brown letterman jacket"
- path = /obj/item/clothing/suit/jacket/letterman
-
-/datum/loadout_item/suit/jacket/jacketlettermanred
- name = "Red letterman jacket"
- path = /obj/item/clothing/suit/jacket/letterman_red
-
-/datum/loadout_item/suit/jacket/jacketlettermanNT
- name = "Nanotrasen letterman jacket"
- path = /obj/item/clothing/suit/jacket/letterman_nanotrasen
-
-/datum/loadout_item/suit/jacket/militaryjacket
- name = "Military Jacket"
- path = /obj/item/clothing/suit/jacket/miljacket
-
-/datum/loadout_item/suit/jacket/flakjack
- name = "Flak Jacket"
- path = /obj/item/clothing/suit/flakjack
- cost = 2
-
-/datum/loadout_item/suit/jacket/cardigan
- name = "Cardigan"
- path = /obj/item/clothing/suit/toggle/jacket/cardigan
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-//HOODIES
-/datum/loadout_item/suit/hoodie
- subcategory = LOADOUT_SUBCATEGORY_SUIT_HOODIES
-
-/datum/loadout_item/suit/hoodie/grey
- name = "Grey Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie
-
-/datum/loadout_item/suit/hoodie/black
- name = "Black Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/black
-
-/datum/loadout_item/suit/hoodie/red
- name = "Red Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/red
-
-/datum/loadout_item/suit/hoodie/blue
- name = "Blue Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/blue
-
-/datum/loadout_item/suit/hoodie/green
- name = "Green Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/green
-
-/datum/loadout_item/suit/hoodie/orange
- name = "orange hoodies"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/orange
-
-/datum/loadout_item/suit/hoodie/yellow
- name = "Yellow Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/yellow
-
-/datum/loadout_item/suit/hoodie/white
- name = "White Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/white
-
-/datum/loadout_item/suit/hoodie/cti
- name = "CTI Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/cti
-
-/datum/loadout_item/suit/hoodie/mu
- name = "Mojave University Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/mu
-
-/datum/loadout_item/suit/hoodie/nt
- name = "NT Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/nt
-
-/datum/loadout_item/suit/hoodie/smw
- name = "SMW Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/smw
-
-/datum/loadout_item/suit/hoodie/nrti
- name = "NRTI Hoodie"
- path = /obj/item/clothing/suit/storage/toggle/hoodie/nrti
-
-//JOB RELATED
-/datum/loadout_item/suit/job
- subcategory = LOADOUT_SUBCATEGORY_JOB
-
-/datum/loadout_item/suit/job/coat_med
- name = "Medical winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/medical
- restricted_roles = list("Chief Medical Officer", "Medical Doctor") // Reserve it to Medical Doctors and their boss, the Chief Medical Officer
-
-/datum/loadout_item/suit/job/coat_paramedic
- name = "Paramedic winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/paramedic
- restricted_roles = list("Chief Medical Officer", "Paramedic") // Reserve it to Paramedics and their boss, the Chief Medical Officer
-
-/datum/loadout_item/suit/job/coat_robotics
- name = "Robotics winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/robotics
- restricted_roles = list("Research Director", "Roboticist")
-
-/datum/loadout_item/suit/job/coat_sci
- name = "Science winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/science
- restricted_roles = list("Research Director", "Scientist", "Roboticist", "Vanguard Operative") // Reserve it to the Science Departement
-
-/datum/loadout_item/suit/job/coat_eng
- name = "Engineering winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/engineering
- restricted_roles = list("Chief Engineer", "Station Engineer") // Reserve it to Station Engineers and their boss, the Chief Engineer
-
-/datum/loadout_item/suit/job/coat_atmos
- name = "Atmospherics winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/engineering/atmos
- restricted_roles = list("Chief Engineer", "Atmospheric Technician") // Reserve it to Atmos Techs and their boss, the Chief Engineer
-
-/datum/loadout_item/suit/job/coat_hydro
- name = "Hydroponics winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/hydro
- restricted_roles = list("Head of Personnel", "Botanist") // Reserve it to Botanists and their boss, the Head of Personnel
-
-/datum/loadout_item/suit/job/coat_bar
- name = "Bartender winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/bartender
- restricted_roles = list("Head of Personnel", "Bartender") //Reserved for Bartenders and their head-of-staff
-
-/datum/loadout_item/suit/job/coat_cargo
- name = "Cargo winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/cargo
- restricted_roles = list("Quartermaster", "Cargo Technician") // Reserve it to Cargo Techs and their boss, the Quartermaster
-
-/datum/loadout_item/suit/job/coat_miner
- name = "Mining winter coat"
- path = /obj/item/clothing/suit/hooded/wintercoat/miner
- restricted_roles = list("Quartermaster", "Shaft Miner") // Reserve it to Miners and their boss, the Quartermaster
-
-// SKYRAT EDIT REMOVAL - COMMAND CLOTHING VENDOR
-/*
-/datum/loadout_item/suit/job/navybluejackethos
- name = "head of security's navyblue jacket"
- path = /obj/item/clothing/suit/armor/hos/navyblue
- restricted_roles = list("Head of Security")
-*/
-// SKYRAT EDIT END
-
-/datum/loadout_item/suit/job/navybluejacketofficer
- name = "security officer's navyblue jacket"
- path = /obj/item/clothing/suit/armor/navyblue
- restricted_roles = list("Security Officer","Security Medic","Security Sergeant","Head of Security", "Warden") // I aint making a medic one, maybe i'll add some rank thing from cm or civ for it
-
-/datum/loadout_item/suit/job/navybluejacketwarden
- name = "warden navyblue jacket"
- path = /obj/item/clothing/suit/armor/vest/warden/navyblue
- restricted_roles = list("Warden")
-
-/datum/loadout_item/suit/job/security_jacket
- name = "Security Jacket"
- path = /obj/item/clothing/suit/toggle/jacket/sec
- restricted_roles = list("Head of Security", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant") //Not giving this one to CDOs or COs because it's actually better than the one they spawn with
- //restricted_desc = "All Security Personnel" //Liar...
-
-/datum/loadout_item/suit/job/cossak
- name = "Ukrainian Security Jacket"
- path = /obj/item/clothing/suit/cossack/sec
- restricted_roles = list("Head of Security", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Corrections Officer")
- restricted_desc = "All Security Personnel"
-
-/datum/loadout_item/suit/job/brit
- name = "High Vis Armored Vest"
- path = /obj/item/clothing/suit/toggle/brit/sec
- restricted_roles = list("Head of Security", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Corrections Officer")
- restricted_desc = "All Security Personnel"
-
-/datum/loadout_item/suit/job/british_jacket
- name = "Peacekeeper Officer Coat"
- path = /obj/item/clothing/suit/british_officer
- restricted_roles = list("Head of Security", "Warden","Detective","Security Sergeant")
- restricted_desc = "All High Ranking Security Personnel"
-
-/datum/loadout_item/suit/job/engi_jacket
- name = "Engineering Jacket"
- path = /obj/item/clothing/suit/toggle/jacket/engi
- restricted_roles = list("Chief Engineer", "Station Engineer", "Atmospheric Technician")
- restricted_desc = "All Engineering Personnel"
-
-/datum/loadout_item/suit/job/sci_jacket
- name = "Science Jacket"
- path = /obj/item/clothing/suit/toggle/jacket/sci
- restricted_roles = list("Research Director", "Scientist", "Roboticist", "Geneticist", "Vanguard Operative")
- restricted_desc = "All Science Personnel"
-
-/datum/loadout_item/suit/job/med_jacket
- name = "Medbay Jacket"
- path = /obj/item/clothing/suit/toggle/jacket/med
- restricted_roles = list("Chief Medical Officer", "Medical Doctor", "Paramedic", "Chemist", "Virologist")
- restricted_desc = "All Medical Personnel"
-
-/datum/loadout_item/suit/job/supply_jacket
- name = "Supply Jacket"
- path = /obj/item/clothing/suit/toggle/jacket/supply
- restricted_roles = list("Quartermaster", "Cargo Technician", "Miner")
- restricted_desc = "All Cargo Personnel"
-
-/datum/loadout_item/suit/job/supply_gorka_jacket
- name = "Supply Gorka Jacket"
- path = /obj/item/clothing/suit/gorka/supply
- restricted_roles = list("Quartermaster", "Cargo Technician", "Miner")
- restricted_desc = "All Cargo Personnel"
-
-// SKYRAT EDIT REMOVAL - COMMAND CLOTHING VENDOR
-/*
-/datum/loadout_item/suit/job/supply_head_jacket
- name = "Quartermaster's Jacket"
- path = /obj/item/clothing/suit/toggle/jacket/supply/head
- restricted_roles = list("Quartermaster")
- restricted_desc = "Quartermaster Only"
-
-/datum/loadout_item/suit/job/supply_head_jacket
- name = "Quartermaster's Fur Jacket"
- path = /obj/item/clothing/suit/brownfurrich
- restricted_roles = list("Quartermaster")
- restricted_desc = "Quartermaster Only"
-*/
-// SKYRAT EDIT REMOVAL END
-
-/datum/loadout_item/suit/job/labcoat_parared
- name = "Red Paramedic Labcoat"
- path = /obj/item/clothing/suit/toggle/labcoat/para_red
- restricted_roles = list("Chief Medical Officer", "Paramedic","Security Medic") // its a medic jacket anyway screw you
- restricted_desc = "Medical First Responders"
-
-/datum/loadout_item/suit/job/labcoat_highvis
- name = "High-Vis Labcoat"
- path = /obj/item/clothing/suit/toggle/labcoat/highvis
- restricted_roles = list("Chief Medical Officer", "Paramedic", "Atmospheric Technician", "Detective", "Security Medic") // why does the atmos get this? sec med is more of a first responder lmao
- restricted_desc = "First Responders and Detective"
-
-/datum/loadout_item/suit/job/discojacket
- name = "Disco Ass Blazer"
- path = /obj/item/clothing/suit/discoblazer
- restricted_roles = list("Detective")
- restricted_desc = "Superstar Detectives"
-
-/datum/loadout_item/suit/job/deckard
- name = "Runner Coat"
- path = /obj/item/clothing/suit/toggle/deckard
- restricted_roles = list("Detective")
- restricted_desc = "Hungry Detectives"
-
-//Trekkie things.
-/datum/loadout_item/suit/job/trek/fedcoat
- name = "Federation Jacket"
- path = /obj/item/clothing/suit/storage/fluff/fedcoat
-
-/datum/loadout_item/suit/job/trek/fedmedsci
- name = "Medsci fed jacket"
- path = /obj/item/clothing/suit/storage/fluff/fedcoat/medsci
- restricted_desc = "Medical and Science"
- restricted_roles = list("Chief Medical Officer","Medical Doctor","Chemist","Virologist","Paramedic","Security Medic","Psychologist","Geneticist","Research Director","Scientist","Roboticist","Vanguard Operative")
-
-/datum/loadout_item/suit/job/trek/fedeng
- name = "Ops/Sec fed Jacket"
- path = /obj/item/clothing/suit/storage/fluff/fedcoat/eng
- restricted_desc = "Engineering, Security, and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Warden","Security Medic","Security Sergeant","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-//Modern trekkie
-/datum/loadout_item/suit/job/trek/fedcoatmodern
- name = "Modern fed jacket"
- path = /obj/item/clothing/suit/storage/fluff/mfedcoat
-
-/datum/loadout_item/suit/job/trek/fedcoatmodernmedsci
- name = "Modern medsci jacket"
- path = /obj/item/clothing/suit/storage/fluff/mfedcoat/medsci
- restricted_desc = "Medical and Science"
- restricted_roles = list("Chief Medical Officer","Medical Doctor","Paramedic","Chemist","Virologist","Security Medic","Psychologist","Geneticist","Research Director","Scientist","Roboticist","Vanguard Operative")
-
-/datum/loadout_item/suit/job/trek/fedcoatmoderneng
- name = "Modern ops jacket"
- path = /obj/item/clothing/suit/storage/fluff/mfedcoat/eng
- restricted_desc = "Engineering and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-/datum/loadout_item/suit/job/trek/fedcoatmodernsec
- name = "Modern sec jacket"
- path = /obj/item/clothing/suit/storage/fluff/mfedcoat/sec
- restricted_roles = list("Head of Security", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant","Civil Disputes Officer","Corrections Officer")
- restricted_desc = "All Security Personnel"
-
-/datum/loadout_item/suit/bltrench
- name = "Black Trenchcoat"
- path = /obj/item/clothing/suit/trenchblack
- cost = 2
-
-/datum/loadout_item/suit/brtrench
- name = "Brown Trenchcoat"
- path = /obj/item/clothing/suit/trenchbrown
- cost = 2
diff --git a/modular_skyrat/modules/customization/modules/client/loadout/uniform.dm b/modular_skyrat/modules/customization/modules/client/loadout/uniform.dm
deleted file mode 100644
index 6402c1dea62..00000000000
--- a/modular_skyrat/modules/customization/modules/client/loadout/uniform.dm
+++ /dev/null
@@ -1,1214 +0,0 @@
-/datum/loadout_item/uniform
- category = LOADOUT_CATEGORY_UNIFORM
-
-//MISC
-/datum/loadout_item/uniform/gandalf
- name = "Nostalgic Costume"
- path = /obj/item/clothing/under/nostalgiacritic
- ckeywhitelist = list("gandalf2k15")
-
-/datum/loadout_item/uniform/assistantformal
- name = "Assistant's formal uniform"
- path = /obj/item/clothing/under/misc/assistantformal
-
-/datum/loadout_item/uniform/greyshirt
- name = "Grey Shirt"
- path = /obj/item/clothing/under/misc/greyshirt
-
-/datum/loadout_item/uniform/maidcostume
- name = "Maid costume"
- path = /obj/item/clothing/under/costume/maid
-
-/datum/loadout_item/uniform/mailmanuniform
- name = "Mailman's jumpsuit"
- path = /obj/item/clothing/under/misc/mailman
-
-/datum/loadout_item/uniform/croptop
- name = "Croptop"
- path = /obj/item/clothing/under/croptop
-
-/datum/loadout_item/uniform/kilt
- name = "Kilt"
- path = /obj/item/clothing/under/costume/kilt
-
-/datum/loadout_item/uniform/royalkilt
- name = "Royal Kilt"
- path = /obj/item/clothing/under/misc/royalkilt
-
-/datum/loadout_item/uniform/poly_kilt
- name = "Polychromic Kilt"
- path = /obj/item/clothing/under/misc/poly_kilt
- extra_info = LOADOUT_INFO_THREE_COLORS
- cost = 3
-
-/datum/loadout_item/uniform/poly_button_up
- name = "Polychromic Button Up Shirt"
- path = /obj/item/clothing/under/misc/poly_shirt
- extra_info = LOADOUT_INFO_THREE_COLORS
- cost = 3
-
-/datum/loadout_item/uniform/poly_tri_jumpsuit
- name = "Polychromic Tri-Tone Jumpsuit"
- path = /obj/item/clothing/under/misc/polyjumpsuit
- extra_info = LOADOUT_INFO_THREE_COLORS
- cost = 3
-
-/datum/loadout_item/uniform/poly_tanktop
- name = "Polychromic Tanktop"
- path = /obj/item/clothing/under/misc/poly_tanktop
- extra_info = LOADOUT_INFO_THREE_COLORS
- cost = 3
-
-/datum/loadout_item/uniform/poly_tanktop_fem
- name = "Polychromic Feminine Tanktop"
- path = /obj/item/clothing/under/misc/poly_tanktop/female
- extra_info = LOADOUT_INFO_THREE_COLORS
- cost = 3
-
-/datum/loadout_item/uniform/gear_harnesses
- name = "Gear Harness"
- path = /obj/item/clothing/under/misc/gear_harness
-
-/datum/loadout_item/uniform/loincloth //Sensor version for station crew
- name = "Leather Loincloth"
- path = /obj/item/clothing/under/costume/loincloth/sensor
-
-/datum/loadout_item/uniform/loincloth/cloth //Sensor version for station crew
- name = "Cloth Loincloth"
- path = /obj/item/clothing/under/costume/loincloth/cloth/sensor
- extra_info = LOADOUT_INFO_ONE_COLOR
-
-/datum/loadout_item/uniform/pinkstripper
- name = "Pink stripper outfit"
- path = /obj/item/clothing/under/misc/stripper
- cost = 3
-
-/datum/loadout_item/uniform/greenstripper
- name = "Green stripper outfit"
- path = /obj/item/clothing/under/misc/stripper/green
- cost = 3
-
-/datum/loadout_item/uniform/bunnysuit
- name = "Bunny suit"
- path = /obj/item/clothing/under/misc/stripper/bunnysuit
- cost = 3
-
-/datum/loadout_item/uniform/bunnysuit/white
- name = "Bunny suit, white"
- path = /obj/item/clothing/under/misc/stripper/bunnysuit/white
- cost = 3
-
-/datum/loadout_item/uniform/qipao
- name = "Qipao, Black"
- path = /obj/item/clothing/under/costume/qipao
- cost = 3
-
-/datum/loadout_item/uniform/qipao/white
- name = "Qipao, White"
- path = /obj/item/clothing/under/costume/qipao/white
- cost = 3
-
-/datum/loadout_item/uniform/qipao/red
- name = "Qipao, Red"
- path = /obj/item/clothing/under/costume/qipao/red
- cost = 3
-
-/datum/loadout_item/uniform/cheongsam
- name = "Cheongsam, Black"
- path = /obj/item/clothing/under/costume/cheongsam
- cost = 3
-
-/datum/loadout_item/uniform/cheongsam/white
- name = "Cheongsam, White"
- path = /obj/item/clothing/under/costume/cheongsam/white
- cost = 3
-
-/datum/loadout_item/uniform/cheongsam/red
- name = "Cheongsam, Red"
- path = /obj/item/clothing/under/costume/cheongsam/red
- cost = 3
-
-/datum/loadout_item/uniform/kimono
- name = "Kimono, White"
- path = /obj/item/clothing/under/costume/kimono
- cost = 1
-
-/datum/loadout_item/uniform/kimono/dark
- name = "Kimono, Black"
- path = /obj/item/clothing/under/costume/kimono/dark
- cost = 1
-
-/datum/loadout_item/uniform/kimono/sakura
- name = "Kimono, Sakura"
- path = /obj/item/clothing/under/costume/kimono/sakura
- cost = 1
-
-/datum/loadout_item/uniform/kimono/fancy
- name = "Kimono, Fancy"
- path = /obj/item/clothing/under/costume/kimono/fancy
- cost = 1
-
-/datum/loadout_item/uniform/kamishimo
- name = "Kamishimo"
- path = /obj/item/clothing/under/costume/kamishimo
- cost = 1
-
-/datum/loadout_item/uniform/corset
- name = "Black Corset"
- path = /obj/item/clothing/under/dress/corset
- cost = 1
-
-/datum/loadout_item/uniform/chaps
- name = "Black Chaps"
- path = /obj/item/clothing/under/pants/chaps
- cost = 1
-
-/datum/loadout_item/uniform/taccas
- name = "Tacticasual Uniform"
- path = /obj/item/clothing/under/misc/taccas
- cost = 1
-
-/datum/loadout_item/uniform/tracky
- name = "Blue Tracksuit"
- path = /obj/item/clothing/under/misc/bluetracksuit
- cost = 1
-
-
-
-//SUITS
-/datum/loadout_item/uniform/suit
- subcategory = LOADOUT_SUBCATEGORY_UNIFORM_SUITS
-
-/datum/loadout_item/uniform/suit/suitblack
- name = "Black suit"
- path = /obj/item/clothing/under/suit/black
-
-/datum/loadout_item/uniform/suit/suitgreen
- name = "Green suit"
- path = /obj/item/clothing/under/suit/green
-
-/datum/loadout_item/uniform/suit/suitred
- name = "Red suit"
- path = /obj/item/clothing/under/suit/red
-
-/datum/loadout_item/uniform/suit/suitcharcoal
- name = "Charcoal suit"
- path = /obj/item/clothing/under/suit/charcoal
-
-/datum/loadout_item/uniform/suit/suitnavy
- name = "Navy suit"
- path = /obj/item/clothing/under/suit/navy
-
-/datum/loadout_item/uniform/suit/suitburgundy
- name = "Burgundy suit"
- path = /obj/item/clothing/under/suit/burgundy
-
-/datum/loadout_item/uniform/suit/suittan
- name = "Tan suit"
- path = /obj/item/clothing/under/suit/tan
-
-/datum/loadout_item/uniform/suit/suitwhite
- name = "White suit"
- path = /obj/item/clothing/under/suit/white
-
-/datum/loadout_item/uniform/suit/scarface
- name = "Cuban suit"
- path = /obj/item/clothing/under/suit/white/scarface
-
-/datum/loadout_item/uniform/suit/vice
- name = "Grey and Black suit"
- path = /obj/item/clothing/under/misc/vice_officer
-
-/datum/loadout_item/uniform/suit/greyskirty
- name = "Grey Suit Skirt"
- path = /obj/item/clothing/under/rank/civilian/lawyer/grey/skirtybaby
-
-/datum/loadout_item/uniform/suit/blackskirty
- name = "Black Suit Skirt"
- path = /obj/item/clothing/under/rank/civilian/lawyer/black/skirtybaby
-
-/datum/loadout_item/uniform/suit/gentleskirty
- name = "Gentle Suit Skirt"
- path = /obj/item/clothing/under/rank/civilian/lawyer/gentle/skirtybaby
-
-/datum/loadout_item/uniform/suit/gentleskirty
- name = "Gentle Suit Skirt"
- path = /obj/item/clothing/under/rank/civilian/lawyer/gentle/skirtybaby
-
-/datum/loadout_item/uniform/suit/burgskirty
- name = "Burgundy Suit Skirt"
- path = /obj/item/clothing/under/rank/civilian/lawyer/red/skirtybaby
-
-/datum/loadout_item/uniform/suit/tanskirty
- name = "Tan Suit Skirt"
- path = /obj/item/clothing/under/rank/civilian/lawyer/tan/skirtybaby
-
-/datum/loadout_item/uniform/suit/blueskirty
- name = "Blue Suit Skirt"
- path = /obj/item/clothing/under/rank/civilian/lawyer/blue/skirtybaby
-
-/datum/loadout_item/uniform/suit/greenskirty
- name = "Green Suit Skirt"
- path = /obj/item/clothing/under/rank/civilian/lawyer/green/skirtybaby
-
-/datum/loadout_item/uniform/suit/femblack
- name = "Feminine Suit"
- path = /obj/item/clothing/under/suit/black/female
-
-/datum/loadout_item/uniform/suit/femblacksk
- name = "Feminine Skirt Suit"
- path = /obj/item/clothing/under/suit/black/female/skirt
-
-/datum/loadout_item/uniform/suit/femblackskirt
- name = "Executive Skirt"
- path = /obj/item/clothing/under/suit/black_really/skirt
-
-/datum/loadout_item/uniform/suit/cybersleek //Cyberpunk P.I. Outfit
- name = "Sleek Modern Coat"
- path = /obj/item/clothing/under/costume/cybersleek
-
-/datum/loadout_item/uniform/suit/cybersleek_long //Alt of above
- name = "Long Modern Coat"
- path = /obj/item/clothing/under/costume/cybersleek/long
-
-/datum/loadout_item/uniform/suit/victorian
- name = "Victorian Vest, Black Shirt"
- path = /obj/item/clothing/under/costume/vic_vest
- cost = 1
-
-/datum/loadout_item/uniform/suit/victorian/red
- name = "Victorian Vest, Red Shirt"
- path = /obj/item/clothing/under/costume/vic_vest/red
- cost = 1
-
-/datum/loadout_item/uniform/suit/victorian/blue
- name = "Victorian Vest, Blue Shirt"
- path = /obj/item/clothing/under/costume/vic_vest/blue
- cost = 1
-
-/datum/loadout_item/uniform/suit/victorian/red_alt
- name = "Red Victorian Vest, Black Shirt"
- path = /obj/item/clothing/under/costume/vic_vest/red_alt
- cost = 1
-
-/datum/loadout_item/uniform/suit/peaky
- name = "Birmingham Bling"
- path = /obj/item/clothing/under/misc/peakyblinder
- cost = 1
-
-/datum/loadout_item/uniform/suit/dutch
- name = "Dutch Suit"
- path = /obj/item/clothing/under/costume/dutch
- cost = 3
-
-/datum/loadout_item/uniform/suit/arthur
- name = "Dutch Assistant Suit"
- path = /obj/item/clothing/under/costume/arthur
- cost = 2
-
-/datum/loadout_item/uniform/suit/rancher
- name = "Rancher Suit"
- path = /obj/item/clothing/under/rancher
-
-/datum/loadout_item/uniform/suit/pioneer
- name = "Pioneer Suit"
- path = /obj/item/clothing/under/rancher/pioneer
-
-/datum/loadout_item/uniform/suit/worker
- name = "Western Worker Suit"
- path = /obj/item/clothing/under/rancher/worker
-
-/datum/loadout_item/uniform/suit/cowboywhatever
- name = "Cowboy Suit"
- path = /obj/item/clothing/under/rancher/cowboy
-
-/datum/loadout_item/uniform/suit/checkered
- name = "Checkered Shirt"
- path = /obj/item/clothing/under/rancher/checkered
-
-/datum/loadout_item/uniform/suit/whiterussian
- name = "Russian Baron Suit"
- path = /obj/item/clothing/under/whiterussian
- cost = 4
-
-/datum/loadout_item/uniform/suit/beige
- name = "Beige Suit"
- path = /obj/item/clothing/under/suit/beige
-
-
-//SKIRTS
-/datum/loadout_item/uniform/skirt
- subcategory = LOADOUT_SUBCATEGORY_UNIFORM_SKIRTS
-
-/datum/loadout_item/uniform/skirt/skirtblack
- name = "Black skirt"
- path = /obj/item/clothing/under/dress/skirt
-
-/datum/loadout_item/uniform/skirt/suitskirtblack
- name = "Black Suitskirt"
- path = /obj/item/clothing/under/suit/black/skirt
-
-/datum/loadout_item/uniform/skirt/skirtblue
- name = "Blue skirt"
- path = /obj/item/clothing/under/dress/skirt/blue
-
-/datum/loadout_item/uniform/skirt/skirtred
- name = "Red skirt"
- path = /obj/item/clothing/under/dress/skirt/red
-
-/datum/loadout_item/uniform/skirt/curatorthing
- name = "Sensible Suitskirt"
- path = /obj/item/clothing/under/rank/civilian/curator/skirt
-
-/datum/loadout_item/uniform/skirt/skirtpurple
- name = "Purple skirt"
- path = /obj/item/clothing/under/dress/skirt/purple
-
-/datum/loadout_item/uniform/skirt/skirtplaid
- name = "Plaid skirt"
- path = /obj/item/clothing/under/dress/skirt/plaid
-
-/datum/loadout_item/uniform/skirt/sweptskirt
- name = "Swept skirt"
- path = /obj/item/clothing/under/dress/skirt/swept
-
-/datum/loadout_item/uniform/skirt/schoolgirlblue
- name = "Blue Schoolgirl Uniform"
- path = /obj/item/clothing/under/costume/schoolgirl
-
-/datum/loadout_item/uniform/skirt/schoolgirlred
- name = "Red Schoolgirl Uniform"
- path = /obj/item/clothing/under/costume/schoolgirl/red
-
-/datum/loadout_item/uniform/skirt/schoolgirlgreen
- name = "Green Schoolgirl Uniform"
- path = /obj/item/clothing/under/costume/schoolgirl/green
-
-/datum/loadout_item/uniform/skirt/schoolgirlorange
- name = "Orange Schoolgirl Uniform"
- path = /obj/item/clothing/under/costume/schoolgirl/orange
-
-/datum/loadout_item/uniform/skirt/denimskirt
- name = "Denim Skirt"
- path = /obj/item/clothing/under/pants/denimskirt
-
-/datum/loadout_item/uniform/skirt/poly_skirt
- name = "Polychromic Skirt"
- path = /obj/item/clothing/under/dress/skirt/polychromic
- extra_info = LOADOUT_INFO_THREE_COLORS
- cost = 3
-
-/datum/loadout_item/uniform/skirt/poly_skirt_pleated
- name = "Polychromic Pleated Skirt"
- path = /obj/item/clothing/under/dress/skirt/polychromic/pleated
- extra_info = LOADOUT_INFO_THREE_COLORS
- cost = 3
-
-/datum/loadout_item/uniform/skirt/littleblack
- name = "Short Black Dress"
- path = /obj/item/clothing/under/dress/littleblack
-
-/datum/loadout_item/uniform/skirt/pinktutu
- name = "Pink Tutu"
- path = /obj/item/clothing/under/dress/pinktutu
-
-//DRESSES
-/datum/loadout_item/uniform/dress
- subcategory = LOADOUT_SUBCATEGORY_UNIFORM_DRESSES
-
-/datum/loadout_item/uniform/dress/stripeddress
- name = "Striped Dress"
- path = /obj/item/clothing/under/dress/striped
-
-/datum/loadout_item/uniform/dress/sailordress
- name = "Sailor Dress"
- path = /obj/item/clothing/under/dress/sailor
-
-/datum/loadout_item/uniform/dress/sundresswhite
- name = "White Sundress"
- path = /obj/item/clothing/under/dress/sundress/white
-
-/datum/loadout_item/uniform/dress/sundress
- name = "Sundress"
- path = /obj/item/clothing/under/dress/sundress
-
-/datum/loadout_item/uniform/dress/greendress
- name = "Green Dress"
- path = /obj/item/clothing/under/dress/green
-
-/datum/loadout_item/uniform/dress/pinkdress
- name = "Pink Dress"
- path = /obj/item/clothing/under/dress/pink
-
-/datum/loadout_item/uniform/dress/flowerdress
- name = "Flower Dress"
- path = /obj/item/clothing/under/dress/flower
-
-/datum/loadout_item/uniform/dress/countess
- name = "Countess Dress"
- path = /obj/item/clothing/under/misc/countess
- cost = 3
-
-/datum/loadout_item/uniform/dress/formal
- name = "Formal Red Dress"
- path = /obj/item/clothing/under/misc/formaldressred
- cost = 2
-
-/datum/loadout_item/uniform/dress/victorian
- name = "Victorian Vest, Black"
- path = /obj/item/clothing/under/costume/vic_dress
-
-/datum/loadout_item/uniform/dress/victorian/red
- name = "Victorian Vest, Red"
- path = /obj/item/clothing/under/costume/vic_dress/red
-
-//PANTS
-/datum/loadout_item/uniform/pants
- subcategory = LOADOUT_SUBCATEGORY_UNIFORM_PANTS
-
-/datum/loadout_item/uniform/pants/yoga
- name = "Yoga Pants"
- path = /obj/item/clothing/under/pants/yoga
-
-/datum/loadout_item/uniform/pants/bjeans
- name = "Black Jeans"
- path = /obj/item/clothing/under/pants/blackjeans
-
-/datum/loadout_item/uniform/pants/bpants
- name = "Black Pants"
- path = /obj/item/clothing/under/pants/black
-
-/datum/loadout_item/uniform/pants/cjeans
- name = "Classic Jeans"
- path = /obj/item/clothing/under/pants/classicjeans
-
-/datum/loadout_item/uniform/pants/khaki
- name = "Khaki Pants"
- path = /obj/item/clothing/under/pants/khaki
-
-/datum/loadout_item/uniform/pants/wpants
- name = "White Pants"
- path = /obj/item/clothing/under/pants/white
-
-/datum/loadout_item/uniform/pants/rpants
- name = "Red Pants"
- path = /obj/item/clothing/under/pants/red
-
-/datum/loadout_item/uniform/pants/tpants
- name = "Tan Pants"
- path = /obj/item/clothing/under/pants/tan
-
-/datum/loadout_item/uniform/pants/trpants
- name = "Track Pants"
- path = /obj/item/clothing/under/pants/track
-
-/datum/loadout_item/uniform/pants/rippedjeans
- name = "Ripped Jeans"
- path = /obj/item/clothing/under/pants/jeanripped
-
-/datum/loadout_item/uniform/pants/yakuza
- name = "Japanese Jeans"
- path = /obj/item/clothing/under/costume/yakuza
-
-//SHORTS
-/datum/loadout_item/uniform/shorts
- subcategory = LOADOUT_SUBCATEGORY_UNIFORM_SHORTS
-
-/*/datum/loadout_item/uniform/shorts/polyshorts
- name = "Polychromic Shorts"
- path = /obj/item/clothing/under/misc/polyshorts
- cost = 3*/
-
-/*/datum/loadout_item/uniform/shorts/polyshortpants
- name = "Polychromic Athletic Shorts"
- path = /obj/item/clothing/under/shorts/polychromic*/
- cost = 2
-
-/datum/loadout_item/uniform/shorts/jeanshort
- name = "Jean Shorts"
- path = /obj/item/clothing/under/pants/jeanshort
-
-/datum/loadout_item/uniform/shorts/camoshorts
- name = "Camo Pants"
- path = /obj/item/clothing/under/pants/camo
-
-/datum/loadout_item/uniform/shorts/athleticshorts
- name = "Athletic Shorts"
- path = /obj/item/clothing/under/shorts/red
-
-
-//SWEATERS
-/datum/loadout_item/uniform/sweater
- subcategory = LOADOUT_SUBCATEGORY_UNIFORM_SWEATERS
-
-/datum/loadout_item/uniform/sweater/turtleneck
- name = "Tactitool Turtleneck"
- path = /obj/item/clothing/under/syndicate/tacticool/sensors
-
-/datum/loadout_item/uniform/sweater/tactical1
- name = "Irish Tactical Sweater"
- path = /obj/item/clothing/under/misc/tactical1
-
-/datum/loadout_item/uniform/sweater/tactical2
- name = "British Tactical Sweater"
- path = /obj/item/clothing/under/uvf
-
-/datum/loadout_item/uniform/sweater/turtleneck/skirt
- name = "Tactitool Skirtleneck"
- path = /obj/item/clothing/under/syndicate/tacticool/skirt/sensors
-
-/datum/loadout_item/uniform/sweater/creamsweater
- name = "Cream Commando Sweater"
- path = /obj/item/clothing/under/sweater
-
-/datum/loadout_item/uniform/sweater/blacksweater
- name = "Black Commando Sweater"
- path = /obj/item/clothing/under/sweater/black
-
-/datum/loadout_item/uniform/sweater/purpsweater
- name = "Purple Commando Sweater"
- path = /obj/item/clothing/under/sweater/purple
-
-/datum/loadout_item/uniform/sweater/greensweater
- name = "Green Commando Sweater"
- path = /obj/item/clothing/under/sweater/green
-
-/datum/loadout_item/uniform/sweater/redsweater
- name = "Red Commando Sweater"
- path = /obj/item/clothing/under/sweater/red
-
-/datum/loadout_item/uniform/sweater/bluesweater
- name = "Navy Commando Sweater"
- path = /obj/item/clothing/under/sweater/blue
-
-/datum/loadout_item/uniform/sweater/polysweater
- name = "Polychromic Commando Sweater"
- path = /obj/item/clothing/under/misc/polysweater
- extra_info = LOADOUT_INFO_ONE_COLOR
- cost = 3
-
-/datum/loadout_item/uniform/sweater/keyholesweater
- name = "Keyhole Sweater"
- path = /obj/item/clothing/under/sweater/keyhole
-
-//JOB
-/datum/loadout_item/uniform/job
- subcategory = LOADOUT_SUBCATEGORY_JOB
-
-// SKYRAT EDIT REMOVAL BEGIN - COMMAND CLOTHING VENDOR
-/*
-/datum/loadout_item/uniform/job/humblecaptain
- name = "Humble Captain Jumpsuit"
- path = /obj/item/clothing/under/rank/captain/humble
- restricted_roles = list("Captain")
-
-/datum/loadout_item/uniform/job/captaindress
- name = "Captain's Dress"
- path = /obj/item/clothing/under/rank/captain/dress
- restricted_roles = list("Captain")
-*/ // REMOVAL END
-
-/datum/loadout_item/uniform/job/impcap
- name = "Captain's Naval Jumpsuit"
- path = /obj/item/clothing/under/rank/captain/imperial
- restricted_roles = list("Captain", "Nanotrasen Representative")
-
-/datum/loadout_item/uniform/job/imphop
- name = "Head of Personnel's Naval Jumpsuit"
- path = /obj/item/clothing/under/rank/civilian/head_of_personnel/imperial
- restricted_roles = list("Head of Personnel", "Nanotrasen Representative")
-
-/datum/loadout_item/uniform/job/lowprison
- name = "Low Security Prisoner Jumpsuit"
- path = /obj/item/clothing/under/rank/prisoner/lowsec
- restricted_roles = list("Prisoner")
-
-/datum/loadout_item/uniform/job/lowprisons
- name = "Low Security Prisoner Jumpskirt"
- path = /obj/item/clothing/under/rank/prisoner/lowsec/skirt
- restricted_roles = list("Prisoner")
-
-/datum/loadout_item/uniform/job/procusprison
- name = "Protective Custody Prisoner Jumpsuit"
- path = /obj/item/clothing/under/rank/prisoner/protcust
- restricted_roles = list("Prisoner")
-
-/datum/loadout_item/uniform/job/procusprisons
- name = "Protective Custody Prisoner Jumpskirt"
- path = /obj/item/clothing/under/rank/prisoner/protcust/skirt
- restricted_roles = list("Prisoner")
-
-/datum/loadout_item/uniform/job/supmaxprison
- name = "Supermax Prisoner Jumpsuit"
- path = /obj/item/clothing/under/rank/prisoner/supermax
- restricted_roles = list("Prisoner")
-
-/datum/loadout_item/uniform/job/supmaxprisons
- name = "Supermax Prisoner Jumpskirt"
- path = /obj/item/clothing/under/rank/prisoner/supermax/skirt
- restricted_roles = list("Prisoner")
-
-/datum/loadout_item/uniform/job/highprison
- name = "High Risk Prisoner Jumpsuit"
- path = /obj/item/clothing/under/rank/prisoner/highsec
- restricted_roles = list("Prisoner")
-
-/datum/loadout_item/uniform/job/supmaxprisons
- name = "High Risk Prisoner Jumpskirt"
- path = /obj/item/clothing/under/rank/prisoner/highsec/skirt
- restricted_roles = list("Prisoner")
-
-/datum/loadout_item/uniform/job/blacknwhite
- name = "Classic Prisoner Jumpsuit"
- path = /obj/item/clothing/under/rank/prisoner/classic
- restricted_roles = list("Prisoner")
-
-/datum/loadout_item/uniform/job/priestrobe
- name = "Priestess Robe"
- path = /obj/item/clothing/under/rank/pmarsrobe
- cost = 2
- restricted_roles = list("Chaplain")
-
-
-/datum/loadout_item/uniform/job/imphos
- name = "Head of Security's Naval Uniform"
- path = /obj/item/clothing/under/rank/security/head_of_security/imperial
- restricted_roles = list("Head of Security")
-
-
-/datum/loadout_item/uniform/job/navyblueuniformofficer
- name = "Security officer navyblue uniform"
- path = /obj/item/clothing/under/rank/security/officer/formal
- restricted_roles = list("Security Officer","Security Medic","Security Sergeant")
-
-/datum/loadout_item/uniform/job/navyblueuniformwarden
- name = "Warden navyblue uniform"
- path = /obj/item/clothing/under/rank/security/warden/formal
- restricted_roles = list("Warden")
-
-/datum/loadout_item/uniform/job/solwarden
- name = "Sol Warden Uniform"
- path = /obj/item/clothing/under/rank/security/warden/peacekeeper/sol
- restricted_roles = list("Warden")
-
-/datum/loadout_item/uniform/job/coroffw
- name = "Corrections Officer Sweater (skirt)"
- path = /obj/item/clothing/under/rank/security/brigguard/sweater/women
- restricted_roles = list("Corrections Officer","Warden")
-
-/datum/loadout_item/uniform/job/coroffshir
- name = "Corrections Officer Shirt"
- path = /obj/item/clothing/under/rank/security/brigguard
- restricted_roles = list("Corrections Officer","Warden")
-
-/datum/loadout_item/uniform/job/coroffshirw
- name = "Corrections Officer Skirt"
- path = /obj/item/clothing/under/rank/security/brigguard/women
- restricted_roles = list("Corrections Officer","Warden")
-
-/datum/loadout_item/uniform/job/peacetrouse
- name = "Peacekeeper Trousers"
- path = /obj/item/clothing/under/rank/security/peacekeeper/trousers
- restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant")
-
-/datum/loadout_item/uniform/job/peacetrouse
- name = "Security Trousers"
- path = /obj/item/clothing/under/rank/security/peacekeeper/trousers/red
- restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant")
-
-/datum/loadout_item/uniform/job/secskirt
- name = "Security skirt"
- path = /obj/item/clothing/under/rank/security/officer/skirt
- restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant") //i want a femboy sergeant to shove a baton up my ass //disgusting
-
-/datum/loadout_item/uniform/job/seccadett
- name = "Sol Cadet Uniform"
- path = /obj/item/clothing/under/rank/security/peacekeeper/junior/sol
- restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant", "Civil Disputes Officer")
-
-/datum/loadout_item/uniform/job/sectrafficop
- name = "Sol Traffic Cop Uniform"
- path = /obj/item/clothing/under/rank/security/peacekeeper/junior/sol/traffic
- restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant", "Civil Disputes Officer")
-
-/datum/loadout_item/uniform/job/solofficerthing
- name = "Sol Officer Uniform"
- path = /obj/item/clothing/under/rank/security/peacekeeper/sol
- restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant")
-
-// SKYRAT EDIT REMOVAL BEGIN - MOVED TO COMMAND CLOTHING VENDOR
-/*
-/datum/loadout_item/uniform/job/hosskirt
- name = "Head of security's skirt"
- path = /obj/item/clothing/under/rank/security/head_of_security/skirt
- restricted_roles = list("Head of Security")
-*/ // END
-
-/datum/loadout_item/uniform/job/disco
- name = "Superstar Cop Suit"
- path = /obj/item/clothing/under/misc/discounder
- restricted_roles = list("Detective")
- restricted_desc = "Superstar Detectives"
-
-/datum/loadout_item/uniform/job/seckilt
- name = "Security Kilt"
- path = /obj/item/clothing/under/rank/security/blackwatch
- restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant")
-
-/datum/loadout_item/uniform/job/medrscrubs
- name = "Red Scrubs (security)"
- path = /obj/item/clothing/under/rank/medical/doctor/red
- restricted_roles = list("Security Medic")
- restricted_desc = "Security Medic"
-
-/datum/loadout_item/uniform/job/redscrubs
- name = "Red Scrubs"
- path = /obj/item/clothing/under/rank/medical/doctor/red/unarm
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
- restricted_desc = "Medical"
-
-/datum/loadout_item/uniform/job/bluescrubs
- name = "Blue Scrubs"
- path = /obj/item/clothing/under/rank/medical/doctor/blue
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
- restricted_desc = "Medical"
-
-/datum/loadout_item/uniform/job/greenscrubs
- name = "Green Scrubs"
- path = /obj/item/clothing/under/rank/medical/doctor/green
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
- restricted_desc = "Medical"
-
-/datum/loadout_item/uniform/job/purplescrubs
- name = "Purple Scrubs"
- path = /obj/item/clothing/under/rank/medical/doctor/purple
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
- restricted_desc = "Medical"
-
-/datum/loadout_item/uniform/job/whitescrubs
- name = "White Scrubs"
- path = /obj/item/clothing/under/rank/medical/doctor/white
- extra_info = LOADOUT_INFO_ONE_COLOR
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
- restricted_desc = "Medical"
-
-/datum/loadout_item/uniform/job/nursesuit
- name = "Nurse Suit"
- path = /obj/item/clothing/under/rank/medical/doctor/nurse
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
- restricted_desc = "Medical"
-
-/datum/loadout_item/uniform/job/formalmed
- name = "Formal Medical Suit"
- path = /obj/item/clothing/under/rank/medical/doctor/formal
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic", "Psychologist")
- restricted_desc = "Medical"
-
-/datum/loadout_item/uniform/job/formalmedskirt
- name = "Formal Medical Skirt"
- path = /obj/item/clothing/under/rank/medical/doctor/formal/skirt
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic", "Psychologist")
- restricted_desc = "Medical"
-
-/datum/loadout_item/uniform/job/formalvir
- name = "Formal Virologist Suit"
- path = /obj/item/clothing/under/rank/medical/virologist/formal
- restricted_roles = list("Chief Medical Officer", "Virologist")
- restricted_desc = "Virology"
-
-/datum/loadout_item/uniform/job/formalvirskirt
- name = "Formal Virologist Skirt"
- path = /obj/item/clothing/under/rank/medical/virologist/formal/skirt
- restricted_roles = list("Chief Medical Officer", "Virologist")
- restricted_desc = "Virology"
-
-/datum/loadout_item/uniform/job/formalchem
- name = "Formal Chemist Suit"
- path = /obj/item/clothing/under/rank/medical/chemist/formal
- restricted_roles = list("Chief Medical Officer", "Chemist")
- restricted_desc = "Chemistry"
-
-/datum/loadout_item/uniform/job/formalchemskirt
- name = "Formal Chemist Skirt"
- path = /obj/item/clothing/under/rank/medical/chemist/formal/skirt
- restricted_roles = list("Chief Medical Officer", "Chemist")
- restricted_desc = "Chemistry"
-
-/datum/loadout_item/uniform/job/impcmo
- name = "Chief Medical Officer's Naval Uniform"
- path = /obj/item/clothing/under/rank/medical/chief_medical_officer/imperial
- restricted_roles = list("Chief Medical Officer")
-
-/datum/loadout_item/uniform/job/gorka_cargo
- name = "Supply Gorka"
- path = /obj/item/clothing/under/utility/cargo/gorka
- restricted_roles = list("Cargo Technician", "Miner", "Quartermaster")
- restricted_desc = "All Cargo Personnel"
-
-/datum/loadout_item/uniform/job/turtleneck_cargo
- name = "Supply Turtleneck"
- path = /obj/item/clothing/under/utility/cargo/turtleneck
- restricted_roles = list("Cargo Technician", "Miner", "Quartermaster")
- restricted_desc = "All Cargo Personnel"
-
-/datum/loadout_item/uniform/job/casualcargothing
- name = "Casual Cargo Gear"
- path = /obj/item/clothing/under/rank/cargo/casualman
- restricted_roles = list("Cargo Technician", "Miner", "Quartermaster")
- restricted_desc = "All Cargo Personnel"
-
-// SKYRAT EDIT REMOVAL BEGIN - MOVED TO COMMAND CLOTHING VENDOR
-/*
-/datum/loadout_item/uniform/job/gorka_qm
- name = "Quartermaster's Gorka"
- path = /obj/item/clothing/under/utility/cargo/gorka/head
- restricted_roles = list("Quartermaster")
-
-/datum/loadout_item/uniform/job/turtleneck_qm
- name = "Quartermaster's Turtleneck"
- path = /obj/item/clothing/under/utility/cargo/turtleneck/head
- restricted_roles = list("Quartermaster")
-
-/datum/loadout_item/uniform/job/qmformal
- name = "Quartermaster's Formal Suit"
- path = /obj/item/clothing/under/rank/cargo/qm/formal
- restricted_roles = list("Quartermaster")
-
-/datum/loadout_item/uniform/job/qmformalskirt
- name = "Quartermaster's Formal Skirt"
- path = /obj/item/clothing/under/rank/cargo/qm/formal/skirt
- restricted_roles = list("Quartermaster")
-
-/datum/loadout_item/uniform/job/qmcasual
- name = "Quartermaster's Casual Suit"
- path = /obj/item/clothing/under/rank/cargo/qm/casual
- restricted_roles = list("Quartermaster")
-*/ // END
-
-/datum/loadout_item/uniform/job/engtrous
- name = "Engineering Trousers"
- path = /obj/item/clothing/under/rank/engineering/engineer/trouser
- restricted_roles = list("Station Engineer","Atmospheric Technician", "Chief Engineer")
- restricted_desc = "Engineering"
-
-/datum/loadout_item/uniform/job/engformal
- name = "Engineering Formal Suit"
- path = /obj/item/clothing/under/rank/engineering/engineer/formal
- restricted_roles = list("Station Engineer","Atmospheric Technician", "Chief Engineer")
- restricted_desc = "Engineering"
-
-/datum/loadout_item/uniform/job/engformalskirt
- name = "Engineering Formal Skirt"
- path = /obj/item/clothing/under/rank/engineering/engineer/formal/skirt
- restricted_roles = list("Station Engineer","Atmospheric Technician", "Chief Engineer")
- restricted_desc = "Engineering"
-
-/datum/loadout_item/uniform/job/impce
- name = "Chief Engineer's Naval Uniform"
- path = /obj/item/clothing/under/rank/engineering/chief_engineer/imperial
- restricted_roles = list("Chief Engineer")
-
-/datum/loadout_item/uniform/job/imprd
- name = "Research Director's Naval Uniform"
- path = /obj/item/clothing/under/rank/rnd/research_director/imperial
- restricted_roles = list("Research Director")
-
-
-
-//JOB - UTILITY
-/datum/loadout_item/uniform/job/utility
- name = "Utility Uniform"
- path = /obj/item/clothing/under/utility
- restricted_desc = "Unrestricted"
-
-/datum/loadout_item/uniform/job/utility_eng
- name = "Engineering Utility Uniform"
- path = /obj/item/clothing/under/utility/eng
- restricted_roles = list("Station Engineer","Atmospheric Technician", "Chief Engineer")
- restricted_desc = "Engineering"
-
-/datum/loadout_item/uniform/job/utility_med
- name = "Medical Utility Uniform"
- path = /obj/item/clothing/under/utility/med
- restricted_roles = list("Medical Doctor", "Paramedic", "Chemist", "Virologist", "Geneticist", "Security Medic", "Chief Medical Officer", "Psychologist")
- restricted_desc = "Medical"
-
-/datum/loadout_item/uniform/job/utility_sci
- name = "Science Utility Uniform"
- path = /obj/item/clothing/under/utility/sci
- restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Vanguard Operative")
- restricted_desc = "Science"
-
-/datum/loadout_item/uniform/job/hlscientist
- name = "Ridiculous Scientist Outfit"
- path = /obj/item/clothing/under/misc/hlscience
- restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Vanguard Operative")
- restricted_desc = "Science"
-
-/datum/loadout_item/uniform/job/utility_cargo
- name = "Supply Utility Uniform"
- path = /obj/item/clothing/under/utility/cargo
- restricted_roles = list("Cargo Technician", "Miner", "Quartermaster")
- restricted_desc = "Cargo"
-
-/datum/loadout_item/uniform/job/utility_sec
- name = "Security Utility Uniform"
- path = /obj/item/clothing/under/utility/sec
- restricted_roles = list("Security Officer", "Detective", "Warden", "Blueshield", "Security Medic","Security Sergeant", "Head of Security", "Civil Disputes Officer", "Corrections Officer") //i dunno about the blueshield, they're a weird combo of sec and command, thats why they arent in the loadout pr im making
- restricted_desc = "Security"
-
-/datum/loadout_item/uniform/job/utility_com
- name = "Command Utility Uniform"
- path = /obj/item/clothing/under/utility/com
- restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer")
- restricted_desc = "Command Staff"
-
-/datum/loadout_item/uniform/job/impcommand
- name = "Light Grey Officer's Naval Jumpsuit"
- path = /obj/item/clothing/under/imperial
- restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
- restricted_desc = "Command Staff"
-
-/datum/loadout_item/uniform/job/impcom
- name = "Grey Officer's Naval Jumpsuit"
- path = /obj/item/clothing/under/imperial/grey
- restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
- restricted_desc = "Command Staff"
-
-/datum/loadout_item/uniform/job/impred
- name = "Red Officer's Naval Jumpsuit"
- path = /obj/item/clothing/under/imperial/red
- restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer") //NT Reps would never wear red, it's unbefitting
- restricted_desc = "Command Staff"
-
-/datum/loadout_item/uniform/job/impcomtrous
- name = "Grey Officer's Naval Jumpsuit (Trousers)"
- path = /obj/item/clothing/under/imperial/grey/trouser
- restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
- restricted_desc = "Command Staff"
-
-/datum/loadout_item/uniform/job/robosleek
- name = "Sleek Roboticist Jumpsuit"
- path = /obj/item/clothing/under/utility/robo_sleek
- restricted_roles = list("Roboticist", "Research Director")
- restricted_desc = "Robotics"
-
-/datum/loadout_item/uniform/job/para_red
- name = "Red Paramedic Jumpsuit"
- path = /obj/item/clothing/under/utility/para_red
- restricted_roles = list("Chief Medical Officer", "Paramedic", "Security Medic") //BRO RED USED TO BE THE SEC COLOUR
- restricted_desc = "Medical First Responders"
-
-// Trekie things
-//TOS
-/datum/loadout_item/uniform/job/trek/trekcmdtos
- name = "TOS - cmd"
- path = /obj/item/clothing/under/trek/command
- restricted_desc = "Heads of Staff"
- restricted_roles = list("Head of Security","Captain","Head of Personnel","Chief Engineer","Research Director","Chief Medical Officer","Quartermaster")
-
-/datum/loadout_item/uniform/job/trek/trekmedscitos
- name = "TOS - med/sci"
- path = /obj/item/clothing/under/trek/medsci
- restricted_desc = "Medical and Science"
- restricted_roles = list("Chief Medical Officer","Medical Doctor","Chemist","Virologist","Security Medic","Paramedic","Psychologist","Geneticist","Research Director","Scientist","Roboticist","Vanguard Operative")
-
-/datum/loadout_item/uniform/job/trek/trekengtos
- name = "TOS - ops/sec"
- path = /obj/item/clothing/under/trek/engsec
- restricted_desc = "Engineering, Security, and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Security Medic","Security Sergeant","Warden","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-//handled by modular skyrat code as reskins
-//TNG
-/datum/loadout_item/uniform/job/trek/trekcmdtng
- name = "TNG - cmd"
- path = /obj/item/clothing/under/trek/command/next
- restricted_desc = "Heads of Staff"
- restricted_roles = list("Head of Security","Captain","Head of Personnel","Chief Engineer","Research Director","Chief Medical Officer","Quartermaster")
-
-/datum/loadout_item/uniform/job/trekmedscitng
- name = "TNG - med/sci"
- path = /obj/item/clothing/under/trek/medsci/next
- restricted_desc = "Medical and Science"
- restricted_roles = list("Chief Medical Officer","Medical Doctor","Paramedic","Chemist","Virologist","Security Medic","Psychologist","Geneticist","Research Director","Scientist","Roboticist","Vanguard Operative")
-
-/datum/loadout_item/uniform/job/trekengtng
- name = "TNG - ops/sec"
- path = /obj/item/clothing/under/trek/engsec/next
- restricted_desc = "Engineering, Security, and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Security Medic","Security Sergeant","Warden","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-//VOY
-/datum/loadout_item/uniform/job/trekcmdvoy
- name = "VOY - cmd"
- path = /obj/item/clothing/under/trek/modular_skyrat/command/voy
- restricted_desc = "Heads of Staff"
- restricted_roles = list("Head of Security","Captain","Head of Personnel","Chief Engineer","Research Director","Chief Medical Officer","Quartermaster")
-
-/datum/loadout_item/uniform/job/trekmedscivoy
- name = "VOY - med/sci"
- path = /obj/item/clothing/under/trek/modular_skyrat/medsci/voy
- restricted_desc = "Medical and Science"
- restricted_roles = list("Chief Medical Officer","Security Medic","Medical Doctor","Paramedic","Chemist","Virologist","Psychologist","Geneticist","Research Director","Scientist","Roboticist","Vanguard Operative")
-
-/datum/loadout_item/uniform/job/trekengvoy
- name = "VOY - ops/sec"
- path = /obj/item/clothing/under/trek/modular_skyrat/engsec/voy
- restricted_desc = "Engineering, Security, and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Warden","Security Medic","Security Sergeant","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-//DS9
-/datum/loadout_item/uniform/job/trekcmdds9
- name = "DS9 - cmd"
- path = /obj/item/clothing/under/trek/modular_skyrat/command/ds9
- restricted_desc = "Heads of Staff"
- restricted_roles = list("Head of Security","Captain","Head of Personnel","Chief Engineer","Research Director","Chief Medical Officer","Quartermaster")
-
-/datum/loadout_item/uniform/job/trekmedscids9
- name = "DS9 - med/sci"
- path = /obj/item/clothing/under/trek/modular_skyrat/medsci/ds9
- restricted_desc = "Medical and Science"
- restricted_roles = list("Chief Medical Officer","Medical Doctor","Paramedic","Security Medic","Chemist","Virologist","Psychologist","Geneticist","Research Director","Scientist", "Roboticist")
-
-/datum/loadout_item/uniform/job/trekengds9
- name = "DS9 - ops/sec"
- path = /obj/item/clothing/under/trek/modular_skyrat/engsec/ds9
- restricted_desc = "Engineering, Security, and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Security Medic","Security Sergeant","Warden","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-//ENT
-/datum/loadout_item/uniform/job/trekcmdent
- name = "ENT - cmd"
- path = /obj/item/clothing/under/trek/command/ent
- restricted_desc = "Heads of Staff"
- restricted_roles = list("Head of Security","Captain","Head of Personnel","Chief Engineer","Research Director","Chief Medical Officer","Quartermaster")
-
-/datum/loadout_item/uniform/job/trekmedscient
- name = "ENT - med/sci"
- path = /obj/item/clothing/under/trek/medsci/ent
- restricted_desc = "Medical and Science"
- restricted_roles = list("Chief Medical Officer","Medical Doctor","Security Medic","Paramedic","Chemist","Virologist","Psychologist","Geneticist","Research Director","Scientist", "Roboticist","Vanguard Operative")
-
-/datum/loadout_item/uniform/job/trekengent
- name = "ENT - ops/sec"
- path = /obj/item/clothing/under/trek/engsec/ent
- restricted_desc = "Engineering, Security, and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Security Medic","Security Sergeant","Warden","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-//Orville+
-/datum/loadout_item/uniform/job/trekcptorv
- name = "ORV - captain"
- path = /obj/item/clothing/under/trek/modular_skyrat/command/orv/captain
- restricted_roles = list("Captain")
-
-/datum/loadout_item/uniform/job/trekcmdorv_medsci
- name = "ORV - cmd - med/sci"
- path = /obj/item/clothing/under/trek/modular_skyrat/command/orv/medsci
- restricted_roles = list("Research Director","Chief Medical Officer")
-
-/datum/loadout_item/uniform/job/trekcmdorv_engsec
- name = "ORV - cmd - ops/sec"
- path = /obj/item/clothing/under/trek/modular_skyrat/command/orv/engsec
- restricted_roles = list("Head of Security","Chief Engineer","Quartermaster")
-
-/datum/loadout_item/uniform/job/trekcmdorv
- name = "ORV - cmd"
- path = /obj/item/clothing/under/trek/modular_skyrat/command/orv
- restricted_desc = "Heads of Staff"
- restricted_roles = list("Head of Security","Captain","Head of Personnel","Chief Engineer","Research Director","Chief Medical Officer","Quartermaster")
-
-/datum/loadout_item/uniform/job/trekmedsciorv
- name = "ORV - med/sci"
- path = /obj/item/clothing/under/trek/modular_skyrat/medsci/orv
- restricted_desc = "Medical and Science"
- restricted_roles = list("Chief Medical Officer","Medical Doctor","Paramedic","Chemist","Virologist","Psychologist","Security Medic","Geneticist","Research Director","Scientist", "Roboticist","Vanguard Operative")
-
-/datum/loadout_item/uniform/job/trekengorv
- name = "ORV - ops/sec"
- path = /obj/item/clothing/under/trek/modular_skyrat/engsec/orv
- restricted_desc = "Engineering, Security, and Cargo"
- restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Security Medic","Security Sergeant","Warden","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
-
-/datum/loadout_item/uniform/job/trekservorv
- name = "ORV - service"
- path = /obj/item/clothing/under/trek/modular_skyrat/orv/service
- restricted_desc = "Service"
- restricted_roles = list("Assistant", "Bartender", "Botanist", "Cook", "Curator", "Janitor", "Clown", "Mime", "Lawyer", "Psychologist")
-
-/datum/loadout_item/uniform/job/trekadjtorv
- name = "ORV - assistant"
- path = /obj/item/clothing/under/trek/modular_skyrat/orv
- restricted_roles = list("Assistant")
-
-/datum/loadout_item/uniform/tactical_hawaiian_orange
- name = "Tactical Hawaiian Outfit - Orange"
- path = /obj/item/clothing/under/tachawaiian
-
-/datum/loadout_item/uniform/tactical_hawaiian_blue
- name = "Tactical Hawaiian Outfit - Blue"
- path = /obj/item/clothing/under/tachawaiian/blue
-
-/datum/loadout_item/uniform/tactical_hawaiian_purple
- name = "Tactical Hawaiian Outfit - Purple"
- path = /obj/item/clothing/under/tachawaiian/purple
-
-/datum/loadout_item/uniform/tactical_hawaiian_green
- name = "Tactical Hawaiian Outfit - Green"
- path = /obj/item/clothing/under/tachawaiian/green
-
-/datum/loadout_item/uniform/texas
- name = "Texan Suit"
- path = /obj/item/clothing/under/texas
-
-/datum/loadout_item/uniform/dimmadome
- name = "Doug Dimmadome Suit"
- path = /obj/item/clothing/under/doug_dimmadome
-
-/datum/loadout_item/uniform/westender
- name = "Westender Suit"
- path = /obj/item/clothing/under/westender
-
-//TheMotionPicture
-/*/datum/loadout_item/trekfedutil
- name = "TMP uniform"
- category = SLOT_W_UNIFORM
- path = /obj/item/clothing/under/trek/fedutil
- restricted_desc = "All, barring Service and Civilian"
- restricted_roles = list("Head of Security","Captain","Head of Personnel","Chief Engineer","Research Director","Chief Medical Officer","Quartermaster",
- "Medical Doctor","Chemist","Virologist","Geneticist","Scientist", "Roboticist",
- "Atmospheric Technician","Station Engineer","Warden","Detective","Security Officer",
- "Cargo Technician", "Shaft Miner")
-
-/datum/loadout_item/trekfedtrainee
- name = "TMP - trainee"
- category = SLOT_W_UNIFORM
- path = /obj/item/clothing/under/trek/fedutil/trainee
- restricted_roles = list("Assistant", "Janitor", "Cargo Technician")
-
-/datum/loadout_item/trekfedservice
- name = "TMP - service"
- category = SLOT_W_UNIFORM
- path = /obj/item/clothing/under/trek/fedutil/service
- restricted_desc = "Service and Civilian, barring Clown, Mime and Lawyer"
- restricted_roles = list("Assistant", "Bartender", "Botanist", "Cook", "Curator", "Janitor")*/
-
-
-//Christmas
-/*Commenting out Until next Christmas or made automatic
-/datum/loadout_item/christmasmaler
- name = "Red Masculine Christmas Suit"
- category = SLOT_W_UNIFORM
- path = /obj/item/clothing/under/costume/christmas
-/datum/loadout_item/christmasmaleg
- name = "Green Masculine Christmas Suit"
- category = SLOT_W_UNIFORM
- path = /obj/item/clothing/under/costume/christmas/green
-/datum/loadout_item/christmasfemaler
- name = "Red Feminine Christmas Suit"
- category = SLOT_W_UNIFORM
- path = /obj/item/clothing/under/costume/christmas/croptop
-/datum/loadout_item/christmasfemaleg
- name = "Green Feminine Christmas Suit"
- category = SLOT_W_UNIFORM
- path = /obj/item/clothing/under/costume/christmas/croptop/green*/
diff --git a/modular_skyrat/modules/customization/modules/client/preferences.dm b/modular_skyrat/modules/customization/modules/client/preferences.dm
index 9c07208bf8a..84220221233 100644
--- a/modular_skyrat/modules/customization/modules/client/preferences.dm
+++ b/modular_skyrat/modules/customization/modules/client/preferences.dm
@@ -1,3 +1,4 @@
+/*
GLOBAL_LIST_EMPTY(preferences_datums)
// i shall taint your pretty preferences file with bobcode
@@ -3566,3 +3567,4 @@ GLOBAL_LIST_INIT(food, list(
popup.set_window_options("can_close=0")
popup.set_content(dat.Join())
popup.open(FALSE)
+*/
diff --git a/modular_skyrat/modules/customization/modules/client/preferences_savefile.dm b/modular_skyrat/modules/customization/modules/client/preferences_savefile.dm
index 27bd969fb38..ecde6f878e4 100644
--- a/modular_skyrat/modules/customization/modules/client/preferences_savefile.dm
+++ b/modular_skyrat/modules/customization/modules/client/preferences_savefile.dm
@@ -1,3 +1,4 @@
+/*
//This is the lowest supported version, anything below this is completely obsolete and the entire savefile will be wiped.
#define SAVEFILE_VERSION_MIN 32
@@ -827,3 +828,4 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
S.ImportText("/",file("[path].txt"))
#endif
+*/
diff --git a/modular_skyrat/modules/customization/modules/jobs/_job.dm b/modular_skyrat/modules/customization/modules/jobs/_job.dm
index bba9b979b4b..57a97df10d3 100644
--- a/modular_skyrat/modules/customization/modules/jobs/_job.dm
+++ b/modular_skyrat/modules/customization/modules/jobs/_job.dm
@@ -13,8 +13,10 @@
var/list/species_blacklist
/// Which languages does the job require, associative to LANGUAGE_UNDERSTOOD or LANGUAGE_SPOKEN
var/list/required_languages = list(/datum/language/common = LANGUAGE_SPOKEN)
- //Alt titles
- var/list/alt_titles = list()
+
+ ///Is this job veteran only? If so, then this job requires the player to be in the veteran_players.txt
+ var/veteran_only = FALSE
+
/datum/job/proc/has_banned_quirk(datum/preferences/pref)
if(!pref) //No preferences? We'll let you pass, this time (just a precautionary check,you dont wanna mess up gamemode setting logic)
@@ -26,7 +28,9 @@
return FALSE
/datum/job/proc/has_banned_species(datum/preferences/pref)
- var/my_id = pref.pref_species.id
+ var/species_type = pref.read_preference(/datum/preference/choiced/species)
+ var/datum/species/species = new species_type
+ var/my_id = species.id
if(species_whitelist && !species_whitelist[my_id])
return TRUE
else if(!GLOB.roundstart_races[my_id])
diff --git a/modular_skyrat/modules/customization/modules/mob/dead/new_player/preferences_setup.dm b/modular_skyrat/modules/customization/modules/mob/dead/new_player/preferences_setup.dm
index b1880f8f5e2..d26dc0e5669 100644
--- a/modular_skyrat/modules/customization/modules/mob/dead/new_player/preferences_setup.dm
+++ b/modular_skyrat/modules/customization/modules/mob/dead/new_player/preferences_setup.dm
@@ -1,94 +1,15 @@
+/*
/// Fully randomizes everything in the character.
/datum/preferences/proc/randomise_appearance_prefs(randomise_flags = ALL)
- if(randomise_flags & RANDOMIZE_GENDER)
- gender = pick(MALE, FEMALE, PLURAL)
- switch(gender)
- if(MALE, FEMALE)
- body_type = gender
- else
- body_type = pick(MALE, FEMALE)
if(randomise_flags & RANDOMIZE_SPECIES)
var/rando_race = GLOB.species_list[pick(GLOB.roundstart_races)]
pref_species = new rando_race()
if(randomise_flags & RANDOMIZE_NAME)
real_name = pref_species.random_name(gender, TRUE)
- if(randomise_flags & RANDOMIZE_AGE)
- age = rand(AGE_MIN, AGE_MAX)
- if(randomise_flags & RANDOMIZE_UNDERWEAR)
- underwear = random_underwear(gender)
- if(randomise_flags & RANDOMIZE_UNDERWEAR_COLOR)
- underwear_color = random_short_color()
- if(randomise_flags & RANDOMIZE_UNDERSHIRT)
- undershirt = random_undershirt(gender)
- if(randomise_flags & RANDOMIZE_SOCKS)
- socks = random_socks()
- if(randomise_flags & RANDOMIZE_BACKPACK)
- backpack = random_backpack()
- if(randomise_flags & RANDOMIZE_JUMPSUIT_STYLE)
- jumpsuit_style = pick(GLOB.jumpsuitlist)
- if(randomise_flags & RANDOMIZE_HAIRSTYLE)
- hairstyle = random_hairstyle(gender)
- if(randomise_flags & RANDOMIZE_FACIAL_HAIRSTYLE)
- facial_hairstyle = random_facial_hairstyle(gender)
- if(randomise_flags & RANDOMIZE_HAIR_COLOR)
- hair_color = random_short_color()
- if(randomise_flags & RANDOMIZE_FACIAL_HAIR_COLOR)
- facial_hair_color = random_short_color()
- if(randomise_flags & RANDOMIZE_SKIN_TONE)
- skin_tone = random_skin_tone()
- if(randomise_flags & RANDOMIZE_EYE_COLOR)
- eye_color = random_eye_color()
- if(randomise_flags & RANDOMIZE_FEATURES)
- features = random_features()
- var/list/new_features = pref_species.get_random_features() //We do this to keep flavor text, genital sizes etc.
- for(var/key in new_features)
- features[key] = new_features[key]
- mutant_bodyparts = pref_species.get_random_mutant_bodyparts(features)
- body_markings = pref_species.get_random_body_markings(features)
/// Randomizes the character according to preferences.
/datum/preferences/proc/apply_character_randomization_prefs(antag_override = FALSE)
- if(!randomise[RANDOM_BODY] && !(antag_override && randomise[RANDOM_BODY_ANTAG]))
- return // Prefs say "no, thank you"
- if(randomise[RANDOM_GENDER] || antag_override && randomise[RANDOM_GENDER_ANTAG])
- gender = pick(MALE, FEMALE, PLURAL)
- switch(gender)
- if(MALE, FEMALE)
- body_type = gender
- else
- body_type = pick(MALE, FEMALE)
- if(randomise[RANDOM_SPECIES])
- random_species()
- else if(randomise[RANDOM_NAME] || antag_override && randomise[RANDOM_NAME_ANTAG])
- real_name = pref_species.random_name(gender, TRUE)
- if(randomise[RANDOM_AGE] || antag_override && randomise[RANDOM_AGE_ANTAG])
- age = rand(AGE_MIN, AGE_MAX)
- if(randomise[RANDOM_UNDERWEAR])
- underwear = random_underwear(gender)
- if(randomise[RANDOM_UNDERWEAR_COLOR])
- underwear_color = random_short_color()
- if(randomise[RANDOM_UNDERSHIRT])
- undershirt = random_undershirt(gender)
- if(randomise[RANDOM_SOCKS])
- socks = random_socks()
- if(randomise[RANDOM_BACKPACK])
- backpack = random_backpack()
- if(randomise[RANDOM_JUMPSUIT_STYLE])
- jumpsuit_style = pick(GLOB.jumpsuitlist)
- if(randomise[RANDOM_HAIRSTYLE])
- hairstyle = random_hairstyle(gender)
- if(randomise[RANDOM_FACIAL_HAIRSTYLE])
- facial_hairstyle = random_facial_hairstyle(gender)
- if(randomise[RANDOM_HAIR_COLOR])
- hair_color = random_short_color()
- if(randomise[RANDOM_FACIAL_HAIR_COLOR])
- facial_hair_color = random_short_color()
- if(randomise[RANDOM_SKIN_TONE])
- skin_tone = random_skin_tone()
- if(randomise[RANDOM_EYE_COLOR])
- eye_color = random_eye_color()
- features = random_features()
-
+ return
/datum/preferences/proc/random_species()
var/random_species_type = GLOB.species_list[pick(GLOB.roundstart_races)]
@@ -330,3 +251,4 @@
humanoid_icon_cache[icon_id] = out_icon
dummy_key? unset_busy_human_dummy(dummy_key) : qdel(body)
return out_icon
+*/
diff --git a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/human.dm b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/human.dm
index 324d0f64f91..546854ec588 100644
--- a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/human.dm
+++ b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/human.dm
@@ -25,50 +25,6 @@
popup.open()
return
- if("ooc_prefs")
- if(client)
- var/str = "[src]'s OOC Notes : ERP : [client.prefs.erp_pref] | Non-Con : [client.prefs.noncon_pref] | Vore : [client.prefs.vore_pref] | Lewd-system: [client.prefs.sextoys_pref]"
- str += " [html_encode(client.prefs.ooc_prefs)]"
- var/datum/browser/popup = new(usr, "[name]'s ooc info", "[name]'s OOC Information", 500, 200)
- popup.set_content(text("[] [] ", "[name]'s OOC information", replacetext(str, "\n", " ")))
- popup.open()
- return
-
- if("general_record")
- if(client && usr.client.holder)
- var/datum/browser/popup = new(usr, "[name]'s gen rec", "[name]'s General Record", 500, 200)
- popup.set_content(text("[] [] ", "[name]'s general record", replacetext(client.prefs.general_record, "\n", " ")))
- popup.open()
- return
-
- if("security_record")
- if(client && usr.client.holder)
- var/datum/browser/popup = new(usr, "[name]'s sec rec", "[name]'s Security Record", 500, 200)
- popup.set_content(text("[] [] ", "[name]'s security record", replacetext(client.prefs.security_record, "\n", " ")))
- popup.open()
- return
-
- if("medical_record")
- if(client && usr.client.holder)
- var/datum/browser/popup = new(usr, "[name]'s med rec", "[name]'s Medical Record", 500, 200)
- popup.set_content(text("[] [] ", "[name]'s medical record", replacetext(client.prefs.medical_record, "\n", " ")))
- popup.open()
- return
-
- if("background_info")
- if(client && usr.client.holder)
- var/datum/browser/popup = new(usr, "[name]'s flav bg", "[name]'s Background", 500, 200)
- popup.set_content(text("[] [] ", "[name]'s background flavor", replacetext(client.prefs.background_info, "\n", " ")))
- popup.open()
- return
-
- if("exploitable_info")
- if(client && usr.client.holder)
- var/datum/browser/popup = new(usr, "[name]'s exp info", "[name]'s Exploitable Info", 500, 200)
- popup.set_content(text("[] [] ", "[name]'s exploitable information", replacetext(client.prefs.exploitable_info, "\n", " ")))
- popup.open()
- return
-
/mob/living/carbon/human/species/synthliz
race = /datum/species/robotic/synthliz
diff --git a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm
index b0cffe7af71..85f878efcaa 100644
--- a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm
+++ b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm
@@ -1,17 +1,5 @@
GLOBAL_LIST_EMPTY(customizable_races)
-/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] = TRUE
- GLOB.customizable_races[S.id] = TRUE
- else if (S.always_customizable)
- GLOB.customizable_races[S.id] = TRUE
- qdel(S)
- if(!GLOB.roundstart_races.len)
- GLOB.roundstart_races[SPECIES_HUMAN] = TRUE
-
/datum/species
mutant_bodyparts = list()
///Self explanatory
@@ -347,7 +335,7 @@ GLOBAL_LIST_EMPTY(customizable_races)
/datum/species/proc/get_random_body_markings(list/features) //Needs features to base the colour off of
return list()
-/datum/species/proc/on_species_gain(mob/living/carbon/C, datum/species/old_species, pref_load)
+/datum/species/proc/on_species_gain(mob/living/carbon/C, datum/species/old_species)
// Drop the items the new species can't wear
if((AGENDER in species_traits))
C.gender = PLURAL
@@ -387,15 +375,7 @@ GLOBAL_LIST_EMPTY(customizable_races)
if(istype(I))
C.dropItemToGround(I)
else //Entries in the list should only ever be items or null, so if it's not an item, we can assume it's an empty hand
- C.put_in_hands(new mutanthands())
-
- if(ishuman(C))
- var/mob/living/carbon/human/human = C
- for(var/obj/item/organ/external/organ_path as anything in external_organs)
- //Load a persons preferences from DNA
- var/preference_name = human.dna.features[initial(organ_path.preference)]
- var/obj/item/organ/external/new_organ = new organ_path(null, preference_name, human.body_type)
- new_organ.Insert(human)
+ INVOKE_ASYNC(C, /mob/living/carbon.proc/put_in_hands, new mutanthands())
for(var/X in inherent_traits)
ADD_TRAIT(C, X, SPECIES_TRAIT)
diff --git a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/robotic.dm b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/robotic.dm
index 25ce121694a..5102b95941d 100644
--- a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/robotic.dm
+++ b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/robotic.dm
@@ -67,7 +67,6 @@
id = SPECIES_IPC
species_traits = list(
ROBOTIC_DNA_ORGANS,
- MUTCOLORS_PARTSONLY,
EYECOLOR,
LIPS,
HAIR,
diff --git a/modular_skyrat/modules/events/code/event_spawner.dm b/modular_skyrat/modules/events/code/event_spawner.dm
index d4a91c92480..cb91c0b52e5 100644
--- a/modular_skyrat/modules/events/code/event_spawner.dm
+++ b/modular_skyrat/modules/events/code/event_spawner.dm
@@ -48,7 +48,7 @@
gender_string += ", "
gender_string += "[spec_key]"
first = FALSE
- var/warning_string = "WARNING: This spawner will use your currently selected character in prefs ([user.client.prefs.real_name])\nMake sure that the character is not used as a station crew, or would have a good reason to be this role.\nDo you wanna proceed?"
+ var/warning_string = "WARNING: This spawner will use your currently selected character in prefs ([user.client.prefs?.read_preference(/datum/preference/name/real_name)])\nMake sure that the character is not used as a station crew, or would have a good reason to be this role.\nDo you wanna proceed?"
if(species_string)
warning_string += "\nThis role is restricted to those species: [species_string]"
if(gender_string)
@@ -64,10 +64,10 @@
alias = msg
if(!user || !user.client)
return
- if(species_whitelist && !(user.client.prefs.pref_species.id in species_whitelist))
+ if(species_whitelist && !(user.client.prefs?.read_preference(/datum/preference/choiced/species) in species_whitelist))
alert(user, "Sorry, This spawner is limited to those species: [species_string]. Please switch your character.", "", "Ok")
return
- if(gender_whitelist && !(user.client.prefs.gender in gender_whitelist))
+ if(gender_whitelist && !(user.client.prefs?.read_preference(/datum/preference/choiced/gender) in gender_whitelist))
alert(user, "Sorry, This spawner is limited to those genders: [gender_string]. Please switch your character.", "", "Ok")
return
if(used)
@@ -126,12 +126,9 @@
else
equipped.forceMove(get_turf(H))
- var/list/packed_items
if(gets_loadout)
- packed_items = user.client.prefs.equip_preference_loadout(H, FALSE, null)
-
- if(packed_items)
- user.client.prefs.add_packed_items(H, packed_items, FALSE)
+ for(var/datum/loadout_item/item as anything in loadout_list_to_datums(H?.client?.prefs?.loadout_list))
+ item.post_equip_item(H.client?.prefs, H)
//Override access of the ID card here
var/obj/item/card/id/ID
diff --git a/modular_skyrat/modules/examinemore/code/examine_more.dm b/modular_skyrat/modules/examinemore/code/examine_more.dm
index ceb0eb6ed42..1f38aee26dc 100644
--- a/modular_skyrat/modules/examinemore/code/examine_more.dm
+++ b/modular_skyrat/modules/examinemore/code/examine_more.dm
@@ -27,7 +27,7 @@ would only be recognisable with someone that had the syndicate trait.
/obj/item/examine_more(mob/user)
- . = list()
+ . = ..()
if(special_desc)
var/composed_message
switch(special_desc_requirement)
@@ -95,10 +95,6 @@ would only be recognisable with someone that had the syndicate trait.
composed_message += special_desc
. += composed_message
- SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE_MORE, user, .)
- if(!LAZYLEN(.)) // lol ..length
- return list("You examine [src] closer, but find nothing of interest... ")
-
//////////
//Examples:
//////////
diff --git a/modular_skyrat/modules/huds/code/loadout/glasses_loadout.dm b/modular_skyrat/modules/huds/code/loadout/glasses_loadout.dm
deleted file mode 100644
index 1690a7017aa..00000000000
--- a/modular_skyrat/modules/huds/code/loadout/glasses_loadout.dm
+++ /dev/null
@@ -1,14 +0,0 @@
-/datum/loadout_item/glasses/sechud_glasses
- name = "Prescription Security Hud"
- path = /obj/item/clothing/glasses/hud/security/prescription
- restricted_roles = list("Security Officer", "Warden", "Head of Security")
-
-/datum/loadout_item/glasses/medhud_glasses
- name = "Prescription Medical Hud"
- path = /obj/item/clothing/glasses/hud/health/prescription
- restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Paramedic")
-
-/datum/loadout_item/glasses/diaghud_glasses
- name = "Prescription Diagnostic Hud"
- path = /obj/item/clothing/glasses/hud/diagnostic/prescription
- restricted_roles = list("Research Director","Scientist", "Roboticist")
diff --git a/modular_skyrat/modules/hydra/neutral.dm b/modular_skyrat/modules/hydra/neutral.dm
index 460bf0de07b..834df46d170 100644
--- a/modular_skyrat/modules/hydra/neutral.dm
+++ b/modular_skyrat/modules/hydra/neutral.dm
@@ -6,6 +6,7 @@
gain_text = "You hear two other voices inside of your head(s). "
lose_text = "All of your minds become singular. "
medical_record_text = "There are multiple heads and personalities affixed to one body."
+ icon = "horse-head"
/datum/quirk/hydra/add()
var/mob/living/carbon/human/hydra = quirk_holder
diff --git a/modular_skyrat/modules/icspawning/code/modules/spells/spell.dm b/modular_skyrat/modules/icspawning/code/modules/spells/spell.dm
index 98c047c7a2e..6b810578cbb 100644
--- a/modular_skyrat/modules/icspawning/code/modules/spells/spell.dm
+++ b/modular_skyrat/modules/icspawning/code/modules/spells/spell.dm
@@ -38,4 +38,4 @@
// Get them back to their regular name.
ghost.set_ghost_appearance()
if(ghost.client && ghost.client.prefs)
- ghost.deadchat_name = ghost.client.prefs.real_name
+ ghost.deadchat_name = ghost.client.prefs?.read_preference(/datum/preference/name/real_name)
diff --git a/modular_skyrat/modules/loadouts/loadout_items/_loadout_datum.dm b/modular_skyrat/modules/loadouts/loadout_items/_loadout_datum.dm
new file mode 100644
index 00000000000..2575f1ff97c
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/_loadout_datum.dm
@@ -0,0 +1,102 @@
+// -- The loadout item datum and related procs. --
+
+/// Global list of ALL loadout datums instantiated.
+GLOBAL_LIST_EMPTY(all_loadout_datums)
+
+/*
+ * Generate a list of singleton loadout_item datums from all subtypes of [type_to_generate]
+ *
+ * returns a list of singleton datums.
+ */
+/proc/generate_loadout_items(type_to_generate)
+ RETURN_TYPE(/list)
+
+ . = list()
+ if(!ispath(type_to_generate))
+ CRASH("generate_loadout_items(): called with an invalid or null path as an argument!")
+
+ for(var/datum/loadout_item/found_type as anything in subtypesof(type_to_generate))
+ /// Any item without a name is "abstract"
+ if(isnull(initial(found_type.name)))
+ continue
+
+ if(!ispath(initial(found_type.item_path)))
+ stack_trace("generate_loadout_items(): Attempted to instantiate a loadout item ([initial(found_type.name)]) with an invalid or null typepath! (got path: [initial(found_type.item_path)])")
+ continue
+
+ var/datum/loadout_item/spawned_type = new found_type()
+ GLOB.all_loadout_datums[spawned_type.item_path] = spawned_type
+ . |= spawned_type
+
+/// Loadout item datum.
+/// Holds all the information about each loadout items.
+/// A list of singleton loadout items are generated on initialize.
+/datum/loadout_item
+ /// Displayed name of the loadout item.
+ var/name
+ /// Whether this item has greyscale support.
+ var/can_be_greyscale = FALSE
+ /// Whether this item can be renamed.
+ var/can_be_named = FALSE
+ /// The category of the loadout item.
+ var/category
+ /// The actual item path of the loadout item.
+ var/atom/item_path
+ /// List of additional text for the tooltip displayed on this item.
+ var/list/additional_tooltip_contents
+ /// If set, it's a list containing ckeys which only can get the item
+ var/list/ckeywhitelist
+ /// If set, is a list of job names of which can get the loadout item
+ var/list/restricted_roles
+ /// Whether the item is restricted to supporters
+ var/donator_only
+
+/*
+ * Place our [var/item_path] into [outfit].
+ *
+ * By default, just adds the item into the outfit's backpack contents, if non-visual.
+ *
+ * equipper - If we're equipping out outfit onto a mob at the time, this is the mob it is equipped on. Can be null.
+ * outfit - The outfit we're equipping our items into.
+ * visual - If TRUE, then our outfit is only for visual use (for example, a preview).
+ */
+/datum/loadout_item/proc/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(!visuals_only)
+ LAZYADD(outfit.backpack_contents, item_path)
+
+/*
+ * Called When the item is equipped on [equipper].
+ */
+/datum/loadout_item/proc/on_equip_item(datum/preferences/preference_source, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(!preference_source)
+ return
+
+ var/list/our_loadout = null //preference_source?.loadout_list
+ if(can_be_greyscale && (INFO_GREYSCALE in our_loadout[item_path]))
+ if(ispath(item_path, /obj/item/clothing))
+ // When an outfit is equipped in preview, get_equipped_items() does not work, so we have to use GetAllContents()
+ var/obj/item/clothing/equipped_item = locate(item_path) in (visuals_only ? equipper.GetAllContents() : equipper.get_equipped_items())
+ if(equipped_item)
+ equipped_item.set_greyscale(our_loadout[item_path][INFO_GREYSCALE])
+ else
+ stack_trace("[type] on_equip_item(): Could not locate clothing item (path: [item_path]) in [equipper]'s [visuals_only ? "visible":"all"] contents to set greyscaling!")
+
+ else if(!visuals_only)
+ var/obj/item/other_item = locate(item_path) in equipper.get_all_gear()
+ if(other_item)
+ other_item.set_greyscale(our_loadout[item_path][INFO_GREYSCALE])
+ else
+ stack_trace("[type] on_equip_item(): Could not locate backpack item (path: [item_path]) in [equipper]'s contents to set greyscaling!")
+
+ if(can_be_named && !visuals_only && (INFO_NAMED in our_loadout[item_path]))
+ var/obj/item/equipped_item = locate(item_path) in equipper.get_all_gear()
+ if(equipped_item)
+ equipped_item.name = our_loadout[item_path][INFO_NAMED]
+ else
+ stack_trace("[type] on_equip_item(): Could not locate item (path: [item_path]) in [equipper]'s contents to set name!")
+
+/*
+ * Called after the item is equipped on [equipper], at the end of character setup.
+ */
+/datum/loadout_item/proc/post_equip_item(datum/preferences/preference_source, mob/living/carbon/human/equipper)
+ return FALSE
diff --git a/modular_skyrat/modules/loadouts/loadout_items/donator/personal/donator_personal.dm b/modular_skyrat/modules/loadouts/loadout_items/donator/personal/donator_personal.dm
new file mode 100644
index 00000000000..2531c9a722c
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/donator/personal/donator_personal.dm
@@ -0,0 +1,282 @@
+/datum/loadout_item/pocket_items/miafoxplush
+ name = "Mia’s fox plushie"
+ item_path = /obj/item/toy/plush/fox/mia
+ ckeywhitelist = list("fuzlet")
+
+/datum/loadout_item/pocket_items/teasefoxplush
+ name = "Teasable fox plushie"
+ item_path = /obj/item/toy/plush/fox/kailyn
+ ckeywhitelist = list("ratraus")
+
+/datum/loadout_item/pocket_items/drawingtablet
+ name = "Drawing Tablet"
+ item_path = /obj/item/canvas/drawingtablet
+ ckeywhitelist = list("thedragmeme")
+
+/datum/loadout_item/shoes/heeled_jackboots
+ name = "High-heel Jackboots"
+ item_path = /obj/item/clothing/shoes/jackboots/heel
+// ckeywhitelist = list("thedragmeme")
+//As they requested, it's properly public now.
+
+/datum/loadout_item/suit/furcoat
+ name = "Leather coat with fur"
+ item_path = /obj/item/clothing/suit/furcoat
+ ckeywhitelist = list("thedragmeme")
+
+/datum/loadout_item/under/jumpsuit/black_turtleneck
+ name = "Black turtleneck"
+ item_path = /obj/item/clothing/under/syndicate/tacticool/black
+ ckeywhitelist = list("thedragmeme")
+
+/datum/loadout_item/suit/ryddid
+ name = "The Ryddid"
+ item_path = /obj/item/clothing/suit/jacket/ryddid
+ ckeywhitelist = list("grunnyyy")
+
+/datum/loadout_item/under/jumpsuit/draculass
+ name = "Draculass Dress"
+ item_path = /obj/item/clothing/under/costume/draculass
+ ckeywhitelist = list("grunnyyy")
+
+/datum/loadout_item/neck/grunnyyycloak
+ name = "Black and Red cloak"
+ item_path = /obj/item/clothing/neck/cloak/grunnyyy
+ ckeywhitelist = list("grunnyyy")
+
+/datum/loadout_item/gloves/hypnoring_coffee
+ name = "Hypnodemon's Ring"
+ item_path = /obj/item/clothing/gloves/ring/hypno/coffeepot
+ ckeywhitelist = list("coffeepot")
+
+/datum/loadout_item/gloves/hypnoring_bippy
+ name = "Hypnodemon's Ring"
+ item_path = /obj/item/clothing/gloves/ring/hypno/bippys
+ ckeywhitelist = list("bippys")
+
+/datum/loadout_item/suit/kimjacket
+ name = "Aerostatic Bomber Jacket"
+ item_path = /obj/item/clothing/suit/kimjacket
+ ckeywhitelist = list("raxraus")
+
+/datum/loadout_item/under/jumpsuit/rax_turtleneck
+ name = "Black Turtleneck"
+ item_path = /obj/item/clothing/under/rax_turtleneck
+ ckeywhitelist = list("raxraus")
+
+/datum/loadout_item/shoes/rax_armadyne_boots
+ name = "Tactical Boots"
+ item_path = /obj/item/clothing/shoes/combat/rax
+ ckeywhitelist = list("raxraus")
+
+/datum/loadout_item/suit/rax_peacekeeper_jacket
+ name = "Peacekeeper jacket"
+ item_path = /obj/item/clothing/suit/armor/vest/warden/rax
+ ckeywhitelist = list("raxraus")
+ restricted_roles = list("Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
+
+/datum/loadout_item/suit/rax_peacekeeper_turtleneck
+ name = "Peacekeeper turtleneck"
+ item_path = /obj/item/clothing/under/rank/security/blueshieldturtleneck/rax
+ ckeywhitelist = list("raxraus")
+ restricted_roles = list("Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
+
+/datum/loadout_item/suit/rax_aerostatic_jacket
+ name = "Navy Aerostatic Jacket"
+ item_path = /obj/item/clothing/suit/jacket/rax
+ ckeywhitelist = list("raxraus")
+
+/datum/loadout_item/under/jumpsuit/rax_gray_turtleneck
+ name = "Gray Turtleneck"
+ item_path = /obj/item/clothing/under/rax_turtleneck_gray
+ ckeywhitelist = list("raxraus")
+
+/datum/loadout_item/glasses/zentaiglasses
+ name = "Demonic Sunglasses"
+ item_path = /obj/item/clothing/glasses/zentai
+ ckeywhitelist = list("candlejax")
+
+/datum/loadout_item/head/hephelmet
+ name = "HepUnit Standard Helmet"
+ item_path = /obj/item/clothing/head/helmet/sec/peacekeeper/jax
+ ckeywhitelist = list("candlejax")
+ restricted_roles = list("Vanguard Operative","Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
+
+/datum/loadout_item/head/emissionhelm
+ name = "Emission's Helmet"
+ item_path = /obj/item/clothing/head/helmet/space/plasmaman/candlejax
+ ckeywhitelist = list("candlejax")
+ restricted_roles = list("Shaft Miner","Vanguard Operative","Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
+
+/datum/loadout_item/under/jumpsuit/emissionsuit
+ name = "Emission's Suit"
+ item_path = /obj/item/clothing/under/plasmaman/security/candlejax
+ ckeywhitelist = list("candlejax")
+ restricted_roles = list("Shaft Miner","Vanguard Operative","Blueshield", "Head of Security", "Security Officer","Civil Disputes Officer", "Warden", "Detective", "Security Sergeant", "Security Medic", "Corrections Officer")
+
+/datum/loadout_item/head/avipilot
+ name = "Smuggler's Flying Cap"
+ item_path = /obj/item/clothing/head/avipilot
+ ckeywhitelist = list("slippyjoe")
+
+/datum/loadout_item/shoes/britches_shoes
+ name = "Britches' shoes"
+ item_path = /obj/item/clothing/shoes/clown_shoes/britches
+ ckeywhitelist = list("bloodrite")
+
+/datum/loadout_item/under/jumpsuit/britches_dress
+ name = "Britches' dress"
+ item_path = /obj/item/clothing/under/rank/civilian/clown/britches
+ ckeywhitelist = list("bloodrite")
+
+/datum/loadout_item/mask/britches_mask
+ name = "Britches' mask"
+ item_path = /obj/item/clothing/mask/gas/britches
+ ckeywhitelist = list("bloodrite")
+
+/datum/loadout_item/mask/luchador_mask
+ name = "Mask of El Red Templar"
+ item_path = /obj/item/clothing/mask/luchador/enzo
+ ckeywhitelist = list("enzoman")
+
+/datum/loadout_item/mask/nightlight_mask
+ name = "FAR-13 SRU"
+ item_path = /obj/item/clothing/mask/gas/nightlight
+ ckeywhitelist = list("farsightednightlight")
+
+/datum/loadout_item/mask/kindle_mask
+ name = "Kindle's mask"
+ item_path = /obj/item/clothing/mask/kindle
+ ckeywhitelist = list("theooz")
+
+/datum/loadout_item/head/drake_skull
+ name = "Skull of an ashdrake"
+ item_path = /obj/item/clothing/head/drake_skull
+ ckeywhitelist = list("random516")
+
+/datum/loadout_item/gloves/blutigen_wraps
+ name = "Blutigen Wraps"
+ item_path = /obj/item/clothing/gloves/fingerless/blutigen_wraps
+ ckeywhitelist = list("random516")
+
+/datum/loadout_item/suit/blutigen_kimono
+ name = "Blutigen Kimono"
+ item_path = /obj/item/clothing/suit/blutigen_kimono
+ ckeywhitelist = list("random516")
+
+/datum/loadout_item/under/jumpsuit/blutigen_undergarment
+ name = "Dragon Undergarment"
+ item_path = /obj/item/clothing/under/custom/blutigen_undergarment
+ ckeywhitelist = list("random516")
+
+/datum/loadout_item/under/jumpsuit/captain_dress
+ name = "Captain's Dress"
+ item_path = /obj/item/clothing/under/rank/captain/dress
+ ckeywhitelist = list("netrakyram")
+ restricted_roles = list("Captain")
+
+/datum/loadout_item/under/jumpsuit/kilano_suit
+ name = "black and gold dress uniform"
+ item_path = /obj/item/clothing/under/custom/kilano
+ ckeywhitelist = list("netrakyram")
+
+/datum/loadout_item/gloves/kilano_gloves
+ name = "black and gold gloves"
+ item_path = /obj/item/clothing/gloves/kilano
+ ckeywhitelist = list("netrakyram")
+
+/datum/loadout_item/shoes/kilano_boots
+ name = "black and gold boots"
+ item_path = /obj/item/clothing/shoes/winterboots/kilano
+ ckeywhitelist = list("netrakyram")
+
+/datum/loadout_item/neck/kiaracloak
+ name = "Kiara's cloak"
+ item_path = /obj/item/clothing/neck/cloak/inferno
+ ckeywhitelist = list("inferno707")
+
+/datum/loadout_item/neck/kiaracollar
+ name = "Kiara's collar"
+ item_path = /obj/item/clothing/neck/human_petcollar/inferno
+ ckeywhitelist = list("inferno707")
+
+/datum/loadout_item/pocket_items/kiaramedal
+ name = "Insignia of Steele"
+ item_path = /obj/item/clothing/accessory/medal/steele
+ ckeywhitelist = list("inferno707")
+
+/datum/loadout_item/mask/hheart
+ name = "The Hollow Heart"
+ item_path = /obj/item/clothing/mask/hheart
+ ckeywhitelist = list("inferno707")
+
+/datum/loadout_item/pocket_items/darksabre
+ name = "Dark Sabre"
+ item_path = /obj/item/toy/darksabre
+ ckeywhitelist = list("inferno707")
+
+/datum/loadout_item/pocket_items/darksabresheath
+ name = "Dark Sabre Sheath"
+ item_path = /obj/item/storage/belt/sabre/darksabre
+ ckeywhitelist = list("inferno707")
+
+/datum/loadout_item/pocket_items/darkarmor
+ name = "Dark Armor"
+ item_path = /obj/item/clothing/suit/armor/vest/darkcarapace
+ ckeywhitelist = list("inferno707")
+
+/datum/loadout_item/neck/zuliecloak
+ name = "Project: Zul-E"
+ item_path = /obj/item/clothing/suit/hooded/cloak/zuliecloak
+ ckeywhitelist = list("asky")
+
+/datum/loadout_item/pocket_items/brightcosmos
+ name = "Bright Cosmos cigar"
+ item_path = /obj/item/clothing/mask/holocigarette/cigar
+ ckeywhitelist = list("lyricalpaws")
+
+/datum/loadout_item/under/jumpsuit/lannese
+ name = "Lannese Dress"
+ item_path = /obj/item/clothing/under/custom/lannese
+ ckeywhitelist = list("kathrinbailey")
+
+/datum/loadout_item/under/jumpsuit/lannese/vambrace
+ name = "Lannese Dress w/ Vambraces"
+ item_path = /obj/item/clothing/under/custom/lannese/vambrace
+ ckeywhitelist = list("kathrinbailey")
+
+/datum/loadout_item/suit/scraparmour
+ name = "Scrap Armor"
+ item_path = /obj/item/clothing/suit/scraparmour
+ ckeywhitelist = list("hackertdog")
+
+/datum/loadout_item/under/jumpsuit/mechanic
+ name = "Mechanic's Overalls"
+ item_path = /obj/item/clothing/under/misc/mechanic
+ ckeywhitelist = list("cypressb")
+
+/datum/loadout_item/under/jumpsuit/mikubikini
+ name = "starlight singer bikini"
+ item_path = /obj/item/clothing/under/mikubikini
+ ckeywhitelist = list("grandvegeta")
+
+/datum/loadout_item/suit/mikujacket
+ name = "starlight singer jacket"
+ item_path = /obj/item/clothing/suit/mikujacket
+ ckeywhitelist = list("grandvegeta")
+
+/datum/loadout_item/head/mikuhair
+ name = "starlight singer hair"
+ item_path = /obj/item/clothing/head/mikuhair
+ ckeywhitelist = list("grandvegeta")
+
+/datum/loadout_item/gloves/mikugloves
+ name = "starlight singer gloves"
+ item_path = /obj/item/clothing/gloves/mikugloves
+ ckeywhitelist = list("grandvegeta")
+
+/datum/loadout_item/shoes/mikulegging
+ name = "starlight singer bikini"
+ item_path = /obj/item/clothing/shoes/sneakers/mikuleggings
+ ckeywhitelist = list("grandvegeta")
+
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_accessory.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_accessory.dm
new file mode 100644
index 00000000000..cdd4dd34478
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_accessory.dm
@@ -0,0 +1,83 @@
+// --- Loadout item datums for accessories ---
+
+/// Accessory Items (Moves overrided items to backpack)
+GLOBAL_LIST_INIT(loadout_accessory, generate_loadout_items(/datum/loadout_item/accessory))
+
+/datum/loadout_item/accessory
+ category = LOADOUT_ITEM_ACCESSORY
+
+/datum/loadout_item/accessory/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(outfit.accessory)
+ LAZYADD(outfit.backpack_contents, outfit.accessory)
+ outfit.accessory = item_path
+
+/datum/loadout_item/accessory/maid_apron
+ name = "Maid Apron"
+ item_path = /obj/item/clothing/accessory/maidapron
+
+/datum/loadout_item/accessory/waistcoat
+ name = "Waistcoat"
+ item_path = /obj/item/clothing/accessory/waistcoat
+
+/datum/loadout_item/accessory/pocket_protector
+ name = "Pocket Protector"
+ item_path = /obj/item/clothing/accessory/pocketprotector
+
+/datum/loadout_item/accessory/full_pocket_protector
+ name = "Pocket Protector (Filled)"
+ item_path = /obj/item/clothing/accessory/pocketprotector/full
+ additional_tooltip_contents = list("CONTAINS PENS - This item contains multiple pens on spawn.")
+
+/datum/loadout_item/accessory/ribbon
+ name = "Ribbon"
+ item_path = /obj/item/clothing/accessory/medal/ribbon
+
+
+/* TO DO - ADD THESE
+/datum/loadout_item/accessory/blue_green_armband
+ name = "Blue and Green Armband"
+ item_path = /obj/item/clothing/accessory/armband/hydro_cosmetic
+
+/datum/loadout_item/accessory/brown_armband
+ name = "Brown Armband"
+ item_path = /obj/item/clothing/accessory/armband/cargo_cosmetic
+
+/datum/loadout_item/accessory/green_armband
+ name = "Green Armband"
+ item_path = /obj/item/clothing/accessory/armband/service_cosmetic
+
+/datum/loadout_item/accessory/purple_armband
+ name = "Purple Armband"
+ item_path = /obj/item/clothing/accessory/armband/science_cosmetic
+
+/datum/loadout_item/accessory/red_armband
+ name = "Red Armband"
+ item_path = /obj/item/clothing/accessory/armband/deputy_cosmetic
+
+/datum/loadout_item/accessory/yellow_armband
+ name = "Yellow Reflective Armband"
+ item_path = /obj/item/clothing/accessory/armband/engine_cosmetic
+
+/datum/loadout_item/accessory/white_armband
+ name = "White Armband"
+ item_path = /obj/item/clothing/accessory/armband/med_cosmetic
+
+/datum/loadout_item/accessory/white_blue_armband
+ name = "White and Blue Armband"
+ item_path = /obj/item/clothing/accessory/armband/medblue_cosmetic
+
+/datum/loadout_item/accessory/dogtags
+ name = "Name-Inscribed Dogtags"
+ item_path = /obj/item/clothing/accessory/cosmetic_dogtag
+ additional_tooltip_contents = list("MATCHES NAME - The name inscribed on this item matches your character's name on spawn.")
+
+/datum/loadout_item/accessory/bone_charm
+ name = "Heirloom Bone Talismin"
+ item_path = /obj/item/clothing/accessory/armorless_talisman
+ additional_tooltip_contents = list(TOOLTIP_NO_ARMOR)
+
+/datum/loadout_item/accessory/bone_codpiece
+ name = "Heirloom Skull Codpiece"
+ item_path = /obj/item/clothing/accessory/armorless_skullcodpiece
+ additional_tooltip_contents = list(TOOLTIP_NO_ARMOR)
+*/
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_belts.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_belts.dm
new file mode 100644
index 00000000000..90fd995e76c
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_belts.dm
@@ -0,0 +1,64 @@
+// --- Loadout item datums for belts ---
+
+/// Belt Slot Items (Moves overrided items to backpack)
+GLOBAL_LIST_INIT(loadout_belts, generate_loadout_items(/datum/loadout_item/belts))
+
+/datum/loadout_item/belts
+ category = LOADOUT_ITEM_BELT
+
+/datum/loadout_item/belts/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(outfit.belt)
+ LAZYADD(outfit.backpack_contents, outfit.belt)
+ outfit.belt = item_path
+
+/datum/loadout_item/belts/fanny_pack_black
+ name = "Black Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/black
+
+/datum/loadout_item/belts/fanny_pack_blue
+ name = "Blu Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/blue
+
+/datum/loadout_item/belts/fanny_pack_brown
+ name = "Brown Fannypack"
+ item_path = /obj/item/storage/belt/fannypack
+
+/datum/loadout_item/belts/fanny_pack_cyan
+ name = "Cyan Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/cyan
+
+/datum/loadout_item/belts/fanny_pack_green
+ name = "Green Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/green
+
+/datum/loadout_item/belts/fanny_pack_orange
+ name = "Orange Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/orange
+
+/datum/loadout_item/belts/fanny_pack_pink
+ name = "Pink Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/pink
+
+/datum/loadout_item/belts/fanny_pack_purple
+ name = "Purple Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/purple
+
+/datum/loadout_item/belts/fanny_pack_red
+ name = "Red Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/red
+
+/datum/loadout_item/belts/fanny_pack_yellow
+ name = "Yellow Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/yellow
+
+/datum/loadout_item/belts/fanny_pack_white
+ name = "White Fannypack"
+ item_path = /obj/item/storage/belt/fannypack/white
+
+/datum/loadout_item/belts/lantern
+ name = "Lantern"
+ item_path = /obj/item/flashlight/lantern
+
+/datum/loadout_item/belts/candle_box
+ name = "Candle Box"
+ item_path = /obj/item/storage/fancy/candle_box
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_ears.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_ears.dm
new file mode 100644
index 00000000000..4e712433927
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_ears.dm
@@ -0,0 +1,21 @@
+
+// --- Loadout item datums for ears ---
+
+/// Ear Slot Items (Moves overrided items to backpack)
+GLOBAL_LIST_INIT(loadout_ears, generate_loadout_items(/datum/loadout_item/ears))
+
+/datum/loadout_item/ears
+ category = LOADOUT_ITEM_EARS
+
+/datum/loadout_item/ears/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(outfit.ears)
+ LAZYADD(outfit.backpack_contents, outfit.ears)
+ outfit.ears = item_path
+
+/datum/loadout_item/ears/headphones
+ name = "Headphones"
+ item_path = /obj/item/instrument/piano_synth/headphones
+
+/datum/loadout_item/ears/earmuffs
+ name = "Earmuffs"
+ item_path = /obj/item/clothing/ears/earmuffs
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_glasses.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_glasses.dm
new file mode 100644
index 00000000000..02cf5121c52
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_glasses.dm
@@ -0,0 +1,148 @@
+// --- Loadout item datums for glasses ---
+
+/// Glasses Slot Items (Moves overrided items to backpack)
+GLOBAL_LIST_INIT(loadout_glasses, generate_loadout_items(/datum/loadout_item/glasses))
+
+/datum/loadout_item/glasses
+ category = LOADOUT_ITEM_GLASSES
+
+/datum/loadout_item/glasses/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(outfit.glasses)
+ LAZYADD(outfit.backpack_contents, outfit.glasses)
+ outfit.glasses = item_path
+
+/datum/loadout_item/glasses/post_equip_item(datum/preferences/preference_source, mob/living/carbon/human/equipper)
+ var/obj/item/clothing/glasses/equipped_glasses = locate(item_path) in equipper.get_equipped_items()
+ if(equipped_glasses.glass_colour_type)
+ equipper.update_glasses_color(equipped_glasses, TRUE)
+ if(equipped_glasses.tint)
+ equipper.update_tint()
+ if(equipped_glasses.vision_correction)
+ equipper.clear_fullscreen("nearsighted")
+ if(equipped_glasses.vision_flags \
+ || equipped_glasses.darkness_view \
+ || equipped_glasses.invis_override \
+ || equipped_glasses.invis_view \
+ || !isnull(equipped_glasses.lighting_alpha))
+ equipper.update_sight()
+
+/datum/loadout_item/glasses/prescription_glasses
+ name = "Glasses"
+ item_path = /obj/item/clothing/glasses/regular
+ additional_tooltip_contents = list("PRESCRIPTION - This item functions with the 'nearsighted' quirk.")
+
+/datum/loadout_item/glasses/prescription_glasses/circle_glasses
+ name = "Circle Glasses"
+ item_path = /obj/item/clothing/glasses/regular/circle
+
+/datum/loadout_item/glasses/prescription_glasses/hipster_glasses
+ name = "Hipster Glasses"
+ item_path = /obj/item/clothing/glasses/regular/hipster
+
+/datum/loadout_item/glasses/prescription_glasses/jamjar_glasses
+ name = "Jamjar Glasses"
+ item_path = /obj/item/clothing/glasses/regular/jamjar
+
+/datum/loadout_item/glasses/black_blindfold
+ name = "Black Blindfold"
+ item_path = /obj/item/clothing/glasses/blindfold
+
+/datum/loadout_item/glasses/cold_glasses
+ name = "Cold Glasses"
+ item_path = /obj/item/clothing/glasses/cold
+
+/datum/loadout_item/glasses/heat_glasses
+ name = "Heat Glasses"
+ item_path = /obj/item/clothing/glasses/heat
+
+/datum/loadout_item/glasses/geist_glasses
+ name = "Geist Gazers"
+ item_path = /obj/item/clothing/glasses/geist_gazers
+
+/datum/loadout_item/glasses/orange_glasses
+ name = "Orange Glasses"
+ item_path = /obj/item/clothing/glasses/orange
+
+/datum/loadout_item/glasses/psych_glasses
+ name = "Psych Glasses"
+ item_path = /obj/item/clothing/glasses/psych
+
+/datum/loadout_item/glasses/red_glasses
+ name = "Red Glasses"
+ item_path = /obj/item/clothing/glasses/red
+
+/datum/loadout_item/glasses/welding_goggles
+ name = "Welding Goggles"
+ item_path = /obj/item/clothing/glasses/welding
+
+/datum/loadout_item/glasses/eyepatch
+ name = "Eyepatch"
+ item_path = /obj/item/clothing/glasses/eyepatch
+
+
+/datum/loadout_item/glasses/fakeblindfold
+ name = "Fake Blindfold"
+ item_path = /obj/item/clothing/glasses/trickblindfold
+
+/datum/loadout_item/glasses/monocle
+ name = "Monocle"
+ item_path = /obj/item/clothing/glasses/monocle
+
+/datum/loadout_item/glasses/thin
+ name = "Thin Glasses"
+ item_path = /obj/item/clothing/glasses/thin
+
+/datum/loadout_item/glasses/better
+ name = "Modern Glasses"
+ item_path = /obj/item/clothing/glasses/betterunshit
+
+/datum/loadout_item/glasses/eyewrap
+ name = "Eyepatch Wrap"
+ item_path = /obj/item/clothing/glasses/eyepatch/wrap
+
+/datum/loadout_item/glasses/whiteeyepatch
+ name = "White Eyepatch"
+ item_path = /obj/item/clothing/glasses/eyepatch/white
+
+/datum/loadout_item/glasses/biker
+ name = "Biker Goggles"
+ item_path = /obj/item/clothing/glasses/biker
+
+/datum/loadout_item/glasses/medicpatch
+ name = "Medical Eyepatch"
+ item_path = /obj/item/clothing/glasses/hud/eyepatch/med
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Paramedic")
+
+/datum/loadout_item/glasses/robopatch
+ name = "Diagnostic Eyepatch"
+ item_path = /obj/item/clothing/glasses/hud/eyepatch/diagnostic
+ restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Vanguard Operative")
+/datum/loadout_item/glasses/scipatch
+ name = "Science Eyepatch"
+ item_path = /obj/item/clothing/glasses/hud/eyepatch/sci
+ restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Chemist", "Vanguard Operative")
+
+/datum/loadout_item/glasses/sechud
+ name = "Security Hud"
+ item_path = /obj/item/clothing/glasses/hud/security
+ restricted_roles = list("Security Officer", "Security Sergeant", "Warden", "Head of Security", "Civil Disputes Officer")
+
+/datum/loadout_item/glasses/secpatch
+ name = "Security Eyepatch Hud"
+ item_path = /obj/item/clothing/glasses/hud/eyepatch/sec
+ restricted_roles = list("Security Officer", "Security Sergeant", "Warden", "Head of Security", "Civil Disputes Officer")
+
+/datum/loadout_item/glasses/sechud_glasses
+ name = "Prescription Security Hud"
+ item_path = /obj/item/clothing/glasses/hud/security/prescription
+ restricted_roles = list("Security Officer", "Warden", "Head of Security")
+
+/datum/loadout_item/glasses/medhud_glasses
+ name = "Prescription Medical Hud"
+ item_path = /obj/item/clothing/glasses/hud/health/prescription
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Paramedic")
+
+/datum/loadout_item/glasses/diaghud_glasses
+ name = "Prescription Diagnostic Hud"
+ item_path = /obj/item/clothing/glasses/hud/diagnostic/prescription
+ restricted_roles = list("Research Director","Scientist", "Roboticist")
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_gloves.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_gloves.dm
new file mode 100644
index 00000000000..04f36101459
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_gloves.dm
@@ -0,0 +1,93 @@
+// --- Loadout item datums for gloves ---
+
+/// Glove Slot Items (Deletes overrided items)
+GLOBAL_LIST_INIT(loadout_gloves, generate_loadout_items(/datum/loadout_item/gloves))
+
+/datum/loadout_item/gloves
+ category = LOADOUT_ITEM_GLOVES
+
+/datum/loadout_item/gloves/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(isplasmaman(equipper))
+ if(!visuals_only)
+ to_chat(equipper, "Your loadout gloves were not equipped directly due to your envirosuit gloves.")
+ LAZYADD(outfit.backpack_contents, item_path)
+ else
+ outfit.gloves = item_path
+
+/datum/loadout_item/gloves/fingerless
+ name = "Fingerless Gloves"
+ item_path = /obj/item/clothing/gloves/fingerless
+
+/datum/loadout_item/gloves/black
+ name = "Black Gloves"
+ item_path = /obj/item/clothing/gloves/color/black
+
+/datum/loadout_item/gloves/blue
+ name = "Blue Gloves"
+ item_path = /obj/item/clothing/gloves/color/blue
+
+/datum/loadout_item/gloves/brown
+ name = "Brown Gloves"
+ item_path = /obj/item/clothing/gloves/color/brown
+
+/datum/loadout_item/gloves/green
+ name = "Green Gloves"
+ item_path = /obj/item/clothing/gloves/color/green
+
+/datum/loadout_item/gloves/grey
+ name = "Grey Gloves"
+ item_path = /obj/item/clothing/gloves/color/grey
+
+/datum/loadout_item/gloves/light_brown
+ name = "Light Brown Gloves"
+ item_path = /obj/item/clothing/gloves/color/light_brown
+
+/datum/loadout_item/gloves/orange
+ name = "Orange Gloves"
+ item_path = /obj/item/clothing/gloves/color/orange
+
+/datum/loadout_item/gloves/purple
+ name = "Purple Gloves"
+ item_path = /obj/item/clothing/gloves/color/purple
+
+/datum/loadout_item/gloves/rainbow
+ name = "Rainbow Gloves"
+ item_path = /obj/item/clothing/gloves/color/rainbow
+
+/datum/loadout_item/gloves/red
+ name = "Red Gloves"
+ item_path = /obj/item/clothing/gloves/color/red
+
+/datum/loadout_item/gloves/yellow
+ name = "Yellow Gloves"
+ item_path = /obj/item/clothing/gloves/color/yellow
+ additional_tooltip_contents = list("NON-INSULATING - This item is purely cosmetic and provide no shock insulation.")
+
+/datum/loadout_item/gloves/yellow
+ name = "White Gloves"
+ item_path = /obj/item/clothing/gloves/color/white
+
+/datum/loadout_item/gloves/evening
+ name = "Evening Gloves"
+ item_path = /obj/item/clothing/gloves/evening
+
+/datum/loadout_item/gloves/goldring
+ name = "A gold ring"
+ item_path = /obj/item/clothing/gloves/ring
+
+/datum/loadout_item/gloves/silverring
+ name = "A silver ring"
+ item_path = /obj/item/clothing/gloves/ring/silver
+
+/datum/loadout_item/gloves/diamondring
+ name = "A diamond ring"
+ item_path = /obj/item/clothing/gloves/ring/diamond
+
+//Donator gloves down here
+
+/datum/loadout_item/gloves/donator
+ donator_only = TRUE
+
+/datum/loadout_item/gloves/donator/military
+ name = "Military Gloves"
+ item_path = /obj/item/clothing/gloves/military
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_heads.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_heads.dm
new file mode 100644
index 00000000000..c3636fbf93e
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_heads.dm
@@ -0,0 +1,604 @@
+// --- Loadout item datums for heads ---
+
+/// Head Slot Items (Deletes overrided items)
+GLOBAL_LIST_INIT(loadout_helmets, generate_loadout_items(/datum/loadout_item/head))
+
+/datum/loadout_item/head
+ category = LOADOUT_ITEM_HEAD
+
+/datum/loadout_item/head/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(isplasmaman(equipper))
+ if(!visuals_only)
+ to_chat(equipper, "Your loadout helmet was not equipped directly due to your envirosuit helmet.")
+ LAZYADD(outfit.backpack_contents, item_path)
+ else
+ outfit.head = item_path
+
+/datum/loadout_item/head/black_beanie
+ name = "Black Beanie"
+ item_path = /obj/item/clothing/head/beanie/black
+
+/datum/loadout_item/head/christmas_beanie
+ name = "Christmas Beanie"
+ item_path = /obj/item/clothing/head/beanie/christmas
+
+/datum/loadout_item/head/cyan_beanie
+ name = "Cyan Beanie"
+ item_path = /obj/item/clothing/head/beanie/cyan
+
+/datum/loadout_item/head/dark_blue_beanie
+ name = "Dark Blue Beanie"
+ item_path = /obj/item/clothing/head/beanie/darkblue
+
+/datum/loadout_item/head/green_beanie
+ name = "Green Beanie"
+ item_path = /obj/item/clothing/head/beanie/green
+
+/datum/loadout_item/head/orange_beanie
+ name = "Orange Beanie"
+ item_path = /obj/item/clothing/head/beanie/orange
+
+/datum/loadout_item/head/purple_beanie
+ name = "Purple Beanie"
+ item_path = /obj/item/clothing/head/beanie/purple
+
+/datum/loadout_item/head/red_beanie
+ name = "Red Beanie"
+ item_path = /obj/item/clothing/head/beanie/red
+
+/datum/loadout_item/head/striped_beanie
+ name = "Striped Beanie"
+ item_path = /obj/item/clothing/head/beanie/striped
+
+/datum/loadout_item/head/striped_red_beanie
+ name = "Striped Red Beanie"
+ item_path = /obj/item/clothing/head/beanie/stripedred
+
+/datum/loadout_item/head/striped_blue_beanie
+ name = "Striped Blue Beanie"
+ item_path = /obj/item/clothing/head/beanie/stripedblue
+
+/datum/loadout_item/head/striped_green_beanie
+ name = "Striped Green Beanie"
+ item_path = /obj/item/clothing/head/beanie/stripedgreen
+
+/datum/loadout_item/head/white_beanie
+ name = "White Beanie"
+ item_path = /obj/item/clothing/head/beanie
+
+/datum/loadout_item/head/yellow_beanie
+ name = "Yellow Beanie"
+ item_path = /obj/item/clothing/head/beanie/yellow
+
+/datum/loadout_item/head/greyscale_beret
+ name = "Greyscale Beret"
+ can_be_greyscale = TRUE
+ item_path = /obj/item/clothing/head/beret
+
+/datum/loadout_item/head/black_beret
+ name = "Black Beret"
+ item_path = /obj/item/clothing/head/beret/black
+
+/datum/loadout_item/head/black_cap
+ name = "Black Cap"
+ item_path = /obj/item/clothing/head/soft/black
+
+/datum/loadout_item/head/blue_cap
+ name = "Blue Cap"
+ item_path = /obj/item/clothing/head/soft/blue
+
+/datum/loadout_item/head/delinquent_cap
+ name = "Delinquent Cap"
+ item_path = /obj/item/clothing/head/delinquent
+
+/datum/loadout_item/head/green_cap
+ name = "Green Cap"
+ item_path = /obj/item/clothing/head/soft/green
+
+/datum/loadout_item/head/grey_cap
+ name = "Grey Cap"
+ item_path = /obj/item/clothing/head/soft/grey
+
+/datum/loadout_item/head/orange_cap
+ name = "Orange Cap"
+ item_path = /obj/item/clothing/head/soft/orange
+
+/datum/loadout_item/head/purple_cap
+ name = "Purple Cap"
+ item_path = /obj/item/clothing/head/soft/purple
+
+/datum/loadout_item/head/rainbow_cap
+ name = "Rainbow Cap"
+ item_path = /obj/item/clothing/head/soft/rainbow
+
+/datum/loadout_item/head/red_cap
+ name = "Red Cap"
+ item_path = /obj/item/clothing/head/soft/red
+
+/datum/loadout_item/head/white_cap
+ name = "White Cap"
+ item_path = /obj/item/clothing/head/soft
+
+/datum/loadout_item/head/yellow_cap
+ name = "Yellow Cap"
+ item_path = /obj/item/clothing/head/soft/yellow
+
+/datum/loadout_item/head/flatcap
+ name = "Flat Cap"
+ item_path = /obj/item/clothing/head/flatcap
+
+/datum/loadout_item/head/beige_fedora
+ name = "Beige Fedora"
+ item_path = /obj/item/clothing/head/fedora/beige
+
+/datum/loadout_item/head/black_fedora
+ name = "Black Fedora"
+ item_path = /obj/item/clothing/head/fedora
+
+/datum/loadout_item/head/white_fedora
+ name = "White Fedora"
+ item_path = /obj/item/clothing/head/fedora/white
+
+/datum/loadout_item/head/dark_blue_hardhat
+ name = "Dark Blue Hardhat"
+ item_path = /obj/item/clothing/head/hardhat/dblue
+
+/datum/loadout_item/head/orange_hardhat
+ name = "Orange Hardhat"
+ item_path = /obj/item/clothing/head/hardhat/orange
+
+/datum/loadout_item/head/red_hardhat
+ name = "Red Hardhat"
+ item_path = /obj/item/clothing/head/hardhat/red
+
+/datum/loadout_item/head/white_hardhat
+ name = "White Hardhat"
+ item_path = /obj/item/clothing/head/hardhat/white
+
+/datum/loadout_item/head/yellow_hardhat
+ name = "Yellow Hardhat"
+ item_path = /obj/item/clothing/head/hardhat
+
+/datum/loadout_item/head/mail_cap
+ name = "Mail Cap"
+ item_path = /obj/item/clothing/head/mailman
+
+/datum/loadout_item/head/nurse_hat
+ name = "Nurse Hat"
+ item_path = /obj/item/clothing/head/nursehat
+
+/datum/loadout_item/head/kitty_ears
+ name = "Kitty Ears"
+ item_path = /obj/item/clothing/head/kitty
+
+/datum/loadout_item/head/rabbit_ears
+ name = "Rabbit Ears"
+ item_path = /obj/item/clothing/head/rabbitears
+
+/datum/loadout_item/head/bandana
+ name = "Bandana"
+ item_path = /obj/item/clothing/head/bandana
+
+/datum/loadout_item/head/rastafarian
+ name = "Rastafarian Cap"
+ item_path = /obj/item/clothing/head/beanie/rasta
+
+/datum/loadout_item/head/top_hat
+ name = "Top Hat"
+ item_path = /obj/item/clothing/head/that
+
+/datum/loadout_item/head/bowler_hat
+ name = "Bowler Hat"
+ item_path = /obj/item/clothing/head/bowler
+
+/datum/loadout_item/head/bear_pelt
+ name = "Bear Pelt"
+ item_path = /obj/item/clothing/head/bearpelt
+
+/datum/loadout_item/head/ushanka
+ name ="Ushanka"
+ item_path = /obj/item/clothing/head/ushanka
+
+/datum/loadout_item/head/plague_doctor
+ name = "Plague Doctor Cap"
+ item_path = /obj/item/clothing/head/plaguedoctorhat
+
+/datum/loadout_item/head/wedding_veil
+ name = "Wedding Veil"
+ item_path = /obj/item/clothing/head/weddingveil
+
+/datum/loadout_item/head/poppy
+ name = "Poppy"
+ item_path = /obj/item/food/grown/poppy
+
+/datum/loadout_item/head/lily
+ name = "Lily"
+ item_path = /obj/item/food/grown/poppy/lily
+
+/datum/loadout_item/head/geranium
+ name = "Geranium"
+ item_path = /obj/item/food/grown/poppy/geranium
+
+/datum/loadout_item/head/rose
+ name = "Rose"
+ item_path = /obj/item/food/grown/rose
+
+/datum/loadout_item/head/sunflower
+ name = "Sunflower"
+ item_path = /obj/item/grown/sunflower
+
+/datum/loadout_item/head/harebell
+ name = "Harebell"
+ item_path = /obj/item/food/grown/harebell
+
+/datum/loadout_item/head/rainbow_bunch
+ name = "Rainbow Bunch"
+ item_path = /obj/item/food/grown/rainbow_flower
+ additional_tooltip_contents = list(TOOLTIP_RANDOM_COLOR)
+
+//MISC
+/datum/loadout_item/head/baseball
+ name = "Ballcap"
+ item_path = /obj/item/clothing/head/soft/mime
+
+/datum/loadout_item/head/beanie
+ name = "Beanie"
+ item_path = /obj/item/clothing/head/beanie
+
+
+/datum/loadout_item/head/beret
+ name = "Black beret"
+ item_path = /obj/item/clothing/head/beret/black
+
+/datum/loadout_item/head/flatcap
+ name = "Flat cap"
+ item_path = /obj/item/clothing/head/flatcap
+
+/datum/loadout_item/head/pflatcap
+ name = "Poly Flat cap"
+ item_path = /obj/item/clothing/head/polyflatc
+
+
+/datum/loadout_item/head/pirate
+ name = "Pirate hat"
+ item_path = /obj/item/clothing/head/pirate
+
+/datum/loadout_item/head/flowerpin
+ name = "Flower Pin"
+ item_path = /obj/item/clothing/head/flowerpin
+
+
+/datum/loadout_item/head/rice_hat
+ name = "Rice hat"
+ item_path = /obj/item/clothing/head/rice_hat
+
+/datum/loadout_item/head/ushanka
+ name = "Ushanka"
+ item_path = /obj/item/clothing/head/ushanka
+
+/datum/loadout_item/head/wrussian
+ name = "Black Papakha"
+ item_path = /obj/item/clothing/head/whiterussian
+
+/datum/loadout_item/head/wrussianw
+ name = "White Papakha"
+ item_path = /obj/item/clothing/head/whiterussian/white
+
+/datum/loadout_item/head/wrussianb
+ name = "Black and Red Papakha"
+ item_path = /obj/item/clothing/head/whiterussian/black
+
+/datum/loadout_item/head/slime
+ name = "Slime hat"
+ item_path = /obj/item/clothing/head/collectable/slime
+
+/datum/loadout_item/head/fedora
+ name = "Fedora"
+ item_path = /obj/item/clothing/head/fedora
+
+/datum/loadout_item/head/that
+ name = "Top Hat"
+ item_path = /obj/item/clothing/head/that
+
+/datum/loadout_item/head/flakhelm
+ name = "Flak Helmet"
+ item_path = /obj/item/clothing/head/flakhelm
+
+
+/datum/loadout_item/head/bunnyears
+ name = "Bunny Ears"
+ item_path = /obj/item/clothing/head/rabbitears
+
+/datum/loadout_item/head/mailmanhat
+ name = "Mailman's Hat"
+ item_path = /obj/item/clothing/head/mailman
+
+/datum/loadout_item/head/whitekepi
+ name = "White Kepi"
+ item_path = /obj/item/clothing/head/kepi
+
+/datum/loadout_item/head/whitekepiold
+ name = "White Kepi, Old"
+ item_path = /obj/item/clothing/head/kepi/old
+
+/datum/loadout_item/head/hijab
+ name = "Hijab"
+ item_path = /obj/item/clothing/head/hijab
+
+
+/datum/loadout_item/head/turban
+ name = "Turban"
+ item_path = /obj/item/clothing/head/turb
+
+
+/datum/loadout_item/head/keff
+ name = "Keffiyeh"
+ item_path = /obj/item/clothing/head/keffiyeh
+
+
+/datum/loadout_item/head/maidhead
+ name = "Maid Headband"
+ item_path = /obj/item/clothing/head/maid
+
+/datum/loadout_item/head/widehat
+ name = "Wide Black Hat"
+ item_path = /obj/item/clothing/head/costume/widehat
+
+/datum/loadout_item/head/widehat_red
+ name = "Wide Red Hat"
+ item_path = /obj/item/clothing/head/costume/widehat/red
+
+/datum/loadout_item/head/cardboard
+ name = "Cardboard Helmet"
+ item_path = /obj/item/clothing/head/cardborg
+
+
+/datum/loadout_item/head/wig
+ name = "Wig"
+ item_path = /obj/item/clothing/head/wig
+
+
+/datum/loadout_item/head/wignatural
+ name = "Natural Wig"
+ item_path = /obj/item/clothing/head/wig/natural
+
+//Cowboy Stuff
+/datum/loadout_item/head/cowboyhat
+ name = "Cowboy Hat, Brown"
+ item_path = /obj/item/clothing/head/cowboyhat
+
+/datum/loadout_item/head/cowboyhat_black
+ name = "Cowboy Hat, Black"
+ item_path = /obj/item/clothing/head/cowboyhat/black
+
+/datum/loadout_item/head/cowboyhat_blackwide
+ name = "Wide Cowboy Hat, Black"
+ item_path = /obj/item/clothing/head/cowboyhat/blackwide
+
+/datum/loadout_item/head/cowboyhat_wide
+ name = "Wide Cowboy Hat, Brown"
+ item_path = /obj/item/clothing/head/cowboyhat/wide
+
+/datum/loadout_item/head/cowboyhat_white
+ name = "Cowboy Hat, White"
+ item_path = /obj/item/clothing/head/cowboyhat/white
+
+/datum/loadout_item/head/cowboyhat_pink
+ name = "Cowboy Hat, Pink"
+ item_path = /obj/item/clothing/head/cowboyhat/pink
+
+/datum/loadout_item/head/cowboyhat_winter
+ name = "Winter Cowboy Hat"
+ item_path = /obj/item/clothing/head/cowboyhat/sheriff
+
+/datum/loadout_item/head/cowboyhat_sheriff
+ name = "Sheriff Hat"
+ item_path = /obj/item/clothing/head/cowboyhat/sheriff/alt
+
+/datum/loadout_item/head/cowboyhat_deputy
+ name = "Deputy Hat"
+ item_path = /obj/item/clothing/head/cowboyhat/deputy
+
+//trek fancy Hats!
+/datum/loadout_item/head/trekcap
+ name = "Federation Officer's Cap (White)"
+ item_path = /obj/item/clothing/head/caphat/formal/fedcover
+ restricted_roles = list("Captain","Head of Personnel")
+
+/datum/loadout_item/head/trekcapcap
+ name = "Federation Officer's Cap (Black)"
+ item_path = /obj/item/clothing/head/caphat/formal/fedcover/black
+ restricted_roles = list("Captain","Head of Personnel")
+
+/datum/loadout_item/head/trekcapmedisci
+ name = "Federation Officer's Cap (Blue)"
+ item_path = /obj/item/clothing/head/caphat/formal/fedcover/medsci
+ restricted_roles = list("Chief Medical Officer","Medical Doctor","Security Medic","Paramedic","Chemist","Virologist","Psychologist","Geneticist","Research Director","Scientist","Roboticist","Vanguard Operative")
+
+/datum/loadout_item/head/trekcapeng
+ name = "Federation Officer's Cap (Yellow)"
+ item_path = /obj/item/clothing/head/caphat/formal/fedcover/eng
+ restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Security Medic","Security Sergeant","Warden","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
+
+/datum/loadout_item/head/trekcapsec
+ name = "Federation Officer's Cap (Red)"
+ item_path = /obj/item/clothing/head/caphat/formal/fedcover/sec
+ restricted_roles = list("Chief Engineer","Atmospheric Technician","Station Engineer","Security Medic","Security Sergeant","Warden","Detective","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer","Cargo Technician", "Shaft Miner", "Quartermaster")
+
+/datum/loadout_item/head/imperial_cap
+ name = "Captain's Naval Cap"
+ item_path = /obj/item/clothing/head/imperial/cap
+ restricted_roles = list("Captain", "Nanotrasen Representative")
+
+/datum/loadout_item/head/imperial_hop
+ name = "Head of Personnel's Naval Cap"
+ item_path = /obj/item/clothing/head/imperial/hop
+ restricted_roles = list("Head of Personnel", "Nanotrasen Representative")
+
+/datum/loadout_item/head/imperial_ce
+ name = "Chief Engineer's blast helmet."
+ item_path = /obj/item/clothing/head/imperial/ce
+ restricted_roles = list("Chief Engineer")
+
+/datum/loadout_item/head/cowboyhat_sec
+ name = "Cowboy Hat, Security"
+ item_path = /obj/item/clothing/head/cowboyhat/sec
+ restricted_roles = list("Warden","Detective","Security Medic","Security Sergeant","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer")
+
+/datum/loadout_item/head/cowboyhat_secwide
+ name = "Wide Cowboy Hat, Security"
+ item_path = /obj/item/clothing/head/cowboyhat/widesec
+ restricted_roles = list("Warden","Detective","Security Medic","Security Sergeant","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer")
+
+/datum/loadout_item/head/ushanka
+ name = "Security Ushanka"
+ item_path = /obj/item/clothing/head/ushankasec
+ restricted_roles = list("Warden","Detective","Security Medic","Security Sergeant","Security Officer","Head of Security","Civil Disputes Officer","Corrections Officer")
+
+/datum/loadout_item/head/blasthelmet
+ name = "General's Helmet"
+ item_path = /obj/item/clothing/head/imperialhelmet
+ restricted_roles = list("Warden","Detective","Security Medic","Security Sergeant","Security Officer","Civil Disputes Officer","Corrections Officer","Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer")
+
+/datum/loadout_item/head/navybluehoscap
+ name = "Head of Security's Naval Cap"
+ item_path = /obj/item/clothing/head/imperial/hos
+ restricted_roles = list("Head of Security")
+
+/datum/loadout_item/head/navyblueofficerberet
+ name = "Security officer's Navyblue beret"
+ item_path = /obj/item/clothing/head/beret/sec/navyofficer
+ restricted_roles = list("Security Officer","Security Medic","Security Sergeant",)
+
+/datum/loadout_item/head/solofficercap
+ name = "Security officer's Sol Cap"
+ item_path = /obj/item/clothing/head/sec/peacekeeper/sol
+ restricted_roles = list("Security Officer","Security Medic","Security Sergeant",)
+
+/datum/loadout_item/head/soltrafficoff
+ name = "Traffic Officer Cap"
+ item_path = /obj/item/clothing/head/soltraffic
+ restricted_roles = list("Security Officer","Security Medic","Security Sergeant","Civil Disputes Officer")
+
+/datum/loadout_item/head/navybluewardenberet
+ name = "Warden's navyblue beret"
+ item_path = /obj/item/clothing/head/beret/sec/navywarden
+ restricted_roles = list("Warden")
+
+/datum/loadout_item/head/cybergoggles //Cyberpunk-P.I. Outfit
+ name = "Type-34C Forensics Headwear"
+ item_path = /obj/item/clothing/head/fedora/det_hat/cybergoggles
+ restricted_roles = list("Detective")
+
+/datum/loadout_item/head/nursehat
+ name = "Nurse Hat"
+ item_path = /obj/item/clothing/head/nursehat
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist","Security Medic")
+
+/datum/loadout_item/head/imperial_generic
+ name = "Naval Officer Cap"
+ item_path = /obj/item/clothing/head/imperial
+ restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
+
+/datum/loadout_item/head/imperial_grey
+ name = "Grey Naval Officer Cap"
+ item_path = /obj/item/clothing/head/imperial/grey
+ restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
+
+/datum/loadout_item/head/imperial_red
+ name = "Red Naval Officer Cap"
+ item_path = /obj/item/clothing/head/imperial/red
+ restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer")
+
+// JOB - Berets
+/datum/loadout_item/head/atmos_beret
+ name = "Atmospherics Beret"
+ item_path = /obj/item/clothing/head/beret/atmos
+ restricted_roles = list("Station Engineer", "Atmospheric Technician", "Chief Engineer")
+
+/datum/loadout_item/head/engi_beret
+ name = "Engineering Beret"
+ item_path = /obj/item/clothing/head/beret/engi
+ restricted_roles = list("Station Engineer", "Atmospheric Technician", "Chief Engineer")
+/datum/loadout_item/head/beret_med
+ name = "Medical Beret"
+ item_path = /obj/item/clothing/head/beret/medical
+ restricted_roles = list("Medical Doctor","Virologist", "Chemist", "Chief Medical Officer","Security Medic")
+
+/datum/loadout_item/head/beret_paramedic
+ name = "Paramedic Beret"
+ item_path = /obj/item/clothing/head/beret/medical/paramedic
+ restricted_roles = list("Paramedic", "Chief Medical Officer")
+
+/datum/loadout_item/head/beret_viro
+ name = "Virologist Beret"
+ item_path = /obj/item/clothing/head/beret/medical/virologist
+ restricted_roles = list("Virologist", "Chief Medical Officer")
+
+/datum/loadout_item/head/beret_chem
+ name = "Chemist Beret"
+ item_path = /obj/item/clothing/head/beret/medical/chemist
+ restricted_roles = list("Chemist", "Chief Medical Officer")
+
+/datum/loadout_item/head/beret_sci
+ name = "Scientist's Beret"
+ item_path = /obj/item/clothing/head/beret/science
+ restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Vanguard Operative")
+
+/datum/loadout_item/head/beret_robo
+ name = "Roboticist's Beret"
+ item_path = /obj/item/clothing/head/beret/science/fancy/robo
+ restricted_roles = list("Roboticist", "Research Director")
+
+/datum/loadout_item/head/brfed
+ name = "Brown Fedora"
+ item_path = /obj/item/clothing/head/fedora/fedbrown
+
+/datum/loadout_item/head/blfed
+ name = "Black Fedora"
+ item_path = /obj/item/clothing/head/fedora/fedblack
+
+//head
+/datum/loadout_item/head/dominacap
+ name = "Dominant cap"
+ item_path = /obj/item/clothing/head/domina_cap
+
+// Donator hats here
+/datum/loadout_item/head/donator
+ donator_only = TRUE
+
+/datum/loadout_item/head/donator/poppy
+ name = "Poppy Flower"
+ item_path = /obj/item/food/grown/poppy
+
+/datum/loadout_item/head/donator/lily
+ name = "Lily Flower"
+ item_path = /obj/item/food/grown/poppy/lily
+
+/datum/loadout_item/head/donator/geranium
+ name = "Geranium Flower"
+ item_path = /obj/item/food/grown/poppy/geranium
+
+/datum/loadout_item/head/donator/fraxinella
+ name = "Fraxinella Flower"
+ item_path = /obj/item/food/grown/poppy/geranium/fraxinella
+
+/datum/loadout_item/head/donator/harebell
+ name = "Harebell Flower"
+ item_path = /obj/item/food/grown/harebell
+
+/datum/loadout_item/head/donator/rose
+ name = "Rose Flower"
+ item_path = /obj/item/food/grown/rose
+
+/datum/loadout_item/head/donator/carbon_rose
+ name = "Carbon Rose Flower"
+ item_path = /obj/item/grown/carbon_rose
+
+/datum/loadout_item/head/donator/enclave
+ name = "Enclave Cap"
+ item_path = /obj/item/clothing/head/soft/enclave
+
+/datum/loadout_item/head/donator/enclaveo
+ name = "Enclave Cap - officer"
+ item_path = /obj/item/clothing/head/soft/enclaveo
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_inhands.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_inhands.dm
new file mode 100644
index 00000000000..660d85daad0
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_inhands.dm
@@ -0,0 +1,51 @@
+// --- Loadout item datums for inhand items ---
+
+/// Inhand items (Moves overrided items to backpack)
+GLOBAL_LIST_INIT(loadout_inhand_items, generate_loadout_items(/datum/loadout_item/inhand))
+
+/datum/loadout_item/inhand
+ category = LOADOUT_ITEM_INHAND
+
+/datum/loadout_item/inhand/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(outfit.l_hand && !outfit.r_hand)
+ outfit.r_hand = item_path
+ else
+ if(outfit.l_hand)
+ LAZYADD(outfit.backpack_contents, outfit.l_hand)
+ outfit.l_hand = item_path
+
+/datum/loadout_item/inhand/cane
+ name = "Cane"
+ item_path = /obj/item/cane
+
+/datum/loadout_item/inhand/briefcase
+ name = "Briefcase"
+ item_path = /obj/item/storage/briefcase
+
+/datum/loadout_item/inhand/briefcase_secure
+ name = "Secure Briefcase"
+ item_path = /obj/item/storage/secure/briefcase
+
+/datum/loadout_item/inhand/skateboard
+ name = "Skateboard"
+ item_path = /obj/item/melee/skateboard
+
+/datum/loadout_item/inhand/bouquet_mixed
+ name = "Mixed Bouquet"
+ item_path = /obj/item/bouquet
+
+/datum/loadout_item/inhand/bouquet_sunflower
+ name = "Sunflower Bouquet"
+ item_path = /obj/item/bouquet/sunflower
+
+/datum/loadout_item/inhand/bouquet_poppy
+ name = "Poppy Bouquet"
+ item_path = /obj/item/bouquet/poppy
+
+/datum/loadout_item/inhand/bouquet_rose
+ name = "Rose Bouquet"
+ item_path = /obj/item/bouquet/rose
+
+/datum/loadout_item/inhand/cane
+ name = "Cane"
+ item_path = /obj/item/cane
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_masks.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_masks.dm
new file mode 100644
index 00000000000..71174e91e3a
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_masks.dm
@@ -0,0 +1,130 @@
+// --- Loadout item datums for masks ---
+
+/// Mask Slot Items (Deletes overrided items)
+GLOBAL_LIST_INIT(loadout_masks, generate_loadout_items(/datum/loadout_item/mask))
+
+/datum/loadout_item/mask
+ category = LOADOUT_ITEM_MASK
+
+/datum/loadout_item/mask/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(isplasmaman(equipper))
+ if(!visuals_only)
+ to_chat(equipper, "Your loadout mask was not equipped directly due to your envirosuit mask.")
+ LAZYADD(outfit.backpack_contents, item_path)
+ else
+ outfit.mask = item_path
+
+/datum/loadout_item/mask/balaclava
+ name = "Balaclava"
+ item_path = /obj/item/clothing/mask/balaclava
+
+/datum/loadout_item/mask/gas_mask
+ name = "Gas Mask"
+ item_path = /obj/item/clothing/mask/gas
+
+/datum/loadout_item/mask/black_bandana
+ name = "Black Bandana"
+ item_path = /obj/item/clothing/mask/bandana/black
+
+/datum/loadout_item/mask/blue_bandana
+ name = "Blue Bandana"
+ item_path = /obj/item/clothing/mask/bandana/blue
+
+/datum/loadout_item/mask/gold_bandana
+ name = "Gold Bandana"
+ item_path = /obj/item/clothing/mask/bandana/gold
+
+/datum/loadout_item/mask/green_bandana
+ name = "Green Bandana"
+ item_path = /obj/item/clothing/mask/bandana/green
+
+/datum/loadout_item/mask/red_bandana
+ name = "Red Bandana"
+ item_path = /obj/item/clothing/mask/bandana/red
+
+/datum/loadout_item/mask/skull_bandana
+ name = "Skull Bandana"
+ item_path = /obj/item/clothing/mask/bandana/skull
+
+/datum/loadout_item/mask/surgical_mask
+ name = "Face Mask"
+ item_path = /obj/item/clothing/mask/surgical
+
+/datum/loadout_item/mask/fake_mustache
+ name = "Fake Moustache"
+ item_path = /obj/item/clothing/mask/fakemoustache
+
+/datum/loadout_item/mask/pipe
+ name = "Pipe"
+ item_path = /obj/item/clothing/mask/cigarette/pipe
+
+/datum/loadout_item/mask/corn_pipe
+ name = "Corn Cob Pipe"
+ item_path = /obj/item/clothing/mask/cigarette/pipe/cobpipe
+
+/datum/loadout_item/mask/plague_doctor
+ name = "Plague Doctor Mask"
+ item_path = /obj/item/clothing/mask/gas/plaguedoctor
+
+/datum/loadout_item/mask/joy
+ name = "Joy Mask"
+ item_path = /obj/item/clothing/mask/joy
+
+/datum/loadout_item/mask/lollipop
+ name = "Lollipop"
+ item_path = /obj/item/food/lollipop
+
+/datum/loadout_item/mask/balaclava
+ name = "Balaclava"
+ item_path = /obj/item/clothing/mask/balaclava
+
+/datum/loadout_item/mask/balaclavaadj
+ name = "Adjustable Balaclava"
+ item_path = /obj/item/clothing/mask/balaclavaadjust
+
+/datum/loadout_item/mask/balaclavathree
+ name = "Three Hole Balaclava"
+ item_path = /obj/item/clothing/mask/balaclava/threehole
+
+/datum/loadout_item/mask/balaclavagreen
+ name = "Three Hole Green Balaclava"
+ item_path = /obj/item/clothing/mask/balaclava/threehole/green
+
+/datum/loadout_item/mask/moustache
+ name = "Fake moustache"
+ item_path = /obj/item/clothing/mask/fakemoustache
+
+/datum/loadout_item/mask/bandana_redft
+ name = "Skin Tight Red Bandana"
+ item_path = /obj/item/clothing/mask/bandana/red/ft
+
+/datum/loadout_item/mask/bandana_blueft
+ name = "Skin Tight Blue Bandana"
+ item_path = /obj/item/clothing/mask/bandana/blue/ft
+
+/datum/loadout_item/mask/bandana_greenft
+ name = "Skin Tight Green Bandana"
+ item_path = /obj/item/clothing/mask/bandana/green/ft
+
+/datum/loadout_item/mask/bandana_goldft
+ name = "Skin Tight Gold Bandana"
+ item_path = /obj/item/clothing/mask/bandana/gold/ft
+
+/datum/loadout_item/mask/bandana_blackft
+ name = "Skin Tight Black Bandana"
+ item_path = /obj/item/clothing/mask/bandana/black/ft
+
+/datum/loadout_item/mask/bandana_skullft
+ name = "Skin Tight Skull Bandana"
+ item_path = /obj/item/clothing/mask/bandana/skull/ft
+
+
+/datum/loadout_item/mask/gas_glass
+ name = "Glass Gas Mask"
+ item_path = /obj/item/clothing/mask/gas/glass
+
+
+/datum/loadout_item/mask/surgical
+ name = "Sterile Mask"
+ item_path = /obj/item/clothing/mask/surgical
+ restricted_roles = list("Chief Medical Officer", "Medical Doctor", "Virologist", "Chemist", "Geneticist", "Paramedic", "Psychologist","Security Medic")
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_neck.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_neck.dm
new file mode 100644
index 00000000000..54b0b12dbd9
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_neck.dm
@@ -0,0 +1,228 @@
+// --- Loadout item datums for neck items ---
+
+/// Neck Slot Items (Deletes overrided items)
+GLOBAL_LIST_INIT(loadout_necks, generate_loadout_items(/datum/loadout_item/neck))
+
+/datum/loadout_item/neck
+ category = LOADOUT_ITEM_NECK
+
+/datum/loadout_item/neck/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ outfit.neck = item_path
+
+/datum/loadout_item/neck/scarf_black
+ name = "Black Scarf"
+ item_path = /obj/item/clothing/neck/scarf/black
+
+/datum/loadout_item/neck/scarf_christmas
+ name = "Christmas Scarf"
+ item_path = /obj/item/clothing/neck/scarf/christmas
+
+/datum/loadout_item/neck/scarf_cyan
+ name = "Cyan Scarf"
+ item_path = /obj/item/clothing/neck/scarf/cyan
+
+/datum/loadout_item/neck/scarf_dark_blue
+ name = "Darkblue Scarf"
+ item_path = /obj/item/clothing/neck/scarf/darkblue
+
+/datum/loadout_item/neck/scarf_green
+ name = "Green Scarf"
+ item_path = /obj/item/clothing/neck/scarf/green
+
+/datum/loadout_item/neck/scarf_pink
+ name = "Pink Scarf"
+ item_path = /obj/item/clothing/neck/scarf/pink
+
+/datum/loadout_item/neck/scarf_purple
+ name = "Purple Scarf"
+ item_path = /obj/item/clothing/neck/scarf/purple
+
+/datum/loadout_item/neck/scarf_red
+ name = "Red Scarf"
+ item_path = /obj/item/clothing/neck/scarf/red
+
+/datum/loadout_item/neck/scarf_blue_striped
+ name = "Striped Blue Scarf"
+ item_path = /obj/item/clothing/neck/stripedbluescarf
+
+/datum/loadout_item/neck/scarf_green_striped
+ name = "Striped Green Scarf"
+ item_path = /obj/item/clothing/neck/stripedgreenscarf
+
+/datum/loadout_item/neck/scarf_red_striped
+ name = "Striped Red Scarf"
+ item_path = /obj/item/clothing/neck/stripedredscarf
+
+/datum/loadout_item/neck/scarf_orange
+ name = "Orange Scarf"
+ item_path = /obj/item/clothing/neck/scarf/orange
+
+/datum/loadout_item/neck/scarf_yellow
+ name = "Yellow Scarf"
+ item_path = /obj/item/clothing/neck/scarf/yellow
+
+/datum/loadout_item/neck/scarf_white
+ name = "White Scarf"
+ item_path = /obj/item/clothing/neck/scarf
+
+/datum/loadout_item/neck/scarf_zebra
+ name = "Zebra Scarf"
+ item_path = /obj/item/clothing/neck/scarf/zebra
+
+/datum/loadout_item/neck/necktie_black
+ name = "Black Necktie"
+ item_path = /obj/item/clothing/neck/tie/black
+
+/datum/loadout_item/neck/necktie_blue
+ name = "Blue Necktie"
+ item_path = /obj/item/clothing/neck/tie/blue
+
+/datum/loadout_item/neck/necktie_disco
+ name = "Horrific Necktie"
+ item_path = /obj/item/clothing/neck/tie/horrible
+
+/datum/loadout_item/neck/necktie_loose
+ name = "Loose Necktie"
+ item_path = /obj/item/clothing/neck/tie/detective
+
+/datum/loadout_item/neck/necktie_red
+ name = "Red Necktie"
+ item_path = /obj/item/clothing/neck/tie/red
+
+/datum/loadout_item/neck/stethoscope
+ name = "Stethoscope"
+ item_path = /obj/item/clothing/neck/stethoscope
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer","Security Medic")
+
+/datum/loadout_item/neck/choker
+ name = "Choker"
+ item_path = /obj/item/clothing/neck/human_petcollar/choker
+
+/datum/loadout_item/neck/collar
+ name = "Collar"
+ item_path = /obj/item/clothing/neck/human_petcollar
+
+/datum/loadout_item/neck/leathercollar
+ name = "Leather collar"
+ item_path = /obj/item/clothing/neck/human_petcollar/leather
+
+/datum/loadout_item/neck/cbellcollar
+ name = "Cowbell collar"
+ item_path = /obj/item/clothing/neck/human_petcollar/locked/cowcollar
+
+/datum/loadout_item/neck/bellcollar
+ name = "Bell collar"
+ item_path = /obj/item/clothing/neck/human_petcollar/locked/bellcollar
+
+/datum/loadout_item/neck/spikecollar
+ name = "Spike collar"
+ item_path = /obj/item/clothing/neck/human_petcollar/locked/spikecollar
+
+/datum/loadout_item/neck/hcollar
+ name = "Holocollar"
+ item_path = /obj/item/clothing/neck/human_petcollar/locked/holocollar
+
+/datum/loadout_item/neck/ponchocowboy
+ name = "Green cowboy poncho"
+ item_path = /obj/item/clothing/neck/cowboylea
+
+/datum/loadout_item/neck/brownponchocowboy
+ name = "Brown cowboy poncho"
+ item_path = /obj/item/clothing/neck/ponchoranger
+
+/datum/loadout_item/neck/crosscollar
+ name = "Cross collar"
+ item_path = /obj/item/clothing/neck/human_petcollar/locked/cross
+
+/datum/loadout_item/neck/poly_cloak
+ name = "Polychromic Cloak"
+ item_path = /obj/item/clothing/neck/cloak/polychromic
+
+/datum/loadout_item/neck/poly_veil
+ name = "Polychromic Veil"
+ item_path = /obj/item/clothing/neck/cloak/polychromic/veil
+
+/datum/loadout_item/neck/poly_shroud
+ name = "Polychromic Shroud"
+ item_path = /obj/item/clothing/neck/cloak/polychromic/shroud
+
+/datum/loadout_item/neck/poly_boat
+ name = "Polychromic Boatcloak"
+ item_path = /obj/item/clothing/neck/cloak/polychromic/boat
+
+
+/datum/loadout_item/neck/discoproper
+ name = "Horrible Necktie"
+ item_path = /obj/item/clothing/neck/tie/disco
+ restricted_roles = list("Detective")
+
+/datum/loadout_item/neck/mantle
+ name = "Mantle"
+ item_path = /obj/item/clothing/neck/mantle
+
+/datum/loadout_item/neck/mantle_hop
+ name = "Head of Personnel's Mantle"
+ item_path = /obj/item/clothing/neck/mantle/hopmantle
+ restricted_roles = list("Head of Personnel")
+
+/datum/loadout_item/neck/mantle_cmo
+ name = "Chief Medical Officer's Mantle"
+ item_path = /obj/item/clothing/neck/mantle/cmomantle
+ restricted_roles = list("Chief Medical Officer")
+
+/datum/loadout_item/neck/mantle_rd
+ name = "Research Director's Mantle"
+ item_path = /obj/item/clothing/neck/mantle/rdmantle
+ restricted_roles = list("Research Director")
+
+/datum/loadout_item/neck/mantle_ce
+ name = "Chief Engineer's Mantle"
+ item_path = /obj/item/clothing/neck/mantle/cemantle
+ restricted_roles = list("Chief Engineer")
+
+/datum/loadout_item/neck/mantle_hos
+ name = "Head of Security's Mantle"
+ item_path = /obj/item/clothing/neck/mantle/hosmantle
+ restricted_roles = list("Head of Security")
+
+/datum/loadout_item/neck/mantle_bs
+ name = "Blueshield's Mantle"
+ item_path = /obj/item/clothing/neck/mantle/bsmantle
+ restricted_roles = list("Blueshield")
+
+/datum/loadout_item/neck/mantle_cap
+ name = "Captain's Mantle"
+ item_path = /obj/item/clothing/neck/mantle/capmantle
+ restricted_roles = list("Captain")
+
+/datum/loadout_item/neck/kinkycollar
+ name = "Kinky collar"
+ item_path = /obj/item/clothing/neck/kink_collar
+
+//Donator neck items here
+/datum/loadout_item/neck/donator
+ donator_only = TRUE
+
+/datum/loadout_item/neck/donator/rscloak
+ name = "Black Cape"
+ item_path = /obj/item/clothing/neck/cloak/rscloak
+
+/datum/loadout_item/neck/donator/rscloakcross
+ name = "Black Cross Cape"
+ item_path = /obj/item/clothing/neck/cloak/rscloak_cross
+
+/datum/loadout_item/neck/donator/rscloakchampion
+ name = "Champion Cape"
+ item_path = /obj/item/clothing/neck/cloak/rscloak_champion
+
+/datum/loadout_item/neck/donator/rscloak_poly
+ name = "Polychromic Cape"
+ item_path = /obj/item/clothing/neck/cloak/polychromic/rscloak
+
+/datum/loadout_item/neck/donator/rscloakcross_poly
+ name = "Polychromic Cape - Cross"
+ item_path = /obj/item/clothing/neck/cloak/polychromic/rscloak_cross
+
+/datum/loadout_item/neck/donator/mantle/regal
+ name = "Regal Mantle"
+ item_path = /obj/item/clothing/neck/mantle/regal
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_pocket.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_pocket.dm
new file mode 100644
index 00000000000..300c09dac57
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_pocket.dm
@@ -0,0 +1,245 @@
+// --- Loadout item datums for backpack / pocket items ---
+
+/// Pocket items (Moved to backpack)
+GLOBAL_LIST_INIT(loadout_pocket_items, generate_loadout_items(/datum/loadout_item/pocket_items))
+
+/datum/loadout_item/pocket_items
+ category = LOADOUT_ITEM_MISC
+
+// The wallet loadout item is special, and puts the player's ID and other small items into it on initialize (fancy!)
+/datum/loadout_item/pocket_items/wallet
+ name = "Wallet"
+ item_path = /obj/item/storage/wallet
+ additional_tooltip_contents = list("FILLS AUTOMATICALLY - This item will populate itself with your ID card and other small items you may have on spawn.")
+
+// We add our wallet manually, later, so no need to put it in any outfits.
+/datum/loadout_item/pocket_items/wallet/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only)
+ return FALSE
+
+// We didn't spawn any item yet, so nothing to call here.
+/datum/loadout_item/pocket_items/wallet/on_equip_item(datum/preferences/preference_source, mob/living/carbon/human/equipper, visuals_only)
+ return FALSE
+
+// We add our wallet at the very end of character initialization (after quirks, etc) to ensure the backpack / their ID is all set by now.
+/datum/loadout_item/pocket_items/wallet/post_equip_item(datum/preferences/preference_source, mob/living/carbon/human/equipper)
+ var/obj/item/card/id/advanced/id_card = equipper.get_item_by_slot(ITEM_SLOT_ID)
+ if(istype(id_card, /obj/item/storage/wallet))
+ return
+
+ var/obj/item/storage/wallet/wallet = new(equipper)
+ if(istype(id_card))
+ equipper.temporarilyRemoveItemFromInventory(id_card, force = TRUE)
+ equipper.equip_to_slot_if_possible(wallet, ITEM_SLOT_ID, initial = TRUE)
+ id_card.forceMove(wallet)
+
+ if(equipper.back)
+ var/list/backpack_stuff = list()
+ SEND_SIGNAL(equipper.back, COMSIG_TRY_STORAGE_RETURN_INVENTORY, backpack_stuff, FALSE)
+ for(var/obj/item/thing in backpack_stuff)
+ if(wallet.contents.len >= 3)
+ break
+ if(thing.w_class <= WEIGHT_CLASS_SMALL)
+ SEND_SIGNAL(wallet, COMSIG_TRY_STORAGE_INSERT, thing, equipper, TRUE, FALSE)
+ else
+ if(!equipper.equip_to_slot_if_possible(wallet, slot = ITEM_SLOT_BACKPACK, initial = TRUE))
+ wallet.forceMove(equipper.drop_location())
+
+/datum/loadout_item/pocket_items/rag
+ name = "Rag"
+ item_path = /obj/item/reagent_containers/glass/rag
+
+/datum/loadout_item/pocket_items/gum_pack
+ name = "Pack of Gum"
+ item_path = /obj/item/storage/box/gum
+
+/datum/loadout_item/pocket_items/gum_pack_nicotine
+ name = "Pack of Nicotine Gum"
+ item_path = /obj/item/storage/box/gum/nicotine
+
+/datum/loadout_item/pocket_items/gum_pack_hp
+ name = "Pack of HP+ Gum"
+ item_path = /obj/item/storage/box/gum/happiness
+
+/datum/loadout_item/pocket_items/lipstick_black
+ name = "Black Lipstick"
+ item_path = /obj/item/lipstick/black
+
+/datum/loadout_item/pocket_items/lipstick_jade
+ name = "Jade Lipstick"
+ item_path = /obj/item/lipstick/jade
+
+/datum/loadout_item/pocket_items/lipstick_purple
+ name = "Purple Lipstick"
+ item_path = /obj/item/lipstick/purple
+
+/datum/loadout_item/pocket_items/lipstick_red
+ name = "Red Lipstick"
+ item_path = /obj/item/lipstick
+
+/datum/loadout_item/pocket_items/razor
+ name = "Razor"
+ item_path = /obj/item/razor
+
+/datum/loadout_item/pocket_items/lighter
+ name = "Lighter"
+ item_path = /obj/item/lighter
+
+/datum/loadout_item/pocket_items/matches
+ name = "Matchbox"
+ item_path = /obj/item/storage/box/matches
+
+/datum/loadout_item/pocket_items/cheaplighter
+ name = "Cheap lighter"
+ item_path = /obj/item/lighter/greyscale
+
+/datum/loadout_item/pocket_items/zippolighter
+ name = "Zippo Lighter"
+ item_path = /obj/item/lighter
+
+/datum/loadout_item/pocket_items/ttsdevice
+ name = "Text-to-Speech Device"
+ item_path = /obj/item/ttsdevice
+
+
+/datum/loadout_item/pocket_items/paicard
+ name = "Personal AI device"
+ item_path = /obj/item/paicard
+
+
+/datum/loadout_item/pocket_items/cigar
+ name = "Cigar"
+ item_path = /obj/item/clothing/mask/cigarette/cigar
+ //smoking is bad mkay
+
+/datum/loadout_item/pocket_items/cigarettes
+ name = "Cigarette pack"
+ item_path = /obj/item/storage/fancy/cigarettes
+
+/datum/loadout_item/pocket_items/wallet
+ name = "Wallet"
+ item_path = /obj/item/storage/wallet
+
+/datum/loadout_item/pocket_items/flask
+ name = "Flask"
+ item_path = /obj/item/reagent_containers/food/drinks/flask
+
+
+/datum/loadout_item/pocket_items/skub
+ name = "Skub"
+ item_path = /obj/item/skub
+
+/datum/loadout_item/pocket_items/multipen
+ name = "A multicolored pen"
+ item_path = /obj/item/pen/fourcolor
+
+/datum/loadout_item/pocket_items/fountainpen
+ name = "A fancy pen"
+ item_path = /obj/item/pen/fountain
+
+
+/datum/loadout_item/pocket_items/modular_tablet
+ name = "A modular tablet"
+ item_path = /obj/item/modular_computer/tablet/preset/cheap/
+
+
+/datum/loadout_item/pocket_items/modular_laptop
+ name = "A modular laptop"
+ item_path = /obj/item/modular_computer/laptop/preset/civilian
+
+
+/datum/loadout_item/pocket_items/ringbox_gold
+ name = "A gold ring box"
+ item_path = /obj/item/storage/fancy/ringbox
+
+
+/datum/loadout_item/pocket_items/ringbox_silver
+ name = "A silver ring box"
+ item_path = /obj/item/storage/fancy/ringbox/silver
+
+
+/datum/loadout_item/pocket_items/ringbox_diamond
+ name = "A diamond ring box"
+ item_path = /obj/item/storage/fancy/ringbox/diamond
+
+
+/datum/loadout_item/pocket_items/tapeplayer
+ name = "Taperecorder"
+ item_path = /obj/item/taperecorder
+
+/datum/loadout_item/pocket_items/tape
+ name = "Spare cassette tape"
+ item_path = /obj/item/tape/random
+
+/datum/loadout_item/pocket_items/newspaper
+ name = "Newspaper"
+ item_path = /obj/item/newspaper
+
+/datum/loadout_item/pocket_items/hhmirror
+ name = "Handheld Mirror"
+ item_path = /obj/item/hhmirror
+
+
+
+/datum/loadout_item/pocket_items/cross
+ name = "Ornate Cross"
+ item_path = /obj/item/crucifix
+ restricted_roles = list("Chaplain")
+
+/datum/loadout_item/pocket_items/fragrance_cologne
+ name = "Cologne Bottle"
+ item_path = /obj/item/perfume/cologne
+
+/datum/loadout_item/pocket_items/fragrance_wood
+ name = "Wood Perfume"
+ item_path = /obj/item/perfume/wood
+
+/datum/loadout_item/pocket_items/fragrance_rose
+ name = "Rose Perfume"
+ item_path = /obj/item/perfume/rose
+
+/datum/loadout_item/pocket_items/fragrance_jasmine
+ name = "Jasmine Perfume"
+ item_path = /obj/item/perfume/jasmine
+
+/datum/loadout_item/pocket_items/fragrance_mint
+ name = "Mint Perfume"
+ item_path = /obj/item/perfume/mint
+
+/datum/loadout_item/pocket_items/fragrance_vanilla
+ name = "Vanilla Perfume"
+ item_path = /obj/item/perfume/vanilla
+
+/datum/loadout_item/pocket_items/fragrance_pear
+ name = "Pear Perfume"
+ item_path = /obj/item/perfume/pear
+
+/datum/loadout_item/pocket_items/fragrance_strawberry
+ name = "Strawberry Perfume"
+ item_path = /obj/item/perfume/strawberry
+
+/datum/loadout_item/pocket_items/fragrance_cherry
+ name = "Cherry Perfume"
+ item_path = /obj/item/perfume/cherry
+
+/datum/loadout_item/pocket_items/fragrance_amber
+ name = "Amber Perfume"
+ item_path = /obj/item/perfume/amber
+
+//Donator stuffs down here
+/datum/loadout_item/pocket_items/donator
+ donator_only = TRUE
+
+/datum/loadout_item/pocket_items/donator/coin
+ name = "Iron Coin"
+ item_path = /obj/item/coin/iron
+ donator_only = TRUE
+
+/datum/loadout_item/pocket_items/donator/havana_cigar_case
+ name = "Havanian Cigars"
+ item_path = /obj/item/storage/fancy/cigarettes/cigars/havana
+ donator_only = TRUE
+
+/datum/loadout_item/pocket_items/donator/vape
+ name = "E-Cigarette"
+ item_path = /obj/item/clothing/mask/vape
+ donator_only = TRUE
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_shoes.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_shoes.dm
new file mode 100644
index 00000000000..c78c3ede34a
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_shoes.dm
@@ -0,0 +1,169 @@
+// --- Loadout item datums for shoes items ---
+
+/// Shoe Slot Items (Deletes overrided items)
+GLOBAL_LIST_INIT(loadout_shoes, generate_loadout_items(/datum/loadout_item/shoes))
+
+/datum/loadout_item/shoes
+ category = LOADOUT_ITEM_SHOES
+
+/datum/loadout_item/shoes/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ outfit.shoes = item_path
+
+/datum/loadout_item/shoes/jackboots
+ name = "Jackboots"
+ item_path = /obj/item/clothing/shoes/jackboots
+
+// Thedragmeme's donator reward, they've decided to make them available to everybody.
+/datum/loadout_item/shoes/jackboots/heel
+ name = "High-heel Jackboots"
+ item_path = /obj/item/clothing/shoes/jackboots/heel
+
+/datum/loadout_item/shoes/junhle
+ name = "Brown Jackboots"
+ item_path = /obj/item/clothing/shoes/jungleboots
+
+/datum/loadout_item/shoes/thighboot
+ name = "Thigh Boots"
+ item_path = /obj/item/clothing/shoes/jackboots/thigh
+
+/datum/loadout_item/shoes/kneeboot
+ name = "Knee Boots"
+ item_path = /obj/item/clothing/shoes/jackboots/knee
+
+/datum/loadout_item/shoes/timbs
+ name = "Fashionable Boots"
+ item_path = /obj/item/clothing/shoes/jackboots/timbs
+
+/datum/loadout_item/shoes/winter_boots
+ name = "Winter Boots"
+ item_path = /obj/item/clothing/shoes/winterboots
+
+/datum/loadout_item/shoes/work_boots
+ name = "Work Boots"
+ item_path = /obj/item/clothing/shoes/workboots
+
+/datum/loadout_item/shoes/mining_boots
+ name = "Mining Boots"
+ item_path = /obj/item/clothing/shoes/workboots/mining
+
+/datum/loadout_item/shoes/laceup
+ name = "Laceup Shoes"
+ item_path = /obj/item/clothing/shoes/laceup
+
+/datum/loadout_item/shoes/russian_boots
+ name = "Russian Boots"
+ item_path = /obj/item/clothing/shoes/russian
+
+/datum/loadout_item/shoes/black_cowboy_boots
+ name = "Black Cowboy Boots"
+ item_path = /obj/item/clothing/shoes/cowboy/black
+
+/datum/loadout_item/shoes/brown_cowboy_boots
+ name = "Brown Cowboy Boots"
+ item_path = /obj/item/clothing/shoes/cowboy
+
+/datum/loadout_item/shoes/white_cowboy_boots
+ name = "White Cowboy Boots"
+ item_path = /obj/item/clothing/shoes/cowboy/white
+
+/datum/loadout_item/shoes/greyscale_sneakers
+ name = "Greyscale Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers
+ can_be_greyscale = TRUE
+
+/datum/loadout_item/shoes/black_sneakers
+ name = "Black Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers/black
+
+/datum/loadout_item/shoes/blue_sneakers
+ name = "Blue Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers/blue
+
+/datum/loadout_item/shoes/brown_sneakers
+ name = "Brown Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers/brown
+
+/datum/loadout_item/shoes/green_sneakers
+ name = "Green Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers/green
+
+/datum/loadout_item/shoes/purple_sneakers
+ name = "Purple Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers/purple
+
+/datum/loadout_item/shoes/orange_sneakers
+ name = "Orange Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers/orange
+
+/datum/loadout_item/shoes/rainbow_sneakers
+ name = "Rainbow Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers/rainbow
+
+/datum/loadout_item/shoes/yellow_sneakers
+ name = "Yellow Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers/yellow
+
+/datum/loadout_item/shoes/white_sneakers
+ name = "White Sneakers"
+ item_path = /obj/item/clothing/shoes/sneakers/white
+
+/datum/loadout_item/shoes/sandals
+ name = "Sandals"
+ item_path = /obj/item/clothing/shoes/sandal
+
+/datum/loadout_item/shoes/sportshoes
+ name = "Sport Shoes"
+ item_path = /obj/item/clothing/shoes/sports
+
+/datum/loadout_item/shoes/gildedcuffs
+ name = "Gilded leg wraps"
+ item_path = /obj/item/clothing/shoes/wraps
+
+/datum/loadout_item/shoes/silvercuffs
+ name = "Silver leg wraps"
+ item_path = /obj/item/clothing/shoes/wraps/silver
+
+/datum/loadout_item/shoes/redcuffs
+ name = "Red leg wraps"
+ item_path = /obj/item/clothing/shoes/wraps/red
+
+/datum/loadout_item/shoes/bluecuffs
+ name = "Blue leg wraps"
+ item_path = /obj/item/clothing/shoes/wraps/blue
+
+/datum/loadout_item/shoes/clothwrap
+ name = "Colourable Cloth Wraps"
+ item_path = /obj/item/clothing/shoes/wraps/poly
+
+/datum/loadout_item/shoes/high_heels
+ name = "High Heels"
+ item_path = /obj/item/clothing/shoes/high_heels
+
+/datum/loadout_item/shoes/cowboyboots
+ name = "Cowboy Boots, Brown"
+ item_path = /obj/item/clothing/shoes/cowboyboots
+
+/datum/loadout_item/shoes/cowboyboots_black
+ name = "Cowboy Boots, Black"
+ item_path = /obj/item/clothing/shoes/cowboyboots/black
+
+/datum/loadout_item/shoes/disco
+ name = "Green Snakeskin Shoes"
+ item_path = /obj/item/clothing/shoes/discoshoes
+
+//shoes
+/datum/loadout_item/shoes/dominaheels
+ name = "Dominant heels"
+ item_path = /obj/item/clothing/shoes/dominaheels
+
+//Donator shoes here
+/datum/loadout_item/shoes/donator
+ donator_only = TRUE
+
+/datum/loadout_item/shoes/donator/blackjackboots
+ name = "Black Jackboots"
+ item_path = /obj/item/clothing/shoes/jackboots/black
+
+/datum/loadout_item/shoes/donator/rainbow
+ name = "Rainbow Converse"
+ item_path = /obj/item/clothing/shoes/sneakers/rainbow
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_suit.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_suit.dm
new file mode 100644
index 00000000000..479a9e1e9e3
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_suit.dm
@@ -0,0 +1,519 @@
+// --- Loadout item datums for exosuits / suits ---
+
+/// Exosuit / Outersuit Slot Items (Deletes overrided items)
+GLOBAL_LIST_INIT(loadout_exosuits, generate_loadout_items(/datum/loadout_item/suit))
+
+/datum/loadout_item/suit
+ category = LOADOUT_ITEM_SUIT
+
+/datum/loadout_item/suit/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ outfit.suit = item_path
+ if(outfit.suit_store)
+ LAZYADD(outfit.backpack_contents, outfit.suit_store)
+ outfit.suit_store = null
+
+/datum/loadout_item/suit/winter_coat
+ name = "Winter Coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat
+
+/datum/loadout_item/suit/winter_coat_greyscale
+ name = "Greyscale Winter Coat"
+ can_be_greyscale = TRUE
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/custom
+
+/datum/loadout_item/suit/denim_overalls
+ name = "Denim Overalls"
+ item_path = /obj/item/clothing/suit/apron/overalls
+
+/datum/loadout_item/suit/black_suit_jacket
+ name = "Black Suit Jacket"
+ item_path = /obj/item/clothing/suit/toggle/lawyer/black
+
+/datum/loadout_item/suit/blue_suit_jacket
+ name = "Blue Suit Jacket"
+ item_path = /obj/item/clothing/suit/toggle/lawyer
+
+/datum/loadout_item/suit/purple_suit_jacket
+ name = "Purple Suit Jacket"
+ item_path = /obj/item/clothing/suit/toggle/lawyer/purple
+
+/datum/loadout_item/suit/purple_apron
+ name = "Purple Apron"
+ item_path = /obj/item/clothing/suit/apron/purple_bartender
+
+
+/datum/loadout_item/suit/Suspenders_blue
+ name = "Blue Suspenders"
+ item_path = /obj/item/clothing/suit/toggle/suspenders/blue
+
+/datum/loadout_item/suit/suspenders_grey
+ name = "Grey Suspenders"
+ item_path = /obj/item/clothing/suit/toggle/suspenders/gray
+
+/datum/loadout_item/suit/suspenders_red
+ name = "Red Suspenders"
+ item_path = /obj/item/clothing/suit/toggle/suspenders
+
+/datum/loadout_item/suit/white_dress
+ name = "White Dress"
+ item_path = /obj/item/clothing/suit/whitedress
+
+/datum/loadout_item/suit/labcoat
+ name = "Labcoat"
+ item_path = /obj/item/clothing/suit/toggle/labcoat
+
+/datum/loadout_item/suit/labcoat_green
+ name = "Green Labcoat"
+ item_path = /obj/item/clothing/suit/toggle/labcoat/mad
+
+/datum/loadout_item/suit/poncho
+ name = "Poncho"
+ item_path = /obj/item/clothing/suit/poncho
+
+/datum/loadout_item/suit/poncho_green
+ name = "Green Poncho"
+ item_path = /obj/item/clothing/suit/poncho/green
+
+/datum/loadout_item/suit/poncho_red
+ name = "Red Poncho"
+ item_path = /obj/item/clothing/suit/poncho/red
+
+/datum/loadout_item/suit/wawaiian_shirt
+ name = "Hawaiian Shirt"
+ item_path = /obj/item/clothing/suit/hawaiian
+
+/datum/loadout_item/suit/bomber_jacket
+ name = "Bomber Jacket"
+ item_path = /obj/item/clothing/suit/jacket
+
+/datum/loadout_item/suit/military_jacket
+ name = "Military Jacket"
+ item_path = /obj/item/clothing/suit/jacket/miljacket
+
+/datum/loadout_item/suit/puffer_jacket
+ name = "Puffer Jacket"
+ item_path = /obj/item/clothing/suit/jacket/puffer
+
+/datum/loadout_item/suit/puffer_vest
+ name = "Puffer Vest"
+ item_path = /obj/item/clothing/suit/jacket/puffer/vest
+
+/datum/loadout_item/suit/leather_jacket
+ name = "Leather Jacket"
+ item_path = /obj/item/clothing/suit/jacket/leather
+
+/datum/loadout_item/suit/leather_coat
+ name = "Leather Coat"
+ item_path = /obj/item/clothing/suit/jacket/leather/overcoat
+
+/datum/loadout_item/suit/brown_letterman
+ name = "Brown Letterman"
+ item_path = /obj/item/clothing/suit/jacket/letterman
+
+/datum/loadout_item/suit/red_letterman
+ name = "Red Letterman"
+ item_path = /obj/item/clothing/suit/jacket/letterman_red
+
+/datum/loadout_item/suit/blue_letterman
+ name = "Blue Letterman"
+ item_path = /obj/item/clothing/suit/jacket/letterman_nanotrasen
+
+/datum/loadout_item/suit/bee
+ name = "Bee Outfit"
+ item_path = /obj/item/clothing/suit/hooded/bee_costume
+
+/datum/loadout_item/suit/plague_doctor
+ name = "Plague Doctor Suit"
+ item_path = /obj/item/clothing/suit/bio_suit/plaguedoctorsuit
+
+//MISC
+/datum/loadout_item/suit/poncho
+ name = "Poncho"
+ item_path = /obj/item/clothing/suit/poncho
+
+/datum/loadout_item/suit/ponchogreen
+ name = "Green poncho"
+ item_path = /obj/item/clothing/suit/poncho/green
+
+/datum/loadout_item/suit/ponchored
+ name = "Red poncho"
+ item_path = /obj/item/clothing/suit/poncho/red
+
+/datum/loadout_item/suit/redhood
+ name = "Red cloak"
+ item_path = /obj/item/clothing/suit/hooded/cloak/david
+
+/datum/loadout_item/suit/ianshirt
+ name = "Ian Shirt"
+ item_path = /obj/item/clothing/suit/ianshirt
+
+/datum/loadout_item/suit/wornshirt
+ name = "Worn Shirt"
+ item_path = /obj/item/clothing/suit/wornshirt
+
+/datum/loadout_item/suit/tailcoat
+ name = "Tailcoat"
+ item_path = /obj/item/clothing/suit/costume/tailcoat
+
+/datum/loadout_item/suit/duster
+ name = "Colorable Duster"
+ item_path = /obj/item/clothing/suit/duster/colorable
+
+/datum/loadout_item/suit/peacoat
+ name = "Colorable Peacoat"
+ item_path = /obj/item/clothing/suit/toggle/peacoat
+
+/datum/loadout_item/suit/dresscoat
+ name = "Black Dresscoat"
+ item_path = /obj/item/clothing/suit/costume/vic_dresscoat
+
+/datum/loadout_item/suit/dresscoat_red
+ name = "Red Dresscoat"
+ item_path = /obj/item/clothing/suit/costume/vic_dresscoat/red
+
+/datum/loadout_item/suit/trackjacket
+ name = "Track Jacket"
+ item_path = /obj/item/clothing/suit/toggle/trackjacket
+
+/*Flannels go inside Misc*/
+/datum/loadout_item/suit/flannel_black
+ name = "Black Flannel"
+ item_path = /obj/item/clothing/suit/toggle/jacket/flannel
+
+/datum/loadout_item/suit/flannel_red
+ name = "Red Flannel"
+ item_path = /obj/item/clothing/suit/toggle/jacket/flannel/red
+
+/datum/loadout_item/suit/flannel_aqua
+ name = "Aqua Flannel"
+ item_path = /obj/item/clothing/suit/toggle/jacket/flannel/aqua
+
+/datum/loadout_item/suit/flannel_brown
+ name = "Brown Flannel"
+ item_path = /obj/item/clothing/suit/toggle/jacket/flannel/brown
+
+/*Hawaiian Shirts*/
+/datum/loadout_item/suit/hawaiian_blue
+ name = "Blue Hawaiian Shirt"
+ item_path = /obj/item/clothing/suit/hawaiian_blue
+
+/datum/loadout_item/suit/hawaiian_orange
+ name = "Orange Hawaiian Shirt"
+ item_path = /obj/item/clothing/suit/hawaiian_orange
+
+/datum/loadout_item/suit/hawaiian_purple
+ name = "Purple Hawaiian Shirt"
+ item_path = /obj/item/clothing/suit/hawaiian_purple
+
+/datum/loadout_item/suit/hawaiian_green
+ name = "Green Hawaiian Shirt"
+ item_path = /obj/item/clothing/suit/hawaiian_green
+
+/datum/loadout_item/suit/frenchtrench
+ name = "Blue Trenchcoat"
+ item_path = /obj/item/clothing/suit/frenchtrench
+
+/datum/loadout_item/suit/cossak
+ name = "Ukrainian Coat"
+ item_path = /obj/item/clothing/suit/cossack
+
+//COATS
+
+/datum/loadout_item/suit/aformal
+ name = "Assistant's formal winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/aformal
+
+/datum/loadout_item/suit/runed
+ name = "Runed winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/narsie/fake
+
+/datum/loadout_item/suit/brass
+ name = "Brass winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/ratvar/fake
+
+/datum/loadout_item/suit/korea
+ name = "Eastern winter coat"
+ item_path = /obj/item/clothing/suit/koreacoat
+
+/datum/loadout_item/suit/czech
+ name = "Modern winter coat"
+ item_path = /obj/item/clothing/suit/modernwintercoatthing
+
+/datum/loadout_item/suit/parka
+ name = "Falls Parka"
+ item_path = /obj/item/clothing/suit/fallsparka
+
+/datum/loadout_item/suit/poly
+ name = "Polychromic Wintercoat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/polychromic
+
+/datum/loadout_item/suit/urban
+ name = "Urban Coat"
+ item_path = /obj/item/clothing/suit/urban/polychromic
+
+/datum/loadout_item/suit/maxson
+ name = "Fancy Brown Coat"
+ item_path = /obj/item/clothing/suit/brownbattlecoat
+
+
+/datum/loadout_item/suit/bossu
+ name = "Fancy Black Coat"
+ item_path = /obj/item/clothing/suit/blackfurrich
+
+/datum/loadout_item/suit/dutchjacket
+ name = "Dutch Jacket"
+ item_path = /obj/item/clothing/suit/dutchjacketsr
+
+/datum/loadout_item/suit/caretaker
+ name = "Caretaker Jacket"
+ item_path = /obj/item/clothing/suit/victoriantailcoatbutler
+
+/datum/loadout_item/suit/yakuzajacket
+ name = "Asian Jacket"
+ item_path = /obj/item/clothing/suit/yakuza
+
+/datum/loadout_item/suit/suitblackbetter
+ name = "Light Black Suit Jacket"
+ item_path = /obj/item/clothing/suit/toggle/lawyer/black/better
+
+/datum/loadout_item/suit/suitwhite
+ name = "White Suit Jacket"
+ item_path = /obj/item/clothing/suit/texas
+
+/datum/loadout_item/suit/jacketbomber_alt
+ name = "Bomber Jacket w/ Zipper"
+ item_path = /obj/item/clothing/suit/toggle/jacket
+
+/datum/loadout_item/suit/polyjacketleather
+ name = "Colorable leather jacket"
+ item_path = /obj/item/clothing/suit/jacket/leather/polychromic
+
+/datum/loadout_item/suit/woolcoat
+ name = "Leather overcoat"
+ item_path = /obj/item/clothing/suit/woolcoat
+
+/datum/loadout_item/suit/jacketpuffer
+ name = "Puffer jacket"
+ item_path = /obj/item/clothing/suit/jacket/puffer
+
+/datum/loadout_item/suit/flakjack
+ name = "Flak Jacket"
+ item_path = /obj/item/clothing/suit/flakjack
+
+
+//HOODIES
+
+/datum/loadout_item/suit/hoodie/grey
+ name = "Grey Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie
+
+/datum/loadout_item/suit/black
+ name = "Black Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/black
+
+/datum/loadout_item/suit/red
+ name = "Red Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/red
+
+/datum/loadout_item/suit/blue
+ name = "Blue Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/blue
+
+/datum/loadout_item/suit/green
+ name = "Green Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/green
+
+/datum/loadout_item/suit/orange
+ name = "orange hoodies"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/orange
+
+/datum/loadout_item/suit/yellow
+ name = "Yellow Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/yellow
+
+/datum/loadout_item/suit/white
+ name = "White Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/white
+
+/datum/loadout_item/suit/cti
+ name = "CTI Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/cti
+
+/datum/loadout_item/suit/mu
+ name = "Mojave University Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/mu
+
+/datum/loadout_item/suit/nt
+ name = "NT Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/nt
+
+/datum/loadout_item/suit/smw
+ name = "SMW Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/smw
+
+/datum/loadout_item/suit/nrti
+ name = "NRTI Hoodie"
+ item_path = /obj/item/clothing/suit/storage/toggle/hoodie/nrti
+
+//JOB RELATED
+
+/datum/loadout_item/suit/coat_med
+ name = "Medical winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/medical
+ restricted_roles = list("Chief Medical Officer", "Medical Doctor") // Reserve it to Medical Doctors and their boss, the Chief Medical Officer
+
+/datum/loadout_item/suit/coat_paramedic
+ name = "Paramedic winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/paramedic
+ restricted_roles = list("Chief Medical Officer", "Paramedic") // Reserve it to Paramedics and their boss, the Chief Medical Officer
+
+/datum/loadout_item/suit/coat_robotics
+ name = "Robotics winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/robotics
+ restricted_roles = list("Research Director", "Roboticist")
+
+/datum/loadout_item/suit/coat_sci
+ name = "Science winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/science
+ restricted_roles = list("Research Director", "Scientist", "Roboticist", "Vanguard Operative") // Reserve it to the Science Departement
+
+/datum/loadout_item/suit/coat_eng
+ name = "Engineering winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/engineering
+ restricted_roles = list("Chief Engineer", "Station Engineer") // Reserve it to Station Engineers and their boss, the Chief Engineer
+
+/datum/loadout_item/suit/coat_atmos
+ name = "Atmospherics winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/engineering/atmos
+ restricted_roles = list("Chief Engineer", "Atmospheric Technician") // Reserve it to Atmos Techs and their boss, the Chief Engineer
+
+/datum/loadout_item/suit/coat_hydro
+ name = "Hydroponics winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/hydro
+ restricted_roles = list("Head of Personnel", "Botanist") // Reserve it to Botanists and their boss, the Head of Personnel
+
+/datum/loadout_item/suit/coat_bar
+ name = "Bartender winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/bartender
+ restricted_roles = list("Head of Personnel", "Bartender") //Reserved for Bartenders and their head-of-staff
+
+/datum/loadout_item/suit/coat_cargo
+ name = "Cargo winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/cargo
+ restricted_roles = list("Quartermaster", "Cargo Technician") // Reserve it to Cargo Techs and their boss, the Quartermaster
+
+/datum/loadout_item/suit/coat_miner
+ name = "Mining winter coat"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/miner
+ restricted_roles = list("Quartermaster", "Shaft Miner") // Reserve it to Miners and their boss, the Quartermaster
+
+/datum/loadout_item/suit/navybluejacketofficer
+ name = "security officer's navyblue jacket"
+ item_path = /obj/item/clothing/suit/armor/navyblue
+ restricted_roles = list("Security Officer","Security Medic","Security Sergeant","Head of Security", "Warden") // I aint making a medic one, maybe i'll add some rank thing from cm or civ for it
+
+/datum/loadout_item/suit/navybluejacketwarden
+ name = "warden navyblue jacket"
+ item_path = /obj/item/clothing/suit/armor/vest/warden/navyblue
+ restricted_roles = list("Warden")
+
+/datum/loadout_item/suit/security_jacket
+ name = "Security Jacket"
+ item_path = /obj/item/clothing/suit/toggle/jacket/sec
+ restricted_roles = list("Head of Security", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant") //Not giving this one to CDOs or COs because it's actually better than the one they spawn with
+/datum/loadout_item/suit/cossak
+ name = "Ukrainian Security Jacket"
+ item_path = /obj/item/clothing/suit/cossack/sec
+ restricted_roles = list("Head of Security", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Corrections Officer")
+
+/datum/loadout_item/suit/brit
+ name = "High Vis Armored Vest"
+ item_path = /obj/item/clothing/suit/toggle/brit/sec
+ restricted_roles = list("Head of Security", "Security Officer", "Warden", "Detective", "Security Medic", "Security Sergeant", "Civil Disputes Officer", "Corrections Officer")
+/datum/loadout_item/suit/british_jacket
+ name = "Peacekeeper Officer Coat"
+ item_path = /obj/item/clothing/suit/british_officer
+ restricted_roles = list("Head of Security", "Warden","Detective","Security Sergeant")
+
+/datum/loadout_item/suit/engi_jacket
+ name = "Engineering Jacket"
+ item_path = /obj/item/clothing/suit/toggle/jacket/engi
+ restricted_roles = list("Chief Engineer", "Station Engineer", "Atmospheric Technician")
+
+/datum/loadout_item/suit/sci_jacket
+ name = "Science Jacket"
+ item_path = /obj/item/clothing/suit/toggle/jacket/sci
+ restricted_roles = list("Research Director", "Scientist", "Roboticist", "Geneticist", "Vanguard Operative")
+
+/datum/loadout_item/suit/med_jacket
+ name = "Medbay Jacket"
+ item_path = /obj/item/clothing/suit/toggle/jacket/med
+ restricted_roles = list("Chief Medical Officer", "Medical Doctor", "Paramedic", "Chemist", "Virologist")
+
+/datum/loadout_item/suit/supply_jacket
+ name = "Supply Jacket"
+ item_path = /obj/item/clothing/suit/toggle/jacket/supply
+ restricted_roles = list("Quartermaster", "Cargo Technician", "Miner")
+
+/datum/loadout_item/suit/supply_gorka_jacket
+ name = "Supply Gorka Jacket"
+ item_path = /obj/item/clothing/suit/gorka/supply
+ restricted_roles = list("Quartermaster", "Cargo Technician", "Miner")
+
+
+/datum/loadout_item/suit/labcoat_parared
+ name = "Red Paramedic Labcoat"
+ item_path = /obj/item/clothing/suit/toggle/labcoat/para_red
+ restricted_roles = list("Chief Medical Officer", "Paramedic","Security Medic") // its a medic jacket anyway screw you
+
+/datum/loadout_item/suit/labcoat_highvis
+ name = "High-Vis Labcoat"
+ item_path = /obj/item/clothing/suit/toggle/labcoat/highvis
+ restricted_roles = list("Chief Medical Officer", "Paramedic", "Atmospheric Technician", "Detective", "Security Medic") // why does the atmos get this? sec med is more of a first responder lmao
+
+/datum/loadout_item/suit/discojacket
+ name = "Disco Ass Blazer"
+ item_path = /obj/item/clothing/suit/discoblazer
+ restricted_roles = list("Detective")
+
+/datum/loadout_item/suit/deckard
+ name = "Runner Coat"
+ item_path = /obj/item/clothing/suit/toggle/deckard
+ restricted_roles = list("Detective")
+
+/datum/loadout_item/suit/bltrench
+ name = "Black Trenchcoat"
+ item_path = /obj/item/clothing/suit/trenchblack
+
+
+/datum/loadout_item/suit/brtrench
+ name = "Brown Trenchcoat"
+ item_path = /obj/item/clothing/suit/trenchbrown
+
+//Donator sutis here
+/datum/loadout_item/suit/donator
+ donator_only = TRUE
+
+/datum/loadout_item/suit/donator/furredjacket
+ name = "Furred Jacket"
+ item_path = /obj/item/clothing/suit/brownfurrich/public
+
+/datum/loadout_item/suit/donator/whitefurredjacket
+ name = "White Furred Jacket"
+ item_path = /obj/item/clothing/suit/brownfurrich/white
+
+/datum/loadout_item/suit/donator/creamfurredjacket
+ name = "Cream Furred Jacket"
+ item_path = /obj/item/clothing/suit/brownfurrich/cream
+
+/datum/loadout_item/suit/donator/british_officer
+ name = "British Officers Coat"
+ item_path = /obj/item/clothing/suit/british_officer
+
+/datum/loadout_item/suit/donator/british_officer
+ name = "Modern Winter Coat"
+ item_path = /obj/item/clothing/suit/modern_winter
+
+/datum/loadout_item/suit/donator/blondie
+ name = "Cowboy Vest"
+ item_path = /obj/item/clothing/suit/cowboyvest
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_toys.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_toys.dm
new file mode 100644
index 00000000000..30ee9ec379d
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_toys.dm
@@ -0,0 +1,286 @@
+GLOBAL_LIST_INIT(loadout_toys, generate_loadout_items(/datum/loadout_item/toys))
+
+/datum/loadout_item/toys
+ category = LOADOUT_ITEM_TOYS
+ can_be_named = TRUE
+
+/datum/loadout_item/toys/bee
+ name = "Bee Plush"
+ item_path = /obj/item/toy/plush/beeplushie
+
+/datum/loadout_item/toys/carp
+ name = "Carp Plush"
+ item_path = /obj/item/toy/plush/carpplushie
+
+/datum/loadout_item/toys/lizard_greyscale
+ name = "Greyscale Lizard Plush"
+ can_be_greyscale = TRUE
+ item_path = /obj/item/toy/plush/lizard_plushie
+
+/datum/loadout_item/toys/lizard_random
+ name = "Random Lizard Plush"
+ item_path = /obj/item/toy/plush/lizard_plushie
+ additional_tooltip_contents = list(TOOLTIP_RANDOM_COLOR)
+
+/datum/loadout_item/toys/moth
+ name = "Moth Plush"
+ item_path = /obj/item/toy/plush/moth
+
+/datum/loadout_item/toys/narsie
+ name = "Nar'sie Plush"
+ item_path = /obj/item/toy/plush/narplush
+ restricted_roles = list("Chaplain")
+
+/datum/loadout_item/toys/nukie
+ name = "Nukie Plush"
+ item_path = /obj/item/toy/plush/nukeplushie
+
+/datum/loadout_item/toys/peacekeeper
+ name = "Peacekeeper Plush"
+ item_path = /obj/item/toy/plush/pkplush
+
+/datum/loadout_item/toys/plasmaman
+ name = "Plasmaman Plush"
+ item_path = /obj/item/toy/plush/plasmamanplushie
+
+/datum/loadout_item/toys/ratvar
+ name = "Ratvar Plush"
+ item_path = /obj/item/toy/plush/ratplush
+ restricted_roles = list("Chaplain")
+
+/datum/loadout_item/toys/rouny
+ name = "Rouny Plush"
+ item_path = /obj/item/toy/plush/rouny
+
+/datum/loadout_item/toys/snake
+ name = "Snake Plush"
+ item_path = /obj/item/toy/plush/snakeplushie
+
+
+/datum/loadout_item/toys/slime
+ name = "Slime plushie"
+ item_path = /obj/item/toy/plush/slimeplushie
+
+/datum/loadout_item/toys/bubble
+ name = "Bubblegum plushie"
+ item_path = /obj/item/toy/plush/bubbleplush
+
+/datum/loadout_item/toys/goat
+ name = "Strange Goat plushie"
+ item_path = /obj/item/toy/plush/goatplushie
+
+/datum/loadout_item/toys/sechound
+ name = "Sechound plushie"
+ item_path = /obj/item/toy/plush/sechound
+
+/datum/loadout_item/toys/medihound
+ name = "Medihound plushie"
+ item_path = /obj/item/toy/plush/medihound
+
+/datum/loadout_item/toys/scrubpuppy
+ name = "Scrubpuppy plushie"
+ item_path = /obj/item/toy/plush/scrubpuppy
+
+/datum/loadout_item/toys/meddrake
+ name = "MediDrake Plushie"
+ item_path = /obj/item/toy/plush/meddrake
+
+/datum/loadout_item/toys/secdrake
+ name = "SecDrake Plushie"
+ item_path = /obj/item/toy/plush/secdrake
+
+/datum/loadout_item/toys/borbplushie
+ name = "Borb plushie"
+ item_path = /obj/item/toy/plush/borbplushie
+
+/datum/loadout_item/toys/deer
+ name = "Deer plushie"
+ item_path = /obj/item/toy/plush/deer
+
+/datum/loadout_item/toys/fermis
+ name = "Medcat plushie"
+ item_path = /obj/item/toy/plush/fermis
+
+/datum/loadout_item/toys/chen
+ name = "Securicat plushie"
+ item_path = /obj/item/toy/plush/fermis/chen
+
+/datum/loadout_item/toys/fox
+ name = "Fox plushie"
+ item_path = /obj/item/toy/plush/fox
+
+/datum/loadout_item/toys/duffmoff
+ name = "Suspicious moth plushie"
+ item_path = /obj/item/toy/plush/duffmoth
+
+/datum/loadout_item/toys/musicalduffy
+ name = "Suspicious musical moth"
+ item_path = /obj/item/instrument/musicalduffy
+
+/datum/loadout_item/toys/leaplush
+ name = "Suspicious deer plushie"
+ item_path = /obj/item/toy/plush/leaplush
+
+/datum/loadout_item/toys/sarmie
+ name = "Cosplayer plushie"
+ item_path = /obj/item/toy/plush/sarmieplush
+
+/datum/loadout_item/toys/arcplush
+ name = "Familiar Lizard plushie"
+ item_path = /obj/item/toy/plush/arcplush
+
+/datum/loadout_item/toys/sharknet
+ name = "Gluttonous Shark plushie"
+ item_path = /obj/item/toy/plush/sharknet
+
+/datum/loadout_item/toys/pintaplush
+ name = "Smaller Deer plushie"
+ item_path = /obj/item/toy/plush/pintaplush
+
+/datum/loadout_item/toys/oleplush
+ name = "Irritable Goat plushie"
+ item_path = /obj/item/toy/plush/oleplush
+
+/datum/loadout_item/toys/szaplush
+ name = "Suspicious spider plushie"
+ item_path = /obj/item/toy/plush/szaplush
+
+/datum/loadout_item/toys/riffplush
+ name = "Valid plushie"
+ item_path = /obj/item/toy/plush/riffplush
+
+/datum/loadout_item/toys/ianbastardman
+ name = "Ian plushie"
+ item_path = /obj/item/toy/plush/ian
+
+/datum/loadout_item/toys/corgiman
+ name = "Corgi plushie"
+ item_path = /obj/item/toy/plush/ian/small
+
+/datum/loadout_item/toys/corgiwoman
+ name = "Girly Corgi plushie"
+ item_path = /obj/item/toy/plush/ian/lisa
+
+/datum/loadout_item/toys/cat
+ name = "Cat plushie"
+ item_path = /obj/item/toy/plush/cat
+
+/datum/loadout_item/toys/tuxcat
+ name = "Tux Cat plushie"
+ item_path = /obj/item/toy/plush/cat/tux
+
+/datum/loadout_item/toys/whitecat
+ name = "White Cat plushie"
+ item_path = /obj/item/toy/plush/cat/white
+
+/datum/loadout_item/toys/seaduplush
+ name = "Sneed plushie"
+ item_path = /obj/item/toy/plush/seaduplush
+
+/datum/loadout_item/toys/card_binder
+ name = "Card Binder"
+ item_path = /obj/item/storage/card_binder
+
+/datum/loadout_item/toys/card_deck
+ name = "Playing Card Deck"
+ item_path = /obj/item/toy/cards/deck
+
+/datum/loadout_item/toys/kotahi_deck
+ name = "Kotahi Deck"
+ item_path = /obj/item/toy/cards/deck/kotahi
+
+/datum/loadout_item/toys/wizoff_deck
+ name = "Wizoff Deck"
+ item_path = /obj/item/toy/cards/deck/wizoff
+
+/datum/loadout_item/toys/d1
+ name = "D1"
+ item_path = /obj/item/dice/d1
+
+/datum/loadout_item/toys/d2
+ name = "D2"
+ item_path = /obj/item/dice/d2
+
+/datum/loadout_item/toys/d4
+ name = "D4"
+ item_path = /obj/item/dice/d4
+
+/datum/loadout_item/toys/d6
+ name = "D6"
+ item_path = /obj/item/dice/d6
+
+/datum/loadout_item/toys/d6_ebony
+ name = "D6 (Ebony)"
+ item_path = /obj/item/dice/d6/ebony
+
+/datum/loadout_item/toys/d6_space
+ name = "D6 (Space)"
+ item_path = /obj/item/dice/d6/space
+
+/datum/loadout_item/toys/d8
+ name = "D8"
+ item_path = /obj/item/dice/d8
+
+/datum/loadout_item/toys/d10
+ name = "D10"
+ item_path = /obj/item/dice/d10
+
+/datum/loadout_item/toys/d12
+ name = "D12"
+ item_path = /obj/item/dice/d12
+
+/datum/loadout_item/toys/d20
+ name = "D20"
+ item_path = /obj/item/dice/d20
+
+/datum/loadout_item/toys/d100
+ name = "D100"
+ item_path = /obj/item/dice/d100
+
+/datum/loadout_item/toys/d00
+ name = "D00"
+ item_path = /obj/item/dice/d00
+
+/datum/loadout_item/toys/dice
+ name = "Dice bag"
+ item_path = /obj/item/storage/dice
+
+/datum/loadout_item/toys/eightball
+ name = "Magic eightball"
+ item_path = /obj/item/toy/eightball
+
+/datum/loadout_item/toys/tennis
+ name = "Classic Tennis Ball"
+ item_path = /obj/item/toy/tennis
+
+/datum/loadout_item/toys/tennisred
+ name = "Red Tennis Ball"
+ item_path = /obj/item/toy/tennis/red
+
+/datum/loadout_item/toys/tennisyellow
+ name = "Yellow Tennis Ball"
+ item_path = /obj/item/toy/tennis/yellow
+
+/datum/loadout_item/toys/tennisgreen
+ name = "Green Tennis Ball"
+ item_path = /obj/item/toy/tennis/green
+
+/datum/loadout_item/toys/tenniscyan
+ name = "Cyan Tennis Ball"
+ item_path = /obj/item/toy/tennis/cyan
+
+/datum/loadout_item/toys/tennisblue
+ name = "Blue Tennis Ball"
+ item_path = /obj/item/toy/tennis/blue
+
+/datum/loadout_item/toys/tennispurple
+ name = "Purple Tennis Ball"
+ item_path = /obj/item/toy/tennis/purple
+
+/datum/loadout_item/toys/toykatana
+ name = "Toy Katana"
+ item_path = /obj/item/toy/katana
+
+/datum/loadout_item/toys/crayons
+ name = "Box of crayons"
+ item_path = /obj/item/storage/crayons
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_under.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_under.dm
new file mode 100644
index 00000000000..b2b574e87a6
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_under.dm
@@ -0,0 +1,638 @@
+
+// --- Loadout item datums for under suits ---
+
+/// Underslot - Jumpsuit Items (Deletes overrided items)
+GLOBAL_LIST_INIT(loadout_jumpsuits, generate_loadout_items(/datum/loadout_item/under/jumpsuit))
+
+/// Underslot - Formal Suit Items (Deletes overrided items)
+GLOBAL_LIST_INIT(loadout_undersuits, generate_loadout_items(/datum/loadout_item/under/formal))
+
+/// Underslot - Misc. Under Items (Deletes overrided items)
+GLOBAL_LIST_INIT(loadout_miscunders, generate_loadout_items(/datum/loadout_item/under/miscellaneous))
+
+/datum/loadout_item/under
+ category = LOADOUT_ITEM_UNIFORM
+
+/datum/loadout_item/under/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
+ if(isplasmaman(equipper))
+ if(!visuals_only)
+ to_chat(equipper, "Your loadout uniform was not equipped directly due to your envirosuit.")
+ LAZYADD(outfit.backpack_contents, item_path)
+ else
+ outfit.uniform = item_path
+
+// jumpsuit undersuits
+/datum/loadout_item/under/jumpsuit
+
+/datum/loadout_item/under/jumpsuit/greyscale
+ name = "Greyscale Jumpsuit"
+ can_be_greyscale = TRUE
+ item_path = /obj/item/clothing/under/color
+
+/datum/loadout_item/under/jumpsuit/greyscale_skirt
+ name = "Greyscale Jumpskirt"
+ can_be_greyscale = TRUE
+ item_path = /obj/item/clothing/under/color/jumpskirt
+
+/datum/loadout_item/under/jumpsuit/random
+ name = "Random Jumpsuit"
+ item_path = /obj/item/clothing/under/color/random
+ additional_tooltip_contents = list(TOOLTIP_RANDOM_COLOR)
+
+/datum/loadout_item/under/jumpsuit/random_skirt
+ name = "Random Jumpskirt"
+ item_path = /obj/item/clothing/under/color/jumpskirt/random
+ additional_tooltip_contents = list(TOOLTIP_RANDOM_COLOR)
+
+/datum/loadout_item/under/jumpsuit/rainbow
+ name = "Rainbow Jumpsuit"
+ item_path = /obj/item/clothing/under/color/rainbow
+
+/datum/loadout_item/under/jumpsuit/rainbow_skirt
+ name = "Rainbow Jumpskirt"
+ item_path = /obj/item/clothing/under/color/jumpskirt/rainbow
+
+/datum/loadout_item/under/jumpsuit/impcap
+ name = "Captain's Naval Jumpsuit"
+ item_path = /obj/item/clothing/under/rank/captain/imperial
+ restricted_roles = list("Captain", "Nanotrasen Representative")
+
+/datum/loadout_item/under/jumpsuit/imphop
+ name = "Head of Personnel's Naval Jumpsuit"
+ item_path = /obj/item/clothing/under/rank/civilian/head_of_personnel/imperial
+ restricted_roles = list("Head of Personnel", "Nanotrasen Representative")
+
+/datum/loadout_item/under/jumpsuit/imphos
+ name = "Head of Security's Naval Uniform"
+ item_path = /obj/item/clothing/under/rank/security/head_of_security/imperial
+ restricted_roles = list("Head of Security")
+
+/datum/loadout_item/under/jumpsuit/impcmo
+ name = "Chief Medical Officer's Naval Uniform"
+ item_path = /obj/item/clothing/under/rank/medical/chief_medical_officer/imperial
+ restricted_roles = list("Chief Medical Officer")
+
+/datum/loadout_item/under/jumpsuit/impce
+ name = "Chief Engineer's Naval Uniform"
+ item_path = /obj/item/clothing/under/rank/engineering/chief_engineer/imperial
+ restricted_roles = list("Chief Engineer")
+
+/datum/loadout_item/under/jumpsuit/imprd
+ name = "Research Director's Naval Uniform"
+ item_path = /obj/item/clothing/under/rank/rnd/research_director/imperial
+ restricted_roles = list("Research Director")
+
+
+/datum/loadout_item/under/jumpsuit/impcommand
+ name = "Light Grey Officer's Naval Jumpsuit"
+ item_path = /obj/item/clothing/under/imperial
+ restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
+
+/datum/loadout_item/under/jumpsuit/impcom
+ name = "Grey Officer's Naval Jumpsuit"
+ item_path = /obj/item/clothing/under/imperial/grey
+ restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
+
+/datum/loadout_item/under/jumpsuit/impred
+ name = "Red Officer's Naval Jumpsuit"
+ item_path = /obj/item/clothing/under/imperial/red
+ restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer") //NT Reps would never wear red, it's unbefitting
+
+
+/datum/loadout_item/under/jumpsuit/impcomtrous
+ name = "Grey Officer's Naval Jumpsuit (Trousers)"
+ item_path = /obj/item/clothing/under/imperial/grey/trouser
+ restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer", "Nanotrasen Representative")
+
+/datum/loadout_item/under/jumpsuit/solwarden
+ name = "Sol Warden Uniform"
+ item_path = /obj/item/clothing/under/rank/security/warden/peacekeeper/sol
+ restricted_roles = list("Warden")
+
+/datum/loadout_item/under/jumpsuit/peacetrouse
+ name = "Peacekeeper Trousers"
+ item_path = /obj/item/clothing/under/rank/security/peacekeeper/trousers
+ restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant")
+
+/datum/loadout_item/under/jumpsuit/security_trousers
+ name = "Security Trousers"
+ item_path = /obj/item/clothing/under/rank/security/peacekeeper/trousers/red
+ restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant")
+
+/datum/loadout_item/under/jumpsuit/sectrafficop
+ name = "Sol Traffic Cop Uniform"
+ item_path = /obj/item/clothing/under/rank/security/peacekeeper/junior/sol/traffic
+ restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant", "Civil Disputes Officer")
+
+/datum/loadout_item/under/jumpsuit/solofficerthing
+ name = "Sol Officer Uniform"
+ item_path = /obj/item/clothing/under/rank/security/peacekeeper/sol
+ restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant")
+
+/datum/loadout_item/under/jumpsuit/disco
+ name = "Superstar Cop Suit"
+ item_path = /obj/item/clothing/under/misc/discounder
+ restricted_roles = list("Detective")
+
+/datum/loadout_item/under/jumpsuit/seckilt
+ name = "Security Kilt"
+ item_path = /obj/item/clothing/under/rank/security/blackwatch
+ restricted_roles = list("Security Officer", "Warden", "Head of Security","Security Medic","Security Sergeant")
+
+/datum/loadout_item/under/jumpsuit/para_red
+ name = "Red Paramedic Jumpsuit"
+ item_path = /obj/item/clothing/under/utility/para_red
+ restricted_roles = list("Chief Medical Officer", "Paramedic", "Security Medic")
+
+
+/datum/loadout_item/under/jumpsuit/utility
+ name = "Utility Uniform"
+ item_path = /obj/item/clothing/under/utility
+
+/datum/loadout_item/under/jumpsuit/utility_eng
+ name = "Engineering Utility Uniform"
+ item_path = /obj/item/clothing/under/utility/eng
+ restricted_roles = list("Station Engineer","Atmospheric Technician", "Chief Engineer")
+
+/datum/loadout_item/under/jumpsuit/utility_med
+ name = "Medical Utility Uniform"
+ item_path = /obj/item/clothing/under/utility/med
+ restricted_roles = list("Medical Doctor", "Paramedic", "Chemist", "Virologist", "Geneticist", "Security Medic", "Chief Medical Officer", "Psychologist")
+
+/datum/loadout_item/under/jumpsuit/utility_sci
+ name = "Science Utility Uniform"
+ item_path = /obj/item/clothing/under/utility/sci
+ restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Vanguard Operative")
+
+
+/datum/loadout_item/under/jumpsuit/hlscientist
+ name = "Ridiculous Scientist Outfit"
+ item_path = /obj/item/clothing/under/misc/hlscience
+ restricted_roles = list("Scientist", "Roboticist", "Geneticist", "Research Director", "Vanguard Operative")
+
+/datum/loadout_item/under/jumpsuit/utility_cargo
+ name = "Supply Utility Uniform"
+ item_path = /obj/item/clothing/under/utility/cargo
+ restricted_roles = list("Cargo Technician", "Miner", "Quartermaster")
+
+/datum/loadout_item/under/jumpsuit/utility_sec
+ name = "Security Utility Uniform"
+ item_path = /obj/item/clothing/under/utility/sec
+ restricted_roles = list("Security Officer", "Detective", "Warden", "Blueshield", "Security Medic","Security Sergeant", "Head of Security", "Civil Disputes Officer", "Corrections Officer") //i dunno about the blueshield, they're a weird combo of sec and command, thats why they arent in the loadout pr im making
+
+/datum/loadout_item/under/jumpsuit/utility_com
+ name = "Command Utility Uniform"
+ item_path = /obj/item/clothing/under/utility/com
+ restricted_roles = list("Captain", "Head of Personnel", "Blueshield", "Head of Security", "Research Director", "Quartermaster", "Chief Medical Officer", "Chief Engineer")
+
+// Donator jumpsuit stuffs here
+/datum/loadout_item/under/jumpsuit/donator
+ donator_only = TRUE
+
+/datum/loadout_item/under/jumpsuit/donator/enclavesergeant
+ name = "Enclave - Sergeant"
+ item_path = /obj/item/clothing/under/enclave
+
+/datum/loadout_item/under/jumpsuit/donator/enclaveofficer
+ name = "Enclave - Officer"
+ item_path = /obj/item/clothing/under/enclaveo
+
+/datum/loadout_item/under/jumpsuit/donator/fashionableskirt
+ name = "Fashionable Skirt"
+ item_path = /obj/item/clothing/under/rank/civilian/skirt
+
+/datum/loadout_item/under/jumpsuit/donator/blondie
+ name = "Blonde Cowboy Uniform"
+ item_path = /obj/item/clothing/under/rank/security/detective/cowboy/armorless
+
+
+// formal undersuits
+/datum/loadout_item/under/formal
+
+/datum/loadout_item/under/formal/amish_suit
+ name = "Amish Suit"
+ item_path = /obj/item/clothing/under/suit/sl
+
+/datum/loadout_item/under/formal/assistant
+ name = "Assistant Formal"
+ item_path = /obj/item/clothing/under/misc/assistantformal
+
+/datum/loadout_item/under/formal/beige_suit
+ name = "Beige Suit"
+ item_path = /obj/item/clothing/under/suit/beige
+
+/datum/loadout_item/under/formal/black_suit
+ name = "Black Suit"
+ item_path = /obj/item/clothing/under/suit/black
+
+/datum/loadout_item/under/formal/black_suitskirt
+ name = "Black Suitskirt"
+ item_path = /obj/item/clothing/under/suit/black/skirt
+
+/datum/loadout_item/under/formal/black_tango
+ name = "Black Tango Dress"
+ item_path = /obj/item/clothing/under/dress/blacktango
+
+/datum/loadout_item/under/formal/Black_twopiece
+ name = "Black Two-Piece Suit"
+ item_path = /obj/item/clothing/under/suit/blacktwopiece
+
+/datum/loadout_item/under/formal/black_skirt
+ name = "Black Skirt"
+ item_path = /obj/item/clothing/under/dress/skirt
+
+/datum/loadout_item/under/formal/black_lawyer_suit
+ name = "Black Lawyer Suit"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/black
+
+/datum/loadout_item/under/formal/black_lawyer_skirt
+ name = "Black Lawyer Suitskirt"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/black/skirt
+
+/datum/loadout_item/under/formal/blue_suit
+ name = "Blue Suit"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/bluesuit
+
+/datum/loadout_item/under/formal/blue_suitskirt
+ name = "Blue Suitskirt"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/bluesuit/skirt
+
+/datum/loadout_item/under/formal/blue_lawyer_suit
+ name = "Blue Lawyer Suit"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/blue
+
+/datum/loadout_item/under/formal/blue_lawyer_skirt
+ name = "Blue Lawyer Suitskirt"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/blue/skirt
+
+/datum/loadout_item/under/formal/blue_skirt
+ name = "Blue Skirt"
+ item_path = /obj/item/clothing/under/dress/skirt/blue
+
+/datum/loadout_item/under/formal/blue_skirt_plaid
+ name = "Blue Plaid Skirt"
+ item_path = /obj/item/clothing/under/dress/skirt/plaid/blue
+
+/datum/loadout_item/under/formal/burgundy_suit
+ name = "Burgundy Suit"
+ item_path = /obj/item/clothing/under/suit/burgundy
+
+/datum/loadout_item/under/formal/charcoal_suit
+ name = "Charcoal Suit"
+ item_path = /obj/item/clothing/under/suit/charcoal
+
+/datum/loadout_item/under/formal/checkered_suit
+ name = "Checkered Suit"
+ item_path = /obj/item/clothing/under/suit/checkered
+
+/datum/loadout_item/under/formal/executive_suit
+ name = "Executive Suit"
+ item_path = /obj/item/clothing/under/suit/black_really
+
+/datum/loadout_item/under/formal/executive_skirt
+ name = "Executive Suitskirt"
+ item_path = /obj/item/clothing/under/suit/black_really/skirt
+
+/datum/loadout_item/under/formal/executive_suit_alt
+ name = "Executive Suit Alt"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/female
+
+/datum/loadout_item/under/formal/executive_skirt_alt
+ name = "Executive Suitskirt Alt"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/female/skirt
+
+/datum/loadout_item/under/formal/green_suit
+ name = "Green Suit"
+ item_path = /obj/item/clothing/under/suit/green
+
+/datum/loadout_item/under/formal/green_skirt_plaid
+ name = "Green Plaid Skirt"
+ item_path = /obj/item/clothing/under/dress/skirt/plaid/green
+
+/datum/loadout_item/under/formal/navy_suit
+ name = "Navy Suit"
+ item_path = /obj/item/clothing/under/suit/navy
+
+/datum/loadout_item/under/formal/maid_outfit
+ name = "Maid Outfit"
+ item_path = /obj/item/clothing/under/costume/maid
+
+/datum/loadout_item/under/formal/maid_uniform
+ name = "Maid Uniform"
+ item_path = /obj/item/clothing/under/rank/civilian/janitor/maid
+
+/datum/loadout_item/under/formal/purple_suit
+ name = "Purple Suit"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/purpsuit
+
+/datum/loadout_item/under/formal/purple_suitskirt
+ name = "Purple Suitskirt"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/purpsuit/skirt
+
+/datum/loadout_item/under/formal/purple_skirt
+ name = "Purple Skirt"
+ item_path = /obj/item/clothing/under/dress/skirt/purple
+
+/datum/loadout_item/under/formal/purple_skirt_plaid
+ name = "Purple Plaid Skirt"
+ item_path = /obj/item/clothing/under/dress/skirt/plaid/purple
+
+/datum/loadout_item/under/formal/red_suit
+ name = "Red Suit"
+ item_path = /obj/item/clothing/under/suit/red
+
+/datum/loadout_item/under/formal/red_lawyer_skirt
+ name = "Red Lawyer Suit"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/red
+
+/datum/loadout_item/under/formal/red_lawyer_skirt
+ name = "Red Lawyer Suitskirt"
+ item_path = /obj/item/clothing/under/rank/civilian/lawyer/red/skirt
+
+/datum/loadout_item/under/formal/red_gown
+ name = "Red Evening Gown"
+ item_path = /obj/item/clothing/under/dress/redeveninggown
+
+/datum/loadout_item/under/formal/red_skirt
+ name = "Red Skirt"
+ item_path = /obj/item/clothing/under/dress/skirt/red
+
+/datum/loadout_item/under/formal/red_skirt_plaid
+ name = "Red Plaid Skirt"
+ item_path = /obj/item/clothing/under/dress/skirt/plaid
+
+/datum/loadout_item/under/formal/sailor
+ name = "Sailor Suit"
+ item_path = /obj/item/clothing/under/costume/sailor
+
+/datum/loadout_item/under/formal/sailor_skirt
+ name = "Sailor Dress"
+ item_path = /obj/item/clothing/under/dress/sailor
+
+/datum/loadout_item/under/formal/scratch_suit
+ name = "Scratch Suit"
+ item_path = /obj/item/clothing/under/suit/white_on_white
+
+/datum/loadout_item/under/formal/striped_skirt
+ name = "Striped Dress"
+ item_path = /obj/item/clothing/under/dress/striped
+
+/datum/loadout_item/under/formal/sensible_suit
+ name = "Sensible Suit"
+ item_path = /obj/item/clothing/under/rank/civilian/curator
+
+/datum/loadout_item/under/formal/sensible_skirt
+ name = "Sensible Suitskirt"
+ item_path = /obj/item/clothing/under/rank/civilian/curator/skirt
+
+/datum/loadout_item/under/formal/sundress
+ name = "Sundress"
+ item_path = /obj/item/clothing/under/dress/sundress
+
+/datum/loadout_item/under/formal/tuxedo
+ name = "Tuxedo Suit"
+ item_path = /obj/item/clothing/under/suit/tuxedo
+
+/datum/loadout_item/under/formal/waiter
+ name = "Waiter's Suit"
+ item_path = /obj/item/clothing/under/suit/waiter
+
+/datum/loadout_item/under/formal/white_suit
+ name = "White Suit"
+ item_path = /obj/item/clothing/under/suit/white
+
+/datum/loadout_item/under/formal/formalmed
+ name = "Formal Medical Suit"
+ item_path = /obj/item/clothing/under/rank/medical/doctor/formal
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic", "Psychologist")
+
+// misc undersuits
+/datum/loadout_item/under/miscellaneous
+
+/datum/loadout_item/under/miscellaneous/camo
+ name = "Camo Pants"
+ item_path = /obj/item/clothing/under/pants/camo
+
+/datum/loadout_item/under/miscellaneous/jeans_classic
+ name = "Classic Jeans"
+ item_path = /obj/item/clothing/under/pants/classicjeans
+
+/datum/loadout_item/under/miscellaneous/jeans_black
+ name = "Black Jeans"
+ item_path = /obj/item/clothing/under/pants/blackjeans
+
+/datum/loadout_item/under/miscellaneous/black
+ name = "Black Pants"
+ item_path = /obj/item/clothing/under/pants/black
+
+/datum/loadout_item/under/miscellaneous/black_short
+ name = "Black Shorts"
+ item_path = /obj/item/clothing/under/shorts/black
+
+/datum/loadout_item/under/miscellaneous/blue_short
+ name = "Blue Shorts"
+ item_path = /obj/item/clothing/under/shorts/blue
+
+/datum/loadout_item/under/miscellaneous/green_short
+ name = "Green Shorts"
+ item_path = /obj/item/clothing/under/shorts/green
+
+/datum/loadout_item/under/miscellaneous/grey_short
+ name = "Grey Shorts"
+ item_path = /obj/item/clothing/under/shorts/grey
+
+/datum/loadout_item/under/miscellaneous/jeans
+ name = "Jeans"
+ item_path = /obj/item/clothing/under/pants/jeans
+
+/datum/loadout_item/under/miscellaneous/khaki
+ name = "Khaki Pants"
+ item_path = /obj/item/clothing/under/pants/khaki
+
+/datum/loadout_item/under/miscellaneous/jeans_musthang
+ name = "Must Hang Jeans"
+ item_path = /obj/item/clothing/under/pants/mustangjeans
+
+/datum/loadout_item/under/miscellaneous/purple_short
+ name = "Purple Shorts"
+ item_path = /obj/item/clothing/under/shorts/purple
+
+/datum/loadout_item/under/miscellaneous/red
+ name = "Red Pants"
+ item_path = /obj/item/clothing/under/pants/red
+
+/datum/loadout_item/under/miscellaneous/red_short
+ name = "Red Shorts"
+ item_path = /obj/item/clothing/under/shorts/red
+
+/datum/loadout_item/under/miscellaneous/tam
+ name = "Tan Pants"
+ item_path = /obj/item/clothing/under/pants/tan
+
+/datum/loadout_item/under/miscellaneous/track
+ name = "Track Pants"
+ item_path = /obj/item/clothing/under/pants/track
+
+/datum/loadout_item/under/miscellaneous/jeans_youngfolk
+ name = "Young Folks Jeans"
+ item_path = /obj/item/clothing/under/pants/youngfolksjeans
+
+/datum/loadout_item/under/miscellaneous/white
+ name = "White Pants"
+ item_path = /obj/item/clothing/under/pants/white
+
+/datum/loadout_item/under/miscellaneous/kilt
+ name = "Kilt"
+ item_path = /obj/item/clothing/under/costume/kilt
+
+/datum/loadout_item/under/miscellaneous/treasure_hunter
+ name = "Treasure Hunter"
+ item_path = /obj/item/clothing/under/rank/civilian/curator/treasure_hunter
+
+/datum/loadout_item/under/miscellaneous/overalls
+ name = "Overalls"
+ item_path = /obj/item/clothing/under/misc/overalls
+
+/datum/loadout_item/under/miscellaneous/pj_blue
+ name = "Mailman Jumpsuit"
+ item_path = /obj/item/clothing/under/misc/mailman
+
+/datum/loadout_item/under/miscellaneous/vice_officer
+ name = "Vice Officer Jumpsuit"
+ item_path = /obj/item/clothing/under/misc/vice_officer
+
+/datum/loadout_item/under/miscellaneous/soviet
+ name = "Soviet Uniform"
+ item_path = /obj/item/clothing/under/costume/soviet
+
+/datum/loadout_item/under/miscellaneous/redcoat
+ name = "Redcoat"
+ item_path = /obj/item/clothing/under/costume/redcoat
+
+/datum/loadout_item/under/miscellaneous/pj_red
+ name = "Red PJs"
+ item_path = /obj/item/clothing/under/misc/pj/red
+
+/datum/loadout_item/under/miscellaneous/pj_blue
+ name = "Blue PJs"
+ item_path = /obj/item/clothing/under/misc/pj/blue
+
+/datum/loadout_item/under/miscellaneous/tactical_hawaiian_orange
+ name = "Tactical Hawaiian Outfit - Orange"
+ item_path = /obj/item/clothing/under/tachawaiian
+
+/datum/loadout_item/under/miscellaneous/tactical_hawaiian_blue
+ name = "Tactical Hawaiian Outfit - Blue"
+ item_path = /obj/item/clothing/under/tachawaiian/blue
+
+/datum/loadout_item/under/miscellaneous/tactical_hawaiian_purple
+ name = "Tactical Hawaiian Outfit - Purple"
+ item_path = /obj/item/clothing/under/tachawaiian/purple
+
+/datum/loadout_item/under/miscellaneous/tactical_hawaiian_green
+ name = "Tactical Hawaiian Outfit - Green"
+ item_path = /obj/item/clothing/under/tachawaiian/green
+
+/datum/loadout_item/under/miscellaneous/greyshirt
+ name = "Grey Shirt"
+ item_path = /obj/item/clothing/under/misc/greyshirt
+
+/datum/loadout_item/under/miscellaneous/maidcostume
+ name = "Maid costume"
+ item_path = /obj/item/clothing/under/costume/maid
+
+/datum/loadout_item/under/miscellaneous/croptop
+ name = "Croptop"
+ item_path = /obj/item/clothing/under/croptop
+
+/datum/loadout_item/under/miscellaneous/royalkilt
+ name = "Royal Kilt"
+ item_path = /obj/item/clothing/under/misc/royalkilt
+
+/datum/loadout_item/under/miscellaneous/qipao
+ name = "Qipao, Black"
+ item_path = /obj/item/clothing/under/costume/qipao
+
+/datum/loadout_item/under/miscellaneous/cheongsam
+ name = "Cheongsam, Black"
+ item_path = /obj/item/clothing/under/costume/cheongsam
+
+/datum/loadout_item/under/miscellaneous/chaps
+ name = "Black Chaps"
+ item_path = /obj/item/clothing/under/pants/chaps
+
+/datum/loadout_item/under/miscellaneous/tracky
+ name = "Blue Tracksuit"
+ item_path = /obj/item/clothing/under/misc/bluetracksuit
+
+/datum/loadout_item/under/miscellaneous/cybersleek
+ name = "Sleek Modern Coat"
+ item_path = /obj/item/clothing/under/costume/cybersleek
+
+/datum/loadout_item/under/miscellaneous/cybersleek_long
+ name = "Long Modern Coat"
+ item_path = /obj/item/clothing/under/costume/cybersleek/long
+
+/datum/loadout_item/under/miscellaneous/dutch
+ name = "Dutch Suit"
+ item_path = /obj/item/clothing/under/costume/dutch
+
+/datum/loadout_item/under/miscellaneous/arthur
+ name = "Dutch Assistant Suit"
+ item_path = /obj/item/clothing/under/costume/arthur
+
+/datum/loadout_item/under/miscellaneous/yoga
+ name = "Yoga Pants"
+ item_path = /obj/item/clothing/under/pants/yoga
+
+/datum/loadout_item/under/miscellaneous/tacticool_turtleneck
+ name = "Tactitool Turtleneck"
+ item_path = /obj/item/clothing/under/syndicate/tacticool/sensors
+
+/datum/loadout_item/under/miscellaneous/tactical_irish
+ name = "Irish Tactical Sweater"
+ item_path = /obj/item/clothing/under/misc/tactical1
+
+/datum/loadout_item/under/miscellaneous/tactical_irish
+ name = "British Tactical Sweater"
+ item_path = /obj/item/clothing/under/uvf
+
+/datum/loadout_item/under/miscellaneous/tactical_skirt
+ name = "Tactitool Skirtleneck"
+ item_path = /obj/item/clothing/under/syndicate/tacticool/skirt/sensors
+
+
+/datum/loadout_item/under/miscellaneous/blacknwhite
+ name = "Classic Prisoner Jumpsuit"
+ item_path = /obj/item/clothing/under/rank/prisoner/classic
+ restricted_roles = list("Prisoner")
+
+/datum/loadout_item/under/miscellaneous/medrscrubs
+ name = "Red Scrubs (security)"
+ item_path = /obj/item/clothing/under/rank/medical/doctor/red
+ restricted_roles = list("Security Medic")
+
+/datum/loadout_item/under/miscellaneous/redscrubs
+ name = "Red Scrubs"
+ item_path = /obj/item/clothing/under/rank/medical/doctor/red/unarm
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
+
+/datum/loadout_item/under/miscellaneous/bluescrubs
+ name = "Blue Scrubs"
+ item_path = /obj/item/clothing/under/rank/medical/doctor/blue
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
+
+/datum/loadout_item/under/miscellaneous/greenscrubs
+ name = "Green Scrubs"
+ item_path = /obj/item/clothing/under/rank/medical/doctor/green
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
+
+/datum/loadout_item/under/miscellaneous/purplescrubs
+ name = "Purple Scrubs"
+ item_path = /obj/item/clothing/under/rank/medical/doctor/purple
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
+
+/datum/loadout_item/under/miscellaneous/whitescrubs
+ name = "White Scrubs"
+ item_path = /obj/item/clothing/under/rank/medical/doctor/white
+ restricted_roles = list("Medical Doctor", "Chief Medical Officer", "Geneticist", "Chemist", "Virologist", "Security Medic", "Paramedic")
diff --git a/modular_skyrat/modules/loadouts/loadout_ui/loadout_manager.dm b/modular_skyrat/modules/loadouts/loadout_ui/loadout_manager.dm
new file mode 100644
index 00000000000..c53a3f7aa8a
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_ui/loadout_manager.dm
@@ -0,0 +1,382 @@
+/// -- The loadout manager and UI --
+/// Tracking when a client has an open loadout manager, to prevent funky stuff.
+/client
+ /// A ref to loadout_manager datum.
+ var/datum/loadout_manager/open_loadout_ui = null
+
+/// Datum holder for the loadout manager UI.
+/datum/loadout_manager
+ /// The client of the person using the UI
+ var/client/owner
+ /// The current selected loadout list.
+ var/list/loadout_on_open
+ /// The key of the dummy we use to generate sprites
+ var/dummy_key
+ /// The dir the dummy is facing.
+ var/list/dummy_dir = list(SOUTH)
+ /// A ref to the dummy outfit we're using
+ var/datum/outfit/player_loadout/custom_loadout
+ /// A preview of the current character
+ var/atom/movable/screen/character_preview_view/character_preview_view
+ /// Whether we see our favorite job's clothes on the dummy
+ var/view_job_clothes = TRUE
+ /// Whether we see tutorial text in the UI
+ var/tutorial_status = FALSE
+ /// Our currently open greyscaling menu.
+ var/datum/greyscale_modify_menu/menu
+ /// Whether we need to update our dummy sprite next ui_data or not.
+ var/update_dummysprite = TRUE
+ /// Our preview sprite.
+ var/icon/dummysprite
+
+/datum/loadout_manager/Destroy(force, ...)
+ owner = null
+ QDEL_NULL(character_preview_view)
+ QDEL_NULL(menu)
+ QDEL_NULL(custom_loadout)
+ return ..()
+
+/datum/loadout_manager/New(user)
+ owner = CLIENT_FROM_VAR(user)
+ owner.open_loadout_ui = src
+ loadout_on_open = LAZYLISTDUPLICATE(owner.prefs.loadout_list)
+ custom_loadout = new()
+
+/datum/loadout_manager/ui_close(mob/user)
+ owner?.prefs.save_character()
+ if(menu)
+ SStgui.close_uis(menu)
+ menu = null
+ owner?.open_loadout_ui = null
+ QDEL_NULL(character_preview_view)
+ qdel(custom_loadout)
+ qdel(src)
+
+/datum/loadout_manager/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/loadout_manager/ui_interact(mob/user, datum/tgui/ui)
+ if (!isnull(character_preview_view) && !(character_preview_view in owner?.screen))
+ owner?.register_map_obj(character_preview_view)
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "LoadoutManager")
+ ui.open()
+
+ addtimer(CALLBACK(character_preview_view, /atom/movable/screen/character_preview_view/proc/update_body), 1 SECONDS)
+
+/datum/loadout_manager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ var/datum/loadout_item/interacted_item
+ if(params["path"])
+ interacted_item = GLOB.all_loadout_datums[text2path(params["path"])]
+ if(!interacted_item)
+ stack_trace("Failed to locate desired loadout item (path: [params["path"]]) in the global list of loadout datums!")
+ return
+
+ //Here we will perform basic checks to ensure there are no exploits happening
+ if(interacted_item.donator_only && !GLOB.donator_list[owner.ckey])
+ message_admins("LOADOUT SYSTEM: Possible exploit detected, non-donator [owner.ckey] tried loading [interacted_item.item_path], but this is donator only.")
+ return
+
+ if(interacted_item.ckeywhitelist && !(owner.ckey in interacted_item.ckeywhitelist))
+ message_admins("LOADOUT SYSTEM: Possible exploit detected, non-donator [owner.ckey] tried loading [interacted_item.item_path], but this is ckey locked.")
+ return
+
+ switch(action)
+ // Turns the tutorial on and off.
+ if("toggle_tutorial")
+ tutorial_status = !tutorial_status
+ return TRUE
+
+ // Closes the UI, reverting our loadout to before edits if params["revert"] is set
+ if("close_ui")
+ if(params["revert"])
+ owner.prefs.loadout_list = loadout_on_open
+ SStgui.close_uis(src)
+ return
+
+ if("select_item")
+ if(params["deselect"])
+ deselect_item(interacted_item)
+ character_preview_view.update_body()
+ else
+ select_item(interacted_item)
+ character_preview_view.update_body()
+
+ if("select_color")
+ select_item_color(interacted_item)
+
+ if("set_name")
+ set_item_name(interacted_item)
+
+ if("display_restrictions")
+ display_job_restrictions(interacted_item)
+
+ // Clears the loadout list entirely.
+ if("clear_all_items")
+ LAZYNULL(owner.prefs.loadout_list)
+ character_preview_view.update_body()
+
+ // Rotates the dummy left or right depending on params["dir"]
+ if("rotate_dummy")
+ rotate_model_dir(params["dir"])
+
+ // Toggles between showing all dirs of the dummy at once.
+ if("show_all_dirs")
+ toggle_model_dirs()
+
+ if("update_preview")
+ owner?.prefs.preview_pref = params["updated_preview"]
+ character_preview_view.update_body()
+
+ if("donator_explain")
+ if(GLOB.donator_list[owner.ckey])
+ to_chat(owner, examine_block("Thank you for donating, this item is for you <3! "))
+ else
+ to_chat(owner, examine_block(span_boldnotice("This item is restricted to donators only, for more information, please check the discord(#server-info) for more information!")))
+
+ if("ckey_explain")
+ to_chat(owner, examine_block(span_green("This item is restricted to your ckey only. Thank you!")))
+
+ return TRUE
+
+/// Select [path] item to [category_slot] slot.
+/datum/loadout_manager/proc/select_item(datum/loadout_item/selected_item)
+ var/num_misc_items = 0
+ var/datum/loadout_item/first_misc_found
+ for(var/datum/loadout_item/item as anything in loadout_list_to_datums(owner.prefs.loadout_list))
+ if(item.category == selected_item.category)
+ if(item.category == LOADOUT_ITEM_MISC && ++num_misc_items < MAX_ALLOWED_MISC_ITEMS)
+ if(!first_misc_found)
+ first_misc_found = item
+ continue
+
+ deselect_item(first_misc_found || item)
+ continue
+
+ LAZYSET(owner.prefs.loadout_list, selected_item.item_path, list())
+
+/// Deselect [deselected_item].
+/datum/loadout_manager/proc/deselect_item(datum/loadout_item/deselected_item)
+ LAZYREMOVE(owner.prefs.loadout_list, deselected_item.item_path)
+
+/// Select [path] item to [category_slot] slot, and open up the greyscale UI to customize [path] in [category] slot.
+/datum/loadout_manager/proc/select_item_color(datum/loadout_item/item)
+ if(menu)
+ to_chat(owner, span_warning("You already have a greyscaling window open!"))
+ return
+
+ var/obj/item/colored_item = item.item_path
+
+ var/list/allowed_configs = list()
+ if(initial(colored_item.greyscale_config))
+ allowed_configs += "[initial(colored_item.greyscale_config)]"
+ if(initial(colored_item.greyscale_config_worn))
+ allowed_configs += "[initial(colored_item.greyscale_config_worn)]"
+ if(initial(colored_item.greyscale_config_inhand_left))
+ allowed_configs += "[initial(colored_item.greyscale_config_inhand_left)]"
+ if(initial(colored_item.greyscale_config_inhand_right))
+ allowed_configs += "[initial(colored_item.greyscale_config_inhand_right)]"
+
+ var/slot_starting_colors = initial(colored_item.greyscale_colors)
+ if(INFO_GREYSCALE in owner.prefs.loadout_list[colored_item])
+ slot_starting_colors = owner.prefs.loadout_list[colored_item][INFO_GREYSCALE]
+
+ menu = new(
+ src,
+ usr,
+ allowed_configs,
+ CALLBACK(src, .proc/set_slot_greyscale, colored_item),
+ starting_icon_state = initial(colored_item.icon_state),
+ starting_config = initial(colored_item.greyscale_config),
+ starting_colors = slot_starting_colors,
+ )
+ RegisterSignal(menu, COMSIG_PARENT_PREQDELETED, /datum/loadout_manager.proc/cleanup_greyscale_menu)
+ menu.ui_interact(usr)
+
+/// A proc to make sure our menu gets null'd properly when it's deleted.
+/// If we delete the greyscale menu from the greyscale datum, we don't null it correctly here, it harddels.
+/datum/loadout_manager/proc/cleanup_greyscale_menu(datum/source)
+ SIGNAL_HANDLER
+
+ menu = null
+
+/// Sets [category_slot]'s greyscale colors to the colors in the currently opened [open_menu].
+/datum/loadout_manager/proc/set_slot_greyscale(path, datum/greyscale_modify_menu/open_menu)
+ if(!open_menu)
+ CRASH("set_slot_greyscale called without a greyscale menu!")
+
+ if(!(path in owner.prefs.loadout_list))
+ to_chat(owner, span_warning("Select the item before attempting to apply greyscale to it!"))
+ return
+
+ var/list/colors = open_menu.split_colors
+ if(colors)
+ owner.prefs.loadout_list[path][INFO_GREYSCALE] = colors.Join("")
+ update_dummysprite = TRUE
+
+/// Set [item]'s name to input provided.
+/datum/loadout_manager/proc/set_item_name(datum/loadout_item/item)
+ var/current_name = ""
+ if(INFO_NAMED in owner.prefs.loadout_list[item.item_path])
+ current_name = owner.prefs.loadout_list[item.item_path][INFO_NAMED]
+
+ var/input_name = stripped_input(owner, "What name do you want to give [item.name]? Leave blank to clear.", "[item.name] name", current_name, MAX_NAME_LEN)
+ if(QDELETED(src) || QDELETED(owner) || QDELETED(owner.prefs))
+ return
+
+ if(!(item.item_path in owner.prefs.loadout_list))
+ to_chat(owner, span_warning("Select the item before attempting to name to it!"))
+ return
+
+ if(input_name)
+ owner.prefs.loadout_list[item.item_path][INFO_NAMED] = input_name
+ else
+ if(INFO_NAMED in owner.prefs.loadout_list[item.item_path])
+ owner.prefs.loadout_list[item.item_path] -= INFO_NAMED
+
+/datum/loadout_manager/proc/display_job_restrictions(datum/loadout_item/item)
+ var/composed_message = span_boldnotice("The [initial(item.item_path.name)] is restricted to the following roles: ")
+ for(var/job_type in item.restricted_roles)
+ composed_message += span_green("[job_type] ")
+
+ to_chat(owner, examine_block(composed_message))
+
+
+/// Rotate the dummy [DIR] direction, or reset it to SOUTH dir if we're showing all dirs at once.
+/datum/loadout_manager/proc/rotate_model_dir(dir)
+ if(dir == "left")
+ character_preview_view.dir = turn(character_preview_view.dir, 90)
+ else
+ character_preview_view.dir = turn(character_preview_view.dir, -90)
+
+/// Toggle between showing all the dirs and just the front dir of the dummy.
+/datum/loadout_manager/proc/toggle_model_dirs()
+ if(dummy_dir.len > 1)
+ dummy_dir = list(SOUTH)
+ else
+ dummy_dir = GLOB.cardinals
+
+/datum/loadout_manager/ui_data(mob/user)
+ var/list/data = list()
+
+ if (isnull(character_preview_view))
+ character_preview_view = create_character_preview_view(user)
+ else if (character_preview_view.client != owner)
+ // 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(owner)
+
+ var/list/all_selected_paths = list()
+ for(var/path in owner.prefs.loadout_list)
+ all_selected_paths += path
+
+ data["character_preview_view"] = character_preview_view.assigned_map
+ data["selected_loadout"] = all_selected_paths
+ data["user_is_donator"] = GLOB.donator_list[owner.ckey]
+ data["mob_name"] = owner.prefs.read_preference(/datum/preference/name/real_name)
+ data["ismoth"] = istype(owner.prefs.read_preference(/datum/preference/choiced/species), /datum/species/moth) // Moth's humanflaticcon isn't the same dimensions for some reason
+ data["preivew_options"] = list(PREVIEW_PREF_JOB, PREVIEW_PREF_LOADOUT, PREVIEW_PREF_NAKED)
+ data["preview_selection"] = owner?.prefs.preview_pref
+ data["tutorial_status"] = tutorial_status
+ if(tutorial_status)
+ data["tutorial_text"] = get_tutorial_text()
+
+ return data
+
+/datum/loadout_manager/ui_static_data()
+ var/list/data = list()
+
+ // [name] is the name of the tab that contains all the corresponding contents.
+ // [title] is the name at the top of the list of corresponding contents.
+ // [contents] is a formatted list of all the possible items for that slot.
+ // - [contents.path] is the path the singleton datum holds
+ // - [contents.name] is the name of the singleton datum
+ // - [contents.is_renamable], whether the item can be renamed in the UI
+ // - [contents.is_greyscale], whether the item can be greyscaled in the UI
+ // - [contents.tooltip_text], any additional tooltip text that hovers over the item's select button
+
+ var/list/loadout_tabs = list()
+ loadout_tabs += list(list("name" = "Belt", "title" = "Belt Slot Items", "contents" = list_to_data(GLOB.loadout_belts)))
+ loadout_tabs += list(list("name" = "Ears", "title" = "Ear Slot Items", "contents" = list_to_data(GLOB.loadout_ears)))
+ loadout_tabs += list(list("name" = "Glasses", "title" = "Glasses Slot Items", "contents" = list_to_data(GLOB.loadout_glasses)))
+ loadout_tabs += list(list("name" = "Gloves", "title" = "Glove Slot Items", "contents" = list_to_data(GLOB.loadout_gloves)))
+ loadout_tabs += list(list("name" = "Head", "title" = "Head Slot Items", "contents" = list_to_data(GLOB.loadout_helmets)))
+ loadout_tabs += list(list("name" = "Mask", "title" = "Mask Slot Items", "contents" = list_to_data(GLOB.loadout_masks)))
+ loadout_tabs += list(list("name" = "Neck", "title" = "Neck Slot Items", "contents" = list_to_data(GLOB.loadout_necks)))
+ loadout_tabs += list(list("name" = "Shoes", "title" = "Shoe Slot Items", "contents" = list_to_data(GLOB.loadout_shoes)))
+ loadout_tabs += list(list("name" = "Suit", "title" = "Suit Slot Items", "contents" = list_to_data(GLOB.loadout_exosuits)))
+ loadout_tabs += list(list("name" = "Jumpsuit", "title" = "Uniform Slot Items", "contents" = list_to_data(GLOB.loadout_jumpsuits)))
+ loadout_tabs += list(list("name" = "Formal", "title" = "Uniform Slot Items (cont)", "contents" = list_to_data(GLOB.loadout_undersuits)))
+ loadout_tabs += list(list("name" = "Misc. Under", "title" = "Uniform Slot Items (cont)", "contents" = list_to_data(GLOB.loadout_miscunders)))
+ loadout_tabs += list(list("name" = "Accessory", "title" = "Uniform Accessory Slot Items", "contents" = list_to_data(GLOB.loadout_accessory)))
+ loadout_tabs += list(list("name" = "Inhand", "title" = "In-hand Items", "contents" = list_to_data(GLOB.loadout_inhand_items)))
+ loadout_tabs += list(list("name" = "Toys", "title" = "Toys!", "contents" = list_to_data(GLOB.loadout_toys)))
+ loadout_tabs += list(list("name" = "Other", "title" = "Backpack Items ([MAX_ALLOWED_MISC_ITEMS] max)", "contents" = list_to_data(GLOB.loadout_pocket_items)))
+
+ data["loadout_tabs"] = loadout_tabs
+
+ return data
+
+/datum/loadout_manager/proc/create_character_preview_view(mob/user)
+ character_preview_view = new(null, owner?.prefs, user.client)
+ character_preview_view.update_body()
+ character_preview_view.register_to_client(user.client)
+
+ return character_preview_view
+
+/// Returns a formatted string for use in the UI.
+/datum/loadout_manager/proc/get_tutorial_text()
+ return {"This is the Loadout Manager.
+It allows you to customize what your character will wear on shift start in addition to their job's uniform.
+Only one item can be selected per tab, with the exception of backpack items (three items are allowed in total).
+Some items have tooltips displaying additional information about how they work.
+Some items are compatible with greyscale coloring! You can choose what color they spawn as
+by selecting the item, then by pressing the paint icon next to it and using the greyscaling UI.
+Your loadout items will override the corresponding item in your job's outfit,
+with the exception being BELT, EAR, and GLASSES items,
+which will be placed in your backpack to prevent important items being deleted.
+Additionally, UNDERSUITS, HELMETS, MASKS, and GLOVES loadout items
+selected by plasmamen will spawn in their backpack instead of overriding their clothes
+to avoid an untimely and sudden death by fire or suffocation at the start of the shift."}
+
+/*
+ * Takes an assoc list of [typepath]s to [singleton datum]
+ * And formats it into an object for TGUI.
+ *
+ * - list[name] is the name of the datum.
+ * - list[path] is the typepath of the item.
+ */
+/datum/loadout_manager/proc/list_to_data(list_of_datums)
+ if(!LAZYLEN(list_of_datums))
+ return
+
+ var/list/formatted_list = new(length(list_of_datums))
+
+ var/array_index = 1
+ for(var/datum/loadout_item/item as anything in list_of_datums)
+ if(!isnull(item.ckeywhitelist)) //These checks are also performed in the backend.
+ if(!(owner.ckey in item.ckeywhitelist))
+ formatted_list.len--
+ continue
+ var/list/formatted_item = list()
+ formatted_item["name"] = item.name
+ formatted_item["path"] = item.item_path
+ formatted_item["is_greyscale"] = item.can_be_greyscale
+ formatted_item["is_renamable"] = item.can_be_named
+ formatted_item["is_job_restricted"] = !isnull(item.restricted_roles)
+ formatted_item["is_donator_only"] = !isnull(item.donator_only)
+ formatted_item["is_ckey_whitelisted"] = !isnull(item.ckeywhitelist)
+ if(LAZYLEN(item.additional_tooltip_contents))
+ formatted_item["tooltip_text"] = item.additional_tooltip_contents.Join("\n")
+
+ formatted_list[array_index++] = formatted_item
+
+ return formatted_list
+
+
diff --git a/modular_skyrat/modules/loadouts/loadout_ui/loadout_outfit_helpers.dm b/modular_skyrat/modules/loadouts/loadout_ui/loadout_outfit_helpers.dm
new file mode 100644
index 00000000000..5c9166ef1d1
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/loadout_ui/loadout_outfit_helpers.dm
@@ -0,0 +1,112 @@
+/// -- Outfit and mob helpers to equip our loadout items. --
+
+/// An empty outfit we fill in with our loadout items to dress our dummy.
+/datum/outfit/player_loadout
+ name = "Player Loadout"
+
+/*
+ * Actually equip our mob with our job outfit and our loadout items.
+ * Loadout items override the pre-existing item in the corresponding slot of the job outfit.
+ * Some job items are preserved after being overridden - belt items, ear items, and glasses.
+ * The rest of the slots, the items are overridden completely and deleted.
+ *
+ * Plasmamen are snowflaked to not have any envirosuit pieces removed just in case.
+ * Their loadout items for those slots will be added to their backpack on spawn.
+ *
+ * outfit - the job outfit we're equipping
+ * visuals_only - whether we call special equipped procs, or if we just look like we equipped it
+ * preference_source - the preferences of the thing we're equipping
+ */
+/mob/living/carbon/human/proc/equip_outfit_and_loadout(datum/outfit/outfit, datum/preferences/preference_source, visuals_only = FALSE, datum/job/equipping_job)
+ var/datum/outfit/equipped_outfit
+
+ if(ispath(outfit))
+ equipped_outfit = new outfit()
+ else if(istype(outfit))
+ equipped_outfit = outfit
+ else
+ CRASH("Outfit passed to equip_outfit_and_loadout was neither a path nor an instantiated type!")
+
+ var/list/loadout_datums = loadout_list_to_datums(preference_source?.loadout_list)
+ for(var/datum/loadout_item/item as anything in loadout_datums)
+ if(item.restricted_roles && equipping_job && !(equipping_job.title in item.restricted_roles))
+ if(client)
+ to_chat(src, span_warning("You were unable to get a loadout item([initial(item.item_path.name)]) due to job restrictions!"))
+ continue
+ item.insert_path_into_outfit(equipped_outfit, src, visuals_only)
+
+ equipOutfit(equipped_outfit, visuals_only)
+
+ for(var/datum/loadout_item/item as anything in loadout_datums)
+ item.on_equip_item(preference_source, src, visuals_only)
+
+ regenerate_icons()
+ return TRUE
+
+/*
+ * Takes a list of paths (such as a loadout list)
+ * and returns a list of their singleton loadout item datums
+ *
+ * loadout_list - the list being checked
+ *
+ * returns a list of singleton datums
+ */
+/proc/loadout_list_to_datums(list/loadout_list)
+ RETURN_TYPE(/list)
+
+ . = list()
+
+ if(!GLOB.all_loadout_datums.len)
+ CRASH("No loadout datums in the global loadout list!")
+
+ for(var/path in loadout_list)
+ if(!GLOB.all_loadout_datums[path])
+ stack_trace("Could not find ([path]) loadout item in the global list of loadout datums!")
+ continue
+
+ . |= GLOB.all_loadout_datums[path]
+
+
+/*
+ * Removes all invalid paths from loadout lists.
+ *
+ * passed_list - the loadout list we're sanitizing.
+ *
+ * returns a list
+ */
+/proc/update_loadout_list(list/passed_list)
+ RETURN_TYPE(/list)
+
+ var/list/list_to_update = LAZYLISTDUPLICATE(passed_list)
+ for(var/thing in list_to_update) //thing, 'cause it could be a lot of things
+ if(ispath(thing))
+ break
+ var/our_path = text2path(list_to_update[thing])
+
+ LAZYREMOVE(list_to_update, thing)
+ if(ispath(our_path))
+ LAZYSET(list_to_update, our_path, list())
+
+ return list_to_update
+
+/*
+ * Removes all invalid paths from loadout lists.
+ *
+ * passed_list - the loadout list we're sanitizing.
+ *
+ * returns a list
+ */
+/proc/sanitize_loadout_list(list/passed_list)
+ RETURN_TYPE(/list)
+
+ var/list/list_to_clean = LAZYLISTDUPLICATE(passed_list)
+ for(var/path in list_to_clean)
+ if(!ispath(path))
+ stack_trace("invalid path found in loadout list! (Path: [path])")
+ LAZYREMOVE(list_to_clean, path)
+
+ else if(!(path in GLOB.all_loadout_datums))
+ stack_trace("invalid loadout slot found in loadout list! Path: [path]")
+ LAZYREMOVE(list_to_clean, path)
+
+ return list_to_clean
diff --git a/modular_skyrat/modules/loadouts/readme.md b/modular_skyrat/modules/loadouts/readme.md
new file mode 100644
index 00000000000..f3ce555ea8f
--- /dev/null
+++ b/modular_skyrat/modules/loadouts/readme.md
@@ -0,0 +1,12 @@
+## Title: MrMelbert's loadout system!
+
+MODULE ID: LOADOUT
+
+### Description:
+
+A loadout system that uses TGUI to equip players post roundstart.
+
+
+### Credits:
+MrMelbert - https://github.com/Jolly-66/JollyStationCode/pull/78 (original code)
+Gandalf2k15 - Porting
diff --git a/modular_skyrat/modules/mentor/code/client_procs.dm b/modular_skyrat/modules/mentor/code/client_procs.dm
index 56d95344ef6..5d68874444f 100644
--- a/modular_skyrat/modules/mentor/code/client_procs.dm
+++ b/modular_skyrat/modules/mentor/code/client_procs.dm
@@ -7,17 +7,6 @@
GLOB.mentors -= src
return ..()
-/client/verb/toggle_auto_dementor()
- set name = "Toggle Admin Auto-Dementor"
- set category = "Mentor"
-
- if(!holder || !check_rights_for(src, R_ADMIN))
- return
-
- prefs.auto_dementor = !prefs.auto_dementor
- to_chat(src, "You just toggled Auto-Dementor [(prefs.auto_dementor) ? "on" : "off"].")
- prefs.save_preferences()
-
/client/proc/mentor_client_procs(href_list)
if(href_list["mentor_msg"])
if(CONFIG_GET(flag/mentors_mobname_only))
@@ -48,7 +37,7 @@
mentor_datum.owner = src
GLOB.mentors[src] = TRUE
add_mentor_verbs()
- if(check_rights_for(src, R_ADMIN,0) && prefs.auto_dementor)
+ if(check_rights_for(src, R_ADMIN,0) && prefs.read_preference(/datum/preference/toggle/admin/auto_dementor))
cmd_mentor_dementor()
/client/proc/is_mentor() // admins are mentors too.
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/arousal_system.dm b/modular_skyrat/modules/modular_items/lewd_items/code/arousal_system.dm
index 7df48414674..d4169aa2e9a 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/arousal_system.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/arousal_system.dm
@@ -177,7 +177,7 @@
/datum/status_effect/body_fluid_regen/tick()
var/mob/living/carbon/human/H = owner
- if(owner.stat != DEAD && H.client?.prefs.sextoys_pref == "Yes")
+ if(owner.stat != DEAD && H.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
var/obj/item/organ/genital/testicles/balls = owner.getorganslot(ORGAN_SLOT_TESTICLES)
var/obj/item/organ/genital/breasts/breasts = owner.getorganslot(ORGAN_SLOT_BREASTS)
var/obj/item/organ/genital/vagina/vagina = owner.getorganslot(ORGAN_SLOT_VAGINA)
@@ -213,7 +213,7 @@
return arousal
/mob/living/carbon/human/proc/adjustArousal(arous = 0)
- if(stat != DEAD && client?.prefs.sextoys_pref == "Yes")
+ if(stat != DEAD && client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
arousal += arous
var/arousal_flag = AROUSAL_NONE
@@ -289,7 +289,7 @@
return pain
/mob/living/carbon/human/proc/adjustPain(pn = 0)
- if(stat != DEAD && client?.prefs.sextoys_pref == "Yes")
+ if(stat != DEAD && client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(pain > pain_limit || pn > pain_limit / 10) // pain system // YOUR SYSTEM IS PAIN, WHY WE'RE GETTING AROUSED BY STEPPING ON ANTS?!
if(HAS_TRAIT(src, TRAIT_MASOCHISM))
var/p = pn - (pain_limit / 10)
@@ -316,7 +316,7 @@
return pleasure
/mob/living/carbon/human/proc/adjustPleasure(pleas = 0)
- if(stat != DEAD && client?.prefs.sextoys_pref == "Yes")
+ if(stat != DEAD && client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
pleasure += pleas
if(pleasure >= 100) // lets cum
climax(FALSE)
@@ -329,7 +329,7 @@
. = ..()
if(!.)
return
- if(H.client?.prefs.sextoys_pref == "No")
+ if(H.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
return
var/hit_percent = (100-(blocked+armor))/100
hit_percent = (hit_percent * (100-H.physiology.damage_resistance))/100
@@ -363,7 +363,7 @@
/mob/living/carbon/human/proc/climax(manual = TRUE)
var/obj/item/organ/genital/penis = getorganslot(ORGAN_SLOT_PENIS)
var/obj/item/organ/genital/vagina = getorganslot(ORGAN_SLOT_VAGINA)
- if(manual == TRUE && !has_status_effect(/datum/status_effect/climax_cooldown) && client?.prefs.sextoys_pref == "Yes")
+ if(manual == TRUE && !has_status_effect(/datum/status_effect/climax_cooldown) && client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(!HAS_TRAIT(src, TRAIT_NEVERBONER) && !has_status_effect(/datum/status_effect/climax_cooldown))
switch(gender)
if(MALE)
@@ -426,7 +426,7 @@
span_purple("You can't have an orgasm!"))
return TRUE
- else if(manual == FALSE && client?.prefs.sextoys_pref == "Yes")
+ else if(manual == FALSE && client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(!HAS_TRAIT(src, TRAIT_NEVERBONER) && !has_status_effect(/datum/status_effect/climax_cooldown))
switch(gender)
if(MALE)
@@ -485,7 +485,7 @@
/datum/status_effect/masturbation_climax/tick() //this one should not leave decals on the floor. Used in case if character cumming on somebody's face or in beaker.
var/mob/living/carbon/human/H = owner
- if(H.client?.prefs.sextoys_pref == "Yes")
+ if(H.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
var/temp_arousal = -12
var/temp_pleasure = -12
var/temp_stamina = 8
@@ -503,7 +503,7 @@
/datum/status_effect/climax/tick()
var/mob/living/carbon/human/H = owner
- if(H.client?.prefs.sextoys_pref == "Yes")
+ if(H.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
var/temp_arousal = -12
var/temp_pleasure = -12
var/temp_stamina = 15
@@ -519,7 +519,7 @@
var/obj/item/organ/genital/testicles/balls = owner.getorganslot(ORGAN_SLOT_TESTICLES)
var/obj/item/organ/genital/testicles/penis = owner.getorganslot(ORGAN_SLOT_PENIS)
- if((H.client?.prefs.sextoys_pref == "Yes") && (H.stat != DEAD))
+ if((H.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy)) && (H.stat != DEAD))
if(penis && balls && H.wear_condom())
if(prob(40))
H.emote("moan")
@@ -602,7 +602,7 @@
/obj/item/organ/brain/on_life(delta_time, times_fired) //All your horny is here *points to the head*
. = ..()
var/mob/living/carbon/human/brain_owner = owner
- if(istype(brain_owner, /mob/living/carbon/human) && brain_owner.client?.prefs.sextoys_pref == "Yes")
+ if(istype(brain_owner, /mob/living/carbon/human) && brain_owner.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(!(organ_flags & ORGAN_FAILING))
brain_owner.dna.species.handle_arousal(brain_owner, delta_time, times_fired)
@@ -652,7 +652,7 @@
/datum/species/proc/handle_arousal(mob/living/carbon/human/target_human, atom/movable/screen/alert/aroused_X)
var/atom/movable/screen/alert/aroused_X/arousal_alert = target_human.alerts["aroused"]
- if(target_human.client?.prefs.sextoys_pref == "Yes")
+ if(target_human.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(target_human.arousal)
if(-100 to 1)
target_human.clear_alert("aroused", /atom/movable/screen/alert/aroused_X)
@@ -843,7 +843,7 @@
if(!human_cumvictim.client)
to_chat(user, span_warning("You can't cum onto [M]."))
return
- if(!(human_cumvictim.client.prefs.skyrat_toggles & CUMFACE_PREF)) //im just paranoid about runtime errors
+ if(!(human_cumvictim.client.prefs.read_preference(/datum/preference/toggle/erp/cum_face))) //im just paranoid about runtime errors
to_chat(user, span_warning("You can't cum onto [M]."))
return
var/mob/living/carbon/human/H = user
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_chemistry/chemistry_for_ERP.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_chemistry/chemistry_for_ERP.dm
index ee44cb40837..6e7ee70a357 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_chemistry/chemistry_for_ERP.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_chemistry/chemistry_for_ERP.dm
@@ -12,7 +12,7 @@
//can_synth = FALSE
/datum/reagent/drug/crocin/on_mob_life(mob/living/carbon/human/M)
- if(M.client && (M.client.prefs.skyrat_toggles & APHRO_PREF))
+ if(M.client && (M.client.prefs.read_preference(/datum/preference/toggle/erp/aphro)))
if((prob(min(current_cycle/2,5))))
M.emote(pick("moan","blush"))
if(prob(min(current_cycle/4,10)))
@@ -38,7 +38,7 @@
overdose_threshold = 25 //Heavy consequences. Supposed to be big value.
/datum/reagent/drug/hexacrocin/on_mob_life(mob/living/carbon/human/M)
- if(M.client && (M.client.prefs.skyrat_toggles & APHRO_PREF))
+ if(M.client && (M.client.prefs.read_preference(/datum/preference/toggle/erp/aphro)))
if(prob(5))
if(prob(current_cycle))
M.say(pick("Hnnnnngghh...", "Ohh...", "Mmnnn...","Ghhmph..."))
@@ -64,7 +64,7 @@
/datum/reagent/drug/hexacrocin/overdose_process(mob/living/carbon/human/M)
var/mob/living/carbon/human/H = M
- if(M.client && (M.client.prefs.skyrat_toggles & BIMBO_PREF))
+ if(M.client && (M.client.prefs.read_preference(/datum/preference/toggle/erp/bimbofication)))
if(prob(5) && ishuman(M) && !HAS_TRAIT(M, TRAIT_BIMBO) && !HAS_TRAIT(M, TRAIT_SOBSESSED)/* && M.has_dna() && some shit about bimbofication*/) //yes, pal. an i'm the horseman of the Apocalypse that will make it work. Sorry.
to_chat(M, span_purple("Your libido is going haywire! Speaking gets much harder..."))
H.gain_trauma(/datum/brain_trauma/special/bimbo, TRAUMA_RESILIENCE_BASIC) //what am i doing with my life.
@@ -90,7 +90,7 @@
..()
/datum/reagent/drug/dopamine/on_mob_life(mob/living/carbon/human/M)
- if(M.client && (M.client.prefs.skyrat_toggles & APHRO_PREF))
+ if(M.client && (M.client.prefs.read_preference(/datum/preference/toggle/erp/aphro)))
M.set_drugginess(5)
if(prob(7))
M.emote(pick("twitch","drool","moan","giggle","shaking"))
@@ -125,7 +125,7 @@
reagent_state = SOLID
/datum/reagent/drug/camphor/on_mob_life(mob/living/carbon/human/M)
- if(M.client && (M.client.prefs.skyrat_toggles & APHRO_PREF))
+ if(M.client && (M.client.prefs.read_preference(/datum/preference/toggle/erp/aphro)))
if(ishuman(M))
var/old_arousal = M.arousal
M.adjustArousal(-12)
@@ -145,7 +145,7 @@
overdose_threshold = 20
/datum/reagent/drug/pentacamphor/on_mob_life(mob/living/carbon/human/M)
- if(M.client && (M.client.prefs.skyrat_toggles & APHRO_PREF))
+ if(M.client && (M.client.prefs.read_preference(/datum/preference/toggle/erp/aphro)))
if(ishuman(M))
var/old_arousal = M.arousal
M.adjustArousal(-18)
@@ -161,7 +161,7 @@
/datum/reagent/drug/pentacamphor/overdose_process(mob/living/carbon/human/M)
var/mob/living/carbon/human/H = M
- if(M.client && (M.client.prefs.skyrat_toggles & APHRO_PREF))
+ if(M.client && (M.client.prefs.read_preference(/datum/preference/toggle/erp/aphro)))
if(!HAS_TRAIT(M, TRAIT_BIMBO) && !HAS_TRAIT(M, TRAIT_NEVERBONER))
to_chat(M, span_notice("You feel like you'll never feel aroused again...")) //Go to horny jail *bonk*
ADD_TRAIT(M,TRAIT_NEVERBONER, LEWDCHEM_TRAIT)
@@ -232,7 +232,7 @@
if(!ishuman(M))//Just in case
return..()
- if(H.client?.prefs.skyrat_toggles & BREAST_ENLARGEMENT)
+ if(H.client?.prefs.read_preference(/datum/preference/toggle/erp/breast_enlargement))
M.breast_enlarger_amount += 5
if(M.breast_enlarger_amount >= 100)
if(B?.genital_size < 16)
@@ -257,7 +257,7 @@
H.apply_damage(1, BRUTE, target)
//If they've opted out, then route processing though liver.
- if(!(H.client?.prefs.skyrat_toggles & BREAST_ENLARGEMENT))
+ if(!(H.client?.prefs.read_preference(/datum/preference/toggle/erp/breast_enlargement)))
var/obj/item/organ/liver/L = H.getorganslot(ORGAN_SLOT_LIVER)
if(L)
L.applyOrganDamage(0.25)
@@ -269,7 +269,7 @@
/datum/reagent/breast_enlarger/overdose_process(mob/living/carbon/human/M) //Turns you into a female if character is male. Also supposed to add breasts but i'm too dumb to figure out how to make it work
var/obj/item/organ/genital/penis/P = M.getorganslot(ORGAN_SLOT_PENIS)
var/obj/item/organ/genital/testicles/T = M.getorganslot(ORGAN_SLOT_TESTICLES)
- if(!(M.client?.prefs.skyrat_toggles & FORCED_FEM))
+ if(!(M.client?.prefs.read_preference(/datum/preference/toggle/erp/gender_change)))
var/obj/item/organ/liver/L = M.getorganslot(ORGAN_SLOT_LIVER)
if(L)
L.applyOrganDamage(0.25)
@@ -372,7 +372,7 @@ Haha! Kill me please.
if(!ishuman(M))
return ..()
- if(H.client?.prefs.skyrat_toggles & PENIS_ENLARGEMENT)
+ if(H.client?.prefs.read_preference(/datum/preference/toggle/erp/penis_enlargement))
M.penis_enlarger_amount += 5
if(M.penis_enlarger_amount >= 100)
if(P?.genital_size < 20)
@@ -396,7 +396,7 @@ Haha! Kill me please.
H.apply_damage(1, BRUTE, target)
//If they've opted out, then route processing though liver.
- if(!(H.client?.prefs.skyrat_toggles & PENIS_ENLARGEMENT))
+ if(!(H.client?.prefs.read_preference(/datum/preference/toggle/erp/penis_enlargement)))
var/obj/item/organ/liver/L = H.getorganslot(ORGAN_SLOT_LIVER)
if(L)
L.applyOrganDamage(0.25)
@@ -409,7 +409,7 @@ Haha! Kill me please.
if(!istype(M))
return ..()
// let's not kill them if they didn't consent.
- if(!(M.client?.prefs.skyrat_toggles & FORCED_MALE))
+ if(!(M.client?.prefs.read_preference(/datum/preference/toggle/erp/gender_change)))
return..()
var/obj/item/organ/genital/breasts/B = M.getorganslot(ORGAN_SLOT_BREASTS)
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/ballgag.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/ballgag.dm
index 35a9c28a7ae..3c539a7e650 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/ballgag.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/ballgag.dm
@@ -172,7 +172,7 @@
/obj/item/clothing/mask/ballgag/equipped(mob/user, slot)
if(chokes_wearer)
var/mob/living/carbon/human/U = loc
- if(src == U.wear_mask && U.client?.prefs.sextoys_pref == "Yes") //To prevent abusing this thing on non-erp players. We care about them, yes.
+ if(src == U.wear_mask && U.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy)) //To prevent abusing this thing on non-erp players. We care about them, yes.
START_PROCESSING(SSobj, src)
return ..()
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/bdsm_mask.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/bdsm_mask.dm
index 51f04d95224..5a4eb031382 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/bdsm_mask.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/bdsm_mask.dm
@@ -226,7 +226,7 @@
update_icon()
var/mob/living/carbon/human/C = usr
if(mask_on)
- if(src == C.wear_mask && C.client?.prefs.sextoys_pref == "Yes")
+ if(src == C.wear_mask && C.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
START_PROCESSING(SSobj, src)
time_to_choke_left = time_to_choke
else
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/hypnogoggles.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/hypnogoggles.dm
index 8cdfe8ef14e..4c8121e7583 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/hypnogoggles.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/hypnogoggles.dm
@@ -19,7 +19,7 @@
victim = user
if(slot != ITEM_SLOT_EYES)
return
- if(iscarbon(victim) && victim.client?.prefs.sextoys_pref == "Yes")
+ if(iscarbon(victim) && victim.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(codephrase != "")
victim.gain_trauma(new /datum/brain_trauma/induced_hypnosis(codephrase), TRAUMA_RESILIENCE_BASIC)
else
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/strapon.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/strapon.dm
index 48aac5b073d..3058a8c425f 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/strapon.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/strapon.dm
@@ -200,7 +200,7 @@
var/message = ""
var/obj/item/organ/genital/vagina = M.getorganslot(ORGAN_SLOT_VAGINA)
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna fuck
if(BODY_ZONE_PRECISE_GROIN)
if(vagina)
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/dildo.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/dildo.dm
index 7e9315a8aca..215535f0c0e 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/dildo.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/dildo.dm
@@ -84,7 +84,7 @@
var/message = ""
var/obj/item/organ/genital/vagina = M.getorganslot(ORGAN_SLOT_VAGINA)
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna fuck
if(BODY_ZONE_PRECISE_GROIN)
if(vagina)
@@ -288,7 +288,7 @@ GLOBAL_LIST_INIT(dildo_colors, list(//mostly neon colors
var/message = ""
var/obj/item/organ/genital/vagina = M.getorganslot(ORGAN_SLOT_VAGINA)
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna fuck
if(BODY_ZONE_PRECISE_GROIN)
if(vagina)
@@ -529,7 +529,7 @@ GLOBAL_LIST_INIT(dildo_colors, list(//mostly neon colors
var/message = ""
var/obj/item/organ/genital/vagina = M.getorganslot(ORGAN_SLOT_VAGINA)
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna fuck
if(BODY_ZONE_PRECISE_GROIN)
if(vagina)
@@ -617,7 +617,7 @@ GLOBAL_LIST_INIT(dildo_colors, list(//mostly neon colors
var/message = ""
var/obj/item/organ/genital/vagina = M.getorganslot(ORGAN_SLOT_VAGINA)
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna fuck
if(BODY_ZONE_PRECISE_GROIN)
if(vagina)
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/fleshlight.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/fleshlight.dm
index f81264740bf..fb4d010c0c8 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/fleshlight.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/fleshlight.dm
@@ -62,7 +62,7 @@
return
var/message = ""
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna... Uhh... You get the point.
if(BODY_ZONE_PRECISE_GROIN)
var/obj/item/organ/genital/penis = M.getorganslot(ORGAN_SLOT_PENIS)
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/kinky_shocker.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/kinky_shocker.dm
index 46d93e54497..fb85ae134cb 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/kinky_shocker.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/kinky_shocker.dm
@@ -104,7 +104,7 @@
if(shocker_on == TRUE)
var/message = ""
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
deductcharge(cell_hit_cost)
playsound(loc, 'sound/weapons/taserhit.ogg', 70, 1, -1)
switch(user.zone_selected) //to let code know what part of body we gonna tickle
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/leather_whip.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/leather_whip.dm
index ebfd530b312..e9a42b1cf62 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/leather_whip.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/leather_whip.dm
@@ -158,7 +158,7 @@
var/message = ""
//and there is code for successful check, so we are whipping someone
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna whip
if(BODY_ZONE_L_LEG)
if(M.has_feet())
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/magic_wand.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/magic_wand.dm
index 7e27c942021..9774c6c8671 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/magic_wand.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/magic_wand.dm
@@ -82,7 +82,7 @@
var/message = ""
if(toy_on == TRUE)
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna... Yeah.
if(BODY_ZONE_PRECISE_GROIN)
var/obj/item/organ/genital/penis = M.getorganslot(ORGAN_SLOT_PENIS)
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/spanking_pad.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/spanking_pad.dm
index 95bae13529e..a09bb3744ba 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/spanking_pad.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/spanking_pad.dm
@@ -62,12 +62,12 @@
return
var/message = ""
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna spank.
if(BODY_ZONE_PRECISE_GROIN)
if(M.is_bottomless())
message = (user == M) ? pick("spanks themselves with [src]","uses [src] to slap their hips") : pick("slaps [M]'s hips with [src]", "uses [src] to slap [M]'s butt","spanks [M] with [src], making a loud slapping noise","slaps [M]'s thighs with [src]")
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(prob(40) && (M.stat != DEAD))
M.emote(pick("twitch_s","moan","blush","gasp"))
M.adjustArousal(2)
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/torture candle.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/torture candle.dm
index 4d814c9e80a..680ad9c8fd6 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/torture candle.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/torture candle.dm
@@ -123,7 +123,7 @@
var/message = ""
if(lit == TRUE)
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna wax
if(BODY_ZONE_PRECISE_GROIN)
var/obj/item/organ/genital/penis = M.getorganslot(ORGAN_SLOT_PENIS)
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/vibrator.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/vibrator.dm
index 7780b041f7e..678af8067ff 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/vibrator.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_items/vibrator.dm
@@ -110,7 +110,7 @@
var/message = ""
if(toy_on == TRUE)
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
switch(user.zone_selected) //to let code know what part of body we gonna vibe
if(BODY_ZONE_PRECISE_GROIN)
var/obj/item/organ/genital/penis = M.getorganslot(ORGAN_SLOT_PENIS)
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_loadout.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_loadout.dm
deleted file mode 100644
index 60a7d54d204..00000000000
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_loadout.dm
+++ /dev/null
@@ -1,27 +0,0 @@
-//////////////////////////////////
-///Loadout items from our update///
-//////////////////////////////////
-
-//uniform
-/datum/loadout_item/uniform/latexmaid
- name = "Latex maid uniform"
- path = /obj/item/clothing/under/costume/lewdmaid
-
-/datum/loadout_item/uniform/stripper
- name = "Stripper outfit"
- path = /obj/item/clothing/under/stripper_outfit
-
-//head
-/datum/loadout_item/head/dominacap
- name = "Dominant cap"
- path = /obj/item/clothing/head/domina_cap
-
-//shoes
-/datum/loadout_item/shoes/dominaheels
- name = "Dominant heels"
- path = /obj/item/clothing/shoes/dominaheels
-
-//neck
-/datum/loadout_item/neck/kinkycollar
- name = "Kinky collar"
- path = /obj/item/clothing/neck/kink_collar
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_quirks.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_quirks.dm
index f5c6e612969..779a8a81bd8 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_quirks.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_quirks.dm
@@ -16,6 +16,7 @@
medical_record_text = "Subject has nymphomania."
var/obj/item/sextoy
var/where
+ icon = "grin-hearts"
//nymphomania players need to satisfy lust, so they need "tools" to "cool" them from time to time. In case if there is NO PLAYERS AROUND.
/datum/quirk/nymphomania/add_unique()
@@ -60,6 +61,7 @@
var/satisfaction = 1000
var/stress = 0
+
/datum/brain_trauma/special/nymphomania/on_gain()
var/mob/living/carbon/human/H = owner
ADD_TRAIT(H, TRAIT_NYMPHOMANIA, APHRO_TRAIT)
@@ -278,7 +280,7 @@ But i keeped it as unobtainable breain trauma, so admins can add it through VV *
for(var/mob/Player in GLOB.player_list)//prevents crewmembers falling in love with nuke ops they never met, and other annoying hijinks
if(Player.mind && Player.stat != DEAD && !isnewplayer(Player) && !isbrain(Player) && Player.client && Player != owner && SSjob.GetJob(Player.mind.assigned_role))
- if(Player.client.prefs?.erp_pref == "Yes" && Player.client.prefs?.noncon_pref == "Yes")
+ if(Player.client.prefs?.read_preference(/datum/preference/toggle/erp/noncon))
viable_minds += Player.mind
for(var/datum/mind/possible_target in viable_minds)
if(possible_target != owner && ishuman(possible_target.current))
@@ -442,6 +444,7 @@ But i keeped it as unobtainable breain trauma, so admins can add it through VV *
gain_text = span_danger("You have a sudden desire for pain...")
lose_text = span_notice("Ouch! Pain is... Painful again! Ou-ou-ouch!")
medical_record_text = "Subject has masochism."
+ icon = "hammer"
/datum/quirk/masochism/post_add()
. = ..()
@@ -488,6 +491,7 @@ But i keeped it as unobtainable breain trauma, so admins can add it through VV *
gain_text = span_danger("You feel a sudden desire to inflict pain.")
lose_text = span_notice("Others' pain doesn't satisfy you anymore.")
medical_record_text = "Subject has sadism."
+ icon = "angry"
/datum/quirk/sadism/post_add()
. = ..()
@@ -511,7 +515,7 @@ But i keeped it as unobtainable breain trauma, so admins can add it through VV *
/datum/brain_trauma/special/sadism/on_life(delta_time, times_fired)
var/mob/living/carbon/human/H = owner
- if(someone_suffering() && H.client?.prefs.erp_pref == "Yes")
+ if(someone_suffering() && H.client?.prefs?.read_preference(/datum/preference/toggle/erp))
H.adjustArousal(2)
SEND_SIGNAL(owner, COMSIG_ADD_MOOD_EVENT, "sadistic", /datum/mood_event/sadistic)
else
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/technical_stuff_for_lewd.dm b/modular_skyrat/modules/modular_items/lewd_items/code/technical_stuff_for_lewd.dm
index b514a7ed72c..87b0df55859 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/technical_stuff_for_lewd.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/technical_stuff_for_lewd.dm
@@ -531,7 +531,7 @@
///////////////////////////////////////////////////////////////
/mob/living/proc/set_gender(ngender = NEUTER, silent = FALSE, update_icon = TRUE, forced = FALSE)
- if(forced || (!ckey || client?.prefs.skyrat_toggles & (ngender == FEMALE ? FORCED_FEM : FORCED_MALE)))
+ if(forced || (!ckey || client?.prefs.read_preference(/datum/preference/toggle/erp/gender_change)))
gender = ngender
return TRUE
return FALSE
@@ -707,7 +707,7 @@
// Updating vagina slot
/mob/living/carbon/human/update_inv_vagina()
- if(client?.prefs?.sextoys_pref == "Yes")
+ if(client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(client && hud_used && hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_VAGINA) + 1])
var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_VAGINA) + 1]
inv.update_icon()
@@ -746,7 +746,7 @@
// Updating anus slot
/mob/living/carbon/human/update_inv_anus()
- if(client?.prefs?.sextoys_pref == "Yes")
+ if(client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(client && hud_used && hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_ANUS) + 1])
var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_ANUS) + 1]
inv.update_icon()
@@ -785,7 +785,7 @@
// Updating nipples slot
/mob/living/carbon/human/update_inv_nipples()
- if(client?.prefs?.sextoys_pref == "Yes")
+ if(client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(client && hud_used && hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_NIPPLES) + 1])
var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_NIPPLES) + 1]
inv.update_icon()
@@ -824,7 +824,7 @@
// Updating penis slot
/mob/living/carbon/human/update_inv_penis()
- if(client?.prefs?.sextoys_pref == "Yes")
+ if(client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
if(client && hud_used && hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_PENIS) + 1])
var/atom/movable/screen/inventory/inv = hud_used.inv_slots[TOBITSHIFT(ITEM_SLOT_PENIS) + 1]
inv.update_icon()
@@ -1009,7 +1009,7 @@
// Obscuring for ERP slots
/datum/strippable_item/mob_item_slot/vagina/get_obscuring(atom/source)
var/mob/M = source
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
return isnull(get_item(source)) \
? STRIPPABLE_OBSCURING_NONE \
: STRIPPABLE_OBSCURING_HIDDEN
@@ -1018,7 +1018,7 @@
// Obscuring for ERP slots
/datum/strippable_item/mob_item_slot/anus/get_obscuring(atom/source)
var/mob/M = source
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
return isnull(get_item(source)) \
? STRIPPABLE_OBSCURING_NONE \
: STRIPPABLE_OBSCURING_HIDDEN
@@ -1027,7 +1027,7 @@
// Obscuring for ERP slots
/datum/strippable_item/mob_item_slot/nipples/get_obscuring(atom/source)
var/mob/M = source
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
return isnull(get_item(source)) \
? STRIPPABLE_OBSCURING_NONE \
: STRIPPABLE_OBSCURING_HIDDEN
@@ -1036,7 +1036,7 @@
// Obscuring for ERP slots
/datum/strippable_item/mob_item_slot/penis/get_obscuring(atom/source)
var/mob/M = source
- if(M.client?.prefs.sextoys_pref == "Yes")
+ if(M.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
return isnull(get_item(source)) \
? STRIPPABLE_OBSCURING_NONE \
: STRIPPABLE_OBSCURING_HIDDEN
diff --git a/modular_skyrat/modules/verbs/code/modules/client/preferences_toggles.dm b/modular_skyrat/modules/verbs/code/modules/client/preferences_toggles.dm
deleted file mode 100644
index dac3338d6b7..00000000000
--- a/modular_skyrat/modules/verbs/code/modules/client/preferences_toggles.dm
+++ /dev/null
@@ -1,20 +0,0 @@
-
-/client/verb/listen_looc()
- set name = "Show/Hide LOOC"
- set category = "Preferences"
- set desc = "Toggles seeing LocalOutOfCharacter chat"
- usr.client.prefs.skyrat_toggles ^= CHAT_LOOC
- usr.client.prefs.save_preferences()
- to_chat(usr, "You will [(usr.client.prefs.skyrat_toggles & CHAT_LOOC) ? "now" : "no longer"] see messages on the LOOC channel. ")
- SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Seeing LOOC", "[usr.client.prefs.skyrat_toggles & CHAT_LOOC ? "Enabled" : "Disabled"]"))
-
-/client/verb/admin_delete_zap_pref()
- set name = "Toggle Delete Sparks"
- set category = "Preferences.Admin"
- set desc = "Toggles the appearance of bluespace zaps when you use the Delete command on stuff. Also applies to simple/advanced/del buildmode."
- if(!holder)
- return
- usr.client.prefs.skyrat_toggles ^= ADMINDEL_ZAP_PREF
- usr.client.prefs.save_preferences()
- to_chat(usr, "There will [(usr.client.prefs.skyrat_toggles & ADMINDEL_ZAP_PREF) ? "now" : "no longer"] be bluespace sparks when you Delete objects. ")
- SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Delete Sparks", "[usr.client.prefs.skyrat_toggles & ADMINDEL_ZAP_PREF ? "Enabled" : "Disabled"]"))
diff --git a/modular_skyrat/modules/verbs/code/modules/client/verbs/looc.dm b/modular_skyrat/modules/verbs/code/modules/client/verbs/looc.dm
index 5804e7d49b2..3acd84a12de 100644
--- a/modular_skyrat/modules/verbs/code/modules/client/verbs/looc.dm
+++ b/modular_skyrat/modules/verbs/code/modules/client/verbs/looc.dm
@@ -67,16 +67,5 @@
var/client/C = cli
if (admin_seen[C])
to_chat(C, "[ADMIN_FLW(usr)] LOOC: [src.key]/[src.mob.name]: [msg] ")
- else if (C.prefs.skyrat_toggles & CHAT_LOOC_ADMIN)
+ else if (C.prefs.read_preference(/datum/preference/toggle/admin/see_looc))
to_chat(C, "[ADMIN_FLW(usr)] (R)LOOC: [src.key]/[src.mob.name]: [msg] ")
-
-/client/proc/toggle_admin_looc_global()
- set name = "See/Hide Global LOOC"
- set category = "Preferences.Admin"
- set desc = "Show Global LOOC"
- if(!holder)
- return
- prefs.skyrat_toggles ^= CHAT_LOOC_ADMIN
- prefs.save_preferences()
- to_chat(usr, "You will [(prefs.skyrat_toggles & CHAT_LOOC_ADMIN) ? "now" : "no longer"] hear LOOC globally. ")
- SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Togle Admin LOOC", "[usr.client.prefs.skyrat_toggles & CHAT_LOOC_ADMIN ? "Enabled" : "Disabled"]"))
diff --git a/modular_skyrat/readme.md b/modular_skyrat/readme.md
index 1ebec9779fb..ffa3b617964 100644
--- a/modular_skyrat/readme.md
+++ b/modular_skyrat/readme.md
@@ -42,11 +42,7 @@ but then our upstream introduces a change in their codebase, changing it from 1
As easy of an example as it is, it results in a relatively simple conflict, in the form of
```byond
-<<<<<<< HEAD
var/something = 2 //SKYRAT EDIT
-=======
-var/something = 4
->>>>>>> upstream-mirror-123132
```
where we pick the preferable option manually.
diff --git a/tgstation.dme b/tgstation.dme
index abc9478e732..3f2d3849f38 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -96,7 +96,6 @@
#include "code\__DEFINES\mecha.dm"
#include "code\__DEFINES\melee.dm"
#include "code\__DEFINES\memory_defines.dm"
-#include "code\__DEFINES\menu.dm"
#include "code\__DEFINES\misc.dm"
#include "code\__DEFINES\mobs.dm"
#include "code\__DEFINES\monkeys.dm"
@@ -173,8 +172,10 @@
#include "code\__DEFINES\research\anomalies.dm"
#include "code\__DEFINES\~skyrat_defines\ambitions.dm"
#include "code\__DEFINES\~skyrat_defines\ammo_defines.dm"
+#include "code\__DEFINES\~skyrat_defines\augment.dm"
#include "code\__DEFINES\~skyrat_defines\banning.dm"
#include "code\__DEFINES\~skyrat_defines\cells.dm"
+#include "code\__DEFINES\~skyrat_defines\chat.dm"
#include "code\__DEFINES\~skyrat_defines\combat.dm"
#include "code\__DEFINES\~skyrat_defines\culture.dm"
#include "code\__DEFINES\~skyrat_defines\DNA.dm"
@@ -189,6 +190,7 @@
#include "code\__DEFINES\~skyrat_defines\jobs.dm"
#include "code\__DEFINES\~skyrat_defines\keybindings.dm"
#include "code\__DEFINES\~skyrat_defines\liquids.dm"
+#include "code\__DEFINES\~skyrat_defines\loadouts.dm"
#include "code\__DEFINES\~skyrat_defines\logging.dm"
#include "code\__DEFINES\~skyrat_defines\maintenance_loot.dm"
#include "code\__DEFINES\~skyrat_defines\mapping.dm"
@@ -197,9 +199,9 @@
#include "code\__DEFINES\~skyrat_defines\mining_traits.dm"
#include "code\__DEFINES\~skyrat_defines\obj_flags.dm"
#include "code\__DEFINES\~skyrat_defines\pollution.dm"
-#include "code\__DEFINES\~skyrat_defines\preferences.dm"
#include "code\__DEFINES\~skyrat_defines\projectiles.dm"
#include "code\__DEFINES\~skyrat_defines\robot_defines.dm"
+#include "code\__DEFINES\~skyrat_defines\role_preferences.dm"
#include "code\__DEFINES\~skyrat_defines\say.dm"
#include "code\__DEFINES\~skyrat_defines\security_alerts.dm"
#include "code\__DEFINES\~skyrat_defines\signals.dm"
@@ -208,11 +210,13 @@
#include "code\__HELPERS\_lists.dm"
#include "code\__HELPERS\_logging.dm"
#include "code\__HELPERS\_string_lists.dm"
+#include "code\__HELPERS\admin.dm"
#include "code\__HELPERS\areas.dm"
#include "code\__HELPERS\atmospherics.dm"
#include "code\__HELPERS\bitflag_lists.dm"
#include "code\__HELPERS\chat.dm"
#include "code\__HELPERS\cmp.dm"
+#include "code\__HELPERS\colors.dm"
#include "code\__HELPERS\config.dm"
#include "code\__HELPERS\construction.dm"
#include "code\__HELPERS\dates.dm"
@@ -276,6 +280,7 @@
#include "code\_globalvars\tgui.dm"
#include "code\_globalvars\time_vars.dm"
#include "code\_globalvars\traits.dm"
+#include "code\_globalvars\traumas.dm"
#include "code\_globalvars\lists\achievements.dm"
#include "code\_globalvars\lists\ambience.dm"
#include "code\_globalvars\lists\client.dm"
@@ -369,6 +374,7 @@
#include "code\controllers\subsystem\dcs.dm"
#include "code\controllers\subsystem\discord.dm"
#include "code\controllers\subsystem\disease.dm"
+#include "code\controllers\subsystem\early_assets.dm"
#include "code\controllers\subsystem\economy.dm"
#include "code\controllers\subsystem\eigenstate.dm"
#include "code\controllers\subsystem\events.dm"
@@ -397,6 +403,7 @@
#include "code\controllers\subsystem\parallax.dm"
#include "code\controllers\subsystem\pathfinder.dm"
#include "code\controllers\subsystem\persistence.dm"
+#include "code\controllers\subsystem\persistent_paintings.dm"
#include "code\controllers\subsystem\profiler.dm"
#include "code\controllers\subsystem\radiation.dm"
#include "code\controllers\subsystem\radio.dm"
@@ -420,7 +427,6 @@
#include "code\controllers\subsystem\time_track.dm"
#include "code\controllers\subsystem\timer.dm"
#include "code\controllers\subsystem\title.dm"
-#include "code\controllers\subsystem\traumas.dm"
#include "code\controllers\subsystem\vis_overlays.dm"
#include "code\controllers\subsystem\vote.dm"
#include "code\controllers\subsystem\weather.dm"
@@ -2126,9 +2132,64 @@
#include "code\modules\client\message.dm"
#include "code\modules\client\player_details.dm"
#include "code\modules\client\preferences.dm"
+#include "code\modules\client\preferences_menu.dm"
#include "code\modules\client\preferences_savefile.dm"
-#include "code\modules\client\preferences_toggles.dm"
-#include "code\modules\client\verbs\etips.dm"
+#include "code\modules\client\preferences\_preference.dm"
+#include "code\modules\client\preferences\admin.dm"
+#include "code\modules\client\preferences\age.dm"
+#include "code\modules\client\preferences\ai_core_display.dm"
+#include "code\modules\client\preferences\ambient_occlusion.dm"
+#include "code\modules\client\preferences\assets.dm"
+#include "code\modules\client\preferences\auto_fit_viewport.dm"
+#include "code\modules\client\preferences\body_type.dm"
+#include "code\modules\client\preferences\broadcast_login_logout.dm"
+#include "code\modules\client\preferences\buttons_locked.dm"
+#include "code\modules\client\preferences\clothing.dm"
+#include "code\modules\client\preferences\darkened_flash.dm"
+#include "code\modules\client\preferences\fps.dm"
+#include "code\modules\client\preferences\gender.dm"
+#include "code\modules\client\preferences\ghost.dm"
+#include "code\modules\client\preferences\hotkeys.dm"
+#include "code\modules\client\preferences\item_outlines.dm"
+#include "code\modules\client\preferences\jobless_role.dm"
+#include "code\modules\client\preferences\names.dm"
+#include "code\modules\client\preferences\ooc.dm"
+#include "code\modules\client\preferences\parallax.dm"
+#include "code\modules\client\preferences\pda.dm"
+#include "code\modules\client\preferences\phobia.dm"
+#include "code\modules\client\preferences\pixel_size.dm"
+#include "code\modules\client\preferences\playtime_reward_cloak.dm"
+#include "code\modules\client\preferences\preferred_map.dm"
+#include "code\modules\client\preferences\random.dm"
+#include "code\modules\client\preferences\runechat.dm"
+#include "code\modules\client\preferences\scaling_method.dm"
+#include "code\modules\client\preferences\screentips.dm"
+#include "code\modules\client\preferences\security_department.dm"
+#include "code\modules\client\preferences\skin_tone.dm"
+#include "code\modules\client\preferences\species.dm"
+#include "code\modules\client\preferences\tgui.dm"
+#include "code\modules\client\preferences\tgui_prefs_migration.dm"
+#include "code\modules\client\preferences\tooltips.dm"
+#include "code\modules\client\preferences\ui_style.dm"
+#include "code\modules\client\preferences\underwear_color.dm"
+#include "code\modules\client\preferences\uplink_location.dm"
+#include "code\modules\client\preferences\widescreen.dm"
+#include "code\modules\client\preferences\window_flashing.dm"
+#include "code\modules\client\preferences\middleware\_middleware.dm"
+#include "code\modules\client\preferences\middleware\antags.dm"
+#include "code\modules\client\preferences\middleware\jobs.dm"
+#include "code\modules\client\preferences\middleware\keybindings.dm"
+#include "code\modules\client\preferences\middleware\legacy_toggles.dm"
+#include "code\modules\client\preferences\middleware\names.dm"
+#include "code\modules\client\preferences\middleware\quirks.dm"
+#include "code\modules\client\preferences\middleware\random.dm"
+#include "code\modules\client\preferences\middleware\species.dm"
+#include "code\modules\client\preferences\species_features\basic.dm"
+#include "code\modules\client\preferences\species_features\ethereal.dm"
+#include "code\modules\client\preferences\species_features\felinid.dm"
+#include "code\modules\client\preferences\species_features\lizard.dm"
+#include "code\modules\client\preferences\species_features\moth.dm"
+#include "code\modules\client\preferences\species_features\mutants.dm"
#include "code\modules\client\verbs\ooc.dm"
#include "code\modules\client\verbs\ping.dm"
#include "code\modules\client\verbs\reset_held_keys.dm"
@@ -2871,7 +2932,6 @@
#include "code\modules\mob\living\carbon\human\species_types\zombies.dm"
#include "code\modules\mob\living\silicon\damage_procs.dm"
#include "code\modules\mob\living\silicon\death.dm"
-#include "code\modules\mob\living\silicon\examine.dm"
#include "code\modules\mob\living\silicon\laws.dm"
#include "code\modules\mob\living\silicon\login.dm"
#include "code\modules\mob\living\silicon\silicon.dm"
@@ -3893,7 +3953,6 @@
#include "interface\menu.dm"
#include "interface\stylesheet.dm"
#include "interface\skin.dmf"
-#include "modular_skyrat\master_files\code\__DEFINES\chat.dm"
#include "modular_skyrat\master_files\code\_globalvars\configuration.dm"
#include "modular_skyrat\master_files\code\_globalvars\maint_loot_common.dm"
#include "modular_skyrat\master_files\code\_globalvars\maint_loot_oddity.dm"
@@ -3939,6 +3998,16 @@
#include "modular_skyrat\master_files\code\modules\antagonists\wizard\equipment\spellbook.dm"
#include "modular_skyrat\master_files\code\modules\awaymission\mission_code\black_mesa.dm"
#include "modular_skyrat\master_files\code\modules\buildmode\submodes\offercontrol.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences_savefile.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\_admin.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\auto_dementor.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\be_antag.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\delete_sparks.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\erp_preferences.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\looc.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\species_features\generate_side_shots.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\species_features\mutant_colors.dm"
#include "modular_skyrat\master_files\code\modules\clothing\anthro_clothes.dm"
#include "modular_skyrat\master_files\code\modules\clothing\chameleon.dm"
#include "modular_skyrat\master_files\code\modules\clothing\clothing.dm"
@@ -3952,7 +4021,7 @@
#include "modular_skyrat\master_files\code\modules\mob\living\emote_popup.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\carbon\carbon_say.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\carbon\human_helpers.dm"
-#include "modular_skyrat\master_files\code\modules\mob\living\dead\new_player\new_player.dm"
+#include "modular_skyrat\master_files\code\modules\mob\living\human\species.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\simple_animal\simple_animal.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\simple_animal\friendly\bumbles.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\simple_animal\friendly\dogs.dm"
@@ -4089,9 +4158,6 @@
#include "modular_skyrat\modules\altborgs\code\modules\mob\living\silicon\robot\robot_model.dm"
#include "modular_skyrat\modules\altborgs\code\modules\mob\living\silicon\robot\robot_modules.dm"
#include "modular_skyrat\modules\altborgs\code\modules\mob\living\silicon\robot\update_icons.dm"
-#include "modular_skyrat\modules\altjobtitles\code\_job.dm"
-#include "modular_skyrat\modules\altjobtitles\code\cards_ids.dm"
-#include "modular_skyrat\modules\altjobtitles\code\game.dm"
#include "modular_skyrat\modules\ambitions\code\ambition.dm"
#include "modular_skyrat\modules\ambitions\code\ambition_templates.dm"
#include "modular_skyrat\modules\ambitions\code\antag_datums.dm"
@@ -4204,7 +4270,6 @@
#include "modular_skyrat\modules\cortical_borer\code\cortical_borer_chems.dm"
#include "modular_skyrat\modules\cryosleep\code\ai.dm"
#include "modular_skyrat\modules\cryosleep\code\config.dm"
-#include "modular_skyrat\modules\customization\__DEFINES\augment.dm"
#include "modular_skyrat\modules\customization\__DEFINES\DNA.dm"
#include "modular_skyrat\modules\customization\__DEFINES\lists.dm"
#include "modular_skyrat\modules\customization\__DEFINES\loadout.dm"
@@ -4230,31 +4295,10 @@
#include "modular_skyrat\modules\customization\game\objects\items\tanks\n2_tanks.dm"
#include "modular_skyrat\modules\customization\modules\admin\donator_list.dm"
#include "modular_skyrat\modules\customization\modules\client\preferences.dm"
-#include "modular_skyrat\modules\customization\modules\client\preferences_savefile.dm"
#include "modular_skyrat\modules\customization\modules\client\augment\_augment.dm"
#include "modular_skyrat\modules\customization\modules\client\augment\implants.dm"
#include "modular_skyrat\modules\customization\modules\client\augment\limbs.dm"
#include "modular_skyrat\modules\customization\modules\client\augment\organs.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\_loadout.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\backpack.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\glasses.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\gloves.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\hands.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\head.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\mask.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\neck.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\shoes.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\suit.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\uniform.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\donator\_donator.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\donator\backpack.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\donator\feet.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\donator\hands.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\donator\head.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\donator\neck.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\donator\suit.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\donator\uniform.dm"
-#include "modular_skyrat\modules\customization\modules\client\loadout\donator\personal\donator_personal.dm"
#include "modular_skyrat\modules\customization\modules\clothing\clothing.dm"
#include "modular_skyrat\modules\customization\modules\clothing\toggle_base.dm"
#include "modular_skyrat\modules\customization\modules\clothing\ears\ears.dm"
@@ -4474,7 +4518,6 @@
#include "modular_skyrat\modules\horrorform\code\modules\mob\hostile\true_changeling.dm"
#include "modular_skyrat\modules\huds\code\designs.dm"
#include "modular_skyrat\modules\huds\code\glasses\HUD_Glasses.dm"
-#include "modular_skyrat\modules\huds\code\loadout\glasses_loadout.dm"
#include "modular_skyrat\modules\hydra\neutral.dm"
#include "modular_skyrat\modules\hyposprays\code\game\autolathe_designs.dm"
#include "modular_skyrat\modules\hyposprays\code\game\hypospray_kits.dm"
@@ -4519,6 +4562,24 @@
#include "modular_skyrat\modules\liquids\code\liquid_systems\liquid_pump.dm"
#include "modular_skyrat\modules\liquids\code\liquid_systems\liquid_status_effect.dm"
#include "modular_skyrat\modules\liquids\code\liquid_systems\liquid_turf.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\_loadout_datum.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_accessory.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_belts.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_ears.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_glasses.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_gloves.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_heads.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_inhands.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_masks.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_neck.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_pocket.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_shoes.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_suit.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_toys.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\loadout_datum_under.dm"
+#include "modular_skyrat\modules\loadouts\loadout_items\donator\personal\donator_personal.dm"
+#include "modular_skyrat\modules\loadouts\loadout_ui\loadout_manager.dm"
+#include "modular_skyrat\modules\loadouts\loadout_ui\loadout_outfit_helpers.dm"
#include "modular_skyrat\modules\lobbyscreen\code\load_servers.dm"
#include "modular_skyrat\modules\mapping\code\_unsorted\dungeon.dm"
#include "modular_skyrat\modules\mapping\code\_unsorted\pool.dm"
@@ -4595,7 +4656,6 @@
#include "modular_skyrat\modules\modular_items\code\game\objects\items\storage\bags.dm"
#include "modular_skyrat\modules\modular_items\code\game\objects\items\tools\makeshift.dm"
#include "modular_skyrat\modules\modular_items\lewd_items\code\arousal_system.dm"
-#include "modular_skyrat\modules\modular_items\lewd_items\code\lewd_loadout.dm"
#include "modular_skyrat\modules\modular_items\lewd_items\code\lewd_quirks.dm"
#include "modular_skyrat\modules\modular_items\lewd_items\code\technical_stuff_for_lewd.dm"
#include "modular_skyrat\modules\modular_items\lewd_items\code\lewd_chemistry\chemistry_for_ERP.dm"
@@ -4820,7 +4880,6 @@
#include "modular_skyrat\modules\trim_tokens\code\cards_ids.dm"
#include "modular_skyrat\modules\trim_tokens\code\trim_tokens.dm"
#include "modular_skyrat\modules\turretid\code\turret_id_system.dm"
-#include "modular_skyrat\modules\verbs\code\modules\client\preferences_toggles.dm"
#include "modular_skyrat\modules\verbs\code\modules\client\verbs\looc.dm"
#include "modular_skyrat\modules\verbs\code\modules\mob\say.dm"
#include "modular_skyrat\modules\verbs\code\modules\mob\subtle.dm"
diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml
index 13dc2fb3025..24f6975e688 100644
--- a/tgui/.eslintrc.yml
+++ b/tgui/.eslintrc.yml
@@ -12,6 +12,7 @@ env:
plugins:
- radar
- react
+ - unused-imports
settings:
react:
version: '16.10'
@@ -756,3 +757,6 @@ rules:
react/jsx-uses-vars: error
## Prevent missing parentheses around multilines JSX (fixable)
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/package.json b/tgui/package.json
index ad9ec49c44d..a203dc4a3a2 100644
--- a/tgui/package.json
+++ b/tgui/package.json
@@ -39,6 +39,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",
diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts
index f0a8bfeba31..5ab378070f0 100644
--- a/tgui/packages/common/collections.ts
+++ b/tgui/packages/common/collections.ts
@@ -77,6 +77,25 @@ export const map: MapFunction = (iterateeFn) =>
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 finalCollection;
+};
+
const COMPARATOR = (objA, objB) => {
const criteriaA = objA.criteria;
const criteriaB = objB.criteria;
@@ -99,8 +118,6 @@ 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: ((input: T) => unknown)[]
@@ -134,6 +151,8 @@ export const sortBy = (
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].
@@ -246,3 +265,48 @@ 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;
+ };
+};
diff --git a/tgui/packages/common/exhaustive.ts b/tgui/packages/common/exhaustive.ts
new file mode 100644
index 00000000000..bc41757515b
--- /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/tgfont/icons/ATTRIBUTIONS.md b/tgui/packages/tgfont/icons/ATTRIBUTIONS.md
new file mode 100644
index 00000000000..2f218388d36
--- /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/bad-touch.svg b/tgui/packages/tgfont/icons/bad-touch.svg
new file mode 100644
index 00000000000..6dc3c9a718a
--- /dev/null
+++ b/tgui/packages/tgfont/icons/bad-touch.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tgui/packages/tgfont/icons/non-binary.svg b/tgui/packages/tgfont/icons/non-binary.svg
new file mode 100644
index 00000000000..9aaec674bbb
--- /dev/null
+++ b/tgui/packages/tgfont/icons/non-binary.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tgui/packages/tgfont/icons/prosthetic-leg.svg b/tgui/packages/tgfont/icons/prosthetic-leg.svg
new file mode 100644
index 00000000000..c1f6ceee3fc
--- /dev/null
+++ b/tgui/packages/tgfont/icons/prosthetic-leg.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/tgui/packages/tgui-panel/styles/goon/chat-dark.scss b/tgui/packages/tgui-panel/styles/goon/chat-dark.scss
index 34012cd9599..faa850a3467 100644
--- a/tgui/packages/tgui-panel/styles/goon/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/goon/chat-dark.scss
@@ -937,3 +937,18 @@ em {
.mentor {color: #8a2be2;}
.looc {color: #d8b555;}
.rlooc {color: #b09448;}
+
+.examine_block {
+ background: rgba(0, 0, 0, 0.2);
+ border: 1px solid rgba(55, 55, 55, 0.33);
+ margin: 7px 4px;
+ padding: 8px 12px;
+ max-width: 550px;
+}
+
+.examine_block hr {
+ border: none;
+ background: #222;
+ height: 1px;
+ margin: 8px 16px 8px 16px;
+}
diff --git a/tgui/packages/tgui-panel/styles/goon/chat-light.scss b/tgui/packages/tgui-panel/styles/goon/chat-light.scss
index ed25b83ce5e..c06a75fed20 100644
--- a/tgui/packages/tgui-panel/styles/goon/chat-light.scss
+++ b/tgui/packages/tgui-panel/styles/goon/chat-light.scss
@@ -924,3 +924,18 @@ h1.alert, h2.alert {
.mentor {color: #8a2be2;}
.looc {color: #6699CC; font-weight: bold;}
.rlooc {color: #507294; font-weight: bold;}
+
+.examine_block {
+ background: rgba(0, 0, 0, 0.1);
+ border: 1px solid #111a27;
+ margin: 2px 8px;
+ padding: 8px 12px;
+ max-width: 550px;
+}
+
+.examine_block hr {
+ border: none;
+ background: #333;
+ height: 1px;
+ margin: 8px 16px 8px 16px;
+}
diff --git a/tgui/packages/tgui/components/Autofocus.tsx b/tgui/packages/tgui/components/Autofocus.tsx
new file mode 100644
index 00000000000..f5ae00650aa
--- /dev/null
+++ b/tgui/packages/tgui/components/Autofocus.tsx
@@ -0,0 +1,19 @@
+import { Component, createRef } from "inferno";
+
+export class Autofocus extends Component {
+ ref = createRef();
+
+ componentDidMount() {
+ setTimeout(() => {
+ this.ref.current?.focus();
+ }, 1);
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
diff --git a/tgui/packages/tgui/components/Box.tsx b/tgui/packages/tgui/components/Box.tsx
index 4f4cd4e9ea1..188d9b8c81f 100644
--- a/tgui/packages/tgui/components/Box.tsx
+++ b/tgui/packages/tgui/components/Box.tsx
@@ -5,7 +5,7 @@
*/
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
-import { createVNode, InfernoNode } from 'inferno';
+import { createVNode, InfernoNode, SFC } from 'inferno';
import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags';
import { CSS_COLORS } from '../constants';
@@ -256,7 +256,7 @@ export const computeBoxClassName = (props: BoxProps) => {
]);
};
-export const Box = (props: BoxProps) => {
+export const Box: SFC = (props: BoxProps) => {
const {
as = 'div',
className,
@@ -278,7 +278,9 @@ export const Box = (props: BoxProps) => {
computedClassName,
children,
ChildFlags.UnknownChildren,
- computedProps);
+ computedProps,
+ undefined,
+ );
};
Box.defaultHooks = pureComponentHooks;
diff --git a/tgui/packages/tgui/components/Button.js b/tgui/packages/tgui/components/Button.js
index 776d53136ef..fec3153021d 100644
--- a/tgui/packages/tgui/components/Button.js
+++ b/tgui/packages/tgui/components/Button.js
@@ -74,6 +74,10 @@ export const Button = props => {
}
}}
onKeyDown={e => {
+ if (props.captureKeys === false) {
+ return;
+ }
+
const keyCode = window.event ? e.which : e.keyCode;
// Simulate a click when pressing space or enter.
if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) {
diff --git a/tgui/packages/tgui/components/Dropdown.js b/tgui/packages/tgui/components/Dropdown.js
index fb407882b00..a02f0715ab7 100644
--- a/tgui/packages/tgui/components/Dropdown.js
+++ b/tgui/packages/tgui/components/Dropdown.js
@@ -48,16 +48,28 @@ export class Dropdown extends Component {
buildMenu() {
const { options = [] } = this.props;
- const ops = options.map(option => (
- {
- this.setSelected(option);
- }}>
- {option}
-
- ));
+ const ops = options.map(option => {
+ let displayText, value;
+
+ if (typeof option === "string") {
+ displayText = option;
+ value = option;
+ } else {
+ displayText = option.displayText;
+ value = option.value;
+ }
+
+ return (
+ {
+ this.setSelected(value);
+ }}>
+ {displayText}
+
+ );
+ });
return ops.length ? ops : 'No Options Found';
}
@@ -67,13 +79,16 @@ export class Dropdown extends Component {
icon,
iconRotation,
iconSpin,
+ clipSelectedText = true,
color = 'default',
+ dropdownStyle,
over,
noscroll,
nochevron,
width,
openWidth = width,
onClick,
+ onOpen,
selected,
disabled,
displayText,
@@ -102,7 +117,7 @@ export class Dropdown extends Component {
) : null;
return (
-
+
{
+ onClick={(event) => {
if (disabled && !this.state.open) {
return;
}
this.setOpen(!this.state.open);
+
+ if (props.onOpen) {
+ props.onOpen(event);
+ }
}}>
{icon && (
)}
-
+
{displayText ? displayText : this.state.selected}
{!!nochevron || (
diff --git a/tgui/packages/tgui/components/FitText.tsx b/tgui/packages/tgui/components/FitText.tsx
new file mode 100644
index 00000000000..445e59119c9
--- /dev/null
+++ b/tgui/packages/tgui/components/FitText.tsx
@@ -0,0 +1,89 @@
+import { Component, createRef, RefObject } from "inferno";
+
+const DEFAULT_ACCEPTABLE_DIFFERENCE = 5;
+
+export class FitText extends Component<{
+ acceptableDifference?: number,
+ maxWidth: number,
+ maxFontSize: number,
+ native?: HTMLAttributes,
+}, {
+ fontSize: number,
+}> {
+ ref: RefObject = createRef();
+ state = {
+ fontSize: 0,
+ }
+
+ constructor() {
+ super();
+
+ this.resize = this.resize.bind(this);
+
+ window.addEventListener("resize", this.resize);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.children !== this.props.children) {
+ this.resize();
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this.resize);
+ }
+
+ resize() {
+ const element = this.ref.current;
+ if (!element) {
+ return;
+ }
+
+ const maxWidth = this.props.maxWidth;
+
+ let start = 0;
+ let end = this.props.maxFontSize;
+
+ for (let _ = 0; _ < 10; _++) {
+ const middle = Math.round((start + end) / 2);
+ element.style.fontSize = `${middle}px`;
+
+ const difference = element.offsetWidth - maxWidth;
+
+ if (difference > 0) {
+ end = middle;
+ } else if (
+ difference
+ < (this.props.acceptableDifference ?? DEFAULT_ACCEPTABLE_DIFFERENCE)
+ ) {
+ start = middle;
+ } else {
+ break;
+ }
+ }
+
+ this.setState({
+ fontSize: Math.round((start + end) / 2),
+ });
+ }
+
+ componentDidMount() {
+ this.resize();
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
diff --git a/tgui/packages/tgui/components/Input.js b/tgui/packages/tgui/components/Input.js
index b0c5f3f0cbc..4996ed32234 100644
--- a/tgui/packages/tgui/components/Input.js
+++ b/tgui/packages/tgui/components/Input.js
@@ -69,6 +69,11 @@ export class Input extends Component {
return;
}
if (e.keyCode === KEY_ESCAPE) {
+ if (this.props.onEscape) {
+ this.props.onEscape(e);
+ return;
+ }
+
this.setEditing(false);
e.target.value = toInputValue(this.props.value);
e.target.blur();
@@ -83,8 +88,15 @@ export class Input extends Component {
if (input) {
input.value = toInputValue(nextValue);
}
- if (this.props.autoFocus) {
- setTimeout(() => input.focus(), 1);
+
+ if (this.props.autoFocus || this.props.autoSelect) {
+ setTimeout(() => {
+ input.focus();
+
+ if (this.props.autoSelect) {
+ input.select();
+ }
+ }, 1);
}
}
diff --git a/tgui/packages/tgui/components/KeyListener.tsx b/tgui/packages/tgui/components/KeyListener.tsx
new file mode 100644
index 00000000000..46b52765365
--- /dev/null
+++ b/tgui/packages/tgui/components/KeyListener.tsx
@@ -0,0 +1,39 @@
+import { Component } from "inferno";
+import { KeyEvent } from "../events";
+import { listenForKeyEvents } from "../hotkeys";
+
+type KeyListenerProps = Partial<{
+ onKey: (key: KeyEvent) => void,
+ onKeyDown: (key: KeyEvent) => void,
+ onKeyUp: (key: KeyEvent) => void,
+}>;
+
+export class KeyListener extends Component {
+ dispose: () => void;
+
+ constructor() {
+ super();
+
+ this.dispose = listenForKeyEvents(key => {
+ if (this.props.onKey) {
+ this.props.onKey(key);
+ }
+
+ if (key.isDown() && this.props.onKeyDown) {
+ this.props.onKeyDown(key);
+ }
+
+ if (key.isUp() && this.props.onKeyUp) {
+ this.props.onKeyUp(key);
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ this.dispose();
+ }
+
+ render() {
+ return null;
+ }
+}
diff --git a/tgui/packages/tgui/components/LabeledList.tsx b/tgui/packages/tgui/components/LabeledList.tsx
index 029724dd13a..db4ed54e696 100644
--- a/tgui/packages/tgui/components/LabeledList.tsx
+++ b/tgui/packages/tgui/components/LabeledList.tsx
@@ -34,6 +34,7 @@ type LabeledListItemProps = {
/** @deprecated */
content?: any,
children?: InfernoNode;
+ verticalAlign?: string;
};
const LabeledListItem = (props: LabeledListItemProps) => {
@@ -46,6 +47,7 @@ const LabeledListItem = (props: LabeledListItemProps) => {
buttons,
content,
children,
+ verticalAlign = "baseline",
} = props;
return (
{
className={classes([
'LabeledList__cell',
'LabeledList__label',
- ])}>
+ ])}
+ verticalAlign={verticalAlign}>
{label ? label + ':' : null}
{
'LabeledList__cell',
'LabeledList__content',
])}
- colSpan={buttons ? undefined : 2}>
+ colSpan={buttons ? undefined : 2}
+ verticalAlign={verticalAlign}>
{content}
{children}
diff --git a/tgui/packages/tgui/components/Popper.tsx b/tgui/packages/tgui/components/Popper.tsx
index 83200fb1759..326d5ec384b 100644
--- a/tgui/packages/tgui/components/Popper.tsx
+++ b/tgui/packages/tgui/components/Popper.tsx
@@ -1,4 +1,4 @@
-import { createPopper, OptionsGeneric } from "@popperjs/core";
+import { createPopper } from "@popperjs/core";
import { ArgumentsOf } from "common/types";
import { Component, findDOMfromVNode, InfernoNode, render } from "inferno";
@@ -64,11 +64,18 @@ export class Popper extends Component {
componentWillUnmount() {
this.popperInstance?.destroy();
- this.renderedContent.remove();
+ render(null, this.renderedContent, () => this.renderedContent.remove());
}
renderPopperContent(callback: () => void) {
- render(this.props.popperContent, this.renderedContent, callback);
+ // `render` errors when given false, so we convert it to `null`,
+ // which is supported.
+ render(
+ this.props.popperContent || null,
+ this.renderedContent,
+ callback,
+ this.context,
+ );
}
render() {
diff --git a/tgui/packages/tgui/components/Stack.tsx b/tgui/packages/tgui/components/Stack.tsx
index b43ef6059db..1a6ace07f39 100644
--- a/tgui/packages/tgui/components/Stack.tsx
+++ b/tgui/packages/tgui/components/Stack.tsx
@@ -5,6 +5,7 @@
*/
import { classes } from 'common/react';
+import { RefObject } from 'inferno';
import { computeBoxClassName, computeBoxProps } from './Box';
import { computeFlexClassName, computeFlexItemClassName, computeFlexItemProps, computeFlexProps, FlexItemProps, FlexProps } from './Flex';
@@ -35,8 +36,12 @@ export const Stack = (props: StackProps) => {
);
};
-const StackItem = (props: FlexProps) => {
- const { className, ...rest } = props;
+type StackItemProps = FlexProps & {
+ innerRef?: RefObject,
+};
+
+const StackItem = (props: StackItemProps) => {
+ const { className, innerRef, ...rest } = props;
return (
{
computeFlexItemClassName(rest),
computeBoxClassName(rest),
])}
+ ref={innerRef}
{...computeBoxProps(computeFlexItemProps(rest))}
/>
);
diff --git a/tgui/packages/tgui/components/Tooltip.tsx b/tgui/packages/tgui/components/Tooltip.tsx
index dc347f2a117..0cf61ed04fc 100644
--- a/tgui/packages/tgui/components/Tooltip.tsx
+++ b/tgui/packages/tgui/components/Tooltip.tsx
@@ -7,7 +7,7 @@ const DEFAULT_PLACEMENT = "top";
type TooltipProps = {
children?: InfernoNode;
- content: string;
+ content: InfernoNode;
position?: Placement,
};
@@ -15,6 +15,11 @@ type TooltipState = {
hovered: boolean;
};
+const DISABLE_EVENT_LISTENERS = [{
+ name: "eventListeners",
+ enabled: false,
+}];
+
export class Tooltip extends Component
{
state = {
hovered: false,
@@ -53,10 +58,7 @@ export class Tooltip extends Component {
{
}
additionalStyles={{
"pointer-events": "none",
+ "z-index": 2,
}}>
{this.props.children}
diff --git a/tgui/packages/tgui/components/TrackOutsideClicks.tsx b/tgui/packages/tgui/components/TrackOutsideClicks.tsx
new file mode 100644
index 00000000000..fc52eba7e6c
--- /dev/null
+++ b/tgui/packages/tgui/components/TrackOutsideClicks.tsx
@@ -0,0 +1,38 @@
+import { Component, createRef } from "inferno";
+
+export class TrackOutsideClicks extends Component<{
+ onOutsideClick: () => void,
+}> {
+ ref = createRef();
+
+ constructor() {
+ super();
+
+ this.handleOutsideClick = this.handleOutsideClick.bind(this);
+
+ document.addEventListener("click", this.handleOutsideClick);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("click", this.handleOutsideClick);
+ }
+
+ handleOutsideClick(event: MouseEvent) {
+ if (!(event.target instanceof Node)) {
+ return;
+ }
+
+ if (this.ref.current && !this.ref.current.contains(event.target)) {
+ this.props.onOutsideClick();
+ }
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
diff --git a/tgui/packages/tgui/components/index.js b/tgui/packages/tgui/components/index.js
index 3833d1a69ab..48cd147df39 100644
--- a/tgui/packages/tgui/components/index.js
+++ b/tgui/packages/tgui/components/index.js
@@ -5,6 +5,7 @@
*/
export { AnimatedNumber } from './AnimatedNumber';
+export { Autofocus } from './Autofocus';
export { Blink } from './Blink';
export { BlockQuote } from './BlockQuote';
export { Box } from './Box';
@@ -18,10 +19,12 @@ export { Divider } from './Divider';
export { DraggableControl } from './DraggableControl';
export { Dropdown } from './Dropdown';
export { Flex } from './Flex';
+export { FitText } from './FitText';
export { Grid } from './Grid';
export { Icon } from './Icon';
export { InfinitePlane } from './InfinitePlane';
export { Input } from './Input';
+export { KeyListener } from './KeyListener';
export { Knob } from './Knob';
export { LabeledControls } from './LabeledControls';
export { LabeledList } from './LabeledList';
@@ -38,4 +41,5 @@ export { Table } from './Table';
export { Tabs } from './Tabs';
export { TextArea } from './TextArea';
export { TimeDisplay } from './TimeDisplay';
+export { TrackOutsideClicks } from './TrackOutsideClicks';
export { Tooltip } from './Tooltip';
diff --git a/tgui/packages/tgui/hotkeys.ts b/tgui/packages/tgui/hotkeys.ts
index 1358ff25101..2330a8b20ae 100644
--- a/tgui/packages/tgui/hotkeys.ts
+++ b/tgui/packages/tgui/hotkeys.ts
@@ -31,6 +31,9 @@ const hotKeysAcquired = [
// State of passed-through keys.
const keyState: Record = {};
+// Custom listeners for key events
+const keyListeners: ((key: KeyEvent) => void)[] = [];
+
/**
* Converts a browser keycode to BYOND keycode.
*/
@@ -178,6 +181,38 @@ export const setupHotKeys = () => {
releaseHeldKeys();
});
globalEvents.on('key', (key: KeyEvent) => {
+ for (const keyListener of keyListeners) {
+ keyListener(key);
+ }
+
handlePassthrough(key);
});
};
+
+/**
+ * Registers for any key events, such as key down or key up.
+ * This should be preferred over directly connecting to keydown/keyup
+ * as it lets tgui prevent the key from reaching BYOND.
+ *
+ * If using in a component, prefer KeyListener, which automatically handles
+ * stopping listening when unmounting.
+ *
+ * @param callback The function to call whenever a key event occurs
+ * @returns A callback to stop listening
+ */
+export const listenForKeyEvents = (
+ callback: (key: KeyEvent) => void,
+): () => void => {
+ keyListeners.push(callback);
+
+ let removed = false;
+
+ return () => {
+ if (removed) {
+ return;
+ }
+
+ removed = true;
+ keyListeners.splice(keyListeners.indexOf(callback), 1);
+ };
+};
diff --git a/tgui/packages/tgui/http.ts b/tgui/packages/tgui/http.ts
new file mode 100644
index 00000000000..96a2d9b56df
--- /dev/null
+++ b/tgui/packages/tgui/http.ts
@@ -0,0 +1,17 @@
+/**
+ * An equivalent to `fetch`, except will automatically retry.
+ */
+export const fetchRetry
+ = (
+ url: string,
+ options?: RequestInit,
+ retryTimer: number = 1000,
+ ): Promise => {
+ return fetch(url, options).catch(() => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ fetchRetry(url, options, retryTimer).then(resolve);
+ }, retryTimer);
+ });
+ });
+ };
diff --git a/tgui/packages/tgui/interfaces/AntagInfoBrainwashed.tsx b/tgui/packages/tgui/interfaces/AntagInfoBrainwashed.tsx
index 8e88760ab2c..2e677ca4916 100644
--- a/tgui/packages/tgui/interfaces/AntagInfoBrainwashed.tsx
+++ b/tgui/packages/tgui/interfaces/AntagInfoBrainwashed.tsx
@@ -1,5 +1,5 @@
-import { useBackend, useLocalState } from '../backend';
-import { Blink, BlockQuote, Box, Dimmer, Icon, Section, Stack } from '../components';
+import { useBackend } from '../backend';
+import { Icon, Section, Stack } from '../components';
import { BooleanLike } from 'common/react';
import { Window } from '../layouts';
diff --git a/tgui/packages/tgui/interfaces/AntagInfoChangeling.tsx b/tgui/packages/tgui/interfaces/AntagInfoChangeling.tsx
index 2e644d70414..ed3dad8e744 100644
--- a/tgui/packages/tgui/interfaces/AntagInfoChangeling.tsx
+++ b/tgui/packages/tgui/interfaces/AntagInfoChangeling.tsx
@@ -1,6 +1,6 @@
import { multiline } from 'common/string';
-import { useBackend, useLocalState, useSharedState } from '../backend';
-import { BlockQuote, Box, Button, Dimmer, Dropdown, Modal, Section, Stack } from '../components';
+import { useBackend, useSharedState } from '../backend';
+import { Box, Button, Dimmer, Dropdown, Section, Stack } from '../components';
import { Window } from '../layouts';
const hivestyle = {
diff --git a/tgui/packages/tgui/interfaces/AntagInfoGeneric.tsx b/tgui/packages/tgui/interfaces/AntagInfoGeneric.tsx
index 3b71a573420..a07848d24bc 100644
--- a/tgui/packages/tgui/interfaces/AntagInfoGeneric.tsx
+++ b/tgui/packages/tgui/interfaces/AntagInfoGeneric.tsx
@@ -1,5 +1,5 @@
-import { useBackend, useLocalState } from '../backend';
-import { Blink, BlockQuote, Box, Dimmer, Icon, Section, Stack } from '../components';
+import { useBackend } from '../backend';
+import { Section, Stack } from '../components';
import { BooleanLike } from 'common/react';
import { Window } from '../layouts';
diff --git a/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx
index 5515b2b08a5..bc3b4b34d5d 100644
--- a/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx
+++ b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx
@@ -1,4 +1,4 @@
-import { useBackend, useLocalState } from '../backend';
+import { useBackend } from '../backend';
import { multiline } from 'common/string';
import { BlockQuote, Button, Dimmer, Section, Stack } from '../components';
import { BooleanLike } from 'common/react';
diff --git a/tgui/packages/tgui/interfaces/AntagInfoWizard.tsx b/tgui/packages/tgui/interfaces/AntagInfoWizard.tsx
index 046030a7bc5..4951c7b2486 100644
--- a/tgui/packages/tgui/interfaces/AntagInfoWizard.tsx
+++ b/tgui/packages/tgui/interfaces/AntagInfoWizard.tsx
@@ -1,5 +1,5 @@
-import { useBackend, useLocalState } from '../backend';
-import { Blink, BlockQuote, Box, Dimmer, Icon, Section, Stack } from '../components';
+import { useBackend } from '../backend';
+import { Section, Stack } from '../components';
import { BooleanLike } from 'common/react';
import { Window } from '../layouts';
diff --git a/tgui/packages/tgui/interfaces/Aquarium.js b/tgui/packages/tgui/interfaces/Aquarium.js
index a9049b554b1..5758f801536 100644
--- a/tgui/packages/tgui/interfaces/Aquarium.js
+++ b/tgui/packages/tgui/interfaces/Aquarium.js
@@ -1,5 +1,5 @@
import { useBackend } from '../backend';
-import { Button, Dropdown, Flex, Knob, LabeledControls, Section } from '../components';
+import { Button, Flex, Knob, LabeledControls, Section } from '../components';
import { Window } from '../layouts';
export const Aquarium = (props, context) => {
diff --git a/tgui/packages/tgui/interfaces/BluespaceSender.js b/tgui/packages/tgui/interfaces/BluespaceSender.js
index d1deed2a906..80ea5ee54bb 100644
--- a/tgui/packages/tgui/interfaces/BluespaceSender.js
+++ b/tgui/packages/tgui/interfaces/BluespaceSender.js
@@ -2,7 +2,7 @@ import { filter, sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { toFixed } from 'common/math';
import { useBackend } from '../backend';
-import { Button, Divider, LabeledList, NumberInput, ProgressBar, Section, Stack, Box, AnimatedNumber } from '../components';
+import { Button, Divider, LabeledList, NumberInput, ProgressBar, Section, Stack, Box } from '../components';
import { getGasColor, getGasLabel } from '../constants';
import { Window } from '../layouts';
diff --git a/tgui/packages/tgui/interfaces/BluespaceVendor.js b/tgui/packages/tgui/interfaces/BluespaceVendor.js
index 7491bc7a1db..05504153455 100644
--- a/tgui/packages/tgui/interfaces/BluespaceVendor.js
+++ b/tgui/packages/tgui/interfaces/BluespaceVendor.js
@@ -3,7 +3,7 @@ import { flow } from 'common/fp';
import { toFixed } from 'common/math';
import { multiline } from 'common/string';
import { useBackend } from '../backend';
-import { Button, Divider, LabeledList, NumberInput, ProgressBar, Section, Stack, Box } from '../components';
+import { Button, Divider, LabeledList, NumberInput, ProgressBar, Section, Stack } from '../components';
import { getGasColor, getGasLabel } from '../constants';
import { Window } from '../layouts';
diff --git a/tgui/packages/tgui/interfaces/CharacterPreview.tsx b/tgui/packages/tgui/interfaces/CharacterPreview.tsx
new file mode 100644
index 00000000000..441cacecae9
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/CharacterPreview.tsx
@@ -0,0 +1,15 @@
+import { ByondUi } from "../components";
+
+export const CharacterPreview = (props: {
+ height: string,
+ id: string,
+}) => {
+ return ( );
+};
diff --git a/tgui/packages/tgui/interfaces/CryopodConsole.js b/tgui/packages/tgui/interfaces/CryopodConsole.js
index 3036eafc2cb..fda64940b6f 100644
--- a/tgui/packages/tgui/interfaces/CryopodConsole.js
+++ b/tgui/packages/tgui/interfaces/CryopodConsole.js
@@ -1,14 +1,11 @@
import { useBackend } from '../backend';
-import { Button, Box, LabeledList, NoticeBox, Section, Stack } from '../components';
+import { LabeledList, NoticeBox, Section, Stack } from '../components';
import { Window } from '../layouts';
export const CryopodConsole = (props, context) => {
const { data } = useBackend(context);
const { account_name } = data;
-
const welcomeTitle = `Hello, ${account_name || '[REDACTED]'}!`;
- // Skyrat Edit Addition - Cryostorage stores items.
- // Original does not contain or its containing Stack.Item
return (
@@ -22,20 +19,14 @@ export const CryopodConsole = (props, context) => {
-
-
-
);
-// Skyrat Edit End
};
-
const CrewList = (props, context) => {
const { data } = useBackend(context);
const { frozen_crew } = data;
-
return (
frozen_crew.length && (
{
)
);
};
-
-// Skyrat Edit Addition - Cryostorage stores items.
-const ItemList = (props, context) => {
- const { act, data } = useBackend(context);
- const { ref_list, ref_name, ref_allw } = data;
- if (!ref_allw) {
- return (
- You are not authorized for item management.
- );
- }
- return (
- ref_list.length && (
-
-
- {ref_list.map((item) => (
-
- act('item_get', { item_get: item })} />
-
- ))}
-
-
- ) || (
- No stored items!
- )
- );
-};
-// Skyrat Edit End
diff --git a/tgui/packages/tgui/interfaces/Crystallizer.js b/tgui/packages/tgui/interfaces/Crystallizer.js
index 35815a21f67..40181ed793b 100644
--- a/tgui/packages/tgui/interfaces/Crystallizer.js
+++ b/tgui/packages/tgui/interfaces/Crystallizer.js
@@ -1,5 +1,5 @@
import { useBackend } from '../backend';
-import { AnimatedNumber, Button, Flex, Input, LabeledList, ProgressBar, Section, Table, NumberInput, Box } from '../components';
+import { Button, LabeledList, ProgressBar, Section, NumberInput, Box } from '../components';
import { getGasColor, getGasLabel } from '../constants';
import { toFixed } from 'common/math';
import { Window } from '../layouts';
diff --git a/tgui/packages/tgui/interfaces/IntegratedCircuit/CircuitInfo.js b/tgui/packages/tgui/interfaces/IntegratedCircuit/CircuitInfo.js
index 8fd1263b7aa..49ba43e2de2 100644
--- a/tgui/packages/tgui/interfaces/IntegratedCircuit/CircuitInfo.js
+++ b/tgui/packages/tgui/interfaces/IntegratedCircuit/CircuitInfo.js
@@ -1,4 +1,4 @@
-import { Button, Section, Stack, Box } from '../../components';
+import { Button, Stack, Box } from '../../components';
export const CircuitInfo = (props, context) => {
const {
diff --git a/tgui/packages/tgui/interfaces/IntegratedCircuit/DisplayName.js b/tgui/packages/tgui/interfaces/IntegratedCircuit/DisplayName.js
index b281cbd00a0..5bebe9762ff 100644
--- a/tgui/packages/tgui/interfaces/IntegratedCircuit/DisplayName.js
+++ b/tgui/packages/tgui/interfaces/IntegratedCircuit/DisplayName.js
@@ -1,7 +1,6 @@
import { useBackend } from '../../backend';
import { Box, Button, Flex } from '../../components';
import { FUNDAMENTAL_DATA_TYPES, DATATYPE_DISPLAY_HANDLERS } from './FundamentalTypes';
-import { NULL_REF } from './constants';
export const DisplayName = (props, context) => {
const { act } = useBackend(context);
diff --git a/tgui/packages/tgui/interfaces/IntegratedCircuit/ObjectComponent.js b/tgui/packages/tgui/interfaces/IntegratedCircuit/ObjectComponent.js
index 08b7e750940..3a721c572f3 100644
--- a/tgui/packages/tgui/interfaces/IntegratedCircuit/ObjectComponent.js
+++ b/tgui/packages/tgui/interfaces/IntegratedCircuit/ObjectComponent.js
@@ -1,7 +1,7 @@
import { useBackend } from '../../backend';
import {
Box,
- Stack, Button, Dropdown,
+ Stack, Button,
} from '../../components';
import { Component } from 'inferno';
import { shallowDiffers } from '../../../common/react';
diff --git a/tgui/packages/tgui/interfaces/IntegratedCircuit/VariableMenu.js b/tgui/packages/tgui/interfaces/IntegratedCircuit/VariableMenu.js
index ee7d19f5a74..03951705de5 100644
--- a/tgui/packages/tgui/interfaces/IntegratedCircuit/VariableMenu.js
+++ b/tgui/packages/tgui/interfaces/IntegratedCircuit/VariableMenu.js
@@ -2,7 +2,6 @@ import { useLocalState } from '../../backend';
import {
Box,
Stack,
- Icon,
Section,
Button,
Input,
diff --git a/tgui/packages/tgui/interfaces/IntegratedCircuit/index.js b/tgui/packages/tgui/interfaces/IntegratedCircuit/index.js
index a2e027e9d03..c24c4a15b20 100644
--- a/tgui/packages/tgui/interfaces/IntegratedCircuit/index.js
+++ b/tgui/packages/tgui/interfaces/IntegratedCircuit/index.js
@@ -5,13 +5,12 @@ import {
Stack,
Box,
Button,
- Section,
} from '../../components';
import { Component } from 'inferno';
-import { Layout, Window } from '../../layouts';
+import { Window } from '../../layouts';
import { resolveAsset } from '../../assets';
import { CircuitInfo } from './CircuitInfo';
-import { NULL_REF, ABSOLUTE_Y_OFFSET, MOUSE_BUTTON_LEFT } from './constants';
+import { ABSOLUTE_Y_OFFSET, MOUSE_BUTTON_LEFT } from './constants';
import { Connections } from './Connections';
import { ObjectComponent } from './ObjectComponent';
import { VariableMenu } from './VariableMenu';
diff --git a/tgui/packages/tgui/interfaces/InteractionMenu.js b/tgui/packages/tgui/interfaces/InteractionMenu.js
index 0e464725535..89bb41588fe 100644
--- a/tgui/packages/tgui/interfaces/InteractionMenu.js
+++ b/tgui/packages/tgui/interfaces/InteractionMenu.js
@@ -1,6 +1,5 @@
-import { right } from '@popperjs/core';
import { useBackend } from '../backend';
-import { Button, LabeledList, NoticeBox, Flex, Section } from '../components';
+import { Button, NoticeBox, Section } from '../components';
import { Window } from '../layouts';
export const InteractionMenu = (props, context) => {
diff --git a/tgui/packages/tgui/interfaces/LoadoutManager.js b/tgui/packages/tgui/interfaces/LoadoutManager.js
new file mode 100644
index 00000000000..13f8f33b4cc
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LoadoutManager.js
@@ -0,0 +1,269 @@
+import { useBackend, useSharedState } from '../backend';
+import { Box, Button, Dimmer, Section, Stack, Tabs, Dropdown } from '../components';
+import { Window } from '../layouts';
+import { CharacterPreview } from "./CharacterPreview";
+
+export const LoadoutManager = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ selected_loadout,
+ loadout_tabs,
+ user_is_donator,
+ mob_name,
+ preivew_options,
+ preview_selection,
+ tutorial_status,
+ } = data;
+
+ const [selectedTabName, setSelectedTab] = useSharedState(
+ context, 'tabs', loadout_tabs[0]?.name);
+ const selectedTab = loadout_tabs.find(curTab => {
+ return curTab.name === selectedTabName;
+ });
+
+ return (
+
+
+ { !!tutorial_status && (
+
+ )}
+
+
+ act('toggle_tutorial')} />
+ )}>
+
+ {loadout_tabs.map(curTab => (
+ setSelectedTab(curTab.name)}>
+ {curTab.name}
+
+ ))}
+
+
+
+
+
+
+ { selectedTab && selectedTab.contents ? (
+ act('clear_all_items')} />
+ )}>
+
+ {selectedTab.contents.map(item => (
+
+
+
+ {item.name}
+
+ { !!item.is_greyscale
+ && (
+
+ act('select_color', {
+ path: item.path,
+ })} />
+
+ )}
+ { !!item.is_renamable
+ && (
+
+ act('set_name', {
+ path: item.path,
+ })} />
+
+ )}
+ { !!item.is_job_restricted
+ && (
+
+ act('display_restrictions', {
+ path: item.path,
+ })} />
+
+ )}
+ { !!item.is_donator_only
+ && (
+
+ act('donator_explain', {
+ path: item.path,
+ })}
+ />
+
+ )}
+ { !!item.is_ckey_whitelisted
+ && (
+
+ act('ckey_explain', {
+ path: item.path,
+ })}
+ />
+
+ )}
+
+ act('select_item', {
+ path: item.path,
+ deselect:
+ selected_loadout.includes(item.path),
+ })} />
+
+
+
+ ))}
+
+
+ ) : (
+
+
+ No contents for selected tab.
+
+
+ )}
+
+
+ act('update_preview', {
+ updated_preview: value,
+ })} />
+ )}>
+
+
+
+
+
+
+
+
+ );
+};
+
+export const LoadoutTutorialDimmer = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ tutorial_text,
+ } = data;
+ return (
+
+
+
+ {tutorial_text}
+
+
+ act('toggle_tutorial')}>
+ Okay.
+
+
+
+
+ );
+};
+
+export const LoadoutPreview = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ character_preview_view: string,
+ } = data;
+
+ return (
+
+
+
+
+
+
+
+
+ act('close_ui', { revert: 0 })} />
+
+
+ act('rotate_dummy', {
+ dir: "left",
+ })} />
+
+
+ act('rotate_dummy', {
+ dir: "right",
+ })} />
+
+
+ act('close_ui', { revert: 1 })} />
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/MechpadConsole.js b/tgui/packages/tgui/interfaces/MechpadConsole.js
index c8eb9047352..3beb6504a1f 100644
--- a/tgui/packages/tgui/interfaces/MechpadConsole.js
+++ b/tgui/packages/tgui/interfaces/MechpadConsole.js
@@ -1,5 +1,5 @@
import { useBackend } from '../backend';
-import { Box, Button, Divider, Flex, Grid, Input, NoticeBox, NumberInput, Section } from '../components';
+import { Box, Button, Divider, Flex, Input, NoticeBox, Section } from '../components';
import { Window } from '../layouts';
export const MechpadControl = (props, context) => {
diff --git a/tgui/packages/tgui/interfaces/NtosNetChat.js b/tgui/packages/tgui/interfaces/NtosNetChat.js
index 56def6773f3..c74b40513c9 100644
--- a/tgui/packages/tgui/interfaces/NtosNetChat.js
+++ b/tgui/packages/tgui/interfaces/NtosNetChat.js
@@ -1,7 +1,6 @@
import { useBackend } from '../backend';
-import { Box, Button, Dimmer, Icon, Input, Section, Stack, Table, Tooltip } from '../components';
+import { Box, Button, Dimmer, Icon, Input, Section, Stack } from '../components';
import { NtosWindow } from '../layouts';
-import { StackingConsole } from './StackingConsole';
// byond defines for the program state
const CLIENT_ONLINE = 2;
diff --git a/tgui/packages/tgui/interfaces/NtosSecurEye.js b/tgui/packages/tgui/interfaces/NtosSecurEye.js
index 8d4bc627452..c5780b3efb3 100644
--- a/tgui/packages/tgui/interfaces/NtosSecurEye.js
+++ b/tgui/packages/tgui/interfaces/NtosSecurEye.js
@@ -1,13 +1,7 @@
-import { filter, sortBy } from 'common/collections';
-import { flow } from 'common/fp';
-import { classes } from 'common/react';
-import { createSearch } from 'common/string';
-import { Fragment } from 'inferno';
-import { useBackend, useLocalState } from '../backend';
-import { Button, ByondUi, Input, Section } from '../components';
+import { useBackend } from '../backend';
+import { Button, ByondUi } from '../components';
import { NtosWindow } from '../layouts';
import { prevNextCamera, selectCameras, CameraConsoleContent } from './CameraConsole';
-import { logger } from "../logging";
export const NtosSecurEye = (props, context) => {
const { act, data, config } = useBackend(context);
diff --git a/tgui/packages/tgui/interfaces/NtosTechweb.js b/tgui/packages/tgui/interfaces/NtosTechweb.js
index a7ff19c8f02..0c8b98529db 100644
--- a/tgui/packages/tgui/interfaces/NtosTechweb.js
+++ b/tgui/packages/tgui/interfaces/NtosTechweb.js
@@ -1,5 +1,5 @@
import { AppTechweb } from './Techweb.js';
-import { useBackend, useLocalState } from '../backend';
+import { useBackend } from '../backend';
import { createLogger } from '../logging';
const logger = createLogger('backend');
diff --git a/tgui/packages/tgui/interfaces/OrionGame.js b/tgui/packages/tgui/interfaces/OrionGame.js
index e4902bcfe37..8824cb56b29 100644
--- a/tgui/packages/tgui/interfaces/OrionGame.js
+++ b/tgui/packages/tgui/interfaces/OrionGame.js
@@ -1,6 +1,5 @@
-import { multiline } from 'common/string';
-import { useBackend, useLocalState } from '../backend';
-import { Box, Button, Dimmer, Divider, Icon, NumberInput, Section, Stack } from '../components';
+import { useBackend } from '../backend';
+import { Box, Button, Divider, Section, Stack } from '../components';
import { Window } from '../layouts';
const buttonWidth = 2;
diff --git a/tgui/packages/tgui/interfaces/ParticleAccelerator.js b/tgui/packages/tgui/interfaces/ParticleAccelerator.js
index 480fdbe05ec..7a49222a5e7 100644
--- a/tgui/packages/tgui/interfaces/ParticleAccelerator.js
+++ b/tgui/packages/tgui/interfaces/ParticleAccelerator.js
@@ -1,4 +1,3 @@
-import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Box, Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx
new file mode 100644
index 00000000000..5307c25a9fd
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx
@@ -0,0 +1,207 @@
+import { binaryInsertWith } from "common/collections";
+import { classes } from "common/react";
+import { useBackend, useLocalState } from "../../backend";
+import { Box, Button, Divider, Flex, Section, Stack, Tooltip } from "../../components";
+import { Antagonist, Category } from "./antagonists/base";
+import { PreferencesMenuData } from "./data";
+
+const requireAntag = require.context("./antagonists/antagonists", false, /.ts$/);
+
+const antagsByCategory = new Map();
+
+// This will break at priorities higher than 10, but that almost definitely
+// will not happen.
+const binaryInsertAntag = binaryInsertWith((antag: Antagonist) => {
+ return `${antag.priority}_${antag.name}`;
+});
+
+for (const antagKey of requireAntag.keys()) {
+ const antag = requireAntag<{
+ default?: Antagonist,
+ }>(antagKey).default;
+
+ if (!antag) {
+ continue;
+ }
+
+ antagsByCategory.set(
+ antag.category,
+ binaryInsertAntag(
+ antagsByCategory.get(antag.category) || [],
+ antag,
+ )
+ );
+}
+
+const AntagSelection = (props: {
+ antagonists: Antagonist[],
+ name: string,
+}, context) => {
+ const { act, data } = useBackend(context);
+ const className = "PreferencesMenu__Antags__antagSelection";
+
+ const [predictedState, setPredictedState]
+ = useLocalState(
+ context,
+ "AntagSelection_predictedState",
+ new Set(data.selected_antags),
+ );
+
+ const enableAntags = (antags: string[]) => {
+ const newState = new Set(predictedState);
+
+ for (const antag of antags) {
+ newState.add(antag);
+ }
+
+ setPredictedState(newState);
+
+ act("set_antags", {
+ antags,
+ toggled: true,
+ });
+ };
+
+ const disableAntags = (antags: string[]) => {
+ const newState = new Set(predictedState);
+
+ for (const antag of antags) {
+ newState.delete(antag);
+ }
+
+ setPredictedState(newState);
+
+ act("set_antags", {
+ antags,
+ toggled: false,
+ });
+ };
+
+ const antagonistKeys = props.antagonists.map(antagonist => antagonist.key);
+
+ return (
+
+ enableAntags(antagonistKeys)}
+ >
+ Enable All
+
+
+ disableAntags(antagonistKeys)}
+ >
+ Disable All
+
+ >
+ )}>
+
+ {props.antagonists.map(antagonist => {
+ const isBanned = data.antag_bans
+ && data.antag_bans.indexOf(antagonist.key) !== -1;
+
+ const daysLeft
+ = (data.antag_days_left && data.antag_days_left[antagonist.key])
+ || 0;
+
+ return (
+ 0)
+ ? "banned"
+ : predictedState.has(antagonist.key) ? "on" : "off"
+ }`,
+ ])}
+ key={antagonist.key}
+ >
+
+
+ {antagonist.name}
+
+
+
+ {
+ return (
+
+ {text}
+ {
+ index !== antagonist.description.length - 1
+ &&
+ }
+
+ );
+ })
+ } position="bottom">
+ {
+ if (isBanned) {
+ return;
+ }
+
+ if (predictedState.has(antagonist.key)) {
+ disableAntags([antagonist.key]);
+ } else {
+ enableAntags([antagonist.key]);
+ }
+ }}
+ >
+
+
+ {isBanned && (
+
+ )}
+
+ {daysLeft > 0 && (
+
+ {daysLeft} days left
+
+ )}
+
+
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export const AntagsPage = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx
new file mode 100644
index 00000000000..14f622362af
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx
@@ -0,0 +1,164 @@
+import { exhaustiveCheck } from "common/exhaustive";
+import { useBackend, useLocalState } from "../../backend";
+import { Button, Stack } from "../../components";
+import { Window } from "../../layouts";
+import { PreferencesMenuData } from "./data";
+import { PageButton } from "./PageButton";
+import { AntagsPage } from "./AntagsPage";
+import { JobsPage } from "./JobsPage";
+import { MainPage } from "./MainPage";
+import { SpeciesPage } from "./SpeciesPage";
+import { QuirksPage } from "./QuirksPage";
+
+enum Page {
+ Antags,
+ Main,
+ Jobs,
+ Species,
+ Quirks,
+}
+
+const CharacterProfiles = (props: {
+ activeSlot: number,
+ onClick: (index: number) => void,
+ profiles: (string | null)[],
+}) => {
+ const { profiles } = props;
+
+ return (
+
+ {profiles.map((profile, slot) => (
+
+ {
+ props.onClick(slot);
+ }} fluid>{profile ?? "New Character"}
+
+
+ ))}
+
+ );
+};
+
+export const CharacterPreferenceWindow = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const [currentPage, setCurrentPage] = useLocalState(context, "currentPage", Page.Main);
+
+ let pageContents;
+
+ switch (currentPage) {
+ case Page.Antags:
+ pageContents = ;
+ break;
+ case Page.Jobs:
+ pageContents = ;
+ break;
+ case Page.Main:
+ pageContents = ( setCurrentPage(Page.Species)}
+ />);
+
+ break;
+ case Page.Species:
+ pageContents = ( setCurrentPage(Page.Main)}
+ />);
+
+ break;
+ case Page.Quirks:
+ pageContents = ;
+ break;
+ default:
+ exhaustiveCheck(currentPage);
+ }
+
+ return (
+
+
+
+
+ {
+ act("change_slot", {
+ slot: slot + 1,
+ });
+ }}
+ profiles={data.character_profiles}
+ />
+
+
+ {!data.content_unlocked && (
+
+ Buy BYOND premium for more slots!
+
+ )}
+
+
+
+
+
+
+
+ Character
+
+
+
+
+
+ {/*
+ Fun fact: This isn't "Jobs" so that it intentionally
+ catches your eyes, because it's really important!
+ */}
+
+ Occupations
+
+
+
+
+
+ Antagonists
+
+
+
+
+
+ Quirks
+
+
+
+
+
+
+
+
+ {pageContents}
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreview.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreview.tsx
new file mode 100644
index 00000000000..ab27d2fe2f5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreview.tsx
@@ -0,0 +1,15 @@
+import { ByondUi } from "../../components";
+
+export const CharacterPreview = (props: {
+ height: string,
+ id: string,
+}) => {
+ return ( );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferenceWindow.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferenceWindow.tsx
new file mode 100644
index 00000000000..4bddd3305ed
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferenceWindow.tsx
@@ -0,0 +1,77 @@
+import { Stack } from "../../components";
+import { Window } from "../../layouts";
+import { KeybindingsPage } from "./KeybindingsPage";
+import { GamePreferencesPage } from "./GamePreferencesPage";
+import { PageButton } from "./PageButton";
+import { useBackend, useLocalState } from "../../backend";
+import { GamePreferencesSelectedPage, PreferencesMenuData } from "./data";
+import { exhaustiveCheck } from "common/exhaustive";
+
+export const GamePreferenceWindow = (props: {
+ startingPage?: GamePreferencesSelectedPage,
+}, context) => {
+ const { act, data } = useBackend(context);
+
+ const [currentPage, setCurrentPage]
+ = useLocalState(
+ context,
+ "currentPage",
+ props.startingPage ?? GamePreferencesSelectedPage.Settings,
+ );
+
+ let pageContents;
+
+ switch (currentPage) {
+ case GamePreferencesSelectedPage.Keybindings:
+ pageContents = ;
+ break;
+ case GamePreferencesSelectedPage.Settings:
+ pageContents = ;
+ break;
+ default:
+ exhaustiveCheck(currentPage);
+ }
+
+
+ return (
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
+ Keybindings
+
+
+
+
+
+
+
+
+ {pageContents}
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx
new file mode 100644
index 00000000000..7e0245ed766
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx
@@ -0,0 +1,102 @@
+import { binaryInsertWith, sortBy } from "common/collections";
+import { InfernoNode } from "inferno";
+import { useBackend } from "../../backend";
+import { Box, Flex, Tooltip } from "../../components";
+import { PreferencesMenuData } from "./data";
+import features from "./preferences/features";
+import { FeatureValueInput } from "./preferences/features/base";
+import { TabbedMenu } from "./TabbedMenu";
+
+type PreferenceChild = {
+ name: string,
+ children: InfernoNode,
+};
+
+const binaryInsertPreference = binaryInsertWith(
+ (child) => child.name,
+);
+
+const sortByName = sortBy<[string, PreferenceChild[]]>(([name]) => name);
+
+export const GamePreferencesPage = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const gamePreferences: Record = {};
+
+ for (const [featureId, value] of Object.entries(
+ data.character_preferences.game_preferences
+ )) {
+ const feature = features[featureId];
+
+ let nameInner: InfernoNode = feature?.name || featureId;
+
+ if (feature?.description) {
+ nameInner = (
+
+ {nameInner}
+
+ );
+ }
+
+ let name: InfernoNode = (
+
+ {nameInner}
+
+ );
+
+ if (feature?.description) {
+ name = (
+
+ {name}
+
+ );
+ }
+
+ const child = (
+
+ {name}
+
+
+ {feature && || (
+
+ ...is not filled out properly!!!
+
+ )}
+
+
+ );
+
+ const entry = {
+ name: feature?.name || featureId,
+ children: child,
+ };
+
+ const category = feature?.category || "ERROR";
+
+ gamePreferences[category]
+ = binaryInsertPreference(gamePreferences[category] || [], entry);
+ }
+
+ const gamePreferenceEntries: [string, InfernoNode][] = sortByName(
+ Object.entries(gamePreferences)
+ ).map(
+ ([category, preferences]) => {
+ return [category, preferences.map(entry => entry.children)];
+ });
+
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/JobsPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/JobsPage.tsx
new file mode 100644
index 00000000000..46f6fa2bf63
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/JobsPage.tsx
@@ -0,0 +1,441 @@
+import { binaryInsertWith } from "common/collections";
+import { classes } from "common/react";
+import { InfernoNode } from "inferno";
+import { useBackend } from "../../backend";
+import { Box, Button, Dropdown, Stack, Tooltip } from "../../components";
+import { createSetPreference, JoblessRole, JobPriority, PreferencesMenuData } from "./data";
+import { Job } from "./jobs/base";
+import * as Departments from "./jobs/departments";
+
+const requireJob = require.context("./jobs/jobs", false, /.ts$/);
+const jobsByDepartment = new Map();
+
+const binaryInsertJob = binaryInsertWith((job: Job) => {
+ return job.name;
+});
+
+const PRIORITY_BUTTON_SIZE = "18px";
+
+for (const jobKey of requireJob.keys()) {
+ const job = requireJob<{
+ default?: Job,
+ }>(jobKey).default;
+
+ if (!job) {
+ continue;
+ }
+
+
+ let departmentInfo = jobsByDepartment.get(job.department);
+ if (departmentInfo === undefined) {
+ departmentInfo = {
+ jobs: [],
+ head: undefined,
+ };
+
+ jobsByDepartment.set(job.department, departmentInfo);
+ }
+
+ if (job.department.head === job.name) {
+ departmentInfo.head = job;
+ } else {
+ departmentInfo.jobs = binaryInsertJob(departmentInfo.jobs, job);
+ }
+}
+
+const PriorityButton = (props: {
+ name: string,
+ color: string,
+ modifier?: string,
+ enabled: boolean,
+ onClick: () => void,
+}) => {
+ const className = `PreferencesMenu__Jobs__departments__priority`;
+
+ return (
+
+
+
+ );
+};
+
+type CreateSetPriority = (priority: JobPriority | null) => () => void;
+
+const createSetPriorityCache: Record = {};
+
+const createCreateSetPriorityFromName
+ = (context, jobName: string): CreateSetPriority => {
+ if (createSetPriorityCache[jobName] !== undefined) {
+ return createSetPriorityCache[jobName];
+ }
+
+ const perPriorityCache: Map void> = new Map();
+
+ const createSetPriority = (priority: JobPriority | null) => {
+ const existingCallback = perPriorityCache.get(priority);
+ if (existingCallback !== undefined) {
+ return existingCallback;
+ }
+
+ const setPriority = () => {
+ const { act } = useBackend(context);
+
+ act("set_job_preference", {
+ job: jobName,
+ level: priority,
+ });
+ };
+
+ perPriorityCache.set(priority, setPriority);
+ return setPriority;
+ };
+
+ createSetPriorityCache[jobName] = createSetPriority;
+
+ return createSetPriority;
+ };
+
+const PriorityHeaders = () => {
+ const className = "PreferencesMenu__Jobs__PriorityHeader";
+
+ return (
+
+
+
+
+ Off
+
+
+
+ Low
+
+
+
+ Medium
+
+
+
+ High
+
+
+ );
+};
+
+const PriorityButtons = (props: {
+ createSetPriority: CreateSetPriority,
+ isOverflow: boolean,
+ priority: JobPriority,
+}) => {
+ const { createSetPriority, isOverflow, priority } = props;
+
+ return (
+
+ {isOverflow
+ ? (
+ <>
+
+
+
+ >
+ )
+ : (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+const JobRow = (props: {
+ className?: string,
+ job: Job,
+}, context) => {
+ const { data } = useBackend(context);
+ const { job } = props;
+
+ const isOverflow = data.overflow_role === job.name;
+ const priority = data.job_preferences[job.name];
+
+ const createSetPriority = createCreateSetPriorityFromName(context, job.name);
+
+ const experienceNeeded = data.job_required_experience
+ && data.job_required_experience[job.name];
+ const daysLeft = data.job_days_left ? data.job_days_left[job.name] : 0;
+
+ let rightSide: InfernoNode;
+
+ if (experienceNeeded) {
+ const { experience_type, required_playtime } = experienceNeeded;
+ const hoursNeeded = Math.ceil(required_playtime / 60);
+
+ rightSide = (
+
+
+ {hoursNeeded}h as {experience_type}
+
+
+ );
+ } else if (daysLeft > 0) {
+ rightSide = (
+
+
+ {daysLeft} day{daysLeft === 1 ? "" : "s"} left
+
+
+ );
+ } else if (data.job_bans && data.job_bans.indexOf(job.name) !== -1) {
+ rightSide = (
+
+
+ Banned
+
+
+ );
+ } else {
+ rightSide = ( );
+ }
+
+ return (
+
+
+
+
+
+ {props.job.name}
+
+
+
+
+ {rightSide}
+
+
+
+ );
+};
+
+const Department = (props: {
+ department: Departments.Department,
+ name: string,
+}) => {
+ const { department, name } = props;
+ const jobs = jobsByDepartment.get(department);
+ const className = `PreferencesMenu__Jobs__departments--${name}`;
+
+ if (!jobs) {
+ return (
+
+ ERROR: Department {name} could not be found!
+
+ );
+ }
+
+ return (
+
+
+ {jobs.head
+ && }
+ {jobs.jobs.map((job) => {
+ if (job === jobs.head) {
+ return null;
+ }
+
+ return ;
+ })}
+
+
+ );
+};
+
+// *Please* find a better way to do this, this is RIDICULOUS.
+// All I want is for a gap to pretend to be an empty space.
+// But in order for everything to align, I also need to add the 0.2em padding.
+// But also, we can't be aligned with names that break into multiple lines!
+const Gap = (props: {
+ amount: number,
+}) => {
+ // 0.2em comes from the padding-bottom in the department listing
+ return ;
+};
+
+const JoblessRoleDropdown = (props, context) => {
+ const { act, data } = useBackend(context);
+ const selected = data.character_preferences.misc.joblessrole;
+
+ const options = [
+ {
+ displayText: `Join as ${data.overflow_role} if unavailable`,
+ value: JoblessRole.BeOverflow,
+ },
+ {
+ displayText: `Join as a random job if unavailable`,
+ value: JoblessRole.BeRandomJob,
+ },
+ {
+ displayText: `Return to lobby if unavailable`,
+ value: JoblessRole.ReturnToLobby,
+ },
+ ];
+
+ return (
+
+
+ {options.find(option => option.value === selected)!.displayText}
+
+ }
+ />
+
+ );
+};
+
+export const JobsPage = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/KeybindingsPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/KeybindingsPage.tsx
new file mode 100644
index 00000000000..7044dfe29c4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/KeybindingsPage.tsx
@@ -0,0 +1,446 @@
+import { Component } from "inferno";
+import { Box, Button, KeyListener, Stack, Tooltip, TrackOutsideClicks } from "../../components";
+import { resolveAsset } from "../../assets";
+import { PreferencesMenuData } from "./data";
+import { useBackend } from "../../backend";
+import { range, sortBy } from "common/collections";
+import { KeyEvent } from "../../events";
+import { TabbedMenu } from "./TabbedMenu";
+import { fetchRetry } from "../../http";
+
+type Keybinding = {
+ name: string;
+ description?: string;
+};
+
+type Keybindings = Record>;
+
+type KeybindingsPageState = {
+ keybindings?: Keybindings;
+ lastKeyboardEvent?: KeyboardEvent;
+ selectedKeybindings?: PreferencesMenuData["keybindings"];
+
+ /**
+ * The current hotkey that the user is rebinding.
+ *
+ * First element is the hotkey name, the second is the slot.
+ */
+ rebindingHotkey?: [string, number];
+};
+
+const isStandardKey = (event: KeyboardEvent): boolean => {
+ return event.key !== "Alt"
+ && event.key !== "Control"
+ && event.key !== "Shift"
+ && event.key !== "Esc";
+};
+
+const KEY_CODE_TO_BYOND: Record = {
+ "DEL": "Delete",
+ "DOWN": "South",
+ "END": "Southwest",
+ "HOME": "Northwest",
+ "INSERT": "Insert",
+ "LEFT": "West",
+ "PAGEDOWN": "Southeast",
+ "PAGEUP": "Northeast",
+ "RIGHT": "East",
+ "SPACEBAR": "Space",
+ "UP": "North",
+};
+
+const sortKeybindings = sortBy(
+ ([_, keybinding]: [string, Keybinding]) => {
+ return keybinding.name;
+ });
+
+const sortKeybindingsByCategory = sortBy(
+ ([category, _]: [string, Record]) => {
+ return category;
+ });
+
+const formatKeyboardEvent = (event: KeyboardEvent): string => {
+ let text = "";
+
+ if (event.altKey) {
+ text += "Alt";
+ }
+
+ if (event.ctrlKey) {
+ text += "Ctrl";
+ }
+
+ if (event.shiftKey) {
+ text += "Shift";
+ }
+
+ if (event.location === KeyboardEvent.DOM_KEY_LOCATION_NUMPAD) {
+ text += "Numpad";
+ }
+
+ if (isStandardKey(event)) {
+ const key = event.key.toUpperCase();
+ text += KEY_CODE_TO_BYOND[key] || key;
+ }
+
+ return text;
+};
+
+const moveToBottom = (entries: [string, unknown][], findCategory: string) => {
+ entries.push(
+ entries.splice(
+ entries.findIndex(([category, _]) => {
+ return category === findCategory;
+ }), 1
+ )[0]
+ );
+};
+
+class KeybindingButton extends Component<{
+ currentHotkey?: string,
+ onClick?: () => void,
+ typingHotkey?: string,
+}> {
+ shouldComponentUpdate(nextProps) {
+ return this.props.typingHotkey !== nextProps.typingHotkey
+ || this.props.currentHotkey !== nextProps.currentHotkey;
+ }
+
+ render() {
+ const {
+ currentHotkey,
+ onClick,
+ typingHotkey,
+ } = this.props;
+
+ const child = (
+
+ {typingHotkey || currentHotkey || "Unbound"}
+
+ );
+
+ if (typingHotkey && onClick) {
+ return (
+ // onClick will cancel it
+
+ {child}
+
+ );
+ } else {
+ return child;
+ }
+ }
+}
+
+const KeybindingName = (props: {
+ keybinding: Keybinding,
+}) => {
+ const { keybinding } = props;
+
+ return keybinding.description
+ ? (
+
+
+ {keybinding.name}
+
+
+ )
+ : {keybinding.name} ;
+};
+
+KeybindingName.defaultHooks = {
+ onComponentShouldUpdate: (lastProps, nextProps) => {
+ return lastProps.keybinding !== nextProps.keybinding;
+ },
+};
+
+const ResetToDefaultButton = (props: {
+ keybindingId: string,
+}, context) => {
+ const { act } = useBackend(context);
+
+ return (
+ {
+ act("reset_keybinds_to_defaults", {
+ keybind_name: props.keybindingId,
+ });
+ }}
+ >
+ Reset to Defaults
+
+ );
+};
+
+export class KeybindingsPage extends Component<{}, KeybindingsPageState> {
+ cancelNextKeyUp?: number;
+ keybindingOnClicks: Record void)[]> = {};
+ lastKeybinds?: PreferencesMenuData["keybindings"];
+
+ state: KeybindingsPageState = {
+ lastKeyboardEvent: undefined,
+ keybindings: undefined,
+ selectedKeybindings: undefined,
+ rebindingHotkey: undefined,
+ };
+
+ constructor() {
+ super();
+
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handleKeyUp = this.handleKeyUp.bind(this);
+ }
+
+ componentDidMount() {
+ this.populateSelectedKeybindings();
+ this.populateKeybindings();
+ }
+
+ componentDidUpdate() {
+ const { data } = useBackend(this.context);
+
+ // keybindings is static data, so it'll pass `===` checks.
+ // This'll change when resetting to defaults.
+ if (data.keybindings !== this.lastKeybinds) {
+ this.populateSelectedKeybindings();
+ }
+ }
+
+ setRebindingHotkey(value?: string) {
+ const { act } = useBackend(this.context);
+
+ this.setState((state) => {
+ let selectedKeybindings = state.selectedKeybindings;
+ if (!selectedKeybindings) {
+ return state;
+ }
+
+ if (!state.rebindingHotkey) {
+ return state;
+ }
+
+ selectedKeybindings = { ...selectedKeybindings };
+
+ const [keybindName, slot] = state.rebindingHotkey;
+
+ if (selectedKeybindings[keybindName]) {
+ if (value) {
+ selectedKeybindings[keybindName][Math.min(
+ selectedKeybindings[keybindName].length,
+ slot
+ )] = value;
+ } else {
+ selectedKeybindings[keybindName].splice(slot, 1);
+ }
+ } else if (!value) {
+ return state;
+ } else {
+ selectedKeybindings[keybindName] = [value];
+ }
+
+ act("set_keybindings", {
+ "keybind_name": keybindName,
+ "hotkeys": selectedKeybindings[keybindName],
+ });
+
+ return {
+ lastKeyboardEvent: undefined,
+ rebindingHotkey: undefined,
+ selectedKeybindings,
+ };
+ });
+ }
+
+ handleKeyDown(keyEvent: KeyEvent) {
+ const event = keyEvent.event;
+ const rebindingHotkey = this.state.rebindingHotkey;
+
+ if (!rebindingHotkey) {
+ return;
+ }
+
+ event.preventDefault();
+
+ this.cancelNextKeyUp = keyEvent.code;
+
+ if (isStandardKey(event)) {
+ this.setRebindingHotkey(formatKeyboardEvent(event));
+ return;
+ } else if (event.key === "Esc") {
+ this.setRebindingHotkey(undefined);
+ return;
+ }
+
+ this.setState({
+ lastKeyboardEvent: event,
+ });
+ }
+
+ handleKeyUp(keyEvent: KeyEvent) {
+ if (this.cancelNextKeyUp === keyEvent.code) {
+ this.cancelNextKeyUp = undefined;
+ keyEvent.event.preventDefault();
+ }
+ }
+
+ getKeybindingOnClick(
+ keybindingId: string,
+ slot: number,
+ ): () => void {
+ if (!this.keybindingOnClicks[keybindingId]) {
+ this.keybindingOnClicks[keybindingId] = [];
+ }
+
+ if (!this.keybindingOnClicks[keybindingId][slot]) {
+ this.keybindingOnClicks[keybindingId][slot] = () => {
+ if (this.state.rebindingHotkey === undefined) {
+ this.setState({
+ lastKeyboardEvent: undefined,
+ rebindingHotkey: [keybindingId, slot],
+ });
+ } else {
+ this.setState({
+ lastKeyboardEvent: undefined,
+ rebindingHotkey: undefined,
+ });
+ }
+ };
+ }
+
+ return this.keybindingOnClicks[keybindingId][slot];
+ }
+
+ getTypingHotkey(keybindingId: string, slot: number): string | undefined {
+ const { lastKeyboardEvent, rebindingHotkey } = this.state;
+
+ if (!rebindingHotkey) {
+ return undefined;
+ }
+
+ if (rebindingHotkey[0] !== keybindingId
+ || rebindingHotkey[1] !== slot
+ ) {
+ return undefined;
+ }
+
+ if (lastKeyboardEvent === undefined) {
+ return "...";
+ }
+
+ return formatKeyboardEvent(lastKeyboardEvent);
+ }
+
+ async populateKeybindings() {
+ const keybindingsResponse = await fetchRetry(resolveAsset("keybindings.json"));
+ const keybindingsData: Keybindings = await keybindingsResponse.json();
+
+ this.setState({
+ keybindings: keybindingsData,
+ });
+ }
+
+ populateSelectedKeybindings() {
+ const { data } = useBackend(this.context);
+
+ this.lastKeybinds = data.keybindings;
+
+ this.setState({
+ selectedKeybindings: Object.fromEntries(
+ Object.entries(data.keybindings)
+ .map(([keybind, hotkeys]) => {
+ return [keybind, hotkeys.filter(value => value !== "Unbound")];
+ })
+ ),
+ });
+ }
+
+ render() {
+ const keybindings = this.state.keybindings;
+
+ if (!keybindings) {
+ return Loading keybindings... ;
+ }
+
+ const keybindingEntries = sortKeybindingsByCategory(
+ Object.entries(keybindings)
+ );
+
+ moveToBottom(keybindingEntries, "EMOTE");
+ moveToBottom(keybindingEntries, "ADMIN");
+
+ return (
+ <>
+
+
+ {
+ return [category, (
+
+ {sortKeybindings(Object.entries(keybindings)).map(
+ ([keybindingId, keybinding]) => {
+ const keys
+ = this.state.selectedKeybindings![keybindingId]
+ || [];
+
+ const name = (
+
+
+
+ );
+
+ return (
+
+
+ {name}
+
+ {range(0, 3).map(key => (
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+ }
+ )}
+
+ )];
+ }
+ )}
+ />
+ >
+ );
+ }
+}
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx
new file mode 100644
index 00000000000..c70f7bdd756
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx
@@ -0,0 +1,638 @@
+import { classes } from "common/react";
+import { sendAct, useBackend, useLocalState } from "../../backend";
+import { Autofocus, Box, Button, Flex, LabeledList, Popper, Stack, TrackOutsideClicks, Dropdown } from "../../components"; // SKYRAT EDIT CHANGE
+import { createSetPreference, PreferencesMenuData, RandomSetting } from "./data";
+import { CharacterPreview } from "./CharacterPreview";
+import { RandomizationButton } from "./RandomizationButton";
+import { ServerPreferencesFetcher } from "./ServerPreferencesFetcher";
+import { MultiNameInput, NameInput } from "./names";
+import { Gender, GENDERS } from "./preferences/gender";
+import features from "./preferences/features";
+import { FeatureChoicedServerData, FeatureValueInput } from "./preferences/features/base";
+import { filterMap, sortBy } from "common/collections";
+import { useRandomToggleState } from "./useRandomToggleState";
+
+const CLOTHING_CELL_SIZE = 48;
+const CLOTHING_SIDEBAR_ROWS = 9;
+
+const CLOTHING_SELECTION_CELL_SIZE = 48;
+const CLOTHING_SELECTION_WIDTH = 5.4;
+const CLOTHING_SELECTION_MULTIPLIER = 5.2;
+
+const CharacterControls = (props: {
+ handleRotate: () => void,
+ handleOpenSpecies: () => void,
+ handleAdvancedPrefs: () => void, // SKYRAT EDIT ADDITION
+ handleLoadout: () => void, // SKYRAT EDIT ADDITION
+ gender: Gender,
+ setGender: (gender: Gender) => void,
+ showGender: boolean,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+ {props.showGender && (
+
+
+
+ )}
+ {props.handleAdvancedPrefs && (
+ // SKYRAT EDIT ADDITION
+
+
+
+ )}
+ {props.handleLoadout && (
+ // SKYRAT EDIT ADDITION
+
+
+
+ )}
+
+ );
+};
+
+const ChoicedSelection = (props: {
+ name: string,
+ catalog: FeatureChoicedServerData,
+ selected: string,
+ supplementalFeature?: string,
+ supplementalValue?: unknown,
+ onClose: () => void,
+ onSelect: (value: string) => void,
+}, context) => {
+ const { act } = useBackend(context);
+
+ const {
+ catalog,
+ supplementalFeature,
+ supplementalValue,
+ } = props;
+
+ if (!catalog.icons) {
+ return (
+
+ Provided catalog had no icons!
+
+ );
+ }
+
+ return (
+
+
+
+
+ {supplementalFeature && (
+
+
+
+ )}
+
+
+
+ Select {props.name.toLowerCase()}
+
+
+
+
+
+ X
+
+
+
+
+
+
+
+
+ {Object.entries(catalog.icons).map(([name, image], index) => {
+ return (
+
+ {
+ props.onSelect(name);
+ }}
+ selected={name === props.selected}
+ tooltip={name}
+ tooltipPosition="right"
+ style={{
+ height: `${CLOTHING_SELECTION_CELL_SIZE}px`,
+ width: `${CLOTHING_SELECTION_CELL_SIZE}px`,
+ }}
+ >
+
+
+
+ );
+ })}
+
+
+
+
+
+ );
+};
+
+const GenderButton = (props: {
+ handleSetGender: (gender: Gender) => void,
+ gender: Gender,
+}, context) => {
+ const [genderMenuOpen, setGenderMenuOpen] = useLocalState(context, "genderMenuOpen", false);
+
+ return (
+
+ {[Gender.Male, Gender.Female, Gender.Other].map(gender => {
+ return (
+
+ {
+ props.handleSetGender(gender);
+ setGenderMenuOpen(false);
+ }}
+ fontSize="22px"
+ icon={GENDERS[gender].icon}
+ tooltip={GENDERS[gender].text}
+ tooltipPosition="top"
+ />
+
+ );
+ })}
+
+ )
+ )}>
+ {
+ setGenderMenuOpen(!genderMenuOpen);
+ }}
+ fontSize="22px"
+ icon={GENDERS[props.gender].icon}
+ tooltip="Gender"
+ tooltipPosition="top"
+ />
+
+ );
+};
+
+const MainFeature = (props: {
+ catalog: FeatureChoicedServerData & {
+ name: string,
+ supplemental_feature?: string,
+ },
+ currentValue: string,
+ isOpen: boolean,
+ handleClose: () => void,
+ handleOpen: () => void,
+ handleSelect: (newClothing: string) => void,
+ randomization?: RandomSetting,
+ setRandomization: (newSetting: RandomSetting) => void,
+}, context) => {
+ const { act, data } = useBackend(context);
+
+ const {
+ catalog,
+ currentValue,
+ isOpen,
+ handleOpen,
+ handleClose,
+ handleSelect,
+ randomization,
+ setRandomization,
+ } = props;
+
+ const supplementalFeature = catalog.supplemental_feature;
+
+ return (
+
+
+
+ )}>
+ {
+ if (isOpen) {
+ handleClose();
+ } else {
+ handleOpen();
+ }
+ }}
+ style={{
+ height: `${CLOTHING_CELL_SIZE}px`,
+ width: `${CLOTHING_CELL_SIZE}px`,
+ }}
+ position="relative"
+ tooltip={catalog.name}
+ tooltipPosition="right"
+ >
+
+
+ {(randomization && {
+ // We're a button inside a button.
+ // Did you know that's against the W3C standard? :)
+ event.cancelBubble = true;
+ event.stopPropagation();
+ },
+ }}
+ value={randomization}
+ setValue={setRandomization}
+ />)}
+
+
+ );
+};
+
+const createSetRandomization = (
+ act: typeof sendAct,
+ preference: string,
+) => (newSetting: RandomSetting) => {
+ act("set_random_preference", {
+ preference,
+ value: newSetting,
+ });
+};
+
+const sortPreferences = sortBy<[string, unknown]>(
+ ([featureId, _]) => {
+ const feature = features[featureId];
+ return feature?.name;
+ });
+
+const PreferenceList = (props: {
+ act: typeof sendAct,
+ preferences: Record,
+ randomizations: Record,
+}) => {
+ return (
+
+
+ {
+ sortPreferences(Object.entries(props.preferences))
+ .map(([featureId, value]) => {
+ const feature = features[featureId];
+ const randomSetting = props.randomizations[featureId];
+
+ if (feature === undefined) {
+ return (
+
+ Feature {featureId} is not recognized.
+
+ );
+ }
+
+ return (
+
+
+ {randomSetting && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+ })
+ }
+
+
+ );
+};
+
+export const MainPage = (props: {
+ openSpecies: () => void,
+}, context) => {
+ const { act, data } = useBackend(context);
+ const [currentClothingMenu, setCurrentClothingMenu]
+ = useLocalState(context, "currentClothingMenu", null);
+ const [multiNameInputOpen, setMultiNameInputOpen]
+ = useLocalState(context, "multiNameInputOpen", false);
+ const [randomToggleEnabled] = useRandomToggleState(context);
+
+ return (
+ {
+ const currentSpeciesData = serverData && serverData.species[
+ data
+ .character_preferences
+ .misc
+ .species
+ ];
+
+ const contextualPreferences
+ = data.character_preferences.secondary_features || [];
+
+ const mainFeatures = [
+ ...Object.entries(data.character_preferences.clothing),
+ ...Object.entries(data.character_preferences.features)
+ .filter(([featureName]) => {
+ if (!currentSpeciesData) {
+ return false;
+ }
+
+ return currentSpeciesData
+ .enabled_features
+ .indexOf(featureName) !== -1;
+ }),
+ ];
+
+ const randomBodyEnabled
+ = (data.character_preferences.non_contextual.random_body
+ !== RandomSetting.Disabled)
+ || randomToggleEnabled;
+
+ const getRandomization = (
+ preferences: Record
+ ): Record => {
+ if (!serverData) {
+ return {};
+ }
+
+ return Object.fromEntries(filterMap(
+ Object.keys(preferences), preferenceKey => {
+ if (serverData.random.randomizable.indexOf(preferenceKey) === -1) {
+ return undefined;
+ }
+
+ if (!randomBodyEnabled) {
+ return undefined;
+ }
+
+ return [
+ preferenceKey,
+ data.character_preferences.randomization[preferenceKey]
+ || RandomSetting.Disabled,
+ ];
+ }));
+ };
+
+ const randomizationOfMainFeatures
+ = getRandomization(Object.fromEntries(mainFeatures));
+
+ const nonContextualPreferences
+ = {
+ ...data.character_preferences.non_contextual,
+ };
+
+ if (randomBodyEnabled) {
+ nonContextualPreferences["random_species"]
+ = data.character_preferences.randomization["species"];
+ } else {
+ // We can't use random_name/is_accessible because the
+ // server doesn't know whether the random toggle is on.
+ delete nonContextualPreferences["random_name"];
+ }
+
+ return (
+ <>
+ {multiNameInputOpen && setMultiNameInputOpen(false)}
+ handleRandomizeName={preference => act("randomize_name", {
+ preference,
+ })}
+ handleUpdateName={(nameType, value) => act("set_preference", {
+ preference: nameType,
+ value,
+ })}
+ names={data.character_preferences.names}
+ />}
+
+
+
+
+
+ {
+ act("rotate");
+ }}
+ // SKYRAT EDIT ADDITION
+ handleAdvancedPrefs={() => {
+ act("open_advanced_prefs");
+ }}
+ handleLoadout={() => {
+ act("open_loadout");
+ }}
+ // SKYRAT EDIT END
+ setGender={createSetPreference(act, "gender")}
+ showGender={
+ currentSpeciesData ? !!currentSpeciesData.sexes : true
+ }
+ />
+
+
+
+
+
+
+ act('update_preview', {
+ updated_preview: value,
+ })} />
+
+ {
+ setMultiNameInputOpen(true);
+ }}
+ />
+
+
+
+
+
+
+
+ {mainFeatures
+ .map(([clothingKey, clothing]) => {
+ const catalog = (
+ serverData
+ && serverData[clothingKey] as
+ FeatureChoicedServerData & {
+ name: string,
+ }
+ );
+
+ return catalog && (
+
+ {
+ setCurrentClothingMenu(null);
+ }}
+ handleOpen={() => {
+ setCurrentClothingMenu(clothingKey);
+ }}
+ handleSelect={createSetPreference(act, clothingKey)}
+ randomization={
+ randomizationOfMainFeatures[clothingKey]
+ }
+ setRandomization={createSetRandomization(
+ act,
+ clothingKey,
+ )}
+ />
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ }} />
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/PageButton.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/PageButton.tsx
new file mode 100644
index 00000000000..8ac3de35975
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/PageButton.tsx
@@ -0,0 +1,30 @@
+import { InfernoNode } from "inferno";
+import { Button } from "../../components";
+
+export const PageButton = (props: {
+ currentPage: P,
+ page: P,
+ otherActivePages?: P[],
+
+ setPage: (page: P) => void,
+
+ children?: InfernoNode,
+}) => {
+ const pageIsActive = props.currentPage === props.page
+ || (
+ props.otherActivePages
+ && props.otherActivePages.indexOf(props.currentPage) !== -1
+ );
+
+ return (
+ props.setPage(props.page)}
+ >
+ {props.children}
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx
new file mode 100644
index 00000000000..75e688bf76f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx
@@ -0,0 +1,327 @@
+import { StatelessComponent } from "inferno";
+import { Box, Icon, Stack, Tooltip } from "../../components";
+import { PreferencesMenuData, Quirk } from "./data";
+import { useBackend, useLocalState } from "../../backend";
+import { ServerPreferencesFetcher } from "./ServerPreferencesFetcher";
+
+const getValueClass = (value: number): string => {
+ if (value > 0) {
+ return "positive";
+ } else if (value < 0) {
+ return "negative";
+ } else {
+ return "neutral";
+ }
+};
+
+const QuirkList = (props: {
+ quirks: [string, Quirk & {
+ failTooltip?: string;
+ }][],
+ onClick: (quirkName: string, quirk: Quirk) => void,
+}) => {
+ return (
+ // Stack is not used here for a variety of IE flex bugs
+
+ {props.quirks.map(([quirkKey, quirk]) => {
+ const className = "PreferencesMenu__Quirks__QuirkList__quirk";
+
+ const child = (
+ {
+ props.onClick(quirkKey, quirk);
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ {quirk.name}
+
+
+
+ {quirk.value}
+
+
+
+
+
+ {quirk.description}
+
+
+
+
+
+ );
+
+ if (quirk.failTooltip) {
+ return (
+
+ {child}
+
+ );
+ } else {
+ return child;
+ }
+ })}
+
+ );
+};
+
+const StatDisplay: StatelessComponent<{}> = (props) => {
+ return (
+
+ {props.children}
+
+ );
+};
+
+export const QuirksPage = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const [selectedQuirks, setSelectedQuirks] = useLocalState(
+ context,
+ `selectedQuirks_${data.active_slot}`,
+ data.selected_quirks,
+ );
+
+ return (
+ {
+ if (!data) {
+ return Loading quirks... ;
+ }
+
+ const {
+ max_positive_quirks: maxPositiveQuirks,
+ quirk_blacklist: quirkBlacklist,
+ quirk_info: quirkInfo,
+ } = data.quirks;
+
+ const quirks = Object.entries(quirkInfo);
+ quirks.sort(([_, quirkA], [__, quirkB]) => {
+ if (quirkA.value === quirkB.value) {
+ return (quirkA.name > quirkB.name) ? 1 : -1;
+ } else {
+ return quirkA.value - quirkB.value;
+ }
+ });
+
+ let balance = 0;
+ let positiveQuirks = 0;
+
+ for (const selectedQuirkName of selectedQuirks) {
+ const selectedQuirk = quirkInfo[selectedQuirkName];
+ if (!selectedQuirk) {
+ continue;
+ }
+
+ if (selectedQuirk.value > 0) {
+ positiveQuirks += 1;
+ }
+
+ balance += selectedQuirk.value;
+ }
+
+ const getReasonToNotAdd = (quirkName: string) => {
+ const quirk = quirkInfo[quirkName];
+
+ if (
+ quirk.value > 0
+ ) {
+ if (positiveQuirks >= maxPositiveQuirks) {
+ return "You can't have any more positive quirks!";
+ } else if (balance + quirk.value > 0) {
+ return "You need a negative quirk to balance this out!";
+ }
+ }
+
+ const selectedQuirkNames = selectedQuirks.map(quirkKey => {
+ return quirkInfo[quirkKey].name;
+ });
+
+ for (const blacklist of quirkBlacklist) {
+ if (blacklist.indexOf(quirk.name) === -1) {
+ continue;
+ }
+
+ for (const incompatibleQuirk of blacklist) {
+ if (
+ incompatibleQuirk !== quirk.name
+ && selectedQuirkNames.indexOf(incompatibleQuirk) !== -1
+ ) {
+ return `This is incompatible with ${incompatibleQuirk}!`;
+ }
+ }
+ }
+
+ return undefined;
+ };
+
+ const getReasonToNotRemove = (quirkName: string) => {
+ const quirk = quirkInfo[quirkName];
+
+ if (balance - quirk.value > 0) {
+ return "You need to remove a positive quirk first!";
+ }
+
+ return undefined;
+ };
+
+ return (
+
+
+
+
+
+ Positive Quirks
+
+
+
+
+
+ {positiveQuirks} / {maxPositiveQuirks}
+
+
+
+
+
+ Available Quirks
+
+
+
+
+ {
+ if (getReasonToNotAdd(quirkName) !== undefined) {
+ return;
+ }
+
+ setSelectedQuirks(selectedQuirks.concat(quirkName));
+
+ act("give_quirk", { quirk: quirk.name });
+ }}
+ quirks={quirks.filter(([quirkName, _]) => {
+ return selectedQuirks
+ .indexOf(quirkName) === -1;
+ }).map(([quirkName, quirk]) => {
+ return [quirkName, {
+ ...quirk,
+ failTooltip: getReasonToNotAdd(quirkName),
+ }];
+ })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ Quirk Balance
+
+
+
+
+
+ {balance}
+
+
+
+
+
+ Current Quirks
+
+
+
+
+ {
+ if (getReasonToNotRemove(quirkName) !== undefined) {
+ return;
+ }
+
+ setSelectedQuirks(
+ selectedQuirks
+ .filter(otherQuirk => quirkName !== otherQuirk),
+ );
+
+ act("remove_quirk", { quirk: quirk.name });
+ }}
+ quirks={quirks.filter(([quirkName, _]) => {
+ return selectedQuirks
+ .indexOf(quirkName) !== -1;
+ }).map(([quirkName, quirk]) => {
+ return [quirkName, {
+ ...quirk,
+ failTooltip: getReasonToNotRemove(quirkName),
+ }];
+ })}
+ />
+
+
+
+
+ );
+ }}
+ />
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/RandomizationButton.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/RandomizationButton.tsx
new file mode 100644
index 00000000000..ea9a4a64a60
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/RandomizationButton.tsx
@@ -0,0 +1,64 @@
+import { Dropdown, Icon } from "../../components";
+import { RandomSetting } from "./data";
+import { exhaustiveCheck } from "common/exhaustive";
+
+export const RandomizationButton = (props: {
+ dropdownProps?: Record,
+ setValue: (newValue: RandomSetting) => void,
+ value: RandomSetting,
+}) => {
+ const {
+ dropdownProps = {},
+ setValue,
+ value,
+ } = props;
+
+ let color;
+
+ switch (value) {
+ case RandomSetting.AntagOnly:
+ color = "orange";
+ break;
+ case RandomSetting.Disabled:
+ color = "red";
+ break;
+ case RandomSetting.Enabled:
+ color = "green";
+ break;
+ default:
+ exhaustiveCheck(value);
+ }
+
+ return (
+
+ )}
+ options={[
+ {
+ displayText: "Do not randomize",
+ value: RandomSetting.Disabled,
+ },
+
+ {
+ displayText: "Always randomize",
+ value: RandomSetting.Enabled,
+ },
+
+ {
+ displayText: "Randomize when antagonist",
+ value: RandomSetting.AntagOnly,
+ },
+ ]}
+ nochevron
+ onSelected={setValue}
+ width="auto"
+ />
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx
new file mode 100644
index 00000000000..d21cff506ac
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx
@@ -0,0 +1,38 @@
+import { Component, InfernoNode } from "inferno";
+import { resolveAsset } from "../../assets";
+import { fetchRetry } from "../../http";
+import { ServerData } from "./data";
+
+// Cache response so it's only sent once
+let fetchServerData: Promise | undefined;
+
+export class ServerPreferencesFetcher extends Component<{
+ render: (serverData: ServerData | undefined) => InfernoNode,
+}, {
+ serverData?: ServerData;
+}> {
+ state = {
+ serverData: undefined,
+ };
+
+ componentDidMount() {
+ this.populateServerData();
+ }
+
+ async populateServerData() {
+ if (!fetchServerData) {
+ fetchServerData = fetchRetry(resolveAsset("preferences.json"))
+ .then(response => response.json());
+ }
+
+ const preferencesData: ServerData = await fetchServerData;
+
+ this.setState({
+ serverData: preferencesData,
+ });
+ }
+
+ render() {
+ return this.props.render(this.state.serverData);
+ }
+}
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/SpeciesPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/SpeciesPage.tsx
new file mode 100644
index 00000000000..237722163e4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/SpeciesPage.tsx
@@ -0,0 +1,360 @@
+import { classes } from "common/react";
+import { useBackend } from "../../backend";
+import { BlockQuote, Box, Button, Divider, Icon, Section, Stack, Tooltip } from "../../components";
+import { CharacterPreview } from "./CharacterPreview";
+import { createSetPreference, Food, PreferencesMenuData, ServerData, ServerSpeciesData } from "./data";
+import { Feature, Species, fallbackSpecies } from "./preferences/species/base";
+import { ServerPreferencesFetcher } from "./ServerPreferencesFetcher";
+
+const requireSpecies = require.context("./preferences/species");
+
+const FOOD_ICONS = {
+ [Food.Cloth]: "tshirt",
+ [Food.Dairy]: "cheese",
+ [Food.Fried]: "bacon",
+ [Food.Fruit]: "apple-alt",
+ [Food.Grain]: "bread-slice",
+ [Food.Gross]: "trash",
+ [Food.Junkfood]: "pizza-slice",
+ [Food.Meat]: "hamburger",
+ [Food.Raw]: "drumstick-bite",
+ [Food.Sugar]: "candy-cane",
+ [Food.Toxic]: "biohazard",
+ [Food.Vegetables]: "carrot",
+};
+
+const FOOD_NAMES: Record = {
+ [Food.Cloth]: "Clothing",
+ [Food.Dairy]: "Dairy",
+ [Food.Fried]: "Fried food",
+ [Food.Fruit]: "Fruit",
+ [Food.Grain]: "Grain",
+ [Food.Gross]: "Gross food",
+ [Food.Junkfood]: "Junk food",
+ [Food.Meat]: "Meat",
+ [Food.Raw]: "Raw",
+ [Food.Sugar]: "Sugar",
+ [Food.Toxic]: "Toxic food",
+ [Food.Vegetables]: "Vegetables",
+};
+
+const IGNORE_UNLESS_LIKED: Set = new Set([
+ Food.Cloth,
+ Food.Gross,
+ Food.Toxic,
+]);
+
+const notIn = function (set: Set) {
+ return (value: T) => {
+ return !set.has(value);
+ };
+};
+
+const FoodList = (props: {
+ food: Food[],
+ icon: string,
+ name: string,
+ className: string,
+}) => {
+ if (props.food.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {props.name}
+
+
+ {props.food
+ .reduce((names, food) => {
+ const foodName = FOOD_NAMES[food];
+ return foodName ? names.concat(foodName) : names;
+ }, []).join(", ")}
+
+
+ }>
+
+ {props.food.map(food => {
+ return FOOD_ICONS[food]
+ && (
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+const Diet = (props: {
+ likedFood: Food[],
+ dislikedFood: Food[],
+ toxicFood: Food[],
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const SpeciesFeature = (props: {
+ className: string,
+ feature: Feature,
+}) => {
+ const { className, feature } = props;
+
+ return (
+
+ {feature.name}
+
+ {feature.description}
+
+ }>
+
+
+
+
+ );
+};
+
+const SpeciesFeatures = (props: {
+ features: Species["features"],
+}) => {
+ const { good, neutral, bad } = props.features;
+
+ return (
+
+
+
+ {good.map(feature => {
+ return (
+
+
+
+ );
+ })}
+
+
+
+
+ {neutral.map(feature => {
+ return (
+
+
+
+ );
+ })}
+
+
+
+ {bad.map(feature => {
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+const SpeciesPageInner = (props: {
+ handleClose: () => void,
+ species: ServerData["species"],
+}, context) => {
+ const { act, data } = useBackend(context);
+ const setSpecies = createSetPreference(act, "species");
+
+ let species: [string, Species & ServerSpeciesData][]
+ = Object.entries(props.species)
+ .map(([species, serverData]) => {
+ return [
+ species,
+ {
+ ...serverData,
+ ...(requireSpecies.keys().indexOf(`./${species}`) === -1
+ ? fallbackSpecies
+ : requireSpecies(`./${species}`).default) as Species,
+ },
+ ];
+ });
+
+ // Humans are always the top of the list
+ const humanIndex = species.findIndex(([species]) => species === "human");
+ const swapWith = species[0];
+ species[0] = species[humanIndex];
+ species[humanIndex] = swapWith;
+
+ const currentSpecies = species.filter(([speciesKey]) => {
+ return speciesKey === data.character_preferences.misc.species;
+ })[0][1];
+
+ const { lore } = currentSpecies;
+
+ return (
+
+
+
+ Go back
+
+
+
+
+
+
+
+ {species.map(([speciesKey, species]) => {
+ return (
+
+ setSpecies(speciesKey)}
+ selected={
+ data.character_preferences.misc.species === speciesKey
+ }
+ tooltip={species.name}
+ style={{
+ height: "64px",
+ width: "64px",
+ }}
+ >
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ )
+ }>
+
+ {currentSpecies.description}
+
+
+
+
+
+
+
+
+
+
+
+
+ {lore && (
+
+
+
+ {lore.map((text, index) => (
+
+ {text}
+ {index !== lore.length - 1
+ && (<> >)}
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export const SpeciesPage = (props: {
+ closeSpecies: () => void,
+}) => {
+ return (
+ {
+ if (serverData) {
+ return ( );
+ } else {
+ return Loading species... ;
+ }
+ }}
+ />
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/TabbedMenu.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/TabbedMenu.tsx
new file mode 100644
index 00000000000..450bf0dc3e4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/TabbedMenu.tsx
@@ -0,0 +1,94 @@
+import { Component, createRef, InfernoNode, RefObject } from "inferno";
+import { Button, Section, Stack } from "../../components";
+import { FlexProps } from "../../components/Flex";
+
+type TabbedMenuProps = {
+ categoryEntries: [string, InfernoNode][],
+ contentProps?: FlexProps,
+};
+
+export class TabbedMenu extends Component {
+ categoryRefs: Record> = {};
+ sectionRef: RefObject = createRef();
+
+ getCategoryRef(category: string): RefObject {
+ if (!this.categoryRefs[category]) {
+ this.categoryRefs[category] = createRef();
+ }
+
+ return this.categoryRefs[category];
+ }
+
+ render() {
+ return (
+
+
+
+ {this.props.categoryEntries.map(([category]) => {
+ return (
+
+ {
+ const offsetTop = this.categoryRefs[category]
+ .current?.offsetTop;
+
+ if (offsetTop === undefined) {
+ return;
+ }
+
+ const currentSection = this.sectionRef.current;
+
+ if (!currentSection) {
+ return;
+ }
+
+ currentSection.scrollTop = offsetTop;
+ }}
+ >
+ {category}
+
+
+ );
+ })}
+
+
+
+
+
+ {this.props.categoryEntries.map(
+ ([category, children]) => {
+ return (
+
+
+
+ );
+ }
+ )}
+
+
+
+ );
+ }
+}
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/abductor.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/abductor.ts
new file mode 100644
index 00000000000..9f0abe6da87
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/abductor.ts
@@ -0,0 +1,24 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const Abductor: Antagonist = {
+ key: "abductor",
+ name: "Abductor",
+ description: [
+ multiline`
+ Abductors are technologically advanced alien society set on cataloging
+ all species in the system. Unfortunately for their subjects their methods
+ are quite invasive.
+ `,
+
+ multiline`
+ You and a partner will become the abductor scientist and agent duo.
+ As an agent, abduct unassuming victims and bring them back to your UFO.
+ As a scientist, scout out victims for your agent, keep them safe, and
+ operate on whoever they bring back.
+ `,
+ ],
+ category: Category.Midround,
+};
+
+export default Abductor;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blob.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blob.ts
new file mode 100644
index 00000000000..1d2de66ab48
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blob.ts
@@ -0,0 +1,20 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+export const BLOB_MECHANICAL_DESCRIPTION = multiline`
+ The blob infests the station and destroys everything in its path, including
+ hull, fixtures, and creatures. Spread your mass, collect resources, and
+ consume the entire station. Make sure to prepare your defenses, because the
+ crew will be alerted to your presence!
+`;
+
+const Blob: Antagonist = {
+ key: "blob",
+ name: "Blob",
+ description: [
+ BLOB_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Midround,
+};
+
+export default Blob;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blobinfection.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blobinfection.ts
new file mode 100644
index 00000000000..45a8a03dc84
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blobinfection.ts
@@ -0,0 +1,18 @@
+import { Antagonist, Category } from "../base";
+import { BLOB_MECHANICAL_DESCRIPTION } from "./blob";
+import { multiline } from "common/string";
+
+const BlobInfection: Antagonist = {
+ key: "blobinfection",
+ name: "Blob Infection",
+ description: [
+ multiline`
+ At any point in the middle of the shift, be strucken with an infection
+ that will turn you into the terrifying blob.
+ `,
+ BLOB_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Midround,
+};
+
+export default BlobInfection;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodbrother.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodbrother.ts
new file mode 100644
index 00000000000..3a05cf1ec00
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodbrother.ts
@@ -0,0 +1,17 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const BloodBrother: Antagonist = {
+ key: "bloodbrother",
+ name: "Blood Brother",
+ description: [
+ multiline`
+ Team up with other crew members as blood brothers to combine the strengths
+ of your departments, break each other out of prison, and overwhelm the
+ station.
+ `,
+ ],
+ category: Category.Roundstart,
+};
+
+export default BloodBrother;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changeling.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changeling.ts
new file mode 100644
index 00000000000..114e6413850
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changeling.ts
@@ -0,0 +1,21 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const Changeling: Antagonist = {
+ key: "changeling",
+ 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,
+};
+
+export default Changeling;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/clownoperative.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/clownoperative.ts
new file mode 100644
index 00000000000..ac305e2fe8b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/clownoperative.ts
@@ -0,0 +1,21 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+import { OPERATIVE_MECHANICAL_DESCRIPTION } from "./operative";
+
+const ClownOperative: Antagonist = {
+ key: "clownoperative",
+ name: "Clown Operative",
+ description: [
+ multiline`
+ Honk! You have been chosen, for better or worse to join the Syndicate
+ Clown Operative strike team. Your mission, whether or not you choose
+ to tickle it, is to honk Nanotrasen's most advanced research facility!
+ That's right, you're going to Clown Station 13.
+ `,
+
+ OPERATIVE_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Roundstart,
+};
+
+export default ClownOperative;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/cultist.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/cultist.ts
new file mode 100644
index 00000000000..ea27413786a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/cultist.ts
@@ -0,0 +1,23 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const Cultist: Antagonist = {
+ key: "cultist",
+ name: "Cultist",
+ description: [
+ multiline`
+ The Geometer of Blood, Nar-Sie, has sent a number of her followers to
+ Space Station 13. As a cultist, you have an abundance of cult magics at
+ your disposal, something for all situations. You must work with your
+ brethren to summon an avatar of your eldritch goddess!
+ `,
+
+ multiline`
+ Armed with blood magic, convert crew members to the Blood Cult, sacrifice
+ those who get in the way, and summon Nar-Sie.
+ `,
+ ],
+ category: Category.Roundstart,
+};
+
+export default Cultist;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/familyheadaspirant.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/familyheadaspirant.ts
new file mode 100644
index 00000000000..6ed2e8dda57
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/familyheadaspirant.ts
@@ -0,0 +1,18 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+import { GANGSTER_MECHANICAL_DESCRIPTION } from "./gangster";
+
+const FamilyHeadAspirant: Antagonist = {
+ key: "familyheadaspirant",
+ name: "Family Head Aspirant",
+ description: [
+ multiline`
+ A form of family leader that can activate at any point in the middle
+ of the shift.
+ `,
+ GANGSTER_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Midround,
+};
+
+export default FamilyHeadAspirant;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/gangster.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/gangster.ts
new file mode 100644
index 00000000000..38df184b26a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/gangster.ts
@@ -0,0 +1,19 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+export const GANGSTER_MECHANICAL_DESCRIPTION
+ = multiline`
+ Convince people to join your family, wear your uniform, tag turf
+ for the family, and accomplish your family's goals.
+ `;
+
+const Gangster: Antagonist = {
+ key: "gangster",
+ name: "Family Leader",
+ description: [
+ GANGSTER_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Roundstart,
+};
+
+export default Gangster;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/headrevolutionary.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/headrevolutionary.ts
new file mode 100644
index 00000000000..ea670d854df
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/headrevolutionary.ts
@@ -0,0 +1,20 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+export const REVOLUTIONARY_MECHANICAL_DESCRIPTION
+ = multiline`
+ Armed with a flash, convert as many people to the revolution as you can.
+ Kill or exile all heads of staff on the station.
+ `;
+
+const HeadRevolutionary: Antagonist = {
+ key: "headrevolutionary",
+ name: "Head Revolutionary",
+ description: [
+ "VIVA LA REVOLUTION!",
+ REVOLUTIONARY_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Roundstart,
+};
+
+export default HeadRevolutionary;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/heretic.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/heretic.ts
new file mode 100644
index 00000000000..611ad714eda
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/heretic.ts
@@ -0,0 +1,24 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+export const HERETIC_MECHANICAL_DESCRIPTION
+ = multiline`
+ Find hidden influences and sacrifice crew members to gain magical
+ powers and ascend as one of several paths.
+ `;
+
+const Heretic: Antagonist = {
+ key: "heretic",
+ name: "Heretic",
+ description: [
+ multiline`
+ Forgotten, devoured, gutted. Humanity has forgotten the eldritch forces
+ of decay, but the mansus veil has weakened. We will make them taste fear
+ again...
+ `,
+ HERETIC_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Roundstart,
+};
+
+export default Heretic;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/hereticsmuggler.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/hereticsmuggler.ts
new file mode 100644
index 00000000000..8667e0b1999
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/hereticsmuggler.ts
@@ -0,0 +1,14 @@
+import { Antagonist, Category } from "../base";
+import { HERETIC_MECHANICAL_DESCRIPTION } from "./heretic";
+
+const HereticSmuggler: Antagonist = {
+ key: "hereticsmuggler",
+ name: "Heretic Smuggler",
+ description: [
+ "A form of heretic that can activate when joining an ongoing shift.",
+ HERETIC_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Latejoin,
+};
+
+export default HereticSmuggler;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/loneoperative.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/loneoperative.ts
new file mode 100644
index 00000000000..b0de7f13b3e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/loneoperative.ts
@@ -0,0 +1,19 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+import { OPERATIVE_MECHANICAL_DESCRIPTION } from "./operative";
+
+const LoneOperative: Antagonist = {
+ key: "loneoperative",
+ name: "Lone Operative",
+ description: [
+ multiline`
+ A solo nuclear operative that has a higher chance of spawning the longer
+ the nuclear authentication disk stays in one place.
+ `,
+
+ OPERATIVE_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Midround,
+};
+
+export default LoneOperative;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfai.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfai.ts
new file mode 100644
index 00000000000..ed6b5d7f923
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfai.ts
@@ -0,0 +1,20 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+export const MALF_AI_MECHANICAL_DESCRIPTION
+ = multiline`
+ With a law zero to complete your objectives at all costs, combine your
+ omnipotence and malfunction modules to wreak havoc across the station.
+ Go delta to destroy the station and all those who opposed you.
+ `;
+
+const MalfAI: Antagonist = {
+ key: "malfai",
+ name: "Malfunctioning AI",
+ description: [
+ MALF_AI_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Roundstart,
+};
+
+export default MalfAI;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfaimidround.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfaimidround.ts
new file mode 100644
index 00000000000..1334e48379b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfaimidround.ts
@@ -0,0 +1,18 @@
+import { multiline } from "common/string";
+import { Antagonist, Category } from "../base";
+import { MALF_AI_MECHANICAL_DESCRIPTION } from "./malfai";
+
+const MalfAIMidround: Antagonist = {
+ key: "malfaimidround",
+ name: "Value Drifted AI",
+ description: [
+ multiline`
+ A form of malfunctioning AI that is given to existing AIs in the middle
+ of the shift.
+ `,
+ MALF_AI_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Midround,
+};
+
+export default MalfAIMidround;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/monkey.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/monkey.ts
new file mode 100644
index 00000000000..224c1d45516
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/monkey.ts
@@ -0,0 +1,17 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const Monkey: Antagonist = {
+ key: "monkey",
+ name: "Monkey",
+ description: [
+ multiline`
+ When the round starts, become infected with Jungle Fever, a disease which
+ turns its victim into a monkey. These monkeys can attack humans and give
+ them the deadly virus. Turn the entire crew into monkeys!
+ `,
+ ],
+ category: Category.Roundstart,
+};
+
+export default Monkey;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/obsessed.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/obsessed.ts
new file mode 100644
index 00000000000..4caf77f29d6
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/obsessed.ts
@@ -0,0 +1,17 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const Obsessed: Antagonist = {
+ key: "obsessed",
+ name: "Obsessed",
+ description: [
+ multiline`
+ You're obsessed with someone! Your obsession may begin to notice their
+ personal items are stolen and their coworkers have gone missing,
+ but will they realize they are your next victim in time?
+ `,
+ ],
+ category: Category.Midround,
+};
+
+export default Obsessed;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operative.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operative.ts
new file mode 100644
index 00000000000..582c74f003f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operative.ts
@@ -0,0 +1,25 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+export const OPERATIVE_MECHANICAL_DESCRIPTION = multiline`
+ Retrieve the nuclear authentication disk, use it to activate the nuclear
+ fission explosive, and destroy the station.
+`;
+
+const Operative: Antagonist = {
+ key: "operative",
+ name: "Nuclear Operative",
+ description: [
+ multiline`
+ Congratulations, agent. You have been chosen to join the Syndicate
+ Nuclear Operative strike team. Your mission, whether or not you choose
+ to accept it, is to destroy Nanotrasen's most advanced research facility!
+ That's right, you're going to Space Station 13.
+ `,
+
+ OPERATIVE_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Roundstart,
+};
+
+export default Operative;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operativemidround.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operativemidround.ts
new file mode 100644
index 00000000000..ae23adc8151
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operativemidround.ts
@@ -0,0 +1,18 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+import { OPERATIVE_MECHANICAL_DESCRIPTION } from "./operative";
+
+const OperativeMidround: Antagonist = {
+ key: "operativemidround",
+ name: "Nuclear Assailant",
+ description: [
+ multiline`
+ A form of nuclear operative that is offered to ghosts in the middle
+ of the shift.
+ `,
+ OPERATIVE_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Midround,
+};
+
+export default OperativeMidround;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/provocateur.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/provocateur.ts
new file mode 100644
index 00000000000..a55ab96435d
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/provocateur.ts
@@ -0,0 +1,19 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+import { REVOLUTIONARY_MECHANICAL_DESCRIPTION } from "./headrevolutionary";
+
+const Provocateur: Antagonist = {
+ key: "provocateur",
+ name: "Provocateur",
+ description: [
+ multiline`
+ A form of head revolutionary that can activate when joining an ongoing
+ shift.
+ `,
+
+ REVOLUTIONARY_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Latejoin,
+};
+
+export default Provocateur;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/revenant.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/revenant.ts
new file mode 100644
index 00000000000..f351dac602a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/revenant.ts
@@ -0,0 +1,17 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const Revenant: Antagonist = {
+ key: "revenant",
+ name: "Revenant",
+ description: [
+ multiline`
+ Become the mysterious revenant. Break windows, overload lights, and eat
+ the crew's life force, all while talking to your old community of
+ disgruntled ghosts.
+ `,
+ ],
+ category: Category.Midround,
+};
+
+export default Revenant;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/sentientdisease.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/sentientdisease.ts
new file mode 100644
index 00000000000..4691fda264e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/sentientdisease.ts
@@ -0,0 +1,16 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const SentientDisease: Antagonist = {
+ key: "sentientdisease",
+ name: "Sentient Disease",
+ description: [
+ multiline`
+ Mutate and spread yourself and infect as much of the crew as possible
+ with a deadly plague of your own creation.
+ `,
+ ],
+ category: Category.Midround,
+};
+
+export default SentientDisease;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spacedragon.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spacedragon.ts
new file mode 100644
index 00000000000..f4bebfcf50e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spacedragon.ts
@@ -0,0 +1,16 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const SpaceDragon: Antagonist = {
+ key: "spacedragon",
+ name: "Space Dragon",
+ description: [
+ multiline`
+ Become a ferocious space dragon. Breathe fire, summon an army of space
+ carps, crush walls, and terrorize the station.
+ `,
+ ],
+ category: Category.Midround,
+};
+
+export default SpaceDragon;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spaceninja.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spaceninja.ts
new file mode 100644
index 00000000000..971fe816061
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spaceninja.ts
@@ -0,0 +1,24 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const SpaceNinja: Antagonist = {
+ key: "spaceninja",
+ name: "Space Ninja",
+ description: [
+ multiline`
+ The Spider Clan practice a sort of augmentation of human flesh in order to
+ achieve a more perfect state of being and follow Postmodern Space Bushido.
+ `,
+
+ multiline`
+ Become a conniving space ninja, equipped with a katana, gloves to hack
+ into airlocks and APCs, a suit to make you go near-invisible,
+ as well as a variety of abilities in your kit. Hack into arrest consoles
+ to mark everyone as arrest, and even hack into communication consoles to
+ summon more threats to cause chaos on the station!
+ `,
+ ],
+ category: Category.Midround,
+};
+
+export default SpaceNinja;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/swarmer.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/swarmer.ts
new file mode 100644
index 00000000000..9b6696e261b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/swarmer.ts
@@ -0,0 +1,22 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const Swarmer: Antagonist = {
+ key: "swarmer",
+ name: "Swarmer",
+ description: [
+ multiline`
+ A swarmer is a small robot that replicates itself autonomously with
+ nearby given materials and prepare structures that they come
+ across for the following invasion force.
+ `,
+
+ multiline`
+ Consume machines, structures, walls, anything to get materials. Replicate
+ as many swarmers as you can to repeat the process.
+ `,
+ ],
+ category: Category.Midround,
+};
+
+export default Swarmer;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicateinfiltrator.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicateinfiltrator.ts
new file mode 100644
index 00000000000..2bcafe666f8
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicateinfiltrator.ts
@@ -0,0 +1,15 @@
+import { Antagonist, Category } from "../base";
+import { TRAITOR_MECHANICAL_DESCRIPTION } from "./traitor";
+
+const SyndicateInfiltrator: Antagonist = {
+ key: "syndicateinfiltrator",
+ name: "Syndicate Infiltrator",
+ description: [
+ "A form of traitor that can activate when joining an ongoing shift.",
+ TRAITOR_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Latejoin,
+ priority: -1,
+};
+
+export default SyndicateInfiltrator;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicatesleeperagent.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicatesleeperagent.ts
new file mode 100644
index 00000000000..bc3fb9d44f1
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicatesleeperagent.ts
@@ -0,0 +1,19 @@
+import { Antagonist, Category } from "../base";
+import { TRAITOR_MECHANICAL_DESCRIPTION } from "./traitor";
+import { multiline } from "common/string";
+
+const SyndicateSleeperAgent: Antagonist = {
+ key: "syndicatesleeperagent",
+ name: "Syndicate Sleeper Agent",
+ description: [
+ multiline`
+ A form of traitor that can activate at any point in the middle
+ of the shift.
+ `,
+ TRAITOR_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Midround,
+ priority: -1,
+};
+
+export default SyndicateSleeperAgent;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/traitor.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/traitor.ts
new file mode 100644
index 00000000000..9f11609f29f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/traitor.ts
@@ -0,0 +1,25 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+export const TRAITOR_MECHANICAL_DESCRIPTION
+ = multiline`
+ Start with a set of sinister objectives and an uplink to purchase
+ items to get the job done.
+ `;
+
+const Traitor: Antagonist = {
+ key: "traitor",
+ name: "Traitor",
+ description: [
+ multiline`
+ An unpaid debt. A score to be settled. Maybe you were just in the wrong
+ place at the wrong time. Whatever the reasons, you were selected to
+ infiltrate Space Station 13.
+ `,
+ TRAITOR_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Roundstart,
+ priority: -1,
+};
+
+export default Traitor;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizard.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizard.ts
new file mode 100644
index 00000000000..0455235cb58
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizard.ts
@@ -0,0 +1,20 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+export const WIZARD_MECHANICAL_DESCRIPTION
+ = multiline`
+ Choose between a variety of powerful spells in order to cause chaos
+ among Space Station 13.
+ `;
+
+const Wizard: Antagonist = {
+ key: "wizard",
+ name: "Wizard",
+ description: [
+ `"GREETINGS. WE'RE THE WIZARDS OF THE WIZARD'S FEDERATION."`,
+ WIZARD_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Roundstart,
+};
+
+export default Wizard;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizardmidround.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizardmidround.ts
new file mode 100644
index 00000000000..3a1f88e8f7a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizardmidround.ts
@@ -0,0 +1,14 @@
+import { Antagonist, Category } from "../base";
+import { WIZARD_MECHANICAL_DESCRIPTION } from "./wizard";
+
+const WizardMidround: Antagonist = {
+ key: "wizardmidround",
+ name: "Wizard (Midround)",
+ description: [
+ "A form of wizard that is offered to ghosts in the middle of the shift.",
+ WIZARD_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Midround,
+};
+
+export default WizardMidround;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/xenomorph.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/xenomorph.ts
new file mode 100644
index 00000000000..82e75ce7d8e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/xenomorph.ts
@@ -0,0 +1,16 @@
+import { Antagonist, Category } from "../base";
+import { multiline } from "common/string";
+
+const Xenomorph: Antagonist = {
+ key: "xenomorph",
+ name: "Xenomorph",
+ description: [
+ multiline`
+ Become the extraterrestrial xenomorph. Start as a larva, and progress
+ your way up the caste, including even the Queen!
+ `,
+ ],
+ category: Category.Midround,
+};
+
+export default Xenomorph;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/base.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/base.ts
new file mode 100644
index 00000000000..a91b6af8781
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/base.ts
@@ -0,0 +1,35 @@
+/**
+ * This folder represents the antagonists you can choose in the preferences
+ * menu.
+ *
+ * Every file in this folder represents one antagonist.
+ *
+ * For example "Syndicate Sleeper Agent" -> syndicatesleeperagent.ts
+ *
+ * "Antagonist" in this context actually means ruleset.
+ * This is an important distinction--it means that players can choose to be
+ * a roundstart traitor, but not a latejoin traitor.
+ *
+ * Icons are generated from the antag datums themselves, provided by the
+ * `antag_datum` variable on the /datum/dynamic_ruleset.
+ *
+ * The icon used is whatever the return value of get_preview_icon() is.
+ * Most antagonists, unless they want an especially cool effect, can simply
+ * set preview_outfit to some typepath representing their character.
+ */
+
+export type Antagonist = {
+ // the antag_flag, made lowercase, and with non-alphanumerics removed.
+ key: string;
+
+ name: string;
+ description: string[];
+ category: Category;
+ priority?: number;
+};
+
+export enum Category {
+ Roundstart,
+ Midround,
+ Latejoin,
+}
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts
new file mode 100644
index 00000000000..9fba753dabe
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts
@@ -0,0 +1,158 @@
+import { BooleanLike } from "common/react";
+import { sendAct } from "../../backend";
+import { Gender } from "./preferences/gender";
+
+export enum Food {
+ Alcohol = "ALCOHOL",
+ Breakfast = "BREAKFAST",
+ Cloth = "CLOTH",
+ Dairy = "DAIRY",
+ Fried = "FRIED",
+ Fruit = "FRUIT",
+ Grain = "GRAIN",
+ Gross = "GROSS",
+ Junkfood = "JUNKFOOD",
+ Meat = "MEAT",
+ Pineapple = "PINEAPPLE",
+ Raw = "RAW",
+ Sugar = "SUGAR",
+ Toxic = "TOXIC",
+ Vegetables = "VEGETABLES",
+}
+
+export enum JobPriority {
+ Low = 1,
+ Medium = 2,
+ High = 3,
+}
+
+export type Name = {
+ can_randomize: BooleanLike;
+ explanation: string;
+ group: string;
+};
+
+export type ServerSpeciesData = {
+ name: string;
+ icon: string;
+
+ use_skintones: BooleanLike;
+ sexes: BooleanLike;
+
+ enabled_features: string[];
+
+ liked_food: Food[];
+ disliked_food: Food[];
+ toxic_food: Food[];
+};
+
+export type Quirk = {
+ description: string;
+ icon: string;
+ name: string;
+ value: number;
+};
+
+export type QuirkInfo = {
+ max_positive_quirks: number;
+ quirk_info: Record;
+ quirk_blacklist: string[][];
+};
+
+export enum RandomSetting {
+ AntagOnly = 1,
+ Disabled = 2,
+ Enabled = 3,
+}
+
+export enum JoblessRole {
+ BeOverflow = 1,
+ BeRandomJob = 2,
+ ReturnToLobby = 3,
+}
+
+export enum GamePreferencesSelectedPage {
+ Settings,
+ Keybindings,
+}
+
+export const createSetPreference = (
+ act: typeof sendAct,
+ preference: string
+) => (value: unknown) => {
+ act("set_preference", {
+ preference,
+ value,
+ });
+};
+
+export enum Window {
+ Character = 0,
+ Game = 1,
+ Keybindings = 2,
+}
+
+export type PreferencesMenuData = {
+ character_preview_view: string;
+ character_profiles: (string | null)[];
+
+ preview_options: string;
+ preview_selection: string;
+
+ character_preferences: {
+ clothing: Record;
+ features: Record;
+ game_preferences: Record;
+ non_contextual: {
+ random_body: RandomSetting,
+ [otherKey: string]: unknown;
+ };
+ secondary_features: Record;
+ supplemental_features: Record;
+
+ names: Record;
+
+ misc: {
+ gender: Gender;
+ joblessrole: JoblessRole;
+ species: string;
+ };
+
+ randomization: Record;
+ };
+
+ content_unlocked: BooleanLike,
+
+ job_bans?: string[];
+ job_days_left?: Record;
+ job_required_experience?: Record;
+ job_preferences: Record;
+
+ keybindings: Record;
+ overflow_role: string;
+ selected_quirks: string[];
+
+ antag_bans?: string[];
+ antag_days_left?: Record;
+ selected_antags: string[];
+
+ active_slot: number;
+ name_to_use: string;
+
+ window: Window;
+};
+
+export type ServerData = {
+ names: {
+ types: Record;
+ };
+ quirks: QuirkInfo,
+ random: {
+ randomizable: string[];
+ };
+ species: Record;
+ [otheyKey: string]: unknown;
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/index.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/index.tsx
new file mode 100644
index 00000000000..f7b3f87c8a0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/index.tsx
@@ -0,0 +1,24 @@
+import { exhaustiveCheck } from "common/exhaustive";
+import { useBackend } from "../../backend";
+import { GamePreferencesSelectedPage, PreferencesMenuData, Window } from "./data";
+import { CharacterPreferenceWindow } from "./CharacterPreferenceWindow";
+import { GamePreferenceWindow } from "./GamePreferenceWindow";
+
+export const PreferencesMenu = (props, context) => {
+ const { data } = useBackend(context);
+
+ const window = data.window;
+
+ switch (window) {
+ case Window.Character:
+ return ;
+ case Window.Game:
+ return ;
+ case Window.Keybindings:
+ return ( );
+ default:
+ exhaustiveCheck(window);
+ }
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/base.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/base.ts
new file mode 100644
index 00000000000..ced244afee6
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/base.ts
@@ -0,0 +1,7 @@
+import { Department } from "./departments";
+
+export type Job = {
+ name: string;
+ description: string;
+ department: Department;
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/departments.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/departments.ts
new file mode 100644
index 00000000000..bbd7aa9325f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/departments.ts
@@ -0,0 +1,40 @@
+export type Department = {
+ head?: string;
+};
+
+export const Assistant: Department = {
+ // "Assistant" is not the head of its own department, as otherwise
+ // it would show as large and bold.
+};
+
+export const Captain: Department = {
+ head: "Captain",
+};
+
+export const Cargo: Department = {
+ head: "Quartermaster",
+};
+
+export const Engineering: Department = {
+ head: "Chief Engineer",
+};
+
+export const Medical: Department = {
+ head: "Chief Medical Officer",
+};
+
+export const Security: Department = {
+ head: "Head of Security",
+};
+
+export const Service: Department = {
+ head: "Head of Personnel",
+};
+
+export const Science: Department = {
+ head: "Research Director",
+};
+
+export const Silicon: Department = {
+ head: "AI",
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/ai.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/ai.ts
new file mode 100644
index 00000000000..eaf932b0b6d
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/ai.ts
@@ -0,0 +1,10 @@
+import { Job } from "../base";
+import { Silicon } from "../departments";
+
+const AI: Job = {
+ name: "AI",
+ description: "Assist the crew, follow your laws, coordinate your cyborgs.",
+ department: Silicon,
+};
+
+export default AI;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/assistant.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/assistant.ts
new file mode 100644
index 00000000000..66d034f1c3a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/assistant.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Assistant as DepartmentAssistant } from "../departments";
+
+const Assistant: Job = {
+ name: "Assistant",
+ description: "Get your space legs, assist people, ask the HoP to \
+ give you a job.",
+ department: DepartmentAssistant,
+};
+
+export default Assistant;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/atmospheric_technician.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/atmospheric_technician.ts
new file mode 100644
index 00000000000..9b9f17199e2
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/atmospheric_technician.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Engineering } from "../departments";
+
+const AtmosphericTechnician: Job = {
+ name: "Atmospheric Technician",
+ description: "Ensure the air is breathable on the station, fill oxygen \
+ tanks, fight fires, purify the air.",
+ department: Engineering,
+};
+
+export default AtmosphericTechnician;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/bartender.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/bartender.ts
new file mode 100644
index 00000000000..0d5670f89f9
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/bartender.ts
@@ -0,0 +1,10 @@
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const Bartender: Job = {
+ name: "Bartender",
+ description: "Serve booze, mix drinks, keep the crew drunk.",
+ department: Service,
+};
+
+export default Bartender;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/botanist.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/botanist.ts
new file mode 100644
index 00000000000..5f00a5faefa
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/botanist.ts
@@ -0,0 +1,10 @@
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const Botanist: Job = {
+ name: "Botanist",
+ description: "Grow plants for the cook, for medicine, and for recreation.",
+ department: Service,
+};
+
+export default Botanist;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/captain.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/captain.ts
new file mode 100644
index 00000000000..893b7e4907c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/captain.ts
@@ -0,0 +1,12 @@
+import { Job } from "../base";
+import { Captain as DepartmentCaptain } from "../departments";
+
+const Captain: Job = {
+ name: "Captain",
+ description: "Be responsible for the station, manage your Heads of Staff, \
+ keep the crew alive, be prepared to do anything and everything or die \
+ horribly trying.",
+ department: DepartmentCaptain,
+};
+
+export default Captain;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/cargo_technician.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/cargo_technician.ts
new file mode 100644
index 00000000000..8666c6e1fe7
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/cargo_technician.ts
@@ -0,0 +1,12 @@
+import { Job } from "../base";
+import { Cargo } from "../departments";
+
+const CargoTechnician: Job = {
+ name: "Cargo Technician",
+ description: "Distribute supplies to the departments that ordered them, \
+ collect empty crates, load and unload the supply shuttle, \
+ ship bounty cubes.",
+ department: Cargo,
+};
+
+export default CargoTechnician;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chaplain.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chaplain.ts
new file mode 100644
index 00000000000..da105113c0c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chaplain.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const Chaplain: Job = {
+ name: "Chaplain",
+ description: "Hold services and funerals, cremate people, preach your \
+ religion, protect the crew against cults.",
+ department: Service,
+};
+
+export default Chaplain;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chemist.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chemist.ts
new file mode 100644
index 00000000000..f53bed85525
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chemist.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Medical } from "../departments";
+
+const Chemist: Job = {
+ name: "Chemist",
+ description: "Supply the doctors with chemicals, make medicine, as well as \
+ less likable substances in the comfort of a fully reinforced room.",
+ department: Medical,
+};
+
+export default Chemist;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chief_engineer.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chief_engineer.ts
new file mode 100644
index 00000000000..177c31bd63c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chief_engineer.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Engineering } from "../departments";
+
+const ChiefEngineer: Job = {
+ name: "Chief Engineer",
+ description: "Coordinate engineering, ensure equipment doesn't get stolen, \
+ make sure the Supermatter doesn't blow up, maintain telecommunications.",
+ department: Engineering,
+};
+
+export default ChiefEngineer;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chief_medical_officer.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chief_medical_officer.ts
new file mode 100644
index 00000000000..15a152ed6ac
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/chief_medical_officer.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Medical } from "../departments";
+
+const ChiefMedicalOfficer: Job = {
+ name: "Chief Medical Officer",
+ description: "Coordinate doctors and other medbay employees, ensure they \
+ know how to save lives, check for injuries on the crew monitor.",
+ department: Medical,
+};
+
+export default ChiefMedicalOfficer;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/clown.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/clown.ts
new file mode 100644
index 00000000000..253a6aa3c1b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/clown.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const Clown: Job = {
+ name: "Clown",
+ description: "Entertain the crew, make bad jokes, go on a holy quest to find \
+ bananium, HONK!",
+ department: Service,
+};
+
+export default Clown;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/cook.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/cook.ts
new file mode 100644
index 00000000000..d072cb193e4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/cook.ts
@@ -0,0 +1,10 @@
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const Cook: Job = {
+ name: "Cook",
+ description: "Serve food, cook meat, keep the crew fed.",
+ department: Service,
+};
+
+export default Cook;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/curator.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/curator.ts
new file mode 100644
index 00000000000..454c93fb426
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/curator.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const Curator: Job = {
+ name: "Curator",
+ description: "Read and write books and hand them to people, stock \
+ bookshelves, report on station news.",
+ department: Service,
+};
+
+export default Curator;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/cyborg.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/cyborg.ts
new file mode 100644
index 00000000000..4969aaa3199
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/cyborg.ts
@@ -0,0 +1,10 @@
+import { Job } from "../base";
+import { Silicon } from "../departments";
+
+const Cyborg: Job = {
+ name: "Cyborg",
+ description: "Assist the crew, follow your laws, obey your AI.",
+ department: Silicon,
+};
+
+export default Cyborg;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/detective.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/detective.ts
new file mode 100644
index 00000000000..408dcb0978f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/detective.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Security } from "../departments";
+
+const Detective: Job = {
+ name: "Detective",
+ description: "Investigate crimes, gather evidence, perform interrogations, \
+ look badass, smoke cigarettes.",
+ department: Security,
+};
+
+export default Detective;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/geneticist.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/geneticist.ts
new file mode 100644
index 00000000000..e36e08c3116
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/geneticist.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Science } from "../departments";
+
+const Geneticist: Job = {
+ name: "Geneticist",
+ description: "Alter genomes, turn monkeys into humans (and vice-versa), make \
+ DNA backups so they can get stolen.",
+ department: Science,
+};
+
+export default Geneticist;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/head_of_personnel.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/head_of_personnel.ts
new file mode 100644
index 00000000000..9f06282d346
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/head_of_personnel.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const HeadOfPersonnel: Job = {
+ name: "Head of Personnel",
+ description: "Alter access on ID cards, manage civil and supply departments, \
+ protect Ian, run the station when the captain dies.",
+ department: Service,
+};
+
+export default HeadOfPersonnel;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/head_of_security.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/head_of_security.ts
new file mode 100644
index 00000000000..56a96e80470
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/head_of_security.ts
@@ -0,0 +1,12 @@
+import { Job } from "../base";
+import { Security } from "../departments";
+
+const HeadOfSecurity: Job = {
+ name: "Head of Security",
+ description: "Coordinate security personnel, ensure they are not corrupt, \
+ make sure every department is protected, save the Virologist when he gets \
+ mauled by monkeys.",
+ department: Security,
+};
+
+export default HeadOfSecurity;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/janitor.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/janitor.ts
new file mode 100644
index 00000000000..aa64e0687c5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/janitor.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const Janitor: Job = {
+ name: "Janitor",
+ description: "Clean up trash and blood. Replace broken lights. \
+ Slip people over.",
+ department: Service,
+};
+
+export default Janitor;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/lawyer.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/lawyer.ts
new file mode 100644
index 00000000000..559b82b37e0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/lawyer.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Security } from "../departments";
+
+const Lawyer: Job = {
+ name: "Lawyer",
+ description: "Advocate for prisoners, create law-binding contracts, \
+ ensure Security is following protocol and Space Law.",
+ department: Security,
+};
+
+export default Lawyer;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/medical_doctor.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/medical_doctor.ts
new file mode 100644
index 00000000000..b248624b33a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/medical_doctor.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Medical } from "../departments";
+
+const MedicalDoctor: Job = {
+ name: "Medical Doctor",
+ description: "Save lives, run around the station looking for victims, \
+ scan everyone in sight",
+ department: Medical,
+};
+
+export default MedicalDoctor;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/mime.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/mime.ts
new file mode 100644
index 00000000000..8eca6aa19a3
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/mime.ts
@@ -0,0 +1,10 @@
+import { Job } from "../base";
+import { Service } from "../departments";
+
+const Mime: Job = {
+ name: "Mime",
+ description: "...",
+ department: Service,
+};
+
+export default Mime;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/paramedic.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/paramedic.ts
new file mode 100644
index 00000000000..88a4450232a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/paramedic.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Medical } from "../departments";
+
+const Paramedic: Job = {
+ name: "Paramedic",
+ description: "Run around the station looking for patients, respond to \
+ emergencies, give patients a roller bed ride to medbay.",
+ department: Medical,
+};
+
+export default Paramedic;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/prisoner.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/prisoner.ts
new file mode 100644
index 00000000000..82308a9b1b9
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/prisoner.ts
@@ -0,0 +1,10 @@
+import { Job } from "../base";
+import { Security } from "../departments";
+
+const Prisoner: Job = {
+ name: "Prisoner",
+ description: "Keep yourself occupied while in permabrig.",
+ department: Security,
+};
+
+export default Prisoner;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/psychologist.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/psychologist.ts
new file mode 100644
index 00000000000..f6233d82b09
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/psychologist.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Medical } from "../departments";
+
+const Psychologist: Job = {
+ name: "Psychologist",
+ description: "Advocate sanity, self-esteem, and teamwork in a station \
+ staffed with headcases.",
+ department: Medical,
+};
+
+export default Psychologist;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/quartermaster.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/quartermaster.ts
new file mode 100644
index 00000000000..05f266fba6f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/quartermaster.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Cargo } from "../departments";
+
+const Quartermaster: Job = {
+ name: "Quartermaster",
+ description: "Coordinate cargo technicians and shaft miners, assist with \
+ economical purchasing.",
+ department: Cargo,
+};
+
+export default Quartermaster;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/research_director.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/research_director.ts
new file mode 100644
index 00000000000..6ecce763ed8
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/research_director.ts
@@ -0,0 +1,12 @@
+import { Job } from "../base";
+import { Science } from "../departments";
+
+const ResearchDirector: Job = {
+ name: "Research Director",
+ description: "Supervise research efforts, ensure Robotics is in working \
+ order, make sure the AI and its Cyborgs aren't rogue, replacing them if \
+ they are",
+ department: Science,
+};
+
+export default ResearchDirector;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/roboticist.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/roboticist.ts
new file mode 100644
index 00000000000..64cf661eae9
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/roboticist.ts
@@ -0,0 +1,10 @@
+import { Job } from "../base";
+import { Science } from "../departments";
+
+const Roboticist: Job = {
+ name: "Roboticist",
+ description: "Build and repair the AI and cyborgs, create mechs.",
+ department: Science,
+};
+
+export default Roboticist;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/scientist.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/scientist.ts
new file mode 100644
index 00000000000..9f140ac954f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/scientist.ts
@@ -0,0 +1,10 @@
+import { Job } from "../base";
+import { Science } from "../departments";
+
+const Scientist: Job = {
+ name: "Scientist",
+ description: "Do experiments, perform research, feed the slimes, make bombs.",
+ department: Science,
+};
+
+export default Scientist;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/security_officer.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/security_officer.ts
new file mode 100644
index 00000000000..93117dd3b87
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/security_officer.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Security } from "../departments";
+
+const SecurityOfficer: Job = {
+ name: "Security Officer",
+ description: "Protect company assets, follow the Standard Operating \
+ Procedure, eat donuts",
+ department: Security,
+};
+
+export default SecurityOfficer;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/shaft_miner.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/shaft_miner.ts
new file mode 100644
index 00000000000..38866707262
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/shaft_miner.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Cargo } from "../departments";
+
+const ShaftMiner: Job = {
+ name: "Shaft Miner",
+ description: "Mine ores and process them into useful materials. \
+ Kill hostile wildlife. Go on an adventure in mysterious ruins.",
+ department: Cargo,
+};
+
+export default ShaftMiner;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/station_engineer.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/station_engineer.ts
new file mode 100644
index 00000000000..4280c833735
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/station_engineer.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Engineering } from "../departments";
+
+const StationEngineer: Job = {
+ name: "Station Engineer",
+ description: "Start the Supermatter, wire the solars, repair station hull \
+ and wiring damage.",
+ department: Engineering,
+};
+
+export default StationEngineer;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/virologist.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/virologist.ts
new file mode 100644
index 00000000000..13a5be66650
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/virologist.ts
@@ -0,0 +1,11 @@
+import { Job } from "../base";
+import { Medical } from "../departments";
+
+const Virologist: Job = {
+ name: "Virologist",
+ description: "Study the effects of various diseases and synthesize a \
+ vaccine for them. Engineer beneficial viruses.",
+ department: Medical,
+};
+
+export default Virologist;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/warden.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/warden.ts
new file mode 100644
index 00000000000..e8ac63fe466
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/jobs/jobs/warden.ts
@@ -0,0 +1,12 @@
+import { Job } from "../base";
+import { Security } from "../departments";
+
+const Warden: Job = {
+ name: "Warden",
+ description: "Watch over the Brig and Prison Wing, release prisoners when \
+ their time is up, issue equipment to security, be a security officer when \
+ they all eventually die.",
+ department: Security,
+};
+
+export default Warden;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/names.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/names.tsx
new file mode 100644
index 00000000000..8f798394491
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/names.tsx
@@ -0,0 +1,237 @@
+import { binaryInsertWith, sortBy } from "common/collections";
+import { useLocalState } from "../../backend";
+import { Box, Button, FitText, Icon, Input, LabeledList, Modal, Section, Stack, TrackOutsideClicks } from "../../components";
+import { Name } from "./data";
+import { ServerPreferencesFetcher } from "./ServerPreferencesFetcher";
+
+type NameWithKey = {
+ key: string,
+ name: Name,
+};
+
+const binaryInsertName = binaryInsertWith(({ key }) => key);
+
+const sortNameWithKeyEntries = sortBy<[string, NameWithKey[]]>(
+ ([key]) => key,
+);
+
+export const MultiNameInput = (props: {
+ handleClose: () => void,
+ handleRandomizeName: (nameType: string) => void,
+ handleUpdateName: (nameType: string, value: string) => void,
+ names: Record,
+}, context) => {
+ const [currentlyEditingName, setCurrentlyEditingName]
+ = useLocalState(context, "currentlyEditingName", null);
+
+ return (
+ {
+ if (!data) {
+ return null;
+ }
+
+ const namesIntoGroups: Record = {};
+
+ for (const [key, name] of Object.entries(data.names.types)) {
+ namesIntoGroups[name.group] = binaryInsertName(
+ namesIntoGroups[name.group] || [],
+ {
+ key,
+ name,
+ }
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+ }} />
+ );
+};
+
+export const NameInput = (props: {
+ handleUpdateName: (name: string) => void,
+ name: string,
+ openMultiNameInput: () => void,
+}, context) => {
+ const [lastNameBeforeEdit, setLastNameBeforeEdit]
+ = useLocalState(context, "lastNameBeforeEdit", null);
+ const editing = lastNameBeforeEdit === props.name;
+
+ const updateName = (e, value) => {
+ setLastNameBeforeEdit(null);
+ props.handleUpdateName(value);
+ };
+
+ return (
+ {
+ setLastNameBeforeEdit(props.name);
+ }} textAlign="center" width="100%" height="28px">
+
+
+
+
+
+
+ {editing && (
+ {
+ setLastNameBeforeEdit(null);
+ }}
+ value={props.name}
+ />
+ ) || (
+
+ {props.name}
+
+ )}
+
+
+
+
+ {/* We only know other names when the server tells us */}
+ data ? (
+
+ {
+ props.openMultiNameInput();
+
+ // We're a button inside a button.
+ // Did you know that's against the W3C standard? :)
+ event.cancelBubble = true;
+ event.stopPropagation();
+ }}
+ >
+
+
+
+ ) : null} />
+
+
+ );
+};
+
+
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx
new file mode 100644
index 00000000000..38861d75f2e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx
@@ -0,0 +1,395 @@
+import { sortBy, sortStrings } from "common/collections";
+import { BooleanLike, classes } from "common/react";
+import { ComponentType, createComponentVNode, InfernoNode } from "inferno";
+import { VNodeFlags } from "inferno-vnode-flags";
+import { sendAct, useBackend, useLocalState } from "../../../../backend";
+import { Box, Button, Dropdown, NumberInput, Stack } from "../../../../components";
+import { createSetPreference, PreferencesMenuData } from "../../data";
+import { ServerPreferencesFetcher } from "../../ServerPreferencesFetcher";
+
+export const sortChoices = sortBy<[string, InfernoNode]>(([name]) => name);
+
+export type Feature<
+ TReceiving,
+ TSending = TReceiving,
+ TServerData = undefined,
+> = {
+ name: string;
+ component: FeatureValue<
+ TReceiving,
+ TSending,
+ TServerData
+ >;
+ category?: string;
+ description?: string;
+};
+
+/**
+ * Represents a preference.
+ * TReceiving = The type you will be receiving
+ * TSending = The type you will be sending
+ * TServerData = The data the server sends through preferences.json
+ */
+type FeatureValue<
+ TReceiving,
+ TSending = TReceiving,
+ TServerData = undefined,
+>
+ = ComponentType>;
+
+export type FeatureValueProps<
+ TReceiving,
+ TSending = TReceiving,
+ TServerData = undefined,
+> = {
+ act: typeof sendAct,
+ featureId: string,
+ handleSetValue: (newValue: TSending) => void,
+ serverData: TServerData | undefined,
+ shrink?: boolean,
+ value: TReceiving,
+};
+
+export const FeatureColorInput = (props: FeatureValueProps) => {
+ return (
+ {
+ props.act("set_color_preference", {
+ preference: props.featureId,
+ });
+ }}>
+
+
+
+
+
+ {!props.shrink && (
+
+ Change
+
+ )}
+
+
+ );
+};
+
+export type FeatureToggle = Feature;
+
+export const CheckboxInput = (
+ props: FeatureValueProps
+) => {
+ return ( {
+ props.handleSetValue(!props.value);
+ }}
+ />);
+};
+
+export const CheckboxInputInverse = (
+ props: FeatureValueProps
+) => {
+ return ( {
+ props.handleSetValue(!props.value);
+ }}
+ />);
+};
+
+export const createDropdownInput = (
+ // Map of value to display texts
+ choices: Record,
+ dropdownProps?: Record,
+): FeatureValue => {
+ return (props: FeatureValueProps) => {
+ return ( {
+ return {
+ displayText: label,
+ value: dataValue,
+ };
+ })}
+ {...dropdownProps}
+ />);
+ };
+};
+
+export type FeatureChoicedServerData = {
+ choices: string[];
+ display_names?: Record;
+ icons?: Record;
+};
+
+export type FeatureChoiced = Feature;
+
+const capitalizeFirstLetter = (text: string) => (
+ text.toString().charAt(0).toUpperCase() + text.toString().slice(1)
+);
+
+export const StandardizedDropdown = (props: {
+ choices: string[],
+ disabled?: boolean,
+ displayNames: Record,
+ onSetValue: (newValue: string) => void,
+ value: string,
+}) => {
+ const {
+ choices,
+ disabled,
+ displayNames,
+ onSetValue,
+ value,
+ } = props;
+
+ return ( {
+ return {
+ displayText: displayNames[choice],
+ value: choice,
+ };
+ })
+ }
+ />);
+};
+
+export const FeatureDropdownInput = (
+ props: FeatureValueProps & {
+ disabled?: boolean,
+ },
+) => {
+ const serverData = props.serverData;
+ if (!serverData) {
+ return null;
+ }
+
+ const displayNames = serverData.display_names
+ || Object.fromEntries(
+ serverData.choices.map(choice => [choice, capitalizeFirstLetter(choice)])
+ );
+
+ return ( );
+};
+
+export type FeatureWithIcons = Feature<
+ { value: T },
+ T,
+ FeatureChoicedServerData
+>;
+
+export const FeatureIconnedDropdownInput = (
+ props: FeatureValueProps<{
+ value: string,
+ }, string, FeatureChoicedServerData>,
+) => {
+ const serverData = props.serverData;
+ if (!serverData) {
+ return null;
+ }
+
+ const icons = serverData.icons;
+
+ const textNames = serverData.display_names
+ || Object.fromEntries(
+ serverData.choices.map(choice => [choice, capitalizeFirstLetter(choice)])
+ );
+
+ const displayNames = Object.fromEntries(
+ Object.entries(textNames).map(([choice, textName]) => {
+ let element: InfernoNode = textName;
+
+ if (icons && icons[choice]) {
+ const icon = icons[choice];
+ element = (
+
+
+
+
+
+
+ {element}
+
+
+ );
+ }
+
+ return [choice, element];
+ })
+ );
+
+ return ( );
+};
+
+
+export type FeatureNumericData = {
+ minimum: number,
+ maximum: number,
+ step: number,
+}
+
+export type FeatureNumeric = Feature;
+
+export const FeatureNumberInput = (
+ props: FeatureValueProps
+) => {
+ if (!props.serverData) {
+ return Loading... ;
+ }
+
+ return ( {
+ props.handleSetValue(value);
+ }}
+ minValue={props.serverData.minimum}
+ maxValue={props.serverData.maximum}
+ step={props.serverData.step}
+ value={props.value}
+ />);
+};
+
+export const FeatureValueInput = (props: {
+ feature: Feature,
+ featureId: string,
+ shrink?: boolean,
+ value: unknown,
+
+ act: typeof sendAct,
+}, context) => {
+ const { data } = useBackend(context);
+
+ const feature = props.feature;
+
+ const [predictedValue, setPredictedValue] = useLocalState(
+ context,
+ `${props.featureId}_predictedValue_${data.active_slot}`,
+ props.value,
+ );
+
+ const changeValue = (newValue: unknown) => {
+ setPredictedValue(newValue);
+ createSetPreference(props.act, props.featureId)(newValue);
+ };
+
+ return (
+ {
+ return createComponentVNode(
+ VNodeFlags.ComponentUnknown,
+ feature.component,
+ {
+ act: props.act,
+ featureId: props.featureId,
+ serverData: serverData && serverData[props.featureId],
+ shrink: props.shrink,
+
+ handleSetValue: changeValue,
+ value: predictedValue,
+ });
+ }}
+ />
+ );
+};
+
+// SKYRAT FEATURES DOWN HERE
+export const FeatureTriColorInput = (props: FeatureValueProps) => {
+ return (
+
+
+ props.act('set_tri_color_preference', {
+ color: 1,
+ })}>
+
+
+
+
+ props.act('set_tri_color_preference', {
+ picked_color: 2,
+ })}>
+
+
+
+
+ props.act('set_tri_color_preference', {
+ picked_color: 3,
+ })}>
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/age.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/age.tsx
new file mode 100644
index 00000000000..41889a1fc68
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/age.tsx
@@ -0,0 +1,6 @@
+import { Feature, FeatureNumberInput } from "../base";
+
+export const age: Feature = {
+ name: "Age",
+ component: FeatureNumberInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/ai_core_display.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/ai_core_display.tsx
new file mode 100644
index 00000000000..b67e62dbe8d
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/ai_core_display.tsx
@@ -0,0 +1,6 @@
+import { FeatureIconnedDropdownInput, FeatureWithIcons } from "../base";
+
+export const preferred_ai_core_display: FeatureWithIcons = {
+ name: "AI core display",
+ component: FeatureIconnedDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/body_type.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/body_type.tsx
new file mode 100644
index 00000000000..9ae25fa1486
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/body_type.tsx
@@ -0,0 +1,6 @@
+import { FeatureChoiced, FeatureDropdownInput } from "../base";
+
+export const body_type: FeatureChoiced = {
+ name: "Body type",
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/pda.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/pda.tsx
new file mode 100644
index 00000000000..823459a2a21
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/pda.tsx
@@ -0,0 +1,15 @@
+import { Feature, FeatureColorInput, FeatureDropdownInput } from "../base";
+
+export const pda_color: Feature = {
+ name: "PDA color",
+ category: "GAMEPLAY",
+ description: "The background color of your PDA.",
+ component: FeatureColorInput,
+};
+
+export const pda_style: Feature = {
+ name: "PDA style",
+ category: "GAMEPLAY",
+ description: "The style of your equipped PDA. Changes font.",
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/phobia.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/phobia.tsx
new file mode 100644
index 00000000000..04d91daea06
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/phobia.tsx
@@ -0,0 +1,6 @@
+import { FeatureChoiced, FeatureDropdownInput } from "../base";
+
+export const phobia: FeatureChoiced = {
+ name: "Phobia",
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/playtime_reward_cloak.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/playtime_reward_cloak.tsx
new file mode 100644
index 00000000000..af24a49b7f4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/playtime_reward_cloak.tsx
@@ -0,0 +1,7 @@
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const playtime_reward_cloak: FeatureToggle = {
+ name: "Don gamer cloak",
+ component: CheckboxInput,
+};
+
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/security_department.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/security_department.tsx
new file mode 100644
index 00000000000..118f2cf4678
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/security_department.tsx
@@ -0,0 +1,6 @@
+import { FeatureChoiced, FeatureDropdownInput } from "../base";
+
+export const prefered_security_department: FeatureChoiced = {
+ name: "Security department",
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/skin_tone.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/skin_tone.tsx
new file mode 100644
index 00000000000..40856fed36a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/skin_tone.tsx
@@ -0,0 +1,63 @@
+import { sortBy } from "common/collections";
+import { Box, Stack } from "../../../../../components";
+import { Feature, FeatureChoicedServerData, FeatureValueProps, StandardizedDropdown } from "../base";
+
+type HexValue = {
+ lightness: number,
+ value: string,
+};
+
+type SkinToneServerData = FeatureChoicedServerData & {
+ display_names: NonNullable,
+ to_hex: Record,
+};
+
+const sortHexValues
+ = sortBy<[string, HexValue]>(([_, hexValue]) => -hexValue.lightness);
+
+export const skin_tone: Feature = {
+ name: "Skin tone",
+ component: (props: FeatureValueProps) => {
+ const {
+ handleSetValue,
+ serverData,
+ value,
+ } = props;
+
+ if (!serverData) {
+ return null;
+ }
+
+ return (
+ key)}
+ displayNames={Object.fromEntries(
+ Object.entries(serverData.display_names)
+ .map(([key, displayName]) => {
+ const hexColor = serverData.to_hex[key];
+
+ return [key, (
+
+
+
+
+
+
+ {displayName}
+
+
+ )];
+ })
+ )}
+ onSetValue={handleSetValue}
+ value={value}
+ />
+ );
+ },
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/skyrat/species_features.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/skyrat/species_features.tsx
new file mode 100644
index 00000000000..e7e2a401b61
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/skyrat/species_features.tsx
@@ -0,0 +1,16 @@
+import { FeatureChoiced, FeatureDropdownInput, Feature, FeatureColorInput } from "../../base";
+
+export const feature_leg_type: FeatureChoiced = {
+ name: "Leg type",
+ component: FeatureDropdownInput,
+};
+
+export const feature_mcolor2: Feature = {
+ name: "Mutant color 2",
+ component: FeatureColorInput,
+};
+export const feature_mcolor3: Feature = {
+ name: "Mutant color 3",
+ component: FeatureColorInput,
+};
+
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/uplink_loc.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/uplink_loc.tsx
new file mode 100644
index 00000000000..45989259708
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/uplink_loc.tsx
@@ -0,0 +1,6 @@
+import { FeatureChoiced, FeatureDropdownInput } from "../base";
+
+export const uplink_loc: FeatureChoiced = {
+ name: "Uplink Spawn Location",
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx
new file mode 100644
index 00000000000..b59aa47fbae
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx
@@ -0,0 +1,15 @@
+import { FeatureColorInput, Feature, FeatureDropdownInput } from "../base";
+
+export const asaycolor: Feature = {
+ name: "Admin chat color",
+ category: "ADMIN",
+ description: "The color of your messages in Adminsay.",
+ component: FeatureColorInput,
+};
+
+export const brief_outfit: Feature = {
+ name: "Brief outfit",
+ category: "ADMIN",
+ description: "The outfit to gain when spawning as the briefing officer.",
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ambient_occlusion.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ambient_occlusion.tsx
new file mode 100644
index 00000000000..4884c07377f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ambient_occlusion.tsx
@@ -0,0 +1,8 @@
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const ambientocclusion: FeatureToggle = {
+ name: "Enable ambient occlusion",
+ category: "GAMEPLAY",
+ description: "Enable ambient occlusion, light shadows around characters.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/auto_fit_viewport.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/auto_fit_viewport.tsx
new file mode 100644
index 00000000000..5339de43d15
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/auto_fit_viewport.tsx
@@ -0,0 +1,7 @@
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const auto_fit_viewport: FeatureToggle = {
+ name: "Auto fit viewport",
+ category: "UI",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/broadcast_login_logout.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/broadcast_login_logout.tsx
new file mode 100644
index 00000000000..a997b265f64
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/broadcast_login_logout.tsx
@@ -0,0 +1,11 @@
+import { multiline } from "common/string";
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const broadcast_login_logout: FeatureToggle = {
+ name: "Broadcast login/logout",
+ category: "GAMEPLAY",
+ description: multiline`
+ When enabled, disconnecting and reconnecting will announce to deadchat.
+ `,
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/buttons_locked.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/buttons_locked.tsx
new file mode 100644
index 00000000000..67849342094
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/buttons_locked.tsx
@@ -0,0 +1,8 @@
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const buttons_locked: FeatureToggle = {
+ name: "Lock action buttons",
+ category: "GAMEPLAY",
+ description: "When enabled, action buttons will be locked in place.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/darkened_flash.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/darkened_flash.tsx
new file mode 100644
index 00000000000..706b1787705
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/darkened_flash.tsx
@@ -0,0 +1,12 @@
+import { multiline } from "common/string";
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const darkened_flash: FeatureToggle = {
+ name: "Enable darkened flashes",
+ category: "GAMEPLAY",
+ description: multiline`
+ When toggled, being flashed will show a dark screen rather than a
+ bright one.
+ `,
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/fps.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/fps.tsx
new file mode 100644
index 00000000000..55c091a9714
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/fps.tsx
@@ -0,0 +1,56 @@
+import { Dropdown, NumberInput, Stack } from "../../../../../components";
+import { Feature, FeatureNumericData, FeatureValueProps } from "../base";
+
+type FpsServerData = FeatureNumericData & {
+ recommended_fps: number;
+}
+
+const FpsInput = (
+ props: FeatureValueProps
+) => {
+ const { handleSetValue, serverData } = props;
+
+ let recommened = `Recommended`;
+ if (serverData) {
+ recommened += ` (${serverData.recommended_fps})`;
+ }
+
+ return (
+
+
+ {
+ if (value === recommened) {
+ handleSetValue(-1);
+ } else {
+ handleSetValue(serverData?.recommended_fps || 60);
+ }
+ }}
+ width="100%"
+ options={[
+ recommened,
+ "Custom",
+ ]}
+ />
+
+
+
+ {serverData && props.value !== -1 && ( {
+ props.handleSetValue(value);
+ }}
+ minValue={1}
+ maxValue={serverData.maximum}
+ value={props.value}
+ />)}
+
+
+ );
+};
+
+export const clientfps: Feature = {
+ name: "FPS",
+ category: "GAMEPLAY",
+ component: FpsInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx
new file mode 100644
index 00000000000..c9dc71ab1fa
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx
@@ -0,0 +1,139 @@
+import { multiline } from "common/string";
+import { CheckboxInput, FeatureChoiced, FeatureChoicedServerData, FeatureDropdownInput, FeatureToggle, FeatureValueProps } from "../base";
+import { Box, Dropdown, Flex } from "../../../../../components";
+import { classes } from "common/react";
+import { InfernoNode } from "inferno";
+import { binaryInsertWith } from "common/collections";
+import { useBackend } from "../../../../../backend";
+import { PreferencesMenuData } from "../../../data";
+
+export const ghost_accs: FeatureChoiced = {
+ name: "Ghost accessories",
+ category: "GHOST",
+ description: "Determines what adjustments your ghost will have.",
+ component: FeatureDropdownInput,
+};
+
+const insertGhostForm
+ = binaryInsertWith<{
+ displayText: InfernoNode,
+ value: string,
+ }>(({ value }) => value);
+
+const GhostFormInput = (
+ props: FeatureValueProps,
+ context,
+) => {
+ const { data } = useBackend(context);
+
+ const serverData = props.serverData;
+ if (!serverData) {
+ return;
+ }
+
+ const displayNames = serverData.display_names;
+ if (!displayNames) {
+ return (
+
+ No display names for ghost_form!
+
+ );
+ }
+
+ const displayTexts = {};
+ let options: {
+ displayText: InfernoNode,
+ value: string,
+ }[] = [];
+
+ for (const [name, displayName] of Object.entries(displayNames)) {
+ const displayText = (
+
+
+
+
+
+
+ {displayName}
+
+
+ );
+
+ displayTexts[name] = displayText;
+
+ const optionEntry = {
+ displayText,
+ value: name,
+ };
+
+ // Put the default ghost on top
+ if (name === "ghost") {
+ options.unshift(optionEntry);
+ } else {
+ options = insertGhostForm(options, optionEntry);
+ }
+ }
+
+ return ( );
+};
+
+export const ghost_form: FeatureChoiced = {
+ name: "Ghosts form",
+ category: "GHOST",
+ description: "The appearance of your ghost. Requires BYOND membership.",
+ component: GhostFormInput,
+};
+
+export const ghost_hud: FeatureToggle = {
+ name: "Ghost HUD",
+ category: "GHOST",
+ description: "Enable HUD buttons for ghosts.",
+ component: CheckboxInput,
+};
+
+export const ghost_orbit: FeatureChoiced = {
+ name: "Ghost orbit",
+ category: "GHOST",
+ description: multiline`
+ The shape in which your ghost will orbit.
+ Requires BYOND membership.
+ `,
+ component: (
+ props: FeatureValueProps,
+ context,
+ ) => {
+ const { data } = useBackend(context);
+
+ return ( );
+ },
+};
+
+export const ghost_others: FeatureChoiced = {
+ name: "Ghosts of others",
+ category: "GHOST",
+ description: multiline`
+ 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?
+ `,
+ component: FeatureDropdownInput,
+};
+
+export const inquisitive_ghost: FeatureToggle = {
+ name: "Ghost inquisitiveness",
+ category: "GHOST",
+ description: "Clicking on something as a ghost will examine it.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/hotkeys.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/hotkeys.tsx
new file mode 100644
index 00000000000..73cc3c9de16
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/hotkeys.tsx
@@ -0,0 +1,8 @@
+import { CheckboxInputInverse, FeatureToggle } from "../base";
+
+export const hotkeys: FeatureToggle = {
+ name: "Classic hotkeys",
+ category: "GAMEPLAY",
+ description: "When enabled, will revert to the legacy hotkeys, using the input bar rather than popups.",
+ component: CheckboxInputInverse,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/item_outlines.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/item_outlines.tsx
new file mode 100644
index 00000000000..3877da11007
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/item_outlines.tsx
@@ -0,0 +1,8 @@
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const itemoutline_pref: FeatureToggle = {
+ name: "Item outlines",
+ category: "GAMEPLAY",
+ description: "When enabled, hovering over items will outline them.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/legacy_chat_toggles.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/legacy_chat_toggles.tsx
new file mode 100644
index 00000000000..dc359e57d45
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/legacy_chat_toggles.tsx
@@ -0,0 +1,89 @@
+import { multiline } from "common/string";
+import { FeatureToggle, CheckboxInput } from "../base";
+
+export const chat_bankcard: FeatureToggle = {
+ name: "Enable income updates",
+ category: "CHAT",
+ description: "Receive notifications for your bank account.",
+ component: CheckboxInput,
+};
+
+export const chat_dead: FeatureToggle = {
+ name: "Enable deadchat",
+ category: "ADMIN",
+ component: CheckboxInput,
+};
+
+export const chat_ghostears: FeatureToggle = {
+ name: "Hear all messages",
+ category: "GHOST",
+ description: multiline`
+ When enabled, you will be able to hear all speech as a ghost.
+ When disabled, you will only be able to hear nearby speech.
+ `,
+ component: CheckboxInput,
+};
+
+export const chat_ghostlaws: FeatureToggle = {
+ name: "Enable law change updates",
+ category: "GHOST",
+ description: "When enabled, be notified of any new law changes as a ghost.",
+ component: CheckboxInput,
+};
+
+export const chat_ghostpda: FeatureToggle = {
+ name: "Enable PDA notifications",
+ category: "GHOST",
+ description: "When enabled, be notified of any PDA messages as a ghost.",
+ component: CheckboxInput,
+};
+
+export const chat_ghostradio: FeatureToggle = {
+ name: "Enable radio",
+ category: "GHOST",
+ description: "When enabled, be notified of any radio messages as a ghost.",
+ component: CheckboxInput,
+};
+
+export const chat_ghostsight: FeatureToggle = {
+ name: "See all emotes",
+ category: "GHOST",
+ description: "When enabled, see all emotes as a ghost.",
+ component: CheckboxInput,
+};
+
+export const chat_ghostwhisper: FeatureToggle = {
+ name: "See all whispers",
+ category: "GHOST",
+ description: multiline`
+ When enabled, you will be able to hear all whispers as a ghost.
+ When disabled, you will only be able to hear nearby whispers.
+ `,
+ component: CheckboxInput,
+};
+
+export const chat_login_logout: FeatureToggle = {
+ name: "See login/logout messages",
+ category: "GHOST",
+ description: "When enabled, be notified when a player logs in or out.",
+ component: CheckboxInput,
+};
+
+export const chat_ooc: FeatureToggle = {
+ name: "Enable OOC",
+ category: "CHAT",
+ component: CheckboxInput,
+};
+
+export const chat_prayer: FeatureToggle = {
+ name: "Listen to prayers",
+ category: "ADMIN",
+ component: CheckboxInput,
+};
+
+export const chat_pullr: FeatureToggle = {
+ name: "Enable pull request notifications",
+ category: "CHAT",
+ description: "Be notified when a pull request is made, closed, or merged.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/legacy_toggles.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/legacy_toggles.tsx
new file mode 100644
index 00000000000..6b6c44661e5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/legacy_toggles.tsx
@@ -0,0 +1,153 @@
+import { multiline } from "common/string";
+import { FeatureToggle, CheckboxInput, CheckboxInputInverse } from "../base";
+
+export const admin_ignore_cult_ghost: FeatureToggle = {
+ name: "Prevent being summoned as a cult ghost",
+ category: "ADMIN",
+ description: multiline`
+ When enabled and observing, prevents Spirit Realm from forcing you
+ into a cult ghost.
+ `,
+ component: CheckboxInput,
+};
+
+export const announce_login: FeatureToggle = {
+ name: "Announce login",
+ category: "ADMIN",
+ description: "Admins will be notified when you login.",
+ component: CheckboxInput,
+};
+
+export const combohud_lighting: FeatureToggle = {
+ name: "Enable fullbright Combo HUD",
+ category: "ADMIN",
+ component: CheckboxInput,
+};
+
+export const deadmin_always: FeatureToggle = {
+ name: "Auto deadmin - Always",
+ category: "ADMIN",
+ description: "When enabled, you will automatically deadmin.",
+ component: CheckboxInput,
+};
+
+export const deadmin_antagonist: FeatureToggle = {
+ name: "Auto deadmin - Antagonist",
+ category: "ADMIN",
+ description: "When enabled, you will automatically deadmin as an antagonist.",
+ component: CheckboxInput,
+};
+
+export const deadmin_position_head: FeatureToggle = {
+ name: "Auto deadmin - Head of Staff",
+ category: "ADMIN",
+ description: "When enabled, you will automatically deadmin as a head of staff.",
+ component: CheckboxInput,
+};
+
+export const deadmin_position_security: FeatureToggle = {
+ name: "Auto deadmin - Security",
+ category: "ADMIN",
+ description: "When enabled, you will automatically deadmin as a member of security.",
+ component: CheckboxInput,
+};
+
+export const deadmin_position_silicon: FeatureToggle = {
+ name: "Auto deadmin - Silicon",
+ category: "ADMIN",
+ description: "When enabled, you will automatically deadmin as a silicon.",
+ component: CheckboxInput,
+};
+
+export const disable_arrivalrattle: FeatureToggle = {
+ name: "Notify for new arrivals",
+ category: "GHOST",
+ description: "When enabled, you will be notified as a ghost for new crew.",
+ component: CheckboxInputInverse,
+};
+
+export const disable_deathrattle: FeatureToggle = {
+ name: "Notify for deaths",
+ category: "GHOST",
+ description: "When enabled, you will be notified as a ghost whenever someone dies.",
+ component: CheckboxInputInverse,
+};
+
+export const member_public: FeatureToggle = {
+ name: "Publicize BYOND membership",
+ category: "CHAT",
+ description: "When enabled, a BYOND logo will be shown next to your name in OOC.",
+ component: CheckboxInput,
+};
+
+export const sound_adminhelp: FeatureToggle = {
+ name: "Enable adminhelp sounds",
+ category: "ADMIN",
+ component: CheckboxInput,
+};
+
+export const sound_ambience: FeatureToggle = {
+ name: "Enable ambience",
+ category: "SOUND",
+ component: CheckboxInput,
+};
+
+export const sound_announcements: FeatureToggle = {
+ name: "Enable announcement sounds",
+ category: "SOUND",
+ description: "When enabled, hear sounds for command reports, notices, etc.",
+ component: CheckboxInput,
+};
+
+export const sound_combatmode: FeatureToggle = {
+ name: "Enable combat mode sound",
+ category: "SOUND",
+ description: "When enabled, hear sounds when toggling combat mode.",
+ component: CheckboxInput,
+};
+
+export const sound_endofround: FeatureToggle = {
+ name: "Enable end of round sounds",
+ category: "SOUND",
+ description: "When enabled, hear a sound when the server is rebooting.",
+ component: CheckboxInput,
+};
+
+export const sound_instruments: FeatureToggle = {
+ name: "Enable instruments",
+ category: "SOUND",
+ description: "When enabled, be able hear instruments in game.",
+ component: CheckboxInput,
+};
+
+export const sound_lobby: FeatureToggle = {
+ name: "Enable lobby music",
+ category: "SOUND",
+ component: CheckboxInput,
+};
+
+export const sound_midi: FeatureToggle = {
+ name: "Enable admin music",
+ category: "SOUND",
+ description: "When enabled, admins will be able to play music to you.",
+ component: CheckboxInput,
+};
+
+export const sound_prayers: FeatureToggle = {
+ name: "Enable prayer sound",
+ category: "ADMIN",
+ component: CheckboxInput,
+};
+
+export const sound_ship_ambience: FeatureToggle = {
+ name: "Enable ship ambience",
+ category: "SOUND",
+ component: CheckboxInput,
+};
+
+export const split_admin_tabs: FeatureToggle = {
+ name: "Split admin tabs",
+ category: "ADMIN",
+ description: "When enabled, will split the 'Admin' panel into several tabs.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ooc.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ooc.tsx
new file mode 100644
index 00000000000..e4f5c5466f0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ooc.tsx
@@ -0,0 +1,8 @@
+import { FeatureColorInput, Feature } from "../base";
+
+export const ooccolor: Feature = {
+ name: "OOC color",
+ category: "CHAT",
+ description: "The color of your OOC messages.",
+ component: FeatureColorInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/parallax.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/parallax.tsx
new file mode 100644
index 00000000000..1fbb2230ff2
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/parallax.tsx
@@ -0,0 +1,7 @@
+import { Feature, FeatureDropdownInput } from "../base";
+
+export const parallax: Feature = {
+ name: "Parallax (fancy space)",
+ category: "GAMEPLAY",
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/pixel_size.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/pixel_size.tsx
new file mode 100644
index 00000000000..bb07453e340
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/pixel_size.tsx
@@ -0,0 +1,13 @@
+import { createDropdownInput, Feature } from "../base";
+
+export const pixel_size: Feature = {
+ name: "Pixel Scaling",
+ category: "UI",
+ component: createDropdownInput({
+ 0: "Stretch to fit",
+ 1: "Pixel Perfect 1x",
+ 1.5: "Pixel Perfect 1.5x",
+ 2: "Pixel Perfect 2x",
+ 3: "Pixel Perfect 3x",
+ }),
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/preferred_map.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/preferred_map.tsx
new file mode 100644
index 00000000000..3acfad00445
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/preferred_map.tsx
@@ -0,0 +1,13 @@
+import { multiline } from "common/string";
+import { Feature, FeatureDropdownInput } from "../base";
+
+export const preferred_map: Feature = {
+ name: "Preferred map",
+ category: "GAMEPLAY",
+ description: multiline`
+ During map rotation, prefer this map be chosen.
+ This does not affect the map vote, only random rotation when a vote
+ is not held.
+ `,
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/runechat.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/runechat.tsx
new file mode 100644
index 00000000000..d85a814d8ad
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/runechat.tsx
@@ -0,0 +1,29 @@
+import { CheckboxInput, FeatureNumberInput, FeatureNumeric, FeatureToggle } from "../base";
+
+export const chat_on_map: FeatureToggle = {
+ name: "Enable Runechat",
+ category: "RUNECHAT",
+ description: "Chat messages will show above heads.",
+ component: CheckboxInput,
+};
+
+export const see_chat_non_mob: FeatureToggle = {
+ name: "Enable Runechat on objects",
+ category: "RUNECHAT",
+ description: "Chat messages will show above objects when they speak.",
+ component: CheckboxInput,
+};
+
+export const see_rc_emotes: FeatureToggle = {
+ name: "Enable Runechat emotes",
+ category: "RUNECHAT",
+ description: "Emotes will show above heads.",
+ component: CheckboxInput,
+};
+
+export const max_chat_length: FeatureNumeric = {
+ name: "Max chat length",
+ category: "RUNECHAT",
+ description: "The maximum length a Runechat message will show as.",
+ component: FeatureNumberInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/scaling_method.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/scaling_method.tsx
new file mode 100644
index 00000000000..114cc80ef5e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/scaling_method.tsx
@@ -0,0 +1,11 @@
+import { createDropdownInput, Feature } from "../base";
+
+export const scaling_method: Feature = {
+ name: "Scaling method",
+ category: "UI",
+ component: createDropdownInput({
+ blur: "Bilinear",
+ distort: "Nearest Neighbor",
+ normal: "Point Sampling",
+ }),
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/screentips.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/screentips.tsx
new file mode 100644
index 00000000000..5d8575dd280
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/screentips.tsx
@@ -0,0 +1,20 @@
+import { multiline } from "common/string";
+import { FeatureColorInput, Feature, FeatureToggle, CheckboxInput } from "../base";
+
+export const screentip_color: Feature = {
+ name: "Screentips color",
+ category: "UI",
+ description: multiline`
+ The color of screen tips, the text you see when hovering over something.
+ `,
+ component: FeatureColorInput,
+};
+
+export const screentip_pref: FeatureToggle = {
+ name: "Enable screentips",
+ category: "UI",
+ description: multiline`
+ Enables screen tips, the text you see when hovering over something.
+ `,
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/auto_dementor.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/auto_dementor.tsx
new file mode 100644
index 00000000000..e8203eb4ed4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/auto_dementor.tsx
@@ -0,0 +1,8 @@
+import { FeatureToggle, CheckboxInput } from "../../base";
+
+export const auto_dementor_pref: FeatureToggle = {
+ name: "Auto dementor",
+ category: "ADMIN",
+ description: "When enabled, you will automatically dementor.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/be_antag.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/be_antag.tsx
new file mode 100644
index 00000000000..3d9fea988cc
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/be_antag.tsx
@@ -0,0 +1,8 @@
+import { CheckboxInput, FeatureToggle } from "../../base";
+
+export const be_antag_pref: FeatureToggle = {
+ name: "Be antagonist",
+ category: "GAMEPLAY",
+ description: "Toggles whether you wish to be an antagonist or not.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/delete_sparks.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/delete_sparks.tsx
new file mode 100644
index 00000000000..1810df36e29
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/delete_sparks.tsx
@@ -0,0 +1,8 @@
+import { CheckboxInput, FeatureToggle } from "../../base";
+
+export const delete_sparks_pref: FeatureToggle = {
+ name: "Deletion sparks",
+ category: "ADMIN",
+ description: "Toggles if you want to play a sparking animation when deleting things as an admin.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/erp_preferences.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/erp_preferences.tsx
new file mode 100644
index 00000000000..b2495471b26
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/erp_preferences.tsx
@@ -0,0 +1,64 @@
+import { CheckboxInput, FeatureToggle } from "../../base";
+
+export const master_erp_pref: FeatureToggle = {
+ name: "Erotic Roleplay Interaction",
+ category: "ERP",
+ description: "This informs players of if you wish to engage in erotic roleplay.",
+ component: CheckboxInput,
+};
+
+export const cum_face_pref: FeatureToggle = {
+ name: "Cum face",
+ category: "ERP",
+ description: "Toggles if you are able to recieve the 'cumface' trait.",
+ component: CheckboxInput,
+};
+
+export const bimbofication_pref: FeatureToggle = {
+ name: "Bimbofication",
+ category: "ERP",
+ description: "Toggles if you are able to react to the effects of bimbofication.",
+ component: CheckboxInput,
+};
+
+export const aphro_pref: FeatureToggle = {
+ name: "Aphrodisiacs",
+ category: "ERP",
+ description: "Toggles whether you wish to recieve the effects of aphrodisiacs",
+ component: CheckboxInput,
+};
+
+export const noncon_pref: FeatureToggle = {
+ name: "Non-con Roleplay",
+ category: "ERP",
+ description: "This informs players of if you wish to engage in non-conforming erotic roleplay.",
+ component: CheckboxInput,
+};
+
+export const sextoy_pref: FeatureToggle = {
+ name: "Sex toy interaction",
+ category: "ERP",
+ description: "When enabled, you will be able to interact with sex toys.",
+ component: CheckboxInput,
+};
+
+export const breast_enlargement_pref: FeatureToggle = {
+ name: "Breast enlargement",
+ category: "ERP",
+ description: "Determines if you wish to recieve the effects of breast enlargement chemicals.",
+ component: CheckboxInput,
+};
+
+export const penis_enlargement_pref: FeatureToggle = {
+ name: "Penis enlargement",
+ category: "ERP",
+ description: "Determines if you wish to recieve the effects of penis enlargement chemicals.",
+ component: CheckboxInput,
+};
+
+export const gender_change_pref: FeatureToggle = {
+ name: "Forced gender change",
+ category: "ERP",
+ description: "Determines if you wish to allow forced gender changing.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/looc.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/looc.tsx
new file mode 100644
index 00000000000..49bf8828aca
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/looc.tsx
@@ -0,0 +1,15 @@
+import { CheckboxInput, FeatureToggle } from "../../base";
+
+export const looc_pref: FeatureToggle = {
+ name: "See LOOC",
+ category: "CHAT",
+ description: "Toggles whether you want to see LOOC or not.",
+ component: CheckboxInput,
+};
+
+export const looc_admin_pref: FeatureToggle = {
+ name: "See admin LOOC",
+ category: "ADMIN",
+ description: "Toggles whether you want to see LOOC anywhere as an admin or not.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/tgui.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/tgui.tsx
new file mode 100644
index 00000000000..f436e9d45a5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/tgui.tsx
@@ -0,0 +1,15 @@
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const tgui_fancy: FeatureToggle = {
+ name: "Enable fancy tgui",
+ category: "UI",
+ description: "Makes tgui windows look better, at the cost of compatibility.",
+ component: CheckboxInput,
+};
+
+export const tgui_lock: FeatureToggle = {
+ name: "Lock tgui to main monitor",
+ category: "UI",
+ description: "Locks tgui windows to your main monitor.",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/tooltips.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/tooltips.tsx
new file mode 100644
index 00000000000..d3147a01a1f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/tooltips.tsx
@@ -0,0 +1,20 @@
+import { multiline } from "common/string";
+import { CheckboxInput, Feature, FeatureNumberInput, FeatureToggle } from "../base";
+
+export const enable_tips: FeatureToggle = {
+ name: "Enable tooltips",
+ category: "TOOLTIPS",
+ description: multiline`
+ Do you want to see tooltips when hovering over items?
+ `,
+ component: CheckboxInput,
+};
+
+export const tip_delay: Feature = {
+ name: "Tooltip delay (in milliseconds)",
+ category: "TOOLTIPS",
+ description: multiline`
+ How long should it take to see a tooltip when hovering over items?
+ `,
+ component: FeatureNumberInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ui_style.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ui_style.tsx
new file mode 100644
index 00000000000..83a315bccac
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ui_style.tsx
@@ -0,0 +1,63 @@
+import { classes } from "common/react";
+import { FeatureChoiced, FeatureChoicedServerData, FeatureValueProps, sortChoices } from "../base";
+import { Box, Dropdown, Stack } from "../../../../../components";
+
+const UIStyleInput = (
+ props: FeatureValueProps
+) => {
+ const { serverData, value } = props;
+ if (!serverData) {
+ return null;
+ }
+
+ const { icons } = serverData;
+
+ if (!icons) {
+ return (ui_style had no icons! );
+ }
+
+ const choices = Object.fromEntries(
+ Object.entries(icons)
+ .map(([name, icon]) => {
+ return [name, (
+
+
+
+
+
+
+ {name}
+
+
+ )];
+ })
+ );
+
+ return (
+ {
+ return {
+ displayText: label,
+ value: dataValue,
+ };
+ })}
+ />
+ );
+};
+
+export const UI_style: FeatureChoiced = {
+ name: "UI Style",
+ category: "UI",
+ component: UIStyleInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/widescreen.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/widescreen.tsx
new file mode 100644
index 00000000000..e4d7595775e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/widescreen.tsx
@@ -0,0 +1,7 @@
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const widescreenpref: FeatureToggle = {
+ name: "Enable widescreen",
+ category: "UI",
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/window_flashing.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/window_flashing.tsx
new file mode 100644
index 00000000000..58f32c94768
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/window_flashing.tsx
@@ -0,0 +1,12 @@
+import { multiline } from "common/string";
+import { CheckboxInput, FeatureToggle } from "../base";
+
+export const windowflashing: FeatureToggle = {
+ name: "Enable window flashing",
+ category: "UI",
+ description: multiline`
+ When toggled, some important events will make your game icon flash on your
+ task tray.
+ `,
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/index.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/index.ts
new file mode 100644
index 00000000000..d487a282e58
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/index.ts
@@ -0,0 +1,23 @@
+// Unlike species and others, feature files export arrays of features
+// rather than individual ones. This is because a lot of features are
+// extremely small, and so it's easier for everyone to just combine them
+// together.
+// This still helps to prevent the server from needing to send client UI data
+import { Feature } from "./base";
+
+// while also preventing downstreams from needing to mutate existing files.
+const features: Record> = {};
+
+const requireFeature = require.context("./", true, /.tsx$/);
+
+for (const key of requireFeature.keys()) {
+ if (key === "index" || key === "base") {
+ continue;
+ }
+
+ for (const [featureKey, feature] of Object.entries(requireFeature(key))) {
+ features[featureKey] = feature as Feature;
+ }
+}
+
+export default features;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/randomization.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/randomization.tsx
new file mode 100644
index 00000000000..3d47560edf9
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/randomization.tsx
@@ -0,0 +1,90 @@
+import { useBackend } from "../../../../backend";
+import { Button, Stack } from "../../../../components";
+import { PreferencesMenuData, RandomSetting } from "../../data";
+import { RandomizationButton } from "../../RandomizationButton";
+import { useRandomToggleState } from "../../useRandomToggleState";
+import { CheckboxInput, Feature, FeatureToggle } from "./base";
+
+export const random_body: Feature = {
+ name: "Random body",
+ component: (props, context) => {
+ const [randomToggle, setRandomToggle] = useRandomToggleState(context);
+
+ return (
+
+
+ props.handleSetValue(newValue)}
+ value={props.value}
+ />
+
+
+ {
+ randomToggle
+ ? (
+ <>
+
+ {
+ props.act("randomize_character");
+ setRandomToggle(false);
+ }}>
+ Randomize
+
+
+
+
+ setRandomToggle(false)}>
+ Cancel
+
+
+ >
+ )
+ : (
+
+ setRandomToggle(true)}>
+ Randomize
+
+
+ )
+ }
+
+
+ );
+ },
+};
+
+export const random_hardcore: FeatureToggle = {
+ name: "Hardcore random",
+ component: CheckboxInput,
+};
+
+export const random_name: Feature = {
+ name: "Random name",
+ component: (props, context) => {
+ return (
+ props.handleSetValue(value)}
+ value={props.value}
+ />
+ );
+ },
+};
+
+export const random_species: Feature = {
+ name: "Random species",
+ component: (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const species = data.character_preferences.randomization["species"];
+
+ return (
+ act("set_random_preference", {
+ preference: "species",
+ value: newValue,
+ })}
+ value={species || RandomSetting.Disabled}
+ />
+ );
+ },
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/species_features.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/species_features.tsx
new file mode 100644
index 00000000000..aafa1588fde
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/species_features.tsx
@@ -0,0 +1,51 @@
+import { FeatureColorInput, Feature, FeatureChoiced, FeatureDropdownInput } from "./base";
+
+export const eye_color: Feature = {
+ name: "Eye color",
+ component: FeatureColorInput,
+};
+
+export const facial_hair_color: Feature = {
+ name: "Facial hair color",
+ component: FeatureColorInput,
+};
+
+export const hair_color: Feature = {
+ name: "Hair color",
+ component: FeatureColorInput,
+};
+
+export const feature_human_ears: FeatureChoiced = {
+ name: "Ears",
+ component: FeatureDropdownInput,
+};
+
+export const feature_human_tail: FeatureChoiced = {
+ name: "Tail",
+ component: FeatureDropdownInput,
+};
+
+export const feature_lizard_legs: FeatureChoiced = {
+ name: "Legs",
+ component: FeatureDropdownInput,
+};
+
+export const feature_lizard_spines: FeatureChoiced = {
+ name: "Spines",
+ component: FeatureDropdownInput,
+};
+
+export const feature_lizard_tail: FeatureChoiced = {
+ name: "Tail",
+ component: FeatureDropdownInput,
+};
+
+export const feature_mcolor: Feature = {
+ name: "Mutant color",
+ component: FeatureColorInput,
+};
+
+export const underwear_color: Feature = {
+ name: "Underwear color",
+ component: FeatureColorInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/gender.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/gender.ts
new file mode 100644
index 00000000000..787a3c900cb
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/gender.ts
@@ -0,0 +1,22 @@
+export enum Gender {
+ Male = "male",
+ Female = "female",
+ Other = "plural",
+}
+
+export const GENDERS = {
+ [Gender.Male]: {
+ icon: "male",
+ text: "Male",
+ },
+
+ [Gender.Female]: {
+ icon: "female",
+ text: "Female",
+ },
+
+ [Gender.Other]: {
+ icon: "tg-non-binary",
+ text: "Other",
+ },
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/base.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/base.ts
new file mode 100644
index 00000000000..7f99548c5b5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/base.ts
@@ -0,0 +1,33 @@
+export type Species = {
+ description: string;
+ features: {
+ good: Feature[],
+ neutral: Feature[],
+ bad: Feature[],
+ };
+ lore?: string[];
+};
+
+export type Feature = {
+ icon: string;
+ name: string;
+ description: string;
+};
+
+export const fallbackSpecies: Species = {
+ description: "No description! File a bug report!",
+ features: {
+ good: [],
+ neutral: [],
+ bad: [],
+ },
+};
+
+export const createLanguagePerk = (language: string): Feature => {
+ return {
+ icon: "comment",
+ name: "Native Speaker",
+ description:
+ `Alongside Galactic Common, gain the ability to speak ${language}.`,
+ };
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/ethereal.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/ethereal.ts
new file mode 100644
index 00000000000..2ff0f9cc891
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/ethereal.ts
@@ -0,0 +1,64 @@
+import { createLanguagePerk, Species } from "./base";
+
+const Ethereal: Species = {
+ description: "Coming from the planet of Sprout, the theocratic ethereals are \
+ separated socially by caste, and espouse a dogma of aiding the weak and \
+ downtrodden.",
+ features: {
+ good: [{
+ icon: "bolt",
+ name: "Shockingly Tasty",
+ description: "Ethereals can feed on electricity from APCs, and do not \
+ otherwise need to eat.",
+ }, {
+ icon: "lightbulb",
+ name: "Disco Ball",
+ description: "Ethereals passively generate their own light.",
+ }, {
+ icon: "shield-alt",
+ name: "Shock Resistance",
+ description: "Ethereals are less affected by shocks.",
+ }, {
+ icon: "temperature-high",
+ name: "Heat Resistance",
+ description: "Ethereals have much better tolerance for high \
+ temperatures.",
+ }, createLanguagePerk("Voltaic")],
+ neutral: [{
+ icon: "tint",
+ name: "Liquid Electricity",
+ description: "Ethereals have liquid electricity instead of blood. \
+ Great for them, horrid for anyone else. Can make receiving medical \
+ treatment harder.",
+ }, {
+ icon: "fire",
+ name: "Flaming Punch",
+ description: "Ethereals deal burn damage when punching instead of \
+ brute damage.",
+ }, {
+ icon: "gem",
+ name: "Crystal Core",
+ description: "The hearts of ethereals will protect them in a cystal when \
+ they die, reviving them with a permanent brain trauma.",
+ }],
+ bad: [{
+ icon: "biohazard",
+ name: "Starving Artist",
+ description: "Ethereals take toxin damage while starving.",
+ }, {
+ icon: "fist-raised",
+ name: "Brutal Weakness",
+ description: "Ethereals are weak to brute damage.",
+ }, {
+ icon: "temperature-low",
+ name: "Cold Weakness",
+ description: "Ethereals have much lower tolerance for cold \
+ temperatures.",
+ }],
+ },
+ lore: [
+ "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.",
+ ],
+};
+
+export default Ethereal;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/felinid.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/felinid.ts
new file mode 100644
index 00000000000..4ab3ff566bd
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/felinid.ts
@@ -0,0 +1,28 @@
+import { createLanguagePerk, Species } from "./base";
+
+const Felinid: Species = {
+ description: "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?",
+ features: {
+ good: [{
+ icon: "grin-tongue",
+ name: "Grooming",
+ description: "Felinids can lick wounds to reduce bleeding.",
+ }, createLanguagePerk("Nekomimetic")],
+ neutral: [],
+ bad: [{
+ icon: "assistive-listening-systems",
+ name: "Sensitive Hearing",
+ description: "Felinids are more sensitive to loud sounds, such as \
+ flashbangs.",
+ }],
+ },
+ lore: [
+ "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.",
+ ],
+};
+
+export default Felinid;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/human.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/human.ts
new file mode 100644
index 00000000000..30b1c28853b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/human.ts
@@ -0,0 +1,30 @@
+import { Species } from "./base";
+
+const Human: Species = {
+ description: "Humans are the dominant species in the known galaxy, their \
+ kind extend from old Earth to the edges of known space.",
+ features: {
+ good: [{
+ icon: "robot",
+ name: "Asimov Superiority",
+ description: "The AI and their cyborgs are, by default, subservient only \
+ to humans. As a human, silicons are required to both protect and obey \
+ you.",
+ }, {
+ icon: "bullhorn",
+ name: "Chain of Command",
+ description: "Nanotrasen only recognizes humans for command roles, such \
+ as Captain.",
+ }],
+ neutral: [],
+ bad: [],
+ },
+ lore: [
+ "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\".",
+ ],
+};
+
+export default Human;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/lizard.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/lizard.ts
new file mode 100644
index 00000000000..0345b469dab
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/lizard.ts
@@ -0,0 +1,32 @@
+import { createLanguagePerk, Species } from "./base";
+
+const Lizard: Species = {
+ description: "The militaristic 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.",
+ features: {
+ good: [createLanguagePerk("Draconic")],
+ neutral: [{
+ icon: "thermometer-empty",
+ name: "Cold-blooded",
+ description: "Higher tolerance for high temperatures, but lower \
+ tolerance for cold temperatures.",
+ }],
+ bad: [{
+ icon: "tint",
+ name: "Exotic Blood",
+ description: "Lizards have a unique \"L\" type blood, which can make \
+ receiving medical treatment more difficult.",
+ }],
+ },
+ lore: [
+ "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!\"",
+ ],
+};
+
+export default Lizard;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/moth.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/moth.ts
new file mode 100644
index 00000000000..338c650ddbb
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/moth.ts
@@ -0,0 +1,38 @@
+import { createLanguagePerk, Species } from "./base";
+
+const Moth: Species = {
+ description: "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.",
+ features: {
+ good: [{
+ icon: "feather-alt",
+ name: "Precious Wings",
+ description: "Moths can fly in pressurized, zero-g environments using \
+ their wings.",
+ }, {
+ icon: "tshirt",
+ name: "Meal Plan",
+ description: "Moths can eat clothes for nourishment.",
+ }, createLanguagePerk("Moffic")],
+ neutral: [],
+ bad: [{
+ icon: "fire",
+ name: "Ablazed Wings",
+ description: "Moth wings are fragile, and can be easily burnt off.",
+ }, {
+ icon: "sun",
+ name: "Bright Lights",
+ description: "Moths need an extra layer of flash protection to protect \
+ themselves, such as against security officers or when welding. Welding \
+ masks will work.",
+ }],
+ },
+ lore: [
+ "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.",
+ ],
+};
+
+export default Moth;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/plasmaman.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/plasmaman.ts
new file mode 100644
index 00000000000..34a7bee72d2
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/species/plasmaman.ts
@@ -0,0 +1,71 @@
+import { createLanguagePerk, Species } from "./base";
+
+const Plasmaman: Species = {
+ description: "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.",
+ features: {
+ good: [{
+ icon: "shield-alt",
+ name: "Protected",
+ description: "Plasmamen are immune to radiation, poisons, and most \
+ diseases.",
+ }, {
+ icon: "tint-slash",
+ name: "Bloodletted",
+ description: "Plasmamen do not have blood.",
+ }, {
+ icon: "bone",
+ name: "Wound Resistance",
+ description: "Plasmamen have higher tolerance for damage that would \
+ wound others.",
+ }, {
+ icon: "temperature-low",
+ name: "Cold Resistance",
+ description: "Plasmamen have a higher resistance to cold temperatures.",
+ }, {
+ icon: "wind",
+ name: "Plasma Healing",
+ description: "Plasmamen can heal wounds by consuming plasma.",
+ }, {
+ icon: "hard-hat",
+ name: "Protective Helmet",
+ description: "Plasmamen's helmets provide them shielding from the \
+ flashes of welding, as well as a flashlight.",
+ }, createLanguagePerk("Calcic")],
+ neutral: [],
+ bad: [{
+ icon: "fire",
+ name: "Human* Torch",
+ description: "Plasmamen instantly ignite when their body makes contact \
+ with oxygen.",
+ }, {
+ icon: "wind",
+ name: "Plasma Breathing",
+ description: "Plasmamen must breathe plasma to survive. You receive a \
+ tank when you arrive.",
+ }, {
+ icon: "temperature-high",
+ name: "Heat Weakness",
+ description: "Plasmamen have a lower resistance to high temperatures.",
+ }, {
+ icon: "fist-raised",
+ name: "Total Weakness",
+ description: "Plasmamen take more burn and brute damage.",
+ }, {
+ icon: "briefcase-medical",
+ name: "An Apple a Day",
+ description: "Plasmamen take specialized medical knowledge to be \
+ treated. Do not expect speedy revival, if you are lucky enough to get \
+ one at all.",
+ }],
+ },
+ lore: [
+ "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.",
+ ],
+};
+
+export default Plasmaman;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/useRandomToggleState.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/useRandomToggleState.ts
new file mode 100644
index 00000000000..6721ee70cf5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/useRandomToggleState.ts
@@ -0,0 +1,4 @@
+import { useLocalState } from "../../backend";
+
+export const useRandomToggleState
+ = context => useLocalState(context, "randomToggle", false);
diff --git a/tgui/packages/tgui/interfaces/RapidPipeDispenser.js b/tgui/packages/tgui/interfaces/RapidPipeDispenser.js
index 40ad17ec32e..814daed2217 100644
--- a/tgui/packages/tgui/interfaces/RapidPipeDispenser.js
+++ b/tgui/packages/tgui/interfaces/RapidPipeDispenser.js
@@ -1,7 +1,7 @@
import { classes } from 'common/react';
import { multiline } from 'common/string';
import { useBackend, useLocalState } from '../backend';
-import { Box, Button, ColorBox, Dimmer, LabeledList, Section, Stack, Tabs } from '../components';
+import { Box, Button, ColorBox, LabeledList, Section, Stack, Tabs } from '../components';
import { Window } from '../layouts';
const ROOT_CATEGORIES = [
diff --git a/tgui/packages/tgui/interfaces/ReligiousTool.js b/tgui/packages/tgui/interfaces/ReligiousTool.js
index c5c28ae75db..48c7d6832dd 100644
--- a/tgui/packages/tgui/interfaces/ReligiousTool.js
+++ b/tgui/packages/tgui/interfaces/ReligiousTool.js
@@ -1,6 +1,5 @@
-import { capitalize } from 'common/string';
import { useBackend, useSharedState } from '../backend';
-import { AnimatedNumber, BlockQuote, Box, Button, Collapsible, Dimmer, Icon, LabeledList, NoticeBox, ProgressBar, Section, Stack, Tabs } from '../components';
+import { BlockQuote, Box, Button, Collapsible, Dimmer, Icon, Section, Stack, Tabs } from '../components';
import { Window } from '../layouts';
const ALIGNMENT2COLOR = {
diff --git a/tgui/packages/tgui/interfaces/SDQLSpellAdminPanel.tsx b/tgui/packages/tgui/interfaces/SDQLSpellAdminPanel.tsx
index 0a2f5cce6b2..5dd4d6099a4 100644
--- a/tgui/packages/tgui/interfaces/SDQLSpellAdminPanel.tsx
+++ b/tgui/packages/tgui/interfaces/SDQLSpellAdminPanel.tsx
@@ -1,4 +1,3 @@
-import { BooleanLike } from "common/react";
import { useBackend } from "../backend";
import { Button, Table } from "../components";
import { Window } from "../layouts";
diff --git a/tgui/packages/tgui/interfaces/ScannerGate.js b/tgui/packages/tgui/interfaces/ScannerGate.js
index 8de12f681e5..2c3398017ca 100644
--- a/tgui/packages/tgui/interfaces/ScannerGate.js
+++ b/tgui/packages/tgui/interfaces/ScannerGate.js
@@ -1,5 +1,5 @@
import { useBackend } from '../backend';
-import { Box, Button, LabeledList, NumberInput, Section } from '../components';
+import { Box, Button, LabeledList, Section } from '../components';
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
import { Window } from '../layouts';
diff --git a/tgui/packages/tgui/interfaces/Spellbook.js b/tgui/packages/tgui/interfaces/Spellbook.js
index 3c7a28ae61a..72ef5acbbd8 100644
--- a/tgui/packages/tgui/interfaces/Spellbook.js
+++ b/tgui/packages/tgui/interfaces/Spellbook.js
@@ -1,6 +1,6 @@
import { multiline } from 'common/string';
import { useBackend, useLocalState } from '../backend';
-import { Blink, Box, Button, Dimmer, Divider, Icon, Modal, NoticeBox, ProgressBar, Section, Stack } from '../components';
+import { Box, Button, Dimmer, Divider, Icon, NoticeBox, ProgressBar, Section, Stack } from '../components';
import { Window } from '../layouts';
const TAB2NAME = [
diff --git a/tgui/packages/tgui/interfaces/StripMenu.tsx b/tgui/packages/tgui/interfaces/StripMenu.tsx
index 8a24362e443..ccca6ba657f 100644
--- a/tgui/packages/tgui/interfaces/StripMenu.tsx
+++ b/tgui/packages/tgui/interfaces/StripMenu.tsx
@@ -390,17 +390,9 @@ export const StripMenu = (props, context) => {
{slot.image && (
)}
diff --git a/tgui/packages/tgui/interfaces/Telecomms.js b/tgui/packages/tgui/interfaces/Telecomms.js
index 77175c7c4f8..ad83346594c 100644
--- a/tgui/packages/tgui/interfaces/Telecomms.js
+++ b/tgui/packages/tgui/interfaces/Telecomms.js
@@ -1,5 +1,3 @@
-import { map, sortBy } from 'common/collections';
-import { flow } from 'common/fp';
import { useBackend } from '../backend';
import { Button, Input, LabeledList, Section, Table, NoticeBox, NumberInput, LabeledControls, Box } from '../components';
import { RADIO_CHANNELS } from '../constants';
diff --git a/tgui/packages/tgui/interfaces/TramControl.js b/tgui/packages/tgui/interfaces/TramControl.js
index f6590b011d0..43b7016f85d 100644
--- a/tgui/packages/tgui/interfaces/TramControl.js
+++ b/tgui/packages/tgui/interfaces/TramControl.js
@@ -1,4 +1,3 @@
-import { classes } from 'common/react';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Dimmer, Icon, Section, Stack } from '../components';
import { Window } from '../layouts';
diff --git a/tgui/packages/tgui/layouts/Window.js b/tgui/packages/tgui/layouts/Window.js
index a291cc6d5a3..1225e34c3f5 100644
--- a/tgui/packages/tgui/layouts/Window.js
+++ b/tgui/packages/tgui/layouts/Window.js
@@ -9,7 +9,7 @@ import { useDispatch } from 'common/redux';
import { decodeHtmlEntities, toTitleCase } from 'common/string';
import { Component } from 'inferno';
import { backendSuspendStart, useBackend } from '../backend';
-import { Icon, Flex } from '../components';
+import { Icon } from '../components';
import { UI_DISABLED, UI_INTERACTIVE, UI_UPDATE } from '../constants';
import { useDebug } from '../debug';
import { toggleKitchenSink } from '../debug/actions';
diff --git a/tgui/packages/tgui/stories/Popper.stories.js b/tgui/packages/tgui/stories/Popper.stories.js
index 2f86fa02311..25388740bb2 100644
--- a/tgui/packages/tgui/stories/Popper.stories.js
+++ b/tgui/packages/tgui/stories/Popper.stories.js
@@ -1,4 +1,3 @@
-import { Component, forwardRef } from "inferno";
import { Box, Popper } from "../components";
export const meta = {
diff --git a/tgui/packages/tgui/stories/Tabs.stories.js b/tgui/packages/tgui/stories/Tabs.stories.js
index 44ee1218bcb..67efa65f259 100644
--- a/tgui/packages/tgui/stories/Tabs.stories.js
+++ b/tgui/packages/tgui/stories/Tabs.stories.js
@@ -5,7 +5,7 @@
*/
import { useLocalState } from '../backend';
-import { Box, Button, Divider, Section, Tabs } from '../components';
+import { Button, Section, Tabs } from '../components';
export const meta = {
title: 'Tabs',
diff --git a/tgui/packages/tgui/stories/Tooltip.stories.js b/tgui/packages/tgui/stories/Tooltip.stories.js
index a5c4b54e998..03425a222a9 100644
--- a/tgui/packages/tgui/stories/Tooltip.stories.js
+++ b/tgui/packages/tgui/stories/Tooltip.stories.js
@@ -4,7 +4,6 @@
* @license MIT
*/
-import { Placement } from '@popperjs/core';
import { Box, Button, Section, Tooltip } from '../components';
export const meta = {
diff --git a/tgui/packages/tgui/styles/atomic/centered-image.scss b/tgui/packages/tgui/styles/atomic/centered-image.scss
new file mode 100644
index 00000000000..cce5bfdf2c1
--- /dev/null
+++ b/tgui/packages/tgui/styles/atomic/centered-image.scss
@@ -0,0 +1,7 @@
+.centered-image {
+ position: absolute;
+ height: 100%;
+ left: 50%;
+ top: 50%;
+ transform: translateX(-50%) translateY(-50%) scale(0.8);
+}
diff --git a/tgui/packages/tgui/styles/atomic/fit-text.scss b/tgui/packages/tgui/styles/atomic/fit-text.scss
new file mode 100644
index 00000000000..2a899601ba2
--- /dev/null
+++ b/tgui/packages/tgui/styles/atomic/fit-text.scss
@@ -0,0 +1,10 @@
+$mqIterations: 19;
+@mixin fontResize($iterations) {
+ $i: 1;
+ @while $i <= $iterations {
+ @media all and (min-width: 100px * $i) { .fit-text { font-size:0.1em * $i; } }
+ $i: $i + 1;
+ }
+}
+
+@include fontResize($mqIterations);
diff --git a/tgui/packages/tgui/styles/colors.scss b/tgui/packages/tgui/styles/colors.scss
index ca894ffab36..496bb31ef50 100644
--- a/tgui/packages/tgui/styles/colors.scss
+++ b/tgui/packages/tgui/styles/colors.scss
@@ -22,6 +22,7 @@ $purple: #a333c8 !default;
$pink: #e03997 !default;
$brown: #a5673f !default;
$grey: #767676 !default;
+$light-grey: #aaa !default;
$primary: #4972a1 !default;
$good: #5baa27 !default;
@@ -58,6 +59,7 @@ $_gen_map: (
'pink': $pink,
'brown': $brown,
'grey': $grey,
+ 'light-grey': $light-grey,
'good': $good,
'average': $average,
'bad': $bad,
diff --git a/tgui/packages/tgui/styles/components/Dropdown.scss b/tgui/packages/tgui/styles/components/Dropdown.scss
index a5369b53f16..40928642d67 100644
--- a/tgui/packages/tgui/styles/components/Dropdown.scss
+++ b/tgui/packages/tgui/styles/components/Dropdown.scss
@@ -74,7 +74,6 @@
.Dropdown__selected-text {
display: inline-block;
text-overflow: ellipsis;
- overflow: hidden;
white-space: nowrap;
height: base.em(17px);
width: calc(100% - 1.2em);
diff --git a/tgui/packages/tgui/styles/components/LabeledList.scss b/tgui/packages/tgui/styles/components/LabeledList.scss
index bbda277c90c..9a0a82d0378 100644
--- a/tgui/packages/tgui/styles/components/LabeledList.scss
+++ b/tgui/packages/tgui/styles/components/LabeledList.scss
@@ -32,7 +32,6 @@
padding: 0.25em 0.5em;
border: 0;
text-align: left;
- vertical-align: baseline;
}
.LabeledList__label {
diff --git a/tgui/packages/tgui/styles/interfaces/CrewManifest.scss b/tgui/packages/tgui/styles/interfaces/CrewManifest.scss
index 47c23ff1a29..285b5e9a12b 100644
--- a/tgui/packages/tgui/styles/interfaces/CrewManifest.scss
+++ b/tgui/packages/tgui/styles/interfaces/CrewManifest.scss
@@ -9,7 +9,7 @@ $department_map: (
'Science': colors.$purple,
'Supply': colors.$brown,
'Service': colors.$green,
- 'Silicon': colors.$pink
+ 'Silicon': colors.$pink,
);
.CrewManifest {
diff --git a/tgui/packages/tgui/styles/interfaces/PreferencesMenu.scss b/tgui/packages/tgui/styles/interfaces/PreferencesMenu.scss
new file mode 100644
index 00000000000..6f936a1ce4c
--- /dev/null
+++ b/tgui/packages/tgui/styles/interfaces/PreferencesMenu.scss
@@ -0,0 +1,241 @@
+@use 'sass:color';
+@use 'sass:map';
+@use '../components/Button.scss';
+@use '../colors.scss';
+
+$department_map: (
+ 'Assistant': colors.$grey,
+ 'Captain': colors.fg(colors.$blue),
+ 'Command': colors.$yellow,
+ 'Security': colors.$red,
+ 'Engineering': #f1a839,
+ 'Medical': colors.$teal,
+ 'Misc': colors.$white,
+ 'Science': colors.fg(colors.$purple),
+ 'Supply': colors.$brown,
+ 'Service': colors.$green,
+ 'Silicon': colors.$pink,
+);
+
+.PreferencesMenu {
+ &__Antags {
+ &__antagSelection {
+ $antagonist_bottom_padding: 10px;
+
+ margin-bottom: -$antagonist_bottom_padding;
+
+ @mixin animate-hover {
+ .antagonist-icon-parent .antagonist-icon {
+ &:hover {
+ transform: scale(1.3);
+ transition: transform 1s ease-out;
+ }
+ }
+ }
+
+ &__antagonist {
+ padding-bottom: $antagonist_bottom_padding;
+ padding-right: 20px;
+
+ .antagonist-icon-parent {
+ border-style: solid;
+ border-radius: 50%;
+ border-width: 4px;
+ box-sizing: content-box;
+ overflow: hidden;
+ position: relative;
+
+ height: 96px;
+ width: 96px;
+
+ .antagonist-icon {
+ border-radius: 50%;
+ -ms-interpolation-mode: nearest-neighbor;
+ overflow: hidden;
+ transition: transform 0.1s ease-in;
+ }
+ }
+
+ &--off {
+ @include animate-hover;
+
+ .antagonist-icon-parent {
+ border-color: colors.$red;
+
+ .antagonist-icon {
+ opacity: 0.5;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+ }
+
+ &--on {
+ @include animate-hover;
+
+ .antagonist-icon-parent {
+ border-color: colors.$green;
+ }
+ }
+
+ &--banned {
+ .antagonist-icon-parent {
+ border-color: colors.$grey;
+ }
+
+ .antagonist-icon {
+ opacity: 0.5;
+ }
+ }
+
+ .antagonist-banned-slash {
+ background: colors.$grey;
+
+ width: 100%;
+ height: 3px;
+
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translateY(-50%) translateX(-50%) rotate(35deg);
+
+ opacity: 0.8;
+ }
+
+ .antagonist-days-left {
+ text-align: center;
+ text-shadow: 1px 1px 1px #222;
+
+ width: 100%;
+
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translateY(-50%) translateX(-50%);
+ }
+ }
+ }
+ }
+
+ &__Jobs {
+ > * {
+ flex: 1;
+ }
+
+ &__departments {
+ @each $department-name, $color-value in $department_map {
+ &--#{$department-name} {
+ &.head {
+ background: $color-value;
+
+ .job-name {
+ font-weight: bold;
+ }
+ }
+
+ background: colors.fg($color-value);
+ border-bottom: 2px solid rgba(0, 0, 0, 0.3);
+ border-left: 2px solid rgba(0, 0, 0, 0.3);
+ border-right: 2px solid rgba(0, 0, 0, 0.3);
+ color: black;
+
+ > * {
+ height: calc(100% + 0.2em);
+ padding-bottom: 0.2em;
+ }
+
+ &:first-child {
+ border-top: 2px solid rgba(0, 0, 0, 0.3);
+ }
+
+ .options {
+ background: rgba(0, 0, 0, 0.2);
+ height: 100%;
+ }
+ }
+
+ &--Captain {
+ border: 3px solid rgba(200, 200, 0, 1);
+
+ &:first-child {
+ border-top: 3px solid rgba(200, 200, 0, 1);
+ }
+
+ .job-name {
+ font-size: 17px;
+ }
+ }
+ }
+
+ &__priority {
+ border: 1px solid rgba(0, 0, 0, 0.3);
+
+ &--off::after {
+ content: "";
+
+ background: rgba(0, 0, 0, 0.2);
+ display: block;
+ height: 80%;
+ left: 50%;
+ position: relative;
+ top: 50%;
+ transform: translateX(-50%) translateY(-50%) rotate(40deg);
+ width: 2px;
+ }
+ }
+ }
+
+ &__PriorityHeader {
+ font-weight: bold;
+ transform:
+ translateX(-4px)
+ translateY(-8px)
+ rotate(315deg);
+ white-space: nowrap;
+ width: 19px;
+ }
+ }
+
+ &__Quirks {
+ &__QuirkList {
+ background-color: colors.$light-grey;
+ height: calc(90vh - 170px);
+ min-height: 100%;
+ overflow-y: scroll;
+
+ &__quirk {
+ background-color: colors.$white;
+ border-bottom: 1px solid black;
+ color: #111;
+ transition: background-color 0.1s ease-in;
+
+ $quality_map: (
+ "positive": colors.$green,
+ "neutral": colors.$white,
+ "negative": colors.$red,
+ );
+
+ @each $quality, $color-value in $quality_map {
+ &--#{$quality} {
+ background-color: $color-value;
+ transition: background-color 0.1s ease-in;
+ }
+ }
+
+ &:hover {
+ background-color: colors.$grey;
+ transition: background-color 0.1s ease-out;
+
+ @each $quality, $color-value in $quality_map {
+ .PreferencesMenu__Quirks__QuirkList__quirk--#{$quality} {
+ background-color: color.scale($color-value, $lightness: -25%);
+ transition: background-color 0.1s ease-out;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss
index 5669a16cef9..eb52319e031 100644
--- a/tgui/packages/tgui/styles/main.scss
+++ b/tgui/packages/tgui/styles/main.scss
@@ -11,8 +11,10 @@
// Atomic classes
@include meta.load-css('./atomic/candystripe.scss');
+@include meta.load-css('./atomic/centered-image.scss');
@include meta.load-css('./atomic/color.scss');
@include meta.load-css('./atomic/debug-layout.scss');
+@include meta.load-css('./atomic/fit-text.scss');
@include meta.load-css('./atomic/links.scss');
@include meta.load-css('./atomic/outline.scss');
@include meta.load-css('./atomic/text.scss');
@@ -53,6 +55,7 @@
@include meta.load-css('./interfaces/HellishRunes.scss');
@include meta.load-css('./interfaces/NuclearBomb.scss');
@include meta.load-css('./interfaces/Paper.scss');
+@include meta.load-css('./interfaces/PreferencesMenu.scss');
@include meta.load-css('./interfaces/Roulette.scss');
@include meta.load-css('./interfaces/Safe.scss');
@include meta.load-css('./interfaces/TachyonArray.scss');
diff --git a/tgui/yarn.lock b/tgui/yarn.lock
index e4d113f1528..a157d9e8af2 100644
--- a/tgui/yarn.lock
+++ b/tgui/yarn.lock
@@ -3709,6 +3709,28 @@ __metadata:
languageName: node
linkType: hard
+"eslint-plugin-unused-imports@npm:^1.1.4":
+ version: 1.1.4
+ resolution: "eslint-plugin-unused-imports@npm:1.1.4"
+ dependencies:
+ eslint-rule-composer: ^0.3.0
+ peerDependencies:
+ "@typescript-eslint/eslint-plugin": ^4.14.2
+ eslint: ^7.19.0
+ peerDependenciesMeta:
+ "@typescript-eslint/eslint-plugin":
+ optional: true
+ checksum: 62c2dac60efee7e0efe0b9b5e1b7c708251ba6208ecdbe7e04015e8c415ba4e1a84e7bd5716cd9e51895449b57077510e3e98b2bfc3d1f288ca6f3dd46883ca6
+ languageName: node
+ linkType: hard
+
+"eslint-rule-composer@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "eslint-rule-composer@npm:0.3.0"
+ checksum: c2f57cded8d1c8f82483e0ce28861214347e24fd79fd4144667974cd334d718f4ba05080aaef2399e3bbe36f7d6632865110227e6b176ed6daa2d676df9281b1
+ languageName: node
+ linkType: hard
+
"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1":
version: 5.1.1
resolution: "eslint-scope@npm:5.1.1"
@@ -8159,6 +8181,7 @@ resolve@^2.0.0-next.3:
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