Repository navigation
Expand file tree
/
Copy pathinput.zig
More file actions
3370 lines (3114 loc) · 155 KB
/
Copy pathinput.zig
File metadata and controls
3370 lines (3114 loc) · 155 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
//! Player input as the game reads it: DirectInput's device states, the control bindings, and the
//! setting that picks the device the player steers with. [`input/controls.zig`](input/controls.zig)
//! transcribes the actions and their default bindings. **Unknown:** the source files. The device
//! code lies between `DPSession.cpp`'s and `srAPI.cpp`'s, the player's controls between
//! `airipper.cpp`'s and `jump.cpp`'s.
const std = @import("std");
const log = std.log.scoped(.input);
const assert = std.debug.assert;
const layout = @import("../formats/layout.zig");
const profile = @import("profile.zig");
const language = @import("game/language.zig");
pub const controls = @import("input/controls.zig");
pub const force = @import("input/force.zig");
pub const power = @import("input/power.zig");
/// DirectInput's `DIJOYSTATE`, which the game polls the joystick into at `joystick` each simulation
/// step. The game sets the axes to run from -1000 to 1000.
pub const JoystickState = extern struct {
x: i32,
y: i32,
z: i32,
rx: i32,
ry: i32,
rz: i32,
sliders: [2]i32,
pov: [max_hats]u32,
/// Nonzero while the button is down.
buttons: [max_buttons]u8,
/// The most hats and buttons it holds.
pub const max_hats = 4;
pub const max_buttons = 32;
/// The value DirectInput reports for a centered hat.
pub const centred: u32 = 0xFFFF_FFFF;
/// The value DirectInput reports for a button down: its high bit.
pub const pressed: u8 = 0x80;
comptime {
assert(@offsetOf(JoystickState, "rz") == 0x14);
assert(@offsetOf(JoystickState, "buttons") == 0x30);
assert(@sizeOf(JoystickState) == 0x50);
}
/// Where hat `n` points, in hundredths of a degree clockwise from forward, or null while it is
/// centred.
pub fn hat(state: JoystickState, n: usize) ?u32 {
return if (state.pov[n] == centred) null else state.pov[n];
}
/// Where `axis`'s value lies.
pub fn axis(state: *JoystickState, which: Axis) *i32 {
return switch (which) {
.x => &state.x,
.y => &state.y,
.z => &state.z,
.rx => &state.rx,
.ry => &state.ry,
.rz => &state.rz,
.slider => &state.sliders[0],
.second_slider => &state.sliders[1],
};
}
};
/// A joystick's axes, in `DIJOYSTATE` order. DirectInput identifies an axis by its offset in
/// `DIJOYSTATE`.
pub const Axis = enum(u3) {
x,
y,
/// The throttle, on most joysticks that have one.
z,
rx,
ry,
/// The twist.
rz,
/// The first slider, where some joysticks put the throttle.
slider,
second_slider,
/// The range `joystick_object_found` (`0x004BD050`) sets for the axis, or null for axes the
/// game doesn't set up or read.
pub fn range(which: Axis) ?[2]i32 {
return switch (which) {
.x, .y, .rz => .{ -1000, 1000 },
.z, .slider => .{ 0, 1000 },
.rx, .ry, .second_slider => null,
};
}
};
/// The dead zone `joystick_object_found` sets for the whole device, in hundredths of a percent of
/// each axis's travel from the center: 10%. OpenReliant reads `DeadZone` from `starlancer.ini` to
/// change it.
pub const default_dead_zone: u16 = 1000;
/// A joystick device: OpenReliant's replacement for `joystick_device` (`0x005DDD24`), the game's
/// `IDirectInputDevice7`, with the calls the game makes on it. The platform implements it for each
/// connected controller, including gamepads.
pub const JoystickDevice = struct {
context: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
capabilities: *const fn (context: *anyopaque) Capabilities,
setRange: *const fn (context: *anyopaque, axis: Axis, min: i32, max: i32) void,
setDeadZone: *const fn (context: *anyopaque, zone: u16) void,
poll: *const fn (context: *anyopaque, state: *JoystickState) error{Unplugged}!void,
/// OpenReliant's: turns the motors of a controller that rumbles.
rumble: *const fn (context: *anyopaque, motors: force.Motors) void,
};
/// What `joystick_found` needs from the device: the counts from `GetCapabilities`, the axes
/// `EnumObjects` reports, and the product name from DirectInput's enumeration.
pub const Capabilities = struct {
name: []const u8,
axes: std.EnumSet(Axis),
/// `DIDEVCAPS.dwButtons`, at most the 32 `JoystickState` holds.
buttons: u8,
/// `DIDEVCAPS.dwPOVs`, at most 4.
hats: u8,
/// Added by OpenReliant: whether the controller is a gamepad. A gamepad's buttons are
/// numbered as in `GamepadButton`, and it has its own default bindings.
kind: Kind = .joystick,
/// Added by OpenReliant: whether it rumbles, which is how OpenReliant plays the force
/// feedback (`force`), in place of DirectInput's force feedback (`DIDC_FORCEFEEDBACK`).
rumbles: bool = false,
};
pub const Kind = enum { joystick, gamepad };
pub fn capabilities(device: JoystickDevice) Capabilities {
return device.vtable.capabilities(device.context);
}
/// `SetProperty(DIPROP_RANGE)` for one axis: the values it reports at either end of its travel.
pub fn setRange(device: JoystickDevice, axis: Axis, min: i32, max: i32) void {
device.vtable.setRange(device.context, axis, min, max);
}
/// `SetProperty(DIPROP_DEADZONE)` for the whole device: how far an axis can move from its
/// center, in hundredths of a percent of its travel, and still read as centered.
pub fn setDeadZone(device: JoystickDevice, zone: u16) void {
device.vtable.setDeadZone(device.context, zone);
}
/// `Poll` and `GetDeviceState`. Fails if the device has been disconnected.
pub fn poll(device: JoystickDevice, state: *JoystickState) error{Unplugged}!void {
return device.vtable.poll(device.context, state);
}
/// Turns the motors as hard as `motors` says, until the next call.
pub fn rumble(device: JoystickDevice, motors: force.Motors) void {
device.vtable.rumble(device.context, motors);
}
};
/// The game's joystick globals: the device (`joystick_device`), its state (`joystick`,
/// `0x00588340`), the axes that were set up (`joystick_axes`), the button and hat counts
/// (`joystick_buttons`, `joystick_hats`), the name (`joystick_name`), and the latches
/// `control_active` uses to count each press only once (`button_latched`, `0x005DDC98`).
pub const Joystick = struct {
device: ?JoystickDevice = null,
state: JoystickState = idle,
axes: JoystickAxes = std.mem.zeroes(JoystickAxes),
buttons: u8 = 0,
hats: u8 = 0,
name: []const u8 = "",
kind: JoystickDevice.Kind = .joystick,
/// Whether the device rumbles (`force_feedback`, `0x0050E1A4`, for OpenReliant's rumble).
rumbles: bool = false,
latched: [JoystickState.max_buttons]bool = @splat(false),
/// OpenReliant's: the presses of the buttons that the other side has taken, which count as
/// taken here too (`Keyboard.taken`).
taken: [JoystickState.max_buttons]bool = @splat(false),
/// OpenReliant's: the state the main thread last polled, while a joystick is attached, for the
/// game frames' joystick, which has no device of its own (`Devices.takeUp`). A read takes it in
/// place of polling. Null for the main thread's joystick, which polls its device.
handed: ?JoystickState = null,
/// The state when there is no device: all zero, as `read_joystick` leaves it.
const idle = std.mem.zeroes(JoystickState);
/// `joystick_found` (`0x004BD190`): stores the device's button and hat counts and its name,
/// then, as `joystick_object_found` does, sets the range of each axis the game uses, marks it
/// in `axes`, and sets the dead zone to `zone`. Not yet ported: turning off the centering
/// spring of a force feedback joystick.
pub fn open(joystick: *Joystick, device: JoystickDevice, zone: u16) void {
const found = device.capabilities();
joystick.* = .{
.device = device,
.buttons = @min(found.buttons, JoystickState.max_buttons),
.hats = @min(found.hats, JoystickState.max_hats),
.name = found.name,
.kind = found.kind,
.rumbles = found.rumbles,
};
var axes = found.axes.iterator();
while (axes.next()) |axis| {
const range = axis.range() orelse continue;
device.setRange(axis, range[0], range[1]);
device.setDeadZone(zone);
switch (axis) {
inline else => |known| @field(joystick.axes, @tagName(known)) = true,
}
}
}
/// Removes the device, for example when it has been disconnected; the joystick then reads as
/// idle.
pub fn close(joystick: *Joystick) void {
joystick.* = .{};
}
/// `read_joystick` (`0x004BD300`): reads the device's state (all zero without a device), then
/// clears the latches of the released buttons, its own and those the other side took
/// (`taken`). A disconnected device is closed. The game frames' joystick takes the state the
/// main thread handed over instead (`handed`).
pub fn read(joystick: *Joystick) void {
if (joystick.handed) |polled| {
joystick.state = polled;
} else {
joystick.state = idle;
const device = joystick.device orelse return;
device.poll(&joystick.state) catch {
joystick.close();
return;
};
}
const count = joystick.buttons;
for (joystick.latched[0..count], joystick.taken[0..count], joystick.state.buttons[0..count]) |*latched, *taken, state| {
if (state != 0) continue;
latched.* = false;
taken.* = false;
}
}
/// Whether a joystick is attached: the device itself, or for the game frames' joystick the
/// state handed over with it.
pub fn attached(joystick: Joystick) bool {
return joystick.device != null or joystick.handed != null;
}
/// Turns the motors of a device that rumbles as hard as `motors` says.
pub fn rumble(joystick: *const Joystick, motors: force.Motors) void {
const device = joystick.device orelse return;
if (joystick.rumbles) device.rumble(motors);
}
/// Whether `button` is pressed. Numbers beyond the 32 buttons in `JoystickState` never are.
pub fn down(joystick: Joystick, button: u8) bool {
return button < joystick.state.buttons.len and joystick.state.buttons[button] != 0;
}
};
/// Which of the joystick's axes the game set up: a byte for each axis, in the order of
/// `JoystickState`, which `joystick_object_found` sets when it gives the axis a range.
pub const JoystickAxes = extern struct {
x: bool,
y: bool,
/// The throttle, when the joystick has one.
z: bool,
/// Never set: the game gives the axis no range and never reads it.
rx: bool,
/// Never set, like `rx`.
ry: bool,
/// The twist.
rz: bool,
/// The first slider, which the game reads as the throttle without a Z axis.
slider: bool,
/// Never set, like `rx`.
second_slider: bool,
comptime {
// Each flag sits at its axis's offset in `JoystickState` over four.
assert(@offsetOf(JoystickAxes, "rz") == @offsetOf(JoystickState, "rz") / 4);
assert(@offsetOf(JoystickAxes, "slider") == @offsetOf(JoystickState, "sliders") / 4);
assert(@sizeOf(JoystickAxes) == @offsetOf(JoystickState, "pov") / 4);
}
};
/// DirectInput's `DIMOUSESTATE2`, which the game reads the mouse into at `mouse` each simulation
/// step: the movement since the previous read, and eight buttons.
pub const MouseState = extern struct {
x: i32,
y: i32,
z: i32,
/// Bit 7 is set while the button is down.
buttons: [8]u8,
comptime {
assert(@offsetOf(MouseState, "buttons") == 0xC);
assert(@sizeOf(MouseState) == 0x14);
}
};
/// One action's bindings, an entry of `control_bindings`;
/// [`input/controls.zig`](input/controls.zig) lists the actions and the bindings the game starts
/// with.
pub const ControlBinding = extern struct {
/// A DirectInput scan code (`DIK_*`), an index into `keyboard`.
key: u16,
modifier: Modifier,
/// The action's name, which `starlancer.ini` keys its bindings by.
name: [0x28]u8,
/// The language string of the action's name as the controls screens show it.
string: u16,
/// The key's name, which the game copies in from `key_names` as it loads the bindings.
key_name: [0x1E]u8,
/// A joystick button, or -1 for none.
button: i16,
/// The modifier held with the key: either of its two keys, left or right, counts.
pub const Modifier = enum(u16) {
none = 0,
shift = 1,
control = 2,
alt = 3,
_,
/// The word the game reads and writes the modifier by in `starlancer.ini` and
/// `default.txt`, and shows on `WaitForKey`'s prompt (`0x004E8550`, `0x004E8548`,
/// `0x004E8544`); null for none, and for one past them.
pub fn word(modifier: Modifier) ?[]const u8 {
return switch (modifier) {
.none => null,
.shift => "SHIFT",
.control => "CONTROL",
.alt => "ALT",
_ => null,
};
}
pub fn format(modifier: Modifier, writer: *std.Io.Writer) std.Io.Writer.Error!void {
return layout.formatTagAs(Modifier, modifier, "modifier", writer);
}
};
comptime {
assert(@offsetOf(ControlBinding, "name") == 0x4);
assert(@offsetOf(ControlBinding, "string") == 0x2C);
assert(@offsetOf(ControlBinding, "key_name") == 0x2E);
assert(@offsetOf(ControlBinding, "button") == 0x4C);
assert(@sizeOf(ControlBinding) == 0x4E);
}
};
/// The language strings the game writes bindings with, on the controls screens and in the prompt
/// `WaitForKey` shows: PRESS, before what to press, and JOY, before a joystick button's number
/// (`ButtonName`).
pub const press_string = 0x5AE;
pub const joy_string = 0x32C;
/// A joystick button as the game writes it: JOY and the button's number, as it holds it (`%s %d`,
/// `0x004E5444`). Print it with `{f}`.
pub const ButtonName = struct {
strings: *const language.Language,
button: u8,
pub fn format(name: ButtonName, writer: *std.Io.Writer) std.Io.Writer.Error!void {
try writer.print("{s} {d}", .{ name.strings.string(joy_string) orelse "", name.button });
}
};
test ButtonName {
var buffer: [16]u8 = undefined;
const strings: language.Language = .{ .strings = &.{} };
// Without the strings, only the number shows.
try std.testing.expectEqualStrings(" 3", try std.mem.print(&buffer, "{f}", .{ButtonName{ .strings = &strings, .button = 3 }}));
}
/// A key an action can be bound to, an entry of `key_names` (`0x004E5CD0`): its DirectInput scan
/// code and its name, which `WinMain` renames by the keyboard's own (`key_names_rename`,
/// `0x004BCF70`; `KeyNames`).
pub const KeyName = extern struct {
code: u32,
name: [0x20]u8,
comptime {
assert(@sizeOf(KeyName) == 0x24);
}
};
/// The keys' names as the controls screens show them, by scan code: the names the keyboard's
/// layout gives them, which `WinMain` takes from Windows as it starts (`key_names_rename`,
/// `0x004BCF70`) for every binding and every entry of `key_names`, and the loaders again for each
/// binding they read (`0x0042CA26`, `0x0042CCB2`). The platform names them (`set`); until it does,
/// they are the executable's own (`controls.keys`).
pub const KeyNames = struct {
names: [256]Name = english,
/// A name in the game's code page, as long as `key_name_get` (`0x004BCFF0`) keeps one.
pub const Name = struct {
bytes: [room]u8 = undefined,
len: u8 = 0,
/// `text`, already in the game's code page, cut to the room.
fn of(text: []const u8) Name {
var name: Name = .{ .len = @intCast(@min(text.len, room)) };
@memcpy(name.bytes[0..name.len], text[0..name.len]);
return name;
}
};
/// The most bytes a name keeps: `key_name_get` tells `GetKeyNameTextA` one byte less than the
/// 0x1D `WinMain` gives each name, and the terminator takes another.
pub const room = 0x1D - 2;
/// What a key the layout has no name for is called (`0x0050E1B4`).
pub const unknown = "Unknown";
/// The executable's names, and `unknown` for every key it names none.
const english: [256]Name = names: {
var table: [256]Name = @splat(.of(unknown));
for (controls.keys) |key| table[key.code] = .of(key.name);
break :names table;
};
/// The name of the key of scan code `code`; `unknown` past the codes.
pub fn of(names: *const KeyNames, code: u16) []const u8 {
if (code >= names.names.len) return unknown;
const name = &names.names[code];
return name.bytes[0..name.len];
}
/// The name of a binding's key `code` (`of`), and none for a binding without a key, which the
/// loaders leave without a name (`0x0042CA2B`).
pub fn ofBound(names: *const KeyNames, code: u16) []const u8 {
return if (code == 0) "" else names.of(code);
}
/// Names the key of scan code `code` `text`, UTF-8 as the platform gives a name, in the game's
/// code page (`language.encode`) and cut to the room; `unknown` for an empty name, as
/// `key_name_get` names a key Windows has no name for.
pub fn set(names: *KeyNames, code: u8, text: []const u8) void {
var buffer: [room]u8 = undefined;
const encoded = language.encode(&buffer, text);
names.names[code] = .of(if (encoded.len == 0) unknown else encoded);
}
};
/// How `player_controls` steers the player's ship: the `Controller` setting.
pub const ControlMode = enum(u32) {
/// The joystick's axes, or the keyboard while no joystick is attached (`Devices.controlMode`).
joystick = 0,
/// Steering keys held step the inputs.
keyboard = 1,
/// The mouse's movement, gathered into a stick position.
mouse = 2,
_,
pub fn format(mode: ControlMode, writer: *std.Io.Writer) std.Io.Writer.Error!void {
return layout.formatTagAs(ControlMode, mode, "controller", writer);
}
};
/// A key, by its DirectInput scan code (`DIK_*`): its place on the keyboard, whatever it types.
/// The codes follow the IBM PC's set 1, with the extended keys from `0x80` up. The bindings and
/// `starlancer.ini` hold them, and `Keyboard` is indexed by them.
pub const Key = enum(u8) {
escape = 0x01,
one = 0x02,
two = 0x03,
three = 0x04,
four = 0x05,
five = 0x06,
six = 0x07,
seven = 0x08,
eight = 0x09,
nine = 0x0A,
zero = 0x0B,
minus = 0x0C,
equals = 0x0D,
backspace = 0x0E,
tab = 0x0F,
q = 0x10,
w = 0x11,
e = 0x12,
r = 0x13,
t = 0x14,
y = 0x15,
u = 0x16,
i = 0x17,
o = 0x18,
p = 0x19,
left_bracket = 0x1A,
right_bracket = 0x1B,
enter = 0x1C,
left_control = 0x1D,
a = 0x1E,
s = 0x1F,
d = 0x20,
f = 0x21,
g = 0x22,
h = 0x23,
j = 0x24,
k = 0x25,
l = 0x26,
semicolon = 0x27,
apostrophe = 0x28,
grave = 0x29,
left_shift = 0x2A,
backslash = 0x2B,
z = 0x2C,
x = 0x2D,
c = 0x2E,
v = 0x2F,
b = 0x30,
n = 0x31,
m = 0x32,
comma = 0x33,
period = 0x34,
slash = 0x35,
right_shift = 0x36,
keypad_multiply = 0x37,
left_alt = 0x38,
space = 0x39,
caps_lock = 0x3A,
f1 = 0x3B,
f2 = 0x3C,
f3 = 0x3D,
f4 = 0x3E,
f5 = 0x3F,
f6 = 0x40,
f7 = 0x41,
f8 = 0x42,
f9 = 0x43,
f10 = 0x44,
num_lock = 0x45,
scroll_lock = 0x46,
keypad_7 = 0x47,
keypad_8 = 0x48,
keypad_9 = 0x49,
keypad_minus = 0x4A,
keypad_4 = 0x4B,
keypad_5 = 0x4C,
keypad_6 = 0x4D,
keypad_plus = 0x4E,
keypad_1 = 0x4F,
keypad_2 = 0x50,
keypad_3 = 0x51,
keypad_0 = 0x52,
keypad_period = 0x53,
/// The key between the left Shift and Z on a European keyboard (`DIK_OEM_102`).
non_us_backslash = 0x56,
f11 = 0x57,
f12 = 0x58,
keypad_enter = 0x9C,
right_control = 0x9D,
keypad_divide = 0xB5,
print_screen = 0xB7,
right_alt = 0xB8,
pause = 0xC5,
home = 0xC7,
up = 0xC8,
page_up = 0xC9,
left = 0xCB,
right = 0xCD,
end = 0xCF,
down = 0xD0,
page_down = 0xD1,
insert = 0xD2,
delete = 0xD3,
left_windows = 0xDB,
right_windows = 0xDC,
menu = 0xDD,
_,
pub fn format(key: Key, writer: *std.Io.Writer) std.Io.Writer.Error!void {
return switch (key) {
_ => writer.print("key 0x{X:0>2}", .{@backingInt(key)}),
inline else => |named| writer.writeAll(@tagName(named)),
};
}
};
/// The keys the input code names, as indices into `Keyboard`: the modifiers, and the keys
/// `frame_controls` steers the orbiting views with.
pub const scan = struct {
pub const escape = @backingInt(Key.escape);
pub const left_control = @backingInt(Key.left_control);
pub const left_shift = @backingInt(Key.left_shift);
pub const right_shift = @backingInt(Key.right_shift);
pub const left_alt = @backingInt(Key.left_alt);
pub const right_control = @backingInt(Key.right_control);
pub const right_alt = @backingInt(Key.right_alt);
pub const up = @backingInt(Key.up);
pub const left = @backingInt(Key.left);
pub const right = @backingInt(Key.right);
pub const down = @backingInt(Key.down);
};
/// The keyboard as the game reads it (`keyboard`, `0x00595C68`): each key down or up, by scan
/// code; and the latches `key_pressed` keeps so that a press counts once (`key_latched`,
/// `0x005D54EC`, and one for each modifier).
pub const Keyboard = struct {
down: [256]bool = @splat(false),
latched: [256]bool = @splat(false),
shift_latched: bool = false,
control_latched: bool = false,
alt_latched: bool = false,
/// Whether the keys 1 to 8 are the radio menu's, so that no action bound to them counts: while
/// the communications window is open (`hud.State.commsOpen`). OpenReliant sets it before each
/// tick for the game frames (`main.gameFrames`), and before them for the main thread.
numbers_taken: bool = false,
/// OpenReliant's: the presses the other side has taken, which count as taken here too: in the
/// game frames' keyboard the main thread's, and in the main thread's the game frames', as each
/// last handed them over (`Devices.takeUp`, `Devices.takePresses`). Like a latch, each is let
/// go here once its key is up.
taken: Latches = .{},
/// A keyboard's latches: the presses it has taken (`latches`).
pub const Latches = struct {
keys: [256]bool = @splat(false),
shift: bool = false,
control: bool = false,
alt: bool = false,
};
/// What `read_keyboard` (`0x004BD490`) does once it has the keys: frees the latches of the keys
/// that are up, and of each modifier with both its keys up, its own and those the other side
/// took (`taken`).
pub fn read(keyboard: *Keyboard) void {
for (&keyboard.latched, &keyboard.taken.keys, keyboard.down) |*latched, *taken, down| {
if (down) continue;
latched.* = false;
taken.* = false;
}
if (!keyboard.shift()) {
keyboard.shift_latched = false;
keyboard.taken.shift = false;
}
if (!keyboard.control()) {
keyboard.control_latched = false;
keyboard.taken.control = false;
}
if (!keyboard.alt()) {
keyboard.alt_latched = false;
keyboard.taken.alt = false;
}
}
/// The presses this keyboard has taken itself, which the other side counts as taken (`taken`).
pub fn latches(keyboard: *const Keyboard) Latches {
return .{
.keys = keyboard.latched,
.shift = keyboard.shift_latched,
.control = keyboard.control_latched,
.alt = keyboard.alt_latched,
};
}
/// Whether a modifier is latched, here or by the other side.
fn modifierLatched(keyboard: Keyboard) bool {
const own = keyboard.shift_latched or keyboard.control_latched or keyboard.alt_latched;
const taken = keyboard.taken;
return own or taken.shift or taken.control or taken.alt;
}
fn shift(keyboard: Keyboard) bool {
return keyboard.down[scan.left_shift] or keyboard.down[scan.right_shift];
}
fn control(keyboard: Keyboard) bool {
return keyboard.down[scan.left_control] or keyboard.down[scan.right_control];
}
fn alt(keyboard: Keyboard) bool {
return keyboard.down[scan.left_alt] or keyboard.down[scan.right_alt];
}
/// Whether `key` is down with `modifier` (`key_pressed`, `0x004BD570`). With `once`, only once
/// for each press, and with no modifier, only while no modifier key is down; it latches the
/// key and the modifier. Without it, with no modifier, only while no modifier is latched; it
/// frees the key's latch and the modifier's. What the other side has taken counts as latched
/// (`taken`).
pub fn pressed(keyboard: *Keyboard, key: u8, modifier: ControlBinding.Modifier, once: bool) bool {
if (!keyboard.down[key]) return false;
if (!once) {
switch (modifier) {
.none => if (keyboard.modifierLatched()) return false,
.shift => if (keyboard.shift()) {
keyboard.shift_latched = false;
} else return false,
.control => if (keyboard.control()) {
keyboard.control_latched = false;
} else return false,
.alt => if (keyboard.alt()) {
keyboard.alt_latched = false;
} else return false,
_ => return false,
}
keyboard.latched[key] = false;
return true;
}
if (keyboard.latched[key] or keyboard.taken.keys[key]) return false;
switch (modifier) {
.none => if (keyboard.shift() or keyboard.control() or keyboard.alt()) return false,
.shift => if (keyboard.shift()) {
keyboard.shift_latched = true;
} else return false,
.control => if (keyboard.control()) {
keyboard.control_latched = true;
} else return false,
.alt => if (keyboard.alt()) {
keyboard.alt_latched = true;
} else return false,
_ => return false,
}
keyboard.latched[key] = true;
return true;
}
/// Whether `key` is down with `modifier` as `cmd_WaitForKey` (`0x00459AE0`) asks: with either
/// of the modifier's keys down where it has one, and whatever modifier keys are down where it
/// has none. Unlike `pressed`, it latches nothing.
///
/// **Fix:** the game never takes a key bound with Alt, so that a script waiting for one waits
/// for ever unless the action has a joystick button. OpenReliant takes Alt as it takes Shift
/// and Control.
pub fn heldWith(keyboard: Keyboard, key: u8, modifier: ControlBinding.Modifier) bool {
if (!keyboard.down[key]) return false;
return switch (modifier) {
.none => true,
.shift => keyboard.shift(),
.control => keyboard.control(),
.alt => keyboard.alt(),
_ => false,
};
}
};
/// The input settings `load_key_config` reads from the `KeyConfig` section of `starlancer.ini`,
/// with the game's defaults.
pub const Settings = struct {
/// `ForceFeedback` (`0x0051DA4C`): whether the joystick's force feedback plays (`force`).
force_feedback: bool = true,
/// `JoystickInvert` (`joystick_invert`, `0x0051D610`): while false, pitch is reversed, from
/// the stick, the keys and the mouse.
joystick_invert: bool = true,
/// `HatEnable` (`hat_enabled`, `0x0052029C`): whether the hat switches to the left, right and
/// rear views.
hat_enabled: bool = true,
/// `TwistEnable` (`twist_enabled`, `0x00595D88`): whether the joystick's twist rolls the ship.
/// A gamepad's right stick rolls whatever it says (`Devices.twistRolls`).
twist_enabled: bool = false,
/// `Controller` (`control_mode`, `0x0057E064`).
control_mode: ControlMode = .joystick,
/// Added by OpenReliant: `DeadZone` in the `JoyConfig` section, the joystick's dead zone in
/// hundredths of a percent.
dead_zone: u16 = default_dead_zone,
};
/// `control_bindings` (`0x004E2380`): each action's key, modifier and joystick button, starting
/// from the game's defaults, which `load_key_config` changes from `starlancer.ini`.
pub const Bindings = std.EnumArray(controls.Action, controls.Binding);
pub const actions = @import("input/actions.zig");
/// The executable's bindings for a controller of `kind`, which `default.txt` changes
/// (`game.interface.keyConfigDefaults`): the game's own table for a joystick, and for a gamepad
/// (added by OpenReliant) the same keys with `gamepad_buttons` as the buttons.
pub fn defaultBindings(kind: JoystickDevice.Kind) Bindings {
var bindings: Bindings = undefined;
for (std.enums.values(controls.Action)) |action| bindings.set(action, controls.binding(action));
if (kind == .gamepad) {
for (&bindings.values) |*binding| binding.button = null;
for (gamepad_buttons) |pair| bindings.getPtr(pair[0]).button = @backingInt(pair[1]);
}
return bindings;
}
/// How OpenReliant numbers a gamepad's buttons when it presents the gamepad to the game as a
/// joystick. These are the numbers `JOY BUTTON` uses in `JoyConfig`. Face buttons are named by
/// position, not by label. The triggers and the four directions of the right stick are buttons too.
/// The left stick is the X and Y axes, the right stick's horizontal axis is the twist, and the
/// D-pad is the hat.
pub const GamepadButton = enum(u5) {
south,
east,
west,
north,
back,
guide,
start,
left_stick,
right_stick,
left_shoulder,
right_shoulder,
dpad_up,
dpad_down,
dpad_left,
dpad_right,
misc1,
right_paddle1,
left_paddle1,
right_paddle2,
left_paddle2,
touchpad,
misc2,
misc3,
misc4,
misc5,
misc6,
left_trigger,
right_trigger,
right_stick_up,
right_stick_down,
right_stick_left,
right_stick_right,
};
/// The default gamepad bindings, added by OpenReliant. Gamepads have no throttle axis, so the right
/// stick's up and down directions change the throttle, like the throttle keys.
pub const gamepad_buttons = [_]struct { controls.Action, GamepadButton }{
.{ .fire_lasers, .right_trigger },
.{ .launch_missile, .left_trigger },
.{ .afterburners, .south },
.{ .match_speed, .east },
.{ .countermeasures, .west },
.{ .target_nearest_enemy, .north },
.{ .next_enemy_target, .right_shoulder },
.{ .previous_enemy_target, .left_shoulder },
.{ .accelerate, .right_stick_up },
.{ .decelerate, .right_stick_down },
.{ .afterburner_toggle, .left_stick },
.{ .target_under_reticule, .right_stick },
.{ .radar_ranges, .back },
};
/// A button's press that counts only once the button has come up since what it acts on began, so
/// that a press held over from before, which ended the screen or the movie before, acts on nothing
/// new.
pub const FreshPress = struct {
/// Whether the button has stayed down since it began.
held: bool = true,
/// Whether the button, down where `down` has it, counts as pressed: down, and having come up
/// since it began.
pub fn pressed(press: *FreshPress, down: bool) bool {
if (!down) press.held = false;
return down and !press.held;
}
};
test "Mouse.pixelsIn" {
var mouse: Mouse = .{};
try std.testing.expectEqual(null, mouse.pixelsIn(.{ 800, 600 }));
mouse.at = .{ 0.5, 0.25 };
try std.testing.expectEqual(@Vector(2, f32){ 400, 150 }, mouse.pixelsIn(.{ 800, 600 }).?);
}
test "Mouse.notches" {
var mouse: Mouse = .{};
// A trackpad turns the wheel by parts of a notch, which count once they make a whole one.
mouse.wheel += 0.6;
try std.testing.expectEqual(0, mouse.notches());
mouse.wheel += 0.6;
try std.testing.expectEqual(1, mouse.notches());
mouse.wheel -= 2.5;
try std.testing.expectEqual(-2, mouse.notches());
try std.testing.expectApproxEqAbs(-0.3, mouse.wheel, 1e-5);
}
test FreshPress {
var press: FreshPress = .{};
// Held over from before, the press counts only once the button has come up.
try std.testing.expect(!press.pressed(true));
try std.testing.expect(!press.pressed(false));
try std.testing.expect(press.pressed(true));
try std.testing.expect(press.pressed(true));
}
/// The mouse, in place of DirectInput's: where the pointer is over the window, as fractions of its
/// size, and which buttons are down, as the platform last reported them, which the pause menu reads
/// (`menu_mouse_update`); and what `read_mouse` read at the last simulation step, which steers in
/// the mouse's mode (`player_controls`).
pub const Mouse = struct {
/// Null until the pointer has been over the window.
at: ?[2]f32 = null,
buttons: Buttons = .{},
/// The movement the platform has reported since the last read, in the mouse's counts.
motion: [2]f32 = .{ 0, 0 },
/// `mouse` (`0x00588398`): what the last read found.
state: State = .{},
/// Whether the stick has gathered the movement of the last read (`player_controls`).
gathered: bool = false,
/// The notches the wheel has turned that no screen has taken (`notches`), positive to scroll
/// up. Added by OpenReliant, as the game reads no wheel.
wheel: f32 = 0,
/// Where the pointer is, in pixels of an area `size` pixels across, such as the window; null
/// until the pointer has been over the window.
pub fn pixelsIn(mouse: Mouse, size: [2]u32) ?@Vector(2, f32) {
const share: @Vector(2, f32) = mouse.at orelse return null;
return share * @as(@Vector(2, f32), @floatFromInt(@as(@Vector(2, u32), size)));
}
/// What `read_mouse` reads, the parts of `MouseState` the game uses: the movement since the
/// read before, in whole counts, and the buttons down.
pub const State = struct {
moved: [2]i32 = .{ 0, 0 },
buttons: Buttons = .{},
};
/// `read_mouse` (`0x004BD3A0`): the movement since the last read, in whole counts, the rest
/// kept for the next, and the buttons down.
pub fn read(mouse: *Mouse) void {
const whole = @trunc(@as(@Vector(2, f32), mouse.motion));
const moved: @Vector(2, i32) = @intFromFloat(whole);
mouse.state = .{ .moved = moved, .buttons = mouse.buttons };
mouse.motion = @as(@Vector(2, f32), mouse.motion) - whole;
mouse.gathered = false;
}
/// The wheel's whole notches turned since they were last taken, positive to scroll up; the part
/// of a notch left over waits for the next.
pub fn notches(mouse: *Mouse) i32 {
const whole = @trunc(mouse.wheel);
mouse.wheel -= whole;
return @intFromFloat(whole);
}
pub const Buttons = packed struct(u2) {
left: bool = false,
right: bool = false,
pub fn any(buttons: Buttons) bool {
return buttons.left or buttons.right;
}
};
};
/// OpenReliant's: what the main thread has read of the devices for the game frames
/// (`Devices.handOver`), which they take up before each tick (`Devices.takeUp`). The game frames
/// keep devices of their own, so they never read the main thread's.
pub const Reading = struct {
/// The keys down, and the presses the main thread has taken.
keyboard: Keyboard = .{},
/// The joystick without its device: what it can do, the state last polled while it is
/// attached (`Joystick.handed`), and the presses the main thread has taken.
joystick: Joystick = .{},
/// The mouse's movement that the game frames haven't taken up yet, in the mouse's counts.
motion: [2]f32 = .{ 0, 0 },
/// The mouse's buttons down.
buttons: Mouse.Buttons = .{},
bindings: Bindings = defaultBindings(.joystick),
settings: Settings = .{},
};
/// The input state the game keeps in globals: the keyboard, joystick and mouse states, the
/// bindings and the input settings. The main thread has one set, which the platform fills; the
/// game frames have their own, which take up what the main thread has read (`Reading`).
pub const Devices = struct {
keyboard: Keyboard = .{},
joystick: Joystick = .{},
mouse: Mouse = .{},
bindings: Bindings = defaultBindings(.joystick),
settings: Settings = .{},
/// `default.txt` from the game folder, or a mod's replacement for it, which the bindings start
/// from (`game.interface.keyConfigDefaults`); null if the folder doesn't have one, which leaves
/// the executable's built-in bindings.
defaults_file: ?profile.Profile = null,
/// The keys' names as the keyboard's layout gives them, which the controls screens show.
key_names: KeyNames = .{},
/// **Improvement:** named mod actions shown in the controls screen (#617).
mod_actions: ?*actions.Registry = null,
/// What the player steers with: the controller chosen, but the keyboard while the joystick
/// chosen is not attached, as `load_key_config` has it (`0x0042C8A5`).
///
/// **Fix:** the game steers with the keyboard by changing the setting itself, which its controls
/// screen then writes to `starlancer.ini`, so that the joystick is lost as the choice for every
/// game after. OpenReliant keeps the choice, and steers with the joystick as soon as one is
/// attached.
pub fn controlMode(devices: Devices) ControlMode {
if (devices.settings.control_mode == .joystick and !devices.joystick.attached()) return .keyboard;
return devices.settings.control_mode;
}
/// Whether the joystick's twist rolls the ship: a joystick's where `TwistEnable` says so, and a
/// gamepad's right stick always, as OpenReliant presents it to the game (`GamepadButton`).
pub fn twistRolls(devices: Devices) bool {
return devices.settings.twist_enabled or devices.joystick.kind == .gamepad;
}
/// Reads the keyboard, the joystick and the mouse, as `simulation_step` does at the start of
/// each step.
pub fn read(devices: *Devices) void {
devices.keyboard.read();
devices.joystick.read();
devices.mouse.read();
}
/// OpenReliant's: the main thread reads its devices for the game frames, once a pass while the
/// game plays: it lets go of the latches of the keys and the buttons that are up and polls the