diff --git a/generated/items.d.ts b/generated/items.d.ts index eea2d61..82338fa 100644 --- a/generated/items.d.ts +++ b/generated/items.d.ts @@ -169,6 +169,11 @@ export type items = { neverStackable?: true | null unknown_A7?: true | null unknown_A8?: true | null + unknown_B2?: true | null + big_value?: [ + number, + number, + ] | null extra?: { prop: number, intvalue: number | null, diff --git a/generated/maplabels.d.ts b/generated/maplabels.d.ts new file mode 100644 index 0000000..4948462 --- /dev/null +++ b/generated/maplabels.d.ts @@ -0,0 +1,6 @@ +// GENERATED DO NOT EDIT +// This source data is located at '..\src\opcodes\maplabels.jsonc' +// run `npm run filetypes` to rebuild + +export type maplabels = any; +// TypeError: parserFunctions[chunkdef[0]] is not a function \ No newline at end of file diff --git a/generated/mapsquare_underlays.d.ts b/generated/mapsquare_underlays.d.ts index 9b3c006..8bd92cd 100644 --- a/generated/mapsquare_underlays.d.ts +++ b/generated/mapsquare_underlays.d.ts @@ -1,5 +1,5 @@ // GENERATED DO NOT EDIT -// This source data is located at '..\src\opcodes\mapsquare_underlays.json' +// This source data is located at '..\src\opcodes\mapsquare_underlays.jsonc' // run `npm run filetypes` to rebuild export type mapsquare_underlays = { diff --git a/src/3d/mapsquare.ts b/src/3d/mapsquare.ts index b7c8bec..dd27e54 100644 --- a/src/3d/mapsquare.ts +++ b/src/3d/mapsquare.ts @@ -130,7 +130,7 @@ export type ModelExtrasLocation = { worldz: number, rotation: number, mirror: boolean, - type: number, + isGroundDecor: boolean, level: number, locationInstance: WorldLocation } @@ -752,7 +752,7 @@ export class TileGrid implements TileGridSource { } } -export type ParsemapOpts = { padfloor?: boolean, invisibleLayers?: boolean, collision?: boolean, map2d?: boolean, minimap?: boolean, skybox?: boolean, mask?: MapRect[] }; +export type ParsemapOpts = { padfloor?: boolean, invisibleLayers?: boolean, collision?: boolean, map2d?: boolean, skybox?: boolean, mask?: MapRect[] }; export type ChunkModelData = { floors: FloorMeshData[], models: MapsquareLocation[], overlays: PlacedModel[], chunk: ChunkData, grid: TileGrid }; export async function getMapsquareData(engine: EngineCache, chunkx: number, chunkz: number) { @@ -963,19 +963,7 @@ async function mapsquareFloors(scene: ThreejsSceneCache, grid: TileGrid, chunk: } for (let level = 0; level < squareLevels; level++) { - let base = mapsquareMesh(grid, chunk, level, atlas, false, true, false); - floors.push(base); - if (opts?.minimap) { - let mini = { - ...base, - extra: { - ...base.extra, - modelgroup: "minimap" + level - }, - minimap: true - }; - floors.push(mini); - } + floors.push(mapsquareMesh(grid, chunk, level, atlas, false, true, false)); if (opts?.map2d) { floors.push(mapsquareMesh(grid, chunk, level, atlas, false, false, true)); } @@ -1391,7 +1379,7 @@ function mapsquareObjectModels(cache: CacheFileSource, locs: WorldLocation[]) { worldz: inst.z, rotation: inst.rotation, mirror: !!objectmeta.mirror, - type: inst.type, + isGroundDecor: inst.type == 22 && !objectmeta.unknown_49, level: inst.visualLevel, locationInstance: inst }; @@ -2123,7 +2111,6 @@ function mapsquareMesh(grid: TileGrid, chunk: ChunkData, level: number, atlas: S showhidden, tileinfos, worldmap, - minimap: false, vertexstride: vertexstride, //TODO i'm not actually using these, can get rid of it again @@ -2171,7 +2158,7 @@ function floorToThree(scene: ThreejsSceneCache, floor: FloorMeshData) { mat.vertexColors = true; if (!floor.showhidden) { if (!floor.worldmap) { - augmentThreeJsFloorMaterial(mat, floor.minimap); + augmentThreeJsFloorMaterial(mat, false); let img = floor.atlas.convert(); //no clue why this doesn't work diff --git a/src/3d/modelnodes.ts b/src/3d/modelnodes.ts index ed5f041..d87610b 100644 --- a/src/3d/modelnodes.ts +++ b/src/3d/modelnodes.ts @@ -1,7 +1,7 @@ import { parse } from "../opdecoder"; import { appearanceUrl, avatarStringToBytes, avatarToModel } from "./avatar"; import * as THREE from "three"; -import { ThreejsSceneCache, mergeModelDatas, ob3ModelToThree, mergeNaiveBoneids, constModelsIds } from '../3d/modeltothree'; +import { ThreejsSceneCache, mergeModelDatas, ob3ModelToThree, mergeNaiveBoneids, constModelsIds, augmentThreeJsFloorMaterial } from '../3d/modeltothree'; import { ModelModifications, constrainedMap, TypedEmitter, CallbackPromise } from '../utils'; import { boundMethod } from 'autobind-decorator'; import { resolveMorphedObject, modifyMesh, MapRect, ParsemapOpts, parseMapsquare, mapsquareModels, mapsquareToThreeSingle, ChunkData, TileGrid, mapsquareSkybox, generateLocationMeshgroups, PlacedMesh, classicChunkSize, rs2ChunkSize, tiledimensions, ModelExtras } from '../3d/mapsquare'; @@ -365,12 +365,13 @@ export type RSMapChunkData = { chunkSize: number, groups: Set, sky: { skybox: Object3D, fogColor: number[], skyboxModelid: number } | null, + generatedMinimap: boolean, modeldata: PlacedMesh[][], chunkmodels: Group[], rect: MapRect } -export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData }> implements ThreeJsSceneElementSource { +export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData, changed: undefined }> implements ThreeJsSceneElementSource { chunkdata: Promise; loaded: RSMapChunkData | null = null; cache: ThreejsSceneCache; @@ -379,7 +380,7 @@ export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData }> impleme renderscene: ThreeJsRenderer | null = null; toggles: Record = {}; rect: MapRect; - minimapmode = false; + generated cleanup() { this.listeners = {}; @@ -414,32 +415,75 @@ export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData }> impleme } onModelLoaded() { - this.setToggles(this.toggles, this.minimapmode); + this.setToggles(this.toggles); this.emit("loaded", this.loaded!); + this.emit("changed", undefined); this.renderscene?.sceneElementsChanged(); // this.renderscene?.setCameraLimits();//TODO fix this, current bounding box calc is too large } - setToggles(toggles: Record, minimap = this.minimapmode) { + generateMinimap() { + if (!this.loaded) { throw new Error("model not loaded yet"); } + if (this.loaded.generatedMinimap) { return; } + this.rootnode.traverse(child => { + if (child instanceof THREE.Mesh) { + let extra = child.userData as ModelExtras | undefined; + let cloned: Mesh | null = null; + if (extra?.modeltype == "location" || extra?.modeltype == "locationgroup") { + cloned = child.clone(); + cloned.position.y -= 0.1 * tiledimensions; + cloned.updateMatrix(); + cloned.updateMatrixWorld(); + + if (extra.modeltype == "location" && extra.isGroundDecor) { + cloned = null; + } else if (extra.modeltype == "locationgroup") { + let geo = cloned.geometry.clone(); + let indexclone = geo.index!.clone(); + let original = geo.index!.array as any as Float32Array; + let array = indexclone.array as any as Float32Array; + let pos = 0; + let startpos = 0; + for (let i = 0; i < extra.subranges.length; i++) { + let endpos = extra.subranges[i]; + let obj = extra.subobjects[i]; + if (obj.modeltype == "location" && !obj.isGroundDecor) { + array.set(original.slice(startpos, endpos), pos); + pos += endpos - startpos + } + startpos = endpos; + } + geo.setDrawRange(0, pos); + geo.setIndex(indexclone); + if (pos == 0) { + cloned = null; + } + } + + } + if (extra?.modeltype == "floor") { + cloned = child.clone(); + cloned.material = (cloned.material as Material).clone(); + augmentThreeJsFloorMaterial(cloned.material, true); + } + if (extra && cloned) { + let modelgroup = `mini_${extra.modelgroup}`; + cloned.userData = { ...extra, modelgroup } satisfies ModelExtras; + this.loaded!.groups.add(modelgroup); + child.parent!.add(cloned); + } + } + }); + this.emit("changed", undefined); + } + + setToggles(toggles: Record) { this.toggles = toggles; this.rootnode.traverse(node => { if (node.userData.modelgroup) { let newvis = toggles[node.userData.modelgroup] ?? true; node.traverse(child => { if (child instanceof THREE.Mesh) { - // let extra = child.userData as ModelExtras | undefined; - // if (extra?.modeltype == "location" || extra?.modeltype == "locationgroup") { - // child.position.y = (minimap ? -0.1 : 0) * tiledimensions; - // child.updateMatrix(); - // child.updateMatrixWorld(); - // } else if (extra?.modeltype == "floor") { - // let qq=child.material as Material; - // qq - // // - // } else { - // // - // } - child.visible = newvis; } }); @@ -447,12 +491,12 @@ export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData }> impleme }); } - constructor(rect: MapRect, cache: ThreejsSceneCache, extraopts?: ParsemapOpts) { + constructor(rect: MapRect, cache: ThreejsSceneCache, extraopts?: ParsemapOpts & { minimap?: boolean }) { super(); this.rect = rect; this.cache = cache; this.chunkdata = (async () => { - let opts: ParsemapOpts = { invisibleLayers: true, minimap: true, collision: true, map2d: false, padfloor: true, skybox: false, ...extraopts }; + let opts: ParsemapOpts = { invisibleLayers: true, collision: true, map2d: false, padfloor: true, skybox: false, ...extraopts }; let { grid, chunks } = await parseMapsquare(cache.engine, rect, opts); let processedChunks = await Promise.all(chunks.map(async chunkdata => { let chunk = await mapsquareModels(cache, grid, chunkdata, opts); @@ -493,7 +537,10 @@ export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData }> impleme let modeldata = processedChunks.flatMap(q => q.locmeshes.byLogical); let chunkmodels = processedChunks.map(q => q.group); - this.loaded = { grid, chunks, groups, sky, modeldata, chunkmodels, chunkSize, rect }; + this.loaded = { grid, chunks, groups, sky, modeldata, chunkmodels, chunkSize, rect, generatedMinimap: false }; + if (extraopts?.minimap) { + this.generateMinimap(); + } this.onModelLoaded(); return this.loaded; })(); diff --git a/src/constants.ts b/src/constants.ts index e1fbd5a..45946e8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -63,6 +63,7 @@ export const cacheConfigPages = { environments: 29, animgroups: 32, mapscenes: 34, + maplabels: 36, //used before 488 (feb 2008) locs_old: 6, diff --git a/src/opcodes/maplabels.jsonc b/src/opcodes/maplabels.jsonc new file mode 100644 index 0000000..eb8d178 --- /dev/null +++ b/src/opcodes/maplabels.jsonc @@ -0,0 +1,48 @@ +{ + "0x01": { "name": "sprite", "read": "varuint" }, + "0x02": { "name": "sprite_hover", "read": "varuint" }, + "0x03": { "name": "text", "read": "string" }, + "0x04": { "name": "color_1", "read": ["tuple","ubyte","ubyte","ubyte"] }, + "0x05": { "name": "color_2", "read": ["tuple","ubyte","ubyte","ubyte"] }, + "0x06": { "name": "font_size", "read": "ubyte" }, + "0x07": { "name": "unknown_07", "read": "ubyte" }, + "0x08": { "name": "unknown_08", "read": "ubyte" }, + "0x09": { "name": "toggle_1", "read": ["struct", + ["varbit","ushort"], + ["varp","ushort"], + ["lower","uint"], + ["upper","uint"] + ] }, + "0x0a": { "name": "rightclick_1", "read": "string" }, + "0x0f": { "name": "polygon", "read": ["struct", + ["pointcount","ubyte"], + ["points",["array",["ref","pointcount"],["struct", + ["x","short"], + ["y","short"] + ]]], + ["color",["tuple","ubyte","ubyte","ubyte","ubyte"]], + ["always_1","ubyte"], + ["back_color",["tuple","ubyte","ubyte","ubyte","ubyte"]], + ["pointplanes",["array",["ref","pointcount"],"ubyte"]] + ] }, + "0x11": { "name": "rightclick_2", "read": "string" }, + "0x13": { "name": "category", "read": "ushort" }, + "0x14": { "name": "toggle_2", "read": ["struct", + ["varbit","ushort"], + ["varp","ushort"], + ["lower","uint"], + ["upper","uint"] + ] }, + "0x15": { "name": "unknown_15", "read": ["array",6,"ubyte"] }, + "0x19": { "name": "background_sprite", "read": "varuint" }, + "0x1a": { "name": "legacy_switch", "read": ["struct", + ["varbit","ushort"], + ["varp","ushort"], + ["value","ubyte"], + ["default_ref","ushort"], + ["legacy_ref","ushort"] + ] }, + "0x1c": { "name": "unknown_1c", "read": "ubyte" }, + "0x1e": { "name": "unknown_1e", "read": "ubyte" }, + "0xF9": { "name": "extra", "read": "extrasmap" } +} \ No newline at end of file diff --git a/src/opdecoder.ts b/src/opdecoder.ts index 78d7edd..0d54a5f 100644 --- a/src/opdecoder.ts +++ b/src/opdecoder.ts @@ -118,6 +118,7 @@ function allParsers() { particles_1: FileParser.fromJson(require("./opcodes/particles_1.jsonc")), audio: FileParser.fromJson(require("./opcodes/audio.jsonc")), proctexture: FileParser.fromJson(require("./opcodes/proctexture.jsonc")), - oldproctexture: FileParser.fromJson(require("./opcodes/oldproctexture.jsonc")) + oldproctexture: FileParser.fromJson(require("./opcodes/oldproctexture.jsonc")), + maplabels: FileParser.fromJson(require("./opcodes/maplabels.jsonc")), } } \ No newline at end of file diff --git a/src/scripts/filetypes.ts b/src/scripts/filetypes.ts index 04f77ea..4bc85f6 100644 --- a/src/scripts/filetypes.ts +++ b/src/scripts/filetypes.ts @@ -493,6 +493,7 @@ export const cacheFileJsonModes = constrainedMap()({ mapscenes: { parser: parse.mapscenes, lookup: singleMinorIndex(cacheMajors.config, cacheConfigPages.mapscenes) }, environments: { parser: parse.environments, lookup: singleMinorIndex(cacheMajors.config, cacheConfigPages.environments) }, animgroupconfigs: { parser: parse.animgroupConfigs, lookup: singleMinorIndex(cacheMajors.config, cacheConfigPages.animgroups) }, + maplabels: { parser: parse.maplabels, lookup: singleMinorIndex(cacheMajors.config, cacheConfigPages.maplabels) }, particles0: { parser: parse.particles_0, lookup: singleMinorIndex(cacheMajors.particles, 0) }, particles1: { parser: parse.particles_1, lookup: singleMinorIndex(cacheMajors.particles, 1) }, diff --git a/src/viewer/scenenodes.tsx b/src/viewer/scenenodes.tsx index 8a1b454..0f5ff60 100644 --- a/src/viewer/scenenodes.tsx +++ b/src/viewer/scenenodes.tsx @@ -1662,22 +1662,21 @@ export class SceneMapModel extends React.Component { - let combined = chunk.rootnode; - + chunk.on("changed", () => { let toggles = this.state.toggles; [...chunk.loaded!.groups].sort((a, b) => a.localeCompare(b)).forEach(q => { if (typeof toggles[q] != "boolean") { - toggles[q] = !q.match(/(floorhidden|collision|walls|map|mapscenes)/); + toggles[q] = !q.match(/^(floorhidden|collision|walls|map|mapscenes)/); } }); - + this.setState({ toggles }); + chunk.setToggles(toggles); + }) + chunk.once("loaded", () => { + let combined = chunk.rootnode; let center = this.state.center; combined.position.add(new Vector3(-center.x, 0, -center.z)); chunk.addToScene(renderer); - chunk.setToggles(toggles); - - this.setState({ toggles }); }); let center = this.state.center; diff --git a/src/viewer/threejsrender.ts b/src/viewer/threejsrender.ts index 662dff6..07ca80d 100644 --- a/src/viewer/threejsrender.ts +++ b/src/viewer/threejsrender.ts @@ -72,6 +72,9 @@ export class ThreeJsRenderer extends TypedEmitter{ private vr360cam: VR360Render | null = null; private forceAspectRatio: number | null = null; + private standardLights: Group; + private minimapLights: Group; + private camMode: RenderCameraMode = "standard"; private camera: THREE.PerspectiveCamera; private topdowncam: THREE.OrthographicCamera; @@ -154,15 +157,23 @@ export class ThreeJsRenderer extends TypedEmitter{ this.modelnode.scale.set(1 / 512, 1 / 512, -1 / 512); this.scene.add(this.modelnode); - //TODO figure out which lights work or not - scene.add(new THREE.AmbientLight(0xffffff, 0.7)); - + //classic light config + this.standardLights = new Group(); + this.standardLights.add(new THREE.AmbientLight(0xffffff, 0.7)); var dirLight = new THREE.DirectionalLight(0xffffff); dirLight.position.set(75, 300, -75); - scene.add(dirLight); - + this.standardLights.add(dirLight); let hemilight = new THREE.HemisphereLight(0xffffff, 0x888844); - scene.add(hemilight); + this.standardLights.add(hemilight); + scene.add(this.standardLights); + + //minimap lights + this.minimapLights = new Group(); + let minidirlight = new THREE.DirectionalLight(0xffffff, 1.2); + minidirlight.position.set(-1, 1, 1); + this.minimapLights.add(minidirlight); + this.minimapLights.add(new THREE.AmbientLight(0xffffff, 0.8)) + scene.add(this.minimapLights); this.scene.fog = new THREE.Fog("#FFFFFF", 10000, 10000); @@ -517,7 +528,9 @@ export class ThreeJsRenderer extends TypedEmitter{ -(cnvy - cnvrect.y) / cnvrect.height * 2 + 1, ); - raycaster.setFromCamera(mousepos, this.camera); + let currentcam = this.camMode == "standard" ? this.getStandardCamera() : this.camMode == "topdown" ? this.getTopdownCamera() : null; + if (!currentcam) { return; } + raycaster.setFromCamera(mousepos, currentcam); let intersects = raycaster.intersectObjects(this.scene.children); let firstloggable = true;