@yagejs/core 0.1.0 → 0.2.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.cjs +624 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +361 -23
- package/dist/index.d.ts +361 -23
- package/dist/index.js +620 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -367,13 +367,16 @@ var Logger = class {
|
|
|
367
367
|
|
|
368
368
|
// src/EngineContext.ts
|
|
369
369
|
var ServiceKey = class {
|
|
370
|
-
constructor(id) {
|
|
370
|
+
constructor(id, options) {
|
|
371
371
|
this.id = id;
|
|
372
|
+
this.scope = options?.scope ?? "engine";
|
|
372
373
|
}
|
|
373
374
|
id;
|
|
374
375
|
static {
|
|
375
376
|
__name(this, "ServiceKey");
|
|
376
377
|
}
|
|
378
|
+
/** Declared scope (engine or scene). Defaults to `"engine"`. */
|
|
379
|
+
scope;
|
|
377
380
|
};
|
|
378
381
|
var EngineContext = class {
|
|
379
382
|
static {
|
|
@@ -419,6 +422,50 @@ var SystemSchedulerKey = new ServiceKey("systemScheduler");
|
|
|
419
422
|
var ProcessSystemKey = new ServiceKey("processSystem");
|
|
420
423
|
var AssetManagerKey = new ServiceKey("assetManager");
|
|
421
424
|
|
|
425
|
+
// src/SceneHooks.ts
|
|
426
|
+
var SceneHookRegistry = class {
|
|
427
|
+
static {
|
|
428
|
+
__name(this, "SceneHookRegistry");
|
|
429
|
+
}
|
|
430
|
+
hooks = [];
|
|
431
|
+
register(hooks) {
|
|
432
|
+
this.hooks.push(hooks);
|
|
433
|
+
return () => {
|
|
434
|
+
const idx = this.hooks.indexOf(hooks);
|
|
435
|
+
if (idx !== -1) this.hooks.splice(idx, 1);
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** Run all `beforeEnter` hooks serially. */
|
|
439
|
+
async runBeforeEnter(scene) {
|
|
440
|
+
for (const h of this.hooks) {
|
|
441
|
+
await h.beforeEnter?.(scene);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
runAfterExit(scene) {
|
|
445
|
+
for (const h of this.hooks) {
|
|
446
|
+
try {
|
|
447
|
+
h.afterExit?.(scene);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
const logger = scene.context.tryResolve(LoggerKey);
|
|
450
|
+
if (logger) {
|
|
451
|
+
logger.error("core", "Scene afterExit hook threw", {
|
|
452
|
+
scene: scene.name,
|
|
453
|
+
error: err
|
|
454
|
+
});
|
|
455
|
+
} else {
|
|
456
|
+
console.error(
|
|
457
|
+
`[yage] Scene afterExit hook threw for scene "${scene.name}":`,
|
|
458
|
+
err
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
var SceneHookRegistryKey = new ServiceKey(
|
|
466
|
+
"sceneHookRegistry"
|
|
467
|
+
);
|
|
468
|
+
|
|
422
469
|
// src/EventToken.ts
|
|
423
470
|
var EventToken = class {
|
|
424
471
|
constructor(name) {
|
|
@@ -640,10 +687,11 @@ var Component = class {
|
|
|
640
687
|
_cleanups;
|
|
641
688
|
/**
|
|
642
689
|
* Access the entity's scene. Throws if the entity is not in a scene.
|
|
643
|
-
* Prefer this over `this.entity.scene
|
|
690
|
+
* Prefer this over threading through `this.entity.scene` in component
|
|
691
|
+
* code.
|
|
644
692
|
*/
|
|
645
693
|
get scene() {
|
|
646
|
-
const scene = this.entity.
|
|
694
|
+
const scene = this.entity.tryScene;
|
|
647
695
|
if (!scene) {
|
|
648
696
|
throw new Error(
|
|
649
697
|
"Cannot access scene: entity is not attached to a scene."
|
|
@@ -658,20 +706,43 @@ var Component = class {
|
|
|
658
706
|
get context() {
|
|
659
707
|
return this.scene.context;
|
|
660
708
|
}
|
|
661
|
-
/**
|
|
709
|
+
/**
|
|
710
|
+
* Resolve a service by key, cached after first lookup. Scene-scoped values
|
|
711
|
+
* (registered via `scene._registerScoped`) take precedence over engine
|
|
712
|
+
* scope. A key declared with `scope: "scene"` that falls back to engine
|
|
713
|
+
* scope emits a one-shot dev warning — almost always signals a missed
|
|
714
|
+
* `beforeEnter` hook.
|
|
715
|
+
*/
|
|
662
716
|
use(key) {
|
|
663
717
|
this._serviceCache ??= /* @__PURE__ */ new Map();
|
|
664
|
-
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
718
|
+
const cached = this._serviceCache.get(key.id);
|
|
719
|
+
if (cached !== void 0) return cached;
|
|
720
|
+
const scene = this.entity.tryScene;
|
|
721
|
+
const scoped = scene?._resolveScoped(key);
|
|
722
|
+
if (scoped !== void 0) {
|
|
723
|
+
this._serviceCache.set(key.id, scoped);
|
|
724
|
+
return scoped;
|
|
725
|
+
}
|
|
726
|
+
const value = this.context.resolve(key);
|
|
727
|
+
if (key.scope === "scene") {
|
|
728
|
+
this._warnScopedFallback(key);
|
|
729
|
+
return value;
|
|
730
|
+
}
|
|
731
|
+
this._serviceCache.set(key.id, value);
|
|
669
732
|
return value;
|
|
670
733
|
}
|
|
734
|
+
_warnScopedFallback(key) {
|
|
735
|
+
const logger = this.context.tryResolve(LoggerKey);
|
|
736
|
+
logger?.warn(
|
|
737
|
+
"core",
|
|
738
|
+
`Scoped key "${key.id}" fell back to engine scope \u2014 did a plugin forget to register a beforeEnter hook?`,
|
|
739
|
+
{ component: this.constructor.name }
|
|
740
|
+
);
|
|
741
|
+
}
|
|
671
742
|
/**
|
|
672
743
|
* Lazy proxy-based service resolution. Can be used at field-declaration time:
|
|
673
744
|
* ```ts
|
|
674
|
-
* readonly
|
|
745
|
+
* readonly input = this.service(InputManagerKey);
|
|
675
746
|
* ```
|
|
676
747
|
* The actual resolution is deferred until first property access.
|
|
677
748
|
*/
|
|
@@ -928,8 +999,27 @@ var Entity = class {
|
|
|
928
999
|
this.name = name ?? new.target.name ?? "Entity";
|
|
929
1000
|
this.tags = new Set(tags);
|
|
930
1001
|
}
|
|
931
|
-
/**
|
|
1002
|
+
/**
|
|
1003
|
+
* The scene this entity belongs to. Throws if the entity is not attached
|
|
1004
|
+
* to a scene — which in practice only happens before `scene.spawn` /
|
|
1005
|
+
* `addChild` wires it up, or after `destroy()` tears it down. Inside
|
|
1006
|
+
* lifecycle methods (`setup`, component `onAdd`, `update`, etc.) this is
|
|
1007
|
+
* always safe to access.
|
|
1008
|
+
*
|
|
1009
|
+
* For the rare case where you genuinely need to inspect whether an
|
|
1010
|
+
* entity has a scene (e.g. defensive code in systems iterating a query
|
|
1011
|
+
* result), use `tryScene` instead.
|
|
1012
|
+
*/
|
|
932
1013
|
get scene() {
|
|
1014
|
+
if (!this._scene) {
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
`Entity "${this.name}" is not attached to a scene. Use \`tryScene\` if you need to check.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return this._scene;
|
|
1020
|
+
}
|
|
1021
|
+
/** The scene this entity belongs to, or `null` if detached. */
|
|
1022
|
+
get tryScene() {
|
|
933
1023
|
return this._scene;
|
|
934
1024
|
}
|
|
935
1025
|
/** True if destroy() has been called. */
|
|
@@ -967,6 +1057,20 @@ var Entity = class {
|
|
|
967
1057
|
this._scene._addExistingEntity(child);
|
|
968
1058
|
}
|
|
969
1059
|
}
|
|
1060
|
+
spawnChild(name, classOrBlueprint, params) {
|
|
1061
|
+
const scene = this.scene;
|
|
1062
|
+
if (this._children?.has(name)) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`Entity "${this.name}" already has a child named "${name}".`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const child = classOrBlueprint === void 0 ? scene.spawn(name) : scene.spawn(
|
|
1068
|
+
classOrBlueprint,
|
|
1069
|
+
params
|
|
1070
|
+
);
|
|
1071
|
+
this.addChild(name, child);
|
|
1072
|
+
return child;
|
|
1073
|
+
}
|
|
970
1074
|
/** Remove a named child. Returns the detached entity. */
|
|
971
1075
|
removeChild(name) {
|
|
972
1076
|
const child = this._children?.get(name);
|
|
@@ -1459,7 +1563,7 @@ var GameLoop = class {
|
|
|
1459
1563
|
}
|
|
1460
1564
|
/** Process one frame with the given dt in milliseconds. */
|
|
1461
1565
|
tick(dtMs) {
|
|
1462
|
-
if (!this.callbacks) return;
|
|
1566
|
+
if (!this.running || !this.callbacks) return;
|
|
1463
1567
|
this._frameCount++;
|
|
1464
1568
|
this.callbacks.earlyUpdate(dtMs);
|
|
1465
1569
|
this.accumulator += dtMs;
|
|
@@ -1487,6 +1591,8 @@ var Scene = class {
|
|
|
1487
1591
|
transparentBelow = false;
|
|
1488
1592
|
/** Asset handles to load before onEnter(). Override in subclasses. */
|
|
1489
1593
|
preload;
|
|
1594
|
+
/** Default transition used when this scene is the destination of a push/pop/replace. */
|
|
1595
|
+
defaultTransition;
|
|
1490
1596
|
/** Manual pause flag. Set by game code to pause this scene regardless of stack position. */
|
|
1491
1597
|
paused = false;
|
|
1492
1598
|
/** Time scale multiplier for this scene. 1.0 = normal, 0.5 = half speed. Default: 1. */
|
|
@@ -1498,6 +1604,7 @@ var Scene = class {
|
|
|
1498
1604
|
queryCache;
|
|
1499
1605
|
bus;
|
|
1500
1606
|
_entityEventHandlers;
|
|
1607
|
+
_scopedServices;
|
|
1501
1608
|
/** Access the EngineContext. */
|
|
1502
1609
|
get context() {
|
|
1503
1610
|
return this._context;
|
|
@@ -1515,6 +1622,11 @@ var Scene = class {
|
|
|
1515
1622
|
}
|
|
1516
1623
|
return false;
|
|
1517
1624
|
}
|
|
1625
|
+
/** Whether a scene transition is currently running. */
|
|
1626
|
+
get isTransitioning() {
|
|
1627
|
+
const sm = this._context?.tryResolve(SceneManagerKey);
|
|
1628
|
+
return sm?.isTransitioning ?? false;
|
|
1629
|
+
}
|
|
1518
1630
|
/** Convenience accessor for the AssetManager. */
|
|
1519
1631
|
get assets() {
|
|
1520
1632
|
return this._context.resolve(AssetManagerKey);
|
|
@@ -1639,6 +1751,31 @@ var Scene = class {
|
|
|
1639
1751
|
}
|
|
1640
1752
|
}
|
|
1641
1753
|
// ---- Internal methods ----
|
|
1754
|
+
/**
|
|
1755
|
+
* Register a scene-scoped service. Called from a plugin's `beforeEnter`
|
|
1756
|
+
* hook to make per-scene state (render tree, physics world) resolvable via
|
|
1757
|
+
* `Component.use(key)`.
|
|
1758
|
+
* @internal
|
|
1759
|
+
*/
|
|
1760
|
+
_registerScoped(key, value) {
|
|
1761
|
+
this._scopedServices ??= /* @__PURE__ */ new Map();
|
|
1762
|
+
this._scopedServices.set(key.id, value);
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Resolve a scene-scoped service, or `undefined` if none was registered.
|
|
1766
|
+
* @internal
|
|
1767
|
+
*/
|
|
1768
|
+
_resolveScoped(key) {
|
|
1769
|
+
return this._scopedServices?.get(key.id);
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Clear all scene-scoped services. Called by the SceneManager after
|
|
1773
|
+
* `afterExit` hooks run, so plugin cleanup code still sees scoped state.
|
|
1774
|
+
* @internal
|
|
1775
|
+
*/
|
|
1776
|
+
_clearScopedServices() {
|
|
1777
|
+
this._scopedServices?.clear();
|
|
1778
|
+
}
|
|
1642
1779
|
/**
|
|
1643
1780
|
* Set the engine context. Called by SceneManager when the scene is pushed.
|
|
1644
1781
|
* @internal
|
|
@@ -1690,6 +1827,138 @@ var Scene = class {
|
|
|
1690
1827
|
}
|
|
1691
1828
|
};
|
|
1692
1829
|
|
|
1830
|
+
// src/LoadingScene.ts
|
|
1831
|
+
var LoadingScene = class extends Scene {
|
|
1832
|
+
static {
|
|
1833
|
+
__name(this, "LoadingScene");
|
|
1834
|
+
}
|
|
1835
|
+
name = "loading";
|
|
1836
|
+
/**
|
|
1837
|
+
* Minimum wall-clock ms the scene stays visible before handing off.
|
|
1838
|
+
* Prevents flicker on cached loads. Default 0.
|
|
1839
|
+
*/
|
|
1840
|
+
minDuration = 0;
|
|
1841
|
+
/** Transition used for the loading → target handoff. */
|
|
1842
|
+
transition;
|
|
1843
|
+
/**
|
|
1844
|
+
* When true (default), the handoff fires automatically after loading and
|
|
1845
|
+
* `minDuration`. Set false to gate it behind `continue()` — useful when
|
|
1846
|
+
* the loading scene also asks the player to press a key or click.
|
|
1847
|
+
*/
|
|
1848
|
+
autoContinue = true;
|
|
1849
|
+
_progress = 0;
|
|
1850
|
+
_started = false;
|
|
1851
|
+
_active = true;
|
|
1852
|
+
_continueRequested = false;
|
|
1853
|
+
_continueGate;
|
|
1854
|
+
// Bumped on every `_run` attempt. `AssetManager.loadAll` uses `Promise.all`
|
|
1855
|
+
// under the hood, so individual loaders from a failed attempt can still
|
|
1856
|
+
// resolve and fire `onProgress` after the attempt rejects. Without this
|
|
1857
|
+
// guard, a retry kicked off from `onLoadError` would see stale progress
|
|
1858
|
+
// callbacks mutate `_progress` and emit `scene:loading:progress` events
|
|
1859
|
+
// attributed to the current attempt.
|
|
1860
|
+
_attempt = 0;
|
|
1861
|
+
/** Current load progress, 0 → 1. Updated as the AssetManager reports progress. */
|
|
1862
|
+
get progress() {
|
|
1863
|
+
return this._progress;
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Kick off asset loading. While a load is in flight, subsequent calls
|
|
1867
|
+
* are no-ops. After a load failure the guard is released, so calling
|
|
1868
|
+
* `startLoading()` from `onLoadError` (or from a retry button) kicks off
|
|
1869
|
+
* a fresh load against the same target.
|
|
1870
|
+
*
|
|
1871
|
+
* Usually called once from `onEnter` after spawning the loading UI:
|
|
1872
|
+
* ```ts
|
|
1873
|
+
* override onEnter() {
|
|
1874
|
+
* this.spawn(LoadingSceneProgressBar);
|
|
1875
|
+
* this.startLoading();
|
|
1876
|
+
* }
|
|
1877
|
+
* ```
|
|
1878
|
+
*
|
|
1879
|
+
* Deferring the call lets you gate the start of the load behind a
|
|
1880
|
+
* title screen, "press any key" prompt, intro animation, etc.
|
|
1881
|
+
*/
|
|
1882
|
+
startLoading() {
|
|
1883
|
+
if (this._started) return;
|
|
1884
|
+
this._started = true;
|
|
1885
|
+
this._run().catch((err) => {
|
|
1886
|
+
if (!this._active) return;
|
|
1887
|
+
const logger = this.context.tryResolve(LoggerKey);
|
|
1888
|
+
if (logger) {
|
|
1889
|
+
logger.error("LoadingScene", "loading failed", { error: err });
|
|
1890
|
+
} else {
|
|
1891
|
+
console.error("[LoadingScene] loading failed:", err);
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Trigger the handoff to `target`. No-op if already called or if
|
|
1897
|
+
* `autoContinue` already fired it. If called before loading finishes,
|
|
1898
|
+
* the handoff runs as soon as loading + `minDuration` complete.
|
|
1899
|
+
*/
|
|
1900
|
+
continue() {
|
|
1901
|
+
if (this._continueRequested) return;
|
|
1902
|
+
this._continueRequested = true;
|
|
1903
|
+
this._continueGate?.();
|
|
1904
|
+
}
|
|
1905
|
+
onExit() {
|
|
1906
|
+
this._active = false;
|
|
1907
|
+
this._continueGate?.();
|
|
1908
|
+
}
|
|
1909
|
+
async _run() {
|
|
1910
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1911
|
+
if (!this._active) return;
|
|
1912
|
+
const attempt = ++this._attempt;
|
|
1913
|
+
const target = typeof this.target === "function" ? this.target() : this.target;
|
|
1914
|
+
const startedAt = performance.now();
|
|
1915
|
+
const bus = this.context.resolve(EventBusKey);
|
|
1916
|
+
try {
|
|
1917
|
+
await this.assets.loadAll(target.preload ?? [], (ratio) => {
|
|
1918
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
1919
|
+
this._progress = ratio;
|
|
1920
|
+
bus.emit("scene:loading:progress", { scene: this, ratio });
|
|
1921
|
+
});
|
|
1922
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
1923
|
+
const elapsed = performance.now() - startedAt;
|
|
1924
|
+
const remaining = this.minDuration - elapsed;
|
|
1925
|
+
if (remaining > 0) {
|
|
1926
|
+
await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
1927
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
1928
|
+
}
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
1931
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1932
|
+
this._started = false;
|
|
1933
|
+
this._attempt++;
|
|
1934
|
+
if (this.onLoadError) {
|
|
1935
|
+
await this.onLoadError(error);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
throw error;
|
|
1939
|
+
}
|
|
1940
|
+
bus.emit("scene:loading:done", { scene: this });
|
|
1941
|
+
if (!this.autoContinue && !this._continueRequested) {
|
|
1942
|
+
await new Promise((resolve) => {
|
|
1943
|
+
this._continueGate = resolve;
|
|
1944
|
+
});
|
|
1945
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
1946
|
+
}
|
|
1947
|
+
const scenes = this.context.resolve(SceneManagerKey);
|
|
1948
|
+
await scenes.replace(
|
|
1949
|
+
target,
|
|
1950
|
+
this.transition ? { transition: this.transition } : void 0
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
// src/SceneTransition.ts
|
|
1956
|
+
function resolveTransition(callSite, destination) {
|
|
1957
|
+
if (callSite) return callSite;
|
|
1958
|
+
return destination?.defaultTransition;
|
|
1959
|
+
}
|
|
1960
|
+
__name(resolveTransition, "resolveTransition");
|
|
1961
|
+
|
|
1693
1962
|
// src/SceneManager.ts
|
|
1694
1963
|
var SceneManager = class {
|
|
1695
1964
|
static {
|
|
@@ -1699,6 +1968,12 @@ var SceneManager = class {
|
|
|
1699
1968
|
_context;
|
|
1700
1969
|
bus;
|
|
1701
1970
|
assetManager;
|
|
1971
|
+
hookRegistry;
|
|
1972
|
+
logger;
|
|
1973
|
+
_currentRun;
|
|
1974
|
+
_pendingChain = Promise.resolve();
|
|
1975
|
+
_mutationDepth = 0;
|
|
1976
|
+
_destroyed = false;
|
|
1702
1977
|
/**
|
|
1703
1978
|
* Set the engine context.
|
|
1704
1979
|
* @internal
|
|
@@ -1707,6 +1982,8 @@ var SceneManager = class {
|
|
|
1707
1982
|
this._context = context;
|
|
1708
1983
|
this.bus = context.tryResolve(EventBusKey);
|
|
1709
1984
|
this.assetManager = context.tryResolve(AssetManagerKey);
|
|
1985
|
+
this.hookRegistry = context.tryResolve(SceneHookRegistryKey);
|
|
1986
|
+
this.logger = context.tryResolve(LoggerKey);
|
|
1710
1987
|
}
|
|
1711
1988
|
/** The topmost (active) scene. */
|
|
1712
1989
|
get active() {
|
|
@@ -1718,84 +1995,129 @@ var SceneManager = class {
|
|
|
1718
1995
|
}
|
|
1719
1996
|
/** All non-paused scenes in the stack, bottom to top. */
|
|
1720
1997
|
get activeScenes() {
|
|
1721
|
-
return this.stack.filter((
|
|
1998
|
+
return this.stack.filter((scene) => !scene.isPaused);
|
|
1999
|
+
}
|
|
2000
|
+
/** Whether a scene transition is currently running. */
|
|
2001
|
+
get isTransitioning() {
|
|
2002
|
+
return this._currentRun !== void 0;
|
|
1722
2003
|
}
|
|
1723
2004
|
/**
|
|
1724
2005
|
* Push a scene onto the stack. Scenes below may receive onPause().
|
|
1725
2006
|
* If the scene declares a `preload` array, assets are loaded before onEnter().
|
|
1726
|
-
* Await the returned promise when using preloaded scenes.
|
|
1727
2007
|
*/
|
|
1728
|
-
push(scene) {
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
1739
|
-
scene.onEnter?.();
|
|
1740
|
-
this.bus?.emit("scene:pushed", { scene });
|
|
1741
|
-
return Promise.resolve();
|
|
2008
|
+
async push(scene, opts) {
|
|
2009
|
+
this._assertNotMutating("push");
|
|
2010
|
+
await this._enqueue(async () => {
|
|
2011
|
+
const fromScene = this.active;
|
|
2012
|
+
await this._pushScene(scene);
|
|
2013
|
+
const transition = resolveTransition(opts?.transition, scene);
|
|
2014
|
+
if (!transition) return;
|
|
2015
|
+
await this._runTransition("push", transition, fromScene, scene);
|
|
2016
|
+
});
|
|
1742
2017
|
}
|
|
1743
2018
|
/** Pop the top scene. Scenes below may receive onResume(). */
|
|
1744
|
-
pop() {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
2019
|
+
async pop(opts) {
|
|
2020
|
+
this._assertNotMutating("pop");
|
|
2021
|
+
return this._enqueue(async () => {
|
|
2022
|
+
if (this.stack.length === 0) return void 0;
|
|
2023
|
+
const fromScene = this.active;
|
|
2024
|
+
const destination = this.stack.length > 1 ? this.stack[this.stack.length - 2] : void 0;
|
|
2025
|
+
const transition = resolveTransition(opts?.transition, destination);
|
|
2026
|
+
if (transition) {
|
|
2027
|
+
await this._runTransition("pop", transition, fromScene, destination);
|
|
2028
|
+
}
|
|
2029
|
+
return this._popScene();
|
|
2030
|
+
});
|
|
1753
2031
|
}
|
|
1754
2032
|
/**
|
|
1755
|
-
* Replace the top scene.
|
|
1756
|
-
*
|
|
2033
|
+
* Replace the top scene. Without a transition the old scene exits first,
|
|
2034
|
+
* then the new scene enters. With a transition the new scene is pushed
|
|
2035
|
+
* first, both scenes coexist for the transition duration, then the old
|
|
2036
|
+
* scene is removed at the end.
|
|
1757
2037
|
*/
|
|
1758
|
-
replace(scene) {
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2038
|
+
async replace(scene, opts) {
|
|
2039
|
+
this._assertNotMutating("replace");
|
|
2040
|
+
await this._enqueue(async () => {
|
|
2041
|
+
const transition = resolveTransition(opts?.transition, scene);
|
|
2042
|
+
if (!transition) {
|
|
2043
|
+
await this._replaceScene(scene);
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const old = this.active;
|
|
2047
|
+
await this._pushScene(scene, true);
|
|
2048
|
+
await this._runTransition("replace", transition, old, scene);
|
|
2049
|
+
if (old) {
|
|
2050
|
+
this._removeScene(old, true);
|
|
2051
|
+
}
|
|
2052
|
+
this.bus?.emit("scene:replaced", {
|
|
2053
|
+
oldScene: old ?? scene,
|
|
2054
|
+
newScene: scene
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Pop every scene on the stack, top to bottom. Each receives onExit().
|
|
2060
|
+
* Queued like push/pop/replace — runs after any in-flight transition.
|
|
2061
|
+
* Use for "restart from menu"-style flows. Does not run transitions.
|
|
2062
|
+
*/
|
|
2063
|
+
async popAll() {
|
|
2064
|
+
this._assertNotMutating("popAll");
|
|
2065
|
+
await this._enqueue(async () => {
|
|
2066
|
+
this._withMutationSync(() => {
|
|
2067
|
+
while (this.stack.length > 0) {
|
|
2068
|
+
const scene = this.stack.pop();
|
|
2069
|
+
if (!scene) break;
|
|
2070
|
+
this._teardownScene(scene);
|
|
2071
|
+
this.bus?.emit("scene:popped", { scene });
|
|
1779
2072
|
}
|
|
1780
2073
|
});
|
|
1781
|
-
}
|
|
1782
|
-
scene.onEnter?.();
|
|
1783
|
-
if (old) {
|
|
1784
|
-
this.bus?.emit("scene:replaced", { oldScene: old, newScene: scene });
|
|
1785
|
-
} else {
|
|
1786
|
-
this.bus?.emit("scene:pushed", { scene });
|
|
1787
|
-
}
|
|
1788
|
-
return Promise.resolve();
|
|
2074
|
+
});
|
|
1789
2075
|
}
|
|
1790
|
-
/**
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2076
|
+
/**
|
|
2077
|
+
* Run the full scene-enter lifecycle (beforeEnter hooks, preload, onEnter)
|
|
2078
|
+
* for a scene that is NOT placed on the stack. Used by infrastructure
|
|
2079
|
+
* plugins like DebugPlugin that render a scene off-stack.
|
|
2080
|
+
* @internal
|
|
2081
|
+
*/
|
|
2082
|
+
async _mountDetached(scene) {
|
|
2083
|
+
await this._withMutation(async () => {
|
|
2084
|
+
scene._setContext(this._context);
|
|
2085
|
+
await this.hookRegistry?.runBeforeEnter(scene);
|
|
2086
|
+
await this._preloadScene(scene);
|
|
2087
|
+
scene.onEnter?.();
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Run the scene-exit lifecycle (onExit, entity destruction, afterExit
|
|
2092
|
+
* hooks, scoped-service clear) for a detached scene.
|
|
2093
|
+
* @internal
|
|
2094
|
+
*/
|
|
2095
|
+
_unmountDetached(scene) {
|
|
2096
|
+
this._withMutationSync(() => {
|
|
2097
|
+
this._teardownScene(scene);
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Mark the manager destroyed and synchronously tear down every scene.
|
|
2102
|
+
* Called by Engine.destroy(). Any queued async work short-circuits on
|
|
2103
|
+
* resume; in-flight transitions' pending promises are resolved via
|
|
2104
|
+
* _cleanupRun so they don't leak.
|
|
2105
|
+
* @internal
|
|
2106
|
+
*/
|
|
2107
|
+
_destroy() {
|
|
2108
|
+
this._destroyed = true;
|
|
2109
|
+
if (this._currentRun) {
|
|
2110
|
+
this._cleanupRun(this._currentRun);
|
|
2111
|
+
}
|
|
2112
|
+
this._pendingChain = Promise.resolve();
|
|
2113
|
+
this._withMutationSync(() => {
|
|
2114
|
+
while (this.stack.length > 0) {
|
|
2115
|
+
const scene = this.stack.pop();
|
|
2116
|
+
if (!scene) break;
|
|
2117
|
+
this._teardownScene(scene);
|
|
2118
|
+
this.bus?.emit("scene:popped", { scene });
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
1799
2121
|
}
|
|
1800
2122
|
/**
|
|
1801
2123
|
* Flush destroy queues for all active scenes.
|
|
@@ -1807,21 +2129,216 @@ var SceneManager = class {
|
|
|
1807
2129
|
scene._flushDestroyQueue();
|
|
1808
2130
|
}
|
|
1809
2131
|
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Advance the active transition by `dt` ms. Called by Engine's earlyUpdate
|
|
2134
|
+
* callback with raw (unscaled) wall-clock dt.
|
|
2135
|
+
* @internal
|
|
2136
|
+
*/
|
|
2137
|
+
_tickTransition(dt) {
|
|
2138
|
+
const run = this._currentRun;
|
|
2139
|
+
if (!run) return;
|
|
2140
|
+
const remaining = run.transition.duration - run.elapsed;
|
|
2141
|
+
const consume = Math.min(dt, remaining);
|
|
2142
|
+
run.elapsed += consume;
|
|
2143
|
+
this._safeTick(run, consume);
|
|
2144
|
+
if (run.elapsed >= run.transition.duration) {
|
|
2145
|
+
this._cleanupRun(run);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
// ---- Private helpers ----
|
|
2149
|
+
_enqueue(work) {
|
|
2150
|
+
if (this._destroyed) return Promise.resolve(void 0);
|
|
2151
|
+
const next = this._pendingChain.then(async () => {
|
|
2152
|
+
if (this._destroyed) return void 0;
|
|
2153
|
+
return work();
|
|
2154
|
+
});
|
|
2155
|
+
this._pendingChain = next.then(
|
|
2156
|
+
() => void 0,
|
|
2157
|
+
() => void 0
|
|
2158
|
+
);
|
|
2159
|
+
return next;
|
|
2160
|
+
}
|
|
2161
|
+
async _pushScene(scene, suppressEvent = false) {
|
|
2162
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2163
|
+
await this._withMutation(async () => {
|
|
2164
|
+
scene._setContext(this._context);
|
|
2165
|
+
await this.hookRegistry?.runBeforeEnter(scene);
|
|
2166
|
+
await this._preloadScene(scene);
|
|
2167
|
+
this.stack.push(scene);
|
|
2168
|
+
scene.onEnter?.();
|
|
2169
|
+
this._firePauseTransitions(wasPaused);
|
|
2170
|
+
if (!suppressEvent) {
|
|
2171
|
+
this.bus?.emit("scene:pushed", { scene });
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
_popScene(suppressEvent = false) {
|
|
2176
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2177
|
+
return this._withMutationSync(() => {
|
|
2178
|
+
const removed = this.stack.pop();
|
|
2179
|
+
if (!removed) return void 0;
|
|
2180
|
+
this._teardownScene(removed);
|
|
2181
|
+
this._fireResumeTransitions(wasPaused);
|
|
2182
|
+
if (!suppressEvent) {
|
|
2183
|
+
this.bus?.emit("scene:popped", { scene: removed });
|
|
2184
|
+
}
|
|
2185
|
+
return removed;
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
async _replaceScene(scene) {
|
|
2189
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2190
|
+
await this._withMutation(async () => {
|
|
2191
|
+
scene._setContext(this._context);
|
|
2192
|
+
await this.hookRegistry?.runBeforeEnter(scene);
|
|
2193
|
+
await this._preloadScene(scene);
|
|
2194
|
+
const old = this.stack.pop();
|
|
2195
|
+
if (old) this._teardownScene(old);
|
|
2196
|
+
this.stack.push(scene);
|
|
2197
|
+
scene.onEnter?.();
|
|
2198
|
+
this._firePauseTransitions(wasPaused);
|
|
2199
|
+
this._fireResumeTransitions(wasPaused);
|
|
2200
|
+
this.bus?.emit("scene:replaced", {
|
|
2201
|
+
oldScene: old ?? scene,
|
|
2202
|
+
newScene: scene
|
|
2203
|
+
});
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
_removeScene(scene, suppressEvent = false) {
|
|
2207
|
+
this._withMutationSync(() => {
|
|
2208
|
+
const idx = this.stack.indexOf(scene);
|
|
2209
|
+
if (idx === -1) return;
|
|
2210
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2211
|
+
this.stack.splice(idx, 1);
|
|
2212
|
+
this._teardownScene(scene);
|
|
2213
|
+
this._firePauseTransitions(wasPaused);
|
|
2214
|
+
this._fireResumeTransitions(wasPaused);
|
|
2215
|
+
if (!suppressEvent) {
|
|
2216
|
+
this.bus?.emit("scene:popped", { scene });
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
async _preloadScene(scene) {
|
|
2221
|
+
if (!scene.preload?.length || !this.assetManager) return;
|
|
2222
|
+
await this.assetManager.loadAll(
|
|
2223
|
+
scene.preload,
|
|
2224
|
+
scene.onProgress?.bind(scene)
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
_teardownScene(scene) {
|
|
2228
|
+
scene.onExit?.();
|
|
2229
|
+
scene._destroyAllEntities();
|
|
2230
|
+
this.hookRegistry?.runAfterExit(scene);
|
|
2231
|
+
scene._clearScopedServices();
|
|
2232
|
+
}
|
|
2233
|
+
async _runTransition(kind, transition, fromScene, toScene) {
|
|
2234
|
+
if (this._destroyed) return;
|
|
2235
|
+
let resolveRun;
|
|
2236
|
+
const promise = new Promise((resolve) => {
|
|
2237
|
+
resolveRun = resolve;
|
|
2238
|
+
});
|
|
2239
|
+
const run = {
|
|
2240
|
+
kind,
|
|
2241
|
+
transition,
|
|
2242
|
+
elapsed: 0,
|
|
2243
|
+
fromScene,
|
|
2244
|
+
toScene,
|
|
2245
|
+
resolve: resolveRun
|
|
2246
|
+
};
|
|
2247
|
+
this._currentRun = run;
|
|
2248
|
+
this.bus?.emit("scene:transition:started", {
|
|
2249
|
+
kind,
|
|
2250
|
+
fromScene,
|
|
2251
|
+
toScene
|
|
2252
|
+
});
|
|
2253
|
+
this._safeCall(run, "begin");
|
|
2254
|
+
if (!Number.isFinite(transition.duration) || transition.duration <= 0) {
|
|
2255
|
+
this._cleanupRun(run);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
await promise;
|
|
2259
|
+
}
|
|
2260
|
+
_cleanupRun(run) {
|
|
2261
|
+
if (this._currentRun !== run) return;
|
|
2262
|
+
this._safeCall(run, "end");
|
|
2263
|
+
this._currentRun = void 0;
|
|
2264
|
+
this.bus?.emit("scene:transition:ended", {
|
|
2265
|
+
kind: run.kind,
|
|
2266
|
+
fromScene: run.fromScene,
|
|
2267
|
+
toScene: run.toScene
|
|
2268
|
+
});
|
|
2269
|
+
run.resolve();
|
|
2270
|
+
}
|
|
2271
|
+
_safeTick(run, dt) {
|
|
2272
|
+
try {
|
|
2273
|
+
run.transition.tick(dt, this._makeContext(run));
|
|
2274
|
+
} catch (err) {
|
|
2275
|
+
this.logger?.warn(
|
|
2276
|
+
"SceneManager",
|
|
2277
|
+
`Transition tick error: ${err instanceof Error ? err.message : String(err)}`
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
_safeCall(run, method) {
|
|
2282
|
+
try {
|
|
2283
|
+
run.transition[method]?.(this._makeContext(run));
|
|
2284
|
+
} catch (err) {
|
|
2285
|
+
this.logger?.warn(
|
|
2286
|
+
"SceneManager",
|
|
2287
|
+
`Transition ${method} error: ${err instanceof Error ? err.message : String(err)}`
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
_makeContext(run) {
|
|
2292
|
+
return {
|
|
2293
|
+
elapsed: run.elapsed,
|
|
2294
|
+
kind: run.kind,
|
|
2295
|
+
engineContext: this._context,
|
|
2296
|
+
fromScene: run.fromScene,
|
|
2297
|
+
toScene: run.toScene
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
_snapshotPauseStates() {
|
|
2301
|
+
return new Map(
|
|
2302
|
+
this.stack.map((scene) => [scene, scene.isPaused])
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
_assertNotMutating(method) {
|
|
2306
|
+
if (this._mutationDepth === 0) return;
|
|
2307
|
+
throw new Error(
|
|
2308
|
+
`SceneManager.${method}() called reentrantly from a scene lifecycle hook (onEnter/onExit/onPause/onResume or a beforeEnter/afterExit hook). Defer the call outside the hook, e.g. via queueMicrotask() or from a component update().`
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
async _withMutation(work) {
|
|
2312
|
+
this._mutationDepth++;
|
|
2313
|
+
try {
|
|
2314
|
+
return await work();
|
|
2315
|
+
} finally {
|
|
2316
|
+
this._mutationDepth--;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
_withMutationSync(work) {
|
|
2320
|
+
this._mutationDepth++;
|
|
2321
|
+
try {
|
|
2322
|
+
return work();
|
|
2323
|
+
} finally {
|
|
2324
|
+
this._mutationDepth--;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
1810
2327
|
/** Fire onPause() for scenes that transitioned from not-paused to paused. */
|
|
1811
2328
|
_firePauseTransitions(wasPaused) {
|
|
1812
|
-
for (const
|
|
1813
|
-
const was = wasPaused.get(
|
|
1814
|
-
if (
|
|
1815
|
-
|
|
2329
|
+
for (const scene of this.stack) {
|
|
2330
|
+
const was = wasPaused.get(scene) ?? false;
|
|
2331
|
+
if (scene.isPaused && !was) {
|
|
2332
|
+
scene.onPause?.();
|
|
1816
2333
|
}
|
|
1817
2334
|
}
|
|
1818
2335
|
}
|
|
1819
2336
|
/** Fire onResume() for scenes that transitioned from paused to not-paused. */
|
|
1820
2337
|
_fireResumeTransitions(wasPaused) {
|
|
1821
|
-
for (const
|
|
1822
|
-
const was = wasPaused.get(
|
|
1823
|
-
if (!
|
|
1824
|
-
|
|
2338
|
+
for (const scene of this.stack) {
|
|
2339
|
+
const was = wasPaused.get(scene) ?? false;
|
|
2340
|
+
if (!scene.isPaused && was) {
|
|
2341
|
+
scene.onResume?.();
|
|
1825
2342
|
}
|
|
1826
2343
|
}
|
|
1827
2344
|
}
|
|
@@ -2658,6 +3175,7 @@ var Engine = class {
|
|
|
2658
3175
|
scheduler;
|
|
2659
3176
|
errorBoundary;
|
|
2660
3177
|
queryCache;
|
|
3178
|
+
sceneHooks;
|
|
2661
3179
|
/** The asset manager. */
|
|
2662
3180
|
assets;
|
|
2663
3181
|
plugins = /* @__PURE__ */ new Map();
|
|
@@ -2676,6 +3194,7 @@ var Engine = class {
|
|
|
2676
3194
|
this.scheduler = new SystemScheduler();
|
|
2677
3195
|
this.inspector = new Inspector(this);
|
|
2678
3196
|
this.assets = new AssetManager();
|
|
3197
|
+
this.sceneHooks = new SceneHookRegistry();
|
|
2679
3198
|
this.scheduler.setErrorBoundary(this.errorBoundary);
|
|
2680
3199
|
this.context.register(EngineKey, this);
|
|
2681
3200
|
this.context.register(EventBusKey, this.events);
|
|
@@ -2687,11 +3206,13 @@ var Engine = class {
|
|
|
2687
3206
|
this.context.register(InspectorKey, this.inspector);
|
|
2688
3207
|
this.context.register(SystemSchedulerKey, this.scheduler);
|
|
2689
3208
|
this.context.register(AssetManagerKey, this.assets);
|
|
3209
|
+
this.context.register(SceneHookRegistryKey, this.sceneHooks);
|
|
2690
3210
|
this.scenes._setContext(this.context);
|
|
2691
3211
|
this.registerBuiltInSystems();
|
|
2692
3212
|
this.loop.setCallbacks({
|
|
2693
3213
|
earlyUpdate: /* @__PURE__ */ __name((dt) => {
|
|
2694
3214
|
this.logger.setFrame(this.loop.frameCount);
|
|
3215
|
+
this.scenes._tickTransition(dt);
|
|
2695
3216
|
this.scheduler.run("earlyUpdate" /* EarlyUpdate */, dt);
|
|
2696
3217
|
}, "earlyUpdate"),
|
|
2697
3218
|
fixedUpdate: /* @__PURE__ */ __name((dt) => this.scheduler.run("fixedUpdate" /* FixedUpdate */, dt), "fixedUpdate"),
|
|
@@ -2704,6 +3225,14 @@ var Engine = class {
|
|
|
2704
3225
|
}, "endOfFrame")
|
|
2705
3226
|
});
|
|
2706
3227
|
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Register scene lifecycle hooks. The returned function unregisters the
|
|
3230
|
+
* hooks. Infrastructure plugins (renderer, physics, debug) register hooks
|
|
3231
|
+
* in their `install` or `onStart` to set up and tear down per-scene state.
|
|
3232
|
+
*/
|
|
3233
|
+
registerSceneHooks(hooks) {
|
|
3234
|
+
return this.sceneHooks.register(hooks);
|
|
3235
|
+
}
|
|
2707
3236
|
/** Register a plugin. Must be called before start(). */
|
|
2708
3237
|
use(plugin) {
|
|
2709
3238
|
if (this.started) {
|
|
@@ -2739,7 +3268,7 @@ var Engine = class {
|
|
|
2739
3268
|
};
|
|
2740
3269
|
}
|
|
2741
3270
|
for (const plugin of sorted) {
|
|
2742
|
-
plugin.onStart?.();
|
|
3271
|
+
await plugin.onStart?.();
|
|
2743
3272
|
}
|
|
2744
3273
|
this.events.emit("engine:started", void 0);
|
|
2745
3274
|
}
|
|
@@ -2747,7 +3276,7 @@ var Engine = class {
|
|
|
2747
3276
|
destroy() {
|
|
2748
3277
|
this.events.emit("engine:stopped", void 0);
|
|
2749
3278
|
this.loop.stop();
|
|
2750
|
-
this.scenes.
|
|
3279
|
+
this.scenes._destroy();
|
|
2751
3280
|
const allSystems = this.scheduler.getAllSystems();
|
|
2752
3281
|
for (let i = allSystems.length - 1; i >= 0; i--) {
|
|
2753
3282
|
allSystems[i].onUnregister?.();
|
|
@@ -2890,6 +3419,7 @@ export {
|
|
|
2890
3419
|
Inspector,
|
|
2891
3420
|
InspectorKey,
|
|
2892
3421
|
KeyframeAnimator,
|
|
3422
|
+
LoadingScene,
|
|
2893
3423
|
LogLevel,
|
|
2894
3424
|
Logger,
|
|
2895
3425
|
LoggerKey,
|
|
@@ -2905,6 +3435,8 @@ export {
|
|
|
2905
3435
|
QueryResult,
|
|
2906
3436
|
SERIALIZABLE_KEY,
|
|
2907
3437
|
Scene,
|
|
3438
|
+
SceneHookRegistry,
|
|
3439
|
+
SceneHookRegistryKey,
|
|
2908
3440
|
SceneManager,
|
|
2909
3441
|
SceneManagerKey,
|
|
2910
3442
|
Sequence,
|
|
@@ -2937,6 +3469,7 @@ export {
|
|
|
2937
3469
|
getSerializableType,
|
|
2938
3470
|
interpolate,
|
|
2939
3471
|
isSerializable,
|
|
3472
|
+
resolveTransition,
|
|
2940
3473
|
serializable,
|
|
2941
3474
|
trait
|
|
2942
3475
|
};
|