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