player avatar customization

This commit is contained in:
skillbert
2022-06-10 21:57:53 +02:00
parent bc25b397ca
commit 6d5ecb221e
18 changed files with 1008 additions and 365 deletions

306
generated/avataroverrides.d.ts vendored Normal file
View File

@@ -0,0 +1,306 @@
// GENERATED DO NOT EDIT
// This source data is located at '..\src\opcodes\avataroverrides.jsonc'
// run `npm run filetypes` to rebuild
export type avataroverrides = {
flags: number,
slots: [
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
{
slot: any,
cust: {
model: number[] | null,
flag2: true | null,
color: {
col2: number[] | null,
col4: [
number,
number,
][] | null,
} | null,
material: {
header: number,
materials: number[],
} | null,
} | null,
},
],
haircol0: number,
bodycol: number,
legscol: number,
bootscol: number,
skincol0: number,
skincol1: number,
haircol1: number,
unkbuf: Uint8Array,
stance: number,
};

View File

@@ -4,7 +4,6 @@
export type avatars = {
gender: number,
avatype: number,
player: {
slots: [
number,
@@ -24,7 +23,6 @@ export type avatars = {
number,
number,
],
flags: number,
rest: Uint8Array,
} | null,
npc: {

7
generated/npcs.d.ts vendored
View File

@@ -55,12 +55,7 @@ export type npcs = {
unk4: number,
} | null
movementCapabilities?: number | null
translations?: [
number,
number,
number,
number,
][] | null
translations?: Uint8Array[] | null
iconHeight?: number | null
respawnDirection?: number | null
animation_group?: number | null

View File

@@ -1,10 +1,14 @@
import { CacheFileSource } from "cache";
import { cacheConfigPages, cacheMajors } from "../constants";
import { parseAnimgroupConfigs, parseAvatars, parseEnums, parseIdentitykit, parseItem, parseNpc, parseStructs } from "../opdecoder";
import { parseAnimgroupConfigs, parseAvatarOverrides, parseAvatars, parseEnums, parseIdentitykit, parseItem, parseNpc, parseStructs } from "../opdecoder";
import { ob3ModelToThreejsNode, ThreejsSceneCache } from "./ob3tothree";
import { HSL2packHSL, HSL2RGB, ModelModifications, packedHSL2HSL, RGB2HSL, Stream } from "../utils";
import { SimpleModelDef, serializeAnimset } from "../viewer/scenenodes";
import { items } from "../../generated/items";
import { avataroverrides } from "../../generated/avataroverrides";
import { ScriptOutput } from "../viewer/scriptsui";
import { testDecodeFile } from "../scripts/testdecode";
import { avatars } from "../../generated/avatars";
export function avatarStringToBytes(text: string) {
let base64 = text.replace(/\*/g, "+").replace(/-/g, "/");
@@ -17,7 +21,26 @@ export function lowname(name: string) {
return res;
}
let defaultcols = {
export const slotNames = [
"helm",
"cape",
"necklace",
"weapon",
"body",
"slot5",
"slot6",
"legs",
"face",
"gloves",
"boots",
"beard",
"aura",
"slot13",
"slot14",
"slot15"
]
const defaultcols = {
hair0: 6798,
hair1: 55232,
@@ -36,10 +59,24 @@ let defaultcols = {
boots1: 4626
}
const humanheadanims: Record<string, number> = {
tpose: -1,
default: 9804,
worried: 9743,
talkfast: 9745,
scared: 9748,
wtf: 9752,
drunk: 9851,
happy: 9843,
evil: 9842,
laughing: 9841,
crying: 9765
}
let kitcolors: Record<"feet" | "skin" | "hair" | "clothes", Record<number, number>> | null = null;
async function loadKitData(source: CacheFileSource) {
let mapcololenum = async (enumid: number, mappingid: number, reverse: boolean) => {
let mapcolorenum = async (enumid: number, mappingid: number, reverse: boolean) => {
let colorfile = await source.getFileById(cacheMajors.enums, enumid);
let colordata = parseEnums.read(colorfile);
let orderfile = await source.getFileById(cacheMajors.enums, mappingid);
@@ -55,10 +92,10 @@ async function loadKitData(source: CacheFileSource) {
}
kitcolors = {
feet: await mapcololenum(753, 3297, false),
skin: await mapcololenum(746, 748, false),
hair: await mapcololenum(2343, 2345, false),
clothes: await mapcololenum(2347, 3282, false)
feet: await mapcolorenum(753, 3297, false),
skin: await mapcolorenum(746, 748, false),
hair: await mapcolorenum(2343, 2345, false),
clothes: await mapcolorenum(2347, 3282, false)
}
// for (let [id, colhsl] of Object.entries(kitcolors.hair)) {
@@ -70,7 +107,23 @@ async function loadKitData(source: CacheFileSource) {
return kitcolors;
}
export async function avatarToModel(scene: ThreejsSceneCache, avadata: Buffer) {
export type EquipCustomization = avataroverrides["slots"][number]["cust"];
export type EquipSlot = {
name: string,
type: "kit" | "item",
id: number,
models: number[],
indexMale: [number, number],
indexFemale: [number, number],
indexMaleHead: [number, number],
indexFemaleHead: [number, number],
replaceMaterials: [number, number][],
replaceColors: [number, number][]
}
//TODO remove output and name args
export async function avatarToModel(output: ScriptOutput | null, scene: ThreejsSceneCache, avadata: Buffer, name = "", head = false) {
let kitdata = kitcolors ?? await loadKitData(scene.source);
let avabase = parseAvatars.read(avadata);
let models: SimpleModelDef = [];
@@ -78,29 +131,34 @@ export async function avatarToModel(scene: ThreejsSceneCache, avadata: Buffer) {
let playerkitarch = await scene.source.getArchiveById(cacheMajors.config, cacheConfigPages.identityKit);
let playerkit = Object.fromEntries(playerkitarch.map(q => [q.fileid, parseIdentitykit.read(q.buffer)]));
let animgroup = 2699;
let items: items[] = [];
let slots: (EquipSlot | null)[] = [];
let avatar: avataroverrides | null = null;
let anims: Record<string, number> = { tpose: -1 };
if (avabase.player) {
slots = avabase.player.slots.map(() => null);
let isfemale = (avabase.gender & 1) != 0;
let animstruct = -1;
let kitmods: { mods: ModelModifications, slotid: number }[] = [];
let modstream = new Stream(Buffer.from(avabase.player.rest));
for (let [index, slot] of avabase.player.slots.entries()) {
if (slot == 0 || slot == 0x3fff) { continue; }
if (slot < 0x4000) {
let kitid = slot - 0x100;
let kit = playerkit[kitid];
if (kit?.models) {
for (let modelid of kit.models) {
let model = {
modelid: modelid,
mods: { replaceColors: kit.recolor ?? [] }
}
models.push(model);
kitmods.push({ mods: model.mods, slotid: kit.bodypart ?? -1 });
let models = [...kit.models];
if (kit.headmodel) { models.push(kit.headmodel); }
slots[index] = {
name: "playerkit_" + kitid,
type: "kit",
id: kitid,
models: models,
indexMale: [0, kit.models.length],
indexFemale: [0, kit.models.length],
indexMaleHead: [kit.models.length, kit.models.length + (kit.headmodel ? 1 : 0)],
indexFemaleHead: [kit.models.length, kit.models.length + (kit.headmodel ? 1 : 0)],
replaceColors: kit.recolor ?? [],
replaceMaterials: []
}
// console.log("kit", kit);
continue;
}
}
@@ -108,136 +166,150 @@ export async function avatarToModel(scene: ThreejsSceneCache, avadata: Buffer) {
let itemid = (slot - 0x4000) & 0xffff;
let file = await scene.source.getFileById(cacheMajors.items, itemid);
let item = parseItem.read(file);
let mods: ModelModifications = {};
if (item.color_replacements) { mods.replaceColors = item.color_replacements; }
if (item.material_replacements) { mods.replaceMaterials = item.material_replacements; }
let animprop = item.extra?.find(q => q.prop == 686);
if (animprop) { animstruct = animprop.intvalue!; }
//TODO item model overrides/recolors/retextures
let itemmodels: SimpleModelDef = [];
let itemmodels: number[] = [];
let maleindex = itemmodels.length;
if (item.maleModels_0) { itemmodels.push({ modelid: item.maleModels_0, mods }); }
if (item.maleModels_1) { itemmodels.push({ modelid: item.maleModels_1, mods }); }
if (item.maleModels_2) { itemmodels.push({ modelid: item.maleModels_2, mods }); }
if (item.maleModels_0) { itemmodels.push(item.maleModels_0); }
if (item.maleModels_1) { itemmodels.push(item.maleModels_1); }
if (item.maleModels_2) { itemmodels.push(item.maleModels_2); }
let femaleindex = itemmodels.length;
if (item.femaleModels_0) { itemmodels.push({ modelid: item.femaleModels_0, mods }); }
if (item.femaleModels_1) { itemmodels.push({ modelid: item.femaleModels_1, mods }); }
if (item.femaleModels_2) { itemmodels.push({ modelid: item.femaleModels_2, mods }); }
if (item.femaleModels_0) { itemmodels.push(item.femaleModels_0); }
if (item.femaleModels_1) { itemmodels.push(item.femaleModels_1); }
if (item.femaleModels_2) { itemmodels.push(item.femaleModels_2); }
let maleheadindex = itemmodels.length;
if (item.maleHeads_0) { itemmodels.push({ modelid: item.maleHeads_0, mods }); }
if (item.maleHeads_1) { itemmodels.push({ modelid: item.maleHeads_1, mods }); }
if (item.maleHeads_0) { itemmodels.push(item.maleHeads_0); }
if (item.maleHeads_1) { itemmodels.push(item.maleHeads_1); }
let femaleheadindex = itemmodels.length;
if (item.femaleHeads_0) { itemmodels.push({ modelid: item.femaleHeads_0, mods }); }
if (item.femaleHeads_1) { itemmodels.push({ modelid: item.femaleHeads_1, mods }); }
if (item.femaleHeads_0) { itemmodels.push(item.femaleHeads_0); }
if (item.femaleHeads_1) { itemmodels.push(item.femaleHeads_1); }
let endindex = itemmodels.length;
if (avabase.player.flags & (1 << index)) {
let type = modstream.readByte();
if (type & 1) {//override model itself
for (let i = 0; i < itemmodels.length; i++) {
itemmodels[i].modelid = modstream.readUIntSmart();
}
}
if (type & 2) {//unknown
console.log("avatar customization flag 2 on item " + item.name);
}
if (type & 4) {//color
//not really understood yet
let coltype = modstream.readUShort(true);
mods.replaceColors ??= [];
if (coltype == 0x3210) {
for (let recol of mods.replaceColors) {
recol[1] = modstream.readUShort(true);
}
} else if (coltype == 0x220f) {
mods.replaceColors.push([modstream.readUShort(true), modstream.readUShort(true)]);
mods.replaceColors.push([modstream.readUShort(true), modstream.readUShort(true)]);
mods.replaceColors.push([modstream.readUShort(true), modstream.readUShort(true)]);
mods.replaceColors.push([modstream.readUShort(true), modstream.readUShort(true)]);
} else {
throw new Error("unknown avatar item recolor header 0x" + coltype.toString(16));
}
}
if (type & 8) {
let header = modstream.readUByte();
console.log("retexture header 0x" + header.toString(16));
mods.replaceMaterials ??= [];
if (header == 0x10) {
for (let remat of mods.replaceMaterials) {
remat[1] = modstream.readUShort(true);
}
// } else if (header == 0xf0) {
// // mods.replaceMaterials.push([modstream.readUShort(), modstream.readUShort()]);
// modstream.readUShort(true);
} else {
throw new Error("unknown avatar item material header 0x" + header.toString(16))
}
}
}
models.push(...itemmodels.slice(isfemale ? femaleindex : maleindex, isfemale ? maleheadindex : femaleindex));
items.push(item);
slots[index] = {
name: item.name ?? "no name",
type: "item",
id: itemid,
models: itemmodels,
indexMale: [maleindex, femaleindex],
indexFemale: [femaleindex, maleheadindex],
indexMaleHead: [maleheadindex, femaleheadindex],
indexFemaleHead: [femaleheadindex, endindex],
replaceColors: item.color_replacements ?? [],
replaceMaterials: item.material_replacements ?? []
};
}
let haircol0 = modstream.readUByte();
let bodycol = modstream.readUByte();
let legscol = modstream.readUByte();
let bootscol = modstream.readUByte();
let skincol0 = modstream.readUByte();
let skincol1 = modstream.readUByte();
let haircol1 = modstream.readUByte();
console.log("hair", haircol0, haircol1);
let res = testDecodeFile(parseAvatarOverrides, "json", Buffer.from(avabase.player.rest), { slots });
if (!res.success) {
if (!output) { throw new Error(); }
output.writeFile(name + ".hexerr.json", res.errorfile)
}
avatar = parseAvatarOverrides.read(Buffer.from(avabase.player.rest), { slots });
let extramods: [number, number][] = [
[defaultcols.hair0, kitdata.hair[haircol0]],
[defaultcols.hair1, kitdata.hair[haircol1]],
let globalrecolors: [number, number][] = [
[defaultcols.hair0, kitdata.hair[avatar.haircol0]],
[defaultcols.hair1, kitdata.hair[avatar.haircol1]],
[defaultcols.skin0, kitdata.skin[skincol0]],
[defaultcols.skin1, kitdata.skin[skincol0]],
[defaultcols.skin2, kitdata.skin[skincol0]],
[defaultcols.skin3, kitdata.skin[skincol0]],
[defaultcols.skin0, kitdata.skin[avatar.skincol0]],
[defaultcols.skin1, kitdata.skin[avatar.skincol0]],
[defaultcols.skin2, kitdata.skin[avatar.skincol0]],
[defaultcols.skin3, kitdata.skin[avatar.skincol0]],
[defaultcols.body0, kitdata.clothes[bodycol]],
[defaultcols.body1, kitdata.clothes[bodycol]],
[defaultcols.legs0, kitdata.clothes[legscol]],
[defaultcols.legs1, kitdata.clothes[legscol]],
[defaultcols.boots0, kitdata.feet[bootscol]],
[defaultcols.boots1, kitdata.feet[bootscol]],
[defaultcols.body0, kitdata.clothes[avatar.bodycol]],
[defaultcols.body1, kitdata.clothes[avatar.bodycol]],
[defaultcols.legs0, kitdata.clothes[avatar.legscol]],
[defaultcols.legs1, kitdata.clothes[avatar.legscol]],
[defaultcols.boots0, kitdata.feet[avatar.bootscol]],
[defaultcols.boots1, kitdata.feet[avatar.bootscol]],
];
models.forEach(q => {
q.mods.replaceColors ??= [];
q.mods.replaceColors.push(...extramods);
})
modstream.skip(13);
let unknownint = modstream.readUShort();
if (animstruct != -1) {
let file = await scene.source.getFileById(cacheMajors.structs, animstruct);
let anims = parseStructs.read(file);
//2954 for combat stance
let noncombatset = anims.extra?.find(q => q.prop == 2954);
if (noncombatset) {
animgroup = noncombatset.intvalue!;
avatar.slots.forEach(slot => {
const equip: EquipSlot = slot.slot;
if (slot.slot) {
let mods: ModelModifications = {
replaceColors: [...equip.replaceColors],
replaceMaterials: [...equip.replaceMaterials]
};
if (slot.cust?.color?.col2) {
for (let i in mods.replaceColors) { mods.replaceColors[i][1] = slot.cust.color.col2[i]; }
}
if (slot.cust?.color?.col4) {
mods.replaceColors!.push(...slot.cust.color.col4);
}
if (slot.cust?.material) {
for (let i in mods.replaceMaterials) { mods.replaceMaterials[i][1] = slot.cust.material.materials[i]; }
}
if (slot.cust?.model) {
for (let i in slot.cust.model) { equip.models[i] = slot.cust.model[i]; }
}
mods.replaceColors!.push(...globalrecolors);
let range = (isfemale ?
(head ? equip.indexFemaleHead : equip.indexFemale) :
(head ? equip.indexMaleHead : equip.indexMale));
equip.models.forEach((id, i) => i >= range[0] && i < range[1] && models.push({ modelid: id, mods }));
}
});
if (head) {
anims = humanheadanims;
} else {
let animgroup = 2699;
if (animstruct != -1) {
let file = await scene.source.getFileById(cacheMajors.structs, animstruct);
let animfile = parseStructs.read(file);
//2954 for combat stance
let noncombatset = animfile.extra?.find(q => q.prop == 2954);
if (noncombatset) { animgroup = noncombatset.intvalue!; }
}
anims = await animGroupToAnims(scene, animgroup);
}
} else if (avabase.npc) {
let file = await scene.source.getFileById(cacheMajors.npcs, avabase.npc.id);
let npc = parseNpc.read(file);
if (npc.models) { models.push(...npc.models.map(q => ({ modelid: q, mods: {} }))) };
if (npc.animation_group) {
animgroup = npc.animation_group;
let mods: ModelModifications = {
replaceColors: npc.color_replacements ?? [],
replaceMaterials: npc.color_replacements ?? []
};
if (!head) {
if (npc.models) { models.push(...npc.models.map(q => ({ modelid: q, mods: mods }))) }
if (npc.animation_group) { anims = await animGroupToAnims(scene, npc.animation_group); }
} else {
if (npc.headModels) { models.push(...npc.headModels.map(q => ({ modelid: q, mods: mods }))); }
}
}
return { models, anims, info: { avatar, gender: avabase.gender, npc: avabase.npc } };
}
export function writeAvatar(avatar: avataroverrides | null, gender: number, npc: avatars["npc"]) {
let base: avatars = {
gender: gender,
npc: npc,
player: null
}
if (avatar) {
let overrides = parseAvatarOverrides.write(avatar);
base.player = {
slots: avatar.slots.map(q => {
const slot = q.slot as EquipSlot | null;
return (!slot ? 0 : slot.type == "item" ? slot.id + 0x4000 : slot.id + 0x100);
}) as any,
rest: overrides
}
}
return parseAvatars.write(base);
}
async function animGroupToAnims(scene: ThreejsSceneCache, groupid: number) {
let animsetarch = await scene.source.getArchiveById(cacheMajors.config, cacheConfigPages.animgroups);
let animsetfile = animsetarch[animgroup];
let animsetfile = animsetarch[groupid];
let animset = parseAnimgroupConfigs.read(animsetfile.buffer);
let anims = serializeAnimset(animset);
return { models, anims, info: { items, animset } };
return serializeAnimset(animset);
}
export function appearanceUrl(name: string) {

View File

@@ -491,7 +491,7 @@ export function modifyMesh(mesh: ModelMeshData, mods: ModelModifications) {
newmesh.materialId = (newmat == (1 << 16) - 1 ? -1 : newmat);
}
if (mods.replaceColors && mesh.attributes.color) {
if (mods.replaceColors && mods.replaceColors.length != 0 && mesh.attributes.color) {
let colors = mesh.attributes.color;
let clonedcolors: BufferAttribute | undefined = undefined;

View File

@@ -100,13 +100,16 @@ const quickchat = command({
const scrapeavatars = command({
name: "run",
args: {
...filesource,
save: option({ long: "save", short: "s" }),
skip: option({ long: "skip", short: "i", type: cmdts.number, defaultValue: () => 0 }),
max: option({ long: "max", short: "m", type: cmdts.number, defaultValue: () => 500 })
max: option({ long: "max", short: "m", type: cmdts.number, defaultValue: () => 500 }),
json: flag({ long: "json", short: "j" })
},
handler: async (args) => {
let output = new CLIScriptOutput(args.save);
await output.run(scrapePlayerAvatars, args.skip, args.max);
let source = (args.json ? await args.source() : null);
await output.run(scrapePlayerAvatars, source, args.skip, args.max, args.json);
}
});

View File

@@ -56,8 +56,6 @@ export type ComposedChunk = string
type TypeDef = { [name: string]: ChunkType<any> | ComposedChunk };
type ChunkParserParent = ChunkParser | null;
const BufferTypes = {
hex: { constr: Uint8Array },//used to debug into json file
byte: { constr: Int8Array },
@@ -160,21 +158,21 @@ export function buildParser(chunkdef: ComposedChunk, typedef: TypeDef): ChunkPar
if (chunkdef.length < 3) throw new Error(`3 arguments exptected for proprety with type opt`);
let cond: string;
let arg1 = chunkdef[1];
let valuearg: ChunkParser;
let condvalue: number;
let cmpmode: CompareMode = "eq";
if (Array.isArray(arg1)) {
cond = arg1[0];
cmpmode = arg1[2] ?? "eq";
if (typeof arg1[1] == "number") {
valuearg = literalValueParser({ primitive: "value", value: arg1[1] });
condvalue = arg1[1];
} else {
valuearg = buildParser(arg1[1], typedef);
throw new Error("only literal ints as condition value are supported");
}
} else {
cond = "$opcode";
valuearg = literalValueParser({ primitive: "value", value: chunkdef[1] });
condvalue = arg1;
}
return optParser(buildParser(chunkdef[2], typedef), cond, valuearg, cmpmode);
return optParser(buildParser(chunkdef[2], typedef), cond, condvalue, cmpmode);
}
case "chunkedarray": {
if (chunkdef.length < 2) throw new Error(`'read' variables interpretted as an array must contain items: ${JSON.stringify(chunkdef)}`);
@@ -334,7 +332,7 @@ function opcodesParser<T extends Record<string, any>>(opcodetype: ChunkParser<nu
let r = "{\n";
let newindent = indent + "\t";
for (let val of map.values()) {
r += newindent + (val.key as string) + "?: " + val.parser.getTypescriptType(newindent) + "\n";
r += newindent + (val.key as string) + "?: " + val.parser.getTypescriptType(newindent) + " | null\n";
}
r += indent + "}";
return r;
@@ -345,7 +343,7 @@ function opcodesParser<T extends Record<string, any>>(opcodetype: ChunkParser<nu
properties: Object.fromEntries([...map.values()]
.filter(prop => !(prop.key as string).startsWith("$"))
.map((prop) => {
return [prop.key, prop.parser.getJsonSchema()];
return [prop.key, { oneOf: [prop.parser.getJsonSchema(), { type: "null" }] }];
})
)
}
@@ -366,10 +364,10 @@ function tuppleParser(props: ChunkParser[]) {
}
return r;
},
write(buffer, value) {
write(state, value) {
if (!Array.isArray(value)) { throw new Error("array expected"); }
for (let [i, prop] of props.entries()) {
prop.write(buffer, value[i]);
prop.write(state, value[i]);
}
},
setReferenceParent(parent) {
@@ -412,10 +410,6 @@ function buildReference(name: string, container: ChunkParserContainer | null, st
return container.resolveReference(name, startingpoint);
}
function externalArgParser() {
}
function refgetter(owner: ChunkParser, refparent: ChunkParserContainer | null, propname: string, resolve: (v: unknown, old: number) => number) {
let final = buildReference(propname, refparent, { owner, stackdepth: 0, resolve });
let depth = final.stackdepth;
@@ -457,7 +451,7 @@ function structParser<T extends Record<string, any>>(props: { [key in keyof T]:
state.hiddenstack.pop();
return r;
},
write(buffer, value) {
write(state, value) {
if (typeof value != "object" || !value) { throw new Error("object expected"); }
for (let key of keys) {
let propvalue = value[key as string];
@@ -465,11 +459,11 @@ function structParser<T extends Record<string, any>>(props: { [key in keyof T]:
if (refarray) {
propvalue = propvalue ?? 0;
for (let ref of refarray) {
//TODO
propvalue = ref.resolve(value, propvalue);
}
}
let prop = props[key];
prop.write(buffer, propvalue);
prop.write(state, propvalue);
}
},
setReferenceParent(parent) {
@@ -520,21 +514,20 @@ function structParser<T extends Record<string, any>>(props: { [key in keyof T]:
return r;
}
function optParser<T>(type: ChunkParser<T>, condvar: string, condvalue: ChunkParser, compare: CompareMode): ChunkParser<T | null> {
function optParser<T>(type: ChunkParser<T>, condvar: string, condvalue: number, compare: CompareMode): ChunkParser<T | null> {
let refparent: ChunkParserContainer | null = null;
let ref: ReturnType<typeof refgetter>;
let r: ChunkParserContainer<T | null> = {
read(state) {
let value = ref.read(state);
let cmpvalue = condvalue.read(state);
if (!checkCondition(compare, cmpvalue, value)) {
if (!checkCondition(compare, condvalue, value)) {
return null;
}
return type.read(state);
},
write(buffer, value) {
write(state, value) {
if (value != null) {
return type.write(buffer, value);
return type.write(state, value);
}
},
setReferenceParent(parent) {
@@ -542,9 +535,7 @@ function optParser<T>(type: ChunkParser<T>, condvar: string, condvalue: ChunkPar
type.setReferenceParent?.(r);
ref = refgetter(r, parent, condvar, (v: unknown, oldvalue: number) => {
// return forceCondition(compare,, oldvalue, v != null);
//need a ref to condvalue here and resolve that
throw new Error("not implemented");
return forceCondition(compare, condvalue, oldvalue, v != null);
});
},
resolveReference(name, child) {
@@ -634,7 +625,7 @@ function chunkedArrayParser<T extends object>(lengthtype: ChunkParser<number>, c
obj = r[i];
hidden = hiddenprops[i];
}
//TODO check if we can save speed by manually overwriting state[length-1] instead of pop->push
//TODO check if we can save speed by manually overwriting stack[length-1] instead of pop->push
state.stack.push(obj);
state.hiddenstack.push(hidden);
for (let key in proptype) {
@@ -774,11 +765,11 @@ function arrayParser<T>(lengthtype: ChunkParser<number>, subtype: ChunkParser<T>
}
return r;
},
write(buffer, value) {
write(state, value) {
if (!Array.isArray(value)) { throw new Error("array expected"); }
lengthtype.write(buffer, value.length);
lengthtype.write(state, value.length);
for (let i = 0; i < value.length; i++) {
subtype.write(buffer, value[i]);
subtype.write(state, value[i]);
}
},
setReferenceParent(parent) {
@@ -832,14 +823,14 @@ function arrayNullTerminatedParser<T>(lengthtype: ChunkParser<number>, proptype:
state.stack.pop();
return r;
},
write(buffer, value) {
write(state, value) {
if (!Array.isArray(value)) { throw new Error("array expected"); }
//TODO probably very wrong
for (let prop of value) {
lengthtype.write(buffer, 1);
proptype.write(buffer, prop);
lengthtype.write(state, 1);
proptype.write(state, prop);
}
lengthtype.write(buffer, 0);
lengthtype.write(state, 0);
},
setReferenceParent(parent) {
refparent = parent;
@@ -957,7 +948,7 @@ function intParser(primitive: PrimitiveInt): ChunkParser<number> {
}
return output;
},
write(buffer, value) {
write(state, value) {
if (typeof value != "number" || value % 1 != 0) throw new Error(`integer expected`);
let unsigned = primitive.unsigned;
let bytes = primitive.bytes;
@@ -979,13 +970,13 @@ function intParser(primitive: PrimitiveInt): ChunkParser<number> {
let mask = ~(~0 << (bytes * 8 - 1));
let int = (value & mask) | ((fitshalf ? 0 : 1) << (bytes * 8 - 1));
//write 32bit ints as unsigned since js bitwise operations cast to int32
buffer[`write${unsigned && bytes != 4 ? "U" : ""}IntBE`](int, buffer.scan, bytes);
buffer.scan += bytes;
state.buffer[`write${unsigned && bytes != 4 ? "U" : ""}IntBE`](int, state.scan, bytes);
state.scan += bytes;
} else if (readmode == "sumtail") {
throw new Error("not implemented");
} else {
output = buffer[`write${unsigned ? "U" : ""}Int${endianness.charAt(0).toUpperCase()}E`](value, buffer.scan, bytes);
buffer.scan += bytes;
output = state.buffer[`write${unsigned ? "U" : ""}Int${endianness.charAt(0).toUpperCase()}E`](value, state.scan, bytes);
state.scan += bytes;
}
},
getTypescriptType() {
@@ -1032,7 +1023,7 @@ function referenceValueParser(propname: string, minbit: number, bitlength: numbe
}
return value + offset;
},
write(buffer, value) {
write(state, value) {
//noop, the referenced value does the writing and will get its value from this prop through refgetter
},
setReferenceParent(parent) {
@@ -1059,7 +1050,7 @@ function bytesRemainingParser(): ChunkParser<number> {
read(state) {
return state.endoffset - state.scan;
},
write(buffer, value) {
write(state, value) {
//nop, value exists only in context of output
},
getTypescriptType() {
@@ -1100,7 +1091,7 @@ function intAccumlatorParser(refname: string, value: ChunkParser<number | undefi
resolveReference(name, child) {
return buildReference(name, refparent, { owner: r, stackdepth: child.stackdepth, resolve: child.resolve });
},
write(buffer, value) {
write(state, value) {
//need to make the struct writer grab its value from here for invisible props
throw new Error("write for accumolator not implemented");
},
@@ -1186,7 +1177,16 @@ let hardcodes: Record<string, (args: unknown[]) => ChunkParser> = {
if (byte1 == 0xff && byte0 == 0xff) { return -1; }
return (byte0 << 8) | byte1;
},
write(state, value) { throw new Error("not implemented"); },
write(state, value) {
if (typeof value != "number") { throw new Error("number expected"); }
if (value == 0) {
state.buffer.writeUInt8(0, state.scan++);
} else {
//replicate explicit 16bit overflow bug since that's what the game does
state.buffer.writeUint16BE((value == -1 ? 0xffff : value & 0xffff), state.scan);
state.scan += 2;
}
},
getTypescriptType() { return "number"; },
getJsonSchema() { return { type: "integer", minimum: -1, maximum: 0xffff - 0x4000 - 1 }; }
}
@@ -1195,17 +1195,20 @@ let hardcodes: Record<string, (args: unknown[]) => ChunkParser> = {
let type = args[0];
if (typeof type != "string" || !["ref", "matcount", "colorcount", "modelcount"].includes(type)) { throw new Error(); }
//yes this is hacky af...
return {
read(state) {
if (type == "ref") { state.args.activeitem = (state.args.activeitem ?? -1) + 1; }
let ref = state.args.items[state.args.activeitem];
let ref = state.args.slots[state.args.activeitem];
if (type == "ref") { return ref; }
else if (type == "matcount") { return ref.materials.length; }
else if (type == "modelcount") { return ref.models.length; }
else if (type == "colorcount") { return ref.colors.length; }
else if (type == "matcount") { return ref?.replaceMaterials?.length ?? 0; }
else if (type == "colorcount") { return ref?.replaceColors?.length ?? 0; }
else if (type == "modelcount") { return ref?.models.length; }
else { throw new Error(); }
},
write() { throw new Error("not implemented"); },
write() {
//noop
},
getTypescriptType() { return (type == "ref" ? "any" : "number"); },
getJsonSchema() { return { type: (type == "ref" ? "any" : "integer") } }
}

View File

@@ -0,0 +1,79 @@
["struct",
["flags","ushort"],
//can't use array since i have a way to ref array index in the flag...
["slots",["tuple",
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",0,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",1,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",2,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",3,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",4,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",5,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",6,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",7,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",8,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",9,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",10,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",11,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",12,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",13,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",14,"bitflag"],"playeritemedit"]]
],
["struct",
["slot",["itemvar","ref"]],
["cust",["opt",["flags",15,"bitflag"],"playeritemedit"]]
]
]],
["haircol0","ubyte"],
["bodycol","ubyte"],
["legscol","ubyte"],
["bootscol","ubyte"],
["skincol0","ubyte"],
["skincol1","ubyte"],
["haircol1","ubyte"],
["unkbuf",["buffer",13,"hex"]],
["stance","ushort"]
]

View File

@@ -1,9 +1,9 @@
["struct",
["gender","ubyte"],
["avatype","playeritem"],
["player",["opt",["avatype",-1,"eqnot"],["struct",
["slots",[
["ref","avatype"],
["$avatype","playeritem"],
["player",["opt",["$avatype",-1,"eqnot"],["struct",
["slots",["tuple",
["ref","$avatype"],
"playeritem",
"playeritem",
"playeritem",
@@ -20,52 +20,9 @@
"playeritem",
"playeritem"
]],
// ["cape","playeritem"],
// ["necklace","playeritem"],
// ["weapon","playeritem"],
// ["body","playeritem"],
// ["slot5","playeritem"],
// ["slot6","playeritem"],//ring aura quiver pocket
// ["legs","playeritem"],
// ["slot8","playeritem"],
// ["gloves","playeritem"],
// ["boots","playeritem"],
// ["slot11","playeritem"],
// ["aura","playeritem"],
// ["slot13","playeritem"],
// ["slot14","playeritem"],
// ["slot15","playeritem"],
["flags","ushort"],
["rest",["buffer",["bytesleft"],"hex"]]
// ["cust0",["opt",["flags",0,"bitflag"],"playeritemedit"]],
// ["cust1",["opt",["flags",1,"bitflag"],"playeritemedit"]],
// ["cust2",["opt",["flags",2,"bitflag"],"playeritemedit"]],
// ["cust3",["opt",["flags",3,"bitflag"],"playeritemedit"]],
// ["cust4",["opt",["flags",4,"bitflag"],"playeritemedit"]],
// ["cust5",["opt",["flags",5,"bitflag"],"playeritemedit"]],
// ["cust6",["opt",["flags",6,"bitflag"],"playeritemedit"]],
// ["cust7",["opt",["flags",7,"bitflag"],"playeritemedit"]],
// ["cust8",["opt",["flags",8,"bitflag"],"playeritemedit"]],
// ["cust9",["opt",["flags",9,"bitflag"],"playeritemedit"]],
// ["cust10",["opt",["flags",10,"bitflag"],"playeritemedit"]],
// ["cust11",["opt",["flags",11,"bitflag"],"playeritemedit"]],
// ["cust12",["opt",["flags",12,"bitflag"],"playeritemedit"]],
// ["cust13",["opt",["flags",13,"bitflag"],"playeritemedit"]],
// ["cust14",["opt",["flags",14,"bitflag"],"playeritemedit"]],
// ["cust15",["opt",["flags",15,"bitflag"],"playeritemedit"]],
// // ["fil3","ubyte"],
// ["fil3","ushort"],
// ["fil4","ushort"],
// ["fil7","ushort"],
// ["fil8","ushort"],
// ["fil9","ushort"],
// ["buf1",["buffer",10,"hex"]],
// ["anim","ushort"]
]]],
["npc",["opt",["avatype",-1],["struct",
["npc",["opt",["$avatype",-1],["struct",
["id","ushort"],
["buf",["buffer",21,"hex"]],
["unkff","ushort"]

View File

@@ -31,18 +31,17 @@
"playeritem": { "primitive": "hardcode", "name": "playeritem" },
"playeritemedit": ["struct",
["type","ubyte"],
["model",["opt",["type",0,"bitflag"],["varuint","varuint"]]],
["flag2",["opt",["type",1,"bitflag"],"true"]],
["color",["opt",["type",2,"bitflag"],["struct",
["type","ushort"],
["col2",["opt",["type",12816],["array",4,"ushort"]]],
["col4",["opt",["type",8719],["array",8,"ushort"]]]
["$type","ubyte"],
["model",["opt",["$type",0,"bitflag"],["array",["itemvar","modelcount"],"varuint"]]],
["flag2",["opt",["$type",1,"bitflag"],"true"]],
["color",["opt",["$type",2,"bitflag"],["struct",
["$coltype","ushort"],
["col2",["opt",["$coltype",12816],["array",["itemvar","colorcount"],"ushort"]]],
["col4",["opt",["$coltype",8719],["array",4,["tuple","ushort","ushort"]]]]
]]],
["texture",["opt",["type",3,"bitflag"],["struct",
["material",["opt",["$type",3,"bitflag"],["struct",
["header","ubyte"],
["tex0","ushort"],
["tex1","ushort"]
["materials",["array",["itemvar","matcount"],"ushort"]]
]]]
],

View File

@@ -7,7 +7,7 @@ const typedef = commentJson.parse(require("./opcodes/typedef.json")) as any;
//alloc a large static buffer to write data to without knowing the data size
//then copy what we need out of it
//the buffer is reused so it saves a ton of buffer allocs
const scratchbuf = Object.assign(Buffer.alloc(1024 * 100), { scan: 0 });
const scratchbuf = Buffer.alloc(1024 * 100);
let bytesleftoverwarncount = 0;
@@ -40,7 +40,7 @@ export class FileParser<T> {
return res;
}
read(buffer: Buffer) {
read(buffer: Buffer, args?: Record<string, any>) {
let state: opcode_reader.DecodeState = {
buffer,
stack: [],
@@ -48,7 +48,7 @@ export class FileParser<T> {
scan: 0,
startoffset: 0,
endoffset: buffer.byteLength,
args: {}
args: args ?? {}
};
return this.readInternal(state);
}
@@ -58,10 +58,9 @@ export class FileParser<T> {
this.parser.write(state, obj);
if (state.scan > scratchbuf.byteLength) { throw new Error("tried to write file larger than scratchbuffer size"); }
//do the weird prototype slice since we need a copy, not a ref
let r: Buffer = Uint8Array.prototype.slice.call(scratchbuf, 0, scratchbuf.scan);
let r: Buffer = Uint8Array.prototype.slice.call(scratchbuf, 0, state.scan);
//clear it for next use
scratchbuf.fill(0, 0, scratchbuf.scan);
scratchbuf.scan = 0;
scratchbuf.fill(0, 0, state.scan);
return r;
}
}
@@ -91,6 +90,7 @@ export const parseQuickchatCategories = new FileParser<import("../generated/quic
export const parseQuickchatLines = new FileParser<import("../generated/quickchatlines").quickchatlines>(require("./opcodes/quickchatlines.jsonc"));
export const parseEnvironments = new FileParser<import("../generated/environments").environments>(require("./opcodes/environments.jsonc"));
export const parseAvatars = new FileParser<import("../generated/avatars").avatars>(require("./opcodes/avatars.jsonc"));
export const parseAvatarOverrides = new FileParser<import("../generated/avataroverrides").avataroverrides>(require("./opcodes/avataroverrides.jsonc"));
export const parseIdentitykit = new FileParser<import("../generated/identitykit").identitykit>(require("./opcodes/identitykit.jsonc"));
export const parseStructs = new FileParser<import("../generated/structs").structs>(require("./opcodes/structs.jsonc"));
export const parseParams = new FileParser<import("../generated/params").params>(require("./opcodes/params.jsonc"));

View File

@@ -34,5 +34,5 @@ export async function indexOverview(output: ScriptOutput, source: CacheFileSourc
missingindices: index.subindexcount + 1 - index.subindices[index.subindices.length - 1]
});
}
output.writeFile("indexoverview.json", prettyJson({ majors, configs }), "json");
output.writeFile("indexoverview.json", prettyJson({ majors, configs }));
}

View File

@@ -42,5 +42,5 @@ export async function quickChatLookup(output: ScriptOutput, source: CacheFileSou
}
iter(cats[85], "");
output.writeFile("quickchat.json", prettyJson(hotkeys), "json");
output.writeFile("quickchat.json", prettyJson(hotkeys));
}

View File

@@ -1,6 +1,9 @@
import { EngineCache, ThreejsSceneCache } from "../3d/ob3tothree";
import fetch from "node-fetch";
import { avatarStringToBytes, lowname } from "../3d/avatar";
import { avatarStringToBytes, avatarToModel, lowname } from "../3d/avatar";
import { ScriptOutput } from "../viewer/scriptsui";
import prettyJson from "json-stringify-pretty-compact";
import { CacheFileSource } from "../cache";
async function getPlayerNames(cat: number, subcat: number, page: number) {
@@ -21,7 +24,24 @@ async function getPlayerAvatar(name: string) {
return avatarStringToBytes(await res.text());
}
export async function scrapePlayerAvatars(output: ScriptOutput, skip: number, max: number) {
export async function scrapePlayerAvatars(output: ScriptOutput, source: CacheFileSource | null, skip: number, max: number, parsed: boolean) {
let scene: ThreejsSceneCache | null = null;
if (parsed) {
if (!source) { throw new Error("need file source when extracting avatar data"); }
let engine = await EngineCache.create(source);
scene = new ThreejsSceneCache(engine);
}
for await (let file of fetchPlayerAvatars(skip, max)) {
if (parsed) {
let data = await avatarToModel(null, scene!, file.buf);
await output.writeFile(`playerdata_${file.name}.json`, prettyJson(data.info.avatar));
} else {
output.writeFile(`playerdata_${file.name}.bin`, file.buf);
}
}
}
async function* fetchPlayerAvatars(skip: number, max: number) {
let count = 0;
const pagesize = 25;
let startpage = Math.floor(skip / pagesize);
@@ -30,9 +50,18 @@ export async function scrapePlayerAvatars(output: ScriptOutput, skip: number, ma
for (let player of players) {
let data = await getPlayerAvatar(player);
if (data) {
output.writeFile(`playerdata_${lowname(player)}.bin`, data);
count++;
yield { name: lowname(player), buf: data };
}
}
}
}
export async function extractAvatars(output: ScriptOutput, source: CacheFileSource, files: AsyncGenerator<{ name: string, buf: Buffer }>) {
let engine = await EngineCache.create(source);
let scene = new ThreejsSceneCache(engine);
for await (let file of files) {
let data = await avatarToModel(output, scene, file.buf, file.name);
// await output.writeFile(file.name, prettyJson(data.info.avatar));
}
}

View File

@@ -2,6 +2,7 @@ import { DecodeState, getDebug } from "../opcode_reader";
import { CacheFileSource, CacheIndex, SubFile } from "../cache";
import { DecodeMode, cacheFileDecodeModes } from "./extractfiles";
import { CLIScriptOutput, ScriptOutput } from "../viewer/scriptsui";
import { FileParser } from "opdecoder";
export type DecodeErrorJson = {
@@ -13,13 +14,15 @@ export type DecodeErrorJson = {
export type DecodeEntry = { major: number, minor: number, subfile: number, file: Buffer, name?: string };
type Outputmode = "json" | "hextext" | "original" | "none";
export function defaultTestDecodeOpts() {
return {
skipMinorAfterError: false,
skipFilesizeAfterError: false,
memlimit: 200e6,
orderBySize: false,
outmode: "json" as "json" | "hextext" | "original" | "none"
outmode: "json" as Outputmode
};
}
@@ -84,93 +87,28 @@ export async function testDecode(output: ScriptOutput, source: CacheFileSource,
if (Date.now() - lastProgress > 10000) {
output.log("progress, file ", file.major, file.minor, file.subfile);
lastProgress = Date.now();
}
getDebug(true);
let state: DecodeState = {
buffer: file.file,
stack: [],
hiddenstack: [],
scan: 0,
startoffset: 0,
endoffset: file.file.byteLength,
args: {}
};
try {
decoder.readInternal(state);
nsuccess++;
return true;
} catch (e) {
errminors.push(file.minor);
errfilesizes.push(file.file.byteLength);
let debugdata = getDebug(false)!;
output.log("decode", file.minor, file.subfile, (e as Error).message);
let res = testDecodeFile(decoder, opts.outmode, file.file, {});
if (output.state == "running") {
let outname = (file.name ? `err-${file.name}` : `err-${file.major}_${file.minor}_${file.subfile}.bin`);
if (opts.outmode == "original") {
output.writeFile(outname, file.file, "bin");
}
if (opts.outmode == "json" || opts.outmode == "hextext") {
let err: DecodeErrorJson = {
chunks: [],
remainder: "",
state: null,
error: (e as Error).message
};
let index = 0;
let outindex = 0;
for (let i = 0; i < debugdata.opcodes.length; i++) {
let op = debugdata.opcodes[i];
let endindex = (i + 1 < debugdata.opcodes.length ? debugdata.opcodes[i + 1].index : state.scan);
let bytes = file.file.slice(index, endindex).toString("hex");
outindex += endindex - index;
let opstr = " ".repeat(op.stacksize - 1) + (typeof op.op == "number" ? "0x" + op.op.toString(16).padStart(2, "0") : op.op);
err.chunks.push({ offset: index, bytes, text: opstr });
index = endindex;
}
err.remainder = file.file.slice(index).toString("hex");
// err.state = state.stack[state.stack.length - 1] ?? null;
err.state = debugdata.structstack[debugdata.structstack.length - 1] ?? null;
if (res.success) {
nsuccess++;
} else {
errminors.push(file.minor);
errfilesizes.push(file.file.byteLength);
maxerrs--;
let filename = (file.name ? `err-${file.name}` : `err-${file.major}_${file.minor}_${file.subfile}`);
if (opts.outmode == "json") {
output.writeFile(outname, JSON.stringify(err), "filedecodeerror");
output.writeFile(filename + ".hexerr.json", res.errorfile);
}
if (opts.outmode == "hextext") {
let chunks: Buffer[] = [];
let outindex = 0;
for (let chunk of err.chunks) {
chunks.push(Buffer.from(chunk.bytes, "hex"));
outindex += chunk.bytes.length;
let opstr = chunk.text.slice(0, 6).padStart(6, "\0");
let minfill = opstr.length + 1;
let fillsize = (outindex == 0 ? 0 : Math.ceil((outindex + minfill) / 16) * 16 - outindex);
if (fillsize > 0) {
chunks.push(Buffer.alloc(1, 0xDD));
chunks.push(Buffer.alloc(fillsize - 1 - opstr.length, 0xff));
chunks.push(Buffer.from(opstr, "ascii"));
}
outindex += fillsize;
}
let remainder = Buffer.from(err.remainder, "hex");
chunks.push(remainder);
outindex += remainder.byteLength
chunks.push(Buffer.alloc(2, 0xcc));
outindex += 2;
let fillsize = (outindex == 0 ? 0 : Math.ceil((outindex + 33) / 16) * 16 - outindex);
chunks.push(Buffer.alloc(fillsize, 0xff));
chunks.push(Buffer.from(err.error, "ascii"));
chunks.push(Buffer.alloc(5));
chunks.push(Buffer.from(err.state, "ascii"));
output.writeFile(outname, Buffer.concat(chunks), "bin");
if (opts.outmode == "original" || opts.outmode == "hextext") {
output.writeFile(filename + ".bin", res.errorfile);
}
}
}
maxerrs--;
return maxerrs > 0;
}
}
for await (let file of fileiter()) {
@@ -184,3 +122,84 @@ export async function testDecode(output: ScriptOutput, source: CacheFileSource,
output.log("completed files:", nsuccess);
}
export function testDecodeFile(decoder: FileParser<any>, outmode: Outputmode, buffer: Buffer, args?: Record<string, any>) {
getDebug(true);
let state: DecodeState = {
buffer: buffer,
stack: [],
hiddenstack: [],
scan: 0,
startoffset: 0,
endoffset: buffer.byteLength,
args: args ?? {}
};
try {
let res = decoder.readInternal(state);
getDebug(false);
return { success: true as true, result: res };
} catch (e) {
let debugdata = getDebug(false)!;
let errorfile: string | Buffer = "";
if (outmode == "original") {
errorfile = buffer;
}
if (outmode == "json" || outmode == "hextext") {
let err: DecodeErrorJson = {
chunks: [],
remainder: "",
state: null,
error: (e as Error).message
};
let index = 0;
for (let i = 0; i < debugdata.opcodes.length; i++) {
let op = debugdata.opcodes[i];
let endindex = (i + 1 < debugdata.opcodes.length ? debugdata.opcodes[i + 1].index : state.scan);
let bytes = buffer.slice(index, endindex).toString("hex");
let opstr = " ".repeat(op.stacksize - 1) + (typeof op.op == "number" ? "0x" + op.op.toString(16).padStart(2, "0") : op.op);
err.chunks.push({ offset: index, bytes, text: opstr });
index = endindex;
}
err.remainder = buffer.slice(index).toString("hex");
// err.state = state.stack[state.stack.length - 1] ?? null;
err.state = debugdata.structstack[debugdata.structstack.length - 1] ?? null;
if (outmode == "json") {
errorfile = JSON.stringify(err);
}
if (outmode == "hextext") {
let chunks: Buffer[] = [];
let outindex = 0;
for (let chunk of err.chunks) {
chunks.push(Buffer.from(chunk.bytes, "hex"));
outindex += chunk.bytes.length;
let opstr = chunk.text.slice(0, 6).padStart(6, "\0");
let minfill = opstr.length + 1;
let fillsize = (outindex == 0 ? 0 : Math.ceil((outindex + minfill) / 16) * 16 - outindex);
if (fillsize > 0) {
chunks.push(Buffer.alloc(1, 0xDD));
chunks.push(Buffer.alloc(fillsize - 1 - opstr.length, 0xff));
chunks.push(Buffer.from(opstr, "ascii"));
}
outindex += fillsize;
}
let remainder = Buffer.from(err.remainder, "hex");
chunks.push(remainder);
outindex += remainder.byteLength
chunks.push(Buffer.alloc(2, 0xcc));
outindex += 2;
let fillsize = (outindex == 0 ? 0 : Math.ceil((outindex + 33) / 16) * 16 - outindex);
chunks.push(Buffer.alloc(fillsize, 0xff));
chunks.push(Buffer.from(err.error, "ascii"));
chunks.push(Buffer.alloc(5));
chunks.push(Buffer.from(err.state, "ascii"));
errorfile = Buffer.concat(chunks);
}
}
return { success: false as false, error: (e as Error), errorfile };
}
}

View File

@@ -419,7 +419,7 @@ export function FileViewer(p: { file: UIScriptFile, onSelectFile: (f: UIScriptFi
let el: React.ReactNode = null;
let filedata = p.file.data;
if (typeof filedata == "string") {
if (p.file.type == "filedecodeerror") {
if (p.file.name.endsWith(".hexerr.json")) {
el = <FileDecodeErrorViewer file={filedata} />;
} else {
el = <SimpleTextViewer file={filedata} />;
@@ -430,7 +430,7 @@ export function FileViewer(p: { file: UIScriptFile, onSelectFile: (f: UIScriptFi
return (
<div style={{ overflow: "auto" }}>
<div>{p.file.name} - {p.file.type ?? "no type"} <span onClick={e => p.onSelectFile(null)}>x</span></div>
<div>{p.file.name} <span onClick={e => p.onSelectFile(null)}>x</span></div>
{el}
</div>
);

View File

@@ -1,7 +1,7 @@
import * as THREE from "three";
import { augmentThreeJsFloorMaterial, ob3ModelToThreejsNode, ThreejsSceneCache, mergeModelDatas, ob3ModelToThree } from '../3d/ob3tothree';
import { ModelModifications, FlatImageData, constrainedMap, delay } from '../utils';
import { ModelModifications, FlatImageData, constrainedMap, delay, packedHSL2HSL, HSL2RGB, RGB2HSL, HSL2packHSL } from '../utils';
import { boundMethod } from 'autobind-decorator';
import { CacheFileSource } from '../cache';
@@ -13,7 +13,7 @@ import { cacheConfigPages, cacheMajors } from "../constants";
import * as React from "react";
import classNames from "classnames";
import { ParsedTexture } from "../3d/textures";
import { appearanceUrl, avatarStringToBytes, avatarToModel } from "../3d/avatar";
import { appearanceUrl, avatarStringToBytes, avatarToModel, EquipCustomization, EquipSlot, slotNames, writeAvatar } from "../3d/avatar";
import { ThreeJsRenderer, ThreeJsRendererEvents, highlightModelGroup, saveGltf, ThreeJsSceneElement, ThreeJsSceneElementSource } from "./threejsrender";
import { ModelData, parseOb3Model } from "../3d/ob3togltf";
import { mountSkeletalSkeleton, parseSkeletalAnimation } from "../3d/animationskeletal";
@@ -30,6 +30,8 @@ import { animgroupconfigs } from "../../generated/animgroupconfigs";
import { runMapRender } from "../map";
import { diffCaches } from "../scripts/cachediff";
import { JsonSearch } from "./jsonsearch";
import { extractAvatars, scrapePlayerAvatars } from "../scripts/scrapeavatars";
import { avataroverrides } from "../../generated/avataroverrides";
type LookupMode = "model" | "item" | "npc" | "object" | "material" | "map" | "avatar" | "spotanim" | "scenario" | "scripts";
@@ -40,7 +42,7 @@ export class ModelBrowser extends React.Component<{ ctx: UIContext }, { search:
super(p);
this.state = {
mode: localStorage.rsmv_lastmode ?? "model",
search: localStorage.rsmv_lastsearch ?? "0"
search: localStorage.rsmv_lastsearch ?? ""
};
}
@@ -100,7 +102,7 @@ export function StringInput({ initialid, onChange }: { initialid?: string, onCha
export type SimpleModelDef = { modelid: number, mods: ModelModifications }[];
export class RSModel extends TypedEmitter<{ loaded: undefined }> implements ThreeJsSceneElementSource {
export class RSModel extends TypedEmitter<{ loaded: undefined, animchanged: number }> implements ThreeJsSceneElementSource {
model: Promise<{ modeldata: ModelData, mesh: Object3D, nullAnim: AnimationClip }>;
loaded: { modeldata: ModelData, mesh: Object3D, nullAnim: AnimationClip } | null = null;
cache: ThreejsSceneCache;
@@ -351,23 +353,36 @@ function LabeledInput(p: { label: string, children: React.ReactNode }) {
export class InputCommitted extends React.Component<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>>{
el: HTMLInputElement | null = null;
stale = false;
@boundMethod
onInput() {
this.stale = true;
}
@boundMethod
onChange(e: Event) {
this.props.onChange?.(e as any);
this.stale = false;
}
@boundMethod
ref(el: HTMLInputElement | null) {
if (this.el) {
this.el.removeEventListener("change", this.onChange);
this.el.removeEventListener("input", this.onInput);
}
if (el) {
el.addEventListener("change", this.onChange);
el.addEventListener("input", this.onInput);
this.el = el;
}
}
render() {
if (!this.stale && this.el && this.props.value) {
this.el.value = this.props.value as string;
}
let newp = { ...this.props, onChange: undefined, value: undefined, defaultValue: this.props.value };
return <input ref={this.ref} {...newp} />;
}
@@ -730,11 +745,16 @@ async function modelToModel(cache: ThreejsSceneCache, id: number) {
return { models: [{ modelid: id, mods: {} }], anims: {}, info: { modeldata, info } };
}
async function playerDataToModel(cache: ThreejsSceneCache, modeldata: { head: boolean, data: Buffer }) {
let avainfo = await avatarToModel(null, cache, modeldata.data, "", modeldata.head);
return avainfo;
}
async function playerToModel(cache: ThreejsSceneCache, name: string) {
let url = appearanceUrl(name);
let data = await fetch(url).then(q => q.text());
if (data.indexOf("404 - Page not found") != -1) { throw new Error("player avatar not found"); }
let avainfo = await avatarToModel(cache, avatarStringToBytes(data));
let avainfo = await avatarToModel(null, cache, avatarStringToBytes(data), "", false);
return avainfo;
}
@@ -854,11 +874,50 @@ async function materialToModel(sceneCache: ThreejsSceneCache, modelid: number) {
}
function ScenePlayer(p: LookupModeProps) {
const [data, model, setId] = useAsyncModelData(p.initialId, p.ctx, playerToModel);
const [data, model, setId] = useAsyncModelData(p.ctx, playerDataToModel);
const [head, setHead] = React.useState(false);
const modelBuf = React.useRef<Buffer | null>(null);
const forceUpdate = useForceUpdate();
const oncheck = (e: React.ChangeEvent<HTMLInputElement>) => {
if (modelBuf.current) { setId({ data: modelBuf.current, head: e.currentTarget.checked }); }
setHead(e.currentTarget.checked);
}
const nameChange = async (v: string) => {
let url = appearanceUrl(v);
let data = await fetch(url).then(q => q.text());
if (data.indexOf("404 - Page not found") != -1) { throw new Error("player avatar not found"); }
modelBuf.current = avatarStringToBytes(data);
setId({ data: modelBuf.current, head });
}
const equipChanged = (index: number, type: "item" | "kit" | "none", id: number) => {
let oldava = data?.info.avatar;
if (!oldava) { console.trace("unexpected"); return; }
let newava = { ...oldava };
newava.slots = oldava.slots.slice() as any;
if (type == "none") {
newava.slots[index] = { slot: null, cust: null };
} else {
newava.slots[index] = { slot: { type, id } as EquipSlot, cust: null };
}
modelBuf.current = writeAvatar(newava, data?.info.gender ?? 0, null);
setId({ data: modelBuf.current, head });
}
const customizationChanged = (index: number, cust: EquipCustomization) => {
let oldava = data?.info.avatar
if (!oldava) { console.trace("unexpected"); return; }
let newava = { ...oldava };
newava.slots = oldava.slots.slice() as any;
newava.slots[index] = { ...oldava.slots[index], cust };
modelBuf.current = writeAvatar(newava, data?.info.gender ?? 0, null);
setId({ data: modelBuf.current, head });
}
return (
<React.Fragment>
<StringInput onChange={setId} initialid={p.initialId} />
<StringInput onChange={nameChange} initialid={""} />
<label><input type="checkbox" checked={head} onChange={oncheck} />Head</label>
<ExportModelButton model={model?.loaded} />
{model && data && (
<LabeledInput label="Animation">
@@ -867,16 +926,109 @@ function ScenePlayer(p: LookupModeProps) {
</select>
</LabeledInput>
)}
<div>
{data?.info.items.map((q, i) => (
<div key={i}>{q.name ?? "??"}</div>
))}
<div style={{ userSelect: "text" }}>
{data?.info.avatar?.slots.map((q, i) => {
return (
<AvatarSlot key={i} index={i} slot={q.slot} cust={q.cust} custChanged={customizationChanged} equipChanged={equipChanged} />
);
})}
</div>
<JsonDisplay obj={data?.info.animset} />
</React.Fragment>
)
}
function AvatarSlot({ index, slot, cust, custChanged, equipChanged }: { index: number, slot: EquipSlot | null, cust: EquipCustomization, equipChanged: (index, type: "kit" | "item" | "none", id: number) => void, custChanged: (index: number, v: EquipCustomization) => void }) {
let editcust = (ch?: (cust: NonNullable<EquipCustomization>) => {}) => {
if (!ch) { custChanged(index, null); }
else {
let newcust = { color: null, flag2: null, material: null, model: null, ...cust };
ch(newcust);
if (!newcust.color && !newcust.flag2 && !newcust.material && !newcust.model) { custChanged(index, null); }
else { custChanged(index, newcust); }
}
}
return (
<div>
{slot && (
<div style={{ display: "grid", gridTemplateColumns: "auto repeat(10,min-content)" }}>
{slot.name}
{!cust?.color?.col2 && !cust?.color?.col4 && slot.replaceColors.length != 0 && (
<input type="button" className="sub-btn" value="C" onClick={e => editcust(c => c.color = { col4: null, col2: slot.replaceColors.map(q => q[1]) })} />
)}
{!cust?.color?.col2 && !cust?.color?.col4 && (
<input type="button" className="sub-btn" value="C4" onClick={e => editcust(c => c.color = { col4: [[0, 0], [0, 0], [0, 0], [0, 0]], col2: null })} />
)}
{!cust?.material && slot.replaceMaterials.length != 0 && (
<input type="button" className="sub-btn" value="T" onClick={e => editcust(c => c.material = { header: 0, materials: slot.replaceMaterials.map(q => q[1]) })} />
)}
{!cust?.model && (
<input type="button" className="sub-btn" value="M" onClick={e => editcust(c => c.model = slot.models.slice())} />
)}
<input type="button" className="sub-btn" value="x" onClick={e => equipChanged(index, "none", 0)} />
</div>
)}
{!slot && (
<div style={{ display: "grid", gridTemplateColumns: "auto repeat(10,min-content)" }}>
{slotNames[index]}
<input type="button" className="sub-btn" value="Item" />
<input type="button" className="sub-btn" value="Kit" />
</div>
)}
{slot && cust?.color?.col2 && (
<div>
{slot.replaceColors.map((q, i) => (
<InputCommitted key={i} type="color" value={hsl2hex(cust.color!.col2![i])} onChange={e => editcust(c => c.color!.col2![i] = hex2hsl(e.currentTarget.value))} />
))}
<input type="button" className="sub-btn" value="x" onClick={e => editcust(c => c.color = null!)} />
</div>
)}
{slot && cust?.color?.col4 && (
<div>
{cust.color.col4.map(([from, to], i) => (
<span key={i}>
<InputCommitted type="number" value={from} onChange={e => editcust(c => c.color!.col4![i][0] = +e.currentTarget.value)} />
<InputCommitted type="color" value={hsl2hex(to)} onChange={e => editcust(c => c.color!.col4![i][1] = hex2hsl(e.currentTarget.value))} />
</span>
))}
<input type="button" className="sub-btn" value="x" onClick={e => editcust(c => c.color = null!)} />
</div>
)}
{slot && cust?.material && (
<div>
{slot.replaceMaterials.map((q, i) => (
<InputCommitted key={i} type="number" value={cust.material!.materials![i]} onChange={e => editcust(c => c.material!.materials[i] = +e.currentTarget.value)} />
))}
<input type="button" className="sub-btn" value="x" onClick={e => editcust(c => c.material = null!)} />
</div>
)}
{slot && cust?.model && (
<div>
{slot.models.map((modelid, i) => (
<InputCommitted key={i} type="number" value={modelid} onChange={e => editcust(c => c.model![i] = +e.currentTarget.value)} />
))}
<input type="button" className="sub-btn" value="x" onClick={e => editcust(c => c.model = null!)} />
</div>
)}
</div>
)
}
function hsl2hex(hsl: number) {
let rgb = HSL2RGB(packedHSL2HSL(hsl));
return `#${((rgb[0] << 16) | (rgb[1] << 8) | (rgb[2] << 0)).toString(16).padStart(6, "0")}`;
}
function hex2hsl(hex: string) {
let n = parseInt(hex.replace(/^#/, ""), 16);
return HSL2packHSL(...RGB2HSL((n >> 16) & 0xff, (n >> 8) & 0xff, (n >> 0) & 0xff));
}
globalThis.hsl2hex = hsl2hex;
globalThis.hex2hsl = hex2hsl;
function ExportModelButton(p: { model: RSModel["loaded"] | null | undefined }) {
let exportmodel = () => {
if (p.model) {
@@ -914,8 +1066,8 @@ function ImageData(p: { img: ImageData }) {
)
}
function useAsyncModelData<ID, T>(initial: ID, ctx: UIContextReady | null, getter: (cache: ThreejsSceneCache, id: ID) => Promise<SimpleModelInfo<T>>) {
let idref = React.useRef(initial);
function useAsyncModelData<ID, T>(ctx: UIContextReady | null, getter: (cache: ThreejsSceneCache, id: ID) => Promise<SimpleModelInfo<T>>) {
let idref = React.useRef<ID | null>(null);
let [loadedModel, setLoadedModel] = React.useState<RSModel | null>(null);
let [visible, setVisible] = React.useState<{ info: SimpleModelInfo<T>, id: ID } | null>(null);
let ctxref = React.useRef(ctx);
@@ -926,7 +1078,7 @@ function useAsyncModelData<ID, T>(initial: ID, ctx: UIContextReady | null, gette
let prom = getter(ctxref.current.sceneCache, id);
prom.then(res => {
if (idref.current == id) {
localStorage.rsmv_lastsearch = id;
localStorage.rsmv_lastsearch = JSON.stringify(id);
setVisible({ info: res, id });
}
})
@@ -952,7 +1104,7 @@ function useAsyncModelData<ID, T>(initial: ID, ctx: UIContextReady | null, gette
}
function SceneMaterial(p: LookupModeProps) {
let [data, model, setId] = useAsyncModelData(+p.initialId, p.ctx, materialToModel);
let [data, model, setId] = useAsyncModelData(p.ctx, materialToModel);
return (
<React.Fragment>
@@ -971,7 +1123,7 @@ function SceneMaterial(p: LookupModeProps) {
}
function SceneRawModel(p: LookupModeProps) {
let [data, model, setId] = useAsyncModelData(+p.initialId, p.ctx, modelToModel);
let [data, model, setId] = useAsyncModelData(p.ctx, modelToModel);
return (
<React.Fragment>
<IdInput onChange={setId} initialid={+p.initialId} />
@@ -983,7 +1135,7 @@ function SceneRawModel(p: LookupModeProps) {
}
function SceneLocation(p: LookupModeProps) {
const [data, model, setId] = useAsyncModelData(+p.initialId, p.ctx, locToModel);
const [data, model, setId] = useAsyncModelData(p.ctx, locToModel);
const forceUpdate = useForceUpdate();
const anim = data?.anims.default ?? -1;
return (
@@ -997,7 +1149,7 @@ function SceneLocation(p: LookupModeProps) {
}
function SceneItem(p: LookupModeProps) {
let [data, model, setId] = useAsyncModelData(+p.initialId, p.ctx, itemToModel);
let [data, model, setId] = useAsyncModelData(p.ctx, itemToModel);
return (
<React.Fragment>
<IdInput onChange={setId} initialid={+p.initialId} />
@@ -1009,7 +1161,7 @@ function SceneItem(p: LookupModeProps) {
}
function SceneNpc(p: LookupModeProps) {
const [data, model, setId] = useAsyncModelData(+p.initialId, p.ctx, npcToModel);
const [data, model, setId] = useAsyncModelData(p.ctx, npcToModel);
const forceUpdate = useForceUpdate();
return (
<React.Fragment>
@@ -1028,7 +1180,7 @@ function SceneNpc(p: LookupModeProps) {
}
function SceneSpotAnim(p: LookupModeProps) {
let [data, model, setId] = useAsyncModelData(+p.initialId, p.ctx, spotAnimToModel);
let [data, model, setId] = useAsyncModelData(p.ctx, spotAnimToModel);
return (
<React.Fragment>
<IdInput onChange={setId} initialid={+p.initialId} />
@@ -1290,6 +1442,31 @@ function ExtractFilesScript(p: { onRun: (output: UIScriptOutput) => void, source
</React.Fragment>
)
}
function ExtractAvatarsScript(p: { onRun: (output: UIScriptOutput) => void, source: CacheFileSource }) {
let [files, setFiles] = React.useState<FileList | null>(null);
let run = () => {
if (!files) { return; }
let output = new UIScriptOutput();
let iter = (async function* () {
for (let i = 0; i < files!.length; i++) {
yield {
name: files![i].name,
buf: Buffer.from(await files![i].arrayBuffer())
}
}
})();
output.run(extractAvatars, p.source, iter);
p.onRun(output);
}
return (
<React.Fragment>
<input type="file" multiple={true} onChange={e => setFiles(e.currentTarget.files)} />
<input type="button" className="sub-btn" value="Run" onClick={run} />
</React.Fragment>
)
}
function MaprenderScript(p: { onRun: (output: UIScriptOutput) => void, source: CacheFileSource }) {
let [endpoint, setEndpoint] = React.useState("");
let [auth, setAuth] = React.useState("");
@@ -1361,7 +1538,15 @@ function TestFilesScript(p: { onRun: (output: UIScriptOutput) => void, source: C
)
}
class ScriptsUI extends React.Component<LookupModeProps, { script: "test" | "extract" | "maprender" | "diff", running: UIScriptOutput | null }>{
const uiScripts = {
test: TestFilesScript,
extract: ExtractFilesScript,
maprender: MaprenderScript,
diff: CacheDiffScript
}
class ScriptsUI extends React.Component<LookupModeProps, { script: keyof typeof uiScripts, running: UIScriptOutput | null }>{
constructor(p) {
super(p);
this.state = {
@@ -1379,19 +1564,16 @@ class ScriptsUI extends React.Component<LookupModeProps, { script: "test" | "ext
render() {
const source = this.props.partial.source;
if (!source) { throw new Error("trying to render modelbrowser wouth source loaded"); }
const SelectedScript = uiScripts[this.state.script];
return (
<React.Fragment>
<h2>Script runner</h2>
<div className="sidebar-browser-tab-strip">
<div className={classNames("rsmv-icon-button", { active: this.state.script == "test" })} onClick={() => this.setState({ script: "test" })}>Test</div>
<div className={classNames("rsmv-icon-button", { active: this.state.script == "extract" })} onClick={() => this.setState({ script: "extract" })}>Extract</div>
<div className={classNames("rsmv-icon-button", { active: this.state.script == "maprender" })} onClick={() => this.setState({ script: "maprender" })}>Maprender</div>
<div className={classNames("rsmv-icon-button", { active: this.state.script == "diff" })} onClick={() => this.setState({ script: "diff" })}>Diff</div>
{Object.keys(uiScripts).map((q, i) => (
<div key={q} className={classNames("rsmv-icon-button", { active: this.state.script == q })} onClick={() => this.setState({ script: q as any })}>{q}</div>
))}
</div>
{this.state.script == "test" && <TestFilesScript source={source} onRun={this.onRun} />}
{this.state.script == "extract" && <ExtractFilesScript source={source} onRun={this.onRun} />}
{this.state.script == "maprender" && <MaprenderScript source={source} onRun={this.onRun} />}
{this.state.script == "diff" && <CacheDiffScript source={source} onRun={this.onRun} />}
{SelectedScript && <SelectedScript source={source} onRun={this.onRun} />}
<h2>Script output</h2>
<OutputUI output={this.state.running} ctx={this.props.partial} />
</React.Fragment>

View File

@@ -13,7 +13,7 @@ export interface ScriptOutput {
log(...args: any[]): void;
setUI(ui: HTMLElement | null): void;
mkDir(name: string): Promise<any>;
writeFile(name: string, data: Buffer | string, type?: string): Promise<void>;
writeFile(name: string, data: Buffer | string): Promise<void>;
setState(state: ScriptState): void;
run<ARGS extends any[], RET extends any>(fn: (output: ScriptOutput, ...args: [...ARGS]) => Promise<RET>, ...args: ARGS): Promise<RET | null>;
}
@@ -24,6 +24,7 @@ export class CLIScriptOutput implements ScriptOutput {
constructor(dir: string) {
this.dir = dir;
fs.mkdirSync(dir, { recursive: true });
}
log(...args: any[]) {
@@ -35,7 +36,7 @@ export class CLIScriptOutput implements ScriptOutput {
mkDir(name: string) {
return fs.promises.mkdir(path.resolve(this.dir, name), { recursive: true });
}
writeFile(name: string, data: Buffer | string, type?: string) {
writeFile(name: string, data: Buffer | string) {
return fs.promises.writeFile(path.resolve(this.dir, name), data);
}
@@ -61,7 +62,7 @@ export class CLIScriptOutput implements ScriptOutput {
}
}
export type UIScriptFile = { name: string, data: Buffer | string, type: string };
export type UIScriptFile = { name: string, data: Buffer | string };
export class UIScriptOutput extends TypedEmitter<{ log: string, writefile: undefined, statechange: undefined }> implements ScriptOutput {
state: ScriptState = "running";
logs: string[] = [];
@@ -80,8 +81,8 @@ export class UIScriptOutput extends TypedEmitter<{ log: string, writefile: undef
this.outdirhandles.set(name, null);
this.emit("writefile", undefined);
}
async writeFile(name: string, data: Buffer | string, type?: string) {
this.files.push({ name, data, type: type ?? "" });
async writeFile(name: string, data: Buffer | string) {
this.files.push({ name, data });
if (this.rootdirhandle) { await this.saveLocalFile(name, data); }
this.emit("writefile", undefined);
}