support ~2016 models

This commit is contained in:
Skillbert
2023-02-25 23:16:51 +01:00
parent ad6782e349
commit fbd2feadbb
23 changed files with 539 additions and 276 deletions

View File

@@ -11,9 +11,11 @@ export type materials = {
opt0data: [
number,
number,
number,
] | null,
arr: number[],
arr: {
op: number,
value: number,
}[],
textureflags: number,
diffuse: (number|0) | null,
normal: (number|0) | null,

View File

@@ -3,13 +3,18 @@
// run `npm run filetypes` to rebuild
export type rootcacheindex = {
cachemajors: {
minor: number,
crc: number,
version: number,
subindexcount: (number|0),
integer_10: (number|0),
maybe_checksum1: Uint8Array,
}[],
cachemajors: ({
minor: number,
crc: number,
version: number,
subindexcount: (number|0),
integer_10: (number|0),
maybe_checksum1: Uint8Array,
}[]|{
minor: number,
crc: number,
version: (number|0),
subindexcount: 0,
}[]),
maybe_proper_checksum: Uint8Array,
};

View File

@@ -6,9 +6,6 @@ import type { CacheFileSource } from "cache";
export type MaterialData = {
textures: {
diffuse?: number,
specular?: number,
metalness?: number,
color?: number,
normal?: number,
compound?: number
},
@@ -38,7 +35,7 @@ export function materialCacheKey(matid: number, hasVertexAlpha: boolean) {
return matid | (hasVertexAlpha ? 0x800000 : 0);
}
export function convertMaterial(data: Buffer, source: CacheFileSource) {
export function convertMaterial(data: Buffer, materialid: number, source: CacheFileSource) {
let rawparsed = parse.materials.read(data, source);
let mat = defaultMaterial();
@@ -46,8 +43,11 @@ export function convertMaterial(data: Buffer, source: CacheFileSource) {
if (rawparsed.v0) {
let raw = rawparsed.v0;
mat.textures.diffuse = raw.arr.find(q => q.op == 1)?.value;
if (raw.diffuse) { mat.textures.diffuse = raw.diffuse; }
else if (raw.textureflags & 0x11) { mat.textures.diffuse = materialid; }
if (raw.normal) { mat.textures.normal = raw.normal; }
else if (raw.textureflags & 0x0a) { mat.textures.normal = materialid; }
mat.alphamode = raw.alphamode == 0 ? "opaque" : raw.alphamode == 1 ? "cutoff" : "blend";
if (raw.alphacutoff) { mat.alphacutoff = raw.alphacutoff / 255; }

View File

@@ -1031,7 +1031,7 @@ export async function mapsquareModels(scene: ThreejsSceneCache, grid: TileGrid,
for (let [matid, repeat] of matids.entries()) {
let mat = scene.engine.getMaterialData(matid);
if (mat.textures.diffuse) {
textureproms.push(scene.getTextureFile(mat.textures.diffuse, mat.stripDiffuseAlpha)
textureproms.push(scene.getTextureFile("diffuse", mat.textures.diffuse, mat.stripDiffuseAlpha)
.then(tex => tex.toWebgl())
.then(src => {
textures.set(mat.textures.diffuse!, { tex: src, repeat });

View File

@@ -14,6 +14,7 @@ import { svgfloor } from "../map/svgrender";
import { ThreeJsRenderer, ThreeJsSceneElement, ThreeJsSceneElementSource } from "../viewer/threejsrender";
import { animgroupconfigs } from "../../generated/animgroupconfigs";
import fetch from "node-fetch";
import { MaterialData } from "./jmat";
export type SimpleModelDef = {
@@ -128,15 +129,15 @@ export async function materialToModel(sceneCache: ThreejsSceneCache, modelid: nu
// ];
let mat = sceneCache.engine.getMaterialData(modelid);
let texs: Record<string, { texid: number, filesize: number, img0: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | ImageBitmap }> = {};
let addtex = async (name: string, texid: number) => {
let tex = await sceneCache.getTextureFile(texid, mat.stripDiffuseAlpha && name == "diffuse");
let addtex = async (type: keyof MaterialData["textures"], name: string, texid: number) => {
let tex = await sceneCache.getTextureFile(type, texid, mat.stripDiffuseAlpha && name == "diffuse");
let drawable = await tex.toWebgl();
texs[name] = { texid, filesize: tex.filesize, img0: drawable };
}
for (let tex in mat.textures) {
if (mat.textures[tex] != 0) {
await addtex(tex, mat.textures[tex]);
await addtex(tex as keyof MaterialData["textures"], tex, mat.textures[tex]);
}
}
return {

View File

@@ -134,7 +134,7 @@ export class EngineCache extends CachingFileSource {
} else {
let file = this.materialArchive.get(id);
if (!file) { throw new Error("material " + id + " not found"); }
cached = convertMaterial(file, this.rawsource);
cached = convertMaterial(file, id, this.rawsource);
}
this.materialCache.set(id, cached);
}
@@ -178,27 +178,125 @@ export class EngineCache extends CachingFileSource {
}
export async function detectTextureMode(source: CacheFileSource) {
let lastdds = -1;
try {
let ddsindex = await source.getCacheIndex(cacheMajors.texturesDds);
let last = ddsindex[ddsindex.length - 1];
await source.getFile(last.major, last.minor, last.crc);
lastdds = last.minor;
} catch (e) { }
let detectmajor = async (major: number) => {
let lastfile = -1;
try {
let indexfile = await source.getCacheIndex(major);
let last = indexfile[indexfile.length - 1];
await source.getFile(last.major, last.minor, last.crc);
lastfile = last.minor;
} catch (e) { }
return lastfile;
}
let lastbmp = -1;
try {
let bmpindex = await source.getCacheIndex(cacheMajors.texturesBmp);
let last = bmpindex[bmpindex.length - 1];
await source.getFile(last.major, last.minor, last.crc);
lastbmp = last.minor;
} catch (e) { }
let textureMode: TextureModes = "dds";
let numbmp = await detectmajor(cacheMajors.texturesBmp);
let numdds = await detectmajor(cacheMajors.texturesDds);
if (numbmp > 0 || numdds > 0) {
textureMode = (numbmp > numdds ? "bmp" : "dds");
} else {
let numpng2014 = await detectmajor(cacheMajors.textures2015Png);
let numdds2014 = await detectmajor(cacheMajors.textures2015Dds);
if (numpng2014 > 0 || numdds2014 >= 0) {
textureMode = (numdds2014 > numpng2014 ? "dds2014" : "png2014");
} else if (await detectmajor(cacheMajors.texturesOldPng) > 0) {
textureMode = "oldpng";
}
}
console.log(`detectedtexture mode. ${textureMode}`);
let textureMode: "bmp" | "dds" = (lastbmp > lastdds ? "bmp" : "dds");
console.log(`detectedtexture mode. dds:${lastdds}, bmp:${lastbmp}`, textureMode);
return textureMode;
}
async function convertMaterialToThree(source: ThreejsSceneCache, material: MaterialData, hasVertexAlpha: boolean) {
// let mat = new THREE.MeshPhongMaterial();
// mat.shininess = 0;
let mat = new THREE.MeshStandardMaterial();
mat.alphaTest = (material.alphamode == "cutoff" ? 0.5 : 0.1);//TODO use value from material
mat.transparent = hasVertexAlpha || material.alphamode == "blend";
const wraptype = THREE.RepeatWrapping;//TODO find value of this in material
if (material.textures.diffuse) {
let diffuse = await (await source.getTextureFile("diffuse", material.textures.diffuse, material.stripDiffuseAlpha)).toImageData();
let difftex = new THREE.DataTexture(diffuse.data, diffuse.width, diffuse.height, THREE.RGBAFormat);
difftex.needsUpdate = true;
difftex.wrapS = wraptype;
difftex.wrapT = wraptype;
difftex.encoding = THREE.sRGBEncoding;
difftex.magFilter = THREE.LinearFilter;
difftex.minFilter = THREE.NearestMipMapNearestFilter;
difftex.generateMipmaps = true;
mat.map = difftex;
if (material.textures.normal) {
let parsed = await source.getTextureFile("normal", material.textures.normal, false);
let raw = await parsed.toImageData();
let normals = makeImageData(null, raw.width, raw.height);
let emisive = makeImageData(null, raw.width, raw.height);
const data = raw.data;
for (let i = 0; i < data.length; i += 4) {
//normals
let dx = data[i + 1] / 127.5 - 1;
let dy = data[i + 3] / 127.5 - 1;
normals.data[i + 0] = data[i + 1];
normals.data[i + 1] = data[i + 3];
normals.data[i + 2] = (Math.sqrt(Math.max(1 - dx * dx - dy * dy, 0)) + 1) * 127.5;
normals.data[i + 3] = 255;
//emisive //TODO check if normals flag always implies emisive
const emissive = data[i + 0] / 255;
emisive.data[i + 0] = diffuse.data[i + 0] * emissive;
emisive.data[i + 1] = diffuse.data[i + 1] * emissive;
emisive.data[i + 2] = diffuse.data[i + 2] * emissive;
emisive.data[i + 3] = 255;
}
mat.normalMap = new THREE.DataTexture(normals.data, normals.width, normals.height, THREE.RGBAFormat);
mat.normalMap.needsUpdate = true;
mat.normalMap.wrapS = wraptype;
mat.normalMap.wrapT = wraptype;
mat.normalMap.magFilter = THREE.LinearFilter;
mat.emissiveMap = new THREE.DataTexture(emisive.data, emisive.width, emisive.height, THREE.RGBAFormat);
mat.emissiveMap.needsUpdate = true;
mat.emissiveMap.wrapS = wraptype;
mat.emissiveMap.wrapT = wraptype;
mat.emissiveMap.magFilter = THREE.LinearFilter;
mat.emissive.setRGB(material.reflectionColor[0] / 255, material.reflectionColor[1] / 255, material.reflectionColor[2] / 255);
}
if (material.textures.compound) {
let compound = await (await source.getTextureFile("compound", material.textures.compound, false)).toImageData();
let compoundmapped = makeImageData(null, compound.width, compound.height);
//threejs expects g=metal,b=roughness, rs has r=metal,g=roughness
for (let i = 0; i < compound.data.length; i += 4) {
compoundmapped.data[i + 1] = compound.data[i + 1];
compoundmapped.data[i + 2] = compound.data[i + 0];
compoundmapped.data[i + 3] = 255;
}
let tex = new THREE.DataTexture(compoundmapped.data, compoundmapped.width, compoundmapped.height, THREE.RGBAFormat);
tex.needsUpdate = true;
tex.wrapS = wraptype;
tex.wrapT = wraptype;
tex.encoding = THREE.sRGBEncoding;
tex.magFilter = THREE.LinearFilter;
mat.metalnessMap = tex;
mat.roughnessMap = tex;
mat.metalness = 1;
}
}
mat.vertexColors = material.vertexColorWhitening != 1 || hasVertexAlpha;
mat.userData = material;
if (material.uvAnim) {
(mat.userData.gltfExtensions ??= {}).RA_materials_uvanim = {
uvAnim: [material.uvAnim.u, material.uvAnim.v]
};
}
return { mat, matmeta: material };
}
type TextureModes = "png" | "dds" | "bmp" | "ktx" | "oldpng" | "png2014" | "dds2014";
type TextureTypes = keyof MaterialData["textures"];
export class ThreejsSceneCache {
private modelCache = new Map<number, CachedObject<ModelData>>();
@@ -206,28 +304,55 @@ export class ThreejsSceneCache {
private threejsTextureCache = new Map<number, CachedObject<ParsedTexture>>();
private threejsMaterialCache = new Map<number, CachedObject<ParsedMaterial>>();
engine: EngineCache;
textureType: "png" | "dds" | "bmp" | "ktx" = "dds";//png support currently incomplete (and seemingly unused by jagex)
textureType: TextureModes = "png2014";
useOldModels: boolean;
static textureIndices = {
png: cacheMajors.texturesPng,
dds: cacheMajors.texturesDds,
bmp: cacheMajors.texturesBmp,
ktx: cacheMajors.texturesKtx
static textureIndices: Record<TextureTypes, Record<TextureModes, number>> = {
diffuse: {
png: cacheMajors.texturesPng,
dds: cacheMajors.texturesDds,
bmp: cacheMajors.texturesBmp,
ktx: cacheMajors.texturesKtx,
png2014: cacheMajors.textures2015Png,
dds2014: cacheMajors.textures2015Dds,
oldpng: cacheMajors.texturesOldPng
},
normal: {
png: cacheMajors.texturesPng,
dds: cacheMajors.texturesDds,
bmp: cacheMajors.texturesBmp,
ktx: cacheMajors.texturesKtx,
//TODO are these normals or compounds?
png2014: cacheMajors.textures2015CompoundPng,
dds2014: cacheMajors.textures2015CompoundDds,
oldpng: cacheMajors.texturesOldCompoundPng
},
compound: {
png: cacheMajors.texturesPng,
dds: cacheMajors.texturesDds,
bmp: cacheMajors.texturesBmp,
ktx: cacheMajors.texturesKtx,
//TODO are these normals or compounds?
png2014: cacheMajors.textures2015CompoundPng,
dds2014: cacheMajors.textures2015CompoundDds,
oldpng: cacheMajors.texturesOldCompoundPng
}
}
constructor(scenecache: EngineCache) {
this.engine = scenecache;
this.useOldModels = scenecache.hasOldModels && !scenecache.hasNewModels;
//TODO set useOldModels depending on cache build nr
}
getFileById(major: number, id: number) {
return this.engine.getFileById(major, id);
}
getTextureFile(texid: number, stripAlpha: boolean) {
return this.engine.fetchCachedObject(this.threejsTextureCache, texid, async () => {
let file = await this.getFileById(ThreejsSceneCache.textureIndices[this.textureType], texid);
getTextureFile(type: TextureTypes, texid: number, stripAlpha: boolean) {
let cacheindex = ThreejsSceneCache.textureIndices[type][this.textureType];
let cachekey = ((cacheindex | 0xff) << 23) | texid;
return this.engine.fetchCachedObject(this.threejsTextureCache, cachekey, async () => {
let file = await this.getFileById(cacheindex, texid);
let parsed = new ParsedTexture(file, stripAlpha, true);
return parsed;
}, obj => obj.filesize * 2);
@@ -248,104 +373,19 @@ export class ThreejsSceneCache {
}
getMaterial(matid: number, hasVertexAlpha: boolean) {
//TODO the material should have this data, not the mesh
let matcacheid = materialCacheKey(matid, hasVertexAlpha);
return this.engine.fetchCachedObject(this.threejsMaterialCache, matcacheid, async () => {
let material = this.engine.getMaterialData(matid);
// let mat = new THREE.MeshPhongMaterial();
// mat.shininess = 0;
let mat = new THREE.MeshStandardMaterial();
mat.alphaTest = (material.alphamode == "cutoff" ? 0.5 : 0.1);//TODO use value from material
mat.transparent = hasVertexAlpha || material.alphamode == "blend";
const wraptype = THREE.RepeatWrapping;//TODO find value of this in material
if (material.textures.diffuse) {
let diffuse = await (await this.getTextureFile(material.textures.diffuse, material.stripDiffuseAlpha)).toImageData();
let difftex = new THREE.DataTexture(diffuse.data, diffuse.width, diffuse.height, THREE.RGBAFormat);
difftex.needsUpdate = true;
difftex.wrapS = wraptype;
difftex.wrapT = wraptype;
difftex.encoding = THREE.sRGBEncoding;
difftex.magFilter = THREE.LinearFilter;
difftex.minFilter = THREE.NearestMipMapNearestFilter;
difftex.generateMipmaps = true;
mat.map = difftex;
if (material.textures.normal) {
let parsed = await this.getTextureFile(material.textures.normal, false);
let raw = await parsed.toImageData();
let normals = makeImageData(null, raw.width, raw.height);
let emisive = makeImageData(null, raw.width, raw.height);
const data = raw.data;
for (let i = 0; i < data.length; i += 4) {
//normals
let dx = data[i + 1] / 127.5 - 1;
let dy = data[i + 3] / 127.5 - 1;
normals.data[i + 0] = data[i + 1];
normals.data[i + 1] = data[i + 3];
normals.data[i + 2] = (Math.sqrt(Math.max(1 - dx * dx - dy * dy, 0)) + 1) * 127.5;
normals.data[i + 3] = 255;
//emisive //TODO check if normals flag always implies emisive
const emissive = data[i + 0] / 255;
emisive.data[i + 0] = diffuse.data[i + 0] * emissive;
emisive.data[i + 1] = diffuse.data[i + 1] * emissive;
emisive.data[i + 2] = diffuse.data[i + 2] * emissive;
emisive.data[i + 3] = 255;
}
mat.normalMap = new THREE.DataTexture(normals.data, normals.width, normals.height, THREE.RGBAFormat);
mat.normalMap.needsUpdate = true;
mat.normalMap.wrapS = wraptype;
mat.normalMap.wrapT = wraptype;
mat.normalMap.magFilter = THREE.LinearFilter;
mat.emissiveMap = new THREE.DataTexture(emisive.data, emisive.width, emisive.height, THREE.RGBAFormat);
mat.emissiveMap.needsUpdate = true;
mat.emissiveMap.wrapS = wraptype;
mat.emissiveMap.wrapT = wraptype;
mat.emissiveMap.magFilter = THREE.LinearFilter;
mat.emissive.setRGB(material.reflectionColor[0] / 255, material.reflectionColor[1] / 255, material.reflectionColor[2] / 255);
}
if (material.textures.compound) {
let compound = await (await this.getTextureFile(material.textures.compound, false)).toImageData();
let compoundmapped = makeImageData(null, compound.width, compound.height);
//threejs expects g=metal,b=roughness, rs has r=metal,g=roughness
for (let i = 0; i < compound.data.length; i += 4) {
compoundmapped.data[i + 1] = compound.data[i + 1];
compoundmapped.data[i + 2] = compound.data[i + 0];
compoundmapped.data[i + 3] = 255;
}
let tex = new THREE.DataTexture(compoundmapped.data, compoundmapped.width, compoundmapped.height, THREE.RGBAFormat);
tex.needsUpdate = true;
tex.wrapS = wraptype;
tex.wrapT = wraptype;
tex.encoding = THREE.sRGBEncoding;
tex.magFilter = THREE.LinearFilter;
mat.metalnessMap = tex;
mat.roughnessMap = tex;
mat.metalness = 1;
}
}
mat.vertexColors = material.vertexColorWhitening != 1 || hasVertexAlpha;
// if (!material.vertexColorWhitening && hasVertexAlpha) {
// mat.customProgramCacheKey = () => "vertexalphaonly";
// mat.onBeforeCompile = (shader, renderer) => {
// //this sucks but is nessecary since three doesn't support vertex alpha without vertex color
// //hard to rewrite the color attribute since we don't know if other meshes do use the colors
// shader.fragmentShader = shader.fragmentShader.replace("#include <color_fragment>", "diffuseColor.a *= vColor.a;");
// }
// }
mat.userData = material;
if (material.uvAnim) {
(mat.userData.gltfExtensions ??= {}).RA_materials_uvanim = {
uvAnim: [material.uvAnim.u, material.uvAnim.v]
};
}
return { mat, matmeta: material };
}, mat => 256 * 256 * 4 * 2);
if (this.engine.getBuildNr() < 759) {
let mat = defaultMaterial();
mat.textures.diffuse = matid;
//TODO other material props
return convertMaterialToThree(this, mat, hasVertexAlpha);
} else {
//TODO the material should have this data, not the mesh
let matcacheid = materialCacheKey(matid, hasVertexAlpha);
return this.engine.fetchCachedObject(this.threejsMaterialCache, matcacheid, async () => {
let material = this.engine.getMaterialData(matid);
return convertMaterialToThree(this, material, hasVertexAlpha);
}, mat => 256 * 256 * 4 * 2);
}
}
}

View File

@@ -169,7 +169,7 @@ export function parseRT5Model(modelfile: Buffer, source: CacheFileSource) {
for (let [matid, facecount] of matusecount) {
let finalvertcount = facecount * 3;
let colstride = (modeldata.colors ? modeldata.alpha ? 4 : 3 : 0);
let mesh = {
let mesh: WorkingSubmesh = {
pos: new BufferAttribute(new Float32Array(finalvertcount * 3), 3),
normals: new BufferAttribute(new Float32Array(finalvertcount * 3), 3),
color: new BufferAttribute(new Uint8Array(finalvertcount * colstride), colstride, true),

View File

@@ -23,57 +23,64 @@ export class ParsedTexture {
this.cachedImageDatas = [];
this.filesize = texture.byteLength;
//this should be first byte of uint32BE file size, which would always be 0 if filesize<16.7mb, but it appears that this byte repr can also change into a png file
let header = texture.readUint32BE(0);
if (header == 0x89504e47) {//"%png"
//raw png file, used by old textures in index 9 before 2015
this.type = "png";
this.imagefiles.push(texture);
this.mipmaps = 1;
} else {
//this should be first byte of uint32BE file size, which would always be 0 if filesize<16.7mb, but it appears that this byte repr can also change into a png file
let offset = 0;
let offset = 0;
//peek first bytes of first image file
let foundtype = false;
for (let extraoffset = 0; extraoffset <= 1; extraoffset++) {
let byte0 = texture.readUInt8(extraoffset + offset + 1 + 4 + 0);
let byte1 = texture.readUInt8(extraoffset + offset + 1 + 4 + 1);
if (byte0 == 0 && byte1 == 0) {
//has no header magic, but starts by writing the width in uint32 BE, any widths under 65k have 0x0000xxxx
this.type = "bmpmips";
} else if (byte0 == 0x44 && byte1 == 0x44) {
//0x44445320 "DDS "
this.type = "dds";
} else if (byte0 == 0x89 && byte1 == 0x50) {
//0x89504e47 ".PNG"
this.type = "png";
} else if (byte0 == 0xab && byte1 == 0x4b) {
//0xab4b5458 "«KTX"
this.type = "ktx";
} else {
continue;
//peek first bytes of first image file
let foundtype = false;
for (let extraoffset = 0; extraoffset <= 1; extraoffset++) {
let byte0 = texture.readUInt8(extraoffset + offset + 1 + 4 + 0);
let byte1 = texture.readUInt8(extraoffset + offset + 1 + 4 + 1);
if (byte0 == 0 && byte1 == 0) {
//has no header magic, but starts by writing the width in uint32 BE, any widths under 65k have 0x0000xxxx
this.type = "bmpmips";
} else if (byte0 == 0x44 && byte1 == 0x44) {
//0x44445320 "DDS "
this.type = "dds";
} else if (byte0 == 0x89 && byte1 == 0x50) {
//0x89504e47 ".PNG"
this.type = "png";
} else if (byte0 == 0xab && byte1 == 0x4b) {
//0xab4b5458 "«KTX"
this.type = "ktx";
} else {
continue;
}
foundtype = true;
if (extraoffset == 1) {
let numtexs = texture.readUint8(offset++);
//TODO figure this out further
}
break;
} if (!foundtype) {
throw new Error(`failed to detect texture`);
}
foundtype = true;
if (extraoffset == 1) {
let numtexs = texture.readUint8(offset++);
//TODO figure this out further
}
break;
} if (!foundtype) {
throw new Error(`failed to detect texture`);
}
this.mipmaps = texture.readUInt8(offset++);
this.mipmaps = texture.readUInt8(offset++);
if (this.type == "bmpmips") {
this.bmpWidth = texture.readUInt32BE(offset); offset += 4;
this.bmpHeight = texture.readUInt32BE(offset); offset += 4;
}
for (let i = 0; i < this.mipmaps; i++) {
let compressedsize: number;
if (this.type == "bmpmips") {
compressedsize = (this.bmpWidth >> i) * (this.bmpHeight >> i) * 4;
} else {
compressedsize = texture.readUInt32BE(offset);
offset += 4;
this.bmpWidth = texture.readUInt32BE(offset); offset += 4;
this.bmpHeight = texture.readUInt32BE(offset); offset += 4;
}
for (let i = 0; i < this.mipmaps; i++) {
let compressedsize: number;
if (this.type == "bmpmips") {
compressedsize = (this.bmpWidth >> i) * (this.bmpHeight >> i) * 4;
} else {
compressedsize = texture.readUInt32BE(offset);
offset += 4;
}
this.imagefiles.push(texture.slice(offset, offset + compressedsize))
offset += compressedsize;
this.cachedDrawables.push(null);
this.cachedImageDatas.push(null)
}
this.imagefiles.push(texture.slice(offset, offset + compressedsize))
offset += compressedsize;
this.cachedDrawables.push(null);
this.cachedImageDatas.push(null)
}
}
@@ -102,7 +109,7 @@ export class ParsedTexture {
let imgdata = loadBmp(this.imagefiles[subimg], width, height, padsize, this.stripAlpha);
return makeImageData(imgdata.data, imgdata.width, imgdata.height);
} else if (this.type == "png") {
return fileToImageData(this.imagefiles[subimg]);
return fileToImageData(this.imagefiles[subimg], "image/png", this.stripAlpha);
} else if (this.type == "dds") {
let imgdata = loadDds(this.imagefiles[subimg], padsize, this.stripAlpha);
return makeImageData(imgdata.data, imgdata.width, imgdata.height);

View File

@@ -210,7 +210,7 @@
.mv-modal-head {
font-size: 1.2em;
display: grid;
grid-template-columns: auto min-content;
grid-template-columns: auto min-content min-content min-content;
padding: 8px;
background: var(--secondary-bg-colour);
}

2
src/cache/index.ts vendored
View File

@@ -175,7 +175,7 @@ export function rootIndexBufferToObject(metaindex: Buffer, source: CacheFileSour
crc: q.crc,
version: q.version,
size: 0,
subindexcount: 1,
subindexcount: q.subindexcount,
subindices: [0],
uncompressed_crc: 0,
uncompressed_size: 0,

View File

@@ -35,6 +35,7 @@ export function validOpenrs2Caches() {
423,//osrs cache wrongly labeled as rs3
623,//seems to have different builds in it
693,//wrong timestamp?
621,619,618,620,617,//wrong timestamp/osrs?
840,//multiple builds
734, 736, 733,//don't have items index
20, 19, 17, 13, 10, 9, 8, 7, 6, 5,//don't have items index

View File

@@ -11,6 +11,8 @@ import { diffCaches } from "./scripts/cachediff";
import { quickChatLookup } from "./scripts/quickchatlookup";
import { scrapePlayerAvatars } from "./scripts/scrapeavatars";
import { validOpenrs2Caches } from "./cache/openrs2loader";
import { fileHistory } from "./scripts/filehistory";
import { openrs2Ids } from "./scripts/openrs2ids";
const testdecode = command({
name: "testdecode",
@@ -77,6 +79,25 @@ const extract = command({
}
});
const filehist = command({
name: "filehist",
args: {
id: option({ long: "id", short: "i", type: cmdts.string }),
save: option({ long: "save", short: "s", type: cmdts.string, defaultValue: () => "extract" }),
mode: option({ long: "mode", short: "m", type: cmdts.string, defaultValue: () => "bin" })
},
async handler(args) {
let outdir = new CLIScriptFS(args.save);
let output = new CLIScriptOutput();
if (!cacheFileDecodeModes[args.mode]) { throw new Error("unkown mode"); }
let id = args.id.split(".").map(q => +q);
if (id.length == 0 || id.some(q => isNaN(q))) { throw new Error("invalid id"); }
await output.run(fileHistory, outdir, args.mode as any, id, null);
}
});
const edit = command({
name: "edit",
args: {
@@ -160,37 +181,18 @@ const openrs2ids = command({
name: "openrs2ids",
args: {
date: option({ long: "year", short: "d", defaultValue: () => "" }),
near: option({ long: "near", short: "n", defaultValue: () => "" })
near: option({ long: "near", short: "n", defaultValue: () => "" }),
full: flag({ long: "full", short: "f" })
},
async handler(args) {
let allids = await validOpenrs2Caches();
if (args.date) {
let m = args.date.match(/20\d\d/);
if (!m) { throw new Error("4 digit year expected"); }
let year = +m[0];
let enddate = new Date((year + 1) + "");
let startdate = new Date(year + "");
allids = allids.filter(q => q.timestamp && new Date(q.timestamp) >= startdate && new Date(q.timestamp) <= enddate);
}
if (args.near) {
let index = allids.findIndex(q => q.id == +args.near);
if (index == -1) { throw new Error("cache id not found"); }
let amount = 10;
let beforeamount = Math.min(index, amount);
allids = allids.slice(index - beforeamount, index + 1 + amount);
}
for (let cache of allids) {
let line = `id ${cache.id.toString().padStart(4)}, build ${cache.builds[0]?.major ?? "???"}`;
line += ` - ${cache.timestamp ? new Date(cache.timestamp).toDateString() : "unknown date"}`;
if (args.near && +args.near == cache.id) { line += " <--"; }
console.log(line);
}
let output = new CLIScriptOutput();
await output.run(openrs2Ids, args.date, args.near, args.full);
}
})
let subcommands = cmdts.subcommands({
name: "cache tools cli",
cmds: { extract, indexoverview, testdecode, diff, quickchat, scrapeavatars, edit, historicdecode, openrs2ids }
cmds: { extract, indexoverview, testdecode, diff, quickchat, scrapeavatars, edit, historicdecode, openrs2ids, filehist }
});
cmdts.run(subcommands, cliArguments());

View File

@@ -21,10 +21,21 @@ export const cacheMajors = {
models: 47,
frames: 48,
texturesOldPng: 9,
texturesOldCompoundPng: 37,
textures2015Png: 43,
textures2015CompoundPng: 44,
textures2015Dds: 45,
textures2015CompoundPngMips: 46,
textures2015CompoundDds: 50,
textures2015PngMips: 51,
texturesDds: 52,
texturesPng: 53,
texturesBmp: 54,
texturesKtx: 55,
skeletalAnims: 56,
achievements: 57,

View File

@@ -35,10 +35,47 @@ export async function pixelsToImageFile(imgdata: ImageData, format: "png" | "web
}
}
export async function fileToImageData(file: Uint8Array) {
if (typeof HTMLCanvasElement != "undefined") {
let warnedstripalpha = false;
declare global {
const ImageDecoder: any;
interface ImageDecoder { }
}
export async function fileToImageData(file: Uint8Array, mimetype: "image/png" | "image/jpg", stripAlpha: boolean) {
if (typeof ImageDecoder != "undefined") {
let decoder = new ImageDecoder({ data: file, type: mimetype, premultiplyAlpha: (stripAlpha ? "none" : "default"), colorSpaceConversion: "none" });
let frame = await decoder.decode();
let pixels = new Uint8Array(frame.image.allocationSize());
frame.image.copyTo(pixels);
let pixelcount = frame.image.visibleRect.width * frame.image.visibleRect.height;
if (frame.image.format == "BGRX" || frame.image.format == "RGBX") {
stripAlpha = true;
}
if (frame.image.format == "BGRA" || frame.image.format == "BGRX") {
for (let plane = 0; plane < 4; plane++) {
for (let i = 0; i < pixelcount; i++) {
pixels[i * 4 + 0] = pixels[i * 4 + 2];
pixels[i * 4 + 1] = pixels[i * 4 + 1];
pixels[i * 4 + 2] = pixels[i * 4 + 0];
pixels[i * 4 + 3] = (stripAlpha ? 255 : pixels[i * 4 + 0]);
}
}
} else if (frame.image.format == "RGBA" || frame.image.format == "RGBX") {
if (stripAlpha) {
for (let i = 0; i < pixelcount; i++) {
pixels[i * 4 + 3] = 255;
}
}
} else {
throw new Error("unexpected image format");
}
return makeImageData(pixels, frame.image.visibleRect.width, frame.image.visibleRect.height);
} else if (typeof HTMLCanvasElement != "undefined") {
if (stripAlpha && !warnedstripalpha) {
console.warn("can not strip alpha in browser context that does not support ImageDecoder");
}
let img = new Image();
let blob = new Blob([file], { type: "image/png" });//mime doesn't actually matter as long as it's img/*
let blob = new Blob([file], { type: mimetype });
let url = URL.createObjectURL(blob);
img.src = url;
await img.decode();
@@ -52,6 +89,7 @@ export async function fileToImageData(file: Uint8Array) {
} else {
const sharp = require("sharp") as typeof import("sharp");
let img = sharp(file);
if (stripAlpha) { img.removeAlpha(); }
let decoded = await img.raw().toBuffer({ resolveWithObject: true });
let pixbuf = new Uint8ClampedArray(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength);
return makeImageData(pixbuf, decoded.info.width, decoded.info.height);

View File

@@ -6,8 +6,11 @@
//always 0000 after 2015
["opt0","ubyte"],
["opt0data",["opt",["opt0",16],["tuple","ubyte","ubyte","ubyte"]]],
["arr",["nullarray","ubyte","ushort"]],
["opt0data",["opt",["opt0",16],["tuple","ubyte","ushort"]]],
["arr",["nullarray","ubyte",["struct",
["op",["ref","$opcode"]],
["value","ushort"]
]]],
["textureflags","ubyte"],//always 1,3,9,16
["diffuse",["opt",["textureflags",17,"bitor"],["match","buildnr",{">=887":"uint",">=0":0}]]],

View File

@@ -1,13 +1,21 @@
["struct",
["$minorindex","-1"],
["cachemajors",["array","ubyte",["struct",
["minor",["accum","$minorindex","1"]],
["crc","uint"],
["version","uint"],
["subindexcount",["match","buildnr",{">=816":"uint","other":0}]],
["integer_10",["match","buildnr",{">=816":"uint","other":0}]],
["maybe_checksum1",["buffer",64,"hex"]]
]]],
["cachemajors",["match","buildnr",{
">=605":["array","ubyte",["struct",
["minor",["accum","$minorindex","1"]],
["crc","uint"],
["version","uint"],
["subindexcount",["match","buildnr",{">=816":"uint","other":0}]],
["integer_10",["match","buildnr",{">=816":"uint","other":0}]],
["maybe_checksum1",["buffer",64,"hex"]]
]],
"other":["nullarray",["bytesleft"],["struct",
["minor",["accum","$minorindex","1"]],
["crc","uint"],
["version",["match","buildnr",{">=457":"uint","other":0}]],
["subindexcount",0]
]]
}]],
//512bytes of data but variable length encoding that makes is 511-513
["maybe_proper_checksum",["buffer",["bytesleft"],"hex"]]
]

View File

@@ -124,7 +124,7 @@ const materialDeps: DepCollector = async (cache, addDep, addHash) => {
for (let file of arch) {
addHash("material", file.fileid, crc32(file.buffer), index.version);
let mat = convertMaterial(file.buffer, cache);
let mat = convertMaterial(file.buffer, file.fileid, cache);
for (let tex of Object.values(mat.textures)) {
if (typeof tex == "number") {
addDep("texture", tex, "material", file.fileid)

View File

@@ -315,16 +315,15 @@ const decodeSound = (major: number): DecodeModeFactory => () => {
}
}
const decodeSprite: DecodeModeFactory = () => {
const decodeSprite = (major: number): DecodeModeFactory => () => {
return {
ext: "png",
major: cacheMajors.sprites,
major: major,
logicalDimensions: 1,
multiIndexArchives: false,
fileToLogical(major, minor, subfile) { return [minor]; },
logicalToFile(id) { return { major: cacheMajors.sprites, minor: id[0], subid: 0 }; },
logicalToFile(id) { return { major, minor: id[0], subid: 0 }; },
async logicalRangeToFiles(source, start, end) {
let major = cacheMajors.sprites;
return filerange(source, { major, minor: start[0], subid: 0 }, { major, minor: end[0], subid: 0 });
},
prepareDump() { },
@@ -484,9 +483,16 @@ const npcmodels: DecodeModeFactory = function (flags) {
export const cacheFileDecodeModes = constrainedMap<DecodeModeFactory>()({
bin: decodeBinary,
sprites: decodeSprite,
sprites: decodeSprite(cacheMajors.sprites),
spritehash: decodeSpriteHash,
modelhash: decodeMeshHash,
textures_oldpng: decodeTexture(cacheMajors.texturesOldPng),
textures_2015png: decodeTexture(cacheMajors.textures2015Png),
textures_2015dds: decodeTexture(cacheMajors.textures2015Dds),
textures_2015pngmips: decodeTexture(cacheMajors.textures2015PngMips),
textures_2015compoundpng: decodeTexture(cacheMajors.textures2015CompoundPng),
textures_2015compounddds: decodeTexture(cacheMajors.textures2015CompoundDds),
textures_2015compoundpngmips: decodeTexture(cacheMajors.textures2015CompoundPngMips),
textures_dds: decodeTexture(cacheMajors.texturesDds),
textures_png: decodeTexture(cacheMajors.texturesPng),
textures_bmp: decodeTexture(cacheMajors.texturesBmp),

View File

@@ -0,0 +1,86 @@
import { CacheFileSource, CacheIndexFile } from "../cache";
import { Openrs2CacheSource, validOpenrs2Caches } from "../cache/openrs2loader";
import { cacheMajors } from "../constants";
import { ScriptFS, ScriptOutput } from "../viewer/scriptsui";
import { cacheFileDecodeModes } from "./extractfiles";
type HistoricVersion = {
cacheids: string[],
buildnr: number[],
hash: number | null,
file: Buffer | null,
decoded: string | Buffer | null
decodedname: string
}
export async function fileHistory(output: ScriptOutput, outdir: ScriptFS, mode: keyof typeof cacheFileDecodeModes, id: number[], basecache: CacheFileSource | null) {
let histsources = await validOpenrs2Caches();
let decoder = cacheFileDecodeModes[mode]({});
let allsources = function* () {
if (basecache) {
yield basecache;
}
for (let id of histsources) {
yield new Openrs2CacheSource(id);
}
}
let lastversion: HistoricVersion | null = null;
let history: HistoricVersion[] = [];
for (let source of allsources()) {
try {
let sourcename = source.getCacheMeta().name.replace(/:/g, "-");
let changed = false;
let fileid = decoder.logicalToFile(id);
let indexfile = await source.getCacheIndex(fileid.major);
let filemeta = indexfile.at(fileid.minor);
let newfile: Buffer | null = null;
let decoded: string | Buffer | null = null;
if (filemeta) {
let newarchive = await source.getFileArchive(filemeta);
newfile = newarchive[fileid.subid]?.buffer;
if (!newfile) { throw new Error("invalid subid"); }
if (!lastversion?.file || Buffer.compare(newfile, lastversion.file) != 0) {
if (lastversion && filemeta.crc == lastversion.hash) {
console.log("file change detected without crc change");
}
changed = true;
decoded = await decoder.read(newfile, id, source);
}
} else if (lastversion && lastversion.file) {
changed = true;
}
if (changed) {
let majorname = Object.entries(cacheMajors).find(([k, v]) => v == fileid.major)?.[0] ?? `unkown-${fileid.major}`;
let decodedname = `${majorname}-${fileid.minor}-${fileid.subid}-${sourcename}.${decoded ? decoder.ext : "txt"}`;
lastversion = {
cacheids: [],
buildnr: [],
hash: filemeta?.crc ?? 0,
decoded,
decodedname,
file: newfile,
};
history.push(lastversion);
await outdir.writeFile(decodedname, decoded ?? "empty");
}
lastversion!.buildnr.push(source.getBuildNr());
lastversion!.cacheids.push(source.getCacheMeta().name);
} catch (e) {
console.log(`error while decoding diffing file ${id} in "${source.getCacheMeta().name}, ${source.getCacheMeta().descr}"`);
//TODO use different stopping condition
return history;
} finally {
if (source != basecache) {
source.close();
}
}
}
return history;
}

62
src/scripts/openrs2ids.ts Normal file
View File

@@ -0,0 +1,62 @@
import { Openrs2CacheSource, validOpenrs2Caches } from "../cache/openrs2loader";
import { cacheMajors } from "../constants";
import { ScriptOutput } from "../viewer/scriptsui";
export async function openrs2Ids(output: ScriptOutput, date: string, near: string, logcontents: boolean) {
let allids = await validOpenrs2Caches();
if (date) {
let m = date.match(/20\d\d/);
if (!m) { throw new Error("4 digit year expected"); }
let year = +m[0];
let enddate = new Date((year + 1) + "");
let startdate = new Date(year + "");
allids = allids.filter(q => q.timestamp && new Date(q.timestamp) >= startdate && new Date(q.timestamp) <= enddate);
}
if (near) {
let index = allids.findIndex(q => q.id == +near);
if (index == -1) { throw new Error("cache id not found"); }
let amount = 10;
let beforeamount = Math.min(index, amount);
allids = allids.slice(index - beforeamount, index + 1 + amount);
}
let linenr = 0;
for (let cache of allids) {
let line = `id ${cache.id.toString().padStart(4)}, build ${cache.builds[0]?.major ?? "???"}`;
line += ` - ${(cache.timestamp ? new Date(cache.timestamp).toDateString() : "unknown date").padEnd(12)}`;
if (near) { line += (+near == cache.id ? " <--" : " "); }
if (logcontents) {
if (linenr % 10 == 0) {
let extraline = "-".repeat(2 + 1 + 4 + 9 + 3 + 3 + 12 + 4);
for (let i = 0; i < 60; i++) {
extraline += `+-${` ${i} `.padStart(6, "-")}--`;
}
output.log(extraline);
}
let src = new Openrs2CacheSource(cache);
try {
if (cache.builds[0].major >= 410) {
let index = await src.getCacheIndex(cacheMajors.index);
for (let i = 0; i < index.length; i++) {
let config = index[i];
if (!config) {
line += " ".repeat(10);
} else {
let subcount = 0;
if (config.crc != 0 && config.subindexcount == 0) {
let subindex = await src.getCacheIndex(config.minor);
subcount = subindex.reduce((a, v) => a + (v ? 1 : 0), 0);
} else {
subcount = config.subindexcount;
}
line += ` ${subcount.toString().padStart(9)}`;
}
}
}
} finally {
src.close();
}
}
output.log(line);
linenr++;
}
}

View File

@@ -50,9 +50,7 @@ class App extends React.Component<{ ctx: UIContext }, { openedFile: UIScriptFile
let engine = await EngineCache.create(cache);
console.log("engine loaded", cache.getBuildNr());
let scene = new ThreejsSceneCache(engine);
if (source.type == "sqliteblobs" || source.type == "sqlitehandle" || source.type == "sqlitenodejs") {
scene.textureType = await detectTextureMode(cache);
}
scene.textureType = await detectTextureMode(cache);
this.props.ctx.setSceneCache(scene);
globalThis.sceneCache = scene;

View File

@@ -511,7 +511,7 @@ function SimpleTextViewer(p: { file: string }) {
);
}
export function FileViewer(p: { file: UIScriptFile, onSelectFile: (f: UIScriptFile | null) => void }) {
export function FileDisplay(p: { file: UIScriptFile }) {
let el: React.ReactNode = null;
let filedata = p.file.data;
let cnvref = React.useRef<HTMLCanvasElement | null>(null);
@@ -520,6 +520,8 @@ export function FileViewer(p: { file: UIScriptFile, onSelectFile: (f: UIScriptFi
if (ext == "hexerr.json") {
el = <FileDecodeErrorViewer file={filedata} />;
} else {
//TODO make this not depend on wether file is Buffer or string
//string types so far: txt, json, batch.json
el = <SimpleTextViewer file={filedata} />;
}
} else {
@@ -549,15 +551,19 @@ export function FileViewer(p: { file: UIScriptFile, onSelectFile: (f: UIScriptFi
el = <TrivialHexViewer data={filedata} />
}
}
return el;
}
export function FileViewer(p: { file: UIScriptFile, onSelectFile: (f: UIScriptFile | null) => void }) {
return (
<div style={{ display: "grid", gridTemplateRows: "auto 1fr" }}>
<div className="mv-modal-head">
<span>{p.file.name}</span>
<span style={{ float: "right" }} onClick={e => p.onSelectFile(null)}>x</span>
<span style={{ float: "right", marginLeft: "10px" }} onClick={e => downloadBlob(p.file.name, new Blob([p.file.data]))}>download</span>
<span style={{ float: "right", marginLeft: "10px" }} onClick={e => p.onSelectFile(null)}>x</span>
</div>
<div style={{ overflow: "auto", flex: "1" }}>
{el}
<FileDisplay file={p.file} />
</div>
</div>
);

View File

@@ -11,7 +11,7 @@ import { appearanceUrl, avatarStringToBytes, EquipCustomization, EquipSlot, slot
import { ThreeJsRendererEvents, highlightModelGroup, ThreeJsSceneElement, ThreeJsSceneElementSource, exportThreeJsGltf, exportThreeJsStl, RenderCameraMode } from "./threejsrender";
import { cacheFileJsonModes, extractCacheFiles, cacheFileDecodeModes } from "../scripts/extractfiles";
import { defaultTestDecodeOpts, testDecode } from "../scripts/testdecode";
import { UIScriptOutput, OutputUI, useForceUpdate, VR360View } from "./scriptsui";
import { UIScriptOutput, OutputUI, useForceUpdate, VR360View, UIScriptFiles, UIScriptFS } from "./scriptsui";
import { CacheSelector, downloadBlob, openSavedCache, SavedCacheSource, UIContext, UIContextReady } from "./maincomponents";
import { tiledimensions } from "../3d/mapsquare";
import { runMapRender } from "../map";
@@ -27,6 +27,8 @@ import { mapsquare_overlays } from '../../generated/mapsquare_overlays';
import { mapsquare_underlays } from '../../generated/mapsquare_underlays';
import { FileParser } from '../opdecoder';
import { assertSchema, customModelDefSchema, parseJsonOrDefault, scenarioStateSchema } from '../jsonschemas';
import { fileHistory } from '../scripts/filehistory';
import { MaterialData } from '../3d/jmat';
type LookupMode = "model" | "item" | "npc" | "object" | "material" | "map" | "avatar" | "spotanim" | "scenario" | "scripts";
@@ -1245,8 +1247,8 @@ async function materialIshToModel(sceneCache: ThreejsSceneCache, reqid: { mode:
let json: any = null;
let texs: Record<string, { texid: number, filesize: number, img0: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | ImageBitmap }> = {};
let models: SimpleModelDef = [];
let addtex = async (name: string, texid: number, stripalpha: boolean) => {
let tex = await sceneCache.getTextureFile(texid, stripalpha);
let addtex = async (type: keyof MaterialData["textures"], name: string, texid: number, stripalpha: boolean) => {
let tex = await sceneCache.getTextureFile(type, texid, stripalpha);
let drawable = await tex.toWebgl();
texs[name] = { texid, filesize: tex.filesize, img0: drawable };
@@ -1265,8 +1267,8 @@ async function materialIshToModel(sceneCache: ThreejsSceneCache, reqid: { mode:
} else if (reqid.mode == "mat") {
matid = reqid.id;
} else if (reqid.mode == "texture") {
await addtex("original", reqid.id, false);
await addtex("opaque", reqid.id, true);
await addtex("diffuse", "original", reqid.id, false);
await addtex("diffuse", "opaque", reqid.id, true);
} else {
throw new Error("invalid materialish mode");
}
@@ -1281,7 +1283,7 @@ async function materialIshToModel(sceneCache: ThreejsSceneCache, reqid: { mode:
let mat = sceneCache.engine.getMaterialData(matid);
for (let tex in mat.textures) {
if (mat.textures[tex] != 0) {
await addtex(tex, mat.textures[tex], mat.stripDiffuseAlpha && tex == "diffuse");
await addtex("diffuse", tex, mat.textures[tex], mat.stripDiffuseAlpha && tex == "diffuse");
}
}
json = mat;
@@ -1330,32 +1332,6 @@ function SceneMaterialIsh(p: LookupModeProps) {
)
}
function SceneMaterial(p: LookupModeProps) {
let [data, model, id, setId] = useAsyncModelData(p.ctx, materialToModel);
let initid = id ?? (typeof p.initialId == "number" ? p.initialId : 0);
return (
<React.Fragment>
<IdInput onChange={setId} initialid={initid} />
{id == null && (
<React.Fragment>
<p>Enter a material id.</p>
<p>Materials define how a piece of geometry looks, besides the color texture they also define how the model interacts with light to create highlights and reflections.</p>
</React.Fragment>
)}
<div className="mv-sidebar-scroll">
{data && Object.entries(data.info.texs).map(([name, img]) => (
<div key={name}>
<div>{name} - {img.texid} - {img.filesize / 1024 | 0}kb - {img.img0.width}x{img.img0.height}</div>
<ImageDataView img={img.img0} />
</div>
))}
<JsonDisplay obj={data?.info.obj} />
</div>
</React.Fragment>
)
}
function SceneRawModel(p: LookupModeProps) {
let initid = (typeof p.initialId == "number" ? p.initialId : 0);
let [data, model, id, setId] = useAsyncModelData(p.ctx, modelToModel);
@@ -1515,9 +1491,18 @@ function SceneItem(p: LookupModeProps) {
let [data, model, id, setId] = useAsyncModelData(p.ctx, itemToModel);
let initid = id ?? (typeof p.initialId == "number" ? p.initialId : 0);
let [enablecam, setenablecam] = React.useState(false);
// let [histfs, sethistfs] = React.useState<UIScriptFS | null>(null);
let centery = (model?.loaded ? (model.loaded.modeldata.maxy + model.loaded.modeldata.miny) / 2 : 0);
// let gethistory = async () => {
// if (id == null || !p.ctx) { return; }
// let output = new UIScriptOutput();
// let fs = new UIScriptFS(output);
// sethistfs(fs);
// await output.run(fileHistory, fs, "items", [id], p.ctx.source);
// }
return (
<React.Fragment>
{p.ctx && <IdInputSearch cache={p.ctx.sceneCache.engine} mode="items" onChange={setId} initialid={initid} />}
@@ -1529,6 +1514,8 @@ function SceneItem(p: LookupModeProps) {
{enablecam && p.ctx && <ItemCameraMode ctx={p.ctx} meta={data?.info} centery={centery} />}
<JsonDisplay obj={data?.info} />
</div>
{/* <input type="button" className="sub-btn" value="history" onClick={gethistory} />
{histfs && p.ctx && <UIScriptFiles fs={histfs} ctx={p.ctx} />} */}
</React.Fragment>
)
}
@@ -1836,7 +1823,7 @@ function ExtractFilesScript(p: UiScriptProps) {
let [initmode, initbatched, initkeepbuffs, initfilestext] = p.initialArgs.split(":");
let [filestext, setFilestext] = React.useState(initfilestext ?? "");
let [mode, setMode] = React.useState<keyof typeof cacheFileDecodeModes>(initmode as any || "items");
let [batched, setbatched] = React.useState(initbatched != "false");
let [batched, setbatched] = React.useState(initbatched == "true");
let [keepbuffers, setkepbuffers] = React.useState(initkeepbuffs == "true");
let run = () => {
@@ -1976,7 +1963,7 @@ function TestFilesScript(p: UiScriptProps) {
let [initmode, initrange, initdumpall, initordersize] = p.initialArgs.split(":");
let [mode, setMode] = React.useState(initmode || "");
let [range, setRange] = React.useState(initrange || "");
let [dumpall, setDumpall] = React.useState(initdumpall == "true");
let [dumpall, setDumpall] = React.useState(initdumpall != "false");
let [ordersize, setOrdersize] = React.useState(initordersize == "true");
let [customparser, setCustomparser] = React.useState("");