diff --git a/citadel.dme b/citadel.dme index 557b0fe859d5..e8292eaa5c98 100644 --- a/citadel.dme +++ b/citadel.dme @@ -1033,9 +1033,9 @@ #include "code\game\atoms\movement.dm" #include "code\game\atoms\say.dm" #include "code\game\atoms\vv.dm" +#include "code\game\atoms\movable\movable-movement.dm" #include "code\game\atoms\movable\movable-throw.dm" #include "code\game\atoms\movable\movable.dm" -#include "code\game\atoms\movable\movement.dm" #include "code\game\atoms\movable\pulling.dm" #include "code\game\atoms\movable\vv.dm" #include "code\game\atoms\movable\special\overlay.dm" @@ -2202,6 +2202,7 @@ #include "code\modules\admin\view_variables\topic_list.dm" #include "code\modules\admin\view_variables\view_variables.dm" #include "code\modules\ai\ai_pathing.dm" +#include "code\modules\ai\ai_tracking.dm" #include "code\modules\ai\holders\ai_holder-movement.dm" #include "code\modules\ai\holders\ai_holder-pathfinding.dm" #include "code\modules\ai\holders\ai_holder-scheduling.dm" diff --git a/code/__HELPERS/lists/string.dm b/code/__HELPERS/lists/string.dm index 2acdf2a131b7..f19028b98fad 100644 --- a/code/__HELPERS/lists/string.dm +++ b/code/__HELPERS/lists/string.dm @@ -1,10 +1,22 @@ /** * Returns a list in plain english as a string. * - * * input - (optional) list or null; if null, we use empty_text + * @params + * * input - the list to interpolate into strings + * * nothing_text - the text to emit if the list is empty + * * and_text - the text to emit on the last element instead of a comma + * * comma_text - the glue between elements + * * final_comma_text - the glue between the last two elements; usually empty for use with 'and_text' + * * limit - limit the entries processed. if the limit was reached, the last two elements will not use the usual glue text. + * * limit_text - text to append at the end if we were limited. defaults to "..." */ -/proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "" ) +/proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "", limit, limit_text = "...") var/total = length(input) + var/limited = FALSE + if(!isnull(limit)) + if(total > limit) + total = limit + limited = TRUE if (!total) return "[nothing_text]" else if (total == 1) @@ -15,13 +27,13 @@ var/output = "" var/index = 1 while (index < total) - if (index == total - 1) + if ((index == (total - 1)) && !limited) comma_text = final_comma_text output += "[input[index]][comma_text]" index++ - return "[output][and_text][input[index]]" + return "[output][limited ? "" : and_text][input[index]][limited? limit_text : ""]" /** * Removes a string from a list. diff --git a/code/controllers/subsystem/air.dm b/code/controllers/subsystem/air.dm index 8609e1f99b1a..a7b59ec711b0 100644 --- a/code/controllers/subsystem/air.dm +++ b/code/controllers/subsystem/air.dm @@ -137,7 +137,7 @@ SUBSYSTEM_DEF(air) var/timer if(!resumed) if(LAZYLEN(currentrun) != 0) - stack_trace("Currentrun not empty before processing cycle when it should be. [english_list(currentrun)]") + stack_trace("Currentrun not empty before processing cycle when it should be. [english_list(currentrun, limit = 5)]") currentrun = list() if(current_step != null) stack_trace("current_step before processing cycle was [current_step] instead of null") @@ -152,7 +152,7 @@ SUBSYSTEM_DEF(air) // Okay, we're done! Woo! Got thru a whole SSair cycle! if(LAZYLEN(currentrun) != 0) - stack_trace("Currentrun not empty after processing cycle when it should be. [english_list(currentrun.Copy(1, min(currentrun.len, 5)))]") + stack_trace("Currentrun not empty after processing cycle when it should be. [english_list(currentrun, limit = 5)]") currentrun = null if(current_step != SSAIR_DONE) stack_trace("current_step after processing cycle was [current_step] instead of [SSAIR_DONE]") diff --git a/code/game/atoms/movable/movement.dm b/code/game/atoms/movable/movable-movement.dm similarity index 87% rename from code/game/atoms/movable/movement.dm rename to code/game/atoms/movable/movable-movement.dm index a0d0b0c016bf..412d21532059 100644 --- a/code/game/atoms/movable/movement.dm +++ b/code/game/atoms/movable/movable-movement.dm @@ -208,77 +208,96 @@ if(glide_size_override) set_glide_size(glide_size_override, FALSE) - if(loc != newloc) - if (!(direct & (direct - 1))) //Cardinal move - . = ..() - else //Diagonal move, split it into cardinal moves - moving_diagonally = FIRST_DIAG_STEP - var/first_step_dir - // The `&& moving_diagonally` checks are so that a force_move taking - // place due to a Crossed, Bumped, etc. call will interrupt - // the second half of the diagonal movement, or the second attempt - // at a first half if step() fails because we hit something. - if (direct & NORTH) - if (direct & EAST) - if (step(src, NORTH) && moving_diagonally) - first_step_dir = NORTH - moving_diagonally = SECOND_DIAG_STEP - . = step(src, EAST) - else if (moving_diagonally && step(src, EAST)) - first_step_dir = EAST - moving_diagonally = SECOND_DIAG_STEP - . = step(src, NORTH) - else if (direct & WEST) - if (step(src, NORTH) && moving_diagonally) - first_step_dir = NORTH - moving_diagonally = SECOND_DIAG_STEP - . = step(src, WEST) - else if (moving_diagonally && step(src, WEST)) - first_step_dir = WEST - moving_diagonally = SECOND_DIAG_STEP - . = step(src, NORTH) - else if (direct & SOUTH) - if (direct & EAST) - if (step(src, SOUTH) && moving_diagonally) - first_step_dir = SOUTH - moving_diagonally = SECOND_DIAG_STEP - . = step(src, EAST) - else if (moving_diagonally && step(src, EAST)) - first_step_dir = EAST - moving_diagonally = SECOND_DIAG_STEP - . = step(src, SOUTH) - else if (direct & WEST) - if (step(src, SOUTH) && moving_diagonally) - first_step_dir = SOUTH - moving_diagonally = SECOND_DIAG_STEP - . = step(src, WEST) - else if (moving_diagonally && step(src, WEST)) - first_step_dir = WEST - moving_diagonally = SECOND_DIAG_STEP - . = step(src, SOUTH) - if(moving_diagonally == SECOND_DIAG_STEP) - if(!.) - setDir(first_step_dir) - else if (!inertia_moving) - inertia_next_move = world.time + inertia_move_delay - newtonian_move(direct) - moving_diagonally = NOT_IN_DIAG_STEP - --in_move - return - else // trying to move to the same place + var/time_since_last_move = world.time - last_move + last_move = world.time + + // trying to move to the same place + if(loc == newloc) if(direct) last_move_dir = direct setDir(direct) --in_move - return TRUE // not moving is technically success + ai_tracking?.track_movement(time_since_last_move, NONE) + // not moving is technically success + return TRUE + + //Cardinal move + if (!(direct & (direct - 1))) + . = ..() + //Diagonal move, split it into cardinal moves + else + moving_diagonally = FIRST_DIAG_STEP + var/first_step_dir + // The `&& moving_diagonally` checks are so that a force_move taking + // place due to a Crossed, Bumped, etc. call will interrupt + // the second half of the diagonal movement, or the second attempt + // at a first half if step() fails because we hit something. + if (direct & NORTH) + if (direct & EAST) + if (step(src, NORTH) && moving_diagonally) + first_step_dir = NORTH + moving_diagonally = SECOND_DIAG_STEP + . = step(src, EAST) + else if (moving_diagonally && step(src, EAST)) + first_step_dir = EAST + moving_diagonally = SECOND_DIAG_STEP + . = step(src, NORTH) + else if (direct & WEST) + if (step(src, NORTH) && moving_diagonally) + first_step_dir = NORTH + moving_diagonally = SECOND_DIAG_STEP + . = step(src, WEST) + else if (moving_diagonally && step(src, WEST)) + first_step_dir = WEST + moving_diagonally = SECOND_DIAG_STEP + . = step(src, NORTH) + else if (direct & SOUTH) + if (direct & EAST) + if (step(src, SOUTH) && moving_diagonally) + first_step_dir = SOUTH + moving_diagonally = SECOND_DIAG_STEP + . = step(src, EAST) + else if (moving_diagonally && step(src, EAST)) + first_step_dir = EAST + moving_diagonally = SECOND_DIAG_STEP + . = step(src, SOUTH) + else if (direct & WEST) + if (step(src, SOUTH) && moving_diagonally) + first_step_dir = SOUTH + moving_diagonally = SECOND_DIAG_STEP + . = step(src, WEST) + else if (moving_diagonally && step(src, WEST)) + first_step_dir = WEST + moving_diagonally = SECOND_DIAG_STEP + . = step(src, SOUTH) + if(moving_diagonally == SECOND_DIAG_STEP) + if(!.) + setDir(first_step_dir) + else if (!inertia_moving) + inertia_next_move = world.time + inertia_move_delay + newtonian_move(direct) + moving_diagonally = NOT_IN_DIAG_STEP + --in_move + // track movement if we're no longer in a move; this way this fires only once for diag steps + if(ai_tracking && !in_move) + ai_tracking.track_movement(time_since_last_move, . ? direct : (moving_diagonally == SECOND_DIAG_STEP ? first_step_dir : NONE)) + return if(!loc || (loc == oldloc && oldloc != newloc)) last_move_dir = NONE --in_move - return + // track movement if we're no longer in a move; this way this fires only once for diag steps + if(ai_tracking && !in_move) + ai_tracking.track_movement(time_since_last_move, NONE) + return FALSE - if(. && has_buckled_mobs() && !handle_buckled_mob_movement(loc, direct, glide_size_override)) //movement failed due to buckled mob(s) + //movement failed due to buckled mob(s) + if(. && has_buckled_mobs() && !handle_buckled_mob_movement(loc, direct, glide_size_override)) + last_move_dir = NONE --in_move + // track movement if we're no longer in a move; this way this fires only once for diag steps + if(ai_tracking && !in_move) + ai_tracking.track_movement(time_since_last_move, NONE) return FALSE if(.) @@ -311,6 +330,10 @@ --in_move + // track movement if we're no longer in a move; this way this fires only once for diag steps + if(ai_tracking && !in_move) + ai_tracking.track_movement(time_since_last_move, direct) + // legacy move_speed = world.time - l_move_time l_move_time = world.time @@ -579,6 +602,9 @@ /atom/movable/proc/doMove(atom/destination) . = FALSE + // completely reset ai tracking + ai_tracking?.reset_movement() + ++in_move var/atom/oldloc = loc diff --git a/code/game/atoms/movable/movable-throw.dm b/code/game/atoms/movable/movable-throw.dm index f329f1ab5bd2..742afd66cd8f 100644 --- a/code/game/atoms/movable/movable-throw.dm +++ b/code/game/atoms/movable/movable-throw.dm @@ -186,7 +186,7 @@ // user momentum var/user_speed = L.movement_delay() // 1 decisecond of margin - if(L.last_move_dir && (L.last_move_time >= (world.time - user_speed + 1))) + if(L.last_move_dir && (L.last_self_move >= (world.time - user_speed + 1))) user_speed = max(user_speed, world.tick_lag) // convert to tiles per **decisecond** user_speed = 1/user_speed diff --git a/code/game/atoms/movable/movable.dm b/code/game/atoms/movable/movable.dm index d2e4a5f95cf7..9be084a15cc5 100644 --- a/code/game/atoms/movable/movable.dm +++ b/code/game/atoms/movable/movable.dm @@ -25,6 +25,8 @@ //* AI Holders /// AI holder bound to us var/datum/ai_holder/ai_holder + /// AI tracking datum. Handled by procs in [code/modules/ai/ai_tracking.dm]. + var/datum/ai_tracking/ai_tracking //? Intrinsics /// movable flags - see [code/__DEFINES/_flags/atoms.dm] @@ -36,28 +38,39 @@ /// Set this to TRUE if we are not a [TILE_MOVER]! var/pixel_movement = FALSE /// Whatever we're pulling. + /// /// * this variable is not visible and should not be edited in the map editor. var/tmp/atom/movable/pulling /// Who's currently pulling us + /// /// * this variable is not visible and should not be edited in the map editor. var/tmp/atom/movable/pulledby /// If false makes [CanPass][/atom/proc/CanPass] call [CanPassThrough][/atom/movable/proc/CanPassThrough] on this type instead of using default behaviour + /// /// * this variable is not visible and should not be edited in the map editor. var/tmp/generic_canpass = TRUE /// Pass flags. var/pass_flags = NONE /// movement calls we're in + /// /// * this variable is not visible and should not be edited in the map editor. var/tmp/in_move = 0 /// a direction, or null + /// /// * this variable is not visible and should not be edited in the map editor. var/tmp/moving_diagonally = NOT_IN_DIAG_STEP /// attempt to resume grab after moving instead of before. This is what atom/movable is pulling us during move-from-pulling. + /// /// * this variable is not visible and should not be edited in the map editor. var/tmp/atom/movable/moving_from_pull /// Direction of our last move. + /// /// * this variable is not visible and should not be edited in the map editor. var/tmp/last_move_dir = NONE + /// world.time of our last move + /// + /// * this variable is not visible and should not be edited in the map editor. + var/tmp/last_move /// Our default glide_size. Null to use global default. var/default_glide_size /// Movement types, see [code/__DEFINES/flags/movement.dm] diff --git a/code/modules/ai/ai_pathing.dm b/code/modules/ai/ai_pathing.dm index 9fe14ef0f440..aa949e6b6f3f 100644 --- a/code/modules/ai/ai_pathing.dm +++ b/code/modules/ai/ai_pathing.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// /** * Pathfinding results. diff --git a/code/modules/ai/ai_tracking.dm b/code/modules/ai/ai_tracking.dm new file mode 100644 index 000000000000..fc962abb4066 --- /dev/null +++ b/code/modules/ai/ai_tracking.dm @@ -0,0 +1,156 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 Citadel Station Developers *// + +/** + * Datum to track a movable entity for things like motion and other predictive qualities. + * + * todo: either this or spatial grid signals need to allow for signal on projectile fire / attack / etc. + */ +/datum/ai_tracking + //* System *// + /// last time we were requested + var/last_requested + /// time since last requested we should delete at + var/gc_timeout = 30 SECONDS + /// timerid + var/gc_timerid + + //* Movement *// + + // last movement record + var/movement_record_last + + // basic vel x / y immediate ; tracks within a second or two. only useful for current velocity. // + + /// in tiles / second + var/imm_vel_x = 0 + /// in tiles / second + var/imm_vel_y = 0 + + var/static/imm_vel_multiplier = 0.1 + + // average vel x / y ; uses a fast rolling average. use to suppress attempts at baiting shots. // + + /// in tiles / second + var/fast_vel_x = 0 + /// in tiles / second + var/fast_vel_y = 0 + + var/static/fast_vel_multiplier = 2.5 + +/datum/ai_tracking/Destroy() + if(gc_timerid) + deltimer(gc_timerid) + return ..() + +/** + * Notifies us that we've been requested. + */ +/datum/ai_tracking/proc/keep_alive() + src.last_requested = world.time + +//* Movement *// + +/** + * Tracks movement state + * + * todo: this doesn't support forced movement or anything like that that is faster than a tile second + * + * * time - time since last move + * * dir - direction of move. if it's just a Move() or otherwie standing still, this is NONE. + */ +/datum/ai_tracking/proc/track_movement(time, dir) + var/elapsed = max(world.time - movement_record_last, world.tick_lag) + // flushing changes last record + flush_movement() + + var/sx + var/sy + switch(dir) + if(NORTH) + sy = 1 + if(SOUTH) + sy = -1 + if(EAST) + sx = 1 + if(WEST) + sx = -1 + if(NORTHWEST) + sy = 1 + sx = -1 + if(NORTHEAST) + sy = 1 + sx = 1 + if(SOUTHEAST) + sy = -1 + sx = 1 + if(SOUTHWEST) + sy = -1 + sx = -1 + + /// tiles / ds + var/immediate_speed = 10 / time + var/imm_vel_inverse = 1 - imm_vel_multiplier + imm_vel_x = min(immediate_speed, imm_vel_x * imm_vel_multiplier + immediate_speed * imm_vel_inverse * sx) + imm_vel_y = min(immediate_speed, imm_vel_y * imm_vel_multiplier + immediate_speed * imm_vel_inverse * sy) + + var/fast_new_multiplier = clamp(fast_vel_multiplier * (time / 10), 0, 1) + var/fast_old_multiplier = clamp(1 - fast_new_multiplier, 0, 1) + fast_vel_x = (fast_vel_x) * fast_old_multiplier + imm_vel_x * fast_new_multiplier + fast_vel_y = (fast_vel_y) * fast_old_multiplier + imm_vel_y * fast_new_multiplier + +/** + * Tells us to completely drop movement state. + */ +/datum/ai_tracking/proc/reset_movement() + movement_record_last = world.time + imm_vel_x = imm_vel_y = 0 + fast_vel_x = fast_vel_y = 0 + +/** + * Tells us to flush movement state. + * + * * Always call this before checking movement vars. + */ +/datum/ai_tracking/proc/flush_movement() + if(movement_record_last == world.time) + return + + var/elapsed = world.time - movement_record_last + movement_record_last = world.time + + // penalize immediate speed + imm_vel_x = min(imm_vel_x, 10 / elapsed) + imm_vel_y = min(imm_vel_y, 10 / elapsed) + // penalize fast speed + fast_vel_x = min(fast_vel_x, 20 / elapsed) + fast_vel_y = min(fast_vel_y, 20 / elapsed) + +// todo: get projected tile location (): list(center, radius) +// todo: calculate immediate intercept (atom/source, speed (pixels per second)): list(x, y) +// todo: calculate fast intercept (atom/source, speed (pixels per second)): list(x, y) + +//* /atom/movable API *// + +/** + * Requests our AI tracking datum. + * + * * Will make one if it's not there. + * * Will keep an existing one alive. + */ +/atom/movable/proc/request_ai_tracking() + RETURN_TYPE(/datum/ai_tracking) + if(src.ai_tracking) + return src.ai_tracking + src.ai_tracking = new + src.ai_tracking.keep_alive() + src.ai_tracking.gc_timerid = addtimer(CALLBACK(src, PROC_REF(expire_ai_tracking)), src.ai_tracking.gc_timeout, TIMER_LOOP | TIMER_STOPPABLE) + return src.ai_tracking + +/atom/movable/proc/expire_ai_tracking() + if(!ai_tracking) + return + if(ai_tracking.last_requested + ai_tracking.gc_timeout > world.time) + return + deltimer(ai_tracking.gc_timerid) + QDEL_NULL(ai_tracking) diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index f8d9fc7ab7f9..3c939862f217 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -79,9 +79,9 @@ /// Next world.time we will be able to move. var/move_delay = 0 /// Last world.time we finished a normal, non relay/intercepted move - var/last_move_time = 0 + var/last_self_move = 0 /// Last world.time we turned in our spot without moving (see: facing directions) - var/last_turn = 0 + var/last_self_turn = 0 /// Tracks if we have gravity from environment right now. var/in_gravity diff --git a/code/modules/mob/movement.dm b/code/modules/mob/movement.dm index c02dd0d4d7ec..e9a8621df515 100644 --- a/code/modules/mob/movement.dm +++ b/code/modules/mob/movement.dm @@ -352,14 +352,14 @@ // preserve momentum: for non-evenly-0.5-multiple movespeeds (HELLO, DIAGONAL MOVES), // we need to store how much we're cheated out of our tick and carry it through // make an intelligent guess at if they're trying to keep moving, tho! - if(mob.last_move_time > (world.time - add_delay * 1.25)) + if(mob.last_self_move > (world.time - add_delay * 1.25)) mob.move_delay = old_delay + add_delay else mob.move_delay = world.time + add_delay SMOOTH_GLIDE_SIZE(mob, DELAY_TO_GLIDE_SIZE(add_delay)) - mob.last_move_time = world.time + mob.last_self_move = world.time /mob/proc/SelfMove(turf/T, dir) in_selfmove = TRUE @@ -537,7 +537,7 @@ * * we are not restrained */ /mob/proc/canface() - if(world.time <= last_turn) + if(world.time <= last_self_turn) return FALSE if(stat == DEAD || stat == UNCONSCIOUS) return FALSE @@ -554,7 +554,7 @@ if(!canface()) return FALSE setDir(EAST) - last_turn = world.time + last_self_turn = world.time return TRUE ///Hidden verb to turn west @@ -564,7 +564,7 @@ if(!canface()) return FALSE setDir(WEST) - last_turn = world.time + last_self_turn = world.time return TRUE ///Hidden verb to turn north @@ -574,7 +574,7 @@ if(!canface()) return FALSE setDir(NORTH) - last_turn = world.time + last_self_turn = world.time return TRUE ///Hidden verb to turn south @@ -584,7 +584,7 @@ if(!canface()) return FALSE setDir(SOUTH) - last_turn = world.time + last_self_turn = world.time return TRUE //! Pixel Shifting diff --git a/code/modules/movespeed/movespeed_modifier.dm b/code/modules/movespeed/movespeed_modifier.dm index 0cc2c6291271..97be21d22456 100644 --- a/code/modules/movespeed/movespeed_modifier.dm +++ b/code/modules/movespeed/movespeed_modifier.dm @@ -290,14 +290,14 @@ GLOBAL_LIST_EMPTY(movespeed_modification_cache) cached_multiplicative_slowdown = min(., 10 / MOVESPEED_ABSOLUTE_MINIMUM_TILES_PER_SECOND) if(!client) return - var/diff = (last_move_time - move_delay) - cached_multiplicative_slowdown + var/diff = (last_self_move - move_delay) - cached_multiplicative_slowdown if(diff > 0) // your delay decreases, "give" the delay back to the client if(move_delay > world.time + 1.5) move_delay -= diff #ifdef SMOOTH_MOVEMENT var/timeleft = world.time - move_delay - var/elapsed = world.time - last_move_time + var/elapsed = world.time - last_self_move var/glide_size_current = glide_size if((timeleft <= 0) || (elapsed > 20)) SMOOTH_GLIDE_SIZE(src, 16, TRUE)