diff --git a/byond-extools.dll b/byond-extools.dll
index 4910fad01b..bd6b34c48e 100644
Binary files a/byond-extools.dll and b/byond-extools.dll differ
diff --git a/code/__DEFINES/qdel.dm b/code/__DEFINES/qdel.dm
index 4296e3c2e9..63259774fa 100644
--- a/code/__DEFINES/qdel.dm
+++ b/code/__DEFINES/qdel.dm
@@ -6,10 +6,17 @@
#define QDEL_HINT_IWILLGC 2 //functionally the same as the above. qdel should assume the object will gc on its own, and not check it.
#define QDEL_HINT_HARDDEL 3 //qdel should assume this object won't gc, and queue a hard delete using a hard reference.
#define QDEL_HINT_HARDDEL_NOW 4 //qdel should assume this object won't gc, and hard del it post haste.
-#define QDEL_HINT_FINDREFERENCE 5 //functionally identical to QDEL_HINT_QUEUE if TESTING is not enabled in _compiler_options.dm.
- //if TESTING is enabled, qdel will call this object's find_references() verb.
-#define QDEL_HINT_IFFAIL_FINDREFERENCE 6 //Above but only if gc fails.
-//defines for the gc_destroyed var
+
+#ifdef LEGACY_REFERENCE_TRACKING
+/** If LEGACY_REFERENCE_TRACKING is enabled, qdel will call this object's find_references() verb.
+ *
+ * Functionally identical to QDEL_HINT_QUEUE if GC_FAILURE_HARD_LOOKUP is not enabled in _compiler_options.dm.
+*/
+#define QDEL_HINT_FINDREFERENCE 5
+/// Behavior as QDEL_HINT_FINDREFERENCE, but only if the GC fails and a hard delete is forced.
+#define QDEL_HINT_IFFAIL_FINDREFERENCE 6
+#endif
+
#define GC_QUEUE_CHECK 1
#define GC_QUEUE_HARDDELETE 2
diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm
index 101330cc8b..fe46fdc710 100644
--- a/code/__DEFINES/vv.dm
+++ b/code/__DEFINES/vv.dm
@@ -75,6 +75,7 @@
#define VV_HK_MARK "mark"
#define VV_HK_ADDCOMPONENT "addcomponent"
#define VV_HK_MODIFY_TRAITS "modtraits"
+#define VV_HK_VIEW_REFERENCES "viewreferences"
// /datum/gas_mixture
#define VV_HK_SET_MOLES "set_moles"
diff --git a/code/_compile_options.dm b/code/_compile_options.dm
index 9a36c626ba..0e0bd4ffaa 100644
--- a/code/_compile_options.dm
+++ b/code/_compile_options.dm
@@ -11,15 +11,28 @@
#ifdef TESTING
#define DATUMVAR_DEBUGGING_MODE
-//#define GC_FAILURE_HARD_LOOKUP //makes paths that fail to GC call find_references before del'ing.
- //implies FIND_REF_NO_CHECK_TICK
+/*
+* Enables extools-powered reference tracking system, letting you see what is referencing objects that refuse to hard delete.
+*
+* * Requires TESTING to be defined to work.
+*/
+//#define REFERENCE_TRACKING
-//#define FIND_REF_NO_CHECK_TICK //Sets world.loop_checks to false and prevents find references from sleeping
+///Method of tracking references without using extools. Slower, kept to avoid over-reliance on extools.
+//#define LEGACY_REFERENCE_TRACKING
+#ifdef LEGACY_REFERENCE_TRACKING
+///Use the legacy reference on things hard deleting by default.
+//#define GC_FAILURE_HARD_LOOKUP
+#ifdef GC_FAILURE_HARD_LOOKUP
+#define FIND_REF_NO_CHECK_TICK
+#endif //ifdef GC_FAILURE_HARD_LOOKUP
+
+#endif //ifdef LEGACY_REFERENCE_TRACKING
//#define VISUALIZE_ACTIVE_TURFS //Highlights atmos active turfs in green
-#endif
+#endif //ifdef TESTING
//#define UNIT_TESTS //Enables unit tests via TEST_RUN_PARAMETER
#ifndef PRELOAD_RSC //set to:
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index 2ae57f0a71..db099f1bcf 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -536,4 +536,4 @@
config_entry_value = 6
/datum/config_entry/number/max_shuttle_size
- config_entry_value = 250
+ config_entry_value = 500
diff --git a/code/controllers/subsystem/garbage.dm b/code/controllers/subsystem/garbage.dm
index 8a1c08bc35..b46e22c1fa 100644
--- a/code/controllers/subsystem/garbage.dm
+++ b/code/controllers/subsystem/garbage.dm
@@ -25,7 +25,7 @@ SUBSYSTEM_DEF(garbage)
//Queue
var/list/queues
- #ifdef TESTING
+ #ifdef LEGACY_REFERENCE_TRACKING
var/list/reference_find_on_fail = list()
var/list/reference_find_on_fail_types = list()
#endif
@@ -134,7 +134,7 @@ SUBSYSTEM_DEF(garbage)
++gcedlasttick
++totalgcs
pass_counts[level]++
- #ifdef TESTING
+ #ifdef LEGACY_REFERENCE_TRACKING
reference_find_on_fail -= refID //It's deleted we don't care anymore.
#endif
if (MC_TICK_CHECK)
@@ -145,7 +145,9 @@ SUBSYSTEM_DEF(garbage)
fail_counts[level]++
switch (level)
if (GC_QUEUE_CHECK)
- #ifdef TESTING
+ #ifdef REFERENCE_TRACKING
+ D.find_references()
+ #elif defined(LEGACY_REFERENCE_TRACKING)
if(reference_find_on_fail[refID])
D.find_references()
#ifdef GC_FAILURE_HARD_LOOKUP
@@ -156,7 +158,19 @@ SUBSYSTEM_DEF(garbage)
#endif
var/type = D.type
var/datum/qdel_item/I = items[type]
+ #ifdef TESTING
+ log_world("## TESTING: GC: -- \ref[D] | [type] was unable to be GC'd --")
+ for(var/c in GLOB.admins) //Using testing() here would fill the logs with ADMIN_VV garbage
+ var/client/admin = c
+ if(!check_rights_for(admin, R_ADMIN))
+ continue
+ to_chat(admin, "## TESTING: GC: -- [ADMIN_VV(D)] | [type] was unable to be GC'd --")
testing("GC: -- \ref[src] | [type] was unable to be GC'd --")
+ #endif
+ #ifdef REFERENCE_TRACKING
+ GLOB.deletion_failures += D //It should no longer be bothered by the GC, manual deletion only.
+ continue
+ #endif
I.failures++
if (GC_QUEUE_HARDDELETE)
HardDelete(D)
@@ -181,7 +195,7 @@ SUBSYSTEM_DEF(garbage)
var/gctime = world.time
var/refid = "\ref[D]"
-#ifdef TESTING
+#ifdef LEGACY_REFERENCE_TRACKING
if(reference_find_on_fail_types[D.type])
reference_find_on_fail["\ref[D]"] = TRUE
#endif
@@ -193,7 +207,7 @@ SUBSYSTEM_DEF(garbage)
queue[refid] = gctime
-#ifdef TESTING
+#ifdef LEGACY_REFERENCE_TRACKING
/datum/controller/subsystem/garbage/proc/add_type_to_findref(type)
if(!ispath(type))
return "NOT A VAILD PATH"
@@ -260,12 +274,6 @@ SUBSYSTEM_DEF(garbage)
/datum/qdel_item/New(mytype)
name = "[mytype]"
-#ifdef TESTING
-/proc/qdel_and_find_ref_if_fail(datum/D, force = FALSE)
- SSgarbage.reference_find_on_fail["\ref[D]"] = TRUE
- qdel(D, force)
-#endif
-
// Should be treated as a replacement for the 'del' keyword.
// Datums passed to this will be given a chance to clean up references to allow the GC to collect them.
/proc/qdel(datum/D, force=FALSE, ...)
@@ -319,16 +327,13 @@ SUBSYSTEM_DEF(garbage)
SSgarbage.Queue(D, GC_QUEUE_HARDDELETE)
if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste.
SSgarbage.HardDelete(D)
- if (QDEL_HINT_FINDREFERENCE)//qdel will, if TESTING is enabled, display all references to this object, then queue the object for deletion.
+ #ifdef LEGACY_REFERENCE_TRACKING
+ if (QDEL_HINT_FINDREFERENCE) //qdel will, if LEGACY_REFERENCE_TRACKING is enabled, display all references to this object, then queue the object for deletion.
SSgarbage.Queue(D)
- #ifdef TESTING
- D.find_references()
- #endif
if (QDEL_HINT_IFFAIL_FINDREFERENCE)
SSgarbage.Queue(D)
- #ifdef TESTING
SSgarbage.reference_find_on_fail["\ref[D]"] = TRUE
- #endif
+ #endif
else
#ifdef TESTING
if(!I.no_hint)
@@ -339,119 +344,6 @@ SUBSYSTEM_DEF(garbage)
else if(D.gc_destroyed == GC_CURRENTLY_BEING_QDELETED)
CRASH("[D.type] destroy proc was called multiple times, likely due to a qdel loop in the Destroy logic")
-#ifdef TESTING
-
-/datum/verb/find_refs()
- set category = "Debug"
- set name = "Find References"
- set src in world
-
- find_references(FALSE)
-
-/datum/proc/find_references(skip_alert)
- running_find_references = type
- if(usr && usr.client)
- if(usr.client.running_find_references)
- testing("CANCELLED search for references to a [usr.client.running_find_references].")
- usr.client.running_find_references = null
- running_find_references = null
- //restart the garbage collector
- SSgarbage.can_fire = 1
- SSgarbage.next_fire = world.time + world.tick_lag
- return
-
- if(!skip_alert)
- if(alert("Running this will lock everything up for about 5 minutes. Would you like to begin the search?", "Find References", "Yes", "No") == "No")
- running_find_references = null
- return
-
- //this keeps the garbage collector from failing to collect objects being searched for in here
- SSgarbage.can_fire = 0
-
- if(usr && usr.client)
- usr.client.running_find_references = type
-
- testing("Beginning search for references to a [type].")
- last_find_references = world.time
-
- DoSearchVar(GLOB) //globals
- for(var/datum/thing in world) //atoms (don't beleive it's lies)
- DoSearchVar(thing, "World -> [thing]")
-
- for (var/datum/thing) //datums
- DoSearchVar(thing, "World -> [thing]")
-
- for (var/client/thing) //clients
- DoSearchVar(thing, "World -> [thing]")
-
- testing("Completed search for references to a [type].")
- if(usr && usr.client)
- usr.client.running_find_references = null
- running_find_references = null
-
- //restart the garbage collector
- SSgarbage.can_fire = 1
- SSgarbage.next_fire = world.time + world.tick_lag
-
-/datum/verb/qdel_then_find_references()
- set category = "Debug"
- set name = "qdel() then Find References"
- set src in world
-
- qdel(src, TRUE) //Force.
- if(!running_find_references)
- find_references(TRUE)
-
-/datum/verb/qdel_then_if_fail_find_references()
- set category = "Debug"
- set name = "qdel() then Find References if GC failure"
- set src in world
-
- qdel_and_find_ref_if_fail(src, TRUE)
-
-/datum/proc/DoSearchVar(X, Xname, recursive_limit = 64)
- if(usr && usr.client && !usr.client.running_find_references)
- return
- if (!recursive_limit)
- return
-
- if(istype(X, /datum))
- var/datum/D = X
- if(D.last_find_references == last_find_references)
- return
-
- D.last_find_references = last_find_references
- var/list/L = D.vars
-
- for(var/varname in L)
- if (varname == "vars")
- continue
- var/variable = L[varname]
-
- if(variable == src)
- testing("Found [src.type] \ref[src] in [D.type]'s [varname] var. [Xname]")
-
- else if(islist(variable))
- DoSearchVar(variable, "[Xname] -> list", recursive_limit-1)
-
- else if(islist(X))
- var/normal = IS_NORMAL_LIST(X)
- for(var/I in X)
- if (I == src)
- testing("Found [src.type] \ref[src] in list [Xname].")
-
- else if (I && !isnum(I) && normal && X[I] == src)
- testing("Found [src.type] \ref[src] in list [Xname]\[[I]\]")
-
- else if (islist(I))
- DoSearchVar(I, "[Xname] -> list", recursive_limit-1)
-
-#ifndef FIND_REF_NO_CHECK_TICK
- CHECK_TICK
-#endif
-
-#endif
-
#ifdef TESTING
/proc/writeDatumCount()
var/list/datums = list()
diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm
index 348f9e6778..a91549ab4c 100644
--- a/code/datums/datumvars.dm
+++ b/code/datums/datumvars.dm
@@ -31,6 +31,9 @@
VV_DROPDOWN_OPTION(VV_HK_EXPOSE, "Show VV To Player")
VV_DROPDOWN_OPTION(VV_HK_ADDCOMPONENT, "Add Component/Element")
VV_DROPDOWN_OPTION(VV_HK_MODIFY_TRAITS, "Modify Traits")
+ #ifdef REFERENCE_TRACKING
+ VV_DROPDOWN_OPTION(VV_HK_VIEW_REFERENCES, "View References")
+ #endif
//This proc is only called if everything topic-wise is verified. The only verifications that should happen here is things like permission checks!
//href_list is a reference, modifying it in these procs WILL change the rest of the proc in topic.dm of admin/view_variables!
diff --git a/code/game/world.dm b/code/game/world.dm
index 55333fb3e6..a342200b3d 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -12,6 +12,9 @@ GLOBAL_LIST(topic_status_cache)
if (fexists(EXTOOLS))
call(EXTOOLS, "maptick_initialize")()
enable_debugger()
+#ifdef REFERENCE_TRACKING
+ enable_reference_tracking()
+#endif
world.Profile(PROFILE_START)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 22b260360e..5734b2e557 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -172,7 +172,11 @@ GLOBAL_LIST_INIT(admin_verbs_debug, world.AVerbsDebug())
/client/proc/cmd_display_overlay_log,
/client/proc/reload_configuration,
/datum/admins/proc/create_or_modify_area,
- /client/proc/generate_wikichem_list //DO NOT PRESS UNLESS YOU WANT SUPERLAG
+#ifdef REFERENCE_TRACKING
+ /datum/admins/proc/view_refs,
+ /datum/admins/proc/view_del_failures,
+#endif
+ /client/proc/generate_wikichem_list, //DO NOT PRESS UNLESS YOU WANT SUPERLAG
)
GLOBAL_PROTECT(admin_verbs_debug)
GLOBAL_LIST_INIT(admin_verbs_possess, list(/proc/possess, /proc/release))
diff --git a/code/modules/admin/view_variables/reference_tracking.dm b/code/modules/admin/view_variables/reference_tracking.dm
new file mode 100644
index 0000000000..70aac2f107
--- /dev/null
+++ b/code/modules/admin/view_variables/reference_tracking.dm
@@ -0,0 +1,225 @@
+#ifdef REFERENCE_TRACKING
+
+GLOBAL_LIST_EMPTY(deletion_failures)
+
+/world/proc/enable_reference_tracking()
+ if (fexists(EXTOOLS))
+ call(EXTOOLS, "ref_tracking_initialize")()
+
+/proc/get_back_references(datum/D)
+ CRASH("/proc/get_back_references not hooked by extools, reference tracking will not function!")
+
+/proc/get_forward_references(datum/D)
+ CRASH("/proc/get_forward_references not hooked by extools, reference tracking will not function!")
+
+/proc/clear_references(datum/D)
+ return
+
+/datum/admins/proc/view_refs(atom/D in world) //it actually supports datums as well but byond no likey
+ set category = "Debug"
+ set name = "View References"
+
+ if(!check_rights(R_DEBUG) || !D)
+ return
+
+ var/list/backrefs = get_back_references(D)
+ if(isnull(backrefs))
+ var/datum/browser/popup = new(usr, "ref_view", "
Error
")
+ popup.set_content("Reference tracking not enabled")
+ popup.open(FALSE)
+ return
+
+ var/list/frontrefs = get_forward_references(D)
+ var/list/dat = list()
+ dat += "References of \ref[D] - [D]
\[Refresh\]
"
+ dat += "Back references - these things hold references to this object.
"
+ dat += ""
+ dat += "| Ref | Type | Variable Name | Follow | "
+ for(var/ref in backrefs)
+ var/datum/backreference = ref
+ if(isnull(backreference))
+ dat += "
|---|
| GC'd Reference |
"
+ if(istype(backreference))
+ dat += "| [REF(backreference)] | [backreference.type] | [backrefs[backreference]] | \[Follow\] |
"
+ else if(islist(backreference))
+ dat += "| [REF(backreference)] | list | [backrefs[backreference]] | \[Follow\] |
"
+ else
+ dat += "| Weird reference type. Add more debugging checks. |
"
+ dat += "
"
+ dat += "Forward references - this object is referencing those things.
"
+ dat += ""
+ dat += "| Variable name | Ref | Type | Follow | "
+ for(var/ref in frontrefs)
+ var/datum/backreference = frontrefs[ref]
+ dat += "
|---|
| [ref] | [REF(backreference)] | [backreference.type] | \[Follow\] |
"
+ dat += "
"
+ dat = dat.Join()
+
+ var/datum/browser/popup = new(usr, "ref_view", "References of \ref[D]
")
+ popup.set_content(dat)
+ popup.open(FALSE)
+
+
+/datum/admins/proc/view_del_failures()
+ set category = "Debug"
+ set name = "View Deletion Failures"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/list/dat = list("")
+ for(var/t in GLOB.deletion_failures)
+ if(isnull(t))
+ dat += "| GC'd Reference | Clear Nulls |
"
+ continue
+ var/datum/thing = t
+ dat += "| \ref[thing] | [thing.type][thing.gc_destroyed ? " (destroyed)" : ""] [ADMIN_VV(thing)] |
"
+ dat += "
"
+ dat = dat.Join()
+
+ var/datum/browser/popup = new(usr, "del_failures", "Deletion Failures
")
+ popup.set_content(dat)
+ popup.open(FALSE)
+
+
+/datum/proc/find_references()
+ testing("Beginning search for references to a [type].")
+ var/list/backrefs = get_back_references(src)
+ for(var/ref in backrefs)
+ if(isnull(ref))
+ log_world("## TESTING: Datum reference found, but gone now.")
+ continue
+ if(islist(ref))
+ log_world("## TESTING: Found [type] \ref[src] in list.")
+ continue
+ var/datum/datum_ref = ref
+ if(!istype(datum_ref))
+ log_world("## TESTING: Found [type] \ref[src] in unknown type reference: [datum_ref].")
+ return
+ log_world("## TESTING: Found [type] \ref[src] in [datum_ref.type][datum_ref.gc_destroyed ? " (destroyed)" : ""]")
+ message_admins("Found [type] \ref[src] [ADMIN_VV(src)] in [datum_ref.type][datum_ref.gc_destroyed ? " (destroyed)" : ""] [ADMIN_VV(datum_ref)]")
+ testing("Completed search for references to a [type].")
+
+#endif
+
+#ifdef LEGACY_REFERENCE_TRACKING
+
+/datum/verb/legacy_find_refs()
+ set category = "Debug"
+ set name = "Find References"
+ set src in world
+
+ find_references(FALSE)
+
+
+/datum/proc/find_references_legacy(skip_alert)
+ running_find_references = type
+ if(usr?.client)
+ if(usr.client.running_find_references)
+ testing("CANCELLED search for references to a [usr.client.running_find_references].")
+ usr.client.running_find_references = null
+ running_find_references = null
+ //restart the garbage collector
+ SSgarbage.can_fire = TRUE
+ SSgarbage.next_fire = world.time + world.tick_lag
+ return
+
+ if(!skip_alert && alert("Running this will lock everything up for about 5 minutes. Would you like to begin the search?", "Find References", "Yes", "No") != "Yes")
+ running_find_references = null
+ return
+
+ //this keeps the garbage collector from failing to collect objects being searched for in here
+ SSgarbage.can_fire = FALSE
+
+ if(usr?.client)
+ usr.client.running_find_references = type
+
+ testing("Beginning search for references to a [type].")
+ last_find_references = world.time
+
+ DoSearchVar(GLOB) //globals
+ for(var/datum/thing in world) //atoms (don't beleive its lies)
+ DoSearchVar(thing, "World -> [thing]")
+
+ for(var/datum/thing) //datums
+ DoSearchVar(thing, "World -> [thing]")
+
+ for(var/client/thing) //clients
+ DoSearchVar(thing, "World -> [thing]")
+
+ testing("Completed search for references to a [type].")
+ if(usr?.client)
+ usr.client.running_find_references = null
+ running_find_references = null
+
+ //restart the garbage collector
+ SSgarbage.can_fire = TRUE
+ SSgarbage.next_fire = world.time + world.tick_lag
+
+
+/datum/verb/qdel_then_find_references()
+ set category = "Debug"
+ set name = "qdel() then Find References"
+ set src in world
+
+ qdel(src, TRUE) //force a qdel
+ if(!running_find_references)
+ find_references(TRUE)
+
+
+/datum/verb/qdel_then_if_fail_find_references()
+ set category = "Debug"
+ set name = "qdel() then Find References if GC failure"
+ set src in world
+
+ qdel_and_find_ref_if_fail(src, TRUE)
+
+
+/datum/proc/DoSearchVar(potential_container, container_name, recursive_limit = 64)
+ if(usr?.client && !usr.client.running_find_references)
+ return
+
+ if(!recursive_limit)
+ return
+
+ if(istype(potential_container, /datum))
+ var/datum/datum_container = potential_container
+ if(datum_container.last_find_references == last_find_references)
+ return
+
+ datum_container.last_find_references = last_find_references
+ var/list/vars_list = datum_container.vars
+
+ for(var/varname in vars_list)
+ if (varname == "vars")
+ continue
+ var/variable = vars_list[varname]
+
+ if(variable == src)
+ testing("Found [type] \ref[src] in [datum_container.type]'s [varname] var. [container_name]")
+
+ else if(islist(variable))
+ DoSearchVar(variable, "[container_name] -> list", recursive_limit - 1)
+
+ else if(islist(potential_container))
+ var/normal = IS_NORMAL_LIST(potential_container)
+ for(var/element_in_list in potential_container)
+ if(element_in_list == src)
+ testing("Found [type] \ref[src] in list [container_name].")
+
+ else if(element_in_list && !isnum(element_in_list) && normal && potential_container[element_in_list] == src)
+ testing("Found [type] \ref[src] in list [container_name]\[[element_in_list]\]")
+
+ else if(islist(element_in_list))
+ DoSearchVar(element_in_list, "[container_name] -> list", recursive_limit - 1)
+
+ #ifndef FIND_REF_NO_CHECK_TICK
+ CHECK_TICK
+ #endif
+
+
+/proc/qdel_and_find_ref_if_fail(datum/thing_to_del, force = FALSE)
+ SSgarbage.reference_find_on_fail[REF(thing_to_del)] = TRUE
+ qdel(thing_to_del, force)
+
+#endif
diff --git a/code/modules/admin/view_variables/topic_basic.dm b/code/modules/admin/view_variables/topic_basic.dm
index d6e4c2b944..9ee7103562 100644
--- a/code/modules/admin/view_variables/topic_basic.dm
+++ b/code/modules/admin/view_variables/topic_basic.dm
@@ -45,6 +45,16 @@
usr.client.admin_delete(target)
if (isturf(src)) // show the turf that took its place
usr.client.debug_variables(src)
+ return
+ #ifdef REFERENCE_TRACKING
+ if(href_list[VV_HK_VIEW_REFERENCES])
+ var/datum/D = locate(href_list[VV_HK_TARGET])
+ if(!D)
+ to_chat(usr, "Unable to locate item.")
+ return
+ usr.client.holder.view_refs(target)
+ return
+ #endif
if(href_list[VV_HK_MARK])
usr.client.mark_datum(target)
if(href_list[VV_HK_ADDCOMPONENT])
diff --git a/code/modules/admin/view_variables/view_variables.dm b/code/modules/admin/view_variables/view_variables.dm
index abe445589f..a4dff725f7 100644
--- a/code/modules/admin/view_variables/view_variables.dm
+++ b/code/modules/admin/view_variables/view_variables.dm
@@ -61,6 +61,7 @@
"Set len" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_SET_LENGTH),
"Shuffle" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_SHUFFLE),
"Show VV To Player" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_EXPOSE),
+ "View References" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_VIEW_REFERENCES),
"---"
)
for(var/i in 1 to length(dropdownoptions))
diff --git a/code/modules/projectiles/boxes_magazines/_box_magazine.dm b/code/modules/projectiles/boxes_magazines/_box_magazine.dm
index d0d846c144..78ca6e9280 100644
--- a/code/modules/projectiles/boxes_magazines/_box_magazine.dm
+++ b/code/modules/projectiles/boxes_magazines/_box_magazine.dm
@@ -76,7 +76,8 @@
return 1
/obj/item/ammo_box/attackby(obj/item/A, mob/user, params, silent = FALSE, replace_spent = 0)
- if(INTERACTING_WITH(user, A))
+ if(INTERACTING_WITH(user, src) || INTERACTING_WITH(user, A))
+ to_chat(user, "You're already doing that!")
return FALSE
var/num_loaded = 0
if(!can_load(user))
@@ -84,13 +85,16 @@
if(istype(A, /obj/item/ammo_box))
var/obj/item/ammo_box/AM = A
for(var/obj/item/ammo_casing/AC in AM.stored_ammo)
- if(load_delay && do_after(user, load_delay, target = src))
- var/did_load = give_round(AC, replace_spent)
- if(did_load)
- AM.stored_ammo -= AC
- num_loaded++
- if(!did_load || !multiload)
- break
+ if(load_delay || AM.load_delay)
+ var/loadtime = max(AM.load_delay, load_delay)
+ if(!do_after(user, loadtime, target = src))
+ return FALSE
+ var/did_load = give_round(AC, replace_spent)
+ if(did_load)
+ AM.stored_ammo -= AC
+ num_loaded++
+ if(!did_load || !multiload)
+ break
if(istype(A, /obj/item/ammo_casing))
var/obj/item/ammo_casing/AC = A
if(give_round(AC, replace_spent))
diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm
index fa8099a257..1aefa51a51 100644
--- a/code/modules/projectiles/guns/ballistic.dm
+++ b/code/modules/projectiles/guns/ballistic.dm
@@ -53,6 +53,8 @@
..()
if (istype(A, /obj/item/ammo_box/magazine))
var/obj/item/ammo_box/magazine/AM = A
+ if(AM.load_delay && !do_after(user, AM.load_delay, target = src))
+ return FALSE
if (!magazine && istype(AM, mag_type))
if(user.transferItemToLoc(AM, src))
magazine = AM
diff --git a/code/modules/shuttle/shuttle_creation/shuttle_creator.dm b/code/modules/shuttle/shuttle_creation/shuttle_creator.dm
index f5a11db60f..b3d99f22ab 100644
--- a/code/modules/shuttle/shuttle_creation/shuttle_creator.dm
+++ b/code/modules/shuttle/shuttle_creation/shuttle_creator.dm
@@ -43,6 +43,7 @@ GLOBAL_LIST_EMPTY(custom_shuttle_machines) //Machines that require updating (He
. = ..()
internal_shuttle_creator = new()
internal_shuttle_creator.owner_rsd = src
+ desc += " Attention, the max size of the shuttle is [SHUTTLE_CREATOR_MAX_SIZE]."
overlay_holder = new()
/obj/item/shuttle_creator/Destroy()
@@ -237,13 +238,13 @@ GLOBAL_LIST_EMPTY(custom_shuttle_machines) //Machines that require updating (He
port.register()
- icon_state = "rsd_used"
+ icon_state = "rsd_empty"
//Clear highlights
overlay_holder.clear_highlights()
GLOB.custom_shuttle_count ++
- message_admins("[ADMIN_LOOKUPFLW(user)] created a new shuttle with a [src] at [ADMIN_VERBOSEJMP(user)] ([GLOB.custom_shuttle_count] custom shuttles, limit is [CUSTOM_SHUTTLE_LIMIT])")
- log_game("[key_name(user)] created a new shuttle with a [src] at [AREACOORD(user)] ([GLOB.custom_shuttle_count] custom shuttles, limit is [CUSTOM_SHUTTLE_LIMIT])")
+ message_admins("[ADMIN_LOOKUPFLW(user)] created a new shuttle with a [src] at [ADMIN_VERBOSEJMP(user)] ([GLOB.custom_shuttle_count] custom shuttles)")
+ log_game("[key_name(user)] created a new shuttle with a [src] at [AREACOORD(user)] ([GLOB.custom_shuttle_count] custom shuttles)")
return TRUE
/obj/item/shuttle_creator/proc/create_shuttle_area(mob/user)
@@ -350,7 +351,7 @@ GLOBAL_LIST_EMPTY(custom_shuttle_machines) //Machines that require updating (He
loggedOldArea = get_area(get_turf(user))
loggedTurfs |= turfs
overlay_holder.highlight_area(turfs)
- //TODO READD THIS SHIT: icon_state = "rsd_used"
+ //TODO READD THIS SHIT: icon_state = "rsd_empty"
to_chat(user, "You add the area into the buffer of the [src], you made add more areas or select an airlock to act as a docking port to complete the shuttle.")
return turfs
diff --git a/html/changelogs/AutoChangeLog-pr-13183.yml b/html/changelogs/AutoChangeLog-pr-13183.yml
new file mode 100644
index 0000000000..f93214e200
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13183.yml
@@ -0,0 +1,6 @@
+author: "SmArtKar"
+delete-after: True
+changes:
+ - tweak: "RSD limitation is now 500 tiles"
+ - bugfix: "Fixed broken RSD sprites"
+ - config: "Removed that shuttle limit"
diff --git a/html/changelogs/AutoChangeLog-pr-13211.yml b/html/changelogs/AutoChangeLog-pr-13211.yml
new file mode 100644
index 0000000000..0c11c20a0d
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13211.yml
@@ -0,0 +1,4 @@
+author: "LetterN"
+delete-after: True
+changes:
+ - rscadd: "Updates and adds some of the tips"
diff --git a/html/changelogs/AutoChangeLog-pr-13212.yml b/html/changelogs/AutoChangeLog-pr-13212.yml
new file mode 100644
index 0000000000..0bc2d57909
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13212.yml
@@ -0,0 +1,4 @@
+author: "Putnam3145"
+delete-after: True
+changes:
+ - admin: "added reftracking as a compile flag"
diff --git a/strings/tips.txt b/strings/tips.txt
index a1d2befab3..5dc4e1b985 100644
--- a/strings/tips.txt
+++ b/strings/tips.txt
@@ -25,7 +25,7 @@ As the Captain, you have absolute access and control over the station, but this
As the Captain, you have a fancy pen that can be used as a holdout dagger or even as a scalpel in surgery!
As the Captain, you can purchase a new emergency shuttle using a communications console. Some require credits, while others give you credits in exchange. Keep in mind that purchasing dangerous shuttles will incur the ire of your crew.
As the Chief Medical Officer, your hypospray is like the ones that your Medical Doctors can buy, except it comes in a fancy box that can hold several more hypovials than the standard, and already comes preloaded with specially-made high-capacity hypovials that hold double the reagents the standard ones do.
-As the Chief Medical Officer, coordinate and communicate with your doctors, chemists, and geneticists during a nuclear emergency, blob infestation, or some other crisis to keep people alive and fighting.
+As the Chief Medical Officer, coordinate and communicate with your doctors, chemists, and paramedics during a nuclear emergency, blob infestation, or some other crisis to keep people alive and fighting.
As a Medical Doctor, pester Research for improved surgical tools. They work faster, combine the purposes of several tools in one (scalpel/saw, retractor/hemostat, drill/cautery), and don't cost many materials to boot!
As a Medical Doctor, the surgical saw and drill are both powerful weapons, the saw is sharp and can slice and dice, while the drill can quickly blind someone if aimed for the eyes. The laser scalpel is an upgraded version producible with Research's aid, and it has the highest force of most common place weapons, while still remaining sharp.
As a Medical Doctor, your belt can hold a full set of surgical tools. Using sterilizine before each attempt during surgery will reduce your failure chance on tricky steps or when using less-than-optimal equipment.
@@ -34,11 +34,14 @@ As a Medical Doctor, while both heal toxin damage, the difference between charco
As a Medical Doctor, you can surgically implant or extract things from people's chests by performing a cavity implant. This could range from inserting a suicide bomb to embedding the nuke disk into the Captain's chest.
As a Medical Doctor, it's of utmost urgency that you tend to anyone who's been hugged by a facehugger. You only have a couple of minutes from the initial attachment to perform organ manipulation to their chest and remove the rapidly developing alien embryo before it bursts out and immediately kills your patient.
As a Medical Doctor, you must target the correct limb and be on help intent when trying to perform surgery on someone. Using disarm attempt will intentionally fail the surgery step.
-As a Medical Doctor, corpses with the "...and their soul has departed" description no longer have a ghost attached to them and aren't usually revivable or cloneable. However it may prove useful to be creative in your revivification techniques with these bodies.
+As a Medical Doctor, corpses with the "...and their soul has departed" description no longer have a ghost attached to them and can't be revived. However it may prove useful to be creative in your revivification techniques with these bodies.
As a Medical Doctor, treating plasmamen is not impossible! Salbutamol and epinephrine stops them from suffocating due to lack of internals and showers stop them from burning alive. You can even perform surgery on them by doing the procedure on a roller bed under a shower.
As a Medical Doctor, you can point your penlight at people to create a medical hologram. This lets them know that you're coming to treat them.
As a Medical Doctor, you can extract implants by holding an empty implant case in your offhand while performing the extraction step.
As a Medical Doctor, clone scanning people will implant them with a health tracker that displays their vitals in the clone records. Useful to check on crew members that didn't activate suit sensors!
+As a Medical Doctor, you can deal with patients who have absurd amounts of wounds by putting them in cryo. This will slowly treat all of their wounds simultaneously, but is much slower than direct treatment.
+As a Medical Doctor, Critical Slash wounds are one of the most dangerous conditions someone can have. Apply gauze, epipens, sutures, cauteries, whatever you can, as soon as possible!
+As a Medical Doctor, Saline-Glucose not only acts as a temporary boost to a patient's blood level, it also speeds regeneration! Perfect for drained patients!
As a Medical Doctor, medical gauze is an incredibly underrated tool. It can be used to entirely halt a limb from bleeding or sling one that's been shattered until it can be given proper attention. This even works on the dead, too! Be sure to stop someone's bleeding whether they're in critical condition or a corpse, as dragging someone whom is bleeding will rapidly deplete them of all their blood.
As a Chemist, there are dozens of chemicals that can heal, and even more that can cause harm. See which chemicals have the best synergy, both in healing, and in harming. Experiment!
As a Chemist, some chemicals can only be synthesized by heating up the contents in the chemical heater.
@@ -135,10 +138,10 @@ As a Security Officer, examining someone while wearing your security HUDglasses
As a Security Officer, you can take out the power cell on your baton to replace it with a better or fully charged one. Just use a screwdriver on your baton to remove the old cell.
As a Security Officer, you can just about any firearm on your vest, this even works with other non-standard armor-substitutes like security winter coats!
As the Detective, people leave fingerprints everywhere and on everything. With the exception of white latex, gloves will hide them. All is not lost, however, as gloves leave fibers specific to their kind such as black or nitrile, pointing to a general department.
-As the Detective, you can use your forensics scanner from a distance.
+As the Detective, you can use your forensics scanner from a distance. Use this to scan boxes or other storage containers.
As the Detective, your revolver can be loaded with .357 ammunition. Use a screwdriver to permanently modify your revolver into using this type of ammunition, be warned however, firing it has a decent chance to cause the revolver to misfire and shoot you in the foot.
As the Lawyer, try to negotiate with the Warden if sentences seem too high for the crime.
-As the Lawyer, you can try to convince the captain and Head of Security to hold trials for prisoners in the courtroom.
+As the Lawyer, you can try to convince the Captain and Head of Security to hold trials for prisoners in the courtroom.
As the Head of Personnel, you are not higher ranking than other heads of staff, even though you are expected to take the Captain's place first should he go missing. If the situation seems too rough for you, consider allowing another head to become temporary Captain.
As the Head of Personnel, you are just as large a target as the Captain because of the potential power your ID and computer can hand out and your comparative vulnerability.
As the Mime, your invisible wall power blocks people as well as projectiles. You can use it in a pinch to delay your pursuer.
@@ -187,7 +190,7 @@ As a Shaft Miner, every monster on Lavaland has a pattern you can exploit to min
As a Shaft Miner, you can harvest goliath plates from goliaths and upgrade your explorer's suit, mining hardsuits as well as Firefighter APLUs with them, greatly reducing incoming melee damage.
As a Shaft Miner, always have a GPS on you, so a fellow miner or cyborg can come to save you if you die.
As a Shaft Miner, you can craft a variety of equipment from the local fauna. Bone axes, lava boats and ash drake armour are just a few of them!
-As a Traitor, the cryptographic sequencer (emag) can not only open doors, but also lockers, crates, APCs and more. It can hack cyborgs, and even cause bots to go berserk. Use it on the right machines, and you may just be able to create some difficult to obtain substances, or contact your employers to request special objectives! Experiment!
+As a Traitor, the cryptographic sequencer (emag) can not only open lockers, crates, APCs and more. It can also do things like hack cyborgs, and even cause bots to go berserk. Use it on the right machines, and you can even contact the Syndicate. Experiment!
As a Traitor, subverting the AI to serve you can make it an extremely powerful ally. However, be careful of the wording in the laws you give it, as it may use your poorly written laws against you!
As a Traitor, the Captain and the Head of Security are two of the most difficult to kill targets on the station. If either one is your target, plan carefully.
As a Traitor, you can manufacture and recycle revolver bullets at a hacked autolathe, making the revolver an extremely powerful tool if you manage to nab an autolathe for yourself.
@@ -235,6 +238,7 @@ As a Changeling, absorbing someone will give you their full memory. This can inc
As a Changeling, absorbing another Changeling will permanently boost your chemical reserve, allow you to pick more abilities, and make the victim unable to revive. Be careful when exposing your identity to other Changelings, as they may be out of those wonderful benefits.
As a Changeling, BZ gas will dramatically slow down or even halt your natural chemical regeneration, be sure to avoid it at all costs as some lunatics may try and flood portions of the station to deal with you.
As a Changeling, death is not the end for you! You can revive after two minutes from being dead by triggering your stasis ability, and then waiting for the prompt to resurrect yourself to show up.
+As a Changeling, your Regenerate Limbs power will quickly heal all of your wounds, but they'll still leave scars. Changelings can use Fleshmend to get rid of scars, or you can ingest Carpotoxin to get rid of them like a normal person.
As a Cultist, do not cause too much chaos before your objective is completed. If the shuttle gets called too soon, you may not have enough time to win.
As a Cultist, your team starts off very weak, but if necessary can quickly convert everything they have into raw power. Make sure you have the numbers and equipment to support going loud, or the cult will fall flat on its face.
As a Cultist, the Blood Boil rune will deal massive amounts of brute damage to non-cultists, stamina damage to Ratvarian scum, and some damage to fellow cultists of Nar-Sie nearby, but will create a fire where the rune stands on use.
@@ -281,12 +285,17 @@ As a Devil, you gain power for every three souls you control, however you also b
As a Devil, as long as you control at least one other soul, you will automatically resurrect, as long as a banishment ritual is not performed.
At which time a Devil's nameth is spake on the tongue of man, the Devil may appeareth.
You can swap floor tiles by holding a crowbar in one hand and a stack of tiles in the other.
-When hacking doors, cutting and mending the "test light wire" will restore power to the door.
-When hacking, remote singulars pulse when attached to a wire and pinged. This can allow you to hack things or set traps from far away.
+When hacking doors, cutting and mending a "test light wire" will restore power to the door.
When crafting most items, you can either manually combine parts or use the crafting menu.
Suit storage units not only remove blood and dirt from clothing, but also radiation!
-Suit storage units entirely purge radiation from any carbon mob put inside of them when cycling, at the cost of some horrific burns, this is a very effective strategy to clean someone up after they bathed in the engine.
Remote devices will work when used through cameras. For example: Bluespace RPEDs and door remotes.
+You can light a cigar on a supermatter crystal.
+Using sticky tape on items can make them stick to people and walls! Be careful, grenades might stick to your hand during the moment of truth!
+In a pinch, stripping yourself naked will give you a sizeable resistance to being tackled. What do you value more, your freedom or your dignity?
+Wearing riot armor makes you significantly more effective at performing tackle takedowns, but will use extra stamina with each leap! It will also significantly protect you from other tackles!
+Epipens contain a powerful coagulant that drastically reduces bleeding on all bleeding wounds. If you don't have time to properly treat someone with lots of slashes or piercings, stick them with a pen to buy some time!
+Anything you can light a cigarette with, you can use to cauterize a bleeding wound. Technically, that includes the supermatter.
+Suit storage units entirely purge radiation from any carbon mob put inside of them when cycling, at the cost of some horrific burns, this is a very effective strategy to clean someone up after they bathed in the engine.
Laser pointers can be upgraded by replacing its micro laser with a better one from RnD! Use a screwdriver on it to remove the old laser. Upgrading the laser pointer gives you better odds of stunning a cyborg, and even blinding people with sunglasses.
Being out of combat mode makes makes you deal less damage to people and objects when attacking. This stacks with the penalty incurred by resting.
Resting makes you deal less damage to people and objects when attacking. This stacks with the penalty incurred by being out of combat mode.
diff --git a/tgstation.dme b/tgstation.dme
index 0497784ce4..36f19d96bb 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1393,6 +1393,7 @@
#include "code\modules\admin\view_variables\mark_datum.dm"
#include "code\modules\admin\view_variables\mass_edit_variables.dm"
#include "code\modules\admin\view_variables\modify_variables.dm"
+#include "code\modules\admin\view_variables\reference_tracking.dm"
#include "code\modules\admin\view_variables\topic.dm"
#include "code\modules\admin\view_variables\topic_basic.dm"
#include "code\modules\admin\view_variables\topic_list.dm"