@driftengine/tools 4.1.0 → 4.1.1
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/README.md +23 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -2
- package/dist/inspector.d.ts +42 -0
- package/dist/inspector.js +55 -0
- package/dist/network.d.ts +42 -0
- package/dist/network.js +43 -0
- package/package.json +6 -6
- package/src/index.ts +6 -0
- package/src/inspector.ts +77 -0
- package/src/network.ts +68 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The editor's panels, as something a shipped game can carry: an inspector, a cons
|
|
|
4
4
|
a network panel, with the command stack that makes their edits undoable, and the overlay that puts
|
|
5
5
|
them on screen on a key.
|
|
6
6
|
|
|
7
|
-
**5,
|
|
7
|
+
**5,391 bytes gzipped**, measured by `scripts/size-gate.test.mjs` against
|
|
8
8
|
`scripts/fixtures/size/tools-only.ts`. Optional — nothing in `@driftengine/core` imports it, so a
|
|
9
9
|
game that never asks for these pays nothing.
|
|
10
10
|
|
|
@@ -77,6 +77,28 @@ if (overlay.route(keyEvent(event.key, event.shiftKey, event.ctrlKey))) event.pre
|
|
|
77
77
|
While it is closed `frame` returns before it builds anything and `route` refuses every event but the
|
|
78
78
|
one that opens it, so a game that never presses the key pays for a boolean.
|
|
79
79
|
|
|
80
|
+
## Two adapters, because every consumer was about to write the same ones
|
|
81
|
+
|
|
82
|
+
A panel needs its world in the shape the panel reads. Two of those shapes are the same in every
|
|
83
|
+
game that has the thing behind them, so they are here rather than in each of them.
|
|
84
|
+
|
|
85
|
+
`entitiesInspectable(world, types)` turns an entity world into an `InspectableWorld`: the inspector
|
|
86
|
+
addresses a component by name because that is what a row carries, and a world addresses it by the
|
|
87
|
+
type object because that is what indexes its stores. A name with no type behind it does nothing
|
|
88
|
+
rather than guessing, so a row built from a stale schema cannot write a field into a component the
|
|
89
|
+
world does not have and report success.
|
|
90
|
+
|
|
91
|
+
`createSessionRecorder` plus `observeSession` and `sessionReadout` turn a lockstep session into a
|
|
92
|
+
`NetworkReadout`. The watching is the part worth having written once: `session.desync` is **latched
|
|
93
|
+
rather than an event**, so it keeps answering with the same disagreement every frame until the next
|
|
94
|
+
one arrives, and a recorder that appended what it read would turn one divergence into sixty a
|
|
95
|
+
second. `snapshotBytes` is the caller's, because `RewindLoop` is generic over the state it
|
|
96
|
+
snapshots and cannot know the size of one.
|
|
97
|
+
|
|
98
|
+
**Both are typed structurally**, the way `@driftengine/drft` takes DTEX types that
|
|
99
|
+
`@driftengine/texture` satisfies, so this package depends on nothing new and anything shaped right
|
|
100
|
+
can be read — including a consumer whose state is not an entity world at all.
|
|
101
|
+
|
|
80
102
|
## Building it in or leaving it out
|
|
81
103
|
|
|
82
104
|
Import it and it is in the bundle; do not and it is not. There is no flag, because a flag would be a
|
package/dist/index.d.ts
CHANGED
|
@@ -25,12 +25,14 @@ export type { Selection } from './selection.ts';
|
|
|
25
25
|
export { addToSelection, clearSelection, createSelection, isSelected, primarySelection, selectOnly, selectRange, selectedEntities, toggleSelection, } from './selection.ts';
|
|
26
26
|
export { divergenceIsUnaccounted, divergentComponents } from './divergence.ts';
|
|
27
27
|
export type { InspectableWorld, InspectorRow, InspectorView, InspectorWorld, RowKind, } from './inspector.ts';
|
|
28
|
-
export {
|
|
28
|
+
export type { EntityWorldLike, InspectableComponentType } from './inspector.ts';
|
|
29
|
+
export { MIXED, addComponentCommand, createInspectorView, entitiesInspectable, fieldEditable, inspectorPanel, inspectorRows, removeComponentCommand, rowText, setFieldCommand, } from './inspector.ts';
|
|
29
30
|
export type { ConsoleView, ConsoleWorld, LogEntry, LogFilter, LogRing, Severity, SourceCursor, } from './console.ts';
|
|
30
31
|
export { DISPLAY_LIMIT, appendLog, consolePanel, createConsoleView, createLogFilter, createLogRing, displayText, filteredEntries, jumpToSourceCommand, logEntries, } from './console.ts';
|
|
31
32
|
export type { FrameHistory, ProfilerRow, ProfilerView, ProfilerWorld } from './profiler.ts';
|
|
32
33
|
export { createFrameHistory, createGpuPassTimings, createProfilerView, formatMs, historyValues, occupancy, profilerPanel, profilerRows, pushFrame, recordGpuSample, } from './profiler.ts';
|
|
33
34
|
export type { NetworkReadout, NetworkView, NetworkWorld, RollbackHistory } from './network.ts';
|
|
34
|
-
export {
|
|
35
|
+
export type { SessionLike, SessionRecorder } from './network.ts';
|
|
36
|
+
export { componentDivergence, createNetworkView, createRollbackHistory, createSessionRecorder, networkPanel, observeSession, pushRollback, rollbackValues, sessionReadout, } from './network.ts';
|
|
35
37
|
export type { OverlayPainter, OverlaySite, PanelBinding, ToolsOverlay, ToolsOverlayOptions, } from './overlay.ts';
|
|
36
38
|
export { bindPanel, createToolsOverlay, paintOverlay } from './overlay.ts';
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ export { createUndoStack } from './command.js';
|
|
|
3
3
|
export { PANEL_PREFIX, createPanelRoot, emptyPanel, keyEvent, panelIdOf, pointerEvent, treeShape, wheelEvent, } from './panel.js';
|
|
4
4
|
export { addToSelection, clearSelection, createSelection, isSelected, primarySelection, selectOnly, selectRange, selectedEntities, toggleSelection, } from './selection.js';
|
|
5
5
|
export { divergenceIsUnaccounted, divergentComponents } from './divergence.js';
|
|
6
|
-
export { MIXED, addComponentCommand, createInspectorView, fieldEditable, inspectorPanel, inspectorRows, removeComponentCommand, rowText, setFieldCommand, } from './inspector.js';
|
|
6
|
+
export { MIXED, addComponentCommand, createInspectorView, entitiesInspectable, fieldEditable, inspectorPanel, inspectorRows, removeComponentCommand, rowText, setFieldCommand, } from './inspector.js';
|
|
7
7
|
export { DISPLAY_LIMIT, appendLog, consolePanel, createConsoleView, createLogFilter, createLogRing, displayText, filteredEntries, jumpToSourceCommand, logEntries, } from './console.js';
|
|
8
8
|
export { createFrameHistory, createGpuPassTimings, createProfilerView, formatMs, historyValues, occupancy, profilerPanel, profilerRows, pushFrame, recordGpuSample, } from './profiler.js';
|
|
9
|
-
export { componentDivergence, createNetworkView, createRollbackHistory, networkPanel, pushRollback, rollbackValues, } from './network.js';
|
|
9
|
+
export { componentDivergence, createNetworkView, createRollbackHistory, createSessionRecorder, networkPanel, observeSession, pushRollback, rollbackValues, sessionReadout, } from './network.js';
|
|
10
10
|
export { bindPanel, createToolsOverlay, paintOverlay } from './overlay.js';
|
package/dist/inspector.d.ts
CHANGED
|
@@ -84,3 +84,45 @@ export declare function createInspectorView(options: {
|
|
|
84
84
|
/** What a row shows to the right of its label. `—` is the mark for a disagreement. */
|
|
85
85
|
export declare function rowText(row: InspectorRow): string;
|
|
86
86
|
export declare const inspectorPanel: Panel<InspectorWorld, InspectorView>;
|
|
87
|
+
/**
|
|
88
|
+
* The shape of a component type, as this package needs it.
|
|
89
|
+
*
|
|
90
|
+
* **Structural rather than imported.** `@driftengine/entities`' `ComponentType` satisfies it, and
|
|
91
|
+
* so does anything else a consumer describes its state with — which is the arrangement
|
|
92
|
+
* `@driftengine/drft` already has with `@driftengine/texture`'s DTEX types across a boundary the
|
|
93
|
+
* one package must not depend across. This package gains no dependency for the adapter below, so
|
|
94
|
+
* the release's place count does not move.
|
|
95
|
+
*/
|
|
96
|
+
export interface InspectableComponentType {
|
|
97
|
+
readonly name: string;
|
|
98
|
+
readonly schema: {
|
|
99
|
+
readonly fields: readonly {
|
|
100
|
+
readonly name: string;
|
|
101
|
+
readonly type: string;
|
|
102
|
+
}[];
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** The five methods the adapter forwards to. `World` has all of them. */
|
|
106
|
+
export interface EntityWorldLike<T> {
|
|
107
|
+
has(entity: number, type: T): boolean;
|
|
108
|
+
add(entity: number, type: T): void;
|
|
109
|
+
remove(entity: number, type: T): boolean;
|
|
110
|
+
read(entity: number, type: T, field: string): unknown;
|
|
111
|
+
write(entity: number, type: T, field: string, value: unknown): void;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* An entity world, as something the inspector can read and edit.
|
|
115
|
+
*
|
|
116
|
+
* **Written once here because every consumer with a world was about to write it.** The inspector
|
|
117
|
+
* addresses a component by name, since a name is what a row carries; a world addresses it by the
|
|
118
|
+
* type object, since that is what indexes its stores. Bridging the two is seven forwarding methods
|
|
119
|
+
* and a lookup, identical wherever it is written.
|
|
120
|
+
*
|
|
121
|
+
* **A name with no type behind it does nothing rather than guessing.** A row built against a stale
|
|
122
|
+
* schema would otherwise write a field into a component the world does not have and report
|
|
123
|
+
* success, which is a silent loss of the edit a person just made and watched apply.
|
|
124
|
+
*
|
|
125
|
+
* `componentsOf` answers in the order the types were given, so a panel's rows do not reorder
|
|
126
|
+
* between frames.
|
|
127
|
+
*/
|
|
128
|
+
export declare function entitiesInspectable<T extends InspectableComponentType>(world: EntityWorldLike<T>, types: readonly T[]): InspectableWorld;
|
package/dist/inspector.js
CHANGED
|
@@ -263,3 +263,58 @@ export const inspectorPanel = {
|
|
|
263
263
|
return null;
|
|
264
264
|
},
|
|
265
265
|
};
|
|
266
|
+
/**
|
|
267
|
+
* An entity world, as something the inspector can read and edit.
|
|
268
|
+
*
|
|
269
|
+
* **Written once here because every consumer with a world was about to write it.** The inspector
|
|
270
|
+
* addresses a component by name, since a name is what a row carries; a world addresses it by the
|
|
271
|
+
* type object, since that is what indexes its stores. Bridging the two is seven forwarding methods
|
|
272
|
+
* and a lookup, identical wherever it is written.
|
|
273
|
+
*
|
|
274
|
+
* **A name with no type behind it does nothing rather than guessing.** A row built against a stale
|
|
275
|
+
* schema would otherwise write a field into a component the world does not have and report
|
|
276
|
+
* success, which is a silent loss of the edit a person just made and watched apply.
|
|
277
|
+
*
|
|
278
|
+
* `componentsOf` answers in the order the types were given, so a panel's rows do not reorder
|
|
279
|
+
* between frames.
|
|
280
|
+
*/
|
|
281
|
+
export function entitiesInspectable(world, types) {
|
|
282
|
+
const byName = new Map();
|
|
283
|
+
for (const type of types)
|
|
284
|
+
byName.set(type.name, type);
|
|
285
|
+
return {
|
|
286
|
+
componentsOf(entity) {
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const type of types)
|
|
289
|
+
if (world.has(entity, type))
|
|
290
|
+
out.push(type.name);
|
|
291
|
+
return out;
|
|
292
|
+
},
|
|
293
|
+
schemaOf(component) {
|
|
294
|
+
return byName.get(component)?.schema.fields ?? [];
|
|
295
|
+
},
|
|
296
|
+
read(entity, component, field) {
|
|
297
|
+
const type = byName.get(component);
|
|
298
|
+
return type === undefined ? undefined : world.read(entity, type, field);
|
|
299
|
+
},
|
|
300
|
+
write(entity, component, field, value) {
|
|
301
|
+
const type = byName.get(component);
|
|
302
|
+
if (type !== undefined)
|
|
303
|
+
world.write(entity, type, field, value);
|
|
304
|
+
},
|
|
305
|
+
hasComponent(entity, component) {
|
|
306
|
+
const type = byName.get(component);
|
|
307
|
+
return type !== undefined && world.has(entity, type);
|
|
308
|
+
},
|
|
309
|
+
addComponent(entity, component) {
|
|
310
|
+
const type = byName.get(component);
|
|
311
|
+
if (type !== undefined)
|
|
312
|
+
world.add(entity, type);
|
|
313
|
+
},
|
|
314
|
+
removeComponent(entity, component) {
|
|
315
|
+
const type = byName.get(component);
|
|
316
|
+
if (type !== undefined)
|
|
317
|
+
world.remove(entity, type);
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
package/dist/network.d.ts
CHANGED
|
@@ -56,3 +56,45 @@ export declare function createNetworkView(options: {
|
|
|
56
56
|
historyLength?: number;
|
|
57
57
|
}): NetworkView;
|
|
58
58
|
export declare const networkPanel: Panel<NetworkWorld, NetworkView>;
|
|
59
|
+
/**
|
|
60
|
+
* A session, as this package needs it.
|
|
61
|
+
*
|
|
62
|
+
* Structural rather than imported, so nothing here loads `@driftengine/network` at run time and a
|
|
63
|
+
* consumer whose session is its own shape can still be read.
|
|
64
|
+
*/
|
|
65
|
+
export interface SessionLike {
|
|
66
|
+
readonly participants: number;
|
|
67
|
+
readonly inputDelay: number;
|
|
68
|
+
/** The disagreement the session is currently holding, or null. Latched, not an event. */
|
|
69
|
+
readonly desync: Desync | null;
|
|
70
|
+
readonly loop: {
|
|
71
|
+
readonly depth: number;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** What a recorder keeps: the disagreements it has seen, oldest first. */
|
|
75
|
+
export interface SessionRecorder {
|
|
76
|
+
readonly desyncs: Desync[];
|
|
77
|
+
readonly limit: number;
|
|
78
|
+
}
|
|
79
|
+
export declare function createSessionRecorder(limit?: number): SessionRecorder;
|
|
80
|
+
/**
|
|
81
|
+
* Watch a session for one frame.
|
|
82
|
+
*
|
|
83
|
+
* **`desync` is latched rather than an event**, which is the whole reason this is not one line at
|
|
84
|
+
* the call site. A session keeps answering with the same disagreement on every frame until the
|
|
85
|
+
* next one arrives, so appending what it reads turns one divergence into sixty a second and buries
|
|
86
|
+
* the tick somebody is looking for under copies of itself. A disagreement is new when its tick and
|
|
87
|
+
* peer differ from the last one kept.
|
|
88
|
+
*
|
|
89
|
+
* Bounded, because a session that desyncs every tick would otherwise grow an array forever behind
|
|
90
|
+
* a panel nobody has open.
|
|
91
|
+
*/
|
|
92
|
+
export declare function observeSession(recorder: SessionRecorder, session: SessionLike): void;
|
|
93
|
+
/**
|
|
94
|
+
* The session and what has been seen of it, as the panel's readout.
|
|
95
|
+
*
|
|
96
|
+
* **`snapshotBytes` is the caller's, because the engine cannot know it.** `RewindLoop` is generic
|
|
97
|
+
* over the state it snapshots, so its depth is knowable here and the size of one is not. A caller
|
|
98
|
+
* passing zero is saying it has not measured, which the panel renders as such.
|
|
99
|
+
*/
|
|
100
|
+
export declare function sessionReadout(session: SessionLike, recorder: SessionRecorder, snapshotBytes: number, componentHashes?: NetworkReadout['componentHashes']): NetworkReadout;
|
package/dist/network.js
CHANGED
|
@@ -118,3 +118,46 @@ export const networkPanel = {
|
|
|
118
118
|
return null;
|
|
119
119
|
},
|
|
120
120
|
};
|
|
121
|
+
export function createSessionRecorder(limit = 32) {
|
|
122
|
+
return { desyncs: [], limit: Math.max(1, limit) };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Watch a session for one frame.
|
|
126
|
+
*
|
|
127
|
+
* **`desync` is latched rather than an event**, which is the whole reason this is not one line at
|
|
128
|
+
* the call site. A session keeps answering with the same disagreement on every frame until the
|
|
129
|
+
* next one arrives, so appending what it reads turns one divergence into sixty a second and buries
|
|
130
|
+
* the tick somebody is looking for under copies of itself. A disagreement is new when its tick and
|
|
131
|
+
* peer differ from the last one kept.
|
|
132
|
+
*
|
|
133
|
+
* Bounded, because a session that desyncs every tick would otherwise grow an array forever behind
|
|
134
|
+
* a panel nobody has open.
|
|
135
|
+
*/
|
|
136
|
+
export function observeSession(recorder, session) {
|
|
137
|
+
const seen = session.desync;
|
|
138
|
+
if (seen === null)
|
|
139
|
+
return;
|
|
140
|
+
const last = recorder.desyncs[recorder.desyncs.length - 1];
|
|
141
|
+
if (last !== undefined && last.tick === seen.tick && last.peer === seen.peer)
|
|
142
|
+
return;
|
|
143
|
+
recorder.desyncs.push(seen);
|
|
144
|
+
if (recorder.desyncs.length > recorder.limit)
|
|
145
|
+
recorder.desyncs.shift();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The session and what has been seen of it, as the panel's readout.
|
|
149
|
+
*
|
|
150
|
+
* **`snapshotBytes` is the caller's, because the engine cannot know it.** `RewindLoop` is generic
|
|
151
|
+
* over the state it snapshots, so its depth is knowable here and the size of one is not. A caller
|
|
152
|
+
* passing zero is saying it has not measured, which the panel renders as such.
|
|
153
|
+
*/
|
|
154
|
+
export function sessionReadout(session, recorder, snapshotBytes, componentHashes = null) {
|
|
155
|
+
return {
|
|
156
|
+
inputDelay: session.inputDelay,
|
|
157
|
+
participants: session.participants,
|
|
158
|
+
snapshotBytes,
|
|
159
|
+
snapshotCount: session.loop.depth,
|
|
160
|
+
desyncs: recorder.desyncs,
|
|
161
|
+
componentHashes,
|
|
162
|
+
};
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/tools",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "The editor panels a shipped game can carry, and the command stack that makes their edits undoable",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@driftengine/core": "4.1.
|
|
31
|
-
"@driftengine/editor": "4.1.
|
|
32
|
-
"@driftengine/network": "4.1.
|
|
33
|
-
"@driftengine/texture": "4.1.
|
|
34
|
-
"@driftengine/ui2d": "4.1.
|
|
30
|
+
"@driftengine/core": "4.1.1",
|
|
31
|
+
"@driftengine/editor": "4.1.1",
|
|
32
|
+
"@driftengine/network": "4.1.1",
|
|
33
|
+
"@driftengine/texture": "4.1.1",
|
|
34
|
+
"@driftengine/ui2d": "4.1.1"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
37
37
|
"repository": {
|
package/src/index.ts
CHANGED
|
@@ -58,10 +58,12 @@ export type {
|
|
|
58
58
|
InspectorWorld,
|
|
59
59
|
RowKind,
|
|
60
60
|
} from './inspector.ts';
|
|
61
|
+
export type { EntityWorldLike, InspectableComponentType } from './inspector.ts';
|
|
61
62
|
export {
|
|
62
63
|
MIXED,
|
|
63
64
|
addComponentCommand,
|
|
64
65
|
createInspectorView,
|
|
66
|
+
entitiesInspectable,
|
|
65
67
|
fieldEditable,
|
|
66
68
|
inspectorPanel,
|
|
67
69
|
inspectorRows,
|
|
@@ -107,13 +109,17 @@ export {
|
|
|
107
109
|
} from './profiler.ts';
|
|
108
110
|
|
|
109
111
|
export type { NetworkReadout, NetworkView, NetworkWorld, RollbackHistory } from './network.ts';
|
|
112
|
+
export type { SessionLike, SessionRecorder } from './network.ts';
|
|
110
113
|
export {
|
|
111
114
|
componentDivergence,
|
|
112
115
|
createNetworkView,
|
|
113
116
|
createRollbackHistory,
|
|
117
|
+
createSessionRecorder,
|
|
114
118
|
networkPanel,
|
|
119
|
+
observeSession,
|
|
115
120
|
pushRollback,
|
|
116
121
|
rollbackValues,
|
|
122
|
+
sessionReadout,
|
|
117
123
|
} from './network.ts';
|
|
118
124
|
|
|
119
125
|
/*
|
package/src/inspector.ts
CHANGED
|
@@ -348,3 +348,80 @@ export const inspectorPanel: Panel<InspectorWorld, InspectorView> = {
|
|
|
348
348
|
return null;
|
|
349
349
|
},
|
|
350
350
|
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The shape of a component type, as this package needs it.
|
|
354
|
+
*
|
|
355
|
+
* **Structural rather than imported.** `@driftengine/entities`' `ComponentType` satisfies it, and
|
|
356
|
+
* so does anything else a consumer describes its state with — which is the arrangement
|
|
357
|
+
* `@driftengine/drft` already has with `@driftengine/texture`'s DTEX types across a boundary the
|
|
358
|
+
* one package must not depend across. This package gains no dependency for the adapter below, so
|
|
359
|
+
* the release's place count does not move.
|
|
360
|
+
*/
|
|
361
|
+
export interface InspectableComponentType {
|
|
362
|
+
readonly name: string;
|
|
363
|
+
readonly schema: { readonly fields: readonly { readonly name: string; readonly type: string }[] };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The five methods the adapter forwards to. `World` has all of them. */
|
|
367
|
+
export interface EntityWorldLike<T> {
|
|
368
|
+
has(entity: number, type: T): boolean;
|
|
369
|
+
add(entity: number, type: T): void;
|
|
370
|
+
remove(entity: number, type: T): boolean;
|
|
371
|
+
read(entity: number, type: T, field: string): unknown;
|
|
372
|
+
write(entity: number, type: T, field: string, value: unknown): void;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* An entity world, as something the inspector can read and edit.
|
|
377
|
+
*
|
|
378
|
+
* **Written once here because every consumer with a world was about to write it.** The inspector
|
|
379
|
+
* addresses a component by name, since a name is what a row carries; a world addresses it by the
|
|
380
|
+
* type object, since that is what indexes its stores. Bridging the two is seven forwarding methods
|
|
381
|
+
* and a lookup, identical wherever it is written.
|
|
382
|
+
*
|
|
383
|
+
* **A name with no type behind it does nothing rather than guessing.** A row built against a stale
|
|
384
|
+
* schema would otherwise write a field into a component the world does not have and report
|
|
385
|
+
* success, which is a silent loss of the edit a person just made and watched apply.
|
|
386
|
+
*
|
|
387
|
+
* `componentsOf` answers in the order the types were given, so a panel's rows do not reorder
|
|
388
|
+
* between frames.
|
|
389
|
+
*/
|
|
390
|
+
export function entitiesInspectable<T extends InspectableComponentType>(
|
|
391
|
+
world: EntityWorldLike<T>,
|
|
392
|
+
types: readonly T[],
|
|
393
|
+
): InspectableWorld {
|
|
394
|
+
const byName = new Map<string, T>();
|
|
395
|
+
for (const type of types) byName.set(type.name, type);
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
componentsOf(entity: number): readonly string[] {
|
|
399
|
+
const out: string[] = [];
|
|
400
|
+
for (const type of types) if (world.has(entity, type)) out.push(type.name);
|
|
401
|
+
return out;
|
|
402
|
+
},
|
|
403
|
+
schemaOf(component: string): readonly { readonly name: string; readonly type: string }[] {
|
|
404
|
+
return byName.get(component)?.schema.fields ?? [];
|
|
405
|
+
},
|
|
406
|
+
read(entity: number, component: string, field: string): unknown {
|
|
407
|
+
const type = byName.get(component);
|
|
408
|
+
return type === undefined ? undefined : world.read(entity, type, field);
|
|
409
|
+
},
|
|
410
|
+
write(entity: number, component: string, field: string, value: unknown): void {
|
|
411
|
+
const type = byName.get(component);
|
|
412
|
+
if (type !== undefined) world.write(entity, type, field, value);
|
|
413
|
+
},
|
|
414
|
+
hasComponent(entity: number, component: string): boolean {
|
|
415
|
+
const type = byName.get(component);
|
|
416
|
+
return type !== undefined && world.has(entity, type);
|
|
417
|
+
},
|
|
418
|
+
addComponent(entity: number, component: string): void {
|
|
419
|
+
const type = byName.get(component);
|
|
420
|
+
if (type !== undefined) world.add(entity, type);
|
|
421
|
+
},
|
|
422
|
+
removeComponent(entity: number, component: string): void {
|
|
423
|
+
const type = byName.get(component);
|
|
424
|
+
if (type !== undefined) world.remove(entity, type);
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
package/src/network.ts
CHANGED
|
@@ -183,3 +183,71 @@ export const networkPanel: Panel<NetworkWorld, NetworkView> = {
|
|
|
183
183
|
return null;
|
|
184
184
|
},
|
|
185
185
|
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A session, as this package needs it.
|
|
189
|
+
*
|
|
190
|
+
* Structural rather than imported, so nothing here loads `@driftengine/network` at run time and a
|
|
191
|
+
* consumer whose session is its own shape can still be read.
|
|
192
|
+
*/
|
|
193
|
+
export interface SessionLike {
|
|
194
|
+
readonly participants: number;
|
|
195
|
+
readonly inputDelay: number;
|
|
196
|
+
/** The disagreement the session is currently holding, or null. Latched, not an event. */
|
|
197
|
+
readonly desync: Desync | null;
|
|
198
|
+
readonly loop: { readonly depth: number };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** What a recorder keeps: the disagreements it has seen, oldest first. */
|
|
202
|
+
export interface SessionRecorder {
|
|
203
|
+
readonly desyncs: Desync[];
|
|
204
|
+
readonly limit: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createSessionRecorder(limit = 32): SessionRecorder {
|
|
208
|
+
return { desyncs: [], limit: Math.max(1, limit) };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Watch a session for one frame.
|
|
213
|
+
*
|
|
214
|
+
* **`desync` is latched rather than an event**, which is the whole reason this is not one line at
|
|
215
|
+
* the call site. A session keeps answering with the same disagreement on every frame until the
|
|
216
|
+
* next one arrives, so appending what it reads turns one divergence into sixty a second and buries
|
|
217
|
+
* the tick somebody is looking for under copies of itself. A disagreement is new when its tick and
|
|
218
|
+
* peer differ from the last one kept.
|
|
219
|
+
*
|
|
220
|
+
* Bounded, because a session that desyncs every tick would otherwise grow an array forever behind
|
|
221
|
+
* a panel nobody has open.
|
|
222
|
+
*/
|
|
223
|
+
export function observeSession(recorder: SessionRecorder, session: SessionLike): void {
|
|
224
|
+
const seen = session.desync;
|
|
225
|
+
if (seen === null) return;
|
|
226
|
+
const last = recorder.desyncs[recorder.desyncs.length - 1];
|
|
227
|
+
if (last !== undefined && last.tick === seen.tick && last.peer === seen.peer) return;
|
|
228
|
+
recorder.desyncs.push(seen);
|
|
229
|
+
if (recorder.desyncs.length > recorder.limit) recorder.desyncs.shift();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The session and what has been seen of it, as the panel's readout.
|
|
234
|
+
*
|
|
235
|
+
* **`snapshotBytes` is the caller's, because the engine cannot know it.** `RewindLoop` is generic
|
|
236
|
+
* over the state it snapshots, so its depth is knowable here and the size of one is not. A caller
|
|
237
|
+
* passing zero is saying it has not measured, which the panel renders as such.
|
|
238
|
+
*/
|
|
239
|
+
export function sessionReadout(
|
|
240
|
+
session: SessionLike,
|
|
241
|
+
recorder: SessionRecorder,
|
|
242
|
+
snapshotBytes: number,
|
|
243
|
+
componentHashes: NetworkReadout['componentHashes'] = null,
|
|
244
|
+
): NetworkReadout {
|
|
245
|
+
return {
|
|
246
|
+
inputDelay: session.inputDelay,
|
|
247
|
+
participants: session.participants,
|
|
248
|
+
snapshotBytes,
|
|
249
|
+
snapshotCount: session.loop.depth,
|
|
250
|
+
desyncs: recorder.desyncs,
|
|
251
|
+
componentHashes,
|
|
252
|
+
};
|
|
253
|
+
}
|