/* * Portions of this file are copyright Rebirth contributors and licensed as * described in COPYING.txt. * Portions of this file are copyright Parallax Software and licensed * according to the Parallax license below. * See COPYING.txt for license details. THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX SOFTWARE CORPORATION ("PARALLAX"). PARALLAX, IN DISTRIBUTING THE CODE TO END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS IN USING, DISPLAYING, AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE FREE PURPOSES. IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES. THE END-USER UNDERSTANDS AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE. COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED. */ /* * * AI path forming stuff. * */ #include #include #include // for printf() #include // for d_rand() and qsort() #include // for memset() #include "inferno.h" #include "console.h" #include "3d.h" #include "object.h" #include "dxxerror.h" #include "ai.h" #include "robot.h" #include "fvi.h" #include "gameseg.h" #include "physics.h" #include "wall.h" #if DXX_USE_EDITOR #include "editor/editor.h" #include "editor/esegment.h" #endif #include "player.h" #include "game.h" #include "compiler-range_for.h" #include "partial_range.h" #include "d_levelstate.h" // Length in segments of avoidance path #define AVOID_SEG_LENGTH 7 #ifdef NDEBUG #define PATH_VALIDATION 0 #else #define PATH_VALIDATION 1 #endif namespace dsx { namespace { static void ai_path_set_orient_and_vel(const d_robot_info_array &Robot_info, object &objp, const vms_vector &goal_point #if defined(DXX_BUILD_DESCENT_II) , player_visibility_state player_visibility, const vms_vector *vec_to_player #endif ); static void maybe_ai_path_garbage_collect(void); static void ai_path_garbage_collect(void); #if PATH_VALIDATION static void validate_all_paths(); static int validate_path(int, point_seg* psegs, uint_fast32_t num_points); #endif // ----------------------------------------------------------------------------------------------------------- // Insert the point at the center of the side connecting two segments between the two points. // This is messy because we must insert into the list. The simplest (and not too slow) way to do this is to start // at the end of the list and go backwards. static uint_fast32_t insert_center_points(segment_array &segments, point_seg *psegs, uint_fast32_t count) { auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state(); auto &Vertices = LevelSharedVertexState.get_vertices(); auto &vcsegptr = segments.vcptr; auto &vcsegptridx = segments.vcptridx; if (count < 2) return count; uint_fast32_t last_point = count - 1; for (uint_fast32_t i = last_point; i; --i) { psegs[2*i] = psegs[i]; const auto &&seg1 = vcsegptr(psegs[i-1].segnum); auto connect_side = find_connect_side(vcsegptridx(psegs[i].segnum), seg1); Assert(connect_side != side_none); // Impossible! These two segments must be connected, they were created by create_path_points (which was created by mk!) if (connect_side == side_none) // Try to blow past the assert, this should at least prevent a hang. connect_side = sidenum_t::WLEFT; auto &vcvertptr = Vertices.vcptr; const auto &¢er_point = compute_center_point_on_side(vcvertptr, seg1, connect_side); auto new_point = vm_vec_sub(psegs[i-1].point, center_point); new_point.x /= 16; new_point.y /= 16; new_point.z /= 16; vm_vec_sub(psegs[2*i-1].point, center_point, new_point); #if defined(DXX_BUILD_DESCENT_II) const auto &&segp = segments.imptridx(psegs[2*i].segnum); const auto &&temp_segnum = find_point_seg(LevelSharedSegmentState, psegs[2*i-1].point, segp); if (temp_segnum == segment_none) { psegs[2*i-1].point = center_point; find_point_seg(LevelSharedSegmentState, psegs[2*i-1].point, segp); } #endif psegs[2*i-1].segnum = psegs[2*i].segnum; count++; } #if defined(DXX_BUILD_DESCENT_II) // Now, remove unnecessary center points. // A center point is unnecessary if it is close to the line between the two adjacent points. // MK, OPTIMIZE! Can get away with about half the math since every vector gets computed twice. for (uint_fast32_t i = 1; i < count - 1; i += 2) { vms_vector temp1, temp2; const auto dot = vm_vec_dot(vm_vec_sub(temp1, psegs[i].point, psegs[i-1].point), vm_vec_sub(temp2, psegs[i+1].point, psegs[i].point)); if (dot * 9/8 > fixmul(vm_vec_mag(temp1), vm_vec_mag(temp2))) psegs[i].segnum = segment_none; } // Now, scan for points with segnum == -1 auto predicate = [](const point_seg &p) { return p.segnum == segment_none; }; count = std::distance(psegs, std::remove_if(psegs, psegs + count, predicate)); #endif return count; } #if defined(DXX_BUILD_DESCENT_II) // ----------------------------------------------------------------------------------------------------------- // Move points halfway to outside of segment. static void move_towards_outside(const d_level_shared_segment_state &LevelSharedSegmentState, point_seg *const psegs, const unsigned num_points, const vmobjptridx_t objp, const create_path_random_flag rand_flag) { int i; std::array new_psegs; assert(num_points < new_psegs.size()); auto &Segments = LevelSharedSegmentState.get_segments(); auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state(); auto &Vertices = LevelSharedVertexState.get_vertices(); auto &vcvertptr = Vertices.vcptr; for (i = 1; i < num_points - 1; ++i) { fix segment_size; segnum_t segnum; vms_vector e; int count; const auto &&temp_segnum = find_point_seg(LevelSharedSegmentState, psegs[i].point, Segments.vcptridx(psegs[i].segnum)); Assert(temp_segnum != segment_none); psegs[i].segnum = temp_segnum; segnum = psegs[i].segnum; // I don't think we can use quick version here and this is _very_ rarely called. --MK, 07/03/95 const auto a = vm_vec_normalized_quick(vm_vec_sub(psegs[i].point, psegs[i-1].point)); const auto b = vm_vec_normalized_quick(vm_vec_sub(psegs[i+1].point, psegs[i].point)); const auto c = vm_vec_sub(psegs[i+1].point, psegs[i-1].point); if (abs(vm_vec_dot(a, b)) > 3*F1_0/4 ) { if (abs(a.z) < F1_0/2) { if (rand_flag != create_path_random_flag::nonrandom) { e.x = (d_rand()-16384)/2; e.y = (d_rand()-16384)/2; e.z = abs(e.x) + abs(e.y) + 1; vm_vec_normalize_quick(e); } else { e.x = 0; e.y = 0; e.z = F1_0; } } else { if (rand_flag != create_path_random_flag::nonrandom) { e.y = (d_rand()-16384)/2; e.z = (d_rand()-16384)/2; e.x = abs(e.y) + abs(e.z) + 1; vm_vec_normalize_quick(e); } else { e.x = F1_0; e.y = 0; e.z = 0; } } } else { const auto d = vm_vec_cross(a, b); vm_vec_cross(e, c, d); vm_vec_normalize_quick(e); } if (vm_vec_mag_quick(e) < F1_0/2) Int3(); const shared_segment &segp = *vcsegptr(segnum); segment_size = vm_vec_dist_quick(vcvertptr(segp.verts[segment_relative_vertnum::_0]), vcvertptr(segp.verts[segment_relative_vertnum::_6])); if (segment_size > F1_0*40) segment_size = F1_0*40; auto goal_pos = vm_vec_scale_add(psegs[i].point, e, segment_size/4); count = 3; while (count) { fvi_info hit_data; auto &p0 = psegs[i].point; const auto hit_type = find_vector_intersection(fvi_query{ p0, goal_pos, fvi_query::unused_ignore_obj_list, fvi_query::unused_LevelUniqueObjectState, fvi_query::unused_Robot_info, 0, objp, }, psegs[i].segnum, objp->size, hit_data); if (hit_type == fvi_hit_type::None) count = 0; else { if (count == 3 && hit_type == fvi_hit_type::BadP0) Int3(); goal_pos.x = (p0.x + hit_data.hit_pnt.x)/2; goal_pos.y = (p0.y + hit_data.hit_pnt.y)/2; goal_pos.z = (p0.z + hit_data.hit_pnt.z)/2; count--; if (count == 0) { // Couldn't move towards outside, that's ok, sometimes things can't be moved. goal_pos = psegs[i].point; } } } // Only move towards outside if remained inside segment. const auto &&new_segnum = find_point_seg(LevelSharedSegmentState, goal_pos, Segments.vcptridx(psegs[i].segnum)); if (new_segnum == psegs[i].segnum) { new_psegs[i].point = goal_pos; new_psegs[i].segnum = new_segnum; } else { new_psegs[i].point = psegs[i].point; new_psegs[i].segnum = psegs[i].segnum; } } for (i = 1; i < num_points - 1; ++i) psegs[i] = new_psegs[i]; } #endif } // ----------------------------------------------------------------------------------------------------------- // Create a path from objp->pos to the center of end_seg. // Return a list of (segment_num, point_locations) at psegs // Return number of points in *num_points. // if max_depth == -1, then there is no maximum depth. // If unable to create path, return -1, else return 0. // If random_flag !0, then introduce randomness into path by looking at sides in random order. This means // that a path between two segments won't always be the same, unless it is unique. // If safety_flag is set, then additional points are added to "make sure" that points are reachable. I would // like to say that it ensures that the object can move between the points, but that would require knowing what // the object is (which isn't passed, right?) and making fvi calls (slow, right?). So, consider it the more_or_less_safe_flag. // If end_seg == -2, then end seg will never be found and this routine will drop out due to depth (probably called by create_n_segment_path). std::pair create_path_points(const vmobjptridx_t objp, const robot_info *const robptr, const vcsegidx_t start_seg, icsegidx_t end_seg, point_seg_array_t::iterator psegs, const unsigned max_depth, create_path_random_flag random_flag, const create_path_safety_flag safety_flag, icsegidx_t avoid_seg) { #if defined(DXX_BUILD_DESCENT_II) auto &Objects = LevelUniqueObjectState.Objects; auto &vmobjptr = Objects.vmptr; #endif segnum_t cur_seg; int qtail = 0, qhead = 0; int i; std::array seg_queue; int cur_depth; point_seg_array_t::iterator original_psegs = psegs; unsigned l_num_points = 0; #if PATH_VALIDATION validate_all_paths(); #endif auto &obj = *objp; if (obj.type == OBJ_ROBOT && obj.ctype.ai_info.behavior == ai_behavior::AIB_RUN_FROM) { random_flag = create_path_random_flag::random; avoid_seg = ConsoleObject->segnum; // Int3(); } visited_segment_bitarray_t visited; std::array depth{}; // If there is a segment we're not allowed to visit, mark it. if (avoid_seg != segment_none) { Assert(avoid_seg <= Highest_segment_index); if ((start_seg != avoid_seg) && (end_seg != avoid_seg)) { visited[avoid_seg] = true; } } cur_seg = start_seg; visited[cur_seg] = true; cur_depth = 0; auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state(); auto &Vertices = LevelSharedVertexState.get_vertices(); auto &vcvertptr = Vertices.vcptr; auto &Walls = LevelUniqueWallSubsystemState.Walls; auto &vcwallptr = Walls.vcptr; per_side_array side_traversal_translation; std::iota(side_traversal_translation.begin(), side_traversal_translation.end(), sidenum_t{}); #if defined(DXX_BUILD_DESCENT_I) /* Descent 1 can only shuffle once, before the loop begins. * Descent 2 can shuffle on every pass of the loop. */ if (random_flag != create_path_random_flag::nonrandom) std::shuffle(side_traversal_translation.begin(), side_traversal_translation.end(), std::minstd_rand(d_rand())); #elif defined(DXX_BUILD_DESCENT_II) /* If shuffling is enabled, always shuffle on the first pass of the * loop. */ unsigned shuffle_random_flag = 0; std::minstd_rand mrd((random_flag != create_path_random_flag::nonrandom) ? d_rand() : std::minstd_rand::default_seed ); std::uniform_int_distribution uid03(0, 3); #endif while (cur_seg != end_seg) { const cscusegment &&segp = vcsegptr(cur_seg); #if defined(DXX_BUILD_DESCENT_II) /* If path randomness is enabled, conditionally shuffle based on * the value of shuffle_random_flag. Since shuffle_random_flag * is initialized to 0 above, it is guaranteed to shuffle on the * first iteration of the while loop. This is done since * side_traversal_translation was initialized via std::iota, and * is therefore predictable. However, if path randomness is * enabled by the caller, side_traversal_translation should be * unpredictable, and therefore a shuffle is needed. * * Each subsequent iteration may or may not shuffle, depending * on the value from d_rand in the most recent loop. Regardless * of whether a pass shuffles, it will reload the value so that * the next pass may shuffle. This is necessary to avoid * pinning the value in no-shuffle mode after the first time * d_rand returns a no-shuffle value. */ if (random_flag != create_path_random_flag::nonrandom && std::exchange(shuffle_random_flag, uid03(mrd)) == 0) { std::shuffle(side_traversal_translation.begin(), side_traversal_translation.end(), mrd); } #endif for (const auto snum : side_traversal_translation) { if (!IS_CHILD(segp.s.children[snum])) continue; #if defined(DXX_BUILD_DESCENT_I) #define AI_DOOR_OPENABLE_PLAYER_FLAGS #elif defined(DXX_BUILD_DESCENT_II) #define AI_DOOR_OPENABLE_PLAYER_FLAGS player_info.powerup_flags, auto &player_info = get_local_plrobj().ctype.player_info; #endif if ((WALL_IS_DOORWAY(GameBitmaps, Textures, vcwallptr, segp, snum) & WALL_IS_DOORWAY_FLAG::fly) || ai_door_is_openable(obj, robptr, AI_DOOR_OPENABLE_PLAYER_FLAGS segp, snum)) #undef AI_DOOR_OPENABLE_PLAYER_FLAGS { const auto this_seg = segp.s.children[snum]; #if defined(DXX_BUILD_DESCENT_II) Assert(this_seg != segment_none); if (((cur_seg == avoid_seg) || (this_seg == avoid_seg)) && (ConsoleObject->segnum == avoid_seg)) { const auto &¢er_point = compute_center_point_on_side(vcvertptr, segp, snum); fvi_info hit_data; const auto hit_type = find_vector_intersection(fvi_query{ obj.pos, center_point, fvi_query::unused_ignore_obj_list, fvi_query::unused_LevelUniqueObjectState, fvi_query::unused_Robot_info, 0, objp, }, obj.segnum, obj.size, hit_data); if (hit_type != fvi_hit_type::None) { goto dont_add; } } #endif if (!visited[this_seg]) { seg_queue[qtail].start = cur_seg; seg_queue[qtail].end = this_seg; visited[this_seg] = true; depth[qtail++] = cur_depth+1; if (depth[qtail-1] == max_depth) { end_seg = seg_queue[qtail-1].end; goto cpp_done1; } // end if (depth[... } // end if (!visited... } // if (WALL_IS_DOORWAY(... #if defined(DXX_BUILD_DESCENT_II) dont_add: ; #endif } if (qtail <= 0) break; if (qhead >= qtail) { // Couldn't get to goal, return a path as far as we got, which probably acceptable to the unparticular caller. end_seg = seg_queue[qtail-1].end; break; } cur_seg = seg_queue[qhead].end; cur_depth = depth[qhead]; qhead++; cpp_done1: ; } // while (cur_seg ... if (qtail > 0) { // Set qtail to the segment which ends at the goal. while (seg_queue[--qtail].end != end_seg) if (qtail < 0) { return std::make_pair(create_path_result::early, l_num_points); } } else qtail = -1; #if defined(DXX_BUILD_DESCENT_I) #if DXX_USE_EDITOR Selected_segs.clear(); #endif #endif while (qtail >= 0) { segnum_t parent_seg, this_seg; this_seg = seg_queue[qtail].end; parent_seg = seg_queue[qtail].start; psegs->segnum = this_seg; compute_segment_center(vcvertptr, psegs->point, vcsegptr(this_seg)); psegs++; l_num_points++; #if defined(DXX_BUILD_DESCENT_I) #if DXX_USE_EDITOR Selected_segs.emplace_back(this_seg); #endif #endif if (parent_seg == start_seg) break; while (seg_queue[--qtail].end != parent_seg) Assert(qtail >= 0); } psegs->segnum = start_seg; compute_segment_center(vcvertptr, psegs->point, vcsegptr(start_seg)); psegs++; l_num_points++; #if PATH_VALIDATION validate_path(1, original_psegs, l_num_points); #endif // Now, reverse point_segs in place. for (i=0; i< l_num_points/2; i++) { point_seg temp_point_seg = *(original_psegs + i); *(original_psegs + i) = *(original_psegs + l_num_points - i - 1); *(original_psegs + l_num_points - i - 1) = temp_point_seg; } #if PATH_VALIDATION validate_path(2, original_psegs, l_num_points); #endif // Now, if safety_flag set, then insert the point at the center of the side connecting two segments // between the two points. This is messy because we must insert into the list. The simplest (and not too slow) // way to do this is to start at the end of the list and go backwards. if (safety_flag != create_path_safety_flag::unsafe) { if (psegs - Point_segs + l_num_points + 2 > MAX_POINT_SEGS) { // Ouch! Cannot insert center points in path. So return unsafe path. ai_reset_all_paths(); return std::make_pair(create_path_result::early, l_num_points); } else { l_num_points = insert_center_points(Segments, original_psegs, l_num_points); } } #if PATH_VALIDATION validate_path(3, original_psegs, l_num_points); #endif #if defined(DXX_BUILD_DESCENT_II) // -- MK, 10/30/95 -- This code causes apparent discontinuities in the path, moving a point // into a new segment. It is not necessarily bad, but it makes it hard to track down actual // discontinuity problems. auto &Robot_info = LevelSharedRobotInfoState.Robot_info; if (obj.type == OBJ_ROBOT) if (Robot_info[get_robot_id(obj)].companion) move_towards_outside(LevelSharedSegmentState, original_psegs, l_num_points, objp, create_path_random_flag::nonrandom); #endif #if PATH_VALIDATION validate_path(4, original_psegs, l_num_points); #endif return std::make_pair(create_path_result::finished, l_num_points); } #if defined(DXX_BUILD_DESCENT_II) // ------------------------------------------------------------------------------------------------------- // polish_path // Takes an existing path and makes it nicer. // Drops as many leading points as possible still maintaining direct accessibility // from current position to first point. // Will not shorten path to fewer than 3 points. // Returns number of points. // Starting position in psegs doesn't change. // Changed, MK, 10/18/95. I think this was causing robots to get hung up on walls. // Only drop up to the first three points. int polish_path(const vmobjptridx_t objp, point_seg *psegs, int num_points) { auto &BuddyState = LevelUniqueObjectState.BuddyState; int i, first_point=0; if (num_points <= 4) return num_points; // Prevent the buddy from polishing his path twice in one tick, which can cause him to get hung up. Pretty ugly, huh? auto &Robot_info = LevelSharedRobotInfoState.Robot_info; auto &obj = *objp; if (Robot_info[get_robot_id(obj)].companion) { if (d_tick_count == BuddyState.Last_buddy_polish_path_tick) return num_points; else BuddyState.Last_buddy_polish_path_tick = d_tick_count; } // -- MK: 10/18/95: for (i=0; isegnum; if (curseg > Highest_segment_index) { Int3(); // Contact Mike: Debug trap for elusive, nasty bug. return 0; } range_for (const auto &ps, unchecked_partial_range(psegs, 1u, num_points)) { auto nextseg = ps.segnum; if (curseg != nextseg) { const shared_segment &csegp = vcsegptr(curseg); auto &children = csegp.children; if (std::find(children.begin(), children.end(), nextseg) == children.end()) { // Assert(sidenum != MAX_SIDES_PER_SEGMENT); // Hey, created path is not contiguous, why!? Int3(); return 0; } curseg = nextseg; } } #endif return 1; } // ----------------------------------------------------------------------------------------------------------- void validate_all_paths(void) { #if PATH_VALIDATION auto &Objects = LevelUniqueObjectState.Objects; auto &vmobjptr = Objects.vmptr; range_for (const auto &&objp, vmobjptr) { auto &obj = *objp; if (obj.type == OBJ_ROBOT) { auto &aip = obj.ctype.ai_info; if (obj.control_source == object::control_type::ai) { if ((aip.hide_index != -1) && (aip.path_length > 0)) if (!validate_path(4, &Point_segs[aip.hide_index], aip.path_length)) { Int3(); // This path is bogus! Who corrupted it! Danger! Danger! // Contact Mike, he caused this mess. aip.path_length=0; // This allows people to resume without harm... } } } } #endif } } #endif // ------------------------------------------------------------------------------------------------------- // Creates a path from the objects current segment (objp->segnum) to the specified segment for the object to // hide in Ai_local_info[objnum].goal_segment. // Sets objp->ctype.ai_info.hide_index, a pointer into Point_segs, the first point_seg of the path. // objp->ctype.ai_info.path_length, length of path // Point_segs_free_ptr global pointer into Point_segs array void create_path_to_segment(const vmobjptridx_t objp, const robot_info &robptr, const unsigned max_length, const create_path_safety_flag safety_flag, const icsegidx_t goal_segment) { auto &obj = *objp; ai_static *const aip = &obj.ctype.ai_info; ai_local *const ailp = &obj.ctype.ai_info.ail; ailp->time_player_seen = GameTime64; // Prevent from resetting path quickly. ailp->goal_segment = goal_segment; segnum_t start_seg; start_seg = obj.segnum; const auto end_seg = goal_segment; if (end_seg == segment_none) { ; } else { aip->path_length = create_path_points(objp, &robptr, start_seg, end_seg, Point_segs_free_ptr, max_length, create_path_random_flag::random, safety_flag, segment_none).second; #if defined(DXX_BUILD_DESCENT_II) aip->path_length = polish_path(objp, Point_segs_free_ptr, aip->path_length); #endif aip->hide_index = Point_segs_free_ptr - Point_segs; aip->cur_path_index = 0; #if defined(DXX_BUILD_DESCENT_I) #ifndef NDEBUG validate_path(6, Point_segs_free_ptr, aip->path_length); #endif #endif Point_segs_free_ptr += aip->path_length; if (Point_segs_free_ptr - Point_segs + MAX_PATH_LENGTH*2 > MAX_POINT_SEGS) { //Int3(); // Contact Mike: This is stupid. Should call maybe_ai_garbage_collect before the add. ai_reset_all_paths(); return; } // Assert(Point_segs_free_ptr - Point_segs + MAX_PATH_LENGTH*2 < MAX_POINT_SEGS); aip->PATH_DIR = 1; // Initialize to moving forward. #if defined(DXX_BUILD_DESCENT_I) aip->SUBMODE = AISM_GOHIDE; // This forces immediate movement. #endif ailp->mode = ai_mode::AIM_FOLLOW_PATH; ailp->player_awareness_type = player_awareness_type_t::PA_NONE; // If robot too aware of player, will set mode to chase } maybe_ai_path_garbage_collect(); } // Change, 10/07/95: Used to create path to ConsoleObject->pos. Now creates path to Believed_player_pos. void create_path_to_believed_player_segment(const vmobjptridx_t objp, const robot_info &robptr, const unsigned max_length, const create_path_safety_flag safety_flag) { #if defined(DXX_BUILD_DESCENT_I) const auto goal_segment = ConsoleObject->segnum; #elif defined(DXX_BUILD_DESCENT_II) const auto goal_segment = Believed_player_seg; #endif create_path_to_segment(objp, robptr, max_length, safety_flag, goal_segment); } #if defined(DXX_BUILD_DESCENT_II) void create_path_to_guidebot_player_segment(const vmobjptridx_t objp, const robot_info &robptr, const unsigned max_length, const create_path_safety_flag safety_flag) { auto &BuddyState = LevelUniqueObjectState.BuddyState; auto &Objects = LevelUniqueObjectState.Objects; auto &plr = get_player_controlling_guidebot(BuddyState, Players); if (plr.objnum == object_none) return; auto &plrobj = *Objects.vcptr(plr.objnum); const auto goal_segment = plrobj.segnum; create_path_to_segment(objp, robptr, max_length, safety_flag, goal_segment); } // ------------------------------------------------------------------------------------------------------- // Creates a path from the object's current segment (objp->segnum) to segment goalseg. void create_path_to_segment(const vmobjptridx_t objp, const robot_info &robptr, segnum_t goalseg, const unsigned max_length, const create_path_safety_flag safety_flag) { ai_static *aip = &objp->ctype.ai_info; ai_local *ailp = &objp->ctype.ai_info.ail; ailp->time_player_seen = GameTime64; // Prevent from resetting path quickly. ailp->goal_segment = goalseg; segnum_t start_seg, end_seg; start_seg = objp->segnum; end_seg = ailp->goal_segment; if (end_seg == segment_none) { ; } else { aip->path_length = create_path_points(objp, &robptr, start_seg, end_seg, Point_segs_free_ptr, max_length, create_path_random_flag::random, safety_flag, segment_none).second; aip->hide_index = Point_segs_free_ptr - Point_segs; aip->cur_path_index = 0; Point_segs_free_ptr += aip->path_length; if (Point_segs_free_ptr - Point_segs + MAX_PATH_LENGTH*2 > MAX_POINT_SEGS) { ai_reset_all_paths(); return; } aip->PATH_DIR = 1; // Initialize to moving forward. // -- UNUSED! aip->SUBMODE = AISM_GOHIDE; // This forces immediate movement. ailp->player_awareness_type = player_awareness_type_t::PA_NONE; // If robot too aware of player, will set mode to chase } maybe_ai_path_garbage_collect(); } #endif // ------------------------------------------------------------------------------------------------------- // Creates a path from the objects current segment (objp->segnum) to the specified segment for the object to // hide in Ai_local_info[objnum].goal_segment // Sets objp->ctype.ai_info.hide_index, a pointer into Point_segs, the first point_seg of the path. // objp->ctype.ai_info.path_length, length of path // Point_segs_free_ptr global pointer into Point_segs array void create_path_to_station(const vmobjptridx_t objp, const robot_info &robptr, int max_length) { auto &obj = *objp; ai_static *const aip = &obj.ctype.ai_info; ai_local *const ailp = &obj.ctype.ai_info.ail; if (max_length == -1) max_length = MAX_DEPTH_TO_SEARCH_FOR_PLAYER; ailp->time_player_seen = GameTime64; // Prevent from resetting path quickly. segnum_t start_seg, end_seg; start_seg = obj.segnum; end_seg = aip->hide_segment; if (end_seg == segment_none) { ; } else { aip->path_length = create_path_points(objp, &robptr, start_seg, end_seg, Point_segs_free_ptr, max_length, create_path_random_flag::random, create_path_safety_flag::safe, segment_none).second; #if defined(DXX_BUILD_DESCENT_II) aip->path_length = polish_path(objp, Point_segs_free_ptr, aip->path_length); #endif aip->hide_index = Point_segs_free_ptr - Point_segs; aip->cur_path_index = 0; #if defined(DXX_BUILD_DESCENT_I) #ifndef NDEBUG validate_path(7, Point_segs_free_ptr, aip->path_length); #endif #endif Point_segs_free_ptr += aip->path_length; if (Point_segs_free_ptr - Point_segs + MAX_PATH_LENGTH*2 > MAX_POINT_SEGS) { //Int3(); // Contact Mike: Stupid. ai_reset_all_paths(); return; } // Assert(Point_segs_free_ptr - Point_segs + MAX_PATH_LENGTH*2 < MAX_POINT_SEGS); aip->PATH_DIR = 1; // Initialize to moving forward. // aip->SUBMODE = AISM_GOHIDE; // This forces immediate movement. ailp->mode = ai_mode::AIM_FOLLOW_PATH; ailp->player_awareness_type = player_awareness_type_t::PA_NONE; } maybe_ai_path_garbage_collect(); } // ------------------------------------------------------------------------------------------------------- // Create a path of length path_length for an object, stuffing info in ai_info field. void create_n_segment_path(const vmobjptridx_t objp, const robot_info &robptr, unsigned path_length, const imsegidx_t avoid_seg) { auto &obj = *objp; ai_static *const aip = &obj.ctype.ai_info; ai_local *const ailp = &obj.ctype.ai_info.ail; const auto &&cr0 = create_path_points(objp, &robptr, obj.segnum, segment_exit, Point_segs_free_ptr, path_length, create_path_random_flag::random, create_path_safety_flag::unsafe, avoid_seg); aip->path_length = cr0.second; if (cr0.first == create_path_result::early) { Point_segs_free_ptr += aip->path_length; for (;;) { const auto &&crf = create_path_points(objp, &robptr, obj.segnum, segment_exit, Point_segs_free_ptr, --path_length, create_path_random_flag::random, create_path_safety_flag::unsafe, segment_none); aip->path_length = crf.second; if (crf.first != create_path_result::early) break; } assert(path_length); } aip->hide_index = Point_segs_free_ptr - Point_segs; aip->cur_path_index = 0; #if PATH_VALIDATION validate_path(8, Point_segs_free_ptr, aip->path_length); #endif Point_segs_free_ptr += aip->path_length; if (Point_segs_free_ptr - Point_segs + MAX_PATH_LENGTH*2 > MAX_POINT_SEGS) { //Int3(); // Contact Mike: This is curious, though not deadly. /eip++;g ai_reset_all_paths(); } aip->PATH_DIR = 1; // Initialize to moving forward. #if defined(DXX_BUILD_DESCENT_I) aip->SUBMODE = -1; // Don't know what this means. #endif ailp->mode = ai_mode::AIM_FOLLOW_PATH; #if defined(DXX_BUILD_DESCENT_II) // If this robot is visible (player_visibility is not available) and it's running away, move towards outside with // randomness to prevent a stream of bots from going away down the center of a corridor. if (player_is_visible(ailp->previous_visibility)) { if (aip->path_length) { int t_num_points = aip->path_length; move_towards_outside(LevelSharedSegmentState, &Point_segs[aip->hide_index], t_num_points, objp, create_path_random_flag::random); aip->path_length = t_num_points; } } #endif maybe_ai_path_garbage_collect(); } // ------------------------------------------------------------------------------------------------------- void create_n_segment_path_to_door(const vmobjptridx_t objp, const robot_info &robptr, const unsigned path_length) { create_n_segment_path(objp, robptr, path_length, segment_none); } #define Int3_if(cond) if (!cond) Int3(); // -- too much work -- // ---------------------------------------------------------------------------------------------------------- // -- too much work -- // Return true if the object the companion wants to kill is reachable. // -- too much work -- int attack_kill_object(object *objp) // -- too much work -- { // -- too much work -- object *kill_objp; // -- too much work -- fvi_info hit_data; // -- too much work -- int fate; // -- too much work -- fvi_query fq; // -- too much work -- // -- too much work -- if (Escort_kill_object == -1) // -- too much work -- return 0; // -- too much work -- // -- too much work -- kill_objp = &Objects[Escort_kill_object]; // -- too much work -- // -- too much work -- fq.p0 = &objp->pos; // -- too much work -- fq.startseg = objp->segnum; // -- too much work -- fq.p1 = &kill_objp->pos; // -- too much work -- fq.rad = objp->size; // -- too much work -- fq.thisobjnum = objp-Objects; // -- too much work -- fq.ignore_obj_list = NULL; // -- too much work -- fq.flags = 0; // -- too much work -- // -- too much work -- fate = find_vector_intersection(&fq,&hit_data); // -- too much work -- // -- too much work -- if (fate == HIT_NONE) // -- too much work -- return 1; // -- too much work -- else // -- too much work -- return 0; // -- too much work -- } // ------------------------------------------------------------------------------------------------------- // Creates a path from the objects current segment (objp->segnum) to the specified segment for the object to // hide in Ai_local_info[objnum].goal_segment. // Sets objp->ctype.ai_info.hide_index, a pointer into Point_segs, the first point_seg of the path. // objp->ctype.ai_info.path_length, length of path // Point_segs_free_ptr global pointer into Point_segs array #if defined(DXX_BUILD_DESCENT_I) namespace { static void create_path(const vmobjptridx_t objp, const robot_info &robptr) { auto &obj = *objp; ai_static *const aip = &obj.ctype.ai_info; const auto start_seg = obj.segnum; const auto end_seg = obj.ctype.ai_info.ail.goal_segment; if (end_seg == segment_none) create_n_segment_path(objp, robptr, 3, segment_none); if (end_seg == segment_none) { ; } else { aip->path_length = create_path_points(objp, &robptr, start_seg, end_seg, Point_segs_free_ptr, MAX_PATH_LENGTH, create_path_random_flag::nonrandom, create_path_safety_flag::unsafe, segment_none).second; aip->hide_index = Point_segs_free_ptr - Point_segs; aip->cur_path_index = 0; #ifndef NDEBUG validate_path(5, Point_segs_free_ptr, aip->path_length); #endif Point_segs_free_ptr += aip->path_length; if (Point_segs_free_ptr - Point_segs + MAX_PATH_LENGTH*2 > MAX_POINT_SEGS) { //Int3(); // Contact Mike: This is curious, though not deadly. /eip++;g ai_reset_all_paths(); } aip->PATH_DIR = 1; // Initialize to moving forward. aip->SUBMODE = AISM_HIDING; // Pretend we are hiding, so we sit here until bothered. } maybe_ai_path_garbage_collect(); } } #endif // ---------------------------------------------------------------------------------------------------------- // Optimization: If current velocity will take robot near goal, don't change velocity void ai_follow_path(const d_robot_info_array &Robot_info, const vmobjptridx_t objp, const player_visibility_state player_visibility, const vms_vector *const vec_to_player) { auto &obj = *objp; ai_static *const aip = &obj.ctype.ai_info; vms_vector goal_point, new_goal_point; #if defined(DXX_BUILD_DESCENT_II) auto &BuddyState = LevelUniqueObjectState.BuddyState; #endif auto &robptr = Robot_info[get_robot_id(obj)]; int forced_break, original_dir, original_index; ai_local *const ailp = &obj.ctype.ai_info.ail; if ((aip->hide_index == -1) || (aip->path_length == 0)) { if (ailp->mode == ai_mode::AIM_RUN_FROM_OBJECT) { create_n_segment_path(objp, robptr, 5, segment_none); //--Int3_if((aip->path_length != 0)); ailp->mode = ai_mode::AIM_RUN_FROM_OBJECT; } else { #if defined(DXX_BUILD_DESCENT_I) create_path(objp, robptr); #elif defined(DXX_BUILD_DESCENT_II) create_n_segment_path(objp, robptr, 5, segment_none); #endif //--Int3_if((aip->path_length != 0)); } } if ((aip->hide_index + aip->path_length > Point_segs_free_ptr - Point_segs) && (aip->path_length>0)) { #if defined(DXX_BUILD_DESCENT_II) Int3(); // Contact Mike: Bad. Path goes into what is believed to be free space. // This is debugging code. Figure out why garbage collection // didn't compress this object's path information. ai_path_garbage_collect(); #endif ai_reset_all_paths(); } if (aip->path_length < 2) { #if defined(DXX_BUILD_DESCENT_I) if (ailp->mode == ai_mode::AIM_RUN_FROM_OBJECT) #elif defined(DXX_BUILD_DESCENT_II) if ((aip->behavior == ai_behavior::AIB_SNIPE) || (ailp->mode == ai_mode::AIM_RUN_FROM_OBJECT)) #endif { create_n_segment_path(objp, robptr, AVOID_SEG_LENGTH, ConsoleObject->segnum == obj.segnum ? segment_none : ConsoleObject->segnum); // Can't avoid segment player is in, robot is already in it! (That's what the -1 is for) //--Int3_if((aip->path_length != 0)); #if defined(DXX_BUILD_DESCENT_II) if (aip->behavior == ai_behavior::AIB_SNIPE) { if (robot_is_thief(robptr)) ailp->mode = ai_mode::AIM_THIEF_ATTACK; // It gets bashed in create_n_segment_path else ailp->mode = ai_mode::AIM_SNIPE_FIRE; // It gets bashed in create_n_segment_path } else #endif { ailp->mode = ai_mode::AIM_RUN_FROM_OBJECT; // It gets bashed in create_n_segment_path } } #if defined(DXX_BUILD_DESCENT_I) else { ailp->mode = ai_mode::AIM_STILL; } return; #elif defined(DXX_BUILD_DESCENT_II) else if (robot_is_companion(robptr) == 0) { ailp->mode = ai_mode::AIM_STILL; aip->path_length = 0; return; } #endif } #if defined(DXX_BUILD_DESCENT_I) Assert((aip->PATH_DIR == -1) || (aip->PATH_DIR == 1)); if ((aip->SUBMODE == AISM_HIDING) && (aip->behavior == ai_behavior::AIB_HIDE)) return; #endif goal_point = Point_segs[aip->hide_index + aip->cur_path_index].point; auto dist_to_goal = vm_vec_dist_quick(goal_point, obj.pos); // If running from player, only run until can't be seen. if (ailp->mode == ai_mode::AIM_RUN_FROM_OBJECT) { if (player_visibility == player_visibility_state::no_line_of_sight && ailp->player_awareness_type == player_awareness_type_t::PA_NONE) { fix vel_scale; vel_scale = F1_0 - FrameTime/2; if (vel_scale < F1_0/2) vel_scale = F1_0/2; vm_vec_scale(obj.mtype.phys_info.velocity, vel_scale); return; } else #if defined(DXX_BUILD_DESCENT_II) if (!(d_tick_count ^ ((objp) & 0x07))) #endif { // Done 1/8 ticks. // If player on path (beyond point robot is now at), then create a new path. point_seg *curpsp = &Point_segs[aip->hide_index]; auto player_segnum = ConsoleObject->segnum; int i; // This is probably being done every frame, which is wasteful. for (i=aip->cur_path_index; ipath_length; i++) { if (curpsp[i].segnum == player_segnum) { create_n_segment_path(objp, robptr, AVOID_SEG_LENGTH, player_segnum != obj.segnum ? player_segnum : segment_none); #if defined(DXX_BUILD_DESCENT_I) Assert(aip->path_length != 0); #endif ailp->mode = ai_mode::AIM_RUN_FROM_OBJECT; // It gets bashed in create_n_segment_path break; } } if (player_is_visible(player_visibility)) { ailp->player_awareness_type = player_awareness_type_t::PA_NEARBY_ROBOT_FIRED; ailp->player_awareness_time = F1_0; } } } if (aip->cur_path_index < 0) { aip->cur_path_index = 0; } else if (aip->cur_path_index >= aip->path_length) { if (ailp->mode == ai_mode::AIM_RUN_FROM_OBJECT) { create_n_segment_path(objp, robptr, AVOID_SEG_LENGTH, ConsoleObject->segnum); ailp->mode = ai_mode::AIM_RUN_FROM_OBJECT; // It gets bashed in create_n_segment_path #if defined(DXX_BUILD_DESCENT_II) Assert(aip->path_length != 0); #endif } else { aip->cur_path_index = aip->path_length-1; } } goal_point = Point_segs[aip->hide_index + aip->cur_path_index].point; // If near goal, pick another goal point. forced_break = 0; // Gets set for short paths. original_dir = aip->PATH_DIR; original_index = aip->cur_path_index; const vm_distance threshold_distance{fixmul(vm_vec_mag_quick(obj.mtype.phys_info.velocity), FrameTime)*2 + F1_0*2}; #if defined(DXX_BUILD_DESCENT_II) new_goal_point = Point_segs[aip->hide_index + aip->cur_path_index].point; //--Int3_if(((aip->cur_path_index >= 0) && (aip->cur_path_index < aip->path_length))); #endif while ((dist_to_goal < threshold_distance) && !forced_break) { // Advance to next point on path. aip->cur_path_index += aip->PATH_DIR; // See if next point wraps past end of path (in either direction), and if so, deal with it based on mode. if ((aip->cur_path_index >= aip->path_length) || (aip->cur_path_index < 0)) { #if defined(DXX_BUILD_DESCENT_II) // Buddy bot. If he's in mode to get away from player and at end of line, // if player visible, then make a new path, else just return. if (robot_is_companion(robptr)) { if (BuddyState.Escort_special_goal == ESCORT_GOAL_SCRAM) { if (player_is_visible(player_visibility)) { create_n_segment_path(objp, robptr, 16 + d_rand() * 16, segment_none); aip->path_length = polish_path(objp, &Point_segs[aip->hide_index], aip->path_length); Assert(aip->path_length != 0); ailp->mode = ai_mode::AIM_WANDER; // Special buddy mode. //--Int3_if(((aip->cur_path_index >= 0) && (aip->cur_path_index < aip->path_length))); return; } else { ailp->mode = ai_mode::AIM_WANDER; // Special buddy mode. obj.mtype.phys_info.velocity = {}; obj.mtype.phys_info.rotvel = {}; //!!Assert((aip->cur_path_index >= 0) && (aip->cur_path_index < aip->path_length)); return; } } } #endif #if defined(DXX_BUILD_DESCENT_I) if (ailp->mode == ai_mode::AIM_HIDE) { ailp->mode = ai_mode::AIM_STILL; return; // Stay here until bonked or hit by player. } #elif defined(DXX_BUILD_DESCENT_II) if (aip->behavior == ai_behavior::AIB_FOLLOW) { create_n_segment_path(objp, robptr, 10, ConsoleObject->segnum); //--Int3_if(((aip->cur_path_index >= 0) && (aip->cur_path_index < aip->path_length))); } #endif else if (aip->behavior == ai_behavior::AIB_STATION) { create_path_to_station(objp, robptr, 15); if ((aip->hide_segment != Point_segs[aip->hide_index+aip->path_length-1].segnum) #if defined(DXX_BUILD_DESCENT_II) || (aip->path_length == 0) #endif ) { ailp->mode = ai_mode::AIM_STILL; } return; } else if (ailp->mode == ai_mode::AIM_FOLLOW_PATH #if defined(DXX_BUILD_DESCENT_I) && (aip->behavior != ai_behavior::AIB_FOLLOW_PATH) #endif ) { create_path_to_believed_player_segment(objp, robptr, 10, create_path_safety_flag::safe); #if defined(DXX_BUILD_DESCENT_II) if (aip->hide_segment != Point_segs[aip->hide_index+aip->path_length-1].segnum) { ailp->mode = ai_mode::AIM_STILL; return; } #endif } else if (ailp->mode == ai_mode::AIM_RUN_FROM_OBJECT) { create_n_segment_path(objp, robptr, AVOID_SEG_LENGTH, ConsoleObject->segnum); ailp->mode = ai_mode::AIM_RUN_FROM_OBJECT; // It gets bashed in create_n_segment_path #if defined(DXX_BUILD_DESCENT_II) if (aip->path_length < 1) { create_n_segment_path(objp, robptr, AVOID_SEG_LENGTH, ConsoleObject->segnum); ailp->mode = ai_mode::AIM_RUN_FROM_OBJECT; // It gets bashed in create_n_segment_path if (aip->path_length < 1) { aip->behavior = ai_behavior::AIB_NORMAL; ailp->mode = ai_mode::AIM_STILL; return; } } //--Int3_if(((aip->cur_path_index >= 0) && (aip->cur_path_index < aip->path_length))); #endif } else { // Reached end of the line. First see if opposite end point is reachable, and if so, go there. // If not, turn around. int opposite_end_index; fvi_info hit_data; // See which end we're nearer and look at the opposite end point. if (abs(aip->cur_path_index - aip->path_length) < aip->cur_path_index) { // Nearer to far end (ie, index not 0), so try to reach 0. opposite_end_index = 0; } else { // Nearer to 0 end, so try to reach far end. opposite_end_index = aip->path_length-1; } const auto fate = find_vector_intersection(fvi_query{ obj.pos, Point_segs[aip->hide_index + opposite_end_index].point, fvi_query::unused_ignore_obj_list, fvi_query::unused_LevelUniqueObjectState, fvi_query::unused_Robot_info, 0, //what about trans walls??? objp, }, obj.segnum, obj.size, hit_data); if (fate != fvi_hit_type::Wall) { // We can be circular! Do it! // Path direction is unchanged. aip->cur_path_index = opposite_end_index; } else { aip->PATH_DIR = -aip->PATH_DIR; #if defined(DXX_BUILD_DESCENT_II) aip->cur_path_index += aip->PATH_DIR; #endif } } break; } else { new_goal_point = Point_segs[aip->hide_index + aip->cur_path_index].point; goal_point = new_goal_point; dist_to_goal = vm_vec_dist_quick(goal_point, obj.pos); } // If went all the way around to original point, in same direction, then get out of here! if ((aip->cur_path_index == original_index) && (aip->PATH_DIR == original_dir)) { create_path_to_believed_player_segment(objp, robptr, 3, create_path_safety_flag::safe); forced_break = 1; } } // end while // Set velocity (objp->mtype.phys_info.velocity) and orientation (objp->orient) for this object. ai_path_set_orient_and_vel(Robot_info, objp, goal_point #if defined(DXX_BUILD_DESCENT_II) , player_visibility, vec_to_player #endif ); } } namespace { int Last_tick_garbage_collected; struct obj_path { short path_start; objnum_t objnum; }; static int path_index_compare(const void *const v1, const void *const v2) { const auto i1 = reinterpret_cast(v1); const auto i2 = reinterpret_cast(v2); if (i1->path_start < i2->path_start) return -1; else if (i1->path_start == i2->path_start) return 0; else return 1; } } namespace dsx { namespace { // ---------------------------------------------------------------------------------------------------------- // Set orientation matrix and velocity for objp based on its desire to get to a point. void ai_path_set_orient_and_vel(const d_robot_info_array &Robot_info, object &objp, const vms_vector &goal_point #if defined(DXX_BUILD_DESCENT_II) , const player_visibility_state player_visibility, const vms_vector *const vec_to_player #endif ) { vms_vector cur_vel = objp.mtype.phys_info.velocity; vms_vector cur_pos = objp.pos; fix speed_scale; fix dot; auto &robptr = Robot_info[get_robot_id(objp)]; fix max_speed; // If evading player, use highest difficulty level speed, plus something based on diff level const auto Difficulty_level = GameUniqueState.Difficulty_level; max_speed = robptr.max_speed[Difficulty_level]; ai_local *ailp = &objp.ctype.ai_info.ail; if (ailp->mode == ai_mode::AIM_RUN_FROM_OBJECT #if defined(DXX_BUILD_DESCENT_II) || objp.ctype.ai_info.behavior == ai_behavior::AIB_SNIPE #endif ) max_speed = max_speed*3/2; auto norm_vec_to_goal = vm_vec_normalized_quick(vm_vec_sub(goal_point, cur_pos)); auto norm_cur_vel = vm_vec_normalized_quick(cur_vel); const auto norm_fvec = vm_vec_normalized_quick(objp.orient.fvec); dot = vm_vec_dot(norm_vec_to_goal, norm_fvec); // If very close to facing opposite desired vector, perturb vector if (dot < -15*F1_0/16) { norm_cur_vel = norm_vec_to_goal; } else { norm_cur_vel.x += norm_vec_to_goal.x/2/(static_cast(DESIGNATED_GAME_FRAMETIME)/FrameTime); norm_cur_vel.y += norm_vec_to_goal.y/2/(static_cast(DESIGNATED_GAME_FRAMETIME)/FrameTime); norm_cur_vel.z += norm_vec_to_goal.z/2/(static_cast(DESIGNATED_GAME_FRAMETIME)/FrameTime); } vm_vec_normalize_quick(norm_cur_vel); // Set speed based on this robot type's maximum allowed speed and how hard it is turning. // How hard it is turning is based on the dot product of (vector to goal) and (current velocity vector) // Note that since 3*F1_0/4 is added to dot product, it is possible for the robot to back up. // Set speed and orientation. if (dot < 0) dot /= -4; #if defined(DXX_BUILD_DESCENT_II) // If in snipe mode, can move fast even if not facing that direction. if (objp.ctype.ai_info.behavior == ai_behavior::AIB_SNIPE) if (dot < F1_0/2) dot = (dot + F1_0)/2; #endif speed_scale = fixmul(max_speed, dot); vm_vec_scale(norm_cur_vel, speed_scale); objp.mtype.phys_info.velocity = norm_cur_vel; fix rate; if (ailp->mode == ai_mode::AIM_RUN_FROM_OBJECT #if defined(DXX_BUILD_DESCENT_II) || robot_is_companion(robptr) == 1 || objp.ctype.ai_info.behavior == ai_behavior::AIB_SNIPE #endif ) { #if defined(DXX_BUILD_DESCENT_II) if (ailp->mode == ai_mode::AIM_SNIPE_RETREAT_BACKWARDS) { if (player_is_visible(player_visibility) && vec_to_player) norm_vec_to_goal = *vec_to_player; else vm_vec_negate(norm_vec_to_goal); } #endif rate = robptr.turn_time[Difficulty_level_type::_4] / 2; } else rate = robptr.turn_time[Difficulty_level]; ai_turn_towards_vector(norm_vec_to_goal, objp, rate); } // ---------------------------------------------------------------------------------------------------------- // Garbage colledion -- Free all unused records in Point_segs and compress all paths. void ai_path_garbage_collect() { int free_path_index = 0; int num_path_objects = 0; int objind; obj_path object_list[MAX_OBJECTS]; auto &Objects = LevelUniqueObjectState.Objects; auto &vcobjptridx = Objects.vcptridx; auto &vmobjptridx = Objects.vmptridx; Last_tick_garbage_collected = d_tick_count; #if PATH_VALIDATION validate_all_paths(); #endif // Create a list of objects which have paths of length 1 or more. range_for (const auto &&objp, vcobjptridx) { if (objp->type == OBJ_ROBOT && (objp->control_source == object::control_type::ai #if defined(DXX_BUILD_DESCENT_II) || objp->control_source == object::control_type::morph #endif )) { const auto &aip = objp->ctype.ai_info; if (aip.path_length) { object_list[num_path_objects].path_start = aip.hide_index; object_list[num_path_objects++].objnum = objp; } } } qsort(object_list, num_path_objects, sizeof(object_list[0]), path_index_compare); for (objind=0; objind < num_path_objects; objind++) { ai_static *aip; int i; int old_index; auto objnum = object_list[objind].objnum; object &objp = vmobjptridx(objnum); aip = &objp.ctype.ai_info; old_index = aip->hide_index; aip->hide_index = free_path_index; for (i=0; ipath_length; i++) Point_segs[free_path_index++] = Point_segs[old_index++]; } Point_segs_free_ptr = Point_segs.begin() + free_path_index; #ifndef NDEBUG { auto &vcobjptr = Objects.vcptr; range_for (const auto &&objp, vcobjptr) { auto &obj = *objp; const auto &aip = obj.ctype.ai_info; if (obj.type == OBJ_ROBOT && obj.control_source == object::control_type::ai) if ((aip.hide_index + aip.path_length > Point_segs_free_ptr - Point_segs) && (aip.path_length>0)) Int3(); // Contact Mike: Debug trap for nasty, elusive bug. } validate_all_paths(); } #endif } // ----------------------------------------------------------------------------- // Do garbage collection if not been done for awhile, or things getting really critical. void maybe_ai_path_garbage_collect(void) { if (Point_segs_free_ptr - Point_segs > MAX_POINT_SEGS - MAX_PATH_LENGTH) { if (Last_tick_garbage_collected+1 >= d_tick_count) { // This is kind of bad. Garbage collected last frame or this frame. // Just destroy all paths. Too bad for the robots. They are memory wasteful. ai_reset_all_paths(); } else { // We are really close to full, but didn't just garbage collect, so maybe this is recoverable. ai_path_garbage_collect(); } } else if (Point_segs_free_ptr - Point_segs > 3*MAX_POINT_SEGS/4) { if (Last_tick_garbage_collected + 16 < d_tick_count) { ai_path_garbage_collect(); } } else if (Point_segs_free_ptr - Point_segs > MAX_POINT_SEGS/2) { if (Last_tick_garbage_collected + 256 < d_tick_count) { ai_path_garbage_collect(); } } } } // ----------------------------------------------------------------------------- // Reset all paths. Do garbage collection. // Should be called at the start of each level. void ai_reset_all_paths(void) { auto &Objects = LevelUniqueObjectState.Objects; auto &vmobjptr = Objects.vmptr; range_for (const auto &&objp, vmobjptr) { auto &obj = *objp; if (obj.type == OBJ_ROBOT && obj.control_source == object::control_type::ai) { obj.ctype.ai_info.hide_index = -1; obj.ctype.ai_info.path_length = 0; } } ai_path_garbage_collect(); } // --------------------------------------------------------------------------------------------------------- // Probably called because a robot bashed a wall, getting a bunch of retries. // Try to resume path. void attempt_to_resume_path(const d_robot_info_array &Robot_info, const vmobjptridx_t objp) { ai_static *aip = &objp->ctype.ai_info; int new_path_index; auto &robptr = Robot_info[get_robot_id(objp)]; if (aip->behavior == ai_behavior::AIB_STATION #if defined(DXX_BUILD_DESCENT_II) && robptr.companion != 1 #endif ) if (d_rand() > 8192) { ai_local *ailp = &objp->ctype.ai_info.ail; aip->hide_segment = objp->segnum; ailp->mode = ai_mode::AIM_STILL; } new_path_index = aip->cur_path_index - aip->PATH_DIR; if ((new_path_index >= 0) && (new_path_index < aip->path_length)) { aip->cur_path_index = new_path_index; } else { // At end of line and have nowhere to go. move_towards_segment_center(Robot_info, LevelSharedSegmentState, objp); create_path_to_station(objp, robptr, 15); } } // ---------------------------------------------------------------------------------------------------------- // DEBUG FUNCTIONS FOLLOW // ---------------------------------------------------------------------------------------------------------- #if DXX_USE_EDITOR namespace { __attribute_used static void test_create_path_many(fvmobjptridx &vmobjptridx, fimsegptridx &imsegptridx) { std::array point_segs; int i; const unsigned Test_size = 1000; for (i=0; i((d_rand() * (Highest_segment_index + 1)) / D_RAND_MAX)); Markedsegp = imsegptridx(static_cast((d_rand() * (Highest_segment_index + 1)) / D_RAND_MAX)); create_path_points(vmobjptridx(object_first), create_path_unused_robot_info, Cursegp, Markedsegp, point_segs.begin(), MAX_PATH_LENGTH, create_path_random_flag::nonrandom, create_path_safety_flag::unsafe, segment_none); } } __attribute_used static void test_create_path(fvmobjptridx &vmobjptridx) { std::array point_segs; create_path_points(vmobjptridx(object_first), create_path_unused_robot_info, Cursegp, Markedsegp, point_segs.begin(), MAX_PATH_LENGTH, create_path_random_flag::nonrandom, create_path_safety_flag::unsafe, segment_none); } // For all segments in mine, create paths to all segments in mine, print results. __attribute_used static void test_create_all_paths(fvmobjptridx &vmobjptridx, fvcsegptridx &vcsegptridx) { Point_segs_free_ptr = Point_segs.begin(); range_for (const auto &&segp0, vcsegptridx) { const shared_segment &sseg0 = segp0; if (sseg0.segnum != segment_none) { for (const auto &&segp1 : partial_range(vcsegptridx, segp0.get_unchecked_index(), vcsegptridx.count())) { const shared_segment &sseg1 = segp1; if (sseg1.segnum != segment_none) { create_path_points(vmobjptridx(object_first), create_path_unused_robot_info, segp0, segp1, Point_segs_free_ptr, MAX_PATH_LENGTH, create_path_random_flag::nonrandom, create_path_safety_flag::unsafe, segment_none); } } } } } short Player_path_length=0; int Player_hide_index=-1; int Player_cur_path_index=0; int Player_following_path_flag=0; // ------------------------------------------------------------------------------------------------------------------ // Set orientation matrix and velocity for objp based on its desire to get to a point. static void player_path_set_orient_and_vel(object &objp, const vms_vector &goal_point) { const auto &cur_vel = objp.mtype.phys_info.velocity; const auto &cur_pos = objp.pos; fix speed_scale; fix dot; const fix max_speed = F1_0*50; const auto norm_vec_to_goal = vm_vec_normalized_quick(vm_vec_sub(goal_point, cur_pos)); auto norm_cur_vel = vm_vec_normalized_quick(cur_vel); const auto &&norm_fvec = vm_vec_normalized_quick(objp.orient.fvec); dot = vm_vec_dot(norm_vec_to_goal, norm_fvec); // If very close to facing opposite desired vector, perturb vector if (dot < -15*F1_0/16) { norm_cur_vel = norm_vec_to_goal; } else { norm_cur_vel.x += norm_vec_to_goal.x/2/(static_cast(DESIGNATED_GAME_FRAMETIME)/FrameTime); norm_cur_vel.y += norm_vec_to_goal.y/2/(static_cast(DESIGNATED_GAME_FRAMETIME)/FrameTime); norm_cur_vel.z += norm_vec_to_goal.z/2/(static_cast(DESIGNATED_GAME_FRAMETIME)/FrameTime); } vm_vec_normalize_quick(norm_cur_vel); // Set speed based on this robot type's maximum allowed speed and how hard it is turning. // How hard it is turning is based on the dot product of (vector to goal) and (current velocity vector) // Note that since 3*F1_0/4 is added to dot product, it is possible for the robot to back up. // Set speed and orientation. if (dot < 0) dot /= 4; speed_scale = fixmul(max_speed, dot); vm_vec_scale(norm_cur_vel, speed_scale); objp.mtype.phys_info.velocity = norm_cur_vel; physics_turn_towards_vector(norm_vec_to_goal, objp, F1_0); } } // ---------------------------------------------------------------------------------------------------------- // Optimization: If current velocity will take robot near goal, don't change velocity void player_follow_path(object &objp) { vms_vector goal_point; int count, forced_break, original_index; int goal_seg; if (!Player_following_path_flag) return; if (Player_hide_index == -1) return; if (Player_path_length < 2) return; goal_point = Point_segs[Player_hide_index + Player_cur_path_index].point; goal_seg = Point_segs[Player_hide_index + Player_cur_path_index].segnum; Assert((goal_seg >= 0) && (goal_seg <= Highest_segment_index)); (void)goal_seg; auto dist_to_goal = vm_vec_dist_quick(goal_point, objp.pos); if (Player_cur_path_index < 0) Player_cur_path_index = 0; else if (Player_cur_path_index >= Player_path_length) Player_cur_path_index = Player_path_length-1; goal_point = Point_segs[Player_hide_index + Player_cur_path_index].point; count=0; // If near goal, pick another goal point. forced_break = 0; // Gets set for short paths. //original_dir = 1; original_index = Player_cur_path_index; const vm_distance threshold_distance{fixmul(vm_vec_mag_quick(objp.mtype.phys_info.velocity), FrameTime)*2 + F1_0*2}; while ((dist_to_goal < threshold_distance) && !forced_break) { // ----- Debug stuff ----- if (count++ > 20) { break; } // Advance to next point on path. Player_cur_path_index += 1; // See if next point wraps past end of path (in either direction), and if so, deal with it based on mode. if ((Player_cur_path_index >= Player_path_length) || (Player_cur_path_index < 0)) { Player_following_path_flag = 0; forced_break = 1; } // If went all the way around to original point, in same direction, then get out of here! if (Player_cur_path_index == original_index) { Player_following_path_flag = 0; forced_break = 1; } goal_point = Point_segs[Player_hide_index + Player_cur_path_index].point; dist_to_goal = vm_vec_dist_quick(goal_point, objp.pos); } // end while // Set velocity (objp->mtype.phys_info.velocity) and orientation (objp->orient) for this object. player_path_set_orient_and_vel(objp, goal_point); } namespace { // ------------------------------------------------------------------------------------------------------------------ // Create path for player from current segment to goal segment. static void create_player_path_to_segment(fvmobjptridx &vmobjptridx, segnum_t segnum) { const auto objp = vmobjptridx(ConsoleObject); Player_hide_index=-1; Player_cur_path_index=0; Player_following_path_flag=0; auto &&cr = create_path_points(objp, create_path_unused_robot_info, objp->segnum, segnum, Point_segs_free_ptr, 100, create_path_random_flag::nonrandom, create_path_safety_flag::unsafe, segment_none); Player_path_length = cr.second; if (cr.first == create_path_result::early) con_printf(CON_DEBUG,"Unable to form path of length %i for myself", 100); Player_following_path_flag = 1; Player_hide_index = Point_segs_free_ptr - Point_segs; Player_cur_path_index = 0; Point_segs_free_ptr += Player_path_length; if (Point_segs_free_ptr - Point_segs + MAX_PATH_LENGTH*2 > MAX_POINT_SEGS) { //Int3(); // Contact Mike: This is curious, though not deadly. /eip++;g ai_reset_all_paths(); } } } segnum_t Player_goal_segment = segment_none; void check_create_player_path(void) { auto &Objects = LevelUniqueObjectState.Objects; auto &vmobjptridx = Objects.vmptridx; if (Player_goal_segment != segment_none) create_player_path_to_segment(vmobjptridx, Player_goal_segment); Player_goal_segment = segment_none; } #endif } // ---------------------------------------------------------------------------------------------------------- // DEBUG FUNCTIONS ENDED // ----------------------------------------------------------------------------------------------------------