@amxts/core 0.1.0 → 0.2.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 (74) hide show
  1. package/README.md +18 -23
  2. package/README.ru.md +19 -22
  3. package/as/amxts.d.ts +63 -3
  4. package/as/constants.ts +684 -116
  5. package/as/entities.ts +1905 -1364
  6. package/as/events.ts +1694 -905
  7. package/as/facade.ts +1240 -220
  8. package/as/fetch.ts +1519 -0
  9. package/as/flags.ts +159 -145
  10. package/as/hlds.ts +1071 -0
  11. package/as/hooks.ts +5226 -5379
  12. package/as/kit.ts +15 -5
  13. package/as/lib/check.ts +1 -1
  14. package/as/natives.ts +2658 -2499
  15. package/as/remote.ts +39 -15
  16. package/as/tsconfig.json +6 -2
  17. package/as/vector.ts +12 -9
  18. package/includes/order.txt +22 -29
  19. package/includes/sources.json +0 -8
  20. package/node_modules/assemblyscript/std/assembly/index.d.ts +20 -11
  21. package/package.json +29 -7
  22. package/runtime/deps/assemblyscript/dist/asc.js +86 -16
  23. package/runtime/deps/assemblyscript/dist/assemblyscript.js +29 -29
  24. package/runtime/licenses/MPL-2.0.txt +373 -0
  25. package/runtime/licenses/README.md +5 -1
  26. package/runtime/licenses/curl-COPYING +22 -0
  27. package/runtime/licenses/libssh2-COPYING +43 -0
  28. package/runtime/licenses/mbedTLS-LICENSE +553 -0
  29. package/runtime/natives.txt +108 -105
  30. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +752 -296
  31. package/runtime/patches/wamr-2.4.5-amxts.patch +148 -0
  32. package/scripts/apply-docs.ts +2 -2
  33. package/scripts/auto-imports.ts +2 -2
  34. package/scripts/build-identity.ts +115 -0
  35. package/scripts/build-wasm.ts +101 -50
  36. package/scripts/check.ts +17 -3
  37. package/scripts/client-messages.ts +246 -0
  38. package/scripts/compile-pool.ts +128 -0
  39. package/scripts/compile-worker.ts +23 -0
  40. package/scripts/compile.ts +156 -59
  41. package/scripts/docker-server.ts +13 -3
  42. package/scripts/docs/as/amxts.ts +86 -0
  43. package/scripts/docs/as/facade.ts +688 -188
  44. package/scripts/docs/as/fetch.ts +805 -0
  45. package/scripts/docs/entities.ts +43 -144
  46. package/scripts/docs/events.ts +15 -26
  47. package/scripts/docs/game.ts +68 -22
  48. package/scripts/docs/messages.ts +330 -16
  49. package/scripts/editor-api.ts +101 -0
  50. package/scripts/hlds-events.ts +370 -0
  51. package/scripts/includes.ts +12 -10
  52. package/scripts/player-fields.ts +16 -15
  53. package/scripts/prebuilt.ts +65 -23
  54. package/scripts/prepare.ts +39 -28
  55. package/scripts/project.ts +120 -20
  56. package/scripts/reapi-events.ts +98 -0
  57. package/scripts/shared-modules.ts +85 -23
  58. package/scripts/source-map.ts +242 -0
  59. package/scripts/system.ts +1 -1
  60. package/scripts/typed-commands.ts +263 -0
  61. package/scripts/typed-configs.ts +23 -12
  62. package/scripts/ui.ts +3 -1
  63. package/scripts/upgrade-menus.ts +274 -0
  64. package/scripts/upgrade-names.ts +384 -0
  65. package/scripts/upgrade.ts +830 -0
  66. package/src/cli-api.mjs +37 -4
  67. package/src/system.mjs +74 -3
  68. package/src/testing/compile.ts +8 -5
  69. package/src/testing/natives.ts +199 -5
  70. package/src/testing/network.ts +531 -0
  71. package/src/testing/server.ts +565 -47
  72. package/src/testing/tables.ts +25 -0
  73. package/as/modules/http.ts +0 -184
  74. package/scripts/docs/as/modules/http.ts +0 -66
