#define RECOMMENDED_VERSION 513 /world/New() world_startup_time = world.timeofday rollover_safety_date = world.realtime - world.timeofday // 00:00 today (ish, since floating point error with world.realtime) of today to_world_log("Map Loading Complete") //logs //VOREStation Edit Start log_path += time2text(world.realtime, "YYYY/MM-Month/DD-Day/round-hh-mm-ss") diary = start_log("[log_path].log") href_logfile = start_log("[log_path]-hrefs.htm") error_log = start_log("[log_path]-error.log") sql_error_log = start_log("[log_path]-sql-error.log") query_debug_log = start_log("[log_path]-query-debug.log") debug_log = start_log("[log_path]-debug.log") //VOREStation Edit End //changelog_hash = md5('html/changelog.html') //used for telling if the changelog has changed recently //Chomp REMOVE //ChompADD Start - Better Changelogs var/latest_changelog = file("html/changelogs_ch/archive/" + time2text(world.timeofday, "YYYY-MM") + ".yml") changelog_hash = fexists(latest_changelog) ? md5(latest_changelog) : 0 //for telling if the changelog has changed recently //Newsfile var/savefile/F = new(NEWSFILE) if(F) var/title F["title"] >> title F["title"] >> title //This is done twice on purpose. For some reason BYOND misses the first read, if performed before the world starts var/body F["body"] >> body servernews_hash = md5("[title]" + "[body]") //ChompADD End if(byond_version < RECOMMENDED_VERSION) to_world_log("Your server's byond version does not meet the recommended requirements for this server. Please update BYOND") TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_TRUSTED) // CHOMPEdit - tgs event handler config.Load(params[OVERRIDE_CONFIG_DIRECTORY_PARAMETER]) ConfigLoaded() makeDatumRefLists() VgsNew() var servername = CONFIG_GET(string/servername) if(config && servername != null && CONFIG_GET(flag/server_suffix) && world.port > 0) // dumb and hardcoded but I don't care~ servername += " #[(world.port % 1000) / 100]" CONFIG_SET(string/servername, servername) // TODO - Figure out what this is. Can you assign to world.log? // if(config && CONFIG_FLAG(flag/log_runtime)) // log = file("data/logs/runtime/[time2text(world.realtime,"YYYY-MM-DD-(hh-mm-ss)")]-runtime.log") GLOB.timezoneOffset = world.timezone * 36000 callHook("startup") //Emergency Fix load_mods() //end-emergency fix src.update_status() setup_season() //VOREStation Addition var/debug_server = world.GetConfig("env", "AUXTOOLS_DEBUG_DLL") if (debug_server) call_ext(debug_server, "auxtools_init")() enable_debugging() . = ..() #ifdef UNIT_TEST log_unit_test("Unit Tests Enabled. This will destroy the world when testing is complete.") log_unit_test("If you did not intend to enable this please check code/__defines/unit_testing.dm") #endif // This is kinda important. Set up details of what the hell things are made of. populate_material_list() // Create frame types. populate_frame_types() // Create floor types. populate_flooring_types() // Create robolimbs for chargen. populate_robolimb_list() master_controller = new /datum/controller/game_controller() Master.Initialize(10, FALSE, TRUE) // VOREStation Edit spawn(1) master_controller.setup() #ifdef UNIT_TEST initialize_unit_tests() #endif spawn(3000) //so we aren't adding to the round-start lag if(CONFIG_GET(flag/ToRban)) ToRban_autoupdate() #undef RECOMMENDED_VERSION return /// Runs after config is loaded but before Master is initialized /world/proc/ConfigLoaded() // Everything in here is prioritized in a very specific way. // If you need to add to it, ask yourself hard if what your adding is in the right spot // (i.e. basically nothing should be added before load_admins() in here) // Try to set round ID SSdbcore.InitializeRound() //apply a default value to config.python_path, if needed if (!CONFIG_GET(string/python_path)) if(world.system_type == UNIX) CONFIG_SET(string/python_path, "/usr/bin/env python2") else //probably windows, if not this should work anyway CONFIG_SET(string/python_path, "python") var/world_topic_spam_protect_ip = "0.0.0.0" var/world_topic_spam_protect_time = world.timeofday /world/Topic(T, addr, master, key) VGS_TOPIC // VOREStation Edit - VGS //CHOMP Edit swapped lines around TGS_TOPIC //CHOMP Edit swapped lines around log_topic("\"[T]\", from:[addr], master:[master], key:[key]") if (T == "ping") var/x = 1 for (var/client/C) x++ return x else if(T == "players") var/n = 0 for(var/mob/M in player_list) if(M.client) n++ return n else if (copytext(T,1,7) == "status") var/input[] = params2list(T) var/list/s = list() s["version"] = game_version s["mode"] = master_mode s["respawn"] = CONFIG_GET(flag/abandon_allowed) s["persistance"] = CONFIG_GET(flag/persistence_disabled) s["enter"] = CONFIG_GET(flag/enter_allowed) s["vote"] = CONFIG_GET(flag/allow_vote_mode) s["ai"] = CONFIG_GET(flag/allow_ai) s["host"] = host ? host : null // This is dumb, but spacestation13.com's banners break if player count isn't the 8th field of the reply, so... this has to go here. s["players"] = 0 s["stationtime"] = stationtime2text() s["roundduration"] = roundduration2text() s["map"] = strip_improper(using_map.full_name) //Done to remove the non-UTF-8 text macros if(input["status"] == "2") // Shiny new hip status. var/active = 0 var/list/players = list() var/list/admins = list() for(var/client/C in GLOB.clients) if(C.holder) if(C.holder.fakekey) continue admins[C.key] = C.holder.rank players += C.key if(isliving(C.mob)) active++ s["players"] = players.len s["playerlist"] = list2params(players) s["active_players"] = active var/list/adm = get_admin_counts() var/list/presentmins = adm["present"] var/list/afkmins = adm["afk"] s["admins"] = presentmins.len + afkmins.len //equivalent to the info gotten from adminwho s["adminlist"] = list2params(admins) else // Legacy. var/n = 0 var/admins = 0 for(var/client/C in GLOB.clients) if(C.holder) if(C.holder.fakekey) continue //so stealthmins aren't revealed by the hub admins++ s["player[n]"] = C.key n++ s["players"] = n s["admins"] = admins return list2params(s) else if(T == "manifest") var/list/positions = list() var/list/set_names = list( "heads" = SSjob.get_job_titles_in_department(DEPARTMENT_COMMAND), "sec" = SSjob.get_job_titles_in_department(DEPARTMENT_SECURITY), "eng" = SSjob.get_job_titles_in_department(DEPARTMENT_ENGINEERING), "med" = SSjob.get_job_titles_in_department(DEPARTMENT_MEDICAL), "sci" = SSjob.get_job_titles_in_department(DEPARTMENT_RESEARCH), "car" = SSjob.get_job_titles_in_department(DEPARTMENT_CARGO), "pla" = SSjob.get_job_titles_in_department(DEPARTMENT_PLANET), //VOREStation Add, "civ" = SSjob.get_job_titles_in_department(DEPARTMENT_CIVILIAN), "bot" = SSjob.get_job_titles_in_department(DEPARTMENT_SYNTHETIC) ) for(var/datum/data/record/t in data_core.general) var/name = t.fields["name"] var/rank = t.fields["rank"] var/real_rank = make_list_rank(t.fields["real_rank"]) var/department = 0 var/active = 0 //CHOMPStation Edit Begin for(var/mob/M in player_list) if(M.real_name == name && M.client && M.client.inactivity <= 10 MINUTES) active = 1 break var/isactive = active ? "Active" : "Inactive" for(var/k in set_names) if(real_rank in set_names[k]) if(!positions[k]) positions[k] = list() positions[k][name] = list(rank,isactive) department = 1 if(!department) if(!positions["misc"]) positions["misc"] = list() positions["misc"][name] = list(rank,isactive) for(var/datum/data/record/t in data_core.hidden_general) var/name = t.fields["name"] var/rank = t.fields["rank"] var/real_rank = make_list_rank(t.fields["real_rank"]) var/active = 0 //CHOMPStation Edit Begin for(var/mob/M in player_list) if(M.real_name == name && M.client && M.client.inactivity <= 10 MINUTES) active = 1 break var/isactive = active ? "Active" : "Inactive" var/datum/job/J = SSjob.get_job(real_rank) if(J?.offmap_spawn) if(!positions["off"]) positions["off"] = list() positions["off"][name] = list(rank,isactive) // Synthetics don't have actual records, so we will pull them from here. for(var/mob/living/silicon/ai/ai in mob_list) var/isactive = (ai.client && ai.client.inactivity <= 10 MINUTES) ? "Active" : "Inactive" if(!positions["bot"]) positions["bot"] = list() positions["bot"][ai.name] = list("Artificial Intelligence",isactive) for(var/mob/living/silicon/robot/robot in mob_list) // No combat/syndicate cyborgs, no drones, and no AI shells. var/isactive = (robot.client && robot.client.inactivity <= 10 MINUTES) ? "Active" : "Inactive" if(robot.shell) continue if(robot.module && robot.module.hide_on_manifest()) continue if(!positions["bot"]) positions["bot"] = list() positions["bot"][robot.name] = list("[robot.modtype] [robot.braintype]",isactive) //CHOMPEdit end for(var/k in positions) positions[k] = list2params(positions[k]) // converts positions["heads"] = list("Bob"="Captain", "Bill"="CMO") into positions["heads"] = "Bob=Captain&Bill=CMO" return list2params(positions) else if(T == "revision") if(GLOB.revdata.revision) return list2params(list(branch = GLOB.revdata.branch, date = GLOB.revdata.date, revision = GLOB.revdata.revision)) else return "unknown" else if(copytext(T,1,5) == "info") var/input[] = params2list(T) if(input["key"] != CONFIG_GET(string/comms_password)) if(world_topic_spam_protect_ip == addr && abs(world_topic_spam_protect_time - world.time) < 50) spawn(50) world_topic_spam_protect_time = world.time return world_topic_spam_protect_time = world.time world_topic_spam_protect_ip = addr return "Bad Key" var/list/search = params2list(input["info"]) var/list/ckeysearch = list() for(var/text in search) ckeysearch += ckey(text) var/list/match = list() for(var/mob/M in mob_list) var/strings = list(M.name, M.ckey) if(M.mind) strings += M.mind.assigned_role strings += M.mind.special_role for(var/text in strings) if(ckey(text) in ckeysearch) match[M] += 10 // an exact match is far better than a partial one else for(var/searchstr in search) if(findtext(text, searchstr)) match[M] += 1 var/maxstrength = 0 for(var/mob/M in match) maxstrength = max(match[M], maxstrength) for(var/mob/M in match) if(match[M] < maxstrength) match -= M if(!match.len) return "No matches" else if(match.len == 1) var/mob/M = match[1] var/info = list() info["key"] = M.key info["name"] = M.name == M.real_name ? M.name : "[M.name] ([M.real_name])" info["role"] = M.mind ? (M.mind.assigned_role ? M.mind.assigned_role : "No role") : "No mind" var/turf/MT = get_turf(M) info["loc"] = M.loc ? "[M.loc]" : "null" info["turf"] = MT ? "[MT] @ [MT.x], [MT.y], [MT.z]" : "null" info["area"] = MT ? "[MT.loc]" : "null" info["antag"] = M.mind ? (M.mind.special_role ? M.mind.special_role : "Not antag") : "No mind" info["hasbeenrev"] = M.mind ? M.mind.has_been_rev : "No mind" info["stat"] = M.stat info["type"] = M.type if(isliving(M)) var/mob/living/L = M info["damage"] = list2params(list( oxy = L.getOxyLoss(), tox = L.getToxLoss(), fire = L.getFireLoss(), brute = L.getBruteLoss(), clone = L.getCloneLoss(), brain = L.getBrainLoss() )) else info["damage"] = "non-living" info["gender"] = M.gender return list2params(info) else var/list/ret = list() for(var/mob/M in match) ret[M.key] = M.name return list2params(ret) else if(copytext(T,1,9) == "adminmsg") /* We got an adminmsg from IRC bot lets split the input then validate the input. expected output: 1. adminmsg = ckey of person the message is to 2. msg = contents of message, parems2list requires 3. validatationkey = the key the bot has, it should match the gameservers commspassword in it's configuration. 4. sender = the ircnick that send the message. */ var/input[] = params2list(T) if(input["key"] != CONFIG_GET(string/comms_password)) if(world_topic_spam_protect_ip == addr && abs(world_topic_spam_protect_time - world.time) < 50) spawn(50) world_topic_spam_protect_time = world.time return world_topic_spam_protect_time = world.time world_topic_spam_protect_ip = addr return "Bad Key" var/client/C var/req_ckey = ckey(input["adminmsg"]) for(var/client/K in GLOB.clients) if(K.ckey == req_ckey) C = K break if(!C) return "No client with that name on server" var/rank = input["rank"] if(!rank) rank = "Admin" var/message = span_red("IRC-[rank] PM from IRC-[input["sender"]]: [input["msg"]]") var/amessage = span_blue("IRC-[rank] PM from IRC-[input["sender"]] to [key_name(C)] : [input["msg"]]") C.received_irc_pm = world.time C.irc_admin = input["sender"] C << 'sound/effects/adminhelp.ogg' to_chat(C,message) for(var/client/A in GLOB.admins) if(A != C) to_chat(A,amessage) return "Message Successful" else if(copytext(T,1,6) == "notes") /* We got a request for notes from the IRC Bot expected output: 1. notes = ckey of person the notes lookup is for 2. validationkey = the key the bot has, it should match the gameservers commspassword in it's configuration. */ var/input[] = params2list(T) if(input["key"] != CONFIG_GET(string/comms_password)) if(world_topic_spam_protect_ip == addr && abs(world_topic_spam_protect_time - world.time) < 50) spawn(50) world_topic_spam_protect_time = world.time return world_topic_spam_protect_time = world.time world_topic_spam_protect_ip = addr return "Bad Key" return show_player_info_irc(ckey(input["notes"])) else if(copytext(T,1,4) == "age") var/input[] = params2list(T) if(input["key"] != CONFIG_GET(string/comms_password)) if(world_topic_spam_protect_ip == addr && abs(world_topic_spam_protect_time - world.time) < 50) spawn(50) world_topic_spam_protect_time = world.time return world_topic_spam_protect_time = world.time world_topic_spam_protect_ip = addr return "Bad Key" var/age = get_player_age(input["age"]) if(isnum(age)) if(age >= 0) return "[age]" else return "Ckey not found" else return "Database connection failed or not set up" /world/Reboot(reason = 0, fast_track = FALSE) /*spawn(0) world << sound(pick('sound/AI/newroundsexy.ogg','sound/misc/apcdestroyed.ogg','sound/misc/bangindonk.ogg')) // random end sounds!! - LastyBatsy */ if (reason || fast_track) //special reboot, do none of the normal stuff if (usr) log_admin("[key_name(usr)] Has requested an immediate world restart via client side debugging tools") message_admins("[key_name_admin(usr)] Has requested an immediate world restart via client side debugging tools") to_world(span_boldannounce("[key_name_admin(usr)] has requested an immediate world restart via client side debugging tools")) else to_world(span_boldannounce("Rebooting world immediately due to host request")) else Master.Shutdown() //run SS shutdowns for(var/client/C in GLOB.clients) if(CONFIG_GET(string/server)) //if you set a server location in config.txt, it sends you there instead of trying to reconnect to the same world address. -- NeoFite C << link("byond://[CONFIG_GET(string/server)]") TgsReboot() log_world("World rebooted at [time_stamp()]") ..() /hook/startup/proc/loadMode() world.load_mode() return 1 /world/proc/load_mode() if(!fexists("data/mode.txt")) return var/list/Lines = file2list("data/mode.txt") if(Lines.len) if(Lines[1]) master_mode = Lines[1] log_misc("Saved mode is '[master_mode]'") /world/proc/save_mode(var/the_mode) var/F = file("data/mode.txt") fdel(F) F << the_mode /hook/startup/proc/loadMOTD() world.load_motd() return 1 /world/proc/load_motd() join_motd = file2text("config/motd.txt") /* Replaced with configuration controller /proc/load_configuration() config = new /datum/configuration() config.load("config/config.txt") config.load("config/game_options.txt","game_options") config.loadsql("config/dbconfig.txt") config.loadforumsql("config/forumdbconfig.txt") */ /hook/startup/proc/loadMods() world.load_mods() world.load_mentors() // no need to write another hook. return 1 /world/proc/load_mods() if(CONFIG_GET(flag/admin_legacy_system)) var/text = file2text("config/moderators.txt") if (!text) error("Failed to load config/mods.txt") else var/list/lines = splittext(text, "\n") for(var/line in lines) if (!line) continue if (copytext(line, 1, 2) == ";") continue var/title = "Moderator" var/rights = admin_ranks[title] var/ckey = copytext(line, 1, length(line)+1) var/datum/admins/D = new /datum/admins(title, rights, ckey) D.associate(GLOB.directory[ckey]) /world/proc/load_mentors() if(CONFIG_GET(flag/admin_legacy_system)) var/text = file2text("config/mentors.txt") if (!text) error("Failed to load config/mentors.txt") else var/list/lines = splittext(text, "\n") for(var/line in lines) if (!line) continue if (copytext(line, 1, 2) == ";") continue var/ckey = copytext(line, 1, length(line)+1) var/datum/mentor/M = new /datum/mentor(ckey) M.associate(GLOB.directory[ckey]) else establish_db_connection() if(!SSdbcore.IsConnected()) error("Failed to connect to database in load_mentors().") log_misc("Failed to connect to database in load_mentors().") return var/datum/db_query/query = SSdbcore.NewQuery("SELECT ckey, mentor FROM erro_mentor") query.Execute() while(query.NextRow()) var/ckey = query.item[1] var/mentor = query.item[2] if(mentor) var/datum/mentor/M = new /datum/mentor(ckey) M.associate(GLOB.directory[ckey]) qdel(query) /world/proc/update_status() var/s = "" if (config && CONFIG_GET(string/servername)) s += span_bold("[CONFIG_GET(string/servername)]") + " — " s += span_bold("[station_name()]"); s += " (" s += "" //Change this to wherever you want the hub to link to. // s += "[game_version]" s += "Default" //Replace this with something else. Or ever better, delete it and uncomment the game version. s += "" s += ")" var/list/features = list() if(ticker) if(master_mode) features += master_mode else features += span_bold("STARTING") if (!CONFIG_GET(flag/enter_allowed)) features += "closed" features += CONFIG_GET(flag/abandon_allowed) ? "respawn" : "no respawn" features += CONFIG_GET(flag/persistence_disabled) ? "persistence disabled" : "persistence enabled" features += CONFIG_GET(flag/persistence_ignore_mapload) ? "persistence mapload disabled" : "persistence mapload enabled" if (config && CONFIG_GET(flag/allow_vote_mode)) features += "vote" if (config && CONFIG_GET(flag/allow_ai)) features += "AI allowed" var/n = 0 for (var/mob/M in player_list) if (M.client) n++ if (n > 1) features += "~[n] players" else if (n > 0) features += "~[n] player" if (config && CONFIG_GET(string/hostedby)) features += "hosted by [CONFIG_GET(string/hostedby)]" if (features) s += ": [jointext(features, ", ")]" /* does this help? I do not know */ if (src.status != s) src.status = s #define FAILED_DB_CONNECTION_CUTOFF 5 var/failed_db_connections = 0 var/failed_old_db_connections = 0 /hook/startup/proc/connectDB() if(!CONFIG_GET(flag/sql_enabled)) to_world_log("SQL connection disabled in config.") else if(!setup_database_connection()) to_world_log("Your server failed to establish a connection with the feedback database.") else to_world_log("Feedback database connection established.") // CHOMPEdit Begin - Truncating the temporary dialog/attacklog tables var/datum/db_query/query_truncate = SSdbcore.NewQuery("TRUNCATE erro_dialog") var/num_tries = 0 while(!query_truncate.Execute() && num_tries<5) num_tries++ if(num_tries==5) log_admin("ERROR TRYING TO CLEAR erro_dialog") qdel(query_truncate) var/datum/db_query/query_truncate2 = SSdbcore.NewQuery("TRUNCATE erro_attacklog") num_tries = 0 while(!query_truncate2.Execute() && num_tries<5) num_tries++ if(num_tries==5) log_admin("ERROR TRYING TO CLEAR erro_attacklog") qdel(query_truncate2) // CHOMPEdit End return 1 /proc/setup_database_connection() if(!CONFIG_GET(flag/sql_enabled)) return 0 if(failed_db_connections > FAILED_DB_CONNECTION_CUTOFF) //If it failed to establish a connection more than 5 times in a row, don't bother attempting to conenct anymore. return 0 if(!SSdbcore) SSdbcore = new() var/user = CONFIG_GET(string/feedback_login) var/pass = CONFIG_GET(string/feedback_password) var/db = CONFIG_GET(string/feedback_database) var/address = CONFIG_GET(string/address) var/port = CONFIG_GET(number/port) SSdbcore.Connect("dbi:mysql:[db]:[address]:[port]","[user]","[pass]") . = SSdbcore.IsConnected() if ( . ) failed_db_connections = 0 //If this connection succeeded, reset the failed connections counter. else failed_db_connections++ //If it failed, increase the failed connections counter. to_world_log(SSdbcore.ErrorMsg()) return . //This proc ensures that the connection to the feedback database (global variable dbcon) is established /proc/establish_db_connection() if(failed_db_connections > FAILED_DB_CONNECTION_CUTOFF) return 0 if(!SSdbcore || !SSdbcore.IsConnected()) return setup_database_connection() else return 1 // Cleans up DB connections and recreates them /proc/reset_database_connections() var/list/results = list("-- Resetting DB connections --") failed_db_connections = 0 if(SSdbcore?.IsConnected()) SSdbcore.Disconnect() results += "SSdbcore was connected and asked to disconnect" else results += "SSdbcore was not connected" if(!CONFIG_GET(flag/sql_enabled)) results += "stopping because config.sql_enabled = false" else . = setup_database_connection() if(.) results += "SUCCESS: set up a connection successfully with setup_database_connection()" else results += "FAIL: failed to connect to the database with setup_database_connection()" results += "-- DB Reset End --" to_world_log(results.Join("\n")) // Things to do when a new z-level was just made. /world/proc/max_z_changed() if(!istype(GLOB.players_by_zlevel, /list)) GLOB.players_by_zlevel = new /list(world.maxz, 0) GLOB.living_players_by_zlevel = new /list(world.maxz, 0) while(GLOB.players_by_zlevel.len < world.maxz) GLOB.players_by_zlevel.len++ GLOB.players_by_zlevel[GLOB.players_by_zlevel.len] = list() GLOB.living_players_by_zlevel.len++ GLOB.living_players_by_zlevel[GLOB.living_players_by_zlevel.len] = list() // Call this to make a new blank z-level, don't modify maxz directly. /world/proc/increment_max_z() maxz++ max_z_changed() // Call this to change world.fps, don't modify it directly. /world/proc/change_fps(new_value = 20) if(new_value <= 0) CRASH("change_fps() called with [new_value] new_value.") if(fps == new_value) return //No change required. fps = new_value on_tickrate_change() // Called whenver world.tick_lag or world.fps are changed. /world/proc/on_tickrate_change() SStimer?.reset_buckets() #undef FAILED_DB_CONNECTION_CUTOFF /proc/get_world_url() . = "byond://" if(CONFIG_GET(string/serverurl)) . += CONFIG_GET(string/serverurl) else if(CONFIG_GET(string/server)) . += CONFIG_GET(string/server) else . += "[world.address]:[world.port]" var/global/game_id = null /hook/startup/proc/generate_gameid() if(game_id != null) return game_id = "" var/list/c = list( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" ) var/l = c.len var/t = world.timeofday for(var/_ = 1 to 4) game_id = "[c[(t % l) + 1]][game_id]" t = round(t / l) game_id = "-[game_id]" t = round(world.realtime / (10 * 60 * 60 * 24)) for(var/_ = 1 to 3) game_id = "[c[(t % l) + 1]][game_id]" t = round(t / l) return 1 /proc/auxtools_stack_trace(msg) CRASH(msg) /proc/auxtools_expr_stub() CRASH("auxtools not loaded") /proc/enable_debugging(mode, port) CRASH("auxtools not loaded") /world/Del() var/debug_server = world.GetConfig("env", "AUXTOOLS_DEBUG_DLL") if (debug_server) call_ext(debug_server, "auxtools_shutdown")() . = ..()