@rezless/vm 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/dist/builtins/agents.d.ts +14 -0
  2. package/dist/builtins/agents.d.ts.map +1 -0
  3. package/dist/builtins/agents.js +117 -0
  4. package/dist/builtins/agents.js.map +1 -0
  5. package/dist/builtins/animation.d.ts +46 -0
  6. package/dist/builtins/animation.d.ts.map +1 -0
  7. package/dist/builtins/animation.js +316 -0
  8. package/dist/builtins/animation.js.map +1 -0
  9. package/dist/builtins/attachments.d.ts +9 -0
  10. package/dist/builtins/attachments.d.ts.map +1 -0
  11. package/dist/builtins/attachments.js +127 -0
  12. package/dist/builtins/attachments.js.map +1 -0
  13. package/dist/builtins/base64.d.ts +8 -0
  14. package/dist/builtins/base64.d.ts.map +1 -1
  15. package/dist/builtins/base64.js +1 -1
  16. package/dist/builtins/base64.js.map +1 -1
  17. package/dist/builtins/camera.d.ts +35 -0
  18. package/dist/builtins/camera.d.ts.map +1 -0
  19. package/dist/builtins/camera.js +104 -0
  20. package/dist/builtins/camera.js.map +1 -0
  21. package/dist/builtins/collision.d.ts +21 -0
  22. package/dist/builtins/collision.d.ts.map +1 -0
  23. package/dist/builtins/collision.js +44 -0
  24. package/dist/builtins/collision.js.map +1 -0
  25. package/dist/builtins/controls.d.ts +19 -0
  26. package/dist/builtins/controls.d.ts.map +1 -0
  27. package/dist/builtins/controls.js +27 -0
  28. package/dist/builtins/controls.js.map +1 -0
  29. package/dist/builtins/damage.d.ts +24 -0
  30. package/dist/builtins/damage.d.ts.map +1 -0
  31. package/dist/builtins/damage.js +166 -0
  32. package/dist/builtins/damage.js.map +1 -0
  33. package/dist/builtins/detected.d.ts +22 -1
  34. package/dist/builtins/detected.d.ts.map +1 -1
  35. package/dist/builtins/detected.js +27 -1
  36. package/dist/builtins/detected.js.map +1 -1
  37. package/dist/builtins/dialog.d.ts +23 -0
  38. package/dist/builtins/dialog.d.ts.map +1 -0
  39. package/dist/builtins/dialog.js +69 -0
  40. package/dist/builtins/dialog.js.map +1 -0
  41. package/dist/builtins/email.d.ts +61 -0
  42. package/dist/builtins/email.d.ts.map +1 -0
  43. package/dist/builtins/email.js +121 -0
  44. package/dist/builtins/email.js.map +1 -0
  45. package/dist/builtins/environment.d.ts +27 -0
  46. package/dist/builtins/environment.d.ts.map +1 -0
  47. package/dist/builtins/environment.js +126 -0
  48. package/dist/builtins/environment.js.map +1 -0
  49. package/dist/builtins/experience.d.ts +51 -0
  50. package/dist/builtins/experience.d.ts.map +1 -0
  51. package/dist/builtins/experience.js +202 -0
  52. package/dist/builtins/experience.js.map +1 -0
  53. package/dist/builtins/game-control.d.ts +17 -0
  54. package/dist/builtins/game-control.d.ts.map +1 -0
  55. package/dist/builtins/game-control.js +21 -0
  56. package/dist/builtins/game-control.js.map +1 -0
  57. package/dist/builtins/give.d.ts +16 -0
  58. package/dist/builtins/give.d.ts.map +1 -0
  59. package/dist/builtins/give.js +137 -0
  60. package/dist/builtins/give.js.map +1 -0
  61. package/dist/builtins/ground.d.ts +18 -0
  62. package/dist/builtins/ground.d.ts.map +1 -0
  63. package/dist/builtins/ground.js +83 -0
  64. package/dist/builtins/ground.js.map +1 -0
  65. package/dist/builtins/http.d.ts +99 -4
  66. package/dist/builtins/http.d.ts.map +1 -1
  67. package/dist/builtins/http.js +216 -6
  68. package/dist/builtins/http.js.map +1 -1
  69. package/dist/builtins/index.d.ts.map +1 -1
  70. package/dist/builtins/index.js +235 -10
  71. package/dist/builtins/index.js.map +1 -1
  72. package/dist/builtins/inventory.d.ts +1 -0
  73. package/dist/builtins/inventory.d.ts.map +1 -1
  74. package/dist/builtins/inventory.js +1 -1
  75. package/dist/builtins/inventory.js.map +1 -1
  76. package/dist/builtins/land.d.ts +29 -0
  77. package/dist/builtins/land.d.ts.map +1 -0
  78. package/dist/builtins/land.js +339 -0
  79. package/dist/builtins/land.js.map +1 -0
  80. package/dist/builtins/links.d.ts +20 -0
  81. package/dist/builtins/links.d.ts.map +1 -0
  82. package/dist/builtins/links.js +53 -0
  83. package/dist/builtins/links.js.map +1 -0
  84. package/dist/builtins/linksetdata.d.ts +5 -0
  85. package/dist/builtins/linksetdata.d.ts.map +1 -1
  86. package/dist/builtins/linksetdata.js +16 -0
  87. package/dist/builtins/linksetdata.js.map +1 -1
  88. package/dist/builtins/lists.d.ts +6 -0
  89. package/dist/builtins/lists.d.ts.map +1 -1
  90. package/dist/builtins/lists.js +19 -6
  91. package/dist/builtins/lists.js.map +1 -1
  92. package/dist/builtins/math.d.ts +16 -0
  93. package/dist/builtins/math.d.ts.map +1 -1
  94. package/dist/builtins/math.js +74 -0
  95. package/dist/builtins/math.js.map +1 -1
  96. package/dist/builtins/media.d.ts +8 -0
  97. package/dist/builtins/media.d.ts.map +1 -0
  98. package/dist/builtins/media.js +167 -0
  99. package/dist/builtins/media.js.map +1 -0
  100. package/dist/builtins/motion.d.ts +79 -0
  101. package/dist/builtins/motion.d.ts.map +1 -0
  102. package/dist/builtins/motion.js +396 -0
  103. package/dist/builtins/motion.js.map +1 -0
  104. package/dist/builtins/notecard-search.d.ts +15 -0
  105. package/dist/builtins/notecard-search.d.ts.map +1 -0
  106. package/dist/builtins/notecard-search.js +374 -0
  107. package/dist/builtins/notecard-search.js.map +1 -0
  108. package/dist/builtins/object-animation.d.ts +21 -0
  109. package/dist/builtins/object-animation.d.ts.map +1 -0
  110. package/dist/builtins/object-animation.js +193 -0
  111. package/dist/builtins/object-animation.js.map +1 -0
  112. package/dist/builtins/object-reads.d.ts +39 -0
  113. package/dist/builtins/object-reads.d.ts.map +1 -0
  114. package/dist/builtins/object-reads.js +332 -0
  115. package/dist/builtins/object-reads.js.map +1 -0
  116. package/dist/builtins/object.d.ts +6 -0
  117. package/dist/builtins/object.d.ts.map +1 -1
  118. package/dist/builtins/object.js +19 -3
  119. package/dist/builtins/object.js.map +1 -1
  120. package/dist/builtins/particles.d.ts +4 -0
  121. package/dist/builtins/particles.d.ts.map +1 -0
  122. package/dist/builtins/particles.js +65 -0
  123. package/dist/builtins/particles.js.map +1 -0
  124. package/dist/builtins/pathfinding.d.ts +39 -0
  125. package/dist/builtins/pathfinding.d.ts.map +1 -0
  126. package/dist/builtins/pathfinding.js +83 -0
  127. package/dist/builtins/pathfinding.js.map +1 -0
  128. package/dist/builtins/permissions.d.ts.map +1 -1
  129. package/dist/builtins/permissions.js +6 -0
  130. package/dist/builtins/permissions.js.map +1 -1
  131. package/dist/builtins/physics.d.ts +49 -0
  132. package/dist/builtins/physics.d.ts.map +1 -0
  133. package/dist/builtins/physics.js +47 -0
  134. package/dist/builtins/physics.js.map +1 -0
  135. package/dist/builtins/prim-accessors.d.ts +3 -0
  136. package/dist/builtins/prim-accessors.d.ts.map +1 -1
  137. package/dist/builtins/prim-accessors.js +27 -7
  138. package/dist/builtins/prim-accessors.js.map +1 -1
  139. package/dist/builtins/raycast.d.ts +3 -0
  140. package/dist/builtins/raycast.d.ts.map +1 -0
  141. package/dist/builtins/raycast.js +65 -0
  142. package/dist/builtins/raycast.js.map +1 -0
  143. package/dist/builtins/region.d.ts +31 -0
  144. package/dist/builtins/region.d.ts.map +1 -0
  145. package/dist/builtins/region.js +125 -0
  146. package/dist/builtins/region.js.map +1 -0
  147. package/dist/builtins/remote-load.d.ts +3 -0
  148. package/dist/builtins/remote-load.d.ts.map +1 -0
  149. package/dist/builtins/remote-load.js +53 -0
  150. package/dist/builtins/remote-load.js.map +1 -0
  151. package/dist/builtins/rez.d.ts +10 -0
  152. package/dist/builtins/rez.d.ts.map +1 -0
  153. package/dist/builtins/rez.js +255 -0
  154. package/dist/builtins/rez.js.map +1 -0
  155. package/dist/builtins/rsa.d.ts +4 -0
  156. package/dist/builtins/rsa.d.ts.map +1 -0
  157. package/dist/builtins/rsa.js +44 -0
  158. package/dist/builtins/rsa.js.map +1 -0
  159. package/dist/builtins/script-control.d.ts +6 -2
  160. package/dist/builtins/script-control.d.ts.map +1 -1
  161. package/dist/builtins/script-control.js +9 -4
  162. package/dist/builtins/script-control.js.map +1 -1
  163. package/dist/builtins/sensor.d.ts +11 -0
  164. package/dist/builtins/sensor.d.ts.map +1 -0
  165. package/dist/builtins/sensor.js +145 -0
  166. package/dist/builtins/sensor.js.map +1 -0
  167. package/dist/builtins/sit.d.ts +23 -0
  168. package/dist/builtins/sit.d.ts.map +1 -0
  169. package/dist/builtins/sit.js +150 -0
  170. package/dist/builtins/sit.js.map +1 -0
  171. package/dist/builtins/sound.d.ts +19 -0
  172. package/dist/builtins/sound.d.ts.map +1 -0
  173. package/dist/builtins/sound.js +113 -0
  174. package/dist/builtins/sound.js.map +1 -0
  175. package/dist/builtins/targets.d.ts +12 -0
  176. package/dist/builtins/targets.d.ts.map +1 -0
  177. package/dist/builtins/targets.js +121 -0
  178. package/dist/builtins/targets.js.map +1 -0
  179. package/dist/builtins/time.d.ts +12 -0
  180. package/dist/builtins/time.d.ts.map +1 -1
  181. package/dist/builtins/time.js +32 -4
  182. package/dist/builtins/time.js.map +1 -1
  183. package/dist/builtins/vehicles.d.ts +22 -0
  184. package/dist/builtins/vehicles.d.ts.map +1 -0
  185. package/dist/builtins/vehicles.js +78 -0
  186. package/dist/builtins/vehicles.js.map +1 -0
  187. package/dist/builtins/viewer.d.ts +39 -0
  188. package/dist/builtins/viewer.d.ts.map +1 -0
  189. package/dist/builtins/viewer.js +74 -0
  190. package/dist/builtins/viewer.js.map +1 -0
  191. package/dist/clock.d.ts +29 -1
  192. package/dist/clock.d.ts.map +1 -1
  193. package/dist/clock.js +89 -5
  194. package/dist/clock.js.map +1 -1
  195. package/dist/experience.d.ts +39 -0
  196. package/dist/experience.d.ts.map +1 -0
  197. package/dist/experience.js +38 -0
  198. package/dist/experience.js.map +1 -0
  199. package/dist/index.d.ts +20 -3
  200. package/dist/index.d.ts.map +1 -1
  201. package/dist/index.js +1 -0
  202. package/dist/index.js.map +1 -1
  203. package/dist/interpreter.d.ts +0 -1
  204. package/dist/interpreter.d.ts.map +1 -1
  205. package/dist/interpreter.js.map +1 -1
  206. package/dist/inventory.d.ts +2 -0
  207. package/dist/inventory.d.ts.map +1 -1
  208. package/dist/inventory.js +2 -0
  209. package/dist/inventory.js.map +1 -1
  210. package/dist/linkset.d.ts +139 -11
  211. package/dist/linkset.d.ts.map +1 -1
  212. package/dist/linkset.js +376 -24
  213. package/dist/linkset.js.map +1 -1
  214. package/dist/prim-params.d.ts.map +1 -1
  215. package/dist/prim-params.js +11 -3
  216. package/dist/prim-params.js.map +1 -1
  217. package/dist/prim.d.ts +94 -4
  218. package/dist/prim.d.ts.map +1 -1
  219. package/dist/prim.js +94 -5
  220. package/dist/prim.js.map +1 -1
  221. package/dist/runtime.d.ts +41 -0
  222. package/dist/runtime.d.ts.map +1 -1
  223. package/dist/script.d.ts +147 -2
  224. package/dist/script.d.ts.map +1 -1
  225. package/dist/script.js +373 -19
  226. package/dist/script.js.map +1 -1
  227. package/dist/values/ops.d.ts +3 -0
  228. package/dist/values/ops.d.ts.map +1 -1
  229. package/dist/values/ops.js +2 -2
  230. package/dist/values/ops.js.map +1 -1
  231. package/dist/world.d.ts +247 -1
  232. package/dist/world.d.ts.map +1 -1
  233. package/dist/world.js +318 -6
  234. package/dist/world.js.map +1 -1
  235. package/package.json +2 -2
