Repository navigation
Expand file tree
/
Copy pathai.zig
More file actions
1783 lines (1630 loc) · 86.6 KB
/
Copy pathai.zig
File metadata and controls
1783 lines (1630 loc) · 86.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! `C:\lancer\game\Ai.cpp`: the order table ([`ai/orders.zig`](ai/orders.zig)), the AI's
//! steering, arriving and avoidance, its aiming and collision course, a ship's end, and the walk
//! of an order's targets.
//!
//! **Unverified:** the table lies in the data before this file's path, which holds this file's
//! data or an earlier file's; and the code from `ai_along_nose` (`0x004010F0`) to
//! `object_hull_lost` (`0x00401F00`), before this file's known code, and from
//! `player_control_entry` (`0x00402860`) through `ai_roll_upright` (`0x00403170`), after it, is
//! taken as this file's, since it does the orders' work.
const std = @import("std");
const assert = std.debug.assert;
const engine = @import("../../engine.zig");
const Pointer = engine.Pointer;
const dte = @import("../../formats/dte.zig");
const bind = @import("mission/bind.zig");
const events = @import("mission/events.zig");
const hooks = @import("../hooks.zig");
const gameobj = @import("gameobj.zig");
const Routine = gameobj.Routine;
const camera = @import("camera.zig");
const create = @import("create.zig");
const math = @import("../surrender/math.zig");
const Vector = math.Vector;
const motion = @import("motion.zig");
const aigeneric = @import("aigeneric.zig");
const guns = @import("guns.zig");
const objects = @import("objects.zig");
const GameObject = gameobj.GameObject;
/// The order table, every order objects follow.
pub const orders = @import("ai/orders.zig");
/// The names of the order table's routines.
pub const routines = @import("ai/routines.zig");
/// Ship Follow Curve and its backwards twin.
pub const follow = @import("ai/follow.zig");
/// Formation Regroup and Patrol Route, which fly a flight group in its formation.
pub const formations = @import("ai/formations.zig");
/// A record of the order table. `order_groups` points at the records of each hundred order
/// numbers: order `n` is record `n % 100` of group `n / 100`.
pub const Record = extern struct {
/// Runs before the order's first update. Null for none.
init: Pointer(Routine),
/// Runs each time `object_orders` runs the order.
update: Pointer(Routine),
/// Runs when the order is popped or replaced after it has started. Null for none.
exit: Pointer(Routine),
flags: Flags,
/// The developers' name for the order, which fatal errors show.
name: Pointer(u8),
/// Zero for an order that any other replaces. Otherwise, once it has started, only `explode`, a
/// one-shot order or an order of higher priority may be pushed on it, and pushing another is a
/// fatal error.
priority: i32,
pub const Flags = packed struct(u32) {
/// The name mods' scripts know it by.
pub const script_name = "OrderFlags";
/// It may be given to a player's ship. A player's ship refuses the other orders numbered
/// below 100.
players: bool = false,
/// **Unknown.** Bits set on some orders that nothing in the payload tests.
_unknown_1: u4 = 0,
/// It runs its update once, then pops itself, and the order below carries on without
/// starting again. Its `init` never runs.
one_shot: bool = false,
/// While it runs, a ship that takes enough damage turns to fight its attacker
/// (`order_retaliate`).
retaliate: bool = false,
/// While it runs, `avoidance_scan` lists the objects the ship could hit, up to ten of each
/// of two kinds (`GameObject.avoid_near`, `avoid_ahead`), unless the object has
/// `no_avoidance`; `docs/engine/orders.md` says which.
avoidance: bool = false,
_unknown_8: u2 = 0,
/// While it runs, a multiplayer game sends the ship's steering inputs, throttle, rates and
/// velocity.
send_flight: bool = false,
_unknown_11: u21 = 0,
};
comptime {
assert(@offsetOf(Record, "flags") == 0xC);
assert(@sizeOf(Record) == 0x18);
}
};
test {
std.testing.refAllDecls(@This());
}
/// Where the AI aims at a target, and how far across that is: the part a component names, the
/// part a few types are aimed at by (`gameobj.Type.aimedChild`), or the object itself
/// (`ai_target_node`, `0x004018F0`, which gives the part's node).
pub const Aimed = struct {
position: Vector,
radius: f32,
/// How the part hanging from the root that it is, or hangs from, is turned; the object's own
/// turn for the object itself (`maneuver_new_attack_run_run`, which walks up to it).
orientation: math.Matrix,
/// Where part `part` of `model` stands and how far it reaches, turned as the part hanging from
/// the root that it is or hangs from.
fn ofPart(model: *const objects.Model, part: *const objects.Model.Part) Aimed {
return .{ .position = part.object.position, .radius = part.object.radius, .orientation = model.topOf(part).object.orientation };
}
};
/// Where the AI aims at `aimed`, and how far across that is (`ai_target_node`, then the node's
/// frame).
pub fn aimedAt(all: *const create.Objects, aimed: ValidTarget) Aimed {
const slot = &all.slots[aimed.slot];
if (slot.model) |*model| if (targetPart(all, aimed)) |part| return .ofPart(model, part);
return .{ .position = slot.drawn.position, .radius = slot.object.radius, .orientation = slot.drawn.orientation };
}
/// `ai_target_node` (`0x004018F0`): the part whose node a target names, the component or the part
/// a few types are aimed at by, or null for the object's root.
pub fn targetPart(all: *const create.Objects, aimed: ValidTarget) ?*const objects.Model.Part {
const slot = &all.slots[aimed.slot];
const model = if (slot.model) |*model| model else return null;
if (aimed.target.part()) |component| if (slot.component(component)) |part| return part;
const child = slot.object.type.base().aimedChild() orelse return null;
return model.rootChild(child);
}
/// `player_control_entry` (`0x00402860`): the entry of the player's orders that is Player Control,
/// which holds the player's target, or null for none.
pub fn playerControlEntry(all: *create.Objects) ?*aigeneric.Entry {
return all.slots[all.player].firstOrder(.player_control);
}
/// How much further a Turret Flak's shot is led for, over its lifetime (`0x004DC3D8`), and the
/// share of a gun's life within which a shot is led at all (`0x004DC3D4`).
const flak_lead: f32 = 3;
const lead_range: f32 = 0.25;
/// `ai_lead_aim` (`0x00401280`): where to aim at `target` for the fastest of the guns the ship
/// fires together to hit it, from its root (`leadAimWithGun`).
///
/// **Fix:** the game reads each gun's turret kind as its type, which leads every ship's shots as
/// a Laser Cannon's; OpenReliant leads by the fastest gun's own type.
pub fn leadAim(all: *const create.Objects, index: u16, target: ValidTarget, lead: f32) ?Vector {
const slot = &all.slots[index];
var fastest: guns.GunType = .of(.laser_cannon);
var best: f32 = -1;
var chosen: guns.Chosen = .of(&slot.object, slot.guns, slot.gun_groups);
while (chosen.next()) |gun| {
const barrel = gun.barrel() orelse continue;
const speed = barrel.type.stats(&all.gun_stats).speed;
if (speed > best) {
best = speed;
fastest = barrel.type;
}
}
return leadAimWithGun(all, slot.drawn.position, target, fastest, lead);
}
/// `ai_lead_aim_with_gun` (`0x00401180`): where to aim from `from` at `target` for a shot of `gun`
/// to hit it: ahead of it along its heading by how far it flies, times `lead`, while the shot
/// flies to it. Null where that takes longer than a quarter of the gun's life, when the caller
/// aims at it unled or not at all.
///
/// A Turret Flak's shot is led within three times its gun's lifetime.
///
/// **Fix:** the game takes the Laser Cannon's lifetime there, the table's first gun's, which leads
/// flak past the life of its own shells; OpenReliant the flak's own.
pub fn leadAimWithGun(all: *const create.Objects, from: Vector, target: ValidTarget, gun: guns.GunType, lead: f32) ?Vector {
const record = gun.stats(&all.gun_stats);
const lifetime = @as(f32, @floatFromInt(record.lifetime)) * @as(f32, if (gun.base() == .turret_flak) flak_lead else 1);
const met = intercept(all, target, from, record.speed, lead);
if (!(met.ticks <= lifetime * lead_range)) return null;
return met.at;
}
/// Where a shot flying at `speed` from `from` meets `target`: the point it aims at (`aimedAt`), led
/// along the target's nose by how far the target flies in the shot's ticks, times `lead`; and those
/// ticks. `ai_lead_aim_with_gun` works it out, and the homing missiles and the torpedoes each in
/// their own routine (`missiles.home`, `missiles.torpedo`).
pub fn intercept(all: *const create.Objects, target: ValidTarget, from: Vector, speed: f32, lead: f32) Intercept {
const aimed = aimedAt(all, target).position;
const ticks = math.distance(from, aimed) / speed;
const struck = &all.slots[target.slot];
return .{ .at = aimed + math.forward(struck.drawn.orientation) * @as(Vector, @splat(ticks * struck.object.speed * lead)), .ticks = ticks };
}
/// Where `intercept` meets a target, and the ticks the shot takes to.
pub const Intercept = struct { at: Vector, ticks: f32 };
/// `ai_along_nose` (`0x004010F0`): whether `point` lies ahead of `place` and within `radius` of the
/// line along its nose.
pub fn alongNose(place: math.Place, point: Vector, radius: f32) bool {
const offset = point - place.position;
const along = math.dot(math.forward(place.orientation), offset);
if (!(along > 0)) return false;
return math.lengthSquared(offset) - along * along < radius * radius;
}
/// The cosine of the angle between `toward` and the nose of something turned as `orientation`:
/// `toward` normalized, dotted with the nose, as the ejection's Sabre, the Ripper, Scoop Up and the
/// formation orders work it out (`order_eject_fighter_attack`, `order_ripper_grabs_target_object`,
/// `order_scoop_up`, `formation_face`).
/// `math.cosineOff` divides by the length instead, which rounds differently.
pub fn noseCosine(orientation: math.Matrix, toward: Vector) f32 {
return math.dot(math.normalize(toward), math.forward(orientation));
}
/// How near the point an approach must face it before it flies on, in the ship's speed per pitch
/// rate (`0x004DC400`, where the game pools it with `rate_damping`), and how nearly: the cosine
/// (`0x004DC484`).
const approach_room: f32 = 6;
const approach_facing: f32 = 0.7;
/// Whether an approach holds its throttle at 0 to turn first: the point lies `reach` off, nearer
/// than `approach_room` times the ship's speed per pitch rate, so that flying on would overshoot
/// it, and the way `toward` it is not within `approach_facing` of the nose of something turned as
/// `orientation` (`noseCosine`). The Ripper's approaches and Scoop Up's test it so
/// (`order_ripper_grabs_target_object`, `order_ripper_attach_cargo_pod_to_mammoth`,
/// `order_scoop_up`).
pub fn turnFirst(flight: *const create.FlightModel, orientation: math.Matrix, reach: f32, toward: Vector) bool {
return reach < flight.speed_per_pitch_rate * approach_room and noseCosine(orientation, toward) < approach_facing;
}
/// How an approach's throttle runs with the reach to its point (`approachToRest`): full beyond
/// `full_beyond`, `slow_throttle` beyond `slow_beyond`, and none nearer.
pub const ApproachBands = struct {
full_beyond: f32,
slow_beyond: f32,
slow_throttle: f32,
/// The throttle at `reach` from the point.
pub fn throttle(bands: ApproachBands, reach: f32) f32 {
return if (reach > bands.full_beyond) full_throttle else if (reach > bands.slow_beyond) bands.slow_throttle else 0;
}
};
/// The approach of the ship in `slot` to a point `reach` off along `toward`, as Scoop Up and the
/// Ripper's grab and drop fly it (`order_scoop_up`, `order_ripper_grabs_target_object`): where it
/// turns first (`turnFirst`), its throttle is held at 0; otherwise it runs in `bands`. Whether it
/// has come to rest, its throttle counted (`Stillness.at_rest`). Each caller measures `reach` as
/// the game does there, by `math.length` or `math.distance`, which round differently.
pub fn approachToRest(slot: *create.Slot, toward: Vector, reach: f32, bands: ApproachBands) bool {
const object = &slot.object;
const flight = slot.flight orelse return false;
if (turnFirst(flight, slot.drawn.orientation, reach, toward)) {
object.throttle = 0;
return false;
}
object.throttle = bands.throttle(reach);
return Stillness.at_rest.holds(object, true);
}
/// How still a ship must be for an order to go on: each of its turning inputs, and its throttle
/// where the order counts it, within `inputs`, and each of its rates of turn within `rates`, or
/// below them where `bound` asks, as the Ripper's orders, Scoop Up and Jump Out test it
/// (`order_ripper_grabs_target_object`, `order_scoop_up`, `order_jump_out`).
pub const Stillness = struct {
inputs: f32,
rates: f32,
bound: Bound = .within,
/// How each value must stand to its limit: within it, the limit itself counted, or below it.
/// A value that is not a number passes either way, as the game's comparisons let it.
pub const Bound = enum { within, below };
/// The Ripper's and Scoop Up's: the inputs within `0x004DC53C`, the rates within
/// `0x004DC4AC`.
pub const at_rest: Stillness = .{ .inputs = 0.025, .rates = 0.02 };
/// The same limits, each value to stay below its limit rather than reach it.
pub fn strictly(still: Stillness) Stillness {
return .{ .inputs = still.inputs, .rates = still.rates, .bound = .below };
}
/// Whether `value`, either way, stands to `limit` as `bound` asks.
fn holdsTo(still: Stillness, value: f32, limit: f32) bool {
return switch (still.bound) {
.within => !(@abs(value) > limit),
.below => !(@abs(value) >= limit),
};
}
/// Whether its yaw, pitch and roll inputs, and its throttle where `throttle`, each stand to
/// `inputs` as `bound` asks.
pub fn inputsHeld(still: Stillness, object: *const GameObject, throttle: bool) bool {
for ([_]f32{ object.yaw_input, object.pitch_input, object.roll_input }) |input| {
if (!still.holdsTo(input, still.inputs)) return false;
}
return !throttle or still.holdsTo(object.throttle, still.inputs);
}
/// Whether its yaw, pitch and roll rates each stand to `rates` as `bound` asks.
pub fn ratesHeld(still: Stillness, object: *const GameObject) bool {
for ([_]f32{ object.yaw_rate, object.pitch_rate, object.roll_rate }) |rate| {
if (!still.holdsTo(rate, still.rates)) return false;
}
return true;
}
/// Whether its inputs (`inputsHeld`) and its rates (`ratesHeld`) are both held.
pub fn holds(still: Stillness, object: *const GameObject, throttle: bool) bool {
return still.inputsHeld(object, throttle) and still.ratesHeld(object);
}
};
/// How near a box of a hull has to be for `escapeDirection` to push away from it (`0x004DC444`).
const escape_reach: f32 = 20000;
/// `ai_escape_direction` (`0x00402500`): which way lies clear of a ship's hull from `from`. Each
/// box of the collision trees of its parts, the root's child list (`objects.Model.rootChildren`),
/// whose edge, taking it as a sphere as wide as its half-size, is within `escape_reach` of `from`
/// pushes away from it, the harder the nearer it is; the sum, normalized.
pub fn escapeDirection(slot: *const create.Slot, from: Vector) Vector {
var away: Vector = @splat(0);
const model = if (slot.model) |*model| model else return math.normalize(away);
var children = model.rootChildren();
while (children.next()) |child| {
const data = model.partData(child.index) orelse continue;
for (data.nodes) |node| {
const toward = child.part.drawn().point(node.centre.vector()) - from;
const gap = math.length(toward) - math.length(node.half_size.vector());
if (gap < escape_reach and gap > 0) away += math.normalize(toward) * @as(Vector, @splat(gap - escape_reach));
}
}
return math.normalize(away);
}
/// How much of the target's velocity the course is closed against (`0x00401A31`, in
/// `ai_collision_course`), and the least closing that counts (`0x004DC418`).
const crash_target_share: f32 = -2;
const least_closing: f32 = 0.001;
/// `ai_collision_course` (`0x00401980`): whether the ship at `index` is on course to hit `target`
/// within `steps` simulation steps and with `margin` to spare. First they must be within reach of
/// each other at their cruise speeds. Against a target that lists components, the ship must then be
/// within reach of a box of one of its parts (`partsInReach`). Against any other, the target must
/// be ahead of the ship, and the ship closing on it by its velocity less twice the target's, to
/// within both radii and the margin.
///
/// **Fix:** the game follows a null pointer where the ship or the target has no flight stats, as a
/// stand-in has none; OpenReliant takes it for no collision course.
///
/// **Fix:** the game also hands back the part and the box through two pointers it does not test,
/// which every caller passes null. Avoid Target (`order_avoid_target`, `0x0040B330`) asks only that
/// its target's type is below 0x100, not that it lacks components, so the game writes to address
/// zero once the ship comes within reach of a box of a ship that lists components. OpenReliant
/// hands back neither.
pub fn collisionCourse(world: gameobj.World, index: u16, target: u16, steps: f32, margin: f32) bool {
const all = world.objects;
const ship = &all.slots[index];
const struck = &all.slots[target];
const own_cruise = slotCruise(ship, world.view()) orelse return false;
const speeds = (slotCruise(struck, world.view()) orelse return false) + own_cruise;
var apart = ship.object.nextPosition() - struck.object.nextPosition();
const reach = speeds * steps + struck.object.radius + ship.object.radius + margin;
if (math.lengthSquared(apart) > reach * reach) return false;
if (struck.object.flags.components) return partsInReach(struck, ship.object.nextPosition(), own_cruise * steps + margin);
const closing = struck.object.velocity.vector() * @as(Vector, @splat(crash_target_share)) + ship.object.velocity.vector();
if (math.dot(ship.object.nextHeading(), apart) > 0) return false;
const along = math.dot(apart, closing);
if (-along < 0) return false;
const rate = math.dot(closing, closing);
if (rate < least_closing) return false;
apart += closing * @as(Vector, @splat(@min(-along / rate, steps)));
const touching = struck.object.radius + ship.object.radius + margin;
return math.lengthSquared(apart) < touching * touching;
}
/// `ai_collision_course`'s test of a target that lists components (`0x00401A5E`): whether `at`
/// lies within `reach` of one of the target's parts. Of the parts in its root's child list, every
/// part whatever it is linked to (`objects.Model.rootChildren`), shown and with a collision tree,
/// each whose frame's sphere, `reach` wider, holds `at` has each box of its tree tested: `at` lies
/// within reach of the box where it stands nearer to the box's centre, squared, than the box's half
/// size and the reach, each squared, together.
fn partsInReach(struck: *const create.Slot, at: Vector, reach: f32) bool {
const model = if (struck.model) |*live| live else return false;
var children = model.rootChildren();
while (children.next()) |child| {
const part = child.part;
if (part.hidden) continue;
const data = model.partData(child.index) orelse continue;
if (data.nodes.len == 0) continue;
const place = part.drawn();
const limit = reach + part.object.radius;
if (limit * limit < math.distanceSquared(place.position, at)) continue;
for (data.nodes) |node| {
const half = math.lengthSquared(node.half_size.vector());
if (half + reach * reach > math.distanceSquared(place.point(node.centre.vector()), at)) return true;
}
}
return false;
}
/// A pilot ejects rather than go down with the ship where its `eject_roll` is below this
/// (`0x00401FB0`, in `object_destroyed`).
pub const eject_below = 40;
/// `object_hull_lost` (`0x00401F00`): the end of a ship that lists components, as the part of its
/// hull holding it together is destroyed. Its current order gives way as to Explode, its stack is
/// emptied, and it is marked exploding, so it takes no order again; no Explode order runs it down.
/// Its Destroyed event is posted (`events.destroyed`).
pub fn hullLost(ctx: aigeneric.Context, index: u16) void {
const object = &ctx.world.objects.slots[index].object;
_ = aigeneric.giveWay(ctx, index, .explode) catch false;
object.order_count = 0;
object.flags.exploding = true;
events.destroyed(ctx.world, index, null);
}
/// `object_destroyed` (`0x00401F30`): a ship's end. An AI ship's pilot ejects where the ship is in
/// the player's wing and its roll says so, or where the ship is told to eject before exploding, and
/// the ship spins on under Eject Spin. The player's ejects, unless it already has or the blow was
/// too heavy, or it is flying the Kamov, and its ship blows up later (`aieject.playerInit`): Eject
/// Player is aimed at its current order's index and component taken as a ship's
/// (`order_push_ship`). Otherwise the ship explodes (`aiexplode`), in place of whatever it was
/// doing: the stack is overwritten whether or not its order gives way, and the order's state is
/// left for Explode's `init` to fill in. `may_spin` goes into the order's data.
///
/// Not ported: multiplayer, where the player's ship explodes at once
/// ([#55](https://github.com/OpenReliant/openreliant/issues/55)).
pub fn objectDestroyed(ctx: aigeneric.Context, index: u16, may_spin: bool, no_eject: bool) void {
if (hooks.enter(.object_destroyed, objectDestroyed, .{ ctx, index, may_spin, no_eject })) |done| return done;
const all = ctx.world.objects;
const slot = &all.slots[index];
const object = &slot.object;
const ai_pilot_ejects = index >= all.players and object.wing == .player and object.eject_roll < eject_below;
if (!object.flags.ejected and (ai_pilot_ejects or object.invulnerable == .eject_before_exploding)) {
replaceOrders(ctx, index, .eject, .eject_spin, may_spin);
return;
}
if (slot.orders[0].order == .explode) return;
if (index == all.player and !object.flags.ejected and !no_eject and object.type.base() != .kamov) {
if (slot.orders[0].order != .eject_player) {
_ = aigeneric.give(ctx, index, .eject_player, slot.orders[0].target.asShip());
object.flags.ejected = true;
return;
}
if (slot.state.eject_player.ended == 0) return;
aigeneric.end(ctx, index);
}
replaceOrders(ctx, index, .explode, .explode, may_spin);
object.flags.exploding = true;
}
/// Clears the way as for `making_way`, whatever the current order says, and leaves `order`,
/// aimed at nothing, as the only one, starting.
fn replaceOrders(ctx: aigeneric.Context, index: u16, making_way: orders.Order, order: orders.Order, may_spin: bool) void {
const slot = &ctx.world.objects.slots[index];
_ = aigeneric.giveWay(ctx, index, making_way) catch false;
slot.object.order_count = 1;
slot.orders[0].order = order;
slot.orders[0].target = .none;
slot.orders[0].data.destroyed = .{ .may_spin = may_spin };
slot.object.order_starting = true;
}
/// `object_set_targetable` (`0x00401830`): sets or clears the object's `targetable` flag, which
/// stays clear for an object with no stats or of a type that can't be targeted
/// (`ShipCombat.Targeting`).
pub fn setTargetable(object: *gameobj.GameObject, combat: ?*const create.ShipCombat, targetable: bool) void {
const allowed = if (combat) |stats| stats.targeting.targetable else false;
object.flags.targetable = targetable and allowed;
}
/// `order_target_walk` (`0x00401CB0`): `visitor.visit` for each ship an order's `target` names,
/// until a visit returns true: the ship itself, as the target names it; each ship of a flight
/// group, whole; and each ship of a squad (`squadWalk`). Whether a visit ended the walk. A flight
/// group or a squad names no ship where no mission is bound (`gameobj.World.mission`). Dock,
/// Escort, the search for a new target, the search for a pod to scoop up, Dark Reign shoot's search
/// for a ship to fire at and Launch walk their targets so.
pub fn eachShip(world: gameobj.World, target: aigeneric.Target, visitor: anytype) bool {
switch (target.kind) {
.ship => return visitor.visit(target),
.flight_group => {
const mission = world.mission orelse return false;
const group = mission.flightGroup(std.math.cast(usize, target.index) orelse return false) orelse return false;
return groupWalk(mission, group.*, visitor);
},
.squad => {
const mission = world.mission orelse return false;
var walk: bind.Mission.SquadPath.Walk = .{};
return squadWalk(mission, &walk.start(std.math.cast(u16, target.index) orelse return false), visitor);
},
_ => return false,
}
}
/// Each ship of flight group `group`, whole, for `eachShip`.
fn groupWalk(mission: *const bind.Mission, group: dte.FlightGroup, visitor: anytype) bool {
for (mission.groupShips(group)) |ship| {
if (visitor.visit(aigeneric.Target.at(ship, null))) return true;
}
return false;
}
/// `squad_walk` (`0x00401D80`): `eachShip`'s walk of the squad `path` stands in: its members in
/// turn (`bind.Mission.squadMembers`): a ship, as the member names its component; a flight group's
/// ships, whole; and a squad's own walk, where the path goes into it (`SquadPath.into`).
fn squadWalk(mission: *const bind.Mission, path: *const bind.Mission.SquadPath, visitor: anytype) bool {
var members = mission.squadMembers(path.squad);
while (members.next()) |member| switch (member) {
.ship => |ship| {
const component: ?u16 = if (ship.component) |part| part else null;
if (visitor.visit(aigeneric.Target.at(ship.index, component))) return true;
},
.flight_group => |group| if (groupWalk(mission, group, visitor)) return true,
.squad => |inner| if (path.into(inner)) |deeper| if (squadWalk(mission, &deeper, visitor)) return true,
};
return false;
}
test hullLost {
var mission: gameobj.testing.Mission = undefined;
try mission.init(std.testing.allocator);
defer mission.deinit();
const ctx = mission.orders();
_ = try mission.add(.of(.predator), @splat(0));
const ship = try mission.add(.of(.sabre), .{ 0, 0, 1000 });
const object = &mission.objects.slots[ship].object;
// Its orders are cleared, and it takes none again.
try std.testing.expect(try aigeneric.push(ctx, ship, .do_nothing, .none));
hullLost(ctx, ship);
try std.testing.expectEqual(0, object.order_count);
try std.testing.expect(object.flags.exploding);
try std.testing.expect(!try aigeneric.push(ctx, ship, .do_nothing, .none));
}
test objectDestroyed {
var mission: gameobj.testing.Mission = undefined;
try mission.init(std.testing.allocator);
defer mission.deinit();
const ctx = mission.orders();
const player = try mission.add(.of(.predator), @splat(0));
const other = try mission.add(.of(.sabre), .{ 0, 0, 1000 });
const slots = &mission.objects.slots;
// An AI ship explodes, taking no order after.
try std.testing.expect(try aigeneric.push(ctx, other, .do_nothing, .none));
objectDestroyed(ctx, other, true, false);
try std.testing.expectEqual(.explode, slots[other].orders[0].order);
try std.testing.expectEqual(1, slots[other].object.order_count);
try std.testing.expect(slots[other].object.flags.exploding and slots[other].object.order_starting);
try std.testing.expect(!try aigeneric.push(ctx, other, .do_nothing, .none));
// In the player's wing, where its pilot may eject, a low roll ejects, and the ship spins on.
const ejecting = try mission.add(.of(.sabre), .{ 0, 0, 2000 });
slots[ejecting].object.wing = .player;
slots[ejecting].object.eject_roll = eject_below - 1;
objectDestroyed(ctx, ejecting, true, false);
try std.testing.expectEqual(.eject_spin, slots[ejecting].orders[0].order);
try std.testing.expect(!slots[ejecting].object.flags.exploding);
// The player's pilot ejects, unless the blow was too heavy.
objectDestroyed(ctx, player, true, false);
try std.testing.expectEqual(.eject_player, slots[player].orders[0].order);
try std.testing.expect(slots[player].object.flags.ejected);
const heavy = try mission.add(.of(.predator), .{ 0, 0, 3000 });
mission.objects.player = heavy;
objectDestroyed(ctx, heavy, true, true);
try std.testing.expectEqual(.explode, slots[heavy].orders[0].order);
}
test setTargetable {
var object = gameobj.testing.object();
var combat = std.mem.zeroes(create.ShipCombat);
setTargetable(&object, &combat, true);
try std.testing.expect(!object.flags.targetable);
combat.targeting.targetable = true;
setTargetable(&object, &combat, true);
try std.testing.expect(object.flags.targetable);
setTargetable(&object, &combat, false);
try std.testing.expect(!object.flags.targetable);
object.flags.targetable = true;
setTargetable(&object, null, true);
try std.testing.expect(!object.flags.targetable);
}
test eachShip {
const gpa = std.testing.allocator;
// Ships 0 to 3, the first two in flight group 0. Squad 0 holds ship 2's component 5, the flight
// group, and squad 1, which holds ship 3. Squad 2 holds ship 3 and itself.
const records = dte.testing;
var ships = records.ships(4, 0);
for (ships[0..2]) |*ship| ship.flight_group = 0;
const squads = [_]dte.Squad{ records.squad(5, 0), records.squad(6, 3), records.squad(7, 4) };
const member = records.squadMember;
const whole = dte.Trigger.whole_object;
const members = [_]dte.SquadMember{ member(2, 0, 5), member(4, 0, whole), member(6, 0, whole), member(3, 1, whole), member(3, 2, whole), member(7, 2, whole) };
var kinds: [8]dte.Object = @splat(records.object(.ship, 0, 0));
kinds[4].kind = .flight_group;
for (kinds[5..]) |*kind| kind.kind = .squad;
var game: @import("../vm.zig").machine.testing.Game = undefined;
try game.init(gpa, &.{}, .{ .ships = &ships, .flight_groups = &.{records.flightGroup(4, .player)}, .objects = &kinds, .squads = &squads, .squad_members = &members });
defer game.deinit();
const world = game.world();
const Seen = struct {
targets: [8]aigeneric.Target = undefined,
count: usize = 0,
stop_at: usize = 0,
pub fn visit(seen: *@This(), target: aigeneric.Target) bool {
seen.targets[seen.count] = target;
seen.count += 1;
return seen.count == seen.stop_at;
}
};
// A ship is visited as the target names it.
var one: Seen = .{};
try std.testing.expect(!eachShip(world, .at(3, 7), &one));
try std.testing.expectEqualSlices(aigeneric.Target, &.{.at(3, 7)}, one.targets[0..one.count]);
// A flight group's ships, whole.
var flight: Seen = .{};
_ = eachShip(world, .group(.flight_group, 0), &flight);
try std.testing.expectEqualSlices(aigeneric.Target, &.{ .at(0, null), .at(1, null) }, flight.targets[0..flight.count]);
// A squad's members in turn, down into the flight group and the squad it holds.
const squad: aigeneric.Target = .group(.squad, 0);
var all: Seen = .{};
_ = eachShip(world, squad, &all);
try std.testing.expectEqualSlices(aigeneric.Target, &.{ .at(2, 5), .at(0, null), .at(1, null), .at(3, null) }, all.targets[0..all.count]);
// A visit that returns true ends the walk.
var two: Seen = .{ .stop_at = 2 };
try std.testing.expect(eachShip(world, squad, &two));
try std.testing.expectEqual(2, two.count);
// A squad that holds itself is passed over as its own member.
var cycle: Seen = .{};
try std.testing.expect(!eachShip(world, .group(.squad, 2), &cycle));
try std.testing.expectEqualSlices(aigeneric.Target, &.{.at(3, null)}, cycle.targets[0..cycle.count]);
// With no mission bound, a flight group names no ship.
var none: Seen = .{};
try std.testing.expect(!eachShip(game.mission.world(), .group(.flight_group, 0), &none));
try std.testing.expectEqual(0, none.count);
}
test "eachShip walks a squad named twice twice, as far as a walk goes" {
const gpa = std.testing.allocator;
const records = dte.testing;
const SquadPath = bind.Mission.SquadPath;
const Game = @import("../vm.zig").machine.testing.Game;
const Counted = struct {
count: usize = 0,
pub fn visit(counted: *@This(), _: aigeneric.Target) bool {
counted.count += 1;
return false;
}
};
// Squad 0 names squad 1 twice, which names the ship twice: four visits.
const short: records.DoubledChain(1, 2) = .init();
var game: Game = undefined;
try game.init(gpa, &.{}, .{ .ships = &records.ships(1, 0), .objects = &short.objects, .squads = &short.squads, .squad_members = &short.members });
defer game.deinit();
var four: Counted = .{};
_ = eachShip(game.world(), .group(.squad, 0), &four);
try std.testing.expectEqual(4, four.count);
// A chain of 40 such squads would take 2^40 visits; the walk stops after going into
// `most_entered` squads.
const long: records.DoubledChain(1, 40) = .init();
var deep: Game = undefined;
try deep.init(gpa, &.{}, .{ .ships = &records.ships(1, 0), .objects = &long.objects, .squads = &long.squads, .squad_members = &long.members });
defer deep.deinit();
var many: Counted = .{};
_ = eachShip(deep.world(), .group(.squad, 0), &many);
try std.testing.expect(many.count > 0 and many.count <= 2 * SquadPath.most_entered);
}
/// `object_cruise_speed` (`0x00403060`): `max_speed` scaled by `speed_factor`, by the share of its
/// engines left, and, unless the camera is in view 13 or the object is invulnerable, by
/// `armor_speed_factor` as well. So losing engines or armor slows a ship.
///
/// OpenReliant takes the flight stats and the view rather than reaching them through the object and
/// a global, since `GameObject` holds the binary's own 32-bit pointers.
pub fn cruiseSpeed(object: *const gameobj.GameObject, flight: *const create.FlightModel, view: camera.View) f32 {
var speed = flight.max_speed * object.speed_factor * object.engines_intact;
if (view != .director and object.invulnerable == .none) speed *= object.armor_speed_factor;
return speed;
}
/// `cruiseSpeed` of the object in `slot`, where it has flight stats. The game reads them through
/// the object, and every ship its orders fly has them; each caller says what a slot without them
/// does.
pub fn slotCruise(slot: *const create.Slot, view: camera.View) ?f32 {
const flight = slot.flight orelse return null;
return cruiseSpeed(&slot.object, flight, view);
}
test cruiseSpeed {
var object = gameobj.testing.object();
try std.testing.expectEqual(320, cruiseSpeed(&object, &gameobj.testing.flight, .chase));
// Losing half its engines and a fifth of its armor slows it.
object.engines_intact = 0.5;
object.armor_speed_factor = 0.8;
try std.testing.expectEqual(128, cruiseSpeed(&object, &gameobj.testing.flight, .chase));
// The armor tells in every view but 13, and not at all while it is invulnerable.
try std.testing.expectEqual(160, cruiseSpeed(&object, &gameobj.testing.flight, .director));
object.invulnerable = .player_can_hit;
try std.testing.expectEqual(160, cruiseSpeed(&object, &gameobj.testing.flight, .chase));
}
test slotCruise {
var slot: create.Slot = .{ .object = gameobj.testing.object(), .flight = &gameobj.testing.flight };
try std.testing.expectEqual(320, slotCruise(&slot, .chase));
slot.flight = null;
try std.testing.expectEqual(null, slotCruise(&slot, .chase));
}
/// What `ai_steer` does besides turning toward its point, as the orders and the maneuvers ask for
/// it.
pub const Steering = packed struct(u32) {
/// Move the point around the objects with listed components the ship could hit (`avoidNear`).
avoid_near: bool = false,
/// Move the point around the objects the ship is on course to hit (`avoidAhead`).
avoid_ahead: bool = false,
/// Unless avoidance took the point over, roll toward the world's Y axis as well
/// (`rollUpright`).
roll_upright: bool = false,
/// Hold the pitch at `pitch_floor` or more, so the ship keeps its nose up.
pitch_up: bool = false,
_unknown_4: u28 = 0,
/// Keeping clear of what the ship could hit, both ways (flags 3).
pub const clear: Steering = .{ .avoid_near = true, .avoid_ahead = true };
};
/// The steering's turn limit at its full, which is also the full turning input either way
/// (`0x004DC404`, `0x004DC3DC`), and no ease on the turn, which `steer` takes where it goes round
/// something and most orders give it; and the throttle at its full.
pub const full_limit: f32 = 1;
pub const no_ease: f32 = 0;
pub const full_throttle: f32 = 1;
/// The turn that fills a steering input: an input reaches 1 at five degrees off, the angle in
/// degrees over five (`0x004DC3FC`).
///
/// **Improvement:** the game holds this rounded to 11.459155, one place in the last digit below
/// the figure OpenReliant computes.
pub const input_per_radian: f32 = std.math.deg_per_rad / 5.0;
/// The turn that fills a roll input as an order rolls a ship to stand as a point's frame stands,
/// Arrive's and the station style of Dock's (`arrive`, `dock_station`): the angle in degrees over
/// forty (`0x004DC428`).
///
/// **Improvement:** the game holds this rounded to 1.4323944.
pub const roll_input_per_radian: f32 = std.math.deg_per_rad / 40.0;
/// How much of the turn rate the steering takes off its input at no ease, which damps the turn as
/// the ship comes round (`0x004DC400`).
pub const rate_damping: f32 = 6;
/// While a frame takes more than this many ticks, `steer` halves the turns under `small_turn`, so
/// a slow frame doesn't overshoot (`0x00401461`, in `ai_steer`, against `frame_duration`).
const slow_frame: i32 = 10;
/// The turns `steer` counts as small, which it halves while frames are slow: a sixteenth of a turn
/// (`0x004DC40C`).
const small_turn: f32 = std.math.pi / 8.0;
/// How much of a small turn, and of the limit, a slow frame keeps (`0x004DC408`).
const slow_frame_share: f32 = 0.5;
/// Where `Steering.pitch_up` holds the pitch input (`0x004DC3F8`).
const pitch_floor: f32 = 0.2;
/// How near its nose a direction counts as ahead: the cosine of the angle (`0x004DC414`).
const ahead_cosine: f32 = 0.95;
/// How far the roll may be off, in radians, before the ship pitches as well (`0x004DC410`).
const roll_before_pitch: f32 = 0.8;
/// `ai_steer` (`0x00401380`): the AI's steering, which the orders and the maneuvers turn a ship
/// with. It aims the ship at `at`, a point in the world, by setting its three turning inputs
/// (`turn`). Where the flags ask for avoidance, the point first moves around what the ship could
/// hit (`avoidNear`, `avoidAhead`), and the turn is then made at full limit with no ease and no
/// pitch held up. Whether avoidance moved the point.
pub fn steer(world: gameobj.World, index: u16, at: Vector, limit: f32, ease: f32, flags: Steering) bool {
var point = at;
var avoided = false;
if (flags.avoid_near) avoided = avoidNear(world, index, &point);
if (flags.avoid_ahead) avoided = avoidAhead(world, index, &point) or avoided;
turn(&world.objects.slots[index], point, limit, ease, flags, world.clock.frame_duration, avoided);
return avoided;
}
/// The rest of `ai_steer`: the turning inputs that aim the ship at `at`, banking or flat as its
/// flight stats have it (`create.FlightModel.Turns`): the angles off its nose, less
/// `(1 - ease) * 6` times each turn rate, as shares of five degrees, each held within
/// `limit` and 1. `ease` slackens the damping, so an eased turn swings further. Where `avoided`,
/// at full limit, with no ease and no pitch held up, and no roll upright.
///
/// It steers by the place the ship is drawn at, which is where the frames have moved it to since
/// the last step, since the orders run once a frame.
///
/// **Improvement:** the angles come from `std.math.atan2` rather than the engine's table
/// (`sr_atan2`), as they do elsewhere in OpenReliant.
///
/// **Fix:** the game follows a null pointer for a ship with no flight stats, a stand-in;
/// OpenReliant leaves its inputs as they are.
pub fn turn(slot: *create.Slot, at: Vector, limit_given: f32, ease_given: f32, flags_given: Steering, frame_duration: i32, avoided: bool) void {
const object = &slot.object;
const flight = slot.flight orelse return;
var flags = flags_given;
var ease = ease_given;
var limit = limit_given;
if (avoided) {
limit = full_limit;
ease = no_ease;
flags.pitch_up = false;
}
const held = limit;
object.holdTurns();
const direction = slot.drawn.inverse(at);
switch (flight.turns) {
.banking => steerAngles(object, direction, slot.motion == .backward, flags),
.flat, _ => steerAxes(object, direction),
}
// A slow frame turns the ship further than a quick one, so the small turns are halved.
if (frame_duration > slow_frame) {
var halved = false;
inline for (.{ &object.pitch_input, &object.yaw_input, &object.roll_input }) |input| {
if (@abs(input.*) < small_turn) {
input.* *= slow_frame_share;
halved = true;
}
}
if (halved) limit = held * slow_frame_share;
}
const damping = (1 - ease) * rate_damping;
object.pitch_input = (object.pitch_input - damping * object.pitch_rate) * input_per_radian;
object.yaw_input = (object.yaw_input - damping * object.yaw_rate) * input_per_radian;
object.roll_input = (object.roll_input - damping * object.roll_rate) * input_per_radian;
limit = @min(limit, full_limit);
object.pitch_input = @min(object.pitch_input, limit);
object.pitch_input = if (flags.pitch_up) @max(object.pitch_input, pitch_floor) else @max(object.pitch_input, -limit);
object.yaw_input = std.math.clamp(object.yaw_input, -limit, limit);
object.roll_input = std.math.clamp(object.roll_input, -limit, limit);
if (!avoided and flags.roll_upright) rollUpright(object, at);
}
/// `ai_arrive` (`0x00402140`) through `ai_arrive_steer` (`0x00402160`): steers the ship in slot
/// `index` to arrive at `at`, turned as `orientation`, its throttle `least` or more; whether it has
/// arrived, within `arrive_reach` of it, its throttle then `least` and its turning inputs nothing.
/// It goes by where the ship stands next (`Node.next_position`).
///
/// Where the ship stands behind `at` along the way `orientation` faces, near that line, and faces
/// that way itself, it flies at `at` (`steer`, keeping clear of what it could hit), rolling to
/// stand as `orientation` stands. Otherwise it flies round the circle that runs through it and
/// meets that line at `at`, its radius `2 * speed_per_pitch_rate` at least, aiming half a radian
/// round ahead of itself, or at `at` in the circle's last half radian; standing ahead of `at`, it
/// goes round the far side of the smallest circle. Either way it slows as it nears `at`: its
/// throttle is the way left over `arrive_steps` times its cruise speed through its inertia, less
/// `arrive_throttle_less`.
///
/// **Improvement:** the angles, sines and cosines come from `std.math` rather than the engine's
/// tables (`sr_atan2`, `sr_sin`, `sr_cos`).
///
/// **Fix:** the game follows a null pointer for a ship with no flight stats, a stand-in;
/// OpenReliant counts it as not arrived, and steers it nowhere.
pub fn arrive(world: gameobj.World, index: u16, at: Vector, orientation: math.Matrix, least: f32) bool {
const slot = &world.objects.slots[index];
const object = &slot.object;
const flight = slot.flight orelse return false;
const off = object.nextPosition() - at;
const reach = math.length(off);
if (reach < arrive_reach) {
object.throttle = least;
object.holdTurns();
return true;
}
const local = math.transformTransposed(orientation, off);
const slowing = arrive_steps * cruiseSpeed(object, flight, world.view()) / (1 - flight.inertia);
if (-local[2] > reach * ahead_cosine and
math.transformTransposed(orientation, object.nextHeading())[2] > facing_cosine)
{
_ = steer(world, index, at, full_limit, no_ease, .clear);
const up = math.transformTransposed(orientation, math.yAxis(slot.drawn.orientation));
const roll = math.halfTurn(std.math.atan2(up[0], up[1]));
object.roll_input = (roll - arrive_roll_damping * object.roll_rate) * roll_input_per_radian;
object.throttle = @max(reach / slowing - arrive_throttle_less, least);
return false;
}
const smallest = 2 * flight.speed_per_pitch_rate;
const side = math.normalize(.{ local[0], local[1], 0 });
const across = math.dot(local, side);
var radius = @abs((across * across + local[2] * local[2]) / (2 * across));
const round = std.math.atan2(local[2], radius - across);
var aim = if (round > -circle_lead and round < 0) 0 else round + circle_lead;
if (local[2] > 0) {
radius = smallest;
aim = std.math.pi;
} else if (radius < smallest) {
radius = smallest;
}
const toward = side * @as(Vector, @splat(radius - radius * @cos(aim))) + Vector{ 0, 0, radius * @sin(aim) };
_ = steer(world, index, math.transform(orientation, toward) + at, full_limit, no_ease, .clear);
const gone = if (round < 0) round + std.math.tau else round;
object.throttle = @max((std.math.tau - gone) * radius / slowing - arrive_throttle_less, least);
return false;
}
/// How near `arrive` counts as there (`0x004DC438`).
pub const arrive_reach: f32 = 2000;
/// How near the way the point faces the ship must face for `arrive` to fly straight at the point,
/// standing near its line (`ahead_cosine`): the cosine of the angle (`0x004DC434`).
const facing_cosine: f32 = 0.98;
/// How far round its circle ahead of itself `arrive` aims, in radians, and within how much of the
/// circle's end it aims at the point itself (`0x004DC408`, `0x004DC41C`).
const circle_lead: f32 = 0.5;
/// How much of its roll rate `arrive` takes off the roll as it rolls the ship to stand as the
/// point's orientation stands, before `roll_input_per_radian` turns it into the input
/// (`0x004DC42C`).
const arrive_roll_damping: f32 = 12;
/// How `arrive` slows the ship as it nears the point: over this many steps of its cruise speed,
/// through its inertia, and by this much less throttle (`0x004DC424`, `0x004DC420`).
const arrive_steps: f32 = 4;
const arrive_throttle_less: f32 = 0.1;
/// How long ahead, in steps, `avoidNear` looks for a meeting (`0x004DC448`).
const near_steps: f32 = 250;
/// `avoid_near` (`0x004028F0`): for each object of the ship's first avoidance list not standing in,
/// exploding or disabled, not far behind along the line to the point and closing along it, that it
/// would meet within 250 steps: where the line from where the ship goes next to the point crosses
/// that object's box where it will then be, widened by the ship's radius, the point moves onto the
/// box, widened again: to the face the line enters nearest, at the corner of it that is nearest the
/// point, one way or the other along each of its two other axes. Whether it moved the point.
///
/// **Quirk:** it scales the point by the object's visibility into the box's frame, and again out
/// of it.
pub fn avoidNear(world: gameobj.World, index: u16, point: *Vector) bool {
const all = world.objects;
const ship = &all.slots[index].object;
if (ship.flags.no_avoidance) return false;
const from = ship.nextPosition();
const heading = math.normalize(point.* - from);
var avoided = false;
for (ship.avoid_near.list()) |listed| {
const object = &all.slots[@intCast(listed)].object;
if (object.flags.outOfSearch()) continue;
const apart = object.nextPosition() - from;
if (math.dot(apart, heading) < -(object.radius + ship.radius)) continue;
const closing = object.velocity.vector() - ship.velocity.vector();
if (!(math.dot(heading, closing) < 0)) continue;
const steps = (math.length(apart) - ship.radius - object.radius) / math.length(closing);
if (!(steps <= near_steps)) continue;
// Where the object will be then, as it will then be turned.
const met: math.Place = .{
.position = object.nextPosition() + object.velocity.vector() * @as(Vector, @splat(steps)),
.orientation = object.root.next_orientation,
};
const scale: Vector = @splat(object.visibility);
const widen: Vector = @splat(ship.radius);
var box: [2]Vector = .{ object.bounds_min.vector() - widen, object.bounds_max.vector() + widen };
const start = met.inverse(from) * scale;
const end = met.inverse(point.*) * scale;
const share = objects.boxEntry(start, end, box) orelse continue;
const entry = math.lerp(start, end, share);
box[0] -= widen;
box[1] += widen;
point.* = met.point(roundBox(box, entry, end) * scale);
avoided = true;
}
return avoided;
}
/// Where `avoidNear` moves a point entering `box` at `entry` and heading for `end`: on the face
/// nearest the entry, the nearest to `end` of four points of it, at the entry along one of its
/// two other axes and at an edge of the box along the other.
fn roundBox(box_given: [2]Vector, entry_given: Vector, end: Vector) Vector {
const box: [2][3]f32 = .{ box_given[0], box_given[1] };
const entry: [3]f32 = entry_given;
var face: [3]f32 = undefined;
var nearest: f32 = std.math.floatMax(f32);
var axis: usize = 0;
for (0..3) |i| {
const low = @abs(entry[i] - box[0][i]);
const high = @abs(box[1][i] - entry[i]);
face[i] = if (low <= high) box[0][i] else box[1][i];
const gap = @min(low, high);
if (gap < nearest) {
nearest = gap;
axis = i;
}
}
const across = (axis + 1) % 3;
const up = (axis + 2) % 3;
var best: Vector = undefined;
var best_distance: f32 = std.math.floatMax(f32);