@amxts/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/README.ru.md +162 -0
- package/amxmodx/base/include/amxconst.inc +594 -0
- package/amxmodx/base/include/amxmisc.inc +902 -0
- package/amxmodx/base/include/amxmodx.inc +3447 -0
- package/amxmodx/base/include/amxmodx_version.inc +20 -0
- package/amxmodx/base/include/cellarray.inc +525 -0
- package/amxmodx/base/include/cellstack.inc +166 -0
- package/amxmodx/base/include/celltrie.inc +412 -0
- package/amxmodx/base/include/core.inc +202 -0
- package/amxmodx/base/include/csstats.inc +298 -0
- package/amxmodx/base/include/csstats_const.inc +29 -0
- package/amxmodx/base/include/cstrike.inc +1287 -0
- package/amxmodx/base/include/cstrike_const.inc +521 -0
- package/amxmodx/base/include/csx.inc +259 -0
- package/amxmodx/base/include/cvars.inc +547 -0
- package/amxmodx/base/include/datapack.inc +161 -0
- package/amxmodx/base/include/dbi.inc +154 -0
- package/amxmodx/base/include/dodconst.inc +152 -0
- package/amxmodx/base/include/dodfun.inc +152 -0
- package/amxmodx/base/include/dodstats.inc +69 -0
- package/amxmodx/base/include/dodx.inc +161 -0
- package/amxmodx/base/include/engine.inc +1343 -0
- package/amxmodx/base/include/engine_const.inc +310 -0
- package/amxmodx/base/include/engine_stocks.inc +248 -0
- package/amxmodx/base/include/esf.inc +70 -0
- package/amxmodx/base/include/esf_const.inc +74 -0
- package/amxmodx/base/include/fakemeta.inc +1127 -0
- package/amxmodx/base/include/fakemeta_const.inc +793 -0
- package/amxmodx/base/include/fakemeta_stocks.inc +395 -0
- package/amxmodx/base/include/fakemeta_util.inc +882 -0
- package/amxmodx/base/include/file.inc +666 -0
- package/amxmodx/base/include/float.inc +422 -0
- package/amxmodx/base/include/fun.inc +333 -0
- package/amxmodx/base/include/gameconfig.inc +93 -0
- package/amxmodx/base/include/geoip.inc +264 -0
- package/amxmodx/base/include/ham_const.inc +4352 -0
- package/amxmodx/base/include/hamsandwich.inc +439 -0
- package/amxmodx/base/include/hlsdk_const.inc +808 -0
- package/amxmodx/base/include/json.inc +777 -0
- package/amxmodx/base/include/lang.inc +119 -0
- package/amxmodx/base/include/message_const.inc +1130 -0
- package/amxmodx/base/include/message_stocks.inc +161 -0
- package/amxmodx/base/include/messages.inc +604 -0
- package/amxmodx/base/include/newmenus.inc +387 -0
- package/amxmodx/base/include/ns.inc +754 -0
- package/amxmodx/base/include/ns_const.inc +196 -0
- package/amxmodx/base/include/nvault.inc +154 -0
- package/amxmodx/base/include/regex.inc +348 -0
- package/amxmodx/base/include/sockets.inc +183 -0
- package/amxmodx/base/include/sorting.inc +107 -0
- package/amxmodx/base/include/sqlx.inc +582 -0
- package/amxmodx/base/include/string.inc +786 -0
- package/amxmodx/base/include/string_const.inc +156 -0
- package/amxmodx/base/include/string_stocks.inc +325 -0
- package/amxmodx/base/include/textparse_ini.inc +214 -0
- package/amxmodx/base/include/textparse_smc.inc +261 -0
- package/amxmodx/base/include/tfcconst.inc +84 -0
- package/amxmodx/base/include/tfcstats.inc +62 -0
- package/amxmodx/base/include/tfcx.inc +148 -0
- package/amxmodx/base/include/time.inc +118 -0
- package/amxmodx/base/include/tsconst.inc +154 -0
- package/amxmodx/base/include/tsfun.inc +198 -0
- package/amxmodx/base/include/tsstats.inc +65 -0
- package/amxmodx/base/include/tsx.inc +88 -0
- package/amxmodx/base/include/vault.inc +60 -0
- package/amxmodx/base/include/vector.inc +131 -0
- package/amxmodx/base/include/xs.inc +1570 -0
- package/as/amxts.d.ts +153 -0
- package/as/constants.ts +4730 -0
- package/as/effects.ts +1449 -0
- package/as/entities.ts +5482 -0
- package/as/events.ts +3054 -0
- package/as/facade.ts +4639 -0
- package/as/flags.ts +299 -0
- package/as/fs.ts +287 -0
- package/as/hooks.ts +18536 -0
- package/as/kit.ts +25 -0
- package/as/lib/check.ts +86 -0
- package/as/modules/http.ts +184 -0
- package/as/natives.ts +11845 -0
- package/as/os.ts +30 -0
- package/as/promise.ts +1392 -0
- package/as/promise.types.d.ts +82 -0
- package/as/remote.ts +288 -0
- package/as/tsconfig.json +22 -0
- package/as/vector.ts +65 -0
- package/bin/amxts.mjs +10 -0
- package/includes/menu_core.inc +355 -0
- package/includes/order.txt +54 -0
- package/includes/sources.json +27 -0
- package/includes/universal_config.inc +355 -0
- package/lint/oxlint-plugin.mjs +77 -0
- package/node_modules/assemblyscript/LICENSE +201 -0
- package/node_modules/assemblyscript/NOTICE +93 -0
- package/node_modules/assemblyscript/package.json +63 -0
- package/node_modules/assemblyscript/std/README.md +6 -0
- package/node_modules/assemblyscript/std/assembly/array.ts +526 -0
- package/node_modules/assemblyscript/std/assembly/arraybuffer.ts +77 -0
- package/node_modules/assemblyscript/std/assembly/atomics.ts +127 -0
- package/node_modules/assemblyscript/std/assembly/bindings/asyncify.ts +16 -0
- package/node_modules/assemblyscript/std/assembly/bindings/dom.ts +291 -0
- package/node_modules/assemblyscript/std/assembly/bindings/node.ts +6 -0
- package/node_modules/assemblyscript/std/assembly/builtins.ts +2631 -0
- package/node_modules/assemblyscript/std/assembly/compat.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/console.ts +42 -0
- package/node_modules/assemblyscript/std/assembly/crypto.ts +9 -0
- package/node_modules/assemblyscript/std/assembly/dataview.ts +181 -0
- package/node_modules/assemblyscript/std/assembly/date.ts +373 -0
- package/node_modules/assemblyscript/std/assembly/diagnostics.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/error.ts +44 -0
- package/node_modules/assemblyscript/std/assembly/function.ts +38 -0
- package/node_modules/assemblyscript/std/assembly/index.d.ts +2822 -0
- package/node_modules/assemblyscript/std/assembly/iterator.ts +35 -0
- package/node_modules/assemblyscript/std/assembly/map.ts +260 -0
- package/node_modules/assemblyscript/std/assembly/math.ts +3289 -0
- package/node_modules/assemblyscript/std/assembly/memory.ts +123 -0
- package/node_modules/assemblyscript/std/assembly/number.ts +388 -0
- package/node_modules/assemblyscript/std/assembly/object.ts +36 -0
- package/node_modules/assemblyscript/std/assembly/performance.ts +9 -0
- package/node_modules/assemblyscript/std/assembly/polyfills.ts +27 -0
- package/node_modules/assemblyscript/std/assembly/process.ts +50 -0
- package/node_modules/assemblyscript/std/assembly/reference.ts +48 -0
- package/node_modules/assemblyscript/std/assembly/regexp.ts +12 -0
- package/node_modules/assemblyscript/std/assembly/rt/README.md +83 -0
- package/node_modules/assemblyscript/std/assembly/rt/common.ts +81 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-incremental.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-minimal.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-stub.ts +1 -0
- package/node_modules/assemblyscript/std/assembly/rt/index.d.ts +37 -0
- package/node_modules/assemblyscript/std/assembly/rt/itcms.ts +419 -0
- package/node_modules/assemblyscript/std/assembly/rt/rtrace.ts +15 -0
- package/node_modules/assemblyscript/std/assembly/rt/stub.ts +133 -0
- package/node_modules/assemblyscript/std/assembly/rt/tcms.ts +254 -0
- package/node_modules/assemblyscript/std/assembly/rt/tlsf.ts +592 -0
- package/node_modules/assemblyscript/std/assembly/rt.ts +87 -0
- package/node_modules/assemblyscript/std/assembly/set.ts +220 -0
- package/node_modules/assemblyscript/std/assembly/shared/feature.ts +65 -0
- package/node_modules/assemblyscript/std/assembly/shared/runtime.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/target.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/tsconfig.json +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/typeinfo.ts +72 -0
- package/node_modules/assemblyscript/std/assembly/staticarray.ts +401 -0
- package/node_modules/assemblyscript/std/assembly/string.ts +847 -0
- package/node_modules/assemblyscript/std/assembly/symbol.ts +114 -0
- package/node_modules/assemblyscript/std/assembly/table.ts +16 -0
- package/node_modules/assemblyscript/std/assembly/tsconfig.json +6 -0
- package/node_modules/assemblyscript/std/assembly/typedarray.ts +1945 -0
- package/node_modules/assemblyscript/std/assembly/uri.ts +17 -0
- package/node_modules/assemblyscript/std/assembly/util/bytes.ts +107 -0
- package/node_modules/assemblyscript/std/assembly/util/casemap.ts +497 -0
- package/node_modules/assemblyscript/std/assembly/util/error.ts +54 -0
- package/node_modules/assemblyscript/std/assembly/util/hash.ts +117 -0
- package/node_modules/assemblyscript/std/assembly/util/math.ts +1922 -0
- package/node_modules/assemblyscript/std/assembly/util/memory.ts +290 -0
- package/node_modules/assemblyscript/std/assembly/util/number.ts +873 -0
- package/node_modules/assemblyscript/std/assembly/util/sort.ts +313 -0
- package/node_modules/assemblyscript/std/assembly/util/string.ts +1202 -0
- package/node_modules/assemblyscript/std/assembly/util/uri.ts +275 -0
- package/node_modules/assemblyscript/std/assembly/vector.ts +4 -0
- package/node_modules/assemblyscript/std/assembly.json +16 -0
- package/node_modules/assemblyscript/std/portable/index.d.ts +462 -0
- package/node_modules/assemblyscript/std/portable/index.js +416 -0
- package/node_modules/assemblyscript/std/portable.json +11 -0
- package/node_modules/assemblyscript/std/types/assembly/index.d.ts +1 -0
- package/node_modules/assemblyscript/std/types/assembly/package.json +3 -0
- package/node_modules/assemblyscript/std/types/portable/index.d.ts +1 -0
- package/node_modules/assemblyscript/std/types/portable/package.json +3 -0
- package/node_modules/assemblyscript/tsconfig-base.json +13 -0
- package/package.json +112 -0
- package/runtime/deps/assemblyscript/LICENSE +201 -0
- package/runtime/deps/assemblyscript/NOTICE +93 -0
- package/runtime/deps/assemblyscript/bin/asc.js +35 -0
- package/runtime/deps/assemblyscript/dist/asc.js +25035 -0
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +337 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/LICENSE +201 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/index.js +28905 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/package.json +50 -0
- package/runtime/deps/assemblyscript/node_modules/long/LICENSE +202 -0
- package/runtime/deps/assemblyscript/node_modules/long/index.js +1581 -0
- package/runtime/deps/assemblyscript/node_modules/long/package.json +58 -0
- package/runtime/deps/assemblyscript/package.json +114 -0
- package/runtime/host/amxts.inc +85 -0
- package/runtime/licenses/AMXX-LICENSE.txt +34 -0
- package/runtime/licenses/AssemblyScript-LICENSE +201 -0
- package/runtime/licenses/AssemblyScript-NOTICE +93 -0
- package/runtime/licenses/Binaryen-LICENSE +201 -0
- package/runtime/licenses/Bun-LICENSE.md +84 -0
- package/runtime/licenses/GPL-3.0.txt +674 -0
- package/runtime/licenses/LLVM-LICENSE.TXT +279 -0
- package/runtime/licenses/README.md +25 -0
- package/runtime/licenses/WAMR-LICENSE +219 -0
- package/runtime/natives.txt +1095 -0
- package/runtime/patches/README.md +59 -0
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +14174 -0
- package/runtime/patches/wamr-2.4.5-amxts.patch +364 -0
- package/scripts/apply-docs.ts +508 -0
- package/scripts/asc.ts +27 -0
- package/scripts/auto-imports.ts +258 -0
- package/scripts/build-wasm.ts +595 -0
- package/scripts/check.ts +93 -0
- package/scripts/compile-cache.ts +179 -0
- package/scripts/compile.ts +401 -0
- package/scripts/docker-server.ts +73 -0
- package/scripts/docs/as/amxts.ts +210 -0
- package/scripts/docs/as/effects.ts +1684 -0
- package/scripts/docs/as/facade.ts +3302 -0
- package/scripts/docs/as/fs.ts +173 -0
- package/scripts/docs/as/lib/check.ts +68 -0
- package/scripts/docs/as/modules/http.ts +66 -0
- package/scripts/docs/as/os.ts +16 -0
- package/scripts/docs/as/promise.ts +132 -0
- package/scripts/docs/as/promise.types.ts +63 -0
- package/scripts/docs/as/vector.ts +48 -0
- package/scripts/docs/entities.ts +2338 -0
- package/scripts/docs/events.ts +245 -0
- package/scripts/docs/game.ts +497 -0
- package/scripts/docs/messages.ts +148 -0
- package/scripts/file-contents.ts +74 -0
- package/scripts/includes.ts +134 -0
- package/scripts/player-fields.ts +866 -0
- package/scripts/plugin-cache.ts +92 -0
- package/scripts/plugin-natives.ts +1157 -0
- package/scripts/prebuilt.ts +268 -0
- package/scripts/prepare.ts +134 -0
- package/scripts/project.ts +973 -0
- package/scripts/run.ts +28 -0
- package/scripts/shared-modules.ts +906 -0
- package/scripts/system.ts +33 -0
- package/scripts/tracked-fs.ts +115 -0
- package/scripts/typed-configs.ts +788 -0
- package/scripts/ui.ts +77 -0
- package/src/cli-api.mjs +175 -0
- package/src/parser/include-parser.ts +506 -0
- package/src/system.mjs +155 -0
- package/src/testing/compile-cache.ts +31 -0
- package/src/testing/compile.ts +222 -0
- package/src/testing/coroutines.ts +191 -0
- package/src/testing/index.ts +39 -0
- package/src/testing/kits.ts +55 -0
- package/src/testing/memory.ts +125 -0
- package/src/testing/natives.ts +925 -0
- package/src/testing/server.ts +1874 -0
- package/src/testing/setup.ts +63 -0
- package/src/testing/tables.ts +139 -0
- package/src/types.ts +51 -0
package/as/facade.ts
ADDED
|
@@ -0,0 +1,4639 @@
|
|
|
1
|
+
/// <reference path="./amxts.d.ts" />
|
|
2
|
+
// The plugin-facing API.
|
|
3
|
+
//
|
|
4
|
+
// Two layers sit under this one: as/natives.ts, which is every native in
|
|
5
|
+
// includes/*.inc as Pawn sees it, and the bridge natives that
|
|
6
|
+
// runtime/src/module.cpp registers by hand for the places where a string
|
|
7
|
+
// crossing as UTF-16 is worth a purpose-built thunk. Both are direct calls —
|
|
8
|
+
// an import whose signature wamrc knows costs 2 ns.
|
|
9
|
+
//
|
|
10
|
+
// The `// @ts-ignore: decorator` lines are AssemblyScript's own idiom, used
|
|
11
|
+
// throughout its standard library: an editor reading this as TypeScript would
|
|
12
|
+
// otherwise mark every @external as an error, because TypeScript allows
|
|
13
|
+
// decorators on classes only. asc is what actually reads them.
|
|
14
|
+
//
|
|
15
|
+
// What this file adds is shape: a player as an object, a target for a message,
|
|
16
|
+
// a handler registered rather than the name of a public passed around. Game
|
|
17
|
+
// state is not cached: every read asks the server, because another plugin may
|
|
18
|
+
// have changed it in between.
|
|
19
|
+
import {
|
|
20
|
+
get_user_armor, get_user_authid, get_user_deaths, get_user_frags, get_user_ip,
|
|
21
|
+
get_maxplayers, is_user_alive, is_user_bot, is_user_connected,
|
|
22
|
+
set_user_armor, set_user_frags,
|
|
23
|
+
get_member, set_member, get_user_msgid, message_begin, message_end, write_byte, write_short,
|
|
24
|
+
rg_give_item, rg_remove_all_items, rg_set_user_bpammo, rg_get_user_bpammo, cs_get_user_bpammo, rg_round_respawn, rg_switch_weapon,
|
|
25
|
+
rg_reset_maxspeed, rg_set_user_team, rg_join_team, rg_find_weapon_bpack_by_name, user_kill, get_user_flags, read_flags,
|
|
26
|
+
get_cvar_num, get_cvar_string, set_cvar_num, set_cvar_string, get_players, get_user_info,
|
|
27
|
+
LibraryExists, module_exists, give_item, strip_user_weapons, user_has_weapon, engclient_cmd,
|
|
28
|
+
cs_get_user_team, cs_set_user_team, cs_get_user_deaths, get_speak, set_speak, cs_set_user_bpammo, cs_set_user_deaths, ExecuteHamB,
|
|
29
|
+
read_argc, read_argv, set_hudmessage, show_hudmessage, create_cvar, get_cvar_pointer,
|
|
30
|
+
get_pcvar_float, get_pcvar_num, get_pcvar_string, set_pcvar_float, set_pcvar_num, set_pcvar_string,
|
|
31
|
+
hook_cvar_change, get_localinfo, register_srvcmd, register_dictionary,
|
|
32
|
+
set_dhudmessage, show_dhudmessage, CreateHudSyncObj, ShowSyncHudMsg, ClearSyncHud,
|
|
33
|
+
precache_model, precache_sound, precache_generic, query_client_cvar, register_touch,
|
|
34
|
+
register_message, get_msg_args, get_msg_argtype, get_msg_arg_int, get_msg_arg_float, get_msg_arg_string,
|
|
35
|
+
set_msg_arg_int, set_msg_arg_float, set_msg_arg_string
|
|
36
|
+
} from "./natives";
|
|
37
|
+
// Promise, async/await and AbortSignal, as the globals they are in JavaScript.
|
|
38
|
+
import "./promise";
|
|
39
|
+
import { Vector } from "./vector";
|
|
40
|
+
|
|
41
|
+
// @ts-ignore: decorator
|
|
42
|
+
@external("env", "print_client") declare function _printClient(id: i32, channel: i32, msg: string): void;
|
|
43
|
+
// @ts-ignore: decorator
|
|
44
|
+
@external("env", "say_text") declare function _sayText(id: i32, text: string, swapTo: string): void;
|
|
45
|
+
// @ts-ignore: decorator
|
|
46
|
+
@external("env", "get_name") declare function _getName(id: i32, out: i32, max: i32): i32;
|
|
47
|
+
// @ts-ignore: decorator
|
|
48
|
+
@external("env", "get_health") declare function _getHealth(id: i32): i32;
|
|
49
|
+
// @ts-ignore: decorator
|
|
50
|
+
@external("env", "set_health") declare function _setHealth(id: i32, hp: i32): void;
|
|
51
|
+
// @ts-ignore: decorator
|
|
52
|
+
@external("env", "clcmd") declare function _clcmd(pattern: string, fn: i32, flags: i32, info: string, shape: i32): i32;
|
|
53
|
+
// @ts-ignore: decorator
|
|
54
|
+
@external("env", "task") declare function _task(secondsBits: i32, fn: i32, id: i32, repeat: i32): i32;
|
|
55
|
+
// @ts-ignore: decorator
|
|
56
|
+
@external("env", "stop_task") declare function _stopTask(id: i32): i32;
|
|
57
|
+
// @ts-ignore: decorator
|
|
58
|
+
@external("env", "outcome") declare function _outcome(value: i32): void;
|
|
59
|
+
// @ts-ignore: decorator
|
|
60
|
+
@external("env", "export") declare function _export(name: string, fn: i32): i32;
|
|
61
|
+
// @ts-ignore: decorator
|
|
62
|
+
@external("env", "slot") declare function _slot(fn: i32, shape: i32, key: string, fallback: i32): i32;
|
|
63
|
+
// @ts-ignore: decorator
|
|
64
|
+
@external("env", "arg") declare function _arg(index: i32): i32;
|
|
65
|
+
// @ts-ignore: decorator
|
|
66
|
+
@external("env", "arg_text") declare function _argText(index: i32, out: i32, max: i32): i32;
|
|
67
|
+
// @ts-ignore: decorator
|
|
68
|
+
@external("env", "set_arg_text") declare function _setArgText(index: i32, text: string, max: i32): i32;
|
|
69
|
+
// @ts-ignore: decorator
|
|
70
|
+
@external("env", "set_arg") declare function _setArg(index: i32, value: i32): i32;
|
|
71
|
+
// @ts-ignore: decorator
|
|
72
|
+
@external("env", "caller") declare function _caller(): i32;
|
|
73
|
+
// @ts-ignore: decorator
|
|
74
|
+
@external("env", "argc") declare function _argc(): i32;
|
|
75
|
+
// @ts-ignore: decorator
|
|
76
|
+
@external("env", "plugin") declare function _plugin(name: string, version: string, author: string, description: string): void;
|
|
77
|
+
|
|
78
|
+
// call_indirect checks the type on the module's side, so a handler's shape is
|
|
79
|
+
// fixed at registration and there are exactly two: this one and WideHandler.
|
|
80
|
+
/** A handler that takes a player `id` and returns nothing — what player events and commands call. */
|
|
81
|
+
export type Handler = (id: number) => void;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A handler that takes up to four numbers and returns nothing: for a forward
|
|
85
|
+
* that passes more than a player `id`, and for a hookchain.
|
|
86
|
+
*
|
|
87
|
+
* A string argument arrives as a number: read it with `argString`. To stop
|
|
88
|
+
* the event, or to block what a hookchain hooks, call `handled()`.
|
|
89
|
+
*/
|
|
90
|
+
export type WideHandler = (a: number, b: number, c: number, d: number) => void;
|
|
91
|
+
|
|
92
|
+
const SHAPE_NARROW: i32 = 0;
|
|
93
|
+
const SHAPE_WIDE: i32 = 1;
|
|
94
|
+
|
|
95
|
+
// @ts-ignore: decorator
|
|
96
|
+
@external("env", "tag") declare function _tag(tag: i32): void;
|
|
97
|
+
// @ts-ignore: decorator
|
|
98
|
+
@external("env", "co_id") declare function _uniqueId(): i32;
|
|
99
|
+
|
|
100
|
+
// A function handed to the host is called by its table index. A closure is
|
|
101
|
+
// its table entry and its variables together, so for a closure the host gets
|
|
102
|
+
// the dispatcher of the shape instead, with a tag - the closure's place here
|
|
103
|
+
// - which the host passes first (Handler.tag in runtime/src/module.cpp). A
|
|
104
|
+
// function declared by name goes as itself, as it always has.
|
|
105
|
+
const hostClosures: Object[] = [];
|
|
106
|
+
|
|
107
|
+
function narrowFired(tag: i32, id: i32): void {
|
|
108
|
+
changetype<Handler>(hostClosures[tag - 1])(<f64>id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function wideFired(tag: i32, a: i32, b: i32, c: i32, d: i32): void {
|
|
112
|
+
changetype<WideHandler>(hostClosures[tag - 1])(<f64>a, <f64>b, <f64>c, <f64>d);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @hidden For the hood: what the server calls for `handler`. */
|
|
116
|
+
export function hostIndex<T>(handler: T, wide: bool): i32 {
|
|
117
|
+
const fn = changetype<usize>(handler);
|
|
118
|
+
if (load<usize>(fn, sizeof<u32>()) == 0) return load<u32>(fn);
|
|
119
|
+
hostClosures.push(changetype<Object>(fn));
|
|
120
|
+
_tag(hostClosures.length);
|
|
121
|
+
return wide ? wideFired.index : narrowFired.index;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Said rather than returned, so that the nine handlers in ten which have
|
|
125
|
+
// nothing to say about it end without a line about this bridge.
|
|
126
|
+
/**
|
|
127
|
+
* Stops the event: AMX Mod X passes it to no one else, and a hookchain does
|
|
128
|
+
* not call what it hooked. Applies only to the handler that is running.
|
|
129
|
+
*
|
|
130
|
+
* function onSay(id: number) {
|
|
131
|
+
* if (muted(id)) handled();
|
|
132
|
+
* }
|
|
133
|
+
*
|
|
134
|
+
* Pawn: `return PLUGIN_HANDLED`
|
|
135
|
+
*/
|
|
136
|
+
export function handled(): void {
|
|
137
|
+
_outcome(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @hidden Sets the handler's answer to a number other than `0` or `1`: a game event's status. */
|
|
141
|
+
export function __outcome(value: number): void {
|
|
142
|
+
_outcome(<i32>value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Every argument and return crosses as a 32-bit cell, and a float rides in one
|
|
146
|
+
// as its bit pattern. A number here is 64-bit, so the narrowing is done in one
|
|
147
|
+
// place rather than at every call.
|
|
148
|
+
/**
|
|
149
|
+
* Converts a number to a Pawn `Float:` cell, for a raw native or `ret()`:
|
|
150
|
+
*
|
|
151
|
+
* ret(floatCell(2.5));
|
|
152
|
+
* rg_round_end(floatCell(5.0), ...);
|
|
153
|
+
*/
|
|
154
|
+
export function floatCell(value: f64): number {
|
|
155
|
+
return reinterpret<i32>(<f32>value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// `<i32>x` truncates, which loses a second off every round time that is not exact.
|
|
159
|
+
/**
|
|
160
|
+
* Rounds a number to the nearest whole number: a round time of 59.6 seconds
|
|
161
|
+
* is `60`, not `59`.
|
|
162
|
+
*
|
|
163
|
+
* Pawn: `floatround`
|
|
164
|
+
*/
|
|
165
|
+
export function rounded(value: number): number {
|
|
166
|
+
return <i32>Math.round(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Converts a Pawn `Float:` cell back to a number. */
|
|
170
|
+
export function cellFloat(cell: number): f64 {
|
|
171
|
+
return <f64>reinterpret<f32>(cell);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Sets the value an exported native returns to the plugin that called it. */
|
|
175
|
+
export function ret(value: number): void {
|
|
176
|
+
_outcome(value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A public name that calls `handler`, for an AMX Mod X native that takes a
|
|
181
|
+
* callback by name: `register_think`, `set_native_filter`.
|
|
182
|
+
*
|
|
183
|
+
* ```ts
|
|
184
|
+
* const pub = publicFor(onThink, "think:myplugin_box");
|
|
185
|
+
* if (pub.length > 0) register_think("myplugin_box", pub);
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* An empty name means “already registered”: such a registration cannot be
|
|
189
|
+
* undone and survives a hot reload, so registering again would call the
|
|
190
|
+
* handler twice. `key` identifies the registration across reloads — any name
|
|
191
|
+
* unique within the plugin. `fallback` is the answer when the handler returns
|
|
192
|
+
* nothing: `0` for most natives, `1` where the native expects the event handled.
|
|
193
|
+
*
|
|
194
|
+
* Register from the `"cfg"` event, not at the top of the file: a console
|
|
195
|
+
* command registered that early (`register_concmd`, `register_srvcmd`) crashes
|
|
196
|
+
* the server when typed.
|
|
197
|
+
*/
|
|
198
|
+
export function publicFor(handler: WideHandler, key: string, fallback: number = 0): string {
|
|
199
|
+
// Must match SLOT_REUSED in runtime/src/module.cpp.
|
|
200
|
+
const REUSED: i32 = 0x10000;
|
|
201
|
+
|
|
202
|
+
const slot = _slot(hostIndex(handler, true), SHAPE_WIDE, key, fallback);
|
|
203
|
+
|
|
204
|
+
if (slot < 0) {
|
|
205
|
+
console.error(`no callback slot left for "${key}"`);
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return (slot & REUSED) ? "" : `__amxts_cb${slot}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// AMX Mod X implements a native as a public in some plugin, so this names the
|
|
213
|
+
// host plugin's one public for them all, and the module keeps the name:
|
|
214
|
+
// register_native cannot be undone, so a reload takes the same entry back
|
|
215
|
+
// rather than registering the name twice. Natives are asked for before any
|
|
216
|
+
// plugin starts, which is why plugins are loaded from plugin_natives at all.
|
|
217
|
+
/**
|
|
218
|
+
* Exports a native for other plugins - Pawn ones included - to call.
|
|
219
|
+
*
|
|
220
|
+
* nativeFn("myplugin_get_mode", getGameMode)
|
|
221
|
+
*
|
|
222
|
+
* function getGameMode(a: number, b: number, c: number, d: number) {
|
|
223
|
+
* ret(mode);
|
|
224
|
+
* }
|
|
225
|
+
*
|
|
226
|
+
* The handler gets the first four arguments as numbers; `arg(i)` and
|
|
227
|
+
* `argText(i)` read any of them, `setArg` and `setArgText` write back.
|
|
228
|
+
* Simpler: an `export function` of the entry file is a native with real
|
|
229
|
+
* types (see the Natives page).
|
|
230
|
+
*
|
|
231
|
+
* Call it at the top level of the file: AMX Mod X asks every plugin for its
|
|
232
|
+
* natives before it starts any of them.
|
|
233
|
+
*
|
|
234
|
+
* Pawn: `register_native`
|
|
235
|
+
*/
|
|
236
|
+
export function nativeFn(name: string, handler: WideHandler): void {
|
|
237
|
+
if (_export(name, hostIndex(handler, true)) < 0) {
|
|
238
|
+
console.error(`could not export the native "${name}"`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A number that a plugin's own native passes to Pawn as `Float:`.
|
|
244
|
+
*
|
|
245
|
+
* To TypeScript it is `number`. Write it only in the signature of an exported
|
|
246
|
+
* native, where Pawn has to know which numbers are fractional; everywhere
|
|
247
|
+
* else a number is `number`.
|
|
248
|
+
*
|
|
249
|
+
* ```ts
|
|
250
|
+
* export function cfg_get_float(file: string, key: string): Float { ... }
|
|
251
|
+
* // native Float:cfg_get_float(const file[], const key[]);
|
|
252
|
+
* export function set_speed(id: number, speed: Float) { ... }
|
|
253
|
+
* // native set_speed(id, Float:speed);
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
export type Float = number;
|
|
257
|
+
|
|
258
|
+
// The hood under `export function` in a plugin's entry file. scripts/plugin-natives.ts
|
|
259
|
+
// writes one wrapper per exported function: it reads the Pawn caller's
|
|
260
|
+
// arguments with these, calls the function, and hands its result back. A
|
|
261
|
+
// plugin never calls them itself.
|
|
262
|
+
|
|
263
|
+
// @ts-ignore: decorator
|
|
264
|
+
@external("env", "arg_string") declare function _argString(index: i32, out: usize, max: i32): i32;
|
|
265
|
+
// @ts-ignore: decorator
|
|
266
|
+
@external("env", "arg_array") declare function _argArray(index: i32, out: usize, count: i32): i32;
|
|
267
|
+
// @ts-ignore: decorator
|
|
268
|
+
@external("env", "set_arg_array") declare function _setArgArray(index: i32, cells: usize, count: i32): i32;
|
|
269
|
+
|
|
270
|
+
/** @hidden Registers a generated wrapper as the native `name`. */
|
|
271
|
+
export function __native(name: string, wrapper: () => void): void {
|
|
272
|
+
if (_export(name, wrapper.index) < 0) console.error(`could not export the native "${name}"`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @hidden A cell argument as a number. */
|
|
276
|
+
export function __nativeInt(index: i32): f64 {
|
|
277
|
+
return <f64>_arg(index);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A `Float:` a Pawn plugin passed, as the number it wrote: 0.1, not the
|
|
282
|
+
* 0.10000000149011612 its 32 bits hold. The shortest decimal that is the same
|
|
283
|
+
* float is what the Pawn source said - and what a value written back to a
|
|
284
|
+
* file, a chat line or a config should say.
|
|
285
|
+
*/
|
|
286
|
+
function pawnFloat(cell: i32): f64 {
|
|
287
|
+
return parseFloat(reinterpret<f32>(cell).toString());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** @hidden A `Float:` argument. */
|
|
291
|
+
export function __nativeFloat(index: i32): f64 {
|
|
292
|
+
return pawnFloat(_arg(index));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @hidden A `bool:` argument. */
|
|
296
|
+
export function __nativeBool(index: i32): bool {
|
|
297
|
+
return _arg(index) != 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @hidden A string argument, whole: the module says how long it is first, so
|
|
302
|
+
* there is no buffer to outgrow.
|
|
303
|
+
*/
|
|
304
|
+
export function __nativeString(index: i32): string {
|
|
305
|
+
const length = _argString(index, 0, 0);
|
|
306
|
+
if (length <= 0) return "";
|
|
307
|
+
const bytes = new ArrayBuffer(length);
|
|
308
|
+
_argString(index, changetype<usize>(bytes), length);
|
|
309
|
+
return String.UTF8.decode(bytes);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function nativeCells(index: i32, count: i32): StaticArray<i32> {
|
|
313
|
+
const cells = new StaticArray<i32>(count > 0 ? count : 0);
|
|
314
|
+
if (count > 0) _argArray(index, changetype<usize>(cells), count);
|
|
315
|
+
return cells;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Cells as numbers: whole ones, or `Float:` ones read as the number they are. */
|
|
319
|
+
function numbersOf(cells: StaticArray<i32>, floats: bool): f64[] {
|
|
320
|
+
const values = new Array<f64>(cells.length);
|
|
321
|
+
for (let i = 0; i < cells.length; i++) unchecked(values[i] = floats ? pawnFloat(unchecked(cells[i])) : <f64>unchecked(cells[i]));
|
|
322
|
+
return values;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Numbers into the caller's `x[], size` at `index`, as many as it has room
|
|
327
|
+
* for: whole ones, or `Float:` ones. Returns how many.
|
|
328
|
+
*/
|
|
329
|
+
function writeNumbers(values: f64[], index: i32, floats: bool): i32 {
|
|
330
|
+
const count = min(values.length, _arg(index + 1));
|
|
331
|
+
const cells = new StaticArray<i32>(count > 0 ? count : 0);
|
|
332
|
+
for (let i = 0; i < count; i++) unchecked(cells[i] = floats ? reinterpret<i32>(<f32>unchecked(values[i])) : <i32>unchecked(values[i]));
|
|
333
|
+
_setArgArray(index, changetype<usize>(cells), count);
|
|
334
|
+
return count;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** @hidden An array argument whose size is the argument after it. */
|
|
338
|
+
export function __nativeInts(index: i32): f64[] {
|
|
339
|
+
return numbersOf(nativeCells(index, _arg(index + 1)), false);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** @hidden A `Float:` array argument whose size is the argument after it. */
|
|
343
|
+
export function __nativeFloats(index: i32): f64[] {
|
|
344
|
+
return numbersOf(nativeCells(index, _arg(index + 1)), true);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** @hidden A `Float:v[3]` argument. */
|
|
348
|
+
export function __nativeVector(index: i32): Vector {
|
|
349
|
+
const cells = nativeCells(index, 3);
|
|
350
|
+
return new Vector(pawnFloat(cells[0]), pawnFloat(cells[1]), pawnFloat(cells[2]));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// @ts-ignore: decorator
|
|
354
|
+
@external("env", "arg_length") declare function _argLength(index: i32): i32;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @hidden A forward's array argument, as many numbers as it has: what the
|
|
358
|
+
* emitting plugin sent, or the size the include declares; none when neither
|
|
359
|
+
* says. `floats` reads them as `Float:`.
|
|
360
|
+
*/
|
|
361
|
+
export function __forwardNumbers(index: i32, floats: bool): f64[] {
|
|
362
|
+
return numbersOf(nativeCells(index, _argLength(index)), floats);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @hidden What the function returned, handed back the way its type says:
|
|
367
|
+
* a string into the caller's `out[], len`, a `string | null` the same and
|
|
368
|
+
* `true` or `false` as the result, an array into `out[], max` with the count
|
|
369
|
+
* as the result, a number or a boolean as the result itself.
|
|
370
|
+
*/
|
|
371
|
+
export function __nativeReturn<T>(value: T, out: i32): void {
|
|
372
|
+
if (isString<T>()) {
|
|
373
|
+
const max = _arg(out + 1);
|
|
374
|
+
if (isNullable<T>()) {
|
|
375
|
+
if (changetype<usize>(value) == 0) {
|
|
376
|
+
_setArgText(out, "", max);
|
|
377
|
+
_outcome(0);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
_setArgText(out, changetype<string>(value), max);
|
|
381
|
+
_outcome(1);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
_outcome(_setArgText(out, changetype<string>(value), max));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (isArray<T>()) {
|
|
388
|
+
// @ts-ignore: valueof is AssemblyScript's
|
|
389
|
+
if (isBoolean<valueof<T>>() || !isFloat<valueof<T>>()) ERROR("an exported native returns number[] or Float[]");
|
|
390
|
+
_outcome(writeNumbers(changetype<f64[]>(value), out, false));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (isBoolean<T>()) {
|
|
394
|
+
_outcome(value ? 1 : 0);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (isFloat<T>() || isInteger<T>()) {
|
|
398
|
+
_outcome(<i32>value);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// @ts-ignore: nonnull is AssemblyScript's
|
|
402
|
+
if (isReference<T>() && idof<nonnull<T>>() == idof<CellArray>()) {
|
|
403
|
+
__nativeReturnArray(changetype<CellArray | null>(value));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
ERROR("an exported native returns string, string | null, number, Float, boolean, number[], Float[], CellArray or an exported enum");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** @hidden A cell argument under one of the plugin's exported enums. */
|
|
410
|
+
export function __nativeCell(index: i32): i32 {
|
|
411
|
+
return _arg(index);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* @hidden Whether a `Player` argument is a player slot, 1 to maxPlayers -
|
|
416
|
+
* or 0, for a `Player | null` one. The wrapper calls the function only then.
|
|
417
|
+
*/
|
|
418
|
+
export function __nativeTarget(index: i32, orNone: bool): bool {
|
|
419
|
+
const id = _arg(index);
|
|
420
|
+
return (orNone && id == 0) || (id >= 1 && id <= get_maxplayers());
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** @hidden A `Player` argument: the player in that slot, null for 0. */
|
|
424
|
+
export function __nativePlayer(index: i32): Player | null {
|
|
425
|
+
const id = _arg(index);
|
|
426
|
+
return id >= 1 ? new Player(id) : null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @hidden Whether a `Team` argument - `TeamName:` in the include - is a
|
|
431
|
+
* TeamName number, 0 to 3. The wrapper calls the function only then.
|
|
432
|
+
*/
|
|
433
|
+
export function __nativeIsTeam(index: i32): bool {
|
|
434
|
+
const team = _arg(index);
|
|
435
|
+
return team >= 0 && team < TEAM_NAMES.length;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** @hidden A `Team` argument: its TeamName number as the name. */
|
|
439
|
+
export function __nativeTeam(index: i32): Team {
|
|
440
|
+
return TEAM_NAMES[_arg(index)];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @hidden A native not called because a `Player` argument was no player:
|
|
445
|
+
* its result's default - 0, which Pawn reads as false, 0.0 or no array, and
|
|
446
|
+
* "" in the caller's out[] when it passed one (`cells` is how many it passes
|
|
447
|
+
* without).
|
|
448
|
+
*/
|
|
449
|
+
export function __nativeSkip(out: i32, cells: i32): void {
|
|
450
|
+
if (_argc() > cells) _setArgText(out, "", _arg(out + 1));
|
|
451
|
+
_outcome(0);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* @hidden How far past its own place a parameter after the result buffer
|
|
456
|
+
* is: 2 when the caller passed `out[], len`, 0 when it passed only the
|
|
457
|
+
* parameters' `cells`. See indexes() in scripts/plugin-natives.ts.
|
|
458
|
+
*/
|
|
459
|
+
export function __nativeShift(cells: i32): i32 {
|
|
460
|
+
return _argc() > cells ? 2 : 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** @hidden A CellArray result: its handle, Invalid_Array (0) for null. */
|
|
464
|
+
export function __nativeReturnArray(value: CellArray | null): void {
|
|
465
|
+
_outcome(value != null ? value.handle : 0);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** @hidden A `Float:` result. */
|
|
469
|
+
export function __nativeReturnFloat(value: f64): void {
|
|
470
|
+
_outcome(reinterpret<i32>(<f32>value));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** @hidden How many cells the Pawn caller passed. */
|
|
474
|
+
export function __nativeCount(): i32 {
|
|
475
|
+
return _argc();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** @hidden A number an `any:...` tail passes, which Pawn does by address. */
|
|
479
|
+
export function __nativeRef(index: i32): f64 {
|
|
480
|
+
return <f64>nativeCells(index, 1)[0];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* @hidden The result of a native whose include declares no out-argument: a
|
|
485
|
+
* number or a boolean as the cell the native returns.
|
|
486
|
+
*/
|
|
487
|
+
export function __nativeResult<T>(value: T): void {
|
|
488
|
+
if (isBoolean<T>()) {
|
|
489
|
+
_outcome(value ? 1 : 0);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (isFloat<T>() || isInteger<T>()) {
|
|
493
|
+
_outcome(<i32>value);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
ERROR("a native whose include declares no out-argument returns a number or a boolean");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* @hidden One field of a native's result, written through the argument the
|
|
501
|
+
* include declares for it: text into `x[], len`, a number through `&x`, an
|
|
502
|
+
* array into `x[], size`.
|
|
503
|
+
*/
|
|
504
|
+
export function __nativeOut<T>(value: T, index: i32): void {
|
|
505
|
+
if (isString<T>()) {
|
|
506
|
+
_setArgText(index, changetype<string>(value), _arg(index + 1));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (isArray<T>()) {
|
|
510
|
+
writeNumbers(changetype<f64[]>(value), index, false);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (isBoolean<T>()) {
|
|
514
|
+
_setArg(index, value ? 1 : 0);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (isFloat<T>() || isInteger<T>()) {
|
|
518
|
+
_setArg(index, <i32>value);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
ERROR("a native's out-argument takes a string, a number, a boolean or number[]");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** @hidden The same for a `Float:&x` or `Float:x[], size` out-argument. */
|
|
525
|
+
export function __nativeOutFloat<T>(value: T, index: i32): void {
|
|
526
|
+
if (isArray<T>()) {
|
|
527
|
+
writeNumbers(changetype<f64[]>(value), index, true);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
_setArg(index, reinterpret<i32>(<f32><f64>value));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* @hidden A `const fmt[], any:...` pair, formatted as AMX Mod X's format()
|
|
535
|
+
* would: %s %d %i %u %c %x %f (with width, `-`, `0` and a precision) and %L,
|
|
536
|
+
* which takes a player and a key and formats that player's translation with
|
|
537
|
+
* the arguments after them. The tail is passed by address, as Pawn passes
|
|
538
|
+
* `any:...`: a string is read there, a number through it.
|
|
539
|
+
*/
|
|
540
|
+
export function __nativeFormat(index: i32): string {
|
|
541
|
+
return formatPawn(__nativeString(index), new FormatCursor(index + 1));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Where format()'s arguments come from: the `any:...` tail of a native's
|
|
545
|
+
// call, read by address as Pawn passes it...
|
|
546
|
+
class FormatCursor {
|
|
547
|
+
constructor(public next: i32) {}
|
|
548
|
+
|
|
549
|
+
has(): bool {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
text(): string {
|
|
554
|
+
return __nativeString(this.next++);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
cell(): i32 {
|
|
558
|
+
return <i32>__nativeRef(this.next++);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
float(): f64 {
|
|
562
|
+
return <f64>reinterpret<f32>(this.cell());
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ...or a list of texts, as lang.translate takes them: a number is read out
|
|
567
|
+
// of its text.
|
|
568
|
+
class TextCursor extends FormatCursor {
|
|
569
|
+
constructor(public args: string[]) {
|
|
570
|
+
super(0);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
has(): bool {
|
|
574
|
+
return this.next < this.args.length;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
text(): string {
|
|
578
|
+
const at = this.next++;
|
|
579
|
+
return at < this.args.length ? unchecked(this.args[at]) : "";
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
cell(): i32 {
|
|
583
|
+
const value = this.float();
|
|
584
|
+
return isFinite(value) ? <i32>value : 0;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
float(): f64 {
|
|
588
|
+
return parseFloat(this.text());
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function formatPawn(template: string, cursor: FormatCursor): string {
|
|
593
|
+
let out = "";
|
|
594
|
+
let i = 0;
|
|
595
|
+
const length = template.length;
|
|
596
|
+
while (i < length) {
|
|
597
|
+
const c = template.charCodeAt(i);
|
|
598
|
+
if (c != 0x25 || i + 1 >= length) {
|
|
599
|
+
out += template.charAt(i);
|
|
600
|
+
i++;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
let j = i + 1;
|
|
605
|
+
let left = false;
|
|
606
|
+
let zero = false;
|
|
607
|
+
for (; j < length; j++) {
|
|
608
|
+
const flag = template.charCodeAt(j);
|
|
609
|
+
if (flag == 0x2D) left = true;
|
|
610
|
+
else if (flag == 0x30) zero = true;
|
|
611
|
+
else break;
|
|
612
|
+
}
|
|
613
|
+
let width = 0;
|
|
614
|
+
while (j < length && isDigitCode(template.charCodeAt(j))) width = width * 10 + template.charCodeAt(j++) - 0x30;
|
|
615
|
+
let precision = -1;
|
|
616
|
+
if (j < length && template.charCodeAt(j) == 0x2E) {
|
|
617
|
+
precision = 0;
|
|
618
|
+
j++;
|
|
619
|
+
while (j < length && isDigitCode(template.charCodeAt(j))) precision = precision * 10 + template.charCodeAt(j++) - 0x30;
|
|
620
|
+
}
|
|
621
|
+
if (j >= length) {
|
|
622
|
+
out += template.substring(i);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const type = template.charAt(j);
|
|
627
|
+
let piece = "";
|
|
628
|
+
let numeric = false;
|
|
629
|
+
// Not a placeholder, or one whose argument the call did not pass: it stays as written.
|
|
630
|
+
const written = !"%sdiucxXfL".includes(type) || (type != "%" && !cursor.has());
|
|
631
|
+
if (written) piece = template.substring(i, j + 1);
|
|
632
|
+
else if (type == "%") piece = "%";
|
|
633
|
+
else if (type == "s") {
|
|
634
|
+
piece = cursor.text();
|
|
635
|
+
if (precision >= 0 && piece.length > precision) piece = piece.substring(0, precision);
|
|
636
|
+
} else if (type == "d" || type == "i" || type == "u") {
|
|
637
|
+
piece = cursor.cell().toString();
|
|
638
|
+
numeric = true;
|
|
639
|
+
} else if (type == "c") piece = String.fromCharCode(cursor.cell());
|
|
640
|
+
else if (type == "x" || type == "X") {
|
|
641
|
+
piece = (<u32>cursor.cell()).toString(16);
|
|
642
|
+
if (type == "X") piece = piece.toUpperCase();
|
|
643
|
+
numeric = true;
|
|
644
|
+
} else if (type == "f") {
|
|
645
|
+
piece = fixedFloat(cursor.float(), precision < 0 ? 6 : precision);
|
|
646
|
+
numeric = true;
|
|
647
|
+
} else if (type == "L") {
|
|
648
|
+
const player = cursor.cell();
|
|
649
|
+
const key = cursor.text();
|
|
650
|
+
const found = lookupLang(key, player > 0 ? player : 0);
|
|
651
|
+
piece = formatPawn(found.length > 0 ? found : `ML_NOTFOUND: ${key}`, cursor);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
while (!written && piece.length < width) piece = left ? piece + " " : zero && numeric ? "0" + piece : " " + piece;
|
|
655
|
+
out += piece;
|
|
656
|
+
i = j + 1;
|
|
657
|
+
}
|
|
658
|
+
return out;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// @ts-ignore: decorator
|
|
662
|
+
@external("env", "LookupLangKey") declare function _lookupLangKey(out: i32, size: i32, key: i32, id: i32): i32;
|
|
663
|
+
|
|
664
|
+
/** The longest line a dictionary holds: AMX Mod X reads a value into 512 bytes. */
|
|
665
|
+
const LANG_TEXT: i32 = 512;
|
|
666
|
+
|
|
667
|
+
// One buffer for every lookup: a menu translates each of its lines on every draw.
|
|
668
|
+
const langOut = new StaticArray<i32>(LANG_TEXT);
|
|
669
|
+
const langId = new StaticArray<i32>(1);
|
|
670
|
+
|
|
671
|
+
/** The key's line in the player's language (`0`: the server's), as the dictionary loaded it; empty when no dictionary has it. */
|
|
672
|
+
function lookupLang(key: string, player: i32): string {
|
|
673
|
+
unchecked(langId[0] = player);
|
|
674
|
+
unchecked(langOut[0] = 0);
|
|
675
|
+
_lookupLangKey(changetype<i32>(langOut), LANG_TEXT - 1, changetype<i32>(__cellsOf(key)), changetype<i32>(langId));
|
|
676
|
+
return __cellText(changetype<usize>(langOut), LANG_TEXT);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function isDigitCode(code: i32): bool {
|
|
680
|
+
return code >= 0x30 && code <= 0x39;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** `%.Nf`: the number with exactly `digits` digits after the point, rounded. */
|
|
684
|
+
function fixedFloat(value: f64, digits: i32): string {
|
|
685
|
+
const text = value.toFixed(Math.min(digits, 100));
|
|
686
|
+
// A minus the rounding left nothing of is not written: -0.001 is "0.00".
|
|
687
|
+
return text.startsWith("-") && parseFloat(text) == 0 ? text.substring(1) : text;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* @hidden A value as a new AMX Mod X `Array:` - the handle, or
|
|
692
|
+
* Invalid_Array (0) for null. Text is an item a string; a number a Float
|
|
693
|
+
* cell (a TypeScript number is a float); a list of lists an Array of their
|
|
694
|
+
* handles, except rows of numbers, which are items of whole-number cells, as
|
|
695
|
+
* a list-menu row is. The Pawn plugin that gets it destroys it.
|
|
696
|
+
*/
|
|
697
|
+
export function __nativePawnArray<T>(value: T): i32 {
|
|
698
|
+
if (isNullable<T>() && changetype<usize>(value) == 0) return 0;
|
|
699
|
+
if (!isArray<T>()) ERROR("an Array: result is a list");
|
|
700
|
+
// @ts-ignore: valueof is AssemblyScript's
|
|
701
|
+
if (isString<valueof<T>>()) return CellArray.fromStrings(changetype<string[]>(value)).handle;
|
|
702
|
+
// @ts-ignore: valueof is AssemblyScript's
|
|
703
|
+
if (isFloat<valueof<T>>()) return CellArray.fromFloats(changetype<f64[]>(value)).handle;
|
|
704
|
+
// @ts-ignore: valueof is AssemblyScript's
|
|
705
|
+
if (isString<valueof<valueof<T>>>()) {
|
|
706
|
+
const lists = changetype<string[][]>(value);
|
|
707
|
+
const handles = new Array<i32>(lists.length);
|
|
708
|
+
for (let i = 0; i < lists.length; i++) handles[i] = CellArray.fromStrings(lists[i]).handle;
|
|
709
|
+
return __nativePawnHandles(handles);
|
|
710
|
+
}
|
|
711
|
+
const rows = changetype<f64[][]>(value);
|
|
712
|
+
const array = new CellArray(rows.length > 0 ? rows[0].length : 1);
|
|
713
|
+
for (let i = 0; i < rows.length; i++) array.pushCells(rows[i]);
|
|
714
|
+
return array.handle;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** @hidden An `Array:` of handles, a cell each. */
|
|
718
|
+
export function __nativePawnHandles(handles: i32[]): i32 {
|
|
719
|
+
const array = new CellArray(1);
|
|
720
|
+
for (let i = 0; i < handles.length; i++) array.pushCell(handles[i]);
|
|
721
|
+
return array.handle;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** @hidden A `Float:out[], max` result; the count is what the native returns. */
|
|
725
|
+
export function __nativeReturnFloats(values: f64[], out: i32): void {
|
|
726
|
+
_outcome(writeNumbers(values, out, true));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// @ts-ignore: decorator
|
|
730
|
+
@external("env", "ArrayCreate") declare function _arrayCreate(cellSize: i32, reserved: i32): i32;
|
|
731
|
+
// @ts-ignore: decorator
|
|
732
|
+
@external("env", "ArrayPushString") declare function _arrayPushString(handle: i32, text: usize): i32;
|
|
733
|
+
// @ts-ignore: decorator
|
|
734
|
+
@external("env", "ArrayPushCell") declare function _arrayPushCell(handle: i32, value: i32): i32;
|
|
735
|
+
// @ts-ignore: decorator
|
|
736
|
+
@external("env", "ArrayPushArray") declare function _arrayPushArray(handle: i32, cells: usize, size: i32): i32;
|
|
737
|
+
// @ts-ignore: decorator
|
|
738
|
+
@external("env", "ArraySize") declare function _arraySize(handle: i32): i32;
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* A dynamic array of AMX Mod X, for returning a list to a Pawn plugin that
|
|
742
|
+
* expects an `Array:` handle.
|
|
743
|
+
*
|
|
744
|
+
* ```ts
|
|
745
|
+
* export function cfg_get_value_array(section: ConfigSection, key: string) {
|
|
746
|
+
* const words = lookup(section, key); // string[] | null
|
|
747
|
+
* return words != null ? CellArray.fromStrings(words) : null;
|
|
748
|
+
* }
|
|
749
|
+
* // native Array:cfg_get_value_array(ConfigSection:section, const key[]);
|
|
750
|
+
* ```
|
|
751
|
+
*
|
|
752
|
+
* The Pawn plugin that gets the array owns it and destroys it; `null` reaches
|
|
753
|
+
* it as `Invalid_Array`. Inside TypeScript a list is a `string[]` or a
|
|
754
|
+
* `number[]`: this class is only for handing one to Pawn.
|
|
755
|
+
*
|
|
756
|
+
* Pawn: `ArrayCreate`, `Array:`
|
|
757
|
+
*/
|
|
758
|
+
export class CellArray {
|
|
759
|
+
/** The array's handle, the `Array:` Pawn gets. */
|
|
760
|
+
readonly handle: i32;
|
|
761
|
+
|
|
762
|
+
/** An empty array whose items are `cellSize` cells each: 1 for a number, the buffer size for text. */
|
|
763
|
+
constructor(cellSize: number = 1) {
|
|
764
|
+
this.handle = _arrayCreate(<i32>cellSize, 32);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** A new array of strings, each up to `cellSize - 1` bytes. */
|
|
768
|
+
static fromStrings(values: string[], cellSize: number = 512): CellArray {
|
|
769
|
+
const array = new CellArray(cellSize);
|
|
770
|
+
for (let i = 0; i < values.length; i++) array.pushString(values[i]);
|
|
771
|
+
return array;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** A new array of fractional numbers, which Pawn reads as `Float:`. */
|
|
775
|
+
static fromFloats(values: number[]): CellArray {
|
|
776
|
+
const array = new CellArray(1);
|
|
777
|
+
for (let i = 0; i < values.length; i++) array.pushFloat(values[i]);
|
|
778
|
+
return array;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** The number of items in the array. */
|
|
782
|
+
get length(): number {
|
|
783
|
+
return _arraySize(this.handle);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Adds a string; it must fit in `cellSize - 1` bytes of UTF-8. */
|
|
787
|
+
pushString(value: string): void {
|
|
788
|
+
_arrayPushString(this.handle, changetype<usize>(__cellsOf(value)));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Adds a whole number, or another array's handle. */
|
|
792
|
+
pushCell(value: number): void {
|
|
793
|
+
_arrayPushCell(this.handle, <i32>value);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** Adds a fractional number, which Pawn reads as `Float:`. */
|
|
797
|
+
pushFloat(value: number): void {
|
|
798
|
+
_arrayPushCell(this.handle, reinterpret<i32>(<f32>value));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** Adds one item of several numbers: `[a, b]` into an array made with `new CellArray(2)`. */
|
|
802
|
+
pushCells(values: number[]): void {
|
|
803
|
+
pushCellArrayRow(this.handle, values);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// A handler takes four cells because the module calls it through one fixed type.
|
|
808
|
+
/**
|
|
809
|
+
* Reads an argument of the running handler by position; `0` is its first
|
|
810
|
+
* parameter. Useful past the fourth, which a handler does not get as a
|
|
811
|
+
* parameter.
|
|
812
|
+
*
|
|
813
|
+
* function onSomething(a: number, b: number, c: number, d: number) {
|
|
814
|
+
* const fifth = arg(4);
|
|
815
|
+
* }
|
|
816
|
+
*/
|
|
817
|
+
export function arg(index: number): number {
|
|
818
|
+
return _arg(index);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// A string does not cross as text: what arrives is an address in the memory of
|
|
822
|
+
// whoever made the call - the host plugin for a command or a hook, the calling
|
|
823
|
+
// plugin for a native this one exported. The module reads it there.
|
|
824
|
+
/**
|
|
825
|
+
* Reads an argument of the running callback as a string.
|
|
826
|
+
*
|
|
827
|
+
* A forward's strings need no call: its handler gets them as text already.
|
|
828
|
+
*/
|
|
829
|
+
export function argText(index: number): string {
|
|
830
|
+
const len = _argText(index, changetype<i32>(textBuf), 512);
|
|
831
|
+
return String.UTF8.decodeUnsafe(changetype<usize>(textBuf), len);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* The number of arguments the running call actually carried.
|
|
836
|
+
*
|
|
837
|
+
* A handler always gets four, padded with zeros, so a native with optional
|
|
838
|
+
* arguments tells a passed zero from a missing one by this:
|
|
839
|
+
*
|
|
840
|
+
* const flashes = argc() >= 2 ? arg(1) : -1;
|
|
841
|
+
*/
|
|
842
|
+
export function argc(): number {
|
|
843
|
+
return _argc();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* The `id` of the plugin that called the running exported native, or `-1`.
|
|
848
|
+
*
|
|
849
|
+
* For a native that is about its caller: a cvar registered by a plugin, a
|
|
850
|
+
* chat prefix set for one.
|
|
851
|
+
*/
|
|
852
|
+
export function caller(): number {
|
|
853
|
+
return _caller();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Writes a number back through an argument passed by reference (`&value`
|
|
858
|
+
* in Pawn).
|
|
859
|
+
*
|
|
860
|
+
* Pawn: `set_param_byref`
|
|
861
|
+
*/
|
|
862
|
+
export function setArg(index: number, value: number): void {
|
|
863
|
+
_setArg(index, value);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Writes text back through a string argument.
|
|
868
|
+
*
|
|
869
|
+
* For a callback that hands over a buffer to fill rather than text to read.
|
|
870
|
+
* `max` is the room the caller gave, usually the argument right after the
|
|
871
|
+
* buffer.
|
|
872
|
+
*
|
|
873
|
+
* function placeholder(id: number, target: number, out: number, max: number) {
|
|
874
|
+
* setArgText(2, "ready", max);
|
|
875
|
+
* }
|
|
876
|
+
*/
|
|
877
|
+
export function setArgText(index: number, text: string, max: number): void {
|
|
878
|
+
_setArgText(index, text, max);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const textBuf = new StaticArray<u8>(512);
|
|
882
|
+
|
|
883
|
+
/** Reads a string argument that a wide handler got as a number. */
|
|
884
|
+
export function argString(pointer: number): string {
|
|
885
|
+
return pointer == 0 ? "" : __cellText(<usize>pointer, 512);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* @hidden A Pawn string at `pointer`: a byte of UTF-8 in each cell, up to the
|
|
890
|
+
* zero cell or `max` cells. AMX Mod X, the engine and the game keep text as
|
|
891
|
+
* bytes - get_amxstring takes the low byte of each cell - so a letter outside
|
|
892
|
+
* ASCII is two or three cells, and a cell per UTF-16 unit came out as
|
|
893
|
+
* mojibake one way and a truncated byte the other ("раз" arrived as "@0").
|
|
894
|
+
*/
|
|
895
|
+
export function __cellText(pointer: usize, max: i32): string {
|
|
896
|
+
let length = 0;
|
|
897
|
+
while (length < max && load<i32>(pointer + (<usize>length << 2)) != 0) length++;
|
|
898
|
+
|
|
899
|
+
const bytes = new Uint8Array(length);
|
|
900
|
+
for (let i = 0; i < length; i++) unchecked(bytes[i] = <u8>load<i32>(pointer + (<usize>i << 2)));
|
|
901
|
+
return String.UTF8.decode(bytes.buffer);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* @hidden Text as a Pawn string, a byte of UTF-8 a cell, into `cells` - at
|
|
906
|
+
* most `cells.length - 1` bytes and never half a letter - and the zero cell.
|
|
907
|
+
* Returns how many bytes went in.
|
|
908
|
+
*/
|
|
909
|
+
export function __writeCellText(text: string, cells: StaticArray<i32>, at: i32 = 0): i32 {
|
|
910
|
+
const bytes = Uint8Array.wrap(String.UTF8.encode(text));
|
|
911
|
+
let n = min(bytes.length, cells.length - 1 - at);
|
|
912
|
+
if (n < 0) return 0;
|
|
913
|
+
// A continuation byte (10xxxxxx) where the text is cut belongs to a letter
|
|
914
|
+
// that does not fit: leave all of it out.
|
|
915
|
+
if (n < bytes.length) while (n > 0 && (unchecked(bytes[n]) & 0xC0) == 0x80) n--;
|
|
916
|
+
for (let i = 0; i < n; i++) unchecked(cells[at + i] = <i32>unchecked(bytes[i]));
|
|
917
|
+
unchecked(cells[at + n] = 0);
|
|
918
|
+
return n;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** @hidden Text as a Pawn string in a buffer of its own size. */
|
|
922
|
+
export function __cellsOf(text: string): StaticArray<i32> {
|
|
923
|
+
const cells = new StaticArray<i32>(String.UTF8.byteLength(text) + 1);
|
|
924
|
+
__writeCellText(text, cells);
|
|
925
|
+
return cells;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** Reads the text a raw native from `~/natives` wrote into a cell array. `stringToCells` is the other way. */
|
|
929
|
+
export function cellsToString(cells: StaticArray<i32>): string {
|
|
930
|
+
return __cellText(changetype<usize>(cells), cells.length);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** Writes text into a cell array as a Pawn string, for a raw native. */
|
|
934
|
+
export function stringToCells(text: string, cells: StaticArray<i32>): void {
|
|
935
|
+
__writeCellText(text, cells);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// The natives in ~/natives take addresses, because that is what Pawn pushes: a
|
|
939
|
+
// string has to be in memory this plugin owns before its address means anything.
|
|
940
|
+
/**
|
|
941
|
+
* Passes a string to a raw native from `~/natives` without declaring a
|
|
942
|
+
* buffer for it:
|
|
943
|
+
*
|
|
944
|
+
* cfg_set_base_dir(cells("myplugin"));
|
|
945
|
+
*
|
|
946
|
+
* Up to eight strings in one call; a ninth overwrites the first. Only for
|
|
947
|
+
* arguments going in: a native that writes text back needs `out()`.
|
|
948
|
+
*/
|
|
949
|
+
export function cells(text: string): number {
|
|
950
|
+
ring = (ring + 1) % 8;
|
|
951
|
+
const buffer = unchecked(rings[ring]);
|
|
952
|
+
stringToCells(text, buffer);
|
|
953
|
+
return changetype<i32>(buffer);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const rings: StaticArray<i32>[] = [
|
|
957
|
+
new StaticArray<i32>(256), new StaticArray<i32>(256),
|
|
958
|
+
new StaticArray<i32>(256), new StaticArray<i32>(256),
|
|
959
|
+
new StaticArray<i32>(256), new StaticArray<i32>(256),
|
|
960
|
+
new StaticArray<i32>(256), new StaticArray<i32>(256)
|
|
961
|
+
];
|
|
962
|
+
|
|
963
|
+
let ring: i32 = 0;
|
|
964
|
+
|
|
965
|
+
// Without it a plugin would declare a StaticArray, changetype it and decode it
|
|
966
|
+
// by hand at every call.
|
|
967
|
+
/**
|
|
968
|
+
* A buffer for a raw native to write text into; `text()` reads it back.
|
|
969
|
+
*
|
|
970
|
+
* const name = out();
|
|
971
|
+
* get_user_name(id, name, TEXT_MAX);
|
|
972
|
+
* console.log(text(name));
|
|
973
|
+
*
|
|
974
|
+
* Up to four at once. A number a native writes through a `&reference` needs
|
|
975
|
+
* one too; `cell()` reads it back.
|
|
976
|
+
*/
|
|
977
|
+
export function out(): number {
|
|
978
|
+
slot = (slot + 1) % 4;
|
|
979
|
+
const buffer = unchecked(outs[slot]);
|
|
980
|
+
unchecked(buffer[0] = 0);
|
|
981
|
+
return changetype<i32>(buffer);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** Reads the text a native wrote into an `out()` buffer. */
|
|
985
|
+
export function text(buffer: number): string {
|
|
986
|
+
return __cellText(<usize>buffer, TEXT_MAX);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// So that a plugin never writes changetype or StaticArray: those are how
|
|
990
|
+
// AssemblyScript reaches raw memory, and they belong here.
|
|
991
|
+
/**
|
|
992
|
+
* A row of numbers for a native to read from or write into: a vector, a list
|
|
993
|
+
* of players, a line of text. Used like an ordinary array - `buffer[0]`,
|
|
994
|
+
* `buffer.float(2)`, `buffer.text()` - and its `address` is what the native
|
|
995
|
+
* takes.
|
|
996
|
+
*
|
|
997
|
+
* const origin = new CellBuffer(3);
|
|
998
|
+
* entity_set_origin(cube, origin.address);
|
|
999
|
+
*/
|
|
1000
|
+
export class CellBuffer {
|
|
1001
|
+
private data: StaticArray<i32>;
|
|
1002
|
+
|
|
1003
|
+
constructor(length: number) {
|
|
1004
|
+
this.data = new StaticArray<i32>(length);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** A buffer of three fractional numbers, for a native that takes a vector: an origin, a size, a colour. */
|
|
1008
|
+
static vector(x: f64, y: f64, z: f64): CellBuffer {
|
|
1009
|
+
const buffer = new CellBuffer(3);
|
|
1010
|
+
buffer.setFloat(0, x);
|
|
1011
|
+
buffer.setFloat(1, y);
|
|
1012
|
+
buffer.setFloat(2, z);
|
|
1013
|
+
return buffer;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** The number of cells in the buffer. */
|
|
1017
|
+
get length(): number {
|
|
1018
|
+
return this.data.length;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/** The buffer's address, the argument a native that takes an array needs. */
|
|
1022
|
+
get address(): number {
|
|
1023
|
+
return changetype<i32>(this.data);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** The whole number in the cell at `index`: `buffer[0]`. */
|
|
1027
|
+
@operator("[]") get(index: number): number {
|
|
1028
|
+
return this.data[index];
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** Puts a whole number in the cell at `index`: `buffer[0] = 5`. */
|
|
1032
|
+
@operator("[]=") set(index: number, value: number): void {
|
|
1033
|
+
this.data[index] = value;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** The fractional number in the cell at `index`, which a native wrote as `Float:`. */
|
|
1037
|
+
float(index: number): f64 {
|
|
1038
|
+
return cellFloat(this.data[index]);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** Puts a fractional number in the cell at `index`, for a native that reads `Float:`. */
|
|
1042
|
+
setFloat(index: number, value: f64): void {
|
|
1043
|
+
this.data[index] = floatCell(value);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/** The text a native wrote into the buffer. */
|
|
1047
|
+
text(): string {
|
|
1048
|
+
return cellsToString(this.data);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** Writes text into the buffer from the cell `at`, as a Pawn string. */
|
|
1052
|
+
write(at: number, value: string): void {
|
|
1053
|
+
__writeCellText(value, this.data, <i32>at);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** Sets every cell to `value`: `buffer.fill(0)` clears it. */
|
|
1057
|
+
fill(value: number): void {
|
|
1058
|
+
this.data.fill(value);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// `Array(33).fill(0)` needs a type argument AssemblyScript cannot infer; this
|
|
1063
|
+
// takes the type from `value`.
|
|
1064
|
+
/** An array of `length` copies of `value`: `arrayOf(33, 0)`. Every slot is the same `value` - for objects, give each slot its own. */
|
|
1065
|
+
export function arrayOf<T>(length: number, value: T): T[] {
|
|
1066
|
+
return new Array<T>(length).fill(value);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** Reads the number a native wrote through an `out()` buffer passed as a `&reference`. */
|
|
1070
|
+
export function cell(buffer: number): number {
|
|
1071
|
+
return load<i32>(buffer);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** Puts a number in an `out()` buffer, for a reference a native reads as well as writes. */
|
|
1075
|
+
export function putCell(buffer: number, value: number): number {
|
|
1076
|
+
store<i32>(buffer, value);
|
|
1077
|
+
return buffer;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* An empty origin for a message sent to one player:
|
|
1082
|
+
*
|
|
1083
|
+
* message_begin(MSG_ONE, msgid, noOrigin(), id);
|
|
1084
|
+
*
|
|
1085
|
+
* Only messages sent to players near a point read the origin.
|
|
1086
|
+
*/
|
|
1087
|
+
export function noOrigin(): number[] {
|
|
1088
|
+
return [0, 0, 0];
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/** The length of text an `out()` buffer holds: `255`. */
|
|
1092
|
+
export const TEXT_MAX: i32 = 255;
|
|
1093
|
+
|
|
1094
|
+
const outs: StaticArray<i32>[] = [
|
|
1095
|
+
new StaticArray<i32>(TEXT_MAX + 1), new StaticArray<i32>(TEXT_MAX + 1),
|
|
1096
|
+
new StaticArray<i32>(TEXT_MAX + 1), new StaticArray<i32>(TEXT_MAX + 1)
|
|
1097
|
+
];
|
|
1098
|
+
|
|
1099
|
+
let slot: i32 = 0;
|
|
1100
|
+
|
|
1101
|
+
// Scratch for readText, so a read does not allocate per call.
|
|
1102
|
+
const scratch = new StaticArray<i32>(256);
|
|
1103
|
+
const nameBuf = new StaticArray<u8>(64);
|
|
1104
|
+
|
|
1105
|
+
/** A Counter-Strike team, by the name the game gives it: one of `"TERRORIST"`, `"CT"`, `"SPECTATOR"`, `"UNASSIGNED"`. */
|
|
1106
|
+
export type Team = "TERRORIST" | "CT" | "SPECTATOR" | "UNASSIGNED";
|
|
1107
|
+
|
|
1108
|
+
const TEAM_NAMES: Team[] = ["UNASSIGNED", "TERRORIST", "CT", "SPECTATOR"];
|
|
1109
|
+
|
|
1110
|
+
/** A weapon a player can hold, by its class name, e.g. `"weapon_ak47"` or `"weapon_knife"`. */
|
|
1111
|
+
export type WeaponName =
|
|
1112
|
+
| "weapon_p228" | "weapon_scout" | "weapon_hegrenade" | "weapon_xm1014" | "weapon_c4"
|
|
1113
|
+
| "weapon_mac10" | "weapon_aug" | "weapon_smokegrenade" | "weapon_elite" | "weapon_fiveseven"
|
|
1114
|
+
| "weapon_ump45" | "weapon_sg550" | "weapon_galil" | "weapon_famas" | "weapon_usp"
|
|
1115
|
+
| "weapon_glock18" | "weapon_awp" | "weapon_mp5navy" | "weapon_m249" | "weapon_m3"
|
|
1116
|
+
| "weapon_m4a1" | "weapon_tmp" | "weapon_g3sg1" | "weapon_flashbang" | "weapon_deagle"
|
|
1117
|
+
| "weapon_sg552" | "weapon_ak47" | "weapon_knife" | "weapon_p90";
|
|
1118
|
+
|
|
1119
|
+
// Spelled out rather than `WeaponName | "item_kevlar" | ...`: AssemblyScript
|
|
1120
|
+
// reads a union of string literals as `string`, but not one that mixes a named
|
|
1121
|
+
// type in.
|
|
1122
|
+
/** An item `player.give` hands over: a weapon, armour (`"item_kevlar"`, `"item_assaultsuit"`) or the defuse kit (`"item_thighpack"`). */
|
|
1123
|
+
export type ItemName =
|
|
1124
|
+
| "weapon_p228" | "weapon_scout" | "weapon_hegrenade" | "weapon_xm1014" | "weapon_c4"
|
|
1125
|
+
| "weapon_mac10" | "weapon_aug" | "weapon_smokegrenade" | "weapon_elite" | "weapon_fiveseven"
|
|
1126
|
+
| "weapon_ump45" | "weapon_sg550" | "weapon_galil" | "weapon_famas" | "weapon_usp"
|
|
1127
|
+
| "weapon_glock18" | "weapon_awp" | "weapon_mp5navy" | "weapon_m249" | "weapon_m3"
|
|
1128
|
+
| "weapon_m4a1" | "weapon_tmp" | "weapon_g3sg1" | "weapon_flashbang" | "weapon_deagle"
|
|
1129
|
+
| "weapon_sg552" | "weapon_ak47" | "weapon_knife" | "weapon_p90"
|
|
1130
|
+
| "item_kevlar" | "item_assaultsuit" | "item_thighpack";
|
|
1131
|
+
|
|
1132
|
+
const WEAPON_IDS = [
|
|
1133
|
+
"", "weapon_p228", "", "weapon_scout", "weapon_hegrenade", "weapon_xm1014", "weapon_c4",
|
|
1134
|
+
"weapon_mac10", "weapon_aug", "weapon_smokegrenade", "weapon_elite", "weapon_fiveseven",
|
|
1135
|
+
"weapon_ump45", "weapon_sg550", "weapon_galil", "weapon_famas", "weapon_usp",
|
|
1136
|
+
"weapon_glock18", "weapon_awp", "weapon_mp5navy", "weapon_m249", "weapon_m3",
|
|
1137
|
+
"weapon_m4a1", "weapon_tmp", "weapon_g3sg1", "weapon_flashbang", "weapon_deagle",
|
|
1138
|
+
"weapon_sg552", "weapon_ak47", "weapon_knife", "weapon_p90"
|
|
1139
|
+
];
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* The filter of `Player.all`; every field is optional, e.g.
|
|
1143
|
+
* `Player.all({ alive: true, team: "CT" })`.
|
|
1144
|
+
*/
|
|
1145
|
+
export interface PlayerFilter {
|
|
1146
|
+
/** Only living players. */
|
|
1147
|
+
alive?: boolean;
|
|
1148
|
+
/** Only dead players. */
|
|
1149
|
+
dead?: boolean;
|
|
1150
|
+
/** Only players of this team, e.g. `"CT"`. */
|
|
1151
|
+
team?: Team;
|
|
1152
|
+
/** Only bots. */
|
|
1153
|
+
bots?: boolean;
|
|
1154
|
+
/** Only people, no bots. */
|
|
1155
|
+
humans?: boolean;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/** An AMX Mod X module a plugin can check for, one of `"reapi"`, `"cstrike"`, `"fun"`, `"hamsandwich"`, `"engine"`, `"fakemeta"`. */
|
|
1159
|
+
export type ModuleName = "reapi" | "cstrike" | "fun" | "hamsandwich" | "engine" | "fakemeta";
|
|
1160
|
+
|
|
1161
|
+
// Asked once each: a module is either loaded for the whole map or not at all.
|
|
1162
|
+
const moduleNames: string[] = [];
|
|
1163
|
+
const moduleLoaded: boolean[] = [];
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* `true` when the server has the module: `if (hasModule("reapi")) ...`.
|
|
1167
|
+
* Check it before a native that only one module has.
|
|
1168
|
+
*
|
|
1169
|
+
* Pawn: `LibraryExists`, `module_exists`
|
|
1170
|
+
*/
|
|
1171
|
+
export function hasModule(name: ModuleName): boolean {
|
|
1172
|
+
const at = moduleNames.indexOf(name);
|
|
1173
|
+
if (at >= 0) return moduleLoaded[at];
|
|
1174
|
+
|
|
1175
|
+
const loaded = LibraryExists(name, LibType_Library) != 0 || module_exists(name) != 0;
|
|
1176
|
+
moduleNames.push(name);
|
|
1177
|
+
moduleLoaded.push(loaded);
|
|
1178
|
+
return loaded;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/** Tells everyone's scoreboard a player's frags and deaths - what cs_set_user_deaths sends. */
|
|
1182
|
+
function sendScoreInfo(id: number, frags: number, deaths: number, team: number) {
|
|
1183
|
+
message_begin(MSG_ALL, get_user_msgid("ScoreInfo"), [0, 0, 0], 0);
|
|
1184
|
+
write_byte(id);
|
|
1185
|
+
write_short(frags);
|
|
1186
|
+
write_short(deaths);
|
|
1187
|
+
write_short(0);
|
|
1188
|
+
write_short(team);
|
|
1189
|
+
message_end();
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** The options of `player.kill()`. */
|
|
1193
|
+
export interface KillOptions {
|
|
1194
|
+
/** Keeps the player's frags: no penalty for the suicide. */
|
|
1195
|
+
keepFrags?: boolean;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Plugins add fields of their own to this class, shared by every plugin and by
|
|
1199
|
+
// Pawn, with `interface Player { spawnProtected: boolean }` in a
|
|
1200
|
+
// `declare module "~/facade"` block (docs/en/3.game/02.players.md) - not written
|
|
1201
|
+
// here as one block, which the build would take for a declaration of every
|
|
1202
|
+
// plugin's. The build turns each into a getter and a setter over the module's
|
|
1203
|
+
// store and adds them here (scripts/player-fields.ts).
|
|
1204
|
+
/**
|
|
1205
|
+
* A connecting player, in `"connect"`, `"authorized"` and `"putinserver"`: name,
|
|
1206
|
+
* address, SteamID and team, but no health or weapons yet. Every Player is a
|
|
1207
|
+
* Client too.
|
|
1208
|
+
*
|
|
1209
|
+
* ```ts
|
|
1210
|
+
* server.addEventListener("putinserver", (event) => {
|
|
1211
|
+
* print(event.player, `Welcome, ${event.player.name}!`);
|
|
1212
|
+
* });
|
|
1213
|
+
* ```
|
|
1214
|
+
*/
|
|
1215
|
+
export interface Client {
|
|
1216
|
+
/** The player's slot, `1` to `32`. */
|
|
1217
|
+
readonly id: number;
|
|
1218
|
+
/** The player's name. */
|
|
1219
|
+
readonly name: string;
|
|
1220
|
+
/** The player's IP address without the port, e.g. `"192.168.0.10"`. */
|
|
1221
|
+
readonly ip: string;
|
|
1222
|
+
/** The player's SteamID, e.g. `"STEAM_0:1:12345"`. A bot has `"BOT"`, HLTV has `"HLTV"`; until Steam confirms the player it is `"STEAM_ID_PENDING"` (wait for the `"authorized"` event), and on a LAN server `"STEAM_ID_LAN"`. */
|
|
1223
|
+
readonly authid: string;
|
|
1224
|
+
/** `true` for a bot. */
|
|
1225
|
+
readonly isBot: boolean;
|
|
1226
|
+
/** `true` while the player is on the server. */
|
|
1227
|
+
readonly isConnected: boolean;
|
|
1228
|
+
/** The player's admin rights, from the letters in `users.ini`: `client.access.includes("Cvar")`. */
|
|
1229
|
+
readonly access: Access[];
|
|
1230
|
+
/** The player's team, one of `"TERRORIST"`, `"CT"`, `"SPECTATOR"` or `"UNASSIGNED"` (until the player joins a team). Setting it moves the player, as `player.team` does. */
|
|
1231
|
+
team: Team;
|
|
1232
|
+
/** `true` when nobody hears the player on the voice chat. Setting it mutes or unmutes him. */
|
|
1233
|
+
muted: boolean;
|
|
1234
|
+
/** `true` when every player hears him on the voice chat, whatever the side. */
|
|
1235
|
+
heardByEveryone: boolean;
|
|
1236
|
+
/** `true` when he hears every player on the voice chat, whatever the side. */
|
|
1237
|
+
hearsEveryone: boolean;
|
|
1238
|
+
/** The language the player reads the server's text in, e.g. `"en"`, `"ru"`: the one `lang.translate` picks for him. */
|
|
1239
|
+
readonly language: string;
|
|
1240
|
+
/** A signal that aborts when the player leaves the server: `fetch(url, { signal: client.signal })`. */
|
|
1241
|
+
readonly signal: AbortSignal;
|
|
1242
|
+
/** Runs a command in the player's console, as if he had typed it: `client.command("stop")`. */
|
|
1243
|
+
command(text: string): void;
|
|
1244
|
+
/** Joins a side as the game joins a player who picks it in the team menu: `client.joinTeam("CT")`. `false` if the game refused. */
|
|
1245
|
+
joinTeam(team: Team): boolean;
|
|
1246
|
+
/** Asks the player's game for one of its cvars: `await client.queryCvar("fps_max")`, the value as text, or `null` when his game has none. */
|
|
1247
|
+
queryCvar(name: string): Promise<string | null>;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* A player in the game: everything a Client has, plus health, armor, frags,
|
|
1252
|
+
* weapons and the screen.
|
|
1253
|
+
*
|
|
1254
|
+
* An event about a player gives one as `event.player`; `Player.all()`
|
|
1255
|
+
* lists everyone on the server.
|
|
1256
|
+
*/
|
|
1257
|
+
export class Player extends PlayerFields implements Client {
|
|
1258
|
+
constructor(id: number) {
|
|
1259
|
+
super(id);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* The players on the server: `Player.all({ alive: true })`.
|
|
1264
|
+
*
|
|
1265
|
+
* Every field narrows: `{ bots: true }` is bots only, `{ humans: true }`
|
|
1266
|
+
* people only. Without a filter, everyone connected, never an HLTV proxy.
|
|
1267
|
+
*
|
|
1268
|
+
* Pawn: `get_players`
|
|
1269
|
+
*/
|
|
1270
|
+
static all(filter: PlayerFilter = {}): Player[] {
|
|
1271
|
+
let flags = "h";
|
|
1272
|
+
if (filter.alive) flags += "a";
|
|
1273
|
+
if (filter.dead) flags += "b";
|
|
1274
|
+
if (filter.humans) flags += "c";
|
|
1275
|
+
if (filter.bots) flags += "d";
|
|
1276
|
+
|
|
1277
|
+
const team = filter.team;
|
|
1278
|
+
if (team != null) flags += "e";
|
|
1279
|
+
|
|
1280
|
+
const ids = playerIds(flags, team ?? "");
|
|
1281
|
+
const list: Player[] = [];
|
|
1282
|
+
for (let i = 0; i < ids.length; i++) list.push(new Player(ids[i]));
|
|
1283
|
+
return list;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* The player's name.
|
|
1288
|
+
*
|
|
1289
|
+
* Pawn: `get_user_name`
|
|
1290
|
+
*/
|
|
1291
|
+
get name(): string {
|
|
1292
|
+
const len = _getName(this.id, changetype<i32>(nameBuf), 64);
|
|
1293
|
+
return String.UTF8.decodeUnsafe(changetype<usize>(nameBuf), len);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* The player's health. `100` at spawn. Setting it to `0` or less kills the player.
|
|
1298
|
+
*
|
|
1299
|
+
* Pawn: `get_user_health`, `set_user_health`
|
|
1300
|
+
*/
|
|
1301
|
+
get health(): number { return _getHealth(this.id); }
|
|
1302
|
+
set health(hp: number) { _setHealth(this.id, hp); }
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* The player's armor points: `100` with a bought vest, `0` without one.
|
|
1306
|
+
*
|
|
1307
|
+
* Pawn: `get_user_armor`, `set_user_armor`
|
|
1308
|
+
*/
|
|
1309
|
+
get armor(): number { return get_user_armor(this.id); }
|
|
1310
|
+
set armor(value: number) { set_user_armor(this.id, value); }
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* The player's frags on the scoreboard.
|
|
1314
|
+
*
|
|
1315
|
+
* Pawn: `get_user_frags`, `set_user_frags`
|
|
1316
|
+
*/
|
|
1317
|
+
get frags(): number { return get_user_frags(this.id); }
|
|
1318
|
+
set frags(value: number) { set_user_frags(this.id, value); }
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* The player's deaths on the scoreboard. Setting it updates the scoreboard
|
|
1322
|
+
* at once.
|
|
1323
|
+
*
|
|
1324
|
+
* Pawn: `cs_get_user_deaths`, `cs_set_user_deaths`
|
|
1325
|
+
*/
|
|
1326
|
+
get deaths(): number {
|
|
1327
|
+
return hasModule("reapi") ? get_member(this.id, m_iDeaths) : cs_get_user_deaths(this.id);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/** The deaths on the scoreboard; setting them tells the scoreboard too. */
|
|
1331
|
+
set deaths(value: number) {
|
|
1332
|
+
if (hasModule("reapi")) {
|
|
1333
|
+
set_member(this.id, m_iDeaths, value);
|
|
1334
|
+
sendScoreInfo(this.id, this.frags, value, get_member(this.id, m_iTeam));
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
cs_set_user_deaths(this.id, value);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* The player's team, one of `"TERRORIST"`, `"CT"`, `"SPECTATOR"` or `"UNASSIGNED"`. Correct
|
|
1342
|
+
* right after a team change too. Setting it moves the player.
|
|
1343
|
+
*
|
|
1344
|
+
* Pawn: `cs_get_user_team`, `rg_set_user_team`
|
|
1345
|
+
*/
|
|
1346
|
+
get team(): Team {
|
|
1347
|
+
const index = hasModule("reapi") ? get_member(this.id, m_iTeam) : cs_get_user_team(this.id);
|
|
1348
|
+
return index >= 0 && index < TEAM_NAMES.length ? TEAM_NAMES[index] : "UNASSIGNED";
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Moves the player to another team, as reapi's rg_set_user_team does: the
|
|
1353
|
+
* model is picked for the new team, the scoreboard is told, and the round's
|
|
1354
|
+
* win conditions are not checked - the caller decides when that happens.
|
|
1355
|
+
*/
|
|
1356
|
+
set team(value: Team) {
|
|
1357
|
+
const index = teamCell(value);
|
|
1358
|
+
if (hasModule("reapi")) {
|
|
1359
|
+
rg_set_user_team(this.id, index, MODEL_AUTO, true, false);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
// cstrike keeps the model unless told: the first one of the new side.
|
|
1363
|
+
const model = value == "TERRORIST" ? CS_T_TERROR : value == "CT" ? CS_CT_URBAN : CS_DONTCHANGE;
|
|
1364
|
+
cs_set_user_team(this.id, index, model);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Joins a side the way the game joins a player who picks it in the team
|
|
1369
|
+
* menu, appearance picked for him: `player.joinTeam("CT")`. A player who
|
|
1370
|
+
* has just arrived is in the game after it and can spawn, which
|
|
1371
|
+
* `player.team = ...` does not do for him. `false` if the game refused.
|
|
1372
|
+
*
|
|
1373
|
+
* Pawn: `rg_join_team`
|
|
1374
|
+
*/
|
|
1375
|
+
joinTeam(team: Team): boolean {
|
|
1376
|
+
if (team == "UNASSIGNED") return false;
|
|
1377
|
+
if (hasModule("reapi")) return rg_join_team(this.id, teamCell(team)) != 0;
|
|
1378
|
+
|
|
1379
|
+
// Without reapi, what the player would type: the team menu's slot, then
|
|
1380
|
+
// the appearance menu's automatic pick.
|
|
1381
|
+
engclient_cmd(this.id, "jointeam", team == "TERRORIST" ? "1" : team == "CT" ? "2" : "6");
|
|
1382
|
+
if (team != "SPECTATOR") engclient_cmd(this.id, "joinclass", "5");
|
|
1383
|
+
return this.team == team;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* The player's IP address without the port, e.g. `"192.168.0.10"`.
|
|
1388
|
+
*
|
|
1389
|
+
* Pawn: `get_user_ip`
|
|
1390
|
+
*/
|
|
1391
|
+
get ip(): string {
|
|
1392
|
+
// The fourth argument drops the port, which is what a plugin means by
|
|
1393
|
+
// an address nine times out of ten.
|
|
1394
|
+
return get_user_ip(this.id, 1);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* The player's SteamID, e.g. `"STEAM_0:1:12345"`. A bot has `"BOT"`, HLTV has `"HLTV"`; until Steam confirms the player it is `"STEAM_ID_PENDING"` (wait for the `"authorized"` event), and on a LAN server `"STEAM_ID_LAN"`.
|
|
1399
|
+
*
|
|
1400
|
+
* Pawn: `get_user_authid`
|
|
1401
|
+
*/
|
|
1402
|
+
get authid(): string {
|
|
1403
|
+
return get_user_authid(this.id);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* `true` while the player is alive.
|
|
1408
|
+
*
|
|
1409
|
+
* Pawn: `is_user_alive`
|
|
1410
|
+
*/
|
|
1411
|
+
get isAlive(): boolean { return is_user_alive(this.id) != 0; }
|
|
1412
|
+
/**
|
|
1413
|
+
* `true` while the player is on the server.
|
|
1414
|
+
*
|
|
1415
|
+
* Pawn: `is_user_connected`
|
|
1416
|
+
*/
|
|
1417
|
+
get isConnected(): boolean { return is_user_connected(this.id) != 0; }
|
|
1418
|
+
/**
|
|
1419
|
+
* `true` for a bot.
|
|
1420
|
+
*
|
|
1421
|
+
* Pawn: `is_user_bot`
|
|
1422
|
+
*/
|
|
1423
|
+
get isBot(): boolean { return is_user_bot(this.id) != 0; }
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* A signal that aborts when the player leaves the server, with an Error named
|
|
1427
|
+
* `"AbortError"`; the next player in the slot gets a new one.
|
|
1428
|
+
*
|
|
1429
|
+
* ```ts
|
|
1430
|
+
* const response = await fetch(url, { signal: player.signal });
|
|
1431
|
+
* ```
|
|
1432
|
+
*
|
|
1433
|
+
* An async command handler or player event already runs under it: its
|
|
1434
|
+
* `await`s stop quietly when the player leaves.
|
|
1435
|
+
*/
|
|
1436
|
+
get signal(): AbortSignal { return __co_player_signal(this.id); }
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* `true` when nobody hears the player on the voice chat, with alltalk or
|
|
1440
|
+
* without. Setting it mutes or unmutes him; his other voice settings stay.
|
|
1441
|
+
*
|
|
1442
|
+
* Pawn: `set_speak`, `SPEAK_MUTED`
|
|
1443
|
+
*/
|
|
1444
|
+
get muted(): boolean { return (get_speak(this.id) & SPEAK_MUTED) != 0; }
|
|
1445
|
+
set muted(value: boolean) { this.setSpeak(SPEAK_MUTED, value); }
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* `true` when every player hears him on the voice chat, whatever the side
|
|
1449
|
+
* and without alltalk. A mute (`muted`) still silences him.
|
|
1450
|
+
*
|
|
1451
|
+
* Pawn: `set_speak`, `SPEAK_ALL`
|
|
1452
|
+
*/
|
|
1453
|
+
get heardByEveryone(): boolean { return (get_speak(this.id) & SPEAK_ALL) != 0; }
|
|
1454
|
+
set heardByEveryone(value: boolean) { this.setSpeak(SPEAK_ALL, value); }
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* `true` when he hears every player on the voice chat, whatever the side
|
|
1458
|
+
* and without alltalk: a spectator who hears both sides.
|
|
1459
|
+
*
|
|
1460
|
+
* Pawn: `set_speak`, `SPEAK_LISTENALL`
|
|
1461
|
+
*/
|
|
1462
|
+
get hearsEveryone(): boolean { return (get_speak(this.id) & SPEAK_LISTENALL) != 0; }
|
|
1463
|
+
set hearsEveryone(value: boolean) { this.setSpeak(SPEAK_LISTENALL, value); }
|
|
1464
|
+
|
|
1465
|
+
// One voice setting of set_speak's, the others kept.
|
|
1466
|
+
private setSpeak(flag: i32, on: bool): void {
|
|
1467
|
+
const flags = get_speak(this.id);
|
|
1468
|
+
set_speak(this.id, on ? flags | flag : flags & ~flag);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* The language the player reads the server's text in, e.g. `"en"`, `"ru"`:
|
|
1473
|
+
* the one `lang.translate` picks for him. It is his `setinfo lang`, or the
|
|
1474
|
+
* server's language when he has none or `amx_client_languages` is `0`.
|
|
1475
|
+
*
|
|
1476
|
+
* Pawn: `get_user_info(id, "lang")`, `amx_language`
|
|
1477
|
+
*/
|
|
1478
|
+
get language(): string {
|
|
1479
|
+
// AMX Mod X's own rule (playerlang, translate): a language starts with a letter.
|
|
1480
|
+
const own = get_cvar_num("amx_client_languages") != 0 ? get_user_info(this.id, "lang") : "";
|
|
1481
|
+
const first = own.length > 0 ? own.charCodeAt(0) | 0x20 : 0;
|
|
1482
|
+
return first >= 0x61 && first <= 0x7a ? own : get_cvar_string("amx_language");
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* Shows a line of text on the player's screen:
|
|
1487
|
+
* `player.showHud("-35 HP", { color: [255, 40, 40], x: 0.02, y: 0.88, hold: 2 })`.
|
|
1488
|
+
* Every option has a default.
|
|
1489
|
+
*
|
|
1490
|
+
* Pawn: `set_hudmessage`, `show_hudmessage`
|
|
1491
|
+
*/
|
|
1492
|
+
showHud(text: string, options: HudOptions = {}): void {
|
|
1493
|
+
showHudTo(this.id, text, options);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Plays a sound to the player alone, the way the radio does - heard as it
|
|
1498
|
+
* is wherever he stands: `player.playSound("vox/one.wav")`. The path is
|
|
1499
|
+
* under `sound/`, as `server.precache` takes it.
|
|
1500
|
+
*
|
|
1501
|
+
* Pawn: `SendAudio`, `rg_send_audio`
|
|
1502
|
+
*/
|
|
1503
|
+
playSound(sample: string): void {
|
|
1504
|
+
message_begin(MSG_ONE, get_user_msgid("SendAudio"), [0, 0, 0], this.id);
|
|
1505
|
+
write_byte(this.id);
|
|
1506
|
+
write_string(sample);
|
|
1507
|
+
write_short(100);
|
|
1508
|
+
message_end();
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/** The player's screen effects: `player.screen.fade({ ... })`, `.shake(...)`, `.statusIcon(...)` — see Screen. */
|
|
1512
|
+
get screen(): Screen { return new Screen(this.id); }
|
|
1513
|
+
|
|
1514
|
+
// ------------------------------------------------------------ actions
|
|
1515
|
+
//
|
|
1516
|
+
// reapi where the server has it, the stock AMX Mod X modules where it does
|
|
1517
|
+
// not: fun, cstrike, hamsandwich and the core.
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Gives the player a weapon or an item: `player.give("weapon_flashbang")`.
|
|
1521
|
+
* `false` if the game did not give it.
|
|
1522
|
+
*
|
|
1523
|
+
* Pawn: `rg_give_item`, `give_item`
|
|
1524
|
+
*/
|
|
1525
|
+
give(item: ItemName): boolean {
|
|
1526
|
+
if (hasModule("reapi")) return rg_give_item(this.id, item) > 0;
|
|
1527
|
+
return give_item(this.id, item) > 0;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Takes all the player's weapons away. The suit (armour, HUD) stays unless
|
|
1532
|
+
* `removeSuit` is `true`.
|
|
1533
|
+
*
|
|
1534
|
+
* Pawn: `rg_remove_all_items`, `strip_user_weapons`
|
|
1535
|
+
*/
|
|
1536
|
+
removeAllItems(removeSuit: boolean = false): void {
|
|
1537
|
+
if (hasModule("reapi")) {
|
|
1538
|
+
rg_remove_all_items(this.id, removeSuit);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
strip_user_weapons(this.id);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Sets the player's reserve ammo for a weapon he carries: `player.setAmmo("weapon_flashbang", 2)`.
|
|
1546
|
+
*
|
|
1547
|
+
* Pawn: `rg_set_user_bpammo`, `cs_set_user_bpammo`
|
|
1548
|
+
*/
|
|
1549
|
+
setAmmo(weapon: WeaponName, amount: number): void {
|
|
1550
|
+
const id = WEAPON_IDS.indexOf(weapon);
|
|
1551
|
+
if (id <= 0) return;
|
|
1552
|
+
if (hasModule("reapi")) rg_set_user_bpammo(this.id, id, amount);
|
|
1553
|
+
else cs_set_user_bpammo(this.id, id, amount);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* The player's reserve ammo for a weapon he carries: `player.getAmmo("weapon_ak47")`;
|
|
1558
|
+
* for a grenade, how many he has. `0` for a weapon he does not carry.
|
|
1559
|
+
*
|
|
1560
|
+
* Pawn: `rg_get_user_bpammo`, `cs_get_user_bpammo`
|
|
1561
|
+
*/
|
|
1562
|
+
getAmmo(weapon: WeaponName): number {
|
|
1563
|
+
const id = WEAPON_IDS.indexOf(weapon);
|
|
1564
|
+
if (id <= 0) return 0;
|
|
1565
|
+
return hasModule("reapi") ? rg_get_user_bpammo(this.id, id) : cs_get_user_bpammo(this.id, id);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* Respawns the player in the current round, at a spawn point the game picks.
|
|
1570
|
+
*
|
|
1571
|
+
* Pawn: `rg_round_respawn`
|
|
1572
|
+
*/
|
|
1573
|
+
respawn(): void {
|
|
1574
|
+
if (hasModule("reapi")) rg_round_respawn(this.id);
|
|
1575
|
+
else ExecuteHamB(Ham_CS_RoundRespawn, this.id);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Kills the player, as the `kill` console command does. With
|
|
1580
|
+
* `{ keepFrags: true }` the death costs no frags.
|
|
1581
|
+
*
|
|
1582
|
+
* Pawn: `user_kill`, `user_silentkill`
|
|
1583
|
+
*/
|
|
1584
|
+
kill(options: KillOptions = {}): void {
|
|
1585
|
+
user_kill(this.id, options.keepFrags ? 1 : 0);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* The player's admin rights, from the letters in `users.ini`:
|
|
1590
|
+
* `player.access.includes("Cvar")`.
|
|
1591
|
+
*
|
|
1592
|
+
* Pawn: `get_user_flags`
|
|
1593
|
+
*/
|
|
1594
|
+
get access(): Access[] {
|
|
1595
|
+
return <Access[]>ACCESS.namesOf(get_user_flags(this.id));
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Puts a weapon the player carries into his hands. `false` if he does not
|
|
1600
|
+
* have it.
|
|
1601
|
+
*
|
|
1602
|
+
* Pawn: `rg_switch_weapon`
|
|
1603
|
+
*/
|
|
1604
|
+
switchWeapon(weapon: WeaponName): boolean {
|
|
1605
|
+
if (hasModule("reapi")) {
|
|
1606
|
+
const entity = rg_find_weapon_bpack_by_name(this.id, weapon);
|
|
1607
|
+
return entity > 0 && rg_switch_weapon(this.id, entity) != 0;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const id = WEAPON_IDS.indexOf(weapon);
|
|
1611
|
+
if (id <= 0 || user_has_weapon(this.id, id) == 0) return false;
|
|
1612
|
+
engclient_cmd(this.id, weapon);
|
|
1613
|
+
return true;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Recomputes the player's speed from the weapon in hand, after a slowdown for
|
|
1618
|
+
* instance.
|
|
1619
|
+
*
|
|
1620
|
+
* Pawn: `rg_reset_maxspeed`
|
|
1621
|
+
*/
|
|
1622
|
+
resetMaxSpeed(): void {
|
|
1623
|
+
if (hasModule("reapi")) rg_reset_maxspeed(this.id);
|
|
1624
|
+
else ExecuteHamB(Ham_CS_Player_ResetMaxSpeed, this.id);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Runs a command in the player's own console, as if he had typed it:
|
|
1629
|
+
* `player.command("messagemode say_team")`, `player.command("stop")`.
|
|
1630
|
+
* The player's game runs it, not the server.
|
|
1631
|
+
*
|
|
1632
|
+
* Pawn: `client_cmd`
|
|
1633
|
+
*/
|
|
1634
|
+
command(text: string): void {
|
|
1635
|
+
new Call(NATIVE_client_cmd).num(this.id).str("%s").str(text).run();
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Asks the player's game for one of its cvars: `await
|
|
1640
|
+
* player.queryCvar("fps_max")` is the value as text, e.g. `"100"`, or
|
|
1641
|
+
* `null` when his game has no such cvar or will not tell. The answer is
|
|
1642
|
+
* what his game says, a claim a cheat can change. A bot has no game to ask
|
|
1643
|
+
* and answers `null` at once; when the player leaves before he answers, the
|
|
1644
|
+
* promise is rejected with an `"AbortError"`.
|
|
1645
|
+
*
|
|
1646
|
+
* Pawn: `query_client_cvar`
|
|
1647
|
+
*/
|
|
1648
|
+
queryCvar(name: string): Promise<string | null> {
|
|
1649
|
+
const promise = __co_promise<string | null>();
|
|
1650
|
+
|
|
1651
|
+
if (this.isBot || !this.isConnected) {
|
|
1652
|
+
__co_resolve<string | null>(promise, null);
|
|
1653
|
+
return promise;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (cvarAnswer.length == 0) cvarAnswer = callbackName(cvarAnswered, "querycvar");
|
|
1657
|
+
const query = new CvarQuery(this.id, name.toLowerCase(), promise);
|
|
1658
|
+
query.guard = new __AbortGuard(query, this.signal);
|
|
1659
|
+
cvarQueries.push(query);
|
|
1660
|
+
query_client_cvar(this.id, name, cvarAnswer);
|
|
1661
|
+
return promise;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// ---------------------------------------------------------------- a client's cvars
|
|
1666
|
+
|
|
1667
|
+
// The questions sent and not answered yet, in the order asked. The engine
|
|
1668
|
+
// hands every answer to one public, with the player and the cvar's name.
|
|
1669
|
+
class CvarQuery extends __AbortWatch {
|
|
1670
|
+
guard: __AbortGuard | null = null;
|
|
1671
|
+
|
|
1672
|
+
constructor(public player: i32, public name: string, public promise: Promise<string | null>) {
|
|
1673
|
+
super();
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/** The player left, or what awaits the answer gave up. */
|
|
1677
|
+
run(reason: Error): void {
|
|
1678
|
+
forgetQuery(this);
|
|
1679
|
+
this.promise.__reject(reason);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const cvarQueries: CvarQuery[] = [];
|
|
1684
|
+
let cvarAnswer = "";
|
|
1685
|
+
|
|
1686
|
+
function forgetQuery(query: CvarQuery): void {
|
|
1687
|
+
const at = cvarQueries.indexOf(query);
|
|
1688
|
+
if (at >= 0) cvarQueries.splice(at, 1);
|
|
1689
|
+
(query.guard as __AbortGuard).release();
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/** public cvar_answer(id, const cvar[], const value[], const param[]) */
|
|
1693
|
+
function cvarAnswered(id: number, cvar: number, value: number, param: number): void {
|
|
1694
|
+
const name = argText(1).toLowerCase();
|
|
1695
|
+
|
|
1696
|
+
for (let i = 0; i < cvarQueries.length; i++) {
|
|
1697
|
+
const query = cvarQueries[i];
|
|
1698
|
+
if (query.player != <i32>id || query.name != name) continue;
|
|
1699
|
+
|
|
1700
|
+
forgetQuery(query);
|
|
1701
|
+
// What the engine answers for a cvar the client has not, or will not tell.
|
|
1702
|
+
const text = argText(2);
|
|
1703
|
+
const unknown = text == "Bad CVAR request" || text == "CVAR is privileged";
|
|
1704
|
+
__co_resolve<string | null>(query.promise, unknown ? null : text);
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* The public name that calls `handler`, whether the slot is new or taken back
|
|
1711
|
+
* after a reload: for a native handed a callback on every call
|
|
1712
|
+
* (query_client_cvar), not a registration made once (see publicFor).
|
|
1713
|
+
*/
|
|
1714
|
+
function callbackName(handler: WideHandler, key: string): string {
|
|
1715
|
+
const slot = _slot(hostIndex(handler, true), SHAPE_WIDE, key, 0);
|
|
1716
|
+
return slot < 0 ? "" : `__amxts_cb${slot & 0xffff}`;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// A Pawn string is one character per cell, so calling such a native directly
|
|
1720
|
+
// means a StaticArray<i32>, its address, and decoding it afterwards.
|
|
1721
|
+
/**
|
|
1722
|
+
* Calls a native that fills a text buffer and returns the text. The native
|
|
1723
|
+
* itself is passed in:
|
|
1724
|
+
*
|
|
1725
|
+
* readText(get_mapname) // "c21_kitty"
|
|
1726
|
+
* readText(get_user_name, 32, id) // not this one: see Player.name
|
|
1727
|
+
*/
|
|
1728
|
+
export function readText(fill: (out: number, max: number) => number, max: number = 255): string {
|
|
1729
|
+
fill(changetype<i32>(scratch), min(max, 255));
|
|
1730
|
+
return cellsToString(scratch);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* The ids of the players on the server, as an array. `flags`: `"a"` living,
|
|
1735
|
+
* `"b"` dead, `"c"` no bots, `"h"` no HLTV, `"e"` only `team`. `Player.all` does the
|
|
1736
|
+
* same with readable options.
|
|
1737
|
+
*
|
|
1738
|
+
* Pawn: `get_players`
|
|
1739
|
+
*/
|
|
1740
|
+
export function playerIds(flags: string = "", team: string = ""): number[] {
|
|
1741
|
+
const list = new StaticArray<i32>(33);
|
|
1742
|
+
const count = new StaticArray<i32>(1);
|
|
1743
|
+
|
|
1744
|
+
get_players(changetype<i32>(list), changetype<i32>(count), cells(flags), cells(team));
|
|
1745
|
+
|
|
1746
|
+
const ids: number[] = [];
|
|
1747
|
+
for (let i = 0; i < unchecked(count[0]); i++) ids.push(unchecked(list[i]));
|
|
1748
|
+
return ids;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Server's event name is typed like a DOM one - `K extends keyof ServerEventMap` -
|
|
1752
|
+
// so an editor completes it, refuses a misspelled one, and hands the listener
|
|
1753
|
+
// the event's own type. The compiler reads the same signature through a patch
|
|
1754
|
+
// (runtime/patches): the name has to be written out as a string literal,
|
|
1755
|
+
// because a string held in a variable says nothing about which event it is.
|
|
1756
|
+
// Its words for the plugin author are the Server entry of scripts/docs/as/facade.ts.
|
|
1757
|
+
|
|
1758
|
+
// ---------------------------------------------------------------- commands
|
|
1759
|
+
|
|
1760
|
+
/** A command's handler: gets the player who typed it and the words after the command's name. */
|
|
1761
|
+
export type CommandHandler = (player: Player, args: string[]) => void;
|
|
1762
|
+
|
|
1763
|
+
/** The options of a command: who may use it and its description in a listing. */
|
|
1764
|
+
export interface CommandOptions {
|
|
1765
|
+
/** The admin right a player needs to use the command; left out, everyone may. */
|
|
1766
|
+
access?: Access;
|
|
1767
|
+
/** The command's description, shown by `amx_help` and the like. */
|
|
1768
|
+
description?: string;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// The commands of this plugin, by name. One trampoline serves them all - the
|
|
1772
|
+
// host calls it by its table index - and it finds the handler by the name the
|
|
1773
|
+
// player typed.
|
|
1774
|
+
const commandNames: string[] = [];
|
|
1775
|
+
const commandHandlers: CommandHandler[] = [];
|
|
1776
|
+
const commandAccess: string[] = [];
|
|
1777
|
+
let chatHooked = false;
|
|
1778
|
+
|
|
1779
|
+
function findCommand(name: string): i32 {
|
|
1780
|
+
const lower = name.toLowerCase();
|
|
1781
|
+
for (let i = 0; i < commandNames.length; i++) {
|
|
1782
|
+
if (commandNames[i] == lower) return i;
|
|
1783
|
+
}
|
|
1784
|
+
return -1;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* Converts `users.ini` letters to rights: `accessOf("abc")` is
|
|
1789
|
+
* [`"Immunity"`, `"Reservation"`, `"Kick"`]. An unknown letter is skipped.
|
|
1790
|
+
*
|
|
1791
|
+
* Pawn: `read_flags`
|
|
1792
|
+
*/
|
|
1793
|
+
export function accessOf(letters: string): Access[] {
|
|
1794
|
+
return <Access[]>ACCESS.namesOf(read_flags(letters));
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/** Whether a player holds the admin flag a command asks for. */
|
|
1798
|
+
function mayRun(id: i32, at: i32): bool {
|
|
1799
|
+
const access = commandAccess[at];
|
|
1800
|
+
if (access.length == 0) return true;
|
|
1801
|
+
return (get_user_flags(id) & ACCESS.bitOf(access)) != 0;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/** say /name a b - the chat text split on spaces, the first word the name. */
|
|
1805
|
+
function chatCommand(player: number, level: number, cid: number, unused: number): void {
|
|
1806
|
+
const id = <i32>player;
|
|
1807
|
+
const text = read_argv(1).trim();
|
|
1808
|
+
const slash = text.startsWith("/");
|
|
1809
|
+
|
|
1810
|
+
// "/name a b" - or a phrase added as "say <phrase>", which is the whole line.
|
|
1811
|
+
const words = text.split(" ").filter((word: string) => word.length > 0);
|
|
1812
|
+
const at = slash && words.length > 0 ? findCommand(words[0]) : findCommand("say " + text);
|
|
1813
|
+
if (at < 0 || !mayRun(id, at)) { _outcome(0); return; }
|
|
1814
|
+
if (!slash) words.length = 1;
|
|
1815
|
+
runCommand(at, id, words.slice(1));
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/** name a b - a console command, its arguments as the engine split them. */
|
|
1819
|
+
function consoleCommand(player: number, level: number, cid: number, unused: number): void {
|
|
1820
|
+
const id = <i32>player;
|
|
1821
|
+
const at = findCommand(read_argv(0));
|
|
1822
|
+
if (at < 0 || !mayRun(id, at)) { _outcome(0); return; }
|
|
1823
|
+
runCommand(at, id, commandArgs());
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Runs a player's command. An async handler runs under the player's signal
|
|
1828
|
+
* (see __co_ambient_player); the command is handled, so a chat command is not
|
|
1829
|
+
* repeated in chat, as a Pawn command's PLUGIN_HANDLED does.
|
|
1830
|
+
*/
|
|
1831
|
+
function runCommand(at: i32, id: i32, args: string[]): void {
|
|
1832
|
+
const ambient = __co_ambient_player;
|
|
1833
|
+
__co_ambient_player = id;
|
|
1834
|
+
commandHandlers[at](new Player(id), args);
|
|
1835
|
+
__co_ambient_player = ambient;
|
|
1836
|
+
handled();
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/** The words after the command's name, as the engine split them. */
|
|
1840
|
+
function commandArgs(): string[] {
|
|
1841
|
+
const args: string[] = [];
|
|
1842
|
+
const count = read_argc();
|
|
1843
|
+
for (let i = 1; i < count; i++) args.push(read_argv(i));
|
|
1844
|
+
return args;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/** A server command's handler: gets the words after the command's name. */
|
|
1848
|
+
export type ServerCommandHandler = (args: string[]) => void;
|
|
1849
|
+
|
|
1850
|
+
// The server commands of this plugin, by name, and the ones still waiting for
|
|
1851
|
+
// plugin_init. One trampoline serves them all, as it does the player commands.
|
|
1852
|
+
const serverCommandNames: string[] = [];
|
|
1853
|
+
const serverCommandHandlers: ServerCommandHandler[] = [];
|
|
1854
|
+
const waitingServerCommands: string[] = [];
|
|
1855
|
+
|
|
1856
|
+
// A touch the engine module filters by class, so a touch nobody listens for -
|
|
1857
|
+
// and there is one every frame for a player on the ground - never reaches the
|
|
1858
|
+
// plugin. One register_touch per pair of classes, its listeners behind it.
|
|
1859
|
+
class TouchFilter {
|
|
1860
|
+
listeners: TouchListener[] = [];
|
|
1861
|
+
constructor(public toucher: string, public touched: string) {}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const touchFilters: TouchFilter[] = [];
|
|
1865
|
+
const waitingTouches: TouchFilter[] = [];
|
|
1866
|
+
|
|
1867
|
+
// A Ham Sandwich hook waiting for plugin_init: RegisterHam makes an entity of
|
|
1868
|
+
// the class to find its function, which is not for the moment a plugin loads.
|
|
1869
|
+
class HamRegistration {
|
|
1870
|
+
constructor(public fn: i32, public classname: string, public handler: WideHandler, public post: bool) {}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const waitingHams: HamRegistration[] = [];
|
|
1874
|
+
|
|
1875
|
+
function registerHam(registration: HamRegistration): void {
|
|
1876
|
+
_ham(registration.fn, registration.classname, hostIndex(registration.handler, true), registration.post ? 1 : 0);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/**
|
|
1880
|
+
* @hidden The hood of a game event Ham Sandwich delivers (as/hooks.ts):
|
|
1881
|
+
* `fn` hooked on the class, a reload taking its slot back.
|
|
1882
|
+
*/
|
|
1883
|
+
export function __ham(fn: i32, classname: string, handler: WideHandler, post: bool): void {
|
|
1884
|
+
const registration = new HamRegistration(fn, classname, handler, post);
|
|
1885
|
+
if (serverUp) registerHam(registration);
|
|
1886
|
+
else waitingHams.push(registration);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/** name a b - typed in the server console or sent over rcon. */
|
|
1890
|
+
function serverCommand(a: number, b: number, c: number, d: number): void {
|
|
1891
|
+
const at = serverCommandNames.indexOf(read_argv(0).toLowerCase());
|
|
1892
|
+
if (at < 0) { _outcome(0); return; }
|
|
1893
|
+
|
|
1894
|
+
serverCommandHandlers[at](commandArgs());
|
|
1895
|
+
handled();
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Hands one name to the engine. Not from a plugin's top level: that runs
|
|
1900
|
+
* during plugin_natives, and a command registered then crashes the server
|
|
1901
|
+
* when it is typed (see publicFor). A reload takes the same public back, and
|
|
1902
|
+
* the engine still knows the name, so nothing is registered twice.
|
|
1903
|
+
*/
|
|
1904
|
+
function registerServerCommand(name: string): void {
|
|
1905
|
+
const pub = publicFor(serverCommand, "srvcmd:" + name, 1);
|
|
1906
|
+
if (pub.length > 0) register_srvcmd(name, pub);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// ---------------------------------------------------------------- HUD
|
|
1910
|
+
|
|
1911
|
+
/**
|
|
1912
|
+
* The look of a HUD message. Every field has a default, so
|
|
1913
|
+
* `{ color: [255, 40, 40] }` is enough.
|
|
1914
|
+
*
|
|
1915
|
+
* Pawn: `set_hudmessage`
|
|
1916
|
+
*/
|
|
1917
|
+
export interface HudOptions {
|
|
1918
|
+
/** The text's colour: red, green, blue, `0` to `255` each. */
|
|
1919
|
+
color?: number[];
|
|
1920
|
+
/** The horizontal position: `0` is the left edge, `1` the right; `-1` centres the text. */
|
|
1921
|
+
x?: number;
|
|
1922
|
+
/** The vertical position: `0` is the top, `1` the bottom; `-1` centres the text. */
|
|
1923
|
+
y?: number;
|
|
1924
|
+
/** The time the message stays on screen, in seconds. */
|
|
1925
|
+
hold?: number;
|
|
1926
|
+
/** The appearance effect, one of: `"fade"` in and out, `"flicker"`, or `"typewriter"` — letter by letter. */
|
|
1927
|
+
effect?: HudEffect;
|
|
1928
|
+
/** The fade-in time, in seconds. */
|
|
1929
|
+
fadeIn?: number;
|
|
1930
|
+
/** The fade-out time, in seconds. */
|
|
1931
|
+
fadeOut?: number;
|
|
1932
|
+
/** The HUD channel, `1` to `4`; `-1` picks a free one. */
|
|
1933
|
+
channel?: number;
|
|
1934
|
+
/** The duration of the `"flicker"` and `"typewriter"` effects, in seconds. */
|
|
1935
|
+
effectTime?: number;
|
|
1936
|
+
/**
|
|
1937
|
+
* Large letters, for a result or a headline. They have no channels, so
|
|
1938
|
+
* `channel` does not apply.
|
|
1939
|
+
*
|
|
1940
|
+
* Pawn: `set_dhudmessage`
|
|
1941
|
+
*/
|
|
1942
|
+
large?: boolean;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
/** A HUD message's appearance effect, one of: `"fade"` in and out, `"flicker"`, or `"typewriter"` — letter by letter. */
|
|
1946
|
+
export type HudEffect = "fade" | "flicker" | "typewriter";
|
|
1947
|
+
|
|
1948
|
+
function hudEffect(options: HudOptions): i32 {
|
|
1949
|
+
return options.effect == "flicker" ? 1 : options.effect == "typewriter" ? 2 : 0;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// A field left out is AMX Mod X's own default, set_hudmessage's.
|
|
1953
|
+
function setHud(options: HudOptions): void {
|
|
1954
|
+
const color = options.color ?? [200, 100, 0];
|
|
1955
|
+
const red = color.length > 0 ? color[0] : 200;
|
|
1956
|
+
const green = color.length > 1 ? color[1] : 100;
|
|
1957
|
+
const blue = color.length > 2 ? color[2] : 0;
|
|
1958
|
+
const x = options.x ?? -1.0;
|
|
1959
|
+
const y = options.y ?? 0.35;
|
|
1960
|
+
const effectTime = options.effectTime ?? 6.0;
|
|
1961
|
+
const hold = options.hold ?? 12.0;
|
|
1962
|
+
const fadeIn = options.fadeIn ?? 0.1;
|
|
1963
|
+
const fadeOut = options.fadeOut ?? 0.2;
|
|
1964
|
+
if (options.large) {
|
|
1965
|
+
set_dhudmessage(red, green, blue, x, y, hudEffect(options), effectTime, hold, fadeIn, fadeOut);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
set_hudmessage(red, green, blue, x, y, hudEffect(options), effectTime, hold, fadeIn, fadeOut, options.channel ?? -1);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function showHudTo(id: i32, text: string, options: HudOptions): void {
|
|
1972
|
+
setHud(options);
|
|
1973
|
+
if (options.large) show_dhudmessage(id, text);
|
|
1974
|
+
else show_hudmessage(id, text);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/**
|
|
1978
|
+
* A HUD line for one message: showing a new one replaces the old instead of
|
|
1979
|
+
* taking another channel — for a countdown redrawn every second, a warning
|
|
1980
|
+
* that changes. `clear` removes it early.
|
|
1981
|
+
*
|
|
1982
|
+
* ```ts
|
|
1983
|
+
* const countdown = new HudLine();
|
|
1984
|
+
* countdown.show(player, `${left}`, { color: [255, 50, 50], hold: 1.1 });
|
|
1985
|
+
* countdown.clear(player);
|
|
1986
|
+
* countdown.clearAll();
|
|
1987
|
+
* ```
|
|
1988
|
+
*
|
|
1989
|
+
* Pawn: `CreateHudSyncObj`, `ShowSyncHudMsg`
|
|
1990
|
+
*/
|
|
1991
|
+
export class HudLine {
|
|
1992
|
+
private handle: i32 = -1;
|
|
1993
|
+
|
|
1994
|
+
/** Shows `text` to the player on this line, replacing what it showed him before. */
|
|
1995
|
+
show(player: Player, text: string, options: HudOptions = {}): void {
|
|
1996
|
+
if (this.handle < 0) this.handle = <i32>CreateHudSyncObj();
|
|
1997
|
+
setHud(options);
|
|
1998
|
+
ShowSyncHudMsg(player.id, this.handle, text);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/** Removes the line's message from the player's screen before its time is up. */
|
|
2002
|
+
clear(player: Player): void {
|
|
2003
|
+
if (this.handle >= 0) ClearSyncHud(player.id, this.handle);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/** Removes the line's message from every screen. */
|
|
2007
|
+
clearAll(): void {
|
|
2008
|
+
if (this.handle >= 0) ClearSyncHud(0, this.handle);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// ---------------------------------------------------------------- the screen
|
|
2013
|
+
|
|
2014
|
+
/** A fade's direction, one of: `"in"` from the colour to a clear view, `"out"` from a clear view to the colour. */
|
|
2015
|
+
export type FadeDirection = "in" | "out";
|
|
2016
|
+
|
|
2017
|
+
/** The options of `player.screen.fade`. Times are in seconds. */
|
|
2018
|
+
export interface FadeOptions {
|
|
2019
|
+
/** The colour: red, green, blue and alpha, `0` to `255` each; black by default. */
|
|
2020
|
+
color?: number[];
|
|
2021
|
+
/** The fade's duration, in seconds; `1` by default. */
|
|
2022
|
+
duration?: number;
|
|
2023
|
+
/** The time the full colour holds, in seconds; `0` by default. */
|
|
2024
|
+
hold?: number;
|
|
2025
|
+
/** The fade's direction, one of: `"in"` (the default) from the colour to a clear view, `"out"` from a clear view to the colour. */
|
|
2026
|
+
direction?: FadeDirection;
|
|
2027
|
+
/** Keeps the colour on the screen until the next fade. */
|
|
2028
|
+
stay?: boolean;
|
|
2029
|
+
/** Tints what is on the screen rather than painting over it. */
|
|
2030
|
+
modulate?: boolean;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/** The options of `player.screen.shake`. */
|
|
2034
|
+
export interface ShakeOptions {
|
|
2035
|
+
/** The shake's strength: how far the view moves, up to 16 units; `4` by default. */
|
|
2036
|
+
amplitude?: number;
|
|
2037
|
+
/** The shake's duration, in seconds; `1` by default. */
|
|
2038
|
+
duration?: number;
|
|
2039
|
+
/** The shake's frequency, in jolts a second; `5` by default. */
|
|
2040
|
+
frequency?: number;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/** A status icon's state, one of `"hide"`, `"show"` (lit) or `"flash"`. */
|
|
2044
|
+
export type StatusIconState = "hide" | "show" | "flash";
|
|
2045
|
+
|
|
2046
|
+
/** A time or a size as a message's fixed point: `units` of it make one, capped at 16 bits. */
|
|
2047
|
+
function fixed(value: number, units: number): i32 {
|
|
2048
|
+
return <i32>Math.max(0.0, Math.min(65535.0, Math.round(value * units)));
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
/**
|
|
2052
|
+
* The effects one player sees over the world: fades, shakes, status icons and
|
|
2053
|
+
* the parts of the HUD the game draws itself.
|
|
2054
|
+
*
|
|
2055
|
+
* ```ts
|
|
2056
|
+
* player.screen.fade({ color: [0, 0, 0, 255], duration: 0.5, hold: 1, stay: true });
|
|
2057
|
+
* player.screen.shake({ amplitude: 8, duration: 1, frequency: 5 });
|
|
2058
|
+
* player.screen.statusIcon("dmg_cold", "show", [0, 160, 255]);
|
|
2059
|
+
* ```
|
|
2060
|
+
*
|
|
2061
|
+
* Times are in seconds.
|
|
2062
|
+
*
|
|
2063
|
+
* Pawn: `ScreenFade`, `ScreenShake`, `StatusIcon`, ...
|
|
2064
|
+
*/
|
|
2065
|
+
export class Screen {
|
|
2066
|
+
constructor(private id: i32) {}
|
|
2067
|
+
|
|
2068
|
+
private begin(name: string, reliable: bool = true): void {
|
|
2069
|
+
message_begin(reliable ? MSG_ONE : MSG_ONE_UNRELIABLE, get_user_msgid(name), [0, 0, 0], this.id);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
/**
|
|
2073
|
+
* Colours the player's screen, fading in or out.
|
|
2074
|
+
*
|
|
2075
|
+
* Pawn: `ScreenFade`
|
|
2076
|
+
*/
|
|
2077
|
+
fade(options: FadeOptions = {}): void {
|
|
2078
|
+
const color = options.color ?? [0, 0, 0, 255];
|
|
2079
|
+
let flags = options.direction == "out" ? FFADE_OUT : FFADE_IN;
|
|
2080
|
+
if (options.modulate) flags |= FFADE_MODULATE;
|
|
2081
|
+
if (options.stay) flags |= FFADE_STAYOUT;
|
|
2082
|
+
|
|
2083
|
+
this.begin("ScreenFade");
|
|
2084
|
+
write_short(fixed(options.duration ?? 1.0, 4096.0));
|
|
2085
|
+
write_short(fixed(options.hold ?? 0.0, 4096.0));
|
|
2086
|
+
write_short(flags);
|
|
2087
|
+
for (let i = 0; i < 4; i++) write_byte(i < color.length ? <i32>color[i] : 255);
|
|
2088
|
+
message_end();
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
/**
|
|
2092
|
+
* Shakes the player's view.
|
|
2093
|
+
*
|
|
2094
|
+
* Pawn: `ScreenShake`
|
|
2095
|
+
*/
|
|
2096
|
+
shake(options: ShakeOptions = {}): void {
|
|
2097
|
+
this.begin("ScreenShake");
|
|
2098
|
+
write_short(fixed(options.amplitude ?? 4.0, 4096.0));
|
|
2099
|
+
write_short(fixed(options.duration ?? 1.0, 4096.0));
|
|
2100
|
+
write_short(fixed(options.frequency ?? 5.0, 256.0));
|
|
2101
|
+
message_end();
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
/**
|
|
2105
|
+
* Shows, flashes or hides a status icon by its sprite name (`"dmg_cold"`,
|
|
2106
|
+
* `"buyzone"`, `"c4"`, ...), in a colour.
|
|
2107
|
+
*
|
|
2108
|
+
* Pawn: `StatusIcon`
|
|
2109
|
+
*/
|
|
2110
|
+
statusIcon(sprite: string, state: StatusIconState, color: number[] = [0, 160, 0]): void {
|
|
2111
|
+
const status = state == "show" ? 1 : state == "flash" ? 2 : 0;
|
|
2112
|
+
this.begin("StatusIcon");
|
|
2113
|
+
write_byte(status);
|
|
2114
|
+
write_string(sprite);
|
|
2115
|
+
if (status != 0) {
|
|
2116
|
+
for (let i = 0; i < 3; i++) write_byte(i < color.length ? <i32>color[i] : 0);
|
|
2117
|
+
}
|
|
2118
|
+
message_end();
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
/**
|
|
2122
|
+
* Sets the round clock at the top of the player's HUD, in seconds. Sent
|
|
2123
|
+
* unreliably, as the game does: a client with a lagging connection may skip
|
|
2124
|
+
* it.
|
|
2125
|
+
*
|
|
2126
|
+
* Pawn: `RoundTime`
|
|
2127
|
+
*/
|
|
2128
|
+
roundTime(seconds: number): void {
|
|
2129
|
+
this.begin("RoundTime", false);
|
|
2130
|
+
write_short(<i32>seconds);
|
|
2131
|
+
message_end();
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
/**
|
|
2135
|
+
* Hides parts of the player's HUD right now. Setting `player.hideHud` does
|
|
2136
|
+
* the same a frame later; this is for when that is too late.
|
|
2137
|
+
*
|
|
2138
|
+
* Pawn: `HideWeapon`
|
|
2139
|
+
*/
|
|
2140
|
+
hideHud(parts: HideHud[]): void {
|
|
2141
|
+
this.begin("HideWeapon");
|
|
2142
|
+
write_byte(HIDE_HUD.maskOf(parts));
|
|
2143
|
+
message_end();
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/**
|
|
2147
|
+
* Shows or hides Counter-Strike's own crosshair on the player's screen.
|
|
2148
|
+
*
|
|
2149
|
+
* Pawn: `Crosshair`
|
|
2150
|
+
*/
|
|
2151
|
+
crosshair(shown: boolean): void {
|
|
2152
|
+
this.begin("Crosshair");
|
|
2153
|
+
write_byte(shown ? 1 : 0);
|
|
2154
|
+
message_end();
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* Sets the flashlight icon on the player's HUD: on or off, and the battery in
|
|
2159
|
+
* percent.
|
|
2160
|
+
*
|
|
2161
|
+
* Pawn: `Flashlight`
|
|
2162
|
+
*/
|
|
2163
|
+
flashlight(on: boolean, battery: number = 100): void {
|
|
2164
|
+
this.begin("Flashlight");
|
|
2165
|
+
write_byte(on ? 1 : 0);
|
|
2166
|
+
write_byte(<i32>battery);
|
|
2167
|
+
message_end();
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/**
|
|
2171
|
+
* Shows the progress bar in the middle of the player's screen, filling up
|
|
2172
|
+
* over `seconds`; `0` hides it.
|
|
2173
|
+
*
|
|
2174
|
+
* Pawn: `BarTime`, `rg_send_bartime`
|
|
2175
|
+
*/
|
|
2176
|
+
progressBar(seconds: number): void {
|
|
2177
|
+
this.begin("BarTime");
|
|
2178
|
+
write_short(fixed(seconds, 1.0));
|
|
2179
|
+
message_end();
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* The channel a sound plays on, one of `"auto"` (the default), `"weapon"`,
|
|
2185
|
+
* `"voice"`, `"item"`, `"body"`, `"stream"`, `"static"`. A new sound on an
|
|
2186
|
+
* entity's channel cuts the one playing there; `"auto"` never cuts.
|
|
2187
|
+
*/
|
|
2188
|
+
export type SoundChannel = "auto" | "weapon" | "voice" | "item" | "body" | "stream" | "static";
|
|
2189
|
+
|
|
2190
|
+
/** The options of `entity.emitSound`; every one has a default. */
|
|
2191
|
+
export interface SoundOptions {
|
|
2192
|
+
/** The entity's channel the sound plays on; `"auto"` by default. */
|
|
2193
|
+
channel?: SoundChannel;
|
|
2194
|
+
/** The volume, `0` to `1`; `1` by default. */
|
|
2195
|
+
volume?: number;
|
|
2196
|
+
/** The sound's fall-off with distance: `0` is heard across the map, `0.8` (the default) as a footstep, `2` only close by. */
|
|
2197
|
+
attenuation?: number;
|
|
2198
|
+
/** The pitch in percent: `100` (the default) as recorded, `50` an octave lower, up to `255`. */
|
|
2199
|
+
pitch?: number;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// A precached file's index lives in the module's store of player fields, in
|
|
2203
|
+
// slot 0, which no player has: the store lasts as long as the map - as a
|
|
2204
|
+
// precache does - and keeps what it holds through amxts_reload, when a
|
|
2205
|
+
// plugin comes back after the map has loaded and can precache nothing.
|
|
2206
|
+
// @ts-ignore: decorator
|
|
2207
|
+
@external("env", "player_data_get") declare function _storedIndex(slot: i32, key: string): f64;
|
|
2208
|
+
// @ts-ignore: decorator
|
|
2209
|
+
@external("env", "player_data_set") declare function _storeIndex(slot: i32, key: string, index: f64): void;
|
|
2210
|
+
|
|
2211
|
+
/**
|
|
2212
|
+
* A file the game has precached - a sprite, a model, a sound - as
|
|
2213
|
+
* `server.precache` returns it. An effect takes it where it draws a sprite or
|
|
2214
|
+
* a model:
|
|
2215
|
+
*
|
|
2216
|
+
* ```ts
|
|
2217
|
+
* const shock = server.precache("sprites/shockwave.spr");
|
|
2218
|
+
* effects.beamCylinder({ at: here, radius: 385, sprite: shock, life: 0.4, width: 60 });
|
|
2219
|
+
* ```
|
|
2220
|
+
*/
|
|
2221
|
+
export class Resource {
|
|
2222
|
+
constructor(
|
|
2223
|
+
/** The file's path, as `server.precache` was given it, e.g. `"sprites/shockwave.spr"`. */
|
|
2224
|
+
public readonly path: string
|
|
2225
|
+
) {}
|
|
2226
|
+
|
|
2227
|
+
/**
|
|
2228
|
+
* The file's index in the game's precache list, as a native takes it; `0`
|
|
2229
|
+
* while it is not precached.
|
|
2230
|
+
*/
|
|
2231
|
+
get index(): number {
|
|
2232
|
+
return _storedIndex(0, `precache:${this.path}`);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// Files asked for before the map loads - at the top level of a plugin - and
|
|
2237
|
+
// precached when it does.
|
|
2238
|
+
const waitingPrecaches: string[] = [];
|
|
2239
|
+
let precacheOpen = false;
|
|
2240
|
+
|
|
2241
|
+
function precacheNow(path: string): void {
|
|
2242
|
+
const lower = path.toLowerCase();
|
|
2243
|
+
const index = lower.endsWith(".wav") ? precache_sound(path)
|
|
2244
|
+
: lower.endsWith(".mdl") || lower.endsWith(".spr") ? precache_model(path)
|
|
2245
|
+
: precache_generic(path);
|
|
2246
|
+
_storeIndex(0, `precache:${path}`, index);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function precacheWaiting(event: PluginPrecacheEvent): void {
|
|
2250
|
+
precacheOpen = true;
|
|
2251
|
+
for (let i = 0; i < waitingPrecaches.length; i++) precacheNow(waitingPrecaches[i]);
|
|
2252
|
+
waitingPrecaches.length = 0;
|
|
2253
|
+
}
|
|
2254
|
+
addServerListener<PluginPrecacheEvent>(precacheWaiting);
|
|
2255
|
+
|
|
2256
|
+
// ScreenFade's flags (hlsdk shake.h), which no include carries.
|
|
2257
|
+
const FFADE_IN = 0x0000;
|
|
2258
|
+
const FFADE_OUT = 0x0001;
|
|
2259
|
+
const FFADE_MODULATE = 0x0002;
|
|
2260
|
+
const FFADE_STAYOUT = 0x0004;
|
|
2261
|
+
|
|
2262
|
+
// ---------------------------------------------------------------- client messages
|
|
2263
|
+
|
|
2264
|
+
/**
|
|
2265
|
+
* The arguments of a message, by their place, `0` for the first: a number
|
|
2266
|
+
* or a text, as the message wrote it.
|
|
2267
|
+
*
|
|
2268
|
+
* ```ts
|
|
2269
|
+
* server.addEventListener("message:TeamScore", (event) => {
|
|
2270
|
+
* console.log(`${event.args.text(0)} ${event.args.number(1)}`);
|
|
2271
|
+
* });
|
|
2272
|
+
* ```
|
|
2273
|
+
*
|
|
2274
|
+
* Pawn: `get_msg_args`, `get_msg_arg_*`, `set_msg_arg_*`
|
|
2275
|
+
*/
|
|
2276
|
+
export class MessageArgs {
|
|
2277
|
+
/** The number of arguments. */
|
|
2278
|
+
get length(): number {
|
|
2279
|
+
return get_msg_args();
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
/** Whether the argument at `index` is text; otherwise it is a number. */
|
|
2283
|
+
isText(index: number): boolean {
|
|
2284
|
+
return get_msg_argtype(<i32>index + 1) == ARG_STRING;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/** The argument at `index` as a number: a byte, a short, a coordinate, an angle. */
|
|
2288
|
+
number(index: number): number {
|
|
2289
|
+
const arg = <i32>index + 1;
|
|
2290
|
+
const type = get_msg_argtype(arg);
|
|
2291
|
+
return type == ARG_COORD || type == ARG_ANGLE ? get_msg_arg_float(arg) : get_msg_arg_int(arg);
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
/** The argument at `index` as text. */
|
|
2295
|
+
text(index: number): string {
|
|
2296
|
+
return get_msg_arg_string(<i32>index + 1);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
/** Writes a number argument: the message goes out with it. */
|
|
2300
|
+
setNumber(index: number, value: number): void {
|
|
2301
|
+
const arg = <i32>index + 1;
|
|
2302
|
+
const type = get_msg_argtype(arg);
|
|
2303
|
+
if (type == ARG_COORD || type == ARG_ANGLE) set_msg_arg_float(arg, type, value);
|
|
2304
|
+
else set_msg_arg_int(arg, type, value);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/** Writes a text argument: the message goes out with it. */
|
|
2308
|
+
setText(index: number, value: string): void {
|
|
2309
|
+
set_msg_arg_string(<i32>index + 1, value);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
/**
|
|
2314
|
+
* A message the server sends its clients - a chat line, the round clock, a
|
|
2315
|
+
* HUD icon - heard on its way, before it leaves:
|
|
2316
|
+
*
|
|
2317
|
+
* ```ts
|
|
2318
|
+
* server.addEventListener("message:TextMsg", (event) => {
|
|
2319
|
+
* if (event.text == "#Round_Draw") event.preventDefault();
|
|
2320
|
+
* });
|
|
2321
|
+
* ```
|
|
2322
|
+
*
|
|
2323
|
+
* A message the editor knows has typed fields (`event.text`); every one has
|
|
2324
|
+
* its arguments in `event.args`. Writing a field changes what the client
|
|
2325
|
+
* gets.
|
|
2326
|
+
*
|
|
2327
|
+
* Pawn: `register_message`
|
|
2328
|
+
*/
|
|
2329
|
+
export class ClientMessage {
|
|
2330
|
+
/** @hidden What a message's event is told apart by, at compile time. */
|
|
2331
|
+
__message: bool = true;
|
|
2332
|
+
/** @hidden The player it goes to: the message's msg_entity. */
|
|
2333
|
+
__receiver: i32 = 0;
|
|
2334
|
+
|
|
2335
|
+
/** The message's name, e.g. `"TextMsg"`. */
|
|
2336
|
+
name: string = "";
|
|
2337
|
+
|
|
2338
|
+
/** The player the message goes to; `null` for a message to everyone. */
|
|
2339
|
+
get player(): Player | null {
|
|
2340
|
+
return this.__receiver >= 1 && this.__receiver <= get_maxplayers() ? new Player(this.__receiver) : null;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
/** The message's arguments, by their place: `event.args.text(1)`. */
|
|
2344
|
+
get args(): MessageArgs {
|
|
2345
|
+
return new MessageArgs();
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
/**
|
|
2349
|
+
* Stops the message: the client does not get it.
|
|
2350
|
+
*
|
|
2351
|
+
* Pawn: `return PLUGIN_HANDLED`
|
|
2352
|
+
*/
|
|
2353
|
+
preventDefault(): void {
|
|
2354
|
+
handled();
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
/** @hidden An argument by its number, 1 for the first, as a number. */
|
|
2358
|
+
protected __number(arg: i32): f64 {
|
|
2359
|
+
return this.args.number(arg - 1);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
/** @hidden */
|
|
2363
|
+
protected __setNumber(arg: i32, value: f64): void {
|
|
2364
|
+
this.args.setNumber(arg - 1, value);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
/** @hidden */
|
|
2368
|
+
protected __text(arg: i32): string {
|
|
2369
|
+
return get_msg_arg_string(arg);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
/** @hidden */
|
|
2373
|
+
protected __setText(arg: i32, value: string): void {
|
|
2374
|
+
set_msg_arg_string(arg, value);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
/** @hidden A player's number argument; `0` or past the players is none. */
|
|
2378
|
+
protected __player(arg: i32): Player | null {
|
|
2379
|
+
const id = get_msg_arg_int(arg);
|
|
2380
|
+
return id >= 1 && id <= get_maxplayers() ? new Player(id) : null;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// One register_message per message name, its listeners behind it: the engine
|
|
2385
|
+
// hands the plugin only the messages it listens to. Registered when the
|
|
2386
|
+
// server is up - a message's id is known from plugin_init - and a reload
|
|
2387
|
+
// takes the same public back.
|
|
2388
|
+
class MessageChannel {
|
|
2389
|
+
listeners: ((event: ClientMessage) => void)[] = [];
|
|
2390
|
+
constructor(public name: string, public make: () => ClientMessage) {}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
const messageChannels: MessageChannel[] = [];
|
|
2394
|
+
const waitingMessages: MessageChannel[] = [];
|
|
2395
|
+
|
|
2396
|
+
function messageChannel(name: string): MessageChannel | null {
|
|
2397
|
+
for (let i = 0; i < messageChannels.length; i++) {
|
|
2398
|
+
if (messageChannels[i].name == name) return messageChannels[i];
|
|
2399
|
+
}
|
|
2400
|
+
return null;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function addMessageListener<E>(name: string, listener: (event: E) => void): void {
|
|
2404
|
+
let channel = messageChannel(name);
|
|
2405
|
+
|
|
2406
|
+
if (channel == null) {
|
|
2407
|
+
channel = new MessageChannel(name, (): ClientMessage => changetype<ClientMessage>(instantiate<E>()));
|
|
2408
|
+
messageChannels.push(channel);
|
|
2409
|
+
if (serverUp) registerMessage(channel);
|
|
2410
|
+
else waitingMessages.push(channel);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
channel.listeners.push(changetype<(event: ClientMessage) => void>(listener));
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function removeMessageListener<E>(name: string, listener: (event: E) => void): void {
|
|
2417
|
+
const channel = messageChannel(name);
|
|
2418
|
+
if (channel == null) return;
|
|
2419
|
+
const at = channel.listeners.indexOf(changetype<(event: ClientMessage) => void>(listener));
|
|
2420
|
+
if (at >= 0) channel.listeners.splice(at, 1);
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function registerMessage(channel: MessageChannel): void {
|
|
2424
|
+
const id = get_user_msgid(channel.name);
|
|
2425
|
+
|
|
2426
|
+
if (id == 0) {
|
|
2427
|
+
console.error(`message:${channel.name} - the game has no message by that name`);
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
const fired = (message: number, dest: number, receiver: number, d: number): void => messageFired(channel, <i32>receiver);
|
|
2432
|
+
const pub = publicFor(fired, `msg:${channel.name}`);
|
|
2433
|
+
if (pub.length > 0) register_message(id, pub);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
/** The public register_message calls: (message, dest, receiver). */
|
|
2437
|
+
function messageFired(channel: MessageChannel, receiver: i32): void {
|
|
2438
|
+
const event = channel.make();
|
|
2439
|
+
event.name = channel.name;
|
|
2440
|
+
event.__receiver = receiver;
|
|
2441
|
+
// A copy: a listener that removes itself must not make the next one skip.
|
|
2442
|
+
const listeners = channel.listeners.slice(0);
|
|
2443
|
+
for (let i = 0; i < listeners.length; i++) listeners[i](event);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// ---------------------------------------------------------------- player fields changing
|
|
2447
|
+
|
|
2448
|
+
// @ts-ignore: decorator
|
|
2449
|
+
@external("env", "player_change_listen") declare function _playerChangeListen(field: string, fn: i32): void;
|
|
2450
|
+
// @ts-ignore: decorator
|
|
2451
|
+
@external("env", "player_change_get") declare function _playerChangeGet(which: i32): f64;
|
|
2452
|
+
// @ts-ignore: decorator
|
|
2453
|
+
@external("env", "player_change_get_text") declare function _playerChangeGetText(which: i32, out: usize, max: i32): i32;
|
|
2454
|
+
|
|
2455
|
+
/**
|
|
2456
|
+
* A field plugins added to `Player` changed on a player - written by any
|
|
2457
|
+
* plugin, TypeScript or Pawn:
|
|
2458
|
+
*
|
|
2459
|
+
* ```ts
|
|
2460
|
+
* server.addEventListener("playerchange", (event) => {
|
|
2461
|
+
* print(event.player, event.value ? "You are protected" : "Your spawn protection is over");
|
|
2462
|
+
* }, { field: "spawnProtected" });
|
|
2463
|
+
* ```
|
|
2464
|
+
*
|
|
2465
|
+
* With `field`, `event.value` and `event.previous` have the field's type; a
|
|
2466
|
+
* named listener takes `PlayerChangeEvent<"spawnProtected">`. Without it
|
|
2467
|
+
* every field is heard, and `event.field` says which.
|
|
2468
|
+
*/
|
|
2469
|
+
export class PlayerChangeEvent<F extends string = string> {
|
|
2470
|
+
/** @hidden What a change's event is told apart by, at compile time. */
|
|
2471
|
+
__playerChange: bool = true;
|
|
2472
|
+
/** @hidden The player's slot. */
|
|
2473
|
+
__slot: i32 = 0;
|
|
2474
|
+
/** @hidden The value before the change, as the module keeps it: a number, or a text. */
|
|
2475
|
+
__previousNumber: f64 = 0;
|
|
2476
|
+
/** @hidden */
|
|
2477
|
+
__previousText: string = "";
|
|
2478
|
+
/** @hidden The value after it. */
|
|
2479
|
+
__number: f64 = 0;
|
|
2480
|
+
/** @hidden */
|
|
2481
|
+
__text: string = "";
|
|
2482
|
+
|
|
2483
|
+
/** The field that changed, e.g. `"spawnProtected"`; a member of an object field is dotted, `"glow.enabled"`. */
|
|
2484
|
+
field: string = "";
|
|
2485
|
+
|
|
2486
|
+
/** The player whose field changed. */
|
|
2487
|
+
get player(): Player {
|
|
2488
|
+
return new Player(this.__slot);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
/** The third argument of `server.addEventListener`. */
|
|
2493
|
+
export interface ServerListenerOptions {
|
|
2494
|
+
/**
|
|
2495
|
+
* For `"playerchange"`: the field listened for, e.g. `"spawnProtected"`, or
|
|
2496
|
+
* an object field's member, `"glow.enabled"`; an object field's name
|
|
2497
|
+
* hears each of its members. Left out, every field.
|
|
2498
|
+
*/
|
|
2499
|
+
field?: string;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// This plugin's playerchange listeners. The module wakes the plugin only for
|
|
2503
|
+
// the fields they are for (player_change_listen), and the trampoline hands a
|
|
2504
|
+
// change to the ones it concerns, each as its own event class - the field's
|
|
2505
|
+
// typed one, which the build makes (scripts/player-fields.ts).
|
|
2506
|
+
class PlayerChangeListener {
|
|
2507
|
+
constructor(
|
|
2508
|
+
public field: string,
|
|
2509
|
+
public listener: (event: PlayerChangeEvent) => void,
|
|
2510
|
+
public make: () => PlayerChangeEvent
|
|
2511
|
+
) {}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
const playerChangeListeners: PlayerChangeListener[] = [];
|
|
2515
|
+
const heardFields: string[] = [];
|
|
2516
|
+
|
|
2517
|
+
/** Whether a listener for `field` hears `key`: the field itself, a member of it, or "" for every one. */
|
|
2518
|
+
function hears(field: string, key: string): bool {
|
|
2519
|
+
return field.length == 0 || key == field || (key.startsWith(field) && key.charCodeAt(field.length) == 46);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
function addPlayerChangeListener<E>(field: string, listener: (event: E) => void): void {
|
|
2523
|
+
const make = (): PlayerChangeEvent => changetype<PlayerChangeEvent>(instantiate<E>());
|
|
2524
|
+
playerChangeListeners.push(new PlayerChangeListener(field, changetype<(event: PlayerChangeEvent) => void>(listener), make));
|
|
2525
|
+
if (heardFields.includes(field)) return;
|
|
2526
|
+
heardFields.push(field);
|
|
2527
|
+
_playerChangeListen(field, playerChanged.index);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
function removePlayerChangeListener<E>(field: string, listener: (event: E) => void): void {
|
|
2531
|
+
const fn = changetype<(event: PlayerChangeEvent) => void>(listener);
|
|
2532
|
+
for (let i = 0; i < playerChangeListeners.length; i++) {
|
|
2533
|
+
const one = playerChangeListeners[i];
|
|
2534
|
+
if (one.field != field || one.listener != fn) continue;
|
|
2535
|
+
playerChangeListeners.splice(i, 1);
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
/** The change's key (0) or its text before (1) or after (2). */
|
|
2541
|
+
function changeText(which: i32): string {
|
|
2542
|
+
const length = _playerChangeGetText(which, 0, 0);
|
|
2543
|
+
if (length <= 0) return "";
|
|
2544
|
+
const bytes = new ArrayBuffer(length);
|
|
2545
|
+
_playerChangeGetText(which, changetype<usize>(bytes), length);
|
|
2546
|
+
return String.UTF8.decode(bytes);
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
/** What the module calls when a field this plugin listens for changes on player `slot`. */
|
|
2550
|
+
function playerChanged(slot: i32): void {
|
|
2551
|
+
const key = changeText(0);
|
|
2552
|
+
const previousNumber = _playerChangeGet(1);
|
|
2553
|
+
const previousText = changeText(1);
|
|
2554
|
+
const number = _playerChangeGet(2);
|
|
2555
|
+
const text = changeText(2);
|
|
2556
|
+
|
|
2557
|
+
// A copy: a listener that removes itself must not make the next one skip.
|
|
2558
|
+
const listeners = playerChangeListeners.slice(0);
|
|
2559
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
2560
|
+
const one = listeners[i];
|
|
2561
|
+
if (!hears(one.field, key)) continue;
|
|
2562
|
+
const event = one.make();
|
|
2563
|
+
event.__slot = slot;
|
|
2564
|
+
event.field = key;
|
|
2565
|
+
event.__previousNumber = previousNumber;
|
|
2566
|
+
event.__previousText = previousText;
|
|
2567
|
+
event.__number = number;
|
|
2568
|
+
event.__text = text;
|
|
2569
|
+
one.listener(event);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
// ---------------------------------------------------------------- cvars
|
|
2574
|
+
|
|
2575
|
+
/** The event a cvar's change listener receives: the cvar, its old and its new value. */
|
|
2576
|
+
export class CvarChangeEvent {
|
|
2577
|
+
constructor(
|
|
2578
|
+
/** The cvar that changed. */
|
|
2579
|
+
public cvar: Cvar,
|
|
2580
|
+
/** The cvar's value before the change, as text. */
|
|
2581
|
+
public oldValue: string,
|
|
2582
|
+
/** The cvar's new value, as text. */
|
|
2583
|
+
public value: string
|
|
2584
|
+
) {}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
/** A cvar's change listener: `(event) => ...`, with the old and the new value in `event`. */
|
|
2588
|
+
export type CvarListener = (event: CvarChangeEvent) => void;
|
|
2589
|
+
|
|
2590
|
+
// Every Cvar with a change listener, for the one trampoline that hears them.
|
|
2591
|
+
const watchedCvars: Cvar[] = [];
|
|
2592
|
+
|
|
2593
|
+
// Cvars made before plugin_init, attached when it comes. The facade's own init
|
|
2594
|
+
// listener is added before any plugin's, so a plugin's init sees them ready.
|
|
2595
|
+
const waitingCvars: Cvar[] = [];
|
|
2596
|
+
let serverUp = false;
|
|
2597
|
+
|
|
2598
|
+
// A Cvar may be made from here on: plugin_natives is over.
|
|
2599
|
+
let cvarsReady = false;
|
|
2600
|
+
|
|
2601
|
+
function attachWaitingCvars(event: PluginInitEvent): void {
|
|
2602
|
+
serverUp = true;
|
|
2603
|
+
// No precache came - the plugin was reloaded mid-map: what it asked for
|
|
2604
|
+
// is found in the store, or was never precached.
|
|
2605
|
+
precacheOpen = false;
|
|
2606
|
+
waitingPrecaches.length = 0;
|
|
2607
|
+
makeWaitingCvars();
|
|
2608
|
+
for (let i = 0; i < waitingServerCommands.length; i++) registerServerCommand(waitingServerCommands[i]);
|
|
2609
|
+
waitingServerCommands.length = 0;
|
|
2610
|
+
for (let i = 0; i < waitingTouches.length; i++) registerTouch(waitingTouches[i]);
|
|
2611
|
+
waitingTouches.length = 0;
|
|
2612
|
+
for (let i = 0; i < waitingHams.length; i++) registerHam(waitingHams[i]);
|
|
2613
|
+
waitingHams.length = 0;
|
|
2614
|
+
for (let i = 0; i < waitingMessages.length; i++) registerMessage(waitingMessages[i]);
|
|
2615
|
+
waitingMessages.length = 0;
|
|
2616
|
+
}
|
|
2617
|
+
addServerListener<PluginInitEvent>(attachWaitingCvars);
|
|
2618
|
+
|
|
2619
|
+
|
|
2620
|
+
function makeWaitingCvars(): void {
|
|
2621
|
+
cvarsReady = true;
|
|
2622
|
+
for (let i = 0; i < waitingCvars.length; i++) waitingCvars[i].attach();
|
|
2623
|
+
waitingCvars.length = 0;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
/**
|
|
2627
|
+
* @hidden The start of every exported native. A Pawn plugin calls one from
|
|
2628
|
+
* its plugin_init at the earliest - often before the host's own - and by then
|
|
2629
|
+
* plugin_natives is over, so a Cvar the native makes (a register_cvar native
|
|
2630
|
+
* of the plugin's) is made at once rather than when this plugin's init comes.
|
|
2631
|
+
*/
|
|
2632
|
+
export function __nativeCall(): void {
|
|
2633
|
+
if (!cvarsReady) makeWaitingCvars();
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function cvarChanged(pointer: number, oldText: number, newText: number, unused: number): void {
|
|
2637
|
+
for (let i = 0; i < watchedCvars.length; i++) {
|
|
2638
|
+
const cvar = watchedCvars[i];
|
|
2639
|
+
if (cvar.pointer != <i32>pointer) continue;
|
|
2640
|
+
cvar.dispatch(new CvarChangeEvent(cvar, argText(1), argText(2)));
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
/**
|
|
2646
|
+
* A server cvar, read and written like an input's `value`:
|
|
2647
|
+
*
|
|
2648
|
+
* ```ts
|
|
2649
|
+
* const freeze = new Cvar("mp_freezetime");
|
|
2650
|
+
* freeze.number = 5;
|
|
2651
|
+
* const speed = new Cvar("my_speed", "250"); // made with 250 if it does not exist
|
|
2652
|
+
* speed.addEventListener("change", (event) => console.log(`${event.oldValue} -> ${event.value}`));
|
|
2653
|
+
* ```
|
|
2654
|
+
*
|
|
2655
|
+
* `value` is the cvar's text; `number` and `boolean` read and write the same
|
|
2656
|
+
* cvar as a number and as an on/off switch.
|
|
2657
|
+
*
|
|
2658
|
+
* Pawn: `get_cvar_pointer`, `create_cvar`, `get_pcvar_string`, `set_pcvar_num`, `hook_cvar_change`
|
|
2659
|
+
*/
|
|
2660
|
+
export class Cvar {
|
|
2661
|
+
/**
|
|
2662
|
+
* The cvar's handle in the engine; `0` when the server has no such cvar.
|
|
2663
|
+
*
|
|
2664
|
+
* Pawn: `get_cvar_pointer`
|
|
2665
|
+
*/
|
|
2666
|
+
pointer: i32 = 0;
|
|
2667
|
+
private listeners: CvarListener[] = [];
|
|
2668
|
+
private hooked: bool = false;
|
|
2669
|
+
|
|
2670
|
+
constructor(
|
|
2671
|
+
/** The cvar's name, as the console knows it, e.g. `"mp_timelimit"`. */
|
|
2672
|
+
public name: string,
|
|
2673
|
+
private defaultValue: string | null = null
|
|
2674
|
+
) {
|
|
2675
|
+
if (cvarsReady) this.attach();
|
|
2676
|
+
else waitingCvars.push(this);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/**
|
|
2680
|
+
* @internal Finds or creates the cvar and starts hearing its changes. A Cvar
|
|
2681
|
+
* made at the top level of a plugin waits for `plugin_init`: creating a cvar
|
|
2682
|
+
* while plugins are still loading takes the server down.
|
|
2683
|
+
*/
|
|
2684
|
+
attach(): void {
|
|
2685
|
+
let pointer = get_cvar_pointer(this.name);
|
|
2686
|
+
const fallback = this.defaultValue;
|
|
2687
|
+
if (pointer == 0 && fallback != null) pointer = create_cvar(this.name, fallback);
|
|
2688
|
+
this.pointer = pointer;
|
|
2689
|
+
if (this.listeners.length > 0) this.hook();
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
private hook(): void {
|
|
2693
|
+
if (this.hooked || this.pointer == 0) return;
|
|
2694
|
+
this.hooked = true;
|
|
2695
|
+
watchedCvars.push(this);
|
|
2696
|
+
const pub = publicFor(cvarChanged, "cvarchange:" + this.name);
|
|
2697
|
+
if (pub.length > 0) hook_cvar_change(this.pointer, pub);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
/** `true` when the server has this cvar. */
|
|
2701
|
+
get exists(): bool { return this.pointer != 0; }
|
|
2702
|
+
|
|
2703
|
+
/** The cvar's value as text, e.g. `"250"`. */
|
|
2704
|
+
get value(): string { return this.pointer != 0 ? get_pcvar_string(this.pointer) : ""; }
|
|
2705
|
+
set value(text: string) { if (this.pointer != 0) set_pcvar_string(this.pointer, text); }
|
|
2706
|
+
|
|
2707
|
+
/** The cvar's value as a number. A whole number is written without a fraction: `"5"`, not `"5.000000"`. */
|
|
2708
|
+
get number(): number { return this.pointer != 0 ? get_pcvar_float(this.pointer) : 0; }
|
|
2709
|
+
set number(value: number) {
|
|
2710
|
+
if (this.pointer == 0) return;
|
|
2711
|
+
if (value == Math.floor(value)) set_pcvar_num(this.pointer, <i32>value);
|
|
2712
|
+
else set_pcvar_float(this.pointer, value);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
/** The cvar as an on/off switch: `true` for anything but `0`. Writing `true` sets `1`, `false` sets `0`. */
|
|
2716
|
+
get boolean(): bool { return this.pointer != 0 && get_pcvar_num(this.pointer) != 0; }
|
|
2717
|
+
set boolean(on: bool) { if (this.pointer != 0) set_pcvar_num(this.pointer, on ? 1 : 0); }
|
|
2718
|
+
|
|
2719
|
+
/** Calls `listener` whenever the cvar's value changes. */
|
|
2720
|
+
addEventListener(type: "change", listener: CvarListener): void {
|
|
2721
|
+
this.listeners.push(listener);
|
|
2722
|
+
this.hook();
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
/** Stops calling a listener added with `addEventListener`. */
|
|
2726
|
+
removeEventListener(type: "change", listener: CvarListener): void {
|
|
2727
|
+
const at = this.listeners.indexOf(listener);
|
|
2728
|
+
if (at >= 0) this.listeners.splice(at, 1);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
/** @internal Calls the change listeners; the server does it when the cvar changes. A plugin listens with `addEventListener`. */
|
|
2732
|
+
dispatch(event: CvarChangeEvent): void {
|
|
2733
|
+
const listeners = this.listeners.slice(0);
|
|
2734
|
+
for (let i = 0; i < listeners.length; i++) listeners[i](event);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
/**
|
|
2739
|
+
* The server, an event target like the DOM's: its events, commands, map and
|
|
2740
|
+
* the folders AMX Mod X keeps. Used through `server`:
|
|
2741
|
+
*
|
|
2742
|
+
* ```ts
|
|
2743
|
+
* server.addEventListener("putinserver", (event) => {
|
|
2744
|
+
* print(event.player, "Welcome!"); // event is a PutinserverEvent
|
|
2745
|
+
* });
|
|
2746
|
+
* server.map; // "de_dust2"
|
|
2747
|
+
* server.maxPlayers; // 32
|
|
2748
|
+
* server.command("echo hi");
|
|
2749
|
+
* ```
|
|
2750
|
+
*
|
|
2751
|
+
* The event's name is written out as a string: the editor completes it and
|
|
2752
|
+
* hands the listener the event's own type. A listener may use the variables
|
|
2753
|
+
* of the function it is written in - it is a closure, as in JavaScript.
|
|
2754
|
+
*/
|
|
2755
|
+
export class Server {
|
|
2756
|
+
/**
|
|
2757
|
+
* Calls `listener` every time the server raises the event `type` - or,
|
|
2758
|
+
* for `"message:<Name>"`, every time it sends that message to a client.
|
|
2759
|
+
* `"playerchange"` takes the field it is for: `{ field: "spawnProtected" }`.
|
|
2760
|
+
*/
|
|
2761
|
+
addEventListener<K extends keyof ServerEventMap>(type: K, listener: (event: ServerEventMap[K]) => void, options: ServerListenerOptions = {}): void {
|
|
2762
|
+
// @ts-ignore: a message's event is told apart by its field, at compile time
|
|
2763
|
+
if (isDefined(changetype<ServerEventMap[K]>(0).__message)) addMessageListener<ServerEventMap[K]>(type.slice(8), listener);
|
|
2764
|
+
// @ts-ignore: and so is a field's change
|
|
2765
|
+
else if (isDefined(changetype<ServerEventMap[K]>(0).__playerChange)) addPlayerChangeListener<ServerEventMap[K]>(options.field ?? "", listener);
|
|
2766
|
+
else addServerListener<ServerEventMap[K]>(listener);
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
/** Stops calling a listener added with `addEventListener` - the same function and the same options. */
|
|
2770
|
+
removeEventListener<K extends keyof ServerEventMap>(type: K, listener: (event: ServerEventMap[K]) => void, options: ServerListenerOptions = {}): void {
|
|
2771
|
+
// @ts-ignore: as in addEventListener
|
|
2772
|
+
if (isDefined(changetype<ServerEventMap[K]>(0).__message)) removeMessageListener<ServerEventMap[K]>(type.slice(8), listener);
|
|
2773
|
+
// @ts-ignore: as in addEventListener
|
|
2774
|
+
else if (isDefined(changetype<ServerEventMap[K]>(0).__playerChange)) removePlayerChangeListener<ServerEventMap[K]>(options.field ?? "", listener);
|
|
2775
|
+
else removeServerListener<ServerEventMap[K]>(listener);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
/**
|
|
2779
|
+
* The current map's name, e.g. `"de_dust2"`.
|
|
2780
|
+
*
|
|
2781
|
+
* Pawn: `get_mapname`
|
|
2782
|
+
*/
|
|
2783
|
+
get map(): string {
|
|
2784
|
+
return get_mapname();
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
/**
|
|
2788
|
+
* The number of player slots on the server, e.g. `32`.
|
|
2789
|
+
*
|
|
2790
|
+
* Pawn: `get_maxplayers`
|
|
2791
|
+
*/
|
|
2792
|
+
get maxPlayers(): number {
|
|
2793
|
+
return get_maxplayers();
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
/**
|
|
2797
|
+
* The AMX Mod X configs folder, relative to the game folder, as `fs` takes it:
|
|
2798
|
+
* `addons/amxmodx/configs` unless the server moved it.
|
|
2799
|
+
*
|
|
2800
|
+
* ```ts
|
|
2801
|
+
* const text = fs.readFileSync(`${server.configsDir}/myplugin.ini`);
|
|
2802
|
+
* ```
|
|
2803
|
+
*
|
|
2804
|
+
* Pawn: `get_configsdir`
|
|
2805
|
+
*/
|
|
2806
|
+
get configsDir(): string {
|
|
2807
|
+
const dir = get_localinfo("amxx_configsdir");
|
|
2808
|
+
return dir.length > 0 ? dir : "addons/amxmodx/configs";
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
/**
|
|
2812
|
+
* The AMX Mod X folder for plugins' data files: `addons/amxmodx/data` unless the server moved it.
|
|
2813
|
+
*
|
|
2814
|
+
* Pawn: `get_datadir`
|
|
2815
|
+
*/
|
|
2816
|
+
get dataDir(): string {
|
|
2817
|
+
const dir = get_localinfo("amxx_datadir");
|
|
2818
|
+
return dir.length > 0 ? dir : "addons/amxmodx/data";
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
/**
|
|
2822
|
+
* Runs a command in the server console, as if typed there:
|
|
2823
|
+
* `server.command("changelevel de_dust2")`. The text goes as it is: a `%` stays a `%`.
|
|
2824
|
+
*
|
|
2825
|
+
* Pawn: `server_cmd`
|
|
2826
|
+
*/
|
|
2827
|
+
command(text: string): void {
|
|
2828
|
+
// server_cmd takes a `...` tail, so it goes through Call; the text is an
|
|
2829
|
+
// argument, not the format, or a `%` in a map name would be a conversion.
|
|
2830
|
+
new Call(NATIVE_server_cmd).str("%s").str(text).run();
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
/**
|
|
2834
|
+
* Adds a command players type, e.g. `"/hp"` in chat, or a name without the slash
|
|
2835
|
+
* in the console.
|
|
2836
|
+
*
|
|
2837
|
+
* ```ts
|
|
2838
|
+
* server.addCommand("/hp", (player) => print(player, `${player.health} HP`));
|
|
2839
|
+
* server.addCommand("/kick", kick, { access: "Kick", description: "Kick a player" });
|
|
2840
|
+
* ```
|
|
2841
|
+
*
|
|
2842
|
+
* `args` are the words after the command. A chat command is not repeated in
|
|
2843
|
+
* chat, and it does not run for a player without the `access` right.
|
|
2844
|
+
* `"say time"` runs when a player writes exactly `"time"` in chat.
|
|
2845
|
+
*
|
|
2846
|
+
* Pawn: `register_clcmd`
|
|
2847
|
+
*/
|
|
2848
|
+
addCommand(name: string, handler: CommandHandler, options: CommandOptions = {}): void {
|
|
2849
|
+
const chat = name.startsWith("/") || name.startsWith("say ");
|
|
2850
|
+
const access = options.access;
|
|
2851
|
+
|
|
2852
|
+
commandNames.push(name.toLowerCase());
|
|
2853
|
+
commandHandlers.push(handler);
|
|
2854
|
+
commandAccess.push(access ?? "");
|
|
2855
|
+
|
|
2856
|
+
if (!chat) {
|
|
2857
|
+
const flags = access != null ? ACCESS.bitOf(access) : 0;
|
|
2858
|
+
_clcmd(name, consoleCommand.index, flags, options.description ?? "", SHAPE_WIDE);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
if (chatHooked) return;
|
|
2863
|
+
chatHooked = true;
|
|
2864
|
+
_clcmd("say", chatCommand.index, 0, "", SHAPE_WIDE);
|
|
2865
|
+
_clcmd("say_team", chatCommand.index, 0, "", SHAPE_WIDE);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
/**
|
|
2869
|
+
* Adds a command of the server console - typed there, sent over rcon or run by
|
|
2870
|
+
* another plugin. Players cannot use it.
|
|
2871
|
+
*
|
|
2872
|
+
* ```ts
|
|
2873
|
+
* server.addServerCommand("myplugin_reset", (args) => reset(args.length > 0 ? args[0] : "all"));
|
|
2874
|
+
* ```
|
|
2875
|
+
*
|
|
2876
|
+
* `args` are the words after the command. It may be added at the top level of
|
|
2877
|
+
* the file.
|
|
2878
|
+
*
|
|
2879
|
+
* Pawn: `register_srvcmd`
|
|
2880
|
+
*/
|
|
2881
|
+
addServerCommand(name: string, handler: ServerCommandHandler): void {
|
|
2882
|
+
const lower = name.toLowerCase();
|
|
2883
|
+
serverCommandNames.push(lower);
|
|
2884
|
+
serverCommandHandlers.push(handler);
|
|
2885
|
+
|
|
2886
|
+
if (serverUp) registerServerCommand(lower);
|
|
2887
|
+
else waitingServerCommands.push(lower);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
/** Shows a HUD message to every player, with the same options as `player.showHud`. */
|
|
2891
|
+
showHud(text: string, options: HudOptions = {}): void {
|
|
2892
|
+
showHudTo(0, text, options);
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
/**
|
|
2896
|
+
* Precaches a file, so the game can use it and players download it:
|
|
2897
|
+
* `const shock = server.precache("sprites/shockwave.spr")`. At the top level
|
|
2898
|
+
* of the file it is precached when the map loads; in the `"precache"` event,
|
|
2899
|
+
* at once. A sound is written as the game plays it, under `sound/`:
|
|
2900
|
+
* `"myplugin/hit.wav"`. Returns the file as a `Resource` - what an effect
|
|
2901
|
+
* takes for a sprite or a model.
|
|
2902
|
+
*
|
|
2903
|
+
* Pawn: `precache_model`, `precache_sound`, `precache_generic`
|
|
2904
|
+
*/
|
|
2905
|
+
precache(path: string): Resource {
|
|
2906
|
+
const resource = new Resource(path);
|
|
2907
|
+
if (precacheOpen) precacheNow(path);
|
|
2908
|
+
else if (!serverUp) waitingPrecaches.push(path);
|
|
2909
|
+
else if (resource.index == 0) console.error(`"${path}" is not precached: precache it at the top level of the plugin or in the "precache" event`);
|
|
2910
|
+
return resource;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
/** The server the plugin runs on: its events, commands and map. */
|
|
2915
|
+
export const server = new Server();
|
|
2916
|
+
|
|
2917
|
+
/**
|
|
2918
|
+
* The game's events (reapi hookchains and Ham Sandwich functions) and round
|
|
2919
|
+
* control, as an event target like the DOM's:
|
|
2920
|
+
*
|
|
2921
|
+
* ```ts
|
|
2922
|
+
* game.addEventListener("takeDamage", (event) => {
|
|
2923
|
+
* if (event.player.isBot) event.preventDefault();
|
|
2924
|
+
* });
|
|
2925
|
+
* game.addEventListener("canPlayerHearPlayer", (event) => event.listener.team == event.sender.team);
|
|
2926
|
+
* game.addEventListener("flPlayerFallDamage", (event) => event.result / 2, true);
|
|
2927
|
+
* ```
|
|
2928
|
+
*
|
|
2929
|
+
* The event's type follows from its name. What a listener returns is the
|
|
2930
|
+
* answer to the game: before the game acts it replaces what the game would
|
|
2931
|
+
* do, after it (`post`) it replaces the result. A listener that returns
|
|
2932
|
+
* nothing leaves it to the game; `event.preventDefault()` blocks without an
|
|
2933
|
+
* answer. A value of the wrong type is an error in the editor and in the build.
|
|
2934
|
+
*
|
|
2935
|
+
* The game rules are its fields: `game.freezePeriod`, `game.numCtWins`,
|
|
2936
|
+
* `game.roundWinner`.
|
|
2937
|
+
*
|
|
2938
|
+
* Pawn: `RegisterHookChain`, `RegisterHam`, `get_member_game`
|
|
2939
|
+
*/
|
|
2940
|
+
export class Game extends GameFields {
|
|
2941
|
+
/**
|
|
2942
|
+
* Calls `listener` every time the game runs `type`. With `true` - or
|
|
2943
|
+
* `{ post: true }` - it runs after the game has acted, with the game's answer
|
|
2944
|
+
* in `event.result`; by default it runs before and can stop it.
|
|
2945
|
+
* `classname` narrows it to one class of entity, and only that class's
|
|
2946
|
+
* reach the plugin: `{ classname: "weapon_knife" }`. A `"touch"` listener
|
|
2947
|
+
* takes the classes it is about instead: `{ toucher: "player", touched: "player" }`.
|
|
2948
|
+
*
|
|
2949
|
+
* Pawn: `RegisterHookChain`, `RegisterHam`, `register_touch`
|
|
2950
|
+
*/
|
|
2951
|
+
addEventListener<K extends keyof GameEventMap, R extends GameAnswerMap[K] | void | Promise<GameAnswerMap[K] | void> = void>(
|
|
2952
|
+
type: K, listener: (event: GameEventMap[K]) => R, options: boolean | GameListenerOptions = {}
|
|
2953
|
+
): void {
|
|
2954
|
+
const given = options as GameListenerOptions;
|
|
2955
|
+
if (idof<GameEventMap[K]>() == idof<TouchEvent>()) {
|
|
2956
|
+
if (!isVoid<R>()) ERROR("a touch listener answers nothing: block the touch with event.preventDefault()");
|
|
2957
|
+
addTouchListener(changetype<TouchListener>(listener), given.toucher ?? "*", given.touched ?? "*");
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
addGameListener<GameEventMap[K], R>(listener, given.post == true, given.classname ?? "");
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
/** Stops calling a listener added with `addEventListener` - the same function and the same options. */
|
|
2964
|
+
removeEventListener<K extends keyof GameEventMap, R extends GameAnswerMap[K] | void | Promise<GameAnswerMap[K] | void> = void>(
|
|
2965
|
+
type: K, listener: (event: GameEventMap[K]) => R, options: boolean | GameListenerOptions = {}
|
|
2966
|
+
): void {
|
|
2967
|
+
const given = options as GameListenerOptions;
|
|
2968
|
+
if (idof<GameEventMap[K]>() == idof<TouchEvent>()) {
|
|
2969
|
+
removeTouchListener(changetype<TouchListener>(listener), given.toucher ?? "*", given.touched ?? "*");
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
removeGameListener<GameEventMap[K], R>(listener, given.post == true, given.classname ?? "");
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
/**
|
|
2976
|
+
* The game's clock: seconds since the map started. Entity fields that hold a
|
|
2977
|
+
* moment - `nextThink`, `damageTime` - are on it:
|
|
2978
|
+
* `grenade.damageTime = game.time + 1`. The attack timers - a weapon's
|
|
2979
|
+
* `nextPrimaryAttack`, a player's `nextAttack` - count from now instead:
|
|
2980
|
+
* `weapon.nextPrimaryAttack = 1` is a second away.
|
|
2981
|
+
*
|
|
2982
|
+
* Pawn: `get_gametime`
|
|
2983
|
+
*/
|
|
2984
|
+
get time(): number {
|
|
2985
|
+
return get_gametime();
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
/**
|
|
2989
|
+
* Ends the round now:
|
|
2990
|
+
*
|
|
2991
|
+
* ```ts
|
|
2992
|
+
* game.endRound({ winner: "TERRORIST" }); // terrorists win, next round in 5 s
|
|
2993
|
+
* game.endRound({ winner: "draw", delay: 3 }); // a draw, next round in 3 s
|
|
2994
|
+
* game.endRound({ winner: "none", message: "" }); // a quiet restart: no message
|
|
2995
|
+
* ```
|
|
2996
|
+
*
|
|
2997
|
+
* The winner sets the score, the message and the sound (`"Terrorists Win!"`);
|
|
2998
|
+
* `message` and `sound` replace them, `""` turns them off.
|
|
2999
|
+
*
|
|
3000
|
+
* Pawn: `rg_round_end`
|
|
3001
|
+
*/
|
|
3002
|
+
endRound(options: EndRoundOptions): void {
|
|
3003
|
+
const status = max(WINNER_NAMES.indexOf(options.winner), 0);
|
|
3004
|
+
rg_round_end(options.delay ?? 5.0, status, ROUND_REASONS[status], options.message ?? "default", options.sound ?? "default", options.dispatch ?? false);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
/**
|
|
3009
|
+
* The third argument of `game.addEventListener`: `true` stands for
|
|
3010
|
+
* `{ post: true }`; a `"touch"` listener names the classes it is about.
|
|
3011
|
+
*/
|
|
3012
|
+
export interface GameListenerOptions {
|
|
3013
|
+
/** Runs the listener after the game has acted, with its answer in `event.result`. */
|
|
3014
|
+
post?: boolean;
|
|
3015
|
+
/** The class of entity the event is listened for on, e.g. `"weapon_knife"`: only its entities reach the listener. */
|
|
3016
|
+
classname?: string;
|
|
3017
|
+
/** For `"touch"`: the class of the entity that moves into the other, e.g. `"player"`; left out, any. */
|
|
3018
|
+
toucher?: string;
|
|
3019
|
+
/** For `"touch"`: the class of the entity touched, e.g. `"func_door"`; left out, any. */
|
|
3020
|
+
touched?: string;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
/**
|
|
3024
|
+
* The way one entity uses another - a button pressed, a door opened - one of
|
|
3025
|
+
* `"off"`, `"on"`, `"set"` or `"toggle"`.
|
|
3026
|
+
*
|
|
3027
|
+
* Pawn: `USE_OFF`, `USE_ON`, `USE_SET`, `USE_TOGGLE`
|
|
3028
|
+
*/
|
|
3029
|
+
export type UseType = "off" | "on" | "set" | "toggle";
|
|
3030
|
+
|
|
3031
|
+
/** The options of an entity's action such as `weapon.deploy()` or `entity.takeHealth(...)`. */
|
|
3032
|
+
export interface ActionOptions {
|
|
3033
|
+
/**
|
|
3034
|
+
* Whether the game's listeners run too - this plugin's and every other's,
|
|
3035
|
+
* Pawn ones included; `true` by default. `false` runs the game's own
|
|
3036
|
+
* function alone.
|
|
3037
|
+
*
|
|
3038
|
+
* Pawn: `ExecuteHamB`, `ExecuteHam`
|
|
3039
|
+
*/
|
|
3040
|
+
hooks?: boolean;
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
/**
|
|
3044
|
+
* Two entities touched: `toucher` moved into `touched`. Only the classes a
|
|
3045
|
+
* listener asked for reach it:
|
|
3046
|
+
*
|
|
3047
|
+
* ```ts
|
|
3048
|
+
* game.addEventListener("touch", onTouch, { toucher: "player", touched: "player" });
|
|
3049
|
+
* ```
|
|
3050
|
+
*
|
|
3051
|
+
* Pawn: `register_touch`
|
|
3052
|
+
*/
|
|
3053
|
+
export class TouchEvent {
|
|
3054
|
+
constructor(
|
|
3055
|
+
/** The entity that moved into the other. */
|
|
3056
|
+
public toucher: Entity,
|
|
3057
|
+
/** The entity it touched. */
|
|
3058
|
+
public touched: Entity
|
|
3059
|
+
) {}
|
|
3060
|
+
|
|
3061
|
+
/** Blocks the touch: the game does not act on it. */
|
|
3062
|
+
preventDefault(): void {
|
|
3063
|
+
handled();
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
/** A touch listener, as game.addEventListener("touch", ...) takes one. */
|
|
3068
|
+
type TouchListener = (event: TouchEvent) => void;
|
|
3069
|
+
|
|
3070
|
+
function touchFilter(toucher: string, touched: string): TouchFilter | null {
|
|
3071
|
+
for (let i = 0; i < touchFilters.length; i++) {
|
|
3072
|
+
const filter = touchFilters[i];
|
|
3073
|
+
if (filter.toucher == toucher && filter.touched == touched) return filter;
|
|
3074
|
+
}
|
|
3075
|
+
return null;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
function addTouchListener(listener: TouchListener, toucher: string, touched: string): void {
|
|
3079
|
+
let filter = touchFilter(toucher, touched);
|
|
3080
|
+
|
|
3081
|
+
if (filter == null) {
|
|
3082
|
+
filter = new TouchFilter(toucher, touched);
|
|
3083
|
+
touchFilters.push(filter);
|
|
3084
|
+
if (serverUp) registerTouch(filter);
|
|
3085
|
+
else waitingTouches.push(filter);
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
filter.listeners.push(listener);
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
function removeTouchListener(listener: TouchListener, toucher: string, touched: string): void {
|
|
3092
|
+
const filter = touchFilter(toucher, touched);
|
|
3093
|
+
if (filter == null) return;
|
|
3094
|
+
const at = filter.listeners.indexOf(listener);
|
|
3095
|
+
if (at >= 0) filter.listeners.splice(at, 1);
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
/** Hands one pair of classes to the engine module; a reload takes the same public back. */
|
|
3099
|
+
function registerTouch(filter: TouchFilter): void {
|
|
3100
|
+
const fired = (touched: number, toucher: number, c: number, d: number): void => touchFired(filter, touched, toucher);
|
|
3101
|
+
const pub = publicFor(fired, `touch:${filter.toucher}:${filter.touched}`);
|
|
3102
|
+
if (pub.length > 0) register_touch(filter.touched, filter.toucher, pub);
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
/** The public register_touch calls: (touched, toucher), in its order. */
|
|
3106
|
+
function touchFired(filter: TouchFilter, touched: number, toucher: number): void {
|
|
3107
|
+
const event = new TouchEvent(new Entity(toucher), new Entity(touched));
|
|
3108
|
+
// A copy: a listener that removes itself must not make the next one skip.
|
|
3109
|
+
const listeners = filter.listeners.slice(0);
|
|
3110
|
+
for (let i = 0; i < listeners.length; i++) listeners[i](event);
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
/** The winner of a round, one of `"TERRORIST"`, `"CT"`, `"draw"`, or `"none"` - a restart without a winner. */
|
|
3114
|
+
export type RoundWinner = "TERRORIST" | "CT" | "draw" | "none";
|
|
3115
|
+
|
|
3116
|
+
/** The options of `game.endRound`; only `winner` is required. */
|
|
3117
|
+
export interface EndRoundOptions {
|
|
3118
|
+
/** The round's winner, one of `"TERRORIST"`, `"CT"`, `"draw"`, or `"none"` - a restart. */
|
|
3119
|
+
winner: RoundWinner;
|
|
3120
|
+
/** Seconds until the next round starts; `5` by default. */
|
|
3121
|
+
delay?: number;
|
|
3122
|
+
/** The message in the middle of the screen, or a game text such as `"#Terrorists_Win"`; `"default"` is the usual one for this winner, `""` none. */
|
|
3123
|
+
message?: string;
|
|
3124
|
+
/** The sound, a radio phrase such as `"terwin"`; `"default"` is the usual one for this winner, `""` none. */
|
|
3125
|
+
sound?: string;
|
|
3126
|
+
/**
|
|
3127
|
+
* `true` to tell the `roundEnd` listeners of every plugin, Pawn ones too, as when
|
|
3128
|
+
* the game ends a round itself. `false` by default: a `roundEnd` listener that
|
|
3129
|
+
* ends the round would call itself.
|
|
3130
|
+
*
|
|
3131
|
+
* Pawn: `rg_round_end(..., trigger)`
|
|
3132
|
+
*/
|
|
3133
|
+
dispatch?: boolean;
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
/** The game the plugin runs in: its events (reapi hookchains and Ham Sandwich functions), its rules' fields and `endRound`. */
|
|
3137
|
+
export const game = new Game();
|
|
3138
|
+
|
|
3139
|
+
/**
|
|
3140
|
+
* The places a message can show: `Variant.chat`, `center`, `console`, `notify`.
|
|
3141
|
+
* A plain string works the same - `"center"`; an unknown name goes to chat.
|
|
3142
|
+
*
|
|
3143
|
+
* Pawn: `print_chat`, `print_center`, `print_console`, `print_notify`
|
|
3144
|
+
*/
|
|
3145
|
+
export namespace Variant {
|
|
3146
|
+
/** A line in the player's chat. */
|
|
3147
|
+
export const chat: string = "chat";
|
|
3148
|
+
/** Text in the middle of the player's screen. */
|
|
3149
|
+
export const center: string = "center";
|
|
3150
|
+
/** A line in the player's console. */
|
|
3151
|
+
export const console: string = "console";
|
|
3152
|
+
/** A line in the player's console, sent as a notification; with `developer 1` CS also shows it in the top-left corner of the screen. */
|
|
3153
|
+
export const notify: string = "notify";
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
/** The place a message shows, as a string: one of `"chat"`, `"center"`, `"console"` or `"notify"`. */
|
|
3157
|
+
export type VariantName = "chat" | "center" | "console" | "notify";
|
|
3158
|
+
|
|
3159
|
+
// Both come from the generator, so neither can fall behind: Flag is every
|
|
3160
|
+
// ADMIN_* the includes declare, and Event is every forward the host plugin
|
|
3161
|
+
// relays. They are re-exported here so that a plugin imports one file.
|
|
3162
|
+
export { Flag } from "./constants";
|
|
3163
|
+
export * from "./events";
|
|
3164
|
+
|
|
3165
|
+
// Re-exporting does not bring a name into this file's own scope, and these
|
|
3166
|
+
// are used here: `Flag` for a command's default argument, the other two to
|
|
3167
|
+
// turn an event's short name into the forward the host plugin relays.
|
|
3168
|
+
import { Flag, FlagName, flagOf, HookName, hookIdOf } from "./constants";
|
|
3169
|
+
import { Entity, GameFields, PlayerFields } from "./entities";
|
|
3170
|
+
import {
|
|
3171
|
+
MSG_ALL, MSG_ONE, MSG_ONE_UNRELIABLE, MODEL_AUTO, m_iDeaths, m_iTeam, LibType_Library, SPEAK_MUTED, SPEAK_ALL, SPEAK_LISTENALL, CS_T_TERROR, CS_CT_URBAN, CS_DONTCHANGE,
|
|
3172
|
+
Ham_CS_RoundRespawn, Ham_CS_Player_ResetMaxSpeed, ARG_ANGLE, ARG_COORD, ARG_STRING
|
|
3173
|
+
} from "./constants";
|
|
3174
|
+
export { Entity, Weapon, WeaponKind, weaponKindOf } from "./entities";
|
|
3175
|
+
// The names an enum field takes and gives: `entity.renderMode = "additive"`.
|
|
3176
|
+
export {
|
|
3177
|
+
RenderMode, RenderFx, MoveType, Solid, TakeDamage, DeadFlag, WaterLevel, Contents, FixAngle, HitGroup,
|
|
3178
|
+
ArmorType, ObserverMode, JoinState, GameMenu, PlayerModel, IgnoredChat, ThrowDirection, BloodColor, MonsterState, MusicState
|
|
3179
|
+
} from "./entities";
|
|
3180
|
+
import {
|
|
3181
|
+
NATIVE_server_cmd, NATIVE_client_cmd, NATIVE_CreateMultiForward, NATIVE_ExecuteForward, get_gametime, get_mapname,
|
|
3182
|
+
nvault_open, nvault_set, nvault_remove, rg_round_end
|
|
3183
|
+
} from "./natives";
|
|
3184
|
+
import { ET_IGNORE, ET_STOP, FP_ARRAY, FP_CELL, FP_FLOAT, FP_STRING } from "./constants";
|
|
3185
|
+
import { ROUND_NONE, ROUND_CTS_WIN, ROUND_TERRORISTS_WIN, ROUND_END_DRAW } from "./constants";
|
|
3186
|
+
import { PluginInitEvent, PluginPrecacheEvent, ServerEventMap, addServerListener, removeServerListener } from "./events";
|
|
3187
|
+
|
|
3188
|
+
function variantOf(variant: VariantName): number {
|
|
3189
|
+
if (variant == Variant.center) return 4;
|
|
3190
|
+
if (variant == Variant.console) return 2;
|
|
3191
|
+
if (variant == Variant.notify) return 1;
|
|
3192
|
+
return 3;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
/** A message's recipient together with its place: `{ id: 0, variant: "center" }`. `id` is a player's `id`, `0` for everyone. */
|
|
3196
|
+
export interface Target {
|
|
3197
|
+
/** The recipient's player `id`; `0` for every player. */
|
|
3198
|
+
id: number;
|
|
3199
|
+
/** The place the message shows, one of `"chat"` (the default), `"center"`, `"console"` or `"notify"`. */
|
|
3200
|
+
variant?: VariantName;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// One letter is one colour everywhere; a place draws the ones it can and
|
|
3204
|
+
// drops the rest.
|
|
3205
|
+
/** The colour tags chat draws: `!y` yellow, `!r` red, `!d` grey, `!g` green, `!b` blue, `!t` the reader's team. */
|
|
3206
|
+
const CHAT_TAGS = "yrdgbt";
|
|
3207
|
+
/** The colour tags a menu draws: `!y` yellow, `!r` red, `!d` grey, `!w` white, `!R` to the right edge. */
|
|
3208
|
+
const MENU_TAGS = "yrdwR";
|
|
3209
|
+
|
|
3210
|
+
/** Turns a chat line's colour tags (`!g`, `!r`, ...) into the colour codes the client reads, and records in `swapTeam` which team colour the line needs. `print` calls it; exported for tests. */
|
|
3211
|
+
export function paint(text: string): string {
|
|
3212
|
+
swapTeam = "";
|
|
3213
|
+
|
|
3214
|
+
let out = "";
|
|
3215
|
+
|
|
3216
|
+
for (let i = 0; i < text.length; i++) {
|
|
3217
|
+
const tag = i + 1 < text.length ? text.charAt(i + 1) : "";
|
|
3218
|
+
|
|
3219
|
+
if (text.charAt(i) != "!" || tag.length == 0 || !(CHAT_TAGS.includes(tag) || MENU_TAGS.includes(tag))) {
|
|
3220
|
+
// Not a tag after all: the `!` stands for itself.
|
|
3221
|
+
out += text.charAt(i);
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
i++;
|
|
3226
|
+
|
|
3227
|
+
// A menu's tag (`!w`, `!R`) means nothing in chat: it goes.
|
|
3228
|
+
if (!CHAT_TAGS.includes(tag)) continue;
|
|
3229
|
+
|
|
3230
|
+
let code: i32 = 0x03;
|
|
3231
|
+
let swap = "";
|
|
3232
|
+
|
|
3233
|
+
if (tag == "g") code = 0x04;
|
|
3234
|
+
else if (tag == "y") code = 0x01;
|
|
3235
|
+
else if (tag == "t") swap = SENDER_TEAM;
|
|
3236
|
+
else if (tag == "r") swap = "TERRORIST";
|
|
3237
|
+
else if (tag == "b") swap = "CT";
|
|
3238
|
+
else if (tag == "d") swap = "SPECTATOR";
|
|
3239
|
+
|
|
3240
|
+
out += String.fromCharCode(code);
|
|
3241
|
+
|
|
3242
|
+
// The first colour that needs a swap decides it; the rest inherit,
|
|
3243
|
+
// because the swap belongs to the message and not to a character.
|
|
3244
|
+
if (swap.length > 0 && swapTeam.length == 0) swapTeam = swap;
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
return out;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
function send(id: i32, channel: i32, message: string): void {
|
|
3251
|
+
// Chat is the one channel with colours in it; the rest are plain text and
|
|
3252
|
+
// go where they always went.
|
|
3253
|
+
if (channel != 3) {
|
|
3254
|
+
_printClient(id, channel, message);
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
const painted = paint(message);
|
|
3259
|
+
_sayText(id, painted, swapTeam);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
/** The team colour the last `paint()` chose for the line, one of `"TERRORIST"` (red), `"CT"` (blue), `"SPECTATOR"` (grey), or `""` - the reader's own team colour. `print` reads it right after. */
|
|
3263
|
+
export let swapTeam: string = "";
|
|
3264
|
+
|
|
3265
|
+
/** `!t` asks for the sender's own team, which the module fills in. */
|
|
3266
|
+
const SENDER_TEAM = "";
|
|
3267
|
+
|
|
3268
|
+
/**
|
|
3269
|
+
* Sends a message to a player, or to every player (`0`).
|
|
3270
|
+
*
|
|
3271
|
+
* ```ts
|
|
3272
|
+
* print(player, "Health restored!"); // the player's chat
|
|
3273
|
+
* print(0, "Round starts in 5 seconds"); // everyone's chat
|
|
3274
|
+
* print(player, "Health restored!", "center"); // the middle of the player's screen
|
|
3275
|
+
* print({ id: 0, variant: "center" }, "Go!"); // the middle of everyone's screen
|
|
3276
|
+
* ```
|
|
3277
|
+
*
|
|
3278
|
+
* The first argument is a player, a player's `id`, or `0` for everyone. The third
|
|
3279
|
+
* is where the message shows, one of `"chat"` (the default), `"center"` - the middle of
|
|
3280
|
+
* the screen, `"console"` - the player's console, `"notify"` - the console too; CS
|
|
3281
|
+
* shows it on screen only with `developer 1`.
|
|
3282
|
+
*
|
|
3283
|
+
* Colour tags work in chat only, and a letter is the same colour as in a menu:
|
|
3284
|
+
* - `!y` yellow (the usual chat colour), `!g` green
|
|
3285
|
+
* - `!r` red, `!b` blue, `!d` grey, `!t` the colour of the reader's team
|
|
3286
|
+
*
|
|
3287
|
+
* A menu's own tags (`!w`, `!R`) are dropped from a chat line. Red, blue,
|
|
3288
|
+
* grey and `!t` share the message's one team colour: the first one used wins.
|
|
3289
|
+
*
|
|
3290
|
+
* Pawn: `client_print`, `client_print_color`
|
|
3291
|
+
*/
|
|
3292
|
+
export function print<T extends Target | Client | number = Target>(to: T, message: string, variant: VariantName = "chat"): void {
|
|
3293
|
+
// A player id is a number - an f64, since number is JavaScript's.
|
|
3294
|
+
if (isInteger<T>() || isFloat<T>()) {
|
|
3295
|
+
send(<i32>to, variantOf(variant), message);
|
|
3296
|
+
} else if (isReference<T>() && idof<T>() == idof<Client>()) {
|
|
3297
|
+
send(<i32>changetype<Client>(to).id, variantOf(variant), message);
|
|
3298
|
+
} else if (isReference<T>() && idof<T>() == idof<Player>()) {
|
|
3299
|
+
send(<i32>changetype<Player>(to).id, variantOf(variant), message);
|
|
3300
|
+
} else if (isReference<T>() && idof<T>() == idof<Target>()) {
|
|
3301
|
+
const target = changetype<Target>(to);
|
|
3302
|
+
send(<i32>target.id, variantOf(target.variant ?? "chat"), message);
|
|
3303
|
+
} else {
|
|
3304
|
+
ERROR("print takes a player, a player id or a target object");
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
/**
|
|
3309
|
+
* The server's dictionaries: the files of `data/lang`, a line per key and
|
|
3310
|
+
* language, and each player reads them in his own.
|
|
3311
|
+
*
|
|
3312
|
+
* ```ts
|
|
3313
|
+
* lang.load("myplugin"); // data/lang/myplugin.txt
|
|
3314
|
+
* print(player, lang.translate(player, "MYPLUGIN_WELCOME", [player.name]));
|
|
3315
|
+
* ```
|
|
3316
|
+
*
|
|
3317
|
+
* Pawn: `register_dictionary`, `LookupLangKey`
|
|
3318
|
+
*/
|
|
3319
|
+
export namespace lang {
|
|
3320
|
+
/**
|
|
3321
|
+
* Loads the dictionary `data/lang/<name>.txt`: `lang.load("myplugin")`.
|
|
3322
|
+
* `false` when there is no such file.
|
|
3323
|
+
*
|
|
3324
|
+
* Pawn: `register_dictionary`
|
|
3325
|
+
*/
|
|
3326
|
+
export function load(name: string): boolean {
|
|
3327
|
+
return register_dictionary(`${name}.txt`) != 0;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
/**
|
|
3331
|
+
* The key's line in the player's language, `null` for the server's:
|
|
3332
|
+
* `lang.translate(player, "MYPLUGIN_WELCOME", [player.name])`.
|
|
3333
|
+
*
|
|
3334
|
+
* `%s`, `%d`, `%f` (`%.1f`, `%02d`, ...) are filled from `args` in order;
|
|
3335
|
+
* one no argument is left for stays as written. The dictionary's colour
|
|
3336
|
+
* codes come back as tags - `\y` as `!y`, `^4` as `!g` - so the line goes
|
|
3337
|
+
* to a menu and to chat alike. A key no dictionary has comes back as it is.
|
|
3338
|
+
*
|
|
3339
|
+
* Pawn: `LookupLangKey`, `format` with `%L`
|
|
3340
|
+
*/
|
|
3341
|
+
export function translate(player: Client | null, key: string, args: string[] = []): string {
|
|
3342
|
+
const found = lookupLang(key, player != null ? <i32>player.id : 0);
|
|
3343
|
+
return found.length > 0 ? formatPawn(colorTags(found), new TextCursor(args)) : key;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
/**
|
|
3348
|
+
* Reads and sets a cvar by name in one call: `cvar.num("mp_freezetime")`.
|
|
3349
|
+
*
|
|
3350
|
+
* For a cvar used more than once `Cvar` is the better choice: it creates a
|
|
3351
|
+
* missing cvar, hears its changes and reads it as text, a number or a switch.
|
|
3352
|
+
*/
|
|
3353
|
+
export namespace cvar {
|
|
3354
|
+
/**
|
|
3355
|
+
* The cvar's value as a whole number: `cvar.num("mp_freezetime")`.
|
|
3356
|
+
*
|
|
3357
|
+
* Pawn: `get_cvar_num`
|
|
3358
|
+
*/
|
|
3359
|
+
export function num(name: string): number {
|
|
3360
|
+
return get_cvar_num(name);
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
/**
|
|
3364
|
+
* Sets the cvar to a whole number: `cvar.setNum("mp_freezetime", 5)`.
|
|
3365
|
+
*
|
|
3366
|
+
* Pawn: `set_cvar_num`
|
|
3367
|
+
*/
|
|
3368
|
+
export function setNum(name: string, value: number): void {
|
|
3369
|
+
set_cvar_num(name, value);
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
/**
|
|
3373
|
+
* The cvar's value as text: `cvar.str("hostname")`.
|
|
3374
|
+
*
|
|
3375
|
+
* Pawn: `get_cvar_string`
|
|
3376
|
+
*/
|
|
3377
|
+
export function str(name: string): string {
|
|
3378
|
+
return get_cvar_string(name);
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
/**
|
|
3382
|
+
* Sets the cvar's text: `cvar.setStr("hostname", "My Server")`.
|
|
3383
|
+
*
|
|
3384
|
+
* Pawn: `set_cvar_string`
|
|
3385
|
+
*/
|
|
3386
|
+
export function setStr(name: string, value: string): void {
|
|
3387
|
+
set_cvar_string(name, value);
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
/**
|
|
3392
|
+
* Registers a console command for players; the handler gets the `id` of the
|
|
3393
|
+
* player who typed it. `server.addCommand` is the usual way.
|
|
3394
|
+
*
|
|
3395
|
+
* Pawn: `register_clcmd`
|
|
3396
|
+
*/
|
|
3397
|
+
export function cmd(pattern: string, handler: Handler, flag: FlagName = "ALL", info: string = ""): void {
|
|
3398
|
+
_clcmd(pattern, hostIndex(handler, false), flagOf(flag), info, SHAPE_NARROW);
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
/**
|
|
3402
|
+
* Registers a console command for players whose handler gets the raw
|
|
3403
|
+
* arguments `(id, level, cid)`. `handled()` in the handler stops the command
|
|
3404
|
+
* from going on to other plugins.
|
|
3405
|
+
*
|
|
3406
|
+
* Pawn: `register_clcmd`
|
|
3407
|
+
*/
|
|
3408
|
+
export function cmdWide(pattern: string, handler: WideHandler, flag: FlagName = "ALL", info: string = ""): void {
|
|
3409
|
+
_clcmd(pattern, hostIndex(handler, true), flagOf(flag), info, SHAPE_WIDE);
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
/** The function a timer runs: `() => ...`. */
|
|
3413
|
+
export type TimerHandler = () => void;
|
|
3414
|
+
|
|
3415
|
+
// The timers that are armed, by handle. The host fires a task by calling one
|
|
3416
|
+
// function with the task's id; that function is timerFired, which calls the
|
|
3417
|
+
// handler from here - with its closure, which a bare table index would lose.
|
|
3418
|
+
class Timer {
|
|
3419
|
+
constructor(public handler: TimerHandler, public repeat: bool) {}
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
const timers = new Map<i32, Timer>();
|
|
3423
|
+
|
|
3424
|
+
function timerFired(handle: i32): void {
|
|
3425
|
+
if (!timers.has(handle)) return;
|
|
3426
|
+
const timer = timers.get(handle);
|
|
3427
|
+
if (!timer.repeat) timers.delete(handle);
|
|
3428
|
+
timer.handler();
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
function armTimer(handler: TimerHandler, ms: number, repeat: bool): i32 {
|
|
3432
|
+
// The host's task ids are one space for every plugin, and a timer is
|
|
3433
|
+
// stopped by its id: the handle comes from the host, so no other plugin
|
|
3434
|
+
// has it and clearTimeout here cannot stop a timer there.
|
|
3435
|
+
const handle = _uniqueId();
|
|
3436
|
+
timers.set(handle, new Timer(handler, repeat));
|
|
3437
|
+
// The delay crosses as the bit pattern of a 32-bit float, because a Pawn
|
|
3438
|
+
// native takes cells and every signature in the table is all-i on purpose.
|
|
3439
|
+
_task(floatCell(ms / 1000.0), timerFired.index, handle, repeat ? 1 : 0);
|
|
3440
|
+
return handle;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
/**
|
|
3444
|
+
* Runs `handler` once after `ms` milliseconds and returns the timer's handle,
|
|
3445
|
+
* as in the browser:
|
|
3446
|
+
*
|
|
3447
|
+
* ```ts
|
|
3448
|
+
* const handle = setTimeout(() => print(player, "Welcome!"), 2000);
|
|
3449
|
+
* clearTimeout(handle);
|
|
3450
|
+
* ```
|
|
3451
|
+
*
|
|
3452
|
+
* The handler may use the variables around it. The precision is one server frame.
|
|
3453
|
+
*
|
|
3454
|
+
* Pawn: `set_task`
|
|
3455
|
+
*/
|
|
3456
|
+
export function setTimeout(handler: TimerHandler, ms: number = 0): number {
|
|
3457
|
+
return armTimer(handler, ms, false);
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
/** The options of `sleep()`. */
|
|
3461
|
+
export interface SleepOptions {
|
|
3462
|
+
/** An AbortSignal that cancels the wait: the promise then rejects with the signal's reason. */
|
|
3463
|
+
signal?: AbortSignal;
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
/**
|
|
3467
|
+
* Returns a promise fulfilled after `ms` milliseconds - the way to wait inside
|
|
3468
|
+
* an async function:
|
|
3469
|
+
*
|
|
3470
|
+
* ```ts
|
|
3471
|
+
* await sleep(1000);
|
|
3472
|
+
* await sleep(5000, { signal: AbortSignal.timeout(2000) }); // rejects after 2 s
|
|
3473
|
+
* ```
|
|
3474
|
+
*
|
|
3475
|
+
* The precision is one server frame. Inside an async command handler or
|
|
3476
|
+
* player event the wait also ends when that player leaves.
|
|
3477
|
+
*
|
|
3478
|
+
* Pawn: `set_task`
|
|
3479
|
+
*/
|
|
3480
|
+
export function sleep(ms: number, options: SleepOptions = {}): Promise<void> {
|
|
3481
|
+
return __co_sleep(ms, options.signal ?? null);
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
/**
|
|
3485
|
+
* Runs `handler` every `ms` milliseconds until `clearInterval` stops it;
|
|
3486
|
+
* returns the timer's handle.
|
|
3487
|
+
*
|
|
3488
|
+
* Pawn: `set_task` with the "b" flag
|
|
3489
|
+
*/
|
|
3490
|
+
export function setInterval(handler: TimerHandler, ms: number): number {
|
|
3491
|
+
return armTimer(handler, ms, true);
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
/**
|
|
3495
|
+
* Stops the timer with this handle. A timer that has already fired or been
|
|
3496
|
+
* stopped is ignored. Only this stops these timers: Pawn's task natives do not
|
|
3497
|
+
* see them.
|
|
3498
|
+
*
|
|
3499
|
+
* Pawn: `remove_task`
|
|
3500
|
+
*/
|
|
3501
|
+
export function clearTimeout(handle: number): void {
|
|
3502
|
+
if (!timers.has(<i32>handle)) return;
|
|
3503
|
+
timers.delete(<i32>handle);
|
|
3504
|
+
_stopTask(<i32>handle);
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
/** Stops the interval with this handle; the same as `clearTimeout`. */
|
|
3508
|
+
export function clearInterval(handle: number): void {
|
|
3509
|
+
clearTimeout(handle);
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
// @ts-ignore: decorator
|
|
3513
|
+
@external("env", "call") declare function _call(id: i32, args: i32, mask: i32, argc: i32): i32;
|
|
3514
|
+
|
|
3515
|
+
// WebAssembly fixes an import's arity, so a native with a `...` tail cannot be
|
|
3516
|
+
// declared in ~/natives at all - only its id is - and the module cannot infer
|
|
3517
|
+
// the tail's types.
|
|
3518
|
+
/**
|
|
3519
|
+
* A call of a Pawn native with a `...` tail, built one argument at a time -
|
|
3520
|
+
* the low-level way. A native from `~/natives` is an ordinary function and
|
|
3521
|
+
* needs none of this.
|
|
3522
|
+
*
|
|
3523
|
+
* new Call(NATIVE_server_print).str("%s").str(text).run();
|
|
3524
|
+
*
|
|
3525
|
+
* Find the `...` in the native's declaration. Arguments before it go as they
|
|
3526
|
+
* are, with `num` or `str`. A number in the tail goes with `ref`, which passes
|
|
3527
|
+
* it by address, and `out(i)` reads back what the native wrote there; a string
|
|
3528
|
+
* goes with `str` anywhere.
|
|
3529
|
+
*
|
|
3530
|
+
* ExecuteHam(Ham:function, this, any:...) num, num, then the tail
|
|
3531
|
+
* SetHookChainArg(number, AType:type, any:...) num, num, then the tail
|
|
3532
|
+
* ExecuteForward(handle, &ret, any:...) num, ref for &ret, then tail
|
|
3533
|
+
*
|
|
3534
|
+
* A wrong kind of argument fails quietly, somewhere else.
|
|
3535
|
+
*/
|
|
3536
|
+
export class Call {
|
|
3537
|
+
// Room for eight arguments, doubled whenever a call needs more: a native
|
|
3538
|
+
// read on every frame takes two or three, a forward's ExecuteForward 34.
|
|
3539
|
+
private args: StaticArray<i32> = new StaticArray<i32>(8);
|
|
3540
|
+
private mask: StaticArray<u8> = new StaticArray<u8>(8);
|
|
3541
|
+
private cells: StaticArray<i32> = new StaticArray<i32>(8);
|
|
3542
|
+
// Strings have to outlive the call, and one shared buffer would have the
|
|
3543
|
+
// second overwrite the first. Most calls - a field read - hold none, so
|
|
3544
|
+
// the list is made by the first.
|
|
3545
|
+
private held: StaticArray<i32>[] | null = null;
|
|
3546
|
+
private n: i32 = 0;
|
|
3547
|
+
|
|
3548
|
+
constructor(private id: i32) {}
|
|
3549
|
+
|
|
3550
|
+
/** Keeps an argument's cells alive until the call is done. */
|
|
3551
|
+
private hold(cells: StaticArray<i32>): void {
|
|
3552
|
+
let held = this.held;
|
|
3553
|
+
if (held == null) {
|
|
3554
|
+
held = [];
|
|
3555
|
+
this.held = held;
|
|
3556
|
+
}
|
|
3557
|
+
held.push(cells);
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
/** Puts an argument and its kind at the next place. */
|
|
3561
|
+
private push(value: i32, kind: u8): Call {
|
|
3562
|
+
if (this.n == this.args.length) this.grow();
|
|
3563
|
+
unchecked(this.args[this.n] = value);
|
|
3564
|
+
unchecked(this.mask[this.n] = kind);
|
|
3565
|
+
this.n++;
|
|
3566
|
+
return this;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
// A `ref` argument is an address inside `cells`, so it moves with it.
|
|
3570
|
+
private grow(): void {
|
|
3571
|
+
const size = this.n * 2;
|
|
3572
|
+
const args = new StaticArray<i32>(size);
|
|
3573
|
+
const mask = new StaticArray<u8>(size);
|
|
3574
|
+
const cells = new StaticArray<i32>(size);
|
|
3575
|
+
for (let i = 0; i < this.n; i++) {
|
|
3576
|
+
const ref = unchecked(this.mask[i]) == REF;
|
|
3577
|
+
unchecked(args[i] = ref ? changetype<i32>(cells) + i * 4 : unchecked(this.args[i]));
|
|
3578
|
+
unchecked(mask[i] = unchecked(this.mask[i]));
|
|
3579
|
+
unchecked(cells[i] = unchecked(this.cells[i]));
|
|
3580
|
+
}
|
|
3581
|
+
this.args = args;
|
|
3582
|
+
this.mask = mask;
|
|
3583
|
+
this.cells = cells;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
/** Adds a number argument as it is: an entity index, a constant, a count. */
|
|
3587
|
+
num(value: number): Call {
|
|
3588
|
+
return this.push(<i32>value, 0x6e); // n
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
/** Adds a fractional number argument, one the native declares as `Float:`. */
|
|
3592
|
+
float(value: f64): Call {
|
|
3593
|
+
return this.num(reinterpret<i32>(<f32>value));
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
/** Adds a string argument, before the `...` or in the tail alike. */
|
|
3597
|
+
str(text: string): Call {
|
|
3598
|
+
const buffer = __cellsOf(text);
|
|
3599
|
+
this.hold(buffer);
|
|
3600
|
+
return this.push(changetype<i32>(buffer), 0x73); // s
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
/** Adds an array of cells the native reads and may write into, followed by its length. */
|
|
3604
|
+
buffer(cells: CellBuffer, length: number): Call {
|
|
3605
|
+
return this.push(cells.address, 0x62).num(length); // b
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
/**
|
|
3609
|
+
* Adds an array of cells the native writes into, in a `...` tail: its length
|
|
3610
|
+
* follows by address, as a tail's numbers do - `get_member(id, member,
|
|
3611
|
+
* dest[], len)`.
|
|
3612
|
+
*/
|
|
3613
|
+
tailBuffer(cells: CellBuffer, length: number): Call {
|
|
3614
|
+
return this.push(cells.address, 0x62).ref(length); // b
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
/** Adds a vector: three fractional numbers at one address - an origin, angles, a colour. Unlike `buffer`, no length follows it. */
|
|
3618
|
+
vec(x: f64, y: f64, z: f64): Call {
|
|
3619
|
+
const cells = new StaticArray<i32>(3);
|
|
3620
|
+
unchecked(cells[0] = reinterpret<i32>(<f32>x));
|
|
3621
|
+
unchecked(cells[1] = reinterpret<i32>(<f32>y));
|
|
3622
|
+
unchecked(cells[2] = reinterpret<i32>(<f32>z));
|
|
3623
|
+
this.hold(cells);
|
|
3624
|
+
return this.push(changetype<i32>(cells), 0x76); // v
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
/** Adds a vector the native fills in; after `run` the result is in `cells`. */
|
|
3628
|
+
vecInto(cells: CellBuffer): Call {
|
|
3629
|
+
return this.push(cells.address, 0x76); // v
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
/**
|
|
3633
|
+
* Adds an array to a forward's `...` tail: `ExecuteForward` gets it as
|
|
3634
|
+
* `PrepareArray` makes it. `floats` sends the numbers as `Float:`.
|
|
3635
|
+
*/
|
|
3636
|
+
array(values: number[], floats: bool = false): Call {
|
|
3637
|
+
const cells = arrayCells(values, floats);
|
|
3638
|
+
this.hold(cells);
|
|
3639
|
+
return this.push(changetype<i32>(cells), 0x61); // a
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
/** Adds a number passed by address, as a `...` tail argument must be; `out` reads what the native wrote into it. */
|
|
3643
|
+
ref(value: number): Call {
|
|
3644
|
+
if (this.n == this.args.length) this.grow();
|
|
3645
|
+
unchecked(this.cells[this.n] = <i32>value);
|
|
3646
|
+
return this.push(changetype<i32>(this.cells) + this.n * 4, REF);
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
/** The value the native left in the `ref` argument at this position, after `run`. */
|
|
3650
|
+
out(index: i32): number {
|
|
3651
|
+
return unchecked(this.cells[index]);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
/** Calls the native with the arguments added so far and returns its result. */
|
|
3655
|
+
run(): number {
|
|
3656
|
+
return _call(this.id, changetype<i32>(this.args), changetype<i32>(this.mask), this.n);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
/** A Call argument's kind: a number passed by address. */
|
|
3661
|
+
const REF: u8 = 0x72; // r
|
|
3662
|
+
|
|
3663
|
+
/** An array as the module reads one for a forward: its count, then its cells. */
|
|
3664
|
+
function arrayCells(values: number[], floats: bool): StaticArray<i32> {
|
|
3665
|
+
const cells = new StaticArray<i32>(values.length + 1);
|
|
3666
|
+
unchecked(cells[0] = values.length);
|
|
3667
|
+
for (let i = 0; i < values.length; i++) unchecked(cells[i + 1] = floats ? reinterpret<i32>(<f32>unchecked(values[i])) : <i32>unchecked(values[i]));
|
|
3668
|
+
return cells;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// ---------------------------------------------------------------- field natives
|
|
3672
|
+
|
|
3673
|
+
// What a field of reapi's field natives - get_entvar, get_member, get_pmove -
|
|
3674
|
+
// holds, as the generated __<native>_kind tables of ~/natives say
|
|
3675
|
+
// (scripts/generate-wasm-api.ts): a whole number, a Float, a vector or text,
|
|
3676
|
+
// and FIELD_ELEMENT on top for an array member.
|
|
3677
|
+
const FIELD_FLOAT: i32 = 1;
|
|
3678
|
+
const FIELD_VECTOR: i32 = 2;
|
|
3679
|
+
const FIELD_STRING: i32 = 3;
|
|
3680
|
+
const FIELD_ELEMENT: i32 = 4;
|
|
3681
|
+
|
|
3682
|
+
/** The longest text a field native writes back: reapi's longest member holds 256 bytes. */
|
|
3683
|
+
const FIELD_TEXT: i32 = 1024;
|
|
3684
|
+
|
|
3685
|
+
/** The type a field of this kind reads as - what `T` has to be. */
|
|
3686
|
+
function fieldType(kind: i32): string {
|
|
3687
|
+
const base = kind & 3;
|
|
3688
|
+
return base == FIELD_VECTOR ? "Vector" : base == FIELD_STRING ? "string" : "number";
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
/** Whether T is what a field of this kind reads as; says so in the console when it is not. */
|
|
3692
|
+
function fieldTakes<T>(kind: i32, native: string): bool {
|
|
3693
|
+
const base = kind & 3;
|
|
3694
|
+
const vector = isArray<T>() || isVector<T>();
|
|
3695
|
+
const fits = isString<T>() ? base == FIELD_STRING : vector ? base == FIELD_VECTOR : base < FIELD_VECTOR;
|
|
3696
|
+
if (!fits) console.error(`${native}: this field is a ${fieldType(kind)} - write ${native}<${fieldType(kind)}>(...)`);
|
|
3697
|
+
return fits;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
/**
|
|
3701
|
+
* @hidden A field of a reapi field native, as T: a whole number or a Float
|
|
3702
|
+
* as a number (or a boolean), a vector as a Vector, text as a string. A T the
|
|
3703
|
+
* field is not reads as nothing, and the console says which one to write.
|
|
3704
|
+
*/
|
|
3705
|
+
export function __getField<T>(call: Call, kind: i32, element: number, native: string): T {
|
|
3706
|
+
if (isString<T>()) {
|
|
3707
|
+
if (!fieldTakes<T>(kind, native)) return changetype<T>("");
|
|
3708
|
+
const text = new CellBuffer(FIELD_TEXT);
|
|
3709
|
+
call.tailBuffer(text, FIELD_TEXT - 1);
|
|
3710
|
+
if (kind & FIELD_ELEMENT) call.ref(element);
|
|
3711
|
+
call.run();
|
|
3712
|
+
return changetype<T>(text.text());
|
|
3713
|
+
}
|
|
3714
|
+
if (isReference<T>()) {
|
|
3715
|
+
// A Vector, or number[] it is one of.
|
|
3716
|
+
if (!fieldTakes<T>(kind, native)) return changetype<T>(new Vector());
|
|
3717
|
+
const cells = new CellBuffer(3);
|
|
3718
|
+
call.vecInto(cells);
|
|
3719
|
+
if (kind & FIELD_ELEMENT) call.ref(element);
|
|
3720
|
+
call.run();
|
|
3721
|
+
return changetype<T>(new Vector(cells.float(0), cells.float(1), cells.float(2)));
|
|
3722
|
+
}
|
|
3723
|
+
if (!isFloat<T>() && !isInteger<T>()) ERROR("a field reads as number, boolean, string or Vector");
|
|
3724
|
+
if (!fieldTakes<T>(kind, native)) return <T>0;
|
|
3725
|
+
if (kind & FIELD_ELEMENT) call.ref(element);
|
|
3726
|
+
const cell = <i32>call.run();
|
|
3727
|
+
if (isBoolean<T>()) return <T>(cell != 0);
|
|
3728
|
+
return <T>((kind & 3) == FIELD_FLOAT ? cellFloat(cell) : <f64>cell);
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
/**
|
|
3732
|
+
* @hidden Writes a field of a reapi field native from a value of its kind:
|
|
3733
|
+
* a number - a Float where the field is one - a boolean, a vector (a Vector or
|
|
3734
|
+
* any three numbers), text. A value of another kind writes nothing, and the
|
|
3735
|
+
* console says so. The native's result: 1 when it wrote.
|
|
3736
|
+
*/
|
|
3737
|
+
export function __setField<T>(call: Call, kind: i32, value: T, element: number, native: string): number {
|
|
3738
|
+
if (!fieldTakes<T>(kind, native)) return 0;
|
|
3739
|
+
if (isString<T>()) {
|
|
3740
|
+
call.str(changetype<string>(value));
|
|
3741
|
+
} else if (isArray<T>()) {
|
|
3742
|
+
const vector = changetype<number[]>(value);
|
|
3743
|
+
call.vec(vector[0], vector[1], vector[2]);
|
|
3744
|
+
} else if (isBoolean<T>()) {
|
|
3745
|
+
call.ref(value ? 1 : 0);
|
|
3746
|
+
} else {
|
|
3747
|
+
call.ref((kind & 3) == FIELD_FLOAT ? floatCell(<f64>value) : <i32>value);
|
|
3748
|
+
}
|
|
3749
|
+
if (kind & FIELD_ELEMENT) call.ref(element);
|
|
3750
|
+
return call.run();
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
// @ts-ignore: decorator
|
|
3754
|
+
@external("env", "hook") declare function _hook(id: i32, fn: i32, post: i32): i32;
|
|
3755
|
+
// @ts-ignore: decorator
|
|
3756
|
+
@external("env", "ham") declare function _ham(id: i32, entityClass: string, fn: i32, post: i32): i32;
|
|
3757
|
+
|
|
3758
|
+
/**
|
|
3759
|
+
* Registers a reapi hookchain with a raw handler of four numbers - the low
|
|
3760
|
+
* level under `game.addEventListener`, which is what a plugin uses.
|
|
3761
|
+
*
|
|
3762
|
+
* The name is reapi's, without the class where it is not needed:
|
|
3763
|
+
* `"restart_round"`, `"player_spawn"`; the editor completes them. The numbers
|
|
3764
|
+
* behind the names come from reapi's includes, so they must be the ones of the
|
|
3765
|
+
* reapi the server runs. Returns the hook's handle.
|
|
3766
|
+
*
|
|
3767
|
+
* Pawn: `RegisterHookChain`, `EnableHookChain`, `DisableHookChain`
|
|
3768
|
+
*/
|
|
3769
|
+
export function hook(name: HookName, handler: WideHandler, post: bool = false): number {
|
|
3770
|
+
const id = hookIdOf(name);
|
|
3771
|
+
|
|
3772
|
+
if (id < 0) {
|
|
3773
|
+
console.error(`no hookchain named "${name}"`);
|
|
3774
|
+
return 0;
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
return _hook(id, hostIndex(handler, true), post ? 1 : 0);
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
// AMX Mod X has one entry per .amxx file and every plugin here shares the
|
|
3781
|
+
// host's, so this is the only place their names exist.
|
|
3782
|
+
/** The plugin's name, version, author and description, given to `plugin({ ... })`; `amxts_plugins` in the server console lists them. */
|
|
3783
|
+
export interface PluginInfo {
|
|
3784
|
+
/** The plugin's name, e.g. `"My Plugin"`. */
|
|
3785
|
+
name: string;
|
|
3786
|
+
/** The plugin's version, e.g. `"1.0.0"`. */
|
|
3787
|
+
version: string;
|
|
3788
|
+
/** The plugin's author. */
|
|
3789
|
+
author: string;
|
|
3790
|
+
/** The plugin's description, in one line. */
|
|
3791
|
+
description?: string;
|
|
3792
|
+
/**
|
|
3793
|
+
* The Pawn include whose natives the plugin implements, e.g. `"myplugin.inc"`, from
|
|
3794
|
+
* `includes/` or beside the plugin. Each exported function reaches Pawn as the
|
|
3795
|
+
* include declares it, and Pawn plugins use that include.
|
|
3796
|
+
*/
|
|
3797
|
+
include?: string;
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
/**
|
|
3801
|
+
* Declares the plugin: its name, version, author and description, as the
|
|
3802
|
+
* server's plugin list shows them. Called once, at the top level:
|
|
3803
|
+
*
|
|
3804
|
+
* ```ts
|
|
3805
|
+
* plugin({ name: "Hello", version: "1.0.0", author: "you", description: "An example" });
|
|
3806
|
+
* ```
|
|
3807
|
+
*
|
|
3808
|
+
* `include` names the Pawn include whose natives the plugin implements.
|
|
3809
|
+
*
|
|
3810
|
+
* Pawn: `register_plugin`
|
|
3811
|
+
*/
|
|
3812
|
+
export function plugin(info: PluginInfo): void {
|
|
3813
|
+
_plugin(info.name, info.version, info.author, info.description ?? "");
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
// ---------------------------------------------------------------- modules
|
|
3817
|
+
|
|
3818
|
+
/**
|
|
3819
|
+
* The modules' settings in `amxts.config.ts`, each under its module's `configKey`.
|
|
3820
|
+
* Empty here: a module adds its key by augmenting this interface in
|
|
3821
|
+
* `"@amxts/core"` - `menus?: Partial<MenuCoreOptions>` - and the editor checks
|
|
3822
|
+
* the config against it.
|
|
3823
|
+
*/
|
|
3824
|
+
export interface ModuleOptions {}
|
|
3825
|
+
|
|
3826
|
+
/**
|
|
3827
|
+
* Defines a module: `export default defineModule<Options>({ meta, requires,
|
|
3828
|
+
* defaults, setup })` in its module file. `setup` runs once,
|
|
3829
|
+
* when the server loads the module, with `defaults` and what `amxts.config.ts`
|
|
3830
|
+
* sets over them. Global; `import { defineModule } from "@amxts/core"` works too.
|
|
3831
|
+
*/
|
|
3832
|
+
// @ts-ignore: decorator
|
|
3833
|
+
@global export function defineModule<T>(definition: AmxtsModule<T>): AmxtsModule<T> {
|
|
3834
|
+
return definition;
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
// ---------------------------------------------------------------- forwards
|
|
3838
|
+
|
|
3839
|
+
// @ts-ignore: decorator
|
|
3840
|
+
@external("env", "subscribe") declare function _subscribe(forward: string, fn: i32, tag: i32): void;
|
|
3841
|
+
// @ts-ignore: decorator
|
|
3842
|
+
@external("env", "emit_local") declare function _emitLocal(forward: string, mask: string, cells: i32, count: i32): void;
|
|
3843
|
+
|
|
3844
|
+
// @ts-ignore: decorator
|
|
3845
|
+
@external("env", "nvault_lookup") declare function _nvaultLookup(vault: i32, key: i32, value: i32, maxlen: i32, timestamp: i32): i32;
|
|
3846
|
+
|
|
3847
|
+
/**
|
|
3848
|
+
* The forward's stopping rule, one of: `"never"` - every plugin hears it, whatever it
|
|
3849
|
+
* returns; `"handled"` - the first plugin that says it handled the forward
|
|
3850
|
+
* stops it.
|
|
3851
|
+
*
|
|
3852
|
+
* Pawn: `ET_IGNORE`, `ET_STOP`
|
|
3853
|
+
*/
|
|
3854
|
+
export type ForwardStop = "never" | "handled";
|
|
3855
|
+
|
|
3856
|
+
/** A placeholder for an unused type argument of `Forward`: `Forward<number>` has one argument. */
|
|
3857
|
+
export class NoArgument {}
|
|
3858
|
+
|
|
3859
|
+
function isArgument<T>(): bool {
|
|
3860
|
+
return !(isReference<T>() && !isString<T>() && nameof<T>() == "NoArgument");
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
/**
|
|
3864
|
+
* How a Forward's argument crosses, as the build read it off the forward's
|
|
3865
|
+
* Pawn declaration or its type (scripts/plugin-natives.ts, crossForwards): one
|
|
3866
|
+
* letter per argument - `t` a Team as its TeamName number, `w` a RoundWinner
|
|
3867
|
+
* as its WinStatus number, `f` a number as a Float, `F` an array of numbers as
|
|
3868
|
+
* Floats, `_` (or nothing) as its type says.
|
|
3869
|
+
*/
|
|
3870
|
+
const CROSS_TEAM = "t";
|
|
3871
|
+
const CROSS_WINNER = "w";
|
|
3872
|
+
const CROSS_FLOAT = "f";
|
|
3873
|
+
const CROSS_FLOATS = "F";
|
|
3874
|
+
|
|
3875
|
+
/** The most arguments an AMX Mod X forward takes (FORWARD_MAX_PARAMS), and so a Forward. */
|
|
3876
|
+
const MAX_FORWARD_ARGS: i32 = 32;
|
|
3877
|
+
|
|
3878
|
+
/** WinStatus by number: WINSTATUS_NONE 0, CTS 1, TERRORISTS 2, DRAW 3. */
|
|
3879
|
+
const WINNER_NAMES: RoundWinner[] = ["none", "CT", "TERRORIST", "draw"];
|
|
3880
|
+
|
|
3881
|
+
/** The reason a round ends with, by WinStatus number: rg_round_end's `event`. */
|
|
3882
|
+
const ROUND_REASONS: i32[] = [ROUND_NONE, ROUND_CTS_WIN, ROUND_TERRORISTS_WIN, ROUND_END_DRAW];
|
|
3883
|
+
|
|
3884
|
+
/** A name that crosses as a number - a Team or a RoundWinner - as that number. */
|
|
3885
|
+
function nameCell(name: string, crossing: string): i32 {
|
|
3886
|
+
if (crossing == CROSS_WINNER) return max(WINNER_NAMES.indexOf(name as RoundWinner), 0);
|
|
3887
|
+
return teamCell(name);
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
/** The other way: a number a subscriber received, as the name. */
|
|
3891
|
+
function nameOfCell(cell: i32, crossing: string): string {
|
|
3892
|
+
if (crossing == CROSS_WINNER) return cell >= 0 && cell < WINNER_NAMES.length ? WINNER_NAMES[cell] : "none";
|
|
3893
|
+
return cell >= 0 && cell < TEAM_NAMES.length ? TEAM_NAMES[cell] : "UNASSIGNED";
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
/** Whether the argument is a name that crosses as a number. */
|
|
3897
|
+
function crossesAsNumber(crossing: string): bool {
|
|
3898
|
+
return crossing == CROSS_TEAM || crossing == CROSS_WINNER;
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
/** A team's TeamName number: TEAM_UNASSIGNED 0, TERRORIST 1, CT 2, SPECTATOR 3. */
|
|
3902
|
+
function teamCell(team: string): i32 {
|
|
3903
|
+
return max(TEAM_NAMES.indexOf(team as Team), 0);
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
/** The crossing letter of the argument at `index`; `_` past the end. */
|
|
3907
|
+
function crossingAt(crossing: string, index: i32): string {
|
|
3908
|
+
return index < crossing.length ? crossing.charAt(index) : "_";
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
/** Whether a Forward's argument of type T is a Vector: three Floats, whatever the declaration. */
|
|
3912
|
+
function isVector<T>(): bool {
|
|
3913
|
+
return isReference<T>() && !isString<T>() && nameof<T>() == "Vector";
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
/** Whether an array argument of type T crosses as Floats. */
|
|
3917
|
+
function crossesAsFloats<T>(crossing: string): bool {
|
|
3918
|
+
return isVector<T>() || crossing == CROSS_FLOATS;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
/** The FP_* constant for a forward argument of type T. */
|
|
3922
|
+
function forwardParam<T>(crossing: string): i32 {
|
|
3923
|
+
// A plugin's number is an f64, but what a forward carries is a Pawn cell -
|
|
3924
|
+
// a round end's status and every forward like it. Declaring it Float
|
|
3925
|
+
// handed Pawn plugins the bits of a double where they read a whole number,
|
|
3926
|
+
// so a Float is only what the declaration, or the type Float, says is one.
|
|
3927
|
+
if (isArray<T>()) return FP_ARRAY;
|
|
3928
|
+
if (crossing == CROSS_FLOAT) return FP_FLOAT;
|
|
3929
|
+
if (crossesAsNumber(crossing)) return FP_CELL;
|
|
3930
|
+
if (isString<T>()) return FP_STRING;
|
|
3931
|
+
return FP_CELL;
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
/** The value an unused argument stands at: null for a reference, 0 for the rest. */
|
|
3935
|
+
function zeroOf<T>(): T {
|
|
3936
|
+
if (isReference<T>()) return changetype<T>(0);
|
|
3937
|
+
return <T>0;
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
/** CreateMultiForward's FP_* for the argument at `index`, when T is one. */
|
|
3941
|
+
function declareArgument<T>(call: Call, crossing: string, index: i32): void {
|
|
3942
|
+
if (isArgument<T>()) call.ref(forwardParam<T>(crossingAt(crossing, index)));
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
/** A number, a boolean, a Player or a name as the cell Pawn gets. */
|
|
3946
|
+
function cellOf<T>(value: T, crossing: string): i32 {
|
|
3947
|
+
if (isString<T>()) return nameCell(changetype<string>(value), crossing);
|
|
3948
|
+
if (isReference<T>()) return nameof<T>() == "Player" && value ? changetype<Player>(value).id : 0;
|
|
3949
|
+
if (isBoolean<T>()) return value ? 1 : 0;
|
|
3950
|
+
if (crossing == CROSS_FLOAT) return reinterpret<i32>(<f32>value);
|
|
3951
|
+
return <i32>value;
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
/**
|
|
3955
|
+
* What an emit hands the module for this server's TypeScript subscribers
|
|
3956
|
+
* (emit_local): a cell and a letter per argument - `n` a cell, `s` a string,
|
|
3957
|
+
* `a` an array as arrayCells lays it out.
|
|
3958
|
+
*/
|
|
3959
|
+
class LocalArguments {
|
|
3960
|
+
cells: StaticArray<i32> = new StaticArray<i32>(MAX_FORWARD_ARGS);
|
|
3961
|
+
mask: string = "";
|
|
3962
|
+
count: i32 = 0;
|
|
3963
|
+
// The arrays have to outlive the call, as a Call's do.
|
|
3964
|
+
private held: StaticArray<i32>[] = [];
|
|
3965
|
+
|
|
3966
|
+
add(cell: i32, kind: string): void {
|
|
3967
|
+
unchecked(this.cells[this.count++] = cell);
|
|
3968
|
+
this.mask += kind;
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
array(cells: StaticArray<i32>): void {
|
|
3972
|
+
this.held.push(cells);
|
|
3973
|
+
this.add(changetype<i32>(cells), "a");
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
/**
|
|
3978
|
+
* The argument at `index` of an emit: onto ExecuteForward's call the way it
|
|
3979
|
+
* wants it - a number by address, a string, an array PrepareArray makes - and
|
|
3980
|
+
* into what this server's TypeScript subscribers get. Each branch is decided
|
|
3981
|
+
* as it compiles, so only the one for T is built.
|
|
3982
|
+
*/
|
|
3983
|
+
function sendArgument<T>(call: Call, local: LocalArguments, value: T, crossing: string, index: i32): void {
|
|
3984
|
+
if (!isArgument<T>()) return;
|
|
3985
|
+
const cross = crossingAt(crossing, index);
|
|
3986
|
+
|
|
3987
|
+
if (isArray<T>()) {
|
|
3988
|
+
// @ts-ignore: valueof is AssemblyScript's
|
|
3989
|
+
if (!isFloat<valueof<T>>()) ERROR("a Forward's array argument is number[] or a Vector");
|
|
3990
|
+
const values = changetype<number[]>(value);
|
|
3991
|
+
const floats = crossesAsFloats<T>(cross);
|
|
3992
|
+
call.array(values, floats);
|
|
3993
|
+
local.array(arrayCells(values, floats));
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
if (isString<T>() && !crossesAsNumber(cross)) {
|
|
3997
|
+
call.str(changetype<string>(value));
|
|
3998
|
+
local.add(changetype<i32>(value), "s");
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
const cell = cellOf<T>(value, cross);
|
|
4002
|
+
call.ref(cell);
|
|
4003
|
+
local.add(cell, "n");
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
/** The argument at `index` of the forward being delivered, as T - read from the running call (the module's CallArgs). */
|
|
4007
|
+
function argumentOf<T>(crossing: string, index: i32): T {
|
|
4008
|
+
if (!isArgument<T>()) return zeroOf<T>();
|
|
4009
|
+
const cross = crossingAt(crossing, index);
|
|
4010
|
+
|
|
4011
|
+
if (isString<T>()) {
|
|
4012
|
+
if (crossesAsNumber(cross)) return changetype<T>(nameOfCell(__nativeCell(index), cross));
|
|
4013
|
+
return changetype<T>(__nativeString(index));
|
|
4014
|
+
}
|
|
4015
|
+
// A reference's branches are chosen as it runs, so they stay among references.
|
|
4016
|
+
if (isReference<T>()) {
|
|
4017
|
+
if (isVector<T>()) return changetype<T>(__nativeVector(index));
|
|
4018
|
+
if (isArray<T>()) return changetype<T>(__forwardNumbers(index, crossesAsFloats<T>(cross)));
|
|
4019
|
+
return nameof<T>() == "Player" ? changetype<T>(new Player(__nativeCell(index))) : changetype<T>(0);
|
|
4020
|
+
}
|
|
4021
|
+
if (isBoolean<T>()) return <T>(__nativeCell(index) != 0);
|
|
4022
|
+
if (isFloat<T>() && cross == CROSS_FLOAT) return <T>reinterpret<f32>(__nativeCell(index));
|
|
4023
|
+
return <T>__nativeCell(index);
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
/** What subscribe() hangs on: a Forward, reached by its tag - see forwardTrampoline. */
|
|
4027
|
+
abstract class ForwardListener {
|
|
4028
|
+
abstract deliver(): void;
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
/** Every Forward of this plugin that has subscribers; a Forward's tag is its index. */
|
|
4032
|
+
const subscribedForwards: ForwardListener[] = [];
|
|
4033
|
+
|
|
4034
|
+
/**
|
|
4035
|
+
* What the module calls for a forward someone subscribed to.
|
|
4036
|
+
*
|
|
4037
|
+
* One function for every Forward: a trampoline cannot capture the Forward it
|
|
4038
|
+
* belongs to, so the module hands back the tag subscribe() gave it, and the
|
|
4039
|
+
* Forward reads its arguments by its own type arguments.
|
|
4040
|
+
*/
|
|
4041
|
+
function forwardTrampoline(tag: i32): void {
|
|
4042
|
+
if (tag >= 0 && tag < subscribedForwards.length) subscribedForwards[tag].deliver();
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
/**
|
|
4046
|
+
* A forward other plugins listen to, Pawn and TypeScript alike. Its arguments
|
|
4047
|
+
* are its type parameters, up to 32 - as many as AMX Mod X gives a forward:
|
|
4048
|
+
*
|
|
4049
|
+
* ```ts
|
|
4050
|
+
* const roundStart = new Forward("myplugin_on_round_start");
|
|
4051
|
+
* const roundEnd = new Forward<RoundWinner>("myplugin_on_round_end");
|
|
4052
|
+
* const configChanged = new Forward<string, string>("myplugin_on_config_changed");
|
|
4053
|
+
*
|
|
4054
|
+
* roundStart.emit();
|
|
4055
|
+
* roundEnd.emit(winner);
|
|
4056
|
+
* configChanged.emit(id, value);
|
|
4057
|
+
*
|
|
4058
|
+
* const swapped = new Forward<Player, Player>("myplugin_on_player_swapped");
|
|
4059
|
+
* swapped.emit(catcher, caught); // Pawn gets their ids
|
|
4060
|
+
* ```
|
|
4061
|
+
*
|
|
4062
|
+
* A Pawn plugin listens with `public myplugin_on_round_end(winner)`, as usual;
|
|
4063
|
+
* a TypeScript plugin with `subscribe(handler)`. A number, a boolean and a
|
|
4064
|
+
* Player (its `id`) reach Pawn as numbers, a `Float` as a Float, a string as
|
|
4065
|
+
* a string, a `number[]` or a `Vector` as an array. A `Team` or a
|
|
4066
|
+
* `RoundWinner` goes as Pawn's number where an include declares the forward
|
|
4067
|
+
* with that tag; a `Team` for a forward no include declares is a build error.
|
|
4068
|
+
*
|
|
4069
|
+
* The forward is created on its first emit, or earlier by `create()` - once
|
|
4070
|
+
* every plugin has loaded (`plugin_cfg` or later), or those loaded after it
|
|
4071
|
+
* would not hear it.
|
|
4072
|
+
*
|
|
4073
|
+
* Pawn: `CreateMultiForward`, `ExecuteForward`
|
|
4074
|
+
*/
|
|
4075
|
+
export class Forward<T1 = NoArgument, T2 = NoArgument, T3 = NoArgument, T4 = NoArgument, T5 = NoArgument, T6 = NoArgument, T7 = NoArgument, T8 = NoArgument, T9 = NoArgument, T10 = NoArgument, T11 = NoArgument, T12 = NoArgument, T13 = NoArgument, T14 = NoArgument, T15 = NoArgument, T16 = NoArgument, T17 = NoArgument, T18 = NoArgument, T19 = NoArgument, T20 = NoArgument, T21 = NoArgument, T22 = NoArgument, T23 = NoArgument, T24 = NoArgument, T25 = NoArgument, T26 = NoArgument, T27 = NoArgument, T28 = NoArgument, T29 = NoArgument, T30 = NoArgument, T31 = NoArgument, T32 = NoArgument> extends ForwardListener {
|
|
4076
|
+
/** The forward's stopping rule, one of `"never"` (the default) or `"handled"`. Set it before the first emit. */
|
|
4077
|
+
stopWhen: ForwardStop = "never";
|
|
4078
|
+
|
|
4079
|
+
private handle: i32 = -1;
|
|
4080
|
+
private tag: i32 = -1;
|
|
4081
|
+
private handlers: ((a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19, a20: T20, a21: T21, a22: T22, a23: T23, a24: T24, a25: T25, a26: T26, a27: T27, a28: T28, a29: T29, a30: T30, a31: T31, a32: T32) => void)[] = [];
|
|
4082
|
+
|
|
4083
|
+
/**
|
|
4084
|
+
* @param name The forward's name, as Pawn plugins hook it.
|
|
4085
|
+
* @param crossing @internal Written by the build from the forward's Pawn
|
|
4086
|
+
* declaration in an include, or its types; a plugin leaves it out.
|
|
4087
|
+
*/
|
|
4088
|
+
constructor(
|
|
4089
|
+
/** The forward's name, as Pawn plugins listen to it. */
|
|
4090
|
+
public name: string,
|
|
4091
|
+
private crossing: string = ""
|
|
4092
|
+
) {
|
|
4093
|
+
super();
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
/**
|
|
4097
|
+
* Calls `handler` each time the forward is emitted - by this plugin, another
|
|
4098
|
+
* TypeScript plugin or a Pawn plugin:
|
|
4099
|
+
*
|
|
4100
|
+
* ```ts
|
|
4101
|
+
* const greeted = new Forward<string, number>("showcase_on_greeted");
|
|
4102
|
+
* greeted.subscribe((name, count) => console.log(`${name}: ${count}`));
|
|
4103
|
+
* ```
|
|
4104
|
+
*
|
|
4105
|
+
* The handler's parameters take the forward's types; a named function may
|
|
4106
|
+
* take fewer of them. A forward created by a Pawn plugin reaches TypeScript
|
|
4107
|
+
* only when one of the amxts host plugin's includes declares it; one emitted
|
|
4108
|
+
* from TypeScript reaches every subscriber.
|
|
4109
|
+
*/
|
|
4110
|
+
subscribe(handler: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19, a20: T20, a21: T21, a22: T22, a23: T23, a24: T24, a25: T25, a26: T26, a27: T27, a28: T28, a29: T29, a30: T30, a31: T31, a32: T32) => void): void {
|
|
4111
|
+
if (this.tag < 0) {
|
|
4112
|
+
this.tag = subscribedForwards.length;
|
|
4113
|
+
subscribedForwards.push(this);
|
|
4114
|
+
_subscribe(this.name, forwardTrampoline.index, this.tag);
|
|
4115
|
+
}
|
|
4116
|
+
this.handlers.push(handler);
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
/** Stops calling a handler given to `subscribe()`. */
|
|
4120
|
+
unsubscribe(handler: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19, a20: T20, a21: T21, a22: T22, a23: T23, a24: T24, a25: T25, a26: T26, a27: T27, a28: T28, a29: T29, a30: T30, a31: T31, a32: T32) => void): void {
|
|
4121
|
+
const at = this.handlers.indexOf(handler);
|
|
4122
|
+
if (at >= 0) this.handlers.splice(at, 1);
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
/** Passes the forward's arguments to every `subscribe()` handler; the server calls it, not a plugin. */
|
|
4126
|
+
deliver(): void {
|
|
4127
|
+
const a1 = argumentOf<T1>(this.crossing, 0);
|
|
4128
|
+
const a2 = argumentOf<T2>(this.crossing, 1);
|
|
4129
|
+
const a3 = argumentOf<T3>(this.crossing, 2);
|
|
4130
|
+
const a4 = argumentOf<T4>(this.crossing, 3);
|
|
4131
|
+
const a5 = argumentOf<T5>(this.crossing, 4);
|
|
4132
|
+
const a6 = argumentOf<T6>(this.crossing, 5);
|
|
4133
|
+
const a7 = argumentOf<T7>(this.crossing, 6);
|
|
4134
|
+
const a8 = argumentOf<T8>(this.crossing, 7);
|
|
4135
|
+
const a9 = argumentOf<T9>(this.crossing, 8);
|
|
4136
|
+
const a10 = argumentOf<T10>(this.crossing, 9);
|
|
4137
|
+
const a11 = argumentOf<T11>(this.crossing, 10);
|
|
4138
|
+
const a12 = argumentOf<T12>(this.crossing, 11);
|
|
4139
|
+
const a13 = argumentOf<T13>(this.crossing, 12);
|
|
4140
|
+
const a14 = argumentOf<T14>(this.crossing, 13);
|
|
4141
|
+
const a15 = argumentOf<T15>(this.crossing, 14);
|
|
4142
|
+
const a16 = argumentOf<T16>(this.crossing, 15);
|
|
4143
|
+
const a17 = argumentOf<T17>(this.crossing, 16);
|
|
4144
|
+
const a18 = argumentOf<T18>(this.crossing, 17);
|
|
4145
|
+
const a19 = argumentOf<T19>(this.crossing, 18);
|
|
4146
|
+
const a20 = argumentOf<T20>(this.crossing, 19);
|
|
4147
|
+
const a21 = argumentOf<T21>(this.crossing, 20);
|
|
4148
|
+
const a22 = argumentOf<T22>(this.crossing, 21);
|
|
4149
|
+
const a23 = argumentOf<T23>(this.crossing, 22);
|
|
4150
|
+
const a24 = argumentOf<T24>(this.crossing, 23);
|
|
4151
|
+
const a25 = argumentOf<T25>(this.crossing, 24);
|
|
4152
|
+
const a26 = argumentOf<T26>(this.crossing, 25);
|
|
4153
|
+
const a27 = argumentOf<T27>(this.crossing, 26);
|
|
4154
|
+
const a28 = argumentOf<T28>(this.crossing, 27);
|
|
4155
|
+
const a29 = argumentOf<T29>(this.crossing, 28);
|
|
4156
|
+
const a30 = argumentOf<T30>(this.crossing, 29);
|
|
4157
|
+
const a31 = argumentOf<T31>(this.crossing, 30);
|
|
4158
|
+
const a32 = argumentOf<T32>(this.crossing, 31);
|
|
4159
|
+
// A copy: a handler that unsubscribes itself must not make the next one skip.
|
|
4160
|
+
const handlers = this.handlers.slice(0);
|
|
4161
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
4162
|
+
handlers[i](a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32);
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
/**
|
|
4167
|
+
* Creates the forward now rather than on its first emit; later calls do nothing.
|
|
4168
|
+
*
|
|
4169
|
+
* Pawn: `CreateMultiForward`
|
|
4170
|
+
*/
|
|
4171
|
+
create(): void {
|
|
4172
|
+
if (this.handle >= 0) return;
|
|
4173
|
+
|
|
4174
|
+
const call = new Call(NATIVE_CreateMultiForward)
|
|
4175
|
+
.str(this.name)
|
|
4176
|
+
.num(this.stopWhen == "handled" ? ET_STOP : ET_IGNORE);
|
|
4177
|
+
declareArgument<T1>(call, this.crossing, 0);
|
|
4178
|
+
declareArgument<T2>(call, this.crossing, 1);
|
|
4179
|
+
declareArgument<T3>(call, this.crossing, 2);
|
|
4180
|
+
declareArgument<T4>(call, this.crossing, 3);
|
|
4181
|
+
declareArgument<T5>(call, this.crossing, 4);
|
|
4182
|
+
declareArgument<T6>(call, this.crossing, 5);
|
|
4183
|
+
declareArgument<T7>(call, this.crossing, 6);
|
|
4184
|
+
declareArgument<T8>(call, this.crossing, 7);
|
|
4185
|
+
declareArgument<T9>(call, this.crossing, 8);
|
|
4186
|
+
declareArgument<T10>(call, this.crossing, 9);
|
|
4187
|
+
declareArgument<T11>(call, this.crossing, 10);
|
|
4188
|
+
declareArgument<T12>(call, this.crossing, 11);
|
|
4189
|
+
declareArgument<T13>(call, this.crossing, 12);
|
|
4190
|
+
declareArgument<T14>(call, this.crossing, 13);
|
|
4191
|
+
declareArgument<T15>(call, this.crossing, 14);
|
|
4192
|
+
declareArgument<T16>(call, this.crossing, 15);
|
|
4193
|
+
declareArgument<T17>(call, this.crossing, 16);
|
|
4194
|
+
declareArgument<T18>(call, this.crossing, 17);
|
|
4195
|
+
declareArgument<T19>(call, this.crossing, 18);
|
|
4196
|
+
declareArgument<T20>(call, this.crossing, 19);
|
|
4197
|
+
declareArgument<T21>(call, this.crossing, 20);
|
|
4198
|
+
declareArgument<T22>(call, this.crossing, 21);
|
|
4199
|
+
declareArgument<T23>(call, this.crossing, 22);
|
|
4200
|
+
declareArgument<T24>(call, this.crossing, 23);
|
|
4201
|
+
declareArgument<T25>(call, this.crossing, 24);
|
|
4202
|
+
declareArgument<T26>(call, this.crossing, 25);
|
|
4203
|
+
declareArgument<T27>(call, this.crossing, 26);
|
|
4204
|
+
declareArgument<T28>(call, this.crossing, 27);
|
|
4205
|
+
declareArgument<T29>(call, this.crossing, 28);
|
|
4206
|
+
declareArgument<T30>(call, this.crossing, 29);
|
|
4207
|
+
declareArgument<T31>(call, this.crossing, 30);
|
|
4208
|
+
declareArgument<T32>(call, this.crossing, 31);
|
|
4209
|
+
this.handle = call.run();
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
/**
|
|
4213
|
+
* Sends the forward to every plugin that listens to it; `true` if it went out.
|
|
4214
|
+
*
|
|
4215
|
+
* Pawn: `ExecuteForward`
|
|
4216
|
+
*/
|
|
4217
|
+
emit(
|
|
4218
|
+
a1: T1 = zeroOf<T1>(),
|
|
4219
|
+
a2: T2 = zeroOf<T2>(),
|
|
4220
|
+
a3: T3 = zeroOf<T3>(),
|
|
4221
|
+
a4: T4 = zeroOf<T4>(),
|
|
4222
|
+
a5: T5 = zeroOf<T5>(),
|
|
4223
|
+
a6: T6 = zeroOf<T6>(),
|
|
4224
|
+
a7: T7 = zeroOf<T7>(),
|
|
4225
|
+
a8: T8 = zeroOf<T8>(),
|
|
4226
|
+
a9: T9 = zeroOf<T9>(),
|
|
4227
|
+
a10: T10 = zeroOf<T10>(),
|
|
4228
|
+
a11: T11 = zeroOf<T11>(),
|
|
4229
|
+
a12: T12 = zeroOf<T12>(),
|
|
4230
|
+
a13: T13 = zeroOf<T13>(),
|
|
4231
|
+
a14: T14 = zeroOf<T14>(),
|
|
4232
|
+
a15: T15 = zeroOf<T15>(),
|
|
4233
|
+
a16: T16 = zeroOf<T16>(),
|
|
4234
|
+
a17: T17 = zeroOf<T17>(),
|
|
4235
|
+
a18: T18 = zeroOf<T18>(),
|
|
4236
|
+
a19: T19 = zeroOf<T19>(),
|
|
4237
|
+
a20: T20 = zeroOf<T20>(),
|
|
4238
|
+
a21: T21 = zeroOf<T21>(),
|
|
4239
|
+
a22: T22 = zeroOf<T22>(),
|
|
4240
|
+
a23: T23 = zeroOf<T23>(),
|
|
4241
|
+
a24: T24 = zeroOf<T24>(),
|
|
4242
|
+
a25: T25 = zeroOf<T25>(),
|
|
4243
|
+
a26: T26 = zeroOf<T26>(),
|
|
4244
|
+
a27: T27 = zeroOf<T27>(),
|
|
4245
|
+
a28: T28 = zeroOf<T28>(),
|
|
4246
|
+
a29: T29 = zeroOf<T29>(),
|
|
4247
|
+
a30: T30 = zeroOf<T30>(),
|
|
4248
|
+
a31: T31 = zeroOf<T31>(),
|
|
4249
|
+
a32: T32 = zeroOf<T32>()
|
|
4250
|
+
): boolean {
|
|
4251
|
+
this.create();
|
|
4252
|
+
if (this.handle < 0) return false;
|
|
4253
|
+
|
|
4254
|
+
const call = new Call(NATIVE_ExecuteForward).num(this.handle).ref(0);
|
|
4255
|
+
const local = new LocalArguments();
|
|
4256
|
+
sendArgument<T1>(call, local, a1, this.crossing, 0);
|
|
4257
|
+
sendArgument<T2>(call, local, a2, this.crossing, 1);
|
|
4258
|
+
sendArgument<T3>(call, local, a3, this.crossing, 2);
|
|
4259
|
+
sendArgument<T4>(call, local, a4, this.crossing, 3);
|
|
4260
|
+
sendArgument<T5>(call, local, a5, this.crossing, 4);
|
|
4261
|
+
sendArgument<T6>(call, local, a6, this.crossing, 5);
|
|
4262
|
+
sendArgument<T7>(call, local, a7, this.crossing, 6);
|
|
4263
|
+
sendArgument<T8>(call, local, a8, this.crossing, 7);
|
|
4264
|
+
sendArgument<T9>(call, local, a9, this.crossing, 8);
|
|
4265
|
+
sendArgument<T10>(call, local, a10, this.crossing, 9);
|
|
4266
|
+
sendArgument<T11>(call, local, a11, this.crossing, 10);
|
|
4267
|
+
sendArgument<T12>(call, local, a12, this.crossing, 11);
|
|
4268
|
+
sendArgument<T13>(call, local, a13, this.crossing, 12);
|
|
4269
|
+
sendArgument<T14>(call, local, a14, this.crossing, 13);
|
|
4270
|
+
sendArgument<T15>(call, local, a15, this.crossing, 14);
|
|
4271
|
+
sendArgument<T16>(call, local, a16, this.crossing, 15);
|
|
4272
|
+
sendArgument<T17>(call, local, a17, this.crossing, 16);
|
|
4273
|
+
sendArgument<T18>(call, local, a18, this.crossing, 17);
|
|
4274
|
+
sendArgument<T19>(call, local, a19, this.crossing, 18);
|
|
4275
|
+
sendArgument<T20>(call, local, a20, this.crossing, 19);
|
|
4276
|
+
sendArgument<T21>(call, local, a21, this.crossing, 20);
|
|
4277
|
+
sendArgument<T22>(call, local, a22, this.crossing, 21);
|
|
4278
|
+
sendArgument<T23>(call, local, a23, this.crossing, 22);
|
|
4279
|
+
sendArgument<T24>(call, local, a24, this.crossing, 23);
|
|
4280
|
+
sendArgument<T25>(call, local, a25, this.crossing, 24);
|
|
4281
|
+
sendArgument<T26>(call, local, a26, this.crossing, 25);
|
|
4282
|
+
sendArgument<T27>(call, local, a27, this.crossing, 26);
|
|
4283
|
+
sendArgument<T28>(call, local, a28, this.crossing, 27);
|
|
4284
|
+
sendArgument<T29>(call, local, a29, this.crossing, 28);
|
|
4285
|
+
sendArgument<T30>(call, local, a30, this.crossing, 29);
|
|
4286
|
+
sendArgument<T31>(call, local, a31, this.crossing, 30);
|
|
4287
|
+
sendArgument<T32>(call, local, a32, this.crossing, 31);
|
|
4288
|
+
const sent = call.run() != 0;
|
|
4289
|
+
|
|
4290
|
+
// TypeScript subscribers, which AMX Mod X reaches only through a host
|
|
4291
|
+
// public - the module skips the forwards it has one for.
|
|
4292
|
+
_emitLocal(this.name, local.mask, changetype<i32>(local.cells), local.count);
|
|
4293
|
+
return sent;
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
// ---------------------------------------------------------------- storage
|
|
4298
|
+
|
|
4299
|
+
/**
|
|
4300
|
+
* A key-to-text store on disk: a Map that survives a map change and a server
|
|
4301
|
+
* restart.
|
|
4302
|
+
*
|
|
4303
|
+
* ```ts
|
|
4304
|
+
* const demos = new Storage("core_demo_counters");
|
|
4305
|
+
* const last = demos.get(auth); // string | null
|
|
4306
|
+
* demos.set(auth, "3");
|
|
4307
|
+
* if (demos.has(auth)) ...
|
|
4308
|
+
* demos.delete(auth);
|
|
4309
|
+
* ```
|
|
4310
|
+
*
|
|
4311
|
+
* The file is named after the storage, kept in the `vault` folder of the AMX
|
|
4312
|
+
* Mod X data folder, and opened on first use. Values are text: a number goes
|
|
4313
|
+
* in with `toString()` and comes out with `parseInt`.
|
|
4314
|
+
*
|
|
4315
|
+
* Pawn: `nvault_open`, `nvault_get`, `nvault_set`, `nvault_remove`
|
|
4316
|
+
*/
|
|
4317
|
+
export class Storage {
|
|
4318
|
+
private vault: i32 = -1;
|
|
4319
|
+
|
|
4320
|
+
constructor(
|
|
4321
|
+
/** The storage's name, which is also its file's name. */
|
|
4322
|
+
public name: string
|
|
4323
|
+
) {}
|
|
4324
|
+
|
|
4325
|
+
/** The value under `key`, or `null` when there is none. */
|
|
4326
|
+
get(key: string): string | null {
|
|
4327
|
+
const vault = this.open();
|
|
4328
|
+
if (vault < 0) return null;
|
|
4329
|
+
|
|
4330
|
+
const value = new CellBuffer(TEXT_MAX + 1);
|
|
4331
|
+
const stamp = new CellBuffer(1);
|
|
4332
|
+
|
|
4333
|
+
// nvault_lookup rather than nvault_get: the length of nvault_get's
|
|
4334
|
+
// buffer rides in its `...` tail by address, which the dispatcher's
|
|
4335
|
+
// buffer argument cannot say; nvault_lookup takes it as a plain
|
|
4336
|
+
// argument, and answers whether the key exists besides.
|
|
4337
|
+
const found = _nvaultLookup(vault, changetype<i32>(__cellsOf(key)), value.address, TEXT_MAX, stamp.address);
|
|
4338
|
+
return found != 0 ? value.text() : null;
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
/** Puts `value` under `key`, replacing what was there. */
|
|
4342
|
+
set(key: string, value: string): void {
|
|
4343
|
+
const vault = this.open();
|
|
4344
|
+
if (vault >= 0) nvault_set(vault, key, value);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
/** `true` when there is a value under `key`. */
|
|
4348
|
+
has(key: string): boolean {
|
|
4349
|
+
return this.get(key) != null;
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
/** Removes `key` and its value; a key that is not there is ignored. */
|
|
4353
|
+
delete(key: string): void {
|
|
4354
|
+
const vault = this.open();
|
|
4355
|
+
if (vault >= 0) nvault_remove(vault, key);
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
private open(): i32 {
|
|
4359
|
+
if (this.vault < 0) this.vault = nvault_open(this.name);
|
|
4360
|
+
return this.vault;
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
// Flag enums and the array that stands for a mask - generated, see scripts/generate-flags.ts.
|
|
4365
|
+
export * from "./flags";
|
|
4366
|
+
import { Access, ACCESS, HIDE_HUD, HideHud } from "./flags";
|
|
4367
|
+
|
|
4368
|
+
// Hookchain events and game.addEventListener's hood - generated, see scripts/generate-hooks.ts.
|
|
4369
|
+
export * from "./hooks";
|
|
4370
|
+
import { GameAnswerMap, GameEventMap, addGameListener, removeGameListener } from "./hooks";
|
|
4371
|
+
|
|
4372
|
+
// Three numbers with vector math - what entity.origin and the rest return.
|
|
4373
|
+
export * from "./vector";
|
|
4374
|
+
export * from "./effects";
|
|
4375
|
+
export { EntityFilter } from "./entities";
|
|
4376
|
+
|
|
4377
|
+
// =============================================================================
|
|
4378
|
+
// What modules call through the kit (as/kit.ts): another plugin's public by
|
|
4379
|
+
// name, AMX Mod X `Array:` rows, and an old-style menu. Hood, like the rest of
|
|
4380
|
+
// this file.
|
|
4381
|
+
// =============================================================================
|
|
4382
|
+
|
|
4383
|
+
// get_user_msgid, message_begin, message_end, write_byte and write_short are
|
|
4384
|
+
// imported at the top of the file.
|
|
4385
|
+
import {
|
|
4386
|
+
callfunc_begin_i, callfunc_push_int, get_func_id, show_menu, write_string, ArrayGetArray, ArrayDestroy
|
|
4387
|
+
} from "./natives";
|
|
4388
|
+
|
|
4389
|
+
/**
|
|
4390
|
+
* A public function of another plugin - a Pawn plugin's public, or a
|
|
4391
|
+
* TypeScript plugin's `publicFor` name - to call from here:
|
|
4392
|
+
*
|
|
4393
|
+
* ```ts
|
|
4394
|
+
* const fn = PawnFunction.find(caller(), "OnAction"); // null when there is none
|
|
4395
|
+
* if (fn != null) fn.call().int(id).text("KEY").run();
|
|
4396
|
+
* const call = fn.call().int(id).int(target).buffer(256).int(255);
|
|
4397
|
+
* call.run();
|
|
4398
|
+
* const value = call.bufferText; // what it wrote into value[]
|
|
4399
|
+
* ```
|
|
4400
|
+
*
|
|
4401
|
+
* Pawn: `get_func_id`, `callfunc_begin_i`
|
|
4402
|
+
*/
|
|
4403
|
+
export class PawnFunction {
|
|
4404
|
+
constructor(
|
|
4405
|
+
/** The `id` of the plugin the function belongs to. */
|
|
4406
|
+
readonly plugin: i32,
|
|
4407
|
+
/**
|
|
4408
|
+
* The function's index in its plugin.
|
|
4409
|
+
*
|
|
4410
|
+
* Pawn: `get_func_id`
|
|
4411
|
+
*/
|
|
4412
|
+
readonly index: i32
|
|
4413
|
+
) {}
|
|
4414
|
+
|
|
4415
|
+
/**
|
|
4416
|
+
* Finds the public `name` in the plugin with this `id` (a native's `caller()`);
|
|
4417
|
+
* `null` when the plugin has no such public.
|
|
4418
|
+
*
|
|
4419
|
+
* Pawn: `get_func_id`
|
|
4420
|
+
*/
|
|
4421
|
+
static find(plugin: number, name: string): PawnFunction | null {
|
|
4422
|
+
const index = get_func_id(name, plugin);
|
|
4423
|
+
return index >= 0 ? new PawnFunction(<i32>plugin, <i32>index) : null;
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
/** Starts a call of the function: add its arguments in order, then `run()`. */
|
|
4427
|
+
call(): PawnCall {
|
|
4428
|
+
return new PawnCall(this);
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
const PAWN_INT: u8 = 0;
|
|
4433
|
+
const PAWN_TEXT: u8 = 1;
|
|
4434
|
+
const PAWN_BUFFER: u8 = 2;
|
|
4435
|
+
|
|
4436
|
+
// @ts-ignore: decorator
|
|
4437
|
+
@external("env", "callfunc_text") declare function _callfuncText(text: string): i32;
|
|
4438
|
+
// @ts-ignore: decorator
|
|
4439
|
+
@external("env", "callfunc_buffer") declare function _callfuncBuffer(cells: usize, count: i32): i32;
|
|
4440
|
+
// @ts-ignore: decorator
|
|
4441
|
+
@external("env", "callfunc_finish") declare function _callfuncFinish(): i32;
|
|
4442
|
+
|
|
4443
|
+
/**
|
|
4444
|
+
* One call of a PawnFunction: its arguments in order, then `run()`. Any
|
|
4445
|
+
* number of strings and arrays may be passed, and what the function writes
|
|
4446
|
+
* into a `buffer()` is read afterwards from `bufferText`.
|
|
4447
|
+
*
|
|
4448
|
+
* Pawn: `callfunc_push_int`, `callfunc_push_str`, `callfunc_push_array`, `callfunc_end`
|
|
4449
|
+
*/
|
|
4450
|
+
export class PawnCall {
|
|
4451
|
+
private kinds: u8[] = [];
|
|
4452
|
+
private ints: i32[] = [];
|
|
4453
|
+
private texts: string[] = [];
|
|
4454
|
+
private cells: StaticArray<i32> | null = null;
|
|
4455
|
+
/** The text the function wrote into its `buffer()`, once `run()` is over. */
|
|
4456
|
+
bufferText: string = "";
|
|
4457
|
+
|
|
4458
|
+
constructor(private fn: PawnFunction) {}
|
|
4459
|
+
|
|
4460
|
+
/** Adds a number argument. */
|
|
4461
|
+
int(value: number): PawnCall {
|
|
4462
|
+
return this.add(PAWN_INT, <i32>value, "");
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
/** Adds a boolean argument; the function gets `1` or `0`. */
|
|
4466
|
+
bool(value: boolean): PawnCall {
|
|
4467
|
+
return this.add(PAWN_INT, value ? 1 : 0, "");
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
/** Adds a string argument. */
|
|
4471
|
+
text(value: string): PawnCall {
|
|
4472
|
+
return this.add(PAWN_TEXT, 0, value);
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
/** Adds an array of `size` cells for the function to fill - its `value[]`; one per call. The text is then in `bufferText`. */
|
|
4476
|
+
buffer(size: number): PawnCall {
|
|
4477
|
+
return this.add(PAWN_BUFFER, <i32>size, "");
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
/** Calls the function and returns its result, or `0` when it could not be called. */
|
|
4481
|
+
run(): number {
|
|
4482
|
+
if (callfunc_begin_i(this.fn.index, this.fn.plugin) != 1) return 0;
|
|
4483
|
+
|
|
4484
|
+
for (let i = 0; i < this.kinds.length; i++) {
|
|
4485
|
+
const kind = unchecked(this.kinds[i]);
|
|
4486
|
+
if (kind == PAWN_INT) callfunc_push_int(unchecked(this.ints[i]));
|
|
4487
|
+
else if (kind == PAWN_TEXT) _callfuncText(unchecked(this.texts[i]));
|
|
4488
|
+
else {
|
|
4489
|
+
const cells = new StaticArray<i32>(unchecked(this.ints[i]));
|
|
4490
|
+
this.cells = cells;
|
|
4491
|
+
_callfuncBuffer(changetype<usize>(cells), cells.length);
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
const result = _callfuncFinish();
|
|
4496
|
+
const filled = this.cells;
|
|
4497
|
+
if (filled != null) this.bufferText = __cellText(changetype<usize>(filled), filled.length);
|
|
4498
|
+
return result;
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
private add(kind: u8, value: i32, text: string): PawnCall {
|
|
4502
|
+
this.kinds.push(kind);
|
|
4503
|
+
this.ints.push(value);
|
|
4504
|
+
this.texts.push(text);
|
|
4505
|
+
return this;
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
/**
|
|
4510
|
+
* Creates an AMX Mod X `Array:` of `cellSize` cells an item and returns its handle.
|
|
4511
|
+
*
|
|
4512
|
+
* Pawn: `ArrayCreate`
|
|
4513
|
+
*/
|
|
4514
|
+
export function createCellArray(cellSize: number): number {
|
|
4515
|
+
return _arrayCreate(<i32>cellSize, 32);
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
/**
|
|
4519
|
+
* Frees an `Array:` made by `createCellArray`; its handle is no good afterwards.
|
|
4520
|
+
*
|
|
4521
|
+
* Pawn: `ArrayDestroy`
|
|
4522
|
+
*/
|
|
4523
|
+
export function destroyCellArray(handle: number): void {
|
|
4524
|
+
ArrayDestroy(handle);
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
/**
|
|
4528
|
+
* Reads every item of an `Array:` of `cellSize` cells an item, each as an array of numbers.
|
|
4529
|
+
*
|
|
4530
|
+
* Pawn: `ArrayGetArray`
|
|
4531
|
+
*/
|
|
4532
|
+
export function cellArrayRows(handle: number, cellSize: number): number[][] {
|
|
4533
|
+
const rows: number[][] = [];
|
|
4534
|
+
const count = _arraySize(<i32>handle);
|
|
4535
|
+
const cells = new StaticArray<i32>(<i32>cellSize);
|
|
4536
|
+
for (let i = 0; i < count; i++) {
|
|
4537
|
+
ArrayGetArray(<i32>handle, i, changetype<i32>(cells), <i32>cellSize);
|
|
4538
|
+
const row: number[] = [];
|
|
4539
|
+
for (let k = 0; k < cells.length; k++) row.push(unchecked(cells[k]));
|
|
4540
|
+
rows.push(row);
|
|
4541
|
+
}
|
|
4542
|
+
return rows;
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
/**
|
|
4546
|
+
* Adds one item, an array of numbers, to the end of an `Array:`.
|
|
4547
|
+
*
|
|
4548
|
+
* Pawn: `ArrayPushArray`
|
|
4549
|
+
*/
|
|
4550
|
+
export function pushCellArrayRow(handle: number, row: number[]): void {
|
|
4551
|
+
const cells = new StaticArray<i32>(row.length);
|
|
4552
|
+
for (let i = 0; i < row.length; i++) unchecked(cells[i] = <i32>row[i]);
|
|
4553
|
+
_arrayPushArray(<i32>handle, changetype<usize>(cells), row.length);
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
/** Reads a Pawn string from `count` cells of a row, from `start`: a UTF-8 byte a cell, up to the first zero. */
|
|
4557
|
+
export function cellsText(row: number[], start: number, count: number): string {
|
|
4558
|
+
const cells = new StaticArray<i32>(<i32>count + 1);
|
|
4559
|
+
for (let i = 0; i < <i32>count && <i32>start + i < row.length; i++) unchecked(cells[i] = <i32>row[<i32>start + i]);
|
|
4560
|
+
return __cellText(changetype<usize>(cells), <i32>count);
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
/** Writes text as `count` cells of a Pawn string: at most `count - 1` bytes, never half a letter, the rest zeros. */
|
|
4564
|
+
export function textCells(text: string, count: number): number[] {
|
|
4565
|
+
const cells = new StaticArray<i32>(<i32>count);
|
|
4566
|
+
__writeCellText(text, cells);
|
|
4567
|
+
const row: number[] = [];
|
|
4568
|
+
for (let i = 0; i < cells.length; i++) row.push(unchecked(cells[i]));
|
|
4569
|
+
return row;
|
|
4570
|
+
}
|
|
4571
|
+
|
|
4572
|
+
/** Turns a menu's colour tags (`!y`, `!R`, ...) into the codes the game draws, and drops the ones only chat has (`!g`, `!b`, `!t`) and the game's own codes written into the text (`\y`); any other `!` stays. `showMenu` calls it, and a module hands its result to Pawn. */
|
|
4573
|
+
export function menuColors(text: string): string {
|
|
4574
|
+
if (!text.includes("!") && !text.includes("\\")) return text;
|
|
4575
|
+
|
|
4576
|
+
let out = "";
|
|
4577
|
+
|
|
4578
|
+
for (let i = 0; i < text.length; i++) {
|
|
4579
|
+
const tag = i + 1 < text.length ? text.charAt(i + 1) : "";
|
|
4580
|
+
|
|
4581
|
+
// Text is written with tags: a code is Pawn's, and a module turns Pawn's text into tags first.
|
|
4582
|
+
if (text.charAt(i) == "\\" && tag.length > 0 && MENU_TAGS.includes(tag)) {
|
|
4583
|
+
i++;
|
|
4584
|
+
continue;
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4587
|
+
if (text.charAt(i) != "!" || tag.length == 0 || !(CHAT_TAGS.includes(tag) || MENU_TAGS.includes(tag))) {
|
|
4588
|
+
out += text.charAt(i);
|
|
4589
|
+
continue;
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
i++;
|
|
4593
|
+
|
|
4594
|
+
// The game's own code is the same letter: `!y` is `\y`.
|
|
4595
|
+
if (MENU_TAGS.includes(tag)) out += "\\" + tag;
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
return out;
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
/**
|
|
4602
|
+
* Text from Pawn - a dictionary's line, a Pawn plugin's argument - with its
|
|
4603
|
+
* colour codes made tags: a menu's `\y` `\r` `\d` `\w` `\R` are `!y` `!r`
|
|
4604
|
+
* `!d` `!w` `!R`, and chat's bytes `^1` `^3` `^4` are `!y` `!t` `!g`.
|
|
4605
|
+
*/
|
|
4606
|
+
export function colorTags(text: string): string {
|
|
4607
|
+
let out = "";
|
|
4608
|
+
|
|
4609
|
+
for (let i = 0; i < text.length; i++) {
|
|
4610
|
+
const c = text.charAt(i);
|
|
4611
|
+
const tag = i + 1 < text.length ? text.charAt(i + 1) : "";
|
|
4612
|
+
|
|
4613
|
+
if (c == "\\" && tag.length > 0 && MENU_TAGS.includes(tag)) {
|
|
4614
|
+
out += "!" + tag;
|
|
4615
|
+
i++;
|
|
4616
|
+
} else if (c == "\x01") out += "!y";
|
|
4617
|
+
else if (c == "\x03") out += "!t";
|
|
4618
|
+
else if (c == "\x04") out += "!g";
|
|
4619
|
+
else out += c;
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
return out;
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
/**
|
|
4626
|
+
* Shows a player an old-style menu of any length: `keys` are the keys it
|
|
4627
|
+
* accepts, `title` the name its key presses come back under.
|
|
4628
|
+
*
|
|
4629
|
+
* Colour tags, the same letters as in chat: `!y` yellow, `!r` red, `!d` grey,
|
|
4630
|
+
* `!w` white, `!R` to the right edge. Chat's own tags (`!g`, `!b`, `!t`)
|
|
4631
|
+
* are dropped, and so are the game's own codes (`\y`): text is written with tags.
|
|
4632
|
+
*
|
|
4633
|
+
* Pawn: `show_menu`, `register_menucmd`
|
|
4634
|
+
*/
|
|
4635
|
+
export function showMenu(id: number, keys: number, text: string, title: string): void {
|
|
4636
|
+
// show_menu takes the text whole and sends it in ShowMenu messages of the
|
|
4637
|
+
// game's 175 bytes itself.
|
|
4638
|
+
show_menu(id, keys, menuColors(text), -1, title);
|
|
4639
|
+
}
|