package/dist/script.js CHANGED
@@ -1,23 +1,37 @@
1
+ import { clearTargetMotion } from './builtins/motion.js';
2
+ import { copyGameControl, gameControlAvatars } from './builtins/game-control.js';
3
+ import { PERMISSION_GAME_CONTROL } from './generated/constants.js';
4
+ import { clearTargets } from './builtins/targets.js';
5
+ import { withCollisionFilter } from './builtins/collision.js';
6
+ import { stopSensors } from './builtins/sensor.js';
7
+ import { enterDamageEvent, leaveDamageEvent, combatAvatars, } from './builtins/damage.js';
8
+ import { sendIncomingHttp } from './builtins/http.js';
9
+ import { deliverEmail, emailQueue } from './builtins/email.js';
10
+ import { characterFor, pathUpdate, setNavigationResults } from './builtins/pathfinding.js';
1
11
  import { truncateUtf8 } from './values/utf8.js';
12
+ import { agentPresence } from './builtins/viewer.js';
13
+ import { defaultCamera } from './builtins/camera.js';
14
+ import { animationAvatars, playingAnimations, setAnimationState } from './builtins/animation.js';
2
15
  import { HaltSignal, ResetScriptSignal } from './builtins/object.js';
3
16
  import { Mulberry32 } from './random.js';
