From fbd2feadbb87c94e0aeff25b2409f1bf764cc1e2 Mon Sep 17 00:00:00 2001 From: Skillbert Date: Sat, 25 Feb 2023 23:16:51 +0100 Subject: [PATCH] support ~2016 models --- generated/materials.d.ts | 6 +- generated/rootcacheindex.d.ts | 21 ++- src/3d/jmat.ts | 8 +- src/3d/mapsquare.ts | 2 +- src/3d/modelnodes.ts | 7 +- src/3d/modeltothree.ts | 290 ++++++++++++++++++------------- src/3d/rt5model.ts | 2 +- src/3d/textures.ts | 101 ++++++----- src/assets/index.css | 2 +- src/cache/index.ts | 2 +- src/cache/openrs2loader.ts | 1 + src/cli.ts | 50 +++--- src/constants.ts | 11 ++ src/imgutils.ts | 44 ++++- src/opcodes/materials.jsonc | 7 +- src/opcodes/rootcacheindex.jsonc | 24 ++- src/scripts/dependencies.ts | 2 +- src/scripts/extractfiles.ts | 16 +- src/scripts/filehistory.ts | 86 +++++++++ src/scripts/openrs2ids.ts | 62 +++++++ src/viewer/index.tsx | 4 +- src/viewer/maincomponents.tsx | 12 +- src/viewer/scenenodes.tsx | 55 +++--- 23 files changed, 539 insertions(+), 276 deletions(-) create mode 100644 src/scripts/filehistory.ts create mode 100644 src/scripts/openrs2ids.ts diff --git a/generated/materials.d.ts b/generated/materials.d.ts index 3774515..0aa67b5 100644 --- a/generated/materials.d.ts +++ b/generated/materials.d.ts @@ -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, diff --git a/generated/rootcacheindex.d.ts b/generated/rootcacheindex.d.ts index 0cfc62c..8137b2f 100644 --- a/generated/rootcacheindex.d.ts +++ b/generated/rootcacheindex.d.ts @@ -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, }; diff --git a/src/3d/jmat.ts b/src/3d/jmat.ts index 4b2567e..03e693c 100644 --- a/src/3d/jmat.ts +++ b/src/3d/jmat.ts @@ -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; } diff --git a/src/3d/mapsquare.ts b/src/3d/mapsquare.ts index 42b060f..204db8f 100644 --- a/src/3d/mapsquare.ts +++ b/src/3d/mapsquare.ts @@ -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 }); diff --git a/src/3d/modelnodes.ts b/src/3d/modelnodes.ts index 956fb7e..3278b7c 100644 --- a/src/3d/modelnodes.ts +++ b/src/3d/modelnodes.ts @@ -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 = {}; - 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 { diff --git a/src/3d/modeltothree.ts b/src/3d/modeltothree.ts index 8846a7a..9c53320 100644 --- a/src/3d/modeltothree.ts +++ b/src/3d/modeltothree.ts @@ -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>(); @@ -206,28 +304,55 @@ export class ThreejsSceneCache { private threejsTextureCache = new Map>(); private threejsMaterialCache = new Map>(); 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> = { + 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 ", "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); + } } } diff --git a/src/3d/rt5model.ts b/src/3d/rt5model.ts index f11471b..5954470 100644 --- a/src/3d/rt5model.ts +++ b/src/3d/rt5model.ts @@ -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), diff --git a/src/3d/textures.ts b/src/3d/textures.ts index 0bb16b0..9e8b011 100644 --- a/src/3d/textures.ts +++ b/src/3d/textures.ts @@ -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); diff --git a/src/assets/index.css b/src/assets/index.css index ab5837d..c13bf06 100644 --- a/src/assets/index.css +++ b/src/assets/index.css @@ -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); } diff --git a/src/cache/index.ts b/src/cache/index.ts index 5369ba9..fca439c 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -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, diff --git a/src/cache/openrs2loader.ts b/src/cache/openrs2loader.ts index 190b57f..5f4bedf 100644 --- a/src/cache/openrs2loader.ts +++ b/src/cache/openrs2loader.ts @@ -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 diff --git a/src/cli.ts b/src/cli.ts index b869277..cad748f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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()); \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 731a262..e0dd916 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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, diff --git a/src/imgutils.ts b/src/imgutils.ts index bf54336..1b659fd 100644 --- a/src/imgutils.ts +++ b/src/imgutils.ts @@ -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); diff --git a/src/opcodes/materials.jsonc b/src/opcodes/materials.jsonc index 55725a5..7bbae0c 100644 --- a/src/opcodes/materials.jsonc +++ b/src/opcodes/materials.jsonc @@ -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}]]], diff --git a/src/opcodes/rootcacheindex.jsonc b/src/opcodes/rootcacheindex.jsonc index be394e1..f7355f9 100644 --- a/src/opcodes/rootcacheindex.jsonc +++ b/src/opcodes/rootcacheindex.jsonc @@ -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"]] ] \ No newline at end of file diff --git a/src/scripts/dependencies.ts b/src/scripts/dependencies.ts index 6536c65..69ade5a 100644 --- a/src/scripts/dependencies.ts +++ b/src/scripts/dependencies.ts @@ -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) diff --git a/src/scripts/extractfiles.ts b/src/scripts/extractfiles.ts index 37e992b..7041e1d 100644 --- a/src/scripts/extractfiles.ts +++ b/src/scripts/extractfiles.ts @@ -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()({ 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), diff --git a/src/scripts/filehistory.ts b/src/scripts/filehistory.ts new file mode 100644 index 0000000..f8aacb4 --- /dev/null +++ b/src/scripts/filehistory.ts @@ -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; +} diff --git a/src/scripts/openrs2ids.ts b/src/scripts/openrs2ids.ts new file mode 100644 index 0000000..faf3ca9 --- /dev/null +++ b/src/scripts/openrs2ids.ts @@ -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++; + } +} \ No newline at end of file diff --git a/src/viewer/index.tsx b/src/viewer/index.tsx index c8c2f04..00c1bbf 100644 --- a/src/viewer/index.tsx +++ b/src/viewer/index.tsx @@ -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; diff --git a/src/viewer/maincomponents.tsx b/src/viewer/maincomponents.tsx index f00d1c3..324f07d 100644 --- a/src/viewer/maincomponents.tsx +++ b/src/viewer/maincomponents.tsx @@ -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(null); @@ -520,6 +520,8 @@ export function FileViewer(p: { file: UIScriptFile, onSelectFile: (f: UIScriptFi if (ext == "hexerr.json") { el = ; } else { + //TODO make this not depend on wether file is Buffer or string + //string types so far: txt, json, batch.json el = ; } } else { @@ -549,15 +551,19 @@ export function FileViewer(p: { file: UIScriptFile, onSelectFile: (f: UIScriptFi el = } } + return el; +} +export function FileViewer(p: { file: UIScriptFile, onSelectFile: (f: UIScriptFile | null) => void }) { return (
{p.file.name} - p.onSelectFile(null)}>x + downloadBlob(p.file.name, new Blob([p.file.data]))}>download + p.onSelectFile(null)}>x
- {el} +
); diff --git a/src/viewer/scenenodes.tsx b/src/viewer/scenenodes.tsx index 1b27a6a..13b42ef 100644 --- a/src/viewer/scenenodes.tsx +++ b/src/viewer/scenenodes.tsx @@ -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 = {}; 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 ( - - - {id == null && ( - -

Enter a material id.

-

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.

-
- )} -
- {data && Object.entries(data.info.texs).map(([name, img]) => ( -
-
{name} - {img.texid} - {img.filesize / 1024 | 0}kb - {img.img0.width}x{img.img0.height}
- -
- ))} - -
-
- ) -} - 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(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 ( {p.ctx && } @@ -1529,6 +1514,8 @@ function SceneItem(p: LookupModeProps) { {enablecam && p.ctx && } + {/* + {histfs && p.ctx && } */} ) } @@ -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(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("");