From 19e524ac849b4a47badfbc41823e91affc5b688c Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Sun, 18 Sep 2022 07:27:19 +0200 Subject: [PATCH] [MIRROR] Object Window Niceties [MDB IGNORE] (#16292) * Object Window Niceties (#69825) * Object Window Niceties Alright. I got bored and polished up the object/alt click window. It had a few issues: First, we generated all our images in bulk, as soon as requested Second, the caching was global, despite only working on a client to client basis Third, we only generated up to 10 images. This could be fine, but the javascript code will continuiously rerender assuming unrendered images will come eventually, and they well, weren't. This caused MASSIVE clientside lag Fourth and finally, I did not like how moving away from the viewed turf lagged behind, in sync with the stat tab update. Looked bad. I've resolved all these. I solved the first three issues by reworking how obj images were generatated and managed. Rather then storing a basic cache on the subsystem, and doing all the image generation at once, we queue up image generation as we like, and generate images inside a new processing subsystem fire. This isn't the best solution, since it still eats cpu somewhat, but it's a whole lot better then the other options, outside either removing the need to getflat, or somehow predicting what items a client will want to see I've started storing three bits of info. First, a list of all the objects we currently want to display. Second, a list of atom -> image html Third, a list of atoms to imageify. This information is stored on a datum on /client, since I want this to have a lifetime linked to well, clients. I've used this datum to solve that fourth bit, using a component I made for parallax a bit back. This lets me react to our client's mob, and update the tab linked to that, rather then on a subsystem call by call basis. That's about it. Co-authored-by: san7890 * Object Window Niceties Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Co-authored-by: san7890 --- code/_onclick/click.dm | 6 +- .../subsystem/processing/obj_tab_items.dm | 24 +++ code/controllers/subsystem/statpanel.dm | 186 ++++++++++++++---- code/modules/client/client_defines.dm | 2 + code/modules/client/client_procs.dm | 2 + tgstation.dme | 1 + 6 files changed, 184 insertions(+), 37 deletions(-) create mode 100644 code/controllers/subsystem/processing/obj_tab_items.dm diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 334b7c11e1b..b8e386e1136 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -418,8 +418,7 @@ return var/turf/T = get_turf(src) if(T && (isturf(loc) || isturf(src)) && user.TurfAdjacent(T) && !HAS_TRAIT(user, TRAIT_MOVE_VENTCRAWLING)) - user.listed_turf = T - user.client.stat_panel.send_message("create_listedturf", T.name) + user.set_listed_turf(T) ///The base proc of when something is right clicked on when alt is held - generally use alt_click_secondary instead /atom/proc/alt_click_on_secondary(atom/A) @@ -442,8 +441,7 @@ /atom/proc/AltClickNoInteract(mob/user, atom/A) var/turf/T = get_turf(A) if(T && user.TurfAdjacent(T)) - user.listed_turf = T - user.client.stat_panel.send_message("create_listedturf", T.name) + user.set_listed_turf(T) /mob/proc/TurfAdjacent(turf/T) return T.Adjacent(src) diff --git a/code/controllers/subsystem/processing/obj_tab_items.dm b/code/controllers/subsystem/processing/obj_tab_items.dm new file mode 100644 index 00000000000..53786daf011 --- /dev/null +++ b/code/controllers/subsystem/processing/obj_tab_items.dm @@ -0,0 +1,24 @@ +PROCESSING_SUBSYSTEM_DEF(obj_tab_items) + name = "Obj Tab Items" + flags = SS_NO_INIT + runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT + wait = 0.1 SECONDS + +// I know this is mostly copypasta, but I want to change the processing logic +// Sorry bestie :( +/datum/controller/subsystem/processing/obj_tab_items/fire(resumed = FALSE) + if (!resumed) + currentrun = processing.Copy() + //cache for sanic speed (lists are references anyways) + var/list/current_run = currentrun + + while(current_run.len) + var/datum/thing = current_run[current_run.len] + if(QDELETED(thing)) + processing -= thing + else if(thing.process(wait * 0.1) == PROCESS_KILL) + // fully stop so that a future START_PROCESSING will work + STOP_PROCESSING(src, thing) + if (MC_TICK_CHECK) + return + current_run.len-- diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm index 22ec21825c9..e016c293526 100644 --- a/code/controllers/subsystem/statpanel.dm +++ b/code/controllers/subsystem/statpanel.dm @@ -8,7 +8,6 @@ SUBSYSTEM_DEF(statpanels) var/list/currentrun = list() var/list/global_data var/list/mc_data - var/list/cached_images = list() ///how many subsystem fires between most tab updates var/default_wait = 10 @@ -107,14 +106,10 @@ SUBSYSTEM_DEF(statpanels) if(update_actions && num_fires % default_wait == 0) set_action_tabs(target, target_mob) - // Handle the examined turf of the stat panel - - if(target_mob?.listed_turf && num_fires % default_wait == 0) - if(!target_mob.TurfAdjacent(target_mob.listed_turf) || isnull(target_mob.listed_turf)) - target.stat_panel.send_message("remove_listedturf") - target_mob.listed_turf = null - - else if(target.stat_tab == target_mob?.listed_turf.name || !(target_mob?.listed_turf.name in target.panel_tabs)) + // Handle the examined turf of the stat panel, if it's been long enough, or if we've generated new images for it + var/turf/listed_turf = target_mob?.listed_turf + if(listed_turf && num_fires % default_wait == 0) + if(target.stat_tab == listed_turf.name || !(listed_turf.name in target.panel_tabs)) set_turf_examine_tab(target, target_mob) if(MC_TICK_CHECK) @@ -191,14 +186,12 @@ SUBSYSTEM_DEF(statpanels) /datum/controller/subsystem/statpanels/proc/set_turf_examine_tab(client/target, mob/target_mob) var/list/overrides = list() - var/list/turfitems = list() for(var/image/target_image as anything in target.images) if(!target_image.loc || target_image.loc.loc != target_mob.listed_turf || !target_image.override) continue overrides += target_image.loc - turfitems[++turfitems.len] = list("[target_mob.listed_turf]", REF(target_mob.listed_turf), icon2html(target_mob.listed_turf, target, sourceonly=TRUE)) - + var/list/atoms_to_display = list(target_mob.listed_turf) for(var/atom/movable/turf_content as anything in target_mob.listed_turf) if(turf_content.mouse_opacity == MOUSE_OPACITY_TRANSPARENT) continue @@ -208,24 +201,54 @@ SUBSYSTEM_DEF(statpanels) continue if(turf_content.IsObscured()) continue + atoms_to_display += turf_content - if(length(turfitems) < 10) // only create images for the first 10 items on the turf, for performance reasons - var/turf_content_ref = REF(turf_content) - if(!(turf_content_ref in cached_images)) - cached_images += turf_content_ref - turf_content.RegisterSignal(turf_content, COMSIG_PARENT_QDELETING, /atom/.proc/remove_from_cache) // we reset cache if anything in it gets deleted + /// Set the atoms we're meant to display + var/datum/object_window_info/obj_window = target.obj_window + obj_window.atoms_to_show = atoms_to_display + START_PROCESSING(SSobj_tab_items, obj_window) + refresh_client_obj_view(target) - if(ismob(turf_content) || length(turf_content.overlays) > 2) - turfitems[++turfitems.len] = list("[turf_content.name]", turf_content_ref, costly_icon2html(turf_content, target, sourceonly=TRUE)) - else - turfitems[++turfitems.len] = list("[turf_content.name]", turf_content_ref, icon2html(turf_content, target, sourceonly=TRUE)) - else - turfitems[++turfitems.len] = list("[turf_content.name]", turf_content_ref) - else - turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content)) +/datum/controller/subsystem/statpanels/proc/refresh_client_obj_view(client/refresh) + var/list/turf_items = return_object_images(refresh) + if(!length(turf_items) || !refresh.mob?.listed_turf) + return + refresh.stat_panel.send_message("update_listedturf", turf_items) - turfitems = turfitems - target.stat_panel.send_message("update_listedturf", turfitems) +#define OBJ_IMAGE_LOADING "statpanels obj loading temporary" +/// Returns all our ready object tab images +/// Returns a list in the form list(list(object_name, object_ref, loaded_image), ...) +/datum/controller/subsystem/statpanels/proc/return_object_images(client/load_from) + // You might be inclined to think that this is a waste of cpu time, since we + // A: Double iterate over atoms in the build case, or + // B: Generate these lists over and over in the refresh case + // It's really not very hot. The hot portion of this code is genuinely mostly in the image generation + // So it's ok to pay a performance cost for cleanliness here + + // No turf? go away + if(!load_from.mob?.listed_turf) + return list() + var/datum/object_window_info/obj_window = load_from.obj_window + var/list/already_seen = obj_window.atoms_to_images + var/list/to_make = obj_window.atoms_to_imagify + var/list/turf_items = list() + for(var/atom/turf_item as anything in obj_window.atoms_to_show) + // First, we fill up the list of refs to display + // If we already have one, just use that + var/existing_image = already_seen[turf_item] + if(existing_image == OBJ_IMAGE_LOADING) + continue + // We already have it. Success! + if(existing_image) + turf_items[++turf_items.len] = list("[turf_item.name]", REF(turf_item), existing_image) + continue + // Now, we're gonna queue image generation out of those refs + to_make += turf_item + already_seen[turf_item] = OBJ_IMAGE_LOADING + obj_window.RegisterSignal(turf_item, COMSIG_PARENT_QDELETING, /datum/object_window_info/proc/viewing_atom_deleted) // we reset cache if anything in it gets deleted + return turf_items + +#undef OBJ_IMAGE_LOADING /datum/controller/subsystem/statpanels/proc/generate_mc_data() mc_data = list( @@ -271,8 +294,7 @@ SUBSYSTEM_DEF(statpanels) if(target_mob?.listed_turf) if(!target_mob.TurfAdjacent(target_mob.listed_turf)) - target.stat_panel.send_message("removed_listedturf") - target_mob.listed_turf = null + target_mob.set_listed_turf(null) else if(target.stat_tab == target_mob?.listed_turf.name || !(target_mob?.listed_turf.name in target.panel_tabs)) set_turf_examine_tab(target, target_mob) @@ -295,9 +317,107 @@ SUBSYSTEM_DEF(statpanels) else if(length(GLOB.sdql2_queries) && target.stat_tab == "SDQL2") set_SDQL2_tab(target) -/atom/proc/remove_from_cache() - SIGNAL_HANDLER - SSstatpanels.cached_images -= REF(src) - /// Stat panel window declaration /client/var/datum/tgui_window/stat_panel + +/// Datum that holds and tracks info about a client's object window +/// Really only exists because I want to be able to do logic with signals +/// And need a safe place to do the registration +/datum/object_window_info + /// list of atoms to show to our client via the object tab, at least currently + var/list/atoms_to_show = list() + /// list of atom -> image string for objects we have had in the right click tab + /// this is our caching + var/list/atoms_to_images = list() + /// list of atoms to turn into images for the object tab + var/list/atoms_to_imagify = list() + /// Our owner client + var/client/parent + /// Are we currently tracking a turf? + var/actively_tracking = FALSE + +/datum/object_window_info/New(client/parent) + . = ..() + src.parent = parent + +/datum/object_window_info/Destroy(force, ...) + atoms_to_show = null + atoms_to_images = null + atoms_to_imagify = null + parent.obj_window = null + parent = null + STOP_PROCESSING(SSobj_tab_items, src) + return ..() + +/// Takes a client, attempts to generate object images for it +/// We will update the client with any improvements we make when we're done +/datum/object_window_info/process(delta_time) + // Cache the datum access for sonic speed + var/list/to_make = atoms_to_imagify + var/list/newly_seen = atoms_to_images + var/index = 0 + for(index in 1 to length(to_make)) + var/atom/thing = to_make[index] + + var/generated_string + if(ismob(thing) || length(thing.overlays) > 2) + generated_string = costly_icon2html(thing, parent, sourceonly=TRUE) + else + generated_string = icon2html(thing, parent, sourceonly=TRUE) + + newly_seen[thing] = generated_string + if(TICK_CHECK) + to_make.Cut(1, index + 1) + index = 0 + break + // If we've not cut yet, do it now + if(index) + to_make.Cut(1, index + 1) + SSstatpanels.refresh_client_obj_view(parent) + if(!length(to_make)) + return PROCESS_KILL + +/datum/object_window_info/proc/start_turf_tracking() + if(actively_tracking) + stop_turf_tracking() + var/static/list/connections = list( + COMSIG_MOVABLE_MOVED = .proc/on_mob_move, + COMSIG_MOB_LOGOUT = .proc/on_mob_logout, + ) + AddComponent(/datum/component/connect_mob_behalf, parent, connections) + actively_tracking = TRUE + +/datum/object_window_info/proc/stop_turf_tracking() + qdel(GetComponent(/datum/component/connect_mob_behalf)) + actively_tracking = FALSE + +/datum/object_window_info/proc/on_mob_move(mob/source) + SIGNAL_HANDLER + var/turf/listed = source.listed_turf + if(!listed || !source.TurfAdjacent(listed)) + source.set_listed_turf(null) + +/datum/object_window_info/proc/on_mob_logout(mob/source) + SIGNAL_HANDLER + on_mob_move(parent.mob) + +/// Clears any cached object window stuff +/// We use hard refs cause we'd need a signal for this anyway. Cleaner this way +/datum/object_window_info/proc/viewing_atom_deleted(atom/deleted) + SIGNAL_HANDLER + atoms_to_show -= deleted + atoms_to_imagify -= deleted + atoms_to_images -= deleted + +/mob/proc/set_listed_turf(turf/new_turf) + listed_turf = new_turf + if(!client) + return + if(!client.obj_window) + client.obj_window = new(client) + if(listed_turf) + client.stat_panel.send_message("create_listedturf", listed_turf.name) + client.obj_window.start_turf_tracking() + else + client.stat_panel.send_message("remove_listedturf") + client.obj_window.stop_turf_tracking() diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 577b9de0992..10c7125d7fd 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -194,6 +194,8 @@ var/list/spell_tabs = list() ///A lazy list of atoms we've examined in the last RECENT_EXAMINE_MAX_WINDOW (default 2) seconds, so that we will call [/atom/proc/examine_more] instead of [/atom/proc/examine] on them when examining var/list/recent_examines + ///Our object window datum. It stores info about and handles behavior for the object tab + var/datum/object_window_info/obj_window var/list/parallax_layers var/list/parallax_layers_cached diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index dee25f3abdf..0cb6efbbd4c 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -553,6 +553,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( SSserver_maint.UpdateHubStatus() if(credits) QDEL_LIST(credits) + if(obj_window) + QDEL_NULL(obj_window) if(holder) adminGreet(1) holder.owner = null diff --git a/tgstation.dme b/tgstation.dme index e4c175e391f..272df2acfd2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -641,6 +641,7 @@ #include "code\controllers\subsystem\processing\instruments.dm" #include "code\controllers\subsystem\processing\networks.dm" #include "code\controllers\subsystem\processing\obj.dm" +#include "code\controllers\subsystem\processing\obj_tab_items.dm" #include "code\controllers\subsystem\processing\plumbing.dm" #include "code\controllers\subsystem\processing\processing.dm" #include "code\controllers\subsystem\processing\projectiles.dm"