mirror of
https://github.com/skillbert/rsmv.git
synced 2026-05-19 03:54:33 -04:00
support ~2016 models
This commit is contained in:
6
generated/materials.d.ts
vendored
6
generated/materials.d.ts
vendored
@@ -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,
|
||||
|
||||
21
generated/rootcacheindex.d.ts
vendored
21
generated/rootcacheindex.d.ts
vendored
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
2
src/cache/index.ts
vendored
@@ -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,
|
||||
|
||||
1
src/cache/openrs2loader.ts
vendored
1
src/cache/openrs2loader.ts
vendored
@@ -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
|
||||
|
||||
50
src/cli.ts
50
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());
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}]]],
|
||||
|
||||
@@ -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"]]
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
86
src/scripts/filehistory.ts
Normal file
86
src/scripts/filehistory.ts
Normal 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
62
src/scripts/openrs2ids.ts
Normal 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++;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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("");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user