Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Glide size is now determined by cpu usage. #4146

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion code/__DEFINES/movement.dm
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@

/// 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.
/// The division result is given a min value of 1 to prevent obscenely slow glide sizes from being set
/// 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)
Expand Down
2 changes: 1 addition & 1 deletion code/__DEFINES/perf_test.dm
Original file line number Diff line number Diff line change
@@ -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;\
}
Expand Down
94 changes: 94 additions & 0 deletions code/controllers/master.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
GLOB.cpu_index = WRAP(cpu_index + 1, 1, CPU_SIZE + 1)
WRAP_UP(GLOB.cpu_index, CPU_SIZE + 1)

double check that it's the same tho, just to be 100% sure

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would need to run a few number tests but I think WRAP_UP would be different



/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))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if(world.time >= trouble.glide_stopping_time || QDELETED(trouble))
if(world.time >= trouble.glide_stopping_time || QDELING(trouble))

QDELETED(trouble) is just (isnull(trouble) || QDELING(trouble)) - and we'd runtime here anyways if trouble is null.

GLOB.gliding_atoms -= trouble
trouble.glide_tracking = FALSE
continue
trouble.account_for_glide_error(error)
7 changes: 5 additions & 2 deletions code/controllers/subsystem.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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++
Expand Down
3 changes: 3 additions & 0 deletions code/controllers/subsystem/input.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down
1 change: 0 additions & 1 deletion code/controllers/subsystem/time_track.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion code/datums/components/scope.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
71 changes: 66 additions & 5 deletions code/game/atoms_movable.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions code/modules/client/client_defines.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions code/modules/client/client_procs.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
40 changes: 29 additions & 11 deletions code/modules/keybindings/bindings_atom.dm
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
// 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))
movement_dir &= ~(NORTH|SOUTH)
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
14 changes: 8 additions & 6 deletions code/modules/keybindings/bindings_client.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions code/modules/mob/mob_defines.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading