Files
Bubberstation/code/modules/admin/view_variables/reference_tracking.dm
T
Lucy 0fe77e9dd3 read-only alist support for VV (#95501)
## About The Pull Request

This adds experimental read-only support for `/alist`s in VV.

I'm not quite sure how to add support for properly changing alists, but
this is better than nothing at least.

### Before

<img width="474" height="262" alt="image"
src="https://github.com/user-attachments/assets/8701fc64-855b-429e-88d1-f6607000b2c8"
/>

### After

<img width="943" height="539" alt="2026-03-23 (1774313721) ~
dreamseeker"
src="https://github.com/user-attachments/assets/cb6caf2a-19fd-49a9-b228-1bccb2c315f3"
/>

## Why It's Good For The Game

Should be able to at least view alists with VV.

## Changelog
🆑
admin: View Variables can now properly view alists, altho it cannot edit
them.
/🆑
2026-04-03 15:26:31 +01:00

280 lines
9.7 KiB
Plaintext

#ifdef REFERENCE_TRACKING
#define REFSEARCH_RECURSE_LIMIT 64
#ifdef FAST_REFERENCE_TRACKING
// typecache of types that almost certainly have no refs, and thus can be safely skipped when finding references
GLOBAL_ALIST_INIT(reftracker_skip_typecache, init_reftracker_skip_typecache())
// empty alist we swap with GLOB.reftracker_skip_typecache whenever someone calls toggle_fast_reftracking
GLOBAL_ALIST_EMPTY(reftracker_skip_typecache_b)
/proc/toggle_fast_reftracking()
var/alist/a = GLOB.reftracker_skip_typecache
var/alist/b = GLOB.reftracker_skip_typecache_b
GLOB.reftracker_skip_typecache = b
GLOB.reftracker_skip_typecache_b = a
/proc/init_reftracker_skip_typecache()
. = alist()
for(var/base_type in list(
/icon,
/matrix,
/regex,
/atom/movable/mirage_holder,
/atom/movable/render_step/emissive_blocker,
/datum/armor,
/datum/asset_cache_item,
/datum/book_info,
/datum/card,
/datum/chat_payload,
/datum/comm_log_entry,
/datum/gas_mixture,
/datum/greyscale_layer,
/datum/icon_transformer,
/datum/instrument_key,
/datum/lighting_object, // only contains turf and MA refs
/datum/movespeed_modifier,
/datum/painting,
/datum/paper_input,
/datum/physiology,
/datum/plant_gene/reagent,
/datum/qdel_item,
/datum/stack_recipe,
/datum/tlv,
/datum/universal_icon,
/datum/weakref,
/datum/z_pillar,
/obj/effect/abstract/z_holder,
// stuff below isn't 100% guaranteed to be ref-free, but they're prolly not an issue
/turf/closed/mineral,
/turf/open/lava,
/turf/open/misc/asteroid,
/turf/open/openspace,
/turf/open/space,
/obj/structure/flora, // icebox and such has a LOT of these
/datum/chatmessage,
/datum/lighting_corner,
/datum/log_entry, // hopefully nobody's silly enough to accidentally pass a reference to these... right???
/datum/reagent/consumable/nutriment,
))
for(var/type in typesof(base_type))
.[type] = TRUE
#endif
/datum/proc/find_references(references_to_clear = INFINITY)
if(usr?.client)
if(tgui_alert(usr,"Running this will lock everything up for about 5 minutes. Would you like to begin the search?", "Find References", list("Yes", "No")) != "Yes")
return
src.references_to_clear = references_to_clear
//this keeps the garbage collector from failing to collect objects being searched for in here
SSgarbage.can_fire = FALSE
_search_references()
//restart the garbage collector
SSgarbage.can_fire = TRUE
SSgarbage.update_nextfire(reset_time = TRUE)
/datum/proc/_search_references()
log_reftracker("Beginning search for references to a [type], looking for [references_to_clear] refs.")
var/starting_time = world.time
//Time to search the whole game for our ref
DoSearchVar(GLOB, "GLOB", starting_time) //globals
log_reftracker("Finished searching globals")
if(src.references_to_clear == 0)
return
//Yes we do actually need to do this. The searcher refuses to read weird lists
//And global.vars is a really weird list
var/global_vars = list()
for(var/key in global.vars)
global_vars[key] = global.vars[key]
DoSearchVar(global_vars, "Native Global", starting_time)
log_reftracker("Finished searching native globals")
if(src.references_to_clear == 0)
return
#ifdef FAST_REFERENCE_TRACKING
var/alist/skip_types = GLOB.reftracker_skip_typecache
#endif
for(var/datum/thing in world) //atoms (don't beleive its lies)
#ifdef FAST_REFERENCE_TRACKING
if(skip_types[thing.type])
continue
#endif
DoSearchVar(thing, "World -> [thing.type]", starting_time)
if(src.references_to_clear == 0)
break
log_reftracker("Finished searching atoms")
if(src.references_to_clear == 0)
return
for(var/datum/thing) //datums
#ifdef FAST_REFERENCE_TRACKING
if(skip_types[thing.type])
continue
#endif
DoSearchVar(thing, "Datums -> [thing.type]", starting_time)
if(src.references_to_clear == 0)
break
log_reftracker("Finished searching datums")
if(src.references_to_clear == 0)
return
//Warning, attempting to search clients like this will cause crashes if done on live. Watch yourself
#ifndef REFERENCE_DOING_IT_LIVE
for(var/client/thing) //clients
DoSearchVar(thing, "Clients -> [thing.type]", starting_time)
if(src.references_to_clear == 0)
break
log_reftracker("Finished searching clients")
if(src.references_to_clear == 0)
return
#endif
log_reftracker("Completed search for references to a [type].")
/datum/proc/DoSearchVar(potential_container, container_name, search_time, recursion_count, is_special_list)
if(recursion_count >= REFSEARCH_RECURSE_LIMIT)
log_reftracker("Recursion limit reached. [container_name]")
return
if(references_to_clear == 0)
return
//Check each time you go down a layer. This makes it a bit slow, but it won't effect the rest of the game at all
#ifndef FIND_REF_NO_CHECK_TICK
CHECK_TICK
#endif
if(isdatum(potential_container))
var/datum/datum_container = potential_container
if(datum_container.last_find_references == search_time)
return
datum_container.last_find_references = search_time
var/list/vars_list = datum_container.vars
var/is_atom = FALSE
var/is_area = FALSE
if(isatom(datum_container))
is_atom = TRUE
if(isarea(datum_container))
is_area = TRUE
for(var/varname in vars_list)
var/variable = vars_list[varname]
if(islist(variable))
//Fun fact, vis_locs don't count for references
if(varname == "vars" || (is_atom && (varname == "vis_locs" || varname == "overlays" || varname == "underlays" || varname == "filters" || varname == "verbs" || (is_area && varname == "contents"))))
continue
// We do this after the varname check to avoid area contents (reading it incures a world loop's worth of cost)
if(!length(variable))
continue
DoSearchVar(variable,\
"[container_name] [datum_container.ref_search_details()] -> [varname] (list)",\
search_time,\
recursion_count + 1,\
/*is_special_list = */ is_atom && (varname == "contents" || varname == "vis_contents" || varname == "locs"))
else if(variable == src)
#ifdef REFERENCE_TRACKING_DEBUG
if(SSgarbage.should_save_refs)
if(!found_refs)
found_refs = list()
found_refs[varname] = TRUE
continue //End early, don't want these logging
else
log_reftracker("Found [type] [text_ref(src)] in [datum_container.type]'s [datum_container.ref_search_details()] [varname] var. [container_name]")
#else
log_reftracker("Found [type] [text_ref(src)] in [datum_container.type]'s [datum_container.ref_search_details()] [varname] var. [container_name]")
#endif
references_to_clear -= 1
if(references_to_clear == 0)
log_reftracker("All references to [type] [text_ref(src)] found, exiting.")
return
continue
else if(islist(potential_container))
var/list/potential_cache = potential_container
var/is_alist = isalist(potential_cache)
for(var/element_in_list in potential_cache)
//Check normal sublists
if(islist(element_in_list))
if(length(element_in_list))
DoSearchVar(element_in_list, "[container_name] -> [element_in_list] (list)", search_time, recursion_count + 1)
//Check normal entrys
else if(element_in_list == src)
#ifdef REFERENCE_TRACKING_DEBUG
if(SSgarbage.should_save_refs)
if(!found_refs)
found_refs = list()
found_refs[potential_cache] = TRUE
continue
else
log_reftracker("Found [type] [text_ref(src)] in list [container_name].")
#else
log_reftracker("Found [type] [text_ref(src)] in list [container_name].")
#endif
// This is dumb as hell I'm sorry
// I don't want the garbage subsystem to count as a ref for the purposes of this number
// If we find all other refs before it I want to early exit, and if we don't I want to keep searching past it
var/ignore_ref = FALSE
var/list/queues = SSgarbage.queues
for(var/list/queue in queues)
if(potential_cache in queue)
ignore_ref = TRUE
break
if(ignore_ref)
log_reftracker("[container_name] does not count as a ref for our count")
else
references_to_clear -= 1
if(references_to_clear == 0)
log_reftracker("All references to [type] [text_ref(src)] found, exiting.")
return
if((!isnum(element_in_list) || is_alist) && !is_special_list)
// This exists to catch an error that throws when we access a special list
// is_special_list is a hint, it can be wrong
try
var/assoc_val = potential_cache[element_in_list]
//Check assoc sublists
if(islist(assoc_val))
if(length(assoc_val))
DoSearchVar(potential_container[element_in_list], "[container_name]\[[element_in_list]\] -> [assoc_val] (list)", search_time, recursion_count + 1)
//Check assoc entry
else if(assoc_val == src)
#ifdef REFERENCE_TRACKING_DEBUG
if(SSgarbage.should_save_refs)
if(!found_refs)
found_refs = list()
found_refs[potential_cache] = TRUE
continue
else
log_reftracker("Found [type] [text_ref(src)] in list [container_name]\[[element_in_list]\]")
#else
log_reftracker("Found [type] [text_ref(src)] in list [container_name]\[[element_in_list]\]")
#endif
references_to_clear -= 1
if(references_to_clear == 0)
log_reftracker("All references to [type] [text_ref(src)] found, exiting.")
return
catch
// So if it goes wrong we kill it
is_special_list = TRUE
log_reftracker("Curiosity: [container_name] lead to an error when acessing [element_in_list], what is it?")
#undef REFSEARCH_RECURSE_LIMIT
#endif
// Kept outside the ifdef so overrides are easy to implement
/// Return info about us for reference searching purposes
/// Will be logged as a representation of this datum if it's a part of a search chain
/datum/proc/ref_search_details()
return text_ref(src)
/datum/callback/ref_search_details()
return "[text_ref(src)] (obj: [object] proc: [delegate] args: [json_encode(arguments)] user: [user?.resolve() || "null"])"