1 /*
2  * Portions of this file are copyright Rebirth contributors and licensed as
3  * described in COPYING.txt.
4  * Portions of this file are copyright Parallax Software and licensed
5  * according to the Parallax license below.
6  * See COPYING.txt for license details.
7 
8 THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
9 SOFTWARE CORPORATION ("PARALLAX").  PARALLAX, IN DISTRIBUTING THE CODE TO
10 END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
11 ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
12 IN USING, DISPLAYING,  AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
13 SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
14 FREE PURPOSES.  IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
15 CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES.  THE END-USER UNDERSTANDS
16 AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
17 COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION.  ALL RIGHTS RESERVED.
18 */
19 
20 /*
21  *
22  * Autonomous Individual movement.
23  *
24  */
25 
26 #include <algorithm>
27 #include <cstdlib>
28 #include <stdio.h>
29 #include <time.h>
30 
31 #include "inferno.h"
32 #include "game.h"
33 #include "console.h"
34 #include "3d.h"
35 
36 #include "object.h"
37 #include "dxxerror.h"
38 #include "ai.h"
39 #include "escort.h"
40 #include "laser.h"
41 #include "fvi.h"
42 #include "physfsx.h"
43 #include "physfs-serial.h"
44 #include "robot.h"
45 #include "bm.h"
46 #include "weapon.h"
47 #include "physics.h"
48 #include "collide.h"
49 #include "player.h"
50 #include "wall.h"
51 #include "vclip.h"
52 #include "fireball.h"
53 #include "morph.h"
54 #include "effects.h"
55 #include "sounds.h"
56 #include "gameseg.h"
57 #include "cntrlcen.h"
58 #include "multibot.h"
59 #include "multi.h"
60 #include "gameseq.h"
61 #include "powerup.h"
62 #include "args.h"
63 #include "fuelcen.h"
64 #include "controls.h"
65 #include "kconfig.h"
66 
67 #if DXX_USE_EDITOR
68 #include "editor/editor.h"
69 #include "editor/esegment.h"
70 #endif
71 
72 //added 05/17/99 Matt Mueller
73 #include "u_mem.h"
74 //end addition -MM
75 
76 #include "compiler-range_for.h"
77 #include "segiter.h"
78 #include "d_enumerate.h"
79 #include "d_levelstate.h"
80 #include <utility>
81 
82 using std::min;
83 
84 #define	AI_TURN_SCALE	1
85 #define	BABY_SPIDER_ID	14
86 
87 namespace dsx {
88 
89 const object *Ai_last_missile_camera;
90 
91 namespace {
92 static void init_boss_segments(const segment_array &segments, const object &boss_objnum, d_level_shared_boss_state::special_segment_array_t &a, int size_check, int one_wall_hack);
93 static void ai_multi_send_robot_position(object &objnum, int force);
94 }
95 
96 #if defined(DXX_BUILD_DESCENT_I)
97 #define	BOSS_DEATH_SOUND_DURATION	0x2ae14		//	2.68 seconds
98 
99 #elif defined(DXX_BUILD_DESCENT_II)
100 #define	FIRE_AT_NEARBY_PLAYER_THRESHOLD	(F1_0*40)
101 
102 #define	FIRE_K	8		//	Controls average accuracy of robot firing.  Smaller numbers make firing worse.  Being power of 2 doesn't matter.
103 
104 // ====================================================================================================================
105 
106 #define	MIN_LEAD_SPEED		(F1_0*4)
107 #define	MAX_LEAD_DISTANCE	(F1_0*200)
108 #define	LEAD_RANGE			(F1_0/2)
109 
110 constexpr std::array<std::array<int, 3>, NUM_D2_BOSSES> Spew_bots{{
111 	{{38, 40, -1}},
112 	{{37, -1, -1}},
113 	{{43, 57, -1}},
114 	{{26, 27, 58}},
115 	{{59, 58, 54}},
116 	{{60, 61, 54}},
117 
118 	{{69, 29, 24}},
119 	{{72, 60, 73}}
120 }};
121 
122 constexpr std::array<int, NUM_D2_BOSSES> Max_spew_bots{{2, 1, 2, 3, 3, 3, 3, 3}};
123 static fix Dist_to_last_fired_upon_player_pos;
124 #endif
125 }
126 
127 namespace dcx {
128 namespace {
129 constexpr std::integral_constant<int, F1_0 * 8> CHASE_TIME_LENGTH{};
130 constexpr std::integral_constant<int, F0_5> Robot_sound_volume{};
131 enum {
132 	Flinch_scale = 4,
133 	Attack_scale = 24,
134 };
135 #define	ANIM_RATE		(F1_0/16)
136 #define	DELTA_ANG_SCALE	16
137 
138 constexpr std::array<int8_t, 8> Mike_to_matt_xlate{{
139 	AS_REST, AS_REST, AS_ALERT, AS_ALERT, AS_FLINCH, AS_FIRE, AS_RECOIL, AS_REST
140 }};
141 
142 #define	OVERALL_AGITATION_MAX	100
143 
144 #define		MAX_AI_CLOAK_INFO	8	//	Must be a power of 2!
145 
146 #define	BOSS_CLOAK_DURATION	Boss_cloak_duration
147 #define	BOSS_DEATH_DURATION	(F1_0*6)
148 //	Amount of time since the current robot was last processed for things such as movement.
149 //	It is not valid to use FrameTime because robots do not get moved every frame.
150 
151 // ---------- John: These variables must be saved as part of gamesave. --------
152 static int             Overall_agitation;
153 static std::array<ai_cloak_info, MAX_AI_CLOAK_INFO>   Ai_cloak_info;
154 
build_savegametime_from_gametime(const fix64 gametime,const fix64 t)155 fix build_savegametime_from_gametime(const fix64 gametime, const fix64 t)
156 {
157 	const auto delta_time = t - gametime;
158 	if (delta_time < F1_0 * (-18000))
159 		return fix{F1_0 * (-18000)};
160 	return static_cast<fix>(delta_time);
161 }
162 
163 }
164 point_seg_array_t       Point_segs;
165 point_seg_array_t::iterator       Point_segs_free_ptr;
166 
167 // ------ John: End of variables which must be saved as part of gamesave. -----
168 
169 //	0	mech
170 //	1	green claw
171 //	2	spider
172 //	3	josh
173 //	4	violet
174 //	5	cloak vulcan
175 //	6	cloak mech
176 //	7	brain
177 //	8	onearm
178 //	9	plasma
179 //	10	toaster
180 //	11	bird
181 //	12	missile bird
182 //	13	polyhedron
183 //	14	baby spider
184 //	15	mini boss
185 //	16	super mech
186 //	17	shareware boss
187 //	18	cloak-green	; note, gating in this guy benefits player, cloak objects
188 //	19	vulcan
189 //	20	toad
190 //	21	4-claw
191 //	22	quad-laser
192 // 23 super boss
193 
194 // byte	Super_boss_gate_list[] = {0, 1, 2, 9, 11, 16, 18, 19, 21, 22, 0, 9, 9, 16, 16, 18, 19, 19, 22, 22};
195 constexpr std::array<int8_t, 21> Super_boss_gate_list{{
196 	0, 1, 8, 9, 10, 11, 12, 15, 16, 18, 19, 20, 22, 0, 8, 11, 19, 20, 8, 20, 8
197 }};
198 }
199 #define	MAX_GATE_INDEX	(Super_boss_gate_list.size())
200 
201 #if defined(DXX_BUILD_DESCENT_II)
202 namespace dsx {
203 
204 
205 // ------ John: End of variables which must be saved as part of gamesave. -----
206 
207 vms_vector	Last_fired_upon_player_pos;
208 
209 
210 // -- ubyte Boss_cloaks[NUM_D2_BOSSES]              = {1,1,1,1,1,1};      // Set byte if this boss can cloak
211 
212 const boss_flags_t Boss_teleports{{1,1,1,1,1,1, 1,1}}; // Set byte if this boss can teleport
213 const boss_flags_t Boss_spew_more{{0,1,0,0,0,0, 0,0}}; // If set, 50% of time, spew two bots.
214 const boss_flags_t Boss_spews_bots_energy{{1,1,0,1,0,1, 1,1}}; // Set byte if boss spews bots when hit by energy weapon.
215 const boss_flags_t Boss_spews_bots_matter{{0,0,1,1,1,1, 0,1}}; // Set byte if boss spews bots when hit by matter weapon.
216 const boss_flags_t Boss_invulnerable_energy{{0,0,1,1,0,0, 0,0}}; // Set byte if boss is invulnerable to energy weapons.
217 const boss_flags_t Boss_invulnerable_matter{{0,0,0,0,1,1, 1,0}}; // Set byte if boss is invulnerable to matter weapons.
218 const boss_flags_t Boss_invulnerable_spot{{0,0,0,0,0,1, 0,1}}; // Set byte if boss is invulnerable in all but a certain spot.  (Dot product fvec|vec_to_collision < BOSS_INVULNERABLE_DOT)
219 
220 segnum_t             Believed_player_seg;
221 }
222 #endif
223 
224 namespace dcx {
225 
226 namespace {
227 
228 struct robot_to_player_visibility_state
229 {
230 	vms_vector vec_to_player;
231 	player_visibility_state visibility;
232 	uint8_t initialized = 0;
233 };
234 
235 struct awareness_t : enumerated_array<player_awareness_type_t, MAX_SEGMENTS, segnum_t>
236 {
237 };
238 
239 }
240 
241 }
242 
243 static int ai_evaded;
244 
245 // These globals are set by a call to find_vector_intersection, which is a slow routine,
246 // so we don't want to call it again (for this object) unless we have to.
247 static vms_vector  Hit_pos;
248 static int         Hit_type;
249 static fvi_info    Hit_data;
250 
251 namespace dcx {
252 vms_vector      Believed_player_pos;
253 
254 namespace {
255 
silly_animation_angle(fixang vms_angvec::* const a,const vms_angvec & jp,const vms_angvec & pobjp,const int flinch_attack_scale,vms_angvec & goal_angles,vms_angvec & delta_angles)256 static bool silly_animation_angle(fixang vms_angvec::*const a, const vms_angvec &jp, const vms_angvec &pobjp, const int flinch_attack_scale, vms_angvec &goal_angles, vms_angvec &delta_angles)
257 {
258 	const fix delta_angle = jp.*a - pobjp.*a;
259 	if (!delta_angle)
260 		return false;
261 	goal_angles.*a = jp.*a;
262 	const fix delta_anim_rate = (delta_angle >= F1_0/2)
263 		? -ANIM_RATE
264 		: (delta_angle >= 0)
265 			? ANIM_RATE
266 			: (delta_angle >= -F1_0/2)
267 				? -ANIM_RATE
268 				: ANIM_RATE
269 	;
270 	const fix delta_2 = (flinch_attack_scale != 1)
271 		? delta_anim_rate * flinch_attack_scale
272 		: delta_anim_rate;
273 	delta_angles.*a = delta_2 / DELTA_ANG_SCALE;		// complete revolutions per second
274 	return true;
275 }
276 
frame_animation_angle(fixang vms_angvec::* const a,const fix frametime,const vms_angvec & deltaang,const vms_angvec & goalang,vms_angvec & curang)277 static void frame_animation_angle(fixang vms_angvec::*const a, const fix frametime, const vms_angvec &deltaang, const vms_angvec &goalang, vms_angvec &curang)
278 {
279 	fix delta_to_goal = goalang.*a - curang.*a;
280 	if (delta_to_goal > 32767)
281 		delta_to_goal = delta_to_goal - 65536;
282 	else if (delta_to_goal < -32767)
283 		delta_to_goal = 65536 + delta_to_goal;
284 	if (delta_to_goal)
285 	{
286 		// Due to deltaang.*a being usually small values and frametime getting smaller with higher FPS, the usual use of fixmul will have scaled_delta_angle result in 0 way too often and long, making the robot animation run circles around itself. So multiply deltaang by DELTA_ANG_SCALE when passed to fixmul.
287 		const fix scaled_delta_angle = fixmul(deltaang.*a * DELTA_ANG_SCALE, frametime); // fixmul(deltaang.*a, frametime) * DELTA_ANG_SCALE;
288 		auto &ca = curang.*a;
289 		if (abs(delta_to_goal) < abs(scaled_delta_angle))
290 			ca = goalang.*a;
291 		else
292 			ca += scaled_delta_angle;
293 	}
294 }
295 
move_toward_vector_component_assign(fix vms_vector::* const p,const vms_vector & vec_goal,const fix frametime32,vms_vector & velocity)296 static void move_toward_vector_component_assign(fix vms_vector::*const p, const vms_vector &vec_goal, const fix frametime32, vms_vector &velocity)
297 {
298 	velocity.*p = (velocity.*p / 2) + fixmul(vec_goal.*p, frametime32);
299 }
300 
move_toward_vector_component_add(fix vms_vector::* const p,const vms_vector & vec_goal,const fix frametime64,const fix difficulty_scale,vms_vector & velocity)301 static void move_toward_vector_component_add(fix vms_vector::*const p, const vms_vector &vec_goal, const fix frametime64, const fix difficulty_scale, vms_vector &velocity)
302 {
303 	velocity.*p += fixmul(vec_goal.*p, frametime64) * difficulty_scale / 4;
304 }
305 
306 }
307 
308 }
309 
310 #define	AIS_MAX	8
311 #define	AIE_MAX	4
312 
313 #ifndef NDEBUG
314 #if PARALLAX
315 #if defined(DXX_BUILD_DESCENT_I)
316 // Index into this array with ailp->mode
317 constexpr char mode_text[][16] = {
318 	"STILL",
319 	"WANDER",
320 	"FOL_PATH",
321 	"CHASE_OBJ",
322 	"RUN_FROM",
323 	"HIDE",
324 	"FOL_PATH2",
325 	"OPEN_DOOR",
326 };
327 
328 //	Index into this array with aip->behavior
329 constexpr std::array<char[9], 6> behavior_text{
330 	"STILL   ",
331 	"NORMAL  ",
332 	"HIDE    ",
333 	"RUN_FROM",
334 	"FOLPATH ",
335 	"STATION "
336 };
337 #endif
338 #endif
339 #endif
340 
341 // Current state indicates where the robot current is, or has just done.
342 // Transition table between states for an AI object.
343 // First dimension is trigger event.
344 // Second dimension is current state.
345 // Third dimension is goal state.
346 // Result is new goal state.
347 // ERR_ means something impossible has happened.
348 constexpr int8_t Ai_transition_table[AI_MAX_EVENT][AI_MAX_STATE][AI_MAX_STATE] = {
349 	{
350 		// Event = AIE_FIRE, a nearby object fired
351 		// none     rest      srch      lock      flin      fire      reco        // CURRENT is rows, GOAL is columns
352 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO }, // none
353 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO }, // rest
354 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO }, // search
355 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO }, // lock
356 		{ AIS_ERR_, AIS_REST, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FIRE, AIS_RECO }, // flinch
357 		{ AIS_ERR_, AIS_FIRE, AIS_FIRE, AIS_FIRE, AIS_FLIN, AIS_FIRE, AIS_RECO }, // fire
358 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_FIRE }  // recoil
359 	},
360 
361 	// Event = AIE_HITT, a nearby object was hit (or a wall was hit)
362 	{
363 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO},
364 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO},
365 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO},
366 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO},
367 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FLIN},
368 		{ AIS_ERR_, AIS_REST, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FIRE, AIS_RECO},
369 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_FIRE}
370 	},
371 
372 	// Event = AIE_COLL, player collided with robot
373 	{
374 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO},
375 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO},
376 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO},
377 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_RECO},
378 		{ AIS_ERR_, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_LOCK, AIS_FLIN, AIS_FLIN},
379 		{ AIS_ERR_, AIS_REST, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FIRE, AIS_RECO},
380 		{ AIS_ERR_, AIS_LOCK, AIS_LOCK, AIS_LOCK, AIS_FLIN, AIS_FIRE, AIS_FIRE}
381 	},
382 
383 	// Event = AIE_HURT, player hurt robot (by firing at and hitting it)
384 	// Note, this doesn't necessarily mean the robot JUST got hit, only that that is the most recent thing that happened.
385 	{
386 		{ AIS_ERR_, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN},
387 		{ AIS_ERR_, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN},
388 		{ AIS_ERR_, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN},
389 		{ AIS_ERR_, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN},
390 		{ AIS_ERR_, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN},
391 		{ AIS_ERR_, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN},
392 		{ AIS_ERR_, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN, AIS_FLIN}
393 	}
394 };
395 
396 namespace dsx {
397 
get_robot_weapon(const robot_info & ri,const unsigned gun_num)398 weapon_id_type get_robot_weapon(const robot_info &ri, const unsigned gun_num)
399 {
400 #if defined(DXX_BUILD_DESCENT_I)
401 	(void)gun_num;
402 #elif defined(DXX_BUILD_DESCENT_II)
403 	if (ri.weapon_type2 != weapon_none && !gun_num)
404 		return ri.weapon_type2;
405 #endif
406 	return ri.weapon_type;
407 }
408 
409 namespace {
410 
ready_to_fire_weapon1(const ai_local & ailp,fix threshold)411 static int ready_to_fire_weapon1(const ai_local &ailp, fix threshold)
412 {
413 	return ailp.next_fire <= threshold;
414 }
415 
ready_to_fire_weapon2(const robot_info & robptr,const ai_local & ailp,fix threshold)416 static int ready_to_fire_weapon2(const robot_info &robptr, const ai_local &ailp, fix threshold)
417 {
418 #if defined(DXX_BUILD_DESCENT_I)
419 	(void)robptr;
420 	(void)ailp;
421 	(void)threshold;
422 	return 0;
423 #elif defined(DXX_BUILD_DESCENT_II)
424 	if (robptr.weapon_type2 == weapon_none)
425 		return 0;
426 	return ailp.next_fire2 <= threshold;
427 #endif
428 }
429 
430 // ----------------------------------------------------------------------------
431 // Return firing status.
432 // If ready to fire a weapon, return true, else return false.
433 // Ready to fire a weapon if next_fire <= 0 or next_fire2 <= 0.
ready_to_fire_any_weapon(const robot_info & robptr,const ai_local & ailp,fix threshold)434 static int ready_to_fire_any_weapon(const robot_info &robptr, const ai_local &ailp, fix threshold)
435 {
436 	return ready_to_fire_weapon1(ailp, threshold) || ready_to_fire_weapon2(robptr, ailp, threshold);
437 }
438 
439 }
440 
441 // ---------------------------------------------------------------------------------------------------------------------
442 //	Given a behavior, set initial mode.
ai_behavior_to_mode(ai_behavior behavior)443 ai_mode ai_behavior_to_mode(ai_behavior behavior)
444 {
445 	switch (behavior) {
446 		case ai_behavior::AIB_STILL:
447 			return ai_mode::AIM_STILL;
448 		case ai_behavior::AIB_NORMAL:
449 			return ai_mode::AIM_CHASE_OBJECT;
450 		case ai_behavior::AIB_RUN_FROM:
451 			return ai_mode::AIM_RUN_FROM_OBJECT;
452 		case ai_behavior::AIB_STATION:
453 			return ai_mode::AIM_STILL;
454 #if defined(DXX_BUILD_DESCENT_I)
455 		case ai_behavior::AIB_HIDE:
456 			return ai_mode::AIM_HIDE;
457 		case ai_behavior::AIB_FOLLOW_PATH:
458 			return ai_mode::AIM_FOLLOW_PATH;
459 #elif defined(DXX_BUILD_DESCENT_II)
460 		case ai_behavior::AIB_BEHIND:
461 			return ai_mode::AIM_BEHIND;
462 		case ai_behavior::AIB_SNIPE:
463 			return ai_mode::AIM_STILL;	//	Changed, 09/13/95, MK, snipers are still until they see you or are hit.
464 		case ai_behavior::AIB_FOLLOW:
465 			return ai_mode::AIM_FOLLOW_PATH;
466 #endif
467 		default:	Int3();	//	Contact Mike: Error, illegal behavior type
468 	}
469 
470 	return ai_mode::AIM_STILL;
471 }
472 
473 // ---------------------------------------------------------------------------------------------------------------------
474 //	Call every time the player starts a new ship.
ai_init_boss_for_ship(void)475 void ai_init_boss_for_ship(void)
476 {
477 #if defined(DXX_BUILD_DESCENT_II)
478 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
479 	BossUniqueState.Boss_hit_time = -F1_0*10;
480 #endif
481 }
482 
483 namespace {
484 
boss_init_all_segments(const segment_array & Segments,const object & boss_objnum)485 static void boss_init_all_segments(const segment_array &Segments, const object &boss_objnum)
486 {
487 	auto &Boss_gate_segs = LevelSharedBossState.Gate_segs;
488 	auto &Boss_teleport_segs = LevelSharedBossState.Teleport_segs;
489 	if (!Boss_teleport_segs.empty())
490 		return;	// already have boss segs
491 
492 	init_boss_segments(Segments, boss_objnum, Boss_gate_segs, 0, 0);
493 
494 	init_boss_segments(Segments, boss_objnum, Boss_teleport_segs, 1, 0);
495 #if defined(DXX_BUILD_DESCENT_II)
496 	if (Boss_teleport_segs.size() < 2)
497 		init_boss_segments(Segments, boss_objnum, Boss_teleport_segs, 1, 1);
498 #endif
499 }
500 
501 }
502 
503 // ---------------------------------------------------------------------------------------------------------------------
504 //	initial_mode == -1 means leave mode unchanged.
init_ai_object(const vmobjptridx_t objp,ai_behavior behavior,const imsegidx_t hide_segment)505 void init_ai_object(const vmobjptridx_t objp, ai_behavior behavior, const imsegidx_t hide_segment)
506 {
507 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
508 	ai_static	*const aip = &objp->ctype.ai_info;
509 	ai_local		*const ailp = &aip->ail;
510 
511 	*ailp = {};
512 
513 	if (static_cast<unsigned>(behavior) == 0) {
514 		behavior = ai_behavior::AIB_NORMAL;
515 		aip->behavior = behavior;
516 	}
517 
518 	//	mode is now set from the Robot dialog, so this should get overwritten.
519 	ailp->mode = ai_mode::AIM_STILL;
520 
521 	ailp->previous_visibility = player_visibility_state::no_line_of_sight;
522 
523 	{
524 		aip->behavior = behavior;
525 		ailp->mode = ai_behavior_to_mode(aip->behavior);
526 	}
527 
528 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
529 	auto &robptr = Robot_info[get_robot_id(objp)];
530 #if defined(DXX_BUILD_DESCENT_II)
531 	if (robot_is_companion(robptr)) {
532 		auto &BuddyState = LevelUniqueObjectState.BuddyState;
533 		BuddyState.Buddy_objnum = objp;
534 		ailp->mode = ai_mode::AIM_GOTO_PLAYER;
535 	}
536 
537 	if (robot_is_thief(robptr)) {
538 		aip->behavior = ai_behavior::AIB_SNIPE;
539 		ailp->mode = ai_mode::AIM_THIEF_WAIT;
540 	}
541 
542 	if (robptr.attack_type) {
543 		aip->behavior = ai_behavior::AIB_NORMAL;
544 		ailp->mode = ai_behavior_to_mode(aip->behavior);
545 	}
546 #endif
547 
548 	// This is astonishingly stupid!  This routine gets called by matcens! KILL KILL KILL!!! Point_segs_free_ptr = Point_segs;
549 
550 	objp->mtype.phys_info.velocity = {};
551 	ailp->player_awareness_time = 0;
552 	ailp->player_awareness_type = player_awareness_type_t::PA_NONE;
553 	aip->GOAL_STATE = AIS_SRCH;
554 	aip->CURRENT_STATE = AIS_REST;
555 	ailp->time_player_seen = GameTime64;
556 	ailp->next_misc_sound_time = GameTime64;
557 	ailp->time_player_sound_attacked = GameTime64;
558 
559 	aip->hide_segment = hide_segment;
560 	ailp->goal_segment = hide_segment;
561 	aip->hide_index = -1;			// This means the path has not yet been created.
562 	aip->cur_path_index = 0;
563 
564 	aip->SKIP_AI_COUNT = 0;
565 
566 	if (robptr.cloak_type == RI_CLOAKED_ALWAYS)
567 		aip->CLOAKED = 1;
568 	else
569 		aip->CLOAKED = 0;
570 
571 	objp->mtype.phys_info.flags |= (PF_BOUNCE | PF_TURNROLL);
572 
573 	aip->REMOTE_OWNER = -1;
574 
575 #if defined(DXX_BUILD_DESCENT_II)
576 	aip->dying_sound_playing = 0;
577 	aip->dying_start_time = 0;
578 #endif
579 	aip->danger_laser_num = object_none;
580 
581 	if (robptr.boss_flag
582 #if DXX_USE_EDITOR
583 		&& !EditorWindow
584 #endif
585 		)
586 	{
587 		BossUniqueState = {};
588 		boss_init_all_segments(Segments, objp);
589 	}
590 }
591 
592 // ---------------------------------------------------------------------------------------------------------------------
init_ai_objects(void)593 void init_ai_objects(void)
594 {
595 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
596 	auto &Boss_gate_segs = LevelSharedBossState.Gate_segs;
597 	auto &Boss_teleport_segs = LevelSharedBossState.Teleport_segs;
598 	auto &Objects = LevelUniqueObjectState.Objects;
599 	auto &vmobjptridx = Objects.vmptridx;
600 	Point_segs_free_ptr = Point_segs.begin();
601 	Boss_gate_segs.clear();
602 	Boss_teleport_segs.clear();
603 
604 	range_for (const auto &&o, vmobjptridx)
605 	{
606 		auto &obj = *o;
607 		if (obj.type == OBJ_ROBOT && obj.control_source == object::control_type::ai)
608 			init_ai_object(o, obj.ctype.ai_info.behavior, obj.ctype.ai_info.hide_segment);
609 	}
610 
611 	BossUniqueState.Boss_dying_sound_playing = 0;
612 	BossUniqueState.Boss_dying = 0;
613 
614 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
615 #define D1_Boss_gate_interval	F1_0*5 - Difficulty_level*F1_0/2
616 #if defined(DXX_BUILD_DESCENT_I)
617 	GameUniqueState.Boss_gate_interval = D1_Boss_gate_interval;
618 #elif defined(DXX_BUILD_DESCENT_II)
619 	ai_do_cloak_stuff();
620 
621 	init_buddy_for_level();
622 
623 	if (EMULATING_D1)
624 	{
625 		LevelSharedBossState.Boss_cloak_interval = d_level_shared_boss_state::D1_Boss_cloak_interval::value;
626 		LevelSharedBossState.Boss_teleport_interval = d_level_shared_boss_state::D1_Boss_teleport_interval::value;
627 		GameUniqueState.Boss_gate_interval = D1_Boss_gate_interval;
628 	}
629 	else
630 	{
631 		GameUniqueState.Boss_gate_interval = F1_0*4 - Difficulty_level*i2f(2)/3;
632 		if (Current_level_num == Current_mission->last_level)
633 		{
634 		LevelSharedBossState.Boss_teleport_interval = F1_0*10;
635 		LevelSharedBossState.Boss_cloak_interval = F1_0*15;					//	Time between cloaks
636 		} else
637 		{
638 		LevelSharedBossState.Boss_teleport_interval = F1_0*7;
639 		LevelSharedBossState.Boss_cloak_interval = F1_0*10;					//	Time between cloaks
640 		}
641 	}
642 #endif
643 #undef D1_Boss_gate_interval
644 }
645 
646 //-------------------------------------------------------------------------------------------
ai_turn_towards_vector(const vms_vector & goal_vector,object_base & objp,fix rate)647 void ai_turn_towards_vector(const vms_vector &goal_vector, object_base &objp, fix rate)
648 {
649 	//	Not all robots can turn, eg, SPECIAL_REACTOR_ROBOT
650 	if (rate == 0)
651 		return;
652 
653 	if (objp.type == OBJ_ROBOT && (get_robot_id(objp) == BABY_SPIDER_ID)) {
654 		physics_turn_towards_vector(goal_vector, objp, rate);
655 		return;
656 	}
657 
658 	auto new_fvec = goal_vector;
659 
660 	const fix dot = vm_vec_dot(goal_vector, objp.orient.fvec);
661 
662 	if (dot < (F1_0 - FrameTime/2)) {
663 		fix	new_scale = fixdiv(FrameTime * AI_TURN_SCALE, rate);
664 		vm_vec_scale(new_fvec, new_scale);
665 		vm_vec_add2(new_fvec, objp.orient.fvec);
666 		auto mag = vm_vec_normalize_quick(new_fvec);
667 		if (mag < F1_0/256) {
668 			new_fvec = goal_vector;		//	if degenerate vector, go right to goal
669 		}
670 	}
671 
672 #if defined(DXX_BUILD_DESCENT_II)
673 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
674 	if (const auto Seismic_tremor_magnitude = LevelUniqueSeismicState.Seismic_tremor_magnitude)
675 	{
676 		fix			scale;
677 		scale = fixdiv(2*Seismic_tremor_magnitude, Robot_info[get_robot_id(objp)].mass);
678 		vm_vec_scale_add2(new_fvec, make_random_vector(), scale);
679 	}
680 #endif
681 
682 	vm_vector_2_matrix(objp.orient, new_fvec, nullptr, &objp.orient.rvec);
683 }
684 
685 #if defined(DXX_BUILD_DESCENT_I)
686 namespace {
ai_turn_randomly(const vms_vector & vec_to_player,object_base & obj,const fix rate,const player_visibility_state previous_visibility)687 static void ai_turn_randomly(const vms_vector &vec_to_player, object_base &obj, const fix rate, const player_visibility_state previous_visibility)
688 {
689 	vms_vector	curvec;
690 
691 	//	Random turning looks too stupid, so 1/4 of time, cheat.
692 	if (player_is_visible(previous_visibility))
693                 if (d_rand() > 0x7400) {
694 			ai_turn_towards_vector(vec_to_player, obj, rate);
695 			return;
696 		}
697 //--debug--     if (d_rand() > 0x6000)
698 //--debug-- 		Prevented_turns++;
699 
700 	curvec = obj.mtype.phys_info.rotvel;
701 
702 	curvec.y += F1_0/64;
703 
704 	curvec.x += curvec.y/6;
705 	curvec.y += curvec.z/4;
706 	curvec.z += curvec.x/10;
707 
708 	if (abs(curvec.x) > F1_0/8) curvec.x /= 4;
709 	if (abs(curvec.y) > F1_0/8) curvec.y /= 4;
710 	if (abs(curvec.z) > F1_0/8) curvec.z /= 4;
711 
712 	obj.mtype.phys_info.rotvel = curvec;
713 
714 }
715 }
716 #endif
717 
718 //	Overall_agitation affects:
719 //		Widens field of view.  Field of view is in range 0..1 (specified in bitmaps.tbl as N/360 degrees).
720 //			Overall_agitation/128 subtracted from field of view, making robots see wider.
721 //		Increases distance to which robot will search to create path to player by Overall_agitation/8 segments.
722 //		Decreases wait between fire times by Overall_agitation/64 seconds.
723 
724 
725 // --------------------------------------------------------------------------------------------------------------------
726 //	Returns:
727 //		0		Player is not visible from object, obstruction or something.
728 //		1		Player is visible, but not in field of view.
729 //		2		Player is visible and in field of view.
730 //	Note: Uses Believed_player_pos as player's position for cloak effect.
731 //	NOTE: Will destructively modify *pos if *pos is outside the mine.
player_is_visible_from_object(const vmobjptridx_t objp,vms_vector & pos,const fix field_of_view,const vms_vector & vec_to_player)732 player_visibility_state player_is_visible_from_object(const vmobjptridx_t objp, vms_vector &pos, const fix field_of_view, const vms_vector &vec_to_player)
733 {
734 	fix			dot;
735 	fvi_query	fq;
736 
737 #if defined(DXX_BUILD_DESCENT_II)
738 	//	Assume that robot's gun tip is in same segment as robot's center.
739 	if (objp->control_source == object::control_type::ai)
740 		objp->ctype.ai_info.SUB_FLAGS &= ~SUB_FLAGS_GUNSEG;
741 #endif
742 
743 	fq.p0						= &pos;
744 	if ((pos.x != objp->pos.x) || (pos.y != objp->pos.y) || (pos.z != objp->pos.z)) {
745 		auto &Segments = LevelSharedSegmentState.get_segments();
746 		const auto &&segnum = find_point_seg(LevelSharedSegmentState, pos, Segments.vcptridx(objp->segnum));
747 		if (segnum == segment_none) {
748 			fq.startseg = objp->segnum;
749 			pos = objp->pos;
750 			move_towards_segment_center(LevelSharedSegmentState, objp);
751 		} else
752 		{
753 #if defined(DXX_BUILD_DESCENT_II)
754 			if (segnum != objp->segnum) {
755 				if (objp->control_source == object::control_type::ai)
756 					objp->ctype.ai_info.SUB_FLAGS |= SUB_FLAGS_GUNSEG;
757 			}
758 #endif
759 			fq.startseg = segnum;
760 		}
761 	} else
762 		fq.startseg			= objp->segnum;
763 	fq.p1						= &Believed_player_pos;
764 	fq.rad					= F1_0/4;
765 	fq.thisobjnum			= objp;
766 	fq.ignore_obj_list.first = nullptr;
767 	fq.flags					= FQ_TRANSWALL; // -- Why were we checking objects? | FQ_CHECK_OBJS;		//what about trans walls???
768 
769 	Hit_type = find_vector_intersection(fq, Hit_data);
770 
771 	Hit_pos = Hit_data.hit_pnt;
772 
773 	if (Hit_type == HIT_NONE)
774 	{
775 		dot = vm_vec_dot(vec_to_player, objp->orient.fvec);
776 		if (dot > field_of_view - (Overall_agitation << 9)) {
777 			return player_visibility_state::visible_and_in_field_of_view;
778 		} else {
779 			return player_visibility_state::visible_not_in_field_of_view;
780 		}
781 	} else {
782 		return player_visibility_state::no_line_of_sight;
783 	}
784 }
785 
786 namespace {
787 // ------------------------------------------------------------------------------------------------------------------
788 //	Return 1 if animates, else return 0
do_silly_animation(object & objp)789 static int do_silly_animation(object &objp)
790 {
791 	int				robot_type, gun_num, robot_state;
792 	polyobj_info	*const pobj_info = &objp.rtype.pobj_info;
793 	auto &aip = objp.ctype.ai_info;
794 	int				num_guns, at_goal;
795 	int				attack_type;
796 	int				flinch_attack_scale = 1;
797 
798 	robot_type = get_robot_id(objp);
799 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
800 	auto &Robot_joints = LevelSharedRobotJointState.Robot_joints;
801 	auto &robptr = Robot_info[robot_type];
802 	num_guns = robptr.n_guns;
803 	attack_type = robptr.attack_type;
804 
805 	if (num_guns == 0) {
806 		return 0;
807 	}
808 
809 	//	This is a hack.  All positions should be based on goal_state, not GOAL_STATE.
810 	robot_state = Mike_to_matt_xlate[aip.GOAL_STATE];
811 	// previous_robot_state = Mike_to_matt_xlate[aip->CURRENT_STATE];
812 
813 	if (attack_type) // && ((robot_state == AS_FIRE) || (robot_state == AS_RECOIL)))
814 		flinch_attack_scale = Attack_scale;
815 	else if ((robot_state == AS_FLINCH) || (robot_state == AS_RECOIL))
816 		flinch_attack_scale = Flinch_scale;
817 
818 	at_goal = 1;
819 	auto &Polygon_models = LevelSharedPolygonModelState.Polygon_models;
820 	for (gun_num=0; gun_num <= num_guns; gun_num++) {
821 		const auto &&ras = robot_get_anim_state(Robot_info, Robot_joints, robot_type, gun_num, robot_state);
822 
823 		auto &ail = aip.ail;
824 		range_for (auto &jr, ras)
825 		{
826 			unsigned jointnum = jr.jointnum;
827 			auto &jp = jr.angles;
828 			vms_angvec	*pobjp = &pobj_info->anim_angles[jointnum];
829 
830 			if (jointnum >= Polygon_models[objp.rtype.pobj_info.model_num].n_models) {
831 				Int3();		// Contact Mike: incompatible data, illegal jointnum, problem in pof file?
832 				continue;
833 			}
834 			auto &goal_angles = ail.goal_angles[jointnum];
835 			auto &delta_angles = ail.delta_angles[jointnum];
836 			const auto animate_p = silly_animation_angle(&vms_angvec::p, jp, *pobjp, flinch_attack_scale, goal_angles, delta_angles);
837 			const auto animate_b = silly_animation_angle(&vms_angvec::b, jp, *pobjp, flinch_attack_scale, goal_angles, delta_angles);
838 			const auto animate_h = silly_animation_angle(&vms_angvec::h, jp, *pobjp, flinch_attack_scale, goal_angles, delta_angles);
839 			if (gun_num == 0)
840 			{
841 				if (animate_p || animate_b || animate_h)
842 					at_goal = 0;
843 			}
844 		}
845 
846 		if (at_goal) {
847 			//ai_static	*aip = &objp->ctype.ai_info;
848 			ail.achieved_state[gun_num] = ail.goal_state[gun_num];
849 			if (ail.achieved_state[gun_num] == AIS_RECO)
850 				ail.goal_state[gun_num] = AIS_FIRE;
851 			else if (ail.achieved_state[gun_num] == AIS_FLIN)
852 				ail.goal_state[gun_num] = AIS_LOCK;
853 		}
854 	}
855 
856 	if (at_goal == 1) //num_guns)
857 		aip.CURRENT_STATE = aip.GOAL_STATE;
858 
859 	return 1;
860 }
861 
862 //	------------------------------------------------------------------------------------------
863 //	Move all sub-objects in an object towards their goals.
864 //	Current orientation of object is at:	pobj_info.anim_angles
865 //	Goal orientation of object is at:		ai_info.goal_angles
866 //	Delta orientation of object is at:		ai_info.delta_angles
ai_frame_animation(object & objp)867 static void ai_frame_animation(object &objp)
868 {
869 	int	joint;
870 	auto &pobj_info = objp.rtype.pobj_info;
871 	auto &Polygon_models = LevelSharedPolygonModelState.Polygon_models;
872 	const auto num_joints = Polygon_models[pobj_info.model_num].n_models;
873 	const auto &ail = objp.ctype.ai_info.ail;
874 	for (joint=1; joint<num_joints; joint++) {
875 		auto &curang = pobj_info.anim_angles[joint];
876 		auto &goalang = ail.goal_angles[joint];
877 		auto &deltaang = ail.delta_angles[joint];
878 
879 		const fix frametime = FrameTime;
880 		frame_animation_angle(&vms_angvec::p, frametime, deltaang, goalang, curang);
881 		frame_animation_angle(&vms_angvec::b, frametime, deltaang, goalang, curang);
882 		frame_animation_angle(&vms_angvec::h, frametime, deltaang, goalang, curang);
883 	}
884 }
885 
886 // ----------------------------------------------------------------------------------
set_next_fire_time(object & objp,ai_local & ailp,const robot_info & robptr,const unsigned gun_num)887 static void set_next_fire_time(object &objp, ai_local &ailp, const robot_info &robptr, const unsigned gun_num)
888 {
889 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
890 #if defined(DXX_BUILD_DESCENT_I)
891 	(void)objp;
892 	(void)gun_num;
893 	ailp.rapidfire_count++;
894 
895 	if (ailp.rapidfire_count < robptr.rapidfire_count[Difficulty_level]) {
896 		ailp.next_fire = min(F1_0/8, robptr.firing_wait[Difficulty_level]/2);
897 	} else {
898 		ailp.rapidfire_count = 0;
899 		ailp.next_fire = robptr.firing_wait[Difficulty_level];
900 	}
901 #elif defined(DXX_BUILD_DESCENT_II)
902 	//	For guys in snipe mode, they have a 50% shot of getting this shot in free.
903 	if ((gun_num != 0) || (robptr.weapon_type2 == weapon_none))
904 		if ((objp.ctype.ai_info.behavior != ai_behavior::AIB_SNIPE) || (d_rand() > 16384))
905 			ailp.rapidfire_count++;
906 
907 	//	Old way, 10/15/95: Continuous rapidfire if rapidfire_count set.
908 // -- 	if (((robptr.weapon_type2 == -1) || (gun_num != 0)) && (ailp->rapidfire_count < robptr.rapidfire_count[Difficulty_level])) {
909 // -- 		ailp->next_fire = min(F1_0/8, robptr.firing_wait[Difficulty_level]/2);
910 // -- 	} else {
911 // -- 		if ((robptr.weapon_type2 == -1) || (gun_num != 0)) {
912 // -- 			ailp->rapidfire_count = 0;
913 // -- 			ailp->next_fire = robptr.firing_wait[Difficulty_level];
914 // -- 		} else
915 // -- 			ailp->next_fire2 = robptr.firing_wait2[Difficulty_level];
916 // -- 	}
917 
918 	if ((gun_num != 0 || robptr.weapon_type2 == weapon_none) && ailp.rapidfire_count < robptr.rapidfire_count[Difficulty_level])
919 	{
920 		ailp.next_fire = min(F1_0/8, robptr.firing_wait[Difficulty_level]/2);
921 	} else {
922 		if ((robptr.weapon_type2 == weapon_none) || (gun_num != 0)) {
923 			ailp.next_fire = robptr.firing_wait[Difficulty_level];
924 			if (ailp.rapidfire_count >= robptr.rapidfire_count[Difficulty_level])
925 				ailp.rapidfire_count = 0;
926 		} else
927 			ailp.next_fire2 = robptr.firing_wait2[Difficulty_level];
928 	}
929 #endif
930 }
931 }
932 
933 // ----------------------------------------------------------------------------------
934 //	When some robots collide with the player, they attack.
935 //	If player is cloaked, then robot probably didn't actually collide, deal with that here.
do_ai_robot_hit_attack(const vmobjptridx_t robot,const vmobjptridx_t playerobj,const vms_vector & collision_point)936 void do_ai_robot_hit_attack(const vmobjptridx_t robot, const vmobjptridx_t playerobj, const vms_vector &collision_point)
937 {
938 	ai_local &ailp = robot->ctype.ai_info.ail;
939 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
940 	auto &robptr = Robot_info[get_robot_id(robot)];
941 
942 //#ifndef NDEBUG
943 	if (cheats.robotfiringsuspended)
944 		return;
945 //#endif
946 
947 	//	If player is dead, stop firing.
948 	object &plrobj = *playerobj;
949 	if (plrobj.type == OBJ_GHOST)
950 		return;
951 
952 	if (robptr.attack_type == 1) {
953 		if (ready_to_fire_weapon1(ailp, 0)) {
954 			auto &player_info = plrobj.ctype.player_info;
955 			if (!(player_info.powerup_flags & PLAYER_FLAGS_CLOAKED))
956 				if (vm_vec_dist_quick(plrobj.pos, robot->pos) < robot->size + plrobj.size + F1_0*2)
957 				{
958 					collide_player_and_nasty_robot( playerobj, robot, collision_point );
959 #if defined(DXX_BUILD_DESCENT_II)
960 					auto &energy = player_info.energy;
961 					if (robptr.energy_drain && energy) {
962 						energy -= robptr.energy_drain * F1_0;
963 						if (energy < 0)
964 							energy = 0;
965 					}
966 #endif
967 				}
968 
969 			robot->ctype.ai_info.GOAL_STATE = AIS_RECO;
970 			set_next_fire_time(robot, ailp, robptr, 1);	//	1 = gun_num: 0 is special (uses next_fire2)
971 		}
972 	}
973 }
974 
975 namespace {
976 #if defined(DXX_BUILD_DESCENT_II)
977 // --------------------------------------------------------------------------------------------------------------------
978 //	Computes point at which projectile fired by robot can hit player given positions, player vel, elapsed time
compute_lead_component(fix player_pos,fix robot_pos,fix player_vel,fix elapsed_time)979 static fix compute_lead_component(fix player_pos, fix robot_pos, fix player_vel, fix elapsed_time)
980 {
981 	return fixdiv(player_pos - robot_pos, elapsed_time) + player_vel;
982 }
983 
compute_lead_component(fix vms_vector::* const m,vms_vector & out,const vms_vector & believed_player_pos,const vms_vector & fire_point,const vms_vector & velocity,const fix projected_time)984 static void compute_lead_component(fix vms_vector::*const m, vms_vector &out, const vms_vector &believed_player_pos, const vms_vector &fire_point, const vms_vector &velocity, const fix projected_time)
985 {
986 	out.*m = compute_lead_component(believed_player_pos.*m, fire_point.*m, velocity.*m, projected_time);
987 }
988 
989 // --------------------------------------------------------------------------------------------------------------------
990 //	Lead the player, returning point to fire at in fire_point.
991 //	Rules:
992 //		Player not cloaked
993 //		Player must be moving at a speed >= MIN_LEAD_SPEED
994 //		Player not farther away than MAX_LEAD_DISTANCE
995 //		dot(vector_to_player, player_direction) must be in -LEAD_RANGE..LEAD_RANGE
996 //		if firing a matter weapon, less leading, based on skill level.
lead_player(const object_base & objp,const vms_vector & fire_point,const vms_vector & believed_player_pos,int gun_num,vms_vector & fire_vec)997 static int lead_player(const object_base &objp, const vms_vector &fire_point, const vms_vector &believed_player_pos, int gun_num, vms_vector &fire_vec)
998 {
999 	const auto &plrobj = *ConsoleObject;
1000 	if (plrobj.ctype.player_info.powerup_flags & PLAYER_FLAGS_CLOAKED)
1001 		return 0;
1002 
1003 	const auto &velocity = plrobj.mtype.phys_info.velocity;
1004 	auto player_movement_dir = velocity;
1005 	const auto player_speed = vm_vec_normalize_quick(player_movement_dir);
1006 
1007 	if (player_speed < MIN_LEAD_SPEED)
1008 		return 0;
1009 
1010 	auto vec_to_player = vm_vec_sub(believed_player_pos, fire_point);
1011 	const auto dist_to_player = vm_vec_normalize_quick(vec_to_player);
1012 	if (dist_to_player > MAX_LEAD_DISTANCE)
1013 		return 0;
1014 
1015 	const fix dot = vm_vec_dot(vec_to_player, player_movement_dir);
1016 
1017 	if ((dot < -LEAD_RANGE) || (dot > LEAD_RANGE))
1018 		return 0;
1019 
1020 	//	Looks like it might be worth trying to lead the player.
1021 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1022 	const auto weapon_type = get_robot_weapon(Robot_info[get_robot_id(objp)], gun_num);
1023 
1024 	const weapon_info *const wptr = &Weapon_info[weapon_type];
1025 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
1026 	fix max_weapon_speed = wptr->speed[Difficulty_level];
1027 	if (max_weapon_speed < F1_0)
1028 		return 0;
1029 
1030 	//	Matter weapons:
1031 	//	At Rookie or Trainee, don't lead at all.
1032 	//	At higher skill levels, don't lead as well.  Accomplish this by screwing up max_weapon_speed.
1033 	if (wptr->matter != weapon_info::matter_flag::energy)
1034 	{
1035 		if (Difficulty_level <= 1)
1036 			return 0;
1037 		else
1038 			max_weapon_speed *= (NDL-Difficulty_level);
1039 	}
1040 
1041 	const fix projected_time = fixdiv(dist_to_player, max_weapon_speed);
1042 
1043 	compute_lead_component(&vms_vector::x, fire_vec, believed_player_pos, fire_point, velocity, projected_time);
1044 	compute_lead_component(&vms_vector::y, fire_vec, believed_player_pos, fire_point, velocity, projected_time);
1045 	compute_lead_component(&vms_vector::z, fire_vec, believed_player_pos, fire_point, velocity, projected_time);
1046 
1047 	vm_vec_normalize_quick(fire_vec);
1048 
1049 	Assert(vm_vec_dot(fire_vec, objp.orient.fvec) < 3*F1_0/2);
1050 
1051 	//	Make sure not firing at especially strange angle.  If so, try to correct.  If still bad, give up after one try.
1052 	if (vm_vec_dot(fire_vec, objp.orient.fvec) < F1_0/2) {
1053 		vm_vec_add2(fire_vec, vec_to_player);
1054 		vm_vec_scale(fire_vec, F1_0/2);
1055 		if (vm_vec_dot(fire_vec, objp.orient.fvec) < F1_0/2) {
1056 			return 0;
1057 		}
1058 	}
1059 
1060 	return 1;
1061 }
1062 #endif
1063 
1064 // --------------------------------------------------------------------------------------------------------------------
1065 //	Note: Parameter vec_to_player is only passed now because guns which aren't on the forward vector from the
1066 //	center of the robot will not fire right at the player.  We need to aim the guns at the player.  Barring that, we cheat.
1067 //	When this routine is complete, the parameter vec_to_player should not be necessary.
ai_fire_laser_at_player(const d_level_shared_segment_state & LevelSharedSegmentState,const vmobjptridx_t obj,const player_info & player_info,const vms_vector & fire_point,const int gun_num,const vms_vector & believed_player_pos)1068 static void ai_fire_laser_at_player(const d_level_shared_segment_state &LevelSharedSegmentState, const vmobjptridx_t obj, const player_info &player_info, const vms_vector &fire_point, const int gun_num
1069 #if defined(DXX_BUILD_DESCENT_II)
1070 									, const vms_vector &believed_player_pos
1071 #endif
1072 									)
1073 {
1074 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
1075 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
1076 	const auto powerup_flags = player_info.powerup_flags;
1077 	ai_local &ailp = obj->ctype.ai_info.ail;
1078 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1079 	auto &robptr = Robot_info[get_robot_id(obj)];
1080 	vms_vector	fire_vec;
1081 	vms_vector	bpp_diff;
1082 
1083 	Assert(robptr.attack_type == 0);	//	We should never be coming here for the green guy, as he has no laser!
1084 
1085 	if (cheats.robotfiringsuspended)
1086 		return;
1087 
1088 	if (obj->control_source == object::control_type::morph)
1089 		return;
1090 
1091 	//	If player is exploded, stop firing.
1092 	if (Player_dead_state == player_dead_state::exploded)
1093 		return;
1094 
1095 #if defined(DXX_BUILD_DESCENT_II)
1096 	//	If this robot is only awake because a camera woke it up, don't fire.
1097 	if (obj->ctype.ai_info.SUB_FLAGS & SUB_FLAGS_CAMERA_AWAKE)
1098 		return;
1099 
1100 	if (obj->ctype.ai_info.dying_start_time)
1101 		return;		//	No firing while in death roll.
1102 
1103 	//	Don't let the boss fire while in death roll.  Sorry, this is the easiest way to do this.
1104 	//	If you try to key the boss off obj->ctype.ai_info.dying_start_time, it will hose the endlevel stuff.
1105 	if (BossUniqueState.Boss_dying_start_time && Robot_info[get_robot_id(obj)].boss_flag)
1106 		return;
1107 #endif
1108 
1109 	//	If player is cloaked, maybe don't fire based on how long cloaked and randomness.
1110 	if (powerup_flags & PLAYER_FLAGS_CLOAKED) {
1111 		fix64	cloak_time = Ai_cloak_info[static_cast<imobjptridx_t::index_type>(obj) % MAX_AI_CLOAK_INFO].last_time;
1112 
1113 		if (GameTime64 - cloak_time > CLOAK_TIME_MAX/4)
1114 			if (d_rand() > fixdiv(GameTime64 - cloak_time, CLOAK_TIME_MAX)/2) {
1115 				set_next_fire_time(obj, ailp, robptr, gun_num);
1116 				return;
1117 			}
1118 	}
1119 
1120 #if defined(DXX_BUILD_DESCENT_I)
1121 	(void)LevelSharedSegmentState;
1122 	//	Set position to fire at based on difficulty level.
1123 	bpp_diff.x = Believed_player_pos.x + (d_rand()-16384) * (NDL-Difficulty_level-1) * 4;
1124 	bpp_diff.y = Believed_player_pos.y + (d_rand()-16384) * (NDL-Difficulty_level-1) * 4;
1125 	bpp_diff.z = Believed_player_pos.z + (d_rand()-16384) * (NDL-Difficulty_level-1) * 4;
1126 
1127 	//	Half the time fire at the player, half the time lead the player.
1128         if (d_rand() > 16384) {
1129 
1130 		vm_vec_normalized_dir_quick(fire_vec, bpp_diff, fire_point);
1131 
1132 	} else {
1133 		// If player is not moving, fire right at him!
1134 		//	Note: If the robot fires in the direction of its forward vector, this is bad because the weapon does not
1135 		//	come out from the center of the robot; it comes out from the side.  So it is common for the weapon to miss
1136 		//	its target.  Ideally, we want to point the guns at the player.  For now, just fire right at the player.
1137 		{
1138 			vm_vec_normalized_dir_quick(fire_vec, bpp_diff, fire_point);
1139 		// Player is moving.  Determine where the player will be at the end of the next frame if he doesn't change his
1140 		//	behavior.  Fire at exactly that point.  This isn't exactly what you want because it will probably take the laser
1141 		//	a different amount of time to get there, since it will probably be a different distance from the player.
1142 		//	So, that's why we write games, instead of guiding missiles...
1143 		}
1144 	}
1145 #elif defined(DXX_BUILD_DESCENT_II)
1146 	auto &Walls = LevelUniqueWallSubsystemState.Walls;
1147 	auto &vcwallptr = Walls.vcptr;
1148 	//	Handle problem of a robot firing through a wall because its gun tip is on the other
1149 	//	side of the wall than the robot's center.  For speed reasons, we normally only compute
1150 	//	the vector from the gun point to the player.  But we need to know whether the gun point
1151 	//	is separated from the robot's center by a wall.  If so, don't fire!
1152 	if (obj->ctype.ai_info.SUB_FLAGS & SUB_FLAGS_GUNSEG) {
1153 		//	Well, the gun point is in a different segment than the robot's center.
1154 		//	This is almost always ok, but it is not ok if something solid is in between.
1155 		//	See if these segments are connected, which should almost always be the case.
1156 		auto &Segments = LevelSharedSegmentState.get_segments();
1157 		const auto &&csegp = Segments.vcptridx(obj->segnum);
1158 		const auto &&gun_segnum = find_point_seg(LevelSharedSegmentState, fire_point, csegp);
1159 		const auto conn_side = find_connect_side(gun_segnum, csegp);
1160 		if (conn_side != side_none)
1161 		{
1162 			//	They are connected via conn_side in segment obj->segnum.
1163 			//	See if they are unobstructed.
1164 			if (!(WALL_IS_DOORWAY(GameBitmaps, Textures, vcwallptr, csegp, conn_side) & WALL_IS_DOORWAY_FLAG::fly))
1165 			{
1166 				//	Can't fly through, so don't let this bot fire through!
1167 				return;
1168 			}
1169 		} else {
1170 			//	Well, they are not directly connected, so use find_vector_intersection to see if they are unobstructed.
1171 			fvi_query	fq;
1172 			fvi_info		hit_data;
1173 			int			fate;
1174 
1175 			fq.startseg				= obj->segnum;
1176 			fq.p0						= &obj->pos;
1177 			fq.p1						= &fire_point;
1178 			fq.rad					= 0;
1179 			fq.thisobjnum			= obj;
1180 			fq.ignore_obj_list.first = nullptr;
1181 			fq.flags					= FQ_TRANSWALL;
1182 
1183 			fate = find_vector_intersection(fq, hit_data);
1184 			if (fate != HIT_NONE) {
1185 				Int3();		//	This bot's gun is poking through a wall, so don't fire.
1186 				move_towards_segment_center(LevelSharedSegmentState, obj);		//	And decrease chances it will happen again.
1187 				return;
1188 			}
1189 		}
1190 	}
1191 
1192 	//	Set position to fire at based on difficulty level and robot's aiming ability
1193 	fix aim = FIRE_K*F1_0 - (FIRE_K-1)*(robptr.aim << 8);	//	F1_0 in bitmaps.tbl = same as used to be.  Worst is 50% more error.
1194 
1195 	//	Robots aim more poorly during seismic disturbance.
1196 	if (const auto Seismic_tremor_magnitude = LevelUniqueSeismicState.Seismic_tremor_magnitude)
1197 	{
1198 		fix	temp;
1199 		temp = F1_0 - abs(Seismic_tremor_magnitude);
1200 		if (temp < F1_0/2)
1201 			temp = F1_0/2;
1202 
1203 		aim = fixmul(aim, temp);
1204 	}
1205 
1206 	//	Lead the player half the time.
1207 	//	Note that when leading the player, aim is perfect.  This is probably acceptable since leading is so hacked in.
1208 	//	Problem is all robots will lead equally badly.
1209 	if (d_rand() < 16384) {
1210 		if (lead_player(obj, fire_point, believed_player_pos, gun_num, fire_vec))		//	Stuff direction to fire at in fire_point.
1211 			goto player_led;
1212 	}
1213 
1214 	{
1215 	fix dot = 0;
1216 	unsigned count = 0;			//	Don't want to sit in this loop forever...
1217 	while ((count < 4) && (dot < F1_0/4)) {
1218 		bpp_diff.x = believed_player_pos.x + fixmul((d_rand()-16384) * (NDL-Difficulty_level-1) * 4, aim);
1219 		bpp_diff.y = believed_player_pos.y + fixmul((d_rand()-16384) * (NDL-Difficulty_level-1) * 4, aim);
1220 		bpp_diff.z = believed_player_pos.z + fixmul((d_rand()-16384) * (NDL-Difficulty_level-1) * 4, aim);
1221 
1222 		vm_vec_normalized_dir_quick(fire_vec, bpp_diff, fire_point);
1223 		dot = vm_vec_dot(obj->orient.fvec, fire_vec);
1224 		count++;
1225 	}
1226 	}
1227 player_led: ;
1228 #endif
1229 
1230 	const auto weapon_type = get_robot_weapon(robptr, gun_num);
1231 
1232 	Laser_create_new_easy( fire_vec, fire_point, obj, weapon_type, 1);
1233 
1234 	if (Game_mode & GM_MULTI)
1235 	{
1236 		ai_multi_send_robot_position(obj, -1);
1237 		multi_send_robot_fire(obj, obj->ctype.ai_info.CURRENT_GUN, fire_vec);
1238 	}
1239 
1240 	create_awareness_event(obj, player_awareness_type_t::PA_NEARBY_ROBOT_FIRED, LevelUniqueRobotAwarenessState);
1241 
1242 	set_next_fire_time(obj, ailp, robptr, gun_num);
1243 
1244 	//	If the boss fired, allow him to teleport very soon (right after firing, cool!), pending other factors.
1245 	if (robptr.boss_flag == BOSS_D1 || robptr.boss_flag == BOSS_SUPER)
1246 		BossUniqueState.Last_teleport_time -= LevelSharedBossState.Boss_teleport_interval / 2;
1247 }
1248 
1249 // --------------------------------------------------------------------------------------------------------------------
1250 //	vec_goal must be normalized, or close to it.
1251 //	if dot_based set, then speed is based on direction of movement relative to heading
move_towards_vector(object_base & objp,const vms_vector & vec_goal,int dot_based)1252 static void move_towards_vector(object_base &objp, const vms_vector &vec_goal, int dot_based)
1253 {
1254 	auto &velocity = objp.mtype.phys_info.velocity;
1255 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1256 	auto &robptr = Robot_info[get_robot_id(objp)];
1257 
1258 	//	Trying to move towards player.  If forward vector much different than velocity vector,
1259 	//	bash velocity vector twice as much towards player as usual.
1260 
1261 	const auto vel = vm_vec_normalized_quick(velocity);
1262 	fix dot = vm_vec_dot(vel, objp.orient.fvec);
1263 
1264 #if defined(DXX_BUILD_DESCENT_I)
1265 	dot_based = 1;
1266 #elif defined(DXX_BUILD_DESCENT_II)
1267 	if (robot_is_thief(robptr))
1268 		dot = (F1_0+dot)/2;
1269 #endif
1270 
1271 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
1272 	if (dot_based && (dot < 3*F1_0/4)) {
1273 		//	This funny code is supposed to slow down the robot and move his velocity towards his direction
1274 		//	more quickly than the general code
1275 		const fix frametime32 = FrameTime * 32;
1276 		move_toward_vector_component_assign(&vms_vector::x, vec_goal, frametime32, velocity);
1277 		move_toward_vector_component_assign(&vms_vector::y, vec_goal, frametime32, velocity);
1278 		move_toward_vector_component_assign(&vms_vector::z, vec_goal, frametime32, velocity);
1279 	} else {
1280 		const fix frametime64 = FrameTime * 64;
1281 		const fix difficulty_scale = Difficulty_level + 5;
1282 		move_toward_vector_component_add(&vms_vector::x, vec_goal, frametime64, difficulty_scale, velocity);
1283 		move_toward_vector_component_add(&vms_vector::y, vec_goal, frametime64, difficulty_scale, velocity);
1284 		move_toward_vector_component_add(&vms_vector::z, vec_goal, frametime64, difficulty_scale, velocity);
1285 	}
1286 
1287 	const auto speed = vm_vec_mag_quick(velocity);
1288 	fix max_speed = robptr.max_speed[Difficulty_level];
1289 
1290 	//	Green guy attacks twice as fast as he moves away.
1291 #if defined(DXX_BUILD_DESCENT_I)
1292 	if (robptr.attack_type == 1)
1293 #elif defined(DXX_BUILD_DESCENT_II)
1294 	if (robptr.attack_type == 1 || robot_is_thief(robptr) || robptr.kamikaze)
1295 #endif
1296 		max_speed *= 2;
1297 
1298 	if (speed > max_speed) {
1299 		velocity.x = (velocity.x * 3) / 4;
1300 		velocity.y = (velocity.y * 3) / 4;
1301 		velocity.z = (velocity.z * 3) / 4;
1302 	}
1303 }
1304 
1305 }
1306 
1307 // --------------------------------------------------------------------------------------------------------------------
1308 #if defined(DXX_BUILD_DESCENT_I)
1309 static
1310 #endif
move_towards_player(object & objp,const vms_vector & vec_to_player)1311 void move_towards_player(object &objp, const vms_vector &vec_to_player)
1312 //	vec_to_player must be normalized, or close to it.
1313 {
1314 	move_towards_vector(objp, vec_to_player, 1);
1315 }
1316 
1317 namespace {
1318 
1319 // --------------------------------------------------------------------------------------------------------------------
1320 //	I am ashamed of this: fast_flag == -1 means normal slide about.  fast_flag = 0 means no evasion.
move_around_player(const vmobjptridx_t objp,const player_flags powerup_flags,const vms_vector & vec_to_player,int fast_flag)1321 static void move_around_player(const vmobjptridx_t objp, const player_flags powerup_flags, const vms_vector &vec_to_player, int fast_flag)
1322 {
1323 	physics_info	*pptr = &objp->mtype.phys_info;
1324 	vms_vector		evade_vector;
1325 
1326 	if (fast_flag == 0)
1327 		return;
1328 
1329 	const unsigned dir = ((objp) ^ ((d_tick_count + 3*(objp)) >> 5)) & 3;
1330 	switch (dir) {
1331 		case 0:
1332 			evade_vector.x = vec_to_player.z;
1333 			evade_vector.y = vec_to_player.y;
1334 			evade_vector.z = -vec_to_player.x;
1335 			break;
1336 		case 1:
1337 			evade_vector.x = -vec_to_player.z;
1338 			evade_vector.y = vec_to_player.y;
1339 			evade_vector.z = vec_to_player.x;
1340 			break;
1341 		case 2:
1342 			evade_vector.x = -vec_to_player.y;
1343 			evade_vector.y = vec_to_player.x;
1344 			evade_vector.z = vec_to_player.z;
1345 			break;
1346 		case 3:
1347 			evade_vector.x = vec_to_player.y;
1348 			evade_vector.y = -vec_to_player.x;
1349 			evade_vector.z = vec_to_player.z;
1350 			break;
1351 		default:
1352 			throw std::runtime_error("move_around_player: bad case");
1353 	}
1354 	const auto frametime32 = FrameTime * 32;
1355 	evade_vector.x = fixmul(evade_vector.x, frametime32);
1356 	evade_vector.y = fixmul(evade_vector.y, frametime32);
1357 	evade_vector.z = fixmul(evade_vector.z, frametime32);
1358 
1359 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1360 	auto &robptr = Robot_info[get_robot_id(objp)];
1361 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
1362 	//	Note: -1 means normal circling about the player.  > 0 means fast evasion.
1363 	if (fast_flag > 0) {
1364 		fix	dot;
1365 
1366 		//	Only take evasive action if looking at player.
1367 		//	Evasion speed is scaled by percentage of shields left so wounded robots evade less effectively.
1368 
1369 		dot = vm_vec_dot(vec_to_player, objp->orient.fvec);
1370 		if (dot > robptr.field_of_view[Difficulty_level] && !(powerup_flags & PLAYER_FLAGS_CLOAKED)) {
1371 			fix	damage_scale;
1372 
1373 			if (!robptr.strength)
1374 				damage_scale = F1_0;
1375 			else
1376 				damage_scale = fixdiv(objp->shields, robptr.strength);
1377 			if (damage_scale > F1_0)
1378 				damage_scale = F1_0;		//	Just in case...
1379 			else if (damage_scale < 0)
1380 				damage_scale = 0;			//	Just in case...
1381 
1382 			vm_vec_scale(evade_vector, i2f(fast_flag) + damage_scale);
1383 		}
1384 	}
1385 
1386 	pptr->velocity.x += evade_vector.x;
1387 	pptr->velocity.y += evade_vector.y;
1388 	pptr->velocity.z += evade_vector.z;
1389 
1390 	const auto speed = vm_vec_mag_quick(pptr->velocity);
1391 	if (speed > robptr.max_speed[Difficulty_level]) {
1392 		pptr->velocity.x = (pptr->velocity.x*3)/4;
1393 		pptr->velocity.y = (pptr->velocity.y*3)/4;
1394 		pptr->velocity.z = (pptr->velocity.z*3)/4;
1395 	}
1396 }
1397 
1398 // --------------------------------------------------------------------------------------------------------------------
move_away_from_player(const vmobjptridx_t objp,const vms_vector & vec_to_player,int attack_type)1399 static void move_away_from_player(const vmobjptridx_t objp, const vms_vector &vec_to_player, int attack_type)
1400 {
1401 	physics_info	*pptr = &objp->mtype.phys_info;
1402 
1403 	const auto frametime = FrameTime;
1404 	pptr->velocity.x -= fixmul(vec_to_player.x, frametime*16);
1405 	pptr->velocity.y -= fixmul(vec_to_player.y, frametime*16);
1406 	pptr->velocity.z -= fixmul(vec_to_player.z, frametime*16);
1407 
1408 	if (attack_type) {
1409 		//	Get value in 0..3 to choose evasion direction.
1410 		const int objref = objp ^ ((d_tick_count + 3 * objp) >> 5);
1411 		vm_vec_scale_add2(pptr->velocity, (objref & 2) ? objp->orient.rvec : objp->orient.uvec, ((objref & 1) ? -frametime : frametime) << 5);
1412 	}
1413 
1414 
1415 	auto speed = vm_vec_mag_quick(pptr->velocity);
1416 
1417 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1418 	auto &robptr = Robot_info[get_robot_id(objp)];
1419 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
1420 	if (speed > robptr.max_speed[Difficulty_level])
1421 	{
1422 		pptr->velocity.x = (pptr->velocity.x*3)/4;
1423 		pptr->velocity.y = (pptr->velocity.y*3)/4;
1424 		pptr->velocity.z = (pptr->velocity.z*3)/4;
1425 	}
1426 
1427 }
1428 
1429 // --------------------------------------------------------------------------------------------------------------------
1430 //	Move towards, away_from or around player.
1431 //	Also deals with evasion.
1432 //	If the flag evade_only is set, then only allowed to evade, not allowed to move otherwise (must have mode == AIM_STILL).
ai_move_relative_to_player(const vmobjptridx_t objp,ai_local & ailp,const fix dist_to_player,const fix circle_distance,const int evade_only,const robot_to_player_visibility_state & player_visibility,const player_info & player_info)1433 static void ai_move_relative_to_player(const vmobjptridx_t objp, ai_local &ailp, const fix dist_to_player, const fix circle_distance, const int evade_only, const robot_to_player_visibility_state &player_visibility, const player_info &player_info)
1434 {
1435 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
1436 	auto &vec_to_player = player_visibility.vec_to_player;
1437 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1438 	auto &robptr = Robot_info[get_robot_id(objp)];
1439 
1440 	//	See if should take avoidance.
1441 
1442 	// New way, green guys don't evade:	if ((robptr.attack_type == 0) && (objp->ctype.ai_info.danger_laser_num != -1))
1443 	if (objp->ctype.ai_info.danger_laser_num != object_none) {
1444 		const auto &&dobjp = objp.absolute_sibling(objp->ctype.ai_info.danger_laser_num);
1445 
1446 		if ((dobjp->type == OBJ_WEAPON) && (dobjp->signature == objp->ctype.ai_info.danger_laser_signature)) {
1447 			vms_vector	laser_fvec;
1448 
1449 			const fix field_of_view = robptr.field_of_view[Difficulty_level];
1450 
1451 			auto vec_to_laser = vm_vec_sub(dobjp->pos, objp->pos);
1452 			auto dist_to_laser = vm_vec_normalize_quick(vec_to_laser);
1453 			const fix dot = vm_vec_dot(vec_to_laser, objp->orient.fvec);
1454 
1455 			if (dot > field_of_view || robot_is_companion(robptr))
1456 			{
1457 				fix			laser_robot_dot;
1458 
1459 				//	The laser is seen by the robot, see if it might hit the robot.
1460 				//	Get the laser's direction.  If it's a polyobj, it can be gotten cheaply from the orientation matrix.
1461 				if (dobjp->render_type == RT_POLYOBJ)
1462 					laser_fvec = dobjp->orient.fvec;
1463 				else {		//	Not a polyobj, get velocity and normalize.
1464 					laser_fvec = vm_vec_normalized_quick(dobjp->mtype.phys_info.velocity);	//dobjp->orient.fvec;
1465 				}
1466 				const auto laser_vec_to_robot = vm_vec_normalized_quick(vm_vec_sub(objp->pos, dobjp->pos));
1467 				laser_robot_dot = vm_vec_dot(laser_fvec, laser_vec_to_robot);
1468 
1469 				if ((laser_robot_dot > F1_0*7/8) && (dist_to_laser < F1_0*80)) {
1470 					int	evade_speed;
1471 
1472 					ai_evaded = 1;
1473 					evade_speed = robptr.evade_speed[Difficulty_level];
1474 					move_around_player(objp, player_info.powerup_flags, vec_to_player, evade_speed);
1475 				}
1476 			}
1477 			return;
1478 		}
1479 	}
1480 
1481 	//	If only allowed to do evade code, then done.
1482 	//	Hmm, perhaps brilliant insight.  If want claw-type guys to keep coming, don't return here after evasion.
1483 	if (!robptr.attack_type && !robot_is_thief(robptr) && evade_only)
1484 		return;
1485 
1486 	//	If we fall out of above, then no object to be avoided.
1487 	objp->ctype.ai_info.danger_laser_num = object_none;
1488 
1489 	//	Green guy selects move around/towards/away based on firing time, not distance.
1490 	if (robptr.attack_type == 1) {
1491 		if ((!ready_to_fire_weapon1(ailp, robptr.firing_wait[Difficulty_level]/4) && dist_to_player < F1_0*30) ||
1492 			Player_dead_state != player_dead_state::no)
1493 		{
1494 			//	1/4 of time, move around player, 3/4 of time, move away from player
1495 			if (d_rand() < 8192) {
1496 				move_around_player(objp, player_info.powerup_flags, vec_to_player, -1);
1497 			} else {
1498 				move_away_from_player(objp, vec_to_player, 1);
1499 			}
1500 		} else {
1501 			move_towards_player(objp, vec_to_player);
1502 		}
1503 	}
1504 	else if (robot_is_thief(robptr))
1505 	{
1506 		move_towards_player(objp, vec_to_player);
1507 	}
1508 	else {
1509 #if defined(DXX_BUILD_DESCENT_I)
1510 		if (dist_to_player < circle_distance)
1511 			move_away_from_player(objp, vec_to_player, 0);
1512 		else if (dist_to_player < circle_distance*2)
1513 			move_around_player(objp, player_info.powerup_flags, vec_to_player, -1);
1514 		else
1515 			move_towards_player(objp, vec_to_player);
1516 #elif defined(DXX_BUILD_DESCENT_II)
1517 		int	objval = ((objp) & 0x0f) ^ 0x0a;
1518 
1519 		//	Changes here by MK, 12/29/95.  Trying to get rid of endless circling around bots in a large room.
1520 		if (robptr.kamikaze) {
1521 			move_towards_player(objp, vec_to_player);
1522 		} else if (dist_to_player < circle_distance)
1523 			move_away_from_player(objp, vec_to_player, 0);
1524 		else if ((dist_to_player < (3+objval)*circle_distance/2) && !ready_to_fire_weapon1(ailp, -F1_0)) {
1525 			move_around_player(objp, player_info.powerup_flags, vec_to_player, -1);
1526 		} else {
1527 			if (ready_to_fire_weapon1(ailp, -(F1_0 + (objval << 12))) && player_is_visible(player_visibility.visibility))
1528 			{
1529 				//	Usually move away, but sometimes move around player.
1530 				if ((((GameTime64 >> 18) & 0x0f) ^ objval) > 4) {
1531 					move_away_from_player(objp, vec_to_player, 0);
1532 				} else {
1533 					move_around_player(objp, player_info.powerup_flags, vec_to_player, -1);
1534 				}
1535 			} else
1536 				move_towards_player(objp, vec_to_player);
1537 		}
1538 #endif
1539 	}
1540 }
1541 
1542 }
1543 
1544 }
1545 
1546 namespace dcx {
1547 
1548 // --------------------------------------------------------------------------------------------------------------------
1549 //	Compute a somewhat random, normalized vector.
make_random_vector(vms_vector & vec)1550 void make_random_vector(vms_vector &vec)
1551 {
1552 	vec.x = (d_rand() - 16384) | 1;	// make sure we don't create null vector
1553 	vec.y = d_rand() - 16384;
1554 	vec.z = d_rand() - 16384;
1555 	vm_vec_normalize_quick(vec);
1556 }
1557 
1558 }
1559 
1560 namespace dsx {
1561 namespace {
1562 
1563 //	-------------------------------------------------------------------------------------------------------------------
do_firing_stuff(object & obj,const player_flags powerup_flags,const robot_to_player_visibility_state & player_visibility)1564 static void do_firing_stuff(object &obj, const player_flags powerup_flags, const robot_to_player_visibility_state &player_visibility)
1565 {
1566 #if defined(DXX_BUILD_DESCENT_I)
1567 	if (player_is_visible(player_visibility.visibility))
1568 #elif defined(DXX_BUILD_DESCENT_II)
1569 	if (Dist_to_last_fired_upon_player_pos < FIRE_AT_NEARBY_PLAYER_THRESHOLD || player_is_visible(player_visibility.visibility))
1570 #endif
1571 	{
1572 		//	Now, if in robot's field of view, lock onto player
1573 		const fix dot = vm_vec_dot(obj.orient.fvec, player_visibility.vec_to_player);
1574 		if ((dot >= 7*F1_0/8) || (powerup_flags & PLAYER_FLAGS_CLOAKED)) {
1575 			ai_static *const aip = &obj.ctype.ai_info;
1576 			ai_local *const ailp = &obj.ctype.ai_info.ail;
1577 
1578 			switch (aip->GOAL_STATE) {
1579 				case AIS_NONE:
1580 				case AIS_REST:
1581 				case AIS_SRCH:
1582 				case AIS_LOCK:
1583 					aip->GOAL_STATE = AIS_FIRE;
1584 					if (ailp->player_awareness_type <= player_awareness_type_t::PA_NEARBY_ROBOT_FIRED) {
1585 						ailp->player_awareness_type = player_awareness_type_t::PA_NEARBY_ROBOT_FIRED;
1586 						ailp->player_awareness_time = PLAYER_AWARENESS_INITIAL_TIME;
1587 					}
1588 					break;
1589 			}
1590 		} else if (dot >= F1_0/2) {
1591 			ai_static *const aip = &obj.ctype.ai_info;
1592 			switch (aip->GOAL_STATE) {
1593 				case AIS_NONE:
1594 				case AIS_REST:
1595 				case AIS_SRCH:
1596 					aip->GOAL_STATE = AIS_LOCK;
1597 					break;
1598 			}
1599 		}
1600 	}
1601 }
1602 
1603 }
1604 
1605 // --------------------------------------------------------------------------------------------------------------------
1606 //	If a hiding robot gets bumped or hit, he decides to find another hiding place.
do_ai_robot_hit(const vmobjptridx_t objp,player_awareness_type_t type)1607 void do_ai_robot_hit(const vmobjptridx_t objp, player_awareness_type_t type)
1608 {
1609 	if (objp->control_source == object::control_type::ai)
1610 	{
1611 		if (type == player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION || type == player_awareness_type_t::PA_PLAYER_COLLISION)
1612 			switch (objp->ctype.ai_info.behavior) {
1613 #if defined(DXX_BUILD_DESCENT_I)
1614 				case ai_behavior::AIB_HIDE:
1615 					objp->ctype.ai_info.SUBMODE = AISM_GOHIDE;
1616 					break;
1617 				case ai_behavior::AIB_STILL:
1618                                         // objp->ctype.ai_info.ail.mode = ai_mode::AIM_CHASE_OBJECT; // NOTE: Should be triggered but causes unwanted movements with bosses. I leave this here for future reference.
1619 					break;
1620 				case ai_behavior::AIB_FOLLOW_PATH:
1621 #elif defined(DXX_BUILD_DESCENT_II)
1622 				case ai_behavior::AIB_STILL:
1623 				{
1624 					int	r;
1625 
1626 					r = d_rand();
1627 					//	1/8 time, charge player, 1/4 time create path, rest of time, do nothing
1628 					ai_local		*ailp = &objp->ctype.ai_info.ail;
1629 					if (r < 4096) {
1630 						create_path_to_believed_player_segment(objp, 10, create_path_safety_flag::safe);
1631 						objp->ctype.ai_info.behavior = ai_behavior::AIB_STATION;
1632 						objp->ctype.ai_info.hide_segment = objp->segnum;
1633 						ailp->mode = ai_mode::AIM_CHASE_OBJECT;
1634 					} else if (r < 4096+8192) {
1635 						create_n_segment_path(objp, d_rand()/8192 + 2, segment_none);
1636 						ailp->mode = ai_mode::AIM_FOLLOW_PATH;
1637 					}
1638 					break;
1639 				}
1640 				case ai_behavior::AIB_BEHIND:
1641 				case ai_behavior::AIB_SNIPE:
1642 				case ai_behavior::AIB_FOLLOW:
1643 #endif
1644 				case ai_behavior::AIB_NORMAL:
1645 				case ai_behavior::AIB_RUN_FROM:
1646 				case ai_behavior::AIB_STATION:
1647 					break;
1648 			}
1649 	}
1650 }
1651 
1652 namespace {
1653 
1654 // --------------------------------------------------------------------------------------------------------------------
1655 //	Note: This function could be optimized.  Surely player_is_visible_from_object would benefit from the
1656 //	information of a normalized vec_to_player.
1657 //	Return player visibility:
1658 //		0		not visible
1659 //		1		visible, but robot not looking at player (ie, on an unobstructed vector)
1660 //		2		visible and in robot's field of view
1661 //		-1		player is cloaked
1662 //	If the player is cloaked, set vec_to_player based on time player cloaked and last uncloaked position.
1663 //	Updates ailp->previous_visibility if player is not cloaked, in which case the previous visibility is left unchanged
1664 //	and is copied to player_visibility
1665 
compute_vis_and_vec(const vmobjptridx_t objp,const player_info & player_info,vms_vector & pos,ai_local & ailp,robot_to_player_visibility_state & player_visibility,const robot_info & robptr)1666 static void compute_vis_and_vec(const vmobjptridx_t objp, const player_info &player_info, vms_vector &pos, ai_local &ailp, robot_to_player_visibility_state &player_visibility, const robot_info &robptr)
1667 {
1668 	if (player_visibility.initialized)
1669 		return;
1670 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
1671 	const auto powerup_flags = player_info.powerup_flags;
1672 		if (powerup_flags & PLAYER_FLAGS_CLOAKED)
1673 		{
1674 			const unsigned cloak_index = (objp) % MAX_AI_CLOAK_INFO;
1675 			const fix delta_time = GameTime64 - Ai_cloak_info[cloak_index].last_time;
1676 			if (delta_time > F1_0*2) {
1677 				Ai_cloak_info[cloak_index].last_time = GameTime64;
1678 				vm_vec_scale_add2(Ai_cloak_info[cloak_index].last_position, make_random_vector(), 8 * delta_time);
1679 			}
1680 
1681 			const auto dist = vm_vec_normalized_dir_quick(player_visibility.vec_to_player, Ai_cloak_info[cloak_index].last_position, pos);
1682 			player_visibility.visibility = player_is_visible_from_object(objp, pos, robptr.field_of_view[Difficulty_level], player_visibility.vec_to_player);
1683 			if (ailp.next_misc_sound_time < GameTime64 && ready_to_fire_any_weapon(robptr, ailp, F1_0) && dist < F1_0 * 20)
1684 			{
1685 				ailp.next_misc_sound_time = GameTime64 + (d_rand() + F1_0) * (7 - Difficulty_level) / 1;
1686 				digi_link_sound_to_object(robptr.see_sound, objp, 0, Robot_sound_volume, sound_stack::allow_stacking);
1687 			}
1688 		} else {
1689 			//	Compute expensive stuff -- vec_to_player and player_visibility
1690 			vm_vec_normalized_dir_quick(player_visibility.vec_to_player, Believed_player_pos, pos);
1691 			if (player_visibility.vec_to_player.x == 0 && player_visibility.vec_to_player.y == 0 && player_visibility.vec_to_player.z == 0)
1692 			{
1693 				player_visibility.vec_to_player.x = F1_0;
1694 			}
1695 			player_visibility.visibility = player_is_visible_from_object(objp, pos, robptr.field_of_view[Difficulty_level], player_visibility.vec_to_player);
1696 
1697 			//	This horrible code added by MK in desperation on 12/13/94 to make robots wake up as soon as they
1698 			//	see you without killing frame rate.
1699 			{
1700 				ai_static	*aip = &objp->ctype.ai_info;
1701 			if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view && ailp.previous_visibility != player_visibility_state::visible_and_in_field_of_view)
1702 				if ((aip->GOAL_STATE == AIS_REST) || (aip->CURRENT_STATE == AIS_REST)) {
1703 					aip->GOAL_STATE = AIS_FIRE;
1704 					aip->CURRENT_STATE = AIS_FIRE;
1705 				}
1706 			}
1707 
1708 #if defined(DXX_BUILD_DESCENT_I)
1709 			if (Player_dead_state != player_dead_state::exploded)
1710 #endif
1711 			if (ailp.previous_visibility != player_visibility.visibility && player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view)
1712 			{
1713 				if (ailp.previous_visibility == player_visibility_state::no_line_of_sight)
1714 				{
1715 					if (ailp.time_player_seen + F1_0/2 < GameTime64)
1716 					{
1717 						digi_link_sound_to_object(robptr.see_sound, objp, 0, Robot_sound_volume, sound_stack::allow_stacking);
1718 						ailp.time_player_sound_attacked = GameTime64;
1719 						ailp.next_misc_sound_time = GameTime64 + F1_0 + d_rand()*4;
1720 					}
1721 				}
1722 				else if (ailp.time_player_sound_attacked + F1_0/4 < GameTime64)
1723 				{
1724 					digi_link_sound_to_object(robptr.attack_sound, objp, 0, Robot_sound_volume, sound_stack::allow_stacking);
1725 					ailp.time_player_sound_attacked = GameTime64;
1726 				}
1727 			}
1728 
1729 			if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view && ailp.next_misc_sound_time < GameTime64)
1730 			{
1731 				ailp.next_misc_sound_time = GameTime64 + (d_rand() + F1_0) * (7 - Difficulty_level) / 2;
1732 				digi_link_sound_to_object(robptr.attack_sound, objp, 0, Robot_sound_volume, sound_stack::allow_stacking);
1733 			}
1734 			ailp.previous_visibility = player_visibility.visibility;
1735 		}
1736 
1737 #if defined(DXX_BUILD_DESCENT_II)
1738 		//	@mk, 09/21/95: If player view is not obstructed and awareness is at least as high as a nearby collision,
1739 		//	act is if robot is looking at player.
1740 		if (ailp.player_awareness_type >= player_awareness_type_t::PA_NEARBY_ROBOT_FIRED)
1741 			if (player_visibility.visibility == player_visibility_state::visible_not_in_field_of_view)
1742 				player_visibility.visibility = player_visibility_state::visible_and_in_field_of_view;
1743 #endif
1744 
1745 		if (player_is_visible(player_visibility.visibility))
1746 		{
1747 			ailp.time_player_seen = GameTime64;
1748 		}
1749 	player_visibility.initialized = 1;
1750 }
1751 
1752 #if defined(DXX_BUILD_DESCENT_II)
compute_buddy_vis_vec(const vmobjptridx_t buddy_obj,const vms_vector & buddy_pos,robot_to_player_visibility_state & player_visibility,const robot_info & robptr)1753 static void compute_buddy_vis_vec(const vmobjptridx_t buddy_obj, const vms_vector &buddy_pos, robot_to_player_visibility_state &player_visibility, const robot_info &robptr)
1754 {
1755 	if (player_visibility.initialized)
1756 		return;
1757 	auto &BuddyState = LevelUniqueObjectState.BuddyState;
1758 	auto &Objects = LevelUniqueObjectState.Objects;
1759 	auto &plr = get_player_controlling_guidebot(BuddyState, Players);
1760 	if (plr.objnum == object_none)
1761 	{
1762 		player_visibility.vec_to_player = {};
1763 		player_visibility.visibility = player_visibility_state::no_line_of_sight;
1764 		player_visibility.initialized = 1;
1765 		return;
1766 	}
1767 	auto &plrobj = *Objects.vcptr(plr.objnum);
1768 	/* Buddy ignores cloaking */
1769 	vm_vec_normalized_dir_quick(player_visibility.vec_to_player, plrobj.pos, buddy_pos);
1770 	if (player_visibility.vec_to_player.x == 0 && player_visibility.vec_to_player.y == 0 && player_visibility.vec_to_player.z == 0)
1771 		player_visibility.vec_to_player.x = F1_0;
1772 
1773 	fvi_query fq;
1774 	fq.p0 = &buddy_pos;
1775 	fq.startseg = buddy_obj->segnum;
1776 	fq.p1 = &plrobj.pos;
1777 	fq.rad = F1_0/4;
1778 	fq.thisobjnum = buddy_obj;
1779 	fq.ignore_obj_list.first = nullptr;
1780 	fq.flags = FQ_TRANSWALL;
1781 	fvi_info hit_data;
1782 	const auto hit_type = find_vector_intersection(fq, hit_data);
1783 
1784 	auto &ailp = buddy_obj->ctype.ai_info.ail;
1785 	player_visibility.visibility = (hit_type == HIT_NONE)
1786 		? ((ailp.time_player_seen = GameTime64, vm_vec_dot(player_visibility.vec_to_player, buddy_obj->orient.fvec) > robptr.field_of_view[GameUniqueState.Difficulty_level])
1787 		   ? player_visibility_state::visible_and_in_field_of_view
1788 		   : player_visibility_state::visible_not_in_field_of_view
1789 		)
1790 		: player_visibility_state::no_line_of_sight;
1791 	ailp.previous_visibility = player_visibility.visibility;
1792 	player_visibility.initialized = 1;
1793 }
1794 #endif
1795 
1796 }
1797 
1798 // --------------------------------------------------------------------------------------------------------------------
1799 //	Move object one object radii from current position towards segment center.
1800 //	If segment center is nearer than 2 radii, move it to center.
move_towards_segment_center(const d_level_shared_segment_state & LevelSharedSegmentState,object_base & objp)1801 void move_towards_segment_center(const d_level_shared_segment_state &LevelSharedSegmentState, object_base &objp)
1802 {
1803 /* ZICO's change of 20081103:
1804    Make move to segment center smoother by using move_towards vector.
1805    Bot's should not jump around and maybe even intersect with each other!
1806    In case it breaks something what I do not see, yet, old code is still there. */
1807 	const auto segnum = objp.segnum;
1808 	vms_vector	vec_to_center;
1809 
1810 	auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
1811 	auto &Vertices = LevelSharedVertexState.get_vertices();
1812 	auto &SSegments = LevelSharedSegmentState.get_segments();
1813 	const auto &&segment_center = compute_segment_center(Vertices.vcptr, SSegments.vcptr(segnum));
1814 	vm_vec_normalized_dir_quick(vec_to_center, segment_center, objp.pos);
1815 	move_towards_vector(objp, vec_to_center, 1);
1816 }
1817 
1818 //	-----------------------------------------------------------------------------------------------------------
1819 //	Return true if door can be flown through by a suitable type robot.
1820 //	Brains, avoid robots, companions can open doors.
1821 //	objp == NULL means treat as buddy.
ai_door_is_openable(const vmobjptr_t objp,const player_flags powerup_flags,const shared_segment & segp,const unsigned sidenum)1822 int ai_door_is_openable(
1823 	const vmobjptr_t objp,
1824 #if defined(DXX_BUILD_DESCENT_II)
1825 	const player_flags powerup_flags,
1826 #endif
1827 	const shared_segment &segp, const unsigned sidenum)
1828 {
1829 	if (!IS_CHILD(segp.children[sidenum]))
1830 		return 0;		//trap -2 (exit side)
1831 
1832 	const auto wall_num = segp.sides[sidenum].wall_num;
1833 
1834 	if (wall_num == wall_none)		//if there's no door at all...
1835 		return 0;				//..then say it can't be opened
1836 
1837 	auto &Walls = LevelUniqueWallSubsystemState.Walls;
1838 	auto &vcwallptr = Walls.vcptr;
1839 	auto &wall = *vcwallptr(wall_num);
1840 	//	The mighty console object can open all doors (for purposes of determining paths).
1841 	if (objp == ConsoleObject) {
1842 		const auto wt = wall.type;
1843 		if (wt == WALL_DOOR)
1844 		{
1845 			static_assert(WALL_DOOR != 0, "WALL_DOOR must be nonzero for this shortcut to work properly.");
1846 			return wt;
1847 		}
1848 	}
1849 
1850 #if defined(DXX_BUILD_DESCENT_I)
1851 	if ((get_robot_id(objp) == ROBOT_BRAIN) || (objp->ctype.ai_info.behavior == ai_behavior::AIB_RUN_FROM))
1852 	{
1853 
1854 		if (wall_num != wall_none)
1855 		{
1856 			const auto wt = wall.type;
1857 			if (wt == WALL_DOOR && wall.keys == wall_key::none && !(wall.flags & wall_flag::door_locked))
1858 			{
1859 				static_assert(WALL_DOOR != 0, "WALL_DOOR must be nonzero for this shortcut to work properly.");
1860 				return wt;
1861 			}
1862 		}
1863 	}
1864 #elif defined(DXX_BUILD_DESCENT_II)
1865 	auto &WallAnims = GameSharedState.WallAnims;
1866 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1867 	if (Robot_info[get_robot_id(objp)].companion)
1868 	{
1869 		const auto wt = wall.type;
1870 		if (wall.flags & wall_flag::buddy_proof) {
1871 			if (wt == WALL_DOOR && wall.state == wall_state::closed)
1872 				return 0;
1873 			else if (wt == WALL_CLOSED)
1874 				return 0;
1875 			else if (wt == WALL_ILLUSION && !(wall.flags & wall_flag::illusion_off))
1876 				return 0;
1877 		}
1878 		switch (const auto wall_keys = wall.keys)
1879 		{
1880 			case wall_key::blue:
1881 			case wall_key::gold:
1882 			case wall_key::red:
1883 				{
1884 					return powerup_flags & static_cast<PLAYER_FLAG>(wall_keys);
1885 				}
1886 			default:
1887 				break;
1888 		}
1889 
1890 		if (wt != WALL_DOOR && wt != WALL_CLOSED)
1891 			return 1;
1892 
1893 		//	If Buddy is returning to player, don't let him think he can get through triggered doors.
1894 		//	It's only valid to think that if the player is going to get him through.  But if he's
1895 		//	going to the player, the player is probably on the opposite side.
1896 		const ai_mode ailp_mode = objp->ctype.ai_info.ail.mode;
1897 
1898 		// -- if (Buddy_got_stuck) {
1899 		if (ailp_mode == ai_mode::AIM_GOTO_PLAYER) {
1900 			if (wt == WALL_BLASTABLE && !(wall.flags & wall_flag::blasted))
1901 				return 0;
1902 			if (wt == WALL_CLOSED)
1903 				return 0;
1904 			if (wt == WALL_DOOR) {
1905 				if ((wall.flags & wall_flag::door_locked) && (wall.state == wall_state::closed))
1906 					return 0;
1907 			}
1908 		}
1909 		// -- }
1910 
1911 		if (ailp_mode != ai_mode::AIM_GOTO_PLAYER && wall.controlling_trigger != trigger_none)
1912 		{
1913 			const auto clip_num = wall.clip_num;
1914 			if (clip_num == -1)
1915 				return clip_num;
1916 			else if (WallAnims[clip_num].flags & WCF_HIDDEN) {
1917 				static_assert(static_cast<unsigned>(wall_state::closed) == 0, "wall_state::closed must be zero for this shortcut to work properly.");
1918 				return underlying_value(wall.state);
1919 			} else
1920 				return 1;
1921 		}
1922 
1923 		if (wt == WALL_DOOR)  {
1924 				const auto clip_num = wall.clip_num;
1925 
1926 				if (clip_num == -1)
1927 					return clip_num;
1928 				//	Buddy allowed to go through secret doors to get to player.
1929 				else if ((ailp_mode != ai_mode::AIM_GOTO_PLAYER) && (WallAnims[clip_num].flags & WCF_HIDDEN)) {
1930 					static_assert(static_cast<unsigned>(wall_state::closed) == 0, "wall_state::closed must be zero for this shortcut to work properly.");
1931 					return underlying_value(wall.state);
1932 				} else
1933 					return 1;
1934 		}
1935 	} else if ((get_robot_id(objp) == ROBOT_BRAIN) || (objp->ctype.ai_info.behavior == ai_behavior::AIB_RUN_FROM) || (objp->ctype.ai_info.behavior == ai_behavior::AIB_SNIPE)) {
1936 		if (wall_num != wall_none)
1937 		{
1938 			const auto wt = wall.type;
1939 			if (wt == WALL_DOOR && (wall.keys == wall_key::none) && !(wall.flags & wall_flag::door_locked))
1940 			{
1941 				static_assert(WALL_DOOR != 0, "WALL_DOOR must be nonzero for this shortcut to work properly.");
1942 				return wt;
1943 			}
1944 			else if (wall.keys != wall_key::none) {	//	Allow bots to open doors to which player has keys.
1945 				return powerup_flags & static_cast<PLAYER_FLAG>(wall.keys);
1946 			}
1947 		}
1948 	}
1949 #endif
1950 	return 0;
1951 }
1952 
1953 namespace {
1954 
1955 //	-----------------------------------------------------------------------------------------------------------
1956 //	Return side of openable door in segment, if any.  If none, return side_none.
openable_doors_in_segment(fvcwallptr & vcwallptr,const shared_segment & segp)1957 static unsigned openable_doors_in_segment(fvcwallptr &vcwallptr, const shared_segment &segp)
1958 {
1959 #if defined(DXX_BUILD_DESCENT_II)
1960 	auto &WallAnims = GameSharedState.WallAnims;
1961 #endif
1962 	for (const auto &&[idx, value] : enumerate(segp.sides))
1963 	{
1964 		const auto wall_num = value.wall_num;
1965 		if (wall_num != wall_none)
1966 		{
1967 			auto &w = *vcwallptr(wall_num);
1968 			if (w.type != WALL_DOOR)
1969 				continue;
1970 			if (w.keys != wall_key::none)
1971 				continue;
1972 			if (w.state != wall_state::closed)
1973 				continue;
1974 			if (w.flags & wall_flag::door_locked)
1975 				continue;
1976 #if defined(DXX_BUILD_DESCENT_II)
1977 			if (WallAnims[w.clip_num].flags & WCF_HIDDEN)
1978 				continue;
1979 #endif
1980 			return idx;
1981 		}
1982 	}
1983 	return side_none;
1984 }
1985 
1986 // --------------------------------------------------------------------------------------------------------------------
1987 //	Return true if placing an object of size size at pos *pos intersects a (player or robot or control center) in segment *segp.
check_object_object_intersection(const vms_vector & pos,fix size,const unique_segment & segp)1988 static int check_object_object_intersection(const vms_vector &pos, fix size, const unique_segment &segp)
1989 {
1990 	auto &Objects = LevelUniqueObjectState.Objects;
1991 	auto &vcobjptridx = Objects.vcptridx;
1992 	//	If this would intersect with another object (only check those in this segment), then try to move.
1993 	range_for (const object_base &curobj, objects_in(segp, vcobjptridx, vcsegptr))
1994 	{
1995 		if (curobj.type == OBJ_PLAYER || curobj.type == OBJ_ROBOT || curobj.type == OBJ_CNTRLCEN)
1996 		{
1997 			if (vm_vec_dist_quick(pos, curobj.pos) < size + curobj.size)
1998 				return 1;
1999 		}
2000 	}
2001 	return 0;
2002 }
2003 
2004 // --------------------------------------------------------------------------------------------------------------------
2005 //	Return objnum if object created, else return -1.
2006 //	If pos == NULL, pick random spot in segment.
create_gated_robot(const d_vclip_array & Vclip,fvcobjptr & vcobjptr,const vmsegptridx_t segp,const unsigned object_id,const vms_vector * const pos)2007 static imobjptridx_t create_gated_robot(const d_vclip_array &Vclip, fvcobjptr &vcobjptr, const vmsegptridx_t segp, const unsigned object_id, const vms_vector *const pos)
2008 {
2009 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
2010 	auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
2011 	auto &LevelUniqueMorphObjectState = LevelUniqueObjectState.MorphObjectState;
2012 	auto &Vertices = LevelSharedVertexState.get_vertices();
2013 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
2014 	const auto Gate_interval = GameUniqueState.Boss_gate_interval;
2015 #if defined(DXX_BUILD_DESCENT_I)
2016 	const unsigned maximum_gated_robots = 2*Difficulty_level + 3;
2017 #elif defined(DXX_BUILD_DESCENT_II)
2018 	if (GameTime64 - BossUniqueState.Last_gate_time < Gate_interval)
2019 		return object_none;
2020 	const unsigned maximum_gated_robots = 2*Difficulty_level + 6;
2021 #endif
2022 
2023 	unsigned count = 0;
2024 	range_for (const auto &&objp, vcobjptr)
2025 	{
2026 		auto &obj = *objp;
2027 		if (obj.type == OBJ_ROBOT)
2028 			if (obj.matcen_creator == BOSS_GATE_MATCEN_NUM)
2029 				count++;
2030 	}
2031 
2032 	if (count > maximum_gated_robots)
2033 	{
2034 		BossUniqueState.Last_gate_time = GameTime64 - 3*Gate_interval/4;
2035 		return object_none;
2036 	}
2037 
2038 	auto &vcvertptr = Vertices.vcptr;
2039 	const auto object_pos = pos ? *pos : pick_random_point_in_seg(vcvertptr, segp, std::minstd_rand(d_rand()));
2040 
2041 	//	See if legal to place object here.  If not, move about in segment and try again.
2042 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
2043 	auto &robptr = Robot_info[object_id];
2044 	auto &Polygon_models = LevelSharedPolygonModelState.Polygon_models;
2045 	const fix objsize = Polygon_models[robptr.model_num].rad;
2046 	if (check_object_object_intersection(object_pos, objsize, segp)) {
2047 		BossUniqueState.Last_gate_time = GameTime64 - 3*Gate_interval/4;
2048 		return object_none;
2049 	}
2050 
2051 #if defined(DXX_BUILD_DESCENT_I)
2052 	const ai_behavior default_behavior = (object_id == 10) //	This is a toaster guy!
2053 	? ai_behavior::AIB_RUN_FROM
2054 	: ai_behavior::AIB_NORMAL;
2055 #elif defined(DXX_BUILD_DESCENT_II)
2056 	const ai_behavior default_behavior = robptr.behavior;
2057 #endif
2058 	auto objp = robot_create(object_id, segp, object_pos, &vmd_identity_matrix, objsize, default_behavior);
2059 
2060 	if ( objp == object_none ) {
2061 		BossUniqueState.Last_gate_time = GameTime64 - 3*Gate_interval/4;
2062 		return object_none;
2063 	}
2064 
2065 	//Set polygon-object-specific data
2066 
2067 	objp->rtype.pobj_info.model_num = robptr.model_num;
2068 	objp->rtype.pobj_info.subobj_flags = 0;
2069 
2070 	//set Physics info
2071 
2072 	objp->mtype.phys_info.mass = robptr.mass;
2073 	objp->mtype.phys_info.drag = robptr.drag;
2074 
2075 	objp->mtype.phys_info.flags |= (PF_LEVELLING);
2076 
2077 	objp->shields = robptr.strength;
2078 	objp->matcen_creator = BOSS_GATE_MATCEN_NUM;	//	flag this robot as having been created by the boss.
2079 
2080 #if defined(DXX_BUILD_DESCENT_II)
2081 	objp->lifeleft = F1_0*30;	//	Gated in robots only live 30 seconds.
2082 #endif
2083 
2084 	object_create_explosion(segp, object_pos, i2f(10), VCLIP_MORPHING_ROBOT );
2085 	digi_link_sound_to_pos( Vclip[VCLIP_MORPHING_ROBOT].sound_num, segp, 0, object_pos, 0 , F1_0);
2086 	morph_start(LevelUniqueMorphObjectState, LevelSharedPolygonModelState, objp);
2087 
2088 	BossUniqueState.Last_gate_time = GameTime64;
2089 	++LevelUniqueObjectState.accumulated_robots;
2090 	++GameUniqueState.accumulated_robots;
2091 
2092 	return objp;
2093 }
2094 
2095 }
2096 
2097 // --------------------------------------------------------------------------------------------------------------------
2098 //	Make object objp gate in a robot.
2099 //	The process of him bringing in a robot takes one second.
2100 //	Then a robot appears somewhere near the player.
2101 //	Return objnum if robot successfully created, else return -1
gate_in_robot(const unsigned type,const vmsegptridx_t segnum)2102 imobjptridx_t gate_in_robot(const unsigned type, const vmsegptridx_t segnum)
2103 {
2104 	auto &Objects = LevelUniqueObjectState.Objects;
2105 	auto &vcobjptr = Objects.vcptr;
2106 	return create_gated_robot(Vclip, vcobjptr, segnum, type, nullptr);
2107 }
2108 
gate_in_robot(fvmsegptridx & vmsegptridx,int type)2109 static imobjptridx_t gate_in_robot(fvmsegptridx &vmsegptridx, int type)
2110 {
2111 	auto &Boss_gate_segs = LevelSharedBossState.Gate_segs;
2112 	auto segnum = Boss_gate_segs[(d_rand() * Boss_gate_segs.size()) >> 15];
2113 	return gate_in_robot(type, vmsegptridx(segnum));
2114 }
2115 
2116 }
2117 
2118 namespace dcx {
2119 
2120 namespace {
2121 
boss_intersects_wall(fvcvertptr & vcvertptr,const object_base & boss_objp,const vcsegptridx_t segp)2122 static const shared_segment *boss_intersects_wall(fvcvertptr &vcvertptr, const object_base &boss_objp, const vcsegptridx_t segp)
2123 {
2124 	const auto size = boss_objp.size;
2125 	const auto &&segcenter = compute_segment_center(vcvertptr, segp);
2126 	auto pos = segcenter;
2127 	for (uint_fast32_t posnum = 0;;)
2128 	{
2129 		const auto seg = sphere_intersects_wall(vcsegptridx, vcvertptr, pos, segp, size).seg;
2130 		if (!seg)
2131 			return seg;
2132 		if (posnum == segp->verts.size())
2133 			return seg;
2134 		auto &vertex_pos = *vcvertptr(segp->verts[posnum ++]);
2135 		vm_vec_avg(pos, vertex_pos, segcenter);
2136 	}
2137 }
2138 
2139 // ----------------------------------------------------------------------------------
pae_aux(const vcsegptridx_t segnum,const player_awareness_type_t type,awareness_t & New_awareness,const unsigned allowed_recursions_remaining)2140 static void pae_aux(const vcsegptridx_t segnum, const player_awareness_type_t type, awareness_t &New_awareness, const unsigned allowed_recursions_remaining)
2141 {
2142 	if (New_awareness[segnum] < type)
2143 		New_awareness[segnum] = type;
2144 	const auto sub_allowed_recursions_remaining = allowed_recursions_remaining - 1;
2145 	if (!sub_allowed_recursions_remaining)
2146 		return;
2147 	// Process children.
2148 	const auto subtype = (type == player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION)
2149 		? player_awareness_type_t::PA_PLAYER_COLLISION
2150 		: type;
2151 	for (const auto j : segnum->shared_segment::children)
2152 		if (IS_CHILD(j))
2153 			pae_aux(segnum.absolute_sibling(j), subtype, New_awareness, sub_allowed_recursions_remaining);
2154 }
2155 
2156 }
2157 
2158 }
2159 
2160 namespace dsx {
2161 
2162 #if defined(DXX_BUILD_DESCENT_II)
2163 // --------------------------------------------------------------------------------------------------------------------
2164 //	Create a Buddy bot.
2165 //	This automatically happens when you bring up the Buddy menu in a debug version.
2166 //	It is available as a cheat in a non-debug (release) version.
create_buddy_bot(void)2167 void create_buddy_bot(void)
2168 {
2169 	auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
2170 	auto &Vertices = LevelSharedVertexState.get_vertices();
2171 	for (auto &&[idx, ri] : enumerate(partial_const_range(LevelSharedRobotInfoState.Robot_info, LevelSharedRobotInfoState.N_robot_types)))
2172 	{
2173 		if (!ri.companion)
2174 			continue;
2175 		const auto &&segp = vmsegptridx(ConsoleObject->segnum);
2176 		const auto &&object_pos = compute_segment_center(Vertices.vcptr, segp);
2177 		create_morph_robot(segp, object_pos, idx);
2178 		break;
2179 	}
2180 }
2181 #endif
2182 
2183 namespace {
2184 
2185 // --------------------------------------------------------------------------------------------------------------------
2186 //	Create list of segments boss is allowed to teleport to at imsegptr.
2187 //	Set *num_segs.
2188 //	Boss is allowed to teleport to segments he fits in (calls object_intersects_wall) and
2189 //	he can reach from his initial position (calls find_connected_distance).
2190 //	If size_check is set, then only add segment if boss can fit in it, else any segment is legal.
2191 //	one_wall_hack added by MK, 10/13/95: A mega-hack!  Set to !0 to ignore the
init_boss_segments(const segment_array & segments,const object & boss_objp,d_level_shared_boss_state::special_segment_array_t & a,const int size_check,int one_wall_hack)2192 static void init_boss_segments(const segment_array &segments, const object &boss_objp, d_level_shared_boss_state::special_segment_array_t &a, const int size_check, int one_wall_hack)
2193 {
2194 	auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
2195 	auto &Vertices = LevelSharedVertexState.get_vertices();
2196 	constexpr unsigned QUEUE_SIZE = 256;
2197 	auto &vcsegptridx = segments.vcptridx;
2198 	auto &vmsegptr = segments.vmptr;
2199 #if defined(DXX_BUILD_DESCENT_I)
2200 	one_wall_hack = 0;
2201 #endif
2202 
2203 	a.clear();
2204 #if DXX_USE_EDITOR
2205 	Selected_segs.clear();
2206 #endif
2207 
2208 	auto &Walls = LevelUniqueWallSubsystemState.Walls;
2209 	auto &vcwallptr = Walls.vcptr;
2210 	{
2211 		int			head, tail;
2212 		std::array<segnum_t, QUEUE_SIZE> seg_queue;
2213 
2214 		const auto original_boss_seg = boss_objp.segnum;
2215 		head = 0;
2216 		tail = 0;
2217 		seg_queue[head++] = original_boss_seg;
2218 		auto &vcvertptr = Vertices.vcptr;
2219 
2220 		if (!size_check || !boss_intersects_wall(vcvertptr, boss_objp, vcsegptridx(original_boss_seg)))
2221 		{
2222 			a.emplace_back(original_boss_seg);
2223 #if DXX_USE_EDITOR
2224 			Selected_segs.emplace_back(original_boss_seg);
2225 #endif
2226 		}
2227 
2228 		visited_segment_bitarray_t visited;
2229 
2230 		while (tail != head) {
2231 			const cscusegment segp = *vmsegptr(seg_queue[tail++]);
2232 
2233 			tail &= QUEUE_SIZE-1;
2234 
2235 			for (const auto &&[sidenum, csegnum] : enumerate(segp.s.children))
2236 			{
2237 				const auto w = WALL_IS_DOORWAY(GameBitmaps, Textures, vcwallptr, segp, sidenum);
2238 				if ((w & WALL_IS_DOORWAY_FLAG::fly) || one_wall_hack)
2239 				{
2240 #if defined(DXX_BUILD_DESCENT_II)
2241 					//	If we get here and w == WID_WALL, then we want to process through this wall, else not.
2242 					if (IS_CHILD(csegnum)) {
2243 						if (one_wall_hack)
2244 							one_wall_hack--;
2245 					} else
2246 						continue;
2247 #endif
2248 
2249 					if (auto &&v = visited[csegnum])
2250 					{
2251 					}
2252 					else
2253 					{
2254 						v = true;
2255 						seg_queue[head++] = csegnum;
2256 						head &= QUEUE_SIZE-1;
2257 						if (head > tail) {
2258 							if (head == tail + QUEUE_SIZE-1)
2259 								Int3();	//	queue overflow.  Make it bigger!
2260 						} else
2261 							if (head+QUEUE_SIZE == tail + QUEUE_SIZE-1)
2262 								Int3();	//	queue overflow.  Make it bigger!
2263 
2264 						if (!size_check || !boss_intersects_wall(vcvertptr, boss_objp, vcsegptridx(csegnum))) {
2265 							a.emplace_back(csegnum);
2266 #if DXX_USE_EDITOR
2267 							Selected_segs.emplace_back(csegnum);
2268 							#endif
2269 							if (a.size() >= a.max_size())
2270 							{
2271 								tail = head;
2272 								break;
2273 							}
2274 						}
2275 					}
2276 				}
2277 			}
2278 		}
2279 		// Last resort - add original seg even if boss doesn't fit in it
2280 		if (a.empty())
2281 			a.emplace_back(original_boss_seg);
2282 	}
2283 }
2284 
2285 // --------------------------------------------------------------------------------------------------------------------
teleport_boss(const d_vclip_array & Vclip,fvmsegptridx & vmsegptridx,const vmobjptridx_t objp,const vms_vector & target_pos)2286 static void teleport_boss(const d_vclip_array &Vclip, fvmsegptridx &vmsegptridx, const vmobjptridx_t objp, const vms_vector &target_pos)
2287 {
2288 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
2289 	auto &Boss_teleport_segs = LevelSharedBossState.Teleport_segs;
2290 	auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
2291 	segnum_t			rand_segnum;
2292 	int			rand_index;
2293 	assert(!Boss_teleport_segs.empty());
2294 	auto &Objects = LevelUniqueObjectState.Objects;
2295 	auto &vmobjptr = Objects.vmptr;
2296 
2297 	//	Pick a random segment from the list of boss-teleportable-to segments.
2298 	rand_index = (d_rand() * Boss_teleport_segs.size()) >> 15;
2299 	rand_segnum = Boss_teleport_segs[rand_index];
2300 	Assert(rand_segnum <= Highest_segment_index);
2301 
2302 	if (Game_mode & GM_MULTI)
2303 		multi_send_boss_teleport(objp, rand_segnum);
2304 
2305 	const auto &&rand_segp = vmsegptridx(rand_segnum);
2306 	auto &Vertices = LevelSharedVertexState.get_vertices();
2307 	auto &vcvertptr = Vertices.vcptr;
2308 	compute_segment_center(vcvertptr, objp->pos, rand_segp);
2309 	obj_relink(vmobjptr, vmsegptr, objp, rand_segp);
2310 
2311 	BossUniqueState.Last_teleport_time = GameTime64;
2312 
2313 	//	make boss point right at player
2314 	const auto boss_dir = vm_vec_sub(target_pos, objp->pos);
2315 	vm_vector_2_matrix(objp->orient, boss_dir, nullptr, nullptr);
2316 
2317 	digi_link_sound_to_pos(Vclip[VCLIP_MORPHING_ROBOT].sound_num, rand_segp, 0, objp->pos, 0, F1_0);
2318 	digi_kill_sound_linked_to_object( objp);
2319 
2320 	//	After a teleport, boss can fire right away.
2321 	ai_local		*ailp = &objp->ctype.ai_info.ail;
2322 	ailp->next_fire = 0;
2323 #if defined(DXX_BUILD_DESCENT_II)
2324 	ailp->next_fire2 = 0;
2325 #endif
2326 	boss_link_see_sound(objp);
2327 
2328 }
2329 
2330 }
2331 
2332 //	----------------------------------------------------------------------
start_boss_death_sequence(object & objp)2333 void start_boss_death_sequence(object &objp)
2334 {
2335 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
2336 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
2337 	auto &robptr = Robot_info[get_robot_id(objp)];
2338 	if (robptr.boss_flag)
2339 	{
2340 		BossUniqueState.Boss_dying = 1;
2341 		BossUniqueState.Boss_dying_start_time = GameTime64;
2342 	}
2343 
2344 }
2345 
2346 //	----------------------------------------------------------------------------------------------------------
2347 #if defined(DXX_BUILD_DESCENT_I)
2348 namespace {
do_boss_dying_frame(const vmobjptridx_t objp)2349 static void do_boss_dying_frame(const vmobjptridx_t objp)
2350 {
2351 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
2352 	const auto time_boss_dying = GameTime64 - BossUniqueState.Boss_dying_start_time;
2353 	{
2354 		auto &rotvel = objp->mtype.phys_info.rotvel;
2355 		rotvel.x = time_boss_dying / 9;
2356 		rotvel.y = time_boss_dying / 5;
2357 		rotvel.z = time_boss_dying / 7;
2358 	}
2359 
2360 	if (BossUniqueState.Boss_dying_start_time + BOSS_DEATH_DURATION - BOSS_DEATH_SOUND_DURATION < GameTime64) {
2361 		if (!BossUniqueState.Boss_dying_sound_playing)
2362 		{
2363 			BossUniqueState.Boss_dying_sound_playing = 1;
2364 			digi_link_sound_to_object2(SOUND_BOSS_SHARE_DIE, objp, 0, F1_0*4, sound_stack::allow_stacking, vm_distance{F1_0*1024});	//	F1_0*512 means play twice as loud
2365                 } else if (d_rand() < FrameTime*16)
2366                         create_small_fireball_on_object(objp, (F1_0 + d_rand()) * 8, 0);
2367         } else if (d_rand() < FrameTime*8)
2368                 create_small_fireball_on_object(objp, (F1_0/2 + d_rand()) * 8, 1);
2369 
2370 	if (BossUniqueState.Boss_dying_start_time + BOSS_DEATH_DURATION < GameTime64 || GameTime64+(F1_0*2) < BossUniqueState.Boss_dying_start_time)
2371 	{
2372 		BossUniqueState.Boss_dying_start_time = GameTime64; // make sure following only happens one time!
2373 		do_controlcen_destroyed_stuff(object_none);
2374 		explode_object(objp, F1_0/4);
2375 		digi_link_sound_to_object2(SOUND_BADASS_EXPLOSION, objp, 0, F2_0, sound_stack::allow_stacking, vm_distance{F1_0*512});
2376 	}
2377 }
2378 
do_any_robot_dying_frame(const vmobjptridx_t)2379 static int do_any_robot_dying_frame(const vmobjptridx_t)
2380 {
2381 	return 0;
2382 }
2383 }
2384 #elif defined(DXX_BUILD_DESCENT_II)
2385 //	objp points at a boss.  He was presumably just hit and he's supposed to create a bot at the hit location *pos.
boss_spew_robot(const object_base & objp,const vms_vector & pos)2386 imobjptridx_t boss_spew_robot(const object_base &objp, const vms_vector &pos)
2387 {
2388 	auto &Objects = LevelUniqueObjectState.Objects;
2389 	auto &vcobjptr = Objects.vcptr;
2390 	int		boss_index;
2391 
2392 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
2393 	boss_index = Robot_info[get_robot_id(objp)].boss_flag - BOSS_D2;
2394 
2395 	Assert((boss_index >= 0) && (boss_index < NUM_D2_BOSSES));
2396 
2397 	auto &Segments = LevelUniqueSegmentState.get_segments();
2398 	const auto &&segnum = find_point_seg(LevelSharedSegmentState, LevelUniqueSegmentState, pos, Segments.vmptridx(objp.segnum));
2399 	if (segnum == segment_none) {
2400 		return object_none;
2401 	}
2402 
2403 	const auto &&newobjp = create_gated_robot(Vclip, vcobjptr, segnum, Spew_bots[boss_index][(Max_spew_bots[boss_index] * d_rand()) >> 15], &pos);
2404 
2405 	//	Make spewed robot come tumbling out as if blasted by a flash missile.
2406 	if (newobjp != object_none) {
2407 		int		force_val;
2408 
2409 		force_val = F1_0/FrameTime;
2410 
2411 		if (force_val) {
2412 			newobjp->ctype.ai_info.SKIP_AI_COUNT += force_val;
2413 			newobjp->mtype.phys_info.rotthrust.x = ((d_rand() - 16384) * force_val)/16;
2414 			newobjp->mtype.phys_info.rotthrust.y = ((d_rand() - 16384) * force_val)/16;
2415 			newobjp->mtype.phys_info.rotthrust.z = ((d_rand() - 16384) * force_val)/16;
2416 			newobjp->mtype.phys_info.flags |= PF_USES_THRUST;
2417 
2418 			//	Now, give a big initial velocity to get moving away from boss.
2419 			vm_vec_sub(newobjp->mtype.phys_info.velocity, pos, objp.pos);
2420 			vm_vec_normalize_quick(newobjp->mtype.phys_info.velocity);
2421 			vm_vec_scale(newobjp->mtype.phys_info.velocity, F1_0*128);
2422 		}
2423 	}
2424 
2425 	return newobjp;
2426 }
2427 
2428 //	----------------------------------------------------------------------
start_robot_death_sequence(object & obj)2429 void start_robot_death_sequence(object &obj)
2430 {
2431 	auto &ai_info = obj.ctype.ai_info;
2432 	ai_info.dying_start_time = GameTime64;
2433 	ai_info.dying_sound_playing = 0;
2434 	ai_info.SKIP_AI_COUNT = 0;
2435 }
2436 
2437 namespace {
2438 
2439 //	----------------------------------------------------------------------
2440 //	General purpose robot-dies-with-death-roll-and-groan code.
2441 //	Return true if object just died.
2442 //	scale: F1_0*4 for boss, much smaller for much smaller guys
do_robot_dying_frame(const vmobjptridx_t objp,fix64 start_time,fix roll_duration,sbyte * dying_sound_playing,int death_sound,fix expl_scale,fix sound_scale)2443 static int do_robot_dying_frame(const vmobjptridx_t objp, fix64 start_time, fix roll_duration, sbyte *dying_sound_playing, int death_sound, fix expl_scale, fix sound_scale)
2444 {
2445 	fix	sound_duration;
2446 
2447 	if (!roll_duration)
2448 		roll_duration = F1_0/4;
2449 
2450 	objp->mtype.phys_info.rotvel.x = (GameTime64 - start_time)/9;
2451 	objp->mtype.phys_info.rotvel.y = (GameTime64 - start_time)/5;
2452 	objp->mtype.phys_info.rotvel.z = (GameTime64 - start_time)/7;
2453 
2454 	if (const auto SndDigiSampleRate = GameArg.SndDigiSampleRate)
2455 		sound_duration = fixdiv(GameSounds[digi_xlat_sound(death_sound)].length, SndDigiSampleRate);
2456 	else
2457 		sound_duration = F1_0;
2458 
2459 	if (start_time + roll_duration - sound_duration < GameTime64) {
2460 		if (!*dying_sound_playing) {
2461 			*dying_sound_playing = 1;
2462 			digi_link_sound_to_object2(death_sound, objp, 0, sound_scale, sound_stack::allow_stacking, vm_distance{sound_scale*256});	//	F1_0*512 means play twice as loud
2463 		} else if (d_rand() < FrameTime*16)
2464 			create_small_fireball_on_object(objp, (F1_0 + d_rand()) * (16 * expl_scale/F1_0)/8, 0);
2465 	} else if (d_rand() < FrameTime*8)
2466 		create_small_fireball_on_object(objp, (F1_0/2 + d_rand()) * (16 * expl_scale/F1_0)/8, 1);
2467 
2468 	if (start_time + roll_duration < GameTime64 || GameTime64+(F1_0*2) < start_time)
2469 		return 1;
2470 	else
2471 		return 0;
2472 }
2473 
2474 //	----------------------------------------------------------------------
do_boss_dying_frame(const vmobjptridx_t objp)2475 static void do_boss_dying_frame(const vmobjptridx_t objp)
2476 {
2477 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
2478 	int	rval;
2479 
2480 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
2481 	rval = do_robot_dying_frame(objp, BossUniqueState.Boss_dying_start_time, BOSS_DEATH_DURATION, &BossUniqueState.Boss_dying_sound_playing, Robot_info[get_robot_id(objp)].deathroll_sound, F1_0*4, F1_0*4);
2482 
2483 	if (rval)
2484 	{
2485 		BossUniqueState.Boss_dying_start_time = GameTime64; // make sure following only happens one time!
2486 		do_controlcen_destroyed_stuff(object_none);
2487 		explode_object(objp, F1_0/4);
2488 		digi_link_sound_to_object2(SOUND_BADASS_EXPLOSION, objp, 0, F2_0, sound_stack::allow_stacking, vm_distance{F1_0*512});
2489 	}
2490 }
2491 
2492 //	----------------------------------------------------------------------
do_any_robot_dying_frame(const vmobjptridx_t objp)2493 static int do_any_robot_dying_frame(const vmobjptridx_t objp)
2494 {
2495 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
2496 	if (objp->ctype.ai_info.dying_start_time) {
2497 		int	rval, death_roll;
2498 
2499 		death_roll = Robot_info[get_robot_id(objp)].death_roll;
2500 		rval = do_robot_dying_frame(objp, objp->ctype.ai_info.dying_start_time, min(death_roll/2+1,6)*F1_0, &objp->ctype.ai_info.dying_sound_playing, Robot_info[get_robot_id(objp)].deathroll_sound, death_roll*F1_0/8, death_roll*F1_0/2);
2501 
2502 		if (rval) {
2503 			objp->ctype.ai_info.dying_start_time = GameTime64; // make sure following only happens one time!
2504 			explode_object(objp, F1_0/4);
2505 			digi_link_sound_to_object2(SOUND_BADASS_EXPLOSION, objp, 0, F2_0, sound_stack::allow_stacking, vm_distance{F1_0*512});
2506 			if (Current_level_num < 0)
2507 			{
2508 				const auto id = get_robot_id(objp);
2509 				if (Robot_info[id].thief)
2510 					recreate_thief(id);
2511 			}
2512 		}
2513 
2514 		return 1;
2515 	}
2516 
2517 	return 0;
2518 }
2519 
2520 }
2521 #endif
2522 
2523 // --------------------------------------------------------------------------------------------------------------------
2524 //	Called for an AI object if it is fairly aware of the player.
2525 //	awareness_level is in 0..100.  Larger numbers indicate greater awareness (eg, 99 if firing at player).
2526 //	In a given frame, might not get called for an object, or might be called more than once.
2527 //	The fact that this routine is not called for a given object does not mean that object is not interested in the player.
2528 //	Objects are moved by physics, so they can move even if not interested in a player.  However, if their velocity or
2529 //	orientation is changing, this routine will be called.
2530 //	Return value:
2531 //		0	this player IS NOT allowed to move this robot.
2532 //		1	this player IS allowed to move this robot.
ai_multiplayer_awareness(const vmobjptridx_t objp,int awareness_level)2533 int ai_multiplayer_awareness(const vmobjptridx_t objp, int awareness_level)
2534 {
2535 	int	rval=1;
2536 
2537 	if (Game_mode & GM_MULTI) {
2538 		if (awareness_level == 0)
2539 			return 0;
2540 		rval = multi_can_move_robot(objp, awareness_level);
2541 	}
2542 
2543 	return rval;
2544 }
2545 
2546 }
2547 
2548 #ifndef NDEBUG
2549 namespace {
2550 fix	Prev_boss_shields = -1;
2551 }
2552 #endif
2553 
2554 namespace dsx {
2555 namespace {
2556 
2557 #if defined(DXX_BUILD_DESCENT_I)
2558 #define do_d1_boss_stuff(FS,FO,PV)	do_d1_boss_stuff(FS,FO)
2559 #endif
2560 
2561 // --------------------------------------------------------------------------------------------------------------------
2562 //	Do special stuff for a boss.
do_d1_boss_stuff(fvmsegptridx & vmsegptridx,const vmobjptridx_t objp,const player_visibility_state player_visibility)2563 static void do_d1_boss_stuff(fvmsegptridx &vmsegptridx, const vmobjptridx_t objp, const player_visibility_state player_visibility)
2564 {
2565 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
2566 	auto &Objects = LevelUniqueObjectState.Objects;
2567 	auto &vmobjptr = Objects.vmptr;
2568 #ifndef NDEBUG
2569 	if (objp->shields != Prev_boss_shields) {
2570 		Prev_boss_shields = objp->shields;
2571 	}
2572 #endif
2573 
2574 #if defined(DXX_BUILD_DESCENT_II)
2575 	if (!EMULATING_D1 && !player_is_visible(player_visibility) && !BossUniqueState.Boss_hit_this_frame)
2576 		return;
2577 #endif
2578 
2579 	if (!BossUniqueState.Boss_dying) {
2580 		const auto Boss_cloak_start_time = BossUniqueState.Boss_cloak_start_time;
2581 		if (objp->ctype.ai_info.CLOAKED == 1) {
2582 			if (GameTime64 - Boss_cloak_start_time > Boss_cloak_duration / 3 &&
2583 				(Boss_cloak_start_time + Boss_cloak_duration) - GameTime64 > Boss_cloak_duration / 3 &&
2584 				GameTime64 - BossUniqueState.Last_teleport_time > LevelSharedBossState.Boss_teleport_interval)
2585 			{
2586 				if (ai_multiplayer_awareness(objp, 98))
2587 					teleport_boss(Vclip, vmsegptridx, objp, get_local_plrobj().pos);
2588 			} else if (BossUniqueState.Boss_hit_this_frame) {
2589 				BossUniqueState.Boss_hit_this_frame = 0;
2590 				BossUniqueState.Last_teleport_time -= LevelSharedBossState.Boss_teleport_interval / 4;
2591 			}
2592 
2593 			if (GameTime64 > (Boss_cloak_start_time + Boss_cloak_duration) ||
2594 				GameTime64 < Boss_cloak_start_time)
2595 				objp->ctype.ai_info.CLOAKED = 0;
2596 		} else {
2597 			if (BossUniqueState.Boss_hit_this_frame ||
2598 				GameTime64 - (Boss_cloak_start_time + Boss_cloak_duration) > LevelSharedBossState.Boss_cloak_interval)
2599 			{
2600 				if (ai_multiplayer_awareness(objp, 95))
2601 				{
2602 					BossUniqueState.Boss_hit_this_frame = 0;
2603 					BossUniqueState.Boss_cloak_start_time = GameTime64;
2604 					objp->ctype.ai_info.CLOAKED = 1;
2605 					if (Game_mode & GM_MULTI)
2606 						multi_send_boss_cloak(objp);
2607 				}
2608 			}
2609 		}
2610 	} else
2611 		do_boss_dying_frame(objp);
2612 
2613 }
2614 
2615 #define	BOSS_TO_PLAYER_GATE_DISTANCE	(F1_0*150)
2616 
2617 
2618 // --------------------------------------------------------------------------------------------------------------------
2619 //	Do special stuff for a boss.
do_super_boss_stuff(fvmsegptridx & vmsegptridx,const vmobjptridx_t objp,const fix dist_to_player,const player_visibility_state player_visibility)2620 static void do_super_boss_stuff(fvmsegptridx &vmsegptridx, const vmobjptridx_t objp, const fix dist_to_player, const player_visibility_state player_visibility)
2621 {
2622 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
2623 	static int eclip_state = 0;
2624 	do_d1_boss_stuff(vmsegptridx, objp, player_visibility);
2625 
2626 	// Only master player can cause gating to occur.
2627 	if ((Game_mode & GM_MULTI) && !multi_i_am_master())
2628                 return;
2629 
2630 	if (dist_to_player < BOSS_TO_PLAYER_GATE_DISTANCE || player_is_visible(player_visibility) || (Game_mode & GM_MULTI)) {
2631 		const auto Gate_interval = GameUniqueState.Boss_gate_interval;
2632 		if (GameTime64 - BossUniqueState.Last_gate_time > Gate_interval/2) {
2633 			restart_effect(ECLIP_NUM_BOSS);
2634 			if (eclip_state == 0) {
2635 				multi_send_boss_start_gate(objp);
2636 				eclip_state = 1;
2637 			}
2638 		}
2639 		else {
2640 			stop_effect(ECLIP_NUM_BOSS);
2641 			if (eclip_state == 1) {
2642 				multi_send_boss_stop_gate(objp);
2643 				eclip_state = 0;
2644 			}
2645 		}
2646 
2647 		if (GameTime64 - BossUniqueState.Last_gate_time > Gate_interval)
2648 			if (ai_multiplayer_awareness(objp, 99)) {
2649 				uint_fast32_t randtype = (d_rand() * MAX_GATE_INDEX) >> 15;
2650 
2651 				Assert(randtype < MAX_GATE_INDEX);
2652 				randtype = Super_boss_gate_list[randtype];
2653 
2654 				const auto &&rtval = gate_in_robot(vmsegptridx, randtype);
2655 				if (rtval != object_none && (Game_mode & GM_MULTI))
2656 				{
2657 					multi_send_boss_create_robot(objp, rtval);
2658 				}
2659 			}
2660 	}
2661 }
2662 
2663 #if defined(DXX_BUILD_DESCENT_II)
do_d2_boss_stuff(fvmsegptridx & vmsegptridx,const vmobjptridx_t objp,const player_visibility_state player_visibility)2664 static void do_d2_boss_stuff(fvmsegptridx &vmsegptridx, const vmobjptridx_t objp, const player_visibility_state player_visibility)
2665 {
2666 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
2667 	auto &Objects = LevelUniqueObjectState.Objects;
2668 	auto &vmobjptr = Objects.vmptr;
2669 	int	boss_id, boss_index;
2670 
2671 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
2672 	boss_id = Robot_info[get_robot_id(objp)].boss_flag;
2673 
2674 	Assert((boss_id >= BOSS_D2) && (boss_id < BOSS_D2 + NUM_D2_BOSSES));
2675 
2676 	boss_index = boss_id - BOSS_D2;
2677 
2678 #ifndef NDEBUG
2679 	if (objp->shields != Prev_boss_shields) {
2680 		Prev_boss_shields = objp->shields;
2681 	}
2682 #endif
2683 
2684 	//	@mk, 10/13/95:  Reason:
2685 	//		Level 4 boss behind locked door.  But he's allowed to teleport out of there.  So he
2686 	//		teleports out of there right away, and blasts player right after first door.
2687 	if (!player_is_visible(player_visibility) && GameTime64 - BossUniqueState.Boss_hit_time > F1_0 * 2)
2688 		return;
2689 
2690 	if (!BossUniqueState.Boss_dying && Boss_teleports[boss_index]) {
2691 		const auto Boss_cloak_start_time = BossUniqueState.Boss_cloak_start_time;
2692 		if (objp->ctype.ai_info.CLOAKED == 1) {
2693 			BossUniqueState.Boss_hit_time = GameTime64;	//	Keep the cloak:teleport process going.
2694 			if (GameTime64 - Boss_cloak_start_time > Boss_cloak_duration / 3 &&
2695 				(Boss_cloak_start_time + Boss_cloak_duration) - GameTime64 > Boss_cloak_duration / 3 &&
2696 				GameTime64 - BossUniqueState.Last_teleport_time > LevelSharedBossState.Boss_teleport_interval)
2697 			{
2698 				if (ai_multiplayer_awareness(objp, 98))
2699 					teleport_boss(Vclip, vmsegptridx, objp, get_local_plrobj().pos);
2700 			} else if (GameTime64 - BossUniqueState.Boss_hit_time > F1_0*2) {
2701 				BossUniqueState.Last_teleport_time -= LevelSharedBossState.Boss_teleport_interval / 4;
2702 			}
2703 
2704 			if (GameTime64 > (Boss_cloak_start_time + Boss_cloak_duration) ||
2705 				GameTime64 < Boss_cloak_start_time)
2706 				objp->ctype.ai_info.CLOAKED = 0;
2707 		} else if (GameTime64 - (Boss_cloak_start_time + Boss_cloak_duration) > LevelSharedBossState.Boss_cloak_interval ||
2708 				GameTime64 - (Boss_cloak_start_time + Boss_cloak_duration) < -Boss_cloak_duration) {
2709 			if (ai_multiplayer_awareness(objp, 95)) {
2710 				BossUniqueState.Boss_cloak_start_time = GameTime64;
2711 				objp->ctype.ai_info.CLOAKED = 1;
2712 				if (Game_mode & GM_MULTI)
2713 					multi_send_boss_cloak(objp);
2714 			}
2715 		}
2716 	}
2717 
2718 }
2719 #endif
2720 
2721 
ai_multi_send_robot_position(object & obj,int force)2722 static void ai_multi_send_robot_position(object &obj, int force)
2723 {
2724 	if (Game_mode & GM_MULTI)
2725 	{
2726 		multi_send_robot_position(obj, force != -1);
2727 	}
2728 	return;
2729 }
2730 
2731 // --------------------------------------------------------------------------------------------------------------------
2732 //	Returns true if this object should be allowed to fire at the player.
maybe_ai_do_actual_firing_stuff(object & obj)2733 static int maybe_ai_do_actual_firing_stuff(object &obj)
2734 {
2735 	if (Game_mode & GM_MULTI)
2736 	{
2737 		auto &aip = obj.ctype.ai_info;
2738 		if (aip.GOAL_STATE != AIS_FLIN && get_robot_id(obj) != ROBOT_BRAIN)
2739 		{
2740 			const auto s = aip.CURRENT_STATE;
2741 			if (s == AIS_FIRE)
2742 			{
2743 				static_assert(AIS_FIRE != 0, "AIS_FIRE must be nonzero for this shortcut to work properly.");
2744 				return s;
2745 			}
2746 		}
2747 	}
2748 
2749 	return 0;
2750 }
2751 
2752 #if defined(DXX_BUILD_DESCENT_I)
ai_do_actual_firing_stuff(fvmobjptridx & vmobjptridx,const vmobjptridx_t obj,ai_static * const aip,ai_local & ailp,const robot_info & robptr,const fix dist_to_player,const vms_vector & gun_point,const robot_to_player_visibility_state & player_visibility,const int object_animates,const player_info & player_info,int)2753 static void ai_do_actual_firing_stuff(fvmobjptridx &vmobjptridx, const vmobjptridx_t obj, ai_static *const aip, ai_local &ailp, const robot_info &robptr, const fix dist_to_player, const vms_vector &gun_point, const robot_to_player_visibility_state &player_visibility, const int object_animates, const player_info &player_info, int)
2754 {
2755 	auto &vec_to_player = player_visibility.vec_to_player;
2756 	fix	dot;
2757 
2758 	if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view)
2759 	{
2760 		//	Changed by mk, 01/04/94, onearm would take about 9 seconds until he can fire at you.
2761 		// if (((!object_animates) || (ailp->achieved_state[aip->CURRENT_GUN] == AIS_FIRE)) && (ailp->next_fire <= 0))
2762 		if (!object_animates || ready_to_fire_any_weapon(robptr, ailp, 0)) {
2763 			dot = vm_vec_dot(obj->orient.fvec, vec_to_player);
2764 			if (dot >= 7*F1_0/8) {
2765 
2766 				if (aip->CURRENT_GUN < robptr.n_guns) {
2767 					if (robptr.attack_type == 1) {
2768 						if (Player_dead_state != player_dead_state::exploded &&
2769 							dist_to_player < obj->size + ConsoleObject->size + F1_0*2)
2770 						{
2771 							if (!ai_multiplayer_awareness(obj, ROBOT_FIRE_AGITATION-2))
2772 								return;
2773 							do_ai_robot_hit_attack(obj, vmobjptridx(ConsoleObject), obj->pos);
2774 						} else {
2775 							return;
2776 						}
2777 					} else {
2778 						if ((gun_point.x == 0) && (gun_point.y == 0) && (gun_point.z == 0)) {
2779 							;
2780 						} else {
2781 							if (!ai_multiplayer_awareness(obj, ROBOT_FIRE_AGITATION))
2782 								return;
2783 							ai_fire_laser_at_player(LevelSharedSegmentState, obj, player_info, gun_point, 0);
2784 						}
2785 					}
2786 
2787 					//	Wants to fire, so should go into chase mode, probably.
2788 					if (aip->behavior != ai_behavior::AIB_RUN_FROM && aip->behavior != ai_behavior::AIB_STILL && aip->behavior != ai_behavior::AIB_FOLLOW_PATH && (ailp.mode == ai_mode::AIM_FOLLOW_PATH || ailp.mode == ai_mode::AIM_STILL))
2789 						ailp.mode = ai_mode::AIM_CHASE_OBJECT;
2790 				}
2791 
2792 				aip->GOAL_STATE = AIS_RECO;
2793 				ailp.goal_state[aip->CURRENT_GUN] = AIS_RECO;
2794 
2795 				// Switch to next gun for next fire.
2796 				aip->CURRENT_GUN++;
2797 				if (aip->CURRENT_GUN >= robptr.n_guns)
2798 					aip->CURRENT_GUN = 0;
2799 			}
2800 		}
2801 	} else if (Weapon_info[robptr.weapon_type].homing_flag) {
2802 		//	Robots which fire homing weapons might fire even if they don't have a bead on the player.
2803 		if ((!object_animates || ailp.achieved_state[aip->CURRENT_GUN] == AIS_FIRE)
2804 			&& ready_to_fire_weapon1(ailp, 0)
2805 			&& (vm_vec_dist_quick(Hit_pos, obj->pos) > F1_0*40)) {
2806 			if (!ai_multiplayer_awareness(obj, ROBOT_FIRE_AGITATION))
2807 				return;
2808 			ai_fire_laser_at_player(LevelSharedSegmentState, obj, player_info, gun_point, 0);
2809 
2810 			aip->GOAL_STATE = AIS_RECO;
2811 			ailp.goal_state[aip->CURRENT_GUN] = AIS_RECO;
2812 
2813 			// Switch to next gun for next fire.
2814 			aip->CURRENT_GUN++;
2815 			if (aip->CURRENT_GUN >= robptr.n_guns)
2816 				aip->CURRENT_GUN = 0;
2817 		} else {
2818 			// Switch to next gun for next fire.
2819 			aip->CURRENT_GUN++;
2820 			if (aip->CURRENT_GUN >= robptr.n_guns)
2821 				aip->CURRENT_GUN = 0;
2822 		}
2823 	}
2824 }
2825 #elif defined(DXX_BUILD_DESCENT_II)
2826 
2827 // --------------------------------------------------------------------------------------------------------------------
2828 //	If fire_anyway, fire even if player is not visible.  We're firing near where we believe him to be.  Perhaps he's
2829 //	lurking behind a corner.
ai_do_actual_firing_stuff(fvmobjptridx & vmobjptridx,const vmobjptridx_t obj,ai_static * const aip,ai_local & ailp,const robot_info & robptr,const fix dist_to_player,vms_vector & gun_point,const robot_to_player_visibility_state & player_visibility,const int object_animates,const player_info & player_info,const int gun_num)2830 static void ai_do_actual_firing_stuff(fvmobjptridx &vmobjptridx, const vmobjptridx_t obj, ai_static *const aip, ai_local &ailp, const robot_info &robptr, const fix dist_to_player, vms_vector &gun_point, const robot_to_player_visibility_state &player_visibility, const int object_animates, const player_info &player_info, const int gun_num)
2831 {
2832 	auto &vec_to_player = player_visibility.vec_to_player;
2833 	fix	dot;
2834 
2835 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
2836 	if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view || Dist_to_last_fired_upon_player_pos < FIRE_AT_NEARBY_PLAYER_THRESHOLD)
2837 	{
2838 		vms_vector	fire_pos;
2839 
2840 		fire_pos = Believed_player_pos;
2841 
2842 		//	Hack: If visibility not == 2, we're here because we're firing at a nearby player.
2843 		//	So, fire at Last_fired_upon_player_pos instead of the player position.
2844 		if (!robptr.attack_type && player_visibility.visibility != player_visibility_state::visible_and_in_field_of_view)
2845 			fire_pos = Last_fired_upon_player_pos;
2846 
2847 		//	Changed by mk, 01/04/95, onearm would take about 9 seconds until he can fire at you.
2848 		//	Above comment corrected.  Date changed from 1994, to 1995.  Should fix some very subtle bugs, as well as not cause me to wonder, in the future, why I was writing AI code for onearm ten months before he existed.
2849 		if (!object_animates || ready_to_fire_any_weapon(robptr, ailp, 0)) {
2850 			dot = vm_vec_dot(obj->orient.fvec, vec_to_player);
2851 			if ((dot >= 7*F1_0/8) || ((dot > F1_0/4) &&  robptr.boss_flag)) {
2852 
2853 				if (gun_num < robptr.n_guns) {
2854 					if (robptr.attack_type == 1) {
2855 						if (Player_dead_state != player_dead_state::exploded &&
2856 							dist_to_player < obj->size + ConsoleObject->size + F1_0*2)
2857 						{
2858 							if (!ai_multiplayer_awareness(obj, ROBOT_FIRE_AGITATION-2))
2859 								return;
2860 							do_ai_robot_hit_attack(obj, vmobjptridx(ConsoleObject), obj->pos);
2861 						} else {
2862 							return;
2863 						}
2864 					} else {
2865 						if ((gun_point.x == 0) && (gun_point.y == 0) && (gun_point.z == 0)) {
2866 							;
2867 						} else {
2868 							if (!ai_multiplayer_awareness(obj, ROBOT_FIRE_AGITATION))
2869 								return;
2870 							//	New, multi-weapon-type system, 06/05/95 (life is slipping away...)
2871 							if (ready_to_fire_weapon1(ailp, 0)) {
2872 								ai_fire_laser_at_player(LevelSharedSegmentState, obj, player_info, gun_point, gun_num, fire_pos);
2873 								Last_fired_upon_player_pos = fire_pos;
2874 							}
2875 							if (gun_num != 0) {
2876 								if (ready_to_fire_weapon2(robptr, ailp, 0)) {
2877 									calc_gun_point(gun_point, obj, 0);
2878 									ai_fire_laser_at_player(LevelSharedSegmentState, obj, player_info, gun_point, 0, fire_pos);
2879 									Last_fired_upon_player_pos = fire_pos;
2880 								}
2881 							}
2882 						}
2883 					}
2884 
2885 					//	Wants to fire, so should go into chase mode, probably.
2886 					if ( (aip->behavior != ai_behavior::AIB_RUN_FROM)
2887 						 && (aip->behavior != ai_behavior::AIB_STILL)
2888 						 && (aip->behavior != ai_behavior::AIB_SNIPE)
2889 						 && (aip->behavior != ai_behavior::AIB_FOLLOW)
2890 						 && (!robptr.attack_type)
2891 						 && (ailp.mode == ai_mode::AIM_FOLLOW_PATH || ailp.mode == ai_mode::AIM_STILL))
2892 						ailp.mode = ai_mode::AIM_CHASE_OBJECT;
2893 				}
2894 
2895 				aip->GOAL_STATE = AIS_RECO;
2896 				ailp.goal_state[aip->CURRENT_GUN] = AIS_RECO;
2897 
2898 				// Switch to next gun for next fire.  If has 2 gun types, select gun #1, if exists.
2899 				aip->CURRENT_GUN++;
2900 				if (aip->CURRENT_GUN >= robptr.n_guns)
2901 				{
2902 					if ((robptr.n_guns == 1) || (robptr.weapon_type2 == weapon_none))
2903 						aip->CURRENT_GUN = 0;
2904 					else
2905 						aip->CURRENT_GUN = 1;
2906 				}
2907 			}
2908 		}
2909 	}
2910 	else if ((!robptr.attack_type && Weapon_info[Robot_info[get_robot_id(obj)].weapon_type].homing_flag) || ((Robot_info[get_robot_id(obj)].weapon_type2 != weapon_none && Weapon_info[Robot_info[get_robot_id(obj)].weapon_type2].homing_flag)))
2911 	{
2912 		//	Robots which fire homing weapons might fire even if they don't have a bead on the player.
2913 		if ((!object_animates || ailp.achieved_state[aip->CURRENT_GUN] == AIS_FIRE)
2914 			&& (((ready_to_fire_weapon1(ailp, 0)) && (aip->CURRENT_GUN != 0)) || ((ready_to_fire_weapon2(robptr, ailp, 0)) && (aip->CURRENT_GUN == 0)))
2915 			 && (vm_vec_dist_quick(Hit_pos, obj->pos) > F1_0*40)) {
2916 			if (!ai_multiplayer_awareness(obj, ROBOT_FIRE_AGITATION))
2917 				return;
2918 			ai_fire_laser_at_player(LevelSharedSegmentState, obj, player_info, gun_point, gun_num, Believed_player_pos);
2919 
2920 			aip->GOAL_STATE = AIS_RECO;
2921 			ailp.goal_state[aip->CURRENT_GUN] = AIS_RECO;
2922 		}
2923 			// Switch to next gun for next fire.
2924 			aip->CURRENT_GUN++;
2925 			if (aip->CURRENT_GUN >= robptr.n_guns)
2926 				aip->CURRENT_GUN = 0;
2927 	} else {
2928 
2929 
2930 	//	---------------------------------------------------------------
2931 
2932 		vms_vector	vec_to_last_pos;
2933 
2934 		if (d_rand()/2 < fixmul(FrameTime, (GameUniqueState.Difficulty_level << 12) + 0x4000)) {
2935 		if ((!object_animates || ready_to_fire_any_weapon(robptr, ailp, 0)) && (Dist_to_last_fired_upon_player_pos < FIRE_AT_NEARBY_PLAYER_THRESHOLD)) {
2936 			vm_vec_normalized_dir_quick(vec_to_last_pos, Believed_player_pos, obj->pos);
2937 			dot = vm_vec_dot(obj->orient.fvec, vec_to_last_pos);
2938 			if (dot >= 7*F1_0/8) {
2939 
2940 				if (aip->CURRENT_GUN < robptr.n_guns) {
2941 					if (robptr.attack_type == 1) {
2942 						if (Player_dead_state != player_dead_state::exploded &&
2943 							dist_to_player < obj->size + ConsoleObject->size + F1_0*2)
2944 						{
2945 							if (!ai_multiplayer_awareness(obj, ROBOT_FIRE_AGITATION-2))
2946 								return;
2947 							do_ai_robot_hit_attack(obj, vmobjptridx(ConsoleObject), obj->pos);
2948 						} else {
2949 							return;
2950 						}
2951 					} else {
2952 						if ((gun_point.x == 0) && (gun_point.y == 0) && (gun_point.z == 0)) {
2953 							;
2954 						} else {
2955 							if (!ai_multiplayer_awareness(obj, ROBOT_FIRE_AGITATION))
2956 								return;
2957 							//	New, multi-weapon-type system, 06/05/95 (life is slipping away...)
2958 							if (ready_to_fire_weapon1(ailp, 0))
2959 							{
2960 								ai_fire_laser_at_player(LevelSharedSegmentState, obj, player_info, gun_point, gun_num, Last_fired_upon_player_pos);
2961 							}
2962 							if (gun_num != 0) {
2963 
2964 								if (ready_to_fire_weapon2(robptr, ailp, 0)) {
2965 									calc_gun_point(gun_point, obj, 0);
2966 									ai_fire_laser_at_player(LevelSharedSegmentState, obj, player_info, gun_point, 0, Last_fired_upon_player_pos);
2967 								}
2968 							}
2969 						}
2970 					}
2971 
2972 					//	Wants to fire, so should go into chase mode, probably.
2973 					if (aip->behavior != ai_behavior::AIB_RUN_FROM && aip->behavior != ai_behavior::AIB_STILL && aip->behavior != ai_behavior::AIB_SNIPE && aip->behavior != ai_behavior::AIB_FOLLOW && (ailp.mode == ai_mode::AIM_FOLLOW_PATH || ailp.mode == ai_mode::AIM_STILL))
2974 						ailp.mode = ai_mode::AIM_CHASE_OBJECT;
2975 				}
2976 				aip->GOAL_STATE = AIS_RECO;
2977 				ailp.goal_state[aip->CURRENT_GUN] = AIS_RECO;
2978 
2979 				// Switch to next gun for next fire.
2980 				aip->CURRENT_GUN++;
2981 				if (aip->CURRENT_GUN >= robptr.n_guns)
2982 				{
2983 					if (robptr.n_guns == 1)
2984 						aip->CURRENT_GUN = 0;
2985 					else
2986 						aip->CURRENT_GUN = 1;
2987 				}
2988 			}
2989 		}
2990 		}
2991 
2992 
2993 	//	---------------------------------------------------------------
2994 	}
2995 }
2996 
2997 }
2998 
2999 // ----------------------------------------------------------------------------
init_ai_frame(const player_flags powerup_flags,const control_info & Controls)3000 void init_ai_frame(const player_flags powerup_flags, const control_info &Controls)
3001 {
3002 	Dist_to_last_fired_upon_player_pos = vm_vec_dist_quick(Last_fired_upon_player_pos, Believed_player_pos);
3003 
3004 	if (!(powerup_flags & PLAYER_FLAGS_CLOAKED) ||
3005 		(powerup_flags & PLAYER_FLAGS_HEADLIGHT_ON) ||
3006 		(Afterburner_charge && Controls.state.afterburner && (powerup_flags & PLAYER_FLAGS_AFTERBURNER)))
3007 	{
3008 		ai_do_cloak_stuff();
3009 	}
3010 }
3011 
3012 namespace {
3013 
3014 // ----------------------------------------------------------------------------
3015 // Make a robot near the player snipe.
3016 #define	MNRS_SEG_MAX	70
make_nearby_robot_snipe(fvmsegptr & vmsegptr,const vmobjptr_t robot,const player_flags powerup_flags)3017 static void make_nearby_robot_snipe(fvmsegptr &vmsegptr, const vmobjptr_t robot, const player_flags powerup_flags)
3018 {
3019 	auto &Objects = LevelUniqueObjectState.Objects;
3020 	auto &vmobjptridx = Objects.vmptridx;
3021 	std::array<segnum_t, MNRS_SEG_MAX> bfs_list;
3022 	/* Passing powerup_flags here seems wrong.  Sniping robots do not
3023 	 * open doors, so they should not care what doors the player can
3024 	 * open.  However, passing powerup_flags here maintains the
3025 	 * semantics that past versions used.
3026 	 */
3027 	const auto bfs_length = create_bfs_list(robot, ConsoleObject->segnum, powerup_flags, bfs_list);
3028 
3029 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
3030 	range_for (auto &i, partial_const_range(bfs_list, bfs_length)) {
3031 		range_for (object &objp, objects_in(vmsegptr(i), vmobjptridx, vmsegptr))
3032 		{
3033 			object &obj = objp;
3034 			if (obj.type != OBJ_ROBOT)
3035 				continue;
3036 			if (obj.ctype.ai_info.behavior == ai_behavior::AIB_SNIPE)
3037 				continue;
3038 			if (obj.ctype.ai_info.behavior == ai_behavior::AIB_RUN_FROM)
3039 				continue;
3040 			const auto rid = get_robot_id(obj);
3041 			if (rid == ROBOT_BRAIN)
3042 				continue;
3043 			auto &robptr = Robot_info[rid];
3044 			if (!robptr.boss_flag && !robot_is_companion(robptr))
3045 			{
3046 					obj.ctype.ai_info.behavior = ai_behavior::AIB_SNIPE;
3047 					obj.ctype.ai_info.ail.mode = ai_mode::AIM_SNIPE_ATTACK;
3048 					return;
3049 			}
3050 		}
3051 	}
3052 }
3053 
openable_door_on_near_path(fvcsegptr & vcsegptr,fvcwallptr & vcwallptr,const object & obj,const ai_static & aip)3054 static int openable_door_on_near_path(fvcsegptr &vcsegptr, fvcwallptr &vcwallptr, const object &obj, const ai_static &aip)
3055 {
3056 	const auto path_length = aip.path_length;
3057 	if (path_length < 1)
3058 		return 0;
3059 	if (const auto r = openable_doors_in_segment(vcwallptr, vcsegptr(obj.segnum)) - side_none)
3060 		return r;
3061 	if (path_length < 2)
3062 		return 0;
3063 	size_t idx;
3064 	idx = aip.hide_index + aip.cur_path_index + aip.PATH_DIR;
3065 	/* Hack: Point_segs[idx].segnum should never be none here, but
3066 	 * sometimes the guidebot has a none.  Guard against the bogus none
3067 	 * until someone can track down why the guidebot does this.
3068 	 */
3069 	if (idx < Point_segs.size() && Point_segs[idx].segnum != segment_none)
3070 	{
3071 		if (const auto r = openable_doors_in_segment(vcwallptr, vcsegptr(Point_segs[idx].segnum)) - side_none)
3072 			return r;
3073 	}
3074 	if (path_length < 3)
3075 		return 0;
3076 	idx = aip.hide_index + aip.cur_path_index + 2*aip.PATH_DIR;
3077 	if (idx < Point_segs.size() && Point_segs[idx].segnum != segment_none)
3078 	{
3079 		if (const auto r = openable_doors_in_segment(vcwallptr, vcsegptr(Point_segs[idx].segnum)) - side_none)
3080 			return r;
3081 	}
3082 	return 0;
3083 }
3084 
guidebot_should_fire_flare(fvcobjptr & vcobjptr,fvcsegptr & vcsegptr,fvcwallptr & vcwallptr,d_unique_buddy_state & BuddyState,const robot_info & robptr,const object & buddy_obj)3085 static unsigned guidebot_should_fire_flare(fvcobjptr &vcobjptr, fvcsegptr &vcsegptr, fvcwallptr &vcwallptr, d_unique_buddy_state &BuddyState, const robot_info &robptr, const object &buddy_obj)
3086 {
3087 	auto &ais = buddy_obj.ctype.ai_info;
3088 	auto &ail = ais.ail;
3089 	if (const auto r = ready_to_fire_any_weapon(robptr, ail, 0))
3090 	{
3091 	}
3092 	else
3093 		return r;
3094 	if (const auto r = openable_door_on_near_path(vcsegptr, vcwallptr, buddy_obj, ais))
3095 		return r;
3096 	if (ail.mode != ai_mode::AIM_GOTO_PLAYER)
3097 		return 0;
3098 	auto &plr = get_player_controlling_guidebot(BuddyState, Players);
3099 	if (plr.objnum == object_none)
3100 		/* should never happen */
3101 		return 0;
3102 	auto &plrobj = *vcobjptr(plr.objnum);
3103 	if (plrobj.type != OBJ_PLAYER)
3104 		return 0;
3105 	vms_vector vec_to_controller;
3106 	const auto dist_to_controller = vm_vec_normalized_dir_quick(vec_to_controller, plrobj.pos, buddy_obj.pos);
3107 	if (dist_to_controller >= 3 * MIN_ESCORT_DISTANCE / 2)
3108 		return 0;
3109 	if (vm_vec_dot(plrobj.orient.fvec, vec_to_controller) <= -F1_0 / 4)
3110 		return 0;
3111 	return 1;
3112 }
3113 #endif
3114 
3115 #ifdef NDEBUG
is_break_object(vcobjidx_t)3116 static bool is_break_object(vcobjidx_t)
3117 {
3118 	return false;
3119 }
3120 #else
3121 __attribute_used
3122 static objnum_t Break_on_object = object_none;
3123 
is_break_object(const vcobjidx_t robot)3124 static bool is_break_object(const vcobjidx_t robot)
3125 {
3126 	return Break_on_object == robot;
3127 }
3128 #endif
3129 
skip_ai_for_time_splice(const vcobjptridx_t robot,const robot_info & robptr,const vm_distance & dist_to_player)3130 static bool skip_ai_for_time_splice(const vcobjptridx_t robot, const robot_info &robptr, const vm_distance &dist_to_player)
3131 {
3132 	if (unlikely(is_break_object(robot)))
3133 		// don't time slice if we're interested in this object.
3134 		return false;
3135 	const auto &aip = robot->ctype.ai_info;
3136 	const auto &ailp = aip.ail;
3137 #if defined(DXX_BUILD_DESCENT_I)
3138 	(void)robptr;
3139 	if (static_cast<uint8_t>(ailp.player_awareness_type) < static_cast<uint8_t>(player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION) - 1)
3140 	{ // If robot got hit, he gets to attack player always!
3141 		{
3142 			if ((dist_to_player > F1_0*250) && (ailp.time_since_processed <= F1_0*2))
3143 				return true;
3144 			else if (!((aip.behavior == ai_behavior::AIB_STATION) && (ailp.mode == ai_mode::AIM_FOLLOW_PATH) && (aip.hide_segment != robot->segnum))) {
3145 				if ((dist_to_player > F1_0*150) && (ailp.time_since_processed <= F1_0))
3146 					return true;
3147 				else if ((dist_to_player > F1_0*100) && (ailp.time_since_processed <= F1_0/2))
3148 					return true;
3149 			}
3150 		}
3151 	}
3152 #elif defined(DXX_BUILD_DESCENT_II)
3153 	if (!((aip.behavior == ai_behavior::AIB_SNIPE) && (ailp.mode != ai_mode::AIM_SNIPE_WAIT)) && !robot_is_companion(robptr) && !robot_is_thief(robptr) && static_cast<uint8_t>(ailp.player_awareness_type) < static_cast<uint8_t>(player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION) - 1)
3154 	{ // If robot got hit, he gets to attack player always!
3155 		{
3156 			if ((aip.behavior == ai_behavior::AIB_STATION) && (ailp.mode == ai_mode::AIM_FOLLOW_PATH) && (aip.hide_segment != robot->segnum)) {
3157 				if (dist_to_player > F1_0*250)  // station guys not at home always processed until 250 units away.
3158 					return true;
3159 			}
3160 			else if (!player_is_visible(ailp.previous_visibility) && ((dist_to_player >> 7) > ailp.time_since_processed))
3161 			{  // 128 units away (6.4 segments) processed after 1 second.
3162 				return true;
3163 			}
3164 		}
3165 	}
3166 #endif
3167 	return false;
3168 }
3169 
3170 }
3171 
3172 // --------------------------------------------------------------------------------------------------------------------
do_ai_frame(const vmobjptridx_t obj)3173 void do_ai_frame(const vmobjptridx_t obj)
3174 {
3175 	auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
3176 	auto &Vertices = LevelSharedVertexState.get_vertices();
3177 	const auto Difficulty_level = GameUniqueState.Difficulty_level;
3178 	const objnum_t &objnum = obj;
3179 	ai_static	*const aip = &obj->ctype.ai_info;
3180 	ai_local &ailp = obj->ctype.ai_info.ail;
3181 	int			obj_ref;
3182 	int			object_animates;
3183 	int			new_goal_state;
3184 	vms_vector	gun_point;
3185 	vms_vector	vis_vec_pos;
3186 	auto &Objects = LevelUniqueObjectState.Objects;
3187 	auto &vmobjptr = Objects.vmptr;
3188 	auto &vmobjptridx = Objects.vmptridx;
3189 
3190 #if defined(DXX_BUILD_DESCENT_II)
3191 	auto &BuddyState = LevelUniqueObjectState.BuddyState;
3192 	auto &vcobjptr = Objects.vcptr;
3193 	ailp.next_action_time -= FrameTime;
3194 #endif
3195 
3196 	if (aip->SKIP_AI_COUNT) {
3197 		aip->SKIP_AI_COUNT--;
3198 #if defined(DXX_BUILD_DESCENT_II)
3199 		if (obj->mtype.phys_info.flags & PF_USES_THRUST) {
3200 			obj->mtype.phys_info.rotthrust.x = (obj->mtype.phys_info.rotthrust.x * 15)/16;
3201 			obj->mtype.phys_info.rotthrust.y = (obj->mtype.phys_info.rotthrust.y * 15)/16;
3202 			obj->mtype.phys_info.rotthrust.z = (obj->mtype.phys_info.rotthrust.z * 15)/16;
3203 			if (!aip->SKIP_AI_COUNT)
3204 				obj->mtype.phys_info.flags &= ~PF_USES_THRUST;
3205 		}
3206 #endif
3207 		return;
3208 	}
3209 
3210 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
3211 #if defined(DXX_BUILD_DESCENT_II)
3212 	auto &Station = LevelUniqueFuelcenterState.Station;
3213 #endif
3214 	auto &robptr = Robot_info[get_robot_id(obj)];
3215 	Assert(robptr.always_0xabcd == 0xabcd);
3216 
3217 	if (do_any_robot_dying_frame(obj))
3218 		return;
3219 
3220 	// Kind of a hack.  If a robot is flinching, but it is time for it to fire, unflinch it.
3221 	// Else, you can turn a big nasty robot into a wimp by firing flares at it.
3222 	// This also allows the player to see the cool flinch effect for mechs without unbalancing the game.
3223 	if ((aip->GOAL_STATE == AIS_FLIN) && ready_to_fire_any_weapon(robptr, ailp, 0)) {
3224 		aip->GOAL_STATE = AIS_FIRE;
3225 	}
3226 
3227 #ifndef NDEBUG
3228 	if (aip->behavior == ai_behavior::AIB_RUN_FROM && ailp.mode != ai_mode::AIM_RUN_FROM_OBJECT)
3229 		Int3(); // This is peculiar.  Behavior is run from, but mode is not.  Contact Mike.
3230 
3231 	if (Break_on_object != object_none)
3232 		if ((obj) == Break_on_object)
3233 			Int3(); // Contact Mike: This is a debug break
3234 #endif
3235 
3236 	//Assert((aip->behavior >= MIN_BEHAVIOR) && (aip->behavior <= MAX_BEHAVIOR));
3237 	switch (aip->behavior)
3238 	{
3239 		case ai_behavior::AIB_STILL:
3240 		case ai_behavior::AIB_NORMAL:
3241 		case ai_behavior::AIB_RUN_FROM:
3242 		case ai_behavior::AIB_STATION:
3243 #if defined(DXX_BUILD_DESCENT_I)
3244 		case ai_behavior::AIB_HIDE:
3245 		case ai_behavior::AIB_FOLLOW_PATH:
3246 #elif defined(DXX_BUILD_DESCENT_II)
3247 		case ai_behavior::AIB_BEHIND:
3248 		case ai_behavior::AIB_SNIPE:
3249 		case ai_behavior::AIB_FOLLOW:
3250 #endif
3251 			break;
3252 		default:
3253 			aip->behavior = ai_behavior::AIB_NORMAL;
3254 			break;
3255 	}
3256 
3257 	Assert(obj->segnum != segment_none);
3258 	assert(get_robot_id(obj) < LevelSharedRobotInfoState.N_robot_types);
3259 
3260 	obj_ref = objnum ^ d_tick_count;
3261 
3262 	if (!ready_to_fire_weapon1(ailp, -F1_0*8))
3263 		ailp.next_fire -= FrameTime;
3264 
3265 #if defined(DXX_BUILD_DESCENT_I)
3266 	Believed_player_pos = Ai_cloak_info[objnum & (MAX_AI_CLOAK_INFO-1)].last_position;
3267 #elif defined(DXX_BUILD_DESCENT_II)
3268 	if (robptr.weapon_type2 != weapon_none) {
3269 		if (!ready_to_fire_weapon2(robptr, ailp, -F1_0*8))
3270 			ailp.next_fire2 -= FrameTime;
3271 	} else
3272 		ailp.next_fire2 = F1_0*8;
3273 #endif
3274 
3275 	if (ailp.time_since_processed < F1_0*256)
3276 		ailp.time_since_processed += FrameTime;
3277 
3278 	const auto previous_visibility = ailp.previous_visibility;    //  Must get this before we toast the master copy!
3279 
3280 	auto &plrobj = get_local_plrobj();
3281 	auto &player_info = plrobj.ctype.player_info;
3282 	robot_to_player_visibility_state player_visibility;
3283 	auto &vec_to_player = player_visibility.vec_to_player;
3284 #if defined(DXX_BUILD_DESCENT_I)
3285 	if (!(player_info.powerup_flags & PLAYER_FLAGS_CLOAKED))
3286 		Believed_player_pos = ConsoleObject->pos;
3287 #elif defined(DXX_BUILD_DESCENT_II)
3288 	// If only awake because of a camera, make that the believed player position.
3289 	if ((aip->SUB_FLAGS & SUB_FLAGS_CAMERA_AWAKE) && Ai_last_missile_camera)
3290 		Believed_player_pos = Ai_last_missile_camera->pos;
3291 	else {
3292 		if (cheats.robotskillrobots) {
3293 			vis_vec_pos = obj->pos;
3294 			compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3295 			if (player_is_visible(player_visibility.visibility))
3296 			{
3297 				icobjptr_t min_obj = nullptr;
3298 				fix min_dist = F1_0*200, cur_dist;
3299 
3300 				range_for (const auto &&objp, vcobjptr)
3301 				{
3302 					if (objp->type == OBJ_ROBOT && objp != obj)
3303 					{
3304 						cur_dist = vm_vec_dist_quick(obj->pos, objp->pos);
3305 						if (cur_dist < F1_0*100)
3306 							if (object_to_object_visibility(obj, objp, FQ_TRANSWALL))
3307 								if (cur_dist < min_dist) {
3308 									min_obj = objp;
3309 									min_dist = cur_dist;
3310 								}
3311 					}
3312 				}
3313 
3314 				if (min_obj != nullptr)
3315 				{
3316 					Believed_player_pos = min_obj->pos;
3317 					Believed_player_seg = min_obj->segnum;
3318 					vm_vec_normalized_dir_quick(vec_to_player, Believed_player_pos, obj->pos);
3319 				} else
3320 					goto _exit_cheat;
3321 			} else
3322 				goto _exit_cheat;
3323 		} else {
3324 _exit_cheat:
3325 			DXX_MAKE_MEM_UNDEFINED(&player_visibility, sizeof(player_visibility));
3326 			player_visibility.initialized = 0;
3327 			if (!(player_info.powerup_flags & PLAYER_FLAGS_CLOAKED))
3328 				Believed_player_pos = ConsoleObject->pos;
3329 			else
3330 				Believed_player_pos = Ai_cloak_info[objnum & (MAX_AI_CLOAK_INFO-1)].last_position;
3331 		}
3332 	}
3333 #endif
3334 	auto dist_to_player = vm_vec_dist_quick(Believed_player_pos, obj->pos);
3335 
3336 	// If this robot can fire, compute visibility from gun position.
3337 	// Don't want to compute visibility twice, as it is expensive.  (So is call to calc_gun_point).
3338 #if defined(DXX_BUILD_DESCENT_I)
3339 	if (ready_to_fire_any_weapon(robptr, ailp, 0) && (dist_to_player < F1_0*200) && (robptr.n_guns) && !(robptr.attack_type))
3340 #elif defined(DXX_BUILD_DESCENT_II)
3341 	if ((player_is_visible(previous_visibility) || !(obj_ref & 3)) && ready_to_fire_any_weapon(robptr, ailp, 0) && (dist_to_player < F1_0*200) && (robptr.n_guns) && !(robptr.attack_type))
3342 #endif
3343 	{
3344 		// Since we passed ready_to_fire_any_weapon(), either next_fire or next_fire2 <= 0.  calc_gun_point from relevant one.
3345 		// If both are <= 0, we will deal with the mess in ai_do_actual_firing_stuff
3346 		const auto gun_num =
3347 #if defined(DXX_BUILD_DESCENT_II)
3348 			(!ready_to_fire_weapon1(ailp, 0))
3349 			? 0
3350 			:
3351 #endif
3352 			aip->CURRENT_GUN;
3353 			calc_gun_point(gun_point, obj, gun_num);
3354 		vis_vec_pos = gun_point;
3355 	} else {
3356 		vis_vec_pos = obj->pos;
3357 		vm_vec_zero(gun_point);
3358 	}
3359 
3360 	switch (const auto boss_flag = robptr.boss_flag) {
3361 		case 0:
3362 			break;
3363 
3364 		case BOSS_SUPER:
3365 			if (aip->GOAL_STATE == AIS_FLIN)
3366 				aip->GOAL_STATE = AIS_FIRE;
3367 			if (aip->CURRENT_STATE == AIS_FLIN)
3368 				aip->CURRENT_STATE = AIS_FIRE;
3369 			compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3370 
3371 			{
3372 				auto pv = player_visibility.visibility;
3373 			auto dtp = dist_to_player/4;
3374 
3375 			// If player cloaked, visibility is screwed up and superboss will gate in robots when not supposed to.
3376 			if (player_info.powerup_flags & PLAYER_FLAGS_CLOAKED) {
3377 				pv = player_visibility_state::no_line_of_sight;
3378 				dtp = vm_vec_dist_quick(ConsoleObject->pos, obj->pos)/4;
3379 			}
3380 
3381 			do_super_boss_stuff(vmsegptridx, obj, dtp, pv);
3382 		}
3383 			break;
3384 
3385 		default:
3386 #if defined(DXX_BUILD_DESCENT_I)
3387 			(void)boss_flag;
3388 			Int3();	//	Bogus boss flag value.
3389 			break;
3390 #endif
3391 		case BOSS_D1:
3392 		{
3393 			if (aip->GOAL_STATE == AIS_FLIN)
3394 				aip->GOAL_STATE = AIS_FIRE;
3395 			if (aip->CURRENT_STATE == AIS_FLIN)
3396 				aip->CURRENT_STATE = AIS_FIRE;
3397 
3398 			compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3399 
3400 #if defined(DXX_BUILD_DESCENT_II)
3401 			auto pv = player_visibility.visibility;
3402 			// If player cloaked, visibility is screwed up and superboss will gate in robots when not supposed to.
3403 			if (player_info.powerup_flags & PLAYER_FLAGS_CLOAKED) {
3404 				pv = player_visibility_state::no_line_of_sight;
3405 			}
3406 
3407 			if (boss_flag != BOSS_D1)
3408 			{
3409 				do_d2_boss_stuff(vmsegptridx, obj, pv);
3410 				break;
3411 			}
3412 #endif
3413 			do_d1_boss_stuff(vmsegptridx, obj, pv);
3414 		}
3415 			break;
3416 	}
3417 
3418 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
3419 	// Occasionally make non-still robots make a path to the player.  Based on agitation and distance from player.
3420 #if defined(DXX_BUILD_DESCENT_I)
3421 	if ((aip->behavior != ai_behavior::AIB_RUN_FROM) && (aip->behavior != ai_behavior::AIB_STILL) && !(Game_mode & GM_MULTI))
3422 #elif defined(DXX_BUILD_DESCENT_II)
3423 	if ((aip->behavior != ai_behavior::AIB_SNIPE) && (aip->behavior != ai_behavior::AIB_RUN_FROM) && (aip->behavior != ai_behavior::AIB_STILL) && !(Game_mode & GM_MULTI) && (robot_is_companion(robptr) != 1) && (robot_is_thief(robptr) != 1))
3424 #endif
3425 		if (Overall_agitation > 70) {
3426 			if ((dist_to_player < F1_0*200) && (d_rand() < FrameTime/4)) {
3427 				if (d_rand() * (Overall_agitation - 40) > F1_0*5) {
3428 					create_path_to_believed_player_segment(obj, 4 + Overall_agitation/8 + Difficulty_level, create_path_safety_flag::safe);
3429 					return;
3430 				}
3431 			}
3432 		}
3433 
3434 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
3435 	// If retry count not 0, then add it into consecutive_retries.
3436 	// If it is 0, cut down consecutive_retries.
3437 	// This is largely a hack to speed up physics and deal with stupid
3438 	// AI.  This is low level communication between systems of a sort
3439 	// that should not be done.
3440 	if (ailp.retry_count && !(Game_mode & GM_MULTI))
3441 	{
3442 		ailp.consecutive_retries += ailp.retry_count;
3443 		ailp.retry_count = 0;
3444 		if (ailp.consecutive_retries > 3)
3445 		{
3446 			switch (ailp.mode) {
3447 #if defined(DXX_BUILD_DESCENT_II)
3448 				case ai_mode::AIM_GOTO_PLAYER:
3449 					move_towards_segment_center(LevelSharedSegmentState, obj);
3450 					create_path_to_guidebot_player_segment(obj, 100, create_path_safety_flag::safe);
3451 					break;
3452 				case ai_mode::AIM_GOTO_OBJECT:
3453 					BuddyState.Escort_goal_object = ESCORT_GOAL_UNSPECIFIED;
3454 					break;
3455 #endif
3456 				case ai_mode::AIM_CHASE_OBJECT:
3457 					create_path_to_believed_player_segment(obj, 4 + Overall_agitation/8 + Difficulty_level, create_path_safety_flag::safe);
3458 					break;
3459 				case ai_mode::AIM_STILL:
3460 #if defined(DXX_BUILD_DESCENT_I)
3461 					if (!((aip->behavior == ai_behavior::AIB_STILL) || (aip->behavior == ai_behavior::AIB_STATION)))	//	Behavior is still, so don't follow path.
3462 #elif defined(DXX_BUILD_DESCENT_II)
3463 					if (robptr.attack_type)
3464 						move_towards_segment_center(LevelSharedSegmentState, obj);
3465 					else if (!((aip->behavior == ai_behavior::AIB_STILL) || (aip->behavior == ai_behavior::AIB_STATION) || (aip->behavior == ai_behavior::AIB_FOLLOW)))    // Behavior is still, so don't follow path.
3466 #endif
3467 						attempt_to_resume_path(obj);
3468 					break;
3469 				case ai_mode::AIM_FOLLOW_PATH:
3470 					if (Game_mode & GM_MULTI)
3471 						ailp.mode = ai_mode::AIM_STILL;
3472 					else
3473 						attempt_to_resume_path(obj);
3474 					break;
3475 				case ai_mode::AIM_RUN_FROM_OBJECT:
3476 					move_towards_segment_center(LevelSharedSegmentState, obj);
3477 					obj->mtype.phys_info.velocity = {};
3478 					create_n_segment_path(obj, 5, segment_none);
3479 					ailp.mode = ai_mode::AIM_RUN_FROM_OBJECT;
3480 					break;
3481 #if defined(DXX_BUILD_DESCENT_I)
3482 				case ai_mode::AIM_HIDE:
3483 					move_towards_segment_center(LevelSharedSegmentState, obj);
3484 					obj->mtype.phys_info.velocity = {};
3485 					if (Overall_agitation > (50 - Difficulty_level*4))
3486 						create_path_to_believed_player_segment(obj, 4 + Overall_agitation/8, create_path_safety_flag::safe);
3487 					else {
3488 						create_n_segment_path(obj, 5, segment_none);
3489 					}
3490 					break;
3491 #elif defined(DXX_BUILD_DESCENT_II)
3492 				case ai_mode::AIM_BEHIND:
3493 					move_towards_segment_center(LevelSharedSegmentState, obj);
3494 					obj->mtype.phys_info.velocity = {};
3495 					break;
3496 				case ai_mode::AIM_SNIPE_ATTACK:
3497 				case ai_mode::AIM_SNIPE_FIRE:
3498 				case ai_mode::AIM_SNIPE_RETREAT:
3499 				case ai_mode::AIM_SNIPE_RETREAT_BACKWARDS:
3500 				case ai_mode::AIM_SNIPE_WAIT:
3501 				case ai_mode::AIM_THIEF_ATTACK:
3502 				case ai_mode::AIM_THIEF_RETREAT:
3503 				case ai_mode::AIM_THIEF_WAIT:
3504 					break;
3505 #endif
3506 				case ai_mode::AIM_OPEN_DOOR:
3507 					create_n_segment_path_to_door(obj, 5);
3508 					break;
3509 				case ai_mode::AIM_FOLLOW_PATH_2:
3510 					Int3(); // Should never happen!
3511 					break;
3512 				case ai_mode::AIM_WANDER:
3513 					break;
3514 			}
3515 			ailp.consecutive_retries = 0;
3516 		}
3517 	} else
3518 		ailp.consecutive_retries /= 2;
3519 
3520 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
3521 	// If in materialization center, exit
3522 	if (!(Game_mode & GM_MULTI))
3523 	{
3524 		const shared_segment &seg = *vcsegptr(obj->segnum);
3525 		if (seg.special == segment_special::robotmaker)
3526 		{
3527 #if defined(DXX_BUILD_DESCENT_II)
3528 			if (Station[seg.station_idx].Enabled)
3529 #endif
3530 		{
3531 			ai_follow_path(obj, player_visibility_state::visible_not_in_field_of_view, nullptr);    // 1 = player is visible, which might be a lie, but it works.
3532 			return;
3533 		}
3534 		}
3535 	}
3536 
3537 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
3538 	// Decrease player awareness due to the passage of time.
3539 	if (ailp.player_awareness_type != player_awareness_type_t::PA_NONE)
3540 	{
3541 		if (ailp.player_awareness_time > 0)
3542 		{
3543 			ailp.player_awareness_time -= FrameTime;
3544 			if (ailp.player_awareness_time <= 0)
3545 			{
3546 				ailp.player_awareness_time = F1_0*2;   //new: 11/05/94
3547 				ailp.player_awareness_type = static_cast<player_awareness_type_t>(static_cast<unsigned>(ailp.player_awareness_type) - 1);          //new: 11/05/94
3548 			}
3549 		} else {
3550 			ailp.player_awareness_time = F1_0*2;
3551 			ailp.player_awareness_type = static_cast<player_awareness_type_t>(static_cast<unsigned>(ailp.player_awareness_type) - 1);
3552 			//aip->GOAL_STATE = AIS_REST;
3553 		}
3554 	} else
3555 		aip->GOAL_STATE = AIS_REST;                     //new: 12/13/94
3556 
3557 
3558 	if (Player_dead_state != player_dead_state::no &&
3559 		ailp.player_awareness_type == player_awareness_type_t::PA_NONE)
3560 		if ((dist_to_player < F1_0*200) && (d_rand() < FrameTime/8)) {
3561 			if ((aip->behavior != ai_behavior::AIB_STILL) && (aip->behavior != ai_behavior::AIB_RUN_FROM)) {
3562 				if (!ai_multiplayer_awareness(obj, 30))
3563 					return;
3564 				ai_multi_send_robot_position(obj, -1);
3565 
3566 				if (!(ailp.mode == ai_mode::AIM_FOLLOW_PATH && aip->cur_path_index < aip->path_length - 1))
3567 #if defined(DXX_BUILD_DESCENT_II)
3568 					if ((aip->behavior != ai_behavior::AIB_SNIPE) && (aip->behavior != ai_behavior::AIB_RUN_FROM))
3569 #endif
3570 					{
3571 						if (dist_to_player < F1_0*30)
3572 							create_n_segment_path(obj, 5, segment_none);
3573 						else
3574 							create_path_to_believed_player_segment(obj, 20, create_path_safety_flag::safe);
3575 					}
3576 			}
3577 		}
3578 
3579 	//	Make sure that if this guy got hit or bumped, then he's chasing player.
3580 	if (ailp.player_awareness_type == player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION || ailp.player_awareness_type >= player_awareness_type_t::PA_PLAYER_COLLISION)
3581 #if defined(DXX_BUILD_DESCENT_I)
3582 	{
3583 		if ((aip->behavior != ai_behavior::AIB_STILL) && (aip->behavior != ai_behavior::AIB_FOLLOW_PATH) && (aip->behavior != ai_behavior::AIB_RUN_FROM) && (get_robot_id(obj) != ROBOT_BRAIN))
3584 			ailp.mode = ai_mode::AIM_CHASE_OBJECT;
3585 	}
3586 #elif defined(DXX_BUILD_DESCENT_II)
3587 	{
3588 		compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3589 		if (player_visibility.visibility == player_visibility_state::visible_not_in_field_of_view) // Only increase visibility if unobstructed, else claw guys attack through doors.
3590 			player_visibility.visibility = player_visibility_state::visible_and_in_field_of_view;
3591 	} else if (((obj_ref&3) == 0) && !player_is_visible(previous_visibility) && (dist_to_player < F1_0*100)) {
3592 		fix sval, rval;
3593 
3594 		rval = d_rand();
3595 		sval = (dist_to_player * (static_cast<int>(Difficulty_level) + 1)) / 64;
3596 
3597 		if ((fixmul(rval, sval) < FrameTime) || (player_info.powerup_flags & PLAYER_FLAGS_HEADLIGHT_ON)) {
3598 			ailp.player_awareness_type = player_awareness_type_t::PA_PLAYER_COLLISION;
3599 			ailp.player_awareness_time = F1_0*3;
3600 			compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3601 			if (player_visibility.visibility == player_visibility_state::visible_not_in_field_of_view)
3602 				player_visibility.visibility = player_visibility_state::visible_and_in_field_of_view;
3603 		}
3604 	}
3605 #endif
3606 
3607 	//	- -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -
3608 	if ((aip->GOAL_STATE == AIS_FLIN) && (aip->CURRENT_STATE == AIS_FLIN))
3609 		aip->GOAL_STATE = AIS_LOCK;
3610 
3611 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
3612 	// Note: Should only do these two function calls for objects which animate
3613 	if (dist_to_player < F1_0*100) { // && !(Game_mode & GM_MULTI))
3614 		object_animates = do_silly_animation(obj);
3615 		if (object_animates)
3616 			ai_frame_animation(obj);
3617 	} else {
3618 		// If Object is supposed to animate, but we don't let it animate due to distance, then
3619 		// we must change its state, else it will never update.
3620 		aip->CURRENT_STATE = aip->GOAL_STATE;
3621 		object_animates = 0;        // If we're not doing the animation, then should pretend it doesn't animate.
3622 	}
3623 
3624 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
3625 	// Time-slice, don't process all the time, purely an efficiency hack.
3626 	// Guys whose behavior is station and are not at their hide segment get processed anyway.
3627 	if (skip_ai_for_time_splice(obj, robptr, dist_to_player))
3628 		return;
3629 
3630 	// Reset time since processed, but skew objects so not everything
3631 	// processed synchronously, else we get fast frames with the
3632 	// occasional very slow frame.
3633 	// AI_proc_time = ailp->time_since_processed;
3634 	ailp.time_since_processed = - ((objnum & 0x03) * FrameTime ) / 2;
3635 
3636 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
3637 	//	Perform special ability
3638 	switch (get_robot_id(obj)) {
3639 		case ROBOT_BRAIN:
3640 			// Robots function nicely if behavior is Station.  This
3641 			// means they won't move until they can see the player, at
3642 			// which time they will start wandering about opening doors.
3643 			if (ConsoleObject->segnum == obj->segnum) {
3644 				if (!ai_multiplayer_awareness(obj, 97))
3645 					return;
3646 				compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3647 				move_away_from_player(obj, vec_to_player, 0);
3648 				ai_multi_send_robot_position(obj, -1);
3649 			}
3650 			else if (ailp.mode != ai_mode::AIM_STILL)
3651 			{
3652 				auto &Walls = LevelUniqueWallSubsystemState.Walls;
3653 				auto &vcwallptr = Walls.vcptr;
3654 				const auto r = openable_doors_in_segment(vcwallptr, vcsegptr(obj->segnum));
3655 				if (r != side_none)
3656 				{
3657 					ailp.mode = ai_mode::AIM_OPEN_DOOR;
3658 					aip->GOALSIDE = r;
3659 				}
3660 				else if (ailp.mode != ai_mode::AIM_FOLLOW_PATH)
3661 				{
3662 					if (!ai_multiplayer_awareness(obj, 50))
3663 						return;
3664 					create_n_segment_path_to_door(obj, 8+Difficulty_level);     // third parameter is avoid_seg, -1 means avoid nothing.
3665 					ai_multi_send_robot_position(obj, -1);
3666 				}
3667 
3668 #if defined(DXX_BUILD_DESCENT_II)
3669 				if (ailp.next_action_time < 0)
3670 				{
3671 					compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3672 					if (player_is_visible(player_visibility.visibility))
3673 					{
3674 						const auto powerup_flags = player_info.powerup_flags;
3675 						make_nearby_robot_snipe(vmsegptr, obj, powerup_flags);
3676 						ailp.next_action_time = (NDL - Difficulty_level) * 2*F1_0;
3677 					}
3678 				}
3679 #endif
3680 			} else {
3681 				compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3682 				if (player_is_visible(player_visibility.visibility))
3683 				{
3684 					if (!ai_multiplayer_awareness(obj, 50))
3685 						return;
3686 					create_n_segment_path_to_door(obj, 8+Difficulty_level);     // third parameter is avoid_seg, -1 means avoid nothing.
3687 					ai_multi_send_robot_position(obj, -1);
3688 				}
3689 			}
3690 			break;
3691 		default:
3692 			break;
3693 	}
3694 
3695 #if defined(DXX_BUILD_DESCENT_II)
3696 	if (aip->behavior == ai_behavior::AIB_SNIPE) {
3697 		if ((Game_mode & GM_MULTI) && !robot_is_thief(robptr)) {
3698 			aip->behavior = ai_behavior::AIB_NORMAL;
3699 			ailp.mode = ai_mode::AIM_CHASE_OBJECT;
3700 			return;
3701 		}
3702 
3703 		if (!(obj_ref & 3) || player_is_visible(previous_visibility))
3704 		{
3705 			compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3706 
3707 			// If this sniper is in still mode, if he was hit or can see player, switch to snipe mode.
3708 			if (ailp.mode == ai_mode::AIM_STILL)
3709 				if (player_is_visible(player_visibility.visibility) || ailp.player_awareness_type == player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION)
3710 					ailp.mode = ai_mode::AIM_SNIPE_ATTACK;
3711 
3712 			if (!robot_is_thief(robptr) && ailp.mode != ai_mode::AIM_STILL)
3713 				do_snipe_frame(obj, dist_to_player, player_visibility.visibility, vec_to_player);
3714 		} else if (!robot_is_thief(robptr) && !robot_is_companion(robptr))
3715 			return;
3716 	}
3717 
3718 	auto &Walls = LevelUniqueWallSubsystemState.Walls;
3719 	auto &vcwallptr = Walls.vcptr;
3720 	// More special ability stuff, but based on a property of a robot, not its ID.
3721 	if (robot_is_companion(robptr)) {
3722 		compute_buddy_vis_vec(obj, obj->pos, player_visibility, robptr);
3723 		auto &player_controlling_guidebot = get_player_controlling_guidebot(BuddyState, Players);
3724 		if (player_controlling_guidebot.objnum != object_none)
3725 		{
3726 			auto &plrobj_controlling_guidebot = *Objects.vcptr(player_controlling_guidebot.objnum);
3727 			do_escort_frame(obj, plrobj_controlling_guidebot, player_visibility.visibility);
3728 		}
3729 
3730 		if (obj->ctype.ai_info.danger_laser_num != object_none) {
3731 			auto &dobjp = *vcobjptr(obj->ctype.ai_info.danger_laser_num);
3732 			if ((dobjp.type == OBJ_WEAPON) && (dobjp.signature == obj->ctype.ai_info.danger_laser_signature))
3733 			{
3734 				fix circle_distance;
3735 				circle_distance = robptr.circle_distance[Difficulty_level] + ConsoleObject->size;
3736 				ai_move_relative_to_player(obj, ailp, dist_to_player, circle_distance, 1, player_visibility, player_info);
3737 			}
3738 		}
3739 
3740 		if (guidebot_should_fire_flare(vcobjptr, vcsegptr, vcwallptr, BuddyState, robptr, *obj))
3741 		{
3742 				Laser_create_new_easy( obj->orient.fvec, obj->pos, obj, weapon_id_type::FLARE_ID, 1);
3743 				ailp.next_fire = F1_0/2;
3744 				if (!BuddyState.Buddy_allowed_to_talk) // If buddy not talking, make him fire flares less often.
3745 					ailp.next_fire += d_rand()*4;
3746 		}
3747 	}
3748 
3749 	if (robot_is_thief(robptr)) {
3750 
3751 		compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3752 		do_thief_frame(obj, dist_to_player, player_visibility.visibility, vec_to_player);
3753 
3754 		if (ready_to_fire_any_weapon(robptr, ailp, 0)) {
3755 			if (openable_door_on_near_path(vmsegptr, vcwallptr, *obj, *aip))
3756 			{
3757 				// @mk, 05/08/95: Firing flare from center of object, this is dumb...
3758 				Laser_create_new_easy( obj->orient.fvec, obj->pos, obj, weapon_id_type::FLARE_ID, 1);
3759 				ailp.next_fire = F1_0/2;
3760 				if (LevelUniqueObjectState.ThiefState.Stolen_item_index == 0)     // If never stolen an item, fire flares less often (bad: Stolen_item_index wraps, but big deal)
3761 					ailp.next_fire += d_rand()*4;
3762 			}
3763 		}
3764 	}
3765 #endif
3766 
3767 	switch (ailp.mode)
3768 	{
3769 		case ai_mode::AIM_CHASE_OBJECT: {        // chasing player, sort of, chase if far, back off if close, circle in between
3770 			fix circle_distance;
3771 
3772 			circle_distance = robptr.circle_distance[Difficulty_level] + ConsoleObject->size;
3773 			// Green guy doesn't get his circle distance boosted, else he might never attack.
3774 			if (robptr.attack_type != 1)
3775 				circle_distance += (objnum&0xf) * F1_0/2;
3776 
3777 			compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3778 
3779 			// @mk, 12/27/94, structure here was strange.  Would do both clauses of what are now this if/then/else.  Used to be if/then, if/then.
3780 			if (player_visibility.visibility != player_visibility_state::visible_and_in_field_of_view && previous_visibility == player_visibility_state::visible_and_in_field_of_view)
3781 			{ // this is redundant: mk, 01/15/95: && (ailp->mode == ai_mode::AIM_CHASE_OBJECT))
3782 				if (!ai_multiplayer_awareness(obj, 53)) {
3783 					if (maybe_ai_do_actual_firing_stuff(obj))
3784 						ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
3785 					return;
3786 				}
3787 				create_path_to_believed_player_segment(obj, 8, create_path_safety_flag::safe);
3788 				ai_multi_send_robot_position(obj, -1);
3789 			} else if (!player_is_visible(player_visibility.visibility) && dist_to_player > F1_0 * 80 && !(Game_mode & GM_MULTI))
3790 			{
3791 				// If pretty far from the player, player cannot be seen
3792 				// (obstructed) and in chase mode, switch to follow path mode.
3793 				// This has one desirable benefit of avoiding physics retries.
3794 				if (aip->behavior == ai_behavior::AIB_STATION) {
3795 					ailp.goal_segment = aip->hide_segment;
3796 					create_path_to_station(obj, 15);
3797 				} // -- this looks like a dumb thing to do...robots following paths far away from you! else create_n_segment_path(obj, 5, -1);
3798 #if defined(DXX_BUILD_DESCENT_I)
3799 				else
3800 					create_n_segment_path(obj, 5, segment_none);
3801 #endif
3802 				break;
3803 			}
3804 
3805 			if ((aip->CURRENT_STATE == AIS_REST) && (aip->GOAL_STATE == AIS_REST)) {
3806 				const auto pv = player_visibility.visibility;
3807 				if (player_is_visible(pv))
3808 				{
3809 					const auto upv = static_cast<unsigned>(pv);
3810 					if (d_rand() < FrameTime * upv)
3811 						if (dist_to_player/256 < static_cast<fix>(d_rand() * upv))
3812 						{
3813 							aip->GOAL_STATE = AIS_SRCH;
3814 							aip->CURRENT_STATE = AIS_SRCH;
3815 						}
3816 				}
3817 			}
3818 
3819 			if (GameTime64 - ailp.time_player_seen > CHASE_TIME_LENGTH)
3820 			{
3821 
3822 				if (Game_mode & GM_MULTI)
3823 				{
3824 					if (!player_is_visible(player_visibility.visibility) && dist_to_player > F1_0 * 70)
3825 					{
3826 						ailp.mode = ai_mode::AIM_STILL;
3827 						return;
3828 					}
3829 				}
3830 
3831 				if (!ai_multiplayer_awareness(obj, 64)) {
3832 					if (maybe_ai_do_actual_firing_stuff(obj))
3833 						ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
3834 					return;
3835 				}
3836 #if defined(DXX_BUILD_DESCENT_I)
3837 				create_path_to_believed_player_segment(obj, 10, create_path_safety_flag::safe);
3838 				ai_multi_send_robot_position(obj, -1);
3839 #endif
3840 			} else if ((aip->CURRENT_STATE != AIS_REST) && (aip->GOAL_STATE != AIS_REST)) {
3841 				if (!ai_multiplayer_awareness(obj, 70)) {
3842 					if (maybe_ai_do_actual_firing_stuff(obj))
3843 						ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
3844 					return;
3845 				}
3846 				ai_move_relative_to_player(obj, ailp, dist_to_player, circle_distance, 0, player_visibility, player_info);
3847 
3848 				if ((obj_ref & 1) && ((aip->GOAL_STATE == AIS_SRCH) || (aip->GOAL_STATE == AIS_LOCK))) {
3849 					if (player_is_visible(player_visibility.visibility))
3850 						ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
3851 #if defined(DXX_BUILD_DESCENT_I)
3852 					else
3853 						ai_turn_randomly(vec_to_player, obj, robptr.turn_time[Difficulty_level], previous_visibility);
3854 #endif
3855 				}
3856 
3857 				ai_multi_send_robot_position(obj, ai_evaded ? (ai_evaded = 0, 1) : -1);
3858 
3859 				do_firing_stuff(obj, player_info.powerup_flags, player_visibility);
3860 			}
3861 			break;
3862 		}
3863 
3864 		case ai_mode::AIM_RUN_FROM_OBJECT:
3865 			compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3866 
3867 			{
3868 				const auto pv = player_visibility.visibility;
3869 				const auto player_is_in_line_of_sight = player_is_visible(pv);
3870 				if (player_is_in_line_of_sight)
3871 				{
3872 				if (ailp.player_awareness_type == player_awareness_type_t::PA_NONE)
3873 					ailp.player_awareness_type = player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION;
3874 				}
3875 
3876 			// If in multiplayer, only do if player visible.  If not multiplayer, do always.
3877 				if (!(Game_mode & GM_MULTI) || player_is_in_line_of_sight)
3878 				if (ai_multiplayer_awareness(obj, 75)) {
3879 					ai_follow_path(obj, player_visibility.visibility, &vec_to_player);
3880 					ai_multi_send_robot_position(obj, -1);
3881 				}
3882 
3883 			if (aip->GOAL_STATE != AIS_FLIN)
3884 				aip->GOAL_STATE = AIS_LOCK;
3885 			else if (aip->CURRENT_STATE == AIS_FLIN)
3886 				aip->GOAL_STATE = AIS_LOCK;
3887 
3888 			// Bad to let run_from robot fire at player because it
3889 			// will cause a war in which it turns towards the player
3890 			// to fire and then towards its goal to move.
3891 			// do_firing_stuff(obj, player_visibility, &vec_to_player);
3892 			// Instead, do this:
3893 			// (Note, only drop if player is visible.  This prevents
3894 			// the bombs from being a giveaway, and also ensures that
3895 			// the robot is moving while it is dropping.  Also means
3896 			// fewer will be dropped.)
3897 			if (ready_to_fire_weapon1(ailp, 0) && player_is_in_line_of_sight)
3898 			{
3899 				if (!ai_multiplayer_awareness(obj, 75))
3900 					return;
3901 
3902 				const auto fire_vec = vm_vec_negated(obj->orient.fvec);
3903 				const auto fire_pos = vm_vec_add(obj->pos, fire_vec);
3904 
3905 #if defined(DXX_BUILD_DESCENT_I)
3906 				ailp.next_fire = F1_0*5;		//	Drop a proximity bomb every 5 seconds.
3907 				const auto weapon_id =
3908 #elif defined(DXX_BUILD_DESCENT_II)
3909 				ailp.next_fire = (F1_0/2)*(NDL+5 - Difficulty_level);      // Drop a proximity bomb every 5 seconds.
3910 				const auto weapon_id = (aip->SUB_FLAGS & SUB_FLAGS_SPROX)
3911 					? weapon_id_type::ROBOT_SUPERPROX_ID
3912 					:
3913 #endif
3914 					weapon_id_type::PROXIMITY_ID;
3915 				Laser_create_new_easy(fire_vec, fire_pos, obj, weapon_id, 1);
3916 
3917 				if (Game_mode & GM_MULTI)
3918 				{
3919 					ai_multi_send_robot_position(obj, -1);
3920 					const int gun_num =
3921 #if defined(DXX_BUILD_DESCENT_II)
3922 						(aip->SUB_FLAGS & SUB_FLAGS_SPROX)
3923 						? -2
3924 						:
3925 #endif
3926 					-1;
3927 					multi_send_robot_fire(obj, gun_num, fire_vec);
3928 				}
3929 			}
3930 			}
3931 			break;
3932 
3933 #if defined(DXX_BUILD_DESCENT_II)
3934 		case ai_mode::AIM_GOTO_PLAYER:
3935 		case ai_mode::AIM_GOTO_OBJECT:
3936 			ai_follow_path(obj, player_visibility_state::visible_and_in_field_of_view, &vec_to_player);    // Follows path as if player can see robot.
3937 			ai_multi_send_robot_position(obj, -1);
3938 			break;
3939 #endif
3940 
3941 		case ai_mode::AIM_FOLLOW_PATH: {
3942 			int anger_level = 65;
3943 
3944 			if (aip->behavior == ai_behavior::AIB_STATION)
3945 			{
3946 				const std::size_t idx = aip->hide_index + aip->path_length - 1;
3947 				if (idx < Point_segs.size() && Point_segs[idx].segnum == aip->hide_segment)
3948 				{
3949 					anger_level = 64;
3950 				}
3951 			}
3952 
3953 			compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
3954 
3955 			if (!ai_multiplayer_awareness(obj, anger_level)) {
3956 				if (maybe_ai_do_actual_firing_stuff(obj)) {
3957 					ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
3958 				}
3959 				return;
3960 			}
3961 
3962 			ai_follow_path(obj, player_visibility.visibility, &vec_to_player);
3963 
3964 			if (aip->GOAL_STATE != AIS_FLIN)
3965 				aip->GOAL_STATE = AIS_LOCK;
3966 			else if (aip->CURRENT_STATE == AIS_FLIN)
3967 				aip->GOAL_STATE = AIS_LOCK;
3968 
3969 #if defined(DXX_BUILD_DESCENT_I)
3970 			if ((aip->behavior != ai_behavior::AIB_FOLLOW_PATH) && (aip->behavior != ai_behavior::AIB_RUN_FROM))
3971 				do_firing_stuff(obj, player_info.powerup_flags, player_visibility);
3972 
3973 			if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view && aip->behavior != ai_behavior::AIB_FOLLOW_PATH && aip->behavior != ai_behavior::AIB_RUN_FROM && get_robot_id(obj) != ROBOT_BRAIN)
3974 			{
3975 				if (robptr.attack_type == 0)
3976 					ailp.mode = ai_mode::AIM_CHASE_OBJECT;
3977 			}
3978 			else if (!player_is_visible(player_visibility.visibility)
3979 				&& (aip->behavior == ai_behavior::AIB_NORMAL)
3980 				&& (ailp.mode == ai_mode::AIM_FOLLOW_PATH)
3981 				&& (aip->behavior != ai_behavior::AIB_RUN_FROM))
3982 			{
3983 				ailp.mode = ai_mode::AIM_STILL;
3984 				aip->hide_index = -1;
3985 				aip->path_length = 0;
3986 			}
3987 #elif defined(DXX_BUILD_DESCENT_II)
3988 			if (aip->behavior != ai_behavior::AIB_RUN_FROM)
3989 				do_firing_stuff(obj, player_info.powerup_flags, player_visibility);
3990 
3991 			if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view &&
3992 				aip->behavior != ai_behavior::AIB_SNIPE &&
3993 				aip->behavior != ai_behavior::AIB_FOLLOW &&
3994 				aip->behavior != ai_behavior::AIB_RUN_FROM &&
3995 				get_robot_id(obj) != ROBOT_BRAIN &&
3996 				robot_is_companion(robptr) != 1 &&
3997 				robot_is_thief(robptr) != 1)
3998 			{
3999 				if (robptr.attack_type == 0)
4000 					ailp.mode = ai_mode::AIM_CHASE_OBJECT;
4001 				// This should not just be distance based, but also time-since-player-seen based.
4002 			}
4003 			else if ((dist_to_player > F1_0 * (20 * (2 * static_cast<int>(Difficulty_level) + robptr.pursuit)))
4004 				&& (GameTime64 - ailp.time_player_seen > (F1_0/2*(Difficulty_level + robptr.pursuit)))
4005 				&& !player_is_visible(player_visibility.visibility)
4006 				&& (aip->behavior == ai_behavior::AIB_NORMAL)
4007 				&& (ailp.mode == ai_mode::AIM_FOLLOW_PATH))
4008 			{
4009 				ailp.mode = ai_mode::AIM_STILL;
4010 				aip->hide_index = -1;
4011 				aip->path_length = 0;
4012 			}
4013 #endif
4014 
4015 			ai_multi_send_robot_position(obj, -1);
4016 
4017 			break;
4018 		}
4019 
4020 #if defined(DXX_BUILD_DESCENT_I)
4021 		case ai_mode::AIM_HIDE:
4022 #elif defined(DXX_BUILD_DESCENT_II)
4023 		case ai_mode::AIM_BEHIND:
4024 #endif
4025 			if (!ai_multiplayer_awareness(obj, 71)) {
4026 				if (maybe_ai_do_actual_firing_stuff(obj)) {
4027 					compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4028 					ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
4029 				}
4030 				return;
4031 			}
4032 
4033 			compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4034 
4035 #if defined(DXX_BUILD_DESCENT_I)
4036 			ai_follow_path(obj, player_visibility.visibility, nullptr);
4037 #elif defined(DXX_BUILD_DESCENT_II)
4038 			if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view)
4039 			{
4040 				// Get behind the player.
4041 				// Method:
4042 				// If vec_to_player dot player_rear_vector > 0, behind is goal.
4043 				// Else choose goal with larger dot from left, right.
4044 				vms_vector  goal_vector;
4045 				fix         dot;
4046 
4047 				dot = vm_vec_dot(ConsoleObject->orient.fvec, vec_to_player);
4048 				if (dot > 0) {          // Remember, we're interested in the rear vector dot being < 0.
4049 					goal_vector = vm_vec_negated(ConsoleObject->orient.fvec);
4050 				} else {
4051 					auto &orient = ConsoleObject->orient;
4052 					constexpr unsigned choice_count = 15;
4053 					const unsigned selector = (ConsoleObject->id ^ obj.get_unchecked_index() ^ ConsoleObject->segnum ^ obj->segnum) % (choice_count + 1);
4054 					if (selector == 0)
4055 						goal_vector = orient.rvec;
4056 					else if (selector == choice_count)
4057 						goal_vector = orient.uvec;
4058 					else
4059 					{
4060 						vm_vec_scale_add2(goal_vector, orient.rvec, (choice_count - selector) * F1_0);
4061 						vm_vec_copy_scale(goal_vector, orient.uvec, selector * F1_0);
4062 						vm_vec_normalize_quick(goal_vector);
4063 					}
4064 					if (vm_vec_dot(goal_vector, vec_to_player) > 0)
4065 						vm_vec_negate(goal_vector);
4066 				}
4067 
4068 				vm_vec_scale(goal_vector, 2*(ConsoleObject->size + obj->size + (((objnum*4 + d_tick_count) & 63) << 12)));
4069 				auto goal_point = vm_vec_add(ConsoleObject->pos, goal_vector);
4070 				vm_vec_scale_add2(goal_point, make_random_vector(), F1_0*8);
4071 				const auto vec_to_goal = vm_vec_normalized_quick(vm_vec_sub(goal_point, obj->pos));
4072 				move_towards_vector(obj, vec_to_goal, 0);
4073 				ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4074 				ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
4075 			}
4076 #endif
4077 
4078 			if (aip->GOAL_STATE != AIS_FLIN)
4079 				aip->GOAL_STATE = AIS_LOCK;
4080 			else if (aip->CURRENT_STATE == AIS_FLIN)
4081 				aip->GOAL_STATE = AIS_LOCK;
4082 
4083 			ai_multi_send_robot_position(obj, -1);
4084 			break;
4085 
4086 		case ai_mode::AIM_STILL:
4087 			if ((dist_to_player < F1_0 * 120 + static_cast<int>(Difficulty_level) * F1_0 * 20) || (static_cast<unsigned>(ailp.player_awareness_type) >= static_cast<unsigned>(player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION) - 1))
4088 			{
4089 				compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4090 
4091 				// turn towards vector if visible this time or last time, or rand
4092 				// new!
4093 #if defined(DXX_BUILD_DESCENT_I)
4094 				if (player_is_visible(player_visibility.visibility) || player_is_visible(previous_visibility) || ((d_rand() > 0x4000) && !(Game_mode & GM_MULTI)))
4095 #elif defined(DXX_BUILD_DESCENT_II)
4096 				if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view || previous_visibility == player_visibility_state::visible_and_in_field_of_view) // -- MK, 06/09/95:  || ((d_rand() > 0x4000) && !(Game_mode & GM_MULTI)))
4097 #endif
4098 				{
4099 					if (!ai_multiplayer_awareness(obj, 71)) {
4100 						if (maybe_ai_do_actual_firing_stuff(obj))
4101 							ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
4102 						return;
4103 					}
4104 					ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4105 					ai_multi_send_robot_position(obj, -1);
4106 				}
4107 
4108 				do_firing_stuff(obj, player_info.powerup_flags, player_visibility);
4109 #if defined(DXX_BUILD_DESCENT_I)
4110 				if (player_is_visible(player_visibility.visibility)) //	Change, MK, 01/03/94 for Multiplayer reasons.  If robots can't see you (even with eyes on back of head), then don't do evasion.
4111 #elif defined(DXX_BUILD_DESCENT_II)
4112 				if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view) // Changed @mk, 09/21/95: Require that they be looking to evade.  Change, MK, 01/03/95 for Multiplayer reasons.  If robots can't see you (even with eyes on back of head), then don't do evasion.
4113 #endif
4114 				{
4115 					if (robptr.attack_type == 1) {
4116 						aip->behavior = ai_behavior::AIB_NORMAL;
4117 						if (!ai_multiplayer_awareness(obj, 80)) {
4118 							if (maybe_ai_do_actual_firing_stuff(obj))
4119 								ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
4120 							return;
4121 						}
4122 						ai_move_relative_to_player(obj, ailp, dist_to_player, 0, 0, player_visibility, player_info);
4123 						ai_multi_send_robot_position(obj, ai_evaded ? (ai_evaded = 0, 1) : -1);
4124 					} else {
4125 						// Robots in hover mode are allowed to evade at half normal speed.
4126 						if (!ai_multiplayer_awareness(obj, 81)) {
4127 							if (maybe_ai_do_actual_firing_stuff(obj))
4128 								ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
4129 							return;
4130 						}
4131 						ai_move_relative_to_player(obj, ailp, dist_to_player, 0, 1, player_visibility, player_info);
4132 						if (ai_evaded) {
4133 							ai_multi_send_robot_position(obj, -1);
4134 							ai_evaded = 0;
4135 						}
4136 						else
4137 							ai_multi_send_robot_position(obj, -1);
4138 					}
4139 				} else if ((obj->segnum != aip->hide_segment) && (dist_to_player > F1_0*80) && (!(Game_mode & GM_MULTI))) {
4140 					// If pretty far from the player, player cannot be
4141 					// seen (obstructed) and in chase mode, switch to
4142 					// follow path mode.
4143 					// This has one desirable benefit of avoiding physics retries.
4144 					if (aip->behavior == ai_behavior::AIB_STATION) {
4145 						ailp.goal_segment = aip->hide_segment;
4146 						create_path_to_station(obj, 15);
4147 					}
4148 					break;
4149 				}
4150 			}
4151 
4152 			break;
4153 		case ai_mode::AIM_OPEN_DOOR: {       // trying to open a door.
4154 			Assert(get_robot_id(obj) == ROBOT_BRAIN);     // Make sure this guy is allowed to be in this mode.
4155 
4156 			if (!ai_multiplayer_awareness(obj, 62))
4157 				return;
4158 			auto &vcvertptr = Vertices.vcptr;
4159 			const auto &&center_point = compute_center_point_on_side(vcvertptr, vcsegptr(obj->segnum), aip->GOALSIDE);
4160 			const auto goal_vector = vm_vec_normalized_quick(vm_vec_sub(center_point, obj->pos));
4161 			ai_turn_towards_vector(goal_vector, obj, robptr.turn_time[Difficulty_level]);
4162 			move_towards_vector(obj, goal_vector, 0);
4163 			ai_multi_send_robot_position(obj, -1);
4164 
4165 			break;
4166 		}
4167 
4168 #if defined(DXX_BUILD_DESCENT_II)
4169 		case ai_mode::AIM_SNIPE_WAIT:
4170 			break;
4171 		case ai_mode::AIM_SNIPE_RETREAT:
4172 			// -- if (ai_multiplayer_awareness(obj, 53))
4173 			// -- 	if (ailp->next_fire < -F1_0)
4174 			// -- 		ai_do_actual_firing_stuff(obj, aip, ailp, robptr, &vec_to_player, dist_to_player, &gun_point, player_visibility, object_animates, aip->CURRENT_GUN);
4175 			break;
4176 		case ai_mode::AIM_SNIPE_RETREAT_BACKWARDS:
4177 		case ai_mode::AIM_SNIPE_ATTACK:
4178 		case ai_mode::AIM_SNIPE_FIRE:
4179 			if (ai_multiplayer_awareness(obj, 53)) {
4180 				ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
4181 				if (robot_is_thief(robptr))
4182 					ai_move_relative_to_player(obj, ailp, dist_to_player, 0, 0, player_visibility, player_info);
4183 				break;
4184 			}
4185 			break;
4186 
4187 		case ai_mode::AIM_THIEF_WAIT:
4188 		case ai_mode::AIM_THIEF_ATTACK:
4189 		case ai_mode::AIM_THIEF_RETREAT:
4190 		case ai_mode::AIM_WANDER:    // Used for Buddy Bot
4191 			break;
4192 #endif
4193 
4194 		default:
4195 			ailp.mode = ai_mode::AIM_CHASE_OBJECT;
4196 			break;
4197 	}       // end: switch (ailp->mode)
4198 
4199 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
4200 	// If the robot can see you, increase his awareness of you.
4201 	// This prevents the problem of a robot looking right at you but doing nothing.
4202 	// Assert(player_visibility != -1); // Means it didn't get initialized!
4203 	compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4204 	if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view)
4205 	{
4206 #if defined(DXX_BUILD_DESCENT_I)
4207 		if (ailp.player_awareness_type == player_awareness_type_t::PA_NONE)
4208 			ailp.player_awareness_type = player_awareness_type_t::PA_PLAYER_COLLISION;
4209 #elif defined(DXX_BUILD_DESCENT_II)
4210 	if (aip->behavior != ai_behavior::AIB_FOLLOW && !robot_is_thief(robptr))
4211 	{
4212 		if (ailp.player_awareness_type == player_awareness_type_t::PA_NONE && (aip->SUB_FLAGS & SUB_FLAGS_CAMERA_AWAKE))
4213 			aip->SUB_FLAGS &= ~SUB_FLAGS_CAMERA_AWAKE;
4214 		else if (ailp.player_awareness_type == player_awareness_type_t::PA_NONE)
4215 			ailp.player_awareness_type = player_awareness_type_t::PA_PLAYER_COLLISION;
4216 	}
4217 #endif
4218 	}
4219 
4220 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
4221 	if (!object_animates) {
4222 		aip->CURRENT_STATE = aip->GOAL_STATE;
4223 	}
4224 
4225 	assert(static_cast<unsigned>(ailp.player_awareness_type) <= AIE_MAX);
4226 	Assert(aip->CURRENT_STATE < AIS_MAX);
4227 	Assert(aip->GOAL_STATE < AIS_MAX);
4228 
4229 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
4230 	if (ailp.player_awareness_type != player_awareness_type_t::PA_NONE) {
4231 		new_goal_state = Ai_transition_table[static_cast<unsigned>(ailp.player_awareness_type) - 1][aip->CURRENT_STATE][aip->GOAL_STATE];
4232 		if (ailp.player_awareness_type == player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION)
4233 		{
4234 			// Decrease awareness, else this robot will flinch every frame.
4235 			ailp.player_awareness_type = static_cast<player_awareness_type_t>(static_cast<unsigned>(ailp.player_awareness_type) - 1);
4236 			ailp.player_awareness_time = F1_0*3;
4237 		}
4238 
4239 		if (new_goal_state == AIS_ERR_)
4240 			new_goal_state = AIS_REST;
4241 
4242 		if (aip->CURRENT_STATE == AIS_NONE)
4243 			aip->CURRENT_STATE = AIS_REST;
4244 
4245 		aip->GOAL_STATE = new_goal_state;
4246 
4247 	}
4248 
4249 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
4250 	// If new state = fire, then set all gun states to fire.
4251 	if (aip->GOAL_STATE == AIS_FIRE)
4252 	{
4253 		uint_fast32_t num_guns = robptr.n_guns;
4254 		for (uint_fast32_t i=0; i<num_guns; i++)
4255 			ailp.goal_state[i] = AIS_FIRE;
4256 	}
4257 
4258 	// - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  - -  -
4259 	// Hack by mk on 01/04/94, if a guy hasn't animated to the firing state, but his next_fire says ok to fire, bash him there
4260 	if (ready_to_fire_any_weapon(robptr, ailp, 0) && (aip->GOAL_STATE == AIS_FIRE))
4261 		aip->CURRENT_STATE = AIS_FIRE;
4262 
4263 	if ((aip->GOAL_STATE != AIS_FLIN)  && (get_robot_id(obj) != ROBOT_BRAIN)) {
4264 		switch (aip->CURRENT_STATE) {
4265 			case AIS_NONE:
4266 				compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4267 
4268 				{
4269 				const fix dot = vm_vec_dot(obj->orient.fvec, vec_to_player);
4270 				if (dot >= F1_0/2)
4271 					if (aip->GOAL_STATE == AIS_REST)
4272 						aip->GOAL_STATE = AIS_SRCH;
4273 				}
4274 				break;
4275 			case AIS_REST:
4276 				if (aip->GOAL_STATE == AIS_REST) {
4277 					compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4278 					if (ready_to_fire_any_weapon(robptr, ailp, 0) && player_is_visible(player_visibility.visibility))
4279 					{
4280 						aip->GOAL_STATE = AIS_FIRE;
4281 					}
4282 				}
4283 				break;
4284 			case AIS_SRCH:
4285 				if (!ai_multiplayer_awareness(obj, 60))
4286 					return;
4287 
4288 				compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4289 
4290 #if defined(DXX_BUILD_DESCENT_I)
4291 				if (player_is_visible(player_visibility.visibility))
4292 				{
4293 					ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4294 					ai_multi_send_robot_position(obj, -1);
4295 				} else if (!(Game_mode & GM_MULTI))
4296 					ai_turn_randomly(vec_to_player, obj, robptr.turn_time[Difficulty_level], previous_visibility);
4297 #elif defined(DXX_BUILD_DESCENT_II)
4298 				if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view)
4299 				{
4300 					ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4301 					ai_multi_send_robot_position(obj, -1);
4302 				}
4303 #endif
4304 				break;
4305 			case AIS_LOCK:
4306 				compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4307 
4308 				if (!(Game_mode & GM_MULTI) || player_is_visible(player_visibility.visibility))
4309 				{
4310 					if (!ai_multiplayer_awareness(obj, 68))
4311 						return;
4312 
4313 #if defined(DXX_BUILD_DESCENT_I)
4314 					if (player_is_visible(player_visibility.visibility))
4315 					{
4316 						ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4317 						ai_multi_send_robot_position(obj, -1);
4318 					}
4319 					else if (!(Game_mode & GM_MULTI))
4320 						ai_turn_randomly(vec_to_player, obj, robptr.turn_time[Difficulty_level], previous_visibility);
4321 #elif defined(DXX_BUILD_DESCENT_II)
4322 					if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view)
4323 					{   // @mk, 09/21/95, require that they be looking towards you to turn towards you.
4324 						ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4325 						ai_multi_send_robot_position(obj, -1);
4326 					}
4327 #endif
4328 				}
4329 				break;
4330 			case AIS_FIRE:
4331 				compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4332 
4333 #if defined(DXX_BUILD_DESCENT_I)
4334 				if (player_is_visible(player_visibility.visibility))
4335 				{
4336 					if (!ai_multiplayer_awareness(obj, (ROBOT_FIRE_AGITATION-1)))
4337 					{
4338 						if (Game_mode & GM_MULTI) {
4339 							ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, 0);
4340 							return;
4341 						}
4342 					}
4343 					ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4344 					ai_multi_send_robot_position(obj, -1);
4345 				} else if (!(Game_mode & GM_MULTI)) {
4346 					ai_turn_randomly(vec_to_player, obj, robptr.turn_time[Difficulty_level], previous_visibility);
4347 				}
4348 #elif defined(DXX_BUILD_DESCENT_II)
4349 				if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view)
4350 				{
4351 					if (!ai_multiplayer_awareness(obj, (ROBOT_FIRE_AGITATION-1))) {
4352 						if (Game_mode & GM_MULTI) {
4353 							ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
4354 							return;
4355 						}
4356 					}
4357 					ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4358 					ai_multi_send_robot_position(obj, -1);
4359 				}
4360 #endif
4361 
4362 				// Fire at player, if appropriate.
4363 				ai_do_actual_firing_stuff(vmobjptridx, obj, aip, ailp, robptr, dist_to_player, gun_point, player_visibility, object_animates, player_info, aip->CURRENT_GUN);
4364 
4365 				break;
4366 			case AIS_RECO:
4367 				if (!(obj_ref & 3)) {
4368 					compute_vis_and_vec(obj, player_info, vis_vec_pos, ailp, player_visibility, robptr);
4369 #if defined(DXX_BUILD_DESCENT_I)
4370 					if (player_is_visible(player_visibility.visibility))
4371 					{
4372 						if (!ai_multiplayer_awareness(obj, 69))
4373 							return;
4374 						ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4375 						ai_multi_send_robot_position(obj, -1);
4376 					}
4377 					else if (!(Game_mode & GM_MULTI)) {
4378 						ai_turn_randomly(vec_to_player, obj, robptr.turn_time[Difficulty_level], previous_visibility);
4379 					}
4380 #elif defined(DXX_BUILD_DESCENT_II)
4381 					if (player_visibility.visibility == player_visibility_state::visible_and_in_field_of_view)
4382 					{
4383 						if (!ai_multiplayer_awareness(obj, 69))
4384 							return;
4385 						ai_turn_towards_vector(vec_to_player, obj, robptr.turn_time[Difficulty_level]);
4386 						ai_multi_send_robot_position(obj, -1);
4387 					}
4388 #endif
4389 				}
4390 				break;
4391 			case AIS_FLIN:
4392 				break;
4393 			default:
4394 				aip->GOAL_STATE = AIS_REST;
4395 				aip->CURRENT_STATE = AIS_REST;
4396 				break;
4397 		}
4398 	} // end of: if (aip->GOAL_STATE != AIS_FLIN)
4399 
4400 	// Switch to next gun for next fire.
4401 	if (!player_is_visible(player_visibility.visibility))
4402 	{
4403 		aip->CURRENT_GUN++;
4404 		if (aip->CURRENT_GUN >= robptr.n_guns)
4405 		{
4406 #if defined(DXX_BUILD_DESCENT_II)
4407 			if (!((robptr.n_guns == 1) || (robptr.weapon_type2 == weapon_none)))  // Two weapon types hack.
4408 				aip->CURRENT_GUN = 1;
4409 			else
4410 #endif
4411 				aip->CURRENT_GUN = 0;
4412 		}
4413 	}
4414 
4415 }
4416 
4417 // ----------------------------------------------------------------------------
ai_do_cloak_stuff(void)4418 void ai_do_cloak_stuff(void)
4419 {
4420 	range_for (auto &i, Ai_cloak_info) {
4421 		i.last_time = GameTime64;
4422 #if defined(DXX_BUILD_DESCENT_II)
4423 		i.last_segment = ConsoleObject->segnum;
4424 #endif
4425 		i.last_position = ConsoleObject->pos;
4426 	}
4427 
4428 	// Make work for control centers.
4429 	Believed_player_pos = Ai_cloak_info[0].last_position;
4430 #if defined(DXX_BUILD_DESCENT_II)
4431 	Believed_player_seg = Ai_cloak_info[0].last_segment;
4432 #endif
4433 }
4434 
4435 // --------------------------------------------------------------------------------------------------------------------
4436 //	Call this each time the player starts a new ship.
init_ai_for_ship()4437 void init_ai_for_ship()
4438 {
4439 	ai_do_cloak_stuff();
4440 }
4441 
4442 namespace {
4443 
4444 // ----------------------------------------------------------------------------
4445 // Returns false if awareness is considered too puny to add, else returns true.
add_awareness_event(const object_base & objp,player_awareness_type_t type,d_level_unique_robot_awareness_state & awareness)4446 static int add_awareness_event(const object_base &objp, player_awareness_type_t type, d_level_unique_robot_awareness_state &awareness)
4447 {
4448 	// If player cloaked and hit a robot, then increase awareness
4449 	if (type == player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION ||
4450 		type == player_awareness_type_t::PA_WEAPON_WALL_COLLISION ||
4451 		type == player_awareness_type_t::PA_PLAYER_COLLISION)
4452 		ai_do_cloak_stuff();
4453 
4454 	if (awareness.Num_awareness_events < awareness.Awareness_events.size())
4455 	{
4456 		if (type == player_awareness_type_t::PA_WEAPON_WALL_COLLISION ||
4457 			type == player_awareness_type_t::PA_WEAPON_ROBOT_COLLISION)
4458 			if (objp.type == OBJ_WEAPON && get_weapon_id(objp) == weapon_id_type::VULCAN_ID)
4459 				if (d_rand() > 3276)
4460 					return 0;       // For vulcan cannon, only about 1/10 actually cause awareness
4461 
4462 		auto &e = awareness.Awareness_events[awareness.Num_awareness_events++];
4463 		e.segnum = objp.segnum;
4464 		e.pos = objp.pos;
4465 		e.type = type;
4466 	} else {
4467 		//Int3();   // Hey -- Overflowed Awareness_events, make more or something
4468 		// This just gets ignored, so you can just
4469 		// continue.
4470 	}
4471 	return 1;
4472 
4473 }
4474 
4475 }
4476 
4477 // ----------------------------------------------------------------------------------
4478 // Robots will become aware of the player based on something that occurred.
4479 // The object (probably player or weapon) which created the awareness is objp.
create_awareness_event(object & objp,player_awareness_type_t type,d_level_unique_robot_awareness_state & LevelUniqueRobotAwarenessState)4480 void create_awareness_event(object &objp, player_awareness_type_t type, d_level_unique_robot_awareness_state &LevelUniqueRobotAwarenessState)
4481 {
4482 	// If not in multiplayer, or in multiplayer with robots, do this, else unnecessary!
4483 	if (!(Game_mode & GM_MULTI) || (Game_mode & GM_MULTI_ROBOTS))
4484 	{
4485 		if (add_awareness_event(objp, type, LevelUniqueRobotAwarenessState))
4486 		{
4487 			if (((d_rand() * (static_cast<unsigned>(type) + 4)) >> 15) > 4)
4488 				Overall_agitation++;
4489 			if (Overall_agitation > OVERALL_AGITATION_MAX)
4490 				Overall_agitation = OVERALL_AGITATION_MAX;
4491 		}
4492 	}
4493 }
4494 
4495 namespace {
4496 
4497 // ----------------------------------------------------------------------------------
process_awareness_events(fvcsegptridx & vcsegptridx,d_level_unique_robot_awareness_state & LevelUniqueRobotAwarenessState,awareness_t & New_awareness)4498 static unsigned process_awareness_events(fvcsegptridx &vcsegptridx, d_level_unique_robot_awareness_state &LevelUniqueRobotAwarenessState, awareness_t &New_awareness)
4499 {
4500 	unsigned result = 0;
4501 	if (!(Game_mode & GM_MULTI) || (Game_mode & GM_MULTI_ROBOTS))
4502 	{
4503 		const auto Num_awareness_events = std::exchange(LevelUniqueRobotAwarenessState.Num_awareness_events, 0);
4504 		if (!Num_awareness_events)
4505 			return Num_awareness_events;
4506 		result = Num_awareness_events;
4507 		New_awareness.fill(player_awareness_type_t::PA_NONE);
4508 		const unsigned allowed_recursions_remaining =
4509 #if defined(DXX_BUILD_DESCENT_II)
4510 			!EMULATING_D1 ? 3 :
4511 #endif
4512 			4;
4513 		range_for (auto &i, partial_const_range(LevelUniqueRobotAwarenessState.Awareness_events, Num_awareness_events))
4514 			pae_aux(vcsegptridx(i.segnum), i.type, New_awareness, allowed_recursions_remaining);
4515 	}
4516 	return result;
4517 }
4518 
4519 // ----------------------------------------------------------------------------------
set_player_awareness_all(fvmobjptr & vmobjptr,fvcsegptridx & vcsegptridx,d_level_unique_robot_awareness_state & LevelUniqueRobotAwarenessState)4520 static void set_player_awareness_all(fvmobjptr &vmobjptr, fvcsegptridx &vcsegptridx, d_level_unique_robot_awareness_state &LevelUniqueRobotAwarenessState)
4521 {
4522 	awareness_t New_awareness;
4523 
4524 	if (!process_awareness_events(vcsegptridx, LevelUniqueRobotAwarenessState, New_awareness))
4525 		return;
4526 
4527 	range_for (const auto &&objp, vmobjptr)
4528 	{
4529 		object &obj = objp;
4530 		if (obj.type == OBJ_ROBOT && obj.control_source == object::control_type::ai)
4531 		{
4532 			auto &ailp = obj.ctype.ai_info.ail;
4533 			auto &na = New_awareness[obj.segnum];
4534 			if (ailp.player_awareness_type < na)
4535 			{
4536 				ailp.player_awareness_type = na;
4537 				ailp.player_awareness_time = PLAYER_AWARENESS_INITIAL_TIME;
4538 
4539 #if defined(DXX_BUILD_DESCENT_II)
4540 			// Clear the bit that says this robot is only awake because a camera woke it up.
4541 				obj.ctype.ai_info.SUB_FLAGS &= ~SUB_FLAGS_CAMERA_AWAKE;
4542 #endif
4543 			}
4544 		}
4545 	}
4546 }
4547 
4548 }
4549 
4550 }
4551 
4552 #ifndef NDEBUG
4553 #if PARALLAX
4554 int Ai_dump_enable = 0;
4555 
4556 FILE *Ai_dump_file = NULL;
4557 
4558 char Ai_error_message[128] = "";
4559 
4560 // ----------------------------------------------------------------------------------
4561 namespace dsx {
4562 namespace {
dump_ai_objects_all()4563 static void dump_ai_objects_all()
4564 {
4565 #if defined(DXX_BUILD_DESCENT_I)
4566 	int	total=0;
4567 	time_t	time_of_day;
4568 
4569 	time_of_day = time(NULL);
4570 
4571 	if (!Ai_dump_enable)
4572 		return;
4573 
4574 	if (Ai_dump_file == NULL)
4575 		Ai_dump_file = fopen("ai.out","a+t");
4576 
4577 	fprintf(Ai_dump_file, "\nnum: seg distance __mode__ behav.    [velx vely velz] (Tick = %i)\n", d_tick_count);
4578 	fprintf(Ai_dump_file, "Date & Time = %s\n", ctime(&time_of_day));
4579 
4580 	if (Ai_error_message[0])
4581 		fprintf(Ai_dump_file, "Error message: %s\n", Ai_error_message);
4582 
4583 	range_for (const auto &&objp, vcobjptridx)
4584 	{
4585 		ai_static	*aip = &objp->ctype.ai_info;
4586 		ai_local		*ailp = &objp->ctype.ai_info.ail;
4587 		fix			dist_to_player;
4588 
4589 		dist_to_player = vm_vec_dist(objp->pos, ConsoleObject->pos);
4590 
4591 		if (objp->control_source == object::control_type::ai)
4592 		{
4593 			fprintf(Ai_dump_file, "%3i: %3i %8.3f %8s %8s [%3i %4i]\n",
4594 				static_cast<uint16_t>(objp), objp->segnum, f2fl(dist_to_player), mode_text[ailp->mode], behavior_text[aip->behavior-0x80], aip->hide_index, aip->path_length);
4595 			if (aip->path_length)
4596 				total += aip->path_length;
4597 		}
4598 	}
4599 
4600 	fprintf(Ai_dump_file, "Total path length = %4i\n", total);
4601 #endif
4602 }
4603 }
4604 }
4605 
force_dump_ai_objects_all(const char * msg)4606 void force_dump_ai_objects_all(const char *msg)
4607 {
4608 	int tsave;
4609 
4610 	tsave = Ai_dump_enable;
4611 
4612 	Ai_dump_enable = 1;
4613 
4614 	sprintf(Ai_error_message, "%s\n", msg);
4615 	dump_ai_objects_all();
4616 	Ai_error_message[0] = 0;
4617 
4618 	Ai_dump_enable = tsave;
4619 }
4620 
4621 // ----------------------------------------------------------------------------------
4622 #else
dump_ai_objects_all()4623 static inline void dump_ai_objects_all()
4624 {
4625 }
4626 #endif
4627 #endif
4628 
4629 namespace dsx {
4630 
4631 // ----------------------------------------------------------------------------------
4632 // Do things which need to get done for all AI objects each frame.
4633 // This includes:
4634 //  Setting player_awareness (a fix, time in seconds which object is aware of player)
do_ai_frame_all(void)4635 void do_ai_frame_all(void)
4636 {
4637 	auto &Objects = LevelUniqueObjectState.Objects;
4638 	auto &vmobjptr = Objects.vmptr;
4639 #ifndef NDEBUG
4640 	dump_ai_objects_all();
4641 #endif
4642 
4643 	set_player_awareness_all(vmobjptr, vcsegptridx, LevelUniqueRobotAwarenessState);
4644 
4645 #if defined(DXX_BUILD_DESCENT_II)
4646 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
4647 	auto &vmobjptridx = Objects.vmptridx;
4648 	if (Ai_last_missile_camera)
4649 	{
4650 		// Clear if supposed misisle camera is not a weapon, or just every so often, just in case.
4651 		if (((d_tick_count & 0x0f) == 0) || (Ai_last_missile_camera->type != OBJ_WEAPON)) {
4652 			Ai_last_missile_camera = nullptr;
4653 			range_for (const auto &&objp, vmobjptr)
4654 			{
4655 				if (objp->type == OBJ_ROBOT)
4656 					objp->ctype.ai_info.SUB_FLAGS &= ~SUB_FLAGS_CAMERA_AWAKE;
4657 			}
4658 		}
4659 	}
4660 
4661 	// (Moved here from do_d2_boss_stuff() because that only gets called if robot aware of player.)
4662 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
4663 	if (BossUniqueState.Boss_dying) {
4664 		range_for (const auto &&objp, vmobjptridx)
4665 		{
4666 			if (objp->type == OBJ_ROBOT)
4667 				if (Robot_info[get_robot_id(objp)].boss_flag)
4668 					do_boss_dying_frame(objp);
4669 		}
4670 	}
4671 #endif
4672 }
4673 
4674 
4675 // Initializations to be performed for all robots for a new level.
init_robots_for_level(void)4676 void init_robots_for_level(void)
4677 {
4678 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
4679 	BossUniqueState.Boss_dying_start_time = 0;
4680 	Overall_agitation = 0;
4681 #if defined(DXX_BUILD_DESCENT_II)
4682 	GameUniqueState.Final_boss_countdown_time = 0;
4683 	Ai_last_missile_camera = nullptr;
4684 #endif
4685 }
4686 
4687 namespace {
4688 
4689 // Following functions convert ai_local/ai_cloak_info to ai_local/ai_cloak_info_rw to be written to/read from Savegames. Convertin back is not done here - reading is done specifically together with swapping (if necessary). These structs differ in terms of timer values (fix/fix64). as we reset GameTime64 for writing so it can fit into fix it's not necessary to increment savegame version. But if we once store something else into object which might be useful after restoring, it might be handy to increment Savegame version and actually store these new infos.
state_ai_local_to_ai_local_rw(const ai_local * ail,ai_local_rw * ail_rw)4690 static void state_ai_local_to_ai_local_rw(const ai_local *ail, ai_local_rw *ail_rw)
4691 {
4692 	int i = 0;
4693 
4694 	ail_rw->player_awareness_type      = static_cast<int8_t>(ail->player_awareness_type);
4695 	ail_rw->retry_count                = ail->retry_count;
4696 	ail_rw->consecutive_retries        = ail->consecutive_retries;
4697 	ail_rw->mode                       = static_cast<uint8_t>(ail->mode);
4698 	ail_rw->previous_visibility        = static_cast<int8_t>(ail->previous_visibility);
4699 	ail_rw->rapidfire_count            = ail->rapidfire_count;
4700 	ail_rw->goal_segment               = ail->goal_segment;
4701 #if defined(DXX_BUILD_DESCENT_I)
4702 	ail_rw->last_see_time              = 0;
4703 	ail_rw->last_attack_time           = 0;
4704 #elif defined(DXX_BUILD_DESCENT_II)
4705 	ail_rw->next_fire2                 = ail->next_fire2;
4706 #endif
4707 	ail_rw->next_action_time           = ail->next_action_time;
4708 	ail_rw->next_fire                  = ail->next_fire;
4709 	ail_rw->player_awareness_time      = ail->player_awareness_time;
4710 	const auto gametime = GameTime64;
4711 	ail_rw->time_player_seen = build_savegametime_from_gametime(gametime, ail->time_player_seen);
4712 	ail_rw->time_player_sound_attacked = build_savegametime_from_gametime(gametime, ail->time_player_sound_attacked);
4713 	ail_rw->next_misc_sound_time = build_savegametime_from_gametime(gametime, ail->next_misc_sound_time);
4714 	ail_rw->time_since_processed       = ail->time_since_processed;
4715 	for (i = 0; i < MAX_SUBMODELS; i++)
4716 	{
4717 		ail_rw->goal_angles[i].p   = ail->goal_angles[i].p;
4718 		ail_rw->goal_angles[i].b   = ail->goal_angles[i].b;
4719 		ail_rw->goal_angles[i].h   = ail->goal_angles[i].h;
4720 		ail_rw->delta_angles[i].p  = ail->delta_angles[i].p;
4721 		ail_rw->delta_angles[i].b  = ail->delta_angles[i].b;
4722 		ail_rw->delta_angles[i].h  = ail->delta_angles[i].h;
4723 		ail_rw->goal_state[i]      = ail->goal_state[i];
4724 		ail_rw->achieved_state[i]  = ail->achieved_state[i];
4725 	}
4726 }
4727 
state_ai_cloak_info_to_ai_cloak_info_rw(const ai_cloak_info * const aic,ai_cloak_info_rw * const aic_rw)4728 static void state_ai_cloak_info_to_ai_cloak_info_rw(const ai_cloak_info *const aic, ai_cloak_info_rw *const aic_rw)
4729 {
4730 	aic_rw->last_time = build_savegametime_from_gametime(GameTime64, aic->last_time);
4731 #if defined(DXX_BUILD_DESCENT_II)
4732 	aic_rw->last_segment    = aic->last_segment;
4733 #endif
4734 	aic_rw->last_position.x = aic->last_position.x;
4735 	aic_rw->last_position.y = aic->last_position.y;
4736 	aic_rw->last_position.z = aic->last_position.z;
4737 }
4738 
4739 }
4740 
4741 }
4742 
4743 namespace dcx {
4744 
4745 DEFINE_SERIAL_VMS_VECTOR_TO_MESSAGE();
4746 DEFINE_SERIAL_UDT_TO_MESSAGE(point_seg, p, (p.segnum, serial::pad<2>(), p.point));
4747 ASSERT_SERIAL_UDT_MESSAGE_SIZE(point_seg, 16);
4748 
4749 DEFINE_SERIAL_MUTABLE_UDT_TO_MESSAGE(point_seg_array_t, p, (static_cast<std::array<point_seg, MAX_POINT_SEGS> &>(p)));
4750 DEFINE_SERIAL_CONST_UDT_TO_MESSAGE(point_seg_array_t, p, (static_cast<const std::array<point_seg, MAX_POINT_SEGS> &>(p)));
4751 
4752 }
4753 
4754 namespace dsx {
4755 
ai_save_state(PHYSFS_File * fp)4756 int ai_save_state(PHYSFS_File *fp)
4757 {
4758 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
4759 #if defined(DXX_BUILD_DESCENT_II)
4760 	auto &Boss_gate_segs = LevelSharedBossState.Gate_segs;
4761 	auto &Boss_teleport_segs = LevelSharedBossState.Teleport_segs;
4762 	auto &BuddyState = LevelUniqueObjectState.BuddyState;
4763 #endif
4764 	auto &Objects = LevelUniqueObjectState.Objects;
4765 	fix tmptime32 = 0;
4766 
4767 	const int Ai_initialized = 0;
4768 	PHYSFS_write(fp, &Ai_initialized, sizeof(int), 1);
4769 	PHYSFS_write(fp, &Overall_agitation, sizeof(int), 1);
4770 	{
4771 		ai_local_rw zero{};
4772 		range_for (const auto &i, Objects)
4773 		{
4774 			ai_local_rw ail_rw;
4775 			PHYSFS_write(fp, i.type == OBJ_ROBOT ? (state_ai_local_to_ai_local_rw(&i.ctype.ai_info.ail, &ail_rw), &ail_rw) : &zero, sizeof(ail_rw), 1);
4776 		}
4777 	}
4778 	PHYSFSX_serialize_write(fp, Point_segs);
4779 	//PHYSFS_write(fp, Ai_cloak_info, sizeof(ai_cloak_info) * MAX_AI_CLOAK_INFO, 1);
4780 	range_for (auto &i, Ai_cloak_info)
4781 	{
4782 		ai_cloak_info_rw aic_rw;
4783 		state_ai_cloak_info_to_ai_cloak_info_rw(&i, &aic_rw);
4784 		PHYSFS_write(fp, &aic_rw, sizeof(aic_rw), 1);
4785 	}
4786 	const auto gametime = GameTime64;
4787 	{
4788 		const auto Boss_cloak_start_time = BossUniqueState.Boss_cloak_start_time;
4789 		tmptime32 = build_savegametime_from_gametime(gametime, Boss_cloak_start_time);
4790 	PHYSFS_write(fp, &tmptime32, sizeof(fix), 1);
4791 		tmptime32 = build_savegametime_from_gametime(gametime, Boss_cloak_start_time + Boss_cloak_duration);
4792 	PHYSFS_write(fp, &tmptime32, sizeof(fix), 1);
4793 	}
4794 	{
4795 		const auto Last_teleport_time = BossUniqueState.Last_teleport_time;
4796 		tmptime32 = build_savegametime_from_gametime(gametime, Last_teleport_time);
4797 	PHYSFS_write(fp, &tmptime32, sizeof(fix), 1);
4798 	}
4799 	{
4800 		const fix Boss_teleport_interval = LevelSharedBossState.Boss_teleport_interval;
4801 		PHYSFS_write(fp, &Boss_teleport_interval, sizeof(fix), 1);
4802 		const fix Boss_cloak_interval = LevelSharedBossState.Boss_cloak_interval;
4803 		PHYSFS_write(fp, &Boss_cloak_interval, sizeof(fix), 1);
4804 	}
4805 	PHYSFS_write(fp, &Boss_cloak_duration, sizeof(fix), 1);
4806 	tmptime32 = build_savegametime_from_gametime(gametime, BossUniqueState.Last_gate_time);
4807 	PHYSFS_write(fp, &tmptime32, sizeof(fix), 1);
4808 	PHYSFS_write(fp, &GameUniqueState.Boss_gate_interval, sizeof(fix), 1);
4809 	{
4810 		const auto Boss_dying_start_time = BossUniqueState.Boss_dying_start_time;
4811 	if (Boss_dying_start_time == 0) // if Boss not dead, yet we expect this to be 0, so do not convert!
4812 	{
4813 		tmptime32 = 0;
4814 	}
4815 	else
4816 	{
4817 		tmptime32 = build_savegametime_from_gametime(gametime, Boss_dying_start_time);
4818 		if (tmptime32 == 0) // now if our converted value went 0 we should do something against it
4819 			tmptime32 = -1;
4820 	}
4821 	PHYSFS_write(fp, &tmptime32, sizeof(fix), 1);
4822 	}
4823 	{
4824 	const int boss_dying = BossUniqueState.Boss_dying;
4825 	PHYSFS_write(fp, &boss_dying, sizeof(int), 1);
4826 	}
4827 	const int boss_dying_sound_playing = BossUniqueState.Boss_dying_sound_playing;
4828 	PHYSFS_write(fp, &boss_dying_sound_playing, sizeof(int), 1);
4829 #if defined(DXX_BUILD_DESCENT_I)
4830 	const int boss_hit_this_frame = BossUniqueState.Boss_hit_this_frame;
4831 	PHYSFS_write(fp, &boss_hit_this_frame, sizeof(int), 1);
4832 	const int Boss_been_hit = 0;
4833 	PHYSFS_write(fp, &Boss_been_hit, sizeof(int), 1);
4834 #elif defined(DXX_BUILD_DESCENT_II)
4835 	tmptime32 = build_savegametime_from_gametime(gametime, BossUniqueState.Boss_hit_time);
4836 	PHYSFS_write(fp, &tmptime32, sizeof(fix), 1);
4837 	PHYSFS_writeSLE32(fp, -1);
4838 	tmptime32 = build_savegametime_from_gametime(gametime, BuddyState.Escort_last_path_created);
4839 	PHYSFS_write(fp, &tmptime32, sizeof(fix), 1);
4840 	{
4841 		const uint32_t Escort_goal_object = BuddyState.Escort_goal_object;
4842 	PHYSFS_write(fp, &Escort_goal_object, sizeof(Escort_goal_object), 1);
4843 	}
4844 	{
4845 		const uint32_t Escort_special_goal = BuddyState.Escort_special_goal;
4846 	PHYSFS_write(fp, &Escort_special_goal, sizeof(Escort_special_goal), 1);
4847 	}
4848 	{
4849 		const int egi = BuddyState.Escort_goal_reachable == d_unique_buddy_state::Escort_goal_reachability::unreachable ? -2 : BuddyState.Escort_goal_objidx.get_unchecked_index();
4850 		PHYSFS_write(fp, &egi, sizeof(int), 1);
4851 	}
4852 	{
4853 		auto &Stolen_items = LevelUniqueObjectState.ThiefState.Stolen_items;
4854 		PHYSFS_write(fp, &Stolen_items, sizeof(Stolen_items[0]) * Stolen_items.size(), 1);
4855 	}
4856 
4857 	{
4858 		int temp;
4859 		temp = Point_segs_free_ptr - Point_segs;
4860 		PHYSFS_write(fp, &temp, sizeof(int), 1);
4861 	}
4862 
4863 	unsigned Num_boss_teleport_segs = Boss_teleport_segs.size();
4864 	PHYSFS_write(fp, &Num_boss_teleport_segs, sizeof(Num_boss_teleport_segs), 1);
4865 	unsigned Num_boss_gate_segs = Boss_gate_segs.size();
4866 	PHYSFS_write(fp, &Num_boss_gate_segs, sizeof(Num_boss_gate_segs), 1);
4867 
4868 	if (Num_boss_gate_segs)
4869 		PHYSFS_write(fp, &Boss_gate_segs[0], sizeof(Boss_gate_segs[0]), Num_boss_gate_segs);
4870 
4871 	if (Num_boss_teleport_segs)
4872 		PHYSFS_write(fp, &Boss_teleport_segs[0], sizeof(Boss_teleport_segs[0]), Num_boss_teleport_segs);
4873 #endif
4874 
4875 	return 1;
4876 }
4877 
4878 }
4879 
4880 namespace dcx {
4881 
PHYSFSX_readAngleVecX(PHYSFS_File * file,vms_angvec & v,int swap)4882 static void PHYSFSX_readAngleVecX(PHYSFS_File *file, vms_angvec &v, int swap)
4883 {
4884 	v.p = PHYSFSX_readSXE16(file, swap);
4885 	v.b = PHYSFSX_readSXE16(file, swap);
4886 	v.h = PHYSFSX_readSXE16(file, swap);
4887 }
4888 
4889 }
4890 
4891 namespace dsx {
4892 namespace {
4893 
ai_local_read_swap(ai_local * ail,int swap,PHYSFS_File * fp)4894 static void ai_local_read_swap(ai_local *ail, int swap, PHYSFS_File *fp)
4895 {
4896 	{
4897 		fix tmptime32 = 0;
4898 
4899 #if defined(DXX_BUILD_DESCENT_I)
4900 		ail->player_awareness_type = static_cast<player_awareness_type_t>(PHYSFSX_readByte(fp));
4901 		ail->retry_count = PHYSFSX_readByte(fp);
4902 		ail->consecutive_retries = PHYSFSX_readByte(fp);
4903 		ail->mode = static_cast<ai_mode>(PHYSFSX_readByte(fp));
4904 		ail->previous_visibility = static_cast<player_visibility_state>(PHYSFSX_readByte(fp));
4905 		ail->rapidfire_count = PHYSFSX_readByte(fp);
4906 		ail->goal_segment = PHYSFSX_readSXE16(fp, swap);
4907 		PHYSFSX_readSXE32(fp, swap);
4908 		PHYSFSX_readSXE32(fp, swap);
4909 		ail->next_action_time = PHYSFSX_readSXE32(fp, swap);
4910 		ail->next_fire = PHYSFSX_readSXE32(fp, swap);
4911 #elif defined(DXX_BUILD_DESCENT_II)
4912 		ail->player_awareness_type = static_cast<player_awareness_type_t>(PHYSFSX_readSXE32(fp, swap));
4913 		ail->retry_count = PHYSFSX_readSXE32(fp, swap);
4914 		ail->consecutive_retries = PHYSFSX_readSXE32(fp, swap);
4915 		ail->mode = static_cast<ai_mode>(PHYSFSX_readSXE32(fp, swap));
4916 		ail->previous_visibility = static_cast<player_visibility_state>(PHYSFSX_readSXE32(fp, swap));
4917 		ail->rapidfire_count = PHYSFSX_readSXE32(fp, swap);
4918 		ail->goal_segment = PHYSFSX_readSXE32(fp, swap);
4919 		ail->next_action_time = PHYSFSX_readSXE32(fp, swap);
4920 		ail->next_fire = PHYSFSX_readSXE32(fp, swap);
4921 		ail->next_fire2 = PHYSFSX_readSXE32(fp, swap);
4922 #endif
4923 		ail->player_awareness_time = PHYSFSX_readSXE32(fp, swap);
4924 		tmptime32 = PHYSFSX_readSXE32(fp, swap);
4925 		ail->time_player_seen = static_cast<fix64>(tmptime32);
4926 		tmptime32 = PHYSFSX_readSXE32(fp, swap);
4927 		ail->time_player_sound_attacked = static_cast<fix64>(tmptime32);
4928 		tmptime32 = PHYSFSX_readSXE32(fp, swap);
4929 		ail->next_misc_sound_time = static_cast<fix64>(tmptime32);
4930 		ail->time_since_processed = PHYSFSX_readSXE32(fp, swap);
4931 
4932 		range_for (auto &j, ail->goal_angles)
4933 			PHYSFSX_readAngleVecX(fp, j, swap);
4934 		range_for (auto &j, ail->delta_angles)
4935 			PHYSFSX_readAngleVecX(fp, j, swap);
4936 		range_for (auto &j, ail->goal_state)
4937 			j = PHYSFSX_readByte(fp);
4938 		range_for (auto &j, ail->achieved_state)
4939 			j = PHYSFSX_readByte(fp);
4940 	}
4941 }
4942 
4943 }
4944 
4945 }
4946 
4947 namespace dcx {
4948 
PHYSFSX_readVectorX(PHYSFS_File * file,vms_vector & v,int swap)4949 static void PHYSFSX_readVectorX(PHYSFS_File *file, vms_vector &v, int swap)
4950 {
4951 	v.x = PHYSFSX_readSXE32(file, swap);
4952 	v.y = PHYSFSX_readSXE32(file, swap);
4953 	v.z = PHYSFSX_readSXE32(file, swap);
4954 }
4955 
4956 }
4957 
4958 namespace dsx {
4959 namespace {
4960 
ai_cloak_info_read_n_swap(ai_cloak_info * ci,int n,int swap,PHYSFS_File * fp)4961 static void ai_cloak_info_read_n_swap(ai_cloak_info *ci, int n, int swap, PHYSFS_File *fp)
4962 {
4963 	int i;
4964 	fix tmptime32 = 0;
4965 
4966 	for (i = 0; i < n; i++, ci++)
4967 	{
4968 		tmptime32 = PHYSFSX_readSXE32(fp, swap);
4969 		ci->last_time = static_cast<fix64>(tmptime32);
4970 #if defined(DXX_BUILD_DESCENT_II)
4971 		ci->last_segment = PHYSFSX_readSXE32(fp, swap);
4972 #endif
4973 		PHYSFSX_readVectorX(fp, ci->last_position, swap);
4974 	}
4975 }
4976 
4977 }
4978 
ai_restore_state(PHYSFS_File * fp,int version,int swap)4979 int ai_restore_state(PHYSFS_File *fp, int version, int swap)
4980 {
4981 	auto &BossUniqueState = LevelUniqueObjectState.BossState;
4982 #if defined(DXX_BUILD_DESCENT_II)
4983 	auto &Boss_gate_segs = LevelSharedBossState.Gate_segs;
4984 	auto &Boss_teleport_segs = LevelSharedBossState.Teleport_segs;
4985 	auto &BuddyState = LevelUniqueObjectState.BuddyState;
4986 #endif
4987 	auto &Objects = LevelUniqueObjectState.Objects;
4988 	auto &vmobjptridx = Objects.vmptridx;
4989 	fix tmptime32 = 0;
4990 
4991 	PHYSFSX_readSXE32(fp, swap);
4992 	Overall_agitation = PHYSFSX_readSXE32(fp, swap);
4993 	range_for (object &obj, Objects)
4994 	{
4995 		ai_local discard;
4996 		ai_local_read_swap(obj.type == OBJ_ROBOT ? &obj.ctype.ai_info.ail : &discard, swap, fp);
4997 	}
4998 	PHYSFSX_serialize_read(fp, Point_segs);
4999 	ai_cloak_info_read_n_swap(Ai_cloak_info.data(), Ai_cloak_info.size(), swap, fp);
5000 	tmptime32 = PHYSFSX_readSXE32(fp, swap);
5001 	BossUniqueState.Boss_cloak_start_time = static_cast<fix64>(tmptime32);
5002 	tmptime32 = PHYSFSX_readSXE32(fp, swap);
5003 	tmptime32 = PHYSFSX_readSXE32(fp, swap);
5004 	BossUniqueState.Last_teleport_time = static_cast<fix64>(tmptime32);
5005 
5006 	// If boss teleported, set the looping 'see' sound -kreatordxx
5007 	// Also make sure any bosses that were generated/released during the game have teleport segs
5008 	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
5009 #if DXX_USE_EDITOR
5010 	if (!EditorWindow)
5011 #endif
5012 		range_for (const auto &&o, vmobjptridx)
5013 		{
5014 			if (o->type == OBJ_ROBOT)
5015 			{
5016 				auto boss_id = Robot_info[get_robot_id(o)].boss_flag;
5017 				if (boss_id
5018 #if defined(DXX_BUILD_DESCENT_II)
5019 				&& (boss_id == BOSS_D1 || boss_id == BOSS_D2 || (boss_id >= BOSS_D2 && Boss_teleports[boss_id - BOSS_D2]))
5020 #endif
5021 				)
5022 				{
5023 					const auto Last_teleport_time = BossUniqueState.Last_teleport_time;
5024 					if (Last_teleport_time != 0 && Last_teleport_time != BossUniqueState.Boss_cloak_start_time)
5025 						boss_link_see_sound(o);
5026 					boss_init_all_segments(Segments, o);
5027 				}
5028 			}
5029 		}
5030 
5031 #if defined(DXX_BUILD_DESCENT_II)
5032 	LevelSharedBossState.Boss_teleport_interval =
5033 #endif
5034 		PHYSFSX_readSXE32(fp, swap);
5035 #if defined(DXX_BUILD_DESCENT_II)
5036 	LevelSharedBossState.Boss_cloak_interval =
5037 #endif
5038 		PHYSFSX_readSXE32(fp, swap);
5039 	PHYSFSX_readSXE32(fp, swap);
5040 	tmptime32 = PHYSFSX_readSXE32(fp, swap);
5041 	BossUniqueState.Last_gate_time = static_cast<fix64>(tmptime32);
5042 	GameUniqueState.Boss_gate_interval = PHYSFSX_readSXE32(fp, swap);
5043 	tmptime32 = PHYSFSX_readSXE32(fp, swap);
5044 	BossUniqueState.Boss_dying_start_time = static_cast<fix64>(tmptime32);
5045 	BossUniqueState.Boss_dying = PHYSFSX_readSXE32(fp, swap);
5046 	BossUniqueState.Boss_dying_sound_playing = PHYSFSX_readSXE32(fp, swap);
5047 #if defined(DXX_BUILD_DESCENT_I)
5048 	(void)version;
5049 	BossUniqueState.Boss_hit_this_frame = PHYSFSX_readSXE32(fp, swap);
5050 	PHYSFSX_readSXE32(fp, swap);
5051 #elif defined(DXX_BUILD_DESCENT_II)
5052 	tmptime32 = PHYSFSX_readSXE32(fp, swap);
5053 	BossUniqueState.Boss_hit_time = static_cast<fix64>(tmptime32);
5054 
5055 	if (version >= 8) {
5056 		PHYSFSX_readSXE32(fp, swap);
5057 		tmptime32 = PHYSFSX_readSXE32(fp, swap);
5058 		BuddyState.Escort_last_path_created = static_cast<fix64>(tmptime32);
5059 		BuddyState.Escort_goal_object = static_cast<escort_goal_t>(PHYSFSX_readSXE32(fp, swap));
5060 		BuddyState.Escort_special_goal = static_cast<escort_goal_t>(PHYSFSX_readSXE32(fp, swap));
5061 		const int egi = PHYSFSX_readSXE32(fp, swap);
5062 		if (static_cast<unsigned>(egi) < Objects.size())
5063 		{
5064 			BuddyState.Escort_goal_objidx = egi;
5065 			BuddyState.Escort_goal_reachable = d_unique_buddy_state::Escort_goal_reachability::reachable;
5066 		}
5067 		else
5068 		{
5069 			BuddyState.Escort_goal_objidx = object_none;
5070 			BuddyState.Escort_goal_reachable = d_unique_buddy_state::Escort_goal_reachability::unreachable;
5071 		}
5072 		{
5073 			auto &Stolen_items = LevelUniqueObjectState.ThiefState.Stolen_items;
5074 			PHYSFS_read(fp, &Stolen_items, sizeof(Stolen_items[0]) * Stolen_items.size(), 1);
5075 		}
5076 	} else {
5077 		BuddyState.Escort_last_path_created = 0;
5078 		BuddyState.Escort_goal_object = ESCORT_GOAL_UNSPECIFIED;
5079 		BuddyState.Escort_special_goal = ESCORT_GOAL_UNSPECIFIED;
5080 		BuddyState.Escort_goal_objidx = object_none;
5081 		BuddyState.Escort_goal_reachable = d_unique_buddy_state::Escort_goal_reachability::unreachable;
5082 
5083 		LevelUniqueObjectState.ThiefState.Stolen_items.fill(255);
5084 	}
5085 
5086 	if (version >= 15) {
5087 		unsigned temp;
5088 		temp = PHYSFSX_readSXE32(fp, swap);
5089 		if (temp > Point_segs.size())
5090 			throw std::out_of_range("too many points");
5091 		Point_segs_free_ptr = Point_segs.begin() + temp;
5092 	} else
5093 		ai_reset_all_paths();
5094 
5095 	if (version >= 21) {
5096 		unsigned Num_boss_teleport_segs = PHYSFSX_readSXE32(fp, swap);
5097 		unsigned Num_boss_gate_segs = PHYSFSX_readSXE32(fp, swap);
5098 
5099 		Boss_gate_segs.clear();
5100 		for (unsigned i = 0; i < Num_boss_gate_segs; i++)
5101 			Boss_gate_segs.emplace_back(PHYSFSX_readSXE16(fp, swap));
5102 
5103 		Boss_teleport_segs.clear();
5104 		for (unsigned i = 0; i < Num_boss_teleport_segs; i++)
5105 			Boss_teleport_segs.emplace_back(PHYSFSX_readSXE16(fp, swap));
5106 	}
5107 #endif
5108 
5109 	return 1;
5110 }
5111 
5112 }
5113