@patterkit/runtime 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -270,6 +270,10 @@ interface FlowHost {
270
270
  sceneId: string;
271
271
  }>;
272
272
  blockById: Map<string, CompiledBlock>;
273
+ /** Host-facing addresses (spec §6), shared with the engine: scene gameId -> internal id (project-wide),
274
+ * and per-scene block gameId -> internal id. A flow needs them to resolve `goto` by address. */
275
+ sceneGameIdToId: Map<string, string>;
276
+ blockGameIdToId: Map<string, Map<string, string>>;
273
277
  /** Author tags (#215): node id -> accumulated tags (own + every ancestor's, deduped). Built once. */
274
278
  tagIndex: Map<string, string[]>;
275
279
  /** The SHARED `@patter` globals (owned scope "patter") + world properties (`@world`). */
@@ -382,9 +386,41 @@ declare class Engine {
382
386
  /**
383
387
  * Open (and start) a named flow. Each flow has its own cursor, PRNG, and per-flow
384
388
  * half of the scopes (not-shared `@patter`/`@scene`); all flows share the shared
385
- * half. Re-opening an existing id replaces it with a fresh flow.
389
+ * half.
390
+ *
391
+ * Re-opening an existing id REPLACES it with a fresh flow, and CLOSES the old one
392
+ * ({@link Flow.close}) so a host still holding it cannot keep driving the shared world. Replacing is
393
+ * therefore a reset: that name's cursor, visit counts, selector cursors (so any shuffle / once-each
394
+ * position) and per-flow properties all start over.
395
+ *
396
+ * Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
397
+ * call to reach for when you want a speaker's variation state to carry on.
386
398
  */
387
399
  openFlow(id: string, opts?: OpenFlowOptions): Flow;
400
+ /**
401
+ * "Play this address and give me everything it produced" - the one-call form of the bark / one-shot
402
+ * pattern. The NAMED flow is reused if it already exists (moved with {@link Flow.goto}) and opened at
403
+ * the address if not, then run to its next stop, returning every beat it played.
404
+ *
405
+ * Calling it again with the SAME NAME does NOT replace the flow - it reuses it, and that is the whole
406
+ * point. A flow owns its selector cursors, visit counts and per-flow properties, so reusing one lets a
407
+ * **shuffle keep its bag** and an **"once each" list keep its place**: successive calls give the next
408
+ * variation instead of replaying the first forever. (A fresh flow each time would reset all of it,
409
+ * unless every such group happened to be authored `shared`.) Use one name per independent speaker;
410
+ * different names never share per-flow state.
411
+ *
412
+ * This is exactly where it differs from {@link openFlow}, which REPLACES a flow of the same name and
413
+ * so resets that variation state. Never mix the two on one name unless you mean to start over.
414
+ *
415
+ * Returns the played beats in order - `[]` means the address had nothing left to give (an exhausted
416
+ * variation list, say), which is the signal to fall back to other content. It THROWS on an address
417
+ * that does not resolve: unlike `goto` (a navigation primitive, where probing is legitimate), naming a
418
+ * location here asserts it exists, and keeping `[]` unambiguous is worth more than a soft failure.
419
+ *
420
+ * A run that stops at a CHOICE returns the beats up to it and leaves the choice pending on the flow -
421
+ * fetch it with `engine.getFlow(name)?.getChoices()`.
422
+ */
423
+ runFlow(flow: string, scene: string, block?: string): AdvanceToStopResult["played"];
388
424
  /** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
389
425
  private resolveSceneRef;
390
426
  /** Resolve a block reference (a scene-scoped gameId OR an internal id) to its internal id. */
@@ -425,7 +461,8 @@ declare class Engine {
425
461
  getFlow(id: string): Flow | undefined;
426
462
  /** All currently-open flows. */
427
463
  flows(): Flow[];
428
- /** Close (remove) a flow. */
464
+ /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
465
+ * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
429
466
  closeFlow(id: string): void;
430
467
  /**
431
468
  * Reset the whole game to its initial state: drop every flow, re-seed the shared
@@ -458,6 +495,9 @@ declare class Flow {
458
495
  private rngState;
459
496
  private started;
460
497
  private flowEnded;
498
+ /** Closed by the engine (see `close()`). Terminal, and distinct from `flowEnded`: an ENDED flow is
499
+ * merely out of content and `goto` revives it; a CLOSED one is finished for good. */
500
+ private closed;
461
501
  private currentSceneId;
462
502
  private stack;
463
503
  private activeSnippet;
@@ -485,6 +525,37 @@ declare class Flow {
485
525
  * untouched. A clearer-named alias of `start()`.
486
526
  */
487
527
  reset(sceneId?: string, blockId?: string): void;
528
+ /**
529
+ * Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
530
+ * `onEntry` effects run, entering counts as a visit, and the callstack is REPLACED - any pending
531
+ * `call` returns are discarded, just as a goto inside a call does.
532
+ *
533
+ * `scene` and `block` are host-facing gameIds (spec §6) or internal ids; `block` is scene-scoped, so
534
+ * it is looked up within `scene`. `"END"` ends the flow. To move within the current scene, pass the
535
+ * current scene's address again (`flow.currentScene` -> `engine.sceneAddress`).
536
+ *
537
+ * This is HOST navigation, not authoring, and it takes effect IMMEDIATELY: any beats left in the
538
+ * snippet being delivered are abandoned, and a pending choice is dropped. The format stops an AUTHOR
539
+ * writing a divert into the middle of a snippet; a host teleport is out-of-band, like `reset()` or
540
+ * `loadGame()`. A flow that never started starts here; one that already ended resumes here.
541
+ *
542
+ * Returns false - leaving the cursor exactly where it was - if the address does not resolve. Per-flow
543
+ * state (properties, visit counts, selector cursors) is untouched either way: this MOVES, never resets.
544
+ */
545
+ goto(scene: string, block?: string): boolean;
546
+ /**
547
+ * Finish this flow for good. Engine-managed: `engine.closeFlow(id)`, `engine.reset()`, and re-opening
548
+ * a name with `engine.openFlow` all call it on the flow being dropped.
549
+ *
550
+ * A dropped flow used to stay fully live: unregistered and invisible to `engine.flows()`, but a host
551
+ * still holding the object could keep advancing it, and every scene `onEntry`, shared property, world
552
+ * visit count and shared selector cursor it touched still landed on the engine. Closing makes that
553
+ * stale reference inert - `advance()` reports the end and `goto()` refuses - so a forgotten reference
554
+ * cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
555
+ */
556
+ close(): void;
557
+ /** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
558
+ get isClosed(): boolean;
488
559
  /** The scene the cursor is currently in - set on entry and whenever a jump crosses scenes. Read
489
560
  * right after `advance()` to know which scene the just-played beat lives in (tooling that mirrors
490
561
  * the playhead, e.g. an editor following a cross-scene jump). `null` before the flow has started. */
package/dist/index.d.ts CHANGED
@@ -270,6 +270,10 @@ interface FlowHost {
270
270
  sceneId: string;
271
271
  }>;
272
272
  blockById: Map<string, CompiledBlock>;
273
+ /** Host-facing addresses (spec §6), shared with the engine: scene gameId -> internal id (project-wide),
274
+ * and per-scene block gameId -> internal id. A flow needs them to resolve `goto` by address. */
275
+ sceneGameIdToId: Map<string, string>;
276
+ blockGameIdToId: Map<string, Map<string, string>>;
273
277
  /** Author tags (#215): node id -> accumulated tags (own + every ancestor's, deduped). Built once. */
274
278
  tagIndex: Map<string, string[]>;
275
279
  /** The SHARED `@patter` globals (owned scope "patter") + world properties (`@world`). */
@@ -382,9 +386,41 @@ declare class Engine {
382
386
  /**
383
387
  * Open (and start) a named flow. Each flow has its own cursor, PRNG, and per-flow
384
388
  * half of the scopes (not-shared `@patter`/`@scene`); all flows share the shared
385
- * half. Re-opening an existing id replaces it with a fresh flow.
389
+ * half.
390
+ *
391
+ * Re-opening an existing id REPLACES it with a fresh flow, and CLOSES the old one
392
+ * ({@link Flow.close}) so a host still holding it cannot keep driving the shared world. Replacing is
393
+ * therefore a reset: that name's cursor, visit counts, selector cursors (so any shuffle / once-each
394
+ * position) and per-flow properties all start over.
395
+ *
396
+ * Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
397
+ * call to reach for when you want a speaker's variation state to carry on.
386
398
  */
387
399
  openFlow(id: string, opts?: OpenFlowOptions): Flow;
400
+ /**
401
+ * "Play this address and give me everything it produced" - the one-call form of the bark / one-shot
402
+ * pattern. The NAMED flow is reused if it already exists (moved with {@link Flow.goto}) and opened at
403
+ * the address if not, then run to its next stop, returning every beat it played.
404
+ *
405
+ * Calling it again with the SAME NAME does NOT replace the flow - it reuses it, and that is the whole
406
+ * point. A flow owns its selector cursors, visit counts and per-flow properties, so reusing one lets a
407
+ * **shuffle keep its bag** and an **"once each" list keep its place**: successive calls give the next
408
+ * variation instead of replaying the first forever. (A fresh flow each time would reset all of it,
409
+ * unless every such group happened to be authored `shared`.) Use one name per independent speaker;
410
+ * different names never share per-flow state.
411
+ *
412
+ * This is exactly where it differs from {@link openFlow}, which REPLACES a flow of the same name and
413
+ * so resets that variation state. Never mix the two on one name unless you mean to start over.
414
+ *
415
+ * Returns the played beats in order - `[]` means the address had nothing left to give (an exhausted
416
+ * variation list, say), which is the signal to fall back to other content. It THROWS on an address
417
+ * that does not resolve: unlike `goto` (a navigation primitive, where probing is legitimate), naming a
418
+ * location here asserts it exists, and keeping `[]` unambiguous is worth more than a soft failure.
419
+ *
420
+ * A run that stops at a CHOICE returns the beats up to it and leaves the choice pending on the flow -
421
+ * fetch it with `engine.getFlow(name)?.getChoices()`.
422
+ */
423
+ runFlow(flow: string, scene: string, block?: string): AdvanceToStopResult["played"];
388
424
  /** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
389
425
  private resolveSceneRef;
390
426
  /** Resolve a block reference (a scene-scoped gameId OR an internal id) to its internal id. */
@@ -425,7 +461,8 @@ declare class Engine {
425
461
  getFlow(id: string): Flow | undefined;
426
462
  /** All currently-open flows. */
427
463
  flows(): Flow[];
428
- /** Close (remove) a flow. */
464
+ /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
465
+ * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
429
466
  closeFlow(id: string): void;
430
467
  /**
431
468
  * Reset the whole game to its initial state: drop every flow, re-seed the shared
@@ -458,6 +495,9 @@ declare class Flow {
458
495
  private rngState;
459
496
  private started;
460
497
  private flowEnded;
498
+ /** Closed by the engine (see `close()`). Terminal, and distinct from `flowEnded`: an ENDED flow is
499
+ * merely out of content and `goto` revives it; a CLOSED one is finished for good. */
500
+ private closed;
461
501
  private currentSceneId;
462
502
  private stack;
463
503
  private activeSnippet;
@@ -485,6 +525,37 @@ declare class Flow {
485
525
  * untouched. A clearer-named alias of `start()`.
486
526
  */
487
527
  reset(sceneId?: string, blockId?: string): void;
528
+ /**
529
+ * Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
530
+ * `onEntry` effects run, entering counts as a visit, and the callstack is REPLACED - any pending
531
+ * `call` returns are discarded, just as a goto inside a call does.
532
+ *
533
+ * `scene` and `block` are host-facing gameIds (spec §6) or internal ids; `block` is scene-scoped, so
534
+ * it is looked up within `scene`. `"END"` ends the flow. To move within the current scene, pass the
535
+ * current scene's address again (`flow.currentScene` -> `engine.sceneAddress`).
536
+ *
537
+ * This is HOST navigation, not authoring, and it takes effect IMMEDIATELY: any beats left in the
538
+ * snippet being delivered are abandoned, and a pending choice is dropped. The format stops an AUTHOR
539
+ * writing a divert into the middle of a snippet; a host teleport is out-of-band, like `reset()` or
540
+ * `loadGame()`. A flow that never started starts here; one that already ended resumes here.
541
+ *
542
+ * Returns false - leaving the cursor exactly where it was - if the address does not resolve. Per-flow
543
+ * state (properties, visit counts, selector cursors) is untouched either way: this MOVES, never resets.
544
+ */
545
+ goto(scene: string, block?: string): boolean;
546
+ /**
547
+ * Finish this flow for good. Engine-managed: `engine.closeFlow(id)`, `engine.reset()`, and re-opening
548
+ * a name with `engine.openFlow` all call it on the flow being dropped.
549
+ *
550
+ * A dropped flow used to stay fully live: unregistered and invisible to `engine.flows()`, but a host
551
+ * still holding the object could keep advancing it, and every scene `onEntry`, shared property, world
552
+ * visit count and shared selector cursor it touched still landed on the engine. Closing makes that
553
+ * stale reference inert - `advance()` reports the end and `goto()` refuses - so a forgotten reference
554
+ * cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
555
+ */
556
+ close(): void;
557
+ /** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
558
+ get isClosed(): boolean;
488
559
  /** The scene the cursor is currently in - set on entry and whenever a jump crosses scenes. Read
489
560
  * right after `advance()` to know which scene the just-played beat lives in (tooling that mirrors
490
561
  * the playhead, e.g. an editor following a cross-scene jump). `null` before the flow has started. */
package/dist/index.js CHANGED
@@ -122,6 +122,9 @@ var Engine = class _Engine {
122
122
  nodeIndex,
123
123
  blockIndex,
124
124
  blockById,
125
+ sceneGameIdToId: this.sceneGameIdToId,
126
+ blockGameIdToId: this.blockGameIdToId,
127
+ // same instances the engine resolves with
125
128
  tagIndex: buildTagIndex(bundle),
126
129
  shared,
127
130
  patterSharedDecls,
@@ -230,16 +233,56 @@ var Engine = class _Engine {
230
233
  /**
231
234
  * Open (and start) a named flow. Each flow has its own cursor, PRNG, and per-flow
232
235
  * half of the scopes (not-shared `@patter`/`@scene`); all flows share the shared
233
- * half. Re-opening an existing id replaces it with a fresh flow.
236
+ * half.
237
+ *
238
+ * Re-opening an existing id REPLACES it with a fresh flow, and CLOSES the old one
239
+ * ({@link Flow.close}) so a host still holding it cannot keep driving the shared world. Replacing is
240
+ * therefore a reset: that name's cursor, visit counts, selector cursors (so any shuffle / once-each
241
+ * position) and per-flow properties all start over.
242
+ *
243
+ * Contrast {@link runFlow}, which REUSES a flow of the same name instead of replacing it - that is the
244
+ * call to reach for when you want a speaker's variation state to carry on.
234
245
  */
235
246
  openFlow(id, opts = {}) {
236
247
  const sceneId = this.resolveSceneRef(opts.scene);
237
248
  const blockId = this.resolveBlockRef(sceneId, opts.block);
249
+ this.flowsById.get(id)?.close();
238
250
  const flow = new Flow(id, this.host, opts.seed ?? this.defaultSeed);
239
251
  this.flowsById.set(id, flow);
240
252
  flow.start(sceneId, blockId);
241
253
  return flow;
242
254
  }
255
+ /**
256
+ * "Play this address and give me everything it produced" - the one-call form of the bark / one-shot
257
+ * pattern. The NAMED flow is reused if it already exists (moved with {@link Flow.goto}) and opened at
258
+ * the address if not, then run to its next stop, returning every beat it played.
259
+ *
260
+ * Calling it again with the SAME NAME does NOT replace the flow - it reuses it, and that is the whole
261
+ * point. A flow owns its selector cursors, visit counts and per-flow properties, so reusing one lets a
262
+ * **shuffle keep its bag** and an **"once each" list keep its place**: successive calls give the next
263
+ * variation instead of replaying the first forever. (A fresh flow each time would reset all of it,
264
+ * unless every such group happened to be authored `shared`.) Use one name per independent speaker;
265
+ * different names never share per-flow state.
266
+ *
267
+ * This is exactly where it differs from {@link openFlow}, which REPLACES a flow of the same name and
268
+ * so resets that variation state. Never mix the two on one name unless you mean to start over.
269
+ *
270
+ * Returns the played beats in order - `[]` means the address had nothing left to give (an exhausted
271
+ * variation list, say), which is the signal to fall back to other content. It THROWS on an address
272
+ * that does not resolve: unlike `goto` (a navigation primitive, where probing is legitimate), naming a
273
+ * location here asserts it exists, and keeping `[]` unambiguous is worth more than a soft failure.
274
+ *
275
+ * A run that stops at a CHOICE returns the beats up to it and leaves the choice pending on the flow -
276
+ * fetch it with `engine.getFlow(name)?.getChoices()`.
277
+ */
278
+ runFlow(flow, scene, block) {
279
+ const existing = this.flowsById.get(flow);
280
+ if (!existing) return this.openFlow(flow, { scene, block }).advanceToStop().played;
281
+ if (!existing.goto(scene, block)) {
282
+ throw new Error(`runFlow: address not found: ${scene}${block === void 0 ? "" : ` / ${block}`}`);
283
+ }
284
+ return existing.advanceToStop().played;
285
+ }
243
286
  /** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
244
287
  resolveSceneRef(ref) {
245
288
  if (ref == null) return void 0;
@@ -377,8 +420,10 @@ var Engine = class _Engine {
377
420
  flows() {
378
421
  return [...this.flowsById.values()];
379
422
  }
380
- /** Close (remove) a flow. */
423
+ /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
424
+ * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
381
425
  closeFlow(id) {
426
+ this.flowsById.get(id)?.close();
382
427
  this.flowsById.delete(id);
383
428
  }
384
429
  /**
@@ -388,6 +433,7 @@ var Engine = class _Engine {
388
433
  * After reset, open fresh flows with `openFlow`.
389
434
  */
390
435
  reset() {
436
+ for (const flow of this.flowsById.values()) flow.close();
391
437
  this.flowsById.clear();
392
438
  this.host.shared.reseedOwned("patter", this.host.patterSharedDecls);
393
439
  this.host.sharedVisits.clear();
@@ -479,6 +525,9 @@ var Flow = class {
479
525
  // `activeSnippet`/`beatIndex`.
480
526
  started = false;
481
527
  flowEnded = false;
528
+ /** Closed by the engine (see `close()`). Terminal, and distinct from `flowEnded`: an ENDED flow is
529
+ * merely out of content and `goto` revives it; a CLOSED one is finished for good. */
530
+ closed = false;
482
531
  currentSceneId = null;
483
532
  stack = [];
484
533
  activeSnippet = null;
@@ -585,6 +634,81 @@ var Flow = class {
585
634
  reset(sceneId, blockId) {
586
635
  this.start(sceneId, blockId);
587
636
  }
637
+ /**
638
+ * Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
639
+ * `onEntry` effects run, entering counts as a visit, and the callstack is REPLACED - any pending
640
+ * `call` returns are discarded, just as a goto inside a call does.
641
+ *
642
+ * `scene` and `block` are host-facing gameIds (spec §6) or internal ids; `block` is scene-scoped, so
643
+ * it is looked up within `scene`. `"END"` ends the flow. To move within the current scene, pass the
644
+ * current scene's address again (`flow.currentScene` -> `engine.sceneAddress`).
645
+ *
646
+ * This is HOST navigation, not authoring, and it takes effect IMMEDIATELY: any beats left in the
647
+ * snippet being delivered are abandoned, and a pending choice is dropped. The format stops an AUTHOR
648
+ * writing a divert into the middle of a snippet; a host teleport is out-of-band, like `reset()` or
649
+ * `loadGame()`. A flow that never started starts here; one that already ended resumes here.
650
+ *
651
+ * Returns false - leaving the cursor exactly where it was - if the address does not resolve. Per-flow
652
+ * state (properties, visit counts, selector cursors) is untouched either way: this MOVES, never resets.
653
+ */
654
+ goto(scene, block) {
655
+ if (this.closed) return false;
656
+ if (scene === "END") {
657
+ this.started = true;
658
+ this.pendingChoice = null;
659
+ this.pendingPromptBeat = null;
660
+ this.pendingPromptOwnerId = null;
661
+ this.activeSnippet = null;
662
+ this.beatIndex = 0;
663
+ this.flowEnded = true;
664
+ this.stack = [];
665
+ return true;
666
+ }
667
+ const sceneId = this.host.sceneGameIdToId.get(scene) ?? (this.host.bundle.scenes[scene] ? scene : void 0);
668
+ if (sceneId === void 0) return false;
669
+ let blockId;
670
+ if (block !== void 0) {
671
+ blockId = this.host.blockGameIdToId.get(sceneId)?.get(block) ?? (this.host.blockIndex.get(block)?.sceneId === sceneId ? block : void 0);
672
+ if (blockId === void 0) return false;
673
+ }
674
+ if (!this.started) {
675
+ this.start(sceneId, blockId);
676
+ return true;
677
+ }
678
+ this.pendingChoice = null;
679
+ this.pendingPromptBeat = null;
680
+ this.pendingPromptOwnerId = null;
681
+ this.activeSnippet = null;
682
+ this.beatIndex = 0;
683
+ this.flowEnded = false;
684
+ this.enterTarget(blockId ?? sceneId, "jump");
685
+ this.settle();
686
+ return true;
687
+ }
688
+ /**
689
+ * Finish this flow for good. Engine-managed: `engine.closeFlow(id)`, `engine.reset()`, and re-opening
690
+ * a name with `engine.openFlow` all call it on the flow being dropped.
691
+ *
692
+ * A dropped flow used to stay fully live: unregistered and invisible to `engine.flows()`, but a host
693
+ * still holding the object could keep advancing it, and every scene `onEntry`, shared property, world
694
+ * visit count and shared selector cursor it touched still landed on the engine. Closing makes that
695
+ * stale reference inert - `advance()` reports the end and `goto()` refuses - so a forgotten reference
696
+ * cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
697
+ */
698
+ close() {
699
+ this.closed = true;
700
+ this.flowEnded = true;
701
+ this.stack = [];
702
+ this.activeSnippet = null;
703
+ this.beatIndex = 0;
704
+ this.pendingChoice = null;
705
+ this.pendingPromptBeat = null;
706
+ this.pendingPromptOwnerId = null;
707
+ }
708
+ /** True once the engine has closed this flow (closed, dropped by `reset()`, or replaced by name). */
709
+ get isClosed() {
710
+ return this.closed;
711
+ }
588
712
  /** The scene the cursor is currently in - set on entry and whenever a jump crosses scenes. Read
589
713
  * right after `advance()` to know which scene the just-played beat lives in (tooling that mirrors
590
714
  * the playhead, e.g. an editor following a cross-scene jump). `null` before the flow has started. */
@@ -593,6 +717,7 @@ var Flow = class {
593
717
  }
594
718
  /** Run until the next line, game event, choice, or the end of the flow. */
595
719
  advance() {
720
+ if (this.closed) return { type: "end" };
596
721
  if (!this.started) throw new Error("flow has not been started");
597
722
  if (this.pendingPromptBeat) {
598
723
  const b = this.pendingPromptBeat;