From 8797d580244a105dffd42d033beef6e43dfed20e Mon Sep 17 00:00:00 2001 From: Hardy--Lee Date: Mon, 6 Jul 2026 07:00:00 +0800 Subject: [PATCH] fix: expression evaluation --- .../gamePlay/expressionEvaluation.ts | 80 +++++++++++++++++++ .../controller/gamePlay/scriptExecutor.ts | 12 ++- .../webgal/src/Core/gameScripts/setVar.ts | 48 ++++++++--- .../util/coreInitialFunction/infoFetcher.ts | 3 + packages/webgal/src/Core/webgalCore.ts | 1 + 5 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 packages/webgal/src/Core/controller/gamePlay/expressionEvaluation.ts diff --git a/packages/webgal/src/Core/controller/gamePlay/expressionEvaluation.ts b/packages/webgal/src/Core/controller/gamePlay/expressionEvaluation.ts new file mode 100644 index 000000000..fb6dacc79 --- /dev/null +++ b/packages/webgal/src/Core/controller/gamePlay/expressionEvaluation.ts @@ -0,0 +1,80 @@ +import expression from 'angular-expressions'; +import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; +import { webgalStore } from '@/store/store'; +import { logger } from '@/Core/util/logger'; +import random from 'lodash/random'; + +/** + * 提取变量名和表达式 + * @param expressionString 表达式字符串,例如 "x = a + 5" + * @returns 包含变量名和表达式,如果无效则返回 undefined + */ +export function extractVariableNameAndExpression( + expressionString: string, +): { variableName: string; expression: string } | undefined { + const equalIndex = expressionString.indexOf('='); + if (equalIndex === -1) { + return undefined; + } + + const variableName = expressionString.substring(0, equalIndex).trim(); + if (variableName.length === 0) { + return undefined; + } + + const expression = expressionString.substring(equalIndex + 1).trim(); + if (expression.length === 0) { + return undefined; + } + + return { variableName, expression }; +} + +// 内置函数 +const builtinFunctions = { + random: (...args: any[]) => { + return args.length ? random(...args) : Math.random(); + }, +}; + +/** + * 评估表达式字符串 + * @param expressionString 表达式字符串 + * @returns 评估结果,可能是 string、number、boolean 或 undefined + * 但也有可能返回其他类型,取决于表达式的内容和上下文 + */ +export function evaluateExpression(expressionString: string): string | number | boolean | undefined { + try { + const evaluate = expression.compile(expressionString); + + const stageState = stageStateManager.getCalculationStageState(); + const stageVar = stageState.GameVar; + const userData = webgalStore.getState().userData; + const globalVar = userData.globalGameVar; + + const scope: any = {}; + // 先加入内置函数(最低优先级) + Object.assign(scope, builtinFunctions); + // 然后加入全局变量 + Object.assign(scope, globalVar); + // 最后加入舞台变量以保证最高优先级 + Object.assign(scope, stageVar); + // 支持 $ 前缀的特殊值 + scope['$stage'] = stageState; + scope['$userData'] = userData; + + const result = evaluate(scope); + switch (typeof result) { + case 'string': + case 'number': + case 'boolean': + return result; + default: + logger.warn(`不支持的表达式求值类型: ${typeof result},表达式: ${expressionString}`); + return undefined; + } + } catch (error) { + logger.warn(`表达式求值失败: ${expressionString}`, error); + return undefined; + } +} diff --git a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts index 718e4a4e8..d8dd40451 100644 --- a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts +++ b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts @@ -3,7 +3,7 @@ import { runScript } from './runScript'; import { logger } from '../../util/logger'; import { restoreScene } from '../scene/restoreScene'; import { webgalStore } from '@/store/store'; -import { getValueFromStateElseKey } from '@/Core/gameScripts/setVar'; +import { legacyGetValueFromStateElseKey } from '@/Core/gameScripts/setVar'; import { strIf } from '@/Core/controller/gamePlay/strIf'; import cloneDeep from 'lodash/cloneDeep'; import { ISceneEntry } from '@/Core/Modules/scene'; @@ -13,6 +13,7 @@ import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; import { jumpToLabel } from '@/Core/gameScripts/label/jumpToLabel'; import { prefetchCurrentSceneByProgress } from '@/Core/util/prefetcher/progressPrefetcher'; import { WEBGAL_NONE } from '@/Core/constants'; +import { evaluateExpression } from '@/Core/controller/gamePlay/expressionEvaluation'; const MAX_FORWARD_SCRIPT_EXECUTION = 1000; @@ -29,6 +30,11 @@ export const whenChecker = (whenValue: string | undefined): boolean => { if (whenValue === undefined) { return true; } + + if (!WebGAL.legacyExpressionParser) { + return Boolean(evaluateExpression(whenValue)); + } + // 先把变量解析出来 const valExpArr = whenValue.split(/([+\-*\/()>=|<=|==|&&|\|\||!=)/g); const valExp = valExpArr @@ -38,7 +44,7 @@ export const whenChecker = (whenValue: string | undefined): boolean => { if (e.match(/^(true|false)$/)) { return e; } - return getValueFromStateElseKey(e, true, true); + return legacyGetValueFromStateElseKey(e, true, true); } else return e; }) .reduce((pre, curr) => pre + curr, ''); @@ -84,7 +90,7 @@ export const scriptExecutor = (depth = 0, options: ScriptExecutionOptions = {}) if (contentExp !== null) { contentExp.forEach((e) => { - const contentVarValue = getValueFromStateElseKey(e.replace(/(? { const setGlobal = getBooleanArgByKey(sentence, 'global') ?? false; - if (sentence.content.match(/\s*=\s*/)) { - const key = sentence.content.split(/\s*=\s*/)[0]; - const valExp = sentence.content.split(/\s*=\s*/)[1]; - setGameVarFromExpression({ key, value: valExp, isGlobal: setGlobal }); + if (WebGAL.legacyExpressionParser) { + if (sentence.content.match(/\s*=\s*/)) { + const key = sentence.content.split(/\s*=\s*/)[0]; + const valExp = sentence.content.split(/\s*=\s*/)[1]; + setGameVarFromExpression({ key, value: valExp, isGlobal: setGlobal }); + } + } else { + const extracted = extractVariableNameAndExpression(sentence.content); + if (extracted) { + const { variableName, expression } = extracted; + setGameVarFromExpression({ key: variableName, value: expression, isGlobal: setGlobal }); + } else { + logger.error(`setVar 语句格式错误,无法提取变量名和表达式: ${sentence.content}`); + } } return createNonePerform(); }; type BaseVal = string | number | boolean | undefined; -export function resolveSetVarValue(valExp: string): string | boolean | number { +export function resolveSetVarValue(valExp: string): string | boolean | number | undefined { + if (!WebGAL.legacyExpressionParser) { + return evaluateExpression(valExp); + } + if (/^\s*[a-zA-Z_$][\w$]*\s*\(.*\)\s*$/.test(valExp)) { - return EvaluateExpression(valExp); + return LegacyEvaluateExpression(valExp); } else if (valExp.match(/[+\-*\/()]/)) { const valExpArr = valExp.split(/([+\-*\/()])/g); const valExp2 = valExpArr @@ -77,7 +99,7 @@ export function resolveSetVarValue(valExp: string): string | boolean | number { if (!e.trim().match(/^[a-zA-Z_$][a-zA-Z0-9_.]*$/)) { return e; } - const _r = getValueFromStateElseKey(e.trim(), true); + const _r = legacyGetValueFromStateElseKey(e.trim(), true); return typeof _r === 'string' ? `'${_r}'` : _r; }) .reduce((pre, curr) => pre + curr, ''); @@ -102,7 +124,7 @@ export function resolveSetVarValue(valExp: string): string | boolean | number { if (!isNaN(Number(valExp))) { return Number(valExp); } else { - return getValueFromStateElseKey(valExp, true) ?? ''; + return legacyGetValueFromStateElseKey(valExp, true) ?? ''; } } return ''; @@ -111,7 +133,7 @@ export function resolveSetVarValue(valExp: string): string | boolean | number { /** * 执行函数 */ -function EvaluateExpression(val: string) { +function LegacyEvaluateExpression(val: string) { const instance = expression.compile(val); return instance({ random: (...args: any[]) => { @@ -123,7 +145,7 @@ function EvaluateExpression(val: string) { /** * 取不到时返回 undefined */ -export function getValueFromState(key: string) { +export function legacyGetValueFromState(key: string) { let ret: any; const stage = stageStateManager.getCalculationStageState(); const userData = webgalStore.getState().userData; @@ -142,8 +164,8 @@ export function getValueFromState(key: string) { /** * 取不到时返回 {key} */ -export function getValueFromStateElseKey(key: string, useKeyNameAsReturn = false, quoteString = false) { - const valueFromState = getValueFromState(key); +export function legacyGetValueFromStateElseKey(key: string, useKeyNameAsReturn = false, quoteString = false) { + const valueFromState = legacyGetValueFromState(key); if (valueFromState === null || valueFromState === undefined) { logger.warn('valueFromState result null, key = ' + key); if (useKeyNameAsReturn) { diff --git a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts index 299e34218..ca9f0fd37 100644 --- a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts +++ b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts @@ -67,6 +67,9 @@ export const infoFetcher = (url: string): Promise => { const appId = String(res); WebGAL.steam.initialize(appId); } + if (command === 'Legacy_Expression_Parser') { + WebGAL.legacyExpressionParser = res === true; + } } } }); diff --git a/packages/webgal/src/Core/webgalCore.ts b/packages/webgal/src/Core/webgalCore.ts index 88320c516..d3ecd5376 100644 --- a/packages/webgal/src/Core/webgalCore.ts +++ b/packages/webgal/src/Core/webgalCore.ts @@ -25,4 +25,5 @@ export class WebgalCore { public steam = new SteamIntegration(); public template: WebgalTemplate | null = null; public styleObjects: Map = new Map(); + public legacyExpressionParser = false; }