import { isList, isStruct, NO_OBJECT, type PropertyBag, type PropertyValue, tagOf, } from '../model/Properties'; import type { EntityHandle } 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; } export function bagWithoutObjects(bag: PropertyBag): PropertyBag { const out: PropertyBag = {}; for (const [key, value] of Object.entries(bag)) out[key] = withoutObjects(value); return out; } /** * 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. Alone it reads as nothing (`null`), inside a * list or struct as the empty object. */ export function liveValue( value: PropertyValue | undefined, isAlive: (target: EntityHandle) => boolean, ): Value { if (value === undefined || tagOf(value) === 'object') return null; if (isEntity(value)) return isAlive(value) ? value : null; return mapObjects(value, (held) => (isAlive(held) ? held : NO_OBJECT)) as Value; } /** What `outputs` read of a bag, each value live as `liveValue` says. */ export function readLive( bag: PropertyBag, outputs: readonly PortDef[], isAlive: (target: EntityHandle) => boolean, ): Record { const values: Record = {}; for (const port of outputs) if (port.key in bag) values[port.key] = liveValue(bag[port.key], isAlive); 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; untouched parts keep their identity. */ function mapObjects(value: unknown, swap: (held: EntityHandle) => unknown): unknown { if (isEntity(value as Value)) return swap(value as EntityHandle); if (isList(value)) { const items = value.map((item) => mapObjects(item, swap)); return items.some((item, i) => item !== value[i]) ? items : value; } if (isStruct(value) && tagOf(value) === undefined) { const entries = Object.entries(value); const mapped = entries.map(([key, field]) => [key, mapObjects(field, swap)] as const); return mapped.some(([, field], i) => field !== entries[i]![1]) ? Object.fromEntries(mapped) : value; } return value; }