From 4fc07286524bc3e1d8acb4c956b20a4782f12807 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Tue, 10 Nov 2026 14:40:01 +0900 Subject: [PATCH 1/3] Schema.bind takes the game's root, always A game bound without a root was branded by its schema type, which is covariant: a game and one whose schema is a superset were interchangeable, so a system, schedule, query, or entity id of one compiled against the other. The unrooted overloads are gone; a call without a root fails with a message that names the fix. Entity.isHandle takes a root, not a schema. defineRoot says why a root's name must be unique in the program: two roots of one name are one type. Co-Authored-By: Claude Opus 5.5 --- .changeset/root-mandatory.md | 35 +++++++++++ packages/core/dtslint/Roots.tst.ts | 88 ++++++++++++++++++++++++++++ packages/core/src/Entity.ts | 4 +- packages/core/src/Schema.ts | 94 +++++++++++++++++------------- packages/core/test/codecs.ts | 3 +- 5 files changed, 179 insertions(+), 45 deletions(-) create mode 100644 .changeset/root-mandatory.md create mode 100644 packages/core/dtslint/Roots.tst.ts diff --git a/.changeset/root-mandatory.md b/.changeset/root-mandatory.md new file mode 100644 index 00000000..51424c3a --- /dev/null +++ b/.changeset/root-mandatory.md @@ -0,0 +1,35 @@ +--- +"@typeonce/ritmo": minor +--- + +**Breaking: `Schema.bind` takes the game's root, always.** Without a root, a game was branded by its schema type, which is covariant: a game and a game whose schema is a superset were interchangeable, so a system, schedule, query, or entity id of one compiled in the other, and a system that spawned and queried `Velocity` ran inside a runtime with no `Velocity`. Declare the root with `Schema.defineRoot` and pass it last: + +```diff +-const Game = Schema.bind(Schema.fragment({ components: { Position } })) ++const Root = Schema.defineRoot("Game") ++const Game = Schema.bind(Schema.fragment({ components: { Position } }), Root) +``` + +```diff +-const Game = Schema.bind(Core, Combat) ++const Root = Schema.defineRoot("Game") ++const Game = Schema.bind(Core, Combat, Root) +``` + +This replaces 0.7's `Schema.merge` fix: `Schema.bind(A, B)` is now `Schema.bind(A, B, Root)`. A call without a root fails with `Schema.bind takes the game's root last: const Root = Schema.defineRoot('Game'), then Schema.bind(...fragments, Root)`. Values of games with different roots do not mix: the error names both, `Type 'RootToken<"Editor">' is not assignable to type 'RootToken<"Game">'`. + +The root's name is its identity: two `defineRoot("Game")` calls, in one module or in two, make the same type, and games bound under them mix as one game would. Declare one root per game, under a name no other game in the program uses, and import that constant where descriptors name it (`Entity.Handle`). + +Types a game spelled with the schema in the root's place name the root instead: + +```diff +-type Id = Entity.EntityId ++type Id = Entity.EntityId +``` + +The same goes for `Command.SpawnedId`, `Schema.Schema.BoundSchedule`, `Schema.Schema.BoundStateMachine`, `EntityScope.EntityScope`, and the other types that take a root. `Schema.Schema.Game` no longer defaults `Root` to `S`. `Entity.isHandle` takes the root, not the schema: + +```diff +-const isHandle = Entity.isHandle(Game.schema) ++const isHandle = Entity.isHandle(Root) +``` diff --git a/packages/core/dtslint/Roots.tst.ts b/packages/core/dtslint/Roots.tst.ts new file mode 100644 index 00000000..94217f14 --- /dev/null +++ b/packages/core/dtslint/Roots.tst.ts @@ -0,0 +1,88 @@ +import { Descriptor, Entity, Schema } from "@typeonce/ritmo" +import type * as SchemaTypes from "@typeonce/ritmo/Schema" +import { describe, expect, it } from "tstyche" + +// A game is its root: `Schema.bind` takes one last, and values of games with +// different roots do not mix, even when one schema contains the other. + +const Position = Descriptor.Component<{ x: number; y: number }>()("Roots/Position") +const Velocity = Descriptor.Component<{ x: number; y: number }>()("Roots/Velocity") +const Moving = Schema.fragment({ components: { Position, Velocity } }) +const Placed = Schema.fragment({ components: { Position } }) + +describe("Schema.bind", () => { + it("brands the game with the root passed last", () => { + const Root = Schema.defineRoot("Roots/Game") + expect(Root).type.toBe>() + expect(Schema.bind(Moving, Root)).type.toBe>>() + expect(Schema.bind(Placed, Schema.fragment({ components: { Velocity } }), Root).schema.components) + .type.toBe<{ readonly Position: typeof Position } & { readonly Velocity: typeof Velocity }>() + }) + + it("does not compile without a root, and names the fix", () => { + // @ts-expect-error Schema.bind takes the game's root last: const Root = Schema.defineRoot('Game'), then Schema.bind(...fragments, Root) + Schema.bind(Moving) + // @ts-expect-error Schema.bind takes the game's root last + Schema.bind(Placed, Schema.fragment({ components: { Velocity } })) + // @ts-expect-error Schema.bind takes the game's root last + Schema.bind(Placed, Schema.fragment({ components: { Velocity } }), Placed) + }) +}) + +describe("games with different roots", () => { + const RootA = Schema.defineRoot("Roots/A") + const RootB = Schema.defineRoot("Roots/B") + // B's schema is part of A's: without roots, the two would be one type. + const GameA = Schema.bind(Moving, RootA) + const GameB = Schema.bind(Placed, RootB) + const SystemOfA = GameA.System("Roots/MoveA", { + queries: { moving: GameA.Query({ selection: { position: GameA.Query.read(Position), velocity: GameA.Query.read(Velocity) } }) } + }, () => {}) + const runtimeB = GameB.Runtime.make({ services: GameB.Runtime.services(), resources: {} }).value + + it("do not share schedules, systems, or runtimes", () => { + runtimeB.tick(GameB.Schedule(GameB.System("Roots/B", {}, () => {}))) + // @ts-expect-error Type 'RootToken<"Roots/A">' is not assignable to type 'RootToken<"Roots/B">' + runtimeB.tick(GameA.Schedule(SystemOfA)) + // @ts-expect-error Type 'RootToken<"Roots/A">' is not assignable to type 'RootToken<"Roots/B">' + GameB.Schedule(SystemOfA) + }) + + it("do not share queries, entity ids, handles, or conditions", () => { + const PlacedA = GameA.Query({ selection: { position: GameA.Query.read(Position) } }) + const FlowA = GameA.StateMachine("Roots/FlowA", { Menu: {}, Playing: {} }) + // @ts-expect-error Type 'RootToken<"Roots/A">' is not assignable to type 'RootToken<"Roots/B">' + GameB.System("Roots/QueryOfA", { queries: { placed: PlacedA } }, () => {}) + // @ts-expect-error Type 'RootToken<"Roots/A">' is not assignable to type 'RootToken<"Roots/B">' + GameB.System("Roots/WhenOfA", { when: [GameA.Condition.inState(FlowA, "Menu")] }, () => {}) + GameA.System("Roots/Ids", { queries: { placed: PlacedA } }, ({ queries }) => { + for (const entity of queries.placed.each()) { + GameB.System("Roots/IdOfA", { queries: { placed: GameB.Query({ selection: { position: GameB.Query.read(Position) } }) } }, ({ queries }) => { + expect(queries.placed.get).type.not.toBeCallableWith(entity.id) + // @ts-expect-error Type 'RootToken<"Roots/A">' is not assignable to type 'RootToken<"Roots/B">' + queries.placed.get(entity.id) + }) + const handle = GameA.Entity.handle(entity) + expect(handle).type.toBe>() + expect(GameA.Entity.id).type.toBeCallableWith(handle) + // @ts-expect-error Type 'RootToken<"Roots/A">' is not assignable to type 'RootToken<"Roots/B">' + GameB.Entity.id(handle) + } + }) + }) + + it("name the root of a stored handle, not the schema", () => { + expect(Entity.isHandle(RootA)).type.toBe<(raw: unknown) => raw is Entity.Handle>() + expect(Entity.isHandle).type.not.toBeCallableWith(GameA.schema) + }) +}) + +describe("roots with the same name", () => { + it("are one type: a root's name must be unique in the program", () => { + // Why `defineRoot` asks for one root per game, under a name no other game uses. + const First = Schema.defineRoot("Roots/Same") + const Second = Schema.defineRoot("Roots/Same") + expect(Second).type.toBe() + expect(Schema.defineRoot("Roots/Other")).type.not.toBe() + }) +}) diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index 787d9efb..34764966 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -40,7 +40,7 @@ import type { Brand } from "./internal/brand.ts" import { makeHandle } from "./internal/entity.ts" import type { Descriptor } from "./Descriptor.ts" import type { StagedRelation } from "./Relation.ts" -import type { Schema } from "./Schema.ts" +import type { RootToken, Schema } from "./Schema.ts" /** * Entity identities, proofs, and long-lived handles. @@ -318,7 +318,7 @@ export const handle = < * ``` */ export const isHandle = < - Root, + Root extends RootToken, const Intent extends Descriptor<"component", string, any> | undefined = undefined >( _root: Root, diff --git a/packages/core/src/Schema.ts b/packages/core/src/Schema.ts index 71ea7af5..5a9b327a 100644 --- a/packages/core/src/Schema.ts +++ b/packages/core/src/Schema.ts @@ -337,10 +337,19 @@ export type ValidateRequires = } /** - * Creates one explicit root token for schema-bound long-lived references. + * Creates the root of one game, passed last to `bind` + * (`Schema.bind(Core, Root)`). Every system, schedule, query, entity id, + * handle, and runtime of the game carries it, so values of games with + * different roots do not mix. * - * Root tokens exist before schema construction so durable entity handles can be - * stored in descriptor payload types without widening to `Schema.Any`. + * A root exists before the game, so descriptors can name it: a stored entity + * handle is `Entity.Handle`, never widened to `Schema.Any`. + * + * Declare one root per game, under a name no other game in the program uses. + * The name is the root's identity: two `defineRoot("Game")` calls, in one + * module or in two, make the same type, and games bound under them mix as if + * they were one game. Share the one `Root` constant instead of declaring it + * again. * * @example * ```ts @@ -398,11 +407,24 @@ export const fragment = < }) as SchemaDefinition, PairsOf> /** - * Composes one or more fragments and returns one bound `Game` surface. + * What `bind` takes last: the game's root, made by `defineRoot`. A call + * without one fails with this message, which names the fix. + */ +type RootArgument = [Root] extends [RootToken] ? unknown : MissingRoot + +/** Names the fix when `bind` is called without a root. No value has this type. */ +type MissingRoot = { + readonly "Schema.bind takes the game's root last: const Root = Schema.defineRoot('Game'), then Schema.bind(...fragments, Root)": true +} + +/** + * Composes one or more fragments under the game's root and returns one bound + * `Game` surface. The root (`Schema.defineRoot("Game")`) is passed last. * - * Everything created from the returned object carries the same hidden root - * brand, so systems, schedules, and runtimes from different bound schemas - * cannot be connected accidentally. + * Everything created from the returned object carries the root's brand, so + * systems, schedules, queries, entity ids, handles, and runtimes of games + * with different roots cannot be connected. A root exists before the game, + * so descriptors can name it (stored handles: `Entity.Handle`). * * @example * ```ts @@ -413,11 +435,11 @@ export const fragment = < * const Game = Schema.bind(Core, Combat, Root) * ``` * - * Name the schema's type with an interface before binding it under the - * root, so type errors print `GameSchema` instead of every component, - * resource, and event of the game (`QueryEntity`), and - * helpers type it as `GameSchema`. Bind the fragments once without a root - * to merge them, or name a single `Schema.fragment(...)` the same way. + * Name the schema's type with an interface before binding the game, so type + * errors print `GameSchema` instead of every component, resource, and event + * of the game (`QueryEntity`), and helpers type it as + * `GameSchema`. Bind the fragments once to merge them, or name a single + * `Schema.fragment(...)` the same way. * * @example * ```ts @@ -425,46 +447,34 @@ export const fragment = < * const Combat = Schema.fragment({ components: { Health, Enemy }, events: { Hit } }) * const Root = Schema.defineRoot("Game") * // ---cut--- - * const schema = Schema.bind(Core, Combat).schema + * const schema = Schema.bind(Core, Combat, Root).schema * type SchemaOf = typeof schema * export interface GameSchema extends SchemaOf {} * const named: GameSchema = schema * export const Game = Schema.bind(named, Root) * ``` */ -export function bind< - S extends Schema.Any ->(schema: S & ValidateFragments<[S]> & ValidateRequires): Schema.Game -/** - * Binds one fragment under an explicit root (`Schema.defineRoot("Game")`), - * so descriptors can name the root before the game exists (stored handles: - * `Entity.Handle`). The usual way to bind a game. - */ export function bind< S extends Schema.Any, - const Name extends string ->(schema: S & ValidateFragments<[S]> & ValidateRequires, root: RootToken): Schema.Game> -/** - * Composes two or more fragments into one bound game; a descriptor - * registered by two fragments must be the same descriptor. Pass a root last - * to name it explicitly. - */ -export function bind< - const Fragments extends readonly [Schema.Any, Schema.Any, ...Array] ->(...fragments: Fragments & ValidateFragments & ValidateRequires>): Schema.Game, BuildFragments> + const Root +>(schema: S & ValidateFragments<[S]> & ValidateRequires, root: Root & RootArgument): Schema.Game /** - * Composes two or more fragments into one bound game under an explicit root - * (`Schema.bind(Core, Combat, Root)`). + * Composes two or more fragments into one bound game under its root + * (`Schema.bind(Core, Combat, Root)`); a descriptor registered by two + * fragments must be the same descriptor. */ export function bind< - const Fragments extends readonly [Schema.Any, Schema.Any, ...Array], - const Name extends string ->(...args: [...Fragments, RootToken] & ValidateFragments & ValidateRequires>): Schema.Game, RootToken> -export function bind(...args: ReadonlyArray) { - const maybeRoot = args[args.length - 1] - const fragments = (isRootToken(maybeRoot) ? args.slice(0, -1) : args) as Array - const schema = buildSchema(fragments) - return isRootToken(maybeRoot) ? makeGame(schema, maybeRoot) : makeGame(schema, schema) + const Fragments extends readonly [Schema.Any, ...Array], + const Root +>(...args: [...Fragments, Root & RootArgument] & ValidateFragments & ValidateRequires>): Schema.Game, Root> +/** A call without a root: it fails, and its message names the fix. */ +export function bind(schema: MissingRoot): never +export function bind(...args: ReadonlyArray) { + const root = args[args.length - 1] + if (!isRootToken(root)) { + throw new Error("Schema.bind takes the game's root last: const Root = Schema.defineRoot('Game'), then Schema.bind(...fragments, Root)") + } + return makeGame(buildSchema(args.slice(0, -1) as Array), root) } type RuntimeServicesOf> = @@ -691,7 +701,7 @@ export namespace Schema { /** * One fully bound public authoring surface. */ - export interface Game { + export interface Game { readonly schema: S /** * Creates a typed ownership scope for scene or level entities: a flat diff --git a/packages/core/test/codecs.ts b/packages/core/test/codecs.ts index 8b05edf6..75500472 100644 --- a/packages/core/test/codecs.ts +++ b/packages/core/test/codecs.ts @@ -8,6 +8,7 @@ import type * as Brand from "@typeonce/ritmo/Brand" import type { Codec, StandardSchema } from "@typeonce/ritmo/Descriptor" import * as Entity from "@typeonce/ritmo/Entity" import type { Descriptor } from "@typeonce/ritmo/Descriptor" +import type { RootToken } from "@typeonce/ritmo/Schema" type Issue = StandardSchema.Issue type Check = (raw: unknown) => { readonly value: T } | { readonly issues: ReadonlyArray } @@ -93,7 +94,7 @@ export const safeVector = vector as unknown as StandardSchema<{ readonly x: numb export const safeSize = size as unknown as StandardSchema<{ readonly width: number; readonly height: number }, SafeSize> /** A stored entity handle for schema root `Root`, built from `Entity.isHandle`. */ -export const handle = | undefined = undefined>( +export const handle = | undefined = undefined>( root: Root, intent?: Intent ): TestCodec> => { From 946c93be55199ce1c1617177c0e334f7ed74fd73 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Tue, 10 Nov 2026 15:07:12 +0900 Subject: [PATCH 2/3] Pass a root to every Schema.bind The mechanical part, by a script kept out of the repo (it is in the pull request): each unrooted call gets the file's root, declared once before the statement that first needs it and named after the file's descriptor prefix. A second game bound in the same scope gets its own root, so tests that keep two games apart still do. Types spelled with the schema as the root name typeof Root. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +- bench/compare/src/ritmo.ts | 3 +- integration/algorithms/schema.ts | 3 +- packages/browser/dtslint/Controls.tst.ts | 10 ++-- packages/browser/dtslint/InputCapture.tst.ts | 3 +- packages/browser/dtslint/Players.tst.ts | 4 +- packages/browser/dtslint/Pointer.tst.ts | 4 +- packages/browser/test/Controls.test.ts | 6 ++- packages/browser/test/InputCapture.test.ts | 6 ++- packages/browser/test/Players.test.ts | 4 +- packages/core/README.md | 3 +- packages/core/bench/helpers.bench.ts | 4 +- packages/core/bench/runtime.bench.ts | 13 +++-- packages/core/bench/types/stress.ts | 4 +- packages/core/dtslint/Architecture.tst.ts | 3 +- packages/core/dtslint/Clock.tst.ts | 3 +- packages/core/dtslint/Command.tst.ts | 9 ++-- packages/core/dtslint/CommandEntries.tst.ts | 3 +- .../core/dtslint/CommandPermissions.tst.ts | 7 +-- packages/core/dtslint/Condition.tst.ts | 6 ++- packages/core/dtslint/Data.tst.ts | 5 +- packages/core/dtslint/Debug.tst.ts | 10 ++-- packages/core/dtslint/DroppedRelations.tst.ts | 3 +- .../core/dtslint/EntityScopeTriggers.tst.ts | 12 +++-- packages/core/dtslint/EntityState.tst.ts | 3 +- packages/core/dtslint/ExactUpdate.tst.ts | 3 +- packages/core/dtslint/FixMessages.tst.ts | 3 +- packages/core/dtslint/Identity.tst.ts | 12 +++-- packages/core/dtslint/Immutable.tst.ts | 3 +- .../core/dtslint/MachineTransitions.tst.ts | 16 +++--- packages/core/dtslint/OneWay.tst.ts | 3 +- packages/core/dtslint/QueryAnyOf.tst.ts | 3 +- packages/core/dtslint/QueryShape.tst.ts | 3 +- packages/core/dtslint/Requires.tst.ts | 12 +++-- packages/core/dtslint/ResourceChanged.tst.ts | 6 ++- packages/core/dtslint/Rows.tst.ts | 5 +- packages/core/dtslint/Runtime.tst.ts | 3 +- packages/core/dtslint/Schedule.tst.ts | 3 +- packages/core/dtslint/Schema.tst.ts | 49 ++++++++++--------- packages/core/dtslint/Slots.tst.ts | 24 ++++----- packages/core/dtslint/Stability.tst.ts | 7 +-- packages/core/dtslint/StateMachine.tst.ts | 16 +++--- packages/core/dtslint/SubState.tst.ts | 10 ++-- packages/core/dtslint/System.tst.ts | 9 ++-- packages/core/dtslint/ThisTick.tst.ts | 11 +++-- packages/core/dtslint/ThisTickQueries.tst.ts | 3 +- packages/core/test/Checkpoint.graves.test.ts | 3 +- packages/core/test/Clock.test.ts | 7 +-- packages/core/test/Codec.test.ts | 5 +- packages/core/test/Condition.check.test.ts | 3 +- .../core/test/Descriptor.EntityState.test.ts | 3 +- packages/core/test/Descriptor.meta.test.ts | 5 +- packages/core/test/Entity.id.test.ts | 3 +- packages/core/test/FixedLoop.test.ts | 3 +- packages/core/test/HostClock.test.ts | 3 +- packages/core/test/IgnoredFailure.test.ts | 3 +- packages/core/test/Random.resource.test.ts | 3 +- packages/core/test/Runtime.any-of.test.ts | 3 +- packages/core/test/Runtime.checksum.test.ts | 3 +- packages/core/test/Runtime.commands.test.ts | 10 ++-- packages/core/test/Runtime.data.test.ts | 3 +- packages/core/test/Runtime.debug.test.ts | 3 +- .../test/Runtime.descriptor-identity.test.ts | 3 +- packages/core/test/Runtime.documented.test.ts | 3 +- .../test/Runtime.dropped-relations.test.ts | 3 +- packages/core/test/Runtime.events.test.ts | 5 +- .../core/test/Runtime.exact-update.test.ts | 3 +- packages/core/test/Runtime.exit-phase.test.ts | 3 +- packages/core/test/Runtime.host.test.ts | 5 +- packages/core/test/Runtime.immutable.test.ts | 3 +- .../core/test/Runtime.kept-entity.test.ts | 5 +- packages/core/test/Runtime.lifecycle.test.ts | 5 +- .../test/Runtime.machine-transitions.test.ts | 3 +- packages/core/test/Runtime.numbers.test.ts | 5 +- packages/core/test/Runtime.ownership.test.ts | 3 +- packages/core/test/Runtime.pages.test.ts | 5 +- packages/core/test/Runtime.provisions.test.ts | 3 +- .../core/test/Runtime.query-lookup.test.ts | 10 ++-- packages/core/test/Runtime.relations.test.ts | 11 +++-- .../core/test/Runtime.requirements.test.ts | 4 +- packages/core/test/Runtime.requires.test.ts | 5 +- .../test/Runtime.resource-changed.test.ts | 3 +- packages/core/test/Runtime.resources.test.ts | 3 +- .../core/test/Runtime.rewind-readers.test.ts | 3 +- packages/core/test/Runtime.rows.test.ts | 3 +- packages/core/test/Runtime.scheduling.test.ts | 3 +- .../core/test/Runtime.scope-triggers.test.ts | 3 +- .../test/Runtime.snapshot-migrations.test.ts | 3 +- .../core/test/Runtime.snapshot-scopes.test.ts | 3 +- packages/core/test/Runtime.stability.test.ts | 10 ++-- .../core/test/Runtime.stale-entity.test.ts | 3 +- .../core/test/Runtime.stale-target.test.ts | 5 +- .../test/Runtime.state-machine-data.test.ts | 3 +- .../core/test/Runtime.state-machine.test.ts | 17 ++++--- packages/core/test/Runtime.stepping.test.ts | 3 +- .../core/test/Runtime.storage-model.test.ts | 5 +- packages/core/test/Runtime.storage.test.ts | 5 +- .../core/test/Runtime.stream-loss.test.ts | 3 +- packages/core/test/Runtime.sub-states.test.ts | 3 +- packages/core/test/Schedule.when.test.ts | 3 +- packages/core/test/Schema.requires.test.ts | 6 ++- packages/core/test/SchemaLibraries.test.ts | 3 +- packages/core/test/ThisTick.test.ts | 7 +-- packages/core/test/ThisTickQueries.test.ts | 7 +-- packages/core/test/World.pages.test.ts | 3 +- packages/devtools/dtslint/Determinism.tst.ts | 3 +- packages/devtools/dtslint/Devtools.tst.ts | 12 +++-- packages/devtools/dtslint/Fuzz.tst.ts | 3 +- packages/devtools/dtslint/Verify.tst.ts | 3 +- packages/devtools/dtslint/Why.tst.ts | 3 +- packages/devtools/test/Determinism.test.ts | 5 +- .../devtools/test/DroppedRelations.test.ts | 3 +- packages/devtools/test/Fuzz.test.ts | 3 +- packages/devtools/test/FuzzNetwork.test.ts | 3 +- packages/devtools/test/IgnoredFailure.test.ts | 3 +- packages/devtools/test/Labels.test.ts | 3 +- packages/devtools/test/Lint.test.ts | 3 +- packages/devtools/test/Replay.test.ts | 3 +- packages/devtools/test/Session.test.ts | 7 +-- packages/devtools/test/Stepping.test.ts | 3 +- packages/devtools/test/WhyDepth.test.ts | 3 +- packages/devtools/test/fixtures/lint-entry.ts | 3 +- .../devtools/test/fixtures/replay-entry.ts | 3 +- packages/netcode/dtslint/Netcode.tst.ts | 7 +-- packages/netcode/test/InputSchema.test.ts | 3 +- packages/netcode/test/Rewindable.test.ts | 3 +- packages/netcode/test/SyncTest.test.ts | 3 +- packages/netcode/test/game.ts | 3 +- packages/pixi/dtslint/Pixi.tst.ts | 4 +- packages/pixi/test/Pixi.test.ts | 6 ++- packages/render/bench/render.bench.ts | 3 +- packages/render/dtslint/Render.tst.ts | 11 +++-- packages/render/test/Render.test.ts | 13 ++--- .../render/test/RenderSync.hierarchy.test.ts | 5 +- .../render/test/RenderSync.lagged.test.ts | 3 +- .../render/test/RenderSync.membership.test.ts | 5 +- .../render/test/RenderSync.rewind.test.ts | 3 +- website/src/demos/composition/fire.ts | 3 +- website/src/demos/determinism/sim.ts | 3 +- website/src/demos/states/sim.ts | 3 +- website/src/pages/docs/browser.mdx | 9 ++-- website/src/pages/docs/comparison.mdx | 18 ++++--- website/src/pages/docs/concepts.mdx | 19 ++++--- website/src/pages/docs/index.mdx | 16 ++++-- website/src/pages/docs/type-safety.mdx | 34 ++++++++----- website/src/pages/docs/with-an-agent.mdx | 3 +- website/src/pages/features/bugs.mdx | 3 +- website/src/pages/features/composition.mdx | 20 +++++--- website/src/pages/features/determinism.mdx | 6 ++- website/src/pages/features/replays.mdx | 6 ++- website/src/pages/features/rollback.mdx | 15 ++++-- website/src/pages/features/states.mdx | 6 ++- website/src/snippets/home-complete.mdx | 4 +- website/src/snippets/home-declared.mdx | 3 +- website/src/snippets/home-order.mdx | 3 +- website/src/snippets/home-result.mdx | 3 +- website/src/snippets/home-services.mdx | 3 +- website/src/snippets/home-step-run.mdx | 3 +- website/src/snippets/home-step-system.mdx | 3 +- website/src/snippets/home-step-world.mdx | 4 +- 160 files changed, 583 insertions(+), 347 deletions(-) diff --git a/README.md b/README.md index ea65baeb..d978be83 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,14 @@ const Time = Clock.resource("Example/Time", step) const Position = Descriptor.Component()("Example/Position") const Velocity = Descriptor.Component()("Example/Velocity") +const Root = Schema.defineRoot("Example") // Bind the schema to its game-facing APIs. const Game = Schema.bind( Schema.fragment({ components: { Position, Velocity }, resources: { Time } - }) + }), + Root ) const AdvanceTime = Clock.system(Game, { diff --git a/bench/compare/src/ritmo.ts b/bench/compare/src/ritmo.ts index e2dbc7b2..97fb0aaf 100644 --- a/bench/compare/src/ritmo.ts +++ b/bench/compare/src/ritmo.ts @@ -43,7 +43,8 @@ const Tags = { const Position = Descriptor.Component<{ readonly x: number; readonly y: number }>()("Bench/Position") const Velocity = Descriptor.Component<{ readonly x: number; readonly y: number }>()("Bench/Velocity") -const Game = Schema.bind(Schema.fragment({ components: { A, B, C, D, E, Data, Position, Velocity, ...Tags } })) +const Root = Schema.defineRoot("Bench") +const Game = Schema.bind(Schema.fragment({ components: { A, B, C, D, E, Data, Position, Velocity, ...Tags } }), Root) const runtime = () => { const made = Game.Runtime.make({ services: Game.Runtime.services() }) diff --git a/integration/algorithms/schema.ts b/integration/algorithms/schema.ts index 980982ff..73c9f64e 100644 --- a/integration/algorithms/schema.ts +++ b/integration/algorithms/schema.ts @@ -21,4 +21,5 @@ type FragmentOf = typeof fragment /** The game's schema by name: type errors print `GameSchema`, not every descriptor. */ export interface GameSchema extends FragmentOf {} const named: GameSchema = fragment -export const Game = Schema.bind(named) +const Root = Schema.defineRoot("Algorithms") +export const Game = Schema.bind(named, Root) diff --git a/packages/browser/dtslint/Controls.tst.ts b/packages/browser/dtslint/Controls.tst.ts index ee0f4b30..7d326da3 100644 --- a/packages/browser/dtslint/Controls.tst.ts +++ b/packages/browser/dtslint/Controls.tst.ts @@ -18,6 +18,8 @@ const mouseBindings = { jump: ["primary"] } as const const mouse = Pointer.actions(canvas, window, mouseBindings) const keysWithoutPause = Keyboard.actions(host, { jump: [" "] }) +const Root = Schema.defineRoot("ControlsTypes") + describe("Controls", () => { it("types merged actions by the declared names", () => { const input = Actions.merge({ keyboard, gamepad }).snapshot() @@ -87,7 +89,7 @@ describe("Controls", () => { }) => Actions.merge(devices) const ControlsInput = Descriptor.Service>()("ControlsTypes/Controls") const Input = Descriptor.TransientResource>()("ControlsTypes/Input") - const Game = Schema.bind(Schema.fragment({ resources: { Input } })) + const Game = Schema.bind(Schema.fragment({ resources: { Input } }), Root) const Capture = InputCapture.system(Game, { name: "ControlsTypes/Capture", source: ControlsInput, resource: Input }) const runtime = Game.Runtime.make({ services: Game.Runtime.services(Game.Runtime.service(ControlsInput, makeControls({ keyboard, gamepad }))), @@ -104,7 +106,7 @@ describe("Controls", () => { it("rejects snapshot() through a plain service, and keeps the other methods", () => { const makeControls = (devices: { readonly keyboard: Keyboard.Actions }) => Actions.merge(devices) const ControlsInput = Descriptor.Service>()("ControlsTypes/Plain") - const Game = Schema.bind(Schema.fragment({})) + const Game = Schema.bind(Schema.fragment({}), Root) Game.System("ControlsTypes/Settings", { services: { controls: Game.System.service(ControlsInput) } }, ({ services }) => { expect(services.controls.rebind.keyboard({ jump: ["w"], pause: ["p"] })).type.toBe() // @ts-expect-error snapshot() drains ControlsTypes/Plain: declare it with Game.System.consumeService, or read the resource its capture writes @@ -170,7 +172,7 @@ describe("Controls", () => { Actions.merge({ desktop: Actions.group(devices), gamepad }) const ControlsInput = Descriptor.Service>()("ControlsTypes/Grouped") const Input = Descriptor.TransientResource>()("ControlsTypes/GroupedInput") - const Game = Schema.bind(Schema.fragment({ resources: { Input } })) + const Game = Schema.bind(Schema.fragment({ resources: { Input } }), Root) const services = Game.Runtime.services(Game.Runtime.service(ControlsInput, makeControls({ keyboard, mouse }))) const idle = Actions.idle({ keyboard: Keyboard.idle(keyBindings), mouse: Pointer.idle(mouseBindings), gamepad: Gamepad.idle(padBindings) }) Game.Runtime.make({ services, resources: { Input: idle } }) @@ -247,7 +249,7 @@ describe("one home for the shared input types", () => { expect(makeControls({ keyboard, gamepad })).type.toBeAssignableTo>() // @ts-expect-error Property 'rebind' does not exist ;({} as DescriptorOf.Value).rebind - const Game = Schema.bind(Schema.fragment({})) + const Game = Schema.bind(Schema.fragment({}), Root) Game.System("ControlsTypes/Peek", { services: { controls: Game.System.service(ControlsInput) } }, ({ services }) => { // @ts-expect-error snapshot() drains ControlsTypes/Source services.controls.snapshot() diff --git a/packages/browser/dtslint/InputCapture.tst.ts b/packages/browser/dtslint/InputCapture.tst.ts index 2e38744d..a34d9711 100644 --- a/packages/browser/dtslint/InputCapture.tst.ts +++ b/packages/browser/dtslint/InputCapture.tst.ts @@ -16,7 +16,8 @@ const Optional = Descriptor.TransientResource<{ const Wide = Descriptor.TransientResource()("CaptureTypes/Wide") const Declared = Descriptor.TransientResource>()("CaptureTypes/Declared") const Clock = Descriptor.Service<{ readonly now: () => number }>()("CaptureTypes/Clock") -const Game = Schema.bind(Schema.fragment({ resources: { Input, Wrong, Fewer, Optional, Wide, Declared } })) +const Root = Schema.defineRoot("CaptureTypes") +const Game = Schema.bind(Schema.fragment({ resources: { Input, Wrong, Fewer, Optional, Wide, Declared } }), Root) describe("InputCapture", () => { it("requires the service and resource at tick time", () => { diff --git a/packages/browser/dtslint/Players.tst.ts b/packages/browser/dtslint/Players.tst.ts index 070bc3fb..8d3d7455 100644 --- a/packages/browser/dtslint/Players.tst.ts +++ b/packages/browser/dtslint/Players.tst.ts @@ -17,6 +17,8 @@ const makePlayers = (devices: { readonly pads: Gamepad.Pads }) => Players.pool(devices) +const Root = Schema.defineRoot("PlayersTypes") + describe("Controls.players", () => { it("types every slot by name, with device data behind a check of device", () => { const pool = makePlayers({ left: Keyboard.actions(host, leftKeys), right: Keyboard.actions(host, rightKeys), pads: Gamepad.pads(host, padKeys, 4) }) @@ -82,7 +84,7 @@ describe("Controls.players", () => { it("captures into a resource initialized with idle, and keeps snapshot() to the capture", () => { const PlayersInput = Descriptor.Service>()("PlayersTypes/Players") const Input = Descriptor.TransientResource>()("PlayersTypes/Input") - const Game = Schema.bind(Schema.fragment({ resources: { Input } })) + const Game = Schema.bind(Schema.fragment({ resources: { Input } }), Root) const pool = makePlayers({ left: Keyboard.actions(host, leftKeys), right: Keyboard.actions(host, rightKeys), pads: Gamepad.pads(host, padKeys, 2) }) Game.Runtime.make({ services: Game.Runtime.services(Game.Runtime.service(PlayersInput, pool)), diff --git a/packages/browser/dtslint/Pointer.tst.ts b/packages/browser/dtslint/Pointer.tst.ts index 0fa1c51d..8514013a 100644 --- a/packages/browser/dtslint/Pointer.tst.ts +++ b/packages/browser/dtslint/Pointer.tst.ts @@ -7,6 +7,8 @@ declare const blurHost: Pointer.BlurHost const bindings = { fire: ["primary"], aim: ["secondary", "back"] } as const +const Root = Schema.defineRoot("PointerTypes") + describe("Pointer", () => { it("types snapshot actions by the bound action names, with raw buttons and the position", () => { const mouse = Pointer.actions(canvas, blurHost, bindings) @@ -53,7 +55,7 @@ describe("Pointer", () => { it("takes snapshots only through consumeService", () => { const MouseInput = Descriptor.Service>()("PointerTypes/Mouse") - const Game = Schema.bind(Schema.fragment({})) + const Game = Schema.bind(Schema.fragment({}), Root) Game.System("PointerTypes/Settings", { services: { mouse: Game.System.service(MouseInput) } }, ({ services }) => { expect(services.mouse.rebind).type.toBeCallableWith({ fire: ["middle"], aim: ["primary"] }) // @ts-expect-error snapshot() drains PointerTypes/Mouse diff --git a/packages/browser/test/Controls.test.ts b/packages/browser/test/Controls.test.ts index d483d817..437acec7 100644 --- a/packages/browser/test/Controls.test.ts +++ b/packages/browser/test/Controls.test.ts @@ -12,6 +12,8 @@ const Actions = Controls.actions({ buttons: ["jump", "pause"] }) const merged = (keys: Keyboard.Timeline, pad: Gamepad.Timeline) => Actions.merge({ keyboard: Keyboard.scripted(keyBindings, keys), gamepad: Gamepad.scripted(padBindings, pad) }) +const Root = Schema.defineRoot("ControlsTest") + describe("Controls", () => { it("merges an action like one device with every binding", () => { const controls = merged( @@ -102,7 +104,7 @@ describe("Controls", () => { Actions.merge({ keyboard: devices.keyboard, touch: Actions.lacking(devices.touch, ["pause"]) }) const ControlsInput = Descriptor.Service>()("ControlsTest/Controls") const Input = Descriptor.TransientResource>()("ControlsTest/Input") - const Game = Schema.bind(Schema.fragment({ resources: { Input } })) + const Game = Schema.bind(Schema.fragment({ resources: { Input } }), Root) const Capture = InputCapture.system(Game, { name: "ControlsTest/Capture", source: ControlsInput, resource: Input }) const CaptureAgain = InputCapture.system(Game, { name: "ControlsTest/CaptureAgain", source: ControlsInput, resource: Input }) const runtime = Game.Runtime.make({ @@ -195,7 +197,7 @@ describe("Controls groups", () => { }) expect(controls.snapshot().available).toEqual({ moveRight: true, fire: true, pause: false }) const Lints = Descriptor.Service()("ControlsGroups/Controls") - const Game = Schema.bind(Schema.fragment({})) + const Game = Schema.bind(Schema.fragment({}), Root) const runtime = Game.Runtime.make({ services: Game.Runtime.services(Game.Runtime.service(Lints, controls)), resources: {}, diff --git a/packages/browser/test/InputCapture.test.ts b/packages/browser/test/InputCapture.test.ts index a8b0b7d0..dcda700a 100644 --- a/packages/browser/test/InputCapture.test.ts +++ b/packages/browser/test/InputCapture.test.ts @@ -5,11 +5,13 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const bindings = { jump: [" "], left: ["ArrowLeft"] } as const +const Root = Schema.defineRoot("CaptureTest") + describe("InputCapture", () => { it("copies one snapshot per run into the resource", () => { const Source = Descriptor.Service>>()("CaptureTest/Source") const Input = Descriptor.TransientResource>()("CaptureTest/Input") - const Game = Schema.bind(Schema.fragment({ resources: { Input } })) + const Game = Schema.bind(Schema.fragment({ resources: { Input } }), Root) const queued: Array> = [ { actions: { jump: { held: true, pressed: true, released: false }, left: { held: false, pressed: false, released: false } }, vectors: {}, used: true }, @@ -35,7 +37,7 @@ describe("InputCapture", () => { it("skips capture while its run conditions fail, keeping presses in the device", () => { const Source = Descriptor.Service>()("CaptureTest/Keys") const Input = Descriptor.TransientResource>()("CaptureTest/Held") - const Game = Schema.bind(Schema.fragment({ resources: { Input } })) + const Game = Schema.bind(Schema.fragment({ resources: { Input } }), Root) const Pace = Game.StateMachine("CaptureTest/Pace", { Running: {}, Frozen: {} }) const keyboard = Keyboard.scripted(bindings, [{ frame: 0, press: ["jump"], release: ["jump"] }]) const Capture = InputCapture.system(Game, { diff --git a/packages/browser/test/Players.test.ts b/packages/browser/test/Players.test.ts index ccba6add..42462592 100644 --- a/packages/browser/test/Players.test.ts +++ b/packages/browser/test/Players.test.ts @@ -13,6 +13,8 @@ const Players = Controls.players(Actions, { slots: ["p1", "p2"], join: "join", l const tap = (frame: number, name: Keyboard.TimelineName) => ({ frame, press: [name], release: [name] }) +const Root = Schema.defineRoot("PlayersTest") + describe("Controls.players", () => { it("joins the first free slot on a join press, with that snapshot idle", () => { const pool = Players.pool({ @@ -183,7 +185,7 @@ describe("Controls.players", () => { Actions.merge({ keyboard: reused }) const pool = Players.pool({ left: reused, right: Keyboard.scripted(rightKeys, []), other: shared }) const PoolService = Descriptor.Service()("PlayersTest/Pool") - const Game = Schema.bind(Schema.fragment({})) + const Game = Schema.bind(Schema.fragment({}), Root) const runtime = Game.Runtime.make({ services: Game.Runtime.services(Game.Runtime.service(PoolService, pool)), resources: {}, debug: { hostClock: HostClock.test() } }) if (!runtime.ok) throw new Error("runtime") const messages = runtime.value.debug.describe().lints.filter((lint) => lint.code === "service-lint").map((lint) => lint.message) diff --git a/packages/core/README.md b/packages/core/README.md index b0fff0f6..43ce98a2 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -13,7 +13,8 @@ pnpm add @typeonce/ritmo import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Example/Position") -const Game = Schema.bind(Schema.fragment({ components: { Position } })) +const Root = Schema.defineRoot("Example") +const Game = Schema.bind(Schema.fragment({ components: { Position } }), Root) ``` A component or resource can say what its number fields mean with `meta`: diff --git a/packages/core/bench/helpers.bench.ts b/packages/core/bench/helpers.bench.ts index 5af4b37b..b37b2e2a 100644 --- a/packages/core/bench/helpers.bench.ts +++ b/packages/core/bench/helpers.bench.ts @@ -30,12 +30,14 @@ const randomNext: BenchCase = { } } +const Root = Schema.defineRoot("Bench") + const randomDraw: BenchCase = { name: "random/draw-resource-100k", description: "Draw 100,000 random numbers from a resource in one system", setup: () => { const Rng = Descriptor.ConstructedResource(Random.codec)("Bench/Rng") - const Game = Schema.bind(Schema.fragment({ resources: { Rng } })) + const Game = Schema.bind(Schema.fragment({ resources: { Rng } }), Root) const made = Game.Runtime.make({ services: Game.Runtime.services(), resources: { Rng: Random.seed(1) } }) if (!made.ok) throw new Error("bench runtime") const runtime = made.value diff --git a/packages/core/bench/runtime.bench.ts b/packages/core/bench/runtime.bench.ts index de22d990..3dd9c644 100644 --- a/packages/core/bench/runtime.bench.ts +++ b/packages/core/bench/runtime.bench.ts @@ -31,14 +31,15 @@ const Sum = Descriptor.TransientResource()("Bench/Sum") const Ping = Descriptor.Event()("Bench/Ping") const { relation: ChildOf } = Descriptor.Hierarchy("Bench/ChildOf", "Bench/Children") +const Root = Schema.defineRoot("Bench") const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity, Health, Static, Player, Hot, Marker }, resources: { Sum }, events: { Ping }, relations: { ChildOf } -})) +}), Root) -type Id = Entity.EntityId +type Id = Entity.EntityId const makeRuntime = () => Game.Runtime.make({ services: Game.Runtime.services(), @@ -642,7 +643,7 @@ const clockAdvance: BenchCase = { setup: () => { const step = Clock.hz(60) const Time = Clock.resource("ClockBench/Time", step) - const Game = Schema.bind(Schema.fragment({ resources: { Time } })) + const Game = Schema.bind(Schema.fragment({ resources: { Time } }), Root) const made = Game.Runtime.make({ services: Game.Runtime.services(), resources: { Time: Clock.start(step) } }) if (!made.ok) throw new Error("initial clock is valid") const update = Game.Schedule(Clock.system(Game, { name: "ClockBench/Advance", resource: Time })) @@ -655,7 +656,8 @@ const Motion = Descriptor.EntityState("Bench/Motion", { ground: {}, air: Codecs.struct({ vy: Codecs.number }) }, { transitions: { ground: ["air"], air: ["ground"] } }) -const StateGame = Schema.bind(Schema.fragment({ components: { Motion } })) +const StateRoot = Schema.defineRoot("Bench/State") +const StateGame = Schema.bind(Schema.fragment({ components: { Motion } }), StateRoot) const entityStates: BenchCase = { name: "entity-state/update-transition-10k", @@ -686,8 +688,9 @@ const entityStates: BenchCase = { } } +const MachineRoot = Schema.defineRoot("Bench/Machine") // A separate schema, so adding the machine leaves every other case's world unchanged. -const MachineGame = Schema.bind(Schema.fragment({})) +const MachineGame = Schema.bind(Schema.fragment({}), MachineRoot) const Dialogue = MachineGame.StateMachine("Bench/Dialogue", { Closed: {}, Open: Codecs.struct({ page: Codecs.integer, typed: Codecs.integer }) diff --git a/packages/core/bench/types/stress.ts b/packages/core/bench/types/stress.ts index 9ccd0835..8140110a 100644 --- a/packages/core/bench/types/stress.ts +++ b/packages/core/bench/types/stress.ts @@ -60,10 +60,12 @@ const E07 = Descriptor.Event<{ readonly e7: number }>()("Stress/E07") const Clock = Descriptor.Service<{ readonly now: () => number }>()("Stress/Clock") const { relation: ChildOf } = Descriptor.Hierarchy("Stress/ChildOf", "Stress/Children") +const Root = Schema.defineRoot("Stress") const Game = Schema.bind( Schema.fragment({ components: { C00, C01, C02, C03, C04, C05, C06, C07, C08, C09, C10, C11, C12, C13, C14, C15 }, resources: { R00, R01, R02, R03, R04, R05, R06, R07 }, events: { E00, E01, E02, E03 } }), Schema.fragment({ components: { C16, C17, C18, C19, C20, C21, C22, C23, C24, C25, C26, C27, C28, C29, C30, C31 }, resources: { R08, R09, R10, R11, R12, R13, R14, R15 }, events: { E04, E05, E06, E07 }, relations: { ChildOf } }), - Schema.fragment({ requires: [Schema.requires(C28, C29, C30), Schema.requires(C29, C31), Schema.requires(C30, C31)] }) + Schema.fragment({ requires: [Schema.requires(C28, C29, C30), Schema.requires(C29, C31), Schema.requires(C30, C31)] }), + Root ) const Flow = Game.StateMachine("Stress/Flow", { Menu: {}, Playing: {}, Paused: {} }) diff --git a/packages/core/dtslint/Architecture.tst.ts b/packages/core/dtslint/Architecture.tst.ts index a75d4d57..e3b8dbe1 100644 --- a/packages/core/dtslint/Architecture.tst.ts +++ b/packages/core/dtslint/Architecture.tst.ts @@ -19,9 +19,10 @@ const R14 = Descriptor.Resource()("Architecture/R14") const R15 = Descriptor.Resource()("Architecture/R15") const R16 = Descriptor.Resource()("Architecture/R16") +const Root = Schema.defineRoot("Architecture") const Game = Schema.bind(Schema.fragment({ resources: { R01, R02, R03, R04, R05, R06, R07, R08, R09, R10, R11, R12, R13, R14, R15, R16 } -})) +}), Root) const define = < const Name extends string, diff --git a/packages/core/dtslint/Clock.tst.ts b/packages/core/dtslint/Clock.tst.ts index 791fd5e3..415fa6b5 100644 --- a/packages/core/dtslint/Clock.tst.ts +++ b/packages/core/dtslint/Clock.tst.ts @@ -6,7 +6,8 @@ const step = Clock.hz(60) const Time = Clock.resource("ClockTypes/Time", step) const Slow = Clock.resource("ClockTypes/Slow", Clock.hz(10)) const Paused = Descriptor.Resource()("ClockTypes/Paused") -const Game = Schema.bind(Schema.fragment({ resources: { Time, Slow, Paused } })) +const Root = Schema.defineRoot("ClockTypes") +const Game = Schema.bind(Schema.fragment({ resources: { Time, Slow, Paused } }), Root) const Flow = Game.StateMachine("ClockTypes/Flow", { Playing: {}, Menu: {} }) const inMenu = Game.Condition.inState(Flow, "Menu") diff --git a/packages/core/dtslint/Command.tst.ts b/packages/core/dtslint/Command.tst.ts index 07aaf8bc..9b715312 100644 --- a/packages/core/dtslint/Command.tst.ts +++ b/packages/core/dtslint/Command.tst.ts @@ -8,6 +8,7 @@ const Velocity = Descriptor.Component<{ x: number; y: number }>()("Velocity") const Time = Descriptor.Resource()("Time") const SafePosition = Descriptor.ConstructedComponent(Codecs.safeVector)("SafePosition") +const Root = Schema.defineRoot("Command") const Game = Schema.bind(Schema.fragment({ components: { Position, @@ -17,7 +18,7 @@ const Game = Schema.bind(Schema.fragment({ resources: { Time } -})) +}), Root) const schema = Game.schema describe("Command", () => { @@ -65,7 +66,7 @@ describe("Command", () => { }) it("spawn with plain and raw entries preserves proof typing and plain entry slots", () => { - const Game = Schema.bind(schema) + const Game = Schema.bind(schema, Root) const draft = Game.Command.spawn( [Position, { x: 0, y: 0 }], @@ -84,7 +85,7 @@ describe("Command", () => { }) it("entryRaw entries make insert return a Result with per-entry errors", () => { - const Game = Schema.bind(schema) + const Game = Schema.bind(schema, Root) const entry = Game.Command.entryRaw(SafePosition, { x: 0, y: 0 }) expect(entry).type.toBe, Descriptor.SchemaIssues>>() @@ -99,7 +100,7 @@ describe("Command", () => { }) it("entryRaw rejects plain component descriptors", () => { - const Game = Schema.bind(schema) + const Game = Schema.bind(schema, Root) // @ts-expect-error Type 'Descriptor<"component", "Position", { x: number; y: number; }>' is missing the following properties Game.Command.entryRaw(Position, { x: 0, y: 0 }) diff --git a/packages/core/dtslint/CommandEntries.tst.ts b/packages/core/dtslint/CommandEntries.tst.ts index 0b5c4bd0..446307c2 100644 --- a/packages/core/dtslint/CommandEntries.tst.ts +++ b/packages/core/dtslint/CommandEntries.tst.ts @@ -26,10 +26,11 @@ const Config = Descriptor.Resource<{ speed: number; debug: boolean }>()("Entries const Tuning = Descriptor.ConstructedResource(Codecs.struct({ rate: Codecs.number }))("Entries/Tuning") const Rng = Descriptor.ConstructedResource(Random.codec)("Entries/Rng") +const Root = Schema.defineRoot("Entries") const Game = Schema.bind(Schema.fragment({ components: { Hp, Name, Phase, Position, Velocity, Path, Pair, Scores, Body, Named, Seen, Lookup, Ease, Loose, Maybe, Seed, Safe }, resources: { Config, Tuning, Rng } -})) +}), Root) const Positions = Game.Query({ selection: { position: Game.Query.read(Position) } }) describe("spawn and insert entries", () => { diff --git a/packages/core/dtslint/CommandPermissions.tst.ts b/packages/core/dtslint/CommandPermissions.tst.ts index 9cfab792..ef0ff111 100644 --- a/packages/core/dtslint/CommandPermissions.tst.ts +++ b/packages/core/dtslint/CommandPermissions.tst.ts @@ -8,10 +8,11 @@ const Enemy = Descriptor.Component<{}>()("Perms/Enemy") const Noise = Descriptor.Component()("Perms/Noise") const Turned = Descriptor.Component<{ readonly seconds: number }>()("Perms/Turned") const { relation: ChildOf } = Descriptor.Hierarchy("Perms/ChildOf", "Perms/Children") -const Game = Schema.bind(Schema.fragment({ components: { Position, Enemy, Noise, Turned }, relations: { ChildOf } })) +const Root = Schema.defineRoot("Perms") +const Game = Schema.bind(Schema.fragment({ components: { Position, Enemy, Noise, Turned }, relations: { ChildOf } }), Root) const Level = Game.EntityScope("Perms/Level") const Menu = Game.EntityScope("Perms/Menu") -type Id = Entity.EntityId +type Id = Entity.EntityId const id = 1 as Id const Enemies = Game.Query({ selection: { position: Game.Query.read(Position) }, with: [Enemy] }) @@ -173,7 +174,7 @@ describe("declared commands", () => { commands: Game.System.commands(Game.System.spawns(Position)) }, ({ commands }) => { const child = Game.Command.relate(Game.Command.spawn([Position, { x: 0 }]), ChildOf, id) - expect(child).type.toBe>() + expect(child).type.toBe>() // @ts-expect-error Perms/ChildOf is related by this draft but not declared in this system's commands: add it to Game.System.relates(...) commands.spawn(child) // @ts-expect-error Perms/ChildOf is related by this draft diff --git a/packages/core/dtslint/Condition.tst.ts b/packages/core/dtslint/Condition.tst.ts index dced8bef..849753e0 100644 --- a/packages/core/dtslint/Condition.tst.ts +++ b/packages/core/dtslint/Condition.tst.ts @@ -8,11 +8,13 @@ const Health = Descriptor.Component()("CheckTypes/Health") const Ping = Descriptor.Event()("CheckTypes/Ping") const Clock = Descriptor.Service<{ readonly now: () => number }>()("CheckTypes/Clock") -const Game = Schema.bind(Schema.fragment({ components: { Enemy, Health }, resources: { Freeze }, events: { Ping } })) +const Root = Schema.defineRoot("CheckTypes") +const Game = Schema.bind(Schema.fragment({ components: { Enemy, Health }, resources: { Freeze }, events: { Ping } }), Root) const Flow = Game.StateMachine("CheckTypes/Flow", { Playing: {}, Over: {} }) const OtherFreeze = Descriptor.Resource()("CheckTypes/OtherFreeze") -const Other = Schema.bind(Schema.fragment({ resources: { OtherFreeze } })) +const OtherRoot = Schema.defineRoot("CheckTypes/Other") +const Other = Schema.bind(Schema.fragment({ resources: { OtherFreeze } }), OtherRoot) const OtherFlow = Other.StateMachine("CheckTypes/OtherFlow", { A: {}, B: {} }) const Noop = Game.System("CheckTypes/Noop", {}, () => {}) diff --git a/packages/core/dtslint/Data.tst.ts b/packages/core/dtslint/Data.tst.ts index 51fd0de7..55e03bf0 100644 --- a/packages/core/dtslint/Data.tst.ts +++ b/packages/core/dtslint/Data.tst.ts @@ -6,7 +6,8 @@ import { describe, expect, it } from "tstyche" const Position = Descriptor.Component<{ x: number; y: number }>()("Data/Position") const Velocity = Descriptor.Component<{ x: number; y: number }>()("Data/Velocity") const Health = Descriptor.Component()("Data/Health") -const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity, Health } })) +const Root = Schema.defineRoot("Data") +const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity, Health } }), Root) const Movers = Game.Query({ selection: { position: Game.Query.write(Position), velocity: Game.Query.read(Velocity) } }) const Healths = Game.Query({ selection: { health: Game.Query.read(Health) } }) @@ -114,7 +115,7 @@ describe("a query's data and entities", () => { for (const entity of queries.healths.each()) { expect(entity).type.toBeAssignableTo>() expect(entity).type.not.toBeAssignableTo>() - expect(entity).type.toBeAssignableTo>() + expect(entity).type.toBeAssignableTo>() } }) }) diff --git a/packages/core/dtslint/Debug.tst.ts b/packages/core/dtslint/Debug.tst.ts index 63f29c25..bbafa979 100644 --- a/packages/core/dtslint/Debug.tst.ts +++ b/packages/core/dtslint/Debug.tst.ts @@ -7,8 +7,10 @@ import * as Codecs from "../test/codecs.ts" const Position = Descriptor.Component<{ readonly x: number }>()("DebugTypes/Position") const Other = Descriptor.Component<{ readonly y: number }>()("DebugTypes/Other") const Score = Descriptor.Resource()("DebugTypes/Score") -const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Score } })) -const OtherGame = Schema.bind(Schema.fragment({ components: { Other } })) +const Root = Schema.defineRoot("DebugTypes") +const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Score } }), Root) +const OtherRoot = Schema.defineRoot("DebugTypes/Other") +const OtherGame = Schema.bind(Schema.fragment({ components: { Other } }), OtherRoot) describe("Runtime debug option", () => { it("adds the debug handle only when enabled", () => { @@ -22,7 +24,7 @@ describe("Runtime debug option", () => { it("keeps the handle inside the Result of fallible runtimes", () => { const Viewport = Descriptor.ConstructedResource(Codecs.number)("DebugTypes/Viewport") - const Fallible = Schema.bind(Schema.fragment({ resources: { Viewport } })) + const Fallible = Schema.bind(Schema.fragment({ resources: { Viewport } }), Root) const made = Fallible.Runtime.make({ services: Fallible.Runtime.services(), resources: { Viewport: 1 }, debug: { hostClock: HostClock.standard } }) if (made.ok) { expect(made.value.debug.frame()).type.toBe() @@ -32,7 +34,7 @@ describe("Runtime debug option", () => { it("types the handle as optional when the option may be undefined (a dev build)", () => { const dev: boolean = Math.random() > 2 const maybe = Game.Runtime.make({ services: Game.Runtime.services(), debug: dev ? { hostClock: HostClock.standard } : undefined }).value - expect(maybe.debug).type.toBe | undefined>() + expect(maybe.debug).type.toBe | undefined>() // @ts-expect-error 'maybe.debug' is possibly 'undefined'. maybe.debug.describe() const off = Game.Runtime.make({ services: Game.Runtime.services(), debug: undefined }).value diff --git a/packages/core/dtslint/DroppedRelations.tst.ts b/packages/core/dtslint/DroppedRelations.tst.ts index 16686d11..51491610 100644 --- a/packages/core/dtslint/DroppedRelations.tst.ts +++ b/packages/core/dtslint/DroppedRelations.tst.ts @@ -9,10 +9,11 @@ const { relation: Targeting } = Descriptor.Relation("Dropped/Targeting", "Droppe const { relation: ChildOf } = Descriptor.Hierarchy("Dropped/ChildOf", "Dropped/Children") const { relation: Elsewhere } = Descriptor.Relation("Dropped/Elsewhere", "Dropped/ElsewhereBy") +const Root = Schema.defineRoot("Dropped") const Game = Schema.bind(Schema.fragment({ components: { Name, Idle }, relations: { Targeting, ChildOf } -})) +}), Root) type S = typeof Game.schema describe("Game.System.readDroppedRelations", () => { diff --git a/packages/core/dtslint/EntityScopeTriggers.tst.ts b/packages/core/dtslint/EntityScopeTriggers.tst.ts index b84a8d4f..ead78a02 100644 --- a/packages/core/dtslint/EntityScopeTriggers.tst.ts +++ b/packages/core/dtslint/EntityScopeTriggers.tst.ts @@ -3,19 +3,21 @@ import type * as EntityScope from "@typeonce/ritmo/EntityScope" import { describe, expect, it } from "tstyche" const Name = Descriptor.Component()("ScopeTriggersTypes/Name") -const Game = Schema.bind(Schema.fragment({ components: { Name } })) -const OtherGame = Schema.bind(Schema.fragment({ resources: {} })) +const Root = Schema.defineRoot("ScopeTriggersTypes") +const Game = Schema.bind(Schema.fragment({ components: { Name } }), Root) +const OtherRoot = Schema.defineRoot("ScopeTriggersTypes/Other") +const OtherGame = Schema.bind(Schema.fragment({ resources: {} }), OtherRoot) const Flow = Game.StateMachine("ScopeTriggersTypes/Flow", { Menu: {}, Playing: {} }) const OtherFlow = OtherGame.StateMachine("ScopeTriggersTypes/OtherFlow", { Idle: {} }) describe("Game.EntityScope triggers", () => { it("takes a machine of this game and one of its states", () => { const Run = Game.EntityScope("ScopeTriggersTypes/Run", { despawnOnExit: [Flow, "Playing"] }) - expect(Run).type.toBe>() + expect(Run).type.toBe>() const Both = Game.EntityScope("ScopeTriggersTypes/Both", { despawnOnEnter: [Flow, "Menu"], despawnOnExit: [Flow, "Playing"] }) - expect(Both).type.toBe>() + expect(Both).type.toBe>() const Plain = Game.EntityScope("ScopeTriggersTypes/Plain") - expect(Plain).type.toBe>() + expect(Plain).type.toBe>() }) it("rejects an unknown state", () => { diff --git a/packages/core/dtslint/EntityState.tst.ts b/packages/core/dtslint/EntityState.tst.ts index d2b91829..0a65600b 100644 --- a/packages/core/dtslint/EntityState.tst.ts +++ b/packages/core/dtslint/EntityState.tst.ts @@ -20,7 +20,8 @@ type ContactValue = | { readonly _tag: "air"; readonly vy: number; readonly coyote: number } type Mismatch = Result.Result> -const Game = Schema.bind(Schema.fragment({ components: { Contact, Phase, Mode } })) +const Root = Schema.defineRoot("EntityState") +const Game = Schema.bind(Schema.fragment({ components: { Contact, Phase, Mode } }), Root) const Bodies = Game.Query({ selection: { contact: Game.Query.write(Contact), phase: Game.Query.write(Phase), mode: Game.Query.write(Mode) } }) diff --git a/packages/core/dtslint/ExactUpdate.tst.ts b/packages/core/dtslint/ExactUpdate.tst.ts index c463905d..bf0f3546 100644 --- a/packages/core/dtslint/ExactUpdate.tst.ts +++ b/packages/core/dtslint/ExactUpdate.tst.ts @@ -20,10 +20,11 @@ const Safe = Descriptor.ConstructedComponent(Codecs.struct({ x: Codecs.number, y const Config = Descriptor.Resource<{ speed: number; debug: boolean }>()("Exact/Config") const Tuning = Descriptor.ConstructedResource(Codecs.struct({ rate: Codecs.number }))("Exact/Tuning") +const Root = Schema.defineRoot("Exact") const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Path, Pair, Scores, Body, Named, Seen, Loose, Maybe, Safe }, resources: { Config, Tuning } -})) +}), Root) const Moves = Game.Query({ selection: { diff --git a/packages/core/dtslint/FixMessages.tst.ts b/packages/core/dtslint/FixMessages.tst.ts index 404c6b49..52619360 100644 --- a/packages/core/dtslint/FixMessages.tst.ts +++ b/packages/core/dtslint/FixMessages.tst.ts @@ -15,11 +15,12 @@ const Log = Descriptor.Service<{ readonly log: (line: string) => void }>()("Fix/ const ChildOf = Descriptor.Hierarchy("Fix/ChildOf", "Fix/Children") const Targets = Descriptor.Relation("Fix/Targets", "Fix/TargetedBy") +const Root = Schema.defineRoot("Fix") const Game = Schema.bind(Schema.fragment({ components: { Position, Health }, resources: { Score, Lives }, relations: { ChildOf: ChildOf.relation, Targets: Targets.relation } -})) +}), Root) const Flow = Game.StateMachine("Fix/Flow", { Menu: {}, Playing: {} }) describe("system bodies", () => { diff --git a/packages/core/dtslint/Identity.tst.ts b/packages/core/dtslint/Identity.tst.ts index 5de66e53..3f555a3a 100644 --- a/packages/core/dtslint/Identity.tst.ts +++ b/packages/core/dtslint/Identity.tst.ts @@ -8,6 +8,8 @@ const Label = Descriptor.Component()("Identity/Position") const Health = Descriptor.Component()("Identity/Health") const Score = Descriptor.Resource()("Identity/Position") +const Root = Schema.defineRoot("Identity") + describe("descriptor identity", () => { it("rejects two descriptors of one kind with the same name in a fragment", () => { // @ts-expect-error __schemaConflicts__: "components name \"Identity/Position\"" @@ -16,23 +18,23 @@ describe("descriptor identity", () => { it("rejects the same descriptor name across bound fragments", () => { // @ts-expect-error __schemaConflicts__: "components name \"Identity/Position\"" - Schema.bind(Schema.fragment({ components: { Position } }), Schema.fragment({ components: { Label } })) + Schema.bind(Schema.fragment({ components: { Position } }), Schema.fragment({ components: { Label } }), Root) // @ts-expect-error __schemaConflicts__: "components name \"Identity/Position\"" Schema.bind(Schema.fragment({ components: { Position } }), Schema.fragment({ components: { Other: Position } }), Schema.defineRoot("Identity")) }) it("rejects the same registry key across bound fragments", () => { // @ts-expect-error __schemaConflicts__: "components key \"Position\"" - Schema.bind(Schema.fragment({ components: { Position } }), Schema.fragment({ components: { Position: Health } })) + Schema.bind(Schema.fragment({ components: { Position } }), Schema.fragment({ components: { Position: Health } }), Root) }) it("allows the same name across different descriptor kinds", () => { - const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Score } })) + const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Score } }), Root) expect(Game.schema.resources.Score).type.toBe() }) it("rejects a look-alike descriptor whose value type differs from the registered one", () => { - const Game = Schema.bind(Schema.fragment({ components: { Health } })) + const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) const Narrowed = Descriptor.Component<1>()("Identity/Health") const Widened = Descriptor.Component()("Identity/Health") // @ts-expect-error Type 'number' is not assignable to type '1'. @@ -43,7 +45,7 @@ describe("descriptor identity", () => { }) it("allows distinct systems that share a display name", () => { - const Game = Schema.bind(Schema.fragment({ components: { Health } })) + const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) const First = Game.System("Identity/Same", {}, () => {}) const Second = Game.System("Identity/Same", {}, () => {}) expect(Game.Schedule(First, Second).kind).type.toBe<"schedule">() diff --git a/packages/core/dtslint/Immutable.tst.ts b/packages/core/dtslint/Immutable.tst.ts index bf1663e2..109b400b 100644 --- a/packages/core/dtslint/Immutable.tst.ts +++ b/packages/core/dtslint/Immutable.tst.ts @@ -9,7 +9,8 @@ const Kind = Descriptor.Component<"wall" | "pickup">()("ImmutableTypes/Kind", { const Team = Descriptor.ConstructedComponent(Codecs.literal("red", "blue"))("ImmutableTypes/Team", { immutable: true }) const Cache = Descriptor.TransientComponent()("ImmutableTypes/Cache", { immutable: true }) -const Game = Schema.bind(Schema.fragment({ components: { Position, Kind, Team, Cache } })) +const Root = Schema.defineRoot("ImmutableTypes") +const Game = Schema.bind(Schema.fragment({ components: { Position, Kind, Team, Cache } }), Root) describe("immutable components", () => { it("brands only components declared immutable", () => { diff --git a/packages/core/dtslint/MachineTransitions.tst.ts b/packages/core/dtslint/MachineTransitions.tst.ts index 76fd9996..faf29122 100644 --- a/packages/core/dtslint/MachineTransitions.tst.ts +++ b/packages/core/dtslint/MachineTransitions.tst.ts @@ -6,8 +6,10 @@ import type * as SchemaTypes from "@typeonce/ritmo/Schema" import { describe, expect, it } from "tstyche" const Position = Descriptor.Component<{ x: number; y: number }>()("MachineMoves/Position") -const Game = Schema.bind(Schema.fragment({ components: { Position } })) -const OtherGame = Schema.bind(Schema.fragment({ resources: {} })) +const Root = Schema.defineRoot("MachineMoves") +const Game = Schema.bind(Schema.fragment({ components: { Position } }), Root) +const OtherRoot = Schema.defineRoot("MachineMoves/Other") +const OtherGame = Schema.bind(Schema.fragment({ resources: {} }), OtherRoot) const Round = Game.StateMachine("MachineMoves/Round", { Lobby: {}, @@ -28,7 +30,7 @@ type RoundState = "Lobby" | "Countdown" | "Playing" | "Over" describe("Game.StateMachine transitions", () => { it("carries the graph in the machine's type; without one, every move", () => { - expect(Round).type.toBe { readonly Playing: readonly ["Over"] readonly Over: readonly ["Lobby", "Over"] } | undefined>() - expect(Free).type.toBe>() + expect(Free).type.toBe>() expect>().type.toBe<{ readonly A: ReadonlyArray<"A" | "B">; readonly B: ReadonlyArray<"A" | "B"> }>() expect(Phase).type.toBe { it("still fits every machine bound, schedule, and condition", () => { expect().type.toBeAssignableTo() expect().type.toBeAssignableTo() - expect().type.toBeAssignableTo>() - expect(Game.Schedule.onEnter(Round, "Playing", [])).type.toBeAssignableTo>() + expect().type.toBeAssignableTo>() + expect(Game.Schedule.onEnter(Round, "Playing", [])).type.toBeAssignableTo>() expect(Game.Condition.inState(Round, "Over")).type.toBe>() Game.Runtime.machine(Round, { _tag: "Lobby" }) }) diff --git a/packages/core/dtslint/OneWay.tst.ts b/packages/core/dtslint/OneWay.tst.ts index 5af2e40d..98e388e1 100644 --- a/packages/core/dtslint/OneWay.tst.ts +++ b/packages/core/dtslint/OneWay.tst.ts @@ -2,7 +2,8 @@ import * as Core from "@typeonce/ritmo" import { Descriptor, Schema } from "@typeonce/ritmo" import { describe, expect, it } from "tstyche" -const Game = Schema.bind(Schema.fragment({ components: { Position: Descriptor.Component<{ x: number }>()("OneWay/Position") } })) +const Root = Schema.defineRoot("OneWay") +const Game = Schema.bind(Schema.fragment({ components: { Position: Descriptor.Component<{ x: number }>()("OneWay/Position") } }), Root) // Each job has one API. These tests keep the removed second ways removed. describe("one way to do each thing", () => { diff --git a/packages/core/dtslint/QueryAnyOf.tst.ts b/packages/core/dtslint/QueryAnyOf.tst.ts index 00bd7d84..589750e2 100644 --- a/packages/core/dtslint/QueryAnyOf.tst.ts +++ b/packages/core/dtslint/QueryAnyOf.tst.ts @@ -6,7 +6,8 @@ const Position = Descriptor.Component<{ readonly x: number; readonly y: number } const Sprite = Descriptor.Component()("AnyOfTypes/Sprite") const Foreign = Descriptor.Component()("AnyOfTypes/Foreign") -const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } })) +const Root = Schema.defineRoot("AnyOfTypes") +const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } }), Root) describe("Game.Query.anyOf", () => { it("combines added and changed filters", () => { diff --git a/packages/core/dtslint/QueryShape.tst.ts b/packages/core/dtslint/QueryShape.tst.ts index d2b1996e..5f1ffe9f 100644 --- a/packages/core/dtslint/QueryShape.tst.ts +++ b/packages/core/dtslint/QueryShape.tst.ts @@ -7,7 +7,8 @@ const Frozen = Descriptor.Component<{}>()("QueryShape/Frozen") const Tag = Descriptor.Component<{}>()("QueryShape/Tag") const Paved = Descriptor.Component<{}>()("QueryShape/Paved") -const Game = Schema.bind(Schema.fragment({ components: { Position, Frozen, Tag, Paved } })) +const Root = Schema.defineRoot("QueryShape") +const Game = Schema.bind(Schema.fragment({ components: { Position, Frozen, Tag, Paved } }), Root) describe("a query that can never match", () => { it("rejects a component both required and excluded", () => { diff --git a/packages/core/dtslint/Requires.tst.ts b/packages/core/dtslint/Requires.tst.ts index 1a7da021..9aceb83c 100644 --- a/packages/core/dtslint/Requires.tst.ts +++ b/packages/core/dtslint/Requires.tst.ts @@ -21,7 +21,8 @@ const Gameplay = Schema.fragment({ Schema.requires(B, A) ] }) -const Game = Schema.bind(Core, Gameplay) +const Root = Schema.defineRoot("Req") +const Game = Schema.bind(Core, Gameplay, Root) describe("Schema.requires", () => { it("carries one pair per required component", () => { @@ -45,7 +46,7 @@ describe("Schema.requires", () => { it("rejects a requirement naming a component no fragment registers", () => { const Lonely = Schema.fragment({ components: { Player }, requires: [Schema.requires(Player, Position)] }) // @ts-expect-error Req/Position is named in Schema.requires(...) but no fragment registers it: add it to a fragment's components - Schema.bind(Lonely) + Schema.bind(Lonely, Root) }) it("rejects a saved component requiring a transient one", () => { @@ -53,8 +54,9 @@ describe("Schema.requires", () => { const Ghost = Descriptor.Component<{}>()("Req/Ghost") const Shade = Descriptor.TransientComponent<{}>()("Req/Shade") // @ts-expect-error Req/Ghost requires Req/Trail, which is transient and not saved: make Req/Ghost transient too - Schema.bind(Schema.fragment({ components: { Ghost, Trail }, requires: [Schema.requires(Ghost, Trail)] })) - const Shades = Schema.bind(Schema.fragment({ components: { Shade, Trail }, requires: [Schema.requires(Shade, Trail)] })) + Schema.bind(Schema.fragment({ components: { Ghost, Trail }, requires: [Schema.requires(Ghost, Trail)] }), Root) + const ShadesRoot = Schema.defineRoot("Req/Shades") + const Shades = Schema.bind(Schema.fragment({ components: { Shade, Trail }, requires: [Schema.requires(Shade, Trail)] }), ShadesRoot) expect>().type.toBe() }) @@ -127,7 +129,7 @@ describe("commands.spawn with requirements", () => { it("returns the id with what the draft holds", () => { Game.System("Req/SpawnId", { commands: Game.System.commands(Game.System.spawns(Player)) }, ({ commands }) => { const id = commands.spawn(Game.Command.spawn([Player, {}], [Body, {}], [Velocity, { x: 0 }], [Position, { x: 0 }])) - expect(id).type.toBe>() + expect(id).type.toBe>() }) }) }) diff --git a/packages/core/dtslint/ResourceChanged.tst.ts b/packages/core/dtslint/ResourceChanged.tst.ts index c6412f79..ce3a6b03 100644 --- a/packages/core/dtslint/ResourceChanged.tst.ts +++ b/packages/core/dtslint/ResourceChanged.tst.ts @@ -6,8 +6,10 @@ const Score = Descriptor.Resource()("ChangedTypes/Score") const Hud = Descriptor.Resource<{ readonly text: string }>()("ChangedTypes/Hud") const OtherScore = Descriptor.Resource()("ChangedTypes/OtherScore") -const Game = Schema.bind(Schema.fragment({ resources: { Score, Hud } })) -const Other = Schema.bind(Schema.fragment({ resources: { OtherScore } })) +const Root = Schema.defineRoot("ChangedTypes") +const Game = Schema.bind(Schema.fragment({ resources: { Score, Hud } }), Root) +const OtherRoot = Schema.defineRoot("ChangedTypes/Other") +const Other = Schema.bind(Schema.fragment({ resources: { OtherScore } }), OtherRoot) describe("Game.System.changedResource", () => { it("gives a read view with changed()", () => { diff --git a/packages/core/dtslint/Rows.tst.ts b/packages/core/dtslint/Rows.tst.ts index 82210281..bdbcf394 100644 --- a/packages/core/dtslint/Rows.tst.ts +++ b/packages/core/dtslint/Rows.tst.ts @@ -7,7 +7,8 @@ const Position = Descriptor.Component<{ x: number; y: number }>()("Rows/Position const Health = Descriptor.Component()("Rows/Health") const Shield = Descriptor.Component<{ charges: number }>()("Rows/Shield") const Phase = Descriptor.EntityState("Rows/Phase", { idle: {}, busy: {} }) -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Shield, Phase } })) +const Root = Schema.defineRoot("Rows") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Shield, Phase } }), Root) const Units = Game.Query({ selection: { @@ -24,7 +25,7 @@ describe("queries.x.rows()", () => { const rows = queries.units.rows() expect(rows).type.toBe>>() const row = rows[0]! - expect(row.id).type.toBe>() + expect(row.id).type.toBe>() expect(row.values).type.toBe>() expect(row.values.position).type.toBe<{ readonly x: number; readonly y: number }>() expect(row.values.health).type.toBe() diff --git a/packages/core/dtslint/Runtime.tst.ts b/packages/core/dtslint/Runtime.tst.ts index 7280228d..4681ed1d 100644 --- a/packages/core/dtslint/Runtime.tst.ts +++ b/packages/core/dtslint/Runtime.tst.ts @@ -13,6 +13,7 @@ const Camera = Descriptor.ConstructedResource(Codecs.safeVector)("Camera") const Logger = Descriptor.Service<{ readonly log: (message: string) => void }>()("Logger") const PrefixedLogger = Descriptor.Service<{ readonly log: (message: string) => void }>()("RuntimeTypes/Logger") +const Root = Schema.defineRoot("SnapshotTypes") const Game = Schema.bind(Schema.fragment({ resources: { DeltaTime: Time, @@ -21,7 +22,7 @@ const Game = Schema.bind(Schema.fragment({ CurrentPhase: Phase, Camera } -})) +}), Root) const schema = Game.schema const ResourceSystem = System.System( diff --git a/packages/core/dtslint/Schedule.tst.ts b/packages/core/dtslint/Schedule.tst.ts index ab1f3880..7dea4d00 100644 --- a/packages/core/dtslint/Schedule.tst.ts +++ b/packages/core/dtslint/Schedule.tst.ts @@ -7,6 +7,7 @@ import { describe, it } from "tstyche" const Position = Descriptor.Component<{ x: number; y: number }>()("Position") const Time = Descriptor.Resource()("Time") +const Root = Schema.defineRoot("Schedule") const Game = Schema.bind(Schema.fragment({ components: { Position @@ -14,7 +15,7 @@ const Game = Schema.bind(Schema.fragment({ resources: { Time } -})) +}), Root) const schema = Game.schema const MovementSystem = System.System( diff --git a/packages/core/dtslint/Schema.tst.ts b/packages/core/dtslint/Schema.tst.ts index 344f336a..85169cd3 100644 --- a/packages/core/dtslint/Schema.tst.ts +++ b/packages/core/dtslint/Schema.tst.ts @@ -14,6 +14,8 @@ const Velocity = Descriptor.Component<{ dx: number; dy: number }>()("Velocity") const { relation: ChildOf } = Descriptor.Hierarchy("ChildOf", "Children") const { relation: Targeting } = Descriptor.Relation("Targeting", "TargetedBy") +const Root = Schema.defineRoot("Schema") + describe("Schema", () => { it("does not export top-level runtime authoring namespaces from the public barrel", () => { // @ts-expect-error Property 'System' does not exist on type @@ -70,7 +72,7 @@ describe("Schema", () => { } }) - const Game = Schema.bind(left, right) + const Game = Schema.bind(left, right, Root) const schema = Game.schema expect(schema).type.toBe { }) // @ts-expect-error __schemaConflicts__: "resources key \"DeltaTime\"" - Schema.bind(left, right) + Schema.bind(left, right, Root) }) it("bind closes the schema over systems, schedules, and runtimes", () => { @@ -134,7 +136,7 @@ describe("Schema", () => { CurrentPhase: Phase } }) - const Game = Schema.bind(schema) + const Game = Schema.bind(schema, Root) const MoveSystem = Game.System( "Move", @@ -170,7 +172,7 @@ describe("Schema", () => { } }).value - expect(update).type.toBeAssignableTo>() + expect(update).type.toBeAssignableTo>() runtime.tick(update) }) @@ -181,7 +183,7 @@ describe("Schema", () => { Velocity } }) - const Game = Schema.bind(schema) + const Game = Schema.bind(schema, Root) const ObserveSystem = Game.System( "ObserveOptional", @@ -216,7 +218,7 @@ describe("Schema", () => { services: Game.Runtime.services() }).value - expect(Game.Schedule(ObserveSystem)).type.toBeAssignableTo>() + expect(Game.Schedule(ObserveSystem)).type.toBeAssignableTo>() }) it("supports a zero-or-one singleton read without widening successful matches", () => { @@ -226,7 +228,7 @@ describe("Schema", () => { Velocity } }) - const Game = Schema.bind(schema) + const Game = Schema.bind(schema, Root) const MovingQuery = Game.Query({ selection: { position: Game.Query.write(Position), @@ -263,7 +265,7 @@ describe("Schema", () => { services: Game.Runtime.services() }).value - expect(Game.Schedule(ObserveSystem)).type.toBeAssignableTo>() + expect(Game.Schedule(ObserveSystem)).type.toBeAssignableTo>() }) it("rejects non-component descriptors in query selection and structural filters", () => { @@ -275,7 +277,7 @@ describe("Schema", () => { DeltaTime: Time } }) - const Game = Schema.bind(schema) + const Game = Schema.bind(schema, Root) // @ts-expect-error Type '"resource"' is not assignable to type '"component"'. Game.Query.read(Time) @@ -311,7 +313,7 @@ describe("Schema", () => { DeltaTime: Time } }) - const Game = Schema.bind(schema) + const Game = Schema.bind(schema, Root) const ObserveLifecycleSystem = Game.System( "ObserveLifecycle", @@ -339,8 +341,8 @@ describe("Schema", () => { expect(queries.moved.data.velocity.get(entity)).type.toBe | undefined>() } - expect(removed.positions.all()).type.toBe>>() - expect(despawned.entities.all()).type.toBe>>() + expect(removed.positions.all()).type.toBe>>() + expect(despawned.entities.all()).type.toBe>>() expect(removed.positions.lagged()).type.toBe() expect(despawned.entities.lagged()).type.toBe() } @@ -352,7 +354,7 @@ describe("Schema", () => { const schedule = Game.Schedule(ObserveLifecycleSystem) - expect(schedule).type.toBeAssignableTo>() + expect(schedule).type.toBeAssignableTo>() }) it("rejects non-component descriptors in lifecycle query APIs", () => { @@ -364,7 +366,7 @@ describe("Schema", () => { DeltaTime: Time } }) - const Game = Schema.bind(schema) + const Game = Schema.bind(schema, Root) // @ts-expect-error Type '"resource"' is not assignable to type '"component"'. Game.Query.added(Time) @@ -382,14 +384,15 @@ describe("Schema", () => { resources: { DeltaTime: Time } - })) + }), Root) const schemaA = GameA.schema + const RootB = Schema.defineRoot("Schema/B") const GameB = Schema.bind(Schema.fragment({ components: { Velocity } - })) + }), RootB) const schemaB = GameB.schema const SystemA = GameA.System( @@ -455,8 +458,8 @@ describe("Schema", () => { Targeting } }) - const Game = Schema.bind(schema) - const entityId = 1 as Entity.EntityId + const Game = Schema.bind(schema, Root) + const entityId = 1 as Entity.EntityId const query = Game.Query({ selection: { @@ -468,9 +471,9 @@ describe("Schema", () => { }) expect(query).type.toBeAssignableTo - readonly children: Relation.OptionalRelatedReadAccess - readonly target: Relation.OptionalRelationReadAccess + readonly parent: Relation.RelationReadAccess + readonly children: Relation.OptionalRelatedReadAccess + readonly target: Relation.OptionalRelationReadAccess }, readonly [], readonly [], readonly [], readonly [typeof ChildOf], readonly [], readonly [], readonly [], typeof Game.schema>>() const ObserveSystem = Game.System( @@ -704,8 +707,8 @@ describe("Schema", () => { it("cannot write through a query the system did not declare", () => { const Hp = Descriptor.Component()("UndeclaredWrite/Hp") - const Game = Schema.bind(Schema.fragment({ components: { Hp } })) - const entityId = 1 as Entity.EntityId + const Game = Schema.bind(Schema.fragment({ components: { Hp } }), Root) + const entityId = 1 as Entity.EntityId Game.System("UndeclaredWrite/Probe", { queries: { readers: Game.Query({ selection: { hp: Game.Query.read(Hp) } }) } }, ({ queries, lookup }) => { diff --git a/packages/core/dtslint/Slots.tst.ts b/packages/core/dtslint/Slots.tst.ts index 2c35df43..3d29e236 100644 --- a/packages/core/dtslint/Slots.tst.ts +++ b/packages/core/dtslint/Slots.tst.ts @@ -12,8 +12,10 @@ const Safe = Descriptor.ConstructedComponent(Codecs.safeVector)("Cells/Safe") const Label = Descriptor.ConstructedComponent(Codecs.string)("Cells/Label") const Frozen = Descriptor.Tag("Cells/Frozen") -const Game = Schema.bind(Schema.fragment({ components: { Position, Tags, Health, Safe, Label, Frozen } })) -const Other = Schema.bind(Schema.fragment({ components: { Health } })) +const Root = Schema.defineRoot("Cells") +const Game = Schema.bind(Schema.fragment({ components: { Position, Tags, Health, Safe, Label, Frozen } }), Root) +const OtherRoot = Schema.defineRoot("Cells/Other") +const Other = Schema.bind(Schema.fragment({ components: { Health } }), OtherRoot) describe("query slots", () => { it("read slots return deeply readonly values and cannot write", () => { @@ -71,7 +73,7 @@ describe("query slots", () => { it("relation slots read ids; optional ones read undefined while the entity lacks the relation", () => { const { relation: ChildOf } = Descriptor.Hierarchy("Cells/ChildOf", "Cells/Children") - const Linked = Schema.bind(Schema.fragment({ components: { Health }, relations: { ChildOf } })) + const Linked = Schema.bind(Schema.fragment({ components: { Health }, relations: { ChildOf } }), Root) Linked.System("Cells/OptionalRelation", { queries: { items: Linked.Query({ selection: { parent: Linked.Query.optionalRelation(ChildOf), children: Linked.Query.optionalRelated(ChildOf) } }), @@ -80,14 +82,14 @@ describe("query slots", () => { }, ({ queries }) => { const { data } = queries.items for (const entity of queries.items.each()) { - expect(data.parent).type.toBe>>() - expect(data.children).type.toBe>>>() - expect(data.parent.get(entity)).type.toBe | undefined>() + expect(data.parent).type.toBe>>() + expect(data.children).type.toBe>>>() + expect(data.parent.get(entity)).type.toBe | undefined>() // A required relation slot needs an entity whose query proves the relation. expect(queries.parented.data.parent.get).type.not.toBeCallableWith(entity) } for (const entity of queries.parented.each()) { - expect(queries.parented.data.parent.get(entity)).type.toBe>() + expect(queries.parented.data.parent.get(entity)).type.toBe>() } }) }) @@ -102,7 +104,7 @@ describe("query slots", () => { for (const entity of queries.items.each()) { expect(data).type.not.toHaveProperty("frozen") expect(data).type.not.toHaveProperty("position") - expect(entity.id).type.toBe>() + expect(entity.id).type.toBe>() } expect(queries.items.single()).type.toBe ReadonlyArray ? M : never), Query.SingleError>>() }) @@ -125,8 +127,8 @@ describe("commands", () => { ) }, ({ commands }) => { const id = commands.spawn(Game.Command.spawn([Health, 1])) - expect(id).type.toBe>() - expect(id).type.toBeAssignableTo>() + expect(id).type.toBe>() + expect(id).type.toBeAssignableTo>() expect(commands.insert).type.toBeCallableWith(id, [Health, 2], [Position, { x: 0, y: 0 }]) expect(commands.insert).type.not.toBeCallableWith(id, [Health, "2"]) expect(commands.insert).type.not.toBeCallableWith(id, [Position, 2]) @@ -140,7 +142,7 @@ describe("writes take what reads return", () => { it("writes a value read from another cell back without a copy", () => { const Bag = Descriptor.Component<{ items: Array }>()("Cells/Bag") const Hit = Descriptor.Event<{ amount: number }>()("Cells/Hit") - const Game = Schema.bind(Schema.fragment({ components: { Bag }, events: { Hit } })) + const Game = Schema.bind(Schema.fragment({ components: { Bag }, events: { Hit } }), Root) const Bags = Game.Query({ selection: { bag: Game.Query.write(Bag) } }) Game.System("Cells/Copy", { queries: { bags: Bags }, diff --git a/packages/core/dtslint/Stability.tst.ts b/packages/core/dtslint/Stability.tst.ts index faa6c42d..55367db1 100644 --- a/packages/core/dtslint/Stability.tst.ts +++ b/packages/core/dtslint/Stability.tst.ts @@ -5,7 +5,8 @@ import { describe, expect, it } from "tstyche" const Value = Descriptor.Resource()("StabilityTypes/Value") const Position = Descriptor.Component<{ x: number }>()("StabilityTypes/Position") -const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Value } })) +const Root = Schema.defineRoot("StabilityTypes") +const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Value } }), Root) const OtherGame = Schema.bind(Schema.fragment({ resources: { Value } }), Schema.defineRoot("Other")) const Failing = Game.System( @@ -89,7 +90,7 @@ describe("stability types", () => { it("brands entity scopes to one bound game root", () => { const scope = Game.EntityScope("Level") - expect(scope).type.toBe>() + expect(scope).type.toBe>() const otherScope = OtherGame.EntityScope("Level") const system = OtherGame.System("Other/Spawn", { @@ -145,7 +146,7 @@ describe("stability types", () => { size(): number } const Messages = Descriptor.Service()("StabilityTypes/Messages") - const Bound = Schema.bind(Schema.fragment({})) + const Bound = Schema.bind(Schema.fragment({}), Root) const peek = Bound.Inspector( "StabilityTypes/PeekInspector", { services: { queue: Bound.System.service(Messages) } }, diff --git a/packages/core/dtslint/StateMachine.tst.ts b/packages/core/dtslint/StateMachine.tst.ts index 10c18e0c..1ed9d3f6 100644 --- a/packages/core/dtslint/StateMachine.tst.ts +++ b/packages/core/dtslint/StateMachine.tst.ts @@ -18,14 +18,16 @@ const schema = Schema.fragment({ Counter } }) -const Game = Schema.bind(schema) +const Root = Schema.defineRoot("StateMachine") +const Game = Schema.bind(schema, Root) const otherSchema = Schema.fragment({ resources: { Counter } }) -const OtherGame = Schema.bind(otherSchema) +const OtherRoot = Schema.defineRoot("StateMachine/Other") +const OtherGame = Schema.bind(otherSchema, OtherRoot) const AppState = Game.StateMachine("AppState", { Menu: {}, Playing: {}, Paused: {} }) const RoundState = Game.StateMachine("RoundState", { Warmup: {}, Live: {}, SuddenDeath: {} }) @@ -230,7 +232,7 @@ describe("StateMachine", () => { ) }).value - expect(MachineSchedule).type.toBeAssignableTo>() + expect(MachineSchedule).type.toBeAssignableTo>() }) it("accepts direct inline schedule execution for disjunctive machine conditions", () => { @@ -261,7 +263,7 @@ describe("StateMachine", () => { ) }).value - expect(Game.Schedule(IncrementWithOrCondition)).type.toBeAssignableTo>() + expect(Game.Schedule(IncrementWithOrCondition)).type.toBeAssignableTo>() }) it("rejects invalid multi-machine runtime initialization values", () => { @@ -331,7 +333,7 @@ describe("StateMachine", () => { const bundle = Game.Schedule.transitions(TransitionWithService) - expect(bundle).type.toBeAssignableTo>() + expect(bundle).type.toBeAssignableTo>() }) it("keeps transition event unions exact across bundled transition schedules", () => { @@ -340,7 +342,7 @@ describe("StateMachine", () => { Game.Schedule.onExit(AppState, "Paused", [TransitionSystem]) ) - expect(bundle).type.toBeAssignableTo>() + expect(bundle).type.toBeAssignableTo>() }) it("rejects nested transition-application markers in transition schedules", () => { @@ -414,7 +416,7 @@ describe("StateMachine", () => { // @ts-expect-error Argument of type '[]' is not assignable to parameter of type 'readonly [Condition>() + expect(gated).type.toBeAssignableTo>() }) }) diff --git a/packages/core/dtslint/SubState.tst.ts b/packages/core/dtslint/SubState.tst.ts index 4b48901d..d5ad3343 100644 --- a/packages/core/dtslint/SubState.tst.ts +++ b/packages/core/dtslint/SubState.tst.ts @@ -5,16 +5,18 @@ import type * as SchemaTypes from "@typeonce/ritmo/Schema" import { describe, expect, it } from "tstyche" const Name = Descriptor.Component()("SubStateTypes/Name") -const Game = Schema.bind(Schema.fragment({ components: { Name } })) -const OtherGame = Schema.bind(Schema.fragment({ resources: {} })) +const Root = Schema.defineRoot("SubStateTypes") +const Game = Schema.bind(Schema.fragment({ components: { Name } }), Root) +const OtherRoot = Schema.defineRoot("SubStateTypes/Other") +const OtherGame = Schema.bind(Schema.fragment({ resources: {} }), OtherRoot) const Session = Game.StateMachine("SubStateTypes/Session", { Menu: {}, Playing: {} }) const Phase = Game.StateMachine("SubStateTypes/Phase", { Setup: {}, Battle: {} }, { within: [Session, "Playing"] }) const OtherFlow = OtherGame.StateMachine("SubStateTypes/OtherFlow", { Idle: {} }) describe("sub-states", () => { it("is a bound machine that records its parent", () => { - expect(Phase).type.toBe>() - expect(Session).type.toBe>() + expect(Phase).type.toBe>() + expect(Session).type.toBe>() }) it("reads as possibly absent and sets as a Result; top-level machines are unchanged", () => { diff --git a/packages/core/dtslint/System.tst.ts b/packages/core/dtslint/System.tst.ts index ebb6658e..3b584aa8 100644 --- a/packages/core/dtslint/System.tst.ts +++ b/packages/core/dtslint/System.tst.ts @@ -15,6 +15,7 @@ const SafePosition = Descriptor.ConstructedComponent(Codecs.safeVector)("SafePos const Viewport = Descriptor.ConstructedResource(Codecs.safeSize)("Viewport") const Camera = Descriptor.ConstructedResource(Codecs.safeVector)("Camera") +const Root = Schema.defineRoot("Slots") const Game = Schema.bind(Schema.fragment({ components: { Position, @@ -30,7 +31,7 @@ const Game = Schema.bind(Schema.fragment({ events: { TickEvent } -})) +}), Root) const schema = Game.schema describe("System", () => { @@ -174,7 +175,7 @@ describe("System", () => { describe("redundant resource and event slots", () => { const Score = Descriptor.Resource()("Slots/Score") const Ping = Descriptor.Event()("Slots/Ping") - const Slots = Schema.bind(Schema.fragment({ resources: { Score }, events: { Ping } })) + const Slots = Schema.bind(Schema.fragment({ resources: { Score }, events: { Ping } }), Root) it("rejects a read next to another slot of the same resource, and a repeated slot", () => { Slots.System("Slots/ReadAndWrite", { @@ -206,7 +207,7 @@ describe("redundant resource and event slots", () => { describe("Game.System.consumeService", () => { it("gives the same typed access as service", () => { - const Bound = Schema.bind(Schema.fragment({})) + const Bound = Schema.bind(Schema.fragment({}), Root) expect(Bound.System.consumeService(Logger)).type.toBe>() expect(Bound.System.service(Logger)).type.toBe>() Bound.System("Consume/Drain", { services: { logger: Bound.System.consumeService(Logger) } }, ({ services }) => { @@ -222,7 +223,7 @@ describe("Game.System.consumeService", () => { size(): number } const Messages = Descriptor.Service()("Messages") - const Bound = Schema.bind(Schema.fragment({})) + const Bound = Schema.bind(Schema.fragment({}), Root) Bound.System("Consume/Take", { services: { queue: Bound.System.consumeService(Messages) } }, ({ services }) => { expect(services.queue.take()).type.toBe>() }) diff --git a/packages/core/dtslint/ThisTick.tst.ts b/packages/core/dtslint/ThisTick.tst.ts index a308974d..77d8b471 100644 --- a/packages/core/dtslint/ThisTick.tst.ts +++ b/packages/core/dtslint/ThisTick.tst.ts @@ -5,7 +5,8 @@ import { describe, expect, it } from "tstyche" const Damage = Descriptor.Event<{ readonly amount: number }>()("ThisTick/Damage") const Score = Descriptor.Resource()("ThisTick/Score") -const Game = Schema.bind(Schema.fragment({ resources: { Score }, events: { Damage } })) +const Root = Schema.defineRoot("ThisTick") +const Game = Schema.bind(Schema.fragment({ resources: { Score }, events: { Damage } }), Root) const Flow = Game.StateMachine("ThisTick/Flow", { Playing: {}, Paused: {} }) const Deal = Game.System("ThisTick/Deal", { events: { damage: Game.System.writeEvent(Damage) } }, ({ events }) => { @@ -111,14 +112,14 @@ describe("Game.System.thisTick", () => { }) it("fails a tick of a schedule whose order was erased by an annotation, instead of passing it", () => { - const erased: ExecutableScheduleDefinition = Game.Schedule(Apply, Deal) + const erased: ExecutableScheduleDefinition = Game.Schedule(Apply, Deal) // @ts-expect-error the order of this tick is not statically known runtime.tick(erased) }) it("does not let a schedule with thisTick reads widen into an annotation that drops them", () => { - const plain: Schema.Schema.BoundSchedule = Game.Schedule(Deal) - expect(plain).type.toBeAssignableTo>() - expect(Game.Schedule(Apply)).type.not.toBeAssignableTo>() + const plain: Schema.Schema.BoundSchedule = Game.Schedule(Deal) + expect(plain).type.toBeAssignableTo>() + expect(Game.Schedule(Apply)).type.not.toBeAssignableTo>() }) }) diff --git a/packages/core/dtslint/ThisTickQueries.tst.ts b/packages/core/dtslint/ThisTickQueries.tst.ts index ac26655b..279f7054 100644 --- a/packages/core/dtslint/ThisTickQueries.tst.ts +++ b/packages/core/dtslint/ThisTickQueries.tst.ts @@ -9,7 +9,8 @@ const Health = Descriptor.Component()("Tick/Health") const Stunned = Descriptor.Component<{ readonly seconds: number }>()("Tick/Stunned") const Frozen = Descriptor.Component<{}>()("Tick/Frozen") const Hit = Descriptor.Event<{ readonly amount: number }>()("Tick/Hit") -const Game = Schema.bind(Schema.fragment({ components: { Position, Enemy, Health, Stunned, Frozen }, events: { Hit } })) +const Root = Schema.defineRoot("Tick") +const Game = Schema.bind(Schema.fragment({ components: { Position, Enemy, Health, Stunned, Frozen }, events: { Hit } }), Root) const Flow = Game.StateMachine("Tick/Flow", { Menu: {}, Playing: {} }) const Newest = Game.Query({ selection: { position: Game.Query.read(Position), health: Game.Query.read(Health) }, with: [Enemy] }) diff --git a/packages/core/test/Checkpoint.graves.test.ts b/packages/core/test/Checkpoint.graves.test.ts index d8a76155..e1d5979a 100644 --- a/packages/core/test/Checkpoint.graves.test.ts +++ b/packages/core/test/Checkpoint.graves.test.ts @@ -11,7 +11,8 @@ import { makeWorld } from "@typeonce/ritmo/internal/world" */ const Pos = Descriptor.Component()("Graves/Pos") -const Game = Schema.bind(Schema.fragment({ components: { Pos } })) +const Root = Schema.defineRoot("Graves") +const Game = Schema.bind(Schema.fragment({ components: { Pos } }), Root) const Hit = Symbol("Graves/Hit") describe("a held checkpoint keeps of the message log", () => { diff --git a/packages/core/test/Clock.test.ts b/packages/core/test/Clock.test.ts index d2c634eb..fd27028a 100644 --- a/packages/core/test/Clock.test.ts +++ b/packages/core/test/Clock.test.ts @@ -6,7 +6,8 @@ import { worldOf } from "./saved.ts" const step = Clock.hz(60) const Time = Clock.resource("ClockRun/Time", step) const Slow = Clock.resource("ClockRun/Slow", Clock.hz(10)) -const Game = Schema.bind(Schema.fragment({ resources: { Time, Slow } })) +const Root = Schema.defineRoot("ClockRun") +const Game = Schema.bind(Schema.fragment({ resources: { Time, Slow } }), Root) const Flow = Game.StateMachine("ClockRun/Flow", { Playing: {}, Paused: {} }) const paused = Game.Condition.inState(Flow, "Paused") @@ -133,7 +134,7 @@ describe("Clock", () => { runtime.tick(Game.Schedule(AdvanceTime)) const saved = worldOf(runtime.snapshot()) const FastTime = Clock.resource("ClockRun/Time", Clock.hz(120)) - const Fast = Schema.bind(Schema.fragment({ resources: { Time: FastTime, Slow } })) + const Fast = Schema.bind(Schema.fragment({ resources: { Time: FastTime, Slow } }), Root) const made = Fast.Runtime.make({ services: Fast.Runtime.services(), resources: { Time: Clock.start(Clock.hz(120)), Slow: Clock.start(Clock.hz(10)) } }) if (!made.ok) throw new Error("clock values are valid") expect(made.value.restore(saved)).toEqual(Result.failure({ @@ -192,7 +193,7 @@ describe("Clock.deadline", () => { const isReady = Clock.isDeadline(step) const Weapon = Descriptor.ConstructedComponent(z.object({ ready: z.unknown().refine(isReady).nullable() }))("ClockDeadline/Weapon") - const Armed = Schema.bind(Schema.fragment({ components: { Weapon }, resources: { Time } })) + const Armed = Schema.bind(Schema.fragment({ components: { Weapon }, resources: { Time } }), Root) const Flow = Armed.StateMachine("ClockDeadline/Flow", { Playing: {}, Paused: {} }) const Advance = Clock.system(Armed, { name: "ClockDeadline/Advance", resource: Time, hold: [Armed.Condition.inState(Flow, "Paused")] }) const cooldown = Clock.durationOfSteps(step, 3) diff --git a/packages/core/test/Codec.test.ts b/packages/core/test/Codec.test.ts index 385b74ae..45ebb1ff 100644 --- a/packages/core/test/Codec.test.ts +++ b/packages/core/test/Codec.test.ts @@ -20,7 +20,8 @@ const Debt = Descriptor.ConstructedResource(unsigned)("Codec/Debt") const Player = Descriptor.Tag("Codec/Player") const Delayed = Descriptor.ConstructedResource(asyncNumber)("Codec/Delayed") -const Game = Schema.bind(Schema.fragment({ components: { Player }, resources: { Inventory, Debt } })) +const Root = Schema.defineRoot("Codec") +const Game = Schema.bind(Schema.fragment({ components: { Player }, resources: { Inventory, Debt } }), Root) // A state whose data transforms: a cart of item counts, saved as its entries. const items = Codecs.map(Codecs.integer) @@ -94,7 +95,7 @@ describe("Codecs", () => { }) it("fails a schema that validates asynchronously", () => { - const Async = Schema.bind(Schema.fragment({ resources: { Delayed } })) + const Async = Schema.bind(Schema.fragment({ resources: { Delayed } }), Root) const made = Async.Runtime.make({ services: Async.Runtime.services(), resources: { Delayed: 1 } }) expect(made.ok ? undefined : made.error).toEqual({ resources: { diff --git a/packages/core/test/Condition.check.test.ts b/packages/core/test/Condition.check.test.ts index 8c98f78f..cf8b929a 100644 --- a/packages/core/test/Condition.check.test.ts +++ b/packages/core/test/Condition.check.test.ts @@ -9,7 +9,8 @@ const Log = Descriptor.Resource>()("Check/Log") const Enemy = Descriptor.Tag("Check/Enemy") const Ping = Descriptor.Event()("Check/Ping") -const Game = Schema.bind(Schema.fragment({ components: { Enemy }, resources: { Freeze, Log }, events: { Ping } })) +const Root = Schema.defineRoot("Check") +const Game = Schema.bind(Schema.fragment({ components: { Enemy }, resources: { Freeze, Log }, events: { Ping } }), Root) const Flow = Game.StateMachine("Check/Flow", { Playing: {}, Over: {} }) const makeRuntime = (freeze = 0, flow: "Playing" | "Over" = "Playing") => diff --git a/packages/core/test/Descriptor.EntityState.test.ts b/packages/core/test/Descriptor.EntityState.test.ts index cb730dd4..a6d3f94e 100644 --- a/packages/core/test/Descriptor.EntityState.test.ts +++ b/packages/core/test/Descriptor.EntityState.test.ts @@ -10,7 +10,8 @@ const Contact = Descriptor.EntityState("State/Contact", { }, { transitions: { ground: ["air", "crouch"], crouch: ["ground"], air: ["ground"] } }) const Mode = Descriptor.EntityState("State/Mode", { a: {}, b: Codecs.struct({ n: Codecs.number }), c: {} }) -const Game = Schema.bind(Schema.fragment({ components: { Contact, Mode } })) +const Root = Schema.defineRoot("Descriptor") +const Game = Schema.bind(Schema.fragment({ components: { Contact, Mode } }), Root) const Contacts = Game.Query({ selection: { contact: Game.Query.write(Contact) } }) const ReadContacts = Game.Inspector("State/ReadContacts", { diff --git a/packages/core/test/Descriptor.meta.test.ts b/packages/core/test/Descriptor.meta.test.ts index 425b31f8..2f44c796 100644 --- a/packages/core/test/Descriptor.meta.test.ts +++ b/packages/core/test/Descriptor.meta.test.ts @@ -14,10 +14,11 @@ const Tuning = Descriptor.ConstructedResource(Codecs.struct({ speed: Codecs.numb const Wind = Descriptor.Resource<{ readonly force: number }>()("Meta/Wind", { meta: { force: { unit: "N" } } }) const Frame = Descriptor.TransientResource<{ readonly index: number }>()("Meta/Frame", { meta: { index: { hint: "Frames since start." } } }) +const Root = Schema.defineRoot("Meta") const Game = Schema.bind(Schema.fragment({ components: { Health, Position, Sprite, Plain }, resources: { Tuning, Wind, Frame } -})) +}), Root) const makeRuntime = (speed: number) => { const made = Game.Runtime.make({ @@ -62,7 +63,7 @@ describe("descriptor meta", () => { it("is not saved: snapshots and restores are the same with and without it", () => { // Saved descriptors only: a plain resource does not snapshot. - const Saved = Schema.bind(Schema.fragment({ components: { Health, Sprite }, resources: { Tuning, Frame } })) + const Saved = Schema.bind(Schema.fragment({ components: { Health, Sprite }, resources: { Tuning, Frame } }), Root) const make = (speed: number) => { const made = Saved.Runtime.make({ services: Saved.Runtime.services(), resources: { Tuning: { speed }, Frame: { index: 0 } } }) if (!made.ok) throw new Error("invalid fixture") diff --git a/packages/core/test/Entity.id.test.ts b/packages/core/test/Entity.id.test.ts index 39f7a819..003cf45a 100644 --- a/packages/core/test/Entity.id.test.ts +++ b/packages/core/test/Entity.id.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it } from "vitest" import { Descriptor, Schema } from "@typeonce/ritmo" const Health = Descriptor.Component()("EntityId/Health") -const Game = Schema.bind(Schema.fragment({ components: { Health } })) +const Root = Schema.defineRoot("EntityId") +const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) describe("Game.Entity.id", () => { it("returns the id a handle names, and handles of one entity compare by ===", () => { diff --git a/packages/core/test/FixedLoop.test.ts b/packages/core/test/FixedLoop.test.ts index c94a6ce0..1d00a7bd 100644 --- a/packages/core/test/FixedLoop.test.ts +++ b/packages/core/test/FixedLoop.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from "vitest" import { Clock, Debug, FixedLoop, HostClock, Result, Schema } from "@typeonce/ritmo" -const Game = Schema.bind(Schema.fragment({})) +const Root = Schema.defineRoot("FixedLoop") +const Game = Schema.bind(Schema.fragment({}), Root) const Fail = Game.System("LoopIgnored/Fail", {}, () => Result.failure("boom" as const)) const step = Game.Schedule(Fail) diff --git a/packages/core/test/HostClock.test.ts b/packages/core/test/HostClock.test.ts index d456c828..1c0badbe 100644 --- a/packages/core/test/HostClock.test.ts +++ b/packages/core/test/HostClock.test.ts @@ -42,7 +42,8 @@ describe("HostClock.standard", () => { }) const Tick = Descriptor.Service<{ readonly clock: HostClock.TestHostClock; readonly step: number }>()("HostClockTest/Tick") -const Game = Schema.bind(Schema.fragment({})) +const Root = Schema.defineRoot("HostClockTest") +const Game = Schema.bind(Schema.fragment({}), Root) const Advance = Game.System("HostClockTest/Advance", { services: { tick: Game.System.service(Tick) } }, ({ services }) => { services.tick.clock.adjust(services.tick.step) }) diff --git a/packages/core/test/IgnoredFailure.test.ts b/packages/core/test/IgnoredFailure.test.ts index 4e67010c..8311579c 100644 --- a/packages/core/test/IgnoredFailure.test.ts +++ b/packages/core/test/IgnoredFailure.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it } from "vitest" import { Debug, Descriptor, HostClock, Result, Schema } from "@typeonce/ritmo" const Score = Descriptor.Resource()("Ignored/Score") -const Game = Schema.bind(Schema.fragment({ resources: { Score } })) +const Root = Schema.defineRoot("Ignored") +const Game = Schema.bind(Schema.fragment({ resources: { Score } }), Root) const Load = Game.System("Ignored/Load", {}, () => Result.failure("level file missing" as const)) const Place = Game.System("Ignored/Place", {}, () => {}) diff --git a/packages/core/test/Random.resource.test.ts b/packages/core/test/Random.resource.test.ts index 5ef5f7cf..ab8891b5 100644 --- a/packages/core/test/Random.resource.test.ts +++ b/packages/core/test/Random.resource.test.ts @@ -4,7 +4,8 @@ import { Descriptor, Random, Result, Schema } from "@typeonce/ritmo" import { worldOf } from "./saved.ts" const Rng = Descriptor.ConstructedResource(Random.codec)("RandomResource/Rng") -const Game = Schema.bind(Schema.fragment({ resources: { Rng } })) +const Root = Schema.defineRoot("RandomResource") +const Game = Schema.bind(Schema.fragment({ resources: { Rng } }), Root) const State = Game.Inspector("RandomResource/State", { resources: { rng: Game.System.readResource(Rng) } }, ({ resources }) => resources.rng.get()) const makeRuntime = () => { diff --git a/packages/core/test/Runtime.any-of.test.ts b/packages/core/test/Runtime.any-of.test.ts index f243c059..62eabbbc 100644 --- a/packages/core/test/Runtime.any-of.test.ts +++ b/packages/core/test/Runtime.any-of.test.ts @@ -5,7 +5,8 @@ const Position = Descriptor.Component<{ x: number }>()("AnyOf/Position") const Sprite = Descriptor.Component()("AnyOf/Sprite") const Paved = Descriptor.Component<{}>()("AnyOf/Paved") -const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite, Paved } })) +const Root = Schema.defineRoot("AnyOf") +const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite, Paved } }), Root) const makeRuntime = () => Game.Runtime.make({ services: Game.Runtime.services(), debug: { hostClock: HostClock.test() } }).value diff --git a/packages/core/test/Runtime.checksum.test.ts b/packages/core/test/Runtime.checksum.test.ts index f06a2480..e8f10385 100644 --- a/packages/core/test/Runtime.checksum.test.ts +++ b/packages/core/test/Runtime.checksum.test.ts @@ -9,7 +9,8 @@ const Label = Descriptor.ConstructedComponent(Codecs.string)("Checksum/Label") const Cache = Descriptor.TransientComponent<{ readonly hits: number }>()("Checksum/Cache") const Score = Descriptor.ConstructedResource(Codecs.number)("Checksum/Score") const Frame = Descriptor.TransientResource()("Checksum/Frame") -const Game = Schema.bind(Schema.fragment({ components: { Position, Label, Cache }, resources: { Score, Frame } })) +const Root = Schema.defineRoot("Checksum") +const Game = Schema.bind(Schema.fragment({ components: { Position, Label, Cache }, resources: { Score, Frame } }), Root) const makeRuntime = (score = 0, frame = 0) => { const made = Game.Runtime.make({ services: Game.Runtime.services(), resources: { Score: score, Frame: frame } }) diff --git a/packages/core/test/Runtime.commands.test.ts b/packages/core/test/Runtime.commands.test.ts index 76cf6c25..2e606c3e 100644 --- a/packages/core/test/Runtime.commands.test.ts +++ b/packages/core/test/Runtime.commands.test.ts @@ -12,6 +12,7 @@ const SafePosition = Descriptor.ConstructedComponent(Codecs.safeVector)("SafePos const SafeVelocity = Descriptor.ConstructedComponent(Codecs.struct({ x: Codecs.number, y: Codecs.number }))("SafeVelocity") const badVelocity = { _tag: "SchemaIssues", issues: [{ message: "Expected a finite number", path: ["x"] }] } +const Root = Schema.defineRoot("Runtime") const Game = Schema.bind(Schema.fragment({ components: { Position, @@ -22,14 +23,15 @@ const Game = Schema.bind(Schema.fragment({ Count, LastX } -})) +}), Root) const schema = Game.schema +const ConstructedRoot = Schema.defineRoot("Runtime/Constructed") const ConstructedGame = Schema.bind(Schema.fragment({ components: { SafePosition } -})) +}), ConstructedRoot) const makeRuntime = () => Game.Runtime.make({ services: Game.Runtime.services(), @@ -137,7 +139,7 @@ describe("Runtime commands", () => { }) it("despawn removes the entity and exact lookup reports MissingEntity", () => { - let storedId: EntityId | undefined + let storedId: EntityId | undefined const spawn = Game.System( "RuntimeCommands/SpawnAndStoreId", @@ -246,7 +248,7 @@ describe("Runtime commands", () => { }) it("insert on an existing entity becomes visible after explicit applyDeferred in the same schedule", () => { - let storedId: EntityId | undefined + let storedId: EntityId | undefined const spawn = Game.System( "RuntimeCommands/SpawnStoreForInsert", diff --git a/packages/core/test/Runtime.data.test.ts b/packages/core/test/Runtime.data.test.ts index 5055a758..26d65fb3 100644 --- a/packages/core/test/Runtime.data.test.ts +++ b/packages/core/test/Runtime.data.test.ts @@ -5,7 +5,8 @@ import { Descriptor, HostClock, Result, Schema } from "@typeonce/ritmo" const Health = Descriptor.Component()("Data/Health") const Position = Descriptor.Component<{ readonly x: number; readonly y: number }>()("Data/Position") const Mixed = Descriptor.Component()("Data/Mixed") -const Game = Schema.bind(Schema.fragment({ components: { Health, Position, Mixed } })) +const Root = Schema.defineRoot("Data") +const Game = Schema.bind(Schema.fragment({ components: { Health, Position, Mixed } }), Root) const make = () => { const made = Game.Runtime.make({ services: Game.Runtime.services() }) diff --git a/packages/core/test/Runtime.debug.test.ts b/packages/core/test/Runtime.debug.test.ts index 649b61dc..717bd37d 100644 --- a/packages/core/test/Runtime.debug.test.ts +++ b/packages/core/test/Runtime.debug.test.ts @@ -12,12 +12,13 @@ const Lonely = Descriptor.Event()("Debug/Lonely") const Clock = Descriptor.Service<{ readonly now: () => number }>()("Debug/Clock") const { relation: ChildOf } = Descriptor.Hierarchy("Debug/ChildOf", "Debug/Children") +const Root = Schema.defineRoot("Debug") const Game = Schema.bind(Schema.fragment({ components: { Position, Tagged, Sprite }, resources: { Score }, events: { Ping, Lonely }, relations: { ChildOf } -})) +}), Root) const Flow = Game.StateMachine("Debug/Flow", { Menu: {}, Playing: {} }) const makeRuntime = () => Game.Runtime.make({ diff --git a/packages/core/test/Runtime.descriptor-identity.test.ts b/packages/core/test/Runtime.descriptor-identity.test.ts index d728cdba..b2bab3d9 100644 --- a/packages/core/test/Runtime.descriptor-identity.test.ts +++ b/packages/core/test/Runtime.descriptor-identity.test.ts @@ -4,7 +4,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" type ModeValue = { readonly _tag: "a" } | { readonly _tag: "b" } const Mode = Descriptor.Component()("Identity/Mode") const Kind = Descriptor.Component<"tree" | "rock">()("Identity/Kind", { immutable: true }) -const Game = Schema.bind(Schema.fragment({ components: { Mode, Kind } })) +const Root = Schema.defineRoot("Identity") +const Game = Schema.bind(Schema.fragment({ components: { Mode, Kind } }), Root) const spawn = Game.System("Identity/Spawn", { commands: Game.System.commands(Game.System.spawns(Mode, Kind)) }, ({ commands }) => { commands.spawn(Game.Command.spawn([Mode, { _tag: "a" }], [Kind, "tree"])) diff --git a/packages/core/test/Runtime.documented.test.ts b/packages/core/test/Runtime.documented.test.ts index 00d50403..2c46a857 100644 --- a/packages/core/test/Runtime.documented.test.ts +++ b/packages/core/test/Runtime.documented.test.ts @@ -7,7 +7,8 @@ const Count = Descriptor.ConstructedComponent(Codecs.number)("Documented/Count") const Ping = Descriptor.Event()("Documented/Ping") const Gold = Descriptor.ConstructedResource(Codecs.number)("Documented/Gold") const Gems = Descriptor.ConstructedResource(Codecs.number)("Documented/Gems") -const Game = Schema.bind(Schema.fragment({ components: { Count }, events: { Ping }, resources: { Gold, Gems } })) +const Root = Schema.defineRoot("Documented") +const Game = Schema.bind(Schema.fragment({ components: { Count }, events: { Ping }, resources: { Gold, Gems } }), Root) const Flow = Game.StateMachine("Documented/Flow", { A: {}, B: {} }) const unwrap = (result: { readonly ok: true; readonly value: T } | { readonly ok: false }): T => { diff --git a/packages/core/test/Runtime.dropped-relations.test.ts b/packages/core/test/Runtime.dropped-relations.test.ts index 803cf36b..d3930c80 100644 --- a/packages/core/test/Runtime.dropped-relations.test.ts +++ b/packages/core/test/Runtime.dropped-relations.test.ts @@ -8,11 +8,12 @@ const Gate = Descriptor.Resource()("Dropped/Gate") const { relation: Targeting } = Descriptor.Relation("Dropped/Targeting", "Dropped/TargetedBy") const { relation: ChildOf } = Descriptor.Hierarchy("Dropped/ChildOf", "Dropped/Children") +const Root = Schema.defineRoot("Dropped") const Game = Schema.bind(Schema.fragment({ components: { Name }, resources: { Gate }, relations: { Targeting, ChildOf } -})) +}), Root) const open = Game.Condition.check("Dropped/Open", { resources: { gate: Game.System.readResource(Gate) } }, ({ resources }) => resources.gate.get()) diff --git a/packages/core/test/Runtime.events.test.ts b/packages/core/test/Runtime.events.test.ts index d8972d54..39fe34d6 100644 --- a/packages/core/test/Runtime.events.test.ts +++ b/packages/core/test/Runtime.events.test.ts @@ -3,7 +3,8 @@ import { Descriptor, Result, Schema } from "@typeonce/ritmo" import * as Runtime from "@typeonce/ritmo/Runtime" const Ping = Descriptor.Event<{ value: number }>()("Ping") -const Game = Schema.bind(Schema.fragment({ events: { Ping } })) +const Root = Schema.defineRoot("Runtime") +const Game = Schema.bind(Schema.fragment({ events: { Ping } }), Root) const makeRuntime = () => Game.Runtime.make({ services: Game.Runtime.services() }).value @@ -139,7 +140,7 @@ describe("Runtime events", () => { }) it("discards events published while a reader is skipped by its run conditions", () => { - const Local = Schema.bind(Schema.fragment({ events: { Ping } })) + const Local = Schema.bind(Schema.fragment({ events: { Ping } }), Root) const Mode = Local.StateMachine("Events/Mode", { On: {}, Off: {} }) const log: Array> = [] const gated = Local.System("Events/Gated", { diff --git a/packages/core/test/Runtime.exact-update.test.ts b/packages/core/test/Runtime.exact-update.test.ts index a4794557..3f478eaf 100644 --- a/packages/core/test/Runtime.exact-update.test.ts +++ b/packages/core/test/Runtime.exact-update.test.ts @@ -10,7 +10,8 @@ import { z } from "zod" const Position = Descriptor.ConstructedComponent(z.strictObject({ x: z.number(), y: z.number() }))("Exact/Position") const Config = Descriptor.ConstructedResource(z.strictObject({ speed: z.number() }))("Exact/Config") -const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Config } })) +const Root = Schema.defineRoot("Exact") +const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Config } }), Root) const makeRuntime = () => { const made = Game.Runtime.make({ services: Game.Runtime.services(), resources: { Config: { speed: 1 } } }) diff --git a/packages/core/test/Runtime.exit-phase.test.ts b/packages/core/test/Runtime.exit-phase.test.ts index 7346deb9..bc85eb83 100644 --- a/packages/core/test/Runtime.exit-phase.test.ts +++ b/packages/core/test/Runtime.exit-phase.test.ts @@ -5,7 +5,8 @@ import * as Codecs from "./codecs.ts" const Name = Descriptor.ConstructedComponent(Codecs.string)("ExitPhase/Name") const Saved = Descriptor.Resource()("ExitPhase/Saved") const Left = Descriptor.Event()("ExitPhase/Left") -const Game = Schema.bind(Schema.fragment({ components: { Name }, resources: { Saved }, events: { Left } })) +const Root = Schema.defineRoot("ExitPhase") +const Game = Schema.bind(Schema.fragment({ components: { Name }, resources: { Saved }, events: { Left } }), Root) const Flow = Game.StateMachine("ExitPhase/Flow", { Playing: {}, Menu: {} }) const Music = Game.StateMachine("ExitPhase/Music", { Loud: {}, Quiet: {} }) diff --git a/packages/core/test/Runtime.host.test.ts b/packages/core/test/Runtime.host.test.ts index efacf901..76e11e59 100644 --- a/packages/core/test/Runtime.host.test.ts +++ b/packages/core/test/Runtime.host.test.ts @@ -9,7 +9,8 @@ const hostClock = HostClock.test() const Ping = Descriptor.Event()("Host/Ping") const Ready = Descriptor.TransientResource()("Host/Ready") const Count = Descriptor.ConstructedResource(Codecs.number)("Host/Count") -const Game = Schema.bind(Schema.fragment({ events: { Ping }, resources: { Ready, Count } })) +const Root = Schema.defineRoot("Host") +const Game = Schema.bind(Schema.fragment({ events: { Ping }, resources: { Ready, Count } }), Root) const Flow = Game.StateMachine("Host/Flow", { Playing: {}, Menu: {} }) const ready = Game.Condition.check("Host/Ready", { resources: { ready: Game.System.readResource(Ready) } }, ({ resources }) => resources.ready.get()) const playing = Game.Condition.inState(Flow, "Playing") @@ -188,7 +189,7 @@ describe("host events and pause gates", () => { it("rejects a tick or tryTick inside a running tick or tryTick, and rolls the outer failure back", () => { const Gold = Descriptor.ConstructedResource(Codecs.number)("Host/Gold") - const Nested = Schema.bind(Schema.fragment({ resources: { Gold, Count } })) + const Nested = Schema.bind(Schema.fragment({ resources: { Gold, Count } }), Root) const made = Nested.Runtime.make({ services: Nested.Runtime.services(), resources: { Gold: 0, Count: 0 } }) if (!made.ok) throw new Error("invalid fixture") const runtime = made.value diff --git a/packages/core/test/Runtime.immutable.test.ts b/packages/core/test/Runtime.immutable.test.ts index 732ff9a3..073a4ecd 100644 --- a/packages/core/test/Runtime.immutable.test.ts +++ b/packages/core/test/Runtime.immutable.test.ts @@ -4,7 +4,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number }>()("Immutable/Position") const Kind = Descriptor.Component<"wall" | "pickup">()("Immutable/Kind", { immutable: true }) -const Game = Schema.bind(Schema.fragment({ components: { Position, Kind } })) +const Root = Schema.defineRoot("Immutable") +const Game = Schema.bind(Schema.fragment({ components: { Position, Kind } }), Root) const makeRuntime = () => Game.Runtime.make({ services: Game.Runtime.services() }).value diff --git a/packages/core/test/Runtime.kept-entity.test.ts b/packages/core/test/Runtime.kept-entity.test.ts index 7b2540aa..997db4d5 100644 --- a/packages/core/test/Runtime.kept-entity.test.ts +++ b/packages/core/test/Runtime.kept-entity.test.ts @@ -7,7 +7,8 @@ import { worldOf } from "./saved.ts" const Position = Descriptor.Component<{ readonly x: number }>()("Kept/Position") const Dead = Descriptor.Tag("Kept/Dead") const Enemy = Descriptor.Tag("Kept/Enemy") -const Game = Schema.bind(Schema.fragment({ components: { Position, Dead, Enemy } })) +const Root = Schema.defineRoot("Kept") +const Game = Schema.bind(Schema.fragment({ components: { Position, Dead, Enemy } }), Root) const make = (debug: boolean) => { const made = Game.Runtime.make({ services: Game.Runtime.services(), ...(debug ? { debug: { hostClock: HostClock.test() } } : {}) }) @@ -146,7 +147,7 @@ describe("an entity kept past its run, on a debug runtime", () => { describe("a kept entity whose id is used again, on a debug runtime", () => { // Saved worlds and rewinds give ids out again: the kept entity must not reach the new one's slot. const Health = Descriptor.ConstructedComponent(Codecs.number)("Kept/Health") - const Saved = Schema.bind(Schema.fragment({ components: { Health } })) + const Saved = Schema.bind(Schema.fragment({ components: { Health } }), Root) const makeSaved = (debug: boolean) => { const made = Saved.Runtime.make({ services: Saved.Runtime.services(), checkpoints: 4, ...(debug ? { debug: { hostClock: HostClock.test() } } : {}) }) if (!made.ok) throw new Error("runtime") diff --git a/packages/core/test/Runtime.lifecycle.test.ts b/packages/core/test/Runtime.lifecycle.test.ts index ed1d3baa..aed74e86 100644 --- a/packages/core/test/Runtime.lifecycle.test.ts +++ b/packages/core/test/Runtime.lifecycle.test.ts @@ -5,9 +5,10 @@ import type * as Entity from "@typeonce/ritmo/Entity" const Position = Descriptor.Component<{ x: number }>()("Lifecycle/Position") const Tag = Descriptor.Component<{}>()("Lifecycle/Tag") -const Game = Schema.bind(Schema.fragment({ components: { Position, Tag } })) +const Root = Schema.defineRoot("Lifecycle") +const Game = Schema.bind(Schema.fragment({ components: { Position, Tag } }), Root) -type Id = Entity.EntityId +type Id = Entity.EntityId const makeRuntime = () => Game.Runtime.make({ services: Game.Runtime.services() }).value diff --git a/packages/core/test/Runtime.machine-transitions.test.ts b/packages/core/test/Runtime.machine-transitions.test.ts index 86848392..a08acc8d 100644 --- a/packages/core/test/Runtime.machine-transitions.test.ts +++ b/packages/core/test/Runtime.machine-transitions.test.ts @@ -4,7 +4,8 @@ import type * as Machine from "@typeonce/ritmo/Machine" import type { NextMachineWriteView } from "@typeonce/ritmo/System" import * as Codecs from "./codecs.ts" -const Game = Schema.bind(Schema.fragment({ resources: {} })) +const Root = Schema.defineRoot("Runtime") +const Game = Schema.bind(Schema.fragment({ resources: {} }), Root) // Declared before Round, so a marker applies its move first. const Lights = Game.StateMachine("MachineMoves/Lights", { Off: {}, On: {} }) const Round = Game.StateMachine("MachineMoves/Round", { diff --git a/packages/core/test/Runtime.numbers.test.ts b/packages/core/test/Runtime.numbers.test.ts index 0a905a28..13ca9588 100644 --- a/packages/core/test/Runtime.numbers.test.ts +++ b/packages/core/test/Runtime.numbers.test.ts @@ -7,7 +7,8 @@ const Speed = Descriptor.ConstructedComponent(Codecs.number)("Numbers/Speed") const Shield = Descriptor.Component()("Numbers/Shield") const Target = Descriptor.Component()("Numbers/Target") const Label = Descriptor.ConstructedComponent(Codecs.string)("Numbers/Label") -const Game = Schema.bind(Schema.fragment({ components: { Health, Speed, Shield, Target, Label } })) +const Root = Schema.defineRoot("Numbers") +const Game = Schema.bind(Schema.fragment({ components: { Health, Speed, Shield, Target, Label } }), Root) const Flow = Game.StateMachine("Numbers/Flow", { Playing: {}, Menu: {} }) const makeRuntime = (debug = false) => Game.Runtime.make({ @@ -93,7 +94,7 @@ describe("number values", () => { it("round-trip through a snapshot", () => { // Every component validated on load, so the world can be saved. - const Saved = Schema.bind(Schema.fragment({ components: { Speed, Label } })) + const Saved = Schema.bind(Schema.fragment({ components: { Speed, Label } }), Root) const make = () => Saved.Runtime.make({ services: Saved.Runtime.services() }).value const Spawn = Saved.System("Numbers/SpawnSaved", { commands: Saved.System.commands(Saved.System.spawns(Speed, Label)) }, ({ commands }) => { for (let index = 0; index < 300; index++) commands.spawn(Saved.Command.spawn([Speed, index], [Label, `n${index}`])) diff --git a/packages/core/test/Runtime.ownership.test.ts b/packages/core/test/Runtime.ownership.test.ts index ace039e6..e625e2a5 100644 --- a/packages/core/test/Runtime.ownership.test.ts +++ b/packages/core/test/Runtime.ownership.test.ts @@ -10,11 +10,12 @@ const Samples = Descriptor.Component<{ readonly samples: Float32Array }>()("Owne const Tuning = Descriptor.Resource<{ speed: number }>()("Ownership/Tuning") const Hit = Descriptor.Event<{ x: number }>()("Ownership/Hit") +const Root = Schema.defineRoot("Ownership") const Game = Schema.bind(Schema.fragment({ components: { Body: BodyC, Samples }, resources: { Tuning }, events: { Hit } -})) +}), Root) const Mode = Game.StateMachine("Ownership/Mode", { Idle: {}, Moving: Codecs.struct({ speed: Codecs.number }) }) const makeRuntime = (debug: boolean, tuning = { speed: 1 }) => Game.Runtime.make({ diff --git a/packages/core/test/Runtime.pages.test.ts b/packages/core/test/Runtime.pages.test.ts index 2a93aabe..7b08e519 100644 --- a/packages/core/test/Runtime.pages.test.ts +++ b/packages/core/test/Runtime.pages.test.ts @@ -5,9 +5,10 @@ import type * as Entity from "@typeonce/ritmo/Entity" const Health = Descriptor.Component()("Pages/Health") const Position = Descriptor.Component<{ readonly x: number }>()("Pages/Position") -const Game = Schema.bind(Schema.fragment({ components: { Health, Position } })) +const Root = Schema.defineRoot("Pages") +const Game = Schema.bind(Schema.fragment({ components: { Health, Position } }), Root) -type Id = Entity.EntityId +type Id = Entity.EntityId const makeRuntime = () => Game.Runtime.make({ services: Game.Runtime.services() }).value diff --git a/packages/core/test/Runtime.provisions.test.ts b/packages/core/test/Runtime.provisions.test.ts index a1f7b253..94b1eadf 100644 --- a/packages/core/test/Runtime.provisions.test.ts +++ b/packages/core/test/Runtime.provisions.test.ts @@ -3,7 +3,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" import * as Runtime from "@typeonce/ritmo/Runtime" const Logger = Descriptor.Service<{ readonly log: (message: string) => void }>()("Provisions/Logger") -const Game = Schema.bind(Schema.fragment({})) +const Root = Schema.defineRoot("Provisions") +const Game = Schema.bind(Schema.fragment({}), Root) const Flow = Game.StateMachine("Provisions/Flow", { Menu: {}, Playing: {} }) describe("runtime provisions", () => { diff --git a/packages/core/test/Runtime.query-lookup.test.ts b/packages/core/test/Runtime.query-lookup.test.ts index 2269ad6b..d8c73030 100644 --- a/packages/core/test/Runtime.query-lookup.test.ts +++ b/packages/core/test/Runtime.query-lookup.test.ts @@ -821,11 +821,13 @@ describe("Runtime query and lookup", () => { }) }) +const Root = Schema.defineRoot("Optional") + describe("optional slots", () => { it("read their value, or undefined when the entity does not hold the component", () => { const Hp = Descriptor.Component()("Optional/Hp") const Name = Descriptor.Component()("Optional/Name") - const Local = Schema.bind(Schema.fragment({ components: { Hp, Name } })) + const Local = Schema.bind(Schema.fragment({ components: { Hp, Name } }), Root) const spawn = Local.System("Optional/Spawn", { commands: Local.System.commands(Local.System.spawns(Hp, Name)) }, ({ commands }) => { commands.spawn(Local.Command.spawn([Name, "full"], [Hp, 3])) commands.spawn(Local.Command.spawn([Name, "empty"])) @@ -841,7 +843,7 @@ describe("optional slots", () => { it("tell an absent component from a held component whose value is undefined", () => { const Note = Descriptor.Component()("Optional/Note") const Name = Descriptor.Component()("Optional/Name") - const Local = Schema.bind(Schema.fragment({ components: { Note, Name } })) + const Local = Schema.bind(Schema.fragment({ components: { Note, Name } }), Root) const spawn = Local.System("Optional/Spawn", { commands: Local.System.commands(Local.System.spawns(Note, Name)) }, ({ commands }) => { commands.spawn(Local.Command.spawn([Name, "blank"], [Note, undefined])) commands.spawn(Local.Command.spawn([Name, "none"])) @@ -857,7 +859,7 @@ describe("optional slots", () => { it("follow the entity gaining and losing the component while it keeps matching", () => { const Hp = Descriptor.Component()("Optional/Hp") const Name = Descriptor.Component()("Optional/Name") - const Local = Schema.bind(Schema.fragment({ components: { Hp, Name } })) + const Local = Schema.bind(Schema.fragment({ components: { Hp, Name } }), Root) const Named = Local.Query({ selection: { name: Local.Query.read(Name), hp: Local.Query.optional(Hp) } }) const spawn = Local.System("Optional/Spawn", { commands: Local.System.commands(Local.System.spawns(Name)) }, ({ commands }) => { commands.spawn(Local.Command.spawn([Name, "hero"])) @@ -886,7 +888,7 @@ describe("optional slots", () => { it("fail when read after the run that handed them out, held or not", () => { const Hp = Descriptor.Component()("Optional/Hp") const Name = Descriptor.Component()("Optional/Name") - const Local = Schema.bind(Schema.fragment({ components: { Hp, Name } })) + const Local = Schema.bind(Schema.fragment({ components: { Hp, Name } }), Root) const spawn = Local.System("Optional/Spawn", { commands: Local.System.commands(Local.System.spawns(Hp, Name)) }, ({ commands }) => { commands.spawn(Local.Command.spawn([Name, "none"])) }) diff --git a/packages/core/test/Runtime.relations.test.ts b/packages/core/test/Runtime.relations.test.ts index 7d03bffb..bf6a6d35 100644 --- a/packages/core/test/Runtime.relations.test.ts +++ b/packages/core/test/Runtime.relations.test.ts @@ -7,6 +7,7 @@ const Summary = Descriptor.Resource()("Summary") const { relation: ChildOf } = Descriptor.Hierarchy("ChildOf", "Children") const { relation: Targeting } = Descriptor.Relation("Targeting", "TargetedBy") +const Root = Schema.defineRoot("Runtime") const Game = Schema.bind(Schema.fragment({ components: { Name @@ -18,7 +19,7 @@ const Game = Schema.bind(Schema.fragment({ ChildOf, Targeting } -})) +}), Root) const schema = Game.schema const makeRuntime = () => @@ -499,7 +500,7 @@ describe("Runtime relationships", () => { const children = queries.named.children(rootId, ChildOf) const descendants = queries.named.descendants(rootId, ChildOf, { order: "breadth" }) const missing = queries.named.descendants( - unknownEntityId(999), + unknownEntityId(999), ChildOf ) @@ -641,7 +642,7 @@ describe("Runtime relationships", () => { } commands.unrelate(alphaId, Targeting) commands.unrelate(alphaId, Targeting) - commands.unrelate(unknownEntityId(999), Targeting) + commands.unrelate(unknownEntityId(999), Targeting) } ) @@ -702,7 +703,7 @@ describe("Runtime relationships", () => { if (!alphaId || !betaId) { return } - commands.relate(alphaId, Targeting, unknownEntityId(999)) + commands.relate(alphaId, Targeting, unknownEntityId(999)) commands.relate(alphaId, ChildOf, betaId) commands.relate(betaId, ChildOf, alphaId) } @@ -813,7 +814,7 @@ describe("Runtime relationships", () => { commands.reorderChildren(rootId, ChildOf, [secondId, secondId]) commands.reorderChildren(rootId, ChildOf, [firstId]) commands.reorderChildren(rootId, ChildOf, [firstId, unrelatedId]) - commands.reorderChildren(unknownEntityId(999), ChildOf, [firstId, secondId]) + commands.reorderChildren(unknownEntityId(999), ChildOf, [firstId, secondId]) } ) diff --git a/packages/core/test/Runtime.requirements.test.ts b/packages/core/test/Runtime.requirements.test.ts index 6a6648f7..99e4fdd7 100644 --- a/packages/core/test/Runtime.requirements.test.ts +++ b/packages/core/test/Runtime.requirements.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it } from "vitest" import { Descriptor, Schema } from "@typeonce/ritmo" +const Root = Schema.defineRoot("RuntimeRequirements") + describe("Runtime dynamic requirements", () => { it("reports missing nominal requirements before executing an erased schedule", () => { const Counter = Descriptor.Resource()("RuntimeRequirements/Counter") const Logger = Descriptor.Service<{ readonly log: (value: number) => void }>()("RuntimeRequirements/Logger") - const Game = Schema.bind(Schema.fragment({ resources: { Counter } })) + const Game = Schema.bind(Schema.fragment({ resources: { Counter } }), Root) const Phase = Game.StateMachine("RuntimeRequirements/Phase", { Ready: {}, Running: {} }) const run = Game.System("RuntimeRequirements/Run", { diff --git a/packages/core/test/Runtime.requires.test.ts b/packages/core/test/Runtime.requires.test.ts index 31c2abc4..04af1bab 100644 --- a/packages/core/test/Runtime.requires.test.ts +++ b/packages/core/test/Runtime.requires.test.ts @@ -10,10 +10,11 @@ const Trail = Descriptor.TransientComponent()("Requires/Trail") // A transient requirer may require a transient component: a restore drops both. const Ghost = Descriptor.TransientComponent<{}>()("Requires/Ghost") +const Root = Schema.defineRoot("Requires") const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity, Player, Trail, Ghost }, requires: [Schema.requires(Player, Position, Velocity), Schema.requires(Ghost, Trail)] -})) +}), Root) const makeRuntime = () => Game.Runtime.make({ services: Game.Runtime.services(), debug: { hostClock: HostClock.test() } }).value @@ -138,7 +139,7 @@ describe("Schema.requires at runtime", () => { const Saved = Descriptor.Tag("Requires/Saved") const fragment: Schema.Schema.Any = Schema.fragment({ components: { Saved, Trail } }) const requirement: Schema.Schema.Any = Schema.fragment({ requires: [Schema.requires(Saved, Trail)] }) - expect(() => Schema.bind(fragment, requirement)).toThrow( + expect(() => Schema.bind(fragment, requirement, Root)).toThrow( "Requires/Saved requires Requires/Trail, which is transient and not saved: make Requires/Saved transient too, or give Requires/Trail a constructor" ) }) diff --git a/packages/core/test/Runtime.resource-changed.test.ts b/packages/core/test/Runtime.resource-changed.test.ts index 329af880..8156d85a 100644 --- a/packages/core/test/Runtime.resource-changed.test.ts +++ b/packages/core/test/Runtime.resource-changed.test.ts @@ -10,7 +10,8 @@ const Score = Descriptor.ConstructedResource(Codecs.number)("Changed/Score") const Hud = Descriptor.ConstructedResource(Codecs.struct({ label: Codecs.string, tags: Codecs.array(Codecs.string) }))("Changed/Hud") const Open = Descriptor.ConstructedResource(Codecs.boolean)("Changed/Open") -const Game = Schema.bind(Schema.fragment({ resources: { Score, Hud, Open } })) +const Root = Schema.defineRoot("Changed") +const Game = Schema.bind(Schema.fragment({ resources: { Score, Hud, Open } }), Root) const unwrap = (made: { readonly ok: true; readonly value: A } | { readonly ok: false }): A => { if (!made.ok) throw new Error("invalid fixture") diff --git a/packages/core/test/Runtime.resources.test.ts b/packages/core/test/Runtime.resources.test.ts index 6fd7cf26..013f5532 100644 --- a/packages/core/test/Runtime.resources.test.ts +++ b/packages/core/test/Runtime.resources.test.ts @@ -15,6 +15,7 @@ const PrefixedLogger = Descriptor.Service<{ readonly log: (message: string) => v const Viewport = Descriptor.ConstructedResource(Codecs.safeSize)("Viewport") const Camera = Descriptor.ConstructedResource(Codecs.safeVector)("Camera") +const Root = Schema.defineRoot("RuntimeResources") const Game = Schema.bind(Schema.fragment({ resources: { DeltaTime: Time, @@ -23,7 +24,7 @@ const Game = Schema.bind(Schema.fragment({ CurrentPhase: Phase, Camera } -})) +}), Root) const schema = Game.schema const makeRuntime = () => { diff --git a/packages/core/test/Runtime.rewind-readers.test.ts b/packages/core/test/Runtime.rewind-readers.test.ts index abf3fc65..d132efae 100644 --- a/packages/core/test/Runtime.rewind-readers.test.ts +++ b/packages/core/test/Runtime.rewind-readers.test.ts @@ -13,11 +13,12 @@ const Pos = Descriptor.ConstructedComponent(Codecs.number)("RewindReaders/Pos") const Tint = Descriptor.ConstructedComponent(Codecs.number)("RewindReaders/Tint") const Score = Descriptor.ConstructedResource(Codecs.number)("RewindReaders/Score") const Go = Descriptor.TransientResource()("RewindReaders/Go") +const Root = Schema.defineRoot("RewindReaders") const Game = Schema.bind(Schema.fragment({ components: { Pos, Tint }, resources: { Tick, Seen, Score, Go }, events: { Ping } -})) +}), Root) const Flow = Game.StateMachine("RewindReaders/Flow", { Menu: {}, Playing: {} }) const SeenNow = Game.Inspector("RewindReaders/SeenNow", { resources: { seen: Game.System.readResource(Seen) } }, ({ resources }) => resources.seen.get()) diff --git a/packages/core/test/Runtime.rows.test.ts b/packages/core/test/Runtime.rows.test.ts index 4e7307b5..2601c14b 100644 --- a/packages/core/test/Runtime.rows.test.ts +++ b/packages/core/test/Runtime.rows.test.ts @@ -5,7 +5,8 @@ const Position = Descriptor.Component<{ readonly x: number; readonly y: number } const Health = Descriptor.Component()("Rows/Health") const Shield = Descriptor.Component<{ readonly charges: number }>()("Rows/Shield") const Enemy = Descriptor.Tag("Rows/Enemy") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Shield, Enemy } })) +const Root = Schema.defineRoot("Rows") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Shield, Enemy } }), Root) const make = (debug = false) => { const made = Game.Runtime.make({ services: Game.Runtime.services(), ...(debug ? { debug: { hostClock: HostClock.test() } } : {}) }) diff --git a/packages/core/test/Runtime.scheduling.test.ts b/packages/core/test/Runtime.scheduling.test.ts index 77717ae6..63751b82 100644 --- a/packages/core/test/Runtime.scheduling.test.ts +++ b/packages/core/test/Runtime.scheduling.test.ts @@ -8,12 +8,13 @@ import { readResourceValue } from "./utils/fixtures.ts" const Counter = Descriptor.Resource()("Counter") const Log = Descriptor.Resource>()("Log") +const Root = Schema.defineRoot("Runtime") const Game = Schema.bind(Schema.fragment({ resources: { Counter, Log } -})) +}), Root) const schema = Game.schema const makeRuntime = () => diff --git a/packages/core/test/Runtime.scope-triggers.test.ts b/packages/core/test/Runtime.scope-triggers.test.ts index 97c77ae5..d9e36f55 100644 --- a/packages/core/test/Runtime.scope-triggers.test.ts +++ b/packages/core/test/Runtime.scope-triggers.test.ts @@ -4,10 +4,11 @@ import type * as Debug from "@typeonce/ritmo/Debug" const Name = Descriptor.Component()("ScopeTriggers/Name") const Player = Descriptor.Component<{}>()("ScopeTriggers/Player") +const Root = Schema.defineRoot("ScopeTriggers") const Game = Schema.bind(Schema.fragment({ components: { Name, Player }, requires: [Schema.requires(Player, Name)] -})) +}), Root) const Flow = Game.StateMachine("ScopeTriggers/Flow", { Menu: {}, Playing: {}, Paused: {} }) const Run = Game.EntityScope("ScopeTriggers/Run", { despawnOnExit: [Flow, "Playing"] }) const Overlay = Game.EntityScope("ScopeTriggers/Overlay", { despawnOnEnter: [Flow, "Menu"] }) diff --git a/packages/core/test/Runtime.snapshot-migrations.test.ts b/packages/core/test/Runtime.snapshot-migrations.test.ts index 72e93861..416e8b4b 100644 --- a/packages/core/test/Runtime.snapshot-migrations.test.ts +++ b/packages/core/test/Runtime.snapshot-migrations.test.ts @@ -4,7 +4,8 @@ import * as Codecs from "./codecs.ts" import { worldOf } from "./saved.ts" const Gold = Descriptor.ConstructedResource(Codecs.number)("Migrations/Gold") -const Game = Schema.bind(Schema.fragment({ resources: { Gold } })) +const Root = Schema.defineRoot("Migrations") +const Game = Schema.bind(Schema.fragment({ resources: { Gold } }), Root) const Coins = Game.Inspector("Migrations/Coins", { resources: { gold: Game.System.readResource(Gold) } }, ({ resources }) => resources.gold.get()) const saves = Snapshot.migrations( diff --git a/packages/core/test/Runtime.snapshot-scopes.test.ts b/packages/core/test/Runtime.snapshot-scopes.test.ts index 3b7236c1..cb103f1f 100644 --- a/packages/core/test/Runtime.snapshot-scopes.test.ts +++ b/packages/core/test/Runtime.snapshot-scopes.test.ts @@ -5,7 +5,8 @@ import { worldOf } from "./saved.ts" const Name = Descriptor.ConstructedComponent(Codecs.string)("SnapshotScopes/Name") const { relation: ChildOf } = Descriptor.Hierarchy("SnapshotScopes/ChildOf", "SnapshotScopes/Children") -const Game = Schema.bind(Schema.fragment({ components: { Name }, relations: { ChildOf } })) +const Root = Schema.defineRoot("SnapshotScopes") +const Game = Schema.bind(Schema.fragment({ components: { Name }, relations: { ChildOf } }), Root) const Level = Game.EntityScope("SnapshotScopes/Level") const Menu = Game.EntityScope("SnapshotScopes/Menu") diff --git a/packages/core/test/Runtime.stability.test.ts b/packages/core/test/Runtime.stability.test.ts index 18a22148..2d56d27d 100644 --- a/packages/core/test/Runtime.stability.test.ts +++ b/packages/core/test/Runtime.stability.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest" import { Descriptor, Result, Schema } from "@typeonce/ritmo" +const Root = Schema.defineRoot("Stability") + describe("Runtime stabilization APIs", () => { it("rolls back ECS writes and commands from an expected system failure", () => { const Position = Descriptor.Component<{ x: number }>()("Stability/Position") @@ -11,7 +13,7 @@ describe("Runtime stabilization APIs", () => { components: { Position }, resources: { Count }, events: { Signal } - })) + }), Root) const Mode = Game.StateMachine("Stability/Mode", { Ready: {}, Broken: {} }) const Positions = Game.Query({ selection: { @@ -97,7 +99,7 @@ describe("Runtime stabilization APIs", () => { it("despawns one entity scope without affecting persistent entities", () => { const Actor = Descriptor.Component<{ name: string }>()("Scopes/Actor") - const Game = Schema.bind(Schema.fragment({ components: { Actor } })) + const Game = Schema.bind(Schema.fragment({ components: { Actor } }), Root) const Level = Game.EntityScope("Scopes/Level") const Actors = Game.Query({ selection: { actor: Game.Query.read(Actor) } @@ -136,7 +138,7 @@ describe("Runtime stabilization APIs", () => { const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Score } - })) + }), Root) const spawn = Game.System("Inspector/Spawn", { commands: Game.System.commands(Game.System.spawns(Position)) @@ -170,7 +172,7 @@ describe("Runtime stabilization APIs", () => { }) it("propagates an expected failure from a state-transition schedule", () => { - const Game = Schema.bind(Schema.fragment({})) + const Game = Schema.bind(Schema.fragment({}), Root) const Mode = Game.StateMachine("TransitionFailure/Mode", { Menu: {}, Playing: {} }) const start = Game.System( "TransitionFailure/Start", diff --git a/packages/core/test/Runtime.stale-entity.test.ts b/packages/core/test/Runtime.stale-entity.test.ts index 2148c41b..6513debb 100644 --- a/packages/core/test/Runtime.stale-entity.test.ts +++ b/packages/core/test/Runtime.stale-entity.test.ts @@ -3,7 +3,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ readonly x: number }>()("Stale/Position") const Marker = Descriptor.Component<{}>()("Stale/Marker") -const Game = Schema.bind(Schema.fragment({ components: { Position, Marker } })) +const Root = Schema.defineRoot("Stale") +const Game = Schema.bind(Schema.fragment({ components: { Position, Marker } }), Root) const Positions = Game.Query({ selection: { position: Game.Query.write(Position) } }) diff --git a/packages/core/test/Runtime.stale-target.test.ts b/packages/core/test/Runtime.stale-target.test.ts index df824254..d2859644 100644 --- a/packages/core/test/Runtime.stale-target.test.ts +++ b/packages/core/test/Runtime.stale-target.test.ts @@ -4,13 +4,14 @@ import type * as Command from "@typeonce/ritmo/Command" const Boss = Descriptor.Component<{}>()("StaleTarget/Boss") const Mark = Descriptor.Component<{}>()("StaleTarget/Mark") -const Game = Schema.bind(Schema.fragment({ components: { Boss, Mark } })) +const Root = Schema.defineRoot("StaleTarget") +const Game = Schema.bind(Schema.fragment({ components: { Boss, Mark } }), Root) const Bosses = Game.Inspector("StaleTarget/Bosses", { queries: { bosses: Game.Query({ selection: { boss: Game.Query.read(Boss) } }) } }, ({ queries }) => queries.bosses.each().length) describe("a spawned id used after its run", () => { it("is skipped by despawn and remove, traced, and linted", () => { const runtime = Game.Runtime.make({ services: Game.Runtime.services(), debug: { hostClock: HostClock.test() } }).value - let kept: Command.SpawnedId | undefined + let kept: Command.SpawnedId | undefined const Spawn = Game.System("StaleTarget/Spawn", { commands: Game.System.commands(Game.System.spawns(Boss, Mark)) }, ({ commands }) => { kept = commands.spawn(Game.Command.spawn([Boss, {}], [Mark, {}])) }) diff --git a/packages/core/test/Runtime.state-machine-data.test.ts b/packages/core/test/Runtime.state-machine-data.test.ts index 49d6cd71..a46688d5 100644 --- a/packages/core/test/Runtime.state-machine-data.test.ts +++ b/packages/core/test/Runtime.state-machine-data.test.ts @@ -4,7 +4,8 @@ import * as Codecs from "./codecs.ts" import { worldOf } from "./saved.ts" const Log = Descriptor.TransientResource>()("MachineData/Log") -const Game = Schema.bind(Schema.fragment({ resources: { Log } })) +const Root = Schema.defineRoot("MachineData") +const Game = Schema.bind(Schema.fragment({ resources: { Log } }), Root) const Mode = Game.StateMachine("MachineData/Mode", { Exploring: {}, diff --git a/packages/core/test/Runtime.state-machine.test.ts b/packages/core/test/Runtime.state-machine.test.ts index 142554b3..0423342b 100644 --- a/packages/core/test/Runtime.state-machine.test.ts +++ b/packages/core/test/Runtime.state-machine.test.ts @@ -7,12 +7,13 @@ import { readResourceValue } from "./utils/fixtures.ts" const Counter = Descriptor.Resource()("StateMachineRuntime/Counter") const Log = Descriptor.Resource>()("StateMachineRuntime/Log") +const Root = Schema.defineRoot("StateMachineRuntime") const Game = Schema.bind(Schema.fragment({ resources: { Counter, Log } -})) +}), Root) const schema = Game.schema const AppState = Game.StateMachine("AppState", { Menu: {}, Playing: {}, Paused: {} }) const RoundState = Game.StateMachine("RoundState", { Warmup: {}, Live: {}, SuddenDeath: {} }) @@ -442,7 +443,7 @@ describe("Runtime state machine", () => { }) it("applies multiple machine transitions in definition order", () => { - const LocalGame = Schema.bind(schema) + const LocalGame = Schema.bind(schema, Root) const LocalAppState = LocalGame.StateMachine("LocalAppState", { Menu: {}, Playing: {}, Paused: {} }) const LocalRoundState = LocalGame.StateMachine("LocalRoundState", { Warmup: {}, Live: {}, SuddenDeath: {} }) @@ -522,7 +523,7 @@ describe("Runtime state machine", () => { }) it("defers transitions queued during transition schedules to the next marker", () => { - const LocalGame = Schema.bind(schema) + const LocalGame = Schema.bind(schema, Root) const LocalAppState = LocalGame.StateMachine("LocalAppStateDeferred", { Menu: {}, Playing: {}, Paused: {} }) const LocalRoundState = LocalGame.StateMachine("LocalRoundStateDeferred", { Warmup: {}, Live: {}, SuddenDeath: {} }) @@ -625,7 +626,7 @@ describe("Runtime state machine", () => { }) it("commits a request queued before the marker even when a transition schedule queues the same machine", () => { - const LocalGame = Schema.bind(schema) + const LocalGame = Schema.bind(schema, Root) const LocalAppState = LocalGame.StateMachine("LocalAppStateReplaced", { Menu: {}, Playing: {}, Paused: {} }) const LocalRoundState = LocalGame.StateMachine("LocalRoundStateReplaced", { Warmup: {}, Live: {}, SuddenDeath: {} }) @@ -669,7 +670,7 @@ describe("Runtime state machine", () => { }) it("drops a request queued before the marker when a transition schedule resets it", () => { - const LocalGame = Schema.bind(schema) + const LocalGame = Schema.bind(schema, Root) const LocalAppState = LocalGame.StateMachine("LocalAppStateReset", { Menu: {}, Playing: {}, Paused: {} }) const LocalRoundState = LocalGame.StateMachine("LocalRoundStateReset", { Warmup: {}, Live: {}, SuddenDeath: {} }) @@ -712,7 +713,7 @@ describe("Runtime state machine", () => { }) it("reports a transition schedule's machine as a missing requirement in tryTick", () => { - const LocalGame = Schema.bind(schema) + const LocalGame = Schema.bind(schema, Root) const LocalAppState = LocalGame.StateMachine("LocalAppStateMissing", { Menu: {}, Playing: {} }) const bundle = LocalGame.Schedule.transitions(LocalGame.Schedule.onEnter(LocalAppState, "Playing", [])) const runtime = LocalGame.Runtime.make({ services: Runtime.services(), resources: { Counter: 0, Log: [] } }).value @@ -870,7 +871,7 @@ describe("Runtime state machine", () => { }) it("emits transition events in machine definition order", () => { - const LocalGame = Schema.bind(schema) + const LocalGame = Schema.bind(schema, Root) const LocalAppState = LocalGame.StateMachine("LocalAppStateEvents", { Menu: {}, Playing: {}, Paused: {} }) const LocalRoundState = LocalGame.StateMachine("LocalRoundStateEvents", { Warmup: {}, Live: {}, SuddenDeath: {} }) @@ -1143,7 +1144,7 @@ describe("Runtime state machine", () => { }) it("rejects duplicate machine names on one bound game", () => { - const LocalGame = Schema.bind(schema) + const LocalGame = Schema.bind(schema, Root) LocalGame.StateMachine("DuplicateMachine", { A: {}, B: {} }) expect(() => diff --git a/packages/core/test/Runtime.stepping.test.ts b/packages/core/test/Runtime.stepping.test.ts index 95dc20c8..3480dd77 100644 --- a/packages/core/test/Runtime.stepping.test.ts +++ b/packages/core/test/Runtime.stepping.test.ts @@ -6,7 +6,8 @@ const hostClock = HostClock.test() const Position = Descriptor.Component<{ readonly x: number }>()("Stepping/Position") const Ping = Descriptor.Event()("Stepping/Ping") const Clock = Descriptor.Service<{ readonly now: () => number }>()("Stepping/Clock") -const Game = Schema.bind(Schema.fragment({ components: { Position }, events: { Ping } })) +const Root = Schema.defineRoot("Stepping") +const Game = Schema.bind(Schema.fragment({ components: { Position }, events: { Ping } }), Root) const Flow = Game.StateMachine("Stepping/Flow", { Menu: {}, Playing: {} }) const Spawn = Game.System("Stepping/Spawn", { commands: Game.System.commands(Game.System.spawns(Position)) }, ({ commands }) => { diff --git a/packages/core/test/Runtime.storage-model.test.ts b/packages/core/test/Runtime.storage-model.test.ts index a0159fb6..be0bb9b0 100644 --- a/packages/core/test/Runtime.storage-model.test.ts +++ b/packages/core/test/Runtime.storage-model.test.ts @@ -18,10 +18,11 @@ type Value = number | { readonly a: number } | string | symbol | null | undefine const Cell = Descriptor.Component()("Model/Cell") const Left = Descriptor.Component()("Model/Left") const Right = Descriptor.Component()("Model/Right") -const Game = Schema.bind(Schema.fragment({ components: { Cell, Left, Right } })) +const Root = Schema.defineRoot("Model") +const Game = Schema.bind(Schema.fragment({ components: { Cell, Left, Right } }), Root) const Flow = Game.StateMachine("Model/Flow", { A: {}, B: {} }) -type Id = Entity.EntityId +type Id = Entity.EntityId type Via = "left" | "right" type FlowState = "A" | "B" diff --git a/packages/core/test/Runtime.storage.test.ts b/packages/core/test/Runtime.storage.test.ts index 775bc468..a4cdfcef 100644 --- a/packages/core/test/Runtime.storage.test.ts +++ b/packages/core/test/Runtime.storage.test.ts @@ -7,13 +7,14 @@ const Tag = Descriptor.Component<{}>()("Storage/Tag") const Log = Descriptor.Resource>()("Storage/Log") const { relation: ChildOf } = Descriptor.Hierarchy("Storage/ChildOf", "Storage/Children") +const Root = Schema.defineRoot("Storage") const Game = Schema.bind(Schema.fragment({ components: { Position, Tag }, resources: { Log }, relations: { ChildOf } -})) +}), Root) -type Id = Entity.EntityId +type Id = Entity.EntityId const makeRuntime = () => Game.Runtime.make({ services: Game.Runtime.services(), diff --git a/packages/core/test/Runtime.stream-loss.test.ts b/packages/core/test/Runtime.stream-loss.test.ts index 0cc42f6b..6d968c11 100644 --- a/packages/core/test/Runtime.stream-loss.test.ts +++ b/packages/core/test/Runtime.stream-loss.test.ts @@ -3,7 +3,8 @@ import { Descriptor, HostClock, Schema } from "@typeonce/ritmo" import * as Runtime from "@typeonce/ritmo/Runtime" const Hit = Descriptor.Event()("StreamLoss/Hit") -const Game = Schema.bind(Schema.fragment({ events: { Hit } })) +const Root = Schema.defineRoot("StreamLoss") +const Game = Schema.bind(Schema.fragment({ events: { Hit } }), Root) const Running = Game.Condition.check("StreamLoss/Running", {}, () => true) const Emit = Game.System("StreamLoss/Emit", { events: { hit: Game.System.writeEvent(Hit) } }, ({ events }) => { diff --git a/packages/core/test/Runtime.sub-states.test.ts b/packages/core/test/Runtime.sub-states.test.ts index 459d414a..6c8350c3 100644 --- a/packages/core/test/Runtime.sub-states.test.ts +++ b/packages/core/test/Runtime.sub-states.test.ts @@ -4,7 +4,8 @@ import * as Codecs from "./codecs.ts" import { worldOf } from "./saved.ts" const Name = Descriptor.ConstructedComponent(Codecs.string)("SubStates/Name") -const Game = Schema.bind(Schema.fragment({ components: { Name } })) +const Root = Schema.defineRoot("SubStates") +const Game = Schema.bind(Schema.fragment({ components: { Name } }), Root) const Session = Game.StateMachine("SubStates/Session", { Menu: {}, Playing: {} }) const Phase = Game.StateMachine("SubStates/Phase", { Setup: {}, Battle: {} }, { within: [Session, "Playing"] }) const Turn = Game.StateMachine("SubStates/Turn", { Mine: {}, Theirs: {} }, { within: [Phase, "Battle"] }) diff --git a/packages/core/test/Schedule.when.test.ts b/packages/core/test/Schedule.when.test.ts index 119ee3f8..a29f83ba 100644 --- a/packages/core/test/Schedule.when.test.ts +++ b/packages/core/test/Schedule.when.test.ts @@ -6,7 +6,8 @@ const Log = Descriptor.Resource>()("ScheduleWhen/Log") const Ping = Descriptor.Event()("ScheduleWhen/Ping") const Marker = Descriptor.Tag("ScheduleWhen/Marker") -const Game = Schema.bind(Schema.fragment({ components: { Marker }, resources: { Log }, events: { Ping } })) +const Root = Schema.defineRoot("ScheduleWhen") +const Game = Schema.bind(Schema.fragment({ components: { Marker }, resources: { Log }, events: { Ping } }), Root) const Mode = Game.StateMachine("ScheduleWhen/Mode", { Playing: {}, Paused: {} }) const Round = Game.StateMachine("ScheduleWhen/Round", { Live: {}, Over: {} }) diff --git a/packages/core/test/Schema.requires.test.ts b/packages/core/test/Schema.requires.test.ts index 48960066..d138d3f5 100644 --- a/packages/core/test/Schema.requires.test.ts +++ b/packages/core/test/Schema.requires.test.ts @@ -6,12 +6,14 @@ const Velocity = Descriptor.Component<{ x: number }>()("Requires/Velocity") const Body = Descriptor.Component<{}>()("Requires/Body") const Player = Descriptor.Component<{}>()("Requires/Player") +const Root = Schema.defineRoot("Requires") const Game = Schema.bind( Schema.fragment({ components: { Position, Velocity, Body } }), Schema.fragment({ components: { Player }, requires: [Schema.requires(Player, Body, Velocity), Schema.requires(Body, Position)] - }) + }), + Root ) describe("Schema.requires", () => { @@ -33,7 +35,7 @@ describe("Schema.requires", () => { it("rejects an erased requirement naming an unregistered component", () => { const erased = Schema.fragment({ components: { Player }, requires: [Schema.requires(Player, Position)] }) as Schema.Schema.Any - expect(() => Schema.bind(erased)).toThrow("Requires/Position is named in Schema.requires(...) but no fragment registers it") + expect(() => Schema.bind(erased, Root)).toThrow("Requires/Position is named in Schema.requires(...) but no fragment registers it") }) }) diff --git a/packages/core/test/SchemaLibraries.test.ts b/packages/core/test/SchemaLibraries.test.ts index 804c3fcd..20638956 100644 --- a/packages/core/test/SchemaLibraries.test.ts +++ b/packages/core/test/SchemaLibraries.test.ts @@ -33,9 +33,10 @@ const ArkTypeDate = Descriptor.ConstructedResource({ "~encode": type("Date").pipe((date) => date.toISOString()) })("Libraries/ArkTypeDate") +const Root = Schema.defineRoot("Libraries") const Game = Schema.bind(Schema.fragment({ resources: { ZodPlain, ValibotPlain, ArkTypePlain, EffectPlain, ZodDate, EffectMap, ValibotDate, ArkTypeDate } -})) +}), Root) // A state's data from Zod as it is, beside a state without data. const Mode = Game.StateMachine("Libraries/Mode", { Idle: {}, Racing: z.object({ lap: z.number().int() }) }) diff --git a/packages/core/test/ThisTick.test.ts b/packages/core/test/ThisTick.test.ts index f2e17d16..b74d08cd 100644 --- a/packages/core/test/ThisTick.test.ts +++ b/packages/core/test/ThisTick.test.ts @@ -5,7 +5,8 @@ import type { ExecutableScheduleDefinition } from "@typeonce/ritmo/Schedule" const hostClock = HostClock.test() const Damage = Descriptor.Event()("ThisTickRuntime/Damage") -const Game = Schema.bind(Schema.fragment({ events: { Damage } })) +const Root = Schema.defineRoot("ThisTickRuntime") +const Game = Schema.bind(Schema.fragment({ events: { Damage } }), Root) const Deal = Game.System("ThisTickRuntime/Deal", { events: { damage: Game.System.writeEvent(Damage) } }, ({ events }) => { events.damage.emit(1) @@ -18,8 +19,8 @@ const Apply = Game.System("ThisTickRuntime/Apply", { }) /** Erases a schedule's type, as plugin boundaries and devtools do. */ -const erased = (schedule: ExecutableScheduleDefinition): ExecutableScheduleDefinition => - schedule as ExecutableScheduleDefinition +const erased = (schedule: ExecutableScheduleDefinition): ExecutableScheduleDefinition => + schedule as ExecutableScheduleDefinition describe("thisTick at runtime", () => { it("lets tryTick run a schedule whose writers come first", () => { diff --git a/packages/core/test/ThisTickQueries.test.ts b/packages/core/test/ThisTickQueries.test.ts index 749e7282..0a8642fa 100644 --- a/packages/core/test/ThisTickQueries.test.ts +++ b/packages/core/test/ThisTickQueries.test.ts @@ -6,7 +6,8 @@ const hostClock = HostClock.test() const Position = Descriptor.Component()("TickQuery/Position") const Enemy = Descriptor.Component<{}>()("TickQuery/Enemy") -const Game = Schema.bind(Schema.fragment({ components: { Position, Enemy } })) +const Root = Schema.defineRoot("TickQuery") +const Game = Schema.bind(Schema.fragment({ components: { Position, Enemy } }), Root) const Flow = Game.StateMachine("TickQuery/Flow", { Menu: {}, Playing: {} }) const Enemies = Game.Query({ selection: { position: Game.Query.read(Position) }, with: [Enemy] }) @@ -21,8 +22,8 @@ const Aim = Game.System("TickQuery/Aim", { queries: { enemies: Game.System.thisT }) /** Erases a schedule's type, as plugin boundaries and devtools do. */ -const erased = (schedule: ExecutableScheduleDefinition): ExecutableScheduleDefinition => - schedule as ExecutableScheduleDefinition +const erased = (schedule: ExecutableScheduleDefinition): ExecutableScheduleDefinition => + schedule as ExecutableScheduleDefinition const machines = Game.Runtime.machines(Game.Runtime.machine(Flow, { _tag: "Menu" })) const make = () => Game.Runtime.make({ services: Game.Runtime.services(), machines }).value diff --git a/packages/core/test/World.pages.test.ts b/packages/core/test/World.pages.test.ts index ade91e9e..cf0d613d 100644 --- a/packages/core/test/World.pages.test.ts +++ b/packages/core/test/World.pages.test.ts @@ -6,7 +6,8 @@ const Health = Descriptor.Component()("WorldPages/Health") const Speed = Descriptor.Component()("WorldPages/Speed") const Name = Descriptor.Component()("WorldPages/Name") const Armor = Descriptor.Component<{ readonly value: number }>()("WorldPages/Armor") -const Game = Schema.bind(Schema.fragment({ components: { Health, Speed, Name, Armor } })) +const Root = Schema.defineRoot("WorldPages") +const Game = Schema.bind(Schema.fragment({ components: { Health, Speed, Name, Armor } }), Root) const components = [Health, Speed, Name, Armor] as const diff --git a/packages/devtools/dtslint/Determinism.tst.ts b/packages/devtools/dtslint/Determinism.tst.ts index 00b9de13..52210da7 100644 --- a/packages/devtools/dtslint/Determinism.tst.ts +++ b/packages/devtools/dtslint/Determinism.tst.ts @@ -3,7 +3,8 @@ import { Determinism } from "@typeonce/ritmo-devtools" import { describe, expect, it } from "tstyche" const Count = Descriptor.Resource()("DetTypes/Count") -const Game = Schema.bind(Schema.fragment({ resources: { Count } })) +const Root = Schema.defineRoot("DetTypes") +const Game = Schema.bind(Schema.fragment({ resources: { Count } }), Root) const make = ({ hostClock }: Determinism.Context) => ({ runtime: Game.Runtime.make({ services: Game.Runtime.services(), resources: { Count: 0 }, debug: { hostClock } }).value, schedules: { update: Game.Schedule() } diff --git a/packages/devtools/dtslint/Devtools.tst.ts b/packages/devtools/dtslint/Devtools.tst.ts index d926de1e..e51b2176 100644 --- a/packages/devtools/dtslint/Devtools.tst.ts +++ b/packages/devtools/dtslint/Devtools.tst.ts @@ -13,8 +13,10 @@ const Other = Descriptor.Component<{ readonly y: number }>()("DevtoolsTypes/Othe const Limit = Descriptor.Resource()("DevtoolsTypes/Limit") const Request = Descriptor.Event()("DevtoolsTypes/Request") const Foreign = Descriptor.Event()("DevtoolsTypes/ForeignRequest") -const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Limit }, events: { Request } })) -const OtherGame = Schema.bind(Schema.fragment({ components: { Other } })) +const Root = Schema.defineRoot("DevtoolsTypes") +const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Limit }, events: { Request } }), Root) +const OtherRoot = Schema.defineRoot("DevtoolsTypes/Other") +const OtherGame = Schema.bind(Schema.fragment({ components: { Other } }), OtherRoot) const update = Game.Schedule(Game.System("DevtoolsTypes/Noop", {}, () => {})) describe("Session.make", () => { @@ -172,7 +174,7 @@ describe("Replay.recorder", () => { const Rules = Descriptor.Service<{ readonly speed: number; readonly names: ReadonlyArray }>()("DevtoolsTypes/Rules") const Page = Descriptor.Service<{ readonly toast: (message: string) => void }>()("DevtoolsTypes/Page") const Mapped = Descriptor.Service>>()("DevtoolsTypes/Mapped") - const Saved = Schema.bind(Schema.fragment({ components: { Place } })) + const Saved = Schema.bind(Schema.fragment({ components: { Place } }), Root) const runtime = Saved.Runtime.make({ services: Saved.Runtime.services( Saved.Runtime.service(Keys, { snapshot: () => ({ left: false }) }), @@ -205,7 +207,7 @@ describe("Replay.recorder", () => { it("requires a runtime whose world can be saved", () => { const Loose = Descriptor.Component<{ readonly x: number }>()("DevtoolsTypes/Loose") - const Unsaved = Schema.bind(Schema.fragment({ components: { Loose } })) + const Unsaved = Schema.bind(Schema.fragment({ components: { Loose } }), Root) const unsaved = Unsaved.Runtime.make({ services: Unsaved.Runtime.services(), debug: { hostClock } }).value const update = Unsaved.Schedule(Unsaved.System("DevtoolsTypes/Unsaved", {}, () => {})) expect(Replay.recorder).type.not.toBeCallableWith(unsaved, { ...base, schedule: update }) @@ -226,7 +228,7 @@ describe("Replay.recorder", () => { describe("Replay.session", () => { const Place = Descriptor.ConstructedComponent(Codecs.struct({ x: Codecs.number }))("DevtoolsTypes/ReplayPlace") const Cap = Descriptor.ConstructedResource(Codecs.number)("DevtoolsTypes/ReplayCap") - const Saved = Schema.bind(Schema.fragment({ components: { Place }, resources: { Cap } })) + const Saved = Schema.bind(Schema.fragment({ components: { Place }, resources: { Cap } }), Root) const make = () => ({ runtime: Saved.Runtime.make({ services: Saved.Runtime.services(), debug: { hostClock } }).value, schedules: { update: Saved.Schedule() }, diff --git a/packages/devtools/dtslint/Fuzz.tst.ts b/packages/devtools/dtslint/Fuzz.tst.ts index 32edb38c..ba8a6667 100644 --- a/packages/devtools/dtslint/Fuzz.tst.ts +++ b/packages/devtools/dtslint/Fuzz.tst.ts @@ -21,7 +21,8 @@ interface MarkedSource extends System.Consumes<"snapshot"> { readonly snapshot: () => { readonly key: number } } const Page = Descriptor.Service<{ readonly toast: (message: string) => void }>()("FuzzTypes/Page") -const Game = Schema.bind(Schema.fragment({ components: { Place }, resources: { Limit } })) +const Root = Schema.defineRoot("FuzzTypes") +const Game = Schema.bind(Schema.fragment({ components: { Place }, resources: { Limit } }), Root) const update = Game.Schedule(Game.System("FuzzTypes/Move", { services: { pad: Game.System.service(Pad), rules: Game.System.service(Rules) } }, () => {})) const setup = Game.Schedule() diff --git a/packages/devtools/dtslint/Verify.tst.ts b/packages/devtools/dtslint/Verify.tst.ts index 7141ce1d..1c197d66 100644 --- a/packages/devtools/dtslint/Verify.tst.ts +++ b/packages/devtools/dtslint/Verify.tst.ts @@ -6,7 +6,8 @@ import * as PlaywrightEngine from "@typeonce/ritmo-devtools/PlaywrightEngine" import { describe, expect, it } from "tstyche" const Input = Descriptor.Service>()("VerifyTypes/Input") -const Game = Schema.bind(Schema.fragment({})) +const Root = Schema.defineRoot("VerifyTypes") +const Game = Schema.bind(Schema.fragment({}), Root) const Move = Game.System("VerifyTypes/Move", { services: { input: Game.System.service(Input) } }, () => {}) const hostClock = HostClock.test() const make = (playback: Replay.Playback) => ({ diff --git a/packages/devtools/dtslint/Why.tst.ts b/packages/devtools/dtslint/Why.tst.ts index 087fe67a..eb6b4f8a 100644 --- a/packages/devtools/dtslint/Why.tst.ts +++ b/packages/devtools/dtslint/Why.tst.ts @@ -6,7 +6,8 @@ const hostClock = HostClock.test() const Health = Descriptor.Component()("WhyTypes/Health") const Score = Descriptor.Resource()("WhyTypes/Score") -const Game = Schema.bind(Schema.fragment({ components: { Health }, resources: { Score } })) +const Root = Schema.defineRoot("WhyTypes") +const Game = Schema.bind(Schema.fragment({ components: { Health }, resources: { Score } }), Root) const update = Game.Schedule(Game.System("WhyTypes/Noop", {}, () => {})) const session = Session.make(Game.Runtime.make({ services: Game.Runtime.services(), resources: { Score: 0 }, debug: { hostClock } }).value, { hostClock, schedules: { update } }) diff --git a/packages/devtools/test/Determinism.test.ts b/packages/devtools/test/Determinism.test.ts index 368ed526..d5b9d6ea 100644 --- a/packages/devtools/test/Determinism.test.ts +++ b/packages/devtools/test/Determinism.test.ts @@ -4,7 +4,8 @@ import { Determinism } from "@typeonce/ritmo-devtools" const Count = Descriptor.Resource()("Repeat/Count") const Scratch = Descriptor.TransientComponent<{ value: number }>()("Repeat/Scratch") -const Game = Schema.bind(Schema.fragment({ resources: { Count }, components: { Scratch } })) +const Root = Schema.defineRoot("Repeat") +const Game = Schema.bind(Schema.fragment({ resources: { Count }, components: { Scratch } }), Root) const options = { schedule: "update", frames: 4, hostClock: HostClock.test() } as const const factory = (next: (value: number) => number) => ({ hostClock }: Determinism.Context) => { const runtime = Game.Runtime.make({ resources: { Count: 0 }, services: Game.Runtime.services(), debug: { hostClock } }).value @@ -50,7 +51,7 @@ describe("Determinism.check", () => { it("copies mutable values before the next run, including maps and cycles", () => { const Data = Descriptor.Resource<{ map: Map; self?: unknown }>()("Repeat/Data") - const Mutable = Schema.bind(Schema.fragment({ resources: { Data } })) + const Mutable = Schema.bind(Schema.fragment({ resources: { Data } }), Root) let counter = 0 const shared: { map: Map; self?: unknown } = { map: new Map() } shared.self = shared diff --git a/packages/devtools/test/DroppedRelations.test.ts b/packages/devtools/test/DroppedRelations.test.ts index 36e24706..bd388062 100644 --- a/packages/devtools/test/DroppedRelations.test.ts +++ b/packages/devtools/test/DroppedRelations.test.ts @@ -6,7 +6,8 @@ const hostClock = HostClock.test() const Name = Descriptor.Component()("Game/Name") const { relation: Targeting } = Descriptor.Relation("Game/Targeting", "Game/TargetedBy") -const Game = Schema.bind(Schema.fragment({ components: { Name }, relations: { Targeting } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ components: { Name }, relations: { Targeting } }), Root) const Named = Game.Query({ selection: { name: Game.Query.read(Name) } }) diff --git a/packages/devtools/test/Fuzz.test.ts b/packages/devtools/test/Fuzz.test.ts index c66c3345..0babfc80 100644 --- a/packages/devtools/test/Fuzz.test.ts +++ b/packages/devtools/test/Fuzz.test.ts @@ -10,7 +10,8 @@ const Progress = Descriptor.ConstructedResource(Codecs.number)("Fuzz/Progress") type Pad = { readonly key: number; readonly aim: number } const PadInput = Descriptor.Service>()("Fuzz/Pad") const Rules = Descriptor.Service<{ readonly combo: ReadonlyArray }>()("Fuzz/Rules") -const Game = Schema.bind(Schema.fragment({ components: { Health }, resources: { Progress } })) +const Root = Schema.defineRoot("Fuzz") +const Game = Schema.bind(Schema.fragment({ components: { Health }, resources: { Progress } }), Root) const Spawn = Game.System("Fuzz/Spawn", { commands: Game.System.commands(Game.System.spawns(Health)) }, ({ commands }) => { const draft = Game.Command.spawn(Game.Command.entryRaw(Health, 3)) diff --git a/packages/devtools/test/FuzzNetwork.test.ts b/packages/devtools/test/FuzzNetwork.test.ts index 22ebd4af..e5900518 100644 --- a/packages/devtools/test/FuzzNetwork.test.ts +++ b/packages/devtools/test/FuzzNetwork.test.ts @@ -10,7 +10,8 @@ const Pad = Codecs.struct({ dx: Codecs.integer, fire: Codecs.boolean }) const idle: Pad = { dx: 0, fire: false } const Seats = Descriptor.Service>()("FuzzNet/Seats") const Score = Descriptor.ConstructedResource(Codecs.integer)("FuzzNet/Score") -const Game = Schema.bind(Schema.fragment({ resources: { Score } })) +const Root = Schema.defineRoot("FuzzNet") +const Game = Schema.bind(Schema.fragment({ resources: { Score } }), Root) /** * The planted nondeterminism: seat 1 moving right while firing adds a diff --git a/packages/devtools/test/IgnoredFailure.test.ts b/packages/devtools/test/IgnoredFailure.test.ts index 77464bf0..542f6dd5 100644 --- a/packages/devtools/test/IgnoredFailure.test.ts +++ b/packages/devtools/test/IgnoredFailure.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it } from "vitest" import { Clock, HostClock, Result, Schema } from "@typeonce/ritmo" import { Replay, Session } from "@typeonce/ritmo-devtools" -const Game = Schema.bind(Schema.fragment({})) +const Root = Schema.defineRoot("IgnoredFailure") +const Game = Schema.bind(Schema.fragment({}), Root) const Load = Game.System("Ignore/Load", {}, () => Result.failure("level file missing" as const)) const Place = Game.System("Ignore/Place", {}, () => {}) const Decorate = Game.System("Ignore/Decorate", {}, () => {}) diff --git a/packages/devtools/test/Labels.test.ts b/packages/devtools/test/Labels.test.ts index e9377414..3ae0e98d 100644 --- a/packages/devtools/test/Labels.test.ts +++ b/packages/devtools/test/Labels.test.ts @@ -9,7 +9,8 @@ const Position = Descriptor.Component<{ readonly x: number }>()("Labels/Position const Health = Descriptor.Component()("Labels/Health") const Player = Descriptor.Tag("Labels/Player") const Slime = Descriptor.Tag("Labels/Slime") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Player, Slime } })) +const Root = Schema.defineRoot("Labels") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Player, Slime } }), Root) const Spawn = Game.System("Labels/Spawn", { commands: Game.System.commands(Game.System.spawns(Position, Health, Player, Slime)) }, ({ commands }) => { commands.spawn(Game.Command.spawn([Position, { x: 0 }], [Health, 3], [Player, {}])) diff --git a/packages/devtools/test/Lint.test.ts b/packages/devtools/test/Lint.test.ts index d02abf3a..03294028 100644 --- a/packages/devtools/test/Lint.test.ts +++ b/packages/devtools/test/Lint.test.ts @@ -8,7 +8,8 @@ const hostClock = HostClock.test() const Ping = Descriptor.Event()("LintTest/Ping") const Score = Descriptor.Component()("LintTest/Score") -const Game = Schema.bind(Schema.fragment({ components: { Score }, events: { Ping } })) +const Root = Schema.defineRoot("LintTest") +const Game = Schema.bind(Schema.fragment({ components: { Score }, events: { Ping } }), Root) const Emit = Game.System("LintTest/Emit", { events: { ping: Game.System.writeEvent(Ping) } }, ({ events }) => { events.ping.emit(1) }) diff --git a/packages/devtools/test/Replay.test.ts b/packages/devtools/test/Replay.test.ts index 6b775bf0..f4c7da6c 100644 --- a/packages/devtools/test/Replay.test.ts +++ b/packages/devtools/test/Replay.test.ts @@ -13,7 +13,8 @@ const Steps = Descriptor.ConstructedResource(Codecs.number)("Replay/Steps") const Bump = Descriptor.Event()("Replay/Bump") const Input = Descriptor.Service>()("Replay/Input") const Config = Descriptor.Service<{ readonly scale: number }>()("Replay/Config") -const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Steps }, events: { Bump } })) +const Root = Schema.defineRoot("Replay") +const Game = Schema.bind(Schema.fragment({ components: { Position }, resources: { Steps }, events: { Bump } }), Root) const Spawn = Game.System("Replay/Spawn", { commands: Game.System.commands(Game.System.spawns(Position)) }, ({ commands }) => { const draft = Game.Command.spawn(Game.Command.entryRaw(Position, { x: 0 })) diff --git a/packages/devtools/test/Session.test.ts b/packages/devtools/test/Session.test.ts index b7fcf788..0cede42e 100644 --- a/packages/devtools/test/Session.test.ts +++ b/packages/devtools/test/Session.test.ts @@ -10,11 +10,12 @@ const Position = Descriptor.Component<{ readonly x: number }>()("Dev/Position") const Health = Descriptor.Component()("Dev/Health") const Score = Descriptor.Resource()("Dev/Score") const Hit = Descriptor.Event()("Dev/Hit") +const Root = Schema.defineRoot("Dev") const Game = Schema.bind(Schema.fragment({ components: { Position, Health }, resources: { Score }, events: { Hit } -})) +}), Root) const Flow = Game.StateMachine("Dev/Flow", { Playing: {}, Paused: {} }) const Movers = Game.Query({ selection: { position: Game.Query.write(Position) } }) @@ -418,7 +419,7 @@ describe("Session risk checks", () => { const Shot = Descriptor.Component<{ readonly speed: number }>()("Risk/Shot") const Total = Descriptor.Resource<{ readonly sum: number }>()("Risk/Total") const Blast = Descriptor.Event<{ readonly force: number }>()("Risk/Blast") - const RiskGame = Schema.bind(Schema.fragment({ components: { Velocity, Shot }, resources: { Total }, events: { Blast } })) + const RiskGame = Schema.bind(Schema.fragment({ components: { Velocity, Shot }, resources: { Total }, events: { Blast } }), Root) const makeRiskSession = (update: ReturnType) => { const runtime = RiskGame.Runtime.make({ services: RiskGame.Runtime.services(), resources: { Total: { sum: 0 } }, debug: { hostClock } }).value @@ -455,7 +456,7 @@ describe("Session risk checks", () => { meta: { hp: { unit: "points", range: [0, 100], hint: "Hit points left." } } }) const Tuning = Descriptor.Resource<{ readonly speed: number }>()("Range/Tuning", { meta: { speed: { unit: "px/s", range: [0, 400] } } }) - const RangeGame = Schema.bind(Schema.fragment({ components: { Health }, resources: { Tuning } })) + const RangeGame = Schema.bind(Schema.fragment({ components: { Health }, resources: { Tuning } }), Root) const SpawnHero = RangeGame.System("Range/SpawnHero", { commands: RangeGame.System.commands(RangeGame.System.spawns(Health)) }, ({ commands }) => { commands.spawn(RangeGame.Command.spawn([Health, { hp: 50, name: "hero" }])) commands.spawn(RangeGame.Command.spawn([Health, { hp: 150, name: "giant" }])) diff --git a/packages/devtools/test/Stepping.test.ts b/packages/devtools/test/Stepping.test.ts index 2c36854c..b8b1ae0d 100644 --- a/packages/devtools/test/Stepping.test.ts +++ b/packages/devtools/test/Stepping.test.ts @@ -9,7 +9,8 @@ const Position = Descriptor.ConstructedComponent(Codecs.struct({ x: Codecs.numbe const Health = Descriptor.ConstructedComponent(Codecs.number)("Step/Health") const Score = Descriptor.ConstructedResource(Codecs.number)("Step/Score") const Hit = Descriptor.Event()("Step/Hit") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health }, resources: { Score }, events: { Hit } })) +const Root = Schema.defineRoot("Step") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health }, resources: { Score }, events: { Hit } }), Root) const Movers = Game.Query({ selection: { position: Game.Query.write(Position) } }) const Hurtable = Game.Query({ selection: { health: Game.Query.write(Health) } }) diff --git a/packages/devtools/test/WhyDepth.test.ts b/packages/devtools/test/WhyDepth.test.ts index 73d0fc3f..545007db 100644 --- a/packages/devtools/test/WhyDepth.test.ts +++ b/packages/devtools/test/WhyDepth.test.ts @@ -11,11 +11,12 @@ const Aim = Descriptor.Resource<{ readonly target: number }>()("Why/Aim") const Heat = Descriptor.Resource()("Why/Heat") const Cool = Descriptor.Resource()("Why/Cool") const Damage = Descriptor.Event<{ readonly amount: number }>()("Why/Damage") +const Root = Schema.defineRoot("Why") const Game = Schema.bind(Schema.fragment({ components: { Health, Armor, Position }, resources: { Aim, Heat, Cool }, events: { Damage } -})) +}), Root) const Spawn = Game.System("Why/Spawn", { commands: Game.System.commands(Game.System.spawns(Health, Armor, Position)) }, ({ commands }) => { commands.spawn(Game.Command.spawn([Health, 100], [Armor, 5], [Position, { x: 0 }])) diff --git a/packages/devtools/test/fixtures/lint-entry.ts b/packages/devtools/test/fixtures/lint-entry.ts index 2ad43725..a0903a27 100644 --- a/packages/devtools/test/fixtures/lint-entry.ts +++ b/packages/devtools/test/fixtures/lint-entry.ts @@ -4,7 +4,8 @@ import { Descriptor, HostClock, Schema } from "@typeonce/ritmo" const hostClock = HostClock.test() const Noise = Descriptor.Event()("LintFixture/Noise") -const Game = Schema.bind(Schema.fragment({ events: { Noise } })) +const Root = Schema.defineRoot("LintFixture") +const Game = Schema.bind(Schema.fragment({ events: { Noise } }), Root) const Emit = Game.System("LintFixture/Emit", { events: { noise: Game.System.writeEvent(Noise) } }, ({ events }) => { events.noise.emit(1) diff --git a/packages/devtools/test/fixtures/replay-entry.ts b/packages/devtools/test/fixtures/replay-entry.ts index 2a856c6f..def63a43 100644 --- a/packages/devtools/test/fixtures/replay-entry.ts +++ b/packages/devtools/test/fixtures/replay-entry.ts @@ -8,7 +8,8 @@ import * as Codecs from "../../../core/test/codecs.ts" const Position = Descriptor.ConstructedComponent(Codecs.struct({ x: Codecs.number }))("VerifyFixture/Position") export const Input = Descriptor.Service>()("VerifyFixture/Input") -const Game = Schema.bind(Schema.fragment({ components: { Position } })) +const Root = Schema.defineRoot("VerifyFixture") +const Game = Schema.bind(Schema.fragment({ components: { Position } }), Root) let builds = 0 diff --git a/packages/netcode/dtslint/Netcode.tst.ts b/packages/netcode/dtslint/Netcode.tst.ts index fc996805..33f90fad 100644 --- a/packages/netcode/dtslint/Netcode.tst.ts +++ b/packages/netcode/dtslint/Netcode.tst.ts @@ -20,7 +20,8 @@ const Sound = Descriptor.Service<{ readonly play: () => void }>()("NetTypes/Soun const Hit = Descriptor.Event<{ readonly seat: number; readonly at: { readonly x: number } }>()("NetTypes/Hit") const Elsewhere = Descriptor.Event<{ readonly seat: number }>()("NetTypes/Elsewhere") -const Game = Schema.bind(Schema.fragment({ resources: { Inputs, Score }, events: { Hit } })) +const Root = Schema.defineRoot("NetTypes") +const Game = Schema.bind(Schema.fragment({ resources: { Inputs, Score }, events: { Hit } }), Root) const Capture = InputCapture.system(Game, { name: "NetTypes/Capture", source: Seats, resource: Inputs }) const Fail = Game.System("NetTypes/Fail", {}, () => Result.failure("Overflow" as const)) const Remember = Game.System("NetTypes/Remember", { local: () => ({ runs: 0 }) }, ({ local }) => { @@ -89,7 +90,7 @@ describe("Netcode.rollback", () => { it("rejects a runtime that cannot save", () => { const Loose = Descriptor.Resource()("NetTypes/Loose") - const Unsaved = Schema.bind(Schema.fragment({ resources: { Inputs, Loose } })) + const Unsaved = Schema.bind(Schema.fragment({ resources: { Inputs, Loose } }), Root) const unsaved = Unsaved.Runtime.make({ services: Unsaved.Runtime.services(Unsaved.Runtime.service(Seats, theSeats.source)), resources: { Inputs: [], Loose: 0 } }).value const capture = InputCapture.system(Unsaved, { name: "NetTypes/UnsavedCapture", source: Seats, resource: Inputs }) // @ts-expect-error NetTypes/Loose needs a schema @@ -181,7 +182,7 @@ describe("Netcode.syncTest", () => { it("rejects a runtime that cannot save", () => { const Loose = Descriptor.Resource()("NetTypes/SyncLoose") - const Unsaved = Schema.bind(Schema.fragment({ resources: { Inputs, Loose } })) + const Unsaved = Schema.bind(Schema.fragment({ resources: { Inputs, Loose } }), Root) const unsaved = Unsaved.Runtime.make({ services: Unsaved.Runtime.services(Unsaved.Runtime.service(Seats, theSeats.source)), resources: { Inputs: [], Loose: 0 } }).value const capture = InputCapture.system(Unsaved, { name: "NetTypes/SyncUnsavedCapture", source: Seats, resource: Inputs }) // @ts-expect-error NetTypes/SyncLoose needs a schema diff --git a/packages/netcode/test/InputSchema.test.ts b/packages/netcode/test/InputSchema.test.ts index 0ab52e71..648b1f0a 100644 --- a/packages/netcode/test/InputSchema.test.ts +++ b/packages/netcode/test/InputSchema.test.ts @@ -22,7 +22,8 @@ const Seats = Descriptor.Service>()("NetSchema/Seats const Inputs = Descriptor.TransientResource>()("NetSchema/Inputs") const Time = Clock.resource("NetSchema/Time", step) const Sum = Descriptor.ConstructedResource(Whole)("NetSchema/Sum") -const Game = Schema.bind(Schema.fragment({ resources: { Inputs, Time, Sum } })) +const Root = Schema.defineRoot("NetSchema") +const Game = Schema.bind(Schema.fragment({ resources: { Inputs, Time, Sum } }), Root) const Add = Game.System("NetSchema/Add", { resources: { inputs: Game.System.readResource(Inputs), sum: Game.System.writeResource(Sum) } }, ({ resources }) => { diff --git a/packages/netcode/test/Rewindable.test.ts b/packages/netcode/test/Rewindable.test.ts index b122caeb..1247ddbe 100644 --- a/packages/netcode/test/Rewindable.test.ts +++ b/packages/netcode/test/Rewindable.test.ts @@ -42,7 +42,8 @@ const Inputs = Descriptor.TransientResource>()("SyncTest/Seats") const Inputs = Descriptor.TransientResource>()("SyncTest/Inputs") -const Leaky = Schema.bind(Schema.fragment({ resources: { Score, Shots, Inputs } })) +const Root = Schema.defineRoot("SyncTest") +const Leaky = Schema.bind(Schema.fragment({ resources: { Score, Shots, Inputs } }), Root) const Capture = InputCapture.system(Leaky, { name: "SyncTest/Capture", source: Seats, resource: Inputs }) const Fire = Leaky.System("SyncTest/Fire", { resources: { inputs: Leaky.System.readResource(Inputs), score: Leaky.System.writeResource(Score), shots: Leaky.System.writeResource(Shots) } diff --git a/packages/netcode/test/game.ts b/packages/netcode/test/game.ts index 811e4d26..423e4364 100644 --- a/packages/netcode/test/game.ts +++ b/packages/netcode/test/game.ts @@ -44,7 +44,8 @@ export const Fired = Descriptor.Event<{ readonly seat: number; readonly x: numbe /** Host-side and out of band: lets a test break one peer's determinism on purpose. */ export const Drift = Descriptor.ConstructedResource(Whole)("NetTest/Drift") -export const Game = Schema.bind(Schema.fragment({ components: { Player, Marker }, resources: { Total, Time, Inputs, Drift }, events: { Fired } })) +const Root = Schema.defineRoot("NetTest") +export const Game = Schema.bind(Schema.fragment({ components: { Player, Marker }, resources: { Total, Time, Inputs, Drift }, events: { Fired } }), Root) const Capture = InputCapture.system(Game, { name: "NetTest/Capture", source: Seats, resource: Inputs }) const Ticking = Clock.system(Game, { name: "NetTest/Clock", resource: Time }) diff --git a/packages/pixi/dtslint/Pixi.tst.ts b/packages/pixi/dtslint/Pixi.tst.ts index c04d14d1..bb5faed2 100644 --- a/packages/pixi/dtslint/Pixi.tst.ts +++ b/packages/pixi/dtslint/Pixi.tst.ts @@ -12,6 +12,8 @@ interface Node { x: number } +const Root = Schema.defineRoot("PixiTypes") + describe("@typeonce/ritmo-pixi", () => { it("exposes the package entrypoint type through the workspace", () => { expect(packageTag).type.toBe<"pixi">() @@ -40,7 +42,7 @@ describe("@typeonce/ritmo-pixi", () => { const Position = Descriptor.Component<{ x: number }>()("PixiTypes/Position") const Sprite = Descriptor.Component<{ label: string }>()("PixiTypes/Sprite") const Nodes = Descriptor.Service>()("PixiTypes/Nodes") - const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } })) + const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } }), Root) const render = RenderSync.system(Game, { name: "PixiTypes/Render", renderable: Sprite, diff --git a/packages/pixi/test/Pixi.test.ts b/packages/pixi/test/Pixi.test.ts index 6efd6801..b888f23c 100644 --- a/packages/pixi/test/Pixi.test.ts +++ b/packages/pixi/test/Pixi.test.ts @@ -7,6 +7,8 @@ import * as Pixi from "@typeonce/ritmo-pixi" import { PixiNodeRegistry, packageTag } from "@typeonce/ritmo-pixi" import { Container } from "pixi.js" +const Root = Schema.defineRoot("PixiTest") + describe("@typeonce/ritmo-pixi", () => { it("resolves through the workspace package entrypoint", () => { expect(packageTag).toBe("pixi") @@ -78,8 +80,8 @@ describe("@typeonce/ritmo-pixi", () => { const Body = Descriptor.Component<{}>()("PixiTest/Body") const { relation: Attached } = Descriptor.Hierarchy("PixiTest/Attached", "PixiTest/Attachments") const Nodes = Descriptor.Service>()("PixiTest/Nodes") - const Game = Schema.bind(Schema.fragment({ components: { Position, Body }, relations: { Attached } })) - type Id = Entity.EntityId + const Game = Schema.bind(Schema.fragment({ components: { Position, Body }, relations: { Attached } }), Root) + type Id = Entity.EntityId const render = RenderSync.system(Game, { name: "PixiTest/Render", diff --git a/packages/render/bench/render.bench.ts b/packages/render/bench/render.bench.ts index 23f2abe4..c56c8f6e 100644 --- a/packages/render/bench/render.bench.ts +++ b/packages/render/bench/render.bench.ts @@ -41,7 +41,8 @@ const Clock = Descriptor.Service<{ alpha: number }>()("RenderBench/Clock") const { relation: Attached } = Descriptor.Hierarchy("RenderBench/Attached", "RenderBench/Attachments") -const Game = Schema.bind(Schema.fragment({ components: { Position, Previous, Frame, Sprite }, relations: { Attached } })) +const Root = Schema.defineRoot("RenderBench") +const Game = Schema.bind(Schema.fragment({ components: { Position, Previous, Frame, Sprite }, relations: { Attached } }), Root) const Sync = RenderSync.system(Game, { name: "RenderBench/Sync", diff --git a/packages/render/dtslint/Render.tst.ts b/packages/render/dtslint/Render.tst.ts index e7752350..01f015e9 100644 --- a/packages/render/dtslint/Render.tst.ts +++ b/packages/render/dtslint/Render.tst.ts @@ -11,7 +11,8 @@ const Position = Descriptor.Component<{ x: number }>()("RenderTypes/Position") const Sprite = Descriptor.Component<{ label: string }>()("RenderTypes/Sprite") const Health = Descriptor.Component()("RenderTypes/Health") const Nodes = Descriptor.Service>()("RenderTypes/Nodes") -const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } })) +const Root = Schema.defineRoot("RenderTypes") +const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } }), Root) describe("@typeonce/ritmo-render", () => { it("exposes the package entrypoint type through the workspace", () => { @@ -75,7 +76,7 @@ describe("@typeonce/ritmo-render", () => { it("types resources as read cells and only accepts schema resources", () => { const Zoom = Descriptor.Resource()("RenderTypes/Zoom") const Other = Descriptor.Resource()("RenderTypes/Other") - const Zoomed = Schema.bind(Schema.fragment({ components: { Position, Sprite }, resources: { Zoom } })) + const Zoomed = Schema.bind(Schema.fragment({ components: { Position, Sprite }, resources: { Zoom } }), Root) RenderSync.system(Zoomed, { name: "RenderTypes/Zoom", renderable: Sprite, @@ -172,7 +173,7 @@ describe("@typeonce/ritmo-render", () => { const Previous = Descriptor.Component<{ x: number; y: number }>()("RenderTypes/Previous") const Clock = Descriptor.Service<{ readonly alpha: number }>()("RenderTypes/Clock") const NoAlpha = Descriptor.Service<{ readonly now: number }>()("RenderTypes/NoAlpha") - const Planet = Schema.bind(Schema.fragment({ components: { Planar, Previous, Position, Health } })) + const Planet = Schema.bind(Schema.fragment({ components: { Planar, Previous, Position, Health } }), Root) const interpolate = RenderSync.interpolate(Planet, { name: "RenderTypes/Interpolate", @@ -219,7 +220,7 @@ describe("@typeonce/ritmo-render", () => { const PreviousSpatial = Descriptor.Component<{ x: number; y: number; z: number }>()("RenderTypes/PreviousSpatial") const Labeled = Descriptor.Component<{ x: number; y: number; label: string }>()("RenderTypes/Labeled") const Clock = Descriptor.Service<{ readonly alpha: number }>()("RenderTypes/Clock") - const Space = Schema.bind(Schema.fragment({ components: { Spatial, PreviousSpatial, Labeled, Health } })) + const Space = Schema.bind(Schema.fragment({ components: { Spatial, PreviousSpatial, Labeled, Health } }), Root) RenderSync.interpolate(Space, { name: "RenderTypes/Interpolate3D", @@ -291,7 +292,7 @@ describe("@typeonce/ritmo-render", () => { const { relation: Attached } = Descriptor.Hierarchy("RenderTypes/Attached", "RenderTypes/Attachments") const { relation: Targeting } = Descriptor.Relation("RenderTypes/Targeting", "RenderTypes/TargetedBy") const { relation: Foreign } = Descriptor.Hierarchy("RenderTypes/Foreign", "RenderTypes/ForeignChildren") - const Tree = Schema.bind(Schema.fragment({ components: { Position, Sprite }, relations: { Attached, Targeting } })) + const Tree = Schema.bind(Schema.fragment({ components: { Position, Sprite }, relations: { Attached, Targeting } }), Root) const base = { name: "RenderTypes/Tree", renderable: Sprite, diff --git a/packages/render/test/Render.test.ts b/packages/render/test/Render.test.ts index 213e820f..ee92c5ed 100644 --- a/packages/render/test/Render.test.ts +++ b/packages/render/test/Render.test.ts @@ -37,8 +37,9 @@ const Position = Descriptor.Component<{ x: number; y: number }>()("RenderTest/Po const Sprite = Descriptor.Component<{ label: string }>()("RenderTest/Sprite") const Nodes = Descriptor.Service>()("RenderTest/Nodes") -const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } })) -type Id = Entity.EntityId +const Root = Schema.defineRoot("RenderTest") +const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } }), Root) +type Id = Entity.EntityId describe("@typeonce/ritmo-render", () => { it("resolves through the workspace package entrypoint", () => { @@ -136,7 +137,7 @@ describe("@typeonce/ritmo-render", () => { it("passes selected data, resources, and services to the callbacks", () => { const Zoom = Descriptor.Resource()("RenderTest/Zoom") const Prefix = Descriptor.Service()("RenderTest/Prefix") - const Zoomed = Schema.bind(Schema.fragment({ components: { Position, Sprite }, resources: { Zoom } })) + const Zoomed = Schema.bind(Schema.fragment({ components: { Position, Sprite }, resources: { Zoom } }), Root) const layer = new FakeLayer() const render = RenderSync.system(Zoomed, { name: "RenderTest/Zoomed", @@ -169,7 +170,7 @@ describe("@typeonce/ritmo-render", () => { it("re-applies nodes when a redrawOn component changes, once per entity", () => { const Frame = Descriptor.Component()("RenderTest/Frame") - const Animated = Schema.bind(Schema.fragment({ components: { Position, Sprite, Frame } })) + const Animated = Schema.bind(Schema.fragment({ components: { Position, Sprite, Frame } }), Root) const layer = new FakeLayer() const applied: Array = [] const render = RenderSync.system(Animated, { @@ -307,7 +308,7 @@ describe("@typeonce/ritmo-render", () => { it("interpolates nodes between the previous and current positions at the clock's alpha", () => { const Previous = Descriptor.Component<{ x: number; y: number }>()("RenderTest/Previous") const Clock = Descriptor.Service<{ alpha: number }>()("RenderTest/Clock") - const Moving = Schema.bind(Schema.fragment({ components: { Position, Previous, Sprite } })) + const Moving = Schema.bind(Schema.fragment({ components: { Position, Previous, Sprite } }), Root) const layer = new FakeLayer() const clock = { alpha: 0.25 } const sync = RenderSync.system(Moving, { @@ -347,7 +348,7 @@ describe("@typeonce/ritmo-render", () => { const Spatial = Descriptor.Component<{ x: number; y: number; z: number }>()("RenderTest/Spatial") const PreviousSpatial = Descriptor.Component<{ x: number; y: number; z: number }>()("RenderTest/PreviousSpatial") const Clock = Descriptor.Service<{ alpha: number }>()("RenderTest/Clock") - const Space = Schema.bind(Schema.fragment({ components: { Spatial, PreviousSpatial, Sprite } })) + const Space = Schema.bind(Schema.fragment({ components: { Spatial, PreviousSpatial, Sprite } }), Root) const layer = new FakeLayer() const placed: Array<[number, number, number]> = [] const sync = RenderSync.system(Space, { diff --git a/packages/render/test/RenderSync.hierarchy.test.ts b/packages/render/test/RenderSync.hierarchy.test.ts index 1de54d39..aab0f112 100644 --- a/packages/render/test/RenderSync.hierarchy.test.ts +++ b/packages/render/test/RenderSync.hierarchy.test.ts @@ -24,8 +24,9 @@ const Heavy = Descriptor.Component<{}>()("Hierarchy/Heavy") const { relation: Attached } = Descriptor.Hierarchy("Hierarchy/Attached", "Hierarchy/Attachments") const Nodes = Descriptor.Service>()("Hierarchy/Nodes") -const Game = Schema.bind(Schema.fragment({ components: { Position, Body, Heavy }, relations: { Attached } })) -type Id = Entity.EntityId +const Root = Schema.defineRoot("Hierarchy") +const Game = Schema.bind(Schema.fragment({ components: { Position, Body, Heavy }, relations: { Attached } }), Root) +type Id = Entity.EntityId // The edits a test makes in one frame; each spawned entity's x is its label's length. interface Edit { diff --git a/packages/render/test/RenderSync.lagged.test.ts b/packages/render/test/RenderSync.lagged.test.ts index 287dfdd8..f84412a7 100644 --- a/packages/render/test/RenderSync.lagged.test.ts +++ b/packages/render/test/RenderSync.lagged.test.ts @@ -7,7 +7,8 @@ import { NodeRegistry, RenderSync } from "@typeonce/ritmo-render" const Position = Descriptor.Component<{ x: number }>()("Lagged/Position") const Sprite = Descriptor.Component<{}>()("Lagged/Sprite") const Nodes = Descriptor.Service>()("Lagged/Nodes") -const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } })) +const Root = Schema.defineRoot("Lagged") +const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } }), Root) describe("RenderSync after its removal stream dropped entries", () => { it("destroys every node whose entity no longer matches", () => { diff --git a/packages/render/test/RenderSync.membership.test.ts b/packages/render/test/RenderSync.membership.test.ts index 3d9911a6..f7acdb23 100644 --- a/packages/render/test/RenderSync.membership.test.ts +++ b/packages/render/test/RenderSync.membership.test.ts @@ -16,8 +16,9 @@ const Position = Descriptor.Component<{ x: number }>()("Membership/Position") const Sprite = Descriptor.Component<{ label: string }>()("Membership/Sprite") const Visible = Descriptor.Component<{}>()("Membership/Visible") const Nodes = Descriptor.Service>()("Membership/Nodes") -const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite, Visible } })) -type Id = Entity.EntityId +const Root = Schema.defineRoot("Membership") +const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite, Visible } }), Root) +type Id = Entity.EntityId const stepAccess = { commands: Game.System.commands(Game.System.spawns(Position, Sprite, Visible), Game.System.inserts(Position, Visible), Game.System.removes(Position, Visible), Game.System.despawns(Sprite)), queries: { sprites: Game.Query({ selection: { sprite: Game.Query.read(Sprite) } }) } diff --git a/packages/render/test/RenderSync.rewind.test.ts b/packages/render/test/RenderSync.rewind.test.ts index 2c09318a..5bc91e08 100644 --- a/packages/render/test/RenderSync.rewind.test.ts +++ b/packages/render/test/RenderSync.rewind.test.ts @@ -23,7 +23,8 @@ const Position = Descriptor.Component<{ x: number }>()("Rewind/Position") const Sprite = Descriptor.Component<{ label: string }>()("Rewind/Sprite") const Plans = Descriptor.Service>()("Rewind/Plans") const Nodes = Descriptor.Service>()("Rewind/Nodes") -const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } })) +const Root = Schema.defineRoot("Rewind") +const Game = Schema.bind(Schema.fragment({ components: { Position, Sprite } }), Root) const Step = Game.System("Rewind/Step", { services: { plans: Game.System.consumeService(Plans) }, diff --git a/website/src/demos/composition/fire.ts b/website/src/demos/composition/fire.ts index 7521d98e..9f1b29ce 100644 --- a/website/src/demos/composition/fire.ts +++ b/website/src/demos/composition/fire.ts @@ -19,11 +19,12 @@ export const Fired = Descriptor.Resource<{ readonly spread: number; readonly dou export const Heard = Descriptor.Resource<{ readonly count: number }>()("Fire/Heard") export const Rng = Descriptor.ConstructedResource(Random.codec)("Fire/Rng") +const Root = Schema.defineRoot("Fire") export const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity, Villager, Burning, Wet }, events: { Spark }, resources: { Fired, Heard, Rng } -})) +}), Root) export const AREA = { width: 600, height: 420 } as const export const FOUNTAIN = { x: 540, y: 210, radius: 44 } as const diff --git a/website/src/demos/determinism/sim.ts b/website/src/demos/determinism/sim.ts index c860620a..50b5a33b 100644 --- a/website/src/demos/determinism/sim.ts +++ b/website/src/demos/determinism/sim.ts @@ -18,10 +18,11 @@ export const Velocity = Descriptor.Component<{ x: number; y: number }>()("Drift/ export const Rng = Descriptor.ConstructedResource(Random.codec)("Drift/Rng") export const Tick = Descriptor.Resource()("Drift/Tick") +const Root = Schema.defineRoot("Drift") export const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Rng, Tick } -})) +}), Root) const Balls = Game.Query({ selection: { position: Game.Query.write(Position), velocity: Game.Query.write(Velocity) } diff --git a/website/src/demos/states/sim.ts b/website/src/demos/states/sim.ts index bc5344d8..e6f1cd20 100644 --- a/website/src/demos/states/sim.ts +++ b/website/src/demos/states/sim.ts @@ -49,10 +49,11 @@ export const Contact = Descriptor.EntityState("States/Contact", { export const Jump = Descriptor.Event<{}>()("States/Jump") export const Guard = Descriptor.Event<{}>()("States/Guard") +const Root = Schema.defineRoot("States") export const Game = Schema.bind(Schema.fragment({ components: { Height, Contact }, events: { Jump, Guard } -})) +}), Root) const Bodies = Game.Query({ selection: { height: Game.Query.write(Height), contact: Game.Query.write(Contact) } diff --git a/website/src/pages/docs/browser.mdx b/website/src/pages/docs/browser.mdx index 3a2f2813..663fada8 100644 --- a/website/src/pages/docs/browser.mdx +++ b/website/src/pages/docs/browser.mdx @@ -28,7 +28,8 @@ Each animation frame runs as many fixed updates as the time owes, then one ```ts twoslash title="main.ts" import { Clock, Descriptor, Schema } from "@typeonce/ritmo" -const Game = Schema.bind(Schema.fragment({ components: { Tag: Descriptor.Component<{}>()("Example/Tag") } })) +const Root = Schema.defineRoot("Example") +const Game = Schema.bind(Schema.fragment({ components: { Tag: Descriptor.Component<{}>()("Example/Tag") } }), Root) const update = Game.Schedule() const runtime = Game.Runtime.make({ services: Game.Runtime.services() }).value! declare const draw: (alpha: number) => void @@ -122,7 +123,8 @@ const buttons = { move: Gamepad.vector({ stick: "left" }), jump: ["south"] } as const makeControls = (devices: { readonly keyboard: Keyboard.Actions; readonly gamepad: Gamepad.Actions }) => Actions.merge(devices) const ControlsInput = Descriptor.Service>>()("Example/Controls") const Input = Descriptor.TransientResource>()("Example/Input") -const Game = Schema.bind(Schema.fragment({ resources: { Input } })) +const Root = Schema.defineRoot("Example") +const Game = Schema.bind(Schema.fragment({ resources: { Input } }), Root) // ---cut--- const CaptureInput = InputCapture.system(Game, { name: "Example/CaptureInput", @@ -256,7 +258,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("TopDown/Position") const Collider = Descriptor.Component<{ width: number; height: number }>()("TopDown/Collider") const Wall = Descriptor.Component<{}>()("TopDown/Wall") -const Game = Schema.bind(Schema.fragment({ components: { Position, Collider, Wall } })) +const Root = Schema.defineRoot("TopDown") +const Game = Schema.bind(Schema.fragment({ components: { Position, Collider, Wall } }), Root) // ---cut--- import { RenderSync } from "@typeonce/ritmo-render" import type { NodeRegistry } from "@typeonce/ritmo-render" diff --git a/website/src/pages/docs/comparison.mdx b/website/src/pages/docs/comparison.mdx index ade7dfdf..bd33095b 100644 --- a/website/src/pages/docs/comparison.mdx +++ b/website/src/pages/docs/comparison.mdx @@ -84,7 +84,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Velocity = Descriptor.Component<{ x: number; y: number }>()("Arena/Velocity") const Guard = Descriptor.Tag("Arena/Guard") const Stunned = Descriptor.Component()("Arena/Stunned") -const Game = Schema.bind(Schema.fragment({ components: { Velocity, Guard, Stunned } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Velocity, Guard, Stunned } }), Root) // ---cut--- // @errors: 2345 const Calm = Game.Query({ selection: {}, with: [Guard], without: [Stunned] }) @@ -139,8 +140,9 @@ can fail: import { Descriptor, Entity, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") const Guard = Descriptor.Tag("Arena/Guard") -const Game = Schema.bind(Schema.fragment({ components: { Position, Guard } })) -type GuardId = Entity.EntityId +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Guard } }), Root) +type GuardId = Entity.EntityId const Guards = Game.Query({ selection: { position: Game.Query.read(Position) }, with: [Guard] }) // ---cut--- const Follow = Game.System("Arena/Follow", { @@ -175,7 +177,8 @@ its failure as a value, and everything it wrote is rolled back: import { Descriptor, Result, Schema } from "@typeonce/ritmo" const Gold = Descriptor.Resource()("Shop/Gold") const Stock = Descriptor.Resource()("Shop/Stock") -const Game = Schema.bind(Schema.fragment({ resources: { Gold, Stock } })) +const Root = Schema.defineRoot("Shop") +const Game = Schema.bind(Schema.fragment({ resources: { Gold, Stock } }), Root) const made = Game.Runtime.make({ services: Game.Runtime.services(), resources: { Gold: 50, Stock: 0 } }) if (!made.ok) throw new Error() const runtime = made.value @@ -213,7 +216,8 @@ const codec = ()("Arena/Stunned") const Hit = Descriptor.Event<{ amount: number }>()("Arena/Hit") +const Root = Schema.defineRoot("Arena") const Game = Schema.bind( Schema.fragment({ components: { Position, Velocity, Guard, Stunned }, events: { Hit } - }) + }), + Root ) const Stun = Game.System( diff --git a/website/src/pages/docs/concepts.mdx b/website/src/pages/docs/concepts.mdx index 6d820f87..cab01004 100644 --- a/website/src/pages/docs/concepts.mdx +++ b/website/src/pages/docs/concepts.mdx @@ -63,12 +63,14 @@ const Score = Descriptor.Resource()("Arena/Score") // An event: a message read by the systems that ask for it const Hit = Descriptor.Event<{ amount: number }>()("Arena/Hit") +const Root = Schema.defineRoot("Arena") const Game = Schema.bind( Schema.fragment({ components: { Position, Health, Enemy }, resources: { Score }, events: { Hit } - }) + }), + Root ) ``` @@ -85,7 +87,8 @@ const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") const Score = Descriptor.Resource()("Arena/Score") const Hit = Descriptor.Event<{ amount: number }>()("Arena/Hit") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy }, resources: { Score }, events: { Hit } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy }, resources: { Score }, events: { Hit } }), Root) // ---cut--- const Damage = Game.System( "Arena/Damage", @@ -127,7 +130,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy } }), Root) // ---cut--- const Wounded = Game.Query({ // The data each entity gets @@ -153,7 +157,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy } }), Root) const Enemies = Game.Query({ selection: { health: Game.Query.read(Health) }, with: [Enemy] }) // ---cut--- const Cull = Game.System( @@ -187,7 +192,8 @@ const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Positio const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") const Hit = Descriptor.Event<{ amount: number }>()("Arena/Hit") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy }, events: { Hit } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy }, events: { Hit } }), Root) const SpawnWave = Game.System("Arena/SpawnWave", { commands: Game.System.commands(Game.System.spawns(Position, Health, Enemy)) }, () => {}) const Damage = Game.System("Arena/Damage", { queries: { enemies: Game.Query({ selection: { health: Game.Query.write(Health) }, with: [Enemy] }) }, events: { hits: Game.System.readEvent(Hit) } }, () => {}) const Cull = Game.System("Arena/Cull", { queries: { enemies: Game.Query({ selection: { health: Game.Query.read(Health) }, with: [Enemy] }) }, commands: Game.System.commands(Game.System.despawns(Enemy)) }, () => {}) @@ -220,7 +226,8 @@ const codec = ()("Arena/Health") -const Game = Schema.bind(Schema.fragment({ components: { Health } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) // ---cut--- // A closed list of states, each with the data only it has, // and which states may follow which diff --git a/website/src/pages/docs/index.mdx b/website/src/pages/docs/index.mdx index ce176d57..d4cbcc2e 100644 --- a/website/src/pages/docs/index.mdx +++ b/website/src/pages/docs/index.mdx @@ -43,11 +43,13 @@ const Time = Clock.resource("Example/Time", step) const Position = Descriptor.Component()("Example/Position") const Velocity = Descriptor.Component()("Example/Velocity") +const Root = Schema.defineRoot("Example") const Game = Schema.bind( Schema.fragment({ components: { Position, Velocity }, resources: { Time } - }) + }), + Root ) ``` @@ -64,7 +66,8 @@ const step = Clock.hz(60) const Time = Clock.resource("Example/Time", step) const Position = Descriptor.Component()("Example/Position") const Velocity = Descriptor.Component()("Example/Velocity") -const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } })) +const Root = Schema.defineRoot("Example") +const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } }), Root) // ---cut--- const Move = Game.System( "Example/Move", @@ -106,7 +109,8 @@ const step = Clock.hz(60) const Time = Clock.resource("Example/Time", step) const Position = Descriptor.Component()("Example/Position") const Velocity = Descriptor.Component()("Example/Velocity") -const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } })) +const Root = Schema.defineRoot("Example") +const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } }), Root) // ---cut--- const Spawn = Game.System( "Example/Spawn", @@ -138,7 +142,8 @@ const step = Clock.hz(60) const Time = Clock.resource("Example/Time", step) const Position = Descriptor.Component()("Example/Position") const Velocity = Descriptor.Component()("Example/Velocity") -const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } })) +const Root = Schema.defineRoot("Example") +const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } }), Root) const Move = Game.System("Example/Move", { queries: { moving: Game.Query({ selection: { position: Game.Query.write(Position), velocity: Game.Query.read(Velocity) } }) }, resources: { time: Game.System.readResource(Time) } }, () => {}) const Spawn = Game.System("Example/Spawn", { commands: Game.System.commands(Game.System.spawns(Position, Velocity)) }, () => {}) // ---cut--- @@ -181,7 +186,8 @@ const step = Clock.hz(60) const Time = Clock.resource("Example/Time", step) const Position = Descriptor.Component()("Example/Position") const Velocity = Descriptor.Component()("Example/Velocity") -const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } })) +const Root = Schema.defineRoot("Example") +const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } }), Root) const made = Game.Runtime.make({ services: Game.Runtime.services(), resources: { Time: Clock.start(step) } }) if (!made.ok) throw new Error() const runtime = made.value diff --git a/website/src/pages/docs/type-safety.mdx b/website/src/pages/docs/type-safety.mdx index 783c8e98..bd0012d5 100644 --- a/website/src/pages/docs/type-safety.mdx +++ b/website/src/pages/docs/type-safety.mdx @@ -20,7 +20,8 @@ goes. ```ts twoslash import { Descriptor, Schema } from "@typeonce/ritmo" const Health = Descriptor.Component()("Arena/Health") -const Game = Schema.bind(Schema.fragment({ components: { Health } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) // ---cut--- // @errors: 2345 const Logger = Descriptor.Service<{ log(message: string): void }>()( @@ -56,7 +57,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy } }), Root) const SpawnWave = Game.System("Arena/SpawnWave", { commands: Game.System.commands(Game.System.spawns(Position, Health, Enemy)) }, () => {}) const made = Game.Runtime.make({ services: Game.Runtime.services() }) if (!made.ok) throw new Error() @@ -97,7 +99,8 @@ const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Positio const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") const Noise = Descriptor.Component()("Arena/Noise") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy, Noise }, requires: [Schema.requires(Enemy, Position, Health)] })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy, Noise }, requires: [Schema.requires(Enemy, Position, Health)] }), Root) // ---cut--- // @errors: 2379 Game.System( @@ -126,7 +129,8 @@ no `set`. ```ts twoslash import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") -const Game = Schema.bind(Schema.fragment({ components: { Position } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position } }), Root) // ---cut--- // @errors: 2339 // Declared as a read... @@ -160,7 +164,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy }, requires: [Schema.requires(Enemy, Position, Health)] })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy }, requires: [Schema.requires(Enemy, Position, Health)] }), Root) // ---cut--- // @errors: 2379 Game.System( @@ -190,7 +195,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy } }), Root) // ---cut--- // @errors: 2345 Game.System( @@ -220,7 +226,8 @@ presses from the first. The message is in the type: drain it with import { Descriptor, Schema } from "@typeonce/ritmo" import { InputCapture, Keyboard } from "@typeonce/ritmo-browser" const Health = Descriptor.Component()("Arena/Health") -const Game = Schema.bind(Schema.fragment({ components: { Health } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) const keys = { jump: [" "] } as const // ---cut--- // @errors: 2684 @@ -257,7 +264,8 @@ const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Positio const Speed = Descriptor.Component()("Arena/Position") // [!code error] // @errors: 2345 -const Game = Schema.bind(Schema.fragment({ components: { Position, Speed } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Speed } }), Root) ``` @@ -275,7 +283,8 @@ const codec = ()("Arena/Health") -const Game = Schema.bind(Schema.fragment({ components: { Health } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) // ---cut--- // @errors: 2345 // A step rate is a whole number of steps per second @@ -305,7 +314,8 @@ Making a runtime depends on what you pass it at runtime, so it returns a ```ts twoslash import { Descriptor, Schema } from "@typeonce/ritmo" const Health = Descriptor.Component()("Arena/Health") -const Game = Schema.bind(Schema.fragment({ components: { Health } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) // ---cut--- // Making a runtime can fail, so it returns a Result instead of throwing const made = Game.Runtime.make({ services: Game.Runtime.services() }) @@ -329,13 +339,15 @@ const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") const Noise = Descriptor.Component()("Arena/Noise") +const Root = Schema.defineRoot("Arena") const Game = Schema.bind( Schema.fragment({ components: { Position, Health, Enemy, Noise }, // Every enemy has a position and health requires: [Schema.requires(Enemy, Position, Health)] // [!code highlight] - }) + }), + Root ) ``` diff --git a/website/src/pages/docs/with-an-agent.mdx b/website/src/pages/docs/with-an-agent.mdx index f7d8efec..b05b363b 100644 --- a/website/src/pages/docs/with-an-agent.mdx +++ b/website/src/pages/docs/with-an-agent.mdx @@ -22,7 +22,8 @@ const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Positio const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") const Noise = Descriptor.Component()("Arena/Noise") -const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy, Noise } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Health, Enemy, Noise } }), Root) // ---cut--- // @errors: 2379 Game.System( diff --git a/website/src/pages/features/bugs.mdx b/website/src/pages/features/bugs.mdx index 2ac4230d..d356b007 100644 --- a/website/src/pages/features/bugs.mdx +++ b/website/src/pages/features/bugs.mdx @@ -107,7 +107,8 @@ import { Netcode } from "@typeonce/ritmo-netcode" interface PlayerInput { readonly dx: number; readonly fire: boolean } const Seats = Descriptor.Service>()("Game/Seats") const Inputs = Descriptor.TransientResource>()("Game/Inputs") -const Game = Schema.bind(Schema.fragment({ resources: { Inputs } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ resources: { Inputs } }), Root) const Capture = InputCapture.system(Game, { name: "Game/Capture", source: Seats, resource: Inputs }) declare const seats: Netcode.Seats const runtime = Game.Runtime.make({ services: Game.Runtime.services(Game.Runtime.service(Seats, seats.source)), resources: { Inputs: [] } }).value! diff --git a/website/src/pages/features/composition.mdx b/website/src/pages/features/composition.mdx index acba04a1..cf0e618b 100644 --- a/website/src/pages/features/composition.mdx +++ b/website/src/pages/features/composition.mdx @@ -30,7 +30,8 @@ const Burning = Descriptor.Tag("Village/Burning") const Wet = Descriptor.Tag("Village/Wet") const Spark = Descriptor.Event<{}>()("Village/Spark") -const Game = Schema.bind(Schema.fragment({ components: { Villager, Burning, Wet }, events: { Spark } })) +const Root = Schema.defineRoot("Village") +const Game = Schema.bind(Schema.fragment({ components: { Villager, Burning, Wet }, events: { Spark } }), Root) // ---cut--- // @errors: 2345 const Ignite = Game.System( @@ -81,7 +82,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Village/Position") const Velocity = Descriptor.Component<{ x: number; y: number }>()("Village/Velocity") const Burning = Descriptor.Tag("Village/Burning") -const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity, Burning } })) +const Root = Schema.defineRoot("Village") +const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity, Burning } }), Root) const Ignite = Game.System("Village/Ignite", { commands: Game.System.commands(Game.System.inserts(Burning)) }, () => {}) const Panic = Game.System("Village/Panic", {}, () => {}) // ---cut--- @@ -135,7 +137,8 @@ right behind the first. ```ts twoslash title="hiss.ts" import { Descriptor, Schema } from "@typeonce/ritmo" const Burning = Descriptor.Tag("Village/Burning") -const Game = Schema.bind(Schema.fragment({ components: { Burning } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ components: { Burning } }), Root) // ---cut--- const Hiss = Game.System( "Village/Hiss", @@ -179,11 +182,13 @@ const { relation: ChildOf } = Descriptor.Hierarchy( "Arena/Children" ) +const Root = Schema.defineRoot("Arena") const Game = Schema.bind( Schema.fragment({ components: { Position, Hand }, relations: { ChildOf } // [!code highlight] - }) + }), + Root ) const Arm = Game.System( @@ -223,7 +228,8 @@ parented to itself or a cycle, becomes a typed failure any system can read. import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") const { relation: ChildOf } = Descriptor.Hierarchy("Arena/ChildOf", "Arena/Children") -const Game = Schema.bind(Schema.fragment({ components: { Position }, relations: { ChildOf } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position }, relations: { ChildOf } }), Root) // ---cut--- const Follow = Game.System( "Arena/Follow", @@ -274,8 +280,10 @@ const { relation: Targeting } = Descriptor.Relation( "Arena/TargetedBy" ) +const Root = Schema.defineRoot("Game") const Game = Schema.bind( - Schema.fragment({ components: { Idle }, relations: { Targeting } }) + Schema.fragment({ components: { Idle }, relations: { Targeting } }), + Root ) const StandDown = Game.System( diff --git a/website/src/pages/features/determinism.mdx b/website/src/pages/features/determinism.mdx index 59a36c6e..cc0e910b 100644 --- a/website/src/pages/features/determinism.mdx +++ b/website/src/pages/features/determinism.mdx @@ -56,7 +56,8 @@ In a system it is ordinary code with two different imports: import { Descriptor, Ops, Random, Schema } from "@typeonce/ritmo" const Rng = Descriptor.ConstructedResource(Random.codec)("Game/Rng") const Wind = Descriptor.Resource<{ readonly x: number; readonly y: number }>()("Game/Wind") -const Game = Schema.bind(Schema.fragment({ resources: { Rng, Wind } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ resources: { Rng, Wind } }), Root) // ---cut--- const Gust = Game.System( "Game/Gust", @@ -107,7 +108,8 @@ in the world after every frame. import { Descriptor, HostClock, Schema } from "@typeonce/ritmo" import { Determinism } from "@typeonce/ritmo-devtools" const Score = Descriptor.Resource()("Game/Score") -const Game = Schema.bind(Schema.fragment({ resources: { Score } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ resources: { Score } }), Root) const update = Game.Schedule() const makeSimulation = (hostClock: HostClock.HostClock) => ({ runtime: Game.Runtime.make({ services: Game.Runtime.services(), resources: { Score: 0 }, debug: { hostClock } }).value!, diff --git a/website/src/pages/features/replays.mdx b/website/src/pages/features/replays.mdx index 0f9b0fc8..2aa402c1 100644 --- a/website/src/pages/features/replays.mdx +++ b/website/src/pages/features/replays.mdx @@ -53,7 +53,8 @@ declare const readFileSync: (path: string, encoding: "utf8") => string type Input = { readonly dash: boolean } const Controls = Descriptor.Service>()("Game/Controls") const Dashes = Descriptor.ConstructedResource(codec(S.Finite))("Game/Dashes") -const Game = Schema.bind(Schema.fragment({ resources: { Dashes } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ resources: { Dashes } }), Root) const update = Game.Schedule() const makeGame = (controls: Schedule.Source, hostClock: HostClock.HostClock) => { const made = Game.Runtime.make({ services: Game.Runtime.services(Game.Runtime.service(Controls, controls)), resources: { Dashes: 0 }, debug: { hostClock } }) @@ -111,7 +112,8 @@ const codec = >()( "Game/Inputs" ) -const Game = Schema.bind(Schema.fragment({ resources: { Inputs } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ resources: { Inputs } }), Root) // ---cut--- // Every seat's input for the tick being run const Seats = Descriptor.Service>()("Game/Seats") // [!code highlight] @@ -116,7 +117,8 @@ import { Netcode, WebSocketNetworkLink } from "@typeonce/ritmo-netcode" interface PlayerInput { readonly dx: number; readonly fire: boolean } const Seats = Descriptor.Service>()("Game/Seats") const Inputs = Descriptor.TransientResource>()("Game/Inputs") -const Game = Schema.bind(Schema.fragment({ resources: { Inputs } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ resources: { Inputs } }), Root) const Capture = InputCapture.system(Game, { name: "Game/Capture", source: Seats, resource: Inputs }) const simulate = Game.Schedule(Capture) declare const seats: Netcode.Seats @@ -190,7 +192,8 @@ import { Netcode, type NetworkLink } from "@typeonce/ritmo-netcode" interface PlayerInput { readonly dx: number; readonly fire: boolean } const Seats = Descriptor.Service>()("Game/Seats") const Inputs = Descriptor.TransientResource>()("Game/Inputs") -const Game = Schema.bind(Schema.fragment({ resources: { Inputs } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ resources: { Inputs } }), Root) const Capture = InputCapture.system(Game, { name: "Game/Capture", source: Seats, resource: Inputs }) declare const seats: Netcode.Seats declare const link: NetworkLink.NetworkLink @@ -228,7 +231,8 @@ declare class PhysicsWorld { static restoreSnapshot(bytes: Uint8Array): PhysicsWorld | undefined } declare const world: PhysicsWorld -const Game = Schema.bind(Schema.fragment({})) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({}), Root) // ---cut--- const physics = Checkpoint.rewindable(world, { save: (world) => world.takeSnapshot(), @@ -298,7 +302,8 @@ import { Netcode, WebSocketNetworkLink } from "@typeonce/ritmo-netcode" interface PlayerInput { readonly dx: number; readonly fire: boolean } const Seats = Descriptor.Service>()("Game/Seats") const Inputs = Descriptor.TransientResource>()("Game/Inputs") -const Game = Schema.bind(Schema.fragment({ resources: { Inputs } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ resources: { Inputs } }), Root) const Capture = InputCapture.system(Game, { name: "Game/Capture", source: Seats, resource: Inputs }) const simulate = Game.Schedule(Capture) declare const seats: Netcode.Seats diff --git a/website/src/pages/features/states.mdx b/website/src/pages/features/states.mdx index 6fe8777b..193a9a79 100644 --- a/website/src/pages/features/states.mdx +++ b/website/src/pages/features/states.mdx @@ -67,7 +67,8 @@ const Contact = Descriptor.EntityState("Brawl/Contact", { ground: codec(S.Struct({ platform: S.NullOr(S.Int) })), air: codec(S.Struct({ vy: S.Finite, coyote: S.Int })) }, { transitions: { ground: ["air"], air: ["ground"] } }) -const Game = Schema.bind(Schema.fragment({ components: { Contact } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ components: { Contact } }), Root) const Bodies = Game.Query({ selection: { contact: Game.Query.write(Contact) } }) // ---cut--- // @errors: 2741 @@ -106,7 +107,8 @@ const Contact = Descriptor.EntityState("Brawl/Contact", { ground: codec(S.Struct({ platform: S.NullOr(S.Int) })), air: codec(S.Struct({ vy: S.Finite, coyote: S.Int })) }, { transitions: { ground: ["air"], air: ["ground"] } }) -const Game = Schema.bind(Schema.fragment({ components: { Contact } })) +const Root = Schema.defineRoot("Game") +const Game = Schema.bind(Schema.fragment({ components: { Contact } }), Root) const Bodies = Game.Query({ selection: { contact: Game.Query.write(Contact) } }) // ---cut--- // @errors: 2322 diff --git a/website/src/snippets/home-complete.mdx b/website/src/snippets/home-complete.mdx index 23a92e12..448af411 100644 --- a/website/src/snippets/home-complete.mdx +++ b/website/src/snippets/home-complete.mdx @@ -5,12 +5,14 @@ const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") // ---cut--- // @errors: 2379 +const Root = Schema.defineRoot("Arena") // Every enemy has a position and health const Game = Schema.bind( Schema.fragment({ components: { Position, Health, Enemy }, requires: [Schema.requires(Enemy, Position, Health)] // [!code highlight] - }) + }), + Root ) Game.System( diff --git a/website/src/snippets/home-declared.mdx b/website/src/snippets/home-declared.mdx index 42e05e80..22ce0198 100644 --- a/website/src/snippets/home-declared.mdx +++ b/website/src/snippets/home-declared.mdx @@ -1,7 +1,8 @@ ```ts twoslash import { Descriptor, Schema } from "@typeonce/ritmo" const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") -const Game = Schema.bind(Schema.fragment({ components: { Position } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position } }), Root) // ---cut--- // The query only asks to read Position const Moving = Game.Query({ diff --git a/website/src/snippets/home-order.mdx b/website/src/snippets/home-order.mdx index ed6912f8..c77a7a9a 100644 --- a/website/src/snippets/home-order.mdx +++ b/website/src/snippets/home-order.mdx @@ -2,7 +2,8 @@ import { Descriptor, Schema } from "@typeonce/ritmo" const Health = Descriptor.Component()("Arena/Health") const Enemy = Descriptor.Component<{}>()("Arena/Enemy") -const Game = Schema.bind(Schema.fragment({ components: { Health, Enemy } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Health, Enemy } }), Root) const SpawnWave = Game.System("Arena/SpawnWave", { commands: Game.System.commands(Game.System.spawns(Health, Enemy)) }, () => {}) const Enemies = Game.Query({ selection: { health: Game.Query.read(Health) }, with: [Enemy] }) const made = Game.Runtime.make({ services: Game.Runtime.services() }) diff --git a/website/src/snippets/home-result.mdx b/website/src/snippets/home-result.mdx index 3bc43c08..67810afa 100644 --- a/website/src/snippets/home-result.mdx +++ b/website/src/snippets/home-result.mdx @@ -1,7 +1,8 @@ ```ts twoslash import { Descriptor, Schema } from "@typeonce/ritmo" const Health = Descriptor.Component()("Arena/Health") -const Game = Schema.bind(Schema.fragment({ components: { Health } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) const made = Game.Runtime.make({ services: Game.Runtime.services() }) if (!made.ok) throw new Error() const runtime = made.value diff --git a/website/src/snippets/home-services.mdx b/website/src/snippets/home-services.mdx index e226d5bf..fa7cfb99 100644 --- a/website/src/snippets/home-services.mdx +++ b/website/src/snippets/home-services.mdx @@ -1,7 +1,8 @@ ```ts twoslash import { Descriptor, Schema } from "@typeonce/ritmo" const Health = Descriptor.Component()("Arena/Health") -const Game = Schema.bind(Schema.fragment({ components: { Health } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Health } }), Root) const Logger = Descriptor.Service<{ log(message: string): void }>()("Arena/Logger") // ---cut--- // Report needs a Logger diff --git a/website/src/snippets/home-step-run.mdx b/website/src/snippets/home-step-run.mdx index 9eb290f1..a5302231 100644 --- a/website/src/snippets/home-step-run.mdx +++ b/website/src/snippets/home-step-run.mdx @@ -3,7 +3,8 @@ import { Clock, Descriptor, Schema } from "@typeonce/ritmo" const Time = Clock.resource("Arena/Time", Clock.hz(60)) const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") const Velocity = Descriptor.Component<{ x: number; y: number }>()("Arena/Velocity") -const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } }), Root) const Move = Game.System("Arena/Move", { resources: { time: Game.System.readResource(Time) } }, () => {}) // ---cut--- // The order of a tick, checked by the compiler diff --git a/website/src/snippets/home-step-system.mdx b/website/src/snippets/home-step-system.mdx index 3821a2ab..dbc6f535 100644 --- a/website/src/snippets/home-step-system.mdx +++ b/website/src/snippets/home-step-system.mdx @@ -3,7 +3,8 @@ import { Clock, Descriptor, Schema } from "@typeonce/ritmo" const Time = Clock.resource("Arena/Time", Clock.hz(60)) const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Position") const Velocity = Descriptor.Component<{ x: number; y: number }>()("Arena/Velocity") -const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } })) +const Root = Schema.defineRoot("Arena") +const Game = Schema.bind(Schema.fragment({ components: { Position, Velocity }, resources: { Time } }), Root) // ---cut--- // What this system reads and writes, declared up front const Moving = Game.Query({ diff --git a/website/src/snippets/home-step-world.mdx b/website/src/snippets/home-step-world.mdx index 548fcc7f..784b702c 100644 --- a/website/src/snippets/home-step-world.mdx +++ b/website/src/snippets/home-step-world.mdx @@ -8,11 +8,13 @@ const Velocity = Descriptor.Component<{ x: number; y: number }>()("Arena/Velocit // One value for the whole world: a clock at a fixed 60 steps per second const Time = Clock.resource("Arena/Time", Clock.hz(60)) +const Root = Schema.defineRoot("Arena") // Every other API hangs off Game, so nothing can mix up two worlds export const Game = Schema.bind( Schema.fragment({ components: { Position, Velocity }, resources: { Time } - }) + }), + Root ) ``` From 6704ce1705ca05c4459be515e4e86b75e8956457 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Tue, 10 Nov 2026 15:07:12 +0900 Subject: [PATCH 3/3] Name the root where the script cannot JSDoc examples that bind their own game, the type bench's generator, the consumer programs pack-check writes, SETUP's merge for naming the schema, types whose root argument sits on its own line, Entity.isHandle(Game.schema), and the expected messages of tests whose other game now fails on its root. Co-Authored-By: Claude Opus 5.5 --- .changeset/name-the-schema.md | 2 +- .changeset/root-mandatory-browser.md | 5 +++++ .changeset/root-mandatory-render.md | 5 +++++ packages/browser/src/Controls.ts | 3 ++- packages/browser/src/InputCapture.ts | 3 ++- packages/core/SETUP.md | 3 ++- packages/core/bench/types/generate.ts | 4 +++- packages/core/dtslint/Command.tst.ts | 6 +++--- packages/core/dtslint/Condition.tst.ts | 2 +- packages/core/dtslint/DroppedRelations.tst.ts | 2 +- packages/core/dtslint/Schema.tst.ts | 6 +++--- packages/core/dtslint/StateMachine.tst.ts | 14 +++++++------- packages/core/src/Clock.ts | 3 ++- packages/core/src/Descriptor.ts | 3 ++- packages/core/src/Query.ts | 4 ++-- packages/core/src/Random.ts | 3 ++- packages/core/test/Runtime.host.test.ts | 2 +- packages/core/test/Runtime.storage.test.ts | 11 +++++++++-- packages/render/src/RenderSync.ts | 3 ++- scripts/pack-check.ts | 11 +++++++---- website/src/pages/docs/type-safety.mdx | 2 +- 21 files changed, 63 insertions(+), 34 deletions(-) create mode 100644 .changeset/root-mandatory-browser.md create mode 100644 .changeset/root-mandatory-render.md diff --git a/.changeset/name-the-schema.md b/.changeset/name-the-schema.md index ed34e4d5..3406c52f 100644 --- a/.changeset/name-the-schema.md +++ b/.changeset/name-the-schema.md @@ -5,7 +5,7 @@ The docs show how to name a game's schema type, so type errors print `QueryEntity` instead of every component, resource, and event the game declares. In a game of 64 components an error used to start with `SchemaDefinition<{ readonly Position: Descriptor<...>; ... 62 more ...` and was cut off before it reached the query. `SETUP.md` (step 4) and `Schema.bind` show the pattern; a game that already binds works unchanged, and type-checking takes the same time: ```ts -const schema = Schema.fragment({ components: { Position, Velocity } }) // or Schema.bind(Core, Combat).schema +const schema = Schema.fragment({ components: { Position, Velocity } }) // or Schema.bind(Core, Combat, Root).schema type SchemaOf = typeof schema export interface GameSchema extends SchemaOf {} const named: GameSchema = schema diff --git a/.changeset/root-mandatory-browser.md b/.changeset/root-mandatory-browser.md new file mode 100644 index 00000000..86759b4c --- /dev/null +++ b/.changeset/root-mandatory-browser.md @@ -0,0 +1,5 @@ +--- +"@typeonce/ritmo-browser": patch +--- + +The `Controls` and `InputCapture` examples declare their game's root and pass it to `Schema.bind`, which now takes one. diff --git a/.changeset/root-mandatory-render.md b/.changeset/root-mandatory-render.md new file mode 100644 index 00000000..096409fd --- /dev/null +++ b/.changeset/root-mandatory-render.md @@ -0,0 +1,5 @@ +--- +"@typeonce/ritmo-render": patch +--- + +The `RenderSync.interpolate` example declares its game's root and passes it to `Schema.bind`, which now takes one. diff --git a/packages/browser/src/Controls.ts b/packages/browser/src/Controls.ts index 1a008e73..e3aea95f 100644 --- a/packages/browser/src/Controls.ts +++ b/packages/browser/src/Controls.ts @@ -73,7 +73,8 @@ * // Only the snapshot: a replay or the fuzzer provides it in place of the devices. * const ControlsInput = Descriptor.Service>>()("Game/Controls") * const Input = Descriptor.TransientResource>()("Game/Input") - * const Game = Schema.bind(Schema.fragment({ resources: { Input } })) + * const Root = Schema.defineRoot("Game") + * const Game = Schema.bind(Schema.fragment({ resources: { Input } }), Root) * const CaptureInput = InputCapture.system(Game, { name: "Game/CaptureInput", source: ControlsInput, resource: Input }) * * const runtime = Game.Runtime.make({ diff --git a/packages/browser/src/InputCapture.ts b/packages/browser/src/InputCapture.ts index 6394493e..75cfcfd6 100644 --- a/packages/browser/src/InputCapture.ts +++ b/packages/browser/src/InputCapture.ts @@ -39,7 +39,8 @@ * * const KeyboardInput = Descriptor.Service>>()("Game/Keyboard") * const Input = Descriptor.TransientResource>()("Game/Input") - * const Game = Schema.bind(Schema.fragment({ resources: { Input } })) + * const Root = Schema.defineRoot("Game") + * const Game = Schema.bind(Schema.fragment({ resources: { Input } }), Root) * * const CaptureInput = InputCapture.system(Game, { name: "Game/CaptureInput", source: KeyboardInput, resource: Input }) * const Jump = Game.System("Game/Jump", { resources: { input: Game.System.readResource(Input) } }, ({ resources }) => { diff --git a/packages/core/SETUP.md b/packages/core/SETUP.md index 994ae6c0..6133076a 100644 --- a/packages/core/SETUP.md +++ b/packages/core/SETUP.md @@ -25,7 +25,8 @@ Follow these steps once in a new project, and for any step an existing project i After every Ritmo upgrade, compare `.oxlintrc.json` with that README's config: a new rule is not on until the config lists it, so an existing project does not get it by upgrading. 4. Name the schema's type where the game binds it, so type errors print `GameSchema` instead of every descriptor the game has: ```ts - const schema = Schema.fragment({ components: { Position, Velocity } }) // or Schema.bind(Core, Combat).schema + const Root = Schema.defineRoot("Game") + const schema = Schema.fragment({ components: { Position, Velocity } }) // or Schema.bind(Core, Combat, Root).schema type SchemaOf = typeof schema export interface GameSchema extends SchemaOf {} const named: GameSchema = schema diff --git a/packages/core/bench/types/generate.ts b/packages/core/bench/types/generate.ts index af0c9f33..57b63e5b 100644 --- a/packages/core/bench/types/generate.ts +++ b/packages/core/bench/types/generate.ts @@ -39,6 +39,7 @@ for (let index = 0; index < EVENTS; index++) { emit("const Clock = Descriptor.Service<{ readonly now: () => number }>()(\"Stress/Clock\")") emit("const { relation: ChildOf } = Descriptor.Hierarchy(\"Stress/ChildOf\", \"Stress/Children\")") emit() +emit("const Root = Schema.defineRoot(\"Stress\")") emit("const Game = Schema.bind(") const componentKeys = Array.from({ length: COMPONENTS }, (_, index) => `C${pad(index)}`) const resourceKeys = Array.from({ length: RESOURCES }, (_, index) => `R${pad(index)}`) @@ -46,7 +47,8 @@ const eventKeys = Array.from({ length: EVENTS }, (_, index) => `E${pad(index)}`) emit(` Schema.fragment({ components: { ${componentKeys.slice(0, COMPONENTS / 2).join(", ")} }, resources: { ${resourceKeys.slice(0, RESOURCES / 2).join(", ")} }, events: { ${eventKeys.slice(0, EVENTS / 2).join(", ")} } }),`) emit(` Schema.fragment({ components: { ${componentKeys.slice(COMPONENTS / 2).join(", ")} }, resources: { ${resourceKeys.slice(RESOURCES / 2).join(", ")} }, events: { ${eventKeys.slice(EVENTS / 2).join(", ")} }, relations: { ChildOf } }),`) // Requirements (a chain across fragments) on components no other section spawns: C28..C31. -emit(" Schema.fragment({ requires: [Schema.requires(C28, C29, C30), Schema.requires(C29, C31), Schema.requires(C30, C31)] })") +emit(" Schema.fragment({ requires: [Schema.requires(C28, C29, C30), Schema.requires(C29, C31), Schema.requires(C30, C31)] }),") +emit(" Root") emit(")") emit() emit("const Flow = Game.StateMachine(\"Stress/Flow\", [\"Menu\", \"Playing\", \"Paused\"])") diff --git a/packages/core/dtslint/Command.tst.ts b/packages/core/dtslint/Command.tst.ts index 9b715312..744db375 100644 --- a/packages/core/dtslint/Command.tst.ts +++ b/packages/core/dtslint/Command.tst.ts @@ -31,7 +31,7 @@ describe("Command", () => { expect(draft).type.toBe>() + }, typeof Root>>() }) it("spawn starts with an empty draft proof", () => { @@ -76,7 +76,7 @@ describe("Command", () => { expect(draft).type.toBe, readonly [null, Descriptor.SchemaIssues | null]>>() + }, typeof Root>, readonly [null, Descriptor.SchemaIssues | null]>>() }) it("has no entryResult: validation goes through the descriptor", () => { @@ -96,7 +96,7 @@ describe("Command", () => { expect(inserted).type.toBe, readonly [Descriptor.SchemaIssues | null]>>() + }, typeof Root>, readonly [Descriptor.SchemaIssues | null]>>() }) it("entryRaw rejects plain component descriptors", () => { diff --git a/packages/core/dtslint/Condition.tst.ts b/packages/core/dtslint/Condition.tst.ts index 849753e0..eeb3b4da 100644 --- a/packages/core/dtslint/Condition.tst.ts +++ b/packages/core/dtslint/Condition.tst.ts @@ -76,7 +76,7 @@ describe("Game.Condition.check", () => { it("rejects descriptors of another game and non-boolean predicates", () => { // @ts-expect-error Type '"CheckTypes/OtherFreeze"' is not assignable to type '"CheckTypes/Freeze"'. Game.Condition.check("foreignResource", { resources: { freeze: Other.System.readResource(OtherFreeze) } }, () => true) - // @ts-expect-error Type 'BoundStateMachine' is not assignable to type 'RootToken<"CheckTypes">' Game.Condition.check("foreignMachine", { machines: { flow: Other.System.machine(OtherFlow) } }, () => true) Game.Condition.check("notBoolean", { resources: { freeze: Game.System.readResource(Freeze) } }, // @ts-expect-error Type 'number' is not assignable to type 'boolean'. diff --git a/packages/core/dtslint/DroppedRelations.tst.ts b/packages/core/dtslint/DroppedRelations.tst.ts index 51491610..5ee79a4f 100644 --- a/packages/core/dtslint/DroppedRelations.tst.ts +++ b/packages/core/dtslint/DroppedRelations.tst.ts @@ -26,7 +26,7 @@ describe("Game.System.readDroppedRelations", () => { droppedRelations: { lost: Game.System.readDroppedRelations(Targeting) }, commands: Game.System.commands(Game.System.inserts(Idle), Game.System.relates(Targeting)) }, ({ droppedRelations, commands }) => { - expect(droppedRelations.lost.all()).type.toBe>>() + expect(droppedRelations.lost.all()).type.toBe>>() expect(droppedRelations.lost.lagged()).type.toBe() for (const { source, target } of droppedRelations.lost.all()) { // A system that writes the relation may read its dropped edges too. diff --git a/packages/core/dtslint/Schema.tst.ts b/packages/core/dtslint/Schema.tst.ts index 85169cd3..e33599db 100644 --- a/packages/core/dtslint/Schema.tst.ts +++ b/packages/core/dtslint/Schema.tst.ts @@ -474,7 +474,7 @@ describe("Schema", () => { readonly parent: Relation.RelationReadAccess readonly children: Relation.OptionalRelatedReadAccess readonly target: Relation.OptionalRelationReadAccess - }, readonly [], readonly [], readonly [], readonly [typeof ChildOf], readonly [], readonly [], readonly [], typeof Game.schema>>() + }, readonly [], readonly [], readonly [], readonly [typeof ChildOf], readonly [], readonly [], readonly [], typeof Root>>() const ObserveSystem = Game.System( "ObserveRelations", @@ -501,12 +501,12 @@ describe("Schema", () => { expect(relationFailures.targeting.all()).type.toBeAssignableTo>>() expect(relationFailures.childOf.all()).type.toBeAssignableTo>>() expect(queries.nodes.children(entityId, ChildOf)).type.toBeAssignableTo>, diff --git a/packages/core/dtslint/StateMachine.tst.ts b/packages/core/dtslint/StateMachine.tst.ts index 1ed9d3f6..e4260d19 100644 --- a/packages/core/dtslint/StateMachine.tst.ts +++ b/packages/core/dtslint/StateMachine.tst.ts @@ -177,7 +177,7 @@ describe("StateMachine", () => { "StateMachine/CrossSchemaSystem", { machines: { - // @ts-expect-error Property 'Position' is missing in type '{}' but required in type '{ readonly Position + // @ts-expect-error Type 'RootToken<"StateMachine/Other">' is not assignable to type 'RootToken<"StateMachine">' other: System.machine(OtherState) } }, @@ -188,14 +188,14 @@ describe("StateMachine", () => { "StateMachine/CrossSchemaTransitionEvents", { transitionEvents: { - // @ts-expect-error Property 'Position' is missing in type '{}' but required in type '{ readonly Position + // @ts-expect-error Type 'RootToken<"StateMachine/Other">' is not assignable to type 'RootToken<"StateMachine">' other: Game.System.readTransitionEvent(OtherState) } }, () => {} ) - // @ts-expect-error Property 'Position' is missing in type '{}' but required in type '{ readonly Position + // @ts-expect-error Type 'RootToken<"StateMachine/Other">' is not assignable to type 'RootToken<"StateMachine">' Game.Schedule.onEnter(OtherState, "Idle", [ReaderSystem]) const OtherBundle = OtherGame.Schedule.transitions( @@ -204,7 +204,7 @@ describe("StateMachine", () => { Game.Schedule( ReaderSystem, - // @ts-expect-error Property 'Position' is missing in type '{}' but required in type '{ readonly Position + // @ts-expect-error Type 'RootToken<"StateMachine/Other">' is not assignable to type 'RootToken<"StateMachine">' Game.Schedule.applyStateTransitions(OtherBundle) ) }) @@ -348,7 +348,7 @@ describe("StateMachine", () => { it("rejects nested transition-application markers in transition schedules", () => { Game.Schedule.onEnter(AppState, "Playing", [ TransitionSystem, - // @ts-expect-error Type 'ApplyStateTransitionsStep>' is not assignable to type 'BoundTransitionEntry< Game.Schedule.applyStateTransitions() ]) }) @@ -406,14 +406,14 @@ describe("StateMachine", () => { // Conditions from another game do not fit. Game.Schedule.when( [ - // @ts-expect-error Property 'Position' is missing in type '{}' but required in type '{ readonly Position + // @ts-expect-error Type 'RootToken<"StateMachine/Other">' is not assignable to type 'RootToken<"StateMachine">' OtherGame.Condition.inState(OtherState, "Idle") ], Increment ) // A group needs at least one condition. - // @ts-expect-error Argument of type '[]' is not assignable to parameter of type 'readonly [Condition>, ...Condition>[]]' Game.Schedule.when([], Increment) expect(gated).type.toBeAssignableTo>() diff --git a/packages/core/src/Clock.ts b/packages/core/src/Clock.ts index 8a887249..3e0d378d 100644 --- a/packages/core/src/Clock.ts +++ b/packages/core/src/Clock.ts @@ -23,7 +23,8 @@ * ```ts * const step = Clock.hz(60) * const Time = Clock.resource("Game/Time", step) - * const Game = Schema.bind(Schema.fragment({ resources: { Time } })) + * const Root = Schema.defineRoot("Game") + * const Game = Schema.bind(Schema.fragment({ resources: { Time } }), Root) * const Pause = Game.StateMachine("Game/Pause", { Running: {}, Paused: {} }) * const AdvanceTime = Clock.system(Game, { name: "Game/AdvanceTime", resource: Time, hold: [Game.Condition.inState(Pause, "Paused")] }) * // The clock resource is validated, so `make` returns a Result to check. diff --git a/packages/core/src/Descriptor.ts b/packages/core/src/Descriptor.ts index 582a42d5..4895f5cf 100644 --- a/packages/core/src/Descriptor.ts +++ b/packages/core/src/Descriptor.ts @@ -725,7 +725,8 @@ type StateRejection = Definition extends StandardSchema { diff --git a/packages/core/src/Query.ts b/packages/core/src/Query.ts index 1cb19399..1ff92a56 100644 --- a/packages/core/src/Query.ts +++ b/packages/core/src/Query.ts @@ -841,7 +841,7 @@ export interface EntityStateWriteSlot } - * declare const entity: Entity.EntityRef + * declare const entity: Entity.EntityRef * // ---cut--- * const started = data.phase.transition(entity, { from: "windup", to: "active" }) * if (!started.ok) { @@ -860,7 +860,7 @@ export interface EntityStateWriteSlot } - * declare const entity: Entity.EntityRef + * declare const entity: Entity.EntityRef * declare const gravity: number * // ---cut--- * data.contact.update(entity, { state: "air", data: (air) => ({ ...air, vy: air.vy - gravity }) }) diff --git a/packages/core/src/Random.ts b/packages/core/src/Random.ts index 2fd594c1..b7e17587 100644 --- a/packages/core/src/Random.ts +++ b/packages/core/src/Random.ts @@ -12,7 +12,8 @@ * @example * ```ts * const Rng = Descriptor.ConstructedResource(Random.codec)("Game/Rng") - * const Game = Schema.bind(Schema.fragment({ resources: { Rng } })) + * const Root = Schema.defineRoot("Game") + * const Game = Schema.bind(Schema.fragment({ resources: { Rng } }), Root) * const runtime = Game.Runtime.make({ services: Game.Runtime.services(), resources: { Rng: Random.seed(0x5eed) } }) * * const kinds = ["grunt", "archer"] as const diff --git a/packages/core/test/Runtime.host.test.ts b/packages/core/test/Runtime.host.test.ts index 76e11e59..47508477 100644 --- a/packages/core/test/Runtime.host.test.ts +++ b/packages/core/test/Runtime.host.test.ts @@ -378,7 +378,7 @@ describe("numeric handles", () => { it("brands without allocating and compares by entity identity in sets", () => { // A number from a save or a message: the guard makes it a handle, the game names its id. const raw: unknown = JSON.parse("7") - if (!Entity.isHandle(Game.schema)(raw)) throw new Error("not a handle") + if (!Entity.isHandle(Root)(raw)) throw new Error("not a handle") const id = Game.Entity.id(raw) const first = Entity.handle(id) const second = Entity.handle(id) diff --git a/packages/core/test/Runtime.storage.test.ts b/packages/core/test/Runtime.storage.test.ts index a4cdfcef..8884327e 100644 --- a/packages/core/test/Runtime.storage.test.ts +++ b/packages/core/test/Runtime.storage.test.ts @@ -192,10 +192,17 @@ describe("Runtime storage", () => { it("guards duplicate descriptor names when types are erased", () => { const Other = Descriptor.Component<{ x: number }>()("Storage/Position") - const bindErased = Schema.bind as (...fragments: ReadonlyArray) => unknown + const bindErased = Schema.bind as (...args: ReadonlyArray) => unknown expect(() => bindErased( Schema.fragment({ components: { Position } }), - Schema.fragment({ components: { Other } }) + Schema.fragment({ components: { Other } }), + Root )).toThrow("Duplicate descriptor name: Storage/Position") }) + + it("needs a root when types are erased", () => { + const bindErased = Schema.bind as (...args: ReadonlyArray) => unknown + expect(() => bindErased(Schema.fragment({ components: { Position } }))) + .toThrow("Schema.bind takes the game's root last: const Root = Schema.defineRoot('Game'), then Schema.bind(...fragments, Root)") + }) }) diff --git a/packages/render/src/RenderSync.ts b/packages/render/src/RenderSync.ts index bd3d49b0..70f448b8 100644 --- a/packages/render/src/RenderSync.ts +++ b/packages/render/src/RenderSync.ts @@ -671,7 +671,8 @@ interface InterpolateBase< * const PrevPosition = Descriptor.Component<{ x: number; y: number }>()("Game/PrevPosition") * const Position3 = Descriptor.Component<{ x: number; y: number; z: number }>()("Game/Position3") * const PrevPosition3 = Descriptor.Component<{ x: number; y: number; z: number }>()("Game/PrevPosition3") - * const Game = Schema.bind(Schema.fragment({ components: { Position, PrevPosition, Position3, PrevPosition3 } })) + * const Root = Schema.defineRoot("Game") + * const Game = Schema.bind(Schema.fragment({ components: { Position, PrevPosition, Position3, PrevPosition3 } }), Root) * const Sprites = Descriptor.Service>()("Game/Sprites") * const Meshes = Descriptor.Service>()("Game/Meshes") * // The loop's render alpha, provided by the host each frame: diff --git a/scripts/pack-check.ts b/scripts/pack-check.ts index 7a582f10..0949de10 100644 --- a/scripts/pack-check.ts +++ b/scripts/pack-check.ts @@ -124,7 +124,8 @@ import { MemoryNetworkLink, Netcode, WebSocketNetworkLink } from "@typeonce/ritm const step = Clock.hz(60) const Time = Clock.resource("Consumer/Time", step) -const Sim = Schema.bind(Schema.fragment({ resources: { Time } })) +const SimRoot = Schema.defineRoot("Consumer/Sim") +const Sim = Schema.bind(Schema.fragment({ resources: { Time } }), SimRoot) const AdvanceTime = Clock.system(Sim, { name: "Consumer/AdvanceTime", resource: Time }) const repeated = Determinism.check(({ hostClock }) => { const made = Sim.Runtime.make({ services: Sim.Runtime.services(), resources: { Time: Clock.start(step) }, debug: { hostClock } }) @@ -134,7 +135,8 @@ const repeated = Determinism.check(({ hostClock }) => { if (!repeated.ok) throw new Error(JSON.stringify(repeated)) const Seed = Descriptor.ConstructedComponent(Random.codec)("Consumer/Seed") -const Game = Schema.bind(Schema.fragment({ components: { Seed } })) +const Root = Schema.defineRoot("Consumer") +const Game = Schema.bind(Schema.fragment({ components: { Seed } }), Root) const Spawn = Game.System("Consumer/Spawn", { commands: Game.System.commands(Game.System.spawns(Seed)) }, ({ commands }) => { const draft = Game.Command.spawn(Game.Command.entryRaw(Seed, 7)) if (draft.ok) commands.spawn(draft.value) @@ -197,7 +199,7 @@ if (output !== expected) throw new Error(`consumer printed ${output}, expected $ console.log(output) step("run the published devtools CLI") -writeFileSync(path.join(consumer, "lint.mjs"), `import { HostClock, Schema } from "@typeonce/ritmo"; const Game = Schema.bind(Schema.fragment({})); export const lintTarget = () => ({ runtime: Game.Runtime.make({ services: Game.Runtime.services(), debug: { hostClock: HostClock.test() } }).value, schedules: { update: Game.Schedule() } });\n`) +writeFileSync(path.join(consumer, "lint.mjs"), `import { HostClock, Schema } from "@typeonce/ritmo"; const Game = Schema.bind(Schema.fragment({}), Schema.defineRoot("Pack")); export const lintTarget = () => ({ runtime: Game.Runtime.make({ services: Game.Runtime.services(), debug: { hostClock: HostClock.test() } }).value, schedules: { update: Game.Schedule() } });\n`) const lintOutput = run(path.join(consumer, "node_modules", ".bin", "ritmo-devtools"), ["lint", "lint.mjs"], consumer) if (!lintOutput.includes("0 warnings")) throw new Error(`published lint CLI failed: ${lintOutput}`) @@ -208,7 +210,8 @@ writeFileSync(path.join(consumer, "verify.mjs"), [ `import { Replay } from "@typeonce/ritmo-devtools"`, `const step = Clock.hz(1)`, `const Time = Clock.resource("Pack/Time", step)`, - `const Game = Schema.bind(Schema.fragment({ resources: { Time } }))`, + `const Root = Schema.defineRoot("Pack")`, + `const Game = Schema.bind(Schema.fragment({ resources: { Time } }), Root)`, `const update = Game.Schedule(Clock.system(Game, { name: "Pack/Clock", resource: Time }))`, `const make = (hostClock) => Game.Runtime.make({ services: Game.Runtime.services(), resources: { Time: Clock.start(step) }, debug: { hostClock } }).value`, `export const replayTarget = () => { const hostClock = HostClock.test(); return { runtime: make(hostClock), schedules: { update }, hostClock } }`, diff --git a/website/src/pages/docs/type-safety.mdx b/website/src/pages/docs/type-safety.mdx index bd0012d5..bb2ebc9d 100644 --- a/website/src/pages/docs/type-safety.mdx +++ b/website/src/pages/docs/type-safety.mdx @@ -263,7 +263,7 @@ const Position = Descriptor.Component<{ x: number; y: number }>()("Arena/Positio // Copied from Position, and the name was never changed const Speed = Descriptor.Component()("Arena/Position") // [!code error] -// @errors: 2345 +// @errors: 2345 2769 const Root = Schema.defineRoot("Arena") const Game = Schema.bind(Schema.fragment({ components: { Position, Speed } }), Root) ```