import { HZEngineCore } from "../index.js"; import { HzsInfo, Storage } from "../storage/index.js"; import { mergeObjs2Str as joinObjs2Str, parseInterpolatedStr, removeComment, splitStr2Objs, } from "./strtools.js"; import { readline } from "./readscript.js"; import { Save } from "../storage/decorator.js"; export class Script { constructor(public _core: HZEngineCore) {} /** * 调用栈 * 在call时保存当前执行位置和语句栈,在return时恢复执行位置和语句栈 */ @Save("script.callStack") private accessor _callStack: { position: [path: string, index: number] | null; statementStack: Script.StatementStack; }[] = []; /** * 语句栈 * 比如while, if,会在语句开始时入栈,语句结束时出栈 */ @Save("script.statementStack") private accessor _statementStack: Script.StatementStack = []; /** * 下一次执行的脚本位置 * 注意:存储该值的时候应总是拷贝赋值而非直接引用赋值 */ @Save("script.nextRunPosition") private accessor _nextRunPosition: [path: string, index: number] | null = null; public get nextRunPosition() { return this._nextRunPosition; } /** * 当前正在执行的命令内容 */ private _currentRawCommand: string = "-"; public get currentRawCommand() { return this._currentRawCommand; } // Script Run /** * 执行_nextRunPosition,并返回下一行_nextRunPosition是否不为null */ runSingleLine(): boolean { if (!this._nextRunPosition) { // 文件尾隐式执行 return this.return(); return false; } let nowRunPosition: [path: string, index: number] = [ ...this._nextRunPosition, ]; this.incrementNextPosition(); let rawCommand = readline( this._core, nowRunPosition[0], nowRunPosition[1] )!; // remove comment rawCommand = removeComment(rawCommand); this._currentRawCommand = rawCommand.trim(); if (rawCommand.trim().length && !rawCommand.trim().startsWith("#")) { if (rawCommand.trim().startsWith("*")) { if (this._statementStack.length) { throw `label between statement is not allowed, at file [${nowRunPosition[0]}] line [${nowRunPosition[1]}]`; } } else { this._core.debug.log("Run cmd: " + rawCommand); // Process Command this._processCmd(rawCommand, [...nowRunPosition]); } } return !!this._nextRunPosition; } // Label Point Control /** * 跳转到目标标签 * @param targetLabel */ jumpLabel(targetLabel: string) { let labelPosition = this._locateLabel(targetLabel); this._nextRunPosition = labelPosition; this._statementStack = []; } jump(path: string, index: number, clearStatementStack: boolean = false) { this._nextRunPosition = [path, index]; if (clearStatementStack) { this._statementStack = []; } } /** * 调用目标标签 * 保存当前执行位置至调用栈,跳转到目标位置,直到return返回 * @param targetLabel */ callLabel(targetLabel: string) { // console.log( // `pending to call label ${targetLabel}, stack=${JSON.stringify( // this._routeStack // )}` // ); let labelPosition = this._locateLabel(targetLabel); this._callStack.push({ position: this._nextRunPosition ? [...this._nextRunPosition] : null, statementStack: this._statementStack, }); this._nextRunPosition = labelPosition; this._statementStack = []; // console.log( // `finished call label ${targetLabel}, stack=${JSON.stringify( // this._routeStack // )}` // ); } hasLabel(targetLabel: string) { return ( this._core.storage.preloadedData?.script?.labelMap?.[targetLabel] != null ); } return() { // console.log(`pending to return, stack=${JSON.stringify(this._routeStack)}`); let stackItem = this._callStack.pop(); if (stackItem) { this._statementStack = stackItem.statementStack; this._nextRunPosition = stackItem.position; } else { this._nextRunPosition = null; this._statementStack = []; // Game End this._core.end(); } // console.log(`finished return, stack=${JSON.stringify(this._routeStack)}`); } clear() { this._nextRunPosition = null; this._callStack = []; } private _locateLabel(labelName: string) { if (this._core.storage.preloadedData == null) throw "Preloaded Data is Null"; if (!this._core.storage.preloadedData.script.labelMap[labelName]) throw `Error: Label [${labelName}] not found`; let labelData: [name: string, index: number] = [ ...this._core.storage.preloadedData.script.labelMap[labelName], ] as any; return labelData; } public incrementNextPosition() { if (this._core.storage.preloadedData == null) throw "Preloaded Data is Null"; if (!this._nextRunPosition) throw "_nextRunPosition is null"; // console.log(`hzsInfoMap=${JSON.stringify(this._core.storage.preloadedData.script.hzsInfoMap)}`); let hzsInfo: HzsInfo | undefined = this._core.storage.preloadedData.script.hzsInfoMap[ this._nextRunPosition[0] ]; if (!hzsInfo) throw `Preloaded hzsInfo of path(${this._nextRunPosition[0]}) not found`; this._nextRunPosition[1]++; if (this._nextRunPosition[1] >= hzsInfo.totalLines) this._nextRunPosition = null; } // Middleware private _middlewares: Script.Middleware[] = []; use(middleware: Script.Middleware, add_front: boolean = false) { if (add_front) { this._middlewares.unshift(middleware); } else { this._middlewares.push(middleware); } } private _processCmd( cmd: string, nowRunPosition: [path: string, index: number] ) { let ctx = this._buildContext(cmd, nowRunPosition); if (this._middlewares.length === 0) { this._processUnsolvedCmd(cmd); return; } let i = 0, len = this._middlewares.length; let nextFunc = () => { i++; if (i >= len) { this._processUnsolvedCmd(cmd); return; } this._middlewares[i](ctx, nextFunc); }; this._middlewares[0](ctx, nextFunc); } private _processUnsolvedCmd(cmd: string) { if (cmd.trim().length === 0) return; // Empty Line else if (cmd.trim().startsWith("*")) return; // Label Command else { throw `Can not parse command: ${cmd}`; } } private _buildContext( cmd: string, nowRunPosition: [path: string, index: number] ): Script.Context { return new Script.Context( this._core, cmd, nowRunPosition[0], nowRunPosition[1], this._statementStack ); } // Statement Analyse private _statementAnalyseStack: Script.StatementStack = []; private _analyseStatementMiddlewares: Script.Middleware[] = []; private _buildAnalyseStatementContext( cmd: string, nowRunPosition: [path: string, index: number] ) { return new Script.ContextForAnalyseStatement( this._core, cmd, nowRunPosition[0], nowRunPosition[1], this._statementAnalyseStack ); } useAnalyseStatement( middleware: Script.MiddlewareForAnalyseStatement, add_front?: boolean ) { if (add_front) { this._analyseStatementMiddlewares.unshift(middleware); } else { this._analyseStatementMiddlewares.push(middleware); } } /** * 分析statement * * Analyze the statement syntax and record the script point location and related information in advance. * When the script executes a statement, the regular middleware corresponding to that statement will load * the information saved by analyseStatement before the statement, and if there is no analysis, it will call * analyseStatement to analyze, and the corresponding analysis middleware will process and save the information. * After analysis is complete, reset _nextRunPosition to the location before the call, switch back to the normal mode, * and continue executing. */ analyseStatement(ctx: Script.Context) { // this._core.debug.log("[HZEngine] Start statement analyse mode"); // Backup _nextRunPosition let _nextRunPositionBackup: [path: string, index: number] | null = this ._nextRunPosition ? [...this._nextRunPosition] : null; let covered: boolean = false; // Set _nextRunPosition to the current position of the statement this._nextRunPosition = [ctx.currentPath, ctx.currentLineIndex]; while (this._nextRunPosition) { let rawCommand = readline( this._core, this._nextRunPosition[0], this._nextRunPosition[1] ); if (rawCommand == null) throw `Readline Error(got ${rawCommand}), at file [${ this._nextRunPosition[0] }] line [${this._nextRunPosition[1] + 1}]`; // If the command is not empty and not a comment if (rawCommand.trim().length && !rawCommand.trim().startsWith("#")) { // If it is a label command, check if the statement stack is empty if (rawCommand.trim().startsWith("*")) { if (this._statementStack.length) { throw `label between statement is not allowed, at file [${ this._nextRunPosition[0] }] line [${this._nextRunPosition[1] + 1}]`; } } else { // Build context for analyzing statement let sub_ctx = this._buildAnalyseStatementContext(rawCommand, [ ...this._nextRunPosition, ]); // console.log(`[HZEngine] analyse statement command [${rawCommand}]`); // Process command if (this._analyseStatementMiddlewares.length === 0) { // TODO do nothing } else { let i = 0, len = this._analyseStatementMiddlewares.length; let nextFunc = () => { i++; if (i >= len) { // TODO do nothing } else { this._analyseStatementMiddlewares[i](sub_ctx, nextFunc); } }; this._analyseStatementMiddlewares[0](sub_ctx, nextFunc); } } } // Move to the next line this.incrementNextPosition(); if (covered) { if (this._statementAnalyseStack.length === 0) { break; } } else { if (this._statementAnalyseStack.length > 0) { covered = true; } } } this._nextRunPosition = _nextRunPositionBackup; // Check if the statement stack is empty if (this._statementAnalyseStack.length > 0) { throw `statement not closed, at file [${ this._statementAnalyseStack[ this._statementAnalyseStack.length - 1 ][1][0] }] line [${ this._statementAnalyseStack[ this._statementAnalyseStack.length - 1 ][1][1] + 1 }]`; } // Reset _nextRunPosition to the backup value, and switch back to normal mode, and continue executing // this._core.debug.logconsole.log("[HZEngine] Finished analyse statement mode "); } // eval evalScope(code: string) { try { return new Function("sd", "gd", "hz", `${code}`)( this._core.storage.sd, this._core.storage.gd, this._core ); } catch (e) { this._core.debug.log(`Error in evalScope: ${e}`); } } evalExpression(code: string) { this._core.debug.log(`evalExpression: ${code}`); try { return new Function("sd", "gd", "hz", `return (${code})`)( this._core.storage.sd, this._core.storage.gd, this._core ); } catch (e) { this._core.debug.log(`Error in evalExpression: ${e}`); } } // parse string parseString(str: string) { let parsedInterpolated = parseInterpolatedStr(str); let res: string = ""; for (let item of parsedInterpolated) { if (item.isExpression) { res += this.evalExpression(item.str); } else { res += item.str; } } return res; } } export namespace Script { export type Middleware = (ctx: Context, next: () => void) => void; export class Context { constructor( protected _core: HZEngineCore, private _rawtext: string, public readonly currentPath: string, public readonly currentLineIndex: number, private _statementStack: StatementStack ) {} public get rawtext(): string { return this._rawtext; } public set rawtext(rawtext: string) { this._rawtext = rawtext; this._rawtextChanged = true; } private _rawtextChanged = false; private _slicedArgs: Context.SlicedArg[] | null = null; get slicedArgs(): Context.SlicedArg[] { if (!this._slicedArgs || this._rawtextChanged) this._slicedArgs = splitStr2Objs(this.rawtext); this._rawtextChanged = false; return this._slicedArgs; } // 注意只有在修改在触发slicedArgs的时候才会更新rawtext set slicedArgs(slicedArgs: Context.SlicedArg[]) { this._slicedArgs = JSON.parse(JSON.stringify(slicedArgs)); // TODO 深拷贝 性能 this._rawtext = joinObjs2Str(slicedArgs); // TODO this._rawtextChanged = true ??? } /** * 開始一個新的Statement,返回該Statement的數據 * Start a new statement and return the data of the new statement * @param identifier the identifier of the statement * @returns the data of the new statement */ startStatement( identifier: string, data?: Storage.Saveable ): StatementData { let statement_data = data ?? this.getStatementData(); let statementStackItem: StatementStackItem = [ identifier, [this.currentPath, this.currentLineIndex], statement_data, ]; this._statementStack.push(statementStackItem); return statement_data; } endStatement(identifier: string) { if (this._statementStack.length === 0) throw `statement not open, at file [${this.currentPath}] line [${ this.currentLineIndex + 1 }]`; if ( this._statementStack[this._statementStack.length - 1][0] !== identifier ) throw `the last statement in the stack is not ${identifier}, at file [${ this.currentPath }] line [${this.currentLineIndex + 1}]`; return this._statementStack.pop()![2] as NonNullable< Storage.Saveable >; } get statementStack() { return this._statementStack; } // statement data will stored in core.storage.globalData.script.statement_data // the key of the statement data is the line index of the start statement, // for example: the key of the statement data of "menu ... end menu" // is stored in the key of the line index of the "menu" statement getStatementData(): NonNullable> { let statement_data_in_file = this._core.storage.getSaveableData( this._core.storage.globalData, true, "script", "statement_data", this.currentPath ) as Record>>; if (!statement_data_in_file["" + this.currentLineIndex]) { this._core.script.analyseStatement(this); statement_data_in_file = this._core.storage.getSaveableData( this._core.storage.globalData, true, "script", "statement_data", this.currentPath ) as Record>>; if (!statement_data_in_file[this.currentLineIndex]) throw `analyse statement failed as statement data not found, at file [${ this.currentPath }] line [${this.currentLineIndex + 1}]`; } return statement_data_in_file[this.currentLineIndex]!; } setStatementData( statement_data: NonNullable>, start_position: [path: string, index: number] ) { let statement_data_in_file = this._core.storage.getSaveableData( this._core.storage.globalData, true, "script", "statement_data", start_position[0] ) as Record>>; statement_data_in_file["" + start_position[1]] = statement_data; this._core.storage.saveGlobalData(); } } export namespace Utils { export const joinSlicedArgs: typeof joinObjs2Str = joinObjs2Str; export const splitRawtext: typeof splitStr2Objs = splitStr2Objs; export function splitCommas(rawtext: string): string[] { let slicedArgs = splitStr2Objs(rawtext); // console.log(`splitCommas rawtext: ${rawtext}, slicedArgs: ${JSON.stringify(slicedArgs)}`); let res: string[] = []; for (let i = 0; i < slicedArgs.length; i++) { if (slicedArgs[i].isQuoted) res.push(`"${slicedArgs[i].str}"`); else if (slicedArgs[i].isSquared) res.push(`[${slicedArgs[i].str}]`); else if (slicedArgs[i].isRounded) res.push(`(${slicedArgs[i].str})`); else { slicedArgs[i].str.split(",").forEach((str) => { str = str.trim(); if (str) res.push(str); }); } } // console.log(`splitCommas res: ${JSON.stringify(res)}`); return res; } export function parseTuple(rawtext: string) { if ( rawtext.length < 2 || rawtext[0] !== "(" || rawtext[rawtext.length - 1] !== ")" ) { throw `invalid tuple: ${rawtext}`; } rawtext = rawtext.slice(1, rawtext.length - 1); // console.log(`parseTuple rawtext: ${rawtext}`); return parseHzsArgs(rawtext); } export function parseArray(rawtext: string) { if ( rawtext.length < 2 || rawtext[0] !== "[" || rawtext[rawtext.length - 1] !== "]" ) { throw `invalid array: ${rawtext}`; } rawtext = rawtext.slice(1, rawtext.length - 1); return parseHzsArgs(rawtext); } export function parseHzsArgs(rawtext: string): TupleOrArr { rawtext = rawtext.trim(); let arr = splitCommas(rawtext); let res = arr.map((str) => { if (str.startsWith("(")) return parseTuple(str); else if (str.startsWith("[")) return parseArray(str); else return str; }); // console.log(`parseHzsArgs from: "${rawtext}" ; res: ${JSON.stringify(res)}`); return res; } type TupleOrArr = (string | TupleOrArr)[]; } export type MiddlewareForAnalyseStatement = ( ctx: ContextForAnalyseStatement, next: () => void ) => void; export class ContextForAnalyseStatement extends Context { startStatement( identifier: string, data: Storage.Saveable = {} ): StatementData { return super.startStatement(identifier, data ?? {}); } endStatement(identifier: string) { let statement_data = super.endStatement(identifier); // save analysed statement data let statement_data_in_file = this._core.storage.getSaveableData( this._core.storage.globalData, true, "script", "statement_data", this.currentPath ) as Record>>; statement_data_in_file["" + this.currentLineIndex] = statement_data; this._core.storage.saveGlobalData(); return statement_data; } } export namespace Context { export interface SlicedArg { str: string; isQuoted?: boolean; // 是否被雙引號包圍 isSquared?: boolean; // 是否被方括號包圍 } } export type StatementStackItem = [ identifier: string, start_position: [path: string, index: number], statement_data: Storage.JSONValue, ]; export type StatementStack = StatementStackItem[]; export type StatementData = NonNullable; }