Repository navigation
Expand file tree
/
Copy pathlaunch.zig
More file actions
910 lines (832 loc) · 45.3 KB
/
Copy pathlaunch.zig
File metadata and controls
910 lines (832 loc) · 45.3 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
//! `C:\lancer\game\launch.cpp`: order 104, Launch. `order_launch_init` (`0x00418EB0`)
//! prepares a ship to leave its carrier, and `order_launch` (`0x004191C0`) updates it.
//! The ship and carrier types select a style, with an init and update routine in the table
//! at `0x004E3C98`. OpenReliant runs all ten styles: the Yamato
//! ([`launch/yamato.zig`](launch/yamato.zig)), the Reliant
//! ([`launch/reliant.zig`](launch/reliant.zig)), the torpedoes
//! ([`launch/torpedo.zig`](launch/torpedo.zig)), the hangar bays
//! ([`launch/bay.zig`](launch/bay.zig)), the Badanov and the Krasny
//! ([`launch/badanov.zig`](launch/badanov.zig)), the escape pods
//! ([`launch/escape_pod.zig`](launch/escape_pod.zig)), the rogue base
//! ([`launch/rogue_base.zig`](launch/rogue_base.zig)), the Stork
//! ([`launch/stork.zig`](launch/stork.zig)) and the Zakov ([`launch/zakov.zig`](launch/zakov.zig)).
//! **Unverified:** the source-file assignment of the adjacent gate search, StartLaunch handler,
//! style routines and launch-point placement. Their behaviour matches this file's known code;
//! the next known source file is `tractor.cpp`.
//! [Launches](../../../docs/engine/launch.md) describes them.
const std = @import("std");
const assert = std.debug.assert;
const layout = @import("../../formats/layout.zig");
const engine = @import("../../engine.zig");
const shp = @import("../../formats/shp.zig");
const math = @import("../surrender/math.zig");
const ai = @import("ai.zig");
const aigeneric = @import("aigeneric.zig");
const camera = @import("camera.zig");
const create = @import("create.zig");
const models = @import("create/models.zig");
const events = @import("mission/events.zig");
const gameobj = @import("gameobj.zig");
const objects = @import("objects.zig");
const sound3d = @import("sound3d.zig");
const radio = @import("radio.zig");
const xtrabits = @import("xtrabits.zig");
const srapiext = @import("../surrender/surrenderlib/srapiext.zig");
const srmesh = @import("../surrender/surrenderlib/srmesh.zig");
const files = @import("../files.zig");
const log = std.log.scoped(.launch);
pub const badanov = @import("launch/badanov.zig");
pub const bay = @import("launch/bay.zig");
pub const escape_pod = @import("launch/escape_pod.zig");
pub const reliant = @import("launch/reliant.zig");
pub const rogue_base = @import("launch/rogue_base.zig");
pub const stork = @import("launch/stork.zig");
pub const torpedo = @import("launch/torpedo.zig");
pub const zakov = @import("launch/zakov.zig");
pub const yamato = @import("launch/yamato.zig");
/// Launch styles (`launch_styles`, `0x004E3C98`). Each has an init routine that places the
/// ship and selects its riding node, and an update routine that runs the style's steps.
pub const Style = enum(i32) {
/// From a hangar bay (`0x0041A610`, `0x0041A9C0`): the Victorious's, the Endeavour's, the
/// Mitchells', the Bremen's, the Ramases's, the Pukov's, the Kronstadt's, the Krasnaya's, the
/// Varyag's and the Kiev's, and the rogue base's from its seventh gate on.
bay = 0,
/// From the Yamato (`launch_yamato_init`, `0x004192C0`, and `launch_yamato_run`, `0x00419840`).
yamato = 1,
/// From the Badanov and the Krasny (`launch_badanov_init`, `0x00419F60`, and
/// `launch_badanov_run`, `0x0041A100`).
badanov = 2,
/// A torpedo from its tube (`0x0041A360`, `0x0041A390`), whatever it launches from.
torpedo = 3,
/// An escape pod (`launch_point_init`, `0x0041A4B0`, and `launch_pod_run`, `0x0041A4D0`).
escape_pod = 4,
/// From the Stork (`launch_point_init`, `0x0041A4B0`, and `launch_stork_run`, `0x0041AD10`).
stork = 5,
/// From the Reliant (`0x0041AE20`, `0x0041B240`).
reliant = 6,
/// The other escape pod (`launch_point_init`, `0x0041A4B0`, and `launch_pod_other_run`,
/// `0x0041B690`).
other_escape_pod = 7,
/// From the rogue base's first six gates (`launch_rogue_init`, `0x0041B770`, and
/// `launch_rogue_run`, `0x0041B7F0`).
rogue_base = 8,
/// From the Zakov (`0x0041B8B0`, `0x0041B940`).
zakov = 9,
_,
/// Selects the style as `order_launch_init` does: torpedoes and escape pods use their own
/// types; other ships use the carrier's type and gate (`ofCarrier`).
///
/// **Fix:** the game stops with the fatal assertion "Error. Trying to launch from %s."
/// (`0x00419049`) for a carrier with no style of its own. OpenReliant launches from it as from a
/// hangar bay, from its model's launch points.
pub fn of(ship: gameobj.Type, carrier: gameobj.Type, gate: i16) Style {
return switch (ship.base()) {
.torpedo, .russian_torpedo => .torpedo,
.escape_pod => .escape_pod,
.other_escape_pod => .other_escape_pod,
else => ofCarrier(carrier, gate) orelse .bay,
};
}
/// The style of a launch from a carrier of type `carrier` through gate `gate`, by the carrier's
/// switch in `order_launch_init` (`0x00418F39`), or null for a carrier with no style of its
/// own. **Improvement:** a mod's ship type launches as its base does where it keeps its base's
/// model, whose parts the base's style moves, and has no style of its own where it has a model
/// of its own (`launchesAsBase`).
pub fn ofCarrier(carrier: gameobj.Type, gate: i16) ?Style {
if (!launchesAsBase(carrier)) return null;
return switch (carrier.base()) {
.reliant => .reliant,
.yamato => .yamato,
.victorious, .endeavour, .mitchell, .bremen, .ramases, .pukov, .kronstadt, .krasnaya, .varyag, .other_ramases, .other_mitchell, .kiev => .bay,
.stork => .stork,
.badanov, .krasny => .badanov,
.rogue_base => if (gate < rogue_base_gates) .rogue_base else .bay,
.zakov => .zakov,
else => null,
};
}
/// Whether the style's own steps start the player's engine's sound, as the game's Reliant and
/// Yamato launches do; for the others, `update` starts it as the ship lets go.
fn startsEngine(style: Style) bool {
return style == .reliant or style == .yamato;
}
/// The init and update pair from `launch_styles` (`0x004E3C98`), or null for an unknown style.
pub fn routines(style: Style) ?Routines {
return switch (style) {
.bay => .{ .init = &bay.init, .run = &bay.run },
.reliant => .{ .init = &reliant.init, .run = &reliant.run },
.torpedo => .{ .init = &torpedo.init, .run = &torpedo.run },
.stork => .{ .init = &attachAtGate, .run = &stork.run },
.zakov => .{ .init = &zakov.init, .run = &zakov.run },
.badanov => .{ .init = &badanov.init, .run = &badanov.run },
.escape_pod => .{ .init = &attachAtGate, .run = &escape_pod.run },
.other_escape_pod => .{ .init = &attachAtGate, .run = &escape_pod.runOther },
.rogue_base => .{ .init = &rogue_base.init, .run = &rogue_base.run },
.yamato => .{ .init = &yamato.init, .run = &yamato.run },
_ => null,
};
}
pub fn format(style: Style, writer: *std.Io.Writer) std.Io.Writer.Error!void {
return layout.formatTagAs(Style, style, "style", writer);
}
};
/// A style's two routines (`launch_styles`, `0x004E3C98`, 24 bytes a style): the first places the
/// ship in slot `index` for its launch from the carrier in slot `carrier` and names the node it
/// rides, the second runs its launch an update at a time from step 2 on (`Step.styled`).
pub const Routines = struct {
init: *const fn (ctx: aigeneric.Context, index: u16, carrier: u16) void,
run: *const fn (ctx: aigeneric.Context, index: u16) void,
};
/// The rogue base's gates that launch in its own style; those after launch from a bay
/// (`0x00418F8A`).
const rogue_base_gates = 6;
/// A launch's steps as `order_launch` runs them: waiting for StartLaunch, then a moment more
/// (`most_delay`), then its style's own, numbered from `styled` on.
pub const Step = enum(i32) {
waiting = 0,
delaying = 1,
_,
/// The first of the style's own steps, which `order_launch` sets once the wait is over
/// (`0x0041928A`): each style's own step enum starts its steps there.
pub const styled: Step = @fromBackingInt(2);
/// Whether the style's own steps have begun.
pub fn isStyled(step: Step) bool {
return step.reached(styled);
}
/// Whether the launch is at step `other`, the launch's or its style's own, or past it, by
/// their numbers as the game compares them.
pub fn reached(step: Step, other: anytype) bool {
return @backingInt(step) >= @backingInt(of(other));
}
/// The step as a style's own steps, `Styled`, number it.
pub fn as(step: Step, comptime Styled: type) Styled {
comptime assert(@typeInfo(Styled).@"enum".tag_type == i32);
return @fromBackingInt(@backingInt(step));
}
/// A style's own step as a launch's.
pub fn of(own: anytype) Step {
comptime assert(@typeInfo(@TypeOf(own)).@"enum".tag_type == i32);
return @fromBackingInt(@backingInt(own));
}
/// The step after it.
pub fn next(step: Step) Step {
return @fromBackingInt(@backingInt(step) + 1);
}
};
/// The quarter turn the Yamato's and the Badanov's styles turn a ship by about its Y axis
/// (`0x004193AF`, `0x0041A00C`): the float nearest to half of pi, `0x3FC90FDB`.
pub const quarter_turn: f32 = std.math.pi / 2.0;
comptime {
// Each style's own steps begin where the launch's leave off.
assert(Step.of(reliant.Step.start) == Step.styled);
assert(Step.of(torpedo.Step.fire) == Step.styled);
assert(Step.of(zakov.Step.leave) == Step.styled);
assert(Step.of(bay.Step.open) == Step.styled);
assert(Step.of(stork.Step.leave) == Step.styled);
assert(Step.of(badanov.Step.open) == Step.styled);
assert(Step.of(escape_pod.Step.leave) == Step.styled);
assert(Step.of(escape_pod.OtherStep.sound) == Step.styled);
assert(Step.of(rogue_base.Step.leave) == Step.styled);
assert(Step.of(yamato.Step.release) == Step.styled);
}
/// What Launch keeps in the object's order state.
pub const State = extern struct {
style: Style,
/// The frame's tick after which its next step runs.
due: i32,
step: Step,
/// Where the ship stands in the frame of the node it rides, and how it is turned there.
position: shp.Vec3,
orientation: math.Matrix,
/// Whether it rides its node, which each frame's pass places it on (`hold`).
attached: bool,
_unknown_3d: [3]u8,
/// The node it rides, which its style names: the address OpenReliant keeps as
/// `create.Slot.riding`.
node: engine.Pointer(objects.Node),
/// How many launch points the search for a gate passes over (`GateSearch`): its order's place
/// among those its command gave (`aigeneric.Entry.sequence`).
sequence: i32,
/// The carrier, and the gate on it, the search found.
carrier: i32,
gate: i32,
/// The nodes of a bay's doors, which `launch_bay_init` keeps. OpenReliant looks them up from
/// the carrier and the gate instead (`bay.Doors`).
doors: [2]engine.Pointer(objects.Node),
_unknown_58: [aigeneric.state_size - 0x58]u8,
comptime {
assert(@offsetOf(State, "due") == 0x04);
assert(@offsetOf(State, "step") == 0x08);
assert(@offsetOf(State, "position") == 0x0C);
assert(@offsetOf(State, "orientation") == 0x18);
assert(@offsetOf(State, "attached") == 0x3C);
assert(@offsetOf(State, "node") == 0x40);
assert(@offsetOf(State, "sequence") == 0x44);
assert(@offsetOf(State, "carrier") == 0x48);
assert(@offsetOf(State, "gate") == 0x4C);
assert(@offsetOf(State, "doors") == 0x50);
assert(@sizeOf(State) == aigeneric.state_size);
}
/// Moves on to step `next`, which runs once `wait` more ticks have passed from `now`.
pub fn advance(state: *State, next: Step, now: i32, wait: i32) void {
state.step = next;
state.due = now + wait;
}
/// Moves on from the style's own step `from` to the one after it, which runs once `wait` more
/// ticks have passed from `now`.
pub fn moveOn(state: *State, from: anytype, now: i32, wait: i32) void {
state.advance(Step.of(from).next(), now, wait);
}
};
/// What Launch keeps in its entry's data (`aigeneric.Entry.Data`).
pub const Data = extern struct {
/// Set by StartLaunch (`start`): the launch goes.
go: bool,
};
/// How long a launch waits after its start before its style's first step, at most, in ticks: a
/// random share of it (`order_launch`, `0x00419254`).
const most_delay = 200;
/// How long the init has a launch wait before its next step (`order_launch_init`, `0x00419023`),
/// which no step reads before the start sets its own.
const init_wait = 200;
/// Whether a carrier of type `carrier` launches as the game's type it is based on: one of the
/// game's types, or a mod's type that keeps its base's model, whose parts and launch points the
/// base's style and doors are made for. A mod's type with a model of its own launches as a
/// hangar bay, without doors.
pub fn launchesAsBase(carrier: gameobj.Type) bool {
const added = carrier.added() orelse return true;
const base_number = std.math.cast(create.TypeIndex, @backingInt(added.base)) orelse return false;
const base = create.shipFiles(base_number).model orelse return false;
return std.ascii.eqlIgnoreCase(files.leaf(added.extra.model), base);
}
/// `order_launch_init` (`0x00418EB0`): prepares the ship in slot `index` to launch. For a flight
/// group, squad or target without a gate, `GateSearch` walks the target's ships (`ai.eachShip`).
/// It writes the carrier's slot and gate into the target, preserving its kind. The ship and
/// carrier types select the style (`Style.of`), whose init places the ship and selects the node
/// it rides. The order stores its position and orientation relative to that node. The ship
/// passes through its carrier and cannot be targeted until the launch ends.
///
/// **Fix:** the original asserts for a carrier with no style of its own, and dereferences a missing
/// launch node. OpenReliant launches from such a carrier as from a hangar bay (`Style.of`), and a
/// ship without a launch node stays where it was placed.
///
/// **Improvement:** the game records the ship the player launched from (`player_carrier`) only in
/// the Reliant's and the Yamato's launches, the only ones the original's missions give the player.
/// OpenReliant records it for every style, so that PERMISSION TO LAND and the bridge's lines work
/// from any carrier a mission launches the player from.
pub fn init(ctx: aigeneric.Context, index: u16) void {
const all = ctx.world.objects;
const slot = &all.slots[index];
const entry = &slot.orders[0];
const state = &slot.state.launch;
if (entry.target.kind != .ship or entry.target.isWhole()) {
state.carrier = 0;
state.gate = 0;
state.sequence = entry.sequence;
var search: GateSearch = .{ .all = all, .state = state };
_ = ai.eachShip(ctx.world, entry.target, &search);
entry.target.index = @truncate(state.carrier);
entry.target.component = @truncate(state.gate);
}
const carrier = entry.target.slotIn(all) orelse return;
if (index == all.player) ctx.world.player.carrier = carrier;
const carrier_type = all.slots[carrier].object.type;
state.style = Style.of(slot.object.type, carrier_type, entry.target.component);
state.step = .waiting;
slot.riding = null;
if (state.style.routines()) |found| found.init(ctx, index, carrier);
if (if (slot.riding) |riding| riding.place(all) else null) |node| {
const relative = slot.drawn.relativeTo(node);
state.position = .of(relative.position);
state.orientation = relative.orientation;
state.attached = true;
}
state.due = ctx.world.clock.frame_start + init_wait;
slot.object.passes_through[0] = .of(carrier);
ai.setTargetable(&slot.object, slot.combat, false);
}
/// `order_launch` (`0x004191C0`): runs the launch of the ship in slot `index`, once an update.
/// Until the style's own steps begin, a ship whose carrier is gone (a stand-in or exploding) is
/// destroyed with it (`ai.objectDestroyed`). The exception is an escape pod leaving the Ulysses.
/// Once StartLaunch sets `Data.go`, the launch waits a random delay of up to `most_delay` ticks,
/// drawn from the ship's own numbers (`xtrabits.objectRandom15`), and then the style runs it from
/// step 2. When the player's launch starts, the radio plays its line (`radio.launchLine`).
///
/// **Fix:** for a Launch aimed at nothing, the game reads the carrier from the word before its
/// object table (`0x00587CDC`, `mission25_second_part`). Its assertion "Launch Crash Imminent"
/// (`0x004191E6`) compares the sign-extended index with 0xFFFF, so it never fires. OpenReliant lets
/// the ship go (`letGo`).
pub fn update(ctx: aigeneric.Context, index: u16) void {
const all = ctx.world.objects;
const slot = &all.slots[index];
const entry = &slot.orders[0];
const state = &slot.state.launch;
const now = ctx.world.clock.frame_start;
// **Fix:** the player's engine is heard as its ship lets go of the carrier, where the style
// doesn't start it itself (`sound3d.hearEngine`).
if (index == all.player and state.step.isStyled() and !state.attached and !state.style.startsEngine()) sound3d.hearEngine(ctx.world);
if (!state.step.isStyled()) {
const carrier = entry.target.slotIn(all) orelse return letGo(ctx, index);
const from = &all.slots[carrier].object;
if (from.gone() and !(from.type.base() == .ulysses and slot.object.type.base() == .escape_pod)) {
ai.objectDestroyed(ctx, index, false, false);
return;
}
if (entry.data.launch.go and state.step == .waiting) {
state.advance(.delaying, now, @intCast(xtrabits.objectRandom15(&slot.object) % most_delay));
if (index == all.player) radio.launchLine(ctx.world, carrier);
}
if (state.step == .delaying and state.due < now) state.step = Step.styled;
}
if (state.style.routines()) |found| found.run(ctx, index) else if (state.step.isStyled()) letGo(ctx, index);
}
/// The end of the player's launch from a carrier with a hangar, as the last steps of
/// `launch_reliant_run` (`0x0041B240`) and `launch_yamato_run` (`0x00419840`) have it: the caption
/// stops, the camera takes the cockpit's mode again and goes back to the cockpit from any of the
/// launch's `views`, and the player's ship shows everything again.
pub fn endForPlayer(world: gameobj.World, index: u16, views: []const camera.View) void {
if (world.display) |display| display.caption.stop();
world.player.viewing.cockpit_mode = world.player.viewing.cockpit_setting.mode();
if (std.mem.indexOfScalar(camera.View, views, world.player.viewing.view) != null) _ = camera.switchView(world, .cockpit, index, false, true);
world.player.showing = .everything;
}
/// Ends the launch and clears the carrier pass-through entry, as the last step of
/// `launch_reliant_run` does (`0x0041B639`). Unknown styles use this when the launch starts.
pub fn letGo(ctx: aigeneric.Context, index: u16) void {
ctx.world.objects.slots[index].object.passes_through[0] = .none;
finish(ctx, index);
}
/// Pops the Launch order, restores targeting and posts the Launched event (`events.launched`).
/// Styles that preserve the carrier pass-through entry call this directly. **Fix:** the player's
/// ship has its engine heard, where its launch didn't start it (`sound3d.hearEngine`).
pub fn finish(ctx: aigeneric.Context, index: u16) void {
const slot = &ctx.world.objects.slots[index];
slot.riding = null;
aigeneric.end(ctx, index);
ai.setTargetable(&slot.object, slot.combat, true);
if (index == ctx.world.objects.player) sound3d.hearEngine(ctx.world);
events.launched(ctx.world, index);
}
/// `launch_start` (`0x00418DB0`): starts the first Launch among the orders of the ship in slot
/// `index` (`Data.go`), as StartLaunch and the Kamov's LAUNCH MISSILE do. A ship without one is
/// left alone.
pub fn start(all: *create.Objects, index: u16) void {
if (all.slots[index].firstOrder(.launch)) |entry| entry.data.launch.go = true;
}
/// Whether the ship in slot `index` is dropping out of the Reliant, or about to: its current order
/// is Launch in the Reliant's style, at `reliant.Step.drop` or later. `camera_frame` reads this for
/// the bay's view.
pub fn dropping(all: *const create.Objects, index: u16) bool {
const slot = &all.slots[index];
if (slot.running(.launch) == null) return false;
const state = slot.state.launch;
return state.style == .reliant and state.step.reached(reliant.Step.drop);
}
/// `mission_frame`'s placing of a ship that rides a node (`0x00492C14`), once a frame before the
/// ship is drawn (`main.frameObjects`). If the current order of the ship in slot `index` is Launch,
/// its carrier isn't exploding and the ship rides its node, the ship is put back where the launch
/// placed it on the node, turned as it was. Returns whether it placed the ship.
pub fn hold(all: *create.Objects, index: u16) bool {
const slot = &all.slots[index];
const entry = slot.running(.launch) orelse return false;
const carrier = entry.target.slotIn(all) orelse return false;
if (all.slots[carrier].object.flags.exploding) return false;
const state = &slot.state.launch;
if (!state.attached) return false;
const node = (slot.riding orelse return false).place(all) orelse return false;
const riding: math.Place = .{ .position = state.position.vector(), .orientation = state.orientation };
objects.setPlace(&slot.object, &slot.drawn, riding.within(node));
return true;
}
// --- Launch points ------------------------------------------------------------------------------
/// The launch points of a model (`launch_find_gate`, `launch_attach`), part by part in the order of
/// the root's child list, and each part's attachments in order. A launch point is an attachment of
/// kind `launch_point`, or of kind `pod` where the pod table has no model at the part's index.
///
/// **Quirk:** the game looks pods up by the index of the part that holds the attachment, not by
/// the attachment's id, which names the pod it mounts.
pub const Points = objects.Model.RootAttachments(isPoint);
/// A launch point, and the part that holds it.
pub const Point = Points.Point;
/// Whether an attachment is a launch point: one of that kind, or a pod's where the part holds no
/// pod (`podless`), which picks a model's launch points for `Points`.
fn isPoint(attachment: shp.Attachment, part: usize) bool {
return switch (attachment.kind) {
.launch_point => true,
.pod => podless(part),
else => false,
};
}
/// Whether the pod table has no model at `place` (`attachment_models`, kind 5). Past the pods' ids
/// the game reads on into the next kinds' entries, none of which has a model.
fn podless(place: usize) bool {
const id = std.math.cast(u32, place) orelse return true;
const entry = models.attachment(.pod, id) orelse return true;
return entry.model == null;
}
/// `launch_find_gate` (`0x00418DF0`): the search for a gate that `init` runs over each ship the
/// order's target names (`ai.eachShip`). Each ship it visits becomes the carrier, with the gate
/// counted from 0 again, and each of its launch points (`Points`) takes one off the sequence, which
/// carries on from ship to ship. The point that takes the sequence below 0 ends the search, and its
/// index among that ship's points is the gate.
const GateSearch = struct {
all: *const create.Objects,
state: *State,
/// Takes `carrier` as the carrier, its gates counted from 0 again, and counts its launch points
/// off the sequence: whether one took it below 0, which is the gate and ends the walk.
pub fn visit(search: *GateSearch, carrier: aigeneric.Target) bool {
const state = search.state;
const index = carrier.slotIn(search.all) orelse return false;
state.carrier = index;
state.gate = 0;
const model = if (search.all.slots[index].model) |*held| held else return false;
var points: Points = .of(model);
while (points.next()) |_| {
state.sequence -= 1;
if (state.sequence < 0) return true;
state.gate += 1;
}
return false;
}
};
/// The parts of a launch's hangar that its dim light alone reaches: its hull and its two doors, the
/// first three of its root's child list (`0x0041B10C`, `0x00419670`).
const hangar_lit_parts = 3;
/// The light mask a launch's hangar's hull and doors take (`0x0041B10C`, `0x00419670`): every light
/// of the backdrop's but its first ambient (`backdrop.Lights`, `0x04`) is kept out, so that only
/// their baked colours, the ambient's glimmer and the lights that reach every object light them.
const hangar_light_mask: u32 = 0x3B;
/// The hangar the player's ship launches in, as the Reliant's and the Yamato's launches make it
/// (`launch_reliant_init`, `0x0041B10C`; `0x00419670`): an object of type `kind` in the cutaway
/// slot, passing through everything, its hull and doors lit by its dim light alone
/// (`hangar_light_mask`). Its slot, or null where it can't be made, which is logged as the hangar
/// of `carrier`.
pub fn makeHangar(world: gameobj.World, kind: gameobj.Type, carrier: []const u8) ?u16 {
const hangar = create.make(world, create.cutaway_slot, kind) catch |err| {
log.warn("{s}'s hangar is left out: {s}", .{ carrier, @errorName(err) });
return null;
} orelse return null;
const shown = &world.objects.slots[hangar];
shown.object.flags.no_collisions = true;
if (shown.model) |*model| for (model.parts[0..@min(hangar_lit_parts, model.parts.len)]) |*part| {
part.object.light_mask = hangar_light_mask;
};
return hangar;
}
test makeHangar {
const gpa = std.testing.allocator;
var mission: gameobj.testing.Mission = undefined;
try mission.init(gpa);
defer mission.deinit();
var parts: objects.testing.Parts(hangar_lit_parts + 1) = undefined;
parts.init();
const kind: create.Type = .{ .model = &parts.source, .loaded = &parts.loaded };
var world = mission.world();
world.spawn = mission.spawn(create.testing.oneType(&kind));
_ = try mission.add(.of(.predator), @splat(0));
// The hangar stands in the cutaway slot, passing through everything, its hull and its doors
// lit by its dim light alone and its other parts as any part is.
const hangar = makeHangar(world, .of(.reliant_hangar), "the Reliant").?;
try std.testing.expectEqual(create.cutaway_slot, hangar);
const shown = mission.slot(hangar);
try std.testing.expect(shown.object.flags.no_collisions);
const model = &shown.model.?;
for (model.parts[0..hangar_lit_parts]) |part| try std.testing.expectEqual(hangar_light_mask, part.object.light_mask);
try std.testing.expect(model.parts[hangar_lit_parts].object.light_mask != hangar_light_mask);
}
/// `launch_attach` (`0x0041B9F0`): places the ship in slot `index` at launch point `gate` of the
/// object in slot `on`, counting from 0 over its points (`Points`). The ship's centre of mass stands
/// at the point and it is turned as the point is, and it rides the part that holds the point
/// (`State.node`, `create.Slot.riding`). If the object has no such point, the ship stays where it
/// is. The game reads the gate from the component of the ship's order's target, which
/// `launch_reliant_init` overwrites with the hangar's point and then restores (`0x0041B15E`,
/// `0x0041B174`, `0x0041B206`). OpenReliant passes the gate instead.
pub fn attach(all: *create.Objects, index: u16, on: u16, gate: i16) void {
const slot = &all.slots[index];
const found = gateOf(all, on, gate) orelse return;
slot.riding = .{ .object = on, .part = found.part };
const at = found.place;
objects.setPlace(&slot.object, &slot.drawn, .{ .position = at.point(slot.object.centre.vector()), .orientation = at.orientation });
}
/// A launch point of an object: the part that holds it, and where it stands and how it is turned
/// in the world, within that part's frame as the object was drawn last.
pub const Gate = struct {
part: usize,
place: math.Place,
};
/// Launch point `gate` of the object in slot `on`, counting from 0 over its points (`Points`), or
/// null where the object has no model or no such point. A ship launches out along the place's Z
/// axis, and lands back in against it (`ailand`).
pub fn gateOf(all: *const create.Objects, on: u16, gate: i16) ?Gate {
const holder = &all.slots[on];
const model = if (holder.model) |*held| held else return null;
const point = Points.nth(model, std.math.cast(usize, gate) orelse return null) orelse return null;
const frame = model.frameAt(point.part, holder.drawn);
const standing: math.Place = .{ .position = point.attachment.position.vector(), .orientation = point.attachment.orientation };
return .{ .part = point.part, .place = standing.within(frame) };
}
/// `launch_point_init` (`0x0041A4B0`), the first routine of the escape pods' styles and the
/// Stork's: places the ship in slot `index` at the launch point of the carrier in slot `carrier`
/// that its gate names (`attach`), riding the part that holds it. The first routines of the bays,
/// the torpedoes and the Zakov start the same way.
pub fn attachAtGate(ctx: aigeneric.Context, index: u16, carrier: u16) void {
const all = ctx.world.objects;
attach(all, index, carrier, all.slots[index].orders[0].target.component);
}
test {
std.testing.refAllDecls(@This());
}
test "Step.as" {
// A style's own step, as a launch's and back.
try std.testing.expectEqual(reliant.Step.drop, Step.of(reliant.Step.drop).as(reliant.Step));
try std.testing.expectEqual(torpedo.Step.boost, Step.of(torpedo.Step.boost).as(torpedo.Step));
try std.testing.expect(Step.of(torpedo.Step.fire).isStyled());
try std.testing.expect(!Step.delaying.isStyled());
}
test "Step.reached" {
// A step reaches itself and the steps before it, the launch's and its style's alike.
const drop: Step = .of(reliant.Step.drop);
try std.testing.expect(drop.reached(reliant.Step.drop));
try std.testing.expect(drop.reached(Step.waiting));
try std.testing.expect(!Step.delaying.reached(reliant.Step.drop));
try std.testing.expect(drop.next().reached(drop));
}
test "Style.of" {
// Torpedoes and escape pods go by their own type, whatever launches them.
try std.testing.expectEqual(Style.torpedo, Style.of(.of(.russian_torpedo), .of(.reliant), 0));
try std.testing.expectEqual(Style.other_escape_pod, Style.of(.of(.other_escape_pod), .of(.kamov), 0));
// Any other ship by its carrier's.
try std.testing.expectEqual(Style.reliant, Style.of(.of(.predator), .of(.reliant), 3));
try std.testing.expectEqual(Style.badanov, Style.of(.of(.sabre), .of(.krasny), 0));
try std.testing.expectEqual(Style.bay, Style.of(.of(.sabre), .of(.kiev), 0));
// The rogue base's first six gates have a style of their own.
try std.testing.expectEqual(Style.rogue_base, Style.of(.of(.sabre), .of(.rogue_base), 5));
try std.testing.expectEqual(Style.bay, Style.of(.of(.sabre), .of(.rogue_base), 6));
// A carrier with no style of its own launches as a hangar bay, from its model's launch points.
try std.testing.expectEqual(null, Style.ofCarrier(.of(.kurgan), 0));
try std.testing.expectEqual(Style.bay, Style.of(.of(.sabre), .of(.kurgan), 0));
var buffer: [16]u8 = undefined;
try std.testing.expectEqualStrings("reliant", try std.mem.print(&buffer, "{f}", .{Style.reliant}));
try std.testing.expectEqualStrings("style 12", try std.mem.print(&buffer, "{f}", .{@as(Style, @fromBackingInt(12))}));
// Every named style has its original pair of routines.
for (std.enums.values(Style)) |style| {
const runs = switch (style) {
.bay, .reliant, .torpedo, .stork, .zakov, .badanov, .escape_pod, .other_escape_pod, .rogue_base, .yamato => true,
_ => false,
};
try std.testing.expectEqual(runs, style.routines() != null);
}
}
/// Fixtures for the launches' tests.
pub const testing = struct {
/// A carrier's model of three parts hanging from the root, 100 apart along X. The first holds
/// no launch point, only a missile's hardpoint; the second two, 10 and 20 along Z, turned half
/// a turn about Y, with a pod's between them, which the pod table has a model for at the second
/// part's place; the third a pod's, which the table has none for at the third's. Set it up
/// where it stays, as its records point into it.
pub const Carrier = struct {
attachments: [5]shp.Attachment,
parts: objects.testing.Parts(3),
pub fn init(carrier: *Carrier) void {
const kinds = [_]shp.Attachment.Kind{ .missile, .launch_point, .pod, .launch_point, .pod };
const along = [_]f32{ 0, 10, 15, 20, 0 };
for (&carrier.attachments, kinds, along) |*attachment, kind, z| {
attachment.* = std.mem.zeroes(shp.Attachment);
attachment.kind = kind;
attachment.position = .{ .x = 0, .y = 0, .z = z };
attachment.orientation = math.rotation(.y, std.math.pi);
}
carrier.parts.init();
for (&carrier.parts.data, 0..) |*part, n| part.part.position = .{ .x = @floatFromInt(100 * n), .y = 0, .z = 0 };
carrier.parts.data[0].attachments = carrier.attachments[0..1];
carrier.parts.data[1].attachments = carrier.attachments[1..4];
carrier.parts.data[2].attachments = carrier.attachments[4..5];
}
};
/// A carrier's model of `count` parts, each drawing a level with the bounds `init` gives and
/// holding the doors' track (`objects.door_track`), for the styles that place a ship by a
/// part's bounds. Set it up where it stays, as its records point into it.
pub fn Bounded(comptime count: usize) type {
return struct {
mesh: srapiext.Mesh,
levels: [1]srapiext.Level,
tracks: [1]shp.Track,
parts: objects.testing.Parts(count),
const Model = @This();
pub fn init(model: *Model, gpa: std.mem.Allocator, bounds: [2]math.Vector) !void {
model.mesh = try srmesh.testing.square(gpa);
model.mesh.bounds = bounds;
model.levels = .{.{ .mesh = &model.mesh, .until = std.math.inf(f32) }};
model.tracks = .{.{ .clip = objects.testing.clip(100, .once, objects.door_track), .keyframes = &.{}, .events = &.{} }};
model.parts.init();
for (&model.parts.loaded_parts, &model.parts.data) |*part, *data| {
part.levels = &model.levels;
data.tracks = &model.tracks;
}
}
pub fn deinit(model: *Model, gpa: std.mem.Allocator) void {
model.mesh.deinit(gpa);
}
};
}
/// Whether the launch of the ship in `slot` is over: its current order is no longer Launch.
pub fn ended(slot: *const create.Slot) bool {
const entry = slot.current() orelse return true;
return entry.order != .launch;
}
/// Moves the clock past the wait of the launch of the ship in slot `index`, and runs its
/// orders once.
pub fn pastDue(mission: *gameobj.testing.Mission, ctx: aigeneric.Context, index: u16) void {
mission.clock.frame_start = mission.slot(index).state.launch.due + 1;
aigeneric.objectOrders(ctx, index);
}
};
test Points {
var carrier: testing.Carrier = undefined;
carrier.init();
var model = try carrier.parts.create(std.testing.allocator);
defer model.deinit(std.testing.allocator);
// The second part's two points, not the pod between them, which has a model at its place, and
// the third's pod, which has none: a quirk of the game's.
var points: Points = .of(&model);
const expected = [_]struct { usize, f32 }{ .{ 1, 10 }, .{ 1, 20 }, .{ 2, 0 } };
for (expected) |point| {
const found = points.next().?;
try std.testing.expectEqual(point[0], found.part);
try std.testing.expectEqual(point[1], found.attachment.position.z);
}
try std.testing.expectEqual(null, points.next());
// A part taken out of its model holds none.
model.parts[1].removed = true;
points = .of(&model);
try std.testing.expectEqual(2, points.next().?.part);
}
test "a launch stands its ship at its gate's point, riding it, until it starts" {
const gpa = std.testing.allocator;
var mission: gameobj.testing.Mission = undefined;
try mission.init(gpa);
defer mission.deinit();
var carrier_model: testing.Carrier = undefined;
carrier_model.init();
_ = try mission.add(.of(.predator), @splat(0));
const carrier = try mission.add(.of(.kamov), .{ 1000, 0, 0 });
try carrier_model.parts.fit(gpa, mission.slot(carrier));
mission.slot(carrier).object.velocity = .{ .x = 0, .y = 0, .z = 5 };
const torpedo_slot = try mission.add(.of(.torpedo), @splat(0));
const ctx = mission.orders();
// Through its second gate, the second part's second point: its centre there, turned as the
// point is, riding the part, passing through the carrier and not to be targeted.
_ = try aigeneric.pushShip(ctx, torpedo_slot, .launch, carrier, 1);
mission.slot(torpedo_slot).object.flags.targetable = true;
aigeneric.objectOrders(ctx, torpedo_slot);
const slot = mission.slot(torpedo_slot);
const state = &slot.state.launch;
try std.testing.expectEqual(Style.torpedo, state.style);
try std.testing.expectEqual(objects.NodeOf{ .object = carrier, .part = 1 }, slot.riding.?);
try std.testing.expectEqual(math.Vector{ 1100, 0, 20 }, slot.drawn.position);
try std.testing.expectApproxEqAbs(-1, math.forward(slot.drawn.orientation)[2], 1e-6);
try std.testing.expect(state.attached and slot.object.flags.no_collisions);
try std.testing.expectEqual(carrier, slot.object.passes_through[0].index());
try std.testing.expect(!slot.object.flags.targetable);
// It rides its node as the carrier moves.
objects.setPosition(&mission.slot(carrier).object, &mission.slot(carrier).drawn, .{ 1000, 50, 0 });
try std.testing.expect(hold(mission.objects, torpedo_slot));
try std.testing.expectEqual(math.Vector{ 1100, 50, 20 }, slot.drawn.position);
// It waits until the launch starts, then a moment of up to two seconds.
mission.clock.frame_start = 10;
aigeneric.objectOrders(ctx, torpedo_slot);
try std.testing.expectEqual(Step.waiting, state.step);
start(mission.objects, torpedo_slot);
aigeneric.objectOrders(ctx, torpedo_slot);
try std.testing.expectEqual(Step.delaying, state.step);
try std.testing.expect(state.due >= 10 and state.due < 10 + most_delay);
}
test "the player's launch records the carrier, whatever its style" {
const gpa = std.testing.allocator;
var mission: gameobj.testing.Mission = undefined;
try mission.init(gpa);
defer mission.deinit();
var carrier_model: testing.Carrier = undefined;
carrier_model.init();
const player = try mission.add(.of(.wolverine), @splat(0));
const carrier = try mission.add(.of(.kamov), .{ 1000, 0, 0 });
try carrier_model.parts.fit(gpa, mission.slot(carrier));
mission.objects.player = player;
const ctx = mission.orders();
// The Kamov has no style of its own, so the launch is a bay's, and the Kamov is the ship the
// player launched from, which PERMISSION TO LAND asks.
try std.testing.expectEqual(null, mission.player.carrier);
_ = try aigeneric.pushShip(ctx, player, .launch, carrier, 0);
aigeneric.objectOrders(ctx, player);
try std.testing.expectEqual(Style.bay, mission.slot(player).state.launch.style);
try std.testing.expectEqual(carrier, mission.player.carrier.?);
}
test "a launch's search for a gate counts the launch points on" {
const gpa = std.testing.allocator;
var mission: gameobj.testing.Mission = undefined;
try mission.init(gpa);
defer mission.deinit();
var carrier_model: testing.Carrier = undefined;
carrier_model.init();
const first = try mission.add(.of(.kamov), @splat(0));
const second = try mission.add(.of(.kamov), .{ 5000, 0, 0 });
for ([_]u16{ first, second }) |index| try carrier_model.parts.fit(gpa, mission.slot(index));
var state = std.mem.zeroes(State);
var search: GateSearch = .{ .all = mission.objects, .state = &state };
// The third order a command gave takes the first carrier's third point.
state.sequence = 2;
try std.testing.expect(search.visit(.at(first, null)));
try std.testing.expectEqual(first, state.carrier);
try std.testing.expectEqual(2, state.gate);
// The fifth goes on to the second carrier's second, its count starting again there.
state.sequence = 4;
try std.testing.expect(!search.visit(.at(first, null)));
try std.testing.expect(search.visit(.at(second, null)));
try std.testing.expectEqual(second, state.carrier);
try std.testing.expectEqual(1, state.gate);
}
test "a launch ends with its carrier, and an unknown style lets the ship go" {
const gpa = std.testing.allocator;
var mission: gameobj.testing.Mission = undefined;
try mission.init(gpa);
defer mission.deinit();
_ = try mission.add(.of(.predator), @splat(0));
const carrier = try mission.add(.of(.yamato), .{ 0, 0, 5000 });
const ship = try mission.add(.of(.sabre), .{ 0, 0, 5000 });
const lost = try mission.add(.of(.sabre), .{ 0, 0, 5000 });
const ctx = mission.orders();
for ([_]u16{ ship, lost }) |index| {
_ = try aigeneric.pushShip(ctx, index, .launch, carrier, 0);
aigeneric.objectOrders(ctx, index);
}
// A missing bay leaves the ship riding the root. An unknown style still lets it go.
try std.testing.expectEqual(Style.yamato, mission.slot(ship).state.launch.style);
try std.testing.expectEqual(objects.NodeOf{ .object = carrier }, mission.slot(ship).riding.?);
mission.slot(ship).state.launch.style = @fromBackingInt(12);
start(mission.objects, ship);
aigeneric.objectOrders(ctx, ship);
testing.pastDue(&mission, ctx, ship);
try std.testing.expectEqual(0, mission.slot(ship).object.order_count);
try std.testing.expectEqual(null, mission.slot(ship).object.passes_through[0].index());
// A ship still waiting when its carrier explodes goes with it.
mission.slot(carrier).object.flags.exploding = true;
aigeneric.objectOrders(ctx, lost);
try std.testing.expectEqual(ai.orders.Order.explode, mission.slot(lost).orders[0].order);
}
test dropping {
var mission: gameobj.testing.Mission = undefined;
try mission.init(std.testing.allocator);
defer mission.deinit();
const player = try mission.add(.of(.predator), @splat(0));
try std.testing.expect(!dropping(mission.objects, player));
_ = try aigeneric.pushShip(mission.orders(), player, .launch, player, 0);
const state = &mission.slot(player).state.launch;
state.style = .reliant;
state.step = .of(reliant.Step.open);
try std.testing.expect(!dropping(mission.objects, player));
state.step = .of(reliant.Step.drop);
try std.testing.expect(dropping(mission.objects, player));
}
test "a carrier with no style of its own launches its ships as a hangar bay" {
var mission: gameobj.testing.Mission = undefined;
try mission.init(std.testing.allocator);
defer mission.deinit();
_ = try mission.add(.of(.predator), @splat(0));
const carrier = try mission.add(.of(.kamov), .{ 0, 0, 10000 });
const ship = try mission.add(.of(.sabre), .{ 0, 0, 10500 });
const ctx = mission.orders();
_ = try aigeneric.pushShip(ctx, ship, .launch, carrier, 0);
aigeneric.objectOrders(ctx, ship);
const slot = mission.slot(ship);
try std.testing.expectEqual(Style.bay, slot.state.launch.style);
try std.testing.expectEqual(Step.waiting, slot.state.launch.step);
// Without a model and its launch points, it launches from where it stands.
try std.testing.expectEqual(null, slot.riding);
start(mission.objects, ship);
aigeneric.objectOrders(ctx, ship);
while (!testing.ended(slot)) testing.pastDue(&mission, ctx, ship);
try std.testing.expectEqual(0, slot.object.order_count);
try std.testing.expectEqual(null, slot.object.passes_through[0].index());
}
test launchesAsBase {
const additions = @import("additions.zig");
var list = [_]additions.ships.Added{
.{ .name = "a:refit", .mod = "a", .base = .reliant, .extra = .{ .model = "RELIANT.SHP" } },
.{ .name = "a:ark", .mod = "a", .base = .reliant, .extra = .{ .model = "ark.shp" } },
.{ .name = "a:moved", .mod = "a", .base = .reliant, .extra = .{ .model = "ships\\RELIANT.SHP" } },
};
additions.ships.install(&list);
defer additions.ships.reset();
const refit: gameobj.Type = @fromBackingInt(additions.ships.first);
const ark: gameobj.Type = @fromBackingInt(additions.ships.first + 1);
// The game's types, and a mod's type that keeps its base's model, launch as the base does.
try std.testing.expect(launchesAsBase(.of(.reliant)));
try std.testing.expect(launchesAsBase(refit));
// Its base's model named with a folder, on any system.
try std.testing.expect(launchesAsBase(@fromBackingInt(additions.ships.first + 2)));
try std.testing.expectEqual(Style.reliant, Style.of(.of(.predator), refit, 0));
// A model of its own launches as a hangar bay, without its base's doors.
try std.testing.expect(!launchesAsBase(ark));
try std.testing.expectEqual(Style.bay, Style.of(.of(.predator), ark, 0));
try std.testing.expectEqual(bay.Doors{}, bay.Doors.of(ark, 0));
}