Let each variable of a behavior choose whether calls keep it (#229)
The first version of #229 put a Keep state switch on the Run behavior node. The user wanted it where the variable is declared instead. Whether a value lives on belongs to the variable, not to every call site. - The node switch keepState is gone again. RunBehavior, CallResult and nested.ts are back to what they were before #229. - BehaviorAsset.keptVariables lists the kept names. It lives on the asset and not in PropertyDef, because that is the schema of every inspector row. - A variable added from now on starts with Keep on. Variables in existing projects start with Keep off, so old projects run unchanged. - SetVariableKeptCommand flips the switch as one undo step. Add, remove and rename carry the list along. Loading drops names that are not declared. - The interpreter keeps only the listed variables, per Run behavior node and per object as before. The others start at their defaults. - Local state machines in a called behavior start empty on every call again, as before #229. Global and scene machines are unchanged. - The inspector shows a Keep button in each variable row of a behavior. Its state is part of the asset signature, so a click redraws the row. Without that the button kept its old look until the inspector was reopened. Found: the project Garden of Sacrifices still has keepState = true on the call Player -> Character Movement. It is ignored now. Direction there predates the switch, so Keep is off until set by hand. Tests: - ./check.sh: 163 files, 2392 tests, CHECK EXIT 0 (before 2388) - tests/core/keep-state.test.ts rewritten, 15 tests - e2e 19: ALL PASSED, 35 checks. 19c-ii now clicks the Keep button of a new variable: on when added, off by a click, on again, off after undo. - Counter-checks, each fails its own test: keep list ignored (2), states kept (1), new variable not kept (8), rename drops the list (1), no Keep button (e2e 19c-ii). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c70245c79
commit
454d202109
14 files changed
+333
-149
No files matched your search
@@ -43,21 +43,20 @@ function emptyFrame(graph: Graph): Frame {
|
||||
return { graph, outputCache: new Map(), dataCache: new Map() };
|
||||
}
|
||||
|
||||
/** What a `Run behavior` with Keep state left behind in its callee (#229). */
|
||||
interface KeptState {
|
||||
variables: PropertyBag;
|
||||
states: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The variables a kept call resumes with: today's declarations, each at the value it was left at
|
||||
* unless that value is gone or no longer of the declared sort – then at its default.
|
||||
* The variables a call starts with: today's declarations at their defaults, except that a kept one
|
||||
* resumes at the value the last call from the same site left – unless that value is gone or no
|
||||
* longer of the declared sort (#229).
|
||||
*/
|
||||
export function resumedVariables(defaults: PropertyBag, kept: PropertyBag | undefined): PropertyBag {
|
||||
export function resumedVariables(
|
||||
defaults: PropertyBag,
|
||||
left: PropertyBag | undefined,
|
||||
kept: readonly string[],
|
||||
): PropertyBag {
|
||||
const variables: PropertyBag = {};
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
const left = kept?.[key];
|
||||
variables[key] = left !== undefined && typeof left === typeof value ? left : value;
|
||||
const was = kept.includes(key) ? left?.[key] : undefined;
|
||||
variables[key] = was !== undefined && typeof was === typeof value ? was : value;
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
@@ -72,8 +71,8 @@ export class GraphInterpreter {
|
||||
readonly nodeState = new Map<string, Record<string, number>>();
|
||||
/** Assets run as a node so far – the graph editor highlights them from this instance's trace. */
|
||||
readonly calledBehaviors = new Set<string>();
|
||||
/** Kept calls by call site: the chain of `Run behavior` node ids that led there. */
|
||||
readonly keptCalls = new Map<string, KeptState>();
|
||||
/** What calls left in their variables, by call site: the chain of `Run behavior` node ids that led there. */
|
||||
readonly keptCalls = new Map<string, PropertyBag>();
|
||||
private frame: Frame;
|
||||
private callStack: string[] = [];
|
||||
private callSites: string[] = [];
|
||||
@@ -163,7 +162,7 @@ export class GraphInterpreter {
|
||||
* Runs another behavior from its start node to its end node and returns what the end node was
|
||||
* handed. The nested graph gets its own frame and a fresh copy of its variable defaults, so it is
|
||||
* a call and not an inlining: nothing of it survives the return but the values it hands back –
|
||||
* unless the call keeps its state, which then waits for the next call from the same site.
|
||||
* and the variables it keeps, which wait for the next call from the same site.
|
||||
*/
|
||||
private call(call: CallResult, site: string, ctx: NodeContext, depth: number): Record<string, Value> {
|
||||
const program = this.lookup.behavior(call.behaviorId);
|
||||
@@ -172,10 +171,9 @@ export class GraphInterpreter {
|
||||
const start = terminalNode(program.graph, BEHAVIOR_START);
|
||||
if (!start) return {};
|
||||
const sites = [...this.callSites, site].join('/');
|
||||
const kept = call.keep ? this.keptCalls.get(sites) : undefined;
|
||||
const variables = resumedVariables(program.defaults, kept?.variables);
|
||||
const states = kept?.states ?? {};
|
||||
if (call.keep) this.keptCalls.set(sites, { variables, states });
|
||||
const kept = program.kept ?? [];
|
||||
const variables = resumedVariables(program.defaults, this.keptCalls.get(sites), kept);
|
||||
if (kept.length > 0) this.keptCalls.set(sites, variables);
|
||||
const outer = this.frame;
|
||||
this.frame = emptyFrame(program.graph);
|
||||
this.frame.outputCache.set(start.id, { ...call.inputs });
|
||||
@@ -185,16 +183,16 @@ export class GraphInterpreter {
|
||||
this.calledBehaviors.add(call.behaviorId);
|
||||
try {
|
||||
this.trace?.onNode(start.id);
|
||||
// A call sees nothing of the caller's variables or states. What it does to a local machine is
|
||||
// therefore not reported; a `global` machine is the run's, so moving that one is a move like
|
||||
// any other.
|
||||
// Fresh states every call, and nothing of the caller's variables or states. What it does to a
|
||||
// local machine therefore dies with the call and is not reported; a `global` machine is the
|
||||
// run's, so moving that one is a move like any other.
|
||||
this.follow(
|
||||
start,
|
||||
'exec',
|
||||
{
|
||||
...ctx,
|
||||
variables,
|
||||
states,
|
||||
states: {},
|
||||
stateChanged: (machineId, from, to) => {
|
||||
if (machineId in ctx.shared.states) ctx.stateChanged(machineId, from, to);
|
||||
},
|
||||
|
||||
@@ -48,8 +48,6 @@ export interface RepeatResult {
|
||||
export interface CallResult {
|
||||
behaviorId: string;
|
||||
inputs: Record<string, Value>;
|
||||
/** The called behavior picks up the variables and states this call site left behind (#229). */
|
||||
keep?: boolean;
|
||||
}
|
||||
|
||||
export interface RunResult {
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface BehaviorProgram {
|
||||
name: string;
|
||||
graph: Graph;
|
||||
defaults: PropertyBag;
|
||||
/** The variables a call hands on to the next call from the same site (#229); absent is none. */
|
||||
kept?: readonly string[];
|
||||
}
|
||||
|
||||
/** One state of a machine: the node id is what a port and the runtime hold, the name is the label. */
|
||||
|
||||
@@ -22,7 +22,6 @@ export const VARIABLES = 'Variables';
|
||||
export const PROPERTIES = 'Properties';
|
||||
|
||||
export const BEHAVIOR_PARAM = 'behavior';
|
||||
export const KEEP_STATE_PARAM = 'keepState';
|
||||
export const VARIABLES_PARAM = 'variablesAsset';
|
||||
|
||||
/** Whose declarations a `Variables` node reads; empty is the behavior the node sits in. */
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { textOf } from '../../../model/Properties';
|
||||
import {
|
||||
BEHAVIOR_END,
|
||||
BEHAVIOR_PARAM,
|
||||
BEHAVIOR_START,
|
||||
declaredPorts,
|
||||
KEEP_STATE_PARAM,
|
||||
RUN_BEHAVIOR,
|
||||
} from '../../nested';
|
||||
import { BEHAVIOR_END, BEHAVIOR_PARAM, BEHAVIOR_START, declaredPorts, RUN_BEHAVIOR } from '../../nested';
|
||||
import { exec } from '../shared';
|
||||
|
||||
/**
|
||||
@@ -20,15 +13,12 @@ export const RunBehavior: NodeDef = {
|
||||
label: 'Run behavior',
|
||||
category: 'flow',
|
||||
description:
|
||||
'Runs another behavior as a step of this one, passing values in and taking results out. Its ' +
|
||||
'variables start at their defaults every time, unless Keep state is on: then each object keeps ' +
|
||||
'them for this node from one call to the next.',
|
||||
'Runs another behavior as a step of this one, passing values in and taking results out. A ' +
|
||||
'variable marked Keep in that behavior carries on from the last call of this node on this ' +
|
||||
'object; the others start at their defaults every time.',
|
||||
inputs: [exec()],
|
||||
outputs: [exec()],
|
||||
params: [
|
||||
{ key: BEHAVIOR_PARAM, label: 'Behavior', kind: 'asset', default: '', assetTypes: ['behavior'] },
|
||||
{ key: KEEP_STATE_PARAM, label: 'Keep state', kind: 'boolean', default: false },
|
||||
],
|
||||
params: [{ key: BEHAVIOR_PARAM, label: 'Behavior', kind: 'asset', default: '', assetTypes: ['behavior'] }],
|
||||
derivePorts: ({ node, lookup }) => {
|
||||
const program = lookup.behavior(textOf(node.params[BEHAVIOR_PARAM] ?? ''));
|
||||
return {
|
||||
@@ -39,11 +29,7 @@ export const RunBehavior: NodeDef = {
|
||||
};
|
||||
},
|
||||
run: (_ctx, inputs, params) => ({
|
||||
call: {
|
||||
behaviorId: textOf(params[BEHAVIOR_PARAM] ?? ''),
|
||||
inputs,
|
||||
keep: params[KEEP_STATE_PARAM] === true,
|
||||
},
|
||||
call: { behaviorId: textOf(params[BEHAVIOR_PARAM] ?? ''), inputs },
|
||||
next: 'exec',
|
||||
}),
|
||||
};
|
||||
Reference in new issue
Block a user