From f9052dbb607368657bdf2a875b514cdce08017dd Mon Sep 17 00:00:00 2001 From: ShadowLarkens Date: Sun, 1 Sep 2024 13:12:20 -0700 Subject: [PATCH] Revert "Merge pull request #16240 from ShadowLarkens/revert" This reverts commit 885171f985586f37e7ef921fda61c6a3e84c7697, reversing changes made to 3e314f806413c4f477ab552f7e8a0a367df897f4. --- .../components/unary/vent_pump.dm | 6 +- code/__defines/logging.dm | 1 + code/__defines/preferences.dm | 35 ++ code/__defines/text.dm | 3 + code/_global_vars/_regexes.dm | 5 + code/_helpers/game.dm | 6 +- code/_helpers/logging.dm | 2 +- code/_helpers/sanitize_values.dm | 7 + code/_helpers/text.dm | 4 +- code/_macros.dm | 1 + code/controllers/subsystems/ping.dm | 2 +- code/datums/browser.dm | 2 +- code/datums/chat_message.dm | 8 +- code/datums/json_savefile.dm | 116 ++++ code/datums/looping_sounds/_looping_sound.dm | 2 +- .../datums/looping_sounds/machinery_sounds.dm | 4 +- code/datums/looping_sounds/weather_sounds.dm | 4 +- code/datums/progressbar.dm | 4 +- code/game/area/areas.dm | 2 +- code/game/machinery/doors/airlock.dm | 8 +- code/game/machinery/telecomms/broadcaster.dm | 26 +- code/game/objects/items.dm | 10 +- .../items/devices/communicator/UI_tgui.dm | 2 +- .../items/devices/communicator/messaging.dm | 2 +- .../objects/items/devices/radio/headset.dm | 2 - code/game/objects/structures/low_wall.dm | 2 +- code/game/sound.dm | 17 +- code/modules/admin/admin.dm | 2 +- code/modules/admin/admin_verb_lists_vr.dm | 8 - code/modules/admin/verbs/adminhelp.dm | 4 +- code/modules/admin/verbs/adminpm.dm | 2 +- code/modules/admin/verbs/deadsay.dm | 2 +- code/modules/admin/verbs/lightning_strike.dm | 2 +- code/modules/admin/verbs/playsound.dm | 6 +- code/modules/admin/verbs/pray.dm | 2 +- .../modules/asset_cache/assets/preferences.dm | 22 + code/modules/client/client procs.dm | 15 +- .../preference_setup/antagonism/01_basic.dm | 20 +- .../antagonism/02_candidacy.dm | 8 +- .../preference_setup/general/01_basic.dm | 60 +- .../preference_setup/general/02_language.dm | 30 +- .../preference_setup/general/03_body.dm | 256 ++++---- .../preference_setup/general/04_equipment.dm | 32 +- .../preference_setup/general/05_background.dm | 41 +- .../preference_setup/general/06_flavor.dm | 52 +- .../client/preference_setup/global/01_ui.dm | 68 +-- .../preference_setup/global/02_settings.dm | 141 +---- .../client/preference_setup/global/03_pai.dm | 4 +- .../client/preference_setup/global/04_ooc.dm | 10 +- .../preference_setup/global/setting_datums.dm | 481 --------------- .../preference_setup/loadout/loadout.dm | 14 +- .../preference_setup/occupation/occupation.dm | 60 +- .../preference_setup/preference_setup.dm | 44 +- .../client/preference_setup/skills/skills.dm | 16 +- .../client/preference_setup/traits/traits.dm | 8 +- .../volume_sliders/01_volume.dm | 10 +- .../volume_sliders/02_media.dm | 12 +- .../client/preference_setup/vore/02_size.dm | 40 +- .../client/preference_setup/vore/03_egg.dm | 12 +- .../preference_setup/vore/04_resleeving.dm | 17 +- .../preference_setup/vore/05_persistence.dm | 8 +- .../client/preference_setup/vore/06_vantag.dm | 12 +- .../client/preference_setup/vore/07_traits.dm | 68 +-- .../client/preference_setup/vore/08_nif.dm | 16 +- .../client/preference_setup/vore/09_misc.dm | 40 +- code/modules/client/preferences.dm | 106 ++-- code/modules/client/preferences/README.md | 456 +++++++++++++++ .../modules/client/preferences/_preference.dm | 546 ++++++++++++++++++ .../preferences/middleware/_middleware.dm | 52 ++ .../preferences/migrations/13_preferences.dm | 12 + .../client/preferences/preferences_tg.dm | 28 + .../modules/client/preferences/types/admin.dm | 73 +++ code/modules/client/preferences/types/chat.dm | 69 +++ .../modules/client/preferences/types/ghost.dm | 29 + code/modules/client/preferences/types/misc.dm | 71 +++ .../client/preferences/types/runechat.dm | 23 + .../modules/client/preferences/types/sound.dm | 149 +++++ code/modules/client/preferences/types/ui.dm | 34 ++ code/modules/client/preferences_savefile.dm | 307 +++++++--- code/modules/client/preferences_tgui.dm | 138 +++++ .../client/preferences_toggle_procs.dm | 519 ----------------- code/modules/client/preferences_vr.dm | 143 ----- code/modules/client/verbs/ooc.dm | 10 +- code/modules/client/verbs/typing.dm | 14 +- .../emotes/definitions/audible_belch.dm | 2 +- code/modules/emotes/emote_define.dm | 8 +- code/modules/emotes/emote_mob.dm | 2 +- code/modules/instruments/songs/play_legacy.dm | 2 +- .../instruments/songs/play_synthesized.dm | 2 +- code/modules/media/mediamanager.dm | 4 +- code/modules/mentor/mentor.dm | 2 +- code/modules/mentor/mentorhelp.dm | 4 +- code/modules/mob/animations.dm | 3 +- code/modules/mob/emote.dm | 2 +- code/modules/mob/hear_say.dm | 14 +- code/modules/mob/language/synthetic.dm | 4 +- code/modules/mob/living/carbon/human/death.dm | 2 +- code/modules/mob/living/carbon/human/life.dm | 6 +- code/modules/mob/living/inventory.dm | 4 +- code/modules/mob/living/life.dm | 2 +- code/modules/mob/living/say.dm | 10 +- code/modules/mob/living/silicon/pai/pai_vr.dm | 6 +- .../silicon/robot/dogborg/dog_sleeper_vr.dm | 16 +- .../living/silicon/robot/drone/drone_say.dm | 4 +- .../subtypes/animal/alien animals/stardog.dm | 24 +- .../simple_mob/subtypes/animal/borer/borer.dm | 2 +- .../subtypes/animal/borer/borer_captive.dm | 2 +- code/modules/mob/login.dm | 4 +- code/modules/mob/mob.dm | 14 +- code/modules/mob/mob_helpers.dm | 4 +- code/modules/mob/new_player/new_player.dm | 2 +- code/modules/mob/say.dm | 2 +- code/modules/mob/say_vr.dm | 52 +- code/modules/mob/typing_indicator.dm | 12 +- code/modules/organs/pain.dm | 2 +- .../player_tips_controller_vr.dm | 2 +- code/modules/projectiles/gun.dm | 2 +- .../targeting/targeting_triggers.dm | 2 +- code/modules/tables/interactions.dm | 2 +- code/modules/tgui_input/say_modal/modal.dm | 2 +- code/modules/vore/chat_healthbars.dm | 2 +- code/modules/vore/eating/belly_obj_vr.dm | 14 +- .../vore/eating/bellymodes_datum_vr.dm | 2 +- code/modules/vore/eating/bellymodes_vr.dm | 8 +- code/modules/vore/eating/living_vr.dm | 9 +- code/modules/vore/eating/vorepanel_vr.dm | 2 +- interface/interface.dm | 2 +- interface/skin.dmf | 14 + prof.dll | Bin 0 -> 107520 bytes .../PreferencesMenu/GamePreferenceWindow.tsx | 70 +++ .../PreferencesMenu/GamePreferencesPage.tsx | 149 +++++ .../interfaces/PreferencesMenu/PageButton.tsx | 30 + .../ServerPreferencesFetcher.tsx | 43 ++ .../interfaces/PreferencesMenu/TabbedMenu.tsx | 90 +++ .../antagonists/antagonists/abductor.ts | 23 + .../antagonists/antagonists/blob.ts | 17 + .../antagonists/antagonists/blobinfection.ts | 17 + .../antagonists/antagonists/bloodbrother.ts | 16 + .../antagonists/antagonists/changeling.ts | 21 + .../antagonists/changelingmidround.ts | 17 + .../antagonists/antagonists/clownoperative.ts | 20 + .../antagonists/antagonists/cultist.ts | 22 + .../antagonists/antagonists/fugitive.ts | 15 + .../antagonists/antagonists/glitch.ts | 19 + .../antagonists/headrevolutionary.ts | 15 + .../antagonists/antagonists/heretic.ts | 22 + .../antagonists/hereticsmuggler.ts | 14 + .../antagonists/antagonists/loneoperative.ts | 18 + .../antagonists/antagonists/malfai.ts | 16 + .../antagonists/antagonists/malfaimidround.ts | 17 + .../antagonists/antagonists/nightmare.ts | 15 + .../antagonists/antagonists/obsessed.ts | 16 + .../antagonists/antagonists/operative.ts | 24 + .../antagonists/operativemidround.ts | 17 + .../antagonists/antagonists/paradoxclone.ts | 15 + .../antagonists/antagonists/provocateur.ts | 18 + .../antagonists/antagonists/revenant.ts | 16 + .../antagonists/sentiencepotionspawn.ts | 22 + .../antagonists/antagonists/spacedragon.ts | 15 + .../antagonists/antagonists/spaceninja.ts | 23 + .../antagonists/antagonists/spy.ts | 22 + .../antagonists/stowawaychangeling.ts | 17 + .../antagonists/syndicateinfiltrator.ts | 15 + .../antagonists/syndicatesleeperagent.ts | 18 + .../antagonists/antagonists/traitor.ts | 23 + .../antagonists/antagonists/wizard.ts | 18 + .../antagonists/antagonists/wizardmidround.ts | 14 + .../antagonists/antagonists/xenomorph.ts | 15 + .../PreferencesMenu/antagonists/base.ts | 35 ++ .../tgui/interfaces/PreferencesMenu/data.ts | 36 ++ .../tgui/interfaces/PreferencesMenu/index.tsx | 31 + .../preferences/features/base.tsx | 267 +++++++++ .../preferences/features/dropdowns.tsx | 113 ++++ .../features/game_preferences/admin.tsx | 50 ++ .../features/game_preferences/chat.tsx | 81 +++ .../features/game_preferences/ghost.tsx | 36 ++ .../features/game_preferences/misc.tsx | 74 +++ .../features/game_preferences/runechat.tsx | 29 + .../features/game_preferences/sound.tsx | 156 +++++ .../features/game_preferences/ui.tsx | 29 + .../preferences/features/index.ts | 23 + .../PreferencesMenu/preferences/gender.ts | 28 + tgui/public/tgui-panel.bundle.js | 56 +- tgui/public/tgui-say.bundle.js | 58 +- tgui/public/tgui.bundle.js | 184 +++--- vorestation.dme | 15 +- 186 files changed, 5029 insertions(+), 2255 deletions(-) create mode 100644 code/datums/json_savefile.dm create mode 100644 code/modules/asset_cache/assets/preferences.dm delete mode 100644 code/modules/client/preference_setup/global/setting_datums.dm create mode 100644 code/modules/client/preferences/README.md create mode 100644 code/modules/client/preferences/_preference.dm create mode 100644 code/modules/client/preferences/middleware/_middleware.dm create mode 100644 code/modules/client/preferences/migrations/13_preferences.dm create mode 100644 code/modules/client/preferences/preferences_tg.dm create mode 100644 code/modules/client/preferences/types/admin.dm create mode 100644 code/modules/client/preferences/types/chat.dm create mode 100644 code/modules/client/preferences/types/ghost.dm create mode 100644 code/modules/client/preferences/types/misc.dm create mode 100644 code/modules/client/preferences/types/runechat.dm create mode 100644 code/modules/client/preferences/types/sound.dm create mode 100644 code/modules/client/preferences/types/ui.dm create mode 100644 code/modules/client/preferences_tgui.dm create mode 100644 prof.dll create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferenceWindow.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/PageButton.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/TabbedMenu.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/abductor.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blob.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blobinfection.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodbrother.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changeling.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changelingmidround.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/clownoperative.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/cultist.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/fugitive.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/glitch.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/headrevolutionary.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/heretic.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/hereticsmuggler.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/loneoperative.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfai.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfaimidround.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/nightmare.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/obsessed.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operative.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operativemidround.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/paradoxclone.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/provocateur.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/revenant.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/sentiencepotionspawn.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spacedragon.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spaceninja.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/stowawaychangeling.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicateinfiltrator.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicatesleeperagent.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/traitor.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizard.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizardmidround.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/xenomorph.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/base.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/data.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/index.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dropdowns.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/chat.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/misc.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/runechat.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/sound.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ui.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/index.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/gender.ts diff --git a/code/ATMOSPHERICS/components/unary/vent_pump.dm b/code/ATMOSPHERICS/components/unary/vent_pump.dm index 281d7e48f7..487499f697 100644 --- a/code/ATMOSPHERICS/components/unary/vent_pump.dm +++ b/code/ATMOSPHERICS/components/unary/vent_pump.dm @@ -165,14 +165,14 @@ if(welded) vent_icon += "weld" - playsound(src, stop_sound, 25, ignore_walls = FALSE, preference = /datum/client_preference/air_pump_noise) + playsound(src, stop_sound, 25, ignore_walls = FALSE, preference = /datum/preference/toggle/air_pump_noise) else if(!use_power || !node || (stat & (NOPOWER|BROKEN))) vent_icon += "off" - playsound(src, stop_sound, 25, ignore_walls = FALSE, preference = /datum/client_preference/air_pump_noise) + playsound(src, stop_sound, 25, ignore_walls = FALSE, preference = /datum/preference/toggle/air_pump_noise) else vent_icon += "[pump_direction ? "out" : "in"]" - playsound(src, start_sound, 25, ignore_walls = FALSE, preference = /datum/client_preference/air_pump_noise) + playsound(src, start_sound, 25, ignore_walls = FALSE, preference = /datum/preference/toggle/air_pump_noise) add_overlay(icon_manager.get_atmos_icon("device", , , vent_icon)) diff --git a/code/__defines/logging.dm b/code/__defines/logging.dm index 07a121d9bb..c0e2ce9977 100644 --- a/code/__defines/logging.dm +++ b/code/__defines/logging.dm @@ -1,2 +1,3 @@ #define SEND_TEXT(target, text) DIRECT_OUTPUT(target, text) #define WRITE_FILE(file, text) DIRECT_OUTPUT(file, text) +#define READ_FILE(file, text) DIRECT_INPUT(file, text) diff --git a/code/__defines/preferences.dm b/code/__defines/preferences.dm index ba0abca8cf..14adeb62bb 100644 --- a/code/__defines/preferences.dm +++ b/code/__defines/preferences.dm @@ -15,3 +15,38 @@ #define MULTILINGUAL_MODE_MAX 4 #define SAVE_RESET -1 + +// Values for /datum/preference/savefile_identifier +/// This preference is character specific. +#define PREFERENCE_CHARACTER "character" +/// This preference is account specific. +#define PREFERENCE_PLAYER "player" + +// Values for /datum/preferences/current_tab +/// Open the character preference window +#define PREFERENCE_TAB_CHARACTER_PREFERENCES 0 + +/// Open the game preferences window +#define PREFERENCE_TAB_GAME_PREFERENCES 1 + +/// 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" + +/// These preferences will not be rendered on the preferences page, and are practically invisible unless specifically rendered. Used for quirks, currently. +#define PREFERENCE_CATEGORY_MANUALLY_RENDERED "manually_rendered_features" diff --git a/code/__defines/text.dm b/code/__defines/text.dm index 4b520e5771..b48754dd32 100644 --- a/code/__defines/text.dm +++ b/code/__defines/text.dm @@ -8,3 +8,6 @@ * because DM does it so it copies until the char BEFORE the `end` arg, so we need to bump `end` by 1 in these cases. */ #define PREVENT_CHARACTER_TRIM_LOSS(integer) (integer + 1) + +/// Simply removes the < and > characters, and limits the length of the message. +#define STRIP_HTML_SIMPLE(text, limit) (GLOB.angular_brackets.Replace(copytext(text, 1, limit), "")) diff --git a/code/_global_vars/_regexes.dm b/code/_global_vars/_regexes.dm index 46e78a28fa..1d92bc8c52 100644 --- a/code/_global_vars/_regexes.dm +++ b/code/_global_vars/_regexes.dm @@ -2,3 +2,8 @@ GLOBAL_DATUM_INIT(is_http_protocol, /regex, regex("^https?://")) GLOBAL_DATUM_INIT(is_valid_url, /regex, regex("((?:https://)\[-a-zA-Z0-9@:%._+~#=]{1,256}.\[-a-zA-Z0-9@:%._+~#=]{1,256}\\b(?:\[-a-zA-Z0-9@():%_+.,~#?&/=]*\[^.,!?:; ()<>{}\\[]\n\"'ยด`]))", "gm")) + +//All < and > characters +GLOBAL_DATUM_INIT(angular_brackets, /regex, regex(@"[<>]", "g")) + +GLOBAL_DATUM_INIT(is_color, /regex, regex("^#\[0-9a-fA-F]{6}$")) diff --git a/code/_helpers/game.dm b/code/_helpers/game.dm index 812c0d5fe7..488b31d2a2 100644 --- a/code/_helpers/game.dm +++ b/code/_helpers/game.dm @@ -259,7 +259,7 @@ return R // radio, true, false, what's the difference /mob/observer/dead/can_hear_radio(var/list/hearturfs) - return is_preference_enabled(/datum/client_preference/ghost_radio) + return client?.prefs?.read_preference(/datum/preference/toggle/ghost_radio) //Uses dview to quickly return mobs and objects in view, @@ -313,10 +313,10 @@ if(M && M.stat == DEAD && remote_ghosts && !M.forbid_seeing_deadchat) switch(type) if(1) //Audio messages use ghost_ears - if(M.is_preference_enabled(/datum/client_preference/ghost_ears)) + if(M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) mobs |= M if(2) //Visual messages use ghost_sight - if(M.is_preference_enabled(/datum/client_preference/ghost_sight)) + if(M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_sight)) mobs |= M //For objects below the top level who still want to hear diff --git a/code/_helpers/logging.dm b/code/_helpers/logging.dm index d5e9bd841c..9348231911 100644 --- a/code/_helpers/logging.dm +++ b/code/_helpers/logging.dm @@ -45,7 +45,7 @@ WRITE_LOG(debug_log, "DEBUG: [sanitize(text)]") for(var/client/C in GLOB.admins) - if(C.is_preference_enabled(/datum/client_preference/debug/show_debug_logs)) + if(C.prefs?.read_preference(/datum/preference/toggle/show_debug_logs)) to_chat(C, type = MESSAGE_TYPE_DEBUG, html = "DEBUG: [text]") diff --git a/code/_helpers/sanitize_values.dm b/code/_helpers/sanitize_values.dm index c74176043c..7cc687d7ce 100644 --- a/code/_helpers/sanitize_values.dm +++ b/code/_helpers/sanitize_values.dm @@ -6,6 +6,13 @@ return number return default +/proc/sanitize_float(number, min=0, max=1, accuracy=1, default=0) + if(isnum(number)) + number = round(number, accuracy) + if(round(min, accuracy) <= number && number <= round(max, accuracy)) + return number + return default + // Checks if the given input is a valid list index; returns true/false and doesn't change anything. /proc/is_valid_index(input, list/given_list) if(!isnum(input)) diff --git a/code/_helpers/text.dm b/code/_helpers/text.dm index 2ded9ae75a..4d8968ad4e 100644 --- a/code/_helpers/text.dm +++ b/code/_helpers/text.dm @@ -345,7 +345,7 @@ /var/icon/text_tag_icons = 'icons/chattags.dmi' /var/list/text_tag_cache = list() /proc/create_text_tag(var/tagname, var/tagdesc = tagname, var/client/C = null) - if(!(C && C.is_preference_enabled(/datum/client_preference/chat_tags))) + if(!(C && C.prefs?.read_preference(/datum/preference/toggle/chat_tags))) return tagdesc if(!text_tag_cache[tagname]) var/icon/tag = icon(text_tag_icons, tagname) @@ -355,7 +355,7 @@ return icon2html(text_tag_cache[tagname], C, extra_classes = "text_tag") /proc/create_text_tag_old(var/tagname, var/tagdesc = tagname, var/client/C = null) - if(!(C && C.is_preference_enabled(/datum/client_preference/chat_tags))) + if(!(C && C.prefs?.read_preference(/datum/preference/toggle/chat_tags))) return tagdesc return "[tagdesc]" diff --git a/code/_macros.dm b/code/_macros.dm index b37d24fa0d..2ff9da5e76 100644 --- a/code/_macros.dm +++ b/code/_macros.dm @@ -26,6 +26,7 @@ // From TG, might be useful to have. // Didn't port SEND_TEXT() since to_chat() appears to serve the same purpose. #define DIRECT_OUTPUT(A, B) A << B +#define DIRECT_INPUT(A, B) A >> B #define SEND_IMAGE(target, image) DIRECT_OUTPUT(target, image) #define SEND_SOUND(target, sound) DIRECT_OUTPUT(target, sound) //#define WRITE_LOG is in logging.dm diff --git a/code/controllers/subsystems/ping.dm b/code/controllers/subsystems/ping.dm index 0b76d7dea1..20cf8dd446 100644 --- a/code/controllers/subsystems/ping.dm +++ b/code/controllers/subsystems/ping.dm @@ -27,7 +27,7 @@ SUBSYSTEM_DEF(ping) var/client/client = currentrun[currentrun.len] currentrun.len-- - if(!client?.is_preference_enabled(/datum/client_preference/vchat_enable)) + if(!client?.prefs?.read_preference(/datum/preference/toggle/vchat_enable)) winset(client, "output", "on-show=&is-disabled=0&is-visible=1") winset(client, "browseroutput", "is-disabled=1;is-visible=0") client.tgui_panel.oldchat = TRUE diff --git a/code/datums/browser.dm b/code/datums/browser.dm index e38b78ab4d..4aac042789 100644 --- a/code/datums/browser.dm +++ b/code/datums/browser.dm @@ -27,7 +27,7 @@ if (nref) ref = nref // If a client exists, but they have disabled fancy windowing, disable it! - if(user && user.client && !user.client.is_preference_enabled(/datum/client_preference/browser_style)) + if(!user?.client?.prefs?.read_preference(/datum/preference/toggle/browser_style)) return add_stylesheet("common", 'html/browser/common.css') // this CSS sheet is common to all UIs diff --git a/code/datums/chat_message.dm b/code/datums/chat_message.dm index 3a95ba1db5..376f87ed5e 100644 --- a/code/datums/chat_message.dm +++ b/code/datums/chat_message.dm @@ -109,7 +109,7 @@ var/list/runechat_image_cache = list() owned_by = owner.client RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, PROC_REF(unregister_qdel_self)) // this should only call owned_by if the client is destroyed - var/extra_length = owned_by.is_preference_enabled(/datum/client_preference/runechat_long_messages) + var/extra_length = owned_by.prefs?.read_preference(/datum/preference/toggle/runechat_long_messages) var/maxlen = extra_length ? CHAT_MESSAGE_EXT_LENGTH : CHAT_MESSAGE_LENGTH var/msgwidth = extra_length ? CHAT_MESSAGE_EXT_WIDTH : CHAT_MESSAGE_WIDTH @@ -244,10 +244,10 @@ var/list/runechat_image_cache = list() return // Doesn't want to hear - if(ismob(speaker) && !client.is_preference_enabled(/datum/client_preference/runechat_mob)) + if(ismob(speaker) && !client.prefs?.read_preference(/datum/preference/toggle/runechat_mob)) return // I know the pref is 'obj' but people dunno what turfs are - else if(!client.is_preference_enabled(/datum/client_preference/runechat_obj)) + else if(!client.prefs?.read_preference(/datum/preference/toggle/runechat_obj)) return // Incapable of receiving @@ -270,7 +270,7 @@ var/list/runechat_image_cache = list() if(italics) extra_classes |= "italics" - if(client.is_preference_enabled(/datum/client_preference/runechat_border)) + if(client.prefs?.read_preference(/datum/preference/toggle/runechat_border)) extra_classes |= "black_outline" var/dist = get_dist(src, speaker) diff --git a/code/datums/json_savefile.dm b/code/datums/json_savefile.dm new file mode 100644 index 0000000000..f93c885687 --- /dev/null +++ b/code/datums/json_savefile.dm @@ -0,0 +1,116 @@ +/** + * A savefile implementation that handles all data using json. + * Also saves it using JSON too, fancy. + * If you pass in a null path, it simply acts as a memory tree instead, and cannot be saved. + */ +/datum/json_savefile + var/path = "" + VAR_PRIVATE/list/tree + /// If this is set to true, calling set_entry or remove_entry will automatically call save(), this does not catch modifying a sub-tree, nor do I know how to do that + var/auto_save = FALSE + /// Cooldown that tracks the time between attempts to download the savefile. + COOLDOWN_DECLARE(download_cooldown) + +GENERAL_PROTECT_DATUM(/datum/json_savefile) + +/datum/json_savefile/New(path) + src.path = path + tree = list() + if(path && fexists(path)) + load() + +/** + * Gets an entry from the json tree, with an optional default value. + * If no key is specified it throws the entire tree at you instead + */ +/datum/json_savefile/proc/get_entry(key, default_value) + if(!key) + return tree + return (key in tree) ? tree[key] : default_value + +/// Sets an entry in the tree to the given value +/datum/json_savefile/proc/set_entry(key, value) + tree[key] = value + if(auto_save) + save() + +/// Removes the given key from the tree +/datum/json_savefile/proc/remove_entry(key) + if(key) + tree -= key + if(auto_save) + save() + +/// Wipes the entire tree +/datum/json_savefile/proc/wipe() + tree?.Cut() + +/datum/json_savefile/proc/load() + if(!path || !fexists(path)) + return FALSE + try + tree = json_decode(rustg_file_read(path)) + return TRUE + catch(var/exception/err) + stack_trace("failed to load json savefile at '[path]': [err]") + return FALSE + +/datum/json_savefile/proc/save() + if(path) + rustg_file_write(json_encode(tree, JSON_PRETTY_PRINT), path) + +/// Traverses the entire dir tree of the given savefile and dynamically assembles the tree from it +/datum/json_savefile/proc/import_byond_savefile(savefile/savefile) + tree.Cut() + var/list/dirs_to_go = list("/" = tree) + while(length(dirs_to_go)) + var/dir = dirs_to_go[1] + var/list/region = dirs_to_go[dir] + dirs_to_go.Cut(1, 2) + savefile.cd = dir + for(var/entry in savefile.dir) + var/entry_value + savefile.cd = "[dir]/[entry]" + //eof refers to the path you are cd'ed into, not the savefile as a whole. being false right after cding into an entry means this entry has no buffer, which only happens with nested save file directories + if (savefile.eof) + region[entry] = list() + dirs_to_go["[dir]/[entry]"] = region[entry] + continue + READ_FILE(savefile, entry_value) //we are cd'ed to the entry, so we don't need to specify a path to read from + region[entry] = entry_value + +/// Proc that handles generating a JSON file (prettified if 515 and over!) of a user's preferences and showing it to them. +/// Requester is passed in to the ftp() and tgui_alert() procs, and account_name is just used to generate the filename. +/// We don't _need_ to pass in account_name since this is reliant on the json_savefile datum already knowing what we correspond to, but it's here to help people keep track of their stuff. +/datum/json_savefile/proc/export_json_to_client(mob/requester, account_name) + if(!istype(requester) || !path) + return + + if(!json_export_checks(requester)) + return + + // COOLDOWN_START(src, download_cooldown, (CONFIG_GET(number/seconds_cooldown_for_preferences_export) * (1 SECONDS))) + COOLDOWN_START(src, download_cooldown, (10 SECONDS)) + var/file_name = "[account_name ? "[account_name]_" : ""]preferences_[time2text(world.timeofday, "MMM_DD_YYYY_hh-mm-ss")].json" + var/temporary_file_storage = "data/preferences_export_working_directory/[file_name]" + + if(!text2file(json_encode(tree, JSON_PRETTY_PRINT), temporary_file_storage)) + tgui_alert(requester, "Failed to export preferences to JSON! You might need to try again later.", "Export Preferences JSON") + return + + var/exportable_json = file(temporary_file_storage) + + DIRECT_OUTPUT(requester, ftp(exportable_json, file_name)) + fdel(temporary_file_storage) + +/// Proc that just handles all of the checks for exporting a preferences file, returns TRUE if all checks are passed, FALSE otherwise. +/// Just done like this to make the code in the export_json_to_client() proc a bit cleaner. +/datum/json_savefile/proc/json_export_checks(mob/requester) + if(!COOLDOWN_FINISHED(src, download_cooldown)) + tgui_alert(requester, "You must wait [DisplayTimeText(COOLDOWN_TIMELEFT(src, download_cooldown))] before exporting your preferences again!", "Export Preferences JSON") + return FALSE + + if(tgui_alert(requester, "Are you sure you want to export your preferences as a JSON file? This will save to a file on your computer.", "Export Preferences JSON", list("Cancel", "Yes")) == "Yes") + return TRUE + + return FALSE diff --git a/code/datums/looping_sounds/_looping_sound.dm b/code/datums/looping_sounds/_looping_sound.dm index 078db317d5..3b48fc1a84 100644 --- a/code/datums/looping_sounds/_looping_sound.dm +++ b/code/datums/looping_sounds/_looping_sound.dm @@ -100,7 +100,7 @@ if(direct) if(ismob(thing)) var/mob/M = thing - if(pref_check && !M.is_preference_enabled(pref_check)) + if(M.check_sound_preference(pref_check)) continue SEND_SOUND(thing, S) else diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm index 8005d95016..3c0c8c15d2 100644 --- a/code/datums/looping_sounds/machinery_sounds.dm +++ b/code/datums/looping_sounds/machinery_sounds.dm @@ -13,7 +13,7 @@ mid_length = 60 volume = 40 extra_range = 10 - pref_check = /datum/client_preference/supermatter_hum + pref_check = /datum/preference/toggle/supermatter_hum /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -104,7 +104,7 @@ mid_length = 70 end_sound = 'sound/machines/air_pump/airpumpshutdown.ogg' volume = 15 - pref_check = /datum/client_preference/air_pump_noise + pref_check = /datum/preference/toggle/air_pump_noise //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/code/datums/looping_sounds/weather_sounds.dm b/code/datums/looping_sounds/weather_sounds.dm index db8396f3a3..41210df105 100644 --- a/code/datums/looping_sounds/weather_sounds.dm +++ b/code/datums/looping_sounds/weather_sounds.dm @@ -1,5 +1,5 @@ /datum/looping_sound/weather - pref_check = /datum/client_preference/weather_sounds + pref_check = /datum/preference/toggle/weather_sounds /datum/looping_sound/weather/outside_blizzard mid_sounds = list( @@ -95,4 +95,4 @@ volume = 20 //Sound is already quieter in file /datum/looping_sound/weather/rain/indoors/heavy - volume = 40 \ No newline at end of file + volume = 40 diff --git a/code/datums/progressbar.dm b/code/datums/progressbar.dm index 8da8d7c000..b0825c6329 100644 --- a/code/datums/progressbar.dm +++ b/code/datums/progressbar.dm @@ -41,7 +41,7 @@ progress = CLAMP(progress, 0, goal) bar.icon_state = "prog_bar_[round(((progress / goal) * 100), 5)]" - if(!shown && user.is_preference_enabled(/datum/client_preference/show_progress_bar)) + if(!shown && user.client?.prefs?.read_preference(/datum/preference/toggle/show_progress_bar)) user.client.images += bar shown = 1 @@ -66,4 +66,4 @@ qdel(bar) . = ..() -#undef PROGRESSBAR_HEIGHT \ No newline at end of file +#undef PROGRESSBAR_HEIGHT diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 2be00fb258..7d63de088f 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -394,7 +394,7 @@ var/list/mob/living/forced_ambiance_list = new /area/proc/play_ambience(var/mob/living/L, initial = TRUE) // Ambience goes down here -- make sure to list each area seperately for ease of adding things in later, thanks! Note: areas adjacent to each other should have the same sounds to prevent cutoff when possible.- LastyScratch - if(!(L && L.is_preference_enabled(/datum/client_preference/play_ambiance))) + if(!L?.read_preference(/datum/preference/toggle/play_ambience)) return var/volume_mod = L.get_preference_volume_channel(VOLUME_CHANNEL_AMBIENCE) diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index 30d578180f..e473cb5c86 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -1284,8 +1284,8 @@ About the new airlock wires panel: for(var/mob/M as anything in player_list) if(!M || !M.client) continue - var/old_sounds = M.client.is_preference_enabled(/datum/client_preference/old_door_sounds) - var/department_door_sounds = M.client.is_preference_enabled(/datum/client_preference/department_door_sounds) + var/old_sounds = M.read_preference(/datum/preference/toggle/old_door_sounds) + var/department_door_sounds = M.read_preference(/datum/preference/toggle/department_door_sounds) var/sound var/volume if(old_sounds) // Do we have old sounds enabled? Play these even if we have department door sounds enabled. @@ -1412,8 +1412,8 @@ About the new airlock wires panel: for(var/mob/M as anything in player_list) if(!M || !M.client) continue - var/old_sounds = M.client.is_preference_enabled(/datum/client_preference/old_door_sounds) - var/department_door_sounds = M.client.is_preference_enabled(/datum/client_preference/department_door_sounds) + var/old_sounds = M.read_preference(/datum/preference/toggle/old_door_sounds) + var/department_door_sounds = M.read_preference(/datum/preference/toggle/department_door_sounds) var/sound var/volume if(old_sounds) diff --git a/code/game/machinery/telecomms/broadcaster.dm b/code/game/machinery/telecomms/broadcaster.dm index 5fee77b9e3..48f9e1bff2 100644 --- a/code/game/machinery/telecomms/broadcaster.dm +++ b/code/game/machinery/telecomms/broadcaster.dm @@ -406,14 +406,16 @@ var/message_delay = 0 // To make sure restarting the recentmessages list is kept for (var/mob/R in receive) /* --- Loop through the receivers and categorize them --- */ - if(!R.is_preference_enabled(/datum/client_preference/holder/hear_radio)) - continue + // Allows admins to disable radio + if(R?.client?.holder) + if(!R.client?.prefs?.read_preference(/datum/preference/toggle/holder/hear_radio)) + continue if(istype(R, /mob/new_player)) // we don't want new players to hear messages. rare but generates runtimes. continue // Ghosts hearing all radio chat don't want to hear syndicate intercepts, they're duplicates - if(data == DATA_ANTAG && istype(R, /mob/observer/dead) && R.is_preference_enabled(/datum/client_preference/ghost_radio)) + if(data == DATA_ANTAG && istype(R, /mob/observer/dead) && R.client?.prefs?.read_preference(/datum/preference/toggle/ghost_radio)) continue // --- Check for compression --- @@ -513,21 +515,21 @@ var/message_delay = 0 // To make sure restarting the recentmessages list is kept if(length(heard_masked)) for (var/mob/R in heard_masked) R.hear_radio(message_pieces, verbage, part_a, part_b, part_c, part_d, part_e, M, 0, name) - if(R.is_preference_enabled(/datum/client_preference/radio_sounds)) + if(R.read_preference(/datum/preference/toggle/radio_sounds)) R << 'sound/effects/radio_common_quieter.ogg' /* --- Process all the mobs that heard the voice normally (understood) --- */ if(length(heard_normal)) for (var/mob/R in heard_normal) R.hear_radio(message_pieces, verbage, part_a, part_b, part_c, part_d, part_e, M, 0, realname) - if(R.is_preference_enabled(/datum/client_preference/radio_sounds)) + if(R.read_preference(/datum/preference/toggle/radio_sounds)) R << 'sound/effects/radio_common_quieter.ogg' /* --- Process all the mobs that heard the voice normally (did not understand) --- */ if(length(heard_voice)) for (var/mob/R in heard_voice) R.hear_radio(message_pieces, verbage, part_a, part_b, part_c, part_d, part_e, M,0, vname) - if(R.is_preference_enabled(/datum/client_preference/radio_sounds)) + if(R.read_preference(/datum/preference/toggle/radio_sounds)) R << 'sound/effects/radio_common_quieter.ogg' /* --- Process all the mobs that heard a garbled voice (did not understand) --- */ @@ -535,14 +537,14 @@ var/message_delay = 0 // To make sure restarting the recentmessages list is kept if(length(heard_garbled)) for (var/mob/R in heard_garbled) R.hear_radio(message_pieces, verbage, part_a, part_b, part_c, part_d, part_e, M, 1, vname) - if(R.is_preference_enabled(/datum/client_preference/radio_sounds)) + if(R.read_preference(/datum/preference/toggle/radio_sounds)) R << 'sound/effects/radio_common_quieter.ogg' /* --- Complete gibberish. Usually happens when there's a compressed message --- */ if(length(heard_gibberish)) for (var/mob/R in heard_gibberish) R.hear_radio(message_pieces, verbage, part_a, part_b, part_c, part_d, part_e, M, 1) - if(R.is_preference_enabled(/datum/client_preference/radio_sounds)) + if(R.read_preference(/datum/preference/toggle/radio_sounds)) R << 'sound/effects/radio_common_quieter.ogg' return 1 @@ -617,10 +619,10 @@ var/message_delay = 0 // To make sure restarting the recentmessages list is kept for (var/mob/R in receive) /* --- Loop through the receivers and categorize them --- */ - - if(!R.is_preference_enabled(/datum/client_preference/holder/hear_radio)) //Adminning with 80 people on can be fun when you're trying to talk and all you can hear is radios. - continue - + // Allow admins to disable radios completely + if(R?.client?.holder) + if(!R.client?.prefs?.read_preference(/datum/preference/toggle/holder/hear_radio)) + continue // --- Check for compression --- if(compression > 0) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 83844f7995..83a53c1537 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -326,7 +326,7 @@ else playsound(hit_atom, 'sound/weapons/throwtap.ogg', 1, volume, -1) else - playsound(src, drop_sound, 30, preference = /datum/client_preference/drop_sounds) + playsound(src, drop_sound, 30, preference = /datum/preference/toggle/drop_sounds) // apparently called whenever an item is removed from a slot, container, or anything else. /obj/item/proc/dropped(mob/user as mob) @@ -364,11 +364,11 @@ if(user.pulling == src) user.stop_pulling() if(("[slot]" in slot_flags_enumeration) && (slot_flags & slot_flags_enumeration["[slot]"])) if(equip_sound) - playsound(src, equip_sound, 20, preference = /datum/client_preference/pickup_sounds) + playsound(src, equip_sound, 20, preference = /datum/preference/toggle/pickup_sounds) else - playsound(src, drop_sound, 20, preference = /datum/client_preference/pickup_sounds) + playsound(src, drop_sound, 20, preference = /datum/preference/toggle/pickup_sounds) else if(slot == slot_l_hand || slot == slot_r_hand) - playsound(src, pickup_sound, 20, preference = /datum/client_preference/pickup_sounds) + playsound(src, pickup_sound, 20, preference = /datum/preference/toggle/pickup_sounds) return // As above but for items being equipped to an active module on a robot. @@ -948,7 +948,7 @@ modules/mob/living/carbon/human/life.dm if you die, you will be zoomed out. /obj/item/MouseEntered(location,control,params) . = ..() - if(usr.is_preference_enabled(/datum/client_preference/inv_tooltips) && ((src in usr) || isstorage(loc))) // If in inventory or in storage we're looking at + if(usr?.read_preference(/datum/preference/toggle/inv_tooltips) && ((src in usr) || isstorage(loc))) // If in inventory or in storage we're looking at var/user = usr tip_timer = addtimer(CALLBACK(src, PROC_REF(openTip), location, control, params, user), 5, TIMER_STOPPABLE) diff --git a/code/game/objects/items/devices/communicator/UI_tgui.dm b/code/game/objects/items/devices/communicator/UI_tgui.dm index fb59fd2a32..e6c099ad45 100644 --- a/code/game/objects/items/devices/communicator/UI_tgui.dm +++ b/code/game/objects/items/devices/communicator/UI_tgui.dm @@ -384,7 +384,7 @@ var/obj/item/device/communicator/comm = exonet.get_atom_from_address(their_address) to_chat(usr, "[icon2html(src, usr.client)] Sent message to [istype(comm, /obj/item/device/communicator) ? comm.owner : comm.name], \"[text]\" (Reply)") for(var/mob/M in player_list) - if(M.stat == DEAD && M.is_preference_enabled(/datum/client_preference/ghost_ears)) + if(M.stat == DEAD && M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) if(istype(M, /mob/new_player) || M.forbid_seeing_deadchat) continue if(exonet.get_atom_from_address(their_address) == M) diff --git a/code/game/objects/items/devices/communicator/messaging.dm b/code/game/objects/items/devices/communicator/messaging.dm index 84c930479e..a060cf96c1 100644 --- a/code/game/objects/items/devices/communicator/messaging.dm +++ b/code/game/objects/items/devices/communicator/messaging.dm @@ -161,7 +161,7 @@ exonet_messages.Add("To [chosen_communicator]:
[text_message]") log_pda("(DCOMM: [src]) sent \"[text_message]\" to [chosen_communicator]", src) for(var/mob/M in player_list) - if(M.stat == DEAD && M.is_preference_enabled(/datum/client_preference/ghost_ears)) + if(M.stat == DEAD && M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) if(istype(M, /mob/new_player) || M.forbid_seeing_deadchat) continue if(M == src) diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index b253e93056..49a4dab9b4 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -62,12 +62,10 @@ /obj/item/device/radio/headset/receive_range(freq, level, aiOverride = 0) if (aiOverride) - //playsound(loc, 'sound/effects/radio_common.ogg', 20, 1, 1, preference = /datum/client_preference/radio_sounds) return ..(freq, level) if(ishuman(src.loc)) var/mob/living/carbon/human/H = src.loc if(H.l_ear == src || H.r_ear == src) - //playsound(loc, 'sound/effects/radio_common.ogg', 20, 1, 1, preference = /datum/client_preference/radio_sounds) return ..(freq, level) return -1 diff --git a/code/game/objects/structures/low_wall.dm b/code/game/objects/structures/low_wall.dm index d65abcbad5..1a9dc0e6c4 100644 --- a/code/game/objects/structures/low_wall.dm +++ b/code/game/objects/structures/low_wall.dm @@ -108,7 +108,7 @@ if(W.loc != user) // This should stop mounted modules ending up outside the module. return - if(can_place_items() && user.unEquip(W, 0, src.loc) && user.is_preference_enabled(/datum/client_preference/precision_placement)) + if(can_place_items() && user.unEquip(W, 0, src.loc) && user.client?.prefs?.read_preference(/datum/preference/toggle/precision_placement)) auto_align(W, click_parameters) return 1 diff --git a/code/game/sound.dm b/code/game/sound.dm index 256dae7597..dbbbc8abfd 100644 --- a/code/game/sound.dm +++ b/code/game/sound.dm @@ -33,10 +33,23 @@ if(T && T.z == turf_source.z) M.playsound_local(turf_source, soundin, vol, vary, frequency, falloff, is_global, channel, pressure_affected, S, preference, volume_channel) +/mob/proc/check_sound_preference(list/preference) + if(!islist(preference)) + preference = list(preference) + + for(var/p in preference) + // Ignore nulls + if(p) + if(!read_preference(p)) + return FALSE + + return TRUE + /mob/proc/playsound_local(turf/turf_source, soundin, vol as num, vary, frequency, falloff, is_global, channel = 0, pressure_affected = TRUE, sound/S, preference, volume_channel = null) if(!client || ear_deaf > 0) return - if(preference && !client.is_preference_enabled(preference)) + + if(!check_sound_preference(preference)) return if(!S) @@ -122,7 +135,7 @@ /client/proc/playtitlemusic() if(!ticker || !SSmedia_tracks.lobby_tracks.len || !media) return - if(is_preference_enabled(/datum/client_preference/play_lobby_music)) + if(prefs?.read_preference(/datum/preference/toggle/play_lobby_music)) var/datum/track/T = pick(SSmedia_tracks.lobby_tracks) media.push_music(T.url, world.time, 0.85) to_chat(src,"Lobby music: [T.title] by [T.artist].") diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 94b1266c9d..2505d006ab 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -17,7 +17,7 @@ var/global/floorIsLava = 0 var/rendered = "ATTACK: [text]" for(var/client/C in GLOB.admins) if((R_ADMIN|R_MOD) & C.holder.rights) - if(C.is_preference_enabled(/datum/client_preference/mod/show_attack_logs)) + if(C.prefs?.read_preference(/datum/preference/toggle/show_attack_logs)) var/msg = rendered to_chat(C, type = MESSAGE_TYPE_ATTACKLOG, diff --git a/code/modules/admin/admin_verb_lists_vr.dm b/code/modules/admin/admin_verb_lists_vr.dm index 5c32950edb..d60bce76ba 100644 --- a/code/modules/admin/admin_verb_lists_vr.dm +++ b/code/modules/admin/admin_verb_lists_vr.dm @@ -18,8 +18,6 @@ var/list/admin_verbs_default = list( // /client/proc/cmd_mod_say, // /client/proc/deadchat //toggles deadchat on/off, // /client/proc/toggle_ahelp_sound, - /client/proc/toggle_admin_global_looc, - /client/proc/toggle_admin_deadchat ) var/list/admin_verbs_admin = list( @@ -118,8 +116,6 @@ var/list/admin_verbs_admin = list( /client/proc/change_security_level, /client/proc/view_chemical_reaction_logs, /client/proc/makepAI, - /client/proc/toggle_debug_logs, - /client/proc/toggle_attack_logs, /datum/admins/proc/paralyze_mob, /client/proc/fixatmos, /datum/admins/proc/quick_nif, //VOREStation Add, @@ -267,7 +263,6 @@ var/list/admin_verbs_debug = list( /client/proc/jumptomob, /client/proc/jumptocoord, /client/proc/dsay, - /client/proc/toggle_debug_logs, /client/proc/admin_ghost, //allows us to ghost/reenter body at will, /datum/admins/proc/show_player_panel, //shows an interface for individual players, with various links (links require additional flags, //VOREStation Add, /client/proc/player_panel_new, //shows an interface for all players, with links to various panels, //VOREStation Add, @@ -411,7 +406,6 @@ var/list/admin_verbs_mod = list( /client/proc/check_antagonists, /client/proc/aooc, /client/proc/jobbans, - /client/proc/toggle_attack_logs, /client/proc/cmd_admin_subtle_message, //send an message to somebody as a 'voice in their head', /datum/admins/proc/paralyze_mob, /client/proc/cmd_admin_direct_narrate, @@ -547,8 +541,6 @@ var/list/admin_verbs_event_manager = list( /client/proc/change_human_appearance_self, // Allows the human-based mob itself change its basic appearance , /client/proc/change_security_level, /client/proc/makepAI, - /client/proc/toggle_debug_logs, - /client/proc/toggle_attack_logs, /datum/admins/proc/paralyze_mob, /client/proc/fixatmos, /datum/admins/proc/sendFax, diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index e09dce2734..d9040e7c7a 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -251,7 +251,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) for(var/client/X in GLOB.admins) if(!check_rights(R_ADMIN, 0, X)) continue - if(X.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) + if(X.prefs?.read_preference(/datum/preference/toggle/holder/play_adminhelp_ping)) X << 'sound/effects/adminhelp.ogg' window_flash(X) to_chat(X, chat_msg) @@ -362,7 +362,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) return if(initiator) - if(initiator.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) + if(initiator.prefs?.read_preference(/datum/preference/toggle/holder/play_adminhelp_ping)) initiator << 'sound/effects/adminhelp.ogg' to_chat(initiator, "[span_red("- AdminHelp Rejected! -")]
\ diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm index f31b260633..b5e1f9c706 100644 --- a/code/modules/admin/verbs/adminpm.dm +++ b/code/modules/admin/verbs/adminpm.dm @@ -165,7 +165,7 @@ to_chat(src, "PM to-Admins: [msg]") //play the recieving admin the adminhelp sound (if they have them enabled) - if(recipient.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) + if(recipient.prefs?.read_preference(/datum/preference/toggle/holder/play_adminhelp_ping)) recipient << 'sound/effects/adminhelp.ogg' else diff --git a/code/modules/admin/verbs/deadsay.dm b/code/modules/admin/verbs/deadsay.dm index 7e70eba8e5..1d6cead98c 100644 --- a/code/modules/admin/verbs/deadsay.dm +++ b/code/modules/admin/verbs/deadsay.dm @@ -11,7 +11,7 @@ to_chat(src, "You cannot send DSAY messages (muted).") return - if(!is_preference_enabled(/datum/client_preference/show_dsay)) + if(!prefs?.read_preference(/datum/preference/toggle/show_dsay)) to_chat(src, "You have deadchat muted.") return diff --git a/code/modules/admin/verbs/lightning_strike.dm b/code/modules/admin/verbs/lightning_strike.dm index 932652bbb4..cf7fa6a7ed 100644 --- a/code/modules/admin/verbs/lightning_strike.dm +++ b/code/modules/admin/verbs/lightning_strike.dm @@ -65,7 +65,7 @@ var/sound = get_sfx("thunder") for(var/mob/M in player_list) if( (P && (M.z in P.expected_z_levels)) || M.z == T.z) - if(M.is_preference_enabled(/datum/client_preference/weather_sounds)) + if(M.check_sound_preference(/datum/preference/toggle/weather_sounds)) M.playsound_local(get_turf(M), soundin = sound, vol = 70, vary = FALSE, is_global = TRUE) if(cosmetic) // Everything beyond here involves potentially damaging things. If we don't want to do that, stop now. diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm index cbfd0371e0..ed304e0476 100644 --- a/code/modules/admin/verbs/playsound.dm +++ b/code/modules/admin/verbs/playsound.dm @@ -42,7 +42,7 @@ var/list/sounds_cache = list() message_admins("[key_name_admin(src)] played sound [S]", 1) for(var/mob/M in player_list) - if(M.is_preference_enabled(/datum/client_preference/play_admin_midis)) + if(M.read_preference(/datum/preference/toggle/play_admin_midis)) admin_sound.volume = vol * M.client.admin_music_volume SEND_SOUND(M, admin_sound) admin_sound.volume = vol @@ -91,7 +91,7 @@ var/list/sounds_cache = list() log_admin("[key_name(src)] played sound [S] on Z[target_z]") message_admins("[key_name_admin(src)] played sound [S] on Z[target_z]", 1) for(var/mob/M in player_list) - if(M.is_preference_enabled(/datum/client_preference/play_admin_midis) && M.z == target_z) + if(M.read_preference(/datum/preference/toggle/play_admin_midis) && M.z == target_z) M << uploaded_sound feedback_add_details("admin_verb", "Play Z Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -204,7 +204,7 @@ var/list/sounds_cache = list() for(var/m in player_list) var/mob/M = m var/client/C = M.client - if(C.is_preference_enabled(/datum/client_preference/play_admin_midis)) + if(C.prefs?.read_preference(/datum/preference/toggle/play_admin_midis)) 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 d4a405e35c..f5035673a9 100644 --- a/code/modules/admin/verbs/pray.dm +++ b/code/modules/admin/verbs/pray.dm @@ -21,7 +21,7 @@ for(var/client/C in GLOB.admins) if(R_ADMIN|R_EVENT & C.holder.rights) - if(C.is_preference_enabled(/datum/client_preference/admin/show_chat_prayers)) + if(C.prefs?.read_preference(/datum/preference/toggle/show_chat_prayers)) to_chat(C, msg, type = MESSAGE_TYPE_PRAYER, confidential = TRUE) C << 'sound/effects/ding.ogg' to_chat(usr, "Your prayers have been received by the gods.", confidential = TRUE) diff --git a/code/modules/asset_cache/assets/preferences.dm b/code/modules/asset_cache/assets/preferences.dm new file mode 100644 index 0000000000..3d01471a38 --- /dev/null +++ b/code/modules/asset_cache/assets/preferences.dm @@ -0,0 +1,22 @@ +/// Sends information needed for shared details on individual preferences +/datum/asset/json/preferences + name = "preferences" + +/datum/asset/json/preferences/generate() + var/list/preference_data = list() + + for(var/middleware_type in subtypesof(/datum/preference_middleware)) + var/datum/preference_middleware/middleware = new middleware_type + var/data = middleware.get_constant_data() + if(!isnull(data)) + preference_data[middleware.key] = data + + qdel(middleware) + + for(var/preference_type in GLOB.preference_entries) + var/datum/preference/preference_entry = GLOB.preference_entries[preference_type] + var/data = preference_entry.compile_constant_data() + if(!isnull(data)) + preference_data[preference_entry.savefile_key] = data + + return preference_data diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index 6a08f8c16f..f56b56f3b2 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -201,12 +201,15 @@ //preferences datum - also holds some persistant data for the client (because we may as well keep these datums to a minimum) prefs = preferences_datums[ckey] - if(!prefs) + if(prefs) + prefs.client = src + prefs.load_savefile() // just to make sure we have the latest data + prefs.apply_all_client_preferences() + else prefs = new /datum/preferences(src) 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 - prefs.client = src // Only relevant if we reloaded it from the global list, otherwise prefs/New sets it hook_vr("client_new",list(src)) //VOREStation Code. For now this only loads vore prefs, so better put before mob.Login() call but after normal prefs are loaded. @@ -271,7 +274,7 @@ alert = TRUE if(alert) for(var/client/X in GLOB.admins) - if(X.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) + if(X.prefs?.read_preference(/datum/preference/toggle/holder/play_adminhelp_ping)) X << 'sound/effects/tones/newplayerping.ogg' window_flash(X) //VOREStation Edit end. @@ -487,6 +490,12 @@ if(prefs) prefs.ShowChoices(usr) +/client/verb/game_options() + set name = "Game Options" + set category = "Preferences" + if(prefs) + prefs.tgui_interact(usr) + /client/proc/findJoinDate() var/list/http = world.Export("http://byond.com/members/[ckey]?format=text") if(!http) diff --git a/code/modules/client/preference_setup/antagonism/01_basic.dm b/code/modules/client/preference_setup/antagonism/01_basic.dm index 8dc760a00f..3572a2fd62 100644 --- a/code/modules/client/preference_setup/antagonism/01_basic.dm +++ b/code/modules/client/preference_setup/antagonism/01_basic.dm @@ -4,17 +4,17 @@ var/global/list/uplink_locations = list("PDA", "Headset", "None") name = "Basic" sort_order = 1 -/datum/category_item/player_setup_item/antagonism/basic/load_character(var/savefile/S) - S["uplinklocation"] >> pref.uplinklocation - S["exploit_record"] >> pref.exploit_record - S["antag_faction"] >> pref.antag_faction - S["antag_vis"] >> pref.antag_vis +/datum/category_item/player_setup_item/antagonism/basic/load_character(list/save_data) + pref.uplinklocation = save_data["uplinklocation"] + pref.exploit_record = save_data["exploit_record"] + pref.antag_faction = save_data["antag_faction"] + pref.antag_vis = save_data["antag_vis"] -/datum/category_item/player_setup_item/antagonism/basic/save_character(var/savefile/S) - S["uplinklocation"] << pref.uplinklocation - S["exploit_record"] << pref.exploit_record - S["antag_faction"] << pref.antag_faction - S["antag_vis"] << pref.antag_vis +/datum/category_item/player_setup_item/antagonism/basic/save_character(list/save_data) + save_data["uplinklocation"] = pref.uplinklocation + save_data["exploit_record"] = pref.exploit_record + save_data["antag_faction"] = pref.antag_faction + save_data["antag_vis"] = pref.antag_vis /datum/category_item/player_setup_item/antagonism/basic/sanitize_character() pref.uplinklocation = sanitize_inlist(pref.uplinklocation, uplink_locations, initial(pref.uplinklocation)) diff --git a/code/modules/client/preference_setup/antagonism/02_candidacy.dm b/code/modules/client/preference_setup/antagonism/02_candidacy.dm index 945491ed0d..f4eed30278 100644 --- a/code/modules/client/preference_setup/antagonism/02_candidacy.dm +++ b/code/modules/client/preference_setup/antagonism/02_candidacy.dm @@ -31,11 +31,11 @@ var/global/list/special_roles = list( //keep synced with the defines BE_* in set name = "Candidacy" sort_order = 2 -/datum/category_item/player_setup_item/antagonism/candidacy/load_character(var/savefile/S) - S["be_special"] >> pref.be_special +/datum/category_item/player_setup_item/antagonism/candidacy/load_character(list/save_data) + pref.be_special = save_data["be_special"] -/datum/category_item/player_setup_item/antagonism/candidacy/save_character(var/savefile/S) - S["be_special"] << pref.be_special +/datum/category_item/player_setup_item/antagonism/candidacy/save_character(list/save_data) + save_data["be_special"] = pref.be_special /datum/category_item/player_setup_item/antagonism/candidacy/sanitize_character() pref.be_special = sanitize_integer(pref.be_special, 0, 16777215, initial(pref.be_special)) //VOREStation Edit - 24 bits of support diff --git a/code/modules/client/preference_setup/general/01_basic.dm b/code/modules/client/preference_setup/general/01_basic.dm index dc4202f912..fb7b5ed4eb 100644 --- a/code/modules/client/preference_setup/general/01_basic.dm +++ b/code/modules/client/preference_setup/general/01_basic.dm @@ -10,37 +10,37 @@ name = "Basic" sort_order = 1 -/datum/category_item/player_setup_item/general/basic/load_character(var/savefile/S) - S["real_name"] >> pref.real_name - S["nickname"] >> pref.nickname - S["name_is_always_random"] >> pref.be_random_name - S["gender"] >> pref.biological_gender - S["id_gender"] >> pref.identifying_gender - S["age"] >> pref.age - S["bday_month"] >> pref.bday_month - S["bday_day"] >> pref.bday_day - S["last_bday_note"] >> pref.last_birthday_notification - S["bday_announce"] >> pref.bday_announce - S["spawnpoint"] >> pref.spawnpoint - S["OOC_Notes"] >> pref.metadata - S["OOC_Notes_Likes"] >> pref.metadata_likes - S["OOC_Notes_Disikes"] >> pref.metadata_dislikes +/datum/category_item/player_setup_item/general/basic/load_character(list/save_data) + pref.real_name = save_data["real_name"] + pref.nickname = save_data["nickname"] + pref.be_random_name = save_data["name_is_always_random"] + pref.biological_gender = save_data["gender"] + pref.identifying_gender = save_data["id_gender"] + pref.age = save_data["age"] + pref.bday_month = save_data["bday_month"] + pref.bday_day = save_data["bday_day"] + pref.last_birthday_notification = save_data["last_bday_note"] + pref.bday_announce = save_data["bday_announce"] + pref.spawnpoint = save_data["spawnpoint"] + pref.metadata = save_data["OOC_Notes"] + pref.metadata_likes = save_data["OOC_Notes_Likes"] + pref.metadata_dislikes = save_data["OOC_Notes_Disikes"] -/datum/category_item/player_setup_item/general/basic/save_character(var/savefile/S) - S["real_name"] << pref.real_name - S["nickname"] << pref.nickname - S["name_is_always_random"] << pref.be_random_name - S["gender"] << pref.biological_gender - S["id_gender"] << pref.identifying_gender - S["age"] << pref.age - S["bday_month"] << pref.bday_month - S["bday_day"] << pref.bday_day - S["last_bday_note"] << pref.last_birthday_notification - S["bday_announce"] << pref.bday_announce - S["spawnpoint"] << pref.spawnpoint - S["OOC_Notes"] << pref.metadata - S["OOC_Notes_Likes"] << pref.metadata_likes - S["OOC_Notes_Disikes"] << pref.metadata_dislikes +/datum/category_item/player_setup_item/general/basic/save_character(list/save_data) + save_data["real_name"] = pref.real_name + save_data["nickname"] = pref.nickname + save_data["name_is_always_random"] = pref.be_random_name + save_data["gender"] = pref.biological_gender + save_data["id_gender"] = pref.identifying_gender + save_data["age"] = pref.age + save_data["bday_month"] = pref.bday_month + save_data["bday_day"] = pref.bday_day + save_data["last_bday_note"] = pref.last_birthday_notification + save_data["bday_announce"] = pref.bday_announce + save_data["spawnpoint"] = pref.spawnpoint + save_data["OOC_Notes"] = pref.metadata + save_data["OOC_Notes_Likes"] = pref.metadata_likes + save_data["OOC_Notes_Disikes"] = pref.metadata_dislikes /datum/category_item/player_setup_item/general/basic/sanitize_character() pref.age = sanitize_integer(pref.age, get_min_age(), get_max_age(), initial(pref.age)) diff --git a/code/modules/client/preference_setup/general/02_language.dm b/code/modules/client/preference_setup/general/02_language.dm index 0b86c67b04..4c7f408239 100644 --- a/code/modules/client/preference_setup/general/02_language.dm +++ b/code/modules/client/preference_setup/general/02_language.dm @@ -7,25 +7,19 @@ sort_order = 2 var/static/list/forbidden_prefixes = list(";", ":", ".", "!", "*", "^", "-") -/datum/category_item/player_setup_item/general/language/load_character(var/savefile/S) - S["language"] >> pref.alternate_languages - S["extra_languages"] >> pref.extra_languages - if(islist(pref.alternate_languages)) // Because aparently it may not be? - testing("LANGSANI: Loaded from [pref.client]'s character [pref.real_name || "-name not yet loaded-"] savefile: [english_list(pref.alternate_languages || list())]") - S["language_prefixes"] >> pref.language_prefixes - //VORE Edit Begin - S["preflang"] >> pref.preferred_language - //VORE Edit End - S["language_custom_keys"] >> pref.language_custom_keys +/datum/category_item/player_setup_item/general/language/load_character(list/save_data) + pref.alternate_languages = save_data["language"] + pref.extra_languages = save_data["extra_languages"] + pref.language_prefixes = save_data["language_prefixes"] + pref.preferred_language = save_data["preflang"] + pref.language_custom_keys = save_data["language_custom_keys"] -/datum/category_item/player_setup_item/general/language/save_character(var/savefile/S) - S["language"] << pref.alternate_languages - S["extra_languages"] << pref.extra_languages - if(islist(pref.alternate_languages)) // Because aparently it may not be? - testing("LANGSANI: Loaded from [pref.client]'s character [pref.real_name || "-name not yet loaded-"] savefile: [english_list(pref.alternate_languages || list())]") - S["language_prefixes"] << pref.language_prefixes - S["language_custom_keys"] << pref.language_custom_keys - S["preflang"] << pref.preferred_language // VOREStation Edit +/datum/category_item/player_setup_item/general/language/save_character(list/save_data) + save_data["language"] = pref.alternate_languages + save_data["extra_languages"] = pref.extra_languages + save_data["language_prefixes"] = pref.language_prefixes + save_data["language_custom_keys"] = pref.language_custom_keys + save_data["preflang"] = pref.preferred_language /datum/category_item/player_setup_item/general/language/sanitize_character() if(!islist(pref.alternate_languages)) diff --git a/code/modules/client/preference_setup/general/03_body.dm b/code/modules/client/preference_setup/general/03_body.dm index d075e751a6..cadd0a7a4c 100644 --- a/code/modules/client/preference_setup/general/03_body.dm +++ b/code/modules/client/preference_setup/general/03_body.dm @@ -90,135 +90,135 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O name = "Body" sort_order = 3 -/datum/category_item/player_setup_item/general/body/load_character(var/savefile/S) - S["species"] >> pref.species - S["hair_red"] >> pref.r_hair - S["hair_green"] >> pref.g_hair - S["hair_blue"] >> pref.b_hair - S["facial_red"] >> pref.r_facial - S["grad_red"] >> pref.r_grad - S["grad_green"] >> pref.g_grad - S["grad_blue"] >> pref.b_grad - S["facial_green"] >> pref.g_facial - S["facial_blue"] >> pref.b_facial - S["skin_tone"] >> pref.s_tone - S["skin_red"] >> pref.r_skin - S["skin_green"] >> pref.g_skin - S["skin_blue"] >> pref.b_skin - S["hair_style_name"] >> pref.h_style - S["facial_style_name"] >> pref.f_style - S["grad_style_name"] >> pref.grad_style - S["eyes_red"] >> pref.r_eyes - S["eyes_green"] >> pref.g_eyes - S["eyes_blue"] >> pref.b_eyes - S["b_type"] >> pref.b_type - S["disabilities"] >> pref.disabilities - S["organ_data"] >> pref.organ_data - S["rlimb_data"] >> pref.rlimb_data - S["body_markings"] >> pref.body_markings - S["synth_color"] >> pref.synth_color - S["synth_red"] >> pref.r_synth - S["synth_green"] >> pref.g_synth - S["synth_blue"] >> pref.b_synth - S["synth_markings"] >> pref.synth_markings - S["bgstate"] >> pref.bgstate - S["body_descriptors"] >> pref.body_descriptors - S["ear_style"] >> pref.ear_style - S["r_ears"] >> pref.r_ears - S["g_ears"] >> pref.g_ears - S["b_ears"] >> pref.b_ears - S["r_ears2"] >> pref.r_ears2 - S["g_ears2"] >> pref.g_ears2 - S["b_ears2"] >> pref.b_ears2 - S["r_ears3"] >> pref.r_ears3 - S["g_ears3"] >> pref.g_ears3 - S["b_ears3"] >> pref.b_ears3 - S["tail_style"] >> pref.tail_style - S["r_tail"] >> pref.r_tail - S["g_tail"] >> pref.g_tail - S["b_tail"] >> pref.b_tail - S["r_tail2"] >> pref.r_tail2 - S["g_tail2"] >> pref.g_tail2 - S["b_tail2"] >> pref.b_tail2 - S["r_tail3"] >> pref.r_tail3 - S["g_tail3"] >> pref.g_tail3 - S["b_tail3"] >> pref.b_tail3 - S["wing_style"] >> pref.wing_style - S["r_wing"] >> pref.r_wing - S["g_wing"] >> pref.g_wing - S["b_wing"] >> pref.b_wing - S["r_wing2"] >> pref.r_wing2 - S["g_wing2"] >> pref.g_wing2 - S["b_wing2"] >> pref.b_wing2 - S["r_wing3"] >> pref.r_wing3 - S["g_wing3"] >> pref.g_wing3 - S["b_wing3"] >> pref.b_wing3 - S["digitigrade"] >> pref.digitigrade +/datum/category_item/player_setup_item/general/body/load_character(list/save_data) + pref.species = save_data["species"] + pref.r_hair = save_data["hair_red"] + pref.g_hair = save_data["hair_green"] + pref.b_hair = save_data["hair_blue"] + pref.r_facial = save_data["facial_red"] + pref.r_grad = save_data["grad_red"] + pref.g_grad = save_data["grad_green"] + pref.b_grad = save_data["grad_blue"] + pref.g_facial = save_data["facial_green"] + pref.b_facial = save_data["facial_blue"] + pref.s_tone = save_data["skin_tone"] + pref.r_skin = save_data["skin_red"] + pref.g_skin = save_data["skin_green"] + pref.b_skin = save_data["skin_blue"] + pref.h_style = save_data["hair_style_name"] + pref.f_style = save_data["facial_style_name"] + pref.grad_style = save_data["grad_style_name"] + pref.r_eyes = save_data["eyes_red"] + pref.g_eyes = save_data["eyes_green"] + pref.b_eyes = save_data["eyes_blue"] + pref.b_type = save_data["b_type"] + pref.disabilities = save_data["disabilities"] + pref.organ_data = save_data["organ_data"] + pref.rlimb_data = save_data["rlimb_data"] + pref.body_markings = save_data["body_markings"] + pref.synth_color = save_data["synth_color"] + pref.r_synth = save_data["synth_red"] + pref.g_synth = save_data["synth_green"] + pref.b_synth = save_data["synth_blue"] + pref.synth_markings = save_data["synth_markings"] + pref.bgstate = save_data["bgstate"] + pref.body_descriptors = save_data["body_descriptors"] + pref.ear_style = save_data["ear_style"] + pref.r_ears = save_data["r_ears"] + pref.g_ears = save_data["g_ears"] + pref.b_ears = save_data["b_ears"] + pref.r_ears2 = save_data["r_ears2"] + pref.g_ears2 = save_data["g_ears2"] + pref.b_ears2 = save_data["b_ears2"] + pref.r_ears3 = save_data["r_ears3"] + pref.g_ears3 = save_data["g_ears3"] + pref.b_ears3 = save_data["b_ears3"] + pref.tail_style = save_data["tail_style"] + pref.r_tail = save_data["r_tail"] + pref.g_tail = save_data["g_tail"] + pref.b_tail = save_data["b_tail"] + pref.r_tail2 = save_data["r_tail2"] + pref.g_tail2 = save_data["g_tail2"] + pref.b_tail2 = save_data["b_tail2"] + pref.r_tail3 = save_data["r_tail3"] + pref.g_tail3 = save_data["g_tail3"] + pref.b_tail3 = save_data["b_tail3"] + pref.wing_style = save_data["wing_style"] + pref.r_wing = save_data["r_wing"] + pref.g_wing = save_data["g_wing"] + pref.b_wing = save_data["b_wing"] + pref.r_wing2 = save_data["r_wing2"] + pref.g_wing2 = save_data["g_wing2"] + pref.b_wing2 = save_data["b_wing2"] + pref.r_wing3 = save_data["r_wing3"] + pref.g_wing3 = save_data["g_wing3"] + pref.b_wing3 = save_data["b_wing3"] + pref.digitigrade = save_data["digitigrade"] -/datum/category_item/player_setup_item/general/body/save_character(var/savefile/S) - S["species"] << pref.species - S["hair_red"] << pref.r_hair - S["hair_green"] << pref.g_hair - S["hair_blue"] << pref.b_hair - S["grad_red"] << pref.r_grad - S["grad_green"] << pref.g_grad - S["grad_blue"] << pref.b_grad - S["facial_red"] << pref.r_facial - S["facial_green"] << pref.g_facial - S["facial_blue"] << pref.b_facial - S["skin_tone"] << pref.s_tone - S["skin_red"] << pref.r_skin - S["skin_green"] << pref.g_skin - S["skin_blue"] << pref.b_skin - S["hair_style_name"] << pref.h_style - S["facial_style_name"] << pref.f_style - S["grad_style_name"] << pref.grad_style - S["eyes_red"] << pref.r_eyes - S["eyes_green"] << pref.g_eyes - S["eyes_blue"] << pref.b_eyes - S["b_type"] << pref.b_type - S["disabilities"] << pref.disabilities - S["organ_data"] << pref.organ_data - S["rlimb_data"] << pref.rlimb_data - S["body_markings"] << pref.body_markings - S["synth_color"] << pref.synth_color - S["synth_red"] << pref.r_synth - S["synth_green"] << pref.g_synth - S["synth_blue"] << pref.b_synth - S["synth_markings"] << pref.synth_markings - S["bgstate"] << pref.bgstate - S["body_descriptors"] << pref.body_descriptors - S["ear_style"] << pref.ear_style - S["r_ears"] << pref.r_ears - S["g_ears"] << pref.g_ears - S["b_ears"] << pref.b_ears - S["r_ears2"] << pref.r_ears2 - S["g_ears2"] << pref.g_ears2 - S["b_ears2"] << pref.b_ears2 - S["r_ears3"] << pref.r_ears3 - S["g_ears3"] << pref.g_ears3 - S["b_ears3"] << pref.b_ears3 - S["tail_style"] << pref.tail_style - S["r_tail"] << pref.r_tail - S["g_tail"] << pref.g_tail - S["b_tail"] << pref.b_tail - S["r_tail2"] << pref.r_tail2 - S["g_tail2"] << pref.g_tail2 - S["b_tail2"] << pref.b_tail2 - S["r_tail3"] << pref.r_tail3 - S["g_tail3"] << pref.g_tail3 - S["b_tail3"] << pref.b_tail3 - S["wing_style"] << pref.wing_style - S["r_wing"] << pref.r_wing - S["g_wing"] << pref.g_wing - S["b_wing"] << pref.b_wing - S["r_wing2"] << pref.r_wing2 - S["g_wing2"] << pref.g_wing2 - S["b_wing2"] << pref.b_wing2 - S["r_wing3"] << pref.r_wing3 - S["g_wing3"] << pref.g_wing3 - S["b_wing3"] << pref.b_wing3 - S["digitigrade"] << pref.digitigrade +/datum/category_item/player_setup_item/general/body/save_character(list/save_data) + save_data["species"] = pref.species + save_data["hair_red"] = pref.r_hair + save_data["hair_green"] = pref.g_hair + save_data["hair_blue"] = pref.b_hair + save_data["grad_red"] = pref.r_grad + save_data["grad_green"] = pref.g_grad + save_data["grad_blue"] = pref.b_grad + save_data["facial_red"] = pref.r_facial + save_data["facial_green"] = pref.g_facial + save_data["facial_blue"] = pref.b_facial + save_data["skin_tone"] = pref.s_tone + save_data["skin_red"] = pref.r_skin + save_data["skin_green"] = pref.g_skin + save_data["skin_blue"] = pref.b_skin + save_data["hair_style_name"] = pref.h_style + save_data["facial_style_name"] = pref.f_style + save_data["grad_style_name"] = pref.grad_style + save_data["eyes_red"] = pref.r_eyes + save_data["eyes_green"] = pref.g_eyes + save_data["eyes_blue"] = pref.b_eyes + save_data["b_type"] = pref.b_type + save_data["disabilities"] = pref.disabilities + save_data["organ_data"] = pref.organ_data + save_data["rlimb_data"] = pref.rlimb_data + save_data["body_markings"] = pref.body_markings + save_data["synth_color"] = pref.synth_color + save_data["synth_red"] = pref.r_synth + save_data["synth_green"] = pref.g_synth + save_data["synth_blue"] = pref.b_synth + save_data["synth_markings"] = pref.synth_markings + save_data["bgstate"] = pref.bgstate + save_data["body_descriptors"] = pref.body_descriptors + save_data["ear_style"] = pref.ear_style + save_data["r_ears"] = pref.r_ears + save_data["g_ears"] = pref.g_ears + save_data["b_ears"] = pref.b_ears + save_data["r_ears2"] = pref.r_ears2 + save_data["g_ears2"] = pref.g_ears2 + save_data["b_ears2"] = pref.b_ears2 + save_data["r_ears3"] = pref.r_ears3 + save_data["g_ears3"] = pref.g_ears3 + save_data["b_ears3"] = pref.b_ears3 + save_data["tail_style"] = pref.tail_style + save_data["r_tail"] = pref.r_tail + save_data["g_tail"] = pref.g_tail + save_data["b_tail"] = pref.b_tail + save_data["r_tail2"] = pref.r_tail2 + save_data["g_tail2"] = pref.g_tail2 + save_data["b_tail2"] = pref.b_tail2 + save_data["r_tail3"] = pref.r_tail3 + save_data["g_tail3"] = pref.g_tail3 + save_data["b_tail3"] = pref.b_tail3 + save_data["wing_style"] = pref.wing_style + save_data["r_wing"] = pref.r_wing + save_data["g_wing"] = pref.g_wing + save_data["b_wing"] = pref.b_wing + save_data["r_wing2"] = pref.r_wing2 + save_data["g_wing2"] = pref.g_wing2 + save_data["b_wing2"] = pref.b_wing2 + save_data["r_wing3"] = pref.r_wing3 + save_data["g_wing3"] = pref.g_wing3 + save_data["b_wing3"] = pref.b_wing3 + save_data["digitigrade"] = pref.digitigrade /datum/category_item/player_setup_item/general/body/sanitize_character(var/savefile/S) if(!pref.species || !(pref.species in GLOB.playable_species)) diff --git a/code/modules/client/preference_setup/general/04_equipment.dm b/code/modules/client/preference_setup/general/04_equipment.dm index 692e4f7d57..57146e5832 100644 --- a/code/modules/client/preference_setup/general/04_equipment.dm +++ b/code/modules/client/preference_setup/general/04_equipment.dm @@ -6,23 +6,23 @@ name = "Clothing" sort_order = 4 -/datum/category_item/player_setup_item/general/equipment/load_character(var/savefile/S) - S["all_underwear"] >> pref.all_underwear - S["all_underwear_metadata"] >> pref.all_underwear_metadata - S["backbag"] >> pref.backbag - S["pdachoice"] >> pref.pdachoice - S["communicator_visibility"] >> pref.communicator_visibility - S["ringtone"] >> pref.ringtone - S["shoe_hater"] >> pref.shoe_hater //RS ADD +/datum/category_item/player_setup_item/general/equipment/load_character(list/save_data) + pref.all_underwear = save_data["all_underwear"] + pref.all_underwear_metadata = save_data["all_underwear_metadata"] + pref.backbag = save_data["backbag"] + pref.pdachoice = save_data["pdachoice"] + pref.communicator_visibility = save_data["communicator_visibility"] + pref.ringtone = save_data["ringtone"] + pref.shoe_hater = save_data["shoe_hater"] -/datum/category_item/player_setup_item/general/equipment/save_character(var/savefile/S) - S["all_underwear"] << pref.all_underwear - S["all_underwear_metadata"] << pref.all_underwear_metadata - S["backbag"] << pref.backbag - S["pdachoice"] << pref.pdachoice - S["communicator_visibility"] << pref.communicator_visibility - S["ringtone"] << pref.ringtone - S["shoe_hater"] << pref.shoe_hater //RS ADD +/datum/category_item/player_setup_item/general/equipment/save_character(list/save_data) + save_data["all_underwear"] = pref.all_underwear + save_data["all_underwear_metadata"] = pref.all_underwear_metadata + save_data["backbag"] = pref.backbag + save_data["pdachoice"] = pref.pdachoice + save_data["communicator_visibility"] = pref.communicator_visibility + save_data["ringtone"] = pref.ringtone + save_data["shoe_hater"] = pref.shoe_hater var/global/list/valid_ringtones = list( "beep", diff --git a/code/modules/client/preference_setup/general/05_background.dm b/code/modules/client/preference_setup/general/05_background.dm index 70b2e130bd..03efdf9f86 100644 --- a/code/modules/client/preference_setup/general/05_background.dm +++ b/code/modules/client/preference_setup/general/05_background.dm @@ -1,28 +1,27 @@ /datum/category_item/player_setup_item/general/background name = "Background" sort_order = 5 +/datum/category_item/player_setup_item/general/background/load_character(list/save_data) + pref.med_record = save_data["med_record"] + pref.sec_record = save_data["sec_record"] + pref.gen_record = save_data["gen_record"] + pref.home_system = save_data["home_system"] + pref.birthplace = save_data["birthplace"] + pref.citizenship = save_data["citizenship"] + pref.faction = save_data["faction"] + pref.religion = save_data["religion"] + pref.economic_status = save_data["economic_status"] -/datum/category_item/player_setup_item/general/background/load_character(var/savefile/S) - S["med_record"] >> pref.med_record - S["sec_record"] >> pref.sec_record - S["gen_record"] >> pref.gen_record - S["home_system"] >> pref.home_system - S["birthplace"] >> pref.birthplace - S["citizenship"] >> pref.citizenship - S["faction"] >> pref.faction - S["religion"] >> pref.religion - S["economic_status"] >> pref.economic_status - -/datum/category_item/player_setup_item/general/background/save_character(var/savefile/S) - S["med_record"] << pref.med_record - S["sec_record"] << pref.sec_record - S["gen_record"] << pref.gen_record - S["home_system"] << pref.home_system - S["birthplace"] << pref.birthplace - S["citizenship"] << pref.citizenship - S["faction"] << pref.faction - S["religion"] << pref.religion - S["economic_status"] << pref.economic_status +/datum/category_item/player_setup_item/general/background/save_character(list/save_data) + save_data["med_record"] = pref.med_record + save_data["sec_record"] = pref.sec_record + save_data["gen_record"] = pref.gen_record + save_data["home_system"] = pref.home_system + save_data["birthplace"] = pref.birthplace + save_data["citizenship"] = pref.citizenship + save_data["faction"] = pref.faction + save_data["religion"] = pref.religion + save_data["economic_status"] = pref.economic_status /datum/category_item/player_setup_item/general/background/sanitize_character() if(!pref.home_system) pref.home_system = "Unset" diff --git a/code/modules/client/preference_setup/general/06_flavor.dm b/code/modules/client/preference_setup/general/06_flavor.dm index 76af38f5dc..fa14cce603 100644 --- a/code/modules/client/preference_setup/general/06_flavor.dm +++ b/code/modules/client/preference_setup/general/06_flavor.dm @@ -2,37 +2,37 @@ name = "Flavor" sort_order = 6 -/datum/category_item/player_setup_item/general/flavor/load_character(var/savefile/S) - S["flavor_texts_general"] >> pref.flavor_texts["general"] - S["flavor_texts_head"] >> pref.flavor_texts["head"] - S["flavor_texts_face"] >> pref.flavor_texts["face"] - S["flavor_texts_eyes"] >> pref.flavor_texts["eyes"] - S["flavor_texts_torso"] >> pref.flavor_texts["torso"] - S["flavor_texts_arms"] >> pref.flavor_texts["arms"] - S["flavor_texts_hands"] >> pref.flavor_texts["hands"] - S["flavor_texts_legs"] >> pref.flavor_texts["legs"] - S["flavor_texts_feet"] >> pref.flavor_texts["feet"] - S["custom_link"] >> pref.custom_link +/datum/category_item/player_setup_item/general/flavor/load_character(list/save_data) + pref.flavor_texts["general"] = save_data["flavor_texts_general"] + pref.flavor_texts["head"] = save_data["flavor_texts_head"] + pref.flavor_texts["face"] = save_data["flavor_texts_face"] + pref.flavor_texts["eyes"] = save_data["flavor_texts_eyes"] + pref.flavor_texts["torso"] = save_data["flavor_texts_torso"] + pref.flavor_texts["arms"] = save_data["flavor_texts_arms"] + pref.flavor_texts["hands"] = save_data["flavor_texts_hands"] + pref.flavor_texts["legs"] = save_data["flavor_texts_legs"] + pref.flavor_texts["feet"] = save_data["flavor_texts_feet"] + pref.custom_link = save_data["custom_link"] //Flavour text for robots. - S["flavour_texts_robot_Default"] >> pref.flavour_texts_robot["Default"] + pref.flavour_texts_robot["Default"] = save_data["flavour_texts_robot_Default"] for(var/module in robot_module_types) - S["flavour_texts_robot_[module]"] >> pref.flavour_texts_robot[module] + pref.flavour_texts_robot[module] = save_data["flavour_texts_robot_[module]"] -/datum/category_item/player_setup_item/general/flavor/save_character(var/savefile/S) - S["flavor_texts_general"] << pref.flavor_texts["general"] - S["flavor_texts_head"] << pref.flavor_texts["head"] - S["flavor_texts_face"] << pref.flavor_texts["face"] - S["flavor_texts_eyes"] << pref.flavor_texts["eyes"] - S["flavor_texts_torso"] << pref.flavor_texts["torso"] - S["flavor_texts_arms"] << pref.flavor_texts["arms"] - S["flavor_texts_hands"] << pref.flavor_texts["hands"] - S["flavor_texts_legs"] << pref.flavor_texts["legs"] - S["flavor_texts_feet"] << pref.flavor_texts["feet"] - S["custom_link"] << pref.custom_link +/datum/category_item/player_setup_item/general/flavor/save_character(list/save_data) + save_data["flavor_texts_general"] = pref.flavor_texts["general"] + save_data["flavor_texts_head"] = pref.flavor_texts["head"] + save_data["flavor_texts_face"] = pref.flavor_texts["face"] + save_data["flavor_texts_eyes"] = pref.flavor_texts["eyes"] + save_data["flavor_texts_torso"] = pref.flavor_texts["torso"] + save_data["flavor_texts_arms"] = pref.flavor_texts["arms"] + save_data["flavor_texts_hands"] = pref.flavor_texts["hands"] + save_data["flavor_texts_legs"] = pref.flavor_texts["legs"] + save_data["flavor_texts_feet"] = pref.flavor_texts["feet"] + save_data["custom_link"] = pref.custom_link - S["flavour_texts_robot_Default"] << pref.flavour_texts_robot["Default"] + save_data["flavour_texts_robot_Default"] = pref.flavour_texts_robot["Default"] for(var/module in robot_module_types) - S["flavour_texts_robot_[module]"] << pref.flavour_texts_robot[module] + save_data["flavour_texts_robot_[module]"] = pref.flavour_texts_robot[module] /datum/category_item/player_setup_item/general/flavor/sanitize_character() return diff --git a/code/modules/client/preference_setup/global/01_ui.dm b/code/modules/client/preference_setup/global/01_ui.dm index 3130aade42..91bbfea86a 100644 --- a/code/modules/client/preference_setup/global/01_ui.dm +++ b/code/modules/client/preference_setup/global/01_ui.dm @@ -2,41 +2,41 @@ name = "UI" sort_order = 1 -/datum/category_item/player_setup_item/player_global/ui/load_preferences(var/savefile/S) - S["UI_style"] >> pref.UI_style - S["UI_style_color"] >> pref.UI_style_color - S["UI_style_alpha"] >> pref.UI_style_alpha - S["ooccolor"] >> pref.ooccolor - S["tooltipstyle"] >> pref.tooltipstyle - S["client_fps"] >> pref.client_fps - S["ambience_freq"] >> pref.ambience_freq - S["ambience_chance"] >> pref.ambience_chance - S["tgui_fancy"] >> pref.tgui_fancy - S["tgui_lock"] >> pref.tgui_lock - S["tgui_input_mode"] >> pref.tgui_input_mode - S["tgui_large_buttons"] >> pref.tgui_large_buttons - S["tgui_swapped_buttons"] >> pref.tgui_swapped_buttons - S["obfuscate_key"] >> pref.obfuscate_key - S["obfuscate_job"] >> pref.obfuscate_job - S["chat_timestamp"] >> pref.chat_timestamp +/datum/category_item/player_setup_item/player_global/ui/load_preferences(datum/json_savefile/savefile) + pref.UI_style = savefile.get_entry("UI_style") + pref.UI_style_color = savefile.get_entry("UI_style_color") + pref.UI_style_alpha = savefile.get_entry("UI_style_alpha") + pref.ooccolor = savefile.get_entry("ooccolor") + pref.tooltipstyle = savefile.get_entry("tooltipstyle") + pref.client_fps = savefile.get_entry("client_fps") + pref.ambience_freq = savefile.get_entry("ambience_freq") + pref.ambience_chance = savefile.get_entry("ambience_chance") + pref.tgui_fancy = savefile.get_entry("tgui_fancy") + pref.tgui_lock = savefile.get_entry("tgui_lock") + pref.tgui_input_mode = savefile.get_entry("tgui_input_mode") + pref.tgui_large_buttons = savefile.get_entry("tgui_large_buttons") + pref.tgui_swapped_buttons = savefile.get_entry("tgui_swapped_buttons") + pref.obfuscate_key = savefile.get_entry("obfuscate_key") + pref.obfuscate_job = savefile.get_entry("obfuscate_job") + pref.chat_timestamp = savefile.get_entry("chat_timestamp") -/datum/category_item/player_setup_item/player_global/ui/save_preferences(var/savefile/S) - S["UI_style"] << pref.UI_style - S["UI_style_color"] << pref.UI_style_color - S["UI_style_alpha"] << pref.UI_style_alpha - S["ooccolor"] << pref.ooccolor - S["tooltipstyle"] << pref.tooltipstyle - S["client_fps"] << pref.client_fps - S["ambience_freq"] << pref.ambience_freq - S["ambience_chance"] << pref.ambience_chance - S["tgui_fancy"] << pref.tgui_fancy - S["tgui_lock"] << pref.tgui_lock - S["tgui_input_mode"] << pref.tgui_input_mode - S["tgui_large_buttons"] << pref.tgui_large_buttons - S["tgui_swapped_buttons"] << pref.tgui_swapped_buttons - S["obfuscate_key"] << pref.obfuscate_key - S["obfuscate_job"] << pref.obfuscate_job - S["chat_timestamp"] << pref.chat_timestamp +/datum/category_item/player_setup_item/player_global/ui/save_preferences(datum/json_savefile/savefile) + savefile.set_entry("UI_style", pref.UI_style) + savefile.set_entry("UI_style_color", pref.UI_style_color) + savefile.set_entry("UI_style_alpha", pref.UI_style_alpha) + savefile.set_entry("ooccolor", pref.ooccolor) + savefile.set_entry("tooltipstyle", pref.tooltipstyle) + savefile.set_entry("client_fps", pref.client_fps) + savefile.set_entry("ambience_freq", pref.ambience_freq) + savefile.set_entry("ambience_chance", pref.ambience_chance) + savefile.set_entry("tgui_fancy", pref.tgui_fancy) + savefile.set_entry("tgui_lock", pref.tgui_lock) + savefile.set_entry("tgui_input_mode", pref.tgui_input_mode) + savefile.set_entry("tgui_large_buttons", pref.tgui_large_buttons) + savefile.set_entry("tgui_swapped_buttons", pref.tgui_swapped_buttons) + savefile.set_entry("obfuscate_key", pref.obfuscate_key) + savefile.set_entry("obfuscate_job", pref.obfuscate_job) + savefile.set_entry("chat_timestamp", pref.chat_timestamp) /datum/category_item/player_setup_item/player_global/ui/sanitize_preferences() pref.UI_style = sanitize_inlist(pref.UI_style, all_ui_styles, initial(pref.UI_style)) diff --git a/code/modules/client/preference_setup/global/02_settings.dm b/code/modules/client/preference_setup/global/02_settings.dm index 71ff5d0a09..3a63b9f0cc 100644 --- a/code/modules/client/preference_setup/global/02_settings.dm +++ b/code/modules/client/preference_setup/global/02_settings.dm @@ -1,141 +1,20 @@ -/datum/preferences - var/preferences_enabled = null - var/preferences_disabled = null - /datum/category_item/player_setup_item/player_global/settings name = "Settings" sort_order = 2 -/datum/category_item/player_setup_item/player_global/settings/load_preferences(var/savefile/S) - S["lastchangelog"] >> pref.lastchangelog - S["lastnews"] >> pref.lastnews - S["lastlorenews"] >> pref.lastlorenews - S["default_slot"] >> pref.default_slot - S["preferences"] >> pref.preferences_enabled - S["preferences_disabled"] >> pref.preferences_disabled +/datum/category_item/player_setup_item/player_global/settings/load_preferences(datum/json_savefile/savefile) + pref.lastchangelog = savefile.get_entry("lastchangelog") + pref.lastnews = savefile.get_entry("lastnews") + pref.lastlorenews = savefile.get_entry("lastlorenews") + pref.default_slot = savefile.get_entry("default_slot") -/datum/category_item/player_setup_item/player_global/settings/save_preferences(var/savefile/S) - S["lastchangelog"] << pref.lastchangelog - S["lastnews"] << pref.lastnews - S["lastlorenews"] << pref.lastlorenews - S["default_slot"] << pref.default_slot - S["preferences"] << pref.preferences_enabled - S["preferences_disabled"] << pref.preferences_disabled +/datum/category_item/player_setup_item/player_global/settings/save_preferences(datum/json_savefile/savefile) + savefile.set_entry("lastchangelog", pref.lastchangelog) + savefile.set_entry("lastnews", pref.lastnews) + savefile.set_entry("lastlorenews", pref.lastlorenews) + savefile.set_entry("default_slot", pref.default_slot) /datum/category_item/player_setup_item/player_global/settings/sanitize_preferences() - // Ensure our preferences are lists. - if(!istype(pref.preferences_enabled, /list)) - pref.preferences_enabled = list() - if(!istype(pref.preferences_disabled, /list)) - pref.preferences_disabled = list() - - // Arrange preferences that have never been enabled/disabled. - var/list/client_preference_keys = list() - for(var/datum/client_preference/client_pref as anything in get_client_preferences()) - client_preference_keys += client_pref.key - if((client_pref.key in pref.preferences_enabled) || (client_pref.key in pref.preferences_disabled)) - continue - - if(client_pref.enabled_by_default) - pref.preferences_enabled += client_pref.key - else - pref.preferences_disabled += client_pref.key - - // Clean out preferences that no longer exist. - for(var/key in pref.preferences_enabled) - if(!(key in client_preference_keys)) - pref.preferences_enabled -= key - for(var/key in pref.preferences_disabled) - if(!(key in client_preference_keys)) - pref.preferences_disabled -= key - pref.lastchangelog = sanitize_text(pref.lastchangelog, initial(pref.lastchangelog)) pref.lastnews = sanitize_text(pref.lastnews, initial(pref.lastnews)) pref.default_slot = sanitize_integer(pref.default_slot, 1, config.character_slots, initial(pref.default_slot)) - -/datum/category_item/player_setup_item/player_global/settings/content(var/mob/user) - . = list() - . += "Preferences
" - . += "" - var/mob/pref_mob = preference_mob() - for(var/datum/client_preference/client_pref as anything in get_client_preferences()) - if(!client_pref.may_toggle(pref_mob)) - continue - - . += "" - if(pref_mob.is_preference_enabled(client_pref.key)) - . += "" - else - . += "" - . += "" - - . += "
[client_pref.description]: [client_pref.enabled_description] [client_pref.disabled_description][client_pref.enabled_description] [client_pref.disabled_description]
" - return jointext(., "") - -/datum/category_item/player_setup_item/player_global/settings/OnTopic(var/href,var/list/href_list, var/mob/user) - var/mob/pref_mob = preference_mob() - if(href_list["toggle_on"]) - . = pref_mob.set_preference(href_list["toggle_on"], TRUE) - else if(href_list["toggle_off"]) - . = pref_mob.set_preference(href_list["toggle_off"], FALSE) - if(.) - return TOPIC_REFRESH - - return ..() - -/** - * This can take either a single preference datum or a list of preferences, and will return true if *all* preferences in the arguments are enabled. - */ -/client/proc/is_preference_enabled(var/preference) - if(!islist(preference)) - preference = list(preference) - for(var/p in preference) - var/datum/client_preference/cp = get_client_preference(p) - if(!prefs || !cp || !istype(cp, /datum/client_preference) || !(cp.key in prefs.preferences_enabled)) - return FALSE - return TRUE - -/client/proc/set_preference(var/preference, var/set_preference) - var/datum/client_preference/cp = get_client_preference(preference) - if(!cp) - return FALSE - preference = cp.key - - if(set_preference && !(preference in prefs.preferences_enabled)) - return toggle_preference(cp) - else if(!set_preference && (preference in prefs.preferences_enabled)) - return toggle_preference(cp) - -/client/proc/toggle_preference(var/preference, var/set_preference) - var/datum/client_preference/cp = get_client_preference(preference) - if(!cp) - return FALSE - preference = cp.key - - var/enabled - if(preference in prefs.preferences_disabled) - prefs.preferences_enabled |= preference - prefs.preferences_disabled -= preference - enabled = TRUE - . = TRUE - else if(preference in prefs.preferences_enabled) - prefs.preferences_enabled -= preference - prefs.preferences_disabled |= preference - enabled = FALSE - . = TRUE - if(.) - cp.toggled(mob, enabled) - -/mob/proc/is_preference_enabled(var/preference) - if(!client) - return FALSE - return client.is_preference_enabled(preference) - -/mob/proc/set_preference(var/preference, var/set_preference) - if(!client) - return FALSE - if(!client.prefs) - log_debug("Client prefs found to be null for mob [src] and client [ckey], this should be investigated.") - return FALSE - - return client.set_preference(preference, set_preference) diff --git a/code/modules/client/preference_setup/global/03_pai.dm b/code/modules/client/preference_setup/global/03_pai.dm index c4d81e552b..d9b20873d8 100644 --- a/code/modules/client/preference_setup/global/03_pai.dm +++ b/code/modules/client/preference_setup/global/03_pai.dm @@ -4,7 +4,7 @@ var/datum/paiCandidate/candidate -/datum/category_item/player_setup_item/player_global/pai/load_preferences(var/savefile/S) +/datum/category_item/player_setup_item/player_global/pai/load_preferences(datum/json_savefile/savefile) if(!candidate) candidate = new() var/preference_mob = preference_mob() @@ -18,7 +18,7 @@ candidate.savefile_load(preference_mob) -/datum/category_item/player_setup_item/player_global/pai/save_preferences(var/savefile/S) +/datum/category_item/player_setup_item/player_global/pai/save_preferences(datum/json_savefile/savefile) if(!candidate) return diff --git a/code/modules/client/preference_setup/global/04_ooc.dm b/code/modules/client/preference_setup/global/04_ooc.dm index e695bee677..c375107754 100644 --- a/code/modules/client/preference_setup/global/04_ooc.dm +++ b/code/modules/client/preference_setup/global/04_ooc.dm @@ -2,12 +2,12 @@ name = "OOC" sort_order = 4 -/datum/category_item/player_setup_item/player_global/ooc/load_preferences(var/savefile/S) - S["ignored_players"] >> pref.ignored_players +/datum/category_item/player_setup_item/player_global/ooc/load_preferences(datum/json_savefile/savefile) + pref.ignored_players = savefile.get_entry("ignored_players") -/datum/category_item/player_setup_item/player_global/ooc/save_preferences(var/savefile/S) - S["ignored_players"] << pref.ignored_players +/datum/category_item/player_setup_item/player_global/ooc/save_preferences(datum/json_savefile/savefile) + savefile.set_entry("ignored_players", pref.ignored_players) /* /datum/category_item/player_setup_item/player_global/ooc/sanitize_preferences() @@ -39,4 +39,4 @@ return TOPIC_REFRESH return ..() -*/ \ No newline at end of file +*/ diff --git a/code/modules/client/preference_setup/global/setting_datums.dm b/code/modules/client/preference_setup/global/setting_datums.dm deleted file mode 100644 index 7d74e5bd0d..0000000000 --- a/code/modules/client/preference_setup/global/setting_datums.dm +++ /dev/null @@ -1,481 +0,0 @@ -var/list/_client_preferences -var/list/_client_preferences_by_key -var/list/_client_preferences_by_type - -/proc/get_client_preferences() - if(!_client_preferences) - _client_preferences = list() - for(var/datum/client_preference/client_type as anything in subtypesof(/datum/client_preference)) - if(initial(client_type.description)) - _client_preferences += new client_type() - return _client_preferences - -/proc/get_client_preference(var/datum/client_preference/preference) - if(istype(preference)) - return preference - if(ispath(preference)) - return get_client_preference_by_type(preference) - return get_client_preference_by_key(preference) - -/proc/get_client_preference_by_key(var/preference) - if(!_client_preferences_by_key) - _client_preferences_by_key = list() - for(var/datum/client_preference/client_pref as anything in get_client_preferences()) - _client_preferences_by_key[client_pref.key] = client_pref - return _client_preferences_by_key[preference] - -/proc/get_client_preference_by_type(var/preference) - if(!_client_preferences_by_type) - _client_preferences_by_type = list() - for(var/datum/client_preference/client_pref as anything in get_client_preferences()) - _client_preferences_by_type[client_pref.type] = client_pref - return _client_preferences_by_type[preference] - -/datum/client_preference - var/description - var/key - var/enabled_by_default = TRUE - var/enabled_description = "Yes" - var/disabled_description = "No" - -/datum/client_preference/proc/may_toggle(var/mob/preference_mob) - return TRUE - -/datum/client_preference/proc/toggled(var/mob/preference_mob, var/enabled) - return - -/********************* -* Player Preferences * -*********************/ - -/datum/client_preference/play_admin_midis - description ="Play admin midis" - key = "SOUND_MIDI" - -/datum/client_preference/play_lobby_music - description ="Play lobby music" - key = "SOUND_LOBBY" - -/datum/client_preference/play_lobby_music/toggled(var/mob/preference_mob, var/enabled) - if(!preference_mob.client || !preference_mob.client.media) - return - - if(enabled) - preference_mob.client.playtitlemusic() - else - preference_mob.client.media.stop_music() - -/datum/client_preference/play_ambiance - description ="Play ambience" - key = "SOUND_AMBIENCE" - -/datum/client_preference/play_ambiance/toggled(var/mob/preference_mob, var/enabled) - if(!enabled) - preference_mob << sound(null, repeat = 0, wait = 0, volume = 0, channel = 1) - preference_mob << sound(null, repeat = 0, wait = 0, volume = 0, channel = 2) -//VOREStation Add - Need to put it here because it should be ordered riiiight here. -/datum/client_preference/play_jukebox - description ="Play jukebox music" - key = "SOUND_JUKEBOX" - -/datum/client_preference/play_jukebox/toggled(var/mob/preference_mob, var/enabled) - if(!enabled) - preference_mob.stop_all_music() - else - preference_mob.update_music() - -/datum/client_preference/eating_noises - description = "Eating Noises" - key = "EATING_NOISES" - enabled_description = "Noisy" - disabled_description = "Silent" - -/datum/client_preference/digestion_noises - description = "Digestion Noises" - key = "DIGEST_NOISES" - enabled_description = "Noisy" - disabled_description = "Silent" - -/datum/client_preference/belch_noises // Belching noises - pref toggle for 'em - description = "Burping" - key = "BELCH_NOISES" - enabled_description = "Noisy" - disabled_description = "Silent" - -/datum/client_preference/emote_noises - description = "Emote Noises" //MERP - key = "EMOTE_NOISES" - enabled_description = "Noisy" - disabled_description = "Silent" -/datum/client_preference/whisubtle_vis - description = "Whi/Subtles Ghost Visible" - key = "WHISUBTLE_VIS" - enabled_description = "Visible" - disabled_description = "Hidden" - enabled_by_default = FALSE - -/datum/client_preference/ghost_see_whisubtle - description = "See subtles/whispers as ghost" - key = "GHOST_SEE_WHISUBTLE" - enabled_description = "Visible" - disabled_description = "Hidden" - enabled_by_default = TRUE -//VOREStation Add End -/datum/client_preference/weather_sounds - description ="Weather sounds" - key = "SOUND_WEATHER" - enabled_description = "Audible" - disabled_description = "Silent" - -/datum/client_preference/supermatter_hum - description ="Supermatter hum" - key = "SOUND_SUPERMATTER" - enabled_description = "Audible" - disabled_description = "Silent" - -/datum/client_preference/ghost_ears - description ="Ghost ears" - key = "CHAT_GHOSTEARS" - enabled_description = "All Speech" - disabled_description = "Nearby" - -/datum/client_preference/ghost_sight - description ="Ghost sight" - key = "CHAT_GHOSTSIGHT" - enabled_description = "All Emotes" - disabled_description = "Nearby" - -/datum/client_preference/ghost_radio - description ="Ghost radio" - key = "CHAT_GHOSTRADIO" - enabled_description = "All Chatter" - disabled_description = "Nearby" - -/datum/client_preference/chat_tags - description ="Chat tags" - key = "CHAT_SHOWICONS" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/air_pump_noise - description ="Air Pump Ambient Noise" - key = "SOUND_AIRPUMP" - enabled_description = "Audible" - disabled_description = "Silent" - -/datum/client_preference/old_door_sounds - description ="Old Door Sounds" - key = "SOUND_OLDDOORS" - enabled_description = "Old" - disabled_description = "New" - -/datum/client_preference/department_door_sounds - description ="Department-Specific Door Sounds" - key = "SOUND_DEPARTMENTDOORS" - enabled_description = "Enabled" - disabled_description = "Disabled" - -/datum/client_preference/pickup_sounds - description = "Picked Up Item Sounds" - key = "SOUND_PICKED" - enabled_description = "Enabled" - disabled_description = "Disabled" - -/datum/client_preference/drop_sounds - description = "Dropped Item Sounds" - key = "SOUND_DROPPED" - enabled_description = "Enabled" - disabled_description = "Disabled" - -/datum/client_preference/mob_tooltips - description ="Mob tooltips" - key = "MOB_TOOLTIPS" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/inv_tooltips - description ="Inventory tooltips" - key = "INV_TOOLTIPS" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/attack_icons - description ="Attack icons" - key = "ATTACK_ICONS" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/precision_placement - description ="Precision Placement" - key = "PRECISE_PLACEMENT" - enabled_description = "Active" - disabled_description = "Inactive" - -/datum/client_preference/hotkeys_default - description ="Hotkeys Default" - key = "HUD_HOTKEYS" - enabled_description = "Enabled" - disabled_description = "Disabled" - enabled_by_default = FALSE // Backwards compatibility - -/datum/client_preference/show_typing_indicator - description ="Typing indicator" - key = "SHOW_TYPING" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/show_typing_indicator/toggled(var/mob/preference_mob, var/enabled) - if(!enabled) - preference_mob.client?.stop_thinking() - -/datum/client_preference/show_typing_indicator_subtle - description ="Typing indicator (subtle)" - key = "SHOW_TYPING_SUBTLE" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/show_ooc - description ="OOC chat" - key = "CHAT_OOC" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/show_looc - description ="LOOC chat" - key = "CHAT_LOOC" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/show_dsay - description ="Dead chat" - key = "CHAT_DEAD" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/check_mention - description ="Emphasize Name Mention" - key = "CHAT_MENTION" - enabled_description = "Emphasize" - disabled_description = "Normal" - -/datum/client_preference/show_progress_bar - description ="Progress Bar" - key = "SHOW_PROGRESS" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/safefiring - description = "Gun Firing Intent Requirement" - key = "SAFE_FIRING" - enabled_description = "Safe" - disabled_description = "Dangerous" - -/datum/client_preference/browser_style - description = "Fake NanoUI Browser Style" - key = "BROWSER_STYLED" - enabled_description = "Fancy" - disabled_description = "Plain" - -/datum/client_preference/ambient_occlusion - description = "Fake Ambient Occlusion" - key = "AMBIENT_OCCLUSION_PREF" - enabled_by_default = FALSE - enabled_description = "On" - disabled_description = "Off" - -/datum/client_preference/ambient_occlusion/toggled(var/mob/preference_mob, var/enabled) - . = ..() - if(preference_mob && preference_mob.plane_holder) - var/datum/plane_holder/PH = preference_mob.plane_holder - PH.set_ao(VIS_OBJS, enabled) - PH.set_ao(VIS_MOBS, enabled) - -/datum/client_preference/instrument_toggle - description ="Hear In-game Instruments" - key = "SOUND_INSTRUMENT" - -/datum/client_preference/vchat_enable - description = "Enable/Disable TGChat" - key = "VCHAT_ENABLE" - enabled_description = "Enabled" - disabled_description = "Disabled" - -/datum/client_preference/status_indicators - description = "Status Indicators" - key = "SHOW_STATUS" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/radio_sounds - description = "Radio Sounds" - key = "RADIO_SOUNDS" - enabled_description = "On" - disabled_description = "Off" - -/datum/client_preference/say_sounds - description = "Say Sounds" - key = "SAY_SOUNDS" - enabled_description = "On" - disabled_description = "Off" - -/datum/client_preference/emote_sounds - description = "Me Sounds" - key = "EMOTE_SOUNDS" - enabled_description = "On" - disabled_description = "Off" - -/datum/client_preference/whisper_sounds - description = "Whisper Sounds" - key = "WHISPER_SOUNDS" - enabled_description = "On" - disabled_description = "Off" - -/datum/client_preference/subtle_sounds - description = "Subtle Sounds" - key = "SUBTLE_SOUNDS" - enabled_description = "On" - disabled_description = "Off" - -/datum/client_preference/vore_health_bars - description = "Vore Health Bars" - key = "VORE_HEALTH_BARS" - enabled_description = "Enabled" - disabled_description = "Disabled" - -/datum/client_preference/runechat_mob - description = "Runechat (Mobs)" - key = "RUNECHAT_MOB" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/runechat_obj - description = "Runechat (Objs)" - key = "RUNECHAT_OBJ" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/runechat_border - description = "Runechat Message Border" - key = "RUNECHAT_BORDER" - enabled_description = "Show" - disabled_description = "Hide" - enabled_by_default = TRUE - -/datum/client_preference/runechat_long_messages - description = "Runechat Message Length" - key = "RUNECHAT_LONG" - enabled_description = "Long" - disabled_description = "Short" - enabled_by_default = FALSE - -/datum/client_preference/status_indicators/toggled(mob/preference_mob, enabled) - . = ..() - if(preference_mob && preference_mob.plane_holder) - var/datum/plane_holder/PH = preference_mob.plane_holder - PH.set_vis(VIS_STATUS, enabled) - -/datum/client_preference/show_lore_news - description = "Lore News Popup" - key = "NEWS_POPUP" - enabled_by_default = TRUE - enabled_description = "Popup New On Login" - disabled_description = "Do Nothing" - -/datum/client_preference/play_mentorhelp_ping - description = "Mentorhelps" - key = "SOUND_MENTORHELP" - enabled_description = "Hear" - disabled_description = "Silent" - -/datum/client_preference/player_tips - description = "Receive Tips Periodically" - key = "RECEIVE_TIPS" - enabled_description = "Enabled" - disabled_description = "Disabled" - -/datum/client_preference/pain_frequency - description = "Pain Messages Cooldown" - key = "PAIN_FREQUENCY" - enabled_by_default = FALSE - enabled_description = "Extended" - disabled_description = "Default" - -/datum/client_preference/auto_afk - description = "Automatic AFK Status" - key = "AUTO_AFK" - enabled_by_default = TRUE - enabled_description = "Automatic" - disabled_description = "Manual Only" - -/datum/client_preference/tgui_say - description = "TGUI Say: Use TGUI For Say Input" - key = "TGUI_SAY" - enabled_by_default = TRUE - enabled_description = "Yes" - disabled_description = "No" - -/datum/client_preference/tgui_say_light - description = "TGUI Say: Use Light Mode" - key = "TGUI_SAY_LIGHT_MODE" - enabled_by_default = FALSE - enabled_description = "Yes" - disabled_description = "No" - -/******************** -* Staff Preferences * -********************/ -/datum/client_preference/admin/may_toggle(var/mob/preference_mob) - return check_rights(R_ADMIN|R_EVENT, 0, preference_mob) - -/datum/client_preference/mod/may_toggle(var/mob/preference_mob) - return check_rights(R_MOD|R_ADMIN, 0, preference_mob) - -/datum/client_preference/debug/may_toggle(var/mob/preference_mob) - return check_rights(R_DEBUG|R_ADMIN, 0, preference_mob) - -/datum/client_preference/mod/show_attack_logs - description = "Attack Log Messages" - key = "CHAT_ATTACKLOGS" - enabled_description = "Show" - disabled_description = "Hide" - enabled_by_default = FALSE - -/datum/client_preference/debug/show_debug_logs - description = "Debug Log Messages" - key = "CHAT_DEBUGLOGS" - enabled_description = "Show" - disabled_description = "Hide" - enabled_by_default = FALSE - -/datum/client_preference/admin/show_chat_prayers - description = "Chat Prayers" - key = "CHAT_PRAYER" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/holder/may_toggle(var/mob/preference_mob) - return preference_mob && preference_mob.client && preference_mob.client.holder - -/datum/client_preference/holder/play_adminhelp_ping - description = "Adminhelps" - key = "SOUND_ADMINHELP" - enabled_description = "Hear" - disabled_description = "Silent" - -/datum/client_preference/holder/hear_radio - description = "Radio chatter" - key = "CHAT_RADIO" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/holder/show_rlooc - description ="Remote LOOC chat" - key = "CHAT_RLOOC" - enabled_description = "Show" - disabled_description = "Hide" - -/datum/client_preference/holder/show_staff_dsay - description ="Staff Deadchat" - key = "CHAT_ADSAY" - enabled_description = "Show" - disabled_description = "Hide" diff --git a/code/modules/client/preference_setup/loadout/loadout.dm b/code/modules/client/preference_setup/loadout/loadout.dm index 21e6602863..fd92052458 100644 --- a/code/modules/client/preference_setup/loadout/loadout.dm +++ b/code/modules/client/preference_setup/loadout/loadout.dm @@ -45,19 +45,19 @@ var/list/gear_datums = list() sort_order = 1 var/current_tab = "General" -/datum/category_item/player_setup_item/loadout/load_character(var/savefile/S) - from_file(S["gear_list"], pref.gear_list) - from_file(S["gear_slot"], pref.gear_slot) +/datum/category_item/player_setup_item/loadout/load_character(list/save_data) + pref.gear_list = save_data["gear_list"] + pref.gear_slot = save_data["gear_slot"] if(pref.gear_list!=null && pref.gear_slot!=null) pref.gear = pref.gear_list["[pref.gear_slot]"] else - from_file(S["gear"], pref.gear) + pref.gear = save_data["gear"] pref.gear_slot = 1 -/datum/category_item/player_setup_item/loadout/save_character(var/savefile/S) +/datum/category_item/player_setup_item/loadout/save_character(list/save_data) pref.gear_list["[pref.gear_slot]"] = pref.gear - to_file(S["gear_list"], pref.gear_list) - to_file(S["gear_slot"], pref.gear_slot) + save_data["gear_list"] = pref.gear_list + save_data["gear_slot"] = pref.gear_slot /datum/category_item/player_setup_item/loadout/proc/valid_gear_choices(var/max_cost) . = list() diff --git a/code/modules/client/preference_setup/occupation/occupation.dm b/code/modules/client/preference_setup/occupation/occupation.dm index 9ce7f08026..48516c11d4 100644 --- a/code/modules/client/preference_setup/occupation/occupation.dm +++ b/code/modules/client/preference_setup/occupation/occupation.dm @@ -2,41 +2,41 @@ name = "Occupation" sort_order = 1 -/datum/category_item/player_setup_item/occupation/load_character(var/savefile/S) - S["alternate_option"] >> pref.alternate_option - S["job_civilian_high"] >> pref.job_civilian_high - S["job_civilian_med"] >> pref.job_civilian_med - S["job_civilian_low"] >> pref.job_civilian_low - S["job_medsci_high"] >> pref.job_medsci_high - S["job_medsci_med"] >> pref.job_medsci_med - S["job_medsci_low"] >> pref.job_medsci_low - S["job_engsec_high"] >> pref.job_engsec_high - S["job_engsec_med"] >> pref.job_engsec_med - S["job_engsec_low"] >> pref.job_engsec_low +/datum/category_item/player_setup_item/occupation/load_character(list/save_data) + pref.alternate_option = save_data["alternate_option"] + pref.job_civilian_high = save_data["job_civilian_high"] + pref.job_civilian_med = save_data["job_civilian_med"] + pref.job_civilian_low = save_data["job_civilian_low"] + pref.job_medsci_high = save_data["job_medsci_high"] + pref.job_medsci_med = save_data["job_medsci_med"] + pref.job_medsci_low = save_data["job_medsci_low"] + pref.job_engsec_high = save_data["job_engsec_high"] + pref.job_engsec_med = save_data["job_engsec_med"] + pref.job_engsec_low = save_data["job_engsec_low"] //VOREStation Add - S["job_talon_low"] >> pref.job_talon_low - S["job_talon_med"] >> pref.job_talon_med - S["job_talon_high"] >> pref.job_talon_high + pref.job_talon_low = save_data["job_talon_low"] + pref.job_talon_med = save_data["job_talon_med"] + pref.job_talon_high = save_data["job_talon_high"] //VOREStation Add End - S["player_alt_titles"] >> pref.player_alt_titles + pref.player_alt_titles = save_data["player_alt_titles"] -/datum/category_item/player_setup_item/occupation/save_character(var/savefile/S) - S["alternate_option"] << pref.alternate_option - S["job_civilian_high"] << pref.job_civilian_high - S["job_civilian_med"] << pref.job_civilian_med - S["job_civilian_low"] << pref.job_civilian_low - S["job_medsci_high"] << pref.job_medsci_high - S["job_medsci_med"] << pref.job_medsci_med - S["job_medsci_low"] << pref.job_medsci_low - S["job_engsec_high"] << pref.job_engsec_high - S["job_engsec_med"] << pref.job_engsec_med - S["job_engsec_low"] << pref.job_engsec_low +/datum/category_item/player_setup_item/occupation/save_character(list/save_data) + save_data["alternate_option"] = pref.alternate_option + save_data["job_civilian_high"] = pref.job_civilian_high + save_data["job_civilian_med"] = pref.job_civilian_med + save_data["job_civilian_low"] = pref.job_civilian_low + save_data["job_medsci_high"] = pref.job_medsci_high + save_data["job_medsci_med"] = pref.job_medsci_med + save_data["job_medsci_low"] = pref.job_medsci_low + save_data["job_engsec_high"] = pref.job_engsec_high + save_data["job_engsec_med"] = pref.job_engsec_med + save_data["job_engsec_low"] = pref.job_engsec_low //VOREStation Add - S["job_talon_low"] << pref.job_talon_low - S["job_talon_med"] << pref.job_talon_med - S["job_talon_high"] << pref.job_talon_high + save_data["job_talon_low"] = pref.job_talon_low + save_data["job_talon_med"] = pref.job_talon_med + save_data["job_talon_high"] = pref.job_talon_high //VOREStation Add End - S["player_alt_titles"] << pref.player_alt_titles + save_data["player_alt_titles"] = pref.player_alt_titles /datum/category_item/player_setup_item/occupation/sanitize_character() pref.alternate_option = sanitize_integer(pref.alternate_option, 0, 2, initial(pref.alternate_option)) diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm index 5a8e31c324..1aa899e744 100644 --- a/code/modules/client/preference_setup/preference_setup.dm +++ b/code/modules/client/preference_setup/preference_setup.dm @@ -59,21 +59,21 @@ for(var/datum/category_group/player_setup_category/PS in categories) PS.sanitize_setup() -/datum/category_collection/player_setup_collection/proc/load_character(var/savefile/S) +/datum/category_collection/player_setup_collection/proc/load_character(list/save_data) for(var/datum/category_group/player_setup_category/PS in categories) - PS.load_character(S) + PS.load_character(save_data) -/datum/category_collection/player_setup_collection/proc/save_character(var/savefile/S) +/datum/category_collection/player_setup_collection/proc/save_character(list/save_data) for(var/datum/category_group/player_setup_category/PS in categories) - PS.save_character(S) + PS.save_character(save_data) -/datum/category_collection/player_setup_collection/proc/load_preferences(var/savefile/S) +/datum/category_collection/player_setup_collection/proc/load_preferences(datum/json_savefile/savefile) for(var/datum/category_group/player_setup_category/PS in categories) - PS.load_preferences(S) + PS.load_preferences(savefile) -/datum/category_collection/player_setup_collection/proc/save_preferences(var/savefile/S) +/datum/category_collection/player_setup_collection/proc/save_preferences(datum/json_savefile/savefile) for(var/datum/category_group/player_setup_category/PS in categories) - PS.save_preferences(S) + PS.save_preferences(savefile) /datum/category_collection/player_setup_collection/proc/copy_to_mob(var/mob/living/carbon/human/C) for(var/datum/category_group/player_setup_category/PS in categories) @@ -86,6 +86,7 @@ dat += "[PS.name] " // TODO: Check how to properly mark a href/button selected in a classic browser window else dat += "[PS.name] " + dat += "Game Options" return dat /datum/category_collection/player_setup_collection/proc/content(var/mob/user) @@ -105,6 +106,9 @@ selected_category = category . = 1 + else if(href_list["game_prefs"]) + user.client.prefs.tgui_interact(user) + if(.) user.client.prefs.ShowChoices(user) @@ -123,29 +127,29 @@ for(var/datum/category_item/player_setup_item/PI in items) PI.sanitize_character() -/datum/category_group/player_setup_category/proc/load_character(var/savefile/S) +/datum/category_group/player_setup_category/proc/load_character(list/save_data) // Load all data, then sanitize it. // Need due to, for example, the 01_basic module relying on species having been loaded to sanitize correctly but that isn't loaded until module 03_body. for(var/datum/category_item/player_setup_item/PI in items) - PI.load_character(S) + PI.load_character(save_data) -/datum/category_group/player_setup_category/proc/save_character(var/savefile/S) +/datum/category_group/player_setup_category/proc/save_character(list/save_data) // Sanitize all data, then save it for(var/datum/category_item/player_setup_item/PI in items) PI.sanitize_character() for(var/datum/category_item/player_setup_item/PI in items) - PI.save_character(S) + PI.save_character(save_data) -/datum/category_group/player_setup_category/proc/load_preferences(var/savefile/S) +/datum/category_group/player_setup_category/proc/load_preferences(datum/json_savefile/savefile) for(var/datum/category_item/player_setup_item/PI in items) - PI.load_preferences(S) + PI.load_preferences(savefile) -/datum/category_group/player_setup_category/proc/save_preferences(var/savefile/S) +/datum/category_group/player_setup_category/proc/save_preferences(datum/json_savefile/savefile) for(var/datum/category_item/player_setup_item/PI in items) PI.sanitize_preferences() for(var/datum/category_item/player_setup_item/PI in items) - PI.save_preferences(S) + PI.save_preferences(savefile) /datum/category_group/player_setup_category/proc/copy_to_mob(var/mob/living/carbon/human/C) for(var/datum/category_item/player_setup_item/PI in items) @@ -188,25 +192,25 @@ /* * Called when the item is asked to load per character settings */ -/datum/category_item/player_setup_item/proc/load_character(var/savefile/S) +/datum/category_item/player_setup_item/proc/load_character(list/save_data) return /* * Called when the item is asked to save per character settings */ -/datum/category_item/player_setup_item/proc/save_character(var/savefile/S) +/datum/category_item/player_setup_item/proc/save_character(list/save_data) return /* * Called when the item is asked to load user/global settings */ -/datum/category_item/player_setup_item/proc/load_preferences(var/savefile/S) +/datum/category_item/player_setup_item/proc/load_preferences(datum/json_savefile/savefile) return /* * Called when the item is asked to save user/global settings */ -/datum/category_item/player_setup_item/proc/save_preferences(var/savefile/S) +/datum/category_item/player_setup_item/proc/save_preferences(datum/json_savefile/savefile) return /* diff --git a/code/modules/client/preference_setup/skills/skills.dm b/code/modules/client/preference_setup/skills/skills.dm index 645314ab59..eef0512acc 100644 --- a/code/modules/client/preference_setup/skills/skills.dm +++ b/code/modules/client/preference_setup/skills/skills.dm @@ -2,15 +2,15 @@ name = "Skills" sort_order = 1 -/datum/category_item/player_setup_item/skills/load_character(var/savefile/S) - S["skills"] >> pref.skills - S["used_skillpoints"] >> pref.used_skillpoints - S["skill_specialization"] >> pref.skill_specialization +/datum/category_item/player_setup_item/skills/load_character(list/save_data) + pref.skills = save_data["skills"] + pref.used_skillpoints = save_data["used_skillpoints"] + pref.skill_specialization = save_data["skill_specialization"] -/datum/category_item/player_setup_item/skills/save_character(var/savefile/S) - S["skills"] << pref.skills - S["used_skillpoints"] << pref.used_skillpoints - S["skill_specialization"] << pref.skill_specialization +/datum/category_item/player_setup_item/skills/save_character(list/save_data) + save_data["skills"] = pref.skills + save_data["used_skillpoints"] = pref.used_skillpoints + save_data["skill_specialization"] = pref.skill_specialization /datum/category_item/player_setup_item/skills/sanitize_character() if(SKILLS == null) setup_skills() diff --git a/code/modules/client/preference_setup/traits/traits.dm b/code/modules/client/preference_setup/traits/traits.dm index 4102a04f74..60369ff0c0 100644 --- a/code/modules/client/preference_setup/traits/traits.dm +++ b/code/modules/client/preference_setup/traits/traits.dm @@ -35,11 +35,11 @@ var/list/trait_categories = list() // The categories available for the trait men sort_order = 1 var/current_tab = "Physical" -/datum/category_item/player_setup_item/traits/load_character(var/savefile/S) - S["traits"] >> pref.traits +/datum/category_item/player_setup_item/traits/load_character(list/save_data) + pref.traits = save_data["traits"] -/datum/category_item/player_setup_item/traits/save_character(var/savefile/S) - S["traits"] << pref.traits +/datum/category_item/player_setup_item/traits/save_character(list/save_data) + save_data["traits"] = pref.traits /datum/category_item/player_setup_item/traits/content() diff --git a/code/modules/client/preference_setup/volume_sliders/01_volume.dm b/code/modules/client/preference_setup/volume_sliders/01_volume.dm index 7dea5d2053..fef2b04356 100644 --- a/code/modules/client/preference_setup/volume_sliders/01_volume.dm +++ b/code/modules/client/preference_setup/volume_sliders/01_volume.dm @@ -7,11 +7,11 @@ name = "General Volume" sort_order = 1 -/datum/category_item/player_setup_item/volume_sliders/volume/load_preferences(var/savefile/S) - S["volume_channels"] >> pref.volume_channels +/datum/category_item/player_setup_item/volume_sliders/volume/load_preferences(datum/json_savefile/savefile) + pref.volume_channels = savefile.get_entry("volume_channels") -/datum/category_item/player_setup_item/volume_sliders/volume/save_preferences(var/savefile/S) - S["volume_channels"] << pref.volume_channels +/datum/category_item/player_setup_item/volume_sliders/volume/save_preferences(datum/json_savefile/savefile) + savefile.set_entry("volume_channels", pref.volume_channels) /datum/category_item/player_setup_item/volume_sliders/volume/sanitize_preferences() if(isnull(pref.volume_channels)) @@ -74,7 +74,7 @@ /datum/volume_panel/tgui_data(mob/user) if(!user.client || !user.client.prefs) return list("error" = TRUE) - + var/list/data = ..() data["volume_channels"] = user.client.prefs.volume_channels return data diff --git a/code/modules/client/preference_setup/volume_sliders/02_media.dm b/code/modules/client/preference_setup/volume_sliders/02_media.dm index 98f7bb4cfe..007edcbf01 100644 --- a/code/modules/client/preference_setup/volume_sliders/02_media.dm +++ b/code/modules/client/preference_setup/volume_sliders/02_media.dm @@ -6,13 +6,13 @@ name = "Media" sort_order = 2 -/datum/category_item/player_setup_item/volume_sliders/media/load_preferences(var/savefile/S) - S["media_volume"] >> pref.media_volume - S["media_player"] >> pref.media_player +/datum/category_item/player_setup_item/volume_sliders/media/load_preferences(datum/json_savefile/savefile) + pref.media_volume = savefile.get_entry("media_volume") + pref.media_player = savefile.get_entry("media_player") -/datum/category_item/player_setup_item/volume_sliders/media/save_preferences(var/savefile/S) - S["media_volume"] << pref.media_volume - S["media_player"] << pref.media_player +/datum/category_item/player_setup_item/volume_sliders/media/save_preferences(datum/json_savefile/savefile) + savefile.set_entry("media_volume", pref.media_volume) + savefile.set_entry("media_player", pref.media_player) /datum/category_item/player_setup_item/volume_sliders/media/sanitize_preferences() pref.media_volume = isnum(pref.media_volume) ? CLAMP(pref.media_volume, 0, 1) : initial(pref.media_volume) diff --git a/code/modules/client/preference_setup/vore/02_size.dm b/code/modules/client/preference_setup/vore/02_size.dm index 819d67ecc1..55590a2a4b 100644 --- a/code/modules/client/preference_setup/vore/02_size.dm +++ b/code/modules/client/preference_setup/vore/02_size.dm @@ -22,27 +22,27 @@ name = "Size" sort_order = 2 -/datum/category_item/player_setup_item/vore/size/load_character(var/savefile/S) - S["size_multiplier"] >> pref.size_multiplier - S["weight_vr"] >> pref.weight_vr - S["weight_gain"] >> pref.weight_gain - S["weight_loss"] >> pref.weight_loss - S["fuzzy"] >> pref.fuzzy - S["offset_override"] >> pref.offset_override - S["voice_freq"] >> pref.voice_freq - S["voice_sound"] >> pref.voice_sound - S["custom_speech_bubble"] >> pref.custom_speech_bubble +/datum/category_item/player_setup_item/vore/size/load_character(list/save_data) + pref.size_multiplier = save_data["size_multiplier"] + pref.weight_vr = save_data["weight_vr"] + pref.weight_gain = save_data["weight_gain"] + pref.weight_loss = save_data["weight_loss"] + pref.fuzzy = save_data["fuzzy"] + pref.offset_override = save_data["offset_override"] + pref.voice_freq = save_data["voice_freq"] + pref.voice_sound = save_data["voice_sound"] + pref.custom_speech_bubble = save_data["custom_speech_bubble"] -/datum/category_item/player_setup_item/vore/size/save_character(var/savefile/S) - S["size_multiplier"] << pref.size_multiplier - S["weight_vr"] << pref.weight_vr - S["weight_gain"] << pref.weight_gain - S["weight_loss"] << pref.weight_loss - S["fuzzy"] << pref.fuzzy - S["offset_override"] << pref.offset_override - S["voice_freq"] << pref.voice_freq - S["voice_sound"] << pref.voice_sound - S["custom_speech_bubble"] << pref.custom_speech_bubble +/datum/category_item/player_setup_item/vore/size/save_character(list/save_data) + save_data["size_multiplier"] = pref.size_multiplier + save_data["weight_vr"] = pref.weight_vr + save_data["weight_gain"] = pref.weight_gain + save_data["weight_loss"] = pref.weight_loss + save_data["fuzzy"] = pref.fuzzy + save_data["offset_override"] = pref.offset_override + save_data["voice_freq"] = pref.voice_freq + save_data["voice_sound"] = pref.voice_sound + save_data["custom_speech_bubble"] = pref.custom_speech_bubble /datum/category_item/player_setup_item/vore/size/sanitize_character() pref.weight_vr = sanitize_integer(pref.weight_vr, WEIGHT_MIN, WEIGHT_MAX, initial(pref.weight_vr)) diff --git a/code/modules/client/preference_setup/vore/03_egg.dm b/code/modules/client/preference_setup/vore/03_egg.dm index 5ad651fbf9..f330d419ce 100644 --- a/code/modules/client/preference_setup/vore/03_egg.dm +++ b/code/modules/client/preference_setup/vore/03_egg.dm @@ -14,13 +14,13 @@ name = "Egg appearance." sort_order = 3 -/datum/category_item/player_setup_item/vore/egg/load_character(var/savefile/S) - S["vore_egg_type"] >> pref.vore_egg_type - S["autohiss"] >> pref.autohiss // VOREStation Add +/datum/category_item/player_setup_item/vore/egg/load_character(list/save_data) + pref.vore_egg_type = save_data["vore_egg_type"] + pref.autohiss = save_data["autohiss"] -/datum/category_item/player_setup_item/vore/egg/save_character(var/savefile/S) - S["vore_egg_type"] << pref.vore_egg_type - S["autohiss"] << pref.autohiss // VOREStation Add +/datum/category_item/player_setup_item/vore/egg/save_character(list/save_data) + save_data["vore_egg_type"] = pref.vore_egg_type + save_data["autohiss"] = pref.autohiss /datum/category_item/player_setup_item/vore/egg/sanitize_character() pref.vore_egg_type = sanitize_inlist(pref.vore_egg_type, global_vore_egg_types, initial(pref.vore_egg_type)) diff --git a/code/modules/client/preference_setup/vore/04_resleeving.dm b/code/modules/client/preference_setup/vore/04_resleeving.dm index 963c6b1a32..28dcdd603f 100644 --- a/code/modules/client/preference_setup/vore/04_resleeving.dm +++ b/code/modules/client/preference_setup/vore/04_resleeving.dm @@ -9,16 +9,15 @@ name = "Resleeving" sort_order = 4 -/datum/category_item/player_setup_item/vore/resleeve/load_character(var/savefile/S) - S["resleeve_lock"] >> pref.resleeve_lock - S["resleeve_scan"] >> pref.resleeve_scan - S["mind_scan"] >> pref.mind_scan +/datum/category_item/player_setup_item/vore/resleeve/load_character(list/save_data) + pref.resleeve_lock = save_data["resleeve_lock"] + pref.resleeve_scan = save_data["resleeve_scan"] + pref.mind_scan = save_data["mind_scan"] - -/datum/category_item/player_setup_item/vore/resleeve/save_character(var/savefile/S) - S["resleeve_lock"] << pref.resleeve_lock - S["resleeve_scan"] << pref.resleeve_scan - S["mind_scan"] << pref.mind_scan +/datum/category_item/player_setup_item/vore/resleeve/save_character(list/save_data) + save_data["resleeve_lock"] = pref.resleeve_lock + save_data["resleeve_scan"] = pref.resleeve_scan + save_data["mind_scan"] = pref.mind_scan /datum/category_item/player_setup_item/vore/resleeve/sanitize_character() pref.resleeve_lock = sanitize_integer(pref.resleeve_lock, 0, 1, initial(pref.resleeve_lock)) diff --git a/code/modules/client/preference_setup/vore/05_persistence.dm b/code/modules/client/preference_setup/vore/05_persistence.dm index 330a21a73b..bb2535f44b 100644 --- a/code/modules/client/preference_setup/vore/05_persistence.dm +++ b/code/modules/client/preference_setup/vore/05_persistence.dm @@ -7,12 +7,12 @@ name = "Persistence" sort_order = 5 -/datum/category_item/player_setup_item/vore/persistence/load_character(var/savefile/S) - S["persistence_settings"] >> pref.persistence_settings +/datum/category_item/player_setup_item/vore/persistence/load_character(list/save_data) + pref.persistence_settings = save_data["persistence_settings"] sanitize_character() // Don't let new characters start off with nulls -/datum/category_item/player_setup_item/vore/persistence/save_character(var/savefile/S) - S["persistence_settings"] << pref.persistence_settings +/datum/category_item/player_setup_item/vore/persistence/save_character(list/save_data) + save_data["persistence_settings"] = pref.persistence_settings /datum/category_item/player_setup_item/vore/persistence/sanitize_character() pref.persistence_settings = sanitize_integer(pref.persistence_settings, 0, (1<<(PERSIST_COUNT+1)-1), initial(pref.persistence_settings)) diff --git a/code/modules/client/preference_setup/vore/06_vantag.dm b/code/modules/client/preference_setup/vore/06_vantag.dm index c402f1f277..78d801a75e 100644 --- a/code/modules/client/preference_setup/vore/06_vantag.dm +++ b/code/modules/client/preference_setup/vore/06_vantag.dm @@ -8,13 +8,13 @@ name = "VS Events" sort_order = 6 -/datum/category_item/player_setup_item/vore/vantag/load_character(var/savefile/S) - S["vantag_volunteer"] >> pref.vantag_volunteer - S["vantag_preference"] >> pref.vantag_preference +/datum/category_item/player_setup_item/vore/vantag/load_character(list/save_data) + pref.vantag_volunteer = save_data["vantag_volunteer"] + pref.vantag_preference = save_data["vantag_preference"] -/datum/category_item/player_setup_item/vore/vantag/save_character(var/savefile/S) - S["vantag_volunteer"] << pref.vantag_volunteer - S["vantag_preference"] << pref.vantag_preference +/datum/category_item/player_setup_item/vore/vantag/save_character(list/save_data) + save_data["vantag_volunteer"] = pref.vantag_volunteer + save_data["vantag_preference"] = pref.vantag_preference /datum/category_item/player_setup_item/vore/vantag/sanitize_character() pref.vantag_volunteer = sanitize_integer(pref.vantag_volunteer, 0, 1, initial(pref.vantag_volunteer)) diff --git a/code/modules/client/preference_setup/vore/07_traits.dm b/code/modules/client/preference_setup/vore/07_traits.dm index 20f2e00c84..58c0642263 100644 --- a/code/modules/client/preference_setup/vore/07_traits.dm +++ b/code/modules/client/preference_setup/vore/07_traits.dm @@ -107,47 +107,47 @@ var/global/list/valid_bloodreagents = list("default","iron","copper","phoron","s name = "Traits" sort_order = 7 -/datum/category_item/player_setup_item/vore/traits/load_character(var/savefile/S) - S["custom_species"] >> pref.custom_species - S["custom_base"] >> pref.custom_base - S["pos_traits"] >> pref.pos_traits - S["neu_traits"] >> pref.neu_traits - S["neg_traits"] >> pref.neg_traits - S["blood_color"] >> pref.blood_color - S["blood_reagents"] >> pref.blood_reagents +/datum/category_item/player_setup_item/vore/traits/load_character(list/save_data) + pref.custom_species = save_data["custom_species"] + pref.custom_base = save_data["custom_base"] + pref.pos_traits = save_data["pos_traits"] + pref.neu_traits = save_data["neu_traits"] + pref.neg_traits = save_data["neg_traits"] + pref.blood_color = save_data["blood_color"] + pref.blood_reagents = save_data["blood_reagents"] - S["traits_cheating"] >> pref.traits_cheating - S["max_traits"] >> pref.max_traits - S["trait_points"] >> pref.starting_trait_points + pref.traits_cheating = save_data["traits_cheating"] + pref.max_traits = save_data["max_traits"] + pref.starting_trait_points = save_data["trait_points"] - S["custom_say"] >> pref.custom_say - S["custom_whisper"] >> pref.custom_whisper - S["custom_ask"] >> pref.custom_ask - S["custom_exclaim"] >> pref.custom_exclaim + pref.custom_say = save_data["custom_say"] + pref.custom_whisper = save_data["custom_whisper"] + pref.custom_ask = save_data["custom_ask"] + pref.custom_exclaim = save_data["custom_exclaim"] - S["custom_heat"] >> pref.custom_heat - S["custom_cold"] >> pref.custom_cold + pref.custom_heat = save_data["custom_heat"] + pref.custom_cold = save_data["custom_cold"] -/datum/category_item/player_setup_item/vore/traits/save_character(var/savefile/S) - S["custom_species"] << pref.custom_species - S["custom_base"] << pref.custom_base - S["pos_traits"] << pref.pos_traits - S["neu_traits"] << pref.neu_traits - S["neg_traits"] << pref.neg_traits - S["blood_color"] << pref.blood_color - S["blood_reagents"] << pref.blood_reagents +/datum/category_item/player_setup_item/vore/traits/save_character(list/save_data) + save_data["custom_species"] = pref.custom_species + save_data["custom_base"] = pref.custom_base + save_data["pos_traits"] = pref.pos_traits + save_data["neu_traits"] = pref.neu_traits + save_data["neg_traits"] = pref.neg_traits + save_data["blood_color"] = pref.blood_color + save_data["blood_reagents"] = pref.blood_reagents - S["traits_cheating"] << pref.traits_cheating - S["max_traits"] << pref.max_traits - S["trait_points"] << pref.starting_trait_points + save_data["traits_cheating"] = pref.traits_cheating + save_data["max_traits"] = pref.max_traits + save_data["trait_points"] = pref.starting_trait_points - S["custom_say"] << pref.custom_say - S["custom_whisper"] << pref.custom_whisper - S["custom_ask"] << pref.custom_ask - S["custom_exclaim"] << pref.custom_exclaim + save_data["custom_say"] = pref.custom_say + save_data["custom_whisper"] = pref.custom_whisper + save_data["custom_ask"] = pref.custom_ask + save_data["custom_exclaim"] = pref.custom_exclaim - S["custom_heat"] << pref.custom_heat - S["custom_cold"] << pref.custom_cold + save_data["custom_heat"] = pref.custom_heat + save_data["custom_cold"] = pref.custom_cold /datum/category_item/player_setup_item/vore/traits/sanitize_character() if(!pref.pos_traits) pref.pos_traits = list() diff --git a/code/modules/client/preference_setup/vore/08_nif.dm b/code/modules/client/preference_setup/vore/08_nif.dm index d502fb447b..11e1f563dc 100644 --- a/code/modules/client/preference_setup/vore/08_nif.dm +++ b/code/modules/client/preference_setup/vore/08_nif.dm @@ -9,15 +9,15 @@ name = "NIF Data" sort_order = 8 -/datum/category_item/player_setup_item/vore/nif/load_character(var/savefile/S) - S["nif_path"] >> pref.nif_path - S["nif_durability"] >> pref.nif_durability - S["nif_savedata"] >> pref.nif_savedata +/datum/category_item/player_setup_item/vore/nif/load_character(list/save_data) + pref.nif_path = save_data["nif_path"] + pref.nif_durability = save_data["nif_durability"] + pref.nif_savedata = save_data["nif_savedata"] -/datum/category_item/player_setup_item/vore/nif/save_character(var/savefile/S) - S["nif_path"] << pref.nif_path - S["nif_durability"] << pref.nif_durability - S["nif_savedata"] << pref.nif_savedata +/datum/category_item/player_setup_item/vore/nif/save_character(list/save_data) + save_data["nif_path"] = pref.nif_path + save_data["nif_durability"] = pref.nif_durability + save_data["nif_savedata"] = pref.nif_savedata /datum/category_item/player_setup_item/vore/nif/sanitize_character() if(pref.nif_path && !ispath(pref.nif_path)) //We have at least a text string that should be a path. diff --git a/code/modules/client/preference_setup/vore/09_misc.dm b/code/modules/client/preference_setup/vore/09_misc.dm index 5b0ef32246..c1486d76f2 100644 --- a/code/modules/client/preference_setup/vore/09_misc.dm +++ b/code/modules/client/preference_setup/vore/09_misc.dm @@ -2,27 +2,27 @@ name = "Misc Settings" sort_order = 9 -/datum/category_item/player_setup_item/vore/misc/load_character(var/savefile/S) - S["show_in_directory"] >> pref.show_in_directory - S["directory_tag"] >> pref.directory_tag - S["directory_erptag"] >> pref.directory_erptag - S["directory_ad"] >> pref.directory_ad - S["sensorpref"] >> pref.sensorpref - S["capture_crystal"] >> pref.capture_crystal - S["auto_backup_implant"] >> pref.auto_backup_implant - S["borg_petting"] >> pref.borg_petting - S["stomach_vision"] >> pref.stomach_vision +/datum/category_item/player_setup_item/vore/misc/load_character(list/save_data) + pref.show_in_directory = save_data["show_in_directory"] + pref.directory_tag = save_data["directory_tag"] + pref.directory_erptag = save_data["directory_erptag"] + pref.directory_ad = save_data["directory_ad"] + pref.sensorpref = save_data["sensorpref"] + pref.capture_crystal = save_data["capture_crystal"] + pref.auto_backup_implant = save_data["auto_backup_implant"] + pref.borg_petting = save_data["borg_petting"] + pref.stomach_vision = save_data["stomach_vision"] -/datum/category_item/player_setup_item/vore/misc/save_character(var/savefile/S) - S["show_in_directory"] << pref.show_in_directory - S["directory_tag"] << pref.directory_tag - S["directory_erptag"] << pref.directory_erptag - S["directory_ad"] << pref.directory_ad - S["sensorpref"] << pref.sensorpref - S["capture_crystal"] << pref.capture_crystal - S["auto_backup_implant"] << pref.auto_backup_implant - S["borg_petting"] << pref.borg_petting - S["stomach_vision"] << pref.stomach_vision +/datum/category_item/player_setup_item/vore/misc/save_character(list/save_data) + save_data["show_in_directory"] = pref.show_in_directory + save_data["directory_tag"] = pref.directory_tag + save_data["directory_erptag"] = pref.directory_erptag + save_data["directory_ad"] = pref.directory_ad + save_data["sensorpref"] = pref.sensorpref + save_data["capture_crystal"] = pref.capture_crystal + save_data["auto_backup_implant"] = pref.auto_backup_implant + save_data["borg_petting"] = pref.borg_petting + save_data["stomach_vision"] = pref.stomach_vision /datum/category_item/player_setup_item/vore/misc/copy_to_mob(var/mob/living/carbon/human/character) if(pref.sensorpref > 5 || pref.sensorpref < 1) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 86613af1fd..fe950956e7 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -1,10 +1,12 @@ var/list/preferences_datums = list() /datum/preferences - //doohickeys for savefiles + /// The path to the general savefile for this datum var/path - var/default_slot = 1 //Holder so it doesn't default to slot 1, rather the last one used - var/savefile_version = 0 + /// Whether or not we allow saving/loading. Used for guests, if they're enabled + var/load_and_save = TRUE + /// Ensures that we always load the last used save, QOL + var/default_slot = 1 //non-preference stuff var/warns = 0 @@ -176,29 +178,55 @@ var/list/preferences_datums = list() ///If they are currently in the process of swapping slots, don't let them open 999 windows for it and get confused var/selecting_slots = FALSE + /// The json savefile for this datum + var/datum/json_savefile/savefile /datum/preferences/New(client/C) + client = C + + for(var/middleware_type in subtypesof(/datum/preference_middleware)) + middleware += new middleware_type(src) + + if(istype(C)) // IS_CLIENT_OR_MOCK + client_ckey = C.ckey + load_and_save = !IsGuestKey(C.key) + load_path(C.ckey) + if(load_and_save && !fexists(path)) + try_savefile_type_migration() + else + CRASH("attempted to create a preferences datum without a client or mock!") + load_savefile() + + // Legacy code + gear = list() + gear_list = list() + gear_slot = 1 + // End legacy code + player_setup = new(src) + + var/loaded_preferences_successfully = load_preferences() + if(loaded_preferences_successfully) + if(load_character()) + return + + // Didn't load a character, so let's randomize set_biological_gender(pick(MALE, FEMALE)) real_name = random_name(identifying_gender,species) b_type = RANDOM_BLOOD_TYPE - gear = list() - gear_list = list() - gear_slot = 1 - - if(istype(C)) - client = C - client_ckey = C.ckey - if(!IsGuestKey(C.key)) - load_path(C.ckey) - if(load_preferences()) - load_character() + if(client) + apply_all_client_preferences() + if(!loaded_preferences_successfully) + save_preferences() + save_character() // Save random character /datum/preferences/Destroy() - . = ..() QDEL_LIST_ASSOC_VAL(char_render_holders) + QDEL_NULL(middleware) + value_cache = null + return ..() /datum/preferences/proc/ZeroSkills(var/forced = 0) for(var/V in SKILLS) for(var/datum/skill/S in SKILLS[V]) @@ -357,8 +385,8 @@ var/list/preferences_datums = list() return 1 if(href_list["save"]) - save_preferences() save_character() + save_preferences() else if(href_list["reload"]) load_preferences() load_character() @@ -373,7 +401,7 @@ var/list/preferences_datums = list() return 0 if("Yes" != tgui_alert(usr, "Are you completely sure that you want to reset this character slot?", "Reset current slot?", list("No", "Yes"))) return 0 - load_character(SAVE_RESET) + reset_slot() sanitize_preferences() else if(href_list["copy"]) if(!IsGuestKey(usr.key)) @@ -402,6 +430,12 @@ var/list/preferences_datums = list() // Ask the preferences datums to apply their own settings to the new mob player_setup.copy_to_mob(character) + for(var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if(preference.savefile_identifier != PREFERENCE_CHARACTER) + continue + + preference.apply_to_human(character, read_preference(preference.type)) + // VOREStation Edit - Sync up all their organs and species one final time character.force_update_organs() @@ -420,26 +454,23 @@ var/list/preferences_datums = list() if(selecting_slots) to_chat(user, "You already have a slot selection dialog open!") return - var/savefile/S = new /savefile(path) - if(!S) - error("Somehow missing savefile path?! [path]") + if(!savefile) return - var/name - var/nickname //vorestation edit - This set appends nicknames to the save slot + var/default var/list/charlist = list() - var/default //VOREStation edit - for(var/i=1, i<= config.character_slots, i++) - S.cd = "/character[i]" - S["real_name"] >> name - S["nickname"] >> nickname //vorestation edit + + for(var/i = 1 to config.character_slots) + var/list/save_data = savefile.get_entry("character[i]", list()) + var/name = save_data["real_name"] + var/nickname = save_data["nickname"] if(!name) name = "[i] - \[Unused Slot\]" else if(i == default_slot) name = "โ–บ[i] - [name]" else name = "[i] - [name]" - if (i == default_slot) //VOREStation edit + if(i == default_slot) default = "[name][nickname ? " ([nickname])" : ""]" charlist["[name][nickname ? " ([nickname])" : ""]"] = i @@ -463,24 +494,23 @@ var/list/preferences_datums = list() if(selecting_slots) to_chat(user, "You already have a slot selection dialog open!") return - var/savefile/S = new /savefile(path) - if(!S) - error("Somehow missing savefile path?! [path]") + if(!savefile) return - var/name - var/nickname //vorestation edit - This set appends nicknames to the save slot var/list/charlist = list() - for(var/i=1, i<= config.character_slots, i++) - S.cd = "/character[i]" - S["real_name"] >> name - S["nickname"] >> nickname //vorestation edit + + for(var/i in 1 to config.character_slots) + var/list/save_data = savefile.get_entry("character[i]", list()) + var/name = save_data["real_name"] + var/nickname = save_data["nickname"] + if(!name) name = "[i] - \[Unused Slot\]" if(i == default_slot) name = "โ–บ[i] - [name]" else name = "[i] - [name]" + charlist["[name][nickname ? " ([nickname])" : ""]"] = i selecting_slots = TRUE @@ -494,7 +524,7 @@ var/list/preferences_datums = list() error("Player picked [choice] slot to copy to, but that wasn't one we sent.") return - if(tgui_alert(user, "Are you sure you want to override slot [slotnum], [name][nickname ? " ([nickname])" : ""]'s savedata?", "Confirm Override", list("No", "Yes")) == "Yes") + if(tgui_alert(user, "Are you sure you want to override slot [slotnum], [choice]'s savedata?", "Confirm Override", list("No", "Yes")) == "Yes") overwrite_character(slotnum) sanitize_preferences() save_preferences() diff --git a/code/modules/client/preferences/README.md b/code/modules/client/preferences/README.md new file mode 100644 index 0000000000..fabfb779c9 --- /dev/null +++ b/code/modules/client/preferences/README.md @@ -0,0 +1,456 @@ +# Preferences (by Mothblocks) + +This does not contain all the information on specific values--you can find those as doc-comments in relevant paths, such as `/datum/preference`. Rather, this gives you an overview for creating *most* preferences, and getting your foot in the door to create more advanced ones. + +## Anatomy of a preference (A.K.A. how do I make one?) + +Most preferences consist of two parts: + +1. A `/datum/preference` type. +2. A tgui representation in a TypeScript file. + +Every `/datum/preference` requires these three values be set: +1. `category` - See [Categories](#Categories). +2. `savefile_key` - The value which will be saved in the savefile. This will also be the identifier for tgui. +3. `savefile_identifier` - Whether or not this is a character specific preference (`PREFERENCE_CHARACTER`) or one that affects the player (`PREFERENCE_PLAYER`). As an example: hair color is `PREFERENCE_CHARACTER` while your UI settings are `PREFERENCE_PLAYER`, since they do not change between characters. + +For the tgui representation, most preferences will create a `.tsx` file in `tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/`. If your preference is a character preference, make a new file in `character_preferences`. Otherwise, put it in `game_preferences`. The filename does not matter, and this file can hold multiple relevant preferences if you would like. + +From here, you will want to write code resembling: + +```ts +import { Feature } from "../base"; + +export const savefile_key_here: Feature = { + name: "Preference Name Here", + component: Component, + + // Necessary for game preferences, unused for others + category: "CATEGORY", + + // Optional, shown as a tooltip + description: "This preference will blow your mind!", +} +``` + +`T` and `Component` depend on the type of preference you're making. Here are all common examples... + +## Numeric preferences + +Examples include age and FPS. + +A numeric preference derives from `/datum/preference/numeric`. + +```dm +/datum/preference/numeric/legs + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "legs" + + minimum = 1 + maximum = 8 +``` + +You can optionally provide a `step` field. This value is 1 by default, meaning only integers are accepted. + +Your `.tsx` file would look like: + +```ts +import { Feature, FeatureNumberInput } from "../base"; + +export const legs: Feature = { + name: "Legs", + component: FeatureNumberInput, +} +``` + +## Toggle preferences + +Examples include enabling tooltips. + +```dm +/datum/preference/toggle/enable_breathing + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "enable_breathing" + + // Optional, TRUE by default + default_value = FALSE +``` + +Your `.tsx` file would look like: + +```ts +import { CheckboxInput, FeatureToggle } from "../base"; + +export const enable_breathing: FeatureToggle = { + name: "Enable breathing", + component: CheckboxInput, +} +``` + +## Choiced preferences +A choiced preference is one where the only options are in a distinct few amount of choices. Examples include skin tone, shirt, and UI style. + +To create one, derive from `/datum/preference/choiced`. + +```dm +/datum/preference/choiced/favorite_drink + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "favorite_drink" +``` + +Now we need to tell the game what the choices are. We do this by overriding `init_possible_values()`. This will return a list of possible options. + +```dm +/datum/preference/choiced/favorite_drink/init_possible_values() + return list( + "Milk", + "Cola", + "Water", + ) +``` + +Your `.tsx` file would then look like: + +```tsx +import { FeatureChoiced, FeatureDropdownInput } from "../base"; + +export const favorite_drink: FeatureChoiced = { + name: "Favorite drink", + component: FeatureDropdownInput, +}; +``` + +This will create a dropdown input for your preference. + +### Choiced preferences - Icons +Choiced preferences can generate icons. This is how the clothing/species preferences work, for instance. However, if we just want a basic dropdown input with icons, it would look like this: + +```dm +/datum/preference/choiced/favorite_drink + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "favorite_drink" + should_generate_icons = TRUE // NEW! This is necessary. + +/datum/preference/choiced/favorite_drink/init_possible_values() + return list("Milk", "Cola", "Water") + +// New! This proc will get called for every value. +/datum/preference/choiced/favorite_drink/icon_for(value) + switch (value) + if ("Milk") + return icon('drinks.dmi', "milk") + if ("Cola") + return icon('drinks.dmi', "cola") + if ("Water") + return icon('drinks.dmi', "water") +``` + +Then, change your `.tsx` file to look like: + +```tsx +import { FeatureChoiced, FeatureIconnedDropdownInput } from "../base"; + +export const favorite_drink: FeatureChoiced = { + name: "Favorite drink", + component: FeatureIconnedDropdownInput, +}; +``` + +### Choiced preferences - Display names +Sometimes the values you want to save in code aren't the same as the ones you want to display. You can specify display names to change this. + +The only thing you will add is "compiled data". + +```dm +/datum/preference/choiced/favorite_drink/compile_constant_data() + var/list/data = ..() + + // An assoc list of values to display names + data[CHOICED_PREFERENCE_DISPLAY_NAMES] = list( + "Milk" = "Delicious Milk", + "Cola" = "Crisp Cola", + "Water" = "Plain Ol' Water", + ) + + return data +``` + +Your `.tsx` file does not change. The UI will figure it out for you! + +## Color preferences +These refer to colors, such as your OOC color. When read, these values will be given as 6 hex digits, *without* the pound sign. + +```dm +/datum/preference/color/eyeliner_color + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "eyeliner_color" +``` + +Your `.tsx` file would look like: + +```ts +import { FeatureColorInput, Feature } from "../base"; + +export const eyeliner_color: Feature = { + name: "Eyeliner color", + component: FeatureColorInput, +}; +``` + +## Name preferences +These refer to an alternative name. Examples include AI names and backup human names. + +These exist in `code/modules/client/preferences/names.dm`. + +These do not need a `.ts` file, and will be created in the UI automatically. + +```dm +/datum/preference/name/doctor + savefile_key = "doctor_name" + + // The name on the UI + explanation = "Doctor name" + + // This groups together with anything else with the same group + group = "medicine" + + // Optional, if specified the UI will show this name actively + // when the player is a medical doctor. + relevant_job = /datum/job/medical_doctor +``` + +## Making your preference do stuff + +There are a handful of procs preferences can use to act on their own: + +```dm +/// Apply this preference onto the given client. +/// Called when the savefile_identifier == PREFERENCE_PLAYER. +/datum/preference/proc/apply_to_client(client/client, value) + +/// Fired when the preference is updated. +/// Calls apply_to_client by default, but can be overridden. +/datum/preference/proc/apply_to_client_updated(client/client, value) + +/// Apply this preference onto the given human. +/// Must be overriden by subtypes. +/// Called when the savefile_identifier == PREFERENCE_CHARACTER. +/datum/preference/proc/apply_to_human(mob/living/carbon/human/target, value) +``` + +For example, `/datum/preference/numeric/age` contains: + +```dm +/datum/preference/numeric/age/apply_to_human(mob/living/carbon/human/target, value) + target.age = value +``` + +If your preference is `PREFERENCE_CHARACTER`, it MUST override `apply_to_human`, even if just to immediately `return`. + +You can also read preferences directly with `prefs.read_preference(/datum/preference/type/here)`, which will return the stored value. + +## Categories +Every preference needs to be in a `category`. These can be found in `code/__DEFINES/preferences.dm`. + +```dm +/// These will be shown in the character sidebar, but at the bottom. +#define PREFERENCE_CATEGORY_FEATURES "features" + +/// Any preferences that will show to the sides of the character in the setup menu. +#define PREFERENCE_CATEGORY_CLOTHING "clothing" + +/// Preferences that will be put into the 3rd list, and are not contextual. +#define PREFERENCE_CATEGORY_NON_CONTEXTUAL "non_contextual" + +/// Will be put under the game preferences window. +#define PREFERENCE_CATEGORY_GAME_PREFERENCES "game_preferences" + +/// These will show in the list to the right of the character preview. +#define PREFERENCE_CATEGORY_SECONDARY_FEATURES "secondary_features" + +/// These are preferences that are supplementary for main features, +/// such as hair color being affixed to hair. +#define PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES "supplemental_features" +``` + +![Preference categories for the main page](https://raw.githubusercontent.com/tgstation/documentation-assets/main/preferences/preference_categories.png) + +> SECONDARY_FEATURES or NON_CONTEXTUAL? + +Secondary features tend to be species specific. Non contextual features shouldn't change much from character to character. + +## Default values and randomization + +There are three procs to be aware of in regards to this topic: + +- `create_default_value()`. This is used when a value deserializes improperly or when a new character is created. +- `create_informed_default_value(datum/preferences/preferences)` - Used for more complicated default values, like how names require the gender. Will call `create_default_value()` by default. +- `create_random_value(datum/preferences/preferences)` - Explicitly used for random values, such as when a character is being randomized. + +`create_default_value()` in most preferences will create a random value. If this is a problem (like how default characters should always be human), you can override `create_default_value()`. By default (without overriding `create_random_value`), random values are just default values. + +## Advanced - Server data + +As previewed in [the display names implementation](#Choiced-preferences---Display-names), there exists a `compile_constant_data()` proc you can override. + +Compiled data is used wherever the server needs to give the client some value it can't figure out on its own. Skin tones use this to tell the client what colors they represent, for example. + +Compiled data is sent to the `serverData` field in the `FeatureValueProps`. + +## Advanced - Creating your own tgui component + +If you have good knowledge with tgui (especially TypeScript), you'll be able to create your own component to represent preferences. + +The `component` field in a feature accepts __any__ component that accepts `FeatureValueProps`. + +This will give you the fields: + +```ts +act: typeof sendAct, +featureId: string, +handleSetValue: (newValue: TSending) => void, +serverData: TServerData | undefined, +shrink?: boolean, +value: TReceiving, +``` + +`act` is the same as the one you get from `useBackend`. + +`featureId` is the savefile_key of the feature. + +`handleSetValue` is a function that, when called, will tell the server the new value, as well as changing the value immediately locally. + +`serverData` is the [server data](#Advanced---Server-data), if it has been fetched yet (and exists). + +`shrink` is whether or not the UI should appear smaller. This is only used for supplementary features. + +`value` is the current value, could be predicted (meaning that the value was changed locally, but has not yet reached the server). + +For a basic example of how this can look, observe `CheckboxInput`: + +```tsx +export const CheckboxInput = ( + props: FeatureValueProps +) => { + return ( { + props.handleSetValue(!props.value); + }} + />); +}; +``` + +## Advanced - Middleware +A `/datum/preference_middleware` is a way to inject your own data at specific points, as well as hijack actions. + +Middleware can hijack actions by specifying `action_delegations`: + +```dm +/datum/preference_middleware/congratulations + action_delegations = list( + "congratulate_me" = PROC_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 + +In order to make an antagonist selectable, you must do a few things: + +1. Your antagonist needs an icon. +2. Your antagonist must be in a Dynamic ruleset. The ruleset must specify the antagonist as its `antag_flag`. +3. Your antagonist needs a file in `tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/filename.ts`. This file name MUST be the `antag_flag` of your ruleset, with nothing but letters remaining (e.g. "Nuclear Operative" -> `nuclearoperative`). +4. Add it to `special_roles`. + +## Creating icons + +If you are satisfied with your icon just being a dude with some clothes, then you can specify `preview_outfit` in your `/datum/antagonist`. + +Space Ninja, for example, looks like: + +```dm +/datum/antagonist/ninja + preview_outift = /datum/outfit/ninja +``` + +However, if you want to get creative, you can override `/get_preview_icon()`. This proc should return an icon of size `ANTAGONIST_PREVIEW_ICON_SIZE`x`ANTAGONIST_PREVIEW_ICON_SIZE`. + +There are some helper procs you can use as well. `render_preview_outfit(outfit_type)` will take an outfit and give you an icon of someone wearing those clothes. `finish_preview_outfit` will, given an icon, resize it appropriately and zoom in on the head. Note that this will look bad on anything that isn't a human, so if you have a non-human antagonist (such as sentient disease), just run `icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE)`. + +For inspiration, here is changeling's: + +```dm +/datum/antagonist/changeling/get_preview_icon() + var/icon/final_icon = render_preview_outfit(/datum/outfit/changeling) + var/icon/split_icon = render_preview_outfit(/datum/outfit/job/engineer) + + final_icon.Shift(WEST, world.icon_size / 2) + final_icon.Shift(EAST, world.icon_size / 2) + + split_icon.Shift(EAST, world.icon_size / 2) + split_icon.Shift(WEST, world.icon_size / 2) + + final_icon.Blend(split_icon, ICON_OVERLAY) + + return finish_preview_icon(final_icon) +``` + +...which creates: + +![Changeling icon](https://raw.githubusercontent.com/tgstation/documentation-assets/main/preferences/changeling.png) + +## Creating the tgui representation + +In the `.ts` file you created earlier, you must now give the information of your antagonist. For reference, this is the changeling's: + +```ts +import { Antagonist, Category } from "../base"; +import { multiline } from "common/string"; + +const Changeling: Antagonist = { + key: "changeling", // This must be the same as your filename + name: "Changeling", + description: [ + multiline` + A highly intelligent alien predator that is capable of altering their + shape to flawlessly resemble a human. + `, + + multiline` + Transform yourself or others into different identities, and buy from an + arsenal of biological weaponry with the DNA you collect. + `, + ], + category: Category.Roundstart, // Category.Roundstart, Category.Midround, or Category.Latejoin +}; + +export default Changeling; +``` + +## Readying the Dynamic ruleset + +You already need to create a Dynamic ruleset, so in order to get your antagonist recognized, you just need to specify `antag_flag`. This must be unique per ruleset. + +Two other values to note are `antag_flag_override` and `antag_preference`. + +`antag_flag_override` exists for cases where you want the banned antagonist to be separate from `antag_flag`. As an example: roundstart, midround, and latejoin traitors have separate `antag_flag`, but all have `antag_flag_override = ROLE_TRAITOR`. This is because admins want to ban a player from Traitor altogether, not specific rulesets. + +If `antag_preference` is set, it will refer to that preference instead of `antag_flag`. This is used for clown operatives, which we want to be on the same preference as standard nuke ops, but must specify a unique `antag_flag` for. + +## Updating special_roles + +In `code/__DEFINES/role_preferences.dm` (the same place you'll need to make your ROLE_\* defined), simply add your antagonist to the `special_roles` assoc list. The key is your ROLE, the value is the number of days since your first game in order to play as that antagonist. diff --git a/code/modules/client/preferences/_preference.dm b/code/modules/client/preferences/_preference.dm new file mode 100644 index 0000000000..d3ad859249 --- /dev/null +++ b/code/modules/client/preferences/_preference.dm @@ -0,0 +1,546 @@ +// 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 + +/** + * Some preferences get applied directly to bodyparts (anything head_flags related right now). + * These must apply after species, as species gaining might replace the bodyparts of the human. + */ +#define PREFERENCE_PRIORITY_BODYPARTS 3 + +/// The priority at which gender is determined, needed for proper randomization. +#define PREFERENCE_PRIORITY_GENDER 4 + +/// The priority at which body type is decided, applied after gender so we can +/// support the "use gender" option. +#define PREFERENCE_PRIORITY_BODY_TYPE 5 + +/// Used for preferences that rely on body setup being finalized. +#define PREFERENCE_PRORITY_LATE_BODY_TYPE 6 + +/// Equpping items based on preferences. +/// Should happen after species and body type to make sure it looks right. +/// Mostly redundant, but a safety net for saving/loading. +#define PREFERENCE_PRIORITY_LOADOUT 7 + +/// The priority at which names are decided, needed for proper randomization. +#define PREFERENCE_PRIORITY_NAMES 8 + +/// Preferences that aren't names, but change the name changes set by PREFERENCE_PRIORITY_NAMES. +#define PREFERENCE_PRIORITY_NAME_MODIFICATIONS 9 + +/// The maximum preference priority, keep this updated, but don't use it for `priority`. +#define MAX_PREFERENCE_PRIORITY PREFERENCE_PRIORITY_NAME_MODIFICATIONS + +/// For choiced preferences, this key will be used to set display names in constant data. +#define CHOICED_PREFERENCE_DISPLAY_NAMES "display_names" + +/// For main feature preferences, this key refers to a feature considered supplemental. +/// For instance, hair color being supplemental to hair. +#define SUPPLEMENTAL_FEATURE_KEY "supplemental_feature" + +/// An assoc list list of types to instantiated `/datum/preference` instances +GLOBAL_LIST_INIT(preference_entries, init_preference_entries()) + +/// An assoc list of preference entries by their `savefile_key` +GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key()) + +/proc/init_preference_entries() + var/list/output = list() + for(var/datum/preference/preference_type as anything in subtypesof(/datum/preference)) + if(is_abstract(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(is_abstract(preference_type)) + continue + output[initial(preference_type.savefile_key)] = GLOB.preference_entries[preference_type] + return output + +/// Returns a flat list of preferences in order of their priority +/proc/get_preferences_in_priority_order() + var/list/preferences[MAX_PREFERENCE_PRIORITY] + + for(var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + LAZYADD(preferences[preference.priority], preference) + + var/list/flattened = list() + for(var/index in 1 to MAX_PREFERENCE_PRIORITY) + // Don't add nulls to the list if there's no preferences in a given priority level + if(LAZYLEN(preferences[index])) + flattened += preferences[index] + return flattened + +/// Represents an individual preference. +/datum/preference + /// The key inside the savefile to use. + /// This is also sent to the UI. + /// Once you pick this, don't change it. + var/savefile_key + + /// The category of preference, for use by the PreferencesMenu. + /// This isn't used for anything other than as a key for UI data. + /// It is up to the PreferencesMenu UI itself to interpret it. + var/category = "misc" + + /// Do not instantiate if type matches this. + abstract_type = /datum/preference + + /// What savefile should this preference be read from? + /// Valid values are PREFERENCE_CHARACTER and PREFERENCE_PLAYER. + /// See the documentation in [code/__DEFINES/preferences.dm]. + var/savefile_identifier + + /// The priority of when to apply this preference. + /// Used for when you need to rely on another preference. + var/priority = PREFERENCE_PRIORITY_DEFAULT + + /// If set, will be available to randomize, but only if the preference + /// is for PREFERENCE_CHARACTER. + var/can_randomize = TRUE + + /// If randomizable (PREFERENCE_CHARACTER and can_randomize), whether + /// or not to enable randomization by default. + /// This doesn't mean it'll always be random, but rather if a player + /// DOES have random body on, will this already be randomized? + var/randomize_by_default = TRUE + + /// If the selected species has this in its /datum/species/mutant_bodyparts, + /// will show the feature as selectable. + var/relevant_mutant_bodypart = null + + /// If the selected species has this in its /datum/species/body_markings, + /// will show the feature as selectable. + var/relevant_body_markings = null + + /// If the selected species has this in its /datum/species/inherent_traits, + /// will show the feature as selectable. + var/relevant_inherent_trait = null + + /// If the selected species has this in its /datum/species/var/external_organs, + /// will show the feature as selectable. + var/relevant_external_organ = null + + /// If the selected species has this head_flag by default, + /// will show the feature as selectable. + var/relevant_head_flag = null + +/// Called on the saved input when retrieving. +/// Also called by the value sent from the user through UI. Do not trust it. +/// Input is the value inside the savefile, output is to tell other code +/// what the value is. +/// This is useful either for more optimal data saving or for migrating +/// older data. +/// Must be overridden by subtypes. +/// Can return null if no value was found. +/datum/preference/proc/pref_deserialize(input, datum/preferences/preferences) + SHOULD_NOT_SLEEP(TRUE) + SHOULD_CALL_PARENT(FALSE) + CRASH("`pref_deserialize()` was not implemented on [type]!") + +/// Called on the input while saving. +/// Input is the current value, output is what to save in the savefile. +/datum/preference/proc/pref_serialize(input) + SHOULD_NOT_SLEEP(TRUE) + return input + +/// Produce a default, potentially random value for when no value for this +/// preference is found in the savefile. +/// Either this or create_informed_default_value must be overriden by subtypes. +/datum/preference/proc/create_default_value() + SHOULD_NOT_SLEEP(TRUE) + SHOULD_CALL_PARENT(FALSE) + CRASH("`create_default_value()` was not implemented on [type]!") + +/// Produce a default, potentially random value for when no value for this +/// preference is found in the savefile. +/// Unlike create_default_value(), will provide the preferences object if you +/// need to use it. +/// If not overriden, will call create_default_value() instead. +/datum/preference/proc/create_informed_default_value(datum/preferences/preferences) + return create_default_value() + +/// Produce a random value for the purposes of character randomization. +/// Will just create a default value by default. +/datum/preference/proc/create_random_value(datum/preferences/preferences) + return create_informed_default_value(preferences) + +/// Returns whether or not a preference can be randomized. +/datum/preference/proc/is_randomizable() + SHOULD_NOT_OVERRIDE(TRUE) + return savefile_identifier == PREFERENCE_CHARACTER && can_randomize + +/// Given a savefile, return either the saved data or an acceptable default. +/// This will write to the savefile if a value was not found with the new value. +/datum/preference/proc/read(list/save_data, datum/preferences/preferences) + SHOULD_NOT_OVERRIDE(TRUE) + + var/value + + if(!isnull(save_data)) + value = save_data[savefile_key] + + if(isnull(value)) + return null + else + return pref_deserialize(value, preferences) + +/// Given a savefile, writes the inputted value. +/// Returns TRUE for a successful application. +/// Return FALSE if it is invalid. +/datum/preference/proc/write(list/save_data, value) + SHOULD_NOT_OVERRIDE(TRUE) + + if(!is_valid(value)) + return FALSE + + if(!isnull(save_data)) + save_data[savefile_key] = pref_serialize(value) + + return TRUE + +/// Apply this preference onto the given client. +/// Called when the savefile_identifier == PREFERENCE_PLAYER. +/datum/preference/proc/apply_to_client(client/client, value) + SHOULD_NOT_SLEEP(TRUE) + SHOULD_CALL_PARENT(FALSE) + return + +/// Fired when the preference is updated. +/// Calls apply_to_client by default, but can be overridden. +/datum/preference/proc/apply_to_client_updated(client/client, value) + SHOULD_NOT_SLEEP(TRUE) + apply_to_client(client, value) + +/// Apply this preference onto the given human. +/// Must be overriden by subtypes. +/// Called when the savefile_identifier == PREFERENCE_CHARACTER. +/datum/preference/proc/apply_to_human(mob/living/carbon/human/target, value) + SHOULD_NOT_SLEEP(TRUE) + SHOULD_CALL_PARENT(FALSE) + CRASH("`apply_to_human()` was not implemented for [type]!") + +/// Returns which savefile to use for a given savefile identifier +/datum/preferences/proc/get_save_data_for_savefile_identifier(savefile_identifier) + RETURN_TYPE(/list) + + if(!client) + return null + if(!savefile) + CRASH("Attempted to get the savedata for [savefile_identifier] of [client] without a savefile. This should have been handled by load_preferences()") + + // Both of these will cache savefiles, but only for a tick. + // This is because storing a savefile will lock it, causing later issues down the line. + // Do not change them to addtimer, since the timer SS might not be running at this time. + switch (savefile_identifier) + if(PREFERENCE_CHARACTER) + return savefile.get_entry("character[default_slot]") + if(PREFERENCE_PLAYER) + return savefile.get_entry() + else + CRASH("Unknown savefile identifier [savefile_identifier]") + +/// Read a /datum/preference type and return its value. +/// This will write to the savefile if a value was not found with the new value. +/datum/preferences/proc/read_preference(preference_type) + var/datum/preference/preference_entry = GLOB.preference_entries[preference_type] + if(isnull(preference_entry)) + var/extra_info = "" + + // Current initializing subsystem is important to know because it might be a problem with + // things running pre-assets-initialization. + // if(!isnull(Master.current_initializing_subsystem)) + // extra_info = "Info was attempted to be retrieved while [Master.current_initializing_subsystem] was initializing." + // else if(!MC_RUNNING()) + // extra_info = "Info was attempted to be retrieved before the MC started, but not while it was actively initializing a subsystem" + + CRASH("Preference type `[preference_type]` is invalid! [extra_info]") + + if(preference_type in value_cache) + return value_cache[preference_type] + + var/value = preference_entry.read(get_save_data_for_savefile_identifier(preference_entry.savefile_identifier), src) + if(isnull(value)) + value = preference_entry.create_informed_default_value(src) + if(write_preference(preference_entry, value)) + return value + else + CRASH("Couldn't write the default value for [preference_type] (received [value])") + value_cache[preference_type] = value + return value + +/// Read a /datum/preference type and return its value. +/mob/proc/read_preference(preference_type) + return client?.prefs?.read_preference(preference_type) + +/// Set a /datum/preference entry. +/// Returns TRUE for a successful preference application. +/// Returns FALSE if it is invalid. +/datum/preferences/proc/write_preference(datum/preference/preference, preference_value) + var/save_data = get_save_data_for_savefile_identifier(preference.savefile_identifier) + var/new_value = preference.pref_deserialize(preference_value, src) + var/success = preference.write(save_data, new_value) + if(success) + value_cache[preference.type] = new_value + return success + +/// Will perform an update on the preference, but not write to the savefile. +/// This will, for instance, update the character preference view. +/// Performs sanity checks. +/datum/preferences/proc/update_preference(datum/preference/preference, preference_value) + if(!preference.is_accessible(src)) + return FALSE + + var/new_value = preference.pref_deserialize(preference_value, src) + var/success = preference.write(null, new_value) + + if(!success) + return FALSE + + recently_updated_keys |= preference.type + value_cache[preference.type] = new_value + + if(preference.savefile_identifier == PREFERENCE_PLAYER) + preference.apply_to_client_updated(client, read_preference(preference.type)) + else + update_preview_icon() + + 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 pref_serialize(value) + +/// Returns data compiled into the preferences JSON asset +/datum/preference/proc/compile_constant_data() + SHOULD_NOT_SLEEP(TRUE) + + return null + +/// Returns whether or not this preference is accessible. +/// If FALSE, will not show in the UI and will not be editable (by update_preference). +/datum/preference/proc/is_accessible(datum/preferences/preferences) + SHOULD_CALL_PARENT(TRUE) + SHOULD_NOT_SLEEP(TRUE) + + // if( + // !isnull(relevant_mutant_bodypart) + // || !isnull(relevant_inherent_trait) + // || !isnull(relevant_external_organ) + // || !isnull(relevant_head_flag) + // || !isnull(relevant_body_markings) + // ) + // var/species_type = preferences.read_preference(/datum/preference/choiced/species) + + // var/datum/species/species = GLOB.species_prototypes[species_type] + // if(!(savefile_key in species.get_features())) + // return FALSE + + if(!should_show_on_page(preferences.current_window)) + return FALSE + + return TRUE + +/// Returns whether or not, given the PREFERENCE_TAB_*, this preference should +/// appear. +/datum/preference/proc/should_show_on_page(preference_tab) + var/is_on_character_page = preference_tab == PREFERENCE_TAB_CHARACTER_PREFERENCES + var/is_character_preference = savefile_identifier == PREFERENCE_CHARACTER + return is_on_character_page == is_character_preference + +/// A preference that is a choice of one option among a fixed set. +/// Used for preferences such as clothing. +/datum/preference/choiced + /// If this is TRUE, an icon will be generated for every value. + /// If you implement this, you must implement `icon_for(value)` for every possible value. + var/should_generate_icons = FALSE + + var/list/cached_values + + /// If the preference is a main feature (PREFERENCE_CATEGORY_FEATURES or PREFERENCE_CATEGORY_CLOTHING) + /// this is the name of the feature that will be presented. + var/main_feature_name + + abstract_type = /datum/preference/choiced + +/// Returns a list of every possible value. +/// The first time this is called, will run `init_values()`. +/// Return value can be in the form of: +/// - A flat list of raw values, such as list(MALE, FEMALE, PLURAL). +/// - An assoc list of raw values to atoms/icons. +/datum/preference/choiced/proc/get_choices() + // Override `init_values()` instead. + SHOULD_NOT_OVERRIDE(TRUE) + + if(isnull(cached_values)) + cached_values = init_possible_values() + ASSERT(cached_values.len) + + return cached_values + +/// Returns a list of every possible value, serialized. +/datum/preference/choiced/proc/get_choices_serialized() + // Override `init_values()` instead. + SHOULD_NOT_OVERRIDE(TRUE) + + var/list/serialized_choices = list() + + for(var/choice in get_choices()) + serialized_choices += pref_serialize(choice) + + return serialized_choices + +/// Returns a list of every possible value. +/// This must be overriden by `/datum/preference/choiced` subtypes. +/// If `should_generate_icons` is TRUE, then you will also need to implement `icon_for(value)` +/// for every possible value. +/datum/preference/choiced/proc/init_possible_values() + CRASH("`init_possible_values()` was not implemented for [type]!") + +/// When `should_generate_icons` is TRUE, this proc is called for every value. +/// It can return either an icon or a typepath to an atom to create. +/datum/preference/choiced/proc/icon_for(value) + SHOULD_CALL_PARENT(FALSE) + SHOULD_NOT_SLEEP(TRUE) + CRASH("`icon_for()` was not implemented for [type], even though should_generate_icons = TRUE!") + +/datum/preference/choiced/is_valid(value) + return value in get_choices() + +/datum/preference/choiced/pref_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) // TODO: Pref spritesheet asset + // icons[choice] = get_spritesheet_key(choice) + + data["icons"] = icons + + if(!isnull(main_feature_name)) + data["name"] = main_feature_name + + return data + +/// A preference that represents an RGB color of something. +/// Will give the value as 6 hex digits, without a hash. +/datum/preference/color + abstract_type = /datum/preference/color + +/datum/preference/color/pref_deserialize(input, datum/preferences/preferences) + return sanitize_hexcolor(input) + +/datum/preference/color/create_default_value() + return random_color() + +/datum/preference/color/pref_serialize(input) + return sanitize_hexcolor(input) + +/datum/preference/color/is_valid(value) + return findtext(value, GLOB.is_color) + +/// 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/pref_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/pref_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 preference 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/pref_deserialize(input, datum/preferences/preferences) + return !!input + +/datum/preference/toggle/is_valid(value) + return value == TRUE || value == FALSE + + +/// A string-based preference accepting arbitrary string values entered by the user, with a maximum length. +/datum/preference/text + abstract_type = /datum/preference/text + + /// What is the maximum length of the value allowed in this field? + var/maximum_value_length = 256 + + /// Should we strip HTML the input or simply restrict it to the maximum_value_length? + var/should_strip_html = TRUE + + +/datum/preference/text/pref_deserialize(input, datum/preferences/preferences) + return should_strip_html ? STRIP_HTML_SIMPLE(input, maximum_value_length) : copytext(input, 1, maximum_value_length) + +/datum/preference/text/create_default_value() + return "" + +/datum/preference/text/is_valid(value) + return istext(value) && length(value) < maximum_value_length + +/datum/preference/text/compile_constant_data() + return list("maximum_length" = maximum_value_length) diff --git a/code/modules/client/preferences/middleware/_middleware.dm b/code/modules/client/preferences/middleware/_middleware.dm new file mode 100644 index 0000000000..8f47f73642 --- /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/migrations/13_preferences.dm b/code/modules/client/preferences/migrations/13_preferences.dm new file mode 100644 index 0000000000..1c6eb47d31 --- /dev/null +++ b/code/modules/client/preferences/migrations/13_preferences.dm @@ -0,0 +1,12 @@ +/// Transforms the bay style `"preferences": ["SOUND_MIDI", ...]` to `"SOUND_MIDI": 1` and `"preferences_disabled": [...]` to `0`. +/datum/preferences/proc/migration_13_preferences(datum/json_savefile/S) + var/list/preferences_enabled = S.get_entry("preferences") + for(var/key in preferences_enabled) + S.set_entry("[key]", TRUE) + + var/list/preferences_disabled = S.get_entry("preferences_disabled") + for(var/key in preferences_disabled) + S.set_entry("[key]", FALSE) + + // Force a reload of the value cache + apply_all_client_preferences() diff --git a/code/modules/client/preferences/preferences_tg.dm b/code/modules/client/preferences/preferences_tg.dm new file mode 100644 index 0000000000..944bffc039 --- /dev/null +++ b/code/modules/client/preferences/preferences_tg.dm @@ -0,0 +1,28 @@ +// Contains all of the variables and such to make tg prefs work +/datum/preferences + /// The savefile relating to character preferences, PREFERENCE_CHARACTER + var/list/character_data + + /// A list of keys that have been updated since the last save. + var/list/recently_updated_keys = list() + + /// A cache of preference entries to values. + /// Used to avoid expensive READ_FILE every time a preference is retrieved. + var/value_cache = list() + + /// If set to TRUE, will update character_profiles on the next ui_data tick. + var/tainted_character_profiles = FALSE + + var/current_window = PREFERENCE_TAB_GAME_PREFERENCES + + /// A list of instantiated middleware + var/list/datum/preference_middleware/middleware = list() + +/// Applies all PREFERENCE_PLAYER preferences +/datum/preferences/proc/apply_all_client_preferences() + for(var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if(preference.savefile_identifier != PREFERENCE_PLAYER) + continue + + value_cache -= preference.type + preference.apply_to_client(client, read_preference(preference.type)) diff --git a/code/modules/client/preferences/types/admin.dm b/code/modules/client/preferences/types/admin.dm new file mode 100644 index 0000000000..f066bc041e --- /dev/null +++ b/code/modules/client/preferences/types/admin.dm @@ -0,0 +1,73 @@ +/datum/preference/toggle/show_attack_logs + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_ATTACKLOGS" + default_value = FALSE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_attack_logs/is_accessible(datum/preferences/preferences) + . = ..() + if(!.) + return + + return check_rights(R_MOD|R_ADMIN, FALSE, preferences.client) + +/datum/preference/toggle/show_debug_logs + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_DEBUGLOGS" + default_value = FALSE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_debug_logs/is_accessible(datum/preferences/preferences) + . = ..() + if(!.) + return + + return check_rights(R_DEBUG|R_ADMIN, FALSE, preferences.client) + +/datum/preference/toggle/show_chat_prayers + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_PRAYER" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_chat_prayers/is_accessible(datum/preferences/preferences) + . = ..() + if(!.) + return + + return check_rights(R_EVENT|R_ADMIN, FALSE, preferences.client) + +// General holder prefs +/datum/preference/toggle/holder + abstract_type = /datum/preference/toggle/holder + +/datum/preference/toggle/holder/is_accessible(datum/preferences/preferences) + . = ..(preferences) + if(!.) + return + + return preferences.client.holder + +/datum/preference/toggle/holder/play_adminhelp_ping + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_ADMINHELP" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/holder/hear_radio + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_RADIO" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/holder/show_rlooc + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_RLOOC" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/holder/show_staff_dsay + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_ADSAY" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/types/chat.dm b/code/modules/client/preferences/types/chat.dm new file mode 100644 index 0000000000..4bf527a1cf --- /dev/null +++ b/code/modules/client/preferences/types/chat.dm @@ -0,0 +1,69 @@ +/datum/preference/toggle/chat_tags + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_SHOWICONS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_typing_indicator + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SHOW_TYPING" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_typing_indicator/apply_to_client(client/client, value) + if(!value) + client.stop_thinking() + +/datum/preference/toggle/show_typing_indicator_subtle + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SHOW_TYPING_SUBTLE" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_ooc + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_OOC" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_looc + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_LOOC" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_dsay + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_DEAD" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/check_mention + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_MENTION" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/vore_health_bars + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "VORE_HEALTH_BARS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_lore_news + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "NEWS_POPUP" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/player_tips + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "RECEIVE_TIPS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/pain_frequency + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "PAIN_FREQUENCY" + default_value = FALSE + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/types/ghost.dm b/code/modules/client/preferences/types/ghost.dm new file mode 100644 index 0000000000..8ae51631ed --- /dev/null +++ b/code/modules/client/preferences/types/ghost.dm @@ -0,0 +1,29 @@ +/datum/preference/toggle/whisubtle_vis + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "WHISUBTLE_VIS" + default_value = FALSE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/ghost_see_whisubtle + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "GHOST_SEE_WHISUBTLE" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/ghost_ears + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_GHOSTEARS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/ghost_sight + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_GHOSTSIGHT" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/ghost_radio + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "CHAT_GHOSTRADIO" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/types/misc.dm b/code/modules/client/preferences/types/misc.dm new file mode 100644 index 0000000000..b1ec621671 --- /dev/null +++ b/code/modules/client/preferences/types/misc.dm @@ -0,0 +1,71 @@ +/// Whether or not to toggle ambient occlusion, the shadows around people +/datum/preference/toggle/ambient_occlusion + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "AMBIENT_OCCLUSION_PREF" + default_value = FALSE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/ambient_occlusion/apply_to_client(client/client, value) + var/datum/plane_holder/PH = client?.mob?.plane_holder + if(PH) + PH.set_ao(VIS_OBJS, value) + PH.set_ao(VIS_MOBS, value) + +/datum/preference/toggle/mob_tooltips + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "MOB_TOOLTIPS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/inv_tooltips + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "INV_TOOLTIPS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/attack_icons + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "ATTACK_ICONS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/precision_placement + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "PRECISE_PLACEMENT" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/hotkeys_default + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "HUD_HOTKEYS" + default_value = FALSE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/show_progress_bar + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SHOW_PROGRESS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/safefiring + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SAFE_FIRING" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/status_indicators + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SHOW_STATUS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/status_indicators/apply_to_client(client/client, value) + var/datum/plane_holder/PH = client?.mob?.plane_holder + if(PH) + PH.set_vis(VIS_STATUS, value) + +/datum/preference/toggle/auto_afk + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "AUTO_AFK" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/types/runechat.dm b/code/modules/client/preferences/types/runechat.dm new file mode 100644 index 0000000000..1c6ea10fb3 --- /dev/null +++ b/code/modules/client/preferences/types/runechat.dm @@ -0,0 +1,23 @@ +/datum/preference/toggle/runechat_mob + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "RUNECHAT_MOB" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/runechat_obj + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "RUNECHAT_OBJ" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/runechat_border + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "RUNECHAT_BORDER" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/runechat_long_messages + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "RUNECHAT_LONG" + default_value = FALSE + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/types/sound.dm b/code/modules/client/preferences/types/sound.dm new file mode 100644 index 0000000000..83c4109bd0 --- /dev/null +++ b/code/modules/client/preferences/types/sound.dm @@ -0,0 +1,149 @@ +/datum/preference/toggle/play_admin_midis + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_MIDI" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/play_lobby_music + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_LOBBY" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/play_lobby_music/apply_to_client(client/client, value) + if(value) + client?.playtitlemusic() + else + client?.media?.stop_music() + +/datum/preference/toggle/play_ambience + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_AMBIENCE" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/play_ambience/apply_to_client(client/client, value) + if(!value) + client << sound(null, repeat = 0, wait = 0, volume = 0, channel = CHANNEL_AMBIENCE_FORCED) + client << sound(null, repeat = 0, wait = 0, volume = 0, channel = CHANNEL_AMBIENCE) + +/datum/preference/toggle/play_jukebox + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_JUKEBOX" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/play_jukebox/apply_to_client(client/client, value) + if(value) + client?.mob?.update_music() + else + client?.mob?.stop_all_music() + +/datum/preference/toggle/instrument_toggle + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_INSTRUMENT" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/emote_noises + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "EMOTE_NOISES" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/radio_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "RADIO_SOUNDS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/say_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SAY_SOUNDS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/emote_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "EMOTE_SOUNDS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/whisper_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "WHISPER_SOUNDS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/subtle_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SUBTLE_SOUNDS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/air_pump_noise + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_AIRPUMP" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/old_door_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_OLDDOORS" + default_value = FALSE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/department_door_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_DEPARTMENTDOORS" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/pickup_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_PICKED" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/drop_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_DROPPED" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/weather_sounds + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_WEATHER" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/supermatter_hum + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_SUPERMATTER" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/play_mentorhelp_ping + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "SOUND_MENTORHELP" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +// Vorey sounds +/datum/preference/toggle/belch_noises // Belching noises - pref toggle for 'em + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "BELCH_NOISES" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/eating_noises + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "EATING_NOISES" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/digestion_noises + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "DIGEST_NOISES" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER diff --git a/code/modules/client/preferences/types/ui.dm b/code/modules/client/preferences/types/ui.dm new file mode 100644 index 0000000000..b2372b8937 --- /dev/null +++ b/code/modules/client/preferences/types/ui.dm @@ -0,0 +1,34 @@ +/datum/preference/toggle/browser_style + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "BROWSER_STYLED" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/vchat_enable + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "VCHAT_ENABLE" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/vchat_enable/apply_to_client(client/client, value) + if(value) + client.tgui_panel.oldchat = FALSE + else + client.tgui_panel.oldchat = TRUE + + client.nuke_chat() + +/datum/preference/toggle/tgui_say + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "TGUI_SAY" + default_value = TRUE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/tgui_say_light + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "TGUI_SAY_LIGHT_MODE" + default_value = FALSE + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/tgui_say_light/apply_to_client(client/client, value) + client.tgui_say?.load() diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index e98e7de352..0f3aecbf84 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -1,121 +1,244 @@ #define SAVEFILE_VERSION_MIN 8 -#define SAVEFILE_VERSION_MAX 11 +#define SAVEFILE_VERSION_MAX 13 -//handles converting savefiles to new formats -//MAKE SURE YOU KEEP THIS UP TO DATE! -//If the sanity checks are capable of handling any issues. Only increase SAVEFILE_VERSION_MAX, -//this will mean that savefile_version will still be over SAVEFILE_VERSION_MIN, meaning -//this savefile update doesn't run everytime we load from the savefile. -//This is mainly for format changes, such as the bitflags in toggles changing order or something. -//if a file can't be updated, return 0 to delete it and start again -//if a file was updated, return 1 -/datum/preferences/proc/savefile_update() - if(savefile_version < 8) //lazily delete everything + additional files so they can be saved in the new format - for(var/ckey in preferences_datums) - var/datum/preferences/D = preferences_datums[ckey] - if(D == src) - var/delpath = "data/player_saves/[copytext(ckey,1,2)]/[ckey]/" - if(delpath && fexists(delpath)) - fdel(delpath) - break - return 0 +/* +SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn + This proc checks if the current directory of the savefile S needs updating + It is to be used by the load_character and load_preferences procs. + (S.cd == "/" is preferences, S.cd == "/character[integer]" is a character slot, etc) - if(savefile_version == SAVEFILE_VERSION_MAX) //update successful. - save_preferences() - save_character() - return 1 - return 0 + if the current directory's version is below SAVEFILE_VERSION_MIN it will simply wipe everything in that directory + (if we're at root "/" then it'll just wipe the entire savefile, for instance.) -/datum/preferences/proc/load_path(ckey,filename="preferences.sav") - if(!ckey) return + if its version is below SAVEFILE_VERSION_MAX but above the minimum, it will load data but later call the + respective update_preferences() or update_character() proc. + Those procs allow coders to specify format changes so users do not lose their setups and have to redo them again. + + Failing all that, the standard sanity checks are performed. They simply check the data is suitable, reverting to + initial() values if necessary. +*/ +/datum/preferences/proc/save_data_needs_update(list/save_data) + if(!save_data) // empty list, either savefile isnt loaded or its a new char + return -1 + if(!save_data["version"]) // special case: if there is no version key, such as in character slots before v12 + return -3 + if(save_data["version"] < SAVEFILE_VERSION_MIN) + return -2 + if(save_data["version"] < SAVEFILE_VERSION_MAX) + return save_data["version"] + return -1 + +//should these procs get fairly long +//just increase SAVEFILE_VERSION_MIN so it's not as far behind +//SAVEFILE_VERSION_MAX and then delete any obsolete if clauses +//from these procs. +//This only really meant to avoid annoying frequent players +//if your savefile is 3 months out of date, then 'tough shit'. + +/datum/preferences/proc/update_preferences(current_version, datum/json_savefile/S) + // Migration from BYOND savefiles to JSON: Important milemark. + // if(current_version < 11) + + // Migration for client preferences + if(current_version < 13) + migration_13_preferences(S) + + +/datum/preferences/proc/update_character(current_version, list/save_data) + // Migration from BYOND savefiles to JSON: Important milemark. + if(current_version == -3) + // Add a version field inside each character + save_data["version"] = SAVEFILE_VERSION_MAX + +/// Migrates from byond savefile to json savefile +/datum/preferences/proc/try_savefile_type_migration() + load_path(client.ckey, "preferences.sav") // old save file + var/old_path = path + load_path(client.ckey) + if(!fexists(old_path)) + return + var/datum/json_savefile/json_savefile = new(path) + json_savefile.import_byond_savefile(new /savefile(old_path)) + json_savefile.save() + return TRUE + +/datum/preferences/proc/load_path(ckey, filename = "preferences.json") + if(!ckey || !load_and_save) + return path = "data/player_saves/[copytext(ckey,1,2)]/[ckey]/[filename]" - savefile_version = SAVEFILE_VERSION_MAX + +/datum/preferences/proc/load_savefile() + if(load_and_save && !path) + CRASH("Attempted to load savefile without first loading a path!") + savefile = new /datum/json_savefile(load_and_save ? path : null) /datum/preferences/proc/load_preferences() - if(!path) return 0 - if(!fexists(path)) return 0 - var/savefile/S = new /savefile(path) - if(!S) return 0 - S.cd = "/" + if(!savefile) + stack_trace("Attempted to load the preferences of [client] without a savefile; did you forget to call load_savefile?") + load_savefile() + if(!savefile) + stack_trace("Failed to load the savefile for [client] after manually calling load_savefile; something is very wrong.") + return FALSE - S["version"] >> savefile_version - //Conversion - if(!savefile_version || !isnum(savefile_version) || savefile_version < SAVEFILE_VERSION_MIN || savefile_version > SAVEFILE_VERSION_MAX) - if(!savefile_update()) //handles updates - savefile_version = SAVEFILE_VERSION_MAX - save_preferences() - save_character() - return 0 + var/needs_update = save_data_needs_update(savefile.get_entry()) + if(load_and_save && (needs_update <= -2)) //fatal, can't load any data + var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup + if(fexists(bacpath)) + fdel(bacpath) //only keep 1 version of backup + fcopy(savefile.path, bacpath) //byond helpfully lets you use a savefile for the first arg. + return FALSE - player_setup.load_preferences(S) - return 1 + apply_all_client_preferences() + + //try to fix any outdated data if necessary + if(needs_update >= 0) + var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup + if(fexists(bacpath)) + fdel(bacpath) //only keep 1 version of backup + fcopy(savefile.path, bacpath) //byond helpfully lets you use a savefile for the first arg. + update_preferences(needs_update, savefile) //needs_update = savefile_version if we need an update (positive integer) + + // Load general prefs after applying migrations + player_setup.load_preferences(savefile) + + //save the updated version + var/old_default_slot = default_slot + // var/old_max_save_slots = max_save_slots + + for(var/slot in savefile.get_entry()) //but first, update all current character slots. + if (copytext(slot, 1, 10) != "character") + continue + var/slotnum = text2num(copytext(slot, 10)) + if (!slotnum) + continue + // max_save_slots = max(max_save_slots, slotnum) //so we can still update byond member slots after they lose memeber status + default_slot = slotnum + if(load_character()) + save_character() + default_slot = old_default_slot + // max_save_slots = old_max_save_slots + save_preferences() + else + // Load general prefs + player_setup.load_preferences(savefile) + + return TRUE /datum/preferences/proc/save_preferences() - if(!path) return 0 - var/savefile/S = new /savefile(path) - if(!S) return 0 - S.cd = "/" + if(!savefile) + CRASH("Attempted to save the preferences of [client] without a savefile. This should have been handled by load_preferences()") + savefile.set_entry("version", SAVEFILE_VERSION_MAX) //updates (or failing that the sanity checks) will ensure data is not invalid at load. Assume up-to-date - S["version"] << savefile_version - player_setup.save_preferences(S) - return 1 + player_setup.save_preferences(savefile) + + for(var/preference_type in GLOB.preference_entries) + var/datum/preference/preference = GLOB.preference_entries[preference_type] + if(preference.savefile_identifier != PREFERENCE_PLAYER) + continue + + if(!(preference.type in recently_updated_keys)) + continue + + recently_updated_keys -= preference.type + + if(preference_type in value_cache) + write_preference(preference, preference.pref_serialize(value_cache[preference_type])) + + savefile.save() + + return TRUE + +/datum/preferences/proc/reset_slot() + var/bacpath = "[path].resetbac" + if(fexists(bacpath)) + fdel(bacpath) //only keep 1 version of backup + fcopy(savefile.path, bacpath) //byond helpfully lets you use a savefile for the first arg. + + savefile.remove_entry("character[default_slot]") + default_slot = 1 + + clear_character_previews() + + // Load slot 1 character + load_character() + // And save them immediately, in case we load an empty slot + save_character() + save_preferences() + return TRUE /datum/preferences/proc/load_character(slot) - if(!path) return 0 - if(!fexists(path)) return 0 - var/savefile/S = new /savefile(path) - if(!S) return 0 - S.cd = "/" - if(!slot) slot = default_slot - if(slot != SAVE_RESET) // SAVE_RESET will reset the slot as though it does not exist, but keep the current slot for saving purposes. - slot = sanitize_integer(slot, 1, config.character_slots, initial(default_slot)) - if(slot != default_slot) - default_slot = slot - S["default_slot"] << slot - else - S["default_slot"] << default_slot + SHOULD_NOT_SLEEP(TRUE) + if(!slot) + slot = default_slot - if(slot != SAVE_RESET) - S.cd = "/character[slot]" - player_setup.load_character(S) - else - player_setup.load_character(S) - S.cd = "/character[default_slot]" - player_setup.save_character(S) + slot = sanitize_integer(slot, 1, config.character_slots, initial(default_slot)) + if(slot != default_slot) + default_slot = slot + savefile.set_entry("default_slot", slot) - clear_character_previews() // VOREStation Edit - return 1 + var/list/save_data = savefile.get_entry("character[slot]") // This is allowed to be null and will give a -1 in needs_update + + var/needs_update = save_data_needs_update(save_data) + if(needs_update == -2) //fatal, can't load any data + return FALSE + + // Read everything into cache (pre-migrations, as migrations should have access to deserialized data) + // Uses priority order as some values may rely on others for creating default values + for(var/datum/preference/preference as anything in get_preferences_in_priority_order()) + if(preference.savefile_identifier != PREFERENCE_CHARACTER) + continue + + value_cache -= preference.type + read_preference(preference.type) + + // It has to be a list or load_character freaks out + if(!save_data) + player_setup.load_character(list()) + else + player_setup.load_character(save_data) + + //try to fix any outdated data if necessary + //preference updating will handle saving the updated data for us. + if(needs_update >= 0 || needs_update == -3) + update_character(needs_update, save_data) //needs_update == savefile_version if we need an update (positive integer + + clear_character_previews() + return TRUE /datum/preferences/proc/save_character() - if(!path) return 0 - var/savefile/S = new /savefile(path) - if(!S) return 0 - S.cd = "/character[default_slot]" + SHOULD_NOT_SLEEP(TRUE) + if(!savefile) + return FALSE - player_setup.save_character(S) - return 1 + var/tree_key = "character[default_slot]" + if(!(tree_key in savefile.get_entry())) + savefile.set_entry(tree_key, list()) + var/save_data = savefile.get_entry(tree_key) + + save_data["version"] = SAVEFILE_VERSION_MAX //load_character will sanitize any bad data, so assume up-to-date. + player_setup.save_character(save_data) + + return TRUE /datum/preferences/proc/overwrite_character(slot) - if(!path) return 0 - if(!fexists(path)) return 0 - var/savefile/S = new /savefile(path) - if(!S) return 0 - if(!slot) slot = default_slot - if(slot != SAVE_RESET) - slot = sanitize_integer(slot, 1, config.character_slots, initial(default_slot)) - if(slot != default_slot) - default_slot = slot - nif_path = nif_durability = nif_savedata = null //VOREStation Add - Don't copy NIF - S["default_slot"] << slot + if(!savefile) + return FALSE + if(!slot) + slot = default_slot - else - S["default_slot"] << default_slot + // This basically just changes default_slot without loading the correct data, so the next save call will overwrite + // the slot + slot = sanitize_integer(slot, 1, config.character_slots, initial(default_slot)) + if(slot != default_slot) + default_slot = slot + nif_path = nif_durability = nif_savedata = null //VOREStation Add - Don't copy NIF + savefile.set_entry("default_slot", slot) - return 1 + return TRUE /datum/preferences/proc/sanitize_preferences() player_setup.sanitize_setup() - return 1 + return TRUE #undef SAVEFILE_VERSION_MAX #undef SAVEFILE_VERSION_MIN diff --git a/code/modules/client/preferences_tgui.dm b/code/modules/client/preferences_tgui.dm new file mode 100644 index 0000000000..a7f1567497 --- /dev/null +++ b/code/modules/client/preferences_tgui.dm @@ -0,0 +1,138 @@ +/datum/preferences/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui, custom_state) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PreferencesMenu", "Preferences") + ui.set_autoupdate(FALSE) + ui.open() + +/datum/preferences/tgui_state(mob/user) + return GLOB.tgui_always_state + +/datum/preferences/tgui_status(mob/user, datum/tgui_state/state) + return user.client == client ? STATUS_INTERACTIVE : STATUS_CLOSE + +/datum/preferences/ui_assets(mob/user) + var/list/assets = list( + // get_asset_datum(/datum/asset/spritesheet/preferences), + get_asset_datum(/datum/asset/json/preferences), + ) + + for (var/datum/preference_middleware/preference_middleware as anything in middleware) + assets += preference_middleware.get_ui_assets() + + return assets + +/datum/preferences/tgui_data(mob/user) + var/list/data = list() + + if(tainted_character_profiles) + data["character_profiles"] = create_character_profiles() + tainted_character_profiles = FALSE + + data["character_preferences"] = compile_character_preferences(user) + + data["active_slot"] = default_slot + + for(var/datum/preference_middleware/preference_middleware as anything in middleware) + data += preference_middleware.get_ui_data(user) + + return data + +/datum/preferences/tgui_static_data(mob/user) + var/list/data = list() + + data["character_profiles"] = create_character_profiles() + + // data["character_preview_view"] = character_preview_view.assigned_map + // data["overflow_role"] = SSjob.GetJobType(SSjob.overflow_role).title + data["window"] = current_window + + for(var/datum/preference_middleware/preference_middleware as anything in middleware) + data += preference_middleware.get_ui_static_data(user) + + return data + +/datum/preferences/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + 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)) // TODO: do this + // tainted_character_profiles = TRUE + + 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/tgui_close(mob/user) + save_character() + save_preferences() + +/datum/preferences/proc/create_character_profiles() + var/list/profiles = list() + + for(var/index in 1 to config.character_slots) + // TODO: It won't be updated in the savefile yet, so just read the name directly + // if(index == default_slot) + // profiles += read_preference(/datum/preference/name/real_name) + // continue + + var/tree_key = "character[index]" + var/save_data = savefile.get_entry(tree_key) + var/name = save_data?["real_name"] + + if(isnull(name)) + profiles += null + continue + + profiles += name + + return profiles + +/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 + + var/value = read_preference(preference.type) + var/data = preference.compile_ui_data(user, value) + + LAZYINITLIST(preferences[preference.category]) + preferences[preference.category][preference.savefile_key] = data + + for(var/datum/preference_middleware/preference_middleware as anything in middleware) + var/list/append_character_preferences = preference_middleware.get_character_preferences(user) + if(isnull(append_character_preferences)) + continue + + for(var/category in append_character_preferences) + if(category in preferences) + preferences[category] += append_character_preferences[category] + else + preferences[category] = append_character_preferences[category] + + return preferences diff --git a/code/modules/client/preferences_toggle_procs.dm b/code/modules/client/preferences_toggle_procs.dm index 97e33d66c9..6b34f88a59 100644 --- a/code/modules/client/preferences_toggle_procs.dm +++ b/code/modules/client/preferences_toggle_procs.dm @@ -1,229 +1,4 @@ //Toggles for preferences, normal clients -/client/verb/toggle_ghost_ears() - set name = "Toggle Ghost Ears" - set category = "Preferences" - set desc = "Toggles between seeing all mob speech and only nearby mob speech as an observer." - - var/pref_path = /datum/client_preference/ghost_ears - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear all mob speech as a ghost.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TGEars") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_vision() - set name = "Toggle Ghost Sight" - set category = "Preferences" - set desc = "Toggles between seeing all mob emotes and only nearby mob emotes as an observer." - - var/pref_path = /datum/client_preference/ghost_sight - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] see all emotes as a ghost.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TGVision") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_radio() - set name = "Toggle Ghost Radio" - set category = "Preferences" - set desc = "Toggles between seeing all radio chat and only nearby radio chatter as an observer." - - var/pref_path = /datum/client_preference/ghost_radio - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear all radios as a ghost.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TGRadio") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_deadchat() - set name = "Toggle Deadchat" - set category = "Preferences" - set desc = "Toggles visibility of dead chat." - - var/pref_path = /datum/client_preference/show_dsay - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear dead chat as a ghost.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TDeadChat") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ooc() - set name = "Toggle OOC" - set category = "Preferences" - set desc = "Toggles visibility of global out of character chat." - - var/pref_path = /datum/client_preference/show_ooc - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(/datum/client_preference/show_ooc)) ? "now" : "no longer"] hear global out of character chat.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TOOC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_looc() - set name = "Toggle LOOC" - set category = "Preferences" - set desc = "Toggles visibility of local out of character chat." - - var/pref_path = /datum/client_preference/show_looc - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear local out of character chat.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TLOOC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_precision_placement() - set name = "Toggle Precision Placement" - set category = "Preferences" - set desc = "Toggles whether objects placed on table will be on cursor position or centered." - - var/pref_path = /datum/client_preference/precision_placement - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] place items where your cursor is on the table.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TPIP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_typing() - set name = "Toggle Typing Indicator" - set category = "Preferences" - set desc = "Toggles you having the speech bubble typing indicator." - - var/pref_path = /datum/client_preference/show_typing_indicator - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] have the speech indicator.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TTIND") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ahelp_sound() - set name = "Toggle Admin Help Sound" - set category = "Preferences" - set desc = "Toggles the ability to hear a noise broadcasted when you get an admin message." - - var/pref_path = /datum/client_preference/holder/play_adminhelp_ping - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] receive noise from admin messages.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TAHelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_lobby_music() - set name = "Toggle Lobby Music" - set category = "Preferences" - set desc = "Toggles the ability to hear the music in the lobby." - - var/pref_path = /datum/client_preference/play_lobby_music - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear music in the lobby.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TLobMusic") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_admin_midis() - set name = "Toggle Admin Music" - set category = "Preferences" - set desc = "Toggles the ability to hear music played by admins." - - var/pref_path = /datum/client_preference/play_admin_midis - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear music from admins.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TAMidis") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ambience() - set name = "Toggle Ambience" - set category = "Preferences" - set desc = "Toggles the ability to hear local ambience." - - var/pref_path = /datum/client_preference/play_ambiance - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear ambient noise.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TAmbience") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_weather_sounds() - set name = "Toggle Weather Sounds" - set category = "Preferences" - set desc = "Toggles the ability to hear weather sounds while on a planet." - - var/pref_path = /datum/client_preference/weather_sounds - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear weather sounds.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TWeatherSounds") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_supermatter_hum() - set name = "Toggle SM Hum" // Avoiding using the full 'Supermatter' name to not conflict with the Setup-Supermatter adminverb. - set category = "Preferences" - set desc = "Toggles the ability to hear supermatter hums." - - var/pref_path = /datum/client_preference/supermatter_hum - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear a hum from the supermatter.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TSupermatterHum") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_jukebox() - set name = "Toggle Jukebox" - set category = "Preferences" - set desc = "Toggles the ability to hear jukebox music." - - var/pref_path = /datum/client_preference/play_jukebox - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear jukebox music.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TJukebox") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - /client/verb/toggle_be_special(role in be_special_flags) set name = "Toggle Special Role Candidacy" set category = "Preferences" @@ -239,146 +14,6 @@ feedback_add_details("admin_verb","TBeSpecial") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/verb/toggle_air_pump_hum() - set name = "Toggle Air Vent Noise" - set category = "Preferences" - set desc = "Toggles the ability to hear air vent humming." - - var/pref_path = /datum/client_preference/air_pump_noise - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear air vents hum, start, and stop.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TAirPumpNoise") - -/client/verb/toggle_old_door_sounds() - set name = "Toggle Old Door Sounds" - set category = "Preferences" - set desc = "Toggles door sounds between old and new." - - var/pref_path = /datum/client_preference/old_door_sounds - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear the legacy door sounds.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TOldDoorSounds") - -/client/verb/toggle_department_door_sounds() - set name = "Toggle Department Door Sounds" - set category = "Preferences" - set desc = "Toggles hearing of department-specific door sounds." - - var/pref_path = /datum/client_preference/department_door_sounds - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear per-department door sounds.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TDepartmentDoorSounds") - -/client/verb/toggle_pickup_sounds() - set name = "Toggle Picked Up Item Sounds" - set category = "Preferences" - set desc = "Toggles the ability to hear sounds when items are picked up." - - var/pref_path = /datum/client_preference/pickup_sounds - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear sounds when items are picked up.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb", "TPickupSounds") - -/client/verb/toggle_drop_sounds() - set name = "Toggle Dropped Item Sounds" - set category = "Preferences" - set desc = "Toggles the ability to hear sounds when items are dropped or thrown." - - var/pref_path = /datum/client_preference/drop_sounds - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear sounds when items are dropped or thrown.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb", "TDropSounds") - -/client/verb/toggle_safe_firing() - set name = "Toggle Gun Firing Intent Requirement" - set category = "Preferences" - set desc = "Toggles between safe and dangerous firing. Safe requires a non-help intent to fire, dangerous can be fired on help intent." - - var/pref_path = /datum/client_preference/safefiring - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src,"You will now use [(is_preference_enabled(/datum/client_preference/safefiring)) ? "safe" : "dangerous"] firearms firing.") - - feedback_add_details("admin_verb","TFiringMode") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_mob_tooltips() - set name = "Toggle Mob Tooltips" - set category = "Preferences" - set desc = "Toggles displaying name/species over mobs when they are moused over." - - var/pref_path = /datum/client_preference/mob_tooltips - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src,"You will now [(is_preference_enabled(/datum/client_preference/mob_tooltips)) ? "see" : "not see"] mob tooltips.") - - feedback_add_details("admin_verb","TMobTooltips") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_inv_tooltips() - set name = "Toggle Item Tooltips" - set category = "Preferences" - set desc = "Toggles displaying name/desc over items when they are moused over (only applies in inventory)." - - var/pref_path = /datum/client_preference/inv_tooltips - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src,"You will now [(is_preference_enabled(/datum/client_preference/inv_tooltips)) ? "see" : "not see"] inventory tooltips.") - - feedback_add_details("admin_verb","TInvTooltips") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_hear_instruments() - set name = "Toggle Hear/Ignore Instruments" - set category = "Preferences" - set desc = "Toggles the ability to hear instruments playing." - - var/pref_path = /datum/client_preference/instrument_toggle - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/instrument_toggle)) ? "hear" : "not hear"] instruments being played.") - - feedback_add_details("admin_verb","THInstm") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_vchat() - set name = "Toggle TGChat" - set category = "Preferences" - set desc = "Toggles TGChat. Reloading TGChat and/or reconnecting required to affect changes." - - var/pref_path = /datum/client_preference/vchat_enable - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src, "You have toggled TGChat [is_preference_enabled(pref_path) ? "on" : "off"]. \ - You will have to reload TGChat and/or reconnect to the server for these changes to take place. \ - TGChat message persistence is not guaranteed if you change this again before the start of the next round.") - /client/verb/toggle_chat_timestamps() set name = "Toggle Chat Timestamps" set category = "Preferences" @@ -389,98 +24,6 @@ to_chat(src, span_notice("You have toggled chat timestamps: [prefs.chat_timestamp ? "ON" : "OFF"].")) -/client/verb/toggle_status_indicators() - set name = "Toggle Status Indicators" - set category = "Preferences" - set desc = "Toggles seeing status indicators over peoples' heads." - - var/pref_path = /datum/client_preference/status_indicators - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/status_indicators)) ? "see" : "not see"] status indicators.") - - feedback_add_details("admin_verb","TStatusIndicators") - - -/client/verb/toggle_radio_sounds() - set name = "Toggle Radio Sounds" - set category = "Preferences" - set desc = "Toggle hearing a sound when somebody speaks over your headset." - - var/pref_path = /datum/client_preference/radio_sounds - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/radio_sounds)) ? "hear" : "not hear"] radio sounds.") - - feedback_add_details("admin_verb","TRadioSounds") - -/client/verb/toggle_say_sounds() - set name = "Sound-Toggle-Say" - set category = "Preferences" - set desc = "Toggle hearing a sound when somebody speaks using say." - - var/pref_path = /datum/client_preference/say_sounds - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/say_sounds)) ? "hear" : "not hear"] say sounds.") - - feedback_add_details("admin_verb","TSaySounds") - -/client/verb/toggle_emote_sounds() - set name = "Sound-Toggle-Me" - set category = "Preferences" - set desc = "Toggle hearing a sound when somebody speaks using me ." - - var/pref_path = /datum/client_preference/emote_sounds - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/emote_sounds)) ? "hear" : "not hear"] me sounds.") - - feedback_add_details("admin_verb","TMeSounds") - -/client/verb/toggle_whisper_sounds() - set name = "Sound-Toggle-Whisper" - set category = "Preferences" - set desc = "Toggle hearing a sound when somebody speaks using whisper." - - var/pref_path = /datum/client_preference/whisper_sounds - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/whisper_sounds)) ? "hear" : "not hear"] whisper sounds.") - - feedback_add_details("admin_verb","TWhisperSounds") - -/client/verb/toggle_subtle_sounds() - set name = "Sound-Toggle-Subtle" - set category = "Preferences" - set desc = "Toggle hearing a sound when somebody uses subtle." - - var/pref_path = /datum/client_preference/subtle_sounds - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/subtle_sounds)) ? "hear" : "not hear"] subtle sounds.") - - feedback_add_details("admin_verb","TSubtleSounds") - -/client/verb/toggle_vore_health_bars() - set name = "Toggle Vore Health Bars" - set category = "Preferences" - set desc = "Toggle the display of vore related health bars" - - var/pref_path = /datum/client_preference/vore_health_bars - toggle_preference(pref_path) - SScharacter_setup.queue_preferences_save(prefs) - - to_chat(src, "Vore related health bars - [(is_preference_enabled(/datum/client_preference/vore_health_bars)) ? "Enabled" : "Disabled"]") - - feedback_add_details("admin_verb","TVoreHealthBars") - // Not attached to a pref datum because those are strict binary toggles /client/verb/toggle_examine_mode() set name = "Toggle Examine Mode" @@ -515,65 +58,3 @@ to_chat(src, "Multilingual parsing will enforce the a language delimiter after the delimiter-key combination (,0,galcom -2 still galcom). The extra delimiter will be consumed by the pattern-matching.") if(MULTILINGUAL_OFF) to_chat(src, "Multilingual parsing is now disabled. Entire messages will be in the language specified at the start of the message.") - - -//Toggles for Staff -//Developers - -/client/proc/toggle_debug_logs() - set name = "Toggle Debug Logs" - set category = "Preferences" - set desc = "Toggles seeing debug logs." - - var/pref_path = /datum/client_preference/debug/show_debug_logs - - if(check_rights(R_ADMIN|R_DEBUG)) - toggle_preference(pref_path) - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] receive debug logs.") - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TADebugLogs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -//Mods -/client/proc/toggle_attack_logs() - set name = "Toggle Attack Logs" - set category = "Preferences" - set desc = "Toggles seeing attack logs." - - var/pref_path = /datum/client_preference/mod/show_attack_logs - - if(check_rights(R_ADMIN|R_MOD)) - toggle_preference(pref_path) - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] receive attack logs.") - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TAAttackLogs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -//General -/client/proc/toggle_admin_global_looc() - set name = "Toggle Admin Global LOOC Visibility" - set category = "Preferences" - set desc = "Toggles seeing LOOC messages outside your actual LOOC range." - - var/pref_path = /datum/client_preference/holder/show_rlooc - - if(holder) - toggle_preference(pref_path) - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear global LOOC.") - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TAGlobalLOOC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggle_admin_deadchat() - set name = "Toggle Admin Living Deadchat" - set category = "Preferences" - set desc = "Toggles seeing deadchat while not observing." - - var/pref_path = /datum/client_preference/holder/show_staff_dsay - - if(holder) - toggle_preference(pref_path) - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear deadchat while not observing.") - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TADeadchat") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/client/preferences_vr.dm b/code/modules/client/preferences_vr.dm index f1cfc4413a..9ee5ed0b24 100644 --- a/code/modules/client/preferences_vr.dm +++ b/code/modules/client/preferences_vr.dm @@ -14,97 +14,6 @@ var/job_talon_low = 0 //Why weren't these in game toggles already? -/client/verb/toggle_eating_noises() - set name = "Toggle Eating Noises" - set category = "Preferences" - set desc = "Toggles hearing Vore Eating noises." - - var/pref_path = /datum/client_preference/eating_noises - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear eating related vore noises.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TEatNoise") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - - -/client/verb/toggle_digestion_noises() - set name = "Toggle Digestion Noises" - set category = "Preferences" - set desc = "Toggles hearing Vore Digestion noises." - - var/pref_path = /datum/client_preference/digestion_noises - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear digestion related vore noises.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TDigestNoise") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_belch_noises() - set name = "Toggle Audible Belching" - set category = "Preferences" - set desc = "Toggles hearing audible belches." - - var/pref_path = /datum/client_preference/belch_noises - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear belching.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TBelchNoise") - -/client/verb/toggle_emote_noises() - set name = "Toggle Emote Noises" - set category = "Preferences" - set desc = "Toggles hearing emote noises." - - var/pref_path = /datum/client_preference/emote_noises - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear emote-related noises.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TEmoteNoise") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_quiets() - set name = "Toggle Ghost Privacy" - set category = "Preferences" - set desc = "Toggles ghosts being able to see your subtles/whispers." - - var/pref_path = /datum/client_preference/whisubtle_vis - - toggle_preference(pref_path) - - to_chat(src, "Ghosts will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear subtles/whispers made by you.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TWhisubtleVis") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_ghost_privacyvision() - set name = "Toggle Ghost Private Eyes/ears" - set category = "Preferences" - set desc = "Toggles your ability to see subtles/whispers. Overrides admin status. Respects Ghost Privacy" - - var/pref_path = /datum/client_preference/ghost_see_whisubtle - - toggle_preference(pref_path) - - to_chat(src, "As a ghost, you will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] hear subtles/whispers made by players.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TGhostSeeWhisSubtle") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - /client/verb/toggle_capture_crystal() set name = "Toggle Catchable" set category = "Preferences" @@ -123,55 +32,3 @@ SScharacter_setup.queue_preferences_save(prefs) feedback_add_details("admin_verb","TCaptureCrystal") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/verb/toggle_mentorhelp_ping() - set name = "Toggle Mentorhelp Ping" - set category = "Preferences" - set desc = "Toggles the mentorhelp ping" - - var/pref_path = /datum/client_preference/play_mentorhelp_ping - - toggle_preference(pref_path) - - to_chat(src, "Mentorhelp pings are now [ is_preference_enabled(pref_path) ? "enabled" : "disabled"]") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb", "TSoundMentorhelps") - -/client/verb/toggle_player_tips() - set name = "Toggle Receiving Player Tips" - set category = "Preferences" - set desc = "When toggled on, you receive tips periodically on roleplay and gameplay." - - var/pref_path = /datum/client_preference/player_tips - - toggle_preference(pref_path) - - to_chat(src, "You are [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] periodically receiving advice on gameplay and roleplay.") - - SScharacter_setup.queue_preferences_save(prefs) - - feedback_add_details("admin_verb", "TReceivePlayerTips") - -/client/verb/toggle_pain_frequency() - set name = "Toggle Pain Frequency" - set category = "Preferences" - set desc = "When toggled on, increases the cooldown of pain messages sent to chat for minor injuries" - - var/pref_path = /datum/client_preference/pain_frequency - - toggle_preference(pref_path) - - to_chat(src, "The cooldown between pain messages for minor (under 20/5 injury. Multi-limb injuries are still faster) is now [ (is_preference_enabled(pref_path)) ? "extended" : "default"].") - -/client/verb/toggle_automatic_afk() - set name = "Toggle Automatic AFK" - set category = "Preferences" - set desc = "When enabled, causes you to be automatically marked as AFK if you are idle for too long." - - var/pref_path = /datum/client_preference/auto_afk - - toggle_preference(pref_path) - - to_chat(src, "You will [ (is_preference_enabled(pref_path)) ? "now" : "not"] be automatically marked as AFK if you are idle for ten minutes or more.") diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index a020840f21..947c486922 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -15,7 +15,7 @@ msg = sanitize(msg) if(!msg) return - if(!is_preference_enabled(/datum/client_preference/show_ooc)) + if(!prefs?.read_preference(/datum/preference/toggle/show_ooc)) to_chat(src, "You have OOC muted.") return @@ -66,7 +66,7 @@ msg = GLOB.is_valid_url.Replace(msg,"$1") for(var/client/target in GLOB.clients) - if(target.is_preference_enabled(/datum/client_preference/show_ooc)) + if(target.prefs?.read_preference(/datum/preference/toggle/show_ooc)) if(target.is_key_ignored(key)) // If we're ignored by this person, then do nothing. continue var/display_name = src.key @@ -101,7 +101,7 @@ if(!msg) return - if(!is_preference_enabled(/datum/client_preference/show_looc)) + if(!prefs?.read_preference(/datum/preference/toggle/show_looc)) to_chat(src, "You have LOOC muted.") return @@ -159,7 +159,7 @@ // Everyone in normal viewing range of the LOOC for(var/mob/viewer in m_viewers) - if(viewer.client && viewer.client.is_preference_enabled(/datum/client_preference/show_looc)) + if(viewer.client && viewer.client.prefs?.read_preference(/datum/preference/toggle/show_looc)) receivers |= viewer.client else if(istype(viewer,/mob/observer/eye)) // For AI eyes and the like var/mob/observer/eye/E = viewer @@ -168,7 +168,7 @@ // Admins with RLOOC displayed who weren't already in for(var/client/admin in GLOB.admins) - if(!(admin in receivers) && admin.is_preference_enabled(/datum/client_preference/holder/show_rlooc)) + if(!(admin in receivers) && admin.prefs?.read_preference(/datum/preference/toggle/holder/show_rlooc)) if(check_rights(R_SERVER, FALSE, admin)) //Stop rLOOC showing for retired staff r_receivers |= admin diff --git a/code/modules/client/verbs/typing.dm b/code/modules/client/verbs/typing.dm index 4845a40e32..50704f6c93 100644 --- a/code/modules/client/verbs/typing.dm +++ b/code/modules/client/verbs/typing.dm @@ -7,7 +7,7 @@ src << output('html/typing_indicator.html', "commandbar_spy") /client/proc/handle_commandbar_typing(href_list) - if(!is_preference_enabled(/datum/client_preference/show_typing_indicator)) + if(!prefs?.read_preference(/datum/preference/toggle/show_typing_indicator)) return if(length(href_list["verb"]) < 1 || !(lowertext(href_list["verb"]) in IC_VERBS) || text2num(href_list["argument_length"]) < 1) @@ -31,10 +31,10 @@ /** Sets the mob as "thinking" - with indicator and the TRAIT_THINKING_IN_CHARACTER trait */ /client/proc/start_thinking(channel) - if(!is_preference_enabled(/datum/client_preference/show_typing_indicator)) + if(!prefs?.read_preference(/datum/preference/toggle/show_typing_indicator)) return FALSE if(channel == "Whis" || channel == "Subtle" || channel == "whisper" || channel == "subtle") - if(!is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle)) + if(!prefs?.read_preference(/datum/preference/toggle/show_typing_indicator_subtle)) return FALSE ADD_TRAIT(mob, TRAIT_THINKING_IN_CHARACTER, CURRENTLY_TYPING_TRAIT) mob.create_thinking_indicator() @@ -50,10 +50,10 @@ /client/proc/start_typing(channel) var/mob/client_mob = mob client_mob.remove_thinking_indicator() - if(!is_preference_enabled(/datum/client_preference/show_typing_indicator) || !HAS_TRAIT(client_mob, TRAIT_THINKING_IN_CHARACTER)) + if(!prefs?.read_preference(/datum/preference/toggle/show_typing_indicator) || !HAS_TRAIT(client_mob, TRAIT_THINKING_IN_CHARACTER)) return FALSE if(channel == "Whis" || channel == "Subtle" || channel == "whisper" || channel == "subtle") - if(!is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle)) + if(!prefs?.read_preference(/datum/preference/toggle/show_typing_indicator_subtle)) return FALSE client_mob.create_typing_indicator() addtimer(CALLBACK(src, PROC_REF(stop_typing), channel), 5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE) @@ -67,10 +67,10 @@ return FALSE var/mob/client_mob = mob client_mob.remove_typing_indicator() - if(!is_preference_enabled(/datum/client_preference/show_typing_indicator) || !HAS_TRAIT(client_mob, TRAIT_THINKING_IN_CHARACTER)) + if(!prefs?.read_preference(/datum/preference/toggle/show_typing_indicator) || !HAS_TRAIT(client_mob, TRAIT_THINKING_IN_CHARACTER)) return FALSE if(channel == "Whis" || channel == "Subtle" || channel == "whisper" || channel == "subtle") - if(!is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle)) + if(!prefs?.read_preference(/datum/preference/toggle/show_typing_indicator_subtle)) return FALSE client_mob.create_thinking_indicator() diff --git a/code/modules/emotes/definitions/audible_belch.dm b/code/modules/emotes/definitions/audible_belch.dm index 3bb3b6a17c..25db0333cb 100644 --- a/code/modules/emotes/definitions/audible_belch.dm +++ b/code/modules/emotes/definitions/audible_belch.dm @@ -2,7 +2,7 @@ key = "belch" emote_message_3p = "belches." message_type = AUDIBLE_MESSAGE - sound_preferences = list(/datum/client_preference/emote_noises,/datum/client_preference/belch_noises) + sound_preferences = list(/datum/preference/toggle/emote_noises, /datum/preference/toggle/belch_noises) /decl/emote/audible/belch/get_emote_sound(var/atom/user) return list( diff --git a/code/modules/emotes/emote_define.dm b/code/modules/emotes/emote_define.dm index 7aed1e982f..94812dea26 100644 --- a/code/modules/emotes/emote_define.dm +++ b/code/modules/emotes/emote_define.dm @@ -29,8 +29,8 @@ var/global/list/emotes_by_key var/emote_message_radio_synthetic // As above, but for synthetics. var/emote_message_muffled // A message to show if the emote is audible and the user is muzzled. - var/list/emote_sound // A sound for the emote to play. - // Can either be a single sound, a list of sounds to pick from, or an + var/list/emote_sound // A sound for the emote to play. + // Can either be a single sound, a list of sounds to pick from, or an // associative array of gender to single sounds/a list of sounds. var/list/emote_sound_synthetic // As above, but used when check_synthetic() is true. var/emote_volume = 50 // Volume of sound to play. @@ -42,8 +42,8 @@ var/global/list/emotes_by_key var/check_range // falsy, or a range outside which the emote will not work var/conscious = TRUE // Do we need to be awake to emote this? var/emote_range = 0 // If >0, restricts emote visibility to viewers within range. - - var/sound_preferences = list(/datum/client_preference/emote_noises) // Default emote sound_preferences is just emote_noises. Belch emote overrides this list for pref-checks. + + var/sound_preferences = list(/datum/preference/toggle/emote_noises) // Default emote sound_preferences is just emote_noises. Belch emote overrides this list for pref-checks. var/sound_vary = FALSE /decl/emote/Initialize() diff --git a/code/modules/emotes/emote_mob.dm b/code/modules/emotes/emote_mob.dm index 255eb037c3..087cecb6cc 100644 --- a/code/modules/emotes/emote_mob.dm +++ b/code/modules/emotes/emote_mob.dm @@ -211,7 +211,7 @@ if(!T) return if(client) - playsound(T, pick(emote_sound), 25, TRUE, falloff = 1 , is_global = TRUE, frequency = ourfreq, ignore_walls = FALSE, preference = /datum/client_preference/emote_sounds) + playsound(T, pick(emote_sound), 25, TRUE, falloff = 1 , is_global = TRUE, frequency = ourfreq, ignore_walls = FALSE, preference = /datum/preference/toggle/emote_sounds) var/list/in_range = get_mobs_and_objs_in_view_fast(T,range,2,remote_ghosts = client ? TRUE : FALSE) var/list/m_viewers = in_range["mobs"] diff --git a/code/modules/instruments/songs/play_legacy.dm b/code/modules/instruments/songs/play_legacy.dm index bd964765cb..13c3b74453 100644 --- a/code/modules/instruments/songs/play_legacy.dm +++ b/code/modules/instruments/songs/play_legacy.dm @@ -89,5 +89,5 @@ if(!M) hearing_mobs -= M continue - M.playsound_local(source, null, volume * using_instrument.volume_multiplier, S = music_played, preference = /datum/client_preference/instrument_toggle, volume_channel = VOLUME_CHANNEL_INSTRUMENTS) + M.playsound_local(source, null, volume * using_instrument.volume_multiplier, S = music_played, preference = /datum/preference/toggle/instrument_toggle, volume_channel = VOLUME_CHANNEL_INSTRUMENTS) // 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 a838a75615..bbabdb9370 100644 --- a/code/modules/instruments/songs/play_synthesized.dm +++ b/code/modules/instruments/songs/play_synthesized.dm @@ -79,7 +79,7 @@ channel = channel, pressure_affected = null, S = copy, - preference = /datum/client_preference/instrument_toggle, + preference = /datum/preference/toggle/instrument_toggle, volume_channel = VOLUME_CHANNEL_INSTRUMENTS) // Could do environment and echo later but not for now diff --git a/code/modules/media/mediamanager.dm b/code/modules/media/mediamanager.dm index adc522546f..7ab12e909e 100644 --- a/code/modules/media/mediamanager.dm +++ b/code/modules/media/mediamanager.dm @@ -137,9 +137,9 @@ // Tell the player to play something via JS. /datum/media_manager/proc/send_update() - if(!(owner.prefs)) + if(!owner.prefs) return - if(!owner.is_preference_enabled(/datum/client_preference/play_jukebox) && url != "") + if(!owner.prefs.read_preference(/datum/preference/toggle/play_jukebox) && url != "") return // Don't send anything other than a cancel to people with SOUND_STREAMING pref disabled MP_DEBUG("Sending update to mediapanel ([url], [(world.time - start_time) / 10], [volume * source_volume])...") owner << output(list2params(list(url, (world.time - start_time) / 10, volume * source_volume)), "[WINDOW_ID]:SetMusic") diff --git a/code/modules/mentor/mentor.dm b/code/modules/mentor/mentor.dm index 49f113b6ef..a226b7a318 100644 --- a/code/modules/mentor/mentor.dm +++ b/code/modules/mentor/mentor.dm @@ -254,7 +254,7 @@ var/list/mentor_verbs_default = list( log_admin("[key_name(src)]->[key_name(recipient)]: [msg]") - if(recipient.is_preference_enabled(/datum/client_preference/play_mentorhelp_ping)) + if(recipient.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) recipient << 'sound/effects/mentorhelp.mp3' for(var/client/C in GLOB.mentors) diff --git a/code/modules/mentor/mentorhelp.dm b/code/modules/mentor/mentorhelp.dm index d9deb92a76..4dd282fd18 100644 --- a/code/modules/mentor/mentorhelp.dm +++ b/code/modules/mentor/mentorhelp.dm @@ -194,10 +194,10 @@ GLOBAL_DATUM_INIT(mhelp_tickets, /datum/mentor_help_tickets, new) var/chat_msg = "(ESCALATE) Ticket [TicketHref("#[id]", ref_src)]: [LinkedReplyName(ref_src)]: [msg]" AddInteraction("[LinkedReplyName(ref_src)]: [msg]") for (var/client/C in GLOB.mentors) - if (C.is_preference_enabled(/datum/client_preference/play_mentorhelp_ping)) + if (C.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) C << 'sound/effects/mentorhelp.mp3' for (var/client/C in GLOB.admins) - if (C.is_preference_enabled(/datum/client_preference/play_mentorhelp_ping)) + if (C.prefs?.read_preference(/datum/preference/toggle/play_mentorhelp_ping)) C << 'sound/effects/mentorhelp.mp3' message_mentors(chat_msg) diff --git a/code/modules/mob/animations.dm b/code/modules/mob/animations.dm index 9e6efaed9c..3e1d2b5045 100644 --- a/code/modules/mob/animations.dm +++ b/code/modules/mob/animations.dm @@ -238,8 +238,7 @@ note dizziness decrements automatically in the mob's Life() proc. //Check for clients with pref enabled var/list/viewing = list() for(var/mob/M as anything in viewers(A)) - var/client/C = M.client - if(C && C.is_preference_enabled(/datum/client_preference/attack_icons)) + if(M.client?.prefs?.read_preference(/datum/preference/toggle/attack_icons)) viewing += M.client //Animals attacking each other in the distance, probably. Forgeddaboutit. diff --git a/code/modules/mob/emote.dm b/code/modules/mob/emote.dm index 712393fd21..0325ccf80f 100644 --- a/code/modules/mob/emote.dm +++ b/code/modules/mob/emote.dm @@ -11,7 +11,7 @@ to_chat(src, "You cannot send deadchat emotes (muted).") return - if(!is_preference_enabled(/datum/client_preference/show_dsay)) + if(!client?.prefs?.read_preference(/datum/preference/toggle/show_dsay)) to_chat(src, "You have deadchat muted.") return diff --git a/code/modules/mob/hear_say.dm b/code/modules/mob/hear_say.dm index b0f04e6247..cd861a63f8 100644 --- a/code/modules/mob/hear_say.dm +++ b/code/modules/mob/hear_say.dm @@ -66,7 +66,7 @@ if(!client && !teleop) return FALSE - if(isobserver(src) && is_preference_enabled(/datum/client_preference/ghost_ears)) + if(isobserver(src) && client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) if(speaker && !speaker.client && !(speaker in view(src))) //Does the speaker have a client? It's either random stuff that observers won't care about (Experiment 97B says, 'EHEHEHEHEHEHEHE') //Or someone snoring. So we make it where they won't hear it. @@ -108,7 +108,7 @@ if(speaker_name != speaker.real_name && speaker.real_name) speaker_name = "[speaker.real_name] ([speaker_name])" track = "([ghost_follow_link(speaker, src)]) " - if(is_preference_enabled(/datum/client_preference/ghost_ears) && (speaker in view(src))) + if(client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears) && (speaker in view(src))) message = "[message]" if(is_deaf()) @@ -119,7 +119,7 @@ else var/message_to_send = null message_to_send = "[speaker_name][speaker.GetAltName()] [track][message]" - if(check_mentioned(multilingual_to_message(message_pieces)) && is_preference_enabled(/datum/client_preference/check_mention)) + if(check_mentioned(multilingual_to_message(message_pieces)) && client?.prefs?.read_preference(/datum/preference/toggle/check_mention)) message_to_send = "[message_to_send]" on_hear_say(message_to_send, speaker) @@ -227,7 +227,7 @@ if(client.prefs.chat_timestamp) time = say_timestamp() var/final_message = "[part_b][speaker_name][part_c][formatted][part_d]" - if(check_mentioned(formatted) && is_preference_enabled(/datum/client_preference/check_mention)) + if(check_mentioned(formatted) && client?.prefs?.read_preference(/datum/preference/toggle/check_mention)) final_message = "[time][part_a][final_message][part_e]" else final_message = "[time][part_a][final_message][part_e]" @@ -238,7 +238,7 @@ if(client.prefs.chat_timestamp) time = say_timestamp() var/final_message = "[part_b][track][part_c][formatted][part_d]" - if(check_mentioned(formatted) && is_preference_enabled(/datum/client_preference/check_mention)) + if(check_mentioned(formatted) && client?.prefs?.read_preference(/datum/preference/toggle/check_mention)) final_message = "[time][part_a][final_message][part_e]" else final_message = "[time][part_a][final_message][part_e]" @@ -249,7 +249,7 @@ if(client.prefs.chat_timestamp) time = say_timestamp() var/final_message = "[part_b][speaker_name][part_c][formatted][part_d]" - if(check_mentioned(formatted) && is_preference_enabled(/datum/client_preference/check_mention)) + if(check_mentioned(formatted) && client?.prefs?.read_preference(/datum/preference/toggle/check_mention)) final_message = "[time][part_a][final_message][part_e]" else final_message = "[time][part_a][final_message][part_e]" @@ -260,7 +260,7 @@ if(client.prefs.chat_timestamp) time = say_timestamp() var/final_message = "[part_b][track][part_c][formatted][part_d]" - if(check_mentioned(formatted) && is_preference_enabled(/datum/client_preference/check_mention)) + if(check_mentioned(formatted) && client?.prefs?.read_preference(/datum/preference/toggle/check_mention)) final_message = "[time][part_a][final_message][part_e]" else final_message = "[time][part_a][final_message][part_e]" diff --git a/code/modules/mob/language/synthetic.dm b/code/modules/mob/language/synthetic.dm index 6a4ccd494d..0d301117bb 100644 --- a/code/modules/mob/language/synthetic.dm +++ b/code/modules/mob/language/synthetic.dm @@ -26,7 +26,7 @@ for (var/mob/M in dead_mob_list) if(!istype(M,/mob/new_player) && !istype(M,/mob/living/carbon/brain)) //No meta-evesdropping var/message_to_send = span_binary("[message_start] ([ghost_follow_link(speaker, M)]) [message_body]") - if(M.check_mentioned(message) && M.is_preference_enabled(/datum/client_preference/check_mention)) + if(M.check_mentioned(message) && M.client?.prefs?.read_preference(/datum/preference/toggle/check_mention)) message_to_send = "[message_to_send]" M.show_message(message_to_send, 2) @@ -39,7 +39,7 @@ continue var/message_to_send = span_binary("[message_start] [message_body]") - if(S.check_mentioned(message) && S.is_preference_enabled(/datum/client_preference/check_mention)) + if(S.check_mentioned(message) && S.client?.prefs?.read_preference(/datum/preference/toggle/check_mention)) message_to_send = "[message_to_send]" S.show_message(message_to_send, 2) diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm index a34cce9cb1..095d3815d4 100644 --- a/code/modules/mob/living/carbon/human/death.dm +++ b/code/modules/mob/living/carbon/human/death.dm @@ -84,7 +84,7 @@ if(mind) // SSgame_master.adjust_danger(gibbed ? 40 : 20) // VOREStation Edit - We don't use SSgame_master yet. for(var/mob/observer/dead/O in mob_list) - if(O.client && O.client.is_preference_enabled(/datum/client_preference/show_dsay)) + if(O.client?.prefs?.read_preference(/datum/preference/toggle/show_dsay)) to_chat(O, "[src] has died in [get_area(src)]. [ghost_follow_link(src, O)] ") if(!gibbed && species.death_sound) diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm index 6a75c780f2..8473a39b1b 100644 --- a/code/modules/mob/living/carbon/human/life.dm +++ b/code/modules/mob/living/carbon/human/life.dm @@ -1167,7 +1167,7 @@ if(noisy == TRUE && nutrition < 250 && prob(10)) //VOREStation edit for hunger noises. var/sound/growlsound = sound(get_sfx("hunger_sounds")) var/growlmultiplier = 100 - (nutrition / 250 * 100) - playsound(src, growlsound, vol = growlmultiplier, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/client_preference/digestion_noises) + playsound(src, growlsound, vol = growlmultiplier, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/preference/toggle/digestion_noises) // VOREStation Edit End if((CE_DARKSIGHT in chem_effects) && chemical_darksight == 0) @@ -1248,7 +1248,7 @@ if(fear) fear = (fear - 1) - if(fear >= 80 && is_preference_enabled(/datum/client_preference/play_ambiance)) + if(fear >= 80 && client?.prefs?.read_preference(/datum/preference/toggle/play_ambience)) if(last_fear_sound + 51 SECONDS <= world.time) src << sound('sound/effects/Heart Beat.ogg',0,0,0,25) last_fear_sound = world.time @@ -1989,7 +1989,7 @@ if(!H || (H.robotic >= ORGAN_ROBOT)) return - if(pulse >= PULSE_2FAST || shock_stage >= 10 || (istype(get_turf(src), /turf/space) && is_preference_enabled(/datum/client_preference/play_ambiance))) + if(pulse >= PULSE_2FAST || shock_stage >= 10 || (istype(get_turf(src), /turf/space) && read_preference(/datum/preference/toggle/play_ambience))) //PULSE_THREADY - maximum value for pulse, currently it 5. //High pulse value corresponds to a fast rate of heartbeat. //Divided by 2, otherwise it is too slow. diff --git a/code/modules/mob/living/inventory.dm b/code/modules/mob/living/inventory.dm index eb93f679e3..1e493dda99 100644 --- a/code/modules/mob/living/inventory.dm +++ b/code/modules/mob/living/inventory.dm @@ -48,7 +48,7 @@ item_dropped = r_hand . = drop_r_hand(Target) - if (istype(item_dropped) && !QDELETED(item_dropped) && is_preference_enabled(/datum/client_preference/drop_sounds)) + if (istype(item_dropped) && !QDELETED(item_dropped) && check_sound_preference(/datum/preference/toggle/drop_sounds)) addtimer(CALLBACK(src, PROC_REF(make_item_drop_sound), item_dropped), 1) /mob/proc/make_item_drop_sound(obj/item/I) @@ -56,7 +56,7 @@ return if(I.drop_sound) - playsound(I, I.drop_sound, 25, 0, preference = /datum/client_preference/drop_sounds) + playsound(I, I.drop_sound, 25, 0, preference = /datum/preference/toggle/drop_sounds) //Drops the item in our left hand diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm index e012097056..58240e4ab0 100644 --- a/code/modules/mob/living/life.dm +++ b/code/modules/mob/living/life.dm @@ -39,7 +39,7 @@ if(client) var/idle_limit = 10 MINUTES - if(client.inactivity >= idle_limit && !away_from_keyboard && src.is_preference_enabled(/datum/client_preference/auto_afk)) //if we're not already afk and we've been idle too long, and we have automarking enabled... then automark it + if(client.inactivity >= idle_limit && !away_from_keyboard && client.prefs?.read_preference(/datum/preference/toggle/auto_afk)) //if we're not already afk and we've been idle too long, and we have automarking enabled... then automark it add_status_indicator("afk") to_chat(src, "You have been idle for too long, and automatically marked as AFK.") away_from_keyboard = TRUE diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index e6e20d231f..83acf1d11c 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -363,14 +363,14 @@ var/list/channel_to_radio_key = new if(M && src) //If we still exist, when the spawn processes //VOREStation Add - Ghosts don't hear whispers - if(whispering && isobserver(M) && (!M.is_preference_enabled(/datum/client_preference/ghost_see_whisubtle) || \ - (!is_preference_enabled(/datum/client_preference/whisubtle_vis) && !M.client?.holder))) + if(whispering && isobserver(M) && (!M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle) || \ + (!client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) && !M.client?.holder))) M.show_message("[src.name] [w_not_heard].", 2) return //VOREStation Add End var/dst = get_dist(get_turf(M),get_turf(src)) - var/runechat_enabled = M.client?.is_preference_enabled(/datum/client_preference/runechat_mob) + var/runechat_enabled = M.client?.prefs?.read_preference(/datum/preference/toggle/runechat_mob) if(dst <= message_range || (M.stat == DEAD && !forbid_seeing_deadchat)) //Inside normal message range, or dead with ears (handled in the view proc) if(M.hear_say(message_pieces, verb, italics, src, speech_sound, sound_vol)) @@ -413,12 +413,12 @@ var/list/channel_to_radio_key = new message = "([message_mode == "headset" ? "Common" : capitalize(message_mode)]) [message]" //Adds radio keys used if available if(whispering) if(do_sound && message) - playsound(T, pick(voice_sounds_list), 25, TRUE, extrarange = -6, falloff = 1 , is_global = TRUE, frequency = ourfreq, ignore_walls = FALSE, preference = /datum/client_preference/whisper_sounds) + playsound(T, pick(voice_sounds_list), 25, TRUE, extrarange = -6, falloff = 1 , is_global = TRUE, frequency = ourfreq, ignore_walls = FALSE, preference = /datum/preference/toggle/whisper_sounds) log_whisper(message, src) else if(do_sound && message) - playsound(T, pick(voice_sounds_list), 75, TRUE, falloff = 1 , is_global = TRUE, frequency = ourfreq, ignore_walls = FALSE, preference = /datum/client_preference/say_sounds) + playsound(T, pick(voice_sounds_list), 75, TRUE, falloff = 1 , is_global = TRUE, frequency = ourfreq, ignore_walls = FALSE, preference = /datum/preference/toggle/say_sounds) log_say(message, src) return 1 diff --git a/code/modules/mob/living/silicon/pai/pai_vr.dm b/code/modules/mob/living/silicon/pai/pai_vr.dm index 07a59d4f69..4afa7a3fae 100644 --- a/code/modules/mob/living/silicon/pai/pai_vr.dm +++ b/code/modules/mob/living/silicon/pai/pai_vr.dm @@ -514,9 +514,9 @@ for (var/mob/G in player_list) if (istype(G, /mob/new_player)) continue - else if(isobserver(G) && G.is_preference_enabled(/datum/client_preference/ghost_ears)) - if((is_preference_enabled(/datum/client_preference/whisubtle_vis) || G.client.holder) && \ - G.is_preference_enabled(/datum/client_preference/ghost_see_whisubtle)) + else if(isobserver(G) && G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) + if((client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) || G.client.holder) && \ + G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle)) to_chat(G, "[src.name]'s screen prints, \"[message]\"") /mob/living/silicon/pai/proc/touch_window(soft_name) //This lets us touch TGUI procs and windows that may be nested behind other TGUI procs and windows diff --git a/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper_vr.dm b/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper_vr.dm index a7a9386c39..fe632a206c 100644 --- a/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper_vr.dm +++ b/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper_vr.dm @@ -91,7 +91,7 @@ if(do_after(user, 30, target) && length(contents) < max_item_count) target.forceMove(src) user.visible_message("[hound.name]'s [src.name] groans lightly as [target.name] slips inside.", "Your [src.name] groans lightly as [target] slips inside.") - playsound(src, gulpsound, vol = 60, vary = 1, falloff = 0.1, preference = /datum/client_preference/eating_noises) + playsound(src, gulpsound, vol = 60, vary = 1, falloff = 0.1, preference = /datum/preference/toggle/eating_noises) if(analyzer && istype(target,/obj/item)) var/obj/item/tech_item = target var/list/tech_levels = list() @@ -111,7 +111,7 @@ trashmouse.forceMove(src) trashmouse.reset_view(src) user.visible_message("[hound.name]'s [src.name] groans lightly as [trashmouse] slips inside.", "Your [src.name] groans lightly as [trashmouse] slips inside.") - playsound(src, gulpsound, vol = 60, vary = 1, falloff = 0.1, preference = /datum/client_preference/eating_noises) + playsound(src, gulpsound, vol = 60, vary = 1, falloff = 0.1, preference = /datum/preference/toggle/eating_noises) if(delivery) if(islist(deliverylists[delivery_tag])) deliverylists[delivery_tag] |= trashmouse @@ -133,7 +133,7 @@ START_PROCESSING(SSobj, src) user.visible_message("[hound.name]'s [src.name] groans lightly as [trashman] slips inside.", "Your [src.name] groans lightly as [trashman] slips inside.") log_admin("[key_name(hound)] has eaten [key_name(patient)] with a cyborg belly. ([hound ? "JMP" : "null"])") - playsound(src, gulpsound, vol = 100, vary = 1, falloff = 0.1, preference = /datum/client_preference/eating_noises) + playsound(src, gulpsound, vol = 100, vary = 1, falloff = 0.1, preference = /datum/preference/toggle/eating_noises) if(delivery) if(islist(deliverylists[delivery_tag])) deliverylists[delivery_tag] |= trashman @@ -164,7 +164,7 @@ START_PROCESSING(SSobj, src) user.visible_message("[hound.name]'s [src.name] lights up as [H.name] slips inside.", "Your [src] lights up as [H] slips inside. Life support functions engaged.") log_admin("[key_name(hound)] has eaten [key_name(patient)] with a cyborg belly. ([hound ? "JMP" : "null"])") - playsound(src, gulpsound, vol = 100, vary = 1, falloff = 0.1, preference = /datum/client_preference/eating_noises) + playsound(src, gulpsound, vol = 100, vary = 1, falloff = 0.1, preference = /datum/preference/toggle/eating_noises) /obj/item/device/dogborg/sleeper/proc/ingest_atom(var/atom/ingesting) if (!ingesting || ingesting == hound) @@ -551,11 +551,11 @@ 'sound/vore/death8.ogg', 'sound/vore/death9.ogg', 'sound/vore/death10.ogg') - playsound(src, finisher, vol = 100, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/client_preference/digestion_noises) + playsound(src, finisher, vol = 100, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/preference/toggle/digestion_noises) to_chat(hound, "Your [src.name] is now clean. Ending self-cleaning cycle.") cleaning = 0 update_patient() - playsound(src, 'sound/machines/ding.ogg', vol = 100, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/client_preference/digestion_noises) + playsound(src, 'sound/machines/ding.ogg', vol = 100, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/preference/toggle/digestion_noises) return if(prob(20)) @@ -572,7 +572,7 @@ 'sound/vore/digest10.ogg', 'sound/vore/digest11.ogg', 'sound/vore/digest12.ogg') - playsound(src, churnsound, vol = 100, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/client_preference/digestion_noises) + playsound(src, churnsound, vol = 100, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/preference/toggle/digestion_noises) //If the timing is right, and there are items to be touched if(air_master.current_cycle%3==1 && length(touchable_items)) @@ -606,7 +606,7 @@ 'sound/vore/death8.ogg', 'sound/vore/death9.ogg', 'sound/vore/death10.ogg') - playsound(src, deathsound, vol = 100, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/client_preference/digestion_noises) + playsound(src, deathsound, vol = 100, vary = 1, falloff = 0.1, ignore_walls = TRUE, preference = /datum/preference/toggle/digestion_noises) if(is_vore_predator(T)) for(var/obj/belly/B as anything in T.vore_organs) for(var/atom/movable/thing in B) diff --git a/code/modules/mob/living/silicon/robot/drone/drone_say.dm b/code/modules/mob/living/silicon/robot/drone/drone_say.dm index 5e8dd6dc74..d35e87fff6 100644 --- a/code/modules/mob/living/silicon/robot/drone/drone_say.dm +++ b/code/modules/mob/living/silicon/robot/drone/drone_say.dm @@ -32,8 +32,8 @@ for (var/mob/M in player_list) if (istype(M, /mob/new_player)) continue - else if(M.stat == DEAD && M.is_preference_enabled(/datum/client_preference/ghost_ears)) + else if(M.stat == DEAD && M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) if(M.client) to_chat(M, "[src] transmits, \"[message]\"") return 1 - return ..() \ No newline at end of file + return ..() diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/alien animals/stardog.dm b/code/modules/mob/living/simple_mob/subtypes/animal/alien animals/stardog.dm index 633df96ec8..2abb3b52df 100644 --- a/code/modules/mob/living/simple_mob/subtypes/animal/alien animals/stardog.dm +++ b/code/modules/mob/living/simple_mob/subtypes/animal/alien animals/stardog.dm @@ -500,14 +500,14 @@ for(var/mob/M as anything in vis_mobs) if(isnewplayer(M)) continue - if(isobserver(M) && (!M.is_preference_enabled(/datum/client_preference/ghost_see_whisubtle) || \ - !L.is_preference_enabled(/datum/client_preference/whisubtle_vis) && !M.client?.holder)) + if(isobserver(M) && (!M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle) || \ + !L.client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) && !M.client?.holder)) spawn(0) M.show_message(undisplayed_message, 2) else spawn(0) M.show_message(message, 2) - if(M.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(M.read_preference(/datum/preference/toggle/subtle_sounds)) M << sound('sound/talksounds/subtle_sound.ogg', volume = 50) /decl/flooring/fur @@ -930,7 +930,7 @@ spawnstuff = FALSE /area/redgate/stardog/flesh_abyss/play_ambience(var/mob/living/L, initial = TRUE) - if(!L.is_preference_enabled(/datum/client_preference/digestion_noises)) + if(!L.check_sound_preference(/datum/preference/toggle/digestion_noises)) return ..() @@ -1172,14 +1172,14 @@ for(var/mob/M as anything in vis_mobs) if(isnewplayer(M)) continue - if(isobserver(M) && (!M.is_preference_enabled(/datum/client_preference/ghost_see_whisubtle) || \ - !L.is_preference_enabled(/datum/client_preference/whisubtle_vis) && !M.client?.holder)) + if(isobserver(M) && (!M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle) || \ + !L.client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) && !M.client?.holder)) spawn(0) M.show_message(undisplayed_message, 2) else spawn(0) M.show_message(message, 2) - if(M.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(M.read_preference(/datum/preference/toggle/subtle_sounds)) M << sound('sound/talksounds/subtle_sound.ogg', volume = 50) /area/redgate/stardog/eyes @@ -1335,8 +1335,8 @@ var/go = FALSE if(isobserver(AM)) return - playsound(src, teleport_sound, vol = 100, vary = 1, preference = /datum/client_preference/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) - playsound(target, teleport_sound, vol = 100, vary = 1, preference = /datum/client_preference/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, teleport_sound, vol = 100, vary = 1, preference = /datum/preference/toggle/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(target, teleport_sound, vol = 100, vary = 1, preference = /datum/preference/toggle/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) if(isliving(AM)) var/mob/living/L = AM if(teleport_message && L.client) @@ -1374,7 +1374,7 @@ var/mob/living/simple_mob/vore/overmap/stardog/dog = s.parent dog.adjust_nutrition(I.reagents.total_volume) dog.adjust_affinity(25) - playsound(src, teleport_sound, vol = 100, vary = 1, preference = /datum/client_preference/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, teleport_sound, vol = 100, vary = 1, preference = /datum/preference/toggle/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) visible_message("The dog gobbles up \the [I]!") if(dog.client) to_chat(dog, "[I.thrower ? "\The [I.thrower]" : "Someone"] feeds \the [I] to you!") @@ -1699,7 +1699,7 @@ /obj/structure/auto_flesh_door/proc/Open() isSwitchingStates = 1 var/oursound = pick(open_sounds) - playsound(src, oursound, 100, 1, preference = /datum/client_preference/digestion_noises , volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, oursound, 100, 1, preference = /datum/preference/toggle/digestion_noises , volume_channel = VOLUME_CHANNEL_VORE) flick("flesh-opening",src) sleep(8) density = FALSE @@ -1715,7 +1715,7 @@ /obj/structure/auto_flesh_door/proc/Close() isSwitchingStates = 1 var/oursound = pick(open_sounds) - playsound(src, oursound, 100, 1, preference = /datum/client_preference/digestion_noises , volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, oursound, 100, 1, preference = /datum/preference/toggle/digestion_noises , volume_channel = VOLUME_CHANNEL_VORE) flick("flesh-closing",src) sleep(8) density = TRUE diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer.dm b/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer.dm index 50e54cf8aa..649a206306 100644 --- a/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer.dm +++ b/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer.dm @@ -266,7 +266,7 @@ for(var/mob/M in player_list) if(istype(M, /mob/new_player)) continue - else if(M.stat == DEAD && M.is_preference_enabled(/datum/client_preference/ghost_ears)) + else if(M.stat == DEAD && M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) to_chat(M, "[src.true_name] whispers to [host], \"[message]\"") diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer_captive.dm b/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer_captive.dm index ab1d9cfbfe..c4ee2ce60d 100644 --- a/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer_captive.dm +++ b/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer_captive.dm @@ -28,7 +28,7 @@ for (var/mob/M in player_list) if (istype(M, /mob/new_player)) continue - else if(M.stat == DEAD && M.is_preference_enabled(/datum/client_preference/ghost_ears)) + else if(M.stat == DEAD && M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears)) to_chat(M, "The captive mind of [src] whispers, \"[message]\"") /mob/living/captive_brain/me_verb(message as text) diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 14a3c56e8c..4ff396fd0a 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -63,12 +63,12 @@ recalculate_vis() // AO support - var/ao_enabled = client.is_preference_enabled(/datum/client_preference/ambient_occlusion) + var/ao_enabled = client.prefs?.read_preference(/datum/preference/toggle/ambient_occlusion) plane_holder.set_ao(VIS_OBJS, ao_enabled) plane_holder.set_ao(VIS_MOBS, ao_enabled) // Status indicators - var/status_enabled = client.is_preference_enabled(/datum/client_preference/status_indicators) + var/status_enabled = client.prefs?.read_preference(/datum/preference/toggle/status_indicators) plane_holder.set_vis(VIS_STATUS, status_enabled) //set macro to normal incase it was overriden (like cyborg currently does) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 7431cc3980..fb636c0cf5 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1305,20 +1305,18 @@ return TRUE /mob/MouseEntered(location, control, params) - if(usr != src && usr.is_preference_enabled(/datum/client_preference/mob_tooltips) && src.will_show_tooltip()) - openToolTip(user = usr, tip_src = src, params = params, title = get_nametag_name(usr), content = get_nametag_desc(usr)) - - ..() + if(usr != src && will_show_tooltip()) + if(usr?.read_preference(/datum/preference/toggle/mob_tooltips)) + openToolTip(usr, src, params, title = get_nametag_name(usr), content = get_nametag_desc(usr)) + . = ..() /mob/MouseDown() closeToolTip(usr) //No reason not to, really - - ..() + . = ..() /mob/MouseExited() closeToolTip(usr) //No reason not to, really - - ..() + . = ..() // Manages a global list of mobs with clients attached, indexed by z-level. /mob/proc/update_client_z(new_z) // +1 to register, null to unregister. diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index 61d9c6020d..eea78a94c7 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -409,7 +409,7 @@ var/list/intents = list(I_HELP,I_DISARM,I_GRAB,I_HURT) return // Can't talk in deadchat if you can't see it. for(var/mob/M in player_list) - if(M.client && ((!istype(M, /mob/new_player) && M.stat == DEAD) || (M.client.holder && M.client.holder.rights && M.is_preference_enabled(/datum/client_preference/holder/show_staff_dsay))) && M.is_preference_enabled(/datum/client_preference/show_dsay)) + if(M.client && ((!istype(M, /mob/new_player) && M.stat == DEAD) || (M.client.holder && M.client.holder.rights && M.client?.prefs?.read_preference(/datum/preference/toggle/holder/show_staff_dsay))) && M.client?.prefs?.read_preference(/datum/preference/toggle/show_dsay)) var/follow var/lname if(M.forbid_seeing_deadchat && !M.client.holder) @@ -439,7 +439,7 @@ var/list/intents = list(I_HELP,I_DISARM,I_GRAB,I_HURT) /proc/say_dead_object(var/message, var/obj/subject = null) for(var/mob/M in player_list) - if(M.client && ((!istype(M, /mob/new_player) && M.stat == DEAD) || (M.client.holder && M.client.holder.rights && M.is_preference_enabled(/datum/client_preference/holder/show_staff_dsay))) && M.is_preference_enabled(/datum/client_preference/show_dsay)) + if(M.client && ((!istype(M, /mob/new_player) && M.stat == DEAD) || (M.client.holder && M.client.holder.rights && M.client?.prefs?.read_preference(/datum/preference/toggle/holder/show_staff_dsay))) && M.client?.prefs?.read_preference(/datum/preference/toggle/show_dsay)) var/follow var/lname = "Game Master" if(M.forbid_seeing_deadchat && !M.client.holder) diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index 01a74aea44..37e447761e 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -100,7 +100,7 @@ if (client.prefs.lastlorenews == GLOB.news_data.newsindex) client.seen_news = 1 - if(GLOB.news_data.station_newspaper && !client.seen_news && client.is_preference_enabled(/datum/client_preference/show_lore_news)) + if(GLOB.news_data.station_newspaper && !client.seen_news && client.prefs?.read_preference(/datum/preference/toggle/show_lore_news)) show_latest_news(GLOB.news_data.station_newspaper) client.prefs.lastlorenews = GLOB.news_data.newsindex SScharacter_setup.queue_preferences_save(client.prefs) diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm index f2624a91be..f0c494266d 100644 --- a/code/modules/mob/say.dm +++ b/code/modules/mob/say.dm @@ -68,7 +68,7 @@ to_chat(src, "Deadchat is globally muted.") return - if(!is_preference_enabled(/datum/client_preference/show_dsay)) + if(!client?.prefs?.read_preference(/datum/preference/toggle/show_dsay)) to_chat(usr, "You have deadchat muted.") return diff --git a/code/modules/mob/say_vr.dm b/code/modules/mob/say_vr.dm index 00fef766c9..95d4acc8fb 100644 --- a/code/modules/mob/say_vr.dm +++ b/code/modules/mob/say_vr.dm @@ -203,14 +203,14 @@ continue if(src.client && M && !(get_z(src) == get_z(M))) message = "[message]" - if(isobserver(M) && (!M.is_preference_enabled(/datum/client_preference/ghost_see_whisubtle) || \ - !is_preference_enabled(/datum/client_preference/whisubtle_vis) && !M.client?.holder)) + if(isobserver(M) && (!M.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle) || \ + !client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) && !M.client?.holder)) spawn(0) M.show_message(undisplayed_message, 2) else spawn(0) M.show_message(message, 2) - if(M.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(M.read_preference(/datum/preference/toggle/subtle_sounds)) M << sound('sound/talksounds/subtle_sound.ogg', volume = 50) for(var/obj/O as anything in vis_objs) @@ -291,13 +291,13 @@ else pb = db.pred_body to_chat(pb, "The captive mind of \the [M] thinks, \"[message]\"") //To our pred if dominated brain - if(pb.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(pb.read_preference(/datum/preference/toggle/subtle_sounds)) pb << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE else if(M.absorbed && isbelly(M.loc)) pb = M.loc.loc to_chat(pb, "\The [M] thinks, \"[message]\"") //To our pred if absorbed - if(pb.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(pb.read_preference(/datum/preference/toggle/subtle_sounds)) pb << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE @@ -307,14 +307,14 @@ if(istype(I, /mob/living/dominated_brain) && I != M) var/mob/living/dominated_brain/db = I to_chat(db, "The captive mind of \the [M] thinks, \"[message]\"") //To any dominated brains in the pred - if(db.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(db.read_preference(/datum/preference/toggle/subtle_sounds)) db << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE for(var/B in pb.vore_organs) for(var/mob/living/L in B) if(L.absorbed && L != M && L.ckey) to_chat(L, "\The [M] thinks, \"[message]\"") //To any absorbed people in the pred - if(L.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(L.read_preference(/datum/preference/toggle/subtle_sounds)) L << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE @@ -323,32 +323,32 @@ if(istype(I, /mob/living/dominated_brain)) var/mob/living/dominated_brain/db = I to_chat(db, "\The [M] thinks, \"[message]\"") //To any dominated brains inside us - if(db.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(db.read_preference(/datum/preference/toggle/subtle_sounds)) db << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE for(var/B in M.vore_organs) for(var/mob/living/L in B) if(L.absorbed) to_chat(L, "\The [M] thinks, \"[message]\"") //To any absorbed people inside us - if(L.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(L.read_preference(/datum/preference/toggle/subtle_sounds)) L << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE if(f) //We found someone to send the message to if(pb) to_chat(M, "You think \"[message]\"") //To us if we are the prey - if(M.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(M.read_preference(/datum/preference/toggle/subtle_sounds)) M << sound('sound/talksounds/subtle_sound.ogg', volume = 50) else to_chat(M, "You think \"[message]\"") //To us if we are the pred - if(M.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(M.read_preference(/datum/preference/toggle/subtle_sounds)) M << sound('sound/talksounds/subtle_sound.ogg', volume = 50) for (var/mob/G in player_list) if (istype(G, /mob/new_player)) continue - else if(isobserver(G) && G.is_preference_enabled(/datum/client_preference/ghost_ears && \ - G.is_preference_enabled(/datum/client_preference/ghost_see_whisubtle))) - if(is_preference_enabled(/datum/client_preference/whisubtle_vis) || G.client.holder) + else if(isobserver(G) && G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears && \ + G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle))) + if(client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) || G.client.holder) to_chat(G, "\The [M] thinks, \"[message]\"") log_say(message,M) else //There wasn't anyone to send the message to, pred or prey, so let's just say it instead and correct our psay just in case. @@ -388,14 +388,14 @@ else pb = db.pred_body to_chat(pb, "\The [M] [message]") //To our pred if dominated brain - if(pb.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(pb.read_preference(/datum/preference/toggle/subtle_sounds)) pb << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE else if(M.absorbed && isbelly(M.loc)) pb = M.loc.loc to_chat(pb, "\The [M] [message]") //To our pred if absorbed - if(pb.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(pb.read_preference(/datum/preference/toggle/subtle_sounds)) pb << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE @@ -405,14 +405,14 @@ if(istype(I, /mob/living/dominated_brain) && I != M) var/mob/living/dominated_brain/db = I to_chat(db, "\The [M] [message]") //To any dominated brains in the pred - if(db.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(db.read_preference(/datum/preference/toggle/subtle_sounds)) db << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE for(var/B in pb.vore_organs) for(var/mob/living/L in B) if(L.absorbed && L != M && L.ckey) to_chat(L, "\The [M] [message]") //To any absorbed people in the pred - if(L.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(L.read_preference(/datum/preference/toggle/subtle_sounds)) L << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE @@ -421,32 +421,32 @@ if(istype(I, /mob/living/dominated_brain)) var/mob/living/dominated_brain/db = I to_chat(db, "\The [M] [message]") //To any dominated brains inside us - if(db.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(db.read_preference(/datum/preference/toggle/subtle_sounds)) db << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE for(var/B in M.vore_organs) for(var/mob/living/L in B) if(L.absorbed) to_chat(L, "\The [M] [message]") //To any absorbed people inside us - if(L.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(L.read_preference(/datum/preference/toggle/subtle_sounds)) L << sound('sound/talksounds/subtle_sound.ogg', volume = 50) f = TRUE if(f) //We found someone to send the message to if(pb) to_chat(M, "\The [M] [message]") //To us if we are the prey - if(M.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(M.read_preference(/datum/preference/toggle/subtle_sounds)) M << sound('sound/talksounds/subtle_sound.ogg', volume = 50) else to_chat(M, "\The [M] [message]") //To us if we are the pred - if(M.is_preference_enabled(/datum/client_preference/subtle_sounds)) + if(M.read_preference(/datum/preference/toggle/subtle_sounds)) M << sound('sound/talksounds/subtle_sound.ogg', volume = 50) for (var/mob/G in player_list) if (istype(G, /mob/new_player)) continue - else if(isobserver(G) && G.is_preference_enabled(/datum/client_preference/ghost_ears && \ - G.is_preference_enabled(/datum/client_preference/ghost_see_whisubtle))) - if(is_preference_enabled(/datum/client_preference/whisubtle_vis) || G.client.holder) + else if(isobserver(G) && G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears && \ + G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle))) + if(client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) || G.client.holder) to_chat(G, "\The [M] [message]") log_say(message,M) else //There wasn't anyone to send the message to, pred or prey, so let's just emote it instead and correct our psay just in case. @@ -484,7 +484,7 @@ ourfreq = voice_freq if(client) - playsound(T, pick(emote_sound), 25, TRUE, falloff = 1 , is_global = TRUE, frequency = ourfreq, ignore_walls = FALSE, preference = /datum/client_preference/emote_sounds) + playsound(T, pick(emote_sound), 25, TRUE, falloff = 1 , is_global = TRUE, frequency = ourfreq, ignore_walls = FALSE, preference = /datum/preference/toggle/emote_sounds) var/list/in_range = get_mobs_and_objs_in_view_fast(T,world.view,2,remote_ghosts = client ? TRUE : FALSE) var/list/m_viewers = in_range["mobs"] diff --git a/code/modules/mob/typing_indicator.dm b/code/modules/mob/typing_indicator.dm index ec1a0be6d6..f4a36083a4 100644 --- a/code/modules/mob/typing_indicator.dm +++ b/code/modules/mob/typing_indicator.dm @@ -15,7 +15,7 @@ set name = "Say verb" set category = "IC" - if(is_preference_enabled(/datum/client_preference/tgui_say)) + if(client?.prefs?.read_preference(/datum/preference/toggle/tgui_say)) winset(src, null, "command=[client.tgui_say_create_open_command(SAY_CHANNEL)]") return @@ -31,7 +31,7 @@ set name = "Me verb" set category = "IC" - if(is_preference_enabled(/datum/client_preference/tgui_say)) + if(client?.prefs?.read_preference(/datum/preference/toggle/tgui_say)) winset(src, null, "command=[client.tgui_say_create_open_command(ME_CHANNEL)]") return @@ -47,11 +47,11 @@ set name = "Whisper verb" set category = "IC" - if(is_preference_enabled(/datum/client_preference/tgui_say)) + if(client?.prefs?.read_preference(/datum/preference/toggle/tgui_say)) winset(src, null, "command=[client.tgui_say_create_open_command(WHIS_CHANNEL)]") return - if(is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle)) + if(client?.prefs?.read_preference(/datum/preference/toggle/show_typing_indicator_subtle)) client?.start_thinking() client?.start_typing() var/message = tgui_input_text(usr, "Type your message:", "Whisper") @@ -65,11 +65,11 @@ set category = "IC" set desc = "Emote to nearby people (and your pred/prey)" - if(is_preference_enabled(/datum/client_preference/tgui_say)) + if(client?.prefs?.read_preference(/datum/preference/toggle/tgui_say)) winset(src, null, "command=[client.tgui_say_create_open_command(SUBTLE_CHANNEL)]") return - if(is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle)) + if(client?.prefs?.read_preference(/datum/preference/toggle/show_typing_indicator_subtle)) client?.start_thinking() client?.start_typing() var/message = tgui_input_text(usr, "Type your message:", "Subtle", multiline = TRUE) diff --git a/code/modules/organs/pain.dm b/code/modules/organs/pain.dm index d26b7e3088..08b9cc620f 100644 --- a/code/modules/organs/pain.dm +++ b/code/modules/organs/pain.dm @@ -19,7 +19,7 @@ // Anti message spam checks // If multiple limbs are injured, cooldown is ignored to print all injuries until all limbs are iterated over - if(src.is_preference_enabled(/datum/client_preference/pain_frequency)) + if(client?.prefs?.read_preference(/datum/preference/toggle/pain_frequency)) switch(power) if(0 to 5) force = 0 diff --git a/code/modules/player_tips_vr/player_tips_controller_vr.dm b/code/modules/player_tips_vr/player_tips_controller_vr.dm index e23994be92..7854177564 100644 --- a/code/modules/player_tips_vr/player_tips_controller_vr.dm +++ b/code/modules/player_tips_vr/player_tips_controller_vr.dm @@ -27,7 +27,7 @@ Controlled by the player_tips subsystem under code/controllers/subsystems/player break last_tip = tip for(var/mob/M in player_list) - if(M.is_preference_enabled(/datum/client_preference/player_tips)) + if(M.client?.prefs?.read_preference(/datum/preference/toggle/player_tips)) if(!M.key && !(M.key in HasReceived)) to_chat(M, SPAN_WARNING("You have periodic player tips enabled. You may turn them off at any time with the Toggle Receiving Player Tips verb in Preferences, or in character set up under the OOC tab!\n Player tips appear every 45-75 minutes.")) HasReceived.Add(M.key) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index ac6faeb412..068878ab3c 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -229,7 +229,7 @@ PreFire(A,user,params) //They're using the new gun system, locate what they're aiming at. return - if(user && user.a_intent == I_HELP && user.is_preference_enabled(/datum/client_preference/safefiring)) //regardless of what happens, refuse to shoot if help intent is on + if(user && user.a_intent == I_HELP && user.client?.prefs?.read_preference(/datum/preference/toggle/safefiring)) //regardless of what happens, refuse to shoot if help intent is on to_chat(user, "You refrain from firing your [src] as your intent is set to help.") return diff --git a/code/modules/projectiles/targeting/targeting_triggers.dm b/code/modules/projectiles/targeting/targeting_triggers.dm index 1b8f3fcadd..d327ba8e8f 100644 --- a/code/modules/projectiles/targeting/targeting_triggers.dm +++ b/code/modules/projectiles/targeting/targeting_triggers.dm @@ -20,7 +20,7 @@ if(!owner.checkClickCooldown()) return owner.setClickCooldown(5) // Spam prevention, essentially. - if(owner.a_intent == I_HELP && owner.is_preference_enabled(/datum/client_preference/safefiring)) + if(owner.a_intent == I_HELP && owner.client?.prefs?.read_preference(/datum/preference/toggle/safefiring)) to_chat(owner, "You refrain from firing \the [aiming_with] as your intent is set to help.") return owner.visible_message("\The [owner] pulls the trigger reflexively!") diff --git a/code/modules/tables/interactions.dm b/code/modules/tables/interactions.dm index d1aae5fc71..711373cfe3 100644 --- a/code/modules/tables/interactions.dm +++ b/code/modules/tables/interactions.dm @@ -155,7 +155,7 @@ return // Placing stuff on tables - if(user.unEquip(W, 0, src.loc) && user.is_preference_enabled(/datum/client_preference/precision_placement)) + if(user.unEquip(W, 0, src.loc) && user.client?.prefs?.read_preference(/datum/preference/toggle/precision_placement)) auto_align(W, click_parameters) return 1 diff --git a/code/modules/tgui_input/say_modal/modal.dm b/code/modules/tgui_input/say_modal/modal.dm index 725f4321bf..84e9906499 100644 --- a/code/modules/tgui_input/say_modal/modal.dm +++ b/code/modules/tgui_input/say_modal/modal.dm @@ -67,7 +67,7 @@ winset(client, "tgui_say", "pos=410,400;size=360,30;is-visible=0;") window.send_message("props", list( - lightMode = client.is_preference_enabled(/datum/client_preference/tgui_say_light), + lightMode = client?.prefs?.read_preference(/datum/preference/toggle/tgui_say_light), maxLength = max_length, )) diff --git a/code/modules/vore/chat_healthbars.dm b/code/modules/vore/chat_healthbars.dm index 203df0b0b8..583435dea3 100644 --- a/code/modules/vore/chat_healthbars.dm +++ b/code/modules/vore/chat_healthbars.dm @@ -6,7 +6,7 @@ if(!reciever.client) //No one is home, don't bother return if(!override) //Did the person push the verb? Ignore the pref - if(!reciever.client.is_preference_enabled(/datum/client_preference/vore_health_bars)) + if(!reciever.client.prefs?.read_preference(/datum/preference/toggle/vore_health_bars)) return var/ourpercent = 0 diff --git a/code/modules/vore/eating/belly_obj_vr.dm b/code/modules/vore/eating/belly_obj_vr.dm index 52315192ee..61587119dd 100644 --- a/code/modules/vore/eating/belly_obj_vr.dm +++ b/code/modules/vore/eating/belly_obj_vr.dm @@ -443,7 +443,7 @@ else soundfile = fancy_vore_sounds[vore_sound] if(soundfile) - playsound(src, soundfile, vol = 100, vary = 1, falloff = VORE_SOUND_FALLOFF, preference = /datum/client_preference/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, soundfile, vol = 100, vary = 1, falloff = VORE_SOUND_FALLOFF, preference = /datum/preference/toggle/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) recent_sound = TRUE //Messages if it's a mob @@ -631,7 +631,7 @@ else soundfile = fancy_release_sounds[release_sound] if(soundfile) - playsound(src, soundfile, vol = privacy_volume, vary = 1, falloff = VORE_SOUND_FALLOFF, preference = /datum/client_preference/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, soundfile, vol = privacy_volume, vary = 1, falloff = VORE_SOUND_FALLOFF, preference = /datum/preference/toggle/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) return count @@ -717,7 +717,7 @@ else soundfile = fancy_release_sounds[release_sound] if(soundfile) - playsound(src, soundfile, vol = privacy_volume, vary = 1, falloff = VORE_SOUND_FALLOFF, preference = /datum/client_preference/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, soundfile, vol = privacy_volume, vary = 1, falloff = VORE_SOUND_FALLOFF, preference = /datum/preference/toggle/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) //Should fix your view not following you out of mobs sometimes! if(ismob(M)) var/mob/ourmob = M @@ -1376,9 +1376,9 @@ struggle_snuggle = sound(get_sfx("classic_struggle_sounds")) else struggle_snuggle = sound(get_sfx("fancy_prey_struggle")) - playsound(src, struggle_snuggle, vary = 1, vol = 75, falloff = VORE_SOUND_FALLOFF, preference = /datum/client_preference/digestion_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, struggle_snuggle, vary = 1, vol = 75, falloff = VORE_SOUND_FALLOFF, preference = /datum/preference/toggle/digestion_noises, volume_channel = VOLUME_CHANNEL_VORE) else - playsound(src, struggle_rustle, vary = 1, vol = 75, falloff = VORE_SOUND_FALLOFF, preference = /datum/client_preference/digestion_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, struggle_rustle, vary = 1, vol = 75, falloff = VORE_SOUND_FALLOFF, preference = /datum/preference/toggle/digestion_noises, volume_channel = VOLUME_CHANNEL_VORE) if(escapable) //If the stomach has escapable enabled. if(prob(escapechance)) //Let's have it check to see if the prey escapes first. @@ -1655,9 +1655,9 @@ struggle_snuggle = sound(get_sfx("classic_struggle_sounds")) else struggle_snuggle = sound(get_sfx("fancy_prey_struggle")) - playsound(src, struggle_snuggle, vary = 1, vol = 75, falloff = VORE_SOUND_FALLOFF, preference = /datum/client_preference/digestion_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, struggle_snuggle, vary = 1, vol = 75, falloff = VORE_SOUND_FALLOFF, preference = /datum/preference/toggle/digestion_noises, volume_channel = VOLUME_CHANNEL_VORE) else - playsound(src, struggle_rustle, vary = 1, vol = 75, falloff = VORE_SOUND_FALLOFF, preference = /datum/client_preference/digestion_noises, volume_channel = VOLUME_CHANNEL_VORE) + playsound(src, struggle_rustle, vary = 1, vol = 75, falloff = VORE_SOUND_FALLOFF, preference = /datum/preference/toggle/digestion_noises, volume_channel = VOLUME_CHANNEL_VORE) //absorb resists if(escapable || owner.stat) //If the stomach has escapable enabled or the owner is dead/unconscious diff --git a/code/modules/vore/eating/bellymodes_datum_vr.dm b/code/modules/vore/eating/bellymodes_datum_vr.dm index 9c49d8f93f..40c9efef5c 100644 --- a/code/modules/vore/eating/bellymodes_datum_vr.dm +++ b/code/modules/vore/eating/bellymodes_datum_vr.dm @@ -29,7 +29,7 @@ GLOBAL_LIST_INIT(digest_modes, list()) //Person just died in guts! if(L.stat == DEAD) - if(L.is_preference_enabled(/datum/client_preference/digestion_noises)) + if(L.check_sound_preference(/datum/preference/toggle/digestion_noises)) if(!B.fancy_vore) SEND_SOUND(L, sound(get_sfx("classic_death_sounds"))) else diff --git a/code/modules/vore/eating/bellymodes_vr.dm b/code/modules/vore/eating/bellymodes_vr.dm index d71fdac231..a18e6b2408 100644 --- a/code/modules/vore/eating/bellymodes_vr.dm +++ b/code/modules/vore/eating/bellymodes_vr.dm @@ -59,7 +59,7 @@ /////////////////////////// Make any noise /////////////////////////// if(digestion_noise_chance && prob(digestion_noise_chance)) for(var/mob/M in contents) - if(M && M.is_preference_enabled(/datum/client_preference/digestion_noises)) + if(M && M.check_sound_preference(/datum/preference/toggle/digestion_noises)) SEND_SOUND(M, prey_digest) play_sound = pred_digest @@ -68,7 +68,7 @@ updateVRPanels() if(play_sound) for(var/mob/M in hearers(VORE_SOUND_RANGE, get_turf(owner))) //so we don't fill the whole room with the sound effect - if(!M.is_preference_enabled(/datum/client_preference/digestion_noises)) + if(!M.check_sound_preference(/datum/preference/toggle/digestion_noises)) continue if(isturf(M.loc) || (M.loc != src)) //to avoid people on the inside getting the outside sounds and their direct sounds + built in sound pref check if(fancy_vore) @@ -95,7 +95,7 @@ if(play_sound) for(var/mob/M in hearers(VORE_SOUND_RANGE, get_turf(owner))) //so we don't fill the whole room with the sound effect - if(!M.is_preference_enabled(/datum/client_preference/digestion_noises)) + if(!M.check_sound_preference(/datum/preference/toggle/digestion_noises)) continue if(isturf(M.loc) || (M.loc != src)) //to avoid people on the inside getting the outside sounds and their direct sounds + built in sound pref check if(fancy_vore) @@ -224,7 +224,7 @@ /obj/belly/proc/prey_loop() for(var/mob/living/M in contents) //We don't bother executing any other code if the prey doesn't want to hear the noises. - if(!M.is_preference_enabled(/datum/client_preference/digestion_noises)) + if(!M.check_sound_preference(/datum/preference/toggle/digestion_noises)) M.stop_sound_channel(CHANNEL_PREYLOOP) // sanity just in case, because byond is whack and you can't trust it continue diff --git a/code/modules/vore/eating/living_vr.dm b/code/modules/vore/eating/living_vr.dm index 49afffafe9..bc36cff0d0 100644 --- a/code/modules/vore/eating/living_vr.dm +++ b/code/modules/vore/eating/living_vr.dm @@ -1187,11 +1187,10 @@ if(!user) CRASH("display_voreprefs() was called without an associated user.") var/dispvoreprefs = "[src]'s vore preferences


" - if(client && client.prefs) - if("CHAT_OOC" in client.prefs.preferences_disabled) - dispvoreprefs += "OOC DISABLED
" - if("CHAT_LOOC" in client.prefs.preferences_disabled) - dispvoreprefs += "LOOC DISABLED
" + if(!client?.prefs?.read_preference(/datum/preference/toggle/show_ooc)) + dispvoreprefs += "OOC DISABLED
" + if(!client?.prefs?.read_preference(/datum/preference/toggle/show_looc)) + dispvoreprefs += "LOOC DISABLED
" dispvoreprefs += "Digestable: [digestable ? "Enabled" : "Disabled"]
" dispvoreprefs += "Devourable: [devourable ? "Enabled" : "Disabled"]
" dispvoreprefs += "Feedable: [feeding ? "Enabled" : "Disabled"]
" diff --git a/code/modules/vore/eating/vorepanel_vr.dm b/code/modules/vore/eating/vorepanel_vr.dm index 6c6176e838..20332e2dc1 100644 --- a/code/modules/vore/eating/vorepanel_vr.dm +++ b/code/modules/vore/eating/vorepanel_vr.dm @@ -923,7 +923,7 @@ var/global/list/belly_colorable_only_fullscreens = list("a_synth_flesh_mono", l.adjust_nutrition(thismuch) ourtarget.death() // To make sure all on-death procs get properly called if(ourtarget) - if(ourtarget.is_preference_enabled(/datum/client_preference/digestion_noises)) + if(ourtarget.check_sound_preference(/datum/preference/toggle/digestion_noises)) if(!b.fancy_vore) SEND_SOUND(ourtarget, sound(get_sfx("classic_death_sounds"))) else diff --git a/interface/interface.dm b/interface/interface.dm index f2e57398e5..120f6614de 100644 --- a/interface/interface.dm +++ b/interface/interface.dm @@ -223,7 +223,7 @@ Any-Mode: (hotkey doesn't need to be on) /client/proc/set_hotkeys_macro(macro_name = "macro", hotkey_macro_name = "hotkeymode", hotkeys_enabled = null) // If hotkeys mode was not specified, fall back to choice of default in client preferences. if(isnull(hotkeys_enabled)) - hotkeys_enabled = is_preference_enabled(/datum/client_preference/hotkeys_default) + hotkeys_enabled = prefs?.read_preference(/datum/preference/toggle/hotkeys_default) if(hotkeys_enabled) winset(src, null, "mainwindow.macro=[hotkey_macro_name] hotkey_toggle.is-checked=true mapwindow.map.focus=true") diff --git a/interface/skin.dmf b/interface/skin.dmf index a32906eb75..7d732dd0f2 100644 --- a/interface/skin.dmf +++ b/interface/skin.dmf @@ -1149,6 +1149,20 @@ menu "menu" command = "hotkeys-help" category = "&Help" saved-params = "is-checked" + elem + name = "&Preferences" + command = "" + saved-params = "is-checked" + elem + name = "&Character Setup" + command = "character-setup" + category = "&Preferences" + saved-params = "is-checked" + elem + name = "&Game Options" + command = "game-options" + category = "&Preferences" + saved-params = "is-checked" window "mainwindow" elem "mainwindow" diff --git a/prof.dll b/prof.dll new file mode 100644 index 0000000000000000000000000000000000000000..4721a5c18747cae535f09c8c0739faf0531260f2 GIT binary patch literal 107520 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 { + const { act, data } = useBackend(); + + const [currentPage, setCurrentPage] = useState( + props.startingPage ?? GamePreferencesSelectedPage.Settings, + ); + + let pageContents; + + switch (currentPage) { + // case GamePreferencesSelectedPage.Keybindings: + // pageContents = ; + // break; + case GamePreferencesSelectedPage.Settings: + pageContents = ; + break; + // default: + // exhaustiveCheck(currentPage); + } + + return ( + + + + {/* + + + + Settings + + + + + + Keybindings + + + + */} + + {/* */} + + + {pageContents} + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx new file mode 100644 index 0000000000..35f279b428 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx @@ -0,0 +1,149 @@ +import { binaryInsertWith, sortBy } from 'common/collections'; +import { ReactNode, useState } from 'react'; + +import { useBackend } from '../../backend'; +import { Box, Button, Flex, Input, Section, Tooltip } from '../../components'; +import { PreferencesMenuData } from './data'; +import features from './preferences/features'; +import { FeatureValueInput } from './preferences/features/base'; +import { TabbedMenu } from './TabbedMenu'; + +type PreferenceChild = { + name: string; + children: ReactNode; +}; + +const binaryInsertPreference = ( + collection: PreferenceChild[], + value: PreferenceChild, +) => binaryInsertWith(collection, value, (child) => child.name); + +const sortByName = (array: [string, PreferenceChild[]][]) => + sortBy(array, ([name]) => name); + +export const GamePreferencesPage = (props) => { + const { act, data } = useBackend(); + + const gamePreferences: Record = {}; + + for (const [featureId, value] of Object.entries( + data.character_preferences.game_preferences, + )) { + const feature = features[featureId]; + + let nameInner: ReactNode = feature?.name || featureId; + + if (feature?.description) { + nameInner = ( + + {nameInner} + + ); + } + + let name: ReactNode = ( + + {nameInner} + + ); + + if (feature?.description) { + name = ( + + {name} + + ); + } + + const child = ( + + {name} + + + {(feature && ( + + )) || ( + + ...is not filled out properly!!! + + )} + + + ); + + const entry = { + name: feature?.name || featureId, + children: child, + }; + + const category = feature?.category || 'ERROR'; + + gamePreferences[category] = binaryInsertPreference( + gamePreferences[category] || [], + entry, + ); + } + + const [search, setSearch] = useState(''); + const [searchVisible, setSearchVisible] = useState(false); + + // For some reason, typescript thinks that this call to filter() can change the shape of the array + const gamePreferenceEntries: any = sortByName(Object.entries(gamePreferences)) + .map(([category, preferences]) => { + return [ + category, + preferences + .filter( + (entry) => + !search || + entry.name.toLowerCase().includes(search.toLowerCase()), + ) + .map((entry) => entry.children), + ]; + }) + .filter(([category, prefs]) => prefs.length !== 0); + + return ( + <> + {!gamePreferenceEntries.length && ( +

No results found.
+ )} + + {searchVisible && ( + setSearch(val)} + onChange={(e, val) => setSearch(val)} + /> + )} + + ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx new file mode 100644 index 0000000000..0b90fd855c --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/ServerPreferencesFetcher.tsx @@ -0,0 +1,43 @@ +import { Component, ReactNode } from 'react'; + +import { resolveAsset } from '../../assets'; +import { fetchRetry } from '../../http'; +import { ServerData } from './data'; + +// Cache response so it's only sent once +let fetchServerData: Promise | undefined; + +export class ServerPreferencesFetcher extends Component< + { + render: (serverData: ServerData | undefined) => ReactNode; + }, + { + serverData?: ServerData; + } +> { + state = { + serverData: undefined, + }; + + componentDidMount() { + this.populateServerData(); + } + + async populateServerData() { + if (!fetchServerData) { + fetchServerData = fetchRetry(resolveAsset('preferences.json')).then( + (response) => response.json(), + ); + } + + const preferencesData: ServerData = await fetchServerData; + + this.setState({ + serverData: preferencesData, + }); + } + + render() { + return this.props?.render?.(this.state.serverData); + } +} diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/TabbedMenu.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/TabbedMenu.tsx new file mode 100644 index 0000000000..13c572ebc8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/TabbedMenu.tsx @@ -0,0 +1,90 @@ +import { Component, createRef, ReactNode, RefObject } from 'react'; + +import { Button, Section, Stack } from '../../components'; +import { FlexProps } from '../../components/Flex'; + +type TabbedMenuProps = { + categoryEntries: [string, ReactNode][]; + contentProps?: FlexProps; +}; + +export class TabbedMenu extends Component { + categoryRefs: Record> = {}; + sectionRef: RefObject = createRef(); + + getCategoryRef(category: string): RefObject { + if (!this.categoryRefs[category]) { + this.categoryRefs[category] = createRef(); + } + + return this.categoryRefs[category]; + } + + render() { + return ( + + + + {this.props.categoryEntries.map(([category]) => { + return ( + + + + ); + })} + + + + + + {this.props.categoryEntries.map(([category, children]) => { + return ( + +
+ {children} +
+
+ ); + })} +
+
+
+ ); + } +} diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/abductor.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/abductor.ts new file mode 100644 index 0000000000..789e5e9823 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/abductor.ts @@ -0,0 +1,23 @@ +import { Antagonist, Category } from '../base'; + +const Abductor: Antagonist = { + key: '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. + `, + + ` + You and a partner will become the abductor scientist and agent duo. + As an agent, abduct unassuming victims and bring them back to your UFO. + As a scientist, scout out victims for your agent, keep them safe, and + operate on whoever they bring back. + `, + ], + category: Category.Midround, +}; + +export default Abductor; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blob.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blob.ts new file mode 100644 index 0000000000..92cd5bdc44 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blob.ts @@ -0,0 +1,17 @@ +import { Antagonist, Category } from '../base'; + +export const BLOB_MECHANICAL_DESCRIPTION = ` + The blob infests the station and destroys everything in its path, including + hull, fixtures, and creatures. Spread your mass, collect resources, and + consume the entire station. Make sure to prepare your defenses, because the + crew will be alerted to your presence! +`; + +const Blob: Antagonist = { + key: 'blob', + name: 'Blob', + description: [BLOB_MECHANICAL_DESCRIPTION], + category: Category.Midround, +}; + +export default Blob; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blobinfection.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blobinfection.ts new file mode 100644 index 0000000000..d8a5f135fe --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/blobinfection.ts @@ -0,0 +1,17 @@ +import { Antagonist, Category } from '../base'; +import { BLOB_MECHANICAL_DESCRIPTION } from './blob'; + +const BlobInfection: Antagonist = { + key: 'blobinfection', + name: 'Blob Infection', + description: [ + ` + At any point in the middle of the shift, be strucken with an infection + that will turn you into the terrifying blob. + `, + BLOB_MECHANICAL_DESCRIPTION, + ], + category: Category.Midround, +}; + +export default BlobInfection; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodbrother.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodbrother.ts new file mode 100644 index 0000000000..56cbcbfc97 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodbrother.ts @@ -0,0 +1,16 @@ +import { Antagonist, Category } from '../base'; + +const BloodBrother: Antagonist = { + key: 'bloodbrother', + 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. + `, + ], + category: Category.Roundstart, +}; + +export default BloodBrother; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changeling.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changeling.ts new file mode 100644 index 0000000000..9124a07063 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changeling.ts @@ -0,0 +1,21 @@ +import { Antagonist, Category } from '../base'; + +export const CHANGELING_MECHANICAL_DESCRIPTION = ` +Transform yourself or others into different identities, and buy from an +arsenal of biological weaponry with the DNA you collect. +`; + +const Changeling: Antagonist = { + key: 'changeling', + name: 'Changeling', + description: [ + ` + A highly intelligent alien predator that is capable of altering their + shape to flawlessly resemble a human. + `, + CHANGELING_MECHANICAL_DESCRIPTION, + ], + category: Category.Roundstart, +}; + +export default Changeling; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changelingmidround.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changelingmidround.ts new file mode 100644 index 0000000000..8324bdb858 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/changelingmidround.ts @@ -0,0 +1,17 @@ +import { Antagonist, Category } from '../base'; +import { CHANGELING_MECHANICAL_DESCRIPTION } from './changeling'; + +const ChangelingMidround: Antagonist = { + key: 'changelingmidround', + name: 'Space Changeling', + description: [ + ` + A midround changeling does not receive a crew identity, instead arriving + from space. This will be more difficult than being a round-start changeling! + `, + CHANGELING_MECHANICAL_DESCRIPTION, + ], + category: Category.Midround, +}; + +export default ChangelingMidround; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/clownoperative.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/clownoperative.ts new file mode 100644 index 0000000000..61f874a2e9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/clownoperative.ts @@ -0,0 +1,20 @@ +import { Antagonist, Category } from '../base'; +import { OPERATIVE_MECHANICAL_DESCRIPTION } from './operative'; + +const ClownOperative: Antagonist = { + key: 'clownoperative', + name: 'Clown Operative', + description: [ + ` + Honk! You have been chosen, for better or worse to join the Syndicate + Clown Operative strike team. Your mission, whether or not you choose + to tickle it, is to honk Nanotrasen's most advanced research facility! + That's right, you're going to Clown Station 13. + `, + + OPERATIVE_MECHANICAL_DESCRIPTION, + ], + category: Category.Roundstart, +}; + +export default ClownOperative; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/cultist.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/cultist.ts new file mode 100644 index 0000000000..c5cfaf61c2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/cultist.ts @@ -0,0 +1,22 @@ +import { Antagonist, Category } from '../base'; + +const Cultist: Antagonist = { + key: 'cultist', + name: '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! + `, + + ` + Armed with blood magic, convert crew members to the Blood Cult, sacrifice + those who get in the way, and summon Nar-Sie. + `, + ], + category: Category.Roundstart, +}; + +export default Cultist; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/fugitive.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/fugitive.ts new file mode 100644 index 0000000000..f05a96c2ec --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/fugitive.ts @@ -0,0 +1,15 @@ +import { Antagonist, Category } from '../base'; + +const Fugitive: Antagonist = { + key: 'fugitive', + name: 'Fugitive', + description: [ + ` + Wherever you come from, you're being hunted. You have 10 minutes to prepare + before fugitive hunters arrive and start hunting you and your friends down! + `, + ], + category: Category.Midround, +}; + +export default Fugitive; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/glitch.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/glitch.ts new file mode 100644 index 0000000000..3d269c54ca --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/glitch.ts @@ -0,0 +1,19 @@ +import { Antagonist, Category } from '../base'; + +const Glitch: Antagonist = { + key: 'glitch', + name: 'Glitch', + description: [ + ` + The virtual domain is a dangerous place for bitrunners. Make it so. + `, + + ` + You are a short-term antagonist, a glitch in the system. Use martial arts \ + and lethal weaponry to terminate organics. + `, + ], + category: Category.Midround, +}; + +export default Glitch; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/headrevolutionary.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/headrevolutionary.ts new file mode 100644 index 0000000000..0a1d644266 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/headrevolutionary.ts @@ -0,0 +1,15 @@ +import { Antagonist, Category } from '../base'; + +export const REVOLUTIONARY_MECHANICAL_DESCRIPTION = ` + Armed with a flash, convert as many people to the revolution as you can. + Kill or exile all heads of staff on the station. + `; + +const HeadRevolutionary: Antagonist = { + key: 'headrevolutionary', + name: 'Head Revolutionary', + description: ['VIVA LA REVOLUTION!', REVOLUTIONARY_MECHANICAL_DESCRIPTION], + category: Category.Roundstart, +}; + +export default HeadRevolutionary; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/heretic.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/heretic.ts new file mode 100644 index 0000000000..8c2d631bd5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/heretic.ts @@ -0,0 +1,22 @@ +import { Antagonist, Category } from '../base'; + +export const HERETIC_MECHANICAL_DESCRIPTION = ` + Find hidden influences and sacrifice crew members to gain magical + powers and ascend as one of several paths. + `; + +const Heretic: Antagonist = { + key: 'heretic', + name: 'Heretic', + description: [ + ` + Forgotten, devoured, gutted. Humanity has forgotten the eldritch forces + of decay, but the mansus veil has weakened. We will make them taste fear + again... + `, + HERETIC_MECHANICAL_DESCRIPTION, + ], + category: Category.Roundstart, +}; + +export default Heretic; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/hereticsmuggler.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/hereticsmuggler.ts new file mode 100644 index 0000000000..6ce90a3552 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/hereticsmuggler.ts @@ -0,0 +1,14 @@ +import { Antagonist, Category } from '../base'; +import { HERETIC_MECHANICAL_DESCRIPTION } from './heretic'; + +const HereticSmuggler: Antagonist = { + key: 'hereticsmuggler', + name: 'Heretic Smuggler', + description: [ + 'A form of heretic that can activate when joining an ongoing shift.', + HERETIC_MECHANICAL_DESCRIPTION, + ], + category: Category.Latejoin, +}; + +export default HereticSmuggler; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/loneoperative.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/loneoperative.ts new file mode 100644 index 0000000000..3f8ac5c5b7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/loneoperative.ts @@ -0,0 +1,18 @@ +import { Antagonist, Category } from '../base'; +import { OPERATIVE_MECHANICAL_DESCRIPTION } from './operative'; + +const LoneOperative: Antagonist = { + key: 'loneoperative', + name: 'Lone Operative', + description: [ + ` + A solo nuclear operative that has a higher chance of spawning the longer + the nuclear authentication disk stays in one place. + `, + + OPERATIVE_MECHANICAL_DESCRIPTION, + ], + category: Category.Midround, +}; + +export default LoneOperative; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfai.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfai.ts new file mode 100644 index 0000000000..03aadd752c --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfai.ts @@ -0,0 +1,16 @@ +import { Antagonist, Category } from '../base'; + +export const MALF_AI_MECHANICAL_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. + `; + +const MalfAI: Antagonist = { + key: 'malfai', + name: 'Malfunctioning AI', + description: [MALF_AI_MECHANICAL_DESCRIPTION], + category: Category.Roundstart, +}; + +export default MalfAI; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfaimidround.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfaimidround.ts new file mode 100644 index 0000000000..84e52b43dc --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/malfaimidround.ts @@ -0,0 +1,17 @@ +import { Antagonist, Category } from '../base'; +import { MALF_AI_MECHANICAL_DESCRIPTION } from './malfai'; + +const MalfAIMidround: Antagonist = { + key: 'malfaimidround', + name: 'Value Drifted AI', + description: [ + ` + A form of malfunctioning AI that is given to existing AIs in the middle + of the shift. + `, + MALF_AI_MECHANICAL_DESCRIPTION, + ], + category: Category.Midround, +}; + +export default MalfAIMidround; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/nightmare.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/nightmare.ts new file mode 100644 index 0000000000..90046dd625 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/nightmare.ts @@ -0,0 +1,15 @@ +import { Antagonist, Category } from '../base'; + +const Nightmare: Antagonist = { + key: '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. + `, + ], + category: Category.Midround, +}; + +export default Nightmare; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/obsessed.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/obsessed.ts new file mode 100644 index 0000000000..8d4c981398 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/obsessed.ts @@ -0,0 +1,16 @@ +import { Antagonist, Category } from '../base'; + +const Obsessed: Antagonist = { + key: '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? + `, + ], + category: Category.Midround, +}; + +export default Obsessed; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operative.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operative.ts new file mode 100644 index 0000000000..a8182de9e0 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operative.ts @@ -0,0 +1,24 @@ +import { Antagonist, Category } from '../base'; + +export const OPERATIVE_MECHANICAL_DESCRIPTION = ` + Retrieve the nuclear authentication disk, use it to activate the nuclear + fission explosive, and destroy the station. +`; + +const Operative: Antagonist = { + key: 'operative', + name: 'Nuclear Operative', + description: [ + ` + Congratulations, agent. You have been chosen to join the Syndicate + Nuclear Operative strike team. Your mission, whether or not you choose + to accept it, is to destroy Nanotrasen's most advanced research facility! + That's right, you're going to Space Station 13. + `, + + OPERATIVE_MECHANICAL_DESCRIPTION, + ], + category: Category.Roundstart, +}; + +export default Operative; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operativemidround.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operativemidround.ts new file mode 100644 index 0000000000..7497c6442f --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/operativemidround.ts @@ -0,0 +1,17 @@ +import { Antagonist, Category } from '../base'; +import { OPERATIVE_MECHANICAL_DESCRIPTION } from './operative'; + +const OperativeMidround: Antagonist = { + key: 'operativemidround', + name: 'Nuclear Assailant', + description: [ + ` + A form of nuclear operative that is offered to ghosts in the middle + of the shift. + `, + OPERATIVE_MECHANICAL_DESCRIPTION, + ], + category: Category.Midround, +}; + +export default OperativeMidround; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/paradoxclone.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/paradoxclone.ts new file mode 100644 index 0000000000..1456df3ec6 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/paradoxclone.ts @@ -0,0 +1,15 @@ +import { Antagonist, Category } from '../base'; + +const ParadoxClone: Antagonist = { + key: 'paradoxclone', + name: 'Paradox Clone', + description: [ + ` + A freak time-space anomaly has teleported you into another reality! + Now you have to find your counterpart and kill and replace them. + `, + ], + category: Category.Midround, +}; + +export default ParadoxClone; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/provocateur.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/provocateur.ts new file mode 100644 index 0000000000..1d2879c867 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/provocateur.ts @@ -0,0 +1,18 @@ +import { Antagonist, Category } from '../base'; +import { REVOLUTIONARY_MECHANICAL_DESCRIPTION } from './headrevolutionary'; + +const Provocateur: Antagonist = { + key: 'provocateur', + name: 'Provocateur', + description: [ + ` + A form of head revolutionary that can activate when joining an ongoing + shift. + `, + + REVOLUTIONARY_MECHANICAL_DESCRIPTION, + ], + category: Category.Latejoin, +}; + +export default Provocateur; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/revenant.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/revenant.ts new file mode 100644 index 0000000000..241b52b3e4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/revenant.ts @@ -0,0 +1,16 @@ +import { Antagonist, Category } from '../base'; + +const Revenant: Antagonist = { + key: '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. + `, + ], + category: Category.Midround, +}; + +export default Revenant; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/sentiencepotionspawn.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/sentiencepotionspawn.ts new file mode 100644 index 0000000000..e69db3bec3 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/sentiencepotionspawn.ts @@ -0,0 +1,22 @@ +import { Antagonist, Category } from '../base'; + +const SentientCreature: Antagonist = { + key: 'sentiencepotionspawn', + name: 'Sentient Creature', + description: [ + ` + Either by cosmic happenstance, or due to crew's shenanigans, you have been + given sentience! + `, + + ` + This is a blanket preference. The more benign ones include random human + level intelligence events, the cargorilla, and creatures uplifted via sentience + potions. The less friendly ones include the regal rat, and the boosted + mining elite mobs. + `, + ], + category: Category.Midround, +}; + +export default SentientCreature; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spacedragon.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spacedragon.ts new file mode 100644 index 0000000000..4a11cc94d1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spacedragon.ts @@ -0,0 +1,15 @@ +import { Antagonist, Category } from '../base'; + +const SpaceDragon: Antagonist = { + key: 'spacedragon', + name: 'Space Dragon', + description: [ + ` + Become a ferocious space dragon. Breathe fire, summon an army of space + carps, crush walls, and terrorize the station. + `, + ], + category: Category.Midround, +}; + +export default SpaceDragon; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spaceninja.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spaceninja.ts new file mode 100644 index 0000000000..e6db1b96f9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spaceninja.ts @@ -0,0 +1,23 @@ +import { Antagonist, Category } from '../base'; + +const SpaceNinja: Antagonist = { + key: 'spaceninja', + name: 'Space Ninja', + description: [ + ` + The Spider Clan practice a sort of augmentation of human flesh in order to + achieve a more perfect state of being and follow Postmodern Space Bushido. + `, + + ` + Become a conniving space ninja, equipped with a katana, gloves to hack + into airlocks and APCs, a suit to make you go near-invisible, + as well as a variety of abilities in your kit. Hack into arrest consoles + to mark everyone as arrest, and even hack into communication consoles to + summon more threats to cause chaos on the station! + `, + ], + category: Category.Midround, +}; + +export default SpaceNinja; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts new file mode 100644 index 0000000000..b004b972d4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts @@ -0,0 +1,22 @@ +import { Antagonist, Category } from '../base'; + +const Spy: Antagonist = { + key: 'spy', + name: 'Spy', + description: [ + ` + Your mission, should you choose to accept it: Infiltrate Space Station 13. + Disguise yourself as a member of their crew and steal vital equipment. + Should you be caught or killed, your employer will disavow any knowledge + of your actions. Good luck agent. + `, + + ` + Complete Spy Bounties to earn rewards from your employer. + Use these rewards to sow chaos and mischief! + `, + ], + category: Category.Roundstart, +}; + +export default Spy; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/stowawaychangeling.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/stowawaychangeling.ts new file mode 100644 index 0000000000..20ba22ee07 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/stowawaychangeling.ts @@ -0,0 +1,17 @@ +import { Antagonist, Category } from '../base'; +import { CHANGELING_MECHANICAL_DESCRIPTION } from './changeling'; + +const Stowaway_Changeling: Antagonist = { + key: 'stowawaychangeling', + name: 'Stowaway Changeling', + description: [ + ` + A Changeling that found its way onto the shuttle + unbeknownst to the crewmembers on board. + `, + CHANGELING_MECHANICAL_DESCRIPTION, + ], + category: Category.Latejoin, +}; + +export default Stowaway_Changeling; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicateinfiltrator.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicateinfiltrator.ts new file mode 100644 index 0000000000..45ad29292b --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicateinfiltrator.ts @@ -0,0 +1,15 @@ +import { Antagonist, Category } from '../base'; +import { TRAITOR_MECHANICAL_DESCRIPTION } from './traitor'; + +const SyndicateInfiltrator: Antagonist = { + key: 'syndicateinfiltrator', + name: 'Syndicate Infiltrator', + description: [ + 'A form of traitor that can activate when joining an ongoing shift.', + TRAITOR_MECHANICAL_DESCRIPTION, + ], + category: Category.Latejoin, + priority: -1, +}; + +export default SyndicateInfiltrator; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicatesleeperagent.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicatesleeperagent.ts new file mode 100644 index 0000000000..4a680e5d34 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/syndicatesleeperagent.ts @@ -0,0 +1,18 @@ +import { Antagonist, Category } from '../base'; +import { TRAITOR_MECHANICAL_DESCRIPTION } from './traitor'; + +const SyndicateSleeperAgent: Antagonist = { + key: 'syndicatesleeperagent', + name: 'Syndicate Sleeper Agent', + description: [ + ` + A form of traitor that can activate at any point in the middle + of the shift. + `, + TRAITOR_MECHANICAL_DESCRIPTION, + ], + category: Category.Midround, + priority: -1, +}; + +export default SyndicateSleeperAgent; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/traitor.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/traitor.ts new file mode 100644 index 0000000000..ccb6391998 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/traitor.ts @@ -0,0 +1,23 @@ +import { Antagonist, Category } from '../base'; + +export const TRAITOR_MECHANICAL_DESCRIPTION = ` + Start with an uplink to purchase your gear and take on your sinister + objectives. Ascend through the ranks and become an infamous legend. + `; + +const Traitor: Antagonist = { + key: 'traitor', + name: 'Traitor', + description: [ + ` + An unpaid debt. A score to be settled. Maybe you were just in the wrong + place at the wrong time. Whatever the reasons, you were selected to + infiltrate Space Station 13. + `, + TRAITOR_MECHANICAL_DESCRIPTION, + ], + category: Category.Roundstart, + priority: -1, +}; + +export default Traitor; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizard.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizard.ts new file mode 100644 index 0000000000..62530a26f6 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizard.ts @@ -0,0 +1,18 @@ +import { Antagonist, Category } from '../base'; + +export const WIZARD_MECHANICAL_DESCRIPTION = ` + Choose between a variety of powerful spells in order to cause chaos + among Space Station 13. + `; + +const Wizard: Antagonist = { + key: 'wizard', + name: 'Wizard', + description: [ + `"GREETINGS. WE'RE THE WIZARDS OF THE WIZARD'S FEDERATION."`, + WIZARD_MECHANICAL_DESCRIPTION, + ], + category: Category.Roundstart, +}; + +export default Wizard; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizardmidround.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizardmidround.ts new file mode 100644 index 0000000000..b36a9f5170 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/wizardmidround.ts @@ -0,0 +1,14 @@ +import { Antagonist, Category } from '../base'; +import { WIZARD_MECHANICAL_DESCRIPTION } from './wizard'; + +const WizardMidround: Antagonist = { + key: 'wizardmidround', + name: 'Wizard (Midround)', + description: [ + 'A form of wizard that is offered to ghosts in the middle of the shift.', + WIZARD_MECHANICAL_DESCRIPTION, + ], + category: Category.Midround, +}; + +export default WizardMidround; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/xenomorph.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/xenomorph.ts new file mode 100644 index 0000000000..a9a1960f3b --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/xenomorph.ts @@ -0,0 +1,15 @@ +import { Antagonist, Category } from '../base'; + +const Xenomorph: Antagonist = { + key: 'xenomorph', + name: 'Xenomorph', + description: [ + ` + Become the extraterrestrial xenomorph. Start as a larva, and progress + your way up the caste, including even the Queen! + `, + ], + category: Category.Midround, +}; + +export default Xenomorph; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/base.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/base.ts new file mode 100644 index 0000000000..a91b6af878 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/base.ts @@ -0,0 +1,35 @@ +/** + * This folder represents the antagonists you can choose in the preferences + * menu. + * + * Every file in this folder represents one antagonist. + * + * For example "Syndicate Sleeper Agent" -> syndicatesleeperagent.ts + * + * "Antagonist" in this context actually means ruleset. + * This is an important distinction--it means that players can choose to be + * a roundstart traitor, but not a latejoin traitor. + * + * Icons are generated from the antag datums themselves, provided by the + * `antag_datum` variable on the /datum/dynamic_ruleset. + * + * The icon used is whatever the return value of get_preview_icon() is. + * Most antagonists, unless they want an especially cool effect, can simply + * set preview_outfit to some typepath representing their character. + */ + +export type Antagonist = { + // the antag_flag, made lowercase, and with non-alphanumerics removed. + key: string; + + name: string; + description: string[]; + category: Category; + priority?: number; +}; + +export enum Category { + Roundstart, + Midround, + Latejoin, +} diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts new file mode 100644 index 0000000000..ff8d1b112a --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/data.ts @@ -0,0 +1,36 @@ +import { sendAct } from '../../backend'; + +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_profiles: (string | null)[]; + + character_preferences: { + game_preferences: Record; + }; + + active_slot: number; + + window: Window; +}; + +export type ServerData = { + [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 0000000000..e64e62f8ff --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/index.tsx @@ -0,0 +1,31 @@ +// import { exhaustiveCheck } from 'common/exhaustive'; + +import { useBackend } from '../../backend'; +// import { CharacterPreferenceWindow } from './CharacterPreferenceWindow'; +import { + GamePreferencesSelectedPage, + PreferencesMenuData, + Window, +} from './data'; +import { GamePreferenceWindow } from './GamePreferenceWindow'; + +export const PreferencesMenu = (props) => { + const { data } = useBackend(); + + 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/preferences/features/base.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx new file mode 100644 index 0000000000..452eb67751 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx @@ -0,0 +1,267 @@ +import { sortBy } from 'common/collections'; +import { BooleanLike } from 'common/react'; +import { + ComponentType, + createElement, + ReactNode, + useEffect, + useState, +} from 'react'; + +import { sendAct, useBackend } from '../../../../backend'; +import { + Box, + Button, + Dropdown, + Input, + NumberInput, + Slider, + Stack, +} from '../../../../components'; +import { createSetPreference, PreferencesMenuData } from '../../data'; +import { ServerPreferencesFetcher } from '../../ServerPreferencesFetcher'; + +export const sortChoices = (array: [string, ReactNode][]) => + sortBy(array, ([name]) => name); + +export type Feature< + TReceiving, + TSending = TReceiving, + TServerData = undefined, +> = { + name: string; + component: FeatureValue; + category?: string; + description?: string; +}; + +/** + * Represents a preference. + * TReceiving = The type you will be receiving + * TSending = The type you will be sending + * TServerData = The data the server sends through preferences.json + */ +type FeatureValue< + TReceiving, + TSending = TReceiving, + TServerData = undefined, +> = ComponentType>; + +export type FeatureValueProps< + TReceiving, + TSending = TReceiving, + TServerData = undefined, +> = Readonly<{ + 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 CheckboxInput = ( + props: FeatureValueProps, +) => { + return ( + { + props.handleSetValue(!props.value); + }} + /> + ); +}; + +export const CheckboxInputInverse = ( + props: FeatureValueProps, +) => { + return ( + { + props.handleSetValue(!props.value); + }} + /> + ); +}; + +export function createDropdownInput( + // Map of value to display texts + choices: Record, + dropdownProps?: Record, +): FeatureValue { + return (props: FeatureValueProps) => { + return ( + { + return { + displayText: label, + value: dataValue, + }; + }, + )} + {...dropdownProps} + /> + ); + }; +} + +export type FeatureChoicedServerData = { + choices: string[]; + display_names?: Record; + icons?: Record; +}; + +export type FeatureChoiced = Feature; + +export type FeatureNumericData = { + minimum: number; + maximum: number; + step: number; +}; + +export type FeatureNumeric = Feature; + +export const FeatureNumberInput = ( + props: FeatureValueProps, +) => { + if (!props.serverData) { + return Loading...; + } + + return ( + { + props.handleSetValue(value); + }} + minValue={props.serverData.minimum} + maxValue={props.serverData.maximum} + step={props.serverData.step} + value={props.value} + /> + ); +}; + +export const FeatureSliderInput = ( + props: FeatureValueProps, +) => { + if (!props.serverData) { + return Loading...; + } + + return ( + { + props.handleSetValue(value); + }} + minValue={props.serverData.minimum} + maxValue={props.serverData.maximum} + step={props.serverData.step} + value={props.value} + stepPixelSize={10} + /> + ); +}; + +export const FeatureValueInput = (props: { + feature: Feature; + featureId: string; + shrink?: boolean; + value: unknown; + + act: typeof sendAct; +}) => { + const { data } = useBackend(); + + const feature = props.feature; + + const [predictedValue, setPredictedValue] = useState(props.value); + + const changeValue = (newValue: unknown) => { + setPredictedValue(newValue); + createSetPreference(props.act, props.featureId)(newValue); + }; + + useEffect(() => { + setPredictedValue(props.value); + }, [data.active_slot, props.value]); + + return ( + { + return createElement(feature.component, { + act: props.act, + featureId: props.featureId, + serverData: serverData?.[props.featureId] as any, + shrink: props.shrink, + + handleSetValue: changeValue, + value: predictedValue, + }); + }} + /> + ); +}; + +export type FeatureShortTextData = { + maximum_length: number; +}; + +export const FeatureShortTextInput = ( + props: FeatureValueProps, +) => { + if (!props.serverData) { + return Loading...; + } + + return ( + props.handleSetValue(value)} + /> + ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dropdowns.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dropdowns.tsx new file mode 100644 index 0000000000..32e1161e63 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dropdowns.tsx @@ -0,0 +1,113 @@ +import { classes } from 'common/react'; +import { capitalizeFirst } from 'common/string'; +import { ReactNode } from 'react'; + +import { Box, Dropdown, Stack } from '../../../../components'; +import { Feature, FeatureChoicedServerData, FeatureValueProps } from './base'; + +type DropdownInputProps = FeatureValueProps< + string, + string, + FeatureChoicedServerData +> & + Partial<{ + disabled: boolean; + buttons: boolean; + }>; + +type IconnedDropdownInputProps = FeatureValueProps< + string, + string, + FeatureChoicedServerData +>; + +export type FeatureWithIcons = Feature; + +export function FeatureDropdownInput(props: DropdownInputProps) { + const { serverData, disabled, buttons, handleSetValue, value } = props; + + if (!serverData) { + return null; + } + + const { choices, display_names } = serverData; + + const dropdownOptions = choices.map((choice) => { + let displayText: ReactNode = display_names + ? display_names[choice] + : capitalizeFirst(choice); + + return { + displayText, + value: choice, + }; + }); + + let display_text = value; + if (display_names) { + display_text = display_names[value]; + } + + return ( + + ); +} + +export function FeatureIconnedDropdownInput(props: IconnedDropdownInputProps) { + const { serverData, handleSetValue, value } = props; + + if (!serverData) { + return null; + } + + const { choices, display_names, icons } = serverData; + + const dropdownOptions = choices.map((choice) => { + let displayText: ReactNode = display_names + ? display_names[choice] + : capitalizeFirst(choice); + + if (icons?.[choice]) { + displayText = ( + + + + + {displayText} + + ); + } + + return { + displayText, + value: choice, + }; + }); + + let display_text = value; + if (display_names) { + display_text = display_names[value]; + } + + return ( + + ); +} diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx new file mode 100644 index 0000000000..f9f8fec97b --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx @@ -0,0 +1,50 @@ +import { CheckboxInput, FeatureToggle } from '../base'; + +export const CHAT_ATTACKLOGS: FeatureToggle = { + name: 'Attack Log Messages', + category: 'ADMIN', + description: 'Show attack logs.', + component: CheckboxInput, +}; + +export const CHAT_DEBUGLOGS: FeatureToggle = { + name: 'Debug Logs', + category: 'ADMIN', + description: 'Show debug logs.', + component: CheckboxInput, +}; + +export const CHAT_PRAYER: FeatureToggle = { + name: 'Chat Prayers', + category: 'ADMIN', + description: 'Show prayers.', + component: CheckboxInput, +}; + +export const SOUND_ADMINHELP: FeatureToggle = { + name: 'Adminhelp Sound', + category: 'ADMIN', + description: 'Enables playing the bwoink when a new adminhelp is sent.', + component: CheckboxInput, +}; + +export const CHAT_RADIO: FeatureToggle = { + name: 'Radio Chatter', + category: 'ADMIN', + description: 'Completely enable/disable hearing any radio anywhere.', + component: CheckboxInput, +}; + +export const CHAT_RLOOC: FeatureToggle = { + name: 'Remote LOOC Chat', + category: 'ADMIN', + description: 'Hear LOOC from anywhere.', + component: CheckboxInput, +}; + +export const CHAT_ADSAY: FeatureToggle = { + name: 'Living Deadchat', + category: 'ADMIN', + description: 'Enables seeing deadchat when not observing.', + component: CheckboxInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/chat.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/chat.tsx new file mode 100644 index 0000000000..10c081f060 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/chat.tsx @@ -0,0 +1,81 @@ +import { CheckboxInput, FeatureToggle } from '../base'; + +export const CHAT_SHOWICONS: FeatureToggle = { + name: 'Chat Tags', + category: 'CHAT', + description: 'Show tags/badges on special channels like OOC.', + component: CheckboxInput, +}; + +export const SHOW_TYPING: FeatureToggle = { + name: 'Typing Indicator', + category: 'CHAT', + description: 'Show a typing indicator when you are typing ingame.', + component: CheckboxInput, +}; + +export const SHOW_TYPING_SUBTLE: FeatureToggle = { + name: 'Typing Indicator: Subtle', + category: 'CHAT', + description: 'Show typing indicator for subtle and whisper messages.', + component: CheckboxInput, +}; + +export const CHAT_OOC: FeatureToggle = { + name: 'OOC Chat', + category: 'CHAT', + description: 'Enables OOC chat.', + component: CheckboxInput, +}; + +export const CHAT_LOOC: FeatureToggle = { + name: 'LOOC Chat', + category: 'CHAT', + description: 'Enables L(ocal)OOC chat.', + component: CheckboxInput, +}; + +export const CHAT_DEAD: FeatureToggle = { + name: 'Dead Chat', + category: 'CHAT', + description: 'Enables observer/dead/ghost chat.', + component: CheckboxInput, +}; + +export const CHAT_MENTION: FeatureToggle = { + name: 'Emphasize Name Mention', + category: 'CHAT', + description: + 'Makes messages containing your name or nickname appear larger to get your attention.', + component: CheckboxInput, +}; + +export const VORE_HEALTH_BARS: FeatureToggle = { + name: 'Vore Health Bars', + category: 'CHAT', + description: + 'Periodically shows status health bars in chat occasionally during vore absorption/digestion.', + component: CheckboxInput, +}; + +export const NEWS_POPUP: FeatureToggle = { + name: 'Lore News Popups', + category: 'CHAT', + description: 'Show new lore news on login.', + component: CheckboxInput, +}; + +export const RECEIVE_TIPS: FeatureToggle = { + name: 'Receive Tips Periodically', + category: 'CHAT', + description: 'Show helpful tips for new players periodically.', + component: CheckboxInput, +}; + +export const PAIN_FREQUENCY: FeatureToggle = { + name: 'Pain Message Cooldown', + category: 'CHAT', + description: + 'When enabled, reduces the amount of pain messages for minor wounds that you see.', + component: CheckboxInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx new file mode 100644 index 0000000000..10cc2297af --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx @@ -0,0 +1,36 @@ +import { CheckboxInput, FeatureToggle } from '../base'; + +export const WHISUBTLE_VIS: FeatureToggle = { + name: 'Allow ghosts to see whispers/subtles', + category: 'GHOST', + description: 'Enables ghosts to see your whispers and subtle emotes.', + component: CheckboxInput, +}; + +export const GHOST_SEE_WHISUBTLE: FeatureToggle = { + name: 'See whispers/subtles as ghost', + category: 'GHOST', + description: 'As a ghost, see whispers and subtles.', + component: CheckboxInput, +}; + +export const CHAT_GHOSTEARS: FeatureToggle = { + name: 'Ghost Ears', + category: 'GHOST', + description: 'When enabled, hear all speech; otherwise, only hear nearby.', + component: CheckboxInput, +}; + +export const CHAT_GHOSTSIGHT: FeatureToggle = { + name: 'Ghost Sight', + category: 'GHOST', + description: 'When enabled, hear all emotes; otherwise, only hear nearby.', + component: CheckboxInput, +}; + +export const CHAT_GHOSTRADIO: FeatureToggle = { + name: 'Ghost Radio', + category: 'GHOST', + description: 'When enabled, hear all radio; otherwise, only hear nearby.', + component: CheckboxInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/misc.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/misc.tsx new file mode 100644 index 0000000000..33e63600a4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/misc.tsx @@ -0,0 +1,74 @@ +import { CheckboxInput, FeatureToggle } from '../base'; + +export const AMBIENT_OCCLUSION_PREF: FeatureToggle = { + name: 'Enable ambient occlusion', + category: 'GAMEPLAY', + description: 'Enable ambient occlusion, light shadows around characters.', + component: CheckboxInput, +}; + +export const MOB_TOOLTIPS: FeatureToggle = { + name: 'Enable mob tooltips', + category: 'GAMEPLAY', + description: 'Enable tooltips when hovering over mobs.', + component: CheckboxInput, +}; + +export const INV_TOOLTIPS: FeatureToggle = { + name: 'Enable inventory tooltips', + category: 'GAMEPLAY', + description: 'Enable tooltips when hovering over inventory items.', + component: CheckboxInput, +}; + +export const ATTACK_ICONS: FeatureToggle = { + name: 'Attack Icons', + category: 'GAMEPLAY', + description: + 'Enable showing an overlay of what a mob was hit with during the attack animation.', + component: CheckboxInput, +}; + +export const PRECISE_PLACEMENT: FeatureToggle = { + name: 'Precision Placement', + category: 'GAMEPLAY', + description: + 'Objects placed on table will be on cursor position when enabled, or centered when disabled.', + component: CheckboxInput, +}; + +export const HUD_HOTKEYS: FeatureToggle = { + name: 'Hotkeys Default', + category: 'GAMEPLAY', + description: 'Enables turning hotkey mode on by default.', + component: CheckboxInput, +}; + +export const SHOW_PROGRESS: FeatureToggle = { + name: 'Progress Bar', + category: 'GAMEPLAY', + description: 'Enables seeing progress bars for various actions.', + component: CheckboxInput, +}; + +export const SAFE_FIRING: FeatureToggle = { + name: 'Gun Firing Intent Requirement', + category: 'GAMEPLAY', + description: 'When enabled, firing a gun requires a non-help intent to fire.', + component: CheckboxInput, +}; + +export const SHOW_STATUS: FeatureToggle = { + name: 'Status Indicators', + category: 'GAMEPLAY', + description: "Enables seeing status indicators over people's heads.", + component: CheckboxInput, +}; + +export const AUTO_AFK: FeatureToggle = { + name: 'Automatic AFK Status', + category: 'GAMEPLAY', + description: + 'When enabled, you will automatically be marked as AFK if you are idle for too long.', + component: CheckboxInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/runechat.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/runechat.tsx new file mode 100644 index 0000000000..832c3d79fe --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/runechat.tsx @@ -0,0 +1,29 @@ +import { CheckboxInput, FeatureToggle } from '../base'; + +export const RUNECHAT_MOB: FeatureToggle = { + name: 'Runechat: Mobs', + category: 'RUNECHAT', + description: 'Chat messages will show above heads.', + component: CheckboxInput, +}; + +export const RUNECHAT_OBJ: FeatureToggle = { + name: 'Runechat: Objects', + category: 'RUNECHAT', + description: 'Chat messages will show above objects when they speak.', + component: CheckboxInput, +}; + +export const RUNECHAT_BORDER: FeatureToggle = { + name: 'Runechat: Letter Borders', + category: 'RUNECHAT', + description: 'Enables a border around each letter in a runechat message.', + component: CheckboxInput, +}; + +export const RUNECHAT_LONG: FeatureToggle = { + name: 'Runechat: Long Messages', + category: 'RUNECHAT', + description: 'Sets runechat to show more characters.', + component: CheckboxInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/sound.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/sound.tsx new file mode 100644 index 0000000000..937fe35486 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/sound.tsx @@ -0,0 +1,156 @@ +import { CheckboxInput, FeatureToggle } from '../base'; + +export const SOUND_MIDI: FeatureToggle = { + name: 'Play Admin MIDIs', + category: 'SOUNDS', + description: 'Enable hearing admin played sounds.', + component: CheckboxInput, +}; + +export const SOUND_LOBBY: FeatureToggle = { + name: 'Play Lobby Music', + category: 'SOUNDS', + description: 'Enable hearing lobby music.', + component: CheckboxInput, +}; + +export const SOUND_AMBIENCE: FeatureToggle = { + name: 'Play Ambience', + category: 'SOUNDS', + description: 'Enable hearing ambient sounds and music.', + component: CheckboxInput, +}; + +export const SOUND_JUKEBOX: FeatureToggle = { + name: 'Play Jukebox Music', + category: 'SOUNDS', + description: 'Enable hearing music from the jukebox.', + component: CheckboxInput, +}; + +export const SOUND_INSTRUMENT: FeatureToggle = { + name: 'Hear In-game Instruments', + category: 'SOUNDS', + description: 'Enable hearing instruments playing.', + component: CheckboxInput, +}; + +export const EMOTE_NOISES: FeatureToggle = { + name: 'Emote Noises', + category: 'SOUNDS', + description: 'Enable hearing noises from emotes.', + component: CheckboxInput, +}; + +export const RADIO_SOUNDS: FeatureToggle = { + name: 'Radio Sounds', + category: 'SOUNDS', + description: 'Enable hearing a sound when somebody speaks over your headset.', + component: CheckboxInput, +}; + +export const SAY_SOUNDS: FeatureToggle = { + name: 'Say Sounds', + category: 'SOUNDS', + description: 'Enable hearing a sound when somebody speaks using say.', + component: CheckboxInput, +}; + +export const EMOTE_SOUNDS: FeatureToggle = { + name: 'Me Sounds', + category: 'SOUNDS', + description: 'Enable hearing a sound when somebody uses me.', + component: CheckboxInput, +}; + +export const WHISPER_SOUNDS: FeatureToggle = { + name: 'Whisper Sounds', + category: 'SOUNDS', + description: 'Enable hearing a sound when somebody speaks using whisper.', + component: CheckboxInput, +}; + +export const SUBTLE_SOUNDS: FeatureToggle = { + name: 'Subtle Sounds', + category: 'SOUNDS', + description: 'Enable hearing a sound when somebody uses subtle.', + component: CheckboxInput, +}; + +export const SOUND_AIRPUMP: FeatureToggle = { + name: 'Air Pump Ambient Noise', + category: 'SOUNDS', + description: 'Enable hearing air vent humming.', + component: CheckboxInput, +}; + +export const SOUND_OLDDOORS: FeatureToggle = { + name: 'Old Door Sounds', + category: 'SOUNDS', + description: 'Switch to old door sounds.', + component: CheckboxInput, +}; + +export const SOUND_DEPARTMENTDOORS: FeatureToggle = { + name: 'Department Door Sounds', + category: 'SOUNDS', + description: 'Enable hearing department-specific door sounds.', + component: CheckboxInput, +}; + +export const SOUND_PICKED: FeatureToggle = { + name: 'Picked-Up Item Sounds', + category: 'SOUNDS', + description: 'Enable hearing a sound when items are picked up.', + component: CheckboxInput, +}; + +export const SOUND_DROPPED: FeatureToggle = { + name: 'Dropped Item Sounds', + category: 'SOUNDS', + description: 'Enable hearing a sound when items are dropped.', + component: CheckboxInput, +}; + +export const SOUND_WEATHER: FeatureToggle = { + name: 'Weather Sounds', + category: 'SOUNDS', + description: 'Enable hearing weather sounds while on a planet.', + component: CheckboxInput, +}; + +export const SOUND_SUPERMATTER: FeatureToggle = { + name: 'Supermatter Hum', + category: 'SOUNDS', + description: 'Enable hearing supermatter hums.', + component: CheckboxInput, +}; + +export const SOUND_MENTORHELP: FeatureToggle = { + name: 'Mentorhelp Pings', + category: 'SOUNDS', + description: 'Enable hearing mentorhelp pings.', + component: CheckboxInput, +}; + +// Vorey sounds +export const BELCH_NOISES: FeatureToggle = { + name: 'Belch Noises', + category: 'SOUNDS', + description: 'Enable hearing burping noises.', + component: CheckboxInput, +}; + +export const EATING_NOISES: FeatureToggle = { + name: 'Eating Noises', + category: 'SOUNDS', + description: 'Enable hearing vore eating noises.', + component: CheckboxInput, +}; + +export const DIGEST_NOISES: FeatureToggle = { + name: 'Digestion Noises', + category: 'SOUNDS', + description: 'Enable hearing vore digestion noises.', + component: CheckboxInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ui.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ui.tsx new file mode 100644 index 0000000000..7292b65de7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ui.tsx @@ -0,0 +1,29 @@ +import { CheckboxInput, FeatureToggle } from '../base'; + +export const BROWSER_STYLED: FeatureToggle = { + name: 'Use Fake NanoUI Browser Style', + category: 'UI', + description: 'Enable a dark fake NanoUI browser style for older UIs.', + component: CheckboxInput, +}; + +export const VCHAT_ENABLE: FeatureToggle = { + name: 'Enable TGChat', + category: 'UI', + description: 'Enable the TGChat chat panel.', + component: CheckboxInput, +}; + +export const TGUI_SAY: FeatureToggle = { + name: 'Say: Use TGUI', + category: 'UI', + description: 'Use TGUI for Say input.', + component: CheckboxInput, +}; + +export const TGUI_SAY_LIGHT_MODE: FeatureToggle = { + name: 'Say: Light mode', + category: 'UI', + description: 'Sets TGUI Say to use a light mode.', + component: CheckboxInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/index.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/index.ts new file mode 100644 index 0000000000..d8fa77f496 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/index.ts @@ -0,0 +1,23 @@ +// Unlike species and others, feature files export arrays of features +// rather than individual ones. This is because a lot of features are +// extremely small, and so it's easier for everyone to just combine them +// together. +// This still helps to prevent the server from needing to send client UI data +import { Feature } from './base'; + +// while also preventing downstreams from needing to mutate existing files. +const features: Record> = {}; + +const requireFeature = require.context('./', true, /.tsx$/); + +for (const key of requireFeature.keys()) { + if (key === 'index' || key === 'base') { + continue; + } + + for (const [featureKey, feature] of Object.entries(requireFeature(key))) { + features[featureKey] = feature as Feature; + } +} + +export default features; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/gender.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/gender.ts new file mode 100644 index 0000000000..baac865559 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/gender.ts @@ -0,0 +1,28 @@ +export enum Gender { + Male = 'male', + Female = 'female', + Other = 'plural', + Other2 = 'neuter', +} + +export const GENDERS = { + [Gender.Male]: { + icon: 'mars', + text: 'He/Him', + }, + + [Gender.Female]: { + icon: 'venus', + text: 'She/Her', + }, + + [Gender.Other]: { + icon: 'transgender', + text: 'They/Them', + }, + + [Gender.Other2]: { + icon: 'neuter', + text: 'It/Its', + }, +}; diff --git a/tgui/public/tgui-panel.bundle.js b/tgui/public/tgui-panel.bundle.js index 0977b15185..3f28f897a5 100644 --- a/tgui/public/tgui-panel.bundle.js +++ b/tgui/public/tgui-panel.bundle.js @@ -6,7 +6,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */function r(e,n){return n!=null&&typeof Symbol!="undefined"&&n[Symbol.hasInstance]?!!n[Symbol.hasInstance](e):e instanceof n}function o(e){"@swc/helpers - typeof";return e&&typeof Symbol!="undefined"&&e.constructor===Symbol?"symbol":typeof e}var i=t(61358),s=t(20686);function u(e){for(var n="https://reactjs.org/docs/error-decoder.html?invariant="+e,a=1;an}return!1}function j(e,n,a,c,O,M,$){this.acceptsBooleans=n===2||n===3||n===4,this.attributeName=c,this.attributeNamespace=O,this.mustUseProperty=a,this.propertyName=e,this.type=n,this.sanitizeURL=M,this.removeEmptyString=$}var B={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){B[e]=new j(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var n=e[0];B[n]=new j(n,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){B[e]=new j(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){B[e]=new j(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){B[e]=new j(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){B[e]=new j(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){B[e]=new j(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){B[e]=new j(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){B[e]=new j(e,5,!1,e.toLowerCase(),null,!1,!1)});var F=/[\-:]([a-z])/g;function C(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var n=e.replace(F,C);B[n]=new j(n,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var n=e.replace(F,C);B[n]=new j(n,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var n=e.replace(F,C);B[n]=new j(n,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){B[e]=new j(e,1,!1,e.toLowerCase(),null,!1,!1)}),B.xlinkHref=new j("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){B[e]=new j(e,1,!1,e.toLowerCase(),null,!0,!0)});function N(e,n,a,c){var O=B.hasOwnProperty(n)?B[n]:null;(O!==null?O.type!==0:c||!(2X||O[$]!==M[X]){var dt="\n"+O[$].replace(" at new "," at ");return e.displayName&&dt.includes("")&&(dt=dt.replace("",e.displayName)),dt}while(1<=$&&0<=X);break}}}finally{vt=!1,Error.prepareStackTrace=a}return(e=e?e.displayName||e.name:"")?nt(e):""}function bt(e){switch(e.tag){case 5:return nt(e.type);case 16:return nt("Lazy");case 13:return nt("Suspense");case 19:return nt("SuspenseList");case 0:case 2:case 15:return e=mt(e.type,!1),e;case 11:return e=mt(e.type.render,!1),e;case 1:return e=mt(e.type,!0),e;default:return""}}function gt(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case K:return"Fragment";case z:return"Portal";case J:return"Profiler";case k:return"StrictMode";case ct:return"Suspense";case ot:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case _:return(e.displayName||"Context")+".Consumer";case G:return(e._context.displayName||"Context")+".Provider";case Q:var n=e.render;return e=e.displayName,e||(e=n.displayName||n.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ut:return n=e.displayName||null,n!==null?n:gt(e.type)||"Memo";case W:n=e._payload,e=e._init;try{return gt(e(n))}catch(a){}}return null}function pt(e){var n=e.type;switch(e.tag){case 24:return"Cache";case 9:return(n.displayName||"Context")+".Consumer";case 10:return(n._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=n.render,e=e.displayName||e.name||"",n.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return n;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return gt(n);case 8:return n===k?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof n=="function")return n.displayName||n.name||null;if(typeof n=="string")return n}return null}function Dt(e){switch(typeof e=="undefined"?"undefined":o(e)){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ve(e){var n=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(n==="checkbox"||n==="radio")}function fe(e){var n=ve(e)?"checked":"value",a=Object.getOwnPropertyDescriptor(e.constructor.prototype,n),c=""+e[n];if(!e.hasOwnProperty(n)&&typeof a!="undefined"&&typeof a.get=="function"&&typeof a.set=="function"){var O=a.get,M=a.set;return Object.defineProperty(e,n,{configurable:!0,get:function(){return O.call(this)},set:function(X){c=""+X,M.call(this,X)}}),Object.defineProperty(e,n,{enumerable:a.enumerable}),{getValue:function(){return c},setValue:function(X){c=""+X},stopTracking:function(){e._valueTracker=null,delete e[n]}}}}function Jt(e){e._valueTracker||(e._valueTracker=fe(e))}function Ct(e){if(!e)return!1;var n=e._valueTracker;if(!n)return!0;var a=n.getValue(),c="";return e&&(c=ve(e)?e.checked?"true":"false":e.value),e=c,e!==a?(n.setValue(e),!0):!1}function Nt(e){if(e=e||(typeof document!="undefined"?document:void 0),typeof e=="undefined")return null;try{return e.activeElement||e.body}catch(n){return e.body}}function Wt(e,n){var a=n.checked;return Z({},n,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:a!=null?a:e._wrapperState.initialChecked})}function Ut(e,n){var a=n.defaultValue==null?"":n.defaultValue,c=n.checked!=null?n.checked:n.defaultChecked;a=Dt(n.value!=null?n.value:a),e._wrapperState={initialChecked:c,initialValue:a,controlled:n.type==="checkbox"||n.type==="radio"?n.checked!=null:n.value!=null}}function ae(e,n){n=n.checked,n!=null&&N(e,"checked",n,!1)}function ie(e,n){ae(e,n);var a=Dt(n.value),c=n.type;if(a!=null)c==="number"?(a===0&&e.value===""||e.value!=a)&&(e.value=""+a):e.value!==""+a&&(e.value=""+a);else if(c==="submit"||c==="reset"){e.removeAttribute("value");return}n.hasOwnProperty("value")?Ft(e,n.type,a):n.hasOwnProperty("defaultValue")&&Ft(e,n.type,Dt(n.defaultValue)),n.checked==null&&n.defaultChecked!=null&&(e.defaultChecked=!!n.defaultChecked)}function te(e,n,a){if(n.hasOwnProperty("value")||n.hasOwnProperty("defaultValue")){var c=n.type;if(!(c!=="submit"&&c!=="reset"||n.value!==void 0&&n.value!==null))return;n=""+e._wrapperState.initialValue,a||n===e.value||(e.value=n),e.defaultValue=n}a=e.name,a!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,a!==""&&(e.name=a)}function Ft(e,n,a){(n!=="number"||Nt(e.ownerDocument)!==e)&&(a==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+a&&(e.defaultValue=""+a))}var Bt=Array.isArray;function At(e,n,a,c){if(e=e.options,n){n={};for(var O=0;O"+n.valueOf().toString()+"",n=Se.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;n.firstChild;)e.appendChild(n.firstChild)}});function le(e,n){if(n){var a=e.firstChild;if(a&&a===e.lastChild&&a.nodeType===3){a.nodeValue=n;return}}e.textContent=n}var $t={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Gt=["Webkit","ms","Moz","O"];Object.keys($t).forEach(function(e){Gt.forEach(function(n){n=n+e.charAt(0).toUpperCase()+e.substring(1),$t[n]=$t[e]})});function Me(e,n,a){return n==null||typeof n=="boolean"||n===""?"":a||typeof n!="number"||n===0||$t.hasOwnProperty(e)&&$t[e]?(""+n).trim():n+"px"}function Ze(e,n){e=e.style;for(var a in n)if(n.hasOwnProperty(a)){var c=a.indexOf("--")===0,O=Me(a,n[a],c);a==="float"&&(a="cssFloat"),c?e.setProperty(a,O):e[a]=O}}var tr=Z({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function fn(e,n){if(n){if(tr[e]&&(n.children!=null||n.dangerouslySetInnerHTML!=null))throw Error(u(137,e));if(n.dangerouslySetInnerHTML!=null){if(n.children!=null)throw Error(u(60));if(typeof n.dangerouslySetInnerHTML!="object"||!("__html"in n.dangerouslySetInnerHTML))throw Error(u(61))}if(n.style!=null&&typeof n.style!="object")throw Error(u(62))}}function Un(e,n){if(e.indexOf("-")===-1)return typeof n.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Pn=null;function cr(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var rt=null,xt=null,st=null;function yt(e){if(e=Ma(e)){if(typeof rt!="function")throw Error(u(280));var n=e.stateNode;n&&(n=ja(n),rt(e.stateNode,e.type,n))}}function Tt(e){xt?st?st.push(e):st=[e]:xt=e}function Lt(){if(xt){var e=xt,n=st;if(st=xt=null,yt(e),n)for(e=0;e>>=0,e===0?32:31-(Dn(e)/Er|0)|0}var er=64,hn=4194304;function pr(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ln(e,n){var a=e.pendingLanes;if(a===0)return 0;var c=0,O=e.suspendedLanes,M=e.pingedLanes,$=a&268435455;if($!==0){var X=$&~O;X!==0?c=pr(X):(M&=$,M!==0&&(c=pr(M)))}else $=a&~O,$!==0?c=pr($):M!==0&&(c=pr(M));if(c===0)return 0;if(n!==0&&n!==c&&!(n&O)&&(O=c&-c,M=n&-n,O>=M||O===16&&(M&4194240)!==0))return n;if(c&4&&(c|=a&16),n=e.entangledLanes,n!==0)for(e=e.entanglements,n&=c;0a;a++)n.push(e);return n}function Bn(e,n,a){e.pendingLanes|=n,n!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,n=31-On(n),e[n]=a}function To(e,n){var a=e.pendingLanes&~n;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=n,e.mutableReadLanes&=n,e.entangledLanes&=n,n=e.entanglements;var c=e.eventTimes;for(e=e.expirationTimes;0=_a),Pi=" ",Ci=!1;function ms(e,n){switch(e){case"keyup":return mu.indexOf(n.keyCode)!==-1;case"keydown":return n.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function gs(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var so=!1;function ys(e,n){switch(e){case"compositionend":return gs(n);case"keypress":return n.which!==32?null:(Ci=!0,Pi);case"textInput":return e=n.data,e===Pi&&Ci?null:e;default:return null}}function xs(e,n){if(so)return e==="compositionend"||!Ai&&ms(e,n)?(e=gn(),be=se=Vt=null,so=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(n.ctrlKey||n.altKey||n.metaKey)||n.ctrlKey&&n.altKey){if(n.char&&1=n)return{node:a,offset:n-e};e=c}t:{for(;a;){if(a.nextSibling){a=a.nextSibling;break t}a=a.parentNode}a=void 0}a=Es(a)}}function Sa(e,n){return e&&n?e===n?!0:e&&e.nodeType===3?!1:n&&n.nodeType===3?Sa(e,n.parentNode):"contains"in e?e.contains(n):e.compareDocumentPosition?!!(e.compareDocumentPosition(n)&16):!1:!1}function Ts(){for(var e=window,n=Nt();r(n,e.HTMLIFrameElement);){try{var a=typeof n.contentWindow.location.href=="string"}catch(c){a=!1}if(a)e=n.contentWindow;else break;n=Nt(e.document)}return n}function Ni(e){var n=e&&e.nodeName&&e.nodeName.toLowerCase();return n&&(n==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||n==="textarea"||e.contentEditable==="true")}function na(e){var n=Ts(),a=e.focusedElem,c=e.selectionRange;if(n!==a&&a&&a.ownerDocument&&Sa(a.ownerDocument.documentElement,a)){if(c!==null&&Ni(a)){if(n=c.start,e=c.end,e===void 0&&(e=n),"selectionStart"in a)a.selectionStart=n,a.selectionEnd=Math.min(e,a.value.length);else if(e=(n=a.ownerDocument||document)&&n.defaultView||window,e.getSelection){e=e.getSelection();var O=a.textContent.length,M=Math.min(c.start,O);c=c.end===void 0?M:Math.min(c.end,O),!e.extend&&M>c&&(O=c,c=M,M=O),O=Os(a,M);var $=Os(a,c);O&&$&&(e.rangeCount!==1||e.anchorNode!==O.node||e.anchorOffset!==O.offset||e.focusNode!==$.node||e.focusOffset!==$.offset)&&(n=n.createRange(),n.setStart(O.node,O.offset),e.removeAllRanges(),M>c?(e.addRange(n),e.extend($.node,$.offset)):(n.setEnd($.node,$.offset),e.addRange(n)))}}for(n=[],e=a;e=e.parentNode;)e.nodeType===1&&n.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof a.focus=="function"&&a.focus(),a=0;a=document.documentMode,Mo=null,Li=null,Ea=null,Is=!1;function Bi(e,n,a){var c=a.window===a?a.document:a.nodeType===9?a:a.ownerDocument;Is||Mo==null||Mo!==Nt(c)||(c=Mo,"selectionStart"in c&&Ni(c)?c={start:c.selectionStart,end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset}),Ea&&xa(Ea,c)||(Ea=c,c=Pa(Li,"onSelect"),0ba||(e.current=js[ba],js[ba]=null,ba--)}function rn(e,n){ba++,js[ba]=e.current,e.current=n}var Ko={},nr=eo(Ko),gr=eo(!1),Vo=Ko;function Ho(e,n){var a=e.type.contextTypes;if(!a)return Ko;var c=e.stateNode;if(c&&c.__reactInternalMemoizedUnmaskedChildContext===n)return c.__reactInternalMemoizedMaskedChildContext;var O={},M;for(M in a)O[M]=n[M];return c&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=n,e.__reactInternalMemoizedMaskedChildContext=O),O}function Ir(e){return e=e.childContextTypes,e!=null}function Zi(){ln(gr),ln(nr)}function aa(e,n,a){if(nr.current!==Ko)throw Error(u(168));rn(nr,n),rn(gr,a)}function bs(e,n,a){var c=e.stateNode;if(n=n.childContextTypes,typeof c.getChildContext!="function")return a;c=c.getChildContext();for(var O in c)if(!(O in n))throw Error(u(108,pt(e)||"Unknown",O));return Z({},a,c)}function Ji(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ko,Vo=nr.current,rn(nr,e),rn(gr,gr.current),!0}function qi(e,n,a){var c=e.stateNode;if(!c)throw Error(u(169));a?(e=bs(e,n,Vo),c.__reactInternalMemoizedMergedChildContext=e,ln(gr),ln(nr),rn(nr,e)):ln(gr),rn(gr,a)}var Hr=null,ii=!1,_i=!1;function si(e){Hr===null?Hr=[e]:Hr.push(e)}function Mu(e){ii=!0,si(e)}function vo(){if(!_i&&Hr!==null){_i=!0;var e=0,n=Ke;try{var a=Hr;for(Ke=1;e>=$,O-=$,ho=1<<32-On(n)+O|a<Ue?(or=De,De=null):or=De.sibling;var _e=kt(St,De,Ot[Ue],re);if(_e===null){De===null&&(De=or);break}e&&De&&_e.alternate===null&&n(St,De),ht=M(_e,ht,Ue),Ne===null?Ae=_e:Ne.sibling=_e,Ne=_e,De=or}if(Ue===Ot.length)return a(St,De),xn&&ia(St,Ue),Ae;if(De===null){for(;UeUe?(or=De,De=null):or=De.sibling;var va=kt(St,De,_e.value,re);if(va===null){De===null&&(De=or);break}e&&De&&va.alternate===null&&n(St,De),ht=M(va,ht,Ue),Ne===null?Ae=va:Ne.sibling=va,Ne=va,De=or}if(_e.done)return a(St,De),xn&&ia(St,Ue),Ae;if(De===null){for(;!_e.done;Ue++,_e=Ot.next())_e=Yt(St,_e.value,re),_e!==null&&(ht=M(_e,ht,Ue),Ne===null?Ae=_e:Ne.sibling=_e,Ne=_e);return xn&&ia(St,Ue),Ae}for(De=c(St,De);!_e.done;Ue++,_e=Ot.next())_e=me(De,St,Ue,_e.value,re),_e!==null&&(e&&_e.alternate!==null&&De.delete(_e.key===null?Ue:_e.key),ht=M(_e,ht,Ue),Ne===null?Ae=_e:Ne.sibling=_e,Ne=_e);return e&&De.forEach(function(mf){return n(St,mf)}),xn&&ia(St,Ue),Ae}function Fn(St,ht,Ot,re){if(typeof Ot=="object"&&Ot!==null&&Ot.type===K&&Ot.key===null&&(Ot=Ot.props.children),typeof Ot=="object"&&Ot!==null){switch(Ot.$$typeof){case D:t:{for(var Ae=Ot.key,Ne=ht;Ne!==null;){if(Ne.key===Ae){if(Ae=Ot.type,Ae===K){if(Ne.tag===7){a(St,Ne.sibling),ht=O(Ne,Ot.props.children),ht.return=St,St=ht;break t}}else if(Ne.elementType===Ae||typeof Ae=="object"&&Ae!==null&&Ae.$$typeof===W&&at(Ae)===Ne.type){a(St,Ne.sibling),ht=O(Ne,Ot.props),ht.ref=it(St,Ne,Ot),ht.return=St,St=ht;break t}a(St,Ne);break}else n(St,Ne);Ne=Ne.sibling}Ot.type===K?(ht=Ha(Ot.props.children,St.mode,re,Ot.key),ht.return=St,St=ht):(re=qs(Ot.type,Ot.key,Ot.props,null,St.mode,re),re.ref=it(St,ht,Ot),re.return=St,St=re)}return $(St);case z:t:{for(Ne=Ot.key;ht!==null;){if(ht.key===Ne)if(ht.tag===4&&ht.stateNode.containerInfo===Ot.containerInfo&&ht.stateNode.implementation===Ot.implementation){a(St,ht.sibling),ht=O(ht,Ot.children||[]),ht.return=St,St=ht;break t}else{a(St,ht);break}else n(St,ht);ht=ht.sibling}ht=cl(Ot,St.mode,re),ht.return=St,St=ht}return $(St);case W:return Ne=Ot._init,Fn(St,ht,Ne(Ot._payload),re)}if(Bt(Ot))return Ee(St,ht,Ot,re);if(H(Ot))return Te(St,ht,Ot,re);q(St,Ot)}return typeof Ot=="string"&&Ot!==""||typeof Ot=="number"?(Ot=""+Ot,ht!==null&&ht.tag===6?(a(St,ht.sibling),ht=O(ht,Ot),ht.return=St,St=ht):(a(St,ht),ht=ll(Ot,St.mode,re),ht.return=St,St=ht),$(St)):a(St,ht)}return Fn}var Mt=Rt(!0),It=Rt(!1),zt=eo(null),Zt=null,_t=null,ee=null;function pe(){ee=_t=Zt=null}function Ie(e){var n=zt.current;ln(zt),e._currentValue=n}function Kt(e,n,a){for(;e!==null;){var c=e.alternate;if((e.childLanes&n)!==n?(e.childLanes|=n,c!==null&&(c.childLanes|=n)):c!==null&&(c.childLanes&n)!==n&&(c.childLanes|=n),e===a)break;e=e.return}}function de(e,n){Zt=e,ee=_t=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&n&&(Br=!0),e.firstContext=null)}function he(e){var n=e._currentValue;if(ee!==e)if(e={context:e,memoizedValue:n,next:null},_t===null){if(Zt===null)throw Error(u(308));_t=e,Zt.dependencies={lanes:0,firstContext:e}}else _t=_t.next=e;return n}var Be=null;function we(e){Be===null?Be=[e]:Be.push(e)}function We(e,n,a,c){var O=n.interleaved;return O===null?(a.next=a,we(n)):(a.next=O.next,O.next=a),n.interleaved=a,Qt(e,c)}function Qt(e,n){e.lanes|=n;var a=e.alternate;for(a!==null&&(a.lanes|=n),a=e,e=e.return;e!==null;)e.childLanes|=n,a=e.alternate,a!==null&&(a.childLanes|=n),a=e,e=e.return;return a.tag===3?a.stateNode:null}var Ce=!1;function Re(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xe(e,n){e=e.updateQueue,n.updateQueue===e&&(n.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function en(e,n){return{eventTime:e,lane:n,tag:0,payload:null,callback:null,next:null}}function Rn(e,n,a){var c=e.updateQueue;if(c===null)return null;if(c=c.shared,Je&2){var O=c.pending;return O===null?n.next=n:(n.next=O.next,O.next=n),c.pending=n,Qt(e,a)}return O=c.interleaved,O===null?(n.next=n,we(c)):(n.next=O.next,O.next=n),c.interleaved=n,Qt(e,a)}function on(e,n,a){if(n=n.updateQueue,n!==null&&(n=n.shared,(a&4194240)!==0)){var c=n.lanes;c&=e.pendingLanes,a|=c,n.lanes=a,Kn(e,a)}}function Zn(e,n){var a=e.updateQueue,c=e.alternate;if(c!==null&&(c=c.updateQueue,a===c)){var O=null,M=null;if(a=a.firstBaseUpdate,a!==null){do{var $={eventTime:a.eventTime,lane:a.lane,tag:a.tag,payload:a.payload,callback:a.callback,next:null};M===null?O=M=$:M=M.next=$,a=a.next}while(a!==null);M===null?O=M=n:M=M.next=n}else O=M=n;a={baseState:c.baseState,firstBaseUpdate:O,lastBaseUpdate:M,shared:c.shared,effects:c.effects},e.updateQueue=a;return}e=a.lastBaseUpdate,e===null?a.firstBaseUpdate=n:e.next=n,a.lastBaseUpdate=n}function Dr(e,n,a,c){var O=e.updateQueue;Ce=!1;var M=O.firstBaseUpdate,$=O.lastBaseUpdate,X=O.shared.pending;if(X!==null){O.shared.pending=null;var dt=X,Pt=dt.next;dt.next=null,$===null?M=Pt:$.next=Pt,$=dt;var jt=e.alternate;jt!==null&&(jt=jt.updateQueue,X=jt.lastBaseUpdate,X!==$&&(X===null?jt.firstBaseUpdate=Pt:X.next=Pt,jt.lastBaseUpdate=dt))}if(M!==null){var Yt=O.baseState;$=0,jt=Pt=dt=null,X=M;do{var kt=X.lane,me=X.eventTime;if((c&kt)===kt){jt!==null&&(jt=jt.next={eventTime:me,lane:0,tag:X.tag,payload:X.payload,callback:X.callback,next:null});t:{var Ee=e,Te=X;switch(kt=n,me=a,Te.tag){case 1:if(Ee=Te.payload,typeof Ee=="function"){Yt=Ee.call(me,Yt,kt);break t}Yt=Ee;break t;case 3:Ee.flags=Ee.flags&-65537|128;case 0:if(Ee=Te.payload,kt=typeof Ee=="function"?Ee.call(me,Yt,kt):Ee,kt==null)break t;Yt=Z({},Yt,kt);break t;case 2:Ce=!0}}X.callback!==null&&X.lane!==0&&(e.flags|=64,kt=O.effects,kt===null?O.effects=[X]:kt.push(X))}else me={eventTime:me,lane:kt,tag:X.tag,payload:X.payload,callback:X.callback,next:null},jt===null?(Pt=jt=me,dt=Yt):jt=jt.next=me,$|=kt;if(X=X.next,X===null){if(X=O.shared.pending,X===null)break;kt=X,X=kt.next,kt.next=null,O.lastBaseUpdate=kt,O.shared.pending=null}}while(!0);if(jt===null&&(dt=Yt),O.baseState=dt,O.firstBaseUpdate=Pt,O.lastBaseUpdate=jt,n=O.shared.interleaved,n!==null){O=n;do $|=O.lane,O=O.next;while(O!==n)}else M===null&&(O.shared.lanes=0);Wa|=$,e.lanes=$,e.memoizedState=Yt}}function qe(e,n,a){if(e=n.effects,n.effects=null,e!==null)for(n=0;na?a:4,e(!0);var c=es.transition;es.transition={};try{e(!1),n()}finally{Ke=a,es.transition=c}}function zl(){return Pr().memoizedState}function zc(e,n,a){var c=ca(e);if(a={lane:c,action:a,hasEagerState:!1,eagerState:null,next:null},Wl(e))$l(n,a);else if(a=We(e,n,a,c),a!==null){var O=wr();So(a,e,c,O),Kl(a,n,c)}}function Wc(e,n,a){var c=ca(e),O={lane:c,action:a,hasEagerState:!1,eagerState:null,next:null};if(Wl(e))$l(n,O);else{var M=e.alternate;if(e.lanes===0&&(M===null||M.lanes===0)&&(M=n.lastRenderedReducer,M!==null))try{var $=n.lastRenderedState,X=M($,a);if(O.hasEagerState=!0,O.eagerState=X,Mr(X,$)){var dt=n.interleaved;dt===null?(O.next=O,we(n)):(O.next=dt.next,dt.next=O),n.interleaved=O;return}}catch(Pt){}finally{}a=We(e,n,O,c),a!==null&&(O=wr(),So(a,e,c,O),Kl(a,n,c))}}function Wl(e){var n=e.alternate;return e===vn||n!==null&&n===vn}function $l(e,n){Gr=ci=!0;var a=e.pending;a===null?n.next=n:(n.next=a.next,a.next=n),e.pending=n}function Kl(e,n,a){if(a&4194240){var c=n.lanes;c&=e.pendingLanes,a|=c,n.lanes=a,Kn(e,a)}}var Us={readContext:he,useCallback:qn,useContext:qn,useEffect:qn,useImperativeHandle:qn,useInsertionEffect:qn,useLayoutEffect:qn,useMemo:qn,useReducer:qn,useRef:qn,useState:qn,useDebugValue:qn,useDeferredValue:qn,useTransition:qn,useMutableSource:qn,useSyncExternalStore:qn,useId:qn,unstable_isNewReconciler:!1},$c={readContext:he,useCallback:function(n,a){return kr().memoizedState=[n,a===void 0?null:a],n},useContext:he,useEffect:jl,useImperativeHandle:function(n,a,c){return c=c!=null?c.concat([n]):null,Bs(4194308,4,Dl.bind(null,a,n),c)},useLayoutEffect:function(n,a){return Bs(4194308,4,n,a)},useInsertionEffect:function(n,a){return Bs(4,2,n,a)},useMemo:function(n,a){var c=kr();return a=a===void 0?null:a,n=n(),c.memoizedState=[n,a],n},useReducer:function(n,a,c){var O=kr();return a=c!==void 0?c(a):a,O.memoizedState=O.baseState=a,n={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:n,lastRenderedState:a},O.queue=n,n=n.dispatch=zc.bind(null,vn,n),[O.memoizedState,n]},useRef:function(n){var a=kr();return n={current:n},a.memoizedState=n},useState:Rl,useDebugValue:Bu,useDeferredValue:function(n){return kr().memoizedState=n},useTransition:function(){var n=Rl(!1),a=n[0];return n=Uc.bind(null,n[1]),kr().memoizedState=n,[a,n]},useMutableSource:function(){},useSyncExternalStore:function(n,a,c){var O=vn,M=kr();if(xn){if(c===void 0)throw Error(u(407));c=c()}else{if(c=a(),rr===null)throw Error(u(349));Go&30||Il(O,a,c)}M.memoizedState=c;var $={value:c,getSnapshot:a};return M.queue=$,jl(Pl.bind(null,O,$,n),[n]),O.flags|=2048,rs(9,Al.bind(null,O,$,c,a),void 0,null),c},useId:function(){var n=kr(),a=rr.identifierPrefix;if(xn){var c=No,O=ho;c=(O&~(1<<32-On(O)-1)).toString(32)+c,a=":"+a+"R"+c,c=Lo++,0<\/script>",e=e.removeChild(e.firstChild)):typeof c.is=="string"?e=$.createElement(a,{is:c.is}):(e=$.createElement(a),a==="select"&&($=e,c.multiple?$.multiple=!0:c.size&&($.size=c.size))):e=$.createElementNS(e,a),e[Vr]=n,e[$o]=c,uc(e,n,!1,!1),n.stateNode=e;t:{switch($=Un(a,c),a){case"dialog":un("cancel",e),un("close",e),O=c;break;case"iframe":case"object":case"embed":un("load",e),O=c;break;case"video":case"audio":for(O=0;Omi&&(n.flags|=128,c=!0,os(M,!1),n.lanes=4194304)}else{if(!c)if(e=li($),e!==null){if(n.flags|=128,c=!0,a=e.updateQueue,a!==null&&(n.updateQueue=a,n.flags|=4),os(M,!0),M.tail===null&&M.tailMode==="hidden"&&!$.alternate&&!xn)return xr(n),null}else 2*Ye()-M.renderingStartTime>mi&&a!==1073741824&&(n.flags|=128,c=!0,os(M,!1),n.lanes=4194304);M.isBackwards?($.sibling=n.child,n.child=$):(a=M.last,a!==null?a.sibling=$:n.child=$,M.last=$)}return M.tail!==null?(n=M.tail,M.rendering=n,M.tail=n.sibling,M.renderingStartTime=Ye(),n.sibling=null,a=cn.current,rn(cn,c?a&1|2:a&1),n):(xr(n),null);case 22:case 23:return il(),c=n.memoizedState!==null,e!==null&&e.memoizedState!==null!==c&&(n.flags|=8192),c&&n.mode&1?Yr&1073741824&&(xr(n),n.subtreeFlags&6&&(n.flags|=8192)):xr(n),null;case 24:return null;case 25:return null}throw Error(u(156,n.tag))}function Qc(e,n){switch(Ds(n),n.tag){case 1:return Ir(n.type)&&Zi(),e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 3:return ro(),ln(gr),ln(nr),ts(),e=n.flags,e&65536&&!(e&128)?(n.flags=e&-65537|128,n):null;case 5:return mo(n),null;case 13:if(ln(cn),e=n.memoizedState,e!==null&&e.dehydrated!==null){if(n.alternate===null)throw Error(u(340));U()}return e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 19:return ln(cn),null;case 4:return ro(),null;case 10:return Ie(n.type._context),null;case 22:case 23:return il(),null;case 24:return null;default:return null}}var Ks=!1,Sr=!1,Zc=typeof WeakSet=="function"?WeakSet:Set,ye=null;function pi(e,n){var a=e.ref;if(a!==null)if(typeof a=="function")try{a(null)}catch(c){jn(e,n,c)}else a.current=null}function Xu(e,n,a){try{a()}catch(c){jn(e,n,c)}}var fc=!1;function Jc(e,n){if(Ki=$r,e=Ts(),Ni(e)){if("selectionStart"in e)var a={start:e.selectionStart,end:e.selectionEnd};else t:{a=(a=e.ownerDocument)&&a.defaultView||window;var c=a.getSelection&&a.getSelection();if(c&&c.rangeCount!==0){a=c.anchorNode;var O=c.anchorOffset,M=c.focusNode;c=c.focusOffset;try{a.nodeType,M.nodeType}catch(re){a=null;break t}var $=0,X=-1,dt=-1,Pt=0,jt=0,Yt=e,kt=null;e:for(;;){for(var me;Yt!==a||O!==0&&Yt.nodeType!==3||(X=$+O),Yt!==M||c!==0&&Yt.nodeType!==3||(dt=$+c),Yt.nodeType===3&&($+=Yt.nodeValue.length),(me=Yt.firstChild)!==null;)kt=Yt,Yt=me;for(;;){if(Yt===e)break e;if(kt===a&&++Pt===O&&(X=$),kt===M&&++jt===c&&(dt=$),(me=Yt.nextSibling)!==null)break;Yt=kt,kt=Yt.parentNode}Yt=me}a=X===-1||dt===-1?null:{start:X,end:dt}}else a=null}a=a||{start:0,end:0}}else a=null;for(Vi={focusedElem:e,selectionRange:a},$r=!1,ye=n;ye!==null;)if(n=ye,e=n.child,(n.subtreeFlags&1028)!==0&&e!==null)e.return=n,ye=e;else for(;ye!==null;){n=ye;try{var Ee=n.alternate;if(n.flags&1024)switch(n.tag){case 0:case 11:case 15:break;case 1:if(Ee!==null){var Te=Ee.memoizedProps,Fn=Ee.memoizedState,St=n.stateNode,ht=St.getSnapshotBeforeUpdate(n.elementType===n.type?Te:go(n.type,Te),Fn);St.__reactInternalSnapshotBeforeUpdate=ht}break;case 3:var Ot=n.stateNode.containerInfo;Ot.nodeType===1?Ot.textContent="":Ot.nodeType===9&&Ot.documentElement&&Ot.removeChild(Ot.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(u(163))}}catch(re){jn(n,n.return,re)}if(e=n.sibling,e!==null){e.return=n.return,ye=e;break}ye=n.return}return Ee=fc,fc=!1,Ee}function as(e,n,a){var c=n.updateQueue;if(c=c!==null?c.lastEffect:null,c!==null){var O=c=c.next;do{if((O.tag&e)===e){var M=O.destroy;O.destroy=void 0,M!==void 0&&Xu(n,a,M)}O=O.next}while(O!==c)}}function Vs(e,n){if(n=n.updateQueue,n=n!==null?n.lastEffect:null,n!==null){var a=n=n.next;do{if((a.tag&e)===e){var c=a.create;a.destroy=c()}a=a.next}while(a!==n)}}function Qu(e){var n=e.ref;if(n!==null){var a=e.stateNode;switch(e.tag){case 5:e=a;break;default:e=a}typeof n=="function"?n(e):n.current=e}}function dc(e){var n=e.alternate;n!==null&&(e.alternate=null,dc(n)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(n=e.stateNode,n!==null&&(delete n[Vr],delete n[$o],delete n[Xi],delete n[Qi],delete n[Ru])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function vc(e){return e.tag===5||e.tag===3||e.tag===4}function pc(e){t:for(;;){for(;e.sibling===null;){if(e.return===null||vc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue t;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Zu(e,n,a){var c=e.tag;if(c===5||c===6)e=e.stateNode,n?a.nodeType===8?a.parentNode.insertBefore(e,n):a.insertBefore(e,n):(a.nodeType===8?(n=a.parentNode,n.insertBefore(e,a)):(n=a,n.appendChild(e)),a=a._reactRootContainer,a!=null||n.onclick!==null||(n.onclick=ai));else if(c!==4&&(e=e.child,e!==null))for(Zu(e,n,a),e=e.sibling;e!==null;)Zu(e,n,a),e=e.sibling}function Ju(e,n,a){var c=e.tag;if(c===5||c===6)e=e.stateNode,n?a.insertBefore(e,n):a.appendChild(e);else if(c!==4&&(e=e.child,e!==null))for(Ju(e,n,a),e=e.sibling;e!==null;)Ju(e,n,a),e=e.sibling}var ur=null,yo=!1;function sa(e,n,a){for(a=a.child;a!==null;)hc(e,n,a),a=a.sibling}function hc(e,n,a){if(dn&&typeof dn.onCommitFiberUnmount=="function")try{dn.onCommitFiberUnmount(nn,a)}catch(X){}switch(a.tag){case 5:Sr||pi(a,n);case 6:var c=ur,O=yo;ur=null,sa(e,n,a),ur=c,yo=O,ur!==null&&(yo?(e=ur,a=a.stateNode,e.nodeType===8?e.parentNode.removeChild(a):e.removeChild(a)):ur.removeChild(a.stateNode));break;case 18:ur!==null&&(yo?(e=ur,a=a.stateNode,e.nodeType===8?Ra(e.parentNode,a):e.nodeType===1&&Ra(e,a),Wr(e)):Ra(ur,a.stateNode));break;case 4:c=ur,O=yo,ur=a.stateNode.containerInfo,yo=!0,sa(e,n,a),ur=c,yo=O;break;case 0:case 11:case 14:case 15:if(!Sr&&(c=a.updateQueue,c!==null&&(c=c.lastEffect,c!==null))){O=c=c.next;do{var M=O,$=M.destroy;M=M.tag,$!==void 0&&(M&2||M&4)&&Xu(a,n,$),O=O.next}while(O!==c)}sa(e,n,a);break;case 1:if(!Sr&&(pi(a,n),c=a.stateNode,typeof c.componentWillUnmount=="function"))try{c.props=a.memoizedProps,c.state=a.memoizedState,c.componentWillUnmount()}catch(X){jn(a,n,X)}sa(e,n,a);break;case 21:sa(e,n,a);break;case 22:a.mode&1?(Sr=(c=Sr)||a.memoizedState!==null,sa(e,n,a),Sr=c):sa(e,n,a);break;default:sa(e,n,a)}}function mc(e){var n=e.updateQueue;if(n!==null){e.updateQueue=null;var a=e.stateNode;a===null&&(a=e.stateNode=new Zc),n.forEach(function(c){var O=sf.bind(null,e,c);a.has(c)||(a.add(c),c.then(O,O))})}}function xo(e,n){var a=n.deletions;if(a!==null)for(var c=0;cO&&(O=$),c&=~M}if(c=O,c=Ye()-c,c=(120>c?120:480>c?480:1080>c?1080:1920>c?1920:3e3>c?3e3:4320>c?4320:1960*_c(c/1960))-c,10e?16:e,la===null)var c=!1;else{if(e=la,la=null,Xs=0,Je&6)throw Error(u(331));var O=Je;for(Je|=4,ye=e.current;ye!==null;){var M=ye,$=M.child;if(ye.flags&16){var X=M.deletions;if(X!==null){for(var dt=0;dtYe()-tl?Ka(e,0):_u|=a),Ur(e,n)}function Rc(e,n){n===0&&(e.mode&1?(n=hn,hn<<=1,!(hn&130023424)&&(hn=4194304)):n=1);var a=wr();e=Qt(e,n),e!==null&&(Bn(e,n,a),Ur(e,a))}function af(e){var n=e.memoizedState,a=0;n!==null&&(a=n.retryLane),Rc(e,a)}function sf(e,n){var a=0;switch(e.tag){case 13:var c=e.stateNode,O=e.memoizedState;O!==null&&(a=O.retryLane);break;case 19:c=e.stateNode;break;default:throw Error(u(314))}c!==null&&c.delete(n),Rc(e,a)}var Mc;Mc=function(n,a,c){if(n!==null)if(n.memoizedProps!==a.pendingProps||gr.current)Br=!0;else{if(!(n.lanes&c)&&!(a.flags&128))return Br=!1,Yc(n,a,c);Br=!!(n.flags&131072)}else Br=!1,xn&&a.flags&1048576&&ui(a,La,a.index);switch(a.lanes=0,a.tag){case 2:var O=a.type;$s(n,a),n=a.pendingProps;var M=Ho(a,nr.current);de(a,c),M=fi(null,a,O,n,M,c);var $=di();return a.flags|=1,typeof M=="object"&&M!==null&&typeof M.render=="function"&&M.$$typeof===void 0?(a.tag=1,a.memoizedState=null,a.updateQueue=null,Ir(O)?($=!0,Ji(a)):$=!1,a.memoizedState=M.state!==null&&M.state!==void 0?M.state:null,Re(a),M.updater=zs,a.stateNode=M,M._reactInternals=a,Uu(a,O,n,c),a=Ku(null,a,O,!0,$,c)):(a.tag=0,xn&&$&&Ns(a),Cr(null,a,M,c),a=a.child),a;case 16:O=a.elementType;t:{switch($s(n,a),n=a.pendingProps,M=O._init,O=M(O._payload),a.type=O,M=a.tag=lf(O),n=go(O,n),M){case 0:a=$u(null,a,O,n,c);break t;case 1:a=nc(null,a,O,n,c);break t;case 11:a=Jl(null,a,O,n,c);break t;case 14:a=ql(null,a,O,go(O.type,n),c);break t}throw Error(u(306,O,""))}return a;case 0:return O=a.type,M=a.pendingProps,M=a.elementType===O?M:go(O,M),$u(n,a,O,M,c);case 1:return O=a.type,M=a.pendingProps,M=a.elementType===O?M:go(O,M),nc(n,a,O,M,c);case 3:t:{if(rc(a),n===null)throw Error(u(387));O=a.pendingProps,$=a.memoizedState,M=$.element,Xe(n,a),Dr(a,O,null,c);var X=a.memoizedState;if(O=X.element,$.isDehydrated)if($={element:O,isDehydrated:!1,cache:X.cache,pendingSuspenseBoundaries:X.pendingSuspenseBoundaries,transitions:X.transitions},a.updateQueue.baseState=$,a.memoizedState=$,a.flags&256){M=vi(Error(u(423)),a),a=oc(n,a,O,c,M);break t}else if(O!==M){M=vi(Error(u(424)),a),a=oc(n,a,O,c,M);break t}else for(Nr=Kr(a.stateNode.containerInfo.firstChild),br=a,xn=!0,Ar=null,c=It(a,null,O,c),a.child=c;c;)c.flags=c.flags&-3|4096,c=c.sibling;else{if(U(),O===M){a=ko(n,a,c);break t}Cr(n,a,O,c)}a=a.child}return a;case 5:return Ba(a),n===null&&g(a),O=a.type,M=a.pendingProps,$=n!==null?n.memoizedProps:null,X=M.children,wa(O,M)?X=null:$!==null&&wa(O,$)&&(a.flags|=32),ec(n,a),Cr(n,a,X,c),a.child;case 6:return n===null&&g(a),null;case 13:return ac(n,a,c);case 4:return no(a,a.stateNode.containerInfo),O=a.pendingProps,n===null?a.child=Mt(a,null,O,c):Cr(n,a,O,c),a.child;case 11:return O=a.type,M=a.pendingProps,M=a.elementType===O?M:go(O,M),Jl(n,a,O,M,c);case 7:return Cr(n,a,a.pendingProps,c),a.child;case 8:return Cr(n,a,a.pendingProps.children,c),a.child;case 12:return Cr(n,a,a.pendingProps.children,c),a.child;case 10:t:{if(O=a.type._context,M=a.pendingProps,$=a.memoizedProps,X=M.value,rn(zt,O._currentValue),O._currentValue=X,$!==null)if(Mr($.value,X)){if($.children===M.children&&!gr.current){a=ko(n,a,c);break t}}else for($=a.child,$!==null&&($.return=a);$!==null;){var dt=$.dependencies;if(dt!==null){X=$.child;for(var Pt=dt.firstContext;Pt!==null;){if(Pt.context===O){if($.tag===1){Pt=en(-1,c&-c),Pt.tag=2;var jt=$.updateQueue;if(jt!==null){jt=jt.shared;var Yt=jt.pending;Yt===null?Pt.next=Pt:(Pt.next=Yt.next,Yt.next=Pt),jt.pending=Pt}}$.lanes|=c,Pt=$.alternate,Pt!==null&&(Pt.lanes|=c),Kt($.return,c,a),dt.lanes|=c;break}Pt=Pt.next}}else if($.tag===10)X=$.type===a.type?null:$.child;else if($.tag===18){if(X=$.return,X===null)throw Error(u(341));X.lanes|=c,dt=X.alternate,dt!==null&&(dt.lanes|=c),Kt(X,c,a),X=$.sibling}else X=$.child;if(X!==null)X.return=$;else for(X=$;X!==null;){if(X===a){X=null;break}if($=X.sibling,$!==null){$.return=X.return,X=$;break}X=X.return}$=X}Cr(n,a,M.children,c),a=a.child}return a;case 9:return M=a.type,O=a.pendingProps.children,de(a,c),M=he(M),O=O(M),a.flags|=1,Cr(n,a,O,c),a.child;case 14:return O=a.type,M=go(O,a.pendingProps),M=go(O.type,M),ql(n,a,O,M,c);case 15:return _l(n,a,a.type,a.pendingProps,c);case 17:return O=a.type,M=a.pendingProps,M=a.elementType===O?M:go(O,M),$s(n,a),a.tag=1,Ir(O)?(n=!0,Ji(a)):n=!1,de(a,c),Hl(a,O,M),Uu(a,O,M,c),Ku(null,a,O,!0,n,c);case 19:return sc(n,a,c);case 22:return tc(n,a,c)}throw Error(u(156,a.tag))};function jc(e,n){return Qe(e,n)}function uf(e,n,a,c){this.tag=e,this.key=a,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=n,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=c,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function ao(e,n,a,c){return new uf(e,n,a,c)}function ul(e){return e=e.prototype,!(!e||!e.isReactComponent)}function lf(e){if(typeof e=="function")return ul(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Q)return 11;if(e===ut)return 14}return 2}function da(e,n){var a=e.alternate;return a===null?(a=ao(e.tag,n,e.key,e.mode),a.elementType=e.elementType,a.type=e.type,a.stateNode=e.stateNode,a.alternate=e,e.alternate=a):(a.pendingProps=n,a.type=e.type,a.flags=0,a.subtreeFlags=0,a.deletions=null),a.flags=e.flags&14680064,a.childLanes=e.childLanes,a.lanes=e.lanes,a.child=e.child,a.memoizedProps=e.memoizedProps,a.memoizedState=e.memoizedState,a.updateQueue=e.updateQueue,n=e.dependencies,a.dependencies=n===null?null:{lanes:n.lanes,firstContext:n.firstContext},a.sibling=e.sibling,a.index=e.index,a.ref=e.ref,a}function qs(e,n,a,c,O,M){var $=2;if(c=e,typeof e=="function")ul(e)&&($=1);else if(typeof e=="string")$=5;else t:switch(e){case K:return Ha(a.children,O,M,n);case k:$=8,O|=8;break;case J:return e=ao(12,a,n,O|2),e.elementType=J,e.lanes=M,e;case ct:return e=ao(13,a,n,O),e.elementType=ct,e.lanes=M,e;case ot:return e=ao(19,a,n,O),e.elementType=ot,e.lanes=M,e;case V:return _s(a,O,M,n);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case G:$=10;break t;case _:$=9;break t;case Q:$=11;break t;case ut:$=14;break t;case W:$=16,c=null;break t}throw Error(u(130,e==null?e:typeof e=="undefined"?"undefined":o(e),""))}return n=ao($,a,n,O),n.elementType=e,n.type=c,n.lanes=M,n}function Ha(e,n,a,c){return e=ao(7,e,c,n),e.lanes=a,e}function _s(e,n,a,c){return e=ao(22,e,c,n),e.elementType=V,e.lanes=a,e.stateNode={isHidden:!1},e}function ll(e,n,a){return e=ao(6,e,null,n),e.lanes=a,e}function cl(e,n,a){return n=ao(4,e.children!==null?e.children:[],e.key,n),n.lanes=a,n.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},n}function cf(e,n,a,c,O){this.tag=n,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Oo(0),this.expirationTimes=Oo(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Oo(0),this.identifierPrefix=c,this.onRecoverableError=O,this.mutableSourceEagerHydrationData=null}function fl(e,n,a,c,O,M,$,X,dt){return e=new cf(e,n,a,X,dt),n===1?(n=1,M===!0&&(n|=8)):n=0,M=ao(3,null,null,n),e.current=M,M.stateNode=e,M.memoizedState={element:c,isDehydrated:a,cache:null,transitions:null,pendingSuspenseBoundaries:null},Re(M),e}function ff(e,n,a){var c=3Et.length)&&(wt=Et.length);for(var Ht=0,ue=new Array(wt);Ht1?Ht-1:0),Se=1;Se/gm),Ct=R(/\${[\w\W]*}/gm),Nt=R(/^data-[\-\w.\u00B7-\uFFFF]/),Wt=R(/^aria-[\-\w]+$/),Ut=R(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ae=R(/^(?:\w+script|data):/i),ie=R(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),te=R(/^html$/i),Ft=R(/^[a-z][.\w]*(-[.\w]+)+$/i),Bt=function(){return typeof window=="undefined"?null:window},At=function(wt,Ht){if(t(wt)!=="object"||typeof wt.createPolicy!="function")return null;var ue=null,Se="data-tt-policy-suffix";Ht.currentScript&&Ht.currentScript.hasAttribute(Se)&&(ue=Ht.currentScript.getAttribute(Se));var xe="dompurify"+(ue?"#"+ue:"");try{return wt.createPolicy(xe,{createHTML:function($t){return $t},createScriptURL:function($t){return $t}})}catch(le){return console.warn("TrustedTypes policy "+xe+" could not be created."),null}};function Xt(){var Et=arguments.length>0&&arguments[0]!==void 0?arguments[0]:Bt(),wt=function(lt){return Xt(lt)};if(wt.version="2.5.3",wt.removed=[],!Et||!Et.document||Et.document.nodeType!==9)return wt.isSupported=!1,wt;var Ht=Et.document,ue=Et.document,Se=Et.DocumentFragment,xe=Et.HTMLTemplateElement,le=Et.Node,$t=Et.Element,Gt=Et.NodeFilter,Me=Et.NamedNodeMap,Ze=Me===void 0?Et.NamedNodeMap||Et.MozNamedAttrMap:Me,tr=Et.HTMLFormElement,fn=Et.DOMParser,Un=Et.trustedTypes,Pn=$t.prototype,cr=tt(Pn,"cloneNode"),rt=tt(Pn,"nextSibling"),xt=tt(Pn,"childNodes"),st=tt(Pn,"parentNode");if(typeof xe=="function"){var yt=ue.createElement("template");yt.content&&yt.content.ownerDocument&&(ue=yt.content.ownerDocument)}var Tt=At(Un,Ht),Lt=Tt?Tt.createHTML(""):"",oe=ue,ne=oe.implementation,Oe=oe.createNodeIterator,qt=oe.createDocumentFragment,Fe=oe.getElementsByTagName,$e=Ht.importNode,ze={};try{ze=V(ue).documentMode?ue.documentMode:{}}catch(ge){}var je={};wt.isSupported=typeof st=="function"&&ne&&ne.createHTMLDocument!==void 0&&ze!==9;var Ve=fe,Ge=Jt,Le=Ct,Cn=Nt,pn=Wt,bn=ae,fr=ie,Pe=Ft,zn=Ut,tn=null,dr=W({},[].concat(s(H),s(Z),s(ft),s(vt),s(bt))),ke=null,Nn=W({},[].concat(s(gt),s(pt),s(Dt),s(ve))),Qe=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),In=null,vr=null,Wn=!0,Ye=!0,ar=!1,En=!0,$n=!1,ir=!0,wn=!1,Yn=!1,nn=!1,dn=!1,Rr=!1,On=!1,Dn=!0,Er=!1,Xr="user-content-",er=!0,hn=!1,pr={},Ln=null,Xo=W({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),io=null,hr=W({},["audio","video","img","source","image","track"]),Eo=null,Oo=W({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Bn="http://www.w3.org/1998/Math/MathML",To="http://www.w3.org/2000/svg",Kn="http://www.w3.org/1999/xhtml",Ke=Kn,Io=!1,zr=null,Qo=W({},[Bn,To,Kn],z),Qr,Ga=["application/xhtml+xml","text/html"],ka="text/html",mn,Or=null,mr=255,Zr=ue.createElement("form"),Tr=function(lt){return h(lt,RegExp)||h(lt,Function)},Jr=function(lt){Or&&Or===lt||((!lt||t(lt)!=="object")&&(lt={}),lt=V(lt),Qr=Ga.indexOf(lt.PARSER_MEDIA_TYPE)===-1?Qr=ka:Qr=lt.PARSER_MEDIA_TYPE,mn=Qr==="application/xhtml+xml"?z:D,tn="ALLOWED_TAGS"in lt?W({},lt.ALLOWED_TAGS,mn):dr,ke="ALLOWED_ATTR"in lt?W({},lt.ALLOWED_ATTR,mn):Nn,zr="ALLOWED_NAMESPACES"in lt?W({},lt.ALLOWED_NAMESPACES,z):Qo,Eo="ADD_URI_SAFE_ATTR"in lt?W(V(Oo),lt.ADD_URI_SAFE_ATTR,mn):Oo,io="ADD_DATA_URI_TAGS"in lt?W(V(hr),lt.ADD_DATA_URI_TAGS,mn):hr,Ln="FORBID_CONTENTS"in lt?W({},lt.FORBID_CONTENTS,mn):Xo,In="FORBID_TAGS"in lt?W({},lt.FORBID_TAGS,mn):{},vr="FORBID_ATTR"in lt?W({},lt.FORBID_ATTR,mn):{},pr="USE_PROFILES"in lt?lt.USE_PROFILES:!1,Wn=lt.ALLOW_ARIA_ATTR!==!1,Ye=lt.ALLOW_DATA_ATTR!==!1,ar=lt.ALLOW_UNKNOWN_PROTOCOLS||!1,En=lt.ALLOW_SELF_CLOSE_IN_ATTR!==!1,$n=lt.SAFE_FOR_TEMPLATES||!1,ir=lt.SAFE_FOR_XML!==!1,wn=lt.WHOLE_DOCUMENT||!1,dn=lt.RETURN_DOM||!1,Rr=lt.RETURN_DOM_FRAGMENT||!1,On=lt.RETURN_TRUSTED_TYPE||!1,nn=lt.FORCE_BODY||!1,Dn=lt.SANITIZE_DOM!==!1,Er=lt.SANITIZE_NAMED_PROPS||!1,er=lt.KEEP_CONTENT!==!1,hn=lt.IN_PLACE||!1,zn=lt.ALLOWED_URI_REGEXP||zn,Ke=lt.NAMESPACE||Kn,Qe=lt.CUSTOM_ELEMENT_HANDLING||{},lt.CUSTOM_ELEMENT_HANDLING&&Tr(lt.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Qe.tagNameCheck=lt.CUSTOM_ELEMENT_HANDLING.tagNameCheck),lt.CUSTOM_ELEMENT_HANDLING&&Tr(lt.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Qe.attributeNameCheck=lt.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),lt.CUSTOM_ELEMENT_HANDLING&&typeof lt.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(Qe.allowCustomizedBuiltInElements=lt.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),$n&&(Ye=!1),Rr&&(dn=!0),pr&&(tn=W({},s(bt)),ke=[],pr.html===!0&&(W(tn,H),W(ke,gt)),pr.svg===!0&&(W(tn,Z),W(ke,pt),W(ke,ve)),pr.svgFilters===!0&&(W(tn,ft),W(ke,pt),W(ke,ve)),pr.mathMl===!0&&(W(tn,vt),W(ke,Dt),W(ke,ve))),lt.ADD_TAGS&&(tn===dr&&(tn=V(tn)),W(tn,lt.ADD_TAGS,mn)),lt.ADD_ATTR&&(ke===Nn&&(ke=V(ke)),W(ke,lt.ADD_ATTR,mn)),lt.ADD_URI_SAFE_ATTR&&W(Eo,lt.ADD_URI_SAFE_ATTR,mn),lt.FORBID_CONTENTS&&(Ln===Xo&&(Ln=V(Ln)),W(Ln,lt.FORBID_CONTENTS,mn)),er&&(tn["#text"]=!0),wn&&W(tn,["html","head","body"]),tn.table&&(W(tn,["tbody"]),delete In.tbody),A&&A(lt),Or=lt)},Ao=W({},["mi","mo","mn","ms","mtext"]),Xn=W({},["foreignobject","annotation-xml"]),Ya=W({},["title","style","font","a","script"]),Po=W({},Z);W(Po,ft),W(Po,nt);var sr=W({},vt);W(sr,mt);var Fo=function(lt){var Vt=st(lt);(!Vt||!Vt.tagName)&&(Vt={namespaceURI:Ke,tagName:"template"});var se=D(lt.tagName),be=D(Vt.tagName);return zr[lt.namespaceURI]?lt.namespaceURI===To?Vt.namespaceURI===Kn?se==="svg":Vt.namespaceURI===Bn?se==="svg"&&(be==="annotation-xml"||Ao[be]):!!Po[se]:lt.namespaceURI===Bn?Vt.namespaceURI===Kn?se==="math":Vt.namespaceURI===To?se==="math"&&Xn[be]:!!sr[se]:lt.namespaceURI===Kn?Vt.namespaceURI===To&&!Xn[be]||Vt.namespaceURI===Bn&&!Ao[be]?!1:!sr[se]&&(Ya[se]||!Po[se]):!!(Qr==="application/xhtml+xml"&&zr[lt.namespaceURI]):!1},Vn=function(lt){L(wt.removed,{element:lt});try{lt.parentNode.removeChild(lt)}catch(Vt){try{lt.outerHTML=Lt}catch(se){lt.remove()}}},Tn=function(lt,Vt){try{L(wt.removed,{attribute:Vt.getAttributeNode(lt),from:Vt})}catch(se){L(wt.removed,{attribute:null,from:Vt})}if(Vt.removeAttribute(lt),lt==="is"&&!ke[lt])if(dn||Rr)try{Vn(Vt)}catch(se){}else try{Vt.setAttribute(lt,"")}catch(se){}},pa=function(lt){var Vt,se;if(nn)lt=""+lt;else{var be=K(lt,/^[\r\n\t ]+/);se=be&&be[0]}Qr==="application/xhtml+xml"&&Ke===Kn&&(lt=''+lt+"");var gn=Tt?Tt.createHTML(lt):lt;if(Ke===Kn)try{Vt=new fn().parseFromString(gn,Qr)}catch(an){}if(!Vt||!Vt.documentElement){Vt=ne.createDocument(Ke,"template",null);try{Vt.documentElement.innerHTML=Io?Lt:gn}catch(an){}}var An=Vt.body||Vt.documentElement;return lt&&se&&An.insertBefore(ue.createTextNode(se),An.childNodes[0]||null),Ke===Kn?Fe.call(Vt,wn?"html":"body")[0]:wn?Vt.documentElement:An},Xa=function(lt){return Oe.call(lt.ownerDocument||lt,lt,Gt.SHOW_ELEMENT|Gt.SHOW_COMMENT|Gt.SHOW_TEXT|Gt.SHOW_PROCESSING_INSTRUCTION|Gt.SHOW_CDATA_SECTION,null,!1)},Co=function(lt){return h(lt,tr)&&(typeof lt.__depth!="undefined"&&typeof lt.__depth!="number"||typeof lt.__removalCount!="undefined"&&typeof lt.__removalCount!="number"||typeof lt.nodeName!="string"||typeof lt.textContent!="string"||typeof lt.removeChild!="function"||!h(lt.attributes,Ze)||typeof lt.removeAttribute!="function"||typeof lt.setAttribute!="function"||typeof lt.namespaceURI!="string"||typeof lt.insertBefore!="function"||typeof lt.hasChildNodes!="function")},Wr=function(lt){return t(le)==="object"?h(lt,le):lt&&t(lt)==="object"&&typeof lt.nodeType=="number"&&typeof lt.nodeName=="string"},Qn=function(lt,Vt,se){je[lt]&&C(je[lt],function(be){be.call(wt,Vt,se,Or)})},$r=function(lt){var Vt;if(Qn("beforeSanitizeElements",lt,null),Co(lt)||_(/[\u0080-\uFFFF]/,lt.nodeName))return Vn(lt),!0;var se=mn(lt.nodeName);if(Qn("uponSanitizeElement",lt,{tagName:se,allowedTags:tn}),lt.hasChildNodes()&&!Wr(lt.firstElementChild)&&(!Wr(lt.content)||!Wr(lt.content.firstElementChild))&&_(/<[/\w]/g,lt.innerHTML)&&_(/<[/\w]/g,lt.textContent)||se==="select"&&_(/