GLOBAL_LIST_EMPTY(preferences_datums) /datum/preferences var/client/parent /// The path to the general savefile for this datum var/path /// 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 /// The maximum number of slots we're allowed to contain var/max_save_slots = 3 /// Bitflags for communications that are muted var/muted = NONE /// Last IP that this client has connected from var/last_ip /// Last CID that this client has connected from var/last_id /// Cached changelog size, to detect new changelogs since last join var/lastchangelog = "" /// List of ROLE_X that the client wants to be eligible for var/list/be_special = list() //Special role selection /// Custom keybindings. Map of keybind names to keyboard inputs. /// For example, by default would have "swap_hands" -> list("X") var/list/key_bindings = list() /// Cached list of keybindings, mapping keys to actions. /// For example, by default would have "X" -> list("swap_hands") var/list/key_bindings_by_key = list() var/toggles = TOGGLES_DEFAULT var/db_flags = NONE var/chat_toggles = TOGGLES_DEFAULT_CHAT var/ghost_form = "ghost" //character preferences var/slot_randomized //keeps track of round-to-round randomization of the character slot, prevents overwriting var/list/randomise = list() //Quirk list var/list/all_quirks = list() //Job preferences 2.0 - indexed by job title , no key or value implies never var/list/job_preferences = list() /// The current window, PREFERENCE_TAB_* in [`code/__DEFINES/preferences.dm`] var/current_window = PREFERENCE_TAB_CHARACTER_PREFERENCES var/unlock_content = 0 var/list/ignoring = list() var/list/exp = list() var/action_buttons_screen_locs = list() ///Someone thought we were nice! We get a little heart in OOC until we join the server past the below time (we can keep it until the end of the round otherwise) var/hearted ///If we have a hearted commendations, we honor it every time the player loads preferences until this time has been passed var/hearted_until ///What outfit typepaths we've favorited in the SelectEquipment menu var/list/favorite_outfits = list() /// A preview of the current character var/atom/movable/screen/map_view/char_preview/character_preview_view /// A list of instantiated middleware var/list/datum/preference_middleware/middleware = list() /// The json savefile for this datum var/datum/json_savefile/savefile /// 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 /datum/preferences/Destroy(force) QDEL_NULL(character_preview_view) QDEL_LIST(middleware) value_cache = null return ..() /datum/preferences/New(client/parent) src.parent = parent for (var/middleware_type in subtypesof(/datum/preference_middleware)) middleware += new middleware_type(src) if(IS_CLIENT_OR_MOCK(parent)) if(is_guest_key(parent.key)) if(parent.is_localhost()) path = DEV_PREFS_PATH // guest + locallost = dev instance, load dev preferences if possible else load_and_save = FALSE // guest + not localhost = guest on live, don't save anything else load_path(parent.ckey) // not guest = load their actual savefile if(load_and_save && !fexists(path)) try_savefile_type_migration() refresh_membership() else CRASH("attempted to create a preferences datum without a client or mock!") load_savefile() // give them default keybinds and update their movement keys key_bindings = deep_copy_list(GLOB.default_hotkeys) key_bindings_by_key = get_key_bindings_by_key(key_bindings) randomise = get_default_randomization() var/loaded_preferences_successfully = load_preferences() if(loaded_preferences_successfully) if(load_character()) return //we couldn't load character data so just randomize the character appearance + name randomise_appearance_prefs() //let's create a random character then - rather than a fat, bald and naked man. if(parent) apply_all_client_preferences() parent.set_macros() if(!loaded_preferences_successfully) save_preferences() save_character() //let's save this new random character so it doesn't keep generating new ones. /datum/preferences/ui_interact(mob/user, datum/tgui/ui) // There used to be code here that readded the preview view if you "rejoined" // I'm making the assumption that ui close will be called whenever a user logs out, or loses a window // If this isn't the case, kill me and restore the code, thanks // We need IconForge and the assets to be ready before allowing the menu to open if(SSearly_assets.initialized != INITIALIZATION_INNEW_REGULAR) return ui = SStgui.try_update_ui(user, src, ui) if(!ui) character_preview_view = create_character_preview_view(user) ui = new(user, src, "PreferencesMenu") ui.set_autoupdate(FALSE) ui.open() character_preview_view.display_to(user, ui.window) /datum/preferences/ui_state(mob/user) return GLOB.always_state // Without this, a hacker would be able to edit other people's preferences if // they had the ref to Topic to. /datum/preferences/ui_status(mob/user, datum/ui_state/state) return user.client == parent ? UI_INTERACTIVE : UI_CLOSE /datum/preferences/ui_data(mob/user) var/list/data = list() 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/ui_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.get_job_type(SSjob.overflow_role).title data["window"] = current_window data["content_unlocked"] = unlock_content for (var/datum/preference_middleware/preference_middleware as anything in middleware) data += preference_middleware.get_ui_static_data(user) return data /datum/preferences/ui_assets(mob/user) var/list/assets = list( get_asset_datum(/datum/asset/spritesheet_batched/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/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) . = ..() if (.) return switch (action) if ("change_slot") // Save existing character save_character() // SAFETY: `switch_to_slot` performs sanitization on the slot number switch_to_slot(params["slot"]) return TRUE if ("remove_current_slot") remove_current_slot() return TRUE if ("rotate") character_preview_view.setDir(turn(character_preview_view.dir, -90)) return TRUE if ("set_preference") var/requested_preference_key = params["preference"] var/value = params["value"] for (var/datum/preference_middleware/preference_middleware as anything in middleware) if (preference_middleware.pre_set_preference(usr, requested_preference_key, value)) return TRUE var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key] if (isnull(requested_preference)) return FALSE // SAFETY: `update_preference` performs validation checks if (!update_preference(requested_preference, value)) return FALSE if (istype(requested_preference, /datum/preference/name)) tainted_character_profiles = TRUE for(var/datum/preference_middleware/preference_middleware as anything in middleware) preference_middleware.post_set_preference(ui.user, requested_preference_key, value) return TRUE if ("set_color_preference") var/requested_preference_key = params["preference"] var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key] if (isnull(requested_preference)) return FALSE if (!istype(requested_preference, /datum/preference/color)) return FALSE var/default_value = read_preference(requested_preference.type) // Yielding var/new_color = input( usr, "Select new color", null, default_value || COLOR_WHITE, ) as color | null if (!new_color) return FALSE if (!update_preference(requested_preference, new_color)) return FALSE return TRUE for (var/datum/preference_middleware/preference_middleware as anything in middleware) var/delegation = preference_middleware.action_delegations[action] if (!isnull(delegation)) return call(preference_middleware, delegation)(params, usr) return FALSE /datum/preferences/ui_close(mob/user) save_character() save_preferences() QDEL_NULL(character_preview_view) /datum/preferences/Topic(href, list/href_list) . = ..() if (.) return if (href_list["open_keybindings"]) current_window = PREFERENCE_TAB_KEYBINDINGS update_static_data(usr) ui_interact(usr) return TRUE /datum/preferences/proc/create_character_preview_view(mob/user) character_preview_view = new(null, src) character_preview_view.generate_view("character_preview_[REF(character_preview_view)]") character_preview_view.update_body() return character_preview_view /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 /// Applies all PREFERENCE_PLAYER preferences /datum/preferences/proc/apply_all_client_preferences() for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) if (preference.savefile_identifier != PREFERENCE_PLAYER) continue value_cache -= preference.type preference.apply_to_client(parent, read_preference(preference.type)) /// A preview of a character for use in the preferences menu /atom/movable/screen/map_view/char_preview name = "character_preview" /// The body that is displayed var/mob/living/carbon/human/dummy/body /// The preferences this refers to var/datum/preferences/preferences /// Whether we show current job clothes or nude/loadout only var/show_job_clothes = TRUE /atom/movable/screen/map_view/char_preview/Initialize(mapload, datum/preferences/preferences) . = ..() src.preferences = preferences /atom/movable/screen/map_view/char_preview/Destroy() QDEL_NULL(body) preferences?.character_preview_view = null preferences = null return ..() /// Updates the currently displayed body /atom/movable/screen/map_view/char_preview/proc/update_body() if (isnull(body)) create_body() else body.wipe_state() appearance = preferences.render_new_preview_appearance(body, show_job_clothes) /atom/movable/screen/map_view/char_preview/proc/create_body() QDEL_NULL(body) body = new /datum/preferences/proc/create_character_profiles() var/list/profiles = list() for (var/index in 1 to max_save_slots) // It won't be updated in the savefile yet, so just read the name directly if (index == default_slot) profiles += read_preference(/datum/preference/name/real_name) continue 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/set_job_preference_level(datum/job/job, level) if (!job) return FALSE if (level == JP_HIGH) var/datum/job/overflow_role = SSjob.overflow_role var/overflow_role_title = initial(overflow_role.title) for(var/other_job in job_preferences) if(job_preferences[other_job] == JP_HIGH) // Overflow role needs to go to NEVER, not medium! if(other_job == overflow_role_title) job_preferences[other_job] = null else job_preferences[other_job] = JP_MEDIUM if(level == null) job_preferences -= job.title else job_preferences[job.title] = level return TRUE /datum/preferences/proc/GetQuirkBalance() var/bal = CONFIG_GET(number/default_quirk_points) for(var/V in all_quirks) var/datum/quirk/T = SSquirks.quirks[V] bal -= initial(T.value) return bal /datum/preferences/proc/GetPositiveQuirkCount() . = 0 for(var/q in all_quirks) if(SSquirks.quirk_points[q] > 0) .++ /datum/preferences/proc/validate_quirks() var/datum/species/species_type = read_preference(/datum/preference/choiced/species) var/list/quirks_removed for(var/quirk_name in all_quirks) var/quirk_path = SSquirks.quirks[quirk_name] var/datum/quirk/quirk_prototype = SSquirks.quirk_prototypes[quirk_path] if(!quirk_prototype.is_species_appropriate(species_type)) all_quirks -= quirk_name LAZYADD(quirks_removed, quirk_name) var/list/feedback if(LAZYLEN(quirks_removed)) LAZYADD(feedback, "The following quirks are incompatible with your species:") LAZYADD(feedback, quirks_removed) if(!CONFIG_GET(flag/disable_quirk_points) && GetQuirkBalance() < 0) LAZYADD(feedback, "Your quirks have been reset.") all_quirks = list() if(LAZYLEN(feedback)) to_chat(parent, boxed_message(span_greentext(feedback.Join("\n")))) /** * Safely read a given preference datum from a given client. * * Reads the given preference datum from the given client, and guards against null client and null prefs. * The client object is fickle and can go null at times, so use this instead of read_preference() if you * want to ensure no runtimes. * * returns client.prefs.read_preference(prefs_to_read) or FALSE if something went wrong. * * Arguments: * * client/prefs_holder - the client to read the pref from * * datum/preference/pref_to_read - the type of preference datum to read. */ /proc/safe_read_pref(client/prefs_holder, datum/preference/pref_to_read) if(!prefs_holder) return FALSE if(prefs_holder && !prefs_holder?.prefs) stack_trace("[prefs_holder?.mob] ([prefs_holder?.ckey]) had null prefs, which shouldn't be possible!") return FALSE return prefs_holder?.prefs.read_preference(pref_to_read) /** * Get the given client's chat toggle prefs. * * Getter function for prefs.chat_toggles which guards against null client and null prefs. * The client object is fickle and can go null at times, so use this instead of directly accessing the var * if you want to ensure no runtimes. * * returns client.prefs.chat_toggles or FALSE if something went wrong. * * Arguments: * * client/prefs_holder - the client to get the chat_toggles pref from. */ /proc/get_chat_toggles(client/target) if(ismob(target)) var/mob/target_mob = target target = target_mob.client if(isnull(target)) return NONE var/datum/preferences/preferences = target.prefs if(isnull(preferences)) stack_trace("[key_name(target)] preference datum was null") return NONE return preferences.chat_toggles /// Sanitizes the preferences, applies the randomization prefs, and then applies the preference to the human mob. /datum/preferences/proc/safe_transfer_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE, is_antag = FALSE) apply_character_randomization_prefs(is_antag) apply_prefs_to(character, icon_updates) /// Applies the given preferences to a human mob. /datum/preferences/proc/apply_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE) character.dna.features = list() 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)) character.dna.real_name = character.real_name if(icon_updates) character.icon_render_keys = list() character.update_body(is_creating = TRUE) SEND_SIGNAL(character, COMSIG_HUMAN_PREFS_APPLIED) /// Returns whether the parent mob should have the random hardcore settings enabled. Assumes it has a mind. /datum/preferences/proc/should_be_random_hardcore(datum/job/job, datum/mind/mind) if(!read_preference(/datum/preference/toggle/random_hardcore)) return FALSE if(job.job_flags & JOB_HEAD_OF_STAFF) //No heads of staff return FALSE for(var/datum/antagonist/antag as anything in mind.antag_datums) if(antag.get_team()) //No team antags return FALSE return TRUE /// Inverts the key_bindings list such that it can be used for key_bindings_by_key /datum/preferences/proc/get_key_bindings_by_key(list/key_bindings) var/list/output = list() for (var/action in key_bindings) for (var/key in key_bindings[action]) LAZYADD(output[key], action) return output /// Returns the default `randomise` variable ouptut /datum/preferences/proc/get_default_randomization() var/list/default_randomization = list() for (var/preference_key in GLOB.preference_entries_by_key) var/datum/preference/preference = GLOB.preference_entries_by_key[preference_key] if (preference.is_randomizable() && preference.randomize_by_default) default_randomization[preference_key] = RANDOM_ENABLED return default_randomization /datum/preferences/proc/refresh_membership() var/byond_member = parent.IsByondMember() if(isnull(byond_member)) // Connection failure, retry once byond_member = parent.IsByondMember() var/static/admins_warned = FALSE if(!admins_warned) admins_warned = TRUE message_admins("BYOND membership lookup had a connection failure for a user. This is most likely an issue on the BYOND side but if this consistently happens you should bother your server operator to look into it.") if(isnull(byond_member)) // Retrying didn't work, warn the user log_game("BYOND membership lookup for [parent.ckey] failed due to a connection error.") else log_game("BYOND membership lookup for [parent.ckey] failed due to a connection error but succeeded after retry.") if(isnull(byond_member)) to_chat(parent, span_warning("There's been a connection failure while trying to check the status of your BYOND membership. Reconnecting may fix the issue, or BYOND could be experiencing downtime.")) unlock_content = !!byond_member if(unlock_content) max_save_slots = 8