Repository navigation
Expand file tree
/
Copy pathexecutor.zig
More file actions
3045 lines (2826 loc) · 149 KB
/
Copy pathexecutor.zig
File metadata and controls
3045 lines (2826 loc) · 149 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! `C:\lancer\game\Executor.cpp`: the mission script's commands.
//! [`executor/commands.zig`](executor/commands.zig) transcribes the command catalogue.
//! **Unverified:** the catalogue lies in the data before this file's path, and some commands lie
//! outside this file's code.
const std = @import("std");
const assert = std.debug.assert;
const log = std.log.scoped(.mission);
const dte = @import("../../formats/dte.zig");
const vm = @import("../vm.zig");
const input = @import("../input.zig");
const ai = @import("ai.zig");
const aigeneric = @import("aigeneric.zig");
const camera = @import("camera.zig");
const create = @import("create.zig");
const gameobj = @import("gameobj.zig");
const follow = @import("ai/follow.zig");
const friendly_fire = @import("friendly_fire.zig");
const guns = @import("guns.zig");
const hog_snd = @import("hog_snd.zig");
const hud = @import("hud.zig");
const hudmovie = @import("hudmovie.zig");
const launch = @import("launch.zig");
const radio_module = @import("radio.zig");
const mission = @import("mission.zig");
const objects = @import("objects.zig");
const math = @import("../surrender/math.zig");
const pilots = @import("pilots.zig");
const Order = @import("ai/orders.zig").Order;
pub const commands = @import("executor/commands.zig");
pub const curves = @import("executor/curves.zig");
pub const director = @import("executor/director.zig");
/// What a parameter accepts: a mask of the kinds of value it takes. The bit names are read off
/// the labels of the parameters that set them, in the command catalogue (`commands`), and the
/// arguments the shipped missions pass. The game never reads it: it is data for the developers'
/// tools.
pub const Kinds = packed struct(u32) {
_unknown_0: u7 = 0,
/// `0x80`: a count, an ID, a number of seconds.
number: bool = false,
/// `0x100`: the name of a speech or movie file.
file_name: bool = false,
/// `0x200`: text, or an animation name.
text: bool = false,
/// `0x400`: a ship.
ship: bool = false,
/// `0x800`: a flight group, or a patrol route.
flight_group: bool = false,
/// `0x1000`: a percentage. Only ShotAt's damage values have it: the share of its armour a ship
/// has lost, in hundredths. No parameter has it. The shipped scripts push a number compared
/// with one of them with `push_byte_alt` ([Statements](../../../docs/formats/dte.md#statements)).
percentage: bool = false,
_unknown_13: u1 = 0,
/// `0x4000`: a function, meaning a part.
part: bool = false,
_unknown_15: u4 = 0,
/// `0x80000`: a named constant: a pilot, an AI mode, a text ID.
constant: bool = false,
/// `0x100000`: a squad. Every squad the shipped missions pass a command goes to a parameter
/// with it, and none to one without.
squad: bool = false,
/// `0x200000`: a trigger condition.
condition: bool = false,
/// `0x400000`: a camera or flight curve.
curve: bool = false,
_unknown_23: u9 = 0,
};
/// A parameter's other word in the command catalogue (`commands`), which the game never reads
/// either. What its bits mean is read off the labels and the arguments the shipped missions pass.
pub const Extra = packed struct(u32) {
/// What a file name or a named constant refers to. For a file name, 2 is speech, 4 a movie and
/// 9 music; for a constant, 1 an effect type, 2 a text ID, 3 a pilot and 4 a head movement.
/// **Unverified:** read off the labels.
refers_to: u8 = 0,
_unknown_8: u16 = 0,
/// `0x1000000`: the argument can be null, as the labels that say "can be NULL" have it. All
/// but one of the null arguments in the shipped missions go to a parameter with it: mission 15
/// passes null as `CommsFromShip`'s ship.
nullable: bool = false,
/// `0x2000000`: set only on the parameters of the six commands whose words of section 24 the
/// template's missions leave 0, so that the players' ships in a flight group they walk are
/// passed over (`vm.Machine.forEachShip`). **Unverified:** that the developers' tools built
/// section 24 from it.
skips_players: bool = false,
_unknown_26: u6 = 0,
};
const Call = vm.machine.Call;
const Ship = vm.Machine.Ship;
const run_on = vm.run_on;
const yield = vm.yield;
/// The catalogue's number of the command `name`: the operand of `command` that runs it.
pub fn commandIndex(comptime name: []const u8) u8 {
return comptime for (commands.table, 0..) |entry, index| {
if (std.mem.eql(u8, entry.name, name)) break index;
} else @compileError("the Executor has no command " ++ name);
}
/// The catalogue's number of the command `name`, where it has one, for a name read at run time,
/// such as one in mission source.
pub fn commandNamed(name: []const u8) ?u8 {
return command_numbers.get(name);
}
const command_numbers: std.StaticStringMap(u8) = .initComptime(pairs: {
@setEvalBranchQuota(100_000);
var list: [commands.table.len]struct { []const u8, u8 } = undefined;
for (&list, commands.table, 0..) |*pair, command, index| pair.* = .{ command.name, index };
break :pairs list;
});
/// A command of the catalogue, by its number, under the name scripts know it by: its name in snake
/// case, so that `SetAI` is `set_ai` (`snakeCase`).
pub const MissionCommand = @Enum(u8, .exhaustive, &command_names, &std.simd.iota(u8, command_names.len));
const command_names = names: {
@setEvalBranchQuota(100_000);
var names: [commands.table.len][]const u8 = undefined;
for (commands.table, &names) |entry, *name| name.* = snakeCase(entry.name);
break :names names;
};
/// `name`, in camel case, in snake case: an underscore before each capital that follows a small
/// letter or a digit, or that starts a word after a run of capitals, as `SetEnvironmentFXNebula` is
/// `set_environment_fx_nebula`.
fn snakeCase(comptime name: []const u8) []const u8 {
comptime {
var snake: []const u8 = "";
for (name, 0..) |letter, at| {
if (at > 0 and std.ascii.isUpper(letter)) {
const before = name[at - 1];
const word_starts = std.ascii.isUpper(before) and at + 1 < name.len and std.ascii.isLower(name[at + 1]);
if (std.ascii.isLower(before) or std.ascii.isDigit(before) or word_starts) snake = snake ++ "_";
}
snake = snake ++ .{std.ascii.toLower(letter)};
}
return snake;
}
}
/// The most arguments a command of the catalogue takes.
pub const max_arguments = most: {
var most: usize = 0;
for (commands.table) |entry| most = @max(most, entry.params.len);
break :most most;
};
/// A command's arguments, the first first. Those past the ones it takes are 0.
pub const Arguments = [max_arguments]u32;
/// The implementation of command `number`, or null for the one not ported yet:
/// `ResetToSpawnPositions`, which only the multiplayer arenas' missions use
/// ([#554](https://github.com/OpenReliant/openreliant/issues/554)).
pub fn implementation(number: u8) ?vm.Implementation {
return if (number < implementations.len) implementations[number] else null;
}
/// How the implementations table gives a command: by its implementation, by the work it does in a
/// game (`inGame`), or by the work it does for each ship its first argument names (`perShip`), with
/// the orders it gives meanwhile numbered (`numbered`) or not.
const Entry = union(enum) {
command: vm.Implementation,
in_game: vm.GameImplementation,
per_ship: vm.Machine.ShipImplementation,
numbered: vm.Machine.ShipImplementation,
};
const implementations = table: {
@setEvalBranchQuota(10_000);
var table: [commands.table.len]?vm.Implementation = @splat(null);
for ([_]struct { []const u8, Entry }{
.{ "PrintShipName", .{ .command = printShipName } },
.{ "CreateTimer", .{ .command = vm.Machine.createTimer } },
.{ "DestroyTimer", .{ .command = vm.Machine.destroyTimer } },
.{ "CreateFlightGroup", .{ .in_game = createFlightGroup } },
.{ "Wait", .{ .command = vm.Machine.wait } },
.{ "SetAI", .{ .numbered = setAIShip } },
.{ "InterruptTriggerCode", .{ .command = vm.Machine.interruptTriggerCode } },
.{ "Fly", .{ .per_ship = flyShip } },
.{ "SetPatrolRoute", .{ .per_ship = setPatrolRouteShip } },
.{ "SetPilot", .{ .in_game = setPilot } },
.{ "DisplaySubTitle", .{ .in_game = displaySubTitle } },
.{ "ResetCodePriority", .{ .per_ship = resetCodePriorityShip } },
.{ "SetRescueProbabilities", .{ .in_game = setRescueProbabilities } },
.{ "KillAllScriptExecutionExecptMe", .{ .command = vm.Machine.killAllScriptExecutionExceptMe } },
.{ "PlaySpeech", .{ .in_game = playSpeech } },
.{ "WaitForSpeech", .{ .command = waitForSpeech } },
.{ "PlayCommsMovie", .{ .in_game = playCommsMovie } },
.{ "WaitForMovie", .{ .command = waitForMovie } },
.{ "SetupLaunch", .{ .numbered = setupLaunchShip } },
.{ "StartLaunch", .{ .per_ship = startLaunchShip } },
.{ "SetInvulnerability", .{ .per_ship = setInvulnerabilityShip } },
.{ "PlayMusic", .{ .in_game = playMusic } },
.{ "DisableTaunts", .{ .in_game = remarkFlag("taunts_disabled") } },
.{ "DisableGenericComms", .{ .in_game = remarkFlag("generic_comms_disabled") } },
.{ "UpdateEnvironmentFXState", .{ .in_game = updateEnvironmentFXState } },
.{ "CommsFromShip", .{ .command = comms(.ship, .looping) } },
.{ "CommsFromPilot", .{ .command = comms(.pilot, .looping) } },
.{ "CommsFromShipOnce", .{ .command = comms(.ship, .once) } },
.{ "CommsFromPilotOnce", .{ .command = comms(.pilot, .once) } },
.{ "WaitForJumpOrLaunch", .{ .command = waitForJumpOrLaunch } },
.{ "SetEnvironmentFXNebula", .{ .in_game = setEnvironmentFXNebula } },
.{ "SetEnvironmentFX", .{ .in_game = setEnvironmentFX } },
.{ "OpenInstrument", .{ .in_game = vm.Machine.openInstrument } },
.{ "CloseInstrument", .{ .in_game = vm.Machine.closeInstrument } },
.{ "SetObjective", .{ .in_game = setObjective } },
.{ "SetShipAvoidance", .{ .per_ship = setShipAvoidanceShip } },
.{ "MultiplayerScriptSync", .{ .command = multiplayerScriptSync } },
.{ "SetTriggerState", .{ .command = vm.triggers.setTriggerState } },
.{ "SetAnyTriggerState", .{ .command = vm.triggers.setAnyTriggerState } },
.{ "WhenPlayerLastJumped", .{ .command = whenPlayerLastJumped } },
.{ "DestroyFlightGroup", .{ .in_game = destroyFlightGroup } },
.{ "ClearAI", .{ .per_ship = clearAIShip } },
.{ "StartShipAnimation", .{ .in_game = startShipAnimation } },
.{ "StartShipAnimationReverse", .{ .in_game = startShipAnimationReverse } },
.{ "DisableObject", .{ .per_ship = disableObjectShip } },
.{ "PositionRelative", .{ .per_ship = positionRelativeShip } },
.{ "SetPlayerTarget", .{ .in_game = setPlayerTarget } },
.{ "SetTargetable", .{ .per_ship = setTargetableShip } },
.{ "SetActionCentre", .{ .in_game = setActionCentre } },
.{ "DisableGuns", .{ .per_ship = flagShip("guns_disabled") } },
.{ "DisableEject", .{ .per_ship = flagShip("eject_disabled") } },
.{ "SetHostile", .{ .per_ship = setHostileShip } },
.{ "DoNotDisturb", .{ .per_ship = flagShip("do_not_disturb") } },
.{ "SetEscortPoint", .{ .per_ship = setEscortPointShip } },
.{ "SetNavPoint", .{ .per_ship = setNavPointShip } },
.{ "ResetAfterBurners", .{ .in_game = resetAfterBurners } },
.{ "SetPrimaryTarget", .{ .in_game = setPrimaryTarget } },
.{ "SnapToPoint", .{ .in_game = snapToPoint } },
.{ "IsShipThisPlayer", .{ .command = isShipThisPlayer } },
.{ "SetFlybackMarker", .{ .in_game = setFlybackMarker } },
.{ "ResetFlybackMarker", .{ .in_game = resetFlybackMarker } },
.{ "MatchSpeed", .{ .in_game = matchSpeed } },
.{ "Dock", .{ .in_game = dock } },
.{ "ShipFollowCurve", .{ .per_ship = followCurveShip(.ship_follow_curve, false) } },
.{ "MovingShipFollowCurve", .{ .per_ship = followCurveShip(.ship_follow_curve, true) } },
.{ "MovingShipBackupCurve", .{ .per_ship = followCurveShip(.ship_follow_curve_backwards, true) } },
.{ "StartDirectorCam", .{ .in_game = startDirectorCam } },
.{ "StackDirectorCam", .{ .in_game = stackDirectorCam } },
.{ "StopDirectorCam", .{ .in_game = stopDirectorCam } },
.{ "WaitForDirectorCam", .{ .command = waitForDirectorCam } },
.{ "FriendlyFire", .{ .in_game = friendlyFire } },
.{ "DestroySubObject", .{ .in_game = destroySubObject } },
.{ "ReplaceSubObject", .{ .in_game = replaceSubObject } },
.{ "DisableLights", .{ .per_ship = disableLightsShip } },
.{ "TerminateMission", .{ .in_game = terminateMission } },
.{ "TurretSetTarget", .{ .per_ship = turretSetTargetShip } },
.{ "ReplenishWeapons", .{ .in_game = replenishWeapons } },
.{ "DisableListing", .{ .in_game = disableListing } },
.{ "Scanner", .{ .in_game = scanner } },
.{ "Fire", .{ .in_game = fire } },
.{ "Cloak", .{ .per_ship = cloakShip } },
.{ "PrintDebugMessage", .{ .command = printDebugMessage } },
.{ "StartMissileCam", .{ .command = startMissileCam } },
.{ "StartChaseCam", .{ .command = startChaseCam } },
.{ "WaitForKey", .{ .command = waitForKey } },
.{ "ShowHudIcon", .{ .in_game = showHudIcon } },
.{ "MultiPlayerSync", .{ .command = multiPlayerSync } },
.{ "DisableMissiles", .{ .per_ship = flagShip("missiles_disabled") } },
.{ "DisableEngines", .{ .per_ship = flagShip("engines_disabled") } },
.{ "PlayFostersLastStand", .{ .in_game = playFostersLastStand } },
.{ "StopShipAnimation", .{ .in_game = stopShipAnimation } },
.{ "WillsBlag", .{ .in_game = willsBlag } },
.{ "DisableObjectAtNextJump", .{ .in_game = disableObjectAtNextJump } },
.{ "DarrensNaughtyBlag", .{ .in_game = darrensNaughtyBlag } },
}) |pair| {
const number = commandIndex(pair[0]);
table[number] = switch (pair[1]) {
.command => |run| run,
.in_game => |work| inGame(work),
// A command the table walks the ships for is one the game hands `for_each_ship` a
// routine.
.per_ship => |each| perShip(each),
.numbered => |each| numbered(each),
};
switch (pair[1]) {
.per_ship, .numbered => assert(commands.table[number].per_ship != null),
.command, .in_game => {},
}
}
break :table table;
};
/// A command that does `work` in a game, and nothing outside one, and lets its thread run on: each
/// of the game's commands that always ends so.
fn inGame(comptime work: vm.GameImplementation) vm.Implementation {
return &struct {
fn run(call: Call) u32 {
if (call.machine.game) |game| work(call, game);
return run_on;
}
}.run;
}
/// A command that runs `each` for each ship its first argument names (`for_each_ship`,
/// `vm.Machine.forEachShip`), and lets its thread run on: each of the game's commands that hands
/// `for_each_ship` a routine of its own, its `cmd_<name>_ship`, and does nothing else.
fn perShip(comptime each: vm.Machine.ShipImplementation) vm.Implementation {
return &struct {
fn run(call: Call) u32 {
vm.Machine.forEachShip(call, each);
return run_on;
}
}.run;
}
/// `perShip`, the orders pushed meanwhile numbered from 0 as they are given
/// (`aigeneric.startNumbering`), as `cmd_SetAI` (`0x004581F0`) and `cmd_SetupLaunch`
/// (`0x00458970`) give theirs.
fn numbered(comptime each: vm.Machine.ShipImplementation) vm.Implementation {
return &struct {
fn run(call: Call) u32 {
const game = call.machine.game orelse return run_on;
aigeneric.startNumbering(game.world.objects);
vm.Machine.forEachShip(call, each);
aigeneric.stopNumbering(game.world.objects);
return run_on;
}
}.run;
}
/// `cmd_PrintShipName` (`0x00458AB0`, command `0x00`): the developers' test command, which does
/// nothing and lets the thread run on. Its code is `cmd_ResetCodePriority_ship`'s too.
fn printShipName(_: Call) u32 {
return run_on;
}
/// `cmd_CreateFlightGroup` (`0x00457C40`, command `0x03`): creates each ship of the flight group
/// its argument names, in the mission's order (`createShip`), then lists the flight groups in
/// their wings (`mission.buildWings`).
///
/// **Fix:** the game stops with a fatal error where the argument names no flight group; OpenReliant
/// creates nothing.
fn createFlightGroup(call: Call, game: aigeneric.Context) void {
const machine = call.machine;
const group = machine.mission.flightGroup(machine.mission.flightGroupIndex(call.args[0]) orelse return) orelse return;
for (machine.mission.groupShips(group.*)) |ship| createShip(game, machine.mission, ship);
mission.buildWings(game.world.objects, machine.mission);
}
/// `mission_ship_create` (`0x00457CD0`): creates the object of mission ship `index` in the slot of
/// its index, as its record has it, turned by its record (`mission.recordOrientation`).
///
/// A nav point or a marker (`dte.Ship.isMarker`) is a `gameobj.Type.marker` at its place.
///
/// Any other ship is made as an object of its kind (`shipType`), fitted by its loadout tier, at its
/// place. A planet gets its atmosphere and its turn (`atmosphere.Atmospheres.made`) and is set up as
/// a planet (`create.planetMade`), a gate is set up as a gate (`create.gateMade`), and the Dark
/// Reign gets its hat (`create.extra.hatMade`). Each ship gets its first order: Player Control for
/// the player's ship, whose view the camera takes (view 0), Multiplayer Control for another
/// player's, and Do Nothing for the rest. A ship that launches gets a Launch order, which starts at
/// once, through the gate it names on the first of the mission's ships of the kind it launches from
/// (`aigeneric.objectOrders`). A ship flown by a pilot of `pilotstats.bin` gets the pilot.
///
/// The game also counts the nav points it makes (`nav_point_count`, `0x00565698`), which nothing
/// reads, and logs lines; OpenReliant leaves both out.
pub fn createShip(game: aigeneric.Context, bound: *const mission.Mission, index: u16) void {
const all = game.world.objects;
const spawn = game.world.spawn orelse return;
const ships = bound.ships() catch return;
if (index >= ships.len) return;
const ship = ships[index];
const marker = ship.isMarker();
const kind: gameobj.Type, const tier: u8 = if (marker) .{ .of(.marker), 0 } else .{ shipType(all, bound, ship), ship.tier };
const made = create.createObject(all, spawn.tables, spawn.types, index, kind, tier, ship.position, game.world.random) catch |err| {
log.warn("mission ship {d} is not made: {s}", .{ index, @errorName(err) });
return;
};
const slot = &all.slots[made];
const turn = mission.recordOrientation(ship);
if (marker) return objects.setOrientation(&slot.object, &slot.drawn, turn);
if (game.world.atmospheres) |atmospheres| atmospheres.made(all, made);
create.planetMade(all, made);
create.gateMade(game.world, made);
create.extra.hatMade(game.world, made);
const first: Order = if (made >= all.players) .do_nothing else if (made == all.player) .player_control else .multiplayer_control;
_ = aigeneric.give(game, made, first, .none);
if (made == all.player) if (game.world.camera) |view| {
_ = view.setView(.cockpit, made, false, false, game.world.clock.viewTime());
};
objects.setOrientation(&slot.object, &slot.drawn, turn);
if (ship.launchGate()) |gate| for (ships, 0..) |carrier, from| {
if (carrier.kind != ship.launch_from) continue;
_ = aigeneric.giveShip(game, made, .launch, @intCast(from), gate);
aigeneric.objectOrders(game, made);
break;
};
if (ship.pilotRecord()) |pilot| pilots.setPilot(&slot.object, pilot);
}
/// The type `mission_ship_create` asks for a mission ship of: its kind, save that a ship of the
/// player's wing flies the `t_` twin of its kind in a mission whose rules say so (`wing_twins`,
/// from mission 14 in the original), or a Kamov in mission 25's first part
/// (`create.Objects.kamovPart`).
///
/// **Fix:** in those missions, the game takes a ship of the player's wing whose kind is none of
/// the player's ships for the object in the slot its record's address gives, and reads the wing of
/// a ship in no flight group from past the groups; OpenReliant makes the first of its own kind, and
/// takes the second for a ship of no wing.
fn shipType(all: *const create.Objects, bound: *const mission.Mission, ship: dte.Ship) gameobj.Type {
const kind: gameobj.Type = @fromBackingInt(ship.kind);
if (!all.rules().wing_twins) return kind;
const group = bound.flightGroup(ship.flightGroup() orelse return kind) orelse return kind;
if (group.wing != .player) return kind;
if (all.kamovPart()) return .of(.kamov);
return kind.twin() orelse kind;
}
/// `cmd_SetAI` (`0x004581F0`, command `0x0B`) and `cmd_SetAI_ship` (`0x00458220`), for each ship
/// the first argument names, the orders numbered from 0 as they are given (`numbered`): pushes the
/// order the command's second argument gives on the ship's stack, aimed at what its fourth names: a
/// flight group or a squad by its index, or a ship by its slot and the component `push_component`
/// named for it, or nothing. The third, whether the order starts at once, is not read.
fn setAIShip(call: Call, ship: Ship) void {
const machine = call.machine;
const aim = call.args[2];
const target = recordTarget(machine, aim, shipTarget(machine, call.thread, aim, 3));
const order: Order = @fromBackingInt(halfword(call.args[0]));
_ = aigeneric.give(ship.game, ship.index, order, target);
}
/// The dispatch on `record_kind` that `SetAI`, `SetupLaunch` and `Dock` share for what `place`
/// names (`cmd_SetAI_ship`, `cmd_SetupLaunch_ship`, `cmd_Dock`): a flight group or a squad by its
/// index, whole, and `ship` for a ship's record or for a place that is none of the three, which
/// `record_kind` gives as `0xFFFF` and the commands take as a ship's.
fn recordTarget(machine: *const vm.Machine, place: u32, ship: aigeneric.Target) aigeneric.Target {
return switch (machine.mission.recordKind(place) orelse .ship) {
.flight_group => .group(.flight_group, machine.mission.flightGroupIndex(place)),
.squad => .group(.squad, machine.mission.squadIndex(place)),
.ship => ship,
};
}
/// The target a command aims at the ship `aim` names: its slot, and the component
/// `push_component` named for the command's argument `argument`, or none where `aim` names no
/// ship.
fn shipTarget(machine: *const vm.Machine, thread: u8, aim: u32, argument: u8) aigeneric.Target {
const ship = machine.mission.shipIndex(aim) orelse return .none;
return .of(.ship, ship, machine.argumentComponent(thread, argument) orelse aigeneric.Target.whole);
}
/// Gives the ship `order` aimed at `target` (`aigeneric.give`), and the entry on top of its stack
/// that took it, which the command then fills in; null where the ship does not take it.
fn given(ship: Ship, order: Order, target: aigeneric.Target) ?*aigeneric.Entry {
if (!aigeneric.give(ship.game, ship.index, order, target)) return null;
return ship.slot.current();
}
/// `cmd_Fly` (`0x00458F50`, command `0x28`) and `cmd_Fly_ship` (`0x00458F70`), for each ship the
/// first argument names (`perShip`): pushes a Fly order on the ship's stack, aimed at the ship the
/// command's second argument names, its slot the halfword the game stores whatever the argument
/// is, or at none, which holds the heading it starts on, at the speed the third gives, or 0 for its
/// full throttle.
///
/// **Fix:** the game writes the speed into whatever order the ship has on top where it refuses
/// Fly: Player Control's mouse stick on the player's ship, which then turns it, or Explode's
/// `may_spin` on an exploding one; OpenReliant writes none.
fn flyShip(call: Call, ship: Ship) void {
const target: aigeneric.Target = .of(.ship, call.machine.mission.shipIndex(call.args[0]), aigeneric.Target.whole);
const entry = given(ship, .fly, target) orelse return;
entry.data.fly = @bitCast(call.args[1]);
}
/// `cmd_SetPatrolRoute` (`0x00458860`, command `0x0D`) and `cmd_SetPatrolRoute_ship`
/// (`0x00458880`), for each ship the first argument names (`perShip`): the ship takes the Patrol
/// Route order, aimed at the first waypoint of the flight group the command's second argument names
/// (`bind.Mission.firstWaypoint`), as a ship's target holds it, where the group has any. The order
/// flies the route ([`ai/formations.zig`](ai/formations.zig)).
fn setPatrolRouteShip(call: Call, ship: Ship) void {
const bound = call.machine.mission;
const route = bound.flightGroupIndex(call.args[0]) orelse return;
const first = bound.firstWaypoint(route) orelse return;
_ = aigeneric.giveShip(ship.game, ship.index, .patrol_route, first, null);
}
/// `cmd_SetPilot` (`0x00458830`, command `0x0E`): the ship the first argument names is flown by
/// the pilot the second numbers, a record of `pilotstats.bin` (`pilots.setPilot`).
///
/// **Fix:** where the first argument names no ship, the game gives the pilot to an object past the
/// objects' table; OpenReliant gives it to none.
fn setPilot(call: Call, game: aigeneric.Context) void {
const all = game.world.objects;
const ship = call.argumentShip(all, 0) orelse return;
pilots.setPilot(&all.slots[ship].object, @bitCast(call.args[1]));
}
/// `cmd_DisplaySubTitle` (`0x00458A80`, command `0x15`): the display shows the language string the
/// argument numbers as its subtitle, or none for `hud.Subtitle.none` (`hud.Subtitle.show`).
fn displaySubTitle(call: Call, game: aigeneric.Context) void {
const display = game.world.display orelse return;
display.subtitle.show(call.args[0]);
}
/// `cmd_ResetCodePriority` (`0x00458A90`, command `0x16`) and `cmd_ResetCodePriority_ship`
/// (`0x00458AB0`), for each ship the argument names (`perShip`): the walk does what every such
/// command's does (`vm.Machine.forEachShip`), and the routine does nothing for each ship. Whatever
/// its description says, the orders' priorities stay as they are.
fn resetCodePriorityShip(_: Call, _: Ship) void {}
/// `cmd_SetRescueProbabilities` (`0x004598D0`, command `0x44`): the odds of how the player fares
/// after ejecting: picked up by a nanny ship, by the enemy, and killed, each the whole signed
/// number the mission gives.
fn setRescueProbabilities(call: Call, game: aigeneric.Context) void {
game.world.player.rescue_odds = .{
.rescued = @bitCast(call.args[0]),
.captured = @bitCast(call.args[1]),
.killed = @bitCast(call.args[2]),
};
}
/// `cmd_PlaySpeech` (`0x00458090`, command `0x06`): the speech file the argument names plays at
/// once, without the radio's window or a film, ending the line playing
/// (`radio.Radio.playSpeech`).
fn playSpeech(call: Call, game: aigeneric.Context) void {
const radio, const ctx = radio_module.onAir(game.world) orelse return;
const name = argumentText(call, 0) orelse return;
radio.playSpeech(ctx.sound, name);
}
/// `cmd_WaitForSpeech` (`0x00458100`, command `0x07`): the thread waits while a line plays
/// (`radio.Radio.speaking`), running the command again each time
/// (`vm.machine.Call.againItself`).
fn waitForSpeech(call: Call) u32 {
const game = call.machine.game orelse return run_on;
const radio, const ctx = radio_module.onAir(game.world) orelse return run_on;
return if (radio.speaking(ctx.sound)) call.againItself() else run_on;
}
/// The text the command's argument `index` points at in the mission's file (`push_string`), up to
/// its terminating zero; null where it runs outside the file, which is logged the first time in a
/// mission.
///
/// **Fix:** the game reads the text wherever the argument points, past its copy of the mission
/// too.
fn argumentText(call: Call, index: usize) ?[]const u8 {
const at = call.args[index];
return call.machine.mission.text(at) catch |err| {
if (call.machine.firstTime(.text_outside)) log.warn("a script command's text at 0x{X:0>8} runs outside the mission's file, so the command does nothing: {s}", .{ at, @errorName(err) });
return null;
};
}
/// The room the game gives the speech file's name and the film's path (`cmd_PlayCommsMovie`'s
/// buffers, `0x00458120`).
const comms_movie_path_size = 52;
/// `cmd_PlayCommsMovie` (`0x00458120`, command `0x08`): the film the first argument names,
/// `pilots\<film>`, plays in the radio's window with the speech file the second names, at once,
/// looping, under the string the third numbers, the line nobody's. The game also sets a halfword
/// nothing reads (`0x005373EA`).
///
/// **Fix:** the game writes a name longer than its buffer past it; OpenReliant plays nothing, and
/// logs it once.
fn playCommsMovie(call: Call, game: aigeneric.Context) void {
const machine = call.machine;
const radio, const ctx = radio_module.onAir(game.world) orelse return;
const film = argumentText(call, 0) orelse return;
const speech = argumentText(call, 1) orelse return;
var buffer: [comms_movie_path_size]u8 = undefined;
const path = fitted: {
const printed = std.mem.print(&buffer, "pilots\\{s}", .{film}) catch break :fitted null;
break :fitted if (printed.len < comms_movie_path_size and speech.len < comms_movie_path_size) printed else null;
} orelse {
if (machine.firstTime(.long_name)) log.warn("PlayCommsMovie leaves out the film {s} and the speech {s}: a name is too long for the game's {d}-byte buffers", .{ film, speech, comms_movie_path_size });
return;
};
radio.say(ctx, .{ .film = path, .speech = speech, .name = @truncate(call.args[2]), .flags = .looping, .object = radio_module.nobody }, .now);
}
/// `cmd_WaitForMovie` (`0x00458180`, command `0x09`): the thread waits while a film of the radio's
/// plays (`hudmovie.Movie.playing`), running the command again each time
/// (`vm.machine.Call.againItself`).
fn waitForMovie(call: Call) u32 {
const game = call.machine.game orelse return run_on;
const radio = game.world.radio orelse return run_on;
return if (radio.movie.playing) call.againItself() else run_on;
}
/// `cmd_PrintDebugMessage` (`0x004581A0`, command `0x0A`): the game writes `DEBUG: ` and the text
/// the argument names into a line to show for half a second (`0x00536D58`), which nothing in its
/// retail build draws, and passes it to its debug log (`debug_print`), which the retail build
/// leaves empty. Missions 11 and 24 print their start and lines of speech so.
///
/// **Improvement:** OpenReliant writes the line to its log, where a mission's maker can read it.
fn printDebugMessage(call: Call) u32 {
if (argumentText(call, 0)) |text| log.info("DEBUG: {s}", .{text});
return run_on;
}
/// `cmd_SetupLaunch` (`0x00458970`, command `0x13`) and `cmd_SetupLaunch_ship` (`0x004589A0`), for
/// each ship the first argument names, the orders numbered from 0 as they are given (`numbered`),
/// which a launch from a flight group or a squad counts its launch points by (`launch.init`):
/// pushes a Launch order on the ship's stack, aimed at what the command's second argument names it
/// to launch from: a flight group or a squad by its index, whose ships' launch points the launch
/// searches for a gate, or a ship by its slot, through the gate the third argument gives, counted
/// on by one for each ship the walk reached before this one (`launchGate`).
fn setupLaunchShip(call: Call, ship: Ship) void {
const machine = call.machine;
const from = call.args[0];
const target = recordTarget(machine, from, launchGate(machine, from, call.args[1]));
_ = aigeneric.give(ship.game, ship.index, .launch, target);
}
/// The target `SetupLaunch` aims a ship at to launch from the ship `from` names: through `gate`,
/// the low half of the command's argument, and on by one for each ship its walk reached before
/// this one (`vm.Machine.walk_count`), wrapping round as a halfword does.
fn launchGate(machine: *const vm.Machine, from: u32, gate: u32) aigeneric.Target {
const counted = @as(u16, @truncate(gate)) +% machine.walk_count -% 1;
return .of(.ship, machine.mission.shipIndex(from), @bitCast(counted));
}
/// `cmd_StartLaunch` (`0x00458A40`, command `0x14`) and `cmd_StartLaunch_ship` (`0x00458A60`), for
/// each ship the argument names (`perShip`): the ship starts its launch (`launch.start`).
fn startLaunchShip(call: Call, ship: Ship) void {
_ = call;
launch.start(ship.game.world.objects, ship.index);
}
/// `cmd_SetInvulnerability` (`0x00458BC0`, command `0x1A`) and `cmd_SetInvulnerability_ship`
/// (`0x00458BE0`), for each ship the first argument names (`perShip`): the ship takes the
/// invulnerability the command's second argument gives, or where the first names one of its
/// components (`push_component`), that component does (`gameobj.Component.protects`).
/// A ship in the players' slots is reached only in training (`create.Objects.training`): in the
/// training missions, and in the Reliant's simulator's training (`simulator_mode` 1).
fn setInvulnerabilityShip(call: Call, ship: Ship) void {
const machine = call.machine;
const all = ship.game.world.objects;
if (ship.index < all.players and !all.training()) return;
const object = &ship.slot.object;
const value = call.args[0];
if (machine.argumentComponent(call.thread, 0)) |component| {
if (component < object.components.len) object.components[component].invulnerable = @truncate(value);
return;
}
object.invulnerable = @fromBackingInt(@as(u8, @truncate(value)));
}
/// `cmd_Cloak_ship` (`0x00459F60`), called by `cmd_Cloak` (`0x00459F40`, command `0x58`):
/// sets the ship's cloak from the remaining argument. The shared setter checks the model's
/// cloak capability and propagates the change to ships launching from it.
fn cloakShip(call: Call, ship: Ship) void {
@import("cloak.zig").set(ship.game.world, ship.index, call.args[0] != 0);
}
test "Cloak uses the existing setter and its command catalogue entry" {
const cloak = @import("cloak.zig");
var stage: cloak.testing.Cloaked = undefined;
try stage.init(std.testing.allocator);
defer stage.deinit(std.testing.allocator);
var args = [_]u32{1};
var machine: vm.Machine = undefined;
const ctx = stage.mission.orders();
const call: Call = .{ .machine = &machine, .thread = 0, .args = &args };
cloakShip(call, .{ .game = ctx, .index = stage.index, .slot = stage.slot() });
try std.testing.expect(stage.slot().object.flags.cloaked);
try std.testing.expect(implementation(commandIndex("Cloak")) != null);
}
/// An eject roll no pilot's is below (`ai.eject_below`), past the rolls a ship is made with, 0 to
/// 99 (`0x0045A1D9`).
const never_ejects = 100;
/// `cmd_WillsBlag` (`0x0045A1C0`, command `0x5A`): the ship the argument names is alive again to
/// the mission: its record is no longer destroyed (`dte.Ship.Flags.destroyed`), so that its
/// Destroyed event can come again, its pilot no longer ejected, and it never ejects
/// (`never_ejects`). Mission 28 brings Steiner back so before making him invulnerable.
///
/// **Fix:** where the argument names no ship, the game reads past the objects' table; OpenReliant
/// does nothing.
fn willsBlag(call: Call, game: aigeneric.Context) void {
const machine = call.machine;
const all = game.world.objects;
const ship = call.argumentShip(all, 0) orelse return;
if (machine.mission.ship(ship)) |record| record.flags.destroyed = false;
const object = &all.slots[ship].object;
object.eject_roll = never_ejects;
object.flags.ejected = false;
}
/// `cmd_DisableObjectAtNextJump` (`0x0045A250`, command `0x5D`): asks for the object of the ship the
/// first argument names, such as a planet, to be disabled at the next jump or warp while the
/// second is set, and enabled again while it is not (`environfx.Environment.next_jump`). Mission 11
/// disables Saturn at once and has it back at the next jump so.
///
/// **Fix:** where the first argument names no ship, the game reads past the objects' table;
/// OpenReliant does nothing.
fn disableObjectAtNextJump(call: Call, game: aigeneric.Context) void {
const environment = game.world.environment orelse return;
const ship = call.argumentShip(game.world.objects, 0) orelse return;
environment.next_jump[ship] = if (call.args[1] != 0) .disable else .enable;
}
/// `cmd_DarrensNaughtyBlag` (`0x0045A290`, command `0x5E`): the ship the first argument names turns
/// to face the ship the second names, from where each stands next, with no roll
/// (`math.lookAt`, `objects.setOrientation`). Mission 27 turns a ship it has snapped to a point
/// toward the Yamato so.
///
/// **Fix:** where an argument names no ship, the game reads past the objects' table; OpenReliant
/// does nothing.
fn darrensNaughtyBlag(call: Call, game: aigeneric.Context) void {
const all = game.world.objects;
const turned = call.argumentShip(all, 0) orelse return;
const faced = call.argumentShip(all, 1) orelse return;
const slot = &all.slots[turned];
const from = slot.object.root.next_position.vector();
const to = all.slots[faced].object.root.next_position.vector();
objects.setOrientation(&slot.object, &slot.drawn, math.lookAt(to - from));
}
/// How loud a mission's music plays (`cmd_PlayMusic`, `0x00458E16`).
const music_level = 80;
/// The loop count of a mission's music: 0, which loops forever (`cmd_PlayMusic`, `0x00458E18`).
pub const music_forever = 0;
/// The room the game gives a piece's path (`cmd_PlayMusic`'s buffer, `0x00458DF0`).
const music_path_size = 128;
/// The folder the pieces are in (`cmd_PlayMusic`'s format, `0x004F5C14`).
const music_folder = "music\\";
/// `cmd_PlayMusic` (`0x00458DF0`, command `0x23`): plays the piece named by the first argument from
/// the game's music folder, looping forever. It starts at once if the second argument is 1, and
/// otherwise after the current music fades out (`hog_snd.Sound.When.of`, `playPiece`).
fn playMusic(call: Call, game: aigeneric.Context) void {
const hearing = game.world.hearing orelse return;
const name = argumentText(call, 0) orelse return;
playPiece(hearing.sound, name, music_forever, .of(call.args[1], game.world.clock.game_ticks));
}
/// Plays the piece `name` from the game's music folder `loops` times (0 loops forever), as
/// `cmd_PlayMusic` does, starting when `when` says.
///
/// **Fix:** the game writes a path longer than its buffer past it; OpenReliant plays nothing.
pub fn playPiece(sound: *hog_snd.Sound, name: []const u8, loops: u32, when: hog_snd.Sound.When) void {
var buffer: [music_path_size]u8 = undefined;
const path = std.mem.print(&buffer, music_folder ++ "{s}", .{name}) catch {
log.warn("the music {s} is left out: its path is too long", .{name});
return;
};
sound.playMusic(path, loops, music_level, when);
}
/// Who a comms command's first argument names to speak: a mission's ship, or a pilot of the
/// pilots' table.
const Speaker = enum { ship, pilot };
/// The radio's comms commands, each ending the frame's handlers (`vm.yield`):
///
/// | Command | Speaker | Film |
/// |---|---|---|
/// | `cmd_CommsFromShip` (`0x00458AC0`, command `0x18`) | `ship` | `looping` |
/// | `cmd_CommsFromPilot` (`0x00458B10`, command `0x19`) | `pilot` | `looping` |
/// | `cmd_CommsFromShipOnce` (`0x00458FD0`, command `0x29`) | `ship` | `once` |
/// | `cmd_CommsFromPilotOnce` (`0x00459020`, command `0x2A`) | `pilot` | `once` |
///
/// The ship the first argument names (`radio.Radio.sayShip`), or the pilot it numbers
/// (`radio.Radio.sayPilot`), says the speech file the third names at once, its face moving
/// as the second says, the film looping while the line plays, or playing once
/// (`hudmovie.Flags.once`).
fn comms(comptime speaker: Speaker, comptime flags: hudmovie.Flags) vm.Implementation {
return &struct {
fn run(call: Call) u32 {
const machine = call.machine;
const game = machine.game orelse return yield;
const radio, const ctx = radio_module.onAir(game.world) orelse return yield;
const head: pilots.Head = @fromBackingInt(call.args[1]);
const name = argumentText(call, 2) orelse return yield;
switch (speaker) {
.ship => {
const ship = call.argumentShip(ctx.all, 0) orelse return yield;
radio.sayShip(ctx, ship, head, name, .now, flags, radio_module.no_expiry);
},
.pilot => radio.sayPilot(ctx, @truncate(call.args[0]), head, name, .now, flags, radio_module.no_expiry),
}
return yield;
}
}.run;
}
/// A command's argument read as the signed halfword the game stores it as (`(short)`).
fn halfword(argument: u32) i16 {
return @bitCast(@as(u16, @truncate(argument)));
}
/// A command's argument read as the halfword the game stores it as, set or not.
fn halfwordSet(argument: u32) bool {
return halfword(argument) != 0;
}
/// `cmd_FriendlyFire` (`0x00459F30`, command `0x57`): the player's ship is to be sent home as for
/// destroying a friend (`friendly_fire.friendDestroyed`).
fn friendlyFire(_: Call, game: aigeneric.Context) void {
friendly_fire.friendDestroyed(game.world);
}
/// The commands that set a flag of the radio's remarks (`radio.Remarks`) while the argument,
/// read as a halfword, is set, and clear it while it is not (`halfwordSet`):
///
/// | Command | Flag |
/// |---|---|
/// | `cmd_DisableTaunts` (`0x00458F40`, command `0x27`) | `taunts_disabled`: the enemy's taunts on the radio stop |
/// | `cmd_DisableGenericComms` (`0x004591F0`, command `0x2E`) | `generic_comms_disabled`: the remarks the radio makes by itself stop |
fn remarkFlag(comptime flag: []const u8) vm.GameImplementation {
return &struct {
fn set(call: Call, game: aigeneric.Context) void {
@field(game.world.player.remarks, flag) = halfwordSet(call.args[0]);
}
}.set;
}
/// `cmd_UpdateEnvironmentFXState` (`0x004591A0`, command `0x38`): what the script asks of its
/// space takes effect at once, rather than at the next jump (`environfx.Environment.update`), and
/// the mission's markers aim the sun, the lights and the nebula again
/// (`backdrop.Backdrop.place`).
fn updateEnvironmentFXState(_: Call, game: aigeneric.Context) void {
const world = game.world;
const environment = world.environment orelse return;
environment.update(world.objects);
const ships = if (world.mission) |bound| bound.shipCount() else 0;
environment.space.place(environment.sky, world.objects, ships);
}
/// `cmd_SetEnvironmentFXNebula` (`0x00459190`, command `0x3C`): asks for the nebula the argument
/// numbers (`environfx.Environment.requested`), which shows once the space is updated.
fn setEnvironmentFXNebula(call: Call, game: aigeneric.Context) void {
if (game.world.environment) |environment| environment.requested = call.args[0];
}
/// `cmd_SetEnvironmentFX` (`0x00459170`, command `0x2C`): turns the environment effect the first
/// argument numbers on while the second is set, and off while it is not
/// (`environfx.Environment.setEffect`).
fn setEnvironmentFX(call: Call, game: aigeneric.Context) void {
if (game.world.environment) |environment| environment.setEffect(call.args[0], call.args[1] != 0);
}
/// How far back `WaitForJumpOrLaunch` (`0x004595C6`) and `WaitForKey` (`0x00459BA7`) run again:
/// over the command (`vm.machine.command_size`) and the push of its argument before it, which then
/// pushes it afresh.
const wait_back = 4;
/// `cmd_WaitForJumpOrLaunch` (`0x004595A0`, command `0x3A`): the thread waits while any ship the
/// argument names is still jumping or launching (`jumpingOrLaunching`), pushing the argument again
/// and running the command again each time.
fn waitForJumpOrLaunch(call: Call) u32 {
const machine = call.machine;
machine.still_moving = false;
vm.Machine.forEachShip(call, jumpingOrLaunching);
if (machine.still_moving) return call.again(wait_back);
return run_on;
}
/// `cmd_WaitForJumpOrLaunch_ship` (`0x004595E0`): the ship is still jumping or launching where it
/// is one the AI's searches reach (`gameobj.GameObject.Flags.outOfSearch`) and its current order
/// is one by which a ship jumps, warps or launches.
fn jumpingOrLaunching(call: Call, ship: Ship) void {
if (ship.slot.object.flags.outOfSearch()) return;
const entry = ship.slot.current() orelse return;
switch (entry.order) {
.jump_in, .jump_out, .warp_in, .warp_out, .fixed_gate_jump_in, .fixed_gate_jump_out, .jump_in_spread, .jump_out_spread, .launch => call.machine.still_moving = true,
else => {},
}
}
/// `cmd_SetObjective` (`0x00459870`, command `0x43`): the mission's objective the first argument
/// numbers takes the state the second gives (`hud.Objectives.set`).
fn setObjective(call: Call, game: aigeneric.Context) void {
const display = game.world.display orelse return;
_ = display.objectives.set(call.args[0], @fromBackingInt(halfword(call.args[1])));
}
/// `cmd_SetShipAvoidance` (`0x00459A30`, command `0x49`) and `cmd_SetShipAvoidance_ship`
/// (`0x00459A50`), for each ship the first argument names (`perShip`): the ship, unless a
/// stand-in, keeps clear of others no more where the command's second argument is set, and does
/// again where it is not (`gameobj.GameObject.Flags.no_avoidance`).
fn setShipAvoidanceShip(call: Call, ship: Ship) void {
const object = &ship.slot.object;
if (object.type.base() == .stand_in) return;
object.flags.no_avoidance = call.args[0] != 0;
}
/// `cmd_StartMissileCam` (`0x00458B60`, command `0x1F`): the camera switches to the missile view,
/// locked and forced, following the next missile in flight that the ship the argument names
/// launched (`camera.Camera.setView`), and stays as it is where there's none. The thread then
/// yields until it runs next. Mission 28 has the camera follow a torpedo of the Yamato's so.
///
/// **Fix:** where the argument names no ship, the game reads past the objects' table; OpenReliant
/// does nothing.
fn startMissileCam(call: Call) u32 {
const game = call.machine.game orelse return yield;
const view = game.world.camera orelse return yield;
const ship = call.argumentShip(game.world.objects, 0) orelse return yield;
_ = view.setView(.missile, ship, true, true, game.world.clock.viewTime());
return yield;
}
/// `cmd_StartChaseCam` (`0x00458B80`, command `0x20`): the camera follows the ship the argument
/// names in the chase view, locked and forced (`camera.Camera.setView`), or goes back to the
/// player's cockpit, forced, where it names none. The thread then yields until it runs next.
///
/// **Fix:** where the argument names a ship past the objects' slots, the game follows an object
/// past their table; OpenReliant goes back to the cockpit, as for none.
fn startChaseCam(call: Call) u32 {
const game = call.machine.game orelse return yield;
const view = game.world.camera orelse return yield;
const all = game.world.objects;
const now = game.world.clock.viewTime();
if (call.argumentShip(all, 0)) |ship| {
_ = view.setView(.chase, ship, true, true, now);
} else {
_ = view.setView(.cockpit, all.player, false, true, now);
}
return yield;
}
/// `cmd_WaitForKey` (`0x00459AE0`, command `0x4C`): the thread waits until the player holds down
/// the key of the action the argument numbers, with its modifier (`input.Keyboard.heldWith`), or
/// its joystick button, running the command again each time with its argument pushed afresh
/// (`wait_back`). Meanwhile the display prompts for the action (`hud.key_prompt`).
///
/// **Fix:** the game reads the bindings past their table for a number past the actions;
/// OpenReliant lets the thread run on.
fn waitForKey(call: Call) u32 {
const game = call.machine.game orelse return run_on;
const action = std.enums.fromInt(input.controls.Action, call.args[0]) orelse return run_on;
const held = if (game.devices) |devices| bindingHeld(devices, devices.bindings.get(action)) else false;
if (game.world.display) |display| display.key_prompt.action = if (held) null else action;
return if (held) run_on else call.again(wait_back);
}
/// Whether the player holds down `binding` as `WaitForKey` asks: its key with its modifier, or its
/// joystick button.
fn bindingHeld(devices: *const input.Devices, binding: input.controls.Binding) bool {
if (std.math.cast(u8, binding.key)) |key| {
if (devices.keyboard.heldWith(key, binding.modifier)) return true;
}
const button = binding.button orelse return false;
return devices.joystick.down(button);
}
/// `cmd_ShowHudIcon` (`0x0045A1F0`, command `0x5B`): sets the display's icon the first argument
/// numbers off, on or flashing, as the second says, its flash from the start (`hud.Icons.show`).
fn showHudIcon(call: Call, game: aigeneric.Context) void {
const display = game.world.display orelse return;
display.icons.show(@fromBackingInt(call.args[0]), @fromBackingInt(call.args[1]));
}
/// `cmd_MultiPlayerSync` (`0x004591E0`, command `0x2D`): the thread yields until it runs next. In a
/// network game, the game first marks the local player's script as waiting for the others
/// (`0x004B58D0`), which does nothing in single player.
///
/// Not ported: a multiplayer game's players' scripts kept in step
/// ([#55](https://github.com/OpenReliant/openreliant/issues/55)).
fn multiPlayerSync(call: Call) u32 {
_ = call;
return yield;
}
/// `cmd_MultiplayerScriptSync` (`0x00459DF0`, command `0x56`): in a single-player game, the thread
/// runs on at once.
///
/// Not ported: a multiplayer game's players' scripts kept in step
/// ([#55](https://github.com/OpenReliant/openreliant/issues/55)).
fn multiplayerScriptSync(call: Call) u32 {
_ = call;
return run_on;
}
/// `vm_clock_tick` (`0x00458910`): a second of the mission has passed, the script's clock on by
/// one (`vm.Machine.clock`), and the timers to run for it (`vm.Machine.ticked`). The game's timer
/// calls it once a second, and it returns at once while the script debugger holds the clock
/// (`0x005373F8`) or the game is paused (`paused`, `0x0057E04C`); OpenReliant's caller holds it
/// there (`mission.Loaded.tickClock`).
pub fn clockTick(machine: *vm.Machine) void {
machine.ticked = true;
machine.clock +%= 1;
}
/// `cmd_WhenPlayerLastJumped` (`0x00458580`, command `0x1E`): how many seconds of the script's
/// clock ago JUMP DRIVE last took a jump or a warp (`vm.Machine.last_jumped`), at least 1; and
/// `never_jumped` while the clock stands before it, as it does before the first.
fn whenPlayerLastJumped(call: Call) u32 {
const machine = call.machine;
const ago: i32 = @bitCast(machine.clock -% machine.last_jumped);
if (ago < 0) return vm.Machine.never_jumped;
return @max(@as(u32, @intCast(ago)), 1);
}
/// `cmd_DestroyFlightGroup` (`0x00457FD0`, command `0x04`): each ship of the flight group the
/// argument names leaves the mission at once, a stand-in in its place (`create.retire`), a
/// planet's atmosphere let go of with it (`create.atmosphere.Atmospheres.release`).
fn destroyFlightGroup(call: Call, game: aigeneric.Context) void {
const machine = call.machine;
const group = machine.mission.flightGroup(machine.mission.flightGroupIndex(call.args[0]) orelse return) orelse return;
const all = game.world.objects;