mirror of
https://github.com/skillbert/rsmv.git
synced 2025-12-23 21:47:48 -05:00
player avatar customization
This commit is contained in:
306
generated/avataroverrides.d.ts
vendored
Normal file
306
generated/avataroverrides.d.ts
vendored
Normal 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,
|
||||
};
|
||||
2
generated/avatars.d.ts
vendored
2
generated/avatars.d.ts
vendored
@@ -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
7
generated/npcs.d.ts
vendored
@@ -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
|
||||
|
||||
316
src/3d/avatar.ts
316
src/3d/avatar.ts
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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") } }
|
||||
}
|
||||
|
||||
79
src/opcodes/avataroverrides.jsonc
Normal file
79
src/opcodes/avataroverrides.jsonc
Normal 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"]
|
||||
]
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]]
|
||||
]]]
|
||||
],
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user