4
17
  import { inventoryItemName } from './inventory.js';
18
+ import { typedList, listElementTypes } from './values/list-types.js';
5
19
  import { NULL_KEY } from './values/types.js';
6
20
  import { execHandler, StateChangeSignal } from './interpreter.js';
7
21
  import { buildCoveragePlan, CoverageCollector } from './coverage.js';
8
22
  import { defaultEvalFor, rot, toFloat32Value, vec } from './values/types.js';
9
- import { typedList } from './values/list-types.js';
10
23
  import { Env } from './env.js';
11
- import { ScriptClockView } from './clock.js';
24
+ import { DELAYED_EVENTS, ScriptClockView } from './clock.js';
12
25
  import { Linkset } from './linkset.js';
13
26
  import { Prim } from './prim.js';
14
27
  import { EVENT_SPECS } from './generated/events.js';
15
28
  import { CONSTANT_TABLE } from './generated/constants_table.js';
16
- import { DEBUG_CHANNEL, HTTP_BODY_TRUNCATED } from './generated/constants.js';
29
+ import { DEBUG_CHANNEL, HTTP_BODY_TRUNCATED, PERMISSION_TAKE_CONTROLS, XP_ERROR_NOT_PERMITTED, } from './generated/constants.js';
17
30
  import { own } from './own.js';
