@forgeax/engine-ecs 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +278 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__fixtures__/field-shape-fixture.d.ts +16 -0
- package/dist/__fixtures__/field-shape-fixture.d.ts.map +1 -0
- package/dist/__tests__/__fixtures__/inspect-scripts.snapshot.d.ts +3 -0
- package/dist/__tests__/__fixtures__/inspect-scripts.snapshot.d.ts.map +1 -0
- package/dist/__tests__/archetype.unit.test.d.ts +2 -0
- package/dist/__tests__/archetype.unit.test.d.ts.map +1 -0
- package/dist/__tests__/array-handle-element.test-d.d.ts +2 -0
- package/dist/__tests__/array-handle-element.test-d.d.ts.map +1 -0
- package/dist/__tests__/buffer-array-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/buffer-array-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/buffer-field-input-widen.unit.test.d.ts +2 -0
- package/dist/__tests__/buffer-field-input-widen.unit.test.d.ts.map +1 -0
- package/dist/__tests__/buffer-pool.test-d.d.ts +2 -0
- package/dist/__tests__/buffer-pool.test-d.d.ts.map +1 -0
- package/dist/__tests__/change-detection.unit.test.d.ts +2 -0
- package/dist/__tests__/change-detection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/command-buffer.test.d.ts +2 -0
- package/dist/__tests__/command-buffer.test.d.ts.map +1 -0
- package/dist/__tests__/commands-deferred.unit.test.d.ts +2 -0
- package/dist/__tests__/commands-deferred.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-catalog.test.d.ts +2 -0
- package/dist/__tests__/component-catalog.test.d.ts.map +1 -0
- package/dist/__tests__/component-definition-identity.unit.test.d.ts +2 -0
- package/dist/__tests__/component-definition-identity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-nan-preflight.unit.test.d.ts +2 -0
- package/dist/__tests__/component-nan-preflight.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-schema.test-d.d.ts +2 -0
- package/dist/__tests__/component-schema.test-d.d.ts.map +1 -0
- package/dist/__tests__/component-schema.test.d.ts +2 -0
- package/dist/__tests__/component-schema.test.d.ts.map +1 -0
- package/dist/__tests__/define-component-options.test-d.d.ts +2 -0
- package/dist/__tests__/define-component-options.test-d.d.ts.map +1 -0
- package/dist/__tests__/deprecated-vocab-rejection.unit.test.d.ts +2 -0
- package/dist/__tests__/deprecated-vocab-rejection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/despawn-all.unit.test.d.ts +2 -0
- package/dist/__tests__/despawn-all.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-core-reduction.characterization.test.d.ts +2 -0
- package/dist/__tests__/ecs-core-reduction.characterization.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-core-reduction.perf.test.d.ts +2 -0
- package/dist/__tests__/ecs-core-reduction.perf.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-error-code-31.test-d.d.ts +2 -0
- package/dist/__tests__/ecs-error-code-31.test-d.d.ts.map +1 -0
- package/dist/__tests__/entity-disabling.unit.test.d.ts +2 -0
- package/dist/__tests__/entity-disabling.unit.test.d.ts.map +1 -0
- package/dist/__tests__/entity-handle-codec-parity.unit.test.d.ts +2 -0
- package/dist/__tests__/entity-handle-codec-parity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-conflict-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-conflict-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-shared-storage.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-shared-storage.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-portable-value.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-portable-value.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-projection.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-projection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-render-read-lease.test-d.d.ts +2 -0
- package/dist/__tests__/externalization-render-read-lease.test-d.d.ts.map +1 -0
- package/dist/__tests__/externalization-render-read-lease.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-render-read-lease.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-root-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-root-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/field-enum-labels-reflection.test.d.ts +2 -0
- package/dist/__tests__/field-enum-labels-reflection.test.d.ts.map +1 -0
- package/dist/__tests__/field-shape-fixture.test.d.ts +2 -0
- package/dist/__tests__/field-shape-fixture.test.d.ts.map +1 -0
- package/dist/__tests__/field-transient-reflection.test.d.ts +2 -0
- package/dist/__tests__/field-transient-reflection.test.d.ts.map +1 -0
- package/dist/__tests__/field-value-type.test-d.d.ts +2 -0
- package/dist/__tests__/field-value-type.test-d.d.ts.map +1 -0
- package/dist/__tests__/fixed-update.unit.test.d.ts +2 -0
- package/dist/__tests__/fixed-update.unit.test.d.ts.map +1 -0
- package/dist/__tests__/handle.test-d.d.ts +2 -0
- package/dist/__tests__/handle.test-d.d.ts.map +1 -0
- package/dist/__tests__/hierarchy.unit.test.d.ts +2 -0
- package/dist/__tests__/hierarchy.unit.test.d.ts.map +1 -0
- package/dist/__tests__/managed-array-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/managed-array-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/minimal-example.test-d.d.ts +2 -0
- package/dist/__tests__/minimal-example.test-d.d.ts.map +1 -0
- package/dist/__tests__/name-read-types.test-d.d.ts +2 -0
- package/dist/__tests__/name-read-types.test-d.d.ts.map +1 -0
- package/dist/__tests__/plugin-service.test.d.ts +2 -0
- package/dist/__tests__/plugin-service.test.d.ts.map +1 -0
- package/dist/__tests__/query-combinations.unit.test.d.ts +2 -0
- package/dist/__tests__/query-combinations.unit.test.d.ts.map +1 -0
- package/dist/__tests__/query-light-extract-flat-column-ratio.perf.test.d.ts +2 -0
- package/dist/__tests__/query-light-extract-flat-column-ratio.perf.test.d.ts.map +1 -0
- package/dist/__tests__/query-storage-trends.perf.test.d.ts +2 -0
- package/dist/__tests__/query-storage-trends.perf.test.d.ts.map +1 -0
- package/dist/__tests__/query-trs-flat-column-ratio.perf.test.d.ts +2 -0
- package/dist/__tests__/query-trs-flat-column-ratio.perf.test.d.ts.map +1 -0
- package/dist/__tests__/query.unit.test.d.ts +2 -0
- package/dist/__tests__/query.unit.test.d.ts.map +1 -0
- package/dist/__tests__/relationship-index.perf.test.d.ts +2 -0
- package/dist/__tests__/relationship-index.perf.test.d.ts.map +1 -0
- package/dist/__tests__/relationship-index.test.d.ts +2 -0
- package/dist/__tests__/relationship-index.test.d.ts.map +1 -0
- package/dist/__tests__/scalar-field-type-owner.test-d.d.ts +2 -0
- package/dist/__tests__/scalar-field-type-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/schedule-determinism.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-determinism.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-inspection.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-inspection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-scope-error.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-scope-error.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-token.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-token.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schema-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/schema-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/serialization.unit.test.d.ts +2 -0
- package/dist/__tests__/serialization.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shared-kernel-eligibility.test-d.d.ts +2 -0
- package/dist/__tests__/shared-kernel-eligibility.test-d.d.ts.map +1 -0
- package/dist/__tests__/shared-kernel-eligibility.unit.test.d.ts +2 -0
- package/dist/__tests__/shared-kernel-eligibility.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shared-ref-store.unit.test.d.ts +2 -0
- package/dist/__tests__/shared-ref-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sparse-tag.unit.test.d.ts +2 -0
- package/dist/__tests__/sparse-tag.unit.test.d.ts.map +1 -0
- package/dist/__tests__/spawn-field-error.test-d.d.ts +2 -0
- package/dist/__tests__/spawn-field-error.test-d.d.ts.map +1 -0
- package/dist/__tests__/stale-error.test-d.d.ts +2 -0
- package/dist/__tests__/stale-error.test-d.d.ts.map +1 -0
- package/dist/__tests__/string-view-symmetry.test-d.d.ts +2 -0
- package/dist/__tests__/string-view-symmetry.test-d.d.ts.map +1 -0
- package/dist/__tests__/string-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/string-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/structure-epoch-boundaries.unit.test.d.ts +2 -0
- package/dist/__tests__/structure-epoch-boundaries.unit.test.d.ts.map +1 -0
- package/dist/__tests__/structure-epoch.unit.test.d.ts +2 -0
- package/dist/__tests__/structure-epoch.unit.test.d.ts.map +1 -0
- package/dist/__tests__/table-archetype-mapping.unit.test.d.ts +2 -0
- package/dist/__tests__/table-archetype-mapping.unit.test.d.ts.map +1 -0
- package/dist/__tests__/time.unit.test.d.ts +2 -0
- package/dist/__tests__/time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/types.test-d.d.ts +2 -0
- package/dist/__tests__/types.test-d.d.ts.map +1 -0
- package/dist/__tests__/unique-ref-store.unit.test.d.ts +2 -0
- package/dist/__tests__/unique-ref-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/utils/handle-numeric.d.ts +14 -0
- package/dist/__tests__/utils/handle-numeric.d.ts.map +1 -0
- package/dist/__tests__/validation-root-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/validation-root-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/visibility-atomic-write.unit.test.d.ts +2 -0
- package/dist/__tests__/visibility-atomic-write.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-change-journal.unit.test.d.ts +2 -0
- package/dist/__tests__/world-change-journal.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-characterization.unit.test.d.ts +2 -0
- package/dist/__tests__/world-characterization.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-consumer-inference.test-d.d.ts +2 -0
- package/dist/__tests__/world-consumer-inference.test-d.d.ts.map +1 -0
- package/dist/__tests__/world-health.contract.test.d.ts +2 -0
- package/dist/__tests__/world-health.contract.test.d.ts.map +1 -0
- package/dist/__tests__/world-inspection.test.d.ts +2 -0
- package/dist/__tests__/world-inspection.test.d.ts.map +1 -0
- package/dist/__tests__/world-internal-identity.unit.test.d.ts +2 -0
- package/dist/__tests__/world-internal-identity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-managed-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/world-managed-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-scheduling.m4.test.d.ts +2 -0
- package/dist/__tests__/world-scheduling.m4.test.d.ts.map +1 -0
- package/dist/__tests__/world-unique-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/world-unique-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world.test-d.d.ts +2 -0
- package/dist/__tests__/world.test-d.d.ts.map +1 -0
- package/dist/buffer-pool.d.ts +109 -0
- package/dist/buffer-pool.d.ts.map +1 -0
- package/dist/commands.d.ts +93 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/component-default-fallback.d.ts +88 -0
- package/dist/component-default-fallback.d.ts.map +1 -0
- package/dist/component-schema.d.ts +34 -0
- package/dist/component-schema.d.ts.map +1 -0
- package/dist/component-value-validate.d.ts +29 -0
- package/dist/component-value-validate.d.ts.map +1 -0
- package/dist/component.d.ts +581 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/entity-handle.d.ts +68 -0
- package/dist/entity-handle.d.ts.map +1 -0
- package/dist/entity.d.ts +86 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/errors/query-and-component-errors.d.ts +132 -0
- package/dist/errors/query-and-component-errors.d.ts.map +1 -0
- package/dist/errors/relationship-errors.d.ts +112 -0
- package/dist/errors/relationship-errors.d.ts.map +1 -0
- package/dist/errors/sprite-and-shared-errors.d.ts +107 -0
- package/dist/errors/sprite-and-shared-errors.d.ts.map +1 -0
- package/dist/errors/validation-errors.d.ts +229 -0
- package/dist/errors/validation-errors.d.ts.map +1 -0
- package/dist/errors.d.ts +763 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/execution/shared-kernel.d.ts +106 -0
- package/dist/execution/shared-kernel.d.ts.map +1 -0
- package/dist/externalization/index.d.ts +31 -0
- package/dist/externalization/index.d.ts.map +1 -0
- package/dist/externalization/index.mjs +298 -0
- package/dist/externalization/index.mjs.map +1 -0
- package/dist/index.d.ts +261 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +7422 -0
- package/dist/index.mjs.map +1 -0
- package/dist/internal.mjs +479 -0
- package/dist/internal.mjs.map +1 -0
- package/dist/plugin-service.d.ts +11 -0
- package/dist/plugin-service.d.ts.map +1 -0
- package/dist/projection/index.d.ts +103 -0
- package/dist/projection/index.d.ts.map +1 -0
- package/dist/projection/index.mjs +615 -0
- package/dist/projection/index.mjs.map +1 -0
- package/dist/query/query.d.ts +58 -0
- package/dist/query/query.d.ts.map +1 -0
- package/dist/relationship-index.d.ts +81 -0
- package/dist/relationship-index.d.ts.map +1 -0
- package/dist/resource.d.ts +70 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/schedule-token.d.ts +16 -0
- package/dist/schedule-token.d.ts.map +1 -0
- package/dist/schedule.d.ts +271 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/shared-ref-store.d.ts +144 -0
- package/dist/shared-ref-store.d.ts.map +1 -0
- package/dist/shared.d.ts +3 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.mjs +364 -0
- package/dist/shared.mjs.map +1 -0
- package/dist/storage/archetype-graph.d.ts +54 -0
- package/dist/storage/archetype-graph.d.ts.map +1 -0
- package/dist/storage/archetype.d.ts +23 -0
- package/dist/storage/archetype.d.ts.map +1 -0
- package/dist/storage/change-detection.d.ts +40 -0
- package/dist/storage/change-detection.d.ts.map +1 -0
- package/dist/storage/column.d.ts +111 -0
- package/dist/storage/column.d.ts.map +1 -0
- package/dist/storage/table.d.ts +29 -0
- package/dist/storage/table.d.ts.map +1 -0
- package/dist/time.d.ts +57 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/unique-ref-store.d.ts +105 -0
- package/dist/unique-ref-store.d.ts.map +1 -0
- package/dist/world-change-journal.d.ts +34 -0
- package/dist/world-change-journal.d.ts.map +1 -0
- package/dist/world-component-access.d.ts +265 -0
- package/dist/world-component-access.d.ts.map +1 -0
- package/dist/world-component-storage.d.ts +282 -0
- package/dist/world-component-storage.d.ts.map +1 -0
- package/dist/world-core.d.ts +39 -0
- package/dist/world-core.d.ts.map +1 -0
- package/dist/world-entity-lifecycle.d.ts +30 -0
- package/dist/world-entity-lifecycle.d.ts.map +1 -0
- package/dist/world-internal.d.ts +14 -0
- package/dist/world-internal.d.ts.map +1 -0
- package/dist/world-scheduling.d.ts +30 -0
- package/dist/world-scheduling.d.ts.map +1 -0
- package/dist/world.d.ts +620 -0
- package/dist/world.d.ts.map +1 -0
- package/package.json +77 -0
- package/src/__fixtures__/field-shape-fixture.ts +30 -0
- package/src/__tests__/__fixtures__/inspect-scripts.snapshot.ts +2 -0
- package/src/__tests__/archetype.unit.test.ts +1837 -0
- package/src/__tests__/array-handle-element.test-d.ts +34 -0
- package/src/__tests__/buffer-array-vocab.test-d.ts +53 -0
- package/src/__tests__/buffer-field-input-widen.unit.test.ts +99 -0
- package/src/__tests__/buffer-pool.test-d.ts +32 -0
- package/src/__tests__/change-detection.unit.test.ts +86 -0
- package/src/__tests__/command-buffer.test.ts +179 -0
- package/src/__tests__/commands-deferred.unit.test.ts +68 -0
- package/src/__tests__/component-catalog.test.ts +59 -0
- package/src/__tests__/component-definition-identity.unit.test.ts +24 -0
- package/src/__tests__/component-nan-preflight.unit.test.ts +186 -0
- package/src/__tests__/component-schema.test-d.ts +119 -0
- package/src/__tests__/component-schema.test.ts +55 -0
- package/src/__tests__/define-component-options.test-d.ts +35 -0
- package/src/__tests__/deprecated-vocab-rejection.unit.test.ts +59 -0
- package/src/__tests__/despawn-all.unit.test.ts +17 -0
- package/src/__tests__/ecs-core-reduction.characterization.test.ts +130 -0
- package/src/__tests__/ecs-core-reduction.perf.test.ts +260 -0
- package/src/__tests__/ecs-error-code-31.test-d.ts +42 -0
- package/src/__tests__/entity-disabling.unit.test.ts +37 -0
- package/src/__tests__/entity-handle-codec-parity.unit.test.ts +126 -0
- package/src/__tests__/errors.unit.test.ts +1456 -0
- package/src/__tests__/execution-conflict-boundary.unit.test.ts +123 -0
- package/src/__tests__/execution-shared-storage.unit.test.ts +40 -0
- package/src/__tests__/externalization-portable-value.unit.test.ts +275 -0
- package/src/__tests__/externalization-projection.unit.test.ts +325 -0
- package/src/__tests__/externalization-render-read-lease.test-d.ts +30 -0
- package/src/__tests__/externalization-render-read-lease.unit.test.ts +79 -0
- package/src/__tests__/externalization-root-surface.unit.test.ts +33 -0
- package/src/__tests__/field-enum-labels-reflection.test.ts +66 -0
- package/src/__tests__/field-shape-fixture.test.ts +61 -0
- package/src/__tests__/field-transient-reflection.test.ts +59 -0
- package/src/__tests__/field-value-type.test-d.ts +57 -0
- package/src/__tests__/fixed-update.unit.test.ts +78 -0
- package/src/__tests__/handle.test-d.ts +86 -0
- package/src/__tests__/hierarchy.unit.test.ts +2534 -0
- package/src/__tests__/managed-array-vocab.test-d.ts +137 -0
- package/src/__tests__/minimal-example.test-d.ts +42 -0
- package/src/__tests__/name-read-types.test-d.ts +72 -0
- package/src/__tests__/plugin-service.test.ts +13 -0
- package/src/__tests__/query-combinations.unit.test.ts +42 -0
- package/src/__tests__/query-light-extract-flat-column-ratio.perf.test.ts +252 -0
- package/src/__tests__/query-storage-trends.perf.test.ts +104 -0
- package/src/__tests__/query-trs-flat-column-ratio.perf.test.ts +194 -0
- package/src/__tests__/query.unit.test.ts +196 -0
- package/src/__tests__/relationship-index.perf.test.ts +126 -0
- package/src/__tests__/relationship-index.test.ts +70 -0
- package/src/__tests__/scalar-field-type-owner.test-d.ts +71 -0
- package/src/__tests__/schedule-determinism.unit.test.ts +36 -0
- package/src/__tests__/schedule-inspection.unit.test.ts +81 -0
- package/src/__tests__/schedule-scope-error.unit.test.ts +63 -0
- package/src/__tests__/schedule-token.unit.test.ts +54 -0
- package/src/__tests__/schema-vocab.test-d.ts +103 -0
- package/src/__tests__/serialization.unit.test.ts +2740 -0
- package/src/__tests__/shared-kernel-eligibility.test-d.ts +18 -0
- package/src/__tests__/shared-kernel-eligibility.unit.test.ts +78 -0
- package/src/__tests__/shared-ref-store.unit.test.ts +646 -0
- package/src/__tests__/sparse-tag.unit.test.ts +201 -0
- package/src/__tests__/spawn-field-error.test-d.ts +41 -0
- package/src/__tests__/stale-error.test-d.ts +208 -0
- package/src/__tests__/string-view-symmetry.test-d.ts +26 -0
- package/src/__tests__/string-vocab.test-d.ts +49 -0
- package/src/__tests__/structure-epoch-boundaries.unit.test.ts +39 -0
- package/src/__tests__/structure-epoch.unit.test.ts +48 -0
- package/src/__tests__/table-archetype-mapping.unit.test.ts +78 -0
- package/src/__tests__/time.unit.test.ts +79 -0
- package/src/__tests__/types.test-d.ts +38 -0
- package/src/__tests__/unique-ref-store.unit.test.ts +348 -0
- package/src/__tests__/utils/handle-numeric.ts +30 -0
- package/src/__tests__/validation-root-surface.unit.test.ts +15 -0
- package/src/__tests__/visibility-atomic-write.unit.test.ts +82 -0
- package/src/__tests__/world-change-journal.unit.test.ts +97 -0
- package/src/__tests__/world-characterization.unit.test.ts +408 -0
- package/src/__tests__/world-consumer-inference.test-d.ts +16 -0
- package/src/__tests__/world-health.contract.test.ts +87 -0
- package/src/__tests__/world-inspection.test.ts +37 -0
- package/src/__tests__/world-internal-identity.unit.test.ts +17 -0
- package/src/__tests__/world-managed-roundtrip.unit.test.ts +98 -0
- package/src/__tests__/world-scheduling.m4.test.ts +52 -0
- package/src/__tests__/world-unique-roundtrip.unit.test.ts +357 -0
- package/src/__tests__/world.test-d.ts +156 -0
- package/src/buffer-pool.ts +357 -0
- package/src/commands.ts +644 -0
- package/src/component-default-fallback.ts +236 -0
- package/src/component-schema.ts +92 -0
- package/src/component-value-validate.ts +135 -0
- package/src/component.ts +1268 -0
- package/src/entity-handle.ts +104 -0
- package/src/entity.ts +140 -0
- package/src/errors/query-and-component-errors.ts +200 -0
- package/src/errors/relationship-errors.ts +198 -0
- package/src/errors/sprite-and-shared-errors.ts +231 -0
- package/src/errors/validation-errors.ts +506 -0
- package/src/errors.ts +1464 -0
- package/src/execution/shared-kernel.ts +392 -0
- package/src/externalization/index.ts +170 -0
- package/src/index.ts +414 -0
- package/src/plugin-service.ts +35 -0
- package/src/projection/index.ts +318 -0
- package/src/query/query.ts +606 -0
- package/src/relationship-index.ts +175 -0
- package/src/resource.ts +106 -0
- package/src/schedule-token.ts +40 -0
- package/src/schedule.ts +846 -0
- package/src/shared-ref-store.ts +418 -0
- package/src/shared.ts +23 -0
- package/src/storage/archetype-graph.ts +166 -0
- package/src/storage/archetype.ts +78 -0
- package/src/storage/change-detection.ts +231 -0
- package/src/storage/column.ts +247 -0
- package/src/storage/table.ts +153 -0
- package/src/time.ts +122 -0
- package/src/unique-ref-store.ts +278 -0
- package/src/world-change-journal.ts +84 -0
- package/src/world-component-access.ts +1421 -0
- package/src/world-component-storage.ts +1178 -0
- package/src/world-core.ts +74 -0
- package/src/world-entity-lifecycle.ts +539 -0
- package/src/world-internal.ts +75 -0
- package/src/world-scheduling.ts +499 -0
- package/src/world.ts +1399 -0
package/src/world.ts
ADDED
|
@@ -0,0 +1,1399 @@
|
|
|
1
|
+
// @forgeax/engine-ecs — World: top-level ECS container.
|
|
2
|
+
//
|
|
3
|
+
// World owns entities, archetypes (via ArchetypeGraph), and component registry.
|
|
4
|
+
// Supports multi-component spawn, despawn (with generation retirement D-08),
|
|
5
|
+
// get/set, addComponent/removeComponent (archetype migration via edges).
|
|
6
|
+
// M3: addSystem / update (DAG schedule) + deferred commands + Resource CRUD.
|
|
7
|
+
//
|
|
8
|
+
// [w6] All 6 public methods return Result<T, EcsError> (AP-8 Layer 1).
|
|
9
|
+
// Construction errors (EntityIndexOverflowError) still throw — they are
|
|
10
|
+
// build-time / infrastructure failures.
|
|
11
|
+
|
|
12
|
+
import type { Handle, Result } from '@forgeax/engine-types';
|
|
13
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
14
|
+
import type { BufferPool } from './buffer-pool';
|
|
15
|
+
import {
|
|
16
|
+
type Component,
|
|
17
|
+
ComponentCatalog,
|
|
18
|
+
type ComponentSchema,
|
|
19
|
+
componentId,
|
|
20
|
+
type InputShapeOf,
|
|
21
|
+
type ShapeOf,
|
|
22
|
+
} from './component';
|
|
23
|
+
import { type EntityHandle, encodeEntity, entityGeneration, entityIndex } from './entity-handle';
|
|
24
|
+
import type {
|
|
25
|
+
CommandFailedError,
|
|
26
|
+
ComponentAlreadyPresentError,
|
|
27
|
+
ComponentFieldInvalidValueError,
|
|
28
|
+
ComponentNotDefinedError,
|
|
29
|
+
ComponentNotPresentError,
|
|
30
|
+
ComponentNumericValueInvalidError,
|
|
31
|
+
FixedSizeMismatchError,
|
|
32
|
+
ManagedArrayInvalidValueError,
|
|
33
|
+
ManagedBufferOutOfBoundsError,
|
|
34
|
+
ManagedBufferShrinkNotSupportedError,
|
|
35
|
+
RelationshipDetachMismatchError,
|
|
36
|
+
RelationshipMirrorComponentNotRegisteredError,
|
|
37
|
+
RelationshipMirrorFieldTypeMismatchError,
|
|
38
|
+
RelationshipSelfCycleError,
|
|
39
|
+
RemoveEssentialComponentError,
|
|
40
|
+
ScheduleMutationError,
|
|
41
|
+
ScheduleScopeMismatchError,
|
|
42
|
+
SharedKernelEligibilityError,
|
|
43
|
+
SharedKernelFailureError,
|
|
44
|
+
StaleEntityError,
|
|
45
|
+
SystemFailedError,
|
|
46
|
+
SystemSetNotRegisteredError,
|
|
47
|
+
TimeConfigInvalidError,
|
|
48
|
+
TimeDeltaInvalidError,
|
|
49
|
+
UniqueRefDoubleReleaseError,
|
|
50
|
+
UniqueRefReleasedError,
|
|
51
|
+
WorldPoisonedError,
|
|
52
|
+
} from './errors';
|
|
53
|
+
import { ChangeEpochExhaustedError, RelationshipTargetReadonlyError } from './errors';
|
|
54
|
+
import {
|
|
55
|
+
healthyWorldExecutionState,
|
|
56
|
+
poisonedWorldExecutionState,
|
|
57
|
+
type WorldExecutionFault,
|
|
58
|
+
type WorldExecutionState,
|
|
59
|
+
} from './execution/shared-kernel';
|
|
60
|
+
import type { QueryDescriptor } from './query/query';
|
|
61
|
+
import { createQuery, type Query, type QueryCreationError } from './query/query';
|
|
62
|
+
import { isRelationshipTarget, type RelationshipTargetComponent } from './relationship-index';
|
|
63
|
+
import { createResourceStore, type ResourceStore } from './resource';
|
|
64
|
+
import { createSchedule, type Schedule, type SystemDescriptor, type SystemSet } from './schedule';
|
|
65
|
+
import { FixedUpdate, Update } from './schedule-token';
|
|
66
|
+
import type { SharedRefStore } from './shared-ref-store';
|
|
67
|
+
import type { Archetype } from './storage/archetype';
|
|
68
|
+
import type { ArchetypeGraph } from './storage/archetype-graph';
|
|
69
|
+
import {
|
|
70
|
+
type ChangeTicks,
|
|
71
|
+
markComponentChanged,
|
|
72
|
+
markComponentsAdded,
|
|
73
|
+
readComponentChange,
|
|
74
|
+
readTableEntityRange,
|
|
75
|
+
type WorldChangeRead,
|
|
76
|
+
} from './storage/change-detection';
|
|
77
|
+
import {
|
|
78
|
+
type ClockWriter,
|
|
79
|
+
createWorldClock,
|
|
80
|
+
DEFAULT_TIME_POLICY,
|
|
81
|
+
FIXED_TIME_RESOURCE_KEY,
|
|
82
|
+
TIME_RESOURCE_KEY,
|
|
83
|
+
type WorldOptions,
|
|
84
|
+
} from './time';
|
|
85
|
+
import type { UniqueRefStore } from './unique-ref-store';
|
|
86
|
+
import { WorldComponentAccess } from './world-component-access';
|
|
87
|
+
import { WorldCore } from './world-core';
|
|
88
|
+
import {
|
|
89
|
+
despawnCore,
|
|
90
|
+
spawnCore,
|
|
91
|
+
worldAddChild,
|
|
92
|
+
worldIterAncestors,
|
|
93
|
+
worldIterDescendants,
|
|
94
|
+
worldRemoveChild,
|
|
95
|
+
worldReparent,
|
|
96
|
+
} from './world-entity-lifecycle';
|
|
97
|
+
import { type WorldInternal, worldInternal } from './world-internal';
|
|
98
|
+
import {
|
|
99
|
+
worldAddSystem,
|
|
100
|
+
worldAddSystems,
|
|
101
|
+
worldAllocSharedRef,
|
|
102
|
+
worldAllocUniqueRef,
|
|
103
|
+
worldGetResource,
|
|
104
|
+
worldHasResource,
|
|
105
|
+
worldInsertResource,
|
|
106
|
+
worldInspect,
|
|
107
|
+
worldInternSharedRef,
|
|
108
|
+
worldRemoveResource,
|
|
109
|
+
worldRemoveSystem,
|
|
110
|
+
worldReplaceSystem,
|
|
111
|
+
worldScheduleData,
|
|
112
|
+
worldScheduleUsesComponent,
|
|
113
|
+
worldUpdate,
|
|
114
|
+
} from './world-scheduling';
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Union of all EcsError types that World methods can return via Result.
|
|
118
|
+
* AI users: switch on `.code` for programmatic branching.
|
|
119
|
+
*/
|
|
120
|
+
export type EcsError =
|
|
121
|
+
| CommandFailedError
|
|
122
|
+
| StaleEntityError
|
|
123
|
+
| ComponentNotPresentError
|
|
124
|
+
| ComponentAlreadyPresentError
|
|
125
|
+
| ComponentFieldInvalidValueError
|
|
126
|
+
| ComponentNumericValueInvalidError
|
|
127
|
+
| ManagedArrayInvalidValueError
|
|
128
|
+
| UniqueRefReleasedError
|
|
129
|
+
| UniqueRefDoubleReleaseError
|
|
130
|
+
| ManagedBufferOutOfBoundsError
|
|
131
|
+
| ManagedBufferShrinkNotSupportedError
|
|
132
|
+
| FixedSizeMismatchError
|
|
133
|
+
| RelationshipSelfCycleError
|
|
134
|
+
| RelationshipMirrorComponentNotRegisteredError
|
|
135
|
+
| RelationshipMirrorFieldTypeMismatchError
|
|
136
|
+
| RelationshipDetachMismatchError
|
|
137
|
+
| RelationshipTargetReadonlyError
|
|
138
|
+
| ComponentNotDefinedError
|
|
139
|
+
| RemoveEssentialComponentError
|
|
140
|
+
| SystemSetNotRegisteredError
|
|
141
|
+
| SystemFailedError
|
|
142
|
+
| TimeDeltaInvalidError
|
|
143
|
+
| TimeConfigInvalidError
|
|
144
|
+
| ScheduleScopeMismatchError
|
|
145
|
+
| SharedKernelEligibilityError
|
|
146
|
+
| SharedKernelFailureError
|
|
147
|
+
| WorldPoisonedError;
|
|
148
|
+
|
|
149
|
+
/** Component data for spawn/addComponent: component token + initial values.
|
|
150
|
+
*
|
|
151
|
+
* `data` is `Partial<InputShapeOf<S>>` (feat-20260517 / M2; tweak-20260616
|
|
152
|
+
* input/output split): spawn / addComponent / SceneAsset.instantiate share the
|
|
153
|
+
* SAME shape contract via the layer-2 + layer-3 silent fallback applied inside
|
|
154
|
+
* `writeRow` (`fillComponentDefaults`). The input shape widens
|
|
155
|
+
* `array<scalar, N>` / `array<scalar>` to also accept `readonly number[]`
|
|
156
|
+
* because writeArrayField copies bytes from either shape — AI users can write
|
|
157
|
+
* `times: [0.5]` instead of `new Float32Array([0.5])` boilerplate. Wrong-VALUE
|
|
158
|
+
* fields (e.g. `{ fov: 'bad' }`) still fire field-level TS2322 — mapped-tuple
|
|
159
|
+
* primary inference does not degrade to the "No overload matches" wall
|
|
160
|
+
* (AC-03 / C-4). */
|
|
161
|
+
export interface ComponentData<S extends ComponentSchema = ComponentSchema> {
|
|
162
|
+
component: Component<string, S>;
|
|
163
|
+
data: Partial<InputShapeOf<S>>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type WritableComponent<C extends Component> = C extends RelationshipTargetComponent ? never : C;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Per-archetype summary returned by `world.inspect()`. Sorted ComponentId key
|
|
170
|
+
* (always prefixed by the essential id=0 Entity column, e.g. "0+2+5+7"),
|
|
171
|
+
* human-readable component names, live entity count, allocated row capacity.
|
|
172
|
+
*/
|
|
173
|
+
export interface ArchetypeInfo {
|
|
174
|
+
/** Sorted ComponentId key, always prefixed by the id=0 Entity column (e.g. "0+2+5+7"). */
|
|
175
|
+
readonly key: string;
|
|
176
|
+
/** Human-readable component names in this archetype. */
|
|
177
|
+
readonly componentNames: string[];
|
|
178
|
+
/** Number of live entities in this archetype. */
|
|
179
|
+
readonly entityCount: number;
|
|
180
|
+
readonly tableId: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface TableInfo {
|
|
184
|
+
readonly id: number;
|
|
185
|
+
readonly key: string;
|
|
186
|
+
readonly componentNames: string[];
|
|
187
|
+
readonly entityCount: number;
|
|
188
|
+
readonly capacity: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Typed diagnostic snapshot of the World state.
|
|
193
|
+
* Returned by `world.inspect()` for programmatic introspection by AI users.
|
|
194
|
+
*/
|
|
195
|
+
export interface WorldInspection {
|
|
196
|
+
/** Total number of live entities. */
|
|
197
|
+
readonly entityCount: number;
|
|
198
|
+
/** Number of archetypes currently allocated. */
|
|
199
|
+
readonly archetypeCount: number;
|
|
200
|
+
/** Per-archetype details. */
|
|
201
|
+
readonly archetypes: ArchetypeInfo[];
|
|
202
|
+
readonly tableCount: number;
|
|
203
|
+
readonly tables: TableInfo[];
|
|
204
|
+
/**
|
|
205
|
+
* Names of components that are currently active in this World — i.e.
|
|
206
|
+
* every distinct component name appearing on at least one non-empty
|
|
207
|
+
* archetype. Collected by walking the archetype graph, so a component
|
|
208
|
+
* that was defined but never spawned into this World does not appear.
|
|
209
|
+
*/
|
|
210
|
+
readonly activeComponents: string[];
|
|
211
|
+
/** Number of registered systems. Always equals `systems.length` (M2 derived invariant). */
|
|
212
|
+
readonly systemCount: number;
|
|
213
|
+
/**
|
|
214
|
+
* Per-system summary (M3 — plan-strategy D-8). One entry per
|
|
215
|
+
* registered system, in registration order. The `systemCount` field is
|
|
216
|
+
* preserved as a derived alias of `systems.length` so existing inspector
|
|
217
|
+
* P0 e2e cases that read `systemCount` keep working.
|
|
218
|
+
*
|
|
219
|
+
* `sets` is the list of set names this system belongs to (empty array for
|
|
220
|
+
* systems registered via plain `addSystem` without `addSystems`).
|
|
221
|
+
*/
|
|
222
|
+
readonly systems: ReadonlyArray<{ readonly name: string; readonly sets: readonly string[] }>;
|
|
223
|
+
/** Keys of all inserted resources. */
|
|
224
|
+
readonly resourceKeys: string[];
|
|
225
|
+
/** Systems grouped by their schedule token. */
|
|
226
|
+
readonly schedules: ReadonlyArray<{
|
|
227
|
+
readonly schedule: import('./schedule-token').ScheduleToken;
|
|
228
|
+
readonly systems: ReadonlyArray<{ readonly name: string; readonly sets: readonly string[] }>;
|
|
229
|
+
}>;
|
|
230
|
+
/** Count systems in one explicit schedule. */
|
|
231
|
+
scheduleSystemCount(schedule: import('./schedule-token').ScheduleToken): number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** JSON-safe schedule graph and access metadata returned by `world.scheduleData()`. */
|
|
235
|
+
export interface WorldScheduleQueryData {
|
|
236
|
+
readonly with: readonly string[];
|
|
237
|
+
readonly without: readonly string[];
|
|
238
|
+
readonly optional: readonly string[];
|
|
239
|
+
readonly changed: readonly string[];
|
|
240
|
+
readonly added: readonly string[];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** JSON-safe system registration and access metadata. */
|
|
244
|
+
export interface WorldScheduleSystemData {
|
|
245
|
+
readonly name: string;
|
|
246
|
+
readonly sets: readonly string[];
|
|
247
|
+
readonly before: readonly string[];
|
|
248
|
+
readonly after: readonly string[];
|
|
249
|
+
readonly queries: readonly WorldScheduleQueryData[];
|
|
250
|
+
readonly resources: readonly string[];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** JSON-safe system-set membership and ordering metadata. */
|
|
254
|
+
export interface WorldScheduleSetData {
|
|
255
|
+
readonly name: string;
|
|
256
|
+
readonly members: readonly string[];
|
|
257
|
+
readonly before: readonly string[];
|
|
258
|
+
readonly after: readonly string[];
|
|
259
|
+
readonly chained: boolean;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** JSON-safe projection of one explicit World schedule. */
|
|
263
|
+
export interface WorldScheduleData {
|
|
264
|
+
readonly name: string;
|
|
265
|
+
readonly systems: readonly WorldScheduleSystemData[];
|
|
266
|
+
readonly systemSets: readonly WorldScheduleSetData[];
|
|
267
|
+
readonly dependencies: readonly (readonly [string, string])[];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Internal record describing where an entity lives.
|
|
272
|
+
*
|
|
273
|
+
* Liveness (feat-20260602 / plan-strategy D-4): the former `alive` boolean was
|
|
274
|
+
* absorbed into `generation`. A despawn unconditionally bumps `generation` (so a
|
|
275
|
+
* stale handle's `gen` no longer matches), and `gen > 255` retires the slot
|
|
276
|
+
* permanently (it is never pushed back to `freeIndices`). The single liveness
|
|
277
|
+
* predicate is therefore "handle gen matches AND archetypeId !== -1" -- see
|
|
278
|
+
* `World.recordIsLive`. A deferred-spawn allocation is "pending" when
|
|
279
|
+
* archetypeId === -1 (not yet materialized into an archetype row); no separate
|
|
280
|
+
* boolean is needed.
|
|
281
|
+
*/
|
|
282
|
+
export interface EntityRecord {
|
|
283
|
+
generation: number;
|
|
284
|
+
archetypeId: number; // -1 if no archetype (pending / despawned)
|
|
285
|
+
archetypeRow: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The World owns:
|
|
290
|
+
* - the registry of known component schemas;
|
|
291
|
+
* - all archetypes (via ArchetypeGraph);
|
|
292
|
+
* - the entity index table (records by index slot);
|
|
293
|
+
* - the free-list of recyclable entity slots.
|
|
294
|
+
*/
|
|
295
|
+
export class World {
|
|
296
|
+
/** Package-internal implementation seam; not exported from any entry point. */
|
|
297
|
+
readonly [worldInternal]: WorldInternal;
|
|
298
|
+
// ── Internal state ──
|
|
299
|
+
|
|
300
|
+
/** The single package-private owner of storage, epochs, and change evidence. */
|
|
301
|
+
private readonly core: WorldCore;
|
|
302
|
+
get identity() {
|
|
303
|
+
return this.core.identity;
|
|
304
|
+
}
|
|
305
|
+
/** Plugin-owned component discovery scoped to this World and removed through leases. */
|
|
306
|
+
readonly components = new ComponentCatalog((component) => this.componentIsInUse(component));
|
|
307
|
+
private executionState: WorldExecutionState;
|
|
308
|
+
/** Monotonic clock advanced exactly once per successful mutation. */
|
|
309
|
+
/** Monotonic revision for successful entity/component structure writes. */
|
|
310
|
+
/** Last mutation epoch for each component id, used by component-owned projections. */
|
|
311
|
+
/** Ordered, bounded evidence consumed by persistent engine-owned projections. */
|
|
312
|
+
private get mutationEpoch() {
|
|
313
|
+
return this.core.mutationEpoch;
|
|
314
|
+
}
|
|
315
|
+
private set mutationEpoch(value: number) {
|
|
316
|
+
this.core.mutationEpoch = value;
|
|
317
|
+
}
|
|
318
|
+
private get structureEpoch() {
|
|
319
|
+
return this.core.structureEpoch;
|
|
320
|
+
}
|
|
321
|
+
private set structureEpoch(value: number) {
|
|
322
|
+
this.core.structureEpoch = value;
|
|
323
|
+
}
|
|
324
|
+
private get componentMutationEpochs() {
|
|
325
|
+
return this.core.componentMutationEpochs;
|
|
326
|
+
}
|
|
327
|
+
private get changeJournal() {
|
|
328
|
+
return this.core.changeJournal;
|
|
329
|
+
}
|
|
330
|
+
/** Free index slots (LIFO stack). */
|
|
331
|
+
private get records() {
|
|
332
|
+
return this.core.records;
|
|
333
|
+
}
|
|
334
|
+
private get freeIndices() {
|
|
335
|
+
return this.core.freeIndices;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Relationship-sync reentry guard (feat-20260531 M2 / plan-strategy D-7).
|
|
339
|
+
/** The archetype graph: manages all archetypes + edge caching. */
|
|
340
|
+
private get graph(): ArchetypeGraph {
|
|
341
|
+
return this.core.graph;
|
|
342
|
+
}
|
|
343
|
+
/** DAG schedules for the two built-in execution scopes. */
|
|
344
|
+
private readonly schedules = new Map([
|
|
345
|
+
[Update, createSchedule(Update)],
|
|
346
|
+
[FixedUpdate, createSchedule(FixedUpdate)],
|
|
347
|
+
]);
|
|
348
|
+
/** Resource store: typed key-value global singletons. */
|
|
349
|
+
private readonly resources: ResourceStore = createResourceStore();
|
|
350
|
+
private readonly clock: ReturnType<typeof createWorldClock>;
|
|
351
|
+
/** Remainder carried between fixed-step runs. */
|
|
352
|
+
private fixedAccumulator = 0;
|
|
353
|
+
/**
|
|
354
|
+
* ECS-managed handle store (M1). Owned by the World - constructed eagerly
|
|
355
|
+
* so every spawn / despawn / set path can dispatch managed-ref releases
|
|
356
|
+
* without caller-side wiring. AI users obtain `Handle<T,'unique'>` values
|
|
357
|
+
* by accessing the store through internal channels (the surface is
|
|
358
|
+
* private; managed-ref-bearing fields read through `world.get`).
|
|
359
|
+
*/
|
|
360
|
+
// UniqueRefStore is type-erased at the storage layer (alloc/resolve are
|
|
361
|
+
// method-generic over `T`); World holds the single per-instance store and
|
|
362
|
+
// routes payload-agnostic release calls. Typed access flows through
|
|
363
|
+
// `UniqueRefStore.resolve<T>` at the consumer layer.
|
|
364
|
+
private get uniqueRefs(): UniqueRefStore {
|
|
365
|
+
return this.core.uniqueRefs;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Per-World `SharedRefStore` (feat-20260614 M3). Backs every `shared<T>`
|
|
369
|
+
* schema field + the `world.allocSharedRef` facade. Public read-only so AI
|
|
370
|
+
* users can `retain` / `release` / `resolve` user-tier handles directly off
|
|
371
|
+
* the world (the surface is small enough that hiding it behind another
|
|
372
|
+
* facade would be a phantom indirection - charter F1 single-entry
|
|
373
|
+
* indexability).
|
|
374
|
+
*
|
|
375
|
+
* Final release publishes structured evidence; there is no callback surface.
|
|
376
|
+
* M6 D-15: the store manages only user-tier slots
|
|
377
|
+
* (`>= BUILTIN_BASE`); builtin handles are process-static in their
|
|
378
|
+
* authoring package and never reference-counted.
|
|
379
|
+
*/
|
|
380
|
+
get sharedRefs(): SharedRefStore {
|
|
381
|
+
return this.core.sharedRefs;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* BufferPool backing every `buffer:<N>` schema-vocab field (M2). Eagerly
|
|
385
|
+
* constructed (per-World, D-2). `spawn` allocs slots for buffer fields and
|
|
386
|
+
* stores the slot id in the u32 column; `despawn` / `removeComponent`
|
|
387
|
+
* release the slots; `set(e, C, { field: Uint8Array })` copies bytes into
|
|
388
|
+
* the live view without re-allocating (schema-declared byteLength is
|
|
389
|
+
* fixed in v1; runtime grow is reserved for the M4 carry-over path).
|
|
390
|
+
*/
|
|
391
|
+
private get bufferPool() {
|
|
392
|
+
return this.core.bufferPool;
|
|
393
|
+
}
|
|
394
|
+
private readonly componentAccess: WorldComponentAccess;
|
|
395
|
+
|
|
396
|
+
constructor(options: WorldOptions = {}) {
|
|
397
|
+
this.core = new WorldCore(options.storage === 'shared');
|
|
398
|
+
this.executionState = healthyWorldExecutionState(this.identity);
|
|
399
|
+
this.componentAccess = new WorldComponentAccess({
|
|
400
|
+
graph: this.graph,
|
|
401
|
+
records: this.records,
|
|
402
|
+
freeIndices: this.freeIndices,
|
|
403
|
+
bufferPool: this.bufferPool,
|
|
404
|
+
uniqueRefs: this.uniqueRefs,
|
|
405
|
+
sharedRefs: this.sharedRefs,
|
|
406
|
+
relationshipIndexes: this.core.relationshipIndexes,
|
|
407
|
+
markComponentAdded: (entity, component) => this.internalmarkComponentAdded(entity, component),
|
|
408
|
+
markComponentsAdded: (entity, components) =>
|
|
409
|
+
this.internalmarkComponentsAdded(entity, components),
|
|
410
|
+
markComponentChanged: (entity, component) =>
|
|
411
|
+
this.internalmarkComponentChanged(entity, component),
|
|
412
|
+
removeComponentChange: (entity, component) =>
|
|
413
|
+
this.internalremoveComponentChange(entity, component),
|
|
414
|
+
markStructureChanged: () => this.internalmarkStructureChanged(),
|
|
415
|
+
routeError: (error, context) => this.internalrouteError(error as EcsError, context),
|
|
416
|
+
});
|
|
417
|
+
this.clock = createWorldClock({ ...DEFAULT_TIME_POLICY, ...options.time });
|
|
418
|
+
this.resources.entries.set(TIME_RESOURCE_KEY, {
|
|
419
|
+
value: this.clock.time,
|
|
420
|
+
added: 0,
|
|
421
|
+
changed: 0,
|
|
422
|
+
});
|
|
423
|
+
this.resources.entries.set(FIXED_TIME_RESOURCE_KEY, {
|
|
424
|
+
value: this.clock.fixed,
|
|
425
|
+
added: 0,
|
|
426
|
+
changed: 0,
|
|
427
|
+
});
|
|
428
|
+
this[worldInternal] = {
|
|
429
|
+
addComponentCore: this.internaladdComponentCore.bind(this),
|
|
430
|
+
allocateIndex: this.internalallocateIndex.bind(this),
|
|
431
|
+
allocatePendingEntity: this.internalallocatePendingEntity.bind(this),
|
|
432
|
+
cancelPendingEntity: this.internalcancelPendingEntity.bind(this),
|
|
433
|
+
despawnCore: this.internaldespawnCore.bind(this),
|
|
434
|
+
getArrayView: this.internalgetArrayView.bind(this),
|
|
435
|
+
getBufferPool: this.internalgetBufferPool.bind(this),
|
|
436
|
+
getChangeCursor: this.internalgetChangeCursor.bind(this),
|
|
437
|
+
getClockWriter: this.internalgetClockWriter.bind(this),
|
|
438
|
+
getComponentChange: this.internalgetComponentChange.bind(this),
|
|
439
|
+
getComponentMutationEpoch: this.internalgetComponentMutationEpoch.bind(this),
|
|
440
|
+
getEntityArchetype: this.internalgetEntityArchetype.bind(this),
|
|
441
|
+
getFixedAccumulator: this.internalgetFixedAccumulator.bind(this),
|
|
442
|
+
getFreeIndices: this.internalgetFreeIndices.bind(this),
|
|
443
|
+
getGraph: this.internalgetGraph.bind(this),
|
|
444
|
+
getMutationEpoch: this.internalgetMutationEpoch.bind(this),
|
|
445
|
+
getQueryRow: this.internalgetQueryRow.bind(this),
|
|
446
|
+
getRecords: this.internalgetRecords.bind(this),
|
|
447
|
+
getRelationshipEpoch: this.internalgetRelationshipEpoch.bind(this),
|
|
448
|
+
getRelationshipTargetEntities: this.internalgetRelationshipTargetEntities.bind(this),
|
|
449
|
+
getResources: this.internalgetResources.bind(this),
|
|
450
|
+
getSchedule: this.internalgetSchedule.bind(this),
|
|
451
|
+
getSchedules: this.internalgetSchedules.bind(this),
|
|
452
|
+
getSharedRefs: this.internalgetSharedRefs.bind(this),
|
|
453
|
+
getStructureEpoch: this.internalgetStructureEpoch.bind(this),
|
|
454
|
+
getUniqueRefs: this.internalgetUniqueRefs.bind(this),
|
|
455
|
+
lookupAlive: this.internallookupAlive.bind(this),
|
|
456
|
+
markComponentAdded: this.internalmarkComponentAdded.bind(this),
|
|
457
|
+
markComponentChanged: this.internalmarkComponentChanged.bind(this),
|
|
458
|
+
markComponentRangeChanged: this.internalmarkComponentRangeChanged.bind(this),
|
|
459
|
+
markComponentsAdded: this.internalmarkComponentsAdded.bind(this),
|
|
460
|
+
markDerivedComponentChanges: this.internalmarkDerivedComponentChanges.bind(this),
|
|
461
|
+
markStructureChanged: this.internalmarkStructureChanged.bind(this),
|
|
462
|
+
materializePendingEntity: this.internalmaterializePendingEntity.bind(this),
|
|
463
|
+
nextMutationEpoch: this.internalnextMutationEpoch.bind(this),
|
|
464
|
+
poisonExecution: this.internalpoisonExecution.bind(this),
|
|
465
|
+
preflightComponentData: this.internalpreflightComponentData.bind(this),
|
|
466
|
+
readChangesSince: this.internalreadChangesSince.bind(this),
|
|
467
|
+
readRow: this.internalreadRow.bind(this),
|
|
468
|
+
recordIsLive: this.internalrecordIsLive.bind(this),
|
|
469
|
+
relationshipOnInsert: this.internalrelationshipOnInsert.bind(this),
|
|
470
|
+
relationshipOnRemove: this.internalrelationshipOnRemove.bind(this),
|
|
471
|
+
releaseManagedRefsOnRow: this.internalreleaseManagedRefsOnRow.bind(this),
|
|
472
|
+
removeComponentChange: this.internalremoveComponentChange.bind(this),
|
|
473
|
+
removeComponentCore: this.internalremoveComponentCore.bind(this),
|
|
474
|
+
removeEntityChanges: this.internalremoveEntityChanges.bind(this),
|
|
475
|
+
routeError: this.internalrouteError.bind(this),
|
|
476
|
+
setFixedAccumulator: this.internalsetFixedAccumulator.bind(this),
|
|
477
|
+
setQueryRow: this.internalsetQueryRow.bind(this),
|
|
478
|
+
spawnCore: this.internalspawnCore.bind(this),
|
|
479
|
+
writeEntitySelf: this.internalwriteEntitySelf.bind(this),
|
|
480
|
+
writeRow: this.internalwriteRow.bind(this),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Immutable integrity state for execution coordinators and headless callers. */
|
|
485
|
+
get execution(): WorldExecutionState {
|
|
486
|
+
return this.executionState;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Resolve a schedule token owned by this World realm without package singleton identity. */
|
|
490
|
+
scheduleToken(
|
|
491
|
+
name: import('./schedule-token').ScheduleName,
|
|
492
|
+
): import('./schedule-token').ScheduleToken {
|
|
493
|
+
if (name === 'Update') return Update;
|
|
494
|
+
if (name === 'FixedUpdate') return FixedUpdate;
|
|
495
|
+
return FixedUpdate;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** SharedKernel is the only writer; application code recovers by constructing a new World. */
|
|
499
|
+
private internalpoisonExecution(fault: WorldExecutionFault): void {
|
|
500
|
+
if (this.executionState.health === 'healthy') {
|
|
501
|
+
this.executionState = poisonedWorldExecutionState(this.identity, fault);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
query<
|
|
506
|
+
const R extends readonly Component[] = readonly [],
|
|
507
|
+
const W extends readonly Component[] = readonly [],
|
|
508
|
+
const O extends readonly Component[] = readonly [],
|
|
509
|
+
>(descriptor: QueryDescriptor<R, W, O>): Result<Query<R, W, O>, QueryCreationError> {
|
|
510
|
+
return createQuery(this, descriptor);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
514
|
+
// Internal access — query engine
|
|
515
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
/** Expose archetype graph for query engine. Not part of public API. */
|
|
518
|
+
private internalgetGraph(): ArchetypeGraph {
|
|
519
|
+
return this.graph;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private componentIsInUse(component: Component): boolean {
|
|
523
|
+
if (
|
|
524
|
+
this.graph.archetypes.some(
|
|
525
|
+
(archetype) =>
|
|
526
|
+
archetype.size > 0 && archetype.components.some((candidate) => candidate === component),
|
|
527
|
+
)
|
|
528
|
+
) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
return worldScheduleUsesComponent(this, component);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Current upper bound for mutation observation. */
|
|
535
|
+
private internalgetMutationEpoch(): number {
|
|
536
|
+
return this.mutationEpoch;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Current cursor for a persistent projection subscriber. */
|
|
540
|
+
private internalgetChangeCursor(): number {
|
|
541
|
+
return this.changeJournal.cursor();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Read complete mutation evidence or an explicit rebuild signal. */
|
|
545
|
+
private internalreadChangesSince(cursor: number): WorldChangeRead {
|
|
546
|
+
return this.changeJournal.readAfter(cursor);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Structure snapshot used to invalidate borrowed query facades. */
|
|
550
|
+
private internalgetStructureEpoch(): number {
|
|
551
|
+
return this.structureEpoch;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Resolve current logical identity for a packed entity handle. */
|
|
555
|
+
private internalgetEntityArchetype(entity: EntityHandle): Archetype | undefined {
|
|
556
|
+
const record = this.records[entityIndex(entity)];
|
|
557
|
+
if (!this.internalrecordIsLive(record, entityGeneration(entity))) return undefined;
|
|
558
|
+
return this.graph.archetypes[record.archetypeId];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Component change state for query filters. */
|
|
562
|
+
private internalgetComponentChange(
|
|
563
|
+
entity: EntityHandle,
|
|
564
|
+
componentId: number,
|
|
565
|
+
): ChangeTicks | undefined {
|
|
566
|
+
const record = this.records[entityIndex(entity)];
|
|
567
|
+
if (!this.internalrecordIsLive(record, entityGeneration(entity))) return undefined;
|
|
568
|
+
return readComponentChange(this.graph, record, entity, componentId);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** Allocate one epoch after a mutation has succeeded. */
|
|
572
|
+
private internalnextMutationEpoch(): number {
|
|
573
|
+
if (this.mutationEpoch >= Number.MAX_SAFE_INTEGER) {
|
|
574
|
+
throw new ChangeEpochExhaustedError(this.mutationEpoch);
|
|
575
|
+
}
|
|
576
|
+
this.mutationEpoch += 1;
|
|
577
|
+
return this.mutationEpoch;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Record one successful structural mutation. */
|
|
581
|
+
private internalmarkStructureChanged(): void {
|
|
582
|
+
this.structureEpoch += 1;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Current structural revision for mounted World projections. */
|
|
586
|
+
getStructureEpoch(): number {
|
|
587
|
+
return this.structureEpoch;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Mark a component as both added and changed at the current tick. */
|
|
591
|
+
private internalmarkComponentAdded(entity: EntityHandle, componentId: number): void {
|
|
592
|
+
this.internalmarkComponentsAdded(entity, [componentId]);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Mark one mutation's component instances with a shared epoch. */
|
|
596
|
+
private internalmarkComponentsAdded(entity: EntityHandle, componentIds: readonly number[]): void {
|
|
597
|
+
const record = this.records[entityIndex(entity)];
|
|
598
|
+
if (!this.internalrecordIsLive(record, entityGeneration(entity))) return;
|
|
599
|
+
const epoch = this.internalnextMutationEpoch();
|
|
600
|
+
markComponentsAdded(this.graph, record, entity, componentIds, epoch);
|
|
601
|
+
for (const componentId of componentIds) {
|
|
602
|
+
this.componentMutationEpochs[componentId] = epoch;
|
|
603
|
+
this.changeJournal.append({ kind: 'component-added', entity, componentId });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Mark an existing component as changed at the current tick. */
|
|
608
|
+
private internalmarkComponentChanged(entity: EntityHandle, componentId: number): void {
|
|
609
|
+
const record = this.records[entityIndex(entity)];
|
|
610
|
+
if (!this.internalrecordIsLive(record, entityGeneration(entity))) return;
|
|
611
|
+
let epoch: number | undefined;
|
|
612
|
+
markComponentChanged(this.graph, record, entity, componentId, () => {
|
|
613
|
+
epoch = this.internalnextMutationEpoch();
|
|
614
|
+
return epoch;
|
|
615
|
+
});
|
|
616
|
+
if (epoch !== undefined) {
|
|
617
|
+
this.componentMutationEpochs[componentId] = epoch;
|
|
618
|
+
this.changeJournal.append({ kind: 'component-changed', entity, componentId });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Mark one contiguous component range with a single epoch. */
|
|
623
|
+
private internalmarkComponentRangeChanged(
|
|
624
|
+
table: ArchetypeGraph['tables'][number],
|
|
625
|
+
componentId: number,
|
|
626
|
+
rowStart: number,
|
|
627
|
+
rowCount: number,
|
|
628
|
+
): void {
|
|
629
|
+
const epochs = table.storage.get(componentId)?.epochs;
|
|
630
|
+
if (epochs === undefined || rowCount === 0) return;
|
|
631
|
+
const epoch = this.internalnextMutationEpoch();
|
|
632
|
+
epochs.changed.fill(epoch, rowStart, rowStart + rowCount);
|
|
633
|
+
this.componentMutationEpochs[componentId] = epoch;
|
|
634
|
+
for (const entity of readTableEntityRange(table, rowStart, rowCount)) {
|
|
635
|
+
this.changeJournal.append({
|
|
636
|
+
kind: 'component-changed',
|
|
637
|
+
entity,
|
|
638
|
+
componentId,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Latest mutation token for one component-owned projection. */
|
|
644
|
+
private internalgetComponentMutationEpoch(componentId: number): number {
|
|
645
|
+
return this.componentMutationEpochs[componentId] ?? 0;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Read a materialized relationship target in O(1 + k). */
|
|
649
|
+
private internalgetRelationshipTargetEntities(
|
|
650
|
+
source: Component,
|
|
651
|
+
target: EntityHandle,
|
|
652
|
+
): readonly EntityHandle[] {
|
|
653
|
+
return this.componentAccess.relationshipTargetEntries(source, target);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Monotonic epoch for the materialized relationship index. */
|
|
657
|
+
private internalgetRelationshipEpoch(source: Component): number {
|
|
658
|
+
return this.core.relationshipIndexes.get(componentId(source))?.epoch ?? 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Publish changes to a value derived without an authored mutation epoch. */
|
|
662
|
+
private internalmarkDerivedComponentChanges(
|
|
663
|
+
componentId: number,
|
|
664
|
+
entities: Iterable<EntityHandle>,
|
|
665
|
+
): void {
|
|
666
|
+
for (const entity of entities) {
|
|
667
|
+
this.changeJournal.append({ kind: 'derived-component-changed', entity, componentId });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Query facade write after the facade has already marked evidence. */
|
|
672
|
+
private internalsetQueryRow(
|
|
673
|
+
entity: EntityHandle,
|
|
674
|
+
component: Component,
|
|
675
|
+
value: Record<string, unknown>,
|
|
676
|
+
): Result<void, EcsError> {
|
|
677
|
+
return this.componentAccess.set(entity, component, value as never, false);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** Query facade read that does not re-enter the public World API. */
|
|
681
|
+
private internalgetQueryRow(
|
|
682
|
+
entity: EntityHandle,
|
|
683
|
+
component: Component,
|
|
684
|
+
): Result<Record<string, unknown>, EcsError> {
|
|
685
|
+
return this.componentAccess.get(entity, component) as Result<Record<string, unknown>, EcsError>;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** Remove one component's change state after archetype removal. */
|
|
689
|
+
private internalremoveComponentChange(entity: EntityHandle, componentId: number): void {
|
|
690
|
+
this.changeJournal.append({ kind: 'component-removed', entity, componentId });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Remove all change state before an entity handle is retired. */
|
|
694
|
+
private internalremoveEntityChanges(entity: EntityHandle): void {
|
|
695
|
+
this.changeJournal.append({ kind: 'entity-removed', entity });
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Return resource change ticks for diagnostics and resource-driven systems. */
|
|
699
|
+
getResourceChange(name: string): ChangeTicks | undefined {
|
|
700
|
+
const entry = this.resources.entries.get(name);
|
|
701
|
+
return entry === undefined ? undefined : { added: entry.added, changed: entry.changed };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Route a structured error from
|
|
706
|
+
* an engine-internal subsystem (e.g. RenderSystem extract stage, w15).
|
|
707
|
+
*
|
|
708
|
+
* Mirrors the private `errorHandler(err, ctx)` call sites inside `World`
|
|
709
|
+
* itself; the dedicated accessor avoids exposing `errorHandler` directly
|
|
710
|
+
* and keeps the routing contract under the `_xxx` `@internal` umbrella so
|
|
711
|
+
* AI users do not discover it through IDE autocomplete on `World`.
|
|
712
|
+
*
|
|
713
|
+
* Not part of the public API.
|
|
714
|
+
*/
|
|
715
|
+
private internalrouteError(err: EcsError, ctx?: { readonly systemName: string }): void {
|
|
716
|
+
// Internal expected failures are reported without becoming a second
|
|
717
|
+
// schedule or terminal hook. The host owns fatal frame policy.
|
|
718
|
+
console.error(`[${ctx?.systemName ?? 'World'}]`, err);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
722
|
+
// accessors — M1 extraction seam
|
|
723
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
724
|
+
|
|
725
|
+
/** */ private internalgetRecords(): EntityRecord[] {
|
|
726
|
+
return this.records;
|
|
727
|
+
}
|
|
728
|
+
/** */ private internalgetFreeIndices(): number[] {
|
|
729
|
+
return this.freeIndices;
|
|
730
|
+
}
|
|
731
|
+
/** */ private internalgetResources(): ResourceStore {
|
|
732
|
+
return this.resources;
|
|
733
|
+
}
|
|
734
|
+
/** */ private internalgetFixedAccumulator(): number {
|
|
735
|
+
return this.fixedAccumulator;
|
|
736
|
+
}
|
|
737
|
+
/** */ private internalsetFixedAccumulator(value: number): void {
|
|
738
|
+
this.fixedAccumulator = value;
|
|
739
|
+
}
|
|
740
|
+
/** */ private internalgetUniqueRefs(): UniqueRefStore {
|
|
741
|
+
return this.uniqueRefs;
|
|
742
|
+
}
|
|
743
|
+
/** */ private internalgetBufferPool(): BufferPool {
|
|
744
|
+
return this.bufferPool;
|
|
745
|
+
}
|
|
746
|
+
/** Scheduler-owned mutable clock capability. */
|
|
747
|
+
private internalgetClockWriter(): ClockWriter {
|
|
748
|
+
return this.clock.writer;
|
|
749
|
+
}
|
|
750
|
+
/** */ private internalgetSchedule(
|
|
751
|
+
token: import('./schedule-token').ScheduleToken,
|
|
752
|
+
): Schedule | undefined {
|
|
753
|
+
return this.schedules.get(token);
|
|
754
|
+
}
|
|
755
|
+
/** */ private internalgetSchedules(): ReadonlyMap<
|
|
756
|
+
import('./schedule-token').ScheduleToken,
|
|
757
|
+
Schedule
|
|
758
|
+
> {
|
|
759
|
+
return this.schedules;
|
|
760
|
+
}
|
|
761
|
+
/** */ private internalgetSharedRefs(): SharedRefStore {
|
|
762
|
+
return this.sharedRefs;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
766
|
+
// System registration + update (M3)
|
|
767
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Register a system with query descriptor and optional ordering constraints.
|
|
771
|
+
*
|
|
772
|
+
* `const Qs` mirrors the free `addSystem` signature so the call-site
|
|
773
|
+
* `queries` tuple is locked literal-form, letting `descriptor.fn`'s first
|
|
774
|
+
* parameter recover per-query row access shapes (S-5, KD-3 — class method
|
|
775
|
+
* generic, not free function double track).
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```ts
|
|
779
|
+
* const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
|
|
780
|
+
* const world = new World();
|
|
781
|
+
* world.addSystem(Update, {
|
|
782
|
+
* name: 'read-pos',
|
|
783
|
+
* queries: [{ with: [Position] }],
|
|
784
|
+
* fn: (world, queries) => { void world; for (const row of queries[0]) { void row.entity; } },
|
|
785
|
+
* });
|
|
786
|
+
* ```
|
|
787
|
+
*/
|
|
788
|
+
addSystem<const Qs extends ReadonlyArray<QueryDescriptor>>(
|
|
789
|
+
schedule: import('./schedule-token').ScheduleToken,
|
|
790
|
+
descriptor: SystemDescriptor<Qs>,
|
|
791
|
+
): Result<void, ScheduleScopeMismatchError> {
|
|
792
|
+
return worldAddSystem(this, schedule, descriptor);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Remove a registered system by name (M2 — plan-strategy D-3).
|
|
797
|
+
*
|
|
798
|
+
* Returns `Result<void, ScheduleMutationError>`:
|
|
799
|
+
* - ok branch: the slot is dropped and the schedule will rebuild on the
|
|
800
|
+
* next `update()`.
|
|
801
|
+
* - err branch with `.code === 'system-before-unknown'`: no system carries
|
|
802
|
+
* this name; `.detail.candidates` lists the registered names.
|
|
803
|
+
*
|
|
804
|
+
* Designed to support `@forgeax/engine-remote`'s typed `injectSystem` /
|
|
805
|
+
* `removeSystem` channel and the WS-disconnect reverse-remove path.
|
|
806
|
+
*
|
|
807
|
+
* @example
|
|
808
|
+
* ```ts
|
|
809
|
+
* const r = world.removeSystem(Update, 'movement');
|
|
810
|
+
* if (!r.ok) console.error(r.error.code, r.error.detail.candidates);
|
|
811
|
+
* ```
|
|
812
|
+
*/
|
|
813
|
+
removeSystem(
|
|
814
|
+
schedule: import('./schedule-token').ScheduleToken,
|
|
815
|
+
name: string,
|
|
816
|
+
): Result<void, ScheduleMutationError | ScheduleScopeMismatchError> {
|
|
817
|
+
return worldRemoveSystem(this, schedule, name);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Replace a registered system in-place (M2 — plan-strategy D-3 atomic semantics).
|
|
822
|
+
*
|
|
823
|
+
* Overwrites the descriptor stored under `name` while preserving the
|
|
824
|
+
* registration slot — `before / after` references that target this name
|
|
825
|
+
* remain bound.
|
|
826
|
+
*
|
|
827
|
+
* Returns `Result<void, ScheduleMutationError>`:
|
|
828
|
+
* - ok branch: descriptor swapped, schedule marked dirty.
|
|
829
|
+
* - err branch with `.code === 'system-before-unknown'`: no system carries
|
|
830
|
+
* this name; use `addSystem(descriptor)` to register a new one instead.
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* ```ts
|
|
834
|
+
* const r = world.replaceSystem(Update, 'movement', {
|
|
835
|
+
* name: 'movement',
|
|
836
|
+
* queries: [{ with: [Position] }],
|
|
837
|
+
* fn: (world, queryResults) => { ... },
|
|
838
|
+
* });
|
|
839
|
+
* ```
|
|
840
|
+
*/
|
|
841
|
+
replaceSystem<const Qs extends ReadonlyArray<QueryDescriptor>>(
|
|
842
|
+
schedule: import('./schedule-token').ScheduleToken,
|
|
843
|
+
name: string,
|
|
844
|
+
descriptor: SystemDescriptor<Qs>,
|
|
845
|
+
): Result<void, ScheduleMutationError | ScheduleScopeMismatchError> {
|
|
846
|
+
return worldReplaceSystem(this, schedule, name, descriptor);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Batch-register systems to a set. Validates the set token before writing.
|
|
851
|
+
*
|
|
852
|
+
* - First call for a system name: registers it via the existing `addSystem` path.
|
|
853
|
+
* - Subsequent calls: only adds the system name to the set's members (dedup).
|
|
854
|
+
*
|
|
855
|
+
* Returns `Result.err` with `SystemSetNotRegisteredError` if the set token
|
|
856
|
+
* fails identity validation.
|
|
857
|
+
*
|
|
858
|
+
* @example
|
|
859
|
+
* ```ts
|
|
860
|
+
* const GameplaySet = defineSystemSet({ name: 'gameplay' });
|
|
861
|
+
* const world = new World();
|
|
862
|
+
* const r = world.addSystems(Update, GameplaySet, [movement, collision]);
|
|
863
|
+
* if (!r.ok) console.error(r.error.code, r.error.hint);
|
|
864
|
+
* ```
|
|
865
|
+
*/
|
|
866
|
+
addSystems<const Qs extends ReadonlyArray<QueryDescriptor>>(
|
|
867
|
+
schedule: import('./schedule-token').ScheduleToken,
|
|
868
|
+
set: SystemSet,
|
|
869
|
+
systems: ReadonlyArray<SystemDescriptor<Qs>>,
|
|
870
|
+
): Result<void, SystemSetNotRegisteredError | ScheduleScopeMismatchError> {
|
|
871
|
+
return worldAddSystems(this, schedule, set, systems);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Execute one frame: run all systems in DAG order, then flush deferred commands.
|
|
876
|
+
* Empty world (no systems) completes silently (E-09).
|
|
877
|
+
*
|
|
878
|
+
* @example
|
|
879
|
+
* ```ts
|
|
880
|
+
* const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
|
|
881
|
+
* const world = new World();
|
|
882
|
+
* world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
|
|
883
|
+
* world.update(); // run all systems + flush commands
|
|
884
|
+
* ```
|
|
885
|
+
*/
|
|
886
|
+
update(
|
|
887
|
+
deltaSeconds = 0,
|
|
888
|
+
): Result<
|
|
889
|
+
void,
|
|
890
|
+
| TimeDeltaInvalidError
|
|
891
|
+
| TimeConfigInvalidError
|
|
892
|
+
| ScheduleScopeMismatchError
|
|
893
|
+
| WorldPoisonedError
|
|
894
|
+
| CommandFailedError
|
|
895
|
+
| SystemFailedError
|
|
896
|
+
| import('./errors').CyclicDependencyError
|
|
897
|
+
| SharedKernelFailureError
|
|
898
|
+
> {
|
|
899
|
+
return worldUpdate(this, deltaSeconds);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
903
|
+
// Resource CRUD (M3)
|
|
904
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
905
|
+
|
|
906
|
+
/** Insert or overwrite a resource (idempotent, E-13). */
|
|
907
|
+
insertResource<T>(key: string | { readonly name: string }, value: T): void {
|
|
908
|
+
worldInsertResource(this, key, value);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Get a resource by key.
|
|
913
|
+
* @throws ResourceNotFoundError if key not found (E-14).
|
|
914
|
+
*/
|
|
915
|
+
getResource(key: typeof import('./time').Time): import('./time').TimeResource;
|
|
916
|
+
getResource(key: typeof import('./time').FixedTime): import('./time').FixedTimeResource;
|
|
917
|
+
getResource<T>(key: string | { readonly name: string }): T;
|
|
918
|
+
getResource<T>(key: string | { readonly name: string }): T {
|
|
919
|
+
return worldGetResource<T>(this, key);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** Check if a resource exists. */
|
|
923
|
+
hasResource(key: string | { readonly name: string }): boolean {
|
|
924
|
+
return worldHasResource(this, key);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Remove a resource by key. */
|
|
928
|
+
removeResource(key: string | { readonly name: string }): void {
|
|
929
|
+
worldRemoveResource(this, key);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
933
|
+
// Inspection / diagnostics (M4)
|
|
934
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Return a typed diagnostic snapshot of the World state.
|
|
938
|
+
* All fields are non-undefined. Useful for AI users to programmatically
|
|
939
|
+
* introspect entity count, archetypes, registered components, systems,
|
|
940
|
+
* and resources without console.log or a debugger.
|
|
941
|
+
*
|
|
942
|
+
* @example
|
|
943
|
+
* ```ts
|
|
944
|
+
* const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
|
|
945
|
+
* const world = new World();
|
|
946
|
+
* world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
|
|
947
|
+
* const snap = world.inspect();
|
|
948
|
+
* console.log(snap.entityCount, snap.activeComponents);
|
|
949
|
+
* ```
|
|
950
|
+
*/
|
|
951
|
+
inspect(): WorldInspection {
|
|
952
|
+
return detachWorldInspection(worldInspect(this));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** Return the registered schedule graphs and their declared access metadata. */
|
|
956
|
+
scheduleData(): ReadonlyArray<WorldScheduleData> {
|
|
957
|
+
return worldScheduleData(this);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
961
|
+
// Managed-ref public API (feat-20260528-rapier-physics M1 / t4)
|
|
962
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Allocate a standalone managed reference handle with an optional release
|
|
966
|
+
* callback. Returns a branded {@link Handle}<Target, 'unique'> that can be
|
|
967
|
+
* stored in schema-vocab `ref<T>` fields or resolved through
|
|
968
|
+
* {@link UniqueRefStore.resolve} (via `world.get` on a component with
|
|
969
|
+
* `ref<T>` fields).
|
|
970
|
+
*
|
|
971
|
+
* When the handle is released (despawn / removeComponent / set-overwrite),
|
|
972
|
+
* the `onRelease` callback fires with the payload (captured on the stack);
|
|
973
|
+
* by then the slot's bookkeeping (callback table, payload map, freelist) is
|
|
974
|
+
* already cleared, so a *throwing* `onRelease` re-propagates from the first
|
|
975
|
+
* `release` call without leaving the store inconsistent — a second `release`
|
|
976
|
+
* of the same handle returns `UniqueRefDoubleReleaseError` as expected. RAII
|
|
977
|
+
* cleanup semantics preserved (plan-strategy D-5; throw-safety AC-01/02).
|
|
978
|
+
*
|
|
979
|
+
* Handles are *operational, not persistent*: caching them across release
|
|
980
|
+
* boundaries (despawn / removeComponent / set-overwrite) is undefined
|
|
981
|
+
* behavior — the same `u32` may silently resolve to a freshly allocated
|
|
982
|
+
* payload after slot reuse. See `packages/ecs/README.md` § "Managed handles
|
|
983
|
+
* are operational, not persistent" and `docs/specs/2026-06-14-ecs-managed-
|
|
984
|
+
* lifecycle-ssot-design.md` § 3.3.
|
|
985
|
+
*
|
|
986
|
+
* @typeParam Target - phantom string branding the handle (type-level only).
|
|
987
|
+
* @typeParam T - the payload type stored alongside the handle.
|
|
988
|
+
* @param target - phantom target string (type-level discriminant).
|
|
989
|
+
* @param payload - the value to store. Identity-stable until release.
|
|
990
|
+
* @param onRelease - optional cleanup hook called with the payload on release.
|
|
991
|
+
* @returns a branded `Handle<Target, 'unique'>` u32.
|
|
992
|
+
*
|
|
993
|
+
* @example
|
|
994
|
+
* ```ts
|
|
995
|
+
* const world = new World();
|
|
996
|
+
* const handle = world.allocUniqueRef<'PhysicsBody', RigidBodyHandle>(
|
|
997
|
+
* 'PhysicsBody',
|
|
998
|
+
* rapierHandle,
|
|
999
|
+
* (h) => rapierWorld.removeRigidBody(h),
|
|
1000
|
+
* );
|
|
1001
|
+
* const Holder = defineComponent('Holder', { body: 'unique<PhysicsBody>' });
|
|
1002
|
+
* world.spawn(Holder, { body: handle });
|
|
1003
|
+
* // Despawn triggers onRelease -> Rapier body is cleaned up.
|
|
1004
|
+
* ```
|
|
1005
|
+
*/
|
|
1006
|
+
allocUniqueRef<Target extends string, T>(
|
|
1007
|
+
target: Target,
|
|
1008
|
+
payload: T,
|
|
1009
|
+
onRelease?: (payload: T) => void,
|
|
1010
|
+
): Handle<Target, 'unique'> {
|
|
1011
|
+
return worldAllocUniqueRef(this, target, payload, onRelease);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Allocate a shared (refcount-tracked) handle through the per-World
|
|
1016
|
+
* {@link SharedRefStore}. Returns a `Handle<Target, 'shared'>` u32 with
|
|
1017
|
+
* rc=1 (the alloc-grant). Consumers retain/release via `world.sharedRefs`.
|
|
1018
|
+
*
|
|
1019
|
+
* Final release publishes structured evidence through the owning
|
|
1020
|
+
* {@link SharedRefStore}; payload disposal remains with the
|
|
1021
|
+
* renderer/assets/plugin owner and is not a user callback.
|
|
1022
|
+
*
|
|
1023
|
+
* Intended for asset-registry-style producers — anything whose lifecycle
|
|
1024
|
+
* is shared across multiple holders (ECS components + external systems).
|
|
1025
|
+
* The single-holder one-shot release pattern stays on
|
|
1026
|
+
* {@link World.allocUniqueRef} (`Handle<T, 'unique'>`).
|
|
1027
|
+
*
|
|
1028
|
+
* @typeParam Target - phantom string branding the handle (type-level only).
|
|
1029
|
+
* @typeParam T - the payload type stored alongside the handle.
|
|
1030
|
+
* @param target - phantom target string (type-level discriminant).
|
|
1031
|
+
* @param payload - the value to store. Identity-stable until final release.
|
|
1032
|
+
* @returns a branded `Handle<Target, 'shared'>` u32 with rc=1.
|
|
1033
|
+
*
|
|
1034
|
+
* @example
|
|
1035
|
+
* ```ts
|
|
1036
|
+
* const world = new World();
|
|
1037
|
+
* const handle = world.allocSharedRef<'MaterialAsset', MaterialPayload>(
|
|
1038
|
+
* 'MaterialAsset',
|
|
1039
|
+
* payload,
|
|
1040
|
+
* );
|
|
1041
|
+
* const M = defineComponent('M', { asset: 'shared<MaterialAsset>' });
|
|
1042
|
+
* world.spawn({ component: M, data: { asset: handle } });
|
|
1043
|
+
* // The write-barrier dispatch retains/releases automatically on spawn / despawn.
|
|
1044
|
+
* ```
|
|
1045
|
+
*/
|
|
1046
|
+
allocSharedRef<Target extends string, T>(target: Target, payload: T): Handle<Target, 'shared'> {
|
|
1047
|
+
return worldAllocSharedRef(this, target, payload);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Return one producer-owned shared handle per `(target, payload object)` in
|
|
1052
|
+
* this World. Repeated discovery does not retain; ECS holders still retain
|
|
1053
|
+
* and release through the normal write barrier. Asset catalogues use this
|
|
1054
|
+
* when repeated scene instantiation resolves the same catalogued payload.
|
|
1055
|
+
* Use {@link World.allocSharedRef} for independent resources or deleters.
|
|
1056
|
+
*/
|
|
1057
|
+
internSharedRef<Target extends string, T extends object>(
|
|
1058
|
+
target: Target,
|
|
1059
|
+
payload: T,
|
|
1060
|
+
): Handle<Target, 'shared'> {
|
|
1061
|
+
return worldInternSharedRef(this, target, payload);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1065
|
+
// Component access facade — storage ownership is world-component-access.
|
|
1066
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1067
|
+
|
|
1068
|
+
private relationshipTargetWriteError(
|
|
1069
|
+
component: Component,
|
|
1070
|
+
operation: string,
|
|
1071
|
+
): Result<never, EcsError> {
|
|
1072
|
+
return err(new RelationshipTargetReadonlyError(component.name, operation));
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
private relationshipTargetPayloadWrites(data: Readonly<Record<string, unknown>>): boolean {
|
|
1076
|
+
return Object.values(data).some((value) => {
|
|
1077
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
1078
|
+
if (ArrayBuffer.isView(value)) return value.byteLength > 0;
|
|
1079
|
+
return true;
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
get<S extends ComponentSchema>(
|
|
1084
|
+
entity: EntityHandle,
|
|
1085
|
+
component: Component<string, S>,
|
|
1086
|
+
): Result<ShapeOf<S>, EcsError> {
|
|
1087
|
+
return this.componentAccess.get(entity, component);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Test live component presence without constructing a Result error.
|
|
1092
|
+
*
|
|
1093
|
+
* Read projections commonly need to branch on optional components for many
|
|
1094
|
+
* entities. Calling `get` for that branch allocates a structured
|
|
1095
|
+
* ComponentNotPresentError on every ordinary miss (and StaleEntityError for
|
|
1096
|
+
* a dangling handle). This predicate is deliberately non-throwing and
|
|
1097
|
+
* returns false for both cases; callers that need the detailed error should
|
|
1098
|
+
* continue to use `get`.
|
|
1099
|
+
*/
|
|
1100
|
+
hasComponent(entity: EntityHandle, component: Component): boolean {
|
|
1101
|
+
const archetype = this.internalgetEntityArchetype(entity);
|
|
1102
|
+
return (
|
|
1103
|
+
archetype?.components.some(
|
|
1104
|
+
(candidate) => componentId(candidate) === componentId(component),
|
|
1105
|
+
) === true
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
private internalgetArrayView(
|
|
1110
|
+
entity: EntityHandle,
|
|
1111
|
+
component: Component,
|
|
1112
|
+
fieldName: string,
|
|
1113
|
+
): ArrayLike<number> | undefined {
|
|
1114
|
+
return this.componentAccess._getArrayView(entity, component, fieldName);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
set<S extends ComponentSchema, C extends Component<string, S>>(
|
|
1118
|
+
entity: EntityHandle,
|
|
1119
|
+
component: C & WritableComponent<C>,
|
|
1120
|
+
value: Partial<InputShapeOf<S>>,
|
|
1121
|
+
): Result<void, EcsError> {
|
|
1122
|
+
if (isRelationshipTarget(component)) return this.relationshipTargetWriteError(component, 'set');
|
|
1123
|
+
return this.componentAccess.set(entity, component, value);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
addComponent<S extends ComponentSchema, C extends Component<string, S>>(
|
|
1127
|
+
entity: EntityHandle,
|
|
1128
|
+
componentData: ComponentData<S> & { component: C & WritableComponent<C> },
|
|
1129
|
+
): Result<void, EcsError> {
|
|
1130
|
+
if (
|
|
1131
|
+
isRelationshipTarget(componentData.component) &&
|
|
1132
|
+
this.relationshipTargetPayloadWrites(componentData.data as Record<string, unknown>)
|
|
1133
|
+
)
|
|
1134
|
+
return this.relationshipTargetWriteError(componentData.component, 'addComponent');
|
|
1135
|
+
return this.componentAccess.addComponent(entity, componentData);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
private internaladdComponentCore<S extends ComponentSchema>(
|
|
1139
|
+
entity: EntityHandle,
|
|
1140
|
+
componentData: ComponentData<S>,
|
|
1141
|
+
internal: boolean,
|
|
1142
|
+
): Result<void, EcsError> {
|
|
1143
|
+
return this.componentAccess._addComponentCore(entity, componentData, internal);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
removeComponent<S extends ComponentSchema, C extends Component<string, S>>(
|
|
1147
|
+
entity: EntityHandle,
|
|
1148
|
+
component: C & WritableComponent<C>,
|
|
1149
|
+
): Result<void, EcsError> {
|
|
1150
|
+
if (isRelationshipTarget(component))
|
|
1151
|
+
return this.relationshipTargetWriteError(component, 'removeComponent');
|
|
1152
|
+
return this.componentAccess.removeComponent(entity, component);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
private internalremoveComponentCore<S extends ComponentSchema>(
|
|
1156
|
+
entity: EntityHandle,
|
|
1157
|
+
component: Component<string, S>,
|
|
1158
|
+
internal: boolean,
|
|
1159
|
+
): Result<void, EcsError> {
|
|
1160
|
+
return this.componentAccess._removeComponentCore(entity, component, internal);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private internalallocatePendingEntity(): EntityHandle {
|
|
1164
|
+
return this.componentAccess._allocatePendingEntity();
|
|
1165
|
+
}
|
|
1166
|
+
/** */ private internalcancelPendingEntity(entity: EntityHandle): void {
|
|
1167
|
+
this.componentAccess._cancelPendingEntity(entity);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private internalmaterializePendingEntity(
|
|
1171
|
+
entity: EntityHandle,
|
|
1172
|
+
componentDatas: ComponentData[],
|
|
1173
|
+
): Result<void, EcsError> {
|
|
1174
|
+
return this.componentAccess._materializePendingEntity(entity, componentDatas);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** Shared structural preflight for direct and deferred writes. */
|
|
1178
|
+
private internalpreflightComponentData(
|
|
1179
|
+
holder: EntityHandle | null,
|
|
1180
|
+
componentData: ComponentData,
|
|
1181
|
+
pendingEntities?: ReadonlySet<number>,
|
|
1182
|
+
unavailableEntities?: ReadonlySet<number>,
|
|
1183
|
+
): Result<void, EcsError> {
|
|
1184
|
+
return this.componentAccess.preflightComponentData(
|
|
1185
|
+
holder,
|
|
1186
|
+
componentData,
|
|
1187
|
+
pendingEntities,
|
|
1188
|
+
unavailableEntities,
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** */ private internalallocateIndex(): number {
|
|
1193
|
+
return this.componentAccess.allocateIndex();
|
|
1194
|
+
}
|
|
1195
|
+
/** */ private internalrecordIsLive(r: EntityRecord | undefined, g: number): r is EntityRecord {
|
|
1196
|
+
return this.componentAccess.recordIsLive(r, g);
|
|
1197
|
+
}
|
|
1198
|
+
/** */ private internallookupAlive(
|
|
1199
|
+
e: EntityHandle,
|
|
1200
|
+
op: string,
|
|
1201
|
+
c?: string,
|
|
1202
|
+
): Result<EntityRecord, EcsError> {
|
|
1203
|
+
return this.componentAccess.lookupAlive(e, op, c);
|
|
1204
|
+
}
|
|
1205
|
+
/** */ private internalreadRow<S extends ComponentSchema>(
|
|
1206
|
+
a: Archetype,
|
|
1207
|
+
c: Component<string, S>,
|
|
1208
|
+
r: number,
|
|
1209
|
+
): ShapeOf<S> {
|
|
1210
|
+
return this.componentAccess.readRow(a, c, r);
|
|
1211
|
+
}
|
|
1212
|
+
/** */ private internalwriteEntitySelf(a: Archetype, r: number, h: EntityHandle): void {
|
|
1213
|
+
this.componentAccess.writeEntitySelf(a, r, h);
|
|
1214
|
+
}
|
|
1215
|
+
/** */ private internalwriteRow<S extends ComponentSchema>(
|
|
1216
|
+
a: Archetype,
|
|
1217
|
+
c: Component<string, S>,
|
|
1218
|
+
r: number,
|
|
1219
|
+
v: ShapeOf<S>,
|
|
1220
|
+
): void {
|
|
1221
|
+
this.componentAccess.writeRow(a, c, r, v);
|
|
1222
|
+
}
|
|
1223
|
+
/** */ private internalreleaseManagedRefsOnRow(a: Archetype, c: Component, r: number): void {
|
|
1224
|
+
this.componentAccess.releaseManagedRefsOnRow(a, c, r);
|
|
1225
|
+
}
|
|
1226
|
+
/** */ private internalrelationshipOnInsert(
|
|
1227
|
+
h: EntityHandle,
|
|
1228
|
+
c: Component,
|
|
1229
|
+
v: Record<string, unknown>,
|
|
1230
|
+
): Result<void, EcsError> {
|
|
1231
|
+
return this.componentAccess.relationshipOnInsert(h, c, v);
|
|
1232
|
+
}
|
|
1233
|
+
/** */ private internalrelationshipOnRemove(
|
|
1234
|
+
h: EntityHandle,
|
|
1235
|
+
c: Component,
|
|
1236
|
+
v: Record<string, unknown>,
|
|
1237
|
+
): Result<void, EcsError> {
|
|
1238
|
+
return this.componentAccess.relationshipOnRemove(h, c, v);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1242
|
+
// Spawn
|
|
1243
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Spawn an entity with one or more components.
|
|
1247
|
+
* Multi-component spawn directly targets the correct archetype (AC-06).
|
|
1248
|
+
*
|
|
1249
|
+
* @returns `Result<Entity, EcsError>` — `ok(Entity)` on success.
|
|
1250
|
+
* EntityIndexOverflowError still throws (build-time / infrastructure failure).
|
|
1251
|
+
*
|
|
1252
|
+
* @example
|
|
1253
|
+
* ```ts
|
|
1254
|
+
* const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
|
|
1255
|
+
* const world = new World();
|
|
1256
|
+
* const r = world.spawn({ component: Position, data: { x: 0, y: 0 } });
|
|
1257
|
+
* if (!r.ok) { console.error(r.error.code); return; }
|
|
1258
|
+
* const entity = r.value;
|
|
1259
|
+
* ```
|
|
1260
|
+
*/
|
|
1261
|
+
spawn<const SArr extends readonly ComponentSchema[]>(
|
|
1262
|
+
...componentDatas: {
|
|
1263
|
+
[K in keyof SArr]: {
|
|
1264
|
+
component: Component<string, SArr[K]>;
|
|
1265
|
+
data: Partial<InputShapeOf<SArr[K]>>;
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
): Result<EntityHandle, EcsError>;
|
|
1269
|
+
spawn(...componentDatas: ComponentData[]): Result<EntityHandle, EcsError> {
|
|
1270
|
+
const target = componentDatas.find(
|
|
1271
|
+
(data) =>
|
|
1272
|
+
isRelationshipTarget(data.component) &&
|
|
1273
|
+
this.relationshipTargetPayloadWrites(data.data as Record<string, unknown>),
|
|
1274
|
+
);
|
|
1275
|
+
if (target !== undefined) return this.relationshipTargetWriteError(target.component, 'spawn');
|
|
1276
|
+
return spawnCore(this, componentDatas, false);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Core implementation of `spawn` with reentry guard.
|
|
1281
|
+
*
|
|
1282
|
+
* @param internal — `true` when called from relationship maintenance
|
|
1283
|
+
* (lazy mirror create or exclusive reparent).
|
|
1284
|
+
*/
|
|
1285
|
+
private internalspawnCore(
|
|
1286
|
+
componentDatas: ComponentData[],
|
|
1287
|
+
internal: boolean,
|
|
1288
|
+
): Result<EntityHandle, EcsError> {
|
|
1289
|
+
return spawnCore(this, componentDatas, internal);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1293
|
+
// Despawn (D-08: generation retirement)
|
|
1294
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Despawn an entity. Stale handles are silently ignored (E-01, AC-17).
|
|
1298
|
+
* Generation retirement: gen=255 → index permanently retired (D-08/E-08).
|
|
1299
|
+
*
|
|
1300
|
+
* @returns `Result<void, EcsError>` — `ok(void)` always (idempotent on stale handles).
|
|
1301
|
+
*
|
|
1302
|
+
* @example
|
|
1303
|
+
* ```ts
|
|
1304
|
+
* const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
|
|
1305
|
+
* const world = new World();
|
|
1306
|
+
* const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
|
|
1307
|
+
* const r = world.despawn(e);
|
|
1308
|
+
* r.unwrap(); // idempotent: ok(void) even on stale handle
|
|
1309
|
+
* ```
|
|
1310
|
+
*/
|
|
1311
|
+
despawn(entity: EntityHandle): Result<void, EcsError> {
|
|
1312
|
+
return despawnCore(this, entity, false);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/** Despawn every live entity through the normal lifecycle and ref cleanup path. */
|
|
1316
|
+
despawnAll(): Result<void, EcsError> {
|
|
1317
|
+
const entities: EntityHandle[] = [];
|
|
1318
|
+
for (let index = 0; index < this.records.length; index += 1) {
|
|
1319
|
+
const record = this.records[index];
|
|
1320
|
+
if (record !== undefined && record.archetypeId >= 0) {
|
|
1321
|
+
entities.push(encodeEntity(index, record.generation));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
for (const entity of entities) {
|
|
1325
|
+
const result = this.despawn(entity);
|
|
1326
|
+
if (!result.ok) return result;
|
|
1327
|
+
}
|
|
1328
|
+
return ok(undefined);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Core implementation of `despawn` with reentry guard.
|
|
1333
|
+
*
|
|
1334
|
+
* @param internal — `true` when called from within linkedSpawn cascade.
|
|
1335
|
+
* Nested despawn skips relationship pruning after the parent is retired;
|
|
1336
|
+
* the linkedSpawn collection still walks the subtree so grandchildren
|
|
1337
|
+
* cascade correctly (tweak-20260714 M2, R-6).
|
|
1338
|
+
*/
|
|
1339
|
+
private internaldespawnCore(entity: EntityHandle, internal: boolean): Result<void, EcsError> {
|
|
1340
|
+
return despawnCore(this, entity, internal);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1344
|
+
// Hierarchy facade — lifecycle orchestration lives in world-entity-lifecycle.
|
|
1345
|
+
// Component storage still owns typed relationship mutation primitives.
|
|
1346
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1347
|
+
|
|
1348
|
+
addChild<S extends ComponentSchema>(
|
|
1349
|
+
parent: EntityHandle,
|
|
1350
|
+
child: EntityHandle,
|
|
1351
|
+
component: Component<string, S>,
|
|
1352
|
+
data: Partial<InputShapeOf<S>>,
|
|
1353
|
+
): Result<void, EcsError> {
|
|
1354
|
+
return worldAddChild(this, parent, child, component, data);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
removeChild<S extends ComponentSchema>(
|
|
1358
|
+
parent: EntityHandle,
|
|
1359
|
+
child: EntityHandle,
|
|
1360
|
+
component: Component<string, S>,
|
|
1361
|
+
): Result<void, EcsError> {
|
|
1362
|
+
return worldRemoveChild(this, parent, child, component);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
reparent<S extends ComponentSchema>(
|
|
1366
|
+
child: EntityHandle,
|
|
1367
|
+
newParent: EntityHandle,
|
|
1368
|
+
component: Component<string, S>,
|
|
1369
|
+
data: Partial<InputShapeOf<S>>,
|
|
1370
|
+
): Result<void, EcsError> {
|
|
1371
|
+
return worldReparent(this, child, newParent, component, data);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
iterAncestors(entity: EntityHandle): Iterable<EntityHandle> {
|
|
1375
|
+
return worldIterAncestors(this, entity);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
iterDescendants(entity: EntityHandle): Iterable<EntityHandle> {
|
|
1379
|
+
return worldIterDescendants(this, entity);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/** Freeze the detached POD produced by World.inspect(). */
|
|
1384
|
+
function detachWorldInspection<T extends object>(snapshot: T): Readonly<T> {
|
|
1385
|
+
return freezeInspection(snapshot);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function freezeInspection<T>(value: T): Readonly<T> {
|
|
1389
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
1390
|
+
return value as Readonly<T>;
|
|
1391
|
+
}
|
|
1392
|
+
for (const key of Reflect.ownKeys(value as object)) {
|
|
1393
|
+
const child = (value as Record<PropertyKey, unknown>)[key];
|
|
1394
|
+
if (child !== null && (typeof child === 'object' || typeof child === 'function')) {
|
|
1395
|
+
freezeInspection(child);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return Object.freeze(value) as Readonly<T>;
|
|
1399
|
+
}
|