diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm index 696c2bcc70d..cdcf1c21362 100644 --- a/code/__DEFINES/ai/blackboard_defines.dm +++ b/code/__DEFINES/ai/blackboard_defines.dm @@ -68,6 +68,21 @@ /// key holding our eating cooldown #define BB_EAT_FOOD_COOLDOWN "BB_EAT_FOOD_COOLDOWN" +// Ventcrawling + +/// key holding the turf we will move to +#define BB_VENTCRAWL_FINAL_TARGET "BB_VENTCRAWL_FINAL_TARGET" +/// key holding the vent we will move to +#define BB_VENTCRAWL_ENTRANCE "BB_VENTCRAWL_ENTRANCE" +/// key holding the vent we will ventcrawl to +#define BB_VENTCRAWL_EXIT "BB_VENTCRAWL_EXIT" +/// key holding a boolean value if we're entering into a vent +#define BB_VENTCRAWL_IS_ENTERING "BB_VENTCRAWL_IS_ENTERING" +/// key holding the amount of delay between steps in a vent (recommended: 1) +#define BB_VENTCRAWL_DELAY "BB_VENTCRAWL_DELAY" +/// key holding a range to look for vents (recommended: 10) +#define BB_VENT_SEARCH_RANGE "BB_VENT_SEARCH_RANGE" + // Tipped blackboards /// Bool that means a basic mob will start reacting to being tipped in its planning diff --git a/code/__HELPERS/paths/ventcrawl_path.dm b/code/__HELPERS/paths/ventcrawl_path.dm new file mode 100644 index 00000000000..411db4c901a --- /dev/null +++ b/code/__HELPERS/paths/ventcrawl_path.dm @@ -0,0 +1,117 @@ +/datum/ventcrawl_node + var/obj/machinery/atmospherics/atmos + /// The node we just came from + var/datum/ventcrawl_node/previous_node + /// How many steps it's taken to get here from the start + var/number_tiles = 0 + /// The A* node weight (f_value = number_of_tiles + heuristic) + var/f_value = INFINITY + +/datum/ventcrawl_node/New(obj/machinery/atmospherics/atmos, datum/ventcrawl_node/incoming_previous_node, obj/machinery/atmospherics/incoming_goal) + src.atmos = atmos + src.previous_node = incoming_previous_node + if(previous_node) + src.number_tiles = previous_node.number_tiles + 1 + src.f_value = number_tiles + get_dist_euclidian(atmos, incoming_goal) + +/datum/ventcrawl_node/Destroy(force) + previous_node = null + return ..() + +/proc/HeapPathWeightCompareVent(datum/ventcrawl_node/a, datum/ventcrawl_node/b) + return b.f_value - a.f_value + +/** + * Basic A-star implementation (I think), for pathfinding through vents. + */ +/datum/pathfind/ventcrawl + /// The movable we are pathing + var/atom/movable/requester + /// The vent we are trying to pathfind to + var/obj/machinery/atmospherics/unary/end + /// The open list/stack we pop nodes out from (TODO: make this a normal list and macro-ize the heap operations to reduce proc overhead) + var/datum/heap/open + /// The list we compile at the end if successful to pass back + var/list/path + ///An assoc list that serves as the closed list. Key is the turf, points to true if we've seen it before + var/list/closed_set + /// If we should delete the first step in the path or not. Used often because it is just the starting point + var/skip_first = FALSE + +/datum/pathfind/ventcrawl/proc/setup(atom/movable/requester, obj/machinery/atmospherics/unary/goal, skip_first, list/datum/callback/on_finish) + src.requester = requester + src.end = goal + src.on_finish = on_finish + src.skip_first = skip_first + + open = new /datum/heap(/proc/HeapPathWeightCompareVent) + closed_set = list() + +/datum/pathfind/ventcrawl/Destroy(force) + . = ..() + requester = null + end = null + open = null + +/datum/pathfind/ventcrawl/start() + start = start || requester.loc + . = ..() + if(!.) + return . + + if(start.z != end.z || start == end) //no pathfinding between z levels + return FALSE + + open.Insert(new /datum/ventcrawl_node(start, null, end)) + closed_set[start] = TRUE + return TRUE + +/datum/pathfind/ventcrawl/search_step() + . = ..() + if(!.) + return . + if(QDELETED(requester)) + return FALSE + + while(!open.IsEmpty() && !path) + var/datum/ventcrawl_node/current = open.Pop() + // var/obj/machinery/atmospherics/fast_neighbor = current.atmos.findConnecting(angle2dir_cardinal(get_angle(current.atmos, end))) + // if(fast_neighbor && !closed_set[fast_neighbor]) + // open.Insert(new /datum/ventcrawl_node(fast_neighbor, current, end)) // try to move towards it first! + // continue + closed_set[current.atmos] = TRUE + + for(var/dir in GLOB.cardinal) + if(!(dir & current.atmos.initialize_directions)) + continue + var/obj/machinery/atmospherics/neighbor = current.atmos.findConnecting(dir) + if(!neighbor || closed_set[neighbor]) + continue + if(neighbor == end) + unwind_path(current) + return + open.Insert(new /datum/ventcrawl_node(neighbor, current, end)) + + // Stable, we'll just be back later + if(TICK_CHECK) + return TRUE + return TRUE + +/datum/pathfind/ventcrawl/finished() + //we're done! turn our reversed path (end to start) into a path (start to end) + closed_set = null + QDEL_NULL(open) + path ||= list() + if(skip_first && length(path)) + path.Cut(1, 2) + hand_back(path) + return ..() + +/datum/pathfind/ventcrawl/proc/unwind_path(datum/ventcrawl_node/unwind_node) + path = list(end, unwind_node.atmos) + + while(unwind_node.previous_node) + path += unwind_node.previous_node.atmos + unwind_node = unwind_node.previous_node + + path = reverselist(path) diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm index a7b6c56ee77..4ca2e82b005 100644 --- a/code/controllers/subsystem/movement/movement_types.dm +++ b/code/controllers/subsystem/movement/movement_types.dm @@ -833,3 +833,86 @@ var/turf/next = get_step_rand(moving) moving.Move(next, get_dir(moving, next), FALSE, !(flags & MOVEMENT_LOOP_NO_DIR_UPDATE)) return old_loc != moving?.loc ? MOVELOOP_SUCCESS : MOVELOOP_FAILURE + +/** + * Used for getting to a vent in a connected pipeline when ventcrawling. + * + * Returns TRUE if the loop sucessfully started, or FALSE if it failed + * + * Arguments: + * moving - The atom we want to move + * chasing - The atom we want to move towards + * min_dist - the closest we're allower to get to the target + * delay - How many deci-seconds to wait between fires. Defaults to the lowest value, 0.1 + * timeout - Time in deci-seconds until the moveloop self expires. Defaults to infinity + * subsystem - The movement subsystem to use. Defaults to SSmovement. Only one loop can exist for any one subsystem + * priority - Defines how different move loops override each other. Lower numbers beat higher numbers, equal defaults to what currently exists. Defaults to MOVEMENT_DEFAULT_PRIORITY + * flags - Set of bitflags that effect move loop behavior in some way. Check _DEFINES/movement.dm + * +**/ +/datum/move_manager/proc/ventcrawl(moving, chasing, delay, timeout, subsystem, priority, flags, skip_first = TRUE, datum/extra_info) + return add_to_loop(moving, subsystem, /datum/move_loop/has_target/ventcrawl, priority, flags, extra_info, delay, timeout, chasing, skip_first) + +// Move loop for ventcrawling +/datum/move_loop/has_target/ventcrawl + flags = MOVEMENT_LOOP_IGNORE_GLIDE + ///A list for the path we're currently following + var/list/movement_path + ///Cooldown for repathing, prevents spam + COOLDOWN_DECLARE(repath_cooldown) + ///Bool used to determine if we're already making a path in JPS. this prevents us from re-pathing while we're already busy. + var/is_pathing = FALSE + ///Should we skip the first step? This is the tile we're currently on, which breaks some things + var/skip_first + +/datum/move_loop/has_target/ventcrawl/setup(delay, timeout, atom/chasing, skip_first) + . = ..() + if(!.) + return + src.skip_first = skip_first + +/datum/move_loop/has_target/ventcrawl/loop_started() + . = ..() + if(!movement_path) + INVOKE_ASYNC(src, PROC_REF(recalculate_path)) + +/datum/move_loop/has_target/ventcrawl/loop_stopped() + . = ..() + movement_path = null + +///Tries to calculate a new path for this moveloop. +/datum/move_loop/has_target/ventcrawl/proc/recalculate_path() + if(!COOLDOWN_FINISHED(src, repath_cooldown)) + return + COOLDOWN_START(src, repath_cooldown, 0.5 SECONDS) + if(SSpathfinder.ventcrawl_pathfind(moving, target, skip_first, list(CALLBACK(src, PROC_REF(on_finish_pathing))))) + is_pathing = TRUE + +///Called when a path has finished being created +/datum/move_loop/has_target/ventcrawl/proc/on_finish_pathing(list/path) + movement_path = path + is_pathing = FALSE + +/datum/move_loop/has_target/ventcrawl/move() + // jps has skip_first for this purpose i think but we're not jps + // while(length(movement_path) && movement_path[1] == moving.loc) + // movement_path.Cut(1, 2) + if(!length(movement_path)) + if(is_pathing) + return MOVELOOP_NOT_READY + INVOKE_ASYNC(src, PROC_REF(recalculate_path)) + return MOVELOOP_FAILURE + + var/obj/machinery/atmospherics/next_step = movement_path[1] + var/atom/old_loc = moving.loc + old_loc.relaymove(moving, get_dir(moving, next_step)) + . = (old_loc != moving?.loc) ? MOVELOOP_SUCCESS : MOVELOOP_FAILURE + + // this check if we're on exactly the next tile may be overly brittle for dense objects who may get bumped slightly + // to the side while moving but could maybe still follow their path without needing a whole new path + if(.) + if(length(movement_path)) + movement_path.Cut(1, 2) + return + INVOKE_ASYNC(src, PROC_REF(recalculate_path)) + return MOVELOOP_FAILURE diff --git a/code/controllers/subsystem/non_firing/SSpathfinder.dm b/code/controllers/subsystem/non_firing/SSpathfinder.dm index 2db351cc591..18077ed64e5 100644 --- a/code/controllers/subsystem/non_firing/SSpathfinder.dm +++ b/code/controllers/subsystem/non_firing/SSpathfinder.dm @@ -204,3 +204,12 @@ SUBSYSTEM_DEF(pathfinder) continue valid_maps += shared_source return valid_maps + +/// Initiates a pathfind. Returns true if we're good, FALSE if something's failed +/datum/controller/subsystem/pathfinder/proc/ventcrawl_pathfind(atom/movable/caller, atom/end, skip_first, list/datum/callback/on_finish) + var/datum/pathfind/ventcrawl/path = new() + path.setup(caller, end, skip_first, on_finish) + if(path.start()) + active_pathing += path + return TRUE + return FALSE diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm index 9067df3e63f..4d053af2599 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm @@ -34,6 +34,9 @@ /datum/ai_behavior/travel_towards/adjacent required_distance = 1 +/datum/ai_behavior/travel_towards/ventcrawling + new_movement_type = /datum/ai_movement/ventcrawl + /** * # Travel Towards Atom * Travel towards an atom you pass directly from the controller rather than a blackboard key. diff --git a/code/datums/ai/basic_mobs/basic_subtrees/ventcrawl_subtree.dm b/code/datums/ai/basic_mobs/basic_subtrees/ventcrawl_subtree.dm new file mode 100644 index 00000000000..b590cf7067e --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/ventcrawl_subtree.dm @@ -0,0 +1,171 @@ +/datum/ai_planning_subtree/ventcrawl + +/datum/ai_planning_subtree/ventcrawl/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + // we're in a pipe + if(istype(controller.pawn.loc, /obj/machinery/atmospherics)) + // we have an exit vent to go to + if(controller.blackboard_key_exists(BB_VENTCRAWL_EXIT)) + var/obj/machinery/atmospherics/A = controller.blackboard[BB_VENTCRAWL_EXIT] + if(!A.can_crawl_through()) + controller.set_blackboard_key(BB_VENTCRAWL_ENTRANCE, null) + controller.set_blackboard_key(BB_VENTCRAWL_EXIT, null) + return .() // call this proc again + // lets go there! + controller.queue_behavior(/datum/ai_behavior/travel_towards/ventcrawling, BB_VENTCRAWL_EXIT) + return SUBTREE_RETURN_FINISH_PLANNING + // We dont have an exit vent, but we have a target. Let's find an exit vent near it. + if(controller.blackboard_key_exists(BB_VENTCRAWL_FINAL_TARGET)) + controller.queue_behavior(/datum/ai_behavior/find_and_set/ventcrawl, BB_VENTCRAWL_EXIT, null, controller.blackboard[BB_VENT_SEARCH_RANGE]) + return + // we dont have an set exit vent nor a final target... let's just get out of the pipe. + controller.queue_behavior(/datum/ai_behavior/find_and_set/pipenet_vent, BB_VENTCRAWL_EXIT, null, controller.blackboard[BB_VENT_SEARCH_RANGE]) + return + + if(!controller.blackboard_key_exists(BB_VENTCRAWL_FINAL_TARGET)) + return + + // we have a target and we're nearby + if(controller.blackboard_key_exists(BB_VENT_SEARCH_RANGE) && get_dist(controller.pawn, controller.blackboard[BB_VENTCRAWL_FINAL_TARGET]) <= controller.blackboard[BB_VENT_SEARCH_RANGE]) + // sometimes mobs can get stuck here if they're behind a wall... dunno how to fix this. + controller.set_blackboard_key(BB_VENTCRAWL_ENTRANCE, null) + controller.set_blackboard_key(BB_VENTCRAWL_EXIT, null) + controller.queue_behavior(/datum/ai_behavior/travel_towards/stop_on_arrival, BB_VENTCRAWL_FINAL_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + // we're far-away, lets try to find a ventcrawl path + if(!controller.blackboard_key_exists(BB_VENTCRAWL_EXIT)) + controller.queue_behavior(/datum/ai_behavior/find_and_set/ventcrawl, BB_VENTCRAWL_EXIT, null, controller.blackboard[BB_VENT_SEARCH_RANGE], FALSE) + return + // we found a path, lets take it! + controller.queue_behavior(/datum/ai_behavior/interact_with_vent, BB_VENTCRAWL_ENTRANCE) + return SUBTREE_RETURN_FINISH_PLANNING + +/** + * This proc assumes that all ventcrawling creatures are omniscient. + */ +/datum/ai_behavior/find_and_set/ventcrawl/search_tactic(datum/ai_controller/controller, locate_path, search_range) + // we're inside a vent already, lets look for another way + if(istype(controller.pawn.loc, /obj/machinery/atmospherics)) + var/obj/machinery/atmospherics/A = controller.pawn.loc + var/datum/pipeline/P = A.returnPipenet() + var/list/datum/pipeline/connected_pipelines = P.get_connected_pipelines() + for(var/turf/T in spiral_range_turfs(search_range, controller.blackboard[BB_VENTCRAWL_FINAL_TARGET])) // get the nearest vents to the target first + for(var/obj/machinery/atmospherics/exit in T) + if(!is_type_in_list(exit, GLOB.ventcrawl_machinery)) + continue + if(!exit.can_crawl_through()) + continue + if(exit.returnPipenet() in connected_pipelines) + return exit + return + + // else, lets do a search for vents we have access to + var/mob/M = controller.pawn + var/datum/can_pass_info/pass_info = new(M, M.get_access()) + + var/list/obj/machinery/atmospherics/entrance_vents = nearby_vents(get_turf(controller.pawn), FALSE, search_range, pass_info) + if(!length(entrance_vents)) + return + + var/list/obj/machinery/atmospherics/exit_vents = nearby_vents(get_turf(controller.blackboard[BB_VENTCRAWL_FINAL_TARGET]), TRUE, search_range, pass_info) + if(!length(exit_vents)) + return + + // try to find ones that share a pipeline first + for(var/obj/machinery/atmospherics/exit in exit_vents) + var/datum/pipeline/exit_pipeline = exit.returnPipenet() + for(var/obj/machinery/atmospherics/entrance in entrance_vents) + if(exit_pipeline == entrance.returnPipenet()) // It will probably be quicker if theyre on the same pipenet + controller.set_blackboard_key(BB_VENTCRAWL_ENTRANCE, entrance) + return exit + + // fuck it, search all possible paths + for(var/obj/machinery/atmospherics/exit in exit_vents) + var/datum/pipeline/exit_pipeline = exit.returnPipenet() + var/list/obj/machinery/atmospherics/vents = exit_pipeline.get_ventcrawls() + for(var/obj/machinery/atmospherics/entrance in (entrance_vents & vents)) + if(entrance != exit) // They're connected and not the same vent + controller.set_blackboard_key(BB_VENTCRAWL_ENTRANCE, entrance) + return exit + +/** + * Find something interesting nearby that we have access to. + */ +/datum/ai_behavior/find_and_set/ventcrawl/proc/nearby_vents(initial_location, reversed, max_dist, pass_info) + // to search + var/list/open_set = list(initial_location) + // already searched + var/list/closed_set = list() + // the vents to return + var/list/obj/machinery/atmospherics/vents = list() + + while(length(open_set)) + var/turf/current = popleft(open_set) + closed_set |= current + for(var/obj/machinery/atmospherics/atmos in current) + if(is_type_in_list(atmos, GLOB.ventcrawl_machinery) && atmos.can_crawl_through()) + vents |= atmos + + for(var/dir in GLOB.cardinal) + var/turf/next = get_step(current, dir) + if((next in closed_set) || (get_dist(initial_location, next) > max_dist)) + continue + // some accesses are directionally sensitive, like one-way airlocks + if(reversed) + if(current.density || next.LinkBlockedWithAccess(current, pass_info)) + continue + else + if(next.density || current.LinkBlockedWithAccess(next, pass_info)) + continue + open_set |= next + + return vents + +/datum/ai_behavior/find_and_set/pipenet_vent/search_tactic(datum/ai_controller/controller, locate_path, search_range) + // presume that all ventcrawling creatures are omniscient, at least about vents :) + if(!istype(controller.pawn.loc, /obj/machinery/atmospherics)) + return + var/obj/machinery/atmospherics/atmos_loc = controller.pawn.loc + var/datum/pipeline/P = atmos_loc.returnPipenet() + if(!P) + return + + var/obj/machinery/atmospherics/closest + var/closest_dist = INFINITY + for(var/obj/machinery/atmospherics/vent in P.other_atmosmch) + var/vent_dist = get_dist(atmos_loc, vent) + if(vent_dist <= closest_dist) + closest = vent + closest_dist = vent_dist + + return closest + +/datum/ai_behavior/interact_with_vent + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + action_cooldown = 1 SECONDS + +/datum/ai_behavior/interact_with_vent/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/interact_with_vent/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + var/obj/machinery/atmospherics/vent = controller.blackboard[target_key] + var/mob/living/L = controller.pawn + if(!istype(L)) + stack_trace("Something got a non-living pawn to try ventcrawling. How peculiar.") + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + if(controller.blackboard[BB_VENTCRAWL_IS_ENTERING] == TRUE) + return AI_BEHAVIOR_DELAY + controller.set_blackboard_key(BB_VENTCRAWL_IS_ENTERING, TRUE) + L.handle_ventcrawl(vent) + controller.set_blackboard_key(BB_VENTCRAWL_IS_ENTERING, FALSE) + if(controller.pawn.loc != vent) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED diff --git a/code/datums/ai/movement/ai_movement.dm b/code/datums/ai/movement/ai_movement.dm index 98f302111ab..d9d5e1c5ff5 100644 --- a/code/datums/ai/movement/ai_movement.dm +++ b/code/datums/ai/movement/ai_movement.dm @@ -4,6 +4,8 @@ var/list/moving_controllers = list() /// How many times a given controller can fail on their route before they just give up. var/max_pathing_attempts + /// Should this movement update the delay when a moveloop is about to happen? + var/update_moveloop_delay = TRUE /// Override this to setup the moveloop you want to use. /datum/ai_movement/proc/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance) @@ -59,7 +61,8 @@ qdel(source) // stop moving return MOVELOOP_SKIP_STEP - source.delay = controller.movement_delay + if(update_moveloop_delay) + source.delay = controller.movement_delay if(allowed_to_move(source)) return NONE diff --git a/code/datums/ai/movement/ventcrawl.dm b/code/datums/ai/movement/ventcrawl.dm new file mode 100644 index 00000000000..ada02bd3152 --- /dev/null +++ b/code/datums/ai/movement/ventcrawl.dm @@ -0,0 +1,32 @@ +/datum/ai_movement/ventcrawl + max_pathing_attempts = 10 + update_moveloop_delay = FALSE + +/datum/ai_movement/ventcrawl/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance) + . = ..() + var/atom/movable/moving = controller.pawn + // var/delay = controller.movement_delay / 4 // mobs typically move a lot faster in pipes, so divide by 3 + var/delay = controller.blackboard[BB_VENTCRAWL_DELAY] || 1 // mobs typically move a lot faster in pipes + var/datum/move_loop/loop = GLOB.move_manager.ventcrawl(moving, current_movement_target, delay, skip_first = TRUE, subsystem = SSai_movement, extra_info = controller) + RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move)) + RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) + +/datum/ai_movement/ventcrawl/allowed_to_move(datum/move_loop/has_target/ventcrawl/source) + // maybe check if the tile it would move it into make it exit the ventcrawling?? Probably a good idea since it can get sabo'd + // this check will do for now. + if(!length(source.movement_path)) + return FALSE + var/atom/A = source.movement_path[1] + return !QDELETED(A) + +/datum/ai_movement/ventcrawl/increment_pathing_failures(datum/ai_controller/controller) + . = ..() + var/obj/machinery/atmospherics/exit = controller.blackboard[BB_VENTCRAWL_EXIT] + var/datum/pipeline/exit_pipeline = exit?.returnPipenet() + var/obj/machinery/atmospherics/current = controller.pawn.loc + if(current?.returnPipenet() in exit_pipeline?.get_connected_pipelines()) // they're still connected... let it try to find another route. + return + // they're not connected, give us another chance to find a way out. + controller.set_blackboard_key(BB_VENTCRAWL_ENTRANCE, null) + controller.set_blackboard_key(BB_VENTCRAWL_EXIT, null) + controller.cancel_actions() diff --git a/code/game/objects/obj_defense.dm b/code/game/objects/obj_defense.dm index e12847d2e64..5e9099bade5 100644 --- a/code/game/objects/obj_defense.dm +++ b/code/game/objects/obj_defense.dm @@ -107,7 +107,7 @@ return 0 else var/play_soundeffect = 1 - if(M.environment_smash) + if(istype(M) && M.environment_smash) play_soundeffect = 0 var/obj_turf = get_turf(src) // play from the turf in case the object gets deleted mid attack if(M.obj_damage) diff --git a/code/modules/atmospherics/machinery/atmospherics.dm b/code/modules/atmospherics/machinery/atmospherics.dm index 021317f90fd..aa1c62b5a0b 100644 --- a/code/modules/atmospherics/machinery/atmospherics.dm +++ b/code/modules/atmospherics/machinery/atmospherics.dm @@ -466,4 +466,7 @@ Pipelines + Other Objects -> Pipe network if(user) to_chat(user, "You set the target pressure of [src] to maximum.") +/obj/machinery/atmospherics/proc/get_machinery_pipelines() + return list() + #undef VENT_SOUND_DELAY diff --git a/code/modules/atmospherics/machinery/components/binary_devices/binary_atmos_base.dm b/code/modules/atmospherics/machinery/components/binary_devices/binary_atmos_base.dm index 7a8d2465e1f..97dce15d00d 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/binary_atmos_base.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/binary_atmos_base.dm @@ -131,6 +131,9 @@ else if(A == node2) return parent2 +/obj/machinery/atmospherics/binary/get_machinery_pipelines() + return list(parent1, parent2) + /obj/machinery/atmospherics/binary/is_pipenet_split() return TRUE diff --git a/code/modules/atmospherics/machinery/components/trinary_devices/trinary_base.dm b/code/modules/atmospherics/machinery/components/trinary_devices/trinary_base.dm index ce1b0dfd003..42161a7751d 100644 --- a/code/modules/atmospherics/machinery/components/trinary_devices/trinary_base.dm +++ b/code/modules/atmospherics/machinery/components/trinary_devices/trinary_base.dm @@ -182,8 +182,11 @@ else if(A == node3) return parent3 +/obj/machinery/atmospherics/trinary/get_machinery_pipelines() + return list(parent1, parent2, parent3) + /obj/machinery/atmospherics/trinary/is_pipenet_split() - return FALSE + return TRUE /obj/machinery/atmospherics/trinary/replacePipenet(datum/pipeline/Old, datum/pipeline/New) if(Old == parent1) diff --git a/code/modules/atmospherics/machinery/components/unary_devices/unary_base.dm b/code/modules/atmospherics/machinery/components/unary_devices/unary_base.dm index dbd529be80d..99202ade15a 100644 --- a/code/modules/atmospherics/machinery/components/unary_devices/unary_base.dm +++ b/code/modules/atmospherics/machinery/components/unary_devices/unary_base.dm @@ -90,6 +90,9 @@ /obj/machinery/atmospherics/unary/returnPipenet() return parent +/obj/machinery/atmospherics/unary/get_machinery_pipelines() + return parent + /obj/machinery/atmospherics/unary/replacePipenet(datum/pipeline/Old, datum/pipeline/New) if(Old == parent) parent = New diff --git a/code/modules/atmospherics/machinery/datum_pipeline.dm b/code/modules/atmospherics/machinery/datum_pipeline.dm index fb77f74cc46..fd3454c4daa 100644 --- a/code/modules/atmospherics/machinery/datum_pipeline.dm +++ b/code/modules/atmospherics/machinery/datum_pipeline.dm @@ -231,3 +231,30 @@ /datum/pipeline/proc/remove_ventcrawler(mob/living/crawler) UnregisterSignal(crawler, COMSIG_LIVING_EXIT_VENTCRAWL) crawlers -= crawler + +/** + * Gets all pipelines connected to this with valves, including src. + */ +/datum/pipeline/proc/get_connected_pipelines() + . = list() + var/list/possible_expansions = list(src) + while(length(possible_expansions)) + var/datum/pipeline/P = popleft(possible_expansions) + if(!P) + break + . |= P + for(var/obj/machinery/atmospherics/V in P.other_atmosmch) + for(var/datum/pipeline/possible in V.get_machinery_pipelines()) + if(possible in .) + continue + possible_expansions |= possible + +/datum/pipeline/proc/get_ventcrawls(check_welded = TRUE) + . = list() + for(var/datum/pipeline/P in get_connected_pipelines()) + for(var/obj/machinery/atmospherics/V in P.other_atmosmch) + if(!is_type_in_list(V, GLOB.ventcrawl_machinery)) + continue + if(check_welded && !V.can_crawl_through()) + continue + . |= V diff --git a/code/modules/mob/living/carbon/carbon_procs.dm b/code/modules/mob/living/carbon/carbon_procs.dm index 7026304b167..544f7337b19 100644 --- a/code/modules/mob/living/carbon/carbon_procs.dm +++ b/code/modules/mob/living/carbon/carbon_procs.dm @@ -541,8 +541,6 @@ GLOBAL_LIST_INIT(ventcrawl_machinery, list(/obj/machinery/atmospherics/unary/ven var/ventcrawl_delay = 0 SECONDS #else var/ventcrawl_delay = 4.5 SECONDS - if(!client) - return #endif if(ismorph(src)) if(!do_after(src, ventcrawl_delay, target = src, hidden = TRUE)) diff --git a/paradise.dme b/paradise.dme index 9435d85b2fc..5a18e18b41b 100644 --- a/paradise.dme +++ b/paradise.dme @@ -214,6 +214,7 @@ #include "code\__HELPERS\paths\jps.dm" #include "code\__HELPERS\paths\path.dm" #include "code\__HELPERS\paths\sssp.dm" +#include "code\__HELPERS\paths\ventcrawl_path.dm" #include "code\__HELPERS\sorts\__main.dm" #include "code\__HELPERS\sorts\InsertSort.dm" #include "code\__HELPERS\sorts\MergeSort.dm" @@ -472,6 +473,7 @@ #include "code\datums\ai\basic_mobs\basic_subtrees\target_retaliate.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\tip_reaction.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\use_mob_ability.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\ventcrawl_subtree.dm" #include "code\datums\ai\basic_mobs\hunting_behavior\find_and_hunt_target.dm" #include "code\datums\ai\basic_mobs\hunting_behavior\find_hunt_target.dm" #include "code\datums\ai\basic_mobs\hunting_behavior\hunt_target.dm" @@ -483,6 +485,7 @@ #include "code\datums\ai\movement\ai_movement_jps.dm" #include "code\datums\ai\movement\basic_avoidance.dm" #include "code\datums\ai\movement\complete_stop.dm" +#include "code\datums\ai\movement\ventcrawl.dm" #include "code\datums\ai\targeting_strategy\basic_targeting_strategy.dm" #include "code\datums\ai\targeting_strategy\dont_target_friends.dm" #include "code\datums\ai\targeting_strategy\targeting_strategy.dm"