Repository navigation
Expand file tree
/
Copy pathloadout.zig
More file actions
3096 lines (2866 loc) · 158 KB
/
Copy pathloadout.zig
File metadata and controls
3096 lines (2866 loc) · 158 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\interface\loadout\loadout.cpp`: the loadout screen, a hologram the briefing runs
//! between the mission's movie and Enriquez's last word (`interface_briefing`, `0x004376D2` on),
//! built on GenILib's 3D interface (`genilib.interf.i3d`). A disc lies before the camera with the
//! ships the pilot may fly on an arc round its rim, the chosen ship turns slowly above it, and
//! panels with the ship's name and figures, and six buttons, stand round it. All of it spins in
//! out of the disc's glow as the loadout begins. Clicking a ship on the arc flies it up to the
//! chosen spot as the chosen one flies back. On the missile page the other ships sink into the disc
//! and the missiles rise out of it, the chosen ship turns belly up, and a missile clicked flies to
//! a hardpoint of it. The internal guns view sweeps a glowing plane across the chosen ship, which
//! turns into its guns as the plane passes. Exit Loadout Computer spins it all away again, and the
//! ship chosen, with the missiles on its racks, is the one the pilot flies.
//!
//! The file's code also builds the band a planet's atmosphere is made of (`bandMesh`) and the
//! square the chase view's sights and a jump's flare are drawn on (`squareMesh`).
const std = @import("std");
const Allocator = std.mem.Allocator;
const shp = @import("../../../formats/shp.zig");
const spr = @import("../../../formats/spr.zig");
const tcache = @import("../../../formats/tcache.zig");
const tga = @import("../../../formats/tga.zig");
const input = @import("../../input.zig");
const math = @import("../../surrender/math.zig");
const srapi = @import("../../surrender/surrenderlib/srapi.zig");
const srapiext = @import("../../surrender/surrenderlib/srapiext.zig");
const srcore = @import("../../surrender/surrenderlib/srcore.zig");
const srlight = @import("../../surrender/surrenderlib/srlight.zig");
const srtexture = @import("../../surrender/surrenderlib/srtexture.zig");
const i3d = @import("../../genilib/interf/i3d.zig");
const camera = @import("../../game/camera.zig");
const cbox = @import("../../game/cbox.zig");
const create = @import("../../game/create.zig");
const additions = @import("../../game/additions.zig");
const explode = @import("../../game/explode.zig");
const gameflow = @import("../../game/gameflow.zig");
const gameobj = @import("../../game/gameobj.zig");
const hog_snd = @import("../../game/hog_snd.zig");
const hud = @import("../../game/hud.zig");
const language = @import("../../game/language.zig");
const matmanager = @import("../../game/matmanager.zig");
const missiles = @import("../../game/missiles.zig");
const objects = @import("../../game/objects.zig");
const srofiles = @import("../../game/srofiles.zig");
const xtrabits = @import("../../game/xtrabits.zig");
const canvas = @import("../../game/interface/canvas.zig");
const rooms = @import("../../game/interface/rooms.zig");
const Vector = math.Vector;
pub const anims = @import("anims.zig");
pub const bars = @import("bars.zig");
pub const panels = @import("panels.zig");
pub const hologram = @import("hologram.zig");
pub const racks = @import("racks.zig");
pub const simulator_pod = @import("simulator_pod.zig");
pub const tables = @import("tables.zig");
const log = std.log.scoped(.loadout);
/// What the loadout reads, plays and draws with.
pub const Context = struct {
/// The rooms' own: the allocator, `resource.hog`, the sound, and the speech's archive and how
/// it sounds.
rooms: rooms.Context,
/// The texture cache, which the loadout decodes its own textures from.
cache: tcache.Cache,
strings: *const language.Language,
/// The ship types' stats (`stats_load_ships`), which the ships' figures are worked out of.
stats: *const create.Stats,
/// The missile types' stats (`missile_stats`), which the missiles' figures are worked out of.
missile_stats: *const missiles.Table,
/// The mission the loadout comes before (`mission_number`).
mission: u16,
/// The carrier whose backdrop it shows: a game mode's briefing room's, so that the room and
/// the loadout are on the same ship. Null picks it by the mission's number (`backdrop`).
///
/// **Improvement:** the original picks the backdrop by the mission's number alone, so the
/// StarLancer trial's missions 1 and 2 go from the Yamato's briefing room to the Reliant's
/// loadout.
carrier: ?rooms.Carrier = null,
/// Whether the first mission's loadout teaches: says `loadout.ut` and blinks its exit
/// (`tutors`). A game mode's loadout doesn't, like the StarLancer trial's: the trial's
/// `loadout_enter` (`0x00408920` in its executable) has no speech, and its exit's blink times
/// from a speech start it leaves at 0, so the exit never blinks.
tutorial: bool = true,
/// The ships it offers, in order, where a game mode lists them (`tables.listedOffers`), in
/// place of those the tier and the rank open. It then starts on the ship chosen last where the
/// list has it, else on the first (`chosenFor`).
ships: ?[]const gameobj.Type = null,
/// The campaign's tier as the loadout finds it (`campaign_tier`, `0x00562DF0`), which it raises
/// by the mission (`tierBefore`).
tier: u2 = 0,
/// The pilot's rank (`pilot_rank`, `0x00562DEC`), which with the tier sets how many ships the
/// loadout offers.
rank: gameflow.Rank = 0,
/// The campaign's saved loadout, which the loadout starts from and its exit keeps.
saved: *Saved,
/// The settings' graphic detail (`graphic_detail`, `0x005D54E0`), which the ships on the arc
/// are drawn at while they move.
detail: explode.Detail = .high,
/// The longest side a texture keeps, by the settings' texture detail
/// (`srtexture.Table.largest`), null for any.
largest_texture: ?u32 = null,
/// What the game's models are built with: the light maps (`Lmaps`) and the lights.
models: srofiles.Settings = .{},
/// How it draws the models the original draws solid.
look: Look = .original,
/// Whether the loadout plays its lesson: `loadout.ut` and the blinking exit button
/// (`0x00441B6F`, `0x004433D2`). It does when the mission's loadout teaches (`teaches`) and the
/// lesson isn't turned off (`tutorial`).
fn tutors(context: Context) bool {
return context.tutorial and context.teaches();
}
/// Whether the mission's loadout teaches (`gameflow.CampaignMission.Rules.lesson`, mission 1
/// in the original). It then starts on the Predator with the tier's missiles, even with the
/// lesson turned off.
fn teaches(context: Context) bool {
return gameflow.campaignField(context.mission, .rules).lesson;
}
/// The only ship the mission's loadout offers, if any (`gameflow.CampaignMission.only_ship`).
/// In the original, mission 23 offers only the Shroud, which it names by its object
/// (`0x00523994`).
fn onlyShip(context: Context) ?create.TypeIndex {
const only = gameflow.campaignField(context.mission, .only_ship) orelse return null;
return @intCast(only.number());
}
};
/// The campaign's saved loadout (`0x00562F18`): the ship the pilot last chose and the missiles on
/// its racks (`0x00562F1A`), which `campaign_new` makes the Predator with none, and a restart keeps.
pub const Saved = struct {
/// The ship type chosen: one of the game's twelve, or a mod's (`tables.Offer`). Null before a
/// game mode's first loadout (`unchosen`), which then starts on the first ship it offers with
/// the tier's missiles.
ship: ?create.TypeIndex = predator,
racks: racks.Saved = @splat(null),
/// A game mode's saved loadout as the mode starts, before the player has chosen anything.
///
/// **Improvement:** the original's only saved loadout is the campaign's, which `campaign_new`
/// makes the Predator with no missiles.
pub const unchosen: Saved = .{ .ship = null };
};
/// What the loadout leaves the mission (`player_loadouts[player_index]`, `0x00588400`): the ship
/// type the pilot flies and the missile types on its racks, which `create_object` fits it with
/// (`create.Objects.loadout_racks`); and the campaign's tier as it raised it, which the mission's
/// fighters are armed by (`campaign_tier`).
pub const Result = struct {
ship: create.TypeIndex,
racks: create.Racks,
tier: u2,
};
/// What a frame of the loadout reads (`loadout_frame`'s arguments): the clock's milliseconds and
/// the pointer.
pub const Frame = struct {
now: u32,
mouse: i3d.Interface.Mouse,
};
/// The ship a loadout that teaches starts on: the Predator, ship type 0.
pub const predator = 0;
/// The loadout's near plane (`0x00442737`), where the briefing's is `srapi.in_flight_near`.
///
/// Not ported: its far plane, 1000 (`sr + 0x16A2`), which OpenReliant's projection does without.
const near: f32 = 1;
/// The palette and the pictures the loadout loads from `resource.hog`: `palette3.tga`, which it
/// gives the device and VFX while it runs (`0x004EAC54`); the backdrops of the Reliant and the
/// Yamato (`0x004EA9BC`, `0x004EA9CC`); the panels' art (`0x004EA9B0`); its fonts (`0x004EA978`,
/// `0x004EA96C`); and its sounds (`0x004EAC10`).
const palette_name = "palette3.tga";
const reliant_backdrop = "rbackground.tga";
const yamato_backdrop = "background.tga";
const panels_name = "fpanels.tga";
pub const title_font_name = "ld_handel.fnt";
test "Newtown stands in for the tooltip's font" {
try std.testing.expect(hud.outline.standsIn(title_font_name));
}
pub const stats_font_name = "handels.fnt";
const sounds_name = "ldsmp.fat";
/// The textures the loadout requires (`0x004EAA5C` on): the disc's four quarters, the glow, the
/// cursor's, and the hardpoints' markers'.
const plate_names = [4][]const u8{ "plate-nw", "plate-ne", "plate-sw", "plate-se" };
const glow_name = "hologlow";
const cursor_name = "ld_cursor";
const marker_name = "hpoints";
/// The Ship Missile objects' name (`0x004EAAB8`).
const ship_missile_name = "Ship Missile";
/// The speech mission 1's loadout says, from `speech_hog` (`0x004EACC8`).
const speech_name = "loadout.ut";
/// The loadout's sounds in `ldsmp.fat`, and their volumes (`sound_play` calls).
const Sound = enum(u8) {
/// The hologram's hum, looping from the intro, 12 quarter tones down (`0x0044495A`).
hum = 0,
/// A button pressed (`0x00447283`).
button = 1,
/// Exit Loadout Computer (`0x00447996`).
exit = 2,
/// The intro (`0x00444977`).
intro = 3,
/// A missile flying to a hardpoint (`0x00447384`), and one flying back to its icon
/// (`0x004474E3`).
attach = 4,
detach = 5,
/// The loadout's end, as the disc has spun away (`0x004466DE`).
end = 6,
/// The pointer onto another object (`0x004435BE`).
hover = 7,
/// Another ship selected (`0x00448140`).
select = 8,
/// The internal guns view opened or closed (`0x0044A453`, `0x0044A240`).
guns = 10,
_,
};
/// The volume of the loadout's quieter sounds, the hum, a button's, the hover's, a selection's and
/// a missile's flights (`0x28`), and the hum's pitch, 12 quarter tones down (`0x00444950`).
const quiet = 40;
const hum_pitch = -12;
/// A panel's texture (`texture_create_transient`), and the pixels the loadout draws it with.
const PanelTexture = struct {
pixels: panels.Image,
image: srtexture.Image,
/// A texture not made yet, as one is while the loadout loads.
const unmade: PanelTexture = .{ .pixels = undefined, .image = .{ .levels = &.{} } };
};
/// Where the tooltip is written, centred (`0x0044B279`).
const tooltip_at: [2]i32 = .{ 320, 458 };
/// How far a button sinks from the camera while it is pressed (`0x004DC3F8`).
const button_depth: f32 = 0.2;
/// The share of a ship's own scale it is drawn at on the arc (`arc_share`): the arc's unit of
/// scale (`anims.arc_unit`) times the double `0x004DC6E8`.
const arc_share: f32 = anims.arc_unit * 111.11111111111111;
/// What entering scales each ship on the arc by, over `anims.arc_unit` and the ship's scale
/// (`0x004DC6F0`): near enough the loading's scaling undone.
const enter_scale: f32 = 0.009;
/// How fast the chosen ship turns, radians a millisecond: a turn every four seconds
/// (`0x004DC6D8`), and how far it is tilted back about X (`0x00447197`).
const spin_rate: f32 = std.math.pi / 2000.0;
const spin_tilt: f32 = -0.75;
/// When mission 1's exit blinks, in milliseconds after the loadout's speech began, and how
/// (`0x004433DA`): hidden for the first 250 of every 500.
const blink_from = 15000;
const blink_until = 25000;
const blink_period = 500;
const blink_off = 250;
/// How many frames after a page is built each ship's rectangle is made again (`0x004468C9`).
const rect_frames = 2;
/// The light masks the loadout gives its ships' parts, which the green and the ambient lights reach
/// (`0x00441EF5`), and its cursor, which the cursor's and the ambient light reach (`0x0044690E`).
const ship_light_mask = 0xFFFD;
const cursor_light_mask = 0xFFEF;
/// The light mask of a gunship's part drawn in lines (`gunship_light_masks`, `0x004460E0`): the
/// bright green light's, which the loadout never adds, so the ambient light alone reaches it.
const gunship_line_light_mask = 0xFFFB;
/// How many corners a mesh's polygon of a line has (`0x0044610C`).
const line_corners = 2;
/// The colour of a gunship's own, red, green, blue and alpha (`node_tree_colour`, `0x0044204D`): a
/// faint green its lights add to.
const gunship_colour: [4]f32 = .{ 0, 0.03, 0, 1 };
/// How far back along X from the chosen ship's place the ship clipper's sweep starts, and how far
/// along X it goes (`0x004DC5E8`, `0x004DC718`).
const clip_back: f32 = 5.5;
const clip_sweep: f32 = 11;
/// Which way the guns view's portals face as the ship clipper sweeps (`0x0044912E`,
/// `0x0044917B`): the gunship's along X, keeping what lies behind the plane, and the ship's back
/// along it, keeping what lies ahead.
const gunship_normal: Vector = .{ 1, 0, 0 };
const ship_normal: Vector = .{ -1, 0, 0 };
/// How long the portals' normals are as the guns view opens (`0x0044A2C6`, `0x0044A320`): a quarter
/// turn, as if they were angles, until the sweep's first step makes them 1.
const opening_normal_length: f32 = std.math.pi / 2.0;
/// Which page the loadout shows (`loadout_page`, `0x005245E8`).
const Page = enum(u8) { ships = 1, missiles = 2 };
/// The missile page's own buttons, Use Default Loadout and Remove All Missiles, which appear with
/// it alone (`0x005245DC`, `0x005245E0`).
const missile_buttons = [_]hologram.Button{ .default, .remove_all };
/// A ship the loadout offers: its tree of parts, the object of the interface that stands for it
/// (`0x0052396C`), each part's level of detail as the loadout picks it, its zoom with the disc, its
/// sinking into it (`0x00524564`), and its gunship. What it carries are the missiles hung on its
/// racks, as the missile page fits them.
const Ship = struct {
model: objects.Model,
loaded: *const srofiles.Loaded,
object: i3d.Object,
/// A level for each part: the one the loadout shows it at, which it picks by hand.
levels: []srapiext.Level,
zoom: anims.Pair,
sink: anims.Pair,
gunship: Gunship,
/// `node_tree_level` (`0x0044B340`): each part shown at `level`, or its coarsest where it has
/// fewer, whatever the distance: `ship_object_create` takes the parts' levels away
/// (`0x0044B2F0`), leaving the mesh the loadout gives each.
fn showLevel(ship: *Ship, level: usize) void {
for (ship.model.parts, ship.loaded.parts, ship.levels, 0..) |*part, loaded, *shown, index| {
if (loaded.levels.len == 0) continue;
shown.* = .{ .mesh = loaded.levels[@min(level, loaded.levels.len - 1)].mesh, .until = std.math.inf(f32) };
part.object.levels = ship.levels[index..][0..1];
part.object.level = 0;
}
}
/// Its parts clipped by `portal`, or clipped no more (`clipTree`).
fn clip(ship: *Ship, portal: ?*const srapiext.Portal) void {
clipTree(&ship.model, portal);
}
};
/// A ship's gunship, which the internal guns view shows in the ship's place (`0x00523A64`): a tree
/// of the ship's gun model, and the object of the interface that stands for it.
const Gunship = struct {
model: objects.Model,
object: i3d.Object,
};
/// `gunship_light_masks` (`0x004460E0`): each part of a gunship's tree, whose meshes `loaded`
/// holds, reached by the green light and the ambient one, as the ships' parts are, but a part whose
/// finest mesh begins with a line, which the ambient light alone reaches
/// (`gunship_line_light_mask`).
fn gunshipLightMasks(tree: *objects.Model, loaded: *const srofiles.Loaded) void {
for (tree.parts, loaded.parts) |*part, file| {
if (file.meshes.len == 0 or file.meshes[0].surfaces.len == 0) continue;
const polygons = file.meshes[0].polygons;
const lines = polygons.len > 0 and polygons[0].count == line_corners;
part.object.light_mask = if (lines) gunship_line_light_mask else ship_light_mask;
}
}
/// `node_tree_portal` (`0x004492A0`): each part of `tree` clipped by `portal`, or with none, clipped
/// no more, the portal it had left as it was; and the parts of what it carries, the missiles hung
/// on its racks.
fn clipTree(tree: *objects.Model, portal: ?*const srapiext.Portal) void {
for (tree.parts) |*part| {
part.object.flags.portal_clipped = portal != null;
if (portal) |clipping| part.object.portal = clipping;
}
var each = tree.carried();
while (each.next()) |mount| clipTree(&mount.model, portal);
}
/// The missile hung in `held` let go of, its tree freed.
fn unhang(gpa: Allocator, held: *?objects.Model.Mount) void {
if (held.*) |mount| mount.model.deinit(gpa);
held.* = null;
}
/// How the loadout draws the models the original draws solid: the ships, and the missiles on the
/// arc and on the racks.
pub const Look = enum {
/// Solid, as the original draws them.
original,
/// **Improvement:** as a hologram (`srapiext.Surface.hologram`): a little see-through, with
/// faint scan lines drifting down them, and every few seconds a thin band that flares for a
/// moment, as a projector does when it slips its sync. A device that can't draw it draws them
/// solid.
hologram,
};
/// `node_tree_lit_blend` (`0x00445FE0`): every surface of the mesh each part of a model loaded as
/// `loaded` draws, its finest, made one pass, lit or not, and blended as `blend` says. The parts of
/// every tree made of `loaded` share its meshes.
fn litBlend(loaded: *const srofiles.Loaded, lit: bool, blend: srapiext.Material.Blend) void {
for (loaded.parts) |part| {
if (part.meshes.len == 0) continue;
for (part.meshes[0].surfaces) |*surface| {
surface.material.two_pass = false;
surface.material.lit[0] = lit;
surface.material.blend[0] = blend;
surface.hologram = false;
}
}
}
/// `litBlend` of a model the original draws solid, drawn as `look` says: solid, or as a hologram,
/// blended by alpha.
fn litSolid(loaded: *const srofiles.Loaded, lit: bool, look: Look) void {
if (look == .original) return litBlend(loaded, lit, .off);
litBlend(loaded, lit, .alpha);
for (loaded.parts) |part| {
if (part.meshes.len == 0) continue;
for (part.meshes[0].surfaces) |*surface| surface.hologram = true;
}
}
/// A missile's icon on the arc of the missile page (`0x00523CFC`, `missile_icon_create`,
/// `0x00445EF0`): a tree of its model at `anims.missile_scale`, lit and opaque, the object of the
/// interface that stands for it, whose press flies a copy of the missile to the chosen ship, and
/// its sinking into the disc (`0x005239CC`).
const Icon = struct {
model: objects.Model,
object: i3d.Object,
sink: anims.Pair,
};
/// A missile flying to a hardpoint of the chosen ship's, or back to its icon (`0x00523A04`,
/// `missile_flight_create`, `0x00445D80`): a copy of its model with meshes of its own
/// (`node_tree_meshes_copy`, `0x0044B3B0`), unlit and added, at `anims.missile_scale`; the object
/// of the interface that stands for it, shown and not clickable; its Attach Missile, and the
/// missile.
const Flight = struct {
loaded: srofiles.Loaded,
model: objects.Model,
object: i3d.Object,
attach: anims.Pair,
missile: tables.Missile,
};
/// How many missiles may fly at once (`0x00523A04`, 20 slots).
const max_flights = 20;
/// The chosen ship's racks the loadout keeps account of, as it empties them and fits them
/// (`0x00524560`): those its missile hardpoints' markers stand for once they are made
/// (`markers_make`), or every rack once a missile has been hung on it at once (`rack_fit`), until
/// the markers are made again.
const Counted = union(enum) {
markers: usize,
every_rack,
/// How many racks it counts.
fn count(counted: Counted) usize {
return switch (counted) {
.markers => |made| made,
.every_rack => gameobj.max_racks,
};
}
};
/// How a missile comes to hang on the chosen ship (`rack_fit`'s last argument): at once, as the
/// racks are fitted all together, or flown there from its icon.
const Hanging = enum { at_once, flown };
/// Where an Attach Missile takes a copy of a missile (`anim_attach_missile`'s data,
/// `0x00448B8A`): to the next empty hardpoint of the chosen ship's, or back to its icon from the
/// hardpoint of rack `from_rack`.
const Route = union(enum) {
to_next_empty,
from_rack: usize,
};
/// The light masks of the missiles hung on the chosen ship, which the red light and the ambient
/// light reach (`0x0044AC40`), so that they glow in with the red light's fade.
const hung_light_mask = 0xFFFE;
/// The share of the chosen ship's scale a missile hung on it takes (`0x0044B064`, `0x004DC410`).
const hung_share: f32 = 0.8;
/// The loadout, as `loadout_load` makes it and `loadout_enter`, `loadout_frame` and `loadout_leave`
/// run it. It must not move once made.
pub const Loadout = struct {
context: Context,
/// What the loadout's models, meshes and pictures are made in, let go of all at once.
arena: std.heap.ArenaAllocator,
/// What a frame's hit tests work in, emptied each frame.
frame_arena: std.heap.ArenaAllocator,
/// `palette3` (`0x00524258`), which the loadout gives the device and VFX while it runs.
palette: tga.Palette,
/// The loadout's textures, its ships' and its disc's, decoded with `palette`, as the device
/// converts them while the loadout has given it the palette (`texture_require`).
textures: ?srtexture.Table = null,
/// The device's camera and projection, and its background (`sr`'s, while the loadout runs).
view: srapi.Context,
scene: srcore.Scene = .{},
interface: i3d.Interface,
/// The campaign's tier as the loadout raises it (`campaign_tier`, `0x00441AA9`).
tier: u2,
/// The ships the loadout offers (`tables.offers`), the first `offer_count`, in the order of
/// `ships`.
offers: [tables.arc_slot_count]tables.Offer = undefined,
offer_count: usize = 0,
/// The ship chosen (`loadout_ship`, `0x00523E68`), by its place in `offers`.
chosen: u8,
/// The ships the tier or the rank offers (`0x00523E84`), from the Predator on, and the first
/// slot of the arc they stand in (`0x00524754`).
ships: []Ship = &.{},
first_slot: usize = 0,
disc: hologram.Panel = undefined,
glow: hologram.Panel = undefined,
/// The panels of the ship's figures, of its name and of the page's title (`0x00523AE8` on).
info: hologram.Panel = undefined,
name: hologram.Panel = undefined,
title: hologram.Panel = undefined,
buttons: std.EnumArray(hologram.Button, hologram.Panel) = undefined,
/// Two panels that face away from the camera, which only a view nothing opens turns round
/// (`0x00523E6C`, `0x00523E70`).
scrollers: [2]hologram.Panel = undefined,
/// The 3D cursor, made as the ship page is first built (`0x00523A9C`), and its texture, which
/// the loading requires (`0x00524224`).
cursor: ?*i3d.Cursor = null,
cursor_image: ?*srtexture.Image = null,
spin_disc: anims.Pair = undefined,
glow_appears: anims.Pair = undefined,
button_appears: std.EnumArray(hologram.Button, anims.Pair) = undefined,
/// The three panels' zooms (`0x00524480`).
panel_zooms: [3]anims.Pair = undefined,
flip_info: anims.Pair = undefined,
flip_name: anims.Pair = undefined,
/// The latest selection's two flights, where there has been one (`0x00523A98`, `0x00523A00`).
select: ?anims.Pair = null,
deselect: ?anims.Pair = null,
lights: std.EnumArray(hologram.Light, srlight.Light),
/// The portal in the disc's plane, which clips what sinks below it (`0x00524740`).
disc_portal: srapiext.Portal = .{},
/// The plane the internal guns view sweeps across the chosen ship (`0x00523958`), and its sweep
/// (`0x005245C4`); the two portals that follow the plane, the first clipping the gunship and
/// the second the ship, on either side of it (`gun portal1`, `0x00523EB4`; `gun portal2`,
/// `0x00523EB0`); and whether the view is open (`loadout_guns_open`, `0x00524610`).
clipper: hologram.Panel = undefined,
clip_sweep: anims.Pair = undefined,
gunship_portal: srapiext.Portal = .{},
ship_portal: srapiext.Portal = .{},
guns_open: bool = false,
/// The device's background as the loadout begins (`0x00523A54`).
backdrop: srtexture.Image = .{ .levels = &.{} },
/// The stats panel's textures, `finfo` and `binfo`, and the name panels', `fpanels` and
/// `bpanels`, which the buttons take their art from too (`0x00524214` on).
info_textures: std.EnumArray(panels.Face, PanelTexture) = .initFill(.unmade),
title_textures: std.EnumArray(panels.Face, PanelTexture) = .initFill(.unmade),
/// `fpanels.tga`'s pixels (`0x00523960`), which the name panels' textures start from, and
/// whose size the panels' textures take (`panels.Image`).
panels_art: panels.Image = undefined,
/// Each ship's figures as the loadout works them out as it loads (`loadout_ship_stats`,
/// `0x004EC278`).
figures: []tables.ShipFigures = &.{},
/// The missiles the loadout knows (`tables.Missile.all`): the game's, then the mods'. The
/// slices after it hold one for each, in its order (`missilePlace`).
known: []const tables.Missile = &.{},
/// Each missile's figures, worked out as it loads (`loadout_missile_bars_init`, `0x0044B680`).
missile_figures: []tables.MissileFigures = &.{},
/// The missiles' models (`0x00524450`), loaded with their red textures
/// (`loadout_weapon_models`, `0x00524977`), whose meshes their icons and the missiles hung on
/// the chosen ship share; and their icons on the arc.
missile_models: []srofiles.ModelFile = &.{},
icons: []Icon = &.{},
/// The slot of `tables.arc_slots` each missile's icon takes at the loadout's tier, null where
/// the tier offers it none (`tables.arcSlots`).
missile_slots: []?u8 = &.{},
/// The Ship Missile objects (`0x00524228`): each stands for the missile hung on a rack of the
/// chosen ship's, whose press flies it back to its icon.
ship_missiles: [racks.max_hardpoints]i3d.Object = undefined,
/// The chosen ship's racks.
fitted: racks.Racks = .{},
/// The markers of the chosen ship's missile hardpoints (`0x005246D4`), and the racks the
/// loadout keeps account of (`0x00524560`).
markers: [racks.max_hardpoints]?*hologram.Marker = @splat(null),
counted: Counted = .{ .markers = 0 },
/// The markers' texture (`hpoints`, `0x005245E4`), which the loading requires.
marker_image: ?*srtexture.Image = null,
/// The missiles flying to the chosen ship or back to their icons.
flights: [max_flights]?*Flight = @splat(null),
/// How many Attach Missiles have been made and have not ended (`0x00524750`): the next
/// missile clicked flies past as many empty hardpoints.
attaching: usize = 0,
/// The missile the info panel shows (`0x00523968`), none while it shows the chosen ship.
shown_missile: ?tables.Missile = null,
/// Ship to Belly Up (`0x00523E80`), made again each time the missile page is turned to.
belly_up: ?anims.Pair = null,
/// The objects the device's background holds, captured (`capture_background`).
still: std.ArrayList(*i3d.Object) = .empty,
title_font: hud.Opened = undefined,
stats_font: hud.Opened = undefined,
fonts_open: bool = false,
/// VFX's palette as the tooltip's font takes it: the text remap's entries of `palette`, in
/// 6-bit levels.
tooltip_palette: [spr.palette_size]u8 = @splat(0),
sounds: ?hog_snd.BankFile = null,
/// The voice the hum plays on (`0x00524744`).
hum: ?u8 = null,
/// Mission 1's speech and its file (`0x005246CC`), and when the loadout began to say it
/// (`0x0052395C`).
speech: cbox.Player = .{},
speech_line: []u8 = &.{},
speech_start: u32 = 0,
/// `loadout_entered` (`0x00524975`) and `loadout_running` (`0x0052461C`): the next frame
/// returns false once it is clear.
entered: bool = false,
running: bool = false,
page: Page = .ships,
last_page: Page = .ships,
/// The red light's level and the way it fades (`0x00524734`, `0x00523AA0`): 1 up, -1 down, 0
/// still.
red_level: f32 = 0,
red_fade: f32 = 0,
/// When the chosen ship began turning (`0x00523CF4`), and its turn (`0x00524758`).
spin_start: u32 = 0,
spin: math.Matrix = math.identity,
/// This frame's time and the last's (`0x005246C8`, `0x00523720`).
now: u32 = 0,
last: u32 = 0,
/// The object under the pointer at the last frame (`0x005246A4`).
last_hovered: ?*i3d.Object = null,
/// Frames until each ship's rectangle is made again (`0x00523EA8`).
rect_countdown: u8 = 0,
/// The level of detail the ships on the arc are drawn at while they move (`0x00523AB0`).
coarse: usize = 0,
/// Set once the ship has turned back from belly up (`0x00523AC8`), which lets its turn go on
/// while the loadout is busy.
turned_back: bool = false,
/// Whether the last frame's O asked for a screenshot, which the caller then saves.
screenshot: bool = false,
/// `loadout_load` (`0x00441AA0`), as the briefing loads: everything that takes time. The
/// tier is raised to what the missions before this one reached, and the ships' figures worked
/// out; the ships the tier or the rank offers are loaded with their green textures and made,
/// scaled down to the arc's size but for the chosen one; the disc, its glow, the panels and
/// the buttons are made and placed, and their animations built.
///
/// The missiles' models are loaded with their red textures and made into their icons, the
/// ships' gun models into their gunships, and the ships' and the missiles' sinkings built.
pub fn load(context: Context) !*Loadout {
const gpa = context.rooms.gpa;
const loadout = try gpa.create(Loadout);
const tier = @max(context.tier, tierBefore(context.mission));
var offers: [tables.arc_slot_count]tables.Offer = undefined;
const offered = offered: {
if (context.ships) |listed| {
const offered, const left_out = tables.listedOffers(listed, &offers);
if (left_out > 0) log.warn("{d} of the game mode's ships are left out of the loadout: they are listed twice, or the player can't fly them", .{left_out});
if (offered.len > 0) break :offered offered;
}
const offered, const left_out = tables.offers(tier, offeredShips(tier, context.rank), &offers);
if (left_out > 0) log.warn("{d} of the mods' ship types are left out of the loadout: the arc has no room for them", .{left_out});
break :offered offered;
};
loadout.* = .{
.context = context,
.arena = .init(gpa),
.frame_arena = .init(gpa),
.palette = undefined,
.view = .{ .projection = canvas.projection(canvas.size, camera.factors) },
.interface = .create(gpa, loadout),
.tier = tier,
.offers = offers,
.offer_count = offered.len,
.chosen = chosenFor(context, offered),
.lights = hologram.lights(),
};
errdefer loadout.destroy();
try loadout.loadParts();
return loadout;
}
fn loadParts(loadout: *Loadout) !void {
const context = loadout.context;
const arena = loadout.arena.allocator();
const gpa = context.rooms.gpa;
const resources = context.rooms.resources;
loadout.palette = try tga.palette(try resources.readFile(arena, palette_name));
loadout.sounds = .read(gpa, resources, sounds_name, context.rooms.codec);
loadout.textures = .init(gpa, context.cache, loadout.palette);
const textures = &loadout.textures.?;
textures.files = resources.mods.pictures();
textures.largest = context.largest_texture;
// The ships the tier or the rank offers (`loadout_ships_create`, `0x00444760`), each an
// object of the interface whose tooltip is its name (`0x00441E43` on), reached by the green
// and the ambient lights alone (`0x00446070`).
const offered = loadout.offer_count;
loadout.first_slot = (tables.arc_slot_count - offered) / 2;
loadout.ships = try arena.alloc(Ship, offered);
// Nothing hung on any ship yet, as `destroy` finds them should the loading stop part way.
for (loadout.ships) |*ship| ship.model.hung = &.{};
for (loadout.ships, 0..) |*ship, index| {
try loadout.makeShip(ship, @intCast(index));
ship.object = .create(0, context.strings.string(loadout.shipRecord(index).name), true);
ship.object.press = pressShip;
ship.object.target = .{ .tree = &ship.model };
for (ship.model.parts) |*part| part.object.light_mask = ship_light_mask;
}
// The missiles' models with their red textures (`0x00441D84` on), and their icons
// (`missiles_create`, `0x004447F0`), the mods' missiles' after the game's.
var known: [tables.max_missiles]tables.Missile = undefined;
loadout.known = try arena.dupe(tables.Missile, tables.Missile.all(&known));
const count = loadout.known.len;
loadout.missile_models = try arena.alloc(srofiles.ModelFile, count);
loadout.icons = try arena.alloc(Icon, count);
loadout.missile_figures = try arena.alloc(tables.MissileFigures, count);
loadout.missile_slots = try arena.alloc(?u8, count);
const left_out = tables.arcSlots(loadout.tier, loadout.known, loadout.missile_slots);
if (left_out > 0) log.warn("{d} of the mods' missiles are left out of the loadout: the arc has no room for them", .{left_out});
for (loadout.known, loadout.missile_models, loadout.icons) |missile, *file, *icon| {
const record = missile.record();
file.* = try loadout.loadModel(record.model, .loadout_weapons);
try loadout.makeIcon(icon, file.*, record);
}
// Each ship's gunship (`0x00441E08` on, `gunships_create`, `0x004447C0`).
for (loadout.ships, 0..) |*ship, index| try loadout.makeGunship(&ship.gunship, @intCast(index));
// The Ship Missile objects, not clickable and standing for nothing until a missile is hung
// on their rack (`0x0044218A` on).
for (&loadout.ship_missiles) |*object| {
object.* = .create(0, null, false);
object.press = pressShipMissile;
object.shown = false;
}
// Every ship but the chosen shrunk to the arc's share of its scale (`0x00442240` on).
for (loadout.ships, 0..) |*ship, index| {
if (index != loadout.chosen) i3d.scaleTree(&ship.model, arc_share * loadout.shipRecord(index).scale);
}
loadout.backdrop = try matmanager.readImage(arena, resources.*, backdrop(context));
const art = try matmanager.readPixels(arena, resources.*, panels_name, .multiples);
loadout.panels_art = try .create(arena, @max(1, art.width / panels.size));
const side = loadout.panels_art.side();
for (0..@min(art.height, side)) |y| for (0..@min(art.width, side)) |x| {
loadout.panels_art.pixels[y * side + x] = art.pixel(x, y) ++ .{hud.Pane.opaque_alpha};
};
try loadout.openFonts();
for (loadout.panelTextures()) |faces| {
for (&faces.values) |*texture| {
texture.pixels = try .create(arena, loadout.panels_art.multiple);
const levels = try arena.alloc(srtexture.Level, 1);
levels[0] = .{ .width = side, .height = side, .texels = std.mem.sliceAsBytes(texture.pixels.pixels) };
texture.image = .{ .levels = levels };
}
}
loadout.figures = try arena.alloc(tables.ShipFigures, offered);
bars.shipFigures(context.stats, loadout.offers[0..offered], loadout.figures);
bars.missileFigures(context.missile_stats, loadout.known, loadout.missile_figures);
for (loadout.ships) |*ship| try loadout.interface.addObject(&ship.object, "");
for (loadout.icons) |*icon| try loadout.interface.addObject(&icon.object, "");
for (loadout.ships) |*ship| try loadout.interface.addObject(&ship.gunship.object, "");
for (&loadout.ship_missiles) |*object| try loadout.interface.addObject(object, ship_missile_name);
const glow = try matmanager.textureRequire(textures, glow_name);
try loadout.makeGlow(glow);
var plates: [4]?*srtexture.Image = undefined;
for (&plates, plate_names) |*plate, plate_name| plate.* = try matmanager.textureRequire(textures, plate_name);
loadout.cursor_image = try matmanager.textureRequire(textures, cursor_name);
loadout.marker_image = try matmanager.textureRequire(textures, marker_name);
try loadout.makeDisc(plates);
try loadout.makePanels(glow);
loadout.place();
for (&loadout.button_appears.values, std.enums.values(hologram.Button)) |*appears, button| {
try anims.buttonAppears(appears, &loadout.interface, &loadout.buttons.getPtr(button).object, &loadout.glow.object, nextButtonAppears);
}
// Default and Remove All appear with the missile page alone.
for (missile_buttons) |button| loadout.button_appears.getPtr(button).anim.on_share = null;
for (&loadout.panel_zooms, [_]*hologram.Panel{ &loadout.info, &loadout.name, &loadout.title }) |*zoom, panel| {
try anims.panelZoom(zoom, &loadout.interface, &panel.object, &loadout.glow.object);
}
// Each ship's sinking from its slot on the arc, and each missile's from its slot of the
// tier's (`0x004425DA`, `0x00442600`); then the missiles put away.
for (loadout.ships, 0..) |*ship, index| {
try anims.sinkShip(&ship.sink, &loadout.interface, &ship.object, loadout.arcSlot(@intCast(index)), sinkShipShare, sinkShipEnded);
}
for (loadout.icons, loadout.missile_slots) |*icon, slot| {
try anims.sinkMissile(&icon.sink, &loadout.interface, &icon.object, loadout.missileSlot(slot), sinkMissileShare, sinkMissileEnded);
}
loadout.hideMissiles();
for (loadout.ships) |*ship| try anims.shipZoom(&ship.zoom, &loadout.interface, &ship.object);
}
/// `ship_object_create` (`0x00445BD0`): ship type `index`'s tree of parts, of its model loaded
/// with its green textures (`loadout_ship_models`, `0x00524976`), linked and standing at the
/// origin; each part lit and opaque at its finest level (`0x00445FE0`), and drawn at the level
/// the loadout gives it whatever the distance (`0x0044B2F0`).
///
/// Not ported: the swap of a part's `Missiles` texture for `gmissiles` (`0x0044B440`), which
/// the models take already, their textures named with the prefix.
fn makeShip(loadout: *Loadout, ship: *Ship, index: u8) !void {
const arena = loadout.arena.allocator();
const file = try loadout.loadModel(loadout.shipRecord(index).model, .loadout_ships);
try loadout.offers[index].fitModel(file.model, arena);
ship.* = .{
.model = try .create(arena, file.model, file.loaded, .{}),
.loaded = file.loaded,
.object = undefined,
.levels = try arena.alloc(srapiext.Level, file.loaded.parts.len),
.zoom = undefined,
.sink = undefined,
.gunship = undefined,
};
// Room for the missiles the missile page hangs on its racks.
ship.model.hung = try arena.alloc(?objects.Model.Mount, gameobj.max_racks);
@memset(ship.model.hung, null);
gameobj.linkParts(&ship.model, file.model);
ship.model.place(@splat(0), math.identity);
litSolid(file.loaded, true, loadout.context.look);
ship.showLevel(0);
try loadout.fitScale(index, file.model);
}
/// The record of the offered ship `index`.
fn shipRecord(loadout: *const Loadout, index: usize) tables.Ship {
return loadout.offers[index].record;
}
/// The ship type chosen.
fn chosenType(loadout: *const Loadout) create.TypeIndex {
return loadout.offers[loadout.chosen].ship_type;
}
/// The scale the offered ship `index`'s gunship shows at: its own, but for a mod's ship type
/// that shows its base's gun model, the base's (`tables.ownGunsModel`, `fitScale`).
fn gunshipScale(loadout: *const Loadout, index: usize) f32 {
const mod = additions.ships.get(loadout.offers[index].ship_type) orelse return loadout.shipRecord(index).scale;
if (tables.ownGunsModel(mod)) return loadout.shipRecord(index).scale;
return tables.ships[@backingInt(mod.base)].scale;
}
/// A mod's ship type, whose model `own` can be of any size, shown as large as its base: its
/// base's scale, times how much farther the base's model reaches than its own (`reach`).
fn fitScale(loadout: *Loadout, index: u8, own: *const shp.Model) !void {
const offer = &loadout.offers[index];
const mod = additions.ships.get(offer.ship_type) orelse return;
const arena = loadout.arena.allocator();
const base = tables.ships[@backingInt(mod.base)];
const base_model = try shp.Model.parse(arena, try loadout.context.rooms.resources.readFile(arena, base.model));
const own_reach = reach(own);
if (own_reach > 0) offer.record.scale = base.scale * reach(&base_model) / own_reach;
}
/// What the loadout's models are built with: the game's settings (`Context.models`), on the
/// loadout's renderer, their textures named with `prefix`'s letter.
fn modelSettings(loadout: *const Loadout, prefix: srofiles.Prefix) srofiles.Settings {
var settings = loadout.context.models;
settings.prefix = prefix;
return settings;
}
/// `model_load` (`0x004A44D0`) of `file` from `resource.hog`, its textures named with
/// `prefix`'s letter and decoded with the loadout's palette, made in the loadout's arena.
fn loadModel(loadout: *Loadout, file: []const u8, prefix: srofiles.Prefix) !srofiles.ModelFile {
const arena = loadout.arena.allocator();
const source = try arena.create(shp.Model);
source.* = try .parse(arena, try loadout.context.rooms.resources.readFile(arena, file));
const loaded = try arena.create(srofiles.Loaded);
loaded.* = try srofiles.modelLoad(arena, &loadout.textures.?, source, loadout.modelSettings(prefix), false);
return .{ .model = source, .loaded = loaded };
}
/// `missile_icon_create` (`0x00445EF0`) and the icon's object (`0x00441F64` on): a tree of
/// `file`'s model standing at the origin, lit and opaque, at `anims.missile_scale`; an object of
/// the interface whose tooltip is the missile's name, not clickable until the missile page
/// offers it, whose press flies a copy of the missile to the chosen ship (`pressIcon`) and
/// whose pointer's coming onto it shows the missile on the info panel (`enterIcon`); reached
/// by the green and the ambient lights alone.
fn makeIcon(loadout: *Loadout, icon: *Icon, file: srofiles.ModelFile, record: tables.MissileRecord) !void {
icon.model = try .create(loadout.arena.allocator(), file.model, file.loaded, .{});
gameobj.linkParts(&icon.model, file.model);
icon.model.place(@splat(0), math.identity);
litSolid(file.loaded, true, loadout.context.look);
i3d.scaleTree(&icon.model, anims.missile_scale);
icon.object = .create(0, loadout.context.strings.string(record.name), false);
icon.object.press = pressIcon;
icon.object.enter = enterIcon;
icon.object.target = .{ .tree = &icon.model };
for (icon.model.parts) |*part| part.object.light_mask = ship_light_mask;
}
/// `gunship_object_create` (`0x00445CC0`) and its object (`0x00442059` on): ship type `index`'s
/// gunship, a tree of the ship's gun model loaded with its red textures
/// (`loadout_weapon_models`, `0x00524977`), linked and standing at the origin, its parts lit as
/// `gunshipLightMasks` says and given a faint green of their own (`node_tree_colour`,
/// `0x00449310`); an object of the interface whose tooltip is the ship's name, not clickable,
/// and put away. The game names the object after its ship with ` GS` after it
/// (`0x00442103`), a name nothing looks for, and makes the gunships of all twelve ships, where
/// OpenReliant makes those of the ships offered, the only ones the view shows.
fn makeGunship(loadout: *Loadout, gunship: *Gunship, index: u8) !void {
const file = try loadout.loadModel(loadout.shipRecord(index).guns_model, .loadout_weapons);
gunship.model = try .create(loadout.arena.allocator(), file.model, file.loaded, .{});
gameobj.linkParts(&gunship.model, file.model);
gunship.model.place(@splat(0), math.identity);
gunshipLightMasks(&gunship.model, file.loaded);
for (gunship.model.parts) |*part| part.object.colour = gunship_colour;
gunship.object = .create(0, loadout.context.strings.string(loadout.shipRecord(index).name), false);
gunship.object.target = .{ .tree = &gunship.model };
gunship.object.shown = false;
}
/// Opens the loadout's two fonts (`font_open`), and the outline fonts that stand in for them
/// where the tooltip is written over the screen and in panels larger than the original's
/// (`hud.drawTextInto`). Both take VFX's palette through the text remap, in 6-bit levels as a
/// font's palette holds them: the title's for the tooltip, and both to fit their outline
/// fonts by.
fn openFonts(loadout: *Loadout) !void {
const arena = loadout.arena.allocator();
const resources = loadout.context.rooms.resources;
const remap: hud.Remap = .{ .table = &tables.text_remap, .palette = &loadout.palette };
for (0..tables.remap_length) |level| {
const colour = remap.colour(@intCast(level)) orelse continue;
for (loadout.tooltip_palette[level * 3 ..][0..3], colour) |*to, from| to.* = from >> 2;
}
loadout.title_font = .open(try .parse(try resources.readFile(arena, title_font_name)), &loadout.tooltip_palette);
loadout.stats_font = .open(try .parse(try resources.readFile(arena, stats_font_name)), &loadout.tooltip_palette);
loadout.fonts_open = true;
const outlines = loadout.context.rooms.outlines orelse return;
try loadout.title_font.standIn(outlines, title_font_name);
try loadout.stats_font.standIn(outlines, stats_font_name);
}
/// `loadout_glow_create` (`0x00444720`): the disc's glow, a square of `glow` 10 across, and its
/// Glow Appears.
fn makeGlow(loadout: *Loadout, glow: ?*srtexture.Image) !void {
try loadout.glow.make(loadout.arena.allocator(), &loadout.interface, .glow, glow, null, false, .{ 9.999, 9.999 }, .whole);
try anims.glowAppears(&loadout.glow_appears, &loadout.interface, &loadout.glow.object);
}
/// `loadout_disc_create` (`0x00444310`): the disc, 20 across, its four quarters textured with
/// `plates` (`discMesh`), unlit and added, named `Disc` and clickable; then the disc and its
/// glow placed, and its Spin Disc built.
fn makeDisc(loadout: *Loadout, plates: [4]?*srtexture.Image) !void {
const disc = &loadout.disc;
disc.mesh = try hologram.discMesh(loadout.arena.allocator(), plates);
try disc.add(&loadout.interface, .disc);
loadout.place();
try anims.spinDisc(&loadout.spin_disc, &loadout.interface, &disc.object, &loadout.glow.object, placeShipsStep, spinDiscEnded);
}
/// `loadout_panels_create` (`0x00443C20`): the panels' textures drawn with the chosen ship; the
/// ship's figures, its name and the page's title as panels turning over to their backs; the
/// six buttons, each with its tooltip and its press and release, made too small to see until
/// it appears; the two scrollers, facing away; the ship clipper, a square of `glow` on both its
/// faces, put away, and its sweep (Move Clip Point); and the flips of the two ship panels.
///
/// **Fix:** the game makes the ship clipper clickable, as it makes every panel, and the pointer
/// finds it though it is put away. Standing at the origin at its whole size until the guns
/// view first sweeps it, it covers the chosen ship and takes the pointer from the hardpoints'
/// markers, which come after it, so that their tooltip shows only once the view has been
/// opened. OpenReliant's is not clickable.
///
/// Not ported: the panel's clip object and its own Move Clip Point (`0x00523E7C`,
/// `0x00448EF0`), which belong to a view nothing opens (`0x0044A470`).
fn makePanels(loadout: *Loadout, glow: *srtexture.Image) !void {
loadout.drawPanels(loadout.chosen, loadout.chosen);
const info = &loadout.info_textures;
const title_front = &loadout.title_textures.getPtr(.front).image;
const title_back = &loadout.title_textures.getPtr(.back).image;
try loadout.info.make(loadout.arena.allocator(), &loadout.interface, .info, &info.getPtr(.front).image, &info.getPtr(.back).image, true, .{ 9.332, 9.332 }, .whole);
try loadout.name.make(loadout.arena.allocator(), &loadout.interface, .ship_name, title_front, title_back, true, .{ 8.999, 0.667 }, .{ .top = 0.08984375, .left = 0, .bottom = 0.1796875, .right = 0.99609375 });
try loadout.title.make(loadout.arena.allocator(), &loadout.interface, .page_title, title_front, title_back, true, .{ 9.999, 0.833 }, .{ .top = 0, .left = 0, .bottom = 0.09375, .right = 0.99609375 });
for (std.enums.values(hologram.Button)) |button| {
const panel = loadout.buttons.getPtr(button);
try panel.make(loadout.arena.allocator(), &loadout.interface, button.name(), title_front, null, false, hologram.button_size, button.span());
panel.scene_object.scale = hologram.button_hidden_scale;
panel.object.setTooltip(loadout.context.strings.string(button.tooltip()));
panel.object.press = pressButton;
panel.object.release = switch (button) {
.missiles => releaseMissiles,
.exit => releaseExit,
.ships => releaseShips,
.guns => releaseGuns,
.default => releaseDefault,
.remove_all => releaseRemoveAll,
};
}
const scroller_spans = [2]hologram.Span{
.{ .top = 0.3828125, .left = 0.49609375, .bottom = 0.4765625, .right = 0.68359375 },
.{ .top = 0.48046875, .left = 0.49609375, .bottom = 0.57421875, .right = 0.68359375 },
};
const scroller_places = [2]Vector{ .{ -9, 1.2, 0 }, .{ -9, 1.7, 0 } };
for (&loadout.scrollers, [2]hologram.Name{ .scroller0, .scroller1 }, scroller_spans, scroller_places) |*scroller, name, span, at| {
try scroller.make(loadout.arena.allocator(), &loadout.interface, name, title_front, null, false, .{ 0.9999, 0.4999 }, span);
scroller.scene_object.position = at;
scroller.scene_object.orientation = math.fromAngles(0, std.math.pi, 0);
}
try loadout.clipper.make(loadout.arena.allocator(), &loadout.interface, .ship_clipper, glow, glow, true, hologram.clipper_size, .whole);
loadout.clipper.object.shown = false;
loadout.clipper.object.clickable = false;
try anims.moveClipPoint(&loadout.clip_sweep, &loadout.interface, &loadout.clipper.object, sweepStep, sweepEnded);
try anims.flipInfo(&loadout.flip_info, &loadout.interface, &loadout.info.object);
try anims.flipName(&loadout.flip_name, &loadout.interface, &loadout.name.object, noop);
}
/// `loadout_placements` (`0x0044B5E0`): the camera where the loadout's stands, then each object
/// the placements name, where the interface has it by name (`0x0044B080`), put in its place and
/// turned by its angles.
fn place(loadout: *Loadout) void {
loadout.view.camera = .{ .position = tables.camera_position, .orientation = math.fromAngleVector(tables.camera_angles) };
for (tables.placements) |placement| {
const object = loadout.named(placement.name) orelse continue;