From 3d3f10fd533ed4fc21b4ef19a3e9dcb245a5ba63 Mon Sep 17 00:00:00 2001 From: Chris Maltby Date: Tue, 9 Apr 2024 14:24:47 +0100 Subject: [PATCH] Add support for "value" type to replace "union" types. Allows using any RPN values as input and is more strongly typed. Only updated ActorMoveTo, CallScript and VariableSetValue to use this so far --- src/components/forms/PropertySelect.tsx | 8 +- .../forms/UnitsSelectLabelButton.tsx | 89 ++ src/components/forms/ValueSelect.tsx | 956 ++++++++++++++++++ src/components/forms/VariableSelect.tsx | 10 +- src/components/script/ScriptEventFields.tsx | 8 +- src/components/script/ScriptEventForm.tsx | 10 +- .../script/ScriptEventFormField.tsx | 40 +- .../script/ScriptEventFormInput.tsx | 54 +- src/components/ui/buttons/Button.tsx | 1 + src/components/ui/buttons/DropdownButton.tsx | 24 +- src/components/ui/form/Checkbox.tsx | 3 +- src/components/ui/form/CheckboxField.tsx | 6 +- src/components/ui/form/InputGroup.tsx | 37 +- src/components/ui/form/MathTextarea.tsx | 3 +- src/components/ui/form/NumberInput.tsx | 7 +- src/components/ui/icons/Icons.tsx | 50 + src/components/ui/scripting/ScriptEvents.tsx | 41 +- src/components/ui/theme/ThemeInterface.ts | 4 + src/components/ui/theme/darkTheme.ts | 4 + src/components/ui/theme/lightTheme.ts | 4 + src/components/ui/theme/neonTheme.ts | 4 + src/lang/en.json | 3 + src/lib/compiler/scriptBuilder.ts | 556 ++++++++-- src/lib/events/eventActorMoveRelative.js | 1 - src/lib/events/eventActorMoveTo.js | 103 +- src/lib/events/eventActorSetState.js | 1 - src/lib/events/eventLaunchProjectile.js | 4 +- src/lib/events/eventMenu.js | 2 - src/lib/events/eventTextSetAnimationSpeed.js | 1 - src/lib/events/eventVariableMath.js | 1 - src/lib/events/eventVariableSetToValue.js | 32 +- src/shared/lib/entities/entitiesTypes.ts | 4 +- src/shared/lib/rpn/shuntingYard.ts | 3 +- src/shared/lib/scriptValue/format.ts | 110 ++ src/shared/lib/scriptValue/helpers.ts | 236 +++++ src/shared/lib/scriptValue/types.ts | 284 ++++++ src/shared/lib/scripts/autoLabel.ts | 17 +- src/shared/lib/scripts/scriptDefHelpers.ts | 12 + src/store/features/entities/entitiesState.ts | 75 +- test/scriptValue/helpers.test.ts | 533 ++++++++++ 40 files changed, 3045 insertions(+), 296 deletions(-) create mode 100644 src/components/forms/UnitsSelectLabelButton.tsx create mode 100644 src/components/forms/ValueSelect.tsx create mode 100644 src/shared/lib/scriptValue/format.ts create mode 100644 src/shared/lib/scriptValue/helpers.ts create mode 100644 src/shared/lib/scriptValue/types.ts create mode 100644 test/scriptValue/helpers.test.ts diff --git a/src/components/forms/PropertySelect.tsx b/src/components/forms/PropertySelect.tsx index cbdf656b0..9bf7af00b 100644 --- a/src/components/forms/PropertySelect.tsx +++ b/src/components/forms/PropertySelect.tsx @@ -44,8 +44,10 @@ const allCustomEventActors = Array.from(Array(10).keys()).map((i) => ({ name: `Actor ${String.fromCharCode("A".charCodeAt(0) + i)}`, })); -const Wrapper = styled.div` +export const PropertySelectWrapper = styled.div` position: relative; + width: 100%; + min-width: 78px; `; export const PropertySelect = ({ @@ -235,7 +237,7 @@ export const PropertySelect = ({ }, [options, value]); return ( - + = ({ onChange={onChangeUnits} /> )} - + ); }; diff --git a/src/components/ui/icons/Icons.tsx b/src/components/ui/icons/Icons.tsx index 609b7bbbc..53b6a3382 100644 --- a/src/components/ui/icons/Icons.tsx +++ b/src/components/ui/icons/Icons.tsx @@ -554,6 +554,56 @@ export const CodeIcon = () => ( ); +export const NumberIcon = () => ( + + + +); + +export const ExpressionIcon = () => ( + + + +); + +export const CrossIcon = () => ( + + {" "} + +); + +export const DivideIcon = () => ( + + + +); + +export const DiceIcon = () => ( + + + +); + +export const CompassIcon = () => ( + + + +); + export const VariableIcon = () => ( ` export const ScriptEventFields = styled.div` display: flex; flex-wrap: wrap; - align-items: flex-end; + align-items: flex-start; padding: 5px; & > * { @@ -293,6 +293,7 @@ export const ScriptEventFields = styled.div` interface ScriptEventFieldProps { halfWidth?: boolean; inline?: boolean; + alignBottom?: boolean; } export const ScriptEventField = styled.div` @@ -303,14 +304,21 @@ export const ScriptEventField = styled.div` ` : ""} - ${(props) => - props.inline - ? css` - flex-basis: 0; - flex-grow: 0; - margin-left: -2px; - ` - : ""} + ${(props) => + props.inline + ? css` + flex-basis: 0; + flex-grow: 0; + margin-left: -2px; + ` + : ""} + + ${(props) => + props.alignBottom + ? css` + align-self: flex-end; + ` + : ""} } `; @@ -419,6 +427,8 @@ export const ScriptEventWrapper = styled.div` interface ScriptEventFieldGroupProps { halfWidth?: boolean; + wrapItems?: boolean; + alignBottom?: boolean; } export const ScriptEventFieldGroupWrapper = styled.div` @@ -428,9 +438,20 @@ export const ScriptEventFieldGroupWrapper = styled.div + props.alignBottom + ? css` + align-self: flex-end; + ` + : ""} & > div { margin: -10px; - flex-wrap: nowrap; + ${(props) => + !props.wrapItems + ? css` + flex-wrap: nowrap; + ` + : ""} } `; diff --git a/src/components/ui/theme/ThemeInterface.ts b/src/components/ui/theme/ThemeInterface.ts index d67ce749f..04314cc33 100644 --- a/src/components/ui/theme/ThemeInterface.ts +++ b/src/components/ui/theme/ThemeInterface.ts @@ -60,6 +60,10 @@ export interface ThemeInterface { text: string; border: string; }; + brackets: { + color: string; + hoverBackground: string; + }; card: { background: string; text: string; diff --git a/src/components/ui/theme/darkTheme.ts b/src/components/ui/theme/darkTheme.ts index e70cb8394..de6c06cc5 100644 --- a/src/components/ui/theme/darkTheme.ts +++ b/src/components/ui/theme/darkTheme.ts @@ -63,6 +63,10 @@ const darkTheme: ThemeInterface = { text: "#b7babb", border: "#333333", }, + brackets: { + color: "#000000", + hoverBackground: "#222222", + }, card: { background: "#3e4142", text: "#b7babb", diff --git a/src/components/ui/theme/lightTheme.ts b/src/components/ui/theme/lightTheme.ts index 8674bf7ae..e84195ea5 100644 --- a/src/components/ui/theme/lightTheme.ts +++ b/src/components/ui/theme/lightTheme.ts @@ -63,6 +63,10 @@ const lightTheme: ThemeInterface = { text: "#3b3a3b", border: "#d4d4d4", }, + brackets: { + color: "#d4d4d4", + hoverBackground: "#f2f2f2", + }, card: { background: "#ffffff", text: "#3b3a3b", diff --git a/src/components/ui/theme/neonTheme.ts b/src/components/ui/theme/neonTheme.ts index 2ea358060..10fb9d755 100644 --- a/src/components/ui/theme/neonTheme.ts +++ b/src/components/ui/theme/neonTheme.ts @@ -64,6 +64,10 @@ const neonTheme: ThemeInterface = { text: "#f5dcec", border: "#540d6e", }, + brackets: { + color: "#000000", + hoverBackground: "#222222", + }, card: { background: "#ffffff", text: "#3b3a3b", diff --git a/src/lang/en.json b/src/lang/en.json index 02c58c396..d8396c68e 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -759,6 +759,9 @@ "FIELD_STATE_VARIABLE_DESC": "A variable to store the current state of this event", "FIELD_ANIMATION_FRAMES": "Animation Frames", "FIELD_ANIMATION_FRAMES_DESC": "The number of animation frames to cycle through.", + "FIELD_NUMBER": "Number", + "FIELD_PROPERTY": "Property", + "FIELD_REMOVE": "Remove", "// 7": "Asset Viewer ---------------------------------------------", "ASSET_SEARCH": "Search...", diff --git a/src/lib/compiler/scriptBuilder.ts b/src/lib/compiler/scriptBuilder.ts index e00a8c2a9..c4273e7c5 100644 --- a/src/lib/compiler/scriptBuilder.ts +++ b/src/lib/compiler/scriptBuilder.ts @@ -36,6 +36,7 @@ import { isPropertyField, isVariableField, isActorField, + isScriptValueField, } from "shared/lib/scripts/scriptDefHelpers"; import compileEntityEvents from "./compileEntityEvents"; import { @@ -57,6 +58,20 @@ import { encodeString } from "shared/lib/helpers/fonts"; import { mapScript } from "shared/lib/scripts/walk"; import { ScriptEventHandlers } from "lib/project/loadScriptEventHandlers"; import { VariableMapData } from "lib/compiler/compileData"; +import { + isScriptValue, + PrecompiledValueFetch, + PrecompiledValueRPNOperation, + ScriptValue, + ValueFunction, +} from "shared/lib/scriptValue/types"; +import { + mapScriptValueLeafNodes, + optimiseScriptValue, + precompileScriptValue, + sortFetchOperations, + multiplyScriptValueConst, +} from "shared/lib/scriptValue/helpers"; export type ScriptOutput = string[]; @@ -247,6 +262,16 @@ type ScriptBuilderActorFlags = | ".ACTOR_FLAG_COLLISION" | ".ACTOR_FLAG_PERSISTENT"; +type RPNHandler = { + ref: (variable: ScriptBuilderStackVariable) => RPNHandler; + refInd: (variable: ScriptBuilderStackVariable) => RPNHandler; + refVariable: (variable: ScriptBuilderVariable) => RPNHandler; + int8: (value: number | string) => RPNHandler; + int16: (value: number | string) => RPNHandler; + operator: (op: ScriptBuilderRPNOperation) => RPNHandler; + stop: () => void; +}; + // - Helpers -------------- const isObject = (value: unknown): value is Record => { @@ -419,6 +444,38 @@ const toScriptOperator = ( assertUnreachable(operator); }; +const valueFunctionToScriptOperator = ( + operator: ValueFunction +): ScriptBuilderRPNOperation => { + switch (operator) { + case "add": + return ".ADD"; + case "sub": + return ".SUB"; + case "div": + return ".DIV"; + case "mul": + return ".MUL"; + case "eq": + return ".EQ"; + case "ne": + return ".NE"; + case "lt": + return ".LT"; + case "lte": + return ".LTE"; + case "gt": + return ".GT"; + case "gte": + return ".GTE"; + case "min": + return ".MIN"; + case "max": + return ".MAX"; + } + assertUnreachable(operator); +}; + const funToScriptOperator = ( fun: FunctionSymbol ): ScriptBuilderRPNOperation => { @@ -729,7 +786,12 @@ class ScriptBuilder { } rpn.stop(); } else { - this._stackPushConst(0); + // If expression empty use value 0 + if (resultVariable !== undefined) { + this._setVariableConst(resultVariable, 0); + } else { + this._stackPushConst(0); + } } }; @@ -873,7 +935,10 @@ class ScriptBuilder { } }; - _setVariableConst = (variable: string, value: ScriptBuilderStackVariable) => { + _setVariableConst = ( + variable: ScriptBuilderVariable, + value: ScriptBuilderStackVariable + ) => { const variableAlias = this.getVariableAlias(variable); if (this._isIndirectVariable(variable)) { const valueTmpRef = this._declareLocal("value_tmp", 1, true); @@ -1191,6 +1256,173 @@ class ScriptBuilder { return rpn; }; + _performFetchOperations = ( + fetchOps: PrecompiledValueFetch[] + ): Record => { + const localsLookup: Record = {}; + const sortedFetchOps = sortFetchOperations(fetchOps); + + let currentActor = "-1"; + let currentProperty = ""; + let currentPropData = ""; + let prevLocalVar = ""; + for (const fetchOp of sortedFetchOps) { + const localVar = this._declareLocal("local", 1, true); + localsLookup[fetchOp.local] = localVar; + switch (fetchOp.value.type) { + case "rnd": { + const min = Math.min( + fetchOp.value.valueA?.value ?? 0, + fetchOp.value.valueB?.value ?? 0 + ); + const max = Math.max( + fetchOp.value.valueA?.value ?? 0, + fetchOp.value.valueB?.value ?? 0 + ); + this._addComment(`-- Rand between ${min} and ${max} (inclusive)`); + this._rand(localVar, min, max - min + 1); + break; + } + case "property": { + const actorValue = fetchOp.value.target || "player"; + const propertyValue = fetchOp.value.property || "xpos"; + + if ( + actorValue === currentActor && + propertyValue === currentProperty && + prevLocalVar + ) { + // If requested prop was fetched previously, reuse local var, don't fetch again + localsLookup[fetchOp.local] = prevLocalVar; + delete this.localsLookup[localVar]; + continue; + } + + this._addComment(`-- Fetch ${actorValue} ${propertyValue}`); + if (currentActor !== actorValue) { + this.actorSetById(actorValue); + currentActor = actorValue; + currentPropData = ""; + } + if (propertyValue === "xpos") { + const actorRef = this._declareLocal("actor", 4); + if (currentPropData !== "pos") { + this._actorGetPosition(actorRef); + currentPropData = "pos"; + } + this._rpn() // + .ref(this._localRef(actorRef, 1)) + .int16(8 * 16) + .operator(".DIV") + .refSet(localVar) + .stop(); + } else if (propertyValue === "ypos") { + const actorRef = this._declareLocal("actor", 4); + if (currentPropData !== "pos") { + this._actorGetPosition(actorRef); + currentPropData = "pos"; + } + this._rpn() // + .ref(this._localRef(actorRef, 2)) + .int16(8 * 16) + .operator(".DIV") + .refSet(localVar) + .stop(); + } else if (propertyValue === "pxpos") { + const actorRef = this._declareLocal("actor", 4); + if (currentPropData !== "pos") { + this._actorGetPosition(actorRef); + currentPropData = "pos"; + } + this._rpn() // + .ref(this._localRef(actorRef, 1)) + .int16(16) + .operator(".DIV") + .refSet(localVar) + .stop(); + } else if (propertyValue === "pypos") { + const actorRef = this._declareLocal("actor", 4); + if (currentPropData !== "pos") { + this._actorGetPosition(actorRef); + currentPropData = "pos"; + } + this._rpn() // + .ref(this._localRef(actorRef, 2)) + .int16(16) + .operator(".DIV") + .refSet(localVar) + .stop(); + } else if (propertyValue === "direction") { + const actorRef = this._declareLocal("actor", 4); + this._actorGetDirection(actorRef, localVar); + } else if (propertyValue === "frame") { + const actorRef = this._declareLocal("actor", 4); + if (currentPropData !== "frame") { + this._actorGetAnimFrame(actorRef); + currentPropData = "frame"; + } + this._set(localVar, this._localRef(actorRef, 1)); + } else { + throw new Error(`Unsupported property type "${propertyValue}"`); + } + currentProperty = propertyValue; + prevLocalVar = localVar; + break; + } + case "expression": { + this._addComment( + `-- Evaluate expression ${this._expressionToHumanReadable( + fetchOp.value.value + )}` + ); + this._stackPushEvaluatedExpression(fetchOp.value.value, localVar); + break; + } + default: { + assertUnreachable(fetchOp.value); + } + } + } + + return localsLookup; + }; + + _performValueRPN = ( + rpn: RPNHandler, + rpnOps: PrecompiledValueRPNOperation[], + localsLookup: Record + ) => { + for (const rpnOp of rpnOps) { + switch (rpnOp.type) { + case "number": { + rpn.int16(rpnOp.value ?? 0); + break; + } + case "variable": { + rpn.refVariable(rpnOp.value); + break; + } + case "local": { + this._markLocalUse(localsLookup[rpnOp.value]); + rpn.ref(localsLookup[rpnOp.value]); + break; + } + case "indirect": { + rpn.refInd(rpnOp.value); + break; + } + case "direction": { + rpn.int16(toASMDir(rpnOp.value)); + break; + } + default: { + const op = valueFunctionToScriptOperator(rpnOp.type); + rpn.operator(op); + } + } + } + }; + _if = ( operator: ScriptBuilderComparisonOperator, variableA: ScriptBuilderStackVariable, @@ -2188,6 +2420,60 @@ extern void __mute_mask_${symbol}; this._addNL(); }; + actorMoveToScriptValues = ( + actorId: string, + valueX: ScriptValue, + valueY: ScriptValue, + useCollisions: boolean, + moveType: ScriptBuilderMoveType, + units: DistanceUnitType = "tiles" + ) => { + const actorRef = this._declareLocal("actor", 4); + const stackPtr = this.stackPtr; + this._addComment("Actor Move To"); + + const [rpnOpsX, fetchOpsX] = precompileScriptValue( + optimiseScriptValue( + multiplyScriptValueConst(valueX, (units === "tiles" ? 8 : 1) * 16) + ), + "x" + ); + const [rpnOpsY, fetchOpsY] = precompileScriptValue( + optimiseScriptValue( + multiplyScriptValueConst(valueY, (units === "tiles" ? 8 : 1) * 16) + ), + "y" + ); + + const localsLookup = this._performFetchOperations([ + ...fetchOpsX, + ...fetchOpsY, + ]); + + const rpn = this._rpn(); + + this._addComment(`-- Calculate coordinate values`); + + // X Value + this._performValueRPN(rpn, rpnOpsX, localsLookup); + rpn.refSet(this._localRef(actorRef, 1)); + + // Y Value + this._performValueRPN(rpn, rpnOpsY, localsLookup); + rpn.refSet(this._localRef(actorRef, 2)); + + rpn.stop(); + this._setConst( + this._localRef(actorRef, 3), + toASMMoveFlags(moveType, useCollisions) + ); + this._addComment(`-- Move Actor`); + this.actorSetById(actorId); + this._actorMoveTo(actorRef); + this._assertStackNeutral(stackPtr); + this._addNL(); + }; + actorMoveRelative = ( x = 0, y = 0, @@ -3292,10 +3578,7 @@ extern void __mute_mask_${symbol}; // -------------------------------------------------------------------------- // Call Script - callScript = ( - scriptId: string, - input: Dictionary - ) => { + callScript = (scriptId: string, input: Dictionary) => { const { customEvents } = this.options; const customEvent = customEvents.find((ce) => ce.id === scriptId); @@ -3317,18 +3600,44 @@ extern void __mute_mask_${symbol}; const actorArgs = Object.values(customEvent.actors); const variableArgs = Object.values(customEvent.variables); - const constArgLookup: Record = {}; + const constArgLookup: Record = {}; if (variableArgs) { for (const variableArg of variableArgs) { - if (variableArg && variableArg.passByReference) { + if (variableArg) { const variableValue = input?.[`$variable[${variableArg.id}]$`] || ""; + if ( typeof variableValue !== "string" && - variableValue.type === "number" + variableValue.type !== "variable" && + variableValue.type !== "number" ) { + const [rpnOps, fetchOps] = precompileScriptValue( + optimiseScriptValue(variableValue) + ); const argRef = this._declareLocal("arg", 1, true); - this._setConst(argRef, variableValue.value); - constArgLookup[variableValue.value] = argRef; + + if (rpnOps.length === 1 && rpnOps[0].type === "number") { + this._setConst(argRef, rpnOps[0].value); + } else { + const localsLookup = this._performFetchOperations(fetchOps); + this._addComment(`-- Calculate value`); + const rpn = this._rpn(); + this._performValueRPN(rpn, rpnOps, localsLookup); + rpn.refSet(argRef).stop(); + } + + constArgLookup[JSON.stringify(variableValue)] = argRef; + } else if (variableArg.passByReference) { + const variableValue = + input?.[`$variable[${variableArg.id}]$`] || ""; + if ( + typeof variableValue !== "string" && + variableValue.type === "number" + ) { + const argRef = this._declareLocal("arg", 1, true); + this._setConst(argRef, variableValue.value); + constArgLookup[JSON.stringify(variableValue)] = argRef; + } } } } @@ -3356,11 +3665,6 @@ extern void __mute_mask_${symbol}; if (typeof variableValue === "string") { const variableAlias = this.getVariableAlias(variableValue); this._stackPushConst(variableAlias, `Variable ${variableArg.id}`); - } else if (variableValue && variableValue.type === "number") { - // Arg is union number - const argRef = constArgLookup[variableValue.value]; - this._stackPushReference(argRef, `Variable ${variableArg.id}`); - this._markLocalUse(argRef); } else if (variableValue && variableValue.type === "variable") { // Arg is a union variable const variableAlias = this.getVariableAlias(variableValue.value); @@ -3373,6 +3677,11 @@ extern void __mute_mask_${symbol}; `Variable ${variableArg.id}` ); } + } else { + // Arg is a script value + const argRef = constArgLookup[JSON.stringify(variableValue)]; + this._stackPushReference(argRef, `Variable ${variableArg.id}`); + this._markLocalUse(argRef); } // End of Pass by Reference ---------- @@ -3399,6 +3708,11 @@ extern void __mute_mask_${symbol}; // Arg union value is variable id this._stackPush(variableAlias); } + } else { + // Arg is a script value + const argRef = constArgLookup[JSON.stringify(variableValue)]; + this._stackPush(argRef); + this._markLocalUse(argRef); } // End of Pass by Value ---------- @@ -3496,85 +3810,125 @@ extern void __mute_mask_${symbol}; } } - const script = mapScript(customEvent.script, (scriptEvent) => { - if (!scriptEvent.args || scriptEvent.args.__comment) return scriptEvent; - // Clone event - const e = { - ...scriptEvent, - args: { ...scriptEvent.args }, - }; - Object.keys(e.args).forEach((arg) => { - const argValue = e.args[arg]; - // Update variable fields - if ( - isVariableField( - e.command, - arg, - e.args, - this.options.scriptEventHandlers - ) - ) { + const script = mapScript( + customEvent.script, + (event: ScriptEvent): ScriptEvent => { + if (!event.args || event.args.__comment) return event; + // Clone event + const e = { + ...event, + args: { ...event.args }, + }; + Object.keys(e.args).forEach((arg) => { + const argValue = e.args[arg]; + // Update variable fields + if ( + isVariableField( + e.command, + arg, + e.args, + this.options.scriptEventHandlers + ) + ) { + if ( + isUnionVariableValue(argValue) && + argValue.value && + isVariableCustomEvent(argValue.value) + ) { + e.args[arg] = { + ...argValue, + value: getArg("variable", argValue.value), + }; + } else if ( + typeof argValue === "string" && + isVariableCustomEvent(argValue) + ) { + e.args[arg] = getArg("variable", argValue); + } + } + // Update property fields if ( - isUnionVariableValue(argValue) && - argValue.value && - isVariableCustomEvent(argValue.value) + isPropertyField( + e.command, + arg, + e.args, + this.options.scriptEventHandlers + ) ) { - e.args[arg] = { - ...argValue, - value: getArg("variable", argValue.value), + const replacePropertyValueActor = (p: string) => { + const actorValue = p.replace(/:.*/, ""); + if (actorValue === "player") { + return p; + } + const newActorValue = getArg("actor", actorValue); + return { + value: newActorValue, + property: p.replace(/.*:/, ""), + }; }; - } else if ( - typeof argValue === "string" && - isVariableCustomEvent(argValue) + if (isUnionPropertyValue(argValue) && argValue.value) { + e.args[arg] = { + ...argValue, + value: replacePropertyValueActor(argValue.value), + }; + } else if (typeof argValue === "string") { + e.args[arg] = replacePropertyValueActor(argValue); + } + } + // Update actor fields + if ( + isActorField( + e.command, + arg, + e.args, + this.options.scriptEventHandlers + ) && + typeof argValue === "string" ) { - e.args[arg] = getArg("variable", argValue); + e.args[arg] = getArg("actor", argValue); // input[`$variable[${argValue}]$`]; } - } - // Update property fields - if ( - isPropertyField( - e.command, - arg, - e.args, - this.options.scriptEventHandlers - ) - ) { - const replacePropertyValueActor = (p: string) => { - const actorValue = p.replace(/:.*/, ""); - if (actorValue === "player") { - return p; + // Update script value fields + if ( + isScriptValueField( + e.command, + arg, + e.args, + this.options.scriptEventHandlers + ) + ) { + if (isScriptValue(argValue)) { + e.args[arg] = mapScriptValueLeafNodes(argValue, (val) => { + if (val.type === "variable") { + const scriptArg = argLookup["variable"].get(val.value); + if (scriptArg?.indirect) { + return { + type: "indirect", + value: scriptArg.symbol, + }; + } + } else if (val.type === "property") { + const scriptArg = getArg("actor", val.target); + if (scriptArg && typeof scriptArg === "string") { + return { + ...val, + value: scriptArg, + }; + } else if (scriptArg && typeof scriptArg !== "string") { + return { + ...val, + target: scriptArg.symbol, + value: scriptArg, + }; + } + } + return val; + }); } - const newActorValue = getArg("actor", actorValue); - return { - value: newActorValue, - property: p.replace(/.*:/, ""), - }; - }; - if (isUnionPropertyValue(argValue) && argValue.value) { - e.args[arg] = { - ...argValue, - value: replacePropertyValueActor(argValue.value), - }; - } else if (typeof argValue === "string") { - e.args[arg] = replacePropertyValueActor(argValue); } - } - // Update actor fields - if ( - isActorField( - e.command, - arg, - e.args, - this.options.scriptEventHandlers - ) && - typeof argValue === "string" - ) { - e.args[arg] = getArg("actor", argValue); // input[`$variable[${argValue}]$`]; - } - }); - - return e; - }); + }); + return e; + } + ); // Generate symbol and cache it before compiling script to allow recursive function calls to work const symbol = this._getAvailableSymbol( @@ -3584,12 +3938,7 @@ extern void __mute_mask_${symbol}; const result = { scriptRef: symbol, argsLen }; compiledCustomEventScriptCache[customEventId] = result; - this._compileSubScript("custom", script, symbol, { - argLookup, - entity: customEvent, - entityType: "customEvent", - entityScriptKey: "script", - }); + this._compileSubScript("custom", script, symbol, { argLookup }); return result; }; @@ -3865,6 +4214,25 @@ extern void __mute_mask_${symbol}; this._addNL(); }; + variableSetToScriptValue = (variable: string, value: ScriptValue) => { + this._addComment("Variable Set To"); + const [rpnOps, fetchOps] = precompileScriptValue( + optimiseScriptValue(value) + ); + if (rpnOps.length === 1 && rpnOps[0].type === "number") { + this._setVariableConst(variable, rpnOps[0].value); + } else if (rpnOps.length === 1 && rpnOps[0].type === "variable") { + this._setVariableToVariable(variable, rpnOps[0].value); + } else { + const localsLookup = this._performFetchOperations(fetchOps); + this._addComment(`-- Calculate value`); + const rpn = this._rpn(); + this._performValueRPN(rpn, rpnOps, localsLookup); + rpn.refSetVariable(variable).stop(); + } + this._addNL(); + }; + variableCopy = ( setVariable: ScriptBuilderVariable, otherVariable: ScriptBuilderVariable diff --git a/src/lib/events/eventActorMoveRelative.js b/src/lib/events/eventActorMoveRelative.js index 39cb555cb..c7d51b14d 100644 --- a/src/lib/events/eventActorMoveRelative.js +++ b/src/lib/events/eventActorMoveRelative.js @@ -67,7 +67,6 @@ const fields = [ label: l10n("FIELD_USE_COLLISIONS"), description: l10n("FIELD_USE_COLLISIONS_DESC"), width: "50%", - alignCheckbox: true, type: "checkbox", defaultValue: false, }, diff --git a/src/lib/events/eventActorMoveTo.js b/src/lib/events/eventActorMoveTo.js index 28e0493bb..21d2d965d 100644 --- a/src/lib/events/eventActorMoveTo.js +++ b/src/lib/events/eventActorMoveTo.js @@ -21,17 +21,18 @@ const fields = [ description: l10n("FIELD_ACTOR_MOVE_DESC"), type: "actor", defaultValue: "$self$", + flexBasis: 0, + minWidth: 150, }, { type: "group", + wrapItems: true, fields: [ { key: "x", label: l10n("FIELD_X"), description: l10n("FIELD_X_DESC"), - type: "union", - types: ["number", "variable", "property"], - defaultType: "number", + type: "value", min: 0, max: 255, width: "50%", @@ -39,18 +40,15 @@ const fields = [ unitsDefault: "tiles", unitsAllowed: ["tiles", "pixels"], defaultValue: { - number: 0, - variable: "LAST_VARIABLE", - property: "$self$:xpos", + type: "number", + value: 0, }, }, { key: "y", label: l10n("FIELD_Y"), description: l10n("FIELD_Y_DESC"), - type: "union", - types: ["number", "variable", "property"], - defaultType: "number", + type: "value", min: 0, max: 255, width: "50%", @@ -58,65 +56,52 @@ const fields = [ unitsDefault: "tiles", unitsAllowed: ["tiles", "pixels"], defaultValue: { - number: 0, - variable: "LAST_VARIABLE", - property: "$self$:ypos", + type: "number", + value: 0, }, }, ], }, { - key: "moveType", - label: l10n("FIELD_MOVE_TYPE"), - description: l10n("FIELD_MOVE_TYPE_DESC"), - hideLabel: true, - type: "moveType", - defaultValue: "horizontal", - flexBasis: 30, - flexGrow: 0, - }, - { - key: "useCollisions", - label: l10n("FIELD_USE_COLLISIONS"), - description: l10n("FIELD_USE_COLLISIONS_DESC"), - width: "50%", - alignCheckbox: true, - type: "checkbox", - defaultValue: false, + type: "group", + flexBasis: 0, + minWidth: 150, + alignBottom: true, + fields: [ + { + key: "moveType", + label: l10n("FIELD_MOVE_TYPE"), + description: l10n("FIELD_MOVE_TYPE_DESC"), + hideLabel: true, + type: "moveType", + defaultValue: "horizontal", + flexBasis: 35, + flexGrow: 0, + alignBottom: true, + }, + { + key: "useCollisions", + label: l10n("FIELD_USE_COLLISIONS"), + description: l10n("FIELD_USE_COLLISIONS_DESC"), + width: "50%", + type: "checkbox", + defaultValue: false, + alignBottom: true, + }, + ], }, ]; const compile = (input, helpers) => { - const { - actorSetActive, - actorMoveTo, - actorMoveToVariables, - variableFromUnion, - temporaryEntityVariable, - } = helpers; - if (input.x.type === "number" && input.y.type === "number") { - // If all inputs are numbers use fixed implementation - actorSetActive(input.actorId); - actorMoveTo( - input.x.value, - input.y.value, - input.useCollisions, - input.moveType, - input.units - ); - } else { - // If any value is not a number transfer values into variables and use variable implementation - const xVar = variableFromUnion(input.x, temporaryEntityVariable(0)); - const yVar = variableFromUnion(input.y, temporaryEntityVariable(1)); - actorSetActive(input.actorId); - actorMoveToVariables( - xVar, - yVar, - input.useCollisions, - input.moveType, - input.units - ); - } + const { actorMoveToScriptValues } = helpers; + actorMoveToScriptValues( + input.actorId, + input.x, + input.y, + input.useCollisions, + input.moveType, + input.units + ); }; module.exports = { diff --git a/src/lib/events/eventActorSetState.js b/src/lib/events/eventActorSetState.js index 22d28ed79..180f588c7 100644 --- a/src/lib/events/eventActorSetState.js +++ b/src/lib/events/eventActorSetState.js @@ -31,7 +31,6 @@ const fields = [ label: l10n("FIELD_LOOP_ANIMATION"), description: l10n("FIELD_LOOP_ANIMATION_DESC"), type: "checkbox", - alignCheckbox: true, defaultValue: true, width: "50%", }, diff --git a/src/lib/events/eventLaunchProjectile.js b/src/lib/events/eventLaunchProjectile.js index 85e8180f3..df523d4f2 100644 --- a/src/lib/events/eventLaunchProjectile.js +++ b/src/lib/events/eventLaunchProjectile.js @@ -138,6 +138,7 @@ const fields = [ ], inline: true, defaultValue: "direction", + alignBottom: true, }, ], }, @@ -186,13 +187,13 @@ const fields = [ }, { type: "group", + alignBottom: true, fields: [ { key: "loopAnim", label: l10n("FIELD_LOOP_ANIMATION"), description: l10n("FIELD_LOOP_ANIMATION_DESC"), type: "checkbox", - alignCheckbox: true, defaultValue: true, }, { @@ -200,7 +201,6 @@ const fields = [ label: l10n("FIELD_DESTROY_ON_HIT"), description: l10n("FIELD_PROJECTILE_DESTROY_ON_HIT_DESC"), type: "checkbox", - alignCheckbox: true, defaultValue: true, }, ], diff --git a/src/lib/events/eventMenu.js b/src/lib/events/eventMenu.js index 36ea14f16..f1906b9f8 100644 --- a/src/lib/events/eventMenu.js +++ b/src/lib/events/eventMenu.js @@ -112,7 +112,6 @@ const fields = [].concat( label: l10n("FIELD_LAST_OPTION_CANCELS"), description: l10n("FIELD_LAST_OPTION_CANCELS_DESC"), key: "cancelOnLastOption", - alignCheckbox: true, }, { type: "checkbox", @@ -120,7 +119,6 @@ const fields = [].concat( description: l10n("FIELD_CANCEL_IF_B_DESC"), key: "cancelOnB", defaultValue: true, - alignCheckbox: true, }, { key: "layout", diff --git a/src/lib/events/eventTextSetAnimationSpeed.js b/src/lib/events/eventTextSetAnimationSpeed.js index 58a73b9c7..50c0abea8 100644 --- a/src/lib/events/eventTextSetAnimationSpeed.js +++ b/src/lib/events/eventTextSetAnimationSpeed.js @@ -33,7 +33,6 @@ const fields = [ description: l10n("FIELD_ALLOW_FASTFORWARD_DESC"), key: "allowFastForward", defaultValue: true, - alignCheckbox: true, }, ]; diff --git a/src/lib/events/eventVariableMath.js b/src/lib/events/eventVariableMath.js index 01d8308bd..d8d535dc2 100644 --- a/src/lib/events/eventVariableMath.js +++ b/src/lib/events/eventVariableMath.js @@ -151,7 +151,6 @@ const fields = [ }, ], defaultValue: false, - alignCheckbox: true, }, ]; diff --git a/src/lib/events/eventVariableSetToValue.js b/src/lib/events/eventVariableSetToValue.js index 3135ca7d5..9fb3dd062 100644 --- a/src/lib/events/eventVariableSetToValue.js +++ b/src/lib/events/eventVariableSetToValue.js @@ -17,42 +17,24 @@ const fields = [ description: l10n("FIELD_VARIABLE_DESC"), type: "variable", defaultValue: "LAST_VARIABLE", + flexBasis: 0, + minWidth: 150, }, { key: "value", label: l10n("FIELD_VALUE"), description: l10n("FIELD_VALUE_SET_DESC"), - type: "union", - types: ["number", "variable", "property"], - defaultType: "number", - min: -32768, - max: 32767, + type: "value", defaultValue: { - number: 0, - variable: "LAST_VARIABLE", - property: "$self$:xpos", + type: "number", + value: 0, }, }, ]; const compile = (input, helpers) => { - const { variableSetToUnionValue } = helpers; - - if (input.value.type === "number") { - const value = parseInt(input.value.value, 10); - if (value === 1) { - const { variableSetToTrue } = helpers; - variableSetToTrue(input.variable); - } else if (value === 0 || isNaN(value)) { - const { variableSetToFalse } = helpers; - variableSetToFalse(input.variable); - } else { - const { variableSetToValue } = helpers; - variableSetToValue(input.variable, value); - } - } else { - variableSetToUnionValue(input.variable, input.value); - } + const { variableSetToScriptValue } = helpers; + variableSetToScriptValue(input.variable, input.value); }; module.exports = { diff --git a/src/shared/lib/entities/entitiesTypes.ts b/src/shared/lib/entities/entitiesTypes.ts index c75bad675..4c5fa4abf 100644 --- a/src/shared/lib/entities/entitiesTypes.ts +++ b/src/shared/lib/entities/entitiesTypes.ts @@ -498,8 +498,10 @@ export interface ScriptEventFieldSchema { width?: string; flexBasis?: string | number; flexGrow?: number; + alignBottom?: boolean; + wrapItems?: boolean; + minWidth?: string | number; values?: Record; - alignCheckbox?: boolean; placeholder?: string; rows?: number; maxLength?: number; diff --git a/src/shared/lib/rpn/shuntingYard.ts b/src/shared/lib/rpn/shuntingYard.ts index e2f014c48..bd5586683 100644 --- a/src/shared/lib/rpn/shuntingYard.ts +++ b/src/shared/lib/rpn/shuntingYard.ts @@ -3,7 +3,8 @@ import { Associativity, Token } from "./types"; const shuntingYard = (input: Token[]): Token[] => { if (input.length === 0) { - throw new Error("Input was empty"); + // Input was empty + return [{ type: "VAL", value: 0 }]; } const output: Token[] = []; diff --git a/src/shared/lib/scriptValue/format.ts b/src/shared/lib/scriptValue/format.ts new file mode 100644 index 000000000..c29b39ce1 --- /dev/null +++ b/src/shared/lib/scriptValue/format.ts @@ -0,0 +1,110 @@ +import { ScriptValue } from "./types"; + +interface ScriptValueToStringOptions { + variableNameForId: (value: string) => string; + actorNameForId: (value: string) => string; + propertyNameForId: (value: string) => string; + directionForValue: (value: string) => string; +} + +export const assertUnreachable = (_x: never): never => { + throw new Error("Didn't expect to get here"); +}; + +export const scriptValueToString = ( + value: ScriptValue | undefined, + options: ScriptValueToStringOptions +): string => { + if (!value) { + return "0"; + } + if (value.type === "number") { + return String(value.value); + } else if (value.type === "variable") { + return options.variableNameForId(value.value); + } else if (value.type === "direction") { + return options.directionForValue(value.value); + } else if (value.type === "property") { + return `${options.actorNameForId(value.target)}.${options.propertyNameForId( + value.property + )}`; + } else if (value.type === "expression") { + return String(value.value || "0").replace( + /\$([VLT]*[0-9]+)\$/g, + (_, match) => { + return options.variableNameForId(match); + } + ); + } else if (value.type === "add") { + return `(${scriptValueToString( + value.valueA, + options + )} + ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "sub") { + return `(${scriptValueToString( + value.valueA, + options + )} - ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "mul") { + return `(${scriptValueToString( + value.valueA, + options + )} * ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "div") { + return `(${scriptValueToString( + value.valueA, + options + )} / ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "gt") { + return `(${scriptValueToString( + value.valueA, + options + )} > ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "gte") { + return `(${scriptValueToString( + value.valueA, + options + )} >= ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "lt") { + return `(${scriptValueToString( + value.valueA, + options + )} < ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "lte") { + return `(${scriptValueToString( + value.valueA, + options + )} <= ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "eq") { + return `(${scriptValueToString( + value.valueA, + options + )} == ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "ne") { + return `(${scriptValueToString( + value.valueA, + options + )} != ${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "min") { + return `min(${scriptValueToString( + value.valueA, + options + )},${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "max") { + return `max(${scriptValueToString( + value.valueA, + options + )},${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "rnd") { + return `rnd(${scriptValueToString( + value.valueA, + options + )},${scriptValueToString(value.valueB, options)})`; + } else if (value.type === "indirect") { + return `INDIRECT`; + } + + assertUnreachable(value); + + return ""; +}; diff --git a/src/shared/lib/scriptValue/helpers.ts b/src/shared/lib/scriptValue/helpers.ts new file mode 100644 index 000000000..041ea5116 --- /dev/null +++ b/src/shared/lib/scriptValue/helpers.ts @@ -0,0 +1,236 @@ +import { + PrecompiledValueRPNOperation, + PrecompiledValueFetch, + isValueOperation, + ScriptValue, +} from "./types"; + +const boolToInt = (val: boolean) => (val ? 1 : 0); +const zero = { + type: "number", + value: 0, +} as const; + +export const optimiseScriptValue = (input: ScriptValue): ScriptValue => { + if ("valueA" in input && input.type !== "rnd") { + const optimisedA = input.valueA ? optimiseScriptValue(input.valueA) : zero; + const optimisedB = input.valueB ? optimiseScriptValue(input.valueB) : zero; + + if (optimisedA?.type === "number" && optimisedB?.type === "number") { + // Can perform constant folding as both inputs are numbers + if (input.type === "add") { + return { + type: "number", + value: Math.floor(optimisedA.value + optimisedB.value), + }; + } else if (input.type === "sub") { + return { + type: "number", + value: Math.floor(optimisedA.value - optimisedB.value), + }; + } else if (input.type === "mul") { + return { + type: "number", + value: Math.floor(optimisedA.value * optimisedB.value), + }; + } else if (input.type === "div") { + return { + type: "number", + value: Math.floor(optimisedA.value / optimisedB.value), + }; + } else if (input.type === "gt") { + return { + type: "number", + value: boolToInt(optimisedA.value > optimisedB.value), + }; + } else if (input.type === "gte") { + return { + type: "number", + value: boolToInt(optimisedA.value >= optimisedB.value), + }; + } else if (input.type === "lt") { + return { + type: "number", + value: boolToInt(optimisedA.value < optimisedB.value), + }; + } else if (input.type === "lte") { + return { + type: "number", + value: boolToInt(optimisedA.value <= optimisedB.value), + }; + } else if (input.type === "eq") { + return { + type: "number", + value: boolToInt(optimisedA.value === optimisedB.value), + }; + } else if (input.type === "ne") { + return { + type: "number", + value: boolToInt(optimisedA.value !== optimisedB.value), + }; + } else if (input.type === "min") { + return { + type: "number", + value: Math.min(optimisedA.value, optimisedB.value), + }; + } else if (input.type === "max") { + return { + type: "number", + value: Math.max(optimisedA.value, optimisedB.value), + }; + } + } + + return { + ...input, + valueA: optimisedA, + valueB: optimisedB, + }; + } + return input; +}; + +const walkScriptValue = ( + input: ScriptValue, + fn: (val: ScriptValue) => void +): void => { + fn(input); + if ("valueA" in input && input.valueA) { + walkScriptValue(input.valueA, fn); + } + if ("valueB" in input && input.valueB) { + walkScriptValue(input.valueB, fn); + } +}; + +export const mapScriptValueLeafNodes = ( + input: ScriptValue, + fn: (val: ScriptValue) => ScriptValue +): ScriptValue => { + if ("valueA" in input && input.type !== "rnd") { + const mappedA = input.valueA && mapScriptValueLeafNodes(input.valueA, fn); + const mappedB = input.valueB && mapScriptValueLeafNodes(input.valueB, fn); + return { + ...input, + valueA: mappedA, + valueB: mappedB, + }; + } + + if (!isValueOperation(input) && input.type !== "rnd") { + return fn(input); + } + + return input; +}; + +export const extractScriptValueActorIds = (input: ScriptValue): string[] => { + const actorIds: string[] = []; + walkScriptValue(input, (val) => { + if (val.type === "property" && !actorIds.includes(val.target)) { + actorIds.push(val.target); + } + }); + return actorIds; +}; + +export const extractScriptValueVariables = (input: ScriptValue): string[] => { + const variables: string[] = []; + walkScriptValue(input, (val) => { + if (val.type === "variable" && !variables.includes(val.value)) { + variables.push(val.value); + } else if (val.type === "expression") { + const text = val.value; + if (text && typeof text === "string") { + const variablePtrs = text.match(/\$V[0-9]\$/g); + if (variablePtrs) { + variablePtrs.forEach((variablePtr: string) => { + const variable = variablePtr[2]; + const variableId = `V${variable}`; + if (!variables.includes(variableId)) { + variables.push(variableId); + } + }); + } + } + } + }); + return variables; +}; + +export const precompileScriptValue = ( + input: ScriptValue, + localsLabel = "", + rpnOperations: PrecompiledValueRPNOperation[] = [], + fetchOperations: PrecompiledValueFetch[] = [] +): [PrecompiledValueRPNOperation[], PrecompiledValueFetch[]] => { + if ( + input.type === "property" || + input.type === "expression" || + input.type === "rnd" + ) { + const localName = `local_${localsLabel}${fetchOperations.length}`; + rpnOperations.push({ + type: "local", + value: localName, + }); + fetchOperations.push({ + local: localName, + value: input, + }); + } else if (isValueOperation(input)) { + if (input.valueA) { + precompileScriptValue( + input.valueA, + localsLabel, + rpnOperations, + fetchOperations + ); + } + if (input.valueB) { + precompileScriptValue( + input.valueB, + localsLabel, + rpnOperations, + fetchOperations + ); + } + rpnOperations.push({ + type: input.type, + }); + } else { + rpnOperations.push(input); + } + return [rpnOperations, fetchOperations]; +}; + +export const sortFetchOperations = ( + input: PrecompiledValueFetch[] +): PrecompiledValueFetch[] => { + return [...input].sort((a, b) => { + if (a.value.type === "property" && b.value.type === "property") { + if (a.value.target === b.value.target) { + // Sort on Prop + return a.value.property.localeCompare(b.value.property); + } else { + // Sort on Target + return a.value.target.localeCompare(b.value.target); + } + } + return a.value.type.localeCompare(b.value.type); + }); +}; + +export const multiplyScriptValueConst = ( + value: ScriptValue, + num: number +): ScriptValue => { + return { + type: "mul", + valueA: value, + valueB: { + type: "number", + value: num, + }, + }; +}; diff --git a/src/shared/lib/scriptValue/types.ts b/src/shared/lib/scriptValue/types.ts new file mode 100644 index 000000000..6266fe54a --- /dev/null +++ b/src/shared/lib/scriptValue/types.ts @@ -0,0 +1,284 @@ +export type RPNOperation = + | { + type: "add"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "sub"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "mul"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "div"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "eq"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "ne"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "gt"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "gte"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "lt"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "lte"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "min"; + valueA?: ScriptValue; + valueB?: ScriptValue; + } + | { + type: "max"; + valueA?: ScriptValue; + valueB?: ScriptValue; + }; + +export type ScriptValueAtom = + | { + type: "number"; + value: number; + } + | { + type: "variable"; + value: string; + } + | { + type: "direction"; + value: string; + } + | { + type: "indirect"; + value: string; + } + | { + type: "property"; + target: string; + property: string; + } + | { + type: "expression"; + value: string; + }; + +export type ScriptValue = + | RPNOperation + | ScriptValueAtom + | { + type: "rnd"; + valueA?: { + type: "number"; + value: number; + }; + valueB?: { + type: "number"; + value: number; + }; + }; + +export const valueFunctions = [ + "add", + "sub", + "mul", + "div", + "min", + "max", + "eq", + "ne", + "gt", + "gte", + "lt", + "lte", +] as const; +export type ValueFunction = typeof valueFunctions[number]; + +export const valueAtoms = [ + "number", + "direction", + "variable", + "indirect", + "property", + "expression", +] as const; +export type ValueAtom = typeof valueAtoms[number]; + +export type ValueFunctionMenuItem = { + value: ValueFunction; + label: string; + symbol: string; +}; + +export const isScriptValue = (value: unknown): value is ScriptValue => { + if (!value || typeof value !== "object") { + return false; + } + const scriptValue = value as ScriptValue; + // Is a number + if (scriptValue.type === "number" && typeof scriptValue.value === "number") { + return true; + } + if ( + scriptValue.type === "variable" && + typeof scriptValue.value === "string" + ) { + return true; + } + if ( + scriptValue.type === "property" && + typeof scriptValue.target === "string" && + typeof scriptValue.property === "string" + ) { + return true; + } + if ( + scriptValue.type === "expression" && + typeof scriptValue.value === "string" + ) { + return true; + } + if ( + scriptValue.type === "direction" && + typeof scriptValue.value === "string" + ) { + return true; + } + if ( + isValueOperation(scriptValue) && + (isScriptValue(scriptValue.valueA) || !scriptValue.valueA) && + (isScriptValue(scriptValue.valueB) || !scriptValue.valueB) + ) { + return true; + } + if ( + scriptValue.type === "rnd" && + (isScriptValue(scriptValue.valueA) || !scriptValue.valueA) && + (isScriptValue(scriptValue.valueB) || !scriptValue.valueB) + ) { + return true; + } + + return false; +}; + +export type ScriptValueFunction = ScriptValue & { type: ValueFunction }; + +export const isValueOperation = ( + value?: ScriptValue +): value is ScriptValueFunction => { + return ( + !!value && valueFunctions.includes(value.type as unknown as ValueFunction) + ); +}; + +export const isValueAtom = (value?: ScriptValue): value is ScriptValueAtom => { + return !!value && valueAtoms.includes(value.type as unknown as ValueAtom); +}; + +export type PrecompiledValueFetch = { + local: string; + value: + | { + type: "property"; + target: string; + property: string; + } + | { + type: "expression"; + value: string; + } + | { + type: "rnd"; + valueA?: { + type: "number"; + value: number; + }; + valueB?: { + type: "number"; + value: number; + }; + }; +}; + +export type PrecompiledValueRPNOperation = + | { + type: "number"; + value: number; + } + | { + type: "variable"; + value: string; + } + | { + type: "direction"; + value: string; + } + | { + type: "indirect"; + value: string; + } + | { + type: "local"; + value: string; + } + | { + type: "add"; + } + | { + type: "sub"; + } + | { + type: "mul"; + } + | { + type: "div"; + } + | { + type: "eq"; + } + | { + type: "ne"; + } + | { + type: "gt"; + } + | { + type: "gte"; + } + | { + type: "lt"; + } + | { + type: "lte"; + } + | { + type: "min"; + } + | { + type: "max"; + }; diff --git a/src/shared/lib/scripts/autoLabel.ts b/src/shared/lib/scripts/autoLabel.ts index 3494b504a..f9f62737f 100644 --- a/src/shared/lib/scripts/autoLabel.ts +++ b/src/shared/lib/scripts/autoLabel.ts @@ -5,6 +5,8 @@ import { } from "shared/lib/scripts/scriptDefHelpers"; import l10n from "shared/lib/lang/l10n"; import type { ScriptEventHandlers } from "lib/project/loadScriptEventHandlers"; +import { scriptValueToString } from "shared/lib/scriptValue/format"; +import { isScriptValue } from "shared/lib/scriptValue/types"; export const getAutoLabel = ( command: string, @@ -26,8 +28,10 @@ export const getAutoLabel = ( const fieldLookup = field.fieldsLookup; - const extractValue = (arg: unknown): unknown => { + const extractValue = (key: string, arg: unknown): unknown => { + const fieldType = fieldLookup[key]?.type || ""; if ( + fieldType === "union" && arg && typeof arg === "object" && "value" in (arg as { value: unknown }) @@ -46,7 +50,7 @@ export const getAutoLabel = ( return fieldType; }; - const argValue = extractValue(arg); + const argValue = extractValue(key, arg); const fieldType = extractFieldType(key, arg); const fieldDefault = arg && (arg as { type: string })?.type @@ -141,7 +145,14 @@ export const getAutoLabel = ( return l10nInput(value); }; - if (isActorField(command, key, args, scriptEventDefs)) { + if (fieldType === "value" && isScriptValue(value)) { + return scriptValueToString(value, { + variableNameForId: (id) => `||variable:${id}||`, + actorNameForId: (id) => `||actor:${id}||`, + propertyNameForId, + directionForValue, + }); + } else if (isActorField(command, key, args, scriptEventDefs)) { return `||actor:${value}||`; } else if (isVariableField(command, key, args, scriptEventDefs)) { return `||variable:${value}||`; diff --git a/src/shared/lib/scripts/scriptDefHelpers.ts b/src/shared/lib/scripts/scriptDefHelpers.ts index 2a93b7973..b35e76791 100644 --- a/src/shared/lib/scripts/scriptDefHelpers.ts +++ b/src/shared/lib/scripts/scriptDefHelpers.ts @@ -94,3 +94,15 @@ export const isPropertyField = ( isFieldVisible(field, args) ); }; + +export const isScriptValueField = ( + cmd: string, + fieldName: string, + args: ScriptEventArgs, + scriptEventDefs: ScriptEventDefs +) => { + const event = scriptEventDefs[cmd]; + if (!event) return false; + const field = getField(cmd, fieldName, scriptEventDefs); + return field && field.type === "value" && isFieldVisible(field, args); +}; diff --git a/src/store/features/entities/entitiesState.ts b/src/store/features/entities/entitiesState.ts index 81c1583e3..5b1ec1f85 100644 --- a/src/store/features/entities/entitiesState.ts +++ b/src/store/features/entities/entitiesState.ts @@ -34,6 +34,7 @@ import { isVariableField, isPropertyField, ScriptEventDefs, + isScriptValueField, } from "shared/lib/scripts/scriptDefHelpers"; import clamp from "shared/lib/helpers/clamp"; import { RootState } from "store/configureStore"; @@ -95,6 +96,11 @@ import { import spriteActions from "store/features/sprite/spriteActions"; import { sortByKey } from "shared/lib/helpers/sortByKey"; import { walkNormalizedScript } from "shared/lib/scripts/walk"; +import { + extractScriptValueActorIds, + extractScriptValueVariables, +} from "shared/lib/scriptValue/helpers"; +import { isScriptValue, ScriptValue } from "shared/lib/scriptValue/types"; const MIN_SCENE_X = 60; const MIN_SCENE_Y = 30; @@ -2424,8 +2430,28 @@ const refreshCustomEventArgs: CaseReducer< if (!args) return; if (args.__comment) return; Object.keys(args).forEach((arg) => { - if (isActorField(scriptEvent.command, arg, args, scriptEventDefs)) { - const addActor = (actor: string) => { + const addActor = (actor: string) => { + const letter = String.fromCharCode( + "A".charCodeAt(0) + parseInt(actor) + ); + actors[actor] = { + id: actor, + name: oldActors[actor]?.name || `Actor ${letter}`, + }; + }; + const addVariable = (variable: string) => { + const letter = String.fromCharCode( + "A".charCodeAt(0) + parseInt(variable[1]) + ); + variables[variable] = { + id: variable, + name: oldVariables[variable]?.name || `Variable ${letter}`, + passByReference: oldVariables[variable]?.passByReference ?? true, + }; + }; + const addPropertyActor = (property: string) => { + const actor = property && property.replace(/:.*/, ""); + if (actor !== "player" && actor !== "$self$") { const letter = String.fromCharCode( "A".charCodeAt(0) + parseInt(actor) ); @@ -2433,7 +2459,10 @@ const refreshCustomEventArgs: CaseReducer< id: actor, name: oldActors[actor]?.name || `Actor ${letter}`, }; - }; + } + }; + + if (isActorField(scriptEvent.command, arg, args, scriptEventDefs)) { const actor = args[arg]; if ( actor && @@ -2444,17 +2473,8 @@ const refreshCustomEventArgs: CaseReducer< addActor(actor); } } + if (isVariableField(scriptEvent.command, arg, args, scriptEventDefs)) { - const addVariable = (variable: string) => { - const letter = String.fromCharCode( - "A".charCodeAt(0) + parseInt(variable[1]) - ); - variables[variable] = { - id: variable, - name: oldVariables[variable]?.name || `Variable ${letter}`, - passByReference: oldVariables[variable]?.passByReference ?? true, - }; - }; const variable = args[arg]; if ( isUnionVariableValue(variable) && @@ -2470,18 +2490,6 @@ const refreshCustomEventArgs: CaseReducer< } } if (isPropertyField(scriptEvent.command, arg, args, scriptEventDefs)) { - const addPropertyActor = (property: string) => { - const actor = property && property.replace(/:.*/, ""); - if (actor !== "player" && actor !== "$self$") { - const letter = String.fromCharCode( - "A".charCodeAt(0) + parseInt(actor) - ); - actors[actor] = { - id: actor, - name: oldActors[actor]?.name || `Actor ${letter}`, - }; - } - }; const property = args[arg]; if (isUnionPropertyValue(property) && property.value) { addPropertyActor(property.value); @@ -2489,6 +2497,23 @@ const refreshCustomEventArgs: CaseReducer< addPropertyActor(property); } } + if ( + isScriptValueField(scriptEvent.command, arg, args, scriptEventDefs) + ) { + const value = isScriptValue(args[arg]) + ? (args[arg] as ScriptValue) + : undefined; + const actors = value ? extractScriptValueActorIds(value) : []; + const variables = value ? extractScriptValueVariables(value) : []; + for (const actor of actors) { + addPropertyActor(actor); + } + for (const variable of variables) { + if (isVariableCustomEvent(variable)) { + addVariable(variable); + } + } + } }); if (args.text || args.expression) { let text; diff --git a/test/scriptValue/helpers.test.ts b/test/scriptValue/helpers.test.ts new file mode 100644 index 000000000..c93176f74 --- /dev/null +++ b/test/scriptValue/helpers.test.ts @@ -0,0 +1,533 @@ +import { ScriptValue } from "../../src/shared/lib/scriptValue/types"; +import { + optimiseScriptValue, + precompileScriptValue, +} from "../../src/shared/lib/scriptValue/helpers"; + +test("should perform constant folding for addition", () => { + const input: ScriptValue = { + type: "add", + valueA: { + type: "number", + value: 5, + }, + valueB: { + type: "number", + value: 3, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 8, + }); +}); + +test("should perform constant folding for subtraction", () => { + const input: ScriptValue = { + type: "sub", + valueA: { + type: "number", + value: 5, + }, + valueB: { + type: "number", + value: 3, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 2, + }); +}); + +test("should perform constant folding for multiplication", () => { + const input: ScriptValue = { + type: "mul", + valueA: { + type: "number", + value: 5, + }, + valueB: { + type: "number", + value: 3, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 15, + }); +}); + +test("should perform constant folding for division", () => { + const input: ScriptValue = { + type: "div", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 3, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 7, + }); +}); + +test("should round down to nearest int when constant folding for division", () => { + const input: ScriptValue = { + type: "div", + valueA: { + type: "number", + value: 14, + }, + valueB: { + type: "number", + value: 3, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 4, + }); +}); + +test("should perform constant folding for greater than", () => { + const input: ScriptValue = { + type: "gt", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 3, + }, + }; + const input2: ScriptValue = { + type: "gt", + valueA: { + type: "number", + value: 3, + }, + valueB: { + type: "number", + value: 21, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 1, + }); + expect(optimiseScriptValue(input2)).toEqual({ + type: "number", + value: 0, + }); +}); + +test("should perform constant folding for greater than or equal to", () => { + const input: ScriptValue = { + type: "gte", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 3, + }, + }; + const input2: ScriptValue = { + type: "gte", + valueA: { + type: "number", + value: 3, + }, + valueB: { + type: "number", + value: 21, + }, + }; + const input3: ScriptValue = { + type: "gte", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 21, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 1, + }); + expect(optimiseScriptValue(input2)).toEqual({ + type: "number", + value: 0, + }); + expect(optimiseScriptValue(input3)).toEqual({ + type: "number", + value: 1, + }); +}); + +test("should perform constant folding for less than", () => { + const input: ScriptValue = { + type: "lt", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 3, + }, + }; + const input2: ScriptValue = { + type: "lt", + valueA: { + type: "number", + value: 3, + }, + valueB: { + type: "number", + value: 21, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 0, + }); + expect(optimiseScriptValue(input2)).toEqual({ + type: "number", + value: 1, + }); +}); + +test("should perform constant folding for less than or equal to", () => { + const input: ScriptValue = { + type: "lte", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 3, + }, + }; + const input2: ScriptValue = { + type: "lte", + valueA: { + type: "number", + value: 3, + }, + valueB: { + type: "number", + value: 21, + }, + }; + const input3: ScriptValue = { + type: "lte", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 21, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 0, + }); + expect(optimiseScriptValue(input2)).toEqual({ + type: "number", + value: 1, + }); + expect(optimiseScriptValue(input3)).toEqual({ + type: "number", + value: 1, + }); +}); + +test("should perform constant folding for equal to", () => { + const input: ScriptValue = { + type: "eq", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 21, + }, + }; + const input2: ScriptValue = { + type: "eq", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 20, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 1, + }); + expect(optimiseScriptValue(input2)).toEqual({ + type: "number", + value: 0, + }); +}); + +test("should perform constant folding for not equal to", () => { + const input: ScriptValue = { + type: "ne", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 21, + }, + }; + const input2: ScriptValue = { + type: "ne", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 20, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 0, + }); + expect(optimiseScriptValue(input2)).toEqual({ + type: "number", + value: 1, + }); +}); + +test("should perform constant folding for min", () => { + const input: ScriptValue = { + type: "min", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 3, + }, + }; + const input2: ScriptValue = { + type: "min", + valueA: { + type: "number", + value: 3, + }, + valueB: { + type: "number", + value: 21, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 3, + }); + expect(optimiseScriptValue(input2)).toEqual({ + type: "number", + value: 3, + }); +}); + +test("should perform constant folding for max", () => { + const input: ScriptValue = { + type: "max", + valueA: { + type: "number", + value: 21, + }, + valueB: { + type: "number", + value: 3, + }, + }; + const input2: ScriptValue = { + type: "max", + valueA: { + type: "number", + value: 3, + }, + valueB: { + type: "number", + value: 21, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 21, + }); + expect(optimiseScriptValue(input2)).toEqual({ + type: "number", + value: 21, + }); +}); + +test("should perform constant folding for nested input", () => { + const input: ScriptValue = { + type: "mul", + valueA: { + type: "sub", + valueA: { + type: "number", + value: 5, + }, + valueB: { + type: "number", + value: 3, + }, + }, + valueB: { + type: "add", + valueA: { + type: "number", + value: 5, + }, + valueB: { + type: "min", + valueA: { + type: "number", + value: 3, + }, + valueB: { + type: "number", + value: 6, + }, + }, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 16, + }); +}); + +test("should replace missing values with 0", () => { + const input: ScriptValue = { + type: "add", + valueA: { + type: "variable", + value: "5", + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "add", + valueA: { + type: "variable", + value: "5", + }, + valueB: { + type: "number", + value: 0, + }, + }); +}); + +test("should replace missing values with 0 and collapse where possible", () => { + const input: ScriptValue = { + type: "add", + valueA: { + type: "number", + value: 5, + }, + }; + expect(optimiseScriptValue(input)).toEqual({ + type: "number", + value: 5, + }); +}); + +test("should precompile to list of required operations", () => { + const input: ScriptValue = { + type: "add", + valueA: { + type: "variable", + value: "L0", + }, + valueB: { + type: "variable", + value: "L0", + }, + }; + expect(precompileScriptValue(input)).toEqual([ + [ + { + type: "variable", + value: "L0", + }, + { + type: "variable", + value: "L0", + }, + { + type: "add", + }, + ], + [], + ]); +}); + +test("should precompile to list of required operations", () => { + const input: ScriptValue = { + type: "add", + valueA: { + type: "variable", + value: "L0", + }, + valueB: { + type: "property", + target: "player", + property: "xpos", + }, + }; + expect(precompileScriptValue(input)).toEqual([ + [ + { + type: "variable", + value: "L0", + }, + { + type: "local", + value: "local_0", + }, + { + type: "add", + }, + ], + [ + { + local: "local_0", + value: { + type: "property", + target: "player", + property: "xpos", + }, + }, + ], + ]); +});