Merge pull request #15097 from LetterN/generic-housekeeping-pr

Generic syncs
This commit is contained in:
silicons
2021-09-10 00:16:25 -07:00
committed by GitHub
66 changed files with 7132 additions and 4465 deletions
+28 -14
View File
@@ -1,30 +1,44 @@
//defines that give qdel hints. these can be given as a return in destory() or by calling
//! Defines that give qdel hints.
//!
//! These can be given as a return in [/atom/proc/Destroy] or by calling [/proc/qdel].
/// `qdel` should queue the object for deletion.
#define QDEL_HINT_QUEUE 0
/// `qdel` should let the object live after calling [/atom/proc/Destroy].
#define QDEL_HINT_LETMELIVE 1
/// Functionally the same as the above. `qdel` should assume the object will gc on its own, and not check it.
#define QDEL_HINT_IWILLGC 2
/// Qdel should assume this object won't GC, and queue a hard delete using a hard reference.
#define QDEL_HINT_HARDDEL 3
// Qdel should assume this object won't gc, and hard delete it posthaste.
#define QDEL_HINT_HARDDEL_NOW 4
#define QDEL_HINT_QUEUE 0 //qdel should queue the object for deletion.
#define QDEL_HINT_LETMELIVE 1 //qdel should let the object live after calling destory.
#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.
#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.
#ifdef REFERENCE_TRACKING
/** If 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_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
#define GC_QUEUE_COUNT 2 //increase this when adding more steps.
#define QDEL_ITEM_ADMINS_WARNED (1<<0) //! Set when admins are told about lag causing qdels in this type.
#define QDEL_ITEM_SUSPENDED_FOR_LAG (1<<1) //! Set when a type can no longer be hard deleted on failure because of lag it causes while this happens.
// Defines for the [gc_destroyed][/datum/var/gc_destroyed] var.
#define GC_QUEUED_FOR_QUEUING -1
#define GC_CURRENTLY_BEING_QDELETED -2
// Defines for the time left for an item to get its reference cleaned
#define GC_FILTER_QUEUE 5 MINUTES
#define GC_DEL_QUEUE 10 SECONDS
#define QDELING(X) (X.gc_destroyed)
#define QDELETED(X) (!X || QDELING(X))
#define QDESTROYING(X) (!X || X.gc_destroyed == GC_CURRENTLY_BEING_QDELETED)
+13 -2
View File
@@ -38,6 +38,9 @@
#define RUST_G (__rust_g || __detect_rust_g())
#endif
/// Gets the version of rust_g
/proc/rustg_get_version() return call(RUST_G, "get_version")()
/**
* This proc generates a cellular automata noise grid which can be used in procedural generation methods.
*
@@ -77,8 +80,8 @@
#define RUSTG_HTTP_METHOD_PATCH "patch"
#define RUSTG_HTTP_METHOD_HEAD "head"
#define RUSTG_HTTP_METHOD_POST "post"
#define rustg_http_request_blocking(method, url, body, headers) call(RUST_G, "http_request_blocking")(method, url, body, headers)
#define rustg_http_request_async(method, url, body, headers) call(RUST_G, "http_request_async")(method, url, body, headers)
#define rustg_http_request_blocking(method, url, body, headers, options) call(RUST_G, "http_request_blocking")(method, url, body, headers, options)
#define rustg_http_request_async(method, url, body, headers, options) call(RUST_G, "http_request_async")(method, url, body, headers, options)
#define rustg_http_check_request(req_id) call(RUST_G, "http_check_request")(req_id)
#define RUSTG_JOB_NO_RESULTS_YET "NO RESULTS YET"
@@ -99,3 +102,11 @@
#define rustg_sql_disconnect_pool(handle) call(RUST_G, "sql_disconnect_pool")(handle)
#define rustg_sql_check_query(job_id) call(RUST_G, "sql_check_query")("[job_id]")
#define rustg_url_encode(text) call(RUST_G, "url_encode")(text)
#define rustg_url_decode(text) call(RUST_G, "url_decode")(text)
#ifdef RUSTG_OVERRIDE_BUILTINS
#define url_encode(text) rustg_url_encode(text)
#define url_decode(text) rustg_url_decode(text)
#endif
+3
View File
@@ -0,0 +1,3 @@
// RUSTG_OVERRIDE_BUILTINS is not used since the file APIs don't work well over Linux.
#define url_encode(text) rustg_url_encode("[text]")
#define url_decode(text) rustg_url_decode("[text]")
+6
View File
@@ -49,10 +49,16 @@ When using time2text(), please use "DDD" to find the weekday. Refrain from using
#define TICKS *world.tick_lag
#define MILLISECONDS * 0.01
#define DS2TICKS(DS) ((DS)/world.tick_lag)
#define TICKS2DS(T) ((T) TICKS)
#define MS2DS(T) ((T) MILLISECONDS)
#define DS2MS(T) ((T) * 100)
#define GAMETIMESTAMP(format, wtime) time2text(wtime, format)
#define WORLDTIME2TEXT(format) GAMETIMESTAMP(format, world.time)
#define WORLDTIMEOFDAY2TEXT(format) GAMETIMESTAMP(format, world.timeofday)
+4
View File
@@ -343,3 +343,7 @@
#define STICKY_NODROP "sticky-nodrop" //sticky nodrop sounds like a bad soundcloud rapper's name
#define TRAIT_SACRIFICED "sacrificed" //Makes sure that people cant be cult sacrificed twice.
#define TRAIT_SPACEWALK "spacewalk"
/// obtained from mapping helper
#define MAPPING_HELPER_TRAIT "mapping-helper"
+10
View File
@@ -1,9 +1,12 @@
//wrapper macros for easier grepping
#define DIRECT_OUTPUT(A, B) A << B
#define DIRECT_INPUT(A, B) A >> B
#define SEND_IMAGE(target, image) DIRECT_OUTPUT(target, image)
#define SEND_SOUND(target, sound) DIRECT_OUTPUT(target, sound)
#define SEND_TEXT(target, text) DIRECT_OUTPUT(target, text)
#define WRITE_FILE(file, text) DIRECT_OUTPUT(file, text)
#define READ_FILE(file, text) DIRECT_INPUT(file, text)
#ifdef EXTOOLS_LOGGING
// proc hooked, so we can just put in standard TRUE and FALSE
#define WRITE_LOG(log, text) extools_log_write(log,text,TRUE)
@@ -13,6 +16,7 @@
#define WRITE_LOG(log, text) rustg_log_write(log, text, "true")
#define WRITE_LOG_NO_FORMAT(log, text) rustg_log_write(log, text, "false")
#endif
//print a warning message to world.log
#define WARNING(MSG) warning("[MSG] in [__FILE__] at line [__LINE__] src: [UNLINT(src)] usr: [usr].")
/proc/warning(msg)
@@ -38,6 +42,12 @@
SEND_TEXT(world.log, text)
#endif
#ifdef REFERENCE_TRACKING_LOG
#define log_reftracker(msg) log_world("## REF SEARCH [msg]")
#else
#define log_reftracker(msg)
#endif
/* Items with ADMINPRIVATE prefixed are stripped from public logs. */
/proc/log_admin(text)
+13 -13
View File
@@ -218,20 +218,14 @@
to_chat(world, "<BR><BR><BR><span class='big bold'>The round has ended.</span>")
log_game("The round has ended.")
if(LAZYLEN(GLOB.round_end_notifiees))
world.TgsTargetedChatBroadcast("[GLOB.round_end_notifiees.Join(", ")] the round has ended.", FALSE)
CONFIG_SET(flag/suicide_allowed,TRUE) // EORG suicides allowed
for(var/I in round_end_events)
var/datum/callback/cb = I
cb.InvokeAsync()
LAZYCLEARLIST(round_end_events)
for(var/client/C in GLOB.clients)
if(!C.credits)
C.RollCredits()
C.playtitlemusic(40)
CONFIG_SET(flag/suicide_allowed,TRUE) // EORG suicides allowed
var/speed_round = FALSE
if(world.time - SSticker.round_start_time <= 300 SECONDS)
speed_round = TRUE
@@ -264,12 +258,15 @@
send2adminchat("Server", "A round of [mode.name] just ended[mode_result == "undefined" ? "." : " with a [mode_result]."] Survival rate: [survival_rate]")
if(LAZYLEN(GLOB.round_end_notifiees))
world.TgsTargetedChatBroadcast("[GLOB.round_end_notifiees.Join(", ")] the round has ended.", FALSE)
if(length(CONFIG_GET(keyed_list/cross_server)))
send_news_report()
//tell the nice people on discord what went on before the salt cannon happens.
world.TgsTargetedChatBroadcast("The current round has ended. Please standby for your shift interlude Nanotrasen News Network's report!", FALSE)
world.TgsTargetedChatBroadcast(send_news_report(),FALSE)
world.TgsTargetedChatBroadcast(send_news_report(), FALSE)
CHECK_TICK
@@ -543,21 +540,24 @@
///Generate a report for how much money is on station, as well as the richest crewmember on the station.
/datum/controller/subsystem/ticker/proc/market_report()
var/list/parts = list()
parts += "<span class='header'>Station Economic Summary:</span>"
///This is the richest account on station at roundend.
var/datum/bank_account/mr_moneybags
///This is the station's total wealth at the end of the round.
var/station_vault = 0
///How many players joined the round.
var/total_players = GLOB.joined_player_list.len
var/list/typecache_bank = typecacheof(list(/datum/bank_account/department, /datum/bank_account/remote))
for(var/datum/bank_account/current_acc in SSeconomy.generated_accounts)
var/static/list/typecache_bank = typecacheof(list(/datum/bank_account/department, /datum/bank_account/remote))
for(var/i in SSeconomy.generated_accounts)
var/datum/bank_account/current_acc = SSeconomy.generated_accounts[i]
if(typecache_bank[current_acc.type])
continue
station_vault += current_acc.account_balance
if(!mr_moneybags || mr_moneybags.account_balance < current_acc.account_balance)
mr_moneybags = current_acc
parts += "<div class='panel stationborder'>There were [station_vault] credits collected by crew this shift.<br>"
parts += "<div class='panel stationborder'><span class='header'>Station Economic Summary:</span><br>"
parts += "<b>General Statistics:</b><br>"
parts += "There were [station_vault] credits collected by crew this shift.<br>"
if(total_players > 0)
parts += "An average of [station_vault/total_players] credits were collected.<br>"
// log_econ("Roundend credit total: [station_vault] credits. Average Credits: [station_vault/total_players]")
+47 -23
View File
@@ -1,7 +1,7 @@
//#define TESTING //By using the testing("message") proc you can create debug-feedback for people with this
//#define TESTING //By using the testing("message") proc you can create debug-feedback for people with this
//uncommented, but not visible in the release version)
//#define DATUMVAR_DEBUGGING_MODE //Enables the ability to cache datum vars and retrieve later for debugging which vars changed.
//#define DATUMVAR_DEBUGGING_MODE //Enables the ability to cache datum vars and retrieve later for debugging which vars changed.
// Comment this out if you are debugging problems that might be obscured by custom error handling in world/Error
#ifdef DEBUG
@@ -11,34 +11,44 @@
#ifdef TESTING
#define DATUMVAR_DEBUGGING_MODE
/*
* 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.
*/
///Used to find the sources of harddels, quite laggy, don't be surpised if it freezes your client for a good while
//#define REFERENCE_TRACKING
#ifdef REFERENCE_TRACKING
///Method of tracking references without using extools. Slower, kept to avoid over-reliance on extools.
//#define LEGACY_REFERENCE_TRACKING
#ifdef LEGACY_REFERENCE_TRACKING
///alternate to reftracking, extool variant
//#define EXTOOLS_REFERENCE_TRACKING
///Use the legacy reference on things hard deleting by default.
///Should we be logging our findings or not
#define REFERENCE_TRACKING_LOG
///Used for doing dry runs of the reference finder, to test for feature completeness
//#define REFERENCE_TRACKING_DEBUG
///Run a lookup 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
#endif //ifdef REFERENCE_TRACKING
//#define VISUALIZE_ACTIVE_TURFS //Highlights atmos active turfs in green
/*
* Enables debug messages for every single reaction step. This is 1 message per 0.5s for a SINGLE reaction. Useful for tracking down bugs/asking me for help in the main reaction handiler (equilibrium.dm).
*
* * Requires TESTING to be defined to work.
*/
//#define REAGENTS_TESTING
// #define VISUALIZE_ACTIVE_TURFS //Highlights atmos active turfs in green
// #define TRACK_MAX_SHARE //Allows max share tracking, for use in the atmos debugging ui
#endif //ifdef TESTING
//#define UNIT_TESTS //Enables unit tests via TEST_RUN_PARAMETER
#ifndef PRELOAD_RSC //set to:
#define PRELOAD_RSC 2 // 0 to allow using external resources or on-demand behaviour;
#endif // 1 to use the default behaviour;
// 2 for preloading absolutely everything;
//#define UNIT_TESTS //If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between
#ifndef PRELOAD_RSC //set to:
#define PRELOAD_RSC 2 // 0 to allow using external resources or on-demand behaviour;
#endif // 1 to use the default behaviour;
// 2 for preloading absolutely everything;
#ifdef LOWMEMORYMODE
#define FORCE_MAP "_maps/runtimestation.json"
@@ -47,7 +57,7 @@
//Update this whenever you need to take advantage of more recent byond features
#define MIN_COMPILER_VERSION 513
#define MIN_COMPILER_BUILD 1514
#if DM_VERSION < MIN_COMPILER_VERSION || DM_BUILD < MIN_COMPILER_BUILD
#if (DM_VERSION < MIN_COMPILER_VERSION || DM_BUILD < MIN_COMPILER_BUILD) && !defined(SPACEMAN_DMM)
//Don't forget to update this part
#error Your version of BYOND is too out-of-date to compile this project. Go to https://secure.byond.com/download and update.
#error You need version 513.1514 or higher
@@ -58,10 +68,6 @@
#warn compiling in TESTING mode. testing() debug messages will be visible.
#endif
#ifdef GC_FAILURE_HARD_LOOKUP
#define FIND_REF_NO_CHECK_TICK
#endif
#ifdef CIBUILDING
#define UNIT_TESTS
#endif
@@ -70,6 +76,24 @@
#define TESTING
#endif
#if defined(UNIT_TESTS)
//Hard del testing defines
#define REFERENCE_TRACKING
#define REFERENCE_TRACKING_DEBUG
#define FIND_REF_NO_CHECK_TICK
#endif
#ifdef TGS
// TGS performs its own build of dm.exe, but includes a prepended TGS define.
#define CBT
#endif
// A reasonable number of maximum overlays an object needs
// If you think you need more, rethink it
#define MAX_ATOM_OVERLAYS 100
#if !defined(CBT) && !defined(SPACEMAN_DMM)
#warn Building with Dream Maker is no longer supported and will result in errors.
#warn In order to build, run BUILD.bat in the root directory.
#warn Consider switching to VSCode editor instead, where you can press Ctrl+Shift+B to build.
#endif
+39 -1
View File
@@ -1,14 +1,25 @@
// Clickable stat() button.
/obj/effect/statclick
name = "Initializing..."
// blocks_emissive = NONE
var/target
INITIALIZE_IMMEDIATE(/obj/effect/statclick)
/obj/effect/statclick/Initialize(mapload, text, target) //Don't port this to Initialize it's too critical
/obj/effect/statclick/Initialize(mapload, text, target)
. = ..()
name = text
src.target = target
if(istype(target, /datum)) //Harddel man bad
RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/cleanup)
/obj/effect/statclick/Destroy()
target = null
return ..()
/obj/effect/statclick/proc/cleanup()
SIGNAL_HANDLER
qdel(src)
/obj/effect/statclick/proc/update(text)
name = text
@@ -51,3 +62,30 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick)
SSblackbox.record_feedback("tally", "admin_verb", 1, "Restart Failsafe Controller")
message_admins("Admin [key_name_admin(usr)] has restarted the [controller] controller.")
/client/proc/debug_controller()
set category = "Debug"
set name = "Debug Controller"
set desc = "Debug the various periodic loop controllers for the game (be careful!)"
if(!holder)
return
var/list/controllers = list()
var/list/controller_choices = list()
for (var/datum/controller/controller in world)
if (istype(controller, /datum/controller/subsystem))
continue
controllers["[controller] (controller.type)"] = controller //we use an associated list to ensure clients can't hold references to controllers
controller_choices += "[controller] (controller.type)"
var/datum/controller/controller_string = input("Select controller to debug", "Debug Controller") as null|anything in controller_choices
var/datum/controller/controller = controllers[controller_string]
if (!istype(controller))
return
debug_variables(controller)
SSblackbox.record_feedback("tally", "admin_verb", 1, "Restart Failsafe Controller")
message_admins("Admin [key_name_admin(usr)] is debugging the [controller] controller.")
@@ -39,7 +39,9 @@
/datum/config_entry/flag/announce_admin_login
/datum/config_entry/string/centcom_ban_db // URL for the CentCom Galactic Ban DB API
/datum/config_entry/string/centcom_ban_db // URL for the CentCom Galactic Ban DB API
/datum/config_entry/string/centcom_source_whitelist
/datum/config_entry/flag/autoadmin // if autoadmin is enabled
protection = CONFIG_ENTRY_LOCKED
@@ -333,3 +333,12 @@
config_entry_value = 0.333
min_val = 0
integer = FALSE
/datum/config_entry/number/hard_deletes_overrun_threshold
integer = FALSE
min_val = 0
default = 0.5
/datum/config_entry/number/hard_deletes_overrun_limit
default = 0
min_val = 0
+95 -14
View File
@@ -15,7 +15,7 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
// The alert level. For every failed poke, we drop a DEFCON level. Once we hit DEFCON 1, restart the MC.
var/defcon = 5
//the world.time of the last check, so the mc can restart US if we hang.
// (Real friends look out for *eachother*)
// (Real friends look out for *eachother*)
var/lasttick = 0
// Track the MC iteration to make sure its still on track.
@@ -33,6 +33,22 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
/datum/controller/failsafe/Initialize()
set waitfor = FALSE
Failsafe.Loop()
if (!Master || defcon == 0) //Master is gone/not responding and Failsafe just exited its loop
defcon = 3 //Reset defcon level as its used inside the emergency loop
while (defcon > 0)
var/recovery_result = emergency_loop()
if (recovery_result == 1) //Exit emergency loop and delete self if it was able to recover MC
break
else if (defcon == 1) //Exit Failsafe if we weren't able to recover the MC in the last stage
log_game("FailSafe: Failed to recover MC while in emergency state. Failsafe exiting.")
message_admins(span_boldannounce("Failsafe failed criticaly while trying to recreate broken MC. Please manually fix the MC or reboot the server. Failsafe exiting now."))
message_admins(span_boldannounce("You can try manually calling these two procs:."))
message_admins(span_boldannounce("/proc/recover_all_SS_and_recreate_master: Most stuff should still function but expect instability/runtimes/broken stuff."))
message_admins(span_boldannounce("/proc/delete_all_SS_and_recreate_master: Most stuff will be broken but basic stuff like movement and chat should still work."))
else if (recovery_result == -1) //Failed to recreate MC
defcon--
sleep(initial(processing_interval)) //Wait a bit until the next try
if(!QDELETED(src))
qdel(src) //when Loop() returns, we delete ourselves and let the mc recreate us
@@ -45,42 +61,56 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
while(running)
lasttick = world.time
if(!Master)
// Replace the missing Master! This should never, ever happen.
new /datum/controller/master()
// Break out of the main loop so we go into emergency state
break
// Only poke it if overrides are not in effect.
if(processing_interval > 0)
if(Master.processing && Master.iteration)
if (defcon > 1 && (!Master.stack_end_detector || !Master.stack_end_detector.check()))
to_chat(GLOB.admins, span_boldannounce("ERROR: The Master Controller code stack has exited unexpectedly, Restarting..."))
defcon = 0
var/rtn = Recreate_MC()
if(rtn > 0)
master_iteration = 0
to_chat(GLOB.admins, span_adminnotice("MC restarted successfully"))
else if(rtn < 0)
log_game("FailSafe: Could not restart MC, runtime encountered. Entering defcon 0")
to_chat(GLOB.admins, span_boldannounce("ERROR: DEFCON [defcon_pretty()]. Could not restart MC, runtime encountered. I will silently keep retrying."))
// Check if processing is done yet.
if(Master.iteration == master_iteration)
switch(defcon)
if(4,5)
--defcon
if(3)
message_admins("<span class='adminnotice'>Notice: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks.</span>")
--defcon
if(2)
to_chat(GLOB.admins, "<span class='boldannounce'>Warning: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks. Automatic restart in [processing_interval] ticks.</span>")
--defcon
if(1)
to_chat(GLOB.admins, "<span class='boldannounce'>Warning: DEFCON [defcon_pretty()]. The Master Controller has still not fired within the last [(5-defcon) * processing_interval] ticks. Killing and restarting...</span>")
if(3)
message_admins(span_adminnotice("Notice: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks."))
--defcon
if(2)
to_chat(GLOB.admins, span_boldannounce("Warning: DEFCON [defcon_pretty()]. The Master Controller has not fired in the last [(5-defcon) * processing_interval] ticks. Automatic restart in [processing_interval] ticks."))
--defcon
if(1)
to_chat(GLOB.admins, span_boldannounce("Warning: DEFCON [defcon_pretty()]. The Master Controller has still not fired within the last [(5-defcon) * processing_interval] ticks. Killing and restarting..."))
--defcon
var/rtn = Recreate_MC()
if(rtn > 0)
defcon = 4
master_iteration = 0
to_chat(GLOB.admins, "<span class='adminnotice'>MC restarted successfully</span>")
to_chat(GLOB.admins, span_adminnotice("MC restarted successfully"))
else if(rtn < 0)
log_game("FailSafe: Could not restart MC, runtime encountered. Entering defcon 0")
to_chat(GLOB.admins, "<span class='boldannounce'>ERROR: DEFCON [defcon_pretty()]. Could not restart MC, runtime encountered. I will silently keep retrying.</span>")
to_chat(GLOB.admins, span_boldannounce("ERROR: DEFCON [defcon_pretty()]. Could not restart MC, runtime encountered. I will silently keep retrying."))
//if the return number was 0, it just means the mc was restarted too recently, and it just needs some time before we try again
//no need to handle that specially when defcon 0 can handle it
if(0) //DEFCON 0! (mc failed to restart)
var/rtn = Recreate_MC()
if(rtn > 0)
defcon = 4
master_iteration = 0
to_chat(GLOB.admins, "<span class='adminnotice'>MC restarted successfully</span>")
to_chat(GLOB.admins, span_adminnotice("MC restarted successfully"))
else
defcon = min(defcon + 1,5)
master_iteration = Master.iteration
@@ -92,6 +122,57 @@ GLOBAL_REAL(Failsafe, /datum/controller/failsafe)
defcon = 5
sleep(initial(processing_interval))
//Emergency loop used when Master got deleted or the main loop exited while Defcon == 0
//Loop is driven externally so runtimes only cancel the current recovery attempt
/datum/controller/failsafe/proc/emergency_loop()
//The code in this proc should be kept as simple as possible, anything complicated like to_chat might rely on master existing and runtime
//The goal should always be to get a new Master up and running before anything else
. = -1
switch (defcon) //The lower defcon goes the harder we try to fix the MC
if (2 to 3) //Try to normally recreate the MC two times
. = Recreate_MC()
if (1) //Delete the old MC first so we don't transfer any info, in case that caused any issues
del(Master)
. = Recreate_MC()
if (. == 1) //We were able to create a new master
master_iteration = 0
SSticker.Recover(); //Recover the ticket system so the Masters runlevel gets set
Master.Initialize(10, FALSE, TRUE) //Need to manually start the MC, normally world.new would do this
to_chat(GLOB.admins, span_adminnotice("Failsafe recovered MC while in emergency state [defcon_pretty()]"))
else
log_game("FailSafe: Failsafe in emergency state and was unable to recreate MC while in defcon state [defcon_pretty()].")
message_admins(span_boldannounce("Failsafe in emergency state and master down, trying to recreate MC while in defcon level [defcon_pretty()] failed."))
///Recreate all SSs which will still cause data survive due to Recover(), the new Master will then find and take them from global.vars
/proc/recover_all_SS_and_recreate_master()
del(Master)
var/list/subsytem_types = subtypesof(/datum/controller/subsystem)
sortTim(subsytem_types, /proc/cmp_subsystem_init)
for(var/I in subsytem_types)
new I
. = Recreate_MC()
if (. == 1) //We were able to create a new master
SSticker.Recover(); //Recover the ticket system so the Masters runlevel gets set
Master.Initialize(10, FALSE, TRUE) //Need to manually start the MC, normally world.new would do this
to_chat(GLOB.admins, span_adminnotice("MC successfully recreated after recovering all subsystems!"))
else
message_admins(span_boldannounce("Failed to create new MC!"))
///Delete all existing SS to basically start over
/proc/delete_all_SS_and_recreate_master()
del(Master)
for(var/global_var in global.vars)
if (istype(global.vars[global_var], /datum/controller/subsystem))
del(global.vars[global_var])
. = Recreate_MC()
if (. == 1) //We were able to create a new master
SSticker.Recover(); //Recover the ticket system so the Masters runlevel gets set
Master.Initialize(10, FALSE, TRUE) //Need to manually start the MC, normally world.new would do this
to_chat(GLOB.admins, span_adminnotice("MC successfully recreated after deleting and recreating all subsystems!"))
else
message_admins(span_boldannounce("Failed to create new MC!"))
/datum/controller/failsafe/proc/defcon_pretty()
return defcon
+1 -1
View File
@@ -14,7 +14,7 @@ GLOBAL_REAL(GLOB, /datum/controller/global_vars)
var/datum/controller/exclude_these = new
gvars_datum_in_built_vars = exclude_these.vars + list(NAMEOF(src, gvars_datum_protected_varlist), NAMEOF(src, gvars_datum_in_built_vars), NAMEOF(src, gvars_datum_init_order))
QDEL_IN(exclude_these, 0) //signal logging isn't ready
QDEL_IN(exclude_these, 0) //signal logging isn't ready
log_world("[vars.len - gvars_datum_in_built_vars.len] global variables")
+59 -57
View File
@@ -18,15 +18,17 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
/datum/controller/master
name = "Master"
// Are we processing (higher values increase the processing delay by n ticks)
/// Are we processing (higher values increase the processing delay by n ticks)
var/processing = TRUE
// How many times have we ran
/// How many times have we ran
var/iteration = 0
/// Stack end detector to detect stack overflows that kill the mc's main loop
var/datum/stack_end_detector/stack_end_detector
// world.time of last fire, for tracking lag outside of the mc
/// world.time of last fire, for tracking lag outside of the mc
var/last_run
// List of subsystems to process().
/// List of subsystems to process().
var/list/subsystems
// Vars for keeping track of tick drift.
@@ -34,25 +36,27 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/init_time
var/tickdrift = 0
/// How long is the MC sleeping between runs, read only (set by Loop() based off of anti-tick-contention heuristics)
var/sleep_delta = 1
///Only run ticker subsystems for the next n ticks.
/// Only run ticker subsystems for the next n ticks.
var/skip_ticks = 0
/// makes the mc main loop runtime
var/make_runtime = FALSE
var/initializations_finished_with_no_players_logged_in //I wonder what this could be?
var/initializations_finished_with_no_players_logged_in //I wonder what this could be?
// The type of the last subsystem to be process()'d.
/// The type of the last subsystem to be fire()'d.
var/last_type_processed
var/datum/controller/subsystem/queue_head //Start of queue linked list
var/datum/controller/subsystem/queue_tail //End of queue linked list (used for appending to the list)
var/datum/controller/subsystem/queue_head //!Start of queue linked list
var/datum/controller/subsystem/queue_tail //!End of queue linked list (used for appending to the list)
var/queue_priority_count = 0 //Running total so that we don't have to loop thru the queue each run to split up the tick
var/queue_priority_count_bg = 0 //Same, but for background subsystems
var/map_loading = FALSE //Are we loading in a new map?
var/map_loading = FALSE //!Are we loading in a new map?
var/current_runlevel //for scheduling different subsystems for different stages of the round
var/current_runlevel //!for scheduling different subsystems for different stages of the round
var/sleep_offline_after_initializations = TRUE
var/static/restart_clear = 0
@@ -61,8 +65,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/static/random_seed
//current tick limit, assigned before running a subsystem.
//used by CHECK_TICK as well so that the procs subsystems call can obey that SS's tick limits
///current tick limit, assigned before running a subsystem.
///used by CHECK_TICK as well so that the procs subsystems call can obey that SS's tick limits
var/static/current_ticklimit = TICK_LIMIT_RUNNING
/datum/controller/master/New()
@@ -81,15 +85,27 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/list/_subsystems = list()
subsystems = _subsystems
if (Master != src)
if (istype(Master))
if (istype(Master)) //If there is an existing MC take over his stuff and delete it
Recover()
qdel(Master)
Master = src
else
//Code used for first master on game boot or if existing master got deleted
Master = src
var/list/subsytem_types = subtypesof(/datum/controller/subsystem)
sortTim(subsytem_types, /proc/cmp_subsystem_init)
//Find any abandoned subsystem from the previous master (if there was any)
var/list/existing_subsystems = list()
for(var/global_var in global.vars)
if (istype(global.vars[global_var], /datum/controller/subsystem))
existing_subsystems += global.vars[global_var]
//Either init a new SS or if an existing one was found use that
for(var/I in subsytem_types)
_subsystems += new I
Master = src
var/datum/controller/subsystem/existing_subsystem = locate(I) in existing_subsystems
if (istype(existing_subsystem))
_subsystems += existing_subsystem
else
_subsystems += new I
if(!GLOB)
new /datum/controller/global_vars
@@ -109,7 +125,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
log_world("Shutdown complete")
// Returns 1 if we created a new mc, 0 if we couldn't due to a recent restart,
// -1 if we encountered a runtime trying to recreate it
// -1 if we encountered a runtime trying to recreate it
/proc/Recreate_MC()
. = -1 //so if we runtime, things know we failed
if (world.time < Master.restart_timeout)
@@ -120,7 +136,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/delay = 50 * ++Master.restart_count
Master.restart_timeout = world.time + delay
Master.restart_clear = world.time + (delay * 2)
Master.processing = FALSE //stop ticking this one
if (Master) //Can only do this if master hasn't been deleted
Master.processing = FALSE //stop ticking this one
try
new/datum/controller/master()
catch
@@ -156,22 +173,22 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
msg = "The [BadBoy.name] subsystem seems to be destabilizing the MC and will be offlined."
BadBoy.flags |= SS_NO_FIRE
if(msg)
to_chat(GLOB.admins, "<span class='boldannounce'>[msg]</span>")
to_chat(GLOB.admins, span_boldannounce("[msg]"))
log_world(msg)
if (istype(Master.subsystems))
if(FireHim)
Master.subsystems += new BadBoy.type //NEW_SS_GLOBAL will remove the old one
Master.subsystems += new BadBoy.type //NEW_SS_GLOBAL will remove the old one
subsystems = Master.subsystems
current_runlevel = Master.current_runlevel
StartProcessing(10)
else
to_chat(world, "<span class='boldannounce'>The Master Controller is having some issues, we will need to re-initialize EVERYTHING</span>")
to_chat(world, span_boldannounce("The Master Controller is having some issues, we will need to re-initialize EVERYTHING"))
Initialize(20, TRUE)
// Please don't stuff random bullshit here,
// Make a subsystem, give it the SS_NO_FIRE flag, and do your work in it's Initialize()
// Make a subsystem, give it the SS_NO_FIRE flag, and do your work in it's Initialize()
/datum/controller/master/Initialize(delay, init_sss, tgs_prime)
set waitfor = 0
@@ -181,7 +198,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
if(init_sss)
init_subtypes(/datum/controller/subsystem, subsystems)
to_chat(world, "<span class='boldannounce'>Initializing subsystems...</span>")
to_chat(world, span_boldannounce("Initializing subsystems..."))
// Sort subsystems by init_order, so they initialize in the correct order.
sortTim(subsystems, /proc/cmp_subsystem_init)
@@ -190,7 +207,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
// Initialize subsystems.
current_ticklimit = CONFIG_GET(number/tick_limit_mc_init)
for (var/datum/controller/subsystem/SS in subsystems)
if (SS.flags & SS_NO_INIT)
if (SS.flags & SS_NO_INIT || SS.initialized) //Don't init SSs with the correspondig flag or if they already are initialzized
continue
SS.Initialize(REALTIMEOFDAY)
CHECK_TICK
@@ -198,7 +215,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/time = (REALTIMEOFDAY - start_timeofday) / 10
var/msg = "Initializations complete within [time] second[time == 1 ? "" : "s"]!"
to_chat(world, "<span class='boldannounce'>[msg]</span>")
to_chat(world, span_boldannounce("[msg]"))
log_world(msg)
if (!current_runlevel)
@@ -255,11 +272,11 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
/datum/controller/master/proc/Loop()
. = -1
//Prep the loop (most of this is because we want MC restarts to reset as much state as we can, and because
// local vars rock
// local vars rock
//all this shit is here so that flag edits can be refreshed by restarting the MC. (and for speed)
var/list/tickersubsystems = list()
var/list/runlevel_sorted_subsystems = list(list()) //ensure we always have at least one runlevel
var/list/runlevel_sorted_subsystems = list(list()) //ensure we always have at least one runlevel
var/timer = world.time
for (var/thing in subsystems)
var/datum/controller/subsystem/SS = thing
@@ -305,8 +322,12 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/error_level = 0
var/sleep_delta = 1
var/list/subsystems_to_check
//the actual loop.
//setup the stack overflow detector
stack_end_detector = new()
var/datum/stack_canary/canary = stack_end_detector.prime_canary()
canary.use_variable()
//the actual loop.
while (1)
tickdrift = max(0, MC_AVERAGE_FAST(tickdrift, (((REALTIMEOFDAY - init_timeofday) - (world.time - init_time)) / world.tick_lag)))
var/starting_tick_usage = TICK_USAGE
@@ -317,7 +338,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
//Anti-tick-contention heuristics:
//if there are mutiple sleeping procs running before us hogging the cpu, we have to run later.
// (because sleeps are processed in the order received, longer sleeps are more likely to run first)
// (because sleeps are processed in the order received, longer sleeps are more likely to run first)
if (starting_tick_usage > TICK_LIMIT_MC) //if there isn't enough time to bother doing anything this tick, sleep a bit.
sleep_delta *= 2
current_ticklimit = TICK_LIMIT_RUNNING * 0.5
@@ -423,6 +444,10 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
continue
if ((SS_flags & (SS_TICKER|SS_KEEP_TIMING)) == SS_KEEP_TIMING && SS.last_fire + (SS.wait * 0.75) > world.time)
continue
if (SS.postponed_fires >= 1)
SS.postponed_fires--
SS.update_nextfire()
continue
SS.enqueue()
. = 1
@@ -439,12 +464,11 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/tick_precentage
var/tick_remaining
var/ran = TRUE //this is right
var/ran_non_ticker = FALSE
var/bg_calc //have we swtiched current_tick_budget to background mode yet?
var/tick_usage
//keep running while we have stuff to run and we haven't gone over a tick
// this is so subsystems paused eariler can use tick time that later subsystems never used
// this is so subsystems paused eariler can use tick time that later subsystems never used
while (ran && queue_head && TICK_USAGE < TICK_LIMIT_MC)
ran = FALSE
bg_calc = FALSE
@@ -459,20 +483,6 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
if (!(queue_node_flags & SS_TICKER) && skip_ticks)
queue_node = queue_node.queue_next
continue
//super special case, subsystems where we can't make them pause mid way through
//if we can't run them this tick (without going over a tick)
//we bump up their priority and attempt to run them next tick
//(unless we haven't even ran anything this tick, since its unlikely they will ever be able run
// in those cases, so we just let them run)
if (queue_node_flags & SS_NO_TICK_CHECK)
if (queue_node.tick_usage > TICK_LIMIT_RUNNING - TICK_USAGE && ran_non_ticker)
if (!(queue_node_flags & SS_BACKGROUND))
queue_node.queued_priority += queue_priority_count * 0.1
queue_priority_count -= queue_node_priority
queue_priority_count += queue_node.queued_priority
current_tick_budget -= queue_node_priority
queue_node = queue_node.queue_next
continue
if (!bg_calc && (queue_node_flags & SS_BACKGROUND))
current_tick_budget = queue_priority_count_bg
@@ -481,7 +491,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
tick_remaining = TICK_LIMIT_RUNNING - TICK_USAGE
if (current_tick_budget > 0 && queue_node_priority > 0)
tick_precentage = tick_remaining / (current_tick_budget / queue_node_priority)
//Give the subsystem a precentage of the remaining tick based on the remaning priority
tick_precentage = tick_remaining * (queue_node_priority / current_tick_budget)
else
tick_precentage = tick_remaining
@@ -489,8 +500,6 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
current_ticklimit = round(TICK_USAGE + tick_precentage)
if (!(queue_node_flags & SS_TICKER))
ran_non_ticker = TRUE
ran = TRUE
queue_node_paused = (queue_node.state == SS_PAUSED || queue_node.state == SS_PAUSING)
@@ -535,14 +544,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
queue_node.last_fire = world.time
queue_node.times_fired++
if (queue_node_flags & SS_TICKER)
queue_node.next_fire = world.time + (world.tick_lag * queue_node.wait)
else if (queue_node_flags & SS_POST_FIRE_TIMING)
queue_node.next_fire = world.time + queue_node.wait + (world.tick_lag * (queue_node.tick_overrun/100))
else if (queue_node_flags & SS_KEEP_TIMING)
queue_node.next_fire += queue_node.wait
else
queue_node.next_fire = queue_node.queued_time + queue_node.wait + (world.tick_lag * (queue_node.tick_overrun/100))
queue_node.update_nextfire()
queue_node.queued_time = 0
@@ -554,7 +556,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
. = 1
//resets the queue, and all subsystems, while filtering out the subsystem lists
// called if any mc's queue procs runtime or exit improperly.
// called if any mc's queue procs runtime or exit improperly.
/datum/controller/master/proc/SoftReset(list/ticker_SS, list/runlevel_SS)
. = 0
log_world("MC: SoftReset called, resetting MC queue state.")
+43 -14
View File
@@ -29,11 +29,11 @@
var/initialized = FALSE
/// Set to 0 to prevent fire() calls, mostly for admin use or subsystems that may be resumed later
/// use the [SS_NO_FIRE] flag instead for systems that never fire to keep it from even being added to list that is checked every tick
/// use the [SS_NO_FIRE] flag instead for systems that never fire to keep it from even being added to list that is checked every tick
var/can_fire = TRUE
///Bitmap of what game states can this subsystem fire at. See [RUNLEVELS_DEFAULT] for more details.
var/runlevels = RUNLEVELS_DEFAULT //points of the game at which the SS can fire
var/runlevels = RUNLEVELS_DEFAULT //points of the game at which the SS can fire
/*
* The following variables are managed by the MC and should not be modified directly.
@@ -69,6 +69,9 @@
/// Tracks the amount of completed runs for the subsystem
var/times_fired = 0
/// How many fires have we been requested to postpone
var/postponed_fires = 0
/// Time the subsystem entered the queue, (for timing and priority reasons)
var/queued_time = 0
@@ -122,12 +125,38 @@
dequeue()
can_fire = 0
flags |= SS_NO_FIRE
Master.subsystems -= src
if (Master)
Master.subsystems -= src
return ..()
/** Update next_fire for the next run.
* reset_time (bool) - Ignore things that would normally alter the next fire, like tick_overrun, and last_fire. (also resets postpone)
*/
/datum/controller/subsystem/proc/update_nextfire(reset_time = FALSE)
var/queue_node_flags = flags
if (reset_time)
postponed_fires = 0
if (queue_node_flags & SS_TICKER)
next_fire = world.time + (world.tick_lag * wait)
else
next_fire = world.time + wait
return
if (queue_node_flags & SS_TICKER)
next_fire = world.time + (world.tick_lag * wait)
else if (queue_node_flags & SS_POST_FIRE_TIMING)
next_fire = world.time + wait + (world.tick_lag * (tick_overrun/100))
else if (queue_node_flags & SS_KEEP_TIMING)
next_fire += wait
else
next_fire = queued_time + wait + (world.tick_lag * (tick_overrun/100))
//Queue it to run.
// (we loop thru a linked list until we get to the end or find the right point)
// (this lets us sort our run order correctly without having to re-sort the entire already sorted list)
// (we loop thru a linked list until we get to the end or find the right point)
// (this lets us sort our run order correctly without having to re-sort the entire already sorted list)
/datum/controller/subsystem/proc/enqueue()
var/SS_priority = priority
var/SS_flags = flags
@@ -191,9 +220,9 @@
queue_next.queue_prev = queue_prev
if (queue_prev)
queue_prev.queue_next = queue_next
if (src == Master.queue_tail)
if (Master && (src == Master.queue_tail))
Master.queue_tail = queue_prev
if (src == Master.queue_head)
if (Master && (src == Master.queue_head))
Master.queue_head = queue_next
queued_time = 0
if (state == SS_QUEUED)
@@ -217,10 +246,11 @@
//used to initialize the subsystem AFTER the map has loaded
/datum/controller/subsystem/Initialize(start_timeofday)
initialized = TRUE
// SEND_SIGNAL(src, COMSIG_SUBSYSTEM_POST_INITIALIZE, start_timeofday)
var/time = (REALTIMEOFDAY - start_timeofday) / 10
var/msg = "Initialized [name] subsystem within [time] second[time == 1 ? "" : "s"]!"
to_chat(world, "<span class='boldannounce'>[msg]</span>")
log_subsystem("INIT", msg)
to_chat(world, span_boldannounce("[msg]"))
log_subsystem(msg)
return time
/datum/controller/subsystem/stat_entry(msg)
@@ -243,11 +273,10 @@
if (SS_IDLE)
. = " "
//could be used to postpone a costly subsystem for (default one) var/cycles, cycles
//for instance, during cpu intensive operations like explosions
/// Causes the next "cycle" fires to be missed. Effect is accumulative but can reset by calling update_nextfire(reset_time = TRUE)
/datum/controller/subsystem/proc/postpone(cycles = 1)
if(next_fire - world.time < wait)
next_fire += (wait*cycles)
if (can_fire && cycles >= 1)
postponed_fires += cycles
//usually called via datum/controller/subsystem/New() when replacing a subsystem (i.e. due to a recurring crash)
//should attempt to salvage what it can from the old instance of subsystem
@@ -258,7 +287,7 @@
if (NAMEOF(src, can_fire))
//this is so the subsystem doesn't rapid fire to make up missed ticks causing more lag
if (var_value)
next_fire = world.time + wait
update_nextfire(reset_time = TRUE)
if (NAMEOF(src, queued_priority)) //editing this breaks things.
return FALSE
. = ..()
+97 -100
View File
@@ -29,37 +29,35 @@ SUBSYSTEM_DEF(garbage)
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
init_order = INIT_ORDER_GARBAGE
var/list/collection_timeout = list(2 MINUTES, 10 SECONDS) // deciseconds to wait before moving something up in the queue to the next level
var/list/collection_timeout = list(GC_FILTER_QUEUE, GC_DEL_QUEUE) // deciseconds to wait before moving something up in the queue to the next level
//Stat tracking
var/delslasttick = 0 // number of del()'s we've done this tick
var/gcedlasttick = 0 // number of things that gc'ed last tick
var/delslasttick = 0 // number of del()'s we've done this tick
var/gcedlasttick = 0 // number of things that gc'ed last tick
var/totaldels = 0
var/totalgcs = 0
var/highest_del_time = 0
var/highest_del_tickusage = 0
var/highest_del_ms = 0
var/highest_del_type_string = ""
var/list/pass_counts
var/list/fail_counts
var/list/items = list() // Holds our qdel_item statistics datums
var/list/items = list() // Holds our qdel_item statistics datums
//Queue
var/list/queues
#ifdef LEGACY_REFERENCE_TRACKING
#ifdef REFERENCE_TRACKING
var/list/reference_find_on_fail = list()
#ifdef REFERENCE_TRACKING_DEBUG
//Should we save found refs. Used for unit testing
var/should_save_refs = FALSE
#endif
#endif
/datum/controller/subsystem/garbage/PreInit()
queues = new(GC_QUEUE_COUNT)
pass_counts = new(GC_QUEUE_COUNT)
fail_counts = new(GC_QUEUE_COUNT)
for(var/i in 1 to GC_QUEUE_COUNT)
queues[i] = list()
pass_counts[i] = 0
fail_counts[i] = 0
InitQueues()
/datum/controller/subsystem/garbage/stat_entry(msg)
var/list/counts = list()
@@ -90,13 +88,18 @@ SUBSYSTEM_DEF(garbage)
for(var/path in items)
var/datum/qdel_item/I = items[path]
dellog += "Path: [path]"
if (I.qdel_flags & QDEL_ITEM_SUSPENDED_FOR_LAG)
dellog += "\tSUSPENDED FOR LAG"
if (I.failures)
dellog += "\tFailures: [I.failures]"
dellog += "\tqdel() Count: [I.qdels]"
dellog += "\tDestroy() Cost: [I.destroy_time]ms"
if (I.hard_deletes)
dellog += "\tTotal Hard Deletes [I.hard_deletes]"
dellog += "\tTotal Hard Deletes: [I.hard_deletes]"
dellog += "\tTime Spent Hard Deleting: [I.hard_delete_time]ms"
dellog += "\tHighest Time Spent Hard Deleting: [I.hard_delete_max]ms"
if (I.hard_deletes_over_threshold)
dellog += "\tHard Deletes Over Threshold: [I.hard_deletes_over_threshold]"
if (I.slept_destroy)
dellog += "\tSleeps: [I.slept_destroy]"
if (I.no_respect_force)
@@ -122,6 +125,15 @@ SUBSYSTEM_DEF(garbage)
/datum/controller/subsystem/garbage/proc/InitQueues()
if (isnull(queues)) // Only init the queues if they don't already exist, prevents overriding of recovered lists
queues = new(GC_QUEUE_COUNT)
pass_counts = new(GC_QUEUE_COUNT)
fail_counts = new(GC_QUEUE_COUNT)
for(var/i in 1 to GC_QUEUE_COUNT)
queues[i] = list()
pass_counts[i] = 0
fail_counts[i] = 0
/datum/controller/subsystem/garbage/proc/HandleQueue(level = GC_QUEUE_CHECK)
if (level == GC_QUEUE_CHECK)
@@ -153,7 +165,6 @@ SUBSYSTEM_DEF(garbage)
if(GCd_at_time > cut_off_time)
break // Everything else is newer, skip them
count++
var/refID = L[2]
var/datum/D
D = locate(refID)
@@ -162,8 +173,8 @@ SUBSYSTEM_DEF(garbage)
++gcedlasttick
++totalgcs
pass_counts[level]++
#ifdef LEGACY_REFERENCE_TRACKING
reference_find_on_fail -= refID //It's deleted we don't care anymore.
#ifdef REFERENCE_TRACKING
reference_find_on_fail -= refID //It's deleted we don't care anymore.
#endif
if (MC_TICK_CHECK)
return
@@ -171,35 +182,43 @@ SUBSYSTEM_DEF(garbage)
// Something's still referring to the qdel'd object.
fail_counts[level]++
#ifdef REFERENCE_TRACKING
var/ref_searching = FALSE
#endif
switch (level)
if (GC_QUEUE_CHECK)
#ifdef REFERENCE_TRACKING
D.find_references()
#elif defined(LEGACY_REFERENCE_TRACKING)
if(reference_find_on_fail[refID])
D.find_references_legacy()
INVOKE_ASYNC(D, /datum/proc/find_references)
ref_searching = TRUE
#ifdef GC_FAILURE_HARD_LOOKUP
else
D.find_references_legacy()
INVOKE_ASYNC(D, /datum/proc/find_references)
ref_searching = TRUE
#endif
reference_find_on_fail -= refID
#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 --")
#ifdef TESTING
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 (I.qdel_flags & QDEL_ITEM_SUSPENDED_FOR_LAG)
#ifdef REFERENCE_TRACKING
if(ref_searching)
return //ref searching intentionally cancels all further fires while running so things that hold references don't end up getting deleted, so we want to return here instead of continue
#endif
continue
if (GC_QUEUE_HARDDELETE)
HardDelete(D)
if (MC_TICK_CHECK)
@@ -208,27 +227,17 @@ SUBSYSTEM_DEF(garbage)
Queue(D, level+1)
#ifdef REFERENCE_TRACKING
if(ref_searching)
return
#endif
if (MC_TICK_CHECK)
return
if (count)
queue.Cut(1,count+1)
count = 0
#ifdef LEGACY_REFERENCE_TRACKING
/datum/controller/subsystem/garbage/proc/add_type_to_findref(type)
if(!ispath(type))
return "NOT A VAILD PATH"
reference_find_on_fail_types |= typecacheof(type)
/datum/controller/subsystem/garbage/proc/remove_type_from_findref(type)
if(!ispath(type))
return "NOT A VALID PATH"
reference_find_on_fail_types -= typesof(type)
/datum/controller/subsystem/garbage/proc/clear_findref_types()
reference_find_on_fail_types = list()
#endif
/datum/controller/subsystem/garbage/proc/Queue(datum/D, level = GC_QUEUE_CHECK)
if (isnull(D))
return
@@ -238,63 +247,66 @@ SUBSYSTEM_DEF(garbage)
var/gctime = world.time
var/refid = "\ref[D]"
#ifdef LEGACY_REFERENCE_TRACKING
if(reference_find_on_fail_types[D.type])
SSgarbage.reference_find_on_fail[REF(D)] = TRUE
#endif
D.gc_destroyed = gctime
var/list/queue = queues[level]
queue[++queue.len] = list(gctime, refid) // not += for byond reasons
//this is mainly to separate things profile wise.
/datum/controller/subsystem/garbage/proc/HardDelete(datum/D)
var/time = world.timeofday
var/tick = TICK_USAGE
var/ticktime = world.time
++delslasttick
++totaldels
var/type = D.type
var/refID = "\ref[D]"
var/tick_usage = TICK_USAGE
del(D)
tick = (TICK_USAGE-tick+((world.time-ticktime)/world.tick_lag*100))
tick_usage = TICK_USAGE_TO_MS(tick_usage)
var/datum/qdel_item/I = items[type]
I.hard_deletes++
I.hard_delete_time += TICK_DELTA_TO_MS(tick)
I.hard_delete_time += tick_usage
if (tick_usage > I.hard_delete_max)
I.hard_delete_max = tick_usage
if (tick_usage > highest_del_ms)
highest_del_ms = tick_usage
highest_del_type_string = "[type]"
var/time = MS2DS(tick_usage)
if (tick > highest_del_tickusage)
highest_del_tickusage = tick
time = world.timeofday - time
if (!time && TICK_DELTA_TO_MS(tick) > 1)
time = TICK_DELTA_TO_MS(tick)/100
if (time > highest_del_time)
highest_del_time = time
if (time > 10)
log_game("Error: [type]([refID]) took longer than 1 second to delete (took [time/10] seconds to delete)")
message_admins("Error: [type]([refID]) took longer than 1 second to delete (took [time/10] seconds to delete).")
if (time > 0.1 SECONDS)
postpone(time)
var/threshold = CONFIG_GET(number/hard_deletes_overrun_threshold)
if (threshold && (time > threshold SECONDS))
if (!(I.qdel_flags & QDEL_ITEM_ADMINS_WARNED))
log_game("Error: [type]([refID]) took longer than [threshold] seconds to delete (took [round(time/10, 0.1)] seconds to delete)")
message_admins("Error: [type]([refID]) took longer than [threshold] seconds to delete (took [round(time/10, 0.1)] seconds to delete).")
I.qdel_flags |= QDEL_ITEM_ADMINS_WARNED
I.hard_deletes_over_threshold++
var/overrun_limit = CONFIG_GET(number/hard_deletes_overrun_limit)
if (overrun_limit && I.hard_deletes_over_threshold >= overrun_limit)
I.qdel_flags |= QDEL_ITEM_SUSPENDED_FOR_LAG
/datum/controller/subsystem/garbage/Recover()
InitQueues() //We first need to create the queues before recovering data
if (istype(SSgarbage.queues))
for (var/i in 1 to SSgarbage.queues.len)
queues[i] |= SSgarbage.queues[i]
/// Qdel Item: Holds statistics on each type that passes thru qdel
/datum/qdel_item
var/name = ""
var/qdels = 0 //Total number of times it's passed thru qdel.
var/destroy_time = 0 //Total amount of milliseconds spent processing this type's Destroy()
var/failures = 0 //Times it was queued for soft deletion but failed to soft delete.
var/hard_deletes = 0 //Different from failures because it also includes QDEL_HINT_HARDDEL deletions
var/hard_delete_time = 0//Total amount of milliseconds spent hard deleting this type.
var/no_respect_force = 0//Number of times it's not respected force=TRUE
var/no_hint = 0 //Number of times it's not even bother to give a qdel hint
var/slept_destroy = 0 //Number of times it's slept in its destroy
var/name = "" //!Holds the type as a string for this type
var/qdels = 0 //!Total number of times it's passed thru qdel.
var/destroy_time = 0 //!Total amount of milliseconds spent processing this type's Destroy()
var/failures = 0 //!Times it was queued for soft deletion but failed to soft delete.
var/hard_deletes = 0 //!Different from failures because it also includes QDEL_HINT_HARDDEL deletions
var/hard_delete_time = 0 //!Total amount of milliseconds spent hard deleting this type.
var/hard_delete_max = 0 //!Highest time spent hard_deleting this in ms.
var/hard_deletes_over_threshold = 0 //!Number of times hard deletes took longer than the configured threshold
var/no_respect_force = 0 //!Number of times it's not respected force=TRUE
var/no_hint = 0 //!Number of times it's not even bother to give a qdel hint
var/slept_destroy = 0 //!Number of times it's slept in its destroy
var/qdel_flags = 0 //!Flags related to this type's trip thru qdel.
/datum/qdel_item/New(mytype)
name = "[mytype]"
@@ -307,12 +319,12 @@ SUBSYSTEM_DEF(garbage)
if(!istype(D))
del(D)
return
var/datum/qdel_item/I = SSgarbage.items[D.type]
if (!I)
I = SSgarbage.items[D.type] = new /datum/qdel_item(D.type)
I.qdels++
if(isnull(D.gc_destroyed))
if (SEND_SIGNAL(D, COMSIG_PARENT_PREQDELETED, force)) // Give the components a chance to prevent their parent from being deleted
return
@@ -328,12 +340,12 @@ SUBSYSTEM_DEF(garbage)
if(!D)
return
switch(hint)
if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion.
if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion.
SSgarbage.Queue(D)
if (QDEL_HINT_IWILLGC)
D.gc_destroyed = world.time
return
if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory.
if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory.
if(!force)
D.gc_destroyed = null //clear the gc variable (important!)
return
@@ -350,17 +362,17 @@ SUBSYSTEM_DEF(garbage)
I.no_respect_force++
SSgarbage.Queue(D)
if (QDEL_HINT_HARDDEL) //qdel should assume this object won't gc, and queue a hard delete
if (QDEL_HINT_HARDDEL) //qdel should assume this object won't gc, and queue a hard delete
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.
if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste.
SSgarbage.HardDelete(D)
#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.
#ifdef REFERENCE_TRACKING
if (QDEL_HINT_FINDREFERENCE) //qdel will, if REFERENCE_TRACKING is enabled, display all references to this object, then queue the object for deletion.
SSgarbage.Queue(D)
D.find_references_legacy()
if (QDEL_HINT_IFFAIL_FINDREFERENCE)
D.find_references()
if (QDEL_HINT_IFFAIL_FINDREFERENCE) //qdel will, if REFERENCE_TRACKING is enabled and the object fails to collect, display all references to this object.
SSgarbage.Queue(D)
SSgarbage.reference_find_on_fail[REF(D)] = TRUE
SSgarbage.reference_find_on_fail["\ref[D]"] = TRUE
#endif
else
#ifdef TESTING
@@ -371,18 +383,3 @@ SUBSYSTEM_DEF(garbage)
SSgarbage.Queue(D)
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
/proc/writeDatumCount()
var/list/datums = list()
for(var/datum/D in world)
datums[D.type] += 1
for(var/datum/D)
datums[D.type] += 1
datums = sortTim(datums, /proc/cmp_numeric_dsc, associative = TRUE)
if(fexists("data/DATUMCOUNT.txt"))
fdel("data/DATUMCOUNT.txt")
var/outfile = file("data/DATUMCOUNT.txt")
for(var/path in datums)
outfile << "[datums[path]]\t\t\t\t\t[path]"
#endif
+5 -1
View File
@@ -57,9 +57,13 @@
*/
var/list/cooldowns
#ifdef TESTING
#ifdef REFERENCE_TRACKING
var/running_find_references
var/last_find_references = 0
#ifdef REFERENCE_TRACKING_DEBUG
///Stores info about where refs are found, used for sanity checks and testing
var/list/found_refs
#endif
#endif
#ifdef DATUMVAR_DEBUGGING_MODE
@@ -0,0 +1,32 @@
/**
Stack End Detector.
Can detect if a given code stack has exited, used by the mc for stack overflow detection.
**/
/datum/stack_end_detector
var/datum/weakref/_WF
var/datum/stack_canary/_canary
/datum/stack_end_detector/New()
_canary = new()
_WF = WEAKREF(_canary)
/** Prime the stack overflow detector.
Store the return value of this proc call in a proc level var.
Can only be called once.
**/
/datum/stack_end_detector/proc/prime_canary()
if (!_canary)
CRASH("Prime_canary called twice")
. = _canary
_canary = null
/// Returns true if the stack is still going. Calling before the canary has been primed also returns true
/datum/stack_end_detector/proc/check()
return !!_WF.resolve()
/// Stack canary. Will go away if the stack it was primed by is ended by byond for return or stack overflow reasons.
/datum/stack_canary
/// empty proc to avoid warnings about unused variables. Call this proc on your canary in the stack it's watching.
/datum/stack_canary/proc/use_variable()
+11 -3
View File
@@ -6,10 +6,12 @@
var/body
var/headers
var/url
/// If present response body will be saved to this file.
var/output_file
var/_raw_response
/datum/http_request/proc/prepare(method, url, body = "", list/headers)
/datum/http_request/proc/prepare(method, url, body = "", list/headers, output_file)
if (!length(headers))
headers = ""
else
@@ -19,15 +21,16 @@
src.url = url
src.body = body
src.headers = headers
src.output_file = output_file
/datum/http_request/proc/execute_blocking()
_raw_response = rustg_http_request_blocking(method, url, body, headers)
_raw_response = rustg_http_request_blocking(method, url, body, headers, build_options())
/datum/http_request/proc/begin_async()
if (in_progress)
CRASH("Attempted to re-use a request object.")
id = rustg_http_request_async(method, url, body, headers)
id = rustg_http_request_async(method, url, body, headers, build_options())
if (isnull(text2num(id)))
stack_trace("Proc error: [id]")
@@ -35,6 +38,11 @@
else
in_progress = TRUE
/datum/http_request/proc/build_options()
if(output_file)
return json_encode(list("output_filename"=output_file,"body_filename"=null))
return null
/datum/http_request/proc/is_complete()
if (isnull(id))
return TRUE
+1 -1
View File
@@ -16,7 +16,7 @@ GLOBAL_LIST(topic_status_cache)
call(debug_server, "auxtools_init")()
enable_debugging()
AUXTOOLS_CHECK(AUXMOS)
#ifdef REFERENCE_TRACKING
#ifdef EXTOOLS_REFERENCE_TRACKING
enable_reference_tracking()
#endif
world.Profile(PROFILE_START)
+1 -1
View File
@@ -195,7 +195,7 @@ GLOBAL_PROTECT(admin_verbs_debug)
// #endif
/datum/admins/proc/create_or_modify_area,
/datum/admins/proc/fixcorruption,
#ifdef REFERENCE_TRACKING
#ifdef EXTOOLS_REFERENCE_TRACKING
/datum/admins/proc/view_refs,
/datum/admins/proc/view_del_failures,
#endif
+13 -2
View File
@@ -2862,7 +2862,7 @@
return
if(!CONFIG_GET(string/centcom_ban_db))
to_chat(usr, "<span class='warning'>Centcom Galactic Ban DB is disabled!</span>")
to_chat(usr, span_warning("Centcom Galactic Ban DB is disabled!"))
return
var/ckey = href_list["centcomlookup"]
@@ -2889,8 +2889,19 @@
dat += "<center><b>0 bans detected for [ckey]</b></center>"
else
bans = json_decode(response["body"])
dat += "<center><b>[bans.len] ban\s detected for [ckey]</b></center>"
//Ignore bans from non-whitelisted sources, if a whitelist exists
var/list/valid_sources
if(CONFIG_GET(string/centcom_source_whitelist))
valid_sources = splittext(CONFIG_GET(string/centcom_source_whitelist), ",")
dat += "<center><b>Bans detected for [ckey]</b></center>"
else
//Ban count is potentially inaccurate if they're using a whitelist
dat += "<center><b>[bans.len] ban\s detected for [ckey]</b></center>"
for(var/list/ban in bans)
if(valid_sources && !(ban["sourceName"] in valid_sources))
continue
dat += "<b>Server: </b> [sanitize(ban["sourceName"])]<br>"
dat += "<b>RP Level: </b> [sanitize(ban["sourceRoleplayLevel"])]<br>"
dat += "<b>Type: </b> [sanitize(ban["type"])]<br>"
@@ -1,4 +1,4 @@
#ifdef REFERENCE_TRACKING
#ifdef EXTOOLS_REFERENCE_TRACKING
GLOBAL_LIST_EMPTY(deletion_failures)
@@ -102,29 +102,21 @@ GLOBAL_LIST_EMPTY(deletion_failures)
#endif
#ifdef LEGACY_REFERENCE_TRACKING
#ifdef REFERENCE_TRACKING
/datum/verb/legacy_find_refs()
set category = "Debug"
set name = "Find References"
set src in world
find_references_legacy(FALSE)
/datum/proc/find_references_legacy(skip_alert)
/datum/proc/find_references(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].")
log_reftracker("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
SSgarbage.update_nextfire(reset_time = TRUE)
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")
if(!skip_alert && 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")
running_find_references = null
return
@@ -134,92 +126,122 @@ GLOBAL_LIST_EMPTY(deletion_failures)
if(usr?.client)
usr.client.running_find_references = type
testing("Beginning search for references to a [type].")
last_find_references = world.time
log_reftracker("Beginning search for references to a [type].")
var/starting_time = world.time
//Time to search the whole game for our ref
DoSearchVar(GLOB, "GLOB") //globals
log_reftracker("Finished searching globals")
DoSearchVar(GLOB) //globals
for(var/datum/thing in world) //atoms (don't beleive its lies)
DoSearchVar(thing, "World -> [thing]")
DoSearchVar(thing, "World -> [thing.type]", search_time = starting_time)
log_reftracker("Finished searching atoms")
for(var/datum/thing) //datums
DoSearchVar(thing, "World -> [thing]")
DoSearchVar(thing, "Datums -> [thing.type]", search_time = starting_time)
log_reftracker("Finished searching datums")
//Warning, attempting to search clients like this will cause crashes if done on live. Watch yourself
for(var/client/thing) //clients
DoSearchVar(thing, "World -> [thing]")
DoSearchVar(thing, "Clients -> [thing.type]", search_time = starting_time)
log_reftracker("Finished searching clients")
log_reftracker("Completed search for references to a [type].")
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
SSgarbage.update_nextfire(reset_time = TRUE)
/datum/proc/DoSearchVar(potential_container, container_name, recursive_limit = 64, search_time = world.time)
#ifdef REFERENCE_TRACKING_DEBUG
if(!found_refs && SSgarbage.should_save_refs)
found_refs = list()
#endif
/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_legacy(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)
log_reftracker("Recursion limit reached. [container_name]")
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)
//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(istype(potential_container, /datum))
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
for(var/varname in vars_list)
#ifndef FIND_REF_NO_CHECK_TICK
CHECK_TICK
#endif
if (varname == "vars" || varname == "vis_locs") //Fun fact, vis_locs don't count for references
continue
var/variable = vars_list[varname]
if(variable == src)
#ifdef REFERENCE_TRACKING_DEBUG
if(SSgarbage.should_save_refs)
found_refs[varname] = TRUE
#endif
log_reftracker("Found [type] \ref[src] in [datum_container.type]'s \ref[datum_container] [varname] var. [container_name]")
continue
if(islist(variable))
DoSearchVar(variable, "[container_name] \ref[datum_container] -> [varname] (list)", recursive_limit - 1, search_time)
else if(islist(potential_container))
var/normal = IS_NORMAL_LIST(potential_container)
var/list/potential_cache = potential_container
for(var/element_in_list in potential_cache)
#ifndef FIND_REF_NO_CHECK_TICK
CHECK_TICK
#endif
//Check normal entrys
if(element_in_list == src)
#ifdef REFERENCE_TRACKING_DEBUG
if(SSgarbage.should_save_refs)
found_refs[potential_cache] = TRUE
#endif
log_reftracker("Found [type] \ref[src] in list [container_name].")
continue
var/assoc_val = null
if(!isnum(element_in_list) && normal)
assoc_val = potential_cache[element_in_list]
//Check assoc entrys
if(assoc_val == src)
#ifdef REFERENCE_TRACKING_DEBUG
if(SSgarbage.should_save_refs)
found_refs[potential_cache] = TRUE
#endif
log_reftracker("Found [type] \ref[src] in list [container_name]\[[element_in_list]\]")
continue
//We need to run both of these checks, since our object could be hiding in either of them
//Check normal sublists
if(islist(element_in_list))
DoSearchVar(element_in_list, "[container_name] -> [element_in_list] (list)", recursive_limit - 1, search_time)
//Check assoc sublists
if(islist(assoc_val))
DoSearchVar(potential_container[element_in_list], "[container_name]\[[element_in_list]\] -> [assoc_val] (list)", recursive_limit - 1, search_time)
/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)
thing_to_del.qdel_and_find_ref_if_fail(force)
/datum/proc/qdel_and_find_ref_if_fail(force = FALSE)
SSgarbage.reference_find_on_fail["\ref[src]"] = TRUE
qdel(src, force)
#endif
@@ -47,7 +47,7 @@
usr.client.debug_variables(src)
return
#ifdef REFERENCE_TRACKING
#ifdef EXTOOLS_REFERENCE_TRACKING
if(href_list[VV_HK_VIEW_REFERENCES])
var/datum/D = locate(href_list[VV_HK_TARGET])
if(!D)
+10 -8
View File
@@ -3,21 +3,21 @@
/datum/asset/simple/tgui_common
keep_local_name = TRUE
assets = list(
"tgui-common.bundle.js" = 'tgui/public/tgui-common.bundle.js',
"tgui-common.bundle.js" = file("tgui/public/tgui-common.bundle.js"),
)
/datum/asset/simple/tgui
keep_local_name = TRUE
assets = list(
"tgui.bundle.js" = 'tgui/public/tgui.bundle.js',
"tgui.bundle.css" = 'tgui/public/tgui.bundle.css',
"tgui.bundle.js" = file("tgui/public/tgui.bundle.js"),
"tgui.bundle.css" = file("tgui/public/tgui.bundle.css"),
)
/datum/asset/simple/tgui_panel
keep_local_name = TRUE
assets = list(
"tgui-panel.bundle.js" = 'tgui/public/tgui-panel.bundle.js',
"tgui-panel.bundle.css" = 'tgui/public/tgui-panel.bundle.css',
"tgui-panel.bundle.js" = file("tgui/public/tgui-panel.bundle.js"),
"tgui-panel.bundle.css" = file("tgui/public/tgui-panel.bundle.css"),
)
/datum/asset/simple/headers
@@ -168,10 +168,12 @@
/datum/asset/simple/namespaced/tgfont
assets = list(
"tgfont.eot" = 'tgui/packages/tgfont/dist/tgfont.eot',
"tgfont.woff2" = 'tgui/packages/tgfont/dist/tgfont.woff2',
"tgfont.eot" = file("tgui/packages/tgfont/dist/tgfont.eot"),
"tgfont.woff2" = file("tgui/packages/tgfont/dist/tgfont.woff2"),
)
parents = list(
"tgfont.css" = file("tgui/packages/tgfont/dist/tgfont.css"),
)
parents = list("tgfont.css" = 'tgui/packages/tgfont/dist/tgfont.css')
/datum/asset/spritesheet/chat
name = "chat"
+12
View File
@@ -641,6 +641,18 @@ Since Ramadan is an entire month that lasts 29.5 days on average, the start and
/datum/holiday/easter/getStationPrefix()
return pick("Fluffy","Bunny","Easter","Egg")
/datum/holiday/ianbirthday
name = "Ian's Birthday" //github.com/tgstation/tgstation/commit/de7e4f0de0d568cd6e1f0d7bcc3fd34700598acb
begin_month = SEPTEMBER
begin_day = 9
end_day = 10
/datum/holiday/ianbirthday/greet()
return "Happy birthday, Ian!"
/datum/holiday/ianbirthday/getStationPrefix()
return pick("Ian", "Corgi", "Erro")
//Random citadel thing for halloween species
/proc/force_enable_halloween_species()
var/list/oldlist = SSevents.holidays
@@ -8,61 +8,73 @@
..()
return late ? INITIALIZE_HINT_LATELOAD : INITIALIZE_HINT_QDEL
//airlock helpers
/obj/effect/mapping_helpers/airlock
layer = DOOR_HELPER_LAYER
/obj/effect/mapping_helpers/airlock/Initialize(mapload)
. = ..()
if(!mapload)
log_mapping("[src] spawned outside of mapload!")
return
var/obj/machinery/door/airlock/airlock = locate(/obj/machinery/door/airlock) in loc
if(!airlock)
log_mapping("[src] failed to find an airlock at [AREACOORD(src)]")
else
payload(airlock)
/obj/effect/mapping_helpers/airlock/proc/payload(obj/machinery/door/airlock/payload)
return
/obj/effect/mapping_helpers/airlock/cyclelink_helper
name = "airlock cyclelink helper"
icon_state = "airlock_cyclelink_helper"
/obj/effect/mapping_helpers/airlock/cyclelink_helper/Initialize(mapload)
. = ..()
if(!mapload)
log_mapping("[src] spawned outside of mapload!")
return
var/obj/machinery/door/airlock/airlock = locate(/obj/machinery/door/airlock) in loc
if(airlock)
if(airlock.cyclelinkeddir)
log_mapping("[src] at [AREACOORD(src)] tried to set [airlock] cyclelinkeddir, but it's already set!")
else
airlock.cyclelinkeddir = dir
/obj/effect/mapping_helpers/airlock/cyclelink_helper/payload(obj/machinery/door/airlock/airlock)
if(airlock.cyclelinkeddir)
log_mapping("[src] at [AREACOORD(src)] tried to set [airlock] cyclelinkeddir, but it's already set!")
else
log_mapping("[src] failed to find an airlock at [AREACOORD(src)]")
airlock.cyclelinkeddir = dir
/obj/effect/mapping_helpers/airlock/cyclelink_helper_multi
name = "airlock multi-cyclelink helper"
icon_state = "airlock_multicyclelink_helper"
var/cycle_id
/obj/effect/mapping_helpers/airlock/cyclelink_helper_multi/payload(obj/machinery/door/airlock/airlock)
if(airlock.closeOtherId)
log_mapping("[src] at [AREACOORD(src)] tried to set [airlock] closeOtherId, but it's already set!")
else
airlock.closeOtherId = cycle_id
/obj/effect/mapping_helpers/airlock/locked
name = "airlock lock helper"
icon_state = "airlock_locked_helper"
/obj/effect/mapping_helpers/airlock/locked/Initialize(mapload)
. = ..()
if(!mapload)
log_mapping("[src] spawned outside of mapload!")
return
var/obj/machinery/door/airlock/airlock = locate(/obj/machinery/door/airlock) in loc
if(airlock)
if(airlock.locked)
log_mapping("[src] at [AREACOORD(src)] tried to bolt [airlock] but it's already locked!")
else
airlock.locked = TRUE
/obj/effect/mapping_helpers/airlock/locked/payload(obj/machinery/door/airlock/airlock)
if(airlock.locked)
log_mapping("[src] at [AREACOORD(src)] tried to bolt [airlock] but it's already locked!")
else
log_mapping("[src] failed to find an airlock at [AREACOORD(src)]")
airlock.locked = TRUE
/obj/effect/mapping_helpers/airlock/unres
name = "airlock unresctricted side helper"
icon_state = "airlock_unres_helper"
/obj/effect/mapping_helpers/airlock/unres/Initialize(mapload)
. = ..()
if(!mapload)
log_mapping("[src] spawned outside of mapload!")
return
var/obj/machinery/door/airlock/airlock = locate(/obj/machinery/door/airlock) in loc
if(airlock)
airlock.unres_sides ^= dir
/obj/effect/mapping_helpers/airlock/unres/payload(obj/machinery/door/airlock/airlock)
airlock.unres_sides ^= dir
/obj/effect/mapping_helpers/airlock/abandoned
name = "airlock abandoned helper"
icon_state = "airlock_abandoned"
/obj/effect/mapping_helpers/airlock/abandoned/payload(obj/machinery/door/airlock/airlock)
if(airlock.abandoned)
log_mapping("[src] at [AREACOORD(src)] tried to make [airlock] abandoned but it's already abandoned!")
else
log_mapping("[src] failed to find an airlock at [AREACOORD(src)]")
airlock.abandoned = TRUE
//needs to do its thing before spawn_rivers() is called
@@ -79,9 +91,11 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_lava)
//This helper applies components to things on the map directly.
/obj/effect/mapping_helpers/component_injector
name = "Component Injector"
icon_state = "component"
late = TRUE
var/target_type
var/target_name
var/all = FALSE //Will inject into all fitting the criteria if true, otherwise first found
var/target_type //Will inject into atoms of this type
var/target_name //Will inject into atoms with this name
var/component_type
//Late init so everything is likely ready and loaded (no warranty)
@@ -98,8 +112,11 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_lava)
continue
var/cargs = build_args()
A._AddComponent(cargs)
if(!all)
qdel(src)
return
if(all)
qdel(src)
return
/obj/effect/mapping_helpers/component_injector/proc/build_args()
return list(component_type)
@@ -115,3 +132,276 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_lava)
CRASH("Wrong disease type passed in.")
var/datum/disease/D = new disease_type()
return list(component_type,D)
// /obj/effect/mapping_helpers/component_injector/areabound
// name = "Areabound Injector"
// icon_state = "component_areabound"
// component_type = /datum/component/areabound
// target_type = /atom/movable
/obj/effect/mapping_helpers/dead_body_placer
name = "Dead Body placer"
late = TRUE
icon_state = "deadbodyplacer"
var/bodycount = 2 //number of bodies to spawn
/obj/effect/mapping_helpers/dead_body_placer/LateInitialize()
var/area/a = get_area(src)
var/list/trays = list()
for (var/i in a.contents)
if (istype(i, /obj/structure/bodycontainer/morgue))
trays += i
if(!trays.len)
log_mapping("[src] at [x],[y] could not find any morgues.")
return
for (var/i = 1 to bodycount)
var/obj/structure/bodycontainer/morgue/j = pick(trays)
var/mob/living/carbon/human/h = new /mob/living/carbon/human(j, 1)
h.death()
for (var/part in h.internal_organs) //randomly remove organs from each body, set those we keep to be in stasis
if (prob(40))
qdel(part)
else
var/obj/item/organ/O = part
O.organ_flags |= ORGAN_FROZEN
j.update_appearance()
qdel(src)
//On Ian's birthday, the hop's office is decorated.
/obj/effect/mapping_helpers/ianbirthday
name = "Ian's Bday Helper"
late = TRUE
icon_state = "iansbdayhelper"
var/balloon_clusters = 2
/obj/effect/mapping_helpers/ianbirthday/LateInitialize()
if(locate(/datum/holiday/ianbirthday) in SSevents.holidays)
birthday()
qdel(src)
/obj/effect/mapping_helpers/ianbirthday/proc/birthday()
var/area/a = get_area(src)
var/list/table = list()//should only be one aka the front desk, but just in case...
var/list/openturfs = list()
//confetti and a corgi balloon! (and some list stuff for more decorations)
for(var/thing in a.contents)
if(istype(thing, /obj/structure/table/reinforced))
table += thing
if(isopenturf(thing))
// new /obj/effect/decal/cleanable/confetti(thing)
// if(locate(/obj/structure/bed/dogbed/ian) in thing)
// new /obj/item/toy/balloon/corgi(thing)
// else
openturfs += thing
//cake + knife to cut it!
if(length(table))
var/turf/food_turf = get_turf(pick(table))
new /obj/item/kitchen/knife(food_turf)
var/obj/item/reagent_containers/food/snacks/store/cake/birthday/iancake = new(food_turf)
iancake.desc = "Happy birthday, Ian!"
// remind me to give ian proper baloons!
//some balloons! this picks an open turf and pops a few balloons in and around that turf, yay.
// for(var/i in 1 to balloon_clusters)
// var/turf/clusterspot = pick_n_take(openturfs)
// new /obj/item/toy/balloon(clusterspot)
// var/balloons_left_to_give = 3 //the amount of balloons around the cluster
// var/list/dirs_to_balloon = GLOB.cardinals.Copy()
// while(balloons_left_to_give > 0)
// balloons_left_to_give--
// var/chosen_dir = pick_n_take(dirs_to_balloon)
// var/turf/balloonstep = get_step(clusterspot, chosen_dir)
// var/placed = FALSE
// if(isopenturf(balloonstep))
// var/obj/item/toy/balloon/B = new(balloonstep)//this clumps the cluster together
// placed = TRUE
// if(chosen_dir == NORTH)
// B.pixel_y -= 10
// if(chosen_dir == SOUTH)
// B.pixel_y += 10
// if(chosen_dir == EAST)
// B.pixel_x -= 10
// if(chosen_dir == WEST)
// B.pixel_x += 10
// if(!placed)
// new /obj/item/toy/balloon(clusterspot)
//remind me to add wall decor!
/obj/effect/mapping_helpers/ianbirthday/admin//so admins may birthday any room
name = "generic birthday setup"
icon_state = "bdayhelper"
/obj/effect/mapping_helpers/ianbirthday/admin/LateInitialize()
birthday()
qdel(src)
//Ian, like most dogs, loves a good new years eve party.
/obj/effect/mapping_helpers/iannewyear
name = "Ian's New Years Helper"
late = TRUE
icon_state = "iansnewyrshelper"
/obj/effect/mapping_helpers/iannewyear/LateInitialize()
if(SSevents.holidays && SSevents.holidays[NEW_YEAR])
fireworks()
qdel(src)
/obj/effect/mapping_helpers/iannewyear/proc/fireworks()
var/area/a = get_area(src)
var/list/table = list()//should only be one aka the front desk, but just in case...
var/list/openturfs = list()
for(var/thing in a.contents)
if(istype(thing, /obj/structure/table/reinforced))
table += thing
else if(isopenturf(thing))
if(locate(/obj/structure/bed/dogbed/ian) in thing)
new /obj/item/clothing/head/festive(thing)
var/obj/item/reagent_containers/food/drinks/bottle/champagne/iandrink = new(thing)
iandrink.name = "dog champagne"
iandrink.pixel_y += 8
iandrink.pixel_x += 8
else
openturfs += thing
var/turf/fireworks_turf = get_turf(pick(table))
var/obj/item/storage/box/matches/matchbox = new(fireworks_turf)
matchbox.pixel_y += 8
matchbox.pixel_x -= 3
// new /obj/item/storage/box/fireworks/dangerous(fireworks_turf) //dangerous version for extra holiday memes.
//lets mappers place notes on airlocks with custom info or a pre-made note from a path
/obj/effect/mapping_helpers/airlock_note_placer
name = "Airlock Note Placer"
late = TRUE
icon_state = "airlocknoteplacer"
var/note_info //for writing out custom notes without creating an extra paper subtype
var/note_name //custom note name
var/note_path //if you already have something wrote up in a paper subtype, put the path here
/obj/effect/mapping_helpers/airlock_note_placer/LateInitialize()
var/turf/turf = get_turf(src)
if(note_path && !istype(note_path, /obj/item/paper)) //don't put non-paper in the paper slot thank you
log_mapping("[src] at [x],[y] had an improper note_path path, could not place paper note.")
qdel(src)
if(locate(/obj/machinery/door/airlock) in turf)
var/obj/machinery/door/airlock/found_airlock = locate(/obj/machinery/door/airlock) in turf
if(note_path)
found_airlock.note = note_path
found_airlock.update_appearance()
qdel(src)
if(note_info)
var/obj/item/paper/paper = new /obj/item/paper(src)
if(note_name)
paper.name = note_name
paper.info = "[note_info]"
found_airlock.note = paper
paper.forceMove(found_airlock)
found_airlock.update_appearance()
qdel(src)
log_mapping("[src] at [x],[y] had no note_path or note_info, cannot place paper note.")
qdel(src)
log_mapping("[src] at [x],[y] could not find an airlock on current turf, cannot place paper note.")
qdel(src)
//This helper applies traits to things on the map directly.
/obj/effect/mapping_helpers/trait_injector
name = "Trait Injector"
icon_state = "trait"
late = TRUE
///Will inject into all fitting the criteria if false, otherwise first found.
var/first_match_only = TRUE
///Will inject into atoms of this type.
var/target_type
///Will inject into atoms with this name.
var/target_name
///Name of the trait, in the lower-case text (NOT the upper-case define) form.
var/trait_name
//Late init so everything is likely ready and loaded (no warranty)
/obj/effect/mapping_helpers/trait_injector/LateInitialize()
if(!GLOB.trait_name_map)
GLOB.trait_name_map = generate_trait_name_map()
if(!GLOB.trait_name_map.Find(trait_name))
CRASH("Wrong trait in [type] - [trait_name] is not a trait")
var/turf/target_turf = get_turf(src)
var/matches_found = 0
for(var/a in target_turf.GetAllContents())
var/atom/atom_on_turf = a
if(atom_on_turf == src)
continue
if(target_name && atom_on_turf.name != target_name)
continue
if(target_type && !istype(atom_on_turf,target_type))
continue
ADD_TRAIT(atom_on_turf, trait_name, MAPPING_HELPER_TRAIT)
matches_found++
if(first_match_only)
qdel(src)
return
if(!matches_found)
stack_trace("Trait mapper found no targets at ([x], [y], [z]). First Match Only: [first_match_only ? "true" : "false"] target type: [target_type] | target name: [target_name] | trait name: [trait_name]")
qdel(src)
/// Fetches an external dmi and applies to the target object
/obj/effect/mapping_helpers/custom_icon
name = "Custom Icon Helper"
icon_state = "trait"
late = TRUE
///Will inject into all fitting the criteria if false, otherwise first found.
var/first_match_only = TRUE
///Will inject into atoms of this type.
var/target_type
///Will inject into atoms with this name.
var/target_name
/// This is the var tha will be set with the fetched icon. In case you want to set some secondary icon sheets like inhands and such.
var/target_variable = "icon"
/// This should return raw dmi in response to http get request. For example: "https://github.com/tgstation/SS13-sprites/raw/master/mob/medu.dmi?raw=true"
var/icon_url
/obj/effect/mapping_helpers/custom_icon/LateInitialize()
///TODO put this injector stuff under common root
var/I = fetch_icon(icon_url)
var/turf/target_turf = get_turf(src)
var/matches_found = 0
for(var/a in target_turf.GetAllContents())
var/atom/atom_on_turf = a
if(atom_on_turf == src)
continue
if(target_name && atom_on_turf.name != target_name)
continue
if(target_type && !istype(atom_on_turf,target_type))
continue
atom_on_turf.vars[target_variable] = I
matches_found++
if(first_match_only)
qdel(src)
return
if(!matches_found)
stack_trace("[src] found no targets at ([x], [y], [z]). First Match Only: [first_match_only ? "true" : "false"] target type: [target_type] | target name: [target_name]")
qdel(src)
/obj/effect/mapping_helpers/custom_icon/proc/fetch_icon(url)
var/static/icon_cache = list()
var/static/query_in_progress = FALSE //We're using a single tmp file so keep it linear.
if(query_in_progress)
UNTIL(!query_in_progress)
if(icon_cache[url])
return icon_cache[url]
log_asset("Custom Icon Helper fetching dmi from: [url]")
var/datum/http_request/request = new()
var/file_name = "tmp/custom_map_icon.dmi"
request.prepare(RUSTG_HTTP_METHOD_GET, url , "", "", file_name)
query_in_progress = TRUE
request.begin_async()
UNTIL(request.is_complete())
var/datum/http_response/response = request.into_response()
if(response.errored || response.status_code != 200)
query_in_progress = FALSE
CRASH("Failed to fetch mapped custom icon from url [url], code: [response.status_code], error: [response.error]")
var/icon/I = new(file_name)
icon_cache[url] = I
query_in_progress = FALSE
return I
@@ -30,8 +30,8 @@
qdel(src)
/obj/effect/baseturf_helper/proc/replace_baseturf(turf/thing)
var/list/baseturf_cache = thing.baseturfs
if(length(baseturf_cache))
if(length(thing.baseturfs))
var/list/baseturf_cache = thing.baseturfs.Copy()
for(var/i in baseturf_cache)
if(baseturf_to_replace[i])
baseturf_cache -= i
@@ -44,6 +44,8 @@
else
thing.PlaceOnBottom(null, baseturf)
/obj/effect/baseturf_helper/space
name = "space baseturf editor"
baseturf = /turf/open/space
@@ -79,4 +81,3 @@
/obj/effect/baseturf_helper/lava_land/surface
name = "lavaland baseturf editor"
baseturf = /turf/open/lava/smooth/lava_land_surface
+33 -1
View File
@@ -32,47 +32,79 @@
/// Intended to be used in the manner of `TEST_FOCUS(/datum/unit_test/math)`
#define TEST_FOCUS(test_path) ##test_path { focus = TRUE; }
/// Constants indicating unit test completion status
#define UNIT_TEST_PASSED 0
#define UNIT_TEST_FAILED 1
#define UNIT_TEST_SKIPPED 2
#define TEST_DEFAULT 1
#define TEST_DEL_WORLD INFINITY
/// A trait source when adding traits through unit tests
#define TRAIT_SOURCE_UNIT_TESTS "unit_tests"
#include "anchored_mobs.dm"
#include "bespoke_id.dm"
#include "binary_insert.dm"
// #include "bloody_footprints.dm"
// #include "breath.dm"
// #include "card_mismatch.dm"
#include "chain_pull_through_space.dm"
// #include "combat.dm"
#include "component_tests.dm"
// #include "connect_loc.dm"
// #include "confusion.dm"
// #include "crayons.dm"
// #include "create_and_destroy.dm"
// #include "designs.dm"
// #include "dynamic_ruleset_sanity.dm"
// #include "egg_glands.dm"
// #include "emoting.dm"
// #include "food_edibility_check.dm"
// #include "greyscale_config.dm"
// #include "heretic_knowledge.dm"
// #include "holidays.dm"
#include "initialize_sanity.dm"
// #include "hydroponics_harvest.dm"
// #include "keybinding_init.dm"
#include "machine_disassembly.dm"
#include "medical_wounds.dm"
#include "merge_type.dm"
// #include "metabolizing.dm"
// #include "ntnetwork_tests.dm"
// #include "outfit_sanity.dm"
// #include "pills.dm"
// #include "plantgrowth_tests.dm"
// #include "projectiles.dm"
// #include "rcd.dm"
#include "reagent_id_typos.dm"
// #include "reagent_mod_expose.dm"
// #include "reagent_mod_procs.dm"
#include "reagent_recipe_collisions.dm"
#include "resist.dm"
// #include "say.dm"
// #include "security_officer_distribution.dm"
// #include "serving_tray.dm"
// #include "siunit.dm"
#include "spawn_humans.dm"
#include "spawn_mobs.dm"
// #include "species_whitelists.dm"
// #include "stomach.dm"
// #include "strippable.dm"
#include "subsystem_init.dm"
#include "surgeries.dm"
#include "teleporters.dm"
#include "tgui_create_message.dm"
#include "timer_sanity.dm"
#include "unit_test.dm"
// #include "wizard.dm"
/// CIT TESTS
#include "character_saving.dm"
#ifdef REFERENCE_TRACKING //Don't try and parse this file if ref tracking isn't turned on. IE: don't parse ref tracking please mr linter
#include "find_reference_sanity.dm"
#endif
#undef TEST_ASSERT
#undef TEST_ASSERT_EQUAL
#undef TEST_ASSERT_NOTEQUAL
@@ -1,6 +1,6 @@
/datum/unit_test/chain_pull_through_space
var/turf/open/space/space_tile
var/turf/claimed_tile
var/claimed_tile
var/mob/living/carbon/human/alice
var/mob/living/carbon/human/bob
var/mob/living/carbon/human/charlie
@@ -9,25 +9,25 @@
..()
// Create a space tile that goes to another z-level
claimed_tile = run_loc_bottom_left
claimed_tile = run_loc_floor_bottom_left.type
space_tile = new(locate(run_loc_bottom_left.x, run_loc_bottom_left.y, run_loc_bottom_left.z))
space_tile = new(locate(run_loc_floor_bottom_left.x, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
space_tile.destination_x = 100
space_tile.destination_y = 100
space_tile.destination_z = 5
// Create our list of humans, all adjacent to one another
alice = new(locate(run_loc_bottom_left.x + 2, run_loc_bottom_left.y, run_loc_bottom_left.z))
alice = new(locate(run_loc_floor_bottom_left.x + 2, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
alice.name = "Alice"
bob = new(locate(run_loc_bottom_left.x + 3, run_loc_bottom_left.y, run_loc_bottom_left.z))
bob = new(locate(run_loc_floor_bottom_left.x + 3, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
bob.name = "Bob"
charlie = new(locate(run_loc_bottom_left.x + 4, run_loc_bottom_left.y, run_loc_bottom_left.z))
charlie = new(locate(run_loc_floor_bottom_left.x + 4, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
charlie.name = "Charlie"
/datum/unit_test/chain_pull_through_space/Destroy()
space_tile.copyTurf(claimed_tile)
space_tile.ChangeTurf(claimed_tile)
qdel(alice)
qdel(bob)
qdel(charlie)
@@ -40,14 +40,14 @@
bob.start_pulling(charlie)
// Walk normally to the left, make sure we're still a chain
alice.Move(locate(run_loc_bottom_left.x + 1, run_loc_bottom_left.y, run_loc_bottom_left.z))
if (bob.x != run_loc_bottom_left.x + 2)
alice.Move(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
if (bob.x != run_loc_floor_bottom_left.x + 2)
return Fail("During normal move, Bob was not at the correct x ([bob.x])")
if (charlie.x != run_loc_bottom_left.x + 3)
if (charlie.x != run_loc_floor_bottom_left.x + 3)
return Fail("During normal move, Charlie was not at the correct x ([charlie.x])")
// We're going through the space turf now that should teleport us
alice.Move(run_loc_bottom_left)
alice.Move(run_loc_floor_bottom_left)
if (alice.z != space_tile.destination_z)
return Fail("Alice did not teleport to the destination z-level. Current location: ([alice.x], [alice.y], [alice.z])")
@@ -0,0 +1,111 @@
///Used to test the completeness of the reference finder proc.
/datum/unit_test/find_reference_sanity
/atom/movable/ref_holder
var/atom/movable/ref_test/test
var/list/test_list = list()
var/list/test_assoc_list = list()
/atom/movable/ref_holder/Destroy()
test = null
test_list.Cut()
test_assoc_list.Cut()
return ..()
/atom/movable/ref_test
var/atom/movable/ref_test/self_ref
/atom/movable/ref_test/Destroy(force)
self_ref = null
return ..()
/datum/unit_test/find_reference_sanity/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Sanity check
victim.DoSearchVar(testbed, "Sanity Check", search_time = 1) //We increment search time to get around an optimization
TEST_ASSERT(!victim.found_refs.len, "The ref-tracking tool found a ref where none existed")
SSgarbage.should_save_refs = FALSE
/datum/unit_test/find_reference_baseline/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Set up for the first round of tests
testbed.test = victim
testbed.test_list += victim
testbed.test_assoc_list["baseline"] = victim
victim.DoSearchVar(testbed, "First Run", search_time = 2)
TEST_ASSERT(victim.found_refs["test"], "The ref-tracking tool failed to find a regular value")
TEST_ASSERT(victim.found_refs[testbed.test_list], "The ref-tracking tool failed to find a list entry")
TEST_ASSERT(victim.found_refs[testbed.test_assoc_list], "The ref-tracking tool failed to find an assoc list value")
SSgarbage.should_save_refs = FALSE
/datum/unit_test/find_reference_exotic/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Second round, bit harder this time
testbed.overlays += victim
testbed.vis_contents += victim
testbed.test_assoc_list[victim] = TRUE
victim.DoSearchVar(testbed, "Second Run", search_time = 3)
//This is another sanity check
TEST_ASSERT(!victim.found_refs[testbed.overlays], "The ref-tracking tool found an overlays entry? That shouldn't be possible")
TEST_ASSERT(victim.found_refs[testbed.vis_contents], "The ref-tracking tool failed to find a vis_contents entry")
TEST_ASSERT(victim.found_refs[testbed.test_assoc_list], "The ref-tracking tool failed to find an assoc list key")
SSgarbage.should_save_refs = FALSE
/datum/unit_test/find_reference_esoteric/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Let's get a bit esoteric
victim.self_ref = victim
var/list/to_find = list(victim)
testbed.test_list += list(to_find)
var/list/to_find_assoc = list(victim)
testbed.test_assoc_list["Nesting"] = to_find_assoc
victim.DoSearchVar(victim, "Third Run Self", search_time = 4)
victim.DoSearchVar(testbed, "Third Run Testbed", search_time = 4)
TEST_ASSERT(victim.found_refs["self_ref"], "The ref-tracking tool failed to find a self reference")
TEST_ASSERT(victim.found_refs[to_find], "The ref-tracking tool failed to find a nested list entry")
TEST_ASSERT(victim.found_refs[to_find_assoc], "The ref-tracking tool failed to find a nested assoc list entry")
SSgarbage.should_save_refs = FALSE
/datum/unit_test/find_reference_null_key_entry/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Calm before the storm
testbed.test_assoc_list = list(null = victim)
victim.DoSearchVar(testbed, "Fourth Run", search_time = 5)
TEST_ASSERT(testbed.test_assoc_list, "The ref-tracking tool failed to find a null key'd assoc list entry")
/datum/unit_test/find_reference_assoc_investigation/Run()
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
SSgarbage.should_save_refs = TRUE
//Let's do some more complex assoc list investigation
var/list/to_find_in_key = list(victim)
testbed.test_assoc_list[to_find_in_key] = list("memes")
var/list/to_find_null_assoc_nested = list(victim)
testbed.test_assoc_list[null] = to_find_null_assoc_nested
victim.DoSearchVar(testbed, "Fifth Run", search_time = 6)
TEST_ASSERT(victim.found_refs[to_find_in_key], "The ref-tracking tool failed to find a nested assoc list key")
TEST_ASSERT(victim.found_refs[to_find_null_assoc_nested], "The ref-tracking tool failed to find a null key'd nested assoc list entry")
SSgarbage.should_save_refs = FALSE
@@ -1,11 +0,0 @@
/datum/unit_test/initialize_sanity/Run()
if(length(SSatoms.BadInitializeCalls))
Fail("Bad Initialize() calls detected. Please read logs.")
var/list/init_failures_to_text = list(
"[BAD_INIT_QDEL_BEFORE]" = "Qdeleted Before Initialized",
"[BAD_INIT_DIDNT_INIT]" = "Did Not Initialize",
"[BAD_INIT_SLEPT]" = "Initialize() Slept",
"[BAD_INIT_NO_HINT]" = "No Initialize() Hint Returned",
)
for(var/failure in SSatoms.BadInitializeCalls)
log_world("[failure]: [init_failures_to_text["[SSatoms.BadInitializeCalls[failure]]"]]") // You like stacked brackets?
-23
View File
@@ -1,23 +0,0 @@
/// Test to verify message mods are parsed correctly
/datum/unit_test/get_message_mods
var/mob/host_mob
/datum/unit_test/get_message_mods/Run()
host_mob = allocate(/mob/living/carbon/human)
test("Hello", "Hello", list())
test(";HELP", "HELP", list(MODE_HEADSET = TRUE))
test(";%Never gonna give you up", "Never gonna give you up", list(MODE_HEADSET = TRUE, MODE_SING = TRUE))
test(".s Gun plz", "Gun plz", list(RADIO_KEY = RADIO_KEY_SECURITY, RADIO_EXTENSION = RADIO_CHANNEL_SECURITY))
test("...What", "...What", list())
/datum/unit_test/get_message_mods/proc/test(message, expected_message, list/expected_mods)
var/list/mods = list()
TEST_ASSERT_EQUAL(host_mob.get_message_mods(message, mods), expected_message, "Chopped message was not what we expected. Message: [message]")
for (var/mod_key in mods)
TEST_ASSERT_EQUAL(mods[mod_key], expected_mods[mod_key], "The value for [mod_key] was not what we expected. Message: [message]")
expected_mods -= mod_key
if (expected_mods.len)
Fail("Some message mods were expected, but were not returned by get_message_mods: [json_encode(expected_mods)]. Message: [message]")
+1 -1
View File
@@ -1,5 +1,5 @@
/datum/unit_test/spawn_humans/Run()
var/locs = block(run_loc_bottom_left, run_loc_top_right)
var/locs = block(run_loc_floor_bottom_left, run_loc_floor_top_right)
for(var/I in 1 to 5)
new /mob/living/carbon/human(pick(locs))
+8
View File
@@ -0,0 +1,8 @@
///Unit test that spawns all mobs that can be spawned by golden slimes
/datum/unit_test/spawn_mobs
/datum/unit_test/spawn_mobs/Run()
for(var/_animal in typesof(/mob/living/simple_animal))
var/mob/living/simple_animal/animal = _animal
if (initial(animal.gold_core_spawnable) == HOSTILE_SPAWN || initial(animal.gold_core_spawnable) == FRIENDLY_SPAWN)
allocate(_animal)
+2 -2
View File
@@ -1,8 +1,8 @@
/datum/unit_test/auto_teleporter_linking/Run()
// Put down the teleporter machinery
var/obj/machinery/teleport/hub/hub = allocate(/obj/machinery/teleport/hub)
var/obj/machinery/computer/teleporter/computer = allocate(/obj/machinery/computer/teleporter, locate(run_loc_bottom_left.x + 2, run_loc_bottom_left.y, run_loc_bottom_left.z))
var/obj/machinery/teleport/station/station = allocate(/obj/machinery/teleport/station, locate(run_loc_bottom_left.x + 1, run_loc_bottom_left.y, run_loc_bottom_left.z))
var/obj/machinery/computer/teleporter/computer = allocate(/obj/machinery/computer/teleporter, locate(run_loc_floor_bottom_left.x + 2, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
var/obj/machinery/teleport/station/station = allocate(/obj/machinery/teleport/station, locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
TEST_ASSERT_EQUAL(hub.power_station, station, "Hub didn't link to the station")
TEST_ASSERT_EQUAL(station.teleporter_console, computer, "Station didn't link to the teleporter console")
@@ -0,0 +1,28 @@
/// Test that `TGUI_CREATE_MESSAGE` is correctly implemented
/datum/unit_test/tgui_create_message
/datum/unit_test/tgui_create_message/Run()
var/type = "something/here"
var/list/payload = list(
"name" = "Terry McTider",
"heads_caved" = 100,
"accomplishments" = list(
"nothing",
"literally nothing",
list(
"something" = "just kidding",
),
),
)
var/message = TGUI_CREATE_MESSAGE(type, payload)
// Ensure consistent output to compare by performing a round-trip.
var/output = json_encode(json_decode(url_decode(message)))
var/expected = json_encode(list(
"type" = type,
"payload" = payload,
))
TEST_ASSERT_EQUAL(expected, output, "TGUI_CREATE_MESSAGE didn't round trip properly")
+68 -41
View File
@@ -7,7 +7,7 @@ Call Fail() to fail the test (You should specify a reason)
You may use /New() and /Destroy() for setup/teardown respectively
You can use the run_loc_bottom_left and run_loc_top_right to get turfs for testing
You can use the run_loc_floor_bottom_left and run_loc_floor_top_right to get turfs for testing
*/
@@ -19,39 +19,46 @@ GLOBAL_VAR(test_log)
//Bit of metadata for the future maybe
var/list/procs_tested
/// The bottom left turf of the testing zone
var/turf/run_loc_bottom_left
/// The top right turf of the testing zone
var/turf/run_loc_top_right
/// The type of turf to allocate for the testing zone
var/test_turf_type = /turf/open/floor/plasteel
/// The bottom left floor turf of the testing zone
var/turf/run_loc_floor_bottom_left
/// The top right floor turf of the testing zone
var/turf/run_loc_floor_top_right
///The priority of the test, the larger it is the later it fires
var/priority = TEST_DEFAULT
//internal shit
var/focus = FALSE
var/succeeded = TRUE
var/list/allocated
var/list/fail_reasons
var/static/datum/turf_reservation/turf_reservation
var/static/datum/turf_reservation/reservation
/proc/cmp_unit_test_priority(datum/unit_test/a, datum/unit_test/b)
return initial(a.priority) - initial(b.priority)
/datum/unit_test/New()
if (isnull(turf_reservation))
turf_reservation = SSmapping.RequestBlockReservation(5, 5)
if (isnull(reservation))
reservation = SSmapping.RequestBlockReservation(5, 5)
for (var/turf/reserved_turf in turf_reservation.reserved_turfs)
reserved_turf.ChangeTurf(test_turf_type)
for (var/turf/reserved_turf in reservation.reserved_turfs)
reserved_turf.ChangeTurf(/turf/open/floor/plasteel)
allocated = new
run_loc_bottom_left = locate(turf_reservation.bottom_left_coords[1], turf_reservation.bottom_left_coords[2], turf_reservation.bottom_left_coords[3])
run_loc_top_right = locate(turf_reservation.top_right_coords[1], turf_reservation.top_right_coords[2], turf_reservation.top_right_coords[3])
run_loc_floor_bottom_left = locate(reservation.bottom_left_coords[1], reservation.bottom_left_coords[2], reservation.bottom_left_coords[3])
run_loc_floor_top_right = locate(reservation.top_right_coords[1], reservation.top_right_coords[2], reservation.top_right_coords[3])
TEST_ASSERT(isfloorturf(run_loc_floor_bottom_left), "run_loc_floor_bottom_left was not a floor ([run_loc_floor_bottom_left])")
TEST_ASSERT(isfloorturf(run_loc_floor_top_right), "run_loc_floor_top_right was not a floor ([run_loc_floor_top_right])")
/datum/unit_test/Destroy()
//clear the test area
for(var/atom/movable/AM in block(run_loc_bottom_left, run_loc_top_right))
qdel(AM)
QDEL_LIST(allocated)
// clear the test area
for (var/turf/turf in block(locate(1, 1, run_loc_floor_bottom_left.z), locate(world.maxx, world.maxy, run_loc_floor_bottom_left.z)))
for (var/content in turf.contents)
if (istype(content, /obj/effect/landmark))
continue
qdel(content)
return ..()
/datum/unit_test/proc/Run()
@@ -70,44 +77,64 @@ GLOBAL_VAR(test_log)
/datum/unit_test/proc/allocate(type, ...)
var/list/arguments = args.Copy(2)
if (!arguments.len)
arguments = list(run_loc_bottom_left)
arguments = list(run_loc_floor_bottom_left)
else if (arguments[1] == null)
arguments[1] = run_loc_bottom_left
arguments[1] = run_loc_floor_bottom_left
var/instance = new type(arglist(arguments))
allocated += instance
return instance
/proc/RunUnitTest(test_path, list/test_results)
var/datum/unit_test/test = new test_path
GLOB.current_test = test
var/duration = REALTIMEOFDAY
test.Run()
duration = REALTIMEOFDAY - duration
GLOB.current_test = null
GLOB.failed_any_test |= !test.succeeded
var/list/log_entry = list("[test.succeeded ? "PASS" : "FAIL"]: [test_path] [duration / 10]s")
var/list/fail_reasons = test.fail_reasons
for(var/J in 1 to LAZYLEN(fail_reasons))
log_entry += "\tREASON #[J]: [fail_reasons[J]]"
var/message = log_entry.Join("\n")
log_test(message)
test_results[test_path] = list("status" = test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED, "message" = message, "name" = test_path)
qdel(test)
/proc/RunUnitTests()
CHECK_TICK
var/tests_to_run = subtypesof(/datum/unit_test)
var/list/tests_to_run = subtypesof(/datum/unit_test)
for (var/_test_to_run in tests_to_run)
var/datum/unit_test/test_to_run = _test_to_run
if (initial(test_to_run.focus))
tests_to_run = list(test_to_run)
break
for(var/I in tests_to_run)
var/datum/unit_test/test = new I
tests_to_run = sortTim(tests_to_run, /proc/cmp_unit_test_priority)
GLOB.current_test = test
var/duration = REALTIMEOFDAY
var/list/test_results = list()
test.Run()
for(var/unit_path in tests_to_run)
CHECK_TICK //We check tick first because the unit test we run last may be so expensive that checking tick will lock up this loop forever
RunUnitTest(unit_path, test_results)
duration = REALTIMEOFDAY - duration
GLOB.current_test = null
GLOB.failed_any_test |= !test.succeeded
var/list/log_entry = list("[test.succeeded ? "PASS" : "FAIL"]: [I] [duration / 10]s")
var/list/fail_reasons = test.fail_reasons
qdel(test)
for(var/J in 1 to LAZYLEN(fail_reasons))
log_entry += "\tREASON #[J]: [fail_reasons[J]]"
log_test(log_entry.Join("\n"))
CHECK_TICK
var/file_name = "data/unit_tests.json"
fdel(file_name)
file(file_name) << json_encode(test_results)
SSticker.force_ending = TRUE
//We have to call this manually because del_text can preceed us, and SSticker doesn't fire in the post game
SSticker.ready_for_reboot = TRUE
SSticker.standard_reboot()
// /datum/map_template/unit_tests
// name = "Unit Tests Zone"
// mappath = "_maps/templates/unit_tests.dmm"