/*
 * 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 <numeric>
#include <random>
#include <stdio.h>		//	for printf()
#include <stdlib.h>		// for d_rand() and qsort()
#include <string.h>		// 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"

namespace dcx {

namespace {

//	Length in segments of avoidance path
constexpr std::size_t 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 DXX_BUILD_DESCENT == 2
								, 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 center_point{compute_center_point_on_side(vcvertptr, seg1, connect_side)};
		auto new_point{vm_vec_build_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 DXX_BUILD_DESCENT == 2
		const auto &&segp = segments.imptridx(psegs[2*i].segnum);
		const auto &&temp_segnum{find_point_seg(LevelSharedSegmentState, psegs[2*i-1].point, segp DXX_lighting_hack_pass_parameter)};
		if (temp_segnum == segment_none) {
			psegs[2*i-1].point = center_point;
			find_point_seg(LevelSharedSegmentState, psegs[2*i-1].point, segp DXX_lighting_hack_pass_parameter);
		}
#endif

		psegs[2*i-1].segnum = psegs[2*i].segnum;
		count++;
	}

#if DXX_BUILD_DESCENT == 2
	//	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)
	{
		const auto temp1{vm_vec_build_sub(psegs[i].point, psegs[i - 1].point)};
		const auto temp2{vm_vec_build_sub(psegs[i + 1].point, psegs[i].point)};
		const auto dot{vm_vec_build_dot(temp1, temp2)};
		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 DXX_BUILD_DESCENT == 2
//	-----------------------------------------------------------------------------------------------------------
//	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<point_seg, 200> 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) DXX_lighting_hack_pass_parameter)};
		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_build_sub(psegs[i].point, psegs[i-1].point));
		const auto b = vm_vec_normalized_quick(vm_vec_build_sub(psegs[i+1].point, psegs[i].point));
		if (abs(vm_vec_build_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)};
			e = vm_vec_cross(vm_vec_build_sub(psegs[i + 1].point, psegs[i - 1].point), 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) DXX_lighting_hack_pass_parameter)};
		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_result, unsigned> 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 DXX_BUILD_DESCENT == 2
	auto &Objects = LevelUniqueObjectState.Objects;
	auto &vmobjptr = Objects.vmptr;
#endif
	segnum_t		cur_seg;
	int qtail{0}, qhead = 0;
	int		i;
	std::array<seg_seg, MAX_SEGMENTS> 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 == object_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<uint16_t, MAX_SEGMENTS> 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<sidenum_t> side_traversal_translation;
	std::iota(side_traversal_translation.begin(), side_traversal_translation.end(), sidenum_t{});
#if DXX_BUILD_DESCENT == 1
	/* 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 DXX_BUILD_DESCENT == 2
	/* 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 DXX_BUILD_DESCENT == 2
		/* 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 DXX_BUILD_DESCENT == 1
#define AI_DOOR_OPENABLE_PLAYER_FLAGS
#elif DXX_BUILD_DESCENT == 2
#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 DXX_BUILD_DESCENT == 2
				Assert(this_seg != segment_none);
				if (((cur_seg == avoid_seg) || (this_seg == avoid_seg)) && (ConsoleObject->segnum == avoid_seg)) {
					const auto center_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 DXX_BUILD_DESCENT == 2
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 DXX_BUILD_DESCENT == 1
#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;
		psegs->point = compute_segment_center(vcvertptr, vcsegptr(this_seg));
		psegs++;
		l_num_points++;
#if DXX_BUILD_DESCENT == 1
#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;
	psegs->point = compute_segment_center(vcvertptr, 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 DXX_BUILD_DESCENT == 2
// -- 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 == object_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 DXX_BUILD_DESCENT == 2
//	-------------------------------------------------------------------------------------------------------
//	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; i<num_points-3; i++)
	for (i=0; i<2; i++) {
		fvi_info		hit_data;
		const auto hit_type = find_vector_intersection(fvi_query{
			obj.pos,
			psegs[i].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)
			first_point = i+1;
		else
			break;		
	}

	if (first_point) {
		//	Scrunch down all the psegs.
		for (i=first_point; i<num_points; i++)
			psegs[i-first_point] = psegs[i];
	}

	return num_points - first_point;
}
#endif

#ifndef NDEBUG
namespace {
//	-------------------------------------------------------------------------------------------------------
//	Make sure that there are connections between all segments on path.
//	Note that if path has been optimized, connections may not be direct, so this function is useless, or worse.
//	Return true if valid, else return false.
int validate_path(int, point_seg *psegs, uint_fast32_t num_points)
{
#if PATH_VALIDATION
	auto curseg = psegs->segnum;
	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;
	for (auto &obj : vmobjptr)
	{
		if (obj.type == object_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 DXX_BUILD_DESCENT == 2
		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 DXX_BUILD_DESCENT == 1
#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 DXX_BUILD_DESCENT == 1
		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 DXX_BUILD_DESCENT == 1
	const auto goal_segment = ConsoleObject->segnum;
#elif DXX_BUILD_DESCENT == 2
	const auto goal_segment{Believed_player_seg};
#endif
	create_path_to_segment(objp, robptr, max_length, safety_flag, goal_segment);
}

#if DXX_BUILD_DESCENT == 2
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 DXX_BUILD_DESCENT == 2
		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 DXX_BUILD_DESCENT == 1
#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 DXX_BUILD_DESCENT == 1
	aip->SUBMODE = -1;		//	Don't know what this means.
#endif
	ailp->mode = ai_mode::AIM_FOLLOW_PATH;

#if DXX_BUILD_DESCENT == 2
	//	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 DXX_BUILD_DESCENT == 1
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 DXX_BUILD_DESCENT == 2
	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 DXX_BUILD_DESCENT == 1
			create_path(objp, robptr);
#elif DXX_BUILD_DESCENT == 2
			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 DXX_BUILD_DESCENT == 2
		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 DXX_BUILD_DESCENT == 1
		if (ailp->mode == ai_mode::AIM_RUN_FROM_OBJECT)
#elif DXX_BUILD_DESCENT == 2
		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 DXX_BUILD_DESCENT == 2
			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 DXX_BUILD_DESCENT == 1
		else {
			ailp->mode = ai_mode::AIM_STILL;
		}
		return;
#elif DXX_BUILD_DESCENT == 2
		else if (robot_is_companion(robptr) == 0) {
			ailp->mode = ai_mode::AIM_STILL;
			aip->path_length = 0;
			return;
		}
#endif
	}

#if DXX_BUILD_DESCENT == 1
	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 DXX_BUILD_DESCENT == 2
			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; i<aip->path_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 DXX_BUILD_DESCENT == 1
					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 DXX_BUILD_DESCENT == 2
			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 DXX_BUILD_DESCENT == 2
	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 DXX_BUILD_DESCENT == 2
			//	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);
						//--Int3_if(((aip->cur_path_index >= 0) && (aip->cur_path_index < aip->path_length)));
					} else {
						obj.mtype.phys_info.velocity = {};
						obj.mtype.phys_info.rotvel = {};
						//!!Assert((aip->cur_path_index >= 0) && (aip->cur_path_index < aip->path_length));
					}
					ailp->mode = ai_mode::AIM_WANDER;	//	Special buddy mode.
					return;
				}
			}
#endif

#if DXX_BUILD_DESCENT == 1
			if (ailp->mode == ai_mode::AIM_HIDE) {
				ailp->mode = ai_mode::AIM_STILL;
				return;		// Stay here until bonked or hit by player.
			}
#elif DXX_BUILD_DESCENT == 2
			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 DXX_BUILD_DESCENT == 2
					|| (aip->path_length == 0)
#endif
					) {
					ailp->mode = ai_mode::AIM_STILL;
				}
				return;
			} else if (ailp->mode == ai_mode::AIM_FOLLOW_PATH
#if DXX_BUILD_DESCENT == 1
					   && (aip->behavior != ai_behavior::AIB_FOLLOW_PATH)
#endif
					   ) {
				create_path_to_believed_player_segment(objp, robptr, 10, create_path_safety_flag::safe);
#if DXX_BUILD_DESCENT == 2
				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 DXX_BUILD_DESCENT == 2
				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 DXX_BUILD_DESCENT == 2
					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 DXX_BUILD_DESCENT == 2
							   , player_visibility, vec_to_player
#endif
							   );
}

}

namespace {

int	Last_tick_garbage_collected;

struct obj_path {
	short	path_start;
	objnum_t objnum;
};

}

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 DXX_BUILD_DESCENT == 2
								, 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 DXX_BUILD_DESCENT == 2
		|| 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_build_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_build_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<float>(DESIGNATED_GAME_FRAMETIME)/FrameTime);
		norm_cur_vel.y += norm_vec_to_goal.y/2/(static_cast<float>(DESIGNATED_GAME_FRAMETIME)/FrameTime);
		norm_cur_vel.z += norm_vec_to_goal.z/2/(static_cast<float>(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 DXX_BUILD_DESCENT == 2
	//	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 DXX_BUILD_DESCENT == 2
		|| robot_is_companion(robptr) == 1 || objp.ctype.ai_info.behavior == ai_behavior::AIB_SNIPE
#endif
		) {
#if DXX_BUILD_DESCENT == 2
		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;
	std::array<obj_path, MAX_OBJECTS>		object_list;

	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 == object_type::OBJ_ROBOT && (objp->control_source == object::control_type::ai
#if DXX_BUILD_DESCENT == 2
			|| objp->control_source == object::control_type::morph
#endif
										  )) {
			const auto &aip = objp->ctype.ai_info;
			if (aip.path_length) {
				auto &ol{object_list[num_path_objects]};
				++ num_path_objects;
				ol.path_start = aip.hide_index;
				ol.objnum = objp;
			}
		}
	}
	std::ranges::sort(std::span(object_list).first(num_path_objects), {}, &obj_path::path_start);

	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; i<aip->path_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;
	for (auto &obj : vcobjptr)
	{
		const auto &aip = obj.ctype.ai_info;

		if (obj.type == object_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;
	for (auto &obj : vmobjptr)
	{
		if (obj.type == object_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 DXX_BUILD_DESCENT == 2
		&& 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(robptr, LevelSharedSegmentState, objp);
		create_path_to_station(objp, robptr, 15);
	}
}

//	----------------------------------------------------------------------------------------------------------
//					DEBUG FUNCTIONS FOLLOW
//	----------------------------------------------------------------------------------------------------------

#if DXX_USE_EDITOR
namespace {

dxx_compiler_attribute_used
static void test_create_path_many(fvmobjptridx &vmobjptridx, const d_level_shared_segment_state &LevelSharedSegmentState, const unsigned long seed)
{
	auto &SSegments = LevelSharedSegmentState.get_segments();
	std::minstd_rand mrd{seed};
	std::uniform_int_distribution<std::underlying_type_t<segnum_t>> uid{0u, SSegments.get_count()};
	for (const auto i : constant_xrange<std::size_t{0}, 1000u>{})
	{
		(void)i;
		const vcsegidx_t start_segment{segnum_t{uid(mrd)}};
		const vcsegidx_t end_segment{segnum_t{uid(mrd)}};
		std::array<point_seg, 200> point_segs;
		create_path_points(vmobjptridx(object_first), create_path_unused_robot_info, start_segment, end_segment, point_segs.begin(), MAX_PATH_LENGTH, create_path_random_flag::nonrandom, create_path_safety_flag::unsafe, segment_none);
	}
}

dxx_compiler_attribute_used
static void test_create_path(fvmobjptridx &vmobjptridx)
{
	std::array<point_seg, 200> 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.
dxx_compiler_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_build_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_build_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<float>(DESIGNATED_GAME_FRAMETIME)/FrameTime);
		norm_cur_vel.y += norm_vec_to_goal.y/2/(static_cast<float>(DESIGNATED_GAME_FRAMETIME)/FrameTime);
		norm_cur_vel.z += norm_vec_to_goal.z/2/(static_cast<float>(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
//	----------------------------------------------------------------------------------------------------------

