@amxts/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/README.ru.md +162 -0
- package/amxmodx/base/include/amxconst.inc +594 -0
- package/amxmodx/base/include/amxmisc.inc +902 -0
- package/amxmodx/base/include/amxmodx.inc +3447 -0
- package/amxmodx/base/include/amxmodx_version.inc +20 -0
- package/amxmodx/base/include/cellarray.inc +525 -0
- package/amxmodx/base/include/cellstack.inc +166 -0
- package/amxmodx/base/include/celltrie.inc +412 -0
- package/amxmodx/base/include/core.inc +202 -0
- package/amxmodx/base/include/csstats.inc +298 -0
- package/amxmodx/base/include/csstats_const.inc +29 -0
- package/amxmodx/base/include/cstrike.inc +1287 -0
- package/amxmodx/base/include/cstrike_const.inc +521 -0
- package/amxmodx/base/include/csx.inc +259 -0
- package/amxmodx/base/include/cvars.inc +547 -0
- package/amxmodx/base/include/datapack.inc +161 -0
- package/amxmodx/base/include/dbi.inc +154 -0
- package/amxmodx/base/include/dodconst.inc +152 -0
- package/amxmodx/base/include/dodfun.inc +152 -0
- package/amxmodx/base/include/dodstats.inc +69 -0
- package/amxmodx/base/include/dodx.inc +161 -0
- package/amxmodx/base/include/engine.inc +1343 -0
- package/amxmodx/base/include/engine_const.inc +310 -0
- package/amxmodx/base/include/engine_stocks.inc +248 -0
- package/amxmodx/base/include/esf.inc +70 -0
- package/amxmodx/base/include/esf_const.inc +74 -0
- package/amxmodx/base/include/fakemeta.inc +1127 -0
- package/amxmodx/base/include/fakemeta_const.inc +793 -0
- package/amxmodx/base/include/fakemeta_stocks.inc +395 -0
- package/amxmodx/base/include/fakemeta_util.inc +882 -0
- package/amxmodx/base/include/file.inc +666 -0
- package/amxmodx/base/include/float.inc +422 -0
- package/amxmodx/base/include/fun.inc +333 -0
- package/amxmodx/base/include/gameconfig.inc +93 -0
- package/amxmodx/base/include/geoip.inc +264 -0
- package/amxmodx/base/include/ham_const.inc +4352 -0
- package/amxmodx/base/include/hamsandwich.inc +439 -0
- package/amxmodx/base/include/hlsdk_const.inc +808 -0
- package/amxmodx/base/include/json.inc +777 -0
- package/amxmodx/base/include/lang.inc +119 -0
- package/amxmodx/base/include/message_const.inc +1130 -0
- package/amxmodx/base/include/message_stocks.inc +161 -0
- package/amxmodx/base/include/messages.inc +604 -0
- package/amxmodx/base/include/newmenus.inc +387 -0
- package/amxmodx/base/include/ns.inc +754 -0
- package/amxmodx/base/include/ns_const.inc +196 -0
- package/amxmodx/base/include/nvault.inc +154 -0
- package/amxmodx/base/include/regex.inc +348 -0
- package/amxmodx/base/include/sockets.inc +183 -0
- package/amxmodx/base/include/sorting.inc +107 -0
- package/amxmodx/base/include/sqlx.inc +582 -0
- package/amxmodx/base/include/string.inc +786 -0
- package/amxmodx/base/include/string_const.inc +156 -0
- package/amxmodx/base/include/string_stocks.inc +325 -0
- package/amxmodx/base/include/textparse_ini.inc +214 -0
- package/amxmodx/base/include/textparse_smc.inc +261 -0
- package/amxmodx/base/include/tfcconst.inc +84 -0
- package/amxmodx/base/include/tfcstats.inc +62 -0
- package/amxmodx/base/include/tfcx.inc +148 -0
- package/amxmodx/base/include/time.inc +118 -0
- package/amxmodx/base/include/tsconst.inc +154 -0
- package/amxmodx/base/include/tsfun.inc +198 -0
- package/amxmodx/base/include/tsstats.inc +65 -0
- package/amxmodx/base/include/tsx.inc +88 -0
- package/amxmodx/base/include/vault.inc +60 -0
- package/amxmodx/base/include/vector.inc +131 -0
- package/amxmodx/base/include/xs.inc +1570 -0
- package/as/amxts.d.ts +153 -0
- package/as/constants.ts +4730 -0
- package/as/effects.ts +1449 -0
- package/as/entities.ts +5482 -0
- package/as/events.ts +3054 -0
- package/as/facade.ts +4639 -0
- package/as/flags.ts +299 -0
- package/as/fs.ts +287 -0
- package/as/hooks.ts +18536 -0
- package/as/kit.ts +25 -0
- package/as/lib/check.ts +86 -0
- package/as/modules/http.ts +184 -0
- package/as/natives.ts +11845 -0
- package/as/os.ts +30 -0
- package/as/promise.ts +1392 -0
- package/as/promise.types.d.ts +82 -0
- package/as/remote.ts +288 -0
- package/as/tsconfig.json +22 -0
- package/as/vector.ts +65 -0
- package/bin/amxts.mjs +10 -0
- package/includes/menu_core.inc +355 -0
- package/includes/order.txt +54 -0
- package/includes/sources.json +27 -0
- package/includes/universal_config.inc +355 -0
- package/lint/oxlint-plugin.mjs +77 -0
- package/node_modules/assemblyscript/LICENSE +201 -0
- package/node_modules/assemblyscript/NOTICE +93 -0
- package/node_modules/assemblyscript/package.json +63 -0
- package/node_modules/assemblyscript/std/README.md +6 -0
- package/node_modules/assemblyscript/std/assembly/array.ts +526 -0
- package/node_modules/assemblyscript/std/assembly/arraybuffer.ts +77 -0
- package/node_modules/assemblyscript/std/assembly/atomics.ts +127 -0
- package/node_modules/assemblyscript/std/assembly/bindings/asyncify.ts +16 -0
- package/node_modules/assemblyscript/std/assembly/bindings/dom.ts +291 -0
- package/node_modules/assemblyscript/std/assembly/bindings/node.ts +6 -0
- package/node_modules/assemblyscript/std/assembly/builtins.ts +2631 -0
- package/node_modules/assemblyscript/std/assembly/compat.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/console.ts +42 -0
- package/node_modules/assemblyscript/std/assembly/crypto.ts +9 -0
- package/node_modules/assemblyscript/std/assembly/dataview.ts +181 -0
- package/node_modules/assemblyscript/std/assembly/date.ts +373 -0
- package/node_modules/assemblyscript/std/assembly/diagnostics.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/error.ts +44 -0
- package/node_modules/assemblyscript/std/assembly/function.ts +38 -0
- package/node_modules/assemblyscript/std/assembly/index.d.ts +2822 -0
- package/node_modules/assemblyscript/std/assembly/iterator.ts +35 -0
- package/node_modules/assemblyscript/std/assembly/map.ts +260 -0
- package/node_modules/assemblyscript/std/assembly/math.ts +3289 -0
- package/node_modules/assemblyscript/std/assembly/memory.ts +123 -0
- package/node_modules/assemblyscript/std/assembly/number.ts +388 -0
- package/node_modules/assemblyscript/std/assembly/object.ts +36 -0
- package/node_modules/assemblyscript/std/assembly/performance.ts +9 -0
- package/node_modules/assemblyscript/std/assembly/polyfills.ts +27 -0
- package/node_modules/assemblyscript/std/assembly/process.ts +50 -0
- package/node_modules/assemblyscript/std/assembly/reference.ts +48 -0
- package/node_modules/assemblyscript/std/assembly/regexp.ts +12 -0
- package/node_modules/assemblyscript/std/assembly/rt/README.md +83 -0
- package/node_modules/assemblyscript/std/assembly/rt/common.ts +81 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-incremental.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-minimal.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-stub.ts +1 -0
- package/node_modules/assemblyscript/std/assembly/rt/index.d.ts +37 -0
- package/node_modules/assemblyscript/std/assembly/rt/itcms.ts +419 -0
- package/node_modules/assemblyscript/std/assembly/rt/rtrace.ts +15 -0
- package/node_modules/assemblyscript/std/assembly/rt/stub.ts +133 -0
- package/node_modules/assemblyscript/std/assembly/rt/tcms.ts +254 -0
- package/node_modules/assemblyscript/std/assembly/rt/tlsf.ts +592 -0
- package/node_modules/assemblyscript/std/assembly/rt.ts +87 -0
- package/node_modules/assemblyscript/std/assembly/set.ts +220 -0
- package/node_modules/assemblyscript/std/assembly/shared/feature.ts +65 -0
- package/node_modules/assemblyscript/std/assembly/shared/runtime.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/target.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/tsconfig.json +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/typeinfo.ts +72 -0
- package/node_modules/assemblyscript/std/assembly/staticarray.ts +401 -0
- package/node_modules/assemblyscript/std/assembly/string.ts +847 -0
- package/node_modules/assemblyscript/std/assembly/symbol.ts +114 -0
- package/node_modules/assemblyscript/std/assembly/table.ts +16 -0
- package/node_modules/assemblyscript/std/assembly/tsconfig.json +6 -0
- package/node_modules/assemblyscript/std/assembly/typedarray.ts +1945 -0
- package/node_modules/assemblyscript/std/assembly/uri.ts +17 -0
- package/node_modules/assemblyscript/std/assembly/util/bytes.ts +107 -0
- package/node_modules/assemblyscript/std/assembly/util/casemap.ts +497 -0
- package/node_modules/assemblyscript/std/assembly/util/error.ts +54 -0
- package/node_modules/assemblyscript/std/assembly/util/hash.ts +117 -0
- package/node_modules/assemblyscript/std/assembly/util/math.ts +1922 -0
- package/node_modules/assemblyscript/std/assembly/util/memory.ts +290 -0
- package/node_modules/assemblyscript/std/assembly/util/number.ts +873 -0
- package/node_modules/assemblyscript/std/assembly/util/sort.ts +313 -0
- package/node_modules/assemblyscript/std/assembly/util/string.ts +1202 -0
- package/node_modules/assemblyscript/std/assembly/util/uri.ts +275 -0
- package/node_modules/assemblyscript/std/assembly/vector.ts +4 -0
- package/node_modules/assemblyscript/std/assembly.json +16 -0
- package/node_modules/assemblyscript/std/portable/index.d.ts +462 -0
- package/node_modules/assemblyscript/std/portable/index.js +416 -0
- package/node_modules/assemblyscript/std/portable.json +11 -0
- package/node_modules/assemblyscript/std/types/assembly/index.d.ts +1 -0
- package/node_modules/assemblyscript/std/types/assembly/package.json +3 -0
- package/node_modules/assemblyscript/std/types/portable/index.d.ts +1 -0
- package/node_modules/assemblyscript/std/types/portable/package.json +3 -0
- package/node_modules/assemblyscript/tsconfig-base.json +13 -0
- package/package.json +112 -0
- package/runtime/deps/assemblyscript/LICENSE +201 -0
- package/runtime/deps/assemblyscript/NOTICE +93 -0
- package/runtime/deps/assemblyscript/bin/asc.js +35 -0
- package/runtime/deps/assemblyscript/dist/asc.js +25035 -0
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +337 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/LICENSE +201 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/index.js +28905 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/package.json +50 -0
- package/runtime/deps/assemblyscript/node_modules/long/LICENSE +202 -0
- package/runtime/deps/assemblyscript/node_modules/long/index.js +1581 -0
- package/runtime/deps/assemblyscript/node_modules/long/package.json +58 -0
- package/runtime/deps/assemblyscript/package.json +114 -0
- package/runtime/host/amxts.inc +85 -0
- package/runtime/licenses/AMXX-LICENSE.txt +34 -0
- package/runtime/licenses/AssemblyScript-LICENSE +201 -0
- package/runtime/licenses/AssemblyScript-NOTICE +93 -0
- package/runtime/licenses/Binaryen-LICENSE +201 -0
- package/runtime/licenses/Bun-LICENSE.md +84 -0
- package/runtime/licenses/GPL-3.0.txt +674 -0
- package/runtime/licenses/LLVM-LICENSE.TXT +279 -0
- package/runtime/licenses/README.md +25 -0
- package/runtime/licenses/WAMR-LICENSE +219 -0
- package/runtime/natives.txt +1095 -0
- package/runtime/patches/README.md +59 -0
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +14174 -0
- package/runtime/patches/wamr-2.4.5-amxts.patch +364 -0
- package/scripts/apply-docs.ts +508 -0
- package/scripts/asc.ts +27 -0
- package/scripts/auto-imports.ts +258 -0
- package/scripts/build-wasm.ts +595 -0
- package/scripts/check.ts +93 -0
- package/scripts/compile-cache.ts +179 -0
- package/scripts/compile.ts +401 -0
- package/scripts/docker-server.ts +73 -0
- package/scripts/docs/as/amxts.ts +210 -0
- package/scripts/docs/as/effects.ts +1684 -0
- package/scripts/docs/as/facade.ts +3302 -0
- package/scripts/docs/as/fs.ts +173 -0
- package/scripts/docs/as/lib/check.ts +68 -0
- package/scripts/docs/as/modules/http.ts +66 -0
- package/scripts/docs/as/os.ts +16 -0
- package/scripts/docs/as/promise.ts +132 -0
- package/scripts/docs/as/promise.types.ts +63 -0
- package/scripts/docs/as/vector.ts +48 -0
- package/scripts/docs/entities.ts +2338 -0
- package/scripts/docs/events.ts +245 -0
- package/scripts/docs/game.ts +497 -0
- package/scripts/docs/messages.ts +148 -0
- package/scripts/file-contents.ts +74 -0
- package/scripts/includes.ts +134 -0
- package/scripts/player-fields.ts +866 -0
- package/scripts/plugin-cache.ts +92 -0
- package/scripts/plugin-natives.ts +1157 -0
- package/scripts/prebuilt.ts +268 -0
- package/scripts/prepare.ts +134 -0
- package/scripts/project.ts +973 -0
- package/scripts/run.ts +28 -0
- package/scripts/shared-modules.ts +906 -0
- package/scripts/system.ts +33 -0
- package/scripts/tracked-fs.ts +115 -0
- package/scripts/typed-configs.ts +788 -0
- package/scripts/ui.ts +77 -0
- package/src/cli-api.mjs +175 -0
- package/src/parser/include-parser.ts +506 -0
- package/src/system.mjs +155 -0
- package/src/testing/compile-cache.ts +31 -0
- package/src/testing/compile.ts +222 -0
- package/src/testing/coroutines.ts +191 -0
- package/src/testing/index.ts +39 -0
- package/src/testing/kits.ts +55 -0
- package/src/testing/memory.ts +125 -0
- package/src/testing/natives.ts +925 -0
- package/src/testing/server.ts +1874 -0
- package/src/testing/setup.ts +63 -0
- package/src/testing/tables.ts +139 -0
- package/src/types.ts +51 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
// Fields on Player that plugins add, shared by all of them.
|
|
2
|
+
//
|
|
3
|
+
// A plugin declares its fields as TypeScript does, by augmenting the facade's
|
|
4
|
+
// Player:
|
|
5
|
+
//
|
|
6
|
+
// import "~/facade";
|
|
7
|
+
// declare module "~/facade" {
|
|
8
|
+
// interface Player {
|
|
9
|
+
// spawnProtected: boolean;
|
|
10
|
+
// glow: {
|
|
11
|
+
// enabled: true | false | "default";
|
|
12
|
+
// seenBy: Player[];
|
|
13
|
+
// };
|
|
14
|
+
// }
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// and reads and writes them as properties: `if (player.spawnProtected) ...`,
|
|
18
|
+
// `player.spawnProtected = true`, `player.glow.enabled = true`. The values live in
|
|
19
|
+
// the module, by player slot and field name, so every plugin - and Pawn,
|
|
20
|
+
// through the amxts_*_player_data natives - sees the same ones. A member of an
|
|
21
|
+
// object field is stored under a dotted key, "glow.enabled".
|
|
22
|
+
//
|
|
23
|
+
// A field or member is boolean, number, string, a union of string literals
|
|
24
|
+
// (with true and false among them if it likes - "true | false | "default""
|
|
25
|
+
// is one value, see Parser.parseType), or Player[]. A field may also be an
|
|
26
|
+
// object of those: written inline, or an interface declared in the same file.
|
|
27
|
+
//
|
|
28
|
+
// AssemblyScript has neither module augmentation nor a property looked up by a
|
|
29
|
+
// name held in a string, so the build does both:
|
|
30
|
+
//
|
|
31
|
+
// - reading a file (`read`), it records the members of every
|
|
32
|
+
// `interface Player` in a `declare module "~/facade"` block and blanks the
|
|
33
|
+
// block out - spaces, so every line and column after it stays where it was.
|
|
34
|
+
// An interface of the same file that a field is typed with becomes
|
|
35
|
+
// `import { Name } from "~/facade";` (and `export { Name };` if it was
|
|
36
|
+
// exported): the build makes
|
|
37
|
+
// the class of that name in the facade, so the file's own uses of it are
|
|
38
|
+
// the same type as the field;
|
|
39
|
+
// - once everything the plugin imports is parsed (`afterParse`), it parses a
|
|
40
|
+
// hidden library file: global helpers over the module's store, a holder
|
|
41
|
+
// class with one getter and one setter per field, and a class per object
|
|
42
|
+
// field. The holder's members are then moved into the facade's
|
|
43
|
+
// `class Player` declaration and the holder itself is dropped, so the
|
|
44
|
+
// accessors are Player's own members - no other class, no
|
|
45
|
+
// `player.something.` in between. The object classes move into the
|
|
46
|
+
// facade's file. Their bodies are resolved in the facade's scope, which is
|
|
47
|
+
// why the helpers they call are @global.
|
|
48
|
+
//
|
|
49
|
+
// An object field reads as a live view: `player.glow.enabled = true`
|
|
50
|
+
// writes the store at once, and `player.glow = { ... }` writes every
|
|
51
|
+
// member. The same class made by an object literal holds its own values until
|
|
52
|
+
// it is assigned to a player. A Player[] reads as a list whose `push` writes
|
|
53
|
+
// back, as a flag list does (FlagList); the module takes a player who leaves
|
|
54
|
+
// out of every such list.
|
|
55
|
+
//
|
|
56
|
+
// A field whose name Player already has - `solid` is Entity's var_solid - is
|
|
57
|
+
// refused: it would silently replace the engine's property.
|
|
58
|
+
//
|
|
59
|
+
// A change is an event, `"playerchange"`, which the module raises on every
|
|
60
|
+
// write that changes a value (runtime/src/module.cpp). With
|
|
61
|
+
// `{ field: "spawnProtected" }` its listener gets the field's own event class, whose `value` and `previous`
|
|
62
|
+
// have the field's type - one per field, per object member and per object
|
|
63
|
+
// field, made here. The compiler binds an event's type to the name in the
|
|
64
|
+
// call (`K extends keyof ServerEventMap`), and the field is in the options, so
|
|
65
|
+
// the call's name becomes `"playerchange:spawnProtected"` and ServerEventMap is given
|
|
66
|
+
// that key; `PlayerChangeEvent<"spawnProtected">` written as a type becomes the class.
|
|
67
|
+
|
|
68
|
+
// @ts-ignore - shipped as JavaScript, with types beside it we do not need here
|
|
69
|
+
import * as asc from '../runtime/deps/assemblyscript/dist/assemblyscript.js';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What one value is: a primitive, `Player[]` ('players'), or a union of
|
|
73
|
+
* string literals - with true and false among them when `boolean` is set.
|
|
74
|
+
* Until written, a union reads its first literal.
|
|
75
|
+
*/
|
|
76
|
+
export type ValueType
|
|
77
|
+
= | 'boolean' | 'number' | 'string' | 'players'
|
|
78
|
+
| { literals: string[]; boolean: boolean };
|
|
79
|
+
|
|
80
|
+
export interface ObjectMember {
|
|
81
|
+
name: string;
|
|
82
|
+
type: ValueType;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** An object field: its members, and the interface it was declared as, if any. */
|
|
86
|
+
export interface ObjectType {
|
|
87
|
+
members: ObjectMember[];
|
|
88
|
+
interface: string | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type FieldType = ValueType | ObjectType;
|
|
92
|
+
|
|
93
|
+
export interface PlayerField {
|
|
94
|
+
name: string;
|
|
95
|
+
type: FieldType;
|
|
96
|
+
/** Where it was declared: `file:line`. */
|
|
97
|
+
where: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Where the generated accessors are parsed: a library file, so the hood's rules apply to them. */
|
|
101
|
+
export const PLAYER_FIELDS_FILE = '~lib/amxts/player-fields.ts';
|
|
102
|
+
|
|
103
|
+
/** The holder class in that file whose members go into Player. */
|
|
104
|
+
const HOLDER = '__AmxtsPlayerFields';
|
|
105
|
+
|
|
106
|
+
/** The list a Player[] field reads as. */
|
|
107
|
+
const PLAYER_LIST = '__AmxtsPlayerList';
|
|
108
|
+
|
|
109
|
+
/** The event a field's change is, and the facade's class for it. */
|
|
110
|
+
const CHANGE_EVENT = 'playerchange';
|
|
111
|
+
const CHANGE_CLASS = 'PlayerChangeEvent';
|
|
112
|
+
|
|
113
|
+
/** The interface in that file whose members - `"playerchange:spawnProtected": ...` - go into ServerEventMap. */
|
|
114
|
+
const CHANGES = '__AmxtsPlayerChanges';
|
|
115
|
+
|
|
116
|
+
const DECLARE_MODULE = /\bdeclare\s+module\s*(["'])~\/facade\1\s*\{/g;
|
|
117
|
+
|
|
118
|
+
const KINDS = 'boolean, number, string, a union of string literals (true and false may be among them), Player[], or an object of those';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* One compile's fields. `read` goes into the compile's readFile, `transform`
|
|
122
|
+
* into its transforms; a new one for every compile.
|
|
123
|
+
*/
|
|
124
|
+
export function playerFieldsBuild() {
|
|
125
|
+
const fields = new Map<string, PlayerField>();
|
|
126
|
+
const problems: string[] = [];
|
|
127
|
+
|
|
128
|
+
function read(path: string, text: string): string {
|
|
129
|
+
const file = path.replace(/\\/g, '/');
|
|
130
|
+
if (!text.includes('declare') || !/\bdeclare\s+module\b/.test(text)) return text;
|
|
131
|
+
|
|
132
|
+
const blocks: { start: number; open: number; close: number }[] = [];
|
|
133
|
+
for (const match of text.matchAll(DECLARE_MODULE)) {
|
|
134
|
+
const open = match.index! + match[0].length - 1;
|
|
135
|
+
const close = closingBrace(text, open);
|
|
136
|
+
if (close < 0) {
|
|
137
|
+
problems.push(`${file}:${lineOf(text, match.index!)}: declare module "~/facade" - no closing brace`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
blocks.push({ start: match.index!, open, close });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const local = localInterfaces(text, blocks);
|
|
144
|
+
const used = new Set<string>();
|
|
145
|
+
for (const block of blocks) collect(file, text, block.open + 1, block.close, local, used);
|
|
146
|
+
|
|
147
|
+
// Last to first, so an earlier span's offsets still hold.
|
|
148
|
+
const spans = [
|
|
149
|
+
...blocks.map(block => ({ start: block.start, end: block.close + 1, with: '' })),
|
|
150
|
+
...[...used].map((name) => {
|
|
151
|
+
const found = local.get(name)!;
|
|
152
|
+
const imported = `import { ${name} } from "~/facade";`;
|
|
153
|
+
return { start: found.start, end: found.end, with: found.exported ? `${imported} export { ${name} };` : imported };
|
|
154
|
+
}),
|
|
155
|
+
].sort((a, b) => b.start - a.start);
|
|
156
|
+
|
|
157
|
+
let out = text;
|
|
158
|
+
for (const span of spans) {
|
|
159
|
+
const original = out.slice(span.start, span.end);
|
|
160
|
+
const replaced = span.with ? span.with + (original.match(/\r?\n/g) ?? []).join('') : blank(original);
|
|
161
|
+
out = out.slice(0, span.start) + replaced + out.slice(span.end);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function collect(file: string, text: string, from: number, to: number, local: Map<string, LocalInterface>, used: Set<string>) {
|
|
167
|
+
let body = withoutComments(text.slice(from, to));
|
|
168
|
+
const found: { start: number; end: number }[] = [];
|
|
169
|
+
for (const match of body.matchAll(/\b(?:export\s+)?interface\s+Player\s*\{/g)) {
|
|
170
|
+
const open = match.index! + match[0].length - 1;
|
|
171
|
+
const close = closingBrace(body, open);
|
|
172
|
+
if (close < 0) continue;
|
|
173
|
+
found.push({ start: match.index!, end: close + 1 });
|
|
174
|
+
for (const piece of members(body, open + 1, close)) {
|
|
175
|
+
add(piece.text, `${file}:${lineOf(text, from + piece.at)}`, local, used);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Anything else in the block would be dropped with it: say so instead.
|
|
180
|
+
for (const span of found) body = body.slice(0, span.start) + blank(body.slice(span.start, span.end)) + body.slice(span.end);
|
|
181
|
+
const rest = body.search(/\S/);
|
|
182
|
+
if (rest >= 0) {
|
|
183
|
+
const what = body.slice(rest).trim().split('\n')[0].trim();
|
|
184
|
+
problems.push(`${file}:${lineOf(text, from + rest)}: declare module "~/facade" - only \`interface Player { ... }\` goes here, not "${what}"`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function add(member: string, where: string, local: Map<string, LocalInterface>, used: Set<string>) {
|
|
189
|
+
const shape = memberShape(member);
|
|
190
|
+
if (!shape) {
|
|
191
|
+
problems.push(`${where}: Player - "${member}" is not a field`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const { name, optional, written } = shape;
|
|
195
|
+
if (optional) {
|
|
196
|
+
problems.push(`${where}: Player.${name} - a field is never missing: it reads false, 0 or "" until written; drop the ?`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (name.startsWith('__')) {
|
|
200
|
+
problems.push(`${where}: Player.${name} - names starting with __ are the hood's`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const type = fieldType(written, local);
|
|
205
|
+
if (typeof type === 'string' && type.startsWith('!')) {
|
|
206
|
+
problems.push(`${where}: Player.${name}${type.slice(1)}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const resolved = type as FieldType;
|
|
210
|
+
if (isObject(resolved) && resolved.interface) used.add(resolved.interface);
|
|
211
|
+
|
|
212
|
+
const earlier = fields.get(name);
|
|
213
|
+
if (earlier && describe(earlier.type) !== describe(resolved)) {
|
|
214
|
+
problems.push(`${where}: Player.${name} is ${describe(resolved)} here and ${describe(earlier.type)} at ${earlier.where} - one field, one type`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!earlier) fields.set(name, { name, type: resolved, where });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const transform = class {
|
|
221
|
+
afterParse(parser: any): void {
|
|
222
|
+
if (problems.length) throw new Error(problems.join('\n'));
|
|
223
|
+
const listened = typeChangeListeners(parser, new Map(changeEvents([...fields.values()]).map(event => [event.key, event.name])));
|
|
224
|
+
if (listened.length) throw new Error(listened.join('\n'));
|
|
225
|
+
if (!fields.size) return;
|
|
226
|
+
|
|
227
|
+
const { classes, facade, topLevel } = classDeclarations(parser);
|
|
228
|
+
const player = classes.get('Player');
|
|
229
|
+
if (!player) {
|
|
230
|
+
const first = [...fields.values()][0];
|
|
231
|
+
throw new Error(`${first.where}: Player.${first.name} - the facade's Player is not part of this compile; import "~/facade"`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Player's members and those of every class it extends.
|
|
235
|
+
const taken = new Map<string, string>();
|
|
236
|
+
for (let node: any = player; node;) {
|
|
237
|
+
for (const member of node.members) {
|
|
238
|
+
const name = member.name?.text;
|
|
239
|
+
if (name && !taken.has(name)) taken.set(name, node.name.text);
|
|
240
|
+
}
|
|
241
|
+
const base = node.extendsType?.name?.identifier?.text;
|
|
242
|
+
node = base ? classes.get(base) : null;
|
|
243
|
+
}
|
|
244
|
+
const clashes = [...fields.values()].filter(field => taken.has(field.name));
|
|
245
|
+
if (clashes.length) {
|
|
246
|
+
throw new Error(clashes.map(field =>
|
|
247
|
+
`${field.where}: Player.${field.name} - ${taken.get(field.name)} already has ${field.name}; name the field something else`,
|
|
248
|
+
).join('\n'));
|
|
249
|
+
}
|
|
250
|
+
const named = [...fields.values()].filter((field) => {
|
|
251
|
+
const type = field.type;
|
|
252
|
+
return isObject(type) && type.interface && topLevel.has(type.interface);
|
|
253
|
+
});
|
|
254
|
+
if (named.length) {
|
|
255
|
+
throw new Error(named.map(field =>
|
|
256
|
+
`${field.where}: Player.${field.name} - the facade already has a ${(field.type as ObjectType).interface}; name the interface something else`,
|
|
257
|
+
).join('\n'));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
parser.parseFile(playerFieldsSource([...fields.values()]), PLAYER_FIELDS_FILE, false);
|
|
261
|
+
const source = parser.sources.find((s: any) => s.normalizedPath === PLAYER_FIELDS_FILE);
|
|
262
|
+
const at = source.statements.findIndex((s: any) =>
|
|
263
|
+
s.kind === asc.NodeKind.ClassDeclaration && s.name.text === HOLDER);
|
|
264
|
+
const holder = source.statements[at];
|
|
265
|
+
source.statements.splice(at, 1);
|
|
266
|
+
player.members.push(...holder.members);
|
|
267
|
+
|
|
268
|
+
// The change events by the names typeChangeListeners gave the calls. A
|
|
269
|
+
// compile without the server's events has no listener to give them.
|
|
270
|
+
const changes = source.statements.find((s: any) => isNamed(s, CHANGES));
|
|
271
|
+
source.statements = source.statements.filter((s: any) => s !== changes);
|
|
272
|
+
declarationNamed(parser, 'ServerEventMap')?.members.push(...changes.members);
|
|
273
|
+
|
|
274
|
+
// The object classes, the list and the change events use Player: they live in its file.
|
|
275
|
+
const moved = source.statements.filter((s: any) => s.kind === asc.NodeKind.ClassDeclaration);
|
|
276
|
+
source.statements = source.statements.filter((s: any) => !moved.includes(s));
|
|
277
|
+
facade.statements.push(...moved);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// asc takes a transform as a class it instantiates; its own type is not ours to import.
|
|
282
|
+
return { read, transform: transform as any, fields };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface LocalInterface {
|
|
286
|
+
body: string;
|
|
287
|
+
start: number;
|
|
288
|
+
end: number;
|
|
289
|
+
exported: boolean;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The interfaces a file declares at its top level, outside the declare module blocks. */
|
|
293
|
+
function localInterfaces(text: string, blocks: { start: number; close: number }[]): Map<string, LocalInterface> {
|
|
294
|
+
const found = new Map<string, LocalInterface>();
|
|
295
|
+
const plain = withoutComments(text);
|
|
296
|
+
for (const match of plain.matchAll(/\b(export\s+)?interface\s+([A-Za-z_$][\w$]*)\s*\{/g)) {
|
|
297
|
+
const start = match.index!;
|
|
298
|
+
if (blocks.some(block => start > block.start && start < block.close)) continue;
|
|
299
|
+
const open = start + match[0].length - 1;
|
|
300
|
+
const close = closingBrace(plain, open);
|
|
301
|
+
if (close < 0) continue;
|
|
302
|
+
found.set(match[2], { body: plain.slice(open + 1, close), start, end: close + 1, exported: !!match[1] });
|
|
303
|
+
}
|
|
304
|
+
return found;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The members between `from` and `to`, each with where it starts. A member
|
|
309
|
+
* ends at `;` or `,` outside braces, or at the end of a line that does not go
|
|
310
|
+
* on - `mode:` and a union spread over lines stay one member.
|
|
311
|
+
*/
|
|
312
|
+
function members(text: string, from: number, to: number): { text: string; at: number }[] {
|
|
313
|
+
const found: { text: string; at: number }[] = [];
|
|
314
|
+
let depth = 0;
|
|
315
|
+
let start = from;
|
|
316
|
+
const flush = (end: number) => {
|
|
317
|
+
const piece = text.slice(start, end);
|
|
318
|
+
const member = piece.trim();
|
|
319
|
+
if (member) found.push({ text: member, at: start + piece.length - piece.trimStart().length });
|
|
320
|
+
start = end + 1;
|
|
321
|
+
};
|
|
322
|
+
for (let i = from; i < to; i++) {
|
|
323
|
+
const c = text[i];
|
|
324
|
+
if (c === '"' || c === '\'' || c === '`') {
|
|
325
|
+
for (i++; i < to && text[i] !== c; i++) {
|
|
326
|
+
if (text[i] === '\\') i++;
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (c === '{' || c === '(' || c === '[' || c === '<') depth++;
|
|
331
|
+
if (c === '}' || c === ')' || c === ']' || c === '>') depth--;
|
|
332
|
+
if (depth > 0) continue;
|
|
333
|
+
if (c === ';' || c === ',') flush(i);
|
|
334
|
+
if (c === '\n') {
|
|
335
|
+
const sofar = text.slice(start, i).trim();
|
|
336
|
+
const next = text.slice(i + 1, to).trimStart();
|
|
337
|
+
if (sofar && !/[:|&]$/.test(sofar) && !/^[|&]/.test(next)) flush(i);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
flush(to);
|
|
341
|
+
return found;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function memberShape(member: string) {
|
|
345
|
+
const shape = member.match(/^(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*(?:(\?)\s*)?:\s*(\S[\s\S]*)$/);
|
|
346
|
+
return shape ? { name: shape[1], optional: !!shape[2], written: shape[3].trim() } : null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** A field's type, or "!..." - what is wrong with it, to follow `Player.name`. */
|
|
350
|
+
function fieldType(written: string, local: Map<string, LocalInterface>): FieldType | string {
|
|
351
|
+
const type = written.replace(/\s+/g, ' ').trim();
|
|
352
|
+
const inline = type.startsWith('{') && type.endsWith('}');
|
|
353
|
+
const name = local.has(type) ? type : null;
|
|
354
|
+
if (!inline && !name) return valueType(type) ?? `!: ${type} - a field is ${KINDS}`;
|
|
355
|
+
|
|
356
|
+
const body = inline ? written.trim().slice(1, -1) : local.get(name!)!.body;
|
|
357
|
+
const object: ObjectType = { members: [], interface: name };
|
|
358
|
+
for (const piece of members(body, 0, body.length)) {
|
|
359
|
+
const shape = memberShape(piece.text);
|
|
360
|
+
if (!shape) return `! - "${piece.text}" is not a member`;
|
|
361
|
+
if (shape.optional) return `!.${shape.name} - a member is never missing: drop the ?`;
|
|
362
|
+
if (shape.name.startsWith('__')) return `!.${shape.name} - names starting with __ are the hood's`;
|
|
363
|
+
const member = valueType(shape.written.replace(/\s+/g, ' ').trim());
|
|
364
|
+
if (!member) return `!.${shape.name}: ${shape.written.replace(/\s+/g, ' ').trim()} - a member is ${KINDS.replace(', or an object of those', ' - not an object')}`;
|
|
365
|
+
object.members.push({ name: shape.name, type: member });
|
|
366
|
+
}
|
|
367
|
+
if (!object.members.length) return `!: ${type} - an object field has members`;
|
|
368
|
+
return object;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function valueType(type: string): ValueType | null {
|
|
372
|
+
if (type === 'boolean' || type === 'number' || type === 'string') return type;
|
|
373
|
+
if (type === 'Player[]' || type === 'Array<Player>') return 'players';
|
|
374
|
+
|
|
375
|
+
const parts = type.replace(/^\|/, '').split('|').map(part => part.trim());
|
|
376
|
+
if (parts.length < 2) return null;
|
|
377
|
+
const literals: string[] = [];
|
|
378
|
+
let boolean = false;
|
|
379
|
+
for (const part of parts) {
|
|
380
|
+
if (part === 'true' || part === 'false' || part === 'boolean') {
|
|
381
|
+
boolean = true;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const literal = part.match(/^"([^"\\]*)"$|^'([^'\\]*)'$/);
|
|
385
|
+
if (!literal) return null;
|
|
386
|
+
literals.push(literal[1] ?? literal[2]);
|
|
387
|
+
}
|
|
388
|
+
if (!literals.length) return 'boolean';
|
|
389
|
+
return { literals, boolean };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isObject(type: FieldType): type is ObjectType {
|
|
393
|
+
return typeof type === 'object' && 'members' in type;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** A type as it is written, to compare two declarations and to name one. */
|
|
397
|
+
export function describe(type: FieldType): string {
|
|
398
|
+
if (type === 'players') return 'Player[]';
|
|
399
|
+
if (typeof type === 'string') return type;
|
|
400
|
+
if (isObject(type)) return `{ ${type.members.map(member => `${member.name}: ${describe(member.type)}`).join('; ')} }`;
|
|
401
|
+
return [...(type.boolean ? ['true', 'false'] : []), ...type.literals.map(literal => JSON.stringify(literal))].join(' | ');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The facade's classes by name: Player and what it extends, the file Player
|
|
406
|
+
* is in, and every name declared at its top level. Only a source that
|
|
407
|
+
* declares `class Player extends PlayerFields` counts as the facade, so a
|
|
408
|
+
* fixture's own class Player cannot take its place.
|
|
409
|
+
*/
|
|
410
|
+
function classDeclarations(parser: any) {
|
|
411
|
+
const classes = new Map<string, any>();
|
|
412
|
+
let player: any = null;
|
|
413
|
+
let facade: any = null;
|
|
414
|
+
for (const source of parser.sources) {
|
|
415
|
+
for (const statement of source.statements) {
|
|
416
|
+
if (statement.kind !== asc.NodeKind.ClassDeclaration) continue;
|
|
417
|
+
const name = statement.name.text;
|
|
418
|
+
if (name === 'Player') {
|
|
419
|
+
if (statement.extendsType?.name?.identifier?.text === 'PlayerFields') {
|
|
420
|
+
player = statement;
|
|
421
|
+
facade = source;
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (!classes.has(name)) classes.set(name, statement);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (player) classes.set('Player', player);
|
|
429
|
+
|
|
430
|
+
const topLevel = new Set<string>();
|
|
431
|
+
for (const statement of facade?.statements ?? []) {
|
|
432
|
+
if (statement.name?.text) topLevel.add(statement.name.text);
|
|
433
|
+
for (const declaration of statement.declarations ?? []) {
|
|
434
|
+
if (declaration.name?.text) topLevel.add(declaration.name.text);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return { classes, facade, topLevel };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Whether a statement declares the class or interface `name` - an interface
|
|
442
|
+
* of fields only is a class once parsed (Parser.parseClassOrInterface).
|
|
443
|
+
*/
|
|
444
|
+
function isNamed(statement: any, name: string): boolean {
|
|
445
|
+
const kind = statement.kind;
|
|
446
|
+
return (kind === asc.NodeKind.ClassDeclaration || kind === asc.NodeKind.InterfaceDeclaration) && statement.name.text === name;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** The declaration of the class or interface `name` in the compile, or null. */
|
|
450
|
+
function declarationNamed(parser: any, name: string): any {
|
|
451
|
+
for (const source of parser.sources) {
|
|
452
|
+
const found = source.statements.find((s: any) => isNamed(s, name));
|
|
453
|
+
if (found) return found;
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** One field's change event: the key it is heard by - `"spawnProtected"`, `"glow.enabled"`, `"glow"` - and its class. */
|
|
459
|
+
interface ChangeEvent {
|
|
460
|
+
key: string;
|
|
461
|
+
name: string;
|
|
462
|
+
/** A value's type, or the object field whose members `value` holds. */
|
|
463
|
+
type: ValueType | PlayerField;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The change events of these fields: one a field, and one each member of an object field. */
|
|
467
|
+
function changeEvents(fields: PlayerField[]): ChangeEvent[] {
|
|
468
|
+
return fields.flatMap((field): ChangeEvent[] => {
|
|
469
|
+
const type = field.type;
|
|
470
|
+
const own = `__PlayerChange$${field.name}`;
|
|
471
|
+
if (!isObject(type)) return [{ key: field.name, name: own, type }];
|
|
472
|
+
return [
|
|
473
|
+
{ key: field.name, name: own, type: field },
|
|
474
|
+
...type.members.map(member => ({ key: `${field.name}.${member.name}`, name: `${own}$${member.name}`, type: member.type })),
|
|
475
|
+
];
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* A `"playerchange"` listener's `{ field: "spawnProtected" }` and a
|
|
481
|
+
* `PlayerChangeEvent<"spawnProtected">` written as a type, turned into the field's own
|
|
482
|
+
* event class: the call's name becomes `"playerchange:spawnProtected"`, the key
|
|
483
|
+
* ServerEventMap is given for it, and the type the class. A field the compile
|
|
484
|
+
* has no declaration of is a problem, and so is one not written out.
|
|
485
|
+
*/
|
|
486
|
+
function typeChangeListeners(parser: any, classes: Map<string, string>): string[] {
|
|
487
|
+
const problems: string[] = [];
|
|
488
|
+
const where = (node: any) => `${node.range.source.normalizedPath}:${lineOf(node.range.source.text, node.range.start)}`;
|
|
489
|
+
const known = (field: string, node: any) => {
|
|
490
|
+
if (classes.has(field)) return true;
|
|
491
|
+
const names = [...classes.keys()].map(name => `"${name}"`).join(', ');
|
|
492
|
+
problems.push(`${where(node)}: playerchange - "${field}" is not a field of Player in what this plugin imports (${names || 'it imports none'})`);
|
|
493
|
+
return false;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
for (const source of parser.sources) {
|
|
497
|
+
if (!source.text.includes(CHANGE_EVENT) && !source.text.includes(CHANGE_CLASS)) continue;
|
|
498
|
+
walk(source.statements, (node) => {
|
|
499
|
+
if (node.kind === asc.NodeKind.Call) {
|
|
500
|
+
const field = listenedField(node);
|
|
501
|
+
if (field === null) problems.push(`${where(node)}: playerchange - the field is written out, as the event's name is: { field: "spawnProtected" }`);
|
|
502
|
+
else if (field !== undefined && known(field, node)) node.args[0].value = `${CHANGE_EVENT}:${field}`;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const field = node.kind === asc.NodeKind.NamedType ? typedField(node) : undefined;
|
|
506
|
+
if (field === undefined || !known(field, node)) return;
|
|
507
|
+
node.name = asc.Node.createSimpleTypeName(classes.get(field)!, node.name.range);
|
|
508
|
+
node.typeArguments = null;
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return problems;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isStringLiteral(node: any): boolean {
|
|
515
|
+
return node?.kind === asc.NodeKind.Literal && node.literalKind === asc.LiteralKind.String;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** The field a `"playerchange"` listener names in its options; undefined for any other call, null when not written out. */
|
|
519
|
+
function listenedField(call: any): string | null | undefined {
|
|
520
|
+
const method = call.expression.kind === asc.NodeKind.PropertyAccess ? call.expression.property.text : '';
|
|
521
|
+
const [type, , options] = call.args;
|
|
522
|
+
if (method !== 'addEventListener' && method !== 'removeEventListener') return undefined;
|
|
523
|
+
if (!isStringLiteral(type) || type.value !== CHANGE_EVENT || !options) return undefined;
|
|
524
|
+
if (options.kind !== asc.NodeKind.Literal || options.literalKind !== asc.LiteralKind.Object) return null;
|
|
525
|
+
|
|
526
|
+
const at = options.names.findIndex((name: any) => name.text === 'field');
|
|
527
|
+
if (at < 0) return undefined;
|
|
528
|
+
return isStringLiteral(options.values[at]) ? options.values[at].value : null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** The field of a `PlayerChangeEvent<"spawnProtected">`; undefined for any other type. */
|
|
532
|
+
function typedField(node: any): string | undefined {
|
|
533
|
+
if (node.name.identifier.text !== CHANGE_CLASS || node.name.next || node.typeArguments?.length !== 1) return undefined;
|
|
534
|
+
// A literal in a type is read as `string`: its text is where it stood.
|
|
535
|
+
const range = node.typeArguments[0].range;
|
|
536
|
+
const literal = range.source.text.slice(range.start, range.end).trim().match(/^"([^"\\]*)"$|^'([^'\\]*)'$/);
|
|
537
|
+
return literal ? literal[1] ?? literal[2] : undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Calls `visit` on every node under `node`, the node first. */
|
|
541
|
+
function walk(node: any, visit: (node: any) => void) {
|
|
542
|
+
if (Array.isArray(node)) {
|
|
543
|
+
for (const each of node) walk(each, visit);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (!node || typeof node !== 'object' || typeof node.kind !== 'number') return;
|
|
547
|
+
visit(node);
|
|
548
|
+
for (const key in node) {
|
|
549
|
+
if (key !== 'range') walk(node[key], visit);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** The class an object field reads as: the interface's name, or one of the hood's. */
|
|
554
|
+
function objectClass(field: PlayerField): string {
|
|
555
|
+
const type = field.type as ObjectType;
|
|
556
|
+
return type.interface ?? `__AmxtsPlayer_${field.name}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** What the AssemblyScript type of a value is, in the hood. */
|
|
560
|
+
function hoodType(type: ValueType): string {
|
|
561
|
+
if (type === 'boolean') return 'bool';
|
|
562
|
+
if (type === 'number') return 'f64';
|
|
563
|
+
if (type === 'players') return 'Player[]';
|
|
564
|
+
// A union of literals is text, `true | false | "..."` too: false is the
|
|
565
|
+
// interned "false", as the compiler holds it in a plugin's own types.
|
|
566
|
+
return 'string';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Reading a value from slot `slot`, key `key` (both expressions). */
|
|
570
|
+
function readValue(type: ValueType, slot: string, key: string): string {
|
|
571
|
+
if (type === 'boolean') return `__amxts_pf_get(${slot}, ${key}) != 0`;
|
|
572
|
+
if (type === 'number') return `__amxts_pf_get(${slot}, ${key})`;
|
|
573
|
+
if (type === 'string') return `__amxts_pf_text(${slot}, ${key})`;
|
|
574
|
+
if (type === 'players') return `${PLAYER_LIST}.read(${slot}, ${key})`;
|
|
575
|
+
const first = JSON.stringify(type.literals[0]);
|
|
576
|
+
return type.boolean ? `__amxts_pf_choice(${slot}, ${key}, ${first})` : `__amxts_pf_text_or(${slot}, ${key}, ${first})`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Writing `value` (an expression) there. */
|
|
580
|
+
function writeValue(type: ValueType, slot: string, key: string, value: string): string {
|
|
581
|
+
if (type === 'boolean') return `__amxts_pf_set(${slot}, ${key}, ${value} ? 1 : 0)`;
|
|
582
|
+
if (type === 'number') return `__amxts_pf_set(${slot}, ${key}, ${value})`;
|
|
583
|
+
if (type === 'players') return `${PLAYER_LIST}.write(${slot}, ${key}, ${value})`;
|
|
584
|
+
return `__amxts_pf_set_text(${slot}, ${key}, ${value})`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** A value from what the module keeps of it - a number and a text, both expressions - as a change event reads it. */
|
|
588
|
+
function decode(type: ValueType, number: string, text: string): string {
|
|
589
|
+
if (type === 'boolean') return `${number} != 0`;
|
|
590
|
+
if (type === 'number') return number;
|
|
591
|
+
if (type === 'string') return text;
|
|
592
|
+
if (type === 'players') return `${PLAYER_LIST}.parse(${text})`;
|
|
593
|
+
return `${type.boolean ? '__amxts_pf_choose' : '__amxts_pf_or'}(${text}, ${JSON.stringify(type.literals[0])})`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* A field's change event: `value` and `previous` of its type. An object
|
|
598
|
+
* field's holds the object, each member as it is but the one that changed.
|
|
599
|
+
* `@global`, so ServerEventMap's file finds it.
|
|
600
|
+
*/
|
|
601
|
+
function changeEventClass(event: ChangeEvent): string[] {
|
|
602
|
+
const lines = [`// "${CHANGE_EVENT}" with { field: "${event.key}" }.`, '// @ts-ignore: decorator', `@global export class ${event.name} extends ${CHANGE_CLASS} {`];
|
|
603
|
+
const type = event.type;
|
|
604
|
+
if (typeof type === 'string' || 'literals' in type) {
|
|
605
|
+
const hood = hoodType(type);
|
|
606
|
+
return [
|
|
607
|
+
...lines,
|
|
608
|
+
`\tget value(): ${hood} { return ${decode(type, 'this.__number', 'this.__text')}; }`,
|
|
609
|
+
`\tget previous(): ${hood} { return ${decode(type, 'this.__previousNumber', 'this.__previousText')}; }`,
|
|
610
|
+
'}',
|
|
611
|
+
'',
|
|
612
|
+
];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const name = objectClass(type);
|
|
616
|
+
const members = (type.type as ObjectType).members.map((member) => {
|
|
617
|
+
const key = JSON.stringify(`${type.name}.${member.name}`);
|
|
618
|
+
const stored = decode(member.type, `__amxts_pf_get(this.__slot, ${key})`, `__amxts_pf_text(this.__slot, ${key})`);
|
|
619
|
+
return `\t\tvalue.${member.name} = this.field == ${key} ? ${decode(member.type, 'n', 't')} : ${stored};`;
|
|
620
|
+
});
|
|
621
|
+
return [
|
|
622
|
+
...lines,
|
|
623
|
+
`\tget value(): ${name} { return this.__object(this.__number, this.__text); }`,
|
|
624
|
+
`\tget previous(): ${name} { return this.__object(this.__previousNumber, this.__previousText); }`,
|
|
625
|
+
'',
|
|
626
|
+
`\t__object(n: f64, t: string): ${name} {`,
|
|
627
|
+
`\t\tconst value = new ${name}();`,
|
|
628
|
+
...members,
|
|
629
|
+
'\t\treturn value;',
|
|
630
|
+
'\t}',
|
|
631
|
+
'}',
|
|
632
|
+
'',
|
|
633
|
+
];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** What a value holds before it is written: what reading it unwritten gives. */
|
|
637
|
+
function initial(type: ValueType): string {
|
|
638
|
+
if (type === 'boolean') return 'false';
|
|
639
|
+
if (type === 'number') return '0.0';
|
|
640
|
+
if (type === 'string') return '""';
|
|
641
|
+
if (type === 'players') return '[]';
|
|
642
|
+
return JSON.stringify(type.literals[0]);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** The hidden file: the helpers the accessors call, the holder of the accessors, and the classes they read as. */
|
|
646
|
+
export function playerFieldsSource(fields: PlayerField[]): string {
|
|
647
|
+
const accessors: string[] = [];
|
|
648
|
+
const classes: string[] = [];
|
|
649
|
+
const events = changeEvents(fields);
|
|
650
|
+
let lists = false;
|
|
651
|
+
|
|
652
|
+
for (const field of fields) {
|
|
653
|
+
const key = JSON.stringify(field.name);
|
|
654
|
+
const type = field.type;
|
|
655
|
+
if (!isObject(type)) {
|
|
656
|
+
if (type === 'players') lists = true;
|
|
657
|
+
accessors.push(
|
|
658
|
+
`\tget ${field.name}(): ${hoodType(type)} { return ${readValue(type, 'this.id', key)}; }`,
|
|
659
|
+
`\tset ${field.name}(value: ${hoodType(type)}) { ${writeValue(type, 'this.id', key, 'value')}; }`,
|
|
660
|
+
);
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const name = objectClass(field);
|
|
665
|
+
accessors.push(
|
|
666
|
+
`\tget ${field.name}(): ${name} { const value = new ${name}(); value.__slot = this.id; return value; }`,
|
|
667
|
+
`\tset ${field.name}(value: ${name}) { const mine = this.${field.name}; ${type.members.map(member => `mine.${member.name} = value.${member.name};`).join(' ')} }`,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const body: string[] = [];
|
|
671
|
+
body.push('\t/** Whose field this is; -1: an object of its own, holding the values below. */', '\t__slot: i32 = -1;');
|
|
672
|
+
for (const member of type.members) {
|
|
673
|
+
if (member.type === 'players') lists = true;
|
|
674
|
+
body.push(`\t__${member.name}: ${hoodType(member.type)} = ${initial(member.type)};`);
|
|
675
|
+
}
|
|
676
|
+
for (const member of type.members) {
|
|
677
|
+
const memberKey = JSON.stringify(`${field.name}.${member.name}`);
|
|
678
|
+
const hood = hoodType(member.type);
|
|
679
|
+
body.push(
|
|
680
|
+
`\tget ${member.name}(): ${hood} { return this.__slot < 0 ? this.__${member.name} : ${readValue(member.type, 'this.__slot', memberKey)}; }`,
|
|
681
|
+
`\tset ${member.name}(value: ${hood}) { if (this.__slot < 0) this.__${member.name} = value; else ${writeValue(member.type, 'this.__slot', memberKey, 'value')}; }`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
classes.push(
|
|
685
|
+
`// Player.${field.name}: a player's, read and written where the module keeps it, or one of its own.`,
|
|
686
|
+
`export class ${name} {`,
|
|
687
|
+
...body,
|
|
688
|
+
'}',
|
|
689
|
+
'',
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (lists) {
|
|
694
|
+
classes.push(
|
|
695
|
+
'// A Player[] field: its push writes back, and a player is found by his id.',
|
|
696
|
+
`class ${PLAYER_LIST} extends Array<Player> {`,
|
|
697
|
+
'\t__slot: i32 = -1;',
|
|
698
|
+
'\t__key: string = "";',
|
|
699
|
+
'',
|
|
700
|
+
'\tpush(value: Player): i32 {',
|
|
701
|
+
'\t\tconst length = super.push(value);',
|
|
702
|
+
`\t\tif (this.__slot >= 0) ${PLAYER_LIST}.write(this.__slot, this.__key, this);`,
|
|
703
|
+
'\t\treturn length;',
|
|
704
|
+
'\t}',
|
|
705
|
+
'',
|
|
706
|
+
'\tindexOf(value: Player, fromIndex: i32 = 0): i32 {',
|
|
707
|
+
'\t\tfor (let i = max(fromIndex, 0); i < this.length; i++) {',
|
|
708
|
+
'\t\t\tif (unchecked(this[i]).id == value.id) return i;',
|
|
709
|
+
'\t\t}',
|
|
710
|
+
'\t\treturn -1;',
|
|
711
|
+
'\t}',
|
|
712
|
+
'',
|
|
713
|
+
'\tincludes(value: Player, fromIndex: i32 = 0): bool {',
|
|
714
|
+
'\t\treturn this.indexOf(value, fromIndex) >= 0;',
|
|
715
|
+
'\t}',
|
|
716
|
+
'',
|
|
717
|
+
'\t// A list of its own, from the ids as the module keeps them: "3,5".',
|
|
718
|
+
`\tstatic parse(text: string): ${PLAYER_LIST} {`,
|
|
719
|
+
`\t\tconst list = new ${PLAYER_LIST}();`,
|
|
720
|
+
'\t\tconst ids = text.split(",");',
|
|
721
|
+
'\t\tfor (let i = 0; i < ids.length; i++) {',
|
|
722
|
+
'\t\t\tconst id = I32.parseInt(unchecked(ids[i]));',
|
|
723
|
+
'\t\t\tif (id > 0) list.push(new Player(id));',
|
|
724
|
+
'\t\t}',
|
|
725
|
+
'\t\treturn list;',
|
|
726
|
+
'\t}',
|
|
727
|
+
'',
|
|
728
|
+
`\tstatic read(slot: i32, key: string): ${PLAYER_LIST} {`,
|
|
729
|
+
`\t\tconst list = ${PLAYER_LIST}.parse(__amxts_pf_text(slot, key));`,
|
|
730
|
+
'\t\t// Only now: building the list must not write what it just read.',
|
|
731
|
+
'\t\tlist.__slot = slot;',
|
|
732
|
+
'\t\tlist.__key = key;',
|
|
733
|
+
'\t\treturn list;',
|
|
734
|
+
'\t}',
|
|
735
|
+
'',
|
|
736
|
+
'\tstatic write(slot: i32, key: string, list: Player[]): void {',
|
|
737
|
+
'\t\tlet ids = "";',
|
|
738
|
+
'\t\tfor (let i = 0; i < list.length; i++) ids += (i > 0 ? "," : "") + (<i32>unchecked(list[i]).id).toString();',
|
|
739
|
+
'\t\t__amxts_pf_set_players(slot, key, ids);',
|
|
740
|
+
'\t}',
|
|
741
|
+
'}',
|
|
742
|
+
'',
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return [
|
|
747
|
+
'// GENERATED by scripts/player-fields.ts: the fields plugins add to Player.',
|
|
748
|
+
'// @ts-ignore: decorator',
|
|
749
|
+
'@external("env", "player_data_get") declare function _get(id: i32, key: string): f64;',
|
|
750
|
+
'// @ts-ignore: decorator',
|
|
751
|
+
'@external("env", "player_data_set") declare function _set(id: i32, key: string, value: f64): void;',
|
|
752
|
+
'// @ts-ignore: decorator',
|
|
753
|
+
'@external("env", "player_data_get_text") declare function _getText(id: i32, key: string, out: usize, max: i32): i32;',
|
|
754
|
+
'// @ts-ignore: decorator',
|
|
755
|
+
'@external("env", "player_data_set_text") declare function _setText(id: i32, key: string, value: string): void;',
|
|
756
|
+
'// @ts-ignore: decorator',
|
|
757
|
+
'@external("env", "player_data_set_players") declare function _setPlayers(id: i32, key: string, value: string): void;',
|
|
758
|
+
'',
|
|
759
|
+
'// @ts-ignore: decorator',
|
|
760
|
+
'@global export function __amxts_pf_get(id: i32, key: string): f64 {',
|
|
761
|
+
'\treturn _get(id, key);',
|
|
762
|
+
'}',
|
|
763
|
+
'',
|
|
764
|
+
'// @ts-ignore: decorator',
|
|
765
|
+
'@global export function __amxts_pf_set(id: i32, key: string, value: f64): void {',
|
|
766
|
+
'\t_set(id, key, value);',
|
|
767
|
+
'}',
|
|
768
|
+
'',
|
|
769
|
+
'// @ts-ignore: decorator',
|
|
770
|
+
'@global export function __amxts_pf_text(id: i32, key: string): string {',
|
|
771
|
+
'\tconst length = _getText(id, key, 0, 0);',
|
|
772
|
+
'\tif (length <= 0) return "";',
|
|
773
|
+
'\tconst bytes = new ArrayBuffer(length);',
|
|
774
|
+
'\t_getText(id, key, changetype<usize>(bytes), length);',
|
|
775
|
+
'\treturn String.UTF8.decode(bytes);',
|
|
776
|
+
'}',
|
|
777
|
+
'',
|
|
778
|
+
'// @ts-ignore: decorator',
|
|
779
|
+
'@global export function __amxts_pf_set_text(id: i32, key: string, value: string): void {',
|
|
780
|
+
'\t_setText(id, key, value);',
|
|
781
|
+
'}',
|
|
782
|
+
'',
|
|
783
|
+
'// A union of literals: its first until written.',
|
|
784
|
+
'// @ts-ignore: decorator',
|
|
785
|
+
'@global export function __amxts_pf_or(text: string, first: string): string {',
|
|
786
|
+
'\treturn text.length > 0 ? text : first;',
|
|
787
|
+
'}',
|
|
788
|
+
'',
|
|
789
|
+
'// @ts-ignore: decorator',
|
|
790
|
+
'@global export function __amxts_pf_text_or(id: i32, key: string, first: string): string {',
|
|
791
|
+
'\treturn __amxts_pf_or(__amxts_pf_text(id, key), first);',
|
|
792
|
+
'}',
|
|
793
|
+
'',
|
|
794
|
+
'// `true | false | "..."`: stored as "true", "false" or the literal. false comes',
|
|
795
|
+
'// back as the interned "false", the one Compiler.makeIsTrueish reads as false.',
|
|
796
|
+
'// @ts-ignore: decorator',
|
|
797
|
+
'@global export function __amxts_pf_choose(text: string, first: string): string {',
|
|
798
|
+
'\tconst value = __amxts_pf_or(text, first);',
|
|
799
|
+
'\treturn value == "false" ? "false" : value;',
|
|
800
|
+
'}',
|
|
801
|
+
'',
|
|
802
|
+
'// @ts-ignore: decorator',
|
|
803
|
+
'@global export function __amxts_pf_choice(id: i32, key: string, first: string): string {',
|
|
804
|
+
'\treturn __amxts_pf_choose(__amxts_pf_text(id, key), first);',
|
|
805
|
+
'}',
|
|
806
|
+
'',
|
|
807
|
+
'// @ts-ignore: decorator',
|
|
808
|
+
'@global export function __amxts_pf_set_players(id: i32, key: string, ids: string): void {',
|
|
809
|
+
'\t_setPlayers(id, key, ids);',
|
|
810
|
+
'}',
|
|
811
|
+
'',
|
|
812
|
+
...classes,
|
|
813
|
+
...events.flatMap(changeEventClass),
|
|
814
|
+
`// Its members are moved into the facade's Player; the class itself is dropped.`,
|
|
815
|
+
`class ${HOLDER} {`,
|
|
816
|
+
...accessors,
|
|
817
|
+
'}',
|
|
818
|
+
'',
|
|
819
|
+
`// Its members are moved into ServerEventMap: the name a listener's call is given, and its event.`,
|
|
820
|
+
`interface ${CHANGES} {`,
|
|
821
|
+
...events.map(event => `\t"${CHANGE_EVENT}:${event.key}": ${event.name};`),
|
|
822
|
+
'}',
|
|
823
|
+
'',
|
|
824
|
+
].join('\n');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** The `}` that closes the `{` at `open`, past strings and comments; -1 if none. */
|
|
828
|
+
function closingBrace(text: string, open: number): number {
|
|
829
|
+
let depth = 0;
|
|
830
|
+
for (let i = open; i < text.length; i++) {
|
|
831
|
+
const c = text[i];
|
|
832
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
833
|
+
i = text.indexOf('\n', i);
|
|
834
|
+
if (i < 0) return -1;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (c === '/' && text[i + 1] === '*') {
|
|
838
|
+
i = text.indexOf('*/', i + 2);
|
|
839
|
+
if (i < 0) return -1;
|
|
840
|
+
i++;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (c === '"' || c === '\'' || c === '`') {
|
|
844
|
+
for (i++; i < text.length && text[i] !== c; i++) {
|
|
845
|
+
if (text[i] === '\\') i++;
|
|
846
|
+
}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (c === '{') depth++;
|
|
850
|
+
if (c === '}' && --depth === 0) return i;
|
|
851
|
+
}
|
|
852
|
+
return -1;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Comments as spaces, so what is left keeps its offsets. */
|
|
856
|
+
function withoutComments(text: string): string {
|
|
857
|
+
return text.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, blank);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function blank(text: string): string {
|
|
861
|
+
return text.replace(/[^\r\n]/g, ' ');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function lineOf(text: string, at: number): number {
|
|
865
|
+
return text.slice(0, at).split('\n').length;
|
|
866
|
+
}
|