diff --git a/citadel.dme b/citadel.dme
index 79c90a8763f..54792161116 100644
--- a/citadel.dme
+++ b/citadel.dme
@@ -2472,14 +2472,15 @@
#include "code\modules\catalogue\cataloguer_visuals.dm"
#include "code\modules\catalogue\cataloguer_vr.dm"
#include "code\modules\client\client-admin.dm"
+#include "code\modules\client\client-stat.dm"
#include "code\modules\client\client.dm"
#include "code\modules\client\client_procs.dm"
+#include "code\modules\client\client_statpanel.dm"
#include "code\modules\client\connection.dm"
#include "code\modules\client\cutscene.dm"
#include "code\modules\client\perspective.dm"
#include "code\modules\client\security.dm"
#include "code\modules\client\spam_prevention.dm"
-#include "code\modules\client\statpanel.dm"
#include "code\modules\client\throttling.dm"
#include "code\modules\client\ui_style.dm"
#include "code\modules\client\viewport.dm"
@@ -2498,6 +2499,7 @@
#include "code\modules\client\onboarding\age_verification.dm"
#include "code\modules\client\onboarding\panic_bunker.dm"
#include "code\modules\client\onboarding\security_checks.dm"
+#include "code\modules\client\verbs\fix_statpanel.dm"
#include "code\modules\client\verbs\minimap.dm"
#include "code\modules\client\verbs\ooc.dm"
#include "code\modules\client\verbs\system.dm"
@@ -3514,7 +3516,6 @@
#include "code\modules\mining\tools\vertibore.dm"
#include "code\modules\mob\action_feedback.dm"
#include "code\modules\mob\animations.dm"
-#include "code\modules\mob\client.dm"
#include "code\modules\mob\death.dm"
#include "code\modules\mob\emote.dm"
#include "code\modules\mob\floating_message.dm"
@@ -3527,6 +3528,7 @@
#include "code\modules\mob\life.dm"
#include "code\modules\mob\login.dm"
#include "code\modules\mob\logout.dm"
+#include "code\modules\mob\mob-client.dm"
#include "code\modules\mob\mob-damage.dm"
#include "code\modules\mob\mob-defense.dm"
#include "code\modules\mob\mob-iff.dm"
diff --git a/code/__DEFINES/interface/skin.dm b/code/__DEFINES/interface/skin.dm
index f4c2eeea2f8..4ba912e61de 100644
--- a/code/__DEFINES/interface/skin.dm
+++ b/code/__DEFINES/interface/skin.dm
@@ -22,7 +22,7 @@
//* Chat System
#define SKIN_OUTPUT_ID_LEGACY_CHAT "output"
-#define SKIN_BROWSER_ID_CHAT "outputbrowser"
+#define SKIN_BROWSER_ID_CHAT "browseroutput"
//* Cutscene System
#define SKIN_BROWSER_ID_CUTSCENE "cutscenebrowser"
diff --git a/code/controllers/subsystem/server_maint.dm b/code/controllers/subsystem/server_maint.dm
index 742a14393e2..c8fe6fa4ba7 100644
--- a/code/controllers/subsystem/server_maint.dm
+++ b/code/controllers/subsystem/server_maint.dm
@@ -96,3 +96,7 @@ SUBSYSTEM_DEF(server_maint)
world.update_hub_visibility(TRUE)
#undef PING_BUFFER_TIME
+
+// todo: SShub_manager
+/datum/controller/subsystem/server_maint/proc/queue_hub_update()
+ addtimer(CALLBACK(src, PROC_REF(UpdateHubStatus)), 0)
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index 300da509d8b..5c5ea3e4dfa 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -43,15 +43,15 @@ SUBSYSTEM_DEF(statpanels)
// grab victim
var/client/player = currentrun[length(currentrun)]
--currentrun.len
- // check listed turf
- if(player.statpanel_turf && !player.list_turf_check(player.statpanel_turf))
- player.unlist_turf()
// check if we're even on the js one
- if(player.statpanel_on_byond)
+ if(player.tgui_stat.byond_stat_active)
continue
// check if ready
- if(!player.statpanel_ready)
+ if(!player.tgui_stat.ready)
continue
+ // check listed turf
+ if(player.tgui_stat.byond_stat_turf && !player.list_turf_check(player.tgui_stat.byond_stat_turf))
+ player.unlist_turf()
// are they an admin?
var/is_admin = !!player.holder
// grab their mob data
diff --git a/code/game/click/click.dm b/code/game/click/click.dm
index b7770f36037..540d3c6a68a 100644
--- a/code/game/click/click.dm
+++ b/code/game/click/click.dm
@@ -282,7 +282,7 @@
return
if(!client)
return
- if(T == client.statpanel_turf)
+ if(T == client.tgui_stat?.byond_stat_turf)
client.unlist_turf()
return
client.list_turf(T)
diff --git a/code/game/world.dm b/code/game/world.dm
index 799cb479cf9..05ff88358b2 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -75,13 +75,6 @@ GLOBAL_LIST(topic_status_cache)
SSdbcore.SetRoundID()
SetupLogs()
-// #ifndef USE_CUSTOM_ERROR_HANDLER
-// world.log = file("[GLOB.log_directory]/dd.log")
-// #else
-// if (TgsAvailable())
-// world.log = file("[GLOB.log_directory]/dd.log") //not all runtimes trigger world/Error, so this is the only way to ensure we can see all of them.
-// #endif
-
// shunt redirected world log from Master's init back into world log proper, now that logging has been set up.
shunt_redirected_log()
@@ -525,6 +518,9 @@ GLOBAL_LIST(topic_status_cache)
// we already know, we don't care
if(global.world_log_redirected)
return
+ // we're not running in tgs, do not redirect world.log
+ if(!world.params["server_service_version"])
+ return
global.world_log_redirected = TRUE
if(fexists("data/logs/world_init_temporary.log"))
fdel("data/logs/world_init_temporary.log")
@@ -538,8 +534,13 @@ GLOBAL_LIST(topic_status_cache)
/world/proc/shunt_redirected_log()
// if we're unit testing do not ever redirect world.log or the test won't show output.
#ifndef UNIT_TESTS
+ // we're not running in tgs, do not redirect world.log
+ if(!world.params["server_service_version"])
+ return
+ // if logs are to be redirected, send it to that folder
if(!(OVERRIDE_LOG_DIRECTORY_PARAMETER in params))
world.log = file("[GLOB.log_directory]/dd.log")
+ // handle pre-init log redirection
if(!world_log_redirected)
log_world("World log shunt never happened. Something has gone wrong!")
return
diff --git a/code/modules/asset_cache/packs/spritesheet.dm b/code/modules/asset_cache/packs/spritesheet.dm
index 7b83ff7c6a4..0e0834d4544 100644
--- a/code/modules/asset_cache/packs/spritesheet.dm
+++ b/code/modules/asset_cache/packs/spritesheet.dm
@@ -40,6 +40,12 @@
/// "foo_bar" -> list("32x32", 5)
var/list/sprites = list()
+/datum/asset_pack/spritesheet/unload()
+ ..()
+ to_generate = list()
+ sizes = list()
+ sprites = list()
+
/datum/asset_pack/spritesheet/register(generation)
return construct()
diff --git a/code/modules/client/client-stat.dm b/code/modules/client/client-stat.dm
new file mode 100644
index 00000000000..f1d1055eeeb
--- /dev/null
+++ b/code/modules/client/client-stat.dm
@@ -0,0 +1,21 @@
+/**
+ * Client Stat()
+ *
+ * We only run this if the statpanel is on BYOND mode right now.
+ */
+/client/Stat()
+ if(!tgui_stat?.byond_stat_active)
+ return
+ ..() // hit mob.Stat()
+ if(!statpanel("Turf"))
+ return
+ if(!tgui_stat.byond_stat_turf)
+ stat("No turf listed ; Alt click on an adjacent turf to view contents.")
+ return
+ stat(tgui_stat.byond_stat_turf.name, tgui_stat.byond_stat_turf)
+ for(var/atom/movable/AM as anything in tgui_stat.byond_stat_turf)
+ if(!AM.mouse_opacity)
+ continue
+ if(AM.invisibility > using_perspective.see_invisible)
+ continue
+ stat(null, AM)
diff --git a/code/modules/client/client.dm b/code/modules/client/client.dm
index eb77daa6618..7c36b27ebb5 100644
--- a/code/modules/client/client.dm
+++ b/code/modules/client/client.dm
@@ -108,22 +108,6 @@
/// client preferences
var/datum/game_preferences/preferences
- //? Statpanel
- /// statpanel tab ; can be null (e.g. we're looking at verb tabs)
- var/statpanel_tab
- /// statpanel initialized
- var/statpanel_ready = FALSE
- /// turf being listed
- var/turf/statpanel_turf
- /// tabs the panel has
- var/list/statpanel_tabs
- /// statpanel variable tabs: spells / other "simple" action button frameworks
- var/list/statpanel_spell_last
- /// are we on byond stat? if so we can just skip the js one in data transmit (and vice versa)
- var/statpanel_on_byond = FALSE
- /// did we get autoswitched to byond stat for turf? if so we'll switch back when we un-list
- var/statpanel_for_turf = FALSE
-
//? Throttling
/// block re-execution of expensive verbs
var/verb_throttle = 0
@@ -141,11 +125,17 @@
/// since byond is deranged and will send winsets and browse calls out of order sometimes.
var/cutscene_lockout = FALSE
- //* UI *//
- /// Our action drawer
- var/datum/action_drawer/action_drawer
+ //* UI - Client *//
/// our tooltips system
var/datum/tooltip/tooltips
+ /// chat panel
+ var/datum/tgui_panel/tgui_panel
+ /// statpanel
+ var/datum/client_statpanel/tgui_stat
+
+ //* UI - Map *//
+ /// Our action drawer
+ var/datum/action_drawer/action_drawer
////////////////
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index c6158d2fc08..138744a62a7 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -171,15 +171,17 @@
C.New()
/client/New(TopicData)
- //* pre-connect-ish
- // set appadmin for profiling or it might not work (?) (this is old code we just assume it's here for a reason)
+ //* pre-connect-ish *//
+
+ // Byond only populates whether or not you can profile at connect. You have to give someone this
+ // before their client loads/whatever. This cannot be behind a spawn(). We will remove it from non-admins later.
world.SetConfig("APP/admin", ckey, "role=admin")
- // block client.Topic() calls from connect
+ // Block client.Topic() calls from connect.
TopicData = null
- // kick out invalid connections
+ // Kick invalid connections.
if(connection != "seeker" && connection != "web")
return null
- // kick out guests
+ //! legacy: kick out guests !//
if(!config_legacy.guests_allowed && is_guest() && !is_localhost())
security_kick(
message = "This server doesn't allow guest accounts to play. Please go to http://www.byond.com/ and register for a key.",
@@ -187,9 +189,10 @@
immediate = TRUE,
)
return null
- // pre-connect greeting
- to_chat(src, "If the title screen is black, resources are still downloading. Please be patient until the title screen appears.")
- // register in globals
+ // Queue pre-connect greeting
+ spawn(0.5 SECONDS)
+ to_chat(src, "If the title screen is black, resources are still downloading. Please be patient until the title screen appears.")
+ // Register in globals.
GLOB.clients += src
GLOB.directory[ckey] = src
@@ -212,26 +215,18 @@
//* Resolve preferences
preferences = SSpreferences.resolve_game_preferences(key, ckey)
//? 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
// we wait until it inits to do this
// todo: is there a better way this is kind of awful
preferences.active = src
preferences.on_reconnect()
//? END ?//
- //* Setup user interface
+ //* Create UI *//
// todo: move top level menu here, for now it has to be under prefs.
// Instantiate statpanel
- spawn(1)
- statpanel_boot()
+ tgui_stat = new(src, SKIN_BROWSER_ID_STAT)
// Instantiate tgui panel
- tgui_panel = new(src, "browseroutput")
+ tgui_panel = new(src, SKIN_BROWSER_ID_CHAT)
// Instantiate cutscene system
spawn(1)
init_cutscene_system()
@@ -298,48 +293,36 @@
//* therefore, DO NOT PUT ANYTHING YOU WILL RELY ON LATER IN THIS PROC IN LOGIN!
. = ..() //calls mob.Login()
- //* Connection Security
+ //* Connection Security *//
// start caching it immediately
INVOKE_ASYNC(SSipintel, TYPE_PROC_REF(/datum/controller/subsystem/ipintel, vpn_connection_check), address, ckey)
// run onboarding gauntlet
INVOKE_ASYNC(src, PROC_REF(onboarding))
- //* Initialize Input
+ //* Initialize Input *//
if(SSinput.initialized)
set_macros()
update_movement_keys()
- //* Initialize UI
+ //* Initialize UI *//
// initialize statbrowser
- // (we don't, the JS does it for us. by signalling statpanel_ready().)
+ tgui_stat.initialize()
// Initialize tgui panel
- INVOKE_ASYNC(tgui_panel, TYPE_PROC_REF(/datum/tgui_panel, initialize))
+ tgui_panel.initialize()
// initialize cutscene browser
- // (we don't, the JS does it for us.)
-
- //if(alert_mob_dupe_login)
- // spawn()
- // alert(mob, "You have logged in already with another key this round, please log out of this one NOW or risk being banned!")
+ // - (we don't, the JS does it for us.) -
+ // Initialize tooltips
+ tooltips.initialize()
connection_time = world.time
connection_realtime = world.realtime
connection_timeofday = world.timeofday
- winset(src, null, "command=\".configure graphics-hwmode on\"")
- /*
- if (connection == "web" && !connecting_admin)
- if (!CONFIG_GET(flag/allow_webclient))
- to_chat(src, "Web client is disabled")
- qdel(src)
- return 0
- if (CONFIG_GET(flag/webclient_only_byond_members) && !IsByondMember())
- to_chat(src, "Sorry, but the web client is restricted to byond members only.")
- qdel(src)
- return 0
- if( (world.address == address || !address) && !GLOB.host )
- GLOB.host = key
- world.update_status()
- */
+ //* Misc *//
+ // force hardware graphics on
+ spawn(5)
+ winset(src, null, "command=\".configure graphics-hwmode on\"")
+
if(holder)
add_admin_verbs()
admin_memo_show()
@@ -352,7 +335,10 @@
to_chat(src, "[custom_event_msg]")
to_chat(src, "
")
- send_resources()
+ // Preload resources.
+ // todo: re-evaluate this
+ spawn(0)
+ send_resources()
//? Startup rendering
pre_init_viewport()
@@ -366,10 +352,13 @@
// changelog_async()
// run post-init 'lint'-like checks
- on_new_hook_stability_checks()
+ // this is on a spawn() to force a separate call chain
+ spawn(0)
+ on_new_hook_stability_checks()
// todo: fuck you voreprefs
- prefs_vr = new /datum/vore_preferences(src)
+ spawn(0)
+ prefs_vr = new /datum/vore_preferences(src)
if(config_legacy.paranoia_logging)
if(isnum(player.player_age) && player.player_age == -1)
@@ -377,16 +366,18 @@
if(isnum(persistent.account_age) && persistent.account_age <= 2)
log_and_message_admins("PARANOIA: [key_name(src)] has a very new BYOND account ([persistent.account_age] days).")
- //? We are done
+ //* Finalize *//
// set initialized if we're not queued for a security kick
if(!queued_security_kick || panic_bunker_pending)
initialized = TRUE
else
addtimer(CALLBACK(src, PROC_REF(deferred_initialization_block)), 0)
// show any migration errors
+ // todo: this shouldn't be here
prefs.auto_flush_errors()
// update our hub label
- SSserver_maint.UpdateHubStatus()
+ // todo: this should be a global signal that the subsystem hooks
+ SSserver_maint.queue_hub_update()
/**
* Called in the middle of new, after everything critical
@@ -450,25 +441,22 @@
// clear HUDs
clear_atom_hud_providers()
- //* cleanup UI
- // cleanup statbrowser
- statpanel_dispose()
- // cleanup cutscene system
+ //* cleanup client UI *//
+ QDEL_NULL(tgui_stat)
cleanup_cutscene_system()
- // cleanup tgui panel
QDEL_NULL(tgui_panel)
- // cleanup tooltips
QDEL_NULL(tooltips)
- // cleanup actions
+
+ //* cleanup map UI *//
QDEL_NULL(action_holder)
QDEL_NULL(action_drawer)
- //* logout
+ //* logout *//
mob?.pre_logout(src)
- //* cleanup from SSinput
+ //* cleanup from SSinput *//
SSinput.currentrun?.Remove(src)
- //* cleanup from SSping
+ //* cleanup from SSping *//
SSping.currentrun?.Remove(src)
. = ..() //Even though we're going to be hard deleted there are still some things that want to know the destroy is happening
diff --git a/code/modules/client/statpanel.dm b/code/modules/client/client_statpanel.dm
similarity index 55%
rename from code/modules/client/statpanel.dm
rename to code/modules/client/client_statpanel.dm
index 6703ccc3bde..98f178c3394 100644
--- a/code/modules/client/statpanel.dm
+++ b/code/modules/client/client_statpanel.dm
@@ -1,6 +1,3 @@
-// todo: if byond ever gets threaded browsers, we're standardizing tgui.html and tgui_window.dm
-// because manually reimplementing this shit is driving me nuts.
-
/**
* citadel RP stat system
*
@@ -9,81 +6,206 @@
* this is a browser system that uses .js and is relatively fast for serialization/whatnot
* the listed turf, however, gets sent to native Stat() so it supports native mouse
* handling among other things.
+ *
+ * todo: use /datum/tgui_window, have a JS bootstrap instead of the current system.
*/
-//! external - state
+/datum/client_statpanel
+ var/client/client
+ var/window_id
+ // var/datum/tgui_window/window
-/client/proc/statpanel_init()
- src << output(null, "statbrowser:byond_init")
- init_verbs()
+ //* Browser Stat *//
+ /// is the statpanel ready?
+ var/ready = FALSE
+ /// current tab
+ var/current_tab
+ /// currently loaded tabs
+ var/list/current_tabs
+ /// i don't know what this does
+ var/list/spell_last
-/client/proc/statpanel_check()
- if(statpanel_ready)
- return
- to_chat(src, SPAN_USERDANGER("Statpanel failed to load, click here to reload the panel "))
+ //* BYOND Stat *//
+ /// the turf being listed on byond stat
+ var/turf/byond_stat_turf
+ /// are we currently tabbed to byond stat?
+ var/byond_stat_active = FALSE
+ /// did we get autoswitched to byond stat when the player alt clicked?
+ /// determines if we switch back when the turf is no longer listed.
+ var/byond_stat_ephemeral = FALSE
-/**
- * boots statpanel up during connect
- */
-/client/proc/statpanel_boot()
- set waitfor = FALSE
- statpanel_boot_impl()
+/datum/client_statpanel/New(client/client, window_id)
+ src.client = client
+ src.window_id = window_id
+ // src.window = new(client, window_id)
+ // src.window.subscribe(src, PROC_REF(on_message))
-/client/proc/statpanel_boot_impl()
+/datum/client_statpanel/Destroy()
+ client << output(null, "statbrowser:byond_cleanup")
+ client = null
+ return ..()
+
+/datum/client_statpanel/proc/initialize()
+ if(!client.initialized)
+ // todo: this should be a timer, but current MC doesn't really support that until we have
+ // MC init stages
+ spawn(1 SECONDS)
+ UNTIL(!client || client.initialized)
+ if(!client)
+ return
+ boot()
+ else
+ spawn(0)
+ boot()
+
+/datum/client_statpanel/proc/boot()
PRIVATE_PROC(TRUE)
- // give client a second to load
- sleep(world.tick_lag)
// loads statbrowser if it isn't there
- src << browse(file('html/statbrowser.html'), "window=statbrowser")
+ client << browse(file('html/statbrowser.html'), "window=[window_id]")
// if it is there and we can't tell because byond is byond, send it a signal to reload
- src << output(null, "statbrowser:byond_reconnect")
+ client << output(null, "statbrowser:byond_reconnect")
// check for it incase it breaks
- addtimer(CALLBACK(src, TYPE_PROC_REF(/client, statpanel_check)), 30 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(check_initialized)), 5 SECONDS)
+
+/datum/client_statpanel/proc/check_initialized()
+ if(ready)
+ return
+ to_chat(client, SPAN_USERDANGER("Statpanel failed to load, click here to reload the panel "))
+
+//* Internal API *//
/**
- * cleans up statpanel stuff during disconnect
+ * Resends the client's current verbs.
+ *
+ * @params
+ * * reset - if TRUE, we will obliterate all current tabs and entirely redraw the browser. Otherwise, we
+ * only change mutated verbs.
*/
-/client/proc/statpanel_dispose()
- statpanel_ready = FALSE
- statpanel_tab = null
- statpanel_tabs = null
- statpanel_spell_last = null
- unlist_turf()
- src << output(null, "statbrowser:byond_cleanup")
+/datum/client_statpanel/proc/init_verbs(reset = FALSE)
+ PRIVATE_PROC(TRUE)
+ var/list/verblist = list()
+ var/list/verbstoprocess = client.verbs.Copy()
+ if(client.mob)
+ verbstoprocess += client.mob.verbs
+ for(var/atom/movable/AM as anything in client.mob)
+ verbstoprocess += AM.verbs
+ for(var/thing in verbstoprocess)
+ var/procpath/verb_to_init = thing
+ if(verb_to_init.hidden)
+ continue
+ if(!istext(verb_to_init.category) || !verb_to_init.name)
+ continue
+ if(verb_to_init.name[1] == ".")
+ continue
+ LAZYINITLIST(verblist[verb_to_init.category])
+ verblist[verb_to_init.category] |= verb_to_init.name
+ client << output("[url_encode(json_encode(verblist))];[reset]", "statbrowser:byond_init_verbs")
+
+//* External API *//
+
+/datum/client_statpanel/proc/request_verb_update(reset)
+ if(!ready)
+ return
+ init_verbs(reset)
/**
- * instructs statpanel to reload
+ * resets state and reloads everything
*/
-/client/proc/statpanel_reload()
- // this is janky as shit tbh - init_verbs shouldn't be our reset call too.
- init_verbs(TRUE)
-
-/**
- * only called for debug; fully reset statbrowser.
- */
-/client/proc/statpanel_reset()
- statpanel_dispose()
- sleep(1)
- src << browse("RELOADING", "window=statbrowser")
- src << browse(file('html/statbrowser.html'), "window=statbrowser")
- sleep(1)
- statpanel_boot()
-
-/**
- * called by statbrowser when it's ready
- */
-/client/proc/statpanel_ready()
- statpanel_tabs = list()
- statpanel_init()
- statpanel_ready = TRUE
+/datum/client_statpanel/proc/request_reload()
+ current_tabs = list()
+ current_tab = null
+ spell_last = list()
+ request_verb_update(TRUE)
/**
* called to set/dispose admin token
*/
-/client/proc/statpanel_token(token)
+/datum/client_statpanel/proc/set_admin_token(token)
if(!token)
- src << output(null, "statbrowser:byond_dispose_token")
+ client << output(null, "statbrowser:byond_dispose_token")
return
- src << output(token, "statbrowser:byond_grant_token")
+ client << output(token, "statbrowser:byond_grant_token")
+
+/**
+ * sent from a client verb called by statbrowser when it's ready
+ */
+/datum/client_statpanel/proc/ready_received()
+ ready = TRUE
+ client << output(null, "statbrowser:byond_init")
+ request_reload()
+
+//! verb hooks - js stat
+// todo: replace with tgui window messaging
+
+/client/verb/hook_statpanel_ready()
+ set name = ".statpanel_ready"
+ set hidden = TRUE
+ set instant = TRUE
+
+ tgui_stat.ready_received()
+
+/client/verb/hook_statpanel_add_tab(tab as text)
+ set name = ".statpanel_tab_add"
+ set hidden = TRUE
+ set instant = TRUE
+
+ if(length(tgui_stat.current_tabs) > 50)
+ return // bail
+ tgui_stat.current_tabs |= tab
+ if(!tgui_stat.current_tab)
+ tgui_stat.current_tab = tgui_stat.current_tabs[1]
+
+/client/verb/hook_statpanel_remove_tab(tab as text)
+ set name = ".statpanel_tab_remove"
+ set hidden = TRUE
+ set instant = TRUE
+
+ tgui_stat.current_tabs -= tab
+
+/client/verb/hook_statpanel_wipe_tabs()
+ set name = ".statpanel_tab_reset"
+ set hidden = TRUE
+ set instant = TRUE
+
+ tgui_stat.current_tabs = list()
+
+/client/verb/hook_statpanel_set_tab(tab as text)
+ set name = ".statpanel_tab"
+ set hidden = TRUE
+ set instant = TRUE
+
+ tgui_stat.current_tab = tab
+
+/**
+ * This cleanly and gracefully attempts to go to a specific tab via verbs, for the hyperspecific purpose of interacting with the statpanel from other HTML UI
+ */
+/client/verb/hook_statpanel_goto_tab(tab as text)
+ set name = ".statpanel_goto_tab"
+ set hidden = TRUE
+ set instant = TRUE
+ src << output(tab, "statbrowser:change_tab")
+
+//* verb hooks - tab switcher *//
+
+/client/verb/hook_statswitcher_set_tab(tab as text)
+ set name = ".statswitcher"
+ set hidden = TRUE
+ set instant = TRUE
+
+ tgui_stat.byond_stat_active = (tab == "stat_pane_byond")
+
+// todo: legacy stuff below
+
+/// for legacy shit like rigsuits that didn't get the hint about not using verbs
+/client/proc/queue_legacy_verb_update()
+ if(HAS_TRAIT(src, "VERB_UPDATE_QUEUED"))
+ return
+ ADD_TRAIT(src, "VERB_UPDATE_QUEUED", "FUCK")
+ addtimer(CALLBACK(src, PROC_REF(legacy_verb_update)), 1 SECONDS)
+
+/// -_-
+/client/proc/legacy_verb_update()
+ REMOVE_TRAIT(src, "VERB_UPDATE_QUEUED", "FUCK")
+ tgui_stat?.request_verb_update()
/**
* returns TRUE if the tab should exist and we are on the tab
@@ -92,10 +214,10 @@
* - status: use to set if the panel should exist
*/
/client/proc/statpanel_tab(tab, status)
- . = (statpanel_tab == tab)
+ . = (tgui_stat.current_tab == tab)
if(isnull(status))
return
- if(tab in statpanel_tabs)
+ if(tab in tgui_stat.current_tabs)
if(!status)
src << output(url_encode(tab), "statbrowser:byond_remove_tab")
return FALSE // removing
@@ -105,40 +227,26 @@
return FALSE // don't add yet, this is unfortunate but we'll add one tick of update delay to let it add first
/client/proc/list_turf(turf/T)
- if(statpanel_turf)
+ if(tgui_stat.byond_stat_turf)
unlist_turf()
if(!T || !list_turf_check(T))
return
- statpanel_turf = T
+ tgui_stat.byond_stat_turf = T
// using byond atm
- if(!statpanel_on_byond)
- statpanel_for_turf = TRUE
- statpanel_on_byond = TRUE
+ if(!tgui_stat.byond_stat_active)
+ tgui_stat.byond_stat_ephemeral = TRUE
+ tgui_stat.byond_stat_active = TRUE
winset(src, SKIN_TAB_ID_STAT, "current-tab=[SKIN_PANE_ID_BYONDSTAT]")
-/* not using js
- var/list/data = list()
- for(var/atom/movable/AM as anything in T)
- var/list/got = statpanel_encode_atom(AM)
- if(!got)
- continue
- data[++data.len] = got
- src << output("[url_encode(T.name)];[url_encode(REF(T))];[icon2html(T, src, sourceonly = TRUE)];[url_encode(json_encode(data))]", "statbrowser:byond_turf_set")
-*/
+
/client/proc/unlist_turf()
// using byond atm
- if(statpanel_for_turf)
- statpanel_for_turf = FALSE
- statpanel_on_byond = FALSE
+ if(tgui_stat.byond_stat_ephemeral)
+ tgui_stat.byond_stat_ephemeral = FALSE
+ tgui_stat.byond_stat_active = FALSE
winset(src, SKIN_TAB_ID_STAT, "current-tab=[SKIN_PANE_ID_BROWSERSTAT]")
-/* not using js
- src << output(null, "statbrowser:byond_turf_unset")
- UnregisterSignal(statpanel_turf, list(
- COMSIG_ATOM_ENTERED,
- COMSIG_ATOM_EXITED,
- ))
-*/
- statpanel_turf = null
+
+ tgui_stat.byond_stat_turf = null
/client/proc/list_turf_check(turf/T)
return mob.TurfAdjacent(T)
@@ -152,12 +260,6 @@
return
if(AM.invisibility > mob.see_invisible)
return
- // not gonna bother for now - this is to prevent alt clicking to see past override image
- // if(AM in overrides)
- // return
- // meanwhile this is just a shit check in a proc because it's N^2, need proper turf obscure flags.
- // if(AM.IsObscued())
- // return
return list(
"[AM.name]",
REF(AM),
@@ -173,77 +275,6 @@
/client/proc/__stat_hook_turf_exit(datum/source, atom/movable/AM)
src << output("[url_encode(REF(AM))]", "statbrowser:byond_turf_del")
-//! external - load
-/// for legacy shit like rigsuits that didn't get the hint about not using verbs
-/client/proc/queue_legacy_verb_update()
- if(HAS_TRAIT(src, "VERB_UPDATE_QUEUED"))
- return
- ADD_TRAIT(src, "VERB_UPDATE_QUEUED", "FUCK")
- addtimer(CALLBACK(src, PROC_REF(legacy_verb_update)), 1 SECONDS)
-
-/// -_-
-/client/proc/legacy_verb_update()
- REMOVE_TRAIT(src, "VERB_UPDATE_QUEUED", "FUCK")
- init_verbs(FALSE)
-
-/// compiles a full list of verbs and sends it to the browser
-/client/proc/init_verbs(reset = FALSE)
- var/list/verblist = list()
- var/list/verbstoprocess = verbs.Copy()
- if(mob)
- verbstoprocess += mob.verbs
- for(var/atom/movable/AM as anything in mob)
- verbstoprocess += AM.verbs
- for(var/thing in verbstoprocess)
- var/procpath/verb_to_init = thing
- if(verb_to_init.hidden)
- continue
- if(!istext(verb_to_init.category) || !verb_to_init.name)
- continue
- if(verb_to_init.name[1] == ".")
- continue
- LAZYINITLIST(verblist[verb_to_init.category])
- verblist[verb_to_init.category] |= verb_to_init.name
- pass()
- src << output("[url_encode(json_encode(verblist))];[reset]", "statbrowser:byond_init_verbs")
-
-//! native
-
-/client/Stat()
- if(!statpanel_on_byond)
- return
- ..() // hit mob.Stat()
- if(!statpanel("Turf"))
- return
- if(!statpanel_turf)
- stat("No turf listed ; Alt click on an adjacent turf to view contents.")
- return
- stat(statpanel_turf.name, statpanel_turf)
-/*
- var/list/overrides = list()
- for(var/image/I in client.images)
- if(I.loc && I.loc.loc == listed_turf && I.override)
- overrides += I.loc
-*/
- for(var/atom/movable/AM as anything in statpanel_turf)
- if(!AM.mouse_opacity)
- continue
- if(AM.invisibility > using_perspective.see_invisible)
- continue
-// too expensive rn
-/*
- if(overrides.len && (A in overrides))
- continue
-*/
-// not needed rn
-/*
- if(A.IsObscured())
- continue
-*/
- stat(null, AM)
-
-//! data
-
/**
* the big, bad, Citadel Station in house stat proc.
* returns a list of "panel name" : list(entry, ...)
@@ -277,10 +308,10 @@
/client/proc/_statpanel_act(action, list/params)
switch(action)
if("reload")
- statpanel_reload()
+ tgui_stat.request_reload()
return
if("ready")
- statpanel_ready()
+ tgui_stat.ready_received()
return
if("stat_click")
var/datum/D = locate(params["ref"])
@@ -309,70 +340,3 @@
. = statpanel_data(src)
if(!islist(.))
CRASH("[.] was not list.")
-
-//! verb hooks - js stat
-
-/client/verb/hook_statpanel_ready()
- set name = ".statpanel_ready"
- set hidden = TRUE
- set instant = TRUE
-
- statpanel_ready()
-
-/client/verb/hook_statpanel_add_tab(tab as text)
- set name = ".statpanel_tab_add"
- set hidden = TRUE
- set instant = TRUE
-
- if(length(statpanel_tabs) > 50)
- return // bail
- statpanel_tabs |= tab
-
-/client/verb/hook_statpanel_remove_tab(tab as text)
- set name = ".statpanel_tab_remove"
- set hidden = TRUE
- set instant = TRUE
-
- statpanel_tabs -= tab
-
-/client/verb/hook_statpanel_wipe_tabs()
- set name = ".statpanel_tab_reset"
- set hidden = TRUE
- set instant = TRUE
-
- statpanel_tabs = list()
-
-/client/verb/hook_statpanel_set_tab(tab as text)
- set name = ".statpanel_tab"
- set hidden = TRUE
- set instant = TRUE
-
- statpanel_tab = tab
-
-/**
- * This cleanly and gracefully attempts to go to a specific tab via , for the hyperspecific purpose of interacting with the statpanel from other HTML UI
- */
-/client/verb/hook_statpanel_goto_tab(tab as text)
- set name = ".statpanel_goto_tab"
- set hidden = TRUE
- set instant = TRUE
- src << output(tab, "statbrowser:change_tab")
-
-//! verb hooks - byond stat
-
-//! verb hooks - tab switcher
-
-/client/verb/hook_statswitcher_set_tab(tab as text)
- set name = ".statswitcher"
- set hidden = TRUE
- set instant = TRUE
-
- statpanel_on_byond = (tab == "stat_pane_byond")
-
-//? Verbs - For Players
-
-/client/verb/fix_stat_panel()
- set name = "Fix Stat Panel"
- set category = VERB_CATEGORY_OOC
-
- statpanel_reset()
diff --git a/code/modules/client/cutscene.dm b/code/modules/client/cutscene.dm
index 2c94f33ec50..dda4e467597 100644
--- a/code/modules/client/cutscene.dm
+++ b/code/modules/client/cutscene.dm
@@ -1,5 +1,6 @@
// todo: if byond ever gets threaded browsers, we're standardizing tgui.html and tgui_window.dm
// because manually reimplementing this shit is driving me nuts.
+// todo: the cutscene system is kinda jank; we should just do lobbybrowser instead of using it for that.
/client/proc/init_cutscene_system()
set waitfor = FALSE
@@ -146,6 +147,11 @@
push_dispose(C)
C.cutscene_browser = FALSE
winset(C, SKIN_BROWSER_ID_CUTSCENE, "is-visible=0")
+ // just in case
+ spawn(2 SECONDS)
+ // no QDELETED check because we'll already be deleted
+ if(winget(C, "[SKIN_BROWSER_ID_CUTSCENE]", "is-visible") == "true")
+ winset(C, SKIN_BROWSER_ID_CUTSCENE, "is-visible=0")
/datum/cutscene/browser/proc/push_build(client/C, raw_html = build_inner_html())
C << output("[url_encode(json_encode(list("raw_html" = raw_html)))]", "[SKIN_BROWSER_ID_CUTSCENE]:build")
diff --git a/code/modules/client/data/client_data.dm b/code/modules/client/data/client_data.dm
index 7660a0a5e12..8b74a28b368 100644
--- a/code/modules/client/data/client_data.dm
+++ b/code/modules/client/data/client_data.dm
@@ -50,7 +50,8 @@ GLOBAL_LIST_EMPTY(client_data)
is_guest = IsGuestKey(key)
- INVOKE_ASYNC(src, PROC_REF(load_account_age))
+ spawn(0)
+ load_account_age()
var/list/the_cheese_touch = CONFIG_GET(keyed_list/shadowban)
var/client/C = GLOB.directory[src.ckey]
diff --git a/code/modules/client/data/player_data.dm b/code/modules/client/data/player_data.dm
index 08ef61a6f19..dfbc4c6faa9 100644
--- a/code/modules/client/data/player_data.dm
+++ b/code/modules/client/data/player_data.dm
@@ -63,7 +63,9 @@ GLOBAL_LIST_EMPTY(player_data)
if(isnull(available))
available = FALSE
return FALSE
- INVOKE_ASYNC(src, PROC_REF(load_blocking))
+ // make separate call chain; do not mess with client init
+ spawn(0)
+ load_blocking()
return TRUE
/datum/player_data/proc/load_blocking()
@@ -221,8 +223,9 @@ GLOBAL_LIST_EMPTY(player_data)
* async
*/
/datum/player_data/proc/log_connect()
- set waitfor = FALSE
- update_last_seen()
+ // make separate call chain; do not mess with client init
+ spawn(0)
+ update_last_seen()
/datum/player_data/proc/update_last_seen()
// don't interrupt
diff --git a/code/modules/client/game_preferences/game_preference_toggle.dm b/code/modules/client/game_preferences/game_preference_toggle.dm
index db431f3d985..c4fa3ec8aa9 100644
--- a/code/modules/client/game_preferences/game_preference_toggle.dm
+++ b/code/modules/client/game_preferences/game_preference_toggle.dm
@@ -38,7 +38,7 @@
abstract_type = /datum/game_preference_toggle/admin
category = "Admin"
-/datum/game_preference_toggle/admin/is_visible(client/user, silent)
+/datum/game_preference_toggle/admin/is_visible(client/user)
return check_rights(C = user, show_msg = FALSE)
/datum/game_preference_toggle/admin/global_looc
diff --git a/code/modules/client/game_preferences/game_preferences.dm b/code/modules/client/game_preferences/game_preferences.dm
index 3c8da9b9403..c54d71592dc 100644
--- a/code/modules/client/game_preferences/game_preferences.dm
+++ b/code/modules/client/game_preferences/game_preferences.dm
@@ -1,6 +1,14 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//
+/client/on_new_hook_stability_checks()
+ // preferences are critical; if they can't load, kick them
+ spawn(0)
+ if(!preferences.block_on_initialized(5 SECONDS))
+ disconnection_message("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!")
+ qdel(src)
+ return ..()
/**
* Game preferences
*
@@ -61,13 +69,16 @@
//* Init *//
/datum/game_preferences/proc/initialize()
- perform_initial_load()
- initialized = TRUE
+ // do not mess with client init; start a new call chain
+ spawn(0)
+ perform_initial_load()
+ initialized = TRUE
/datum/game_preferences/proc/on_reconnect()
- if(!initialized)
- return
- initialize_client()
+ // do not mess with client init; start a new call chain
+ spawn(0)
+ block_on_initialized()
+ initialize_client()
/datum/game_preferences/proc/block_on_initialized(timeout = 10 SECONDS)
var/wait_until = world.time + timeout
@@ -133,12 +144,12 @@
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)
+ if(islist(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
@@ -151,6 +162,7 @@
return TRUE
/datum/game_preferences/proc/perform_initial_load()
+ sleep(2 SECONDS)
if(!is_guest)
// only if not guest
if(SSdbcore.IsConnected())
@@ -253,9 +265,6 @@
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)
@@ -268,9 +277,6 @@
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])
diff --git a/code/modules/client/verbs/fix_statpanel.dm b/code/modules/client/verbs/fix_statpanel.dm
new file mode 100644
index 00000000000..1d63aac05a0
--- /dev/null
+++ b/code/modules/client/verbs/fix_statpanel.dm
@@ -0,0 +1,7 @@
+/client/verb/fix_stat_panel()
+ set name = "Fix Stat Panel"
+ set category = VERB_CATEGORY_OOC
+
+ if(!istype(tgui_stat))
+ tgui_stat = new(src, SKIN_BROWSER_ID_STAT)
+ tgui_stat.initialize()
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index 256211c38fb..d781b7a82aa 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -84,7 +84,7 @@
// we really hate that this is needed but it is until the screens/images reset isn't there
client.action_drawer.reassert_screen()
// reset statpanel of any verbs/whatnot
- client.statpanel_reload()
+ client.tgui_stat?.request_reload()
// update ssd overlay
addtimer(CALLBACK(src, TYPE_PROC_REF(/mob, update_ssd_overlay)), 0)
// reset cutscene to default; this is a proc for new players.
diff --git a/code/modules/mob/client.dm b/code/modules/mob/mob-client.dm
similarity index 94%
rename from code/modules/mob/client.dm
rename to code/modules/mob/mob-client.dm
index 7968f977f51..1c69bc97884 100644
--- a/code/modules/mob/client.dm
+++ b/code/modules/mob/mob-client.dm
@@ -1,5 +1,5 @@
//* This file is explicitly licensed under the MIT license. *//
-//* Copyright (c) 2024 silicons *//
+//* Copyright (c) 2024 Citadel Station Developers *//
/**
* **never directly set ckey on a mob**
diff --git a/code/modules/spells/spells.dm b/code/modules/spells/spells.dm
index 3adb922981a..c7e219ef87a 100644
--- a/code/modules/spells/spells.dm
+++ b/code/modules/spells/spells.dm
@@ -17,13 +17,13 @@
// i'm going to trust people aren't stupid and won't put the name of a regular panel in spells.
if(!length(spell_list))
- if(C.statpanel_spell_last)
+ if(C.tgui_stat.spell_last)
// dispose
- for(var/tab in C.statpanel_spell_last)
+ for(var/tab in C.tgui_stat.spell_last)
C.statpanel_tab(tab, FALSE)
- C.statpanel_spell_last = null
+ C.tgui_stat.spell_last = null
return
- LAZYINITLIST(C.statpanel_spell_last)
+ LAZYINITLIST(C.tgui_stat.spell_last)
var/list/collected = list()
for(var/spell/S in spell_list)
if(!S.panel || !S.connected_button)
@@ -39,8 +39,8 @@
if(Sp_HOLDVAR)
STATPANEL_DATA_CLICK("[S.holder_var_type] [S.holder_var_amount]", "[S.connected_button]", "\ref[S.connected_button]")
// process tabs
- var/list/removing = C.statpanel_spell_last - collected
- var/list/adding = collected - C.statpanel_spell_last
+ var/list/removing = C.tgui_stat.spell_last - collected
+ var/list/adding = collected - C.tgui_stat.spell_last
for(var/tab in adding)
C.statpanel_tab(adding, TRUE)
for(var/tab in removing)
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index 698529d3954..533d3ecb9df 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -50,11 +50,21 @@
/// processed modules
var/list/datum/modules_processed
+/**
+ * Linter check, do not call.
+ */
+/proc/lint__check_tgui_new_doesnt_sleep()
+ SHOULD_NOT_SLEEP(TRUE)
+ var/datum/tgui/tgui
+ tgui.New()
+
/**
* public
*
* Create a new UI.
*
+ * * Does not block.
+ *
* required user mob The mob who opened/is using the UI.
* required src_object datum The object or datum which owns the UI.
* required interface string The interface used to render the UI.
@@ -91,6 +101,8 @@
*
* Open this UI (and initialize it with data).
*
+ * This proc does not block.
+ *
* @params
* * data - force certain data sends
* * modules - force certain module sends
@@ -98,6 +110,7 @@
* return bool - TRUE if a new pooled window is opened, FALSE in all other situations including if a new pooled window didn't open because one already exists.
*/
/datum/tgui/proc/open(data, modules)
+ SHOULD_NOT_SLEEP(TRUE)
if(!user.client)
return FALSE
if(window)
@@ -108,8 +121,20 @@
window = SStgui.request_pooled_window(user)
if(!window)
return FALSE
+ // point of no return; call initialize() asynchronously.
opened_at = world.time
window.acquire_lock(src)
+ // defer initialize() to after current call chain.
+ spawn(0)
+ initialize(data, modules)
+ return TRUE
+
+/**
+ * Initializes the window.
+ *
+ * * Separate from open() so that open() can be non-blocking.
+ */
+/datum/tgui/proc/initialize(data, modules)
if(!window.is_ready())
window.initialize(
strict_mode = TRUE,
@@ -440,13 +465,16 @@
*
* required data The data to send
* optional force bool Send an update even if UI is not interactive.
+ *
+ * @return TRUE if data was sent, FALSE otherwise.
*/
/datum/tgui/proc/push_data(data, force)
if(!user.client || !initialized || closing)
- return
+ return FALSE
if(!force && status < UI_UPDATE)
- return
+ return FALSE
window.send_message("data", data)
+ return TRUE
/**
* public
@@ -461,13 +489,16 @@
* @params
* * updates - list(id = list(data...), ...) of modules to update.
* * force - (optional) send update even if UI is not interactive
+ *
+ * @return TRUE if data was sent, FALSE otherwise.
*/
/datum/tgui/proc/push_modules(list/updates, force)
if(isnull(user.client) || !initialized || closing)
- return
+ return FALSE
if(!force && status < UI_UPDATE)
- return
+ return FALSE
window.send_message("modules", updates)
+ return TRUE
//* Module System *//
diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm
index 97d3ac21648..035eba008f7 100644
--- a/code/modules/tgui/tgui_window.dm
+++ b/code/modules/tgui/tgui_window.dm
@@ -11,7 +11,9 @@
var/is_browser = FALSE
var/status = TGUI_WINDOW_CLOSED
var/locked = FALSE
+ // todo: combine with subscriber_object
var/datum/tgui/locked_by
+ // todo: combine with locked_by
var/datum/subscriber_object
var/subscriber_delegate
var/fatally_errored = FALSE
@@ -29,15 +31,26 @@
var/initial_inline_css
var/mouse_event_macro_set = FALSE
+/**
+ * Linter check, do not call.
+ */
+/proc/lint__check_tgui_window_new_doesnt_sleep()
+ SHOULD_NOT_SLEEP(TRUE)
+ var/datum/tgui_window/window
+ window.New()
+
/**
* public
*
* Create a new tgui window.
*
+ * * Does not block.
+ *
* required client /client
* required id string A unique window identifier.
*/
/datum/tgui_window/New(client/client, id, pooled = FALSE)
+ ASSERT(!client.tgui_windows[id])
src.id = id
src.client = client
src.client.tgui_windows[id] = src
diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm
index c112a8c147e..4c8e5174478 100644
--- a/code/modules/tgui_panel/external.dm
+++ b/code/modules/tgui_panel/external.dm
@@ -3,8 +3,6 @@
*! SPDX-License-Identifier: MIT
*/
-/client/var/datum/tgui_panel/tgui_panel
-
/**
* tgui panel / chat troubleshooting verb
*/
@@ -31,7 +29,7 @@
log_tgui(src, "tgui_panel datum is missing",
context = "verb/fix_tgui_panel")
tgui_panel = new(src)
- tgui_panel.initialize(force = TRUE)
+ tgui_panel.initialize()
// Force show the panel to see if there are any errors
winset(src, "output", "is-disabled=1&is-visible=0")
winset(src, "browseroutput", "is-disabled=0;is-visible=1")
diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm
index 806b5e6b052..79f9b0d5d4a 100644
--- a/code/modules/tgui_panel/tgui_panel.dm
+++ b/code/modules/tgui_panel/tgui_panel.dm
@@ -34,14 +34,30 @@
return !broken && window.is_ready()
/**
- * public
+ * Initializes the chat panel.
*
- * Initializes tgui panel.
+ * * This is asynchronous and will yield for an appropriate duration for the client's current state.
*/
-/datum/tgui_panel/proc/initialize(force = FALSE)
- set waitfor = FALSE
- // Minimal sleep to defer initialization to after client constructor
- sleep(1)
+/datum/tgui_panel/proc/initialize()
+ if(!client.initialized)
+ // todo: this should be a timer, but current MC doesn't really support that until we have
+ // MC init stages
+ spawn(1 SECONDS)
+ UNTIL(!client || client.initialized)
+ if(!client)
+ return
+ boot()
+ else
+ spawn(0)
+ boot()
+
+/**
+ * Boots the panel.
+ *
+ * * This is a blocking proc.
+ */
+/datum/tgui_panel/proc/boot()
+ PRIVATE_PROC(TRUE)
initialized_at = world.time
// Perform a clean initialization
window.initialize(
diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm
index 9803d4fb96a..0b5ff331feb 100644
--- a/code/modules/tooltip/tooltip.dm
+++ b/code/modules/tooltip/tooltip.dm
@@ -32,30 +32,45 @@ Notes:
/datum/tooltip
- var/client/owner
+ var/client/client
var/control = "mainwindow.tooltip"
var/showing = 0
var/queueHide = 0
var/init = 0
-
/datum/tooltip/New(client/C)
if(!C)
return
- owner = C
- addtimer(CALLBACK(src, PROC_REF(init)), 0)
+ client = C
-/datum/tooltip/proc/init()
- SSassets.send_asset_pack(owner, /datum/asset_pack/simple/jquery)
- owner << browse(file2text('code/modules/tooltip/tooltip.html'), "window=[control]")
+/datum/tooltip/Destroy()
+ client = null
+ return ..()
+
+/datum/tooltip/proc/initialize()
+ if(!client.initialized)
+ // todo: this should be a timer, but current MC doesn't really support that until we have
+ // MC init stages
+ spawn(1 SECONDS)
+ UNTIL(!client || client.initialized)
+ if(!client)
+ return
+ boot()
+ else
+ spawn(0)
+ boot()
+
+/datum/tooltip/proc/boot()
+ SSassets.send_asset_pack(client, /datum/asset_pack/simple/jquery)
+ client << browse(file2text('code/modules/tooltip/tooltip.html'), "window=[control]")
/datum/tooltip/proc/show(atom/movable/thing, params = null, title = null, content = null, theme = "default", special = "none")
- if (!thing || !params || (!title && !content) || !owner || !isnum(world.icon_size))
+ if (!thing || !params || (!title && !content) || !client || !isnum(world.icon_size))
return FALSE
if (!init)
//Initialize some vars
init = 1
- owner << output(list2params(list(world.icon_size, control)), "[control]:tooltip.init")
+ client << output(list2params(list(world.icon_size, control)), "[control]:tooltip.init")
showing = 1
@@ -75,7 +90,7 @@ Notes:
params = {"{ "cursor": "[params]", "screenLoc": "[thing.screen_loc]" }"}
//Send stuff to the tooltip
- owner << output(list2params(list(params, owner.current_viewport_width, owner.current_viewport_height, "[title][content]", theme, special)), "[control]:tooltip.update")
+ client << output(list2params(list(params, client.current_viewport_width, client.current_viewport_height, "[title][content]", theme, special)), "[control]:tooltip.update")
//If a hide() was hit while we were showing, run hide() again to avoid stuck tooltips
showing = 0
@@ -95,7 +110,7 @@ Notes:
return TRUE
/datum/tooltip/proc/do_hide()
- winshow(owner, control, FALSE)
+ winshow(client, control, FALSE)
/* TG SPECIFIC CODE */