diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4dcbd75275..e73367bf7b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -64,6 +64,7 @@ jobs:
unit_tests:
name: Integration Tests
+ # needs: ['file_tests', 'dreamchecker']
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
@@ -105,3 +106,12 @@ jobs:
TEST_DEFINE: "AWAY_MISSION_TEST"
TEST_FILE: "code/_away_mission_tests.dm"
RUN: "0"
+
+ tests_successful:
+ name: Integration Tests
+ needs: ['file_tests', 'dreamchecker', 'unit_tests']
+ runs-on: ubuntu-20.04
+ steps:
+ - name: Report Success
+ run: |
+ echo "Jobs Successful!"
diff --git a/_build_dependencies.sh b/_build_dependencies.sh
index eba3501bf3..b251bfbe32 100644
--- a/_build_dependencies.sh
+++ b/_build_dependencies.sh
@@ -6,6 +6,6 @@ export NODE_VERSION=16
# Byond Major
export BYOND_MAJOR=514
# Byond Minor
-export BYOND_MINOR=1571
+export BYOND_MINOR=1589
# Macro Count
export MACRO_COUNT=4
diff --git a/code/ZAS/Atom.dm b/code/ZAS/Atom.dm
index 0057d18ca0..bfe6fa2ad4 100644
--- a/code/ZAS/Atom.dm
+++ b/code/ZAS/Atom.dm
@@ -3,14 +3,6 @@
/atom/var/pressure_resistance = ONE_ATMOSPHERE
/atom/var/can_atmos_pass = ATMOS_PASS_YES
-// Purpose: Determines if the object can pass this atom.
-// Called by: Movement.
-// Inputs: The moving atom, target turf.
-// Outputs: Boolean if can pass.
-// Airflow and ZAS zones now uses CanZASPass() instead of this proc.
-/atom/proc/CanPass(atom/movable/mover, turf/target)
- return !density
-
// Purpose: Determines if airflow is allowed between T and loc.
// Called by: Airflow.
// Inputs: The turf the airflow is from, which may not be the same as loc. is_zone is for conditionally disallowing merging.
diff --git a/code/ZAS/Fire.dm b/code/ZAS/Fire.dm
index 73ce3f01e8..015ffa36a9 100644
--- a/code/ZAS/Fire.dm
+++ b/code/ZAS/Fire.dm
@@ -170,7 +170,7 @@ If it gains pressure too slowly, it may leak or just rupture instead of explodin
continue
//Spread the fire.
- if(prob( 50 + 50 * (firelevel/vsc.fire_firelevel_multiplier) ) && my_tile.CanPass(src, enemy_tile, 0,0) && enemy_tile.CanPass(src, my_tile, 0,0))
+ if(prob( 50 + 50 * (firelevel/vsc.fire_firelevel_multiplier) ) && my_tile.CanPass(src, enemy_tile) && enemy_tile.CanPass(src, my_tile))
enemy_tile.create_fire(firelevel)
else
diff --git a/code/ZAS/Variable Settings.dm b/code/ZAS/Variable Settings.dm
index c4643632dc..d55bcc64de 100644
--- a/code/ZAS/Variable Settings.dm
+++ b/code/ZAS/Variable Settings.dm
@@ -110,7 +110,7 @@ var/global/vs_control/vsc = new
vw = vars[ch]
if("[ch]_DESC" in vars) vw_desc = vars["[ch]_DESC"]
if("[ch]_NAME" in vars) vw_name = vars["[ch]_NAME"]
- dat += "[vw_name] = [vw] \[Change\]
"
+ dat += "[vw_name] = [vw] \[Change\]
"
dat += "[vw_desc]
"
user << browse(dat,"window=settings")
diff --git a/code/__defines/admin.dm b/code/__defines/admin.dm
index 84e0656c97..0e8730bae8 100644
--- a/code/__defines/admin.dm
+++ b/code/__defines/admin.dm
@@ -46,26 +46,26 @@
#define SMITE_SPONTANEOUSCOMBUSTION "Spontaneous Combustion"
#define SMITE_LIGHTNINGBOLT "Lightning Bolt"
-#define ADMIN_QUE(user) "(?)"
-#define ADMIN_FLW(user) "(FLW)"
-#define ADMIN_PP(user) "(PP)"
-#define ADMIN_VV(atom) "(VV)"
-#define ADMIN_SM(user) "(SM)"
-#define ADMIN_TP(user) "(TP)"
-#define ADMIN_BSA(user) "(BSA)"
-#define ADMIN_KICK(user) "(KICK)"
-#define ADMIN_CENTCOM_REPLY(user) "(RPLY)"
-#define ADMIN_SYNDICATE_REPLY(user) "(RPLY)"
-#define ADMIN_SC(user) "(SC)"
-#define ADMIN_SMITE(user) "(SMITE)"
+#define ADMIN_QUE(user) "(?)"
+#define ADMIN_FLW(user) "(FLW)"
+#define ADMIN_PP(user) "(PP)"
+#define ADMIN_VV(atom) "(VV)"
+#define ADMIN_SM(user) "(SM)"
+#define ADMIN_TP(user) "(TP)"
+#define ADMIN_BSA(user) "(BSA)"
+#define ADMIN_KICK(user) "(KICK)"
+#define ADMIN_CENTCOM_REPLY(user) "(RPLY)"
+#define ADMIN_SYNDICATE_REPLY(user) "(RPLY)"
+#define ADMIN_SC(user) "(SC)"
+#define ADMIN_SMITE(user) "(SMITE)"
#define ADMIN_LOOKUP(user) "[key_name_admin(user)][ADMIN_QUE(user)]"
#define ADMIN_LOOKUPFLW(user) "[key_name_admin(user)][ADMIN_QUE(user)] [ADMIN_FLW(user)]"
#define ADMIN_FULLMONTY_NONAME(user) "[ADMIN_QUE(user)] [ADMIN_PP(user)] [ADMIN_VV(user)] [ADMIN_SM(user)] [ADMIN_FLW(user)] [ADMIN_TP(user)]"
#define ADMIN_FULLMONTY(user) "[key_name_admin(user)] [ADMIN_FULLMONTY_NONAME(user)]"
-#define ADMIN_JMP(src) "(JMP)"
+#define ADMIN_JMP(src) "(JMP)"
#define COORD(src) "[src ? "([src.x],[src.y],[src.z])" : "nonexistent location"]"
#define ADMIN_COORDJMP(src) "[src ? "[COORD(src)] [ADMIN_JMP(src)]" : "nonexistent location"]"
#define AHELP_ACTIVE 1
#define AHELP_CLOSED 2
-#define AHELP_RESOLVED 3
\ No newline at end of file
+#define AHELP_RESOLVED 3
diff --git a/code/__defines/procpath.dm b/code/__defines/procpath.dm
new file mode 100644
index 0000000000..e58a2a48b2
--- /dev/null
+++ b/code/__defines/procpath.dm
@@ -0,0 +1,25 @@
+/// Represents a proc or verb path.
+///
+/// Despite having no DM-defined static type, proc paths have some variables,
+/// listed below. These are not modifiable, but for a given procpath P,
+/// `new P(null, "Name", "Desc")` can be used to create a new procpath with the
+/// same code but new `name` and `desc` values. The other variables cannot be
+/// changed in this way.
+///
+/// This type exists only to act as an annotation, providing reasonable static
+/// typing for procpaths. Previously, types like `/atom/verb` were used, with
+/// the `name` and `desc` vars of `/atom` thus being accessible. Proc and verb
+/// paths will fail `istype` and `ispath` checks against `/procpath`.
+/procpath
+ // Although these variables are effectively const, if they are marked const
+ // below, their accesses are optimized away.
+ /// A text string of the verb's name.
+ var/name as text
+ /// The verb's help text or description.
+ var/desc as text
+ /// The category or tab the verb will appear in.
+ var/category as text
+ /// Only clients/mobs with `see_invisibility` higher can use the verb.
+ var/invisibility as num
+ /// Whether or not the verb appears in statpanel and commandbar when you press space
+ var/hidden as num
diff --git a/code/_helpers/files.dm b/code/_helpers/files.dm
index d6603e4275..9468becf8b 100644
--- a/code/_helpers/files.dm
+++ b/code/_helpers/files.dm
@@ -58,3 +58,18 @@
fileaccess_timer = world.time + FTPDELAY
return 0
#undef FTPDELAY
+
+/// Returns the md5 of a file at a given path.
+/proc/md5filepath(path)
+ . = md5(file(path))
+
+/// Save file as an external file then md5 it.
+/// Used because md5ing files stored in the rsc sometimes gives incorrect md5 results.
+/proc/md5asfile(file)
+ var/static/notch = 0
+ // its importaint this code can handle md5filepath sleeping instead of hard blocking, if it's converted to use rust_g.
+ var/filename = "tmp/md5asfile.[world.realtime].[world.timeofday].[world.time].[world.tick_usage].[notch]"
+ notch = WRAP(notch+1, 0, 2**15)
+ fcopy(file, filename)
+ . = md5filepath(filename)
+ fdel(filename)
diff --git a/code/_helpers/icons.dm b/code/_helpers/icons.dm
index 24f398c8ec..2cb3ef5c69 100644
--- a/code/_helpers/icons.dm
+++ b/code/_helpers/icons.dm
@@ -502,3 +502,184 @@ GLOBAL_LIST_EMPTY(cached_examine_icons)
//Animate it growing
animate(img, alpha = 0, transform = matrix()*grow_to, time = anim_duration, loop = loops)
+
+/// generates a filename for a given asset.
+/// like generate_asset_name(), except returns the rsc reference and the rsc file hash as well as the asset name (sans extension)
+/// used so that certain asset files dont have to be hashed twice
+/proc/generate_and_hash_rsc_file(file, dmi_file_path)
+ var/rsc_ref = fcopy_rsc(file)
+ var/hash
+ //if we have a valid dmi file path we can trust md5'ing the rsc file because we know it doesnt have the bug described in http://www.byond.com/forum/post/2611357
+ if(dmi_file_path)
+ hash = md5(rsc_ref)
+ else //otherwise, we need to do the expensive fcopy() workaround
+ hash = md5asfile(rsc_ref)
+
+ return list(rsc_ref, hash, "asset.[hash]")
+
+///given a text string, returns whether it is a valid dmi icons folder path
+/proc/is_valid_dmi_file(icon_path)
+ if(!istext(icon_path) || !length(icon_path))
+ return FALSE
+
+ var/is_in_icon_folder = findtextEx(icon_path, "icons/")
+ var/is_dmi_file = findtextEx(icon_path, ".dmi")
+
+ if(is_in_icon_folder && is_dmi_file)
+ return TRUE
+ return FALSE
+
+/// given an icon object, dmi file path, or atom/image/mutable_appearance, attempts to find and return an associated dmi file path.
+/// a weird quirk about dm is that /icon objects represent both compile-time or dynamic icons in the rsc,
+/// but stringifying rsc references returns a dmi file path
+/// ONLY if that icon represents a completely unchanged dmi file from when the game was compiled.
+/// so if the given object is associated with an icon that was in the rsc when the game was compiled, this returns a path. otherwise it returns ""
+/proc/get_icon_dmi_path(icon/icon)
+ /// the dmi file path we attempt to return if the given object argument is associated with a stringifiable icon
+ /// if successful, this looks like "icons/path/to/dmi_file.dmi"
+ var/icon_path = ""
+
+ if(isatom(icon) || istype(icon, /image) || istype(icon, /mutable_appearance))
+ var/atom/atom_icon = icon
+ icon = atom_icon.icon
+ //atom icons compiled in from 'icons/path/to/dmi_file.dmi' are weird and not really icon objects that you generate with icon().
+ //if theyre unchanged dmi's then they're stringifiable to "icons/path/to/dmi_file.dmi"
+
+ if(isicon(icon) && isfile(icon))
+ //icons compiled in from 'icons/path/to/dmi_file.dmi' at compile time are weird and arent really /icon objects,
+ ///but they pass both isicon() and isfile() checks. theyre the easiest case since stringifying them gives us the path we want
+ var/icon_ref = "\ref[icon]"
+ var/locate_icon_string = "[locate(icon_ref)]"
+
+ icon_path = locate_icon_string
+
+ else if(isicon(icon) && "[icon]" == "/icon")
+ // icon objects generated from icon() at runtime are icons, but they ARENT files themselves, they represent icon files.
+ // if the files they represent are compile time dmi files in the rsc, then
+ // the rsc reference returned by fcopy_rsc() will be stringifiable to "icons/path/to/dmi_file.dmi"
+ var/rsc_ref = fcopy_rsc(icon)
+
+ var/icon_ref = "\ref[rsc_ref]"
+
+ var/icon_path_string = "[locate(icon_ref)]"
+
+ icon_path = icon_path_string
+
+ else if(istext(icon))
+ var/rsc_ref = fcopy_rsc(icon)
+ //if its the text path of an existing dmi file, the rsc reference returned by fcopy_rsc() will be stringifiable to a dmi path
+
+ var/rsc_ref_ref = "\ref[rsc_ref]"
+ var/rsc_ref_string = "[locate(rsc_ref_ref)]"
+
+ icon_path = rsc_ref_string
+
+ if(is_valid_dmi_file(icon_path))
+ return icon_path
+
+ return FALSE
+
+/**
+ * generate an asset for the given icon or the icon of the given appearance for [thing], and send it to any clients in target.
+ * Arguments:
+ * * thing - either a /icon object, or an object that has an appearance (atom, image, mutable_appearance).
+ * * target - either a reference to or a list of references to /client's or mobs with clients
+ * * icon_state - string to force a particular icon_state for the icon to be used
+ * * dir - dir number to force a particular direction for the icon to be used
+ * * frame - what frame of the icon_state's animation for the icon being used
+ * * moving - whether or not to use a moving state for the given icon
+ * * sourceonly - if TRUE, only generate the asset and send back the asset url, instead of tags that display the icon to players
+ * * extra_clases - string of extra css classes to use when returning the icon string
+ */
+/proc/icon2html(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null)
+ if (!thing)
+ return
+ //if(SSlag_switch.measures[DISABLE_USR_ICON2HTML] && usr && !HAS_TRAIT(usr, TRAIT_BYPASS_MEASURES))
+ //return
+
+ var/key
+ var/icon/icon2collapse = thing
+
+ if (!target)
+ return
+ if (target == world)
+ target = GLOB.clients
+
+ var/list/targets
+ if (!islist(target))
+ targets = list(target)
+ else
+ targets = target
+ if(!length(targets))
+ return
+
+ //check if the given object is associated with a dmi file in the icons folder. if it is then we dont need to do a lot of work
+ //for asset generation to get around byond limitations
+ var/icon_path = get_icon_dmi_path(thing)
+
+ if (!isicon(icon2collapse))
+ if (isfile(thing)) //special snowflake
+ //var/name = SANITIZE_FILENAME("[generate_asset_name(thing)].png")
+ var/name = "[generate_asset_name(thing)].png"
+ if (!SSassets.cache[name])
+ register_asset(name, thing)
+ for (var/thing2 in targets)
+ send_asset(thing2, name)
+ if(sourceonly)
+ return get_asset_url(name)
+ return ""
+
+ //its either an atom, image, or mutable_appearance, we want its icon var
+ icon2collapse = thing.icon
+
+ if (isnull(icon_state))
+ icon_state = thing.icon_state
+ //Despite casting to atom, this code path supports mutable appearances, so let's be nice to them
+ //if(isnull(icon_state) || (isatom(thing) && thing.flags_1 & HTML_USE_INITAL_ICON_1))
+ if(isnull(icon_state) || isatom(thing))
+ icon_state = initial(thing.icon_state)
+ if (isnull(dir))
+ dir = initial(thing.dir)
+
+ if (isnull(dir))
+ dir = thing.dir
+
+ if (ishuman(thing)) // Shitty workaround for a BYOND issue.
+ var/icon/temp = icon2collapse
+ icon2collapse = icon()
+ icon2collapse.Insert(temp, dir = SOUTH)
+ dir = SOUTH
+ else
+ if (isnull(dir))
+ dir = SOUTH
+ if (isnull(icon_state))
+ icon_state = ""
+
+ icon2collapse = icon(icon2collapse, icon_state, dir, frame, moving)
+
+ var/list/name_and_ref = generate_and_hash_rsc_file(icon2collapse, icon_path)//pretend that tuples exist
+
+ var/rsc_ref = name_and_ref[1] //weird object thats not even readable to the debugger, represents a reference to the icons rsc entry
+ var/file_hash = name_and_ref[2]
+ key = "[name_and_ref[3]].png"
+
+ if(!SSassets.cache[key])
+ register_asset(key, rsc_ref, file_hash, icon_path)
+ for (var/client_target in targets)
+ send_asset(client_target, key)
+ if(sourceonly)
+ return get_asset_url(key)
+ return "
"
+
+//Costlier version of icon2html() that uses getFlatIcon() to account for overlays, underlays, etc. Use with extreme moderation, ESPECIALLY on mobs.
+/proc/costly_icon2html(thing, target, sourceonly = FALSE)
+ if (!thing)
+ return
+ //if(SSlag_switch.measures[DISABLE_USR_ICON2HTML] && usr && !HAS_TRAIT(usr, TRAIT_BYPASS_MEASURES))
+ //return
+
+ if (isicon(thing))
+ return icon2html(thing, target)
+
+ var/icon/I = getFlatIcon(thing)
+ return icon2html(I, target, sourceonly = sourceonly)
diff --git a/code/controllers/subsystems/inactivity.dm b/code/controllers/subsystems/inactivity.dm
index e4ab73fcab..ab1b7209ca 100644
--- a/code/controllers/subsystems/inactivity.dm
+++ b/code/controllers/subsystems/inactivity.dm
@@ -46,7 +46,7 @@ SUBSYSTEM_DEF(inactivity)
information = " while an AI."
var/adminlinks
- adminlinks = " (JMP|CRYO)"
+ adminlinks = " (JMP|CRYO)"
log_and_message_admins("being kicked for AFK[information][adminlinks]", C.mob)
diff --git a/code/controllers/subsystems/machines.dm b/code/controllers/subsystems/machines.dm
index f61b94856c..100811f5c0 100644
--- a/code/controllers/subsystems/machines.dm
+++ b/code/controllers/subsystems/machines.dm
@@ -6,9 +6,6 @@
//
// SSmachines subsystem - Processing machines, pipenets, and powernets!
//
-// Implementation Plan:
-// PHASE 1 - Add subsystem using the existing global list vars
-// PHASE 2 - Move the global list vars into the subsystem.
SUBSYSTEM_DEF(machines)
name = "Machines"
@@ -24,12 +21,6 @@ SUBSYSTEM_DEF(machines)
var/cost_powernets = 0
var/cost_power_objects = 0
- // TODO - PHASE 2 - Switch these from globals to instance vars
- // var/list/pipenets = list()
- // var/list/machinery = list()
- // var/list/powernets = list()
- // var/list/power_objects = list()
-
var/list/current_run = list()
/datum/controller/subsystem/machines/Initialize(timeofday)
@@ -51,28 +42,31 @@ SUBSYSTEM_DEF(machines)
// The above is a lie. Turbolifts also call this proc.
/datum/controller/subsystem/machines/proc/makepowernets()
// TODO - check to not run while in the middle of a tick!
- for(var/datum/powernet/PN in powernets)
+ for(var/datum/powernet/PN as anything in powernets)
qdel(PN)
powernets.Cut()
setup_powernets_for_cables(cable_list)
/datum/controller/subsystem/machines/proc/setup_powernets_for_cables(list/cables)
- for(var/obj/structure/cable/PC in cables)
+ for(var/obj/structure/cable/PC as anything in cables)
if(!PC.powernet)
var/datum/powernet/NewPN = new()
NewPN.add_cable(PC)
propagate_network(PC,PC.powernet)
/datum/controller/subsystem/machines/proc/setup_atmos_machinery(list/atmos_machines)
+ var/list/actual_atmos_machines = list()
+
for(var/obj/machinery/atmospherics/machine in atmos_machines)
machine.atmos_init()
+ actual_atmos_machines += machine
CHECK_TICK
- for(var/obj/machinery/atmospherics/machine in atmos_machines)
+ for(var/obj/machinery/atmospherics/machine as anything in actual_atmos_machines)
machine.build_network()
CHECK_TICK
- for(var/obj/machinery/atmospherics/unary/U in atmos_machines)
+ for(var/obj/machinery/atmospherics/unary/U as anything in actual_atmos_machines)
if(istype(U, /obj/machinery/atmospherics/unary/vent_pump))
var/obj/machinery/atmospherics/unary/vent_pump/T = U
T.broadcast_status()
@@ -163,16 +157,24 @@ SUBSYSTEM_DEF(machines)
return
/datum/controller/subsystem/machines/Recover()
- // TODO - PHASE 2
- // if (istype(SSmachines.pipenets))
- // pipenets = SSmachines.pipenets
- // if (istype(SSmachines.machinery))
- // machinery = SSmachines.machinery
- // if (istype(SSmachines.powernets))
- // powernets = SSmachines.powernets
- // if (istype(SSmachines.power_objects))
- // power_objects = SSmachines.power_objects
+ for(var/datum/D as anything in global.pipe_networks)
+ if(!istype(D, /datum/pipe_network))
+ error("Found wrong type during SSmachinery recovery: list=global.pipe_networks, item=[D], type=[D?.type]")
+ global.pipe_networks -= D
+ for(var/datum/D as anything in global.processing_machines)
+ if(!istype(D, /obj/machinery))
+ error("Found wrong type during SSmachinery recovery: list=global.processing_machines, item=[D], type=[D?.type]")
+ global.processing_machines -= D
+ for(var/datum/D as anything in global.powernets)
+ if(!istype(D, /datum/powernet))
+ error("Found wrong type during SSmachinery recovery: list=global.powernets, item=[D], type=[D?.type]")
+ global.powernets -= D
+ for(var/datum/D as anything in global.processing_power_items)
+ if(!istype(D, /obj/item))
+ error("Found wrong type during SSmachinery recovery: list=global.processing_power_items, item=[D], type=[D?.type]")
+ global.processing_power_items -= D
#undef SSMACHINES_PIPENETS
#undef SSMACHINES_MACHINERY
+#undef SSMACHINES_POWERNETS
#undef SSMACHINES_POWER_OBJECTS
diff --git a/code/controllers/subsystems/mobs.dm b/code/controllers/subsystems/mobs.dm
index c124b5573e..66352c9c9e 100644
--- a/code/controllers/subsystems/mobs.dm
+++ b/code/controllers/subsystems/mobs.dm
@@ -43,6 +43,11 @@ SUBSYSTEM_DEF(mobs)
else if(M.low_priority && !(M.loc && get_z(M) && process_z[get_z(M)]))
slept_mobs++
continue
+ //CHOMPEdit Start - Enable pausing mobs (For transformation, holding until reformation, etc.)
+ else if(!M.enabled)
+ slept_mobs++
+ continue
+ //CHOMPEdit End
M.Life(times_fired)
@@ -90,4 +95,4 @@ SUBSYSTEM_DEF(mobs)
/datum/controller/subsystem/mobs/critfail()
..()
- log_recent()
\ No newline at end of file
+ log_recent()
diff --git a/code/controllers/subsystems/vote.dm b/code/controllers/subsystems/vote.dm
index 44076a407b..24f7e33d37 100644
--- a/code/controllers/subsystems/vote.dm
+++ b/code/controllers/subsystems/vote.dm
@@ -315,7 +315,7 @@ SUBSYSTEM_DEF(vote)
. += "