Repository navigation
Expand file tree
/
Copy pathvm.zig
More file actions
634 lines (573 loc) · 28.9 KB
/
Copy pathvm.zig
File metadata and controls
634 lines (573 loc) · 28.9 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
//! The mission script VM's run-time structures. **Unknown:** its source file; the interpreter's
//! code lies between `mission.cpp`'s and `attach.cpp`'s. [`vm/opcodes.zig`](vm/opcodes.zig) and
//! [`vm/conditions.zig`](vm/conditions.zig) transcribe its opcode and condition tables.
//!
//! A thread runs one block with a stack of its own. While it runs, the interpreter (`vm_run`,
//! `0x0045C980`) keeps its stack pointer and block end in globals (`vm_stack_top`, `0x00537570`;
//! `vm_block_end`, `0x005373F0`) and hands every opcode handler the addresses of the thread's
//! instruction pointer and frame pointer.
const std = @import("std");
const assert = std.debug.assert;
const dte = @import("../formats/dte.zig");
const layout = @import("../formats/layout.zig");
const commands = @import("game/executor/commands.zig");
const engine = @import("../engine.zig");
const hud = @import("game/hud.zig");
const Code = engine.Code;
const Pointer = engine.Pointer;
pub const opcodes = @import("vm/opcodes.zig");
pub const conditions = @import("vm/conditions.zig");
pub const machine = @import("vm/machine.zig");
pub const editor = @import("vm/editor.zig");
pub const triggers = @import("vm/triggers.zig");
pub const Machine = machine.Machine;
pub const Implementation = machine.Implementation;
pub const GameImplementation = machine.GameImplementation;
pub const run_on = machine.run_on;
pub const yield = machine.yield;
/// An opcode handler, called through `vm_dispatch_table`. `ip` points at the thread's instruction
/// pointer, already past the opcode, and `frame` at its frame pointer. `previous` is what the last
/// handler returned. A handler returns it to carry on, or zero to end the loop.
pub const Handler = Code("uint __fastcall (byte **ip, uint **frame, uint previous)");
/// A command's implementation, which `command` calls through the catalogue. `args` points at its
/// first argument on the stack. The result is stored in `Thread.result`, and a zero result also
/// ends the handler loop.
pub const Command = Code("uint __fastcall (byte **ip, uint *args)");
/// What a command hands `for_each_ship` to run for each ship its first argument names: the ship,
/// and the command's remaining arguments.
pub const ShipCommand = Code("uint __fastcall (MissionShip *ship, uint *args)");
/// Threads the pool at `vm_threads` holds. `vm_thread_start` (`0x0045B8D0`) starts none while 31
/// are running.
pub const max_threads = 32;
/// Timers the table at `vm_timers` (`0x004F6344`) holds.
pub const max_timers = 16;
/// The values an event carries at most, which a thread's locals and each event an object keeps
/// have room for.
pub const max_event_values = 5;
/// A script thread.
pub const Thread = extern struct {
/// Stack pointer, saved while the thread is suspended.
stack_top: Pointer(u32),
/// End of the running block, where its constants start. Saved like `stack_top`.
block_end: Pointer(u8),
/// The running part's first argument: `push_argument n` reads `frame[n]`.
frame: Pointer(u32),
/// Value of `vm_clock` to resume at, or zero when not waiting.
wake_time: u32,
/// Next instruction to run. Null for a free slot.
ip: Pointer(u8),
/// Block end at which the script debugger's step-over stops.
step_block_end: Pointer(u8),
/// The values of the event that started the thread, which `push_local` reads.
locals: [max_event_values]u32,
stack: [32]u32,
/// **Unknown.** `unknown_ac_start` when the thread starts.
_unknown_ac: u8,
/// Parts called and not yet returned from. A `return` at depth zero ends the thread.
call_depth: u8,
/// Set by `InterruptTriggerCode`: the thread waits for its trigger to fire again, which clears
/// it, and the pass over the threads (`vm_threads_run`) leaves it alone until then.
interrupted: bool,
/// The low byte of the index of the trigger that started the thread, or `no_trigger` for none.
trigger: u8,
/// The last command's result, or the value the last part returned: what `push_result` reads.
result: u32,
/// **Unknown.** Zero when the thread starts.
_unknown_b4: u32,
/// The `trigger` of a thread no trigger started (`part_run`, `0x0045BAB5`).
pub const no_trigger: u8 = 0xFF;
/// The `_unknown_ac` of a thread as it starts (`vm_thread_start`, `0x0045B92F`).
pub const unknown_ac_start: u8 = 0xFF;
comptime {
assert(@offsetOf(Thread, "wake_time") == 0x0C);
assert(@offsetOf(Thread, "ip") == 0x10);
assert(@offsetOf(Thread, "locals") == 0x18);
assert(@offsetOf(Thread, "stack") == 0x2C);
assert(@offsetOf(Thread, "call_depth") == 0xAD);
assert(@offsetOf(Thread, "result") == 0xB0);
assert(@sizeOf(Thread) == 0xB8);
}
};
/// What `call_part` pushes above a part's arguments, and `return` pops. The thread's frame pointer
/// then points at the first argument, `4 * arguments + 16` bytes below the stack pointer.
pub const CallRecord = extern struct {
argument_count: u32,
return_ip: Pointer(u8),
caller_frame: Pointer(u32),
caller_block_end: Pointer(u8),
comptime {
assert(@sizeOf(CallRecord) == 0x10);
}
};
/// An entry of the command catalogue, or of a mission's part table: what `command` and
/// `call_part` call. The catalogue fills every field; a part's entry has only its block and its
/// argument count.
pub const Function = extern struct {
entry: Entry,
/// Arguments taken from the stack. The engine reads a byte of the catalogue's dword.
argument_count: u8,
_unknown_05: [3]u8,
name: Pointer(u8),
params: [max_params]Param,
description: Pointer(u8),
/// **Unknown.**
flag: u32,
pub const max_params = 8;
pub const Entry = extern union {
implementation: Pointer(Command),
/// The part's block: its length halfword, then its code.
block: Pointer(u16),
};
pub const Param = extern struct {
kinds: commands.Kinds,
/// **Unknown.**
extra: u32,
label: Pointer(u8),
comptime {
assert(@sizeOf(Param) == 0x0C);
}
};
comptime {
assert(@offsetOf(Function, "name") == 0x08);
assert(@offsetOf(Function, "params") == 0x0C);
assert(@offsetOf(Function, "description") == 0x6C);
assert(@sizeOf(Function) == 0x74);
}
};
/// A timer that `CreateTimer` set.
pub const Timer = extern struct {
/// The part to start, or `free_part` for a free entry.
part: i32,
/// Countdown to reload after each firing.
period: u16,
/// Firings left. Zero for no limit; `DestroyTimer` runs after the last.
remaining: u16,
/// Clock ticks to the next firing.
countdown: u16,
/// The ID the script gave it, which `DestroyTimer` takes.
id: u16,
/// `vm_clock` when it last counted down, so that it counts once per tick.
last_tick: u32,
/// The `part` of a free entry (`vm_run_timers`, `0x0045D162`).
pub const free_part: i32 = -1;
pub fn isFree(timer: *const Timer) bool {
return timer.part == free_part;
}
/// Frees the entry, as `DestroyTimer` does.
pub fn free(timer: *Timer) void {
timer.part = free_part;
}
comptime {
assert(@sizeOf(Timer) == 0x10);
}
};
/// An entry of a part table (`part_table`, `part_table_b`) as OpenReliant keeps it: the part's
/// block, by where it lies in the mission image, and its argument count. `mission_fill_part`
/// (`0x00452FD0`) fills the game's `Function` with the same two.
pub const Part = struct {
/// Where the block's length halfword lies in the mission image; null for a part with no block.
block: ?u32 = null,
argument_count: u8 = 0,
};
/// Entries each part table has room for (`mission_alloc_part_tables`, `0x0045CB40`).
pub const part_table_size = 256;
/// A part table: the mission's parts, then entries of no block. **Fix:** the game leaves the
/// entries past the mission's parts as `malloc` gave them, and a call to one runs whatever they
/// hold.
pub const Parts = [part_table_size]Part;
/// The game's variables a script reads and writes by number (`push_array`, `select_array`): the
/// block of 64 dwords from `jump_ready` (`0x0052A3F0`) up to the next global (`0x0052A4F0`), of
/// which the engine and the shipped missions use the first 38. Some belong to an attempt at a
/// mission, which `gameflow.resetVariables` clears before each; the rest belong to the campaign,
/// which a new one sets (`gameflow.newCampaign`) and the pilot's saved game keeps. **Unknown:**
/// what most of the campaign's stand for: flags a new campaign sets, which the scripts clear as
/// the story's characters die and the flow between missions reads
/// ([#381](https://github.com/OpenReliant/openreliant/issues/381)).
pub const Variables = extern struct {
/// `jump_ready` and `warp_ready` (0 and 1): whether the mission has a jump or a warp ready for
/// JUMP DRIVE, which the display's prompt reads (`hud.Readiness`).
ready: hud.Readiness = .{},
/// Variable 2, which no script names. A new campaign and a saved game's load clear it with the
/// first 32 (`gameflow.newCampaign`).
_unused_2: u32 = 0,
/// `backup_available` (3): whether the carrier sends backup when the pilot asks for it. The
/// radio's REQUEST BACKUP (`0x004558D0`) raises the mission's PlayerWantsBackup event for the
/// first request while it is set, and the carrier refuses otherwise. The scripts set it as
/// backup can come and clear it as it can no longer (`radio.requestBackup`).
backup_available: u32 = 0,
/// `player_missiles_left` (4): the missile display's counts together.
player_missiles_left: u32 = 0,
/// The campaign's flags, which a new campaign sets to 1 (`gameflow.newCampaign`), the scripts
/// clear as the story goes, and the pilot's saved game keeps. The engine reads some of them to
/// pick the movies between missions (`xtrabits.landing`) and at the story's end; a custom
/// campaign can give them stories of its own.
///
/// `mcgann_alive` (5): whether McGann lives. Mission 15's script clears it as his Phoenix is
/// destroyed, and mission 9's reads it. The news report after mission 16 reads it too, but
/// never plays, as mission 16 ends no chapter.
mcgann_alive: u32 = 0,
/// `nicolai_petrov_alive` (6): whether Nicolai Petrov lives. The Dreamcast's mission 12, which
/// the PC game lacks, clears it as its Petrov's Basilisk is destroyed, and `mission191.dte`,
/// which the campaign never flies, reads it as its Petrov wing's leader alive, apart from Ivan
/// Petrov's (`ivan_petrov_alive`). Mission 27's script clears it as it starts. The third news
/// report after mission 11 (`new_chapter2_thread3.bik`) plays once it's clear, which it never
/// is then. **Unverified:** that this Petrov is Nicolai: the missions don't name him, but the
/// game's radio knows two Petrovs, Ivan and Nicolai, and Ivan's flag is 7.
nicolai_petrov_alive: u32 = 0,
/// `ivan_petrov_alive` (7): whether Ivan Petrov lives. Mission 28's script clears it as his
/// Basilisk is destroyed, and the story's end plays his news report once it's clear.
ivan_petrov_alive: u32 = 0,
/// `kulov_alive` (8): whether Kulov lives. Mission 28's script clears it as the Boridin
/// breakaway is destroyed, and the story's end plays his news report once it's clear.
kulov_alive: u32 = 0,
/// `mission_over` (9): set once the camera has watched the mission's end long enough, or once
/// the player's ship has landed.
mission_over: u32 = 0,
/// `landing_cleared` (10): whether PERMISSION TO LAND is granted, and the player's ship lands
/// (`radio.permissionToLand`). Mission 1's script sets it as the Reliant jumps in.
landing_cleared: u32 = 0,
/// `al_rahan_alive` (11): whether Al-Rahan lives, 0 in a new campaign. Mission 7's script
/// clears it as he dies, and mission 24's sets it as it starts, before any of its tests read
/// it, so mission 7's clear never shows.
al_rahan_alive: u32 = 0,
/// `sharif_alive` (12): whether Sharif lives, 0 in a new campaign. Mission 9's script clears it
/// as he dies, and nothing reads it.
sharif_alive: u32 = 0,
/// `steiner_alive` (13): whether Steiner lives. Mission 28's script clears it as his Wolverine
/// is destroyed, and the story's end plays one of his two news reports by it.
steiner_alive: u32 = 0,
/// `mission_success` (14): how the script rates the mission.
mission_success: Outcome = .failure,
/// `script_players` (15): how many players fly the mission, which the game gives the script as
/// the mission starts (`script_set_players`, `0x004124D0`): 1 outside a multiplayer game. The
/// scripts test it for the enemies a multiplayer game adds, and for which of a part's endings
/// runs: mission 1's ambush ends only for the count it was flown with.
players: u32 = 0,
/// Variable 16, which no script names: 1 in a new campaign, and kept with the pilot's game.
_unused_16: u32 = 0,
/// `krasnaya_alive` (17): whether the Krasnaya got away in mission 8, whose script clears it as
/// the ship is destroyed and sets it again as it jumps out. The second news report after
/// mission 11 plays once it's clear.
krasnaya_alive: u32 = 0,
/// `rameses_alive` (18): whether the Rameses survived mission 7, whose script clears it as the
/// ship is destroyed. Mission 24 brings the Rameses back while it's set, and the news report
/// after mission 7 plays once it's clear.
rameses_alive: u32 = 0,
/// `kozah_alive` (19): whether Kozah lives. Mission 9's script clears it as his group is
/// destroyed, and nothing reads it.
kozah_alive: u32 = 0,
/// Variables 20 and 21, which no script names: 1 in a new campaign, and kept with the pilot's
/// game.
_unused_20: u32 = 0,
_unused_21: u32 = 0,
/// `fixed_gate_alive` (22): whether mission 3's fixed gate stands. Mission 3's script sets it as
/// it starts and clears it as the gate is destroyed, and missions 4, 11, 20 and 26 take their
/// other ways while it's set.
fixed_gate_alive: u32 = 0,
/// `warp_gate_alive` (23): whether mission 16's warp gate stands. Mission 16's script clears it
/// as the gate falls. The news report after mission 16 reads it, but never plays, as mission 16
/// ends no chapter.
warp_gate_alive: u32 = 0,
/// Variables 24 to 26, which no script names: 0 in a new campaign, and kept with the pilot's
/// game.
_unused_24: u32 = 0,
_unused_25: u32 = 0,
_unused_26: u32 = 0,
/// `last_success` (27): how the script rated the last mission the pilot came through, which the
/// mission's end keeps unless the rating is a total failure (`mission_end_record`,
/// `0x00475AC2`). Mission 25's second part weighs its own rating by it. Mission 1's script
/// clears it as the mission starts, and a few others set it; the mission's end then writes
/// over both.
last_success: Outcome = .failure,
/// `objectives_met` (28): set by the script once the mission's objectives are met, as mission
/// 1's is once the ambushers are destroyed. The debriefing of a mission the ejected pilot was
/// picked up in tells the pilot the mission was a success by it, and a failure without it
/// (`0x00424ECE`, `0x0042545B`; `itac.debriefing`).
objectives_met: u32 = 0,
/// `czar_alive` (29): whether the Czar survived mission 11, whose script clears it as the ship
/// is destroyed. The first news report after mission 11 plays once it's clear.
czar_alive: u32 = 0,
/// `ghost_alive` (30): whether Ghost, the ace mission 1 puts up against the player, lives: 1 in
/// a new campaign. Mission 1's script clears it as Ghost dies, and mission 4's has Petrov say
/// a line by it. The engine only keeps it with the pilot's game.
ghost_alive: u32 = 0,
/// Variable 31, which no script names: 1 in a new campaign, and kept with the pilot's game.
_unused_31: u32 = 0,
/// `reliant_alive` (32): whether the Reliant flies. Missions 7, 8 and 18 clear it as she's
/// lost. While it's set, mission 8 ends in a landing on the Reliant rather than the Yamato, and
/// a total failure before mission 19 ends the pilot's career in the transfer off the Reliant
/// (`winmain.careerOver`).
reliant_alive: u32 = 0,
/// `countdown` (33): seconds left, which mission 29's script sets and the display shows as a
/// clock in minutes and seconds (`hud.clockTime`). The game takes one off at every 100th tick
/// of the mission (`gameobj.gameTick`).
countdown: i32 = 0,
/// `chapter2_thread3_shown` (34): set as the third news report after mission 11 plays, and
/// cleared before each attempt at a mission (`gameflow.resetVariables`). Only the news report
/// after mission 16 reads it, which never plays.
chapter2_thread3_shown: u32 = 0,
/// `red_dragon_alive` (35): whether the Red Dragon, the Golden Warriors' leader (pilot 133),
/// lives. 1 in a new campaign and not kept with the pilot's game. Mission 26's script brings
/// him in while it is set, and where he dies, sets a global of its own (`red_dragon_dead`).
/// **Unverified:** that it says whether he lives, since no script clears it.
red_dragon_alive: u32 = 0,
/// `yamato_alive` (36): whether the Yamato lives. Mission 25's second part and mission 27
/// clear it as she's destroyed. Once it's clear, those missions end without the landing
/// (`xtrabits.landing.lastWithoutLanding`), and a total failure in them ends the career in the
/// shuttle at Fort Bear.
yamato_alive: u32 = 0,
/// `ion_cannons_hold_lock` (37): while it is set, an ion cannon keeps its lock on its ship
/// whatever would break it (`aiioncan.update`), and a tower in a network game doesn't give up a
/// long search (`0x0040D40F`, `0x0040D7D9`).
ion_cannons_hold_lock: u32 = 0,
/// The rest of the block, which neither the engine nor the shipped missions use.
spare: [26]u32 = @splat(0),
/// Room for every number past the block that a byte names. In the game these are the globals
/// after the block, which no shipped mission touches.
beyond: [count - block_size]u32 = @splat(0),
/// How many variables the block holds, up to the next global.
pub const block_size = 64;
/// How many variables the script can number: every number a byte names.
pub const count = std.math.maxInt(u8) + 1;
/// Variable `index`, as the script numbers them.
pub fn slot(variables: *Variables, index: u8) *u32 {
return &@as(*[count]u32, @ptrCast(variables))[index];
}
/// The number the scripts give the variable `name`.
pub fn number(comptime name: []const u8) u8 {
return @offsetOf(Variables, name) / @sizeOf(u32);
}
/// The numbers the scripts give the variables `names`, in their order.
pub fn numbers(comptime names: anytype) [names.len]u8 {
var found: [names.len]u8 = undefined;
for (names, &found) |name, *at| at.* = number(name);
return found;
}
/// How the script rates the mission, as the game's debug line names each rating: the carrier's
/// clearance to land picks its line by it, and a rating past the named ones is "incorrectly
/// defined".
pub const Outcome = enum(i32) {
/// The name scripts know these values by.
pub const script_name = "Rating";
/// "Total Failure (kick out)".
total_failure = -1,
failure = 0,
partial_failure = 1,
partial_success = 2,
success = 3,
/// "Success + Bonus".
success_bonus = 4,
_,
pub fn format(outcome: Outcome, writer: *std.Io.Writer) std.Io.Writer.Error!void {
return layout.formatTag(Outcome, outcome, writer);
}
};
comptime {
const at = struct {
fn at(name: []const u8, address: u32) void {
assert(@offsetOf(Variables, name) == address - 0x0052A3F0);
}
}.at;
at("backup_available", 0x0052A3FC);
at("player_missiles_left", 0x0052A400);
at("mission_over", 0x0052A414);
at("landing_cleared", 0x0052A418);
at("mission_success", 0x0052A428);
at("players", 0x0052A42C);
at("last_success", 0x0052A45C);
at("objectives_met", 0x0052A460);
at("ghost_alive", 0x0052A468);
at("countdown", 0x0052A474);
at("ion_cannons_hold_lock", 0x0052A484);
at("spare", 0x0052A488);
at("beyond", 0x0052A4F0);
assert(@offsetOf(Variables, "beyond") == block_size * @sizeOf(u32));
assert(@sizeOf(Variables) == count * @sizeOf(u32));
}
};
comptime {
// Each named variable at the number the scripts give it, and every number a byte names.
const numbered = .{
.{ "_unused_2", 2 }, .{ "mcgann_alive", 5 }, .{ "nicolai_petrov_alive", 6 },
.{ "ivan_petrov_alive", 7 }, .{ "kulov_alive", 8 }, .{ "al_rahan_alive", 11 },
.{ "sharif_alive", 12 }, .{ "steiner_alive", 13 }, .{ "_unused_16", 16 },
.{ "krasnaya_alive", 17 }, .{ "rameses_alive", 18 }, .{ "kozah_alive", 19 },
.{ "_unused_20", 20 }, .{ "_unused_21", 21 }, .{ "fixed_gate_alive", 22 },
.{ "warp_gate_alive", 23 }, .{ "_unused_24", 24 }, .{ "_unused_25", 25 },
.{ "_unused_26", 26 }, .{ "czar_alive", 29 }, .{ "ghost_alive", 30 },
.{ "_unused_31", 31 }, .{ "reliant_alive", 32 }, .{ "chapter2_thread3_shown", 34 },
.{ "red_dragon_alive", 35 }, .{ "yamato_alive", 36 },
};
for (numbered) |named| assert(Variables.number(named[0]) == named[1]);
assert(@sizeOf(Variables) == Variables.count * @sizeOf(u32));
}
/// One of the game's variables, by its number, under the name its field in `Variables` has, such as
/// `rameses_alive` for variable 18; `jump_ready` and `warp_ready` are 0 and 1. Any other number of
/// the block is a variable too, without a name. Scripts name the variables a chapter's news reports
/// wait for this way.
pub const GameVariable = @Enum(u8, .nonexhaustive, &variable_names.names, &variable_names.numbers);
const variable_names = names: {
@setEvalBranchQuota(10_000);
const info = @typeInfo(Variables).@"struct";
var names: []const []const u8 = &.{ "jump_ready", "warp_ready" };
var numbers: []const u8 = &.{ 0, 1 };
for (info.field_names, info.field_types) |name, Type| {
if (@sizeOf(Type) != @sizeOf(u32)) continue;
names = names ++ .{name};
numbers = numbers ++ .{Variables.number(name)};
}
var fixed_names: [names.len][]const u8 = undefined;
var fixed_numbers: [numbers.len]u8 = undefined;
@memcpy(&fixed_names, names);
@memcpy(&fixed_numbers, numbers);
break :names .{ .names = fixed_names, .numbers = fixed_numbers };
};
test GameVariable {
try std.testing.expectEqual(Variables.number("rameses_alive"), @backingInt(GameVariable.rameses_alive));
try std.testing.expectEqual(1, @backingInt(GameVariable.warp_ready));
try std.testing.expectEqual(36, @backingInt(GameVariable.yamato_alive));
}
test Variables {
var variables: Variables = .{};
variables.slot(0).* = 1;
variables.slot(3).* = 1;
variables.slot(9).* = 1;
variables.slot(10).* = 1;
variables.slot(14).* = 3;
variables.slot(15).* = 2;
variables.slot(27).* = 4;
variables.slot(28).* = 1;
variables.slot(30).* = 1;
variables.slot(33).* = 110;
variables.slot(37).* = 1;
variables.slot(255).* = 7;
try std.testing.expectEqual(.newly, variables.ready.jump);
try std.testing.expectEqual(1, variables.backup_available);
try std.testing.expectEqual(1, variables.mission_over);
try std.testing.expectEqual(1, variables.landing_cleared);
try std.testing.expectEqual(.success, variables.mission_success);
try std.testing.expectEqual(2, variables.players);
try std.testing.expectEqual(.success_bonus, variables.last_success);
try std.testing.expectEqual(1, variables.objectives_met);
try std.testing.expectEqual(1, variables.ghost_alive);
try std.testing.expectEqual(110, variables.countdown);
try std.testing.expectEqual(1, variables.ion_cannons_hold_lock);
try std.testing.expectEqual(7, variables.beyond[191]);
try std.testing.expectEqual(10, Variables.number("landing_cleared"));
try std.testing.expectEqual(30, Variables.number("ghost_alive"));
}
/// One condition of the catalogue at `condition_descriptors` (`0x004F6698`).
pub const ConditionDescriptor = extern struct {
/// The developers' `TT_*` name, without the prefix.
name: Pointer(u8),
/// **Unknown.** Zero, except `0x400` for the internal `ExplosionShip`.
_unknown_04: u16,
/// The kinds of object whose triggers can have this condition.
subjects: dte.Object.KindSet,
/// The values an event of this condition carries, in order, up to an entry with a null label.
/// Null for none.
values: Pointer(EventValue),
/// Index into `ObjectEvents` under which the matcher keeps each object's last event, for
/// `push_event_value`. `none` for none.
slot: u8,
/// Triggers with this repeat mode fire even when `verdict` vetoes the event; `none` for none.
veto_exempt: dte.Trigger.Repeat,
_unknown_0e: u16,
/// Called before an event on a flight group or squad is counted.
begin: Pointer(anyopaque),
/// Called once for each member of the flight group.
add_member: Pointer(anyopaque),
/// Returns whether the event goes ahead: the value of `condition_verdict`.
verdict: Pointer(anyopaque),
/// What `slot` and `veto_exempt` hold for none.
pub const none: u8 = 0xFF;
comptime {
assert(@offsetOf(ConditionDescriptor, "subjects") == 0x06);
assert(@offsetOf(ConditionDescriptor, "slot") == 0x0C);
assert(@offsetOf(ConditionDescriptor, "begin") == 0x10);
assert(@sizeOf(ConditionDescriptor) == 0x1C);
}
};
/// One value an event carries: an entry of a condition's `values` list.
pub const EventValue = extern struct {
label: Pointer(u8),
kinds: commands.Kinds,
/// **Unknown.** `0xFF`, or `0x09` for the weapon of `ShotAt`.
_unknown_08: u8,
/// Whether a trigger's operand for this value is checked against the event's.
checked: bool,
_unknown_0a: u16,
comptime {
assert(@sizeOf(EventValue) == 0x0C);
}
};
/// The component `push_component` named for a value it pushed. The list at `vm_component_tags`
/// (`0x004F6340`) holds one per such value since the last command, up to a terminating slot of -1.
pub const ComponentTag = extern struct {
/// The stack slot the value is in.
slot: Pointer(u32),
component: u8,
_unknown_05: [3]u8,
/// Tags the list holds, not counting the terminator.
pub const max = 8;
comptime {
assert(@sizeOf(ComponentTag) == 8);
}
};
/// An event waiting in the queue at `event_queue` (`0x0052ABD8`) for `events_flush`
/// (`0x0045B840`), which raises it on the ship and, for `groups`, on its flight group and the
/// squads that hold it.
pub const QueuedEvent = extern struct {
groups: bool,
_unknown_01: [3]u8,
ship: Pointer(dte.Ship),
condition: dte.Condition,
value_count: u8,
_unknown_0a: u16,
/// Room for `max_values`; an event carries at most `max_event_values`.
values: [max_values]u32,
/// The component of the ship the event concerns, or `dte.Trigger.whole_object`.
qualifier: u8,
_unknown_2d: [3]u8,
/// The values a waiting event has room for.
pub const max_values = 8;
comptime {
assert(max_values >= max_event_values);
assert(@offsetOf(QueuedEvent, "ship") == 0x04);
assert(@offsetOf(QueuedEvent, "values") == 0x0C);
assert(@offsetOf(QueuedEvent, "qualifier") == 0x2C);
assert(@sizeOf(QueuedEvent) == 0x30);
}
};
/// The last events of the conditions that have a `slot`, kept for each object at `event_values`
/// (`0x00538CA0`).
pub const ObjectEvents = extern struct {
shot_at: [max_event_values]u32,
destroyed: [max_event_values]u32,
/// The kept events' values one after another, slot after slot, as `push_event_value` numbers
/// them.
pub fn flat(events: *ObjectEvents) *[slots * max_event_values]u32 {
return @ptrCast(events);
}
/// The events kept, one for each `ConditionDescriptor.slot`.
pub const slots = 2;
comptime {
assert(@sizeOf(ObjectEvents) == 0x28);
assert(@sizeOf(ObjectEvents) == slots * max_event_values * @sizeOf(u32));
// Every condition that keeps its events keeps them in one of the slots.
for (conditions.table) |condition| {
if (condition.slot) |slot| assert(slot < slots);
}
}
};
test ObjectEvents {
var events = std.mem.zeroes(ObjectEvents);
events.flat()[ObjectEvents.slots * max_event_values - 1] = 9;
events.flat()[max_event_values] = 7;
try std.testing.expectEqual(7, events.destroyed[0]);
try std.testing.expectEqual(9, events.destroyed[max_event_values - 1]);
try std.testing.expectEqual(@as([max_event_values]u32, @splat(0)), events.shot_at);
}
test {
std.testing.refAllDecls(@This());
}