diff --git a/code/__DEFINES/movement.dm b/code/__DEFINES/movement.dm index 85285d800582..94e5b3a66b1d 100644 --- a/code/__DEFINES/movement.dm +++ b/code/__DEFINES/movement.dm @@ -6,6 +6,8 @@ /// Compensating for time dilation GLOBAL_VAR_INIT(glide_size_multiplier, 1.0) +/// The error motivating our existing multiplier +GLOBAL_VAR_INIT(glide_size_multi_error, 0) ///Broken down, here's what this does: /// divides the world icon_size (32) by delay divided by ticklag to get the number of pixels something should be moving each tick. @@ -13,7 +15,9 @@ GLOBAL_VAR_INIT(glide_size_multiplier, 1.0) /// Then that's multiplied by the global glide size multiplier. 1.25 by default feels pretty close to spot on. This is just to try to get byond to behave. /// The whole result is then clamped to within the range above. /// Not very readable but it works -#define DELAY_TO_GLIDE_SIZE(delay) (clamp(((world.icon_size / max((delay) / world.tick_lag, 1)) * GLOB.glide_size_multiplier), MIN_GLIDE_SIZE, MAX_GLIDE_SIZE)) +#define DELAY_TO_GLIDE_SIZE(delay) DISTANCE_BOUND_DELAY_TO_GLIDE_SIZE(world.icon_size, delay) + +#define DISTANCE_BOUND_DELAY_TO_GLIDE_SIZE(distance, delay) (clamp((((distance) / max((delay) / world.tick_lag, 1)) * GLOB.glide_size_multiplier), MIN_GLIDE_SIZE, MAX_GLIDE_SIZE)) ///Similar to DELAY_TO_GLIDE_SIZE, except without the clamping, and it supports piping in an unrelated scalar #define MOVEMENT_ADJUSTED_GLIDE_SIZE(delay, movement_disparity) (world.icon_size / ((delay) / world.tick_lag) * movement_disparity * GLOB.glide_size_multiplier) diff --git a/code/__DEFINES/perf_test.dm b/code/__DEFINES/perf_test.dm index 69d766b81e2a..20cc541b505d 100644 --- a/code/__DEFINES/perf_test.dm +++ b/code/__DEFINES/perf_test.dm @@ -1,7 +1,7 @@ /// Macro that takes a tick usage to target, and proceses until we hit it /// This lets us simulate generic load as we'd like, to make testing for overtime easier #define CONSUME_UNTIL(target_usage) \ - while(TICK_USAGE < target_usage) {\ + while(TICK_USAGE < (target_usage)) {\ var/_knockonwood_x = 0;\ _knockonwood_x += 20;\ } diff --git a/code/controllers/master.dm b/code/controllers/master.dm index aace954be081..f1459ea64b0c 100644 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -70,6 +70,10 @@ GLOBAL_REAL(Master, /datum/controller/master) ///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 + var/spike_cpu = 0 + var/sustain_chance = 100 + var/sustain_cpu = 0 + /datum/controller/master/New() // Ensure usr is null, to prevent any potential weirdness resulting from the MC having a usr if it's manually restarted. usr = null @@ -473,6 +477,8 @@ GLOBAL_REAL(Master, /datum/controller/master) tickdrift = max(0, MC_AVERAGE_FAST(tickdrift, (((REALTIMEOFDAY - init_timeofday) - (world.time - init_time)) / world.tick_lag))) var/starting_tick_usage = TICK_USAGE + update_glide_size() + if (init_stage != init_stage_completed) return MC_LOOP_RTN_NEWSTAGES if (processing <= 0) @@ -837,3 +843,91 @@ GLOBAL_REAL(Master, /datum/controller/master) for (var/thing in subsystems) var/datum/controller/subsystem/SS = thing SS.OnConfigLoad() + +/world/Tick() + unroll_cpu_value() + if(Master.sustain_cpu && prob(Master.sustain_chance)) + // avoids byond sleeping the loop and causing the MC to infinistall + CONSUME_UNTIL(min(Master.sustain_cpu, 10000)) + + if(Master.spike_cpu) + CONSUME_UNTIL(min(Master.spike_cpu, 10000)) + Master.spike_cpu = 0 + + +#define CPU_SIZE 20 +#define WINDOW_SIZE 16 +GLOBAL_LIST_INIT(cpu_values, new /list(CPU_SIZE)) +GLOBAL_LIST_INIT(avg_cpu_values, new /list(CPU_SIZE)) +GLOBAL_VAR_INIT(cpu_index, 1) +GLOBAL_VAR_INIT(last_cpu_update, -1) + +/// Inserts our current world.cpu value into our rolling lists +/// Its job is to pull the actual usage last tick instead of the moving average +/world/proc/unroll_cpu_value() + if(GLOB.last_cpu_update == world.time) + return + GLOB.last_cpu_update = world.time + var/avg_cpu = world.cpu + var/list/cpu_values = GLOB.cpu_values + var/cpu_index = GLOB.cpu_index + + // We need to hook into the INSTANT we start our moving average so we can reconstruct gained/lost cpu values + var/lost_value = 0 + lost_value = cpu_values[WRAP(cpu_index - WINDOW_SIZE, 1, CPU_SIZE + 1)] + + // avg = (A + B + C + D) / 4 + // old_avg = (A + B + C) / 3 + // (avg * 4 - old_avg * 3) roughly = D + // avg = (B + C + D) / 3 + // old_avg = (A + B + C) / 3 + // (avg * 4 - old_avg * 3) roughly = D - A + // so if we aren't moving we need to add the value we are losing + // We're trying to do this with as few ops as possible mind + // soooo + // C = (avg * 3 - old_avg * 3) + A + + var/last_avg_cpu = GLOB.avg_cpu_values[WRAP(cpu_index - 1, 1, CPU_SIZE + 1)] + var/real_cpu = (avg_cpu *WINDOW_SIZE - last_avg_cpu * WINDOW_SIZE) + lost_value + + // cache for sonic speed + cpu_values[cpu_index] = real_cpu + GLOB.avg_cpu_values[cpu_index] = avg_cpu + GLOB.cpu_index = WRAP(cpu_index + 1, 1, CPU_SIZE + 1) + + +/proc/update_glide_size() + world.unroll_cpu_value() + var/list/cpu_values = GLOB.cpu_values + var/sum = 0 + var/non_zero = 0 + for(var/value in cpu_values) + sum += max(value, 100) + if(value != 0) + non_zero += 1 + + var/first_average = non_zero ? sum / non_zero : 1 + var/trimmed_sum = 0 + var/used = 0 + for(var/value in cpu_values) + if(!value) + continue + // If we deviate more then 60% away from the average, skip it + if(abs(1 - (max(value, 100) / first_average)) <= 0.3) + trimmed_sum += max(value, 100) + used += 1 + + var/final_average = trimmed_sum ? trimmed_sum / used : first_average + GLOB.glide_size_multiplier = min(100 / final_average, 1) + GLOB.glide_size_multi_error = max((final_average - 100) / 100 * world.tick_lag, 0) + + /// Gets the cpu value we finished the last tick with (since the index reads a step ahead) + var/last_cpu = cpu_values[WRAP(GLOB.cpu_index - 1, 1, CPU_SIZE + 1)] + var/error = max((last_cpu - 100) / 100 * world.tick_lag, 0) + + for(var/atom/movable/trouble as anything in GLOB.gliding_atoms) + if(world.time >= trouble.glide_stopping_time || QDELETED(trouble)) + GLOB.gliding_atoms -= trouble + trouble.glide_tracking = FALSE + continue + trouble.account_for_glide_error(error) diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm index f4b628fe1f14..465840c668f1 100644 --- a/code/controllers/subsystem.dm +++ b/code/controllers/subsystem.dm @@ -112,7 +112,7 @@ //Do not blindly add vars here to the bottom, put it where it goes above //If your var only has two values, put it in as a flag. - + var/consume_most_allocation = FALSE //Do not override ///datum/controller/subsystem/New() @@ -133,7 +133,10 @@ tick_allocation_avg = MC_AVERAGE(tick_allocation_avg, tick_allocation_last) . = SS_SLEEPING - fire(resumed) + if(consume_most_allocation) + CONSUME_UNTIL(Master.current_ticklimit * 0.8) + else + fire(resumed) . = state if (state == SS_SLEEPING) slept_count++ diff --git a/code/controllers/subsystem/input.dm b/code/controllers/subsystem/input.dm index 65dc1e31a1e4..87b65e02263d 100644 --- a/code/controllers/subsystem/input.dm +++ b/code/controllers/subsystem/input.dm @@ -21,6 +21,7 @@ VERB_MANAGER_SUBSYSTEM_DEF(input) ///running average of the amount of real time clicks take to truly execute after the command is originally sent to the server. ///if a click isnt delayed at all then it counts as 0 deciseconds. var/average_click_delay = 0 + var/list/moving_mobs = list() /datum/controller/subsystem/verb_manager/input/Initialize() setup_default_macro_sets() @@ -71,6 +72,8 @@ VERB_MANAGER_SUBSYSTEM_DEF(input) var/moves_this_run = 0 for(var/mob/user in GLOB.keyloop_list) moves_this_run += user.focus?.keyLoop(user.client)//only increments if a player moves due to their own input + for(var/mob/moving in SSinput.moving_mobs) + moves_this_run += moving.keyLoop(gondor_calls_for_aid = TRUE) movements_per_second = MC_AVG_SECONDS(movements_per_second, moves_this_run, wait TICKS) diff --git a/code/controllers/subsystem/time_track.dm b/code/controllers/subsystem/time_track.dm index ab6d5fb2ed57..1a19a754d3f4 100644 --- a/code/controllers/subsystem/time_track.dm +++ b/code/controllers/subsystem/time_track.dm @@ -93,7 +93,6 @@ SUBSYSTEM_DEF(time_track) time_dilation_avg_fast = MC_AVERAGE_FAST(time_dilation_avg_fast, time_dilation_current) time_dilation_avg = MC_AVERAGE(time_dilation_avg, time_dilation_avg_fast) time_dilation_avg_slow = MC_AVERAGE_SLOW(time_dilation_avg_slow, time_dilation_avg) - GLOB.glide_size_multiplier = (current_byondtime - last_tick_byond_time) / (current_realtime - last_tick_realtime) else first_run = FALSE last_tick_realtime = current_realtime diff --git a/code/datums/components/scope.dm b/code/datums/components/scope.dm index 2e10f7663a73..5886c73e8154 100644 --- a/code/datums/components/scope.dm +++ b/code/datums/components/scope.dm @@ -35,7 +35,7 @@ stop_zooming(user_mob) return tracker.calculate_params() - if(!length(user_client.keys_held & user_client.movement_keys)) + if(!user_client.intended_direction) user_mob.face_atom(tracker.given_turf) animate(user_client, world.tick_lag, pixel_x = tracker.given_x, pixel_y = tracker.given_y) diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 552827a98acf..24be25cbc784 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -111,6 +111,23 @@ ///can we grab this object? var/cant_grab = FALSE + /// The world.time we started our last glide + var/last_glide_start = 0 + /// The ammount of unaccounted accumulated error between our glide visuals and the world tickrate + var/accumulated_glide_error = 0 + /// The amount of banked (accounted for) error between our visual and world glide rates + var/banked_glide_error = 0 + /// The amount of error accounted for in the initial delay of our glide (based off GLOB.glide_size_multiplier) + var/built_in_glide_error = 0 + /// The world.time at which we assume our next glide will end + var/glide_stopping_time = 0 + /// We are currently tracking our glide animation + var/glide_tracking = FALSE + /// If we should use glide correction + var/use_correction = FALSE + var/mutable_appearance/glide_text + var/debug_glide = FALSE + /mutable_appearance/emissive_blocker /mutable_appearance/emissive_blocker/New() @@ -584,15 +601,59 @@ 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, recursed = FALSE) +GLOBAL_LIST_EMPTY(gliding_atoms) + +/atom/movable/proc/set_glide_size(target = 8, mid_move = FALSE) if (HAS_TRAIT(src, TRAIT_NO_GLIDE)) return SEND_SIGNAL(src, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, target) glide_size = target - - if(!recursed) - for(var/mob/buckled_mob as anything in buckled_mobs) - buckled_mob.set_glide_size(target, TRUE) + // If we're mid move don't reset ourselves yeah? + if(!mid_move || !glide_tracking) + last_glide_start = world.time + glide_stopping_time = world.time + (world.icon_size / target) * world.tick_lag + accumulated_glide_error = 0 + banked_glide_error = 0 + built_in_glide_error = GLOB.glide_size_multi_error + update_glide_text() + if(!glide_tracking && use_correction) + GLOB.gliding_atoms += src + glide_tracking = TRUE + + for(var/mob/buckled_mob as anything in buckled_mobs) + buckled_mob.set_glide_size(target, mid_move = mid_move) + +/atom/movable/proc/update_glide_text() + if(!debug_glide) + return + cut_overlay(glide_text) + glide_text = mutable_appearance(offset_spokesman = src, plane = ABOVE_LIGHTING_PLANE) + glide_text.maptext = "GS: [glide_size]ppt\nMulti: [GLOB.glide_size_multiplier * 100]% \nBIE: [built_in_glide_error]ds \nErr: [accumulated_glide_error]ds" + glide_text.maptext_width = 500 + glide_text.maptext_height = 500 + glide_text.maptext_y = 32 + add_overlay(glide_text) + +/atom/movable/proc/account_for_glide_error(error) + // Intentionally can go negative to handle being overoptimistic about glide rates + accumulated_glide_error += error - built_in_glide_error + if(abs(accumulated_glide_error) < world.tick_lag * 0.5) + update_glide_text() + return + // we're trying to account for random spikes in error while gliding + // So we're gonna use the known GAME tick we want to stop at, + // alongside how much time has visually past to work out + // exactly how fast we need to move to make up that distance + var/game_time_spent = (world.time - last_glide_start) + var/visual_time_spent = game_time_spent + accumulated_glide_error + built_in_glide_error * game_time_spent + var/distance_covered = glide_size * visual_time_spent + var/distance_remaining = world.icon_size - distance_covered + var/game_time_remaining = (glide_stopping_time - world.time) + built_in_glide_error += accumulated_glide_error / game_time_remaining + accumulated_glide_error = 0 + set_glide_size(clamp((((distance_remaining) / max((game_time_remaining) / world.tick_lag, 1))), MIN_GLIDE_SIZE, MAX_GLIDE_SIZE), mid_move = TRUE) + + update_glide_text() /** * meant for movement with zero side effects. only use for objects that are supposed to move "invisibly" (like camera mobs or ghosts) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 99471bc7ddd6..bff3888e8e5a 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -248,6 +248,9 @@ var/list/keys_held = list() /// A buffer for combinations such of modifiers + keys (ex: CtrlD, AltE, ShiftT). Format: `"key"` -> `"combo"` (ex: `"D"` -> `"CtrlD"`) var/list/key_combos_held = list() + a/// The direction we WANT to move, based off our keybinds + /// Will be udpated to be the actual direction later on + var/intended_direction = NONE /* ** These next two vars are to apply movement for keypresses and releases made while move delayed. ** Because discarding that input makes the game less responsive. diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 42d1e5e67ad1..b058809c7c79 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -1180,6 +1180,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[msay]") else winset(src, "default-[REF(key)]", "parent=default;name=[key];command=") + calculate_move_dir() /client/proc/change_view(new_size) if (isnull(new_size)) diff --git a/code/modules/keybindings/bindings_atom.dm b/code/modules/keybindings/bindings_atom.dm index 6dadcd5768ee..2301865228ff 100644 --- a/code/modules/keybindings/bindings_atom.dm +++ b/code/modules/keybindings/bindings_atom.dm @@ -1,13 +1,20 @@ // You might be wondering why this isn't client level. If focus is null, we don't want you to move. // Only way to do that is to tie the behavior into the focus's keyLoop(). -/atom/movable/keyLoop(client/user) - var/movement_dir = NONE - for(var/_key in user?.keys_held) - movement_dir = movement_dir | user.movement_keys[_key] - if(user?.next_move_dir_add) - movement_dir |= user.next_move_dir_add - if(user?.next_move_dir_sub) +/atom/movable/keyLoop(client/user, gondor_calls_for_aid = FALSE) + // Clients don't go null randomly. They do go null unexpectedly though, when they're poked in particular ways + // keyLoop is called by a for loop over mobs. We're guarenteed that all the mobs have clients at the START + // But the move of one mob might poke the client of another, so we do this + if(gondor_calls_for_aid) + user = src + if(!user) + return FALSE + var/movement_dir = user.intended_direction | user.next_move_dir_add + // If we're not movin anywhere, we aren't movin anywhere + // Safe because nothing adds to movement_dir after this moment + if(!movement_dir) + return FALSE + if(user.next_move_dir_sub) movement_dir &= ~user.next_move_dir_sub // Sanity checks in case you hold left and right and up to make sure you only go up if((movement_dir & NORTH) && (movement_dir & SOUTH)) @@ -15,14 +22,25 @@ if((movement_dir & EAST) && (movement_dir & WEST)) movement_dir &= ~(EAST|WEST) - if(user && movement_dir) //If we're not moving, don't compensate, as byond will auto-fill dir otherwise + if(!gondor_calls_for_aid && user.dir != NORTH && movement_dir) //If we're not moving, don't compensate, as byond will auto-fill dir otherwise movement_dir = turn(movement_dir, -dir2angle(user.dir)) //By doing this we ensure that our input direction is offset by the client (camera) direction //turn without moving while using the movement lock key, unless something wants to ignore it and move anyway - if(user?.movement_locked && !(SEND_SIGNAL(src, COMSIG_MOVABLE_KEYBIND_FACE_DIR, movement_dir) & COMSIG_IGNORE_MOVEMENT_LOCK)) + if(user.movement_locked && !(SEND_SIGNAL(src, COMSIG_MOVABLE_KEYBIND_FACE_DIR, movement_dir) & COMSIG_IGNORE_MOVEMENT_LOCK)) keybind_face_direction(movement_dir) - else - user?.Move(get_step(src, movement_dir), movement_dir) + else if(gondor_calls_for_aid) + var/mob/mob_src = src + mob_src.bullshit_hell(get_step(src, movement_dir), movement_dir) + return !!movement_dir + // Null check cause of the signal above + else if(user) + user.Move(get_step(src, movement_dir), movement_dir) return !!movement_dir //true if there was actually any player input return FALSE + +/client/proc/calculate_move_dir() + var/movement_dir = NONE + for(var/_key in keys_held) + movement_dir |= movement_keys[_key] + intended_direction = movement_dir diff --git a/code/modules/keybindings/bindings_client.dm b/code/modules/keybindings/bindings_client.dm index e4d002142827..bef16d57fec6 100644 --- a/code/modules/keybindings/bindings_client.dm +++ b/code/modules/keybindings/bindings_client.dm @@ -47,9 +47,10 @@ //the time a key was pressed isn't actually used anywhere (as of 2019-9-10) but this allows easier access usage/checking keys_held[_key] = world.time - if(!movement_locked) - var/movement = movement_keys[_key] - if(!(next_move_dir_sub & movement)) + var/movement = movement_keys[_key] + if(movement) + calculate_move_dir() + if(!movement_locked && !(next_move_dir_sub & movement)) next_move_dir_add |= movement // Client-level keybindings are ones anyone should be able to do at any time @@ -94,9 +95,10 @@ keys_held -= _key - if(!movement_locked) - var/movement = movement_keys[_key] - if(!(next_move_dir_add & movement)) + var/movement = movement_keys[_key] + if(movement) + calculate_move_dir() + if(!movement_locked && !(next_move_dir_add & movement)) next_move_dir_sub |= movement // We don't do full key for release, because for mod keys you diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 339acb74409a..cadad66da6d5 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -22,6 +22,7 @@ see_in_dark = 1e6 // A list of factions that this mob is currently in, for hostile mob targeting, amongst other things faction = list(FACTION_NEUTRAL) + use_correction = TRUE /// The current client inhabiting this mob. Managed by login/logout /// This exists so we can do cleanup in logout for occasions where a client was transfere rather then destroyed /// We need to do this because the mob on logout never actually has a reference to client diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index 2f08bbc8ab2f..4ec7b30bc05f 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -163,6 +163,109 @@ if(P && !ismob(P) && P.density) mob.setDir(turn(mob.dir, 180)) +/mob + var/next_move_dir_add = NONE + var/next_move_dir_sub = NONE + var/input_move_delay = 0 + var/intended_direction = EAST + var/movement_locked = FALSE + +/mob/proc/begin_the_infinimove() + SSinput.moving_mobs |= src + +/mob/Destroy() + SSinput.moving_mobs -= src + return ..() + + +/mob/proc/bullshit_hell(new_loc, direct) + if(world.time < input_move_delay) //do not move anything ahead of this check please + return FALSE + next_move_dir_add = NONE + next_move_dir_sub = NONE + var/old_move_delay = input_move_delay + input_move_delay = world.time + world.tick_lag //this is here because Move() can now be called mutiple times per tick + if(!direct || !new_loc) + return FALSE + if(!loc) + return FALSE + if(HAS_TRAIT(src, TRAIT_NO_TRANSFORM)) + return FALSE //This is sorta the goto stop mobs from moving trait + if(SEND_SIGNAL(src, COMSIG_MOB_CLIENT_PRE_LIVING_MOVE, new_loc, direct) & COMSIG_MOB_CLIENT_BLOCK_PRE_LIVING_MOVE) + return FALSE + + var/mob/living/L = src //Already checked for isliving earlier + if(Process_Grab()) //are we restrained by someone's grip? + return + + if(!(L.mobility_flags & MOBILITY_MOVE)) + return FALSE + + if(ismovable(loc)) //Inside an object, tell it we moved + var/atom/loc_atom = loc + return loc_atom.relaymove(src, direct) + + if(!Process_Spacemove(direct)) + return FALSE + + if(SEND_SIGNAL(src, COMSIG_MOB_CLIENT_PRE_MOVE, args) & COMSIG_MOB_CLIENT_BLOCK_PRE_MOVE) + return FALSE + + //We are now going to move + var/add_delay = cached_multiplicative_slowdown + var/new_glide_size = DELAY_TO_GLIDE_SIZE(add_delay * ( (NSCOMPONENT(direct) && EWCOMPONENT(direct)) ? sqrt(2) : 1 ) ) + set_glide_size(new_glide_size) // set it now in case of pulled objects + //If the move was recent, count using old_move_delay + //We want fractional behavior and all + if(old_move_delay + world.tick_lag > world.time) + //Yes this makes smooth movement stutter if add_delay is too fractional + //Yes this is better then the alternative + input_move_delay = old_move_delay + else + input_move_delay = world.time + + //Basically an optional override for our glide size + //Sometimes you want to look like you're moving with a delay you don't actually have yet + var/old_dir = dir + // Move here + var/old_loc = loc + Move(new_loc, direct) + + if((direct & (direct - 1)) && loc == new_loc) //moved diagonally successfully + add_delay *= sqrt(2) + + var/after_glide = DELAY_TO_GLIDE_SIZE(add_delay) + + set_glide_size(after_glide) + + input_move_delay += add_delay + if(.) // If mob is null here, we deserve the runtime + throwing?.finalize(FALSE) + + // At this point we've moved the client's attached mob. This is one of the only ways to guess that a move was done + // as a result of player input and not because they were pulled or any other magic. + SEND_SIGNAL(src, COMSIG_MOB_CLIENT_MOVED, direct, old_dir) + if(old_loc == loc) + intended_direction = REVERSE_DIR(intended_direction) + + var/atom/movable/P = pulling + if(P && !ismob(P) && P.density) + setDir(REVERSE_DIR(dir)) + +/mob/proc/Process_Grab() + if(!pulledby) + return FALSE + if(pulledby == pulling && pulledby.grab_state == GRAB_PASSIVE) //Don't autoresist passive grabs if we're grabbing them too. + return FALSE + if(HAS_TRAIT(src, TRAIT_INCAPACITATED)) + COOLDOWN_START(src, input_move_delay, 1 SECONDS) + return TRUE + else if(HAS_TRAIT(src, TRAIT_RESTRAINED)) + COOLDOWN_START(src, input_move_delay, 1 SECONDS) + to_chat(src, span_warning("You're restrained! You can't move!")) + return TRUE + return resist_grab(TRUE) + /** * Checks to see if you're being grabbed and if so attempts to break it *