Repository navigation
Expand file tree
/
Copy pathreference.zig
More file actions
1025 lines (955 loc) · 49 KB
/
Copy pathreference.zig
File metadata and controls
1025 lines (955 loc) · 49 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
//! The reference of the scripting API, generated from its declarations: the definitions file for
//! Luau's language server (`openreliant.d.luau`), which editors complete names and check types
//! with, the reference page (`reference.md`), and the list `openreliant hooks` prints.
//!
//! `docs/guide` holds the definitions and the reference page as generated, and tests check that
//! they haven't changed. Everything scripts see is in them: the engine handlers, the packages, the
//! fields and methods of objects, the hooks and the fields of their `e`, the records, and the names
//! of values. So a change to OpenReliant's code that would change what scripts see fails the tests,
//! and the scripting API only changes on purpose: then `make definitions` writes the files again.
const std = @import("std");
const Writer = std.Io.Writer;
const openreliant = @import("openreliant");
const engine = openreliant.engine;
const engine_hooks = engine.hooks;
const Hook = engine_hooks.Hook;
const Object = engine_hooks.Object;
const values = @import("values.zig");
const hooks = @import("hooks.zig");
const objects = @import("objects.zig");
const missiles = @import("missiles.zig");
const turrets = @import("turrets.zig");
const instruments = @import("instruments.zig");
const records = @import("records.zig");
const field_tables = records.field_tables;
const bind = @import("bind.zig");
const script = @import("script.zig");
const api = @import("api.zig");
const data = @import("data.zig");
const packages = @import("packages.zig");
/// Whether `T` is a value that scripts hold as a handle or a reference, which has a type of its
/// own in the definitions rather than fields to list.
fn held(comptime T: type) bool {
return T == Object or T == objects.Handle or T == engine_hooks.Missile or T == missiles.Handle or T == engine_hooks.Turret or T == turrets.Handle or T == data.Data or T == values.Table or T == []const u8;
}
/// Whether `T` is one of the records' structs, which the definitions declare as extern types.
fn isRecord(comptime T: type) bool {
return std.mem.findScalar(type, records.Values.kinds, T) != null;
}
/// The kinds of handle scripts hold, which the definitions declare as extern types: each one's
/// name, fields and methods.
const handle_kinds = .{
.{ "Object", objects.fields, objects.methods },
.{ "Missile", missiles.fields, missiles.methods },
.{ "Turret", turrets.fields, turrets.methods },
};
/// The types of what a declared function takes and gives; nothing for a native one.
fn functionTypes(comptime F: type) []const type {
return if (@hasDecl(F, "Parameters")) &(F.Parameters ++ .{F.Result}) else &.{};
}
/// The types at the roots of what scripts see, which `gather` follows.
const roots: []const type = list: {
@setEvalBranchQuota(1_000_000);
var found: []const type = namespaceTypes(objects.fields) ++ namespaceTypes(objects.methods) ++ namespaceTypes(missiles.fields) ++ namespaceTypes(missiles.methods) ++ namespaceTypes(turrets.fields) ++ namespaceTypes(turrets.methods);
for (std.enums.values(script.Package)) |package| {
if (packages.namespace(package)) |Namespace| found = found ++ namespaceTypes(Namespace);
}
for (std.enums.values(@import("builtin_interfaces.zig").Group)) |group| found = found ++ namespaceTypes(group.namespace());
found = found ++ fieldTableTypes();
for (std.enums.values(script.Handler)) |handler| {
found = found ++ @typeInfo(handler.Arguments()).@"struct".field_types;
}
for (std.enums.values(Hook)) |hook| {
const declared = engine_hooks.declaration(hook);
for (values.shownFields(declared.Fields)) |field| found = found ++ .{field.type};
if (declared.Result != void) found = found ++ .{declared.Result};
}
// The game's actions (`on_action`) and camera views (`camera.view`), which scripts pass and get
// as strings, since mods add their own; and the layout a HUD display gives an instrument, which
// only `register_display`'s definition takes.
break :list found ++ records.Values.kinds ++ .{ engine.input.controls.Action, engine.game.camera.View, instruments.Layout, instruments.PartLayout };
};
/// The types of what scripts pass the functions and methods declared, which `given` follows.
const passed: []const type = list: {
@setEvalBranchQuota(1_000_000);
var found: []const type = namespaceParameters(objects.methods) ++ namespaceParameters(missiles.methods) ++ namespaceParameters(turrets.methods);
for (std.enums.values(script.Package)) |package| {
if (packages.namespace(package)) |Namespace| found = found ++ namespaceParameters(Namespace);
}
for (std.enums.values(@import("builtin_interfaces.zig").Group)) |group| found = found ++ namespaceParameters(group.namespace());
// Load scripts give the fields of the records' tables too.
break :list found ++ fieldTableTypes();
};
/// The types of the fields of the records' tables (`field_tables`).
fn fieldTableTypes() []const type {
var found: []const type = &.{};
for (field_tables) |Table| {
for (std.enums.values(Table.Field)) |field| found = found ++ .{Table.TypeOf(field)};
}
return found;
}
/// The types of the parameters of the functions `Namespace` declares; none for a native one.
fn namespaceParameters(comptime Namespace: type) []const type {
var found: []const type = &.{};
for (api.declared(Namespace, .function)) |name| {
const F = @field(Namespace, name);
if (@hasDecl(F, "Parameters")) found = found ++ F.Parameters;
}
return found;
}
/// The tables scripts give, reached from what declared functions take.
const given_tables: []const type = found: {
@setEvalBranchQuota(10_000_000);
var seen: Gathered = .{};
for (passed) |T| seen = gather(T, seen);
break :found seen.tables;
};
/// The types of the fields and functions `Namespace` declares.
fn namespaceTypes(comptime Namespace: type) []const type {
var found: []const type = &.{};
for (api.declared(Namespace, .field)) |name| found = found ++ .{@field(Namespace, name).Type};
for (api.declared(Namespace, .function)) |name| found = found ++ functionTypes(@field(Namespace, name));
return found;
}
/// The enums and the tables (structs given as tables) scripts see, each once, in the order `roots`
/// reaches them.
const Gathered = struct {
enums: []const type = &.{},
tables: []const type = &.{},
};
const gathered: Gathered = found: {
@setEvalBranchQuota(10_000_000);
var seen: Gathered = .{};
for (roots) |T| seen = gather(T, seen);
// Each type takes a name of its own, and so do the records and the handles.
var names: []const []const u8 = &.{};
for (seen.enums ++ seen.tables) |T| names = names ++ .{bind.noun(T)};
for (records.Values.kinds) |T| {
if (@typeInfo(T) == .@"struct") names = names ++ .{bind.noun(T)};
}
for (handle_kinds) |kind| names = names ++ .{kind[0]};
for (names, 0..) |name, at| {
for (names[at + 1 ..]) |other| {
if (std.mem.eql(u8, name, other)) @compileError("two types scripts see are named " ++ name ++ ": give one a script_name");
}
}
break :found seen;
};
fn gather(comptime T: type, comptime seen: Gathered) Gathered {
if (held(T) or T == void) return seen;
if (values.isList(T)) return gather(T.Item, seen);
return switch (@typeInfo(T)) {
.optional => |optional| gather(optional.child, seen),
.array => |array| if (array.child == u8) seen else gather(array.child, seen),
.@"enum" => if (std.mem.findScalar(type, seen.enums, T) != null) seen else .{ .enums = seen.enums ++ .{T}, .tables = seen.tables },
.@"struct" => fields: {
if (std.mem.findScalar(type, seen.tables, T) != null) break :fields seen;
var next = seen;
if (!isRecord(T)) next.tables = next.tables ++ .{T};
for (values.shownFields(T)) |field| next = gather(field.type, next);
break :fields next;
},
.@"union" => |info| variants: {
var next = seen;
for (info.field_types) |Variant| next = gather(Variant, next);
break :variants next;
},
else => seen,
};
}
/// Whether the definitions declare a type for the enum or struct `T`: one `roots` reaches, or a
/// record.
fn isDeclared(comptime T: type) bool {
return isRecord(T) or std.mem.findScalar(type, gathered.enums, T) != null or std.mem.findScalar(type, gathered.tables, T) != null;
}
/// Declares the extern type `name` of the handles whose fields are `Fields` and whose methods are
/// `Methods`, for the definitions file.
fn writeHandleClass(w: *Writer, comptime name: []const u8, comptime Fields: type, comptime Methods: type) Writer.Error!void {
try w.writeAll("declare extern type " ++ name ++ " with\n");
inline for (comptime api.declared(Fields, .field)) |field_name| {
const field = @field(Fields, field_name);
try w.print(" --- {s}\n {s}: {s}\n", .{ field.description, field_name, comptime luauType(field.Type) });
}
inline for (comptime api.declared(Methods, .function)) |method_name| {
const method = @field(Methods, method_name);
const parameters = comptime parameterList(method, 1);
try w.print(" --- {s}\n function {s}(self{s}{s}): {s}\n", .{ method.description, method_name, if (parameters.len > 0) ", " else "", parameters, comptime resultType(method) });
}
try w.writeAll("end\n");
}
/// The tables of the fields `Fields` and the methods `Methods` of a kind of handle, for the
/// reference page.
fn writeHandleTables(w: *Writer, comptime Fields: type, comptime Methods: type) Writer.Error!void {
try w.writeAll("\n| Field | Type | What it is |\n|---|---|---|\n");
inline for (comptime api.declared(Fields, .field)) |name| {
const field = @field(Fields, name);
try w.print("| `{s}` | {s} | {s}{s} |\n", .{ name, comptime markdownType(field.Type), if (field.writable) "*Changes.* " else "", field.description });
}
try w.writeAll("\n| Method | Returns | What it does |\n|---|---|---|\n");
inline for (comptime api.declared(Methods, .function)) |name| {
const method = @field(Methods, name);
try w.print("| `{s}({s})` | {s} | {s} |\n", .{ name, comptime cell(parameterList(method, 1)), comptime markdownResult(method), method.description });
}
}
/// The Luau type of a value of `T` (`values.push`).
fn luauType(comptime T: type) []const u8 {
comptime {
if (T == Object or T == objects.Handle) return "Object";
if (T == engine_hooks.Missile or T == missiles.Handle) return "Missile";
if (T == engine_hooks.Turret or T == turrets.Handle) return "Turret";
if (values.isList(T)) return "{ " ++ luauType(T.Item) ++ " }";
if (T == data.Data) return "any";
if (T == values.Table) return "{ [any]: any }";
return switch (@typeInfo(T)) {
.@"union" => |info| unionType(info),
.void => api.nothing,
.float, .int => "number",
.bool => "boolean",
.@"enum", .@"struct" => if (isDeclared(T)) bind.noun(T) else @compileError("the definitions don't declare " ++ @typeName(T)),
.optional => |optional| if (@typeInfo(optional.child) == .@"union") "(" ++ luauType(optional.child) ++ ")?" else luauType(optional.child) ++ "?",
.vector => "vector",
.array => |array| if (array.child == u8) "string" else "{ " ++ luauType(array.child) ++ " }",
.pointer => "string",
else => @compileError("no Luau type for " ++ @typeName(T)),
};
}
}
/// The Luau type of a union of booleans, numbers and strings: whichever of them.
fn unionType(comptime info: std.lang.Type.Union) []const u8 {
comptime {
var text: []const u8 = "";
for (info.field_types, 0..) |Variant, at| text = text ++ (if (at == 0) "" else " | ") ++ luauType(Variant);
return text;
}
}
/// The parameters of the declared function `F`, as Luau writes them, without the first `skipped`
/// (a method's receiver).
fn parameterList(comptime F: type, comptime skipped: usize) []const u8 {
comptime {
if (@hasDecl(F, "luau_parameters")) return F.luau_parameters;
var text: []const u8 = "";
for (F.names[skipped..], F.Parameters[skipped..], 0..) |name, P, at| {
text = text ++ (if (at == 0) "" else ", ") ++ name ++ ": " ++ luauType(P);
}
return text;
}
}
/// What the declared function `F` returns, as Luau writes it.
fn resultType(comptime F: type) []const u8 {
return if (@hasDecl(F, "luau_result")) F.luau_result else luauType(F.Result);
}
/// The Luau type of the declared function `F`: the type it's declared as (`api.NativeTyped`), or
/// its parameters and its result.
fn functionType(comptime F: type) []const u8 {
if (@hasDecl(F, "luau_function")) return F.luau_function;
return "(" ++ parameterList(F, 0) ++ ") -> " ++ resultType(F);
}
/// How `openreliant hooks` writes the declared function `F`, called `name`: as a call with its
/// result, or with the type it's declared as (`api.NativeTyped`).
fn helpCall(comptime name: []const u8, comptime F: type) []const u8 {
if (@hasDecl(F, "luau_function")) return name ++ ": " ++ F.luau_function;
return name ++ "(" ++ parameterList(F, 0) ++ ") -> " ++ resultType(F);
}
/// How the reference page writes a call of the declared function `F`, called `name`: with its
/// parameters, or its name alone where it's declared as a type (`api.NativeTyped`).
fn markdownCall(comptime name: []const u8, comptime F: type) []const u8 {
if (@hasDecl(F, "luau_function")) return name;
return name ++ "(" ++ cell(parameterList(F, 0)) ++ ")";
}
/// The name of the class of `hook`'s `e`: its name in Pascal case, or `OrderRoutine` for the
/// order table's routines, which all see the same fields.
fn eventClass(comptime hook: Hook) []const u8 {
comptime {
if (engine_hooks.Fields(hook) == engine_hooks.RoutineFields) return "OrderRoutine";
return pascal(@tagName(hook));
}
}
/// `name`, in snake case, in Pascal case.
fn pascal(comptime name: []const u8) []const u8 {
comptime {
var text: []const u8 = "";
var upper = true;
for (name) |c| {
if (c == '_') {
upper = true;
continue;
}
text = text ++ .{if (upper) std.ascii.toUpper(c) else c};
upper = false;
}
return text;
}
}
/// The families of scripts, in order.
const running_families = std.enums.values(script.Family);
/// Whether the engine calls `handler` for a family it runs.
fn called(comptime handler: script.Handler) bool {
for (running_families) |family| {
if (handler.givenBy(family)) return true;
}
return false;
}
/// The arguments of `handler`, as Luau writes them.
fn handlerParameters(comptime handler: script.Handler) []const u8 {
comptime {
var text: []const u8 = "";
const info = @typeInfo(handler.Arguments()).@"struct";
for (info.field_names, info.field_types, 0..) |name, Argument, at| {
text = text ++ (if (at == 0) "" else ", ") ++ name ++ ": " ++ luauType(Argument);
}
return text;
}
}
/// Writes the definitions file.
pub fn writeDefinitions(w: *Writer) Writer.Error!void {
@setEvalBranchQuota(10_000_000);
try w.writeAll(
\\-- The types of OpenReliant's scripting API, for Luau's language server (luau-lsp). Add this
\\-- file to its luau-lsp.types.definitionFiles setting under a name, such as "@openreliant", and
\\-- give each package's local its type, such as `local hooks: Hooks = require("openreliant.hooks")`,
\\-- for completion, descriptions and type checks. Generated by `openreliant hooks --definitions`
\\-- from OpenReliant's bindings; don't change it by hand.
\\
\\
);
try w.writeAll("-- Names of values. A value without a name is a number.\n\n");
inline for (gathered.enums) |T| {
try w.print("export type {s} = ", .{comptime bind.noun(T)});
inline for (comptime values.names(T), 0..) |name, at| {
if (at > 0) try w.writeAll(" | ");
try w.print("\"{s}\"", .{name});
}
if (comptime values.takesNumbers(T)) try w.writeAll(" | number");
if (comptime values.takesModNames(T)) try w.writeAll(" | string");
try w.writeAll("\n");
}
try w.writeAll("\n-- Tables of values, which scripts can only read.\n\n");
inline for (gathered.tables) |T| try writeTable(w, comptime bind.noun(T), T);
try w.writeAll("\n-- Objects, missiles and turrets, which scripts hold by handles.\n\n");
inline for (handle_kinds) |kind| try writeHandleClass(w, kind[0], kind[1], kind[2]);
try w.writeAll("\n-- The records (`openreliant.records`).\n\n");
inline for (comptime records.Values.kinds) |T| {
if (@typeInfo(T) != .@"struct") continue;
try w.print("declare extern type {s} with\n", .{comptime bind.noun(T)});
inline for (comptime values.shownFields(T)) |field| try w.print(" {s}: {s}\n", .{ field.name, comptime luauType(field.type) });
try w.writeAll("end\n");
}
inline for (field_tables) |Table| {
try w.print("declare extern type {s} with\n", .{Table.script_name});
inline for (comptime std.enums.values(Table.Field)) |field| try w.print(" {t}: {s}\n", .{ field, comptime luauType(Table.TypeOf(field)) });
try w.writeAll("end\n");
}
try w.writeAll("export type Records = {\n");
inline for (comptime std.enums.values(records.Set)) |set| {
const Element = set.Element();
const element = comptime if (Element == []const u8) "string" else bind.noun(Element);
try w.print(" {s}: {{ [number | string]: {s} }},\n", .{ @tagName(set), element });
}
try w.writeAll(" --- The campaign's missions, by their numbers, in the order it flies them. Load scripts change it by assigning a new list.\n");
try w.writeAll(" campaign: { number },\n");
try w.writeAll(" --- The colour of the light the planets get, nil for the key lights' colour, (1, 1, 0.8), as the original lights them. Only the planets and their atmospheres take this light, so a mod can light them without lighting any other model. Load scripts set it.\n");
try w.writeAll(" planet_light: vector?,\n");
inline for (field_tables) |Table| {
// A table whose entries scripts also find by name takes strings as keys too
// (`table.Table`).
const key = if (@hasDecl(Table, "placeNamed")) "number | string" else "number";
try w.print(" --- {s}\n {s}: {{ [{s}]: {s} }},\n", .{ Table.entries_about, Table.list_name, key, Table.script_name });
}
try w.writeAll("}\n");
try w.writeAll("\n-- The packages made from declarations. `openreliant.self` is the script's own Object.\n");
inline for (comptime std.enums.values(script.Package)) |package| {
if (comptime packages.namespace(package)) |Namespace| {
try w.print("\n--- {s}\nexport type {s} = {{\n", .{ package.about(), comptime pascal(@tagName(package)) });
inline for (comptime api.declared(Namespace, .field)) |name| {
const field = @field(Namespace, name);
try w.print(" --- {s}\n {s}: {s},\n", .{ field.description, name, comptime luauType(field.Type) });
}
inline for (comptime api.declared(Namespace, .function)) |name| {
const function = @field(Namespace, name);
try w.print(" --- {s}\n {s}: {s},\n", .{ function.description, name, comptime functionType(function) });
}
try w.writeAll("}\n");
}
}
try w.print("\n--- {s}\nexport type Interfaces = {{\n", .{script.Package.interfaces.about()});
inline for (comptime std.enums.values(@import("builtin_interfaces.zig").Group)) |tag| {
const Namespace = comptime tag.namespace();
try w.print(" {s}: {{\n", .{@tagName(tag)});
inline for (comptime api.declared(Namespace, .field)) |name| {
const field = @field(Namespace, name);
try w.print(" {s}: {s},\n", .{ name, comptime luauType(field.Type) });
}
inline for (comptime api.declared(Namespace, .function)) |name| {
const function = @field(Namespace, name);
try w.print(" {s}: {s},\n", .{ name, comptime functionType(function) });
}
try w.writeAll(" },\n");
}
try w.writeAll(" [string]: any,\n}\n");
try w.writeAll("\n--- A section of a mod's storage: its fields by name, each plain data. Reading one gives a copy.\nexport type Section = { [string]: any }\n");
try w.writeAll(
\\
\\-- The hooks (`openreliant.hooks`). Each handler gets `e`, whose type is named after the hook.
\\-- Annotate it, as in `function(e: ObjectDamage)`: Luau doesn't work it out from the hook's name.
\\
\\declare extern type HookHandle with
\\ --- Removes the handler. Returns whether it was there.
\\ function remove(self): boolean
\\end
\\
\\--- The objects a handler is for: each test that's given must hold.
\\export type Filter = {
\\ object: Object?,
\\ type: (ShipType | { ShipType })?,
\\ class: (ShipClass | { ShipClass })?,
\\ side: (Side | { Side })?,
\\}
\\
);
comptime var declared: []const []const u8 = &.{};
inline for (comptime std.enums.values(Hook)) |hook| {
const class = comptime eventClass(hook);
const seen = comptime for (declared) |name| {
if (std.mem.eql(u8, name, class)) break true;
} else false;
if (!seen) {
declared = declared ++ .{class};
try writeEventClass(w, hook, class);
}
}
for (std.enums.values(hooks.Names)) |names| try writeHookNames(w, names);
try w.writeAll("\n");
try writeAdd(w, hooks.add_type, .all);
try writeAdd(w, hooks.after_type, .functions);
try w.print(
\\
\\export type Hooks = {{
\\ --- Adds a handler that runs before the hook's function, or when its event happens.
\\ add: {s},
\\ --- Adds a handler that runs after the hook's function, which sees its result in e.result.
\\ after: {s},
\\}}
\\
\\-- What scripts return.
\\
, .{ hooks.add_type, hooks.after_type });
inline for (running_families) |family| {
try w.print("\nexport type {s}Script = {{\n engine_handlers: {{\n", .{comptime pascal(@tagName(family))});
inline for (comptime std.enums.values(script.Handler)) |handler| {
if (comptime called(handler) and handler.givenBy(family)) {
try w.print(" {t}: (({s}) -> {s})?,\n", .{ handler, comptime handlerParameters(handler), comptime luauType(handler.Result()) });
}
}
try w.writeAll(" }?,\n");
if (comptime script.Offer.event_handlers.offeredBy(family)) {
try w.writeAll(
\\ event_handlers: { [string]: (data: any) -> boolean? }?,
\\ interface_name: string?,
\\ interface: { [any]: any }?,
\\
);
}
try w.writeAll("}\n");
}
}
/// Writes the extern type of `hook`'s `e`.
fn writeEventClass(w: *Writer, comptime hook: Hook, comptime class: []const u8) Writer.Error!void {
const declared = comptime engine_hooks.declaration(hook);
if (comptime engine_hooks.Fields(hook) == engine_hooks.RoutineFields) {
try w.print("\n--- What a hook on one of the order table's routines sees.\ndeclare extern type {s} with\n", .{class});
} else {
try w.print("\n--- {s}\ndeclare extern type {s} with\n", .{ declared.about, class });
}
inline for (comptime values.shownFields(declared.Fields)) |field| try w.print(" {s}: {s}\n", .{ field.name, comptime luauType(field.type) });
if (declared.Result != void) try w.print(" result: {s}\n", .{comptime luauType(declared.Result)});
if (declared.on.isFunction()) {
const returned = comptime if (declared.Result == void) api.nothing else luauType(declared.Result) ++ "?";
try w.print(" --- Runs the rest of the call now: the handlers after this one, then the function.\n function original(self): {s}\n", .{returned});
}
try w.writeAll("end\n");
}
/// Writes the type of the names of the hooks of `names`.
fn writeHookNames(w: *Writer, names: hooks.Names) Writer.Error!void {
try w.print("\n--- {s}\nexport type {s} = ", .{ names.about(), names.typeName() });
var first = true;
for (std.enums.values(Hook)) |hook| {
if (!names.contains(hook)) continue;
try w.print("{s}\"{s}\"", .{ if (first) "" else " | ", @tagName(hook) });
first = false;
}
try w.writeAll("\n");
}
/// Writes `name`, the type of a function that adds a handler to a hook of `names`. The handler's `e`
/// is `any`, which scripts annotate with the hook's type, since Luau can't work it out from the
/// hook's name ([The reference](../../docs/port/scripting.md#the-reference)).
fn writeAdd(w: *Writer, name: []const u8, names: hooks.Names) Writer.Error!void {
try w.print("export type {s} = (name: {s}, {s}) -> HookHandle\n", .{ name, names.typeName(), hooks.handler_parameters });
}
/// Writes a table type of `T`'s fields, as `values.pushTable` gives them.
fn writeTable(w: *Writer, comptime name: []const u8, comptime T: type) Writer.Error!void {
try w.print("export type {s} = {{\n", .{name});
inline for (comptime values.shownFields(T)) |field| {
// A field with a default can be left out of a table a script gives.
const optional = if (comptime given(T) and field.defaultValue() != null and @typeInfo(field.type) != .optional) "?" else "";
try w.print(" {s}: {s}{s},\n", .{ field.name, comptime luauType(field.type), optional });
}
try w.writeAll("}\n");
}
/// Whether `T` is a table scripts give, such as a style, to a function that takes one.
fn given(comptime T: type) bool {
return std.mem.findScalar(type, given_tables, T) != null;
}
/// How many of the fields scripts see of `T` have a default, which a table scripts give may leave
/// out.
fn defaulted(comptime T: type) usize {
var count: usize = 0;
for (values.shownFields(T)) |field| count += @intFromBool(field.defaultValue() != null);
return count;
}
/// A field's default, as a script would write it.
fn defaultText(comptime field: values.Field) []const u8 {
comptime {
const value = field.defaultValue().?;
return switch (@typeInfo(field.type)) {
.float, .int => std.fmt.comptimePrint("{d}", .{value}),
.bool => if (value) "true" else "false",
.@"enum" => "`\"" ++ @tagName(value) ++ "\"`",
.vector => std.fmt.comptimePrint("`vector.create({d}, {d}, {d})`", .{ value[0], value[1], value[2] }),
.optional => "nil",
.pointer => "`\"" ++ value ++ "\"`",
.@"struct" => if (values.isList(field.type) and value.len == 0) "none" else @compileError("no way to write the default of " ++ @typeName(field.type)),
else => @compileError("no way to write the default of " ++ @typeName(field.type)),
};
}
}
/// Writes the reference page, `docs/guide/reference.md`: the engine handlers, the packages, the
/// fields and methods of objects, each hook with the fields of its `e`, the tables, and the names of
/// values.
pub fn writeMarkdown(w: *Writer) Writer.Error!void {
@setEvalBranchQuota(10_000_000);
try w.writeAll(
\\# Scripting reference
\\
\\This page lists everything mods' scripts can use: the engine handlers, the packages, the fields
\\and methods of objects, the hooks with the fields each handler sees in `e`, and the names of
\\values. [Scripting](scripting.md) explains how to use them. `openreliant hooks` prints the list of
\\hooks, and `openreliant hooks <name>` one hook.
\\
\\This page is generated from OpenReliant's code by `make definitions`, so don't change it by hand.
\\
\\- [Engine handlers](#engine-handlers)
\\- [Packages](#packages)
\\- [Objects](#objects)
\\- [Built-in interfaces](#built-in-interfaces)
\\- [The game's functions](#the-games-functions)
\\- [The order routines](#the-order-routines)
\\- [The mission's events](#the-missions-events)
\\- [The engine's events](#the-engines-events)
\\- [Tables](#tables)
\\- [Names of values](#names-of-values)
\\
\\## Engine handlers
\\
\\The functions a script returns in `engine_handlers`, which OpenReliant calls. Global scripts
\\include mission scripts.
\\
\\| Handler | Scripts | When it's called |
\\|---|---|---|
\\
);
inline for (comptime std.enums.values(script.Handler)) |handler| {
if (comptime called(handler)) {
try w.print("| `{t}({s})`{s} | ", .{ handler, comptime handlerParameters(handler), comptime if (handler.Result() == void) "" else ": " ++ markdownType(handler.Result()) });
try writeFamilies(w, handler);
try w.print(" | {s} |\n", .{handler.about()});
}
}
try w.writeAll(
\\
\\## Packages
\\
\\What `require("openreliant.<name>")` gives.
\\
);
inline for (comptime std.enums.values(script.Package)) |package| {
try writePackageSection(w, package);
}
try w.writeAll(
\\
\\## Objects
\\
\\Scripts see objects through handles. A handle stays valid until its object is removed or its
\\mission ends; reading a field of a handle that isn't valid is an error. Every script can read the
\\fields; global scripts can change those marked *changes* on any object, and an object's own
\\scripts on their object.
\\
);
try writeHandleTables(w, objects.fields, objects.methods);
try w.writeAll(
\\
\\## Missiles
\\
\\Scripts see missiles in flight through handles too. A missile's handle stays valid until its
\\flight ends or its mission ends. Every script can read the fields; global scripts can change those
\\marked *changes* on any missile, and a missile's own scripts on their missile.
\\
);
try writeHandleTables(w, missiles.fields, missiles.methods);
try w.writeAll(
\\
\\## Turrets
\\
\\Scripts see an object's turrets through handles too: its guns that turn to aim, spin their
\\barrels or launch missiles. A turret's handle stays valid while its object is in the mission.
\\Every script can read the fields; global scripts can change those marked *changes* on any
\\turret, and a turret's own scripts on their turret.
\\
);
try writeHandleTables(w, turrets.fields, turrets.methods);
try w.writeAll("\n## Built-in interfaces\n\n`require(\"openreliant.interfaces\")` gives these groups of the packages' functions, unless a mod offers an interface of the same name. A group a script's packages don't allow is nil.\n");
inline for (comptime std.enums.values(@import("builtin_interfaces.zig").Group)) |tag| {
const Namespace = comptime tag.namespace();
try w.print("\n### I.{s}\n\n| Member | Type or returns | Description |\n|---|---|---|\n", .{@tagName(tag)});
inline for (comptime api.declared(Namespace, .field)) |name| {
const field = @field(Namespace, name);
try w.print("| `{s}` | {s} | {s} |\n", .{ name, comptime markdownType(field.Type), field.description });
}
inline for (comptime api.declared(Namespace, .function)) |name| {
const function = @field(Namespace, name);
try w.print("| `{s}` | {s} | {s} |\n", .{ comptime markdownCall(name, function), comptime markdownResult(function), function.description });
}
}
try w.writeAll(
\\
\\## The game's functions
\\
\\Each is a function of the original game, under its name. `hooks.add` runs a handler before the
\\function, and `hooks.after` runs one after it. Changing a field of `e` changes what the function
\\does, and a handler that returns `false` stops it.
\\
);
inline for (comptime std.enums.values(Hook)) |hook| {
const declared = comptime engine_hooks.declaration(hook);
if (declared.on == .function and engine_hooks.Fields(hook) != engine_hooks.RoutineFields) try writeHookSection(w, hook);
}
try w.writeAll(
\\
\\## OpenReliant's functions
\\
\\Steps that the original takes inside a larger function, which OpenReliant makes functions of its
\\own so that scripts can hook them. Handlers change them as they change the game's functions.
\\
);
inline for (comptime std.enums.values(Hook)) |hook| {
if (comptime engine_hooks.declaration(hook).on == .engine_function) try writeHookSection(w, hook);
}
try w.writeAll(
\\
\\## The order routines
\\
\\Each order an object follows, such as Fight or Run Away, runs routines of the original game:
\\an `init` as the order starts, an `update` each frame, and for a few an `exit` as it ends. Each
\\routine is a hook under its name. Its handlers see the object that runs the order as `e.object`,
\\and `e.object.order` is the order. Where OpenReliant doesn't run a routine yet, its handlers
\\still run, and the function does nothing.
\\
\\| Hook | What it is |
\\|---|---|
\\
);
inline for (comptime std.enums.values(Hook)) |hook| {
if (comptime engine_hooks.Fields(hook) == engine_hooks.RoutineFields) {
try w.print("| `{s}` | {s} |\n", .{ @tagName(hook), comptime engine_hooks.declaration(hook).about });
}
}
try w.writeAll(
\\
\\## The mission's events
\\
\\The events a mission's triggers can wait for, under the names of their conditions. Each comes
\\for the mission's ships, those its file lists, whether or not a trigger waits for it. Their
\\fields can only be read, and a handler that returns `false` stops the handlers after it.
\\
);
inline for (comptime std.enums.values(Hook)) |hook| {
if (comptime engine_hooks.declaration(hook).on == .mission_event) try writeHookSection(w, hook);
}
try w.writeAll(
\\
\\## The engine's events
\\
\\Their fields can only be read, and a handler that returns `false` stops the handlers after it.
\\
);
inline for (comptime std.enums.values(Hook)) |hook| {
if (comptime engine_hooks.declaration(hook).on == .engine_event) try writeHookSection(w, hook);
}
try w.writeAll(
\\
\\## Tables
\\
\\Values given as tables of fields. Scripts can only read the ones OpenReliant gives them.
\\
);
inline for (gathered.tables) |T| {
if (comptime given(T) and defaulted(T) > 0) {
const left_out = comptime if (defaulted(T) == values.shownFields(T).len) "any field" else "a field with a default";
try w.print("\n### {s}\n\nA table a script gives, which may leave out {s}.\n\n| Field | Type | Default |\n|---|---|---|\n", .{ comptime bind.noun(T), left_out });
inline for (comptime values.shownFields(T)) |field| try w.print("| `{s}` | {s} | {s} |\n", .{ field.name, comptime markdownType(field.type), comptime if (field.defaultValue() != null) defaultText(field) else "needed" });
} else {
try w.print("\n### {s}\n\n| Field | Type |\n|---|---|\n", .{comptime bind.noun(T)});
inline for (comptime values.shownFields(T)) |field| try w.print("| `{s}` | {s} |\n", .{ field.name, comptime markdownType(field.type) });
}
}
try w.writeAll(
\\
\\## Names of values
\\
\\A value that has a name in OpenReliant is given as a string: its name. One without a name is a
\\number. A script can set a field to either.
\\
);
inline for (gathered.enums) |T| {
try w.print("\n### {s}\n\n", .{comptime bind.noun(T)});
inline for (comptime values.names(T), 0..) |name, at| try w.print("{s}`{s}`", .{ if (at == 0) "" else ", ", name });
if (comptime values.takesModNames(T)) try w.writeAll(", the qualified name of one a mod adds, its own name in that mod's scripts");
try w.writeAll(if (comptime values.takesNumbers(T)) ", or a number.\n" else ".\n");
}
}
/// `text` for a cell of a markdown table, its `|` escaped.
fn cell(comptime text: []const u8) []const u8 {
comptime {
var escaped: []const u8 = "";
for (text) |c| escaped = escaped ++ if (c == '|') "\\|" else .{c};
return escaped;
}
}
/// What the declared function `F` returns, for the reference page.
fn markdownResult(comptime F: type) []const u8 {
if (@hasDecl(F, "luau_function")) return cell(F.luau_function);
if (!@hasDecl(F, "Result")) return if (comptime std.mem.eql(u8, F.luau_result, api.nothing)) "nothing" else cell(F.luau_result);
return if (F.Result == void) "nothing" else markdownType(F.Result);
}
/// Writes the names of `families`, as in "global and object".
fn writeFamilyNames(w: *Writer, comptime families: []const script.Family) Writer.Error!void {
inline for (families, 0..) |family, at| {
const separator = if (at == 0) "" else if (at == families.len - 1) " and " else ", ";
try w.print("{s}{t}", .{ separator, family });
}
}
/// The families this version runs that `wanted` holds of.
fn familiesWhere(comptime wanted: fn (script.Family) bool) []const script.Family {
comptime {
var found: []const script.Family = &.{};
for (running_families) |family| {
if (wanted(family)) found = found ++ .{family};
}
return found;
}
}
/// Writes the families of scripts that may give `handler`, among those this version runs.
fn writeFamilies(w: *Writer, comptime handler: script.Handler) Writer.Error!void {
try writeFamilyNames(w, comptime familiesWhere(struct {
fn gives(family: script.Family) bool {
return handler.givenBy(family);
}
}.gives));
}
/// Writes the section of `package` on the reference page.
fn writePackageSection(w: *Writer, comptime package: script.Package) Writer.Error!void {
try w.print("\n### `{s}{t}`\n\n{s} For ", .{ script.Package.prefix, package, package.about() });
try writePackageFamilies(w, package);
try w.writeAll(" scripts.\n");
if (package == .records) inline for (field_tables) |Table| try writeFieldTable(w, Table);
const declared = comptime packages.namespace(package);
if (declared == null) return;
const Namespace = declared.?;
try w.writeAll("\n| Name | Type | What it is |\n|---|---|---|\n");
inline for (comptime api.declared(Namespace, .field)) |name| {
const field = @field(Namespace, name);
try w.print("| `{s}` | {s} | {s} |\n", .{ name, comptime markdownType(field.Type), field.description });
}
inline for (comptime api.declared(Namespace, .function)) |name| {
const function = @field(Namespace, name);
try w.print("| `{s}` | {s} | {s} |\n", .{ comptime markdownCall(name, function), comptime markdownResult(function), function.description });
}
}
/// Writes the fields of the entries of one of the records' tables (`field_tables`) on the reference
/// page.
fn writeFieldTable(w: *Writer, comptime Table: type) Writer.Error!void {
try w.print("\n{s}, `{s}[n]`, is a `{s}` with these fields:\n\n| Field | Type | What it is |\n|---|---|---|\n", .{ Table.each, Table.list_name, Table.script_name });
inline for (comptime std.enums.values(Table.Field)) |field| {
try w.print("| `{t}` | {s} | {s} |\n", .{ field, comptime markdownType(Table.TypeOf(field)), Table.about(field) });
}
}
/// Writes the families of scripts that can require `package`.
fn writePackageFamilies(w: *Writer, comptime package: script.Package) Writer.Error!void {
try writeFamilyNames(w, comptime familiesWhere(struct {
fn reaches(family: script.Family) bool {
return package.reachableFrom(family);
}
}.reaches));
}
/// Writes the section of `hook` on the reference page.
fn writeHookSection(w: *Writer, comptime hook: Hook) Writer.Error!void {
const declared = comptime engine_hooks.declaration(hook);
try w.print("\n### {s}\n\n{s}\n\n", .{ @tagName(hook), declared.about });
const fields = comptime values.shownFields(declared.Fields);
if (fields.len == 0 and declared.Result == void) return;
try w.writeAll("| Field | Type |\n|---|---|\n");
inline for (fields) |field| try w.print("| `{s}` | {s} |\n", .{ field.name, comptime markdownType(field.type) });
if (declared.Result != void) try w.print("| `result` | {s} |\n", .{comptime markdownType(declared.Result)});
}
/// A type on the reference page: its Luau type, with a link to the names of an enum's values.
fn markdownType(comptime T: type) []const u8 {
comptime {
@setEvalBranchQuota(100_000);
const Plain = switch (@typeInfo(T)) {
.optional => |optional| optional.child,
else => T,
};
const optional = if (Plain == T) "" else ", or nil";
if (Plain == Object or Plain == objects.Handle) return "[object](#objects)" ++ optional;
if (Plain == engine_hooks.Missile or Plain == missiles.Handle) return "[missile](#missiles)" ++ optional;
if (Plain == engine_hooks.Turret or Plain == turrets.Handle) return "[turret](#turrets)" ++ optional;
if (values.isList(Plain)) return "list of " ++ (if (Plain.Item == Object) "[objects](#objects)" else markdownType(Plain.Item)) ++ optional;
if (Plain == data.Data) return "plain data";
if (std.mem.findScalar(type, gathered.enums ++ gathered.tables, Plain) != null) {
const name = bind.noun(Plain);
var anchor: []const u8 = "";
for (name) |c| anchor = anchor ++ .{std.ascii.toLower(c)};
return "[" ++ name ++ "](#" ++ anchor ++ ")" ++ optional;
}
return cell(luauType(Plain)) ++ optional;
}
}
/// Writes the list of hooks `openreliant hooks` prints: every hook, or only the one named `only`.
/// Returns false if there's no hook of that name.
pub fn writeList(w: *Writer, only: ?[]const u8) Writer.Error!bool {
@setEvalBranchQuota(1_000_000);
var found = false;
var last: ?engine_hooks.On = null;
inline for (comptime std.enums.values(Hook)) |hook| {
const declared = comptime engine_hooks.declaration(hook);
const matches = if (only) |name| std.mem.eql(u8, name, @tagName(hook)) else true;
if (matches) {
found = true;
if (only == null and last != declared.on) {
if (last != null) try w.writeAll("\n");
try w.writeAll(switch (declared.on) {
.function => "The game's functions. hooks.add runs a handler before the function, hooks.after after it.\n",
.engine_function => "OpenReliant's own functions, steps that the original takes inside a larger function. Handlers change them as they change the game's functions.\n",
.mission_event => "The mission's events, for the mission's ships. Their fields can only be read.\n",
.engine_event => "The engine's events. Their fields can only be read.\n",
});
last = declared.on;
}
try w.print("\n{s}", .{@tagName(hook)});
if (declared.address) |address| try w.print(" (0x{X:0>8})", .{address});
try w.print("\n {s}\n", .{declared.about});
inline for (comptime values.shownFields(declared.Fields)) |field| {
try w.print(" e.{s}: {s}", .{ field.name, comptime luauType(field.type) });
try writeChoices(w, field.type);
try w.writeAll("\n");
}
if (declared.Result != void) try w.print(" e.result: {s}\n", .{comptime luauType(declared.Result)});
}
}
return found;
}
/// Writes what the console's `help` says of `name`: a package, with or without `openreliant.`
/// before it, an engine handler or a hook. Returns false if nothing has that name.
pub fn writeHelp(w: *Writer, name: []const u8) Writer.Error!bool {
@setEvalBranchQuota(1_000_000);
const package_name = if (std.mem.startsWith(u8, name, script.Package.prefix)) name[script.Package.prefix.len..] else name;
inline for (comptime std.enums.values(script.Package)) |package| {
if (std.mem.eql(u8, package_name, @tagName(package))) {
try writePackageHelp(w, package);
return true;
}
}
inline for (comptime std.enums.values(script.Handler)) |handler| {
if (comptime called(handler)) if (std.mem.eql(u8, name, @tagName(handler))) {
try w.print("{t}({s}) -> {s}\n {s}\n For ", .{ handler, comptime handlerParameters(handler), comptime luauType(handler.Result()), handler.about() });
try writeFamilies(w, handler);
try w.writeAll(" scripts.\n");
return true;
};
}
return writeList(w, name);
}
/// Writes what `help` says of `package`: what it holds, who can require it, and its fields and
/// functions.
fn writePackageHelp(w: *Writer, comptime package: script.Package) Writer.Error!void {
try w.print("{s}{t}\n {s}\n For ", .{ script.Package.prefix, package, package.about() });
try writePackageFamilies(w, package);
try w.writeAll(" scripts.\n");
const declared = comptime packages.namespace(package);
if (declared == null) return;
const Namespace = declared.?;
inline for (comptime api.declared(Namespace, .field)) |field_name| {
const field = @field(Namespace, field_name);
try w.print("{s}: {s}\n {s}\n", .{ field_name, comptime luauType(field.Type), field.description });
}
inline for (comptime api.declared(Namespace, .function)) |function_name| {
const function = @field(Namespace, function_name);
try w.print("{s}\n {s}\n", .{ comptime helpCall(function_name, function), function.description });
}
}
/// The most names of an enum's values that `openreliant hooks` lists after a field's type.
const listed_names = 12;
/// Writes the names a field's enum takes, where there are few.
fn writeChoices(w: *Writer, comptime T: type) Writer.Error!void {
const Plain = switch (@typeInfo(T)) {
.optional => |optional| optional.child,
else => T,
};
if (comptime @typeInfo(Plain) != .@"enum" or Plain == Object) return;
const names = comptime values.names(Plain);
if (names.len > listed_names) return;
try w.writeAll(" (");
inline for (names, 0..) |name, at| try w.print("{s}\"{s}\"", .{ if (at == 0) "" else ", ", name });
if (comptime values.takesModNames(Plain)) try w.writeAll(", a mod's qualified name, its own in that mod's scripts");
if (comptime values.takesNumbers(Plain)) try w.writeAll(", or a number");
try w.writeAll(")");
}
test writeHelp {
var buffer: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer buffer.deinit();
try std.testing.expect(try writeHelp(&buffer.writer, "openreliant.storage"));
try std.testing.expect(std.mem.find(u8, buffer.written(), "game_section(name: string) -> Section") != null);
buffer.clearRetainingCapacity();
try std.testing.expect(try writeHelp(&buffer.writer, "on_update"));
try std.testing.expect(std.mem.startsWith(u8, buffer.written(), "on_update(seconds: number) -> ()"));
buffer.clearRetainingCapacity();
try std.testing.expect(try writeHelp(&buffer.writer, "object_damage"));
try std.testing.expect(std.mem.find(u8, buffer.written(), "e.object") != null);
try std.testing.expect(!try writeHelp(&buffer.writer, "no_such_thing"));
}
test "the definitions don't change unless the scripting API does" {
var buffer: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer buffer.deinit();
try writeDefinitions(&buffer.writer);