@vulfram/engine 0.19.2-alpha → 0.20.1-alpha
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/package.json +3 -3
- package/src/engine/ecs/components.ts +8 -0
- package/src/engine/systems/input-mirror.ts +10 -0
- package/src/engine/world/entities/audio.ts +193 -0
- package/src/engine/world/entities/common.ts +30 -0
- package/src/engine/world/entities/intents.ts +12 -0
- package/src/engine/world/entities/resources.ts +185 -0
- package/src/engine/world/entities/scene.ts +242 -0
- package/src/engine/world/entities/state-queries.ts +317 -0
- package/src/engine/world/entities/targets.ts +203 -0
- package/src/engine/world/entities/ui.ts +192 -0
- package/src/engine/world/entities/world-state.ts +140 -0
- package/src/engine/world/entities.ts +8 -1578
- package/src/engine/world/world-ui.ts +50 -2
- package/src/engine/world/world3d.ts +58 -0
- package/src/types/cmds/environment.ts +9 -0
- package/src/types/cmds/index.ts +33 -0
- package/src/types/cmds/render-graph.ts +38 -4
- package/src/types/events/system.ts +1 -1
- package/src/types/events/window.ts +27 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CmdTargetMeasurementArgs,
|
|
3
|
+
CmdTargetDisposeArgs,
|
|
4
|
+
CmdTargetLayerDisposeArgs,
|
|
5
|
+
CmdTargetLayerUpsertArgs,
|
|
6
|
+
CmdTargetUpsertArgs,
|
|
7
|
+
TargetLayerLayout,
|
|
8
|
+
} from '../../../types/cmds/target';
|
|
9
|
+
import type {
|
|
10
|
+
CmdInputTargetListenerDisposeArgs,
|
|
11
|
+
CmdInputTargetListenerListArgs,
|
|
12
|
+
CmdInputTargetListenerUpsertArgs,
|
|
13
|
+
} from '../../../types/cmds/input';
|
|
14
|
+
import { enqueueCommand, markRoutingIndexDirty } from '../../bridge/dispatch';
|
|
15
|
+
import { getWorldOrThrow, requireInitialized } from '../../bridge/guards';
|
|
16
|
+
import type { CameraComponent } from '../../ecs';
|
|
17
|
+
import { allocateGlobalId, recalculateWorldWindowBindings } from './common';
|
|
18
|
+
import { getWorldRealmId } from './world-state';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Upserts a target used to present world output.
|
|
22
|
+
*/
|
|
23
|
+
export function upsertTarget(worldId: number, args: CmdTargetUpsertArgs): number {
|
|
24
|
+
const id = enqueueCommand(worldId, 'cmd-target-upsert', args);
|
|
25
|
+
const world = getWorldOrThrow(worldId);
|
|
26
|
+
if (args.kind === 'window' && args.windowId !== undefined) {
|
|
27
|
+
world.targetWindowBindings.set(args.targetId, args.windowId);
|
|
28
|
+
recalculateWorldWindowBindings(world);
|
|
29
|
+
markRoutingIndexDirty();
|
|
30
|
+
} else {
|
|
31
|
+
if (world.targetWindowBindings.delete(args.targetId)) {
|
|
32
|
+
recalculateWorldWindowBindings(world);
|
|
33
|
+
}
|
|
34
|
+
markRoutingIndexDirty();
|
|
35
|
+
}
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Requests size measurements for a target.
|
|
41
|
+
*/
|
|
42
|
+
export function measureTarget(
|
|
43
|
+
worldId: number,
|
|
44
|
+
args: CmdTargetMeasurementArgs,
|
|
45
|
+
): number {
|
|
46
|
+
return enqueueCommand(worldId, 'cmd-target-measurement', args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates or updates an input listener bound to a routed target.
|
|
51
|
+
*/
|
|
52
|
+
export function upsertInputTargetListener(
|
|
53
|
+
worldId: number,
|
|
54
|
+
args: CmdInputTargetListenerUpsertArgs,
|
|
55
|
+
): number {
|
|
56
|
+
return enqueueCommand(worldId, 'cmd-input-target-listener-upsert', args);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Disposes an input listener bound to routed targets.
|
|
61
|
+
*/
|
|
62
|
+
export function disposeInputTargetListener(
|
|
63
|
+
worldId: number,
|
|
64
|
+
args: CmdInputTargetListenerDisposeArgs,
|
|
65
|
+
): number {
|
|
66
|
+
return enqueueCommand(worldId, 'cmd-input-target-listener-dispose', args);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Lists input listeners (optionally filtered by target id).
|
|
71
|
+
*/
|
|
72
|
+
export function listInputTargetListeners(
|
|
73
|
+
worldId: number,
|
|
74
|
+
args: CmdInputTargetListenerListArgs = {},
|
|
75
|
+
): number {
|
|
76
|
+
return enqueueCommand(worldId, 'cmd-input-target-listener-list', args);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Disposes a target.
|
|
81
|
+
*/
|
|
82
|
+
export function disposeTarget(worldId: number, args: CmdTargetDisposeArgs): number {
|
|
83
|
+
const world = getWorldOrThrow(worldId);
|
|
84
|
+
world.targetLayerBindings.delete(args.targetId);
|
|
85
|
+
if (world.targetWindowBindings.delete(args.targetId)) {
|
|
86
|
+
recalculateWorldWindowBindings(world);
|
|
87
|
+
}
|
|
88
|
+
markRoutingIndexDirty();
|
|
89
|
+
return enqueueCommand(worldId, 'cmd-target-dispose', args);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findPreferredCameraId(worldId: number): number | undefined {
|
|
93
|
+
const world = getWorldOrThrow(worldId);
|
|
94
|
+
let bestId: number | undefined;
|
|
95
|
+
let bestOrder = Number.POSITIVE_INFINITY;
|
|
96
|
+
for (const store of world.components.values()) {
|
|
97
|
+
const camera = store.get('Camera') as CameraComponent | undefined;
|
|
98
|
+
if (!camera) continue;
|
|
99
|
+
if (
|
|
100
|
+
camera.order < bestOrder ||
|
|
101
|
+
(camera.order === bestOrder &&
|
|
102
|
+
(bestId === undefined || camera.id < bestId))
|
|
103
|
+
) {
|
|
104
|
+
bestOrder = camera.order;
|
|
105
|
+
bestId = camera.id;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return bestId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Binds this world's realm to a target layer.
|
|
113
|
+
*/
|
|
114
|
+
export function bindWorldToTarget(
|
|
115
|
+
worldId: number,
|
|
116
|
+
args: Omit<CmdTargetLayerUpsertArgs, 'realmId'>,
|
|
117
|
+
): number {
|
|
118
|
+
const world = getWorldOrThrow(worldId);
|
|
119
|
+
|
|
120
|
+
const resolvedCameraId = args.cameraId ?? findPreferredCameraId(worldId);
|
|
121
|
+
|
|
122
|
+
world.targetLayerBindings.set(args.targetId, {
|
|
123
|
+
targetId: args.targetId,
|
|
124
|
+
layout: args.layout,
|
|
125
|
+
cameraId: resolvedCameraId,
|
|
126
|
+
environmentId: args.environmentId,
|
|
127
|
+
});
|
|
128
|
+
markRoutingIndexDirty();
|
|
129
|
+
|
|
130
|
+
const realmId = getWorldRealmId(worldId);
|
|
131
|
+
if (realmId === null) {
|
|
132
|
+
// Realm not ready yet: keep binding cached and let response-decode flush later.
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return enqueueCommand(worldId, 'cmd-target-layer-upsert', {
|
|
137
|
+
realmId,
|
|
138
|
+
...args,
|
|
139
|
+
cameraId: resolvedCameraId,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Unbinds this world's realm from a target layer.
|
|
145
|
+
*/
|
|
146
|
+
export function unbindWorldFromTarget(
|
|
147
|
+
worldId: number,
|
|
148
|
+
args: Omit<CmdTargetLayerDisposeArgs, 'realmId'>,
|
|
149
|
+
): number {
|
|
150
|
+
const world = getWorldOrThrow(worldId);
|
|
151
|
+
world.targetLayerBindings.delete(args.targetId);
|
|
152
|
+
if (world.targetWindowBindings.delete(args.targetId)) {
|
|
153
|
+
recalculateWorldWindowBindings(world);
|
|
154
|
+
}
|
|
155
|
+
markRoutingIndexDirty();
|
|
156
|
+
|
|
157
|
+
const realmId = getWorldRealmId(worldId);
|
|
158
|
+
if (realmId === null) {
|
|
159
|
+
// Realm not ready yet: nothing to dispose in core.
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return enqueueCommand(worldId, 'cmd-target-layer-dispose', {
|
|
164
|
+
realmId,
|
|
165
|
+
...args,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Convenience helper: presents this world in a window via target/layer bind.
|
|
171
|
+
*/
|
|
172
|
+
export function presentWorldInWindow(
|
|
173
|
+
worldId: number,
|
|
174
|
+
args: {
|
|
175
|
+
windowId: number;
|
|
176
|
+
targetId?: number;
|
|
177
|
+
layout?: TargetLayerLayout;
|
|
178
|
+
cameraId?: number;
|
|
179
|
+
environmentId?: number;
|
|
180
|
+
},
|
|
181
|
+
): { targetId: number; upsertCommandId: number; bindCommandId: number } {
|
|
182
|
+
requireInitialized();
|
|
183
|
+
const targetId = args.targetId ?? allocateGlobalId();
|
|
184
|
+
const upsertCommandId = upsertTarget(worldId, {
|
|
185
|
+
targetId,
|
|
186
|
+
kind: 'window',
|
|
187
|
+
windowId: args.windowId,
|
|
188
|
+
});
|
|
189
|
+
const bindCommandId = bindWorldToTarget(worldId, {
|
|
190
|
+
targetId,
|
|
191
|
+
layout: args.layout ?? {
|
|
192
|
+
left: { unit: 'percent', value: 0 },
|
|
193
|
+
top: { unit: 'percent', value: 0 },
|
|
194
|
+
width: { unit: 'percent', value: 100 },
|
|
195
|
+
height: { unit: 'percent', value: 100 },
|
|
196
|
+
zIndex: 0,
|
|
197
|
+
blendMode: 0,
|
|
198
|
+
},
|
|
199
|
+
cameraId: args.cameraId,
|
|
200
|
+
environmentId: args.environmentId,
|
|
201
|
+
});
|
|
202
|
+
return { targetId, upsertCommandId, bindCommandId };
|
|
203
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CmdUiAccessKitActionRequestArgs,
|
|
3
|
+
CmdUiApplyOpsArgs,
|
|
4
|
+
CmdUiClipboardPasteArgs,
|
|
5
|
+
CmdUiDebugSetArgs,
|
|
6
|
+
CmdUiDocumentCreateArgs,
|
|
7
|
+
CmdUiDocumentDisposeArgs,
|
|
8
|
+
CmdUiDocumentGetLayoutRectsArgs,
|
|
9
|
+
CmdUiDocumentGetTreeArgs,
|
|
10
|
+
CmdUiDocumentSetRectArgs,
|
|
11
|
+
CmdUiDocumentSetThemeArgs,
|
|
12
|
+
CmdUiEventTraceSetArgs,
|
|
13
|
+
CmdUiFocusGetArgs,
|
|
14
|
+
CmdUiFocusSetArgs,
|
|
15
|
+
CmdUiImageCreateFromBufferArgs,
|
|
16
|
+
CmdUiImageDisposeArgs,
|
|
17
|
+
CmdUiScreenshotReplyArgs,
|
|
18
|
+
CmdUiThemeDefineArgs,
|
|
19
|
+
CmdUiThemeDisposeArgs,
|
|
20
|
+
} from '../../../types/cmds/ui';
|
|
21
|
+
import type { UiFocusCycleMode } from '../../ecs';
|
|
22
|
+
import { emitIntent } from './intents';
|
|
23
|
+
|
|
24
|
+
/** Defines or updates a UI theme. */
|
|
25
|
+
export function uiDefineTheme(worldId: number, args: CmdUiThemeDefineArgs): void {
|
|
26
|
+
emitIntent(worldId, { type: 'ui-theme-define', args });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Disposes a UI theme. */
|
|
30
|
+
export function uiDisposeTheme(worldId: number, args: CmdUiThemeDisposeArgs): void {
|
|
31
|
+
emitIntent(worldId, { type: 'ui-theme-dispose', args });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Creates a UI document. */
|
|
35
|
+
export function uiCreateDocument(worldId: number, args: CmdUiDocumentCreateArgs): void {
|
|
36
|
+
emitIntent(worldId, { type: 'ui-document-create', args });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Disposes a UI document. */
|
|
40
|
+
export function uiDisposeDocument(worldId: number, args: CmdUiDocumentDisposeArgs): void {
|
|
41
|
+
emitIntent(worldId, { type: 'ui-document-dispose', args });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Updates document rectangle. */
|
|
45
|
+
export function uiSetDocumentRect(worldId: number, args: CmdUiDocumentSetRectArgs): void {
|
|
46
|
+
emitIntent(worldId, { type: 'ui-document-set-rect', args });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Updates document theme. */
|
|
50
|
+
export function uiSetDocumentTheme(worldId: number, args: CmdUiDocumentSetThemeArgs): void {
|
|
51
|
+
emitIntent(worldId, { type: 'ui-document-set-theme', args });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Applies document ops. */
|
|
55
|
+
export function uiApplyOps(worldId: number, args: CmdUiApplyOpsArgs): void {
|
|
56
|
+
emitIntent(worldId, { type: 'ui-apply-ops', args });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Requests UI document tree for introspection. */
|
|
60
|
+
export function uiGetDocumentTree(worldId: number, args: CmdUiDocumentGetTreeArgs): void {
|
|
61
|
+
emitIntent(worldId, { type: 'ui-document-get-tree', args });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Requests UI layout rects for introspection. */
|
|
65
|
+
export function uiGetLayoutRects(
|
|
66
|
+
worldId: number,
|
|
67
|
+
args: CmdUiDocumentGetLayoutRectsArgs,
|
|
68
|
+
): void {
|
|
69
|
+
emitIntent(worldId, { type: 'ui-document-get-layout-rects', args });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Enables/disables runtime UI debug overlays. */
|
|
73
|
+
export function uiSetDebug(worldId: number, args: CmdUiDebugSetArgs): void {
|
|
74
|
+
emitIntent(worldId, { type: 'ui-debug-set', args });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Sets focused UI node. */
|
|
78
|
+
export function uiSetFocus(worldId: number, args: CmdUiFocusSetArgs): void {
|
|
79
|
+
emitIntent(worldId, { type: 'ui-focus-set', args });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Requests current UI focus state. */
|
|
83
|
+
export function uiGetFocus(worldId: number, args: CmdUiFocusGetArgs = {}): void {
|
|
84
|
+
emitIntent(worldId, { type: 'ui-focus-get', args });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Configures UI event trace level/sampling. */
|
|
88
|
+
export function uiSetEventTrace(worldId: number, args: CmdUiEventTraceSetArgs): void {
|
|
89
|
+
emitIntent(worldId, { type: 'ui-event-trace-set', args });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Creates a UI image from uploaded bytes. */
|
|
93
|
+
export function uiCreateImageFromBuffer(
|
|
94
|
+
worldId: number,
|
|
95
|
+
args: CmdUiImageCreateFromBufferArgs,
|
|
96
|
+
): void {
|
|
97
|
+
emitIntent(worldId, { type: 'ui-image-create-from-buffer', args });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Disposes a UI image. */
|
|
101
|
+
export function uiDisposeImage(worldId: number, args: CmdUiImageDisposeArgs): void {
|
|
102
|
+
emitIntent(worldId, { type: 'ui-image-dispose', args });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Delivers host clipboard paste event to UI. */
|
|
106
|
+
export function uiClipboardPaste(worldId: number, args: CmdUiClipboardPasteArgs): void {
|
|
107
|
+
emitIntent(worldId, { type: 'ui-clipboard-paste', args });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Delivers screenshot response bytes to UI. */
|
|
111
|
+
export function uiScreenshotReply(worldId: number, args: CmdUiScreenshotReplyArgs): void {
|
|
112
|
+
emitIntent(worldId, { type: 'ui-screenshot-reply', args });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Delivers AccessKit action request to UI. */
|
|
116
|
+
export function uiAccessKitActionRequest(
|
|
117
|
+
worldId: number,
|
|
118
|
+
args: CmdUiAccessKitActionRequestArgs,
|
|
119
|
+
): void {
|
|
120
|
+
emitIntent(worldId, { type: 'ui-access-kit-action-request', args });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Registers or updates a UI form scope used for tab navigation. */
|
|
124
|
+
export function uiFormUpsert(
|
|
125
|
+
worldId: number,
|
|
126
|
+
form: {
|
|
127
|
+
formId: string;
|
|
128
|
+
windowId: number;
|
|
129
|
+
realmId: number;
|
|
130
|
+
documentId: number;
|
|
131
|
+
disabled?: boolean;
|
|
132
|
+
cycleMode?: UiFocusCycleMode;
|
|
133
|
+
activeFieldsetId?: string;
|
|
134
|
+
},
|
|
135
|
+
): void {
|
|
136
|
+
emitIntent(worldId, { type: 'ui-form-upsert', form });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Disposes a registered UI form scope. */
|
|
140
|
+
export function uiFormDispose(worldId: number, formId: string): void {
|
|
141
|
+
emitIntent(worldId, { type: 'ui-form-dispose', formId });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Registers or updates fieldset metadata. */
|
|
145
|
+
export function uiFieldsetUpsert(
|
|
146
|
+
worldId: number,
|
|
147
|
+
fieldset: {
|
|
148
|
+
formId: string;
|
|
149
|
+
fieldsetId: string;
|
|
150
|
+
disabled?: boolean;
|
|
151
|
+
legendNodeId?: number;
|
|
152
|
+
},
|
|
153
|
+
): void {
|
|
154
|
+
emitIntent(worldId, { type: 'ui-fieldset-upsert', fieldset });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Disposes fieldset metadata. */
|
|
158
|
+
export function uiFieldsetDispose(
|
|
159
|
+
worldId: number,
|
|
160
|
+
formId: string,
|
|
161
|
+
fieldsetId: string,
|
|
162
|
+
): void {
|
|
163
|
+
emitIntent(worldId, { type: 'ui-fieldset-dispose', formId, fieldsetId });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Registers or updates focusable node metadata. */
|
|
167
|
+
export function uiFocusableUpsert(
|
|
168
|
+
worldId: number,
|
|
169
|
+
focusable: {
|
|
170
|
+
formId: string;
|
|
171
|
+
nodeId: number;
|
|
172
|
+
tabIndex?: number;
|
|
173
|
+
fieldsetId?: string;
|
|
174
|
+
disabled?: boolean;
|
|
175
|
+
orderHint?: number;
|
|
176
|
+
},
|
|
177
|
+
): void {
|
|
178
|
+
emitIntent(worldId, { type: 'ui-focusable-upsert', focusable });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Disposes focusable node metadata. */
|
|
182
|
+
export function uiFocusableDispose(worldId: number, nodeId: number): void {
|
|
183
|
+
emitIntent(worldId, { type: 'ui-focusable-dispose', nodeId });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Advances focus within a form scope using tab ordering. */
|
|
187
|
+
export function uiFocusNext(
|
|
188
|
+
worldId: number,
|
|
189
|
+
args: { windowId: number; backwards?: boolean; formId?: string },
|
|
190
|
+
): void {
|
|
191
|
+
emitIntent(worldId, { type: 'ui-focus-next', ...args });
|
|
192
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CmdSystemDiagnosticsSetArgs,
|
|
3
|
+
CmdUploadBufferDiscardAllArgs,
|
|
4
|
+
} from '../../../types/cmds/system';
|
|
5
|
+
import { EngineError } from '../../errors';
|
|
6
|
+
import {
|
|
7
|
+
enqueueGlobalCommand,
|
|
8
|
+
markRoutingIndexDirty,
|
|
9
|
+
} from '../../bridge/dispatch';
|
|
10
|
+
import { getWorldOrThrow, requireInitialized } from '../../bridge/guards';
|
|
11
|
+
import { engineState } from '../../state';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the core model ID for an entity, if available.
|
|
15
|
+
*/
|
|
16
|
+
export function getModelId(worldId: number, entityId: number): number | null {
|
|
17
|
+
requireInitialized();
|
|
18
|
+
const world = getWorldOrThrow(worldId);
|
|
19
|
+
const store = world.components.get(entityId);
|
|
20
|
+
if (!store) return null;
|
|
21
|
+
const model = store.get('Model') as { id: number } | undefined;
|
|
22
|
+
return model?.id ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the core realm id associated with this world, if already created.
|
|
27
|
+
*/
|
|
28
|
+
export function getWorldRealmId(worldId: number): number | null {
|
|
29
|
+
requireInitialized();
|
|
30
|
+
const world = getWorldOrThrow(worldId);
|
|
31
|
+
return world.coreRealmId ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns true when the world has a resolved core realm id.
|
|
36
|
+
*/
|
|
37
|
+
export function isWorldReady(worldId: number): boolean {
|
|
38
|
+
return getWorldRealmId(worldId) !== null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Disposes a world and optionally releases its bound realm/targets.
|
|
43
|
+
* This operation is immediate on host state; core-side disposal commands are queued globally.
|
|
44
|
+
*/
|
|
45
|
+
export function disposeWorld(
|
|
46
|
+
worldId: number,
|
|
47
|
+
opts: {
|
|
48
|
+
disposeRealm?: boolean;
|
|
49
|
+
disposeTargets?: boolean;
|
|
50
|
+
warnOnUndisposedResources?: boolean;
|
|
51
|
+
strictResourceLifecycle?: boolean;
|
|
52
|
+
} = {},
|
|
53
|
+
): void {
|
|
54
|
+
requireInitialized();
|
|
55
|
+
const world = getWorldOrThrow(worldId);
|
|
56
|
+
const disposeRealm = opts.disposeRealm ?? true;
|
|
57
|
+
const disposeTargets = opts.disposeTargets ?? true;
|
|
58
|
+
const strictResourceLifecycle = opts.strictResourceLifecycle ?? false;
|
|
59
|
+
const warnOnUndisposedResources = opts.warnOnUndisposedResources ?? true;
|
|
60
|
+
|
|
61
|
+
let retainedCoreObjectCount = 0;
|
|
62
|
+
for (const store of world.components.values()) {
|
|
63
|
+
for (const comp of store.values()) {
|
|
64
|
+
if ('id' in comp && typeof comp.id === 'number') {
|
|
65
|
+
retainedCoreObjectCount++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const hasRetainedTargets = world.targetLayerBindings.size > 0;
|
|
70
|
+
const hasPendingWork =
|
|
71
|
+
world.intentStore.size() > 0 || world.pendingCommands.length > 0;
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
(!disposeRealm && (retainedCoreObjectCount > 0 || hasPendingWork)) ||
|
|
75
|
+
(!disposeTargets && hasRetainedTargets)
|
|
76
|
+
) {
|
|
77
|
+
const message =
|
|
78
|
+
`disposeWorld(${worldId}) called without fully releasing resources. ` +
|
|
79
|
+
`retainRealm=${!disposeRealm} retainTargets=${!disposeTargets} ` +
|
|
80
|
+
`trackedCoreObjects=${retainedCoreObjectCount} ` +
|
|
81
|
+
`targetBindings=${world.targetLayerBindings.size} ` +
|
|
82
|
+
`pendingIntents=${world.intentStore.size()} ` +
|
|
83
|
+
`pendingCommands=${world.pendingCommands.length}`;
|
|
84
|
+
if (strictResourceLifecycle) {
|
|
85
|
+
throw new EngineError('WorldDisposeLifecycleRisk', message);
|
|
86
|
+
}
|
|
87
|
+
if (warnOnUndisposedResources) {
|
|
88
|
+
console.warn(`[World ${worldId}] ${message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (disposeTargets) {
|
|
93
|
+
for (const targetId of world.targetLayerBindings.keys()) {
|
|
94
|
+
let isShared = false;
|
|
95
|
+
for (const [otherWorldId, otherWorld] of engineState.worlds) {
|
|
96
|
+
if (otherWorldId === worldId) continue;
|
|
97
|
+
if (otherWorld.targetLayerBindings.has(targetId)) {
|
|
98
|
+
isShared = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!isShared) {
|
|
103
|
+
enqueueGlobalCommand('cmd-target-dispose', { targetId });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (disposeRealm && world.coreRealmId !== undefined) {
|
|
109
|
+
enqueueGlobalCommand('cmd-realm-dispose', { realmId: world.coreRealmId });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const [cmdId, trackedWorldId] of engineState.commandTracker) {
|
|
113
|
+
if (trackedWorldId === worldId) {
|
|
114
|
+
engineState.commandTracker.delete(cmdId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
engineState.worlds.delete(worldId);
|
|
119
|
+
markRoutingIndexDirty();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Configures global runtime diagnostics and pointer tracing.
|
|
124
|
+
*/
|
|
125
|
+
export function setSystemDiagnostics(
|
|
126
|
+
args: CmdSystemDiagnosticsSetArgs,
|
|
127
|
+
): number {
|
|
128
|
+
requireInitialized();
|
|
129
|
+
return enqueueGlobalCommand('cmd-system-diagnostics-set', args);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Requests the core to discard all pending upload buffers.
|
|
134
|
+
*/
|
|
135
|
+
export function discardAllUploadBuffers(
|
|
136
|
+
args: CmdUploadBufferDiscardAllArgs = {},
|
|
137
|
+
): number {
|
|
138
|
+
requireInitialized();
|
|
139
|
+
return enqueueGlobalCommand('cmd-upload-buffer-discard-all', args);
|
|
140
|
+
}
|