@rezless/vm 0.6.2 → 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 (273) 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 +13 -2
  14. package/dist/builtins/base64.d.ts.map +1 -1
  15. package/dist/builtins/base64.js +8 -6
  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 +28 -2
  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/hash.d.ts +1 -0
  66. package/dist/builtins/hash.d.ts.map +1 -1
  67. package/dist/builtins/hash.js +12 -2
  68. package/dist/builtins/hash.js.map +1 -1
  69. package/dist/builtins/http.d.ts +99 -4
  70. package/dist/builtins/http.d.ts.map +1 -1
  71. package/dist/builtins/http.js +216 -6
  72. package/dist/builtins/http.js.map +1 -1
  73. package/dist/builtins/index.d.ts.map +1 -1
  74. package/dist/builtins/index.js +250 -14
  75. package/dist/builtins/index.js.map +1 -1
  76. package/dist/builtins/inventory.d.ts +8 -5
  77. package/dist/builtins/inventory.d.ts.map +1 -1
  78. package/dist/builtins/inventory.js +37 -15
  79. package/dist/builtins/inventory.js.map +1 -1
  80. package/dist/builtins/json.d.ts.map +1 -1
  81. package/dist/builtins/json.js +45 -34
  82. package/dist/builtins/json.js.map +1 -1
  83. package/dist/builtins/land.d.ts +29 -0
  84. package/dist/builtins/land.d.ts.map +1 -0
  85. package/dist/builtins/land.js +339 -0
  86. package/dist/builtins/land.js.map +1 -0
  87. package/dist/builtins/link-info.js +2 -2
  88. package/dist/builtins/link-info.js.map +1 -1
  89. package/dist/builtins/links.d.ts +20 -0
  90. package/dist/builtins/links.d.ts.map +1 -0
  91. package/dist/builtins/links.js +53 -0
  92. package/dist/builtins/links.js.map +1 -0
  93. package/dist/builtins/linksetdata.d.ts +5 -0
  94. package/dist/builtins/linksetdata.d.ts.map +1 -1
  95. package/dist/builtins/linksetdata.js +22 -9
  96. package/dist/builtins/linksetdata.js.map +1 -1
  97. package/dist/builtins/lists.d.ts +21 -0
  98. package/dist/builtins/lists.d.ts.map +1 -1
  99. package/dist/builtins/lists.js +137 -17
  100. package/dist/builtins/lists.js.map +1 -1
  101. package/dist/builtins/math.d.ts +18 -0
  102. package/dist/builtins/math.d.ts.map +1 -1
  103. package/dist/builtins/math.js +104 -5
  104. package/dist/builtins/math.js.map +1 -1
  105. package/dist/builtins/media.d.ts +8 -0
  106. package/dist/builtins/media.d.ts.map +1 -0
  107. package/dist/builtins/media.js +167 -0
  108. package/dist/builtins/media.js.map +1 -0
  109. package/dist/builtins/motion.d.ts +79 -0
  110. package/dist/builtins/motion.d.ts.map +1 -0
  111. package/dist/builtins/motion.js +396 -0
  112. package/dist/builtins/motion.js.map +1 -0
  113. package/dist/builtins/notecard-search.d.ts +15 -0
  114. package/dist/builtins/notecard-search.d.ts.map +1 -0
  115. package/dist/builtins/notecard-search.js +374 -0
  116. package/dist/builtins/notecard-search.js.map +1 -0
  117. package/dist/builtins/object-animation.d.ts +21 -0
  118. package/dist/builtins/object-animation.d.ts.map +1 -0
  119. package/dist/builtins/object-animation.js +193 -0
  120. package/dist/builtins/object-animation.js.map +1 -0
  121. package/dist/builtins/object-reads.d.ts +39 -0
  122. package/dist/builtins/object-reads.d.ts.map +1 -0
  123. package/dist/builtins/object-reads.js +332 -0
  124. package/dist/builtins/object-reads.js.map +1 -0
  125. package/dist/builtins/object.d.ts +11 -2
  126. package/dist/builtins/object.d.ts.map +1 -1
  127. package/dist/builtins/object.js +32 -8
  128. package/dist/builtins/object.js.map +1 -1
  129. package/dist/builtins/particles.d.ts +4 -0
  130. package/dist/builtins/particles.d.ts.map +1 -0
  131. package/dist/builtins/particles.js +65 -0
  132. package/dist/builtins/particles.js.map +1 -0
  133. package/dist/builtins/pathfinding.d.ts +39 -0
  134. package/dist/builtins/pathfinding.d.ts.map +1 -0
  135. package/dist/builtins/pathfinding.js +83 -0
  136. package/dist/builtins/pathfinding.js.map +1 -0
  137. package/dist/builtins/permissions.d.ts.map +1 -1
  138. package/dist/builtins/permissions.js +6 -0
  139. package/dist/builtins/permissions.js.map +1 -1
  140. package/dist/builtins/physics.d.ts +49 -0
  141. package/dist/builtins/physics.d.ts.map +1 -0
  142. package/dist/builtins/physics.js +47 -0
  143. package/dist/builtins/physics.js.map +1 -0
  144. package/dist/builtins/prim-accessors.d.ts +4 -0
  145. package/dist/builtins/prim-accessors.d.ts.map +1 -1
  146. package/dist/builtins/prim-accessors.js +75 -28
  147. package/dist/builtins/prim-accessors.js.map +1 -1
  148. package/dist/builtins/prim-params.d.ts.map +1 -1
  149. package/dist/builtins/prim-params.js +6 -2
  150. package/dist/builtins/prim-params.js.map +1 -1
  151. package/dist/builtins/raycast.d.ts +3 -0
  152. package/dist/builtins/raycast.d.ts.map +1 -0
  153. package/dist/builtins/raycast.js +65 -0
  154. package/dist/builtins/raycast.js.map +1 -0
  155. package/dist/builtins/region.d.ts +31 -0
  156. package/dist/builtins/region.d.ts.map +1 -0
  157. package/dist/builtins/region.js +125 -0
  158. package/dist/builtins/region.js.map +1 -0
  159. package/dist/builtins/remote-load.d.ts +3 -0
  160. package/dist/builtins/remote-load.d.ts.map +1 -0
  161. package/dist/builtins/remote-load.js +53 -0
  162. package/dist/builtins/remote-load.js.map +1 -0
  163. package/dist/builtins/rez.d.ts +10 -0
  164. package/dist/builtins/rez.d.ts.map +1 -0
  165. package/dist/builtins/rez.js +255 -0
  166. package/dist/builtins/rez.js.map +1 -0
  167. package/dist/builtins/rsa.d.ts +4 -0
  168. package/dist/builtins/rsa.d.ts.map +1 -0
  169. package/dist/builtins/rsa.js +44 -0
  170. package/dist/builtins/rsa.js.map +1 -0
  171. package/dist/builtins/script-control.d.ts +8 -3
  172. package/dist/builtins/script-control.d.ts.map +1 -1
  173. package/dist/builtins/script-control.js +27 -8
  174. package/dist/builtins/script-control.js.map +1 -1
  175. package/dist/builtins/sensor.d.ts +11 -0
  176. package/dist/builtins/sensor.d.ts.map +1 -0
  177. package/dist/builtins/sensor.js +145 -0
  178. package/dist/builtins/sensor.js.map +1 -0
  179. package/dist/builtins/sit.d.ts +23 -0
  180. package/dist/builtins/sit.d.ts.map +1 -0
  181. package/dist/builtins/sit.js +150 -0
  182. package/dist/builtins/sit.js.map +1 -0
  183. package/dist/builtins/sound.d.ts +19 -0
  184. package/dist/builtins/sound.d.ts.map +1 -0
  185. package/dist/builtins/sound.js +113 -0
  186. package/dist/builtins/sound.js.map +1 -0
  187. package/dist/builtins/strings.d.ts +6 -0
  188. package/dist/builtins/strings.d.ts.map +1 -1
  189. package/dist/builtins/strings.js +47 -6
  190. package/dist/builtins/strings.js.map +1 -1
  191. package/dist/builtins/targets.d.ts +12 -0
  192. package/dist/builtins/targets.d.ts.map +1 -0
  193. package/dist/builtins/targets.js +121 -0
  194. package/dist/builtins/targets.js.map +1 -0
  195. package/dist/builtins/time.d.ts +15 -5
  196. package/dist/builtins/time.d.ts.map +1 -1
  197. package/dist/builtins/time.js +36 -11
  198. package/dist/builtins/time.js.map +1 -1
  199. package/dist/builtins/vehicles.d.ts +22 -0
  200. package/dist/builtins/vehicles.d.ts.map +1 -0
  201. package/dist/builtins/vehicles.js +78 -0
  202. package/dist/builtins/vehicles.js.map +1 -0
  203. package/dist/builtins/viewer.d.ts +39 -0
  204. package/dist/builtins/viewer.d.ts.map +1 -0
  205. package/dist/builtins/viewer.js +74 -0
  206. package/dist/builtins/viewer.js.map +1 -0
  207. package/dist/clock.d.ts +45 -7
  208. package/dist/clock.d.ts.map +1 -1
  209. package/dist/clock.js +125 -14
  210. package/dist/clock.js.map +1 -1
  211. package/dist/dispatch.d.ts +3 -2
  212. package/dist/dispatch.d.ts.map +1 -1
  213. package/dist/dispatch.js +15 -10
  214. package/dist/dispatch.js.map +1 -1
  215. package/dist/experience.d.ts +39 -0
  216. package/dist/experience.d.ts.map +1 -0
  217. package/dist/experience.js +38 -0
  218. package/dist/experience.js.map +1 -0
  219. package/dist/index.d.ts +20 -3
  220. package/dist/index.d.ts.map +1 -1
  221. package/dist/index.js +1 -0
  222. package/dist/index.js.map +1 -1
  223. package/dist/interpreter.d.ts +5 -10
  224. package/dist/interpreter.d.ts.map +1 -1
  225. package/dist/interpreter.js +143 -70
  226. package/dist/interpreter.js.map +1 -1
  227. package/dist/inventory.d.ts +2 -0
  228. package/dist/inventory.d.ts.map +1 -1
  229. package/dist/inventory.js +2 -0
  230. package/dist/inventory.js.map +1 -1
  231. package/dist/linkset.d.ts +147 -11
  232. package/dist/linkset.d.ts.map +1 -1
  233. package/dist/linkset.js +401 -28
  234. package/dist/linkset.js.map +1 -1
  235. package/dist/memory.d.ts +5 -2
  236. package/dist/memory.d.ts.map +1 -1
  237. package/dist/memory.js +5 -2
  238. package/dist/memory.js.map +1 -1
  239. package/dist/prim-params.d.ts +27 -10
  240. package/dist/prim-params.d.ts.map +1 -1
  241. package/dist/prim-params.js +209 -125
  242. package/dist/prim-params.js.map +1 -1
  243. package/dist/prim.d.ts +95 -5
  244. package/dist/prim.d.ts.map +1 -1
  245. package/dist/prim.js +96 -7
  246. package/dist/prim.js.map +1 -1
  247. package/dist/runtime.d.ts +52 -3
  248. package/dist/runtime.d.ts.map +1 -1
  249. package/dist/script.d.ts +171 -12
  250. package/dist/script.d.ts.map +1 -1
  251. package/dist/script.js +480 -73
  252. package/dist/script.js.map +1 -1
  253. package/dist/typecheck.d.ts +3 -1
  254. package/dist/typecheck.d.ts.map +1 -1
  255. package/dist/typecheck.js +25 -5
  256. package/dist/typecheck.js.map +1 -1
  257. package/dist/values/coerce.d.ts +1 -1
  258. package/dist/values/coerce.d.ts.map +1 -1
  259. package/dist/values/coerce.js +10 -6
  260. package/dist/values/coerce.js.map +1 -1
  261. package/dist/values/format.d.ts +3 -1
  262. package/dist/values/format.d.ts.map +1 -1
  263. package/dist/values/format.js +10 -6
  264. package/dist/values/format.js.map +1 -1
  265. package/dist/values/ops.d.ts +3 -0
  266. package/dist/values/ops.d.ts.map +1 -1
  267. package/dist/values/ops.js +2 -2
  268. package/dist/values/ops.js.map +1 -1
  269. package/dist/world.d.ts +247 -1
  270. package/dist/world.d.ts.map +1 -1
  271. package/dist/world.js +318 -6
  272. package/dist/world.js.map +1 -1
  273. 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';
