Parallax but better: Smooth movement cleanup (#66567)

* Alright, so I'm optimizing parallax code so I can justify making it do a
bit more work

To that end, lets make the checks it does each process event based.
There's two. One is for a difference in view, which is an easy fix since
I added a view setter like a year back now.

The second is something planets do when you change your z level.
This gets more complicated, because we're "owned" by a client.
So the only real pattern we can use to hook into the client's mob's
movement is something like connect_loc_behalf.

So, I've made connect_mob_behalf. Fuck you.

This saves a proc call and some redundant logic

* Fixes random parallax stuttering

Ok so this is kinda a weird one but hear me out.

Parallax has this concept of "direction" that some areas use, mostly
the shuttle transit ones. Set when you move into a new area.
So of course it has a setter. If you pass it a direction that it doesn't
already have, it'll start up the movement animation, and disable normal
parallax for a bit to give it some time to get going.

This var is typically set to 0.

The problem is we were setting /area/space's direction to null in
shuttle movement code, because of a forgotten proc arg.

Null is of course different then 0, so this would trigger a halt in
parallax processing.

This causes a lot of strange stutters in parallax, mostly when you're
moving between nearspace and space. It looks really bad, and I'm a bit
suprised none noticed.

I've fixed it, and added a default arg to the setter to prevent this
class of issue in future. Things look a good bit nicer this way

* Adds animation back to parallax

Ok so like, I know this was removed and "none could tell" and whatever,
and in fairness this animation method is a bit crummy.

What we really want to do is eliminate "halts" and "jumps" in the
parallax moveemnt. So it should be smooth.

As it is on live now, this just isn't what happens, you get jumping
between offsets. Looks frankly, horrible. Especially on the station.

Just what I've done won't be enough however, because what we need to do
is match our parallax scroll speed with our current glide speed. I need
to figure out how to do this well, and I have a feeling it will involve
some system of managing glide sources.

Anyway for now the animation looks really nice for ghosts with default
(high) settings, since they share the same delay.

I've done some refactoring to how old animation code worked pre (4b04f9012d). Two major
changes tho.

First, instead of doing all the animate checks each time we loop over a
layer, we only do the layer dependant ones. This saves a good bit of
time.

Second, we animate movement on absolute layers too. They're staying in
the same position, but they still move on the screen, so we do the same
gental leaning. This has a very nice visual effect.

Oh and I cleaned up some of the code slightly.
This commit is contained in:
LemonInTheDark
2022-05-07 14:59:41 -07:00
committed by GitHub
parent f5bdc92acf
commit 98f32035d8
20 changed files with 197 additions and 83 deletions
@@ -0,0 +1,4 @@
// Yes, they do support this
// from /client/proc/change_view() : (new_size)
#define COMSIG_VIEW_SET "view_set"
+104 -62
View File
@@ -7,12 +7,12 @@
if(!length(C.parallax_layers_cached))
C.parallax_layers_cached = list()
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_1(null, C.view)
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_2(null, C.view)
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/planet(null, C.view)
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_1(null, screenmob)
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_2(null, screenmob)
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/planet(null, screenmob)
if(SSparallax.random_layer)
C.parallax_layers_cached += new SSparallax.random_layer
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_3(null, C.view)
C.parallax_layers_cached += new SSparallax.random_layer(null, screenmob)
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_3(null, screenmob)
C.parallax_layers = C.parallax_layers_cached.Copy()
@@ -53,33 +53,36 @@
return FALSE
var/client/C = screenmob.client
// Default to HIGH
var/parallax_selection = PARALLAX_HIGH
if(C.prefs)
var/pref = C.prefs.read_preference(/datum/preference/choiced/parallax)
if (isnull(pref))
pref = PARALLAX_HIGH
switch(pref)
if (PARALLAX_INSANE)
C.parallax_throttle = FALSE
C.parallax_layers_max = 5
return TRUE
parallax_selection = C.prefs.read_preference(/datum/preference/choiced/parallax)
if (!parallax_selection)
parallax_selection = PARALLAX_HIGH
if (PARALLAX_MED)
C.parallax_throttle = PARALLAX_DELAY_MED
C.parallax_layers_max = 3
return TRUE
switch(parallax_selection)
if (PARALLAX_INSANE)
C.parallax_layers_max = 5
C.do_parallax_animations = TRUE
return TRUE
if (PARALLAX_LOW)
C.parallax_throttle = PARALLAX_DELAY_LOW
C.parallax_layers_max = 1
return TRUE
if(PARALLAX_HIGH)
C.parallax_layers_max = 4
C.do_parallax_animations = TRUE
return TRUE
if (PARALLAX_DISABLE)
return FALSE
if (PARALLAX_MED)
C.parallax_layers_max = 3
C.do_parallax_animations = TRUE
return TRUE
//This is high parallax.
C.parallax_throttle = PARALLAX_DELAY_DEFAULT
C.parallax_layers_max = 4
return TRUE
if (PARALLAX_LOW)
C.parallax_layers_max = 1
C.do_parallax_animations = FALSE
return TRUE
if (PARALLAX_DISABLE)
return FALSE
/datum/hud/proc/update_parallax_pref(mob/viewmob)
remove_parallax(viewmob)
@@ -87,7 +90,7 @@
update_parallax(viewmob)
// This sets which way the current shuttle is moving (returns true if the shuttle has stopped moving so the caller can append their animation)
/datum/hud/proc/set_parallax_movedir(new_parallax_movedir, skip_windups, mob/viewmob)
/datum/hud/proc/set_parallax_movedir(new_parallax_movedir = 0, skip_windups, mob/viewmob)
. = FALSE
var/mob/screenmob = viewmob || mymob
var/client/C = screenmob.client
@@ -171,19 +174,16 @@
var/turf/posobj = get_turf(C.eye)
if(!posobj)
return
var/area/areaobj = posobj.loc
var/area/areaobj = posobj.loc
// Update the movement direction of the parallax if necessary (for shuttles)
set_parallax_movedir(areaobj.parallax_movedir, FALSE, screenmob)
var/force
var/force = FALSE
if(!C.previous_turf || (C.previous_turf.z != posobj.z))
C.previous_turf = posobj
force = TRUE
if (!force && world.time < C.last_parallax_shift+C.parallax_throttle)
return
//Doing it this way prevents parallax layers from "jumping" when you change Z-Levels.
var/offset_x = posobj.x - C.previous_turf.x
var/offset_y = posobj.y - C.previous_turf.y
@@ -191,33 +191,48 @@
if(!offset_x && !offset_y && !force)
return
var/last_delay = world.time - C.last_parallax_shift
last_delay = min(last_delay, C.parallax_throttle)
var/glide_rate = round(world.icon_size / screenmob.glide_size * world.tick_lag, world.tick_lag)
C.previous_turf = posobj
C.last_parallax_shift = world.time
var/largest_change = max(abs(offset_x), abs(offset_y))
var/max_allowed_dist = (glide_rate / world.tick_lag) + 1
// If we aren't already moving/don't allow parallax, have made some movement, and that movement was smaller then our "glide" size, animate
var/run_parralax = (C.do_parallax_animations && glide_rate && !areaobj.parallax_movedir && C.dont_animate_parallax <= world.time && largest_change <= max_allowed_dist)
for(var/atom/movable/screen/parallax_layer/parallax_layer as anything in C.parallax_layers)
parallax_layer.update_status(screenmob)
if (parallax_layer.view_sized != C.view)
parallax_layer.update_o(C.view)
var/our_speed = parallax_layer.speed
var/change_x
var/change_y
if(parallax_layer.absolute)
parallax_layer.offset_x = -(posobj.x - SSparallax.planet_x_offset) * parallax_layer.speed
parallax_layer.offset_y = -(posobj.y - SSparallax.planet_y_offset) * parallax_layer.speed
// We use change here so the typically large absolute objects (just lavaland for now) don't jitter so much
change_x = (posobj.x - SSparallax.planet_x_offset) * our_speed + parallax_layer.offset_x
change_y = (posobj.y - SSparallax.planet_y_offset) * our_speed + parallax_layer.offset_y
else
parallax_layer.offset_x -= offset_x * parallax_layer.speed
parallax_layer.offset_y -= offset_y * parallax_layer.speed
change_x = offset_x * our_speed
change_y = offset_y * our_speed
if(parallax_layer.offset_x > 240)
// This is how we tile parralax sprites
// It doesn't use change because we really don't want to animate this
if(parallax_layer.offset_x - change_x > 240)
parallax_layer.offset_x -= 480
if(parallax_layer.offset_x < -240)
else if(parallax_layer.offset_x - change_x < -240)
parallax_layer.offset_x += 480
if(parallax_layer.offset_y > 240)
if(parallax_layer.offset_y - change_y > 240)
parallax_layer.offset_y -= 480
if(parallax_layer.offset_y < -240)
else if(parallax_layer.offset_y - change_y < -240)
parallax_layer.offset_y += 480
parallax_layer.screen_loc = "CENTER-7:[round(parallax_layer.offset_x,1)],CENTER-7:[round(parallax_layer.offset_y,1)]"
// Now that we have our offsets, let's do our positioning
parallax_layer.offset_x -= change_x
parallax_layer.offset_y -= change_y
parallax_layer.screen_loc = "CENTER-7:[round(parallax_layer.offset_x, 1)],CENTER-7:[round(parallax_layer.offset_y, 1)]"
// We're going to use a transform to "glide" that last movement out, so it looks nicer
// Don't do any animates if we're not actually moving enough distance yeah? thanks lad
if(run_parralax && (largest_change * our_speed > 1))
parallax_layer.transform = matrix(1,0,change_x, 0,1,change_y)
animate(parallax_layer, transform=matrix(), time = glide_rate)
/atom/movable/proc/update_parallax_contents()
for(var/mob/client_mob as anything in client_mobs_in_contents)
@@ -229,6 +244,8 @@
var/area/areaobj = get_area(client.eye)
hud_used.set_parallax_movedir(areaobj.parallax_movedir, TRUE)
// We need parallax to always pass its args down into initialize, so we immediate init it
INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer)
/atom/movable/screen/parallax_layer
icon = 'icons/effects/parallax.dmi'
var/speed = 1
@@ -241,20 +258,31 @@
screen_loc = "CENTER-7,CENTER-7"
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
/atom/movable/screen/parallax_layer/Initialize(mapload, view)
/atom/movable/screen/parallax_layer/Initialize(mapload, mob/owner)
. = ..()
if (!view)
view = world.view
var/client/boss = owner?.client
if(!boss) // If this typepath all starts to harddel your culprit is likely this
return INITIALIZE_HINT_QDEL
// I do not want to know bestie
var/view = boss.view || world.view
update_o(view)
RegisterSignal(boss, COMSIG_VIEW_SET, .proc/on_view_change)
/atom/movable/screen/parallax_layer/proc/on_view_change(datum/source, new_size)
SIGNAL_HANDLER
update_o(new_size)
/atom/movable/screen/parallax_layer/proc/update_o(view)
if (!view)
view = world.view
var/static/parallax_scaler = world.icon_size / 480
// Turn the view size into a grid of correctly scaled overlays
var/list/viewscales = getviewsize(view)
var/countx = CEILING((viewscales[1]/2)/(480/world.icon_size), 1)+1
var/county = CEILING((viewscales[2]/2)/(480/world.icon_size), 1)+1
var/countx = CEILING((viewscales[1] / 2) * parallax_scaler, 1) + 1
var/county = CEILING((viewscales[2] / 2) * parallax_scaler, 1) + 1
var/list/new_overlays = new
for(var/x in -countx to countx)
for(var/y in -county to county)
@@ -267,9 +295,6 @@
add_overlay(new_overlays)
view_sized = view
/atom/movable/screen/parallax_layer/proc/update_status(mob/M)
return
/atom/movable/screen/parallax_layer/layer_1
icon_state = "layer1"
speed = 0.6
@@ -293,7 +318,7 @@
/atom/movable/screen/parallax_layer/random/space_gas
icon_state = "space_gas"
/atom/movable/screen/parallax_layer/random/space_gas/Initialize(mapload, view)
/atom/movable/screen/parallax_layer/random/space_gas/Initialize(mapload, mob/owner)
. = ..()
src.add_atom_colour(SSparallax.random_parallax_color, ADMIN_COLOUR_PRIORITY)
@@ -307,9 +332,26 @@
speed = 3
layer = 30
/atom/movable/screen/parallax_layer/planet/update_status(mob/M)
var/client/C = M.client
var/turf/posobj = get_turf(C.eye)
/atom/movable/screen/parallax_layer/planet/Initialize(mapload, mob/owner)
. = ..()
if(!owner?.client)
return
var/static/list/connections = list(
COMSIG_MOVABLE_Z_CHANGED = .proc/on_z_change,
COMSIG_MOB_LOGOUT = .proc/on_mob_logout,
)
AddComponent(/datum/component/connect_mob_behalf, owner.client, connections)
on_z_change(owner)
/atom/movable/screen/parallax_layer/planet/proc/on_mob_logout(mob/source)
SIGNAL_HANDLER
var/client/boss = source.canon_client
on_z_change(boss.mob)
/atom/movable/screen/parallax_layer/planet/proc/on_z_change(mob/source)
SIGNAL_HANDLER
var/client/boss = source.client
var/turf/posobj = get_turf(boss?.eye)
if(!posobj)
return
invisibility = is_station_level(posobj.z) ? 0 : INVISIBILITY_ABSTRACT
@@ -0,0 +1,59 @@
/// This component behaves similar to connect_loc_behalf, but working off clients and mobs instead of loc
/// To be clear, we hook into a signal on a tracked client's mob
/// We retain the ability to react to that signal on a seperate listener, which makes this quite powerful
/datum/component/connect_mob_behalf
dupe_mode = COMPONENT_DUPE_UNIQUE
/// An assoc list of signal -> procpath to register to the mob our client "owns"
var/list/connections
/// The master client we're working with
var/client/tracked
/// The mob we're currently tracking
var/mob/tracked_mob
/datum/component/connect_mob_behalf/Initialize(client/tracked, list/connections)
. = ..()
if (!istype(tracked))
return COMPONENT_INCOMPATIBLE
src.connections = connections
src.tracked = tracked
/datum/component/connect_mob_behalf/RegisterWithParent()
RegisterSignal(tracked, COMSIG_PARENT_QDELETING, .proc/handle_tracked_qdel)
update_signals()
/datum/component/connect_mob_behalf/UnregisterFromParent()
unregister_signals()
UnregisterSignal(tracked, COMSIG_PARENT_QDELETING)
tracked = null
tracked_mob = null
/datum/component/connect_mob_behalf/proc/handle_tracked_qdel()
SIGNAL_HANDLER
qdel(src)
/datum/component/connect_mob_behalf/proc/update_signals()
unregister_signals()
// Yes this is a runtime silencer
// We could be in a position where logout is sent to two things, one thing intercepts it, then deletes the client's new mob
// It's rare, and the same check in connect_loc_behalf is more fruitful, but it's still worth doing
if(QDELETED(tracked?.mob))
return
tracked_mob = tracked.mob
RegisterSignal(tracked_mob, COMSIG_MOB_LOGOUT, .proc/on_logout)
for (var/signal in connections)
parent.RegisterSignal(tracked_mob, signal, connections[signal])
/datum/component/connect_mob_behalf/proc/unregister_signals()
if(isnull(tracked_mob))
return
parent.UnregisterSignal(tracked_mob, connections)
UnregisterSignal(tracked_mob, COMSIG_MOB_LOGOUT)
tracked_mob = null
/datum/component/connect_mob_behalf/proc/on_logout(mob/source)
SIGNAL_HANDLER
update_signals()
+1 -1
View File
@@ -7,7 +7,7 @@
/atom
layer = TURF_LAYER
plane = GAME_PLANE
appearance_flags = TILE_BOUND
appearance_flags = TILE_BOUND|LONG_GLIDE
/// pass_flags that we are. If any of this matches a pass_flag on a moving thing, by default, we let them through.
var/pass_flags_self = NONE
+7 -3
View File
@@ -1,7 +1,7 @@
/atom/movable
layer = OBJ_LAYER
glide_size = 8
appearance_flags = TILE_BOUND|PIXEL_SCALE
appearance_flags = TILE_BOUND|PIXEL_SCALE|LONG_GLIDE
///how many times a this movable had movement procs called on it since Moved() was last called
var/move_stacks = 0
@@ -427,7 +427,6 @@
if(!only_pulling && pulledby && moving_diagonally != FIRST_DIAG_STEP && (get_dist(src, pulledby) > 1 || z != pulledby.z)) //separated from our puller and not in the middle of a diagonal move.
pulledby.stop_pulling()
/atom/movable/proc/set_glide_size(target = 8)
SEND_SIGNAL(src, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, target)
glide_size = target
@@ -590,6 +589,8 @@
setDir(first_step_dir)
else if (!inertia_moving)
newtonian_move(direct)
if(client_mobs_in_contents) // We're done moving, update our parallax now
update_parallax_contents()
moving_diagonally = 0
return
@@ -656,7 +657,10 @@
if (!inertia_moving)
newtonian_move(movement_dir)
if (client_mobs_in_contents)
// If we ain't moving diagonally right now, update our parallax
// We don't do this all the time because diag movements should trigger one call to this, not two
// Waste of cpu time, and it fucks the animate
if (!moving_diagonally && client_mobs_in_contents)
update_parallax_contents()
move_stacks--
+1 -1
View File
@@ -100,7 +100,7 @@
/atom/movable/warp_effect
plane = GRAVITY_PULSE_PLANE
appearance_flags = PIXEL_SCALE // no tile bound so you can see it around corners and so
appearance_flags = PIXEL_SCALE|LONG_GLIDE // no tile bound so you can see it around corners and so
icon = 'icons/effects/light_overlays/light_352.dmi'
icon_state = "light"
pixel_x = -176
@@ -6,7 +6,7 @@
plane = GAME_PLANE_FOV_HIDDEN
anchored = TRUE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
appearance_flags = 0
appearance_flags = LONG_GLIDE
/obj/effect/projectile/singularity_pull()
return
+1 -1
View File
@@ -81,7 +81,7 @@
base_icon_state = "donutbox"
spawn_type = /obj/item/food/donut/plain
is_open = TRUE
appearance_flags = KEEP_TOGETHER
appearance_flags = KEEP_TOGETHER|LONG_GLIDE
custom_premium_price = PAYCHECK_COMMAND * 1.75
contents_tag = "donut"
@@ -304,7 +304,6 @@ GLOBAL_LIST_EMPTY(lifts)
//if going EAST, will turn to the NORTHEAST or SOUTHEAST and throw the ran over guy away
var/datum/callback/land_slam = new(collided, /mob/living/.proc/tram_slam_land)
collided.throw_at(throw_target, 200, 4, callback = land_slam)
set_glide_size(gliding_amount)
forceMove(destination)
for(var/atom/movable/thing as anything in things_to_move)
+2 -2
View File
@@ -149,7 +149,7 @@
desc = "A very large bluespace engine used to propel very large ships."
bound_width = 64
bound_height = 64
appearance_flags = 0
appearance_flags = LONG_GLIDE
/obj/structure/shuttle/engine/large/in_wall
name = "in-wall engine"
@@ -166,7 +166,7 @@
desc = "An extremely large bluespace engine used to propel extremely large ships."
bound_width = 96
bound_height = 96
appearance_flags = 0
appearance_flags = LONG_GLIDE
/obj/structure/shuttle/engine/huge/in_wall
name = "in-wall engine"
+1 -1
View File
@@ -507,7 +507,7 @@ Moving interrupts
name = "custom statue"
icon_state = "base"
obj_flags = CAN_BE_HIT | UNIQUE_RENAME
appearance_flags = TILE_BOUND | PIXEL_SCALE | KEEP_TOGETHER //Added keep together in case targets has weird layering
appearance_flags = TILE_BOUND | PIXEL_SCALE | KEEP_TOGETHER | LONG_GLIDE //Added keep together in case targets has weird layering
material_flags = MATERIAL_EFFECTS | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
/// primary statue overlay
var/mutable_appearance/content_ma
@@ -23,7 +23,7 @@
smoothing_flags = SMOOTH_CORNERS | SMOOTH_OBJ
smoothing_groups = list(SMOOTH_GROUP_GAS_TANK)
canSmoothWith = list(SMOOTH_GROUP_GAS_TANK)
appearance_flags = KEEP_TOGETHER
appearance_flags = KEEP_TOGETHER|LONG_GLIDE
greyscale_config = /datum/greyscale_config/stationary_canister
greyscale_colors = "#ffffff"
+1 -1
View File
@@ -15,7 +15,7 @@
anchored = TRUE //So it cant slide around after landing
anchorable = FALSE
flags_1 = PREVENT_CONTENTS_EXPLOSION_1
appearance_flags = KEEP_TOGETHER | PIXEL_SCALE
appearance_flags = KEEP_TOGETHER | PIXEL_SCALE | LONG_GLIDE
density = FALSE
divable = FALSE
///List of bitflags for supply pods, see: code\__DEFINES\obj_flags.dm
+7 -4
View File
@@ -198,13 +198,16 @@
var/turf/previous_turf
///world.time of when we can state animate()ing parallax again
var/dont_animate_parallax
///world.time of last parallax update
var/last_parallax_shift
///ds between parallax updates
var/parallax_throttle = 0
/// Direction our current area wants to move parallax
var/parallax_movedir = 0
/// How many parallax layers to show our client
var/parallax_layers_max = 4
/// Timer for the area directional animation
var/parallax_animate_timer
/// Do we want to do parallax animations at all?
/// Exists to prevent laptop fires
var/do_parallax_animations = TRUE
///Are we locking our movement input?
var/movement_locked = FALSE
+1
View File
@@ -1030,6 +1030,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
CRASH("change_view called without argument.")
view = new_size
SEND_SIGNAL(src, COMSIG_VIEW_SET, new_size)
mob.hud_used.screentip_text.update_view()
apply_clickcatcher()
mob.reload_fullscreen()
@@ -12,7 +12,7 @@
icon = 'icons/obj/lavaland/dead_ratvar.dmi'
icon_state = "dead_ratvar"
flags_1 = ON_BORDER_1
appearance_flags = 0
appearance_flags = LONG_GLIDE
layer = FLY_LAYER
plane = ABOVE_GAME_PLANE
anchored = TRUE
@@ -150,7 +150,7 @@ GLOBAL_DATUM_INIT(cameranet, /datum/cameranet, new)
icon = null
icon_state = null
anchored = TRUE // should only appear in vis_contents, but to be safe
appearance_flags = RESET_TRANSFORM | TILE_BOUND
appearance_flags = RESET_TRANSFORM | TILE_BOUND | LONG_GLIDE
// this combination makes the static block clicks to everything below it,
// without appearing in the right-click menu for non-AI clients
mouse_opacity = MOUSE_OPACITY_ICON
+1 -1
View File
@@ -152,7 +152,7 @@
/obj/docking_port/mobile/proc/cleanup_runway(obj/docking_port/stationary/new_dock, list/old_turfs, list/new_turfs, list/areas_to_move, list/moved_atoms, rotation, movement_direction, area/underlying_old_area)
underlying_old_area.afterShuttleMove()
underlying_old_area.afterShuttleMove(0)
// Parallax handling
// This needs to be done before the atom after move
+1 -1
View File
@@ -142,7 +142,7 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE)
pixel_x = -192
bound_width = 352
bound_x = -192
appearance_flags = NONE //Removes default TILE_BOUND
appearance_flags = LONG_GLIDE //Removes default TILE_BOUND
/obj/machinery/bsa/full/wrench_act(mob/living/user, obj/item/I)
return FALSE
+2
View File
@@ -206,6 +206,7 @@
#include "code\__DEFINES\dcs\signals\signals_bot.dm"
#include "code\__DEFINES\dcs\signals\signals_changeling.dm"
#include "code\__DEFINES\dcs\signals\signals_circuit.dm"
#include "code\__DEFINES\dcs\signals\signals_client.dm"
#include "code\__DEFINES\dcs\signals\signals_clothing.dm"
#include "code\__DEFINES\dcs\signals\signals_container.dm"
#include "code\__DEFINES\dcs\signals\signals_customizable.dm"
@@ -695,6 +696,7 @@
#include "code\datums\components\codeword_hearing.dm"
#include "code\datums\components\combustible_flooder.dm"
#include "code\datums\components\connect_containers.dm"
#include "code\datums\components\connect_mob_behalf.dm"
#include "code\datums\components\connect_loc_behalf.dm"
#include "code\datums\components\connect_range.dm"
#include "code\datums\components\construction.dm"