import { isList, isStruct, mapNested, NO_OBJECT, type PropertyBag, type PropertyValue, sameValue, tagOf, } from '../model/Properties'; import { referencedId } from '../model/objectRefs'; import type { EntityHandle, NodeContext } from './NodeContext'; import type { PortDef } from './NodeRegistry'; import { isEntity, isObjectPlace, type Value, writeTyped } from './values'; /** * During a run a bag may hold live objects – in an object variable, a struct field, a list item. None * of it may leave the run: `World.syncToModel` writes a behavior's changed values into the scene, a * save is written as JSON, the inspector and the test hooks show what they are handed. Each of those * ways out passes the value through here, where every object becomes the empty one (`NO_OBJECT`). */ export function withoutObjects(value: T): T { return mapObjects(value, () => NO_OBJECT) as T; } /** * Whether a run's value still says what the model's does. What a graph put into an object's place * stays in the run, so once it holds one, the objects on either side – the model's references * included (#258) – no longer count. */ export function sameAtRest(model: PropertyValue | undefined, live: PropertyValue | undefined): boolean { if (!holdsObjects(live)) return sameValue(model, live); const none = () => NO_OBJECT; const blank = (value: PropertyValue | undefined) => mapObjects(value, none, none) as PropertyValue | undefined; return sameValue(blank(model), blank(live)); } function holdsObjects(value: unknown): boolean { if (isEntity(value as Value)) return true; if (isList(value)) return value.some(holdsObjects); return isStruct(value) && tagOf(value) === undefined && Object.values(value).some(holdsObjects); } export function bagWithoutObjects(bag: PropertyBag): PropertyBag { const out: PropertyBag = {}; for (const [key, value] of Object.entries(bag)) out[key] = withoutObjects(value); return out; } /** How a run tells what a bag's objects are: whether one is still there, and which one a reference names. */ export interface ObjectLookup { isAlive(target: EntityHandle): boolean; /** The object `ref` names, looked up from where the bag sits (see `model/objectRefs.ts`); null when gone. */ referenced(ref: string): EntityHandle | null; } /** * A bag's value as a graph reads it: an object destroyed since it was written is gone, so a graph * never goes on acting on one that has left the game, and a reference picked in the editor (#258) is * the object it names. Alone either reads as nothing (`null`) when there is none, inside a list or * struct as the empty object. */ export function liveValue(value: PropertyValue | undefined, objects: ObjectLookup): Value { if (value === undefined) return null; if (tagOf(value) === 'object') return referencedObject(value, objects); if (isEntity(value)) return objects.isAlive(value) ? value : null; return mapObjects( value, (held) => (objects.isAlive(held) ? held : NO_OBJECT), (place) => referencedObject(place, objects) ?? NO_OBJECT, ) as Value; } /** The lookup for a bag that sits on `owner`: its references are read from there. */ export function objectsOf( ctx: Pick, owner: EntityHandle, ): ObjectLookup { return { isAlive: (target) => ctx.isAlive(target), referenced: (ref) => ctx.findReferenced(owner, ref) }; } function referencedObject(place: PropertyValue, objects: ObjectLookup): Value { const ref = referencedId(place); const found = ref ? objects.referenced(ref) : null; return found && objects.isAlive(found) ? found : null; } /** What `outputs` read of a bag, each value live as `liveValue` says. */ export function readLive( bag: PropertyBag, outputs: readonly PortDef[], objects: ObjectLookup, ): Record { const values: Record = {}; for (const port of outputs) if (port.key in bag) values[port.key] = liveValue(bag[port.key], objects); return values; } /** Writes what arrives on `port` into the bag, typed as the slot is; an object port always makes an object's place. */ export function writePort(bag: PropertyBag, port: PortDef, value: Value | undefined): void { if (port.type === 'object' && !isObjectPlace(bag[port.key])) bag[port.key] = NO_OBJECT; writeTyped(bag, port.key, value); } /** * `value` with each object replaced by what `swap` says, and each object's place by what `place` * says when given; untouched parts keep their identity. */ function mapObjects( value: unknown, swap: (held: EntityHandle) => unknown, place?: (at: PropertyValue) => unknown, ): unknown { return mapNested(value, (part) => { if (isEntity(part as Value)) return swap(part as EntityHandle); if (place && tagOf(part) === 'object') return place(part as PropertyValue); return undefined; }); }