diff --git a/generated/enums.d.ts b/generated/enums.d.ts index 40e872a..b95d894 100644 --- a/generated/enums.d.ts +++ b/generated/enums.d.ts @@ -32,4 +32,5 @@ export type enums = { ][], } | null unknown_83?: true | null + unknown_d1?: true | null }; diff --git a/generated/interfaces.d.ts b/generated/interfaces.d.ts index 36ea20d..2759968 100644 --- a/generated/interfaces.d.ts +++ b/generated/interfaces.d.ts @@ -16,7 +16,7 @@ export type interfaces = { aspectxtype: number, aspectytype: number, parentid: number, - flag: number, + hidden: number, containerdata: { layerwidth: number, layerheight: number, diff --git a/src/clientscript/codeparser.ts b/src/clientscript/codeparser.ts index c3625e7..6b2b265 100644 --- a/src/clientscript/codeparser.ts +++ b/src/clientscript/codeparser.ts @@ -811,7 +811,7 @@ globalThis.testy = async () => { if (!parseresult.success) { return parseresult; } let roundtripped = astToImJson(deob, parseresult.result); let inter = new ClientScriptInterpreter(deob);; - inter.callscript(roundtripped); + inter.callscript(roundtripped, fileid); globalThis.inter = inter; let jsondata = JSON.parse(originaljson); diff --git a/src/clientscript/interpreter.ts b/src/clientscript/interpreter.ts index 2fa0466..5cc2783 100644 --- a/src/clientscript/interpreter.ts +++ b/src/clientscript/interpreter.ts @@ -1,10 +1,18 @@ import { clientscript } from "../../generated/clientscript" import { ClientscriptObfuscation } from "./callibrator"; -import { ClientScriptOp, StackDiff, StackList, SwitchJumpTable, branchInstructions, branchInstructionsInt, getOpName, getParamOps, knownClientScriptOpNames, longBigIntToJson, longJsonToBigInt, namedClientScriptOps, typeToPrimitive } from "./definitions" +import { ClientScriptOp, StackDiff, StackList, SwitchJumpTable, branchInstructions, getParamOps, knownClientScriptOpNames, longBigIntToJson, longJsonToBigInt, namedClientScriptOps, typeToPrimitive } from "./definitions" import { rs3opnames } from "./opnames"; +import type { RsInterFaceTypes, RsInterfaceComponent, RsInterfaceDomTree, TypedRsInterFaceComponent, UiRenderContext } from "../scripts/renderrsinterface"; +import { cacheMajors } from "../constants"; +import { parse } from "../opdecoder"; +import { CacheFileSource } from "../cache"; + +const MAGIC_CONST_CURRENTCOMP = 0x80000003 | 0; + type ScriptScope = { - index: 0; + scriptid: number; + index: number; ops: ClientScriptOp[]; switches: SwitchJumpTable[]; localints: number[]; @@ -13,6 +21,7 @@ type ScriptScope = { } type InterpreterWithScope = ClientScriptInterpreter & { scope: ScriptScope }; +type OpImplementation = (inter: InterpreterWithScope, op: ClientScriptOp) => void | Promise; export class ClientScriptInterpreter { intstack: number[] = []; @@ -22,11 +31,39 @@ export class ClientScriptInterpreter { calli: ClientscriptObfuscation; mockscripts = new Map(); scope: ScriptScope | null = null; - constructor(calli: ClientscriptObfuscation) { + activecompid = -1; + stalled: Promise | void = undefined; + uictx: UiRenderContext | null = null; + constructor(calli: ClientscriptObfuscation, uictx: UiRenderContext | null = null) { this.calli = calli; + this.uictx = uictx; } - callscript(script: clientscript) { + getComponent(compid: number, type = "any" as T): TypedRsInterFaceComponent { + if ((compid | 0) == MAGIC_CONST_CURRENTCOMP) { compid = this.activecompid; } + if (!this.uictx) { throw new Error("tried to run ui op without ui"); }//TODO implement with nops? + let comp = this.uictx.comps.get(compid); + if (!comp) { throw new Error("component doesn't exist"); } + if (type != "any" && !comp.isCompType(type)) { throw new Error(`${type} component expected`); } + this.uictx.touchedComps.add(comp); + return comp as TypedRsInterFaceComponent; + } + async callscriptid(id: number) { + let data = await this.calli.source.getFileById(cacheMajors.clientscript, id); + this.callscript(parse.clientscript.read(data, this.calli.source), id); + } + async runToEnd() { + while (true) { + let res = this.next(); + if (res instanceof Promise) { + res = await res; + } + if (!res) { break; } + } + } + callscript(script: clientscript, scriptid: number) { + this.log(`calling script ${scriptid}`); this.scope = { + scriptid: scriptid, index: 0, ops: script.opcodedata, switches: script.switches, @@ -34,17 +71,29 @@ export class ClientScriptInterpreter { locallongs: new Array(script.locallongcount).fill(0n), localstrings: new Array(script.localstringcount).fill("") }; + for (let i = script.intargcount - 1; i >= 0; i--) { this.scope.localints[i] = this.popint(); } + for (let i = script.longargcount - 1; i >= 0; i--) { this.scope.locallongs[i] = this.poplong(); } + for (let i = script.stringargcount - 1; i >= 0; i--) { this.scope.localstrings[i] = this.popstring(); } this.scopeStack.push(this.scope); } + log(text: string) { + console.log(`CS2: ${" ".repeat(this.scopeStack.length)} ${text}`); + } pushStackdiff(diff: StackDiff) { - if (diff.vararg != 0) { throw new Error("vararg not supported"); } + if (diff.vararg != 0) { throw new Error("cannot push vararg"); } for (let i = 0; i < diff.int; i++) { this.pushint(0); } for (let i = 0; i < diff.long; i++) { this.pushlong(0n); } for (let i = 0; i < diff.string; i++) { this.pushstring(""); } } popStackdiff(diff: StackDiff) { - if (diff.vararg != 0) { throw new Error("vararg not supported"); } + if (diff.vararg != 0) { + let str = this.popstring(); + for (let i = str.match(/Y/g)?.length ?? 0; i > 0; i--) { for (let ii = this.popint(); ii > 0; ii--) { this.popint(); } } + for (let i = str.match(/i/g)?.length ?? 0; i > 0; i--) { this.popint(); } + for (let i = str.match(/l/g)?.length ?? 0; i > 0; i--) { this.poplong(); } + for (let i = str.match(/s/g)?.length ?? 0; i > 0; i--) { this.popstring(); } + } for (let i = 0; i < diff.int; i++) { this.popint(); } for (let i = 0; i < diff.long; i++) { this.poplong(); } for (let i = 0; i < diff.string; i++) { this.popstring(); } @@ -76,10 +125,19 @@ export class ClientScriptInterpreter { if (this.stringstack.length == 0) { throw new Error(`tried to pop string while none are on stack at index ${(this.scope?.index ?? 0) - 1}`); } return this.stringstack.pop()!; } + pushlist(list: (number | bigint | string)[]) { + for (let v of list) { + if (typeof v == "number") { this.pushint(v); } + else if (typeof v == "bigint") { this.pushlong(v); } + else if (typeof v == "string") { this.pushstring(v); } + else { throw new Error("unexpected"); } + } + } pushint(v: number) { this.intstack.push(v); } pushlong(v: bigint) { this.longstack.push(v); } pushstring(v: string) { this.stringstack.push(v); } - next() { + next(): boolean | Promise { + if (this.stalled) { return this.stalled = this.stalled.then(res => res && this.next()); } if (!this.scope) { throw new Error("no script"); } if (this.scope.index < 0 || this.scope.index >= this.scope.ops.length) { throw new Error("jumped out of bounds"); @@ -96,12 +154,13 @@ export class ClientScriptInterpreter { } } } + let res: Promise | void = undefined; if (op.opcode == namedClientScriptOps.return) { - this.scope = null; - //TODO pop scope stack - return true; + this.scopeStack.pop(); + this.scope = this.scopeStack.at(-1) ?? null; + return !!this.scope; } else if (implemented) { - implemented(this as InterpreterWithScope, op); + res = implemented(this as InterpreterWithScope, op); } else { let opinfo = this.calli.decodedMappings.get(op.opcode); if (!opinfo) { throw new Error(`Uknown op with opcode ${op.opcode}`); } @@ -110,7 +169,15 @@ export class ClientScriptInterpreter { this.pushStackdiff(opinfo.stackinfo.out.toStackDiff()); } - return false; + if (res instanceof Promise) { + this.stalled = res.then(() => { + this.stalled = undefined; + return true; + }); + return this.stalled; + } else { + return true; + } } dump() { let res = ""; @@ -123,8 +190,9 @@ export class ClientScriptInterpreter { res += `${this.longstack.join(",")}\n`; res += `${this.stringstack.map(q => `"${q}"`).join(",")}\n`; if (this.scope) { - for (let i = 0; i < 10; i++) { + for (let i = -5; i < 10; i++) { let index = this.scope.index + i; + if (index < 0 || index >= this.scope.ops.length) { continue; } res += `${index} ${index == this.scope.index ? ">>" : " "} `; let op = this.scope.ops[index]; if (op) { @@ -177,21 +245,56 @@ function getParamOp(inter: ClientScriptInterpreter, op: ClientScriptOp) { if (outprim == "string") { inter.pushstring(""); } } } +async function loadEnum(source: CacheFileSource, id: number) { + return parse.enums.read(await source.getFileById(cacheMajors.enums, id), source); +} -const implementedops = new Map void>(); +const implementedops = new Map(); branchInstructions.forEach(id => implementedops.set(id, branchOp)); getParamOps.forEach(id => implementedops.set(id, getParamOp)); -implementedops.set(namedClientScriptOps.enum_getvalue, inter => { + +implementedops.set(namedClientScriptOps.gosub, (inter, op) => { + let mockreturn = inter.mockscripts.get(op.imm); + if (mockreturn) { + let func = inter.calli.scriptargs.get(op.imm); + if (!func) { throw new Error(`calling unknown clientscript ${op.imm}`); } + inter.log(`CS2 - calling sub ${op.imm}${mockreturn ? ` with mocked return value: ${mockreturn}` : ""}`); + inter.popStackdiff(func.stack.in.toStackDiff()); + for (let val of mockreturn) { + if (typeof val == "number") { inter.pushint(val); } + if (typeof val == "bigint") { inter.pushlong(val); } + if (typeof val == "string") { inter.pushstring(val); } + } + } else { + // inter.pushStackdiff(func.stack.out.toStackDiff()); + return inter.callscriptid(op.imm); + } +}); +implementedops.set(namedClientScriptOps.enum_getvalue, async inter => { let key = inter.popint(); let enumid = inter.popint(); let outtype = inter.popint(); let keytype = inter.popint(); let outprim = typeToPrimitive(outtype); - if (outprim == "int") { inter.pushint(0); } - if (outprim == "long") { inter.pushlong(0n); } - if (outprim == "string") { inter.pushstring(""); } + let enumjson = await loadEnum(inter.calli.source, enumid); + + if (outprim != "int") { throw new Error("enum_getvalue can only look up int values"); } + //TODO probably need -1 default if subtype type isn't simple int + let res = (enumjson.intArrayValue1 ?? enumjson.intArrayValue2?.values)?.find(q => q[0] == key)?.[1] ?? enumjson.intValue ?? 0; + inter.pushint(res); +}); +implementedops.set(namedClientScriptOps.struct_getparam, async inter => { + let param = inter.popint(); + let structid = inter.popint(); + + let file = await inter.calli.source.getFileById(cacheMajors.structs, structid); + let json = parse.structs.read(file, inter.calli.source); + let match = json.extra?.find(q => q.prop == param); + if (!match) { inter.pushint(-1); }//TODO should probly be 0 if subtype is plain int + else if (match.intvalue == undefined) { throw new Error("param is not of type int"); } + else { inter.pushint(match.intvalue); } }); implementedops.set(namedClientScriptOps.dbrow_getfield, inter => { @@ -212,23 +315,6 @@ implementedops.set(namedClientScriptOps.joinstring, (inter, op) => { inter.pushstring(new Array(op.imm).fill("").map(q => inter.popstring()).reverse().join("")); }); -implementedops.set(namedClientScriptOps.gosub, (inter, op) => { - let func = inter.calli.scriptargs.get(op.imm); - let mockreturn = inter.mockscripts.get(op.imm); - if (!func) { throw new Error(`calling unknown clientscript ${op.imm}`); } - inter.popStackdiff(func.stack.in.toStackDiff()); - console.log(`CS2 - calling sub ${op.imm}${mockreturn ? ` with mocked return value: ${mockreturn}` : ""}`); - if (mockreturn) { - for (let val of mockreturn) { - if (typeof val == "number") { inter.pushint(val); } - if (typeof val == "bigint") { inter.pushlong(val); } - if (typeof val == "string") { inter.pushstring(val); } - } - } else { - inter.pushStackdiff(func.stack.out.toStackDiff()); - } -}); - implementedops.set(namedClientScriptOps.pushconst, (inter, op) => { if (op.imm == 0) { if (typeof op.imm_obj != "number") { throw new Error("expected imm_obj to be number in pushconst int"); } @@ -280,7 +366,7 @@ implementedops.set(namedClientScriptOps.poplocalstring, (inter, op) => { if (op.imm >= inter.scope.localstrings.length) { throw new Error("invalid poplocalstring"); } inter.scope.localstrings[op.imm] = inter.popstring(); }); -implementedops.set(namedClientScriptOps.printmessage, inter => console.log(`CS2: ${inter.popstring()}`)); +implementedops.set(namedClientScriptOps.printmessage, inter => inter.log(`>> ${inter.popstring()}`)); implementedops.set(namedClientScriptOps.inttostring, inter => inter.pushstring(inter.popdeep(1).toString(inter.popdeep(0)))); implementedops.set(namedClientScriptOps.strcmp, inter => { let right = inter.popstring(); @@ -299,7 +385,7 @@ implementedops.set(namedClientScriptOps.popvar, (inter, op) => { }); -const namedimplementations = new Map void>(); +const namedimplementations = new Map(); namedimplementations.set("STRING_LENGTH", inter => inter.pushint(inter.popstring().length)); namedimplementations.set("SUBSTRING", inter => inter.pushstring(inter.popstring().substring(inter.popdeep(1), inter.popdeep(0)))); namedimplementations.set("STRING_INDEXOF_STRING", inter => inter.pushint(inter.popdeepstr(1).indexOf(inter.popdeepstr(0), inter.popint()))); @@ -314,9 +400,59 @@ namedimplementations.set("AND", inter => inter.pushint(inter.popint() & inter.po namedimplementations.set("OR", inter => inter.pushint(inter.popint() | inter.popint())); namedimplementations.set("LOWERCASE", inter => inter.pushstring(inter.popstring().toLowerCase())); namedimplementations.set("LONG_UNPACK", inter => { let long = longBigIntToJson(inter.poplong()); inter.pushint(long[0] >> 0); inter.pushint(long[1] >> 0); }); -namedimplementations.set("MES_TYPED", inter => console.log(`CS2: ${inter.popint()} ${inter.popint()} ${inter.popstring()}`)); +namedimplementations.set("MES_TYPED", inter => inter.log(`>> ${inter.popint()} ${inter.popint()} ${inter.popstring()}`)); namedimplementations.set("LONG_ADD", inter => inter.pushlong(inter.popdeeplong(1) + inter.popdeeplong(0))); namedimplementations.set("LONG_SUB", inter => inter.pushlong(inter.popdeeplong(1) - inter.popdeeplong(0))); namedimplementations.set("TOSTRING_LONG", inter => inter.pushstring(inter.poplong().toString())); namedimplementations.set("INT_TO_LONG", inter => inter.pushlong(BigInt(inter.popint()))); -namedimplementations.set("OPENURLRAW", inter => console.log("CS2 OPENURLRAW:", inter.popint(), inter.popstring())); \ No newline at end of file +namedimplementations.set("OPENURLRAW", inter => inter.log(`CS2 OPENURLRAW: ${inter.popint()}, ${inter.popstring()}`)); + +namedimplementations.set("ENUM_GETOUTPUTCOUNT", async inter => { + let json = await loadEnum(inter.calli.source, inter.popint()); + inter.pushint((json.intArrayValue1 ?? json.intArrayValue2?.values ?? json.stringArrayValue1 ?? json.stringArrayValue2?.values)?.length ?? 0); +}); + +namedimplementations.set("IF_SETHIDE", inter => { inter.getComponent(inter.popint()).data.hidden = inter.popint(); }); +namedimplementations.set("IF_GETHEIGHT", inter => inter.pushint(inter.getComponent(inter.popint()).data.baseheight)); +namedimplementations.set("IF_GETWIDTH", inter => inter.pushint(inter.getComponent(inter.popint()).data.basewidth)); +namedimplementations.set("IF_GETX", inter => inter.pushint(inter.getComponent(inter.popint()).data.baseposx)); +namedimplementations.set("IF_GETY", inter => inter.pushint(inter.getComponent(inter.popint()).data.baseposy)); +namedimplementations.set("IF_SETOP", inter => { inter.getComponent(inter.popint()).data.rightclickopts[inter.popint()] = inter.popstring(); }); +namedimplementations.set("IF_GETHIDE", inter => inter.pushint(inter.getComponent(inter.popint()).data.hidden)); + +namedimplementations.set("IF_GETTEXT", inter => inter.pushstring(inter.getComponent(inter.popint(), "text").data.textdata.text)); +namedimplementations.set("IF_GETTEXTSHADOW", inter => inter.pushint(+inter.getComponent(inter.popint(), "text").data.textdata.shadow)); +namedimplementations.set("IF_SETTEXT", inter => { inter.getComponent(inter.popint(), "text").data.textdata.text = inter.popstring(); }) +namedimplementations.set("IF_SETTEXTSHADOW", inter => { inter.getComponent(inter.popint(), "text").data.textdata.shadow = !!inter.popint(); }) +//TODO not sure which 3 props are targeted +namedimplementations.set("IF_SETTEXTALIGN", inter => { let data = inter.getComponent(inter.popint(), "text").data.textdata; data.alignhor = inter.popint(); data.alignver = inter.popint(); data.multiline = inter.popint(); }); + +namedimplementations.set("IF_GETGRAPHIC", inter => inter.pushint(inter.getComponent(inter.popint(), "sprite").data.spritedata.spriteid)); +namedimplementations.set("IF_GETHFLIP", inter => inter.pushint(+inter.getComponent(inter.popint(), "sprite").data.spritedata.hflip)); +namedimplementations.set("IF_GETVFLIP", inter => inter.pushint(+inter.getComponent(inter.popint(), "sprite").data.spritedata.vflip)); +namedimplementations.set("IF_SETGRAPHIC", inter => { inter.getComponent(inter.popint(), "sprite").data.spritedata.spriteid = inter.popint(); }); +namedimplementations.set("IF_SETHFLIP", inter => { inter.getComponent(inter.popint(), "sprite").data.spritedata.hflip = !!inter.popint(); }); +namedimplementations.set("IF_SETVFLIP", inter => { inter.getComponent(inter.popint(), "sprite").data.spritedata.vflip = !!inter.popint(); }); + +namedimplementations.set("IF_SETMODEL", inter => { inter.getComponent(inter.popint(), "model").data.modeldata.modelid = inter.popint(); }); + +namedimplementations.set("IF_GETTRANS", inter => inter.pushint(inter.getComponent(inter.popint(), "figure").data.figuredata.trans)); +namedimplementations.set("IF_GETCOLOUR", inter => inter.pushint(inter.getComponent(inter.popint(), "figure").data.figuredata.color)); +namedimplementations.set("IF_GETFILLED", inter => inter.pushint(inter.getComponent(inter.popint(), "figure").data.figuredata.filled)); +namedimplementations.set("IF_SETTRANS", inter => { inter.getComponent(inter.popint(), "figure").data.figuredata.trans = inter.popint(); }); +namedimplementations.set("IF_SETCOLOUR", inter => { inter.getComponent(inter.popint(), "figure").data.figuredata.color = inter.popint(); }); +namedimplementations.set("IF_SETFill", inter => { inter.getComponent(inter.popint(), "figure").data.figuredata.filled = inter.popint(); }); +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) +// namedimplementations.set("xxxxx", inter => xxxx) diff --git a/src/clientscript/jsonwriter.ts b/src/clientscript/jsonwriter.ts index 6930fc8..1fd588d 100644 --- a/src/clientscript/jsonwriter.ts +++ b/src/clientscript/jsonwriter.ts @@ -67,6 +67,7 @@ intrinsics.set("varbittable", { in: new StackList(), out: new StackList(["string"]), write(ctx: OpcodeWriterContext) { + //it think all of this might be obsolete because of VAR_REFERENCE_GET let body: ClientScriptOp[] = []; let lookupstr = ","; for (let [id, meta] of ctx.calli.varbitmeta) { diff --git a/src/constants.ts b/src/constants.ts index 403fca4..a295512 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -68,6 +68,7 @@ export const cacheConfigPages = { mapscenes: 34, maplabels: 36, dbtables: 40, + dbrows: 41, varplayer: 60, varnpc: 61, diff --git a/src/opcodes/enums.json b/src/opcodes/enums.json index 609ae0c..284945e 100644 --- a/src/opcodes/enums.json +++ b/src/opcodes/enums.json @@ -9,5 +9,6 @@ "0x06": { "name": "intArrayValue1", "read": ["array","unsigned short",["tuple","unsigned int","int"]] }, "0x07": { "name": "stringArrayValue2", "read": ["struct",["max","unsigned short"],["values",["array","unsigned short",["tuple","unsigned short","string"]]]] }, "0x08": { "name": "intArrayValue2", "read": ["struct",["max","unsigned short"],["values",["array","unsigned short",["tuple","unsigned short","int"]]]] }, - "0x83": { "name": "unknown_83", "read": "true" } + "0x83": { "name": "unknown_83", "read": "true" }, + "0xd1": { "name": "unknown_d1", "read": "true" } } \ No newline at end of file diff --git a/src/opcodes/interfaces.jsonc b/src/opcodes/interfaces.jsonc index a38e4e7..9862223 100644 --- a/src/opcodes/interfaces.jsonc +++ b/src/opcodes/interfaces.jsonc @@ -13,7 +13,7 @@ ["aspectxtype","byte"], ["aspectytype","byte"], ["parentid","ushort"], - ["flag","ubyte"], + ["hidden","ubyte"], ["containerdata",["opt","type=0",["struct", ["layerwidth","ushort"], ["layerheight","ushort"], diff --git a/src/opdecoder.ts b/src/opdecoder.ts index f9fd49d..2d864a3 100644 --- a/src/opdecoder.ts +++ b/src/opdecoder.ts @@ -135,6 +135,7 @@ function allParsers() { clientscript: FileParser.fromJson(require("./opcodes/clientscript.jsonc")), clientscriptdata: FileParser.fromJson(require("./opcodes/clientscriptdata.jsonc")), interfaces: FileParser.fromJson(require("./opcodes/interfaces.jsonc")), - dbtables: FileParser.fromJson(require("./opcodes/dbtables.jsonc")) + dbtables: FileParser.fromJson(require("./opcodes/dbtables.jsonc")), + dbrows: FileParser.fromJson(require("./opcodes/dbrows.jsonc")) } } \ No newline at end of file diff --git a/src/scripts/filetypes.ts b/src/scripts/filetypes.ts index a4419d9..8fdc85e 100644 --- a/src/scripts/filetypes.ts +++ b/src/scripts/filetypes.ts @@ -601,6 +601,7 @@ export const cacheFileJsonModes = constrainedMap()({ quickchatcats: { parser: parse.quickchatCategories, lookup: singleMinorIndex(cacheMajors.quickchat, 0) }, quickchatlines: { parser: parse.quickchatLines, lookup: singleMinorIndex(cacheMajors.quickchat, 1) }, dbtables: { parser: parse.dbtables, lookup: singleMinorIndex(cacheMajors.config, cacheConfigPages.dbtables) }, + dbrows: { parser: parse.dbrows, lookup: singleMinorIndex(cacheMajors.config, cacheConfigPages.dbrows) }, overlays: { parser: parse.mapsquareOverlays, lookup: singleMinorIndex(cacheMajors.config, cacheConfigPages.mapoverlays) }, identitykit: { parser: parse.identitykit, lookup: singleMinorIndex(cacheMajors.config, cacheConfigPages.identityKit) }, diff --git a/src/scripts/renderrsinterface.ts b/src/scripts/renderrsinterface.ts index 3d94337..627dcc4 100644 --- a/src/scripts/renderrsinterface.ts +++ b/src/scripts/renderrsinterface.ts @@ -3,6 +3,8 @@ import { RSModel } from "../3d/modelnodes"; import { EngineCache, ThreejsSceneCache } from "../3d/modeltothree"; import { expandSprite, parseSprite } from "../3d/sprite"; import { CacheFileSource } from "../cache"; +import { prepareClientScript } from "../clientscript"; +import { ClientScriptInterpreter } from "../clientscript/interpreter"; import { cacheMajors } from "../constants"; import { makeImageData, pixelsToDataUrl } from "../imgutils"; import { parse } from "../opdecoder"; @@ -11,19 +13,26 @@ import { UiCameraParams, updateItemCamera } from "../viewer/scenenodes"; import { ThreeJsRenderer } from "../viewer/threejsrender"; type HTMLResult = string; -export type RsInterfaceElement = { el: HTMLElement, rootcomps: RsInterfaceComponent[] }; +export type RsInterfaceDomTree = { + el: HTMLDivElement; + rootcomps: RsInterfaceComponent[]; + interfaceid: number; + dispose: () => void; +} export class UiRenderContext { source: CacheFileSource; sceneCache: ThreejsSceneCache | null = null; renderer: ThreeJsRenderer | null = null; - comps = new Map(); + comps = new Map(); highlightstack: HTMLElement[] = []; + interpreterprom: Promise | null = null; + touchedComps = new Set(); constructor(source: CacheFileSource) { this.source = source; } toggleHighLightComp(subid: number, highlight: boolean) { - let comp = this.comps.get(subid); + let comp = this.comps.get(subid)?.element; if (comp) { if (highlight) { if (this.highlightstack.length != 0) { @@ -42,6 +51,17 @@ export class UiRenderContext { } } } + async runClientScriptCallback(compid: number, cbdata: (number | string)[]) { + if (cbdata.length == 0) { return; } + let inter = await (this.interpreterprom ??= prepareClientScript(this.source).then(q => new ClientScriptInterpreter(q, this))); + if (typeof cbdata[0] != "number") { throw new Error("expected callback script id but got string"); } + + inter.pushlist(cbdata.slice(1)); + inter.activecompid = compid; + await inter.callscriptid(cbdata[0]); + await inter.runToEnd(); + // console.log(await renderClientScript(p.source, await p.source.getFileById(cacheMajors.clientscript, callbackid), callbackid)) + } } function rsInterfaceStyleSheet() { @@ -76,7 +96,11 @@ export async function loadRsInterfaceData(ctx: UiRenderContext, id: number) { for (let sub of arch) { try { - comps.set(sub.fileid, new RsInterfaceComponent(parse.interfaces.read(sub.buffer, ctx.source), sub.fileid)) + let compid = (id << 16) | sub.fileid; + if (ctx.comps.has(compid)) { throw new Error("ui render context already had comp with same id"); } + let comp = new RsInterfaceComponent(ctx, parse.interfaces.read(sub.buffer, ctx.source), compid); + comps.set(sub.fileid, comp); + ctx.comps.set(compid, comp); } catch (e) { console.log(`failed to parse interface ${id}:${sub.fileid}`); } @@ -101,14 +125,14 @@ export async function loadRsInterfaceData(ctx: UiRenderContext, id: number) { } let basewidth = 520; let baseheight = 340; - return { comps, rootcomps, basewidth, baseheight }; + return { comps, rootcomps, basewidth, baseheight, id }; } export async function renderRsInterfaceHTML(ctx: UiRenderContext, id: number): Promise { let { comps, rootcomps, basewidth, baseheight } = await loadRsInterfaceData(ctx, id); let html = ""; for (let comp of rootcomps) { - html += await comp.toHtmlllllll(ctx); + html += await comp.toHtml(ctx); } let doc = `\n`; doc += `\n` @@ -131,7 +155,7 @@ export async function renderRsInterfaceHTML(ctx: UiRenderContext, id: number): P return doc as any; } -export function renderRsInterfaceDOM(ctx: UiRenderContext, data: Awaited>) { +export function renderRsInterfaceDOM(ctx: UiRenderContext, data: Awaited>): RsInterfaceDomTree { let root = document.createElement("div"); root.classList.add("rs-interface-container"); let style = document.createElement("style"); @@ -143,7 +167,6 @@ export function renderRsInterfaceDOM(ctx: UiRenderContext, data: Awaited { data.rootcomps.forEach(q => q.dispose()); } - return { el: root, rootcomps: data.rootcomps, dispose }; + return { el: root, rootcomps: data.rootcomps, interfaceid: data.id, dispose }; } function cssColor(col: number) { @@ -311,25 +334,48 @@ async function spritePromise(ctx: UiRenderContext, spriteid: number) { return { imgcss, spriteid }; } +export type RsInterFaceTypes = "text" | "sprite" | "container" | "model" | "figure"; + +export type TypedRsInterFaceComponent = RsInterfaceComponent & { + data: { + containerdata: T extends "container" ? {} : unknown, + spritedata: T extends "sprite" ? {} : unknown, + textdata: T extends "text" ? {} : unknown, + modeldata: T extends "model" ? {} : unknown, + figuredata: T extends "figure" ? {} : unknown + } +} + export class RsInterfaceComponent { + ctx: UiRenderContext; data: interfaces; parent: RsInterfaceComponent | null = null; children: RsInterfaceComponent[] = []; - subid: number; + compid: number; modelrenderer: ReturnType | null = null; spriteChild: HTMLDivElement | null = null; loadingSprite = -1; element: HTMLElement | null = null; - constructor(interfacedata: interfaces, subid: number) { + constructor(ctx: UiRenderContext, interfacedata: interfaces, compid: number) { + this.ctx = ctx; this.data = interfacedata; - this.subid = subid; + this.compid = compid; } - async toHtmlllllll(ctx: UiRenderContext) { + isCompType(type: T): this is TypedRsInterFaceComponent { + if (type == "container" && !this.data.containerdata) { return false; } + if (type == "model" && !this.data.modeldata) { return false; } + if (type == "sprite" && !this.data.spritedata) { return false; } + if (type == "text" && !this.data.textdata) { return false; } + if (type == "figure" && !this.data.figuredata) { return false; } + return true; + } + + async toHtml(ctx: UiRenderContext) { let { style, title } = this.getStyle(); let childhtml = ""; for (let child of this.children) { - childhtml += await child.toHtmlllllll(ctx); + childhtml += await child.toHtml(ctx); } if (this.data.textdata) { childhtml += rsmarkupToSafeHtml(this.data.textdata.text); @@ -346,13 +392,14 @@ export class RsInterfaceComponent { childhtml += `
`; } let html = ""; - html += `
\n`; + html += `
\n`; html += childhtml; html += "
\n"; return html as HTMLResult as any; } dispose() { + this.ctx.comps.delete(this.compid); this.modelrenderer?.dispose(); this.element?.remove(); this.children.forEach(q => q.dispose()); @@ -360,46 +407,46 @@ export class RsInterfaceComponent { initDom(ctx: UiRenderContext) { let el = document.createElement("div"); - this.updateDom(ctx, el); + this.element = el; + this.updateDom(); this.children.forEach(child => { el.appendChild(child.initDom(ctx)); }); (el as any).ui = this.data; el.classList.add("rs-component"); - ctx.comps.set(this.subid, el); - this.element = el; return el; } - updateDom(ctx: UiRenderContext, el: HTMLDivElement) { + updateDom() { + if (!this.element) { throw new Error("element not set"); } let { style, title } = this.getStyle(); if (this.data.modeldata) { let isplaceholder = this.data.modeldata.modelid == 0x7fff || this.data.modeldata.modelid == 0xffff; - if (!isplaceholder && ctx.renderer && ctx.sceneCache) { - this.modelrenderer ??= uiModelRenderer(ctx.renderer, ctx.sceneCache, this.data.modeldata.positiondata!); + if (!isplaceholder && this.ctx.renderer && this.ctx.sceneCache) { + this.modelrenderer ??= uiModelRenderer(this.ctx.renderer, this.ctx.sceneCache, this.data.modeldata.positiondata!); this.modelrenderer.setmodel(this.data.modeldata.modelid); this.modelrenderer.setanim(this.data.modeldata.animid); - el.appendChild(this.modelrenderer.canvas); + this.element.appendChild(this.modelrenderer.canvas); } else if (this.modelrenderer) { this.modelrenderer.dispose(); this.modelrenderer = null; style += "background:rgba(0,255,0,0.5);outline:blue;"; - el.innerText = (isplaceholder ? "placeholder" : ""); + this.element.innerText = (isplaceholder ? "placeholder" : ""); } } if (this.data.textdata) { - el.insertAdjacentHTML("beforeend", rsmarkupToSafeHtml(this.data.textdata.text)); + this.element.insertAdjacentHTML("beforeend", rsmarkupToSafeHtml(this.data.textdata.text)); } if (this.data.spritedata) { if (this.loadingSprite != this.data.spritedata.spriteid) { if (!this.spriteChild) { this.spriteChild = document.createElement("div"); - el.appendChild(this.spriteChild); + this.element.appendChild(this.spriteChild); this.spriteChild.classList.add("rs-image"); } this.spriteChild.style.cssText = spriteCss(this.data.spritedata); this.spriteChild.classList.toggle("rs-image--cover", !this.data.spritedata.flag2); - spritePromise(ctx, this.data.spritedata.spriteid).then(({ imgcss, spriteid }) => { + spritePromise(this.ctx, this.data.spritedata.spriteid).then(({ imgcss, spriteid }) => { if (this.spriteChild && spriteid == this.data.spritedata?.spriteid) { this.spriteChild.style.backgroundImage = imgcss; } @@ -410,8 +457,8 @@ export class RsInterfaceComponent { this.spriteChild.remove(); this.spriteChild = null; } - el.style.cssText = style; - el.title = title; + this.element.style.cssText = style; + this.element.title = title; } getStyle() { diff --git a/src/viewer/cs2viewer.tsx b/src/viewer/cs2viewer.tsx index 2a698ed..c147259 100644 --- a/src/viewer/cs2viewer.tsx +++ b/src/viewer/cs2viewer.tsx @@ -21,7 +21,7 @@ export function ClientScriptViewer(p: { data: string }) { if (!calli) { return null!; }//force non-null here to make typescript shut up about it being null in non-reachable callbacks let script: clientscript = JSON.parse(p.data); let inter = new ClientScriptInterpreter(calli); - inter.callscript(script); + inter.callscript(script, -1); return inter; }, [calli, resetcounter, p.data]); diff --git a/src/viewer/rsuiviewer.tsx b/src/viewer/rsuiviewer.tsx index c7a2576..3f3ee22 100644 --- a/src/viewer/rsuiviewer.tsx +++ b/src/viewer/rsuiviewer.tsx @@ -1,15 +1,16 @@ import * as React from "react"; -import { RsInterfaceComponent, RsInterfaceElement, UiRenderContext, loadRsInterfaceData, renderRsInterfaceDOM } from "../scripts/renderrsinterface"; +import { RsInterfaceComponent, RsInterfaceDomTree, UiRenderContext, loadRsInterfaceData, renderRsInterfaceDOM } from "../scripts/renderrsinterface"; import { DomWrap } from "./scriptsui"; import type { ThreejsSceneCache } from "../3d/modeltothree"; import { ThreeJsRenderer } from "./threejsrender"; import { interfaces } from "../../generated/interfaces"; -import { renderClientScript } from "../clientscript"; +import { prepareClientScript, renderClientScript } from "../clientscript"; import { CacheFileSource } from "../cache"; import { cacheMajors } from "../constants"; +import { ClientScriptInterpreter } from "../clientscript/interpreter"; export function RsUIViewer(p: { data: string }) { - let [ui, setui] = React.useState(null); + let [ui, setui] = React.useState(null); let scene: ThreejsSceneCache = globalThis.sceneCache;//TODO pass this properly using args let render: ThreeJsRenderer = globalThis.render;//TODO let ctx = React.useMemo(() => { @@ -47,15 +48,16 @@ export function RsUIViewer(p: { data: string }) { function RsInterfaceDebugger(p: { ctx: UiRenderContext, comp: RsInterfaceComponent, source: CacheFileSource }) { let data = p.comp.data; let mouseevent = React.useCallback((e: React.MouseEvent) => { - p.ctx.toggleHighLightComp(p.comp.subid, e.type == "mouseenter"); + p.ctx.toggleHighLightComp(p.comp.compid, e.type == "mouseenter"); }, [p.ctx, p.comp]); + return (
e.target == e.currentTarget && console.log(p.comp)}> - id={p.comp.subid} t={data.type} {data.textdata?.text ?? "no text"} + id={p.comp.compid & 0xffff} t={data.type} {data.textdata?.text ?? "no text"}
{data.spritedata && "sprite: " + data.spritedata.spriteid} {data.modeldata && "model: " + data.modeldata.modelid} - +
{p.comp.children.map((q, i) => )} @@ -64,15 +66,15 @@ function RsInterfaceDebugger(p: { ctx: UiRenderContext, comp: RsInterfaceCompone ) } -function CallbackDebugger(p: { data: interfaces, source: CacheFileSource }) { +function CallbackDebugger(p: { ctx: UiRenderContext, comp: RsInterfaceComponent, source: CacheFileSource }) { return (
- {Object.entries(p.data.scripts).filter(q => q[1] && q[1].length != 0).map(([key, v]) => { + {Object.entries(p.comp.data.scripts).filter(q => q[1] && q[1].length != 0).map(([key, v]) => { if (!v) { throw new Error("unexpected"); } if (typeof v[0] != "number") { throw new Error("unexpected") } let callbackid = v[0]; return ( -
console.log(await renderClientScript(p.source, await p.source.getFileById(cacheMajors.clientscript, callbackid), callbackid))}> +
p.ctx.runClientScriptCallback(p.comp.compid, v)}> {key} {callbackid}({v.slice(1).map(q => typeof q == "string" ? `"${q}"` : q).join(",")})
)