UNPKG

skynovel

Version:
1,170 lines (983 loc) 38.9 kB
/* ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2018-2020 Famibee (famibee.blog38.fc2.com) This software is released under the MIT License. http://opensource.org/licenses/mit-license.php ** ***** END LICENSE BLOCK ***** */ import {uint, argChk_Boolean, getFn} from './CmnLib'; import {IHTag, IMain, IVariable, IMark, HArg, Script, IPropParser} from './CmnInterface'; import {Config} from './Config'; import {CallStack, ICallStackArg} from './CallStack'; import {Grammar} from './Grammar'; import {AnalyzeTagArg} from './AnalyzeTagArg'; import {EventMng} from './EventMng'; import {Loader, LoaderResource} from 'pixi.js'; import {LayerMng} from './LayerMng'; import {DebugMng} from './DebugMng'; import {SoundMng} from './SoundMng'; import {SysBase} from './SysBase'; interface HScript { [name: string]: Script; }; interface ISeek { idx : number; lineNum : number; }; enum BreakState {running, wait, break, breaking, step, stepping, macro_escaping, macro_esc}; export class ScriptIterator { private script : Script = {aToken: [''], len: 1, aLNum: [1]}; private scriptFn_ = ''; get scriptFn(): string {return this.scriptFn_;}; private idxToken_ = 0; subIdxToken(): void {--this.idxToken_;}; private lineNum_ = 0; get lineNum(): number {return this.lineNum_;} readonly addLineNum = (len: number)=> {this.lineNum_ += len;}; private aCallStk : CallStack[] = []; get lenCallStk(): number {return this.aCallStk.length;}; get lastHArg(): any {return this.aCallStk[this.lenCallStk -1].csArg;}; readonly getCallStk = (idx: number)=> this.aCallStk[idx].csArg; private grm = new Grammar; constructor(private readonly cfg: Config, private readonly hTag: IHTag, private readonly main: IMain, private readonly val: IVariable, private readonly alzTagArg: AnalyzeTagArg, private readonly runAnalyze: ()=> void, private readonly prpPrs: IPropParser, private readonly sndMng: SoundMng, private readonly sys: SysBase) { // 変数操作 hTag.let_ml = o=> this.let_ml(o); // インラインテキスト代入 // デバッグ・その他 hTag.dump_stack = ()=> this.dump_stack(); // スタックのダンプ hTag.dump_script= o=> this.dump_script(o); // スクリプトのダンプ // 条件分岐 hTag['else'] = // その他ifブロック開始 hTag.elsif = // 別条件のifブロック開始 hTag.endif = ()=> this.endif(); // ifブロックの終端 hTag['if'] = o=> this.if(o); // ifブロックの開始 // ラベル・ジャンプ //hTag.button // LayerMng.ts内で定義 // ボタンを表示 hTag.call = o=> this.call(o); // サブルーチンコール hTag.jump = o=> this.jump(o); // シナリオジャンプ hTag.pop_stack = o=> this.pop_stack(o); // コールスタック破棄 hTag.return = ()=> this.return(); // サブルーチンから戻る // マクロ hTag.bracket2macro = o=> this.bracket2macro(o);// 括弧マクロの定義 hTag.char2macro = o=> this.char2macro(o); // 一文字マクロの定義 hTag.endmacro = ()=> this.return(); // マクロ定義の終了 hTag.macro = o=> this.macro(o); // マクロ定義の開始 // しおり //hTag.copybookmark // Variable.ts内で定義 // しおりの複写 //hTag.erasebookmark // Variable.ts内で定義 // しおりの消去 hTag.load = o=> this.load(o); // しおりの読込 hTag.reload_script = o=> this.reload_script(o); // スクリプト再読込 hTag.record_place = ()=> this.record_place(); // セーブポイント指定 hTag.save = o=> this.save(o); // しおりの保存 if (cfg.oCfg.debug.token) this.dbgToken = token=> console.log(`🌱 トークン fn:${this.scriptFn_} idxToken:${this.idxToken_} ln:${this.lineNum} token【${token}】`); val.defTmp('const.sn.vctCallStk.length', ()=> this.aCallStk.length); this.grm.setEscape(cfg.oCfg.init.escape); if (sys.isDbg()) { sys.addHook((type, o)=> this.hHook[type]?.(type, o)); this.isBreak = this.isBreak_base; const fnc = this.analyzeInit; this.analyzeInit = ()=> { this.breakState = BreakState.wait; main.setLoop(false, 'ステップ実行'); // ScriptIterator.ts 'launch' の肩代わり this.sys.callHook('stopOnStep', {}); // sn全体へ通知 this.sys.sendDbg('stopOnStep', {}); this.analyzeInit = fnc; this.analyzeInit(); }; } } destroy() {this.isBreak = ()=> false;} private readonly hHook: {[type: string]: (type: string, o: any)=> void} = { 'attach': ()=> { this.breakState = BreakState.wait; this.main.setLoop(false, '一時停止'); this.sys.sendDbg('stop', {}); }, //'launch': // ここでは冒頭停止に間に合わないのでanalyzeInit()で 'disconnect': ()=> { ScriptIterator.hBrkP = {}; ScriptIterator.hFuncBP = {}; this.isBreak = ()=> false; this.hHook.continue('', {}); this.breakState = BreakState.running; }, 'restart': ()=> this.isBreak = ()=> false, // ブレークポイント登録 'clear_break': (_, o)=> ScriptIterator.hBrkP[getFn(o.fn)] = {}, 'add_break': (_, o)=> ScriptIterator.hBrkP[getFn(o.fn)][o.ln] = o, 'del_break': (_, o)=>delete ScriptIterator.hBrkP[getFn(o.fn)][o.ln], 'data_break': (_, o)=> { if (this.breakState !== BreakState.running) return; this.breakState = BreakState.wait; this.main.setLoop(false, `変数 ${o.dataId}【${o.old_v}】→【${o.new_v}】データブレーク`); this.sys.callHook('stopOnDataBreakpoint', {}); // sn全体へ通知 this.sys.sendDbg('stopOnDataBreakpoint', {}); }, 'set_func_break': (_, o)=> { ScriptIterator.hFuncBP = {}; o.a.forEach((v: any)=> ScriptIterator.hFuncBP[v.name] = 1); this.sys.sendDbg(o.ri, {}); }, // 情報問い合わせ系 'stack': (_, o)=> this.sys.sendDbg(o.ri, {a: this.aStack()}), 'eval': (_,o)=> {this.sys.sendDbg(o.ri, {v: this.prpPrs.parse(o.txt)})}, // デバッガからの操作系 'continue': ()=> { if (this.isIdxOverLast()) return; this.idxToken_ -= this.idxDx4Dbg; this.breakState = BreakState.breaking; this.main.setLoop(true); this.main.resume(); // jumpループ後などで停止している場合があるので }, 'stepover': (type, o)=> this.go_stepover(type, o), 'stepin': ()=> { if (this.isIdxOverLast()) return; const tkn = this.script.aToken[this.idxToken_ -this.idxDx4Dbg]; this.sys.callHook(`stopOnStep${ tkn.charAt(0) === '[' && tkn.slice(1,-1)in this.hMacro ?'In' :'' }`, {}); // sn全体へ通知 this.idxToken_ -= this.idxDx4Dbg; this.breakState = this.breakState === BreakState.wait ? BreakState.step : BreakState.stepping; this.main.setLoop(true); this.main.resume(); // jumpループ後などで停止している場合があるので }, 'stepout': (type, o)=> { if (this.isIdxOverLast()) return; if (this.lenCallStk > 0) this.go_stepout(true); else this.go_stepover(type, o); }, }; private go_stepover(type: string, o: any) { if (this.isIdxOverLast()) return; const tkn = this.script.aToken[this.idxToken_ -this.idxDx4Dbg]; if (tkn.charAt(0) === '[' && tkn.slice(1, -1) in this.hMacro) this.go_stepout(); else { this.sys.callHook('stopOnStep', {}); // sn全体へ通知 this.hHook.stepin(type, o); } } private go_stepout(out = false) { this.sys.callHook(`stopOnStep${out ?'Out' :''}`, {}); // sn全体へ通知 this.csDepth_macro_esc = this.lenCallStk -(out ?1 :0); this.idxToken_ -= this.idxDx4Dbg; this.breakState = out ?BreakState.macro_esc :BreakState.macro_escaping; this.main.setLoop(true); this.main.resume(); // jumpループ後などで停止している場合があるので } private csDepth_macro_esc = 0; private get idxDx4Dbg() { return this.breakState === BreakState.break || this.breakState === BreakState.step ?1 :0 }; private isIdxOverLast(): boolean { if (this.idxToken_ < this.script.len) return false; this.main.setLoop(false, 'スクリプト終端です'); this.sys.callHook('stopOnEntry', {}); // sn全体へ通知 return true; } // reload 再生成 Main に受け渡すため static private static hBrkP: {[fn: string]: {[ln: number]: any}} = {}; private static hFuncBP: {[tag_name: string]: 1} = {}; private breakState = BreakState.running; isBreak = (_token: string)=> false; private isBreak_base(token: string): boolean { switch (this.breakState) { case BreakState.macro_escaping: this.subHitCondition(); this.breakState = BreakState.macro_esc; break; case BreakState.macro_esc: if (this.lenCallStk !== this.csDepth_macro_esc) break; this.breakState = BreakState.step; this.main.setLoop(false, 'ステップ実行'); this.sys.sendDbg('stopOnStep', {}); return true; // タグを実行せず、直前停止 case BreakState.stepping: this.subHitCondition(); this.breakState = BreakState.step; break; case BreakState.step: this.subHitCondition(); this.main.setLoop(false, 'ステップ実行'); this.sys.sendDbg('stopOnStep', {}); return true; // タグを実行せず、直前停止 case BreakState.breaking: this.subHitCondition(); this.breakState = BreakState.running; break; default: { // 関数ブレークポイント const e = Grammar.REG_TAG.exec(token); const tag_name = e?.groups?.name ?? ''; if (tag_name in ScriptIterator.hFuncBP) { this.breakState = BreakState.break; this.main.setLoop(false, `関数 ${token} ブレーク`); this.sys.callHook('stopOnBreakpoint', {}); // sn全体へ通知 this.sys.sendDbg('stopOnBreakpoint', {}); return true; // タグを実行せず、直前停止 } } { // ブレークポイント const bp = ScriptIterator.hBrkP[getFn(this.scriptFn_)]; if (! bp) break; const o: any = bp[this.lineNum_]; if (! o) break; //console.log(`fn:ScriptIterator.ts line:145 👺 【bs:${this.breakState} idx:${this.idxToken_} ln:${this.lineNum_} tkn:${this.script.aToken[this.idxToken_ -1]}:】 o:%o`, o); if (o.condition) {if (! this.prpPrs.parse(o.condition)) break;} else if (('hitCondition' in o) && --o.hitCondition > 0) break; } const isBreak = this.breakState === BreakState.running; this.breakState = BreakState.break; this.main.setLoop(false, isBreak ?'ブレーク' :'ステップ実行'); const type = isBreak ?'stopOnBreakpoint' :'stopOnStep'; this.sys.callHook(type, {}); // sn全体へ通知 this.sys.sendDbg(type, {}); return true; // タグを実行せず、直前停止 } return false; // no break、タグを実行 } private subHitCondition() { // step実行中でbreakしないがヒットカウントだけ減算 const bpo = ScriptIterator.hBrkP[getFn(this.scriptFn_)]?.[this.lineNum_]; if (bpo?.hitCondition) --bpo.hitCondition; } private aStack(): {fn: string, ln: number, col: number, nm: string}[] { const tkn0 = this.script.aToken[this.idxToken_ -1]; const fn0 = this.cfg.searchPath(this.scriptFn_, Config.EXT_SCRIPT); if (this.idxToken_ === 0) return [{fn: fn0, ln: 1, col: 1, nm: tkn0,}]; const lc0 = this.cnvIdx2lineCol(this.script, this.idxToken_); const a = [{fn: fn0, ln: lc0.ln, col: lc0.col_s +1, nm: tkn0,}]; const len = this.aCallStk.length; if (len === 0) return a; for (let i=len -1; i>=0; --i) { const cs = this.aCallStk[i]; if (! cs.csArg) continue; const lc = this.cnvIdx2lineCol(this.hScript[cs.fn], cs.idx); a.push({ fn: this.cfg.searchPath(cs.fn, Config.EXT_SCRIPT), ln: lc.ln, col: lc.col_s +1, nm: cs.csArg.タグ名 ?? '', }); } return a; } // result = true : waitする resume()で再開 タグ解析(tagToken: string): boolean { const e = Grammar.REG_TAG.exec(tagToken); const g = e?.groups; if (! g) throw `タグ記述【${tagToken}】異常です(タグ解析)`; const tag_name = g.name; const tag_fnc = this.hTag[tag_name]; if (! tag_fnc) throw `未定義のタグ【${tag_name}】です`; this.alzTagArg.go(g.args); if (this.cfg.oCfg.debug.tag) console.log(`🌲 タグ解析 fn:${this.scriptFn_} lnum:${this.lineNum_} [${tag_name} %o]`, this.alzTagArg.hPrm); if (this.alzTagArg.hPrm.cond) { const cond = this.alzTagArg.hPrm.cond.val; if (! cond || cond.charAt(0) ==='&') throw '属性condは「&」が不要です'; const p = this.prpPrs.parse(cond); const ps = String(p); if (ps === 'null' || ps === 'undefined') return false; if (! p) return false; } let hArg: any = {}; const lenStk = this.aCallStk.length; if (this.alzTagArg.isKomeParam) { if (lenStk === 0) throw '属性「*」はマクロのみ有効です'; if (! this.lastHArg) throw '属性「*」はマクロのみ有効です'; hArg = {...hArg, ...this.lastHArg}; } hArg.タグ名 = tag_name; for (const k in this.alzTagArg.hPrm) { let v = this.alzTagArg.hPrm[k].val; if (v && v.charAt(0) === '%') { if (lenStk === 0) throw '属性「%」はマクロ定義内でのみ使用できます(そのマクロの引数を示す簡略文法であるため)'; const mac = this.lastHArg[v.slice(1)]; if (mac) {hArg[k] = mac; continue;} v = this.alzTagArg.hPrm[k].def; if (! v || v === 'null') continue; // defのnull指定。%指定が無い場合、タグやマクロに属性を渡さない } v = this.prpPrs.getValAmpersand(v ?? ''); if (v !== 'undefined') {hArg[k] = v; continue;} const def = this.alzTagArg.hPrm[k].def; if (def === undefined) continue; v = this.prpPrs.getValAmpersand(def); if (v !== 'undefined') hArg[k] = v; // 存在しない値の場合、属性を渡さない } return tag_fnc(hArg); } private evtMng : EventMng; private layMng : LayerMng; setOtherObj(evtMng: EventMng, layMng: LayerMng): void { this.evtMng = evtMng; this.layMng = layMng; } // // 変数操作 // インラインテキスト代入 private let_ml(hArg: HArg) { const name = hArg.name; if (! name) throw 'nameは必須です'; let ml = ''; const len = this.script.len; for (; this.idxToken_<len; ++this.idxToken_) { ml = this.script.aToken[this.idxToken_]; if (ml !== '') break; } hArg.text = ml; hArg.cast = 'str'; this.hTag['let'](hArg); this.idxToken_ += 2; this.lineNum_ += (ml.match(/\n/g) ?? []).length; return false; } // // デバッグ・その他 // スタックのダンプ private dump_stack() { if (this.idxToken_ === 0) { console.group(`🥟 [dump_stack] スクリプト現在地 fn:${this.scriptFn_} line:${1} col:${0}`); console.groupEnd(); return false; } const lc0 = this.cnvIdx2lineCol(this.script, this.idxToken_); const now = `スクリプト現在地 fn:${this.scriptFn_} line:${lc0.ln} col:${lc0.col_s +1}`; console.group(`🥟 [dump_stack] ${now}`); const len = this.aCallStk.length; if (len > 0) { console.info(now); for (let i=len -1; i>=0; --i) { const cs = this.aCallStk[i]; if (! cs.csArg) continue; const csa = cs.csArg.hMp; const from_macro_nm = csa ?csa['タグ名'] :null; const call_nm = cs.csArg.タグ名; const lc = this.cnvIdx2lineCol(this.hScript[cs.fn], cs.idx); console.info( `${len -i}つ前のコール元 fn:${cs.fn} line:${lc.ln } col:${lc.col_s +1 }`+ (from_macro_nm ?'(['+ from_macro_nm +']マクロ内)' :' ')+ `で [${call_nm} ...]をコール` ); } } console.groupEnd(); return false; } private cnvIdx2lineCol(st: Script, idx: number): {ln: number, col_s: number, col_e: number} { const ret = {ln: 0, col_s: 0, col_e: 0}; if (! st) return ret; const lN = ret.ln = st.aLNum[idx -1]; let col_e = 0; let i = idx -1; while (st.aLNum[i] === lN) { col_e += st.aToken[i].length; if (--i < 0) break; } ret.col_e = col_e; ret.col_s = col_e -st.aToken[idx -1].length return ret; } // 外部へスクリプトを表示 private dump_script(hArg: HArg) { const set_fnc = hArg.set_fnc; // スクリプトを返すコールバック if (! set_fnc) throw 'set_fncは必須です'; this.fncSet = (globalThis as any)[set_fnc]; if (! this.fncSet) { if (argChk_Boolean(hArg, 'need_err', true)) throw `HTML内に関数${set_fnc}が見つかりません`; this.fncSet = ()=> {}; return false; } this.noticeBreak = (goto: boolean)=> { if (this.fnLastBreak !== this.scriptFn_) { this.fnLastBreak = this.scriptFn_; this.fncSet( this.hScrCache4Dump[this.scriptFn_] = this.hScrCache4Dump[this.scriptFn_] ?? this.script.aToken.join('') ); } this.fncBreak(this.lineNum_, goto); }; this.noticeBreak(true); // 一度目のthis.fncBreak()はスルー(まだ読んでないし) const break_fnc = hArg.break_fnc; // Break通知コールバック if (! break_fnc) return false; this.fncBreak = (globalThis as any)[break_fnc]; if (! this.fncBreak) { if (argChk_Boolean(hArg, 'need_err', true)) throw `HTML内に関数${break_fnc}が見つかりません`; this.fncBreak = ()=> {}; } return false; } private fncSet: (txt: string)=> void = ()=> {}; private fncBreak: (ln: number, goto: boolean)=> void = ()=> {}; private fnLastBreak = ''; private hScrCache4Dump: {[fn: string]: string;} = {}; noticeBreak = (_goto: boolean)=> {} private dumpErrLine = 5; dumpErrForeLine() { if (this.idxToken_ === 0) { console.group(`🥟 Error line (from 0 rows before) fn:${this.scriptFn_}`); console.groupEnd(); return; } let s = ''; for (let i=this.idxToken_ -1; i>=0; --i) { s = this.script.aToken[i] + s; if ((s.match(/\n/g) ?? []).length >= this.dumpErrLine) break; } const a = s.split('\n').slice(-this.dumpErrLine); const len = a.length; console.group(`🥟 Error line (from ${len} rows before) fn:${this.scriptFn_}`); const ln_txt_width = String(this.lineNum_).length; const lc = this.cnvIdx2lineCol(this.script, this.idxToken_); for (let i=0; i<len; ++i) { const ln = this.lineNum_ -len +i +1; const mes = `${String(ln).padStart(ln_txt_width, ' ')}: %c`; const e = a[i]; const line = (e.length > 75) ?e.slice(0, 75) +'…' :e; // 長い場合は後略 if (i === len -1) console.info( mes + line.slice(0, lc.col_s) +'%c'+ line.slice(lc.col_s), 'color: black; background-color: skyblue;', 'color: black; background-color: pink;' ) else console.info(mes + line, 'color: black; background-color: skyblue;'); } console.groupEnd(); //console.log('Linkの出力 : %o', 'file:///Volumes/MacHD2/_Famibee/SKYNovel/prj/mat/main.sn'); } // 条件分岐 private aIfStk : number[] = [-1]; private endif() { if (this.aIfStk[0] === -1) throw 'ifブロック内ではありません'; this.idxToken_ = this.aIfStk[0]; this.lineNum_ = this.script.aLNum[this.idxToken_ -1]; this.aIfStk.shift(); // 最初の要素を取り除く return false; } private if(hArg: HArg) { //console.log('if idxToken:'+ this.idxToken_); const exp = hArg.exp; if (! exp) throw 'expは必須です'; if (exp.charAt(0) === '&') throw '属性expは「&」が不要です'; let cntDepth = 0; // if深度カウンター let idxGo = this.prpPrs.parse(exp) ?this.idxToken_ :-1; for (; this.idxToken_<this.script.len; ++this.idxToken_) { if (! this.script.aLNum[this.idxToken_]) this.script.aLNum[this.idxToken_] = this.lineNum_; const t = this.script.aToken[this.idxToken_]; //console.log(`[if]トークン fn:${this.scriptFn_} lnum:${this.lineNum_} idx:${this.idxToken_} realLn:${this.script.aLNum[this.idxToken_]} idxGo:${idxGo} cntDepth:${cntDepth} token<${t}>`); if (! t) continue; const uc = t.charCodeAt(0); // TokenTopUnicode if (uc === 10) {this.addLineNum(t.length); continue;} // \n 改行 if (uc !== 91) continue; // [ タグ開始以外 const a_tag = Grammar.REG_TAG.exec(t); const g = a_tag?.groups; if (! g) throw 'タグ記述['+ t +']異常です(if文)'; const tag_name = g.name; if (! (tag_name in this.hTag)) throw `未定義のタグ[${tag_name}]です`; this.alzTagArg.go(g.args); switch (tag_name) { case 'if': ++cntDepth; break; case 'elsif': if (cntDepth > 0) break; if (idxGo > -1) break; const e = this.alzTagArg.hPrm.exp.val ?? ''; if (e.charAt(0) === '&') throw '属性expは「&」が不要です'; if (this.prpPrs.parse(e)) idxGo = this.idxToken_ +1; break; case 'else': if (cntDepth > 0) break; if (idxGo === -1) idxGo = this.idxToken_ +1; break; case 'endif': if (cntDepth > 0) {--cntDepth; break;} if (idxGo === -1) { ++this.idxToken_; this.script.aLNum[this.idxToken_] = this.lineNum_; } else { this.aIfStk.unshift(this.idxToken_ +1); // 最初に要素を追加 this.idxToken_ = idxGo; this.lineNum_ = this.script.aLNum[this.idxToken_]; } return false; } } throw '[endif]がないままスクリプト終端です'; //return false; } // // ラベル・ジャンプ // サブルーチンコール private call(hArg: HArg) { if (! argChk_Boolean(hArg, 'count', false)) this.eraseKidoku(); const fn = hArg.fn; if (fn) this.cfg.searchPath(fn, Config.EXT_SCRIPT); // chk only this.callSub({hEvt1Time: this.evtMng.popLocalEvts()}); if (argChk_Boolean(hArg, 'clear_local_event', false)) this.hTag.clear_event({}); this.jumpWork(fn, hArg.label); return true; } private callSub(csa: ICallStackArg) { this.script.aLNum[this.idxToken_] = this.lineNum_; // 戻ったときの行番号 if (! this.resvToken) {csa.resvToken = ''; this.clearResvToken();} this.aCallStk.push(new CallStack(this.scriptFn_, this.idxToken_, csa)); this.aIfStk.unshift(-1); // 最初に要素を追加 } // シナリオジャンプ private jump(hArg: HArg) { if (! argChk_Boolean(hArg, 'count', true)) this.eraseKidoku(); this.aIfStk[0] = -1; this.jumpWork(hArg.fn, hArg.label); return true; } // コールスタック破棄 private pop_stack(hArg: HArg) { if (argChk_Boolean(hArg, 'clear', false)) this.aCallStk = []; else if (! this.aCallStk.pop()) throw '[pop_stack] スタックが空です'; this.clearResvToken(); this.aIfStk = [-1]; return false; } // サブルーチンから戻る private return() { const cs = this.aCallStk.pop(); if (! cs) throw '[return] スタックが空です'; const csArg = cs.csArg; if (! csArg) return false; this.aIfStk.shift(); // 最初の要素を取り除く const hMp = csArg.hMp; // マクロからの復帰の場合にmp:値も復帰 if (hMp) this.val.setMp(hMp); const after_token = csArg.resvToken; if (after_token) this.nextToken = ()=> { this.clearResvToken(); return after_token; } else this.clearResvToken(); if (csArg.hEvt1Time) this.evtMng.pushLocalEvts(csArg.hEvt1Time); if (cs.fn in this.hScript) {this.jump_light(cs); return false;} this.jumpWork(cs.fn, '', cs.idx); // 確実にスクリプトロードなので return true; } private resvToken = ''; private clearResvToken() { this.resvToken = ''; this.nextToken = this.nextToken_Proc; } private skipLabel = ''; private jumpWork(fn = '', label = '', idx = 0) { if (! fn && ! label) this.main.errScript('[jump系] fnまたはlabelは必須です'); if (label) { if (label.charAt(0) !== '*') this.main.errScript('[jump系] labelは*で始まります'); this.skipLabel = label; if (this.skipLabel.slice(0, 2) !== '**') this.idxToken_ = idx; } else { this.skipLabel = ''; this.idxToken_ = idx; } if (! fn) {this.analyzeInit(); return;} const full_path = this.cfg.searchPath(fn, Config.EXT_SCRIPT); if (fn === this.scriptFn_) {this.analyzeInit(); return;} this.scriptFn_ = fn; const st = this.hScript[this.scriptFn_]; if (st) {this.script = st; this.analyzeInit(); return;} (new Loader()).add(this.scriptFn_, full_path) .pre((res: LoaderResource, next: Function)=> res.load(()=> { this.sys.pre(res.extension, res.data) .then(r=> {res.data = r; next();}) // TODO: 暗号化スクリプトかは、前方から一定の長さに(\n|\t)有無で分かる // if (this.onlyCodeScript()) this.main.errScript('[セキュリティ] 暗号化スクリプト以外許されません'); .catch(e=> this.main.errScript(`[jump系]snロード失敗です fn:${res.name} ${e}`, false)); })) .load((_ldr: any, hRes: any)=> { this.nextToken = this.nextToken_Proc; this.resolveScript(hRes[fn].data); this.hTag.record_place({}); this.main.resume(()=> this.analyzeInit()); // 直接呼んでもいいが、内部コールスタック積んだままになるのがなんかイヤで }); this.main.stop(); } private analyzeInit(): void { const o = this.seekScript(this.script, Boolean(this.val.getVal('mp:const.sn.macro_name')), this.lineNum_, this.skipLabel, this.idxToken_); this.idxToken_ = o.idx; this.lineNum_ = o.lineNum; this.runAnalyze(); } // シナリオ解析処理ループ・冒頭処理 nextToken = ()=> ''; // 初期化前に終了した場合向け private nextToken_Proc(): string { if (this.errOverScr()) return ''; this.recordKidoku(); // トークンの行番号更新 if (! this.script.aLNum[this.idxToken_]) this.script.aLNum[this.idxToken_] = this.lineNum_; const token = this.script.aToken[this.idxToken_]; this.dbgToken(token); ++this.idxToken_; return token; } private dbgToken = (_token: string)=> {}; private errOverScr(): boolean { if (this.idxToken_ < this.script.len) return false; this.main.errScript('スクリプト終端です'); return true; } private readonly REG_NONAME_LABEL = /(\*{2,})(.*)/; private readonly REG_LABEL_ESC = /\*/g; private readonly REG_TOKEN_MACRO_BEGIN = /\[macro\s/; private readonly REG_TOKEN_MACRO_END = /\[endmacro[\s\]]/; private readonly REG_TAG_LET_ML = /^\[let_ml\s/g; private readonly REG_TAG_ENDLET_ML = /^\[endlet_ml\s*]/g; private seekScript(st: Script, inMacro: boolean, ln: number, skipLabel: string, idxToken: number): ISeek { //console.log(`seekScript (from)inMacro:${inMacro} (from)lineNum:${ln} (to)skipLabel:${skipLabel}: (to)idxToken:${idxToken}`); const len = st.aToken.length; if (! skipLabel) { if (this.errOverScr()) return {idx: idxToken, lineNum: ln}; if (! st.aLNum[idxToken]) { // undefined ln = 1; for (let j=0; j<idxToken; ++j) { // 走査ついでにトークンの行番号も更新 if (! st.aLNum[j]) st.aLNum[j] = ln; const token_j = st.aToken[j]; if (token_j.charCodeAt(0) === 10) { // \n 改行 ln += token_j.length; } } st.aLNum[idxToken] = ln; } else { ln = st.aLNum[idxToken]; } return { idx: idxToken, lineNum : ln } } st.aLNum[0] = 1; // 先頭トークン=一行目 const a_skipLabel = skipLabel.match(this.REG_NONAME_LABEL); if (a_skipLabel) { skipLabel = a_skipLabel[1]; let i = idxToken; switch (a_skipLabel[2]) { case 'before': while (st.aToken[--i] !== skipLabel) { if (i === 0) DebugMng.myTrace('[jump系 無名ラベルbefore] ' + ln +'行目以前で'+ (inMacro ?'マクロ内に' :'') + 'ラベル【'+ skipLabel +'】がありません', 'ET'); if (inMacro && st.aToken[i].search(this.REG_TOKEN_MACRO_BEGIN) > -1) DebugMng.myTrace('[jump系 無名ラベルbefore] マクロ内にラベル【'+ skipLabel +'】がありません', 'ET'); } return { idx: i +1, lineNum : st.aLNum[i] } // break; case 'after': while (st.aToken[++i] !== skipLabel) { if (i === len) DebugMng.myTrace('[jump系 無名ラベルafter] ' + ln +'行目以後でマクロ内にラベル【'+ skipLabel +'】がありません', 'ET'); if (st.aToken[i].search(this.REG_TOKEN_MACRO_END) > -1) DebugMng.myTrace('[jump系 無名ラベルafter] ' + ln +'行目以後でマクロ内にラベル【'+ skipLabel +'】がありません', 'ET'); } return { idx: i +1, lineNum : st.aLNum[i] } // break; default: DebugMng.myTrace('[jump系] 無名ラベル指定【label='+ skipLabel +'】が間違っています', 'ET'); } } ln = 1; const reLabel = new RegExp( '^'+ skipLabel.replace(this.REG_LABEL_ESC, '\\*') +'(?:\\s|;|\\[|$)'); let in_let_ml = false; for (let i=0; i<len; ++i) { // 走査ついでにトークンの行番号も更新 if (! st.aLNum[i]) st.aLNum[i] = ln; const token = st.aToken[i]; const uc = token.charCodeAt(0); // TokenTopUnicode if (uc !== 42) { // 42 = * if (in_let_ml) { this.REG_TAG_ENDLET_ML.lastIndex = 0; if (this.REG_TAG_ENDLET_ML.test(token)) { in_let_ml = false; continue; } ln += (token.match(/\n/g) ?? []).length; // \n 改行 } else { this.REG_TAG_LET_ML.lastIndex = 0; if (this.REG_TAG_LET_ML.test(token)) { in_let_ml = true; continue; } if (uc === 10) ln += token.length; // \n 改行 } continue; } if (token.search(reLabel) > -1) return { idx: i +1, lineNum : ln } // break; } if (in_let_ml) throw '[let_ml]の終端・[endlet_ml]がありません'; DebugMng.myTrace(`[jump系] ラベル【`+ skipLabel +`】がありません`, 'ET'); throw 'Dummy'; } private hScript : HScript = Object.create(null); //{} シナリオキャッシュ private resolveScript(txt: string) { const v = txt .replace(/(\r\n|\r)/g, '\n') .match(this.grm.REG_TOKEN) ?? []; for (let i=v.length -1; i>=0; --i) { const e = v[i]; this.REG_TAG_LET_ML.lastIndex = 0; if (this.REG_TAG_LET_ML.test(e)) { const idx = e.indexOf(']') +1; if (idx === 0) throw '[let_ml]で閉じる【]】がありません'; const a = e.slice(0, idx); const b = e.slice(idx); v.splice(i, 1, a, b); } } this.script = {aToken :v, len :v.length, aLNum :[]}; let mes = ''; try { mes = 'ScriptIterator.replaceScriptChar2macro'; this.grm.replaceScr_C2M_And_let_ml(this.script); mes = 'ScriptIterator.replaceScript_Wildcard'; this.replaceScript_Wildcard(); } catch (err) { if (err instanceof Error) { const e = err as Error; mes += '例外 mes='+ e.message +'('+ e.name +')'; } else { mes = err as string; } this.main.errScript(mes, false); } this.hScript[this.scriptFn_] = this.script; this.val.loadScrWork(this.scriptFn_); } private jump_light(cs: CallStack) { // jumpでは連続マクロでスタックオーバーフローになるので簡易版を // 主に[return]やマクロ終了でジャンプ先がチェック不要な場合用 // analyzeInit()とかもジャンプ前にやってて不要だし this.scriptFn_ = cs.fn; this.idxToken_ = cs.idx; const st = this.hScript[this.scriptFn_]; if (st) this.script = st; this.lineNum_ = this.script.aLNum[cs.idx]; } private readonly REG_WILDCARD = /^\[(call|loadplugin)\s/; private readonly REG_WILDCARD2 = /\bfn\s*=\s*[^\s\]]+/; private replaceScript_Wildcard = ()=> { for (let i=this.script.len -1; i>=0; --i) { const token = this.script.aToken[i]; this.REG_WILDCARD.lastIndex = 0; if (! this.REG_WILDCARD.test(token)) continue; const e = Grammar.REG_TAG.exec(token); const g = e?.groups; if (! g) continue; this.alzTagArg.go(g.args); const p_fn = this.alzTagArg.hPrm.fn; if (! p_fn) continue; const fn = p_fn.val; if (! fn || fn.slice(-1) !== '*') continue; const ext = (g.name === 'loadplugin') ?'css' :'sn'; const a = this.cfg.matchPath('^'+ fn.slice(0, -1) +'.*', ext); this.script.aToken.splice(i, 1, '\t', '; '+ token); this.script.aLNum.splice(i, 1, NaN, NaN); for (const v of a) { const nt = token.replace( this.REG_WILDCARD2, 'fn='+ decodeURIComponent(getFn(v[ext])) ); //console.log('\t='+ nt +'='); this.script.aToken.splice(i, 0, nt); this.script.aLNum.splice(i, 0, NaN); } } this.script.len = this.script.aToken.length; } private recordKidoku(): void { const areas = this.val.getAreaKidoku(this.scriptFn_); if (! areas) throw `recordKidoku fn:'${this.scriptFn_}' (areas === null)`; // マクロ内やサブルーチンではisKidokuを変更させない if (this.aCallStk.length > 0) {areas.record(this.idxToken_); return;} this.isKidoku_ = areas.search(this.idxToken_); this.val.setVal_Nochk('tmp', 'const.sn.isKidoku', this.isKidoku_); if (this.isKidoku_) return; areas.record(this.idxToken_); // saveKidoku() // 厳密にはここですべきだが、パフォーマンスに問題があるので // クリック待ちを期待できるwait、waitclick、s、l、pタグで // saveKidoku()をコール。 } private isKidoku_ = false; get isKidoku(): boolean {return this.isKidoku_;}; private eraseKidoku(): void { this.val.getAreaKidoku(this.scriptFn_)?.erase(this.idxToken_); this.isKidoku_ = false; } get isNextKidoku(): boolean { let fn = this.scriptFn; let idx = this.idxToken_; let len = this.script.len; if (this.aCallStk.length > 0) { const cs = this.aCallStk[0]; fn = cs.fn; idx = cs.idx; const st = this.hScript[fn]; if (st) len = st.len; } const areas = this.val.getAreaKidoku(fn); if (! areas) return false; if (idx === len) return false; // スクリプト終端 //traceDbg("isNextKidoku fn:"+ fn +" idx:"+ idx +" ret="+ (areas.search(idx))); //traceDbg("【"+ vctT[idx-1] +"】【"+ vctT[idx] +"】"); return areas.search(idx); } get normalWait(): number { return this.isKidoku_ ? ( this.val.getVal('sys:sn.tagCh.doWait_Kidoku') ? uint(this.val.getVal('sys:sn.tagCh.msecWait_Kidoku')) : 0 ) : ( this.val.getVal('sys:sn.tagCh.doWait') ? uint(this.val.getVal('sys:sn.tagCh.msecWait')) : 0 ); } // // マクロ // 括弧マクロの定義 private bracket2macro(hArg: HArg) { this.grm.bracket2macro(hArg, this.script, this.idxToken_); return false; } // 一文字マクロの定義 private char2macro(hArg: HArg) { this.grm.char2macro(hArg, this.hTag, this.script, this.idxToken_); return false; } // マクロ定義の開始 private macro(hArg: HArg) { const name = hArg.name; if (! name) throw 'nameは必須です'; if (name in this.hTag) throw `[${name}]はタグかすでに定義済みのマクロです`; const ln = this.lineNum_; const cs = new CallStack(this.scriptFn_, this.idxToken_); this.hMacro[name] = 1; this.hTag[name] = (hArgM: HArg)=> { this.callSub({...hArgM, hMp: this.val.cloneMp()} as any); // AIRNovelの仕様:親マクロが子マクロコール時、*がないのに値を引き継ぐ //for (const k in hArg) this.val.setVal_Nochk('mp', k, hArg[k]); this.val.setMp(hArgM); this.val.setVal_Nochk('mp', 'const.sn.macro_name', name); this.val.setVal_Nochk('mp', 'const.sn.me_call_scriptFn', this.scriptFn_); this.lineNum_ = ln; this.jump_light(cs); return false; }; for (; this.idxToken_ < this.script.len; ++this.idxToken_) { // トークンの行番号更新 if (! this.script.aLNum[this.idxToken_]) this.script.aLNum[this.idxToken_] = this.lineNum_; const token = this.script.aToken[this.idxToken_]; if (token.search(this.REG_TOKEN_MACRO_END) > -1) { ++this.idxToken_; return false; } const uc = token.charCodeAt(0); // TokenTopUnicode if (uc === 10) this.lineNum_ += token.length; // \n 改行 else if (uc === 91) this.lineNum_ += (token.match(/\n/g) ?? []).length; // [ タグ開始 } throw `マクロ[${name}]定義の終端・[endmacro]がありません`; } private readonly hMacro: {[nm: string]: 1} = {}; // // しおり // しおりの読込 private load(hArg: HArg) { const place = hArg.place; if (! place) throw 'placeは必須です'; if (('fn' in hArg) !== ('label' in hArg)) throw 'fnとlabelはセットで指定して下さい'; const mark = this.val.getMark(place); if (! mark) throw `place【${place}】は存在しません`; return this.loadFromMark(hArg, mark); } private loadFromMark(hArg: HArg, mark: IMark, reload_sound = true) { this.layMng.cover(true); this.hTag.clear_event({}); this.val.mark2save(mark); this.val.setMp({}); this.layMng.recText('', true); if (reload_sound) this.sndMng.playLoopFromSaveObj(); if (argChk_Boolean(hArg, 'do_rec', true)) this.mark = { hSave : this.val.cloneSave(), hPages : {...mark.hPages}, aIfStk : [...mark.aIfStk], } const o: any = { enabled: this.val.getVal('save:const.sn.autowc.enabled'), text: String(this.val.getVal('save:const.sn.autowc.text')), time: String(this.val.getVal('save:const.sn.autowc.time')), }; this.hTag.autowc(o); const fn = String(this.val.getVal('save:const.sn.scriptFn')); const idx = Number(this.val.getVal('save:const.sn.scriptIdx')); delete this.hScript[fn]; // 必ずスクリプトを再読込。吉里吉里に動作を合わせる this.aIfStk = [...this.mark.aIfStk]; this.aCallStk = []; // playback中の画像読み込み完了イベントを破棄 this.layMng.playback(this.mark.hPages, 'label' in hArg ? ()=> { this.layMng.cover(false); this.scriptFn_ = fn; this.idxToken_ = idx; this.hTag.call({fn: hArg.fn, label: hArg.label}); } : ()=> { this.layMng.cover(false); this.jumpWork(fn, '', idx); } ); return true; } // スクリプト再読込 private reload_script(hArg: HArg) { // 最後の[record_place]から再開 const mark = this.val.getMark(0); // 起動から再読込までの間に追加・変更・削除されたファイルがあるかも、に対応 // delete this.hScript[this.scriptFn_]; // これだと[reload_script]位置になる delete this.hScript[getFn(mark.hSave['const.sn.scriptFn'])]; hArg.do_rec = false; return this.loadFromMark(hArg, mark, false); } // セーブポイント指定 private mark: IMark = { hSave : {}, hPages : {}, aIfStk : [-1], }; private record_place() { if (this.main.isDestroyed()) return false; const len = this.aCallStk.length; if (len === 0) { this.val.setVal_Nochk('save', 'const.sn.scriptFn', this.scriptFn); this.val.setVal_Nochk('save', 'const.sn.scriptIdx', this.idxToken_); } else { this.val.setVal_Nochk('save', 'const.sn.scriptFn', this.aCallStk[0].fn); this.val.setVal_Nochk('save', 'const.sn.scriptIdx', this.aCallStk[0].idx); } this.mark = { hSave : this.val.cloneSave(), hPages : this.layMng.record(), aIfStk : this.aIfStk.slice(len), }; return false; } // しおりの保存 private save(hArg: HArg) { const place = hArg.place; if (! place) throw 'placeは必須です'; delete hArg.タグ名; delete hArg.place; hArg.text = (hArg.text ?? '').replace(/^(<br\/>)+/, ''); this.mark.json = hArg; this.val.setMark(place, this.mark); const now_sp = Number(this.val.getVal('sys:const.sn.save.place')); if (place === now_sp) this.val.setVal_Nochk('sys', 'const.sn.save.place', now_sp +1); return false; } }