18
31
  import { deterministicKey } from './keys.js';
19
32
  import { Memory, RuntimeError, codeBytes } from './memory.js';
20
33
  import { invalidPrograms } from './typecheck.js';
34
+ import { grantExperience } from './builtins/experience.js';
21
35
  /**
22
36
  * Public handle for a loaded LSL script. Tests interact almost entirely
23
37
  * with this surface: drive events via fire(), inspect chat / calls,
@@ -25,6 +39,7 @@ import { invalidPrograms } from './typecheck.js';
25
39
  */
26
40
  export class Script {
27
41
  ast;
42
+ options;
28
43
  state;
29
44
  mocks = Object.create(null);
30
45
  globals;
@@ -38,22 +53,28 @@ export class Script {
38
53
  /** Per-script clock facade (timer state + time reference). */
39
54
  clockView;
40
55
  host;
41
- linkset;
42
56
  scriptName;
43
57
  /** When false, drainQueue parks events instead of delivering them. */
44
58
  running = true;
45
59
  /** Events parked while `running === false`; replayed when re-enabled. */
46
60
  parkedEvents = [];
61
+ /** llGetStartParameter: the linkset's start parameter until the script resets, then 0. */
62
+ startParameter;
47
63
  /** The handler chain suspended in llSleep or a builtin delay, if any. */
48
64
  task = null;
65
+ /** Virtual time this script last started a handler or woke from a sleep. */
66
+ resumedAt = 0;
49
67
  constructor(ast, options = {}) {
50
68
  this.ast = ast;
69
+ this.options = options;
51
70
  // Auto-wrap: if no host prim was provided, create a default 1-prim linkset.
52
71
  let host = options.host;
53
72
  if (!host) {
54
73
  const linkset = new Linkset({
55
74
  owner: options.owner ?? NULL_KEY,
56
75
  startUnixTime: options.startUnixTime ?? 0,
76
+ startParameter: options.startParameter ?? 0,
77
+ startString: options.startString ?? '',
57
78
  });
58
79
  const prim = new Prim({
59
80
  key: options.objectKey ??
@@ -67,15 +88,14 @@ export class Script {
67
88
  if (!host.linkset) {
68
89
  throw new Error('Script host prim must belong to a Linkset');
69
90
  }
70
- this.linkset = host.linkset;
91
+ this.startParameter = this.linkset.startParameter;
71
92
  this.scriptName = inventoryItemName(options.scriptName ?? deriveScriptName(options.filename));
72
93
  this.clockView = new ScriptClockView(this.linkset.clock, () => this);
73
- const linkset = this.linkset;
74
94
  const prim = host;
75
95
  const scriptName = this.scriptName;
76
96
  const identity = {
77
97
  get owner() {
78
- return linkset.owner;
98
+ return prim.linkset.owner;
79
99
  },
80
100
  get objectKey() {
81
101
  return prim.key;
@@ -102,6 +122,13 @@ export class Script {
102
122
  clock: this.clockView,
103
123
  httpRequests: [],
104
124
  httpKeyCounter: 0,
125
+ urls: [],
126
+ urlPolicy: 'grant',
127
+ urlCounter: 0,
128
+ incomingHttp: [],
129
+ httpInCounter: 0,
130
+ emails: [],
131
+ targetedEmails: [],
105
132
  listens: [],
106
133
  listenHandleCounter: 0,
107
134
  random: new Mulberry32(options.randomSeed ?? 1),
@@ -110,9 +137,25 @@ export class Script {
110
137
  dataserverRequests: [],
111
138
  dataserverKeyCounter: 0,
112
139
  detectedStack: [],
113
- linksetData: this.linkset.linksetData,
140
+ get linksetData() {
141
+ return prim.linkset.linksetData;
142
+ },
114
143
  appearance: this.host.appearance,
144
+ gameControlSnapshot: null,
115
145
  permissions: { granted: 0, agent: NULL_KEY, requests: [] },
146
+ experience: options.experience ?? null,
147
+ experienceRequests: [],
148
+ controls: { taken: null, level: 0 },
149
+ camera: defaultCamera(),
150
+ cameraParams: new Map(),
151
+ get animations() {
152
+ return animationAvatars(prim.linkset);
153
+ },
154
+ inventoryOffers: [],
155
+ viewerRequests: [],
156
+ teleportRequests: [],
157
+ agentPresence: agentPresence(prim.linkset),
158
+ dialogs: [],
116
159
  lifecycle: {
117
160
  dead: false,
118
161
  dying: false,
@@ -144,7 +187,21 @@ export class Script {
144
187
  host.addScript(this);
145
188
  }
146
189
  }
190
+ /** A state transition leaves on_damage before lifecycle handlers execute. */
191
+ clearDamageAdjustment() {
192
+ const context = this.state.detectedStack.at(-1);
193
+ if (context?.event === 'on_damage')
194
+ context.event = '';
195
+ }
196
+ /** Configure an avatar's combat health, parcel and attachment scripts. */
197
+ setCombatAvatar(id, avatar) {
198
+ combatAvatars(this).set(id.toLowerCase(), { ...avatar, region: this.linkset.region });
199
+ }
147
200
  /** Current virtual time in milliseconds since linkset construction. */
201
+ /** The linkset holding this script's prim; it changes when the prim is linked or unlinked. */
202
+ get linkset() {
203
+ return this.host.linkset;
204
+ }
148
205
  get now() {
149
206
  return this.linkset.clock.now;
150
207
  }
@@ -187,6 +244,18 @@ export class Script {
187
244
  callsOf(name) {
188
245
  return this.state.calls.filter((c) => c.name === name);
189
246
  }
247
+ /** The object's character, or null before creation and after deletion. */
248
+ get character() {
249
+ return characterFor(this);
250
+ }
251
+ /** Set fixed navigation query answers. These fixtures survive script resets. */
252
+ setNavigationResults(results) {
253
+ setNavigationResults(this, results);
254
+ }
255
+ /** Raise path_update on every script in the character's root prim. */
256
+ pathUpdate(type, reserved = []) {
257
+ pathUpdate(this, type, reserved);
258
+ }
190
259
  /** Captured outgoing HTTP requests from `llHTTPRequest`. */
191
260
  get httpRequests() {
192
261
  return this.state.httpRequests;
@@ -224,6 +293,46 @@ export class Script {
224
293
  throw new Error('no HTTP request to respond to');
225
294
  this.respondToHttp(req.key, response);
226
295
  }
296
+ /** URLs the script holds from llRequestURL / llRequestSecureURL, oldest first. */
297
+ get urls() {
298
+ return this.state.urls;
299
+ }
300
+ /** Grant later llRequestURL / llRequestSecureURL calls (the default). */
301
+ grantUrls() {
302
+ this.state.urlPolicy = 'grant';
303
+ }
304
+ /** Deny later llRequestURL / llRequestSecureURL calls with URL_REQUEST_DENIED. */
305
+ denyUrls() {
306
+ this.state.urlPolicy = 'deny';
307
+ }
308
+ /**
309
+ * Send a request to one of the script's URLs, with a path and query after
310
+ * it if you like, and fire `http_request`. The returned request's
311
+ * `response` holds what the script answered with llHTTPResponse.
312
+ */
313
+ sendHttpRequest(url, init = {}) {
314
+ return sendIncomingHttp(this, this.state, url, init);
315
+ }
316
+ /** Mail the script sent with llEmail, oldest first. */
317
+ get emails() {
318
+ return this.state.emails;
319
+ }
320
+ /** Mail the script sent with llTargetedEmail, oldest first. */
321
+ get targetedEmails() {
322
+ return this.state.targetedEmails;
323
+ }
324
+ /** Mail queued on the script's prim, oldest first, waiting for llGetNextEmail. */
325
+ get inbox() {
326
+ return emailQueue(this.host);
327
+ }
328
+ /**
329
+ * Queue mail on the script's prim. It fires `email` only when a script in
330
+ * the prim asks for it with llGetNextEmail. Returns false, queuing
331
+ * nothing, when the prim already holds 101 mails.
332
+ */
333
+ deliverEmail(init) {
334
+ return deliverEmail(this, init);
335
+ }
227
336
  /** Currently active listen registrations (from `llListen`). */
228
337
  get listens() {
229
338
  return this.state.listens;
@@ -280,6 +389,8 @@ export class Script {
280
389
  const granted = req.permissions & (permissions ?? req.permissions);
281
390
  this.state.permissions.granted = granted;
282
391
  this.state.permissions.agent = req.agent;
392
+ if (!(granted & PERMISSION_TAKE_CONTROLS))
393
+ this.state.controls.taken = null;
283
394
  const started = this.handles('run_time_permissions') ? [this] : [];
284
395
  this.clockView.schedule(this.now, 'run_time_permissions', { perm: granted });
285
396
  this.linkset.settle(started);
@@ -288,10 +399,170 @@ export class Script {
288
399
  denyPermissions() {
289
400
  this.grantPermissions(0);
290
401
  }
402
+ /** The experience the script was compiled with, or null. */
403
+ get experience() {
404
+ return this.state.experience;
405
+ }
406
+ /** llRequestExperiencePermissions calls waiting for an answer, oldest first. */
407
+ get experienceRequests() {
408
+ return this.state.experienceRequests;
409
+ }
410
+ /**
411
+ * Accept the most recent unanswered llRequestExperiencePermissions: the
412
+ * agent joins the experience, the script holds the experience permissions
413
+ * for that agent, and `experience_permissions` fires.
414
+ */
415
+ grantExperiencePermissions() {
416
+ const req = this.answerExperienceRequest();
417
+ const started = this.handles('experience_permissions') ? [this] : [];
418
+ grantExperience(this.state, req.agent);
419
+ this.linkset.settle(started);
420
+ }
421
+ /**
422
+ * Decline the most recent unanswered llRequestExperiencePermissions:
423
+ * `experience_permissions_denied` fires with `reason`
424
+ * (XP_ERROR_NOT_PERMITTED by default).
425
+ */
426
+ denyExperiencePermissions(reason = XP_ERROR_NOT_PERMITTED) {
427
+ const req = this.answerExperienceRequest();
428
+ this.fireAndSettle('experience_permissions_denied', { agent_id: req.agent, reason });
429
+ }
430
+ answerExperienceRequest() {
431
+ const req = [...this.state.experienceRequests].reverse().find((r) => !r.answered);
432
+ if (!req)
433
+ throw new Error('no pending experience permission request');
434
+ req.answered = true;
435
+ return req;
436
+ }
437
+ fireAndSettle(event, payload) {
438
+ const started = this.handles(event) ? [this] : [];
439
+ this.clockView.schedule(this.now, event, payload);
440
+ this.linkset.settle(started);
441
+ }
442
+ /** Controls taken with llTakeControls, or null when none are. */
443
+ get controls() {
444
+ return this.state.controls.taken;
445
+ }
446
+ /**
447
+ * Have the permission agent hold exactly the controls in `level` (CONTROL_*
448
+ * bits; 0 lets go of all). Fires `control` with the held and just-changed
449
+ * bits among those taken, if the script took controls with accept TRUE and
450
+ * any of them is held or changed.
451
+ */
452
+ sendControl(level) {
453
+ const edge = level ^ this.state.controls.level;
454
+ this.state.controls.level = level;
455
+ const taken = this.state.controls.taken;
456
+ if (!taken?.accept)
457
+ return;
458
+ const held = level & taken.controls;
459
+ const changed = edge & taken.controls;
460
+ if (held === 0 && changed === 0)
461
+ return;
462
+ const started = this.handles('control') ? [this] : [];
463
+ this.clockView.schedule(this.now, 'control', { id: this.state.permissions.agent, level: held, edge: changed });
464
+ this.linkset.settle(started);
465
+ }
466
+ /** Set an avatar's semantic input fixture, shared by scripts in this World. */
467
+ setGameControl(agent, input) {
468
+ const avatars = gameControlAvatars(this.linkset);
469
+ if (input === null)
470
+ avatars.delete(agent.toLowerCase());
471
+ else
472
+ avatars.set(agent.toLowerCase(), copyGameControl(input));
473
+ }
474
+ /** Queue raw input and a copy of the avatar's semantic fixture for this script. */
475
+ sendGameControl(agent, raw = {}) {
476
+ if (!(this.state.permissions.granted & PERMISSION_GAME_CONTROL)
477
+ || agent.toLowerCase() !== this.state.permissions.agent.toLowerCase())
478
+ return;
479
+ const input = gameControlAvatars(this.linkset).get(agent.toLowerCase());
480
+ const snapshot = input ? { ...copyGameControl(input), agent: agent.toLowerCase() } : null;
481
+ this.fire('game_control', {
482
+ id: agent, button_levels: (raw.buttons ?? 0) | 0,
483
+ axes: typedList((raw.axes ?? []).map(Math.fround), (raw.axes ?? []).map(() => 'float')),
484
+ gameControlSnapshot: snapshot,
485
+ });
486
+ }
487
+ /** Set the permission agent's camera that llGetCameraPos / Rot / FOV / Aspect report. */
488
+ setCamera(camera) {
489
+ const { pos, rot: r, fov, aspect } = { ...this.state.camera, ...camera };
490
+ this.state.camera = {
491
+ pos: vec(pos.x, pos.y, pos.z),
492
+ rot: rot(r.x, r.y, r.z, r.s),
493
+ fov: Math.fround(fov),
494
+ aspect: Math.fround(aspect),
495
+ };
496
+ }
497
+ /** Camera rules set with llSetCameraParams, by rule. */
498
+ get cameraParams() {
499
+ return this.state.cameraParams;
500
+ }
501
+ /** Register an avatar in this region and set its default locomotion animation. */
502
+ setAnimationState(agent, state) {
503
+ setAnimationState(this.state.animations, agent, state);
504
+ }
505
+ /** Playing animation UUIDs for an avatar, in UUID order. Returns a fresh array. */
506
+ getPlayingAnimations(agent) {
507
+ return playingAnimations(this.state.animations, agent);
508
+ }
509
+ /** Inventory offers and completed prim deliveries, oldest first. */
510
+ get inventoryOffers() {
511
+ return this.state.inventoryOffers;
512
+ }
513
+ /** Teleport attempts made by this script, oldest first. */
514
+ get teleportRequests() {
515
+ return this.state.teleportRequests;
516
+ }
517
+ /** URL/map requests and rejected floater calls, oldest first. */
518
+ get viewerRequests() {
519
+ return this.state.viewerRequests;
520
+ }
521
+ /** Override whether an avatar is present in this region, for every script sharing it. */
522
+ setAgentInRegion(agent, present = true) {
523
+ this.state.agentPresence.set(agent.toLowerCase(), present);
524
+ }
525
+ /** Dialogs and text boxes this script showed with llDialog / llTextBox, oldest first. */
526
+ get dialogs() {
527
+ return this.state.dialogs;
528
+ }
529
+ /**
530
+ * Answer a dialog as its avatar: the most recent unanswered one, or
531
+ * `opts.dialog`. The avatar says `text` on the dialog's channel, heard by
532
+ * listens within 20 m of the prim that showed it. A llDialog answer must
533
+ * be one of its buttons; a text box takes any text.
534
+ */
535
+ answerDialog(text, opts = {}) {
536
+ const dialog = opts.dialog ?? [...this.state.dialogs].reverse().find((d) => !d.answered);
537
+ if (!dialog)
538
+ throw new Error('no unanswered dialog');
539
+ if (!this.state.dialogs.includes(dialog))
540
+ throw new Error('that dialog was not shown by this script');
541
+ if (dialog.answered)
542
+ throw new Error('that dialog was already answered');
543
+ if (dialog.kind === 'dialog' && !dialog.buttons.includes(text)) {
544
+ throw new Error(`'${text}' is not a button on the dialog: ${dialog.buttons.join(', ')}`);
545
+ }
546
+ dialog.answered = true;
547
+ const chat = { channel: dialog.channel, name: opts.name ?? '', key: dialog.agent, message: text };
548
+ this.linkset.settle(this.linkset.routeAvatarChat(this.host, chat).filter((s) => s.handles('listen')));
549
+ }
291
550
  /** Currently displayed floating text (from llSetText). null if unset. */
292
551
  get text() {
293
552
  return this.state.appearance.text;
294
553
  }
554
+ /** The prim's llSetTouchText text, '' if unset. */
555
+ get touchText() {
556
+ return this.host.touchText;
557
+ }
558
+ /** The prim's llSetSitText text, '' if unset. */
559
+ get sitText() {
560
+ return this.host.sitText;
561
+ }
562
+ /** The prim's llSetPayPrice arguments, null if unset. */
563
+ get payPrice() {
564
+ return this.host.payPrice;
565
+ }
295
566
  /** Object description from llSetObjectDesc. */
296
567
  get objectDesc() {
297
568
  return this.state.appearance.description;
@@ -312,6 +583,10 @@ export class Script {
312
583
  this.state.linksetData.set(k, { value: v.value, password: v.password ?? '' });
313
584
  }
314
585
  }
586
+ /** The script's memory limit in bytes, as llGetMemoryLimit reads it. */
587
+ get memoryLimit() {
588
+ return this.state.memory.limit;
589
+ }
315
590
  /** True once `llDie()` has been called. Subsequent fire() calls are no-ops. */
316
591
  get dead() {
317
592
  return this.state.lifecycle.dead;
@@ -397,7 +672,7 @@ export class Script {
397
672
  if (this.state.lifecycle.dead)
398
673
  return;
399
674
  if (!this.running) {
400
- this.parkedEvents.push({ at: this.now, event: eventName, payload });
675
+ withCollisionFilter(this, eventName, payload, p => this.parkedEvents.push({ at: this.now, event: eventName, payload: p }));
401
676
  return;
402
677
  }
403
678
  if (!this.started) {
@@ -408,12 +683,15 @@ export class Script {
408
683
  this.linkset.settle([this]);
409
684
  }
410
685
  }
411
- const started = this.handles(eventName) ? [this] : [];
412
- if (this.task)
413
- this.clockView.schedule(this.now, eventName, payload);
414
- else
415
- this.deliver(eventName, payload);
416
- this.linkset.settle(started);
686
+ withCollisionFilter(this, eventName, payload, p => {
687
+ const started = this.handles(eventName) ? [this] : [];
688
+ if (this.task || (DELAYED_EVENTS.has(eventName) && this.clockView.delayedUntilMs > this.now)) {
689
+ this.clockView.schedule(this.now, eventName, p);
690
+ }
691
+ else
692
+ this.deliver(eventName, p);
693
+ this.linkset.settle(started);
694
+ });
417
695
  }
418
696
  /**
419
697
  * Run state_entry of the default state. If the script has been auto-started
@@ -427,6 +705,55 @@ export class Script {
427
705
  this.boot();
428
706
  this.linkset.settle([this]);
429
707
  }
708
+ /** Queue a fresh inventory script's first event without waiting for its handler. */
709
+ queueStart() {
710
+ if (this.started || this.dead)
711
+ return;
712
+ this.started = true;
713
+ this.clockView.schedule(this.now, 'state_entry');
714
+ }
715
+ /** Copy inventory code; fresh copies omit saved runtime state for remote loading. */
716
+ copyTo(host, opts = {}) {
717
+ const copy = new Script(this.ast, { ...this.options, host, coverage: false });
718
+ Object.assign(copy.mocks, this.mocks);
719
+ copy.state.coverage = this.state.coverage;
720
+ if (opts.fresh)
721
+ return copy;
722
+ for (const g of this.ast.globals) {
723
+ const value = this.global(g.name);
724
+ const saved = Array.isArray(value)
725
+ ? typedList(structuredClone(value), listElementTypes(value) ?? [])
726
+ : structuredClone(value);
727
+ copy.setGlobal(g.name, saved);
728
+ }
729
+ copy.state.memory.limit = this.state.memory.limit;
730
+ copy.state.currentState = this.state.currentState;
731
+ copy.started = this.started;
732
+ copy.running = this.running && !this.dead;
733
+ Object.assign(copy.state.random, this.state.random);
734
+ copy.state.listens.push(...structuredClone(this.state.listens));
735
+ copy.state.listenHandleCounter = this.state.listenHandleCounter;
736
+ copy.clockView.timerIntervalMs = this.clockView.timerIntervalMs;
737
+ copy.clockView.timerNextFireMs = copy.now + Math.max(0, this.clockView.timerNextFireMs - this.now);
738
+ copy.clockView.timeReferenceMs = copy.now - (this.now - this.clockView.timeReferenceMs);
739
+ copy.clockView.minEventDelayMs = this.clockView.minEventDelayMs;
740
+ for (const kind of ['move', 'look']) {
741
+ const request = host.linkset.prims[0].targetMotion[kind];
742
+ if (request?.owner.prim === this.host.key && request.owner.script === this.scriptName)
743
+ request.owner.prim = host.key;
744
+ }
745
+ return copy;
746
+ }
747
+ /** Queue rez lifecycle events after restoring the inventory script's saved data. */
748
+ queueRez() {
749
+ if (!this.running)
750
+ return;
751
+ if (!this.started) {
752
+ this.started = true;
753
+ this.clockView.schedule(this.now, 'state_entry');
754
+ }
755
+ this.clockView.schedule(this.now, 'on_rez', { start_param: this.startParameter });
756
+ }
430
757
  boot() {
431
758
  this.started = true;
432
759
  const entry = this.handlersByState.get('default')?.get('state_entry');
@@ -461,11 +788,20 @@ export class Script {
461
788
  this.state.lifecycle.dying = false;
462
789
  this.state.lifecycle.removed = false;
463
790
  this.state.listens.length = 0;
791
+ this.state.urls.length = 0;
464
792
  this.state.permissions.granted = 0;
465
793
  this.state.permissions.agent = NULL_KEY;
466
794
  for (const r of this.state.permissions.requests)
467
795
  r.answered = true;
796
+ for (const r of this.state.experienceRequests)
797
+ r.answered = true;
798
+ this.state.controls.taken = null;
799
+ stopSensors(this);
800
+ clearTargets(this);
801
+ clearTargetMotion(this);
468
802
  this.clockView.cancelTimer();
803
+ this.clockView.setMinEventDelay(0);
804
+ this.startParameter = 0;
469
805
  this.clockView.resetReference();
470
806
  this.linkset.clock.purgeTarget(this);
471
807
  this.parkedEvents.length = 0;
@@ -494,8 +830,11 @@ export class Script {
494
830
  return;
495
831
  }
496
832
  const handler = this.handlersByState.get(this.state.currentState)?.get(event);
497
- if (handler)
498
- this.begin(handler, bindPayload(event, payload), payload);
833
+ if (!handler)
834
+ return;
835
+ if (DELAYED_EVENTS.has(event))
836
+ this.clockView.delayedUntilMs = this.now + this.clockView.minEventDelayMs;
837
+ this.begin(handler, bindPayload(event, payload), payload);
499
838
  }
500
839
  /** Whether the current state has a handler for `event`. */
501
840
  handles(event) {
@@ -517,6 +856,7 @@ export class Script {
517
856
  this.step({ run: this.runHandler(handler, args, payload), startedAt: this.now, wakeAt: 0 });
518
857
  }
519
858
  step(task) {
859
+ this.resumedAt = this.now;
520
860
  let r;
521
861
  try {
522
862
  r = task.run.next();
@@ -530,6 +870,7 @@ export class Script {
530
870
  return;
531
871
  }
532
872
  this.finish(task);
873
+ this.clockView.finishHandler(r.value);
533
874
  if (r.value)
534
875
  this.restart();
535
876
  }
@@ -554,10 +895,15 @@ export class Script {
554
895
  * does `state foo;` continues the chain correctly.
555
896
  */
556
897
  *runHandler(handler, args, payload) {
898
+ this.state.gameControlSnapshot = handler.name === 'game_control'
899
+ ? payload['gameControlSnapshot'] ?? null : null;
900
+ enterDamageEvent(payload);
557
901
  const detected = payload['detected'];
558
902
  const pushed = Array.isArray(detected) && detected.length > 0;
559
903
  if (pushed)
560
- this.state.detectedStack.push({ entries: detected });
904
+ this.state.detectedStack.push({
905
+ entries: detected, event: handler.name,
906
+ });
561
907
  try {
562
908
  this.state.memory.startEvent();
563
909
  return yield* this.runHandlerChain(handler, args);
@@ -570,6 +916,8 @@ export class Script {
570
916
  finally {
571
917
  if (pushed)
572
918
  this.state.detectedStack.pop();
919
+ this.state.gameControlSnapshot = null;
920
+ leaveDamageEvent(payload);
573
921
  }
574
922
  }
575
923
  /** Returns true when the chain ended in llResetScript. */
@@ -583,7 +931,6 @@ export class Script {
583
931
  invalidPrograms: this.invalidPrograms,
584
932
  script: this,
585
933
  prim: this.host,
586
- linkset: this.linkset,
587
934
  };
588
935
  let pending = {
589
936
  handler,
@@ -602,6 +949,8 @@ export class Script {
602
949
  return false;
603
950
  if (!(e instanceof StateChangeSignal))
604
951
  throw e;
952
+ this.state.gameControlSnapshot = null;
953
+ this.clearDamageAdjustment();
605
954
  if (this.state.lifecycle.removed)
606
955
  return false;
607
956
  const target = e.target;
@@ -626,6 +975,7 @@ export class Script {
626
975
  if (!this.handlersByState.has(target)) {
627
976
  throw new Error(`unknown state '${target}' in state change`);
628
977
  }
978
+ stopSensors(this);
629
979
  this.state.listens.length = 0;
630
980
  this.linkset.clock.purgeTarget(this, this.now, '__reset');
631
981
  this.state.currentState = target;
@@ -656,6 +1006,10 @@ export class Script {
656
1006
  */
657
1007
  markDead() {
658
1008
  this.state.lifecycle.dead = true;
1009
+ this.state.urls.length = 0;
1010
+ stopSensors(this);
1011
+ clearTargets(this);
1012
+ clearTargetMotion(this);
659
1013
  this.clockView.cancelTimer();
660
1014
  this.abandon();
661
1015
  }