From 4412e6e78f274176e97d128e7216756d07163508 Mon Sep 17 00:00:00 2001 From: Lucy Date: Tue, 3 Mar 2026 22:55:59 -0500 Subject: [PATCH 1/5] TONIGHT, A STAR OF THE CITY SHALL FALL (adds A* pathfinding) --- code/__HELPERS/paths/path.dm | 9 +- code/__HELPERS/~~oculis_helpers/_lists.dm | 34 ++ code/__HELPERS/~~oculis_helpers/mapping.dm | 10 + .../code/game/turfs/open/openspace.dm | 19 ++ .../code/modules/mob/living/navigation.dm | 3 + modular_oculis/modules/astar/code/astar.dm | 302 ++++++++++++++++++ .../modules/astar/code/navigation.dm | 73 +++++ .../modules/astar/code/pathfinder.dm | 21 ++ modular_oculis/modules/astar/code/turf.dm | 82 +++++ .../modules/astar/code/weighting/areas.dm | 9 + .../modules/astar/code/weighting/atoms.dm | 28 ++ .../modules/astar/code/weighting/turfs.dm | 41 +++ tgstation.dme | 11 + 13 files changed, 641 insertions(+), 1 deletion(-) create mode 100644 code/__HELPERS/~~oculis_helpers/_lists.dm create mode 100644 code/__HELPERS/~~oculis_helpers/mapping.dm create mode 100644 modular_oculis/master_files/code/game/turfs/open/openspace.dm create mode 100644 modular_oculis/master_files/code/modules/mob/living/navigation.dm create mode 100644 modular_oculis/modules/astar/code/astar.dm create mode 100644 modular_oculis/modules/astar/code/navigation.dm create mode 100644 modular_oculis/modules/astar/code/pathfinder.dm create mode 100644 modular_oculis/modules/astar/code/turf.dm create mode 100644 modular_oculis/modules/astar/code/weighting/areas.dm create mode 100644 modular_oculis/modules/astar/code/weighting/atoms.dm create mode 100644 modular_oculis/modules/astar/code/weighting/turfs.dm diff --git a/code/__HELPERS/paths/path.dm b/code/__HELPERS/paths/path.dm index 5e29757e9f861..351fb1eb5d012 100644 --- a/code/__HELPERS/paths/path.dm +++ b/code/__HELPERS/paths/path.dm @@ -316,7 +316,12 @@ /// Require a movable var/datum/weakref/requester_ref = null -/datum/can_pass_info/New(atom/movable/construct_from, list/access, no_id = FALSE, call_depth = 0) + // OCULIS EDIT ADDITION START - multi-z pathfinding + /// Whether to do extra checks for multi-Z pathing or not. + var/multiz_checks = FALSE + // OCULIS EDIT ADDITION END + +/datum/can_pass_info/New(atom/movable/construct_from, list/access, no_id = FALSE, call_depth = 0, multiz_checks = FALSE) // OCULIS EDIT ADDITION - multi-z pathfinding // No infiniloops if(call_depth > 10) return @@ -353,6 +358,8 @@ if(construct_from.pulling) src.pulling_info = new(construct_from.pulling, access, no_id, call_depth + 1) + src.multiz_checks = multiz_checks // OCULIS EDIT ADDITION - multi-z pathfinding + /// List of vars on /datum/can_pass_info to use when checking two instances for equality GLOBAL_LIST_INIT(can_pass_info_vars, GLOBAL_PROC_REF(can_pass_check_vars)) diff --git a/code/__HELPERS/~~oculis_helpers/_lists.dm b/code/__HELPERS/~~oculis_helpers/_lists.dm new file mode 100644 index 0000000000000..b3106b335f7bc --- /dev/null +++ b/code/__HELPERS/~~oculis_helpers/_lists.dm @@ -0,0 +1,34 @@ +/**** + * Even more custom binary search sorted insert (in reverse order), using defines instead of vars. + * INPUT: Item to be inserted + * LIST: List to insert INPUT into + * TYPECONT: A define setting the var to the typepath of the contents of the list + * COMPARE: The item to compare against, usualy the same as INPUT + * COMPARISON: A define that takes an item to compare as input, and returns their comparable value + * COMPTYPE: How should the list be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT_DEFINE_REVERSE(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + ##TYPECONT(__BIN_ITEM);\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(##COMPARISON(__BIN_ITEM) >= ##COMPARISON(COMPARE)) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = ##COMPARISON(__BIN_ITEM) < ##COMPARISON(COMPARE) ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) diff --git a/code/__HELPERS/~~oculis_helpers/mapping.dm b/code/__HELPERS/~~oculis_helpers/mapping.dm new file mode 100644 index 0000000000000..8210cce15c317 --- /dev/null +++ b/code/__HELPERS/~~oculis_helpers/mapping.dm @@ -0,0 +1,10 @@ +/// Checks to see if two atoms are on connected Z-levels, +/// i.e on different floors of the station. +/proc/are_zs_connected(atom/a, atom/b) + a = get_turf(a) + b = get_turf(b) + if(isnull(a) || isnull(b)) + return FALSE + if(a.z == b.z) + return TRUE + return (b.z in SSmapping.get_connected_levels(a)) diff --git a/modular_oculis/master_files/code/game/turfs/open/openspace.dm b/modular_oculis/master_files/code/game/turfs/open/openspace.dm new file mode 100644 index 0000000000000..a3788cda5d6a4 --- /dev/null +++ b/modular_oculis/master_files/code/game/turfs/open/openspace.dm @@ -0,0 +1,19 @@ +/turf/open/openspace/can_cross_safely(atom/movable/crossing) + . = ..() + if(.) + return + var/turf/below = GET_TURF_BELOW(src) + if(below) + var/obj/structure/stairs/stairs_below = locate() in below + if(stairs_below?.isTerminator()) + return TRUE + +/turf/open/openspace/CanAStarPass(to_dir, datum/can_pass_info/pass_info) + . = ..() + if(. || !pass_info.multiz_checks) + return + var/turf/turf_below = GET_TURF_BELOW(src) + if(turf_below) + var/obj/structure/stairs/stairs_below = locate() in turf_below + if(stairs_below?.isTerminator()) + return TRUE diff --git a/modular_oculis/master_files/code/modules/mob/living/navigation.dm b/modular_oculis/master_files/code/modules/mob/living/navigation.dm new file mode 100644 index 0000000000000..1ee4468ddbbc0 --- /dev/null +++ b/modular_oculis/master_files/code/modules/mob/living/navigation.dm @@ -0,0 +1,3 @@ +// Run our modular `create_astar_navigation` proc instead. +/mob/living/create_navigation() + create_astar_navigation() diff --git a/modular_oculis/modules/astar/code/astar.dm b/modular_oculis/modules/astar/code/astar.dm new file mode 100644 index 0000000000000..35aee53e8646d --- /dev/null +++ b/modular_oculis/modules/astar/code/astar.dm @@ -0,0 +1,302 @@ +#define ATURF 1 +#define TOTAL_COST_F 2 +#define DIST_FROM_START_G 3 +#define HEURISTIC_H 4 +#define PREV_NODE 5 +#define NODE_TURN 6 +#define BLOCKED_FROM 7 // Available directions to explore FROM this node + +#define ASTAR_NODE(turf, dist_from_start, heuristic, prev_node, node_turn, blocked_from) \ + list(turf, (dist_from_start + heuristic * (1 + PF_TIEBREAKER)), dist_from_start, heuristic, prev_node, node_turn, blocked_from) + +#define ASTAR_UPDATE_NODE(node, new_prev, new_g, new_h, new_nt) \ + node[PREV_NODE] = new_prev; \ + node[DIST_FROM_START_G] = new_g; \ + node[HEURISTIC_H] = new_h; \ + node[TOTAL_COST_F] = new_g + new_h * (1 + PF_TIEBREAKER); \ + node[NODE_TURN] = new_nt + +#define ASTAR_CLOSE_ENOUGH_TO_END(end, checking_turf, mintargetdist) \ + (checking_turf == end || (mintargetdist && (get_dist_3d(checking_turf, end) <= mintargetdist))) + +#define SORT_TOTAL_COST_F(list) (list[TOTAL_COST_F]) + +#define PF_TIEBREAKER 0.005 +#define MASK_ODD 85 +#define MASK_EVEN 170 + +/datum/pathfind/astar + /// The movable we are pathing + var/atom/movable/requester + /// The turf we're trying to path to. + var/turf/end + /// The proc used to calculate the distance used in every A* calculation (length of path and heuristic) + var/dist = TYPE_PROC_REF(/turf, heuristic_cardinal_3d) + /// The maximum number of nodes the returned path can be (0 = infinite) + var/maxnodes + /// The maximum number of nodes to search (default: 30, 0 = infinite) + var/maxnodedepth + /// Minimum distance to the target before path returns, + /// could be used to get near a target, but not right to it - for an AI mob with a gun, for example. + var/mintargetdist + /// The proc that returns the turfs to consider around the actually processed node. + var/adjacent = TYPE_PROC_REF(/turf, reachable_turf_test) + /// Whether we should do multi-z pathing or not. + var/check_z_levels + /// Whether to smooth the path by replacing cardinal turns with diagonals + var/smooth_diagonals = TRUE + /// Binary sorted list of nodes (lowest weight at end for easy Pop) + VAR_PRIVATE/list/open + /// Turf -> node mapping for nodes in open list + VAR_PRIVATE/list/openc + /// turf -> bitmask of blocked directions + VAR_PRIVATE/list/closed + VAR_PRIVATE/list/cur + VAR_PRIVATE/list/path = null + +/datum/pathfind/astar/Destroy(force) + . = ..() + requester = null + end = null + open = null + openc = null + closed = null + cur = null + path = null + pass_info = null + +/datum/pathfind/astar/proc/setup(atom/requester, atom/end, dist = TYPE_PROC_REF(/turf, heuristic_cardinal_3d), maxnodes, maxnodedepth = 30, mintargetdist, adjacent = TYPE_PROC_REF(/turf, reachable_turf_test), list/access = list(), turf/exclude, simulated_only = TRUE, check_z_levels = TRUE, smooth_diagonals = TRUE, list/datum/callback/on_finish) + src.requester = requester + src.end = get_turf(end) + src.dist = dist + src.maxnodes = maxnodes + src.maxnodedepth = maxnodes || maxnodedepth + src.mintargetdist = mintargetdist + src.adjacent = adjacent + src.avoid = exclude + src.simulated_only = simulated_only + src.pass_info = new(requester, access, multiz_checks = check_z_levels) + src.check_z_levels = check_z_levels + src.smooth_diagonals = smooth_diagonals + src.on_finish = on_finish + +/datum/pathfind/astar/start() + start = get_turf(requester) + . = ..() + if(!.) + return . + if (!start || !end) + . = FALSE + CRASH("Invalid A* start or destination") + if (start == end) + return FALSE + if (maxnodes && start.distance_3d(end) > maxnodes) + return FALSE + + open = list() + openc = new() + closed = new() + + cur = ASTAR_NODE(start, 0, start.distance_3d(end), null, 0, ALL_CARDINALS) + var/list/insert_item = list(cur) + BINARY_INSERT_DEFINE_REVERSE(insert_item, open, SORT_VAR_NO_TYPE, cur, SORT_TOTAL_COST_F, COMPARE_KEY) + openc[start] = cur + + return TRUE + +/datum/pathfind/astar/search_step() + . = ..() + if(!.) + return . + if(QDELETED(requester)) + return FALSE + + var/dist = src.dist + var/adjacent = src.adjacent + var/maxnodedepth = src.maxnodedepth + var/list/open = src.open + var/list/openc = src.openc + var/list/closed = src.closed + var/turf/exclude = src.avoid + var/datum/can_pass_info/can_pass_info = src.pass_info + var/check_z_levels = src.check_z_levels + var/atom/movable/our_movable + + var/list/cardinals = GLOB.cardinals + + while (requester && length(open) && !path) + // Pop from end (highest priority in reverse sorted list) + src.cur = open[length(open)] + var/list/cur = src.cur + open.len-- + + var/turf/cur_turf = cur[ATURF] + openc -= cur_turf + closed[cur_turf] = ALL_CARDINALS + + // Destination check - must be exact match or valid closeenough on same Z-level + var/is_destination = (cur_turf == end) + // Only consider "close enough" if on the same Z-level + var/closeenough = FALSE + if (!check_z_levels || cur_turf.z == end.z) + if (mintargetdist) + closeenough = cur_turf.distance_3d(end) <= mintargetdist + else + closeenough = cur_turf.distance_3d(end) < 1 + + if (is_destination || closeenough) + path = list(cur_turf) + var/list/prev = cur[PREV_NODE] + while (prev) + path.Add(prev[ATURF]) + prev = prev[PREV_NODE] + break + + if(maxnodedepth && (cur[NODE_TURN] > maxnodedepth)) + if(TICK_CHECK) + return TRUE + continue + + for(var/dir_to_check in cardinals) + if(!(cur[BLOCKED_FROM] & dir_to_check)) + continue + + var/turf/T = get_step(cur_turf, dir_to_check) + + if(isopenspaceturf(cur_turf)) + if(isnull(our_movable)) + our_movable = can_pass_info.requester_ref?.resolve() || FALSE + if(our_movable && our_movable.can_z_move(DOWN, cur_turf, null, ZMOVE_FALL_FLAGS)) // don't use ?. as this can be false if it fails to resolve for some reason + var/turf/turf_below = GET_TURF_BELOW(cur_turf) + if(turf_below) + T = turf_below + else + var/obj/structure/stairs/stairs = locate() in cur_turf + if(stairs?.dir == dir_to_check && stairs.isTerminator()) + var/turf/stairs_destination = get_step_multiz(cur_turf, dir_to_check | UP) + if(stairs_destination) + T = stairs_destination + + if(!T || T == exclude) + continue + + var/reverse = REVERSE_DIR(dir_to_check) + if(closed[T] & reverse) + continue + + if(!call(cur_turf, adjacent)(requester, T, can_pass_info)) + closed[T] |= reverse + continue + + var/list/CN = openc[T] + var/newg = cur[DIST_FROM_START_G] + call(cur_turf, dist)(T, requester) + + if(CN) + // Already in open list, check if this is a better path + if(newg < CN[DIST_FROM_START_G]) + // Remove old instance + var/list/old_item = list(CN) + open -= old_item + + // Update node + ASTAR_UPDATE_NODE(CN, cur, newg, CN[HEURISTIC_H], cur[NODE_TURN] + 1) + + // Re-insert with new priority + var/list/new_item = list(CN) + BINARY_INSERT_DEFINE_REVERSE(new_item, open, SORT_VAR_NO_TYPE, CN, SORT_TOTAL_COST_F, COMPARE_KEY) + else + // Not in open list, create new node + CN = ASTAR_NODE(T, newg, call(T, dist)(end, requester), cur, cur[NODE_TURN] + 1, ALL_CARDINALS^reverse) + var/list/new_item = list(CN) + BINARY_INSERT_DEFINE_REVERSE(new_item, open, SORT_VAR_NO_TYPE, CN, SORT_TOTAL_COST_F, COMPARE_KEY) + openc[T] = CN + + if(TICK_CHECK) + return TRUE + + return TRUE + +/datum/pathfind/astar/finished() + if(path) + for(var/i = 1 to round(0.5 * length(path))) + path.Swap(i, length(path) - i + 1) + + if(smooth_diagonals) + path = smooth_path_diagonals(path) + + hand_back(path) + openc = null + closed = null + return ..() + +/datum/pathfind/astar/proc/smooth_path_diagonals(list/input_path) + if(!input_path || length(input_path) < 3) + return input_path + + var/list/smoothed = list() + var/i = 1 + + while(i <= length(input_path)) + var/turf/current = input_path[i] + smoothed += current + + // need at least 2 more turfs to check for smoothing + if(i + 2 > length(input_path)) + i++ + continue + + var/turf/next = input_path[i + 1] + var/turf/after_next = input_path[i + 2] + + var/dir1 = get_dir(current, next) + var/dir2 = get_dir(next, after_next) + + //if card and perp hen attempt to see if diagonal possible + if(!ISDIAGONALDIR(dir1) && !ISDIAGONALDIR(dir2) && dir1 != dir2 && dir1 != REVERSE_DIR(dir2)) + var/diagonal_dir = dir1 | dir2 + var/turf/diagonal_target = get_step(current, diagonal_dir) + if(diagonal_target == after_next) + if(can_move_diagonal(current, after_next, pass_info)) + i += 2 + continue + + i++ + + return smoothed + +/datum/pathfind/astar/proc/can_move_diagonal(turf/from, turf/end, datum/can_pass_info/pass_info) + if(!from || !end) + return FALSE + + var/diagonal_dir = get_dir(from, end) + if(!ISDIAGONALDIR(diagonal_dir)) + return FALSE + + var/dir1 = diagonal_dir & 3 + var/dir2 = diagonal_dir & 12 + + var/turf/intermediate1 = get_step(from, dir1) + var/turf/intermediate2 = get_step(from, dir2) + + if(intermediate1 && !intermediate1.density && call(from, adjacent)(requester, intermediate1, pass_info)) + if(call(intermediate1, adjacent)(requester, end, pass_info)) + return TRUE + + if(intermediate2 && !intermediate2.density && call(from, adjacent)(requester, intermediate2, pass_info)) + if(call(intermediate2, adjacent)(requester, end, pass_info)) + return TRUE + + return FALSE + +#undef ATURF +#undef TOTAL_COST_F +#undef DIST_FROM_START_G +#undef HEURISTIC_H +#undef PREV_NODE +#undef NODE_TURN +#undef BLOCKED_FROM +#undef ASTAR_NODE +#undef ASTAR_UPDATE_NODE +#undef ASTAR_CLOSE_ENOUGH_TO_END +#undef SORT_TOTAL_COST_F + diff --git a/modular_oculis/modules/astar/code/navigation.dm b/modular_oculis/modules/astar/code/navigation.dm new file mode 100644 index 0000000000000..f7cfff2c460e8 --- /dev/null +++ b/modular_oculis/modules/astar/code/navigation.dm @@ -0,0 +1,73 @@ +#define MAX_NAVIGATE_RANGE 145 + +/mob/living + /// Are we currently pathfinding for the navigate verb? + var/navigating = FALSE + +/mob/living/proc/create_astar_navigation() + var/list/destination_list = list() + for(var/atom/destination as anything in GLOB.navigate_destinations) + if(get_dist(destination, src) > MAX_NAVIGATE_RANGE || !are_zs_connected(destination, src)) // monkestation edit: check to ensure that Z-levels are connected, so we don't get centcom destinations while on station and vice-versa + continue + var/destination_name = GLOB.navigate_destinations[destination] + if(destination.z != z && is_multi_z_level(z)) // up or down is just a good indicator "we're on the station", we don't need to check specifics + destination_name += ((get_dir_multiz(src, destination) & UP) ? " (Above)" : " (Below)") + + BINARY_INSERT_DEFINE(destination_name, destination_list, SORT_VAR_NO_TYPE, destination_name, SORT_COMPARE_DIRECTLY, COMPARE_KEY) + destination_list[destination_name] = destination + + if(!length(destination_list)) + balloon_alert(src, "no navigation signals!") + return + + var/platform_code = tgui_input_list(src, "Select a location", "Navigate", destination_list) + var/atom/navigate_target = destination_list[platform_code] + + if(isnull(navigate_target) || incapacitated) + return + + if(!isatom(navigate_target)) + CRASH("Navigate target ([navigate_target]) is not an atom, somehow.") + + navigating = TRUE + var/datum/callback/await = list(CALLBACK(src, PROC_REF(finish_astar_navigation), navigate_target)) + if(!SSpathfinder.astar_pathfind(src, navigate_target, maxnodes = MAX_NAVIGATE_RANGE, mintargetdist = 1, access = get_access(), smooth_diagonals = FALSE, on_finish = await)) // diagonals look kind of weird when visualized for now + navigating = FALSE + balloon_alert(src, "failed to begin navigation!") + +/mob/living/proc/finish_astar_navigation(turf/navigate_target, list/path) + navigating = FALSE + if(!client) + return + if(!length(path)) + balloon_alert(src, "no valid path with current access!") + return + path |= get_turf(navigate_target) + for(var/i in 1 to length(path)) + var/turf/current_turf = path[i] + var/image/path_image = image(icon = 'icons/effects/navigation.dmi', layer = HIGH_PIPE_LAYER, loc = current_turf) + SET_PLANE(path_image, GAME_PLANE, current_turf) + path_image.color = COLOR_CYAN + path_image.alpha = 0 + var/dir_1 = 0 + var/dir_2 = 0 + if(i == 1) + dir_2 = REVERSE_DIR(angle2dir(get_angle(path[i+1], current_turf))) + else if(i == length(path)) + dir_2 = REVERSE_DIR(angle2dir(get_angle(path[i-1], current_turf))) + else + dir_1 = REVERSE_DIR(angle2dir(get_angle(path[i+1], current_turf))) + dir_2 = REVERSE_DIR(angle2dir(get_angle(path[i-1], current_turf))) + if(dir_1 > dir_2) + dir_1 = dir_2 + dir_2 = REVERSE_DIR(angle2dir(get_angle(path[i+1], current_turf))) + path_image.icon_state = "[dir_1]-[dir_2]" + client.images += path_image + client.navigation_images += path_image + animate(path_image, 0.5 SECONDS, alpha = 150) + + addtimer(CALLBACK(src, PROC_REF(shine_navigation)), 0.5 SECONDS) + RegisterSignal(src, COMSIG_LIVING_DEATH, PROC_REF(cut_navigation)) + balloon_alert(src, "navigation path created") + +#undef MAX_NAVIGATE_RANGE diff --git a/modular_oculis/modules/astar/code/pathfinder.dm b/modular_oculis/modules/astar/code/pathfinder.dm new file mode 100644 index 0000000000000..9f79482e9e107 --- /dev/null +++ b/modular_oculis/modules/astar/code/pathfinder.dm @@ -0,0 +1,21 @@ +/// Initiates an A* pathfind. Returns true if we're good, FALSE if something's failed +/datum/controller/subsystem/pathfinder/proc/astar_pathfind(requester, end, dist = TYPE_PROC_REF(/turf, heuristic_cardinal_3d), maxnodes, maxnodedepth = 30, mintargetdist, adjacent = TYPE_PROC_REF(/turf, reachable_turf_test), list/access = list(), turf/exclude, simulated_only = TRUE, check_z_levels = TRUE, smooth_diagonals = TRUE, list/datum/callback/on_finish) + var/datum/pathfind/astar/path = new() + path.setup(requester, end, dist, maxnodes, maxnodedepth, mintargetdist, adjacent, access, exclude, simulated_only, check_z_levels, smooth_diagonals, on_finish) + if(path.start()) + active_pathing += path + return TRUE + return FALSE + +/proc/get_astar_path_to(atom/movable/requester, atom/end, dist = TYPE_PROC_REF(/turf, heuristic_cardinal_3d), maxnodes, maxnodedepth = 30, mintargetdist, adjacent = TYPE_PROC_REF(/turf, reachable_turf_test), list/access = list(), turf/exclude, simulated_only = TRUE, check_z_levels = TRUE, smooth_diagonals = TRUE) + var/list/hand_around = list() + // We're guaranteed that list will be the first list in pathfinding_finished's argset because of how callback handles the arguments list + var/datum/callback/await = list(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pathfinding_finished), hand_around)) + if(!SSpathfinder.astar_pathfind(requester, end, dist, maxnodes, maxnodedepth, mintargetdist, adjacent, access, exclude, simulated_only, check_z_levels, smooth_diagonals, await)) + return list() + + UNTIL(length(hand_around)) + var/list/return_val = hand_around[1] + if(!islist(return_val) || (QDELETED(requester) || QDELETED(end))) // It's trash, just hand back empty to make it easy + return list() + return return_val diff --git a/modular_oculis/modules/astar/code/turf.dm b/modular_oculis/modules/astar/code/turf.dm new file mode 100644 index 0000000000000..395ed9332d9d0 --- /dev/null +++ b/modular_oculis/modules/astar/code/turf.dm @@ -0,0 +1,82 @@ +/turf/proc/reachable_turf_test(requester, turf/target, datum/can_pass_info/pass_info) + if(!target || target.density) + return FALSE + if(!target.can_cross_safely(requester)) // dangerous turf! lava or openspace (or others in the future) + return FALSE + var/z_distance = abs(target.z - z) + if(!z_distance) // standard check for same-z pathing + return !LinkBlockedWithAccess(target, pass_info) + if(z_distance != 1) // no single movement lets you move more than one z-level at a time (currently; update if this changes) + return FALSE + if(target.z > z) // going up stairs + var/obj/structure/stairs/stairs = locate() in src + if(stairs?.isTerminator() && target == get_step_multiz(src, stairs.dir | UP)) + return TRUE + else if(isopenspaceturf(src)) // going down stairs + var/turf/turf_below = GET_TURF_BELOW(src) + if(!turf_below || target != turf_below) + return FALSE + var/obj/structure/stairs/stairs_below = locate() in turf_below + if(stairs_below?.isTerminator()) + return TRUE + return FALSE + +/// Returns an additional distance factor based on slowdown and other factors. +/turf/proc/get_heuristic_slowdown(mob/traverser, travel_dir) + . = astar_weight + var/area/current_area = loc + if(current_area?.astar_weight) + . += current_area.astar_weight + +// Like Distance_cardinal, but includes additional weighting to make A* prefer turfs that are easier to pass through. +/turf/proc/heuristic_cardinal(turf/target, mob/traverser) + var/travel_dir = get_dir(src, target) + . = Distance_cardinal(target, traverser) + get_heuristic_slowdown(traverser, travel_dir) + target.get_heuristic_slowdown(traverser, travel_dir) + +/// A 3d-aware version of heuristic_cardinal that just... adds the Z-axis distance with a multiplier. +/turf/proc/heuristic_cardinal_3d(turf/target, mob/traverser) + return heuristic_cardinal(target, traverser) + abs(z - target.z) * 5 // Weight z-level differences higher so that we try to change Z-level sooner + +/// Helper function to compute 3D Manhattan distance. +/turf/proc/distance_3d(turf/other) + if (!istype(other)) + return 0 + var/dx = abs(x - other.x) + var/dy = abs(y - other.y) + var/dz = abs(z - other.z) * 5 // Weight z-level differences higher + return (dx + dy + dz) + +/// Returns the 3D Manhattan between two objects. +/proc/get_dist_3d(atom/source, atom/target) + var/turf/source_turf = get_turf(source) + return source_turf.distance_3d(get_turf(target)) + +/turf/open/get_heuristic_slowdown(mob/traverser, travel_dir) + . = ..() + if(slowdown) + . += slowdown * 10 + + var/liquid_state = liquids?.liquid_state + if(liquid_state) + if(liquid_state == LIQUID_STATE_FULLTILE) + . += 20 + else if(liquid_state == LIQUID_STATE_SHOULDERS) + . += 10 + else if(liquid_state == LIQUID_STATE_WAIST) + . += 5 + else if(liquid_state == LIQUID_STATE_ANKLES) + . += 3 + else if(liquid_state == LIQUID_STATE_PUDDLE) + . += 2 + + // i don't like these, but they can be improved later ~Lucy + // add cost from climbable obstacles + for(var/obj/structure/some_object in src) + if(some_object.density && HAS_TRAIT(some_object, TRAIT_CLIMBABLE)) + . += 2 // extra tile penalty + break + + // door will have to be opened + var/obj/machinery/door/door = locate() in src + if(door?.density && !door.locked) + . += 5 // try to avoid closed doors where possible diff --git a/modular_oculis/modules/astar/code/weighting/areas.dm b/modular_oculis/modules/astar/code/weighting/areas.dm new file mode 100644 index 0000000000000..f90258cf98d3a --- /dev/null +++ b/modular_oculis/modules/astar/code/weighting/areas.dm @@ -0,0 +1,9 @@ +/area + /// Extra A* weight applied to all turfs in this area. + var/astar_weight = 0 + +/area/station/hallway + astar_weight = -20 // hallways should be pathed through MORE often + +/area/station/maintenance + astar_weight = 10 diff --git a/modular_oculis/modules/astar/code/weighting/atoms.dm b/modular_oculis/modules/astar/code/weighting/atoms.dm new file mode 100644 index 0000000000000..ff82e5a628e37 --- /dev/null +++ b/modular_oculis/modules/astar/code/weighting/atoms.dm @@ -0,0 +1,28 @@ +/atom/movable + /// The weight for A* pathfinding added to turfs this atom is on. + var/astar_weight + +/atom/movable/Initialize(mapload, ...) + . = ..() + if(astar_weight && isturf(loc)) + var/turf/turf_loc = loc + turf_loc.astar_weight += astar_weight + +/atom/movable/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + if(astar_weight) + var/turf/old_turf = get_turf(old_loc) + var/turf/new_turf = get_turf(src) + if(old_turf) + old_turf.astar_weight -= astar_weight + if(new_turf) + new_turf.astar_weight += astar_weight + +/obj/structure/plasticflaps + astar_weight = 15 + +/obj/structure/chair + astar_weight = 2 + +/obj/structure/chair/sofa + astar_weight = 5 diff --git a/modular_oculis/modules/astar/code/weighting/turfs.dm b/modular_oculis/modules/astar/code/weighting/turfs.dm new file mode 100644 index 0000000000000..3de70ccbbcef2 --- /dev/null +++ b/modular_oculis/modules/astar/code/weighting/turfs.dm @@ -0,0 +1,41 @@ +/turf + /// The weight of the turf for A* pathfinding. + var/astar_weight = 50 + +/turf/ChangeTurf(path, list/new_baseturfs, flags) + var/old_astar_weight = (astar_weight - src::astar_weight) // just get the weight that isn't the turf + . = ..() + if(old_astar_weight) + var/turf/new_turf = . + if(new_turf && !(flags & CHANGETURF_SKIP)) + new_turf.astar_weight += old_astar_weight + +/turf/open/chasm + astar_weight = 9999 + +/turf/open/cliff + astar_weight = 500 + +/turf/open/misc/ice + astar_weight = 75 + +/turf/open/misc/dirt + astar_weight = 60 + +/turf/open/misc/dirt/station + astar_weight = /turf/open::astar_weight + +/turf/open/misc/dirt/dark/station + astar_weight = /turf/open::astar_weight + +/turf/open/water + astar_weight = 75 + +/turf/open/floor/plating + astar_weight = 75 + +/turf/open/space + astar_weight = 500 + +/turf/open/floor/tram/plate + astar_weight = 75 diff --git a/tgstation.dme b/tgstation.dme index 23c2564efad78..142b94b18d42a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -666,6 +666,8 @@ #include "code\__HELPERS\~~iris_helpers\game.dm" #include "code\__HELPERS\~~iris_helpers\is_helpers.dm" #include "code\__HELPERS\~~iris_helpers\logging.dm" +#include "code\__HELPERS\~~oculis_helpers\_lists.dm" +#include "code\__HELPERS\~~oculis_helpers\mapping.dm" #include "code\__HELPERS\~~oculis_helpers\text.dm" #include "code\__HELPERS\~~oculis_helpers\time.dm" #include "code\_globalvars\_regexes.dm" @@ -9797,6 +9799,7 @@ #include "modular_nova\modules\xenoarchartifacts\obj\wave_scanner.dm" #include "modular_oculis\master_files\code\__HELPERS\files.dm" #include "modular_oculis\master_files\code\datums\http.dm" +#include "modular_oculis\master_files\code\game\turfs\open\openspace.dm" #include "modular_oculis\master_files\code\modules\admin\verbs\getlogs.dm" #include "modular_oculis\master_files\code\modules\client\client_procs.dm" #include "modular_oculis\master_files\code\modules\client\persistent_client.dm" @@ -9809,7 +9812,15 @@ #include "modular_oculis\master_files\code\modules\logging\log_category.dm" #include "modular_oculis\master_files\code\modules\logging\log_holder.dm" #include "modular_oculis\master_files\code\modules\mentor\mentorsay.dm" +#include "modular_oculis\master_files\code\modules\mob\living\navigation.dm" #include "modular_oculis\master_files\code\modules\mocking\client.dm" +#include "modular_oculis\modules\astar\code\astar.dm" +#include "modular_oculis\modules\astar\code\navigation.dm" +#include "modular_oculis\modules\astar\code\pathfinder.dm" +#include "modular_oculis\modules\astar\code\turf.dm" +#include "modular_oculis\modules\astar\code\weighting\areas.dm" +#include "modular_oculis\modules\astar\code\weighting\atoms.dm" +#include "modular_oculis\modules\astar\code\weighting\turfs.dm" #include "modular_oculis\modules\hairstyles\code\hair.dm" #include "modular_oculis\modules\plexora\code\_plexora.dm" #include "modular_oculis\modules\plexora\code\config.dm" From 2ae28728d86556518063d8d6a621decf0b9852c4 Mon Sep 17 00:00:00 2001 From: Lucy Date: Tue, 3 Mar 2026 23:02:39 -0500 Subject: [PATCH 2/5] Minor optimization for `finish_astar_navigation` --- modular_oculis/modules/astar/code/navigation.dm | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modular_oculis/modules/astar/code/navigation.dm b/modular_oculis/modules/astar/code/navigation.dm index f7cfff2c460e8..c0856a2dcc2aa 100644 --- a/modular_oculis/modules/astar/code/navigation.dm +++ b/modular_oculis/modules/astar/code/navigation.dm @@ -52,15 +52,20 @@ var/dir_1 = 0 var/dir_2 = 0 if(i == 1) - dir_2 = REVERSE_DIR(angle2dir(get_angle(path[i+1], current_turf))) + dir_2 = angle2dir(get_angle(path[i+1], current_turf)) + dir_2 = REVERSE_DIR(dir_2) // if we combined this with the above, we'd do angle2dir/get_angle twice, which is not ideal else if(i == length(path)) - dir_2 = REVERSE_DIR(angle2dir(get_angle(path[i-1], current_turf))) + dir_2 = angle2dir(get_angle(path[i-1], current_turf)) + dir_2 = REVERSE_DIR(dir_2) else - dir_1 = REVERSE_DIR(angle2dir(get_angle(path[i+1], current_turf))) - dir_2 = REVERSE_DIR(angle2dir(get_angle(path[i-1], current_turf))) + dir_1 = angle2dir(get_angle(path[i+1], current_turf)) + dir_2 = angle2dir(get_angle(path[i-1], current_turf)) + dir_1 = REVERSE_DIR(dir_1) + dir_2 = REVERSE_DIR(dir_2) if(dir_1 > dir_2) dir_1 = dir_2 - dir_2 = REVERSE_DIR(angle2dir(get_angle(path[i+1], current_turf))) + dir_2 = angle2dir(get_angle(path[i+1], current_turf)) + dir_2 = REVERSE_DIR(dir_2) path_image.icon_state = "[dir_1]-[dir_2]" client.images += path_image client.navigation_images += path_image From 545fff2f926ae262b232c03de9ccd0b9808bd7a5 Mon Sep 17 00:00:00 2001 From: Lucy Date: Tue, 3 Mar 2026 23:20:33 -0500 Subject: [PATCH 3/5] use defines for weights, and tweak some area weights a bit. --- code/__DEFINES/~~oculis_defines/astar.dm | 25 ++++++++ .../modules/astar/code/weighting/areas.dm | 61 ++++++++++++++++++- .../modules/astar/code/weighting/atoms.dm | 6 +- .../modules/astar/code/weighting/turfs.dm | 22 +++---- tgstation.dme | 1 + 5 files changed, 98 insertions(+), 17 deletions(-) create mode 100644 code/__DEFINES/~~oculis_defines/astar.dm diff --git a/code/__DEFINES/~~oculis_defines/astar.dm b/code/__DEFINES/~~oculis_defines/astar.dm new file mode 100644 index 0000000000000..ac7940753e201 --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/astar.dm @@ -0,0 +1,25 @@ +// Note: higher A* weight = less preferred, lower weight = more preferred. + +/// Default A* weight given to areas. +#define ASTAR_WEIGHT_AREA_DEFAULT 0 +/// A* weight given to hallway areas. +#define ASTAR_WEIGHT_AREA_HALLS -20 +/// A* weight given to maintenance areas. +#define ASTAR_WEIGHT_AREA_MAINTS -(ASTAR_WEIGHT_AREA_HALLS / 2) +/// A* weight given to commons areas, i.e dorms, bar, cafeteria, etc. +#define ASTAR_WEIGHT_AREA_COMMONS (ASTAR_WEIGHT_AREA_HALLS / 4) + + +/// Default A* weight given to turfs. +#define ASTAR_WEIGHT_TURF_DEFAULT 50 +/// A* weight for turfs we'd prefer to not go over if possible, but it's fine if needed. +#define ASTAR_WEIGHT_TURF_DISCOURAGED (ASTAR_WEIGHT_TURF_DEFAULT * 1.5) +/// A* weight for turfs we should prolly never path over but it might be in the realm of possibility anyways? +#define ASTAR_WEIGHT_TURF_ALMOST_NEVER (ASTAR_WEIGHT_TURF_DEFAULT * 10) +/// A* weight for turfs we should never even CONSIDER pathing over. +#define ASTAR_WEIGHT_TURF_NEVER 9999 + +/// A* weight for objects that are better to walk around, but it's not a big deal at all. +#define ASTAR_WEIGHT_OBJ_MEH 2 +/// A* weight for objects to avoid going thru, but not too strongly. +#define ASTAR_WEIGHT_OBJ_DISCOURAGED (ASTAR_WEIGHT_OBJ_MEH * 10) diff --git a/modular_oculis/modules/astar/code/weighting/areas.dm b/modular_oculis/modules/astar/code/weighting/areas.dm index f90258cf98d3a..c20cfa3d6a9a1 100644 --- a/modular_oculis/modules/astar/code/weighting/areas.dm +++ b/modular_oculis/modules/astar/code/weighting/areas.dm @@ -1,9 +1,64 @@ /area /// Extra A* weight applied to all turfs in this area. - var/astar_weight = 0 + var/astar_weight = ASTAR_WEIGHT_AREA_DEFAULT /area/station/hallway - astar_weight = -20 // hallways should be pathed through MORE often + astar_weight = ASTAR_WEIGHT_AREA_HALLS // hallways should be pathed through MORE often +// maints is yucky /area/station/maintenance - astar_weight = 10 + astar_weight = ASTAR_WEIGHT_AREA_MAINTS + +/area/station/service/library/abandoned + astar_weight = ASTAR_WEIGHT_AREA_MAINTS + +/area/station/service/hydroponics/garden/abandoned + astar_weight = ASTAR_WEIGHT_AREA_MAINTS + +/area/station/service/kitchen/abandoned + astar_weight = ASTAR_WEIGHT_AREA_MAINTS + +/area/station/service/electronic_marketing_den + astar_weight = ASTAR_WEIGHT_AREA_MAINTS + +/area/station/service/abandoned_gambling_den + astar_weight = ASTAR_WEIGHT_AREA_MAINTS + +/area/station/service/theater/abandoned + astar_weight = ASTAR_WEIGHT_AREA_MAINTS + +/area/station/medical/abandoned + astar_weight = ASTAR_WEIGHT_AREA_MAINTS + + +/area/station/science/research/abandoned + astar_weight = ASTAR_WEIGHT_AREA_MAINTS + +// very slight preference for public areas like dorms, bar, etc +/area/station/commons/dorms + astar_weight = ASTAR_WEIGHT_AREA_COMMONS + +/area/station/commons/locker + astar_weight = ASTAR_WEIGHT_AREA_COMMONS + +/area/station/commons/fitness + astar_weight = ASTAR_WEIGHT_AREA_COMMONS + +/area/station/service/bar + astar_weight = ASTAR_WEIGHT_AREA_COMMONS + +/area/station/service/library + astar_weight = ASTAR_WEIGHT_AREA_COMMONS + +/area/station/service/cafeteria + astar_weight = ASTAR_WEIGHT_AREA_COMMONS + +/area/station/service/kitchen/diner + astar_weight = ASTAR_WEIGHT_AREA_COMMONS + +// ensure that backroom areas keep the default weight +/area/station/service/library/private + astar_weight = ASTAR_WEIGHT_AREA_DEFAULT + +/area/station/service/bar/backroom + astar_weight = ASTAR_WEIGHT_AREA_DEFAULT diff --git a/modular_oculis/modules/astar/code/weighting/atoms.dm b/modular_oculis/modules/astar/code/weighting/atoms.dm index ff82e5a628e37..13c4d94d52344 100644 --- a/modular_oculis/modules/astar/code/weighting/atoms.dm +++ b/modular_oculis/modules/astar/code/weighting/atoms.dm @@ -19,10 +19,10 @@ new_turf.astar_weight += astar_weight /obj/structure/plasticflaps - astar_weight = 15 + astar_weight = ASTAR_WEIGHT_OBJ_DISCOURAGED /obj/structure/chair - astar_weight = 2 + astar_weight = ASTAR_WEIGHT_OBJ_MEH /obj/structure/chair/sofa - astar_weight = 5 + astar_weight = ASTAR_WEIGHT_OBJ_MEH * 2.5 diff --git a/modular_oculis/modules/astar/code/weighting/turfs.dm b/modular_oculis/modules/astar/code/weighting/turfs.dm index 3de70ccbbcef2..e4e27aede03d0 100644 --- a/modular_oculis/modules/astar/code/weighting/turfs.dm +++ b/modular_oculis/modules/astar/code/weighting/turfs.dm @@ -1,6 +1,6 @@ /turf /// The weight of the turf for A* pathfinding. - var/astar_weight = 50 + var/astar_weight = ASTAR_WEIGHT_TURF_DEFAULT /turf/ChangeTurf(path, list/new_baseturfs, flags) var/old_astar_weight = (astar_weight - src::astar_weight) // just get the weight that isn't the turf @@ -11,31 +11,31 @@ new_turf.astar_weight += old_astar_weight /turf/open/chasm - astar_weight = 9999 + astar_weight = ASTAR_WEIGHT_TURF_NEVER /turf/open/cliff - astar_weight = 500 + astar_weight = ASTAR_WEIGHT_TURF_ALMOST_NEVER /turf/open/misc/ice - astar_weight = 75 + astar_weight = ASTAR_WEIGHT_TURF_DISCOURAGED /turf/open/misc/dirt - astar_weight = 60 + astar_weight = ASTAR_WEIGHT_TURF_DEFAULT * 1.2 /turf/open/misc/dirt/station - astar_weight = /turf/open::astar_weight + astar_weight = ASTAR_WEIGHT_TURF_DEFAULT /turf/open/misc/dirt/dark/station - astar_weight = /turf/open::astar_weight + astar_weight = ASTAR_WEIGHT_TURF_DEFAULT /turf/open/water - astar_weight = 75 + astar_weight = ASTAR_WEIGHT_TURF_DISCOURAGED /turf/open/floor/plating - astar_weight = 75 + astar_weight = ASTAR_WEIGHT_TURF_DISCOURAGED /turf/open/space - astar_weight = 500 + astar_weight = ASTAR_WEIGHT_TURF_ALMOST_NEVER /turf/open/floor/tram/plate - astar_weight = 75 + astar_weight = ASTAR_WEIGHT_TURF_DISCOURAGED diff --git a/tgstation.dme b/tgstation.dme index 142b94b18d42a..d0c7fd3b9392a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -538,6 +538,7 @@ #include "code\__DEFINES\~~iris_defines\research\techweb_nodes.dm" #include "code\__DEFINES\~~iris_defines\traits\declarations.dm" #include "code\__DEFINES\~~oculis_defines\admin.dm" +#include "code\__DEFINES\~~oculis_defines\astar.dm" #include "code\__DEFINES\~~oculis_defines\plexora.dm" #include "code\__HELPERS\_auxtools_api.dm" #include "code\__HELPERS\_dreamluau.dm" From 3d0ceb15d89b0de70a96c1553282ec6071c41677 Mon Sep 17 00:00:00 2001 From: Lucy Date: Wed, 4 Mar 2026 10:36:08 -0500 Subject: [PATCH 4/5] move some files around + add module readme --- .../master_files/code/game/atoms_movable.dm | 19 ++++++++ .../master_files/code/game/turfs/turf.dm | 11 +++++ .../modules/astar/code/weighting/atoms.dm | 20 -------- .../modules/astar/code/weighting/turfs.dm | 12 ----- modular_oculis/modules/astar/readme.md | 48 +++++++++++++++++++ tgstation.dme | 2 + 6 files changed, 80 insertions(+), 32 deletions(-) create mode 100644 modular_oculis/master_files/code/game/atoms_movable.dm create mode 100644 modular_oculis/master_files/code/game/turfs/turf.dm create mode 100644 modular_oculis/modules/astar/readme.md diff --git a/modular_oculis/master_files/code/game/atoms_movable.dm b/modular_oculis/master_files/code/game/atoms_movable.dm new file mode 100644 index 0000000000000..3521d6b94f1ef --- /dev/null +++ b/modular_oculis/master_files/code/game/atoms_movable.dm @@ -0,0 +1,19 @@ +/atom/movable + /// The weight for A* pathfinding added to turfs this atom is on. + var/astar_weight + +/atom/movable/Initialize(mapload, ...) + . = ..() + if(astar_weight && isturf(loc)) + var/turf/turf_loc = loc + turf_loc.astar_weight += astar_weight + +/atom/movable/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + if(astar_weight) + var/turf/old_turf = get_turf(old_loc) + var/turf/new_turf = get_turf(src) + if(old_turf) + old_turf.astar_weight -= astar_weight + if(new_turf) + new_turf.astar_weight += astar_weight diff --git a/modular_oculis/master_files/code/game/turfs/turf.dm b/modular_oculis/master_files/code/game/turfs/turf.dm new file mode 100644 index 0000000000000..f5249bcf17879 --- /dev/null +++ b/modular_oculis/master_files/code/game/turfs/turf.dm @@ -0,0 +1,11 @@ +/turf + /// The weight of the turf for A* pathfinding. + var/astar_weight = ASTAR_WEIGHT_TURF_DEFAULT + +/turf/ChangeTurf(path, list/new_baseturfs, flags) + var/old_astar_weight = (astar_weight - src::astar_weight) // just get the weight that isn't the turf + . = ..() + if(old_astar_weight) + var/turf/new_turf = . + if(new_turf && !(flags & CHANGETURF_SKIP)) + new_turf.astar_weight += old_astar_weight diff --git a/modular_oculis/modules/astar/code/weighting/atoms.dm b/modular_oculis/modules/astar/code/weighting/atoms.dm index 13c4d94d52344..f0e435c944499 100644 --- a/modular_oculis/modules/astar/code/weighting/atoms.dm +++ b/modular_oculis/modules/astar/code/weighting/atoms.dm @@ -1,23 +1,3 @@ -/atom/movable - /// The weight for A* pathfinding added to turfs this atom is on. - var/astar_weight - -/atom/movable/Initialize(mapload, ...) - . = ..() - if(astar_weight && isturf(loc)) - var/turf/turf_loc = loc - turf_loc.astar_weight += astar_weight - -/atom/movable/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) - . = ..() - if(astar_weight) - var/turf/old_turf = get_turf(old_loc) - var/turf/new_turf = get_turf(src) - if(old_turf) - old_turf.astar_weight -= astar_weight - if(new_turf) - new_turf.astar_weight += astar_weight - /obj/structure/plasticflaps astar_weight = ASTAR_WEIGHT_OBJ_DISCOURAGED diff --git a/modular_oculis/modules/astar/code/weighting/turfs.dm b/modular_oculis/modules/astar/code/weighting/turfs.dm index e4e27aede03d0..7a2ff663c7926 100644 --- a/modular_oculis/modules/astar/code/weighting/turfs.dm +++ b/modular_oculis/modules/astar/code/weighting/turfs.dm @@ -1,15 +1,3 @@ -/turf - /// The weight of the turf for A* pathfinding. - var/astar_weight = ASTAR_WEIGHT_TURF_DEFAULT - -/turf/ChangeTurf(path, list/new_baseturfs, flags) - var/old_astar_weight = (astar_weight - src::astar_weight) // just get the weight that isn't the turf - . = ..() - if(old_astar_weight) - var/turf/new_turf = . - if(new_turf && !(flags & CHANGETURF_SKIP)) - new_turf.astar_weight += old_astar_weight - /turf/open/chasm astar_weight = ASTAR_WEIGHT_TURF_NEVER diff --git a/modular_oculis/modules/astar/readme.md b/modular_oculis/modules/astar/readme.md new file mode 100644 index 0000000000000..8323a02711e7d --- /dev/null +++ b/modular_oculis/modules/astar/readme.md @@ -0,0 +1,48 @@ +https://github.com/Monkestation/OculisStation/pull/45 + +## A* Pathfinding / Navigation + +Module ID: ASTAR + +### Description: + +Implements A* pathfinding, and reworks the Navigate verb/button to use it. + +A* works across Z-levels (it can go up/down stairs), and can also "prefer" +specific areas and tiles, i.e preferring to go through hallways rather than maints. + +### TG Proc/File Changes: + +- `code/__HELPERS/paths/path.dm` + - new var: `/datum/can_pass_info/multiz_checks` + - `/datum/can_pass_info/New`: added `multiz_checks` argument + +### Modular Overrides: + +- `modular_oculis/master_files/code/game/turfs/open/openspace.dm` + - `/turf/open/openspace/can_cross_safely` + - `/turf/open/openspace/CanAStarPass` +- `modular_oculis/master_files/code/game/turfs/turf.dm` + - `/turf/ChangeTurf` + - new var: `/turf/astar_weight` +- `modular_oculis/master_files/code/game/atoms_movable.dm` + - `/atom/movable/Initialize` + - `/atom/movable/Moved` + - new var: `/atom/movable/astar_weight` +- `modular_oculis/master_files/code/modules/mob/living/navigation.dm` + - `/mob/living/create_navigation` + +### Defines: + +- `code/__DEFINES/~~oculis_defines/astar.dm`: `ASTAR_*` defines +- `code/__HELPERS/~~oculis_helpers/_lists.dm`: `BINARY_INSERT_DEFINE_REVERSE` + +### Included files that are not contained in this module: + +- `code/__DEFINES/~~oculis_defines/astar.dm` +- `code/__HELPERS/~~oculis_helpers/_lists.dm` +- `code/__HELPERS/~~oculis_helpers/mapping.dm` + +### Credits: + +Absolucy, dwasint diff --git a/tgstation.dme b/tgstation.dme index 4308303769ee1..5915b4f7b47af 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -9888,6 +9888,8 @@ #include "modular_nova\modules\xenoarchartifacts\obj\wave_scanner.dm" #include "modular_oculis\master_files\code\__HELPERS\files.dm" #include "modular_oculis\master_files\code\datums\http.dm" +#include "modular_oculis\master_files\code\game\atoms_movable.dm" +#include "modular_oculis\master_files\code\game\turfs\turf.dm" #include "modular_oculis\master_files\code\game\turfs\open\openspace.dm" #include "modular_oculis\master_files\code\modules\admin\verbs\getlogs.dm" #include "modular_oculis\master_files\code\modules\client\client_procs.dm" From da2779ed5a6d174f71f0118ba12225f17e125d84 Mon Sep 17 00:00:00 2001 From: Lucy Date: Wed, 4 Mar 2026 10:39:28 -0500 Subject: [PATCH 5/5] change modularization comments to match the standard --- code/__HELPERS/paths/path.dm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/__HELPERS/paths/path.dm b/code/__HELPERS/paths/path.dm index 351fb1eb5d012..f8c11f8d8cf2b 100644 --- a/code/__HELPERS/paths/path.dm +++ b/code/__HELPERS/paths/path.dm @@ -316,12 +316,12 @@ /// Require a movable var/datum/weakref/requester_ref = null - // OCULIS EDIT ADDITION START - multi-z pathfinding + // OCULIS EDIT ADDITION START - ASTAR - (multi-z pathfinding) /// Whether to do extra checks for multi-Z pathing or not. var/multiz_checks = FALSE // OCULIS EDIT ADDITION END -/datum/can_pass_info/New(atom/movable/construct_from, list/access, no_id = FALSE, call_depth = 0, multiz_checks = FALSE) // OCULIS EDIT ADDITION - multi-z pathfinding +/datum/can_pass_info/New(atom/movable/construct_from, list/access, no_id = FALSE, call_depth = 0, multiz_checks = FALSE) // OCULIS EDIT ADDITION - ASTAR - (multi-z pathfinding) // No infiniloops if(call_depth > 10) return @@ -358,7 +358,7 @@ if(construct_from.pulling) src.pulling_info = new(construct_from.pulling, access, no_id, call_depth + 1) - src.multiz_checks = multiz_checks // OCULIS EDIT ADDITION - multi-z pathfinding + src.multiz_checks = multiz_checks // OCULIS EDIT ADDITION - ASTAR - (multi-z pathfinding) /// List of vars on /datum/can_pass_info to use when checking two instances for equality GLOBAL_LIST_INIT(can_pass_info_vars, GLOBAL_PROC_REF(can_pass_check_vars))