2
- import { ResetScriptSignal } from './builtins/object.js';
3
- import { ScriptRemovedSignal } from './builtins/inventory.js';
12
+ import { agentPresence } from './builtins/viewer.js';
13
+ import { defaultCamera } from './builtins/camera.js';
14
+ import { animationAvatars, playingAnimations, setAnimationState } from './builtins/animation.js';
15
+ import { HaltSignal, ResetScriptSignal } from './builtins/object.js';
4
16
  import { Mulberry32 } from './random.js';
5
17
  import { inventoryItemName } from './inventory.js';
18
+ import { typedList, listElementTypes } from './values/list-types.js';
6
19
  import { NULL_KEY } from './values/types.js';
7
20
  import { execHandler, StateChangeSignal } from './interpreter.js';
8
21
  import { buildCoveragePlan, CoverageCollector } from './coverage.js';
9
22
  import { defaultEvalFor, rot, toFloat32Value, vec } from './values/types.js';
10
- import { typedList } from './values/list-types.js';
11
23
  import { Env } from './env.js';
12
- import { ScriptClockView } from './clock.js';
24
+ import { DELAYED_EVENTS, ScriptClockView } from './clock.js';
13
25
  import { Linkset } from './linkset.js';
14
26
  import { Prim } from './prim.js';
