reworks game preferences (characters v2 staging) (#6310)

Co-authored-by: silicons <no@you.cat>
Co-authored-by: LordME <58342752+TheLordME@users.noreply.github.com>
This commit is contained in:
silicons
2024-04-08 12:44:30 +02:00
committed by GitHub
co-authored by silicons LordME
parent 9f1b911556
commit 39bc14bb4a
427 changed files with 3981 additions and 2842 deletions
+4
View File
@@ -96,6 +96,10 @@
/// menu group statuses
var/list/menu_group_status = list()
//? Preferences
/// client preferences
var/datum/game_preferences/preferences
//? Statpanel
/// statpanel tab ; can be null (e.g. we're looking at verb tabs)
var/statpanel_tab
+24 -9
View File
@@ -198,9 +198,21 @@
// resolve persistent data
persistent = resolve_client_data(ckey, key)
//* Resolve database data
player = new(key)
player = resolve_player_data(ckey, key)
player.log_connect()
// todo: move preferences up here but above persistent
//* Resolve preferences
preferences = SSpreferences.resolve_game_preferences(key, ckey)
preferences.active = src
preferences.on_reconnect()
//? WARNING: SHITCODE ALERT ?//
// We allow a client/New sleep because preferences is currently required for
// everything else to work
// todo: maybe don't do this?
if(!UNLINT(preferences.block_on_initialized(5 SECONDS)))
security_kick("A fatal error occurred while attempting to load: preferences not initialized. Please notify a coder.")
stack_trace("we just kicked a client due to prefs not loading; something is horribly wrong!")
return
//? END ?//
//* Setup user interface
// todo: move top level menu here, for now it has to be under prefs.
@@ -328,11 +340,12 @@
pre_init_viewport()
mob.reload_rendering()
if(prefs.lastchangelog != GLOB.changelog_hash) //bolds the changelog button on the interface so we know there are updates.
to_chat(src, "<span class='info'>You have unread updates in the changelog.</span>")
winset(src, "infowindow.changelog", "background-color=#eaeaea;font-style=bold")
if(config_legacy.aggressive_changelog)
changelog_async()
// todo: this is dead because changelog is dead but we should fix it tbvqh
// if(prefs.lastchangelog != GLOB.changelog_hash) //bolds the changelog button on the interface so we know there are updates.
// to_chat(src, "<span class='info'>You have unread updates in the changelog.</span>")
// winset(src, "infowindow.changelog", "background-color=#eaeaea;font-style=bold")
// if(config_legacy.aggressive_changelog)
// changelog_async()
// ensure asset cache is there
INVOKE_ASYNC(src, PROC_REF(warn_if_no_asset_cache_browser))
@@ -383,9 +396,11 @@
// log
log_access("Logout: [key_name(src)]")
// unreference storage datums
prefs = null
persistent = null
player = null
if(preferences)
preferences.active = null
preferences = null
//* unsorted
GLOB.ahelp_tickets.ClientLogout(src)
@@ -512,7 +527,7 @@
if(ab) //Citadel edit, things with stuff.
return
if (prefs.hotkeys)
if (preferences.is_hotkeys_mode())
// If hotkey mode is enabled, then clicking the map will automatically
// unfocus the text bar. This removes the red color from the text bar
// so that the visual focus indicator matches reality.
+6 -2
View File
@@ -1,3 +1,6 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2023 Citadel Station developers. *//
GLOBAL_LIST_EMPTY(client_data)
/proc/resolve_client_data(ckey, key)
@@ -65,7 +68,7 @@ GLOBAL_LIST_EMPTY(client_data)
log_shadowban("[ckey] autobanned based on [why].")
message_admins(SPAN_DANGER("Automatically shadowbanning [ckey] based on configuration (matched on [why]). Varedit client.persistent.ligma to change this."))
/datum/client_data/proc/block_on_account_age_loaded(timeout = INFINITY)
/datum/client_data/proc/block_on_account_age_loaded(timeout = 10 SECONDS)
var/timed_out = world.time + timeout
UNTIL(!isnull(account_age) || world.time > timed_out)
return account_age
@@ -143,7 +146,8 @@ GLOBAL_LIST_EMPTY(client_data)
playtime_loaded = TRUE
playtime_mutex = FALSE
/datum/client_data/proc/block_on_playtime_loaded(timeout = INFINITY)
/datum/client_data/proc/block_on_playtime_loaded(timeout = 10 SECONDS)
var/timed_out = world.time + timeout
load_playtime()
UNTIL(playtime_loaded || world.time > timed_out)
return playtime_loaded
+31 -11
View File
@@ -1,4 +1,16 @@
// todo: this has to be globally cached for new admin panel
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2023 Citadel Station developers. *//
GLOBAL_LIST_EMPTY(player_data)
/proc/resolve_player_data(ckey, key)
ckey = ckey(ckey) // just in case
if(!islist(GLOB.player_data))
// we CANNOT RUNTIME
GLOB.player_data = list()
if(!istype(GLOB.player_data[ckey], /datum/player_data))
GLOB.player_data[ckey] = new /datum/player_data(ckey, key)
return GLOB.player_data[ckey]
/**
* holds db-related data
@@ -31,9 +43,12 @@
var/player_age
/// join date
var/player_first_seen
/// arbitrary JSON-serializable key-value data
/// you must handle errors yourself! we won't handle it for you.
var/list/player_misc
/datum/player_data/New(key)
src.ckey = ckey(key)
/datum/player_data/New(ckey, key)
src.ckey = ckey
if(!src.ckey)
return
src.key = key
@@ -94,7 +109,7 @@
lookup_pid = text2num(lookup_pid)
qdel(lookup)
lookup = SSdbcore.ExecuteQuery(
"SELECT id, flags, datediff(Now(), firstseen), firstseen FROM [format_table_name("player")] WHERE id = :id",
"SELECT id, flags, datediff(Now(), firstseen), firstseen, misc FROM [format_table_name("player")] WHERE id = :id",
list(
"id" = lookup_pid
)
@@ -111,6 +126,7 @@
player_flags = lookup_flags
player_first_seen = lookup.item[4]
player_age = lookup_age
player_misc = safe_json_decode(lookup.item[5])
qdel(lookup)
available = TRUE
else
@@ -125,21 +141,24 @@
// new person!
player_age = 0
player_flags = NONE
player_misc = list()
var/datum/db_query/insert
if(migrate_firstseen)
insert = SSdbcore.ExecuteQuery(
"INSERT INTO [format_table_name("player")] (flags, firstseen, lastseen) VALUES (:flags, :fs, Now())",
"INSERT INTO [format_table_name("player")] (flags, firstseen, lastseen, misc) VALUES (:flags, :fs, Now(), :misc)",
list(
"flags" = player_flags,
"fs" = migrate_firstseen
"fs" = migrate_firstseen,
"misc" = safe_json_encode(player_misc),
)
)
player_first_seen = migrate_firstseen
else
insert = SSdbcore.ExecuteQuery(
"INSERT INTO [format_table_name("player")] (flags, firstseen, lastseen) VALUES (:flags, Now(), Now())",
"INSERT INTO [format_table_name("player")] (flags, firstseen, lastseen, misc) VALUES (:flags, Now(), Now(), :misc)",
list(
"flags" = player_flags,
"misc" = safe_json_encode(player_misc),
)
)
player_first_seen = time_stamp()
@@ -190,10 +209,11 @@
/datum/player_data/proc/_save()
qdel(SSdbcore.ExecuteQuery(
"UPDATE [format_table_name("player")] SET flags = :flags WHERE id = :id",
"UPDATE [format_table_name("player")] SET flags = :flags, misc = :misc WHERE id = :id",
list(
"flags" = player_flags,
"id" = player_id
"id" = player_id,
"misc" = safe_json_encode(player_misc, "{}"),
)
))
@@ -211,7 +231,7 @@
qdel(SSdbcore.ExecuteQuery(
"UPDATE [format_table_name("player")] SET lastseen = Now() WHERE id = :id",
list(
"id" = player_id
"id" = player_id,
)
))
return TRUE
@@ -229,7 +249,7 @@
*
* WARNING: without database, or if this is for a guest key, we will never be available.
*/
/datum/player_data/proc/block_on_available(timeout = INFINITY)
/datum/player_data/proc/block_on_available(timeout = 10 SECONDS)
var/timed_out = world.time + timeout
UNTIL(!isnull(available) || world.time > timed_out)
return available
@@ -0,0 +1,73 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//
/datum/game_preference_entry/dropdown/hud_style
name = "HUD Style"
description = "Set the style of your inventory / game HUD"
key = "hud_style"
category = GAME_PREFERENCE_CATEGORY_GAME
subcategory = "HUD Overlay"
legacy_savefile_key = "UI_style"
/datum/game_preference_entry/dropdown/hud_style/New()
options = list()
for(var/key in all_ui_styles)
options += key
..()
/datum/game_preference_entry/dropdown/hud_style/on_set(client/user, value, first_init)
. = ..()
user.set_ui_style(value)
/datum/game_preference_entry/simple_color/hud_color
name = "HUD Color"
description = "Manually recolor your inventory / game HUD"
key = "hud_color"
category = GAME_PREFERENCE_CATEGORY_GAME
subcategory = "HUD Overlay"
default_value = "#ffffff"
legacy_savefile_key = "UI_style_color"
/datum/game_preference_entry/simple_color/hud_color/on_set(client/user, value, first_init)
. = ..()
user.set_ui_color(value)
/datum/game_preference_entry/number/hud_alpha
name = "HUD Alpha"
description = "Manually set the transparency of your inventory / game HUD"
key = "hud_alpha"
category = GAME_PREFERENCE_CATEGORY_GAME
subcategory = "HUD Overlay"
default_value = 255
min_value = 0
max_value = 255
round_to_nearest = 1
legacy_savefile_key = "UI_style_alpha"
/datum/game_preference_entry/number/hud_alpha/on_set(client/user, value, first_init)
. = ..()
user.set_ui_alpha(value)
/datum/game_preference_entry/dropdown/tooltip_style
name = "Tooltips Style"
description = "Set the HUD style of pop up tooltips."
key = "tooltip_style"
category = GAME_PREFERENCE_CATEGORY_GAME
subcategory = "Tooltips"
legacy_savefile_key = "tooltipstyle"
/datum/game_preference_entry/dropdown/tooltip_style/New()
options = all_tooltip_styles.Copy()
..()
/datum/game_preference_entry/simple_color/admin_ooc_color
name = "OOC Color (Admin)"
description = "Choose your OOC color. Do not make it too eye-searing. Set to #000001 or reset to default to sync with server color."
key = "admin_ooc_color"
category = GAME_PREFERENCE_CATEGORY_GAME
subcategory = "OOC"
default_value = "#000001"
legacy_savefile_key = "ooccolor"
/datum/game_preference_entry/simple_color/admin_ooc_color/is_visible(client/user)
return check_rights(C = user, show_msg = FALSE) && CONFIG_GET(flag/allow_admin_ooccolor)
@@ -0,0 +1,42 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//
/datum/game_preference_entry/toggle/parallax
name = "Parallax Enabled"
description = "Render space via parallax. This should be kept on."
key = "enable_parallax"
category = GAME_PREFERENCE_CATEGORY_GRAPHICS
subcategory = "Parallax"
default_value = TRUE
/datum/game_preference_entry/toggle/parallax/on_set(client/user, value, first_init)
. = ..()
user.parallax_holder?.reset()
/datum/game_preference_entry/toggle/ambient_occlusion
name = "Fake Ambient Occlusion"
description = "Fake, filter ambient occlusion. This should be kept off."
key = "fake_ambient_occlusion"
category = GAME_PREFERENCE_CATEGORY_GRAPHICS
subcategory = "Rendering"
default_value = FALSE
/datum/game_preference_entry/toggle/ambient_occlusion/on_set(client/user, value, first_init)
. = ..()
user.using_perspective?.planes?.sync_owner(user)
/datum/game_preference_entry/number/fps
name = "FPS"
description = "Client rendering FPS. It is recommended to set this to a multiple of the server's tickrate. 0 to sync with server."
key = "client_fps"
category = GAME_PREFERENCE_CATEGORY_GRAPHICS
subcategory = "Rendering"
default_value = 0
min_value = 0
max_value = 180
round_to_nearest = 1
legacy_savefile_key = "client_fps"
/datum/game_preference_entry/number/fps/on_set(client/user, value, first_init)
. = ..()
user.fps = value
@@ -0,0 +1,149 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//
/datum/game_preference_entry
abstract_type = /datum/game_preference_entry
var/name = "-- broken entry --"
var/description = "A preference entry."
/// Must be unique
var/key
var/category = "Misc"
var/subcategory = "Misc"
/// priority - higher means it appears first. only valid within the same category.
var/priority = 0
/// default value
var/default_value
/// legacy import id - set if it's using new global prefs system
var/legacy_global_key
/// legacy import id - set if it's using old savefile direct write
var/legacy_savefile_key
/datum/game_preference_entry/proc/default_value(client/user)
return default_value
/datum/game_preference_entry/proc/is_visible(client/user, silent)
return TRUE
/**
* called when a value is changed with a client active
*
* @params
* * user - active client
* * value - current (sanitized) value
* * first_init - are we being called as a client is first being associated to a preferences datum, or on first load?
*/
/datum/game_preference_entry/proc/on_set(client/user, value, first_init)
return
/datum/game_preference_entry/proc/filter_value(value)
return value
/datum/game_preference_entry/proc/migrate_legacy_data(data)
return data
/datum/game_preference_entry/proc/tgui_preference_schema()
return list(
"key" = key,
"category" = category,
"subcategory" = subcategory,
"name" = name,
"desc" = description,
"priority" = priority,
"defaultValue" = default_value,
)
/datum/game_preference_entry/number
abstract_type = /datum/game_preference_entry/number
default_value = 0
/// optional
var/min_value
/// optional
var/max_value
/// optional
var/round_to_nearest
/datum/game_preference_entry/number/filter_value(value)
. = isnum(value)? clamp(value, min_value, max_value) : default_value
if(!isnull(.) && round_to_nearest)
. = round(., round_to_nearest)
/datum/game_preference_entry/number/tgui_preference_schema()
return ..() | list(
"type" = "number",
"minValue" = min_value,
"maxValue" = max_value,
"round_to" = round_to_nearest,
)
/datum/game_preference_entry/string
abstract_type = /datum/game_preference_entry/string
default_value = ""
/// mandatory
var/min_length = 0
/// mandatory
var/max_length = 64
/datum/game_preference_entry/string/filter_value(value)
. = "[value]"
return copytext_char(., 1, min(length_char(.) + 1, max_length + 1))
/datum/game_preference_entry/string/tgui_preference_schema()
return ..() | list(
"type" = "string",
"minLength" = min_length,
"maxLength" = max_length,
)
/datum/game_preference_entry/toggle
abstract_type = /datum/game_preference_entry/toggle
default_value = TRUE
/// mandatory
var/enabled_name = "On"
/// mandatory
var/disabled_name = "Off"
/datum/game_preference_entry/toggle/filter_value(value)
return !!value
/datum/game_preference_entry/toggle/tgui_preference_schema()
return ..() | list(
"type" = "toggle",
"enabledName" = enabled_name,
"disabledName" = disabled_name,
)
/datum/game_preference_entry/dropdown
abstract_type = /datum/game_preference_entry/dropdown
default_value = null
/// entries must be strings
var/list/options = list()
/datum/game_preference_entry/dropdown/New()
if(isnull(default_value) && length(options))
default_value = options[1]
/datum/game_preference_entry/dropdown/filter_value(value)
return (value in options)? value : ((length(options) && options[1]) || null)
/datum/game_preference_entry/dropdown/tgui_preference_schema()
return ..() | list(
"type" = "dropdown",
"options" = options,
)
/datum/game_preference_entry/simple_color
abstract_type = /datum/game_preference_entry/simple_color
default_value = "#ffffff"
/datum/game_preference_entry/simple_color/filter_value(value)
return sanitize_hexcolor(
value,
desired_format = 6,
include_crunch = TRUE,
default = default_value,
)
/datum/game_preference_entry/simple_color/tgui_preference_schema()
return ..() | list(
"type" = "simpleColor",
)
@@ -0,0 +1,47 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//
/// key value id = middleware instance
GLOBAL_LIST_INIT(game_preference_middleware, init_game_preference_middleware())
/proc/init_game_preference_middleware()
var/list/keyed_middleware = list()
for(var/datum/game_preference_middleware/middleware as anything in subtypesof(/datum/game_preference_middleware))
middleware = new middleware
keyed_middleware[middleware.key] = middleware
GLOB.game_preference_middleware = keyed_middleware
return keyed_middleware
/datum/game_preference_middleware
/// category name
var/name
/// key
var/key
/**
* return TRUE to stop handling
*
* we assume the 'usr' has been validated by the prefs datum!
*/
/datum/game_preference_middleware/proc/handle_topic(datum/game_preferences/prefs, action, list/params)
return FALSE
/**
* when we want to reset whatever this handles
*/
/datum/game_preference_middleware/proc/handle_reset(datum/game_preferences/prefs)
prefs.mark_dirty()
return
/**
* sanitize everything
*/
/datum/game_preference_middleware/proc/handle_sanitize(datum/game_preferences/prefs)
prefs.mark_dirty()
return
/**
* on initial load
*/
/datum/game_preference_middleware/proc/on_initial_load(datum/game_preferences/prefs)
return
@@ -0,0 +1,270 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//
/datum/game_preference_toggle
abstract_type = /datum/game_preference_toggle
var/name = "A Toggle"
var/description = "Someone fucked up"
var/enabled_name = "On"
var/disabled_name = "Off"
/// must be unique
var/key
var/category = "Misc"
/// priority - higher means it appears first. only valid within the same category.
var/priority = 0
/// legacy import id
var/legacy_key
var/default_value = TRUE
/datum/game_preference_toggle/proc/toggled(client/user, state)
return
/datum/game_preference_toggle/proc/is_visible(client/user)
return TRUE
/datum/game_preference_toggle/proc/tgui_preference_schema()
return list(
"key" = key,
"category" = category,
"priority" = priority,
"default" = default_value,
"name" = name,
"desc" = description,
"enabled" = enabled_name,
"disabled" = disabled_name,
)
/datum/game_preference_toggle/admin
abstract_type = /datum/game_preference_toggle/admin
category = "Admin"
/datum/game_preference_toggle/admin/is_visible(client/user, silent)
return check_rights(C = user, show_msg = FALSE)
/datum/game_preference_toggle/admin/global_looc
name = "Remote LOOC Sight"
description = "See all LOOC, regardless of location."
key = "admin_global_looc"
default_value = TRUE
legacy_key = "CHAT_RLOOC"
/datum/game_preference_toggle/admin/obfuscate_stealth_dsay
name = "Obfuscate Stealthmin Dsay"
description = "Randomize ckey to a list of joke ckeys when dsay'ing while stealthed."
key = "admin_obfuscate_stealth_dsay"
default_value = TRUE
legacy_key = "OBFUSCATE_STEALTH_DSAY"
/datum/game_preference_toggle/admin/stealth_hides_ghost
name = "Stealthmin Hides Ghost"
description = "Hide your ghost while stealthed."
key = "admin_hide_stealth_ghost"
default_value = TRUE
/datum/game_preference_toggle/chat
abstract_type = /datum/game_preference_toggle/chat
category = "Chat"
/datum/game_preference_toggle/chat/ooc
name = "OOC enabled"
description = "Toggles whether you see OOC (Out of Character) chat."
legacy_key = "CHAT_OOC"
key = "toggle_ooc"
/datum/game_preference_toggle/chat/looc
name = "LOOC enabled"
description = "Toggles whether you see LOOC (Local Out of Character) chat."
legacy_key = "CHAT_LOOC"
key = "toggle_looc"
/datum/game_preference_toggle/chat/dsay
name = "Deadchat enabled"
description = "Toggles whether you see dead chat."
legacy_key = "CHAT_DEAD"
key = "toggle_deadchat"
/datum/game_preference_toggle/chat/legacy_chat_tags
name = "Chat Tags"
description = "Legacy chat visual system for tags to the left of messages; you should likely keep this on."
key = "legacy_chat_tags"
default_value = TRUE
legacy_key = "CHAT_SHOWICONS"
/datum/game_preference_toggle/chat/language_indicators
name = "Language Indicators"
description = "Show language indicators in the chat when you hear a known language."
key = "language_indicators"
default_value = TRUE
legacy_key = "LANGUAGE_INDICATOR"
/datum/game_preference_toggle/game
abstract_type = /datum/game_preference_toggle/game
category = "Game"
/datum/game_preference_toggle/game/mob_tooltips
name = "Mob Tooltips"
key = "mob_tooltips"
legacy_key = "MOB_TOOLTIPS"
/datum/game_preference_toggle/game/overhead_chat
name = "Overhead Chat"
key = "runechat"
legacy_key = "OVERHEAD_CHAT"
description = "Enable rendering messages sent by mobs over their icon on the map."
/datum/game_preference_toggle/game/help_intent_firing
name = "Help Intent Gun Safety"
description = "If on, do not allow firing weapons in help intent."
key = "help_intent_gun_safety"
legacy_key = "HELP_INTENT_SAFETY"
default_value = FALSE
/datum/game_preference_toggle/game/legacy_name_highlight
name = "Emphasize Name Mentions"
description = "Enlargen messages calling you by name."
key = "legacy_name_mention"
legacy_key = "CHAT_MENTION"
/datum/game_preference_toggle/ambience
abstract_type = /datum/game_preference_toggle/ambience
category = "Sound - Ambience"
/datum/game_preference_toggle/ambience/area_ambience
name = "Area Ambience"
key = "area_ambience"
default_value = TRUE
/datum/game_preference_toggle/ambience/supermatter_hum
name = "Supermatter Hum"
key = "supermatter_ambience"
legacy_key = "SOUND_SUPERMATTER"
default_value = TRUE
/datum/game_preference_toggle/ambience/weather
name = "Weather Sounds"
key = "weather_ambience"
legacy_key = "SOUND_WEATHER"
default_value = TRUE
/datum/game_preference_toggle/ambience/atmospherics
name = "Atmos Machinery Sounds"
key = "atmos_machine_ambience"
default_value = TRUE
legacy_key = "SOUND_AIRPUMP"
/datum/game_preference_toggle/sfx
abstract_type = /datum/game_preference_toggle/sfx
category = "Sound - Effects"
/datum/game_preference_toggle/sfx/instruments
name = "Instruments"
description = "Play sounds from in-game instruments."
key = "instrument_music"
legacy_key = "SOUND_INSTRUMENT"
/datum/game_preference_toggle/music
abstract_type = /datum/game_preference_toggle/music
category = "Sound - Music"
/datum/game_preference_toggle/music/lobby
name = "Lobby Music"
description = "Play music in the lobby."
key = "lobby_music"
legacy_key = "SOUND_LOBBY"
/datum/game_preference_toggle/music/lobby/toggled(client/user, state)
if(state)
user.playtitlemusic()
else
user.media?.stop_music()
/datum/game_preference_toggle/music/admin
name = "Admin Music"
description = "Play admin MIDIs (or really just music nowadays)."
key = "admin_music"
legacy_key = "SOUND_MIDI"
/datum/game_preference_toggle/music/jukebox
name = "Jukebox Music"
description = "Play in-game jukebox music."
key = "jukebox_music"
legacy_key = "SOUND_JUKEBOX"
/datum/game_preference_toggle/music/jukebox/toggled(client/user, state)
// todo: jukebox / music rework
if(state)
user.mob?.update_music()
else
user.mob?.stop_all_music()
// todo: this should probably not be in toggles
/datum/game_preference_toggle/vore_sounds
abstract_type = /datum/game_preference_toggle/vore_sounds
category = "Sound - Vore"
/datum/game_preference_toggle/vore_sounds/eating_noises
name = "Eating Noises"
key = "vore_eating_sounds"
default_value = FALSE
legacy_key = "EATING_NOISES"
/datum/game_preference_toggle/vore_sounds/digestion_noises
name = "Digestion Noises"
key = "vore_digestion_sounds"
default_value = FALSE
legacy_key = "DIGEST_NOISES"
/datum/game_preference_toggle/observer
abstract_type = /datum/game_preference_toggle/observer
category = "Observer"
/datum/game_preference_toggle/observer/ghost_ears
name = "Hear All Speech"
description = "Hear all speech, regardless of distance"
key = "ghost_ears"
default_value = TRUE
legacy_key = "CHAT_GHOSTEARS"
/datum/game_preference_toggle/observer/ghost_sight
name = "See All Emotes"
description = "See all emotes, regardless of distance"
key = "ghost_sight"
default_value = TRUE
legacy_key = "CHAT_GHOSTSIGHT"
/datum/game_preference_toggle/observer/ghost_radio
name = "Hear All Radio"
description = "Hear all radio chatter, regardless of distance"
key = "ghost_radio"
default_value = TRUE
legacy_key = "CHAT_GHOSTRADIO"
/datum/game_preference_toggle/observer/ghost_subtle
name = "See Subtle Emotes"
description = "See subtle emotes"
key = "ghost_subtle"
default_value = TRUE
legacy_key = "SUBTLE_SEE"
/datum/game_preference_toggle/presence
abstract_type = /datum/game_preference_toggle/presence
category = "Presence"
/datum/game_preference_toggle/presence/anonymous_ghost_chat
name = "Anonymous Ghost Chat"
description = "Hide your ckey when speaking in deadchat."
key = "hide_ckey_from_deadchat"
default_value = TRUE
/datum/game_preference_toggle/presence/show_advanced_who
name = "Show Status in Advanced Who"
description = "Show your playing status in advanced who."
key = "advanced_who_status"
default_value = FALSE
/datum/game_preference_toggle/presence/announce_ghost_joinleave
name = "Announce Observer Join/Leave"
description = "Announce to deadchat when you enter or exit deadchat."
key = "announce_ghost_joinleave"
default_value = FALSE
@@ -0,0 +1,609 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//
/**
* Game preferences
*
* Game prefs don't need an init order because unlike character setup, there's no dependencies, in theory.
*/
/datum/game_preferences
//* Loading *//
/// loaded?
var/initialized = FALSE
//* Preferences *//
/// preferences by key - key = value
var/list/entries_by_key
/// arbitrary key access used by middleware
var/list/misc_by_key
// todo: move menu options in here and not from /datum/preferences
//* Middleware - Keybindings *//
/// keybindings - key to list of keybinds
var/list/keybindings
//* Middleware - Toggles *//
/// toggles by key - TRUE or FALSE
var/list/toggles_by_key
//* System *//
/// were we originally sql loaded?
/// used to determine if sql is authoritative when sql comes back
///
/// - if sql was originally loaded and sql comes back, we are still authoritative
/// - if sql wasn't originally loaded and sql comes back and a sql save exists, we are overwritten
/// - if a save doesn't exist, we are authoritative and are flushed to sql
var/authoritatively_loaded_by_sql = FALSE
/// something fucky wucky happened and the next time sql comes back,
/// we need to flush data and assert authoritativeness
var/sql_state_desynced = FALSE
/// our player's ckey
var/ckey
/// we are a guest
var/is_guest = FALSE
/// our active client
var/client/active
/// our player's id
///
/// set upon successful sql save
/// we will try to load a sql preferences of a given playerid
/// when we load from sql
var/authoritative_player_id
/// our prefs version
var/version = GAME_PREFERENCES_VERSION_CURRENT
/// are we saved? if TRUE, we have modified vars
var/is_dirty = FALSE
/datum/game_preferences/New(key, ckey)
src.ckey = ckey
src.is_guest = IsGuestKey(key)
//* Init *//
/datum/game_preferences/proc/initialize()
perform_initial_load()
initialized = TRUE
/datum/game_preferences/proc/on_reconnect()
if(!initialized)
return
initialize_client()
/datum/game_preferences/proc/block_on_initialized(timeout = 10 SECONDS)
var/wait_until = world.time + timeout
UNTIL(initialized || (world.time >= wait_until))
if(!initialized)
. = FALSE
CRASH("block_on_initialize timeout")
return initialized
/datum/game_preferences/proc/initialize_client()
if(isnull(active))
return
for(var/key in SSpreferences.entries_by_key)
var/datum/game_preference_entry/entry = SSpreferences.entries_by_key[key]
var/value = entries_by_key[entry.key]
entry.on_set(active, value, TRUE)
for(var/key in GLOB.game_preference_middleware)
var/datum/game_preference_middleware/middleware = GLOB.game_preference_middleware[key]
middleware.on_initial_load(src)
/datum/game_preferences/proc/oops_sql_came_back_perform_a_reload()
// if we were sql loaded, don't desync from sql
if(authoritatively_loaded_by_sql)
if(!sql_state_desynced)
return
if(active)
to_chat(active, SPAN_BOLDANNOUNCE("The server's SQL database has reconnected and your preferences were changed during the lapse. Your preferences has been automatically flushed to database."))
save_to_sql()
return
// load from sql if we can; SQL is authoritative
if(load_from_sql())
if(active)
to_chat(active, SPAN_BOLDANNOUNCE("The server's SQL database has reconnected and your preferences were found to be fully desynced from the copy in the database. Your preferences has been automatically reloaded from the database. Please ensure all settings are workable."))
return
// otherwise, save our current changes to SQL
save_to_sql()
if(active)
to_chat(active, SPAN_BOLDANNOUNCE("The server's SQL database has reconnected and your preferences were not found in them. Your preferences have been automatically saved to database."))
/datum/game_preferences/proc/perform_legacy_migration()
if(is_guest)
return FALSE
if(!fexists("data/player_saves/[copytext(ckey, 1, 2)]/[ckey]/preferences.sav"))
return FALSE
var/savefile/legacy_savefile = new /savefile("data/player_saves/[copytext(ckey, 1, 2)]/[ckey]/preferences.sav")
var/list/legacy_options
legacy_savefile["global"] >> legacy_options
if(isnull(legacy_options))
legacy_options = list()
// we are fired after reset, but before save
// we assume lists are init'd
for(var/key in SSpreferences.entries_by_key)
var/datum/game_preference_entry/entry = SSpreferences.entries_by_key[key]
var/migrated_value
if(entry.legacy_global_key)
migrated_value = legacy_options[entry.legacy_global_key]
else if(entry.legacy_savefile_key)
legacy_savefile[entry.legacy_savefile_key] >> migrated_value
if(!isnull(migrated_value))
migrated_value = entry.filter_value(migrated_value)
entries_by_key[key] = migrated_value
var/list/old_toggles
legacy_savefile["preferences"] >> old_toggles
for(var/key in SSpreferences.toggles_by_key)
var/datum/game_preference_toggle/toggle = SSpreferences.toggles_by_key[key]
if(!toggle.legacy_key)
continue
toggles_by_key[key] = (toggle.legacy_key in old_toggles)
var/list/old_keybinds
legacy_savefile["key_bindings"] >> old_keybinds
keybindings = sanitize_islist(old_keybinds)
var/old_hotkeys
legacy_savefile["hotkeys"] >> old_hotkeys
misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE] = !!old_hotkeys
return TRUE
/datum/game_preferences/proc/perform_initial_load()
if(!is_guest)
// only if not guest
if(SSdbcore.IsConnected())
// sql mode
// load
if(!load_from_sql())
// if not, load from file
if(!load_from_file())
// if not, reset to defaults
full_reset()
// perform legacy migration to see if there's data
if(perform_legacy_migration())
// something was there, set our version to legacy
version = GAME_PREFERENCES_VERSION_LEGACY
// save results to sql, as sql is authoritative
save_to_sql()
// synchronize sql to file for backup for when sql is down
save_to_file()
else
// synchronize sql to file for backup for when sql is down
save_to_file()
else
// normal mode
// load
if(!load_from_file())
// if not, reset to defaults
full_reset()
// perform legacy migration to see if there's data
if(perform_legacy_migration())
// something was there, set our version to legacy
version = GAME_PREFERENCES_VERSION_LEGACY
// save to file
save_to_file()
// do we need to migrate?
if(version < GAME_PREFERENCES_VERSION_CURRENT)
// yes
if(!perform_migration_sequence())
// reset to defaults if failed
full_reset()
// perform save
if(SSdbcore.IsConnected())
save_to_sql()
else
save_to_file()
else
// if guest, just reset lmao
full_reset()
// todo: shouldn't we save after sanitize..?
sanitize_everything()
// loaded; push initial update triggers.
initialize_client()
// initialized!
initialized = TRUE
/**
* @return FALSE if failed
*/
/datum/game_preferences/proc/perform_migration_sequence()
if(version <= GAME_PREFERENCES_VERSION_DROP)
return FALSE
perform_migrations(version)
version = GAME_PREFERENCES_VERSION_CURRENT
/datum/game_preferences/proc/perform_migrations(from_version)
PRIVATE_PROC(TRUE)
//* Reset *//
/datum/game_preferences/proc/reset(category)
if(!category)
entries_by_key = list()
for(var/key in SSpreferences.entries_by_key)
var/datum/game_preference_entry/entry = SSpreferences.entries_by_key[key]
if(category && entry.category != category)
continue
var/value = entry.default_value(active)
entries_by_key[entry.key] = value
if(!isnull(active))
entry.on_set(active, value, TRUE)
mark_dirty()
push_ui_data(data = list("values" = entries_by_key))
/datum/game_preferences/proc/full_reset()
// reset misc stuff first as everything potentially uses it
misc_by_key = list()
// reset normal categories
reset()
// reset middleware
for(var/id in GLOB.game_preference_middleware)
var/datum/game_preference_middleware/middleware = GLOB.game_preference_middleware[id]
middleware.handle_reset(src)
//* Set / Get *//
/datum/game_preferences/proc/set_toggle(datum/game_preference_toggle/id_path_instance, value)
var/datum/game_preference_toggle/toggle = SSpreferences.resolve_preference_toggle(id_path_instance)
if(isnull(toggle))
CRASH("invalid fetch")
if(!initialized)
return FALSE
// we don't check is visible, as it's checked on 'get'
// if(!toggle.is_visible(active))
// return FALSE
toggles_by_key[toggle.key] = value
if(active)
toggle.toggled(active, value)
mark_dirty()
return TRUE
/datum/game_preferences/proc/toggle(datum/game_preference_toggle/id_path_instance)
var/datum/game_preference_toggle/toggle = SSpreferences.resolve_preference_toggle(id_path_instance)
if(isnull(toggle))
CRASH("invalid fetch")
if(!initialized)
return FALSE
// we don't check is visible, as it's checked on 'get'
// if(!toggle.is_visible(active))
// return FALSE
toggles_by_key[toggle.key] = !toggles_by_key[toggle.key]
if(active)
toggle.toggled(active, toggles_by_key[toggle.key])
mark_dirty()
return TRUE
/datum/game_preferences/proc/get_toggle(datum/game_preference_toggle/id_path_instance)
var/datum/game_preference_toggle/toggle = SSpreferences.resolve_preference_toggle(id_path_instance)
if(isnull(toggle))
CRASH("invalid fetch")
if(!initialized)
return toggle.default_value
if(!toggle.is_visible(active, TRUE))
return toggle.default_value
return toggles_by_key[toggle.key]
/datum/game_preferences/proc/set_entry(datum/game_preference_entry/id_path_instance, value)
var/datum/game_preference_entry/entry = SSpreferences.resolve_preference_entry(id_path_instance)
if(isnull(entry))
CRASH("invalid fetch")
if(!initialized)
return FALSE
if(!entry.is_visible(active, TRUE))
return FALSE
value = entry.filter_value(value)
entries_by_key[entry.key] = value
if(active)
entry.on_set(active, value, FALSE)
mark_dirty()
push_ui_data(data = list("values" = entries_by_key))
return TRUE
/datum/game_preferences/proc/get_entry(datum/game_preference_entry/id_path_instance)
var/datum/game_preference_entry/entry = SSpreferences.resolve_preference_entry(id_path_instance)
if(isnull(entry))
CRASH("invalid fetch")
if(!initialized)
return entry.default_value(active)
if(!entry.is_visible(active, TRUE))
return entry.default_value(active)
return entries_by_key[entry.key]
//* Save / Load *//
/datum/game_preferences/proc/auto_save()
if(!is_dirty)
return
sanitize_everything()
save()
/datum/game_preferences/proc/mark_dirty()
is_dirty = TRUE
push_ui_data(data = list("dirty" = TRUE))
/datum/game_preferences/proc/mark_saved()
is_dirty = FALSE
push_ui_data(data = list("dirty" = FALSE))
/datum/game_preferences/proc/save()
if(!initialized)
return FALSE
if(SSdbcore.Connect())
return save_to_sql()
else
return save_to_file()
/datum/game_preferences/proc/load()
if(!initialized)
return FALSE
if(SSdbcore.Connect())
. = load_from_sql()
else
. = load_from_file()
sanitize_everything()
initialize_client()
/**
* this proc does not sanitize!
*
* @return FALSE if we couldn't load
*/
/datum/game_preferences/proc/load_from_sql()
var/datum/player_data/player_data = resolve_player_data(ckey)
if(!player_data.block_on_available(10 SECONDS))
message_admins(SPAN_BOLDANNOUNCE("failed to resolve player data during prefs op for [ckey]. ping maintainers."))
CRASH("failed to grab player data while loading via sql. something bad has happened!")
authoritative_player_id = player_data.player_id
var/mob/allow_admin_proccalls = usr
usr = null
var/datum/db_query/query = SSdbcore.NewQuery(
"SELECT `toggles`, `entries`, `misc`, `keybinds`, `version` FROM [format_table_name("game_preferences")] \
WHERE `player` = :player",
list(
"player" = authoritative_player_id,
),
)
usr = allow_admin_proccalls
query.warn_execute(FALSE)
if(!query.NextRow())
qdel(query)
return FALSE
var/toggles_json = query.item[1]
var/entries_json = query.item[2]
var/misc_json = query.item[3]
var/keybinds_json = query.item[4]
var/loaded_version = query.item[5]
toggles_by_key = sanitize_islist(safe_json_decode(toggles_json))
entries_by_key = sanitize_islist(safe_json_decode(entries_json))
misc_by_key = sanitize_islist(safe_json_decode(misc_json))
keybindings = sanitize_islist(safe_json_decode(keybinds_json))
version = loaded_version
qdel(query)
authoritatively_loaded_by_sql = TRUE
mark_saved()
update_static_data()
return TRUE
/datum/game_preferences/proc/save_to_sql()
var/datum/player_data/player_data = resolve_player_data(ckey)
if(!player_data.block_on_available(10 SECONDS))
message_admins(SPAN_BOLDANNOUNCE("failed to resolve player data during prefs op for [ckey]. ping maintainers."))
CRASH("failed to grab player data while loading via sql. something bad has happened!")
authoritative_player_id = player_data.player_id
var/mob/allow_admin_proccalls = usr
usr = null
var/datum/db_query/query = SSdbcore.NewQuery(
"INSERT INTO [format_table_name("game_preferences")] \
(`player`, `toggles`, `entries`, `misc`, `keybinds`, `version`, `modified`) VALUES \
(:player, :toggles, :entries, :misc, :keybinds, :version, Now()) ON DUPLICATE KEY UPDATE \
`player` = VALUES(player), `toggles` = VALUES(toggles), `entries` = VALUES(entries), `misc` = VALUES(misc), \
`keybinds` = VALUES(keybinds), `version` = VALUES(version), `modified` = Now()",
list(
"player" = authoritative_player_id,
"toggles" = safe_json_encode(toggles_by_key),
"entries" = safe_json_encode(entries_by_key),
"misc" = safe_json_encode(misc_by_key),
"keybinds" = safe_json_encode(keybindings),
"version" = version,
)
)
usr = allow_admin_proccalls
query.warn_execute(FALSE)
qdel(query)
mark_saved()
authoritatively_loaded_by_sql = TRUE
sql_state_desynced = FALSE
return TRUE
/datum/game_preferences/proc/file_path()
return "data/players/[copytext(ckey, 1, 2)]/[ckey]/preferences.json"
/**
* this proc does not sanitize!
*
* @return FALSE if we couldn't load
*/
/datum/game_preferences/proc/load_from_file()
var/savefile_path = file_path()
if(!fexists(savefile_path))
return FALSE
var/list/deserialized = safe_json_decode(file2text(savefile_path))
entries_by_key = sanitize_islist(deserialized["entries"])
toggles_by_key =sanitize_islist( deserialized["toggles"])
keybindings = sanitize_islist(deserialized["keybindings"])
misc_by_key = sanitize_islist(deserialized["misc"])
if(authoritatively_loaded_by_sql)
sql_state_desynced = TRUE
mark_saved()
update_static_data()
return TRUE
/datum/game_preferences/proc/save_to_file()
var/savefile_path = file_path()
var/list/serializing = list(
"entries" = entries_by_key,
"toggles" = toggles_by_key,
"keybindings" = keybindings,
"misc" = misc_by_key,
)
if(fexists(savefile_path))
fdel(savefile_path)
text2file(safe_json_encode(serializing), savefile_path)
mark_saved()
if(authoritatively_loaded_by_sql)
sql_state_desynced = TRUE
return TRUE
/datum/game_preferences/proc/sanitize_everything()
// reset middleware
for(var/id in GLOB.game_preference_middleware)
var/datum/game_preference_middleware/middleware = GLOB.game_preference_middleware[id]
middleware.handle_sanitize(src)
for(var/key in SSpreferences.entries_by_key)
var/datum/game_preference_entry/entry = SSpreferences.entries_by_key[key]
var/current_value = entries_by_key[key]
entries_by_key[key] = entry.filter_value(current_value)
mark_dirty()
//* UI *//
/datum/game_preferences/ui_static_data(mob/user, datum/tgui/ui)
. = ..()
var/list/middleware = list()
for(var/key in GLOB.game_preference_middleware)
var/datum/game_preference_middleware/middleware_instance = GLOB.game_preference_middleware[key]
middleware[key] = middleware_instance.name
.["middleware"] = middleware
var/list/entries = list()
for(var/key in SSpreferences.entries_by_key)
var/datum/game_preference_entry/entry = SSpreferences.entries_by_key[key]
if(!entry.is_visible(user?.client, TRUE))
continue
entries[++entries.len] = entry.tgui_preference_schema()
.["entries"] = entries
.["values"] = entries_by_key
// todo: when do we refactor tgui again i don't like ui_interact because i'm a snowflake who likes being different
// (real complaint: ui_interact is weird for being called from tgui as well as from external)
// (update procs should be internally called, imo, not the 'open ui' proc, it leads to)
// (confusion when people add code with side effects like shocking people on touch in it.)
/datum/game_preferences/ui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui)
if(!initialized)
to_chat(user, SPAN_BOLDANNOUNCE("Your preferences are still being loaded. Please wait."))
return
ui = SStgui.try_update_ui(user, src, ui)
if(isnull(ui))
ui = new(user, src, "GamePreferences")
for(var/key in GLOB.game_preference_middleware)
var/datum/game_preference_middleware/middleware = GLOB.game_preference_middleware[key]
ui.register_module(middleware, key)
ui.set_autoupdate(FALSE)
ui.open()
/datum/game_preferences/ui_route(action, list/params, datum/tgui/ui, id)
. = ..()
if(.)
return
var/datum/game_preference_middleware/middleware = GLOB.game_preference_middleware[id]
if(middleware?.handle_topic(src, action, params))
return TRUE
/datum/game_preferences/ui_status(mob/user, datum/ui_state/state)
if(user.ckey == ckey)
return UI_INTERACTIVE
if(check_rights(C = user, rights_required = R_DEBUG))
return UI_INTERACTIVE
return UI_CLOSE
/datum/game_preferences/ui_act(action, list/params, datum/tgui/ui)
. = ..()
if(.)
return
switch(action)
if("set")
var/key = params["key"]
if(isnull(SSpreferences.entries_by_key[key]))
return TRUE
set_entry(key, params["value"])
return TRUE
if("reset")
reset(params["category"])
return TRUE
if("save")
auto_save()
return TRUE
if("discard")
load()
return TRUE
//? Client Wrappers ?//
/client/proc/get_preference_toggle(datum/game_preference_toggle/id_path_instance)
var/datum/game_preference_toggle/toggle = SSpreferences.resolve_preference_toggle(id_path_instance)
if(isnull(toggle))
CRASH("invalid fetch")
if(!initialized || !preferences.initialized)
return toggle.default_value
if(!toggle.is_visible(src, TRUE))
return toggle.default_value
return preferences.toggles_by_key[toggle.key]
/client/proc/get_preference_entry(datum/game_preference_entry/id_path_instance)
var/datum/game_preference_entry/entry = SSpreferences.resolve_preference_entry(id_path_instance)
if(isnull(entry))
CRASH("invalid fetch")
if(!initialized)
return entry.default_value(src)
if(!entry.is_visible(src, TRUE))
return entry.default_value(src)
return preferences.entries_by_key[entry.key]
//? Mob Wrappers ?//
/mob/proc/get_preference_toggle(datum/game_preference_toggle/id_path_instance)
var/datum/game_preference_toggle/toggle = SSpreferences.resolve_preference_toggle(id_path_instance)
if(isnull(toggle))
CRASH("invalid fetch")
if(!client?.initialized || !client.preferences.initialized)
return toggle.default_value
if(!toggle.is_visible(client, TRUE))
return toggle.default_value
return client.preferences.toggles_by_key[toggle.key]
/mob/proc/get_preference_entry(datum/game_preference_entry/id_path_instance)
var/datum/game_preference_entry/entry = SSpreferences.resolve_preference_entry(id_path_instance)
if(isnull(entry))
CRASH("invalid fetch")
if(!client?.initialized || !client.preferences.initialized)
return entry.default_value(client)
if(!entry.is_visible(client, TRUE))
return entry.default_value(client)
return client.preferences.entries_by_key[entry.key]
@@ -0,0 +1,140 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//
/datum/game_preference_middleware/keybindings
name = "Keybindings"
key = "keybindings"
/datum/game_preference_middleware/keybindings/on_initial_load(datum/game_preferences/prefs)
prefs.active?.update_movement_keys(src)
prefs.active?.set_macros()
return ..()
/datum/game_preference_middleware/keybindings/handle_reset(datum/game_preferences/prefs)
. = ..()
prefs.keybindings = list()
// don't change their hotkey mode.. unless it was never there.
prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE] = \
isnull(prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE])? TRUE : !!prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE]
var/hotkey_mode = prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE]
var/list/defaults = deep_copy_list(hotkey_mode? GLOB.hotkey_keybinding_list_by_key : GLOB.classic_keybinding_list_by_key)
prefs.keybindings = defaults
prefs.push_ui_modules(updates = list((src.key) = list(
"bindings" = prefs.keybindings,
"hotkeyMode" = prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE],
)))
/datum/game_preference_middleware/keybindings/handle_topic(datum/game_preferences/prefs, action, list/params)
. = ..()
if(.)
return
switch(action)
if("reset")
handle_reset(prefs)
return TRUE
if("hotkeys")
var/value = params["value"]
if(prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE] == value)
return TRUE
prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE] = !!value
prefs.mark_dirty()
prefs.push_ui_modules(updates = list((src.key) = list(
"hotkeyMode" = value,
)))
prefs.active?.set_macros()
return TRUE
if("addBind")
var/keybind_id = params["keybind"]
// key can be blank if we're only using ctrl/alt/shift!!
var/key = params["key"]
var/alt_depressed = params["alt"]
var/shift_depressed = params["shift"]
var/ctrl_depressed = params["ctrl"]
var/is_numpad = params["numpad"]
var/replacing_key = params["replaceKey"]
var/adding_key = "[alt_depressed? "Alt":""][ctrl_depressed? "Ctrl":""][shift_depressed? "Shift":""][is_numpad? "Numpad":""][key]"
if(!adding_key)
return TRUE
if(adding_key == replacing_key)
return TRUE
if(!isnull(replacing_key))
prefs.keybindings[replacing_key] -= keybind_id
if(isnull(prefs.keybindings[adding_key]))
prefs.keybindings[adding_key] = list()
if(!(keybind_id in prefs.keybindings[adding_key]))
prefs.keybindings[adding_key] += keybind_id
prefs.push_ui_modules(updates = list((src.key) = list(
"bindings" = prefs.keybindings,
)))
prefs.active?.update_movement_keys()
return TRUE
if("removeBind")
var/keybind_id = params["keybind"]
var/key = params["key"]
if(!key)
return TRUE
prefs.keybindings[key] -= keybind_id
prefs.push_ui_modules(updates = list((src.key) = list(
"bindings" = prefs.keybindings,
)))
prefs.active?.update_movement_keys()
return TRUE
/datum/game_preference_middleware/keybindings/handle_sanitize(datum/game_preferences/prefs)
. = ..()
prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE] = \
isnull(prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE])? TRUE : !!prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE]
var/list/keys_by_bind_id = list()
//! legacy: get rid of known trash values
prefs.keybindings -= "Unbound"
//! end
for(var/key in prefs.keybindings)
var/had_something = FALSE
if(islist(prefs.keybindings[key]))
var/list/keybind_ids = prefs.keybindings[key]
if(length(keybind_ids) > MAX_COMMANDS_PER_KEY)
keybind_ids.len = MAX_COMMANDS_PER_KEY
for(var/bind_id in prefs.keybindings[key])
var/datum/keybinding/found = GLOB.keybindings_by_name[bind_id]
if(isnull(found))
prefs.keybindings[key] -= bind_id
continue
if(isnull(keys_by_bind_id[bind_id]))
keys_by_bind_id[bind_id] = list()
if(length(keys_by_bind_id[bind_id]) > MAX_KEYS_PER_KEYBIND)
prefs.keybindings[key] -= bind_id
continue
keys_by_bind_id[bind_id] += key
had_something = TRUE
if(!had_something)
prefs.keybindings -= key
/datum/game_preference_middleware/keybindings/ui_static_data(mob/user, datum/tgui/ui)
. = ..()
var/datum/game_preferences/prefs = ui.src_object
.["bindings"] = prefs.keybindings
var/list/constructed_keybinds = list()
for(var/key in GLOB.keybindings_by_name)
var/datum/keybinding/keybind = GLOB.keybindings_by_name[key]
if(!keybind.is_visible(user?.client))
continue
constructed_keybinds[++constructed_keybinds.len] = keybind.tgui_keybinding_data()
.["keybinds"] = constructed_keybinds
.["hotkeyMode"] = prefs.misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE]
.["maxBinds"] = MAX_KEYS_PER_KEYBIND
.["maxPerKey"] = MAX_COMMANDS_PER_KEY
//? Preferences Helpers
/datum/game_preferences/proc/is_hotkeys_mode()
if(!initialized)
return TRUE
return misc_by_key[GAME_PREFERENCE_MISC_KEY_HOTKEY_MODE]
//? UI Design Assertions
#if MAX_KEYS_PER_KEYBIND != 3
#error TGUI for prefs is only designed for 3 bindings per keybind datum. Fix it if you want more.
#endif
@@ -0,0 +1,59 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//
/datum/game_preference_middleware/toggles
name = "Toggles"
key = "toggles"
/datum/game_preference_middleware/toggles/handle_reset(datum/game_preferences/prefs)
. = ..()
prefs.toggles_by_key = list()
for(var/key in SSpreferences.toggles_by_key)
var/datum/game_preference_toggle/toggle = SSpreferences.toggles_by_key[key]
prefs.toggles_by_key[toggle.key] = toggle.default_value
prefs.push_ui_modules(updates = list((src.key) = list(
"states" = prefs.toggles_by_key,
)))
/datum/game_preference_middleware/toggles/handle_topic(datum/game_preferences/prefs, action, list/params)
. = ..()
if(.)
return
switch(action)
if("reset")
handle_reset(prefs)
return TRUE
if("toggle")
var/key = params["key"]
if(isnull(SSpreferences.toggles_by_key[key]))
return TRUE
prefs.set_toggle(key, !!params["val"])
// todo: optimize this
prefs.push_ui_modules(updates = list((src.key) = list(
"states" = prefs.toggles_by_key,
)))
prefs.mark_dirty()
return TRUE
/datum/game_preference_middleware/toggles/ui_static_data(mob/user, datum/tgui/ui)
. = ..()
var/datum/game_preferences/prefs = ui.src_object
var/list/collected_toggles = list()
for(var/key in SSpreferences.toggles_by_key)
var/datum/game_preference_toggle/toggle = SSpreferences.toggles_by_key[key]
if(!toggle.is_visible(user?.client))
continue
collected_toggles[toggle.key] = toggle.tgui_preference_schema()
.["toggles"] = collected_toggles
.["states"] = prefs.toggles_by_key
/datum/game_preference_middleware/toggles/handle_sanitize(datum/game_preferences/prefs)
. = ..()
prefs.toggles_by_key = sanitize_islist(prefs.toggles_by_key)
for(var/key in prefs.toggles_by_key)
var/datum/game_preference_toggle/toggle = SSpreferences.toggles_by_key[key]
if(isnull(toggle))
prefs.toggles_by_key -= key
continue
prefs.toggles_by_key[key] = !!prefs.toggles_by_key[key]
@@ -41,7 +41,7 @@ GLOBAL_DATUM_INIT(age_verify_menu, /datum/age_verify_menu, new)
/datum/age_verify_menu/ui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "AgeVerifyMenu")
ui = new(user, src, "UIAgeVerifyMenu")
ui.open()
/datum/age_verify_menu/ui_status(mob/user, datum/ui_state/state)
@@ -53,6 +53,7 @@ GLOBAL_DATUM_INIT(age_verify_menu, /datum/age_verify_menu, new)
return
if(usr.client.is_age_verified())
qdel(ui)
return TRUE
var/player_month = text2num(params["month"])
+2 -2
View File
@@ -31,7 +31,7 @@
/client/proc/statpanel_boot_impl()
PRIVATE_PROC(TRUE)
// give client a second to load
sleep(2 SECONDS)
sleep(world.tick_lag)
// loads statbrowser if it isn't there
src << browse(file('html/statbrowser.html'), "window=statbrowser")
// if it is there and we can't tell because byond is byond, send it a signal to reload
@@ -364,6 +364,6 @@
/client/verb/fix_stat_panel()
set name = "Fix Stat Panel"
set category = "OOC"
set category = VERB_CATEGORY_OOC
statpanel_reset()
+38 -30
View File
@@ -1,6 +1,8 @@
// todo: lol we should datumize these probably?
// todo: refactor all of this
/var/all_ui_styles = list(
/var/global/list/all_ui_styles = list(
UI_STYLE_MIDNIGHT = 'icons/mob/screen/midnight.dmi',
UI_STYLE_ORANGE = 'icons/mob/screen/orange.dmi',
UI_STYLE_OLD = 'icons/mob/screen/old.dmi',
@@ -10,7 +12,7 @@
UI_STYLE_HOLOGRAM = 'icons/mob/screen/holo.dmi'
)
/var/all_ui_styles_robot = list(
/var/global/list/all_ui_styles_robot = list(
UI_STYLE_MIDNIGHT = 'icons/mob/screen1_robot.dmi',
UI_STYLE_ORANGE = 'icons/mob/screen1_robot.dmi',
UI_STYLE_OLD = 'icons/mob/screen1_robot.dmi',
@@ -34,27 +36,11 @@ var/global/list/all_tooltip_styles = list(
return all_ui_styles[ui_style]
return all_ui_styles[UI_STYLE_WHITE]
/client/verb/change_ui()
set name = "Change UI"
set category = "Preferences"
set desc = "Configure your user interface"
if(!ishuman(usr))
if(!isrobot(usr))
to_chat(usr, SPAN_WARNING("You must be a human or a robot to use this verb."))
return
var/UI_style_new = input(usr, "Select a style. White is recommended for customization") as null|anything in all_ui_styles
if(!UI_style_new) return
var/UI_style_alpha_new = input(usr, "Select a new alpha (transparency) parameter for your UI, between 50 and 255") as null|num
if(!UI_style_alpha_new || !(UI_style_alpha_new <= 255 && UI_style_alpha_new >= 50))
// todo: refactor
/client/proc/set_ui_style(style)
if(!usr?.hud_used.adding)
return
var/UI_style_color_new = input(usr, "Choose your UI color. Dark colors are not recommended!") as color|null
if(!UI_style_color_new) return
var/UI_style_new = style
//update UI
var/list/icons = usr.hud_used.adding + usr.hud_used.other + usr.hud_used.hotkeybuttons
icons.Add(usr.zone_sel)
@@ -70,13 +56,35 @@ var/global/list/all_tooltip_styles = list(
for(var/atom/movable/screen/I in icons)
if(I.name in list(INTENT_HELP, INTENT_HARM, INTENT_DISARM, INTENT_GRAB)) continue
I.icon = ic
I.color = UI_style_color_new
I.alpha = UI_style_alpha_new
// todo: refactor
/client/proc/set_ui_alpha(alpha)
if(!usr?.hud_used.adding)
return
//update UI
var/list/icons = usr.hud_used.adding + usr.hud_used.other + usr.hud_used.hotkeybuttons
icons.Add(usr.zone_sel)
icons.Add(usr.gun_setting_icon)
icons.Add(usr.item_use_icon)
icons.Add(usr.gun_move_icon)
icons.Add(usr.radio_use_icon)
if(alert("Like it? Save changes?",,"Yes", "No") == "Yes")
prefs.UI_style = UI_style_new
prefs.UI_style_alpha = UI_style_alpha_new
prefs.UI_style_color = UI_style_color_new
SScharacters.queue_preferences_save(prefs)
to_chat(usr, "UI was saved")
for(var/atom/movable/screen/I in icons)
if(I.name in list(INTENT_HELP, INTENT_HARM, INTENT_DISARM, INTENT_GRAB)) continue
I.alpha = alpha
// todo: refactor
/client/proc/set_ui_color(color)
if(!usr?.hud_used.adding)
return
//update UI
var/list/icons = usr.hud_used.adding + usr.hud_used.other + usr.hud_used.hotkeybuttons
icons.Add(usr.zone_sel)
icons.Add(usr.gun_setting_icon)
icons.Add(usr.item_use_icon)
icons.Add(usr.gun_move_icon)
icons.Add(usr.radio_use_icon)
for(var/atom/movable/screen/I in icons)
if(I.name in list(INTENT_HELP, INTENT_HARM, INTENT_DISARM, INTENT_GRAB)) continue
I.color = color
+1 -1
View File
@@ -1,5 +1,5 @@
/client/verb/show_station_minimap()
set category = "OOC"
set category = VERB_CATEGORY_OOC
set name = "Show Station Minimap"
set desc = "Shows a minimap of the currently loaded station map."
+27 -11
View File
@@ -1,6 +1,20 @@
// todo: most of the things in here should probably be re-thought category wise;
// verticality for one is more graphics/visuals
// but isn't a preference because it's something you need to actively see at some times but not others..
/client/verb/toggle_verticality_visibility()
set name = "Toggle Verticality Plane"
set desc = "Toggle if you see ceiling overlays and similar."
set category = VERB_CATEGORY_OOC
var/atom/movable/screen/plane_master/plane = global_planes.by_plane_type(/atom/movable/screen/plane_master/verticality)
plane.alpha = plane.alpha == 255? 0 : 255
to_chat(src, SPAN_NOTICE("You now [plane.alpha == 255? "see" : "no longer see"] verticality overlays."))
/client/verb/motd()
set name = "MOTD"
set category = "OOC"
set category = VERB_CATEGORY_OOC
set desc ="Check the Message of the Day"
var/motd = config.motd
@@ -45,7 +59,7 @@
/client/verb/ooc(msg as text)
set name = "OOC" //Gave this shit a shorter name so you only have to time out "ooc" rather than "ooc message" to use it --NeoFite
set category = "OOC"
set category = VERB_CATEGORY_OOC
if(!reject_on_initialization_block())
return
@@ -56,7 +70,7 @@
to_chat(src, "Guests may not use OOC.")
return
if(!is_preference_enabled(/datum/client_preference/show_ooc))
if(!get_preference_toggle(/datum/game_preference_toggle/chat/ooc))
to_chat(src, "<span class='warning'>You have OOC muted.</span>")
return
@@ -103,7 +117,7 @@
return
if(!is_preference_enabled(/datum/client_preference/show_ooc))
if(!get_preference_toggle(/datum/game_preference_toggle/chat/ooc))
to_chat(src, "<span class='warning'>You have OOC muted.</span>")
return
@@ -126,11 +140,13 @@
if(holder.rights & R_ADMIN)
ooc_style = "admin"
var/effective_color = holder && preferences.get_entry(/datum/game_preference_entry/simple_color/admin_ooc_color)
for(var/client/target in GLOB.clients)
if(!target.initialized)
continue
if(target.is_preference_enabled(/datum/client_preference/show_ooc))
if(target.get_preference_toggle(/datum/game_preference_toggle/chat/ooc))
if(target.is_key_ignored(key)) // If we're ignored by this person, then do nothing.
continue
var/display_name = src.key
@@ -140,8 +156,8 @@
display_name = "[holder.fakekey]/([src.key])"
else
display_name = holder.fakekey
if(holder && !holder.fakekey && (holder.rights & R_ADMIN) && CONFIG_GET(flag/allow_admin_ooccolor)) // keeping this for the badmins
to_chat(target, "<span class='prefix [ooc_style]'><span class='ooc'><font color='[prefs.ooccolor]'>" + "OOC: " + "<EM>[display_name]: </EM><span class='linkify'>[msg]</span></span></span></font>")
if(effective_color) // keeping this for the badmins
to_chat(target, "<span class='prefix [ooc_style]'><span class='ooc'><font color='[effective_color]'>" + "OOC: " + "<EM>[display_name]: </EM><span class='linkify'>[msg]</span></span></span></font>")
else
to_chat(target, "<span class='ooc'><span class='[ooc_style]'><span class='message'>OOC: <EM>[display_name]: </EM><span class='linkify'>[msg]</span></span></span></span>")
@@ -153,7 +169,7 @@
/client/verb/looc(msg as text)
set name = "LOOC"
set desc = "Local OOC, seen only by those in view."
set category = "OOC"
set category = VERB_CATEGORY_OOC
if(!reject_on_initialization_block())
return
@@ -175,7 +191,7 @@
if(!msg)
return
if(!is_preference_enabled(/datum/client_preference/show_looc))
if(!get_preference_toggle(/datum/game_preference_toggle/chat/looc))
to_chat(src, "<span class='danger'>You have LOOC muted.</span>")
return
@@ -221,7 +237,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.get_preference_toggle(/datum/game_preference_toggle/chat/looc))
receivers |= viewer.client
else if(istype(viewer,/mob/observer/eye)) // For AI eyes and the like
var/mob/observer/eye/E = viewer
@@ -230,7 +246,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.get_preference_toggle(/datum/game_preference_toggle/admin/global_looc))
r_receivers |= admin
msg = emoji_parse(msg)
@@ -1,34 +0,0 @@
/client/verb/bunker_auto_authorize()
set name = "Auto Authorize Panic Bunker"
set desc = "Authorizes your account in the panic bunker of any servers connected to this function."
set category = "OOC"
world.send_cross_server_bunker_overrides(key, src)
/world/proc/send_cross_server_bunker_overrides(key, client/C)
var/comms_key = CONFIG_GET(string/comms_key)
if(!comms_key)
return
var/list/message = list()
message["ckey"] = key
message["source"] = "[CONFIG_GET(string/cross_comms_name)]"
message["key"] = comms_key
message["auto_bunker_override"] = TRUE
var/list/servers = CONFIG_GET(keyed_list/cross_server_bunker_override)
if(!length(servers))
to_chat(C, "<span class='boldwarning'>AUTOBUNKER: No servers are configured to receive from this one.</span>")
return
var/logtext = "[key] ([key_name(C)]) has initiated an autobunker authentication with linked servers."
message_admins(logtext)
log_admin(logtext)
for(var/name in servers)
var/returned = world.Export("[servers[name]]?[list2params(message)]")
switch(returned)
if("Bad Key")
to_chat(C, "<span class='boldwarning'>AUTOBuNKER: [name] failed to authenticate with this server.</span>")
if("Function Disabled")
to_chat(C, "<span class='boldwarning'>AUTOBUNKER: [name] has autobunker receive disabled.</span>")
if("Success")
to_chat(C, "<span class='boldwarning'>AUTOBUNKER: Successfully authenticated with [name]. Panic bunker bypass granted to [key].</span>.")
else
to_chat(C, "<span class='boldwarning'>AUTOBUNKER: Unknown error ([name]).</span>")
-11
View File
@@ -1,11 +0,0 @@
/client/verb/character_setup()
set name = "Character Setup"
set category = "Preferences"
prefs?.ShowChoices(usr)
/client/verb/preferences()
set name = "Game Options"
set category = "Preferences"
prefs?.ShowChoices(usr)
@@ -1,3 +1,28 @@
//? Setup Panels
/client/verb/character_setup()
set name = "Character Setup"
set category = VERB_CATEGORY_SYSTEM
prefs?.ShowChoices(usr)
/client/verb/preferences()
set name = "Game Preferences"
set category = VERB_CATEGORY_SYSTEM
preferences.ui_interact(mob)
//? Sound Management
/client/verb/stop_client_sounds()
set name = "Stop Sounds"
set category = VERB_CATEGORY_SYSTEM
set desc = "Stop Current Sounds"
usr << sound(null)
usr.client?.tgui_panel?.stop_music()
//? Ping System
/client/verb/update_ping(time as num)
set instant = TRUE
set name = ".update_ping"
@@ -18,5 +43,5 @@
/client/verb/ping()
set name = "Ping"
set category = "OOC"
set category = VERB_CATEGORY_SYSTEM
winset(src, null, "command=.display_ping+[world.time+world.tick_lag*TICK_USAGE_REAL/100]")
+1 -7
View File
@@ -1,7 +1 @@
/client/verb/toggle_verticality_visibility()
set name = "Toggle Verticality Plane"
set desc = "Toggle if you see ceiling overlays and similar."
var/atom/movable/screen/plane_master/plane = global_planes.by_plane_type(/atom/movable/screen/plane_master/verticality)
plane.alpha = plane.alpha == 255? 0 : 255
to_chat(src, SPAN_NOTICE("You now [plane.alpha == 255? "see" : "no longer see"] verticality overlays."))
// this section left intentionally blank for now
+1 -1
View File
@@ -410,7 +410,7 @@ GLOBAL_VAR(lock_client_view_y)
*/
/client/verb/user_fit_viewport()
set name = "Fit Viewport"
set category = "OOC"
set category = VERB_CATEGORY_OOC
set desc = "Fit the width of the map window to match the viewport"
request_viewport_fit()