@rezless/vm 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 +202 -0
- package/NOTICE +32 -0
- package/dist/builtins/base64.d.ts +25 -0
- package/dist/builtins/base64.d.ts.map +1 -0
- package/dist/builtins/base64.js +43 -0
- package/dist/builtins/base64.js.map +1 -0
- package/dist/builtins/chat.d.ts +10 -0
- package/dist/builtins/chat.d.ts.map +1 -0
- package/dist/builtins/chat.js +36 -0
- package/dist/builtins/chat.js.map +1 -0
- package/dist/builtins/dataserver.d.ts +26 -0
- package/dist/builtins/dataserver.d.ts.map +1 -0
- package/dist/builtins/dataserver.js +20 -0
- package/dist/builtins/dataserver.js.map +1 -0
- package/dist/builtins/detected.d.ts +40 -0
- package/dist/builtins/detected.d.ts.map +1 -0
- package/dist/builtins/detected.js +23 -0
- package/dist/builtins/detected.js.map +1 -0
- package/dist/builtins/hash.d.ts +6 -0
- package/dist/builtins/hash.d.ts.map +1 -0
- package/dist/builtins/hash.js +34 -0
- package/dist/builtins/hash.js.map +1 -0
- package/dist/builtins/http.d.ts +40 -0
- package/dist/builtins/http.d.ts.map +1 -0
- package/dist/builtins/http.js +87 -0
- package/dist/builtins/http.js.map +1 -0
- package/dist/builtins/identity.d.ts +14 -0
- package/dist/builtins/identity.d.ts.map +1 -0
- package/dist/builtins/identity.js +17 -0
- package/dist/builtins/identity.js.map +1 -0
- package/dist/builtins/index.d.ts +8 -0
- package/dist/builtins/index.d.ts.map +1 -0
- package/dist/builtins/index.js +232 -0
- package/dist/builtins/index.js.map +1 -0
- package/dist/builtins/inventory.d.ts +33 -0
- package/dist/builtins/inventory.d.ts.map +1 -0
- package/dist/builtins/inventory.js +134 -0
- package/dist/builtins/inventory.js.map +1 -0
- package/dist/builtins/json.d.ts +7 -0
- package/dist/builtins/json.d.ts.map +1 -0
- package/dist/builtins/json.js +690 -0
- package/dist/builtins/json.js.map +1 -0
- package/dist/builtins/link-info.d.ts +10 -0
- package/dist/builtins/link-info.d.ts.map +1 -0
- package/dist/builtins/link-info.js +26 -0
- package/dist/builtins/link-info.js.map +1 -0
- package/dist/builtins/linked.d.ts +23 -0
- package/dist/builtins/linked.d.ts.map +1 -0
- package/dist/builtins/linked.js +20 -0
- package/dist/builtins/linked.js.map +1 -0
- package/dist/builtins/linksetdata.d.ts +20 -0
- package/dist/builtins/linksetdata.d.ts.map +1 -0
- package/dist/builtins/linksetdata.js +196 -0
- package/dist/builtins/linksetdata.js.map +1 -0
- package/dist/builtins/listen.d.ts +33 -0
- package/dist/builtins/listen.d.ts.map +1 -0
- package/dist/builtins/listen.js +45 -0
- package/dist/builtins/listen.js.map +1 -0
- package/dist/builtins/lists.d.ts +37 -0
- package/dist/builtins/lists.d.ts.map +1 -0
- package/dist/builtins/lists.js +275 -0
- package/dist/builtins/lists.js.map +1 -0
- package/dist/builtins/math.d.ts +32 -0
- package/dist/builtins/math.d.ts.map +1 -0
- package/dist/builtins/math.js +104 -0
- package/dist/builtins/math.js.map +1 -0
- package/dist/builtins/object.d.ts +29 -0
- package/dist/builtins/object.d.ts.map +1 -0
- package/dist/builtins/object.js +50 -0
- package/dist/builtins/object.js.map +1 -0
- package/dist/builtins/prim-accessors.d.ts +56 -0
- package/dist/builtins/prim-accessors.d.ts.map +1 -0
- package/dist/builtins/prim-accessors.js +353 -0
- package/dist/builtins/prim-accessors.js.map +1 -0
- package/dist/builtins/prim-params.d.ts +12 -0
- package/dist/builtins/prim-params.d.ts.map +1 -0
- package/dist/builtins/prim-params.js +124 -0
- package/dist/builtins/prim-params.js.map +1 -0
- package/dist/builtins/script-control.d.ts +13 -0
- package/dist/builtins/script-control.d.ts.map +1 -0
- package/dist/builtins/script-control.js +41 -0
- package/dist/builtins/script-control.js.map +1 -0
- package/dist/builtins/strings.d.ts +23 -0
- package/dist/builtins/strings.d.ts.map +1 -0
- package/dist/builtins/strings.js +130 -0
- package/dist/builtins/strings.js.map +1 -0
- package/dist/builtins/time.d.ts +28 -0
- package/dist/builtins/time.d.ts.map +1 -0
- package/dist/builtins/time.js +54 -0
- package/dist/builtins/time.js.map +1 -0
- package/dist/clock.d.ts +75 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +125 -0
- package/dist/clock.js.map +1 -0
- package/dist/coverage.d.ts +110 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +221 -0
- package/dist/coverage.js.map +1 -0
- package/dist/dispatch.d.ts +19 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +42 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/env.d.ts +33 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +76 -0
- package/dist/env.js.map +1 -0
- package/dist/generated/constants.d.ts +967 -0
- package/dist/generated/constants.d.ts.map +1 -0
- package/dist/generated/constants.js +969 -0
- package/dist/generated/constants.js.map +1 -0
- package/dist/generated/constants_table.d.ts +11 -0
- package/dist/generated/constants_table.d.ts.map +1 -0
- package/dist/generated/constants_table.js +978 -0
- package/dist/generated/constants_table.js.map +1 -0
- package/dist/generated/events.d.ts +545 -0
- package/dist/generated/events.d.ts.map +1 -0
- package/dist/generated/events.js +49 -0
- package/dist/generated/events.js.map +1 -0
- package/dist/generated/functions.d.ts +6051 -0
- package/dist/generated/functions.d.ts.map +1 -0
- package/dist/generated/functions.js +526 -0
- package/dist/generated/functions.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/interpreter.d.ts +39 -0
- package/dist/interpreter.d.ts.map +1 -0
- package/dist/interpreter.js +381 -0
- package/dist/interpreter.js.map +1 -0
- package/dist/inventory.d.ts +52 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +45 -0
- package/dist/inventory.js.map +1 -0
- package/dist/linkset.d.ts +89 -0
- package/dist/linkset.d.ts.map +1 -0
- package/dist/linkset.js +179 -0
- package/dist/linkset.js.map +1 -0
- package/dist/prim-params.d.ts +221 -0
- package/dist/prim-params.d.ts.map +1 -0
- package/dist/prim-params.js +852 -0
- package/dist/prim-params.js.map +1 -0
- package/dist/prim.d.ts +79 -0
- package/dist/prim.d.ts.map +1 -0
- package/dist/prim.js +96 -0
- package/dist/prim.js.map +1 -0
- package/dist/random.d.ts +16 -0
- package/dist/random.d.ts.map +1 -0
- package/dist/random.js +29 -0
- package/dist/random.js.map +1 -0
- package/dist/runtime.d.ts +108 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +2 -0
- package/dist/runtime.js.map +1 -0
- package/dist/script.d.ts +248 -0
- package/dist/script.d.ts.map +1 -0
- package/dist/script.js +667 -0
- package/dist/script.js.map +1 -0
- package/dist/values/coerce.d.ts +22 -0
- package/dist/values/coerce.d.ts.map +1 -0
- package/dist/values/coerce.js +184 -0
- package/dist/values/coerce.js.map +1 -0
- package/dist/values/format.d.ts +20 -0
- package/dist/values/format.d.ts.map +1 -0
- package/dist/values/format.js +47 -0
- package/dist/values/format.js.map +1 -0
- package/dist/values/ops.d.ts +19 -0
- package/dist/values/ops.d.ts.map +1 -0
- package/dist/values/ops.js +345 -0
- package/dist/values/ops.js.map +1 -0
- package/dist/values/types.d.ts +39 -0
- package/dist/values/types.d.ts.map +1 -0
- package/dist/values/types.js +68 -0
- package/dist/values/types.js.map +1 -0
- package/package.json +52 -0
package/dist/script.js
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { ResetScriptSignal } from './builtins/object.js';
|
|
2
|
+
import { Mulberry32 } from './random.js';
|
|
3
|
+
import { NULL_KEY } from './values/types.js';
|
|
4
|
+
import { execHandler, StateChangeSignal } from './interpreter.js';
|
|
5
|
+
import { buildCoveragePlan, CoverageCollector } from './coverage.js';
|
|
6
|
+
import { defaultEvalFor } from './values/types.js';
|
|
7
|
+
import { Env } from './env.js';
|
|
8
|
+
import { ScriptClockView } from './clock.js';
|
|
9
|
+
import { Linkset } from './linkset.js';
|
|
10
|
+
import { Prim } from './prim.js';
|
|
11
|
+
import { EVENT_SPECS } from './generated/events.js';
|
|
12
|
+
import { CONSTANT_TABLE } from './generated/constants_table.js';
|
|
13
|
+
/**
|
|
14
|
+
* Public handle for a loaded LSL script. Tests interact almost entirely
|
|
15
|
+
* with this surface: drive events via fire(), inspect chat / calls,
|
|
16
|
+
* override functions via mock(), read or seed globals.
|
|
17
|
+
*/
|
|
18
|
+
export class Script {
|
|
19
|
+
ast;
|
|
20
|
+
state;
|
|
21
|
+
mocks = Object.create(null);
|
|
22
|
+
globals;
|
|
23
|
+
userFunctions;
|
|
24
|
+
handlersByState;
|
|
25
|
+
started = false;
|
|
26
|
+
/** Per-script clock facade (timer state + time reference). */
|
|
27
|
+
clockView;
|
|
28
|
+
host;
|
|
29
|
+
linkset;
|
|
30
|
+
scriptName;
|
|
31
|
+
/** When false, drainQueue parks events instead of delivering them. */
|
|
32
|
+
running = true;
|
|
33
|
+
/** Events parked while `running === false`; replayed when re-enabled. */
|
|
34
|
+
parkedEvents = [];
|
|
35
|
+
constructor(ast, options = {}) {
|
|
36
|
+
this.ast = ast;
|
|
37
|
+
// Auto-wrap: if no host prim was provided, create a default 1-prim linkset.
|
|
38
|
+
let host = options.host;
|
|
39
|
+
if (!host) {
|
|
40
|
+
const linkset = new Linkset({ owner: options.owner ?? NULL_KEY });
|
|
41
|
+
const prim = new Prim({
|
|
42
|
+
key: options.objectKey ??
|
|
43
|
+
deterministicKey(options.scriptName ?? options.filename ?? 'script'),
|
|
44
|
+
name: options.objectName ?? 'Object',
|
|
45
|
+
});
|
|
46
|
+
linkset.addPrim(prim);
|
|
47
|
+
host = prim;
|
|
48
|
+
}
|
|
49
|
+
this.host = host;
|
|
50
|
+
if (!host.linkset) {
|
|
51
|
+
throw new Error('Script host prim must belong to a Linkset');
|
|
52
|
+
}
|
|
53
|
+
this.linkset = host.linkset;
|
|
54
|
+
this.scriptName = options.scriptName ?? deriveScriptName(options.filename);
|
|
55
|
+
this.clockView = new ScriptClockView(this.linkset.clock, () => this);
|
|
56
|
+
const linkset = this.linkset;
|
|
57
|
+
const prim = host;
|
|
58
|
+
const scriptName = this.scriptName;
|
|
59
|
+
const identity = {
|
|
60
|
+
get owner() {
|
|
61
|
+
return linkset.owner;
|
|
62
|
+
},
|
|
63
|
+
get objectKey() {
|
|
64
|
+
return prim.key;
|
|
65
|
+
},
|
|
66
|
+
get objectName() {
|
|
67
|
+
return prim.name;
|
|
68
|
+
},
|
|
69
|
+
set objectName(v) {
|
|
70
|
+
prim.name = v;
|
|
71
|
+
},
|
|
72
|
+
get scriptName() {
|
|
73
|
+
return scriptName;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const filename = options.filename ?? '<inline>';
|
|
77
|
+
const source = options.source ?? '';
|
|
78
|
+
const coverage = options.coverage
|
|
79
|
+
? new CoverageCollector(buildCoveragePlan(ast, filename, source))
|
|
80
|
+
: null;
|
|
81
|
+
this.state = {
|
|
82
|
+
currentState: 'default',
|
|
83
|
+
chat: [],
|
|
84
|
+
calls: [],
|
|
85
|
+
clock: this.clockView,
|
|
86
|
+
httpRequests: [],
|
|
87
|
+
httpKeyCounter: 0,
|
|
88
|
+
listens: [],
|
|
89
|
+
listenHandleCounter: 0,
|
|
90
|
+
random: new Mulberry32(options.randomSeed ?? 1),
|
|
91
|
+
identity,
|
|
92
|
+
linkedMessages: [],
|
|
93
|
+
dataserverRequests: [],
|
|
94
|
+
dataserverKeyCounter: 0,
|
|
95
|
+
detectedStack: [],
|
|
96
|
+
linksetData: this.linkset.linksetData,
|
|
97
|
+
appearance: this.host.appearance,
|
|
98
|
+
lifecycle: {
|
|
99
|
+
dead: false,
|
|
100
|
+
},
|
|
101
|
+
filename,
|
|
102
|
+
source,
|
|
103
|
+
coverage,
|
|
104
|
+
};
|
|
105
|
+
if (coverage)
|
|
106
|
+
coverage.hitState('default');
|
|
107
|
+
// Register this script with the prim if it isn't already there.
|
|
108
|
+
if (!host.scripts.includes(this)) {
|
|
109
|
+
host.addScript(this);
|
|
110
|
+
}
|
|
111
|
+
// Build a parent scope holding every kwdb constant (PI, TRUE, HTTP_METHOD,
|
|
112
|
+
// …). Script globals get their own scope below so a user can shadow
|
|
113
|
+
// constants if they really want to (LSL allows it).
|
|
114
|
+
const constants = buildConstantsEnv();
|
|
115
|
+
this.globals = constants.push();
|
|
116
|
+
initGlobals(this.globals, ast.globals);
|
|
117
|
+
this.userFunctions = new Map(ast.functions.map((f) => [f.name, f]));
|
|
118
|
+
this.handlersByState = indexHandlers(ast);
|
|
119
|
+
if (!this.handlersByState.has('default')) {
|
|
120
|
+
throw new Error('LSL script has no default state');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Current virtual time in milliseconds since linkset construction. */
|
|
124
|
+
get now() {
|
|
125
|
+
return this.linkset.clock.now;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Advance the linkset clock by `ms` and fire every queued event whose
|
|
129
|
+
* scheduled time is ≤ the new now (across all scripts in the linkset).
|
|
130
|
+
* Convenience pass-through to `linkset.advanceTime`.
|
|
131
|
+
*/
|
|
132
|
+
advanceTime(ms) {
|
|
133
|
+
this.linkset.advanceTime(ms);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Configured recurring timer interval in seconds, or 0 if no timer is
|
|
137
|
+
* registered. Mirrors `llSetTimerEvent`'s most recent argument.
|
|
138
|
+
*/
|
|
139
|
+
get timerInterval() {
|
|
140
|
+
return this.clockView.timerIntervalMs / 1000;
|
|
141
|
+
}
|
|
142
|
+
/** Current LSL state name. */
|
|
143
|
+
get currentState() {
|
|
144
|
+
return this.state.currentState;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Coverage report snapshot, or null when coverage is disabled. Each call
|
|
148
|
+
* returns a fresh frozen object — safe to retain across further fire()s
|
|
149
|
+
* without seeing later mutations.
|
|
150
|
+
*/
|
|
151
|
+
get coverage() {
|
|
152
|
+
return this.state.coverage?.snapshot() ?? null;
|
|
153
|
+
}
|
|
154
|
+
/** Captured chat output (llSay/llShout/llWhisper/llOwnerSay/...). */
|
|
155
|
+
get chat() {
|
|
156
|
+
return this.state.chat;
|
|
157
|
+
}
|
|
158
|
+
/** Universal log of every ll* call this script has made. Filter with callsOf(name). */
|
|
159
|
+
get calls() {
|
|
160
|
+
return this.state.calls;
|
|
161
|
+
}
|
|
162
|
+
/** Filtered call log: only entries for `name`. */
|
|
163
|
+
callsOf(name) {
|
|
164
|
+
return this.state.calls.filter((c) => c.name === name);
|
|
165
|
+
}
|
|
166
|
+
/** Captured outgoing HTTP requests from `llHTTPRequest`. */
|
|
167
|
+
get httpRequests() {
|
|
168
|
+
return this.state.httpRequests;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Feed a response to a previously captured HTTP request. Schedules an
|
|
172
|
+
* `http_response` event for immediate delivery.
|
|
173
|
+
*/
|
|
174
|
+
respondToHttp(key, response) {
|
|
175
|
+
const req = this.state.httpRequests.find((r) => r.key === key);
|
|
176
|
+
if (!req)
|
|
177
|
+
throw new Error(`unknown HTTP request key: ${key}`);
|
|
178
|
+
req.fulfilled = true;
|
|
179
|
+
this.clockView.schedule(this.now, 'http_response', {
|
|
180
|
+
request_id: key,
|
|
181
|
+
status: response.status,
|
|
182
|
+
metadata: response.metadata ?? [],
|
|
183
|
+
body: response.body ?? '',
|
|
184
|
+
});
|
|
185
|
+
this.linkset.drainQueue();
|
|
186
|
+
}
|
|
187
|
+
/** Convenience: respond to the most recent HTTP request. */
|
|
188
|
+
respondToLastHttp(response) {
|
|
189
|
+
const req = this.state.httpRequests[this.state.httpRequests.length - 1];
|
|
190
|
+
if (!req)
|
|
191
|
+
throw new Error('no HTTP request to respond to');
|
|
192
|
+
this.respondToHttp(req.key, response);
|
|
193
|
+
}
|
|
194
|
+
/** Currently active listen registrations (from `llListen`). */
|
|
195
|
+
get listens() {
|
|
196
|
+
return this.state.listens;
|
|
197
|
+
}
|
|
198
|
+
/** Captured llMessageLinked invocations originating from this script. */
|
|
199
|
+
get linkedMessages() {
|
|
200
|
+
return this.state.linkedMessages;
|
|
201
|
+
}
|
|
202
|
+
/** Drop every captured outgoing linked-message entry for this script. */
|
|
203
|
+
clearLinkedMessages() {
|
|
204
|
+
this.state.linkedMessages.length = 0;
|
|
205
|
+
}
|
|
206
|
+
/** Captured pending dataserver requests (llRequestAgentData and friends). */
|
|
207
|
+
get dataserverRequests() {
|
|
208
|
+
return this.state.dataserverRequests;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Feed a value back to a pending dataserver request. Schedules a
|
|
212
|
+
* `dataserver` event with the request key and a string value.
|
|
213
|
+
*/
|
|
214
|
+
respondToDataserver(key, value) {
|
|
215
|
+
const req = this.state.dataserverRequests.find((r) => r.key === key);
|
|
216
|
+
if (!req)
|
|
217
|
+
throw new Error(`unknown dataserver request key: ${key}`);
|
|
218
|
+
req.fulfilled = true;
|
|
219
|
+
this.clockView.schedule(this.now, 'dataserver', {
|
|
220
|
+
queryid: key,
|
|
221
|
+
data: value,
|
|
222
|
+
});
|
|
223
|
+
this.linkset.drainQueue();
|
|
224
|
+
}
|
|
225
|
+
/** Convenience: respond to the most recent dataserver request. */
|
|
226
|
+
respondToLastDataserver(value) {
|
|
227
|
+
const req = this.state.dataserverRequests[this.state.dataserverRequests.length - 1];
|
|
228
|
+
if (!req)
|
|
229
|
+
throw new Error('no dataserver request to respond to');
|
|
230
|
+
this.respondToDataserver(req.key, value);
|
|
231
|
+
}
|
|
232
|
+
/** Currently displayed floating text (from llSetText). null if unset. */
|
|
233
|
+
get text() {
|
|
234
|
+
return this.state.appearance.text;
|
|
235
|
+
}
|
|
236
|
+
/** Object description from llSetObjectDesc. */
|
|
237
|
+
get objectDesc() {
|
|
238
|
+
return this.state.appearance.description;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Read-only view of the Linkset Data store. Tests can iterate to assert on
|
|
242
|
+
* keys / values / protection. Modify via the LSL builtins or seedLinksetData.
|
|
243
|
+
*/
|
|
244
|
+
get linksetData() {
|
|
245
|
+
return this.state.linksetData;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* White-box helper: pre-populate the Linkset Data store without going
|
|
249
|
+
* through llLinksetDataWrite. Does not fire linkset_data events.
|
|
250
|
+
*/
|
|
251
|
+
seedLinksetData(entries) {
|
|
252
|
+
for (const [k, v] of entries) {
|
|
253
|
+
this.state.linksetData.set(k, { value: v.value, password: v.password ?? '' });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** True once `llDie()` has been called. Subsequent fire() calls are no-ops. */
|
|
257
|
+
get dead() {
|
|
258
|
+
return this.state.lifecycle.dead;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Match chat against this script's listens and queue `listen` events for
|
|
262
|
+
* every match. Does not drain — caller does that. Used by Linkset.deliverChat
|
|
263
|
+
* for cross-script delivery.
|
|
264
|
+
*/
|
|
265
|
+
matchChatListens(opts) {
|
|
266
|
+
for (const l of this.state.listens) {
|
|
267
|
+
if (!l.active)
|
|
268
|
+
continue;
|
|
269
|
+
if (l.channel !== opts.channel)
|
|
270
|
+
continue;
|
|
271
|
+
if (l.name && l.name !== opts.name)
|
|
272
|
+
continue;
|
|
273
|
+
if (l.key && l.key !== NULL_KEY && l.key !== opts.key)
|
|
274
|
+
continue;
|
|
275
|
+
if (l.message && l.message !== opts.message)
|
|
276
|
+
continue;
|
|
277
|
+
this.clockView.schedule(this.now, 'listen', {
|
|
278
|
+
channel: opts.channel,
|
|
279
|
+
name: opts.name,
|
|
280
|
+
id: opts.key,
|
|
281
|
+
message: opts.message,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Deliver chat to this script only. Linkset-wide delivery (every listening
|
|
287
|
+
* script in the linkset) is `linkset.deliverChat`.
|
|
288
|
+
*/
|
|
289
|
+
deliverChat(opts) {
|
|
290
|
+
this.matchChatListens(opts);
|
|
291
|
+
this.linkset.drainQueue();
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Override an `ll*` function for the lifetime of this Script. Replaces
|
|
295
|
+
* any built-in or stub of the same name.
|
|
296
|
+
*/
|
|
297
|
+
mock(name, impl) {
|
|
298
|
+
this.mocks[name] = impl;
|
|
299
|
+
}
|
|
300
|
+
/** Read a global variable's current value. White-box hook for tests. */
|
|
301
|
+
global(name) {
|
|
302
|
+
return this.globals.get(name).value;
|
|
303
|
+
}
|
|
304
|
+
/** Seed a global variable. The value is coerced to the global's declared type. */
|
|
305
|
+
setGlobal(name, value, type) {
|
|
306
|
+
const inferred = type ?? inferType(value);
|
|
307
|
+
this.globals.set(name, { type: inferred, value });
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Drive an event into the current state.
|
|
311
|
+
*
|
|
312
|
+
* `payload` is keyed by the event's documented parameter names (per kwdb).
|
|
313
|
+
* The handler's declared parameters bind by position to the event spec, so
|
|
314
|
+
* the user's chosen names don't have to match.
|
|
315
|
+
*
|
|
316
|
+
* Synchronous: runs the handler to completion before returning, including
|
|
317
|
+
* any state transitions triggered by `state foo;` (state_exit / change /
|
|
318
|
+
* state_entry chain runs before fire() returns).
|
|
319
|
+
*
|
|
320
|
+
* If the current state has no handler for `eventName`, this is a no-op
|
|
321
|
+
* — matches LSL behavior of silently dropping unhandled events.
|
|
322
|
+
*/
|
|
323
|
+
fire(eventName, payload = {}) {
|
|
324
|
+
if (this.state.lifecycle.dead)
|
|
325
|
+
return;
|
|
326
|
+
if (!this.started) {
|
|
327
|
+
this.started = true;
|
|
328
|
+
const entry = this.handlersByState.get(this.state.currentState)?.get('state_entry');
|
|
329
|
+
if (entry && eventName !== 'state_entry') {
|
|
330
|
+
this.runHandler(entry, []);
|
|
331
|
+
this.linkset.drainQueue();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const handler = this.handlersByState.get(this.state.currentState)?.get(eventName);
|
|
335
|
+
if (handler) {
|
|
336
|
+
const args = bindPayload(eventName, payload);
|
|
337
|
+
this.withDetected(payload, () => this.runHandler(handler, args));
|
|
338
|
+
}
|
|
339
|
+
this.linkset.drainQueue();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Run state_entry of the default state. If the script has been auto-started
|
|
343
|
+
* (e.g. by an earlier fire), this is a no-op.
|
|
344
|
+
*/
|
|
345
|
+
start() {
|
|
346
|
+
if (this.state.lifecycle.dead)
|
|
347
|
+
return;
|
|
348
|
+
if (this.started)
|
|
349
|
+
return;
|
|
350
|
+
this.started = true;
|
|
351
|
+
const entry = this.handlersByState.get('default')?.get('state_entry');
|
|
352
|
+
if (entry) {
|
|
353
|
+
this.runHandler(entry, []);
|
|
354
|
+
}
|
|
355
|
+
this.linkset.drainQueue();
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Reset the script as if `llResetScript` had been called: clear globals,
|
|
359
|
+
* reseed them from the AST initializers, return to the default state,
|
|
360
|
+
* cancel timers, and run state_entry. Used internally when llResetScript
|
|
361
|
+
* is invoked from inside a handler; tests can also call it directly.
|
|
362
|
+
*/
|
|
363
|
+
reset() {
|
|
364
|
+
this.globals.clear();
|
|
365
|
+
initGlobals(this.globals, this.ast.globals);
|
|
366
|
+
this.state.currentState = 'default';
|
|
367
|
+
this.state.lifecycle.dead = false;
|
|
368
|
+
this.clockView.cancelTimer();
|
|
369
|
+
this.linkset.clock.purgeTarget(this);
|
|
370
|
+
this.parkedEvents.length = 0;
|
|
371
|
+
this.started = false;
|
|
372
|
+
// Returning to default counts as a state entry. The constructor records
|
|
373
|
+
// the first one; runHandler records each `state foo;` transition. A
|
|
374
|
+
// reset never goes through runHandler's StateChangeSignal path (default
|
|
375
|
+
// → default raises no signal), so credit it explicitly here.
|
|
376
|
+
if (this.state.coverage)
|
|
377
|
+
this.state.coverage.hitState('default');
|
|
378
|
+
// Per LSL: if the script is stopped, reset just clears state — no
|
|
379
|
+
// state_entry runs until it's restarted. If it's running, fire
|
|
380
|
+
// state_entry now via start().
|
|
381
|
+
if (this.running)
|
|
382
|
+
this.start();
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Internal: invoked by `Linkset.drainQueue` to deliver one queued event.
|
|
386
|
+
* The synthetic `__reset` event triggers a sibling-initiated reset
|
|
387
|
+
* (scheduled by `llResetOtherScript`).
|
|
388
|
+
*/
|
|
389
|
+
deliver(event, payload) {
|
|
390
|
+
if (this.state.lifecycle.dead)
|
|
391
|
+
return;
|
|
392
|
+
if (event === '__reset') {
|
|
393
|
+
this.reset();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const handler = this.handlersByState.get(this.state.currentState)?.get(event);
|
|
397
|
+
if (!handler)
|
|
398
|
+
return;
|
|
399
|
+
const args = bindPayload(event, payload);
|
|
400
|
+
this.withDetected(payload, () => this.runHandler(handler, args));
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Push a detected context (if the payload includes `detected`) for the
|
|
404
|
+
* duration of `fn`, so llDetectedKey / Name / Pos / etc. inside the
|
|
405
|
+
* handler resolve to the right entries.
|
|
406
|
+
*/
|
|
407
|
+
withDetected(payload, fn) {
|
|
408
|
+
const detected = payload['detected'];
|
|
409
|
+
if (!Array.isArray(detected) || detected.length === 0) {
|
|
410
|
+
fn();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
this.state.detectedStack.push({ entries: detected });
|
|
414
|
+
try {
|
|
415
|
+
fn();
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
this.state.detectedStack.pop();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Run a handler and process any state-change signal it raises.
|
|
423
|
+
*
|
|
424
|
+
* LSL semantics: when a handler executes `state foo;`, control leaves the
|
|
425
|
+
* handler immediately, the current state's `state_exit` fires, the state
|
|
426
|
+
* changes, then the new state's `state_entry` fires. We mirror that here
|
|
427
|
+
* with a small loop so that a `state_exit` or `state_entry` that itself
|
|
428
|
+
* does `state foo;` continues the chain correctly.
|
|
429
|
+
*/
|
|
430
|
+
runHandler(handler, args) {
|
|
431
|
+
const ctx = {
|
|
432
|
+
state: this.state,
|
|
433
|
+
mocks: this.mocks,
|
|
434
|
+
globals: this.globals,
|
|
435
|
+
userFunctions: this.userFunctions,
|
|
436
|
+
script: this,
|
|
437
|
+
prim: this.host,
|
|
438
|
+
linkset: this.linkset,
|
|
439
|
+
};
|
|
440
|
+
let pending = {
|
|
441
|
+
handler,
|
|
442
|
+
args,
|
|
443
|
+
};
|
|
444
|
+
while (pending) {
|
|
445
|
+
const { handler: h, args: a } = pending;
|
|
446
|
+
pending = null;
|
|
447
|
+
try {
|
|
448
|
+
execHandler(ctx, h, a);
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
if (e instanceof ResetScriptSignal) {
|
|
452
|
+
this.reset();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (!(e instanceof StateChangeSignal))
|
|
456
|
+
throw e;
|
|
457
|
+
const target = e.target;
|
|
458
|
+
const exit = this.handlersByState.get(this.state.currentState)?.get('state_exit');
|
|
459
|
+
if (exit) {
|
|
460
|
+
try {
|
|
461
|
+
execHandler(ctx, exit, []);
|
|
462
|
+
}
|
|
463
|
+
catch (e2) {
|
|
464
|
+
if (e2 instanceof StateChangeSignal) {
|
|
465
|
+
// Discard further changes inside state_exit per LSL convention.
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
throw e2;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (!this.handlersByState.has(target)) {
|
|
473
|
+
throw new Error(`unknown state '${target}' in state change`);
|
|
474
|
+
}
|
|
475
|
+
this.state.currentState = target;
|
|
476
|
+
if (this.state.coverage)
|
|
477
|
+
this.state.coverage.hitState(target);
|
|
478
|
+
const entry = this.handlersByState.get(target)?.get('state_entry');
|
|
479
|
+
if (entry) {
|
|
480
|
+
pending = { handler: entry, args: [] };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Mark this script as dead and cancel its timer. Used by llDie to bring
|
|
487
|
+
* down every script in the linkset. Idempotent.
|
|
488
|
+
*/
|
|
489
|
+
markDead() {
|
|
490
|
+
this.state.lifecycle.dead = true;
|
|
491
|
+
this.clockView.cancelTimer();
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Replay any events parked while this script was disabled. Caller is
|
|
495
|
+
* responsible for advancing the linkset clock if needed afterwards.
|
|
496
|
+
*/
|
|
497
|
+
resumeParked() {
|
|
498
|
+
if (!this.running)
|
|
499
|
+
return;
|
|
500
|
+
if (this.parkedEvents.length === 0)
|
|
501
|
+
return;
|
|
502
|
+
const events = this.parkedEvents.splice(0, this.parkedEvents.length);
|
|
503
|
+
const at = this.now;
|
|
504
|
+
for (const e of events) {
|
|
505
|
+
this.clockView.schedule(at, e.event, e.payload);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function bindPayload(eventName, payload) {
|
|
510
|
+
const spec = EVENT_SPECS[eventName];
|
|
511
|
+
if (!spec)
|
|
512
|
+
return [];
|
|
513
|
+
const args = [];
|
|
514
|
+
for (const p of spec.params) {
|
|
515
|
+
const v = payload[p.name];
|
|
516
|
+
if (v === undefined) {
|
|
517
|
+
args.push(defaultEvalFor(p.type));
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
args.push({ type: p.type, value: v });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return args;
|
|
524
|
+
}
|
|
525
|
+
function indexHandlers(ast) {
|
|
526
|
+
const out = new Map();
|
|
527
|
+
for (const s of ast.states) {
|
|
528
|
+
const map = new Map();
|
|
529
|
+
for (const h of s.handlers) {
|
|
530
|
+
map.set(h.name, h);
|
|
531
|
+
}
|
|
532
|
+
out.set(s.name, map);
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
function initGlobals(env, globals) {
|
|
537
|
+
for (const g of globals) {
|
|
538
|
+
let init;
|
|
539
|
+
if (g.init) {
|
|
540
|
+
init = literalToEval(g.init, g.typeName);
|
|
541
|
+
}
|
|
542
|
+
env.declare(g.name, g.typeName, init);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function literalToEval(expr, declared) {
|
|
546
|
+
switch (expr.kind) {
|
|
547
|
+
case 'IntegerLiteral':
|
|
548
|
+
return { type: 'integer', value: expr.value | 0 };
|
|
549
|
+
case 'FloatLiteral':
|
|
550
|
+
return { type: 'float', value: expr.value };
|
|
551
|
+
case 'StringLiteral':
|
|
552
|
+
return { type: 'string', value: expr.value };
|
|
553
|
+
case 'Identifier': {
|
|
554
|
+
const c = CONSTANT_TABLE[expr.name];
|
|
555
|
+
if (!c)
|
|
556
|
+
return undefined;
|
|
557
|
+
return { type: c.type, value: c.value };
|
|
558
|
+
}
|
|
559
|
+
case 'VectorLiteral': {
|
|
560
|
+
const x = literalToNumber(expr.x);
|
|
561
|
+
const y = literalToNumber(expr.y);
|
|
562
|
+
const z = literalToNumber(expr.z);
|
|
563
|
+
if (x === null || y === null || z === null)
|
|
564
|
+
return undefined;
|
|
565
|
+
return { type: 'vector', value: { x, y, z } };
|
|
566
|
+
}
|
|
567
|
+
case 'RotationLiteral': {
|
|
568
|
+
const x = literalToNumber(expr.x);
|
|
569
|
+
const y = literalToNumber(expr.y);
|
|
570
|
+
const z = literalToNumber(expr.z);
|
|
571
|
+
const s = literalToNumber(expr.s);
|
|
572
|
+
if (x === null || y === null || z === null || s === null)
|
|
573
|
+
return undefined;
|
|
574
|
+
return { type: 'rotation', value: { x, y, z, s } };
|
|
575
|
+
}
|
|
576
|
+
case 'ListLiteral': {
|
|
577
|
+
const elems = [];
|
|
578
|
+
for (const el of expr.elements) {
|
|
579
|
+
const r = literalToEval(el, 'integer');
|
|
580
|
+
if (!r)
|
|
581
|
+
return undefined;
|
|
582
|
+
elems.push(r.value);
|
|
583
|
+
}
|
|
584
|
+
return { type: 'list', value: elems };
|
|
585
|
+
}
|
|
586
|
+
case 'UnaryExpression': {
|
|
587
|
+
const inner = literalToEval(expr.argument, declared);
|
|
588
|
+
if (!inner)
|
|
589
|
+
return undefined;
|
|
590
|
+
if (expr.operator === '-' && (inner.type === 'integer' || inner.type === 'float')) {
|
|
591
|
+
return { ...inner, value: -inner.value };
|
|
592
|
+
}
|
|
593
|
+
return inner;
|
|
594
|
+
}
|
|
595
|
+
default:
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function literalToNumber(expr) {
|
|
600
|
+
if (expr.kind === 'IntegerLiteral')
|
|
601
|
+
return expr.value | 0;
|
|
602
|
+
if (expr.kind === 'FloatLiteral')
|
|
603
|
+
return expr.value;
|
|
604
|
+
if (expr.kind === 'Identifier') {
|
|
605
|
+
const c = CONSTANT_TABLE[expr.name];
|
|
606
|
+
if (c && typeof c.value === 'number')
|
|
607
|
+
return c.value;
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
if (expr.kind === 'UnaryExpression' && expr.operator === '-') {
|
|
611
|
+
const n = literalToNumber(expr.argument);
|
|
612
|
+
return n === null ? null : -n;
|
|
613
|
+
}
|
|
614
|
+
if (expr.kind === 'UnaryExpression' && expr.operator === '+') {
|
|
615
|
+
return literalToNumber(expr.argument);
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
/** A stable, UUID-shaped string derived from `seed`. */
|
|
620
|
+
function deterministicKey(seed) {
|
|
621
|
+
let h1 = 0x811c9dc5;
|
|
622
|
+
for (let i = 0; i < seed.length; i++) {
|
|
623
|
+
h1 ^= seed.charCodeAt(i);
|
|
624
|
+
h1 = Math.imul(h1, 0x01000193) >>> 0;
|
|
625
|
+
}
|
|
626
|
+
let h2 = (h1 ^ 0xdeadbeef) >>> 0;
|
|
627
|
+
for (let i = 0; i < seed.length; i++) {
|
|
628
|
+
h2 ^= seed.charCodeAt(i) << (i & 31);
|
|
629
|
+
h2 = Math.imul(h2, 0x01000193) >>> 0;
|
|
630
|
+
}
|
|
631
|
+
const hex = (h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0')).repeat(2);
|
|
632
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
633
|
+
}
|
|
634
|
+
function deriveScriptName(filename) {
|
|
635
|
+
if (!filename)
|
|
636
|
+
return 'script';
|
|
637
|
+
const last = filename.split('/').pop() ?? filename;
|
|
638
|
+
return last.replace(/\.lsl$/i, '');
|
|
639
|
+
}
|
|
640
|
+
function inferType(v) {
|
|
641
|
+
if (typeof v === 'number') {
|
|
642
|
+
return Number.isInteger(v) ? 'integer' : 'float';
|
|
643
|
+
}
|
|
644
|
+
if (typeof v === 'string')
|
|
645
|
+
return 'string';
|
|
646
|
+
if (Array.isArray(v))
|
|
647
|
+
return 'list';
|
|
648
|
+
if (v && typeof v === 'object' && 's' in v)
|
|
649
|
+
return 'rotation';
|
|
650
|
+
return 'vector';
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Construct a fresh Env with every kwdb constant pre-declared. Cheap
|
|
654
|
+
* enough at ~950 entries that we just rebuild it per Script instance —
|
|
655
|
+
* this avoids any cross-script aliasing of constant slots.
|
|
656
|
+
*/
|
|
657
|
+
function buildConstantsEnv() {
|
|
658
|
+
const env = new Env(null);
|
|
659
|
+
for (const [name, entry] of Object.entries(CONSTANT_TABLE)) {
|
|
660
|
+
env.declare(name, entry.type, {
|
|
661
|
+
type: entry.type,
|
|
662
|
+
value: entry.value,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
return env;
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=script.js.map
|