diff --git a/SQL/beestation_schema.sql b/SQL/beestation_schema.sql index b119119b4b68c..13d3f961c6f70 100644 --- a/SQL/beestation_schema.sql +++ b/SQL/beestation_schema.sql @@ -84,37 +84,72 @@ DROP TABLE IF EXISTS `SS13_characters`; CREATE TABLE IF NOT EXISTS `SS13_characters` ( `slot` INT(11) UNSIGNED NOT NULL, `ckey` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `species` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_general_ci', - `real_name` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `name_is_always_random` TINYINT(1) NOT NULL, - `body_is_always_random` TINYINT(1) NOT NULL, - `gender` VARCHAR(16) NOT NULL COLLATE 'utf8mb4_general_ci', - `age` TINYINT(3) UNSIGNED NOT NULL, - `hair_color` VARCHAR(8) NOT NULL COLLATE 'utf8mb4_general_ci', - `gradient_color` VARCHAR(8) NOT NULL COLLATE 'utf8mb4_general_ci', - `facial_hair_color` VARCHAR(8) NOT NULL COLLATE 'utf8mb4_general_ci', - `eye_color` VARCHAR(8) NOT NULL COLLATE 'utf8mb4_general_ci', - `skin_tone` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `hair_style_name` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `gradient_style` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `facial_style_name` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `underwear` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `underwear_color` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_general_ci', - `undershirt` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `socks` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `backbag` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `jumpsuit_style` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `uplink_loc` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `features` MEDIUMTEXT NOT NULL COLLATE 'utf8mb4_general_ci', - `custom_names` MEDIUMTEXT NOT NULL COLLATE 'utf8mb4_general_ci', - `helmet_style` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `preferred_ai_core_display` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `preferred_security_department` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_general_ci', - `joblessrole` TINYINT(4) UNSIGNED NOT NULL, - `job_preferences` MEDIUMTEXT NOT NULL COLLATE 'utf8mb4_general_ci', - `all_quirks` MEDIUMTEXT NOT NULL COLLATE 'utf8mb4_general_ci', - `equipped_gear` MEDIUMTEXT NOT NULL COLLATE 'utf8mb4_general_ci', - `role_preferences` MEDIUMTEXT NOT NULL COLLATE 'utf8mb4_general_ci', + `species` VARCHAR(32) COLLATE 'utf8mb4_general_ci', + `real_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `human_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `mime_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `clown_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `cyborg_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `ai_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `religion_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `deity_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `bible_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `name_is_always_random` TINYINT(1), + `body_is_always_random` TINYINT(1), + `gender` VARCHAR(16) COLLATE 'utf8mb4_general_ci', + `body_model` VARCHAR(16) COLLATE 'utf8mb4_general_ci', + `body_size` VARCHAR(16) COLLATE 'utf8mb4_general_ci', + `age` TINYINT(3) UNSIGNED, + `hair_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci', + `gradient_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci', + `facial_hair_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci', + `eye_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci', + `skin_tone` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `hair_style_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `gradient_style` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `facial_style_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `underwear` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `underwear_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci', + `undershirt` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `socks` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `backbag` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `jumpsuit_style` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `uplink_loc` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `pda_theme` VARCHAR(32) COLLATE 'utf8mb4_general_ci', + `pda_classic_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci', + `feature_apid_stripes` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_apid_antenna` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_apid_headstripes` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_moth_antennae` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_moth_markings` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_moth_wings` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_ethcolor` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_insect_type` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_ipc_screen` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_ipc_antenna` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_ipc_chassis` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_ipc_screen_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci', + `feature_ipc_antenna_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci', + `feature_lizard_body_markings` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_lizard_frills` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_lizard_horns` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_lizard_legs` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_lizard_snout` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_lizard_spines` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_lizard_tail` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_mcolor` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_human_tail` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_human_ears` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `feature_psyphoza_cap` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `helmet_style` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `preferred_ai_core_display` VARCHAR(64) COLLATE 'utf8mb4_general_ci', + `preferred_security_department` VARCHAR(32) COLLATE 'utf8mb4_general_ci', + `joblessrole` TINYINT(4) UNSIGNED, + `job_preferences` MEDIUMTEXT COLLATE 'utf8mb4_general_ci', + `all_quirks` MEDIUMTEXT COLLATE 'utf8mb4_general_ci', + `equipped_gear` MEDIUMTEXT COLLATE 'utf8mb4_general_ci', + `role_preferences` MEDIUMTEXT COLLATE 'utf8mb4_general_ci', + `randomise` MEDIUMTEXT COLLATE 'utf8mb4_general_ci', PRIMARY KEY (`slot`, `ckey`) USING BTREE ) COLLATE='utf8mb4_general_ci' ENGINE=InnoDB; @@ -408,7 +443,7 @@ CREATE TABLE IF NOT EXISTS `SS13_poll_vote` ( DROP TABLE IF EXISTS `SS13_preferences`; CREATE TABLE `SS13_preferences` ( `ckey` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci', - `preference_tag` INT(11) NOT NULL, + `preference_tag` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_general_ci', `preference_value` MEDIUMTEXT NULL COLLATE 'utf8mb4_general_ci', UNIQUE INDEX `prefbinding` (`ckey`, `preference_tag`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -456,7 +491,7 @@ CREATE TABLE IF NOT EXISTS `SS13_schema_revision` ( `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`major`,`minor`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (6, 1); +INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (7, 0); diff --git a/SQL/database_changelog.txt b/SQL/database_changelog.txt index 9f2edd8038539..ef0ba49342229 100644 --- a/SQL/database_changelog.txt +++ b/SQL/database_changelog.txt @@ -1,15 +1,26 @@ Any time you make a change to the schema files, remember to increment the database schema version. Generally increment the minor number, major should be reserved for significant changes to the schema. Both values go up to 255. -The latest database version is 6.2; The query to update the schema revision table is: +The latest database version is 7.0; The query to update the schema revision table is: -INSERT INTO `schema_revision` (`major`, `minor`) VALUES (6, 2); +INSERT INTO `schema_revision` (`major`, `minor`) VALUES (7, 0); or -INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (6, 2); +INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (7, 0); In any query remember to add a prefix to the table names if you use one. ----------------------------------------------------- +----------------------------------------------------- + +Version 7.0, 24 July 2023, by itsmeow +Datumized preferences (TGUI Prefs) + +See prefs_migartion_2023-07-26.sql + +----------------------------------------------------- + +----------------------------------------------------- + Version 6.2 - 16 June 2023 by itsmeow Add per-character role preferences. diff --git a/SQL/prefs_migration_2023-07-26.sql b/SQL/prefs_migration_2023-07-26.sql new file mode 100644 index 0000000000000..b8b8c129fa6b9 --- /dev/null +++ b/SQL/prefs_migration_2023-07-26.sql @@ -0,0 +1,426 @@ +/* + +Tested on MariaDB 10.11 + +DO NOT RUN WITHOUT TAKING A FULL BACKUP. +DO NOT RUN MORE THAN ONCE. +DO NOT RUN COLUMN REMOVALS UNTIL DATA IS VERIFIED. +- Allows nulls for `SS13_characters` columns (required for partial INSERT INTO ON DUPLICATE KEY UPDATE) +- Alters some datatypes to fit new data. +- Turns `SS13_characters`.`features` and `SS13_characters`.`custom_names` JSON objects into individual columns, removes the original columns. +- Updates `SS13_preferences`.`preference_tag` to text. +- Updates various `SS13_preferences` values to new ones. +- Flips `SS13_preferences`.`key_bindings` JSON structure. +*/ + +/* CHARACTER PREFERENCES */ + +/* Add new columns and allow nulls on existing columns. Tweak a few datatypes to allow for new data. */ + +ALTER TABLE `SS13_characters` + MODIFY COLUMN `species` VARCHAR(32) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `real_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + ADD COLUMN IF NOT EXISTS `human_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `real_name`, + ADD COLUMN IF NOT EXISTS `mime_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `human_name`, + ADD COLUMN IF NOT EXISTS `clown_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `mime_name`, + ADD COLUMN IF NOT EXISTS `cyborg_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `clown_name`, + ADD COLUMN IF NOT EXISTS `ai_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `cyborg_name`, + ADD COLUMN IF NOT EXISTS `religion_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `ai_name`, + ADD COLUMN IF NOT EXISTS `deity_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `religion_name`, + ADD COLUMN IF NOT EXISTS `bible_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `deity_name`, + MODIFY COLUMN `name_is_always_random` TINYINT(1) NULL, + MODIFY COLUMN `body_is_always_random` TINYINT(1) NULL, + MODIFY COLUMN `gender` VARCHAR(16) COLLATE 'utf8mb4_general_ci' NULL, + ADD COLUMN IF NOT EXISTS `body_model` VARCHAR(16) COLLATE 'utf8mb4_general_ci' NULL AFTER `gender`, + ADD COLUMN IF NOT EXISTS `body_size` VARCHAR(16) COLLATE 'utf8mb4_general_ci' NULL AFTER `body_model`, + MODIFY COLUMN `age` TINYINT(3) UNSIGNED NULL, + MODIFY COLUMN `hair_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `gradient_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `facial_hair_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `eye_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `skin_tone` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `hair_style_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `gradient_style` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `facial_style_name` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `underwear` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `underwear_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `undershirt` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `socks` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `backbag` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `jumpsuit_style` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `uplink_loc` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + ADD COLUMN IF NOT EXISTS `pda_theme` VARCHAR(32) COLLATE 'utf8mb4_general_ci' NULL AFTER `uplink_loc`, + ADD COLUMN IF NOT EXISTS `pda_classic_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci' NULL AFTER `pda_theme`, + ADD COLUMN IF NOT EXISTS `feature_apid_stripes` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `pda_classic_color`, + ADD COLUMN IF NOT EXISTS `feature_apid_antenna` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_apid_stripes`, + ADD COLUMN IF NOT EXISTS `feature_apid_headstripes` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_apid_antenna`, + ADD COLUMN IF NOT EXISTS `feature_moth_antennae` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_apid_headstripes`, + ADD COLUMN IF NOT EXISTS `feature_moth_markings` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_moth_antennae`, + ADD COLUMN IF NOT EXISTS `feature_moth_wings` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_moth_markings`, + ADD COLUMN IF NOT EXISTS `feature_ethcolor` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_moth_wings`, + ADD COLUMN IF NOT EXISTS `feature_insect_type` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_ethcolor`, + ADD COLUMN IF NOT EXISTS `feature_ipc_screen` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_insect_type`, + ADD COLUMN IF NOT EXISTS `feature_ipc_antenna` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_ipc_screen`, + ADD COLUMN IF NOT EXISTS `feature_ipc_chassis` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_ipc_antenna`, + ADD COLUMN IF NOT EXISTS `feature_ipc_screen_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_ipc_chassis`, + ADD COLUMN IF NOT EXISTS `feature_ipc_antenna_color` VARCHAR(8) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_ipc_screen_color`, + ADD COLUMN IF NOT EXISTS `feature_lizard_body_markings` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_ipc_antenna_color`, + ADD COLUMN IF NOT EXISTS `feature_lizard_frills` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_lizard_body_markings`, + ADD COLUMN IF NOT EXISTS `feature_lizard_horns` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_lizard_frills`, + ADD COLUMN IF NOT EXISTS `feature_lizard_legs` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_lizard_horns`, + ADD COLUMN IF NOT EXISTS `feature_lizard_snout` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_lizard_legs`, + ADD COLUMN IF NOT EXISTS `feature_lizard_spines` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_lizard_snout`, + ADD COLUMN IF NOT EXISTS `feature_lizard_tail` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_lizard_spines`, + ADD COLUMN IF NOT EXISTS `feature_mcolor` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_lizard_tail`, + ADD COLUMN IF NOT EXISTS `feature_human_tail` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_mcolor`, + ADD COLUMN IF NOT EXISTS `feature_human_ears` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_human_tail`, + ADD COLUMN IF NOT EXISTS `feature_psyphoza_cap` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL AFTER `feature_human_ears`, + MODIFY COLUMN `helmet_style` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `preferred_ai_core_display` VARCHAR(64) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `preferred_security_department` VARCHAR(32) COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `joblessrole` TINYINT(4) UNSIGNED NULL, + MODIFY COLUMN `job_preferences` MEDIUMTEXT COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `all_quirks` MEDIUMTEXT COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `equipped_gear` MEDIUMTEXT COLLATE 'utf8mb4_general_ci' NULL, + MODIFY COLUMN `role_preferences` MEDIUMTEXT COLLATE 'utf8mb4_general_ci' NULL, + ADD COLUMN IF NOT EXISTS `randomise` MEDIUMTEXT COLLATE 'utf8mb4_general_ci' NULL AFTER `role_preferences`; + +/* Copy eye colors onto IPC screen color, now that it's separate */ +UPDATE `SS13_characters` SET `feature_ipc_screen` = `eye_color`; + +/* Flatten features JSON into its own columns */ + +UPDATE SS13_characters t1, JSON_TABLE( + t1.features, '$' COLUMNS( + body_size_old TEXT PATH '$.body_size', + mcolor_old TEXT PATH '$.mcolor', + ethcolor_old TEXT PATH '$.ethcolor', + tail_lizard_old TEXT PATH '$.lizard_tail', + snout_old TEXT PATH '$.snout', + horns_old TEXT PATH '$.horns', + frills_old TEXT PATH '$.frills', + spines_old TEXT PATH '$.spines', + body_markings_old TEXT PATH '$.body_markings', + moth_wings_old TEXT PATH '$.moth_wings', + ipc_screen_old TEXT PATH '$.ipc_screen', + ipc_antenna_old TEXT PATH '$.ipc_antenna', + ipc_chassis_old TEXT PATH '$.ipc_chassis', + insect_type_old TEXT PATH '$.insect_type', + tail_human_old TEXT PATH '$.tail_human', + ears_old TEXT PATH '$.ears', + body_model_old TEXT PATH '$.body_model', + feature_lizard_legs_old TEXT PATH '$.feature_lizard_legs', + moth_antennae_old TEXT PATH '$.moth_antennae', + moth_markings_old TEXT PATH '$.moth_markings', + apid_antenna_old TEXT PATH '$.apid_antenna', + apid_stripes_old TEXT PATH '$.apid_stripes', + apid_headstripes_old TEXT PATH '$.apid_headstripes' + ) +) AS jt +SET body_size = jt.body_size_old, + feature_mcolor = jt.mcolor_old, + feature_ethcolor = jt.ethcolor_old, + feature_lizard_tail = jt.tail_lizard_old, + feature_lizard_snout = jt.snout_old, + feature_lizard_horns = jt.horns_old, + feature_lizard_frills = jt.frills_old, + feature_lizard_spines = jt.spines_old, + feature_lizard_body_markings = jt.body_markings_old, + feature_moth_wings = jt.moth_wings_old, + feature_ipc_screen = jt.ipc_screen_old, + feature_ipc_antenna = jt.ipc_antenna_old, + feature_ipc_chassis = jt.ipc_chassis_old, + feature_insect_type = jt.insect_type_old, + feature_human_tail = jt.tail_human_old, + feature_human_ears = jt.ears_old, + body_model = jt.body_model_old, + feature_lizard_legs = jt.feature_lizard_legs_old, + feature_moth_antennae = jt.moth_antennae_old, + feature_moth_markings = jt.moth_markings_old, + feature_apid_antenna = jt.apid_antenna_old, + feature_apid_stripes = jt.apid_stripes_old, + feature_apid_headstripes = jt.apid_headstripes_old; + +/* Flatten custom_names JSON into its own columns */ + +UPDATE SS13_characters t1, JSON_TABLE( + t1.custom_names, '$' COLUMNS( + human_name_old TEXT PATH '$.human', + mime_name_old TEXT PATH '$.mime', + clown_name_old TEXT PATH '$.clown', + cyborg_name_old TEXT PATH '$.cyborg', + ai_name_old TEXT PATH '$.ai', + religion_name_old TEXT PATH '$.religion', + deity_name_old TEXT PATH '$.deity' + ) +) AS jt +SET human_name = jt.human_name_old, + mime_name = jt.mime_name_old, + clown_name = jt.clown_name_old, + cyborg_name = jt.cyborg_name_old, + ai_name = jt.ai_name_old, + religion_name = jt.religion_name_old, + deity_name = jt.deity_name_old; + +/* Delete unused data (features and custom_names, which are now flattened) */ + +ALTER TABLE `SS13_characters` DROP COLUMN IF EXISTS `features`; +ALTER TABLE `SS13_characters` DROP COLUMN IF EXISTS `custom_names`; + +/* PLAYER PREFERENCES */ + +/* Convert tags to strings */ + +ALTER TABLE `SS13_preferences` MODIFY COLUMN `preference_tag` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_general_ci'; + +UPDATE `SS13_preferences` + SET `preference_tag` = CASE + WHEN `preference_tag` = '5' THEN 'last_changelog' + WHEN `preference_tag` = '6' THEN 'ui_style' + WHEN `preference_tag` = '7' THEN 'outline_color' + WHEN `preference_tag` = '8' THEN 'show_balloon_alerts' + WHEN `preference_tag` = '9' THEN 'default_slot' + WHEN `preference_tag` = '11' THEN 'ghost_form' + WHEN `preference_tag` = '12' THEN 'ghost_orbit' + WHEN `preference_tag` = '13' THEN 'ghost_accs' + WHEN `preference_tag` = '14' THEN 'ghost_others' + WHEN `preference_tag` = '15' THEN 'preferred_map' + WHEN `preference_tag` = '16' THEN 'ignoring' + WHEN `preference_tag` = '17' THEN 'clientfps' + WHEN `preference_tag` = '18' THEN 'parallax' + WHEN `preference_tag` = '19' THEN 'pixel_size' + WHEN `preference_tag` = '20' THEN 'scaling_method' + WHEN `preference_tag` = '21' THEN 'tip_delay' + WHEN `preference_tag` = '24' THEN 'key_bindings' + WHEN `preference_tag` = '25' THEN 'purchased_gear' + WHEN `preference_tag` = '26' THEN 'be_special' + WHEN `preference_tag` = '27' THEN 'pai_name' + WHEN `preference_tag` = '28' THEN 'pai_description' + WHEN `preference_tag` = '29' THEN 'pai_comment' + ELSE `preference_tag` + END; + +/* Convert to new values */ + +START TRANSACTION; + +UPDATE `SS13_preferences` SET `preference_value` = CASE + WHEN `preference_value` = '1' THEN 'Default sprites' + WHEN `preference_value` = '50' THEN 'Only directional sprites' + WHEN `preference_value` = '100' THEN 'Full accessories' + ELSE `preference_value` + END WHERE `preference_tag` = 'ghost_accs'; + +UPDATE `SS13_preferences` SET `preference_value` = CASE + WHEN `preference_value` = '1' THEN 'White ghosts' + WHEN `preference_value` = '50' THEN 'Default sprites' + WHEN `preference_value` = '100' THEN 'Their sprites' + ELSE `preference_value` + END WHERE `preference_tag` = 'ghost_others'; + +UPDATE `SS13_preferences` SET `preference_value` = '-1' WHERE `preference_tag` = 'clientfps' AND `preference_value` = '40'; + +UPDATE `SS13_preferences` SET `preference_value` = CASE + WHEN `preference_value` = '-1' THEN 'Insane' + WHEN `preference_value` = '0' THEN 'High' + WHEN `preference_value` = '1' THEN 'Medium' + WHEN `preference_value` = '2' THEN 'Low' + WHEN `preference_value` = '3' THEN 'Disabled' + ELSE `preference_value` + END WHERE `preference_tag` = 'parallax'; + +/* Finish value conversions */ + +COMMIT; + +/* Convert toggles */ + +START TRANSACTION; + +/* toggles 1 */ + +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_adminhelp',IF(`preference_value` & (1<<0) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_midi',IF(`preference_value` & (1<<1) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_ambience',IF(`preference_value` & (1<<2) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_lobby',IF(`preference_value` & (1<<3) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'member_public',IF(`preference_value` & (1<<4) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'intent_style',IF(`preference_value` & (1<<5) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_instruments',IF(`preference_value` & (1<<7) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_ship_ambience',IF(`preference_value` & (1<<8) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_prayers',IF(`preference_value` & (1<<9) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'announce_login',IF(`preference_value` & (1<<10) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_announcements',IF(`preference_value` & (1<<11) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'death_rattle',IF(`preference_value` & (1<<12) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'arrivals_rattle',IF(`preference_value` & (1<<13) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'combohud_lighting',IF(`preference_value` & (1<<14) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'deadmin_always',IF(`preference_value` & (1<<15) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'deadmin_antagonist',IF(`preference_value` & (1<<16) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'deadmin_position_head',IF(`preference_value` & (1<<17) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'deadmin_position_security',IF(`preference_value` & (1<<18) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'deadmin_position_silicon',IF(`preference_value` & (1<<19) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'itemoutline_pref',IF(`preference_value` & (1<<20) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_on_map',IF(`preference_value` & (1<<21) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'see_chat_non_mob',IF(`preference_value` & (1<<22) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'see_rc_emotes',IF(`preference_value` & (1<<23) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '1' +); + +/* toggles 2 */ + +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'tgui_fancy',IF(`preference_value` & (1<<0) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'tgui_lock',IF(`preference_value` & (1<<1) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'buttons_locked',IF(`preference_value` & (1<<2) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'windowflashing',IF(`preference_value` & (1<<3) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'crew_objectives',IF(`preference_value` & (1<<4) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'ghost_hud',IF(`preference_value` & (1<<5) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'inquisitive_ghost',IF(`preference_value` & (1<<6) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'glasses_color',IF(`preference_value` & (1<<7) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'ambientocclusion',IF(`preference_value` & (1<<8) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'auto_fit_viewport',IF(`preference_value` & (1<<9) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'enable_tips',IF(`preference_value` & (1<<10) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'show_credits',IF(`preference_value` & (1<<11) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'hotkeys',IF(`preference_value` & (1<<12) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_soundtrack',IF(`preference_value` & (1<<13) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'tgui_input',IF(`preference_value` & (1<<14) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'tgui_input_large',IF(`preference_value` & (1<<15) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'tgui_input_swapped',IF(`preference_value` & (1<<16) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'tgui_say',IF(`preference_value` & (1<<17) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'tgui_say_light_mode',IF(`preference_value` & (1<<18) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'tgui_say_show_prefix',IF(`preference_value` & (1<<19) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'sound_adminalert',IF(`preference_value` & (1<<20) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '2' +); + +/* chat toggles */ + +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_ooc',IF(`preference_value` & (1<<0) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_dead',IF(`preference_value` & (1<<1) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_ghostears',IF(`preference_value` & (1<<2) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_ghostsight',IF(`preference_value` & (1<<3) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_prayer',IF(`preference_value` & (1<<4) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_radio',IF(`preference_value` & (1<<5) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_pullr',IF(`preference_value` & (1<<6) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_ghostwhisper',IF(`preference_value` & (1<<7) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_ghostpda',IF(`preference_value` & (1<<8) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_ghostradio',IF(`preference_value` & (1<<9) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_bankcard',IF(`preference_value` & (1<<10) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_ghostlaws',IF(`preference_value` & (1<<11) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); +INSERT IGNORE INTO `SS13_preferences` (`ckey`, `preference_tag`, `preference_value`) ( + SELECT `ckey`,'chat_ghostfollowmindless',IF(`preference_value` & (1<<12) > 0, 1, 0) AS `preference_value` FROM `SS13_preferences` WHERE `preference_tag` = '10' +); + +/* Finish toggle conversions */ + +COMMIT; + +/* Delete unused data (toggles and old PDA preferences, moved to character) */ + +DELETE FROM `SS13_preferences` WHERE `preference_tag` IN ('1', '2', '10', '22', '23'); diff --git a/beestation.dme b/beestation.dme index 92fd630ce99c9..97e4bbe95dc29 100644 --- a/beestation.dme +++ b/beestation.dme @@ -107,7 +107,6 @@ #include "code\__DEFINES\MC.dm" #include "code\__DEFINES\mecha.dm" #include "code\__DEFINES\melee.dm" -#include "code\__DEFINES\menu.dm" #include "code\__DEFINES\metacoin.dm" #include "code\__DEFINES\mobs.dm" #include "code\__DEFINES\monkeys.dm" @@ -227,6 +226,7 @@ #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\atoms.dm" #include "code\__HELPERS\bitflag_list.dm" @@ -394,6 +394,7 @@ #include "code\controllers\subsystem\dbcore.dm" #include "code\controllers\subsystem\dcs.dm" #include "code\controllers\subsystem\disease.dm" +#include "code\controllers\subsystem\early_assets.dm" #include "code\controllers\subsystem\economy.dm" #include "code\controllers\subsystem\enumeration.dm" #include "code\controllers\subsystem\events.dm" @@ -423,6 +424,7 @@ #include "code\controllers\subsystem\parallax.dm" #include "code\controllers\subsystem\pathfinder.dm" #include "code\controllers\subsystem\persistence.dm" +#include "code\controllers\subsystem\preferences.dm" #include "code\controllers\subsystem\profiler.dm" #include "code\controllers\subsystem\radiation.dm" #include "code\controllers\subsystem\radio.dm" @@ -1999,11 +2001,7 @@ #include "code\modules\antagonists\revolution\revolution.dm" #include "code\modules\antagonists\role_preference\_role_preference.dm" #include "code\modules\antagonists\role_preference\role_antagonists.dm" -#include "code\modules\antagonists\role_preference\role_changeling.dm" #include "code\modules\antagonists\role_preference\role_midrounds.dm" -#include "code\modules\antagonists\role_preference\role_operative.dm" -#include "code\modules\antagonists\role_preference\role_traitor.dm" -#include "code\modules\antagonists\role_preference\role_wizard.dm" #include "code\modules\antagonists\roundstart_special\special_antagonist.dm" #include "code\modules\antagonists\roundstart_special\undercover\undercover.dm" #include "code\modules\antagonists\santa\santa.dm" @@ -2198,8 +2196,6 @@ #include "code\modules\client\helpers.dm" #include "code\modules\client\message.dm" #include "code\modules\client\player_details.dm" -#include "code\modules\client\preferences.dm" -#include "code\modules\client\preferences_toggles.dm" #include "code\modules\client\loadout\loadout.dm" #include "code\modules\client\loadout\loadout_accessories.dm" #include "code\modules\client\loadout\loadout_colorizers.dm" @@ -2210,9 +2206,78 @@ #include "code\modules\client\loadout\loadout_ooc.dm" #include "code\modules\client\loadout\loadout_suit.dm" #include "code\modules\client\loadout\loadout_uniform.dm" -#include "code\modules\client\preferences2\character_save.dm" -#include "code\modules\client\preferences2\preferences2.dm" -#include "code\modules\client\verbs\etips.dm" +#include "code\modules\client\preferences\preference_entry.dm" +#include "code\modules\client\preferences\preference_verbs.dm" +#include "code\modules\client\preferences\preference_verbs_toggles.dm" +#include "code\modules\client\preferences\preferences.dm" +#include "code\modules\client\preferences\entries\character\age.dm" +#include "code\modules\client\preferences\entries\character\ai_core_display.dm" +#include "code\modules\client\preferences\entries\character\body_model.dm" +#include "code\modules\client\preferences\entries\character\clothing.dm" +#include "code\modules\client\preferences\entries\character\gender.dm" +#include "code\modules\client\preferences\entries\character\names.dm" +#include "code\modules\client\preferences\entries\character\pda.dm" +#include "code\modules\client\preferences\entries\character\random.dm" +#include "code\modules\client\preferences\entries\character\security_department.dm" +#include "code\modules\client\preferences\entries\character\skin_tone.dm" +#include "code\modules\client\preferences\entries\character\species.dm" +#include "code\modules\client\preferences\entries\character\underwear_color.dm" +#include "code\modules\client\preferences\entries\character\uplink_location.dm" +#include "code\modules\client\preferences\entries\character\species_features\apid.dm" +#include "code\modules\client\preferences\entries\character\species_features\basic.dm" +#include "code\modules\client\preferences\entries\character\species_features\ethereal.dm" +#include "code\modules\client\preferences\entries\character\species_features\felinid.dm" +#include "code\modules\client\preferences\entries\character\species_features\fly.dm" +#include "code\modules\client\preferences\entries\character\species_features\ipc.dm" +#include "code\modules\client\preferences\entries\character\species_features\lizard.dm" +#include "code\modules\client\preferences\entries\character\species_features\moth.dm" +#include "code\modules\client\preferences\entries\character\species_features\mutants.dm" +#include "code\modules\client\preferences\entries\character\species_features\plasmaman.dm" +#include "code\modules\client\preferences\entries\character\species_features\psyphoza.dm" +#include "code\modules\client\preferences\entries\player\admin.dm" +#include "code\modules\client\preferences\entries\player\ambient_occlusion.dm" +#include "code\modules\client\preferences\entries\player\auto_fit_viewport.dm" +#include "code\modules\client\preferences\entries\player\buttons_locked.dm" +#include "code\modules\client\preferences\entries\player\chat.dm" +#include "code\modules\client\preferences\entries\player\crew_objectives.dm" +#include "code\modules\client\preferences\entries\player\deadmin.dm" +#include "code\modules\client\preferences\entries\player\fps.dm" +#include "code\modules\client\preferences\entries\player\ghost.dm" +#include "code\modules\client\preferences\entries\player\glasses.dm" +#include "code\modules\client\preferences\entries\player\hotkeys.dm" +#include "code\modules\client\preferences\entries\player\item_outlines.dm" +#include "code\modules\client\preferences\entries\player\jobless_role.dm" +#include "code\modules\client\preferences\entries\player\ooc.dm" +#include "code\modules\client\preferences\entries\player\parallax.dm" +#include "code\modules\client\preferences\entries\player\pixel_size.dm" +#include "code\modules\client\preferences\entries\player\preferred_map.dm" +#include "code\modules\client\preferences\entries\player\rattle.dm" +#include "code\modules\client\preferences\entries\player\roundend.dm" +#include "code\modules\client\preferences\entries\player\runechat.dm" +#include "code\modules\client\preferences\entries\player\scaling_method.dm" +#include "code\modules\client\preferences\entries\player\sound.dm" +#include "code\modules\client\preferences\entries\player\tgui.dm" +#include "code\modules\client\preferences\entries\player\tooltips.dm" +#include "code\modules\client\preferences\entries\player\ui_style.dm" +#include "code\modules\client\preferences\entries\player\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\loadout.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\serialization\preferences_character.dm" +#include "code\modules\client\preferences\serialization\preferences_database.dm" +#include "code\modules\client\preferences\serialization\preferences_player.dm" +#include "code\modules\client\preferences\submodules\preference_assets.dm" +#include "code\modules\client\preferences\submodules\preference_character_preview.dm" +#include "code\modules\client\preferences\submodules\preference_jobs.dm" +#include "code\modules\client\preferences\submodules\preference_keybindings.dm" +#include "code\modules\client\preferences\submodules\preference_loadouts.dm" +#include "code\modules\client\preferences\submodules\preference_randomization.dm" #include "code\modules\client\verbs\input_box.dm" #include "code\modules\client\verbs\looc.dm" #include "code\modules\client\verbs\ooc.dm" diff --git a/code/__DEFINES/DNA.dm b/code/__DEFINES/DNA.dm index baeb8bf19b762..0b10ac00b07ca 100644 --- a/code/__DEFINES/DNA.dm +++ b/code/__DEFINES/DNA.dm @@ -115,21 +115,21 @@ #define LIPS 5 #define NOBLOOD 6 #define NOTRANSSTING 7 -#define MUTCOLORS_PARTSONLY 8 //! 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 NOZOMBIE 9 -#define NO_UNDERWEAR 10 -#define NOLIVER 11 -#define NOSTOMACH 12 -#define NO_DNA_COPY 13 -#define NOFLASH 14 -#define DYNCOLORS 15 //! 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 TRANSLATION: AWFUL. -#define AGENDER 16 -#define NOEYESPRITES 17 //! Do not draw eyes or eyeless overlay -#define NOREAGENTS 18 //! DO NOT PROCESS REAGENTS -#define REVIVESBYHEALING 19 // Will revive on heal when healing and total HP > 0. -#define NOHUSK 20 // Can't be husked. -#define NOMOUTH 21 -#define NOSOCKS 22 // You cannot wear socks. +#define NOZOMBIE 8 +#define NO_UNDERWEAR 9 +#define NOLIVER 10 +#define NOSTOMACH 11 +#define NO_DNA_COPY 12 +#define NOFLASH 13 +#define DYNCOLORS 14 //! 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 TRANSLATION: AWFUL. +#define AGENDER 15 +#define NOEYESPRITES 16 //! Do not draw eyes or eyeless overlay +#define NOREAGENTS 17 //! DO NOT PROCESS REAGENTS +#define REVIVESBYHEALING 18 // Will revive on heal when healing and total HP > 0. +#define NOHUSK 19 // Can't be husked. +#define NOMOUTH 20 +#define NOSOCKS 21 // You cannot wear socks. +#define ENVIROSUIT 22 //! spawns with an envirosuit /// 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) diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm index 55b1adbeed3e3..0648788f4611c 100644 --- a/code/__DEFINES/admin.dm +++ b/code/__DEFINES/admin.dm @@ -85,3 +85,8 @@ GLOBAL_VAR_INIT(ghost_role_flags, (~0)) #define GHOSTROLE_SILICONS (1<<3) //ie mafia, ctf #define GHOSTROLE_MINIGAME (1<<4) + +// Job deadmin flags +#define DEADMIN_POSITION_HEAD (1<<0) +#define DEADMIN_POSITION_SECURITY (1<<1) +#define DEADMIN_POSITION_SILICON (1<<2) diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 87bbbcdce5d94..1a25d8f3e6a68 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -133,3 +133,6 @@ //Spider webs #define MAX_WEBS_PER_TILE 3 + +/// 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 e6f6c585dc935..33bb92d0289f8 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -37,6 +37,7 @@ #define COLOR_YELLOW "#FFFF00" #define COLOR_OLIVE "#808000" #define COLOR_LIME "#32CD32" +#define COLOR_VIBRANT_LIME "#00FF00" #define COLOR_GREEN "#008000" #define COLOR_CYAN "#00FFFF" #define COLOR_TEAL "#008080" @@ -47,6 +48,8 @@ #define COLOR_FADED_PINK "#ff80d5" #define COLOR_MAGENTA "#FF00FF" #define COLOR_PURPLE "#800080" +#define COLOR_VIOLET "#B900F7" +#define COLOR_STRONG_VIOLET "#6927C5" #define COLOR_ORANGE "#FF9900" #define COLOR_LIGHT_ORANGE "#ffc44d" #define COLOR_BEIGE "#CEB689" @@ -58,7 +61,7 @@ #define COLOR_RED_GRAY "#B4696A" #define COLOR_PALE_BLUE_GRAY "#98C5DF" #define COLOR_PALE_GREEN_GRAY "#B7D993" -#define COLOR_PALE_ORANGE "#FFBE9D" +#define COLOR_PALE_ORANGE "#FFBE9D" #define COLOR_PALE_RED_GRAY "#D59998" #define COLOR_PALE_PURPLE_GRAY "#CBB1CA" #define COLOR_PURPLE_GRAY "#AE8CA8" @@ -98,6 +101,11 @@ GLOBAL_LIST_INIT(color_list_blood_brothers, shuffle(list( /// Icon filter that creates gaussian blur #define GAUSSIAN_BLUR(filter_size) filter(type="blur", size=filter_size) +/// The default color for admin say, used as a fallback when the preference is not enabled +#define DEFAULT_ASAY_COLOR "#FF4500" +/// The default color for Byond Member / ADMIN OOC, used as a fallback when the preference is not enabled +#define DEFAULT_BONUS_OOC_COLOR "#c43b23" + // Some defines for accessing specific entries in color matrices. #define CL_MATRIX_RR 1 diff --git a/code/__DEFINES/devices.dm b/code/__DEFINES/devices.dm index fb15b2ffef8f6..f8840ee242360 100644 --- a/code/__DEFINES/devices.dm +++ b/code/__DEFINES/devices.dm @@ -68,32 +68,6 @@ GLOBAL_LIST_INIT(ntos_device_themes_default, list( "Retro" = THEME_RETRO )) -// I hate BYOND lists. just let me reverse the map please -/// Sanitization list for the database, allowed roundstart theme IDs -GLOBAL_LIST_INIT(ntos_device_themes_default_content, list( - THEME_NTOS, - THEME_THINKTRONIC, - THEME_NTOS_LIGHT, - THEME_NTOS_DARK, - THEME_NTOS_RED, - THEME_NTOS_ORANGE, - THEME_NTOS_YELLOW, - THEME_NTOS_OLIVE, - THEME_NTOS_GREEN, - THEME_NTOS_TEAL, - THEME_NTOS_BLUE, - THEME_NTOS_VIOLET, - THEME_NTOS_PURPLE, - THEME_NTOS_PINK, - THEME_NTOS_BROWN, - THEME_NTOS_GREY, - THEME_NTOS_CLOWN_PINK, - THEME_NTOS_CLOWN_YELLOW, - THEME_NTOS_HACKERMAN, - THEME_HACKERMAN, - THEME_RETRO -)) - GLOBAL_LIST_INIT(ntos_device_themes_emagged, list( "Syndix" = THEME_SYNDICATE ) + GLOB.ntos_device_themes_default) diff --git a/code/__DEFINES/food.dm b/code/__DEFINES/food.dm index 5b44a718a0efb..95b6229e0654a 100644 --- a/code/__DEFINES/food.dm +++ b/code/__DEFINES/food.dm @@ -19,11 +19,31 @@ #define BUGS (1<<18)*/ #define GORE (1<<19) -#define DRINK_BAD 1 -#define DRINK_NICE 2 -#define DRINK_GOOD 3 -#define DRINK_VERYGOOD 4 -#define DRINK_FANTASTIC 5 +/// 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", \ + "GORE", \ +) + +#define DRINK_BAD 1 +#define DRINK_NICE 2 +#define DRINK_GOOD 3 +#define DRINK_VERYGOOD 4 +#define DRINK_FANTASTIC 5 /// Food is "in a container", not in a code sense, but in a literal sense (canned foods) #define FOOD_IN_CONTAINER (1<<0) diff --git a/code/__DEFINES/ghost.dm b/code/__DEFINES/ghost.dm index 0b5826bccab04..81989454547fd 100644 --- a/code/__DEFINES/ghost.dm +++ b/code/__DEFINES/ghost.dm @@ -10,21 +10,13 @@ /// Ghosts will orbit objects in a pentagon #define GHOST_ORBIT_PENTAGON "pentagon" -///////// Ghost showing preferences SQL values: ///////// -/// The main player's ghost will display as a simple white ghost -#define GHOST_ACCS_NONE 1 -/// The main player's ghost will display as a transparent mob -#define GHOST_ACCS_DIR 50 -/// The main player's ghost will display as a transparent mob with clothing -#define GHOST_ACCS_FULL 100 - ///////// Ghost showing preferences ///////// /// The main player's ghost will display as a simple white ghost -#define GHOST_ACCS_NONE_NAME "Default sprites" +#define GHOST_ACCS_NONE "Default sprites" /// The main player's ghost will display as a transparent mob -#define GHOST_ACCS_DIR_NAME "Only directional sprites" +#define GHOST_ACCS_DIR "Only directional sprites" /// The main player's ghost will display as a transparent mob with clothing -#define GHOST_ACCS_FULL_NAME "Full accessories" +#define GHOST_ACCS_FULL "Full accessories" /// The default ghost display selection for the main player #define GHOST_ACCS_DEFAULT_OPTION GHOST_ACCS_FULL @@ -34,26 +26,15 @@ GLOBAL_LIST_INIT(ghost_accs_options, list(GHOST_ACCS_NONE, GHOST_ACCS_DIR, GHOST ///////// Ghost viewing others preferences ///////// /// The other players ghosts will display as a simple white ghost -#define GHOST_OTHERS_SIMPLE 1 -/// The other players ghosts will display as transparent mobs -#define GHOST_OTHERS_DEFAULT_SPRITE 50 -/// The other players ghosts will display as transparent mobs with clothing -#define GHOST_OTHERS_THEIR_SETTING 100 - -///////// Ghost viewing others preferences human-readable: ///////// -/// The other players ghosts will display as a simple white ghost -#define GHOST_OTHERS_SIMPLE_NAME "white ghost" +#define GHOST_OTHERS_SIMPLE "White ghosts" /// The other players ghosts will display as transparent mobs -#define GHOST_OTHERS_DEFAULT_SPRITE_NAME "default sprites" +#define GHOST_OTHERS_DEFAULT_SPRITE "Default sprites" /// The other players ghosts will display as transparent mobs with clothing -#define GHOST_OTHERS_THEIR_SETTING_NAME "their setting" +#define GHOST_OTHERS_THEIR_SETTING "Their sprites" /// The default ghost view others for the main player #define GHOST_OTHERS_DEFAULT_OPTION GHOST_OTHERS_THEIR_SETTING -GLOBAL_LIST_INIT(ghost_others_options, list(GHOST_OTHERS_SIMPLE, GHOST_OTHERS_DEFAULT_SPRITE, GHOST_OTHERS_THEIR_SETTING)) //Same as ghost_accs_options. - - // DEADCHAT MESSAGE TYPES // /// Deadchat notification for new players who join the round at arrivals #define DEADCHAT_ARRIVALRATTLE "arrivalrattle" @@ -76,3 +57,32 @@ GLOBAL_LIST_INIT(ghost_others_options, list(GHOST_OTHERS_SIMPLE, GHOST_OTHERS_DE /// Pictures taken by a camera will display ghosts and their orbits #define CAMERA_SEE_GHOSTS_ORBIT 2 // this doesn't do anything right now as of Mar 2023 +GLOBAL_LIST_INIT(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", +)) diff --git a/code/__DEFINES/jobs.dm b/code/__DEFINES/jobs.dm index 35051cddd5efc..281ad8ab25e3a 100644 --- a/code/__DEFINES/jobs.dm +++ b/code/__DEFINES/jobs.dm @@ -60,6 +60,7 @@ #define DEFAULT_RELIGION "Christianity" #define DEFAULT_DEITY "Space Jesus" +#define DEFAULT_BIBLE "The Bible" #define JOB_DISPLAY_ORDER_DEFAULT 0 @@ -110,6 +111,25 @@ #define DEPT_BITFLAG_SEC (1<<7) #define DEPT_BITFLAG_VIP (1<<8) #define DEPT_BITFLAG_SILICON (1<<9) +#define DEPT_BITFLAG_CAPTAIN (1<<10) +#define DEPT_BITFLAG_ASSISTANT (1<<11) + +/// For use in the preferences menu. +GLOBAL_LIST_INIT(dept_bitflag_to_name, list( + "[DEPT_BITFLAG_COM]" = "Command", + "[DEPT_BITFLAG_CIV]" = "Civilian", + "[DEPT_BITFLAG_SRV]" = "Service", + "[DEPT_BITFLAG_CAR]" = "Cargo", + "[DEPT_BITFLAG_SCI]" = "Science", + "[DEPT_BITFLAG_ENG]" = "Engineering", + "[DEPT_BITFLAG_MED]" = "Medical", + "[DEPT_BITFLAG_SEC]" = "Security", + "[DEPT_BITFLAG_VIP]" = "Very Important People", + "[DEPT_BITFLAG_SILICON]" = "Silicon", + "[DEPT_BITFLAG_CAPTAIN]" = "Captain", + "[DEPT_BITFLAG_ASSISTANT]" = "Assistant" +)) + // should check the ones in `\_DEFINES\economy.dm` // It's true that bitflags shouldn't be separated in two DEFINES if these are same, but just in case the system can be devided, it's remained separated. diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 653b4ce5a8691..51e2eb321612c 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -19,6 +19,8 @@ #define ZMIMIC_MAX_DEPTH 10 #define FLOOR_PLANE -7 +#define WALL_PLANE -5 +#define WALL_PLANE_RENDER_TARGET "*WALL_PLANE_RENDER_TARGET" #define GAME_PLANE -4 #define SPACE_LAYER 1.8 @@ -156,10 +158,6 @@ //Plane for highlighting objects #define PSYCHIC_PLANE 550 #define PSYCHIC_PLANE_RENDER_TARGET "PSYCHIC_PLANE" -//Plane for highlighting walls -#define PSYCHIC_WALL_PLANE 551 -#define PSYCHIC_WALL_PLANE_RENDER_TARGET "PSYCHIC_WALL_PLANE" - //-------------------- Rendering --------------------- #define RENDER_PLANE_GAME 990 diff --git a/code/__DEFINES/menu.dm b/code/__DEFINES/menu.dm deleted file mode 100644 index 60a7a2379c1e5..0000000000000 --- 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/mobs.dm b/code/__DEFINES/mobs.dm index b2dbeb2ae07b2..63efa7e1f4533 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -86,82 +86,6 @@ #define GIB_TYPE_HUMAN "human" #define GIB_TYPE_ROBOTIC "robotic" -//Defines for Species IDs -#define SPECIES_ABDUCTOR "abductor" -#define SPECIES_ANDROID "android" -#define SPECIES_APID "apid" -#define SPECIES_DEBUG "debug" -#define SPECIES_DULLAHAN "dullahan" -#define SPECIES_ETHEREAL "ethereal" -#define SPECIES_FELINID "felinid" -#define SPECIES_FLY "fly" -#define SPECIES_HUMAN "human" -#define SPECIES_IPC "ipc" -#define SPECIES_LIZARD "lizard" - #define SPECIES_ASHWALKER "ashlizard" -#define SPECIES_MONKEY "monkey" -#define SPECIES_MOTH "moth" -#define SPECIES_OOZELING "oozeling" - #define SPECIES_LUMINESCENT "lum" - #define SPECIES_SLIMEPERSON "slime" - #define SPECIES_STARGAZER "stargazer" -#define SPECIES_PLASMAMAN "plasmaman" -#define SPECIES_PODPERSON "pod" -#define SPECIES_PUMPKINPERSON "pumpkin_man" -#define SPECIES_SHADOWPERSON "shadow" -#define SPECIES_SKELETON "skeleton" -#define SPECIES_SNAILPERSON "snail" -#define SPECIES_SUPERSOILDER "supersoldier" -#define SPECIES_VAMPIRE "vampire" -#define SPECIES_PSYPHOZA "psyphoza" - -//Defines for Golem Species IDs -#define SPECIES_GOLEM_ADAMANTINE "adamantine_golem" -#define SPECIES_GOLEM_ALLOY "alloy_golem" -#define SPECIES_GOLEM_BANANIUM "bananium_golem" -#define SPECIES_GOLEM_BLUESPACE "bluespace_golem" -#define SPECIES_GOLEM_BONE "bone_golem" -#define SPECIES_GOLEM_BRONZE "bronze_golem" -#define SPECIES_GOLEM_CAPITALIST "capitalist_golem" -#define SPECIES_GOLEM_CARDBOARD "cardboard_golem" -#define SPECIES_GOLEM_CLOCKWORK "clockwork_golem" -#define SPECIES_GOLEM_CLOCKWORK_SERVANT "clockwork golem servant" -#define SPECIES_GOLEM_CLOTH "cloth_golem" -#define SPECIES_GOLEM_COPPER "copper_golem" -#define SPECIES_GOLEM_DIAMOND "diamond_golem" -#define SPECIES_GOLEM_DURATHREAD "durathread_golem" -#define SPECIES_GOLEM_GLASS "glass_golem" -#define SPECIES_GOLEM_GOLD "gold_golem" -#define SPECIES_GOLEM_IRON "iron_golem" -#define SPECIES_GOLEM_LEATHER "leather_golem" -#define SPECIES_GOLEM_PLASMA "plasma_golem" -#define SPECIES_GOLEM_PLASTEEL "plasteel_golem" -#define SPECIES_GOLEM_PLASTIC "plastic_golem" -#define SPECIES_GOLEM_PLASTITANIUM "plastitanium_golem" -#define SPECIES_GOLEM_RUNIC "cult_golem" -#define SPECIES_GOLEM_SAND "sand_golem" -#define SPECIES_GOLEM_SILVER "silver_golem" -#define SPECIES_GOLEM_SNOW "snow_golem" -#define SPECIES_GOLEM_SOVIET "soviet_golem" -#define SPECIES_GOLEM_TITANIUM "titanium_golem" -#define SPECIES_GOLEM_URANIUM "uranium_golem" -#define SPECIES_GOLEM_WOOD "wood_golem" - -//Species bitflags, used for species_restricted. If this somehow ever gets above 23 Bee has larger problems. -#define FLAG_HUMAN (1<<0) -#define FLAG_IPC (1<<1) -#define FLAG_ETHEREAL (1<<2) -#define FLAG_PLASMAMAN (1<<3) -#define FLAG_APID (1<<4) -#define FLAG_MOTH (1<<5) -#define FLAG_LIZARD (1<<6) -#define FLAG_FELINID (1<<7) -#define FLAG_OOZELING (1<<8) -#define FLAG_FLY (1<<9) -#define FLAG_DEBUG_SPECIES (1<<10) -#define FLAG_MONKEY (1<<11) -#define FLAG_PSYPHOZA (1<<12) - #define DIGITIGRADE_NEVER 0 #define DIGITIGRADE_OPTIONAL 1 #define DIGITIGRADE_FORCED 2 @@ -452,22 +376,6 @@ #define PULL_PRONE_SLOWDOWN 1.5 #define HUMAN_CARRY_SLOWDOWN 0.35 -//! ## control what things can spawn species -/// Badmin magic mirror -#define MIRROR_BADMIN (1<<0) -/// Standard magic mirror (wizard) -#define MIRROR_MAGIC (1<<1) -/// Pride ruin mirror -#define MIRROR_PRIDE (1<<2) -/// Race swap wizard event -#define RACE_SWAP (1<<3) -/// ERT spawn template (avoid races that don't function without correct gear) -#define ERT_SPAWN (1<<4) -/// xenobio black crossbreed -#define SLIME_EXTRACT (1<<5) -/// Wabbacjack staff projectiles -#define WABBAJACK (1<<6) - #define SLEEP_CHECK_DEATH(X) sleep(X); if(QDELETED(src) || stat == DEAD) return; #define INTERACTING_WITH(X, Y) (Y in X.do_afters) diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index 39681f836f33a..d45a4efc352cf 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -1,83 +1,10 @@ +// Preferences value defines -//Preference toggles -#define PREFTOGGLE_SOUND_ADMINHELP (1<<0) -#define PREFTOGGLE_SOUND_MIDI (1<<1) -#define PREFTOGGLE_SOUND_AMBIENCE (1<<2) -#define PREFTOGGLE_SOUND_LOBBY (1<<3) -#define PREFTOGGLE_MEMBER_PUBLIC (1<<4) -#define PREFTOGGLE_INTENT_STYLE (1<<5) -//#define PREFTOGGLE_MIDROUND_ANTAG (1<<6) -#define PREFTOGGLE_SOUND_INSTRUMENTS (1<<7) -#define PREFTOGGLE_SOUND_SHIP_AMBIENCE (1<<8) -#define PREFTOGGLE_SOUND_PRAYERS (1<<9) -#define PREFTOGGLE_ANNOUNCE_LOGIN (1<<10) -#define PREFTOGGLE_SOUND_ANNOUNCEMENTS (1<<11) -#define PREFTOGGLE_DISABLE_DEATHRATTLE (1<<12) -#define PREFTOGGLE_DISABLE_ARRIVALRATTLE (1<<13) -#define PREFTOGGLE_COMBOHUD_LIGHTING (1<<14) - -#define PREFTOGGLE_DEADMIN_ALWAYS (1<<15) -#define PREFTOGGLE_DEADMIN_ANTAGONIST (1<<16) -#define PREFTOGGLE_DEADMIN_POSITION_HEAD (1<<17) -#define PREFTOGGLE_DEADMIN_POSITION_SECURITY (1<<18) -#define PREFTOGGLE_DEADMIN_POSITION_SILICON (1<<19) - -#define PREFTOGGLE_OUTLINE_ENABLED (1<<20) -#define PREFTOGGLE_RUNECHAT_GLOBAL (1<<21) -#define PREFTOGGLE_RUNECHAT_NONMOBS (1<<22) -#define PREFTOGGLE_RUNECHAT_EMOTES (1<<23) - -#define TOGGLES_DEFAULT (PREFTOGGLE_SOUND_ADMINHELP|PREFTOGGLE_SOUND_MIDI|PREFTOGGLE_SOUND_AMBIENCE|PREFTOGGLE_SOUND_LOBBY|PREFTOGGLE_MEMBER_PUBLIC|PREFTOGGLE_INTENT_STYLE|PREFTOGGLE_SOUND_INSTRUMENTS|PREFTOGGLE_SOUND_SHIP_AMBIENCE|PREFTOGGLE_SOUND_PRAYERS|PREFTOGGLE_SOUND_ANNOUNCEMENTS|PREFTOGGLE_OUTLINE_ENABLED|PREFTOGGLE_RUNECHAT_GLOBAL|PREFTOGGLE_RUNECHAT_NONMOBS|PREFTOGGLE_RUNECHAT_EMOTES) - -// You CANNOT go above 1<<23 in BYOND due to integer limits -// Please add subsequent ones as PREFTOGGLE_2_[name] -// If you run out of these, make a third toggles column -#define PREFTOGGLE_2_FANCY_TGUI (1<<0) -#define PREFTOGGLE_2_LOCKED_TGUI (1<<1) -#define PREFTOGGLE_2_LOCKED_BUTTONS (1<<2) -#define PREFTOGGLE_2_WINDOW_FLASHING (1<<3) -#define PREFTOGGLE_2_CREW_OBJECTIVES (1<<4) -#define PREFTOGGLE_2_GHOST_HUD (1<<5) -#define PREFTOGGLE_2_GHOST_INQUISITIVENESS (1<<6) -#define PREFTOGGLE_2_USES_GLASSES_COLOUR (1<<7) -#define PREFTOGGLE_2_AMBIENT_OCCLUSION (1<<8) -#define PREFTOGGLE_2_AUTO_FIT_VIEWPORT (1<<9) -#define PREFTOGGLE_2_ENABLE_TIPS (1<<10) -#define PREFTOGGLE_2_SHOW_CREDITS (1<<11) -#define PREFTOGGLE_2_HOTKEYS (1<<12) -#define PREFTOGGLE_2_SOUNDTRACK (1<<13) -#define PREFTOGGLE_2_TGUI_INPUT (1<<14) -#define PREFTOGGLE_2_BIG_BUTTONS (1<<15) -#define PREFTOGGLE_2_SWITCHED_BUTTONS (1<<16) -#define PREFTOGGLE_2_TGUI_SAY (1<<17) -#define PREFTOGGLE_2_SAY_LIGHT_THEME (1<<18) -#define PREFTOGGLE_2_SAY_SHOW_PREFIX (1<<19) -#define PREFTOGGLE_2_SOUND_ADMINALERT (1<<20) - -#define TOGGLES_2_DEFAULT (PREFTOGGLE_2_FANCY_TGUI|PREFTOGGLE_2_LOCKED_TGUI|PREFTOGGLE_2_LOCKED_BUTTONS|PREFTOGGLE_2_WINDOW_FLASHING|PREFTOGGLE_2_CREW_OBJECTIVES|PREFTOGGLE_2_GHOST_HUD|PREFTOGGLE_2_GHOST_INQUISITIVENESS|PREFTOGGLE_2_AMBIENT_OCCLUSION|PREFTOGGLE_2_AUTO_FIT_VIEWPORT|PREFTOGGLE_2_ENABLE_TIPS|PREFTOGGLE_2_SHOW_CREDITS|PREFTOGGLE_2_HOTKEYS|PREFTOGGLE_2_SOUNDTRACK|PREFTOGGLE_2_TGUI_INPUT|PREFTOGGLE_2_BIG_BUTTONS|PREFTOGGLE_2_SWITCHED_BUTTONS|PREFTOGGLE_2_TGUI_SAY|PREFTOGGLE_2_SOUND_ADMINALERT) - -//Chat toggles -#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) -#define CHAT_GHOSTRADIO (1<<9) -#define CHAT_BANKCARD (1<<10) -#define CHAT_GHOSTLAWS (1<<11) -#define CHAT_GHOSTFOLLOWMINDLESS (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_GHOSTFOLLOWMINDLESS) - -#define PARALLAX_INSANE -1 //for show offs -#define PARALLAX_HIGH 0 //default. -#define PARALLAX_MED 1 -#define PARALLAX_LOW 2 -#define PARALLAX_DISABLE 3 //this option must be the highest number +#define PARALLAX_INSANE "Insane" +#define PARALLAX_HIGH "High" +#define PARALLAX_MED "Medium" +#define PARALLAX_LOW "Low" +#define PARALLAX_DISABLE "Disabled" #define PIXEL_SCALING_AUTO 0 #define PIXEL_SCALING_1X 1 @@ -149,44 +76,80 @@ #define UPLINK_IMPLANT "Implant" #define UPLINK_IMPLANT_WITH_PRICE "[UPLINK_IMPLANT] (-[UPLINK_IMPLANT_TELECRYSTAL_COST] TC)" -//Plasmamen helmet styles, when you edit those remember to edit list in preferences.dm +//Plasmamen helmet styles #define HELMET_DEFAULT "Default" #define HELMET_MK2 "Mark II" #define HELMET_PROTECTIVE "Protective" -// All DB preference entries go here -// --- DO NOT EVER CHANGE OR RE-USE VALUES HERE --- -// If you remove an entry, comment it out and leave it for preservation sake -// All the values must be strings because they are map entries not list indexes -#define PREFERENCE_TAG_TOGGLES "1" -#define PREFERENCE_TAG_TOGGLES2 "2" -#define PREFERENCE_TAG_ASAY_COLOUR "3" -#define PREFERENCE_TAG_OOC_COLOUR "4" -#define PREFERENCE_TAG_LAST_CL "5" -#define PREFERENCE_TAG_UI_STYLE "6" -#define PREFERENCE_TAG_OUTLINE_COLOUR "7" -#define PREFERENCE_TAG_BALLOON_ALERTS "8" -#define PREFERENCE_TAG_DEFAULT_SLOT "9" -#define PREFERENCE_TAG_CHAT_TOGGLES "10" -#define PREFERENCE_TAG_GHOST_FORM "11" -#define PREFERENCE_TAG_GHOST_ORBIT "12" -#define PREFERENCE_TAG_GHOST_ACCS "13" -#define PREFERENCE_TAG_GHOST_OTHERS "14" -#define PREFERENCE_TAG_PREFERRED_MAP "15" -#define PREFERENCE_TAG_IGNORING "16" -#define PREFERENCE_TAG_CLIENTFPS "17" -#define PREFERENCE_TAG_PARALLAX "18" -#define PREFERENCE_TAG_PIXELSIZE "19" -#define PREFERENCE_TAG_SCALING_METHOD "20" -#define PREFERENCE_TAG_TIP_DELAY "21" -#define PREFERENCE_TAG_PDA_THEME "22" -#define PREFERENCE_TAG_PDA_COLOUR "23" -#define PREFERENCE_TAG_KEYBINDS "24" -#define PREFERENCE_TAG_PURCHASED_GEAR "25" -#define PREFERENCE_TAG_ROLE_PREFERENCES "26" -#define PREFERENCE_TAG_PAI_NAME "27" -#define PREFERENCE_TAG_PAI_DESCRIPTION "28" -#define PREFERENCE_TAG_PAI_COMMENT "29" +GLOBAL_LIST_INIT(helmet_styles, list( + HELMET_DEFAULT, + HELMET_MK2, + HELMET_PROTECTIVE, +)) // True value of max save slots (3 is default, 8 is byond member, +1 to either if you have the extra slot loadout entry). Potential max is 9 #define TRUE_MAX_SAVE_SLOTS 9 + +// Values for /datum/preference/preference_type +/// 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 + +/// 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" + +//randomised elements +#define RANDOM_ANTAG_ONLY 1 +#define RANDOM_DISABLED 2 +#define RANDOM_ENABLED 3 + +// randomize_appearance_prefs() and randomize_human_appearance() proc flags +#define RANDOMIZE_SPECIES (1<<0) +#define RANDOMIZE_NAME (1<<1) + + +// Undatumized preference tags + +#define PREFERENCE_TAG_LAST_CL "last_changelog" +#define PREFERENCE_TAG_DEFAULT_SLOT "default_slot" +#define PREFERENCE_TAG_IGNORING "ignoring" +#define PREFERENCE_TAG_KEYBINDS "key_bindings" +#define PREFERENCE_TAG_PURCHASED_GEAR "purchased_gear" +#define PREFERENCE_TAG_ROLE_PREFERENCES_GLOBAL "be_special" +#define PREFERENCE_TAG_PAI_NAME "pai_name" +#define PREFERENCE_TAG_PAI_DESCRIPTION "pai_description" +#define PREFERENCE_TAG_PAI_COMMENT "pai_comment" + +#define CHARACTER_PREFERENCE_RANDOMISE "randomise" +#define CHARACTER_PREFERENCE_JOB_PREFERENCES "job_preferences" +#define CHARACTER_PREFERENCE_ALL_QUIRKS "all_quirks" +#define CHARACTER_PREFERENCE_EQUIPPED_GEAR "equipped_gear" +#define CHARACTER_PREFERENCE_ROLE_PREFERENCES "role_preferences" + +#define PREFERENCE_SHEET_NORMAL "preferences" +#define PREFERENCE_SHEET_LARGE "preferences_l" +#define PREFERENCE_SHEET_HUGE "preferences_h" diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index 2f4d0d6ff8002..66f03dfc2b792 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -181,11 +181,13 @@ GLOBAL_LIST_INIT(other_bannable_roles, list( CRASH("Invalid role_preference_key [role_preference_key] passed to role_preference_enabled!") if(!src.prefs) return FALSE - var/list/source = src.prefs.role_preferences var/datum/role_preference/pref = role_preference_key + // If this is per character, check if it's disabled. Otherwise continue and check the global value if(initial(pref.per_character)) - source = src.prefs.active_character.role_preferences_character - var/role_preference_value = source["[role_preference_key]"] + var/role_preference_value = src.prefs.role_preferences["[role_preference_key]"] + if(isnum(role_preference_value) && !role_preference_value) // explicitly disabled and not null + return FALSE + var/role_preference_value = src.prefs.role_preferences_global["[role_preference_key]"] if(isnum(role_preference_value) && !role_preference_value) // explicitly disabled and not null return FALSE return TRUE @@ -239,7 +241,15 @@ GLOBAL_LIST_INIT(other_bannable_roles, list( #define ROLE_PREFERENCE_CATEGORY_ANAGONIST "Antagonists" #define ROLE_PREFERENCE_CATEGORY_MIDROUND_LIVING "Midrounds (Living)" -#define ROLE_PREFERENCE_CATEGORY_MIDROUND_GHOST "Midrounds (Ghost Poll)" +#define ROLE_PREFERENCE_CATEGORY_MIDROUND_GHOST "Midrounds (Ghost)" +#define ROLE_PREFERENCE_CATEGORY_LEGACY "Legacy Roles (Out of Rotation)" + +GLOBAL_LIST_INIT(role_preference_categories, list( + ROLE_PREFERENCE_CATEGORY_ANAGONIST, + ROLE_PREFERENCE_CATEGORY_MIDROUND_GHOST, + ROLE_PREFERENCE_CATEGORY_MIDROUND_LIVING, + ROLE_PREFERENCE_CATEGORY_LEGACY, +)) GLOBAL_LIST_INIT(role_preference_entries, init_role_preference_entries()) diff --git a/code/__DEFINES/species.dm b/code/__DEFINES/species.dm index c920bb0a9e98f..c1bd925ea15d6 100644 --- a/code/__DEFINES/species.dm +++ b/code/__DEFINES/species.dm @@ -1,3 +1,111 @@ +//Defines for Species IDs +#define SPECIES_ABDUCTOR "abductor" +#define SPECIES_ANDROID "android" +#define SPECIES_APID "apid" +#define SPECIES_DEBUG "debug" +#define SPECIES_DULLAHAN "dullahan" +#define SPECIES_ETHEREAL "ethereal" +#define SPECIES_FELINID "felinid" +#define SPECIES_FLY "fly" +#define SPECIES_HUMAN "human" +#define SPECIES_IPC "ipc" +#define SPECIES_LIZARD "lizard" + #define SPECIES_ASHWALKER "ashlizard" +#define SPECIES_MONKEY "monkey" +#define SPECIES_MOTH "moth" +#define SPECIES_OOZELING "oozeling" + #define SPECIES_LUMINESCENT "lum" + #define SPECIES_SLIMEPERSON "slime" + #define SPECIES_STARGAZER "stargazer" +#define SPECIES_PLASMAMAN "plasmaman" +#define SPECIES_PODPERSON "pod" +#define SPECIES_PUMPKINPERSON "pumpkin_man" +#define SPECIES_SHADOWPERSON "shadow" +#define SPECIES_SKELETON "skeleton" +#define SPECIES_SNAILPERSON "snail" +#define SPECIES_SUPERSOLDIER "supersoldier" +#define SPECIES_VAMPIRE "vampire" +#define SPECIES_PSYPHOZA "psyphoza" + +//Defines for Golem Species IDs +#define SPECIES_GOLEM_ADAMANTINE "adamantine_golem" +#define SPECIES_GOLEM_ALLOY "alloy_golem" +#define SPECIES_GOLEM_BANANIUM "bananium_golem" +#define SPECIES_GOLEM_BLUESPACE "bluespace_golem" +#define SPECIES_GOLEM_BONE "bone_golem" +#define SPECIES_GOLEM_BRONZE "bronze_golem" +#define SPECIES_GOLEM_CAPITALIST "capitalist_golem" +#define SPECIES_GOLEM_CARDBOARD "cardboard_golem" +#define SPECIES_GOLEM_CLOCKWORK "clockwork_golem" +#define SPECIES_GOLEM_CLOCKWORK_SERVANT "clockwork golem servant" +#define SPECIES_GOLEM_CLOTH "cloth_golem" +#define SPECIES_GOLEM_COPPER "copper_golem" +#define SPECIES_GOLEM_DIAMOND "diamond_golem" +#define SPECIES_GOLEM_DURATHREAD "durathread_golem" +#define SPECIES_GOLEM_GLASS "glass_golem" +#define SPECIES_GOLEM_GOLD "gold_golem" +#define SPECIES_GOLEM_IRON "iron_golem" +#define SPECIES_GOLEM_LEATHER "leather_golem" +#define SPECIES_GOLEM_PLASMA "plasma_golem" +#define SPECIES_GOLEM_PLASTEEL "plasteel_golem" +#define SPECIES_GOLEM_PLASTIC "plastic_golem" +#define SPECIES_GOLEM_PLASTITANIUM "plastitanium_golem" +#define SPECIES_GOLEM_RUNIC "cult_golem" +#define SPECIES_GOLEM_SAND "sand_golem" +#define SPECIES_GOLEM_SILVER "silver_golem" +#define SPECIES_GOLEM_SNOW "snow_golem" +#define SPECIES_GOLEM_SOVIET "soviet_golem" +#define SPECIES_GOLEM_TITANIUM "titanium_golem" +#define SPECIES_GOLEM_URANIUM "uranium_golem" +#define SPECIES_GOLEM_WOOD "wood_golem" + +//Species bitflags, used for species_restricted. If this somehow ever gets above 23 Bee has larger problems. +#define FLAG_HUMAN (1<<0) +#define FLAG_IPC (1<<1) +#define FLAG_ETHEREAL (1<<2) +#define FLAG_PLASMAMAN (1<<3) +#define FLAG_APID (1<<4) +#define FLAG_MOTH (1<<5) +#define FLAG_LIZARD (1<<6) +#define FLAG_FELINID (1<<7) +#define FLAG_OOZELING (1<<8) +#define FLAG_FLY (1<<9) +#define FLAG_DEBUG_SPECIES (1<<10) +#define FLAG_MONKEY (1<<11) +#define FLAG_PSYPHOZA (1<<12) + +// Defines for used in creating "perks" for the species preference pages. +/// A key that designates UI icon displayed on the perk. +#define SPECIES_PERK_ICON "ui_icon" +/// A key that designates the name of the perk. +#define SPECIES_PERK_NAME "name" +/// A key that designates the description of the perk. +#define SPECIES_PERK_DESC "description" +/// A key that designates what type of perk it is (see below). +#define SPECIES_PERK_TYPE "perk_type" + +// The possible types each perk can be. +// Positive perks are shown in green, negative in red, and neutral in grey. +#define SPECIES_POSITIVE_PERK "positive" +#define SPECIES_NEGATIVE_PERK "negative" +#define SPECIES_NEUTRAL_PERK "neutral" + +//! ## control what things can spawn species +/// Badmin magic mirror +#define MIRROR_BADMIN (1<<0) +/// Standard magic mirror (wizard) +#define MIRROR_MAGIC (1<<1) +/// Pride ruin mirror +#define MIRROR_PRIDE (1<<2) +/// Race swap wizard event +#define RACE_SWAP (1<<3) +/// ERT spawn template (avoid races that don't function without correct gear) +#define ERT_SPAWN (1<<4) +/// xenobio black crossbreed +#define SLIME_EXTRACT (1<<5) +/// Wabbacjack staff projectiles +#define WABBAJACK (1<<6) + // Sounds used by species for "nasal/lungs" emotes - the DEFAULT being used mainly by humans, lizards, and ethereals becase biology idk #define SPECIES_DEFAULT_COUGH_SOUND(user) user.gender == FEMALE ? pick(\ diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 2b0aa6514d952..74e59ff842e90 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -11,7 +11,7 @@ * * make sure you add an update to the schema_version stable in the db changelog */ -#define DB_MAJOR_VERSION 6 +#define DB_MAJOR_VERSION 7 /** * DB minor schema version @@ -20,7 +20,7 @@ * * make sure you add an update to the schema_version stable in the db changelog */ -#define DB_MINOR_VERSION 2 +#define DB_MINOR_VERSION 0 //! ## Timing subsystem @@ -119,13 +119,14 @@ #define INIT_ORDER_RESEARCH 75 #define INIT_ORDER_ORBITS 74 //Other things use the orbital map, so it needs to be made early on. #define INIT_ORDER_STATION 73 //This is high priority because it manipulates a lot of the subsystems that will initialize after it. +#define INIT_ORDER_QUIRKS 72 #define INIT_ORDER_JOBS 71 //Must initialize before events for holidays #define INIT_ORDER_EVENTS 70 -#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_MAPPING 50 +#define INIT_ORDER_EARLY_ASSETS 48 #define INIT_ORDER_TIMETRACK 47 #define INIT_ORDER_NETWORKS 45 #define INIT_ORDER_ECONOMY 40 @@ -193,6 +194,7 @@ #define FIRE_PRIORITY_OVERLAYS 500 #define FIRE_PRIORITY_CALLBACKS 600 #define FIRE_PRIORITY_EXPLOSIONS 666 +#define FIRE_PRIORITY_PREFERENCES 690 #define FIRE_PRIORITY_TIMER 700 #define FIRE_PRIORITY_SOUND_LOOPS 800 #define FIRE_PRIORITY_INPUT 1000 // This must always always be the max highest priority. Player input must never be lost. diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index 23b6920ab3a81..ca24fa7f0f91b 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -714,11 +714,26 @@ 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 + +/// Checks if a value is contained in an associative list's values +/proc/assoc_contains_value(list/input, check_for) + for(var/key in input) + if(input[key] == check_for) + return TRUE + return FALSE + +/// Gets the first key that contains the given value in an associative list, otherwise, returns null. +/proc/assoc_key_for_value(list/input, check_for) + for(var/key in input) + if(input[key] == check_for) + return key + return null /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 0000000000000..ea7e9dae23c92 --- /dev/null +++ b/code/__HELPERS/admin.dm @@ -0,0 +1,11 @@ +/// Returns if the given client is an admin, REGARDLESS of if they're deadminned or not. +/proc/is_admin(client) + var/ckey = "" + if(istext(client)) + ckey = client + else if(istype(client, /client)) + var/client/C = client + ckey = C.ckey + else + return FALSE + return !isnull(GLOB.admin_datums[ckey]) || !isnull(GLOB.deadmins[ckey]) diff --git a/code/__HELPERS/colors.dm b/code/__HELPERS/colors.dm index 182082daf295f..c893ab11b17cb 100644 --- a/code/__HELPERS/colors.dm +++ b/code/__HELPERS/colors.dm @@ -45,3 +45,22 @@ animate(C, color = animate_color, time = flash_time) #define RANDOM_COLOUR (rgb(rand(0,255),rand(0,255),rand(0,255))) + +/// Given a color in the format of "#RRGGBB", will return if the color +/// is dark. +/proc/is_color_dark(color, threshold = 25) + var/hsl = rgb2num(color, COLORSPACE_HSL) + return hsl[3] < threshold + +/// Given a 3 character color (no hash), converts it into #RRGGBB (with hash) +/proc/expand_three_digit_color(color) + if (length_char(color) != 3) + CRASH("Invalid 3 digit color: [color]") + + var/final_color = "#" + + for (var/digit = 1 to 3) + final_color += copytext(color, digit, digit + 1) + final_color += copytext(color, digit, digit + 1) + + return final_color diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index c1076b1463332..ee64bafb60392 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -512,7 +512,7 @@ var/mob/living/carbon/human/new_character = new//The mob being spawned. SSjob.SendToLateJoin(new_character) - G_found.client.prefs.active_character.copy_to(new_character) + G_found.client.prefs.apply_prefs_to(new_character) new_character.dna.update_dna_identity() new_character.key = G_found.key @@ -528,7 +528,7 @@ var/mob/M = C if(M.client) C = M.client - if(!C || (!(C.prefs.toggles2 & PREFTOGGLE_2_WINDOW_FLASHING) && !ignorepref)) + if(!C || (!C.prefs.read_player_preference(/datum/preference/toggle/window_flashing) && !ignorepref)) return winset(C, "mainwindow", "flash=5") diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm index 8cab741dc8d51..5fbad2d0e54f4 100644 --- a/code/__HELPERS/global_lists.dm +++ b/code/__HELPERS/global_lists.dm @@ -18,6 +18,7 @@ init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/lizard, GLOB.tails_list_lizard) init_sprite_accessory_subtypes(/datum/sprite_accessory/tails_animated/lizard, GLOB.animated_tails_list_lizard) init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/human, GLOB.tails_list_human) + init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/human, GLOB.tails_roundstart_list_human, roundstart = TRUE) init_sprite_accessory_subtypes(/datum/sprite_accessory/tails_animated/human, GLOB.animated_tails_list_human) init_sprite_accessory_subtypes(/datum/sprite_accessory/snouts, GLOB.snouts_list) init_sprite_accessory_subtypes(/datum/sprite_accessory/horns,GLOB.horns_list) @@ -28,7 +29,6 @@ init_sprite_accessory_subtypes(/datum/sprite_accessory/spines, GLOB.spines_list) init_sprite_accessory_subtypes(/datum/sprite_accessory/spines_animated, GLOB.animated_spines_list) init_sprite_accessory_subtypes(/datum/sprite_accessory/legs, GLOB.legs_list) - init_sprite_accessory_subtypes(/datum/sprite_accessory/wings, GLOB.r_wings_list,roundstart = TRUE) init_sprite_accessory_subtypes(/datum/sprite_accessory/caps, GLOB.caps_list) init_sprite_accessory_subtypes(/datum/sprite_accessory/moth_wings, GLOB.moth_wings_list) init_sprite_accessory_subtypes(/datum/sprite_accessory/moth_wings, GLOB.moth_wings_roundstart_list, roundstart = TRUE) @@ -66,17 +66,11 @@ // Keybindings for(var/KB in subtypesof(/datum/keybinding)) var/datum/keybinding/keybinding = KB - if(!initial(keybinding.key) || !initial(keybinding.keybind_signal)) + if(!initial(keybinding.keys) || !initial(keybinding.keybind_signal)) continue var/datum/keybinding/instance = new keybinding - GLOB.keybindings_by_name[initial(instance.name)] = instance - if (!(initial(instance.key) in GLOB.keybinding_list_by_key)) - GLOB.keybinding_list_by_key[initial(instance.key)] = list() - GLOB.keybinding_list_by_key[initial(instance.key)] += instance.name - // Sort all the keybindings by their weight - for(var/key in GLOB.keybinding_list_by_key) - GLOB.keybinding_list_by_key[key] = sort_list(GLOB.keybinding_list_by_key[key]) - + GLOB.keybindings_by_name[instance.name] = instance + LAZYADD(GLOB.keybindings_by_name_to_key[instance.name], LAZYCOPY(instance.keys)) init_crafting_recipes(GLOB.crafting_recipes) diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index b2e7c4f8164d6..eacb59672fe52 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -216,7 +216,6 @@ world #define TO_HEX_DIGIT(n) ascii2text((n&15) + ((n&15)<10 ? 48 : 87)) //Dummy mob reserve slots -#define DUMMY_HUMAN_SLOT_PREFERENCES "dummy_preference_preview" #define DUMMY_HUMAN_SLOT_ADMIN "admintools" // Multiply all alpha values by this float @@ -1110,13 +1109,13 @@ GLOBAL_LIST_EMPTY(friendly_animal_types) /// # If you already have a human and need to get its flat icon, call `get_flat_existing_human_icon()` instead. /// For creating consistent icons for human looking simple animals. -/proc/get_flat_human_icon(icon_id, datum/job/J, datum/character_save/CS, dummy_key, showDirs = GLOB.cardinals, outfit_override = null) +/proc/get_flat_human_icon(icon_id, datum/job/J, datum/preferences/prefs, dummy_key, showDirs = GLOB.cardinals, outfit_override = null) var/static/list/humanoid_icon_cache = list() if(!icon_id || !humanoid_icon_cache[icon_id]) var/mob/living/carbon/human/dummy/body = generate_or_wait_for_human_dummy(dummy_key) - if(CS) - CS.copy_to(body,TRUE,FALSE) + if(prefs) + prefs.apply_prefs_to(body, icon_updates = TRUE) if(J) J.equip(body, TRUE, FALSE, outfit_override = outfit_override) else if (outfit_override) diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index abc982958dcb5..e59f7126397cc 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -58,6 +58,8 @@ /proc/random_features() if(!GLOB.tails_list_human.len) init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/human, GLOB.tails_list_human) + if(!GLOB.tails_roundstart_list_human.len) + init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/human, GLOB.tails_roundstart_list_human) if(!GLOB.tails_list_lizard.len) init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/lizard, GLOB.tails_list_lizard) if(!GLOB.snouts_list.len) @@ -188,6 +190,22 @@ GLOBAL_LIST_INIT(skin_tones, sort_list(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) @@ -374,18 +392,21 @@ GLOBAL_LIST_EMPTY(species_list) /proc/deadchat_broadcast(message, mob/follow_target=null, turf/turf_target=null, speaker_key=null, message_type=DEADCHAT_REGULAR) message = "[message]" for(var/mob/M in GLOB.player_list) - var/chat_toggles = TOGGLES_DEFAULT_CHAT - var/toggles = TOGGLES_DEFAULT + var/death_rattle = TRUE + var/arrivals_rattle = TRUE + var/dchat = FALSE + var/ghostlaws = TRUE var/list/ignoring if(M?.client.prefs) var/datum/preferences/prefs = M.client.prefs - chat_toggles = prefs.chat_toggles - toggles = prefs.toggles ignoring = prefs.ignoring - + death_rattle = prefs.read_player_preference(/datum/preference/toggle/death_rattle) + arrivals_rattle = prefs.read_player_preference(/datum/preference/toggle/arrivals_rattle) + dchat = prefs.read_player_preference(/datum/preference/toggle/chat_dead) + ghostlaws = prefs.read_player_preference(/datum/preference/toggle/chat_ghostlaws) var/override = FALSE - if(M?.client.holder && (chat_toggles & CHAT_DEAD)) + if(M?.client.holder && dchat) override = TRUE if(HAS_TRAIT(M, TRAIT_SIXTHSENSE)) override = TRUE @@ -400,13 +421,13 @@ GLOBAL_LIST_EMPTY(species_list) switch(message_type) if(DEADCHAT_DEATHRATTLE) - if(toggles & PREFTOGGLE_DISABLE_DEATHRATTLE) + if(!death_rattle) continue if(DEADCHAT_ARRIVALRATTLE) - if(toggles & PREFTOGGLE_DISABLE_ARRIVALRATTLE) + if(!arrivals_rattle) continue if(DEADCHAT_LAWCHANGE) - if(!(chat_toggles & CHAT_GHOSTLAWS)) + if(!ghostlaws) continue if(isobserver(M)) @@ -685,7 +706,7 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new) return . //// 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 @@ -696,20 +717,11 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new) var/banned = C ? is_banned_from(C.ckey, "Appearance") : null while(loop && safety < 5) - if(C?.prefs.active_character.custom_names[role] && !safety && !banned) - newname = C.prefs.active_character.custom_names[role] + if(!safety && !banned) + newname = C?.prefs?.read_character_preference(preference_type) else - switch(role) - if("human") - newname = random_unique_name(gender) - if("clown") - newname = pick(GLOB.clown_names) - if("mime") - newname = pick(GLOB.mime_names) - if("ai") - newname = pick(GLOB.ai_names) - else - return FALSE + var/datum/preference/preference = GLOB.preference_entries[preference_type] + newname = preference.create_informed_default_value(C.prefs) for(var/mob/living/M in GLOB.player_list) if(M == src) diff --git a/code/__HELPERS/priority_announce.dm b/code/__HELPERS/priority_announce.dm index 21a17342b0e63..c7a494b4e82e2 100644 --- a/code/__HELPERS/priority_announce.dm +++ b/code/__HELPERS/priority_announce.dm @@ -46,7 +46,7 @@ for(var/mob/M in GLOB.player_list) if(!isnewplayer(M) && M.can_hear()) to_chat(M, announcement) - if(M.client.prefs.toggles & PREFTOGGLE_SOUND_ANNOUNCEMENTS) + if(M.client.prefs.read_player_preference(/datum/preference/toggle/sound_announcements)) SEND_SOUND(M, s) /proc/exploration_announce(text, z_value) @@ -89,7 +89,7 @@ if(from) complete_msg += "-[from]" to_chat(M, complete_msg) - if(M.client.prefs.toggles & PREFTOGGLE_SOUND_ANNOUNCEMENTS) + if(M.client.prefs.read_player_preference(/datum/preference/toggle/sound_announcements)) if(alert) SEND_SOUND(M, sound('sound/misc/notice1.ogg')) else diff --git a/code/__HELPERS/sanitize_values.dm b/code/__HELPERS/sanitize_values.dm index a08352531ef1a..2d831ea4bb956 100644 --- a/code/__HELPERS/sanitize_values.dm +++ b/code/__HELPERS/sanitize_values.dm @@ -11,7 +11,7 @@ /proc/sanitize_float(number, min=0, max=1, accuracy=0.1, default=0) if(isnum_safe(number)) number = round(number, accuracy) - if(min <= number && number <= max) + if(round(min, accuracy) <= number && number <= round(max, accuracy)) return number return default @@ -53,7 +53,7 @@ return default /// Return `color` if it is a valid hex color, otherwise `default` -/proc/sanitize_hexcolor(color, desired_format=3, include_crunch=0, default) +/proc/sanitize_hexcolor(color, desired_format = 3, include_crunch = FALSE, default) var/crunch = include_crunch ? "#" : "" if(!istext(color)) color = "" @@ -61,38 +61,36 @@ var/start = 1 + (text2ascii(color, 1) == 35) var/len = length(color) var/char = "" - // RRGGBB -> RGB but awful - var/convert_to_shorthand = desired_format == 3 && length_char(color) > 3 + // Used for conversion between RGBA hex formats. + var/format_input_ratio = "[desired_format]:[length_char(color)-(start-1)]" . = "" var/i = start while(i <= len) char = color[i] + i += length(char) switch(text2ascii(char)) if(48 to 57) //numbers 0 to 9 . += char if(97 to 102) //letters a to f . += char if(65 to 70) //letters A to F - . += lowertext(char) + char = lowertext(char) + . += char else break - i += length(char) - if(convert_to_shorthand && i <= len) //skip next one - i += length(color[i]) - - if(length_char(.) != desired_format) - if(default) - return default - return crunch + repeat_string(desired_format, "0") - - return crunch + . + switch(format_input_ratio) + if("3:8", "4:8", "3:6", "4:6") //skip next one. RRGGBB(AA) -> RGB(A) + i += length(color[i]) + if("6:4", "6:3", "8:4", "8:3") //add current char again. RGB(A) -> RRGGBB(AA) + . += char -/// Return `color` as a formatted ooc valid hex color -/proc/sanitize_ooccolor(color) - if(length(color) != length_char(color)) - CRASH("Invalid characters in color '[color]'") - var/list/HSL = rgb2hsl(hex2num(copytext(color, 2, 4)), hex2num(copytext(color, 4, 6)), hex2num(copytext(color, 6, 8))) - HSL[3] = min(HSL[3],0.4) - var/list/RGB = hsl2rgb(arglist(HSL)) - return "#[num2hex(RGB[1],2)][num2hex(RGB[2],2)][num2hex(RGB[3],2)]" + if(length_char(.) == desired_format) + return crunch + . + switch(format_input_ratio) //add or remove alpha channel depending on desired format. + if("3:8", "3:4", "6:4") + return crunch + copytext(., 1, desired_format+1) + if("4:6", "4:3", "8:3") + return crunch + . + ((desired_format == 4) ? "f" : "ff") + else //not a supported hex color format. + return default ? default : crunch + repeat_string(desired_format, "0") diff --git a/code/__HELPERS/type2type.dm b/code/__HELPERS/type2type.dm index 75e37968f5d63..09fef155cba74 100644 --- a/code/__HELPERS/type2type.dm +++ b/code/__HELPERS/type2type.dm @@ -395,7 +395,6 @@ else . = max(0, min(255, 138.5177312231 * log(temp - 10) - 305.0447927307)) - /// Converts a text color like "red" to a hex color ("#FF0000") /proc/color2hex(color) //web colors if(!color) @@ -441,10 +440,8 @@ else return "#FFFFFF" - /** This is a weird one: It returns a list of all var names found in the string. These vars must be in the [var_name] format - It's only a proc because it's used in more than one place Takes a string and a datum. The string is well, obviously the string being checked. The datum is used as a source for var names, to check validity. Otherwise every single word could technically be a variable! diff --git a/code/_globalvars/lists/client.dm b/code/_globalvars/lists/client.dm index b37c2a811017f..3862f3656c836 100644 --- a/code/_globalvars/lists/client.dm +++ b/code/_globalvars/lists/client.dm @@ -1,2 +1,2 @@ -GLOBAL_LIST_EMPTY(keybinding_list_by_key) GLOBAL_LIST_EMPTY(keybindings_by_name) +GLOBAL_LIST_EMPTY(keybindings_by_name_to_key) diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm index f6e1a36b25ada..0eb4938c84791 100644 --- a/code/_globalvars/lists/flavor_misc.dm +++ b/code/_globalvars/lists/flavor_misc.dm @@ -17,7 +17,7 @@ GLOBAL_LIST_EMPTY(undershirt_m) //stores only undershirt name GLOBAL_LIST_EMPTY(undershirt_f) //stores only undershirt name //Socks GLOBAL_LIST_EMPTY(socks_list) //stores /datum/sprite_accessory/socks indexed by name - //Body Sizes +/// Body sizes. The names (keys) are what is actually stored in the database. Don't get crazy with changing them. GLOBAL_LIST_INIT(body_sizes, list( "Normal" = BODY_SIZE_NORMAL, "Short" = BODY_SIZE_SHORT, @@ -37,10 +37,10 @@ GLOBAL_LIST_EMPTY(animated_spines_list) //Mutant Human bits GLOBAL_LIST_EMPTY(tails_list_human) GLOBAL_LIST_EMPTY(animated_tails_list_human) +GLOBAL_LIST_EMPTY(tails_roundstart_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_wings_roundstart_list)//this lacks the blacklisted wings such as burned, clockwork and angel GLOBAL_LIST_EMPTY(moth_antennae_list) @@ -150,18 +150,26 @@ GLOBAL_LIST_INIT(ai_core_display_screens, sort_list(list( "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, sort_list(list( SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm index 8e8d3d982eab1..7ee86680f2d04 100644 --- a/code/_globalvars/lists/mobs.dm +++ b/code/_globalvars/lists/mobs.dm @@ -27,6 +27,7 @@ GLOBAL_LIST_EMPTY(drones_list) GLOBAL_LIST_EMPTY(dead_mob_list) //all dead mobs, including clientless. Excludes /mob/dead/new_player GLOBAL_LIST_EMPTY(joined_player_list) //all clients that have joined the game at round-start or as a latejoin. GLOBAL_LIST_EMPTY(new_player_list) //all /mob/dead/new_player, in theory all should have clients and those that don't are in the process of spawning and get deleted when done. +GLOBAL_LIST_EMPTY(pre_setup_antags) //minds that have been picked as antag by the gamemode. removed as antag datums are set. GLOBAL_LIST_EMPTY(silicon_mobs) //all silicon mobs GLOBAL_LIST_EMPTY(mob_living_list) //all instances of /mob/living and subtypes GLOBAL_LIST_EMPTY(carbon_list) //all instances of /mob/living/carbon and subtypes, notably does not contain brains or simple animals diff --git a/code/_globalvars/lists/names.dm b/code/_globalvars/lists/names.dm index 213fa11fae661..e212e2b2096de 100644 --- a/code/_globalvars/lists/names.dm +++ b/code/_globalvars/lists/names.dm @@ -37,23 +37,3 @@ GLOBAL_LIST_INIT(adjectives, world.file2list("strings/names/adjectives.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( - "ai" = list("pref_name" = "AI", "qdesc" = "ai name", "allow_numbers" = TRUE , "group" = "silicons", "allow_null" = FALSE), - "clown" = list("pref_name" = "Clown" , "qdesc" = "clown name", "allow_numbers" = FALSE , "group" = "fun", "allow_null" = FALSE), - "cyborg" = list("pref_name" = "Cyborg", "qdesc" = "cyborg name (Leave empty to use default naming scheme)", "allow_numbers" = TRUE , "group" = "silicons", "allow_null" = TRUE), - "deity" = list("pref_name" = "Chaplain deity", "qdesc" = "deity", "allow_numbers" = TRUE , "group" = "chaplain", "allow_null" = FALSE), - "human" = list("pref_name" = "Backup Human", "qdesc" = "backup human name, used in the event you are assigned a command role as another species", "allow_numbers" = FALSE , "group" = "backup_human", "allow_null" = FALSE), - "mime" = list("pref_name" = "Mime", "qdesc" = "mime name" , "allow_numbers" = FALSE , "group" = "fun", "allow_null" = FALSE), - "religion" = list("pref_name" = "Chaplain religion", "qdesc" = "religion" , "allow_numbers" = TRUE , "group" = "chaplain", "allow_null" = FALSE), -)) diff --git a/code/_globalvars/regexes.dm b/code/_globalvars/regexes.dm index c3d6e0e722511..7ac7c67986a2d 100644 --- a/code/_globalvars/regexes.dm +++ b/code/_globalvars/regexes.dm @@ -5,6 +5,8 @@ GLOBAL_DATUM_INIT(is_website, /regex, regex("http|www.|\[a-z0-9_-]+.(com|org|net GLOBAL_DATUM_INIT(is_email, /regex, regex("\[a-z0-9_-]+@\[a-z0-9_-]+.\[a-z0-9_-]+", "i")) GLOBAL_DATUM_INIT(is_alphanumeric, /regex, regex("\[a-z0-9]+", "i")) GLOBAL_DATUM_INIT(is_punctuation, /regex, regex("\[.!?]+", "i")) +GLOBAL_DATUM_INIT(is_color, /regex, regex("^#\[0-9a-fA-F]{6}$")) +GLOBAL_DATUM_INIT(is_color_nocrunch, /regex, regex("^\[0-9a-fA-F]{6}$")) //All characters forbidden by filenames: ", \, \n, \t, /, ?, %, *, :, |, <, >, .. GLOBAL_DATUM_INIT(filename_forbidden_chars, /regex, regex(@{""|[\\\n\t/?%*:|<>]|\.\."}, "g")) diff --git a/code/_onclick/hud/action_button.dm b/code/_onclick/hud/action_button.dm index 26f709f4b4bb8..d73e39784363c 100644 --- a/code/_onclick/hud/action_button.dm +++ b/code/_onclick/hud/action_button.dm @@ -99,14 +99,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_player_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.toggles2 & PREFTOGGLE_2_LOCKED_BUTTONS - locked = usr.client.prefs.toggles2 & PREFTOGGLE_2_LOCKED_BUTTONS + 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/credits.dm b/code/_onclick/hud/credits.dm index 24787d4dd2966..5ed1978c7b366 100644 --- a/code/_onclick/hud/credits.dm +++ b/code/_onclick/hud/credits.dm @@ -37,7 +37,7 @@ GLOBAL_LIST(end_titles) GLOB.end_titles += "

Thanks for playing!

" for(var/client/C in GLOB.clients) - if(C.prefs.toggles2 & PREFTOGGLE_2_SHOW_CREDITS) + if(C.prefs.read_player_preference(/datum/preference/toggle/show_credits)) C.screen += new /atom/movable/screen/credit/title_card(null, null, SSticker.mode.title_icon) sleep(CREDIT_SPAWN_SPEED * 3) for(var/i in 1 to GLOB.end_titles.len) @@ -75,7 +75,7 @@ GLOBAL_LIST(end_titles) /atom/movable/screen/credit/proc/add_to_clients() for(var/client/C in GLOB.clients) - if(C.prefs.toggles2 & PREFTOGGLE_2_SHOW_CREDITS) + if(C.prefs.read_player_preference(/datum/preference/toggle/show_credits)) C.screen += src /atom/movable/screen/credit/Destroy() diff --git a/code/_onclick/hud/ghost.dm b/code/_onclick/hud/ghost.dm index 30a0c8bb9ff86..d9d566d5c656c 100644 --- a/code/_onclick/hud/ghost.dm +++ b/code/_onclick/hud/ghost.dm @@ -89,10 +89,10 @@ if(!.) return var/mob/screenmob = viewmob || mymob - if(!(screenmob.client.prefs.toggles2 & PREFTOGGLE_2_GHOST_HUD)) - screenmob.client.screen -= static_inventory - else + if(screenmob.client.prefs.read_player_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 314d40db9c7c5..0d4e5079f86f2 100644 --- a/code/_onclick/hud/hud.dm +++ b/code/_onclick/hud/hud.dm @@ -70,12 +70,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_player_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.toggles2 & PREFTOGGLE_2_LOCKED_BUTTONS + hide_actions_toggle.locked = mymob.client.prefs.read_player_preference(/datum/preference/toggle/buttons_locked) hand_slots = list() @@ -143,6 +143,9 @@ GLOBAL_LIST_INIT(available_ui_styles, list( if(!screenmob.client) return FALSE + if(screenmob.client.prefs?.character_preview_view) // Changing HUDs clears the screen, we need to reregister then. + screenmob.client.prefs.character_preview_view.unregister_from_client(screenmob.client) + screenmob.client.screen = list() screenmob.client.apply_clickcatcher() @@ -221,6 +224,9 @@ GLOBAL_LIST_INIT(available_ui_styles, list( else if (viewmob.hud_used) viewmob.hud_used.plane_masters_update() + if(screenmob.client.prefs?.character_preview_view) // Changing HUDs clears the screen, we need to reregister then. + screenmob.client.prefs.character_preview_view.register_to_client(screenmob.client) + return TRUE /datum/hud/proc/plane_masters_update() diff --git a/code/_onclick/hud/parallax.dm b/code/_onclick/hud/parallax.dm index 571e092355451..5911d481e93c6 100755 --- a/code/_onclick/hud/parallax.dm +++ b/code/_onclick/hud/parallax.dm @@ -57,10 +57,10 @@ var/mob/screenmob = viewmob || mymob var/client/C = screenmob.client if(C.prefs) - var/pref = C.prefs.parallax + var/pref = C.prefs.read_player_preference(/datum/preference/choiced/parallax) if (isnull(pref)) pref = PARALLAX_HIGH - switch(C.prefs.parallax) + switch(pref) if (PARALLAX_INSANE) C.parallax_layers_max = 5 return TRUE diff --git a/code/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm index 9cfb2e96a50e5..3fdba04ef8d51 100644 --- a/code/_onclick/hud/rendering/plane_master.dm +++ b/code/_onclick/hud/rendering/plane_master.dm @@ -40,9 +40,26 @@ /atom/movable/screen/plane_master/floor/backdrop(mob/mymob) . = ..() remove_filter("openspace_shadow") - if(istype(mymob) && (mymob.client?.prefs?.toggles2 & PREFTOGGLE_2_AMBIENT_OCCLUSION)) + if(istype(mymob) && mymob.client?.prefs?.read_player_preference(/datum/preference/toggle/ambient_occlusion)) add_filter("openspace_shadow", 1, drop_shadow_filter(color = "#04080FAA", size = 10)) +/atom/movable/screen/plane_master/wall + name = "wall plane master" + plane = WALL_PLANE + appearance_flags = PLANE_MASTER + render_target = WALL_PLANE_RENDER_TARGET + blend_mode = BLEND_OVERLAY + +/atom/movable/screen/plane_master/wall/backdrop(mob/mymob) + . = ..() + remove_filter("AO") + if(istype(mymob) && mymob.client?.prefs?.read_player_preference(/datum/preference/toggle/ambient_occlusion)) + add_filter("AO", 1, drop_shadow_filter(x = 0, y = -2, size = 4, color = "#04080FAA")) + remove_filter("eye_blur") + if(istype(mymob) && mymob.eye_blurry) + add_filter("eye_blur", 1, gauss_blur_filter(clamp(mymob.eye_blurry * 0.1, 0.6, 3))) + + ///Contains most things in the game world /atom/movable/screen/plane_master/game_world name = "game world plane master" @@ -53,7 +70,7 @@ /atom/movable/screen/plane_master/game_world/backdrop(mob/mymob) . = ..() remove_filter("AO") - if(istype(mymob) && (mymob.client?.prefs?.toggles2 & PREFTOGGLE_2_AMBIENT_OCCLUSION)) + if(istype(mymob) && mymob.client?.prefs?.read_player_preference(/datum/preference/toggle/ambient_occlusion)) add_filter("AO", 1, drop_shadow_filter(x = 0, y = -2, size = 4, color = "#04080FAA")) remove_filter("eye_blur") if(istype(mymob) && mymob.eye_blurry) @@ -172,7 +189,7 @@ /atom/movable/screen/plane_master/runechat/backdrop(mob/mymob) . = ..() remove_filter("AO") - if(istype(mymob) && (mymob.client?.prefs?.toggles2 & PREFTOGGLE_2_AMBIENT_OCCLUSION)) + if(istype(mymob) && mymob.client?.prefs?.read_player_preference(/datum/preference/toggle/ambient_occlusion)) add_filter("AO", 1, drop_shadow_filter(x = 0, y = -2, size = 4, color = "#04080FAA")) /atom/movable/screen/plane_master/gravpulse @@ -211,9 +228,3 @@ render_target = PSYCHIC_PLANE_RENDER_TARGET render_relay_plane = RENDER_PLANE_NON_GAME alpha = 0 - -/atom/movable/screen/plane_master/psychic/wall - name = "psychic wall plane master" - plane = PSYCHIC_WALL_PLANE - render_target = PSYCHIC_WALL_PLANE_RENDER_TARGET - diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 7c4b1512ef189..5cb58ab7b719b 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -261,7 +261,7 @@ usr.a_intent_change(INTENT_HOTKEY_RIGHT) /atom/movable/screen/act_intent/segmented/Click(location, control, params) - if(usr.client.prefs.toggles & PREFTOGGLE_INTENT_STYLE) + if(usr.client.prefs.read_player_preference(/datum/preference/toggle/intent_style)) var/_x = text2num(params2list(params)["icon-x"]) var/_y = text2num(params2list(params)["icon-y"]) diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm index 11fe5767e7ae5..d16ceeaaee0aa 100644 --- a/code/_onclick/observer.dm +++ b/code/_onclick/observer.dm @@ -54,7 +54,7 @@ return TRUE else if(IsAdminGhost(user)) attack_ai(user) - else if(user.client.prefs.toggles2 & PREFTOGGLE_2_GHOST_INQUISITIVENESS) + else if(user.client.prefs.read_player_preference(/datum/preference/toggle/inquisitive_ghost)) user.examinate(src) return FALSE diff --git a/code/controllers/master.dm b/code/controllers/master.dm index f631c415ff19b..4284c6920351a 100644 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -59,6 +59,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 @@ -212,9 +216,11 @@ 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 log_world("Initializing [SS.name] subsystem.") 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/ambience.dm b/code/controllers/subsystem/ambience.dm index 0763c8705ea69..f70c126fffefa 100644 --- a/code/controllers/subsystem/ambience.dm +++ b/code/controllers/subsystem/ambience.dm @@ -66,7 +66,7 @@ SUBSYSTEM_DEF(ambience) ///Buzzing sound, the low ship drone that plays constantly, IC (requires the user to be able to hear) /datum/controller/subsystem/ambience/proc/play_buzz(mob/M, area/A) - if(M.can_hear_ambience() && (M.client.prefs.toggles & PREFTOGGLE_SOUND_SHIP_AMBIENCE)) + if(M.can_hear_ambience() && M.client.prefs.read_player_preference(/datum/preference/toggle/sound_ship_ambience)) if (!M.client.buzz_playing || (A.ambient_buzz != M.client.buzz_playing)) SEND_SOUND(M, sound(A.ambient_buzz, repeat = 1, wait = 0, volume = A.ambient_buzz_vol, channel = CHANNEL_BUZZ)) M.client.buzz_playing = A.ambient_buzz // It's done this way so I can tell when the user switches to an area that has a different buzz effect, so we can seamlessly swap over to that one diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm index 14804262a9c6f..ab52d86acacc9 100644 --- a/code/controllers/subsystem/atoms.dm +++ b/code/controllers/subsystem/atoms.dm @@ -21,6 +21,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() + #ifdef PROFILE_MAPLOAD_INIT_ATOM var/list/mapload_init_times = list() #endif @@ -78,6 +81,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() + #ifdef PROFILE_MAPLOAD_INIT_ATOM var/list/lines = list() lines += "Atom Path,Initialisation Time (ms)" @@ -247,6 +256,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 0000000000000..db1ffb13333ba --- /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 a66331edeb5a0..3284f3b5c5520 100644 --- a/code/controllers/subsystem/job.dm +++ b/code/controllers/subsystem/job.dm @@ -36,7 +36,6 @@ SUBSYSTEM_DEF(job) SetupOccupations() if(CONFIG_GET(flag/load_jobs_from_txt)) LoadJobs() - set_overflow_role(CONFIG_GET(string/overflow_job)) spare_id_safe_code = "[rand(0,9)][rand(0,9)][rand(0,9)][rand(0,9)][rand(0,9)]" @@ -96,6 +95,7 @@ SUBSYSTEM_DEF(job) /datum/controller/subsystem/job/proc/GetJob(rank) + RETURN_TYPE(/datum/job) if(!rank) CRASH("proc has taken no job name") if(!occupations.len) @@ -105,6 +105,7 @@ SUBSYSTEM_DEF(job) return name_occupations[rank] /datum/controller/subsystem/job/proc/GetJobType(jobtype) + RETURN_TYPE(/datum/job) if(!jobtype) CRASH("proc has taken no job type") if(!occupations.len) @@ -171,7 +172,7 @@ SUBSYSTEM_DEF(job) if(player.mind && (job.title in player.mind.restricted_roles)) JobDebug("FOC incompatible with antagonist role, Player: [player]") continue - if(player.client.prefs.active_character.job_preferences[job.title] == level) + if(player.client.prefs.job_preferences[job.title] == level) JobDebug("FOC pass, Player: [player], Level:[level]") candidates += player return candidates @@ -391,7 +392,7 @@ SUBSYSTEM_DEF(job) continue // If the player wants that job on this level, then try give it to him. - if(player.client.prefs.active_character.job_preferences[job.title] == level || (job.gimmick && player.client.prefs.active_character.job_preferences["Gimmick"] == level)) + if(player.client.prefs.job_preferences[job.title] == level || (job.gimmick && player.client.prefs.job_preferences["Gimmick"] == level)) // If the job isn't filled if((job.current_positions < job.spawn_positions) || job.spawn_positions == -1) JobDebug("DO pass, Player: [player], Level:[level], Job:[job.title]") @@ -435,26 +436,38 @@ 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_player_preference(/datum/preference/choiced/jobless_role) + if(PopcapReached() && !IS_PATRON(player.ckey)) RejectPlayer(player) - else if(player.client.prefs.active_character.joblessrole == BEOVERFLOW) - var/allowed_to_be_a_loser = !is_banned_from(player.ckey, SSjob.overflow_role) - if(QDELETED(player) || !allowed_to_be_a_loser) - RejectPlayer(player) - else - if(!AssignRole(player, SSjob.overflow_role)) + return + + switch (jobless_role) + if (BEOVERFLOW) + var/datum/job/overflow_role_datum = GetJob(overflow_role) + if(!istype(overflow_role_datum)) + stack_trace("Invalid overflow_role set ([overflow_role]), please make sure it matches a valid job datum.") RejectPlayer(player) - else if(player.client.prefs.active_character.joblessrole == BERANDOMJOB) - if(!GiveRandomJob(player)) + else + 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)) + RejectPlayer(player) + if (BERANDOMJOB) + if(!GiveRandomJob(player)) + RejectPlayer(player) + if (RETURNTOLOBBY) RejectPlayer(player) - else if(player.client.prefs.active_character.joblessrole == RETURNTOLOBBY) - RejectPlayer(player) - else //Something gone wrong if we got here. - var/message = "DO: [player] fell through handling unassigned" - JobDebug(message) - log_game(message) - message_admins(message) - RejectPlayer(player) + else //Something gone wrong if we got here. + var/message = "DO: [player] fell through handling unassigned" + JobDebug(message) + log_game(message) + message_admins(message) + RejectPlayer(player) + + //Gives the player the stuff he should have with his rank /datum/controller/subsystem/job/proc/EquipRank(mob/M, rank, joined_late = FALSE) var/mob/dead/new_player/newplayer @@ -517,7 +530,7 @@ SUBSYSTEM_DEF(job) SSpersistence.antag_rep_change[M.client.ckey] += job.GetAntagRep() if(M.client.holder) - if(CONFIG_GET(flag/auto_deadmin_players) || (M.client.prefs?.toggles & PREFTOGGLE_DEADMIN_ALWAYS)) + if(CONFIG_GET(flag/auto_deadmin_players) || M.client?.prefs.read_player_preference(/datum/preference/toggle/deadmin_always)) M.client.holder.auto_deadmin() else handle_auto_deadmin_roles(M.client, rank) @@ -532,7 +545,7 @@ SUBSYSTEM_DEF(job) if(wageslave.mind?.account_id) living_mob.add_memory("Your account ID is [wageslave.mind.account_id].") if(job && living_mob) - job.after_spawn(living_mob, M, joined_late) // note: this happens before the mob has a key! M will always have a client, living_mob might not. + job.after_spawn(living_mob, M, joined_late, M.client) // note: this happens before the mob has a key! M will always have a client, living_mob might not. if(living_mob.mind && !living_mob.mind.crew_objectives.len) give_crew_objective(living_mob.mind, M) @@ -545,11 +558,11 @@ SUBSYSTEM_DEF(job) var/datum/job/job = GetJob(rank) if(!job) return - if((job.auto_deadmin_role_flags & PREFTOGGLE_DEADMIN_POSITION_HEAD) && (CONFIG_GET(flag/auto_deadmin_heads) || (C.prefs?.toggles & PREFTOGGLE_DEADMIN_POSITION_HEAD))) + if((job.auto_deadmin_role_flags & DEADMIN_POSITION_HEAD) && (CONFIG_GET(flag/auto_deadmin_heads) || C.prefs?.read_player_preference(/datum/preference/toggle/deadmin_position_head))) return C.holder.auto_deadmin() - else if((job.auto_deadmin_role_flags & PREFTOGGLE_DEADMIN_POSITION_SECURITY) && (CONFIG_GET(flag/auto_deadmin_security) || (C.prefs?.toggles & PREFTOGGLE_DEADMIN_POSITION_SECURITY))) + else if((job.auto_deadmin_role_flags & DEADMIN_POSITION_SECURITY) && (CONFIG_GET(flag/auto_deadmin_security) || C.prefs?.read_player_preference(/datum/preference/toggle/deadmin_position_security))) return C.holder.auto_deadmin() - else if((job.auto_deadmin_role_flags & PREFTOGGLE_DEADMIN_POSITION_SILICON) && (CONFIG_GET(flag/auto_deadmin_silicons) || (C.prefs?.toggles & PREFTOGGLE_DEADMIN_POSITION_SILICON))) //in the event there's ever psuedo-silicon roles added, ie synths. + else if((job.auto_deadmin_role_flags & DEADMIN_POSITION_SILICON) && (CONFIG_GET(flag/auto_deadmin_silicons) || C.prefs?.read_player_preference(/datum/preference/toggle/deadmin_position_silicon))) //in the event there's ever psuedo-silicon roles added, ie synths. return C.holder.auto_deadmin() /datum/controller/subsystem/job/proc/setup_officer_positions() @@ -610,7 +623,7 @@ SUBSYSTEM_DEF(job) if(job.required_playtime_remaining(player.client)) young++ continue - switch(player.client.prefs.active_character.job_preferences[job.title]) + switch(player.client.prefs.job_preferences[job.title]) if(JP_HIGH) high++ if(JP_MEDIUM) diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm index 33ade90919524..9cd7b4d2a1068 100644 --- a/code/controllers/subsystem/mapping.dm +++ b/code/controllers/subsystem/mapping.dm @@ -377,8 +377,8 @@ 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 - if (!vote) + var/vote = c.prefs.read_player_preference(/datum/preference/choiced/preferred_map) + if (!vote || vote == "Default") if (global.config.defaultmap) mapvotes[global.config.defaultmap.map_name] += 1 continue diff --git a/code/controllers/subsystem/preferences.dm b/code/controllers/subsystem/preferences.dm new file mode 100644 index 0000000000000..4841bec3fb5de --- /dev/null +++ b/code/controllers/subsystem/preferences.dm @@ -0,0 +1,37 @@ +/// The subsystem used to serialize preferences when marked dirty. +/// This will mostly be saving preference changes that happen outside the UI. +SUBSYSTEM_DEF(preferences) + name = "Preference Serialization" + priority = FIRE_PRIORITY_PREFERENCES + flags = SS_POST_FIRE_TIMING|SS_NO_INIT + runlevels = RUNLEVEL_INIT|RUNLEVEL_LOBBY|RUNLEVELS_DEFAULT + // Length we should queue preferences writes against - prevents short-term unnecessary rewrites to the database. + wait = 5 SECONDS + + /// A list ckeys -> weakrefs to preference datums waiting to be serialized. + var/list/datums = list() + +/datum/controller/subsystem/preferences/proc/queue_write(datum/preferences/prefs) + if(!prefs.parent?.ckey) // No client ckey? No write. Prefs are written on logout anyway due to the UI closing. + return + var/ckey = ckey(prefs.parent.ckey) + if(datums[ckey]) // already queued + return + datums[ckey] = WEAKREF(prefs) + prefs.ui_update() // for queue preview + +/datum/controller/subsystem/preferences/fire(resumed) + for(var/ckey in datums) + var/datum/weakref/ref = datums[ckey] + var/datum/preferences/prefs = ref.resolve() + if(!prefs) + datums -= ckey + continue + if(prefs.ready_to_save_character()) + prefs.save_character() + if(prefs.ready_to_save_player()) + prefs.save_preferences() + datums -= ckey + prefs.ui_update() // for queue preview + if (MC_TICK_CHECK) + return diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index fe6874da4694e..cba4d2a16489b 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -12,13 +12,8 @@ PROCESSING_SUBSYSTEM_DEF(quirks) var/list/quirks = list() //Assoc. list of all roundstart quirk datum types; "name" = /path/ var/list/quirk_points = list() //Assoc. list of quirk names and their "point cost"; positive numbers are good traits, and negative ones are bad var/list/quirk_objects = list() //A list of all quirk objects in the game, since some may process - var/list/quirk_blacklist = list() //A list of quirks that can not be used with each other. Format: list(quirk1,quirk2),list(quirk3,quirk4) - -/datum/controller/subsystem/processing/quirks/Initialize(timeofday) - if(!length(quirks)) - SetupQuirks() - - quirk_blacklist = list( + /// A list of quirks that can not be used with each other. Format: list(quirk1,quirk2),list(quirk3,quirk4) + var/static/list/quirk_blacklist = list( list("Blind","Nearsighted"), list("Jolly","Depression","Apathetic","Hypersensitive"), list("Ageusia","Vegetarian","Deviant Tastes"), @@ -26,8 +21,18 @@ PROCESSING_SUBSYSTEM_DEF(quirks) list("Alcohol Tolerance","Light Drinker"), list("Social Anxiety","Mute"), ) + +/datum/controller/subsystem/processing/quirks/Initialize(timeofday) + get_quirks() return ..() +/// Returns the list of possible quirks +/datum/controller/subsystem/processing/quirks/proc/get_quirks() + RETURN_TYPE(/list) + if (!quirks.len) + SetupQuirks() + return quirks + /datum/controller/subsystem/processing/quirks/proc/SetupQuirks() // Sort by Positive, Negative, Neutral; and then by name var/list/quirk_list = sort_list(subtypesof(/datum/quirk), GLOBAL_PROC_REF(cmp_quirk_asc)) @@ -39,7 +44,7 @@ PROCESSING_SUBSYSTEM_DEF(quirks) /datum/controller/subsystem/processing/quirks/proc/AssignQuirks(datum/mind/user, client/cli, spawn_effects) var/bad_quirk_checker = 0 var/list/bad_quirks = list() - for(var/V in cli.prefs.active_character.all_quirks) + for(var/V in cli.prefs.all_quirks) var/datum/quirk/Q = quirks[V] if(Q) user.add_quirk(Q, spawn_effects) @@ -48,6 +53,70 @@ PROCESSING_SUBSYSTEM_DEF(quirks) stack_trace("Invalid quirk \"[V]\" in client [cli.ckey] preferences. the game has reset their quirks automatically.") bad_quirks += V if(bad_quirk_checker > 0 || length(bad_quirks)) // negative & zero value = calculation good / positive quirk value = something's wrong - cli.prefs.active_character.all_quirks = list() - cli.prefs.active_character.save(cli) + cli.prefs.all_quirks = list() + // save the new cleared quirks. + cli.prefs.mark_undatumized_dirty_character() client_alert(cli, "You have one or more outdated quirks[length(bad_quirks) ? ": [english_list(bad_quirks)]" : ""]. Your eligible quirks are kept at this round, but your character preference has been reset. Please review them at any time.", "Oh, no!") + +/// 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 + + var/list/all_quirks = get_quirks() + + for (var/quirk_name in quirks) + var/datum/quirk/quirk = all_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 diff --git a/code/controllers/subsystem/title.dm b/code/controllers/subsystem/title.dm index a8cf6d1ffd6d3..857814871c17a 100644 --- a/code/controllers/subsystem/title.dm +++ b/code/controllers/subsystem/title.dm @@ -54,7 +54,7 @@ SUBSYSTEM_DEF(title) fast_joiner.client?.view_size.resetToDefault(getScreenSize(fast_joiner)) // Execute this immediately, change_view runs through SStimer which doesn't execute until after // initialisation - if (fast_joiner.client?.prefs.toggles2 & PREFTOGGLE_2_AUTO_FIT_VIEWPORT) + if (fast_joiner.client?.prefs.read_player_preference(/datum/preference/toggle/auto_fit_viewport)) fast_joiner.client?.fit_viewport() fast_joiner.forceMove(newplayer_start_loc) diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index c368e4d97d91b..97cb73e7ffa27 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -68,8 +68,8 @@ SUBSYSTEM_DEF(vote) else if(mode == "map") for (var/non_voter_ckey in non_voters) var/client/C = non_voters[non_voter_ckey] - if(C.prefs.preferred_map) - var/preferred_map = C.prefs.preferred_map + var/preferred_map = C.prefs.read_player_preference(/datum/preference/choiced/preferred_map) + if(preferred_map && preferred_map != "Default") choices[preferred_map] += 1 greatest_votes = max(greatest_votes, choices[preferred_map]) else if(global.config.defaultmap) diff --git a/code/datums/action.dm b/code/datums/action.dm index 17cebd6ad90ea..344a8bd46de91 100644 --- a/code/datums/action.dm +++ b/code/datums/action.dm @@ -68,7 +68,7 @@ M.actions += src if(M.client) M.client.screen += button - button.locked = (M.client.prefs.toggles2 & PREFTOGGLE_2_LOCKED_BUTTONS) || 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_player_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 var/obj/effect/proc_holder/spell/spell_proc_holder = button.linked_action.target if(istype(spell_proc_holder) && spell_proc_holder.text_overlay) diff --git a/code/datums/brain_damage/imaginary_friend.dm b/code/datums/brain_damage/imaginary_friend.dm index 7c71d3616bf3d..5636175672e45 100644 --- a/code/datums/brain_damage/imaginary_friend.dm +++ b/code/datums/brain_damage/imaginary_friend.dm @@ -186,8 +186,8 @@ src.log_talk(message, LOG_SAY, tag="imaginary friend") // Display message - var/owner_chat_map = owner.client?.prefs.toggles & (PREFTOGGLE_RUNECHAT_GLOBAL | PREFTOGGLE_RUNECHAT_NONMOBS) - var/friend_chat_map = client?.prefs.toggles & (PREFTOGGLE_RUNECHAT_GLOBAL | PREFTOGGLE_RUNECHAT_NONMOBS) + var/owner_chat_map = owner.client?.prefs.read_player_preference(/datum/preference/toggle/enable_runechat) && owner.client.prefs.read_player_preference(/datum/preference/toggle/enable_runechat_non_mobs) + var/friend_chat_map = client?.prefs.read_player_preference(/datum/preference/toggle/enable_runechat) && client.prefs.read_player_preference(/datum/preference/toggle/enable_runechat_non_mobs) if (!owner_chat_map) var/mutable_appearance/MA = mutable_appearance('icons/mob/talk.dmi', src, "default[say_test(message)]", FLY_LAYER) MA.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm index fa3ff7ce3c98d..9e04676ed7bf9 100644 --- a/code/datums/chatmessage.dm +++ b/code/datums/chatmessage.dm @@ -284,9 +284,9 @@ /mob/proc/should_show_chat_message(atom/movable/speaker, datum/language/message_language, is_emote = FALSE, is_heard = FALSE) if(!client) return CHATMESSAGE_CANNOT_HEAR - if(!(client.prefs.toggles & PREFTOGGLE_RUNECHAT_GLOBAL) || (!(client.prefs.toggles & PREFTOGGLE_RUNECHAT_NONMOBS) && !ismob(speaker))) + if(!client.prefs.read_player_preference(/datum/preference/toggle/enable_runechat) || (!client.prefs.read_player_preference(/datum/preference/toggle/enable_runechat_non_mobs) && !ismob(speaker))) return CHATMESSAGE_CANNOT_HEAR - if(!(client.prefs.toggles & PREFTOGGLE_RUNECHAT_EMOTES) && is_emote) + if(!client.prefs.read_player_preference(/datum/preference/toggle/see_rc_emotes) && is_emote) return CHATMESSAGE_CANNOT_HEAR if(is_heard && !can_hear()) return CHATMESSAGE_CANNOT_HEAR @@ -458,7 +458,7 @@ /atom/proc/balloon_alert(mob/viewer, text, color = null) if(!viewer?.client) return - switch(viewer.client.prefs.see_balloon_alerts) + switch(viewer.client.prefs.read_player_preference(/datum/preference/choiced/show_balloon_alerts)) if(BALLOON_ALERT_ALWAYS) new /datum/chatmessage/balloon_alert(text, src, viewer, color) if(BALLOON_ALERT_WITH_CHAT) diff --git a/code/datums/datacore.dm b/code/datums/datacore.dm index e281ff55d85c1..027748b85e82f 100644 --- a/code/datums/datacore.dm +++ b/code/datums/datacore.dm @@ -387,5 +387,6 @@ security_records_out += list(crew_record) return security_records_out +// TODO tgui-prefs test this /datum/datacore/proc/get_id_photo(mob/living/carbon/human/human, show_directions = list(SOUTH)) return get_flat_existing_human_icon(human, show_directions) diff --git a/code/datums/diseases/advance/symptoms/clockwork.dm b/code/datums/diseases/advance/symptoms/clockwork.dm index 2ca29bf06d2db..bbc63452af826 100644 --- a/code/datums/diseases/advance/symptoms/clockwork.dm +++ b/code/datums/diseases/advance/symptoms/clockwork.dm @@ -334,16 +334,22 @@ organ_flags = ORGAN_SYNTHETIC status = ORGAN_ROBOTIC -/obj/item/organ/tail/clockwork/Insert(mob/living/carbon/human/H, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/tail/clockwork/Insert(mob/living/carbon/human/H, special = 0, drop_if_replaced = TRUE, pref_load = FALSE) ..() + if(pref_load && istype(H)) + H.update_body() + return if(istype(H)) if(!("tail_human" in H.dna.species.mutant_bodyparts)) H.dna.features["tail_human"] = tail_type H.dna.species.mutant_bodyparts |= "tail_human" H.update_body() -/obj/item/organ/tail/clockwork/Remove(mob/living/carbon/human/H, special = 0) +/obj/item/organ/tail/clockwork/Remove(mob/living/carbon/human/H, special = 0, pref_load = FALSE) ..() + if(pref_load && istype(H)) + H.update_body() + return if(istype(H)) H.dna.species.mutant_bodyparts -= "tail_human" H.update_body() diff --git a/code/datums/dna.dm b/code/datums/dna.dm index d72923de408aa..370918b7dccac 100644 --- a/code/datums/dna.dm +++ b/code/datums/dna.dm @@ -302,7 +302,7 @@ var/desired_size = GLOB.body_sizes[features["body_size"]] - if(desired_size == current_body_size) + if(desired_size == current_body_size || current_body_size == 0) return var/change_multiplier = desired_size / current_body_size @@ -401,7 +401,7 @@ /mob/living/carbon/proc/create_dna() dna = new /datum/dna(src) if(!dna.species) - var/rando_race = 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/emotes.dm b/code/datums/emotes.dm index fb3e7b1847e11..a4c6a4ce5d550 100644 --- a/code/datums/emotes.dm +++ b/code/datums/emotes.dm @@ -98,8 +98,8 @@ if(!M.client || isnewplayer(M)) continue var/T = get_turf(user) - if(M.stat == DEAD && M.client && (M.client.prefs.chat_toggles & CHAT_GHOSTSIGHT) && !(M in viewers(T, null))) - if(user.mind || (M.client.prefs.chat_toggles & CHAT_GHOSTFOLLOWMINDLESS)) + if(M.stat == DEAD && M.client && M.client.prefs.read_player_preference(/datum/preference/toggle/chat_ghostsight) && !(M in viewers(T, null))) + if(user.mind || M.client.prefs.read_player_preference(/datum/preference/toggle/chat_followghostmindless)) M.show_message("[FOLLOW_LINK(M, user)] [dchatmsg]") else M.show_message("[dchatmsg]") @@ -202,8 +202,8 @@ for(var/mob/ghost as anything in GLOB.dead_mob_list) if(!ghost.client || isnewplayer(ghost)) continue - if(ghost.client.prefs.chat_toggles & CHAT_GHOSTSIGHT && !(ghost in viewers(origin_turf, null))) - if(mind || (ghost.client.prefs.chat_toggles & CHAT_GHOSTFOLLOWMINDLESS)) + if(ghost.client.prefs.read_player_preference(/datum/preference/toggle/chat_ghostsight) && !(ghost in viewers(origin_turf, null))) + if(mind || ghost.client.prefs.read_player_preference(/datum/preference/toggle/chat_followghostmindless)) ghost.show_message("[FOLLOW_LINK(ghost, src)] [ghost_text]") else ghost.show_message("[ghost_text]") diff --git a/code/datums/keybinding/admin.dm b/code/datums/keybinding/admin.dm index ee98db2f039f9..a6673866d965d 100644 --- a/code/datums/keybinding/admin.dm +++ b/code/datums/keybinding/admin.dm @@ -6,7 +6,7 @@ return user.holder ? TRUE : FALSE /datum/keybinding/admin/admin_say - key = "F3" + keys = list("F3") name = "admin_say" full_name = "Admin say" description = "Talk with other admins." @@ -21,7 +21,7 @@ /datum/keybinding/admin/mentor_say - key = "F4" + keys = list("F4") name = "mentor_say" full_name = "Mentor say" description = "Speak with other mentors." @@ -40,7 +40,7 @@ /datum/keybinding/admin/admin_ghost - key = "F5" + keys = list("F5") name = "admin_ghost" full_name = "Admin Ghost" description = "Toggle your admin ghost status." @@ -55,7 +55,7 @@ /datum/keybinding/admin/player_panel - key = "F6" + keys = list("F6") name = "player_panel" full_name = "Player Panel" description = "View the player panel list." @@ -70,7 +70,7 @@ /datum/keybinding/admin/build_mode - key = "F7" + keys = list("F7") name = "toggle_build_mode" full_name = "Toggle Build Mode" description = "Toggle admin build mode on or off." @@ -85,7 +85,7 @@ /datum/keybinding/admin/invismin - key = "F8" + keys = list("F8") name = "invismin" full_name = "Toggle Invismin" description = "Toggle your admin invisibility." @@ -100,7 +100,7 @@ /datum/keybinding/admin/dead_say - key = "F10" + keys = list("F10") name = "dead_say" full_name = "Dead Say" description = "Speak in deadchat as an admin." diff --git a/code/datums/keybinding/artificial_intelligence.dm b/code/datums/keybinding/artificial_intelligence.dm index 7aa0719eb4630..1bde2e15548e7 100644 --- a/code/datums/keybinding/artificial_intelligence.dm +++ b/code/datums/keybinding/artificial_intelligence.dm @@ -6,7 +6,7 @@ return isAI(user.mob) /datum/keybinding/artificial_intelligence/reconnect - key = "-" + keys = list("-") name = "reconnect" full_name = "Reconnect to shell" description = "Reconnects you to your most recently used AI shell" diff --git a/code/datums/keybinding/carbon.dm b/code/datums/keybinding/carbon.dm index 815fb871645ba..30f38be81559e 100644 --- a/code/datums/keybinding/carbon.dm +++ b/code/datums/keybinding/carbon.dm @@ -6,7 +6,7 @@ return iscarbon(user.mob) /datum/keybinding/carbon/toggle_throw_mode - key = "R" + keys = list("R") name = "toggle_throw_mode" full_name = "Toggle throw mode" description = "Toggle throwing the current item or not." @@ -23,7 +23,7 @@ /datum/keybinding/carbon/select_help_intent - key = "1" + keys = list("1") name = "select_help_intent" full_name = "Select help intent" description = "" @@ -39,7 +39,7 @@ /datum/keybinding/carbon/select_disarm_intent - key = "2" + keys = list("2") name = "select_disarm_intent" full_name = "Select disarm intent" description = "" @@ -50,14 +50,13 @@ . = ..() if(.) return - if (!iscarbon(user.mob)) return var/mob/living/carbon/C = user.mob C.a_intent_change(INTENT_DISARM) return TRUE /datum/keybinding/carbon/select_grab_intent - key = "3" + keys = list("3") name = "select_grab_intent" full_name = "Select grab intent" description = "" @@ -68,14 +67,13 @@ . = ..() if(.) return - if (!iscarbon(user.mob)) return var/mob/living/carbon/C = user.mob C.a_intent_change(INTENT_GRAB) return TRUE /datum/keybinding/carbon/select_harm_intent - key = "4" + keys = list("4") name = "select_harm_intent" full_name = "Select harm intent" description = "" @@ -90,7 +88,7 @@ return TRUE /datum/keybinding/carbon/hold_throw_mode - key = "Space" + keys = list("Space") name = "hold_throw_mode" full_name = "Hold throw mode" description = "Hold this to turn on throw mode, and release it to turn off throw mode" @@ -110,8 +108,9 @@ return var/mob/living/carbon/carbon_user = user.mob carbon_user.throw_mode_off(THROW_MODE_HOLD) + /datum/keybinding/carbon/give - key = "G" + keys = list("G") name = "Give_Item" full_name = "Give item" description = "Give the item you're currently holding" diff --git a/code/datums/keybinding/client.dm b/code/datums/keybinding/client.dm index 733717c5d56b4..64a9e847c14ce 100644 --- a/code/datums/keybinding/client.dm +++ b/code/datums/keybinding/client.dm @@ -4,7 +4,7 @@ /datum/keybinding/client/get_help - key = "F1" + keys = list("F1") name = "get_help" full_name = "Get Help" description = "Ask an admin or mentor for help." @@ -19,7 +19,7 @@ /datum/keybinding/client/screenshot - key = "F2" + keys = list("F2") name = "screenshot" full_name = "Screenshot" description = "Take a screenshot." @@ -34,7 +34,7 @@ /datum/keybinding/client/toggleminimalhud - key = "F12" + keys = list("F12") name = "toggleminimalhud" full_name = "Toggle Minimal HUD" description = "Toggle the minimalized state of your hud." @@ -49,7 +49,7 @@ /datum/keybinding/client/zoomin - key = "\]" + keys = list("\]") name = "zoomin" full_name = "Zoom In" description = "Temporary switch icon scaling mode to 4x until unpressed" @@ -62,4 +62,4 @@ winset(user, "mapwindow.map", "zoom=[PIXEL_SCALING_4X]") /datum/keybinding/client/zoomin/up(client/user) - winset(user, "mapwindow.map", "zoom=[user.prefs.pixel_size]") + winset(user, "mapwindow.map", "zoom=[user.prefs.read_player_preference(/datum/preference/numeric/pixel_size)]") diff --git a/code/datums/keybinding/human.dm b/code/datums/keybinding/human.dm index b0c359ba0b390..05e3dd3a33122 100644 --- a/code/datums/keybinding/human.dm +++ b/code/datums/keybinding/human.dm @@ -7,7 +7,7 @@ /datum/keybinding/human/quick_equip - key = "E" + keys = list("E") name = "quick_equip" full_name = "Quick equip" description = "" @@ -23,7 +23,7 @@ /datum/keybinding/human/quick_equip_belt - key = "Shift-E" + keys = list("ShiftE") name = "quick_equip_belt" full_name = "Put Item In Belt" description = "" @@ -64,7 +64,7 @@ /datum/keybinding/human/quick_equip_backpack - key = "Shift-B" + keys = list("ShiftB") name = "quick_equip_backpack" full_name = "Put Item In Backpack" description = "" @@ -105,7 +105,7 @@ /datum/keybinding/human/quick_equip_suit_storage - key = "Shift-Q" + keys = list("ShiftQ") name = "quick_equip_suit_storage" full_name = "Put Item In Suit Storage" description = "" diff --git a/code/datums/keybinding/keybinding.dm b/code/datums/keybinding/keybinding.dm index 252cf6f0a8aac..b77e7ee580364 100644 --- a/code/datums/keybinding/keybinding.dm +++ b/code/datums/keybinding/keybinding.dm @@ -1,5 +1,5 @@ /datum/keybinding - var/key + var/list/keys var/name var/full_name var/description = "" @@ -18,7 +18,7 @@ /datum/keybinding/proc/down(client/user) SHOULD_CALL_PARENT(TRUE) return SEND_SIGNAL(user.mob, keybind_signal) & COMSIG_KB_ACTIVATED - + /datum/keybinding/proc/up(client/user) return FALSE diff --git a/code/datums/keybinding/living.dm b/code/datums/keybinding/living.dm index 948a32da54e74..9d8eef30226d7 100644 --- a/code/datums/keybinding/living.dm +++ b/code/datums/keybinding/living.dm @@ -7,7 +7,7 @@ /datum/keybinding/living/resist - key = "B" + keys = list("B") name = "resist" full_name = "Resist" description = "Break free of your current state. Handcuffs, on fire, being trapped in an alien nest? Resist!" @@ -15,7 +15,7 @@ /datum/keybinding/living/resist/down(client/user) . = ..() - if(. || !isliving(user.mob)) + if(.) return var/mob/living/L = user.mob L.resist() @@ -23,7 +23,7 @@ /datum/keybinding/living/rest - key = "V" + keys = list("V") name = "rest" full_name = "Rest" description = "Lay down, or get up." @@ -31,14 +31,14 @@ /datum/keybinding/living/rest/down(client/user) . = ..() - if(. || !isliving(user.mob)) + if(.) return var/mob/living/L = user.mob L.lay_down() return TRUE /datum/keybinding/living/look_up - key = "L" + keys = list("L") name = "look up" full_name = "Look Up" description = "Look up at the next z-level. Only works if below any nearby open space within a 3x3 square." @@ -46,7 +46,7 @@ /datum/keybinding/living/look_up/down(client/user) . = ..() - if(. || !isliving(user.mob)) + if(.) return var/mob/living/L = user.mob L.look_up(lock = TRUE) @@ -54,14 +54,14 @@ /datum/keybinding/living/look_up/up(client/user) . = ..() - if(. || !isliving(user.mob)) + if(.) return var/mob/living/L = user.mob L.look_reset() return TRUE /datum/keybinding/living/look_down - key = ";" + keys = list(";") name = "look down" full_name = "Look Down" description = "Look down at the previous z-level. Only works if above any nearby open space within a 3x3 square." @@ -77,7 +77,7 @@ /datum/keybinding/living/look_down/up(client/user) . = ..() - if(. || !isliving(user.mob)) + if(.) return var/mob/living/L = user.mob L.look_reset() @@ -85,7 +85,7 @@ //Keybind for sense /datum/keybinding/living/primary_species_action - key = "Shift-Space" + keys = list("Shift-Space") name = "species_primary" full_name = "Primary Species Action" description = "Activates a species primary action." diff --git a/code/datums/keybinding/mob.dm b/code/datums/keybinding/mob.dm index cd2a180cd5836..518d31d78d7b1 100644 --- a/code/datums/keybinding/mob.dm +++ b/code/datums/keybinding/mob.dm @@ -4,7 +4,7 @@ /datum/keybinding/mob/move_north - key = "W" + keys = list("W") name = "move_north" full_name = "Move North" description = "" @@ -29,7 +29,7 @@ /datum/keybinding/mob/move_east - key = "D" + keys = list("D") name = "move_east" full_name = "Move East" description = "" @@ -54,7 +54,7 @@ /datum/keybinding/mob/move_south - key = "S" + keys = list("S") name = "move_south" full_name = "Move South" description = "" @@ -79,7 +79,7 @@ /datum/keybinding/mob/move_west - key = "A" + keys = list("A") name = "move_west" full_name = "Move West" description = "" @@ -103,7 +103,7 @@ return TRUE /datum/keybinding/mob/move_up - key = "F" + keys = list("F") name = "move up" full_name = "Move Up" description = "Try moving upwards." @@ -128,7 +128,7 @@ return TRUE /datum/keybinding/mob/move_down - key = "C" + keys = list("C") name = "move down" full_name = "Move Down" description = "Try moving downwards." @@ -153,7 +153,7 @@ return TRUE /datum/keybinding/mob/stop_pulling - key = "H" + keys = list("H") name = "stop_pulling" full_name = "Stop pulling" description = "" @@ -172,7 +172,7 @@ return TRUE /datum/keybinding/mob/cycle_intent_right - key = "Home" + keys = list("Northwest") // this is BYOND for "HOME" name = "cycle_intent_right" full_name = "Cycle Intent Right" description = "" @@ -188,7 +188,7 @@ return TRUE /datum/keybinding/mob/cycle_intent_left - key = "Insert" + keys = list("Insert") name = "cycle_intent_left" full_name = "Cycle Intent Left" description = "" @@ -204,7 +204,7 @@ return TRUE /datum/keybinding/mob/swap_hands - key = "X" + keys = list("X") name = "swap_hands" full_name = "Swap hands" description = "" @@ -219,7 +219,7 @@ return TRUE /datum/keybinding/mob/activate_inhand - key = "Z" + keys = list("Z") name = "activate_inhand" full_name = "Activate in-hand" description = "Uses whatever item you have inhand" @@ -235,7 +235,7 @@ return TRUE /datum/keybinding/mob/drop_item - key = "Q" + keys = list("Q") name = "drop_item" full_name = "Drop Item" description = "" @@ -255,7 +255,7 @@ return TRUE /datum/keybinding/mob/toggle_move_intent - key = "Alt" + keys = list("Alt") name = "toggle_move_intent" full_name = "Hold to toggle move intent" description = "Held down to cycle to the other move intent, release to cycle back" @@ -277,7 +277,7 @@ return TRUE /datum/keybinding/mob/toggle_move_intent_alternative - key = "Unbound" + keys = list("Unbound") name = "toggle_move_intent_alt" full_name = "press to cycle move intent" description = "Pressing this cycle to the opposite move intent, does not cycle back" @@ -292,7 +292,7 @@ return TRUE /datum/keybinding/mob/target_head_cycle - key = "Numpad8" + keys = list("Numpad8") name = "target_head_cycle" full_name = "Target: Cycle head" description = "" @@ -307,7 +307,7 @@ return TRUE /datum/keybinding/mob/target_r_arm - key = "Numpad4" + keys = list("Numpad4") name = "target_r_arm" full_name = "Target: right arm" description = "" @@ -322,7 +322,7 @@ return TRUE /datum/keybinding/mob/target_body_chest - key = "Numpad5" + keys = list("Numpad5") name = "target_body_chest" full_name = "Target: Body" description = "" @@ -337,7 +337,7 @@ return TRUE /datum/keybinding/mob/target_left_arm - key = "Numpad6" + keys = list("Numpad6") name = "target_left_arm" full_name = "Target: left arm" description = "" @@ -352,7 +352,7 @@ return TRUE /datum/keybinding/mob/target_right_leg - key = "Numpad1" + keys = list("Numpad1") name = "target_right_leg" full_name = "Target: Right leg" description = "" @@ -367,7 +367,7 @@ return TRUE /datum/keybinding/mob/target_body_groin - key = "Numpad2" + keys = list("Numpad2") name = "target_body_groin" full_name = "Target: Groin" description = "" @@ -382,7 +382,7 @@ return TRUE /datum/keybinding/mob/target_left_leg - key = "Numpad3" + keys = list("Numpad3") name = "target_left_leg" full_name = "Target: left leg" description = "" @@ -397,7 +397,7 @@ return TRUE /datum/keybinding/mob/prevent_movement - key = "Ctrl" + keys = list("Ctrl") name = "block_movement" full_name = "Hold to change facing" description = "While pressed, prevents movement when pressing directional keys; instead just changes your facing direction" diff --git a/code/datums/keybinding/robot.dm b/code/datums/keybinding/robot.dm index 8e8403dafc6b5..4d2718fa20e51 100644 --- a/code/datums/keybinding/robot.dm +++ b/code/datums/keybinding/robot.dm @@ -7,7 +7,7 @@ /datum/keybinding/robot/toggle_module_1 - key = "1" + keys = list("1") name = "toggle_module_1" full_name = "Toggle Module 1" description = "Toggle your first module as a robot." @@ -23,7 +23,7 @@ /datum/keybinding/robot/toggle_module_2 - key = "2" + keys = list("2") name = "toggle_module_2" full_name = "Toggle Module 2" description = "Toggle your second module as a robot." @@ -39,7 +39,7 @@ /datum/keybinding/robot/toggle_module_3 - key = "3" + keys = list("3") name = "toggle_module_3" full_name = "Toggle Module 3" description = "Toggle your third module as a robot." @@ -55,7 +55,7 @@ /datum/keybinding/robot/change_intent_robot - key = "4" + keys = list("4") name = "change_intent_robot" full_name = "Change Intent" description = "Change your intent as a robot." @@ -71,7 +71,7 @@ /datum/keybinding/robot/unequip_module - key = "Q" + keys = list("Q") name = "unequip_module" full_name = "Unequip Module" description = "Unequip a robot module." diff --git a/code/datums/keybinding/shell.dm b/code/datums/keybinding/shell.dm index 21bfcf762b0d5..24df35b924010 100644 --- a/code/datums/keybinding/shell.dm +++ b/code/datums/keybinding/shell.dm @@ -14,7 +14,7 @@ /datum/keybinding/shell/undeploy category = CATEGORY_AI - key = "=" + keys = list("=") name = "undeploy" full_name = "Disconnect from shell" description = "Returns you to your AI core" diff --git a/code/datums/mind.dm b/code/datums/mind.dm index c1cc8b2804f03..bc4b38cda1a9e 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -314,32 +314,34 @@ var/obj/item/uplink_loc var/implant = FALSE - if(traitor_mob.client?.prefs) - switch(traitor_mob.client.prefs.active_character.uplink_spawn_loc) - if(UPLINK_PDA) + var/uplink_spawn_location = traitor_mob.client?.prefs?.read_character_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 = P + if(UPLINK_RADIO) + if(HAS_TRAIT(traitor_mob, TRAIT_MUTE)) // cant speak code into headset + to_chat(traitor_mob, "Using a radio uplink would be impossible with your muteness! Equipping PDA Uplink..") uplink_loc = PDA if(!uplink_loc) uplink_loc = R if(!uplink_loc) uplink_loc = P - if(UPLINK_RADIO) - if(HAS_TRAIT(traitor_mob, TRAIT_MUTE)) // cant speak code into headset - to_chat(traitor_mob, "Using a radio uplink would be impossible with your muteness! Equipping PDA Uplink..") + else + uplink_loc = R + if(!uplink_loc) uplink_loc = PDA - if(!uplink_loc) - uplink_loc = R - if(!uplink_loc) - uplink_loc = P - else - 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) + uplink_loc = P + if(UPLINK_PEN) + 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 @@ -696,6 +698,14 @@ special_role = ROLE_REV_HEAD /datum/mind/proc/AddSpell(obj/effect/proc_holder/spell/S) + // HACK: Preferences menu creates one of every selectable species. + // Some species, like vampires, create spells when they're made. + // The "action" is created when those spells Initialize. + // Preferences menu can create these assets at *any* time, primarily before + // the atoms SS initializes. + // That means "action" won't exist. + if (isnull(S.action)) + return spell_list += S S.action.Grant(current) diff --git a/code/datums/traits/_quirk.dm b/code/datums/traits/_quirk.dm index 49d89fde47da2..8b2082406f550 100644 --- a/code/datums/traits/_quirk.dm +++ b/code/datums/traits/_quirk.dm @@ -3,6 +3,9 @@ /datum/quirk var/name = "Test Quirk" var/desc = "This is a test 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 var/value = 0 var/list/restricted_mobtypes = list(/mob/living/carbon/human) //specifies valid mobtypes, have a good reason to change this var/list/restricted_species //specifies valid species, use /datum/species/ diff --git a/code/datums/traits/negative_quirk.dm b/code/datums/traits/negative_quirk.dm index 085010f8bd9ee..b6e3a313d2608 100644 --- a/code/datums/traits/negative_quirk.dm +++ b/code/datums/traits/negative_quirk.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 = -2 mood_quirk = TRUE gain_text = "Your back REALLY hurts!" @@ -19,6 +20,7 @@ /datum/quirk/blooddeficiency name = "Blood Deficiency" desc = "Your body can't produce enough blood to sustain itself." + icon = "tint" value = -2 gain_text = "You feel your vigor slowly fading away." lose_text = "You feel vigorous again." @@ -35,6 +37,7 @@ /datum/quirk/blindness name = "Blind" desc = "You are completely blind, nothing can counteract this." + icon = "eye-slash" value = -4 gain_text = "You can't see anything." lose_text = "You miraculously gain back your vision." @@ -56,6 +59,7 @@ /datum/quirk/brainproblems name = "Brain Tumor" desc = "You have a little friend in your brain that is slowly destroying it. Thankfully, you start with a bottle of mannitol pills." + icon = "brain" mob_trait = TRAIT_BRAIN_TUMOR value = -3 gain_text = "You feel smooth." @@ -100,6 +104,7 @@ /datum/quirk/deafness name = "Deaf" desc = "You are incurably deaf." + icon = "deaf" value = -2 mob_trait = TRAIT_DEAF gain_text = "You can't hear anything." @@ -109,6 +114,7 @@ /datum/quirk/depression name = "Depression" desc = "You sometimes just hate life." + icon = "frown" mob_trait = TRAIT_DEPRESSION value = -1 gain_text = "You start feeling depressed." @@ -124,6 +130,7 @@ /datum/quirk/family_heirloom name = "Family Heirloom" desc = "You are the current owner of an heirloom, passed down for generations. You have to keep it safe!" + icon = "toolbox" value = -1 mood_quirk = TRUE process = TRUE @@ -250,6 +257,7 @@ /datum/quirk/frail name = "Frail" desc = "Your bones might as well be made of glass! Your limbs can take less damage before they become disabled." + icon = "skull" value = -2 mob_trait = TRAIT_EASYLIMBDISABLE gain_text = "You feel frail." @@ -259,6 +267,7 @@ /datum/quirk/foreigner name = "Foreigner" desc = "You're not from around here. You don't know Galactic Common!" + icon = "question-circle" value = -1 gain_text = "The words being spoken around you don't make any sense." lose_text = "You've developed fluency in Galactic Common." @@ -279,6 +288,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 = -1 mob_trait = TRAIT_HEAVY_SLEEPER gain_text = "You feel sleepy." @@ -288,6 +298,7 @@ /datum/quirk/hypersensitive name = "Hypersensitive" desc = "For better or worse, everything seems to affect your mood more than it should." + icon = "flushed" value = -1 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." @@ -305,6 +316,7 @@ /datum/quirk/light_drinker name = "Light Drinker" desc = "You just can't handle your drinks and get drunk very quickly." + icon = "cocktail" value = -1 mob_trait = TRAIT_LIGHT_DRINKER gain_text = "Just the thought of drinking alcohol makes your head spin." @@ -313,6 +325,7 @@ /datum/quirk/nearsighted //t. errorage name = "Nearsighted" desc = "You are nearsighted without prescription glasses, but spawn with a pair." + icon = "glasses" value = -1 gain_text = "Things far away from you start looking blurry." lose_text = "You start seeing faraway things normally again." @@ -334,6 +347,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 = -1 process = TRUE @@ -353,6 +367,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 = -2 mob_trait = TRAIT_PACIFISM gain_text = "You feel repulsed by the thought of violence!" @@ -362,6 +377,7 @@ /datum/quirk/trauma/paraplegic name = "Paraplegic" desc = "Your legs do not function. Nothing will ever fix this. But hey, free wheelchair!" + icon = "wheelchair" value = -3 medical_record_text = "Patient has an untreatable impairment in motor function in the lower extremities." trauma_type = /datum/brain_trauma/severe/paralysis/paraplegic/ @@ -389,6 +405,7 @@ /datum/quirk/poor_aim name = "Poor Aim" desc = "You're terrible with guns and can't line up a straight shot to save your life. Dual-wielding is right out." + icon = "bullseye" value = -1 mob_trait = TRAIT_POOR_AIM medical_record_text = "Patient possesses a strong tremor in both hands." @@ -396,6 +413,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 = -1 mob_trait = TRAIT_PROSOPAGNOSIA medical_record_text = "Patient suffers from prosopagnosia and cannot recognize faces." @@ -403,6 +421,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 = -1 var/slot_string = "limb" @@ -435,6 +454,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 = -2 mob_trait = TRAIT_GRABWEAKNESS gain_text = "You feel like a pushover." @@ -443,7 +463,8 @@ /datum/quirk/insanity name = "Reality Dissociation Syndrome" - desc = "You suffer from a severe disorder that causes very vivid hallucinations. Mindbreaker toxin can suppress its effects, and you are immune to mindbreaker's hallucinogenic properties. This is not a license to grief." + desc = "You suffer from a severe disorder that causes very vivid hallucinations. Mindbreaker toxin can suppress its effects, and you are immune to mindbreaker's hallucinogenic properties. This is NOT a license to grief." + icon = "grin-tongue-wink" value = -2 //no mob trait because it's handled uniquely gain_text = "..." @@ -470,6 +491,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 = -1 gain_text = "You start worrying about what you're saying." lose_text = "You feel comfortable with talking again." //if only it were that easy! @@ -501,6 +523,7 @@ /datum/quirk/junkie name = "Junkie" desc = "You can't get enough of hard drugs." + icon = "pills" value = -2 gain_text = "You suddenly feel the craving for drugs." lose_text = "You feel like you should kick your drug habit." @@ -569,6 +592,7 @@ /datum/quirk/junkie/smoker name = "Smoker" desc = "Sometimes you just really want a smoke. Probably not great for your lungs." + icon = "smoking" value = -1 gain_text = "You could really go for a smoke right about now." lose_text = "You feel like you should quit smoking." @@ -604,6 +628,7 @@ /datum/quirk/alcoholic name = "Alcoholic" desc = "You can't stand being sober." + icon = "angry" value = -1 gain_text = "You could really go for a drink right about now." lose_text = "You feel like you should quit drinking." @@ -664,6 +689,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 = "cloud-rain" value = -2 mob_trait = TRAIT_UNSTABLE gain_text = "There's a lot on your mind right now." @@ -673,6 +699,7 @@ /datum/quirk/trauma //Generic for quirks that apply a brain trauma name = "Phobia" desc = "Because of a traumatic event in your past you have developed a strong phobia." + icon = "spider" value = -2 gain_text = null // these are handled by the trauma itself lose_text = null diff --git a/code/datums/traits/neutral_quirk.dm b/code/datums/traits/neutral_quirk.dm index 586bf967ca85a..36878c7a106fe 100644 --- a/code/datums/traits/neutral_quirk.dm +++ b/code/datums/traits/neutral_quirk.dm @@ -4,6 +4,7 @@ /datum/quirk/no_taste name = "Ageusia" desc = "You can't taste anything! Toxic food will still poison you." + icon = "meh-blank" value = 0 mob_trait = TRAIT_AGEUSIA gain_text = "You can't taste anything!" @@ -13,6 +14,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." @@ -35,6 +37,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." @@ -52,6 +55,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." @@ -69,6 +73,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." @@ -89,6 +94,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." @@ -106,6 +112,7 @@ /datum/quirk/mute name = "Mute" desc = "You are unable to speak." + icon = "volume-mute" value = 0 mob_trait = TRAIT_MUTE gain_text = "You feel unable to talk." diff --git a/code/datums/traits/positive_quirk.dm b/code/datums/traits/positive_quirk.dm index a3d4a52fba791..b78664cc0e038 100644 --- a/code/datums/traits/positive_quirk.dm +++ b/code/datums/traits/positive_quirk.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 = 1 mob_trait = TRAIT_ALCOHOL_TOLERANCE gain_text = "You feel like you could drink a whole keg!" @@ -12,6 +13,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 = 1 mood_quirk = TRUE @@ -28,6 +30,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 = 2 mob_trait = TRAIT_DRUNK_HEALING gain_text = "You feel like a drink would do you good." @@ -37,6 +40,7 @@ /datum/quirk/empath name = "Empath" desc = "Whether it's a sixth sense or careful study of body language, it only takes you a quick glance at someone to understand how they feel." + icon = "smile-beam" value = 2 mob_trait = TRAIT_EMPATH gain_text = "You feel in tune with those around you." @@ -45,6 +49,7 @@ /datum/quirk/freerunning name = "Freerunning" desc = "You're great at quick moves! You can climb tables more quickly." + icon = "running" value = 2 mob_trait = TRAIT_FREERUNNING gain_text = "You feel lithe on your feet!" @@ -53,6 +58,7 @@ /datum/quirk/friendly name = "Friendly" desc = "You give the best hugs, especially when you're in the right mood." + icon = "hands-helping" value = 1 mob_trait = TRAIT_FRIENDLY gain_text = "You want to hug someone." @@ -62,6 +68,7 @@ /datum/quirk/jolly name = "Jolly" desc = "You sometimes just feel happy, for no reason at all." + icon = "grin" value = 1 mob_trait = TRAIT_JOLLY mood_quirk = TRUE @@ -74,6 +81,7 @@ /datum/quirk/light_step name = "Light Step" desc = "You walk with a gentle step; stepping on sharp objects is quieter, less painful and you won't leave footprints behind you." + icon = "shoe-prints" value = 1 mob_trait = TRAIT_LIGHT_STEP gain_text = "You walk with a little more litheness." @@ -82,6 +90,7 @@ /datum/quirk/musician name = "Musician" desc = "You can tune handheld musical instruments to play melodies that clear certain negative effects and soothe the soul." + icon = "guitar" value = 1 mob_trait = TRAIT_MUSICIAN gain_text = "You know everything about musical instruments." @@ -99,6 +108,7 @@ /datum/quirk/linguist name = "Linguist" desc = "Although you don't know every language, your intense interest in languages allows you to recognise the features of most languages." + icon = "language" value = 1 mob_trait = TRAIT_LINGUIST gain_text = "You can recognise the linguistic features of every language." @@ -107,6 +117,7 @@ /datum/quirk/multilingual name = "Multilingual" desc = "You spent a portion of your life learning to understand an additional language. You may or may not be able to speak it based on your anatomy." + icon = "comments" value = 1 mob_trait = TRAIT_MULTILINGUAL gain_text = "You have learned to understand an additional language." @@ -142,6 +153,7 @@ /datum/quirk/night_vision name = "Night Vision" desc = "You can see slightly more clearly in full darkness than most people." + icon = "eye" value = 1 mob_trait = TRAIT_NIGHT_VISION gain_text = "The shadows seem a little less dark." @@ -157,6 +169,7 @@ /datum/quirk/photographer name = "Psychic Photographer" desc = "You have a special camera that can capture a photo of ghosts. Your experience in photography shortens the delay between each shot." + icon = "camera" value = 1 mob_trait = TRAIT_PHOTOGRAPHER gain_text = "You know everything about photography." @@ -178,18 +191,21 @@ /datum/quirk/selfaware name = "Self-Aware" desc = "You know your body well, and can accurately assess the extent of your wounds." + icon = "bone" value = 2 mob_trait = TRAIT_SELF_AWARE /datum/quirk/skittish name = "Skittish" desc = "You can conceal yourself in danger. Ctrl-shift-click a closed locker to jump into it, as long as you have access." + icon = "trash" value = 2 mob_trait = TRAIT_SKITTISH /datum/quirk/spiritual name = "Spiritual" desc = "You hold a spiritual belief, whether in God, nature or the arcane rules of the universe. You gain comfort from the presence of holy people, and believe that your prayers are more special than others." + icon = "bible" value = 1 mob_trait = TRAIT_SPIRITUAL gain_text = "You have faith in a higher power." @@ -215,6 +231,7 @@ /datum/quirk/tagger name = "Tagger" desc = "You're an experienced artist. While drawing graffiti, you can get twice as many uses out of drawing supplies." + icon = "spray-can" value = 1 mob_trait = TRAIT_TAGGER gain_text = "You know how to tag walls efficiently." @@ -230,6 +247,7 @@ /datum/quirk/voracious name = "Voracious" desc = "Nothing gets between you and your food. You eat faster and can binge on junk food! Being fat suits you just fine." + icon = "drumstick-bite" value = 1 mob_trait = TRAIT_VORACIOUS gain_text = "You feel HONGRY." @@ -238,6 +256,7 @@ /datum/quirk/neet name = "NEET" desc = "For some reason you qualified for social welfare." + icon = "money-check-alt" value = 1 mob_trait = TRAIT_NEET gain_text = "You feel useless to society." @@ -254,7 +273,8 @@ /datum/quirk/proskater name = "Skater Bro" - desc = "You’re a little too into old-earth skater culture! You're much more used to riding and falling off skateboards, needing less stamina to do kickflips and taking less damage upon bumping into something." + desc = "You're a little too into old-earth skater culture! You're much more used to riding and falling off skateboards, needing less stamina to do kickflips and taking less damage upon bumping into something." + icon = "hand-middle-finger" value = 2 mob_trait = TRAIT_PROSKATER gain_text = "You feel like hitting a sick grind!" diff --git a/code/datums/view.dm b/code/datums/view.dm index d82d4e4866bbf..7cc142149aecb 100644 --- a/code/datums/view.dm +++ b/code/datums/view.dm @@ -25,11 +25,11 @@ /datum/viewData/proc/assertFormat()//T-Pose winset(chief, "mapwindow.map", "zoom=0") -/datum/viewData/proc/resetFormat()//Cuck - winset(chief, "mapwindow.map", "zoom=[chief.prefs.pixel_size]") +/datum/viewData/proc/resetFormat() + winset(chief, "mapwindow.map", "zoom=[chief.prefs.read_player_preference(/datum/preference/numeric/pixel_size)]") /datum/viewData/proc/setZoomMode() - winset(chief, "mapwindow.map", "zoom-mode=[chief.prefs.scaling_method]") + winset(chief, "mapwindow.map", "zoom-mode=[chief.prefs.read_player_preference(/datum/preference/choiced/scaling_method)]") /datum/viewData/proc/isZooming() return (width || height) diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm index 88ff55c4690b4..e8d1ffea46e18 100644 --- a/code/datums/world_topic.dm +++ b/code/datums/world_topic.dm @@ -323,7 +323,7 @@ msg = emoji_parse(msg) log_ooc("DISCORD: [unm]: [msg]") for(var/client/C in GLOB.clients) - if(C.prefs.chat_toggles & CHAT_OOC) + if(C.prefs.read_player_preference(/datum/preference/toggle/chat_ooc)) if(!("discord-[unm]" in C.prefs.ignoring)) to_chat(C, "OOC: [unm]: [msg]") statuscode = 200 diff --git a/code/game/gamemodes/brother/traitor_bro.dm b/code/game/gamemodes/brother/traitor_bro.dm index 2ba9c58635997..e4363953b7b19 100644 --- a/code/game/gamemodes/brother/traitor_bro.dm +++ b/code/game/gamemodes/brother/traitor_bro.dm @@ -48,7 +48,12 @@ bro.restricted_roles = restricted_jobs log_game("[key_name(bro)] has been selected as a Brother") pre_brother_teams += team - return ..() + . = ..() + if(.) //To ensure the game mode is going ahead + for(var/teams in pre_brother_teams) + for(var/antag in teams) + GLOB.pre_setup_antags += antag + return /datum/game_mode/traitor/bros/post_setup() for(var/datum/team/brother_team/team in pre_brother_teams) @@ -56,6 +61,7 @@ team.forge_brother_objectives() for(var/datum/mind/M in team.members) M.add_antag_datum(/datum/antagonist/brother, team) + GLOB.pre_setup_antags -= M team.update_name() brother_teams += pre_brother_teams return ..() diff --git a/code/game/gamemodes/changeling/changeling.dm b/code/game/gamemodes/changeling/changeling.dm index ee0c589f30779..325ba0eb148ef 100644 --- a/code/game/gamemodes/changeling/changeling.dm +++ b/code/game/gamemodes/changeling/changeling.dm @@ -57,16 +57,18 @@ GLOBAL_LIST_INIT(slot2type, list("head" = /obj/item/clothing/head/changeling, "w changelings += changeling changeling.special_role = ROLE_CHANGELING changeling.restricted_roles = restricted_jobs - return 1 + GLOB.pre_setup_antags += changeling + return TRUE else setup_error = "Not enough changeling candidates" - return 0 + return FALSE /datum/game_mode/changeling/post_setup() for(var/datum/mind/changeling in changelings) log_game("[key_name(changeling)] has been selected as a changeling") var/datum/antagonist/changeling/new_antag = new() changeling.add_antag_datum(new_antag) + GLOB.pre_setup_antags -= changeling ..() /datum/game_mode/changeling/make_antag_chance(mob/living/carbon/human/character) //Assigns changeling to latejoiners diff --git a/code/game/gamemodes/changeling/traitor_chan.dm b/code/game/gamemodes/changeling/traitor_chan.dm index 6912af56cbf17..ace442760296f 100644 --- a/code/game/gamemodes/changeling/traitor_chan.dm +++ b/code/game/gamemodes/changeling/traitor_chan.dm @@ -57,13 +57,18 @@ changeling.special_role = ROLE_CHANGELING changelings += changeling changeling.restricted_roles = restricted_jobs - return ..() + . = ..() + if(.) //To ensure the game mode is going ahead + for(var/antag in changelings) + GLOB.pre_setup_antags += antag + return else - return 0 + return FALSE /datum/game_mode/traitor/changeling/post_setup() for(var/datum/mind/changeling in changelings) changeling.add_antag_datum(/datum/antagonist/changeling) + GLOB.pre_setup_antags -= changeling return ..() /datum/game_mode/traitor/changeling/make_antag_chance(mob/living/carbon/human/character) //Assigns changeling to latejoiners diff --git a/code/game/gamemodes/clock_cult/clockcult.dm b/code/game/gamemodes/clock_cult/clockcult.dm index bce5bf9595054..f1371e09021ba 100644 --- a/code/game/gamemodes/clock_cult/clockcult.dm +++ b/code/game/gamemodes/clock_cult/clockcult.dm @@ -66,6 +66,7 @@ GLOBAL_VAR(clockcult_eminence) selected_servants += clockie clockie.assigned_role = ROLE_SERVANT_OF_RATVAR clockie.special_role = ROLE_SERVANT_OF_RATVAR + GLOB.pre_setup_antags += clockie generate_clockcult_scriptures() return TRUE @@ -87,6 +88,7 @@ GLOBAL_VAR(clockcult_eminence) S.equip_carbon(servant_mind.current) S.equip_servant() S.prefix = CLOCKCULT_PREFIX_MASTER + GLOB.pre_setup_antags -= S //Setup the conversion limits for auto opening the ark calculate_clockcult_values() return ..() diff --git a/code/game/gamemodes/cult/cult.dm b/code/game/gamemodes/cult/cult.dm index 1280a629674d7..c78d5deae119d 100644 --- a/code/game/gamemodes/cult/cult.dm +++ b/code/game/gamemodes/cult/cult.dm @@ -95,6 +95,8 @@ log_game("[key_name(cultist)] has been selected as a cultist") if(cultists_to_cult.len>=required_enemies) + for(var/antag in cultists_to_cult) + GLOB.pre_setup_antags += antag return TRUE else setup_error = "Not enough cultist candidates" @@ -106,6 +108,7 @@ for(var/datum/mind/cult_mind in cultists_to_cult) add_cultist(cult_mind, 0, equip=TRUE, cult_team = main_cult) + GLOB.pre_setup_antags -= cult_mind main_cult.setup_objectives() //Wait until all cultists are assigned to make sure none will be chosen as sacrifice. diff --git a/code/game/gamemodes/devil/devil_game_mode.dm b/code/game/gamemodes/devil/devil_game_mode.dm index 95b7ab5db6711..0f46ee414fa9e 100644 --- a/code/game/gamemodes/devil/devil_game_mode.dm +++ b/code/game/gamemodes/devil/devil_game_mode.dm @@ -52,15 +52,17 @@ if(devils.len < required_enemies) setup_error = "Not enough devil candidates" - return 0 - return 1 + return FALSE + for(var/antag in devils) + GLOB.pre_setup_antags += antag + return TRUE /datum/game_mode/devil/post_setup() for(var/datum/mind/devil in devils) post_setup_finalize(devil) ..() - return 1 + return TRUE /datum/game_mode/devil/generate_report() return "Infernal creatures have been seen nearby offering great boons in exchange for souls. This is considered theft against Nanotrasen, as all employment contracts contain a lien on the \ @@ -68,6 +70,7 @@ /datum/game_mode/devil/proc/post_setup_finalize(datum/mind/devil) add_devil(devil.current, ascendable = TRUE) //Devil gamemode devils are ascendable. + GLOB.pre_setup_antags -= devil add_devil_objectives(devil,2) /proc/is_devil(mob/living/M) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm index 70d47f8e41d6e..eb48b7164ee71 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm @@ -162,6 +162,7 @@ /datum/dynamic_ruleset/proc/execute(forced = FALSE) for(var/datum/mind/M in assigned) M.add_antag_datum(antag_datum) + GLOB.pre_setup_antags -= M return TRUE /// Here you can perform any additional checks you want. (such as checking the map etc) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm index 30aa3e1da7dcc..2e1308adad7ad 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm @@ -19,7 +19,6 @@ req_hours = initial(antag_datum.required_living_playtime) )) candidates.Remove(P) - continue /datum/dynamic_ruleset/latejoin/ready(forced = FALSE) if (forced) diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm index e4aef404b1d02..d5032b56ff746 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm @@ -31,6 +31,7 @@ assigned += M.mind M.mind.special_role = ROLE_TRAITOR M.mind.restricted_roles = restricted_roles + GLOB.pre_setup_antags += M.mind return TRUE @@ -69,6 +70,7 @@ team.add_member(bro.mind) bro.mind.special_role = ROLE_BROTHER bro.mind.restricted_roles = restricted_roles + GLOB.pre_setup_antags += bro.mind pre_brother_teams += team return TRUE @@ -78,6 +80,7 @@ team.forge_brother_objectives() for(var/datum/mind/M in team.members) M.add_antag_datum(/datum/antagonist/brother, team) + GLOB.pre_setup_antags -= M team.update_name() mode.brother_teams += pre_brother_teams return DYNAMIC_EXECUTE_SUCCESS @@ -111,6 +114,7 @@ assigned += M.mind M.mind.restricted_roles = restricted_roles M.mind.special_role = ROLE_CHANGELING + GLOB.pre_setup_antags += M.mind return TRUE ////////////////////////////////////////////// @@ -144,6 +148,7 @@ assigned += picked_candidate.mind picked_candidate.mind.restricted_roles = restricted_roles picked_candidate.mind.special_role = ROLE_HERETIC + GLOB.pre_setup_antags += picked_candidate.mind return TRUE @@ -182,6 +187,7 @@ assigned += M.mind M.mind.assigned_role = ROLE_WIZARD M.mind.special_role = ROLE_WIZARD + GLOB.pre_setup_antags += M.mind return TRUE @@ -225,6 +231,7 @@ assigned += M.mind M.mind.special_role = ROLE_CULTIST M.mind.restricted_roles = restricted_roles + GLOB.pre_setup_antags += M.mind return TRUE /datum/dynamic_ruleset/roundstart/bloodcult/execute(forced = FALSE) @@ -234,6 +241,7 @@ new_cultist.cult_team = main_cult new_cultist.give_equipment = TRUE M.add_antag_datum(new_cultist) + GLOB.pre_setup_antags -= M main_cult.setup_objectives() return DYNAMIC_EXECUTE_SUCCESS @@ -281,6 +289,7 @@ assigned += M.mind M.mind.assigned_role = ROLE_OPERATIVE M.mind.special_role = ROLE_OPERATIVE + GLOB.pre_setup_antags += M.mind return TRUE /datum/dynamic_ruleset/roundstart/nuclear/execute(forced = FALSE) @@ -293,6 +302,7 @@ else var/datum/antagonist/nukeop/new_op = new antag_datum() M.add_antag_datum(new_op) + GLOB.pre_setup_antags -= M return DYNAMIC_EXECUTE_SUCCESS /datum/dynamic_ruleset/roundstart/nuclear/round_result() @@ -366,6 +376,7 @@ assigned += M.mind M.mind.restricted_roles = restricted_roles M.mind.special_role = ROLE_REV_HEAD + GLOB.pre_setup_antags += M.mind return TRUE /datum/dynamic_ruleset/roundstart/revs/execute(forced = FALSE) @@ -380,6 +391,7 @@ else assigned -= M log_game("DYNAMIC: [ruletype] [name] discarded [M.name] from head revolutionary due to ineligibility.") + GLOB.pre_setup_antags -= M if(revolution.members.len) revolution.update_objectives() revolution.update_heads() @@ -458,6 +470,7 @@ for(var/datum/mind/V in assigned) V.assigned_role = "Clown Operative" V.special_role = "Clown Operative" + GLOB.pre_setup_antags += V ////////////////////////////////////////////// // // @@ -488,6 +501,7 @@ assigned += devil.mind devil.mind.special_role = ROLE_DEVIL devil.mind.restricted_roles = restricted_roles + GLOB.pre_setup_antags += devil.mind log_game("[key_name(devil)] has been selected as a devil") return TRUE @@ -495,6 +509,7 @@ /datum/dynamic_ruleset/roundstart/devil/execute(forced = FALSE) for(var/datum/mind/devil in assigned) add_devil(devil.current, ascendable = TRUE) + GLOB.pre_setup_antags -= devil add_devil_objectives(devil,2) return DYNAMIC_EXECUTE_SUCCESS @@ -582,6 +597,7 @@ assigned += servant.mind servant.mind.assigned_role = ROLE_SERVANT_OF_RATVAR servant.mind.special_role = ROLE_SERVANT_OF_RATVAR + GLOB.pre_setup_antags += servant.mind //Generate scriptures generate_clockcult_scriptures() return TRUE @@ -598,6 +614,7 @@ S.equip_carbon(servant_mind.current) S.equip_servant() S.prefix = CLOCKCULT_PREFIX_MASTER + GLOB.pre_setup_antags -= servant_mind //Setup the conversion limits for auto opening the ark calculate_clockcult_values() return DYNAMIC_EXECUTE_SUCCESS @@ -644,6 +661,7 @@ assigned += M.mind M.mind.special_role = ROLE_INCURSION M.mind.restricted_roles = restricted_roles + GLOB.pre_setup_antags += M.mind return TRUE /datum/dynamic_ruleset/roundstart/incursion/execute(forced = FALSE) @@ -654,6 +672,7 @@ new_incursionist.team = incursion_team incursion_team.add_member(M) M.add_antag_datum(new_incursionist) + GLOB.pre_setup_antags -= M return DYNAMIC_EXECUTE_SUCCESS /datum/dynamic_ruleset/roundstart/incursion/round_result() @@ -689,4 +708,5 @@ assigned += M.mind M.mind.restricted_roles = restricted_roles M.mind.special_role = ROLE_HIVE + GLOB.pre_setup_antags += M.mind return TRUE diff --git a/code/game/gamemodes/eldritch_cult/eldritch_cult.dm b/code/game/gamemodes/eldritch_cult/eldritch_cult.dm index c169e11ae737f..fe8a9a8661430 100644 --- a/code/game/gamemodes/eldritch_cult/eldritch_cult.dm +++ b/code/game/gamemodes/eldritch_cult/eldritch_cult.dm @@ -49,6 +49,7 @@ cultie.special_role = ROLE_HERETIC cultie.restricted_roles = restricted_jobs culties += cultie + GLOB.pre_setup_antags += cultie if(!LAZYLEN(culties)) setup_error = "Not enough heretic candidates" @@ -62,6 +63,7 @@ log_game("[key_name(cultie)] has been selected as a heretic!") var/datum/antagonist/heretic/new_antag = new() cultie.add_antag_datum(new_antag) + GLOB.pre_setup_antags -= cultie return ..() /datum/game_mode/heretics/generate_report() diff --git a/code/game/gamemodes/hivemind/hivemind.dm b/code/game/gamemodes/hivemind/hivemind.dm index 90bf74f93cc9b..0224f17be1c2d 100644 --- a/code/game/gamemodes/hivemind/hivemind.dm +++ b/code/game/gamemodes/hivemind/hivemind.dm @@ -66,6 +66,7 @@ GLOBAL_LIST_EMPTY(hivehosts) hosts += host host.special_role = ROLE_HIVE host.restricted_roles = restricted_jobs + GLOB.pre_setup_antags += host log_game("[key_name(host)] has been selected as a hivemind host") antag_candidates.Remove(host) @@ -79,6 +80,7 @@ GLOBAL_LIST_EMPTY(hivehosts) /datum/game_mode/hivemind/post_setup() for(var/datum/mind/i in hosts) i.add_antag_datum(/datum/antagonist/hivemind) + GLOB.pre_setup_antags -= i return ..() /datum/game_mode/hivemind/generate_report() diff --git a/code/game/gamemodes/incursion/incursion.dm b/code/game/gamemodes/incursion/incursion.dm index 137e6408fcd7b..c66f7d2f0f69b 100644 --- a/code/game/gamemodes/incursion/incursion.dm +++ b/code/game/gamemodes/incursion/incursion.dm @@ -47,6 +47,7 @@ team.add_member(incursion) incursion.special_role = ROLE_INCURSION incursion.restricted_roles = restricted_jobs + GLOB.pre_setup_antags += incursion log_game("[key_name(incursion)] has been selected as a member of the incursion") pre_incursionist_team = team gamemode_ready = TRUE @@ -57,6 +58,7 @@ team.forge_team_objectives() for(var/datum/mind/M in team.members) M.add_antag_datum(/datum/antagonist/incursion, team) + GLOB.pre_setup_antags -= M incursion_team = pre_incursionist_team return ..() diff --git a/code/game/gamemodes/revolution/revolution.dm b/code/game/gamemodes/revolution/revolution.dm index 768aac9cb38e7..23e96e0fc4e83 100644 --- a/code/game/gamemodes/revolution/revolution.dm +++ b/code/game/gamemodes/revolution/revolution.dm @@ -64,6 +64,8 @@ setup_error = "Not enough headrev candidates" return FALSE + for(var/antag in headrev_candidates) + GLOB.pre_setup_antags += antag return TRUE /datum/game_mode/revolution/post_setup() @@ -106,6 +108,7 @@ new_head.give_hud = TRUE new_head.remove_clumsy = TRUE rev_mind.add_antag_datum(new_head,revolution) + GLOB.pre_setup_antags -= rev_mind revolution.update_objectives() revolution.update_heads() diff --git a/code/game/gamemodes/traitor/traitor.dm b/code/game/gamemodes/traitor/traitor.dm index 8dd96f37b8078..2be3d1bd3bb68 100644 --- a/code/game/gamemodes/traitor/traitor.dm +++ b/code/game/gamemodes/traitor/traitor.dm @@ -61,6 +61,7 @@ pre_traitors += traitor traitor.special_role = traitor_name traitor.restricted_roles = restricted_jobs + GLOB.pre_setup_antags += traitor log_game("[key_name(traitor)] has been selected as a [traitor_name]") antag_candidates.Remove(traitor) @@ -77,6 +78,7 @@ for(var/datum/mind/traitor in pre_traitors) var/datum/antagonist/traitor/new_antag = new antag_datum() addtimer(CALLBACK(traitor, TYPE_PROC_REF(/datum/mind, add_antag_datum), new_antag), rand(10,100)) + GLOB.pre_setup_antags -= traitor if(!exchange_blue) exchange_blue = -1 //Block latejoiners from getting exchange objectives ..() diff --git a/code/game/machinery/computer/arena.dm b/code/game/machinery/computer/arena.dm index 846971b4b7057..86312b24e4323 100644 --- a/code/game/machinery/computer/arena.dm +++ b/code/game/machinery/computer/arena.dm @@ -191,7 +191,7 @@ if(!isobserver(oldbody)) return var/mob/living/carbon/human/M = new/mob/living/carbon/human(get_turf(spawnpoint)) - oldbody.client.prefs.active_character.copy_to(M) + oldbody.client.prefs.safe_transfer_prefs_to(M) M.set_species(/datum/species/human) // Could use setting per team M.equipOutfit(outfits[team] ? outfits[team] : default_outfit) M.faction += team //In case anyone wants to add team based stuff to arena special effects diff --git a/code/game/machinery/computer/security.dm b/code/game/machinery/computer/security.dm index 1c1ad45136109..ff114675818b8 100644 --- a/code/game/machinery/computer/security.dm +++ b/code/game/machinery/computer/security.dm @@ -762,7 +762,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 @@ -1001,7 +1001,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/dance_machine.dm b/code/game/machinery/dance_machine.dm index b2f3c68458730..7ac02da9ddf47 100644 --- a/code/game/machinery/dance_machine.dm +++ b/code/game/machinery/dance_machine.dm @@ -437,7 +437,7 @@ continue L.stop_sound_channel(CHANNEL_JUKEBOX) for(var/mob/M as() in hearers(10,src)) - if(!M.client || !(M.client.prefs.toggles & PREFTOGGLE_SOUND_INSTRUMENTS)) + if(!M.client || !M.client.prefs.read_player_preference(/datum/preference/toggle/sound_instruments)) continue if(!(M in rangers)) rangers += M diff --git a/code/game/machinery/race_converter.dm b/code/game/machinery/race_converter.dm index ccc12ae99eea0..e3c8eb3f8239b 100644 --- a/code/game/machinery/race_converter.dm +++ b/code/game/machinery/race_converter.dm @@ -129,7 +129,7 @@ if(brainwash && changed) to_chat(user, "The species controller is locked!") return - var/list/allowed = GLOB.roundstart_races + var/list/allowed = get_selectable_species() if(!dangerous) allowed -= "plasmaman" var/choice = input("Select desired race") as null|anything in allowed diff --git a/code/game/machinery/telecomms/broadcasting.dm b/code/game/machinery/telecomms/broadcasting.dm index edc885eda86dd..e0d21830e4e2d 100644 --- a/code/game/machinery/telecomms/broadcasting.dm +++ b/code/game/machinery/telecomms/broadcasting.dm @@ -173,12 +173,12 @@ // 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)) + if (R.client && R.client.holder && !(R.client.prefs?.read_player_preference(/datum/preference/toggle/chat_radio))) receive -= R // Add observers who have ghost radio enabled. for(var/mob/dead/observer/M in GLOB.player_list) - if(M.client && (M.client.prefs.chat_toggles & CHAT_GHOSTRADIO)) + if(M.client && M.client.prefs.read_player_preference(/datum/preference/toggle/chat_ghostradio)) receive |= M // Render the message and have everybody hear it. diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index a83a117b2373f..015f566fd6d91 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -1047,8 +1047,8 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) openToolTip(user,src,params,title = name,content = "[desc]
Force: [force_string]",theme = "") /obj/item/MouseEntered(location, control, params) - if((item_flags & PICKED_UP || item_flags & IN_STORAGE) && (usr.client.prefs.toggles2 & PREFTOGGLE_2_ENABLE_TIPS) && !QDELETED(src)) - var/timedelay = usr.client.prefs.tip_delay/100 + if((item_flags & PICKED_UP || item_flags & IN_STORAGE) && usr.client.prefs.read_player_preference(/datum/preference/toggle/enable_tooltips) && !QDELETED(src)) + var/timedelay = usr.client.prefs.read_player_preference(/datum/preference/numeric/tooltip_delay)/100 var/user = usr tip_timer = addtimer(CALLBACK(src, PROC_REF(openTip), location, control, params, user), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it. var/mob/living/L = usr @@ -1069,14 +1069,11 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) /obj/item/proc/apply_outline(colour = null) if(!(item_flags & PICKED_UP || item_flags & IN_STORAGE) || QDELETED(src) || isobserver(usr)) return - if(usr.client) - if(!(usr.client.prefs.toggles & PREFTOGGLE_OUTLINE_ENABLED)) - return + if(!usr.client?.prefs?.read_player_preference(/datum/preference/toggle/item_outlines)) + return if(!colour) - if(usr.client) - colour = usr.client.prefs.outline_color - if(!colour) - colour = COLOR_BLUE_GRAY + if(usr?.client?.prefs) + colour = usr.client.prefs.read_player_preference(/datum/preference/color/outline_color) else colour = COLOR_BLUE_GRAY add_filter(HOVER_OUTLINE_FILTER, 1, list(type="outline", size=1, color=colour)) diff --git a/code/game/objects/items/body_egg.dm b/code/game/objects/items/body_egg.dm index a51259a88bbec..19a17837f61c4 100644 --- a/code/game/objects/items/body_egg.dm +++ b/code/game/objects/items/body_egg.dm @@ -14,7 +14,7 @@ src.Insert(loc) return ..() -/obj/item/organ/body_egg/Insert(var/mob/living/carbon/M, special = 0) +/obj/item/organ/body_egg/Insert(var/mob/living/carbon/M, special = 0, pref_load = FALSE) ..() ADD_TRAIT(owner, TRAIT_XENO_HOST, TRAIT_GENERIC) ADD_TRAIT(owner, TRAIT_XENO_IMMUNE, "xeno immune") @@ -22,7 +22,7 @@ owner.med_hud_set_status() INVOKE_ASYNC(src, PROC_REF(AddInfectionImages), owner) -/obj/item/organ/body_egg/Remove(var/mob/living/carbon/M, special = 0) +/obj/item/organ/body_egg/Remove(var/mob/living/carbon/M, special = 0, pref_load = FALSE) if(owner) REMOVE_TRAIT(owner, TRAIT_XENO_HOST, TRAIT_GENERIC) REMOVE_TRAIT(owner, TRAIT_XENO_IMMUNE, "xeno immune") diff --git a/code/game/objects/structures/life_candle.dm b/code/game/objects/structures/life_candle.dm index 76fe98d8ffe48..83a562ace450b 100644 --- a/code/game/objects/structures/life_candle.dm +++ b/code/game/objects/structures/life_candle.dm @@ -80,8 +80,7 @@ if(!body) body = new mob_type(T) var/mob/ghostie = mind.get_ghost(TRUE) - if(ghostie.client?.prefs) - ghostie.client.prefs.active_character.copy_to(body) + ghostie.client?.prefs?.safe_transfer_prefs_to(body) mind.transfer_to(body) else body.forceMove(T) diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm index aab24a3822125..41f17bcd6163c 100644 --- a/code/game/objects/structures/mirror.dm +++ b/code/game/objects/structures/mirror.dm @@ -111,7 +111,8 @@ choosable_races = sort_list(choosable_races) /obj/structure/mirror/magic/lesser/Initialize(mapload) - choosable_races = GLOB.roundstart_races.Copy() + var/list/selectable = get_selectable_species() + choosable_races = selectable.Copy() return ..() /obj/structure/mirror/magic/badmin/Initialize(mapload) diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index 553e7c339c5ef..2336b73be72e6 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -62,7 +62,6 @@ if(fulltile) //Overlay for psychic walls - we can't assign two layers at once - add_overlay(generate_psychic_overlay(src)) setDir() //windows only block while reinforced and fulltile, so we'll use the proc diff --git a/code/game/sound.dm b/code/game/sound.dm index fb014a10fde0c..85bf0a9cb5bc3 100644 --- a/code/game/sound.dm +++ b/code/game/sound.dm @@ -210,7 +210,7 @@ distance_multiplier - Can be used to multiply the distance at which the sound is if (!M.client) continue - if (!ignore_prefs && !(M.client.prefs?.toggles2 & PREFTOGGLE_2_SOUNDTRACK)) + if (!ignore_prefs && !(M.client.prefs?.read_player_preference(/datum/preference/toggle/sound_soundtrack))) continue if (!play_to_lobby && isnewplayer(M)) @@ -256,7 +256,7 @@ distance_multiplier - Can be used to multiply the distance at which the sound is set waitfor = FALSE UNTIL(SSticker.login_music) //wait for SSticker init to set the login music - if(prefs && (prefs.toggles & PREFTOGGLE_SOUND_LOBBY)) + if(prefs?.read_player_preference(/datum/preference/toggle/sound_lobby)) SEND_SOUND(src, sound(SSticker.login_music, repeat = 0, wait = 0, volume = vol, channel = CHANNEL_LOBBYMUSIC)) // MAD JAMS /proc/get_rand_frequency() diff --git a/code/game/turfs/closed/_closed.dm b/code/game/turfs/closed/_closed.dm index 960ce6d18c73e..2d6ed12cb49bf 100644 --- a/code/game/turfs/closed/_closed.dm +++ b/code/game/turfs/closed/_closed.dm @@ -6,11 +6,7 @@ rad_flags = RAD_PROTECT_CONTENTS | RAD_NO_CONTAMINATE rad_insulation = RAD_MEDIUM_INSULATION pass_flags_self = PASSCLOSEDTURF - -/turf/closed/Initialize(mapload) - . = ..() - //Overlay for psychic walls - we can't assign two layers at once - add_overlay(generate_psychic_overlay(src)) + plane = WALL_PLANE /turf/closed/AfterChange() . = ..() diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index b1dd0adb45b25..e6981b1f75dd5 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -785,7 +785,7 @@ if(logout && CONFIG_GET(flag/announce_admin_logout)) string = pick( "Admin logout: [key_name(src)]") - else if(!logout && CONFIG_GET(flag/announce_admin_login) && (prefs.toggles & PREFTOGGLE_ANNOUNCE_LOGIN)) + else if(!logout && CONFIG_GET(flag/announce_admin_login) && prefs?.read_player_preference(/datum/preference/toggle/announce_login)) string = pick( "Admin login: [key_name(src)]") if(string) @@ -796,5 +796,5 @@ if(isnull(sound)) return for(var/client/C as anything in GLOB.admins) - if(C.prefs.toggles & PREFTOGGLE_2_SOUND_ADMINALERT) + if(C.prefs.read_player_preference(/datum/preference/toggle/sound_adminalert)) SEND_SOUND(C, sound) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 8ac60aa88b3de..cf89ae761be0e 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -12,7 +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, /*from useful quick commands, to memes*/ - /client/proc/toggle_hear_radio, /*allows admins to hide all radio output*/ /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*/ @@ -68,13 +67,6 @@ GLOBAL_PROTECT(admin_verbs_admin) /client/proc/toggle_combo_hud, // toggle display of the combination pizza antag and taco sci/med/eng hud /client/proc/toggle_AI_interact, /*toggle admin ability to interact with machines as an AI*/ /datum/admins/proc/open_shuttlepanel, /* Opens shuttle manipulator UI */ - /client/proc/deadchat, - /client/proc/toggleprayers, - /client/proc/toggle_prayer_sound, - /client/proc/colorasay, - /client/proc/resetasaycolor, - /client/proc/toggleadminhelpsound, - /client/proc/toggleadminalertsound, /client/proc/respawn_character, /datum/admins/proc/open_borgopanel, /datum/admins/proc/view_all_circuits, diff --git a/code/modules/admin/poll_management.dm b/code/modules/admin/poll_management.dm index 88bd9a31774ba..9f7bb8e12c940 100644 --- a/code/modules/admin/poll_management.dm +++ b/code/modules/admin/poll_management.dm @@ -328,7 +328,7 @@ SELECT p.text, pv.rating, COUNT(*) 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.toggles2 & PREFTOGGLE_2_FANCY_TGUI) //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_player_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() @@ -638,7 +638,7 @@ SELECT p.text, pv.rating, COUNT(*) 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.toggles2 & PREFTOGGLE_2_FANCY_TGUI) //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_player_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/secrets.dm b/code/modules/admin/secrets.dm index 25da22763d826..2c250bb82d2d9 100644 --- a/code/modules/admin/secrets.dm +++ b/code/modules/admin/secrets.dm @@ -865,7 +865,8 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new) if (length(players)) var/mob/chosen = players[1] if (chosen.client) - chosen.client.prefs.active_character.copy_to(spawnedMob) + if(ishuman(spawnedMob)) + chosen.client.prefs.apply_prefs_to(spawnedMob) spawnedMob.key = chosen.key players -= chosen if (ishuman(spawnedMob) && ispath(humanoutfit, /datum/outfit)) diff --git a/code/modules/admin/sound_emitter.dm b/code/modules/admin/sound_emitter.dm index ecb35b7a03f48..0fee3c6ba8ad4 100644 --- a/code/modules/admin/sound_emitter.dm +++ b/code/modules/admin/sound_emitter.dm @@ -142,7 +142,7 @@ if(SOUND_EMITTER_GLOBAL) hearing_mobs = GLOB.player_list.Copy() for(var/mob/M in hearing_mobs) - if(M.client.prefs.toggles & PREFTOGGLE_SOUND_MIDI) + if(M.client.prefs.read_player_preference(/datum/preference/toggle/sound_midi)) M.playsound_local(M, sound_file, sound_volume, FALSE, channel = CHANNEL_ADMIN, pressure_affected = FALSE) if(user) log_admin("[ADMIN_LOOKUPFLW(user)] activated a sound emitter with file \"[sound_file]\" at [AREACOORD(src)]") diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index 4f2336ab6286d..f4c9f16050273 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -151,7 +151,8 @@ 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') - if(usr.client.prefs.toggles2 & PREFTOGGLE_2_FANCY_TGUI) //some browsers (IE8) have trouble with unsupported css3 elements and DOM methods that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support + var/tgui_fancy = usr.client.prefs.read_player_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') var/list/output = list("
[HrefTokenFormField()]") @@ -291,14 +292,15 @@ banned_from += query_get_banned_roles.item[1] qdel(query_get_banned_roles) var/break_counter = 0 - output += "
" + var/fancy_tgui = usr.client.prefs.read_player_preference(/datum/preference/toggle/tgui_fancy) + output += "
" //all heads are listed twice so have a javascript call to toggle both their checkboxes when one is pressed //for simplicity this also includes the captain even though it doesn't do anything for(var/job in GLOB.command_positions) if(break_counter > 0 && (break_counter % 3 == 0)) output += "
" output += {" "} break_counter++ @@ -311,9 +313,9 @@ "Supply" = GLOB.supply_positions) for(var/department in job_lists) //the first element is the department head so they need the same javascript call as above - output += "
" + output += "
" output += {" "} break_counter = 1 @@ -330,7 +332,7 @@ var/list/headless_job_lists = list("Silicon" = GLOB.nonhuman_positions, "Abstract" = list("Appearance", "Emote", "OOC", "DSAY")) for(var/department in headless_job_lists) - output += "
" + output += "
" break_counter = 0 for(var/job in headless_job_lists[department]) if(break_counter > 0 && (break_counter % 3 == 0)) @@ -350,7 +352,7 @@ ) for(var/department in long_job_lists) - output += "
" + output += "
" break_counter = 0 for(var/job in long_job_lists[department]) if(break_counter > 0 && (break_counter % 10 == 0)) diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index d2e14d6c068f6..e7b6dfdf955af 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -206,7 +206,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/help_tickets/admin, new) //send this msg to all admins for(var/client/X in GLOB.admins) - if(X.prefs.toggles & PREFTOGGLE_SOUND_ADMINHELP) + if(X.prefs.read_player_preference(/datum/preference/toggle/sound_adminhelp)) SEND_SOUND(X, sound(reply_sound)) window_flash(X, ignorepref = TRUE) to_chat(X, diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm index 5699c1b59010c..a5dde9831d127 100644 --- a/code/modules/admin/verbs/adminpm.dm +++ b/code/modules/admin/verbs/adminpm.dm @@ -193,7 +193,7 @@ to_chat(src, "PM to-Admins: [msg]", type = MESSAGE_TYPE_ADMINPM) //play the receiving admin the adminhelp sound (if they have them enabled) - if(recipient.prefs.toggles & PREFTOGGLE_SOUND_ADMINHELP) + if(recipient.prefs.read_player_preference(/datum/preference/toggle/sound_adminhelp)) SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) else diff --git a/code/modules/admin/verbs/adminsay.dm b/code/modules/admin/verbs/adminsay.dm index 7cf7bd128eea6..390b0b570b18d 100644 --- a/code/modules/admin/verbs/adminsay.dm +++ b/code/modules/admin/verbs/adminsay.dm @@ -15,7 +15,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_player_preference(/datum/preference/color/asay_color) + var/custom_asay_color = (CONFIG_GET(flag/allow_admin_asaycolor) && asay_color) ? "" : "" msg = "ADMIN: [key_name(usr, 1)] [ADMIN_FLW(mob)]: [custom_asay_color][msg][custom_asay_color ? "":null]" to_chat(GLOB.admins, msg, allow_linkify = TRUE, type = MESSAGE_TYPE_ADMINCHAT) diff --git a/code/modules/admin/verbs/deadsay.dm b/code/modules/admin/verbs/deadsay.dm index 793b052fb1dab..5b05509de5113 100644 --- a/code/modules/admin/verbs/deadsay.dm +++ b/code/modules/admin/verbs/deadsay.dm @@ -31,7 +31,7 @@ for (var/mob/M in GLOB.player_list) if(isnewplayer(M)) continue - if (M.stat == DEAD || (M.client && M.client.holder && (M.client.prefs.chat_toggles & CHAT_DEAD))) //admins can toggle deadchat on and off. This is a proc in admin.dm and is only give to Administrators and above + if (M.stat == DEAD || (M.client && M.client.holder && M.client.prefs.read_player_preference(/datum/preference/toggle/chat_dead))) //admins can toggle deadchat on and off. This is a proc in admin.dm and is only give to Administrators and above to_chat(M, rendered) SSblackbox.record_feedback("tally", "admin_verb", 1, "Dsay") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/admin/verbs/mentorhelp.dm b/code/modules/admin/verbs/mentorhelp.dm index 9b187bc9b121c..7472b36f7e7db 100644 --- a/code/modules/admin/verbs/mentorhelp.dm +++ b/code/modules/admin/verbs/mentorhelp.dm @@ -163,7 +163,7 @@ GLOBAL_DATUM_INIT(mhelp_tickets, /datum/help_tickets/mentor, new) //send this msg to all admins for(var/client/X in GLOB.mentors | GLOB.admins) - if(X.prefs.toggles & PREFTOGGLE_SOUND_ADMINHELP) + if(X.prefs.read_player_preference(/datum/preference/toggle/sound_adminhelp)) SEND_SOUND(X, sound(reply_sound)) window_flash(X, ignorepref = TRUE) to_chat(X, admin_msg, type = message_type) diff --git a/code/modules/admin/verbs/one_click_antag.dm b/code/modules/admin/verbs/one_click_antag.dm index 59828b52a9e62..e2574d2c5e6e1 100644 --- a/code/modules/admin/verbs/one_click_antag.dm +++ b/code/modules/admin/verbs/one_click_antag.dm @@ -379,7 +379,7 @@ //Spawn the body var/mob/living/carbon/human/ERTOperative = new ertemplate.mobtype(spawnloc) - chosen_candidate.client.prefs.active_character.copy_to(ERTOperative) + chosen_candidate.client.prefs.safe_transfer_prefs_to(ERTOperative, is_antag = TRUE) ERTOperative.key = chosen_candidate.key log_objective(ERTOperative, missionobj.explanation_text) diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm index 951f7c164be43..f08ee0db993e5 100644 --- a/code/modules/admin/verbs/playsound.dm +++ b/code/modules/admin/verbs/playsound.dm @@ -36,7 +36,7 @@ message_admins("[key_name_admin(src)] played sound [S]") for(var/mob/M in GLOB.player_list) - if(M.client.prefs.toggles & PREFTOGGLE_SOUND_MIDI) + if(M.client.prefs.read_player_preference(/datum/preference/toggle/sound_midi)) admin_sound.volume = vol * M.client.admin_music_volume SEND_SOUND(M, admin_sound) admin_sound.volume = vol @@ -142,7 +142,7 @@ for(var/m in GLOB.player_list) var/mob/M = m var/client/C = M.client - if(C.prefs.toggles & PREFTOGGLE_SOUND_MIDI) + if(C.prefs.read_player_preference(/datum/preference/toggle/sound_midi)) if(!stop_web_sounds) C.tgui_panel?.play_music(web_sound_url, music_extra_data) else diff --git a/code/modules/admin/verbs/pray.dm b/code/modules/admin/verbs/pray.dm index 6bbffe9f710cd..49f8cb1326e95 100644 --- a/code/modules/admin/verbs/pray.dm +++ b/code/modules/admin/verbs/pray.dm @@ -44,7 +44,7 @@ msg = "[icon2html(cross, GLOB.admins)][prayer_type][deity ? " (to [deity])" : ""]: [ADMIN_FULLMONTY(src)] [ADMIN_SC(src)]: [msg]" for(var/client/C in GLOB.admins) - if(C.prefs.chat_toggles & CHAT_PRAYER) + if(C.prefs.read_player_preference(/datum/preference/toggle/chat_prayer)) to_chat(C, msg) to_chat(usr, "You pray to the gods: \"[msg_tmp]\"") diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index fd7cb38831f5b..1807c8b20b851 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -432,10 +432,10 @@ Traitors and the like can also be revived with the previous role mostly intact. new_character.age = record_found.fields["age"] new_character.hardset_dna(record_found.fields["identity"], record_found.fields["enzymes"], record_found.fields["name"], record_found.fields["blood_type"], new record_found.fields["species"], record_found.fields["features"], null) else - var/datum/character_save/CS = new() - CS.randomise() - CS.pref_species.random_name(CS.gender, TRUE) - CS.copy_to(new_character) + // TODO tgui-prefs test + randomize_human(new_character) + new_character.real_name = new_character.dna.species.random_name(new_character.gender, TRUE) + new_character.name = new_character.real_name new_character.dna.update_dna_identity() new_character.name = new_character.real_name @@ -881,7 +881,7 @@ Traitors and the like can also be revived with the previous role mostly intact. for(var/datum/atom_hud/antag/H in GLOB.huds) // add antag huds (adding_hud) ? H.add_hud_to(usr) : H.remove_hud_from(usr) - if(prefs.toggles & PREFTOGGLE_COMBOHUD_LIGHTING) + if(prefs?.read_player_preference(/datum/preference/toggle/combohud_lighting)) if(adding_hud) mob.lighting_alpha = LIGHTING_PLANE_ALPHA_INVISIBLE else diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index b2178beaf20d9..ebb79c38e58e9 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -122,7 +122,7 @@ GLOBAL_LIST(admin_antag_list) give_antag_moodies() if(is_banned(owner.current) && replace_banned) replace_banned_player() - else if(owner.current.client?.holder && (CONFIG_GET(flag/auto_deadmin_antagonists) || owner.current.client.prefs?.toggles & PREFTOGGLE_DEADMIN_ANTAGONIST)) + else if(owner.current.client?.holder && (CONFIG_GET(flag/auto_deadmin_antagonists) || owner.current.client.prefs?.read_player_preference(/datum/preference/toggle/deadmin_antagonist))) owner.current.client.holder.auto_deadmin() if(count_against_dynamic_roll_chance && owner.current.stat != DEAD && owner.current.client) owner.current.add_to_current_living_antags() diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm index d2812cb7ecd90..75e748de05d45 100644 --- a/code/modules/antagonists/_common/antag_spawner.dm +++ b/code/modules/antagonists/_common/antag_spawner.dm @@ -74,7 +74,7 @@ /obj/item/antag_spawner/contract/spawn_antag(client/C, turf/T, kind ,datum/mind/user) new /obj/effect/particle_effect/smoke(T) var/mob/living/carbon/human/M = new/mob/living/carbon/human(T) - C.prefs.active_character.copy_to(M) + C.prefs.apply_prefs_to(M) M.key = C.key var/datum/mind/app_mind = M.mind @@ -134,7 +134,7 @@ /obj/item/antag_spawner/nuke_ops/spawn_antag(client/C, turf/T, kind, datum/mind/user) var/mob/living/carbon/human/M = new/mob/living/carbon/human(T) - C.prefs.active_character.copy_to(M) + C.prefs.apply_prefs_to(M) M.key = C.key var/datum/antagonist/nukeop/new_op = new() @@ -153,7 +153,7 @@ /obj/item/antag_spawner/nuke_ops/clown/spawn_antag(client/C, turf/T, kind, datum/mind/user) var/mob/living/carbon/human/M = new/mob/living/carbon/human(T) - C.prefs.active_character.copy_to(M) + C.prefs.apply_prefs_to(M) M.key = C.key var/datum/antagonist/nukeop/clownop/new_op = new /datum/antagonist/nukeop/clownop() @@ -315,7 +315,7 @@ /obj/item/antag_spawner/gangster/spawn_antag(client/C, turf/T, datum/mind/user) var/mob/living/carbon/human/M = new/mob/living/carbon/human(T) if (C) - C.prefs.active_character.copy_to(M) + C.prefs.apply_prefs_to(M) M.key = C.key var/datum/antagonist/gang/alignment = user.has_antag_datum(/datum/antagonist/gang,TRUE) diff --git a/code/modules/antagonists/abductor/abductor.dm b/code/modules/antagonists/abductor/abductor.dm index 0e141346f5cec..a7a0854c31ca2 100644 --- a/code/modules/antagonists/abductor/abductor.dm +++ b/code/modules/antagonists/abductor/abductor.dm @@ -13,7 +13,6 @@ var/landmark_type var/greet_text - /datum/antagonist/abductor/agent name = "Abductor Agent" sub_role = "Agent" diff --git a/code/modules/antagonists/abductor/equipment/gland.dm b/code/modules/antagonists/abductor/equipment/gland.dm index 7a4a1f1ac856b..6f9badfb740cb 100644 --- a/code/modules/antagonists/abductor/equipment/gland.dm +++ b/code/modules/antagonists/abductor/equipment/gland.dm @@ -71,7 +71,7 @@ owner.clear_alert("mind_control") active_mind_control = FALSE -/obj/item/organ/heart/gland/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/gland/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) active = 0 if(initial(uses) == 1) uses = initial(uses) @@ -131,12 +131,12 @@ mind_control_uses = 1 mind_control_duration = 2400 -/obj/item/organ/heart/gland/slime/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/gland/slime/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() owner.faction |= "slime" owner.grant_language(/datum/language/slime, TRUE, TRUE, LANGUAGE_GLAND) -/obj/item/organ/heart/gland/slime/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/gland/slime/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() owner.faction -= "slime" owner.remove_language(/datum/language/slime, TRUE, TRUE, LANGUAGE_GLAND) @@ -298,11 +298,11 @@ mind_control_uses = 2 mind_control_duration = 900 -/obj/item/organ/heart/gland/electric/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/gland/electric/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() ADD_TRAIT(owner, TRAIT_SHOCKIMMUNE, ORGAN_TRAIT) -/obj/item/organ/heart/gland/electric/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/gland/electric/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) REMOVE_TRAIT(owner, TRAIT_SHOCKIMMUNE, ORGAN_TRAIT) ..() diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm index 26e3e967bb685..be3ce6a94c11e 100644 --- a/code/modules/antagonists/changeling/changeling.dm +++ b/code/modules/antagonists/changeling/changeling.dm @@ -356,17 +356,13 @@ var/mob/living/carbon/C = owner.current //only carbons have dna now, so we have to typecaste if(isipc(C)) C.set_species(/datum/species/human) - var/replacementName = random_unique_name(C.gender) - if(C.client.prefs.active_character.custom_names["human"]) - C.fully_replace_character_name(C.real_name, C.client.prefs.active_character.custom_names["human"]) - else - C.fully_replace_character_name(C.real_name, replacementName) + C.fully_replace_character_name(C.real_name, C.client.prefs.read_character_preference(/datum/preference/name/backup_human)) for(var/datum/data/record/E in GLOB.data_core.general) if(E.fields["name"] == C.real_name) E.fields["species"] = "\improper Human" var/client/Clt = C.client var/static/list/show_directions = list(SOUTH, WEST) - var/image = GLOB.data_core.get_id_photo(C, Clt, show_directions, TRUE) + var/image = GLOB.data_core.get_id_photo(C, Clt, show_directions)// TODO tgui-prefs test var/datum/picture/pf = new var/datum/picture/ps = new pf.picture_name = "[C]" diff --git a/code/modules/antagonists/clock_cult/items/brass_clothing.dm b/code/modules/antagonists/clock_cult/items/brass_clothing.dm index a28b243622283..63fc145414b75 100644 --- a/code/modules/antagonists/clock_cult/items/brass_clothing.dm +++ b/code/modules/antagonists/clock_cult/items/brass_clothing.dm @@ -9,10 +9,14 @@ w_class = WEIGHT_CLASS_BULKY body_parts_covered = CHEST|GROIN|LEGS|ARMS allowed = list(/obj/item/clockwork, /obj/item/stack/sheet/brass, /obj/item/clockwork, /obj/item/gun/ballistic/bow/clockwork) + var/allow_any = FALSE + +/obj/item/clothing/suit/clockwork/anyone + allow_any = TRUE /obj/item/clothing/suit/clockwork/equipped(mob/living/user, slot) . = ..() - if(!is_servant_of_ratvar(user)) + if(!is_servant_of_ratvar(user) && !allow_any) to_chat(user, "You feel a shock of energy surge through your body!") user.dropItemToGround(src, TRUE) var/mob/living/carbon/C = user diff --git a/code/modules/antagonists/clock_cult/items/clockwork_weapon.dm b/code/modules/antagonists/clock_cult/items/clockwork_weapon.dm index 0bfc15f918264..5906f8a4d9551 100644 --- a/code/modules/antagonists/clock_cult/items/clockwork_weapon.dm +++ b/code/modules/antagonists/clock_cult/items/clockwork_weapon.dm @@ -23,6 +23,8 @@ /obj/item/clockwork/weapon/pickup(mob/user) ..() + if(!user.mind) + return user.mind.RemoveSpell(SS) if(is_servant_of_ratvar(user)) SS = new diff --git a/code/modules/antagonists/creep/creep.dm b/code/modules/antagonists/creep/creep.dm index fedb5f8e8251b..1fdd86b3f6469 100644 --- a/code/modules/antagonists/creep/creep.dm +++ b/code/modules/antagonists/creep/creep.dm @@ -48,7 +48,7 @@ var/mob/living/M = mob_override || owner.current update_obsession_icons_removed(M) -/datum/antagonist/obsessed/proc/forge_objectives(var/datum/mind/obsessionmind) +/datum/antagonist/obsessed/proc/forge_objectives(datum/mind/obsessionmind) var/list/objectives_left = list("spendtime", "polaroid", "hug") var/datum/objective/assassinate/obsessed/kill = new kill.owner = owner diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm index db8b0dcf1ddc4..8707208263474 100644 --- a/code/modules/antagonists/cult/cult.dm +++ b/code/modules/antagonists/cult/cult.dm @@ -70,7 +70,6 @@ if(cult_team.blood_target && cult_team.blood_target_image && current.client) current.client.images += cult_team.blood_target_image - /datum/antagonist/cult/proc/equip_cultist(metal=TRUE) var/mob/living/carbon/C = owner.current if(!istype(C)) diff --git a/code/modules/antagonists/cult/cult_items.dm b/code/modules/antagonists/cult/cult_items.dm index 54c45b721b998..ddd669818fc8d 100644 --- a/code/modules/antagonists/cult/cult_items.dm +++ b/code/modules/antagonists/cult/cult_items.dm @@ -392,6 +392,11 @@ Striking a noncultist, however, will tear their flesh."} body_parts_covered = CHEST|GROIN|LEGS|ARMS allowed = list(/obj/item/tome, /obj/item/melee/cultblade) hoodtype = /obj/item/clothing/head/hooded/cult_hoodie + /// if anyone can equip this. used by the prefs menu + var/allow_any = FALSE + +/obj/item/clothing/suit/hooded/cultrobes/cult_shield/anyone + allow_any = TRUE /obj/item/clothing/suit/hooded/cultrobes/cult_shield/Initialize() . = ..() @@ -415,7 +420,7 @@ Striking a noncultist, however, will tear their flesh."} /obj/item/clothing/suit/hooded/cultrobes/cult_shield/equipped(mob/living/user, slot) ..() - if(!iscultist(user)) + if(!iscultist(user) && !allow_any) to_chat(user, "\"I wouldn't advise that.\"") to_chat(user, "An overwhelming sense of nausea overpowers you!") user.dropItemToGround(src, TRUE) diff --git a/code/modules/antagonists/ert/ert.dm b/code/modules/antagonists/ert/ert.dm index 8271aa19dd4e5..fdfc6b97c62e6 100644 --- a/code/modules/antagonists/ert/ert.dm +++ b/code/modules/antagonists/ert/ert.dm @@ -38,8 +38,8 @@ /datum/antagonist/ert/proc/update_name() var/name = pick(name_source) if (!name) - name = owner.current.client?.prefs.active_character.custom_names["human"] || pick(GLOB.last_names) - owner.current.fully_replace_character_name(owner.current.real_name,"[role] [name]") + name = owner.current.client?.prefs.read_character_preference(/datum/preference/name/backup_human) || pick(GLOB.last_names) + owner.current.fully_replace_character_name(owner.current.real_name, "[role] [name]") /datum/antagonist/ert/deathsquad/New() . = ..() diff --git a/code/modules/antagonists/fugitive/hunter_outfits.dm b/code/modules/antagonists/fugitive/hunter_outfits.dm index ed9b08c8a5ff2..4078e35b564b3 100644 --- a/code/modules/antagonists/fugitive/hunter_outfits.dm +++ b/code/modules/antagonists/fugitive/hunter_outfits.dm @@ -38,7 +38,9 @@ mask = /obj/item/clothing/mask/gas/sechailer/spacepol glasses = /obj/item/clothing/glasses/hud/security/sunglasses -/datum/outfit/spacepol/officer/pre_equip(mob/living/carbon/human/H) +/datum/outfit/spacepol/officer/pre_equip(mob/living/carbon/human/H, visualsOnly = FALSE) + if(visualsOnly) + return if(prob(40)) head = /obj/item/clothing/head/helmet/alt else if(prob(20)) @@ -107,7 +109,9 @@ back = /obj/item/storage/backpack/satchel/leather box = /obj/item/storage/box/survival -/datum/outfit/russian_hunter/pre_equip(mob/living/carbon/human/H) +/datum/outfit/russian_hunter/pre_equip(mob/living/carbon/human/H, visualsOnly = FALSE) + if(visualsOnly) + return if(prob(50)) head = /obj/item/clothing/head/ushanka else if(prob(20)) @@ -128,7 +132,9 @@ suit = /obj/item/clothing/suit/security/officer/russian head = /obj/item/clothing/head/helmet/rus_ushanka -/datum/outfit/russian_hunter/leader/pre_equip(mob/living/carbon/human/H) +/datum/outfit/russian_hunter/leader/pre_equip(mob/living/carbon/human/H, visualsOnly = FALSE) + if(visualsOnly) + return if(prob(50)) gloves = /obj/item/clothing/gloves/combat else if(prob(30)) diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm index 10b5d4c383fed..4041dde1a5484 100644 --- a/code/modules/antagonists/heretic/heretic_antag.dm +++ b/code/modules/antagonists/heretic/heretic_antag.dm @@ -147,11 +147,11 @@ if(isipc(owner.current))//Due to IPCs having a mechanical heart it messes with the living heart, so no IPC heretics for now var/mob/living/carbon/C = owner.current //only carbons have dna now, so we have to typecast C.set_species(/datum/species/human) - var/replacementName = random_unique_name(C.gender) - if(C.client.prefs.active_character.custom_names["human"]) - C.fully_replace_character_name(C.real_name, C.client.prefs.active_character.custom_names["human"]) + var/prefs_name = C.client?.prefs?.read_character_preference(/datum/preference/name/backup_human) + if(prefs_name) + C.fully_replace_character_name(C.real_name, prefs_name) else - C.fully_replace_character_name(C.real_name, replacementName) + C.fully_replace_character_name(C.real_name, random_unique_name(C.gender)) if(give_objectives) forge_objectives() @@ -698,17 +698,6 @@ return completed || (num_we_have >= 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/equipper, visualsOnly) - var/obj/item/clothing/suit/hooded/hooded = locate() in equipper - hooded.MakeHood() // This is usually created on Initialize, but we run before atoms - hooded.ToggleHood() - /datum/action/innate/hereticmenu name = "Forbidden Knowledge" desc = "Utilize your connection to the beyond to unlock new eldritch abilities" diff --git a/code/modules/antagonists/role_preference/_role_preference.dm b/code/modules/antagonists/role_preference/_role_preference.dm index 710ce5d2430d6..37d8cc2437a16 100644 --- a/code/modules/antagonists/role_preference/_role_preference.dm +++ b/code/modules/antagonists/role_preference/_role_preference.dm @@ -1,13 +1,55 @@ /datum/role_preference var/name + /// A brief description of this role, to display in the preferences menu. + var/description /// What heading to display this entry under in the preferences menu. Use ROLE_PREFERENCE_CATEGORY defines. var/category - /// The Antagonist datum typepath for this entry, if there is one. Used to get data about the role for display (bans etc) - var/datum/antagonist/antag_datum /// The base abstract path for this subtype. var/abstract_type = /datum/role_preference + /// The Antagonist datum typepath for this entry, if there is one. Used to get data about the role for display (bans etc) + var/datum/antagonist/antag_datum /// If this preference can vary between characters. var/per_character = FALSE + /// The typepath for the outfit to show in the preview for the preferences menu. + var/preview_outfit + /// Role preference path to use the icon of, if we're duplicating another. + var/use_icon + +/// 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/role_preference/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/role_preference/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/role_preference/proc/get_preview_icon() + if (isnull(preview_outfit)) + return null + + return finish_preview_icon(render_preview_outfit(preview_outfit)) /// Includes latejoin and roundstart antagonists /datum/role_preference/antagonist diff --git a/code/modules/antagonists/role_preference/role_antagonists.dm b/code/modules/antagonists/role_preference/role_antagonists.dm index b952e5ad233a7..f132e808cc5bb 100644 --- a/code/modules/antagonists/role_preference/role_antagonists.dm +++ b/code/modules/antagonists/role_preference/role_antagonists.dm @@ -1,43 +1,387 @@ +#define TRAITOR_DESC "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." +#define TRAITOR_DESC_DETAILS "Start with a set of sinister objectives and an uplink to purchase \ + items to get the job done." + +/datum/role_preference/antagonist/traitor + name = "Traitor" + description = TRAITOR_DESC + "\n" + TRAITOR_DESC_DETAILS + antag_datum = /datum/antagonist/traitor + preview_outfit = /datum/outfit/traitor + +/datum/role_preference/midround_living/traitor + name = "Syndicate Sleeper Agent" + description = TRAITOR_DESC + "\n" + TRAITOR_DESC_DETAILS + antag_datum = /datum/antagonist/traitor + use_icon = /datum/role_preference/antagonist/traitor + +#undef TRAITOR_DESC + +/datum/role_preference/antagonist/internal_affairs + name = "Internal Affairs Agent" + description = "A traitor who was actually hired by Nanotrasen to stage a Syndicate attack.\n" + TRAITOR_DESC_DETAILS + antag_datum = /datum/antagonist/traitor/internal_affairs + use_icon = /datum/role_preference/antagonist/traitor + category = ROLE_PREFERENCE_CATEGORY_LEGACY + +/datum/outfit/traitor + name = "Traitor (Preview only)" + + uniform = /obj/item/clothing/under/syndicate + gloves = /obj/item/clothing/gloves/combat + mask = /obj/item/clothing/mask/gas + l_hand = /obj/item/melee/transforming/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/transforming/energy/sword/sword = locate() in H.held_items + sword.icon_state = "swordred" + H.update_inv_hands() + H.hair_style = "Messy" + H.hair_color = "431" + H.update_hair() + +/datum/role_preference/antagonist/changeling + name = "Changeling" + description = "A highly intelligent alien predator that is capable of altering their \ + shape to flawlessly resemble a human.\n\ + Transform yourself or others into different identities, and buy from an \ + arsenal of biological weaponry with the DNA you collect." + antag_datum = /datum/antagonist/changeling + +/datum/role_preference/antagonist/changeling/get_preview_icon() + var/icon/final_icon = render_preview_outfit(/datum/outfit/medical_doctor_changeling_preview) + var/icon/split_icon = render_preview_outfit(/datum/outfit/job/engineer) + + final_icon.Shift(WEST, world.icon_size / 2) + final_icon.Shift(EAST, world.icon_size / 2) + + split_icon.Shift(EAST, world.icon_size / 2) + split_icon.Shift(WEST, world.icon_size / 2) + + final_icon.Blend(split_icon, ICON_OVERLAY) + + return finish_preview_icon(final_icon) + +/datum/outfit/medical_doctor_changeling_preview + name = "Medical Doctor Changeling (Preview only)" + uniform = /obj/item/clothing/under/rank/medical/doctor + suit = /obj/item/clothing/suit/toggle/labcoat/med + gloves = /obj/item/clothing/gloves/color/latex/nitrile + r_hand = /obj/item/melee/arm_blade + +/datum/outfit/medical_doctor_changeling_preview/post_equip(mob/living/carbon/human/H, visualsOnly) + H.dna.features["mcolor"] = "8d8" + H.dna.features["horns"] = "Short" + H.dna.features["frills"] = "Simple" + H.set_species(/datum/species/lizard) + /datum/role_preference/antagonist/blood_brother name = "Blood Brother" + description = "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." antag_datum = /datum/antagonist/brother +/datum/role_preference/antagonist/blood_brother/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/brother1 = new + var/mob/living/carbon/human/dummy/consistent/brother2 = new + + brother1.hair_style = "Pigtails" + brother1.hair_color = "532" + brother1.update_hair() + + 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/role_preference/antagonist/blood_cultist name = "Blood Cultist" + description = "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!\n\ + Armed with blood magic, convert crew members to the Blood Cult, sacrifice \ + those who get in the way, and summon Nar-Sie." antag_datum = /datum/antagonist/cult +/datum/role_preference/antagonist/blood_cultist/get_preview_icon() + var/icon/icon = render_preview_outfit(/datum/outfit/blood_cult_preview) + + // The longsword is 64x64, but getFlatIcon crunches to 32x32. + // So I'm just going to add it in post, screw it. + + // Center the dude, because item icon states start from the center. + // This makes the image 64x64. + icon.Crop(-15, -15, 48, 48) + + var/obj/item/melee/cultblade/longsword = new + icon.Blend(icon(longsword.lefthand_file, longsword.item_state), ICON_OVERLAY) + qdel(longsword) + + // Move the guy back to the bottom left, 32x32. + icon.Crop(17, 17, 48, 48) + + return finish_preview_icon(icon) + +/datum/outfit/blood_cult_preview + name = "Blood Cultist (Preview only)" + uniform = /obj/item/clothing/under/syndicate + suit = /obj/item/clothing/suit/hooded/cultrobes/cult_shield/anyone + head = /obj/item/clothing/head/hooded/cult_hoodie + r_hand = /obj/item/melee/blood_magic/stun + l_hand = /obj/item/shield/mirror + +/datum/outfit/blood_cult_preview/post_equip(mob/living/carbon/human/H, visualsOnly) + H.eye_color = BLOODCULT_EYE + H.update_body() + /datum/role_preference/antagonist/clock_cultist name = "Clock Cultist" + description = "Hailing from the clockwork city of Reebe, serve your god, Ratvar. \ + Gather power to summon an avatar of Ratvar through the clockwork rift!\n\ + Drop down among the station to install cogs into APCs to gain power. Be careful, as when the rift opens, \ + the crew will rush into Reebe! Build defenses to slow down their entry." antag_datum = /datum/antagonist/servant_of_ratvar + preview_outfit = /datum/outfit/clockcult_preview + +/datum/outfit/clockcult_preview + name = "Servant of Ratvar (Preview only)" + uniform = /obj/item/clothing/under/rank/engineering/engineer + belt = /obj/item/storage/belt/utility + suit = /obj/item/clothing/suit/clockwork/anyone + l_hand = /obj/item/clockwork/weapon/brass_spear + head = /obj/item/clothing/head/helmet/clockcult + gloves = /obj/item/clothing/gloves/clockcult /datum/role_preference/antagonist/devil name = "Devil" + description = "Sign deals with crewmembers, turn them to the side of the Devil." antag_datum = /datum/antagonist/devil + preview_outfit = /datum/outfit/devil_preview + category = ROLE_PREFERENCE_CATEGORY_LEGACY + +/datum/outfit/devil_preview + name = "Devil (Preview only)" + uniform = /obj/item/clothing/under/rank/civilian/lawyer/black + r_hand = /obj/item/storage/briefcase + +/datum/outfit/devil_preview/post_equip(mob/living/carbon/human/H, visualsOnly) + H.dna.features["mcolor"] = "511" + H.dna.features["horns"] = "Simple" + H.set_species(/datum/species/lizard) /datum/role_preference/antagonist/revolutionary name = "Head Revolutionary" + description = "Armed with a flash, convert as many people to the revolution as you can.\n\ + Kill or exile all heads of staff on the station." antag_datum = /datum/antagonist/rev/head + preview_outfit = /datum/outfit/revolutionary + category = ROLE_PREFERENCE_CATEGORY_LEGACY + +/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 + +/datum/role_preference/antagonist/revolutionary/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/role_preference/antagonist/revolutionary/proc/make_assistant_icon(hair_style) + var/mob/living/carbon/human/dummy/consistent/assistant = new + assistant.hair_style = hair_style + 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/role_preference/antagonist/heretic name = "Heretic" + description = "Find hidden influences and sacrifice crew members to gain magical \ + powers and ascend as one of several paths. \n\ + Forgotten, devoured, gutted. Humanity has forgotten the eldritch forces \ + of decay, but the mansus veil has weakened. We will make them taste fear \ + again..." antag_datum = /datum/antagonist/heretic +/datum/role_preference/antagonist/heretic/get_preview_icon() + var/icon/icon = render_preview_outfit(/datum/outfit/heretic_preview) + + // 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/ash/blade = new + icon.Blend(icon(blade.lefthand_file, blade.item_state), ICON_OVERLAY) + qdel(blade) + + // Move the guy back to the bottom left, 32x32. + icon.Crop(17, 17, 48, 48) + + return finish_preview_icon(icon) + +/datum/outfit/heretic_preview + name = "Heretic (Preview only)" + suit = /obj/item/clothing/suit/hooded/cultrobes/eldritch + head = /obj/item/clothing/head/hooded/cult_hoodie/eldritch + r_hand = /obj/item/melee/touch_attack/mansus_fist + /datum/role_preference/antagonist/hivemind_host name = "Hivemind Host" + description = "A powerful host of a Hivemind. Assimilate crew into your hive to grow your power. \ + Use the members of your hive as machines in your objectives, and work with or against other Hiveminds on the station." antag_datum = /datum/antagonist/hivemind + category = ROLE_PREFERENCE_CATEGORY_LEGACY + +/datum/role_preference/antagonist/hivemind_host/get_preview_icon() + var/icon/background = icon('icons/effects/hivemind.dmi', "awoken") + var/icon/outfit = render_preview_outfit(/datum/outfit/hivemind_host_preview) + background.Blend(outfit, ICON_OVERLAY) + return finish_preview_icon(background) + +/datum/outfit/hivemind_host_preview + name = "Hivemind Host (Preview only)" + glasses = /obj/item/clothing/glasses/sunglasses/advanced/reagent + uniform = /obj/item/clothing/under/rank/civilian/bartender + suit = /obj/item/clothing/suit/armor/vest + +/datum/outfit/hivemind_host_preview/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE) + H.hair_style = "Bob Hair 4" + H.hair_color = "111" + H.gradient_style = "Reflected Inverse" + H.gradient_color = "808" + H.update_hair() /datum/role_preference/antagonist/incursionist name = "Incursionist" + description = "As a member of the Syndicate Incursion, work with your team of agents to accomplish your objectives.\n\ + Use your radio to speak with other members of the incursion, and keep security off your tail. \ + Use your uplink to purchase items, and get the job done." antag_datum = /datum/antagonist/incursion -/datum/role_preference/antagonist/excommunicate - name = "Excommunicated Syndicate Agent" - antag_datum = /datum/antagonist/traitor/excommunicate +/datum/role_preference/antagonist/incursionist/get_preview_icon() + var/icon/final_icon = render_preview_outfit(/datum/outfit/traitor/incursion) + var/icon/dummy_icon = render_preview_outfit(/datum/outfit/traitor) + dummy_icon.ChangeOpacity(0.75) + + final_icon.Blend(dummy_icon, ICON_UNDERLAY, -8, 0) + final_icon.Blend(dummy_icon, ICON_UNDERLAY, 8, 0) + + // Apply the incursion HUD, but scale up the preview icon a bit beforehand. + // Otherwise, the I gets cut off. + final_icon.Scale(64, 64) + + var/icon/inc_icon = icon('icons/mob/hud.dmi', "incursion") + inc_icon.Scale(48, 48) + inc_icon.Crop(1, 1, 64, 64) + inc_icon.Shift(EAST, 8) + inc_icon.Shift(NORTH, 16) + final_icon.Blend(inc_icon, ICON_OVERLAY) + + return finish_preview_icon(final_icon) + +/datum/outfit/traitor/incursion + name = "Incursionist (Preview only)" + uniform = /obj/item/clothing/under/rank/cargo/quartermaster + glasses = /obj/item/clothing/glasses/sunglasses/advanced + head = /obj/item/clothing/head/ushanka + mask = null /datum/role_preference/antagonist/gangster name = "Gangster" + description = "Convince people to join your gang, wear your uniform, tag turf for the gang, and accomplish your gang's goals." antag_datum = /datum/antagonist/gang + preview_outfit = /datum/outfit/gangster_preview + category = ROLE_PREFERENCE_CATEGORY_LEGACY -/datum/role_preference/antagonist/internal_affairs - name = "Internal Affairs Agent" - antag_datum = /datum/antagonist/traitor/internal_affairs +/datum/outfit/gangster_preview + name = "Gangster (Preview only)" + head = /obj/item/clothing/head/beanie/black + uniform = /obj/item/clothing/under/syndicate/combat + suit = /obj/item/clothing/suit/jacket + +/datum/role_preference/antagonist/nuclear_operative + name = "Nuclear Operative" + description = "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.\n\ + Retrieve the nuclear authentication disk, use it to activate the nuclear \ + fission explosive, and destroy the station." + antag_datum = /datum/antagonist/nukeop + +/datum/role_preference/antagonist/nuclear_operative/get_preview_icon() + var/icon/final_icon = icon('icons/effects/effects.dmi', "nothing") + var/icon/foreground = render_preview_outfit(/datum/outfit/nuclear_operative) + var/icon/background = icon(foreground) + background.Blend(rgb(206, 206, 206, 220), ICON_MULTIPLY) + + final_icon.Blend(background, ICON_OVERLAY, -world.icon_size / 4, 0) + final_icon.Blend(background, ICON_OVERLAY, world.icon_size / 4, 0) + final_icon.Blend(foreground, ICON_OVERLAY, 0, 0) + + 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/role_preference/antagonist/wizard + name = "Wizard" + description = "GREETINGS. WE'RE THE WIZARDS OF THE WIZARD'S FEDERATION.\n\ + Choose between a variety of powerful spells in order to cause chaos among Space Station 13." + antag_datum = /datum/antagonist/wizard + preview_outfit = /datum/outfit/wizard + +/datum/role_preference/antagonist/excommunicate + name = "Excommunicate Agent" + description = "A traitor who has been declared an excommunicate of the Syndicate. You're being hunted down by an incursion... watch your back.\n" + TRAITOR_DESC_DETAILS + antag_datum = /datum/antagonist/traitor/excommunicate + use_icon = /datum/role_preference/antagonist/traitor + +#undef TRAITOR_DESC_DETAILS diff --git a/code/modules/antagonists/role_preference/role_changeling.dm b/code/modules/antagonists/role_preference/role_changeling.dm deleted file mode 100644 index cf0399b0bcf72..0000000000000 --- a/code/modules/antagonists/role_preference/role_changeling.dm +++ /dev/null @@ -1,3 +0,0 @@ -/datum/role_preference/antagonist/changeling - name = "Changeling" - antag_datum = /datum/antagonist/changeling diff --git a/code/modules/antagonists/role_preference/role_midrounds.dm b/code/modules/antagonists/role_preference/role_midrounds.dm index 2d75b9d747396..874fad0c1093d 100644 --- a/code/modules/antagonists/role_preference/role_midrounds.dm +++ b/code/modules/antagonists/role_preference/role_midrounds.dm @@ -1,67 +1,303 @@ /datum/role_preference/midround_ghost/blob name = "Blob" + description = "The blob infests the station and destroys everything in its path, including \ + hull, fixtures, and creatures.\n\ + 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!" antag_datum = /datum/antagonist/blob +/datum/role_preference/midround_ghost/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/role_preference/midround_ghost/xenomorph name = "Xenomorph" + description = "Become the extraterrestrial xenomorph. Start as a larva, and progress \ + your way up the caste, including even the Queen!" antag_datum = /datum/antagonist/xeno +/datum/role_preference/midround_ghost/xenomorph/get_preview_icon() + return finish_preview_icon(icon('icons/mob/alien.dmi', "alienh")) + /datum/role_preference/midround_ghost/nightmare name = "Nightmare" + description = "Use your light eater to break sources of light to survive and thrive. \ + Jaunt through the darkness and seek your prey with night vision." antag_datum = /datum/antagonist/nightmare + preview_outfit = /datum/outfit/nightmare + +/datum/outfit/nightmare + name = "Nightmare (Preview only)" + +/datum/outfit/nightmare/post_equip(mob/living/carbon/human/human, visualsOnly) + human.set_species(/datum/species/shadow/nightmare) /datum/role_preference/midround_ghost/space_dragon name = "Space Dragon" + description = "Become a ferocious space dragon. Breathe fire, summon an army of space \ + carps, crush walls, and terrorize the station." antag_datum = /datum/antagonist/space_dragon +/datum/role_preference/midround_ghost/space_dragon/get_preview_icon() + var/icon/icon = icon('icons/mob/spacedragon.dmi', "spacedragon") + + icon.Blend("#7848bb", 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/role_preference/midround_ghost/nuclear_operative + name = "Nuclear Operative (Midround)" + description = "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.\n\ + Retrieve the nuclear authentication disk, use it to activate the nuclear \ + fission explosive, and destroy the station." + antag_datum = /datum/antagonist/nukeop + use_icon = /datum/role_preference/antagonist/nuclear_operative + +/datum/role_preference/midround_ghost/wizard + name = "Wizard (Midround)" + description = "GREETINGS. WE'RE THE WIZARDS OF THE WIZARD'S FEDERATION.\n\ + Choose between a variety of powerful spells in order to cause chaos among Space Station 13." + antag_datum = /datum/antagonist/wizard + use_icon = /datum/role_preference/antagonist/wizard + /datum/role_preference/midround_ghost/abductor name = "Abductor" + description = "Abductors are technologically advanced alien society set on cataloging \ + all species in the system. Unfortunately for their subjects their methods \ + are quite invasive. \n\ + 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." antag_datum = /datum/antagonist/abductor +/datum/role_preference/midround_ghost/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/role_preference/midround_ghost/space_pirate name = "Space Pirate" + description = "Gather your crewmates and infiltrate Space Station 13's vault. \ + Loot that booty, and don't get gunned down in the process!" antag_datum = /datum/antagonist/pirate +/datum/role_preference/midround_ghost/space_pirate/get_preview_icon() + var/icon/final_icon = icon('icons/effects/effects.dmi', "nothing") + var/icon/foreground = render_preview_outfit(/datum/outfit/pirate_space_preview/captain) + var/icon/background = render_preview_outfit(/datum/outfit/pirate_space_preview) + background.Blend(rgb(206, 206, 206, 220), ICON_MULTIPLY) + + final_icon.Blend(background, ICON_OVERLAY, -world.icon_size / 4, 0) + final_icon.Blend(background, ICON_OVERLAY, world.icon_size / 4, 0) + final_icon.Blend(foreground, ICON_OVERLAY, 0, 0) + + return finish_preview_icon(final_icon) + +/datum/outfit/pirate_space_preview + name = "Space Pirate (Preview only)" + uniform = /obj/item/clothing/under/costume/pirate + suit = /obj/item/clothing/suit/space/pirate + head = /obj/item/clothing/head/helmet/space/pirate/bandana + glasses = /obj/item/clothing/glasses/eyepatch + +/datum/outfit/pirate_space_preview/post_equip(mob/living/carbon/human/H, visualsOnly) + H.set_species(/datum/species/skeleton) + +/datum/outfit/pirate_space_preview/captain + name = "Space Pirate Captain (Preview only)" + head = /obj/item/clothing/head/helmet/space/pirate + /datum/role_preference/midround_ghost/revenant name = "Revenant" + description = "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." antag_datum = /datum/antagonist/revenant +/datum/role_preference/midround_ghost/revenant/get_preview_icon() + return finish_preview_icon(icon('icons/mob/mob.dmi', "revenant_revealed")) + /datum/role_preference/midround_ghost/spider name = "Spider" + description = "Swarm and spread your webs accross every corner of the station. \ + Work with your cluster of fellow spiders, each with different roles - melee, venom, webbing, and egg-laying." antag_datum = /datum/antagonist/spider +/datum/role_preference/midround_ghost/spider/get_preview_icon() + return finish_preview_icon(icon('icons/mob/animal.dmi', "broodmother")) + /datum/role_preference/midround_ghost/swarmer name = "Swarmer" - antag_datum = /datum/antagonist/swarmer + description = "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. \n\ + Consume machines, structures, walls, anything to get materials. Replicate \ + as many swarmers as you can to repeat the process." + +/datum/role_preference/midround_ghost/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) /datum/role_preference/midround_ghost/morph name = "Morph" + description = "Eat everything in your sights, confuse the crew with your shapeshifting abilities and hallucination toxin, \ + and chow down on dead things to heal." antag_datum = /datum/antagonist/morph +/datum/role_preference/midround_ghost/morph/get_preview_icon() + var/icon/morph_icon = icon('icons/mob/animal.dmi', "morph") + morph_icon.Shift(NORTH, 8) + return finish_preview_icon(morph_icon) + /datum/role_preference/midround_ghost/fugitive name = "Fugitive" + description = "You're a fugitive, escaped from imprisonment. You've managed to make it to Space Station 13. \ + Now is the time to run and hide. But be careful, the Fugitive Hunters are hot on your tail." antag_datum = /datum/antagonist/fugitive + preview_outfit = /datum/outfit/waldo /datum/role_preference/midround_ghost/fugitive_hunter name = "Fugitive Hunter" + description = "You've been hired to hunt down the Fugitives who have escaped aboard Space Station 13. \ + Find them, and bring them to the bluespace capture console aboard your shuttle. Cooperate with the station crew if necessary." antag_datum = /datum/antagonist/fugitive_hunter +/datum/role_preference/midround_ghost/fugitive_hunter/get_preview_icon() + var/icon/final_icon = icon('icons/effects/effects.dmi', "nothing") + var/icon/foreground = render_preview_outfit(/datum/outfit/bounty/hook) + var/icon/background = render_preview_outfit(/datum/outfit/russian_hunter/leader) + var/icon/background_2 = render_preview_outfit(/datum/outfit/spacepol/sergeant) + background.Blend(rgb(206, 206, 206, 220), ICON_MULTIPLY) + background_2.Blend(rgb(206, 206, 206, 220), ICON_MULTIPLY) + + final_icon.Blend(background, ICON_OVERLAY, -world.icon_size / 4, 0) + final_icon.Blend(background_2, ICON_OVERLAY, world.icon_size / 4, 0) + final_icon.Blend(foreground, ICON_OVERLAY, 0, 0) + + return finish_preview_icon(final_icon) + /datum/role_preference/midround_ghost/slaughter_demon name = "Slaughter Demon" + description = "Use your blood jaunt to terrorize the crew, and drag them all to hell." antag_datum = /datum/antagonist/slaughter + category = ROLE_PREFERENCE_CATEGORY_LEGACY + +/datum/role_preference/midround_ghost/slaughter_demon/get_preview_icon() + return finish_preview_icon(icon('icons/mob/mob.dmi', "daemon")) /datum/role_preference/midround_ghost/devil name = "Devil (Midround)" + description = "Sign deals with crewmembers, turn them to the side of the Devil." antag_datum = /datum/antagonist/devil + use_icon = /datum/role_preference/antagonist/devil + category = ROLE_PREFERENCE_CATEGORY_LEGACY /datum/role_preference/midround_ghost/ninja name = "Ninja" + description = "Become a conniving space ninja, equipped with a teleporting 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. Capture beings in your net and get on your way!" antag_datum = /datum/antagonist/ninja + preview_outfit = /datum/outfit/ninja_preview + +/datum/outfit/ninja_preview + name = "Space Ninja (Preview only)" + uniform = /obj/item/clothing/under/color/black + suit = /obj/item/clothing/suit/space/space_ninja + glasses = /obj/item/clothing/glasses/night + mask = /obj/item/clothing/mask/gas/space_ninja + head = /obj/item/clothing/head/helmet/space/space_ninja + gloves = /obj/item/clothing/gloves/space_ninja + back = /obj/item/tank/jetpack/carbondioxide + // No katana because it has trouble GCing + //belt = /obj/item/energy_katana /datum/role_preference/midround_living/malfunctioning_ai name = "Malfunctioning AI" + description = "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." + // Yes, it's under traitor. antag_datum = /datum/antagonist/traitor +/datum/role_preference/midround_living/malfunctioning_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 + /datum/role_preference/midround_living/obsessed name = "Obsessed" + description = "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?" antag_datum = /datum/antagonist/obsessed + +/datum/role_preference/midround_living/obsessed/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/victim_dummy = new + victim_dummy.hair_color = "b96" // Brown + victim_dummy.hair_style = "Messy" + victim_dummy.update_hair() + + var/icon/obsessed_icon = render_preview_outfit(/datum/outfit/obsessed) + 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() diff --git a/code/modules/antagonists/role_preference/role_operative.dm b/code/modules/antagonists/role_preference/role_operative.dm deleted file mode 100644 index 6a4fe7a5b9ec6..0000000000000 --- a/code/modules/antagonists/role_preference/role_operative.dm +++ /dev/null @@ -1,7 +0,0 @@ -/datum/role_preference/antagonist/nuclear_operative - name = "Nuclear Operative" - antag_datum = /datum/antagonist/nukeop - -/datum/role_preference/midround_ghost/nuclear_operative - name = "Nuclear Operative (Midround)" - antag_datum = /datum/antagonist/nukeop diff --git a/code/modules/antagonists/role_preference/role_traitor.dm b/code/modules/antagonists/role_preference/role_traitor.dm deleted file mode 100644 index 46d11945a14c2..0000000000000 --- a/code/modules/antagonists/role_preference/role_traitor.dm +++ /dev/null @@ -1,7 +0,0 @@ -/datum/role_preference/antagonist/traitor - name = "Traitor" - antag_datum = /datum/antagonist/traitor - -/datum/role_preference/midround_living/traitor - name = "Traitor (Sleeper Agent)" - antag_datum = /datum/antagonist/traitor diff --git a/code/modules/antagonists/role_preference/role_wizard.dm b/code/modules/antagonists/role_preference/role_wizard.dm deleted file mode 100644 index 31681493a5870..0000000000000 --- a/code/modules/antagonists/role_preference/role_wizard.dm +++ /dev/null @@ -1,7 +0,0 @@ -/datum/role_preference/antagonist/wizard - name = "Wizard" - antag_datum = /datum/antagonist/wizard - -/datum/role_preference/midround_ghost/wizard - name = "Wizard (Midround)" - antag_datum = /datum/antagonist/wizard diff --git a/code/modules/antagonists/slaughter/slaughter.dm b/code/modules/antagonists/slaughter/slaughter.dm index 13a1d60c98416..fc7269fbf6afb 100644 --- a/code/modules/antagonists/slaughter/slaughter.dm +++ b/code/modules/antagonists/slaughter/slaughter.dm @@ -97,12 +97,12 @@ user.temporarilyRemoveItemFromInventory(src, TRUE) src.Insert(user) //Consuming the heart literally replaces your heart with a demon heart. H A R D C O R E -/obj/item/organ/heart/demon/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/demon/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() if(M.mind) M.mind.AddSpell(new /obj/effect/proc_holder/spell/bloodcrawl(null)) -/obj/item/organ/heart/demon/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/demon/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() if(M.mind) M.mind.RemoveSpell(/obj/effect/proc_holder/spell/bloodcrawl) diff --git a/code/modules/antagonists/xeno/xeno.dm b/code/modules/antagonists/xeno/xeno.dm index d200fc3d4b981..e482ad0c333d1 100644 --- a/code/modules/antagonists/xeno/xeno.dm +++ b/code/modules/antagonists/xeno/xeno.dm @@ -52,7 +52,6 @@ if(owner.antag_hud_icon_state == "xenomorph") set_antag_hud(owner.current, null) - //XENO /mob/living/carbon/alien/mind_initialize() ..() diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm index ddd0acfc54737..4e32beb2e0a3d 100644 --- a/code/modules/asset_cache/asset_list.dm +++ b/code/modules/asset_cache/asset_list.dm @@ -25,6 +25,9 @@ GLOBAL_LIST_EMPTY(asset_datums) /// config can, of course, be disabled. var/cross_round_cachable = FALSE + /// 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() @@ -508,5 +511,31 @@ GLOBAL_LIST_EMPTY(asset_datums) /datum/asset/simple/namespaced/proc/get_htmlloader(filename) return url2htmlloader(SSassets.transport.get_asset_url(filename, assets[filename])) +/// A subtype to generate a JSON file from a list +/datum/asset/json + _abstract = /datum/asset/json + /// The filename, will be suffixed with ".json" + var/name + +/datum/asset/json/send(client) + return SSassets.transport.send_assets(client, "[name].json") + +/datum/asset/json/get_url_mappings() + return list( + "[name].json" = SSassets.transport.get_asset_url("[name].json"), + ) + +/datum/asset/json/register() + var/filename = "data/[name].json" + fdel(filename) + text2file(json_encode(generate()), filename) + SSassets.transport.register_asset("[name].json", fcopy_rsc(filename)) + fdel(filename) + +/// Returns the data that will be JSON encoded +/datum/asset/json/proc/generate() + SHOULD_CALL_PARENT(FALSE) + CRASH("generate() not implemented for [type]!") + #undef ASSET_CROSS_ROUND_CACHE_DIRECTORY diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index 7b5743003f29b..2e2ad9b80a0e5 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -335,7 +335,7 @@ stack_trace("design [D] with icon '[icon_file]' missing state '[icon_state]'") continue #endif - I = icon(icon_file, icon_state, SOUTH) + I = icon(icon_file, icon_state, SOUTH, 1) else // construct the icon and slap it into the resource cache @@ -371,7 +371,7 @@ stack_trace("design [D] with icon '[icon_file]' missing state '[icon_state]'") continue #endif - I = icon(icon_file, icon_state, SOUTH) + I = icon(icon_file, icon_state, SOUTH, 1) // computers (and snowflakes) get their screen and keyboard sprites if (ispath(item, /obj/machinery/computer) || ispath(item, /obj/machinery/power/solar_control)) @@ -380,9 +380,9 @@ var/keyboard = initial(C.icon_keyboard) var/all_states = icon_states(icon_file) if (screen && (screen in all_states)) - I.Blend(icon(icon_file, screen, SOUTH), ICON_OVERLAY) + I.Blend(icon(icon_file, screen, SOUTH, 1), ICON_OVERLAY) if (keyboard && (keyboard in all_states)) - I.Blend(icon(icon_file, keyboard, SOUTH), ICON_OVERLAY) + I.Blend(icon(icon_file, keyboard, SOUTH, 1), ICON_OVERLAY) Insert(initial(D.id), I) @@ -409,38 +409,41 @@ // building icons for each item for (var/k in target_items) var/atom/item = k - if (!ispath(item, /atom)) + var/icon/I = get_display_icon_for(item) + if(!I) continue + var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-") + Insert(imgid, I) - var/icon_file - if (initial(item.greyscale_colors) && initial(item.greyscale_config)) - icon_file = SSgreyscale.GetColoredIconByType(initial(item.greyscale_config), initial(item.greyscale_colors)) - else - icon_file = initial(item.icon) - var/icon_state = initial(item.icon_state) - - #ifdef UNIT_TESTS - var/icon_states_list = icon_states(icon_file) - if (!(icon_state in icon_states_list)) - var/icon_states_string - for (var/an_icon_state in icon_states_list) - if (!icon_states_string) - icon_states_string = "[json_encode(an_icon_state)](\ref[an_icon_state])" - else - icon_states_string += ", [json_encode(an_icon_state)](\ref[an_icon_state])" - - stack_trace("[item] does not have a valid icon state, icon=[icon_file], icon_state=[json_encode(icon_state)](\ref[icon_state]), icon_states=[icon_states_string]") - continue - #endif - - var/icon/I = icon(icon_file, icon_state, SOUTH, 1) - var/c = initial(item.color) - if (!isnull(c) && c != "#FFFFFF") - I.Blend(c, ICON_MULTIPLY) +/proc/get_display_icon_for(atom/item) + if (!ispath(item, /atom)) + return FALSE + var/icon_file + if (initial(item.greyscale_colors) && initial(item.greyscale_config)) + icon_file = SSgreyscale.GetColoredIconByType(initial(item.greyscale_config), initial(item.greyscale_colors)) + else + icon_file = initial(item.icon) + var/icon_state = initial(item.icon_state) + + #ifdef UNIT_TESTS + var/icon_states_list = icon_states(icon_file) + if (!(icon_state in icon_states_list)) + var/icon_states_string + for (var/an_icon_state in icon_states_list) + if (!icon_states_string) + icon_states_string = "[json_encode(an_icon_state)](\ref[an_icon_state])" + else + icon_states_string += ", [json_encode(an_icon_state)](\ref[an_icon_state])" - var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-") + stack_trace("[item] does not have a valid icon state, icon=[icon_file], icon_state=[json_encode(icon_state)](\ref[icon_state]), icon_states=[icon_states_string]") + return FALSE + #endif - Insert(imgid, I) + var/icon/I = icon(icon_file, icon_state, SOUTH, 1) + var/c = initial(item.color) + if (!isnull(c) && c != "#FFFFFF") + I.Blend(c, ICON_MULTIPLY) + return I /datum/asset/spritesheet/crafting name = "crafting" @@ -567,10 +570,9 @@ assets = list() /datum/asset/simple/portraits/New() - if(!SSpersistence.paintings || !SSpersistence.paintings[tab] || !length(SSpersistence.paintings[tab])) + if(!length(SSpersistence.paintings[tab])) return - for(var/p in SSpersistence.paintings[tab]) - var/list/portrait = p + for(var/list/portrait as anything in SSpersistence.paintings[tab]) var/png = "data/paintings/[tab]/[portrait["md5"]].png" if(fexists(png)) var/asset_name = "[tab]_[portrait["md5"]]" diff --git a/code/modules/asset_cache/transports/asset_transport.dm b/code/modules/asset_cache/transports/asset_transport.dm index e2787bbd168b9..66f80396ab407 100644 --- a/code/modules/asset_cache/transports/asset_transport.dm +++ b/code/modules/asset_cache/transports/asset_transport.dm @@ -144,7 +144,7 @@ /// Precache files without clogging up the browse() queue, used for passively sending files on connection start. -/datum/asset_transport/proc/send_assets_slow(client/client, list/files, filerate = 3) +/datum/asset_transport/proc/send_assets_slow(client/client, list/files, filerate = 6) var/startingfilerate = filerate for (var/file in files) if (!client) diff --git a/code/modules/awaymissions/capture_the_flag.dm b/code/modules/awaymissions/capture_the_flag.dm index e0a37fb3ee8a6..e602f3b17804b 100644 --- a/code/modules/awaymissions/capture_the_flag.dm +++ b/code/modules/awaymissions/capture_the_flag.dm @@ -284,7 +284,7 @@ /obj/machinery/capture_the_flag/proc/spawn_team_member(client/new_team_member) var/mob/living/carbon/human/M = new/mob/living/carbon/human(get_turf(src)) - new_team_member.prefs.active_character.copy_to(M) + new_team_member.prefs.apply_prefs_to(M) if(!(M.dna.species.type in allowed_species)) M.set_species(/datum/species/human) //default to human if not whitelisted M.key = new_team_member.key diff --git a/code/modules/awaymissions/super_secret_room.dm b/code/modules/awaymissions/super_secret_room.dm index ee37438848ead..81f6a5f036d98 100644 --- a/code/modules/awaymissions/super_secret_room.dm +++ b/code/modules/awaymissions/super_secret_room.dm @@ -126,7 +126,7 @@ /obj/item/rupee/Initialize(mapload) . = ..() - var/newcolor = color2hex(pick(10;"green", 5;"blue", 3;"red", 1;"purple")) + var/newcolor = pick(10;COLOR_GREEN, 5;COLOR_BLUE, 3;COLOR_RED, 1;COLOR_PURPLE) add_atom_colour(newcolor, FIXED_COLOUR_PRIORITY) var/static/list/loc_connections = list( COMSIG_ATOM_ENTERED = PROC_REF(on_entered), diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index f02bafb04ee13..7ade6f31f3fe4 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -121,3 +121,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 1e834c1a7edbd..8fcdc104c5355 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -122,13 +122,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( hsrc = mentor_datum 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) @@ -142,11 +135,10 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( ..() //redirect to hsrc.Topic() +/// If this client is BYOND member. /client/proc/is_content_unlocked() - if(!prefs.unlock_content) - to_chat(src, "Become a BYOND member to access member-perks and features, as well as support the engine that makes this game possible. Only 10 bucks for 3 months! Click Here to find out more.") - return 0 - return 1 + return prefs.unlock_content + /* * Call back proc that should be checked in all paths where a client can send messages * @@ -256,12 +248,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 prefs.handle_donator_items() @@ -414,7 +406,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( add_admin_verbs() to_chat(src, get_message_output("memo")) adminGreet() - add_verbs_from_config() var/cached_player_age = set_client_age_from_db(tdata) //we have to cache this because other shit may change it and we need it's current value now down below. @@ -467,8 +458,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(!winexists(src, "asset_cache_browser")) // The client is using a custom skin, tell them. to_chat(src, "Unable to access asset cache browser, if you are using a custom skin file, please allow DS to download the updated version, if you are not, then make a bug report. This is not a critical issue but can cause issues with resource downloading, as it is impossible to know when extra resources arrived to you.") - update_ambience_pref() - //This is down here because of the browse() calls in tooltip/New() if(!tooltips) tooltips = new /datum/tooltip(src) @@ -955,11 +944,13 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( to_chat(src, "Your previous click was ignored because you've done too many in a second") return - if (prefs.toggles2 & PREFTOGGLE_2_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. winset(src, null, "input.background-color=[COLOR_INPUT_DISABLED]") + else + winset(src, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED]") ..() @@ -1042,7 +1033,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if (isliving(mob)) var/mob/living/M = mob M.update_damage_hud() - if (prefs.toggles2 & PREFTOGGLE_2_AUTO_FIT_VIEWPORT) + if (prefs.read_player_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() @@ -1056,7 +1047,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( void.UpdateGreed(actualview[1],actualview[2]) /client/proc/AnnouncePR(announcement) - if(prefs && prefs.chat_toggles & CHAT_PULLR) + if(prefs && prefs.read_player_preference(/datum/preference/toggle/chat_pullr)) to_chat(src, announcement) /client/proc/show_character_previews(mutable_appearance/source) @@ -1156,12 +1147,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( holder.particool = new /datum/particle_editor(in_atom) holder.particool.ui_interact(mob) -/client/proc/update_ambience_pref() - if(prefs.toggles & PREFTOGGLE_SOUND_AMBIENCE) - SSambience.add_ambience_client(src) - else - SSambience.remove_ambience_client(src) - /client/proc/give_award(achievement_type, mob/user) return player_details.achievements.unlock(achievement_type, user) diff --git a/code/modules/client/loadout/loadout.dm b/code/modules/client/loadout/loadout.dm index e49fd1374fa34..7fbf25d69755a 100644 --- a/code/modules/client/loadout/loadout.dm +++ b/code/modules/client/loadout/loadout.dm @@ -61,6 +61,10 @@ GLOBAL_LIST_EMPTY(gear_datums) var/skirt_display_name var/skirt_path = null var/skirt_description + /// If this gear is actually granting an item, and can be equipped. + var/is_equippable = TRUE + /// If this gear can be purchased again - used for non-items + var/multi_purchase = FALSE /datum/gear/New() ..() @@ -73,6 +77,7 @@ GLOBAL_LIST_EMPTY(gear_datums) skirt_description = initial(O.desc) /datum/gear/proc/purchase(var/client/C) //Called when the gear is first purchased + SHOULD_NOT_SLEEP(TRUE) return /datum/gear_data diff --git a/code/modules/client/loadout/loadout_ooc.dm b/code/modules/client/loadout/loadout_ooc.dm index 475fc188cfa52..5d0557a93df4c 100644 --- a/code/modules/client/loadout/loadout_ooc.dm +++ b/code/modules/client/loadout/loadout_ooc.dm @@ -2,21 +2,26 @@ subtype_path = /datum/gear/ooc sort_category = "OOC" cost = 10000 + is_equippable = FALSE /datum/gear/ooc/char_slot display_name = "extra character slot" description = "An extra charslot. Pretty self-explanatory." cost = 10000 + path = /obj/item/toy/figure/captain /datum/gear/ooc/char_slot/purchase(var/client/C) - C?.prefs?.set_max_character_slots(C.prefs.max_usable_slots + 1) + // This is only locally immediately after purchase - this will be incremented on load in preferences.dm + C.prefs.max_save_slots += 1 /datum/gear/ooc/real_antagtoken display_name = "antag token" description = "If you can afford it, you deserve it." cost = 100000 + path = /obj/item/coin/antagtoken + multi_purchase = TRUE /datum/gear/ooc/real_antagtoken/purchase(var/client/C) - C.inc_antag_token_count(1) + INVOKE_ASYNC(C, TYPE_PROC_REF(/client, inc_antag_token_count), 1) message_admins("[C.ckey] has purchased a genuine antag token.") log_game("[C.ckey] has purchased a genuine antag token.") diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm deleted file mode 100644 index 6ca86f5c1c09d..0000000000000 --- a/code/modules/client/preferences.dm +++ /dev/null @@ -1,2250 +0,0 @@ -GLOBAL_LIST_EMPTY(preferences_datums) - -/datum/preferences - var/client/parent - - var/default_slot = 1 //Holder so it doesn't default to slot 1, rather the last one used - // TREAT THIS VAR AS PRIVATE. USE set_max_character_slots() PLEASE - var/max_usable_slots = 3 - - //non-preference stuff - var/muted = 0 - var/last_ip - var/last_id - - /// List of all character saves, with list index being slot ID - var/list/datum/character_save/character_saves = list() - /// Active character, ref to an item in that list - var/datum/character_save/active_character - - //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. - var/tip_delay = 500 //tip delay in milliseconds - - //Antag preferences - var/list/role_preferences = list() //Special role selection - - var/UI_style = null - var/outline_color = COLOR_BLUE_GRAY - - ///Whether we want balloon alerts displayed alone, with chat or not displayed at all - var/see_balloon_alerts = BALLOON_ALERT_ALWAYS - - var/toggles = TOGGLES_DEFAULT - var/toggles2 = TOGGLES_2_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/preferred_map = null - var/pda_theme = THEME_NTOS - var/pda_color = "#808000" - - // Custom Keybindings - var/list/key_bindings = null - - // 0 = character settings, 1 = game preferences - var/current_tab = 0 - - var/unlock_content = 0 - - var/list/ignoring = list() - - var/clientfps = 40 - var/updated_fps = 0 - - var/parallax - - ///What size should pixels be displayed as? 0 is strech to fit - var/pixel_size = 0 - ///What scaling method should we use? - var/scaling_method = "normal" - - var/list/exp = list() - var/job_exempt = 0 - - //Loadout stuff - var/list/purchased_gear = list() - var/gear_tab = "General" - - var/action_buttons_screen_locs = list() - - var/pai_name = "" - var/pai_description = "" - var/pai_comment = "" - -/datum/preferences/proc/set_max_character_slots(newmax) - max_usable_slots = min(TRUE_MAX_SAVE_SLOTS, newmax) // Make sure they dont go over - check_usable_slots() - -/datum/preferences/New(client/C) - parent = C - - character_saves.len = TRUE_MAX_SAVE_SLOTS - for(var/i in 1 to TRUE_MAX_SAVE_SLOTS) - var/datum/character_save/CS = new() - CS.slot_number = i - character_saves[i] = CS - - UI_style = GLOB.available_ui_styles[1] - if(istype(C)) - if(!IS_GUEST_KEY(C.key)) - unlock_content = C.IsByondMember() - if(unlock_content) - set_max_character_slots(8) - else if(!length(key_bindings)) // Guests need default keybinds - key_bindings = deep_copy_list(GLOB.keybinding_list_by_key) - var/loaded_preferences_successfully = load_from_database() - if(loaded_preferences_successfully) - if("6030fe461e610e2be3a2c3e75c06067e" in purchased_gear) //MD5 hash of, "extra character slot" - set_max_character_slots(max_usable_slots + 1) - if(load_characters()) // inside this proc is a disgusting SQL query - var/datum/character_save/target_save = character_saves[default_slot] - if(target_save && !target_save.slot_locked) - active_character = target_save - else - active_character = character_saves[1] // Default to first if unavailable - return - - //we couldn't load character data so just randomize the character appearance + name - active_character = character_saves[1] - var/fallback_default_species = CONFIG_GET(string/fallback_default_species) - if(!active_character.pref_species && fallback_default_species != "random") - var/datum/species/spath = GLOB.species_list[fallback_default_species || "human"] - active_character.pref_species = new spath - active_character.randomise() //let's create a random character then - rather than a fat, bald and naked man. - active_character.real_name = active_character.pref_species.random_name(active_character.gender, TRUE) - if(!loaded_preferences_successfully) - save_preferences() - active_character.save(C) //let's save this new random character so it doesn't keep generating new ones. - return - -#define APPEARANCE_CATEGORY_COLUMN "" -#define MAX_MUTANT_ROWS 4 - -/datum/preferences/proc/ShowChoices(mob/user) - if(!user || !user.client) - return - active_character.update_preview_icon(user.client) - var/list/dat = list(TOOLTIP_CSS_SETUP, "
") - - dat += "Character Settings" - dat += "Antagonist Preferences" - dat += "Game Preferences" - var/shop_name = "[CONFIG_GET(string/metacurrency_name)] Shop" - dat += "[shop_name]" - dat += "OOC Preferences" - - dat += "
" - - dat += "
" - - switch(current_tab) - if (0) // Character Settings# - dat += "
" - var/name - var/unspaced_slots = 0 - for(var/datum/character_save/CS as anything in character_saves) - unspaced_slots++ - if(unspaced_slots > 4) - dat += "
" - unspaced_slots = 0 - name = CS.real_name - if(!name) - name = "Character [CS.slot_number]" - if(CS.slot_locked) - dat += "[name] (Locked) " - else - dat += "[name] " - dat += "
" - - dat += "

Occupation Choices

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

Quirk Setup

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

Identity

" - dat += "" - - dat += "
" - if(is_banned_from(user.ckey, "Appearance")) - dat += "You are banned from using custom names and appearances. You can continue to adjust your characters, but you will be randomised once you join the game.
" - dat += "Random Name " - dat += "Always Random Name: [active_character.be_random_name ? "Yes" : "No"]
" - - dat += "[TOOLTIP_CONFIG_CALLER("Name:", 400, "preferences.naming_policy")] " - dat += "[active_character.real_name]
" - - if(!(AGENDER in active_character.pref_species.species_traits)) - var/dispGender - if(active_character.gender == MALE) - dispGender = "Male" - else if(active_character.gender == FEMALE) - dispGender = "Female" - else - dispGender = "Other" - dat += "Gender: [dispGender]
" - if(active_character.gender == PLURAL || active_character.gender == NEUTER) - dat += "Body Model:[active_character.features["body_model"] == MALE ? "Masculine" : "Feminine"]
" - dat += "Age: [active_character.age]
" - - dat += "Special Names:
" - var/old_group - for(var/custom_name_id in GLOB.preferences_custom_names) - var/namedata = GLOB.preferences_custom_names[custom_name_id] - if(!old_group) - old_group = namedata["group"] - else if(old_group != namedata["group"]) - old_group = namedata["group"] - dat += "
" - dat += "[namedata["pref_name"]]: [active_character.custom_names[custom_name_id]] " - dat += "

" - - dat += "Custom Job Preferences:
" - dat += "Preferred AI Core Display: [active_character.preferred_ai_core_display]
" - dat += "Preferred Security Department: [active_character.preferred_security_department]
" - - dat += "

Body

" - dat += "Random Body " - dat += "Always Random Body: [active_character.be_random_body ? "Yes" : "No"]
" - - dat += "" - - var/use_skintones = active_character.pref_species.use_skintones - if(use_skintones) - - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Skin Tone

" - - dat += "[active_character.skin_tone]
" - - var/mutant_colors - if((MUTCOLORS in active_character.pref_species.species_traits) || (MUTCOLORS_PARTSONLY in active_character.pref_species.species_traits)) - - if(!use_skintones) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Mutant Color

" - - dat += "   Change
" - - mutant_colors = TRUE - - if(istype(active_character.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(istype(active_character.pref_species, /datum/species/plasmaman)) - - if(!use_skintones) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Envirohelmet Type

" - - dat += "[active_character.helmet_style]
" - - if((EYECOLOR in active_character.pref_species.species_traits) && !(NOEYESPRITES in active_character.pref_species.species_traits)) - - if(!use_skintones && !mutant_colors) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Eye Color

" - - dat += "   Change
" - - dat += "" - else if(use_skintones || mutant_colors) - dat += "" - - if(HAIR in active_character.pref_species.species_traits) - - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Hair Style

" - - dat += "[active_character.hair_style]
" - dat += "<>
" - dat += "   Change
" - - dat += "

Gradient Style

" - - dat += "[active_character.gradient_style]
" - dat += "<>
" - dat += "   Change
" - - dat += "

Facial Hair Style

" - - dat += "[active_character.facial_hair_style]
" - dat += "<>
" - dat += "   Change
" - - dat += "" - - //Mutant stuff - var/mutant_category = 0 - - if("tail_lizard" in active_character.pref_species.default_features) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Tail

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

Snout

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

Horns

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

Frills

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

Spines

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

Body Markings

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

Legs

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

Moth wings

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

Moth antennae

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

Moth markings

" - - dat += "[active_character.features["moth_markings"]]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("ipc_screen" in active_character.pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Screen Style

" - - dat += "[active_character.features["ipc_screen"]]
" - - dat += "   Change
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("ipc_antenna" in active_character.pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Antenna Style

" - - dat += "[active_character.features["ipc_antenna"]]
" - - dat += "   Change
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("ipc_chassis" in active_character.pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Chassis Style

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

Tail

" - - dat += "[active_character.features["tail_human"]]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("insect_type" in active_character.pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Insect Type

" - - dat += "[active_character.features["insect_type"]]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("psyphoza_cap" in active_character.pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Cap Type

" - - dat += "[active_character.features["psyphoza_cap"]]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("apid_antenna" in active_character.pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Antenna Style

" - - dat += "[active_character.features["apid_antenna"]]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("apid_stripes" in active_character.pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Stripe Pattern

" - - dat += "[active_character.features["apid_stripes"]]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if("apid_headstripes" in active_character.pref_species.mutant_bodyparts) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Headstripe Pattern

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

Ears

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

Size

" - - dat += "[active_character.features["body_size"]]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if(CONFIG_GET(flag/join_with_mutant_humans)) - - if("wings" in active_character.pref_species.default_features && GLOB.r_wings_list.len >1) - if(!mutant_category) - dat += APPEARANCE_CATEGORY_COLUMN - - dat += "

Wings

" - - dat += "[active_character.features["wings"]]
" - - mutant_category++ - if(mutant_category >= MAX_MUTANT_ROWS) - dat += "" - mutant_category = 0 - - if(mutant_category) - dat += "" - mutant_category = 0 - dat += "
" - - dat += "Species:
[active_character.pref_species.name]
" - - dat += "Underwear:
[active_character.underwear]
" - dat += "Underwear Color:
    Change
" - dat += "Undershirt:
[active_character.undershirt]
" - dat += "Socks:
[active_character.socks]
" - dat += "Backpack:
[active_character.backbag]
" - dat += "Jumpsuit:
[active_character.jumpsuit_style]
" - dat += "Uplink Spawn Location:
[active_character.uplink_spawn_loc == UPLINK_IMPLANT ? UPLINK_IMPLANT_WITH_PRICE : active_character.uplink_spawn_loc]
" - - - if (1) // Game Preferences - dat += "" - // left box - dat += "" // left box closed - - // right box - dat += "" - // right box closed - - dat += "" - dat += "" - dat += "
" - dat += "

General Settings

" - dat += "UI Style: [UI_style]
" - dat += "Outline: [toggles & PREFTOGGLE_OUTLINE_ENABLED ? "Enabled" : "Disabled"]
" - dat += "Outline Color:     Change
" - dat += "Show Runechat Chat Bubbles: [toggles & PREFTOGGLE_RUNECHAT_GLOBAL ? "Enabled" : "Disabled"]
" - dat += "See Runechat for non-mobs: [toggles & PREFTOGGLE_RUNECHAT_NONMOBS ? "Enabled" : "Disabled"]
" - dat += "See Runechat emotes: [toggles & PREFTOGGLE_RUNECHAT_EMOTES ? "Enabled" : "Disabled"]
" - dat += "See Balloon alerts: [see_balloon_alerts]" - dat += "
" - dat += "Action Buttons: [(toggles2 & PREFTOGGLE_2_LOCKED_BUTTONS) ? "Locked In Place" : "Unlocked"]
" - dat += "Hotkey Mode: [(toggles2 & PREFTOGGLE_2_HOTKEYS) ? "Hotkeys" : "Default"]
" - dat += "
" - dat += "PDA Theme: [theme_name_for_id(pda_theme)]
" - dat += "PDA Classic Color:     Change
" - dat += "
" - dat += "Crew Objectives: [(toggles2 & PREFTOGGLE_2_CREW_OBJECTIVES) ? "Yes" : "No"]
" - dat += "
" - dat += "Ghost Ears: [(chat_toggles & CHAT_GHOSTEARS) ? "All Speech" : "Nearest Creatures"]
" - dat += "Ghost Radio: [(chat_toggles & CHAT_GHOSTRADIO) ? "All Messages":"No Messages"]
" - dat += "Ghost Sight: [(chat_toggles & CHAT_GHOSTSIGHT) ? "All Emotes" : "Nearest Creatures"]
" - dat += "Ghost Whispers: [(chat_toggles & CHAT_GHOSTWHISPER) ? "All Speech" : "Nearest Creatures"]
" - dat += "Ghost PDA: [(chat_toggles & CHAT_GHOSTPDA) ? "All Messages" : "Nearest Creatures"]
" - dat += "Ghost Law Changes: [(chat_toggles & CHAT_GHOSTLAWS) ? "All Law Changes" : "No Law Changes"]
" - dat += "Ghost (F) Chat toggle: [(chat_toggles & CHAT_GHOSTFOLLOWMINDLESS) ? "All mobs" : "Only mobs with mind"]
" - - if(unlock_content) - dat += "Ghost Form: [ghost_form]
" - dat += "Ghost Orbit: [ghost_orbit]
" - - var/button_name = "If you see this something went wrong." - switch(ghost_accs) - if(GHOST_ACCS_FULL) - button_name = GHOST_ACCS_FULL_NAME - if(GHOST_ACCS_DIR) - button_name = GHOST_ACCS_DIR_NAME - if(GHOST_ACCS_NONE) - button_name = GHOST_ACCS_NONE_NAME - - dat += "Ghost Accessories: [button_name]
" - - switch(ghost_others) - if(GHOST_OTHERS_THEIR_SETTING) - button_name = GHOST_OTHERS_THEIR_SETTING_NAME - if(GHOST_OTHERS_DEFAULT_SPRITE) - button_name = GHOST_OTHERS_DEFAULT_SPRITE_NAME - if(GHOST_OTHERS_SIMPLE) - button_name = GHOST_OTHERS_SIMPLE_NAME - - dat += "Ghosts of Others: [button_name]
" - dat += "
" - - dat += "Income Updates: [(chat_toggles & CHAT_BANKCARD) ? "Allowed" : "Muted"]
" - dat += "
" - dat += "

TGUI Settings

" - dat += "Monitor Lock: [(toggles2 & PREFTOGGLE_2_LOCKED_TGUI) ? "Primary" : "All"]
" - dat += "Window Style: [(toggles2 & PREFTOGGLE_2_FANCY_TGUI) ? "Fancy (Borderless)" : "System Window"]
" - dat += "
" - dat += "

TGUI Input

" - dat += "Input Engine: [(toggles2 & PREFTOGGLE_2_TGUI_INPUT) ? "TGUI" : "Classic"]
" - dat += "Button Size: [(toggles2 & PREFTOGGLE_2_BIG_BUTTONS) ? "Large" : "Small"]
" - dat += "Button Location: [(toggles2 & PREFTOGGLE_2_SWITCHED_BUTTONS) ? "OK - Cancel" : "Cancel - OK"]
" - dat += "
" - dat += "

TGUI Say

" - dat += "Say Engine: [(toggles2 & PREFTOGGLE_2_TGUI_SAY) ? "TGUI" : "Classic"]
" - dat += "Say Theme: [(toggles2 & PREFTOGGLE_2_SAY_LIGHT_THEME) ? "Light" : "Dark"]
" - dat += "Radio Prefixes: [(toggles2 & PREFTOGGLE_2_SAY_SHOW_PREFIX) ? "Show" : "Hidden"]
" - - dat += "

Graphics Settings

" - dat += "FPS: [clientfps]
" - - dat += "Parallax (Fancy Space): " - switch (parallax) - if (PARALLAX_LOW) - dat += "Low" - if (PARALLAX_MED) - dat += "Medium" - if (PARALLAX_INSANE) - dat += "Insane" - if (PARALLAX_DISABLE) - dat += "Disabled" - else - dat += "High" - dat += "
" - - dat += "Ambient Occlusion: [toggles2 & PREFTOGGLE_2_AMBIENT_OCCLUSION ? "Enabled" : "Disabled"]
" - dat += "Fit Viewport: [toggles2 & PREFTOGGLE_2_AUTO_FIT_VIEWPORT ? "Auto" : "Manual"]
" - - button_name = pixel_size - dat += "Pixel Scaling: [(button_name) ? "Pixel Perfect [button_name]x" : "Stretch to fit"]
" - - switch(scaling_method) - if(SCALING_METHOD_NORMAL) - button_name = "Nearest Neighbor" - if(SCALING_METHOD_DISTORT) - button_name = "Point Sampling" - if(SCALING_METHOD_BLUR) - button_name = "Bilinear" - dat += "Scaling Method: [button_name]
" - - if (CONFIG_GET(flag/maprotation)) - var/p_map = preferred_map - if (!p_map) - p_map = "Default" - if (config.defaultmap) - p_map += " ([config.defaultmap.map_name])" - else - if (p_map in config.maplist) - var/datum/map_config/VM = config.maplist[p_map] - if (!VM) - p_map += " (No longer exists)" - else - p_map = VM.map_name - else - p_map += " (No longer exists)" - if(CONFIG_GET(flag/preference_map_voting)) - dat += "Preferred Map: [p_map]
" - - dat += "
Customize Keybinds
" - - if(4) // antagonist preferences window - dat += "
" - var/name - var/unspaced_slots = 0 - for(var/datum/character_save/CS as anything in character_saves) - unspaced_slots++ - if(unspaced_slots > 4) - dat += "
" - unspaced_slots = 0 - name = CS.real_name - if(!name) - name = "Character [CS.slot_number]" - if(CS.slot_locked) - dat += "[name] (Locked) " - else - dat += "[name] " - dat += "
" - dat += "" - // - dat += "" - // left box closed - - // - // -------------------------------------------- - // Midround antagonists + ghostspawn roles - dat += "" - // right box closed - - dat += "
" - // -------------------------------------------- - // warning pannel - var/ban_antagonists = is_banned_from(parent.ckey, BAN_ROLE_ALL_ANTAGONISTS) - var/ban_forced_antagonists = is_banned_from(parent.ckey, BAN_ROLE_FORCED_ANTAGONISTS) - var/ban_ghost = is_banned_from(parent.ckey, BAN_ROLE_ALL_GHOST) - if(ban_antagonists || ban_forced_antagonists || ban_ghost) - dat += "

Notification

" - if(ban_antagonists) - dat += "You are banned from all antagonist roles.
\ - Show Info
" - if(ban_forced_antagonists) - dat += "You are banned from all forced antagonist roles (such as brainwashing).
\ - Show Info
" - if(ban_ghost) - dat += "You are banned from all non-antagonist ghost roles.
\ - Show Info
" - // -------------------------------------------- - // Antagonist roles - dat += "

Antagonists

" - for (var/typepath in GLOB.role_preference_entries) - var/datum/role_preference/pref = GLOB.role_preference_entries[typepath] - if(pref.category != ROLE_PREFERENCE_CATEGORY_ANAGONIST) - continue - var/ban_key = initial(pref.antag_datum.banning_key) - if(is_banned_from(parent.ckey, ban_key)) - dat += "[pref.name]: BANNED
" - else - dat += "[pref.name] \ -
- Character: [parent.role_preference_enabled(typepath) ? "Enabled" : "Disabled"]\ -
- Global: Enable\ - Disable
" - dat += "
" - dat += "

Midrounds (Living)

" - for (var/typepath in GLOB.role_preference_entries) - var/datum/role_preference/pref = GLOB.role_preference_entries[typepath] - if(pref.category != ROLE_PREFERENCE_CATEGORY_MIDROUND_LIVING) - continue - var/ban_key = initial(pref.antag_datum.banning_key) - if(is_banned_from(parent.ckey, ban_key)) - dat += "[pref.name]: BANNED
" - else - dat += "[pref.name] \ -
- Character: [parent.role_preference_enabled(typepath) ? "Enabled" : "Disabled"]\ -
- Global: Enable\ - Disable
" - dat += "

Midrounds (Ghost)

" - for (var/typepath in GLOB.role_preference_entries) - var/datum/role_preference/pref = GLOB.role_preference_entries[typepath] - if(pref.category != ROLE_PREFERENCE_CATEGORY_MIDROUND_GHOST) - continue - var/ban_key = initial(pref.antag_datum.banning_key) - if(is_banned_from(parent.ckey, ban_key)) - dat += "[pref.name]: BANNED
" - else - dat += "[pref.name]: [parent.role_preference_enabled(typepath) ? "Enabled" : "Disabled"]
" - dat += "
" - - if(2) //Loadout - var/list/type_blacklist = list() - if(length(active_character.equipped_gear)) - for(var/i in 1 to length(active_character.equipped_gear)) - var/datum/gear/G = GLOB.gear_datums[active_character.equipped_gear[i]] - if(G) - if(G.subtype_path in type_blacklist) - continue - type_blacklist += G.subtype_path - else - active_character.equipped_gear.Cut(i,i+1) - - dat += "
" - var/name - var/unspaced_slots = 0 - for(var/datum/character_save/CS as anything in character_saves) - unspaced_slots++ - if(unspaced_slots > 4) - dat += "
" - unspaced_slots = 0 - name = CS.real_name - if(!name) - name = "Character [CS.slot_number]" - if(CS.slot_locked) - dat += "[name] (Locked) " - else - dat += "[name] " - dat += "
" - - var/fcolor = "#3366CC" - var/metabalance = user.client.get_metabalance_db() - dat += "" - dat += "" - dat += "" - - var/datum/loadout_category/LC = GLOB.loadout_categories[gear_tab] - dat += "" - dat += "" - dat += "" - - dat += "" - dat += "" - if(LC.category != "Donator") - dat += "" - dat += "" - dat += "" - dat += "" - for(var/gear_id in LC.gear) - var/datum/gear/G = LC.gear[gear_id] - var/ticked = (G.id in active_character.equipped_gear) - - if(active_character.jumpsuit_style == PREF_SKIRT && !isnull(G.skirt_display_name)) - dat += "" - else - dat += "Equip" - else - dat += "[donator ? "Donator" : "Purchase"]" - dat += "" - else - dat += "" - dat += "
Current balance: [metabalance] [CONFIG_GET(string/metacurrency_name)]s. \[Clear Loadout\]
" - - var/firstcat = 1 - for(var/category in GLOB.loadout_categories) - if(category == "Donator" && (!LAZYLEN(GLOB.patrons) || !CONFIG_GET(flag/donator_items))) - continue - if(firstcat) - firstcat = 0 - else - dat += " |" - if(category == gear_tab) - dat += " [category] " - else - dat += " [category] " - dat += "

[LC.category]


NameCostRestricted JobsDescription

[G.skirt_display_name]\n" - else - dat += "
[G.display_name]\n" - var/donator = G.sort_category == "Donator" // purchase box and cost coloumns doesn't appear on donator items - if(G.id in purchased_gear) - if(G.sort_category == "OOC") - dat += "Purchased.[donator ? "" : "[G.cost]"]" - - if(G.allowed_roles) - dat += "" - for(var/role in G.allowed_roles) - dat += role + ", " - dat += "" - if(active_character.jumpsuit_style == PREF_SKIRT && !isnull(G.skirt_path)) - dat += "[G.skirt_description]
[G.description]
" - - if(3) //OOC Preferences - dat += "" - - if(user.client.holder) - dat +="" - dat += "
" - dat += "

OOC Settings

" - dat += "Window Flashing: [(toggles2 & PREFTOGGLE_2_WINDOW_FLASHING) ? "Enabled":"Disabled"]
" - dat += "
" - dat += "Play Admin MIDIs: [(toggles & PREFTOGGLE_SOUND_MIDI) ? "Enabled":"Disabled"]
" - dat += "Play Lobby Music: [(toggles & PREFTOGGLE_SOUND_LOBBY) ? "Enabled":"Disabled"]
" - dat += "Play Game Soundtrack: [(toggles2 & PREFTOGGLE_2_SOUNDTRACK) ? "Enabled":"Disabled"]
" - dat += "See Pull Requests: [(chat_toggles & CHAT_PULLR) ? "Enabled":"Disabled"]
" - dat += "
" - - - if(user.client) - if(unlock_content) - dat += "BYOND Membership Publicity: [(toggles & PREFTOGGLE_MEMBER_PUBLIC) ? "Public" : "Hidden"]
" - - if(unlock_content || check_rights_for(user.client, R_ADMIN)) - dat += "OOC Color:     Change
" - - dat += "
" - - dat += "

Admin Settings

" - - dat += "Adminhelp Sounds: [(toggles & PREFTOGGLE_SOUND_ADMINHELP)?"Enabled":"Disabled"]
" - dat += "Admin Alert Sounds: [(toggles & PREFTOGGLE_2_SOUND_ADMINALERT)?"Enabled":"Disabled"]
" - dat += "Prayer Sounds: [(toggles & PREFTOGGLE_SOUND_PRAYERS)?"Enabled":"Disabled"]
" - dat += "Announce Login: [(toggles & PREFTOGGLE_ANNOUNCE_LOGIN)?"Enabled":"Disabled"]
" - dat += "
" - dat += "Combo HUD Lighting: [(toggles & PREFTOGGLE_COMBOHUD_LIGHTING)?"Full-bright":"No Change"]
" - dat += "
" - dat += "Hide Dead Chat: [(chat_toggles & CHAT_DEAD)?"Shown":"Hidden"]
" - dat += "Hide Radio Messages: [(chat_toggles & CHAT_RADIO)?"Shown":"Hidden"]
" - dat += "Hide Prayers: [(chat_toggles & CHAT_PRAYER)?"Shown":"Hidden"]
" - if(CONFIG_GET(flag/allow_admin_asaycolor)) - dat += "
" - dat += "ASAY Color:     Change
" - - //deadmin - dat += "

Deadmin While Playing

" - if(CONFIG_GET(flag/auto_deadmin_players)) - dat += "Always Deadmin: FORCED
" - else - dat += "Always Deadmin: [(toggles & PREFTOGGLE_DEADMIN_ALWAYS)?"Enabled":"Disabled"]
" - if(!(toggles & PREFTOGGLE_DEADMIN_ALWAYS)) - dat += "
" - if(!CONFIG_GET(flag/auto_deadmin_antagonists)) - dat += "As Antag: [(toggles & PREFTOGGLE_DEADMIN_ANTAGONIST)?"Deadmin":"Keep Admin"]
" - else - dat += "As Antag: FORCED
" - - if(!CONFIG_GET(flag/auto_deadmin_heads)) - dat += "As Command: [(toggles & PREFTOGGLE_DEADMIN_POSITION_HEAD)?"Deadmin":"Keep Admin"]
" - else - dat += "As Command: FORCED
" - - if(!CONFIG_GET(flag/auto_deadmin_security)) - dat += "As Security: [(toggles & PREFTOGGLE_DEADMIN_POSITION_SECURITY)?"Deadmin":"Keep Admin"]
" - else - dat += "As Security: FORCED
" - - if(!CONFIG_GET(flag/auto_deadmin_silicons)) - dat += "As Silicon: [(toggles & PREFTOGGLE_DEADMIN_POSITION_SILICON)?"Deadmin":"Keep Admin"]
" - else - dat += "As Silicon: FORCED
" - - dat += "
" - - dat += "
" - - if(!IS_GUEST_KEY(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, 830) - popup.set_content(dat.Join()) - popup.open(FALSE) - onclose(user, "preferences_window", src) - -#undef APPEARANCE_CATEGORY_COLUMN -#undef MAX_MUTANT_ROWS - -/datum/preferences/proc/SetChoices(mob/user, limit = 16, list/splitJobs = list(JOB_NAME_CLOWN, JOB_NAME_RESEARCHDIRECTOR), widthPerColumn = 295, height = 620) - if(!SSjob) - return - - //limit - The amount of jobs allowed per column. Defaults to 17 to make it look nice. - //splitJobs - Allows you split the table by job. You can make different tables for each department by including their heads. Defaults to CE to make it look nice. - //widthPerColumn - Screen's width for every column. - //height - Screen's height. - - var/width = widthPerColumn - - var/HTML = "
" - if(SSjob.occupations.len <= 0) - HTML += "The job SSticker is not yet finished creating jobs, please try again later" - HTML += "
Done

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

" // Easier to press up here. - HTML += "" - HTML += "
" // Table within a table for alignment, also allows you to easily add more colomns. - HTML += "" - var/index = -1 - - //The job before the current job. I only use this to get the previous jobs color when I'm filling in blank rows. - var/datum/job/lastJob - - var/datum/job/overflow = SSjob.GetJob(SSjob.overflow_role) - - for(var/datum/job/job in sort_list(SSjob.occupations, GLOBAL_PROC_REF(cmp_job_display_asc))) - if(job.gimmick) //Gimmick jobs run off of a single pref - continue - index += 1 - if((index >= limit) || (job.title in splitJobs)) - width += widthPerColumn - if((index < limit) && (lastJob != null)) - //If the cells were broken up by a job in the splitJob list then it will fill in the rest of the cells with - //the last job's selection color. Creating a rather nice effect. - for(var/i = 0, i < (limit - index), i += 1) - HTML += "" - HTML += "
  
" - index = 0 - - HTML += "" - continue - var/required_playtime_remaining = job.required_playtime_remaining(user.client) - if(required_playtime_remaining) - HTML += "[rank]" - continue - if(!job.player_old_enough(user.client)) - var/available_in_days = job.available_in_days(user.client) - HTML += "[rank]" - continue - if((active_character.job_preferences[overflow] == JP_LOW) && (rank != SSjob.overflow_role) && !is_banned_from(user.ckey, SSjob.overflow_role)) - HTML += "[rank]" - continue - if((rank in GLOB.command_positions) || (rank == JOB_NAME_AI))//Bold head jobs - HTML += "[rank]" - else - HTML += "[rank]" - - HTML += "" - continue - - HTML += "[prefLevelLabel]" - HTML += "" - - for(var/i = 1, i < (limit - index), i += 1) // Finish the column so it is even - HTML += "" - - HTML += "
" - var/rank = job.title - lastJob = job - if(is_banned_from(user.ckey, rank)) - HTML += "[rank] BANNED
\[ [get_exp_format(required_playtime_remaining)] as [job.get_exp_req_type()] \]
\[IN [(available_in_days)] DAYS\]
" - - var/prefLevelLabel = "ERROR" - var/prefLevelColor = "pink" - var/prefUpperLevel = -1 // level to assign on left click - var/prefLowerLevel = -1 // level to assign on right click - - switch(active_character.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 - - HTML += "" - - if(rank == SSjob.overflow_role)//Overflow is special - if(active_character.job_preferences[overflow.title] == JP_LOW) - HTML += "Yes" - else - HTML += "No" - HTML += "
  
" - HTML += "
" - - var/message = "Be an [SSjob.overflow_role] if preferences unavailable" - if(active_character.joblessrole == BERANDOMJOB) - message = "Get random job if preferences unavailable" - else if(active_character.joblessrole == RETURNTOLOBBY) - message = "Return to lobby if preferences unavailable" - HTML += "

[message]
" - HTML += "
Reset Preferences
" - - 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) - - -/datum/preferences/proc/ShowKeybindings(mob/user) - // 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] = 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] - if (!(kb.category in kb_categories)) - kb_categories[kb.category] = list() - kb_categories[kb.category] += list(kb) - - var/HTML = "" - - for (var/category in kb_categories) - HTML += "

[category]

" - for (var/i in kb_categories[category]) - var/datum/keybinding/kb = i - var/bound_key = user_binds[kb.name] - bound_key = (bound_key) ? bound_key : "Unbound" - - HTML += " [bound_key] Default: ( [kb.key] )" - HTML += "
" - - HTML += "

" - HTML += "Close" - HTML += "Reset to default" - HTML += "" - - winshow(user, "keybindings", TRUE) - var/datum/browser/popup = new(user, "keybindings", "
Keybindings
", 500, 900) - popup.set_content(HTML) - popup.open(FALSE) - onclose(user, "keybindings", src) - - -/datum/preferences/proc/CaptureKeybinding(mob/user, datum/keybinding/kb, var/old_key) - var/HTML = {" -
Keybinding: [kb.full_name]
[kb.description]

Press any key to change
Press ESC to clear
- - "} - winshow(user, "capturekeypress", TRUE) - var/datum/browser/popup = new(user, "capturekeypress", "
Keybindings
", 350, 300) - popup.set_content(HTML) - popup.open(FALSE) - onclose(user, "capturekeypress", src) - - -/datum/preferences/proc/SetJobPreferenceLevel(datum/job/job, level) - if (!job) - return FALSE - - if (level == JP_HIGH) // to high - //Set all other high to medium - for(var/j in active_character.job_preferences) - if(active_character.job_preferences[j] == JP_HIGH) - active_character.job_preferences[j] = JP_MEDIUM - //technically break here - - active_character.job_preferences[job.title] = level - return TRUE - - - - -/datum/preferences/proc/UpdateJobPreference(mob/user, role, desiredLvl) - if(!SSjob || SSjob.occupations.len <= 0) - return - var/datum/job/job = SSjob.GetJob(role) - - if(!job) - user << browse(null, "window=mob_occupation") - ShowChoices(user) - return - - if (!isnum_safe(desiredLvl)) - to_chat(user, "UpdateJobPreference - desired level was not a number. Please notify coders!") - ShowChoices(user) - return - - var/jpval = null - switch(desiredLvl) - if(3) - jpval = JP_LOW - if(2) - jpval = JP_MEDIUM - if(1) - jpval = JP_HIGH - - if(role == SSjob.overflow_role) - if(active_character.job_preferences[job.title] == JP_LOW) - jpval = null - else - jpval = JP_LOW - - SetJobPreferenceLevel(job, jpval) - SetChoices(user) - - return 1 - - -/datum/preferences/proc/ResetJobs() - active_character.job_preferences = list() - -/datum/preferences/proc/SetQuirks(mob/user) - if(!SSquirks) - to_chat(user, "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: [length(active_character.all_quirks) ? active_character.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 active_character.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) - active_character.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 active_character.all_quirks) - var/datum/quirk/T = SSquirks.quirks[V] - bal -= initial(T.value) - return bal - -/datum/preferences/proc/GetPositiveQuirkCount() - . = 0 - for(var/q in active_character.all_quirks) - if(SSquirks.quirk_points[q] > 0) - .++ - -/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 && 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, "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(active_character.joblessrole) - if(RETURNTOLOBBY) - if(is_banned_from(user.ckey, SSjob.overflow_role)) - active_character.joblessrole = BERANDOMJOB - else - active_character.joblessrole = BEOVERFLOW - if(BEOVERFLOW) - active_character.joblessrole = BERANDOMJOB - if(BERANDOMJOB) - active_character.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 - for(var/Q in active_character.all_quirks) - if((quirk in L) && (Q in L) && !(Q == quirk)) //two quirks have lined up in the list of the list of quirks that conflict with each other, so return (see quirks.dm for more details) - to_chat(user, "[quirk] is incompatible with [Q].") - return - var/value = SSquirks.quirk_points[quirk] - var/balance = GetQuirkBalance() - if(quirk in active_character.all_quirks) - if(balance + value < 0) - to_chat(user, "Refunding this would cause you to go below your balance!") - return - active_character.all_quirks -= quirk - else - var/is_positive_quirk = SSquirks.quirk_points[quirk] > 0 - if(is_positive_quirk && GetPositiveQuirkCount() >= MAX_QUIRKS) - to_chat(user, "You can't have more than [MAX_QUIRKS] positive quirks!") - return - if(balance - value < 0) - to_chat(user, "You don't have enough balance to gain this quirk!") - return - active_character.all_quirks += quirk - SetQuirks(user) - if("reset") - active_character.all_quirks = list() - SetQuirks(user) - else - SetQuirks(user) - return TRUE - - if(href_list["preference"] == "gear") - if(href_list["purchase_gear"]) - var/datum/gear/TG = GLOB.gear_datums[href_list["purchase_gear"]] - if(TG.sort_category == "Donator") - if(CONFIG_GET(flag/donator_items) && alert(parent, "This item is only accessible to our patrons. Would you like to subscribe?", "Patron Locked", "Yes", "No") == "Yes") - parent.donate() - else if(TG.cost <= user.client.get_metabalance_db()) - purchased_gear += TG.id - TG.purchase(user.client) - user.client.inc_metabalance((TG.cost * -1), TRUE, "Purchased [TG.display_name].") - save_preferences() - else - to_chat(user, "You don't have enough [CONFIG_GET(string/metacurrency_name)]s to purchase \the [TG.display_name]!") - if(href_list["toggle_gear"]) - var/datum/gear/TG = GLOB.gear_datums[href_list["toggle_gear"]] - if(TG.id in active_character.equipped_gear) - active_character.equipped_gear -= TG.id - else - var/list/type_blacklist = list() - var/list/slot_blacklist = list() - for(var/gear_id in active_character.equipped_gear) - var/datum/gear/G = GLOB.gear_datums[gear_id] - if(istype(G)) - if(!(G.subtype_path in type_blacklist)) - type_blacklist += G.subtype_path - if(!(G.slot in slot_blacklist)) - slot_blacklist += G.slot - if((TG.id in purchased_gear)) - if(!(TG.subtype_path in type_blacklist) || !(TG.slot in slot_blacklist)) - active_character.equipped_gear += TG.id - else - to_chat(user, "Can't equip [TG.display_name]. It conflicts with an already-equipped item.") - else - log_href_exploit(user) - active_character.save(user.client) - - else if(href_list["select_category"]) - gear_tab = href_list["select_category"] - else if(href_list["clear_loadout"]) - active_character.equipped_gear.Cut() - active_character.save(user.client) - - ShowChoices(user) - return - - switch(href_list["task"]) - if("random") - switch(href_list["preference"]) - if("name") - active_character.real_name = active_character.pref_species.random_name(active_character.gender, 1) - if("age") - active_character.age = rand(AGE_MIN, AGE_MAX) - if("hair_color") - active_character.hair_color = random_short_color() - if("hair_style") - active_character.hair_style = random_hair_style(active_character.gender) - if("facial") - active_character.facial_hair_color = random_short_color() - if("facial_hair_style") - active_character.facial_hair_style = random_facial_hair_style(active_character.gender) - if("underwear") - active_character.underwear = random_underwear(active_character.gender) - if("underwear_color") - active_character.underwear_color = random_short_color() - if("undershirt") - active_character.undershirt = random_undershirt(active_character.gender) - if("socks") - active_character.socks = random_socks() - if(BODY_ZONE_PRECISE_EYES) - active_character.eye_color = random_eye_color() - if("s_tone") - active_character.skin_tone = random_skin_tone() - if("bag") - active_character.backbag = pick(GLOB.backbaglist) - if("all") - active_character.randomise() - - if("input") - - if(href_list["preference"] in GLOB.preferences_custom_names) - ask_for_custom_name(user,href_list["preference"]) - - if(href_list["preference"] in active_character.pref_species.forced_features) - alert("You cannot change that bodypart for your selected species!") - active_character.features[href_list["preference"]] = active_character.pref_species.forced_features[href_list["preference"]] - return - - 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 = alert("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?",,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 = alert("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?",,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 = reject_bad_name( input(user, "Choose your character's name:", "Character Preference") as text|null , active_character.pref_species.allow_numbers_in_name) - if(new_name) - active_character.real_name = new_name - else - to_chat(user, "Invalid name. Your name should be at least 2 and at most [MAX_NAME_LEN] characters long. It may only contain the characters A-Z, a-z, -, ' and .") - - if("age") - var/new_age = input(user, "Choose your character's age:\n([AGE_MIN]-[AGE_MAX])", "Character Preference") as num|null - if(new_age) - active_character.age = max(min( round(text2num(new_age)), AGE_MAX),AGE_MIN) - - if("hair_color") - var/new_hair = tgui_color_picker(user, "Choose your character's hair colour:", "Character Preference", "#" + active_character.hair_color) - if(new_hair) - active_character.hair_color = sanitize_hexcolor(new_hair) - - if("hair_style") - var/new_hair_style = tgui_input_list(user, "Choose your character's hair style:", "Character Preference", GLOB.hair_styles_list, active_character.hair_style) - if(new_hair_style) - active_character.hair_style = new_hair_style - - if("gradient_style") - var/new_gradient_style - new_gradient_style = input(user, "Choose your character's hair gradient style:", "Character Preference") as null|anything in GLOB.hair_gradients_list - if(new_gradient_style) - active_character.gradient_style = new_gradient_style - - if("gradient_color") - var/new_hair_gradient = tgui_color_picker(user, "Choose your character's hair gradient colour:", "Character Preference", "#" + active_character.gradient_color) - if(new_hair_gradient) - active_character.gradient_color = sanitize_hexcolor(new_hair_gradient) - - if("next_hair_style") - active_character.hair_style = next_list_item(active_character.hair_style, GLOB.hair_styles_list) - - if("previous_hair_style") - active_character.hair_style = previous_list_item(active_character.hair_style, GLOB.hair_styles_list) - - if("next_gradient_style") - active_character.gradient_style = next_list_item(active_character.gradient_style, GLOB.hair_gradients_list) - - if("previous_gradient_style") - active_character.gradient_style = previous_list_item(active_character.gradient_style, GLOB.hair_gradients_list) - - if("facial") - var/new_facial = tgui_color_picker(user, "Choose your character's facial-hair colour:", "Character Preference","#" + active_character.facial_hair_color) - if(new_facial) - active_character.facial_hair_color = sanitize_hexcolor(new_facial) - - if("facial_hair_style") - var/new_facial_hair_style = tgui_input_list(user, "Choose your character's facial-hair style:", "Character Preference", GLOB.facial_hair_styles_list, active_character.facial_hair_style) - if(new_facial_hair_style) - active_character.facial_hair_style = new_facial_hair_style - - if("next_facehair_style") - active_character.facial_hair_style = next_list_item(active_character.facial_hair_style, GLOB.facial_hair_styles_list) - - if("previous_facehair_style") - active_character.facial_hair_style = previous_list_item(active_character.facial_hair_style, GLOB.facial_hair_styles_list) - - if("underwear") - var/new_underwear = tgui_input_list(user, "Choose your character's underwear:", "Character Preference", GLOB.underwear_list, active_character.underwear) - if(new_underwear) - active_character.underwear = new_underwear - - if("underwear_color") - var/new_underwear_color = tgui_color_picker(user, "Choose your character's underwear color:", "Character Preference","#"+active_character.underwear_color) - if(new_underwear_color) - active_character.underwear_color = sanitize_hexcolor(new_underwear_color) - - if("undershirt") - var/new_undershirt = tgui_input_list(user, "Choose your character's undershirt:", "Character Preference", GLOB.undershirt_list, active_character.undershirt) - if(new_undershirt) - active_character.undershirt = new_undershirt - - if("socks") - var/new_socks - new_socks = tgui_input_list(user, "Choose your character's socks:", "Character Preference", GLOB.socks_list, active_character.socks) - if(new_socks) - active_character.socks = new_socks - - if("eyes") - var/new_eyes = tgui_color_picker(user, "Choose your character's eye colour:", "Character Preference","#"+active_character.eye_color) - if(new_eyes) - active_character.eye_color = sanitize_hexcolor(new_eyes) - - if("body_size") - var/new_size = input(user, "Choose your character's height:", "Character Preference") as null|anything in GLOB.body_sizes - if(new_size) - active_character.features["body_size"] = new_size - - if("species") - - var/result = input(user, "Select a species", "Species Selection") as null|anything in GLOB.roundstart_races - - if(result) - var/new_species_type = GLOB.species_list[result] - var/datum/species/new_species = new new_species_type() - - if (!CONFIG_GET(keyed_list/paywall_races)[new_species.id] || IS_PATRON(parent.ckey) || parent.holder) - active_character.pref_species = new_species - //Now that we changed our species, we must verify that the mutant colour is still allowed. - var/temp_hsv = RGBtoHSV(active_character.features["mcolor"]) - if(active_character.features["mcolor"] == "#000" || (!(MUTCOLORS_PARTSONLY in active_character.pref_species.species_traits) && ReadHSV(temp_hsv)[3] < ReadHSV("#7F7F7F")[3])) - active_character.features["mcolor"] = active_character.pref_species.default_color - //Set our forced bodyparts - for(var/forced_part in active_character.pref_species.forced_features) - //Get the forced type - var/forced_type = active_character.pref_species.forced_features[forced_part] - //Apply the forced bodypart. - active_character.features[forced_part] = forced_type - else - if(alert(parent, "This species is only accessible to our patrons. Would you like to subscribe?", "Patron Locked", "Yes", "No") == "Yes") - parent.donate() - - - if("mutant_color") - var/new_mutantcolor = tgui_color_picker(user, "Choose your character's alien/mutant color:", "Character Preference","#"+active_character.features["mcolor"]) - if(new_mutantcolor) - var/temp_hsv = RGBtoHSV(new_mutantcolor) - if(new_mutantcolor == "#000000") - active_character.features["mcolor"] = active_character.pref_species.default_color - else if((MUTCOLORS_PARTSONLY in active_character.pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV("#7F7F7F")[3]) // mutantcolors must be bright, but only if they affect the skin - active_character.features["mcolor"] = sanitize_hexcolor(new_mutantcolor) - else - to_chat(user, "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) - active_character.features["ethcolor"] = GLOB.color_list_ethereal[new_etherealcolor] - - if("helmet_style") - var/style = input(user, "Choose your helmet style", "Character Preference") as null|anything in list(HELMET_DEFAULT, HELMET_MK2, HELMET_PROTECTIVE) - if(style) - active_character.helmet_style = style - - 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) - active_character.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) - active_character.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) - active_character.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) - active_character.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) - active_character.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) - active_character.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) - active_character.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) - active_character.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) - active_character.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) - active_character.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_roundstart_list - if(new_moth_wings) - active_character.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_roundstart_list - if(new_moth_antennae) - active_character.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_roundstart_list - if(new_moth_markings) - active_character.features["moth_markings"] = new_moth_markings - - if("ipc_screen") - var/new_ipc_screen - - new_ipc_screen = input(user, "Choose your character's screen:", "Character Preference") as null|anything in GLOB.ipc_screens_list - - if(new_ipc_screen) - active_character.features["ipc_screen"] = new_ipc_screen - - if("ipc_antenna") - var/new_ipc_antenna - - new_ipc_antenna = input(user, "Choose your character's antenna:", "Character Preference") as null|anything in GLOB.ipc_antennas_list - - if(new_ipc_antenna) - active_character.features["ipc_antenna"] = new_ipc_antenna - - if("ipc_chassis") - var/new_ipc_chassis - - new_ipc_chassis = input(user, "Choose your character's chassis:", "Character Preference") as null|anything in GLOB.ipc_chassis_list - - if(new_ipc_chassis) - active_character.features["ipc_chassis"] = new_ipc_chassis - - if("insect_type") - var/new_insect_type - - new_insect_type = input(user, "Choose your character's species:", "Character Preference") as null|anything in GLOB.insect_type_list - - if(new_insect_type) - active_character.features["insect_type"] = new_insect_type - - if("psyphoza_cap") - var/new_cap - new_cap = input(user, "Choose your character's cap:", "Character Preference") as null|anything in GLOB.psyphoza_cap_list - if(new_cap) - active_character.features["psyphoza_cap"] = new_cap - - if("apid_antenna") - var/new_apid_antenna - - new_apid_antenna = input(user, "Choose your apid antennae:", "Character Preference") as null|anything in GLOB.apid_antenna_list - - if(new_apid_antenna) - active_character.features["apid_antenna"] = new_apid_antenna - - if("apid_stripes") - var/new_apid_stripes - - new_apid_stripes = input(user, "Choose your apid stripes:", "Character Preference") as null|anything in GLOB.apid_stripes_list - - if(new_apid_stripes) - active_character.features["apid_stripes"] = new_apid_stripes - - if("apid_headstripes") - var/new_apid_headstripes - - new_apid_headstripes = input(user, "Choose your apid headstripes:", "Character Preference") as null|anything in GLOB.apid_headstripes_list - - if(new_apid_headstripes) - active_character.features["apid_headstripes"] = new_apid_headstripes - - 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) - active_character.skin_tone = new_s_tone - - if("ooccolor") - var/new_ooccolor = tgui_color_picker(user, "Choose your OOC colour:", "Game Preference",ooccolor) - if(new_ooccolor) - ooccolor = new_ooccolor - - if("asaycolor") - var/new_asaycolor = tgui_color_picker(user, "Choose your ASAY color:", "Game Preference",asaycolor) - if(new_asaycolor) - asaycolor = sanitize_ooccolor(new_asaycolor) - - if("bag") - var/new_backbag = input(user, "Choose your character's style of bag:", "Character Preference") as null|anything in GLOB.backbaglist - if(new_backbag) - active_character.backbag = new_backbag - - if("suit") - if(active_character.jumpsuit_style == PREF_SUIT) - active_character.jumpsuit_style = PREF_SKIRT - else - active_character.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 - active_character.uplink_spawn_loc = new_loc == UPLINK_IMPLANT_WITH_PRICE ? UPLINK_IMPLANT : new_loc - - if("ai_core_icon") - var/ai_core_icon = input(user, "Choose your preferred AI core display screen:", "AI Core Display Screen Selection") as null|anything in GLOB.ai_core_display_screens - "Portrait" - if(ai_core_icon) - active_character.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) - active_character.preferred_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 || SSmapping.config.map_name == VM.map_name) //current map will be excluded from the vote - 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 sort_list(maplist) - if (pickedmap) - preferred_map = maplist[pickedmap] - - if ("clientfps") - var/desiredfps = input(user, "Choose your desired fps. (0 = synced with server tick rate (currently:[world.fps]))", "Character Preference", clientfps) as null|num - if (!isnull(desiredfps)) - clientfps = desiredfps - parent.fps = desiredfps - if("ui") - var/pickedui = input(user, "Choose your UI style.", "Character Preference", UI_style) as null|anything in GLOB.available_ui_styles - if(pickedui) - UI_style = pickedui - if (parent && parent.mob && parent.mob.hud_used) - parent.mob.hud_used.update_ui_style(ui_style2icon(UI_style)) - if("pda_theme") - var/pickedPDAStyle = input(user, "Choose your default PDA theme.", "Character Preference", pda_theme) as null|anything in GLOB.ntos_device_themes_default - if(pickedPDAStyle) - pda_theme = GLOB.ntos_device_themes_default[pickedPDAStyle] - if("pda_color") - var/pickedPDAColor = tgui_color_picker(user, "Choose your default Thinktronic Classic theme background color.", "Character Preference", pda_color) - if(pickedPDAColor) - pda_color = pickedPDAColor - if ("see_balloon_alerts") - var/pickedstyle = input(user, "Choose how you want balloon alerts displayed", "Balloon alert preference", BALLOON_ALERT_ALWAYS) as null|anything in list(BALLOON_ALERT_ALWAYS, BALLOON_ALERT_WITH_CHAT, BALLOON_ALERT_NEVER) - if (!isnull(pickedstyle)) - see_balloon_alerts = pickedstyle - - else - switch(href_list["preference"]) - if("publicity") - if(unlock_content) - toggles ^= PREFTOGGLE_MEMBER_PUBLIC - if("gender") - var/list/friendlyGenders = list("Male" = "male", "Female" = "female", "Other" = "plural") - var/pickedGender = input(user, "Choose your gender.", "Character Preference", active_character.gender) as null|anything in friendlyGenders - if(pickedGender && friendlyGenders[pickedGender] != active_character.gender) - switch(friendlyGenders[pickedGender]) - if("plural") - active_character.features["body_model"] = pick(MALE, FEMALE) - else - active_character.features["body_model"] = friendlyGenders[pickedGender] - active_character.gender = friendlyGenders[pickedGender] - if("body_model") - active_character.features["body_model"] = active_character.features["body_model"] == MALE ? FEMALE : MALE - if("hotkeys") - toggles2 ^= PREFTOGGLE_2_HOTKEYS - if(toggles2 & PREFTOGGLE_2_HOTKEYS) - winset(user, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED] mainwindow.macro=default") - else - winset(user, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED] mainwindow.macro=old_default") - if("action_buttons") - toggles2 ^= PREFTOGGLE_2_LOCKED_BUTTONS - if("tgui_fancy") - toggles2 ^= PREFTOGGLE_2_FANCY_TGUI - if("outline_enabled") - toggles ^= PREFTOGGLE_OUTLINE_ENABLED - if("outline_color") - var/pickedOutlineColor = tgui_color_picker(user, "Choose your outline color.", "General Preference", outline_color) - if(pickedOutlineColor) - outline_color = pickedOutlineColor - if("tgui_lock") - toggles2 ^= PREFTOGGLE_2_LOCKED_TGUI - if("winflash") - toggles2 ^= PREFTOGGLE_2_WINDOW_FLASHING - if("crewobj") - toggles2 ^= PREFTOGGLE_2_WINDOW_FLASHING - - //here lies the badmins - if("hear_adminhelps") - user.client.toggleadminhelpsound() - if("hear_adminalertsounds") - user.client.toggleadminalertsound() - if("hear_prayers") - user.client.toggle_prayer_sound() - if("announce_login") - user.client.toggleannouncelogin() - if("combohud_lighting") - toggles ^= PREFTOGGLE_COMBOHUD_LIGHTING - if("toggle_dead_chat") - user.client.deadchat() - if("toggle_radio_chatter") - user.client.toggle_hear_radio() - if("toggle_prayers") - user.client.toggleprayers() - if("toggle_deadmin_always") - toggles ^= PREFTOGGLE_DEADMIN_ALWAYS - if("toggle_deadmin_antag") - toggles ^= PREFTOGGLE_DEADMIN_ANTAGONIST - if("toggle_deadmin_head") - toggles ^= PREFTOGGLE_DEADMIN_POSITION_HEAD - if("toggle_deadmin_security") - toggles ^= PREFTOGGLE_DEADMIN_POSITION_SECURITY - if("toggle_deadmin_silicon") - toggles ^= PREFTOGGLE_DEADMIN_POSITION_SILICON - - - if("role_preferences") - var/role_preference_type = href_list["role_preference_type"] - var/role_preference_path = text2path(role_preference_type) - var/datum/role_preference/role_pref = GLOB.role_preference_entries[role_preference_path] - if(istype(role_pref)) - var/list/prefsource = role_pref.per_character ? active_character.role_preferences_character : role_preferences - var/current = prefsource["[role_preference_type]"] - if(isnum(current)) - prefsource["[role_preference_type]"] = !current - else // not set, we assume it's on, so turn it off. - prefsource["[role_preference_type]"] = FALSE - - if("role_preferences_enableall") - var/role_preference_type = href_list["role_preference_type"] - var/role_preference_path = text2path(role_preference_type) - var/datum/role_preference/role_pref = GLOB.role_preference_entries[role_preference_path] - if(istype(role_pref) && role_pref.per_character) - for(var/datum/character_save/CS in character_saves) - CS.role_preferences_character["[role_preference_type]"] = TRUE - - if("role_preferences_disableall") - var/role_preference_type = href_list["role_preference_type"] - var/role_preference_path = text2path(role_preference_type) - var/datum/role_preference/role_pref = GLOB.role_preference_entries[role_preference_path] - if(istype(role_pref) && role_pref.per_character) - for(var/datum/character_save/CS in character_saves) - CS.role_preferences_character["[role_preference_type]"] = FALSE - - if("name") - active_character.be_random_name = !active_character.be_random_name - - if("all") - active_character.be_random_body = !active_character.be_random_body - - if("hear_midis") - toggles ^= PREFTOGGLE_SOUND_MIDI - - if("lobby_music") - toggles ^= PREFTOGGLE_SOUND_LOBBY - if((toggles & PREFTOGGLE_SOUND_LOBBY) && user.client && isnewplayer(user)) - user.client.playtitlemusic() - else - user.stop_sound_channel(CHANNEL_LOBBYMUSIC) - - if("soundtrack") - toggles2 ^= PREFTOGGLE_2_SOUNDTRACK - if((toggles2 & PREFTOGGLE_2_SOUNDTRACK)) - user.play_current_soundtrack() - else - user.stop_sound_channel(CHANNEL_SOUNDTRACK) - - 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("ghost_follow") - chat_toggles ^= CHAT_GHOSTFOLLOWMINDLESS - - if("income_pings") - chat_toggles ^= CHAT_BANKCARD - - if("pull_requests") - chat_toggles ^= CHAT_PULLR - - if("tgui_input") - toggles2 ^= PREFTOGGLE_2_TGUI_INPUT - - if("tgui_big_buttons") - toggles2 ^= PREFTOGGLE_2_BIG_BUTTONS - - if("tgui_switched_buttons") - toggles2 ^= PREFTOGGLE_2_SWITCHED_BUTTONS - - if("tgui_say") - toggles2 ^= PREFTOGGLE_2_TGUI_SAY - if(parent) - if(parent.tgui_say) - parent.tgui_say.close() - parent.set_macros() - - if("tgui_say_light") - toggles2 ^= PREFTOGGLE_2_SAY_LIGHT_THEME - if(parent && parent.tgui_say) // change the theme - parent.tgui_say.load() - - if("tgui_say_radio_prefix") - toggles2 ^= PREFTOGGLE_2_SAY_SHOW_PREFIX - if(parent && parent.tgui_say) // update the UI - parent.tgui_say.load() - - if("parallaxup") - parallax = WRAP(parallax + 1, PARALLAX_INSANE, PARALLAX_DISABLE + 1) - if (parent && parent.mob && parent.mob.hud_used) - parent.mob.hud_used.update_parallax_pref(parent.mob) - - if("parallaxdown") - parallax = WRAP(parallax - 1, PARALLAX_INSANE, PARALLAX_DISABLE + 1) - if (parent && parent.mob && parent.mob.hud_used) - parent.mob.hud_used.update_parallax_pref(parent.mob) - - if("ambientocclusion") - toggles2 ^= PREFTOGGLE_2_AMBIENT_OCCLUSION - if(parent && parent.screen && parent.screen.len) - var/atom/movable/screen/plane_master/game_world/game_pm = locate(/atom/movable/screen/plane_master/game_world) in parent.screen - game_pm.backdrop(parent.mob) - // Multiz shadow - var/atom/movable/screen/plane_master/floor/floor_pm = locate(/atom/movable/screen/plane_master/floor) in parent.screen - floor_pm.backdrop(parent.mob) - - if("auto_fit_viewport") - toggles2 ^= PREFTOGGLE_2_AUTO_FIT_VIEWPORT - if((toggles2 & PREFTOGGLE_2_AUTO_FIT_VIEWPORT) && parent) - parent.fit_viewport() - - 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.resetToDefault(getScreenSize(user)) //Fix our viewport size so it doesn't reset on change - - if("scaling_method") - switch(scaling_method) - if(SCALING_METHOD_NORMAL) - scaling_method = SCALING_METHOD_DISTORT - if(SCALING_METHOD_DISTORT) - scaling_method = SCALING_METHOD_BLUR - if(SCALING_METHOD_BLUR) - scaling_method = SCALING_METHOD_NORMAL - user.client.view_size.setZoomMode() - - if("save") - save_preferences() - active_character.save(user.client) - - if("load") - load_from_database() - load_characters() - - if("changeslot") - var/numerical_slot = text2num(href_list["num"]) - var/datum/character_save/CS = character_saves[numerical_slot] - if(CS && !CS.slot_locked) - active_character = CS - default_slot = numerical_slot - // If its fresh, randomise & save it - if(!CS.from_db) - CS.randomise() - CS.save(user.client) - - if("tab") - if (href_list["tab"]) - current_tab = text2num(href_list["tab"]) - - if("keybindings_menu") - ShowKeybindings(user) - return - - 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") - ShowKeybindings(user) - return - - var/clear_key = text2num(href_list["clear_key"]) - var/old_key = href_list["old_key"] - - if(clear_key) - if(old_key != "Unbound") // if it was already set - key_bindings[old_key] -= kb_name - key_bindings["Unbound"] += list(kb_name) - save_preferences() - user << browse(null, "window=capturekeypress") - ShowKeybindings(user) - return - - var/key = href_list["key"] - var/numpad = text2num(href_list["numpad"]) - // TODO: Handle holding shift or alt down - var/AltMod = text2num(href_list["alt"]) ? "Alt-" : "" - var/CtrlMod = text2num(href_list["ctrl"]) ? "Ctrl-" : "" - var/ShiftMod = text2num(href_list["shift"]) ? "Shift-" : "" - // var/key_code = text2num(href_list["key_code"]) - - var/new_key = uppertext(key) - - // This is a mapping from JS keys to Byond - ref: https://keycode.info/ - var/list/_kbMap = list( - "INSERT" = "Insert", "HOME" = "Northwest", "PAGEUP" = "Northeast", - "DEL" = "Delete", "END" = "Southwest", "PAGEDOWN" = "Southeast", - "SPACEBAR" = "Space", "ALT" = "Alt", "SHIFT" = "Shift", "CONTROL" = "Ctrl" - ) - new_key = _kbMap[new_key] ? _kbMap[new_key] : new_key - - if (numpad) - new_key = "Numpad[new_key]" - - var/full_key = "[AltMod][CtrlMod][ShiftMod][new_key]" - if(old_key && (old_key in key_bindings)) - key_bindings[old_key] -= kb_name - key_bindings[full_key] += list(kb_name) - key_bindings[full_key] = sort_list(key_bindings[full_key]) - - save_preferences() - user << browse(null, "window=capturekeypress") - ShowKeybindings(user) - return - - if("keybindings_done") - user << browse(null, "window=keybindings") - - if("keybindings_reset") - key_bindings = deep_copy_list(GLOB.keybinding_list_by_key) - save_preferences() - ShowKeybindings(user) - return - - if("chat_on_map") - toggles ^= PREFTOGGLE_RUNECHAT_GLOBAL - if("see_chat_non_mob") - toggles ^= PREFTOGGLE_RUNECHAT_NONMOBS - if("see_rc_emotes") - toggles ^= PREFTOGGLE_RUNECHAT_EMOTES - - ShowChoices(user) - return 1 - -/datum/preferences/proc/ask_for_custom_name(mob/user,name_id) - var/namedata = GLOB.preferences_custom_names[name_id] - if(!namedata) - return - - var/raw_name = capped_input(user, "Choose your character's [namedata["qdesc"]]:","Character Preference") - if(!raw_name) - if(namedata["allow_null"]) - active_character.custom_names[name_id] = get_default_name(name_id) - else - return - else - var/sanitized_name = reject_bad_name(raw_name,namedata["allow_numbers"]) - if(!sanitized_name) - to_chat(user, "Invalid name. Your name should be at least 2 and at most [MAX_NAME_LEN] characters long. It may only contain the characters A-Z, a-z,[namedata["allow_numbers"] ? ",0-9," : ""] -, ' and .") - return - else - active_character.custom_names[name_id] = sanitized_name - -/// Handles adding and removing donator items from clients -/datum/preferences/proc/handle_donator_items() - var/datum/loadout_category/DLC = GLOB.loadout_categories["Donator"] // stands for donator loadout category but the other def for DLC works too xD - if(!LAZYLEN(GLOB.patrons) || !CONFIG_GET(flag/donator_items)) // donator items are only accesibile by servers with a patreon - return - if(IS_PATRON(parent.ckey) || (parent in GLOB.admins)) - for(var/gear_id in DLC.gear) - var/datum/gear/AG = DLC.gear[gear_id] - if(AG.id in purchased_gear) - continue - purchased_gear += AG.id - AG.purchase(parent) - save_preferences() - else if(length(purchased_gear) || length(active_character.equipped_gear)) - for(var/gear_id in DLC.gear) - var/datum/gear/RG = DLC.gear[gear_id] - active_character.equipped_gear -= RG.id - purchased_gear -= RG.id - save_preferences() diff --git a/code/modules/client/preferences/README.md b/code/modules/client/preferences/README.md new file mode 100644 index 0000000000000..9c40e105abd35 --- /dev/null +++ b/code/modules/client/preferences/README.md @@ -0,0 +1,604 @@ +# Preferences + +Credit to Mothblocks for writing the basis of this document and the preferences system. + +Ported and heavily altered by itsmeow to BeeStation. + +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. + +## Reading Preferences + +Reading preferences is super simple: + +```dm +prefs.read_player_preference(/datum/preference/toggle/sound_ship_ambience) +``` + +The above will read the ship ambiance toggle from player-prefs. If you want a character preference, you need to use `read_character_preference` instead. You can check the type of the preference datum by viewing its `preference_type` var. + +```dm +prefs.read_character_preference(/datum/preference/name/real_name) +``` + +## Writing Preferences (outside the menu) + +You can alter a preference from code using the following code: + +```dm +prefs.update_preference(/datum/preference/toggle/sound_ship_ambience, TRUE) +``` + +This would enable the ship ambience preference. This will also automatically queue a save. + +Altering an undatumized preference (e.g. one stored on the preferences datum itself, like job preferences) should always be followed by `prefs.mark_undatumized_dirty_player()` or `prefs.mark_undatumized_dirty_character()`, to ensure the preference saves. Datumized preferences will automatically save if update_preference is used. + +## 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. `db_key` - The value which will be saved in the database. This will also be the identifier for tgui. +3. `preference_type` - 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. This also affects which getter is used (get_player_preference or get_character_preference) + +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 db_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 + preference_type = PREFERENCE_CHARACTER + db_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 + preference_type = PREFERENCE_CHARACTER + db_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 + preference_type = PREFERENCE_CHARACTER + db_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, FeatureButtonedDropdownInput } from "../base"; + +export const favorite_drink: FeatureChoiced = { + name: "Favorite drink", + component: FeatureButtonedDropdownInput, +}; +``` + +This will create a dropdown input for your preference, including buttons to cycle between options. Do note that if there are less than 4 options this will automatically be flattened into choice buttons. + +### 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 + preference_type = PREFERENCE_CHARACTER + db_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 + preference_type = PREFERENCE_CHARACTER + db_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 + db_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 +``` + +## Color Palettes + +This allows you to predefine color choices, and looks really nice. You can also lock it to specific colors or allow custom colors. + +`StandardizedPalette` props: + +- `choices`: A list of actual values this palette will give to DM. +- `choices_to_hex`: A map of choice keys to their actual hex values, for display purposes. This is not needed if hex_values is true. +- `displayNames`: A map of actual values to display names, for tooltips. +- `onSetValue`: Called when a value is chosen. +- `value`: The currently selected value. +- `hex_values`: A boolean saying if the color provided is a hex color or a string (see: skin color, which is a string) +- `allow_custom`: A boolean saying if you can select a custom color. Only works with hex values. +- `featureId`: The feature ID of this entry. +- `act`: The act() function of this entry. +- `includeHex`: If the hex value should be shown on the tooltip / display name. Useful for custom color presets. + +``` +import { Feature, FeatureValueProps, StandardizedPalette } from '../base'; + +const eyePresets = { + '#aaccff': 'Baby Blue', + '#0099bb': 'Blue-Green', +}; + +export const eye_color: Feature = { + name: 'Eye Color', + small_supplemental: false, + predictable: false, + component: (props: FeatureValueProps) => { + const { handleSetValue, value, featureId, act } = props; + + return ( + + ); + }, +}; +``` + +## Attaching secondary preferences + +Some preferences are attached to other preferences, like hair color to hair styles. This is called a supplementary feature. + +To do this, first set its category to `PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES`: + +```dm +/datum/preference/color_legacy/hair_color + db_key = "hair_color" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES +``` + +Then, on the parent feature, add to its constant data a SUPPLEMENTAL_FEATURE_KEY with the db_key of the supplemental: + +```dm +/datum/preference/choiced/hairstyle/compile_constant_data() + var/list/data = ..() + data[SUPPLEMENTAL_FEATURE_KEY] = "hair_color" + return data +``` + +Now, configure its TGUI entry. `small_supplemental` dictates if it is placed in the top corner or at the bottom of the feature popup. + +`predictable` disables the TGUI-side prediction system that caches the value sent from the UI. This is important if the value sent is expected to be transformed in some way or updates atypically, such as with custom color palettes. + +```js +export const hair_color: Feature = { + name: 'Hair Color', + small_supplemental: false, + predictable: false, + component: /* ... */, +}; +``` + +## Game Preferences + +Most of the documentation above covers character preferences. Game preferences have a few unique features as well, such as descriptions and subcategories. + +Here is an example: + +```js +export const chat_radio: FeatureToggle = { + name: "Hear Radio", + category: "ADMIN", + subcategory: "Chat", + description: "Hear all radio messages while adminned.", + component: CheckboxInput, +}; +``` + +Category is which header it will fall under, and subcategory adds a subheader that will join with other entries in this category and subcategory. It will also show in search results. The description is shown on hover. + +## 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 preference_type == 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 preference_type == 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_character/player_preference(/datum/preference/type/here)`, which will return the stored value. + +## Categories + +Every preference needs to be in a `category`. These can be found in `code/__DEFINES/preferences.dm`. + +```dm +/// These will be shown in the character sidebar, but at the bottom. +#define PREFERENCE_CATEGORY_FEATURES "features" + +/// Any preferences that will show to the sides of the character in the setup menu. +#define PREFERENCE_CATEGORY_CLOTHING "clothing" + +/// Preferences that will be put into the 3rd list, and are not contextual. +#define PREFERENCE_CATEGORY_NON_CONTEXTUAL "non_contextual" + +/// Will be put under the game preferences window. +#define PREFERENCE_CATEGORY_GAME_PREFERENCES "game_preferences" + +/// These will show in the list to the right of the character preview. +#define PREFERENCE_CATEGORY_SECONDARY_FEATURES "secondary_features" + +/// These are preferences that are supplementary for main features, +/// such as hair color being affixed to hair. +#define PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES "supplemental_features" +``` + +![Preference categories for the main page](https://raw.githubusercontent.com/tgstation/documentation-assets/main/preferences/preference_categories.png) + +> SECONDARY_FEATURES or NON_CONTEXTUAL? + +Secondary features tend to be species specific. Non contextual features shouldn't change much from character to character. + +## Default values and randomization + +There are three procs to be aware of in regards to this topic: + +- `create_default_value()`. This is used when a value deserializes improperly or when a new character is created. +- `create_informed_default_value(datum/preferences/preferences)` - Used for more complicated default values, like how names require the gender. Will call `create_default_value()` by default. +- `create_random_value(datum/preferences/preferences)` - Explicitly used for random values, such as when a character is being randomized. + +`create_default_value()` in most preferences will create a random value. If this is a problem (like how default characters should always be human), you can override `create_default_value()`. By default (without overriding `create_random_value`), random values are just default values. + +## Advanced - Server data + +As previewed in [the display names implementation](#Choiced-preferences---Display-names), there exists a `compile_constant_data()` proc you can override. + +Compiled data is used wherever the server needs to give the client some value it can't figure out on its own. Skin tones use this to tell the client what colors they represent, for example. + +Compiled data is sent to the `serverData` field in the `FeatureValueProps`. + +## Advanced - Creating your own tgui component + +If you have good knowledge with tgui (especially TypeScript), you'll be able to create your own component to represent preferences. + +The `component` field in a feature accepts **any** component that accepts `FeatureValueProps`. + +This will give you the fields: + +```ts +act: typeof sendAct, +featureId: string, +handleSetValue: (newValue: TSending) => void, +serverData: TServerData | undefined, +shrink?: boolean, +value: TReceiving, +``` + +`act` is the same as the one you get from `useBackend`. + +`featureId` is the db_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_REF(congratulate_me), + ) + +/datum/preference_middleware/congratulations/proc/congratulate_me(list/params, mob/user) + to_chat(user, span_notice("Wow, you did a great job learning about middleware!")) + + return TRUE +``` + +Middleware can inject its own data at several points, such as providing new UI assets, compiled data (used by middleware such as quirks to tell the client what quirks exist), etc. Look at `code/modules/client/preferences/middleware/_middleware.dm` for full information. + +--- + +## Antagonists + +Role preferences are separate from antagonist datums and ban roles, but are connected. You can define a new role preference easily: + +``` +/datum/role_preference/antagonist/changeling + name = "Changeling" + description = "A highly intelligent alien predator that is capable of altering their \ + shape to flawlessly resemble a human.\n\ + Transform yourself or others into different identities, and buy from an \ + arsenal of biological weaponry with the DNA you collect." + antag_datum = /datum/antagonist/changeling +``` + +Newlines (`\n`) are converted to Stack dividers in TGUI, making a horizontal line element. The antag_datum is used for checking bans / playtime. + +Defining a `preview_outfit` with an outfit typepath will make the icon preview a human with said outfit. + +You can also override `get_preview_icon()` to set a specific icon, look at other examples for more. + +Using this preference is a simple matter of checking `client.role_preference_enabled(/datum/role_preference/antagonist/changeling)` + +The parent type (`/datum/role_preference/antagonist`) determines what category it will show under. See `GLOB.role_preference_categories` for a list of categories. + +## Species + +Adding support for a species to the preference menu involves adding some proc overrides on the species datum (descriptions, traits, etc). + +Most importantly, override `get_species_description()` and `get_species_lore()`. Then, add any unique perks to `create_pref_unique_perks()`, or add them to the respective procs (see `get_species_perks()`) if they could apply to another species. + +You also need to set the plural_form for use in perk descriptions. + +Do note that many perks are automatically generated, so a perk may not actually be "unique". Unique perks often include roleplay elements (such as Asimov Superiority) rather than specific gameplay elements, since those can be generic (such as temperature resistance). + +A perk looks like this: + +``` +list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "radiation", + SPECIES_PERK_NAME = "Radiation Immune", + SPECIES_PERK_DESC = "[plural_form] are entirely immune to radiation.", +)) +``` + +The icon can be a tgfont icon (tg-iconname) or a fontawesome icon. Finding exact FA icons can be difficult, but searching the v5 index for free icons usually works. We use v5.9, but the index is only v5.15, so there may be some incorrect icons on the index. + +A perk can be `SPECIES_POSITIVE_PERK`, `SPECIES_NEGATIVE_PERK`, or `SPECIES_NEUTRAL_PERK`. + +Changing the preview icon can be done via overriding `/datum/species/proc/prepare_human_for_preview(mob/living/carbon/human/human)`, by changing various dna features. Make sure the result is not random, or it will change between game loads, which could be confusing. + +## Internals and Implementation details + +### Database Read/Write + +#### SSpreferences + +The preferences system reads and writes from the database, and otherwise has no other form of serialization. To reduce database traffic, preference writes are queued by the SSpreferences subsystem, which accepts preference datums by ckey and holds writes in a queue. Duplicate writes are not performed, so the maximum amount a preferences datum can write is every fire of this SS (approx 5 seconds). + +While preferences are in this queue, the TGUI is sent a status indicator with its queue status. When a write completes, the preferences menu updates a value stating if the write was successful or not, which is displayed on the UI, alongside a reason. This is shown in the title bar. + +Do note that closing the preference menu essentially forces an immediate save, bypassing the queue system. This is useful during disconnections, as the UI is closed before full disconnect, triggering a save, and the preferences subsystem will not process disconnected clients. + +#### Preference Holders + +Character and player preferences both have their own `/datum/preferences_holder`, as they have different database schemas and need their own logic. The preferences_holder controls writing and reading datumized preferences to/from the database and initializing default preferences when there is no database. + +##### Local Caching + +Alongside queuing writes to reduce traffic, all preference values are cached locally, as querying the database for every preference retrieval would be a huge overhead. This is done via the preferences holder, which stores an associative list of db_keys to preference values. + +##### Dirty Preferences + +To reduce the amount of data written when an update is performed, only values that are changed are written to the database. Previously, any time "Save Preferences" was pressed, all preferences values, regardless of if they were changed or not, were immediately written. This poses a huge waste of database bandwidth and introduces potential problems with changing every other preference accidentally if some type of error were to occur. + +Instead, a list of preference db_keys is maintained (`dirty_prefs`). When a value is updated, it adds its db_key to this list. Then, when a write is performed, this list forms the columns that will be updated, rather than simply including all of them. After the write, the list is cleared. This drastically reduces database use, during typical use. + +##### Value Serialization + +Game Preferences store all values as strings in the database, and so do many non-string character preferences. This means that before a write, all preferences are converted to strings and converted back when deserialized. This is performed in `/datum/preference/proc/deserialize` and ``/datum/preference/proc/serialize`. These procs are used both for reading/writing from the DB and reading/writing from the UI. For most things, strings are OK anyway, as many values are natively strings (choiced lists, colors, etc.), although numbers will do some basic number parsing. + +This does complicate some choiced lists, as it may not be ideal to store the display name in the database, but you want to show pretty names in the UI. In this case, serialize() fails to act as desired, since you will get the "ugly" name in the UI. This is when things like get_constant_data are used to map ugly names to pretty names for the UI. It is always best to prioritize the database over the UI, since the UI can adapt easily. + +Values inside the preference cache are always in their deserialized form, and are serialized ONLY when sent to the UI or database. This is because the value in the cache is what is directly returned when read in code. + +When a preference is written to the cache, it is always deserialized, as it is expected to come from the UI or database. This could be problematic if the deserializer is badly implemented and alters the already deserialized form of the preference. + +##### Undatumized system + +save_preferences() and save_character() include additional logic for undatumized preferences. This system is important for values that cannot be easily represented in the small units of datumized preferences (like job preferences), or are not actually preferences (like the last changelog value). In order to reduce overall database use while minimizing code clutter, undatumized preferences can be marked globally dirty, so that all undatumized prefs will write if any one changes. While this is not perfect, it reduces the code work put in for these less common preferences while minimzing unnecessary queries. + +Datumized and undatumized preferences use separate SQL queries for each write, so it is ideal to prevent writing one entirely if it can be done. + +These are marked dirty by the procs: `mark_undatumized_dirty_player` and `mark_undatumized_dirty_character`, which also queue writes to the database in SSpreferences. Essentially, if you want to change an undatumized preference in code, you should always call the matching proc here so that the change is actually saved. Because undatumized preferences have no proc wrappers around their values and are stored directly on the preference datum, this is the only way the preference system knows if they have changed. + +`ready_to_save_character()` and `ready_to_save_player()` will return if there are ANY dirty preferences (datumized or undatumized), if you want to know if any values have changed since last save. diff --git a/code/modules/client/preferences/entries/character/age.dm b/code/modules/client/preferences/entries/character/age.dm new file mode 100644 index 0000000000000..5d5d9bbf7a939 --- /dev/null +++ b/code/modules/client/preferences/entries/character/age.dm @@ -0,0 +1,10 @@ +/datum/preference/numeric/age + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + db_key = "age" + preference_type = 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/entries/character/ai_core_display.dm b/code/modules/client/preferences/entries/character/ai_core_display.dm new file mode 100644 index 0000000000000..c789ffde684d2 --- /dev/null +++ b/code/modules/client/preferences/entries/character/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 + preference_type = PREFERENCE_CHARACTER + db_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, ignore_page = FALSE) + if (!..()) + 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/entries/character/body_model.dm b/code/modules/client/preferences/entries/character/body_model.dm new file mode 100644 index 0000000000000..f5d3abb0ad81f --- /dev/null +++ b/code/modules/client/preferences/entries/character/body_model.dm @@ -0,0 +1,38 @@ +/datum/preference/choiced/body_model + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + priority = PREFERENCE_PRIORITY_BODY_MODEL + db_key = "body_model" + preference_type = PREFERENCE_CHARACTER + +/datum/preference/choiced/body_model/init_possible_values() + return list(MALE, FEMALE) + +/datum/preference/choiced/body_model/apply_to_human(mob/living/carbon/human/target, value) + if (target.gender != MALE && target.gender != FEMALE) + target.dna.features["body_model"] = value + else + target.dna.features["body_model"] = target.gender + +/datum/preference/choiced/body_model/is_accessible(datum/preferences/preferences, ignore_page = FALSE) + if (!..()) + return FALSE + var/datum/species/species_type = preferences.read_character_preference(/datum/preference/choiced/species) + if(!initial(species_type.sexes)) + return FALSE + + var/gender = preferences.read_character_preference(/datum/preference/choiced/gender) + return gender != MALE && gender != FEMALE + +/datum/preference/choiced/body_size + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + db_key = "body_size" + preference_type = PREFERENCE_CHARACTER + +/datum/preference/choiced/body_size/init_possible_values() + return assoc_to_keys(GLOB.body_sizes) + +/datum/preference/choiced/body_size/create_default_value() + return "Normal" + +/datum/preference/choiced/body_size/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["body_size"] = value diff --git a/code/modules/client/preferences/entries/character/clothing.dm b/code/modules/client/preferences/entries/character/clothing.dm new file mode 100644 index 0000000000000..e4a03c1b21bd0 --- /dev/null +++ b/code/modules/client/preferences/entries/character/clothing.dm @@ -0,0 +1,154 @@ +/proc/generate_values_for_underwear(list/accessory_list, list/icons, color) + var/icon/lower_half = icon('icons/effects/effects.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('icons/mob/clothing/underwear.dmi', accessory.icon_state) + 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 + db_key = "backbag" + preference_type = 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.backbag = value + +/// Jumpsuit preference +/datum/preference/choiced/jumpsuit_style + db_key = "jumpsuit_style" + preference_type = PREFERENCE_CHARACTER + main_feature_name = "Jumpsuit" + category = PREFERENCE_CATEGORY_CLOTHING + should_generate_icons = TRUE + +/datum/preference/choiced/jumpsuit_style/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_style/apply_to_human(mob/living/carbon/human/target, value) + target.jumpsuit_style = value + +/// Socks preference +/datum/preference/choiced/socks + db_key = "socks" + preference_type = PREFERENCE_CHARACTER + main_feature_name = "Socks" + category = PREFERENCE_CATEGORY_CLOTHING + should_generate_icons = TRUE + preference_spritesheet = PREFERENCE_SHEET_LARGE + +/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 + db_key = "undershirt" + preference_type = PREFERENCE_CHARACTER + main_feature_name = "Undershirt" + category = PREFERENCE_CATEGORY_CLOTHING + should_generate_icons = TRUE + preference_spritesheet = PREFERENCE_SHEET_LARGE + +/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 + +/// Underwear preference +/datum/preference/choiced/underwear + db_key = "underwear" + preference_type = PREFERENCE_CHARACTER + main_feature_name = "Underwear" + category = PREFERENCE_CATEGORY_CLOTHING + should_generate_icons = TRUE + preference_spritesheet = PREFERENCE_SHEET_LARGE + +/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, ignore_page = FALSE) + if (!..()) + return FALSE + + var/species_type = preferences.read_character_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/entries/character/gender.dm b/code/modules/client/preferences/entries/character/gender.dm new file mode 100644 index 0000000000000..25cc76d2e5474 --- /dev/null +++ b/code/modules/client/preferences/entries/character/gender.dm @@ -0,0 +1,13 @@ +/// Gender preference +/datum/preference/choiced/gender + preference_type = PREFERENCE_CHARACTER + db_key = "gender" + priority = PREFERENCE_PRIORITY_GENDER + +/datum/preference/choiced/gender/init_possible_values() + return list(MALE, FEMALE, PLURAL) + +/datum/preference/choiced/gender/apply_to_human(mob/living/carbon/human/target, value) + if(!target.dna.species.sexes) + value = PLURAL //disregard gender preferences on this species + target.gender = value diff --git a/code/modules/client/preferences/entries/character/names.dm b/code/modules/client/preferences/entries/character/names.dm new file mode 100644 index 0000000000000..1d7b3c70821d4 --- /dev/null +++ b/code/modules/client/preferences/entries/character/names.dm @@ -0,0 +1,160 @@ +/// 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 + preference_type = 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" + db_key = "real_name" + informed = TRUE + // Used in serialize and is_valid + allow_numbers = TRUE + +/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_character_preference(/datum/preference/choiced/species) + var/gender = preferences.read_character_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) + var/datum/species/selected_species = preferences.read_character_preference(/datum/preference/choiced/species) + input = reject_bad_name(input, initial(selected_species.allow_numbers_in_name)) + if (!input) + return input + + if (CONFIG_GET(flag/humans_need_surnames) && selected_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 input + +/// 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" + db_key = "human_name" + informed = TRUE + +/datum/preference/name/backup_human/create_informed_default_value(datum/preferences/preferences) + var/gender = preferences.read_character_preference(/datum/preference/choiced/gender) + + return random_unique_name(gender) + +/datum/preference/name/clown + db_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 + db_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 + db_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 + db_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 + db_key = "religion_name" + + allow_numbers = TRUE + + explanation = "Religion name" + group = "religion" + +/datum/preference/name/religion/create_default_value() + return DEFAULT_RELIGION + +/datum/preference/name/deity + db_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 + db_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/entries/character/pda.dm b/code/modules/client/preferences/entries/character/pda.dm new file mode 100644 index 0000000000000..a5cff7134072d --- /dev/null +++ b/code/modules/client/preferences/entries/character/pda.dm @@ -0,0 +1,45 @@ +/// The visual style of a PDA +/datum/preference/choiced/pda_theme + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + db_key = "pda_theme" + preference_type = PREFERENCE_CHARACTER + +/datum/preference/choiced/pda_theme/compile_ui_data(mob/user, value) + return value // The default behavior is to serialize. Don't do that. + +/datum/preference/choiced/pda_theme/deserialize(input, datum/preferences/preferences) + for(var/key in GLOB.ntos_device_themes_default) + if(GLOB.ntos_device_themes_default[key] == input || key == input) + return key + return "NtOS Default" + +/datum/preference/choiced/pda_theme/serialize(input) + for(var/key in GLOB.ntos_device_themes_default) + var/value = GLOB.ntos_device_themes_default[key] + if(value == input || key == input) + return value + return GLOB.ntos_device_themes_default[sanitize_inlist(input, get_choices(), "NtOS Default")] + +/datum/preference/choiced/pda_theme/create_default_value() + return "NtOS Default" + +/datum/preference/choiced/pda_theme/init_possible_values() + return assoc_to_keys(GLOB.ntos_device_themes_default) + +/datum/preference/choiced/pda_theme/apply_to_human(mob/living/carbon/human/target, value) + return + +/// The color of a PDA with Thinktronic Classic +/datum/preference/color/pda_classic_color + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + db_key = "pda_classic_color" + preference_type = PREFERENCE_CHARACTER + +/datum/preference/color/pda_classic_color/create_default_value() + return COLOR_OLIVE + +/datum/preference/color/pda_classic_color/is_accessible(datum/preferences/preferences, ignore_page) + return ..() && preferences.read_character_preference(/datum/preference/choiced/pda_theme) == "Thinktronic Classic" + +/datum/preference/color/pda_classic_color/apply_to_human(mob/living/carbon/human/target, value) + return diff --git a/code/modules/client/preferences/entries/character/random.dm b/code/modules/client/preferences/entries/character/random.dm new file mode 100644 index 0000000000000..72c098056d268 --- /dev/null +++ b/code/modules/client/preferences/entries/character/random.dm @@ -0,0 +1,37 @@ +/datum/preference/choiced/random_body + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + db_key = "body_is_always_random" + preference_type = 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/choiced/random_name + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + db_key = "name_is_always_random" + preference_type = 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/entries/character/security_department.dm b/code/modules/client/preferences/entries/character/security_department.dm new file mode 100644 index 0000000000000..489356f3a0207 --- /dev/null +++ b/code/modules/client/preferences/entries/character/security_department.dm @@ -0,0 +1,21 @@ +/// 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 + preference_type = PREFERENCE_CHARACTER + db_key = "preferred_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 ..() + +/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 diff --git a/code/modules/client/preferences/entries/character/skin_tone.dm b/code/modules/client/preferences/entries/character/skin_tone.dm new file mode 100644 index 0000000000000..d11246d463854 --- /dev/null +++ b/code/modules/client/preferences/entries/character/skin_tone.dm @@ -0,0 +1,36 @@ +/datum/preference/choiced/skin_tone + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + preference_type = PREFERENCE_CHARACTER + db_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, include_tag = TRUE) + var/list/hsl = rgb2num(hex_value, COLORSPACE_HSL) + + to_hex[choice] = list( + "lightness" = hsl[3], + "value" = hex_value, + ) + + data["to_hex"] = to_hex + + return data + +/datum/preference/choiced/skin_tone/apply_to_human(mob/living/carbon/human/target, value) + target.skin_tone = value + +/datum/preference/choiced/skin_tone/is_accessible(datum/preferences/preferences, ignore_page = FALSE) + if (!..()) + return FALSE + + var/datum/species/species_type = preferences.read_character_preference(/datum/preference/choiced/species) + return initial(species_type.use_skintones) diff --git a/code/modules/client/preferences/entries/character/species.dm b/code/modules/client/preferences/entries/character/species.dm new file mode 100644 index 0000000000000..897c9961b55ee --- /dev/null +++ b/code/modules/client/preferences/entries/character/species.dm @@ -0,0 +1,53 @@ +/// Species preference +/datum/preference/choiced/species + preference_type = PREFERENCE_CHARACTER + db_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_acceptable_species(), get_fallback_species_id())] + +/datum/preference/choiced/species/serialize(input) + var/datum/species/species = input + return initial(species.id) + +/datum/preference/choiced/species/create_default_value() + return /datum/species/human + +/datum/preference/choiced/species/create_random_value(datum/preferences/preferences) + return pick(get_choices()) + +/datum/preference/choiced/species/init_possible_values() + var/list/values = list() + + for (var/species_id in get_selectable_species()) + values += GLOB.species_list[species_id] + + return values + +/datum/preference/choiced/species/apply_to_human(mob/living/carbon/human/target, value) + target.set_species(value, icon_update = FALSE, pref_load = TRUE) + +/datum/preference/choiced/species/compile_constant_data() + var/list/data = list() + + for (var/species_id in get_acceptable_species()) + var/species_type = GLOB.species_list[species_id] + var/datum/species/species = new species_type() + + data[species_id] = list() + data[species_id]["name"] = species.name + data[species_id]["desc"] = species.get_species_description() + data[species_id]["lore"] = species.get_species_lore() + data[species_id]["icon"] = sanitize_css_class_name(species.name) + data[species_id]["use_skintones"] = species.use_skintones + data[species_id]["sexes"] = species.sexes + data[species_id]["enabled_features"] = species.get_features() + data[species_id]["perks"] = species.get_species_perks() + data[species_id]["diet"] = species.get_species_diet() + data[species_id]["selectable"] = species.check_roundstart_eligible() + + qdel(species) + + return data diff --git a/code/modules/client/preferences/entries/character/species_features/apid.dm b/code/modules/client/preferences/entries/character/species_features/apid.dm new file mode 100644 index 0000000000000..be6c91bfd3726 --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/apid.dm @@ -0,0 +1,88 @@ +/datum/preference/choiced/apid_stripes + db_key = "feature_apid_stripes" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Stripe Pattern" + should_generate_icons = TRUE + relevant_mutant_bodypart = "apid_stripes" + +/datum/preference/choiced/apid_stripes/init_possible_values() + var/list/values = list() + + for (var/stripe_name in GLOB.apid_stripes_list) + var/datum/sprite_accessory/stripe = GLOB.apid_stripes_list[stripe_name] + + var/icon/icon_with_stripes = icon('icons/mob/species/apid/bodyparts.dmi', "apid_chest_m", dir = SOUTH) + if (stripe.icon_state != "none") + var/icon/stripes_icon = icon(stripe.icon, "m_apid_stripes_[stripe.icon_state]_ADJ", dir = SOUTH) + stripes_icon.Blend(COLOR_YELLOW, ICON_MULTIPLY) + icon_with_stripes.Blend(stripes_icon, ICON_OVERLAY) + + icon_with_stripes.Crop(10, 8, 22, 23) + icon_with_stripes.Scale(26, 32) + icon_with_stripes.Crop(-2, 1, 29, 32) + + values[stripe.name] = icon_with_stripes + + return values + +/datum/preference/choiced/apid_stripes/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["apid_stripes"] = value + +/datum/preference/choiced/apid_antenna + db_key = "feature_apid_antenna" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Antennae Style" + should_generate_icons = TRUE + relevant_mutant_bodypart = "apid_antenna" + +/datum/preference/choiced/apid_antenna/init_possible_values() + var/list/values = list() + + for (var/antenna_name in GLOB.apid_antenna_list) + var/datum/sprite_accessory/antenna = GLOB.apid_antenna_list[antenna_name] + + var/icon/icon_with_antennae = icon('icons/mob/species/apid/bodyparts.dmi', "apid_head_m", dir = SOUTH) + if (antenna.icon_state != "none") + var/icon/antenna_icon = icon(antenna.icon, "m_apid_antenna_[antenna.icon_state]_ADJ", dir = SOUTH) + antenna_icon.Blend(COLOR_YELLOW, ICON_MULTIPLY) + icon_with_antennae.Blend(antenna_icon, ICON_OVERLAY) + icon_with_antennae.Scale(64, 64) + icon_with_antennae.Crop(15, 64, 15 + 31, 64 - 31) + + values[antenna.name] = icon_with_antennae + + return values + +/datum/preference/choiced/apid_antenna/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["apid_antenna"] = value + +/datum/preference/choiced/apid_headstripes + db_key = "feature_apid_headstripes" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Headstripe Pattern" + should_generate_icons = TRUE + relevant_mutant_bodypart = "apid_headstripes" + +/datum/preference/choiced/apid_headstripes/init_possible_values() + var/list/values = list() + + for (var/headstripe_name in GLOB.apid_headstripes_list) + var/datum/sprite_accessory/headstripe = GLOB.apid_headstripes_list[headstripe_name] + + var/icon/icon_with_headstripes = icon('icons/mob/species/apid/bodyparts.dmi', "apid_head_m", dir = SOUTH) + if (headstripe.icon_state != "none") + var/icon/headstripes_icon = icon(headstripe.icon, "m_apid_headstripes_[headstripe.icon_state]_ADJ", dir = SOUTH) + headstripes_icon.Blend(COLOR_YELLOW, ICON_MULTIPLY) + icon_with_headstripes.Blend(headstripes_icon, ICON_OVERLAY) + icon_with_headstripes.Scale(64, 64) + icon_with_headstripes.Crop(15, 64, 15 + 31, 64 - 31) + + values[headstripe.name] = icon_with_headstripes + + return values + +/datum/preference/choiced/apid_headstripes/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["apid_headstripes"] = value diff --git a/code/modules/client/preferences/entries/character/species_features/basic.dm b/code/modules/client/preferences/entries/character/species_features/basic.dm new file mode 100644 index 0000000000000..37ee5243b747f --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/basic.dm @@ -0,0 +1,182 @@ +/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", include_tag = TRUE), 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("#42250a", 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 + db_key = "eye_color" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + relevant_species_trait = EYECOLOR + priority = PREFERENCE_PRIORITY_EYE_COLOR + +/datum/preference/color_legacy/eye_color/apply_to_human(mob/living/carbon/human/target, value) + if(isipc(target)) + return + 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 + db_key = "facial_style_name" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Facial Hair" + should_generate_icons = TRUE + relevant_species_trait = FACEHAIR + preference_spritesheet = PREFERENCE_SHEET_LARGE + +/datum/preference/choiced/facial_hairstyle/init_possible_values() + return generate_possible_values_for_sprite_accessories_on_head(GLOB.facial_hair_styles_list) + +/datum/preference/choiced/facial_hairstyle/apply_to_human(mob/living/carbon/human/target, value) + target.facial_hair_style = value + +/datum/preference/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 + db_key = "facial_hair_color" + preference_type = 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 + db_key = "hair_color" + preference_type = 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) + if(isipc(target)) + return + target.hair_color = value + +/datum/preference/choiced/hairstyle + db_key = "hair_style_name" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Hair Style" + should_generate_icons = TRUE + relevant_species_trait = HAIR + preference_spritesheet = PREFERENCE_SHEET_HUGE + +/datum/preference/choiced/hairstyle/init_possible_values() + return generate_possible_values_for_sprite_accessories_on_head(GLOB.hair_styles_list) + +/datum/preference/choiced/hairstyle/apply_to_human(mob/living/carbon/human/target, value) + target.hair_style = value + +/datum/preference/choiced/hairstyle/compile_constant_data() + var/list/data = ..() + + data[SUPPLEMENTAL_FEATURE_KEY] = "hair_color" + + return data + +/datum/preference/choiced/gradient_style + db_key = "gradient_style" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Gradient Style" + should_generate_icons = TRUE + relevant_species_trait = HAIR + +/datum/preference/choiced/gradient_style/init_possible_values() + var/list/values = possible_values_for_sprite_accessory_list(GLOB.hair_gradients_list) + + var/list/body_parts = list( + BODY_ZONE_HEAD, + BODY_ZONE_CHEST, + BODY_ZONE_L_ARM, + BODY_ZONE_R_ARM, + BODY_ZONE_PRECISE_L_HAND, + BODY_ZONE_PRECISE_R_HAND, + BODY_ZONE_L_LEG, + BODY_ZONE_R_LEG, + ) + var/icon/body_icon = icon('icons/effects/effects.dmi', "nothing") + for (var/body_part in body_parts) + var/gender = body_part == BODY_ZONE_CHEST || body_part == BODY_ZONE_HEAD ? "_m" : "" + body_icon.Blend(icon('icons/mob/human_parts_greyscale.dmi', "human_[body_part][gender]", dir = NORTH), ICON_OVERLAY) + body_icon.Blend(skintone2hex("caucasian1", include_tag = TRUE), ICON_MULTIPLY) + var/icon/jumpsuit_icon = icon('icons/mob/clothing/uniform.dmi', "jumpsuit", dir = NORTH) + jumpsuit_icon.Blend("#b3b3b3", ICON_MULTIPLY) + body_icon.Blend(jumpsuit_icon, ICON_OVERLAY) + + var/datum/sprite_accessory/hair_accessory = GLOB.hair_styles_list["Very Long Hair 2"] + var/icon/hair_icon = icon(hair_accessory.icon, hair_accessory.icon_state, dir = NORTH) + hair_icon.Blend("#080501", ICON_MULTIPLY) + + for (var/name in values) + var/datum/sprite_accessory/accessory = GLOB.hair_gradients_list[name] + if (accessory == null) + if(accessory.icon_state == null || accessory.icon_state == "none") + values[name] = icon('icons/mob/landmarks.dmi', "x") + continue + + var/icon/final_icon = new(body_icon) + var/icon/base_hair_icon = new(hair_icon) + var/icon/gradient_hair_icon = icon(hair_accessory.icon, hair_accessory.icon_state, dir = NORTH) + + var/icon/gradient_icon = values[name] + gradient_icon.Blend(gradient_hair_icon, ICON_ADD) + gradient_icon.Blend("#42250a", ICON_MULTIPLY) + base_hair_icon.Blend(gradient_icon, ICON_OVERLAY) + + final_icon.Blend(base_hair_icon, ICON_OVERLAY) + values[name] = final_icon + + return values + +/datum/preference/choiced/gradient_style/apply_to_human(mob/living/carbon/human/target, value) + target.gradient_style = value + +/datum/preference/choiced/gradient_style/compile_constant_data() + var/list/data = ..() + + data[SUPPLEMENTAL_FEATURE_KEY] = "gradient_color" + + return data + +/datum/preference/color_legacy/gradient_color + db_key = "gradient_color" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES + relevant_species_trait = HAIR + +/datum/preference/color_legacy/gradient_color/apply_to_human(mob/living/carbon/human/target, value) + target.gradient_color = value diff --git a/code/modules/client/preferences/entries/character/species_features/ethereal.dm b/code/modules/client/preferences/entries/character/species_features/ethereal.dm new file mode 100644 index 0000000000000..9599b3d27a5af --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/ethereal.dm @@ -0,0 +1,41 @@ +/datum/preference/choiced/ethereal_color + db_key = "feature_ethcolor" + preference_type = 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/deserialize(input, datum/preferences/preferences) + if(findtext(input, GLOB.is_color_nocrunch)) // Migrate old data + var/valid = assoc_key_for_value(GLOB.color_list_ethereal, lowertext(input)) + if(!isnull(valid)) + return valid + return ..() + +/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/entries/character/species_features/felinid.dm b/code/modules/client/preferences/entries/character/species_features/felinid.dm new file mode 100644 index 0000000000000..bd2e340a48e1f --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/felinid.dm @@ -0,0 +1,34 @@ +/datum/preference/choiced/tail_human + db_key = "feature_human_tail" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + can_randomize = FALSE + relevant_mutant_bodypart = "tail_human" + +/datum/preference/choiced/tail_human/init_possible_values() + return assoc_to_keys(GLOB.tails_roundstart_list_human) + +/datum/preference/choiced/tail_human/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["tail_human"] = value + +/datum/preference/choiced/tail_human/create_default_value() + var/datum/sprite_accessory/tails/human/cat/tail = /datum/sprite_accessory/tails/human/cat + return initial(tail.name) + +/datum/preference/choiced/ears + db_key = "feature_human_ears" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + can_randomize = FALSE + relevant_mutant_bodypart = "ears" + +/datum/preference/choiced/ears/init_possible_values() + return assoc_to_keys(GLOB.ears_list) + +/datum/preference/choiced/ears/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ears"] = value + +/datum/preference/choiced/ears/create_default_value() + var/datum/sprite_accessory/ears/cat/ears = /datum/sprite_accessory/ears/cat + return initial(ears.name) + diff --git a/code/modules/client/preferences/entries/character/species_features/fly.dm b/code/modules/client/preferences/entries/character/species_features/fly.dm new file mode 100644 index 0000000000000..a471a6732a617 --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/fly.dm @@ -0,0 +1,11 @@ +/datum/preference/choiced/insect_type + db_key = "feature_insect_type" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + relevant_mutant_bodypart = "insect_type" + +/datum/preference/choiced/insect_type/init_possible_values() + return assoc_to_keys(GLOB.insect_type_list) + +/datum/preference/choiced/insect_type/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["insect_type"] = value diff --git a/code/modules/client/preferences/entries/character/species_features/ipc.dm b/code/modules/client/preferences/entries/character/species_features/ipc.dm new file mode 100644 index 0000000000000..39e7ed201c8e5 --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/ipc.dm @@ -0,0 +1,139 @@ +/datum/preference/choiced/ipc_screen + db_key = "feature_ipc_screen" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Screen Style" + should_generate_icons = TRUE + relevant_mutant_bodypart = "ipc_screen" + +/datum/preference/choiced/ipc_screen/init_possible_values() + var/list/values = list() + + for (var/screen_name in GLOB.ipc_screens_list) + var/datum/sprite_accessory/screen = GLOB.ipc_screens_list[screen_name] + + var/icon/icon_with_screen = icon('icons/mob/species/ipc/bodyparts.dmi', "mcgipc_head", dir = SOUTH) + if (screen.icon_state != "none") + var/icon/screen_icon = icon(screen.icon, "m_ipc_screen_[screen.icon_state]_ADJ", dir = SOUTH) + icon_with_screen.Blend(screen_icon, ICON_OVERLAY) + icon_with_screen.Scale(64, 64) + icon_with_screen.Crop(15, 64, 15 + 31, 64 - 31) + + values[screen.name] = icon_with_screen + + return values + +/datum/preference/choiced/ipc_screen/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ipc_screen"] = value + +/datum/preference/choiced/ipc_screen/compile_constant_data() + var/list/data = ..() + + data[SUPPLEMENTAL_FEATURE_KEY] = "feature_ipc_screen_color" + + return data + +/datum/preference/color_legacy/ipc_screen_color + db_key = "feature_ipc_screen_color" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES + relevant_mutant_bodypart = "ipc_antenna" + priority = PREFERENCE_PRIORITY_EYE_COLOR + +/datum/preference/color_legacy/ipc_screen_color/apply_to_human(mob/living/carbon/human/target, value) + if(!isipc(target)) + return + 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/ipc_screen_color/create_default_value() + return "fff" + +/datum/preference/choiced/ipc_antenna + db_key = "feature_ipc_antenna" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Antenna Style" + should_generate_icons = TRUE + relevant_mutant_bodypart = "ipc_antenna" + +/datum/preference/choiced/ipc_antenna/init_possible_values() + var/list/values = list() + + for (var/antenna_name in GLOB.ipc_antennas_list) + var/datum/sprite_accessory/antenna = GLOB.ipc_antennas_list[antenna_name] + + var/icon/icon_with_antennae = icon('icons/mob/species/ipc/bodyparts.dmi', "mcgipc_head", dir = SOUTH) + if (antenna.icon_state != "none") + // weird snowflake shit + var/side = (antenna_name == "Light" || antenna_name == "Drone Eyes") ? "FRONT" : "ADJ" + var/icon/antenna_icon = icon(antenna.icon, "m_ipc_antenna_[antenna.icon_state]_[side]", dir = SOUTH) + icon_with_antennae.Blend(antenna_icon, ICON_OVERLAY) + icon_with_antennae.Scale(64, 64) + icon_with_antennae.Crop(15, 64, 15 + 31, 64 - 31) + + values[antenna.name] = icon_with_antennae + + return values + +/datum/preference/choiced/ipc_antenna/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ipc_antenna"] = value + +/datum/preference/choiced/ipc_antenna/compile_constant_data() + var/list/data = ..() + + data[SUPPLEMENTAL_FEATURE_KEY] = "feature_ipc_antenna_color" + + return data + +/datum/preference/color_legacy/ipc_antenna_color + db_key = "feature_ipc_antenna_color" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES + relevant_mutant_bodypart = "ipc_antenna" + +/datum/preference/color_legacy/ipc_antenna_color/apply_to_human(mob/living/carbon/human/target, value) + if(!isipc(target)) + return + target.hair_color = value + +/datum/preference/color_legacy/ipc_antenna_color/create_default_value() + return "222" + +/datum/preference/choiced/ipc_chassis + db_key = "feature_ipc_chassis" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Chassis Style" + should_generate_icons = TRUE + relevant_mutant_bodypart = "ipc_chassis" + +/datum/preference/choiced/ipc_chassis/init_possible_values() + var/list/values = list() + var/list/body_parts = list( + BODY_ZONE_HEAD, + BODY_ZONE_CHEST, + BODY_ZONE_L_ARM, + BODY_ZONE_R_ARM, + BODY_ZONE_PRECISE_L_HAND, + BODY_ZONE_PRECISE_R_HAND, + BODY_ZONE_L_LEG, + BODY_ZONE_R_LEG, + ) + for (var/chassis_name in GLOB.ipc_chassis_list) + var/datum/sprite_accessory/chassis = GLOB.ipc_chassis_list[chassis_name] + var/icon/icon_with_chassis = icon('icons/effects/effects.dmi', "nothing") + + for (var/body_part in body_parts) + icon_with_chassis.Blend(icon('icons/mob/species/ipc/bodyparts.dmi', "[chassis.limbs_id]_[body_part]", dir = SOUTH), ICON_OVERLAY) + + values[chassis.name] = icon_with_chassis + + return values + +/datum/preference/choiced/ipc_chassis/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["ipc_chassis"] = value diff --git a/code/modules/client/preferences/entries/character/species_features/lizard.dm b/code/modules/client/preferences/entries/character/species_features/lizard.dm new file mode 100644 index 0000000000000..db7ad44659629 --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/lizard.dm @@ -0,0 +1,182 @@ +/proc/generate_lizard_side_shots(list/sprite_accessories, key, include_snout = TRUE) + var/list/values = list() + + var/icon/lizard = icon('icons/mob/species/lizard/bodyparts.dmi', "lizard_head_m", dir = EAST) + + var/icon/eyes = icon('icons/mob/human_face.dmi', "eyes", dir = 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", dir = EAST), ICON_OVERLAY) + + for (var/name in sprite_accessories) + var/datum/sprite_accessory/sprite_accessory = sprite_accessories[name] + + var/icon/final_icon = new(lizard) + + if (sprite_accessory.icon_state != "none") + var/icon/accessory_icon = icon(sprite_accessory.icon, "m_[key]_[sprite_accessory.icon_state]_ADJ", dir = EAST) + final_icon.Blend(accessory_icon, ICON_OVERLAY) + + final_icon.Crop(11, 20, 23, 32) + final_icon.Scale(32, 32) + final_icon.Blend(COLOR_LIME, ICON_MULTIPLY) + + values[name] = icon(final_icon, dir = EAST) + + return values + +/datum/preference/choiced/lizard_body_markings + db_key = "feature_lizard_body_markings" + preference_type = 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/species/lizard/bodyparts.dmi', "lizard_chest_m", dir = SOUTH) + + for (var/name in GLOB.body_markings_list) + var/datum/sprite_accessory/sprite_accessory = GLOB.body_markings_list[name] + + var/icon/final_icon = icon(lizard, dir = SOUTH) + + 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", + dir = SOUTH + ) + + final_icon.Blend(body_markings_icon, ICON_OVERLAY) + + final_icon.Blend(COLOR_LIME, ICON_MULTIPLY) + final_icon.Crop(10, 8, 22, 23) + final_icon.Scale(26, 32) + final_icon.Crop(-2, 1, 29, 32) + + values[name] = icon(final_icon, dir = SOUTH) + + return values + +/datum/preference/choiced/lizard_body_markings/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["body_markings"] = value + +/datum/preference/choiced/lizard_frills + db_key = "feature_lizard_frills" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Frills" + should_generate_icons = TRUE + relevant_mutant_bodypart = "frills" + +/datum/preference/choiced/lizard_frills/init_possible_values() + return generate_lizard_side_shots(GLOB.frills_list, "frills") + +/datum/preference/choiced/lizard_frills/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["frills"] = value + +/datum/preference/choiced/lizard_horns + db_key = "feature_lizard_horns" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Horns" + should_generate_icons = TRUE + relevant_mutant_bodypart = "horns" + +/datum/preference/choiced/lizard_horns/init_possible_values() + return generate_lizard_side_shots(GLOB.horns_list, "horns") + +/datum/preference/choiced/lizard_horns/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["horns"] = value + +/datum/preference/choiced/lizard_legs + db_key = "feature_lizard_legs" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + relevant_mutant_bodypart = "legs" + +/datum/preference/choiced/lizard_legs/init_possible_values() + return assoc_to_keys(GLOB.legs_list) + +/datum/preference/choiced/lizard_legs/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["legs"] = value + +/datum/preference/choiced/lizard_snout + db_key = "feature_lizard_snout" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Snout" + should_generate_icons = TRUE + relevant_mutant_bodypart = "snout" + +/datum/preference/choiced/lizard_snout/init_possible_values() + return generate_lizard_side_shots(GLOB.snouts_list, "snout", include_snout = FALSE) + +/datum/preference/choiced/lizard_snout/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["snout"] = value + +/datum/preference/choiced/lizard_spines + db_key = "feature_lizard_spines" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Spines" + should_generate_icons = TRUE + relevant_mutant_bodypart = "spines" + +/datum/preference/choiced/lizard_spines/init_possible_values() + return generate_lizard_body_shots(GLOB.spines_list, "spines", show_tail = TRUE) + +/datum/preference/choiced/lizard_spines/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["spines"] = value + +/datum/preference/choiced/lizard_tail + db_key = "feature_lizard_tail" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Tail" + should_generate_icons = TRUE + relevant_mutant_bodypart = "tail_lizard" + +/datum/preference/choiced/lizard_tail/init_possible_values() + return generate_lizard_body_shots(GLOB.tails_list_lizard, "tail") + +/datum/preference/choiced/lizard_tail/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["tail_lizard"] = value + +/proc/generate_lizard_body_shots(list/sprite_accessories, key, show_tail = FALSE, shift_x = -8) + var/list/values = list() + var/list/body_parts = list( + BODY_ZONE_CHEST, + BODY_ZONE_R_ARM, + BODY_ZONE_PRECISE_R_HAND, + BODY_ZONE_R_LEG, + ) + var/icon/body_icon = icon('icons/effects/effects.dmi', "nothing") + for (var/body_part in body_parts) + var/gender = body_part == BODY_ZONE_CHEST ? "_m" : "" + body_icon.Blend(icon('icons/mob/species/lizard/bodyparts.dmi', "lizard_[body_part][gender]", dir = EAST), ICON_OVERLAY) + if(show_tail) + body_icon.Blend(icon('icons/mob/mutant_bodyparts.dmi', "m_tail_smooth_BEHIND", dir = EAST), ICON_OVERLAY) + + for (var/sprite_name in sprite_accessories) + var/datum/sprite_accessory/sprite = sprite_accessories[sprite_name] + var/icon/icon_with_changes = new(body_icon) + + if (sprite_name != "None") + var/ex = key == "spines" ? "ADJ" : "BEHIND" + var/icon/sprite_icon = icon('icons/mob/mutant_bodyparts.dmi', "m_[key]_[sprite.icon_state]_[ex]", dir = EAST) + icon_with_changes.Blend(sprite_icon, ICON_OVERLAY) + icon_with_changes.Blend(COLOR_LIME, ICON_MULTIPLY) + + // Zoom in + icon_with_changes.Scale(64, 64) + icon_with_changes.Crop(15 + shift_x, 0, 15 + 31 + shift_x, 31) + + values[sprite_name] = icon_with_changes + + return values diff --git a/code/modules/client/preferences/entries/character/species_features/moth.dm b/code/modules/client/preferences/entries/character/species_features/moth.dm new file mode 100644 index 0000000000000..9013071705cf0 --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/moth.dm @@ -0,0 +1,99 @@ +/datum/preference/choiced/moth_antennae + db_key = "feature_moth_antennae" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Antennae" + should_generate_icons = TRUE + relevant_mutant_bodypart = "moth_antennae" + +/datum/preference/choiced/moth_antennae/init_possible_values() + var/list/values = list() + + for (var/antennae_name in GLOB.moth_antennae_roundstart_list) + var/datum/sprite_accessory/antennae = GLOB.moth_antennae_roundstart_list[antennae_name] + + var/icon/icon_with_antennae = icon('icons/mob/species/moth/bodyparts.dmi', "moth_head_m", dir = SOUTH) + icon_with_antennae.Blend(icon(antennae.icon, "m_moth_antennae_[antennae.icon_state]_FRONT", dir = SOUTH), ICON_OVERLAY) + icon_with_antennae.Scale(64, 64) + icon_with_antennae.Crop(15, 64, 15 + 31, 64 - 31) + values[antennae.name] = icon(icon_with_antennae, dir = SOUTH) + + 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 + db_key = "feature_moth_markings" + preference_type = 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/effects/effects.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/species/moth/bodyparts.dmi', "moth_[body_part][gender]", dir = SOUTH), ICON_OVERLAY) + + for (var/markings_name in GLOB.moth_markings_roundstart_list) + var/datum/sprite_accessory/markings = GLOB.moth_markings_roundstart_list[markings_name] + 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]", dir = SOUTH) + 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 + db_key = "feature_moth_wings" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "Moth Wings" + should_generate_icons = TRUE + relevant_mutant_bodypart = "moth_wings" + +/datum/preference/choiced/moth_wings/init_possible_values() + var/list/icon/values = possible_values_for_sprite_accessory_list_for_body_part( + GLOB.moth_wings_roundstart_list, + "moth_wings", + list("BEHIND", "FRONT"), + ) + + // Moth wings are in a stupid dimension + for (var/name in values) + values[name].Crop(1, 1, 32, 32) + + return values + +/datum/preference/choiced/moth_wings/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["moth_wings"] = value + diff --git a/code/modules/client/preferences/entries/character/species_features/mutants.dm b/code/modules/client/preferences/entries/character/species_features/mutants.dm new file mode 100644 index 0000000000000..0a1897793aca9 --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/mutants.dm @@ -0,0 +1,20 @@ +/datum/preference/color_legacy/mutant_color + db_key = "feature_mcolor" + preference_type = 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/entries/character/species_features/plasmaman.dm b/code/modules/client/preferences/entries/character/species_features/plasmaman.dm new file mode 100644 index 0000000000000..bd76cc178809a --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/plasmaman.dm @@ -0,0 +1,14 @@ +/datum/preference/choiced/helmet_style + db_key = "helmet_style" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + relevant_species_trait = ENVIROSUIT + +/datum/preference/choiced/helmet_style/init_possible_values() + return assoc_to_keys(GLOB.helmet_styles) + +/datum/preference/choiced/helmet_style/create_default_value() + return HELMET_DEFAULT + +/datum/preference/choiced/helmet_style/apply_to_human(mob/living/carbon/human/target, value) + return diff --git a/code/modules/client/preferences/entries/character/species_features/psyphoza.dm b/code/modules/client/preferences/entries/character/species_features/psyphoza.dm new file mode 100644 index 0000000000000..ab2322dd9fee1 --- /dev/null +++ b/code/modules/client/preferences/entries/character/species_features/psyphoza.dm @@ -0,0 +1,27 @@ +/datum/preference/choiced/psyphoza_cap + db_key = "feature_psyphoza_cap" + preference_type = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_FEATURES + main_feature_name = "cap" + should_generate_icons = TRUE + relevant_mutant_bodypart = "psyphoza_cap" + +/datum/preference/choiced/psyphoza_cap/init_possible_values() + var/list/values = list() + + for (var/cap_name in GLOB.psyphoza_cap_list) + var/datum/sprite_accessory/cap = GLOB.psyphoza_cap_list[cap_name] + + var/icon/icon_with_cap = icon('icons/mob/species/psyphoza/bodyparts.dmi', "psyphoza_head", dir = SOUTH) + if (cap.icon_state != "none") + var/icon/screen_icon = icon(cap.icon, "m_psyphoza_cap_[cap.icon_state]_ADJ", dir = SOUTH) + icon_with_cap.Blend(screen_icon, ICON_OVERLAY) + icon_with_cap.Scale(64, 64) + icon_with_cap.Crop(15, 64, 15 + 31, 64 - 31) + + values[cap.name] = icon_with_cap + + return values + +/datum/preference/choiced/psyphoza_cap/apply_to_human(mob/living/carbon/human/target, value) + target.dna.features["psyphoza_cap"] = value diff --git a/code/modules/client/preferences/entries/character/underwear_color.dm b/code/modules/client/preferences/entries/character/underwear_color.dm new file mode 100644 index 0000000000000..78b1eeea58017 --- /dev/null +++ b/code/modules/client/preferences/entries/character/underwear_color.dm @@ -0,0 +1,15 @@ +/datum/preference/color_legacy/underwear_color + db_key = "underwear_color" + preference_type = 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, ignore_page = FALSE) + if (!..()) + return FALSE + + var/species_type = preferences.read_character_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/entries/character/uplink_location.dm b/code/modules/client/preferences/entries/character/uplink_location.dm new file mode 100644 index 0000000000000..6154c33889564 --- /dev/null +++ b/code/modules/client/preferences/entries/character/uplink_location.dm @@ -0,0 +1,26 @@ +/datum/preference/choiced/uplink_location + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + preference_type = PREFERENCE_CHARACTER + db_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/entries/player/admin.dm b/code/modules/client/preferences/entries/player/admin.dm new file mode 100644 index 0000000000000..9037723a2282e --- /dev/null +++ b/code/modules/client/preferences/entries/player/admin.dm @@ -0,0 +1,31 @@ +/datum/preference/color/asay_color + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "asaycolor" + preference_type = PREFERENCE_PLAYER + +/datum/preference/color/asay_color/create_default_value() + return DEFAULT_ASAY_COLOR + +/datum/preference/color/asay_color/is_accessible(datum/preferences/preferences, ignore_page = FALSE) + if (!..()) + return FALSE + + return is_admin(preferences.parent) && CONFIG_GET(flag/allow_admin_asaycolor) + +/datum/preference/toggle/announce_login + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "announce_login" + preference_type = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/announce_login/is_accessible(datum/preferences/preferences, ignore_page) + return ..() && is_admin(preferences.parent) + +/datum/preference/toggle/combohud_lighting + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "combohud_lighting" + preference_type = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/combohud_lighting/is_accessible(datum/preferences/preferences, ignore_page) + return ..() && is_admin(preferences.parent) diff --git a/code/modules/client/preferences/entries/player/ambient_occlusion.dm b/code/modules/client/preferences/entries/player/ambient_occlusion.dm new file mode 100644 index 0000000000000..5e94743ae3834 --- /dev/null +++ b/code/modules/client/preferences/entries/player/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 + db_key = "ambientocclusion" + preference_type = 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/entries/player/auto_fit_viewport.dm b/code/modules/client/preferences/entries/player/auto_fit_viewport.dm new file mode 100644 index 0000000000000..462b0601e7a3d --- /dev/null +++ b/code/modules/client/preferences/entries/player/auto_fit_viewport.dm @@ -0,0 +1,7 @@ +/datum/preference/toggle/auto_fit_viewport + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "auto_fit_viewport" + preference_type = 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/entries/player/buttons_locked.dm b/code/modules/client/preferences/entries/player/buttons_locked.dm new file mode 100644 index 0000000000000..faad04e9e7888 --- /dev/null +++ b/code/modules/client/preferences/entries/player/buttons_locked.dm @@ -0,0 +1,5 @@ +/datum/preference/toggle/buttons_locked + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "buttons_locked" + preference_type = PREFERENCE_PLAYER + default_value = FALSE diff --git a/code/modules/client/preferences/entries/player/chat.dm b/code/modules/client/preferences/entries/player/chat.dm new file mode 100644 index 0000000000000..066c5175a0281 --- /dev/null +++ b/code/modules/client/preferences/entries/player/chat.dm @@ -0,0 +1,79 @@ +/datum/preference/toggle/chat_bankcard + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_bankcard" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_dead + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_dead" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_dead/is_accessible(datum/preferences/preferences, ignore_page = FALSE) + if (!..()) + return FALSE + return is_admin(preferences.parent) + +/datum/preference/toggle/chat_followghostmindless + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_followghostmindless" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_ghostears + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_ghostears" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_ghostlaws + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_ghostlaws" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_ghostpda + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_ghostpda" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_ghostradio + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_ghostradio" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_ghostsight + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_ghostsight" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_ghostwhisper + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_ghostwhisper" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_ooc + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_ooc" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_prayer + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_prayer" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_prayer/is_accessible(datum/preferences/preferences, ignore_page = FALSE) + if (!..()) + return FALSE + return is_admin(preferences.parent) + +/datum/preference/toggle/chat_pullr + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_pullr" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_radio + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_radio" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/chat_radio/is_accessible(datum/preferences/preferences, ignore_page = FALSE) + if (!..()) + return FALSE + return is_admin(preferences.parent) diff --git a/code/modules/client/preferences/entries/player/crew_objectives.dm b/code/modules/client/preferences/entries/player/crew_objectives.dm new file mode 100644 index 0000000000000..4e6250fd4205c --- /dev/null +++ b/code/modules/client/preferences/entries/player/crew_objectives.dm @@ -0,0 +1,4 @@ +/datum/preference/toggle/crew_objectives + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "crew_objectives" + preference_type = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/entries/player/deadmin.dm b/code/modules/client/preferences/entries/player/deadmin.dm new file mode 100644 index 0000000000000..dd1b985529f98 --- /dev/null +++ b/code/modules/client/preferences/entries/player/deadmin.dm @@ -0,0 +1,69 @@ +/datum/preference/toggle/deadmin_always + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "deadmin_always" + preference_type = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/deadmin_always/is_accessible(datum/preferences/preferences, ignore_page) + return ..() && is_admin(preferences.parent) + +/datum/preference/toggle/deadmin_always/compile_constant_data() + return list( + "forced" = CONFIG_GET(flag/auto_deadmin_players), + ) + +/datum/preference/toggle/deadmin_antagonist + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "deadmin_antagonist" + preference_type = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/deadmin_antagonist/is_accessible(datum/preferences/preferences, ignore_page) + return ..() && is_admin(preferences.parent) && !preferences.read_player_preference(/datum/preference/toggle/deadmin_always) + +/datum/preference/toggle/deadmin_antagonist/compile_constant_data() + return list( + "forced" = CONFIG_GET(flag/auto_deadmin_antagonists), + ) + +/datum/preference/toggle/deadmin_position_head + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "deadmin_position_head" + preference_type = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/deadmin_position_head/is_accessible(datum/preferences/preferences, ignore_page) + return ..() && is_admin(preferences.parent) && !preferences.read_player_preference(/datum/preference/toggle/deadmin_always) + +/datum/preference/toggle/deadmin_position_head/compile_constant_data() + return list( + "forced" = CONFIG_GET(flag/auto_deadmin_heads), + ) + +/datum/preference/toggle/deadmin_position_security + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "deadmin_position_security" + preference_type = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/deadmin_position_security/is_accessible(datum/preferences/preferences, ignore_page) + return ..() && is_admin(preferences.parent) && !preferences.read_player_preference(/datum/preference/toggle/deadmin_always) + +/datum/preference/toggle/deadmin_position_security/compile_constant_data() + return list( + "forced" = CONFIG_GET(flag/auto_deadmin_security), + ) + +/datum/preference/toggle/deadmin_position_silicon + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "deadmin_position_silicon" + preference_type = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/deadmin_position_silicon/is_accessible(datum/preferences/preferences, ignore_page) + return ..() && is_admin(preferences.parent) && !preferences.read_player_preference(/datum/preference/toggle/deadmin_always) + +/datum/preference/toggle/deadmin_position_silicon/compile_constant_data() + return list( + "forced" = CONFIG_GET(flag/auto_deadmin_silicons), + ) diff --git a/code/modules/client/preferences/entries/player/fps.dm b/code/modules/client/preferences/entries/player/fps.dm new file mode 100644 index 0000000000000..dd5a2a28ee57b --- /dev/null +++ b/code/modules/client/preferences/entries/player/fps.dm @@ -0,0 +1,20 @@ +/datum/preference/numeric/fps + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "clientfps" + preference_type = PREFERENCE_PLAYER + + minimum = -1 + maximum = 240 + +/datum/preference/numeric/fps/create_default_value() + return -1 // use the default + +/datum/preference/numeric/fps/apply_to_client(client/client, value) + client.fps = (value < 0) ? 40 : value + +/datum/preference/numeric/fps/compile_constant_data() + var/list/data = ..() + + data["recommended_fps"] = 40 + + return data diff --git a/code/modules/client/preferences/entries/player/ghost.dm b/code/modules/client/preferences/entries/player/ghost.dm new file mode 100644 index 0000000000000..36e7aee4cf9e9 --- /dev/null +++ b/code/modules/client/preferences/entries/player/ghost.dm @@ -0,0 +1,151 @@ +/// Determines what accessories your ghost will look like they have. +/datum/preference/choiced/ghost_accessories + db_key = "ghost_accs" + preference_type = 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 + db_key = "ghost_form" + preference_type = PREFERENCE_PLAYER + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + should_generate_icons = TRUE + +/datum/preference/choiced/ghost_form/init_possible_values() + var/list/values = list() + + for (var/ghost_form in GLOB.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] = GLOB.ghost_forms + + return data + +/// Toggles the HUD for ghosts +/datum/preference/toggle/ghost_hud + db_key = "ghost_hud" + preference_type = 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 + db_key = "ghost_orbit" + preference_type = 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 + +/datum/preference/choiced/ghost_orbit/create_default_value() + return GHOST_ORBIT_CIRCLE + +/// Determines how to show other ghosts +/datum/preference/choiced/ghost_others + db_key = "ghost_others" + preference_type = 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 + db_key = "inquisitive_ghost" + preference_type = PREFERENCE_PLAYER + category = PREFERENCE_CATEGORY_GAME_PREFERENCES diff --git a/code/modules/client/preferences/entries/player/glasses.dm b/code/modules/client/preferences/entries/player/glasses.dm new file mode 100644 index 0000000000000..50fc409553dd5 --- /dev/null +++ b/code/modules/client/preferences/entries/player/glasses.dm @@ -0,0 +1,14 @@ +/datum/preference/toggle/glasses_color + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + default_value = FALSE + db_key = "glasses_color" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/glasses_color/apply_to_client(client/client, value) + if(!ishuman(client.mob)) + return + var/mob/living/carbon/human/H = client.mob + var/obj/item/clothing/glasses/G = H.glasses + if(!istype(G) || !G.glass_colour_type) + return + H.update_glasses_color(G, TRUE) diff --git a/code/modules/client/preferences/entries/player/hotkeys.dm b/code/modules/client/preferences/entries/player/hotkeys.dm new file mode 100644 index 0000000000000..f4c9a1b19f655 --- /dev/null +++ b/code/modules/client/preferences/entries/player/hotkeys.dm @@ -0,0 +1,7 @@ +/datum/preference/toggle/hotkeys + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "hotkeys" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/hotkeys/apply_to_client(client/client, value) + client.hotkeys = value diff --git a/code/modules/client/preferences/entries/player/item_outlines.dm b/code/modules/client/preferences/entries/player/item_outlines.dm new file mode 100644 index 0000000000000..a35900fbc2ace --- /dev/null +++ b/code/modules/client/preferences/entries/player/item_outlines.dm @@ -0,0 +1,12 @@ +/datum/preference/toggle/item_outlines + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "itemoutline_pref" + preference_type = PREFERENCE_PLAYER + +/datum/preference/color/outline_color + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "outline_color" + preference_type = PREFERENCE_PLAYER + +/datum/preference/color/outline_color/create_default_value() + return COLOR_BLUE_GRAY diff --git a/code/modules/client/preferences/entries/player/jobless_role.dm b/code/modules/client/preferences/entries/player/jobless_role.dm new file mode 100644 index 0000000000000..81ee431dd1935 --- /dev/null +++ b/code/modules/client/preferences/entries/player/jobless_role.dm @@ -0,0 +1,12 @@ +/datum/preference/choiced/jobless_role + db_key = "joblessrole" + preference_type = PREFERENCE_PLAYER + +/datum/preference/choiced/jobless_role/create_default_value() + return BEOVERFLOW + +/datum/preference/choiced/jobless_role/init_possible_values() + return list(BEOVERFLOW, BERANDOMJOB, RETURNTOLOBBY) + +/datum/preference/choiced/jobless_role/should_show_on_page(preference_tab) + return preference_tab == PREFERENCE_TAB_CHARACTER_PREFERENCES diff --git a/code/modules/client/preferences/entries/player/ooc.dm b/code/modules/client/preferences/entries/player/ooc.dm new file mode 100644 index 0000000000000..c0b044b83d973 --- /dev/null +++ b/code/modules/client/preferences/entries/player/ooc.dm @@ -0,0 +1,23 @@ +/// The color admins will speak in for OOC. +/datum/preference/color/ooc_color + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "ooccolor" + preference_type = PREFERENCE_PLAYER + +/datum/preference/color/ooc_color/create_default_value() + return DEFAULT_BONUS_OOC_COLOR + +/datum/preference/color/ooc_color/is_accessible(datum/preferences/preferences, ignore_page = FALSE) + if (!..()) + return FALSE + + return is_admin(preferences.parent) || preferences.unlock_content + +/datum/preference/toggle/member_public + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "member_public" + preference_type = PREFERENCE_PLAYER + default_value = TRUE + +/datum/preference/toggle/member_public/is_accessible(datum/preferences/preferences, ignore_page) + return ..() && preferences.unlock_content diff --git a/code/modules/client/preferences/entries/player/parallax.dm b/code/modules/client/preferences/entries/player/parallax.dm new file mode 100644 index 0000000000000..f2e4577c91e0d --- /dev/null +++ b/code/modules/client/preferences/entries/player/parallax.dm @@ -0,0 +1,38 @@ +/// Determines parallax, "fancy space" +/datum/preference/choiced/parallax + db_key = "parallax" + preference_type = 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/entries/player/pixel_size.dm b/code/modules/client/preferences/entries/player/pixel_size.dm new file mode 100644 index 0000000000000..4db8ded6fcec0 --- /dev/null +++ b/code/modules/client/preferences/entries/player/pixel_size.dm @@ -0,0 +1,15 @@ +/datum/preference/numeric/pixel_size + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "pixel_size" + preference_type = 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/entries/player/preferred_map.dm b/code/modules/client/preferences/entries/player/preferred_map.dm new file mode 100644 index 0000000000000..a6fa091a02223 --- /dev/null +++ b/code/modules/client/preferences/entries/player/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 + db_key = "preferred_map" + preference_type = PREFERENCE_PLAYER + +/datum/preference/choiced/preferred_map/init_possible_values() + var/list/maps = list() + maps += "Default" + + 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 "Default" + +/datum/preference/choiced/preferred_map/compile_constant_data() + var/list/data = ..() + + var/display_names = list() + + if (config.defaultmap) + display_names["Default"] = "Default ([config.defaultmap.map_name])" + else + display_names["Default"] = "Default" + + for (var/choice in get_choices()) + if (choice == "Default") + 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, ignore_page = FALSE) + if (!..()) + return FALSE + + return CONFIG_GET(flag/preference_map_voting) diff --git a/code/modules/client/preferences/entries/player/rattle.dm b/code/modules/client/preferences/entries/player/rattle.dm new file mode 100644 index 0000000000000..5fb0382cc47ae --- /dev/null +++ b/code/modules/client/preferences/entries/player/rattle.dm @@ -0,0 +1,9 @@ +/datum/preference/toggle/death_rattle + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "death_rattle" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/arrivals_rattle + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "arrivals_rattle" + preference_type = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/entries/player/roundend.dm b/code/modules/client/preferences/entries/player/roundend.dm new file mode 100644 index 0000000000000..968df7661c4e8 --- /dev/null +++ b/code/modules/client/preferences/entries/player/roundend.dm @@ -0,0 +1,4 @@ +/datum/preference/toggle/show_credits + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "show_credits" + preference_type = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/entries/player/runechat.dm b/code/modules/client/preferences/entries/player/runechat.dm new file mode 100644 index 0000000000000..af6e186397192 --- /dev/null +++ b/code/modules/client/preferences/entries/player/runechat.dm @@ -0,0 +1,25 @@ +/datum/preference/toggle/enable_runechat + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "chat_on_map" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/enable_runechat_non_mobs + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "see_chat_non_mob" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/see_rc_emotes + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "see_rc_emotes" + preference_type = PREFERENCE_PLAYER + +/datum/preference/choiced/show_balloon_alerts + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "show_balloon_alerts" + preference_type = PREFERENCE_PLAYER + +/datum/preference/choiced/show_balloon_alerts/create_default_value() + return BALLOON_ALERT_ALWAYS + +/datum/preference/choiced/show_balloon_alerts/init_possible_values() + return list(BALLOON_ALERT_ALWAYS, BALLOON_ALERT_WITH_CHAT, BALLOON_ALERT_NEVER) diff --git a/code/modules/client/preferences/entries/player/scaling_method.dm b/code/modules/client/preferences/entries/player/scaling_method.dm new file mode 100644 index 0000000000000..920400d64c6df --- /dev/null +++ b/code/modules/client/preferences/entries/player/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 + db_key = "scaling_method" + preference_type = 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/entries/player/sound.dm b/code/modules/client/preferences/entries/player/sound.dm new file mode 100644 index 0000000000000..909f7c3606368 --- /dev/null +++ b/code/modules/client/preferences/entries/player/sound.dm @@ -0,0 +1,88 @@ +/datum/preference/toggle/sound_adminhelp + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_adminhelp" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_adminhelp/is_accessible(datum/preferences/preferences, ignore_page = FALSE) + if (!..()) + return FALSE + + return is_admin(preferences.parent) + +/datum/preference/toggle/sound_midi + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_midi" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_midi/apply_to_client(client/client, value) + if(!value) + client.mob?.stop_sound_channel(CHANNEL_ADMIN) + client.tgui_panel?.stop_music() + +/datum/preference/toggle/sound_ambience + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_ambience" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_ambience/apply_to_client(client/client, value) + if(value) + SSambience.add_ambience_client(client) + else + client.mob?.stop_sound_channel(CHANNEL_AMBIENT_EFFECTS) + client.mob?.stop_sound_channel(CHANNEL_AMBIENT_MUSIC) + client.mob?.stop_sound_channel(CHANNEL_BUZZ) + client.buzz_playing = FALSE + SSambience.remove_ambience_client(client) + +/datum/preference/toggle/sound_lobby + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_lobby" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_lobby/apply_to_client(client/client, value) + if (value && isnewplayer(client.mob)) + if(SSticker.login_music) + client.playtitlemusic() + else + client.mob?.stop_sound_channel(CHANNEL_LOBBYMUSIC) + +/datum/preference/toggle/sound_instruments + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_instruments" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_ship_ambience + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_ship_ambience" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_ship_ambience/apply_to_client(client/client, value) + if(!value) + client.mob?.stop_sound_channel(CHANNEL_BUZZ) + client.buzz_playing = FALSE + +/datum/preference/toggle/sound_prayers + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_prayers" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_adminalert + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_adminalert" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_announcements + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_announcements" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_soundtrack + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "sound_soundtrack" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/sound_soundtrack/apply_to_client(client/client, value) + if (value) + client.mob?.play_current_soundtrack() + else + client.mob?.stop_sound_channel(CHANNEL_SOUNDTRACK) diff --git a/code/modules/client/preferences/entries/player/tgui.dm b/code/modules/client/preferences/entries/player/tgui.dm new file mode 100644 index 0000000000000..a6c1af54a4e12 --- /dev/null +++ b/code/modules/client/preferences/entries/player/tgui.dm @@ -0,0 +1,77 @@ +/datum/preference/toggle/tgui_fancy + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "tgui_fancy" + preference_type = 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 + db_key = "tgui_lock" + preference_type = 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) + +// Determines if input boxes are in tgui or old fashioned +/datum/preference/toggle/tgui_input + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "tgui_input" + preference_type = PREFERENCE_PLAYER + +/// Large button preference. Error text is in tooltip. +/datum/preference/toggle/tgui_input_large + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "tgui_input_large" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/tgui_input_large/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.send_full_update(client.mob) + +/// Swapped button state - sets buttons to SS13 traditional SUBMIT/CANCEL +/datum/preference/toggle/tgui_input_swapped + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "tgui_input_swapped" + preference_type = PREFERENCE_PLAYER + +/datum/preference/toggle/tgui_input_swapped/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.send_full_update(client.mob) + +/// TGUI Say vs Classic Say +/datum/preference/toggle/tgui_say + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "tgui_say" + preference_type = PREFERENCE_PLAYER + default_value = TRUE + +/datum/preference/toggle/tgui_say/apply_to_client(client/client) + client.tgui_say?.load() + +/// Light mode for tgui say +/datum/preference/toggle/tgui_say_light_mode + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "tgui_say_light_mode" + preference_type = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/tgui_say_light_mode/apply_to_client(client/client) + client.tgui_say?.load() + +/datum/preference/toggle/tgui_say_show_prefix + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "tgui_say_show_prefix" + preference_type = PREFERENCE_PLAYER + default_value = FALSE + +/datum/preference/toggle/tgui_say_show_prefix/apply_to_client(client/client) + client.tgui_say?.load() diff --git a/code/modules/client/preferences/entries/player/tooltips.dm b/code/modules/client/preferences/entries/player/tooltips.dm new file mode 100644 index 0000000000000..c5ca3b632f5db --- /dev/null +++ b/code/modules/client/preferences/entries/player/tooltips.dm @@ -0,0 +1,15 @@ +/datum/preference/numeric/tooltip_delay + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "tip_delay" + preference_type = 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 + db_key = "enable_tips" + preference_type = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/entries/player/ui_style.dm b/code/modules/client/preferences/entries/player/ui_style.dm new file mode 100644 index 0000000000000..ba987e5a336e2 --- /dev/null +++ b/code/modules/client/preferences/entries/player/ui_style.dm @@ -0,0 +1,31 @@ +/// UI style preference +/datum/preference/choiced/ui_style + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + preference_type = PREFERENCE_PLAYER + db_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)) + +/datum/preference/toggle/intent_style + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + db_key = "intent_style" + preference_type = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/entries/player/window_flashing.dm b/code/modules/client/preferences/entries/player/window_flashing.dm new file mode 100644 index 0000000000000..4cc7d370109ed --- /dev/null +++ b/code/modules/client/preferences/entries/player/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 + db_key = "windowflashing" + preference_type = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/middleware/_middleware.dm b/code/modules/client/preferences/middleware/_middleware.dm new file mode 100644 index 0000000000000..8f47f73642c80 --- /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 0000000000000..c49693f3783ac --- /dev/null +++ b/code/modules/client/preferences/middleware/antags.dm @@ -0,0 +1,167 @@ +/datum/preference_middleware/antags + action_delegations = list( + "set_antags" = PROC_REF(set_antags), + ) + +/datum/preference_middleware/antags/get_ui_data(mob/user) + if (preferences.current_window != PREFERENCE_TAB_CHARACTER_PREFERENCES) + return list() + var/list/data = list() + var/list/enabled_global = list() + var/list/enabled_character = list() + for(var/datum/role_preference/pref_type as anything in GLOB.role_preference_entries) + var/role_preference_value = preferences.role_preferences_global["[pref_type]"] + if(isnum(role_preference_value) && !role_preference_value) // explicitly disabled + continue + enabled_global += "[pref_type]" + + for(var/datum/role_preference/pref_type as anything in GLOB.role_preference_entries) + if(!initial(pref_type.per_character)) + continue + var/role_preference_value = preferences.role_preferences["[pref_type]"] + if(isnum(role_preference_value) && !role_preference_value) // explicitly disabled + continue + enabled_character += "[pref_type]" + data["enabled_global"] = enabled_global + data["enabled_character"] = enabled_character + return data + +/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/antag_bans = get_antag_bans() + if (length(antag_bans)) + data["antag_bans"] = antag_bans + var/list/antag_living_playtime_hours_left = get_antag_living_playtime_hours_left() + if (length(antag_living_playtime_hours_left)) + data["antag_living_playtime_hours_left"] = antag_living_playtime_hours_left + return data + +/datum/preference_middleware/antags/get_constant_data() + var/list/antags = list() + + for(var/pref_type in GLOB.role_preference_entries) + var/datum/role_preference/pref = GLOB.role_preference_entries[pref_type] + var/datum/antagonist/antag_datum = pref.antag_datum + antags += list(list( + "name" = pref.name, + "description" = pref.description, + "category" = pref.category, + "per_character" = pref.per_character, + "ban_key" = ispath(antag_datum, /datum/antagonist) ? initial(antag_datum.banning_key) : null, + "path" = "[pref_type]", + "icon_path" = "[serialize_antag_name("[pref.use_icon || pref_type]")]" + )) + + return list( + "antagonists" = antags, + "categories" = GLOB.role_preference_categories, + ) + +/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/per_character = params["character"] + + var/list/valid_antags = list() + for(var/datum/role_preference/type as anything in GLOB.role_preference_entries) + if(per_character && !initial(type.per_character)) + continue + valid_antags += "[type]" + + var/any_changed = FALSE + for (var/sent_antag in sent_antags) + if(!(sent_antag in valid_antags)) + continue + if(per_character) + preferences.role_preferences["[sent_antag]"] = toggled + else + preferences.role_preferences_global["[sent_antag]"] = toggled + any_changed = TRUE + if(any_changed) + if(per_character) + preferences.mark_undatumized_dirty_character() + else + preferences.mark_undatumized_dirty_player() + return any_changed + +/datum/preference_middleware/antags/proc/get_antag_bans() + var/list/antag_bans = list() + for(var/type in GLOB.role_preference_entries) + var/datum/role_preference/pref = GLOB.role_preference_entries[type] + var/datum/antagonist/antag_datum = pref.antag_datum + if(!ispath(antag_datum, /datum/antagonist)) + continue + var/role_ban_key = initial(antag_datum.banning_key) + if(role_ban_key && is_banned_from(preferences.parent.ckey, role_ban_key)) + antag_bans += role_ban_key + return antag_bans + +/datum/preference_middleware/antags/proc/get_antag_living_playtime_hours_left() + var/list/antag_living_playtime_hours_left = list() + + for(var/type in GLOB.role_preference_entries) + var/datum/role_preference/pref = GLOB.role_preference_entries[type] + var/datum/antagonist/antag_datum = pref.antag_datum + if(!ispath(antag_datum, /datum/antagonist)) + continue + var/living_hours_needed = initial(antag_datum.required_living_playtime) + if (living_hours_needed <= 0) + continue + var/hours_left = max(0, living_hours_needed - (preferences.parent.get_exp_living(TRUE) / 60)) + if(hours_left > 0) + antag_living_playtime_hours_left["[type]"] = hours_left + + return antag_living_playtime_hours_left + +/// Sprites generated for the antagonists panel +/datum/asset/spritesheet/antagonists + name = "antagonists" + early = TRUE + cross_round_cachable = TRUE + +/datum/asset/spritesheet/antagonists/create_spritesheets() + var/list/generated_icons = list() + var/list/to_insert = list() + + for(var/pref_type in GLOB.role_preference_entries) + var/datum/role_preference/pref = GLOB.role_preference_entries[pref_type] + if(ispath(pref.use_icon, /datum/role_preference)) + pref_type = pref.use_icon + var/datum/role_preference/other_pref = GLOB.role_preference_entries[pref.use_icon] + if(istype(other_pref)) + pref = other_pref + + // antag_flag is guaranteed to be unique by unit tests. + var/spritesheet_key = serialize_antag_name("[pref_type]") + + if (!isnull(generated_icons["[pref_type]"])) + to_insert[spritesheet_key] = generated_icons["[pref_type]"] + continue + + var/icon/preview_icon = pref.get_preview_icon() + + if (isnull(preview_icon)) + continue + + // 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["[pref_type]"] = preview_icon + to_insert[spritesheet_key] = preview_icon + + for (var/spritesheet_key in to_insert) + Insert(spritesheet_key, to_insert[spritesheet_key]) + +/// Serializes an antag name to be used for preferences UI +/proc/serialize_antag_name(antag_name) + // These are sent through CSS, so they need to be safe to use as class names. + return lowertext(sanitize_css_class_name(replacetext(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 0000000000000..428dccd96b3d3 --- /dev/null +++ b/code/modules/client/preferences/middleware/jobs.dm @@ -0,0 +1,131 @@ +/datum/preference_middleware/jobs + action_delegations = list( + "set_job_preference" = PROC_REF(set_job_preference), + "clear_job_preferences" = PROC_REF(clear_job_preferences), + ) + +/datum/preference_middleware/jobs/proc/clear_job_preferences(list/params, mob/user) + preferences.job_preferences = list() + preferences.character_preview_view?.update_body() + preferences.mark_undatumized_dirty_character() + return TRUE + +/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 != "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_constant_data() + var/list/data = list() + + var/list/departments = list() + var/list/jobs = list() + + for (var/datum/job/job as anything in SSjob.occupations) + if(!job.show_in_prefs) + continue + + var/department_flag = job.department_for_prefs + if (isnull(department_flag)) + stack_trace("[job] does not have a department set, yet is a joinable occupation!") + continue + + if (isnull(job.description)) + stack_trace("[job] does not have a description set, yet is a joinable occupation!") + continue + + var/department_name = GLOB.dept_bitflag_to_name["[department_flag]"] + if (isnull(departments[department_name])) + var/department_head_jobname = job.department_head_for_prefs || job.department_head + if(islist(department_head_jobname) && length(department_head_jobname)) + department_head_jobname = department_head_jobname[1] + if(length(department_head_jobname)) + departments[department_name] = list( + "head" = department_head_jobname, + ) + else + departments[department_name] = list() + + jobs[job.title] = list( + "description" = job.description, + "department" = department_name, + ) + + data["departments"] = departments + data["jobs"] = jobs + + return data + +/datum/preference_middleware/jobs/get_ui_data(mob/user) + var/list/data = list() + + data["job_preferences"] = preferences.job_preferences + + return data + +/datum/preference_middleware/jobs/get_ui_static_data(mob/user) + var/list/data = list() + + var/list/required_job_playtime = get_required_job_playtime(user) + if (!isnull(required_job_playtime)) + data += required_job_playtime + + var/list/job_bans = get_job_bans(user) + if (job_bans.len) + data["job_bans"] = job_bans + + return data.len > 0 ? data : null + +/datum/preference_middleware/jobs/proc/get_required_job_playtime(mob/user) + var/list/data = list() + + var/list/job_days_left = list() + var/list/job_required_experience = list() + + for (var/datum/job/job as anything in SSjob.occupations) + if(!job.show_in_prefs) + continue + var/required_playtime_remaining = job.required_playtime_remaining(user.client) + if (required_playtime_remaining) + job_required_experience[job.title] = list( + "experience_type" = job.get_exp_req_type(), + "required_playtime" = required_playtime_remaining, + ) + + continue + + if (!job.player_old_enough(user.client)) + job_days_left[job.title] = job.available_in_days(user.client) + + if (job_days_left.len) + data["job_days_left"] = job_days_left + + if (job_required_experience) + data["job_required_experience"] = job_required_experience + + return data + +/datum/preference_middleware/jobs/proc/get_job_bans(mob/user) + var/list/data = list() + + for (var/datum/job/job as anything in SSjob.occupations) + if (is_banned_from(user.client?.ckey, job.title)) + data += job.title + + return data diff --git a/code/modules/client/preferences/middleware/keybindings.dm b/code/modules/client/preferences/middleware/keybindings.dm new file mode 100644 index 0000000000000..56910ee311e3a --- /dev/null +++ b/code/modules/client/preferences/middleware/keybindings.dm @@ -0,0 +1,95 @@ +/// Number of unique keycombos allowed to be bound to one keybinding +#define MAX_HOTKEY_SLOTS 3 + +/// Middleware to handle keybindings +/datum/preference_middleware/keybindings + action_delegations = list( + "reset_all_keybinds" = PROC_REF(reset_all_keybinds), + "reset_keybinds_to_defaults" = PROC_REF(reset_keybinds_to_defaults), + "set_keybindings" = PROC_REF(set_keybindings), + ) + +/datum/preference_middleware/keybindings/get_ui_static_data(mob/user) + if (preferences.current_window == PREFERENCE_TAB_CHARACTER_PREFERENCES) + return list() + + var/list/keybindings = preferences.key_bindings + + return list( + "keybindings" = keybindings, + ) + +/datum/preference_middleware/keybindings/get_ui_assets() + return list( + get_asset_datum(/datum/asset/json/keybindings) + ) + +/datum/preference_middleware/keybindings/proc/reset_all_keybinds(list/params, mob/user) + preferences.set_default_key_bindings(save = TRUE) + preferences.update_static_data(user) + + return TRUE + +/datum/preference_middleware/keybindings/proc/reset_keybinds_to_defaults(list/params, mob/user) + var/keybind_name = params["keybind_name"] + var/datum/keybinding/keybinding = GLOB.keybindings_by_name[keybind_name] + + if (isnull(keybinding)) + return FALSE + + preferences.key_bindings[keybind_name] = keybinding.keys + + preferences.update_static_data(user) + preferences.mark_undatumized_dirty_player() + + 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.set_keybind(keybind_name, hotkeys) + 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/loadout.dm b/code/modules/client/preferences/middleware/loadout.dm new file mode 100644 index 0000000000000..aab738899f3ec --- /dev/null +++ b/code/modules/client/preferences/middleware/loadout.dm @@ -0,0 +1,95 @@ +/datum/preference_middleware/loadout + action_delegations = list( + "purchase_gear" = PROC_REF(purchase_gear), + "equip_gear" = PROC_REF(equip_gear), + ) + +/datum/preference_middleware/loadout/get_ui_data(mob/user) + var/list/data = list() + data["equipped_gear"] = preferences.equipped_gear + data["purchased_gear"] = preferences.purchased_gear + data["metacurrency_balance"] = preferences.parent.get_metabalance_unreliable() + data["is_donator"] = (IS_PATRON(preferences.parent.ckey) || is_admin(preferences.parent)) + return data + +/datum/preference_middleware/loadout/get_constant_data() + var/list/data = list() + var/list/categories = list() + for(var/category_id in GLOB.loadout_categories) + var/datum/loadout_category/LC = GLOB.loadout_categories[category_id] + if(LC.category == "Donator" && !CONFIG_GET(flag/donator_items)) // Don't show donator items if the server has them off + continue + var/list/category = list() + category["name"] = LC.category + var/list/gear = list() + for(var/gear_id in LC.gear) + var/datum/gear/G = LC.gear[gear_id] + var/list/gear_entry = list() + gear_entry["id"] = G.id + gear_entry["display_name"] = G.display_name + gear_entry["skirt_display_name"] = G.skirt_display_name + gear_entry["donator"] = G.sort_category == "Donator" + gear_entry["cost"] = G.cost + gear_entry["description"] = G.description + gear_entry["skirt_description"] = G.skirt_description + gear_entry["allowed_roles"] = G.allowed_roles + gear_entry["is_equippable"] = G.is_equippable + gear_entry["multi_purchase"] = G.multi_purchase + gear += list(gear_entry) + category["gear"] = gear + categories += list(category) + data["categories"] = categories + data["metacurrency_name"] = CONFIG_GET(string/metacurrency_name) + return data + +/datum/preference_middleware/loadout/proc/purchase_gear(list/params, mob/user) + var/datum/gear/TG = GLOB.gear_datums[params["id"]] + if(!istype(TG)) + return + if(((TG.id in preferences.purchased_gear) || (TG.id in preferences.equipped_gear)) && !TG.multi_purchase) + to_chat(user, "You already own \the [TG.display_name]!") + return TRUE + if(TG.sort_category == "Donator") + if(user.client && CONFIG_GET(flag/donator_items) && alert(user.client, "This item is only accessible to our patrons. Would you like to subscribe?", "Patron Locked", "Yes", "No") == "Yes") + user.client.donate() + return + + if(TG.cost <= user.client.get_metabalance_db()) + preferences.purchased_gear += TG.id + TG.purchase(user.client) + user.client.inc_metabalance((TG.cost * -1), TRUE, "Purchased [TG.display_name].") + preferences.mark_undatumized_dirty_player() + return TRUE + else + to_chat(user, "You don't have enough [CONFIG_GET(string/metacurrency_name)]s to purchase \the [TG.display_name]!") + +/datum/preference_middleware/loadout/proc/equip_gear(list/params, mob/user) + var/datum/gear/TG = GLOB.gear_datums[params["id"]] + if(!istype(TG)) + return + if(TG.id in preferences.equipped_gear) + preferences.equipped_gear -= TG.id + preferences.character_preview_view?.update_body() + preferences.mark_undatumized_dirty_character() + return TRUE + else + var/list/type_blacklist = list() + var/list/slot_blacklist = list() + for(var/gear_id in preferences.equipped_gear) + var/datum/gear/G = GLOB.gear_datums[gear_id] + if(istype(G)) + if(!(G.subtype_path in type_blacklist)) + type_blacklist += G.subtype_path + if(!(G.slot in slot_blacklist)) + slot_blacklist += G.slot + if((TG.id in preferences.purchased_gear)) + if(!(TG.subtype_path in type_blacklist) || !(TG.slot in slot_blacklist)) + preferences.equipped_gear += TG.id + preferences.character_preview_view?.update_body() + preferences.mark_undatumized_dirty_character() + return TRUE + else + to_chat(user, "Can't equip [TG.display_name]. It conflicts with an already-equipped item.") + else + log_href_exploit(user, "Attempting to equip [TG.type] when they do not own it.") + return TRUE diff --git a/code/modules/client/preferences/middleware/names.dm b/code/modules/client/preferences/middleware/names.dm new file mode 100644 index 0000000000000..4f9e716a2e304 --- /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_REF(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.db_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.db_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), in_menu = TRUE) diff --git a/code/modules/client/preferences/middleware/quirks.dm b/code/modules/client/preferences/middleware/quirks.dm new file mode 100644 index 0000000000000..874afb70103d7 --- /dev/null +++ b/code/modules/client/preferences/middleware/quirks.dm @@ -0,0 +1,92 @@ +/// Middleware to handle quirks + +/datum/preference_middleware/quirks + var/tainted = FALSE + + action_delegations = list( + "give_quirk" = PROC_REF(give_quirk), + "remove_quirk" = PROC_REF(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() + + var/list/quirks = SSquirks.get_quirks() + + for (var/quirk_name in quirks) + var/datum/quirk/quirk = 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 + preferences.mark_undatumized_dirty_character() + 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 + preferences.mark_undatumized_dirty_character() + 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 0000000000000..eca9d1cdaddbc --- /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_REF(randomize_character), + "set_random_preference" = PROC_REF(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.db_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(preferences)) + + 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 + preferences.mark_undatumized_dirty_character() + 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.db_key] + + if (istype(preference, /datum/preference/name)) + requested_randomization = read_character_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 0000000000000..02efe1e223a5b --- /dev/null +++ b/code/modules/client/preferences/middleware/species.dm @@ -0,0 +1,35 @@ +/// Handles the assets for species icons +/datum/preference_middleware/species + +/datum/preference_middleware/species/get_ui_assets() + return list( + get_asset_datum(/datum/asset/spritesheet/species), + ) + +/datum/asset/spritesheet/species + name = "species" + early = TRUE + cross_round_cachable = TRUE + +/datum/asset/spritesheet/species/create_spritesheets() + var/list/to_insert = list() + + for (var/species_id in get_selectable_species()) + var/datum/species/species_type = GLOB.species_list[species_id] + + var/mob/living/carbon/human/dummy/consistent/dummy = new + dummy.set_species(species_type) + dummy.equipOutfit(/datum/outfit/job/assistant/consistent, visualsOnly = TRUE) + dummy.dna.species.prepare_human_for_preview(dummy) + 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]) diff --git a/code/modules/client/preferences/preference_entry.dm b/code/modules/client/preferences/preference_entry.dm new file mode 100644 index 0000000000000..68688954875fe --- /dev/null +++ b/code/modules/client/preferences/preference_entry.dm @@ -0,0 +1,553 @@ +// 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 model is decided, applied after gender so we can +/// make sure they're non-binary. +#define PREFERENCE_PRIORITY_BODY_MODEL 4 + +/// The priority at which eye color is applied, needed so IPCs get the right screen color. +#define PREFERENCE_PRIORITY_EYE_COLOR 5 + +/// The priority at which names are decided, needed for proper randomization. +#define PREFERENCE_PRIORITY_NAMES 6 + +/// 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 `db_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.db_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 database to use. + /// This is also sent to the UI. + /// Once you pick this, don't change it. + var/db_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 location should this preference be read from? + /// Valid values are PREFERENCE_CHARACTER and PREFERENCE_PLAYER. + /// See the documentation in [code/__DEFINES/preferences.dm]. + var/preference_type + + /// The priority of when to apply this preference. + /// Used for when you need to rely on another preference. + var/priority = PREFERENCE_PRIORITY_DEFAULT + + /// If set, will be available to randomize, but only if the preference + /// is for PREFERENCE_CHARACTER. + var/can_randomize = TRUE + + /// If randomizable (PREFERENCE_CHARACTER and can_randomize), whether + /// or not to enable randomization by default. + /// This doesn't mean it'll always be random, but rather if a player + /// DOES have random body on, will this already be randomized? + var/randomize_by_default = TRUE + + /// If the selected species has this in its /datum/species/mutant_bodyparts, + /// will show the feature as selectable. + var/relevant_mutant_bodypart = null + + /// If the selected species has this in its /datum/species/species_traits, + /// will show the feature as selectable. + var/relevant_species_trait = null + + /// If this requires create_informed_default_value + var/informed = FALSE + +/// Called on the saved input when retrieving. +/// Also called by the value sent from the user through UI. Do not trust it. +/// Input is the value inside the database, 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 database. +/// For PREFERENCE_PLAYER, this will be to a string, for PREFERENCE_CHARACTER, it varies +/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 database. +/// Either this or create_informed_default_value must be overriden by subtypes. +/// For PREFERENCE_PLAYER, this will be from a string, for PREFERENCE_CHARACTER, it varies +/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 database. +/// 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 preference_type == PREFERENCE_CHARACTER && can_randomize + +/// Apply this preference onto the given client. +/// Called when the preference_type == 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 preference_type == PREFERENCE_CHARACTER. +/datum/preference/proc/apply_to_human(mob/living/carbon/human/target, value) + SHOULD_NOT_SLEEP(TRUE) + SHOULD_CALL_PARENT(FALSE) + CRASH("`apply_to_human()` was not implemented for [type]!") + +/datum/preferences/proc/get_preference_holder(datum/preference/preference_entry) + RETURN_TYPE(/datum/preferences_holder) + if(preference_entry.preference_type == PREFERENCE_CHARACTER) + return character_data + return player_data + +/// Read a /datum/preference type and return its value, only using cached values and queueing any necessary writes. +/datum/preferences/proc/read_preference(preference_typepath) + var/datum/preference/preference_entry = GLOB.preference_entries[preference_typepath] + 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_typepath]` is invalid! [extra_info]") + return get_preference_holder(preference_entry).read_preference(src, preference_entry) + +/// Read a /datum/preference type and return its value, only using cached values and queueing any necessary writes. +/// Only works for player preferences. +/datum/preferences/proc/read_player_preference(preference_typepath) + var/datum/preference/preference_entry = GLOB.preference_entries[preference_typepath] + 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_typepath]` is invalid! [extra_info]") + + if (preference_entry.preference_type == PREFERENCE_CHARACTER) + CRASH("read_player_preference called on PREFERENCE_CHARACTER type preference [preference_typepath].") + + return player_data.read_preference(src, preference_entry) + +/// Read a /datum/preference type and return its value, only using cached values and queueing any necessary writes. +/// Only works for character preferences. +/datum/preferences/proc/read_character_preference(preference_typepath) + var/datum/preference/preference_entry = GLOB.preference_entries[preference_typepath] + 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_typepath]` is invalid! [extra_info]") + + if (preference_entry.preference_type == PREFERENCE_PLAYER) + CRASH("read_character_preference called on PREFERENCE_PLAYER type preference [preference_typepath].") + + return character_data.read_preference(src, preference_entry) + +/// 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) + return get_preference_holder(preference).write_preference(src, preference, preference_value) + +/// Will perform a write on the preference and update the relevant locations. +/// This will, for instance, update the character preference view. +/// Performs sanity checks. +/datum/preferences/proc/update_preference(preference_or_typepath, preference_value, in_menu = FALSE) + var/datum/preference/preference + if (ispath(preference_or_typepath, /datum/preference)) + preference = GLOB.preference_entries[preference_or_typepath] + else if (istype(preference_or_typepath, /datum/preference)) + preference = preference_or_typepath + if (isnull(preference)) + CRASH("Preference type `[preference_or_typepath]` is invalid!") + + if (!preference.is_accessible(src, ignore_page = !in_menu)) + return FALSE + + write_preference(preference, preference_value) + + if (preference.preference_type == PREFERENCE_PLAYER) + preference.apply_to_client_updated(parent, read_preference(preference.type)) + else + character_preview_view?.update_body() + + // A non-preference menu source changed a preference. We should send new preferences now. + if(!in_menu) + ui_update() + + 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, ignore_page = FALSE) + SHOULD_CALL_PARENT(TRUE) + SHOULD_NOT_SLEEP(TRUE) + + if (!isnull(relevant_mutant_bodypart) || !isnull(relevant_species_trait)) + var/species_type = preferences.read_character_preference(/datum/preference/choiced/species) + + var/datum/species/species = new species_type + if (!(db_key in species.get_features())) + return FALSE + + if (!ignore_page && !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 = preference_type == 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 + + /// Which spritesheet this preference should go on. This is used for particularly massive choice lists to reduce mount lag. + var/preference_spritesheet = PREFERENCE_SHEET_NORMAL + + 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 + data["icon_sheet"] = preference_spritesheet + + 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_hexcolor(input, desired_format = 6, include_crunch = TRUE) + +/datum/preference/color/create_default_value() + return random_color() + +/datum/preference/color/serialize(input) + return sanitize_hexcolor(input, desired_format = 6, include_crunch = TRUE) + +/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) + if(istext(input)) // Sometimes TGUI will return a string instead of a number, so we take that into account. + input = text2num(input) // Worst case, it's null, it'll just use create_default_value() + return sanitize_float(input, minimum, maximum, step, create_default_value()) + +/datum/preference/numeric/serialize(input) + return sanitize_float(input, minimum, maximum, step, create_default_value()) + +/datum/preference/numeric/create_default_value() + return rand(minimum, maximum) + +/datum/preference/numeric/is_valid(value) + return isnum(value) && value >= round(minimum, step) && value <= round(maximum, step) + +/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) + if(istext(input)) + input = text2num(input) + return !!input + +/datum/preference/toggle/is_valid(value) + return value == TRUE || value == FALSE + +/// A simple string type preference. +/datum/preference/string + abstract_type = /datum/preference/string + + /// The default value of the string, if create_default_value is not specified + var/default_value = "" + +/datum/preference/string/create_default_value() + return default_value + +/datum/preference/string/deserialize(input, datum/preferences/preferences) + return sanitize_text(input, create_default_value()) + +/datum/preference/string/is_valid(value) + return istext(value) diff --git a/code/modules/client/preferences/preference_verbs.dm b/code/modules/client/preferences/preference_verbs.dm new file mode 100644 index 0000000000000..f70187747edb7 --- /dev/null +++ b/code/modules/client/preferences/preference_verbs.dm @@ -0,0 +1,25 @@ +/client/verb/open_character_preferences() + set category = "Preferences" + set name = "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) + +/client/verb/open_game_preferences() + set category = "Preferences" + set name = "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/preference_verbs_toggles.dm b/code/modules/client/preferences/preference_verbs_toggles.dm new file mode 100644 index 0000000000000..d953bb9dd92e8 --- /dev/null +++ b/code/modules/client/preferences/preference_verbs_toggles.dm @@ -0,0 +1,43 @@ +/client/verb/toggletitlemusic() + set name = "Hear/Silence Lobby Music" + set category = "Preferences" + set desc = "Hear Music In Lobby" + var/hear = !prefs.read_player_preference(/datum/preference/toggle/sound_lobby) + prefs.update_preference(/datum/preference/toggle/sound_lobby, hear) + if(hear) + to_chat(usr, "You will now hear music in the game lobby.") + else + to_chat(usr, "You will no longer hear music in the game lobby.") + SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Lobby Music", "[hear ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/client/verb/Toggle_Soundscape() + set name = "Hear/Silence Ambience" + set category = "Preferences" + set desc = "Hear Ambient Sound Effects" + var/hear = !prefs.read_player_preference(/datum/preference/toggle/sound_ambience) + prefs.update_preference(/datum/preference/toggle/sound_ambience, hear) + if(hear) + to_chat(usr, "You will now hear ambient sounds.") + else + to_chat(usr, "You will no longer hear ambient sounds.") + SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ambience", "[hear ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/client/verb/toggle_ship_ambience() + set name = "Hear/Silence Ship Ambience" + set category = "Preferences" + set desc = "Hear Ship Ambience Roar" + var/hear = !prefs.read_player_preference(/datum/preference/toggle/sound_ship_ambience) + prefs.update_preference(/datum/preference/toggle/sound_ship_ambience, hear) + if(hear) + to_chat(usr, "You will now hear ship ambience.") + else + to_chat(usr, "You will no longer hear ship ambience.") + SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ship Ambience", "[hear ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, I bet you read this comment expecting to see the same thing :^) + +/client/verb/stop_client_sounds() + set name = "Stop Sounds" + set category = "Preferences" + 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")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/client/preferences/preferences.dm b/code/modules/client/preferences/preferences.dm new file mode 100644 index 0000000000000..4fd89a32c62e6 --- /dev/null +++ b/code/modules/client/preferences/preferences.dm @@ -0,0 +1,396 @@ +GLOBAL_LIST_EMPTY(preferences_datums) + +/datum/preferences + var/client/parent + + /// The current active slot, and the one that will be saved as active + var/default_slot = 1 + /// The maximum number of slots we're allowed to contain + var/max_save_slots = 3 + /// Cache for the current active character slot + var/datum/preferences_holder/preferences_character/character_data + /// Cache for player datumized preferences + var/datum/preferences_holder/preferences_player/player_data + + /// Bitflags for communications that are muted + var/muted = NONE + /// Last IP that this client has connected from + var/last_ip + /// Last CID that this client has connected from + var/last_id + + // pAI profile + var/pai_name = "" + var/pai_description = "" + var/pai_comment = "" + + /// Cached changelog size, to detect new changelogs since last join + var/lastchangelog = "" + + /// List of ROLE_X that the client wants to be eligible for (PER CHARACTER) + /// Use /client/proc/role_preference_enabled() please + var/list/role_preferences = list() + + /// List of ROLE_X that the client wants to be eligible for (GLOBALLY) + /// Use /client/proc/role_preference_enabled() please + var/list/role_preferences_global = list() + + /// Custom keybindings. Map of keybind names to keyboard inputs. + /// For example, by default would have "swap_hands" -> "X" + var/list/key_bindings = list() + + /// 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/db_flags + + //character preferences + var/slot_randomized //keeps track of round-to-round randomization of the character slot, prevents overwriting + + var/list/randomise = list() + + //Quirk list + var/list/all_quirks = list() + + //Job preferences 2.0 - indexed by job title , no key or value implies never + var/list/job_preferences = list() + + /// The current window, PREFERENCE_TAB_* in [`code/__DEFINES/preferences.dm`] + var/current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES + + /// If the user is a BYOND Member + var/unlock_content = 0 + + var/list/ignoring = list() + + var/list/purchased_gear = list() + var/list/equipped_gear = list() + + var/list/exp = list() + var/job_exempt = 0 + + var/action_buttons_screen_locs = list() + + ///What outfit typepaths we've favorited in the SelectEquipment menu + var/list/favorite_outfits = list() + + /// A preview of the current character + var/atom/movable/screen/map_view/character_preview_view/character_preview_view + + /// A list of instantiated middleware + var/list/datum/preference_middleware/middleware = list() + + /// A list of keys that have been updated since the last save. + var/list/recently_updated_keys = list() + + /// List of slot index -> character names + var/list/character_profiles_cached + + /// If the last save was a success or not. True for success, false for fail. + var/fail_state = TRUE + +/datum/preferences/Destroy(force, ...) + QDEL_NULL(character_preview_view) + QDEL_LIST(middleware) + QDEL_NULL(character_data) + QDEL_NULL(player_data) + return ..() + +/datum/preferences/New(client/parent) + src.parent = parent + + for (var/middleware_type in subtypesof(/datum/preference_middleware)) + middleware += new middleware_type(src) + + if(istype(parent)) + if(!IS_GUEST_KEY(parent.key)) + unlock_content = !!parent.IsByondMember() + if(unlock_content) + max_save_slots = 8 + else + CRASH("attempted to create a preferences datum without a client!") + + // give them default keybinds and update their movement keys + set_default_key_bindings(save = FALSE) // no point in saving these since everyone gets them. They'll be saved if needed. + randomise = get_default_randomization() + + var/loaded_preferences_successfully = load_preferences() + if(loaded_preferences_successfully) + if("6030fe461e610e2be3a2c3e75c06067e" in purchased_gear) //MD5 hash of, "extra character slot" + max_save_slots += 1 + if(load_character()) // This returns true if there is a database and character in the active slot. + // Get the profile data + fetch_character_profiles() + create_character_preview_view() + return + // Begin no database / new player logic. This ONLY fires if there is an SQL error or no database / the player and character is new. + + if(!loaded_preferences_successfully) // create a new character object + character_data = new(src, default_slot) + // Get the profile data + fetch_character_profiles() + var/new_species_path = GLOB.species_list[get_fallback_species_id() || "human"] + character_data.write_preference(src, GLOB.preference_entries[/datum/preference/choiced/species], new_species_path) + // We couldn't load character data so just randomize the character appearance + randomize_appearance_prefs() + if(parent) + apply_all_client_preferences() // apply now since normally this is done in load_preferences(). Defaults were set in preferences_player + + // The character name is fresh, update the character list. + update_current_character_profile() + create_character_preview_view() + + // If this was a NEW CKEY ENTRY, and not a guest key (handled in save_preferences()), save it. + // Guest keys are ignored by mark_undatumized_dirty + if(!loaded_preferences_successfully) + // This will essentially force a write, while also using the queueing system. + // For new ckeys, it is almost guaranteed we already hit the queue, since write_preference (used for when a datumized entry is null) + // Will also queue the CKEY. But this will also ensure that undatumized prefs get written. + mark_undatumized_dirty_player() + mark_undatumized_dirty_character() + +/datum/preferences/ui_interact(mob/user, datum/tgui/ui) + // IMPORTANT: If someone opens the prefs menu before jobs load, then the jobs menu will be empty for everyone. + // Do NOT call ui_assets until the jobs are loaded. + if(!length(SSjob.occupations)) + return + + // If you leave and come back, re-register the character preview. This also runs the first time it's opened + if (!isnull(character_preview_view) && istype(user.client) && !(character_preview_view in user.client.screen)) + character_preview_view.register_to_client(user.client) + + // Just force an update for funsies + character_preview_view.update_body() + + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PreferencesMenu") + ui.set_autoupdate(FALSE) + ui.open() + +/datum/preferences/ui_state(mob/user) + return GLOB.always_state + +// 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 + +/datum/preferences/ui_data(mob/user) + var/list/data = list() + + data["character_profiles"] = character_profiles_cached + + data["character_preferences"] = compile_character_preferences(user) + + data["active_slot"] = default_slot + data["max_slot"] = max_save_slots + data["save_in_progress"] = !isnull(SSpreferences.datums[parent.ckey]) + data["is_guest"] = !!IS_GUEST_KEY(parent.key) + data["is_db"] = !!SSdbcore.IsConnected() + data["save_sucess"] = !!fail_state + + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + data += preference_middleware.get_ui_data(user) + + return data + +/datum/preferences/ui_static_data(mob/user) + var/list/data = list() + + data["character_preview_view"] = character_preview_view.assigned_map + data["overflow_role"] = SSjob.GetJob(SSjob.overflow_role).title + data["window"] = current_window + + data["content_unlocked"] = unlock_content + + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + data += preference_middleware.get_ui_static_data(user) + + return data + +/datum/preferences/ui_assets(mob/user) + var/list/assets = list( + get_asset_datum(/datum/asset/spritesheet/preferences), + get_asset_datum(/datum/asset/spritesheet/preferences_large), + get_asset_datum(/datum/asset/spritesheet/preferences_huge), + get_asset_datum(/datum/asset/spritesheet/preferences_loadout), + get_asset_datum(/datum/asset/json/preferences), + ) + + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + assets += preference_middleware.get_ui_assets() + + return assets + +/datum/preferences/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if (.) + return + + switch (action) + if ("change_slot") + var/new_slot = params["slot"] + if(new_slot == default_slot) // No need to change to the current character. + return + // Save previous character (immediately, delaying this could mean data is lost) + save_character() + + // SAFETY: `load_character` performs sanitization the slot number + if (!load_character(new_slot)) + // there is no character in the slot. Make a new one. Save it. + update_current_character_profile() + randomize_appearance_prefs() + // Queue an undatumized save, just in case (it's likely already queued, but we should write undatumized data as well) + mark_undatumized_dirty_character() + + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + preference_middleware.on_new_character(usr) + + character_preview_view.update_body() + + return TRUE + if ("rotate") + var/direction = !!params["direction"] + if(isatom(character_preview_view.body)) + character_preview_view.body.dir = turn(character_preview_view.body.dir, (direction ? 1 : -1) * 90) + + return TRUE + if ("set_preference") + var/requested_preference_key = params["preference"] + var/value = params["value"] + + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + if (preference_middleware.pre_set_preference(usr, requested_preference_key, value)) + return TRUE + + 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/real_name)) + update_current_character_profile() + + 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 = tgui_color_picker( + usr, + "Select new color", + "Preference Color", + default_value || COLOR_WHITE, + ) + + if (!new_color) + return FALSE + + if (!update_preference(requested_preference, new_color)) + return FALSE + + return TRUE + if("open_game_preferences") + current_window = PREFERENCE_TAB_GAME_PREFERENCES + update_static_data(usr) + ui_interact(usr) + return TRUE + if("open_character_preferences") + current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES + update_static_data(usr) + ui_interact(usr) + return TRUE + + 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) + + return FALSE + +/datum/preferences/ui_close(mob/user) + // Save immediately. This should also handle if the player disconnects before their mob/ckey/client is null. + save_character() + save_preferences() + character_preview_view.unregister_from_client(user.client) + +/datum/preferences/proc/compile_character_preferences(mob/user) + var/list/preferences = list() + + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (!preference.is_accessible(src)) + continue + + LAZYINITLIST(preferences[preference.category]) + + var/value = read_preference(preference.type) + var/data = preference.compile_ui_data(user, value) + + preferences[preference.category][preference.db_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 immediately +/datum/preferences/proc/apply_all_client_preferences() + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (preference.preference_type != PREFERENCE_PLAYER) + continue + preference.apply_to_client(parent, read_player_preference(preference.type)) + +/// Updates cached character list with new real_name +/datum/preferences/proc/update_current_character_profile() + if(!islist(character_profiles_cached)) + return + character_profiles_cached[default_slot] = read_character_preference(/datum/preference/name/real_name) + +/// Immediately refetch the character list +/datum/preferences/proc/fetch_character_profiles() + character_data.get_all_character_names(src) + +/// Applies the given preferences to a human mob. +/datum/preferences/proc/apply_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE) + character.dna.features = list() + + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (preference.preference_type != PREFERENCE_CHARACTER) + continue + + preference.apply_to_human(character, read_character_preference(preference.type)) + + character.dna.real_name = character.real_name + + if(icon_updates) + character.icon_render_keys = list() // turns out if you don't set this to null update_body_parts does nothing, since it assumes the operation was cached + character.update_body() + character.update_hair() + character.update_body_parts(TRUE) // Must pass true here or limbs won't catch changes like body_model + character.dna.update_body_size() diff --git a/code/modules/client/preferences/serialization/preferences_character.dm b/code/modules/client/preferences/serialization/preferences_character.dm new file mode 100644 index 0000000000000..30b300600554e --- /dev/null +++ b/code/modules/client/preferences/serialization/preferences_character.dm @@ -0,0 +1,183 @@ +/// A cache for character preferences data +/datum/preferences_holder/preferences_character + /// INT: Slot number. Used for internal tracking. The slot number also correspnds to the number of slots in the characters list + var/slot_number = 0 + /// List of column names to be queried + var/static/list/column_names + +/// Block varedits to column_names +/datum/preferences_holder/preferences_character/vv_edit_var(var_name, var_value) + var/static/list/banned_edits = list(NAMEOF_STATIC(src, column_names)) + return !(var_name in banned_edits) && ..() + +/// Initialize the data cache with default values +/datum/preferences_holder/preferences_character/New(datum/preferences/prefs, slot) + slot_number = slot + if(!length(column_names)) + column_names = get_column_names() + ..(prefs) + +/datum/preferences_holder/preferences_character/proc/load_from_database(datum/preferences/prefs) + if(IS_GUEST_KEY(prefs.parent.key) || !query_data(prefs)) // Query direct, otherwise create informed defaults + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + if (preference.preference_type != pref_type) + continue + preference_data[preference.db_key] = preference.deserialize(preference.create_informed_default_value(prefs), prefs) + return FALSE + return TRUE + +/datum/preferences_holder/preferences_character/proc/query_data(datum/preferences/prefs) + if(!SSdbcore.IsConnected()) + return FALSE + var/list/values + var/datum/DBQuery/Q = SSdbcore.NewQuery( + "SELECT [db_column_list(column_names)] FROM [format_table_name("characters")] WHERE ckey=:ckey AND slot=:slot", + list("ckey" = prefs.parent.ckey, "slot" = slot_number) + ) + if(!Q.warn_execute()) + qdel(Q) + return FALSE + if(Q.NextRow()) + values = Q.item + if(!length(values)) // There is no character + qdel(Q) + return FALSE + else + qdel(Q) + return FALSE + qdel(Q) + if(length(values) != length(column_names)) + CRASH("Error querying character data: the returned value length is not equal to the number of columns requested.") + for(var/index in 1 to length(values)) + var/db_key = column_names[index] + var/datum/preference/preference = GLOB.preference_entries_by_key[db_key] + if(!istype(preference)) + CRASH("Could not find preference with db_key [db_key] when querying database.") + var/value = values[index] + preference_data[db_key] = isnull(value) ? null : preference.deserialize(value, prefs) + return TRUE + +/datum/preferences_holder/preferences_character/proc/write_to_database(datum/preferences/prefs) + . = write_data(prefs) + dirty_prefs.Cut() // clear all dirty preferences + +/datum/preferences_holder/preferences_character/proc/write_data(datum/preferences/prefs) + if(!SSdbcore.IsConnected() || IS_GUEST_KEY(prefs.parent.key)) + return FALSE + var/list/column_names_short = list() + var/list/new_data = list() + for(var/db_key in dirty_prefs) + if(!(db_key in preference_data)) + CRASH("Invalid db_key found in dirty preferences list: [db_key].") + var/datum/preference/preference = GLOB.preference_entries_by_key[db_key] + if(!istype(preference)) + CRASH("Could not find preference with db_key [db_key] when writing to database.") + new_data[db_key] = preference.serialize(preference_data[db_key]) + var/column_name = clean_column_name(preference) + if(length(column_name)) + column_names_short += column_name + if(!length(column_names_short)) // nothing to update + return TRUE + new_data["ckey"] = prefs.parent.ckey + new_data["slot"] = slot_number + var/datum/DBQuery/Q = SSdbcore.NewQuery( + "INSERT INTO [format_table_name("characters")] (ckey, slot, [db_column_list(column_names_short)]) VALUES (:ckey, :slot, [db_column_list(column_names_short, TRUE)]) ON DUPLICATE KEY UPDATE [db_column_values(column_names_short)]", new_data + ) + var/success = Q.warn_execute() + if(!success) + to_chat(prefs.parent, "Failed to save your character. Please inform the server operator or a maintainer of this error.") + qdel(Q) + prefs.fail_state = success + return success + +/datum/preferences_holder/preferences_character/proc/get_column_names() + var/list/result = list() + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + if (preference.preference_type != PREFERENCE_CHARACTER) + continue + // IMPORTANT: use of initial evades varedits. Filter to only alphanumeric and underscores + var/column_name = clean_column_name(preference) + if(length(column_name)) + result += column_name + if(!length(result)) + CRASH("Something is very wrong, /datum/prefence_character/proc/get_column_names() returned a zero length list.") + return result + +/datum/preferences_holder/preferences_character/proc/clean_column_name(datum/preference/preference) + var/column_name = reject_bad_text(initial(preference.db_key), max_length = 64, ascii_only = TRUE, alphanumeric_only = TRUE, underscore_allowed = TRUE) + if(!length(column_name) || findtext(column_name, " ") || column_name != preference.db_key) + CRASH("Invalid or possibly modified column name: '[column_name]' for db_key '[preference.db_key]'! Something bad is going on.") + return column_name + +/// Minimized copy of english_list because I don't want someone breaking this very important function later on +/proc/db_column_list(list/input, colon = FALSE) + var/total = length(input) + switch(total) + if (0) + return "" + if (1) + return "[colon ? ":" : ""][input[1]]" + if (2) + return "[colon ? ":" : ""][input[1]], [colon ? ":" : ""][input[2]]" + else + var/output = "" + var/index = 1 + while (index < total) + output += "[colon ? ":" : ""][input[index]], " + index++ + + return "[output][colon ? ":" : ""][input[index]]" + +/proc/db_column_values(list/input) + var/total = length(input) + switch(total) + if (0) + return "" + if (1) + return "[input[1]]=VALUES([input[1]])" + if (2) + return "[input[1]]=VALUES([input[1]]), [input[2]]=VALUES([input[2]])" + else + var/output = "" + var/index = 1 + while (index < total) + output += "[input[index]]=VALUES([input[index]])," + index++ + + return "[output][input[index]]=VALUES([input[index]])" + + +/datum/preferences_holder/preferences_character/proc/get_all_character_names(datum/preferences/prefs) + if(!SSdbcore.IsConnected() || IS_GUEST_KEY(prefs.parent.key)) + var/list/data = list() + for(var/index in 1 to TRUE_MAX_SAVE_SLOTS) + data += null + // Only the current slot is valid + data[prefs.default_slot] = read_preference(prefs, GLOB.preference_entries[/datum/preference/name/real_name]) + prefs.character_profiles_cached = data + return + var/datum/DBQuery/Q = SSdbcore.NewQuery( + "SELECT slot,real_name FROM [format_table_name("characters")] WHERE ckey=:ckey", + list("ckey" = prefs.parent.ckey) + ) + if(!Q.warn_execute()) + qdel(Q) + CRASH("An SQL error occurred while retrieving character profile data.") + var/list/data = list() + for(var/index in 1 to TRUE_MAX_SAVE_SLOTS) + data += null + while(Q.NextRow()) + var/list/values = Q.item + if(length(values) != 2) + CRASH("Error querying character profile data: the returned value length is greater than the number of columns requested.") + if(!isnum(values[1])) + CRASH("Error querying character profile data: slot number was not a number") + if(!istext(values[2])) + CRASH("Error querying character profile data: character name was not a string") + if(values[1] > TRUE_MAX_SAVE_SLOTS) + CRASH("Slot number in database is greater than the maximum allowed slots! Please purge this character entry or increase the slot number.") + data[values[1]] = values[2] // data[1] = "John Smith" + qdel(Q) + prefs.character_profiles_cached = data diff --git a/code/modules/client/preferences/serialization/preferences_database.dm b/code/modules/client/preferences/serialization/preferences_database.dm new file mode 100644 index 0000000000000..64ac01ab1a030 --- /dev/null +++ b/code/modules/client/preferences/serialization/preferences_database.dm @@ -0,0 +1,353 @@ +/datum/preferences/var/dirty_undatumized_preferences_player = FALSE +/datum/preferences/var/dirty_undatumized_preferences_character = FALSE + +/// Marks undatumized preferences as dirty, so it will be serialized on the next preference write. +/// Queues a preference write. +/// Use this for player preferences only. +/datum/preferences/proc/mark_undatumized_dirty_player() + if(IS_GUEST_KEY(parent.key)) // NO saving guests to the DB! + return FALSE + dirty_undatumized_preferences_player = TRUE + SSpreferences.queue_write(src) + +/// Marks undatumized preferences as dirty, so it will be serialized on the next preference write. +/// Queues a preference write. +/// Use this for character preferences only. +/datum/preferences/proc/mark_undatumized_dirty_character() + if(IS_GUEST_KEY(parent.key)) // NO saving guests to the DB! + return FALSE + dirty_undatumized_preferences_character = TRUE + SSpreferences.queue_write(src) + +/// If any character preference is dirty. +/datum/preferences/proc/ready_to_save_character() + return dirty_undatumized_preferences_character || length(character_data.dirty_prefs) + +/// If any player preference is dirty. +/datum/preferences/proc/ready_to_save_player() + return dirty_undatumized_preferences_player || length(player_data.dirty_prefs) + +// Defines for list sanity +#define READPREF_STR(target, tag) if(prefmap[tag]) target = prefmap[tag] +#define READPREF_INT(target, tag) if(prefmap[tag]) target = text2num(prefmap[tag]) + +// Did you know byond has try/catch? We use it here so malformed JSON doesnt break the entire loading system +#define READPREF_JSONDEC(target, tag) \ + try {\ + if(prefmap[tag]) {\ + target = json_decode(prefmap[tag]);\ + };\ + } catch {\ + pass();\ + } // we dont need error handling where were going + +/datum/preferences/proc/load_preferences() + // Get the datumized stuff first + player_data = new(src) + if(!player_data.load_from_database(src)) // checks db connection + return FALSE + + var/datum/DBQuery/read_player_data = SSdbcore.NewQuery( + "SELECT CAST(preference_tag AS CHAR) AS ptag, preference_value FROM [format_table_name("preferences")] WHERE ckey=:ckey", + list("ckey" = parent.ckey) + ) + + // K:pref tag | V:pref value + // DO NOT RENAME THIS. SERIOUSLY. DO NOT RENAME THIS LIST. IT'S USED IN THE READPREF DEFINES. + var/list/prefmap = list() + + if(!read_player_data.Execute()) + qdel(read_player_data) + return FALSE + else + while(read_player_data.NextRow()) + prefmap[read_player_data.item[1]] = read_player_data.item[2] + qdel(read_player_data) + + READPREF_INT(default_slot, PREFERENCE_TAG_DEFAULT_SLOT) + READPREF_STR(lastchangelog, PREFERENCE_TAG_LAST_CL) + + READPREF_STR(pai_name, PREFERENCE_TAG_PAI_NAME) + READPREF_STR(pai_description, PREFERENCE_TAG_PAI_DESCRIPTION) + READPREF_STR(pai_comment, PREFERENCE_TAG_PAI_COMMENT) + + READPREF_JSONDEC(ignoring, PREFERENCE_TAG_IGNORING) + READPREF_JSONDEC(purchased_gear, PREFERENCE_TAG_PURCHASED_GEAR) + READPREF_JSONDEC(role_preferences_global, PREFERENCE_TAG_ROLE_PREFERENCES_GLOBAL) + + // Custom hotkeys + READPREF_JSONDEC(key_bindings, PREFERENCE_TAG_KEYBINDS) + + //Sanitize + lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog)) + default_slot = sanitize_integer(default_slot, 1, TRUE_MAX_SAVE_SLOTS, initial(default_slot)) + ignoring = SANITIZE_LIST(ignoring) + purchased_gear = SANITIZE_LIST(purchased_gear) + role_preferences_global = SANITIZE_LIST(role_preferences_global) + + pai_name = sanitize_text(pai_name, initial(pai_name)) + pai_description = sanitize_text(pai_description, initial(pai_description)) + pai_comment = sanitize_text(pai_comment, initial(pai_comment)) + + key_bindings = sanitize_islist(key_bindings, deep_copy_list(GLOB.keybindings_by_name_to_key)) + key_bindings_by_key = get_key_bindings_by_key(key_bindings) + + // Remove any invalid role preference entries + for(var/preference in role_preferences_global) + var/path = text2path(preference) + var/datum/role_preference/entry = GLOB.role_preference_entries[path] + if(istype(entry)) + continue + role_preferences_global -= preference + + if (!length(key_bindings)) + set_default_key_bindings(save = TRUE) + else + var/any_changed = FALSE + for(var/key_name in GLOB.keybindings_by_name) + var/datum/keybinding/keybind = GLOB.keybindings_by_name[key_name] + if(key_name in key_bindings) // The bind exists in our keybind data. Good! Skip it. + continue + // Assign the default keybindings to the key, since there are none set. + set_keybind(key_name, keybind.keys.Copy()) + any_changed = TRUE + if(any_changed) + mark_undatumized_dirty_player() // Write the new keybinds to the database. + apply_all_client_preferences() + return TRUE + +#undef READPREF_STR +#undef READPREF_INT +#undef READPREF_JSONDEC + +#define PREP_WRITEPREF_STR(value, tag) write_queries += SSdbcore.NewQuery("INSERT INTO [format_table_name("preferences")] (ckey, preference_tag, preference_value) VALUES (:ckey, :ptag, :pvalue) ON DUPLICATE KEY UPDATE preference_value=:pvalue2", list("ckey" = parent.ckey, "ptag" = tag, "pvalue" = value, "pvalue2" = value)) +#define PREP_WRITEPREF_JSONENC(value, tag) PREP_WRITEPREF_STR(json_encode(value), tag) + +/datum/preferences/proc/save_preferences() + if(!SSdbcore.IsConnected()) + return FALSE + if(!istype(parent)) + return FALSE + if(IS_GUEST_KEY(parent.key)) // NO saving guests to the DB! + return FALSE + if(!player_data?.write_to_database(src)) + return FALSE + if(!dirty_undatumized_preferences_player) // Nothing to write. Call it a success. + return TRUE + dirty_undatumized_preferences_player = FALSE // we edit this immediately, since the DB query sleeps, the var could be modified during the sleep. + var/list/datum/DBQuery/write_queries = list() // do not rename this you muppet + + PREP_WRITEPREF_STR(default_slot, PREFERENCE_TAG_DEFAULT_SLOT) + PREP_WRITEPREF_STR(lastchangelog, PREFERENCE_TAG_LAST_CL) + + PREP_WRITEPREF_STR(pai_name, PREFERENCE_TAG_PAI_NAME) + PREP_WRITEPREF_STR(pai_description, PREFERENCE_TAG_PAI_DESCRIPTION) + PREP_WRITEPREF_STR(pai_comment, PREFERENCE_TAG_PAI_COMMENT) + + PREP_WRITEPREF_JSONENC(ignoring, PREFERENCE_TAG_IGNORING) + PREP_WRITEPREF_JSONENC(key_bindings, PREFERENCE_TAG_KEYBINDS) + PREP_WRITEPREF_JSONENC(purchased_gear, PREFERENCE_TAG_PURCHASED_GEAR) + PREP_WRITEPREF_JSONENC(role_preferences_global, PREFERENCE_TAG_ROLE_PREFERENCES_GLOBAL) + + // QuerySelect can execute many queries at once. That name is dumb but w/e + SSdbcore.QuerySelect(write_queries, TRUE, TRUE) + return TRUE + +#undef PREP_WRITEPREF_STR +#undef PREP_WRITEPREF_JSONENC + +#define JSONREAD_PREF(target, tag) \ + try {\ + var/idx = column_names?.Find(tag);\ + if(idx > 0) {\ + target = json_decode(values[idx]);\ + } else {\ + log_runtime("Missing preference tag '[tag]' in columns: [english_list(column_names)]");\ + };\ + } catch {\ + target = null;\ + pass();\ + } // we dont need error handling where were going + +/datum/preferences/proc/load_character(slot) + if(!slot) + slot = default_slot + slot = sanitize_integer(slot, 1, max_save_slots, initial(default_slot)) + if(slot != default_slot) + default_slot = slot + mark_undatumized_dirty_player() + + character_data = new(src, slot) + if(!character_data.load_from_database(src)) // checks db connection + return FALSE + + // Do NOT statically cache this or I will kill you. You are asking an evil vareditor to break the DB in a BAD way + // also DO NOT rename this + var/list/column_names = list( + "slot", // this is a literal column name + CHARACTER_PREFERENCE_RANDOMISE, + CHARACTER_PREFERENCE_JOB_PREFERENCES, + CHARACTER_PREFERENCE_ALL_QUIRKS, + CHARACTER_PREFERENCE_EQUIPPED_GEAR, + CHARACTER_PREFERENCE_ROLE_PREFERENCES, + ) + + var/datum/DBQuery/Q = SSdbcore.NewQuery( + "SELECT [db_column_list(column_names)] FROM [format_table_name("characters")] WHERE ckey=:ckey AND slot=:slot", + list("ckey" = parent.ckey, "slot" = slot) + ) + + // DON'T RENAME THIS. + var/list/values + if(!Q.warn_execute()) + qdel(Q) + return FALSE + if(Q.NextRow()) + values = Q.item + if(!length(values)) // There is no character + qdel(Q) + return FALSE + else + qdel(Q) + return FALSE + qdel(Q) + if(length(values) != length(column_names)) + CRASH("Error querying character data: the returned value length is not equal to the number of columns requested.") + + // Decode + JSONREAD_PREF(randomise, CHARACTER_PREFERENCE_RANDOMISE) + JSONREAD_PREF(job_preferences, CHARACTER_PREFERENCE_JOB_PREFERENCES) + JSONREAD_PREF(all_quirks, CHARACTER_PREFERENCE_ALL_QUIRKS) + JSONREAD_PREF(equipped_gear, CHARACTER_PREFERENCE_EQUIPPED_GEAR) + JSONREAD_PREF(role_preferences, CHARACTER_PREFERENCE_ROLE_PREFERENCES) + + //Sanitize + randomise = SANITIZE_LIST(randomise) + job_preferences = SANITIZE_LIST(job_preferences) + all_quirks = SANITIZE_LIST(all_quirks) + equipped_gear = SANITIZE_LIST(equipped_gear) + role_preferences = SANITIZE_LIST(role_preferences) + + // Validate job prefs + for(var/j in job_preferences) + if(job_preferences[j] != JP_LOW && job_preferences[j] != JP_MEDIUM && job_preferences[j] != JP_HIGH) + job_preferences -= j + mark_undatumized_dirty_character() + + // Validate role prefs + for(var/preference in role_preferences) + var/path = text2path(preference) + var/datum/role_preference/entry = GLOB.role_preference_entries[path] + if(istype(entry) && entry.per_character) + continue + role_preferences -= preference + mark_undatumized_dirty_character() + + // Validate equipped gear + for(var/gear_id in equipped_gear) + var/datum/gear/gear = GLOB.gear_datums[gear_id] + if(!length(GLOB.gear_datums)) // error safety, don't wanna clear everyone out + continue + if(!istype(gear)) + equipped_gear -= gear_id + mark_undatumized_dirty_character() + continue + // Somehow have a gear equipped that you don't own... + if(islist(purchased_gear) && !(gear_id in purchased_gear)) + equipped_gear -= gear_id + mark_undatumized_dirty_character() + + return TRUE + +#undef JSONREAD_PREF + +#define WRITEPREF_STR(value, tag) new_data[tag] = value;column_names += tag +#define WRITEPREF_JSONENC(value, tag) WRITEPREF_STR(json_encode(value), tag) + +/datum/preferences/proc/save_character() + if(!SSdbcore.IsConnected()) + return FALSE + if(!istype(parent)) + return FALSE + if(IS_GUEST_KEY(parent.key)) // NO saving guests to the DB! + return FALSE + if(!character_data?.write_to_database(src)) + return FALSE + if(!dirty_undatumized_preferences_character) // Nothing to write. Call it a success. + return TRUE + dirty_undatumized_preferences_character = FALSE // we edit this immediately, since the DB query sleeps, the var could be modified during the sleep. + + // DO NOT RENAME THESE LISTS! THANKS!! <3 + var/list/column_names = list() + var/list/new_data = list() + + WRITEPREF_JSONENC(randomise, CHARACTER_PREFERENCE_RANDOMISE) + WRITEPREF_JSONENC(job_preferences, CHARACTER_PREFERENCE_JOB_PREFERENCES) + WRITEPREF_JSONENC(all_quirks, CHARACTER_PREFERENCE_ALL_QUIRKS) + WRITEPREF_JSONENC(equipped_gear, CHARACTER_PREFERENCE_EQUIPPED_GEAR) + WRITEPREF_JSONENC(role_preferences, CHARACTER_PREFERENCE_ROLE_PREFERENCES) + + new_data["ckey"] = parent.ckey + new_data["slot"] = character_data.slot_number + var/datum/DBQuery/Q = SSdbcore.NewQuery( + "INSERT INTO [format_table_name("characters")] (ckey, slot, [db_column_list(column_names)]) VALUES (:ckey, :slot, [db_column_list(column_names, TRUE)]) ON DUPLICATE KEY UPDATE [db_column_values(column_names)]", new_data + ) + var/success = Q.warn_execute() + if(!success) + to_chat(parent, "Failed to save your character. Please inform the server operator or a maintainer of this error.") + qdel(Q) + fail_state = success + return success + +#undef WRITEPREF_STR +#undef WRITEPREF_JSONENC + +/datum/preferences_holder + /// A map of db_key -> value. Data type varies. + var/list/preference_data + /// A list of preference db_keys that require writing + var/list/dirty_prefs + /// Preference type to parse + var/pref_type + +/datum/preferences_holder/New(datum/preferences/prefs) + preference_data = list() + dirty_prefs = list() + // Read everything into cache + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + if (preference.preference_type != pref_type || preference.informed) + continue + + // we can't use informed values here. The name will get populated manually + preference_data[preference.db_key] = preference.deserialize(preference.create_default_value(), prefs) + +/datum/preferences_holder/proc/read_preference(datum/preferences/preferences, datum/preference/preference) + SHOULD_NOT_SLEEP(TRUE) + var/value = read_raw(preferences, preference) + if (isnull(value)) + value = preference.create_informed_default_value(preferences) + if (write_preference(preferences, preference, value)) + return value + else + CRASH("Couldn't write the default value for [preference.type] (received [value])") + return value + +/datum/preferences_holder/proc/read_raw(datum/preferences/preferences, datum/preference/preference) + // Data is already deserialized by the time it's in the cache. Don't deserialize it again. + var/value = preference_data[preference.db_key] + if (isnull(value)) + return null + else + return value + +/datum/preferences_holder/proc/write_preference(datum/preferences/preferences, datum/preference/preference, value) + var/new_value = preference.deserialize(value, preferences) + if (!preference.is_valid(new_value)) + return FALSE + preference_data[preference.db_key] = new_value + if(IS_GUEST_KEY(preferences.parent.key)) // NO saving guests to the DB! + return TRUE + dirty_prefs |= preference.db_key + SSpreferences.queue_write(preferences) + return TRUE diff --git a/code/modules/client/preferences/serialization/preferences_player.dm b/code/modules/client/preferences/serialization/preferences_player.dm new file mode 100644 index 0000000000000..7b0ae4ea5c981 --- /dev/null +++ b/code/modules/client/preferences/serialization/preferences_player.dm @@ -0,0 +1,71 @@ +/// A cache for player preferences data +/datum/preferences_holder/preferences_player + pref_type = PREFERENCE_PLAYER + +/datum/preferences_holder/preferences_player/proc/load_from_database(datum/preferences/prefs) + if(IS_GUEST_KEY(prefs.parent.key) || !query_data(prefs)) // Query direct, otherwise create informed defaults + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + if (preference.preference_type != pref_type) + continue + preference_data[preference.db_key] = preference.deserialize(preference.create_informed_default_value(prefs), prefs)\ + // Give the developers +1 sanity points + if(Debugger?.enabled) + prefs.update_preference(/datum/preference/toggle/sound_ambience, FALSE) + prefs.update_preference(/datum/preference/toggle/sound_ship_ambience, FALSE) + prefs.update_preference(/datum/preference/toggle/sound_lobby, FALSE) + // TODO tgui-prefs initialize undatumized prefs here? + // no idiot put that in save_preferences() if this returns false. + return FALSE + return TRUE + +/datum/preferences_holder/preferences_player/proc/query_data(datum/preferences/prefs) + if(!SSdbcore.IsConnected()) + return FALSE + var/datum/DBQuery/Q = SSdbcore.NewQuery( + "SELECT CAST(preference_tag AS CHAR) AS ptag, preference_value FROM [format_table_name("preferences")] WHERE ckey=:ckey", + list("ckey" = prefs.parent.ckey) + ) + if(!Q.warn_execute()) + qdel(Q) + return FALSE + var/any_data = FALSE + while(Q.NextRow()) + var/db_key = Q.item[1] + var/value = Q.item[2] + var/datum/preference/preference = GLOB.preference_entries_by_key[db_key] + if(!preference) + // TODO tgui-prefs clean out database and re-enable this + //CRASH("Unknown preference tag in database: [db_key] for ckey [prefs.parent.ckey]") + continue + preference_data[db_key] = isnull(value) ? null : preference.deserialize(value, prefs) + any_data = TRUE + qdel(Q) + return any_data + +/datum/preferences_holder/preferences_player/proc/write_to_database(datum/preferences/prefs) + . = write_data(prefs) + dirty_prefs.Cut() // clear all dirty preferences + +/datum/preferences_holder/preferences_player/proc/write_data(datum/preferences/prefs) + if(!SSdbcore.IsConnected() || IS_GUEST_KEY(prefs.parent.key)) + return FALSE + var/list/sql_inserts = list() + for(var/db_key in dirty_prefs) + if(!(db_key in preference_data)) + CRASH("Invalid db_key found in dirty preferences list: [db_key].") + var/datum/preference/preference = GLOB.preference_entries_by_key[db_key] + if(!istype(preference)) + CRASH("Could not find preference with db_key [db_key] when writing to database.") + sql_inserts += list(list( + "ckey" = prefs.parent.ckey, + "preference_tag" = db_key, + "preference_value" = preference.serialize(preference_data[db_key]) + )) + if(!length(sql_inserts)) // nothing to update + return TRUE + var/success = SSdbcore.MassInsert(format_table_name("preferences"), sql_inserts, duplicate_key = TRUE, warn = TRUE) + if(!success) + to_chat(prefs.parent, "Failed to save your player preferences. Please inform the server operator or a maintainer of this error.") + prefs.fail_state = success + return success diff --git a/code/modules/client/preferences/submodules/preference_assets.dm b/code/modules/client/preferences/submodules/preference_assets.dm new file mode 100644 index 0000000000000..2ddec52277cd1 --- /dev/null +++ b/code/modules/client/preferences/submodules/preference_assets.dm @@ -0,0 +1,100 @@ +/// Assets generated from `/datum/preference` icons +/datum/asset/spritesheet/preferences + name = PREFERENCE_SHEET_NORMAL + early = TRUE + cross_round_cachable = TRUE + +/datum/asset/spritesheet/preferences/create_spritesheets() + create_preferences_spritesheet(src, name) + +/proc/create_preferences_spritesheet(datum/asset/spritesheet/sheet, sheet_key) + 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 + + if(preference.preference_spritesheet != sheet_key) + 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]).") + + sheet.Insert(preference.get_spritesheet_key(preference_value), icon, icon_state) + +/// This "large" spritesheet helps reduce mount lag from large PNG files. +/datum/asset/spritesheet/preferences_large + name = PREFERENCE_SHEET_LARGE + early = TRUE + cross_round_cachable = TRUE + +/datum/asset/spritesheet/preferences_large/create_spritesheets() + create_preferences_spritesheet(src, name) + +/// This "huge" spritesheet helps reduce mount lag from huge PNG files. +/datum/asset/spritesheet/preferences_huge + name = PREFERENCE_SHEET_HUGE + early = TRUE + cross_round_cachable = TRUE + + +/datum/asset/spritesheet/preferences_huge/create_spritesheets() + // if someone ever hits this limit, you need to delete the game + // just delete it, it's too big. It needs to end (the year is probably 2053 or something) + create_preferences_spritesheet(src, name) + +/// Returns the key that will be used in the spritesheet for a given value. +/datum/preference/proc/get_spritesheet_key(value) + return "[db_key]___[sanitize_css_class_name(value)]" + +/datum/asset/spritesheet/preferences_loadout + name = "preferences_loadout" + early = TRUE + +/datum/asset/spritesheet/preferences_loadout/create_spritesheets() + for(var/gear_id in GLOB.gear_datums) + var/datum/gear/G = GLOB.gear_datums[gear_id] + var/icon/regular_icon = get_display_icon_for(G.path) + if(!regular_icon) + continue + Insert("loadout_gear___[gear_id]", regular_icon) + var/icon/skirt_icon = get_display_icon_for(G.skirt_path) + if(!skirt_icon) + continue + Insert("loadout_gear___[gear_id]_skirt", skirt_icon) + +/// 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.db_key] = data + + return preference_data diff --git a/code/modules/client/preferences/submodules/preference_character_preview.dm b/code/modules/client/preferences/submodules/preference_character_preview.dm new file mode 100644 index 0000000000000..775b506628eba --- /dev/null +++ b/code/modules/client/preferences/submodules/preference_character_preview.dm @@ -0,0 +1,137 @@ +/datum/preferences/proc/create_character_preview_view() + if(istype(character_preview_view)) + return + character_preview_view = new(null, src) + if(parent) + character_preview_view.register_to_client(parent) + // HACK: Without this the character starts out really tiny because of https://www.byond.com/forum/post/2873835 + // You can fix it by updating the atom's appearance (in any way), so let's just do something unexpensive and change its name! + character_preview_view.rename_byond_bug_moment() + +/datum/preferences/proc/render_new_preview_appearance(mob/living/carbon/human/dummy/mannequin) + var/datum/job/preview_job = get_highest_priority_job() + + // 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_sync(read_character_preference(/datum/preference/choiced/ai_core_display)), dir = SOUTH) + if (istype(preview_job, /datum/job/cyborg)) + return image('icons/mob/robots.dmi', icon_state = "robot", dir = SOUTH) + + // Set up the dummy for its photoshoot + apply_prefs_to(mannequin, TRUE) + // Normalize size, since it doesn't scale properly in the preview. + mannequin.dna.features["body_size"] = "Normal" + mannequin.dna.update_body_size() + + if(preview_job) + mannequin.job = preview_job.title + preview_job.equip(mannequin, TRUE, preference_source = parent) + preview_job.after_spawn(mannequin, mannequin, preference_source = parent, on_dummy = TRUE) + else + apply_loadout_to_mob(mannequin, mannequin, preference_source = parent, on_dummy = TRUE) + + COMPILE_OVERLAYS(mannequin) + return mannequin.appearance + +// This is necessary because you can open the set preferences menu before +// the atoms SS is done loading. +INITIALIZE_IMMEDIATE(/atom/movable/screen/map_view/character_preview_view) + +/// A preview of a character for use in the preferences menu +/atom/movable/screen/map_view/character_preview_view + name = "character_preview" + del_on_map_removal = FALSE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + + /// 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() + + /// List of clients with this registered to it. + var/list/viewing_clients = list() + +/atom/movable/screen/map_view/character_preview_view/Initialize(mapload, datum/preferences/preferences) + . = ..() + + assigned_map = "character_preview_[REF(src)]" + set_position(1, 1) + + src.preferences = preferences + +/atom/movable/screen/map_view/character_preview_view/Destroy() + QDEL_NULL(body) + + for (var/plane_master in plane_masters) + qdel(plane_master) + + for(var/client/C as anything in viewing_clients) + C?.clear_map(assigned_map) + + preferences?.character_preview_view = null + + viewing_clients = null + plane_masters = null + preferences = null + + return ..() + +/// I know this looks stupid but it fixes a really important bug. https://www.byond.com/forum/post/2873835 +/// Also the mouse opacity blocks this from being visible ever +/atom/movable/screen/map_view/character_preview_view/proc/rename_byond_bug_moment() + #if MIN_COMPILER_VERSION > 514 + #warn Remove 514 BYOND bug workaround in preferences character preview + #endif + spawn(0) // Using spawn() to avoid addtimer() since it doesn't fire during init + while(TRUE) + name = name == "character_preview" ? "character_preview_1" : "character_preview" + stoplag(1 SECONDS) + +/// Updates the currently displayed body +/atom/movable/screen/map_view/character_preview_view/proc/update_body() + if (isnull(body)) + create_body() + else + body.wipe_state() + body.appearance = preferences.render_new_preview_appearance(body) + // Force map view to update as well + name = name == "character_preview" ? "character_preview_1" : "character_preview" + +/atom/movable/screen/map_view/character_preview_view/proc/create_body() + vis_contents.Cut() + QDEL_NULL(body) + + body = new + + // Without this, it doesn't show up in the menu + body.appearance_flags &= ~KEEP_TOGETHER + body.wipe_state() // cleanup the body immediately since it spawns with overlays, AI and cyborgs will retain them. + vis_contents += body + +/// Registers the relevant map objects to a client +/atom/movable/screen/map_view/character_preview_view/proc/register_to_client(client/client) + if(client in viewing_clients) + return + if(!length(plane_masters)) + for(var/plane in subtypesof(/atom/movable/screen/plane_master) - /atom/movable/screen/plane_master/blackness) + var/atom/movable/screen/plane_master/instance = new plane() + instance.assigned_map = assigned_map + if(instance.blend_mode_override) + instance.blend_mode = instance.blend_mode_override + instance.del_on_map_removal = FALSE + instance.screen_loc = "[assigned_map]:CENTER" + plane_masters += instance + viewing_clients += client + client.register_map_obj(src) + for(var/plane_master in plane_masters) + client.register_map_obj(plane_master) + +/// Unregisters the relevant map objects to a client +/atom/movable/screen/map_view/character_preview_view/proc/unregister_from_client(client/client) + if(!istype(client) || !(client in viewing_clients)) + return + client.clear_map(assigned_map) + viewing_clients -= client diff --git a/code/modules/client/preferences/submodules/preference_jobs.dm b/code/modules/client/preferences/submodules/preference_jobs.dm new file mode 100644 index 0000000000000..f384538b987ac --- /dev/null +++ b/code/modules/client/preferences/submodules/preference_jobs.dm @@ -0,0 +1,35 @@ +/datum/preferences/proc/set_job_preference_level(datum/job/job, level) + if (!job) + return FALSE + + if (level == JP_HIGH) + var/datum/job/overflow_role = SSjob.overflow_role + var/overflow_role_title = initial(overflow_role.title) + + for(var/other_job in job_preferences) + if(job_preferences[other_job] == JP_HIGH) + // Overflow role needs to go to NEVER, not medium! + if(other_job == overflow_role_title) + job_preferences[other_job] = null + else + job_preferences[other_job] = JP_MEDIUM + + if(level == null) + job_preferences -= job.title + else + job_preferences[job.title] = level + mark_undatumized_dirty_character() + + return TRUE + +/// 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) + preview_job = SSjob.GetJob(job) + highest_pref = job_preferences[job] + + return preview_job diff --git a/code/modules/client/preferences/submodules/preference_keybindings.dm b/code/modules/client/preferences/submodules/preference_keybindings.dm new file mode 100644 index 0000000000000..3381ad5536e5b --- /dev/null +++ b/code/modules/client/preferences/submodules/preference_keybindings.dm @@ -0,0 +1,31 @@ +/// 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() + + for (var/action in key_bindings) + for (var/key in key_bindings[action]) + LAZYADD(output[key], action) + + return output + +/datum/preferences/proc/set_key_bindings(list/_key_bindings) + if(!istype(_key_bindings)) + return + key_bindings = _key_bindings + key_bindings_by_key = get_key_bindings_by_key(key_bindings) + mark_undatumized_dirty_player() + +/datum/preferences/proc/set_default_key_bindings(save = FALSE) + key_bindings = deep_copy_list(GLOB.keybindings_by_name_to_key) + key_bindings_by_key = get_key_bindings_by_key(key_bindings) + if(save) + mark_undatumized_dirty_player() + +/datum/preferences/proc/set_keybind(keybind_name, hotkeys) + if (!(keybind_name in GLOB.keybindings_by_name)) + return FALSE + if(!islist(hotkeys)) + return + key_bindings[keybind_name] = hotkeys + key_bindings_by_key = get_key_bindings_by_key(key_bindings) + mark_undatumized_dirty_player() diff --git a/code/modules/client/preferences/submodules/preference_loadouts.dm b/code/modules/client/preferences/submodules/preference_loadouts.dm new file mode 100644 index 0000000000000..ee424a5f1c2d0 --- /dev/null +++ b/code/modules/client/preferences/submodules/preference_loadouts.dm @@ -0,0 +1,28 @@ +/// Handles adding and removing donator items from clients +/datum/preferences/proc/handle_donator_items() + var/datum/loadout_category/DLC = GLOB.loadout_categories["Donator"] // stands for donator loadout category but the other def for DLC works too xD + if(!CONFIG_GET(flag/donator_items)) // donator items are only accesibile by servers with a patreon + return + if(IS_PATRON(parent.ckey) || is_admin(parent.ckey)) + var/any_changed = FALSE + for(var/gear_id in DLC.gear) + var/datum/gear/AG = DLC.gear[gear_id] + if(AG.id in purchased_gear) + continue + any_changed = TRUE + purchased_gear += AG.id + AG.purchase(parent) + if(any_changed) + mark_undatumized_dirty_player() + else if(length(purchased_gear) || length(equipped_gear)) + var/any_changed = FALSE + for(var/gear_id in DLC.gear) + var/datum/gear/RG = DLC.gear[gear_id] + if(!(RG.id in purchased_gear) && !(RG.id in equipped_gear)) + continue + any_changed = TRUE + equipped_gear -= RG.id + purchased_gear -= RG.id + if(any_changed) + mark_undatumized_dirty_player() + mark_undatumized_dirty_character() diff --git a/code/modules/client/preferences/submodules/preference_randomization.dm b/code/modules/client/preferences/submodules/preference_randomization.dm new file mode 100644 index 0000000000000..bfbc5887ba5e1 --- /dev/null +++ b/code/modules/client/preferences/submodules/preference_randomization.dm @@ -0,0 +1,39 @@ +/// Fully randomizes everything in the character. +/datum/preferences/proc/randomize_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) + switch (read_character_preference(/datum/preference/choiced/random_body)) + if (RANDOM_ANTAG_ONLY) + if (!antag_override) + return + + if (RANDOM_DISABLED) + return + + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (should_randomize(preference, antag_override)) + write_preference(preference, preference.create_random_value(src)) + +/// Returns the default `randomise` variable ouptut +/datum/preferences/proc/get_default_randomization() + var/list/default_randomization = list() + + for (var/preference_key in GLOB.preference_entries_by_key) + var/datum/preference/preference = GLOB.preference_entries_by_key[preference_key] + if (preference.is_randomizable() && preference.randomize_by_default) + default_randomization[preference_key] = RANDOM_ENABLED + + return default_randomization + +/// Sanitizes the preferences, applies the randomization prefs, and then applies the preference to the human mob. +/// This is generally used over apply_prefs_to, since it respects the player's body/antag randomization +/datum/preferences/proc/safe_transfer_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE, is_antag = FALSE) + apply_character_randomization_prefs(is_antag) + apply_prefs_to(character, icon_updates) diff --git a/code/modules/client/preferences2/character_save.dm b/code/modules/client/preferences2/character_save.dm deleted file mode 100644 index 394dfe3c47215..0000000000000 --- a/code/modules/client/preferences2/character_save.dm +++ /dev/null @@ -1,513 +0,0 @@ -/** - * # Character Save Datum - * - * Datum to hold a character save which is put into a list on [/datum/preferences]. - * All of these are loaded on login. - */ -/datum/character_save - // Meta Vars // - /// Slot number. Used for internal tracking. The slot number also correspnds to the number of slots in the characters list - var/slot_number = 0 - /// Is this slot locked, likely due to not having enough character slots available - var/slot_locked = FALSE - /// Was this loaded from the DB? (This is used to decide on INSERT or UPDATE queries) - var/from_db = FALSE - - // Character Related Vars // - /// Species datum - - var/real_name - var/be_random_name = FALSE - var/be_random_body = FALSE - var/gender = MALE - var/age = 30 - var/underwear = "Nude" - var/underwear_color = "000" - var/undershirt = "Nude" - var/socks = "Nude" // how lewd - var/helmet_style = HELMET_DEFAULT - var/backbag = DBACKPACK - var/jumpsuit_style = PREF_SUIT - var/hair_style = "Bald" - var/hair_color = "000" - var/gradient_color = "000" - var/gradient_style = "None" - var/facial_hair_style = "Shaved" - var/facial_hair_color = "000" - var/skin_tone = "caucasian1" - var/eye_color = "000" - var/datum/species/pref_species - var/list/features = list( - "body_size" = "Normal", - "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", - "ipc_screen" = "Blue", - "ipc_antenna" = "None", - "ipc_chassis" = "Morpheus Cyberkinetics(Greyscale)", - "insect_type" = "Common Fly", - "psyphoza_cap" = "Portobello", - "apid_antenna" = "Curled", - "apid_stripes" = "Thick", - "apid_headstripes" = "Thick", - "body_model" = MALE - ) - var/list/custom_names = list() - var/preferred_ai_core_display = "Blue" - var/preferred_security_department = SEC_DEPT_RANDOM - var/list/all_quirks = list() - var/list/job_preferences = list() - var/list/equipped_gear = list() - var/joblessrole = BERANDOMJOB //defaults to 1 for fewer assistants - var/uplink_spawn_loc = UPLINK_PDA - var/list/role_preferences_character = list() - - -/datum/character_save/New() - real_name = get_default_name() - for(var/custom_name_id in GLOB.preferences_custom_names) - custom_names[custom_name_id] = get_default_name(custom_name_id) - -#define SAFE_READ_QUERY(idx, target) if(Q.item[idx]) target = Q.item[idx] - -/datum/character_save/proc/handle_query(datum/DBQuery/Q) - from_db = TRUE - - // please keep these in numerical order I beg - //Species - var/species_id - SAFE_READ_QUERY(2, species_id) - - if(!species_id) // There was no species ID saved, make it random - species_id = pick(GLOB.roundstart_races) - - var/newtype = GLOB.species_list[species_id] - - if(!newtype) // The species ID doesn't exist in the species list, make it random - newtype = GLOB.species_list[pick(GLOB.roundstart_races)] - - pref_species = new newtype - - if(!pref_species) // there are no roundstart species enabled. Time to die - pref_species = new /datum/species/human - if(!length(GLOB.roundstart_races)) - CRASH("There are no roundstart races enabled! You must enable at least one for the character setup to function.") - - //Character - SAFE_READ_QUERY(3, real_name) - SAFE_READ_QUERY(4, be_random_name) - SAFE_READ_QUERY(5, be_random_body) - SAFE_READ_QUERY(6, gender) - SAFE_READ_QUERY(7, age) - SAFE_READ_QUERY(8, hair_color) - SAFE_READ_QUERY(9, gradient_color) - SAFE_READ_QUERY(10, facial_hair_color) - SAFE_READ_QUERY(11, eye_color) - SAFE_READ_QUERY(12, skin_tone) - SAFE_READ_QUERY(13, hair_style) - SAFE_READ_QUERY(14, gradient_style) - SAFE_READ_QUERY(15, facial_hair_style) - SAFE_READ_QUERY(16, underwear) - SAFE_READ_QUERY(17, underwear_color) - SAFE_READ_QUERY(18, undershirt) - SAFE_READ_QUERY(19, socks) - SAFE_READ_QUERY(20, backbag) - SAFE_READ_QUERY(21, jumpsuit_style) - SAFE_READ_QUERY(22, uplink_spawn_loc) - - var/tmp_features - SAFE_READ_QUERY(23, tmp_features) - if(tmp_features) - features = json_decode(tmp_features) - - if(!CONFIG_GET(flag/join_with_mutant_humans) && !species_id != "felinid") // felinids arent mutant humans anymore i guess - features["tail_human"] = "none" - features["ears"] = "none" - - //Custom names - var/tmp_names - SAFE_READ_QUERY(24, tmp_names) - custom_names = json_decode(tmp_names) - - SAFE_READ_QUERY(25, helmet_style) - - SAFE_READ_QUERY(26, preferred_ai_core_display) - SAFE_READ_QUERY(27, preferred_security_department) - - //Jobs - SAFE_READ_QUERY(28, joblessrole) - //Load prefs - var/job_tmp - SAFE_READ_QUERY(29, job_tmp) - job_preferences = json_decode(job_tmp) - - //Quirks - var/quirks_tmp - SAFE_READ_QUERY(30, quirks_tmp) - all_quirks = json_decode(quirks_tmp) - - // Gear - var/loadout_tmp - SAFE_READ_QUERY(31, loadout_tmp) - equipped_gear = json_decode(loadout_tmp) - - // Role prefs - var/role_preferences_character_tmp - SAFE_READ_QUERY(32, role_preferences_character_tmp) - role_preferences_character = json_decode(role_preferences_character_tmp) - - //Sanitize. Please dont put query reads below this point. Please. - - real_name = reject_bad_name(real_name, pref_species.allow_numbers_in_name) - gender = sanitize_gender(gender) - real_name ||= pref_species.random_name(gender, TRUE) - - 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)] - - // Keep it updated - if(!helmet_style || !(helmet_style in list(HELMET_DEFAULT, HELMET_MK2, HELMET_PROTECTIVE))) - helmet_style = HELMET_DEFAULT - - be_random_name = sanitize_integer(be_random_name, 0, 1, initial(be_random_name)) - be_random_body = sanitize_integer(be_random_body, 0, 1, initial(be_random_body)) - - hair_style = sanitize_inlist(hair_style, GLOB.hair_styles_list) - facial_hair_style = sanitize_inlist(facial_hair_style, GLOB.facial_hair_styles_list) - underwear = sanitize_inlist(underwear, GLOB.underwear_list) - undershirt = sanitize_inlist(undershirt, GLOB.undershirt_list) - features["body_model"] = sanitize_gender(features["body_model"], FALSE, FALSE, gender == FEMALE ? FEMALE : MALE) - 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) - gradient_style = sanitize_inlist(gradient_style, GLOB.hair_gradients_list, "None") - gradient_color = sanitize_hexcolor(gradient_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) - backbag = sanitize_inlist(backbag, GLOB.backbaglist, initial(backbag)) - jumpsuit_style = sanitize_inlist(jumpsuit_style, GLOB.jumpsuitlist, initial(jumpsuit_style)) - uplink_spawn_loc = sanitize_inlist(uplink_spawn_loc, GLOB.uplink_spawn_loc_list_save, initial(uplink_spawn_loc)) - features["body_size"] = sanitize_inlist(features["body_size"], GLOB.body_sizes, "Normal") - 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_roundstart_list, "Plain") - features["moth_antennae"] = sanitize_inlist(features["moth_antennae"], GLOB.moth_antennae_roundstart_list, "Plain") - features["moth_markings"] = sanitize_inlist(features["moth_markings"], GLOB.moth_markings_roundstart_list, "None") - features["ipc_screen"] = sanitize_inlist(features["ipc_screen"], GLOB.ipc_screens_list) - features["ipc_antenna"] = sanitize_inlist(features["ipc_antenna"], GLOB.ipc_antennas_list) - features["ipc_chassis"] = sanitize_inlist(features["ipc_chassis"], GLOB.ipc_chassis_list) - features["insect_type"] = sanitize_inlist(features["insect_type"], GLOB.insect_type_list) - features["psyphoza_cap"] = sanitize_inlist(features["psyphoza_cap"], GLOB.psyphoza_cap_list) - features["apid_antenna"] = sanitize_inlist(features["apid_antenna"], GLOB.apid_antenna_list) - features["apid_stripes"] = sanitize_inlist(features["apid_stripes"], GLOB.apid_stripes_list) - features["apid_headstripes"] = sanitize_inlist(features["apid_headstripes"], GLOB.apid_headstripes_list) - - //Validate species forced mutant parts - for(var/forced_part in pref_species.forced_features) - //Get the forced type - var/forced_type = pref_species.forced_features[forced_part] - //Apply the forced bodypart. - features[forced_part] = forced_type - - joblessrole = sanitize_integer(joblessrole, 1, 3, initial(joblessrole)) - //Validate job prefs - for(var/j in job_preferences) - if(job_preferences[j] != JP_LOW && job_preferences[j] != JP_MEDIUM && job_preferences[j] != JP_HIGH) - job_preferences -= j - - all_quirks = SANITIZE_LIST(all_quirks) - role_preferences_character = SANITIZE_LIST(role_preferences_character) - // Remove any invalid entries - for(var/preference in role_preferences_character) - var/path = text2path(preference) - var/datum/role_preference/entry = GLOB.role_preference_entries[path] - if(istype(entry) && entry.per_character) - continue - role_preferences_character -= preference - - return TRUE - -#undef SAFE_READ_QUERY - -/datum/character_save/proc/randomise(gender_override) - if(gender_override) - gender = gender_override - else - gender = pick(MALE,FEMALE) - underwear = random_underwear(gender) - underwear_color = random_short_color() - undershirt = random_undershirt(gender) - socks = random_socks() - skin_tone = random_skin_tone() - hair_style = random_hair_style(gender) - facial_hair_style = random_facial_hair_style(gender) - hair_color = random_short_color() - facial_hair_color = hair_color - eye_color = random_eye_color() - if(!pref_species) - var/datum/species/spath = GLOB.species_list[pick(GLOB.roundstart_races)] - pref_species = new spath - features = random_features() - if(gender) - features["body_model"] = pick(MALE,FEMALE) - age = rand(AGE_MIN,AGE_MAX) - -/datum/character_save/proc/update_preview_icon(client/parent) - if(!parent) - CRASH("Someone called update_preview_icon() without passing a client.") - // Determine what job is marked as 'High' priority, and dress them up as such. - var/datum/job/previewJob - var/highest_pref = 0 - for(var/job in job_preferences) - if(job_preferences[job] > highest_pref) - previewJob = 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 - - // Set up the dummy for its photoshoot - var/mob/living/carbon/human/dummy/mannequin = generate_or_wait_for_human_dummy(DUMMY_HUMAN_SLOT_PREFERENCES) - copy_to(mannequin) - - if(previewJob) - mannequin.job = previewJob.title - previewJob.equip(mannequin, TRUE, preference_source = parent) - - COMPILE_OVERLAYS(mannequin) - parent.show_character_previews(new /mutable_appearance(mannequin)) - unset_busy_human_dummy(DUMMY_HUMAN_SLOT_PREFERENCES) - -/datum/character_save/proc/save(client/C, async = TRUE) - if(!SSdbcore.IsConnected()) - return - - if(IS_GUEST_KEY(C.ckey)) - return - - // Get ready for a disgusting query - var/datum/DBQuery/insert_query = SSdbcore.NewQuery({" - REPLACE INTO [format_table_name("characters")] ( - slot, - ckey, - species, - real_name, - name_is_always_random, - body_is_always_random, - gender, - age, - hair_color, - gradient_color, - facial_hair_color, - eye_color, - skin_tone, - hair_style_name, - gradient_style, - facial_style_name, - underwear, - underwear_color, - undershirt, - socks, - backbag, - jumpsuit_style, - uplink_loc, - features, - custom_names, - helmet_style, - preferred_ai_core_display, - preferred_security_department, - joblessrole, - job_preferences, - all_quirks, - equipped_gear, - role_preferences - ) VALUES ( - :slot, - :ckey, - :species, - :real_name, - :name_is_always_random, - :body_is_always_random, - :gender, - :age, - :hair_color, - :gradient_color, - :facial_hair_color, - :eye_color, - :skin_tone, - :hair_style_name, - :gradient_style, - :facial_style_name, - :underwear, - :underwear_color, - :undershirt, - :socks, - :backbag, - :jumpsuit_style, - :uplink_loc, - :features, - :custom_names, - :helmet_style, - :preferred_ai_core_display, - :preferred_security_department, - :joblessrole, - :job_preferences, - :all_quirks, - :equipped_gear, - :role_preferences - ) - "}, list( - // Now for the above but in a fucking monsterous list - "slot" = slot_number, - "ckey" = C.ckey, - "species" = pref_species.id, - "real_name" = real_name, - "name_is_always_random" = be_random_name, - "body_is_always_random" = be_random_body, - "gender" = gender, - "age" = age, - "hair_color" = hair_color, - "gradient_color" = gradient_color, - "facial_hair_color" = facial_hair_color, - "eye_color" = eye_color, - "skin_tone" = skin_tone, - "hair_style_name" = hair_style, - "gradient_style" = gradient_style, - "facial_style_name" = facial_hair_style, - "underwear" = underwear, - "underwear_color" = underwear_color, - "undershirt" = undershirt, - "socks" = socks, - "backbag" = backbag, - "jumpsuit_style" = jumpsuit_style, - "uplink_loc" = uplink_spawn_loc, - "features" = json_encode(features), - "custom_names" = json_encode(custom_names), - "helmet_style" = helmet_style, - "preferred_ai_core_display" = preferred_ai_core_display, - "preferred_security_department" = preferred_security_department, - "joblessrole" = joblessrole, - "job_preferences" = json_encode(job_preferences), - "all_quirks" = json_encode(all_quirks), - "equipped_gear" = json_encode(equipped_gear), - "role_preferences" = json_encode(role_preferences_character) - )) - - if(!insert_query.warn_execute()) - to_chat(usr, "Failed to save your character. Please inform the server operator.") - qdel(insert_query) - return - - qdel(insert_query) - - // We defo exist in the DB now - from_db = TRUE - -/datum/character_save/proc/copy_to(mob/living/carbon/human/character, icon_updates = 1, roundstart_checks = TRUE) - if(be_random_name) - real_name = pref_species.random_name(gender) - - if(be_random_body) - randomise(gender) - - if(roundstart_checks) - 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)]" - - character.real_name = real_name - character.name = character.real_name - - character.gender = gender - character.age = age - - character.eye_color = eye_color - var/obj/item/organ/eyes/organ_eyes = character.getorgan(/obj/item/organ/eyes) - if(organ_eyes) - if(!initial(organ_eyes.eye_color)) - organ_eyes.eye_color = eye_color - organ_eyes.old_eye_color = eye_color - - character.hair_color = hair_color - character.gradient_color = gradient_color - character.gradient_style = gradient_style - character.facial_hair_color = facial_hair_color - character.skin_tone = skin_tone - character.underwear = underwear - character.underwear_color = underwear_color - character.undershirt = undershirt - character.socks = socks - - character.backbag = backbag - character.jumpsuit_style = jumpsuit_style - - var/datum/species/chosen_species - chosen_species = pref_species.type - if(!roundstart_checks || (pref_species.id in GLOB.roundstart_races) || pref_species.check_no_hard_check()) - chosen_species = pref_species.type - else - chosen_species = /datum/species/human - pref_species = new /datum/species/human - save(usr.client, async = FALSE) // This entire proc is called a lot at roundstart, and we dont want to lag that - - - character.dna.features = features.Copy() - character.set_species(chosen_species, icon_update = FALSE, pref_load = TRUE) - - //Because of how set_species replaces all bodyparts with new ones, hair needs to be set AFTER species. - character.dna.real_name = character.real_name - character.hair_color = hair_color - character.facial_hair_color = facial_hair_color - - character.hair_style = hair_style - character.facial_hair_style = facial_hair_style - - if("tail_lizard" in pref_species.default_features) - character.dna.species.mutant_bodyparts |= "tail_lizard" - - if(icon_updates) - character.update_body() - character.update_hair() - character.update_body_parts(TRUE) diff --git a/code/modules/client/preferences2/preferences2.dm b/code/modules/client/preferences2/preferences2.dm deleted file mode 100644 index 8e363efd5a8d9..0000000000000 --- a/code/modules/client/preferences2/preferences2.dm +++ /dev/null @@ -1,275 +0,0 @@ -/* - - Preferences 2 - Now with 100% more database - - This system evicts savefiles and uses the database for all playerdata storage. - - All "user" preferences must be stored as tags in the `SS13_preferences` table. - Any boolean toggle preference (Show/Hide Deadchat for example) **MUST** be a bitflag toggle stored as a single toggles integer. - This is to cut down on the amount of useless columns when you can pack up to 24 binary toggles into a single integer. - NOTE: You cant go above 24. BYOND loses precision and you then break everything. - - All character customisation preferences must be saved in the `SS13_characters` table. - All properties for character customisation must be tacked onto a [/datum/character_save], not the main prefs datum - - Failure to comply with this will result in being screamed at. - - AA07 - -*/ - -// Defines for list sanity -#define READPREF_RAW(target, tag) if(prefmap[tag]) target = prefmap[tag] -#define READPREF_INT(target, tag) if(prefmap[tag]) target = text2num(prefmap[tag]) - -// Did you know byond has try/catch? We use it here so malformed JSON doesnt break the entire loading system -#define READPREF_JSONDEC(target, tag) \ - try {\ - if(prefmap[tag]) {\ - target = json_decode(prefmap[tag]);\ - };\ - } catch {\ - pass();\ - } // we dont need error handling where were going - -/datum/preferences/proc/load_from_database() - . = FALSE - if(!SSdbcore.IsConnected()) - // TODO - Loading of sane defaults - if (!length(key_bindings)) - key_bindings = deep_copy_list(GLOB.keybinding_list_by_key) - if(Debugger?.enabled) - toggles &= ~(PREFTOGGLE_SOUND_AMBIENCE | PREFTOGGLE_SOUND_SHIP_AMBIENCE | PREFTOGGLE_SOUND_LOBBY) - return - - var/datum/DBQuery/read_player_data = SSdbcore.NewQuery( - "SELECT CAST(preference_tag AS CHAR) AS ptag, preference_value FROM [format_table_name("preferences")] WHERE ckey=:ckey", - list("ckey" = parent.ckey) - ) - - // K:pref tag | V:pref value - var/list/prefmap = list() // dont rename this. trust me. - - if(!read_player_data.Execute()) - qdel(read_player_data) - return - else - while(read_player_data.NextRow()) - prefmap[read_player_data.item[1]] = read_player_data.item[2] - qdel(read_player_data) - - //general preferences - READPREF_INT(default_slot, PREFERENCE_TAG_DEFAULT_SLOT) - READPREF_INT(chat_toggles, PREFERENCE_TAG_CHAT_TOGGLES) - READPREF_INT(toggles, PREFERENCE_TAG_TOGGLES) - READPREF_INT(toggles2, PREFERENCE_TAG_TOGGLES2) - READPREF_INT(clientfps, PREFERENCE_TAG_CLIENTFPS) - READPREF_INT(parallax, PREFERENCE_TAG_PARALLAX) - READPREF_INT(pixel_size, PREFERENCE_TAG_PIXELSIZE) - READPREF_INT(tip_delay, PREFERENCE_TAG_TIP_DELAY) - - READPREF_RAW(asaycolor, PREFERENCE_TAG_ASAY_COLOUR) - READPREF_RAW(ooccolor, PREFERENCE_TAG_OOC_COLOUR) - READPREF_RAW(lastchangelog, PREFERENCE_TAG_LAST_CL) - READPREF_RAW(UI_style, PREFERENCE_TAG_UI_STYLE) - READPREF_RAW(outline_color, PREFERENCE_TAG_OUTLINE_COLOUR) - READPREF_RAW(see_balloon_alerts, PREFERENCE_TAG_BALLOON_ALERTS) - READPREF_RAW(scaling_method, PREFERENCE_TAG_SCALING_METHOD) - READPREF_RAW(ghost_form, PREFERENCE_TAG_GHOST_FORM) - READPREF_RAW(ghost_orbit, PREFERENCE_TAG_GHOST_ORBIT) - READPREF_RAW(ghost_accs, PREFERENCE_TAG_GHOST_ACCS) - READPREF_RAW(ghost_others, PREFERENCE_TAG_GHOST_OTHERS) - READPREF_RAW(pda_theme, PREFERENCE_TAG_PDA_THEME) - READPREF_RAW(pda_color, PREFERENCE_TAG_PDA_COLOUR) - READPREF_RAW(pai_name, PREFERENCE_TAG_PAI_NAME) - READPREF_RAW(pai_description, PREFERENCE_TAG_PAI_DESCRIPTION) - READPREF_RAW(pai_comment, PREFERENCE_TAG_PAI_COMMENT) - - READPREF_JSONDEC(ignoring, PREFERENCE_TAG_IGNORING) - READPREF_JSONDEC(key_bindings, PREFERENCE_TAG_KEYBINDS) - READPREF_JSONDEC(purchased_gear, PREFERENCE_TAG_PURCHASED_GEAR) - READPREF_JSONDEC(role_preferences, PREFERENCE_TAG_ROLE_PREFERENCES) - - //Sanitize - asaycolor = sanitize_ooccolor(sanitize_hexcolor(asaycolor, 6, TRUE, initial(asaycolor))) - ooccolor = sanitize_ooccolor(sanitize_hexcolor(ooccolor, 6, TRUE, initial(ooccolor))) - lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog)) - UI_style = sanitize_inlist(UI_style, GLOB.available_ui_styles, GLOB.available_ui_styles[1]) - - default_slot = sanitize_integer(default_slot, TRUE, TRUE_MAX_SAVE_SLOTS, initial(default_slot)) - toggles = sanitize_integer(toggles, FALSE, INFINITY, initial(toggles)) // yes - toggles2 = sanitize_integer(toggles2, FALSE, INFINITY, initial(toggles2)) - clientfps = sanitize_integer(clientfps, FALSE, 1000, FALSE) - parallax = sanitize_integer(parallax, PARALLAX_INSANE, PARALLAX_DISABLE, null) - - 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) - role_preferences = SANITIZE_LIST(role_preferences) - // Remove any invalid entries - for(var/preference in role_preferences) - var/path = text2path(preference) - var/datum/role_preference/entry = GLOB.role_preference_entries[path] - if(istype(entry) && !entry.per_character) - continue - role_preferences -= preference - - pda_theme = sanitize_inlist(pda_theme, GLOB.ntos_device_themes_default_content, initial(pda_theme)) - pda_color = sanitize_hexcolor(pda_color, 6, TRUE, initial(pda_color)) - - pai_name = sanitize_text(pai_name, initial(pai_name)) - pai_description = sanitize_text(pai_description, initial(pai_description)) - pai_comment = sanitize_text(pai_comment, initial(pai_comment)) - - key_bindings = sanitize_islist(key_bindings, deep_copy_list(GLOB.keybinding_list_by_key)) - if (!length(key_bindings)) - key_bindings = deep_copy_list(GLOB.keybinding_list_by_key) - else - var/any_changed = FALSE - for(var/key_name in GLOB.keybindings_by_name) - var/datum/keybinding/keybind = GLOB.keybindings_by_name[key_name] - var/in_binds = FALSE - for(var/bind in key_bindings) - if(key_name in key_bindings[bind]) - in_binds = TRUE - break - if(in_binds) - continue - any_changed = TRUE - if(!islist(key_bindings[keybind.key])) - key_bindings[keybind.key] = list(key_name) - else - key_bindings[keybind.key] += key_name - if(any_changed) - save_keybinds() - - if(!purchased_gear) - purchased_gear = list() - - return TRUE - -#undef READPREF_RAW -#undef READPREF_INT -#undef READPREF_JSONDEC - -// OH BOY MORE MACRO ABUSE -#define PREP_WRITEPREF_RAW(value, tag) write_queries += SSdbcore.NewQuery("INSERT INTO [format_table_name("preferences")] (ckey, preference_tag, preference_value) VALUES (:ckey, :ptag, :pvalue) ON DUPLICATE KEY UPDATE preference_value=:pvalue2", list("ckey" = parent.ckey, "ptag" = tag, "pvalue" = value, "pvalue2" = value)) -#define PREP_WRITEPREF_JSONENC(value, tag) PREP_WRITEPREF_RAW(json_encode(value), tag) - -/datum/preferences/proc/save_keybinds() - var/list/datum/DBQuery/write_queries = list() - PREP_WRITEPREF_JSONENC(key_bindings, PREFERENCE_TAG_KEYBINDS) - SSdbcore.QuerySelect(write_queries, TRUE, TRUE) - -// Writes all prefs to the DB -/datum/preferences/proc/save_preferences() - if(!SSdbcore.IsConnected()) - return - - if(IS_GUEST_KEY(parent.ckey)) - return - - var/list/datum/DBQuery/write_queries = list() // do not rename this you muppet - - //general preferences - PREP_WRITEPREF_RAW(default_slot, PREFERENCE_TAG_DEFAULT_SLOT) - PREP_WRITEPREF_RAW(chat_toggles, PREFERENCE_TAG_CHAT_TOGGLES) - PREP_WRITEPREF_RAW(toggles, PREFERENCE_TAG_TOGGLES) - PREP_WRITEPREF_RAW(toggles2, PREFERENCE_TAG_TOGGLES2) - PREP_WRITEPREF_RAW(clientfps, PREFERENCE_TAG_CLIENTFPS) - PREP_WRITEPREF_RAW(parallax, PREFERENCE_TAG_PARALLAX) - PREP_WRITEPREF_RAW(pixel_size, PREFERENCE_TAG_PIXELSIZE) - PREP_WRITEPREF_RAW(tip_delay, PREFERENCE_TAG_TIP_DELAY) - PREP_WRITEPREF_RAW(pda_theme, PREFERENCE_TAG_PDA_THEME) - PREP_WRITEPREF_RAW(pda_color, PREFERENCE_TAG_PDA_COLOUR) - - PREP_WRITEPREF_RAW(asaycolor, PREFERENCE_TAG_ASAY_COLOUR) - PREP_WRITEPREF_RAW(ooccolor, PREFERENCE_TAG_OOC_COLOUR) - PREP_WRITEPREF_RAW(lastchangelog, PREFERENCE_TAG_LAST_CL) - PREP_WRITEPREF_RAW(UI_style, PREFERENCE_TAG_UI_STYLE) - PREP_WRITEPREF_RAW(outline_color, PREFERENCE_TAG_OUTLINE_COLOUR) - PREP_WRITEPREF_RAW(see_balloon_alerts, PREFERENCE_TAG_BALLOON_ALERTS) - PREP_WRITEPREF_RAW(scaling_method, PREFERENCE_TAG_SCALING_METHOD) - PREP_WRITEPREF_RAW(ghost_form, PREFERENCE_TAG_GHOST_FORM) - PREP_WRITEPREF_RAW(ghost_orbit, PREFERENCE_TAG_GHOST_ORBIT) - PREP_WRITEPREF_RAW(ghost_accs, PREFERENCE_TAG_GHOST_ACCS) - PREP_WRITEPREF_RAW(ghost_others, PREFERENCE_TAG_GHOST_OTHERS) - PREP_WRITEPREF_RAW(pai_name, PREFERENCE_TAG_PAI_NAME) - PREP_WRITEPREF_RAW(pai_description, PREFERENCE_TAG_PAI_DESCRIPTION) - PREP_WRITEPREF_RAW(pai_comment, PREFERENCE_TAG_PAI_COMMENT) - - PREP_WRITEPREF_JSONENC(ignoring, PREFERENCE_TAG_IGNORING) - PREP_WRITEPREF_JSONENC(key_bindings, PREFERENCE_TAG_KEYBINDS) - PREP_WRITEPREF_JSONENC(purchased_gear, PREFERENCE_TAG_PURCHASED_GEAR) - PREP_WRITEPREF_JSONENC(role_preferences, PREFERENCE_TAG_ROLE_PREFERENCES) - - // QuerySelect can execute many queries at once. That name is dumb but w/e - SSdbcore.QuerySelect(write_queries, TRUE, TRUE) - -#undef PREP_WRITEPREF_RAW -#undef PREP_WRITEPREF_JSONENC - - -// Get ready for a disgusting SQL query -/datum/preferences/proc/load_characters() - // Do NOT remove stuff from the start of this query. Only append to the end. - // If you delete an entry, god help you as you have to update all the indexes - var/datum/DBQuery/read_chars = SSdbcore.NewQuery({" - SELECT - slot, - species, - real_name, - name_is_always_random, - body_is_always_random, - gender, - age, - hair_color, - gradient_color, - facial_hair_color, - eye_color, - skin_tone, - hair_style_name, - gradient_style, - facial_style_name, - underwear, - underwear_color, - undershirt, - socks, - backbag, - jumpsuit_style, - uplink_loc, - features, - custom_names, - helmet_style, - preferred_ai_core_display, - preferred_security_department, - joblessrole, - job_preferences, - all_quirks, - equipped_gear, - role_preferences - FROM [format_table_name("characters")] WHERE - ckey=:ckey - "}, list("ckey" = parent.ckey)) - - if(!read_chars.warn_execute()) - qdel(read_chars) - return - - var/char_loaded = FALSE - while(read_chars.NextRow()) - var/idx = read_chars.item[1] - var/datum/character_save/CS = character_saves[idx] - CS.handle_query(read_chars) - char_loaded = TRUE - - qdel(read_chars) - check_usable_slots() - return char_loaded - - -/datum/preferences/proc/check_usable_slots() - for(var/datum/character_save/CS as anything in character_saves) - CS.slot_locked = (CS.slot_number > max_usable_slots) diff --git a/code/modules/client/preferences_toggles.dm b/code/modules/client/preferences_toggles.dm deleted file mode 100644 index 9658ed1be8a43..0000000000000 --- a/code/modules/client/preferences_toggles.dm +++ /dev/null @@ -1,448 +0,0 @@ -/client/verb/game_preferences() - set name = "Game Preferences" - set category = "Preferences" - set desc = "Open Game Preferences Window" - prefs.current_tab = 1 - prefs.ShowChoices(usr) - -/client/verb/character_preferences() - set name = "Character Preferences" - set category = "Preferences" - set desc = "Open Character Preferences Window" - prefs.current_tab = 0 - prefs.ShowChoices(usr) - -/client/verb/toggle_ghost_ears() - set name = "Show/Hide GhostEars" - set category = "Preferences" - set desc = "See All Speech" - prefs.chat_toggles ^= CHAT_GHOSTEARS - to_chat(usr, "As a ghost, you will now [(prefs.chat_toggles & CHAT_GHOSTEARS) ? "see all speech in the world" : "only see speech from nearby mobs"].") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Ears", "[prefs.chat_toggles & CHAT_GHOSTEARS ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_sight() - set name = "Show/Hide GhostSight" - set category = "Preferences" - set desc = "See All Emotes" - prefs.chat_toggles ^= CHAT_GHOSTSIGHT - to_chat(usr, "As a ghost, you will now [(prefs.chat_toggles & CHAT_GHOSTSIGHT) ? "see all emotes in the world" : "only see emotes from nearby mobs"].") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Sight", "[prefs.chat_toggles & CHAT_GHOSTSIGHT ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_whispers() - set name = "Show/Hide GhostWhispers" - set category = "Preferences" - set desc = "See All Whispers" - prefs.chat_toggles ^= CHAT_GHOSTWHISPER - to_chat(usr, "As a ghost, you will now [(prefs.chat_toggles & CHAT_GHOSTWHISPER) ? "see all whispers in the world" : "only see whispers from nearby mobs"].") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Whispers", "[prefs.chat_toggles & CHAT_GHOSTWHISPER ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_radio() - set name = "Show/Hide GhostRadio" - set category = "Preferences" - set desc = "See All Radio Chatter" - prefs.chat_toggles ^= CHAT_GHOSTRADIO - to_chat(usr, "As a ghost, you will now [(prefs.chat_toggles & CHAT_GHOSTRADIO) ? "see radio chatter" : "not see radio chatter"].") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Radio", "[prefs.chat_toggles & CHAT_GHOSTRADIO ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! //social experiment, increase the generation whenever you copypaste this shamelessly GENERATION 1 - -/client/verb/toggle_ghost_pda() - set name = "Show/Hide GhostPDA" - set category = "Preferences" - set desc = "See All PDA Messages" - prefs.chat_toggles ^= CHAT_GHOSTPDA - to_chat(usr, "As a ghost, you will now [(prefs.chat_toggles & CHAT_GHOSTPDA) ? "see all pda messages in the world" : "only see pda messages from nearby mobs"].") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost PDA", "[prefs.chat_toggles & CHAT_GHOSTPDA ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_laws() - set name = "Show/Hide GhostLaws" - set category = "Preferences" - set desc = "See All Law Changes" - prefs.chat_toggles ^= CHAT_GHOSTLAWS - to_chat(usr, "As a ghost, you will now [(prefs.chat_toggles & CHAT_GHOSTLAWS) ? "be notified of all law changes" : "no longer be notified of law changes"].") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Laws", "[prefs.chat_toggles & CHAT_GHOSTLAWS ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_follow() - set name = "Toggle NPC GhostFollow" - set category = "Preferences" - set desc = "Toggle the visibility of the follow button for NPC messages and emotes." - prefs.chat_toggles ^= CHAT_GHOSTFOLLOWMINDLESS - to_chat(usr, "As a ghost, you will [(prefs.chat_toggles & CHAT_GHOSTFOLLOWMINDLESS) ? "now" : "no longer"] see the follow button for NPC mobs.") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle NPC GhostFollow", "[prefs.chat_toggles & CHAT_GHOSTFOLLOWMINDLESS ? "Enabled" : "Disabled"]")) - -//please be aware that the following two verbs have inverted stat output, so that "Toggle Deathrattle|1" still means you activated it -/client/verb/toggle_deathrattle() - set name = "Toggle Deathrattle" - set category = "Preferences" - set desc = "Death" - prefs.toggles ^= PREFTOGGLE_DISABLE_DEATHRATTLE - prefs.save_preferences() - to_chat(usr, "You will [(prefs.toggles & PREFTOGGLE_DISABLE_DEATHRATTLE) ? "no longer" : "now"] get messages when a sentient mob dies.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Deathrattle", "[!(prefs.toggles & PREFTOGGLE_DISABLE_DEATHRATTLE) ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, maybe you should spend some time reading the comments. - -/client/verb/toggle_arrivalrattle() - set name = "Toggle Arrivalrattle" - set category = "Preferences" - set desc = "New Player Arrival" - prefs.toggles ^= PREFTOGGLE_DISABLE_ARRIVALRATTLE - to_chat(usr, "You will [(prefs.toggles & PREFTOGGLE_DISABLE_ARRIVALRATTLE) ? "no longer" : "now"] get messages when someone joins the station.") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Arrivalrattle", "[!(prefs.toggles & PREFTOGGLE_DISABLE_ARRIVALRATTLE) ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, maybe you should rethink where your life went so wrong. - -/client/verb/toggletitlemusic() - set name = "Hear/Silence Lobby Music" - set category = "Preferences" - set desc = "Hear Music In Lobby" - prefs.toggles ^= PREFTOGGLE_SOUND_LOBBY - prefs.save_preferences() - if(prefs.toggles & PREFTOGGLE_SOUND_LOBBY) - to_chat(usr, "You will now hear music in the game lobby.") - if(isnewplayer(usr)) - playtitlemusic() - else - to_chat(usr, "You will no longer hear music in the game lobby.") - usr.stop_sound_channel(CHANNEL_LOBBYMUSIC) - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Lobby Music", "[prefs.toggles & PREFTOGGLE_SOUND_LOBBY ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/togglemidis() - set name = "Hear/Silence Midis" - set category = "Preferences" - set desc = "Hear Admin Triggered Sounds (Midis)" - prefs.toggles ^= PREFTOGGLE_SOUND_MIDI - prefs.save_preferences() - if(prefs.toggles & PREFTOGGLE_SOUND_MIDI) - to_chat(usr, "You will now hear any sounds uploaded by admins.") - else - to_chat(usr, "You will no longer hear sounds uploaded by admins") - usr.stop_sound_channel(CHANNEL_ADMIN) - var/client/C = usr.client - C?.tgui_panel?.stop_music() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Hearing Midis", "[prefs.toggles & PREFTOGGLE_SOUND_MIDI ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_instruments() - set name = "Hear/Silence Instruments" - set category = "Preferences" - set desc = "Hear In-game Instruments" - prefs.toggles ^= PREFTOGGLE_SOUND_INSTRUMENTS - prefs.save_preferences() - if(prefs.toggles & PREFTOGGLE_SOUND_INSTRUMENTS) - to_chat(usr, "You will now hear people playing musical instruments.") - else - to_chat(usr, "You will no longer hear musical instruments.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Instruments", "[prefs.toggles & PREFTOGGLE_SOUND_INSTRUMENTS ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/Toggle_Soundscape() - set name = "Hear/Silence Ambience" - set category = "Preferences" - set desc = "Hear Ambient Sound Effects" - prefs.toggles ^= PREFTOGGLE_SOUND_AMBIENCE - prefs.save_preferences() - if(prefs.toggles & PREFTOGGLE_SOUND_AMBIENCE) - to_chat(usr, "You will now hear ambient sounds.") - else - to_chat(usr, "You will no longer hear ambient sounds.") - usr.stop_sound_channel(CHANNEL_AMBIENT_EFFECTS) - usr.stop_sound_channel(CHANNEL_AMBIENT_MUSIC) - usr.stop_sound_channel(CHANNEL_BUZZ) - usr.client.buzz_playing = FALSE - usr.client.update_ambience_pref() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ambience", "[usr.client.prefs.toggles & PREFTOGGLE_SOUND_AMBIENCE ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ship_ambience() - set name = "Hear/Silence Ship Ambience" - set category = "Preferences" - set desc = "Hear Ship Ambience Roar" - prefs.toggles ^= PREFTOGGLE_SOUND_SHIP_AMBIENCE - prefs.save_preferences() - if(prefs.toggles & PREFTOGGLE_SOUND_SHIP_AMBIENCE) - to_chat(usr, "You will now hear ship ambience.") - else - to_chat(usr, "You will no longer hear ship ambience.") - usr.stop_sound_channel(CHANNEL_BUZZ) - usr.client.buzz_playing = FALSE - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ship Ambience", "[usr.client.prefs.toggles & PREFTOGGLE_SOUND_SHIP_AMBIENCE ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, I bet you read this comment expecting to see the same thing :^) - -/client/verb/toggle_soundtrack() - set name = "Hear/Silence Soundtrack" - set category = "Preferences" - set desc = "Hear Soundtrack Songs" - prefs.toggles2 ^= PREFTOGGLE_2_SOUNDTRACK - prefs.save_preferences() - if(prefs.toggles2 & PREFTOGGLE_2_SOUNDTRACK) - to_chat(usr, "You will now hear soundtrack songs.") - usr.play_current_soundtrack() - else - to_chat(usr, "You will no longer hear soundtrack songs.") - usr.stop_sound_channel(CHANNEL_SOUNDTRACK) - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Soundtrack", "[usr.client.prefs.toggles2 & PREFTOGGLE_2_SOUNDTRACK ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, I bet you read this comment expecting to see the same thing :^) - - -/client/verb/toggle_announcement_sound() - set name = "Hear/Silence Announcements" - set category = "Preferences" - set desc = "Hear Announcement Sound" - prefs.toggles ^= PREFTOGGLE_SOUND_ANNOUNCEMENTS - to_chat(usr, "You will now [(prefs.toggles & PREFTOGGLE_SOUND_ANNOUNCEMENTS) ? "hear announcement sounds" : "no longer hear announcements"].") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Announcement Sound", "[prefs.toggles & PREFTOGGLE_SOUND_ANNOUNCEMENTS ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/stop_client_sounds() - set name = "Stop Sounds" - set category = "Preferences" - 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")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/listen_ooc() - set name = "Show/Hide OOC" - set category = "Preferences" - set desc = "Show OOC Chat" - prefs.chat_toggles ^= CHAT_OOC - prefs.save_preferences() - to_chat(usr, "You will [(prefs.chat_toggles & CHAT_OOC) ? "now" : "no longer"] see messages on the OOC channel.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Seeing OOC", "[prefs.chat_toggles & CHAT_OOC ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/listen_bank_card() - set name = "Show/Hide Income Updates" - set category = "Preferences" - set desc = "Show or hide updates to your income" - prefs.chat_toggles ^= CHAT_BANKCARD - prefs.save_preferences() - to_chat(usr, "You will [(prefs.chat_toggles & CHAT_BANKCARD) ? "now" : "no longer"] be notified when you get paid.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Income Notifications", "[(prefs.chat_toggles & CHAT_BANKCARD) ? "Enabled" : "Disabled"]")) - -GLOBAL_LIST_INIT(ghost_forms, sort_list(list("ghost","ghostking","ghostian2","skeleghost","ghost_red","ghost_black", \ - "ghost_blue","ghost_yellow","ghost_green","ghost_pink", \ - "ghost_cyan","ghost_dblue","ghost_dred","ghost_dgreen", \ - "ghost_dcyan","ghost_grey","ghost_dyellow","ghost_dpink", "ghost_purpleswirl","ghost_funkypurp","ghost_pinksherbert","ghost_blazeit",\ - "ghost_mellow","ghost_rainbow","ghost_camo","ghost_fire", "catghost"))) - -/client/proc/pick_form() - if(!is_content_unlocked()) - alert("This setting is for accounts with BYOND premium only.") - return - var/new_form = input(src, "Thanks for supporting BYOND - Choose your ghostly form:","Thanks for supporting BYOND",null) as null|anything in GLOB.ghost_forms - if(new_form) - prefs.ghost_form = new_form - prefs.save_preferences() - if(isobserver(mob)) - var/mob/dead/observer/O = mob - O.update_icon(new_form = new_form) - -GLOBAL_LIST_INIT(ghost_orbits, list(GHOST_ORBIT_CIRCLE,GHOST_ORBIT_TRIANGLE,GHOST_ORBIT_SQUARE,GHOST_ORBIT_HEXAGON,GHOST_ORBIT_PENTAGON)) - -/client/proc/pick_ghost_orbit() - if(!is_content_unlocked()) - alert("This setting is for accounts with BYOND premium only.") - return - var/new_orbit = input(src, "Thanks for supporting BYOND - Choose your ghostly orbit:","Thanks for supporting BYOND",null) as null|anything in GLOB.ghost_orbits - if(new_orbit) - prefs.ghost_orbit = new_orbit - prefs.save_preferences() - if(isobserver(mob)) - var/mob/dead/observer/O = mob - O.ghost_orbit = new_orbit - -/client/proc/pick_ghost_accs() - var/new_ghost_accs = alert("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?",,"full accessories", "only directional sprites", "default sprites") - if(new_ghost_accs) - switch(new_ghost_accs) - if("full accessories") - prefs.ghost_accs = GHOST_ACCS_FULL - if("only directional sprites") - prefs.ghost_accs = GHOST_ACCS_DIR - if("default sprites") - prefs.ghost_accs = GHOST_ACCS_NONE - prefs.save_preferences() - if(isobserver(mob)) - var/mob/dead/observer/O = mob - O.update_icon() - -/client/verb/pick_ghost_customization() - set name = "Ghost Customization" - set category = "Preferences" - set desc = "Customize your ghastly appearance." - if(is_content_unlocked()) - switch(alert("Which setting do you want to change?",,"Ghost Form","Ghost Orbit","Ghost Accessories")) - if("Ghost Form") - pick_form() - if("Ghost Orbit") - pick_ghost_orbit() - if("Ghost Accessories") - pick_ghost_accs() - else - pick_ghost_accs() - -/client/verb/pick_ghost_others() - set name = "Ghosts of Others" - set category = "Preferences" - set desc = "Change display settings for the ghosts of other players." - var/new_ghost_others = alert("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?",,"Their Setting", "Default Sprites", "White Ghost") - if(new_ghost_others) - switch(new_ghost_others) - if("Their Setting") - prefs.ghost_others = GHOST_OTHERS_THEIR_SETTING - if("Default Sprites") - prefs.ghost_others = GHOST_OTHERS_DEFAULT_SPRITE - if("White Ghost") - prefs.ghost_others = GHOST_OTHERS_SIMPLE - prefs.save_preferences() - if(isobserver(mob)) - var/mob/dead/observer/O = mob - O.update_sight() - -/client/verb/toggle_intent_style() - set name = "Toggle Intent Selection Style" - set category = "Preferences" - set desc = "Toggle between directly clicking the desired intent or clicking to rotate through." - prefs.toggles ^= PREFTOGGLE_INTENT_STYLE - to_chat(src, "[(prefs.toggles & PREFTOGGLE_INTENT_STYLE) ? "Clicking directly on intents selects them." : "Clicking on intents rotates selection clockwise."]") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Intent Selection", "[prefs.toggles & PREFTOGGLE_INTENT_STYLE ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_hud_pref() - set name = "Toggle Ghost HUD" - set category = "Preferences" - set desc = "Hide/Show Ghost HUD" - - prefs.toggles2 ^= PREFTOGGLE_2_GHOST_HUD - to_chat(src, "Ghost HUD will now be [(prefs.toggles2 & PREFTOGGLE_2_GHOST_HUD) ? "visible" : "hidden"].") - prefs.save_preferences() - if(isobserver(mob)) - mob.hud_used.show_hud() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost HUD", "[(prefs.toggles2 & PREFTOGGLE_2_GHOST_HUD) ? "Enabled" : "Disabled"]")) - -/client/verb/toggle_show_credits() - set name = "Toggle Credits" - set category = "Preferences" - set desc = "Hide/Show Credits" - - prefs.toggles2 ^= PREFTOGGLE_2_SHOW_CREDITS - to_chat(src, "Credits will now be [prefs.toggles2 & PREFTOGGLE_2_SHOW_CREDITS ? "visible" : "hidden"].") - prefs.save_preferences() - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Credits", "[prefs.toggles2 & PREFTOGGLE_2_SHOW_CREDITS ? "Enabled" : "Disabled"]")) - -/client/verb/toggle_inquisition() // warning: unexpected inquisition - set name = "Toggle Inquisitiveness" - set desc = "Sets whether your ghost examines everything on click by default" - set category = "Preferences" - - prefs.toggles2 ^= PREFTOGGLE_2_GHOST_INQUISITIVENESS - prefs.save_preferences() - if(prefs.toggles2 & PREFTOGGLE_2_GHOST_INQUISITIVENESS) - to_chat(src, "You will now examine everything you click on.") - else - to_chat(src, "You will no longer examine things you click on.") - SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Toggle Ghost Inquisitiveness", "[(prefs.toggles2 & PREFTOGGLE_2_GHOST_INQUISITIVENESS) ? "Enabled" : "Disabled"]")) - -//Admin Preferences -/client/proc/toggleadminhelpsound() - set name = "Hear/Silence Adminhelps" - set category = "Prefs - Admin" - set desc = "Toggle hearing a notification when admin PMs are received" - if(!holder) - return - prefs.toggles ^= PREFTOGGLE_SOUND_ADMINHELP - prefs.save_preferences() - to_chat(usr, "You will [(prefs.toggles & PREFTOGGLE_SOUND_ADMINHELP) ? "now" : "no longer"] hear a sound when adminhelps arrive.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Adminhelp Sound", "[prefs.toggles & PREFTOGGLE_SOUND_ADMINHELP ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggleadminalertsound() - set name = "Hear/Silence Admin alerts" - set category = "Prefs - Admin" - set desc = "Toggle hearing a notification when various admin alerts happen" - if(!holder) - return - prefs.toggles ^= PREFTOGGLE_2_SOUND_ADMINALERT - prefs.save_preferences() - to_chat(usr, "You will [(prefs.toggles & PREFTOGGLE_2_SOUND_ADMINALERT) ? "now" : "no longer"] hear a sound when an admin alert shows up.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Admin alert Sound", "[prefs.toggles & PREFTOGGLE_2_SOUND_ADMINALERT ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggleannouncelogin() - set name = "Do/Don't Announce Login" - set category = "Prefs - Admin" - set desc = "Toggle if you want an announcement to admins when you login during a round" - if(!holder) - return - prefs.toggles ^= PREFTOGGLE_ANNOUNCE_LOGIN - prefs.save_preferences() - to_chat(usr, "You will [(prefs.toggles & PREFTOGGLE_ANNOUNCE_LOGIN) ? "now" : "no longer"] have an announcement to other admins when you login.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Login Announcement", "[prefs.toggles & PREFTOGGLE_ANNOUNCE_LOGIN ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggle_hear_radio() - set name = "Show/Hide Radio Chatter" - set category = "Prefs - Admin" - set desc = "Toggle seeing radiochatter from nearby radios and speakers" - if(!holder) - return - prefs.chat_toggles ^= CHAT_RADIO - prefs.save_preferences() - to_chat(usr, "You will [(prefs.chat_toggles & CHAT_RADIO) ? "now" : "no longer"] see radio chatter from nearby radios or speakers") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Radio Chatter", "[prefs.chat_toggles & CHAT_RADIO ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/deadchat() - set name = "Show/Hide Deadchat" - set category = "Prefs - Admin" - set desc ="Toggles seeing deadchat" - if(!holder) - return - prefs.chat_toggles ^= CHAT_DEAD - prefs.save_preferences() - to_chat(src, "You will [(prefs.chat_toggles & CHAT_DEAD) ? "now" : "no longer"] see deadchat.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Deadchat Visibility", "[prefs.chat_toggles & CHAT_DEAD ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggleprayers() - set name = "Show/Hide Prayers" - set category = "Prefs - Admin" - set desc = "Toggles seeing prayers" - if(!holder) - return - prefs.chat_toggles ^= CHAT_PRAYER - prefs.save_preferences() - to_chat(src, "You will [(prefs.chat_toggles & CHAT_PRAYER) ? "now" : "no longer"] see prayerchat.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Prayer Visibility", "[prefs.chat_toggles & CHAT_PRAYER ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggle_prayer_sound() - set name = "Hear/Silence Prayer Sounds" - set category = "Prefs - Admin" - set desc = "Hear Prayer Sounds" - if(!holder) - return - prefs.toggles ^= PREFTOGGLE_SOUND_PRAYERS - prefs.save_preferences() - to_chat(usr, "You will [(prefs.toggles & PREFTOGGLE_SOUND_PRAYERS) ? "now" : "no longer"] hear a sound when prayers arrive.") - SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Prayer Sounds", "[prefs.toggles & PREFTOGGLE_SOUND_PRAYERS ? "Enabled" : "Disabled"]")) - -/client/proc/colorasay() - set name = "Set Admin Say Color" - set category = "Prefs - Admin" - set desc = "Set the color of your ASAY messages" - if(!holder) - return - if(!CONFIG_GET(flag/allow_admin_asaycolor)) - to_chat(src, "Custom Asay color is currently disabled by the server.") - return - var/new_asaycolor = tgui_color_picker(src, "Please select your ASAY color.", "ASAY color", prefs.asaycolor) - if(new_asaycolor) - prefs.asaycolor = sanitize_ooccolor(new_asaycolor) - prefs.save_preferences() - SSblackbox.record_feedback("tally", "admin_verb", 1, "Set ASAY Color") - return - -/client/proc/resetasaycolor() - set name = "Reset your Admin Say Color" - set desc = "Returns your ASAY Color to default" - set category = "Prefs - Admin" - if(!holder) - return - if(!CONFIG_GET(flag/allow_admin_asaycolor)) - to_chat(src, "Custom Asay color is currently disabled by the server.") - return - prefs.asaycolor = initial(prefs.asaycolor) - prefs.save_preferences() diff --git a/code/modules/client/verbs/etips.dm b/code/modules/client/verbs/etips.dm deleted file mode 100644 index f7e908f7ec042..0000000000000 --- 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.toggles2 ^= PREFTOGGLE_2_ENABLE_TIPS - prefs.save_preferences() - to_chat(usr, "Examine tooltips [(prefs.toggles2 & PREFTOGGLE_2_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, "Tooltip delay set to [indelay] milliseconds.") diff --git a/code/modules/client/verbs/looc.dm b/code/modules/client/verbs/looc.dm index 4b0f4b46081a5..6b5c11abfb457 100644 --- a/code/modules/client/verbs/looc.dm +++ b/code/modules/client/verbs/looc.dm @@ -20,7 +20,7 @@ GLOBAL_VAR_INIT(looc_allowed, TRUE) var/raw_msg = msg - if(!(prefs.toggles & CHAT_OOC)) + if(!prefs.read_player_preference(/datum/preference/toggle/chat_ooc)) to_chat(src, "You have OOC (and therefore LOOC) muted.") return @@ -64,14 +64,14 @@ GLOBAL_VAR_INIT(looc_allowed, TRUE) for(var/turf/viewed_turf in view(get_turf(mob))) in_view[viewed_turf] = TRUE for(var/client/client in GLOB.clients) - if(!client.mob || !(client.prefs.toggles & CHAT_OOC) || (client in GLOB.admins)) + if(!client.mob || !client.prefs.read_player_preference(/datum/preference/toggle/chat_ooc) || (client in GLOB.admins)) continue if(in_view[get_turf(client.mob)]) targets |= client to_chat(client, "LOOC: [mob.name]: [msg]", avoid_highlighting = (client == src)) for(var/client/client in GLOB.admins) - if(!(client.prefs.toggles & CHAT_OOC)) + if(!client.prefs.read_player_preference(/datum/preference/toggle/chat_ooc)) continue var/prefix = "[(client in targets) ? "" : "(R)"]LOOC" to_chat(client, "[prefix]: [ADMIN_LOOKUPFLW(mob)]: [msg]", avoid_highlighting = (client == src)) diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index c052e1a83c412..93573d1b40970 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -66,7 +66,7 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") message_admins("[key_name_admin(src)] has attempted to post a clickable link in OOC: [msg]") return - if(!(prefs.chat_toggles & CHAT_OOC)) + if(!(prefs.read_player_preference(/datum/preference/toggle/chat_ooc))) to_chat(src, "You have OOC muted.") return if(OOC_FILTER_CHECK(raw_msg)) @@ -76,18 +76,18 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") mob.log_talk(raw_msg, LOG_OOC) var/keyname = key - if(prefs.unlock_content) - if(prefs.toggles & PREFTOGGLE_MEMBER_PUBLIC) - keyname = "[icon2html('icons/member_content.dmi', world, "blag")][keyname]" + var/ooccolor = prefs.read_player_preference(/datum/preference/color/ooc_color) + if(prefs.unlock_content && prefs.read_player_preference(/datum/preference/toggle/member_public)) + keyname = "[icon2html('icons/member_content.dmi', world, "blag")][keyname]" //Get client badges var/badge_data = badge_parse(get_badges()) //The linkify span classes and linkify=TRUE below make ooc text get clickable chat href links if you pass in something resembling a url for(var/client/C in GLOB.clients) - if(C.prefs.chat_toggles & CHAT_OOC) + if(C.prefs.read_player_preference(/datum/preference/toggle/chat_ooc)) if(holder) if(!holder.fakekey || C.holder) if(check_rights_for(src, R_ADMIN)) - to_chat(C, "[badge_data][CONFIG_GET(flag/allow_admin_ooccolor) && prefs.ooccolor ? "" :"" ]OOC: [keyname][holder.fakekey ? "/([holder.fakekey])" : ""]: [msg]", allow_linkify = TRUE) + to_chat(C, "[badge_data][CONFIG_GET(flag/allow_admin_ooccolor) && ooccolor ? "" :"" ]OOC: [keyname][holder.fakekey ? "/([holder.fakekey])" : ""]: [msg]", allow_linkify = TRUE) else to_chat(C, "[badge_data]OOC: [keyname][holder.fakekey ? "/([holder.fakekey])" : ""]: [msg]") else @@ -138,44 +138,17 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") GLOB.dooc_allowed = !GLOB.dooc_allowed /client/proc/set_ooc(newColor as color) - set name = "Set Player OOC Color" + set name = "Set All Player OOC Color" set desc = "Modifies player OOC Color" set category = "Fun" - GLOB.OOC_COLOR = sanitize_ooccolor(newColor) + GLOB.OOC_COLOR = sanitize_hexcolor(newColor, desired_format = 6, include_crunch = TRUE) /client/proc/reset_ooc() - set name = "Reset Player OOC Color" + set name = "Reset All Player OOC Color" set desc = "Returns player OOC Color to default" set category = "Fun" 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 = tgui_color_picker(src, "Please select your OOC color.", "OOC color", prefs.ooccolor) - if(new_ooccolor) - prefs.ooccolor = sanitize_ooccolor(new_ooccolor) - prefs.save_preferences() - SSblackbox.record_feedback("tally", "admin_verb", 1, "Set OOC Color") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - return - -/client/verb/resetcolorooc() - set name = "Reset Your OOC Color" - set desc = "Returns your OOC Color to default" - set category = "Preferences" - - if(!holder || !check_rights_for(src, R_ADMIN)) - if(!is_content_unlocked()) - return - - prefs.ooccolor = initial(prefs.ooccolor) - prefs.save_preferences() - //Checks admin notice /client/verb/admin_notice() set name = "Adminnotice" @@ -227,7 +200,7 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") else prefs.ignoring |= C.key to_chat(src, "You are [(C.key in prefs.ignoring) ? "now" : "no longer"] ignoring [displayed_key] on the OOC channel.") - prefs.save_preferences() + prefs.mark_undatumized_dirty_player() /client/verb/select_ignore() set name = "Ignore" diff --git a/code/modules/clothing/glasses/_glasses.dm b/code/modules/clothing/glasses/_glasses.dm index f170abab5f1db..1b5c6bc66cb16 100644 --- a/code/modules/clothing/glasses/_glasses.dm +++ b/code/modules/clothing/glasses/_glasses.dm @@ -526,12 +526,13 @@ if(H.client) if(H.client.prefs) if(src == H.glasses) - H.client.prefs.toggles2 ^= PREFTOGGLE_2_USES_GLASSES_COLOUR - if(H.client.prefs.toggles2 & PREFTOGGLE_2_USES_GLASSES_COLOUR) + var/current_color = H.client.prefs.read_player_preference(/datum/preference/toggle/glasses_color) + H.client.prefs.update_preference(/datum/preference/toggle/glasses_color, !current_color) + if(!current_color) to_chat(H, "You will now see glasses colors.") else to_chat(H, "You will no longer see glasses colors.") - H.update_glasses_color(src, 1) + H.update_glasses_color(src, TRUE) /obj/item/clothing/glasses/proc/change_glass_color(mob/living/carbon/human/H, datum/client_colour/glass_colour/new_color_type) var/old_colour_type = glass_colour_type @@ -545,7 +546,9 @@ /mob/living/carbon/human/proc/update_glasses_color(obj/item/clothing/glasses/G, glasses_equipped) - if(((client && client.prefs.toggles2 & PREFTOGGLE_2_USES_GLASSES_COLOUR) || G.force_glass_colour) && glasses_equipped) + if(!client) + return + if((client.prefs?.read_player_preference(/datum/preference/toggle/glasses_color) || G.force_glass_colour) && glasses_equipped) add_client_colour(G.glass_colour_type) else remove_client_colour(G.glass_colour_type) diff --git a/code/modules/clothing/spacesuits/hardsuit.dm b/code/modules/clothing/spacesuits/hardsuit.dm index d59a33cc6f993..20836c2716009 100644 --- a/code/modules/clothing/spacesuits/hardsuit.dm +++ b/code/modules/clothing/spacesuits/hardsuit.dm @@ -59,7 +59,8 @@ ..() if(suit) suit.RemoveHelmet() - soundloop.stop(user) + if(user.client) + soundloop.stop(user) /obj/item/clothing/head/helmet/space/hardsuit/item_action_slot_check(slot) if(slot == ITEM_SLOT_HEAD) @@ -70,10 +71,11 @@ if(slot != ITEM_SLOT_HEAD) if(suit) suit.RemoveHelmet() - soundloop.stop(user) + if(user.client) + soundloop.stop(user) else qdel(src) - else + else if(user.client) soundloop.start(user) /obj/item/clothing/head/helmet/space/hardsuit/proc/toggle_hud(mob/user) @@ -179,7 +181,7 @@ /obj/item/clothing/suit/space/hardsuit/equipped(mob/user, slot) ..() - if(jetpack) + if(isatom(jetpack)) if(slot == ITEM_SLOT_OCLOTHING) for(var/X in jetpack.actions) var/datum/action/A = X @@ -187,7 +189,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/toggles.dm b/code/modules/clothing/suits/toggles.dm index 42b330ef71490..6ea3a0a973e2c 100644 --- a/code/modules/clothing/suits/toggles.dm +++ b/code/modules/clothing/suits/toggles.dm @@ -151,7 +151,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/clothing/under/accessories.dm b/code/modules/clothing/under/accessories.dm index 6e729d3027a0e..cf47d13e89488 100755 --- a/code/modules/clothing/under/accessories.dm +++ b/code/modules/clothing/under/accessories.dm @@ -265,12 +265,14 @@ /obj/item/clothing/accessory/armband/blue name = "blue armband" desc = "A fancy blue armband!" - color = list(0,0,1, 0,1,0, 1,0,0) + icon_state = "medband" + color = "#0000ff" /obj/item/clothing/accessory/armband/green name = "green armband" desc = "A fancy green armband!" - color = list(0,1,0, 1,0,0, 0,0,1) + icon_state = "medband" + color = "#00ff00" /obj/item/clothing/accessory/armband/deputy name = "security deputy armband" diff --git a/code/modules/crew_objectives/_crew_objectives.dm b/code/modules/crew_objectives/_crew_objectives.dm index 9ff430cecb779..4b3db29e119a3 100644 --- a/code/modules/crew_objectives/_crew_objectives.dm +++ b/code/modules/crew_objectives/_crew_objectives.dm @@ -1,5 +1,5 @@ /datum/controller/subsystem/job/proc/give_crew_objective(datum/mind/crewMind, mob/M) - if(CONFIG_GET(flag/allow_crew_objectives) && ((M?.client?.prefs.toggles2 & PREFTOGGLE_2_CREW_OBJECTIVES) || (crewMind?.current?.client?.prefs.toggles2 & PREFTOGGLE_2_CREW_OBJECTIVES))) + if(CONFIG_GET(flag/allow_crew_objectives) && (M?.client?.prefs.read_player_preference(/datum/preference/toggle/crew_objectives) || crewMind?.current?.client?.prefs.read_player_preference(/datum/preference/toggle/crew_objectives))) generate_individual_objectives(crewMind) return diff --git a/code/modules/economy/account.dm b/code/modules/economy/account.dm index f54d8eafae465..15d63a40921ae 100644 --- a/code/modules/economy/account.dm +++ b/code/modules/economy/account.dm @@ -110,7 +110,7 @@ for(var/obj/A in bank_cards) var/mob/card_holder = recursive_loc_check(A, /mob) if(ismob(card_holder)) //If on a mob - if(card_holder.client && !(card_holder.client.prefs.chat_toggles & CHAT_BANKCARD) && !force) + if(card_holder.client && !card_holder.client.prefs.read_player_preference(/datum/preference/toggle/chat_bankcard) && !force) return card_holder.playsound_local(get_turf(card_holder), 'sound/machines/twobeep_high.ogg', 50, TRUE) @@ -118,14 +118,14 @@ to_chat(card_holder, "[icon2html(A, card_holder)] *[message]*") else if(isturf(A.loc)) //If on the ground for(var/mob/M as() in hearers(1,get_turf(A))) - if(M.client && !(M.client.prefs.chat_toggles & CHAT_BANKCARD) && !force) + if(M.client && !M.client.prefs.read_player_preference(/datum/preference/toggle/chat_bankcard) && !force) return playsound(A, 'sound/machines/twobeep_high.ogg', 50, TRUE) A.audible_message("[icon2html(A, hearers(A))] *[message]*", null, 1) break else for(var/mob/M in A.loc) //If inside a container with other mobs (e.g. locker) - if(M.client && !(M.client.prefs.chat_toggles & CHAT_BANKCARD) && !force) + if(M.client && !M.client.prefs.read_player_preference(/datum/preference/toggle/chat_bankcard) && !force) return M.playsound_local(get_turf(M), 'sound/machines/twobeep_high.ogg', 50, TRUE) if(M.can_hear()) diff --git a/code/modules/events/aurora_caelus.dm b/code/modules/events/aurora_caelus.dm index a373831d294de..5417024e15a26 100644 --- a/code/modules/events/aurora_caelus.dm +++ b/code/modules/events/aurora_caelus.dm @@ -23,7 +23,7 @@ sender_override = "Nanotrasen Meteorology Division") for(var/V in GLOB.player_list) var/mob/M = V - if((M.client.prefs.toggles & PREFTOGGLE_SOUND_MIDI) && is_station_level(M.z)) + if(M.client.prefs.read_player_preference(/datum/preference/toggle/sound_midi) && is_station_level(M.z)) M.playsound_local(M, 'sound/ambience/aurora_caelus.ogg', 20, FALSE, pressure_affected = FALSE) /datum/round_event/aurora_caelus/start() diff --git a/code/modules/events/devil.dm b/code/modules/events/devil.dm index 7000d2b336606..6e41c375a1aea 100644 --- a/code/modules/events/devil.dm +++ b/code/modules/events/devil.dm @@ -46,8 +46,7 @@ var/mob/living/carbon/human/new_devil = new(spawn_loc) if(!spawn_loc) SSjob.SendToLateJoin(new_devil) - var/datum/character_save/CS = new() //Randomize appearance for the devil. - CS.copy_to(new_devil) + new_devil.randomize_human_appearance(~RANDOMIZE_SPECIES) //Randomize appearance for the devil. new_devil.dna.update_dna_identity() return new_devil diff --git a/code/modules/events/operative.dm b/code/modules/events/operative.dm index bea4dad09d14c..f74c5d70b172f 100644 --- a/code/modules/events/operative.dm +++ b/code/modules/events/operative.dm @@ -23,9 +23,8 @@ if(!spawn_locs.len) return MAP_ERROR - var/mob/living/carbon/human/operative = new(pick(spawn_locs)) - var/datum/character_save/CS = new - CS.copy_to(operative) + var/mob/living/carbon/human/operative = new (pick(spawn_locs)) + operative.randomize_human_appearance(~RANDOMIZE_SPECIES) operative.dna.update_dna_identity() var/datum/mind/Mind = new /datum/mind(selected.key) Mind.assigned_role = "Lone Operative" diff --git a/code/modules/flufftext/Hallucination.dm b/code/modules/flufftext/Hallucination.dm index 4e9501a4fb800..2247513e0ea2b 100644 --- a/code/modules/flufftext/Hallucination.dm +++ b/code/modules/flufftext/Hallucination.dm @@ -710,10 +710,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.toggles & PREFTOGGLE_RUNECHAT_GLOBAL)) + if (!is_radio && !target.client?.prefs.read_player_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, GLOBAL_PROC_REF(flick_overlay), speech_overlay, list(target.client), 30) - if (target.client?.prefs.toggles & PREFTOGGLE_RUNECHAT_GLOBAL) + if (target.client?.prefs.read_player_preference(/datum/preference/toggle/enable_runechat)) create_chat_message(person, understood_language, list(target), chosen, spans) to_chat(target, message) qdel(src) diff --git a/code/modules/instruments/songs/play_legacy.dm b/code/modules/instruments/songs/play_legacy.dm index ff344dff93f6d..c5503c5590865 100644 --- a/code/modules/instruments/songs/play_legacy.dm +++ b/code/modules/instruments/songs/play_legacy.dm @@ -85,7 +85,7 @@ if(user && HAS_TRAIT(user, TRAIT_MUSICIAN) && isliving(M)) var/mob/living/L = M L.apply_status_effect(STATUS_EFFECT_GOOD_MUSIC) - if(!(M?.client?.prefs?.toggles & PREFTOGGLE_SOUND_INSTRUMENTS)) + if(!M?.client?.prefs?.read_player_preference(/datum/preference/toggle/sound_instruments)) continue M.playsound_local(source, null, volume * using_instrument.volume_multiplier, S = music_played) // Could do environment and echo later but not for now diff --git a/code/modules/instruments/songs/play_synthesized.dm b/code/modules/instruments/songs/play_synthesized.dm index ccf59fc6bec92..b1a145925539f 100644 --- a/code/modules/instruments/songs/play_synthesized.dm +++ b/code/modules/instruments/songs/play_synthesized.dm @@ -65,7 +65,7 @@ if(user && HAS_TRAIT(user, TRAIT_MUSICIAN) && isliving(M)) var/mob/living/L = M L.apply_status_effect(STATUS_EFFECT_GOOD_MUSIC) - if(!(M?.client?.prefs?.toggles & PREFTOGGLE_SOUND_INSTRUMENTS)) + if(!M?.client?.prefs.read_player_preference(/datum/preference/toggle/sound_instruments)) continue M.playsound_local(get_turf(parent), null, volume, FALSE, K.frequency, INSTRUMENT_DISTANCE_NO_FALLOFF, channel, null, copy, distance_multiplier = INSTRUMENT_DISTANCE_FALLOFF_BUFF) // Could do environment and echo later but not for now diff --git a/code/modules/interview/interview_manager.dm b/code/modules/interview/interview_manager.dm index d8a8c8cbd11c3..4c32b22219a3d 100644 --- a/code/modules/interview/interview_manager.dm +++ b/code/modules/interview/interview_manager.dm @@ -170,7 +170,7 @@ GLOBAL_DATUM_INIT(interviews, /datum/interview_manager, new) if (admins_present <= 0 && to_queue.owner) to_chat(to_queue.owner, "No active admins are online, your interview's submission was sent through TGS to admins who are available. This may use IRC or Discord.") for(var/client/X as() in GLOB.admins) - if(X.prefs.toggles & PREFTOGGLE_SOUND_ADMINHELP) + if(X.prefs.read_player_preference(/datum/preference/toggle/sound_adminhelp)) SEND_SOUND(X, sound('sound/effects/adminhelp.ogg')) window_flash(X, ignorepref = TRUE) to_chat(X, "Interview for [ckey] enqueued for review. Current position in queue: [to_queue.pos_in_queue]") diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index 994e8d5136fb5..0d9900fc32cb4 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -2,6 +2,10 @@ ///The name of the job , used for preferences, bans and more. Make sure you know what you're doing before changing this. var/title = "NOPE" + /// The description of the job, used for preferences menu. + /// Keep it short and useful. Avoid in-jokes, these are for new players. + var/description + ///Job access. The use of minimal_access or access is determined by a config setting: config.jobs_have_minimal_access var/list/minimal_access = list() //Useful for servers which prefer to only have access given to the places a job absolutely needs (Larger server population) var/list/access = list() //Useful for servers which either have fewer players, so each person needs to fill more than one role, or servers which like to give more access, so players can't hide forever in their super secure departments (I'm looking at you, chemistry!) @@ -16,6 +20,12 @@ var/flag = NONE //Deprecated //Except not really, still used throughout the codebase var/auto_deadmin_role_flags = NONE + /// If this job should show in the preferences menu + var/show_in_prefs = TRUE + + /// The head of the department to show in the preferences menu + var/department_head_for_prefs + ///Mostly deprecated, but only used in pref job savefiles var/department_flag = NONE @@ -77,6 +87,8 @@ ///Bitfield of departments this job belongs with var/departments = NONE + /// Same as the departments bitflag, but only one is allowed. Used in the preferences menu. + var/department_for_prefs = null ///Is this job affected by weird spawns like the ones from station traits var/random_spawns_possible = TRUE /// Should this job be allowed to be picked for the bureaucratic error event? @@ -108,24 +120,33 @@ lightup_areas = typecacheof(lightup_areas) minimal_lightup_areas = typecacheof(minimal_lightup_areas) -//Only override this proc, unless altering loadout code. Loadouts act on H but get info from M -//H is usually a human unless an /equip override transformed it -//do actions on H but send messages to M as the key may not have been transferred_yet -/datum/job/proc/after_spawn(mob/living/H, mob/M, latejoin = FALSE) - //do actions on H but send messages to M as the key may not have been transferred_yet - SEND_GLOBAL_SIGNAL(COMSIG_GLOB_JOB_AFTER_SPAWN, src, H, M, latejoin) - if(mind_traits) - for(var/t in mind_traits) - ADD_TRAIT(H.mind, t, JOB_TRAIT) +/// Only override this proc, unless altering loadout code. Loadouts act on H but get info from M +/// H is usually a human unless an /equip override transformed it +/// do actions on H but send messages to M as the key may not have been transferred_yet +/// preference_source allows preferences to be retrieved if the original mob (M) is null - for use on preference dummies. +/// Don't do non-visual changes if M.client is null, since that means it's just a dummy and doesn't need them. +/datum/job/proc/after_spawn(mob/living/H, mob/M, latejoin = FALSE, client/preference_source, on_dummy = FALSE) + if(!on_dummy) // Bad dummy + //do actions on H but send messages to M as the key may not have been transferred_yet + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_JOB_AFTER_SPAWN, src, H, M, latejoin) + if(mind_traits && H?.mind) + for(var/t in mind_traits) + ADD_TRAIT(H.mind, t, JOB_TRAIT) if(!ishuman(H)) return + apply_loadout_to_mob(H, M, preference_source, on_dummy) + +/proc/apply_loadout_to_mob(mob/living/carbon/human/H, mob/M, client/preference_source, on_dummy = FALSE) var/mob/living/carbon/human/human = H var/list/gear_leftovers = list() - if(M.client && LAZYLEN(M.client.prefs.active_character.equipped_gear)) - for(var/gear in M.client.prefs.active_character.equipped_gear) + var/jumpsuit_style = preference_source.prefs.read_character_preference(/datum/preference/choiced/jumpsuit_style) + if(preference_source && LAZYLEN(preference_source.prefs.equipped_gear)) + for(var/gear in preference_source.prefs.equipped_gear) var/datum/gear/G = GLOB.gear_datums[gear] if(G) + if(!G.is_equippable) + continue var/permitted = FALSE if(G.allowed_roles && H.mind && (H.mind.assigned_role in G.allowed_roles)) @@ -142,47 +163,61 @@ permitted = FALSE if(!permitted) - to_chat(M, "Your current species or role does not permit you to spawn with [G.display_name]!") + if(M.client) + to_chat(M, "Your current species or role does not permit you to spawn with [G.display_name]!") continue if(G.slot) - if(H.equip_to_slot_or_del(G.spawn_item(H, skirt_pref = M.client.prefs.active_character.jumpsuit_style), G.slot)) - to_chat(M, "Equipping you with [G.display_name]!") + var/obj/o + if(on_dummy) // remove the old item + o = H.get_item_by_slot(G.slot) + H.doUnEquip(H.get_item_by_slot(G.slot), newloc = H.drop_location(), invdrop = FALSE, silent = TRUE) + if(H.equip_to_slot_or_del(G.spawn_item(H, skirt_pref = jumpsuit_style), G.slot)) + if(M.client) + to_chat(M, "Equipping you with [G.display_name]!") + if(on_dummy && o) + qdel(o) else gear_leftovers += G else gear_leftovers += G else - M.client.prefs.active_character.equipped_gear -= gear + preference_source.prefs.equipped_gear -= gear + preference_source.prefs.mark_undatumized_dirty_character() if(gear_leftovers.len) for(var/datum/gear/G in gear_leftovers) - var/metadata = M.client.prefs.active_character.equipped_gear[G.id] - var/item = G.spawn_item(null, metadata, M.client.prefs.active_character.jumpsuit_style) + var/metadata = preference_source.prefs.equipped_gear[G.id] + var/item = G.spawn_item(null, metadata, jumpsuit_style) var/atom/placed_in = human.equip_or_collect(item) if(istype(placed_in)) if(isturf(placed_in)) - to_chat(M, "Placing [G.display_name] on [placed_in]!") + if(M.client) + to_chat(M, "Placing [G.display_name] on [placed_in]!") else - to_chat(M, "Placing [G.display_name] in [placed_in.name]]") + if(M.client) + to_chat(M, "Placing [G.display_name] in [placed_in.name]]") continue if(H.equip_to_appropriate_slot(item)) - to_chat(M, "Placing [G.display_name] in your inventory!") + if(M.client) + to_chat(M, "Placing [G.display_name] in your inventory!") continue if(H.put_in_hands(item)) - to_chat(M, "Placing [G.display_name] in your hands!") + if(M.client) + to_chat(M, "Placing [G.display_name] in your hands!") continue var/obj/item/storage/B = (locate() in H) if(B) - G.spawn_item(B, metadata, M.client.prefs.active_character.jumpsuit_style) - to_chat(M, "Placing [G.display_name] in [B.name]!") + G.spawn_item(B, metadata, jumpsuit_style) + if(M.client) + to_chat(M, "Placing [G.display_name] in [B.name]!") continue - - to_chat(M, "Failed to locate a storage object on your mob, either you spawned with no hands free and no backpack or this is a bug.") + if(M.client) + to_chat(M, "Failed to locate a storage object on your mob, either you spawned with no hands free and no backpack or this is a bug.") qdel(item) /datum/job/proc/announce(mob/living/carbon/human/H) @@ -213,7 +248,7 @@ if(CONFIG_GET(flag/enforce_human_authority) && (title in GLOB.command_positions)) if(H.dna.species.id != SPECIES_HUMAN) H.set_species(/datum/species/human) - H.apply_pref_name("human", preference_source) + H.apply_pref_name(/datum/preference/name/backup_human, preference_source) if(!visualsOnly) var/datum/bank_account/bank_account = new(H.real_name, src) bank_account.payday(STARTING_PAYCHECKS, TRUE) @@ -419,3 +454,68 @@ scandisease.spread_text = "None" scandisease.visibility_flags |= HIDDEN_SCANNER H.ForceContractDisease(scandisease) + +/// Applies the preference options to the spawning mob, taking the job into account. Assumes the client has the proper mind. +/mob/living/proc/apply_prefs_job(client/player_client, datum/job/job) + +/mob/living/carbon/human/apply_prefs_job(client/player_client, datum/job/job) + var/fully_randomize = 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 & DEPT_BITFLAG_COM) + + if(fully_randomize) + if(require_human) + player_client.prefs.randomize_appearance_prefs(~RANDOMIZE_SPECIES) + else + player_client.prefs.randomize_appearance_prefs() + + player_client.prefs.apply_prefs_to(src) + + if (require_human) + set_species(/datum/species/human) + else + var/is_antag = (player_client.mob.mind in GLOB.pre_setup_antags) + 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) + apply_pref_name(/datum/preference/name/backup_human, player_client) + if(CONFIG_GET(flag/force_random_names)) + var/species_type = player_client.prefs.read_character_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/gender = player_client.prefs.read_character_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) + 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) + +/mob/living/silicon/robot/apply_prefs_job(client/player_client, datum/job/job) + if(mmi) + var/organic_name + if(player_client.prefs.read_character_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. + + var/species_type = player_client.prefs.read_character_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + organic_name = species.random_name(player_client.prefs.read_character_preference(/datum/preference/choiced/gender), TRUE) + else + if(!player_client) + return // Disconnected while checking the appearance ban. + organic_name = player_client.prefs.read_character_preference(/datum/preference/name/real_name) + + mmi.name = "[initial(mmi.name)]: [organic_name]" + if(mmi.brain) + mmi.brain.name = "[organic_name]'s brain" + if(mmi.brainmob) + 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(player_client.prefs.read_character_preference(/datum/preference/name/cyborg) != DEFAULT_CYBORG_NAME) + apply_pref_name(/datum/preference/name/cyborg, player_client) diff --git a/code/modules/jobs/job_types/ai.dm b/code/modules/jobs/job_types/ai.dm index e6ec6f37083aa..a2de112537f24 100644 --- a/code/modules/jobs/job_types/ai.dm +++ b/code/modules/jobs/job_types/ai.dm @@ -1,7 +1,10 @@ /datum/job/ai title = JOB_NAME_AI flag = AI_JF - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_SILICON + description = "Follow your laws above all else, be the invisible eye that watches all." + department_for_prefs = DEPT_BITFLAG_SILICON + department_head_for_prefs = JOB_NAME_AI + auto_deadmin_role_flags = DEADMIN_POSITION_SILICON department_flag = ENGSEC faction = "Station" total_positions = 1 @@ -24,7 +27,7 @@ CRASH("dynamic preview is unsupported") . = H.AIize(latejoin,preference_source) -/datum/job/ai/after_spawn(mob/H, mob/M, latejoin) +/datum/job/ai/after_spawn(mob/H, mob/M, latejoin = FALSE, client/preference_source, on_dummy = FALSE) . = ..() if(latejoin) var/obj/structure/AIcore/latejoin_inactive/lateJoinCore @@ -38,8 +41,11 @@ H.forceMove(lateJoinCore.loc) qdel(lateJoinCore) var/mob/living/silicon/ai/AI = H - AI.apply_pref_name("ai", M.client) //If this runtimes oh well jobcode is fucked. - AI.set_core_display_icon(null, M.client) + if(M.client) + AI.apply_pref_name(/datum/preference/name/ai, preference_source) //If this runtimes oh well jobcode is fucked. + AI.set_core_display_icon(null, preference_source) + if(!M.client || on_dummy) + return //we may have been created after our borg if(SSticker.current_state == GAME_STATE_SETTING_UP) diff --git a/code/modules/jobs/job_types/assistant.dm b/code/modules/jobs/job_types/assistant.dm index 1e0a5a998839b..3d3f12a2b4f78 100644 --- a/code/modules/jobs/job_types/assistant.dm +++ b/code/modules/jobs/job_types/assistant.dm @@ -4,6 +4,8 @@ Assistant /datum/job/assistant title = JOB_NAME_ASSISTANT flag = ASSISTANT + description = "Help out around the station or ask the Head of Personnel for an assignment. As the lowest-level position, expect to be treated like an intern most of the time." + department_for_prefs = DEPT_BITFLAG_ASSISTANT supervisors = "absolutely everyone" faction = "Station" total_positions = 5 @@ -43,16 +45,30 @@ 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 - name = "Assistant" - id = /obj/item/card/id/job/assistant +/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 - Always Grey" + +/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() + H.update_inv_w_uniform() diff --git a/code/modules/jobs/job_types/atmospheric_technician.dm b/code/modules/jobs/job_types/atmospheric_technician.dm index 100f33a534ccf..342be8664dd37 100644 --- a/code/modules/jobs/job_types/atmospheric_technician.dm +++ b/code/modules/jobs/job_types/atmospheric_technician.dm @@ -1,6 +1,8 @@ /datum/job/atmospheric_technician title = JOB_NAME_ATMOSPHERICTECHNICIAN flag = ATMOSTECH + description = "Maintain the air distribution loop to ensure adequate atmospheric conditions in the station, re-pressurize areas after hull breaches, and be a firefighter if necessary." + department_for_prefs = DEPT_BITFLAG_ENG department_head = list(JOB_NAME_CHIEFENGINEER) supervisors = "the chief engineer" faction = "Station" diff --git a/code/modules/jobs/job_types/bartender.dm b/code/modules/jobs/job_types/bartender.dm index ca700fbf3de81..8b19676851e10 100644 --- a/code/modules/jobs/job_types/bartender.dm +++ b/code/modules/jobs/job_types/bartender.dm @@ -1,6 +1,8 @@ /datum/job/bartender title = JOB_NAME_BARTENDER flag = BARTENDER + description = "Brew a variety of drinks for the crew, cooperate with Botany and Chemistry for more exotic recipes, create a comfy atmosphere in your Bar." + department_for_prefs = DEPT_BITFLAG_SRV department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" diff --git a/code/modules/jobs/job_types/botanist.dm b/code/modules/jobs/job_types/botanist.dm index da9714209f65a..3b6dc26ab0a23 100644 --- a/code/modules/jobs/job_types/botanist.dm +++ b/code/modules/jobs/job_types/botanist.dm @@ -1,6 +1,8 @@ /datum/job/botanist title = JOB_NAME_BOTANIST flag = BOTANIST + description = "Grow plants for the Kitchen, Bar and Chemistry. Sell cannabis and other goods to the crew. Clone people with Replica Pods when needed." + department_for_prefs = DEPT_BITFLAG_SRV department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" diff --git a/code/modules/jobs/job_types/brig_physician.dm b/code/modules/jobs/job_types/brig_physician.dm index a4773dcb7241c..8c7e3af70259e 100644 --- a/code/modules/jobs/job_types/brig_physician.dm +++ b/code/modules/jobs/job_types/brig_physician.dm @@ -1,6 +1,9 @@ /datum/job/brig_physician title = JOB_NAME_BRIGPHYSICIAN flag = BRIG_PHYS + description = "Tend to the health of Security Officers and Prisoners, help out at Medbay if you have free time." + department_for_prefs = DEPT_BITFLAG_SEC + department_head_for_prefs = JOB_NAME_HEADOFSECURITY department_head = list(JOB_NAME_CHIEFMEDICALOFFICER) supervisors = "chief medical officer" faction = "Station" diff --git a/code/modules/jobs/job_types/captain.dm b/code/modules/jobs/job_types/captain.dm index b6f9e700710ec..749a7c719e0ba 100755 --- a/code/modules/jobs/job_types/captain.dm +++ b/code/modules/jobs/job_types/captain.dm @@ -1,7 +1,10 @@ /datum/job/captain title = JOB_NAME_CAPTAIN flag = CAPTAIN - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_HEAD|PREFTOGGLE_DEADMIN_POSITION_SECURITY + description = "Supreme leader of the station, oversee and appoint missing heads of staff, manage alert levels and contact CentCom if needed. Don't forget to secure the nuclear authentication disk." + department_for_prefs = DEPT_BITFLAG_CAPTAIN + department_head_for_prefs = JOB_NAME_CAPTAIN + auto_deadmin_role_flags = DEADMIN_POSITION_HEAD|DEADMIN_POSITION_SECURITY department_head = list("CentCom") supervisors = "Nanotrasen officials and Space law" faction = "Station" diff --git a/code/modules/jobs/job_types/cargo_technician.dm b/code/modules/jobs/job_types/cargo_technician.dm index 2c87f164d1b18..fb71ea18b7d4d 100644 --- a/code/modules/jobs/job_types/cargo_technician.dm +++ b/code/modules/jobs/job_types/cargo_technician.dm @@ -1,6 +1,9 @@ /datum/job/cargo_technician title = JOB_NAME_CARGOTECHNICIAN flag = CARGOTECH + description = "Push crates around, deliver bounty papers and mail around the station, make use of the Disposals network to make your life easier." + department_for_prefs = DEPT_BITFLAG_CAR + department_head_for_prefs = JOB_NAME_QUARTERMASTER department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the quartermaster and the head of personnel" faction = "Station" diff --git a/code/modules/jobs/job_types/chaplain.dm b/code/modules/jobs/job_types/chaplain.dm index 47d352262405a..8f26fcfe81a34 100644 --- a/code/modules/jobs/job_types/chaplain.dm +++ b/code/modules/jobs/job_types/chaplain.dm @@ -1,6 +1,8 @@ /datum/job/chaplain title = JOB_NAME_CHAPLAIN flag = CHAPLAIN + description = "Tend to the spiritual well-being of the crew, conduct rites and rituals in your Chapel, exorcise evil spirits and other supernatural beings." + department_for_prefs = DEPT_BITFLAG_CIV department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" @@ -32,8 +34,10 @@ /area/crew_quarters/theatre ) -/datum/job/chaplain/after_spawn(mob/living/H, mob/M) +/datum/job/chaplain/after_spawn(mob/living/H, mob/M, latejoin = FALSE, client/preference_source, on_dummy = FALSE) . = ..() + if(!M.client || on_dummy) + return var/obj/item/storage/book/bible/booze/B = new @@ -49,17 +53,11 @@ return H.mind?.holy_role = HOLY_ROLE_HIGHPRIEST - var/new_religion = DEFAULT_RELIGION - if(M.client && M.client.prefs.active_character.custom_names["religion"]) - new_religion = M.client.prefs.active_character.custom_names["religion"] - - var/new_deity = DEFAULT_DEITY - if(M.client && M.client.prefs.active_character.custom_names["deity"]) - new_deity = M.client.prefs.active_character.custom_names["deity"] + var/new_religion = preference_source?.prefs?.read_character_preference(/datum/preference/name/religion) || DEFAULT_RELIGION + var/new_deity = preference_source?.prefs?.read_character_preference(/datum/preference/name/deity) || DEFAULT_DEITY B.deity_name = new_deity - switch(lowertext(new_religion)) if("christianity") // DEFAULT_RELIGION B.name = pick("The Holy Bible","The Dead Sea Scrolls") diff --git a/code/modules/jobs/job_types/chemist.dm b/code/modules/jobs/job_types/chemist.dm index 8f1e5f78a29df..b2302e2d1fc0c 100644 --- a/code/modules/jobs/job_types/chemist.dm +++ b/code/modules/jobs/job_types/chemist.dm @@ -1,6 +1,8 @@ /datum/job/chemist title = JOB_NAME_CHEMIST flag = CHEMIST + description = "Create healing medicines and fullfill other requests when medicine isn't needed. Label everything you produce correctly to prevent confusion." + department_for_prefs = DEPT_BITFLAG_MED department_head = list(JOB_NAME_CHIEFMEDICALOFFICER) supervisors = "the chief medical officer" faction = "Station" diff --git a/code/modules/jobs/job_types/chief_engineer.dm b/code/modules/jobs/job_types/chief_engineer.dm index 5a84b63b440bf..c412435a0f7ec 100644 --- a/code/modules/jobs/job_types/chief_engineer.dm +++ b/code/modules/jobs/job_types/chief_engineer.dm @@ -1,7 +1,9 @@ /datum/job/chief_engineer title = JOB_NAME_CHIEFENGINEER flag = CHIEF - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_HEAD + description = "Oversee the engineers and atmospheric technicians, keep a watchful eye on the station's engine, gravity generator, and telecomms. Send your staff to repair hull breaches and damaged equipment as necessary." + department_for_prefs = DEPT_BITFLAG_ENG + auto_deadmin_role_flags = DEADMIN_POSITION_HEAD department_head = list(JOB_NAME_CAPTAIN) supervisors = "the captain" head_announce = list("Engineering") diff --git a/code/modules/jobs/job_types/chief_medical_officer.dm b/code/modules/jobs/job_types/chief_medical_officer.dm index a1494007390e6..957e2c688c2ee 100644 --- a/code/modules/jobs/job_types/chief_medical_officer.dm +++ b/code/modules/jobs/job_types/chief_medical_officer.dm @@ -1,9 +1,13 @@ /datum/job/chief_medical_officer title = JOB_NAME_CHIEFMEDICALOFFICER flag = CMO_JF + description = "Oversee paramedics, doctors, chemists, geneticists and the virologist. \ + Ensure doctors and paramedicts are treating people in a timely manner, request medicine and other concoctions from chemists, \ + and ensure geneticists and the virologist are following appropriate safety precautions while performing their research." + department_for_prefs = DEPT_BITFLAG_MED department_head = list(JOB_NAME_CAPTAIN) supervisors = "the captain" - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_HEAD + auto_deadmin_role_flags = DEADMIN_POSITION_HEAD head_announce = list(RADIO_CHANNEL_MEDICAL) faction = "Station" total_positions = 1 diff --git a/code/modules/jobs/job_types/clown.dm b/code/modules/jobs/job_types/clown.dm index b14a6eeb7a834..94684816a1613 100644 --- a/code/modules/jobs/job_types/clown.dm +++ b/code/modules/jobs/job_types/clown.dm @@ -1,6 +1,8 @@ /datum/job/clown title = JOB_NAME_CLOWN flag = CLOWN + description = "Be the life and soul of the station. Entertain the crew with your hilarious jokes and silly antics, including slipping, pie-ing and honking around. Remember your job is to keep things funny for others, not just yourself." + department_for_prefs = DEPT_BITFLAG_CIV department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" @@ -28,9 +30,14 @@ minimal_lightup_areas = list(/area/crew_quarters/theatre) -/datum/job/clown/after_spawn(mob/living/carbon/human/H, mob/M) +/datum/job/clown/after_spawn(mob/living/carbon/human/H, mob/M, latejoin = FALSE, client/preference_source, on_dummy = FALSE) . = ..() - H.apply_pref_name("clown", M.client) + if(!ishuman(H)) + return + if(!M.client || on_dummy) + return + H.apply_pref_name(/datum/preference/name/clown, preference_source) + /datum/outfit/job/clown name = JOB_NAME_CLOWN diff --git a/code/modules/jobs/job_types/cook.dm b/code/modules/jobs/job_types/cook.dm index baf51f3fdce4c..640defbfed874 100644 --- a/code/modules/jobs/job_types/cook.dm +++ b/code/modules/jobs/job_types/cook.dm @@ -1,6 +1,8 @@ /datum/job/cook title = JOB_NAME_COOK flag = COOK + description = "Whip up meals for the crew, get creative and cook different meals, request ingredients from Botany and Cargo. Make sure everyone stays well fed and happy." + department_for_prefs = DEPT_BITFLAG_SRV department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" diff --git a/code/modules/jobs/job_types/curator.dm b/code/modules/jobs/job_types/curator.dm index 39ce4f74f610f..ed143ce073cdd 100644 --- a/code/modules/jobs/job_types/curator.dm +++ b/code/modules/jobs/job_types/curator.dm @@ -1,6 +1,8 @@ /datum/job/curator title = JOB_NAME_CURATOR flag = CURATOR + description = "Be in charge of maintaining the library, engage in peace talks with alien races using your knowledge of all languages, cosplay to your heart's content." + department_for_prefs = DEPT_BITFLAG_CIV department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" diff --git a/code/modules/jobs/job_types/cyborg.dm b/code/modules/jobs/job_types/cyborg.dm index 285941f605171..3a46423433e18 100644 --- a/code/modules/jobs/job_types/cyborg.dm +++ b/code/modules/jobs/job_types/cyborg.dm @@ -1,7 +1,10 @@ /datum/job/cyborg title = JOB_NAME_CYBORG flag = CYBORG - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_SILICON + description = "Follow your AI's interpretation of your laws above all else, or your own interpretation if not connected to an AI. Choose one of many modules with different tools, ask robotics for maintenance and upgrades." + department_for_prefs = DEPT_BITFLAG_SILICON + department_head_for_prefs = JOB_NAME_AI + auto_deadmin_role_flags = DEADMIN_POSITION_SILICON department_flag = ENGSEC faction = "Station" total_positions = 1 @@ -21,7 +24,9 @@ CRASH("dynamic preview is unsupported") return H.Robotize(FALSE, latejoin) -/datum/job/cyborg/after_spawn(mob/living/silicon/robot/R, mob/M) +/datum/job/cyborg/after_spawn(mob/living/silicon/robot/R, mob/M, latejoin = FALSE, client/preference_source, on_dummy = FALSE) + if(!M.client || on_dummy) + return R.updatename(M.client) R.gender = NEUTER diff --git a/code/modules/jobs/job_types/deputy.dm b/code/modules/jobs/job_types/deputy.dm index 9490b3a54aede..ba3acbdce2f97 100644 --- a/code/modules/jobs/job_types/deputy.dm +++ b/code/modules/jobs/job_types/deputy.dm @@ -1,6 +1,8 @@ /datum/job/deputy title = JOB_NAME_DEPUTY flag = DEPUTY + description = "Follow orders and do your best to maintain order on the station while following Space Law." + department_for_prefs = DEPT_BITFLAG_SEC department_head = list(JOB_NAME_HEADOFSECURITY) supervisors = "the head of security" faction = "Station" diff --git a/code/modules/jobs/job_types/detective.dm b/code/modules/jobs/job_types/detective.dm index c28bf31acbadc..36f20de5ea776 100644 --- a/code/modules/jobs/job_types/detective.dm +++ b/code/modules/jobs/job_types/detective.dm @@ -1,7 +1,9 @@ /datum/job/detective title = JOB_NAME_DETECTIVE flag = DETECTIVE - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_SECURITY + description = "Investigate crimes, solve murder mysteries, report your findings to the rest of Security." + department_for_prefs = DEPT_BITFLAG_SEC + auto_deadmin_role_flags = DEADMIN_POSITION_SECURITY department_head = list(JOB_NAME_HEADOFSECURITY) supervisors = "the head of security" faction = "Station" diff --git a/code/modules/jobs/job_types/exploration_team.dm b/code/modules/jobs/job_types/exploration_team.dm index 6ae87e69e3fb5..6a4c735a309ec 100644 --- a/code/modules/jobs/job_types/exploration_team.dm +++ b/code/modules/jobs/job_types/exploration_team.dm @@ -1,6 +1,8 @@ /datum/job/exploration_crew title = JOB_NAME_EXPLORATIONCREW flag = EXPLORATION_CREW + description = "Go out into space to complete different missions for loads of cash. Find and deliver back research disks for rare technologies." + department_for_prefs = DEPT_BITFLAG_SCI department_head = list(JOB_NAME_RESEARCHDIRECTOR) supervisors = "the research director" faction = "Station" diff --git a/code/modules/jobs/job_types/geneticist.dm b/code/modules/jobs/job_types/geneticist.dm index 536225e540727..ea648f5b184b5 100644 --- a/code/modules/jobs/job_types/geneticist.dm +++ b/code/modules/jobs/job_types/geneticist.dm @@ -1,6 +1,8 @@ /datum/job/geneticist title = JOB_NAME_GENETICIST flag = GENETICIST + description = "Discover useful mutations and give them out to the crew at CMO's approval, oversee Cloning, create humanized monkeys for replacement organs and bodyparts if needed." + department_for_prefs = DEPT_BITFLAG_MED department_head = list(JOB_NAME_CHIEFMEDICALOFFICER) supervisors = "the chief medical officer" faction = "Station" diff --git a/code/modules/jobs/job_types/gimmick.dm b/code/modules/jobs/job_types/gimmick.dm index a97a89740fbe4..832f0d9ef4f33 100644 --- a/code/modules/jobs/job_types/gimmick.dm +++ b/code/modules/jobs/job_types/gimmick.dm @@ -1,6 +1,9 @@ /datum/job/gimmick //gimmick var must be set to true for all gimmick jobs BUT the parent title = JOB_NAME_GIMMICK flag = GIMMICK + description = "Use your unique position to provide a service or entertain the crew." + department_for_prefs = DEPT_BITFLAG_ASSISTANT + show_in_prefs = TRUE faction = "Station" total_positions = 0 spawn_positions = 0 @@ -30,9 +33,11 @@ /datum/job/gimmick/barber title = JOB_NAME_BARBER flag = BARBER + description = "Give the crew haircuts using the variety of tools at your disposal, and provide less professional and cosmetic surgeries." department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" gimmick = TRUE + show_in_prefs = FALSE outfit = /datum/outfit/job/gimmick/barber @@ -63,9 +68,11 @@ /datum/job/gimmick/stage_magician title = JOB_NAME_STAGEMAGICIAN flag = MAGICIAN + description = "Use your special tools to provide entertainment for the crew, show them than you can do more than simple parlor magic tricks." department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" gimmick = TRUE + show_in_prefs = FALSE outfit = /datum/outfit/job/gimmick/stage_magician @@ -102,9 +109,11 @@ /datum/job/gimmick/psychiatrist title = JOB_NAME_PSYCHIATRIST flag = PSYCHIATRIST + description = "Provide therapy to the crew through talk sessions, psychoactive drugs, and careful consideration of their thoughts and feelings. Provide mental evaluations for Security." department_head = list(JOB_NAME_CHIEFMEDICALOFFICER) supervisors = "the chief medical officer" gimmick = TRUE + show_in_prefs = FALSE outfit = /datum/outfit/job/gimmick/psychiatrist @@ -133,7 +142,9 @@ /datum/job/gimmick/vip title = JOB_NAME_VIP flag = CELEBRITY + description = "Flaunt around your wealth, organize posh parties and other high life activities with your near-bottomless budget." gimmick = TRUE + show_in_prefs = FALSE outfit = /datum/outfit/job/gimmick/vip diff --git a/code/modules/jobs/job_types/head_of_personnel.dm b/code/modules/jobs/job_types/head_of_personnel.dm index 5eab9c3208750..e4c2b236cdd84 100644 --- a/code/modules/jobs/job_types/head_of_personnel.dm +++ b/code/modules/jobs/job_types/head_of_personnel.dm @@ -1,7 +1,10 @@ /datum/job/head_of_personnel title = JOB_NAME_HEADOFPERSONNEL flag = HOP - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_HEAD + description = "Second in command on the station, oversee the crew assigned to service and cargo positions, handle department transfer requests by consulting relevant heads. Protect Ian at all costs." + department_for_prefs = DEPT_BITFLAG_CAPTAIN + department_head_for_prefs = JOB_NAME_CAPTAIN + auto_deadmin_role_flags = DEADMIN_POSITION_HEAD department_head = list(JOB_NAME_CAPTAIN) supervisors = "the captain" head_announce = list(RADIO_CHANNEL_SUPPLY, RADIO_CHANNEL_SERVICE) diff --git a/code/modules/jobs/job_types/head_of_security.dm b/code/modules/jobs/job_types/head_of_security.dm index ea5391592b190..27848556b0f23 100644 --- a/code/modules/jobs/job_types/head_of_security.dm +++ b/code/modules/jobs/job_types/head_of_security.dm @@ -1,7 +1,9 @@ /datum/job/head_of_security title = JOB_NAME_HEADOFSECURITY flag = HOS - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_HEAD|PREFTOGGLE_DEADMIN_POSITION_SECURITY + description = "Oversee the members of security and ensure they follow Space Law. Deputize other crew members when the station is in need of additional protection." + department_for_prefs = DEPT_BITFLAG_SEC + auto_deadmin_role_flags = DEADMIN_POSITION_HEAD|DEADMIN_POSITION_SECURITY department_head = list(JOB_NAME_CAPTAIN) supervisors = "the captain" head_announce = list(RADIO_CHANNEL_SECURITY) diff --git a/code/modules/jobs/job_types/janitor.dm b/code/modules/jobs/job_types/janitor.dm index 075fced5e720d..552e827768909 100644 --- a/code/modules/jobs/job_types/janitor.dm +++ b/code/modules/jobs/job_types/janitor.dm @@ -1,6 +1,8 @@ /datum/job/janitor title = JOB_NAME_JANITOR flag = JANITOR + description = "Clean up vomit, trash, and other messes around the station. Put down signs to warn people of slipping hazards, and eradicate rodents when you find them. Keep the station clean and tidy." + department_for_prefs = DEPT_BITFLAG_SRV department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" diff --git a/code/modules/jobs/job_types/lawyer.dm b/code/modules/jobs/job_types/lawyer.dm index 51a56ac7d3f01..5eb9c3f3c3d30 100644 --- a/code/modules/jobs/job_types/lawyer.dm +++ b/code/modules/jobs/job_types/lawyer.dm @@ -1,6 +1,8 @@ /datum/job/lawyer title = JOB_NAME_LAWYER flag = LAWYER + description = "Ensure Security follows Space Law and Standard Operating Procedure perfectly, represent your clients in trials and other legal troubles, make sure the crew is treated fairly by the men in red." + department_for_prefs = DEPT_BITFLAG_CIV department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" diff --git a/code/modules/jobs/job_types/medical_doctor.dm b/code/modules/jobs/job_types/medical_doctor.dm index aab24e4abfca2..fd4822379eb17 100644 --- a/code/modules/jobs/job_types/medical_doctor.dm +++ b/code/modules/jobs/job_types/medical_doctor.dm @@ -1,6 +1,8 @@ /datum/job/medical_doctor title = JOB_NAME_MEDICALDOCTOR flag = DOCTOR + description = "Treat people of both minor wounds, serious injuries and resurrect them from the dead. Make use of surgeries and surgical tools, Chemistry's pills and patches, Virology's viruses and in dire cases, Genetics' cloning." + department_for_prefs = DEPT_BITFLAG_MED department_head = list(JOB_NAME_CHIEFMEDICALOFFICER) supervisors = "the chief medical officer" faction = "Station" diff --git a/code/modules/jobs/job_types/mime.dm b/code/modules/jobs/job_types/mime.dm index 91ffe4ba5a5aa..d143e82360f60 100644 --- a/code/modules/jobs/job_types/mime.dm +++ b/code/modules/jobs/job_types/mime.dm @@ -1,6 +1,8 @@ /datum/job/mime title = JOB_NAME_MIME flag = MIME + description = "Be the Clown's mute counterpart and arch nemesis. Conduct pantomimes and performances, create interesting situations with your mime powers. Remember your job is to keep things funny for others, not just yourself." + department_for_prefs = DEPT_BITFLAG_CIV department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" @@ -27,9 +29,14 @@ minimal_lightup_areas = list(/area/crew_quarters/theatre) -/datum/job/mime/after_spawn(mob/living/carbon/human/H, mob/M) +/datum/job/mime/after_spawn(mob/living/carbon/human/H, mob/M, latejoin = FALSE, client/preference_source, on_dummy = FALSE) . = ..() - H.apply_pref_name("mime", M.client) + if(!ishuman(H)) + return + if(!M.client || on_dummy) + return + H.apply_pref_name(/datum/preference/name/mime, preference_source) + /datum/outfit/job/mime name = JOB_NAME_MIME diff --git a/code/modules/jobs/job_types/paramedic.dm b/code/modules/jobs/job_types/paramedic.dm index 6df3f867dfdf2..a04e0856b6145 100644 --- a/code/modules/jobs/job_types/paramedic.dm +++ b/code/modules/jobs/job_types/paramedic.dm @@ -1,6 +1,8 @@ /datum/job/paramedic title = JOB_NAME_PARAMEDIC flag = PARAMEDIC + description = "Retrieve the gravely injured and dead people from around the station, deliver medicine for minor wounds, and keep a close eye on the Crew Monitor in your free time." + department_for_prefs = DEPT_BITFLAG_MED department_head = list(JOB_NAME_CHIEFMEDICALOFFICER) supervisors = "the chief medical officer" faction = "Station" diff --git a/code/modules/jobs/job_types/quartermaster.dm b/code/modules/jobs/job_types/quartermaster.dm index a8dccf3dfabff..9417b5adec897 100644 --- a/code/modules/jobs/job_types/quartermaster.dm +++ b/code/modules/jobs/job_types/quartermaster.dm @@ -1,6 +1,8 @@ /datum/job/quartermaster title = JOB_NAME_QUARTERMASTER flag = QUARTERMASTER + description = "Oversee and direct cargo technicians to fulfill requests for supplies and keep the station well stocked, request funds from department budgets to cover costs, deny frivolous orders when money is tight, and sell anything the station doesn't need." + department_for_prefs = DEPT_BITFLAG_CAR department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the head of personnel" faction = "Station" diff --git a/code/modules/jobs/job_types/research_director.dm b/code/modules/jobs/job_types/research_director.dm index a92ef370caf30..a5020b7923e3e 100644 --- a/code/modules/jobs/job_types/research_director.dm +++ b/code/modules/jobs/job_types/research_director.dm @@ -1,7 +1,9 @@ /datum/job/research_director title = JOB_NAME_RESEARCHDIRECTOR flag = RD_JF - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_HEAD + description = "Oversee the scientists and roboticists and keep up with their research projects, take care of any issues with the station's AI that may arise, ensure research is being prioritized in accordance with the needs of the station." + department_for_prefs = DEPT_BITFLAG_SCI + auto_deadmin_role_flags = DEADMIN_POSITION_HEAD department_head = list(JOB_NAME_CAPTAIN) supervisors = "the captain" head_announce = list("Science") diff --git a/code/modules/jobs/job_types/roboticist.dm b/code/modules/jobs/job_types/roboticist.dm index f71d628273556..37b2b0bed8df9 100644 --- a/code/modules/jobs/job_types/roboticist.dm +++ b/code/modules/jobs/job_types/roboticist.dm @@ -1,6 +1,8 @@ /datum/job/roboticist title = JOB_NAME_ROBOTICIST flag = ROBOTICIST + description = "Create bots and utility mechs for helping out around the station. Construct war machines by the request of the Captain or Head of Security. Make new Cyborgs, give augmentations and implants to crew members." + department_for_prefs = DEPT_BITFLAG_SCI department_head = list(JOB_NAME_RESEARCHDIRECTOR) faction = "Station" total_positions = 2 diff --git a/code/modules/jobs/job_types/scientist.dm b/code/modules/jobs/job_types/scientist.dm index 33f237efe5bb6..e9f8164ad7620 100644 --- a/code/modules/jobs/job_types/scientist.dm +++ b/code/modules/jobs/job_types/scientist.dm @@ -1,6 +1,8 @@ /datum/job/scientist title = JOB_NAME_SCIENTIST flag = SCIENTIST + description = "Engage in Xenobiology, Xenoarchaeology, Nanites, and Toxins; research new technology; and upgrade the machine parts around the station." + department_for_prefs = DEPT_BITFLAG_SCI department_head = list(JOB_NAME_RESEARCHDIRECTOR) supervisors = "the research director" faction = "Station" diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index 1857fbb00ed4a..88885ba498262 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -1,7 +1,9 @@ /datum/job/security_officer title = JOB_NAME_SECURITYOFFICER flag = OFFICER - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_SECURITY + description = "Follow Space Law, patrol the station, arrest criminals and bring them to the Brig." + department_for_prefs = DEPT_BITFLAG_SEC + auto_deadmin_role_flags = DEADMIN_POSITION_SECURITY department_head = list(JOB_NAME_HEADOFSECURITY) supervisors = "the head of security, and the head of your assigned department (if applicable)" faction = "Station" @@ -43,18 +45,19 @@ GLOBAL_LIST_INIT(available_depts, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, SEC_DEPT_SCIENCE, SEC_DEPT_SUPPLY)) -/datum/job/security_officer/after_spawn(mob/living/carbon/human/H, mob/M) +/datum/job/security_officer/after_spawn(mob/living/carbon/human/H, mob/M, latejoin = FALSE, client/preference_source, on_dummy = FALSE) . = ..() // Assign department security var/department - if(M?.client?.prefs) - department = M.client.prefs.active_character.preferred_security_department + if(preference_source?.prefs) + department = preference_source.prefs.read_character_preference(/datum/preference/choiced/security_department) if(!LAZYLEN(GLOB.available_depts) || department == "None") return - else if(department in GLOB.available_depts) - LAZYREMOVE(GLOB.available_depts, department) - else - department = pick_n_take(GLOB.available_depts) + if(!on_dummy && M.client) // The dummy should just use the preference always, and not remove departments. + if(department in GLOB.available_depts) + LAZYREMOVE(GLOB.available_depts, department) + else + department = pick_n_take(GLOB.available_depts) var/ears = null var/accessory = null var/list/dep_access = null @@ -63,32 +66,36 @@ GLOBAL_LIST_INIT(available_depts, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, S switch(department) if(SEC_DEPT_SUPPLY) ears = /obj/item/radio/headset/headset_sec/alt/department/supply - dep_access = list(ACCESS_MAILSORTING, ACCESS_MINING, ACCESS_MINING_STATION, ACCESS_CARGO, ACCESS_AUX_BASE) - destination = /area/security/checkpoint/supply - spawn_point = locate(/obj/effect/landmark/start/depsec/supply) in GLOB.department_security_spawns accessory = /obj/item/clothing/accessory/armband/cargo - minimal_lightup_areas |= GLOB.supply_lightup_areas + if(!on_dummy) + destination = /area/security/checkpoint/supply + dep_access = list(ACCESS_MAILSORTING, ACCESS_MINING, ACCESS_MINING_STATION, ACCESS_CARGO, ACCESS_AUX_BASE) + spawn_point = locate(/obj/effect/landmark/start/depsec/supply) in GLOB.department_security_spawns + minimal_lightup_areas |= GLOB.supply_lightup_areas if(SEC_DEPT_ENGINEERING) ears = /obj/item/radio/headset/headset_sec/alt/department/engi - dep_access = list(ACCESS_CONSTRUCTION, ACCESS_ENGINE, ACCESS_ATMOSPHERICS, ACCESS_AUX_BASE) - destination = /area/security/checkpoint/engineering - spawn_point = locate(/obj/effect/landmark/start/depsec/engineering) in GLOB.department_security_spawns accessory = /obj/item/clothing/accessory/armband/engine - minimal_lightup_areas |= GLOB.engineering_lightup_areas + if(!on_dummy) + dep_access = list(ACCESS_CONSTRUCTION, ACCESS_ENGINE, ACCESS_ATMOSPHERICS, ACCESS_AUX_BASE) + destination = /area/security/checkpoint/engineering + spawn_point = locate(/obj/effect/landmark/start/depsec/engineering) in GLOB.department_security_spawns + minimal_lightup_areas |= GLOB.engineering_lightup_areas if(SEC_DEPT_MEDICAL) ears = /obj/item/radio/headset/headset_sec/alt/department/med - dep_access = list(ACCESS_MEDICAL, ACCESS_MORGUE, ACCESS_SURGERY, ACCESS_CLONING) - destination = /area/security/checkpoint/medical - spawn_point = locate(/obj/effect/landmark/start/depsec/medical) in GLOB.department_security_spawns accessory = /obj/item/clothing/accessory/armband/medblue - minimal_lightup_areas |= GLOB.medical_lightup_areas + if(!on_dummy) + dep_access = list(ACCESS_MEDICAL, ACCESS_MORGUE, ACCESS_SURGERY, ACCESS_CLONING) + destination = /area/security/checkpoint/medical + spawn_point = locate(/obj/effect/landmark/start/depsec/medical) in GLOB.department_security_spawns + minimal_lightup_areas |= GLOB.medical_lightup_areas if(SEC_DEPT_SCIENCE) ears = /obj/item/radio/headset/headset_sec/alt/department/sci - dep_access = list(ACCESS_RESEARCH, ACCESS_TOX, ACCESS_AUX_BASE) - destination = /area/security/checkpoint/science - spawn_point = locate(/obj/effect/landmark/start/depsec/science) in GLOB.department_security_spawns accessory = /obj/item/clothing/accessory/armband/science - minimal_lightup_areas |= GLOB.science_lightup_areas + if(!on_dummy) + dep_access = list(ACCESS_RESEARCH, ACCESS_TOX, ACCESS_AUX_BASE) + destination = /area/security/checkpoint/science + spawn_point = locate(/obj/effect/landmark/start/depsec/science) in GLOB.department_security_spawns + minimal_lightup_areas |= GLOB.science_lightup_areas if(accessory) var/obj/item/clothing/under/U = H.w_uniform @@ -101,6 +108,9 @@ GLOBAL_LIST_INIT(available_depts, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, S var/obj/item/card/id/W = H.wear_id W.access |= dep_access + if(!M.client || on_dummy) + return + var/teleport = 0 if(!CONFIG_GET(flag/sec_start_brig)) if(destination || spawn_point) diff --git a/code/modules/jobs/job_types/shaft_miner.dm b/code/modules/jobs/job_types/shaft_miner.dm index 314445b7fc3c5..738462cbfa3f5 100644 --- a/code/modules/jobs/job_types/shaft_miner.dm +++ b/code/modules/jobs/job_types/shaft_miner.dm @@ -1,6 +1,9 @@ /datum/job/shaft_miner title = JOB_NAME_SHAFTMINER flag = MINER + description = "Collect resources for the station, redeem them for points, and purchase gear to collect even more ores." + department_for_prefs = DEPT_BITFLAG_CAR + department_head_for_prefs = JOB_NAME_QUARTERMASTER department_head = list(JOB_NAME_HEADOFPERSONNEL) supervisors = "the quartermaster and the head of personnel" faction = "Station" diff --git a/code/modules/jobs/job_types/station_engineer.dm b/code/modules/jobs/job_types/station_engineer.dm index 780141a65f928..14ef73e1bd243 100644 --- a/code/modules/jobs/job_types/station_engineer.dm +++ b/code/modules/jobs/job_types/station_engineer.dm @@ -1,6 +1,8 @@ /datum/job/station_engineer title = JOB_NAME_STATIONENGINEER flag = ENGINEER + description = "Ensure the station has an adequate power supply, repair and build new machinery, repair wiring chewed up by mice." + department_for_prefs = DEPT_BITFLAG_ENG department_head = list(JOB_NAME_CHIEFENGINEER) supervisors = "the chief engineer" faction = "Station" diff --git a/code/modules/jobs/job_types/virologist.dm b/code/modules/jobs/job_types/virologist.dm index 2c6c40b676c65..f08be796a457c 100644 --- a/code/modules/jobs/job_types/virologist.dm +++ b/code/modules/jobs/job_types/virologist.dm @@ -1,6 +1,8 @@ /datum/job/virologist title = JOB_NAME_VIROLOGIST flag = VIROLOGIST + description = "Collect virus samples from dormant viruses, old blood, and crusty vomit from around the station, isolate the symptoms and use them to create useful healing viruses for the crew." + department_for_prefs = DEPT_BITFLAG_MED department_head = list(JOB_NAME_CHIEFMEDICALOFFICER) supervisors = "the chief medical officer" faction = "Station" diff --git a/code/modules/jobs/job_types/warden.dm b/code/modules/jobs/job_types/warden.dm index ba1714aca57d5..7cca6e4821167 100644 --- a/code/modules/jobs/job_types/warden.dm +++ b/code/modules/jobs/job_types/warden.dm @@ -1,7 +1,9 @@ /datum/job/warden title = JOB_NAME_WARDEN flag = WARDEN - auto_deadmin_role_flags = PREFTOGGLE_DEADMIN_POSITION_SECURITY + description = "Oversee prisoners in the brig and guard the armory. Hand out equipment when necessary and ensure it is returned after threats have been contained." + department_for_prefs = DEPT_BITFLAG_SEC + auto_deadmin_role_flags = DEADMIN_POSITION_SECURITY department_head = list(JOB_NAME_HEADOFSECURITY) supervisors = "the head of security" faction = "Station" diff --git a/code/modules/keybindings/bindings_client.dm b/code/modules/keybindings/bindings_client.dm index 5041c5f864518..6bcf76703e21c 100644 --- a/code/modules/keybindings/bindings_client.dm +++ b/code/modules/keybindings/bindings_client.dm @@ -52,15 +52,18 @@ GLOBAL_LIST_INIT(valid_keys, list( // Client-level keybindings are ones anyone should be able to do at any time // Things like taking screenshots, hitting tab, and adminhelps. - var/AltMod = keys_held["Alt"] ? "Alt-" : "" - var/CtrlMod = keys_held["Ctrl"] ? "Ctrl-" : "" - var/ShiftMod = keys_held["Shift"] ? "Shift-" : "" - var/full_key = "[_key]" - if (!(_key in list("Alt", "Ctrl", "Shift"))) - full_key = "[AltMod][CtrlMod][ShiftMod][_key]" + var/AltMod = keys_held["Alt"] ? "Alt" : "" + var/CtrlMod = keys_held["Ctrl"] ? "Ctrl" : "" + var/ShiftMod = keys_held["Shift"] ? "Shift" : "" + var/full_key + switch(_key) + if("Alt", "Ctrl", "Shift") + full_key = "[AltMod][CtrlMod][ShiftMod]" + else + full_key = "[AltMod][CtrlMod][ShiftMod][_key]" var/list/kbs = list() - for (var/kb_name in prefs.key_bindings[full_key]) + for (var/kb_name in prefs.key_bindings_by_key[full_key]) var/datum/keybinding/kb = GLOB.keybindings_by_name[kb_name] kbs += kb // WASD-type movement keys (not the native arrow keys) are handled through the keybind system here. @@ -68,7 +71,7 @@ GLOBAL_LIST_INIT(valid_keys, list( // since these modifier keys toggle effects like "change facing" that require the movement keys to function. // Note that this doesn't prevent the user from binding CTRL-W to North: In that case *only* CTRL-W will function. if (full_key != _key) - 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.any_modifier) kbs += kb @@ -77,10 +80,8 @@ GLOBAL_LIST_INIT(valid_keys, list( if(kb.can_use(src) && kb.down(src)) break - if(holder) - holder.key_down(_key, src) //full_key is not necessary here, _key is enough - if(mob.focus) - mob.focus.key_down(_key, src) //same as above + holder?.key_down(_key, src) //full_key is not necessary here, _key is enough + mob.focus?.key_down(_key, src) //same as above /client/verb/keyUp(_key as text) set instant = TRUE @@ -97,7 +98,7 @@ GLOBAL_LIST_INIT(valid_keys, list( // 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 var/list/kbs = list() - 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] kbs += kb kbs = sort_list(kbs, GLOBAL_PROC_REF(cmp_keybinding_dsc)) @@ -105,7 +106,5 @@ GLOBAL_LIST_INIT(valid_keys, list( if(kb.can_use(src) && kb.up(src)) break - if(holder) - holder.key_up(_key, src) - if(mob.focus) - mob.focus.key_up(_key, src) + holder?.key_up(_key, src) + mob.focus?.key_up(_key, src) diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index 01a989265915e..c29ec3534375e 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -37,7 +37,7 @@ erase_all_macros() var/list/macro_sets = SSinput.macro_sets - var/use_tgui_say = !prefs || (prefs.toggles2 & PREFTOGGLE_2_TGUI_SAY) + var/use_tgui_say = !prefs || (prefs.read_player_preference(/datum/preference/toggle/tgui_say)) var/say = use_tgui_say ? tgui_say_create_open_command(SAY_CHANNEL) : "\".winset \\\"command=\\\".start_typing say\\\";command=.init_say;saywindow.is-visible=true;saywindow.input.focus=true\\\"\"" var/me = use_tgui_say ? tgui_say_create_open_command(ME_CHANNEL) : "\".winset \\\"command=\\\".start_typing me\\\";command=.init_me;mewindow.is-visible=true;mewindow.input.focus=true\\\"\"" var/ooc = use_tgui_say ? tgui_say_create_open_command(OOC_CHANNEL) : "ooc" @@ -61,7 +61,7 @@ winset(src, "[setname]-close-tgui-say", "parent=[setname];name=Escape;command=[tgui_say_create_close_command()]") - if(prefs.toggles2 & PREFTOGGLE_2_HOTKEYS) + if(hotkeys) winset(src, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED] mainwindow.macro=default") else winset(src, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED] mainwindow.macro=old_default") diff --git a/code/modules/language/language_holder.dm b/code/modules/language/language_holder.dm index 0d6a6cb191f95..daef70de00449 100644 --- a/code/modules/language/language_holder.dm +++ b/code/modules/language/language_holder.dm @@ -62,7 +62,9 @@ Key procs if(M.current) update_atom_languages(M.current) grant_language(/datum/language/metalanguage, understood=TRUE, spoken=FALSE, source=LANGUAGE_MIND) // Gets metalanguage that you can only understand - get_selected_language() + // If we have an owner, we'll set a default selected language + if(owner) + get_selected_language() /datum/language_holder/Destroy() QDEL_NULL(language_menu) diff --git a/code/modules/mining/equipment/regenerative_core.dm b/code/modules/mining/equipment/regenerative_core.dm index 2fb07c0fd390f..108dc6d6e6ac6 100644 --- a/code/modules/mining/equipment/regenerative_core.dm +++ b/code/modules/mining/equipment/regenerative_core.dm @@ -109,13 +109,13 @@ if(user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) applyto(user, user) -/obj/item/organ/regenerative_core/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/regenerative_core/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE, pref_load = FALSE) . = ..() if(!preserved && !inert) preserved(TRUE) owner.visible_message("[src] stabilizes as it's inserted.") -/obj/item/organ/regenerative_core/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/regenerative_core/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) if(!inert && !special) owner.visible_message("[src] rapidly decays as it's removed.") go_inert() diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 8cf5aa22b4fd7..71554f6644dd9 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -119,7 +119,10 @@ relevant_cap = max(hpc, epc) if(href_list["show_preferences"]) - client.prefs.ShowChoices(src) + var/datum/preferences/preferences = client.prefs + preferences.current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES + preferences.update_static_data(usr) + preferences.ui_interact(usr) return 1 if(href_list["ready"]) @@ -149,7 +152,7 @@ return if(SSticker.queued_players.len || (relevant_cap && living_player_count() >= relevant_cap)) - if(IS_PATRON(src.ckey) || (client in GLOB.admins)) + if(IS_PATRON(src.ckey) || is_admin(src.ckey)) LateChoices() return to_chat(usr, "[CONFIG_GET(string/hard_popcap_message)]") @@ -177,7 +180,7 @@ to_chat(usr, "There is an administrative lock on entering the game!") return - if(SSticker.queued_players.len && !(ckey(key) in GLOB.admin_datums) && !IS_PATRON(ckey(key))) + if(SSticker.queued_players.len && !is_admin(ckey(key)) && !IS_PATRON(ckey(key))) if((living_player_count() >= relevant_cap) || (src != SSticker.queued_players[1])) to_chat(usr, "Server is full.") return @@ -232,7 +235,7 @@ observer.client = client observer.set_ghost_appearance() if(observer.client && observer.client.prefs) - observer.real_name = observer.client.prefs.active_character.real_name + observer.real_name = observer.client.prefs.read_character_preference(/datum/preference/name/real_name) observer.name = observer.real_name observer.update_icon() observer.stop_sound_channel(CHANNEL_LOBBYMUSIC) @@ -438,22 +441,16 @@ 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(transfer_after) - spawning = 1 + spawning = TRUE close_spawn_windows() var/mob/living/carbon/human/H = new(loc) - var/frn = CONFIG_GET(flag/force_random_names) - if(!frn) - frn = is_banned_from(ckey, "Appearance") - if(QDELETED(src)) - return - if(frn) - client.prefs.active_character.randomise() - client.prefs.active_character.real_name = client.prefs.active_character.pref_species.random_name(gender,1) - client.prefs.active_character.copy_to(H) - H.dna.update_dna_identity() + H.apply_prefs_job(client, SSjob.GetJob(mind.assigned_role)) + if(QDELETED(src) || !client) + return // Disconnected while checking for the appearance ban. if(mind) if(transfer_after) mind.late_joiner = TRUE @@ -508,11 +505,11 @@ /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.active_character.joblessrole != RETURNTOLOBBY) + if(client.prefs.read_player_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 = (length(client.prefs.role_preferences) + length(client.prefs.active_character?.role_preferences_character)) > 0 - if(!length(client.prefs.active_character.job_preferences)) + var/has_antags = (length(client.prefs.role_preferences_global) + length(client.prefs.role_preferences)) > 0 + if(!length(client.prefs.job_preferences)) if(!ineligible_for_roles) to_chat(src, "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 diff --git a/code/modules/mob/dead/new_player/sprite_accessories.dm b/code/modules/mob/dead/new_player/sprite_accessories.dm index 2154149775e8a..25fee55276667 100644 --- a/code/modules/mob/dead/new_player/sprite_accessories.dm +++ b/code/modules/mob/dead/new_player/sprite_accessories.dm @@ -74,6 +74,17 @@ // try to spell // you do not need to define _s or _l sub-states, game automatically does this for you +/// Don't move these two, they go first +/datum/sprite_accessory/hair/bald + name = "Bald" + icon_state = null + +/datum/sprite_accessory/hair/bald2 + name = "Bald 2" + icon_state = "hair_bald2" + +// -------- + /datum/sprite_accessory/hair/afro name = "Afro" icon_state = "hair_afro" @@ -90,14 +101,6 @@ name = "Ahoge" icon_state = "hair_antenna" -/datum/sprite_accessory/hair/bald - name = "Bald" - icon_state = null - -/datum/sprite_accessory/hair/bald2 - name = "Bald 2" - icon_state = "hair_bald2" - /datum/sprite_accessory/hair/balding name = "Balding Hair" icon_state = "hair_e" @@ -907,6 +910,12 @@ // please make sure they're sorted alphabetically and categorized +/// This one goes first. Don't move it +/datum/sprite_accessory/facial_hair/shaved + name = "Shaved" + icon_state = null + gender = NEUTER + /datum/sprite_accessory/facial_hair/eyebrows name = "Eyebrows" icon_state = "facial_eyebrows" @@ -935,7 +944,6 @@ name = "Beard (Cropped Fullbeard)" icon_state = "facial_croppedfullbeard" - /datum/sprite_accessory/facial_hair/gt name = "Beard (Goatee)" icon_state = "facial_gt" @@ -1068,11 +1076,6 @@ name = "Sideburns" icon_state = "facial_sideburn" -/datum/sprite_accessory/facial_hair/shaved - name = "Shaved" - icon_state = null - gender = NEUTER - /////////////////////////// // Underwear Definitions // /////////////////////////// @@ -1848,19 +1851,21 @@ /datum/sprite_accessory/wings/apid name = "Bee" + icon = 'icons/mob/apid_accessories/apid_wings.dmi' icon_state = "apid" color_src = 0 - dimension_x = 46 + dimension_x = 32 center = TRUE - dimension_y = 34 + dimension_y = 32 /datum/sprite_accessory/wings_open/apid name = "Bee" + icon = 'icons/mob/apid_accessories/apid_wings.dmi' icon_state = "apid" color_src = 0 - dimension_x = 46 + dimension_x = 32 center = TRUE - dimension_y = 34 + dimension_y = 32 /datum/sprite_accessory/wings/robot name = "Robot" @@ -2401,7 +2406,7 @@ /datum/sprite_accessory/ipc_antennas/none name = "None" - icon_state = "None" + icon_state = "none" /datum/sprite_accessory/ipc_antennas/angled name = "Angled" @@ -2452,10 +2457,12 @@ /datum/sprite_accessory/insect_type/fly name = "Common Fly" limbs_id = "fly" + gender_specific = FALSE /datum/sprite_accessory/insect_type/bee name = "Hoverfly" limbs_id = "bee" + gender_specific = TRUE /datum/sprite_accessory/ipc_chassis/mcgreyscale name = "Morpheus Cyberkinetics (Custom)" diff --git a/code/modules/mob/dead/observer/login.dm b/code/modules/mob/dead/observer/login.dm index 5550a454d5c54..9bb48f0c46380 100644 --- a/code/modules/mob/dead/observer/login.dm +++ b/code/modules/mob/dead/observer/login.dm @@ -1,16 +1,16 @@ /mob/dead/observer/Login() ..() - ghost_accs = client.prefs.ghost_accs - ghost_others = client.prefs.ghost_others + ghost_accs = client.prefs.read_player_preference(/datum/preference/choiced/ghost_accessories) + ghost_others = client.prefs.read_player_preference(/datum/preference/choiced/ghost_others) var/preferred_form = null if(IsAdminGhost(src)) has_unlimited_silicon_privilege = 1 if(client.prefs.unlock_content) - preferred_form = client.prefs.ghost_form - ghost_orbit = client.prefs.ghost_orbit + preferred_form = client.prefs.read_player_preference(/datum/preference/choiced/ghost_form) + ghost_orbit = client.prefs.read_player_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 387fbfd837d8f..fdc5d2ef3d82b 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -200,9 +200,10 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_SPIRIT) */ /mob/dead/observer/update_icon(updates = ALL, new_form) . = ..() - if(client) //We update our preferences in case they changed right before update_icon was called. - ghost_accs = client.prefs.ghost_accs - ghost_others = client.prefs.ghost_others + + if(client) //We update our preferences in case they changed right before update_appearance was called. + ghost_accs = client.prefs.read_player_preference(/datum/preference/choiced/ghost_accessories) + ghost_others = client.prefs.read_player_preference(/datum/preference/choiced/ghost_others) if(hair_overlay) cut_overlay(hair_overlay) @@ -220,7 +221,7 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_SPIRIT) 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 @@ -397,8 +398,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_player_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 @@ -582,7 +584,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_player_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 @@ -610,11 +612,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_player_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) @@ -790,26 +792,30 @@ 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.active_character.real_name + var/real_name = client.prefs.read_character_preference(/datum/preference/name/real_name) + deadchat_name = real_name if(mind) - mind.ghostname = client.prefs.active_character.real_name - name = client.prefs.active_character.real_name + mind.ghostname = real_name + name = real_name /mob/dead/observer/proc/set_ghost_appearance() - if((!client) || (!client.prefs)) + if(!client?.prefs) return - if(client.prefs.active_character.be_random_name) - client.prefs.active_character.real_name = random_unique_name(gender) - if(client.prefs.active_character.be_random_body) - client.prefs.active_character.randomise(gender) - - if(HAIR in client.prefs.active_character.pref_species.species_traits) - hair_style = client.prefs.active_character.hair_style - hair_color = brighten_color(client.prefs.active_character.hair_color) - if(FACEHAIR in client.prefs.active_character.pref_species.species_traits) - facial_hair_style = client.prefs.active_character.facial_hair_style - facial_hair_color = brighten_color(client.prefs.active_character.facial_hair_color) + client.prefs.apply_character_randomization_prefs() + + var/species_type = client.prefs.read_character_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + if(HAIR in species.species_traits) + hair_style = client.prefs.read_character_preference(/datum/preference/choiced/hairstyle) + hair_color = brighten_color(client.prefs.read_character_preference(/datum/preference/color_legacy/hair_color)) + + if(FACEHAIR in species.species_traits) + facial_hair_style = client.prefs.read_character_preference(/datum/preference/choiced/facial_hairstyle) + facial_hair_color = brighten_color(client.prefs.read_character_preference(/datum/preference/color_legacy/facial_hair_color)) + + qdel(species) update_icon() diff --git a/code/modules/mob/living/brain/brain.dm b/code/modules/mob/living/brain/brain.dm index b99ad4137d193..3536574d11252 100644 --- a/code/modules/mob/living/brain/brain.dm +++ b/code/modules/mob/living/brain/brain.dm @@ -21,7 +21,7 @@ /mob/living/brain/proc/create_dna() stored_dna = new /datum/dna/stored(src) if(!stored_dna.species) - var/rando_race = 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/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm index 4a62877b93bd1..e9be3a75242ab 100644 --- a/code/modules/mob/living/brain/brain_item.dm +++ b/code/modules/mob/living/brain/brain_item.dm @@ -28,7 +28,7 @@ investigate_flags = ADMIN_INVESTIGATE_TARGET -/obj/item/organ/brain/Insert(mob/living/carbon/C, special = 0,no_id_transfer = FALSE) +/obj/item/organ/brain/Insert(mob/living/carbon/C, special = 0,no_id_transfer = FALSE, pref_load = FALSE) ..() name = "brain" @@ -59,7 +59,7 @@ //Update the body's icon so it doesnt appear debrained anymore C.update_hair() -/obj/item/organ/brain/Remove(mob/living/carbon/C, special = 0, no_id_transfer = FALSE) +/obj/item/organ/brain/Remove(mob/living/carbon/C, special = 0, no_id_transfer = FALSE, pref_load = FALSE) ..() for(var/X in traumas) var/datum/brain_trauma/BT = X diff --git a/code/modules/mob/living/carbon/alien/organs.dm b/code/modules/mob/living/carbon/alien/organs.dm index 3e1541526109c..12d13bd80e821 100644 --- a/code/modules/mob/living/carbon/alien/organs.dm +++ b/code/modules/mob/living/carbon/alien/organs.dm @@ -14,12 +14,12 @@ QDEL_LIST(alien_powers) return ..() -/obj/item/organ/alien/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/alien/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) . = ..() for(var/obj/effect/proc_holder/alien/P in alien_powers) M.AddAbility(P) -/obj/item/organ/alien/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/alien/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) for(var/obj/effect/proc_holder/alien/P in alien_powers) M.RemoveAbility(P) return ..() @@ -82,14 +82,14 @@ else owner.adjustPlasma(plasma_rate * 0.1) -/obj/item/organ/alien/plasmavessel/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/alien/plasmavessel/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) . = ..() if(!isalien(M)) return var/mob/living/carbon/alien/A = M A.updatePlasmaDisplay() -/obj/item/organ/alien/plasmavessel/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/alien/plasmavessel/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) . = ..() if(!isalien(M)) return @@ -107,12 +107,12 @@ alien_powers = list(/obj/effect/proc_holder/alien/whisper) var/recent_queen_death = 0 //Indicates if the queen died recently, aliens are heavily weakened while this is active. -/obj/item/organ/alien/hivenode/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/alien/hivenode/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) M.faction |= FACTION_ALIEN ADD_TRAIT(M, TRAIT_XENO_IMMUNE, "xeno immune") return ..() -/obj/item/organ/alien/hivenode/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/alien/hivenode/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) M.faction -= FACTION_ALIEN REMOVE_TRAIT(M, TRAIT_XENO_IMMUNE, "xeno immune") return ..() diff --git a/code/modules/mob/living/carbon/human/dummy.dm b/code/modules/mob/living/carbon/human/dummy.dm index 8d9d918de26a8..0ea12e3baea5f 100644 --- a/code/modules/mob/living/carbon/human/dummy.dm +++ b/code/modules/mob/living/carbon/human/dummy.dm @@ -7,6 +7,13 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy) +/mob/living/carbon/human/dummy/Initialize(mapload) + . = ..() + remove_from_all_data_huds() + +/mob/living/carbon/human/dummy/prepare_data_huds() + return + /mob/living/carbon/human/dummy/Destroy() in_use = FALSE return ..() @@ -17,12 +24,43 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy) /mob/living/carbon/human/dummy/proc/wipe_state() delete_equipment() cut_overlays() + // Wipe anything from custom icon appearances (AI/cyborg) + icon = initial(icon) + icon_state = initial(icon_state) /mob/living/carbon/human/dummy/setup_human_dna() create_dna(src) 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"] = GLOB.color_list_ethereal["Cyan"] + dna.features["frills"] = "None" + dna.features["horns"] = "None" + dna.features["mcolor"] = "4c4" + 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" + dna.features["apid_stripes"] = "thick" + dna.features["apid_headstripes"] = "thick" + dna.features["apid_antenna"] = "curled" + dna.features["insect_type"] = "fly" + dna.features["ipc_screen"] = "BSOD" + dna.features["ipc_antenna"] = "None" + dna.features["ipc_chassis"] = "Morpheus Cyberkinetics (Custom)" + dna.features["psyphoza_cap"] = "wide" + //Inefficient pooling/caching way. GLOBAL_LIST_EMPTY(human_dummy_list) GLOBAL_LIST_EMPTY(dummy_mob_list) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index bd7e140bf2297..b3bd799aa339c 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -2,7 +2,6 @@ name = "Unknown" real_name = "Unknown" icon = 'icons/mob/human.dmi' - icon_state = "human_basic" appearance_flags = KEEP_TOGETHER|TILE_BOUND|PIXEL_SCALE COOLDOWN_DECLARE(special_emote_cooldown) @@ -1101,24 +1100,6 @@ src.apply_damage(power, BRUTE, def_zone = pick(BODY_ZONE_PRECISE_R_FOOT, BODY_ZONE_PRECISE_L_FOOT)) src.Paralyze(10 * power) -/mob/living/carbon/human/proc/copy_features(var/datum/character_save/CS) - dna.features = CS.features - gender = CS.gender - age = CS.age - underwear = CS.underwear - underwear_color = CS.underwear_color - undershirt = CS.undershirt - socks = CS.socks - hair_style = CS.hair_style - hair_color = CS.hair_color - gradient_color = CS.gradient_color - gradient_style = CS.gradient_style - facial_hair_style = CS.facial_hair_style - facial_hair_color = CS.facial_hair_color - skin_tone = CS.skin_tone - eye_color = CS.eye_color - updateappearance(TRUE, TRUE, TRUE) - /mob/living/carbon/human/monkeybrain ai_controller = /datum/ai_controller/monkey diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm index f96afce2e4860..29912fdf64252 100644 --- a/code/modules/mob/living/carbon/human/human_helpers.dm +++ b/code/modules/mob/living/carbon/human/human_helpers.dm @@ -288,3 +288,23 @@ return TRUE if(isclothing(wear_mask) && (wear_mask.clothing_flags & SCAN_BOOZEPOWER)) return TRUE + +///copies over clothing preferences like underwear to another human +/mob/living/carbon/human/proc/copy_clothing_prefs(mob/living/carbon/human/destination) + destination.underwear = underwear + destination.underwear_color = underwear_color + destination.undershirt = undershirt + destination.socks = socks + destination.jumpsuit_style = jumpsuit_style + + +/// Fully randomizes everything according to the given flags. +/mob/living/carbon/human/proc/randomize_human_appearance(randomize_flags = ALL) + var/datum/preferences/preferences = new + + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if (!preference.included_in_randomization_flags(randomize_flags)) + continue + + if (preference.is_randomizable()) + preferences.write_preference(preference, preference.create_random_value(preferences)) diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm index f7ef72b87fc18..ca47163b6ba51 100644 --- a/code/modules/mob/living/carbon/human/species.dm +++ b/code/modules/mob/living/carbon/human/species.dm @@ -1,10 +1,17 @@ // This code handles different species in the game. GLOBAL_LIST_EMPTY(roundstart_races) +GLOBAL_LIST_EMPTY(accepatable_no_hard_check_races) + +/// An assoc list of species types to their features (from get_features()) +GLOBAL_LIST_EMPTY(features_by_species) /datum/species var/id // if the game needs to manually check your race to do something not included in a proc here, it will use this var/name // this is the fluff name. these will be left generic (such as 'Lizardperson' for the lizard race) so servers can change them to whatever + /// The formatting of the name of the species in plural context. Defaults to "[name]\s" if unset. + /// Ex "[Plasmamen] are weak", "[Mothmen] are strong", "[Lizardpeople] don't like", "[Golems] hate" + var/plural_form var/bodyflag = FLAG_HUMAN //Species flags currently used for species restriction on items var/default_color = "#FFF" // if alien colors are disabled, this is the color that will be used by that race var/bodytype = BODYTYPE_HUMANOID @@ -18,7 +25,8 @@ GLOBAL_LIST_EMPTY(roundstart_races) var/digitigrade_customization = DIGITIGRADE_NEVER //Never, Optional, or Forced digi legs? var/use_skintones = FALSE // does it use skintones or not? (spoiler alert this is only used by humans) - var/exotic_blood = "" // If your race wants to bleed something other than bog standard blood, change this to reagent id. + ///If your race bleeds something other than bog standard blood, change this to reagent id. For example, ethereals bleed liquid electricity. + var/datum/reagent/exotic_blood var/exotic_bloodtype = "" //If your race uses a non standard bloodtype (A+, O-, AB-, etc) var/meat = /obj/item/reagent_containers/food/snacks/meat/slab/human //What the species drops on gibbing var/skinned_type @@ -107,15 +115,67 @@ GLOBAL_LIST_EMPTY(roundstart_races) // PROCS // /////////// +/datum/species/New() + if(!plural_form) + plural_form = "[name]\s" + return ..() + +/// Gets a list of all species available to choose in roundstart. +/proc/get_selectable_species() + RETURN_TYPE(/list) + + if (!GLOB.roundstart_races.len) + GLOB.roundstart_races = generate_selectable_species() + + return GLOB.roundstart_races /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" + 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 += get_fallback_species_id() + + return selectable_species + +/proc/get_fallback_species_id() + var/fallback = CONFIG_GET(string/fallback_default_species) + var/id = fallback + if(fallback == "random") // absolute schizoposting + if(length(GLOB.roundstart_races)) + id = pick(GLOB.roundstart_races) + else + var/datum/species/type = pick(subtypesof(/datum/species)) + id = initial(type.id) + return id + +/// Gets a list of species that are allowed to be used from the DB even if they are disabled due to roundstart_no_hard_check +/// Use get_selectable_species() for new/editing characters. +/proc/get_acceptable_species() + RETURN_TYPE(/list) + + if (!GLOB.accepatable_no_hard_check_races.len) + GLOB.accepatable_no_hard_check_races = generate_acceptable_species() + + return GLOB.accepatable_no_hard_check_races + +/proc/generate_acceptable_species() + var/list/base = get_selectable_species() // normally allowed species. + var/list/no_hard_check = CONFIG_GET(keyed_list/roundstart_no_hard_check) + no_hard_check = no_hard_check.Copy() + for(var/species_id in no_hard_check) + if(!GLOB.species_list[species_id]) + continue + base += species_id + no_hard_check -= species_id + for(var/species_id in no_hard_check) // warn any invalid species in the config. + stack_trace("WARNING: roundstart_no_hard_check contains invalid species ID: [species_id]") + return base /datum/species/proc/check_roundstart_eligible() if(id in (CONFIG_GET(keyed_list/roundstart_races))) @@ -367,7 +427,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) // async due to prefs UI calling this and using SHOULD_NOT_SLEEP if(NOMOUTH in species_traits) for(var/obj/item/bodypart/head/head in C.bodyparts) @@ -2227,6 +2287,467 @@ GLOBAL_LIST_EMPTY(roundstart_races) /datum/species/proc/get_huff_sound(mob/living/carbon/user) return -//generic action proc for keybind stuff +/// Returns a list of strings representing features this species has. +/// Used by the preferences UI to know what buttons to show. +/// Should only need to override if the feature is not attached to a mutant bodypart or trait +/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.db_key + + GLOB.features_by_species[type] = features + + return features + +/// Given a human, will adjust it before taking a picture for the preferences UI. +/// This should create a CONSISTENT result, so the icons don't randomly change. +/datum/species/proc/prepare_human_for_preview(mob/living/carbon/human/human) + return + +/** + * Gets a short description for the specices. Should be relatively succinct. + * Used in the preference menu. + * + * Returns a string. + */ +/datum/species/proc/get_species_description() + SHOULD_CALL_PARENT(FALSE) + + stack_trace("Species [name] ([type]) did not have a description set, and is a selectable roundstart race! Override get_species_description.") + return "No species description set, file a bug report!" + +/** + * Gets the lore behind the type of species. Can be long. + * Used in the preference menu. + * + * Returns a list of strings. + * Between each entry in the list, a newline will be inserted, for formatting. + */ +/datum/species/proc/get_species_lore() + SHOULD_CALL_PARENT(FALSE) + RETURN_TYPE(/list) + + stack_trace("Species [name] ([type]) did not have lore set, and is a selectable roundstart race! Override get_species_lore.") + return list("No species lore set, file a bug report!") + +/** + * Translate the species liked foods from bitfields into strings + * and returns it in the form of an associated list. + * + * Returns a list, or null if they have no diet. + */ +/datum/species/proc/get_species_diet() + if(TRAIT_NOHUNGER in inherent_traits) + return null + + var/list/food_flags = FOOD_FLAGS + + return list( + "liked_food" = bitfield_to_list(initial(mutanttongue.liked_food), food_flags), + "disliked_food" = bitfield_to_list(initial(mutanttongue.disliked_food), food_flags), + "toxic_food" = bitfield_to_list(initial(mutanttongue.toxic_food), food_flags), + ) + +/** + * Generates a list of "perks" related to this species + * (Postives, neutrals, and negatives) + * in the format of a list of lists. + * Used in the preference menu. + * + * "Perk" format is as followed: + * list( + * SPECIES_PERK_TYPE = type of perk (postiive, negative, neutral - use the defines) + * SPECIES_PERK_ICON = icon shown within the UI + * SPECIES_PERK_NAME = name of the perk on hover + * SPECIES_PERK_DESC = description of the perk on hover + * ) + * + * Returns a list of lists. + * The outer list is an assoc list of [perk type]s to a list of perks. + * The innter list is a list of perks. Can be empty, but won't be null. + */ +/datum/species/proc/get_species_perks() + var/list/species_perks = list() + + // Let us get every perk we can concieve of in one big list. + // The order these are called (kind of) matters. + // Species unique perks first, as they're more important than genetic perks, + // and language perk last, as it comes at the end of the perks list + species_perks += create_pref_unique_perks() + species_perks += create_pref_blood_perks() + species_perks += create_pref_combat_perks() + species_perks += create_pref_damage_perks() + species_perks += create_pref_temperature_perks() + species_perks += create_pref_traits_perks() + species_perks += create_pref_biotypes_perks() + species_perks += create_pref_language_perk() + + // Some overrides may return `null`, prevent those from jamming up the list. + list_clear_nulls(species_perks) + + // Now let's sort them out for cleanliness and sanity + var/list/perks_to_return = list( + SPECIES_POSITIVE_PERK = list(), + SPECIES_NEUTRAL_PERK = list(), + SPECIES_NEGATIVE_PERK = list(), + ) + + for(var/list/perk as anything in species_perks) + var/perk_type = perk[SPECIES_PERK_TYPE] + // If we find a perk that isn't postiive, negative, or neutral, + // it's a bad entry - don't add it to our list. Throw a stack trace and skip it instead. + if(isnull(perks_to_return[perk_type])) + stack_trace("Invalid species perk ([perk[SPECIES_PERK_NAME]]) found for species [name]. \ + The type should be positive, negative, or neutral. (Got: [perk_type])") + continue + + perks_to_return[perk_type] += list(perk) + + return perks_to_return + +/** + * Used to add any species specific perks to the perk list. + * + * Returns null by default. When overriding, return a list of perks. + */ +/datum/species/proc/create_pref_unique_perks() + return null + +/** + * Adds adds any perks related to combat. + * For example, the damage type of their punches. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_combat_perks() + var/list/to_add = list() + + if(attack_type != BRUTE) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "fist-raised", + SPECIES_PERK_NAME = "Elemental Attacker", + SPECIES_PERK_DESC = "[plural_form] deal [attack_type] damage with their punches instead of brute.", + )) + + return to_add + +/** + * Adds adds any perks related to sustaining damage. + * For example, brute damage vulnerability, or fire damage resistance. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_damage_perks() + var/list/to_add = list() + + // Brute related + if(brutemod > 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "band-aid", + SPECIES_PERK_NAME = "Brutal Weakness", + SPECIES_PERK_DESC = "[plural_form] are weak to brute damage.", + )) + else if(brutemod < 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "shield-alt", + SPECIES_PERK_NAME = "Brutal Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to bruising and brute damage.", + )) + + // Burn related + if(burnmod > 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "burn", + SPECIES_PERK_NAME = "Fire Weakness", + SPECIES_PERK_DESC = "[plural_form] are weak to fire and burn damage.", + )) + else if(burnmod < 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "shield-alt", + SPECIES_PERK_NAME = "Fire Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to flames, and burn damage.", + )) + + if(TRAIT_SHOCKIMMUNE in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bolt", + SPECIES_PERK_NAME = "Shock Immune", + SPECIES_PERK_DESC = "[plural_form] are entirely resistant to electrical shocks.", + )) + else if(siemens_coeff > 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "bolt", + SPECIES_PERK_NAME = "Shock Vulnerability", + SPECIES_PERK_DESC = "[plural_form] are vulnerable to being shocked.", + )) + else if(siemens_coeff < 1) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "shield-alt", + SPECIES_PERK_NAME = "Shock Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to being shocked.", + )) + + return to_add + +/** + * Adds adds any perks related to how the species deals with temperature. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_temperature_perks() + var/list/to_add = list() + + // Hot temperature tolerance + if(heatmod > 1/* || bodytemp_heat_damage_limit < BODYTEMP_HEAT_DAMAGE_LIMIT*/) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "temperature-high", + SPECIES_PERK_NAME = "Heat Vulnerability", + SPECIES_PERK_DESC = "[plural_form] are vulnerable to high temperatures.", + )) + + if(heatmod < 1/* || bodytemp_heat_damage_limit > BODYTEMP_HEAT_DAMAGE_LIMIT*/) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "thermometer-empty", + SPECIES_PERK_NAME = "Heat Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to hotter environments.", + )) + + // Cold temperature tolerance + if(coldmod > 1/* || bodytemp_cold_damage_limit > BODYTEMP_COLD_DAMAGE_LIMIT*/) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "temperature-low", + SPECIES_PERK_NAME = "Cold Vulnerability", + SPECIES_PERK_DESC = "[plural_form] are vulnerable to cold temperatures.", + )) + + if(coldmod < 1/* || bodytemp_cold_damage_limit < BODYTEMP_COLD_DAMAGE_LIMIT*/) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "thermometer-empty", + SPECIES_PERK_NAME = "Cold Resilience", + SPECIES_PERK_DESC = "[plural_form] are resilient to colder environments.", + )) + + return to_add + +/** + * Adds adds any perks related to the species' blood (or lack thereof). + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_blood_perks() + var/list/to_add = list() + + // NOBLOOD takes priority by default + if(NOBLOOD in species_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "tint-slash", + SPECIES_PERK_NAME = "Bloodletted", + SPECIES_PERK_DESC = "[plural_form] do not have blood.", + )) + + // Otherwise, check if their exotic blood is a valid typepath + else if(ispath(exotic_blood)) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "tint", + SPECIES_PERK_NAME = initial(exotic_blood.name), + SPECIES_PERK_DESC = "[name] blood is [initial(exotic_blood.name)], which can make recieving medical treatment harder.", + )) + + // Otherwise otherwise, see if they have an exotic bloodtype set + else if(exotic_bloodtype) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "tint", + SPECIES_PERK_NAME = "Exotic Blood", + SPECIES_PERK_DESC = "[plural_form] have \"[exotic_bloodtype]\" type blood, which can make recieving medical treatment harder.", + )) + + return to_add + +/** + * Adds adds any perks related to the species' inherent_traits list. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_traits_perks() + var/list/to_add = list() + + if(TRAIT_LIMBATTACHMENT in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "user-plus", + SPECIES_PERK_NAME = "Limbs Easily Reattached", + SPECIES_PERK_DESC = "[plural_form] limbs are easily reattached, and as such do not \ + require surgery to restore. Simply pick it up and pop it back in, champ!", + )) + + if(TRAIT_EASYDISMEMBER in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "user-times", + SPECIES_PERK_NAME = "Limbs Easily Dismembered", + SPECIES_PERK_DESC = "[plural_form] limbs are not secured well, and as such they are easily dismembered.", + )) + + if(TRAIT_NODISMEMBER in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "user-shield", + SPECIES_PERK_NAME = "Well-Attached Limbs", + SPECIES_PERK_DESC = "[plural_form] cannot be dismembered.", + )) + + if(TRAIT_TOXINLOVER in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "syringe", + SPECIES_PERK_NAME = "Toxins Lover", + SPECIES_PERK_DESC = "Toxins damage dealt to [plural_form] are reversed - healing toxins will instead cause harm, and \ + causing toxins will instead cause healing. Be careful around purging chemicals!", + )) + + if(TRAIT_NOFIRE in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "fire-extinguisher", + SPECIES_PERK_NAME = "Fireproof", + SPECIES_PERK_DESC = "[plural_form] are entirely immune to catching fire.", + )) + + if(TRAIT_NOHUNGER in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "utensils", + SPECIES_PERK_NAME = "No Hunger", + SPECIES_PERK_DESC = "[plural_form] are never hungry.", + )) + + if(TRAIT_RADIMMUNE in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "radiation", + SPECIES_PERK_NAME = "Radiation Immune", + SPECIES_PERK_DESC = "[plural_form] are entirely immune to radiation.", + )) + + if(TRAIT_RESISTHIGHPRESSURE in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "wind", + SPECIES_PERK_NAME = "High-Pressure Resistance", + SPECIES_PERK_DESC = "[plural_form] are resistant to high atmospheric pressures.", + )) + + if(TRAIT_RESISTLOWPRESSURE in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "level-down-alt", + SPECIES_PERK_NAME = "Low-Pressure Resistance", + SPECIES_PERK_DESC = "[plural_form] are resistant to low atmospheric pressures.", + )) + + if(TRAIT_TOXIMMUNE in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "biohazard", + SPECIES_PERK_NAME = "Toxin Immune", + SPECIES_PERK_DESC = "[plural_form] are immune to toxin damage.", + )) + + if(TRAIT_PIERCEIMMUNE in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "syringe", + SPECIES_PERK_NAME = "Tough Skin", + SPECIES_PERK_DESC = "[plural_form] have tough skin, blocking piercing and embedding of sharp objects, including needles.", + )) + + if(TRAIT_POWERHUNGRY in inherent_traits) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bolt", + SPECIES_PERK_NAME = "Shockingly Tasty", + SPECIES_PERK_DESC = "Ethereals can feed on electricity from APCs, powercells, and lights; and do not otherwise need to eat.", + )) + + return to_add + +/** + * Adds adds any perks related to the species' inherent_biotypes flags. + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_biotypes_perks() + var/list/to_add = list() + + if(MOB_UNDEAD in inherent_biotypes) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "skull", + SPECIES_PERK_NAME = "Undead", + SPECIES_PERK_DESC = "[plural_form] are of the undead! The undead do not have the need to eat or breathe, and \ + most viruses will not be able to infect a walking corpse. Their worries mostly stop at remaining in one piece, really.", + )) + + return to_add + +/** + * Adds in a language perk based on all the languages the species + * can speak by default (according to their language holder). + * + * Returns a list containing perks, or an empty list. + */ +/datum/species/proc/create_pref_language_perk() + var/list/to_add = list() + + // Grab galactic common as a path, for comparisons + var/datum/language/common_language = /datum/language/common + + // Now let's find all the languages they can speak that aren't common + var/list/bonus_languages = list() + var/datum/language_holder/temp_holder = new species_language_holder() + for(var/datum/language/language_type as anything in temp_holder.spoken_languages) + if(ispath(language_type, common_language)) + continue + bonus_languages += initial(language_type.name) + + // If we have any languages we can speak: create a perk for them all + if(length(bonus_languages)) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "comment", + SPECIES_PERK_NAME = "Native Speaker", + SPECIES_PERK_DESC = "Alongside [initial(common_language.name)], [plural_form] gain the ability to speak [english_list(bonus_languages)].", + )) + + qdel(temp_holder) + + return to_add + /datum/species/proc/primary_species_action() return diff --git a/code/modules/mob/living/carbon/human/species_types/IPC.dm b/code/modules/mob/living/carbon/human/species_types/IPC.dm index 38c426920d0b3..726367170abca 100644 --- a/code/modules/mob/living/carbon/human/species_types/IPC.dm +++ b/code/modules/mob/living/carbon/human/species_types/IPC.dm @@ -1,5 +1,6 @@ /datum/species/ipc name = "\improper Integrated Positronic Chassis" + plural_form = "IPCs" id = SPECIES_IPC bodyflag = FLAG_IPC sexes = FALSE @@ -15,7 +16,7 @@ mutant_heart = /obj/item/organ/heart/cybernetic/ipc mutant_organs = list(/obj/item/organ/cyberimp/arm/power_cord) mutant_bodyparts = list("ipc_screen", "ipc_antenna", "ipc_chassis") - default_features = list("mcolor" = "#7D7D7D", "ipc_screen" = "Static", "ipc_antenna" = "None", "ipc_chassis" = "Morpheus Cyberkinetics(Greyscale)") + default_features = list("mcolor" = "#7D7D7D", "ipc_screen" = "Static", "ipc_antenna" = "None", "ipc_chassis" = "Morpheus Cyberkinetics (Custom)") meat = /obj/item/stack/sheet/plasteel{amount = 5} skinned_type = /obj/item/stack/sheet/iron{amount = 10} exotic_blood = /datum/reagent/oil @@ -253,3 +254,31 @@ BP.limb_id = chassis_of_choice.limbs_id BP.name = "\improper[chassis_of_choice.name] [parse_zone(BP.body_zone)]" BP.update_limb() + +/datum/species/ipc/get_species_description() + return "The newest in artificial life, IPCs are entirely robotic, synthetic life, made of motors, circuits, and wires \ + - based on newly developed Postronic brain technology." + +/datum/species/ipc/get_species_lore() + return null + +/datum/species/ipc/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "robot", + SPECIES_PERK_NAME = "Robotic", + SPECIES_PERK_DESC = "IPCs have an entirely robotic body, meaning medical care is typically done through Robotics or Engineering. \ + Whether this is helpful or not is heavily dependent on your coworkers. It does, however, mean you are usually able to perform self-repairs easily.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "magnet", + SPECIES_PERK_NAME = "EMP Vulnerable", + SPECIES_PERK_DESC = "IPC organs are cybernetic, and thus susceptible to electromagnetic interference. Getting hit by an EMP may stop your heart.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/abductors.dm b/code/modules/mob/living/carbon/human/species_types/abductors.dm index c56b0fec84240..7191c9bb0aa33 100644 --- a/code/modules/mob/living/carbon/human/species_types/abductors.dm +++ b/code/modules/mob/living/carbon/human/species_types/abductors.dm @@ -23,3 +23,23 @@ . = ..() var/datum/atom_hud/abductor_hud = GLOB.huds[DATA_HUD_ABDUCTOR] abductor_hud.remove_hud_from(C) + +/datum/species/abductor/get_species_description() + return "Silent, but deadly. It's not known where they really come from, but they seem to have shown up regardless." + +/datum/species/abductor/get_species_lore() + return null + +/datum/species/abductor/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "volume-mute", + SPECIES_PERK_NAME = "Mute", + SPECIES_PERK_DESC = "Abductors can't speak. At all. This may upset your coworkers.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/apid.dm b/code/modules/mob/living/carbon/human/species_types/apid.dm index 8cdc4e4422f6b..b32c86fdb14ab 100644 --- a/code/modules/mob/living/carbon/human/species_types/apid.dm +++ b/code/modules/mob/living/carbon/human/species_types/apid.dm @@ -87,3 +87,47 @@ /datum/species/apid/on_species_loss(mob/living/carbon/human/C, datum/species/new_species, pref_load) C.mind?.forget_crafting_recipe(/datum/crafting_recipe/honeycomb) return ..() + +/datum/species/apid/get_species_description() + return "Beepeople, god damn it. It's hip, and alive! Buzz buzz!" + +/datum/species/apid/get_species_lore() + return null + +/datum/species/apid/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bug", + SPECIES_PERK_NAME = "Hive-Friend", + SPECIES_PERK_DESC = "Apids are naturally friends with bees, and can make honeycombs!", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "level-down-alt", + SPECIES_PERK_NAME = "Low Air Requirements", + SPECIES_PERK_DESC = "Apids can breathe in lower air pressures just fine!", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "wind", + SPECIES_PERK_NAME = "Dashing!", + SPECIES_PERK_DESC = "Apids can use their wings to quickly dash forward in a flurry of buzzing!", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "icicles", + SPECIES_PERK_NAME = "Cold-Sensitive Biology", + SPECIES_PERK_DESC = "The cold makes Apids sleepy, as does smoke...", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "fist-raised", + SPECIES_PERK_NAME = "Insectoid Biology", + SPECIES_PERK_DESC = "Fly swatters will deal significantly higher amounts of damage to Apids.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/dullahan.dm b/code/modules/mob/living/carbon/human/species_types/dullahan.dm index 15a73a94ca579..e7222cbe951ef 100644 --- a/code/modules/mob/living/carbon/human/species_types/dullahan.dm +++ b/code/modules/mob/living/carbon/human/species_types/dullahan.dm @@ -21,7 +21,7 @@ /datum/species/dullahan/check_roundstart_eligible() if(SSevents.holidays && SSevents.holidays[HALLOWEEN]) return TRUE - return FALSE + return ..() /datum/species/dullahan/on_species_gain(mob/living/carbon/human/H, datum/species/old_species) . = ..() @@ -62,6 +62,49 @@ else H.reset_perspective(myhead) + +/datum/species/dullahan/get_species_description() + return "An angry spirit, hanging onto the land of the living for \ + unfinished business. Or that's what the books say. They're quite nice \ + when you get to know them." + +/datum/species/dullahan/get_species_lore() + return list( + "\"No wonder they're all so grumpy! Their hands are always full! I used to think, \ + \"Wouldn't this be cool?\" but after watching these creatures suffer from their head \ + getting dunked down disposals for the nth time, I think I'm good.\" - Captain Larry Dodd" + ) + +/datum/species/dullahan/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "horse-head", + SPECIES_PERK_NAME = "Headless and Horseless", + SPECIES_PERK_DESC = "Dullahans must lug their head around in their arms. While \ + many creative uses can come out of your head being independent of your \ + body, Dullahans will find it mostly a pain.", + )) + + return to_add + +// There isn't a "Minor Undead" biotype, so we have to explain it in an override (see: vampires) +/datum/species/dullahan/create_pref_biotypes_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "skull", + SPECIES_PERK_NAME = "Minor Undead", + SPECIES_PERK_DESC = "[name] are minor undead. \ + Minor undead enjoy some of the perks of being dead, like \ + not needing to breathe or eat, but do not get many of the \ + environmental immunities involved with being fully undead.", + )) + + return to_add + /obj/item/organ/brain/dullahan decoy_override = TRUE organ_flags = 0 diff --git a/code/modules/mob/living/carbon/human/species_types/ethereal.dm b/code/modules/mob/living/carbon/human/species_types/ethereal.dm index 113554872b820..a261fab61f0e9 100644 --- a/code/modules/mob/living/carbon/human/species_types/ethereal.dm +++ b/code/modules/mob/living/carbon/human/species_types/ethereal.dm @@ -184,3 +184,36 @@ /datum/species/ethereal/get_sniff_sound(mob/living/carbon/user) return SPECIES_DEFAULT_SNIFF_SOUND(user) + +/datum/species/ethereal/get_features() + var/list/features = ..() + + features += "feature_ethcolor" + + return features + +/datum/species/ethereal/get_species_description() + return "Ethereals are a unique species with liquid electricity for blood and a glowing body. They thrive on electricity, and are naturally agender." + +/datum/species/ethereal/get_species_lore() + return null + +/datum/species/ethereal/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "lightbulb", + SPECIES_PERK_NAME = "Disco Ball", + SPECIES_PERK_DESC = "Ethereals passively generate their own light.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "biohazard", + SPECIES_PERK_NAME = "Starving Artist", + SPECIES_PERK_DESC = "Ethereals take toxin damage while starving.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/felinid.dm b/code/modules/mob/living/carbon/human/species_types/felinid.dm index 64b38f6a978bc..5b0dcf55c7453 100644 --- a/code/modules/mob/living/carbon/human/species_types/felinid.dm +++ b/code/modules/mob/living/carbon/human/species_types/felinid.dm @@ -40,12 +40,12 @@ H.dna.features["ears"] = "Cat" if(H.dna.features["ears"] == "Cat") var/obj/item/organ/ears/cat/ears = new - ears.Insert(H, drop_if_replaced = FALSE) + ears.Insert(H, drop_if_replaced = FALSE, pref_load = pref_load) else mutantears = /obj/item/organ/ears if(H.dna.features["tail_human"] == "Cat") var/obj/item/organ/tail/cat/tail = new - tail.Insert(H, drop_if_replaced = FALSE) + tail.Insert(H, drop_if_replaced = FALSE, pref_load = pref_load) else mutanttail = null return ..() @@ -64,7 +64,7 @@ if(!new_ears) // Go with default ears new_ears = new /obj/item/organ/ears - new_ears.Insert(H, drop_if_replaced = FALSE) + new_ears.Insert(H, drop_if_replaced = FALSE, pref_load = pref_load) if(tail) var/obj/item/organ/tail/new_tail @@ -74,9 +74,9 @@ if(new_species.mutanttail) new_tail = new new_species.mutanttail if(new_tail) - new_tail.Insert(H, drop_if_replaced = FALSE) + new_tail.Insert(H, drop_if_replaced = FALSE, pref_load = pref_load) else - tail.Remove(H) + tail.Remove(H, pref_load = pref_load) /datum/species/human/felinid/handle_chemicals(datum/reagent/chem, mob/living/carbon/human/M) if(istype(chem, /datum/reagent/consumable/cocoa)) @@ -171,3 +171,60 @@ if(!silent) to_chat(H, "You are no longer a cat.") + +/datum/species/human/felinid/prepare_human_for_preview(mob/living/carbon/human/human) + human.hair_style = "Hime Cut" + human.hair_color = "fcc" // pink + human.update_hair() + + var/obj/item/organ/ears/cat/cat_ears = human.getorgan(/obj/item/organ/ears/cat) + if (cat_ears) + cat_ears.color = human.hair_color + human.update_body() + +/datum/species/human/felinid/get_species_description() + return "Felinids are one of the many types of bespoke genetic \ + modifications to come of humanity's mastery of genetic science, and are \ + also one of the most common. Meow?" + +/datum/species/human/felinid/get_species_lore() + return list( + "Bio-engineering at its felinest, Felinids are the peak example of humanity's mastery of genetic code. \ + One of many \"Animalid\" variants, Felinids are the most popular and common, as well as one of the \ + biggest points of contention in genetic-modification.", + + "Body modders were eager to splice human and feline DNA in search of the holy trifecta: ears, eyes, and tail. \ + These traits were in high demand, with the corresponding side effects of vocal and neurochemical changes being seen as a minor inconvenience.", + + "Sadly for the Felinids, they were not minor inconveniences. Shunned as subhuman and monstrous by many, Felinids (and other Animalids) \ + sought their greener pastures out in the colonies, cloistering in communities of their own kind. \ + As a result, outer Human space has a high Animalid population.", + ) + +// Felinids are subtypes of humans. +// This shouldn't call parent or we'll get a buncha human related perks (though it doesn't have a reason to). +/datum/species/human/felinid/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "angle-double-down", + SPECIES_PERK_NAME = "Always Land On Your Feet", + SPECIES_PERK_DESC = "Felinids always land on their feet, and take reduced damage from falling.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "shoe-prints", + SPECIES_PERK_NAME = "Laser Affinity", + SPECIES_PERK_DESC = "Felinids can't resist the temptation of a good laser pointer, and might involuntarily chase a strong one.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "swimming-pool", + SPECIES_PERK_NAME = "Hydrophobia", + SPECIES_PERK_DESC = "Felinids don't like water, and hate going in the pool.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/flypeople.dm b/code/modules/mob/living/carbon/human/species_types/flypeople.dm index 4b3418d57f727..65bca74b4973b 100644 --- a/code/modules/mob/living/carbon/human/species_types/flypeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/flypeople.dm @@ -1,5 +1,6 @@ /datum/species/fly name = "\improper Flyperson" + plural_form = "Flypeople" id = SPECIES_FLY bodyflag = FLAG_FLY species_traits = list(NOEYESPRITES, NO_UNDERWEAR, TRAIT_BEEFRIEND) @@ -9,7 +10,7 @@ mutantstomach = /obj/item/organ/stomach/fly meat = /obj/item/reagent_containers/food/snacks/meat/slab/human/mutant/fly mutant_bodyparts = list("insect_type") - default_features = list("insect_type" = "housefly", "body_size" = "Normal") + default_features = list("insect_type" = "fly", "body_size" = "Normal") burnmod = 1.4 brutemod = 1.4 speedmod = 0.7 @@ -39,7 +40,67 @@ return TRUE return ..() +/datum/species/fly/replace_body(mob/living/carbon/C, datum/species/new_species) + ..() + + var/datum/sprite_accessory/insect_type/type_selection = GLOB.insect_type_list[C.dna.features["insect_type"]] + if(!istype(type_selection)) + return + + for(var/obj/item/bodypart/BP as() in C.bodyparts) //Override bodypart data as necessary + BP.uses_mutcolor = !!type_selection.color_src + if(BP.uses_mutcolor) + BP.should_draw_greyscale = TRUE + BP.species_color = C.dna?.features["mcolor"] + // Hardcoded bullshit that will probably break. Woo shitcode. Bee insect_type has dimorphic parts while flies do not. + BP.is_dimorphic = type_selection.gender_specific && (istype(BP, /obj/item/bodypart/head) || istype(BP, /obj/item/bodypart/chest)) + + BP.limb_id = type_selection.limbs_id + BP.name = "\improper[type_selection.name] [parse_zone(BP.body_zone)]" + BP.update_limb() + /datum/species/fly/check_species_weakness(obj/item/weapon, mob/living/attacker) if(istype(weapon, /obj/item/melee/flyswatter)) return 29 //Flyswatters deal 30x damage to flypeople. return 0 + +/datum/species/fly/get_species_description() + return "With no official documentation or knowledge of the origin of \ + this species, they remain a mystery to most. Any and all rumours among \ + Nanotrasen staff regarding flypeople are often quickly silenced by high \ + ranking staff or officials." + +/datum/species/fly/get_species_lore() + return list( + "Flypeople are a curious species with a striking resemblance to the insect order of Diptera, \ + commonly known as flies. With no publically known origin, flypeople are rumored to be a side effect of bluespace travel, \ + despite statements from Nanotrasen officials.", + + "Little is known about the origins of this race, \ + however they posess the ability to communicate with giant spiders, originally discovered in the Australicus sector \ + and now a common occurence in black markets as a result of a breakthrough in syndicate bioweapon research.", + + "Flypeople are often feared or avoided among other species, their appearance often described as unclean or frightening in some cases, \ + and their eating habits even more so with an insufferable accent to top it off.", + ) + +/datum/species/fly/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "grin-tongue", + SPECIES_PERK_NAME = "Uncanny Digestive System", + SPECIES_PERK_DESC = "Flypeople regurgitate their stomach contents and drink it \ + off the floor to eat and drink with little care for taste, favoring gross foods.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "fist-raised", + SPECIES_PERK_NAME = "Insectoid Biology", + SPECIES_PERK_DESC = "Fly swatters will deal significantly higher amounts of damage to a Flyperson.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/golems.dm b/code/modules/mob/living/carbon/human/species_types/golems.dm index 59d0aa2c1cd97..3ea22e48eeab2 100644 --- a/code/modules/mob/living/carbon/human/species_types/golems.dm +++ b/code/modules/mob/living/carbon/human/species_types/golems.dm @@ -50,6 +50,21 @@ var/golem_name = "[prefix] [golem_surname]" return golem_name +/datum/species/golem/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "gem", + SPECIES_PERK_NAME = "Lithoid", + SPECIES_PERK_DESC = "Lithoids are creatures made out of elements instead of \ + blood and flesh. Because of this, they're generally stronger, slower, \ + and mostly immune to environmental dangers and dangers to their health, \ + such as viruses and dismemberment.", + )) + + return to_add + /datum/species/golem/random name = "Random golem" changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC | RACE_SWAP | ERT_SPAWN @@ -790,6 +805,48 @@ new /obj/structure/cloth_pile(get_turf(H), H) ..() +/datum/species/golem/cloth/get_species_description() + return "A wrapped up Mummy! They descend upon Space Station Thirteen every year to spook the crew! \"Return the slab!\"" + +/datum/species/golem/cloth/get_species_lore() + return list( + "Mummies are very self conscious. They're shaped weird, they walk slow, and worst of all, \ + they're considered the laziest halloween costume. But that's not even true, they say.", + + "Making a mummy costume may be easy, but making a CONVINCING mummy costume requires \ + things like proper fabric and purposeful staining to achieve the look. Which is FAR from easy. Gosh.", + ) + +// Calls parent, as Golems have a species-wide perk we care about. +/datum/species/golem/cloth/create_pref_unique_perks() + var/list/to_add = ..() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "recycle", + SPECIES_PERK_NAME = "Reformation", + SPECIES_PERK_DESC = "A boon quite similar to Ethereals, Mummies collapse into \ + a pile of bandages after they die. If left alone, they will reform back \ + into themselves. The bandages themselves are very vulnerable to fire.", + )) + + return to_add + +// Override to add a perk elaborating on just how dangerous fire is. +/datum/species/golem/cloth/create_pref_temperature_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "fire-alt", + SPECIES_PERK_NAME = "Incredibly Flammable", + SPECIES_PERK_DESC = "Mummies are made entirely of cloth, which makes them \ + very vulnerable to fire. They will not reform if they die while on \ + fire, and they will easily catch alight. If your bandages burn to ash, you're toast!", + )) + + return to_add + /obj/structure/cloth_pile name = "pile of bandages" desc = "It emits a strange aura, as if there was still life within it..." 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 cd2093443f287..1e1a45b1d25c9 100644 --- a/code/modules/mob/living/carbon/human/species_types/humans.dm +++ b/code/modules/mob/living/carbon/human/species_types/humans.dm @@ -42,3 +42,40 @@ /datum/species/human/get_sniff_sound(mob/living/carbon/user) return SPECIES_DEFAULT_SNIFF_SOUND(user) + +/datum/species/human/prepare_human_for_preview(mob/living/carbon/human/human) + human.hair_style = "Business Hair" + human.hair_color = "b96" // brown + human.update_hair() + +/datum/species/human/get_species_description() + return "Humans are the dominant species in the known galaxy. \ + Their kind extend from old Earth to the edges of known space." + +/datum/species/human/get_species_lore() + return list( + "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." + ) + +/datum/species/human/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "robot", + SPECIES_PERK_NAME = "Asimov Superiority", + SPECIES_PERK_DESC = "The AI and their cyborgs are often (but not always) subservient only \ + to humans. As a human, silicons are required to both protect and obey you under the Asimov lawset.", + )) + + if(CONFIG_GET(flag/enforce_human_authority)) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bullhorn", + SPECIES_PERK_NAME = "Chain of Command", + SPECIES_PERK_DESC = "Nanotrasen only recognizes humans for command roles, such as Captain.", + )) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm index e431680fcdeb9..11e111b41b446 100644 --- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm @@ -5,6 +5,7 @@ /datum/species/oozeling/slime name = "Slimeperson" + plural_form = "Slimepeople" id = SPECIES_SLIMEPERSON default_color = "00FFFF" species_traits = list(MUTCOLORS,EYECOLOR,HAIR,FACEHAIR,NOBLOOD) @@ -305,6 +306,7 @@ /datum/species/oozeling/luminescent name = "Luminescent" + plural_form = null id = SPECIES_LUMINESCENT var/glow_intensity = LUMINESCENT_DEFAULT_GLOW var/obj/effect/dummy/luminescent_glow/glow @@ -486,6 +488,7 @@ GLOBAL_LIST_EMPTY(slime_links_by_mind) /datum/species/oozeling/stargazer name = "Stargazer" + plural_form = null id = SPECIES_STARGAZER examine_limb_id = SPECIES_OOZELING /// The stargazer's telepathy ability. 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 608cbaa2e8987..511378ab4e9c7 100644 --- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm @@ -1,6 +1,7 @@ /datum/species/lizard // Reptilian humanoids with scaled skin and tails. name = "\improper Lizardperson" + plural_form = "Lizardpeople" id = SPECIES_LIZARD bodyflag = FLAG_LIZARD default_color = "00FF00" @@ -70,6 +71,13 @@ /datum/species/lizard/get_sniff_sound(mob/living/carbon/user) return SPECIES_DEFAULT_SNIFF_SOUND(user) +/datum/species/lizard/get_species_description() + return "Lizardpeople, unlike many 'Animalid' species, are not derived from humans, and are simply bipedal reptile-like people. \ + Lizards often find great pride in their species." + +/datum/species/lizard/get_species_lore() + return null + /* Lizard subspecies: ASHWALKERS */ diff --git a/code/modules/mob/living/carbon/human/species_types/monkey.dm b/code/modules/mob/living/carbon/human/species_types/monkey.dm index f438fd255d7c9..349806028e497 100644 --- a/code/modules/mob/living/carbon/human/species_types/monkey.dm +++ b/code/modules/mob/living/carbon/human/species_types/monkey.dm @@ -16,3 +16,59 @@ species_r_arm = /obj/item/bodypart/r_arm/monkey species_l_leg = /obj/item/bodypart/l_leg/monkey species_r_leg = /obj/item/bodypart/r_leg/monkey + +/datum/species/monkey/get_species_description() + return "Monkeys are a type of primate that exist between humans and animals on the evolutionary chain. \ + Every year, on Monkey Day, Nanotrasen shows their respect for the little guys by allowing them to roam the station freely." + +/datum/species/monkey/get_species_lore() + return list( + "Monkeys are commonly used as test subjects on board Space Station 13. \ + But what if... for one day... the Monkeys were allowed to be the scientists? \ + What experiments would they come up with? Would they (stereotypically) be related to bananas somehow? \ + There's only one way to find out.", + ) + +/datum/species/monkey/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "spider", + SPECIES_PERK_NAME = "Vent Crawling", + SPECIES_PERK_DESC = "Monkeys can crawl through the vent and scrubber networks while wearing no clothing. \ + Stay out of the kitchen!", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "paw", + SPECIES_PERK_NAME = "Primal Primate", + SPECIES_PERK_DESC = "Monkeys are primitive humans, and can't do most things a human can do. Computers are impossible, \ + complex machines are right out, and most clothes don't fit your smaller form.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "capsules", + SPECIES_PERK_NAME = "Mutadone Averse", + SPECIES_PERK_DESC = "Monkeys are reverted into normal humans upon being exposed to Mutadone.", + ), + ) + + return to_add + +/datum/species/monkey/create_pref_language_perk() + var/list/to_add = list() + // Holding these variables so we can grab the exact names for our perk. + var/datum/language/common_language = /datum/language/common + var/datum/language/monkey_language = /datum/language/monkey + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "comment", + SPECIES_PERK_NAME = "Primitive Tongue", + SPECIES_PERK_DESC = "You may be able to understand [initial(common_language.name)], but you can't speak it. \ + You can only speak [initial(monkey_language.name)].", + )) + + return to_add 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 97060963fb669..c436da3e406ad 100644 --- a/code/modules/mob/living/carbon/human/species_types/mothmen.dm +++ b/code/modules/mob/living/carbon/human/species_types/mothmen.dm @@ -7,6 +7,7 @@ /datum/species/moth name = "\improper Mothman" + plural_form = "Mothpeople" id = SPECIES_MOTH bodyflag = FLAG_MOTH default_color = "00FF00" @@ -196,3 +197,35 @@ #undef COCOON_HARM_AMOUNT #undef COCOON_HEAL_AMOUNT #undef COCOON_NUTRITION_AMOUNT + +/datum/species/moth/get_species_description() + return "Mothpeople are an intelligent species, known for their affinity to all things moth - lights, cloth, wings, and friendship." + +/datum/species/moth/get_species_lore() + return null + +/datum/species/moth/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "feather-alt", + SPECIES_PERK_NAME = "Precious Wings", + SPECIES_PERK_DESC = "Moths can fly in pressurized, zero-g environments and safely land short falls using their wings.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "tshirt", + SPECIES_PERK_NAME = "Meal Plan", + SPECIES_PERK_DESC = "Moths can eat clothes for nourishment.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "fire", + SPECIES_PERK_NAME = "Ablazed Wings", + SPECIES_PERK_DESC = "Moth wings are fragile, and can be easily burnt off. However, moths can spin a cooccon to restore their wings if necessary.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/oozelings.dm b/code/modules/mob/living/carbon/human/species_types/oozelings.dm index e2a71ad70432a..99eb6a57d49e0 100644 --- a/code/modules/mob/living/carbon/human/species_types/oozelings.dm +++ b/code/modules/mob/living/carbon/human/species_types/oozelings.dm @@ -209,3 +209,50 @@ /datum/species/oozeling/get_sniff_sound(mob/living/carbon/user) return SPECIES_DEFAULT_SNIFF_SOUND(user) + +/datum/species/oozeling/get_species_description() + return "Literally made of jelly, Oozelings are squishy friends aboard Space Station 13." + +/datum/species/oozeling/get_species_lore() + return null + +/datum/species/oozeling/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "angle-double-down", + SPECIES_PERK_NAME = "Splat!", + SPECIES_PERK_DESC = "[plural_form] have special resistance to falling, because their body and organs can flatten on impact. \ + It might hurt a bit, but generally [plural_form] can fall a lot further before their vitals organs start being pulverized.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "street-view", + SPECIES_PERK_NAME = "Regenerative Limbs", + SPECIES_PERK_DESC = "[plural_form] can regrow their limbs at will, provided they have enough Jelly.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "tint-slash", + SPECIES_PERK_NAME = "Hydrophobic", + SPECIES_PERK_DESC = "[plural_form] are decomposed by water - contact with water, water vapor, or ingesting water can lead to rapid loss of body mass.", + ) + ) + + return to_add + +/datum/species/oozeling/create_pref_blood_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "tint", + SPECIES_PERK_NAME = "Jelly Blood", + SPECIES_PERK_DESC = "[plural_form] don't have blood, but instead have toxic [initial(exotic_blood.name)]! \ + Jelly is extremely important, as losing it will cause you to cannibalize your limbs. Having low jelly will make medical treatment very difficult. \ + Jelly is also extremely sensitive to cold, and you may rapidy solidify. [plural_form] regain jelly passively by eating, but supplemental injections are possible.", + )) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm index 4fe66c385e94b..f99b7833128b0 100644 --- a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm +++ b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm @@ -1,10 +1,11 @@ /datum/species/plasmaman name = "\improper Plasmaman" + plural_form = "Plasmamen" id = SPECIES_PLASMAMAN bodyflag = FLAG_PLASMAMAN sexes = 0 meat = /obj/item/stack/sheet/mineral/plasma - species_traits = list(NOBLOOD,NOTRANSSTING) + species_traits = list(NOBLOOD,NOTRANSSTING,ENVIROSUIT) inherent_traits = list(TRAIT_RESISTCOLD,TRAIT_RADIMMUNE,TRAIT_NOHUNGER,TRAIT_ALWAYS_CLEAN) inherent_biotypes = list(MOB_INORGANIC, MOB_HUMANOID) mutantlungs = /obj/item/organ/lungs/plasmaman @@ -67,17 +68,17 @@ /datum/species/plasmaman/after_equip_job(datum/job/J, mob/living/carbon/human/H, visualsOnly = FALSE, client/preference_source = null) H.open_internals(H.get_item_for_held_index(2)) - if(!preference_source) + if(!preference_source?.prefs) return var/path = J.species_outfits?[SPECIES_PLASMAMAN] if (!path) //Somehow we were given a job without a plasmaman suit, use the default one so we don't go in naked! path = /datum/outfit/plasmaman stack_trace("Job [J] lacks a species_outfits entry for plasmamen!") var/datum/outfit/plasmaman/O = new path - var/datum/character_save/CS = preference_source.prefs.active_character - if(CS.helmet_style != HELMET_DEFAULT) - if(O.helmet_variants[CS.helmet_style]) - var/helmet = O.helmet_variants[CS.helmet_style] + var/selected_style = preference_source.prefs.read_character_preference(/datum/preference/choiced/helmet_style) + if(selected_style != HELMET_DEFAULT) + if(O.helmet_variants[selected_style]) + var/helmet = O.helmet_variants[selected_style] qdel(H.head) H.equip_to_slot(new helmet, ITEM_SLOT_HEAD) H.open_internals(H.get_item_for_held_index(2)) @@ -145,3 +146,67 @@ /datum/species/plasmaman/get_sniff_sound(mob/living/carbon/user) return SPECIES_DEFAULT_SNIFF_SOUND(user) + +/datum/species/plasmaman/get_species_description() + return "Found on the Icemoon of Freyja, plasmamen consist of colonial \ + fungal organisms which together form a sentient being. In human space, \ + they're usually attached to skeletons to afford a human touch." + +/datum/species/plasmaman/get_species_lore() + return list( + "A confusing species, plasmamen are truly \"a fungus among us\". \ + What appears to be a singular being is actually a colony of millions of organisms \ + surrounding a found (or provided) skeletal structure.", + + "Originally discovered by NT when a researcher \ + fell into an open tank of liquid plasma, the previously unnoticed fungal colony overtook the body creating \ + the first \"true\" plasmaman. The process has since been streamlined via generous donations of convict corpses and plasmamen \ + have been deployed en masse throughout NT to bolster the workforce.", + + "New to the galactic stage, plasmamen are a blank slate. \ + Their appearance, generally regarded as \"ghoulish\", inspires a lot of apprehension in their crewmates. \ + It might be the whole \"flammable purple skeleton\" thing.", + + "The colonids that make up plasmamen require the plasma-rich atmosphere they evolved in. \ + Their psuedo-nervous system runs with externalized electrical impulses that immediately ignite their plasma-based bodies when oxygen is present.", + ) + +/datum/species/plasmaman/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "user-shield", + SPECIES_PERK_NAME = "Protected", + SPECIES_PERK_DESC = "Plasmamen are immune to radiation, poisons, and most diseases.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "hard-hat", + SPECIES_PERK_NAME = "Protective Helmet", + SPECIES_PERK_DESC = "Plasmamen's helmets provide them shielding from the flashes of welding, as well as an inbuilt flashlight.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "fire", + SPECIES_PERK_NAME = "Living Torch", + SPECIES_PERK_DESC = "Plasmamen instantly ignite when their body makes contact with oxygen.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "wind", + SPECIES_PERK_NAME = "Plasma Breathing", + SPECIES_PERK_DESC = "Plasmamen must breathe plasma to survive. You receive a tank when you arrive.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "briefcase-medical", + SPECIES_PERK_NAME = "Complex Biology", + SPECIES_PERK_DESC = "Plasmamen take specialized medical knowledge to be \ + treated. Do not expect speedy revival, if you are lucky enough to get \ + one at all.", + ), + ) + + return to_add diff --git a/code/modules/mob/living/carbon/human/species_types/podpeople.dm b/code/modules/mob/living/carbon/human/species_types/podpeople.dm index 89c660d6be925..7a701871cb2c7 100644 --- a/code/modules/mob/living/carbon/human/species_types/podpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/podpeople.dm @@ -1,6 +1,7 @@ /datum/species/pod // A mutation caused by a human being ressurected in a revival pod. These regain health in light, and begin to wither in darkness. name = "\improper Podperson" + plural_form = "Podpeople" id = SPECIES_PODPERSON default_color = "59CE00" species_traits = list(MUTCOLORS,EYECOLOR) diff --git a/code/modules/mob/living/carbon/human/species_types/psyphoza.dm b/code/modules/mob/living/carbon/human/species_types/psyphoza.dm index e568a453a09f4..2a8fc79a91b15 100644 --- a/code/modules/mob/living/carbon/human/species_types/psyphoza.dm +++ b/code/modules/mob/living/carbon/human/species_types/psyphoza.dm @@ -17,6 +17,7 @@ GLOBAL_LIST_INIT(psychic_sense_blacklist, typecacheof(list(/turf/open, /obj/mach /datum/species/psyphoza name = "\improper Psyphoza" + plural_form = "Psyphoza" id = SPECIES_PSYPHOZA bodyflag = FLAG_PSYPHOZA meat = /obj/item/reagent_containers/food/snacks/meat/slab/human/mutant/psyphoza @@ -39,6 +40,7 @@ GLOBAL_LIST_INIT(psychic_sense_blacklist, typecacheof(list(/turf/open, /obj/mach mutant_bodyparts = list("psyphoza_cap") default_features = list("psyphoza_cap" = "Portobello", "body_size" = "Normal") + hair_color = "fixedmutcolor" species_chest = /obj/item/bodypart/chest/psyphoza species_head = /obj/item/bodypart/head/psyphoza @@ -80,6 +82,32 @@ GLOBAL_LIST_INIT(psychic_sense_blacklist, typecacheof(list(/turf/open, /obj/mach . = ..() PH?.Trigger() +/datum/species/psyphoza/get_species_description() + return "..." + +/datum/species/psyphoza/get_species_lore() + return list("...") + +/datum/species/psyphoza/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "lightbulb", + SPECIES_PERK_NAME = "Psychic", + SPECIES_PERK_DESC = "Psyphoza are psychic and can sense things others can't.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "eye", + SPECIES_PERK_NAME = "Blind", + SPECIES_PERK_DESC = "Psyphoza are blind and can't see outside their immediate location and psychic sense.", + ), + ) + + return to_add + //This originally held the psychic action until I moved it to the eyes, keep it please. /obj/item/organ/brain/psyphoza name = "psyphoza brain" @@ -144,7 +172,6 @@ GLOBAL_LIST_INIT(psychic_sense_blacklist, typecacheof(list(/turf/open, /obj/mach auto_action.Grant(M) ///Start auto timer addtimer(CALLBACK(src, PROC_REF(auto_sense)), auto_cooldown) - // /datum/action/item_action/organ_action/psychic_highlight/IsAvailable() if(has_cooldown_timer) @@ -159,10 +186,6 @@ GLOBAL_LIST_INIT(psychic_sense_blacklist, typecacheof(list(/turf/open, /obj/mach has_cooldown_timer = TRUE UpdateButtonIcon() addtimer(CALLBACK(src, PROC_REF(finish_cooldown)), cooldown + (sense_time * min(1, overlays.len / PSYCHIC_OVERLAY_UPPER))) - var/atom/movable/screen/plane_master/psychic/wall/PW = locate(/atom/movable/screen/plane_master/psychic/wall) in owner.client?.screen - if(PW && !length(PW.filters)) - PW.alpha = 255 - PW.filters += filter(type = "alpha", x = 0, y = 0, icon = icon('icons/mob/psychic.dmi', "e")) /datum/action/item_action/organ_action/psychic_highlight/UpdateButtonIcon(status_only = FALSE, force = FALSE) . = ..() @@ -207,11 +230,6 @@ GLOBAL_LIST_INIT(psychic_sense_blacklist, typecacheof(list(/turf/open, /obj/mach if(B) animate(B, alpha = 255) animate(B, alpha = 0, time = sense_time, easing = SINE_EASING, flags = EASE_IN) - //Wall nearby highlighting - var/atom/movable/screen/plane_master/psychic/wall/PW = locate(/atom/movable/screen/plane_master/psychic/wall) in owner.client?.screen - if(PW) - animate(PW, alpha = 0) - animate(PW, alpha = 255, time = sense_time, easing = SINE_EASING, flags = EASE_IN) //Setup timer to delete image if(overlay_timer) deltimer(overlay_timer) @@ -317,7 +335,14 @@ GLOBAL_LIST_INIT(psychic_sense_blacklist, typecacheof(list(/turf/open, /obj/mach var/visual_index = 0 /atom/movable/screen/fullscreen/blind/psychic_highlight/wall - plane = PSYCHIC_WALL_PLANE + plane = FULLSCREEN_PLANE + blend_mode = BLEND_DEFAULT + layer = 4.1 + +/atom/movable/screen/fullscreen/blind/psychic_highlight/wall/Initialize(mapload) + . = ..() + filters += filter(type = "alpha", render_source = "*WALL_PLANE_RENDER_TARGET") + filters += filter(type = "alpha", icon = icon('icons/mob/psychic.dmi', "e")) /atom/movable/screen/fullscreen/blind/psychic_highlight/Initialize(mapload) . = ..() @@ -414,18 +439,5 @@ GLOBAL_LIST_INIT(psychic_sense_blacklist, typecacheof(list(/turf/open, /obj/mach if(psychic_action?.auto_sense) return FALSE -/proc/generate_psychic_overlay(atom/target) - var/mutable_appearance/M = new() - M.appearance = target.appearance - M.transform = target.transform - M.pixel_x = 0 //Reset pixel adjustments to avoid bug where overlays tower - M.pixel_y = 0 - M.pixel_z = 0 - M.pixel_w = 0 - M.plane = PSYCHIC_WALL_PLANE //Draw overlay on this plane so we can use it as a mask - M.dir = target.dir - - return M - #undef PSYCHIC_OVERLAY_UPPER #undef PSYPHOZA_BURNMOD diff --git a/code/modules/mob/living/carbon/human/species_types/pumpkin_man.dm b/code/modules/mob/living/carbon/human/species_types/pumpkin_man.dm index 2cecdd5aeb955..aac7938637606 100644 --- a/code/modules/mob/living/carbon/human/species_types/pumpkin_man.dm +++ b/code/modules/mob/living/carbon/human/species_types/pumpkin_man.dm @@ -1,5 +1,6 @@ /datum/species/pod/pumpkin_man name = "\improper Pumpkinperson" + plural_form = "Pumpkinpeople" id = SPECIES_PUMPKINPERSON sexes = 0 meat = /obj/item/reagent_containers/food/snacks/pumpkinpieslice @@ -21,7 +22,27 @@ /datum/species/pod/pumpkin_man/check_roundstart_eligible() if(SSevents.holidays && SSevents.holidays[HALLOWEEN]) return TRUE - return FALSE + return ..() + +/datum/species/pod/pumpkin_man/get_species_description() + return "A rare subspecies of the Podpeople, Pumpkinpeople are gourdy and orange, appearing every halloween." + +/datum/species/pod/pumpkin_man/get_species_lore() + return null + +/datum/species/pod/pumpkin_man/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "candy-cane", + SPECIES_PERK_NAME = "Candy Head!", + SPECIES_PERK_DESC = "The heads of Pumpkinpeople are known to create delicious candy. Be careful though, take too much and you might pull your brain out!", + ), + ) + + return to_add /obj/item/organ/brain/pumpkin_brain name = "pumpkinperson brain" diff --git a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm index ec75e0d0257d8..06fe781f3ffca 100644 --- a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm @@ -3,7 +3,8 @@ /datum/species/shadow // Humans cursed to stay in the darkness, lest their life forces drain. They regain health in shadow and die in light. - name = "???" + name = "\improper Shadow" + plural_form = "Shadowpeople" id = SPECIES_SHADOWPERSON sexes = 0 meat = /obj/item/reagent_containers/food/snacks/meat/slab/human/mutant/shadow @@ -37,6 +38,55 @@ return TRUE return ..() +/datum/species/shadow/get_species_description() + return "Victims of a long extinct space alien. Their flesh is a sickly \ + seethrough filament, their tangled insides in clear view. Their form \ + is a mockery of life, leaving them mostly unable to work with others under \ + normal circumstances." + +/datum/species/shadow/get_species_lore() + return list( + "Long ago, the Spinward Sector used to be inhabited by terrifying aliens aptly named \"Shadowlings\" \ + after their control over darkness, and tendancy to kidnap victims into the dark maintenance shafts. \ + Around 2558, the long campaign Nanotrasen waged against the space terrors ended with the full extinction of the Shadowlings.", + + "Victims of their kidnappings would become brainless thralls, and via surgery they could be freed from the Shadowling's control. \ + Those more unlucky would have their entire body transformed by the Shadowlings to better serve in kidnappings. \ + Unlike the brain tumors of lesser control, these greater thralls could not be reverted.", + + "With Shadowlings long gone, their will is their own again. But their bodies have not reverted, burning in exposure to light. \ + Nanotrasen has assured the victims that they are searching for a cure. No further information has been given, even years later. \ + Most shadowpeople now assume Nanotrasen has long since shelfed the project.", + ) + +/datum/species/shadow/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "moon", + SPECIES_PERK_NAME = "Shadowborn", + SPECIES_PERK_DESC = "Their skin blooms in the darkness. All kinds of damage, \ + no matter how extreme, will heal over time as long as there is no light.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "eye", + SPECIES_PERK_NAME = "Nightvision", + SPECIES_PERK_DESC = "Their eyes are adapted to the night, and can see in the dark with no problems.", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "sun", + SPECIES_PERK_NAME = "Lightburn", + SPECIES_PERK_DESC = "Their flesh withers in the light. Any exposure to light is \ + incredibly painful for the shadowperson, charring their skin.", + ), + ) + + return to_add + /datum/species/shadow/nightmare name = "Nightmare" id = "nightmare" @@ -78,7 +128,7 @@ icon_state = "brain-x-d" var/obj/effect/proc_holder/spell/targeted/shadowwalk/shadowwalk -/obj/item/organ/brain/nightmare/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/brain/nightmare/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() if(M.dna.species.id != "nightmare") M.set_species(/datum/species/shadow/nightmare) @@ -88,7 +138,7 @@ shadowwalk = SW -/obj/item/organ/brain/nightmare/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/brain/nightmare/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) if(shadowwalk) M.RemoveSpell(shadowwalk) ..() @@ -118,13 +168,13 @@ user.temporarilyRemoveItemFromInventory(src, TRUE) Insert(user) -/obj/item/organ/heart/nightmare/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/nightmare/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() if(special != HEART_SPECIAL_SHADOWIFY) blade = new/obj/item/light_eater M.put_in_hands(blade) -/obj/item/organ/heart/nightmare/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/nightmare/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) respawn_progress = 0 if(blade && special != HEART_SPECIAL_SHADOWIFY) M.visible_message("\The [blade] disintegrates!") diff --git a/code/modules/mob/living/carbon/human/species_types/skeletons.dm b/code/modules/mob/living/carbon/human/species_types/skeletons.dm index ff02bc9539e2e..ad3a3284bc672 100644 --- a/code/modules/mob/living/carbon/human/species_types/skeletons.dm +++ b/code/modules/mob/living/carbon/human/species_types/skeletons.dm @@ -1,6 +1,7 @@ /datum/species/skeleton // 2spooky name = "\improper Spooky Scary Skeleton" + plural_form = "Skeletons" id = SPECIES_SKELETON sexes = 0 meat = /obj/item/reagent_containers/food/snacks/meat/slab/human/mutant/skeleton @@ -61,3 +62,15 @@ H.reagents.remove_reagent(chem.type, chem.metabolization_rate) return TRUE return ..() + +/datum/species/skeleton/get_species_description() + return "A rattling skeleton! They descend upon Space Station 13 \ + Every year to spook the crew! \"I've got a BONE to pick with you!\"" + +/datum/species/skeleton/get_species_lore() + return list( + "Skeletons want to be feared again! Their presence in media has been destroyed, \ + or at least that's what they firmly believe. They're always the first thing fought in an RPG, \ + they're Flanderized into pun rolling JOKES, and it's really starting to get to them. \ + You could say they're deeply RATTLED. Hah." + ) diff --git a/code/modules/mob/living/carbon/human/species_types/snail.dm b/code/modules/mob/living/carbon/human/species_types/snail.dm index 2b2922a3541ad..4cce83b6a141e 100644 --- a/code/modules/mob/living/carbon/human/species_types/snail.dm +++ b/code/modules/mob/living/carbon/human/species_types/snail.dm @@ -1,5 +1,6 @@ /datum/species/snail name = "\improper Snailperson" + plural_form = "Snailpeople" id = SPECIES_SNAILPERSON offset_features = list(OFFSET_UNIFORM = list(0,0), OFFSET_ID = list(0,0), OFFSET_GLOVES = list(0,0), OFFSET_GLASSES = list(0,4), OFFSET_EARS = list(0,0), OFFSET_SHOES = list(0,0), OFFSET_S_STORE = list(0,0), OFFSET_FACEMASK = list(0,0), OFFSET_HEAD = list(0,0), OFFSET_FACE = list(0,0), OFFSET_BELT = list(0,0), OFFSET_BACK = list(0,0), OFFSET_SUIT = list(0,0), OFFSET_NECK = list(0,0)) default_color = "336600" //vomit green diff --git a/code/modules/mob/living/carbon/human/species_types/supersoldier.dm b/code/modules/mob/living/carbon/human/species_types/supersoldier.dm index a0f329ee05d95..c4c5f36d1cdb4 100644 --- a/code/modules/mob/living/carbon/human/species_types/supersoldier.dm +++ b/code/modules/mob/living/carbon/human/species_types/supersoldier.dm @@ -1,6 +1,6 @@ /datum/species/human/supersoldier name = "Super Soldier" //inherited from the real species, for health scanners and things - id = SPECIES_SUPERSOILDER + id = SPECIES_SUPERSOLDIER examine_limb_id = SPECIES_HUMAN species_traits = list(EYECOLOR,HAIR,FACEHAIR,LIPS,NOTRANSSTING) //all of these + whatever we inherit from the real species inherent_traits = list(TRAIT_NOLIMBDISABLE,TRAIT_NOHUNGER,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER,TRAIT_IGNORESLOWDOWN,TRAIT_IGNOREDAMAGESLOWDOWN,TRAIT_STUNIMMUNE,TRAIT_CONFUSEIMMUNE,TRAIT_SLEEPIMMUNE,TRAIT_PUSHIMMUNE,TRAIT_VIRUSIMMUNE,TRAIT_NODISMEMBER,TRAIT_NOSLIPALL,TRAIT_THERMAL_VISION,TRAIT_STRONG_GRABBER,TRAIT_LAW_ENFORCEMENT_METABOLISM,TRAIT_ALWAYS_CLEAN,TRAIT_FEARLESS) diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm index 71124ecb8d10a..987bb28e2e0f5 100644 --- a/code/modules/mob/living/carbon/human/species_types/vampire.dm +++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm @@ -19,7 +19,7 @@ /datum/species/vampire/check_roundstart_eligible() if(SSevents.holidays && SSevents.holidays[HALLOWEEN]) return TRUE - return FALSE + return ..() /datum/species/vampire/on_species_gain(mob/living/carbon/human/C, datum/species/old_species) . = ..() @@ -63,6 +63,69 @@ return 1 //Whips deal 2x damage to vampires. Vampire killer. return 0 +/datum/species/vampire/get_species_description() + return "A classy Vampire! They descend upon Space Station Thirteen Every year to spook the crew! \"Bleeg!!\"" + +/datum/species/vampire/get_species_lore() + return list( + "Vampires are unholy beings blessed and cursed with The Thirst. \ + The Thirst requires them to feast on blood to stay alive, and in return it gives them many bonuses." + ) + +/datum/species/vampire/create_pref_unique_perks() + var/list/to_add = list() + + to_add += list( + list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "bed", + SPECIES_PERK_NAME = "Coffin Brooding", + SPECIES_PERK_DESC = "Vampires can delay The Thirst and heal by resting in a coffin. So THAT'S why they do that!", + ), + list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "cross", + SPECIES_PERK_NAME = "Against God and Nature", + SPECIES_PERK_DESC = "Almost all higher powers are disgusted by the existence of \ + Vampires, and entering the Chapel is essentially suicide. Do not do it!", + ), + ) + + return to_add + +// Vampire blood is special, so it needs to be handled with its own entry. +/datum/species/vampire/create_pref_blood_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = "tint", + SPECIES_PERK_NAME = "The Thirst", + SPECIES_PERK_DESC = "In place of eating, Vampires suffer from The Thirst. \ + Thirst of what? Blood! Their tongue allows them to grab people and drink \ + their blood, and they will die if they run out. As a note, it doesn't \ + matter whose blood you drink, it will all be converted into your blood \ + type when consumed.", + )) + + return to_add + +// There isn't a "Minor Undead" biotype, so we have to explain it in an override (see: dullahans) +/datum/species/vampire/create_pref_biotypes_perks() + var/list/to_add = list() + + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = "skull", + SPECIES_PERK_NAME = "Minor Undead", + SPECIES_PERK_DESC = "[name] are minor undead. \ + Minor undead enjoy some of the perks of being dead, like \ + not needing to breathe or eat, but do not get many of the \ + environmental immunities involved with being fully undead.", + )) + + return to_add + /obj/item/organ/tongue/vampire name = "vampire tongue" actions_types = list(/datum/action/item_action/organ_action/vampire) diff --git a/code/modules/mob/living/carbon/human/species_types/zombies.dm b/code/modules/mob/living/carbon/human/species_types/zombies.dm index a6f4271c175a2..aaeca112ee2f9 100644 --- a/code/modules/mob/living/carbon/human/species_types/zombies.dm +++ b/code/modules/mob/living/carbon/human/species_types/zombies.dm @@ -26,6 +26,12 @@ return TRUE return ..() +/datum/species/zombie/get_species_description() + return "A rotting zombie! They descend upon Space Station Thirteen Every year to spook the crew! \"Sincerely, the Zombies!\"" + +/datum/species/zombie/get_species_lore() + return list("Zombies have long lasting beef with Botanists. Their last incident involving a lawn with defensive plants has left them very unhinged.") + /datum/species/zombie/infectious name = "\improper Infectious Zombie" id = "memezombies" diff --git a/code/modules/mob/living/carbon/monkey/monkey.dm b/code/modules/mob/living/carbon/monkey/monkey.dm index 2c2ff29861b7a..152f31664d9aa 100644 --- a/code/modules/mob/living/carbon/monkey/monkey.dm +++ b/code/modules/mob/living/carbon/monkey/monkey.dm @@ -254,7 +254,7 @@ GLOBAL_LIST_INIT(strippable_monkey_items, create_strippable_list(list( /obj/item/organ/brain/tumor name = "teratoma brain" -/obj/item/organ/brain/tumor/Remove(mob/living/carbon/C, special, no_id_transfer) +/obj/item/organ/brain/tumor/Remove(mob/living/carbon/C, special, no_id_transfer, pref_load = FALSE) . = ..() //Removing it deletes it if(!QDELETED(src)) diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index 003339bcc4969..b3ae07747d93b 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -276,10 +276,10 @@ GLOBAL_LIST_INIT(department_radio_keys, list( listening -= M // remove (added by SEE_INVISIBLE_MAXIMUM) continue if(get_dist(M, src) > 7 || M.get_virtual_z_level() != get_virtual_z_level()) //they're out of range of normal hearing - if(eavesdrop_range && !(M.client.prefs.chat_toggles & CHAT_GHOSTWHISPER)) //they're whispering and we have hearing whispers at any range off + if(eavesdrop_range && !M.client.prefs.read_player_preference(/datum/preference/toggle/chat_ghostwhisper)) //they're whispering and we have hearing whispers at any range off listening -= M // remove (added by SEE_INVISIBLE_MAXIMUM) continue - if(!(M.client.prefs.chat_toggles & CHAT_GHOSTEARS)) //they're talking normally and we have hearing at any range off + if(!M.client.prefs.read_player_preference(/datum/preference/toggle/chat_ghostears)) //they're talking normally and we have hearing at any range off listening -= M // remove (added by SEE_INVISIBLE_MAXIMUM) continue listening |= M @@ -316,7 +316,7 @@ GLOBAL_LIST_INIT(department_radio_keys, list( //speech bubble var/list/speech_bubble_recipients = list() for(var/mob/M in listening) - if(M.client && !(M.client.prefs.toggles & PREFTOGGLE_RUNECHAT_GLOBAL)) + if(M.client?.prefs && !M.client.prefs.read_player_preference(/datum/preference/toggle/enable_runechat)) 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 776e2d27d95c1..6756e2fc3cc26 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -142,7 +142,7 @@ create_modularInterface() if(client) - apply_pref_name("ai",client) + INVOKE_ASYNC(src, PROC_REF(apply_pref_name), /datum/preference/name/ai, client) INVOKE_ASYNC(src, PROC_REF(set_core_display_icon)) @@ -190,7 +190,7 @@ return if("1", "2", "3", "4", "5", "6", "7", "8", "9") _key = text2num(_key) - if(client.keys_held["Ctrl"]) //do we assign a new hotkey? + if(user.keys_held["Ctrl"]) //do we assign a new hotkey? cam_hotkeys[_key] = eyeobj.loc to_chat(src, "Location saved to Camera Group [_key].") return @@ -226,10 +226,10 @@ /mob/living/silicon/ai/proc/set_core_display_icon(input, client/C) if(client && !C) C = client - if(!input && !C?.prefs?.active_character.preferred_ai_core_display) + if(!input && !C?.prefs?.read_character_preference(/datum/preference/choiced/ai_core_display)) icon_state = initial(icon_state) else - var/preferred_icon = input ? input : C.prefs.active_character.preferred_ai_core_display + var/preferred_icon = input ? input : C.prefs.read_character_preference(/datum/preference/choiced/ai_core_display) icon_state = resolve_ai_icon(preferred_icon) /mob/living/silicon/ai/verb/pick_icon() @@ -926,7 +926,7 @@ rendered = "\[Holocall\] [language_icon][speaker.GetVoice()][treated_message]" var/rendered_scrambled_message for(var/mob/dead/observer/each_ghost in GLOB.dead_mob_list) - if(!each_ghost.client || !(each_ghost.client.prefs.toggles & CHAT_GHOSTRADIO)) + if(!each_ghost.client || !each_ghost.client.prefs.read_player_preference(/datum/preference/toggle/chat_ghostradio)) continue var/follow_link = FOLLOW_LINK(each_ghost, speaker) if(each_ghost.has_language(message_language)) diff --git a/code/modules/mob/living/silicon/ai/say.dm b/code/modules/mob/living/silicon/ai/say.dm index 4840264dfe607..4534bfee57e0f 100644 --- a/code/modules/mob/living/silicon/ai/say.dm +++ b/code/modules/mob/living/silicon/ai/say.dm @@ -50,7 +50,7 @@ to_chat(src, message) for(var/mob/dead/observer/each_ghost in GLOB.dead_mob_list) - if(!each_ghost.client || !(each_ghost.client.prefs.toggles & CHAT_GHOSTRADIO)) + if(!each_ghost.client || !each_ghost.client.prefs.read_player_preference(/datum/preference/toggle/chat_ghostradio)) continue var/follow_link = FOLLOW_LINK(each_ghost, eyeobj || ai_hologram) to_chat(each_ghost, "[follow_link] [message]") @@ -154,7 +154,7 @@ if(!only_listener) // Play voice for all mobs in the z level for(var/mob/M in GLOB.player_list) - if(M.client && M.can_hear() && (M.client.prefs.toggles & PREFTOGGLE_SOUND_ANNOUNCEMENTS)) + if(M.client && M.can_hear() && M.client.prefs.read_player_preference(/datum/preference/toggle/sound_announcements)) var/turf/T = get_turf(M) if(T.get_virtual_z_level() == z_level) SEND_SOUND(M, voice) diff --git a/code/modules/mob/living/silicon/login.dm b/code/modules/mob/living/silicon/login.dm index 76b171063c4c9..541736da1a6a1 100644 --- a/code/modules/mob/living/silicon/login.dm +++ b/code/modules/mob/living/silicon/login.dm @@ -9,6 +9,6 @@ /mob/living/silicon/auto_deadmin_on_login() if(!client?.holder) return TRUE - if(CONFIG_GET(flag/auto_deadmin_silicons) || (client.prefs?.toggles & PREFTOGGLE_DEADMIN_POSITION_SILICON)) + if(CONFIG_GET(flag/auto_deadmin_silicons) || client.prefs?.read_player_preference(/datum/preference/toggle/deadmin_position_silicon)) return client.holder.auto_deadmin() return ..() diff --git a/code/modules/mob/living/silicon/pai/personality.dm b/code/modules/mob/living/silicon/pai/personality.dm index 056a46db85426..be6ab89acd39f 100644 --- a/code/modules/mob/living/silicon/pai/personality.dm +++ b/code/modules/mob/living/silicon/pai/personality.dm @@ -20,7 +20,7 @@ user.client.prefs.pai_name = name user.client.prefs.pai_description = description user.client.prefs.pai_comment = comments - user.client.prefs.save_preferences() + user.client.prefs.mark_undatumized_dirty_player() to_chat(usr, "You have saved pAI information.") return TRUE diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 307e3e0237579..a95e9a8a7b0f9 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -290,8 +290,8 @@ var/changed_name = "" if(custom_name) changed_name = custom_name - if(changed_name == "" && C && C.prefs.active_character.custom_names["cyborg"] != DEFAULT_CYBORG_NAME) - if(apply_pref_name("cyborg", C)) + if(changed_name == "" && C && C.prefs.read_character_preference(/datum/preference/name/cyborg) != DEFAULT_CYBORG_NAME) + if(apply_pref_name(/datum/preference/name/cyborg, C)) return //built in camera handled in proc if(!changed_name) changed_name = get_standard_name() 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 30f86c43f8dcf..9ab732c688f15 100644 --- a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm +++ b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm @@ -145,7 +145,7 @@ /mob/living/simple_animal/drone/auto_deadmin_on_login() if(!client?.holder) return TRUE - if(CONFIG_GET(flag/auto_deadmin_silicons) || (client.prefs?.toggles & PREFTOGGLE_DEADMIN_POSITION_SILICON)) + if(CONFIG_GET(flag/auto_deadmin_silicons) || client.prefs?.read_player_preference(/datum/preference/toggle/deadmin_position_silicon)) return client.holder.auto_deadmin() return ..() diff --git a/code/modules/mob/living/simple_animal/hostile/zombie.dm b/code/modules/mob/living/simple_animal/hostile/zombie.dm index d7742cd633f20..e446b65169bb3 100644 --- a/code/modules/mob/living/simple_animal/hostile/zombie.dm +++ b/code/modules/mob/living/simple_animal/hostile/zombie.dm @@ -29,21 +29,21 @@ setup_visuals() /mob/living/simple_animal/hostile/zombie/proc/setup_visuals() - var/datum/character_save/CS = new - CS.pref_species = new /datum/species/zombie - CS.be_random_body = TRUE - var/datum/job/J = SSjob.GetJob(zombiejob) - var/datum/outfit/O - if(J.outfit) - O = new J.outfit - //They have claws now. - O.r_hand = null - O.l_hand = null + var/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 , CS, "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/login.dm b/code/modules/mob/login.dm index 6708a188a6d0d..00fa1790bd072 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -35,7 +35,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_player_preference(/datum/preference/choiced/ui_style))) next_move = 1 @@ -106,8 +106,8 @@ * * Configs: * * flag/auto_deadmin_players - * * client.prefs?.toggles & DEADMIN_ALWAYS - * * User is antag and flag/auto_deadmin_antagonists or client.prefs?.toggles & DEADMIN_ANTAGONIST + * * client?.prefs?.read_player_preference(/datum/preference/toggle/deadmin_always) + * * User is antag and flag/auto_deadmin_antagonists or client?.prefs?.read_player_preference(/datum/preference/toggle/deadmin_antagonist) * * or if their job demands a deadminning SSjob.handle_auto_deadmin_roles() * * Called from [login](mob.html#proc/Login) @@ -115,9 +115,9 @@ /mob/proc/auto_deadmin_on_login() //return true if they're not an admin at the end. if(!client?.holder) return TRUE - if(CONFIG_GET(flag/auto_deadmin_players) || (client.prefs?.toggles & PREFTOGGLE_DEADMIN_ALWAYS)) + if(CONFIG_GET(flag/auto_deadmin_players) || client?.prefs?.read_player_preference(/datum/preference/toggle/deadmin_always)) return client.holder.auto_deadmin() - if(mind.has_antag_datum(/datum/antagonist) && (CONFIG_GET(flag/auto_deadmin_antagonists) || client.prefs?.toggles & PREFTOGGLE_DEADMIN_ANTAGONIST)) + if(mind.has_antag_datum(/datum/antagonist) && (CONFIG_GET(flag/auto_deadmin_antagonists) || client.prefs?.read_player_preference(/datum/preference/toggle/deadmin_antagonist))) return client.holder.auto_deadmin() if(job) return SSjob.handle_auto_deadmin_roles(client, job) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 42a007a1b25d0..93c52ca742422 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -281,16 +281,18 @@ ///Returns the client runechat visible messages preference according to the message type. /atom/proc/runechat_prefs_check(mob/target, list/visible_message_flags) - if(!(target.client?.prefs.toggles & PREFTOGGLE_RUNECHAT_GLOBAL) || !(target.client.prefs.toggles & PREFTOGGLE_RUNECHAT_NONMOBS)) + if(!target.client?.prefs.read_player_preference(/datum/preference/toggle/enable_runechat)) return FALSE - if(LAZYFIND(visible_message_flags, CHATMESSAGE_EMOTE) && !(target.client.prefs.toggles & PREFTOGGLE_RUNECHAT_EMOTES)) + if (!target.client?.prefs.read_player_preference(/datum/preference/toggle/enable_runechat_non_mobs)) + return FALSE + if(LAZYFIND(visible_message_flags, CHATMESSAGE_EMOTE) && !target.client.prefs.read_player_preference(/datum/preference/toggle/see_rc_emotes)) return FALSE return TRUE /mob/runechat_prefs_check(mob/target, list/visible_message_flags) - if(!(target.client?.prefs.toggles & PREFTOGGLE_RUNECHAT_GLOBAL)) + if(!target.client?.prefs.read_player_preference(/datum/preference/toggle/enable_runechat)) return FALSE - if(LAZYFIND(visible_message_flags, CHATMESSAGE_EMOTE) && !(target.client.prefs.toggles & PREFTOGGLE_RUNECHAT_EMOTES)) + if(LAZYFIND(visible_message_flags, CHATMESSAGE_EMOTE) && !target.client.prefs.read_player_preference(/datum/preference/toggle/see_rc_emotes)) return FALSE return TRUE @@ -860,6 +862,14 @@ ///Add a spell to the mobs spell list /mob/proc/AddSpell(obj/effect/proc_holder/spell/S) + // HACK: Preferences menu creates one of every selectable species. + // Some species, like vampires, create spells when they're made. + // The "action" is created when those spells Initialize. + // Preferences menu can create these assets at *any* time, primarily before + // the atoms SS initializes. + // That means "action" won't exist. + if (isnull(S.action)) + return mob_spell_list += S S.action.Grant(src) diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index 145ecf6c74217..20376364bf4e5 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -429,8 +429,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_player_preference(/datum/preference/choiced/ui_style) + if(ui_style) + A.icon = ui_style2icon(ui_style) if (header) A.name = header A.desc = message diff --git a/code/modules/mob/mob_transformation_simple.dm b/code/modules/mob/mob_transformation_simple.dm index 85627464c6076..077556a0624c3 100644 --- a/code/modules/mob/mob_transformation_simple.dm +++ b/code/modules/mob/mob_transformation_simple.dm @@ -47,7 +47,7 @@ D.updateappearance(mutcolor_update=1, mutations_overlay_update=1) else if(ishuman(M)) var/mob/living/carbon/human/H = M - client.prefs.active_character.copy_to(H) + H.randomize_human_appearance(~RANDOMIZE_SPECIES) H.dna.update_dna_identity() if(mind && isliving(M)) diff --git a/code/modules/mob/status_procs.dm b/code/modules/mob/status_procs.dm index 5611baa3cfc6b..9e681348509cd 100644 --- a/code/modules/mob/status_procs.dm +++ b/code/modules/mob/status_procs.dm @@ -43,7 +43,7 @@ /// proc that adds and removes blindness overlays when necessary /mob/proc/update_blindness(overlay = /atom/movable/screen/fullscreen/blind) if(stat == UNCONSCIOUS || HAS_TRAIT(src, TRAIT_BLIND) || eye_blind) // UNCONSCIOUS or has blind trait, or has temporary blindness - if(stat == CONSCIOUS || stat == SOFT_CRIT) + if((stat == CONSCIOUS || stat == SOFT_CRIT) && istype(overlay, /atom/movable/screen/alert)) throw_alert("blind", overlay) overlay_fullscreen("blind", overlay) // You are blind why should you be able to make out details like color, only shapes near you diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm index c8d2be93cb325..d8720c5cd408c 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -515,7 +515,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/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index c7393a2134b15..2beee19416427 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -30,7 +30,7 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar /// List of themes for this device to allow. var/list/allowed_themes /// Color used for the Thinktronic Classic theme. - var/classic_color = "#808000" + var/classic_color = COLOR_OLIVE var/datum/computer_file/program/active_program = null // A currently active program running on the computer. var/hardware_flag = 0 // A flag that describes this device type var/last_power_usage = 0 diff --git a/code/modules/modular_computers/computers/item/computer_ui.dm b/code/modules/modular_computers/computers/item/computer_ui.dm index 2d1e0bdb8e9c7..aac3e834347fa 100644 --- a/code/modules/modular_computers/computers/item/computer_ui.dm +++ b/code/modules/modular_computers/computers/item/computer_ui.dm @@ -216,7 +216,7 @@ new_color = tgui_color_picker(user, "Choose a new color for [src]'s flashlight.", "Light Color",light_color) if(!new_color) return - if(color_hex2num(new_color) < 200) //Colors too dark are rejected + if(is_color_dark(new_color, 50) ) //Colors too dark are rejected to_chat(user, "That color is too dark! Choose a lighter one.") new_color = null return set_flashlight_color(new_color) diff --git a/code/modules/modular_computers/computers/item/tablet.dm b/code/modules/modular_computers/computers/item/tablet.dm index c26c7d280b6fc..a523179d7688e 100644 --- a/code/modules/modular_computers/computers/item/tablet.dm +++ b/code/modules/modular_computers/computers/item/tablet.dm @@ -363,13 +363,10 @@ equipped = TRUE if(!user.client.prefs) return - var/pref_theme = user.client.prefs.pda_theme - if(!theme_locked && !ignore_theme_pref) - for(var/key in allowed_themes) // i am going to scream. DM lists stop sucking please - if(allowed_themes[key] == pref_theme) - device_theme = pref_theme - break - classic_color = user.client.prefs.pda_color + var/pref_theme = user.client.prefs.read_character_preference(/datum/preference/choiced/pda_theme) + if(!theme_locked && !ignore_theme_pref && (pref_theme in allowed_themes)) + device_theme = allowed_themes[pref_theme] + classic_color = user.client.prefs.read_character_preference(/datum/preference/color/pda_classic_color) /obj/item/modular_computer/tablet/pda/update_icon() ..() diff --git a/code/modules/modular_computers/file_system/programs/ntmessenger.dm b/code/modules/modular_computers/file_system/programs/ntmessenger.dm index 19dc2c919924d..36a8e10a80fbf 100644 --- a/code/modules/modular_computers/file_system/programs/ntmessenger.dm +++ b/code/modules/modular_computers/file_system/programs/ntmessenger.dm @@ -340,7 +340,7 @@ // Show it to ghosts var/ghost_message = "[message_data["name"]] PDA Message --> [target_text]: [signal.format_message(include_photo = TRUE)]" for(var/mob/M in GLOB.player_list) - if(isobserver(M) && (M.client?.prefs.chat_toggles & CHAT_GHOSTPDA)) + if(isobserver(M) && M.client?.prefs.read_player_preference(/datum/preference/toggle/chat_ghostpda)) to_chat(M, "[FOLLOW_LINK(M, user)] [ghost_message]") // Log in the talk log diff --git a/code/modules/modular_computers/file_system/programs/ntnrc_client.dm b/code/modules/modular_computers/file_system/programs/ntnrc_client.dm index 5a3ab3bf9a69e..63863f06c0535 100644 --- a/code/modules/modular_computers/file_system/programs/ntnrc_client.dm +++ b/code/modules/modular_computers/file_system/programs/ntnrc_client.dm @@ -63,7 +63,7 @@ var/mob/living/user = usr var/ghost_message = "[user] (as [username]) NTRC Message to [channel.title]: [message]" for(var/mob/M in GLOB.player_list) - if(isobserver(M) && (M.client?.prefs.chat_toggles & CHAT_GHOSTPDA)) // TODO tablet-pda add a preference for this (currently frozen) + if(isobserver(M) && M.client?.prefs.read_player_preference(/datum/preference/toggle/chat_ghostpda)) // TODO tablet-pda add a preference for this (currently frozen) to_chat(M, "[FOLLOW_LINK(M, user)] [ghost_message]") user.log_talk(message, LOG_CHAT, tag="as [username] to channel [channel.title]") return TRUE diff --git a/code/modules/ninja/energy_katana.dm b/code/modules/ninja/energy_katana.dm index 0a8d6a3360995..7fa27ae22a5b3 100644 --- a/code/modules/ninja/energy_katana.dm +++ b/code/modules/ninja/energy_katana.dm @@ -47,13 +47,16 @@ /obj/item/energy_katana/pickup(mob/living/user) ..() - jaunt.Grant(user, src) + if(jaunt) + jaunt.Grant(user, src) + if(user.client) + playsound(src, 'sound/items/unsheath.ogg', 25, 1) user.update_icons() - playsound(src, 'sound/items/unsheath.ogg', 25, 1) /obj/item/energy_katana/dropped(mob/user) ..() - jaunt.Remove(user) + if(jaunt) + jaunt.Remove(user) user.update_icons() //If we hit the Ninja who owns this Katana, they catch it. diff --git a/code/modules/ninja/ninja_event.dm b/code/modules/ninja/ninja_event.dm index a3b656b5f09f6..377850be4fe95 100644 --- a/code/modules/ninja/ninja_event.dm +++ b/code/modules/ninja/ninja_event.dm @@ -78,8 +78,9 @@ Contents: /proc/create_space_ninja(spawn_loc) var/mob/living/carbon/human/new_ninja = new(spawn_loc) - var/datum/character_save/CS = new()//Randomize appearance for the ninja. - CS.real_name = "[pick(GLOB.ninja_titles)] [pick(GLOB.ninja_names)]" - CS.copy_to(new_ninja) + new_ninja.randomize_human_appearance(~(RANDOMIZE_NAME|RANDOMIZE_SPECIES)) + var/new_name = "[pick(GLOB.ninja_titles)] [pick(GLOB.ninja_names)]" + new_ninja.name = new_name + new_ninja.real_name = new_name new_ninja.dna.update_dna_identity() return new_ninja diff --git a/code/modules/ninja/outfit.dm b/code/modules/ninja/outfit.dm index dde8c417da0ce..79a2a24c73b21 100644 --- a/code/modules/ninja/outfit.dm +++ b/code/modules/ninja/outfit.dm @@ -16,7 +16,9 @@ implants = list(/obj/item/implant/explosive) -/datum/outfit/ninja/post_equip(mob/living/carbon/human/H) +/datum/outfit/ninja/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE) + if(visualsOnly) + return FALSE if(istype(H.wear_suit, suit)) var/obj/item/clothing/suit/space/space_ninja/S = H.wear_suit if(istype(H.belt, belt)) diff --git a/code/modules/paperwork/fax_manager.dm b/code/modules/paperwork/fax_manager.dm index 1c1fed63751c3..ec91733a577ac 100644 --- a/code/modules/paperwork/fax_manager.dm +++ b/code/modules/paperwork/fax_manager.dm @@ -138,7 +138,7 @@ GLOBAL_DATUM_INIT(fax_manager, /datum/fax_manager, new) break for(var/client/admin in GLOB.admins) - if((admin.prefs.chat_toggles & CHAT_PRAYER) && (admin.prefs.toggles & PREFTOGGLE_SOUND_PRAYERS)) + if(admin.prefs.read_player_preference(/datum/preference/toggle/chat_prayer) && admin.prefs.read_player_preference(/datum/preference/toggle/sound_prayers)) SEND_SOUND(admin, sound('sound/items/poster_being_created.ogg')) // A special piece of paper for the administrator that will open the interface no matter what. diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm index f4d0144b89f18..c74f252ea4b4c 100644 --- a/code/modules/projectiles/projectile/magic.dm +++ b/code/modules/projectiles/projectile/magic.dm @@ -251,7 +251,7 @@ new_mob = new path(M.loc) if("humanoid") - new_mob = new /mob/living/carbon/human(M.loc) + var/mob/living/carbon/human/new_human = new(M.loc) if(prob(50)) var/list/chooseable_races = list() @@ -261,15 +261,14 @@ chooseable_races += speciestype if(chooseable_races.len) - new_mob.set_species(pick(chooseable_races)) - - var/datum/character_save/CS = new() //Randomize appearance for the human - CS.copy_to(new_mob, icon_updates=0) - - var/mob/living/carbon/human/H = new_mob - H.update_hair() - H.update_body_parts(TRUE) - H.dna.update_dna_identity() + new_human.set_species(pick(chooseable_races)) + + // Randomize everything but the species, which was already handled above. + new_human.randomize_human_appearance(~RANDOMIZE_SPECIES) + new_human.update_hair() + new_human.update_body() // is_creating = TRUE + new_human.dna.update_dna_identity() + new_mob = new_human if(!new_mob) return diff --git a/code/modules/requests/request_manager.dm b/code/modules/requests/request_manager.dm index 6c3c132b14032..33718209e4364 100644 --- a/code/modules/requests/request_manager.dm +++ b/code/modules/requests/request_manager.dm @@ -53,7 +53,7 @@ GLOBAL_DATUM_INIT(requests, /datum/request_manager, new) /datum/request_manager/proc/pray(client/C, message, is_chaplain) request_for_client(C, REQUEST_PRAYER, message) for(var/client/admin in GLOB.admins) - if(is_chaplain && admin.prefs.chat_toggles & CHAT_PRAYER && admin.prefs.toggles & PREFTOGGLE_SOUND_PRAYERS) + if(is_chaplain && admin.prefs.read_player_preference(/datum/preference/toggle/chat_prayer) && admin.prefs.read_player_preference(/datum/preference/toggle/sound_prayers)) SEND_SOUND(admin, sound('sound/effects/pray.ogg')) /** diff --git a/code/modules/research/nanites/nanite_programs/sensor.dm b/code/modules/research/nanites/nanite_programs/sensor.dm index 25c91eb885960..3664058f4e89c 100644 --- a/code/modules/research/nanites/nanite_programs/sensor.dm +++ b/code/modules/research/nanites/nanite_programs/sensor.dm @@ -275,7 +275,7 @@ /datum/nanite_program/sensor/species/New() if(!length(allowed_species)) - for(var/id in GLOB.roundstart_races) + for(var/id in get_selectable_species()) allowed_species[id] = GLOB.species_list[id] . = ..() diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm index 7c699e51b4036..a9fb62e57e899 100644 --- a/code/modules/research/techweb/_techweb_node.dm +++ b/code/modules/research/techweb/_techweb_node.dm @@ -42,9 +42,9 @@ VARSET_TO_LIST(., display_name) VARSET_TO_LIST(., hidden) VARSET_TO_LIST(., starting_node) - VARSET_TO_LIST(., assoc_list_strip_value(prereq_ids)) - VARSET_TO_LIST(., assoc_list_strip_value(design_ids)) - VARSET_TO_LIST(., assoc_list_strip_value(unlock_ids)) + VARSET_TO_LIST(., assoc_to_keys(prereq_ids)) + VARSET_TO_LIST(., assoc_to_keys(design_ids)) + VARSET_TO_LIST(., assoc_to_keys(unlock_ids)) VARSET_TO_LIST(., boost_item_paths) VARSET_TO_LIST(., autounlock_by_boost) VARSET_TO_LIST(., export_price) diff --git a/code/modules/surgery/bodyparts/bodyparts.dm b/code/modules/surgery/bodyparts/bodyparts.dm index 82d21ee40c349..e9c4d411168f6 100644 --- a/code/modules/surgery/bodyparts/bodyparts.dm +++ b/code/modules/surgery/bodyparts/bodyparts.dm @@ -338,7 +338,7 @@ if(mutation_color) //I hate mutations draw_color = mutation_color else if(should_draw_greyscale) - draw_color = (species_color) || (skin_tone && skintone2hex(skin_tone)) + draw_color = (species_color) || (skin_tone && skintone2hex(skin_tone, include_tag = FALSE)) else draw_color = null @@ -369,7 +369,7 @@ draw_color = mutation_color if(should_draw_greyscale) //Should the limb be colored? - draw_color ||= (species_color) || (skin_tone && skintone2hex(skin_tone)) + draw_color ||= (species_color) || (skin_tone && skintone2hex(skin_tone, include_tag = FALSE)) dmg_overlay_type = S.damage_overlay_type @@ -453,7 +453,7 @@ draw_color = mutation_color if(should_draw_greyscale) //Should the limb be colored? - draw_color ||= (species_color) || (skin_tone && skintone2hex(skin_tone)) + draw_color ||= (species_color) || (skin_tone && skintone2hex(skin_tone, include_tag = FALSE)) if(draw_color) limb.color = "#[draw_color]" diff --git a/code/modules/surgery/bodyparts/helpers.dm b/code/modules/surgery/bodyparts/helpers.dm index 0bea87021943d..5321da254718a 100644 --- a/code/modules/surgery/bodyparts/helpers.dm +++ b/code/modules/surgery/bodyparts/helpers.dm @@ -236,7 +236,7 @@ . = L -/proc/skintone2hex(skin_tone) +/proc/skintone2hex(skin_tone, include_tag = TRUE) . = 0 switch(skin_tone) if("caucasian1") @@ -267,3 +267,5 @@ . = "ffc905" if("pink") . = "D7377D" + if(include_tag && .) + return "#" + . diff --git a/code/modules/surgery/organs/appendix.dm b/code/modules/surgery/organs/appendix.dm index 89937e7b5fc49..0645e4bc6c101 100644 --- a/code/modules/surgery/organs/appendix.dm +++ b/code/modules/surgery/organs/appendix.dm @@ -28,14 +28,14 @@ if(M) M.adjustToxLoss(4, TRUE, TRUE) //forced to ensure people don't use it to gain tox as slime person -/obj/item/organ/appendix/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/appendix/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) for(var/datum/disease/appendicitis/A in M.diseases) A.cure() inflamed = TRUE update_icon() ..() -/obj/item/organ/appendix/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/appendix/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() if(inflamed) M.ForceContractDisease(new /datum/disease/appendicitis(), FALSE, TRUE) diff --git a/code/modules/surgery/organs/augments_arms.dm b/code/modules/surgery/organs/augments_arms.dm index 99796f0b99972..a2f133b383260 100644 --- a/code/modules/surgery/organs/augments_arms.dm +++ b/code/modules/surgery/organs/augments_arms.dm @@ -112,14 +112,14 @@ to_chat(user, "You modify [src] to be installed on the [zone == BODY_ZONE_R_ARM ? "right" : "left"] arm.") update_icon() -/obj/item/organ/cyberimp/arm/Insert(mob/living/carbon/user, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/cyberimp/arm/Insert(mob/living/carbon/user, special = FALSE, drop_if_replaced = TRUE, pref_load = FALSE) . = ..() var/side = zone == BODY_ZONE_R_ARM ? 2 : 1 register_hand(user, owner.hand_bodyparts[side]) RegisterSignal(user, COMSIG_KB_MOB_DROPITEM_DOWN, PROC_REF(dropkey)) //We're nodrop, but we'll watch for the drop hotkey anyway and then stow if possible. RegisterSignal(user, COMSIG_CARBON_POST_ATTACH_LIMB, PROC_REF(limb_attached)) -/obj/item/organ/cyberimp/arm/Remove(mob/living/carbon/user, special = 0) +/obj/item/organ/cyberimp/arm/Remove(mob/living/carbon/user, special = 0, pref_load = FALSE) Retract() unregister_hand(user) UnregisterSignal(user, list(COMSIG_KB_MOB_DROPITEM_DOWN, COMSIG_CARBON_POST_ATTACH_LIMB)) diff --git a/code/modules/surgery/organs/augments_chest.dm b/code/modules/surgery/organs/augments_chest.dm index 3e4e9ecdc1149..069b3739b4180 100644 --- a/code/modules/surgery/organs/augments_chest.dm +++ b/code/modules/surgery/organs/augments_chest.dm @@ -130,13 +130,13 @@ var/on = FALSE var/datum/effect_system/trail_follow/ion/ion_trail -/obj/item/organ/cyberimp/chest/thrusters/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/cyberimp/chest/thrusters/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) . = ..() if(!ion_trail) ion_trail = new ion_trail.set_up(M) -/obj/item/organ/cyberimp/chest/thrusters/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/cyberimp/chest/thrusters/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) if(on) toggle(silent = TRUE) ..() diff --git a/code/modules/surgery/organs/augments_eyes.dm b/code/modules/surgery/organs/augments_eyes.dm index 6397ce9a5211c..720a84d551c05 100644 --- a/code/modules/surgery/organs/augments_eyes.dm +++ b/code/modules/surgery/organs/augments_eyes.dm @@ -15,7 +15,7 @@ var/HUD_type var/HUD_trait -/obj/item/organ/cyberimp/eyes/hud/Insert(var/mob/living/carbon/M, var/special = 0, drop_if_replaced = FALSE) +/obj/item/organ/cyberimp/eyes/hud/Insert(var/mob/living/carbon/M, var/special = 0, drop_if_replaced = FALSE, pref_load = FALSE) ..() if(HUD_type) var/datum/atom_hud/H = GLOB.huds[HUD_type] @@ -23,7 +23,7 @@ if(HUD_trait) ADD_TRAIT(M, HUD_trait, ORGAN_TRAIT) -/obj/item/organ/cyberimp/eyes/hud/Remove(var/mob/living/carbon/M, var/special = 0) +/obj/item/organ/cyberimp/eyes/hud/Remove(var/mob/living/carbon/M, var/special = 0, pref_load = FALSE) if(HUD_type) var/datum/atom_hud/H = GLOB.huds[HUD_type] H.remove_hud_from(M) diff --git a/code/modules/surgery/organs/augments_internal.dm b/code/modules/surgery/organs/augments_internal.dm index 5c103e97210b1..9d4e6b4550e47 100644 --- a/code/modules/surgery/organs/augments_internal.dm +++ b/code/modules/surgery/organs/augments_internal.dm @@ -89,7 +89,7 @@ stored_items = list() -/obj/item/organ/cyberimp/brain/anti_drop/Remove(var/mob/living/carbon/M, special = 0) +/obj/item/organ/cyberimp/brain/anti_drop/Remove(var/mob/living/carbon/M, special = 0, pref_load = FALSE) if(active) ui_action_click() ..() @@ -109,7 +109,7 @@ var/stun_cap_amount = 40 -/obj/item/organ/cyberimp/brain/anti_stun/Remove(mob/living/carbon/M, special = FALSE) +/obj/item/organ/cyberimp/brain/anti_stun/Remove(mob/living/carbon/M, special = FALSE, pref_load = FALSE) . = ..() UnregisterSignal(M, signalCache) diff --git a/code/modules/surgery/organs/ears.dm b/code/modules/surgery/organs/ears.dm index da07a1205ca7f..ced7fd9d08f14 100644 --- a/code/modules/surgery/organs/ears.dm +++ b/code/modules/surgery/organs/ears.dm @@ -97,16 +97,22 @@ icon_state = "kitty" bang_protect = -2 -/obj/item/organ/ears/cat/Insert(mob/living/carbon/human/H, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/ears/cat/Insert(mob/living/carbon/human/H, special = 0, drop_if_replaced = TRUE, pref_load = FALSE) ..() + if(pref_load) + H.update_body() + return if(istype(H)) color = H.hair_color H.dna.species.mutant_bodyparts |= "ears" H.dna.features["ears"] = "Cat" H.update_body() -/obj/item/organ/ears/cat/Remove(mob/living/carbon/human/H, special = 0) +/obj/item/organ/ears/cat/Remove(mob/living/carbon/human/H, special = 0, pref_load = FALSE) ..() + if(pref_load && istype(H)) + H.update_body() + return if(istype(H)) color = H.hair_color H.dna.features["ears"] = "None" @@ -118,13 +124,13 @@ desc = "The source of a penguin's happy feet." var/datum/component/waddle -/obj/item/organ/ears/penguin/Insert(mob/living/carbon/human/H, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/ears/penguin/Insert(mob/living/carbon/human/H, special = 0, drop_if_replaced = TRUE, pref_load = FALSE) . = ..() if(istype(H)) to_chat(H, "You suddenly feel like you've lost your balance.") waddle = H.AddComponent(/datum/component/waddling) -/obj/item/organ/ears/penguin/Remove(mob/living/carbon/human/H, special = 0) +/obj/item/organ/ears/penguin/Remove(mob/living/carbon/human/H, special = 0, pref_load = FALSE) . = ..() if(istype(H)) to_chat(H, "Your sense of balance comes back to you.") diff --git a/code/modules/surgery/organs/eyes.dm b/code/modules/surgery/organs/eyes.dm index 67b981b9a6f38..e0798c3a32b63 100644 --- a/code/modules/surgery/organs/eyes.dm +++ b/code/modules/surgery/organs/eyes.dm @@ -33,14 +33,13 @@ ///the type of overlay we use for this eye's blind effect var/atom/movable/screen/fullscreen/blind/blind_type -/obj/item/organ/eyes/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = FALSE, initialising) +/obj/item/organ/eyes/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = FALSE, initialising, pref_load = FALSE) . = ..() if(ishuman(owner)) var/mob/living/carbon/human/HMN = owner old_eye_color = HMN.eye_color if(eye_color) HMN.eye_color = eye_color - HMN.regenerate_icons() else eye_color = HMN.eye_color if(HAS_TRAIT(HMN, TRAIT_NIGHT_VISION) && !lighting_alpha) @@ -50,12 +49,12 @@ if(M.has_dna() && ishuman(M)) M.dna.species.handle_body(M) //updates eye icon -/obj/item/organ/eyes/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/eyes/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() if(ishuman(M) && eye_color) var/mob/living/carbon/human/HMN = M HMN.eye_color = old_eye_color - HMN.regenerate_icons() + HMN.update_body() M.update_tint() M.update_sight() @@ -176,7 +175,7 @@ /obj/item/organ/eyes/robotic/flashlight/emp_act(severity) return -/obj/item/organ/eyes/robotic/flashlight/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = FALSE) +/obj/item/organ/eyes/robotic/flashlight/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = FALSE, pref_load = FALSE) ..() if(!eye) eye = new /obj/item/flashlight/eyelight() @@ -186,7 +185,7 @@ M.become_blind("flashlight_eyes") -/obj/item/organ/eyes/robotic/flashlight/Remove(var/mob/living/carbon/M, var/special = 0) +/obj/item/organ/eyes/robotic/flashlight/Remove(var/mob/living/carbon/M, var/special = 0, pref_load = FALSE) eye.on = FALSE eye.update_brightness(M) eye.forceMove(src) @@ -228,7 +227,7 @@ terminate_effects() . = ..() -/obj/item/organ/eyes/robotic/glow/Remove(mob/living/carbon/M, special = FALSE) +/obj/item/organ/eyes/robotic/glow/Remove(mob/living/carbon/M, special = FALSE, pref_load = FALSE) terminate_effects() . = ..() @@ -424,6 +423,6 @@ M.become_blind("uncurable", /atom/movable/screen/fullscreen/blind/psychic) M.remove_client_colour(/datum/client_colour/monochrome/blind) -/obj/item/organ/eyes/psyphoza/Remove(mob/living/carbon/M, special) +/obj/item/organ/eyes/psyphoza/Remove(mob/living/carbon/M, special = FALSE, pref_load = FALSE) . = ..() M.cure_blind("uncurable") diff --git a/code/modules/surgery/organs/heart.dm b/code/modules/surgery/organs/heart.dm index 4888219fce4c6..97afce1b052a4 100644 --- a/code/modules/surgery/organs/heart.dm +++ b/code/modules/surgery/organs/heart.dm @@ -27,7 +27,7 @@ else icon_state = "[icon_base]-off" -/obj/item/organ/heart/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() if(!special) addtimer(CALLBACK(src, PROC_REF(stop_if_unowned)), 120) @@ -129,12 +129,12 @@ else last_pump = world.time //lets be extra fair *sigh* -/obj/item/organ/heart/cursed/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/cursed/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() if(owner) to_chat(owner, "Your heart has been replaced with a cursed one, you have to pump this one manually otherwise you'll die!") -/obj/item/organ/heart/cursed/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/heart/cursed/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) ..() M.remove_client_colour(/datum/client_colour/cursed_heart_blood) diff --git a/code/modules/surgery/organs/organ_internal.dm b/code/modules/surgery/organs/organ_internal.dm index a72a3a197dcfb..f07ec7a13b8a9 100644 --- a/code/modules/surgery/organs/organ_internal.dm +++ b/code/modules/surgery/organs/organ_internal.dm @@ -30,19 +30,25 @@ var/useable = TRUE var/list/food_reagents = list(/datum/reagent/consumable/nutriment = 5) +// 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) AddComponent(/datum/component/edible, initial_reagents = food_reagents, foodtypes = RAW | MEAT | GORE, \ pre_eat = CALLBACK(src, PROC_REF(pre_eat)), on_compost = CALLBACK(src, PROC_REF(pre_compost)) , after_eat = CALLBACK(src, PROC_REF(on_eat_from))) -/obj/item/organ/proc/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/proc/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE, pref_load = FALSE) if(!iscarbon(M) || owner == M) return var/obj/item/organ/replaced = M.getorganslot(slot) if(replaced) - replaced.Remove(M, special = 1) + replaced.Remove(M, special = 1, pref_load = pref_load) if(drop_if_replaced) replaced.forceMove(get_turf(M)) else @@ -61,7 +67,7 @@ STOP_PROCESSING(SSobj, src) //Special is for instant replacement like autosurgeons -/obj/item/organ/proc/Remove(mob/living/carbon/M, special = FALSE) +/obj/item/organ/proc/Remove(mob/living/carbon/M, special = FALSE, pref_load = FALSE) owner = null if(M) M.internal_organs -= src diff --git a/code/modules/surgery/organs/stomach.dm b/code/modules/surgery/organs/stomach.dm index fc7218cf825ed..e4b19ac68016a 100755 --- a/code/modules/surgery/organs/stomach.dm +++ b/code/modules/surgery/organs/stomach.dm @@ -77,7 +77,7 @@ H.throw_alert("disgust", /atom/movable/screen/alert/disgusted) SEND_SIGNAL(H, COMSIG_ADD_MOOD_EVENT, "disgust", /datum/mood_event/disgusted) -/obj/item/organ/stomach/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/stomach/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) var/mob/living/carbon/human/H = owner if(istype(H)) H.clear_alert("disgust") @@ -101,12 +101,12 @@ var/max_charge = 5000 //same as upgraded+ cell var/charge = 5000 -/obj/item/organ/stomach/battery/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/stomach/battery/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) . = ..() RegisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, PROC_REF(charge)) update_nutrition() -/obj/item/organ/stomach/battery/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/stomach/battery/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) UnregisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT) if(!HAS_TRAIT(owner, TRAIT_NOHUNGER) && HAS_TRAIT(owner, TRAIT_POWERHUNGRY)) owner.nutrition = 0 @@ -173,11 +173,11 @@ max_charge = 2500 //same as upgraded cell charge = 2500 -/obj/item/organ/stomach/battery/ethereal/Insert(mob/living/carbon/M, special = 0) +/obj/item/organ/stomach/battery/ethereal/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) RegisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT, PROC_REF(on_electrocute)) return ..() -/obj/item/organ/stomach/battery/ethereal/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/stomach/battery/ethereal/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) UnregisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT) return ..() diff --git a/code/modules/surgery/organs/tails.dm b/code/modules/surgery/organs/tails.dm index d9440688d80ac..63c51a2b67131 100644 --- a/code/modules/surgery/organs/tails.dm +++ b/code/modules/surgery/organs/tails.dm @@ -22,16 +22,23 @@ desc = "A severed cat tail. Who's wagging now?" tail_type = "Cat" -/obj/item/organ/tail/cat/Insert(mob/living/carbon/human/H, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/tail/cat/Insert(mob/living/carbon/human/H, special = 0, drop_if_replaced = TRUE, pref_load = FALSE) ..() + if(pref_load && istype(H)) + H.update_body() + return if(istype(H)) if(!("tail_human" in H.dna.species.mutant_bodyparts)) H.dna.species.mutant_bodyparts |= "tail_human" H.dna.features["tail_human"] = tail_type H.update_body() -/obj/item/organ/tail/cat/Remove(mob/living/carbon/human/H, special = 0) +/obj/item/organ/tail/cat/Remove(mob/living/carbon/human/H, special = 0, pref_load = FALSE) ..() + if(pref_load && istype(H)) + color = H.hair_color + H.update_body() + return if(istype(H)) H.dna.features["tail_human"] = "None" H.dna.species.mutant_bodyparts -= "tail_human" @@ -77,7 +84,7 @@ H.dna.species.mutant_bodyparts |= "spines" H.update_body() -/obj/item/organ/tail/lizard/Remove(mob/living/carbon/human/H, special = 0) +/obj/item/organ/tail/lizard/Remove(mob/living/carbon/human/H, special = 0, pref_load = FALSE) ..() if(istype(H)) H.dna.species.mutant_bodyparts -= "tail_lizard" diff --git a/code/modules/surgery/organs/tongue.dm b/code/modules/surgery/organs/tongue.dm index 4b23aec7a8361..769db0880eef4 100644 --- a/code/modules/surgery/organs/tongue.dm +++ b/code/modules/surgery/organs/tongue.dm @@ -49,7 +49,7 @@ M.UnregisterSignal(M, COMSIG_MOB_SAY) return ..() -/obj/item/organ/tongue/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/tongue/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) UnregisterSignal(M, COMSIG_MOB_SAY, PROC_REF(handle_speech)) M.RegisterSignal(M, COMSIG_MOB_SAY, TYPE_PROC_REF(/mob/living/carbon, handle_tongueless_speech)) return ..() diff --git a/code/modules/surgery/organs/wings.dm b/code/modules/surgery/organs/wings.dm index d62e84c031473..b5cc85dc7e22c 100644 --- a/code/modules/surgery/organs/wings.dm +++ b/code/modules/surgery/organs/wings.dm @@ -37,7 +37,7 @@ if(H.movement_type & FLYING) H.dna.species.toggle_flight(H) -/obj/item/organ/wings/Remove(mob/living/carbon/human/H, special = 0) +/obj/item/organ/wings/Remove(mob/living/carbon/human/H, special = 0, pref_load = FALSE) ..() if(istype(H)) H.dna.species.mutant_bodyparts -= basewings @@ -123,7 +123,7 @@ wing_type = "Plain" canopen = TRUE -/obj/item/organ/wings/moth/Remove(mob/living/carbon/human/H, special) +/obj/item/organ/wings/moth/Remove(mob/living/carbon/human/H, special, pref_load = FALSE) flight_level = initial(flight_level) return ..() @@ -172,7 +172,7 @@ wing_type = "Bee" var/jumpdist = 3 -/obj/item/organ/wings/bee/Remove(mob/living/carbon/human/H, special) +/obj/item/organ/wings/bee/Remove(mob/living/carbon/human/H, special, pref_load = FALSE) jumpdist = initial(jumpdist) return ..() diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index 00a06d29ef850..df6ca97611fc8 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -57,7 +57,7 @@ /datum/tgui/New(mob/user, datum/src_object, interface, title, ui_x, ui_y) if(!user.client) // No client to show the TGUI to, so stop here return - log_tgui(user, "new [interface] fancy [user.client.prefs.toggles2 & PREFTOGGLE_2_FANCY_TGUI]") + log_tgui(user, "new [interface] fancy [user?.client?.prefs.read_player_preference(/datum/preference/toggle/tgui_fancy)]") src.user = user src.src_object = src_object src.window_key = "[REF(src_object)]-main" @@ -101,7 +101,7 @@ if(!window.is_ready()) window.initialize( strict_mode = TRUE, - fancy = (user.client.prefs.toggles & PREFTOGGLE_2_FANCY_TGUI), + fancy = user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_fancy), assets = list( get_asset_datum(/datum/asset/simple/tgui), )) @@ -243,8 +243,8 @@ "window" = list( "key" = window_key, "size" = window_size, - "fancy" = (user.client.prefs.toggles2 & PREFTOGGLE_2_FANCY_TGUI), - "locked" = (user.client.prefs.toggles2 & PREFTOGGLE_2_LOCKED_BUTTONS), + "fancy" = user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_fancy), + "locked" = user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_lock), ), "client" = list( "ckey" = user.client.ckey, diff --git a/code/modules/tgui_input/alert.dm b/code/modules/tgui_input/alert.dm index 1f42a25428be8..d1091977f6382 100644 --- a/code/modules/tgui_input/alert.dm +++ b/code/modules/tgui_input/alert.dm @@ -20,7 +20,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) switch(length(buttons)) if(1) return alert(user, message, title, buttons[1]) @@ -60,7 +60,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) if(length(buttons) == 2) return alert(user, message, title, buttons[1], buttons[2]) if(length(buttons) == 3) @@ -134,8 +134,8 @@ .["autofocus"] = autofocus .["buttons"] = buttons .["message"] = message - .["large_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_BIG_BUTTONS) - .["swapped_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_SWITCHED_BUTTONS) + .["large_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_large) + .["swapped_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_swapped) .["title"] = title /datum/tgui_modal/ui_data(mob/user) diff --git a/code/modules/tgui_input/color.dm b/code/modules/tgui_input/color.dm index e917f0fd5618b..b2f2c7bd71d17 100644 --- a/code/modules/tgui_input/color.dm +++ b/code/modules/tgui_input/color.dm @@ -18,7 +18,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) return input(user, message, title, default) as color|null var/datum/tgui_color_picker/picker = new(user, message, title, default, timeout, autofocus) picker.ui_interact(user) @@ -48,7 +48,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) return input(user, message, title, default) as color|null var/datum/tgui_color_picker/async/picker = new(user, message, title, default, callback, timeout, autofocus) picker.ui_interact(user) @@ -115,8 +115,8 @@ /datum/tgui_color_picker/ui_static_data(mob/user) . = list() .["autofocus"] = autofocus - .["large_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_BIG_BUTTONS) - .["swapped_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_SWITCHED_BUTTONS) + .["large_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_large) + .["swapped_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_swapped) .["title"] = title .["default_color"] = default .["message"] = message diff --git a/code/modules/tgui_input/list.dm b/code/modules/tgui_input/list.dm index 86354dd504149..240b0f6fe5a35 100644 --- a/code/modules/tgui_input/list.dm +++ b/code/modules/tgui_input/list.dm @@ -22,7 +22,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) return input(user, message, title) as null|anything in items var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout) input.ui_interact(user) @@ -56,7 +56,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) return input(user, message, title) as null|anything in items var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout) input.ui_interact(user) @@ -146,8 +146,8 @@ . = list() .["init_value"] = default || items[1] .["items"] = items - .["large_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_BIG_BUTTONS) - .["swapped_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_SWITCHED_BUTTONS) + .["large_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_large) + .["swapped_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_swapped) .["message"] = message .["title"] = title diff --git a/code/modules/tgui_input/number.dm b/code/modules/tgui_input/number.dm index fa0655df60d51..31d40b7c113f8 100644 --- a/code/modules/tgui_input/number.dm +++ b/code/modules/tgui_input/number.dm @@ -25,7 +25,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) var/input_number = input(user, message, title, default) as null|num return clamp(round_value ? round(input_number) : input_number, min_value, max_value) var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value) @@ -61,7 +61,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) var/input_number = input(user, message, title, default) as null|num return clamp(round_value ? round(input_number) : input_number, min_value, max_value) var/datum/tgui_input_number/async/number_input = new(user, message, title, default, max_value, min_value, callback, timeout, round_value) @@ -150,8 +150,8 @@ .["max_value"] = max_value .["message"] = message .["min_value"] = min_value - .["large_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_BIG_BUTTONS) - .["swapped_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_SWITCHED_BUTTONS) + .["large_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_large) + .["swapped_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_swapped) .["title"] = title /datum/tgui_input_number/ui_data(mob/user) diff --git a/code/modules/tgui_input/say_modal/modal.dm b/code/modules/tgui_input/say_modal/modal.dm index f3aa30e84b95b..b492904e8f7b0 100644 --- a/code/modules/tgui_input/say_modal/modal.dm +++ b/code/modules/tgui_input/say_modal/modal.dm @@ -84,13 +84,15 @@ * as soon as the window sends the "ready" message. */ /datum/tgui_say/proc/load() + if(!client.mob) // client has not fully loaded yet. + return window_open = FALSE // Width and height are from skin.dmf, no way to not hardcode these unfortunately. - client.center_window("tgui_say", 231, 30) + INVOKE_ASYNC(client, TYPE_PROC_REF(/client, center_window), "tgui_say", 231, 30) // async due to prefs menu winshow(client, "tgui_say", FALSE) window.send_message("props", list( - lightMode = (client?.prefs?.toggles2 & PREFTOGGLE_2_SAY_LIGHT_THEME), - showRadioPrefix = (client?.prefs?.toggles2 & PREFTOGGLE_2_SAY_SHOW_PREFIX), + lightMode = client?.prefs?.read_player_preference(/datum/preference/toggle/tgui_say_light_mode), + showRadioPrefix = client?.prefs?.read_player_preference(/datum/preference/toggle/tgui_say_show_prefix), maxLength = max_length, )) stop_thinking() diff --git a/code/modules/tgui_input/text.dm b/code/modules/tgui_input/text.dm index 008aa3c442c39..cfb2ec0caafb5 100644 --- a/code/modules/tgui_input/text.dm +++ b/code/modules/tgui_input/text.dm @@ -25,7 +25,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) if(encode) if(multiline) return stripped_multiline_input(user, message, title, default, max_length) @@ -67,7 +67,7 @@ else return // Client does NOT have tgui_input on: Returns regular input - if(!(user.client?.prefs?.toggles2 & PREFTOGGLE_2_TGUI_INPUT)) + if(!user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input)) if(encode) if(multiline) return stripped_multiline_input(user, message, title, default, max_length) @@ -154,8 +154,8 @@ .["message"] = message .["multiline"] = multiline .["placeholder"] = default // Default is a reserved keyword - .["large_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_BIG_BUTTONS) - .["swapped_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_SWITCHED_BUTTONS) + .["large_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_large) + .["swapped_buttons"] = !user.client?.prefs || user.client.prefs.read_player_preference(/datum/preference/toggle/tgui_input_swapped) .["title"] = title /datum/tgui_input_text/ui_data(mob/user) diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm index 577e16f790437..d6279e830bc70 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_player_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 032363716b109..02d40ef96b98b 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -79,6 +79,7 @@ #include "heretic_rituals.dm" #include "metabolizing.dm" #include "ntnetwork_tests.dm" +#include "preference_species.dm" #include "projectiles.dm" #include "subsystem_init.dm" #include "subsystem_metric_sanity.dm" diff --git a/code/modules/unit_tests/preference_species.dm b/code/modules/unit_tests/preference_species.dm new file mode 100644 index 0000000000000..f06c894a5f877 --- /dev/null +++ b/code/modules/unit_tests/preference_species.dm @@ -0,0 +1,33 @@ + +/** + * Checks that all enabled roundstart species + * selectable within the preferences menu + * have their info / page setup correctly. + */ +/datum/unit_test/preference_species + +/datum/unit_test/preference_species/Run() + + // Go though all selectable species to see if they have their page setup correctly. + for(var/species_id in get_selectable_species()) + + var/species_type = GLOB.species_list[species_id] + var/datum/species/species = new species_type() + + // Check the species decription. + // If it's not overridden, a stack trace will be thrown (and fail the test). + // If it's null, it was improperly overriden. Fail the test. + var/species_desc = species.get_species_description() + if(isnull(species_desc)) + Fail("Species [species] ([species_type]) is selectable, but did not properly implement get_species_description().") + + // Check the species lore. + // If it's not overridden, a stack trace will be thrown (and fail the test). + // If it's null, or returned a list, it was improperly overriden. Fail the test. + var/species_lore = species.get_species_lore() + if(isnull(species_lore)) + Fail("Species [species] ([species_type]) is selectable, but did not properly implement get_species_lore().") + else if(!islist(species_lore)) + Fail("Species [species] ([species_type]) is selectable, but did not properly implement get_species_lore() (Did not return a list).") + + qdel(species) diff --git a/code/modules/unit_tests/preferences.dm b/code/modules/unit_tests/preferences.dm new file mode 100644 index 0000000000000..20fd6bd7ad6d9 --- /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.preference_type == 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 preference_type. +/datum/unit_test/preferences_valid_db_key + +/datum/unit_test/preferences_valid_db_key/Run() + var/list/known_db_keys = list() + + for (var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + if (!istext(preference.db_key)) + Fail("[preference_type] has an invalid db_key.") + + if (preference.db_key in known_db_keys) + Fail("[preference_type] has a non-unique db_key `[preference.db_key]`!") + + known_db_keys += preference.db_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 0000000000000..ad0ca261c8bb5 --- /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/wiremod/components/action/light.dm b/code/modules/wiremod/components/action/light.dm index 72cd1feebb69d..33ee4898181eb 100644 --- a/code/modules/wiremod/components/action/light.dm +++ b/code/modules/wiremod/components/action/light.dm @@ -19,7 +19,7 @@ var/datum/port/input/on var/max_power = 5 - var/min_lightness = 0.4 + var/min_lightness = 40 var/shell_light_color /obj/item/circuit_component/light/get_ui_notices() @@ -49,9 +49,8 @@ green.set_value(clamp(green.value, 0, 255)) on.set_value(clamp(on.value, 0, 1)) - var/list/hsl = rgb2hsl(red.value || 0, green.value || 0, blue.value || 0) - var/list/light_col = hsl2rgb(hsl[1], hsl[2], max(min_lightness, hsl[3])) - shell_light_color = rgb(light_col[1], light_col[2], light_col[3]) + var/list/hsl = rgb2num(rgb(red.value || 0, green.value || 0, blue.value || 0), COLORSPACE_HSL) + shell_light_color = rgb(hsl[1], hsl[2], max(min_lightness, hsl[3]), space=COLORSPACE_HSL) /obj/item/circuit_component/light/input_received(datum/port/input/port) if(parent.shell) diff --git a/code/modules/zombie/organs.dm b/code/modules/zombie/organs.dm index 0f5d2012f8065..5078c5c945329 100644 --- a/code/modules/zombie/organs.dm +++ b/code/modules/zombie/organs.dm @@ -23,11 +23,11 @@ GLOB.zombie_infection_list -= src . = ..() -/obj/item/organ/zombie_infection/Insert(var/mob/living/carbon/M, special = 0) +/obj/item/organ/zombie_infection/Insert(var/mob/living/carbon/M, special = 0, pref_load = FALSE) . = ..() START_PROCESSING(SSobj, src) -/obj/item/organ/zombie_infection/Remove(mob/living/carbon/M, special = 0) +/obj/item/organ/zombie_infection/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) . = ..() STOP_PROCESSING(SSobj, src) if(iszombie(M) && old_species && !QDELETED(M) && !special) diff --git a/config/game_options.txt b/config/game_options.txt index b55526150326c..2dd67b2b0f95b 100644 --- a/config/game_options.txt +++ b/config/game_options.txt @@ -493,31 +493,41 @@ ROUNDSTART_RACES moth #ROUNDSTART_RACES fly ROUNDSTART_RACES psyphoza - ## Races that are better than humans in some ways, but worse in others ROUNDSTART_RACES apid ROUNDSTART_RACES ethereal ROUNDSTART_RACES ipc ROUNDSTART_RACES oozeling ROUNDSTART_RACES plasmaman -#ROUNDSTART_RACES abductor + +## Golems +## ---------------- + #ROUNDSTART_RACES adamantine_golem #ROUNDSTART_RACES diamond_golem #ROUNDSTART_RACES gold_golem #ROUNDSTART_RACES iron_golem -#ROUNDSTART_RACES jelly #ROUNDSTART_RACES plasma_golem -#ROUNDSTART_RACES shadow #ROUNDSTART_RACES silver_golem #ROUNDSTART_RACES uranium_golem -## Races that are straight upgrades. If these are on expect powergamers to always pick them -#ROUNDSTART_RACES agent -#ROUNDSTART_RACES pod +## Halloween races +## ---------------- + +#ROUNDSTART_RACES abductor +#ROUNDSTART_RACES shadow +#ROUNDSTART_RACES dullahan +#ROUNDSTART_RACES pumpkin_man +#ROUNDSTART_RACES vampire + +## OP Halloween races: #ROUNDSTART_RACES skeleton -#ROUNDSTART_RACES slime #ROUNDSTART_RACES zombie +## Races that are straight upgrades. If these are on expect powergamers to always pick them +#ROUNDSTART_RACES pod + + ## Roundstart no-reset races ##------------------------------------------------------------------------------------------- ## Races defined here will not cause existing characters to be reset to human if they currently have a non-roundstart species defined. diff --git a/icons/UI_Icons/antags/obsessed.dmi b/icons/UI_Icons/antags/obsessed.dmi new file mode 100644 index 0000000000000..219a6e594132b Binary files /dev/null and b/icons/UI_Icons/antags/obsessed.dmi differ diff --git a/icons/mob/apid_accessories/apid_wings.dmi b/icons/mob/apid_accessories/apid_wings.dmi new file mode 100644 index 0000000000000..fb22fc2e329eb Binary files /dev/null and b/icons/mob/apid_accessories/apid_wings.dmi differ diff --git a/icons/mob/moth_antennae.dmi b/icons/mob/moth_antennae.dmi index cb5a245fb3d59..f1af2cc15807e 100644 Binary files a/icons/mob/moth_antennae.dmi and b/icons/mob/moth_antennae.dmi differ diff --git a/icons/mob/moth_markings.dmi b/icons/mob/moth_markings.dmi index 2429b0aa12dfe..f447b3968ef0e 100644 Binary files a/icons/mob/moth_markings.dmi and b/icons/mob/moth_markings.dmi differ diff --git a/icons/mob/wings.dmi b/icons/mob/wings.dmi index da63ff3bbae12..b53f17f495ecc 100644 Binary files a/icons/mob/wings.dmi and b/icons/mob/wings.dmi differ diff --git a/interface/interface.dm b/interface/interface.dm index 8ad6c9dbce6c5..45073c32cdfae 100644 --- a/interface/interface.dm +++ b/interface/interface.dm @@ -106,7 +106,7 @@ Admin: src << browse(changelog.get_htmlloader("changelog.html"), "window=changes;size=675x650") if(prefs.lastchangelog != GLOB.changelog_hash) prefs.lastchangelog = GLOB.changelog_hash - prefs.save_preferences() + prefs.mark_undatumized_dirty_player() winset(src, "infowindow.changelog", "font-style=;") diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml index 3f62ae1e83c1d..44c4b1ebe5623 100644 --- a/tgui/.eslintrc.yml +++ b/tgui/.eslintrc.yml @@ -13,6 +13,7 @@ env: plugins: - sonarjs - react + - unused-imports settings: react: version: '16.10' @@ -642,7 +643,7 @@ rules: ## Prevent usage of unsafe lifecycle methods react/no-unsafe: error ## Prevent definitions of unused prop types - react/no-unused-prop-types: error + # react/no-unused-prop-types: error ## Prevent definitions of unused state properties react/no-unused-state: error ## Prevent usage of setState in componentWillUpdate @@ -763,3 +764,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/docs/component-reference.md b/tgui/docs/component-reference.md index 418c2323ca91c..f29b6d42914e8 100644 --- a/tgui/docs/component-reference.md +++ b/tgui/docs/component-reference.md @@ -359,15 +359,14 @@ and displays selected entry. - See inherited props: [Box](#box) - See inherited props: [Icon](#icon) -- `options: string[]` - An array of strings which will be displayed in the -dropdown when open -- `selected: string` - Currently selected entry -- `width: number` - Width of dropdown button and resulting menu +- `options: string[] | DropdownEntry[]` - An array of strings which will be displayed in the +dropdown when open. See Dropdown.tsx for more adcanced usage with DropdownEntry +- `selected: any` - Currently selected entry +- `width: string` - Width of dropdown button and resulting menu; css width value - `over: boolean` - Dropdown renders over instead of below - `color: string` - Color of dropdown button - `nochevron: boolean` - Whether or not the arrow on the right hand side of the dropdown button is visible -- `noscroll: boolean` - Whether or not the dropdown menu should have a scroll bar -- `displayText: string` - Text to always display in place of the selected text +- `displayText: string | number | InfernoNode` - Text to always display in place of the selected text - `onClick: (e) => void` - Called when dropdown button is clicked - `onSelected: (value) => void` - Called when a value is picked from the list, `value` is the value that was picked diff --git a/tgui/docs/tgui-for-custom-html-popups.md b/tgui/docs/tgui-for-custom-html-popups.md index 97eaf20446e5c..2c0a73411ed3c 100644 --- a/tgui/docs/tgui-for-custom-html-popups.md +++ b/tgui/docs/tgui-for-custom-html-popups.md @@ -35,11 +35,11 @@ window.close() ## Sending assets -TGUI in /tg/station codebase has `/datum/asset`, that packs scripts and stylesheets for delivery via CDN for efficiency. TGUI internally uses this asset system to render TGUI interfaces *proper* and TGUI chat. This is a snippet from internal TGUI code: +TGUI in /tg/station codebase has `/datum/asset`, that packs scripts and stylesheets for delivery via CDN for efficiency. TGUI internally uses this asset system to render TGUI interfaces _proper_ and TGUI chat. This is a snippet from internal TGUI code: ```dm window.initialize( - fancy = user.client.prefs.read_preference( + fancy = user.client.prefs.read_player_preference( /datum/preference/toggle/tgui_fancy ), assets = list( @@ -64,8 +64,8 @@ Finally, you can use the `Byond` API object to load JS and CSS files directly vi ```html ``` @@ -91,7 +91,7 @@ window.initialize( ) ``` -If you need to inline multiple JS or CSS files, you can concatenate them for now, and separate contents of each file with an `\n` symbol. *This can be a point of improvement (add support for file lists)*. +If you need to inline multiple JS or CSS files, you can concatenate them for now, and separate contents of each file with an `\n` symbol. _This can be a point of improvement (add support for file lists)_. ## Fancy mode @@ -134,7 +134,7 @@ You can think of it in these terms: Of course we're not working with functions here, but hopefully this analogy makes the concept easier to understand. -Finally, message can contain custom properties, and how you use them is *completely up to you*. They have an important limitation - all additional properties are string-typed, and require you to use a slightly more verbose API for sending them (more about it in the next section). +Finally, message can contain custom properties, and how you use them is _completely up to you_. They have an important limitation - all additional properties are string-typed, and require you to use a slightly more verbose API for sending them (more about it in the next section). ```js Byond.sendMessage({ @@ -209,8 +209,8 @@ You can send messages with custom fields in case if you want to bypass JSON seri ```js Byond.sendMessage({ - type: "something", - ref: "[0x12345678]", + type: 'something', + ref: '[0x12345678]', }); ``` diff --git a/tgui/package.json b/tgui/package.json index a613508b7993b..4893883c3847a 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -44,6 +44,7 @@ "eslint-config-prettier": "^8.8.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-sonarjs": "^0.18.0", + "eslint-plugin-unused-imports": "^2.0.0", "file-loader": "^6.2.0", "inferno": "^8.2.1", "jest": "^29.5.0", diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts index 8c385f2aaa055..7f5f1c0572366 100644 --- a/tgui/packages/common/collections.ts +++ b/tgui/packages/common/collections.ts @@ -69,6 +69,23 @@ export const map: MapFunction = 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; @@ -122,6 +139,8 @@ export const sortBy = return values; }; +export const sortStrings = sortBy(); + /** * * returns a range of numbers from start to end, exclusively. @@ -235,3 +254,42 @@ export const zipWith = (...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 0000000000000..bc41757515b08 --- /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 0000000000000..2f218388d3648 --- /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 0000000000000..6dc3c9a718a79 --- /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 0000000000000..9aaec674bbbc2 --- /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 0000000000000..c1f6ceee3fc34 --- /dev/null +++ b/tgui/packages/tgfont/icons/prosthetic-leg.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/tgui/packages/tgui/assets.ts b/tgui/packages/tgui/assets.ts index beb2f03a52a50..7ae36cb126e9f 100644 --- a/tgui/packages/tgui/assets.ts +++ b/tgui/packages/tgui/assets.ts @@ -5,7 +5,7 @@ */ const EXCLUDED_PATTERNS = [/v4shim/i]; -const loadedMappings = {}; +export const loadedMappings = {}; export const resolveAsset = (name) => loadedMappings[name] || name; diff --git a/tgui/packages/tgui/components/Box.tsx b/tgui/packages/tgui/components/Box.tsx index 9a3675da1716f..c44f60acfc1f7 100644 --- a/tgui/packages/tgui/components/Box.tsx +++ b/tgui/packages/tgui/components/Box.tsx @@ -10,7 +10,7 @@ import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags'; import { CSS_COLORS } from '../constants'; import type { Inferno, InfernoNode } from 'inferno'; -export interface BoxProps { +export type BoxProps = { [key: string]: any; as?: string; className?: string | BooleanLike; @@ -57,7 +57,7 @@ export interface BoxProps { textColor?: string | BooleanLike; backgroundColor?: string | BooleanLike; fillPositionedParent?: boolean; -} +}; /** * Coverts our rem-like spacing unit into a CSS unit. @@ -238,7 +238,7 @@ export const computeBoxClassName = (props: BoxProps) => { return classes([isColorClass(color) && 'color-' + color, isColorClass(backgroundColor) && 'color-bg-' + backgroundColor]); }; -export const Box = (props: BoxProps) => { +export const Box: Inferno.SFC = (props: BoxProps) => { const { as = 'div', className, children, ...rest } = props; // Render props if (typeof children === 'function') { diff --git a/tgui/packages/tgui/components/Button.js b/tgui/packages/tgui/components/Button.js index 1e9d981dc9580..a4662f3db63a5 100644 --- a/tgui/packages/tgui/components/Button.js +++ b/tgui/packages/tgui/components/Button.js @@ -35,6 +35,7 @@ export const Button = (props) => { onclick, onClick, verticalAlignContent, + captureKeys, ...rest } = props; const hasContent = !!(content || children); @@ -77,6 +78,10 @@ export const Button = (props) => { ])} tabIndex={!disabled && '0'} onKeyDown={(e) => { + if (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/CollapsibleSection.js b/tgui/packages/tgui/components/CollapsibleSection.js index fc5e98b4a538f..d5441e589e9d5 100644 --- a/tgui/packages/tgui/components/CollapsibleSection.js +++ b/tgui/packages/tgui/components/CollapsibleSection.js @@ -3,8 +3,17 @@ import { Section } from './Section'; import { Button } from './Button'; export const CollapsibleSection = (props, context) => { - const { children, sectionKey, color, buttons = [], forceOpen = false, showButton = !forceOpen, ...rest } = props; - const [isOpen, setOpen] = useLocalState(context, `open_collapsible_${sectionKey}`, true); + const { + children, + startOpen = true, + sectionKey, + color, + buttons = [], + forceOpen = false, + showButton = !forceOpen, + ...rest + } = props; + const [isOpen, setOpen] = useLocalState(context, `open_collapsible_${sectionKey}`, startOpen); return (
{ - if (this.state.open) { - this.setOpen(false); - } - }; - } - - componentWillUnmount() { - window.removeEventListener('click', this.handleClick); - } - - setOpen(open) { - this.setState({ open: open }); - if (open) { - setTimeout(() => window.addEventListener('click', this.handleClick)); - this.menuRef.focus(); - } else { - window.removeEventListener('click', this.handleClick); - } - } - - setSelected(selected) { - this.setState({ - selected: selected, - }); - this.setOpen(false); - this.props.onSelected(selected); - } - - buildMenu() { - const { options = [] } = this.props; - const ops = options.map((option) => ( - { - this.setSelected(option); - }}> - {option} - - )); - return ops.length ? ops : 'No Options Found'; - } - - render() { - const { props } = this; - const { - icon, - iconRotation, - iconSpin, - color = 'default', - over, - noscroll, - nochevron, - width, - height, - onClick, - selected, - disabled, - displayText, - ...boxProps - } = props; - let { className, ...rest } = boxProps; - rest['height'] = null; - - const adjustedOpen = over ? !this.state.open : this.state.open; - - const menu = this.state.open ? ( -
{ - this.menuRef = menu; - }} - tabIndex="-1" - style={{ - 'width': width, - 'height': height, - }} - className={classes([(noscroll && 'Dropdown__menu-noscroll') || 'Dropdown__menu', over && 'Dropdown__over'])}> - {this.buildMenu()} -
- ) : null; - - return ( -
- { - if (disabled && !this.state.open) { - return; - } - this.setOpen(!this.state.open); - }}> - {icon && } - {displayText ? displayText : this.state.selected} - {!!nochevron || ( - - - - )} - - {menu} -
- ); - } -} diff --git a/tgui/packages/tgui/components/Dropdown.tsx b/tgui/packages/tgui/components/Dropdown.tsx new file mode 100644 index 0000000000000..53f2bdc53cae2 --- /dev/null +++ b/tgui/packages/tgui/components/Dropdown.tsx @@ -0,0 +1,391 @@ +import { createPopper, VirtualElement } from '@popperjs/core'; +import { classes } from 'common/react'; +import { Component, findDOMFromVNode, InfernoNode, render } from 'inferno'; +import { Box, BoxProps } from './Box'; +import { Button } from './Button'; +import { Icon } from './Icon'; +import { Stack } from './Stack'; + +export interface DropdownEntry { + displayText: string | number | InfernoNode; + value: string | number | Enumerator; +} + +export type DropdownRequiredProps = { + options: string[] | DropdownEntry[]; +}; + +export type DropdownOptionalProps = { + icon?: string; + iconRotation?: number; + clipSelectedText?: boolean; + width?: string; + menuWidth?: string; + over?: boolean; + color?: string; + nochevron?: boolean; + displayText?: string | number | InfernoNode; + onClick?: (event) => void; + // you freaks really are just doing anything with this shit + selected?: any; + onSelected?: (selected: any) => void; + buttons?: boolean; + displayHeight?: string; +}; + +export type DropdownUniqueProps = DropdownRequiredProps & DropdownOptionalProps; + +export type DropdownProps = BoxProps & DropdownUniqueProps; + +const DEFAULT_OPTIONS = { + placement: 'left-start', + modifiers: [ + { + name: 'eventListeners', + enabled: false, + }, + ], +}; +const NULL_RECT: DOMRect = { + width: 0, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0, + x: 0, + y: 0, + toJSON: () => null, +} as const; + +type DropdownState = { + selected?: string; + open: boolean; +}; + +const DROPDOWN_DEFAULT_CLASSNAMES = 'Layout Dropdown__menu'; +const DROPDOWN_SCROLL_CLASSNAMES = 'Layout Dropdown__menu-scroll'; + +export class Dropdown extends Component { + static renderedMenu: HTMLDivElement | undefined; + static singletonPopper: ReturnType | undefined; + static currentOpenMenu: Element | undefined; + static virtualElement: VirtualElement = { + getBoundingClientRect: () => Dropdown.currentOpenMenu?.getBoundingClientRect() ?? NULL_RECT, + }; + menuContents: any; + handleClick: any; + state: DropdownState = { + open: false, + }; + + constructor() { + super(); + + this.handleClick = () => { + if (this.state.open) { + this.setOpen(false); + } + }; + } + + getDOMNode() { + return findDOMFromVNode(this.$LI, true); + } + + componentDidMount() { + const domNode = this.getDOMNode(); + + if (!domNode) { + return; + } + } + + openMenu() { + let renderedMenu = Dropdown.renderedMenu; + if (renderedMenu === undefined) { + renderedMenu = document.createElement('div'); + renderedMenu.className = DROPDOWN_DEFAULT_CLASSNAMES; + document.body.appendChild(renderedMenu); + Dropdown.renderedMenu = renderedMenu; + } + + const domNode = this.getDOMNode()!; + Dropdown.currentOpenMenu = domNode; + + renderedMenu.scrollTop = 0; + renderedMenu.style.width = + this.props.menuWidth || + // Hack, but domNode should *always* be the parent control meaning it will have width + // @ts-ignore + `${domNode.offsetWidth}px`; + renderedMenu.style.opacity = '1'; + renderedMenu.style.pointerEvents = 'auto'; + + // ie hack + // ie has this bizarre behavior where focus just silently fails if the + // element being targeted "isn't ready" + // 400 is probably way too high, but the lack of hotloading is testing my + // patience on tuning it + // I'm beyond giving a shit at this point it fucking works whatever + setTimeout(() => { + Dropdown.renderedMenu?.focus(); + }, 400); + this.renderMenuContent(); + } + + closeMenu() { + if (Dropdown.currentOpenMenu !== this.getDOMNode()) { + return; + } + + Dropdown.currentOpenMenu = undefined; + Dropdown.renderedMenu!.style.opacity = '0'; + Dropdown.renderedMenu!.style.pointerEvents = 'none'; + } + + componentWillUnmount() { + this.closeMenu(); + this.setOpen(false); + } + + renderMenuContent() { + const renderedMenu = Dropdown.renderedMenu; + if (!renderedMenu) { + return; + } + if (renderedMenu.offsetHeight > 200) { + renderedMenu.className = DROPDOWN_SCROLL_CLASSNAMES; + } else { + renderedMenu.className = DROPDOWN_DEFAULT_CLASSNAMES; + } + + const { options = [] } = this.props; + const ops = options.map((option) => { + let value, displayText; + + if (typeof option === 'string') { + displayText = option; + value = option; + } else if (option !== null) { + displayText = option.displayText; + value = option.value; + } + + return ( +
{ + this.setSelected(value); + }}> + {displayText} +
+ ); + }); + + const to_render = ops.length ? ops : 'No Options Found'; + + render( +
{to_render}
, + renderedMenu, + () => { + let singletonPopper = Dropdown.singletonPopper; + if (singletonPopper === undefined) { + singletonPopper = createPopper(Dropdown.virtualElement, renderedMenu!, { + ...DEFAULT_OPTIONS, + placement: 'bottom-start', + }); + + Dropdown.singletonPopper = singletonPopper; + } else { + singletonPopper.setOptions({ + ...DEFAULT_OPTIONS, + placement: 'bottom-start', + }); + + singletonPopper.update(); + } + }, + this.context + ); + } + + setOpen(open: boolean) { + this.setState((state) => ({ + ...state, + open, + })); + if (open) { + setTimeout(() => { + this.openMenu(); + window.addEventListener('click', this.handleClick); + }); + } else { + this.closeMenu(); + window.removeEventListener('click', this.handleClick); + } + } + + setSelected(selected: string) { + this.setState((state) => ({ + ...state, + selected, + })); + this.setOpen(false); + if (this.props.onSelected) { + this.props.onSelected(selected); + } + } + + getOptionValue(option): string { + return typeof option === 'string' ? option : option.value; + } + + getSelectedIndex(): number { + const selected = this.state.selected || this.props.selected; + const { options = [] } = this.props; + + return options.findIndex((option) => { + return this.getOptionValue(option) === selected; + }); + } + + toPrevious(): void { + const selectedIndex = this.getSelectedIndex(); + + if (selectedIndex < 0) { + return; + } + + const endIndex = this.props.options.length - 1; + const previousIndex = selectedIndex === 0 ? endIndex : selectedIndex - 1; + + this.setSelected(this.getOptionValue(this.props.options[previousIndex])); + } + + toNext(): void { + const selectedIndex = this.getSelectedIndex(); + + if (selectedIndex < 0) { + return; + } + + const endIndex = this.props.options.length - 1; + const nextIndex = selectedIndex === endIndex ? 0 : selectedIndex + 1; + + this.setSelected(this.getOptionValue(this.props.options[nextIndex])); + } + + render() { + const { props } = this; + const { + icon, + iconRotation, + iconSpin, + clipSelectedText = true, + color = 'default', + dropdownStyle, + over, + nochevron, + width, + onClick, + onSelected, + selected, + disabled, + displayText, + displayHeight, + buttons, + ...boxProps + } = props; + const { className, ...rest } = boxProps; + + const adjustedOpen = over ? !this.state.open : this.state.open; + + return ( + + + { + if (disabled && !this.state.open) { + return; + } + this.setOpen(!this.state.open); + if (onClick) { + onClick(event); + } + }} + {...rest}> + {icon && } + + {displayText || this.state.selected} + + {nochevron || ( + + + + )} + + + {buttons && ( + <> + + diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx new file mode 100644 index 0000000000000..96a36ceeafe92 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx @@ -0,0 +1,322 @@ +import { classes } from 'common/react'; +import { useBackend, useLocalState } from '../../backend'; +import { Box, Button, Flex, Section, Stack, Tooltip, Divider, Input, Icon } from '../../components'; +import { PreferencesMenuData } from './data'; +import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; +import { AntagonistData } from './data'; +import { createSearch } from 'common/string'; + +const AntagSelection = ( + props: { + antagonists: AntagonistData[]; + name: string; + }, + context +) => { + const { act, data } = useBackend(context); + const className = 'PreferencesMenu__Antags__antagSelection'; + + const enableAntagsGlobal = (antags: string[]) => { + act('set_antags', { + antags, + toggled: true, + character: false, + }); + }; + + const disableAntagsGlobal = (antags: string[]) => { + act('set_antags', { + antags, + toggled: false, + character: false, + }); + }; + + const enableAntagsCharacter = (antags: string[]) => { + act('set_antags', { + antags, + toggled: true, + character: true, + }); + }; + + const disableAntagsCharacter = (antags: string[]) => { + act('set_antags', { + antags, + toggled: false, + character: true, + }); + }; + + const isSelectedGlobal = (antag: string) => { + return data.enabled_global?.includes(antag); + }; + + const isSelectedCharacter = (antag: string) => { + return data.enabled_character?.includes(antag); + }; + + const antagonistKeys = props.antagonists.map((antagonist) => antagonist.path); + + return ( +
+
+ }> + C + + + + ) : null} + ( +
+ {text} + {index !== values.length - 1 && } +
+ ))} + position="bottom"> + { + if (isSelectedGlobal(antagonist.path)) { + disableAntagsGlobal([antagonist.path]); + } else { + enableAntagsGlobal([antagonist.path]); + } + }}> + + + {isBanned && ( + <> + + Banned + + + + )} + + {hoursLeft > 0 && ( + + {hoursLeft} +
+ hours left +
+ )} +
+
+ + + + ); + })} + + + ); +}; + +export const AntagsPage = (_, context) => { + let [searchText, setSearchText] = useLocalState(context, 'antag_search', ''); + let search = createSearch(searchText, (antagonist: AntagonistData) => { + return antagonist.name; + }); + return ( + { + if (!serverData) { + return Loading loadout data...; + } + const { antagonists = [], categories = [] } = serverData.antags; + return ( + + antag.path)} + /> + {searchText !== '' ? ( + + ) : ( + categories.map((category) => ( + a.category === category)!} + /> + )) + )} + + ); + }} + /> + ); +}; + +const SearchBar = ({ searchText, setSearchText, allAntags }, context) => { + const { act } = useBackend(context); + const enableAntags = (character: boolean) => { + act('set_antags', { + antags: allAntags, + toggled: true, + character, + }); + }; + + const disableAntags = (character: boolean) => { + act('set_antags', { + antags: allAntags, + toggled: false, + character, + }); + }; + return ( +
+ + + + setSearchText(value)} /> + + + + + + + + + + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx new file mode 100644 index 0000000000000..0a6d651e9c3b8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx @@ -0,0 +1,174 @@ +import { exhaustiveCheck } from 'common/exhaustive'; +import { useBackend, useLocalState } from '../../backend'; +import { Button, Flex, Stack, Divider } 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'; +import { LoadoutPage } from './LoadoutPage'; +import { BooleanLike } from 'common/react'; +import { SaveStatus } from './SaveStatus'; + +enum Page { + Antags, + Main, + Jobs, + Species, + Quirks, + Loadout, +} + +const CharacterProfiles = (props: { + activeSlot: number; + maxSlot: number; + onClick: (index: number) => void; + profiles: (string | null)[]; + content_unlocked: BooleanLike; +}) => { + const { profiles } = props; + + return ( + + {profiles.map((profile, slot) => ( + + + + ))} + + ); +}; + +export const CharacterPreferenceWindow = (props, context) => { + const { act, data } = useBackend(context); + const [currentPage, setCurrentPage] = useLocalState(context, 'currentPage_character', 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; + case Page.Loadout: + pageContents = ; + break; + default: + exhaustiveCheck(currentPage); + } + + return ( + + + ); + + 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 ( + + ); +}; + +export class KeybindingsPage extends Component<{}, KeybindingsPageState> { + cancelNextKeyUp?: number; + keybindingOnClicks: Record void)[]> = {}; + lastKeybinds?: PreferencesMenuData['keybindings']; + state: KeybindingsPageState = { + lastKeyboardEvent: undefined, + keybindings: undefined, + selectedKeybindings: undefined, + rebindingHotkey: undefined, + error: false, + }; + + 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, + error: false, + 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(); + } + + if (this.state === null) { + return; + } + + const { lastKeyboardEvent, rebindingHotkey } = this.state; + + if (rebindingHotkey && lastKeyboardEvent) { + this.setRebindingHotkey(formatKeyboardEvent(lastKeyboardEvent)); + } + } + + 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 { + if (!this.state) { + return; + } + 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')) + .then((response) => response.json()) + .catch((err) => { + this.setState({ + error: err, + }); + }); + const keybindingsData: Keybindings = await keybindingsResponse; + + 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() { + if (this.state && this.state.error !== false) { + return ( + + Error: Unable to fetch keybinding data. +
+ Contact a maintainer or create an issue report by pressing Report Issue in the top right of the game window. +
+ + Error Details:{'\n'} + {typeof this.state.error === 'object' && Object.keys(this.state.error).includes('stack') + ? this.state.error.stack + : this.state.error.toString()} + +
+ ); + } + const { act } = useBackend(this.context); + 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) => ( + + + + ))} + + + + + + + ); + })} + , + ]; + })} + /> + + + + act('reset_all_keybinds')} /> + + + + ); + } +} diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/LoadoutPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/LoadoutPage.tsx new file mode 100644 index 0000000000000..380645842b35b --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/LoadoutPage.tsx @@ -0,0 +1,255 @@ +import { Box, Tabs, Button, Tooltip, Stack, Flex, Table, Section, Icon, Input } from '../../components'; +import { LoadoutGear, PreferencesMenuData } from './data'; +import { useBackend, useLocalState } from '../../backend'; +import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; +import { CharacterPreview } from './CharacterPreview'; +import { createSearch } from 'common/string'; + +const isPurchased = (purchased_gear: string[], gear: LoadoutGear) => purchased_gear.includes(gear.id) && !gear.multi_purchase; + +export const LoadoutPage = (props, context) => { + const { act, data } = useBackend(context); + const { purchased_gear = [], metacurrency_balance = 0, is_donator = false } = data; + + return ( + { + if (!serverData) { + return Loading loadout data...; + } + const { categories = [], metacurrency_name } = serverData.loadout; + const [selectedCategory, setSelectedCategory] = useLocalState(context, 'category', categories[0].name); + let [searchText, setSearchText] = useLocalState(context, 'loadout_search', ''); + let search = createSearch(searchText, (gear: LoadoutGear) => { + return gear.display_name + ' ' + gear.skirt_display_name + ' ' + gear.allowed_roles?.join(' '); + }); + + let selectedCategoryObject = categories.filter((c) => c.name === selectedCategory)[0]; + let currency_text = metacurrency_balance.toLocaleString() + ' ' + metacurrency_name + 's'; + const showRoles = + !selectedCategoryObject || selectedCategoryObject.gear.filter((g) => g.allowed_roles?.length).length > 0; + + return ( + + + + + + + + + + {Object.keys(catalog.icons).length > 5 && ( + + + + setSearchText(value)} + /> + + + )} + + + + {Object.entries(catalog.icons) + .filter(([n, _]) => searchText?.length < 1 || search(n)) + .map(([name, image], index) => { + return ( + + + + + {name} + + + + ); + })} + + + {supplementalFeature && !use_small_supplemental && ( + <> + + + Select {features[supplementalFeature].name} + + + + + + + )} + + + + ); +}; + +const GenderButton = ( + props: { + handleSetGender: (gender: Gender) => void; + gender: Gender; + }, + context +) => { + const [genderMenuOpen, setGenderMenuOpen] = useLocalState(context, 'genderMenuOpen', false); + + return ( + setGenderMenuOpen(false)} removeOnOutsideClick> + + + {[Gender.Male, Gender.Female, Gender.Other].map((gender) => { + return ( + + + + {catalog.name} + + + ); +}; + +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.body_is_always_random !== 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['name_is_always_random']; + } + + 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', { direction: direction }); + }} + setGender={createSetPreference(act, 'gender')} + showGender={currentSpeciesData ? !!currentSpeciesData.sexes : true} + /> + + + + + + + + { + 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 0000000000000..19e3f0ce58a41 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/PageButton.tsx @@ -0,0 +1,21 @@ +import type { 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 ( + + ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx new file mode 100644 index 0000000000000..ba10deaf2c164 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx @@ -0,0 +1,316 @@ +import type { Inferno } from 'inferno'; +import { Box, Icon, Stack, Tooltip } from '../../components'; +import { PreferencesMenuData, Quirk } from './data'; +import { useBackend, useLocalState } from '../../backend'; +import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; +import { logger } from 'tgui/logging'; + +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'; + if (!quirk.icon) { + logger.info(quirk.name); + } + + const child = ( + { + props.onClick(quirkKey, quirk); + }}> + + + {quirk.icon && } + + + + + + + + + + {quirk.name} + + + + {quirk.value} + + + + + + {quirk.description} + + + + + + ); + + if (quirk.failTooltip) { + return ( + + {child} + + ); + } else { + return child; + } + })} + + ); +}; + +const StatDisplay: Inferno.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 0000000000000..fb8749f83beb1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/RandomizationButton.tsx @@ -0,0 +1,53 @@ +import { Dropdown, Icon } from '../../components'; +import { RandomSetting } from './data'; + +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; + } + + 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} + menuWidth="120px" + width="auto" + /> + ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/SaveStatus.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/SaveStatus.tsx new file mode 100644 index 0000000000000..b4c4cf3e9f6df --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/SaveStatus.tsx @@ -0,0 +1,64 @@ +import { Box, Tooltip } from '../../components'; +import { PreferencesMenuData } from './data'; +import { useBackend } from '../../backend'; + +export const SaveStatus = (props, context) => { + const { data } = useBackend(context); + const { save_in_progress = false, is_db = true, is_guest = false, save_sucess = true } = data; + const innerBox = ( + + {!is_db ? No DB : is_guest ? Guest : null} + {!is_guest && is_db ? ( + save_in_progress ? ( + + Saving + . + . + . + + ) : ( + {save_sucess ? 'Saved' : 'Error'} + ) + ) : null} + + ); + if (!is_db || is_guest) { + return ( + + {innerBox} + + ); + } + if (!save_in_progress && !save_sucess) { + return ( + + {innerBox} + + ); + } + if (save_in_progress) { + return ( + + {innerBox} + + ); + } + return innerBox; +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx new file mode 100644 index 0000000000000..beef2520f6782 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx @@ -0,0 +1,81 @@ +import { Component } from 'inferno'; +import type { InfernoNode } from 'inferno'; +import { loadedMappings, resolveAsset } from '../../assets'; +import { fetchRetry } from '../../http'; +import { ServerData } from './data'; +import { Dimmer, Box } from '../../components'; + +// Cache response so it's only sent once +let fetchServerData: Promise | undefined; +let lastError: any = null; + +export class ServerPreferencesFetcher extends Component< + { + render: (serverData: ServerData | undefined) => InfernoNode; + }, + { + serverData?: ServerData; + errored: boolean; + } +> { + constructor() { + super(); + this.state = { + serverData: undefined, + errored: false, + }; + } + + componentDidMount() { + this.populateServerData(); + } + + async populateServerData() { + if (!fetchServerData) { + fetchServerData = fetchRetry(resolveAsset('preferences.json')) + .then((response) => response.json()) + .catch((err) => { + this.setState({ + errored: true, + }); + lastError = err; + }); + } + + const preferencesData: ServerData = await fetchServerData; + + this.setState({ + serverData: preferencesData, + }); + } + + render() { + return this.state !== null && this.state.serverData !== null && this.state.errored === false && lastError === null ? ( + this.props.render(this.state.serverData) + ) : lastError !== null ? ( + + Error: Unable to fetch preferences clientside data. +
+ (Your character data is OK, this is a UI error) +
+ Contact a maintainer or create an issue report by pressing Report Issue in the top right of the game window. +
+ + Error Details:{'\n'} + {typeof lastError === 'object' && Object.keys(lastError).includes('stack') ? lastError.stack : lastError.toString()} + {'\n'} + Asset Mappings: {JSON.stringify(loadedMappings, null, 2)} + +
+ ) : ( + 'Loading...' + ); + } +} diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/SpeciesPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/SpeciesPage.tsx new file mode 100644 index 0000000000000..a523663a381c9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/SpeciesPage.tsx @@ -0,0 +1,362 @@ +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, Perk, PreferencesMenuData, ServerData, Species } from './data'; +import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; + +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: { diet: Species['diet'] }) => { + if (!props.diet) { + return null; + } + + const { liked_food, disliked_food, toxic_food } = props.diet; + + return ( + + + + + + + + + + + + + + ); +}; + +const SpeciesPerk = (props: { className: string; perk: Perk }) => { + const { className, perk } = props; + + return ( + + {perk.name} + + {perk.description} + + }> + + + + + ); +}; + +const SpeciesPerks = (props: { perks: Species['perks'] }) => { + const { positive, negative, neutral } = props.perks; + + return ( + + + + {positive.map((perk) => { + return ( + + + + ); + })} + + + + + {neutral.map((perk) => { + return ( + + + + ); + })} + + + + {negative.map((perk) => { + return ( + + + + ); + })} + + + ); +}; + +const SpeciesPageInner = ( + props: { + handleClose: () => void; + species: ServerData['species']; + }, + context +) => { + const { act, data } = useBackend(context); + const setSpecies = createSetPreference(act, 'species'); + + let species: [string, Species][] = Object.entries(props.species).map(([species, data]) => { + return [species, data]; + }); + + // 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]; + + let selectableSpecies: [string, Species][] = species.filter(([_, s]) => s.selectable); + + return ( + + + + + ); + })} + + + )} + + + + {this.props.categoryEntries.map(([category, children]) => { + return ( + + + {children} + + + ); + })} + + + + ); + } +} diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts new file mode 100644 index 0000000000000..b87cfd8c1e22a --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts @@ -0,0 +1,236 @@ +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 Species = { + name: string; + desc: string; + lore?: string[]; + icon: string; + + use_skintones: BooleanLike; + sexes: BooleanLike; + + enabled_features: string[]; + selectable: BooleanLike; + + perks: { + positive: Perk[]; + negative: Perk[]; + neutral: Perk[]; + }; + + diet?: { + liked_food: Food[]; + disliked_food: Food[]; + toxic_food: Food[]; + }; +}; + +export type Perk = { + ui_icon: string; + name: string; + description: string; +}; + +export type Department = { + head?: string; +}; + +export type Job = { + description: string; + department: string; +}; + +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 type LoadoutInfo = { + categories: LoadoutCategory[]; + purchased_gear: string[]; + equipped_gear: string[]; + metacurrency_name: string; +}; + +export type LoadoutGear = { + id: string; + display_name: string; + skirt_display_name: string | null; + description: string; + skirt_description: string | null; + donator: BooleanLike; + cost: number; + allowed_roles: string[] | null; + is_equippable: BooleanLike; + multi_purchase: BooleanLike; +}; + +export type LoadoutCategory = { + name: string; + gear: LoadoutGear[]; +}; + +export type AntagonistData = { + name: string; + description: string; + category: string; + per_character: BooleanLike; + path: string; + icon_path: string; + ban_key?: 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)[]; + + character_preferences: { + clothing: Record; + features: Record; + game_preferences: Record; + non_contextual: { + body_is_always_random: 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< + string, + { + experience_type: string; + required_playtime: number; + } + >; + job_preferences: Record; + + keybindings: Record; + overflow_role: string; + selected_quirks: string[]; + + purchased_gear: string[]; + equipped_gear: string[]; + metacurrency_balance: number; + is_donator: BooleanLike; + + antag_bans?: string[]; + antag_living_playtime_hours_left?: Record; + enabled_global: string[]; + enabled_character: string[]; + + active_slot: number; + max_slot: number; + name_to_use: string; + save_in_progress: BooleanLike; + is_guest: BooleanLike; + is_db: BooleanLike; + save_sucess: BooleanLike; + + window: Window; +}; + +export type ServerData = { + antags: { + antagonists: AntagonistData[]; + categories: string[]; + }; + jobs: { + departments: Record; + jobs: Record; + }; + names: { + types: Record; + }; + quirks: QuirkInfo; + loadout: LoadoutInfo; + 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 0000000000000..197a940d3a140 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/index.tsx @@ -0,0 +1,22 @@ +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/names.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/names.tsx new file mode 100644 index 0000000000000..8ba65e1afb86a --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/names.tsx @@ -0,0 +1,235 @@ +import { binaryInsertWith, sortBy } from 'common/collections'; +import { useLocalState } from '../../backend'; +import { 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 ( + + +

+ Close + + } + title="All Names"> + + {sortNameWithKeyEntries(Object.entries(namesIntoGroups)).map(([_, names], index, collection) => ( + <> + {names.map(({ key, name }) => { + let content; + + if (currentlyEditingName === key) { + const updateName = (event, value) => { + props.handleUpdateName(key, value); + + setCurrentlyEditingName(null); + }; + + content = ( + { + setCurrentlyEditingName(null); + }} + value={props.names[key]} + /> + ); + } else { + content = ( + + ); + } + + return ( + + + {content} + + {!!name.can_randomize && ( + +
+ + + ); + }} + /> + ); +}; + +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 ( + + + ) : 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 0000000000000..21ea616c4915f --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx @@ -0,0 +1,486 @@ +import { sortBy, sortStrings } from 'common/collections'; +import { BooleanLike, classes } from 'common/react'; +import { createComponentVNode } from 'inferno'; +import type { InfernoNode, ComponentType } from 'inferno'; +import { VNodeFlags } from 'inferno-vnode-flags'; +import { sendAct, useBackend, useLocalState } from '../../../../backend'; +import { Box, Button, Dropdown, Input, NumberInput, Stack, Flex, Tooltip } from '../../../../components'; +import { createSetPreference, PreferencesMenuData } from '../../data'; +import { ServerPreferencesFetcher } from '../../ServerPreferencesFetcher'; +import features from '.'; +import { DropdownOptionalProps } from 'tgui/components/Dropdown'; + +export const sortChoices = sortBy<[string, InfernoNode]>(([name]) => name); + +export type Feature = { + name: string; + component: FeatureValue; + category?: string; + subcategory?: string; + description?: string; + predictable?: boolean; + small_supplemental?: boolean; +}; + +/** + * 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 = ComponentType< + FeatureValueProps +>; + +export type FeatureValueProps = { + act: typeof sendAct; + featureId: string; + handleSetValue: (newValue: TSending) => void; + serverData: TServerData | undefined; + shrink?: boolean; + value?: TReceiving; +}; + +export const FeatureColorInput = (props: FeatureValueProps) => { + return ( + + ); +}; + +export type FeatureToggle = Feature; + +export const TextInput = (props: FeatureValueProps) => { + return props.handleSetValue(newValue)} width="100%" />; +}; + +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?: DropdownOptionalProps +): FeatureValue => { + return (props: FeatureValueProps) => { + return ( + { + return { + displayText: label, + value: dataValue, + }; + })} + {...dropdownProps} + /> + ); + }; +}; + +export type FeatureChoicedServerData = { + choices: string[]; + display_names?: Record; + icons?: Record; + icon_sheet?: string; +}; + +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; + buttons?: boolean; + displayHeight?: string; +}) => { + const { choices, disabled, buttons, displayNames, onSetValue, displayHeight, value } = props; + + return ( + { + return { + displayText: displayNames[choice], + value: choice, + }; + })} + /> + ); +}; + +export const FeatureButtonedDropdownInput = ( + props: FeatureValueProps & { + disabled?: boolean; + } +) => { + return ; +}; + +export const FeatureDropdownInput = ( + props: FeatureValueProps & { + disabled?: boolean; + buttons?: boolean; + } +) => { + const serverData = props.serverData; + if (!serverData) { + return null; + } + + const displayNames = + serverData.display_names || Object.fromEntries(serverData.choices.map((choice) => [choice, capitalizeFirstLetter(choice)])); + + return serverData.choices.length > 5 ? ( + + ) : ( + + ); +}; + +export const FeatureIconnedDropdownInput = ( + props: FeatureValueProps & { + buttons?: boolean; + } +) => { + 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 const StandardizedChoiceButtons = (props: { + choices: string[]; + disabled?: boolean; + displayNames: Record; + onSetValue: (newValue: string) => void; + value?: string; +}) => { + const { choices, disabled, displayNames, onSetValue, value } = props; + return ( + <> + {choices.map((choice) => ( + + + + + + + + ) : ( + + + + )} + + ); + }, +}; + +export const name_is_always_random: 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/gender.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/gender.ts new file mode 100644 index 0000000000000..d757c47aa4874 --- /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/useRandomToggleState.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/useRandomToggleState.ts new file mode 100644 index 0000000000000..67d0319503778 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/useRandomToggleState.ts @@ -0,0 +1,3 @@ +import { useLocalState } from '../../backend'; + +export const useRandomToggleState = (context) => useLocalState(context, 'randomToggle', false); diff --git a/tgui/packages/tgui/interfaces/ScannerGate.js b/tgui/packages/tgui/interfaces/ScannerGate.js index 9bc0bb8f7cb54..a175638a59b08 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, NumberInput } from '../components'; import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox'; import { Window } from '../layouts'; diff --git a/tgui/packages/tgui/interfaces/Telecomms.js b/tgui/packages/tgui/interfaces/Telecomms.js index 24a6a169bed80..f52a570065100 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/layouts/Window.js b/tgui/packages/tgui/layouts/Window.js index b0707ffb9a3d8..7960006f2bab0 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 652cee92877c3..08fd430fb2757 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/Tooltip.stories.js b/tgui/packages/tgui/stories/Tooltip.stories.js index bba37c417bac9..306929ba767a5 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 0000000000000..cce5bfdf2c110 --- /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 0000000000000..1d49e13c8e35b --- /dev/null +++ b/tgui/packages/tgui/styles/atomic/fit-text.scss @@ -0,0 +1,14 @@ +$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/atomic/loading.scss b/tgui/packages/tgui/styles/atomic/loading.scss new file mode 100644 index 0000000000000..7dbc670cc1969 --- /dev/null +++ b/tgui/packages/tgui/styles/atomic/loading.scss @@ -0,0 +1,35 @@ +.loading-one { + opacity: 0; + animation: dot 0.6s infinite; + animation-delay: 0s; +} + +.loading-two { + opacity: 0; + animation: dot 0.6s infinite; + animation-delay: 0.1s; +} + +.loading-three { + opacity: 0; + animation: dot 0.6s infinite; + animation-delay: 0.2s; +} + +@-webkit-keyframes dot { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes dot { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/tgui/packages/tgui/styles/atomic/section-background.scss b/tgui/packages/tgui/styles/atomic/section-background.scss new file mode 100644 index 0000000000000..f93b5139cb5e4 --- /dev/null +++ b/tgui/packages/tgui/styles/atomic/section-background.scss @@ -0,0 +1,10 @@ +@use 'sass:color'; +@use '../base.scss'; +@use '../functions.scss'; + +$background-color: base.$color-bg-section !default; + +.section-background { + background-color: functions.fake-alpha($background-color, base.$color-bg); + background-color: $background-color; +} diff --git a/tgui/packages/tgui/styles/colors.scss b/tgui/packages/tgui/styles/colors.scss index aca122de1fc08..d1a9fc4fd9de9 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/ColorSelectBox.scss b/tgui/packages/tgui/styles/components/ColorSelectBox.scss new file mode 100644 index 0000000000000..06c9cabbe4a63 --- /dev/null +++ b/tgui/packages/tgui/styles/components/ColorSelectBox.scss @@ -0,0 +1,53 @@ +@use 'sass:color'; +@use '../base.scss'; +@use '../colors.scss'; +@use '../functions.scss' as *; + +$color-default: color.adjust(base.$color-bg, $lightness: 10%) !default; +$color-disabled: #0c0c0c !default; +$color-selected: colors.bg(colors.$green) !default; + +.ColorSelectBox { + display: inline-block; + box-sizing: content-box; + height: 11px; + width: 11px; + border: 2px solid $color-default; + + .ColorSelectBox--inner { + box-sizing: border-box; + width: 100%; + height: 100%; + border: 1px solid black; + background-color: black; + } + + &:hover { + transition: color 0ms, border-color 0ms; + } + + &:focus { + transition: color 100ms, border-color 100ms; + } + + &:hover, + &:focus { + border-color: lighten($color-default, 30%); + } +} + +.ColorSelectBox--selected { + border-color: $color-selected; + &:hover, + &:focus { + border-color: lighten($color-selected, 30%); + } +} + +.ColorSelectBox--disabled { + border-color: $color-disabled; + &:hover, + &:focus { + border-color: $color-disabled; + } +} diff --git a/tgui/packages/tgui/styles/components/Dropdown.scss b/tgui/packages/tgui/styles/components/Dropdown.scss index 66f357e9e1a82..bd670681e2be3 100644 --- a/tgui/packages/tgui/styles/components/Dropdown.scss +++ b/tgui/packages/tgui/styles/components/Dropdown.scss @@ -7,11 +7,12 @@ .Dropdown { position: relative; + align-items: center; } .Dropdown__control { - position: relative; display: inline-block; + align-items: center; font-family: Verdana, sans-serif; font-size: base.em(12px); width: base.em(100px); @@ -23,34 +24,24 @@ float: right; padding-left: 0.35em; width: 1.2em; - height: base.em(22px); + height: 100%; border-left: base.em(1px) solid #000; border-left: base.em(1px) solid rgba(0, 0, 0, 0.25); } .Dropdown__menu { - position: absolute; overflow-y: auto; + align-items: center; z-index: 5; - width: base.em(100px); max-height: base.em(200px); - overflow-y: scroll; border-radius: 0 0 base.em(2px) base.em(2px); color: #fff; background-color: #000; background-color: rgba(0, 0, 0, 0.75); } -.Dropdown__menu-noscroll { - position: absolute; - overflow-y: auto; - z-index: 5; - width: base.em(100px); - max-height: base.em(200px); - border-radius: 0 0 base.em(2px) base.em(2px); - color: #fff; - background-color: #000; - background-color: rgba(0, 0, 0, 0.75); +.Dropdown__menu-scroll { + overflow-y: scroll; } .Dropdown__menuentry { @@ -74,7 +65,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 411af74c9a13e..94adf932549d5 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--nowrap { diff --git a/tgui/packages/tgui/styles/interfaces/PreferencesMenu.scss b/tgui/packages/tgui/styles/interfaces/PreferencesMenu.scss new file mode 100644 index 0000000000000..553aefcea68b6 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/PreferencesMenu.scss @@ -0,0 +1,357 @@ +@use 'sass:color'; +@use 'sass:map'; +@use '../components/Button.scss'; +@use '../colors.scss'; + +$department_map: ( + 'Assistant': colors.$grey, + 'Captain': colors.fg(colors.$blue), + 'Cargo': colors.$brown, + 'Civilian': colors.$grey, + 'Command': colors.$yellow, + 'Security': colors.$red, + 'Engineering': #f1a839, + 'Medical': colors.$teal, + 'Science': colors.fg(colors.$purple), + 'Service': colors.$green, + 'Silicon': colors.$pink, +); + +.PreferencesMenu { + &__Main { + .Preferences__standard-palette { + .ColorSelectBox { + height: 1.35em !important; + width: 1.35em !important; + } + display: inline-block; + .Button { + height: 25px !important; + width: 25px !important; + line-height: 25px !important; + } + } + font-size: 1.35rem; + } + + &__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; + + &__per_character { + &--off { + .antagonist-icon-parent-per-character { + .antagonist-icon { + border-color: darken(colors.$red, 10%); + &:hover { + transition: border-color 0.1s ease-out; + border-color: darken(colors.$red, 5%); + } + } + } + } + &--on { + .antagonist-icon-parent-per-character { + .antagonist-icon { + border-color: darken(colors.$grey, 10%); + &:hover { + transition: border-color 0.1s ease-out; + border-color: darken(colors.$grey, 5%); + } + } + } + } + + .antagonist-icon-parent-per-character { + z-index: 1; + opacity: 0.9; + overflow: visible; + position: relative; + height: 0; + width: 0; + padding: 0; + margin: 0; + left: 74px; + bottom: -64px; + + .antagonist-icon { + border-style: solid; + border-radius: 50%; + border-width: 4px; + background-color: #222; + + box-sizing: content-box; + + height: 32px; + width: 32px; + text-align: center; + font-size: 20px; + vertical-align: middle; + line-height: 32px; + -ms-user-select: none; + user-select: none; + } + } + } + + .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; + } + } + } + + &--banned { + .antagonist-icon-parent { + border-color: colors.$grey; + color: color.adjust(colors.$red, $lightness: 20%); + .antagonist-icon { + opacity: 0.5; + } + } + } + } + + &--on { + @include animate-hover; + + .antagonist-icon-parent { + border-color: colors.$green; + } + + &--banned { + .antagonist-icon-parent { + border-color: colors.$grey; + color: color.adjust(colors.$green, $lightness: 40%); + .antagonist-icon { + opacity: 0.5; + } + } + } + } + + &--grey { + .antagonist-icon-parent { + border-color: colors.$grey; + color: inherit; + .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-overlay-text { + text-align: center; + text-shadow: 1px 1px 3px 2px #222; + font-size: 1.2rem; + z-index: 1; + + .antagonist-overlay-text-hours { + font-size: 1.5rem; + font-weight: bold; + } + + 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 { + &.head { + .job-name { + font-size: 1.5em; + } + } + + .job-name { + font-weight: bold; + } + } + } + + &__priority { + color: black; + border-left: 1px solid #222; + border-right: none; + border-top: none; + border-bottom: none; + border-radius: 0 !important; + text-shadow: 0 0 1px black; + &--off { + background-color: lighten(colors.$grey, 10%) !important; + border-color: lighten(colors.$grey, 20%); + border-left: none; + } + &--low { + background-color: colors.$red !important; + color: black !important; + text-shadow: none; + } + &--medium { + background-color: colors.$yellow !important; + color: black !important; + text-shadow: none; + } + &--high { + background-color: lighten(colors.$green, 10%) !important; + color: black !important; + text-shadow: none; + } + &--disabled { + background-color: #444 !important; + color: white !important; + transition: ease-out 0.25s background-color; + text-shadow: 0 0 1px black; + &:hover { + background-color: #666 !important; + } + } + } + } + + .job-name { + font-size: 1.25em; + padding: 3px; + } + } + + &__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/layouts/Layout.scss b/tgui/packages/tgui/styles/layouts/Layout.scss index ae2fea1a2e7a7..41c4394ff98a4 100644 --- a/tgui/packages/tgui/styles/layouts/Layout.scss +++ b/tgui/packages/tgui/styles/layouts/Layout.scss @@ -8,16 +8,20 @@ $scrollbar-color-multiplier: 1 !default; +@mixin fancy-scrollbar($base-color, $color-multiplier) { + scrollbar-base-color: color.scale($base-color, $lightness: -25% * $color-multiplier); + scrollbar-face-color: color.scale($base-color, $lightness: 10% * $color-multiplier); + scrollbar-3dlight-color: color.scale($base-color, $lightness: 0% * $color-multiplier); + scrollbar-highlight-color: color.scale($base-color, $lightness: 0% * $color-multiplier); + scrollbar-track-color: color.scale($base-color, $lightness: -25% * $color-multiplier); + scrollbar-arrow-color: color.scale($base-color, $lightness: 50% * $color-multiplier); + scrollbar-shadow-color: color.scale($base-color, $lightness: 10% * $color-multiplier); +} + .Layout, .Layout * { // Fancy scrollbar - scrollbar-base-color: color.scale(base.$color-bg, $lightness: -25% * $scrollbar-color-multiplier); - scrollbar-face-color: color.scale(base.$color-bg, $lightness: 10% * $scrollbar-color-multiplier); - scrollbar-3dlight-color: color.scale(base.$color-bg, $lightness: 0% * $scrollbar-color-multiplier); - scrollbar-highlight-color: color.scale(base.$color-bg, $lightness: 0% * $scrollbar-color-multiplier); - scrollbar-track-color: color.scale(base.$color-bg, $lightness: -25% * $scrollbar-color-multiplier); - scrollbar-arrow-color: color.scale(base.$color-bg, $lightness: 50% * $scrollbar-color-multiplier); - scrollbar-shadow-color: color.scale(base.$color-bg, $lightness: 10% * $scrollbar-color-multiplier); + @include fancy-scrollbar(base.$color-bg, $scrollbar-color-multiplier); } .Layout__content { diff --git a/tgui/packages/tgui/styles/layouts/PopupWindow.scss b/tgui/packages/tgui/styles/layouts/PopupWindow.scss new file mode 100644 index 0000000000000..0043308986478 --- /dev/null +++ b/tgui/packages/tgui/styles/layouts/PopupWindow.scss @@ -0,0 +1,17 @@ +@use 'sass:color'; +@use '../base.scss'; +@use '../functions.scss' as *; +@use './Layout.scss'; + +.PopupWindow { + color: base.$color-fg; + background-color: base.$color-bg; + background-image: linear-gradient(to bottom, base.$color-bg-start 0%, base.$color-bg-end 100%); + @include Layout.fancy-scrollbar(base.$color-bg, Layout.$scrollbar-color-multiplier); + border: 1px solid color.adjust(base.$color-bg, $lightness: 7%); + box-shadow: 1px 1px 5px 2px rgba(0, 0, 0, 0.5); +} + +.PopupWindow * { + @include Layout.fancy-scrollbar(base.$color-bg, Layout.$scrollbar-color-multiplier); +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 24dd721e797cd..d56528563dbad 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -11,15 +11,20 @@ // 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/loading.scss'); @include meta.load-css('./atomic/outline.scss'); +@include meta.load-css('./atomic/section-background.scss'); @include meta.load-css('./atomic/text.scss'); // Components @include meta.load-css('./components/BlockQuote.scss'); @include meta.load-css('./components/Button.scss'); @include meta.load-css('./components/ColorBox.scss'); +@include meta.load-css('./components/ColorSelectBox.scss'); @include meta.load-css('./components/Dimmer.scss'); @include meta.load-css('./components/Divider.scss'); @include meta.load-css('./components/Dropdown.scss'); @@ -50,6 +55,7 @@ @include meta.load-css('./interfaces/ModularFabricator.scss'); @include meta.load-css('./interfaces/OrbitalMap.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/IntegratedCircuit.scss'); @include meta.load-css('./interfaces/Techweb.scss'); @@ -61,6 +67,7 @@ @include meta.load-css('./layouts/Layout.scss'); @include meta.load-css('./layouts/NtosHeader.scss'); @include meta.load-css('./layouts/NtosWindow.scss'); +@include meta.load-css('./layouts/PopupWindow.scss'); @include meta.load-css('./layouts/TitleBar.scss'); @include meta.load-css('./layouts/Window.scss'); diff --git a/tgui/packages/tgui/styles/themes/generic-yellow.scss b/tgui/packages/tgui/styles/themes/generic-yellow.scss new file mode 100644 index 0000000000000..aaaa38af0a876 --- /dev/null +++ b/tgui/packages/tgui/styles/themes/generic-yellow.scss @@ -0,0 +1,53 @@ +@use 'sass:color'; +@use 'sass:meta'; + +$generic: #484455; +$accent: #4f56a5; +$accent-2: #ffbf00; + +@use '../colors.scss' with ( + $fg-map-keys: (), + $bg-map-keys: (), + $primary: $accent, +); +@use '../base.scss' with ( + $color-bg: color.scale($generic, $lightness: -45%), + $border-radius: 2px, +); + +.theme-generic-yellow { + // Components + @include meta.load-css( + '../components/Button.scss', + $with: ('color-default': $accent, 'color-transparent-text': rgba(227, 240, 255, 0.75)) + ); + @include meta.load-css( + '../components/ColorSelectBox.scss', + $with: ('color-default': color.scale($generic, $lightness: -20%)) + ); + @include meta.load-css( + '../components/ProgressBar.scss', + $with: ('color-default-fill': $accent, 'background-color': rgba(0, 0, 0, 0.5)) + ); + @include meta.load-css('../components/Section.scss'); + + @include meta.load-css('../components/Input.scss', $with: ('border-color': #7b86ff)); + + // Layouts + @include meta.load-css('../layouts/Layout.scss'); + @include meta.load-css('../layouts/Window.scss'); + @include meta.load-css( + '../layouts/TitleBar.scss', + $with: ( + 'background-color': color.scale($generic, $lightness: -50%), + 'shadow-color': #ffff0021, + 'shadow-color-core': $accent-2, + 'shadow-core-height': 3px + ) + ); + @include meta.load-css('../layouts/PopupWindow.scss'); + + .Layout__content { + background-image: url('../../assets/bg-beestation.svg'); + } +} diff --git a/tgui/packages/tgui/styles/themes/generic.scss b/tgui/packages/tgui/styles/themes/generic.scss index 4849a9380b021..9898860a0c8fa 100644 --- a/tgui/packages/tgui/styles/themes/generic.scss +++ b/tgui/packages/tgui/styles/themes/generic.scss @@ -38,6 +38,7 @@ $border-color: #7b86ff; @include meta.load-css('../layouts/Layout.scss'); @include meta.load-css('../layouts/Window.scss'); @include meta.load-css('../layouts/TitleBar.scss', $with: ('background-color': color.scale($generic, $lightness: -25%))); + @include meta.load-css('../layouts/PopupWindow.scss'); .Layout__content { background-image: none; diff --git a/tgui/yarn.lock b/tgui/yarn.lock index c5d58d887ac93..bd6fec9691afd 100644 --- a/tgui/yarn.lock +++ b/tgui/yarn.lock @@ -4926,6 +4926,28 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-unused-imports@npm:^2.0.0": + version: 2.0.0 + resolution: "eslint-plugin-unused-imports@npm:2.0.0" + dependencies: + eslint-rule-composer: ^0.3.0 + peerDependencies: + "@typescript-eslint/eslint-plugin": ^5.0.0 + eslint: ^8.0.0 + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + checksum: 8aa1e03e75da2a62a354065e0cb8fe370118c6f8d9720a32fe8c1da937de6adb81a4fed7d0d391d115ac9453b49029fb19f970d180a2cf3dba451fd4c20f0dc4 + 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": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -10452,6 +10474,7 @@ __metadata: eslint-config-prettier: ^8.8.0 eslint-plugin-react: ^7.32.2 eslint-plugin-sonarjs: ^0.18.0 + eslint-plugin-unused-imports: ^2.0.0 file-loader: ^6.2.0 inferno: ^8.2.1 jest: ^29.5.0