@typeonce/ritmo 0.7.0 → 0.9.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/README.md +3 -2
- package/dist/Clock.d.ts +19 -4
- package/dist/Clock.d.ts.map +1 -1
- package/dist/Clock.js +18 -2
- package/dist/Clock.js.map +1 -1
- package/dist/Command.d.ts +181 -40
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +31 -11
- package/dist/Command.js.map +1 -1
- package/dist/Condition.d.ts +12 -1
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Debug.d.ts +127 -13
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Descriptor.d.ts +61 -10
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +22 -10
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +22 -7
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js.map +1 -1
- package/dist/EntityScope.d.ts +20 -1
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +8 -2
- package/dist/EntityScope.js.map +1 -1
- package/dist/FixedLoop.d.ts +106 -0
- package/dist/FixedLoop.d.ts.map +1 -0
- package/dist/FixedLoop.js +97 -0
- package/dist/FixedLoop.js.map +1 -0
- package/dist/HostClock.d.ts +1 -1
- package/dist/HostClock.d.ts.map +1 -1
- package/dist/HostClock.js +2 -1
- package/dist/HostClock.js.map +1 -1
- package/dist/Machine.d.ts +59 -34
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +10 -3
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +100 -14
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +34 -5
- package/dist/Query.js.map +1 -1
- package/dist/Relation.d.ts +5 -14
- package/dist/Relation.d.ts.map +1 -1
- package/dist/Relation.js +0 -32
- package/dist/Relation.js.map +1 -1
- package/dist/Runtime.d.ts +61 -15
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +594 -70
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +24 -15
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +2 -1
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +233 -48
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +33 -7
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +90 -1
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +42 -1
- package/dist/Snapshot.js.map +1 -1
- package/dist/System.d.ts +132 -31
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +32 -0
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +2 -0
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +34 -8
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +10 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +171 -12
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/equal.d.ts +8 -0
- package/dist/internal/equal.d.ts.map +1 -0
- package/dist/internal/equal.js +38 -0
- package/dist/internal/equal.js.map +1 -0
- package/dist/internal/fragments.d.ts +2 -6
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +36 -3
- package/dist/internal/fragments.js.map +1 -1
- package/dist/internal/game.d.ts +2 -0
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +48 -7
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/order.d.ts.map +1 -1
- package/dist/internal/order.js +4 -2
- package/dist/internal/order.js.map +1 -1
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +57 -26
- package/dist/internal/queries.js.map +1 -1
- package/dist/internal/relations.d.ts +10 -0
- package/dist/internal/relations.d.ts.map +1 -0
- package/dist/internal/relations.js +32 -0
- package/dist/internal/relations.js.map +1 -0
- package/dist/internal/requirements.d.ts +26 -0
- package/dist/internal/requirements.d.ts.map +1 -0
- package/dist/internal/requirements.js +46 -0
- package/dist/internal/requirements.js.map +1 -0
- package/dist/internal/streams.d.ts +2 -0
- package/dist/internal/streams.d.ts.map +1 -1
- package/dist/internal/streams.js +10 -0
- package/dist/internal/streams.js.map +1 -1
- package/dist/internal/world.d.ts +7 -1
- package/dist/internal/world.d.ts.map +1 -1
- package/dist/internal/world.js +53 -3
- package/dist/internal/world.js.map +1 -1
- package/package.json +1 -1
- package/src/Clock.ts +29 -4
- package/src/Command.ts +281 -67
- package/src/Condition.ts +17 -1
- package/src/Debug.ts +124 -15
- package/src/Descriptor.ts +92 -21
- package/src/Entity.ts +23 -7
- package/src/EntityScope.ts +21 -2
- package/src/FixedLoop.ts +218 -0
- package/src/HostClock.ts +2 -1
- package/src/Machine.ts +72 -35
- package/src/Query.ts +118 -17
- package/src/Relation.ts +5 -67
- package/src/Runtime.ts +659 -85
- package/src/Schedule.ts +31 -19
- package/src/Schema.ts +297 -57
- package/src/Snapshot.ts +94 -2
- package/src/System.ts +151 -29
- package/src/index.ts +4 -0
- package/src/internal/cells.ts +42 -10
- package/src/internal/debug.ts +179 -14
- package/src/internal/equal.ts +31 -0
- package/src/internal/fragments.ts +35 -5
- package/src/internal/game.ts +69 -7
- package/src/internal/order.ts +8 -4
- package/src/internal/queries.ts +62 -12
- package/src/internal/relations.ts +52 -0
- package/src/internal/requirements.ts +71 -0
- package/src/internal/streams.ts +11 -0
- package/src/internal/world.ts +60 -4
package/src/internal/queries.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Matches are always returned in ascending entity id order, which is spawn
|
|
15
15
|
* order, so iteration is deterministic.
|
|
16
16
|
*/
|
|
17
|
+
import type { Descriptor } from "../Descriptor.ts"
|
|
17
18
|
import * as DescriptorModule from "../Descriptor.ts"
|
|
18
19
|
import type * as Entity from "../Entity.ts"
|
|
19
20
|
import * as Query from "../Query.ts"
|
|
@@ -46,6 +47,9 @@ interface LifecycleFilterPlan {
|
|
|
46
47
|
readonly ordinal: number
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** A query filter: one lifecycle filter, or `anyOf` several (passes when one does). */
|
|
51
|
+
type FilterPlan = ReadonlyArray<LifecycleFilterPlan>
|
|
52
|
+
|
|
49
53
|
interface QueryState {
|
|
50
54
|
readonly ordinal: number
|
|
51
55
|
readonly required: ReadonlyArray<number>
|
|
@@ -54,7 +58,7 @@ interface QueryState {
|
|
|
54
58
|
readonly withoutRelations: ReadonlyArray<Relation.Relation.Any>
|
|
55
59
|
readonly withRelated: ReadonlyArray<Relation.Relation.Any>
|
|
56
60
|
readonly withoutRelated: ReadonlyArray<Relation.Relation.Any>
|
|
57
|
-
readonly filters: ReadonlyArray<
|
|
61
|
+
readonly filters: ReadonlyArray<FilterPlan>
|
|
58
62
|
readonly slots: ReadonlyArray<SlotPlan>
|
|
59
63
|
readonly proofSlots: ReadonlyArray<readonly [string, number]>
|
|
60
64
|
readonly writeSlots: ReadonlyArray<readonly [string, number]>
|
|
@@ -121,6 +125,31 @@ export const makeQueryEngine = (world: World) => {
|
|
|
121
125
|
const states = new WeakMap<AnyQuery, QueryState>()
|
|
122
126
|
let nextOrdinal = 0
|
|
123
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The ordinal of a selected component, checking that the descriptor the
|
|
130
|
+
* query names behaves like the one the schema registers under that name:
|
|
131
|
+
* identity is (kind, name), so a look-alike built elsewhere must not add
|
|
132
|
+
* or drop behaviour (entity-state transitions, a constructor, immutability,
|
|
133
|
+
* transience) the schema does not declare.
|
|
134
|
+
*/
|
|
135
|
+
const registeredOrdinal = (descriptor: Descriptor.Any): number => {
|
|
136
|
+
const ordinal = world.ordinalOf(descriptor as never)
|
|
137
|
+
const registered = world.descriptorAt(ordinal) as Descriptor.Any
|
|
138
|
+
if (registered === descriptor) return ordinal
|
|
139
|
+
const traits = (candidate: Descriptor.Any) => [
|
|
140
|
+
DescriptorModule.isEntityState(candidate as never) ? "an entity state" : undefined,
|
|
141
|
+
DescriptorModule.hasConstructor(candidate) ? "constructed" : undefined,
|
|
142
|
+
DescriptorModule.isImmutable(candidate) ? "immutable" : undefined,
|
|
143
|
+
DescriptorModule.isTransient(candidate) ? "transient" : undefined
|
|
144
|
+
].filter((trait) => trait !== undefined).join(", ") || "plain"
|
|
145
|
+
const expected = traits(registered)
|
|
146
|
+
const actual = traits(descriptor)
|
|
147
|
+
if (expected !== actual) {
|
|
148
|
+
throw new Error(`${descriptor.name} is ${expected} in this schema, but this query names a descriptor that is ${actual}: use the descriptor the schema registers`)
|
|
149
|
+
}
|
|
150
|
+
return ordinal
|
|
151
|
+
}
|
|
152
|
+
|
|
124
153
|
const compile = (query: AnyQuery): QueryState => {
|
|
125
154
|
const required: Array<number> = query.with.map((descriptor) => world.ordinalOf(descriptor))
|
|
126
155
|
const withRelations: Array<Relation.Relation.Any> = [...query.withRelations]
|
|
@@ -133,7 +162,7 @@ export const makeQueryEngine = (world: World) => {
|
|
|
133
162
|
switch (access.mode) {
|
|
134
163
|
case "read":
|
|
135
164
|
case "write": {
|
|
136
|
-
const ordinal =
|
|
165
|
+
const ordinal = registeredOrdinal(access.descriptor)
|
|
137
166
|
required.push(ordinal)
|
|
138
167
|
proofSlots.push([slot, ordinal])
|
|
139
168
|
if (access.mode === "write") {
|
|
@@ -151,7 +180,7 @@ export const makeQueryEngine = (world: World) => {
|
|
|
151
180
|
break
|
|
152
181
|
}
|
|
153
182
|
case "optional":
|
|
154
|
-
slots.push({ slot, mode: "optional", ordinal:
|
|
183
|
+
slots.push({ slot, mode: "optional", ordinal: registeredOrdinal(access.descriptor) })
|
|
155
184
|
break
|
|
156
185
|
case "readRelation":
|
|
157
186
|
withRelations.push(access.descriptor)
|
|
@@ -185,11 +214,13 @@ export const makeQueryEngine = (world: World) => {
|
|
|
185
214
|
withoutRelations: query.withoutRelations,
|
|
186
215
|
withRelated: unique(withRelated),
|
|
187
216
|
withoutRelated: query.withoutRelated,
|
|
188
|
-
filters: query.filters.map((filter) =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
filters: query.filters.map((filter) =>
|
|
218
|
+
(filter.kind === "anyOf" ? filter.filters : [filter]).map((member) => {
|
|
219
|
+
const ordinal = world.ordinalOf(member.descriptor)
|
|
220
|
+
world.trackChanges(ordinal)
|
|
221
|
+
return { kind: member.kind, ordinal }
|
|
222
|
+
})
|
|
223
|
+
),
|
|
193
224
|
slots,
|
|
194
225
|
proofSlots,
|
|
195
226
|
writeSlots,
|
|
@@ -239,14 +270,33 @@ export const makeQueryEngine = (world: World) => {
|
|
|
239
270
|
|
|
240
271
|
const matchesFilters = (state: QueryState, record: EntityRecord, since: number): boolean => {
|
|
241
272
|
for (const filter of state.filters) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
273
|
+
let matched = false
|
|
274
|
+
for (const member of filter) {
|
|
275
|
+
matched = member.kind === "added"
|
|
276
|
+
? world.addedSince(record, member.ordinal, since)
|
|
277
|
+
: world.changedSince(record, member.ordinal, since)
|
|
278
|
+
if (matched) break
|
|
279
|
+
}
|
|
245
280
|
if (!matched) return false
|
|
246
281
|
}
|
|
247
282
|
return true
|
|
248
283
|
}
|
|
249
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Ids that may pass one filter (may repeat), or `undefined` when a change
|
|
287
|
+
* log no longer covers `since`. An `anyOf` is the union of its members' logs.
|
|
288
|
+
*/
|
|
289
|
+
const filterCandidates = (filter: FilterPlan, since: number): ReadonlyArray<number> | undefined => {
|
|
290
|
+
if (filter.length === 1) return world.changedCandidates(filter[0]!.ordinal, since)
|
|
291
|
+
const union: Array<number> = []
|
|
292
|
+
for (const member of filter) {
|
|
293
|
+
const logged = world.changedCandidates(member.ordinal, since)
|
|
294
|
+
if (logged === undefined) return undefined
|
|
295
|
+
for (const id of logged) union.push(id)
|
|
296
|
+
}
|
|
297
|
+
return union
|
|
298
|
+
}
|
|
299
|
+
|
|
250
300
|
const relatedIds = (relation: Relation.Relation.Any, id: number): ReadonlyArray<Entity.EntityId<any, any>> =>
|
|
251
301
|
world.relatedSourceIds(relation, id).map(world.entityIdOf)
|
|
252
302
|
|
|
@@ -426,7 +476,7 @@ export const makeQueryEngine = (world: World) => {
|
|
|
426
476
|
// bounds the candidates for either filter.
|
|
427
477
|
let candidates: ReadonlyArray<number> | undefined
|
|
428
478
|
for (const filter of state.filters) {
|
|
429
|
-
const logged =
|
|
479
|
+
const logged = filterCandidates(filter, since)
|
|
430
480
|
if (logged === undefined) continue
|
|
431
481
|
if (logged.length === 0) return []
|
|
432
482
|
if (candidates === undefined || logged.length < candidates.length) candidates = logged
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds relation pairs for `Descriptor.Hierarchy` and `Descriptor.Relation`,
|
|
3
|
+
* the one public way to define a relation.
|
|
4
|
+
*/
|
|
5
|
+
import type { PairBrand, RelatedDefinition, RelationDefinition, RelationKind } from "../Relation.ts"
|
|
6
|
+
|
|
7
|
+
const relationTypeId = "~ritmo/Relation"
|
|
8
|
+
|
|
9
|
+
export const makePair = <
|
|
10
|
+
const Name extends string,
|
|
11
|
+
const RelatedName extends string,
|
|
12
|
+
const Kind extends RelationKind
|
|
13
|
+
>(
|
|
14
|
+
kind: Kind,
|
|
15
|
+
name: Name,
|
|
16
|
+
relatedName: RelatedName
|
|
17
|
+
): {
|
|
18
|
+
readonly relation: RelationDefinition<Name, RelatedName, Kind, PairBrand<Name, RelatedName, Kind>>
|
|
19
|
+
readonly related: RelatedDefinition<RelatedName, Name, Kind, PairBrand<Name, RelatedName, Kind>>
|
|
20
|
+
} => {
|
|
21
|
+
type Pair = PairBrand<Name, RelatedName, Kind>
|
|
22
|
+
const related = {
|
|
23
|
+
kind: "related",
|
|
24
|
+
relatedName,
|
|
25
|
+
relationName: name,
|
|
26
|
+
key: Symbol.for(`ritmo/related/${name}`),
|
|
27
|
+
relationKind: kind,
|
|
28
|
+
[relationTypeId]: {
|
|
29
|
+
_Pair: (_: never) => undefined as unknown as Pair
|
|
30
|
+
}
|
|
31
|
+
} as RelatedDefinition<RelatedName, Name, Kind, Pair>
|
|
32
|
+
|
|
33
|
+
const relation = {
|
|
34
|
+
kind: "relation",
|
|
35
|
+
name,
|
|
36
|
+
relatedName,
|
|
37
|
+
key: Symbol.for(`ritmo/relation/${name}`),
|
|
38
|
+
relationKind: kind,
|
|
39
|
+
linkedDespawn: kind === "hierarchy",
|
|
40
|
+
allowSelf: false,
|
|
41
|
+
ordered: kind === "hierarchy",
|
|
42
|
+
related,
|
|
43
|
+
[relationTypeId]: {
|
|
44
|
+
_Pair: (_: never) => undefined as unknown as Pair
|
|
45
|
+
}
|
|
46
|
+
} as RelationDefinition<Name, RelatedName, Kind, Pair>
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
relation,
|
|
50
|
+
related
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime side of `Schema.requires`: the requirement index built once per
|
|
3
|
+
* bound schema, used to expand `Game.System.spawns(...)`, to check commands
|
|
4
|
+
* when they are applied, and to check restored entities.
|
|
5
|
+
*
|
|
6
|
+
* The compile-time counterpart lives in `Command.ts` (spawn, insert, remove)
|
|
7
|
+
* and `Schema.ts` (`ValidateRequires`).
|
|
8
|
+
*/
|
|
9
|
+
import type { Descriptor } from "../Descriptor.ts"
|
|
10
|
+
import type { Schema } from "../Schema.ts"
|
|
11
|
+
|
|
12
|
+
type ComponentDescriptor = Descriptor<"component", string, any>
|
|
13
|
+
|
|
14
|
+
export interface RequirementIndex {
|
|
15
|
+
/** Whether the schema declares any requirement. */
|
|
16
|
+
readonly empty: boolean
|
|
17
|
+
/** Component name to the components it directly requires. */
|
|
18
|
+
readonly requires: ReadonlyMap<string, ReadonlyArray<ComponentDescriptor>>
|
|
19
|
+
/** Component name to the components that directly require it. */
|
|
20
|
+
readonly requiredBy: ReadonlyMap<string, ReadonlyArray<ComponentDescriptor>>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const push = (map: Map<string, Array<ComponentDescriptor>>, key: string, value: ComponentDescriptor): void => {
|
|
24
|
+
const list = map.get(key)
|
|
25
|
+
if (list === undefined) map.set(key, [value])
|
|
26
|
+
else if (!list.some((entry) => entry.name === value.name)) list.push(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const requirementIndex = (schema: Schema.Any): RequirementIndex => {
|
|
30
|
+
const requires = new Map<string, Array<ComponentDescriptor>>()
|
|
31
|
+
const requiredBy = new Map<string, Array<ComponentDescriptor>>()
|
|
32
|
+
for (const requirement of schema.requires) {
|
|
33
|
+
for (const required of requirement.required) {
|
|
34
|
+
push(requires, requirement.component.name, required)
|
|
35
|
+
push(requiredBy, required.name, requirement.component)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { empty: requires.size === 0, requires, requiredBy }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `components` and everything they require, transitively, without duplicates. */
|
|
42
|
+
export const withRequired = (
|
|
43
|
+
index: RequirementIndex,
|
|
44
|
+
components: ReadonlyArray<ComponentDescriptor>
|
|
45
|
+
): ReadonlyArray<ComponentDescriptor> => {
|
|
46
|
+
if (index.empty) return components
|
|
47
|
+
const result: Array<ComponentDescriptor> = []
|
|
48
|
+
const seen = new Set<string>()
|
|
49
|
+
const visit = (descriptor: ComponentDescriptor): void => {
|
|
50
|
+
if (seen.has(descriptor.name)) return
|
|
51
|
+
seen.add(descriptor.name)
|
|
52
|
+
result.push(descriptor)
|
|
53
|
+
for (const required of index.requires.get(descriptor.name) ?? []) visit(required)
|
|
54
|
+
}
|
|
55
|
+
components.forEach(visit)
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The first requirement `held` breaks, as `[requirer, missing]`, or `undefined`. */
|
|
60
|
+
export const firstMissing = (
|
|
61
|
+
index: RequirementIndex,
|
|
62
|
+
held: (name: string) => boolean,
|
|
63
|
+
names: Iterable<string>
|
|
64
|
+
): readonly [string, string] | undefined => {
|
|
65
|
+
for (const name of names) {
|
|
66
|
+
for (const required of index.requires.get(name) ?? []) {
|
|
67
|
+
if (!held(required.name)) return [name, required.name]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
package/src/internal/streams.ts
CHANGED
|
@@ -59,6 +59,8 @@ export interface Streams<T> {
|
|
|
59
59
|
* reader has read, then enforces the capacity.
|
|
60
60
|
*/
|
|
61
61
|
trim(windowBoundary: number): void
|
|
62
|
+
/** Drops every batch stamped after `tick` (what a rolled-back phase published). */
|
|
63
|
+
retract(tick: number): void
|
|
62
64
|
/** Clears simulation messages; unread host messages survive world restore. */
|
|
63
65
|
clear(): void
|
|
64
66
|
/** Every key with retained entries or registered readers. */
|
|
@@ -151,6 +153,15 @@ export const make = <T>(capacity: number): Streams<T> => {
|
|
|
151
153
|
dropFront(log, overflow)
|
|
152
154
|
}
|
|
153
155
|
},
|
|
156
|
+
retract(tick) {
|
|
157
|
+
for (const log of logs.values()) {
|
|
158
|
+
const start = firstAfter(log.ticks, tick)
|
|
159
|
+
for (let index = start; index < log.batches.length; index++) log.size -= log.batches[index]!.length
|
|
160
|
+
log.ticks.length = start
|
|
161
|
+
log.batches.length = start
|
|
162
|
+
log.surviveRestore.length = start
|
|
163
|
+
}
|
|
164
|
+
},
|
|
154
165
|
clear() {
|
|
155
166
|
for (const [key, log] of logs) {
|
|
156
167
|
const ticks: Array<number> = []
|
package/src/internal/world.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface EntityRecord {
|
|
|
48
48
|
* Cached query matches by query ordinal, reused while the entity matches.
|
|
49
49
|
*/
|
|
50
50
|
readonly matches: Array<unknown>
|
|
51
|
+
/** The name of the component at an ordinal, for error messages (shared by the world's records). */
|
|
52
|
+
readonly nameAt: (ordinal: number) => string
|
|
51
53
|
alive: boolean
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -227,6 +229,16 @@ export const makeWorld = <S extends Schema.Any>(schema: S, lifecycleCapacity: nu
|
|
|
227
229
|
const journalRecords: Array<EntityRecord> = []
|
|
228
230
|
const journalOrdinals: Array<number> = []
|
|
229
231
|
const journalValues: Array<unknown> = []
|
|
232
|
+
/**
|
|
233
|
+
* Journal of a phase: several system transactions undone together (a
|
|
234
|
+
* transition's exit schedules). Each committed write keeps its value and
|
|
235
|
+
* change tick from before it, so rollback restores both, newest first.
|
|
236
|
+
*/
|
|
237
|
+
let inPhase = false
|
|
238
|
+
const phaseRecords: Array<EntityRecord> = []
|
|
239
|
+
const phaseOrdinals: Array<number> = []
|
|
240
|
+
const phaseValues: Array<unknown> = []
|
|
241
|
+
const phaseChanged: Array<number> = []
|
|
230
242
|
|
|
231
243
|
/**
|
|
232
244
|
* Entity ownership used by scene and level lifetime scopes.
|
|
@@ -238,6 +250,8 @@ export const makeWorld = <S extends Schema.Any>(schema: S, lifecycleCapacity: nu
|
|
|
238
250
|
/** Suppresses per-component and relation hooks inside spawns and despawns. */
|
|
239
251
|
let hookDepth = 0
|
|
240
252
|
|
|
253
|
+
const nameAt = (ordinal: number): string => descriptors[ordinal]?.name ?? `component #${ordinal}`
|
|
254
|
+
|
|
241
255
|
const ordinalOf = (descriptor: ComponentDescriptor): number => {
|
|
242
256
|
const known = ordinals.get(descriptor.key)
|
|
243
257
|
if (known !== undefined) {
|
|
@@ -337,6 +351,7 @@ export const makeWorld = <S extends Schema.Any>(schema: S, lifecycleCapacity: nu
|
|
|
337
351
|
values,
|
|
338
352
|
marks,
|
|
339
353
|
matches: [],
|
|
354
|
+
nameAt,
|
|
340
355
|
alive: true
|
|
341
356
|
}
|
|
342
357
|
records.set(record.id, record)
|
|
@@ -363,14 +378,19 @@ export const makeWorld = <S extends Schema.Any>(schema: S, lifecycleCapacity: nu
|
|
|
363
378
|
/**
|
|
364
379
|
* Writes an existing component value in place and records the change.
|
|
365
380
|
*/
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
381
|
+
/**
|
|
382
|
+
* Writes a component the entity holds. Returns `false`, writing nothing,
|
|
383
|
+
* when it does not (despawned records hold nothing): only a stale query
|
|
384
|
+
* cell gets here, and it reports the misuse.
|
|
385
|
+
*/
|
|
386
|
+
const setComponentValue = (record: EntityRecord, ordinal: number, value: unknown): boolean => {
|
|
387
|
+
if (record.values[ordinal] === ABSENT) {
|
|
388
|
+
return false
|
|
369
389
|
}
|
|
370
390
|
if (!inTransaction) {
|
|
371
391
|
record.values[ordinal] = value
|
|
372
392
|
markChanged(record, ordinal)
|
|
373
|
-
return
|
|
393
|
+
return true
|
|
374
394
|
}
|
|
375
395
|
const slot = ordinal * MARK_STRIDE + JOURNALED
|
|
376
396
|
if (record.marks[slot] !== transaction) {
|
|
@@ -380,6 +400,7 @@ export const makeWorld = <S extends Schema.Any>(schema: S, lifecycleCapacity: nu
|
|
|
380
400
|
journalValues.push(record.values[ordinal])
|
|
381
401
|
}
|
|
382
402
|
record.values[ordinal] = value
|
|
403
|
+
return true
|
|
383
404
|
}
|
|
384
405
|
|
|
385
406
|
const clearJournal = (): void => {
|
|
@@ -403,6 +424,13 @@ export const makeWorld = <S extends Schema.Any>(schema: S, lifecycleCapacity: nu
|
|
|
403
424
|
inTransaction = false
|
|
404
425
|
for (let index = 0; index < journalRecords.length; index++) {
|
|
405
426
|
const record = journalRecords[index]!
|
|
427
|
+
if (inPhase) {
|
|
428
|
+
const ordinal = journalOrdinals[index]!
|
|
429
|
+
phaseRecords.push(record)
|
|
430
|
+
phaseOrdinals.push(ordinal)
|
|
431
|
+
phaseValues.push(journalValues[index])
|
|
432
|
+
phaseChanged.push(record.marks[ordinal * MARK_STRIDE + CHANGED]!)
|
|
433
|
+
}
|
|
406
434
|
if (record.alive) {
|
|
407
435
|
markChanged(record, journalOrdinals[index]!)
|
|
408
436
|
}
|
|
@@ -421,6 +449,30 @@ export const makeWorld = <S extends Schema.Any>(schema: S, lifecycleCapacity: nu
|
|
|
421
449
|
clearJournal()
|
|
422
450
|
}
|
|
423
451
|
|
|
452
|
+
const clearPhase = (): void => {
|
|
453
|
+
inPhase = false
|
|
454
|
+
phaseRecords.length = 0
|
|
455
|
+
phaseOrdinals.length = 0
|
|
456
|
+
phaseValues.length = 0
|
|
457
|
+
phaseChanged.length = 0
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Starts a phase: the transactions committed until it ends can be undone together. */
|
|
461
|
+
const beginPhase = (): void => {
|
|
462
|
+
inPhase = true
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Restores every write committed in the phase, with its change tick. */
|
|
466
|
+
const rollbackPhase = (): void => {
|
|
467
|
+
for (let index = phaseRecords.length - 1; index >= 0; index--) {
|
|
468
|
+
const record = phaseRecords[index]!
|
|
469
|
+
const ordinal = phaseOrdinals[index]!
|
|
470
|
+
record.values[ordinal] = phaseValues[index]
|
|
471
|
+
record.marks[ordinal * MARK_STRIDE + CHANGED] = phaseChanged[index]!
|
|
472
|
+
}
|
|
473
|
+
clearPhase()
|
|
474
|
+
}
|
|
475
|
+
|
|
424
476
|
const clearEntityScope = (id: number): void => {
|
|
425
477
|
const scopeKey = scopeByEntity.get(id)
|
|
426
478
|
if (scopeKey === undefined) {
|
|
@@ -779,6 +831,7 @@ export const makeWorld = <S extends Schema.Any>(schema: S, lifecycleCapacity: nu
|
|
|
779
831
|
records,
|
|
780
832
|
relationDefinitions,
|
|
781
833
|
ordinalOf,
|
|
834
|
+
nameAt,
|
|
782
835
|
has,
|
|
783
836
|
entityIdOf,
|
|
784
837
|
allocateEntity,
|
|
@@ -792,6 +845,9 @@ export const makeWorld = <S extends Schema.Any>(schema: S, lifecycleCapacity: nu
|
|
|
792
845
|
beginTransaction,
|
|
793
846
|
commitTransaction,
|
|
794
847
|
rollbackTransaction,
|
|
848
|
+
beginPhase,
|
|
849
|
+
commitPhase: clearPhase,
|
|
850
|
+
rollbackPhase,
|
|
795
851
|
tryRelate,
|
|
796
852
|
unrelate,
|
|
797
853
|
reorderChildren,
|