mirror of
https://github.com/skillbert/rsmv.git
synced 2025-12-23 21:47:48 -05:00
ui script interpreter
This commit is contained in:
1
generated/enums.d.ts
vendored
1
generated/enums.d.ts
vendored
@@ -32,4 +32,5 @@ export type enums = {
|
||||
][],
|
||||
} | null
|
||||
unknown_83?: true | null
|
||||
unknown_d1?: true | null
|
||||
};
|
||||
|
||||
2
generated/interfaces.d.ts
vendored
2
generated/interfaces.d.ts
vendored
@@ -16,7 +16,7 @@ export type interfaces = {
|
||||
aspectxtype: number,
|
||||
aspectytype: number,
|
||||
parentid: number,
|
||||
flag: number,
|
||||
hidden: number,
|
||||
containerdata: {
|
||||
layerwidth: number,
|
||||
layerheight: number,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void>;
|
||||
|
||||
export class ClientScriptInterpreter {
|
||||
intstack: number[] = [];
|
||||
@@ -22,11 +31,39 @@ export class ClientScriptInterpreter {
|
||||
calli: ClientscriptObfuscation;
|
||||
mockscripts = new Map<number, (number | bigint | string)[]>();
|
||||
scope: ScriptScope | null = null;
|
||||
constructor(calli: ClientscriptObfuscation) {
|
||||
activecompid = -1;
|
||||
stalled: Promise<boolean> | void = undefined;
|
||||
uictx: UiRenderContext | null = null;
|
||||
constructor(calli: ClientscriptObfuscation, uictx: UiRenderContext | null = null) {
|
||||
this.calli = calli;
|
||||
this.uictx = uictx;
|
||||
}
|
||||
callscript(script: clientscript) {
|
||||
getComponent<T extends RsInterFaceTypes | "any" = "any">(compid: number, type = "any" as T): TypedRsInterFaceComponent<T> {
|
||||
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<T>;
|
||||
}
|
||||
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<boolean> {
|
||||
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> | 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<number, (inter: InterpreterWithScope, op: ClientScriptOp) => void>();
|
||||
const implementedops = new Map<number, OpImplementation>();
|
||||
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<string, (inter: InterpreterWithScope, op: ClientScriptOp) => void>();
|
||||
const namedimplementations = new Map<string, OpImplementation>();
|
||||
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()));
|
||||
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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -68,6 +68,7 @@ export const cacheConfigPages = {
|
||||
mapscenes: 34,
|
||||
maplabels: 36,
|
||||
dbtables: 40,
|
||||
dbrows: 41,
|
||||
|
||||
varplayer: 60,
|
||||
varnpc: 61,
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
["aspectxtype","byte"],
|
||||
["aspectytype","byte"],
|
||||
["parentid","ushort"],
|
||||
["flag","ubyte"],
|
||||
["hidden","ubyte"],
|
||||
["containerdata",["opt","type=0",["struct",
|
||||
["layerwidth","ushort"],
|
||||
["layerheight","ushort"],
|
||||
|
||||
@@ -135,6 +135,7 @@ function allParsers() {
|
||||
clientscript: FileParser.fromJson<import("../generated/clientscript").clientscript>(require("./opcodes/clientscript.jsonc")),
|
||||
clientscriptdata: FileParser.fromJson<import("../generated/clientscriptdata").clientscriptdata>(require("./opcodes/clientscriptdata.jsonc")),
|
||||
interfaces: FileParser.fromJson<import("../generated/interfaces").interfaces>(require("./opcodes/interfaces.jsonc")),
|
||||
dbtables: FileParser.fromJson<import("../generated/dbtables").dbtables>(require("./opcodes/dbtables.jsonc"))
|
||||
dbtables: FileParser.fromJson<import("../generated/dbtables").dbtables>(require("./opcodes/dbtables.jsonc")),
|
||||
dbrows: FileParser.fromJson<import("../generated/dbrows").dbrows>(require("./opcodes/dbrows.jsonc"))
|
||||
}
|
||||
}
|
||||
@@ -601,6 +601,7 @@ export const cacheFileJsonModes = constrainedMap<JsonBasedFile>()({
|
||||
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) },
|
||||
|
||||
@@ -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<number, HTMLElement>();
|
||||
comps = new Map<number, RsInterfaceComponent>();
|
||||
highlightstack: HTMLElement[] = [];
|
||||
interpreterprom: Promise<ClientScriptInterpreter> | null = null;
|
||||
touchedComps = new Set<RsInterfaceComponent>();
|
||||
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<HTMLResult> {
|
||||
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 = `<!DOCTYPE html>\n`;
|
||||
doc += `<html>\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<ReturnType<typeof loadRsInterfaceData>>) {
|
||||
export function renderRsInterfaceDOM(ctx: UiRenderContext, data: Awaited<ReturnType<typeof loadRsInterfaceData>>): 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<ReturnT
|
||||
root.appendChild(style);
|
||||
root.appendChild(container);
|
||||
|
||||
ctx.comps.clear();//TODO dispose here?
|
||||
for (let comp of data.rootcomps) {
|
||||
let sub = comp.initDom(ctx);
|
||||
container.appendChild(sub);
|
||||
@@ -153,7 +176,7 @@ export function renderRsInterfaceDOM(ctx: UiRenderContext, data: Awaited<ReturnT
|
||||
let dispose = () => {
|
||||
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<T extends RsInterFaceTypes | "any"> = 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<typeof uiModelRenderer> | 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<T extends RsInterFaceTypes>(type: T): this is TypedRsInterFaceComponent<T> {
|
||||
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 += `<div class="rs-image${!this.data.spritedata.flag2 ? " rs-image--cover" : ""}" style="${escapeHTML(spritecss)}"></div>`;
|
||||
}
|
||||
let html = "";
|
||||
html += `<div class="rs-component" data-compid=${this.subid} style="${escapeHTML(style)}" onclick="mod.click(event)" title="${escapeHTML(title)}">\n`;
|
||||
html += `<div class="rs-component" data-compid=${this.compid} style="${escapeHTML(style)}" onclick="mod.click(event)" title="${escapeHTML(title)}">\n`;
|
||||
html += childhtml;
|
||||
html += "</div>\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() {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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<RsInterfaceElement | null>(null);
|
||||
let [ui, setui] = React.useState<RsInterfaceDomTree | null>(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 (
|
||||
<div className="rs-componentmeta" onMouseEnter={mouseevent} onMouseLeave={mouseevent} onClick={e => 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"}
|
||||
<br />
|
||||
{data.spritedata && "sprite: " + data.spritedata.spriteid}
|
||||
{data.modeldata && "model: " + data.modeldata.modelid}
|
||||
<CallbackDebugger data={data} source={p.source} />
|
||||
<CallbackDebugger ctx={p.ctx} comp={p.comp} source={p.source} />
|
||||
<hr />
|
||||
<div className="rs-componentmeta-children">
|
||||
{p.comp.children.map((q, i) => <RsInterfaceDebugger ctx={p.ctx} key={i} comp={q} source={p.source} />)}
|
||||
@@ -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 (
|
||||
<div>
|
||||
{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 (
|
||||
<div key={key} className="rs-comonentcallback" onClick={async e => console.log(await renderClientScript(p.source, await p.source.getFileById(cacheMajors.clientscript, callbackid), callbackid))}>
|
||||
<div key={key} className="rs-comonentcallback" onClick={e => p.ctx.runClientScriptCallback(p.comp.compid, v)}>
|
||||
{key} {callbackid}({v.slice(1).map(q => typeof q == "string" ? `"${q}"` : q).join(",")})
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user