15
27
  import { EVENT_SPECS } from './generated/events.js';
16
28
  import { CONSTANT_TABLE } from './generated/constants_table.js';
17
- 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';
18
30
  import { own } from './own.js';
19
31
  import { deterministicKey } from './keys.js';
20
- import { Memory, StackHeapCollision, codeBytes } from './memory.js';
32
+ import { Memory, RuntimeError, codeBytes } from './memory.js';
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,11 +39,13 @@ import { Memory, StackHeapCollision, codeBytes } from './memory.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;
31
46
  userFunctions;
32
47
  functions;
48
+ invalidPrograms;
33
49
  handlersByState;
34
50
  started = false;
35
51
  stubs;
@@ -37,20 +53,28 @@ export class Script {
37
53
  /** Per-script clock facade (timer state + time reference). */
38
54
  clockView;
39
55
  host;
40
- linkset;
41
56
  scriptName;
42
57
  /** When false, drainQueue parks events instead of delivering them. */
43
58
  running = true;
44
59
  /** Events parked while `running === false`; replayed when re-enabled. */
45
60
  parkedEvents = [];
61
+ /** llGetStartParameter: the linkset's start parameter until the script resets, then 0. */
62
+ startParameter;
63
+ /** The handler chain suspended in llSleep or a builtin delay, if any. */
64
+ task = null;
65
+ /** Virtual time this script last started a handler or woke from a sleep. */
66
+ resumedAt = 0;
46
67
  constructor(ast, options = {}) {
47
68
  this.ast = ast;
69
+ this.options = options;
48
70
  // Auto-wrap: if no host prim was provided, create a default 1-prim linkset.
49
71
  let host = options.host;
50
72
  if (!host) {
51
73
  const linkset = new Linkset({
52
74
  owner: options.owner ?? NULL_KEY,
53
75
  startUnixTime: options.startUnixTime ?? 0,
76
+ startParameter: options.startParameter ?? 0,
77
+ startString: options.startString ?? '',
54
78
  });
55
79
  const prim = new Prim({
56
80
  key: options.objectKey ??
@@ -64,15 +88,14 @@ export class Script {
64
88
  if (!host.linkset) {
65
89
  throw new Error('Script host prim must belong to a Linkset');
66
90
  }
67
- this.linkset = host.linkset;
91
+ this.startParameter = this.linkset.startParameter;
68
92
  this.scriptName = inventoryItemName(options.scriptName ?? deriveScriptName(options.filename));
69
93
  this.clockView = new ScriptClockView(this.linkset.clock, () => this);
70
- const linkset = this.linkset;
71
94
  const prim = host;
72
95
  const scriptName = this.scriptName;
73
96
  const identity = {
74
97
  get owner() {
75
- return linkset.owner;
98
+ return prim.linkset.owner;
76
99
  },
77
100
  get objectKey() {
78
101
  return prim.key;
@@ -99,6 +122,13 @@ export class Script {
99
122
  clock: this.clockView,
100
123
  httpRequests: [],
101
124
  httpKeyCounter: 0,
125
+ urls: [],
126
+ urlPolicy: 'grant',
127
+ urlCounter: 0,
128
+ incomingHttp: [],
129
+ httpInCounter: 0,
130
+ emails: [],
131
+ targetedEmails: [],
102
132
  listens: [],
103
133
  listenHandleCounter: 0,
104
134
  random: new Mulberry32(options.randomSeed ?? 1),
@@ -107,11 +137,29 @@ export class Script {
107
137
  dataserverRequests: [],
108
138
  dataserverKeyCounter: 0,
109
139
  detectedStack: [],
110
- linksetData: this.linkset.linksetData,
140
+ get linksetData() {
141
+ return prim.linkset.linksetData;
142
+ },
111
143
  appearance: this.host.appearance,
144
+ gameControlSnapshot: null,
112
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: [],
113
159
  lifecycle: {
114
160
  dead: false,
161
+ dying: false,
162
+ removed: false,
115
163
  },
116
164
  filename,
117
165
  source,
@@ -127,6 +175,7 @@ export class Script {
127
175
  initGlobals(this.globals, ast.globals);
128
176
  this.userFunctions = new Map(ast.functions.map((f) => [f.name, f]));
129
177
  this.functions = options.functions ?? {};
178
+ this.invalidPrograms = invalidPrograms(ast, this.functions);
130
179
  this.stubs = options.stubs ?? 'allow';
131
180
  this.handlersByState = indexHandlers(ast);
132
181
  if (!this.handlersByState.has('default')) {
@@ -138,7 +187,21 @@ export class Script {
138
187
  host.addScript(this);
139
188
  }
140
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
+ }
141
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
+ }
142
205
  get now() {
143
206
  return this.linkset.clock.now;
144
207
  }
@@ -181,6 +244,18 @@ export class Script {
181
244
  callsOf(name) {
182
245
  return this.state.calls.filter((c) => c.name === name);
183
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
+ }
184
259
  /** Captured outgoing HTTP requests from `llHTTPRequest`. */
185
260
  get httpRequests() {
186
261
  return this.state.httpRequests;
@@ -206,9 +281,10 @@ export class Script {
206
281
  : typedList([HTTP_BODY_TRUNCATED, req.bodyMaxLength], ['integer', 'integer']),
207
282
  body: cut,
208
283
  };
284
+ const started = this.host.scripts.filter((s) => s.handles('http_response'));
209
285
  for (const s of this.host.scripts)
210
286
  this.clockView.scheduleOn(s, this.now, 'http_response', payload);
211
- this.linkset.drainQueue();
287
+ this.linkset.settle(started);
212
288
  }
213
289
  /** Convenience: respond to the most recent HTTP request. */
214
290
  respondToLastHttp(response) {
@@ -217,6 +293,46 @@ export class Script {
217
293
  throw new Error('no HTTP request to respond to');
218
294
  this.respondToHttp(req.key, response);
219
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
+ }
220
336
  /** Currently active listen registrations (from `llListen`). */
221
337
  get listens() {
222
338
  return this.state.listens;
@@ -244,9 +360,10 @@ export class Script {
244
360
  throw new Error(`unknown dataserver request key: ${key}`);
245
361
  req.fulfilled = true;
246
362
  const payload = { queryid: key, data: value };
363
+ const started = this.host.scripts.filter((s) => s.handles('dataserver'));
247
364
  for (const s of this.host.scripts)
248
365
  this.clockView.scheduleOn(s, this.now, 'dataserver', payload);
249
- this.linkset.drainQueue();
366
+ this.linkset.settle(started);
250
367
  }
251
368
  /** Convenience: respond to the most recent dataserver request. */
252
369
  respondToLastDataserver(value) {
@@ -272,17 +389,180 @@ export class Script {
272
389
  const granted = req.permissions & (permissions ?? req.permissions);
273
390
  this.state.permissions.granted = granted;
274
391
  this.state.permissions.agent = req.agent;
392
+ if (!(granted & PERMISSION_TAKE_CONTROLS))
393
+ this.state.controls.taken = null;
394
+ const started = this.handles('run_time_permissions') ? [this] : [];
275
395
  this.clockView.schedule(this.now, 'run_time_permissions', { perm: granted });
276
- this.linkset.drainQueue();
396
+ this.linkset.settle(started);
277
397
  }
278
398
  /** Answer the most recent unanswered permission request with No. */
279
399
  denyPermissions() {
280
400
  this.grantPermissions(0);
281
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
+ }
282
550
  /** Currently displayed floating text (from llSetText). null if unset. */
283
551
  get text() {
284
552
  return this.state.appearance.text;
285
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
+ }
286
566
  /** Object description from llSetObjectDesc. */
287
567
  get objectDesc() {
288
568
  return this.state.appearance.description;
@@ -303,13 +583,17 @@ export class Script {
303
583
  this.state.linksetData.set(k, { value: v.value, password: v.password ?? '' });
304
584
  }
305
585
  }
586
+ /** The script's memory limit in bytes, as llGetMemoryLimit reads it. */
587
+ get memoryLimit() {
588
+ return this.state.memory.limit;
589
+ }
306
590
  /** True once `llDie()` has been called. Subsequent fire() calls are no-ops. */
307
591
  get dead() {
308
592
  return this.state.lifecycle.dead;
309
593
  }
310
594
  /**
311
- * Match chat against this script's listens and queue `listen` events for
312
- * every match. Does not drain — caller does that. Used by Linkset.deliverChat
595
+ * Match chat against this script's listens and queue one `listen` event
596
+ * if any of them match; returns whether one did. Does not drain — caller does that. Used by Linkset.deliverChat
313
597
  * for cross-script delivery.
314
598
  */
315
599
  matchChatListens(opts) {
@@ -330,15 +614,16 @@ export class Script {
330
614
  id: opts.key,
331
615
  msg: opts.message,
332
616
  });
617
+ return true;
333
618
  }
619
+ return false;
334
620
  }
335
621
  /**
336
622
  * Deliver chat to this script only. Linkset-wide delivery (every listening
337
623
  * script in the linkset) is `linkset.deliverChat`.
338
624
  */
339
625
  deliverChat(opts) {
340
- this.matchChatListens(opts);
341
- this.linkset.drainQueue();
626
+ this.linkset.settle(this.matchChatListens(opts) && this.handles('listen') ? [this] : []);
342
627
  }
343
628
  /**
344
629
  * Override an `ll*` function for the lifetime of this Script. Replaces
@@ -375,7 +660,10 @@ export class Script {
375
660
  *
376
661
  * Synchronous: runs the handler to completion before returning, including
377
662
  * any state transitions triggered by `state foo;` (state_exit / change /
378
- * state_entry chain runs before fire() returns).
663
+ * state_entry chain runs before fire() returns). A handler that sleeps
664
+ * moves the clock to its wake time, running every other script's due
665
+ * events on the way. A script already sleeping handles the event after
666
+ * its current handler.
379
667
  *
380
668
  * If the current state has no handler for `eventName`, this is a no-op
381
669
  * — matches LSL behavior of silently dropping unhandled events.
@@ -384,23 +672,26 @@ export class Script {
384
672
  if (this.state.lifecycle.dead)
385
673
  return;
386
674
  if (!this.running) {
387
- 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 }));
388
676
  return;
389
677
  }
390
678
  if (!this.started) {
391
679
  this.started = true;
392
680
  const entry = this.handlersByState.get(this.state.currentState)?.get('state_entry');
393
681
  if (entry && eventName !== 'state_entry') {
394
- this.runHandler(entry, []);
395
- this.linkset.drainQueue();
682
+ this.begin(entry, [], {});
683
+ this.linkset.settle([this]);
396
684
  }
397
685
  }
398
- const handler = this.handlersByState.get(this.state.currentState)?.get(eventName);
399
- if (handler) {
400
- const args = bindPayload(eventName, payload);
401
- this.withDetected(payload, () => this.runHandler(handler, args));
402
- }
403
- this.linkset.drainQueue();
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
+ });
404
695
  }
405
696
  /**
406
697
  * Run state_entry of the default state. If the script has been auto-started
@@ -411,38 +702,106 @@ export class Script {
411
702
  return;
412
703
  if (this.started)
413
704
  return;
705
+ this.boot();
706
+ this.linkset.settle([this]);
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;
414
712
  this.started = true;
415
- const entry = this.handlersByState.get('default')?.get('state_entry');
416
- if (entry) {
417
- this.runHandler(entry, []);
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');
418
754
  }
419
- this.linkset.drainQueue();
755
+ this.clockView.schedule(this.now, 'on_rez', { start_param: this.startParameter });
756
+ }
757
+ boot() {
758
+ this.started = true;
759
+ const entry = this.handlersByState.get('default')?.get('state_entry');
760
+ if (entry)
761
+ this.begin(entry, [], {});
420
762
  }
421
763
  /**
422
764
  * Reset the script as if `llResetScript` had been called: clear globals,
423
765
  * reseed them from the AST initializers, return to the default state,
424
766
  * cancel timers, and run state_entry. Used internally when llResetScript
425
767
  * is invoked from inside a handler; tests can also call it directly.
768
+ * A sleeping handler is abandoned.
426
769
  */
427
770
  reset() {
771
+ this.restart();
772
+ this.linkset.settle([this]);
773
+ }
774
+ restart() {
775
+ this.abandon();
428
776
  this.state.memory.releaseStack();
429
777
  this.globals.clear();
430
778
  try {
431
779
  initGlobals(this.globals, this.ast.globals);
432
780
  }
433
781
  catch (e) {
434
- if (e instanceof StackHeapCollision)
782
+ if (e instanceof RuntimeError)
435
783
  this.crash(e);
436
784
  throw e;
437
785
  }
438
786
  this.state.currentState = 'default';
439
787
  this.state.lifecycle.dead = false;
788
+ this.state.lifecycle.dying = false;
789
+ this.state.lifecycle.removed = false;
440
790
  this.state.listens.length = 0;
791
+ this.state.urls.length = 0;
441
792
  this.state.permissions.granted = 0;
442
793
  this.state.permissions.agent = NULL_KEY;
443
794
  for (const r of this.state.permissions.requests)
444
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);
445
802
  this.clockView.cancelTimer();
803
+ this.clockView.setMinEventDelay(0);
804
+ this.startParameter = 0;
446
805
  this.clockView.resetReference();
447
806
  this.linkset.clock.purgeTarget(this);
448
807
  this.parkedEvents.length = 0;
@@ -454,10 +813,9 @@ export class Script {
454
813
  if (this.state.coverage)
455
814
  this.state.coverage.hitState('default');
456
815
  // Per LSL: if the script is stopped, reset just clears state — no
457
- // state_entry runs until it's restarted. If it's running, fire
458
- // state_entry now via start().
816
+ // state_entry runs until it's restarted.
459
817
  if (this.running)
460
- this.start();
818
+ this.boot();
461
819
  }
462
820
  /**
463
821
  * Internal: invoked by `Linkset.drainQueue` to deliver one queued event.
@@ -468,33 +826,64 @@ export class Script {
468
826
  if (this.state.lifecycle.dead)
469
827
  return;
470
828
  if (event === '__reset') {
471
- this.reset();
829
+ this.restart();
472
830
  return;
473
831
  }
474
832
  const handler = this.handlersByState.get(this.state.currentState)?.get(event);
475
833
  if (!handler)
476
834
  return;
477
- const args = bindPayload(event, payload);
478
- this.withDetected(payload, () => this.runHandler(handler, args));
835
+ if (DELAYED_EVENTS.has(event))
836
+ this.clockView.delayedUntilMs = this.now + this.clockView.minEventDelayMs;
837
+ this.begin(handler, bindPayload(event, payload), payload);
479
838
  }
480
- /**
481
- * Push a detected context (if the payload includes `detected`) for the
482
- * duration of `fn`, so llDetectedKey / Name / Pos / etc. inside the
483
- * handler resolve to the right entries.
484
- */
485
- withDetected(payload, fn) {
486
- const detected = payload['detected'];
487
- if (!Array.isArray(detected) || detected.length === 0) {
488
- fn();
839
+ /** Whether the current state has a handler for `event`. */
840
+ handles(event) {
841
+ return this.handlersByState.get(this.state.currentState).has(event);
842
+ }
843
+ /** Virtual time a sleeping handler wakes at, or undefined when the script is idle. */
844
+ get wakeAt() {
845
+ return this.task?.wakeAt;
846
+ }
847
+ /** Internal: invoked by `Linkset.drainQueue` when a sleeping handler's wake time comes. */
848
+ wake() {
849
+ const task = this.task;
850
+ if (!task)
489
851
  return;
490
- }
491
- this.state.detectedStack.push({ entries: detected });
852
+ this.task = null;
853
+ this.step(task);
854
+ }
855
+ begin(handler, args, payload) {
856
+ this.step({ run: this.runHandler(handler, args, payload), startedAt: this.now, wakeAt: 0 });
857
+ }
858
+ step(task) {
859
+ this.resumedAt = this.now;
860
+ let r;
492
861
  try {
493
- fn();
862
+ r = task.run.next();
494
863
  }
495
- finally {
496
- this.state.detectedStack.pop();
864
+ catch (e) {
865
+ this.finish(task);
866
+ throw e;
867
+ }
868
+ if (!r.done) {
869
+ this.task = { ...task, wakeAt: r.value.until };
870
+ return;
497
871
  }
872
+ this.finish(task);
873
+ this.clockView.finishHandler(r.value);
874
+ if (r.value)
875
+ this.restart();
876
+ }
877
+ finish(task) {
878
+ if (this.now === task.startedAt)
879
+ return;
880
+ this.clockView.busyUntilMs = this.now;
881
+ this.linkset.clock.requeue(this);
882
+ }
883
+ abandon() {
884
+ const task = this.task;
885
+ this.task = null;
886
+ task?.run.return(false);
498
887
  }
499
888
  /**
500
889
  * Run a handler and process any state-change signal it raises.
@@ -505,34 +894,43 @@ export class Script {
505
894
  * with a small loop so that a `state_exit` or `state_entry` that itself
506
895
  * does `state foo;` continues the chain correctly.
507
896
  */
508
- runHandler(handler, args) {
509
- if (this.state.lifecycle.dead)
510
- return;
511
- const startedAt = this.now;
897
+ *runHandler(handler, args, payload) {
898
+ this.state.gameControlSnapshot = handler.name === 'game_control'
899
+ ? payload['gameControlSnapshot'] ?? null : null;
900
+ enterDamageEvent(payload);
901
+ const detected = payload['detected'];
902
+ const pushed = Array.isArray(detected) && detected.length > 0;
903
+ if (pushed)
904
+ this.state.detectedStack.push({
905
+ entries: detected, event: handler.name,
906
+ });
512
907
  try {
513
908
  this.state.memory.startEvent();
514
- this.runHandlerChain(handler, args);
909
+ return yield* this.runHandlerChain(handler, args);
515
910
  }
516
911
  catch (e) {
517
- if (e instanceof StackHeapCollision)
912
+ if (e instanceof RuntimeError)
518
913
  this.crash(e);
519
914
  throw e;
520
915
  }
521
916
  finally {
522
- if (this.now > startedAt)
523
- this.clockView.busyUntilMs = this.now;
917
+ if (pushed)
918
+ this.state.detectedStack.pop();
919
+ this.state.gameControlSnapshot = null;
920
+ leaveDamageEvent(payload);
524
921
  }
525
922
  }
526
- runHandlerChain(handler, args) {
923
+ /** Returns true when the chain ended in llResetScript. */
924
+ *runHandlerChain(handler, args) {
527
925
  const ctx = {
528
926
  state: this.state,
529
927
  mocks: this.mocks,
530
928
  globals: this.globals,
531
929
  userFunctions: this.userFunctions,
532
930
  functions: this.functions,
931
+ invalidPrograms: this.invalidPrograms,
533
932
  script: this,
534
933
  prim: this.host,
535
- linkset: this.linkset,
536
934
  };
537
935
  let pending = {
538
936
  handler,
@@ -542,28 +940,30 @@ export class Script {
542
940
  const { handler: h, args: a } = pending;
543
941
  pending = null;
544
942
  try {
545
- execHandler(ctx, h, a);
943
+ yield* execHandler(ctx, h, a);
546
944
  }
547
945
  catch (e) {
548
- if (e instanceof ResetScriptSignal) {
549
- this.reset();
550
- return;
551
- }
552
- if (e instanceof ScriptRemovedSignal)
553
- return;
946
+ if (e instanceof ResetScriptSignal)
947
+ return true;
948
+ if (e instanceof HaltSignal)
949
+ return false;
554
950
  if (!(e instanceof StateChangeSignal))
555
951
  throw e;
952
+ this.state.gameControlSnapshot = null;
953
+ this.clearDamageAdjustment();
954
+ if (this.state.lifecycle.removed)
955
+ return false;
556
956
  const target = e.target;
557
957
  if (target === this.state.currentState || this.state.lifecycle.dead)
558
958
  continue;
559
959
  const exit = this.handlersByState.get(this.state.currentState)?.get('state_exit');
560
960
  if (exit) {
561
961
  try {
562
- execHandler(ctx, exit, []);
962
+ yield* execHandler(ctx, exit, []);
563
963
  }
564
964
  catch (e2) {
565
- if (e2 instanceof ScriptRemovedSignal)
566
- return;
965
+ if (e2 instanceof HaltSignal)
966
+ return false;
567
967
  if (e2 instanceof StateChangeSignal) {
568
968
  // Discard further changes inside state_exit per LSL convention.
569
969
  }
@@ -575,6 +975,7 @@ export class Script {
575
975
  if (!this.handlersByState.has(target)) {
576
976
  throw new Error(`unknown state '${target}' in state change`);
577
977
  }
978
+ stopSensors(this);
578
979
  this.state.listens.length = 0;
579
980
  this.linkset.clock.purgeTarget(this, this.now, '__reset');
580
981
  this.state.currentState = target;
@@ -586,6 +987,7 @@ export class Script {
586
987
  }
587
988
  }
588
989
  }
990
+ return false;
589
991
  }
590
992
  /** Stop the script after a fatal run-time error, reporting it on DEBUG_CHANNEL. */
591
993
  crash(error) {
@@ -604,7 +1006,12 @@ export class Script {
604
1006
  */
605
1007
  markDead() {
606
1008
  this.state.lifecycle.dead = true;
1009
+ this.state.urls.length = 0;
1010
+ stopSensors(this);
1011
+ clearTargets(this);
1012
+ clearTargetMotion(this);
607
1013
  this.clockView.cancelTimer();
1014
+ this.abandon();
608
1015
  }
609
1016
  /**
610
1017
  * Replay any events parked while this script was disabled. Caller is