@amxts/core 0.2.4 → 0.3.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 (71) hide show
  1. package/README.md +5 -5
  2. package/README.ru.md +5 -5
  3. package/as/amxts.d.ts +13 -3
  4. package/as/entities.ts +571 -501
  5. package/as/events.ts +3323 -557
  6. package/as/facade.ts +3079 -718
  7. package/as/fetch.ts +2 -2
  8. package/as/fs.ts +352 -59
  9. package/as/hlds.ts +121 -120
  10. package/as/hooks.ts +10585 -5177
  11. package/as/lib/check.ts +3 -2
  12. package/as/natives.ts +856 -1272
  13. package/as/promise.ts +37 -27
  14. package/as/vector.ts +94 -14
  15. package/includes/order.txt +17 -19
  16. package/node_modules/assemblyscript/std/assembly/index.d.ts +1 -1
  17. package/package.json +9 -6
  18. package/runtime/deps/assemblyscript/dist/asc.js +136 -42
  19. package/runtime/deps/assemblyscript/dist/assemblyscript.js +141 -141
  20. package/runtime/licenses/README.md +9 -7
  21. package/runtime/licenses/ReGameDLL-LICENSE +21 -0
  22. package/runtime/licenses/ReHLDS-LICENSE +21 -0
  23. package/runtime/licenses/zlib-LICENSE +22 -0
  24. package/runtime/natives.txt +618 -523
  25. package/runtime/patches/README.md +87 -1
  26. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +4838 -416
  27. package/runtime/patches/wamr-2.4.5-amxts.patch +417 -5
  28. package/scripts/auto-imports.ts +1 -1
  29. package/scripts/build-identity.ts +102 -65
  30. package/scripts/build-wasm.ts +12 -12
  31. package/scripts/check.ts +4 -1
  32. package/scripts/client-messages.ts +73 -1
  33. package/scripts/compile-cache.ts +11 -8
  34. package/scripts/compile-pool.ts +6 -3
  35. package/scripts/compile-worker.ts +23 -23
  36. package/scripts/compile.ts +142 -19
  37. package/scripts/docs/as/amxts.ts +10 -4
  38. package/scripts/docs/as/facade.ts +1779 -275
  39. package/scripts/docs/as/fetch.ts +2 -2
  40. package/scripts/docs/as/fs.ts +190 -6
  41. package/scripts/docs/as/vector.ts +81 -0
  42. package/scripts/docs/entities.ts +12 -0
  43. package/scripts/docs/events.ts +49 -13
  44. package/scripts/docs/game.ts +6 -0
  45. package/scripts/docs/messages.ts +10 -1
  46. package/scripts/hlds-events.ts +11 -11
  47. package/scripts/includes.ts +3 -4
  48. package/scripts/leaf-natives.ts +84 -0
  49. package/scripts/player-fields.ts +1 -1
  50. package/scripts/plugin-natives.ts +1 -1
  51. package/scripts/prebuilt.ts +41 -10
  52. package/scripts/project.ts +2 -3
  53. package/scripts/reapi-events.ts +9 -9
  54. package/scripts/server-env.ts +145 -0
  55. package/scripts/shared-modules.ts +23 -7
  56. package/scripts/system.ts +1 -1
  57. package/scripts/tracked-fs.ts +11 -2
  58. package/scripts/typed-commands.ts +153 -15
  59. package/scripts/upgrade-calls.ts +225 -0
  60. package/scripts/upgrade-names.ts +1 -4
  61. package/scripts/upgrade.ts +26 -11
  62. package/src/cli-api.mjs +3 -2
  63. package/src/system.mjs +21 -11
  64. package/src/testing/compile.ts +31 -8
  65. package/src/testing/coroutines.ts +0 -10
  66. package/src/testing/memory.ts +89 -2
  67. package/src/testing/natives.ts +67 -30
  68. package/src/testing/server.ts +1094 -155
  69. package/src/testing/tables.ts +23 -6
  70. package/src/typecheck.mjs +110 -0
  71. package/runtime/licenses/Bun-LICENSE.md +0 -84
@@ -11,6 +11,7 @@ import { playerFieldsBuild } from '../../scripts/player-fields';
11
11
  import { nativesBeside, nativesTransform, setNativesBeside } from '../../scripts/plugin-natives';