@@ -1,4 +1,6 @@
1
1
  import type { PluginNative } from './plugin-natives';
2
+ import type { Sources } from './project';
3
+ import type { PluginMap } from './source-map';
2
4
  import type { System } from './system';
3
5
  import { spawnSync } from 'node:child_process';
4
6
 
@@ -17,10 +19,13 @@ import * as assemblyscript from '../runtime/deps/assemblyscript/dist/assemblyscr
17
19
  // @ts-ignore - its types are beside it, under a path tsconfig does not map
18
20
  import binaryen from '../runtime/deps/assemblyscript/node_modules/binaryen/index.js';
19
21
  import { ascMain } from './asc';
22
+ import { ABI_SECTION, abiIdentity } from './build-identity';
20
23
  import { playerFieldsBuild } from './player-fields';
21
24
  import { includeName, nativeContract, nativesTransform, pawnInclude } from './plugin-natives';
22
25
  import { ascPath, sourcesFor } from './project';
26
+ import { targetTransforms } from './reapi-events';
23
27
  import { sharedModulesBuild } from './shared-modules';
28
+ import { displayName, MAP_SECTION, mapText, pluginMap, withSection } from './source-map';
24
29
  import { HOST_SYSTEM, TARGET_ABI } from './system';
25
30
 
26
31
  export interface Plugin {
@@ -96,6 +101,35 @@ export class HoistImports {
96
101
  /** The import a coroutine parks in - the one Asyncify unwinds from. */
97
102
  export const SUSPEND_IMPORT = 'env.co_suspend';
98
103
 
104
+ /** The import of a plugin that makes a promise: it is compiled again with ASYNC_EXPORTS. */
105
+ const WAKE_IMPORT = 'env.co_wake';
106
+
107
+ /**
108
+ * What wamrc keeps for a failed call's stack (scripts/source-map.ts): each
109
+ * function's frame with its index and the offset of the call it is in, or of
110
+ * the trap - a store at each call, a push and a pop at each function. No
111
+ * values: with them WAMR's frames are no longer the small ones. A dev build's
112
+ * alone (`amxts dev`, a quick build): measured, the frames took a fifth to a
113
+ * third of a call-heavy plugin's time, so a full build keeps none, and an
114
+ * error there says its message - and a throw or an abort its place.
115
+ */
116
+ const STACK_FLAGS = ['--enable-dump-call-stack', '--call-stack-features=bounds-checks,ip,func-idx,trap-ip'];
117
+
118
+ /**
119
+ * The map goes into the .aot either way, and so does the ABI the plugin is
120
+ * compiled against, which the module checks before it loads it
121
+ * (scripts/build-identity.ts).
122
+ */
123
+ const SECTIONS = `--emit-custom-sections=${MAP_SECTION},${ABI_SECTION}`;
124
+
125
+ /**
126
+ * The CPU the machine code is for: a Pentium 4's, SSE2. LLVM's i386 left
127
+ * alone does fractions on the x87, where a number made whole - every `%`,
128
+ * every number given where a cell goes - costs several times as much. Every
129
+ * CPU a game server runs on has SSE2.
130
+ */
131
+ const CPU = '--cpu=pentium4';
132
+
99
133
  /**
100
134
  * Where compilePlugin writes the Pawn include for a plugin's natives: beside
101
135
  * the .aot, named after the plugin - or, for a plugin that implements an
@@ -134,6 +168,9 @@ export function compileToMachineCode(plugin: Plugin, wasm: string, natives: Plug
134
168
  '--target=i386',
135
169
  `--target-abi=${TARGET_ABI[plugin.system ?? HOST_SYSTEM]}`,
136
170
  `--native-signatures=${plugin.signatures}`,
171
+ ...(plugin.quick ? STACK_FLAGS : []),
172
+ SECTIONS,
173
+ CPU,
137
174
  ...level,
138
175
  '-o',
139
176
  part,
@@ -150,12 +187,13 @@ export function compileToMachineCode(plugin: Plugin, wasm: string, natives: Plug
150
187
 
151
188
  if (compile.status !== 0) {
152
189
  rmSync(part, { force: true });
153
- return out;
190
+ // A crash says nothing: still a failure, not an empty problem
191
+ return out.trim() || `wamrc stopped (${compile.status ?? compile.signal}) without a word`;
154
192
  }
155
193
 
156
194
  // A signature that disagrees with its import is not an error to wamrc: it
157
- // drops that native to the generic path, 28 ns a call instead of 2, and
158
- // says so only in this warning. That means the API and the module have
195
+ // drops that native to the generic path, many times slower than the direct
196
+ // call, and says so only in this warning. That means the API and the module have
159
197
  // drifted apart, which is worth refusing rather than running slowly.
160
198
  if (/failed to check signature/.test(out)) {
161
199
  rmSync(part, { force: true });
@@ -182,9 +220,10 @@ export function writeInclude(output: string, natives: PluginNative[]): void {
182
220
 
183
221
  /**
184
222
  * The first half of compilePlugin: the plugin's .ts to `wasm`, ready for
185
- * wamrc. Tests run the result as it is; with `names` it keeps its function
186
- * names, so a test can see what Asyncify instrumented. With `natives`, the
187
- * plugin's `export function`s become natives, listed there.
223
+ * wamrc, with its map (scripts/source-map.ts). Tests run the result as it is;
224
+ * with `names` it keeps its function names, so a test can see what Asyncify
225
+ * instrumented. With `natives`, the plugin's `export function`s become
226
+ * natives, listed there.
188
227
  */
189
228
  export async function compileToWasm(
190
229
  plugin: Pick<Plugin, 'source' | 'root' | 'quick'>,
@@ -193,53 +232,93 @@ export async function compileToWasm(
193
232
  natives?: PluginNative[],
194
233
  ): Promise<string | null> {
195
234
  // A quick build leaves the optimising to one Binaryen pass afterwards.
196
- const flags = [...(plugin.quick ? [] : ['--optimize']), ...(names ? ['--debug'] : [])];
197
- const level = plugin.quick ? OPTIMIZE.quick : OPTIMIZE.full;
198
- let problem = await compileWasm(plugin, wasm, BASE_EXPORTS, flags, natives);
199
- if (problem) return problem;
235
+ const flags = [...(plugin.quick ? [] : ['--optimize']), ...(names ? ['--debug'] : []), '--sourceMap'];
236
+ const level = plugin.quick ? OPTIMIZE.quick : null;
237
+ let made = await compileWasm(plugin, BASE_EXPORTS, flags, level, natives);
200
238
 
201
239
  // A plugin that makes a promise - an async function, fetch, sleep - has
202
240
  // the host drive its jobs, so it is compiled again with the scheduler's
203
- // exports. Only then, and only when a coroutine can park, does Asyncify
204
- // run: a plugin without either is the same wasm it always was.
205
- if (importsOf(readFileSync(wasm)).has('env.co_wake')) {
241
+ // exports.
242
+ if (typeof made !== 'string' && importsOf(made.binary).has(WAKE_IMPORT)) {
206
243
  if (natives) natives.length = 0;
207
- problem = await compileWasm(plugin, wasm, ASYNC_EXPORTS, flags, natives);
208
- if (problem) return problem;
244
+ made = await compileWasm(plugin, ASYNC_EXPORTS, flags, level, natives);
209
245
  }
210
- const binary = readFileSync(wasm);
211
- if (importsOf(binary).has(SUSPEND_IMPORT)) writeFileSync(wasm, asyncify(binary, names, level));
212
- else if (plugin.quick) writeFileSync(wasm, optimized(binary, level));
246
+ if (typeof made === 'string') return made;
247
+ mkdirSync(dirname(resolve(wasm)), { recursive: true });
248
+ writeFileSync(wasm, withSection(withSection(made.binary, MAP_SECTION, mapText(made.map)), ABI_SECTION, abiIdentity()));
213
249
  return null;
214
250
  }
215
251
 
252
+ /**
253
+ * The last transform of a compile: Binaryen's work after asc's own, on the
254
+ * module asc is about to write - a quick build's optimisation at `level`,
255
+ * and Asyncify for a plugin whose coroutines can park. It runs there rather
256
+ * than on the written binary so that the source map asc writes beside it is
257
+ * of the code that runs: Binaryen reads a binary without its map. asc has no
258
+ * hook after its optimiser, so this wraps the module's emitBinary, which asc
259
+ * calls once to write the module. `done` gets the functions' names, by wasm
260
+ * index, as they are written.
261
+ *
262
+ * A plugin that makes a promise but was compiled without the scheduler's
263
+ * exports is compiled again (compileToWasm), so nothing is done for it.
264
+ */
265
+ export function finishing(hoodExports: string, level: number | null, done: (first: number, names: string[]) => void = () => {}) {
266
+ return {
267
+ afterCompile(module: any) {
268
+ const emit = module.emitBinary.bind(module);
269
+ module.emitBinary = (url?: string) => {
270
+ const imports = new Set(functionsOf(module).map(each => each.imported));
271
+ if (hoodExports === ASYNC_EXPORTS || !imports.has(WAKE_IMPORT)) {
272
+ if (imports.has(SUSPEND_IMPORT)) asyncify(module, level ?? OPTIMIZE.full);
273
+ else if (level !== null) optimize(module, level);
274
+ }
275
+ const functions = functionsOf(module);
276
+ const first = functions.filter(each => each.imported).length;
277
+ done(first, functions.slice(first).map(each => each.name));
278
+ return emit(url);
279
+ };
280
+ },
281
+ };
282
+ }
283
+
284
+ /**
285
+ * The module's functions in the order of their wasm indexes - Binaryen
286
+ * writes the imported ones first, then the rest, each in its own order -
287
+ * with each one's name, and `module.name` when it is imported.
288
+ */
289
+ function functionsOf(module: any): { name: string; imported: string | null }[] {
290
+ const functions = Array.from({ length: module.getNumFunctions() }, (_, i) => binaryen.getFunctionInfo(module.getFunctionByIndex(i)));
291
+ const ordered = [...functions.filter(info => info.module), ...functions.filter(info => !info.module)];
292
+ return ordered.map(info => ({ name: info.name, imported: info.module ? `${info.module}.${info.base}` : null }));
293
+ }
294
+
216
295
  /** Binaryen's optimisation at `level`: what a quick build runs in place of asc's. */
217
- function optimized(wasm: Uint8Array, level: number): Uint8Array {
218
- const module = binaryen.readBinary(wasm);
219
- try {
220
- module.setFeatures(binaryen.Features.All);
221
- binaryen.setOptimizeLevel(level);
222
- binaryen.setShrinkLevel(0);
223
- module.optimize();
224
- return module.emitBinary();
225
- } finally {
226
- module.dispose();
227
- }
296
+ function optimize(module: any, level: number) {
297
+ binaryen.setOptimizeLevel(level);
298
+ binaryen.setShrinkLevel(0);
299
+ module.optimize();
300
+ }
301
+
302
+ /** What asc made of a plugin: the binary, and its map. */
303
+ interface Compiled {
304
+ binary: Uint8Array;
305
+ map: PluginMap;
228
306
  }
229
307
 
230
308
  /**
231
- * asc, from the plugin's .ts to `wasm`, with `hoodExports` as the second entry.
232
- * With `natives`, the plugin's `export function`s become its natives and are
233
- * listed there; without, they stay plain wasm exports - what a test host that
234
- * calls a fixture's functions by name wants.
309
+ * asc, from the plugin's .ts to a binary and its map, with `hoodExports` as
310
+ * the second entry and `level` a quick build's (finishing). With `natives`,
311
+ * the plugin's `export function`s become its natives and are listed there;
312
+ * without, they stay plain wasm exports - what a test host that calls a
313
+ * fixture's functions by name wants. What went wrong, as text.
235
314
  */
236
315
  async function compileWasm(
237
316
  plugin: Pick<Plugin, 'source' | 'root'>,
238
- wasm: string,
239
317
  hoodExports: string,
240
318
  flags: string[],
319
+ level: number | null,
241
320
  natives?: PluginNative[],
242
- ): Promise<string | null> {
321
+ ): Promise<Compiled | string> {
243
322
  // --exportTable is not optional: a handler reaches the module as its index
244
323
  // in the function table, and without this asc emits no table at all, so
245
324
  // every index points at nothing.
@@ -260,6 +339,9 @@ async function compileWasm(
260
339
  return String((problem as Error).message ?? problem);
261
340
  }
262
341
 
342
+ let binary: Uint8Array | null = null;
343
+ let sourceMap = '';
344
+ let functions = { first: 0, names: [] as string[] };
263
345
  const { error, stderr } = await ascMain(
264
346
  [
265
347
  entry,
@@ -268,7 +350,7 @@ async function compileWasm(
268
350
  // entry file's exports reach the wasm).
269
351
  HOOD_EXPORTS,
270
352
  '--outFile',
271
- wasm,
353
+ 'plugin.wasm',
272
354
  '--exportTable',
273
355
  ...flags,
274
356
  ],
@@ -282,21 +364,25 @@ async function compileWasm(
282
364
  return text === null ? null : shared.read(path, playerFields.read(relative('.', (sources.real(path) ?? path)), text));
283
365
  },
284
366
 
285
- writeFile(filename: string, contents: string | Uint8Array): void {
286
- // The name asc passes back is the one it was given in --outFile,
287
- // so it is already where the caller wants it.
288
- const path = resolve(filename);
289
- mkdirSync(dirname(path), { recursive: true });
290
- writeFileSync(path, contents);
367
+ // The binary and its source map, for compileToWasm to write as one.
368
+ writeFile(_: string, contents: string | Uint8Array): void {
369
+ if (typeof contents === 'string') sourceMap = contents;
370
+ else binary = contents;
291
371
  },
292
372
 
293
373
  listFiles(): string[] {
294
374
  return [];
295
375
  },
296
376
 
297
- transforms: natives
298
- ? [HoistImports, nativesTransform(entry, natives, plugin.root), playerFields.transform, shared.transform]
299
- : [HoistImports, playerFields.transform, shared.transform],
377
+ transforms: [
378
+ HoistImports,
379
+ ...(natives ? [nativesTransform(entry, natives, plugin.root)] : []),
380
+ playerFields.transform,
381
+ shared.transform,
382
+ // A project for plain HLDS listens for no event reapi alone delivers.
383
+ ...targetTransforms(sources.project.config?.target),
384
+ finishing(hoodExports, level, (first, names) => (functions = { first, names })),
385
+ ],
300
386
  },
301
387
  );
302
388
 
@@ -306,7 +392,26 @@ async function compileWasm(
306
392
 
307
393
  // A transform's refusal - a native whose signature cannot cross - is an
308
394
  // error with nothing written to stderr.
309
- return error ? (stderr.toString() || String(error.message ?? error)) : null;
395
+ if (error || !binary) return stderr.toString() || String(error?.message ?? error);
396
+ // A quick build is a dev build (`amxts dev`): its map names the project's
397
+ // folder, where the module finds the sources to show a failed line. A
398
+ // build to ship names none - the machine's folders are not the server's.
399
+ const root = level === null ? '' : sources.project.dir;
400
+ return { binary, map: pluginMap(sourceMap, file => fileName(sources, plugin.root, file), functions.first, functions.names.map(displayName), root) };
401
+ }
402
+
403
+ /**
404
+ * A file of asc's source map as the plugin's map names it: as the project
405
+ * names it - `plugins/shop.ts`, `node_modules/@amxts/core/as/facade.ts` - or
406
+ * null for AssemblyScript's standard library, whose lines would double the
407
+ * map and mean nothing to an author; its functions keep their names.
408
+ */
409
+ function fileName(sources: Sources, root: string, file: string): string | null {
410
+ // asc reads `~/` - the plugins folder - as a library of its own
411
+ const hood = /^~lib\/~\/(.*)$/.exec(file);
412
+ if (!hood && file.startsWith('~lib/')) return null;
413
+ const path = hood ? resolve(root, hood[1]) : ascPath(root, file, '');
414
+ return relative(sources.project.dir, sources.real(path) ?? path).replace(/\\/g, '/');
310
415
  }
311
416
 
312
417
  /**
@@ -319,28 +424,20 @@ async function compileWasm(
319
424
  * prologue asks co_spawn to call it - so no `await` is ever reached through
320
425
  * a function table, and a listener that is async returns to its caller at its
321
426
  * first `await` like any other call. Nothing can be missing from the list,
322
- * which is the failure that hangs rather than traps.
427
+ * which is the failure that hangs rather than traps. Then the optimiser at
428
+ * `level` - asc's own, or a quick build's lower one - so the rest of the
429
+ * module comes out as it went in.
323
430
  */
324
- export function asyncify(wasm: Uint8Array, names = false, level = OPTIMIZE.full): Uint8Array {
325
- const module = binaryen.readBinary(wasm);
431
+ function asyncify(module: any, level: number) {
326
432
  try {
327
- module.setFeatures(binaryen.Features.All);
328
433
  binaryen.setPassArgument('asyncify-imports', SUSPEND_IMPORT);
329
434
  binaryen.setPassArgument('asyncify-ignore-indirect', '1');
330
- // asc's own --optimize levels (a quick build's lower one), so the rest
331
- // of the module comes out as it went in.
332
- binaryen.setOptimizeLevel(level);
333
- binaryen.setShrinkLevel(0);
334
- binaryen.setDebugInfo(names);
335
435
  module.runPasses(['asyncify']);
336
- module.optimize();
436
+ optimize(module, level);
337
437
  if (!module.validate()) throw new Error('Asyncify produced an invalid module');
338
- return module.emitBinary();
339
438
  } finally {
340
439
  binaryen.setPassArgument('asyncify-imports', null);
341
440
  binaryen.setPassArgument('asyncify-ignore-indirect', null);
342
- binaryen.setDebugInfo(false);
343
- module.dispose();
344
441
  }
345
442
  }
346
443
 
@@ -1,15 +1,25 @@
1
- // The Docker server a project runs on (docker/server, the amxts-server image):
2
- // which running containers mount the project, and their console.
1
+ // The Docker server a project runs on (docker/server, published as
2
+ // ghcr.io/amxts/server): which running containers mount the project, and
3
+ // their console.
3
4
  //
4
5
  // `amxts dev --docker` builds into dist/, which such a container reads where
5
6
  // it is - its module reloads a plugin whose .aot changed - so nothing is
6
7
  // deployed. What is left is showing the developer the server's answer, and
7
8
  // that is the container's console: its amxts lines, as they come.
8
9
  import { spawn, spawnSync } from 'node:child_process';
10
+ import { readFileSync } from 'node:fs';
9
11
  import { resolve } from 'node:path';
10
12
  import process from 'node:process';
13
+ import { serverImage } from './system';
11
14
  import { c } from './ui';
12
15
 
16
+ /**
17
+ * The server image of this core's version. Its module loads only the .aot of
18
+ * the same release's wamrc, so a project runs on the image of its own core;
19
+ * `bun run server:image` builds it from a checkout under the same name.
20
+ */
21
+ export const SERVER_IMAGE = serverImage(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
22
+
13
23
  /** Where the image takes a project from. */
14
24
  const PROJECT_MOUNT = '/project';
15
25
 
@@ -69,5 +79,5 @@ export function followConsoles(names: string[]): void {
69
79
 
70
80
  /** The command that starts the server for this project. */
71
81
  export function runCommand(projectDir: string): string {
72
- return `docker run --rm -it -p 27015:27015/udp -v "${resolve(projectDir)}:${PROJECT_MOUNT}" amxts-server`;
82
+ return `docker run --rm -it -p 27015:27015/udp -v "${resolve(projectDir)}:${PROJECT_MOUNT}" ${SERVER_IMAGE}`;
73
83
  }
@@ -207,4 +207,90 @@ export default {
207
207
  en: `Stops calling a listener added with \`addEventListener\` - the same function and the same field.`,
208
208
  ru: `Перестаёт вызывать обработчик, добавленный через \`addEventListener\`, — ту же функцию с тем же полем.`,
209
209
  },
210
+ 'Server.addCommand': {
211
+ en: `
212
+ Adds a command players type, by its usage: the name, then the
213
+ arguments, \`<name>\` required and \`[name]\` optional. Their types are an
214
+ interface, the type argument; without it each is text.
215
+
216
+ \`\`\`ts
217
+ interface KickArgs {
218
+ target: Player;
219
+ reason?: string;
220
+ }
221
+
222
+ server.addCommand<KickArgs>("/kick <target> [reason]", ({ player, target, reason }) => {
223
+ target.kick(reason ?? \`Kicked by \${player.name}\`);
224
+ }, { access: "kick" });
225
+ server.addCommand("/hp", ({ player }) => print(player, \`\${player.health} HP\`));
226
+ \`\`\`
227
+
228
+ A name with \`/\` is a chat command, one without a console command, and
229
+ \`"say <phrase>"\` a phrase written in chat. A \`number\` argument is
230
+ parsed, a \`Player\` found by \`#userid\`, the whole name or a part of it,
231
+ a \`string\` taken as it is - the last one takes the rest of the line. A
232
+ word that is not what the command takes answers the player with the
233
+ usage, and the handler does not run.
234
+
235
+ Pawn: \`register_clcmd\`
236
+ `,
237
+ ru: `
238
+ Добавляет команду, которую набирают игроки, по её использованию: имя,
239
+ затем аргументы, \`<name>\` — обязательный, \`[name]\` — необязательный.
240
+ Их типы — интерфейс, аргумент типа; без него каждый — текст.
241
+
242
+ \`\`\`ts
243
+ interface KickArgs {
244
+ target: Player;
245
+ reason?: string;
246
+ }
247
+
248
+ server.addCommand<KickArgs>("/kick <target> [reason]", ({ player, target, reason }) => {
249
+ target.kick(reason ?? \`Kicked by \${player.name}\`);
250
+ }, { access: "kick" });
251
+ server.addCommand("/hp", ({ player }) => print(player, \`\${player.health} HP\`));
252
+ \`\`\`
253
+
254
+ Имя со \`/\` — команда чата, без него — команда консоли, а
255
+ \`"say <фраза>"\` — фраза, написанная в чат. Аргумент \`number\`
256
+ разбирается как число, \`Player\` находится по \`#userid\`, имени целиком
257
+ или его части, \`string\` берётся как есть — последний забирает остаток
258
+ строки. На слово, которое команда не принимает, игрок получает её
259
+ использование, и обработчик не запускается.
260
+
261
+ Pawn: \`register_clcmd\`
262
+ `,
263
+ },
264
+ 'Server.addServerCommand': {
265
+ en: `
266
+ Adds a command of the server console - typed there, sent over rcon or
267
+ run by another plugin - by its usage, its arguments read as a player's
268
+ command's are. No player types it.
269
+
270
+ \`\`\`ts
271
+ interface ResetArgs {
272
+ what?: "scores" | "all";
273
+ }
274
+
275
+ server.addServerCommand<ResetArgs>("myplugin_reset [what]", ({ what }) => reset(what ?? "all"));
276
+ \`\`\`
277
+
278
+ Pawn: \`register_srvcmd\`
279
+ `,
280
+ ru: `
281
+ Добавляет команду консоли сервера — её набирают там, присылают по rcon
282
+ или запускает другой плагин — по её использованию; аргументы читаются
283
+ так же, как у команды игрока. Игрок её не набирает.
284
+
285
+ \`\`\`ts
286
+ interface ResetArgs {
287
+ what?: "scores" | "all";
288
+ }
289
+
290
+ server.addServerCommand<ResetArgs>("myplugin_reset [what]", ({ what }) => reset(what ?? "all"));
291
+ \`\`\`
292
+
293
+ Pawn: \`register_srvcmd\`
294
+ `,
295
+ },
210
296
  };