hzengine-core
Version:
An Visual Novel Engine for Zepp OS
624 lines (570 loc) • 20.3 kB
text/typescript
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时恢复执行位置和语句栈
*/
private accessor _callStack: {
position: [path: string, index: number] | null;
statementStack: Script.StatementStack;
}[] = [];
/**
* 语句栈
* 比如while, if,会在语句开始时入栈,语句结束时出栈
*/
private accessor _statementStack: Script.StatementStack = [];
/**
* 下一次执行的脚本位置
* 注意:存储该值的时候应总是拷贝赋值而非直接引用赋值
*/
private accessor _nextRunPosition: [path: string, index: number] | null =
null;
// 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);
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<unknown>
): 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<unknown>
>;
}
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<Storage.Saveable<unknown>> {
let statement_data_in_file = this._core.storage.getSaveableData(
this._core.storage.globalData,
true,
"script",
"statement_data",
this.currentPath
) as Record<string, NonNullable<Storage.Saveable<unknown>>>;
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<string, NonNullable<Storage.Saveable<unknown>>>;
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<Storage.Saveable<unknown>>,
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<string, NonNullable<Storage.Saveable<unknown>>>;
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<unknown> = {}
): 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<string, NonNullable<Storage.Saveable<unknown>>>;
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<Storage.JSONValue>;
}