From 1409e4b026f359764ca491fd5f73f646227973e6 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Mon, 21 Nov 2022 23:29:35 -0800 Subject: [PATCH] JPS Optimization (Light Botcode) (#70623) ## About The Pull Request Alright. So. Right now, JPS works like this: ``` code requests path we enter the actual pathfinding pathfinding sleeps when it overruns a tick if it sleeps, it'll then wake up before the mc starts continue ``` This has annoying side effects. Primarily that we have no real control over JPS, we just sorta have to eat its cost. So if there's like 10 different things pathfinding at once, the mc will have no time to do anything. Hell we might even end up eating into maptick's time if the jps work is expensive enough (note the cost of sleeping is not accounted for, and that has overhead) This has happen before, usually when someone makes a lot of bots, and it's really annoying. So then, lets put JPS on a subsystem. That way the MC has control over it. But wait, existing code expects to yield and get back a path list, and that's a sane request. This is solvable, but requires abusing pass by reference lists, and the ability to make callbacks into partials (preinsert arguments into them before they're called, and accept other args later) Because of this, we can now pass callbacks into pathfinders, allowing for async use, rather then JUST yielding. Of note: I've removed the 10 pathfinding datums limit, since ratelimiting like that is handled nicely by the MC. I've also removed the 15 second timeout, since mc yielding would trigger it too often. I'm unsure if this means we don't have exit conditions for pathfinding, need to talk to ryll. (@Ryll-Ryll what happens if jps just like, fails to find a path?) Also of note: I think bots will fire off more then one pathfinding attempt at a time if their first takes too long to complete. This is dumb, why do we do this? Optimizes JPS by more then 40% by removing redundant for(thing in turf) loops, and avoiding making proc calls if objects are non dense. This makes things slightly more fragile, but saves a LOT of time. I think it's worth it, tho talking to mso it might be possible to do better. Maybe I should do a LINDA system style thing. (I did a linda system style thing I fixed it) Optimizes botscanning, fixes bots not seeing things adjacent to them The list of types could be a cached typecache We could inline both checkscan and check_bot check_bot SHOULD NOT BE CALLED ON EVERY OBJECT IN VIEW HOLY SHIT WHY We don't need to process adjacent and the shuffled view separately, it's in fact easier to process them in one block Renames a var Moves bot's pathing images to above most floor objects, so they're visible in maint ## Why It's Good For The Game Speed. Also manuel will stop killing their server by placing 20000 medibots (fucking icebox man every time) ## Changelog :cl: fix: Bots will now "notice" you if you're standing right next to them fix: Bot paths will now draw above things like pipes, rather then below them refactor: Changed how pathfinding paths get generated refactor: Made pathfinding and bot searching significantly faster /:cl: Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com> --- code/__DEFINES/_helpers.dm | 3 + code/__DEFINES/layers.dm | 1 + code/__DEFINES/path.dm | 5 + code/__DEFINES/subsystems.dm | 1 + code/__HELPERS/path.dm | 146 ++++++++++-------- code/__HELPERS/stoplag.dm | 2 - code/controllers/subsystem/pathfinder.dm | 68 ++++---- code/game/atoms.dm | 5 + code/game/objects/structures/plasticflaps.dm | 1 + code/game/turfs/turf.dm | 2 +- .../mob/living/simple_animal/bot/bot.dm | 95 +++++++----- .../mob/living/simple_animal/bot/floorbot.dm | 1 - .../mob/living/simple_animal/parrot.dm | 2 +- tgstation.dme | 1 + 14 files changed, 194 insertions(+), 139 deletions(-) create mode 100644 code/__DEFINES/path.dm diff --git a/code/__DEFINES/_helpers.dm b/code/__DEFINES/_helpers.dm index 08d4b39906e..b9c5e8d7d13 100644 --- a/code/__DEFINES/_helpers.dm +++ b/code/__DEFINES/_helpers.dm @@ -14,6 +14,9 @@ /// subtypesof(), typesof() without the parent path #define subtypesof(typepath) ( typesof(typepath) - typepath ) +/// Until a condition is true, sleep +#define UNTIL(X) while(!(X)) stoplag() + /// Takes a datum as input, returns its ref string, or a cached version of it /// This allows us to cache \ref creation, which ensures it'll only ever happen once per datum, saving string tree time /// It is slightly less optimal then a []'d datum, but the cost is massively outweighed by the potential savings diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index cca6686e99c..8680212a0ba 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -120,6 +120,7 @@ #define GAS_FILTER_LAYER 2.48 #define GAS_PUMP_LAYER 2.49 #define PLUMBING_PIPE_VISIBILE_LAYER 2.495//layer = initial(layer) + ducting_layer / 3333 in atmospherics/handle_layer() to determine order of duct overlap +#define BOT_PATH_LAYER 2.497 #define LOW_OBJ_LAYER 2.5 ///catwalk overlay of /turf/open/floor/plating/catwalk_floor #define CATWALK_LAYER 2.51 diff --git a/code/__DEFINES/path.dm b/code/__DEFINES/path.dm new file mode 100644 index 00000000000..95713c5d36f --- /dev/null +++ b/code/__DEFINES/path.dm @@ -0,0 +1,5 @@ +// Define set that decides how an atom will be scanned for astar things +/// If set, we make the assumption that CanAStarPass() will NEVER return FALSE unless density is true +#define CANASTARPASS_DENSITY 0 +/// If this is set, we bypass density checks and always call the proc +#define CANASTARPASS_ALWAYS_PROC 1 diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 281f682a9c6..ee5d29cd8b2 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -202,6 +202,7 @@ #define FIRE_PRIORITY_NPC 20 #define FIRE_PRIORITY_NPC_MOVEMENT 21 #define FIRE_PRIORITY_NPC_ACTIONS 22 +#define FIRE_PRIORITY_PATHFINDING 23 #define FIRE_PRIORITY_PROCESS 25 #define FIRE_PRIORITY_THROWING 25 #define FIRE_PRIORITY_REAGENTS 26 diff --git a/code/__HELPERS/path.dm b/code/__HELPERS/path.dm index 89f64cf9fb6..08280617e97 100644 --- a/code/__HELPERS/path.dm +++ b/code/__HELPERS/path.dm @@ -7,6 +7,7 @@ /** * This is the proc you use whenever you want to have pathfinding more complex than "try stepping towards the thing". * If no path was found, returns an empty list, which is important for bots like medibots who expect an empty list rather than nothing. + * It will yield until a path is returned, using magic * * Arguments: * * caller: The movable atom that's trying to find the path @@ -19,27 +20,24 @@ * * skip_first: Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break movement for some creatures. * * diagonal_safety: ensures diagonal moves won't use invalid midstep turfs by splitting them into two orthogonal moves if necessary */ -/proc/get_path_to(caller, end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE) - if(!caller || !get_turf(end)) - return +/proc/get_path_to(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE) + var/list/path = list() + // We're guarenteed that list will be the first list in pathfinding_finished's argset because of how callback handles the arguments list + var/datum/callback/await = CALLBACK(GLOBAL_PROC, /proc/pathfinding_finished, path) + if(!SSpathfinder.pathfind(caller, end, max_distance, mintargetdist, id, simulated_only, exclude, skip_first, diagonal_safety, await)) + return null - var/l = SSpathfinder.mobs.getfree(caller) - while(!l) - stoplag(3) - l = SSpathfinder.mobs.getfree(caller) - - var/list/path - var/datum/pathfind/pathfind_datum = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude, diagonal_safety) - path = pathfind_datum.search() - qdel(pathfind_datum) - - SSpathfinder.mobs.found(l) - if(!path) - path = list() - if(length(path) > 0 && skip_first) - path.Cut(1,2) + UNTIL(length(path)) + if(length(path) == 1 && path[1] == null) // It's trash, just hand back null to make it easy + return null return path +/// Uses funny pass by reference bullshit to take the path created by pathfinding, and insert it into a return list +/// We'll be able to use this return list to tell a sleeping proc to continue execution +/proc/pathfinding_finished(list/return_list, list/path) + // We use += here to ensure the list is still pointing at the same thing + return_list += path + /** * A helper macro to see if it's possible to step from the first turf into the second one, minding things like door access and directional windows. * Note that this can only be used inside the [datum/pathfind][pathfind datum] since it uses variables from said datum. @@ -122,10 +120,14 @@ var/simulated_only /// A specific turf we're avoiding, like if a mulebot is being blocked by someone t-posing in a doorway we're trying to get through var/turf/avoid + /// If we should delete the first step in the path or not. Used often because it is just the starting tile + var/skip_first = FALSE /// Ensures diagonal moves won't use invalid midstep turfs by splitting them into two orthogonal moves if necessary var/diagonal_safety = TRUE + /// The callback to invoke when we're done working, passing in the completed var/list/path + var/datum/callback/on_finish -/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid, diagonal_safety) +/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid, skip_first, diagonal_safety, datum/callback/on_finish) src.caller = caller end = get_turf(goal) open = new /datum/heap(/proc/HeapPathWeightCompare) @@ -135,34 +137,46 @@ src.mintargetdist = mintargetdist src.simulated_only = simulated_only src.avoid = avoid + src.skip_first = skip_first src.diagonal_safety = diagonal_safety + src.on_finish = on_finish + +/datum/pathfind/Destroy(force, ...) + . = ..() + SSpathfinder.active_pathing -= src + SSpathfinder.currentrun -= src + if(on_finish) + on_finish.Invoke(null) /** - * search() is the proc you call to kick off and handle the actual pathfinding, and kills the pathfind datum instance when it's done. - * - * If a valid path was found, it's returned as a list. If invalid or cross-z-level params are entered, or if there's no valid path found, we - * return null, which [/proc/get_path_to] translates to an empty list (notable for simple bots, who need empty lists) + * "starts" off the pathfinding, by storing the values this datum will need to work later on + * returns FALSE if it fails to setup properly, TRUE otherwise */ -/datum/pathfind/proc/search() +/datum/pathfind/proc/start() start = get_turf(caller) - if(!start || !end) + if(!start || !get_turf(end)) stack_trace("Invalid A* start or destination") - return + return FALSE if(start.z != end.z || start == end ) //no pathfinding between z levels - return + return FALSE if(max_distance && (max_distance < get_dist(start, end))) //if start turf is farther than max_distance from end turf, no need to do anything - return + return FALSE - //initialization var/datum/jps_node/current_processed_node = new (start, -1, 0, end) open.insert(current_processed_node) sources[start] = start // i'm sure this is fine + return TRUE + +/** + * search_step() is the workhorse of pathfinding. It'll do the searching logic, and will slowly build up a path + * returns TRUE if everything is stable, FALSE if the pathfinding logic has failed, and we need to abort + */ +/datum/pathfind/proc/search_step() + if(QDELETED(caller)) + return FALSE - //then run the main loop while(!open.is_empty() && !path) - if(!caller) - return - current_processed_node = open.pop() //get the lower f_value turf in the open list + var/datum/jps_node/current_processed_node = open.pop() //get the lower f_value turf in the open list if(max_distance && (current_processed_node.number_tiles > max_distance))//if too many steps, don't process that path continue @@ -173,20 +187,37 @@ for(var/scan_direction in list(NORTHEAST, SOUTHEAST, NORTHWEST, SOUTHWEST)) diag_scan_spec(current_turf, scan_direction, current_processed_node) - CHECK_TICK + // Stable, we'll just be back later + if(TICK_CHECK) + return TRUE + return TRUE +/** + * early_exit() is called when something goes wrong in processing, and we need to halt the pathfinding NOW + */ +/datum/pathfind/proc/early_exit() + on_finish.Invoke(null) + on_finish = null + qdel(src) + +/** + * Cleanup pass for the pathfinder. This tidies up the path, and fufills the pathfind's obligations + */ +/datum/pathfind/proc/finished() //we're done! reverse the path to get it from start to finish if(path) for(var/i = 1 to round(0.5 * length(path))) path.Swap(i, length(path) - i + 1) - sources = null - qdel(open) + QDEL_NULL(open) if(diagonal_safety) path = diagonal_movement_safety() - - return path + if(length(path) > 0 && skip_first) + path.Cut(1,2) + on_finish.Invoke(path) + on_finish = null + qdel(src) /// Called when we've hit the goal with the node that represents the last tile, then sets the path var to that path so it can be returned by [datum/pathfind/proc/search] /datum/pathfind/proc/unwind_path(datum/jps_node/unwind_node) @@ -371,6 +402,8 @@ * For seeing if we can actually move between 2 given turfs while accounting for our access and the caller's pass_flags * * Assumes destinantion turf is non-dense - check and shortcircuit in code invoking this proc to avoid overhead. + * Makes some other assumptions, such as assuming that unless declared, non dense objects will not block movement. + * It's fragile, but this is VERY much the most expensive part of JPS, so it'd better be fast * * Arguments: * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach @@ -378,7 +411,7 @@ * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space? * * no_id: When true, doors with public access will count as impassible */ -/turf/proc/LinkBlockedWithAccess(turf/destination_turf, caller, ID, no_id = FALSE) +/turf/proc/LinkBlockedWithAccess(turf/destination_turf, atom/movable/caller, ID, no_id = FALSE) if(destination_turf.x != x && destination_turf.y != y) //diagonal var/in_dir = get_dir(destination_turf,src) // eg. northwest (1+8) = 9 (00001001) var/first_step_direction_a = in_dir & 3 // eg. north (1+8)&3 (0000 0011) = 1 (0000 0001) @@ -386,11 +419,10 @@ for(var/first_step_direction in list(first_step_direction_a,first_step_direction_b)) var/turf/midstep_turf = get_step(destination_turf,first_step_direction) - var/way_blocked = midstep_turf.density || LinkBlockedWithAccess(midstep_turf,caller,ID, no_id = no_id) || midstep_turf.LinkBlockedWithAccess(destination_turf,caller,ID, no_id = no_id) + var/way_blocked = midstep_turf.density || LinkBlockedWithAccess(midstep_turf, caller, ID, no_id) || midstep_turf.LinkBlockedWithAccess(destination_turf, caller, ID, no_id) if(!way_blocked) return FALSE return TRUE - var/actual_dir = get_dir(src, destination_turf) /// These are generally cheaper than looping contents so they go first @@ -400,35 +432,27 @@ // if(destination_turf.density) // return TRUE if(TURF_PATHING_PASS_PROC) - if(!destination_turf.CanAStarPass(ID, actual_dir , caller, no_id = no_id)) + if(!destination_turf.CanAStarPass(ID, actual_dir, caller, no_id)) return TRUE if(TURF_PATHING_PASS_NO) return TRUE + var/static/list/directional_blocker_cache = typecacheof(list(/obj/structure/window, /obj/machinery/door/window, /obj/structure/railing, /obj/machinery/door/firedoor/border_only)) // Source border object checks - for(var/obj/structure/window/iter_window in src) - if(!iter_window.CanAStarPass(ID, actual_dir, no_id = no_id)) - return TRUE - - for(var/obj/machinery/door/window/iter_windoor in src) - if(!iter_windoor.CanAStarPass(ID, actual_dir, no_id = no_id)) - return TRUE - - for(var/obj/structure/railing/iter_rail in src) - if(!iter_rail.CanAStarPass(ID, actual_dir, no_id = no_id)) - return TRUE - - for(var/obj/machinery/door/firedoor/border_only/firedoor in src) - if(!firedoor.CanAStarPass(ID, actual_dir, no_id = no_id)) + for(var/obj/border in src) + if(!directional_blocker_cache[border.type]) + continue + if(!border.density && border.can_astar_pass == CANASTARPASS_DENSITY) + continue + if(!border.CanAStarPass(ID, actual_dir, no_id = no_id)) return TRUE // Destination blockers check var/reverse_dir = get_dir(destination_turf, src) for(var/obj/iter_object in destination_turf) - if(!iter_object.CanAStarPass(ID, reverse_dir, caller, no_id = no_id)) + // This is an optimization because of the massive call count of this code + if(!iter_object.density && iter_object.can_astar_pass == CANASTARPASS_DENSITY) + continue + if(!iter_object.CanAStarPass(ID, reverse_dir, caller, no_id)) return TRUE - return FALSE - -#undef CAN_STEP -#undef STEP_NOT_HERE_BUT_THERE diff --git a/code/__HELPERS/stoplag.dm b/code/__HELPERS/stoplag.dm index a24db8f7972..e838ddd97c9 100644 --- a/code/__HELPERS/stoplag.dm +++ b/code/__HELPERS/stoplag.dm @@ -19,5 +19,3 @@ while (TICK_USAGE > min(TICK_LIMIT_TO_RUN, Master.current_ticklimit)) #undef DELTA_CALC - -#define UNTIL(X) while(!(X)) stoplag() diff --git a/code/controllers/subsystem/pathfinder.dm b/code/controllers/subsystem/pathfinder.dm index b13d3d465f2..6ed21e0a048 100644 --- a/code/controllers/subsystem/pathfinder.dm +++ b/code/controllers/subsystem/pathfinder.dm @@ -1,46 +1,46 @@ +/// Queues and manages JPS pathfinding steps SUBSYSTEM_DEF(pathfinder) name = "Pathfinder" init_order = INIT_ORDER_PATH - flags = SS_NO_FIRE - var/datum/flowcache/mobs + priority = FIRE_PRIORITY_PATHFINDING + /// List of pathfind datums we are currently trying to process + var/list/datum/pathfind/active_pathing = list() + /// List of pathfind datums being ACTIVELY processed. exists to make subsystem stats readable + var/list/datum/pathfind/currentrun = list() var/static/space_type_cache /datum/controller/subsystem/pathfinder/Initialize() space_type_cache = typecacheof(/turf/open/space) - mobs = new(10) return SS_INIT_SUCCESS -/datum/flowcache - var/lcount - var/run - var/free - var/list/flow +/datum/controller/subsystem/pathfinder/stat_entry(msg) + msg = "P:[length(active_pathing)]" + return ..() -/datum/flowcache/New(n) - . = ..() - lcount = n - run = 0 - free = 1 - flow = new/list(lcount) +// This is another one of those subsystems (hey lighting) in which one "Run" means fully processing a queue +// We'll use a copy for this just to be nice to people reading the mc panel +/datum/controller/subsystem/pathfinder/fire(resumed) + if(!resumed) + src.currentrun = active_pathing.Copy() -/datum/flowcache/proc/getfree(atom/M) - if(run < lcount) - run += 1 - while(flow[free]) - CHECK_TICK - free = (free % lcount) + 1 - var/t = addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/flowcache, toolong), free), 150, TIMER_STOPPABLE) - flow[free] = t - flow[t] = M - return free - else - return 0 + // Dies of sonic speed from caching datum var reads + var/list/currentrun = src.currentrun + while(length(currentrun)) + var/datum/pathfind/path = currentrun[length(currentrun)] + if(!path.search_step()) // Something's wrong + path.early_exit() + currentrun.len-- + continue + if(MC_TICK_CHECK) + return + path.finished() + // Next please + currentrun.len-- -/datum/flowcache/proc/toolong(l) - log_game("Pathfinder route took longer than 150 ticks, src bot [flow[flow[l]]]") - found(l) - -/datum/flowcache/proc/found(l) - deltimer(flow[l]) - flow[l] = null - run -= 1 +/// Initiates a pathfind. Returns true if we're good, FALSE if something's failed +/datum/controller/subsystem/pathfinder/proc/pathfind(atom/movable/caller, atom/end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first=TRUE, diagonal_safety=TRUE, datum/callback/on_finish) + var/datum/pathfind/path = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude, skip_first, diagonal_safety, on_finish) + if(path.start()) + active_pathing += path + return TRUE + return FALSE diff --git a/code/game/atoms.dm b/code/game/atoms.dm index d3ac1c5f1aa..c3f2e431116 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -173,6 +173,8 @@ /// the datum handler for our contents - see create_storage() for creation method var/datum/storage/atom_storage + /// How this atom should react to having its astar blocking checked + var/can_astar_pass = CANASTARPASS_DENSITY /** * Called when an atom is created in byond (built in engine proc) @@ -2067,6 +2069,9 @@ * * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions * * caller- The movable we're checking pass flags for, if we're making any such checks * * no_id: When true, doors with public access will count as impassible + * + * IMPORTANT NOTE: /turf/proc/LinkBlockedWithAccess assumes that overrides of CanAStarPass will always return true if density is FALSE + * If this is NOT you, ensure you edit your can_astar_pass variable. Check __DEFINES/path.dm **/ /atom/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) if(caller && (caller.pass_flags & pass_flags_self)) diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm index 5532988725e..ed05978f57c 100644 --- a/code/game/objects/structures/plasticflaps.dm +++ b/code/game/objects/structures/plasticflaps.dm @@ -8,6 +8,7 @@ density = FALSE anchored = TRUE can_atmos_pass = ATMOS_PASS_NO + can_astar_pass = CANASTARPASS_ALWAYS_PROC /obj/structure/plasticflaps/opaque opacity = TRUE diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index b0e9d0d1960..3fac3df03af 100755 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -694,7 +694,7 @@ GLOBAL_LIST_EMPTY(station_turfs) * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space? * * no_id: When true, doors with public access will count as impassible */ -/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only, no_id = FALSE) +/turf/proc/reachableAdjacentTurfs(atom/movable/caller, ID, simulated_only, no_id = FALSE) var/static/space_type_cache = typecacheof(/turf/open/space) . = list() diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index 2f2536078d9..cb18ffe6be6 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -91,8 +91,6 @@ var/destination ///The next destination in the patrol route var/next_destination - ///If we should shuffle our adjacency checking - var/shuffle = FALSE /// the nearest beacon's tag var/nearest_beacon @@ -487,6 +485,7 @@ //Generalized behavior code, override where needed! +GLOBAL_LIST_EMPTY(scan_typecaches) /** * Attempt to scan tiles near [src], first by checking adjacent, then if a target is still not found, nearby. * @@ -495,31 +494,48 @@ * scan_range - how far away from [src] will be scanned, if nothing is found directly adjacent. */ /mob/living/simple_animal/bot/proc/scan(list/scan_types, old_target, scan_range = DEFAULT_SCAN_RANGE) - var/list/adjacent = shuffle(view(1, src)) - for(var/turf/scan as anything in adjacent) //Let's see if there's something right next to us first! - if(check_bot(scan)) //Is there another bot there? Then let's just skip it - continue - var/final_result = checkscan(scan, scan_types, old_target) - if(final_result) - return final_result + var/key = scan_types.Join(",") + var/list/scan_cache = GLOB.scan_typecaches[key] + if(!scan_cache) + scan_cache = typecacheof(scan_types) + GLOB.scan_typecaches[key] = scan_cache + if(!get_turf(src)) + return + // Nicer behavior, ensures we don't conflict with other bots quite so often + var/list/adjacent = list() + for(var/turf/to_walk in view(1, src)) + adjacent += to_walk - for(var/turf/scanned_turfs as anything in view(scan_range, src) - adjacent) //Search for something in range, minus what we already checked. - if(check_bot(scanned_turfs)) //Is there another bot there? Then let's just skip it - continue - var/final_result = checkscan(scanned_turfs, scan_types, old_target) - if(final_result) - return final_result + adjacent = shuffle(adjacent) -/mob/living/simple_animal/bot/proc/checkscan(atom/scan, list/scan_types, old_target) - for(var/scan_type in scan_types) - if(!istype(scan, scan_type)) //Check that the thing we found is the type we want! - continue //If not, keep searching! - if((REF(scan) in ignore_list) || (scan == old_target)) //Filter for blacklisted elements, usually unreachable or previously processed oness + var/list/turfs_to_walk = list() + for(var/turf/victim in view(scan_range, src)) + turfs_to_walk += victim + + turfs_to_walk = turfs_to_walk - adjacent + // Now we prepend adjacent since we want to run those first + turfs_to_walk = adjacent + turfs_to_walk + + for(var/turf/scanned as anything in turfs_to_walk) + // Check bot is inlined here to save cpu time + //Is there another bot there? Then let's just skip it so we dont all atack on top of eachother. + var/bot_found = FALSE + for(var/mob/living/simple_animal/bot/buddy in scanned.contents) + if(istype(buddy, type) && (buddy != src)) + bot_found = TRUE + break + if(bot_found) continue - var/scan_result = process_scan(scan) //Some bots may require additional processing when a result is selected. - if(!isnull(scan_result)) - return scan_result + for(var/atom/thing as anything in scanned) + if(!scan_cache[thing.type]) //Check that the thing we found is the type we want! + continue //If not, keep searching! + if(thing == old_target || (REF(thing) in ignore_list)) //Filter for blacklisted elements, usually unreachable or previously processed oness + continue + + var/scan_result = process_scan(thing) //Some bots may require additional processing when a result is selected. + if(!isnull(scan_result)) + return scan_result //When the scan finds a target, run bot specific processing to select it for the next step. Empty by default. /mob/living/simple_animal/bot/proc/process_scan(scan_target) @@ -529,10 +545,9 @@ var/turf/target_turf = get_turf(targ) if(!target_turf) return FALSE - for(var/turf_contents in target_turf.contents) - //Is there another bot there already? If so, let's skip it so we dont all atack on top of eachother. - if(istype(turf_contents, type) && (turf_contents != src)) - return TRUE //Let's abort if we find a bot so we dont have to keep rechecking + for(var/mob/living/simple_animal/bot/buddy in target_turf.contents) + if(istype(buddy, type) && (buddy != src)) + return TRUE return FALSE /mob/living/simple_animal/bot/proc/add_to_ignore(subject) @@ -813,10 +828,11 @@ Pass a positive integer as an argument to override a bot's default speed. /mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid) check_bot_access() - INVOKE_ASYNC(src, PROC_REF(do_calc_summon_path), avoid) + var/datum/callback/path_complete = CALLBACK(src, PROC_REF(on_summon_path_finish)) + SSpathfinder.pathfind(src, summon_target, max_distance=150, id=access_card, exclude=avoid, on_finish = path_complete) -/mob/living/simple_animal/bot/proc/do_calc_summon_path(turf/avoid) - set_path(get_path_to(src, summon_target, max_distance=150, id=access_card, exclude=avoid)) +/mob/living/simple_animal/bot/proc/on_summon_path_finish(list/path) + set_path(path) if(!length(path)) //Cannot reach target. Give up and announce the issue. speak("Summon command failed, destination unreachable.",radio_channel) bot_reset() @@ -1024,6 +1040,12 @@ Pass a positive integer as an argument to override a bot's default speed. var/list/path_images = active_hud_list[DIAG_PATH_HUD] QDEL_LIST(path_images) if(newpath) + var/mutable_appearance/path_image = new /mutable_appearance() + path_image.icon = path_image_icon + path_image.icon_state = path_image_icon_state + path_image.layer = BOT_PATH_LAYER + path_image.appearance_flags = RESET_COLOR|RESET_TRANSFORM + path_image.color = path_image_color for(var/i in 1 to newpath.len) var/turf/T = newpath[i] if(T == loc) //don't bother putting an image if it's where we already exist. @@ -1047,16 +1069,11 @@ Pass a positive integer as an argument to override a bot's default speed. else ntransform.Scale(1, -1) prevI.transform = ntransform - var/mutable_appearance/MA = new /mutable_appearance() - MA.icon = path_image_icon - MA.icon_state = path_image_icon_state - MA.layer = ABOVE_OPEN_TURF_LAYER - SET_PLANE(MA, GAME_PLANE, T) - MA.appearance_flags = RESET_COLOR|RESET_TRANSFORM - MA.color = path_image_color - MA.dir = direction + + SET_PLANE(path_image, GAME_PLANE, T) + path_image.dir = direction var/image/I = image(loc = T) - I.appearance = MA + I.appearance = path_image path[T] = I path_images += I diff --git a/code/modules/mob/living/simple_animal/bot/floorbot.dm b/code/modules/mob/living/simple_animal/bot/floorbot.dm index 2130f9c443f..cb2e56ccb4b 100644 --- a/code/modules/mob/living/simple_animal/bot/floorbot.dm +++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm @@ -226,7 +226,6 @@ if(target) if(loc == target || loc == get_turf(target)) if(check_bot(target)) //Target is not defined at the parent - shuffle = TRUE if(prob(50)) //50% chance to still try to repair so we dont end up with 2 floorbots failing to fix the last breach target = null path = list() diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index 26f4f11d3c9..66462edc785 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -654,7 +654,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list( item = I break if(item) - if(!get_path_to(src, item)) + if(!get_path_to(src, item)) // WHY DO WE DISREGARD THE PATH AHHHHHH item = null continue return item diff --git a/tgstation.dme b/tgstation.dme index a2a1202bc42..c75ae2bce4a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -139,6 +139,7 @@ #include "code\__DEFINES\pai.dm" #include "code\__DEFINES\paintings.dm" #include "code\__DEFINES\paper.dm" +#include "code\__DEFINES\path.dm" #include "code\__DEFINES\perf_test.dm" #include "code\__DEFINES\pinpointers.dm" #include "code\__DEFINES\pipe_construction.dm"