@s2script/sdk 0.30.3 → 0.31.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/dist/cli.js +756 -526
- package/entity.d.ts +9 -1
- package/ext/s2ext.h +155 -0
- package/package.json +3 -2
- package/unsafe.d.ts +142 -0
package/entity.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* NO runtime code: the engine injects the implementation at load time.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Vector } from "./math";
|
|
6
|
+
import type { Vector, QAngle } from "./math";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A host-liveness-gated handle to a live entity. `id` is a HOST-MINTED monotonic liveness
|
|
@@ -88,6 +88,14 @@ export declare class EntityRef {
|
|
|
88
88
|
* at `finalOff` into a number[]. All in-core (raw pointers never cross); null if the root is stale or any hop
|
|
89
89
|
* is null. */
|
|
90
90
|
readFloatsChain(ptrOffs: number[], finalOff: number, count: number): number[] | null;
|
|
91
|
+
/** The `Vector` at `offset`, or null if the ref is stale. */
|
|
92
|
+
readVector(offset: number): Vector | null;
|
|
93
|
+
/** The `QAngle` at `offset`, or null if the ref is stale. */
|
|
94
|
+
readQAngle(offset: number): QAngle | null;
|
|
95
|
+
/** The `Vector` at the end of a pointer chain, or null on a stale ref or a null hop. */
|
|
96
|
+
readVectorChain(ptrOffs: number[], finalOff: number): Vector | null;
|
|
97
|
+
/** The `QAngle` at the end of a pointer chain, or null on a stale ref or a null hop. */
|
|
98
|
+
readQAngleChain(ptrOffs: number[], finalOff: number): QAngle | null;
|
|
91
99
|
/** Follow a pointer chain (each an offset), then read a scalar at `finalOff`. null if the root is stale or any
|
|
92
100
|
* hop is null. `readHandleVia` decodes a handle field → a liveness-gated EntityRef; vectors use readFloatsChain. */
|
|
93
101
|
readInt32Via(pathOffs: number[], finalOff: number): number | null;
|
package/ext/s2ext.h
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* s2script native extension ABI, version 1.
|
|
2
|
+
*
|
|
3
|
+
* A native extension is a shared library in addons/s2script/extensions/ that adds natives to
|
|
4
|
+
* s2script without a core change, the way SourceMod extensions do. It never sees V8 or Rust:
|
|
5
|
+
* it registers plain C functions with a declared signature, and the host binds each one as a
|
|
6
|
+
* JavaScript function. A plugin that declares the extension in package.json imports it by name:
|
|
7
|
+
*
|
|
8
|
+
* "s2script": { "extensions": { "@community/fastmath": "^0.1.0" } }
|
|
9
|
+
* import { clamp } from "@community/fastmath";
|
|
10
|
+
*
|
|
11
|
+
* The rules:
|
|
12
|
+
*
|
|
13
|
+
* - Export two functions, s2ext_info and s2ext_init. The host calls s2ext_info first and refuses
|
|
14
|
+
* the extension (with a named reason, while the server keeps running) if its ABI version is
|
|
15
|
+
* not one the host speaks. Then it calls s2ext_init once, on the game thread, where the
|
|
16
|
+
* extension registers its natives.
|
|
17
|
+
* - Every native's C function takes an opaque receiver first, then its declared arguments:
|
|
18
|
+
* int32_t clamp(void* recv, int32_t v, int32_t lo, int32_t hi);
|
|
19
|
+
* The receiver is meaningless to the extension; it is there because it is the shape V8's fast
|
|
20
|
+
* API calls, which lets the host point optimised JavaScript straight at your function.
|
|
21
|
+
* - Natives run on the game thread. A native whose types are all numbers or booleans, and that
|
|
22
|
+
* does not set S2N_CAN_FAIL, is a fast native: optimised code calls it directly, so it must not
|
|
23
|
+
* block, must not call back into the host except log and schema_offset, and must not keep the
|
|
24
|
+
* receiver.
|
|
25
|
+
* - Extensions load at server start and are never unloaded.
|
|
26
|
+
* - Structs only grow by appending fields, and each carries its size, so an older or newer peer
|
|
27
|
+
* is detected without a version bump. A real break bumps S2EXT_ABI_VERSION.
|
|
28
|
+
* - Native code runs with full access, the same trust as a Metamod plugin. Engine access goes
|
|
29
|
+
* through the host services below, never through engine tables directly.
|
|
30
|
+
*
|
|
31
|
+
* Supported platform for ABI 1: x86-64 Linux (the System V calling convention).
|
|
32
|
+
*/
|
|
33
|
+
#ifndef S2EXT_H
|
|
34
|
+
#define S2EXT_H
|
|
35
|
+
|
|
36
|
+
#include <stdint.h>
|
|
37
|
+
#ifndef __cplusplus
|
|
38
|
+
#include <stdbool.h>
|
|
39
|
+
#endif
|
|
40
|
+
|
|
41
|
+
#ifdef __cplusplus
|
|
42
|
+
extern "C" {
|
|
43
|
+
#endif
|
|
44
|
+
|
|
45
|
+
#define S2EXT_ABI_VERSION 1u
|
|
46
|
+
|
|
47
|
+
/* Argument and result types, and how each crosses.
|
|
48
|
+
*
|
|
49
|
+
* type C type JavaScript notes
|
|
50
|
+
* S2T_VOID void undefined result only
|
|
51
|
+
* S2T_BOOL bool boolean arguments convert as JS truthiness
|
|
52
|
+
* S2T_I32 int32_t number arguments convert as ToInt32
|
|
53
|
+
* S2T_U32 uint32_t number arguments convert as ToUint32
|
|
54
|
+
* S2T_F32 float number
|
|
55
|
+
* S2T_F64 double number
|
|
56
|
+
* S2T_STRING const char* string UTF-8, NUL-terminated. An argument is
|
|
57
|
+
* valid only during the call, and a string
|
|
58
|
+
* with an embedded NUL is refused. A result
|
|
59
|
+
* is copied at once and must stay valid until
|
|
60
|
+
* your next native returns; NULL gives null.
|
|
61
|
+
* S2T_ENTITY void* EntityRef argument only: the live entity's address,
|
|
62
|
+
* valid only during the call. A stale ref
|
|
63
|
+
* throws S2Error code "stale_entity" and
|
|
64
|
+
* your function is not called.
|
|
65
|
+
*
|
|
66
|
+
* Limits per native: at most 5 arguments of the integer class (bool, i32, u32, string, entity)
|
|
67
|
+
* and at most 8 of the float class (f32, f64), so every argument travels in a register.
|
|
68
|
+
* A native with a string or entity anywhere, or with S2N_CAN_FAIL, is called through the host's
|
|
69
|
+
* generic path rather than directly. That path costs roughly 100-150 ns per call, against
|
|
70
|
+
* under 10 ns for a fast native.
|
|
71
|
+
*/
|
|
72
|
+
typedef enum S2Type {
|
|
73
|
+
S2T_VOID = 0,
|
|
74
|
+
S2T_BOOL = 1,
|
|
75
|
+
S2T_I32 = 2,
|
|
76
|
+
S2T_U32 = 3,
|
|
77
|
+
S2T_F32 = 4,
|
|
78
|
+
S2T_F64 = 5,
|
|
79
|
+
S2T_STRING = 6,
|
|
80
|
+
S2T_ENTITY = 7
|
|
81
|
+
} S2Type;
|
|
82
|
+
|
|
83
|
+
/* Native flags. */
|
|
84
|
+
enum {
|
|
85
|
+
/* The native may call host->fail. If it does, its result is discarded and the call throws an
|
|
86
|
+
* S2Error with the given code. */
|
|
87
|
+
S2N_CAN_FAIL = 1u
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/* Log levels for S2HostApi.log. */
|
|
91
|
+
enum { S2LOG_INFO = 0, S2LOG_WARN = 1, S2LOG_ERROR = 2 };
|
|
92
|
+
|
|
93
|
+
/* What the host hands an extension. Grows only by appending fields: check `size` before using a
|
|
94
|
+
* field added after version 1. Every service takes the host pointer it was reached through. */
|
|
95
|
+
typedef struct S2HostApi {
|
|
96
|
+
uint32_t size;
|
|
97
|
+
uint32_t abi;
|
|
98
|
+
/* The runtime's version, e.g. "0.4.0". */
|
|
99
|
+
const char* host_version;
|
|
100
|
+
/* One line in the server console, prefixed with the extension's name. */
|
|
101
|
+
void (*log)(const struct S2HostApi* host, int32_t level, const char* message);
|
|
102
|
+
/* A networked field's byte offset in its class (Source 2 schema), or -1 if either name is
|
|
103
|
+
* unknown. Cached by the host; safe from a fast native. Game thread only. */
|
|
104
|
+
int32_t (*schema_offset)(const struct S2HostApi* host, const char* class_name, const char* field);
|
|
105
|
+
/* Inside a native registered with S2N_CAN_FAIL: make this call throw S2Error. `code` is a short
|
|
106
|
+
* lowercase identifier ([a-z0-9_], up to 32 bytes), `message` is free text. Ignored, with a
|
|
107
|
+
* warning, anywhere else. */
|
|
108
|
+
void (*fail)(const struct S2HostApi* host, const char* code, const char* message);
|
|
109
|
+
} S2HostApi;
|
|
110
|
+
|
|
111
|
+
/* One native, as s2ext_init registers it. */
|
|
112
|
+
typedef struct S2NativeDesc {
|
|
113
|
+
uint32_t size; /* sizeof(S2NativeDesc) */
|
|
114
|
+
const char* name; /* a JavaScript identifier, unique in this extension */
|
|
115
|
+
void* fn; /* ret fn(void* recv, args...) */
|
|
116
|
+
S2Type ret;
|
|
117
|
+
const S2Type* args;
|
|
118
|
+
uint32_t nargs;
|
|
119
|
+
uint32_t flags; /* S2N_* */
|
|
120
|
+
const char* doc; /* optional: a one-paragraph description, for the generated typings */
|
|
121
|
+
const char* const* arg_names;/* optional: nargs parameter names, for the generated typings */
|
|
122
|
+
} S2NativeDesc;
|
|
123
|
+
|
|
124
|
+
typedef struct S2Registrar S2Registrar;
|
|
125
|
+
typedef struct S2RegistrarApi {
|
|
126
|
+
uint32_t size;
|
|
127
|
+
/* Register one native. Returns 0, or nonzero if the descriptor is refused; the host has then
|
|
128
|
+
* recorded why, and the extension is refused as a whole once s2ext_init returns. Valid only
|
|
129
|
+
* during s2ext_init. */
|
|
130
|
+
int32_t (*native)(S2Registrar* reg, const S2NativeDesc* desc);
|
|
131
|
+
} S2RegistrarApi;
|
|
132
|
+
|
|
133
|
+
typedef struct S2ExtInfo {
|
|
134
|
+
uint32_t size; /* sizeof(S2ExtInfo) */
|
|
135
|
+
uint32_t abi; /* S2EXT_ABI_VERSION */
|
|
136
|
+
const char* name; /* the npm-style name plugins import, e.g. "@community/fastmath" */
|
|
137
|
+
const char* version; /* semver, e.g. "0.1.0" */
|
|
138
|
+
} S2ExtInfo;
|
|
139
|
+
|
|
140
|
+
#if defined(_WIN32)
|
|
141
|
+
#define S2EXT_EXPORT __declspec(dllexport)
|
|
142
|
+
#else
|
|
143
|
+
#define S2EXT_EXPORT __attribute__((visibility("default")))
|
|
144
|
+
#endif
|
|
145
|
+
|
|
146
|
+
/* Exported by the extension. s2ext_info must return a pointer that stays valid for the process.
|
|
147
|
+
* s2ext_init returns 0 on success; anything else refuses the extension. The host pointer stays
|
|
148
|
+
* valid for the process, so keep it for later service calls. */
|
|
149
|
+
S2EXT_EXPORT const S2ExtInfo* s2ext_info(void);
|
|
150
|
+
S2EXT_EXPORT int32_t s2ext_init(const S2HostApi* host, S2Registrar* reg, const S2RegistrarApi* api);
|
|
151
|
+
|
|
152
|
+
#ifdef __cplusplus
|
|
153
|
+
}
|
|
154
|
+
#endif
|
|
155
|
+
#endif
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@s2script/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"license": "MIT OR Apache-2.0",
|
|
5
5
|
"description": "s2script SDK — the builtin capability types (imported as @s2script/sdk/entity, …) plus the `s2s` CLI for building Source 2 / CS2 plugins.",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"README.md",
|
|
35
35
|
"LICENSE-MIT",
|
|
36
36
|
"LICENSE-APACHE",
|
|
37
|
-
"contracts"
|
|
37
|
+
"contracts",
|
|
38
|
+
"ext"
|
|
38
39
|
],
|
|
39
40
|
"exports": {
|
|
40
41
|
".": {
|
package/unsafe.d.ts
CHANGED
|
@@ -10,6 +10,92 @@
|
|
|
10
10
|
* (see docs/superpowers/specs/2026-07-24-plugin-gamedata-design.md and ARCHITECTURE.md §2.0.7).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import type { EntityRef } from "./entity";
|
|
14
|
+
import type { Vector, QAngle } from "./math";
|
|
15
|
+
|
|
16
|
+
declare const ptrBrand: unique symbol;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A raw address in the server process: what an engine call with a `ptr` return gives you, what
|
|
20
|
+
* {@link Ptr.from} makes from an entity, and what a `ptr` argument takes.
|
|
21
|
+
*
|
|
22
|
+
* UNSAFE. Nothing tracks what a `Ptr` points at. It answers typed reads and writes at a byte
|
|
23
|
+
* offset, follows pointer fields with {@link Ptr.deref}, and can be handed to any generated
|
|
24
|
+
* struct view, but a stale or wrong address reads garbage or crashes the server exactly as it
|
|
25
|
+
* would in a Metamod plugin. Only addresses that can never be valid (the null page, anything
|
|
26
|
+
* outside the user half of the address space) are refused, with an `S2Error` whose `code` is
|
|
27
|
+
* `"bad_address"`. Prefer entity handles and their schema fields, which are checked on every
|
|
28
|
+
* access; reach for `Ptr` when an engine structure has no other door.
|
|
29
|
+
*
|
|
30
|
+
* Do not keep a `Ptr` past the call or event that produced it unless you know the lifetime of the
|
|
31
|
+
* memory behind it.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* import { Ptr } from "@s2script/sdk/unsafe";
|
|
35
|
+
* const p = Ptr.from(pawn); // the entity's address, or null when it is stale
|
|
36
|
+
* if (p) console.log(p.readInt32(healthOffset));
|
|
37
|
+
*/
|
|
38
|
+
export declare class Ptr {
|
|
39
|
+
private constructor();
|
|
40
|
+
private readonly [ptrBrand]: true;
|
|
41
|
+
/** The null pointer. Reads through it throw. */
|
|
42
|
+
static readonly NULL: Ptr;
|
|
43
|
+
/** A `Ptr` for an address, or an entity's current address (null once the entity is gone). */
|
|
44
|
+
static from(address: number | Ptr): Ptr;
|
|
45
|
+
static from(entity: EntityRef): Ptr | null;
|
|
46
|
+
/** The address as a number. Exact: every user-space address is below 2^47. */
|
|
47
|
+
readonly address: number;
|
|
48
|
+
isNull(): boolean;
|
|
49
|
+
/** This address plus `bytes`. */
|
|
50
|
+
add(bytes: number): Ptr;
|
|
51
|
+
equals(other: Ptr): boolean;
|
|
52
|
+
/** `0x…` */
|
|
53
|
+
toString(): string;
|
|
54
|
+
/** Hand this address to a view factory, such as a game package's struct view. */
|
|
55
|
+
as<T>(view: (ptr: Ptr) => T): T;
|
|
56
|
+
/** The pointer stored at `offset` (a pointer field), as a `Ptr`. `Ptr.NULL` when it holds null. */
|
|
57
|
+
deref(offset?: number): Ptr;
|
|
58
|
+
readPtr(offset: number): Ptr;
|
|
59
|
+
writePtr(offset: number, value: Ptr): true;
|
|
60
|
+
readInt8(offset: number): number;
|
|
61
|
+
readUInt8(offset: number): number;
|
|
62
|
+
readInt16(offset: number): number;
|
|
63
|
+
readUInt16(offset: number): number;
|
|
64
|
+
readInt32(offset: number): number;
|
|
65
|
+
readUInt32(offset: number): number;
|
|
66
|
+
readFloat32(offset: number): number;
|
|
67
|
+
readFloat64(offset: number): number;
|
|
68
|
+
readBool(offset: number): boolean;
|
|
69
|
+
/** A 64-bit integer, as a decimal string (a JS number cannot hold every value). */
|
|
70
|
+
readInt64(offset: number): string;
|
|
71
|
+
readUInt64(offset: number): string;
|
|
72
|
+
writeInt8(offset: number, value: number): true;
|
|
73
|
+
writeUInt8(offset: number, value: number): true;
|
|
74
|
+
writeInt16(offset: number, value: number): true;
|
|
75
|
+
writeUInt16(offset: number, value: number): true;
|
|
76
|
+
writeInt32(offset: number, value: number): true;
|
|
77
|
+
writeUInt32(offset: number, value: number): true;
|
|
78
|
+
writeFloat32(offset: number, value: number): true;
|
|
79
|
+
writeFloat64(offset: number, value: number): true;
|
|
80
|
+
writeBool(offset: number, value: boolean): true;
|
|
81
|
+
/** The NUL-terminated string at `offset`, reading at most `maxLen` bytes. */
|
|
82
|
+
readString(offset: number, maxLen: number): string;
|
|
83
|
+
/** Write `value` into a `maxLen`-byte buffer, truncated so it always ends in a NUL. */
|
|
84
|
+
writeString(offset: number, maxLen: number, value: string): boolean;
|
|
85
|
+
/** `count` consecutive 32-bit floats (a vector, an angle). */
|
|
86
|
+
readFloats(offset: number, count: number): number[];
|
|
87
|
+
/** The `Vector` at `offset`. */
|
|
88
|
+
readVector(offset: number): Vector;
|
|
89
|
+
/** The `QAngle` at `offset`. */
|
|
90
|
+
readQAngle(offset: number): QAngle;
|
|
91
|
+
/** The `Vector` at the end of a pointer chain, or null on a null hop. */
|
|
92
|
+
readVectorChain(ptrOffs: number[], finalOff: number): Vector | null;
|
|
93
|
+
/** The `QAngle` at the end of a pointer chain, or null on a null hop. */
|
|
94
|
+
readQAngleChain(ptrOffs: number[], finalOff: number): QAngle | null;
|
|
95
|
+
/** A `CEntityHandle` field, resolved through the entity books: null when it names no live entity. */
|
|
96
|
+
readHandle(offset: number): EntityRef | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
13
99
|
/**
|
|
14
100
|
* The calls this plugin declares. The base interface is EMPTY — `s2s build` generates
|
|
15
101
|
* `.s2script/gamedata.d.ts`, which augments this from your gamedata's `calls` section.
|
|
@@ -59,8 +145,64 @@ export interface FunctionSubscription extends Disposable {
|
|
|
59
145
|
* const onFoo = Engine.hook("onFoo");
|
|
60
146
|
* if (onFoo) onFoo((view) => { view.reason = 0; });
|
|
61
147
|
*/
|
|
148
|
+
/** A target validator, as in `gamedata/functions.jsonc`. */
|
|
149
|
+
export interface FunctionValidator {
|
|
150
|
+
prologue?: string;
|
|
151
|
+
'string-xref'?: { at: number; dispOff: number; instrLen: number; expect: string };
|
|
152
|
+
'vtable-member'?: string;
|
|
153
|
+
}
|
|
154
|
+
/** A function located by byte signature. */
|
|
155
|
+
export interface SignatureTarget {
|
|
156
|
+
kind?: 'signature';
|
|
157
|
+
module: string;
|
|
158
|
+
pattern: string;
|
|
159
|
+
validate: FunctionValidator;
|
|
160
|
+
targetValidate?: FunctionValidator;
|
|
161
|
+
}
|
|
162
|
+
/** A function located by vtable slot. */
|
|
163
|
+
export interface VirtualTarget {
|
|
164
|
+
kind?: 'virtual';
|
|
165
|
+
module: string;
|
|
166
|
+
class: string;
|
|
167
|
+
index: number;
|
|
168
|
+
validate: FunctionValidator;
|
|
169
|
+
}
|
|
170
|
+
/** A parameter or return type. `ptr` is a raw address, a {@link Ptr} (or null) in JavaScript. */
|
|
171
|
+
export type FunctionValueType = 'bool' | 'i32' | 'u32' | 'i64' | 'u64' | 'f32' | 'f64' | 'entity' | 'entity?' | 'string' | 'vector' | 'ptr';
|
|
172
|
+
/** One engine function, written exactly as an entry of `gamedata/functions.jsonc`. */
|
|
173
|
+
export interface FunctionDeclaration<T> {
|
|
174
|
+
target: T | { ref: string };
|
|
175
|
+
receiver?: { type: 'none' | 'entity' };
|
|
176
|
+
parameters?: readonly { name: string; type: FunctionValueType; ownership?: 'callee-borrowed' | 'callee-retained' | 'native-observed'; mutable?: 'pre' }[];
|
|
177
|
+
returns?: FunctionValueType | 'void' | { type: 'string' | 'vector'; ownership: 'caller-borrowed' | 'native-observed' };
|
|
178
|
+
surfaces?: readonly ('call' | 'pre' | 'post')[];
|
|
179
|
+
suppression?: 'generic' | 'none';
|
|
180
|
+
requirement?: 'optional' | 'required';
|
|
181
|
+
resolve?: 'direct' | 'ctor-body-xref' | 'lea-disp' | 'validated-call';
|
|
182
|
+
}
|
|
183
|
+
|
|
62
184
|
export declare const Engine: {
|
|
63
185
|
function<K extends keyof EngineFunctions>(name: K): EngineFunctions[K];
|
|
186
|
+
/**
|
|
187
|
+
* Declare an engine function inline, by byte signature, and get its binding: the same as
|
|
188
|
+
* writing the declaration in `gamedata/functions.jsonc` and calling {@link Engine.function}.
|
|
189
|
+
*
|
|
190
|
+
* `s2s build` copies the declaration into the plugin's manifest, so it is validated, listed and
|
|
191
|
+
* checked after game updates like any other. That is why both arguments must be literals; a
|
|
192
|
+
* computed declaration fails the build. The binding's types come from the build, so run it once
|
|
193
|
+
* after adding a declaration.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* const ignite = Engine.fromSignature("ignite", {
|
|
197
|
+
* target: { module: "libserver.so", pattern: "55 48 89 E5 ...", validate: { prologue: "55 48 89 E5" } },
|
|
198
|
+
* receiver: { type: "entity" },
|
|
199
|
+
* parameters: [{ name: "seconds", type: "f32" }],
|
|
200
|
+
* surfaces: ["call"],
|
|
201
|
+
* });
|
|
202
|
+
*/
|
|
203
|
+
fromSignature<K extends keyof EngineFunctions>(name: K, declaration: FunctionDeclaration<SignatureTarget>): EngineFunctions[K];
|
|
204
|
+
/** {@link Engine.fromSignature} for a function located by vtable slot. */
|
|
205
|
+
fromVirtual<K extends keyof EngineFunctions>(name: K, declaration: FunctionDeclaration<VirtualTarget>): EngineFunctions[K];
|
|
64
206
|
/**
|
|
65
207
|
* The declared call, or `null` when its descriptor failed a load-time gate (signature miss,
|
|
66
208
|
* validator rejection, slot outside `.text`, missing platform entry, or the plugin is not
|