Repository navigation
Expand file tree
/
Copy pathwinmain.zig
More file actions
673 lines (616 loc) · 35.4 KB
/
Copy pathwinmain.zig
File metadata and controls
673 lines (616 loc) · 35.4 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
//! `C:\lancer\game\winmain.cpp`: the game's entry, `WinMain` (`0x004A8B10`), and its message pump.
//! **Unverified:** the pump (`0x004AAB20`), the queue of characters typed (`0x004AADA0` on) and the
//! list of call signs' reading and writing (`0x004AAE00`, `0x004AAEE0`) lie after the last of the
//! file's code that its assertions place; by what they do they are this file's.
//!
//! Ported so far: what the pump does as the game's window goes inactive and active again, as far
//! as the sound and the pause go; the characters typed, which the window's procedure queues; the
//! list of call signs the settings keep; and what follows a mission of the campaign
//! (`afterMission`), with the movies of a lost mission and of a career's end, which scripts can
//! choose (`lostMovie`, `careerOverMovie`). `openreliant`'s own frame loop stands in for the rest.
const std = @import("std");
const pilots = @import("pilots.zig");
const main = @import("main.zig");
const Clock = main.Clock;
const hooks = @import("../hooks.zig");
const create = @import("create.zig");
const input = @import("../input.zig");
const camera = @import("camera.zig");
const hog_snd = @import("hog_snd.zig");
const hudoptions = @import("hudoptions.zig");
const interface = @import("interface.zig");
const pilot_roster = @import("interface/pilot_roster.zig");
const rooms = @import("interface/rooms.zig");
const disc = @import("interface/disc.zig");
const CallSigns = pilot_roster.CallSigns;
const profile = @import("../profile.zig");
const Profile = profile.Profile;
const Sound = hog_snd.Sound;
const Ending = main.Ending;
const gameflow = @import("gameflow.zig");
const vm = @import("../vm.zig");
const files = @import("../files.zig");
const landing = @import("xtrabits/landing.zig");
const xtrabits = @import("xtrabits.zig");
const explode = @import("explode.zig");
/// The window's activation, as the pump follows it.
pub const App = struct {
/// Whether the game's window is the active one. The pump goes by `window_suspended`
/// (`0x005DDD28`), which `0x004A8260` sets as it puts the window away and `input_init`
/// clears, while the renderer runs (`app_active`, `0x005D6CAC`). **Unverified:** what puts the
/// window away.
active: bool = true,
/// `app_inactive_paused` (`0x005D6CAD`): whether the pump has paused the game for the window
/// going inactive.
paused: bool = false,
/// Whether an editor is linked (`editor_absent`, `0x004F634C`, inverted). `0x004A8260` puts the
/// window away only while none is (`0x004A8270`), so with one the game plays on while its
/// window is inactive, behind the editor.
editor_linked: bool = false,
};
/// `message_pump` (`0x004AAB20`), the part that follows the window's activation. Going inactive,
/// the music, the 3D voices and the voices pause, and the pump waits on the window's messages
/// until it is active again; then the sound goes on. Only in a multiplayer session with a mission
/// `loaded` (`mission_loaded`, `0x00588734`) does it pause the mission as well (`game_pause`),
/// which it then leaves in its pause menu. The textures, which DirectDraw loses with the window,
/// need nothing in OpenReliant.
///
/// **Improvement:** OpenReliant pauses a mission `loaded` into its menu in single player too, where
/// the game pauses only the sound and the timer's ticks pile up while the window is away. Active
/// again, the music goes on; the rest waits for the menu's CONTINUE.
///
/// With an editor linked, the window counts as active (`App.editor_linked`).
pub fn followActivation(app: *App, pausing: main.Pausing, loaded: bool) !void {
const active = app.active or app.editor_linked;
if (active and app.paused) {
pausing.sound.holdMusic(false);
app.paused = false;
} else if (!active and !app.paused) {
pausing.sound.holdMusic(true);
if (loaded) try main.pause(pausing, true);
app.paused = true;
}
}
test followActivation {
const mss = @import("../mss.zig");
const fat = @import("../../formats/fat.zig");
const gpa = std.testing.allocator;
var speaker: hog_snd.testing.Speaker = undefined;
try speaker.init(2, null);
const driver = speaker.mixer.driver();
const sound = &speaker.sound;
const bytes = comptime hog_snd.testing.bank(2);
const v = sound.play(try fat.Bank.parse(&bytes), 1, hog_snd.loudest, hog_snd.forever, hog_snd.centre, hog_snd.own_pitch).?;
var archive = try hudoptions.testing.fontArchive(gpa);
defer archive.close(gpa);
var app: App = .{};
var clock: Clock = .{};
var mission: @import("gameobj.zig").testing.Mission = undefined;
try mission.init(gpa);
defer mission.deinit();
const world = mission.world();
var menu: hudoptions.PauseMenu = .{};
defer menu.close();
const pausing: main.Pausing = .{
.gpa = gpa,
.clock = &clock,
.sound = sound,
.menu = &menu,
.archive = archive.hog,
.world = &world,
};
// Active, nothing changes.
try followActivation(&app, pausing, true);
try std.testing.expectEqual(mss.Status.playing, driver.sampleStatus(sound.voices[v].sample));
// Inactive with no mission loaded, as in the front end, the music is held, the clock runs on
// and the menu stays shut, for a mission started later to find.
app.active = false;
try followActivation(&app, pausing, false);
try std.testing.expect(sound.music.held and !clock.paused and app.paused and !menu.isOpen());
app.active = true;
try followActivation(&app, pausing, false);
try std.testing.expect(!sound.music.held and !app.paused);
// Inactive with a mission loaded, the sound and the clock stop, once, and the menu opens.
app.active = false;
try followActivation(&app, pausing, true);
try followActivation(&app, pausing, true);
try std.testing.expect(clock.paused and app.paused and menu.isOpen());
try std.testing.expectEqual(mss.Status.stopped, driver.sampleStatus(sound.voices[v].sample));
// Active again, the mission waits in the menu; continuing, the voices go on.
app.active = true;
try followActivation(&app, pausing, true);
try std.testing.expect(clock.paused and !app.paused);
try main.pause(pausing, false);
try std.testing.expect(!clock.paused and !menu.isOpen());
try std.testing.expectEqual(mss.Status.playing, driver.sampleStatus(sound.voices[v].sample));
// With an editor linked, the game plays on behind it.
app.editor_linked = true;
app.active = false;
try followActivation(&app, pausing, true);
try std.testing.expect(!clock.paused and !app.paused and !menu.isOpen());
}
/// What `WinMain` reads of the settings' `[Device]` as the game starts (`0x004A8FB3`) that
/// OpenReliant goes by. **Not ported:** the rest it reads there for the renderer (`Device`, `Xres`,
/// `Yres` and `Windowed`), where OpenReliant's options stand in.
pub const Device = struct {
/// The options' cockpit setting (`View`, `cockpit_mode_setting`, `0x005D5A78`), 0 where the
/// file has none.
view: camera.CockpitSetting,
/// The brightness, which the file keeps in hundredths (`Gamma`, `device_gamma`, `0x005D6080`),
/// 100 where it has none: the renderer opens with it (`renderer_open`, `0x004A8600`), and the
/// settings' video changes it.
brightness: f32,
/// Whether the movies between the front end's screens play (`Transitions`, `0x005D5E80`), 1
/// where the file has none (`0x004A9081`); the settings' video changes it.
transitions: bool,
/// The renderer's details.
details: Details,
const video = interface.settings.video;
/// The key `WinMain` reads the brightness from (`0x00509948`); the video screen writes it as
/// `gamma`, which is the same key, as a key's case does not matter.
const gamma_key = "Gamma";
const default_gamma = 100;
pub fn read(settings: Profile) Device {
return .{
.view = @fromBackingInt(settings.int(video.section, video.view_key, 0)),
.brightness = @as(f32, @floatFromInt(settings.int(video.section, gamma_key, default_gamma))) / video.gamma_scale,
.transitions = settings.int(video.section, video.transitions_key, 1) != 0,
.details = .read(settings),
};
}
};
/// The details `WinMain` reads for the renderer (`0x004A9035` on), which take effect as it starts:
/// the texture detail (`Tdetail`, `texture_detail`, `0x00595D7C`), 1 where the file has none; the
/// graphic detail (`Gdetail`, `graphic_detail`, `0x005D54E0`), 2; and whether the light maps are
/// drawn (`Lmaps`, `light_maps`, `0x005D5618`), 1. The settings' video changes them, and
/// `renderer_open` writes them as it starts the renderer again (`0x004A8716` on).
///
/// **Improvement:** the texture detail is 2, the highest, where the file has none, as the others
/// are; the game's 1 caps the textures at 256, which none of its own pass.
///
/// **Fix:** a graphic detail past 2 counts as 2, where the game's uses of it disagree on one.
pub const Details = struct {
texture: xtrabits.TextureDetail = .high,
graphic: explode.Detail = .high,
light_maps: bool = true,
/// The keys, in `[Device]` (`0x005095DC`, `0x005095D4`, `0x005095CC`).
pub const texture_key = "Tdetail";
pub const graphic_key = "Gdetail";
pub const light_maps_key = "Lmaps";
pub fn read(settings: Profile) Details {
const section = interface.settings.video.section;
const highest = @backingInt(explode.Detail.high);
return .{
.texture = @fromBackingInt(settings.int(section, texture_key, @backingInt(xtrabits.TextureDetail.high))),
.graphic = @fromBackingInt(@min(settings.int(section, graphic_key, highest), highest)),
.light_maps = settings.int(section, light_maps_key, 1) != 0,
};
}
};
test "Device.read" {
// Without the file, the cockpit's view, at the renderer's own brightness.
const none: Device = .read(.empty);
try std.testing.expectEqual(.cockpit, none.view);
try std.testing.expectEqual(1, none.brightness);
try std.testing.expect(none.transitions);
try std.testing.expectEqual(Details{}, none.details);
// As the video screens write them.
const saved: Device = .read(.{ .text = "[Device]\nView=2\ngamma=150\nTransitions=0\nTdetail=0\nGdetail=1\nLmaps=0\n" });
try std.testing.expectEqual(.none, saved.view);
try std.testing.expectEqual(1.5, saved.brightness);
try std.testing.expect(!saved.transitions);
try std.testing.expectEqual(Details{ .texture = .low, .graphic = .medium, .light_maps = false }, saved.details);
// A graphic detail past the highest counts as it.
try std.testing.expectEqual(.high, Details.read(.{ .text = "[Device]\nGdetail=7\n" }).graphic);
}
/// The longest name `missionPath` makes; the game's buffer is far larger.
pub const mission_path_size = 32;
/// The mission file `WinMain` names for mission `number` at the mission's start (`0x004A9C42`,
/// `0x004AA40A`): `.\missions\mission<number>.dte`. Mission 25, once its first part is won
/// (`mission25_second_part`, `0x00587CDC`), is `mission251.dte`, its second part; and in a
/// multiplayer game mission 3 is `mission311.dte`.
///
/// **Improvement:** any mission whose rules give it a second part
/// (`gameflow.CampaignMission.Rules.second_part`) has it in `mission<number>1.dte`, as mission 25
/// has.
pub fn missionPath(buffer: *[mission_path_size]u8, number: u16, second_part: bool, multiplayer: bool) []const u8 {
if (second_part and gameflow.campaignField(number, .rules).second_part) return std.mem.print(buffer, "{s}{d}" ++ second_part_end ++ file_end, .{ path_start, number }) catch unreachable;
if (number == multiplayer_mission and multiplayer) return multiplayer_path;
return std.mem.print(buffer, "{s}{d}" ++ file_end, .{ path_start, number }) catch unreachable;
}
/// A mission's file, `mission<number>.dte`, and where it lies.
const file_start = "mission";
const file_end = ".dte";
const path_start = ".\\missions\\" ++ file_start;
/// What follows a mission's number in the name of its second part's file, as in `mission251.dte`
/// (`0x00509728`); and mission 3, whose multiplayer game is a file of its own (`0x0050970C`).
const second_part_end = "1";
const multiplayer_mission = 3;
const multiplayer_path = path_start ++ "311" ++ file_end;
/// The name of the file of mission `number`, without its folder, as `missionPath` names it: such
/// as `mission40.dte`. Added for OpenReliant, which matches mods' mission scripts to missions by it.
pub fn missionFileName(buffer: *[mission_path_size]u8, number: u16, second_part: bool) []const u8 {
return missionPath(buffer, number, second_part, false)[path_start.len - file_start.len ..];
}
test missionFileName {
var buffer: [mission_path_size]u8 = undefined;
try std.testing.expectEqualStrings("mission40.dte", missionFileName(&buffer, 40, false));
try std.testing.expectEqualStrings("mission251.dte", missionFileName(&buffer, 25, true));
try std.testing.expectEqualStrings("mission0.dte", missionFileName(&buffer, 0, false));
}
/// The number in a mission file's name, `mission<number>.dte` as `missionPath` names it, whatever
/// its case; null for any other name. Added for OpenReliant, which lists the missions a game's
/// folder holds (`openreliant missions`).
pub fn missionNumber(name: []const u8) ?u16 {
if (name.len <= file_start.len + file_end.len) return null;
if (!std.ascii.startsWithIgnoreCase(name, file_start) or !std.ascii.endsWithIgnoreCase(name, file_end)) return null;
return std.fmt.parseInt(u16, name[file_start.len .. name.len - file_end.len], 10) catch null;
}
test missionNumber {
try std.testing.expectEqual(1, missionNumber("mission1.dte"));
try std.testing.expectEqual(251, missionNumber("MISSION251.DTE"));
try std.testing.expectEqual(null, missionNumber("mission.dte"));
try std.testing.expectEqual(null, missionNumber("missionx.dte"));
try std.testing.expectEqual(null, missionNumber("mission1.shp"));
// Every name `missionPath` makes reads back.
var buffer: [mission_path_size]u8 = undefined;
try std.testing.expectEqual(25, missionNumber(files.leaf(missionPath(&buffer, 25, false, false))));
}
/// What `WinMain` does before the hangar's movie of a mission it flies (`0x004AA3B2` on, and
/// `0x004A9BE2` on after a briefing): the music starts fading out by `music_fade_step` from the
/// timer's count `game_ticks` (`music_fade_out`), and the voices stop (`sound_pause_all`). It then
/// waits `launch_wait`, in which the timer fades the music out. **Unverified:** that the call it
/// waits with, a second's worth of milliseconds its one argument, is `Sleep`, whose import the
/// executable's protection hides.
pub fn launchFade(sound: *Sound, game_ticks: u32) void {
sound.fadeMusic(music_fade_step, game_ticks);
sound.pauseAll();
}
/// The step `WinMain` fades the music out by, as a campaign starts and before the hangar's movie.
pub const music_fade_step = 15;
pub const launch_wait = std.time.ns_per_s;
/// What `WinMain` does when START GAME starts a single-player campaign at mission `mission`,
/// before the Reliant's rooms (`vr_rooms`) (`0x004AA1BA` on). The music starts to fade out by
/// `music_fade_step`, and the archive of the disc that holds the rooms opens
/// (`rooms.Carrier.disc`). If the mission's settings ask for it
/// (`gameflow.CampaignMission.Rules.induction`), a new pilot's intro (`new_intro`) and induction
/// (`interface.induction`) come first. The original does this before mission 1 (`0x004AA229`).
pub const CampaignStart = struct {
disc: disc.Number,
induction: bool,
pub fn of(mission: u16) CampaignStart {
return .{ .disc = rooms.Carrier.of(mission).disc(), .induction = gameflow.campaignField(mission, .rules).induction };
}
};
/// A new pilot's intro, played from the disc on a cleared screen (`play_bink_movie_resourced`,
/// `0x0050967C`).
pub const new_intro = "new_intro.bik";
test CampaignStart {
try std.testing.expectEqual(CampaignStart{ .disc = .two, .induction = true }, CampaignStart.of(1));
try std.testing.expectEqual(CampaignStart{ .disc = .two, .induction = false }, CampaignStart.of(18));
try std.testing.expectEqual(CampaignStart{ .disc = .one, .induction = false }, CampaignStart.of(19));
// A campaign that gives the induction to another mission.
var missions = gameflow.CampaignMission.original;
missions[0].rules.induction = false;
missions[4].rules.induction = true;
gameflow.installMissions(&missions);
defer gameflow.installMissions(&gameflow.CampaignMission.original);
try std.testing.expectEqual(CampaignStart{ .disc = .two, .induction = false }, CampaignStart.of(1));
try std.testing.expectEqual(CampaignStart{ .disc = .two, .induction = true }, CampaignStart.of(5));
}
test launchFade {
const mss = @import("../mss.zig");
const fat = @import("../../formats/fat.zig");
var speaker: hog_snd.testing.Speaker = undefined;
try speaker.init(2, null);
const driver = speaker.mixer.driver();
const sound = &speaker.sound;
const bytes = comptime hog_snd.testing.bank(2);
const v = sound.play(try fat.Bank.parse(&bytes), 1, hog_snd.loudest, hog_snd.forever, hog_snd.centre, hog_snd.own_pitch).?;
// Without music, only the voices stop, where they are.
launchFade(sound, 300);
try std.testing.expect(!sound.music.fading);
try std.testing.expect(sound.paused[v]);
try std.testing.expectEqual(mss.Status.stopped, driver.sampleStatus(sound.voices[v].sample));
}
/// Whether `WinMain` plays the landing (`play_landing_movie`, `xtrabits.landing`) after a mission
/// it flew, number `mission`, ended as `ending` (`0x004AA4B2` on): not after the player's ship was
/// destroyed, its pilot captured or the mission left, nor after the first part of a mission with
/// two, mission 25 in the original, which leads into its second (`second_part`); and not where a
/// lobby launched the game (`lobby_launch`,
/// `0x00595C64`), as OpenReliant never is.
pub fn landsAfter(ending: Ending, mission: u16, second_part: bool) bool {
return switch (ending) {
.destroyed, .captured, .left => false,
else => !gameflow.campaignField(mission, .rules).second_part or second_part,
};
}
test landsAfter {
try std.testing.expect(landsAfter(.playing, 1, false));
try std.testing.expect(landsAfter(.rescued, 1, false));
try std.testing.expect(!landsAfter(.destroyed, 1, false));
try std.testing.expect(!landsAfter(.left, 1, false));
try std.testing.expect(!landsAfter(.playing, 25, false));
try std.testing.expect(landsAfter(.playing, 25, true));
}
/// What follows a mission of the campaign, as `WinMain` goes on after it (`afterMission`).
pub const AfterMission = union(enum) {
/// The pilot came through: the next mission and the campaign's tier (`gameflow.endMission`),
/// the medal's ceremony first where there is one; then the ITAC's debriefing, and the rooms from
/// where the ITAC leaves the pilot, whose briefing room leads to the next mission
/// (`0x004AA6A0`, `0x004AA372`).
goes_on: gameflow.Record,
/// Mission 25's first part leads straight into its second, flown after the hangar's movie
/// (`0x004AA597`).
second_part,
/// A movie of how the mission ended, where there is one, then the restart screen
/// (`interface.restart`, `0x004AA557`, `0x004AA756`).
restart: ?[]const u8,
/// A movie of the pilot's career ending, then the front end's main menu.
career_over: []const u8,
/// The campaign's last mission won: the story's end, then the main menu with the campaign back
/// at its first mission (`0x004AA6F7`).
story_end,
};
/// What `WinMain` does as mission `mission` of the campaign ends (`0x004AA4B2` to `0x004AA756`),
/// after the landing where it plays one (`landsAfter`), `variables` being the game's as the mission
/// left them and `second_part` whether it was mission 25's second part, which it sets for what
/// follows:
///
/// - Destroyed, the pilot's funeral; captured, the pilot in the enemy's hands; and each then the
/// restart screen, as leaving the mission from the pause menu turns to it at once (`lost`).
/// - Picked up past the pickups allowed (`gameflow.Campaign.pickedUp`), the pilot's transfer, which
/// ends the career.
/// - Sent home for destroying a friend, the pilot's execution, then the restart screen.
/// - Mission 25's first part leads into its second, unless the script rated it a total failure.
/// - Otherwise the mission's end is recorded (`gameflow.endMission`): the campaign goes on, the
/// story ends after the last mission, and a total failure ends the career in the transfer or
/// the shuttle the story gives (`careerOver`).
pub fn afterMission(campaign: *gameflow.Campaign, player: *input.Player, variables: *vm.Variables, mission: u16, second_part: *bool, tier: u2, wingmen: *pilots.Wingmen) AfterMission {
const carrier = rooms.Carrier.of(mission);
switch (player.ending) {
.destroyed => return lost(second_part, funeral.get(carrier)),
.captured => return lost(second_part, capture),
.left => return lost(second_part, null),
.rescued => if (campaign.pickedUp(mission)) return .{ .career_over = transfer.get(carrier) },
.friendly_fire => return .{ .restart = execution.get(carrier) },
else => {},
}
if (gameflow.campaignField(mission, .rules).second_part and !second_part.* and variables.mission_success != .total_failure) {
second_part.* = true;
return .second_part;
}
second_part.* = false;
const record = gameflow.endMission(player, variables, mission, tier, campaign, wingmen) orelse return .{ .career_over = careerOver(mission, variables) };
if (record.next == gameflow.story_end) return .story_end;
return .{ .goes_on = record };
}
/// The restart screen after `movie`, where there is one, for a pilot lost or a mission left: mission
/// 25 is then replayed from its first part (`0x004AA750`).
fn lost(second_part: *bool, movie: ?[]const u8) AfterMission {
second_part.* = false;
return .{ .restart = movie };
}
/// The movies of how a mission ended, each carrier's (`0x00509694` to `0x005096F8`): the funeral,
/// the pilot's execution and the pilot's transfer. The pilot in the enemy's hands and the shuttle at
/// Fort Bear have one each (`0x0050968C`, `0x00509664`).
const funeral = std.EnumArray(rooms.Carrier, []const u8).init(.{ .reliant = "new_funeral.bik", .yamato = "new_funeral2.bik" });
const execution = std.EnumArray(rooms.Carrier, []const u8).init(.{ .reliant = "new_rel_exec.bik", .yamato = "new_y_exec.bik" });
const transfer = std.EnumArray(rooms.Carrier, []const u8).init(.{ .reliant = "new_reliant_transfer.bik", .yamato = "new_a y trans.bik" });
const capture = "int.bik";
const shuttle = "fortbearshuttle_.bik";
/// The movie of a total failure (`0x004AA5AD` on): after missions 25 and 27, the shuttle at Fort
/// Bear where the landing would be none (`landing.lastWithoutLanding`); on the Reliant, the pilot's
/// transfer off it where `reliant_alive` is set, and otherwise, as on the Yamato, off the
/// Yamato.
fn careerOver(mission: u16, variables: *vm.Variables) []const u8 {
if (landing.lastWithoutLanding(mission, variables)) return shuttle;
const on_reliant = rooms.Carrier.of(mission) == .reliant and variables.reliant_alive != 0;
return transfer.get(if (on_reliant) .reliant else .yamato);
}
/// The movie that plays as mission `mission` is lost or left as `ending`, rated `rating` by its
/// script, before the restart screen: `movie`, the one `WinMain` plays (`AfterMission.restart`), or
/// none for a game mode's mission, as the trial plays none. Scripts can change it
/// (`mission_lost`).
///
/// **Improvement:** a step of OpenReliant's own, so that scripts can choose the movie. The game
/// picks it inside `WinMain` (`0x004AA52F` on, `0x004AA707` on).
pub fn lostMovie(all: *create.Objects, ending: Ending, rating: vm.Variables.Outcome, mission: u16, movie: ?xtrabits.movie.Name) ?xtrabits.movie.Name {
if (hooks.enter(.mission_lost, lostMovie, .{ all, ending, rating, mission, movie })) |done| return done;
return movie;
}
/// The movie that plays as the pilot's career ends after mission `mission`, which ended as
/// `ending`, rated `rating`, before the main menu: `movie`, the one `WinMain` plays
/// (`AfterMission.career_over`). Scripts can change it (`career_over`).
///
/// **Improvement:** a step of OpenReliant's own, so that scripts can choose the movie. The game
/// picks it inside `WinMain` (`0x004AA4F4` on, `0x004AA5AD` on).
pub fn careerOverMovie(all: *create.Objects, ending: Ending, rating: vm.Variables.Outcome, mission: u16, movie: ?xtrabits.movie.Name) ?xtrabits.movie.Name {
if (hooks.enter(.career_over, careerOverMovie, .{ all, ending, rating, mission, movie })) |done| return done;
return movie;
}
test lostMovie {
// Without scripts, the movie the game picked plays.
var random: @import("../random.zig").Random = .{};
const all = try create.Objects.create(std.testing.allocator, &random);
defer all.destroy();
const funeral_name = xtrabits.movie.nameOf(funeral.get(.reliant)).?;
try std.testing.expectEqual(funeral_name, lostMovie(all, .destroyed, .failure, 5, funeral_name).?);
try std.testing.expectEqual(null, lostMovie(all, .left, .failure, 5, null));
try std.testing.expectEqual(funeral_name, careerOverMovie(all, .rescued, .success, 5, funeral_name).?);
}
test afterMission {
var wingmen: pilots.Wingmen = .{};
var campaign: gameflow.Campaign = .begin();
var variables = campaign.attempt();
var player: input.Player = .{};
var second_part = true;
// Destroyed, the funeral, the Yamato's after mission 18, and mission 25 replayed from its first
// part; captured, the capture; and left, the restart screen at once.
player.ending = .destroyed;
try std.testing.expectEqualStrings(funeral.get(.reliant), afterMission(&campaign, &player, &variables, 5, &second_part, 0, &wingmen).restart.?);
try std.testing.expect(!second_part);
try std.testing.expectEqualStrings(funeral.get(.yamato), afterMission(&campaign, &player, &variables, 20, &second_part, 0, &wingmen).restart.?);
player.ending = .captured;
try std.testing.expectEqualStrings(capture, afterMission(&campaign, &player, &variables, 5, &second_part, 0, &wingmen).restart.?);
player.ending = .left;
try std.testing.expectEqual(null, afterMission(&campaign, &player, &variables, 5, &second_part, 0, &wingmen).restart);
// Sent home, the execution, mission 25's part as it was.
player.ending = .friendly_fire;
second_part = true;
try std.testing.expectEqualStrings(execution.get(.reliant), afterMission(&campaign, &player, &variables, 5, &second_part, 0, &wingmen).restart.?);
try std.testing.expect(second_part);
// Won, mission 25's first part leads into its second, and the others go on.
player.ending = .playing;
variables.mission_success = .success;
second_part = false;
try std.testing.expectEqual(.second_part, std.meta.activeTag(afterMission(&campaign, &player, &variables, 25, &second_part, 0, &wingmen)));
try std.testing.expect(second_part);
try std.testing.expectEqual(26, afterMission(&campaign, &player, &variables, 25, &second_part, 0, &wingmen).goes_on.next);
try std.testing.expect(!second_part);
try std.testing.expectEqual(6, afterMission(&campaign, &player, &variables, 5, &second_part, 0, &wingmen).goes_on.next);
try std.testing.expectEqual(.story_end, std.meta.activeTag(afterMission(&campaign, &player, &variables, gameflow.last_mission, &second_part, 0, &wingmen)));
// A total failure ends the career: off the Reliant where `reliant_alive` is set, as a new
// campaign has it; off the Yamato after mission 18; and after mission 25, where the landing
// would be none, the shuttle.
variables.mission_success = .total_failure;
try std.testing.expectEqualStrings(transfer.get(.reliant), afterMission(&campaign, &player, &variables, 5, &second_part, 0, &wingmen).career_over);
player.ending = .playing;
try std.testing.expectEqualStrings(transfer.get(.yamato), afterMission(&campaign, &player, &variables, 20, &second_part, 0, &wingmen).career_over);
player.ending = .playing;
variables.yamato_alive = 0;
try std.testing.expectEqualStrings(shuttle, afterMission(&campaign, &player, &variables, 25, &second_part, 0, &wingmen).career_over);
// Picked up twice, the campaign goes on; the third time, the pilot is transferred.
variables.mission_success = .success;
player.ending = .rescued;
try std.testing.expectEqual(.goes_on, std.meta.activeTag(afterMission(&campaign, &player, &variables, 5, &second_part, 0, &wingmen)));
try std.testing.expectEqual(.goes_on, std.meta.activeTag(afterMission(&campaign, &player, &variables, 5, &second_part, 0, &wingmen)));
try std.testing.expectEqualStrings(transfer.get(.reliant), afterMission(&campaign, &player, &variables, 5, &second_part, 0, &wingmen).career_over);
}
/// The pilot's kills each attempt at a mission starts with: those over the campaign as the last
/// mission the pilot came through kept them (`gameflow.endMission`), and the mission's as its
/// record keeps them, `kept`. In the game, the restart point it saves before each attempt and
/// loads for a replay holds them (`skull_count`, `mission_kills`; `restart_save`, `0x004AA3FC`).
pub fn startMission(player: *input.Player, kept: u16) void {
player.kills.count = player.kills.kept;
player.kills.mission = kept;
}
test startMission {
var player: input.Player = .{ .kills = .{ .count = 9, .kept = 4, .mission = 5 } };
startMission(&player, 0);
try std.testing.expectEqual(4, player.kills.count);
try std.testing.expectEqual(0, player.kills.mission);
}
test missionPath {
var buffer: [mission_path_size]u8 = undefined;
try std.testing.expectEqualStrings(".\\missions\\mission1.dte", missionPath(&buffer, 1, false, false));
try std.testing.expectEqualStrings(".\\missions\\mission25.dte", missionPath(&buffer, 25, false, false));
try std.testing.expectEqualStrings(".\\missions\\mission251.dte", missionPath(&buffer, 25, true, false));
try std.testing.expectEqualStrings(".\\missions\\mission3.dte", missionPath(&buffer, 3, false, false));
try std.testing.expectEqualStrings(".\\missions\\mission311.dte", missionPath(&buffer, 3, false, true));
try std.testing.expectEqualStrings(".\\missions\\mission65535.dte", missionPath(&buffer, 65535, false, false));
// A mission a mod gives a second part has it in a file of its own too.
var missions = gameflow.CampaignMission.original;
missions[26].rules.second_part = true;
gameflow.installMissions(&missions);
defer gameflow.installMissions(&gameflow.CampaignMission.original);
try std.testing.expectEqualStrings(".\\missions\\mission271.dte", missionPath(&buffer, 27, true, false));
try std.testing.expect(!landsAfter(.playing, 27, false));
}
/// The characters typed into the game's window (`typed_keys`, `0x005D547C`, and their count,
/// `0x00595D70`), in the game's code page, as its procedure queues them from `WM_CHAR`
/// (`window_proc`, `0x004A83CF`): a hundred at most. While `file_names` is set (`0x005D6088`), as the pilot roster sets it for the
/// call sign that names the pilot's saves, the characters a file's name can't hold are refused
/// (`file_name_refused`).
pub const Typed = struct {
characters: [capacity]u8 = undefined,
count: usize = 0,
file_names: bool = false,
pub const capacity = 100;
/// The characters a file's name can't hold (`0x0050954C`).
pub const file_name_refused = "\\/:*?<>|\"";
/// Queues `character`, unless the queue is full or it is refused.
pub fn push(typed: *Typed, character: u8) void {
if (typed.count >= capacity) return;
if (typed.file_names and std.mem.findScalar(u8, file_name_refused, character) != null) return;
typed.characters[typed.count] = character;
typed.count += 1;
}
/// `typed_key_pop` (`0x004AADB0`): the first character queued, taken off, or null for none.
pub fn pop(typed: *Typed) ?u8 {
if (typed.count == 0) return null;
const first = typed.characters[0];
@memmove(typed.characters[0 .. typed.count - 1], typed.characters[1..typed.count]);
typed.count -= 1;
return first;
}
/// `typed_keys_clear` (`0x004AADA0`).
pub fn clear(typed: *Typed) void {
typed.count = 0;
}
};
test Typed {
var typed: Typed = .{};
for ("A1:") |character| typed.push(character);
try std.testing.expectEqual('A', typed.pop().?);
// A call sign's file name holds no colon.
typed.file_names = true;
typed.push('?');
typed.push('b');
try std.testing.expectEqual('1', typed.pop().?);
try std.testing.expectEqual(':', typed.pop().?);
try std.testing.expectEqual('b', typed.pop().?);
try std.testing.expectEqual(null, typed.pop());
// A hundred at most.
for (0..Typed.capacity + 5) |_| typed.push('x');
try std.testing.expectEqual(Typed.capacity, typed.count);
typed.clear();
try std.testing.expectEqual(null, typed.pop());
}
/// The settings' section of the call signs, and the key of each place, `name%02d` (`0x00509BC8`,
/// `0x00509BD8`).
const call_signs_section = "CallsignList";
fn callSignKey(buffer: *[8]u8, place: usize) []const u8 {
return std.mem.print(buffer, "name{d:0>2}", .{place}) catch unreachable;
}
/// `callsigns_load` (`0x004AAE00`): the call sign of each of the list's places from `settings`,
/// the first `player` where the settings have none (string `0xBF`, `pilot_roster.String.player`),
/// the rest empty. `WinMain` reads the list as the game starts, and writes it straight back
/// (`saveCallSigns`, `0x004A919B`).
pub fn loadCallSigns(settings: Profile, player: []const u8) CallSigns {
var list: CallSigns = .{};
for (&list.names, 0..) |*name, place| {
var key: [8]u8 = undefined;
const default: []const u8 = if (place == 0) player else "";
name.set(settings.value(call_signs_section, callSignKey(&key, place)) orelse default);
}
return list;
}
/// `callsigns_save` (`0x004AAEE0`): each place's call sign to `settings`, then the list read back
/// from them (`loadCallSigns`), as the settings give each call sign, without spaces at its ends.
pub fn saveCallSigns(list: *CallSigns, settings: *profile.File) std.mem.Allocator.Error!void {
for (&list.names, 0..) |*name, place| {
var key: [8]u8 = undefined;
try settings.write(call_signs_section, callSignKey(&key, place), name.slice());
}
// Every place is in the settings now, so the first needs no call sign in its place.
list.* = loadCallSigns(settings.profile, "");
}
test loadCallSigns {
const gpa = std.testing.allocator;
var arena_state: std.heap.ArenaAllocator = .init(gpa);
defer arena_state.deinit();
// Without the settings, the first place is PLAYER and the rest are empty.
var list = loadCallSigns(.empty, "PLAYER");
try std.testing.expectEqualStrings("PLAYER", list.names[0].slice());
try std.testing.expectEqual(0, list.names[1].len);
// The settings keep them, and give them back without spaces at their ends.
list.names[1].set("Ace ");
var settings: profile.File = .{ .arena = arena_state.allocator(), .profile = .empty };
try saveCallSigns(&list, &settings);
try std.testing.expect(settings.changed);
try std.testing.expectEqualStrings("PLAYER", settings.profile.value(call_signs_section, "name00").?);
try std.testing.expectEqualStrings("", settings.profile.value(call_signs_section, "name09").?);
try std.testing.expectEqualStrings("Ace", list.names[1].slice());
try std.testing.expectEqualStrings("Ace", loadCallSigns(settings.profile, "PLAYER").names[1].slice());
}