@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.
Files changed (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -0
  3. package/README.ru.md +162 -0
  4. package/amxmodx/base/include/amxconst.inc +594 -0
  5. package/amxmodx/base/include/amxmisc.inc +902 -0
  6. package/amxmodx/base/include/amxmodx.inc +3447 -0
  7. package/amxmodx/base/include/amxmodx_version.inc +20 -0
  8. package/amxmodx/base/include/cellarray.inc +525 -0
  9. package/amxmodx/base/include/cellstack.inc +166 -0
  10. package/amxmodx/base/include/celltrie.inc +412 -0
  11. package/amxmodx/base/include/core.inc +202 -0
  12. package/amxmodx/base/include/csstats.inc +298 -0
  13. package/amxmodx/base/include/csstats_const.inc +29 -0
  14. package/amxmodx/base/include/cstrike.inc +1287 -0
  15. package/amxmodx/base/include/cstrike_const.inc +521 -0
  16. package/amxmodx/base/include/csx.inc +259 -0
  17. package/amxmodx/base/include/cvars.inc +547 -0
  18. package/amxmodx/base/include/datapack.inc +161 -0
  19. package/amxmodx/base/include/dbi.inc +154 -0
  20. package/amxmodx/base/include/dodconst.inc +152 -0
  21. package/amxmodx/base/include/dodfun.inc +152 -0
  22. package/amxmodx/base/include/dodstats.inc +69 -0
  23. package/amxmodx/base/include/dodx.inc +161 -0
  24. package/amxmodx/base/include/engine.inc +1343 -0
  25. package/amxmodx/base/include/engine_const.inc +310 -0
  26. package/amxmodx/base/include/engine_stocks.inc +248 -0
  27. package/amxmodx/base/include/esf.inc +70 -0
  28. package/amxmodx/base/include/esf_const.inc +74 -0
  29. package/amxmodx/base/include/fakemeta.inc +1127 -0
  30. package/amxmodx/base/include/fakemeta_const.inc +793 -0
  31. package/amxmodx/base/include/fakemeta_stocks.inc +395 -0
  32. package/amxmodx/base/include/fakemeta_util.inc +882 -0
  33. package/amxmodx/base/include/file.inc +666 -0
  34. package/amxmodx/base/include/float.inc +422 -0
  35. package/amxmodx/base/include/fun.inc +333 -0
  36. package/amxmodx/base/include/gameconfig.inc +93 -0
  37. package/amxmodx/base/include/geoip.inc +264 -0
  38. package/amxmodx/base/include/ham_const.inc +4352 -0
  39. package/amxmodx/base/include/hamsandwich.inc +439 -0
  40. package/amxmodx/base/include/hlsdk_const.inc +808 -0
  41. package/amxmodx/base/include/json.inc +777 -0
  42. package/amxmodx/base/include/lang.inc +119 -0
  43. package/amxmodx/base/include/message_const.inc +1130 -0
  44. package/amxmodx/base/include/message_stocks.inc +161 -0
  45. package/amxmodx/base/include/messages.inc +604 -0
  46. package/amxmodx/base/include/newmenus.inc +387 -0
  47. package/amxmodx/base/include/ns.inc +754 -0
  48. package/amxmodx/base/include/ns_const.inc +196 -0
  49. package/amxmodx/base/include/nvault.inc +154 -0
  50. package/amxmodx/base/include/regex.inc +348 -0
  51. package/amxmodx/base/include/sockets.inc +183 -0
  52. package/amxmodx/base/include/sorting.inc +107 -0
  53. package/amxmodx/base/include/sqlx.inc +582 -0
  54. package/amxmodx/base/include/string.inc +786 -0
  55. package/amxmodx/base/include/string_const.inc +156 -0
  56. package/amxmodx/base/include/string_stocks.inc +325 -0
  57. package/amxmodx/base/include/textparse_ini.inc +214 -0
  58. package/amxmodx/base/include/textparse_smc.inc +261 -0
  59. package/amxmodx/base/include/tfcconst.inc +84 -0
  60. package/amxmodx/base/include/tfcstats.inc +62 -0
  61. package/amxmodx/base/include/tfcx.inc +148 -0
  62. package/amxmodx/base/include/time.inc +118 -0
  63. package/amxmodx/base/include/tsconst.inc +154 -0
  64. package/amxmodx/base/include/tsfun.inc +198 -0
  65. package/amxmodx/base/include/tsstats.inc +65 -0
  66. package/amxmodx/base/include/tsx.inc +88 -0
  67. package/amxmodx/base/include/vault.inc +60 -0
  68. package/amxmodx/base/include/vector.inc +131 -0
  69. package/amxmodx/base/include/xs.inc +1570 -0
  70. package/as/amxts.d.ts +153 -0
  71. package/as/constants.ts +4730 -0
  72. package/as/effects.ts +1449 -0
  73. package/as/entities.ts +5482 -0
  74. package/as/events.ts +3054 -0
  75. package/as/facade.ts +4639 -0
  76. package/as/flags.ts +299 -0
  77. package/as/fs.ts +287 -0
  78. package/as/hooks.ts +18536 -0
  79. package/as/kit.ts +25 -0
  80. package/as/lib/check.ts +86 -0
  81. package/as/modules/http.ts +184 -0
  82. package/as/natives.ts +11845 -0
  83. package/as/os.ts +30 -0
  84. package/as/promise.ts +1392 -0
  85. package/as/promise.types.d.ts +82 -0
  86. package/as/remote.ts +288 -0
  87. package/as/tsconfig.json +22 -0
  88. package/as/vector.ts +65 -0
  89. package/bin/amxts.mjs +10 -0
  90. package/includes/menu_core.inc +355 -0
  91. package/includes/order.txt +54 -0
  92. package/includes/sources.json +27 -0
  93. package/includes/universal_config.inc +355 -0
  94. package/lint/oxlint-plugin.mjs +77 -0
  95. package/node_modules/assemblyscript/LICENSE +201 -0
  96. package/node_modules/assemblyscript/NOTICE +93 -0
  97. package/node_modules/assemblyscript/package.json +63 -0
  98. package/node_modules/assemblyscript/std/README.md +6 -0
  99. package/node_modules/assemblyscript/std/assembly/array.ts +526 -0
  100. package/node_modules/assemblyscript/std/assembly/arraybuffer.ts +77 -0
  101. package/node_modules/assemblyscript/std/assembly/atomics.ts +127 -0
  102. package/node_modules/assemblyscript/std/assembly/bindings/asyncify.ts +16 -0
  103. package/node_modules/assemblyscript/std/assembly/bindings/dom.ts +291 -0
  104. package/node_modules/assemblyscript/std/assembly/bindings/node.ts +6 -0
  105. package/node_modules/assemblyscript/std/assembly/builtins.ts +2631 -0
  106. package/node_modules/assemblyscript/std/assembly/compat.ts +2 -0
  107. package/node_modules/assemblyscript/std/assembly/console.ts +42 -0
  108. package/node_modules/assemblyscript/std/assembly/crypto.ts +9 -0
  109. package/node_modules/assemblyscript/std/assembly/dataview.ts +181 -0
  110. package/node_modules/assemblyscript/std/assembly/date.ts +373 -0
  111. package/node_modules/assemblyscript/std/assembly/diagnostics.ts +11 -0
  112. package/node_modules/assemblyscript/std/assembly/error.ts +44 -0
  113. package/node_modules/assemblyscript/std/assembly/function.ts +38 -0
  114. package/node_modules/assemblyscript/std/assembly/index.d.ts +2822 -0
  115. package/node_modules/assemblyscript/std/assembly/iterator.ts +35 -0
  116. package/node_modules/assemblyscript/std/assembly/map.ts +260 -0
  117. package/node_modules/assemblyscript/std/assembly/math.ts +3289 -0
  118. package/node_modules/assemblyscript/std/assembly/memory.ts +123 -0
  119. package/node_modules/assemblyscript/std/assembly/number.ts +388 -0
  120. package/node_modules/assemblyscript/std/assembly/object.ts +36 -0
  121. package/node_modules/assemblyscript/std/assembly/performance.ts +9 -0
  122. package/node_modules/assemblyscript/std/assembly/polyfills.ts +27 -0
  123. package/node_modules/assemblyscript/std/assembly/process.ts +50 -0
  124. package/node_modules/assemblyscript/std/assembly/reference.ts +48 -0
  125. package/node_modules/assemblyscript/std/assembly/regexp.ts +12 -0
  126. package/node_modules/assemblyscript/std/assembly/rt/README.md +83 -0
  127. package/node_modules/assemblyscript/std/assembly/rt/common.ts +81 -0
  128. package/node_modules/assemblyscript/std/assembly/rt/index-incremental.ts +2 -0
  129. package/node_modules/assemblyscript/std/assembly/rt/index-minimal.ts +2 -0
  130. package/node_modules/assemblyscript/std/assembly/rt/index-stub.ts +1 -0
  131. package/node_modules/assemblyscript/std/assembly/rt/index.d.ts +37 -0
  132. package/node_modules/assemblyscript/std/assembly/rt/itcms.ts +419 -0
  133. package/node_modules/assemblyscript/std/assembly/rt/rtrace.ts +15 -0
  134. package/node_modules/assemblyscript/std/assembly/rt/stub.ts +133 -0
  135. package/node_modules/assemblyscript/std/assembly/rt/tcms.ts +254 -0
  136. package/node_modules/assemblyscript/std/assembly/rt/tlsf.ts +592 -0
  137. package/node_modules/assemblyscript/std/assembly/rt.ts +87 -0
  138. package/node_modules/assemblyscript/std/assembly/set.ts +220 -0
  139. package/node_modules/assemblyscript/std/assembly/shared/feature.ts +65 -0
  140. package/node_modules/assemblyscript/std/assembly/shared/runtime.ts +11 -0
  141. package/node_modules/assemblyscript/std/assembly/shared/target.ts +11 -0
  142. package/node_modules/assemblyscript/std/assembly/shared/tsconfig.json +11 -0
  143. package/node_modules/assemblyscript/std/assembly/shared/typeinfo.ts +72 -0
  144. package/node_modules/assemblyscript/std/assembly/staticarray.ts +401 -0
  145. package/node_modules/assemblyscript/std/assembly/string.ts +847 -0
  146. package/node_modules/assemblyscript/std/assembly/symbol.ts +114 -0
  147. package/node_modules/assemblyscript/std/assembly/table.ts +16 -0
  148. package/node_modules/assemblyscript/std/assembly/tsconfig.json +6 -0
  149. package/node_modules/assemblyscript/std/assembly/typedarray.ts +1945 -0
  150. package/node_modules/assemblyscript/std/assembly/uri.ts +17 -0
  151. package/node_modules/assemblyscript/std/assembly/util/bytes.ts +107 -0
  152. package/node_modules/assemblyscript/std/assembly/util/casemap.ts +497 -0
  153. package/node_modules/assemblyscript/std/assembly/util/error.ts +54 -0
  154. package/node_modules/assemblyscript/std/assembly/util/hash.ts +117 -0
  155. package/node_modules/assemblyscript/std/assembly/util/math.ts +1922 -0
  156. package/node_modules/assemblyscript/std/assembly/util/memory.ts +290 -0
  157. package/node_modules/assemblyscript/std/assembly/util/number.ts +873 -0
  158. package/node_modules/assemblyscript/std/assembly/util/sort.ts +313 -0
  159. package/node_modules/assemblyscript/std/assembly/util/string.ts +1202 -0
  160. package/node_modules/assemblyscript/std/assembly/util/uri.ts +275 -0
  161. package/node_modules/assemblyscript/std/assembly/vector.ts +4 -0
  162. package/node_modules/assemblyscript/std/assembly.json +16 -0
  163. package/node_modules/assemblyscript/std/portable/index.d.ts +462 -0
  164. package/node_modules/assemblyscript/std/portable/index.js +416 -0
  165. package/node_modules/assemblyscript/std/portable.json +11 -0
  166. package/node_modules/assemblyscript/std/types/assembly/index.d.ts +1 -0
  167. package/node_modules/assemblyscript/std/types/assembly/package.json +3 -0
  168. package/node_modules/assemblyscript/std/types/portable/index.d.ts +1 -0
  169. package/node_modules/assemblyscript/std/types/portable/package.json +3 -0
  170. package/node_modules/assemblyscript/tsconfig-base.json +13 -0
  171. package/package.json +112 -0
  172. package/runtime/deps/assemblyscript/LICENSE +201 -0
  173. package/runtime/deps/assemblyscript/NOTICE +93 -0
  174. package/runtime/deps/assemblyscript/bin/asc.js +35 -0
  175. package/runtime/deps/assemblyscript/dist/asc.js +25035 -0
  176. package/runtime/deps/assemblyscript/dist/assemblyscript.js +337 -0
  177. package/runtime/deps/assemblyscript/node_modules/binaryen/LICENSE +201 -0
  178. package/runtime/deps/assemblyscript/node_modules/binaryen/index.js +28905 -0
  179. package/runtime/deps/assemblyscript/node_modules/binaryen/package.json +50 -0
  180. package/runtime/deps/assemblyscript/node_modules/long/LICENSE +202 -0
  181. package/runtime/deps/assemblyscript/node_modules/long/index.js +1581 -0
  182. package/runtime/deps/assemblyscript/node_modules/long/package.json +58 -0
  183. package/runtime/deps/assemblyscript/package.json +114 -0
  184. package/runtime/host/amxts.inc +85 -0
  185. package/runtime/licenses/AMXX-LICENSE.txt +34 -0
  186. package/runtime/licenses/AssemblyScript-LICENSE +201 -0
  187. package/runtime/licenses/AssemblyScript-NOTICE +93 -0
  188. package/runtime/licenses/Binaryen-LICENSE +201 -0
  189. package/runtime/licenses/Bun-LICENSE.md +84 -0
  190. package/runtime/licenses/GPL-3.0.txt +674 -0
  191. package/runtime/licenses/LLVM-LICENSE.TXT +279 -0
  192. package/runtime/licenses/README.md +25 -0
  193. package/runtime/licenses/WAMR-LICENSE +219 -0
  194. package/runtime/natives.txt +1095 -0
  195. package/runtime/patches/README.md +59 -0
  196. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +14174 -0
  197. package/runtime/patches/wamr-2.4.5-amxts.patch +364 -0
  198. package/scripts/apply-docs.ts +508 -0
  199. package/scripts/asc.ts +27 -0
  200. package/scripts/auto-imports.ts +258 -0
  201. package/scripts/build-wasm.ts +595 -0
  202. package/scripts/check.ts +93 -0
  203. package/scripts/compile-cache.ts +179 -0
  204. package/scripts/compile.ts +401 -0
  205. package/scripts/docker-server.ts +73 -0
  206. package/scripts/docs/as/amxts.ts +210 -0
  207. package/scripts/docs/as/effects.ts +1684 -0
  208. package/scripts/docs/as/facade.ts +3302 -0
  209. package/scripts/docs/as/fs.ts +173 -0
  210. package/scripts/docs/as/lib/check.ts +68 -0
  211. package/scripts/docs/as/modules/http.ts +66 -0
  212. package/scripts/docs/as/os.ts +16 -0
  213. package/scripts/docs/as/promise.ts +132 -0
  214. package/scripts/docs/as/promise.types.ts +63 -0
  215. package/scripts/docs/as/vector.ts +48 -0
  216. package/scripts/docs/entities.ts +2338 -0
  217. package/scripts/docs/events.ts +245 -0
  218. package/scripts/docs/game.ts +497 -0
  219. package/scripts/docs/messages.ts +148 -0
  220. package/scripts/file-contents.ts +74 -0
  221. package/scripts/includes.ts +134 -0
  222. package/scripts/player-fields.ts +866 -0
  223. package/scripts/plugin-cache.ts +92 -0
  224. package/scripts/plugin-natives.ts +1157 -0
  225. package/scripts/prebuilt.ts +268 -0
  226. package/scripts/prepare.ts +134 -0
  227. package/scripts/project.ts +973 -0
  228. package/scripts/run.ts +28 -0
  229. package/scripts/shared-modules.ts +906 -0
  230. package/scripts/system.ts +33 -0
  231. package/scripts/tracked-fs.ts +115 -0
  232. package/scripts/typed-configs.ts +788 -0
  233. package/scripts/ui.ts +77 -0
  234. package/src/cli-api.mjs +175 -0
  235. package/src/parser/include-parser.ts +506 -0
  236. package/src/system.mjs +155 -0
  237. package/src/testing/compile-cache.ts +31 -0
  238. package/src/testing/compile.ts +222 -0
  239. package/src/testing/coroutines.ts +191 -0
  240. package/src/testing/index.ts +39 -0
  241. package/src/testing/kits.ts +55 -0
  242. package/src/testing/memory.ts +125 -0
  243. package/src/testing/natives.ts +925 -0
  244. package/src/testing/server.ts +1874 -0
  245. package/src/testing/setup.ts +63 -0
  246. package/src/testing/tables.ts +139 -0
  247. 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
+ }