Repository navigation
Expand file tree
/
Copy pathcreate.zig
More file actions
2734 lines (2522 loc) · 131 KB
/
Copy pathcreate.zig
File metadata and controls
2734 lines (2522 loc) · 131 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\Create.cpp`: creating live objects. `create_object` (`0x00466C10`) fills a slot
//! of `game_objects`, which OpenReliant keeps in `Objects`, with an object of a ship type, and
//! `objects_reset` (`0x00466630`) fills every slot with a stand-in as a mission starts.
//! `stats_load_ships` (`0x00466500`) fills `ship_flight_stats` and `ship_combat_stats` from
//! `shipstats.bin`, [`formats/stats.zig`](../../formats/stats.zig).
//! [`create/models.zig`](create/models.zig) names each ship type's and attachment's models,
//! [`create/flight.zig`](create/flight.zig) and [`create/combat.zig`](create/combat.zig) hold the
//! flight and combat stats' words that the executable keeps, and
//! [`create/library.zig`](create/library.zig) reads the models the types and their attachment
//! points use. **Unverified:** the loader, `objects_reset`, `ship_type_load` and the
//! ship type table lie between `collision.cpp`'s code and data and this file's, and `object_reset`
//! and `objects_update` after this file's known code, before `environfx.cpp`'s.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const engine = @import("../../engine.zig");
const hooks = @import("../hooks.zig");
const shp = @import("../../formats/shp.zig");
const stats = @import("../../formats/stats.zig");
const math = @import("../surrender/math.zig");
const Vector = math.Vector;
const Pointer = engine.Pointer;
const Random = @import("../random.zig").Random;
const ai = @import("ai.zig");
const camera = @import("camera.zig");
const cloak = @import("cloak.zig");
const aigeneric = @import("aigeneric.zig");
const collision = @import("collision.zig");
const gameobj = @import("gameobj.zig");
const gameflow = @import("gameflow.zig");
const guns = @import("guns.zig");
const missiles = @import("missiles.zig");
const WingSlots = @import("mission.zig").WingSlots;
const GameObject = gameobj.GameObject;
const main = @import("main.zig");
const motion = @import("motion.zig");
const objects = @import("objects.zig");
const Order = @import("ai/orders.zig").Order;
const pilots = @import("pilots.zig");
const shield = @import("shield.zig");
const environfx = @import("environfx.zig");
const explode = @import("explode.zig");
const smoke = @import("main/smoke.zig");
const srofiles = @import("srofiles.zig");
const wgate = @import("wgate.zig");
const xtrabits = @import("xtrabits.zig");
const additions = @import("additions.zig");
pub const models = @import("create/models.zig");
pub const flight_stats = @import("create/flight.zig");
pub const combat_stats = @import("create/combat.zig");
pub const library = @import("create/library.zig");
pub const atmosphere = @import("create/atmosphere.zig");
pub const planets = @import("create/planets.zig");
pub const escort = @import("create/escort.zig");
pub const extra = @import("create/extra.zig");
/// Ship types: the records of `shipstats.bin`, and the entries of the tables they index. Types
/// above the last have no stats, markers and nav points among them, except the types mods add
/// (`max_ship_types`).
pub const ship_type_count = 256;
/// The ship types the tables hold rows for: the game's, then the types mods add, up to the
/// markers (`additions.ships`).
pub const max_ship_types = additions.ships.end;
/// A ship type's row in the tables, the game's types' and the mods'.
pub const TypeIndex = u16;
/// The files of ship type `number`'s model and schematic: the game's (`models.ship_types`), or a
/// mod's type's, whose schematic is its base's where it names none.
pub fn shipFiles(number: TypeIndex) models.ShipType {
if (additions.ships.get(number)) |added| return .{
.model = added.extra.model,
.schematic = added.extra.schematic orelse models.ship_types[@backingInt(added.base)].schematic,
};
return if (number < models.ship_types.len) models.ship_types[number] else .{ .model = null, .schematic = null };
}
/// `ship_flight_stats` (`0x004F9E70`) and `ship_combat_stats` (`0x004FC670`): each ship type's
/// flight model and combat stats, which `create_object` points each object of the type at.
pub const Stats = struct {
flight: [max_ship_types]FlightModel,
combat: [max_ship_types]ShipCombat,
/// The tables as the executable holds them before `stats_load_ships` runs: every figure zero,
/// and each record's own words, how the AI turns the type and what its combat record says it is.
pub const initial: Stats = built: {
@setEvalBranchQuota(ship_type_count * 20);
var tables: Stats = .{
.flight = @splat(std.mem.zeroes(FlightModel)),
.combat = @splat(std.mem.zeroes(ShipCombat)),
};
for (tables.flight[0..ship_type_count], 0..) |*flight, ship_type| flight.turns = flight_stats.turns(ship_type);
for (tables.combat[0..ship_type_count], combat_stats.ship_types) |*record, static| record.take(static);
break :built tables;
};
/// `stats_load_ships` (`0x00466500`): each record of `shipstats.bin` in turn fills in its
/// type's figures, up to the last type, and then every type's `speed_per_pitch_rate` is worked
/// out. How each type turns stays as the executable has it. The combat stats keep in whole numbers what the runtime's `__ftol` cuts the record's
/// figures down to, and a `shield_recharge` of zero becomes
/// `stats.Ship.default_shield_recharge`.
pub fn load(tables: *Stats, ships: []align(1) const stats.Ship) void {
const count = @min(ships.len, max_ship_types);
for (tables.flight[0..count], tables.combat[0..count], ships[0..count]) |*flight, *record, ship| {
flight.max_speed = ship.max_speed;
flight.roll_rate = ship.roll_rate;
flight.pitch_rate = ship.pitch_rate;
flight.yaw_rate = ship.yaw_rate;
flight.inertia = ship.inertia;
flight.roll_inertia = ship.roll_inertia;
flight.pitch_inertia = ship.pitch_inertia;
flight.yaw_inertia = ship.yaw_inertia;
record.shield_power = std.math.lossyCast(i32, ship.shield_power);
record.armor_class = std.math.lossyCast(i32, ship.armor_class);
record.afterburner_fuel = std.math.lossyCast(i32, ship.afterburner_fuel);
record.shield_recharge = if (ship.shield_recharge == 0) stats.Ship.default_shield_recharge else ship.shield_recharge;
record.gun_energy = ship.gun_energy;
record.gun_recharge = ship.gun_recharge;
record.rounds = std.math.lossyCast(i32, ship.rounds);
}
for (&tables.flight) |*flight| flight.speed_per_pitch_rate = flight.max_speed / flight.pitch_rate;
}
/// OpenReliant's: gives each type the mods add (`additions.ships`) its base's way of turning.
/// Its figures come from its record (`load`), and its words from its type record
/// (`loadTypes`).
pub fn addTypes(tables: *Stats) void {
for (additions.ships.all(), additions.ships.first..) |added, number| {
tables.flight[number].turns = tables.flight[@backingInt(added.base)].turns;
}
}
/// OpenReliant's: gives each type the words of its type record (`typeRecords`), which the mods'
/// scripts can change (`records.ship_types`).
///
/// **Improvement:** the game keeps them fixed in its executable.
pub fn loadTypes(tables: *Stats, types: []const combat_stats.Static) void {
const count = @min(types.len, max_ship_types);
for (tables.combat[0..count], types[0..count]) |*record, static| record.take(static);
}
/// What `create_object` does for a type that is `from` under another number (`donor`): the
/// type takes `from`'s flight model and its combat stats, save for its own gun groups and
/// name.
fn borrow(tables: *Stats, ship_type: TypeIndex, from: TypeIndex) void {
const own = tables.combat[ship_type];
tables.combat[ship_type] = tables.combat[from];
tables.combat[ship_type].gun_groups = own.gun_groups;
tables.combat[ship_type].gun_group_table = own.gun_group_table;
tables.combat[ship_type].name = own.name;
tables.flight[ship_type] = tables.flight[from];
}
};
/// The type records that the mods' scripts can change (`records.ship_types`, `Stats.loadTypes`):
/// the words the executable holds for each of the game's types (`combat_stats.Static`), then one
/// for each type a mod adds, copied from its base's, with its own name where the mod gives one.
pub fn typeRecords(arena: Allocator) Allocator.Error![]combat_stats.Static {
const made = try additions.ships.records(combat_stats.Static, arena, &combat_stats.ship_types, 0);
for (made[additions.ships.first..], additions.ships.all()) |*record, added| {
if (added.label_string) |name| record.name = name;
}
return made;
}
/// The ship type a type is under another number, or null for none, as for a type a mod adds
/// (`create_object`): the Krasnaya, the Kiev, the Mitchell, the Zakov, the Kestrel and the Mammoth
/// are each more than one type, one model under several numbers. An object of such a type takes
/// the stats of the first (`Stats.borrow`), and then its number.
pub fn donor(ship_type: gameobj.Type) ?gameobj.Type {
const game_type = ship_type.gameType() orelse return null;
const first: gameobj.GameType = switch (game_type) {
.kozlov, .bokov, .bulatov => .krasnaya,
.morzov, .kirov, .shinnik, .kovtun => .kiev,
.other_mitchell => .mitchell,
.ufelsky, .kresta, .yevstafiy => .zakov,
.churchill => .kestrel,
.mammoth_gulliver,
.mammoth_santa_maria,
.mammoth_rosario,
.mammoth_larsons_pride,
.mammoth_brittania,
.mammoth_sierra_madre,
.mammoth_mayan_gold,
.mammoth_dawn_chorus,
.mammoth_calysto,
.mammoth_sundown,
.mammoth_krenna,
.mammoth_seabound,
.mammoth_crimson_sky,
=> .mammoth,
else => return null,
};
return .of(first);
}
/// How a ship or a missile flies. `ship_flight_stats` holds one per ship, `missile_flight_stats`
/// one per missile; a missile's has only its speed and rates set.
pub const FlightModel = extern struct {
/// `Ship.max_speed`, or `Missile.speed`.
max_speed: f32,
/// `Ship.roll_rate`, or `Missile.turn_rate`.
roll_rate: f32,
/// `Ship.pitch_rate`, or `Missile.turn_rate`.
pitch_rate: f32,
/// `Ship.yaw_rate`, or `Missile.turn_rate`.
yaw_rate: f32,
/// `Ship.inertia`.
inertia: f32,
roll_inertia: f32,
pitch_inertia: f32,
yaw_inertia: f32,
/// `max_speed / pitch_rate`, which the ship loader computes after reading the file.
speed_per_pitch_rate: f32,
/// How the AI turns it (`ai_steer`). No file sets it: the executable holds it for each ship
/// type ([`create/flight.zig`](create/flight.zig)) and each missile, and the loaders leave it.
turns: Turns,
_unknown_26: u16,
/// How the AI turns a ship at a point, a word the game tests for zero.
pub const Turns = enum(u16) {
/// Rolling the point overhead, then pitching up at it (`0x00401710`): the fighters, and
/// some support ships, the Nanny and the limpet car among them.
banking = 0,
/// Pitching and yawing at it together, never rolling (`0x00401690`): the capital ships,
/// most other types that are not fighters, and every missile but the fuel pod.
flat = 1,
_,
};
comptime {
assert(@offsetOf(FlightModel, "inertia") == 0x10);
assert(@offsetOf(FlightModel, "speed_per_pitch_rate") == 0x20);
assert(@offsetOf(FlightModel, "turns") == 0x24);
assert(@sizeOf(FlightModel) == 0x28);
}
};
/// A ship type's defences and what it is, one per type in `ship_combat_stats`: its figures from
/// `shipstats.bin` up to `+0x1C`, which `stats_load_ships` fills in, then the gun groups, which
/// the gun code works out at run time, then words the executable holds
/// ([`create/combat.zig`](create/combat.zig)).
pub const ShipCombat = extern struct {
/// `Ship.shield_power`, truncated.
shield_power: i32,
/// `Ship.armor_class`, truncated.
armor_class: i32,
/// `Ship.afterburner_fuel`, truncated.
afterburner_fuel: i32,
/// `Ship.shield_recharge`, or 10 in place of zero.
shield_recharge: f32,
/// `Ship.gun_energy`: the most the guns' charge holds.
gun_energy: f32,
/// `Ship.gun_recharge`: the seconds the guns take to charge fully, over which the guns' step
/// adds `gun_energy * gun_factor * gun_condition` to their charge (`guns.step`).
gun_recharge: f32,
/// `Ship.rounds`, truncated: the rounds a new object's guns have (`GameObject.rounds`).
rounds: i32,
/// The type's guns in groups, which `0x004667F0` works out from its first object's guns,
/// pairing each gun with its mirror image across the ship; zero until then (#131).
gun_groups: i16,
_unknown_1e: u16,
/// The groups, `0x00545900 + type * 0x78`, set with `gun_groups`.
gun_group_table: Pointer(anyopaque),
targeting: Targeting,
/// The language string that names the type; 0 for none (`nameString`).
name: u16,
class: Class,
/// The side the type's objects start on.
side: gameobj.Side(i16),
/// Which form of the target display shows the type's objects.
display: TargetDisplay,
/// Which form of the target display shows a type's objects: the small window
/// (`hud.windows.Window.target`) for fighters and most small craft, the large one
/// (`Window.big_target`) for most capital and support ships. `hud_ship_status` draws a small
/// form's schematic mirrored and a large form's as it stands.
pub const TargetDisplay = enum(u32) {
/// The name scripts know these values by, for a ship type's and for the target display's
/// form.
pub const script_name = "TargetForm";
small = 0,
large = 1,
_,
};
pub const Targeting = packed struct(u16) {
/// The type's objects can be picked as targets: `object_set_targetable` sets an object's
/// `targetable` flag only where this is set.
targetable: bool = false,
_unknown_1: u15 = 0,
};
/// What a ship type is, going by the models of each class.
pub const Class = enum(u16) {
/// The name scripts know these values by.
pub const script_name = "ShipClass";
/// The player's ships, their twins, and the Coalition's fighters.
fighter = 1,
/// Capital ships and their wrecks, and other large bodies such as the asteroids of types
/// 0x79 to 0x7F.
capital = 2,
/// Bombers, transports, tugs, escape pods, some stations: what lies between fighters and
/// capital ships.
support = 3,
/// Gates, containers, satellites, beacons, pods, rock chunks and the like.
other = 4,
torpedo = 5,
/// Wreckage, floating crew, doors and plates. `create_object` gives an object of the class
/// a tenth of its mass.
debris = 6,
/// The proximity mine, which `create_object` gives a radius of 2000.
mine = 7,
planet = 8,
_,
};
/// The language string that names the type, if it has one (`name`).
pub fn nameString(combat: *const ShipCombat) ?u16 {
return if (combat.name != 0) combat.name else null;
}
/// Takes the words of a type record (`static`), the ones the executable holds rather than
/// `shipstats.bin`.
pub fn take(combat: *ShipCombat, static: combat_stats.Static) void {
combat.targeting = .{ .targetable = static.targetable };
combat.name = static.name;
combat.class = static.class;
combat.side = @fromBackingInt(@intCast(@backingInt(static.side)));
combat.display = static.display;
}
/// Six times its `armor_class`: a quadrant's full armour, which the game measures the
/// armour's wear against. `create_object` starts each quadrant one below it.
pub fn fullArmor(combat: *const ShipCombat) f32 {
return @floatFromInt(combat.armor_class * 6);
}
/// A quadrant's armour as `create_object` fills it, one short of `fullArmor`, which the
/// armour's conditions and warning count from (`main.armorConditions`).
pub fn startingArmor(combat: *const ShipCombat) f32 {
return combat.fullArmor() - 1;
}
/// Six times its `shield_power`: a quadrant's full shields, likewise.
pub fn fullShields(combat: *const ShipCombat) f32 {
return @floatFromInt(combat.shield_power * 6);
}
/// The afterburner's fuel of a ship of the type, in the game's ticks: its `afterburner_fuel`
/// in seconds, which a new object starts with (`arm`, fuel pods aside) and
/// `ResetAfterBurners` fills again.
pub fn fullFuel(combat: *const ShipCombat) i32 {
return combat.afterburner_fuel * main.ticks_per_second;
}
/// OpenReliant's: the share of its armour a ship has left, its weakest quadrant of `armor`
/// against `fullArmor`. A ship starts a little under 1 (`startingArmor`), and one with no
/// armour class has nothing to lose, so it stays at 1.
pub fn armorShare(combat: *const ShipCombat, armor: gameobj.Quadrants) f32 {
const full = combat.fullArmor();
if (full <= 0) return 1;
return std.math.clamp(armor.weakest() / full, 0, 1);
}
comptime {
assert(@offsetOf(ShipCombat, "shield_recharge") == 0x0C);
assert(@offsetOf(ShipCombat, "gun_groups") == 0x1C);
assert(@offsetOf(ShipCombat, "gun_group_table") == 0x20);
assert(@offsetOf(ShipCombat, "targeting") == 0x24);
assert(@offsetOf(ShipCombat, "name") == 0x26);
assert(@offsetOf(ShipCombat, "class") == 0x28);
assert(@offsetOf(ShipCombat, "side") == 0x2A);
assert(@offsetOf(ShipCombat, "display") == 0x2C);
assert(@sizeOf(ShipCombat) == 0x30);
}
};
/// An entry of `ship_types`, one for each ship type; [`create/models.zig`](create/models.zig) has
/// the names.
pub const ShipType = extern struct {
model_name: Pointer(u8),
schematic_name: Pointer(u8),
/// Objects of the type, which `create_object` counts up, loading the model for the first.
objects: u16,
_unknown_0a: u16,
/// The model, once loaded.
model: Pointer(anyopaque),
/// What its objects keep as `GameObject.type_data`: the schematic `ship_type_load` loads with
/// the model.
type_data: Pointer(anyopaque),
comptime {
assert(@offsetOf(ShipType, "model") == 0x0C);
assert(@sizeOf(ShipType) == 0x14);
}
};
/// An entry of `attachment_models`: what attachment points of one kind and id mount, as loaded.
/// [`create/models.zig`](create/models.zig) has the file names.
pub const MountedModel = extern struct {
model: Pointer(anyopaque),
/// A second model: the missile, for a missile pod.
second_model: Pointer(anyopaque),
/// **Unknown.** 1 unless the loader sets it.
count: u32,
sprite: Pointer(anyopaque),
comptime {
assert(@sizeOf(MountedModel) == 0x10);
}
};
/// A ship type's model as `ship_type_load` (`0x00466740`) loads it for the type's first object.
/// It loads the type's schematic as well, which OpenReliant leaves with whoever loads the model,
/// since the display draws it.
pub const Type = struct {
model: *const shp.Model,
loaded: *const srofiles.Loaded,
/// What the type's objects light themselves with and mount.
effects: objects.Effects = .{},
/// The type's own sprite (`GameObject.type_data`), which for a ship is its schematic, where
/// the game has one: the ship status indicator and the target display draw it.
schematic: ?@import("hud.zig").TypeArt = null,
/// OpenReliant's: the display's shapes, with the pictures a mod's type gives in place of its
/// base's wire frame on the gunnery display (`gunnery`) and of its icon in the wing's window
/// (`wing_status`); null for a type that gives none.
wire_frame: ?@import("hud.zig").TypeArt = null,
wing_icon: ?@import("hud.zig").TypeArt = null,
};
/// Where ship types' models come from: whoever has the game's files answers, as for
/// `objects.Mounts`.
pub const Types = struct {
context: *anyopaque,
/// Null for a type with no model, or one the game lacks or cannot read; whoever answers says
/// why.
load: *const fn (context: *anyopaque, ship_type: TypeIndex) ?*const Type,
};
/// What `ship_types` keeps of a ship type while a mission runs: how many objects of it
/// `create_object` has made, and what it loaded for the first.
pub const TypeUse = struct {
objects: u16 = 0,
loaded: ?*const Type = null,
};
/// The slot the loops over the objects walk after those handed out (`0x0057E04E`), which the
/// mission's start sets to 399, the last. The cutaway scenes use it: in one of them only the
/// player's ship and this slot's object are drawn.
pub const cutaway_slot: u16 = gameobj.max_objects - 1;
/// A slot of `game_objects`: the object's record, and what OpenReliant keeps beside it where the
/// record holds the original's 32-bit pointers.
pub const Slot = struct {
object: GameObject,
/// Its type's stats in `Stats` (`GameObject.combat`, `flight`); null for a stand-in.
combat: ?*const ShipCombat = null,
flight: ?*const FlightModel = null,
/// Its type's model as loaded (`GameObject.model`), and the nodes of the model's parts, which
/// hang from its root; null for a stand-in, or a type the game has no model for.
type: ?*const Type = null,
model: ?objects.Model = null,
/// What moves it each update (`GameObject.motion`); null for nothing.
motion: ?motion.Motion = null,
/// The motion a jump puts aside while it flies its own, and gives back as it ends
/// (`jump.State.motion`, which the game keeps as the routine's address).
motion_aside: ?motion.Motion = null,
/// How far it goes a tick where the orders place it from tick to tick rather than move it: it
/// is drawn that much further along for the time the frame is past its tick
/// (`objects.frameTree`). The orders set it each frame they place it, and the frame's pass
/// lets it go (`main.frameObjects`), keeping whether it glided, so that the frame after it is
/// drawn back where it was placed.
glide: Vector = @splat(0),
glided: bool = false,
/// Where its root's frame has it drawn (`objects.frameTree`), which stays put between the
/// steps that move it.
drawn: objects.Model.Local = .{},
/// The node it rides while it launches (`launch.State.node`, which the game keeps as the
/// node's address); null for none.
riding: ?objects.NodeOf = null,
/// Its stack of orders, the current one first, `GameObject.order_count` of them
/// (`GameObject.orders`), which the game allocates with the object's first order.
orders: [aigeneric.max_stack]aigeneric.Entry = @splat(std.mem.zeroes(aigeneric.Entry)),
/// What the current order keeps between its updates (`GameObject.order_state`), allocated with
/// the stack.
state: aigeneric.State = .{ .bytes = @splat(0) },
/// Its queue of orders waiting for their tick, `GameObject.queued_order_count` of them
/// (`GameObject.queued_orders`), which the game allocates with the first (`aigeneric.queue`).
queued: [aigeneric.max_queued]aigeneric.Queued = @splat(std.mem.zeroes(aigeneric.Queued)),
/// Its guns, one for each muzzle of its model (`GameObject.guns`), made in the objects'
/// allocator.
guns: []guns.Fitted = &.{},
/// Its type's gun groups (`ShipCombat.gun_group_table`), in `Objects.gun_groups`.
gun_groups: *const [guns.max_groups]guns.Group = &guns.no_groups,
/// The parts of its model that count as components, `GameObject.component_count` of them, the
/// models mounted on it among them (`GameObject.components`, which holds their nodes).
components: [gameobj.max_components]?*objects.Model.Part = @splat(null),
/// Its shields' bubble (`GameObject.render`), which a ship that lists no components and is not
/// debris has.
shield: ?*shield.Bubble = null,
/// Its cloak, from the moment it starts to come on until it has gone (`GameObject.cloak`).
cloak: ?cloak.Cloak = null,
/// Its smoke, while its damage shows (`GameObject.smoke`).
smoke: ?smoke.Stream = null,
/// What its type carries on one of its parts beyond its model, such as the Dark Reign's hat
/// (`extra.Extra`, which the game hangs on the part's frame).
extra: ?*extra.Extra = null,
/// Lets go of what the slot holds for its object: its cloak (`cloak.drop`), its model, its
/// guns, its shield bubble and its extra (`object_free`).
pub fn release(slot: *Slot, gpa: Allocator) void {
cloak.drop(slot);
if (slot.model) |model| model.deinit(gpa);
slot.dropGuns(gpa);
slot.dropShield(gpa);
if (slot.extra) |held| held.destroy(gpa);
slot.extra = null;
}
/// Its current order, the first of its stack, where it has one (`stack`): as mutable as
/// `slot` is.
pub fn current(slot: anytype) ?@TypeOf(&slot.orders[0]) {
if (slot.object.order_count <= 0) return null;
return &slot.orders[0];
}
/// Its current order (`current`), where it is `order`: as mutable as `slot` is.
pub fn running(slot: anytype, order: Order) ?@TypeOf(&slot.orders[0]) {
const entry = slot.current() orelse return null;
return if (entry.order == order) entry else null;
}
/// Its stack of orders, the current one first: `GameObject.order_count` of them, none for a
/// count below 0 and no more than the stack holds: as mutable as `slot` is.
pub fn stack(slot: anytype) Entries(@TypeOf(slot)) {
const count = std.math.cast(usize, slot.object.order_count) orelse 0;
return slot.orders[0..@min(count, slot.orders.len)];
}
/// The entries of a stack, as mutable as the slot pointed at by `SlotPointer` is.
fn Entries(comptime SlotPointer: type) type {
return if (@typeInfo(SlotPointer).pointer.attrs.@"const") []const aigeneric.Entry else []aigeneric.Entry;
}
/// The orders waiting in its queue: `GameObject.queued_order_count` of them, none for a count
/// below 0 and no more than the queue holds.
pub fn waiting(slot: *Slot) []aigeneric.Queued {
const count = std.math.cast(usize, slot.object.queued_order_count) orelse 0;
return slot.queued[0..@min(count, slot.queued.len)];
}
/// The first entry of its stack that is `order`, where there is one (`player_control_entry`,
/// `0x00402860`, and `launch_start`, `0x00418DB0`).
pub fn firstOrder(slot: *Slot, order: Order) ?*aigeneric.Entry {
for (slot.stack()) |*entry| {
if (entry.order == order) return entry;
}
return null;
}
/// The parts it lists as components, `GameObject.component_count` of them.
pub fn listed(slot: *const Slot) []const ?*objects.Model.Part {
return slot.components[0..@intCast(@max(slot.object.component_count, 0))];
}
/// Its component `n`, where it lists one there.
pub fn component(slot: *const Slot, n: usize) ?*objects.Model.Part {
const parts = slot.listed();
return if (n < parts.len) parts[n] else null;
}
/// Where `part`, one of its model's parts or of a model that model carries, stands in the
/// world, its frame brought up to date with those it hangs from (`SR_object_concate_parents`,
/// `0x004C3570`), the root at its drawn place; null for an object without a model, or a part of
/// neither.
pub fn partPlace(slot: *Slot, part: *const objects.Model.Part) ?math.Place {
const model = if (slot.model) |*live| live else return null;
return model.placeOf(slot.drawn, part, .now);
}
/// `object_component_index` (`0x0045ADE0`): where it lists `part` among its components, by
/// which the mission's events and its triggers name a component; null where it does not.
pub fn componentIndex(slot: *const Slot, part: *const objects.Model.Part) ?u8 {
for (slot.listed(), 0..) |entry, n| {
if (entry == part) return @truncate(n);
}
return null;
}
/// The number `collectComponents` gave `part`, which the mission's events and triggers use for
/// it; null for a part that isn't a component. Unlike `componentIndex`, it still works after the
/// part is destroyed and removed from the list (`objects.destroyPart`).
///
/// **Improvement:** the original only finds the number of a component that is still listed
/// (`componentIndex`). Scripts read it after that too.
pub fn componentNumber(slot: *Slot, part: *const objects.Model.Part) ?u8 {
const model = if (slot.model) |*live| live else return null;
if (!part.component) return null;
const Counting = struct {
sought: *const objects.Model.Part,
counted: usize = 0,
found: bool = false,
fn visit(counting: *@This(), each: *objects.Model.Part) bool {
if (each == counting.sought) {
counting.found = true;
return false;
}
counting.counted += 1;
return counting.counted < gameobj.max_components;
}
};
var counting: Counting = .{ .sought = part };
_ = eachComponent(model, null, &counting);
return if (counting.found) @intCast(counting.counted) else null;
}
/// Lets its guns go: it has none from now on.
pub fn dropGuns(slot: *Slot, gpa: Allocator) void {
gpa.free(slot.guns);
slot.guns = &.{};
slot.object.gun_count = 0;
}
/// Lets its shield bubble go: it has none from now on.
pub fn dropShield(slot: *Slot, gpa: Allocator) void {
if (slot.shield) |bubble| bubble.destroy(gpa);
slot.shield = null;
}
/// How many groups of guns its type has (`ShipCombat.gun_groups`); none for a stand-in.
pub fn groupCount(slot: *const Slot) i16 {
const combat = slot.combat orelse return 0;
return combat.gun_groups;
}
/// Draws the object at `size` of its own, as `explode_asteroid` makes its fragments: its
/// `visibility`, which scales the sphere it collides by and how far off it is drawn, and the
/// scale its model's first part is drawn at (that part's frame's `+0x48`).
pub fn shrink(slot: *Slot, size: f32) void {
slot.object.visibility = size;
const model = if (slot.model) |*live| live else return;
model.visibility = size;
if (model.parts.len > 0) model.parts[0].object.scale = size;
}
/// The gun type leading its group `group` (`guns.groupLead`).
pub fn groupLead(slot: *const Slot, group: usize) ?guns.GunType {
return guns.groupLead(slot.guns, slot.gun_groups, group);
}
/// Its guns and their groups, for firing them from `frame_start` (`guns.fire`).
pub fn trigger(slot: *const Slot, frame_start: i32) guns.Trigger {
return .{ .fitted = slot.guns, .groups = slot.gun_groups, .frame_start = frame_start };
}
};
/// `game_objects` (`0x00587CE0`), the GO array: 400 slots, none ever empty. As a mission starts
/// every slot gets a stand-in (`reset`), and `create_object` fills them with objects
/// (`createObject`), in turn or at the slot it is given, such as a mission ship's index among the
/// ship records. The loops over the objects walk the slots handed out, then the cutaway slot
/// (`walk`).
pub const Objects = struct {
gpa: Allocator,
slots: [gameobj.max_objects]Slot,
/// `game_object_count` (`0x00539AA0`): the slots `create_object` has handed out in turn.
count: u16 = 0,
/// `player_slots` (`0x0058832C`): the slots from the first that belong to players, one in a
/// single-player game.
players: u16 = 1,
/// `player_index` (`0x005883FA`): the player's slot, the first in a single-player game.
player: u16 = 0,
/// `camera_marker` (`0x00588390`): the slot of the camera's marker, which the mission's start
/// makes in the slot after the mission's ships, and which the Yamato's launch moves beside the
/// player's bay; null until a mission's start makes it.
camera_marker: ?u16 = null,
/// `bombard_planet` (`0x005531A8`): the slot of the last Titan made, on which Planet Bombard's
/// flashes burst (`environfx.bombard`); null until one is made.
bombarded: ?u16 = null,
types: [max_ship_types]TypeUse = @splat(.{}),
/// Each ship type's gun groups (`0x00545900`), which `gun_groups_build` works out from an
/// object of the type.
gun_groups: [max_ship_types][guns.max_groups]guns.Group = @splat(@splat(.{})),
/// `gun_stats` (`0x00500CA4`): every gun type's figures, which `stats_load_guns` fills from
/// `gunstats.bin`.
gun_stats: guns.Stats = .initial,
/// `missile_stats` and `missile_flight_stats`: every missile type's figures, which
/// `stats_load_missiles` fills from `missilestats.bin`.
missile_stats: missiles.Table = .initial,
/// `campaign_tier` (`0x00562DF0`): the loadout tier the campaign has reached, which a fighter
/// asked for none is fitted by. 0 for a new pilot; the missions after the 11th, 19th and 21st
/// raise it to 1, 2 and 3 (`mission_end_record`).
campaign_tier: u2 = 0,
/// The pilots of the player's wing and the pool of their replacements (`alpha_pilots`,
/// `pilot_pool`), which the game keeps in `pilots.cpp`'s globals. OpenReliant keeps them here
/// too, as it keeps the campaign's tier.
wingmen: pilots.Wingmen = .{},
/// `pilot_stats` (`0x0058A968`): every pilot, which `stats_load_pilots` fills from
/// `pilotstats.bin`.
pilots: pilots.Table = .{},
/// `pilot_faces` (`0x005048D8`): every pilot's face, which the game keeps in its data and
/// OpenReliant loads from the records.
faces: pilots.Faces = .{},
/// The shots in flight (`0x00563148`), which the game keeps in `guns.cpp`'s own globals.
/// OpenReliant keeps them here, beside the objects they fly among.
bullets: guns.Bullets = .{},
/// The missiles in flight (`0x005887F0`), which the game keeps in `missiles.cpp`'s own globals.
/// OpenReliant keeps them here too.
missiles: missiles.Missiles = .{},
/// The player's wing (`player_wing`, `0x00515D88`), which the game keeps in `mission.cpp`'s
/// own globals and a mission lists (`mission.listPlayerWing`). OpenReliant keeps it here too.
wing: WingSlots = @splat(null),
/// The working lists of the collision sweep `objectsUpdate` runs.
sweep: Sweep = .{},
/// `0x005185AC`: the tick at which `aigeneric.ordersUpdate` next clears what every object has
/// lately taken.
damage_cleared_at: u32 = 0,
/// `mission_number` (`0x00562DC8`): the number of the mission being played, from 1, or 0 for
/// OpenReliant's mission 0. The game singles some missions out by it, which OpenReliant asks of
/// the mission's settings instead (`rules`). The launch's caption and the objectives go by it,
/// and Instant Action's mission and the training missions (`training`) have rules of their own.
mission_number: u16 = 0,
/// `mission25_second_part` (`0x00587CDC`): whether mission 25's first part is won and its
/// second is played, before which the player flies a Kamov.
mission25_second_part: bool = false,
/// `0x005883F8`: set while the player's ship is inside a Stalag, having crossed into one of the
/// Stalag's trigger polygons and not out again (`collision.crossTriggers`), which `holdsPlayer`
/// reads. A mission's start clears it (`0x004935D7`).
in_stalag: bool = false,
/// The simulator the front end runs the mission in, where it does.
simulator: Simulator = .{},
/// The ship the loadout screen chose for each player's slot (`player_loadouts`, `0x00588400`,
/// the first word of each), which `create_object` makes the player's ship of (`slotType`).
/// OpenReliant's driver sets the player's from the loadout, from `--ship`, or as the test keys
/// change it; where it sets none, the mission's own kind stands.
loadout_ships: [max_loadouts]?gameobj.Type = @splat(null),
/// The racks the loadout screen fitted for each player's slot (`player_loadouts + 4` on),
/// which `create_object` and a re-arm fit a player's ship with outside the simulator
/// (`loadoutRacks`); null where the mission starts without its briefing and loadout
/// (`skip_briefing`, `0x005883B4`), when the ship is fitted by a tier instead.
loadout_racks: [max_loadouts]?Racks = @splat(null),
/// `0x005185A8`, while the byte at `0x005185B1` is set: the number the next order pushed takes
/// (`aigeneric.Entry.sequence`), as `SetAI` numbers a group's orders
/// (`aigeneric.startNumbering`); null otherwise, when an order takes 0.
order_number: ?i32 = null,
/// The sphere the action keeps to.
action_sphere: aigeneric.ActionSphere = .default,
/// The ships whose engine exhaust burns the player's ship, which the game keeps in
/// `environfx.cpp`'s own globals. OpenReliant keeps them here, as `create_object` adds to them.
exhaust: environfx.Exhaust = .{},
/// OpenReliant's: the mods' scripts, which the hooked functions and events call while a game
/// runs; null without them.
scripts: ?*hooks.Scripts = null,
/// OpenReliant's: how often each slot has had its object replaced, which the scripts' object
/// handles keep, so that a handle to an object that has gone is no longer valid. A slot's count
/// goes up as `create_object` fills it and as its object is removed (`resetSlot`, `retire`),
/// and every slot's as a mission starts (`reset`).
reuses: [gameobj.max_objects]u32 = @splat(0),
/// Every slot standing in, as a mission's start leaves them (`reset`), made in `gpa`.
pub fn create(gpa: Allocator, random: *Random) Allocator.Error!*Objects {
const all = try gpa.create(Objects);
all.* = .{ .gpa = gpa, .slots = @splat(.{ .object = undefined }) };
all.reset(random);
return all;
}
pub fn destroy(all: *Objects) void {
all.missiles.reset(all.gpa);
for (&all.slots) |*slot| slot.release(all.gpa);
all.gpa.destroy(all);
}
/// `objects_reset` (`0x00466630`), as a mission starts: every slot gets a new stand-in, of
/// `gameobj.Type.stand_in` and flagged `stand_in` (`object_alloc`), no slot is handed out, and
/// no ship type has objects or a model loaded. OpenReliant lets go of the objects' nodes as
/// well, which the game frees as the mission before ends.
///
/// The planets' atmospheres, whose texture it loads and whose table it empties, are
/// `atmosphere.Atmospheres`.
pub fn reset(all: *Objects, random: *Random) void {
for (&all.slots) |*slot| {
slot.release(all.gpa);
var object = gameobj.objectAlloc(.of(.stand_in), random);
object.flags.stand_in = true;
slot.* = .{ .object = object };
}
for (&all.reuses) |*count| count.* +%= 1;
all.types = @splat(.{});
all.count = 0;
all.camera_marker = null;
all.bombarded = null;
all.in_stalag = false;
// The game lets the exhaust's list go as the mission before ends (`exhaust_ships_reset`).
all.exhaust.reset();
}
/// Whether the object in slot `index` is a Stalag with the player's ship inside it
/// (`in_stalag`): its turrets aim anywhere, whatever their firing arcs (`turret_aim_angles`),
/// and its shots may strike the Stalag itself (`bullet_place`).
pub fn holdsPlayer(all: *const Objects, index: u16) bool {
return all.in_stalag and all.slots[index].object.type.base() == .stalag;
}
/// A type's model, loaded for its first object where it isn't held, and one more object of it
/// counted, so that it stays (`create_object`, `ship_type_first_levels`). Null where the game
/// has no model for it.
pub fn useType(all: *Objects, types: Types, ship_type: gameobj.Type) ?*const Type {
const use = &all.types[ship_type.number()];
if (use.objects == 0 and use.loaded == null) use.loaded = types.load(types.context, @intCast(ship_type.number()));
use.objects += 1;
return use.loaded;
}
/// Whether the display's clock counts down (`hud.clockTime`): in Instant Action's simulator,
/// and in mission 29 however it is flown.
pub fn countsDown(all: *const Objects) bool {
return all.simulator.mode == .instant_action or all.mission_number == instant_action_mission;
}
/// Whether the player trains: in the simulator's training (`Simulator.Mode.training`), or in
/// one of the training missions however it is flown (`training_missions`), as each of the
/// game's checks reads it (`simulator_mode` 1, or missions 30 to 35). The flight instructor
/// then speaks for the carrier and clears the player's ship to land (`radio`), the player's
/// ship takes the training's missiles (`tierFit`), and `SetInvulnerability` reaches the
/// players' ships too.
pub fn training(all: *const Objects) bool {
if (all.simulator.mode == .training) return true;
return all.mission_number >= training_missions[0] and all.mission_number <= training_missions[1];
}
/// What the ship in slot `index` takes on its missile hardpoints where it is fitted by its
/// loadout tier (`object_loadout_by_tier`, `0x0045E57A` on): the missiles `tier` names, but
/// for the player's ship in training (`training`), the training's (`training_racks`),
/// whatever the tier.
pub fn tierFit(all: *const Objects, index: u16, tier: u2) Fit {
if (index == all.player and all.training()) return .{ .loadout = &training_racks };
return .{ .tier = tier };
}
/// The rules the mission played follows (`gameflow.campaignField`).
pub fn rules(all: *const Objects) gameflow.CampaignMission.Rules {
return gameflow.campaignField(all.mission_number, .rules);
}
/// Whether the player's wing flies Kamovs: in a mission whose rules say so, mission 25 in the
/// original, but not in its second part.
pub fn kamovPart(all: *const Objects) bool {
return all.rules().kamov_wing and !all.mission25_second_part;
}
/// The type `create_object` makes an object asked for as `asked` of in slot `index`: in a
/// player's slot, a Kamov in mission 25's first part (`kamovPart`), or else the ship the
/// loadout chose, its `t_` twin in a mission whose rules say so (`wing_twins`, from mission 14
/// in the original); in any other slot, `asked`.
///
/// Left out: in the simulator's training, a Yamato the loadout chose becomes a Reliant
/// (`0x00466CDF`), which never happens: the training puts the player in a Grendel, and its
/// missions fly the twins, so the type is the twin already.
///
/// Not ported: a multiplayer game, where every slot takes `asked`
/// ([#55](https://github.com/OpenReliant/openreliant/issues/55)).
pub fn slotType(all: *const Objects, index: u16, asked: gameobj.Type) gameobj.Type {
if (index >= all.players or index >= all.loadout_ships.len) return asked;
if (all.kamovPart()) return .of(.kamov);
const chosen = all.loadout_ships[index] orelse return asked;
if (!all.rules().wing_twins) return chosen;
return chosen.twin() orelse chosen;
}
/// The racks the loadout screen fitted for the player's ship in slot `index`, which a re-arm
/// fits it with (`cmd_ReplenishWeapons`, `0x0045A055`): a player's slot but the player's own
/// in the simulator, where the loadout ran. `create_object` passes over mission 25's first
/// part too (`createdRacks`).
///
pub fn loadoutRacks(all: *const Objects, index: u16) ?*const Racks {
if (index >= all.players or index >= all.loadout_racks.len) return null;
if (index == all.player and all.simulator.simulated()) return null;
if (all.loadout_racks[index]) |*racks| return racks;
return null;
}
/// The racks `create_object` fits the ship it makes in slot `index` with (`0x00467690`):
/// the loadout's, as a re-arm takes them, but in the simulator or in mission 25's first part,
/// when the ship is fitted by its tier.
fn createdRacks(all: *const Objects, index: u16) ?*const Racks {
if (all.simulator.simulated() or all.kamovPart()) return null;
return all.loadoutRacks(index);
}
/// The slots the loops over the objects walk, in their order.
pub fn walk(all: *const Objects) Walk {
return .{ .all = all };
}
/// Each slot handed out, from the first, then the cutaway slot. The loops read the count at
/// every slot, so an object created on the way is walked as well.
///
/// **Improvement:** with every slot handed out, the cutaway slot comes round again and again in
/// the game's loops, which never end; OpenReliant walks it once.
pub const Walk = struct {
all: *const Objects,
at: u16 = 0,
pub fn next(w: *Walk) ?u16 {
if (w.at < w.all.count) {
defer w.at += 1;
return w.at;
}
if (w.at > cutaway_slot) return null;
w.at = cutaway_slot + 1;
return cutaway_slot;
}
};
};
/// The players' loadouts `player_loadouts` (`0x00588400`) holds, a slot each.
pub const max_loadouts = 8;
/// The training missions, the first and the last (immediates in `cmd_SetInvulnerability_ship` and
/// `permission_to_land`).
pub const training_missions = [2]u16{ 30, 35 };
/// Mission 29, which INSTANT ACTION flies, and whose display counts down (`0x00486206`).
pub const instant_action_mission = 29;
/// The simulator the front end runs a mission in: `simulator_mode` (`0x00524FE4`), and `simulator`
/// (`0x0057E044`), which Instant Action sets as well. Out of the simulator, the wingmen hear their
/// keys (`frame_controls`), and the player's ship takes the loadout screen's weapons
/// (`create_object`, `cmd_ReplenishWeapons`).
pub const Simulator = struct {
mode: Mode = .none,
/// `simulator`: set while Instant Action runs, from the main menu or the simulator pod.
instant_action: bool = false,
/// What the simulator runs, as the loading screen names it (`0x004AD0A0`).
pub const Mode = enum(u8) {
/// No simulator: PREPARING FOR LAUNCH.
none = 0,
/// The Reliant's simulator's training: Calibrating Simulator.
training = 1,
/// Instant Action, mission 29: Preparing for Instant Action.
instant_action = 2,
_,
};
/// Whether the mission is simulated.
pub fn simulated(simulator: Simulator) bool {
return simulator.instant_action or simulator.mode != .none;
}
};
test "Stats.loadTypes" {
// A type takes the class and side its record gives it, and the other types keep theirs.
const gpa = std.testing.allocator;
const tables = try gpa.create(Stats);
defer gpa.destroy(tables);
tables.* = .initial;
var types = combat_stats.ship_types;
const shuttle = @backingInt(gameobj.GameType.yakob_shuttle);
try std.testing.expectEqual(.support, tables.combat[shuttle].class);
types[shuttle].class = .fighter;
types[shuttle].side = .neutral;
tables.loadTypes(&types);
try std.testing.expectEqual(.fighter, tables.combat[shuttle].class);
try std.testing.expectEqual(.neutral, tables.combat[shuttle].side);
try std.testing.expectEqual(combat_stats.ship_types[0].name, tables.combat[0].name);
}
test typeRecords {
// A mod's type takes its base's words, under its own name.
var list = [_]additions.ships.Added{.{ .name = "a:pot", .mod = "a", .base = .predator, .label_string = 900, .extra = .{ .model = "pot.shp" } }};
additions.ships.install(&list);
defer additions.ships.reset();
const made = try typeRecords(std.testing.allocator);
defer std.testing.allocator.free(made);
try std.testing.expectEqual(additions.ships.first + 1, made.len);
try std.testing.expectEqual(combat_stats.ship_types[0].class, made[additions.ships.first].class);
try std.testing.expectEqual(900, made[additions.ships.first].name);
}
test "ShipCombat.nameString" {
var combat = std.mem.zeroes(ShipCombat);
try std.testing.expectEqual(null, combat.nameString());
combat.name = 1104;
try std.testing.expectEqual(1104, combat.nameString().?);
}
test "ShipCombat.armorShare" {
var combat = std.mem.zeroes(ShipCombat);
combat.armor_class = 5;
// Its weakest quadrant counts, against six times the armour class.
try std.testing.expectEqual(0.5, combat.armorShare(.{ .left = 15, .right = 30, .fore = 30, .aft = 30 }));