12
12
  import { ascPath, CONFIG_FILE, currentProjectDir, sourcesFor } from '../../scripts/project';
13
13
  import { targetTransforms } from '../../scripts/reapi-events';
14
+ import { envBuild } from '../../scripts/server-env';
14
15
  import { sharedModulesBuild } from '../../scripts/shared-modules';
15
16
  import { existsSync, hashOf } from '../../scripts/tracked-fs';
16
17
  import { cached } from './compile-cache';
@@ -26,6 +27,8 @@ export interface Compiled {
26
27
  binary: Uint8Array;
27
28
  /** Whether it carries the coroutine scheduler's exports (as/promise.ts). */
28
29
  async: boolean;
30
+ /** Its env() calls as the section amxts.env writes them, a line a name (scripts/server-env.ts). */
31
+ env: string[];
29
32
  }
30
33
 
31
34
  const compiled = new Map<string, Promise<Compiled>>();
@@ -77,6 +80,19 @@ function moduleOwner(source: string): string | null {
77
80
  /** A compile as the disk cache keeps it: no WebAssembly.Module, and an error as its text. */
78
81
  type Built = (Omit<Compiled, 'module'> & { beside: NativesBeside }) | { error: string };
79
82
 
83
+ /**
84
+ * `AMXTS_GC_STRESS=1` in the environment: every plugin and snippet a test
85
+ * compiles runs a step of the collector on every allocation
86
+ * (`ASC_GC_STRESS`), so a value a function holds without a shadow-stack
87
+ * slot is freed at once and its test fails. Part of the cache's key.
88
+ */
89
+ const GC_STRESS = process.env.AMXTS_GC_STRESS ? ['--use', 'ASC_GC_STRESS=1'] : [];
90
+
91
+ /** `args` with GC_STRESS, unless they ask for it themselves. */
92
+ export function stressed(args: string[]): string[] {
93
+ return args.includes('ASC_GC_STRESS=1') ? args : [...args, ...GC_STRESS];
94
+ }
95
+
80
96
  /** A failure worth keeping on disk is the compiler's; a file that could not be opened is not. */
81
97
  const PASSING = /\b(?:EBUSY|EPERM|EACCES|EMFILE|ENFILE|EAGAIN)\b/;
82
98
 
@@ -89,7 +105,7 @@ function lasting(built: Built): boolean {
89
105
  * when an earlier run compiled it from the same files.
90
106
  */
91
107
  async function build(path: string): Promise<Compiled> {
92
- const built = await cached<Built>(['plugin', process.cwd(), path, PLUGINS_ROOT], async () => {
108
+ const built = await cached<Built>(['plugin', process.cwd(), path, PLUGINS_ROOT, GC_STRESS], async () => {
93
109
  try {
94
110
  const fresh = await buildFresh(path);
95
111
  return { ...fresh, beside: nativesBeside(fresh.natives) };
@@ -127,11 +143,12 @@ async function buildWith(path: string, hoodExports: string): Promise<Omit<Compil
127
143
  let binary: Uint8Array | undefined;
128
144
  const natives: PluginNative[] = [];
129
145
  const playerFields = playerFieldsBuild();
146
+ const env = envBuild();
130
147
  // scripts/compile.ts's shared modules: a proxy for one another plugin owns.
131
148
  const shared = await sharedModulesBuild(PLUGINS_ROOT, entry);
132
149
 
133
150
  const { error, stderr } = await ascMain(
134
- [entry, HOOD_EXPORTS, '--outFile', 'plugin.wasm', '--optimize', '--exportTable', '--exportStart', '_start'],
151
+ stressed([entry, HOOD_EXPORTS, '--outFile', 'plugin.wasm', '--optimize', '--exportTable', '--exportStart', '_start']),
135
152
  {
136
153
  // scripts/compile.ts's readFile: the `~/` alias, the hood's exports and the fields plugins add to Player.
137
154
  readFile(filename: string, baseDir: string): string | null {
@@ -139,7 +156,10 @@ async function buildWith(path: string, hoodExports: string): Promise<Omit<Compil
139
156
 
140
157
  const file = ascPath(PLUGINS_ROOT, filename, baseDir);
141
158
  const text = sources.read(file);
142
- return text === null ? null : shared.read(file, playerFields.read(relative('.', (sources.real(file) ?? file)), text));
159
+ if (text === null) return null;
160
+ const shown = relative('.', (sources.real(file) ?? file));
161
+ env.read(shown, text);
162
+ return shared.read(file, playerFields.read(shown, text));
143
163
  },
144
164
  // Only the binary is wanted; nothing is written to disk.
145
165
  writeFile(_: string, contents: string | Uint8Array) {
@@ -151,12 +171,13 @@ async function buildWith(path: string, hoodExports: string): Promise<Omit<Compil
151
171
  );
152
172
 
153
173
  if (sources.problems.length) throw new Error(sources.problems.join('\n'));
174
+ if (env.problems.length) throw new Error(env.problems.join('\n'));
154
175
 
155
176
  if (error || !binary) {
156
177
  throw new Error(`${relative(process.cwd(), path)} does not compile:\n${stderr.toString() || String(error?.message ?? error)}`);
157
178
  }
158
179
 
159
- return { natives, binary, async: false };
180
+ return { natives, binary, async: false, env: env.entries() };
160
181
  }
161
182
 
162
183
  /**
@@ -188,19 +209,21 @@ export interface SourcesCompiled {
188
209
  /**
189
210
  * asc - the patched one, as the build runs it - on `sources` alone, a test's
190
211
  * snippet of the compiler at work, with the disk cache in front. Nothing is
191
- * read from disk, so the sources and `args` are the whole key.
212
+ * read from disk, so the sources and `args` are the whole key. `finished`
213
+ * adds what a full build does after asc's optimiser (finishing).
192
214
  */
193
- export async function compileSources(args: string[], sources: Record<string, string>): Promise<SourcesCompiled> {
194
- return cached<SourcesCompiled>(['sources', args, sources], async () => {
215
+ export async function compileSources(args: string[], sources: Record<string, string>, finished = false): Promise<SourcesCompiled> {
216
+ return cached<SourcesCompiled>(['sources', stressed(args), sources, finished], async () => {
195
217
  let binary: Uint8Array | null = null;
196
218
  const text: Record<string, string> = {};
197
- const { error, stderr } = await ascMain(args, {
219
+ const { error, stderr } = await ascMain(stressed(args), {
198
220
  readFile: (name: string) => sources[name] ?? null,
199
221
  writeFile(name: string, contents: string | Uint8Array) {
200
222
  if (typeof contents === 'string') text[name] = contents;
201
223
  else binary = contents;
202
224
  },
203
225
  listFiles: () => [],
226
+ transforms: finished ? [finishing(BASE_EXPORTS, null)] : [],
204
227
  });
205
228
  return error ? { error: stderr.toString(), binary: null, text } : { error: null, binary, text };
206
229
  });
@@ -24,15 +24,6 @@ interface Coroutine {
24
24
  drop: boolean;
25
25
  }
26
26
 
27
- // co_id(): a task id no other plugin on the server has, as module.cpp's is -
28
- // one counter for every plugin.
29
- let nextId = 0x60000000;
30
-
31
- /** The next of those ids. @internal */
32
- export function nextTaskId(): number {
33
- return nextId++;
34
- }
35
-
36
27
  export class Coroutines {
37
28
  /** What a trap inside a coroutine said: it drops that one coroutine, as on the server. */
38
29
  readonly traps: string[] = [];
@@ -81,7 +72,6 @@ export class Coroutines {
81
72
  this.exports.asyncify_start_unwind(coroutine.buffer);
82
73
  },
83
74
  co_wake: () => { this.wake = true; },
84
- co_id: nextTaskId,
85
75
  };
86
76
  }
87
77
 
@@ -22,6 +22,22 @@ export function bitsFloat(cell: number): number {
22
22
  return floatView[0];
23
23
  }
24
24
 
25
+ /** How many of the bytes are whole UTF-8 characters: a last one cut short is left out. */
26
+ function wholeUtf8(bytes: Uint8Array): number {
27
+ let lead = bytes.length;
28
+ while (lead > 0 && (bytes[lead - 1] & 0xC0) === 0x80) lead--;
29
+ if (lead === 0 || bytes[lead - 1] < 0xC0) return bytes.length;
30
+ const size = bytes[lead - 1] >= 0xF0 ? 4 : bytes[lead - 1] >= 0xE0 ? 3 : 2;
31
+ return bytes.length - (lead - 1) < size ? lead - 1 : bytes.length;
32
+ }
33
+
34
+ // The AMX heap the module passes text through (Frame::inText, outText):
35
+ // cells of the fake's own at addresses past any plugin's memory, which a
36
+ // native reads and writes as it does the plugin's.
37
+ const HEAP = 0x7F000000;
38
+ // The longest text that crosses, in bytes and its terminator (MAX_CROSSING_CELLS).
39
+ const CROSSING_CELLS = 16384;
40
+
25
41
  /** How many of the bytes fit in `max` without cutting a UTF-8 character in two. */
26
42
  export function utf8Fit(bytes: Uint8Array, max: number): number {
27
43
  let length = Math.min(bytes.length, max);
@@ -31,6 +47,8 @@ export function utf8Fit(bytes: Uint8Array, max: number): number {
31
47
 
32
48
  export class Memory {
33
49
  private dataView?: DataView;
50
+ private heap = new Int32Array(1 << 18);
51
+ private heapTop = 0;
34
52
 
35
53
  constructor(private exports: any) {}
36
54
 
@@ -42,11 +60,70 @@ export class Memory {
42
60
  }
43
61
 
44
62
  cell(pointer: number): number {
45
- return this.view.getInt32(pointer, true);
63
+ return pointer >= HEAP ? this.heap[(pointer - HEAP) >> 2] : this.view.getInt32(pointer, true);
64
+ }
65
+
66
+ byte(pointer: number): number {
67
+ return this.view.getUint8(pointer);
46
68
  }
47
69
 
48
70
  setCell(pointer: number, value: number): void {
49
- this.view.setInt32(pointer, value | 0, true);
71
+ if (pointer >= HEAP) this.heap[(pointer - HEAP) >> 2] = value | 0;
72
+ else this.view.setInt32(pointer, value | 0, true);
73
+ }
74
+
75
+ /**
76
+ * A native's call as the module's thunk makes it, by the native's marks in
77
+ * runtime/natives.txt: an `s` argument is the plugin's string, handed to
78
+ * the native as a Pawn string on the heap; a `t` one is a buffer the
79
+ * native fills on the heap, back in the plugin as UTF-8 bytes up to its end.
80
+ */
81
+ across(marks: readonly string[], args: number[], call: (cells: number[]) => number): number {
82
+ const top = this.heapTop;
83
+ try {
84
+ const cells = args.map((arg, i) => marks[i] === 's' ? this.heapText(this.string(arg)) : marks[i] === 't' ? this.heapCells(args[i + 1] + 1) : arg);
85
+ const result = call(cells);
86
+ marks.forEach((mark, i) => {
87
+ if (mark !== 't') return;
88
+ const bytes = this.bytes(cells[i], args[i + 1]);
89
+ const kept = bytes.subarray(0, wholeUtf8(bytes));
90
+ this.setRaw(args[i], Uint8Array.of(...kept, 0));
91
+ this.setTextHead(args[i], kept);
92
+ });
93
+ return result;
94
+ } finally {
95
+ this.heapTop = top;
96
+ }
97
+ }
98
+
99
+ /** Frame::backText's head before a text a native filled: its count, the top bit for one not ASCII, and its hash. */
100
+ private setTextHead(pointer: number, bytes: Uint8Array): void {
101
+ // As the module's: four bytes a word, little-endian, while all four are there, then a byte at a time.
102
+ let hash = 0;
103
+ let bits = 0;
104
+ let i = 0;
105
+ for (; i + 4 <= bytes.length; i += 4) hash = (Math.imul(hash, 31) + (bytes[i] | bytes[i + 1] << 8 | bytes[i + 2] << 16 | bytes[i + 3] << 24)) >>> 0;
106
+ for (; i < bytes.length; i++) hash = (Math.imul(hash, 31) + bytes[i]) >>> 0;
107
+ for (const byte of bytes) bits |= byte;
108
+ this.view.setUint32(pointer - 8, (bytes.length | (bits >= 0x80 ? 0x80000000 : 0)) >>> 0, true);
109
+ this.view.setUint32(pointer - 4, hash, true);
110
+ }
111
+
112
+ /** `count` empty cells of the heap: their address. */
113
+ private heapCells(count: number): number {
114
+ const at = this.heapTop;
115
+ this.heapTop += Math.min(Math.max(count, 1), CROSSING_CELLS);
116
+ this.heap.fill(0, at, this.heapTop);
117
+ return HEAP + at * 4;
118
+ }
119
+
120
+ /** Text as a Pawn string on the heap, cut at the longest that crosses, between letters. */
121
+ private heapText(text: string): number {
122
+ const bytes = encoder.encode(text);
123
+ const length = utf8Fit(bytes, CROSSING_CELLS - 1);
124
+ const at = this.heapCells(length + 1);
125
+ this.heap.set(bytes.subarray(0, length), (at - HEAP) >> 2);
126
+ return at;
50
127
  }
51
128
 
52
129
  float(pointer: number): number {
@@ -61,6 +138,16 @@ export class Memory {
61
138
  return [this.float(pointer), this.float(pointer + 4), this.float(pointer + 8)];
62
139
  }
63
140
 
141
+ /** A plugin's number (f64) read from its own memory. */
142
+ number(pointer: number): number {
143
+ return this.view.getFloat64(pointer, true);
144
+ }
145
+
146
+ /** A plugin's number (f64) in its own memory. */
147
+ setNumber(pointer: number, value: number): void {
148
+ this.view.setFloat64(pointer, value, true);
149
+ }
150
+
64
151
  setVector(pointer: number, value: number[]): void {
65
152
  for (let i = 0; i < 3; i++) this.setFloat(pointer + i * 4, value[i] ?? 0);
66
153
  }
@@ -200,7 +200,7 @@ function cvarNumber(value: number): string {
200
200
  return Number.isInteger(value) ? String(value) : value.toFixed(6);
201
201
  }
202
202
 
203
- function giveWeapon(target: FakePlayer | undefined, name: string): number {
203
+ export function giveWeapon(target: FakePlayer | undefined, name: string): number {
204
204
  if (!target) return 0;
205
205
  if (name === 'item_kevlar' || name === 'item_assaultsuit') {
206
206
  target.armor = 100;
@@ -214,7 +214,15 @@ function weaponByName(target: FakePlayer | undefined, name: string) {
214
214
  return target?.items.find(w => w.kind === name);
215
215
  }
216
216
 
217
- function switchTo(target: FakePlayer | undefined, name: string): number {
217
+ /** A command run as the player's, as the game takes it: a weapon chosen, a side picked in the team menu. */
218
+ export function clientCommand(target: FakePlayer | undefined, line: string): void {
219
+ target?.commands.push(line);
220
+ if (line.startsWith('weapon_')) switchTo(target, line);
221
+ const side = { 'jointeam 1': 'TERRORIST', 'jointeam 2': 'CT', 'jointeam 6': 'SPECTATOR' }[line];
222
+ if (target && side) target.team = side as TeamName;
223
+ }
224
+
225
+ export function switchTo(target: FakePlayer | undefined, name: string): number {
218
226
  const weapon = weaponByName(target, name);
219
227
  if (!target || !weapon) return 0;
220
228
  target.set('m_pActiveItem', weapon.id);
@@ -273,17 +281,26 @@ function setHamReturn(call: NativeCall, value: Value): number {
273
281
  return 1;
274
282
  }
275
283
 
276
- // The engine's own message for a temporary effect: it is no user message, so
277
- // no name get_user_msgid handed out stands for it.
278
- const SVC_TEMPENTITY = 23;
284
+ // The engine's own messages, by number: below 64 no user message lives, so
285
+ // no name get_user_msgid handed out stands for them.
286
+ const SVC_NAMES = (
287
+ 'BAD NOP DISCONNECT EVENT VERSION SETVIEW SOUND TIME PRINT STUFFTEXT SETANGLE SERVERINFO LIGHTSTYLE UPDATEUSERINFO'
288
+ + ' DELTADESCRIPTION CLIENTDATA STOPSOUND PINGS PARTICLE DAMAGE SPAWNSTATIC EVENT_RELIABLE SPAWNBASELINE TEMPENTITY'
289
+ + ' SETPAUSE SIGNONNUM CENTERPRINT KILLEDMONSTER FOUNDSECRET SPAWNSTATICSOUND INTERMISSION FINALE CDTRACK RESTORE'
290
+ + ' CUTSCENE WEAPONANIM DECALNAME ROOMTYPE ADDANGLE NEWUSERMSG PACKETENTITIES DELTAPACKETENTITIES CHOKE RESOURCELIST'
291
+ + ' NEWMOVEVARS RESOURCEREQUEST CUSTOMIZATION CROSSHAIRANGLE SOUNDFADE FILETXFERFAILED HLTV DIRECTOR VOICEINIT'
292
+ + ' VOICEDATA SENDEXTRAINFO TIMESCALE RESOURCELOCATION SENDCVARVALUE SENDCVARVALUE2'
293
+ ).split(' ');
279
294
 
280
295
  /**
281
- * message_begin: a user message by its name, or a temporary effect - with
282
- * who it goes to (`dest`, MSG_*) and the point it is seen from.
296
+ * message_begin: a user message by its name, or the engine's own (a temporary
297
+ * effect, movevars, ...) as `SVC_<name>` - with who it goes to (`dest`,
298
+ * MSG_*) and the point it is seen from.
283
299
  */
284
300
  function beginMessage(call: NativeCall, dest: number, type: number, player: number, origin: number[]): number {
285
- if (type === SVC_TEMPENTITY) {
286
- call.server.writing = { name: 'SVC_TEMPENTITY', player, args: [], dest, origin };
301
+ const svc = SVC_NAMES[type];
302
+ if (svc) {
303
+ call.server.writing = { name: `SVC_${svc}`, player, args: [], dest, origin };
287
304
  return 1;
288
305
  }
289
306
  const name = [...call.server.messageIds].find(([, id]) => id === type)?.[0];
@@ -298,7 +315,7 @@ function sendText(call: NativeCall, id: number, variant: 'chat' | 'center' | 'co
298
315
  const PRINT_VARIANTS = ['', 'notify', 'console', 'chat', 'center'] as const;
299
316
 
300
317
  // Reunion's client_auth_type by its number, as the player's API names each.
301
- const AUTH_TYPES = ['unknown', 'dproto', 'steam', 'steamEmu', 'revEmu', 'oldRevEmu', 'hltv', 'sc2009', 'avsmp', 'sxei', 'revEmu2013', 'sse3'];
318
+ export const AUTH_TYPES = ['unknown', 'dproto', 'steam', 'steamEmu', 'revEmu', 'oldRevEmu', 'hltv', 'sc2009', 'avsmp', 'sxei', 'revEmu2013', 'sse3'];
302
319
 
303
320
  /** A Pawn string of UTF-8 bytes - a path, a line of a file - as text. */
304
321
  function bytesText(call: NativeCall, pointer: number): string {
@@ -387,6 +404,24 @@ export const NATIVES: Record<string, Native> = {
387
404
  return 1;
388
405
  },
389
406
  close_dir: (c, [handle]) => +c.server.handles.delete(handle),
407
+ delete_file: (c, [name]) => +c.server.files.delete(normalizePath(bytesText(c, name))),
408
+ rename_file: (c, [from, to]) => {
409
+ const source = normalizePath(bytesText(c, from));
410
+ const bytes = c.server.files.get(source);
411
+ if (!bytes) return 0;
412
+ c.server.files.delete(source);
413
+ c.server.files.set(normalizePath(bytesText(c, to)), bytes);
414
+ return 1;
415
+ },
416
+ // An empty folder only, as the real one.
417
+ rmdir: (c, [name]) => {
418
+ const folder = normalizePath(bytesText(c, name));
419
+ const inside = [...c.server.files.keys(), ...c.server.folders].some(path => path.startsWith(`${folder}/`));
420
+ return +(!inside && c.server.folders.delete(folder));
421
+ },
422
+ file_size: (c, [name]) => c.server.files.get(normalizePath(bytesText(c, name)))?.length ?? -1,
423
+ // Every file of the fake changed when the server was made.
424
+ GetFileTime: c => Math.floor(c.server.startedAt / 1000),
390
425
  // 0 when made; the parent has to be there, as for the real one.
391
426
  mkdir: (c, [name]) => {
392
427
  const folder = normalizePath(bytesText(c, name));
@@ -473,6 +508,10 @@ export const NATIVES: Record<string, Native> = {
473
508
  if (!target) throw new Error(`menu_display: player ${id} is not in game`);
474
509
  c.server.displayMenu(target, c.server.menu(menu), page);
475
510
  },
511
+ show_menu: (c, [id, keys, text]) => {
512
+ c.server.showMenu(id, keys, c.memory.text(text));
513
+ return 1;
514
+ },
476
515
  menu_destroy: (c, [menu]) => {
477
516
  c.server.destroyMenu(menu);
478
517
  return 1;
@@ -519,7 +558,7 @@ export const NATIVES: Record<string, Native> = {
519
558
  },
520
559
 
521
560
  set_hudmessage: (c, [r, g, b, x, y, _effects, _fxtime, hold, _fadein, _fadeout, channel]) => {
522
- c.server.hud = { color: [r, g, b], x: bitsFloat(x), y: bitsFloat(y), hold: bitsFloat(hold), channel };
561
+ c.server.hud = { color: [r, g, b], x: bitsFloat(x), y: bitsFloat(y), hold: bitsFloat(hold), channel, line: 0, large: false };
523
562
  },
524
563
  show_hudmessage: (c, [id, format, ...tail]) => sendText(c, id, 'hud', formatPawn(c, c.memory.text(format), tail)),
525
564
 
@@ -633,13 +672,7 @@ export const NATIVES: Record<string, Native> = {
633
672
  rg_switch_weapon: (c, [id, weapon]) => switchTo(player(c, id), entity(c, weapon)?.classname ?? ''),
634
673
  user_has_weapon: (c, [id, weapon]) => +!!weaponByName(player(c, id), WEAPON_NAMES[weapon] ?? ''),
635
674
  engclient_cmd: (c, [id, command, arg1, arg2]) => {
636
- const target = player(c, id);
637
- const line = [command, arg1, arg2].map(p => c.memory.text(p)).filter(Boolean).join(' ');
638
- target?.commands.push(line);
639
- if (line.startsWith('weapon_')) switchTo(target, line);
640
- // The team menu's slots, as the game handles `jointeam`.
641
- const side = { 'jointeam 1': 'TERRORIST', 'jointeam 2': 'CT', 'jointeam 6': 'SPECTATOR' }[line];
642
- if (target && side) target.team = side as TeamName;
675
+ clientCommand(player(c, id), [command, arg1, arg2].map(p => c.memory.text(p)).filter(Boolean).join(' '));
643
676
  },
644
677
  rg_reset_maxspeed: () => {},
645
678
 
@@ -745,23 +778,23 @@ export const NATIVES: Record<string, Native> = {
745
778
 
746
779
  nvault_open: (c, [name]) => {
747
780
  const vault = c.memory.text(name);
748
- c.server.vault(vault);
781
+ c.server.storage(vault);
749
782
  c.server.vaultHandles.push(vault);
750
783
  return c.server.vaultHandles.length - 1;
751
784
  },
752
785
  nvault_close: () => {},
753
786
  nvault_set: (c, [handle, key, value]) => {
754
- c.server.vault(c.server.vaultHandles[handle]).set(c.memory.text(key), c.memory.text(value));
787
+ c.server.storage(c.server.vaultHandles[handle]).set(c.memory.text(key), c.memory.text(value));
755
788
  },
756
789
  nvault_lookup: (c, [handle, key, value, length, stampAt]) => {
757
- const found = c.server.vault(c.server.vaultHandles[handle]).get(c.memory.text(key));
790
+ const found = c.server.storage(c.server.vaultHandles[handle]).get(c.memory.text(key));
758
791
  if (found === undefined) return 0;
759
792
  c.memory.setText(value, length, found);
760
793
  if (stampAt) c.memory.setCell(stampAt, 0);
761
794
  return 1;
762
795
  },
763
796
  nvault_remove: (c, [handle, key]) => {
764
- c.server.vault(c.server.vaultHandles[handle]).delete(c.memory.text(key));
797
+ c.server.storage(c.server.vaultHandles[handle]).delete(c.memory.text(key));
765
798
  },
766
799
 
767
800
  // ------------------------------------------------------------ cvars
@@ -812,6 +845,12 @@ export const NATIVES: Record<string, Native> = {
812
845
  // ------------------------------------------------------------ the game (reapi, engine, resemiclip)
813
846
 
814
847
  register_dictionary: (c, [file]) => (c.server.loadDictionary(c.memory.text(file)) ? 1 : 0),
848
+ get_langsnum: c => c.server.dictionary.size,
849
+ get_lang: (c, [id, name]) => {
850
+ const code = [...c.server.dictionary.keys()][id] ?? '';
851
+ [code.charCodeAt(0) || 0, code.charCodeAt(1) || 0, 0].forEach((cell, at) => c.memory.setCell(name + at * 4, cell));
852
+ return 1;
853
+ },
815
854
  GetLangTransKey: (c, [key]) => c.server.langKey(c.memory.text(key)),
816
855
  LookupLangKey: (c, [out, size, key, id]) => {
817
856
  const text = c.server.lookupLang(c.memory.text(key), c.memory.cell(id));
@@ -1004,7 +1043,7 @@ export const NATIVES: Record<string, Native> = {
1004
1043
  GetHamReturnString: (c, [out, size]) => c.memory.setText(out, size, String(chain(c).answer)),
1005
1044
  GetHamReturnVector: (c, [out]) => c.memory.setVector(out, [0, 0, 0]),
1006
1045
  set_dhudmessage: (c, [r, g, b, x, y, _effects, _fxtime, hold]) => {
1007
- c.server.hud = { color: [r, g, b], x: bitsFloat(x), y: bitsFloat(y), hold: bitsFloat(hold), channel: -1 };
1046
+ c.server.hud = { color: [r, g, b], x: bitsFloat(x), y: bitsFloat(y), hold: bitsFloat(hold), channel: -1, line: 0, large: true };
1008
1047
  },
1009
1048
  // A server without reapi hears ReGameDLL's events through these (as/hlds.ts).
1010
1049
  // register_logevent(function[], argsnum, ...filters): server.gameLog() calls it.
@@ -1056,13 +1095,7 @@ export const NATIVES: Record<string, Native> = {
1056
1095
  if (c.server.players.length >= c.server.maxPlayers) return 0;
1057
1096
  return c.server.join(text(0), { bot: true, team: 'UNASSIGNED', alive: false }).id;
1058
1097
  }
1059
- if (type === constant('EngFunc_RunPlayerMove')) {
1060
- const bot = player(c, cell(0));
1061
- const [, yaw] = c.memory.vector(tail[1]);
1062
- const [forward, msec] = [float(2), cell(7)];
1063
- call(`RunPlayerMove ${cell(0)} ${vector(1)} ${forward} ${float(3)} ${float(4)} ${cell(5)} ${cell(6)} ${msec}`);
1064
- if (bot) bot.origin = [bot.origin[0] + Math.cos(yaw * Math.PI / 180) * forward * msec / 1000, bot.origin[1] + Math.sin(yaw * Math.PI / 180) * forward * msec / 1000, bot.origin[2]];
1065
- }
1098
+ if (type === constant('EngFunc_RunPlayerMove')) c.server.runPlayerMove(cell(0), c.memory.vector(tail[1]), float(2), float(3), float(4), cell(5), cell(6), cell(7));
1066
1099
  return 0;
1067
1100
  },
1068
1101
  dllfunc: (c, [type, ...tail]) => {
@@ -1111,7 +1144,11 @@ const SAME_AS: Record<string, string> = {
1111
1144
  write_long: 'write_byte',
1112
1145
  // A message sent through the engine, which every plugin's message hooks hear (emessage_end).
1113
1146
  emessage_begin: 'message_begin',
1147
+ emessage_begin_f: 'message_begin_f',
1114
1148
  ewrite_byte: 'write_byte',
1149
+ ewrite_char: 'write_byte',
1150
+ ewrite_coord_f: 'write_coord_f',
1151
+ ewrite_angle_f: 'write_coord_f',
1115
1152
  ewrite_long: 'write_byte',
1116
1153
  ewrite_short: 'write_byte',
1117
1154
  ewrite_string: 'write_string',