├── .husky ├── .gitignore └── pre-commit ├── src ├── rs │ ├── Client.ts │ ├── config │ │ ├── defaults │ │ │ └── DefaultsGroup.ts │ │ ├── questtype │ │ │ ├── QuestState.ts │ │ │ └── QuestTypeLoader.ts │ │ ├── objtype │ │ │ ├── ObjStackability.ts │ │ │ └── ObjTypeLoader.ts │ │ ├── floortype │ │ │ ├── FloorType.ts │ │ │ └── FloorTypeLoader.ts │ │ ├── vartype │ │ │ ├── client │ │ │ │ ├── VarClientIntType.ts │ │ │ │ └── VarClientStrType.ts │ │ │ ├── player │ │ │ │ └── VarPlayerType.ts │ │ │ ├── bit │ │ │ │ ├── VarBitType.ts │ │ │ │ └── VarBitTypeLoader.ts │ │ │ └── VarManager.ts │ │ ├── structtype │ │ │ └── StructType.ts │ │ ├── mapscenetype │ │ │ ├── MapSceneTypeLoader.ts │ │ │ └── MapSceneType.ts │ │ ├── invtype │ │ │ └── InvType.ts │ │ ├── meltype │ │ │ └── MapElementTypeLoader.ts │ │ ├── bastype │ │ │ └── BasTypeLoader.ts │ │ ├── loctype │ │ │ ├── LocModelType.ts │ │ │ └── LocTypeLoader.ts │ │ ├── seqtype │ │ │ └── SeqTypeLoader.ts │ │ ├── npctype │ │ │ └── NpcTypeLoader.ts │ │ ├── Type.ts │ │ ├── enumtype │ │ │ └── EnumType.ts │ │ ├── paramtype │ │ │ └── ParamType.ts │ │ ├── idktype │ │ │ └── IdkType.ts │ │ └── spotanimtype │ │ │ └── SpotAnimType.ts │ ├── compression │ │ ├── CompressionType.ts │ │ ├── Gzip.ts │ │ ├── Gzip.web.ts │ │ └── Bzip2.ts │ ├── texture │ │ ├── TextureMaterial.ts │ │ ├── TextureCombineMode.ts │ │ ├── procedural │ │ │ └── operation │ │ │ │ ├── HorizontalGradientOperation.ts │ │ │ │ ├── VerticalGradientOperation.ts │ │ │ │ ├── GrayScaleOperation.ts │ │ │ │ ├── ConstantMonochromeOperation.ts │ │ │ │ ├── TilingSpriteOperation.ts │ │ │ │ ├── PseudoRandomNoiseOperation.ts │ │ │ │ ├── BinaryOperation.ts │ │ │ │ ├── ConstantColourOperation.ts │ │ │ │ ├── InvertOperation.ts │ │ │ │ ├── ColourStripOperation.ts │ │ │ │ ├── MonochromeEdgeDetectorOperation.ts │ │ │ │ ├── MandelbrotOperation.ts │ │ │ │ ├── DiagonalGradientOperation.ts │ │ │ │ ├── RangeOperation.ts │ │ │ │ ├── ClampOperation.ts │ │ │ │ ├── TextureOperation.ts │ │ │ │ ├── WeaveOperation.ts │ │ │ │ ├── HerringboneOperation.ts │ │ │ │ └── SpriteSourceOperation.ts │ │ └── TextureLoader.ts │ ├── cache │ │ ├── ApiType.ts │ │ ├── ref │ │ │ ├── ArchiveFileReference.ts │ │ │ └── ArchiveReference.ts │ │ ├── store │ │ │ ├── CacheStore.ts │ │ │ ├── SectorCluster.ts │ │ │ └── Sector.ts │ │ ├── ArchiveFile.ts │ │ ├── CacheType.ts │ │ ├── ConfigType.ts │ │ ├── CacheInfo.ts │ │ ├── IndexType.ts │ │ ├── Container.ts │ │ └── loader │ │ │ └── CacheLoaderFactory.ts │ ├── model │ │ ├── FaceNormal.ts │ │ ├── seq │ │ │ ├── SeqTransformType.ts │ │ │ ├── SeqFrameMap.ts │ │ │ ├── SeqBaseLoader.ts │ │ │ └── SeqFrameLoader.ts │ │ ├── skeletal │ │ │ ├── CurveInterpType.ts │ │ │ ├── SkeletalTransformType.ts │ │ │ ├── CurveType.ts │ │ │ ├── QuatPool.ts │ │ │ ├── MatrixPool.ts │ │ │ ├── SkeletalSeqLoader.ts │ │ │ └── SkeletalBase.ts │ │ └── VertexNormal.ts │ ├── scene │ │ ├── SceneLoc.ts │ │ ├── FloorDecoration.ts │ │ ├── Wall.ts │ │ ├── entity │ │ │ ├── Entity.ts │ │ │ ├── LocEntity.ts │ │ │ └── EntityTag.ts │ │ ├── WallDecoration.ts │ │ ├── Loc.ts │ │ └── SceneTile.ts │ ├── MenuEntry.ts │ ├── util │ │ ├── ArrayUtils.ts │ │ ├── StringUtil.ts │ │ └── HeightCalc.ts │ ├── pathfinder │ │ ├── RouteStrategy.ts │ │ ├── flag │ │ │ └── DirectionFlag.ts │ │ └── CollisionStrategy.ts │ ├── MathConstants.ts │ ├── crypto │ │ └── Xtea.ts │ ├── graphics │ │ └── Rasterizer2D.ts │ └── map │ │ └── MapFileIndex.ts ├── media │ ├── interface-bg.png │ ├── RuneScape-Bold-12.ttf │ ├── RuneScape-Plain-11.ttf │ ├── RuneScape-Plain-12.ttf │ └── interface-border.png ├── import-json.d.ts ├── components │ ├── rs │ │ ├── minimap │ │ │ ├── compass.png │ │ │ ├── minimap-black.png │ │ │ ├── minimap-frame.png │ │ │ ├── worldmap-icon.png │ │ │ ├── worldmap-icon-hover.png │ │ │ ├── MinimapImage.tsx │ │ │ └── MinimapContainer.css │ │ ├── worldmap │ │ │ ├── zoom-in.png │ │ │ ├── zoom-out.png │ │ │ ├── close-button.png │ │ │ ├── close-button-hover.png │ │ │ ├── WorldMapModal.tsx │ │ │ ├── WorldMapModal.css │ │ │ └── WorldMap.css │ │ ├── loading │ │ │ ├── OsrsLoadingBar.tsx │ │ │ └── OsrsLoadingBar.css │ │ ├── menu │ │ │ └── OsrsMenu.css │ │ └── select │ │ │ ├── OsrsSelect.css │ │ │ └── OsrsSelect.tsx │ └── renderer │ │ ├── RendererCanvas.tsx │ │ ├── RenderStats.ts │ │ └── Renderer.ts ├── mapviewer │ ├── webgpu │ │ └── shaders │ │ │ ├── red.frag.wgsl │ │ │ ├── triangle.vert.wgsl │ │ │ └── fullscreenTexturedQuad.wgsl │ ├── webgl │ │ ├── InteractType.ts │ │ ├── shaders │ │ │ ├── frame.vert.glsl │ │ │ ├── includes │ │ │ │ ├── multi-draw.glsl │ │ │ │ ├── fog.glsl │ │ │ │ ├── scene-uniforms.glsl │ │ │ │ ├── material.glsl │ │ │ │ ├── unpack-float.glsl │ │ │ │ ├── fxaa │ │ │ │ │ └── texcoords.glsl │ │ │ │ ├── branchless-logic.glsl │ │ │ │ ├── height-map.glsl │ │ │ │ ├── vertex.glsl │ │ │ │ └── hsl-to-rgb.glsl │ │ │ ├── frame.frag.glsl │ │ │ ├── ShaderUtil.ts │ │ │ ├── frame-fxaa.frag.glsl │ │ │ ├── frame-fxaa.vert.glsl │ │ │ ├── main.frag.glsl │ │ │ └── Shaders.ts │ │ ├── AnimationFrames.ts │ │ ├── loc │ │ │ ├── LocAnimatedGroup.ts │ │ │ ├── SceneLocEntity.ts │ │ │ └── LocAnimatedData.ts │ │ ├── DrawRange.ts │ │ ├── npc │ │ │ ├── NpcSpawnGroup.ts │ │ │ └── NpcData.ts │ │ ├── loader │ │ │ ├── SdMapLoaderInput.ts │ │ │ └── SdMapData.ts │ │ └── buffer │ │ │ ├── ModelHashBuffer.ts │ │ │ └── VertexBuffer.ts │ ├── MapViewerContainer.css │ ├── data │ │ ├── obj │ │ │ └── ObjSpawn.ts │ │ └── npc │ │ │ ├── npc-spawns-2004.json.LICENSE.txt │ │ │ └── NpcSpawn.ts │ ├── buffer │ │ └── DataBuffer.ts │ ├── MapViewerRenderers.ts │ ├── worker │ │ ├── MinimapData.ts │ │ └── RenderDataLoader.ts │ ├── Caches.ts │ └── Frustum.ts ├── java-random.d.ts ├── shaders.d.ts ├── setupTests.ts ├── util │ ├── BytesUtil.ts │ ├── FloatUtil.ts │ ├── DeviceUtil.ts │ ├── Hasher.ts │ └── MathUtil.ts ├── reportWebVitals.ts ├── react-app-env.d.ts ├── picogl.d.ts ├── index.tsx └── index.css ├── public ├── favicon.ico ├── favicon.png ├── robots.txt ├── static │ └── media │ │ └── .htaccess ├── .htaccess └── index.html ├── screenshots └── lumbridge.png ├── .prettierrc.json ├── .gitignore ├── .prettierignore ├── tsconfig.json ├── README.md ├── LICENSE ├── scripts └── cache │ ├── export-textures.ts │ └── load-util.ts ├── craco.config.js └── package.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /src/rs/Client.ts: -------------------------------------------------------------------------------- 1 | export class Client {} 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/rs/config/defaults/DefaultsGroup.ts: -------------------------------------------------------------------------------- 1 | export enum DefaultsGroup { 2 | GRAPHICS = 3, 3 | } 4 | -------------------------------------------------------------------------------- /screenshots/lumbridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/screenshots/lumbridge.png -------------------------------------------------------------------------------- /src/media/interface-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/media/interface-bg.png -------------------------------------------------------------------------------- /src/media/RuneScape-Bold-12.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/media/RuneScape-Bold-12.ttf -------------------------------------------------------------------------------- /src/media/RuneScape-Plain-11.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/media/RuneScape-Plain-11.ttf -------------------------------------------------------------------------------- /src/media/RuneScape-Plain-12.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/media/RuneScape-Plain-12.ttf -------------------------------------------------------------------------------- /src/media/interface-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/media/interface-border.png -------------------------------------------------------------------------------- /src/rs/compression/CompressionType.ts: -------------------------------------------------------------------------------- 1 | export enum CompressionType { 2 | None, 3 | Bzip2, 4 | Gzip, 5 | } 6 | -------------------------------------------------------------------------------- /src/import-json.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json?url" { 2 | const value: string; 3 | 4 | export default value; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/rs/minimap/compass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/components/rs/minimap/compass.png -------------------------------------------------------------------------------- /src/rs/config/questtype/QuestState.ts: -------------------------------------------------------------------------------- 1 | export enum QuestState { 2 | NOT_STARTED, 3 | IN_PROGRESS, 4 | COMPLETED, 5 | } 6 | -------------------------------------------------------------------------------- /src/components/rs/worldmap/zoom-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/components/rs/worldmap/zoom-in.png -------------------------------------------------------------------------------- /src/components/rs/worldmap/zoom-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/components/rs/worldmap/zoom-out.png -------------------------------------------------------------------------------- /public/static/media/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | # AddEncoding br .wasm 3 | AddType application/wasm .wasm 4 | 5 | -------------------------------------------------------------------------------- /src/rs/config/objtype/ObjStackability.ts: -------------------------------------------------------------------------------- 1 | export enum ObjStackability { 2 | SOMETIMES = 0, 3 | ALWAYS = 1, 4 | NEVER = 2, 5 | } 6 | -------------------------------------------------------------------------------- /src/components/rs/minimap/minimap-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/components/rs/minimap/minimap-black.png -------------------------------------------------------------------------------- /src/components/rs/minimap/minimap-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/components/rs/minimap/minimap-frame.png -------------------------------------------------------------------------------- /src/components/rs/minimap/worldmap-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/components/rs/minimap/worldmap-icon.png -------------------------------------------------------------------------------- /src/components/rs/worldmap/close-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/components/rs/worldmap/close-button.png -------------------------------------------------------------------------------- /src/mapviewer/webgpu/shaders/red.frag.wgsl: -------------------------------------------------------------------------------- 1 | @fragment 2 | fn main() -> @location(0) vec4 { 3 | return vec4(1.0, 0.0, 0.0, 1.0); 4 | } 5 | -------------------------------------------------------------------------------- /src/rs/texture/TextureMaterial.ts: -------------------------------------------------------------------------------- 1 | export interface TextureMaterial { 2 | animU: number; 3 | animV: number; 4 | alphaCutOff: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/rs/minimap/worldmap-icon-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/components/rs/minimap/worldmap-icon-hover.png -------------------------------------------------------------------------------- /src/components/rs/worldmap/close-button-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisdev/rs-map-viewer/HEAD/src/components/rs/worldmap/close-button-hover.png -------------------------------------------------------------------------------- /src/mapviewer/webgl/InteractType.ts: -------------------------------------------------------------------------------- 1 | export enum InteractType { 2 | NONE = 0, 3 | LOC = 1, // object 4 | OBJ = 2, // item 5 | NPC = 3, 6 | } 7 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/frame.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | layout(location = 0) in vec4 a_pos; 4 | 5 | void main() { 6 | gl_Position = a_pos; 7 | } 8 | -------------------------------------------------------------------------------- /src/rs/cache/ApiType.ts: -------------------------------------------------------------------------------- 1 | export enum ApiType { 2 | SYNC, 3 | ASYNC, 4 | } 5 | 6 | export type ApiReturnType = A extends ApiType.SYNC ? T : Promise; 7 | -------------------------------------------------------------------------------- /src/rs/model/FaceNormal.ts: -------------------------------------------------------------------------------- 1 | export class FaceNormal { 2 | constructor( 3 | readonly x: number, 4 | readonly y: number, 5 | readonly z: number, 6 | ) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/java-random.d.ts: -------------------------------------------------------------------------------- 1 | declare module "java-random" { 2 | export default class JavaRandom { 3 | constructor(seed: number); 4 | nextInt(bound?: number): number; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/AnimationFrames.ts: -------------------------------------------------------------------------------- 1 | import { DrawRange } from "./DrawRange"; 2 | 3 | export type AnimationFrames = { 4 | frames: DrawRange[]; 5 | framesAlpha: DrawRange[] | undefined; 6 | }; 7 | -------------------------------------------------------------------------------- /src/rs/texture/TextureCombineMode.ts: -------------------------------------------------------------------------------- 1 | export enum TextureCombineMode { 2 | MODULATE = 0, 3 | REPLACE = 1, 4 | ADD = 2, 5 | SUBTRACT = 3, 6 | ADD_SIGNED = 4, 7 | INTERPOLATE = 5, 8 | } 9 | -------------------------------------------------------------------------------- /src/shaders.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.glsl" { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module "*.wgsl?source" { 7 | const value: string; 8 | export default value; 9 | } 10 | -------------------------------------------------------------------------------- /src/rs/cache/ref/ArchiveFileReference.ts: -------------------------------------------------------------------------------- 1 | export class ArchiveFileReference { 2 | constructor( 3 | readonly id: number, 4 | readonly archiveId: number, 5 | readonly nameHash: number, 6 | ) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/rs/cache/store/CacheStore.ts: -------------------------------------------------------------------------------- 1 | import { ApiReturnType, ApiType } from "../ApiType"; 2 | 3 | export interface CacheStore { 4 | read(indexId: number, archiveId: number): ApiReturnType; 5 | } 6 | -------------------------------------------------------------------------------- /src/rs/scene/SceneLoc.ts: -------------------------------------------------------------------------------- 1 | import { EntityTag } from "./entity/EntityTag"; 2 | 3 | export interface SceneLoc { 4 | tag: EntityTag; 5 | flags: number; 6 | x: number; 7 | y: number; 8 | height: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/rs/model/seq/SeqTransformType.ts: -------------------------------------------------------------------------------- 1 | export enum SeqTransformType { 2 | ORIGIN = 0, 3 | TRANSLATE = 1, 4 | ROTATE = 2, 5 | SCALE = 3, 6 | ALPHA = 5, 7 | TYPE_6 = 6, 8 | LIGHT = 7, 9 | TYPE_10 = 10, 10 | } 11 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/loc/LocAnimatedGroup.ts: -------------------------------------------------------------------------------- 1 | import { AnimationFrames } from "../AnimationFrames"; 2 | import { SceneLocEntity } from "./SceneLocEntity"; 3 | 4 | export type LocAnimatedGroup = { 5 | anim: AnimationFrames; 6 | locs: SceneLocEntity[]; 7 | }; 8 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/multi-draw.glsl: -------------------------------------------------------------------------------- 1 | #ifdef MULTI_DRAW 2 | 3 | #extension GL_ANGLE_multi_draw : require 4 | #define DRAW_ID gl_DrawID 5 | 6 | #else 7 | 8 | #define DRAW_ID u_drawId 9 | 10 | uniform int u_drawId; 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /src/rs/model/seq/SeqFrameMap.ts: -------------------------------------------------------------------------------- 1 | import { SeqFrame } from "./SeqFrame"; 2 | 3 | export class SeqFrameMap { 4 | constructor(readonly frames: SeqFrame[]) {} 5 | 6 | hasAlphaTransform(frame: number) { 7 | return this.frames[frame].hasAlphaTransform; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/DrawRange.ts: -------------------------------------------------------------------------------- 1 | export type DrawRange = [number, number, number]; 2 | 3 | export function newDrawRange(offset: number, elements: number, instances: number = 1): DrawRange { 4 | return [offset, elements, instances]; 5 | } 6 | 7 | export const NULL_DRAW_RANGE = newDrawRange(0, 0, 0); 8 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/npc/NpcSpawnGroup.ts: -------------------------------------------------------------------------------- 1 | import { NpcSpawn } from "../../data/npc/NpcSpawn"; 2 | import { AnimationFrames } from "../AnimationFrames"; 3 | 4 | export type NpcSpawnGroup = { 5 | idleAnim: AnimationFrames; 6 | walkAnim: AnimationFrames | undefined; 7 | spawns: NpcSpawn[]; 8 | }; 9 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/frame.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | uniform highp sampler2D u_frame; 6 | 7 | out vec4 fragColor; 8 | 9 | void main() { 10 | ivec2 fragCoord = ivec2(gl_FragCoord.xy); 11 | fragColor = texelFetch(u_frame, fragCoord, 0); 12 | } 13 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 100, 4 | "trailingComma": "all", 5 | "semi": true, 6 | "importOrder": ["^[./]"], 7 | "importOrderSeparation": true, 8 | "importOrderSortSpecifiers": true, 9 | "plugins": ["@trivago/prettier-plugin-sort-imports"] 10 | } 11 | -------------------------------------------------------------------------------- /src/rs/MenuEntry.ts: -------------------------------------------------------------------------------- 1 | export enum MenuTargetType { 2 | NONE, 3 | PLAYER, 4 | NPC, 5 | LOC, 6 | OBJ, 7 | } 8 | 9 | export interface MenuEntry { 10 | option: string; 11 | targetId: number; 12 | targetType: MenuTargetType; 13 | targetName: string; 14 | targetLevel: number; 15 | } 16 | -------------------------------------------------------------------------------- /src/rs/config/floortype/FloorType.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "../Type"; 2 | 3 | export interface FloorType extends Type { 4 | hue: number; 5 | saturation: number; 6 | lightness: number; 7 | 8 | isOverlay: boolean; 9 | 10 | getHueBlend(): number; 11 | 12 | getHueMultiplier(): number; 13 | } 14 | -------------------------------------------------------------------------------- /src/rs/compression/Gzip.ts: -------------------------------------------------------------------------------- 1 | import pako from "pako"; 2 | 3 | export class Gzip { 4 | static async initWasm(): Promise {} 5 | 6 | static decompress(compressed: Uint8Array): Int8Array { 7 | const decompressed = new Int8Array(pako.ungzip(compressed).buffer); 8 | return decompressed; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteBase / 3 | RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] 4 | RewriteRule ^(.*)$ https://%1/$1 [R=301,L] 5 | 6 | 7 | Header append Cross-Origin-Opener-Policy: same-origin 8 | Header append Cross-Origin-Embedder-Policy: require-corp 9 | 10 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/fog.glsl: -------------------------------------------------------------------------------- 1 | float fogFactorLinear(float dist, float start, float end) { 2 | return 1.0 - clamp((dist - start) / (end - start), 0.0, 1.0); 3 | } 4 | 5 | float sdRoundedBox(vec2 p, vec2 b, float r) { 6 | vec2 q = abs(p) - b + r; 7 | return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; 8 | } 9 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/loader/SdMapLoaderInput.ts: -------------------------------------------------------------------------------- 1 | export type SdMapLoaderInput = { 2 | mapX: number; 3 | mapY: number; 4 | 5 | maxLevel: number; 6 | loadObjs: boolean; 7 | loadNpcs: boolean; 8 | 9 | smoothTerrain: boolean; 10 | 11 | minimizeDrawCalls: boolean; 12 | 13 | loadedTextureIds: Set; 14 | }; 15 | -------------------------------------------------------------------------------- /src/rs/util/ArrayUtils.ts: -------------------------------------------------------------------------------- 1 | export class ArrayUtils { 2 | static fill(array: Int32Array, start: number, length: number, value: number) { 3 | array.fill(value, start, start + length); 4 | } 5 | static fillRange(array: Int32Array, start: number, end: number, value: number) { 6 | array.fill(value, start, end); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/mapviewer/webgpu/shaders/triangle.vert.wgsl: -------------------------------------------------------------------------------- 1 | @vertex 2 | fn main( 3 | @builtin(vertex_index) VertexIndex : u32 4 | ) -> @builtin(position) vec4 { 5 | var pos = array, 3>( 6 | vec2(0.0, 0.5), 7 | vec2(-0.5, -0.5), 8 | vec2(0.5, -0.5) 9 | ); 10 | 11 | return vec4(pos[VertexIndex], 0.0, 1.0); 12 | } 13 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/loc/SceneLocEntity.ts: -------------------------------------------------------------------------------- 1 | import { SceneLoc } from "../../../rs/scene/SceneLoc"; 2 | import { LocEntity } from "../../../rs/scene/entity/LocEntity"; 3 | import { ModelInfo } from "../buffer/SceneBuffer"; 4 | 5 | export type SceneLocEntity = { 6 | entity: LocEntity; 7 | sceneLoc: SceneLoc; 8 | lowDetail: boolean; 9 | } & ModelInfo; 10 | -------------------------------------------------------------------------------- /src/components/rs/minimap/MinimapImage.tsx: -------------------------------------------------------------------------------- 1 | interface MinimapImageProps { 2 | src: string; 3 | 4 | left: number; 5 | top: number; 6 | } 7 | 8 | export function MinimapImage({ src, left, top }: MinimapImageProps) { 9 | return ( 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/rs/cache/ArchiveFile.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../io/ByteBuffer"; 2 | 3 | export class ArchiveFile { 4 | constructor( 5 | readonly id: number, 6 | readonly archiveId: number, 7 | readonly data: Int8Array, 8 | ) {} 9 | 10 | getDataAsBuffer(): ByteBuffer { 11 | return new ByteBuffer(this.data); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/rs/model/skeletal/CurveInterpType.ts: -------------------------------------------------------------------------------- 1 | export enum CurveInterpType { 2 | TYPE_0 = 0, 3 | TYPE_1 = 1, 4 | TYPE_2 = 2, 5 | TYPE_3 = 3, 6 | TYPE_4 = 4, 7 | } 8 | 9 | export function getInterpTypeForId(id: number): CurveInterpType { 10 | if (id < 0 || id > CurveInterpType.TYPE_4) { 11 | return CurveInterpType.TYPE_0; 12 | } 13 | return id; 14 | } 15 | -------------------------------------------------------------------------------- /src/rs/config/vartype/client/VarClientIntType.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { Type } from "../../Type"; 3 | 4 | export class VarClientIntType extends Type { 5 | persist: boolean = false; 6 | 7 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 8 | if (opcode === 2) { 9 | this.persist = true; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/rs/config/vartype/client/VarClientStrType.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { Type } from "../../Type"; 3 | 4 | export class VarClientStrType extends Type { 5 | persist: boolean = false; 6 | 7 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 8 | if (opcode === 2) { 9 | this.persist = true; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/ShaderUtil.ts: -------------------------------------------------------------------------------- 1 | export type ProgramSource = [string, string]; 2 | 3 | export function prependDefines(source: string, defines: string[]): string { 4 | const header = "#version 300 es"; 5 | 6 | const newHeader = defines.reduce((acc, define) => { 7 | return acc + "#define " + define + " 1\n"; 8 | }, header + "\n"); 9 | 10 | return source.replace(header, newHeader); 11 | } 12 | -------------------------------------------------------------------------------- /src/rs/config/structtype/StructType.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../io/ByteBuffer"; 2 | import { ParamsMap, Type } from "../Type"; 3 | 4 | export class StructType extends Type { 5 | params!: ParamsMap; 6 | 7 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 8 | if (opcode === 249) { 9 | this.params = Type.readParamsMap(buffer, this.params); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/scene-uniforms.glsl: -------------------------------------------------------------------------------- 1 | // Per frame 2 | uniform SceneUniforms { 3 | mat4 u_viewProjMatrix; 4 | mat4 u_viewMatrix; 5 | mat4 u_projectionMatrix; 6 | vec4 u_skyColor; 7 | vec2 u_cameraPos; 8 | float u_renderDistance; 9 | float u_fogDepth; 10 | float u_currentTime; 11 | float u_brightness; 12 | float u_colorBanding; 13 | float u_isNewTextureAnim; 14 | }; 15 | -------------------------------------------------------------------------------- /src/rs/model/VertexNormal.ts: -------------------------------------------------------------------------------- 1 | export class VertexNormal { 2 | x: number = 0; 3 | y: number = 0; 4 | z: number = 0; 5 | 6 | magnitude: number = 0; 7 | 8 | static copy(other: VertexNormal): VertexNormal { 9 | const normal = new VertexNormal(); 10 | normal.x = other.x; 11 | normal.y = other.y; 12 | normal.z = other.z; 13 | normal.magnitude = other.magnitude; 14 | return normal; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | caches/ 26 | -------------------------------------------------------------------------------- /src/rs/config/mapscenetype/MapSceneTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheInfo } from "../../cache/CacheInfo"; 3 | import { ArchiveTypeLoader } from "../TypeLoader"; 4 | import { MapSceneType } from "./MapSceneType"; 5 | 6 | export class MapSceneTypeLoader extends ArchiveTypeLoader { 7 | constructor(cacheInfo: CacheInfo, archive: Archive) { 8 | super(MapSceneType, cacheInfo, archive); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/material.glsl: -------------------------------------------------------------------------------- 1 | struct Material { 2 | int animU; 3 | int animV; 4 | float alphaCutOff; 5 | }; 6 | 7 | Material getMaterial(uint textureId) { 8 | ivec4 data = texelFetch(u_textureMaterials, ivec2(textureId, 0), 0); 9 | 10 | Material material; 11 | material.animU = data.r; 12 | material.animV = data.g; 13 | material.alphaCutOff = float(data.b & 0xFF) / 255.0; 14 | 15 | return material; 16 | } 17 | -------------------------------------------------------------------------------- /src/rs/scene/FloorDecoration.ts: -------------------------------------------------------------------------------- 1 | import { SceneLoc } from "./SceneLoc"; 2 | import { Entity } from "./entity/Entity"; 3 | import { EntityTag } from "./entity/EntityTag"; 4 | 5 | export class FloorDecoration implements SceneLoc { 6 | constructor( 7 | public entity: Entity, 8 | readonly x: number, 9 | readonly y: number, 10 | readonly height: number, 11 | readonly tag: EntityTag, 12 | readonly flags: number, 13 | ) {} 14 | } 15 | -------------------------------------------------------------------------------- /src/util/BytesUtil.ts: -------------------------------------------------------------------------------- 1 | export function formatBytes(bytes: number, decimals: number = 2): string { 2 | if (!+bytes) { 3 | return "0 Bytes"; 4 | } 5 | 6 | const k = 1024; 7 | const dm = decimals < 0 ? 0 : decimals; 8 | const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 9 | 10 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 11 | 12 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; 13 | } 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Deliberately minified file 26 | *.min.* -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/frame-fxaa.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | in vec2 v_rgbNW; 6 | in vec2 v_rgbNE; 7 | in vec2 v_rgbSW; 8 | in vec2 v_rgbSE; 9 | in vec2 v_rgbM; 10 | 11 | uniform highp sampler2D u_frame; 12 | uniform vec2 u_resolution; 13 | 14 | out vec4 fragColor; 15 | 16 | #include "./includes/fxaa/fxaa.glsl"; 17 | 18 | void main() { 19 | fragColor = fxaa(u_frame, gl_FragCoord.xy, u_resolution, v_rgbNW, v_rgbNE, v_rgbSW, v_rgbSE, v_rgbM); 20 | } 21 | -------------------------------------------------------------------------------- /src/rs/cache/store/SectorCluster.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../io/ByteBuffer"; 2 | 3 | export class SectorCluster { 4 | static readonly SIZE = 6; 5 | 6 | static decode(buffer: ByteBuffer): SectorCluster { 7 | const size = buffer.readMedium(); 8 | const sector = buffer.readMedium(); 9 | return new SectorCluster(size, sector); 10 | } 11 | 12 | constructor( 13 | readonly size: number, 14 | readonly sector: number, 15 | ) {} 16 | } 17 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/HorizontalGradientOperation.ts: -------------------------------------------------------------------------------- 1 | import { TextureGenerator } from "../TextureGenerator"; 2 | import { TextureOperation } from "./TextureOperation"; 3 | 4 | export class HorizontalGradientOperation extends TextureOperation { 5 | constructor() { 6 | super(0, true); 7 | } 8 | 9 | override getMonochromeOutput(textureGenerator: TextureGenerator, line: number): Int32Array { 10 | return textureGenerator.horizontalGradient; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/rs/scene/Wall.ts: -------------------------------------------------------------------------------- 1 | import { SceneLoc } from "./SceneLoc"; 2 | import { Entity } from "./entity/Entity"; 3 | import { EntityTag } from "./entity/EntityTag"; 4 | 5 | export class Wall implements SceneLoc { 6 | constructor( 7 | readonly tag: EntityTag, 8 | readonly flags: number, 9 | readonly x: number, 10 | readonly y: number, 11 | readonly height: number, 12 | public entity0: Entity | undefined, 13 | public entity1: Entity | undefined, 14 | ) {} 15 | } 16 | -------------------------------------------------------------------------------- /src/rs/pathfinder/RouteStrategy.ts: -------------------------------------------------------------------------------- 1 | export abstract class RouteStrategy { 2 | approxDestX: number = 0; 3 | approxDestY: number = 0; 4 | destSizeX: number = 1; 5 | destSizeY: number = 1; 6 | 7 | abstract hasArrived(tileX: number, tileY: number, level: number): boolean; 8 | } 9 | 10 | export class ExactRouteStrategy extends RouteStrategy { 11 | hasArrived(tileX: number, tileY: number, level: number): boolean { 12 | return tileX === this.approxDestX && tileY === this.approxDestY; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/rs/scene/entity/Entity.ts: -------------------------------------------------------------------------------- 1 | export abstract class Entity { 2 | height: number = 1000; 3 | 4 | canMergeNormals(): boolean { 5 | return false; 6 | } 7 | 8 | mergeNormals( 9 | entity: Entity, 10 | offsetX: number, 11 | offsetY: number, 12 | offsetZ: number, 13 | hideOccluded: boolean, 14 | ): void {} 15 | 16 | // light(textureLoader: TextureLoader, lightX: number, lightY: number, lightZ: number): Entity { 17 | // return this; 18 | // } 19 | } 20 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/rs/scene/entity/LocEntity.ts: -------------------------------------------------------------------------------- 1 | import { LocModelType } from "../../config/loctype/LocModelType"; 2 | import { Entity } from "./Entity"; 3 | 4 | export class LocEntity extends Entity { 5 | constructor( 6 | readonly id: number, 7 | readonly type: LocModelType, 8 | readonly rotation: number, 9 | readonly level: number, 10 | readonly tileX: number, 11 | readonly tileY: number, 12 | readonly seqId: number, 13 | readonly seqRandomStart: boolean, 14 | ) { 15 | super(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/frame-fxaa.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | layout(location = 0) in vec4 a_pos; 4 | 5 | uniform vec2 u_resolution; 6 | 7 | out mediump vec2 v_rgbNW; 8 | out mediump vec2 v_rgbNE; 9 | out mediump vec2 v_rgbSW; 10 | out mediump vec2 v_rgbSE; 11 | out mediump vec2 v_rgbM; 12 | 13 | #include "./includes/fxaa/texcoords.glsl"; 14 | 15 | void main() { 16 | gl_Position = a_pos; 17 | vec2 texCoord = 0.5 * gl_Position.xy + vec2(0.5); 18 | texcoords(texCoord * u_resolution, u_resolution, v_rgbNW, v_rgbNE, v_rgbSW, v_rgbSE, v_rgbM); 19 | } 20 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/loc/LocAnimatedData.ts: -------------------------------------------------------------------------------- 1 | import { AnimationFrames } from "../AnimationFrames"; 2 | 3 | export type LocAnimatedData = { 4 | drawRangeIndex: number; 5 | drawRangeAlphaIndex: number; 6 | 7 | drawRangeLodIndex: number; 8 | drawRangeLodAlphaIndex: number; 9 | 10 | drawRangeInteractIndex: number; 11 | drawRangeInteractAlphaIndex: number; 12 | 13 | drawRangeInteractLodIndex: number; 14 | drawRangeInteractLodAlphaIndex: number; 15 | 16 | anim: AnimationFrames; 17 | 18 | seqId: number; 19 | randomStart: boolean; 20 | }; 21 | -------------------------------------------------------------------------------- /src/rs/config/questtype/QuestTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheInfo } from "../../cache/CacheInfo"; 3 | import { ArchiveTypeLoader, TypeLoader } from "../TypeLoader"; 4 | import { QuestType } from "./QuestType"; 5 | 6 | export type QuestTypeLoader = TypeLoader; 7 | 8 | export class ArchiveQuestTypeLoader 9 | extends ArchiveTypeLoader 10 | implements QuestTypeLoader 11 | { 12 | constructor(cacheInfo: CacheInfo, archive: Archive) { 13 | super(QuestType, cacheInfo, archive); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/rs/config/invtype/InvType.ts: -------------------------------------------------------------------------------- 1 | import { CacheInfo } from "../../cache/CacheInfo"; 2 | import { ByteBuffer } from "../../io/ByteBuffer"; 3 | import { Type } from "../Type"; 4 | 5 | export class InvType extends Type { 6 | itemCount: number; 7 | 8 | constructor(id: number, cacheInfo: CacheInfo) { 9 | super(id, cacheInfo); 10 | this.itemCount = 0; 11 | } 12 | 13 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 14 | if (opcode === 2) { 15 | this.itemCount = buffer.readUnsignedShort(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/rs/config/vartype/player/VarPlayerType.ts: -------------------------------------------------------------------------------- 1 | import { CacheInfo } from "../../../cache/CacheInfo"; 2 | import { ByteBuffer } from "../../../io/ByteBuffer"; 3 | import { Type } from "../../Type"; 4 | 5 | export class VarPlayerType extends Type { 6 | type: number; 7 | 8 | constructor(id: number, cacheInfo: CacheInfo) { 9 | super(id, cacheInfo); 10 | this.type = 0; 11 | } 12 | 13 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 14 | if (opcode === 5) { 15 | this.type = buffer.readUnsignedShort(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/rs/pathfinder/flag/DirectionFlag.ts: -------------------------------------------------------------------------------- 1 | export class DirectionFlag { 2 | static readonly NORTH: number = 0x1; 3 | static readonly EAST: number = 0x2; 4 | static readonly SOUTH: number = 0x4; 5 | static readonly WEST: number = 0x8; 6 | 7 | static readonly SOUTH_WEST: number = DirectionFlag.WEST | DirectionFlag.SOUTH; 8 | static readonly NORTH_WEST: number = DirectionFlag.WEST | DirectionFlag.NORTH; 9 | static readonly SOUTH_EAST: number = DirectionFlag.EAST | DirectionFlag.SOUTH; 10 | static readonly NORTH_EAST: number = DirectionFlag.EAST | DirectionFlag.NORTH; 11 | } 12 | -------------------------------------------------------------------------------- /src/rs/scene/WallDecoration.ts: -------------------------------------------------------------------------------- 1 | import { SceneLoc } from "./SceneLoc"; 2 | import { Entity } from "./entity/Entity"; 3 | import { EntityTag } from "./entity/EntityTag"; 4 | 5 | export class WallDecoration implements SceneLoc { 6 | constructor( 7 | readonly tag: EntityTag, 8 | readonly flags: number, 9 | readonly x: number, 10 | readonly y: number, 11 | readonly height: number, 12 | readonly entity0: Entity, 13 | readonly entity1: Entity | undefined, 14 | public offsetX: number, 15 | public offsetY: number, 16 | ) {} 17 | } 18 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import React from "react"; 3 | 4 | declare global { 5 | interface Window { 6 | wallpaperRegisterAudioListener: any; 7 | 8 | wallpaperPropertyListener: any; 9 | 10 | wallpaperFpsLimit?: number; 11 | } 12 | } 13 | 14 | declare module "react" { 15 | function memo>( 16 | c: T, 17 | areEqual?: ( 18 | prev: Readonly>, 19 | next: Readonly>, 20 | ) => boolean, 21 | ): T; 22 | } 23 | -------------------------------------------------------------------------------- /src/rs/model/skeletal/SkeletalTransformType.ts: -------------------------------------------------------------------------------- 1 | export enum SkeletalTransformType { 2 | TYPE_0 = 0, 3 | BONE = 1, 4 | TYPE_2 = 2, 5 | TYPE_3 = 3, 6 | ALPHA = 4, 7 | TYPE_5 = 5, 8 | } 9 | 10 | export function getTransformTypeForId(id: number): SkeletalTransformType { 11 | if (id < 0 || id > SkeletalTransformType.TYPE_5) { 12 | return SkeletalTransformType.TYPE_0; 13 | } 14 | return id; 15 | } 16 | 17 | const CURVE_COUNTS = [0, 9, 3, 6, 1, 3]; 18 | 19 | export function getCurveCount(type: SkeletalTransformType): number { 20 | return CURVE_COUNTS[type]; 21 | } 22 | -------------------------------------------------------------------------------- /src/rs/config/meltype/MapElementTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheInfo } from "../../cache/CacheInfo"; 3 | import { ArchiveTypeLoader, TypeLoader } from "../TypeLoader"; 4 | import { MapElementType } from "./MapElementType"; 5 | 6 | export type MapElementTypeLoader = TypeLoader; 7 | 8 | export class ArchiveMapElementTypeLoader 9 | extends ArchiveTypeLoader 10 | implements MapElementTypeLoader 11 | { 12 | constructor(cacheInfo: CacheInfo, archive: Archive) { 13 | super(MapElementType, cacheInfo, archive); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/unpack-float.glsl: -------------------------------------------------------------------------------- 1 | float unpackFloat16(int v) { 2 | int exponent = v >> 10; 3 | float mantissa = float(v & 0x3FF) / 1024.0; 4 | return float(exponent) + mantissa; 5 | } 6 | 7 | float unpackFloat12(uint v) { 8 | return 16.0 - float(v) / 128.0; 9 | } 10 | 11 | float unpackFloat11(uint v) { 12 | return 16.0 - float(v) / 64.0; 13 | } 14 | 15 | float unpackFloat11(int v) { 16 | return 16.0 - float(v) / 64.0; 17 | } 18 | 19 | float unpackFloat6(uint v) { 20 | return float(v) / 63.0; 21 | } 22 | 23 | float unpackFloat6(int v) { 24 | return float(v) / 63.0; 25 | } 26 | -------------------------------------------------------------------------------- /src/mapviewer/MapViewerContainer.css: -------------------------------------------------------------------------------- 1 | .overlay-container { 2 | display: flex; 3 | position: absolute; 4 | justify-content: center; 5 | align-items: center; 6 | width: 100%; 7 | background: rgba(0, 0, 0, 0.5); 8 | } 9 | 10 | .hud.left-top { 11 | position: absolute; 12 | padding: 10px; 13 | } 14 | .hud.right-top { 15 | position: absolute; 16 | padding: 10px; 17 | right: 0; 18 | } 19 | 20 | .joystick-container { 21 | position: absolute; 22 | bottom: 0; 23 | padding: 40px; 24 | } 25 | 26 | .joystick-container.left { 27 | left: 0; 28 | } 29 | 30 | .joystick-container.right { 31 | right: 0; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/rs/loading/OsrsLoadingBar.tsx: -------------------------------------------------------------------------------- 1 | import "./OsrsLoadingBar.css"; 2 | 3 | interface OsrsLoadingBarProps { 4 | text: string; 5 | progress: number; 6 | } 7 | 8 | export function OsrsLoadingBar({ text, progress }: OsrsLoadingBarProps): JSX.Element { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 | {text} - {progress}% 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/rs/scene/Loc.ts: -------------------------------------------------------------------------------- 1 | import { SceneLoc } from "./SceneLoc"; 2 | import { Entity } from "./entity/Entity"; 3 | import { EntityTag } from "./entity/EntityTag"; 4 | 5 | export class Loc implements SceneLoc { 6 | constructor( 7 | readonly tag: EntityTag, 8 | readonly flags: number, 9 | public level: number, 10 | readonly x: number, 11 | readonly y: number, 12 | readonly height: number, 13 | public entity: Entity, 14 | readonly rotation: number, 15 | readonly startX: number, 16 | readonly startY: number, 17 | readonly endX: number, 18 | readonly endY: number, 19 | ) {} 20 | } 21 | -------------------------------------------------------------------------------- /src/rs/config/bastype/BasTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheInfo } from "../../cache/CacheInfo"; 3 | import { ArchiveTypeLoader, DummyTypeLoader, TypeLoader } from "../TypeLoader"; 4 | import { BasType } from "./BasType"; 5 | 6 | export type BasTypeLoader = TypeLoader; 7 | 8 | export class DummyBasTypeLoader extends DummyTypeLoader { 9 | constructor(cacheInfo: CacheInfo) { 10 | super(cacheInfo, BasType); 11 | } 12 | } 13 | 14 | export class ArchiveBasTypeLoader extends ArchiveTypeLoader { 15 | constructor(cacheInfo: CacheInfo, archive: Archive) { 16 | super(BasType, cacheInfo, archive); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/rs/config/vartype/bit/VarBitType.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { Type } from "../../Type"; 3 | 4 | export class VarBitType extends Type { 5 | baseVar!: number; 6 | 7 | startBit!: number; 8 | endBit!: number; 9 | 10 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 11 | if (opcode === 1) { 12 | this.baseVar = buffer.readUnsignedShort(); 13 | this.startBit = buffer.readUnsignedByte(); 14 | this.endBit = buffer.readUnsignedByte(); 15 | } else { 16 | throw new Error("VarBitType: Opcode " + opcode + " not implemented. id: " + this.id); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/rs/config/mapscenetype/MapSceneType.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../io/ByteBuffer"; 2 | import { Type } from "../Type"; 3 | 4 | export class MapSceneType extends Type { 5 | spriteId: number = -1; 6 | colorRgb: number = 0; 7 | enlarge: boolean = false; 8 | 9 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 10 | if (opcode === 1) { 11 | this.spriteId = buffer.readUnsignedShort(); 12 | } else if (opcode === 2) { 13 | this.colorRgb = buffer.readMedium(); 14 | } else if (opcode === 3) { 15 | this.enlarge = true; 16 | } else if (opcode === 4) { 17 | this.spriteId = -1; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/rs/cache/CacheType.ts: -------------------------------------------------------------------------------- 1 | import { CacheInfo } from "./CacheInfo"; 2 | 3 | export type CacheType = "classic" | "legacy" | "dat" | "dat2"; 4 | 5 | export function detectCacheType(cacheInfo: CacheInfo): CacheType { 6 | switch (cacheInfo.game) { 7 | case "classic": 8 | return "classic"; 9 | case "runescape": 10 | if (cacheInfo.revision < 234) { 11 | return "legacy"; 12 | } else if (cacheInfo.revision < 410) { 13 | return "dat"; 14 | } else { 15 | return "dat2"; 16 | } 17 | case "oldschool": 18 | return "dat2"; 19 | default: 20 | throw new Error("Unknown game type: " + cacheInfo.game); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/mapviewer/data/obj/ObjSpawn.ts: -------------------------------------------------------------------------------- 1 | import objSpawnsUrl from "./obj-spawns.json?url"; 2 | 3 | export interface ObjSpawn { 4 | id: number; 5 | count: number; 6 | x: number; 7 | y: number; 8 | plane: number; 9 | } 10 | 11 | export async function fetchObjSpawns(): Promise { 12 | const response = await fetch(objSpawnsUrl); 13 | return await response.json(); 14 | } 15 | 16 | export function getMapObjSpawns( 17 | spawns: ObjSpawn[], 18 | maxLevel: number, 19 | mapX: number, 20 | mapY: number, 21 | ): ObjSpawn[] { 22 | return spawns.filter((obj) => { 23 | const objMapX = (obj.x / 64) | 0; 24 | const objMapY = (obj.y / 64) | 0; 25 | return mapX === objMapX && mapY === objMapY && obj.plane <= maxLevel; 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/rs/model/skeletal/CurveType.ts: -------------------------------------------------------------------------------- 1 | export enum CurveType { 2 | TYPE_0 = 0, 3 | TYPE_1 = 1, 4 | TYPE_2 = 2, 5 | TYPE_3 = 3, 6 | TYPE_4 = 4, 7 | TYPE_5 = 5, 8 | TYPE_6 = 6, 9 | TYPE_7 = 7, 10 | TYPE_8 = 8, 11 | TYPE_9 = 9, 12 | TYPE_10 = 10, 13 | TYPE_11 = 11, 14 | TYPE_12 = 12, 15 | TYPE_13 = 13, 16 | TYPE_14 = 14, 17 | TYPE_15 = 15, 18 | TYPE_16 = 16, 19 | } 20 | 21 | export function getCurveTypeForId(id: number): CurveType { 22 | if (id < 0 || id > CurveType.TYPE_16) { 23 | return CurveType.TYPE_0; 24 | } 25 | return id; 26 | } 27 | 28 | const CURVE_INDICES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 0]; 29 | 30 | export function getCurveIndex(type: CurveType): number { 31 | return CURVE_INDICES[type]; 32 | } 33 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/fxaa/texcoords.glsl: -------------------------------------------------------------------------------- 1 | //To save 9 dependent texture reads, you can compute 2 | //these in the vertex shader and use the optimized 3 | //frag.glsl function in your frag shader. 4 | 5 | //This is best suited for mobile devices, like iOS. 6 | 7 | void texcoords( 8 | vec2 fragCoord, 9 | vec2 resolution, 10 | out vec2 v_rgbNW, 11 | out vec2 v_rgbNE, 12 | out vec2 v_rgbSW, 13 | out vec2 v_rgbSE, 14 | out vec2 v_rgbM 15 | ) { 16 | vec2 inverseVP = 1.0 / resolution.xy; 17 | v_rgbNW = (fragCoord + vec2(-1.0, -1.0)) * inverseVP; 18 | v_rgbNE = (fragCoord + vec2(1.0, -1.0)) * inverseVP; 19 | v_rgbSW = (fragCoord + vec2(-1.0, 1.0)) * inverseVP; 20 | v_rgbSE = (fragCoord + vec2(1.0, 1.0)) * inverseVP; 21 | v_rgbM = vec2(fragCoord * inverseVP); 22 | } 23 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/VerticalGradientOperation.ts: -------------------------------------------------------------------------------- 1 | import { TextureGenerator } from "../TextureGenerator"; 2 | import { TextureOperation } from "./TextureOperation"; 3 | 4 | export class VerticalGradientOperation extends TextureOperation { 5 | constructor() { 6 | super(0, true); 7 | } 8 | 9 | override getMonochromeOutput(textureGenerator: TextureGenerator, line: number): Int32Array { 10 | if (!this.monochromeImageCache) { 11 | throw new Error("Monochrome image cache is not initialized"); 12 | } 13 | const output = this.monochromeImageCache.get(line); 14 | if (this.monochromeImageCache.dirty) { 15 | output.fill(textureGenerator.verticalGradient[line], 0, textureGenerator.width); 16 | } 17 | return output; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/picogl.d.ts: -------------------------------------------------------------------------------- 1 | import * as PicoGL from "picogl"; 2 | 3 | declare module "picogl" { 4 | export interface DrawCall extends Omit { 5 | offsets: number[]; 6 | numElements: number[]; 7 | } 8 | 9 | export interface Texture { 10 | bind(unit: number): void; 11 | } 12 | 13 | import PicoGL, { 14 | App, 15 | DrawCall, 16 | VertexArray, 17 | VertexBuffer, 18 | Texture, 19 | Timer, 20 | Program, 21 | UniformBuffer, 22 | } from "picogl"; 23 | 24 | export default PicoGL; 25 | export { 26 | PicoGL, 27 | App, 28 | DrawCall, 29 | VertexArray, 30 | VertexBuffer, 31 | Texture, 32 | Timer, 33 | Program, 34 | UniformBuffer, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/rs/scene/SceneTile.ts: -------------------------------------------------------------------------------- 1 | import { FloorDecoration } from "./FloorDecoration"; 2 | import { Loc } from "./Loc"; 3 | import { SceneTileModel } from "./SceneTileModel"; 4 | import { Wall } from "./Wall"; 5 | import { WallDecoration } from "./WallDecoration"; 6 | 7 | export class SceneTile { 8 | initLevel: number; 9 | level: number; 10 | x: number; 11 | y: number; 12 | minLevel: number; 13 | 14 | tileModel?: SceneTileModel; 15 | floorDecoration?: FloorDecoration; 16 | wall?: Wall; 17 | wallDecoration?: WallDecoration; 18 | locs: Loc[]; 19 | 20 | linkedBelowTile?: SceneTile; 21 | 22 | constructor(level: number, x: number, y: number) { 23 | this.level = this.initLevel = level; 24 | this.x = x; 25 | this.y = y; 26 | this.minLevel = 0; 27 | this.locs = []; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/rs/texture/TextureLoader.ts: -------------------------------------------------------------------------------- 1 | import { TextureMaterial } from "./TextureMaterial"; 2 | 3 | export interface TextureLoader { 4 | getTextureIds(): number[]; 5 | 6 | getTextureIndex(id: number): number; 7 | 8 | isSmall(id: number): boolean; 9 | 10 | isSd(id: number): boolean; 11 | 12 | isTransparent(id: number): boolean; 13 | 14 | // TODO: move to TextureMaterial 15 | getAverageHsl(id: number): number; 16 | 17 | // TODO: remove 18 | getAnimationUv(id: number): [number, number]; 19 | // getMoveU(id: number): number; 20 | // getMoveV(id: number): number; 21 | 22 | getMaterial(id: number): TextureMaterial; 23 | 24 | getPixelsRgb(id: number, size: number, flipH: boolean, brightness: number): Int32Array; 25 | getPixelsArgb(id: number, size: number, flipH: boolean, brightness: number): Int32Array; 26 | } 27 | -------------------------------------------------------------------------------- /src/rs/compression/Gzip.web.ts: -------------------------------------------------------------------------------- 1 | import pako from "pako"; 2 | import init, { decompressGzip } from "wasm-gzip"; 3 | 4 | const wasmGzipUrl = require("wasm-gzip/wasm_gzip_bg.wasm"); 5 | 6 | export class Gzip { 7 | static wasmLoaded = false; 8 | 9 | static async initWasm(): Promise { 10 | await init(wasmGzipUrl); 11 | Gzip.wasmLoaded = true; 12 | } 13 | 14 | static decompress(compressed: Uint8Array): Int8Array { 15 | if (!Gzip.wasmLoaded) { 16 | const decompressed = new Int8Array(pako.ungzip(compressed).buffer); 17 | return decompressed; 18 | } 19 | const decompressed = decompressGzip(compressed); 20 | if (!decompressed) { 21 | throw new Error("Failed to decompress gzip"); 22 | } 23 | return new Int8Array(decompressed.buffer); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/renderer/RendererCanvas.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | import { Renderer } from "./Renderer"; 4 | 5 | export interface RendererCanvasProps { 6 | renderer: Renderer; 7 | } 8 | 9 | export function RendererCanvas({ renderer }: RendererCanvasProps): JSX.Element { 10 | const divRef = useRef(null); 11 | 12 | useEffect(() => { 13 | if (!divRef.current) { 14 | return; 15 | } 16 | divRef.current.appendChild(renderer.canvas); 17 | 18 | renderer.init().then(() => { 19 | renderer.start(); 20 | }); 21 | 22 | return () => { 23 | renderer.stop(); 24 | divRef.current?.removeChild(renderer.canvas); 25 | }; 26 | }, [renderer]); 27 | 28 | return
; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/rs/worldmap/WorldMapModal.tsx: -------------------------------------------------------------------------------- 1 | import Modal from "react-modal"; 2 | 3 | import { WorldMap, WorldMapProps } from "./WorldMap"; 4 | import "./WorldMapModal.css"; 5 | 6 | interface WorldMapModalProps { 7 | isOpen: boolean; 8 | 9 | onRequestClose: () => void; 10 | } 11 | 12 | type Props = WorldMapModalProps & WorldMapProps; 13 | 14 | Modal.setAppElement("#root"); 15 | 16 | export function WorldMapModal(props: Props) { 17 | const { isOpen, onRequestClose } = props; 18 | return ( 19 | 25 |
26 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/rs/model/skeletal/QuatPool.ts: -------------------------------------------------------------------------------- 1 | import { quat } from "gl-matrix"; 2 | 3 | export class QuatPool { 4 | static quatIndex: number; 5 | static quatLimit: number; 6 | static quatPool: quat[]; 7 | 8 | static init(size: number): void { 9 | QuatPool.quatIndex = 0; 10 | QuatPool.quatLimit = size; 11 | QuatPool.quatPool = new Array(size); 12 | } 13 | 14 | static get(): quat { 15 | if (QuatPool.quatIndex === 0) { 16 | return quat.create(); 17 | } else { 18 | quat.identity(QuatPool.quatPool[--QuatPool.quatIndex]); 19 | return QuatPool.quatPool[QuatPool.quatIndex]; 20 | } 21 | } 22 | 23 | static release(q: quat): void { 24 | if (QuatPool.quatIndex < QuatPool.quatLimit - 1) { 25 | QuatPool.quatPool[QuatPool.quatIndex++] = q; 26 | } 27 | } 28 | } 29 | QuatPool.init(100); 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "esm": true, 4 | "files": true, 5 | "compilerOptions": { 6 | "module": "nodenext", 7 | } 8 | }, 9 | "compilerOptions": { 10 | "target": "ES2020", 11 | "lib": [ 12 | "dom", 13 | "dom.iterable", 14 | "esnext", 15 | "ES2020" 16 | ], 17 | "types": ["@webgpu/types"], 18 | "allowJs": true, 19 | "skipLibCheck": true, 20 | "esModuleInterop": true, 21 | "allowSyntheticDefaultImports": true, 22 | "strict": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "module": "esnext", 26 | "moduleResolution": "node", 27 | "resolveJsonModule": true, 28 | "isolatedModules": true, 29 | "noEmit": true, 30 | "jsx": "react-jsx" 31 | }, 32 | "include": [ 33 | "src", "scripts" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/rs/loading/OsrsLoadingBar.css: -------------------------------------------------------------------------------- 1 | .loading-bar { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | position: relative; 6 | width: 305px; 7 | height: 35px; 8 | font-family: ui-monospace, SFMono-Regular, Menlo, "Roboto Mono", monospace; 9 | /* font-family: Helvetica; */ 10 | font-size: 13px; 11 | font-weight: bold; 12 | color: white; 13 | background-color: black; 14 | box-sizing: border-box; 15 | border: solid #8c1111 1px; 16 | } 17 | 18 | .loading-bar-text { 19 | display: flex; 20 | z-index: 1; 21 | } 22 | 23 | .loading-bar-progress-container { 24 | position: absolute; 25 | width: 100%; 26 | height: 100%; 27 | } 28 | 29 | .loading-bar-progress { 30 | border: solid black 1px; 31 | box-sizing: border-box; 32 | background-color: #8c1111; 33 | /* width: 75%; */ 34 | height: 100%; 35 | } 36 | -------------------------------------------------------------------------------- /src/rs/config/loctype/LocModelType.ts: -------------------------------------------------------------------------------- 1 | export enum LocModelType { 2 | WALL = 0, 3 | WALL_TRI_CORNER = 1, 4 | WALL_CORNER = 2, 5 | WALL_RECT_CORNER = 3, 6 | 7 | WALL_DECORATION_INSIDE = 4, 8 | WALL_DECORATION_OUTSIDE = 5, 9 | WALL_DECORATION_DIAGONAL_OUTSIDE = 6, 10 | WALL_DECORATION_DIAGONAL_INSIDE = 7, 11 | WALL_DECORATION_DIAGONAL_DOUBLE = 8, 12 | 13 | WALL_DIAGONAL = 9, 14 | 15 | NORMAL = 10, 16 | NORMAL_DIAGIONAL = 11, 17 | 18 | ROOF_SLOPED = 12, 19 | ROOF_SLOPED_OUTER_CORNER = 13, 20 | ROOF_SLOPED_INNER_CORNER = 14, 21 | ROOF_SLOPED_HARD_INNER_CORNER = 15, 22 | ROOF_SLOPED_HARD_OUTER_CORNER = 16, 23 | ROOF_FLAT = 17, 24 | ROOF_SLOPED_OVERHANG = 18, 25 | ROOF_SLOPED_OVERHANG_OUTER_CORNER = 19, 26 | ROOF_SLOPED_OVERHANG_INNER_CORNER = 20, 27 | ROOF_SLOPED_OVERHANG_HARD_OUTER_CORNER = 21, 28 | 29 | FLOOR_DECORATION = 22, 30 | } 31 | -------------------------------------------------------------------------------- /src/rs/util/StringUtil.ts: -------------------------------------------------------------------------------- 1 | export class StringUtil { 2 | // An implementation of Dan Bernstein's {@code djb2} hash function which is 3 | // slightly modified. Instead of the initial hash being 5381, it is zero. 4 | static hashDjb2(str: string) { 5 | let hash = 0; 6 | if (str.length === 0) { 7 | return hash; 8 | } 9 | let char: number; 10 | for (let i = 0; i < str.length; i++) { 11 | char = str.charCodeAt(i); 12 | hash = (hash << 5) - hash + char; 13 | hash = hash & hash; // Convert to 32bit integer 14 | } 15 | return hash; 16 | } 17 | 18 | static hashOld(name: string) { 19 | name = name.toUpperCase(); 20 | let hash = 0; 21 | for (let i = 0; i < name.length; i++) { 22 | hash = (hash * 61 + name.charCodeAt(i) - 32) | 0; 23 | } 24 | return hash; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/branchless-logic.glsl: -------------------------------------------------------------------------------- 1 | // https://theorangeduck.com/page/avoiding-shader-conditionals 2 | 3 | // == 4 | float when_eq(float x, float y) { 5 | return 1.0 - abs(sign(x - y)); 6 | } 7 | 8 | // != 9 | float when_neq(float x, float y) { 10 | return abs(sign(x - y)); 11 | } 12 | 13 | // > 14 | float when_gt(float x, float y) { 15 | return max(sign(x - y), 0.0); 16 | } 17 | 18 | // < 19 | float when_lt(float x, float y) { 20 | return max(sign(y - x), 0.0); 21 | } 22 | 23 | // >= 24 | float when_ge(float x, float y) { 25 | return 1.0 - when_lt(x, y); 26 | } 27 | 28 | // <= 29 | float when_le(float x, float y) { 30 | return 1.0 - when_gt(x, y); 31 | } 32 | 33 | // && 34 | float and(float a, float b) { 35 | return a * b; 36 | } 37 | 38 | // || 39 | float or(float a, float b) { 40 | return min(a + b, 1.0); 41 | } 42 | 43 | // ^ 44 | float xor(float a, float b) { 45 | return mod(a + b, 2.0); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/renderer/RenderStats.ts: -------------------------------------------------------------------------------- 1 | export class RenderStats { 2 | frameCount: number = 0; 3 | 4 | frameTime: number = 0; 5 | frameTimeFps: number = 0; 6 | 7 | lastFrameTime: DOMHighResTimeStamp | undefined; 8 | 9 | frameTimeStart: number = 0; 10 | frameTimeJs: number = 0; 11 | 12 | getDeltaTime(time: DOMHighResTimeStamp): number { 13 | return time - (this.lastFrameTime ?? time); 14 | } 15 | 16 | update(time: DOMHighResTimeStamp) { 17 | this.frameTime = this.getDeltaTime(time); 18 | this.lastFrameTime = time; 19 | this.frameTimeStart = performance.now(); 20 | if (this.frameTime !== 0) { 21 | this.frameTimeFps = 1000 / this.frameTime; 22 | } 23 | } 24 | 25 | onFrameEnd() { 26 | this.frameCount++; 27 | if (this.lastFrameTime !== undefined) { 28 | this.frameTimeJs = performance.now() - this.frameTimeStart; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/util/FloatUtil.ts: -------------------------------------------------------------------------------- 1 | export class FloatUtil { 2 | static MAX_VALUE = 3.4028234663852886e38; 3 | 4 | static float: Float32Array = new Float32Array(1); 5 | static integer: Int32Array = new Int32Array(FloatUtil.float.buffer); 6 | 7 | static floatBitsToInt(n: number): number { 8 | FloatUtil.float[0] = n; 9 | return FloatUtil.integer[0]; 10 | } 11 | 12 | static intBitsToFloat(n: number): number { 13 | FloatUtil.integer[0] = n; 14 | return FloatUtil.float[0]; 15 | } 16 | 17 | static packFloat11(v: number): number { 18 | return 1024 - Math.round(v / (1 / 64)); 19 | } 20 | 21 | static unpackFloat11(v: number): number { 22 | return 16 - v / 64; 23 | } 24 | 25 | // 0-1, 1/63 decimal precision 26 | static packFloat6(v: number): number { 27 | return Math.round(v / (1 / 63)); 28 | } 29 | 30 | static unpackFloat6(v: number): number { 31 | return v / 63; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/rs/model/skeletal/MatrixPool.ts: -------------------------------------------------------------------------------- 1 | import { mat4 } from "gl-matrix"; 2 | 3 | export class MatrixPool { 4 | static matrixIndex: number; 5 | static matrixLimit: number; 6 | static matrixPool: mat4[]; 7 | 8 | static IDENTITY: mat4 = mat4.create(); 9 | 10 | static init(size: number): void { 11 | MatrixPool.matrixIndex = 0; 12 | MatrixPool.matrixLimit = size; 13 | MatrixPool.matrixPool = new Array(size); 14 | } 15 | 16 | static get(): mat4 { 17 | if (MatrixPool.matrixIndex === 0) { 18 | return mat4.create(); 19 | } else { 20 | mat4.identity(MatrixPool.matrixPool[--MatrixPool.matrixIndex]); 21 | return MatrixPool.matrixPool[MatrixPool.matrixIndex]; 22 | } 23 | } 24 | 25 | static release(m: mat4): void { 26 | if (MatrixPool.matrixIndex < MatrixPool.matrixLimit - 1) { 27 | MatrixPool.matrixPool[MatrixPool.matrixIndex++] = m; 28 | } 29 | } 30 | } 31 | MatrixPool.init(100); 32 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/main.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | layout(std140, column_major) uniform; 6 | 7 | #include "./includes/scene-uniforms.glsl"; 8 | 9 | uniform highp sampler2DArray u_textures; 10 | 11 | in vec4 v_color; 12 | in vec2 v_texCoord; 13 | flat in uint v_texId; 14 | flat in float v_alphaCutOff; 15 | in float v_fogAmount; 16 | flat in vec4 v_interactId; 17 | 18 | layout(location = 0) out vec4 fragColor; 19 | layout(location = 1) out vec4 interactId; 20 | 21 | void main() { 22 | vec4 textureColor = texture(u_textures, vec3(v_texCoord, v_texId)).bgra; 23 | fragColor = pow(textureColor, vec4(vec3(u_brightness), 1.0)) * 24 | vec4(round(v_color.rgb * u_colorBanding) / u_colorBanding, v_color.a); 25 | #ifdef DISCARD_ALPHA 26 | if ((v_texId == 0u && fragColor.a < 0.01) || (textureColor.a < v_alphaCutOff)) { 27 | discard; 28 | } 29 | #endif 30 | fragColor = mix(fragColor, u_skyColor, v_fogAmount); 31 | interactId = v_interactId; 32 | } 33 | -------------------------------------------------------------------------------- /src/rs/cache/ConfigType.ts: -------------------------------------------------------------------------------- 1 | export class ConfigType { 2 | static readonly DAT = { 3 | title: 1, 4 | configs: 2, 5 | interfaces: 3, 6 | media: 4, 7 | versionList: 5, 8 | textures: 6, 9 | }; 10 | 11 | static readonly DAT2 = { 12 | underlays: 1, 13 | identkits: 3, 14 | overlays: 4, 15 | inv: 5, 16 | locs: 6, 17 | enums: 8, 18 | npcs: 9, 19 | objs: 10, 20 | params: 11, 21 | seqs: 12, 22 | spotAnims: 13, 23 | varbits: 14, 24 | varps: 16, 25 | varClient: 19, 26 | varClientString: 15, 27 | varPlayer: 16, 28 | }; 29 | 30 | static readonly OSRS = { 31 | hitSplat: 32, 32 | healthBar: 33, 33 | struct: 34, 34 | mapFunctions: 35, 35 | dbRow: 38, 36 | dbTable: 39, 37 | }; 38 | 39 | static readonly RS2 = { 40 | bas: 32, 41 | mapScenes: 34, 42 | quests: 35, 43 | mapFunctions: 36, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/rs/worldmap/WorldMapModal.css: -------------------------------------------------------------------------------- 1 | .worldmap-modal-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | background-color: rgba(0, 0, 0, 0.3); 8 | z-index: 1337; /* above controls */ 9 | } 10 | 11 | .worldmap-modal { 12 | position: absolute; 13 | inset: 20px; 14 | background-color: black; 15 | overflow: auto; 16 | -webkit-overflow-scrolling: "touch"; 17 | outline: none; 18 | } 19 | 20 | @media screen and (min-width: 480px) and (min-height: 300px) { 21 | .worldmap-modal { 22 | inset: 80px; 23 | } 24 | } 25 | 26 | @media screen and (max-height: 300px) { 27 | .worldmap-modal { 28 | inset: 20px; 29 | } 30 | } 31 | 32 | .worldmap-close-button { 33 | position: absolute; 34 | right: 0; 35 | background-image: url(./close-button.png); 36 | width: 24px; 37 | height: 23px; 38 | margin: 6px; 39 | z-index: 10; 40 | } 41 | 42 | .worldmap-close-button:hover { 43 | background-image: url(./close-button-hover.png); 44 | } 45 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/height-map.glsl: -------------------------------------------------------------------------------- 1 | const int sceneBorderSize = 6; 2 | const int tileSize = 128; 3 | const int tileSizeShift = 7; 4 | 5 | int getTileHeight(int x, int z, uint plane) { 6 | return texelFetch(u_heightMap, ivec3(sceneBorderSize + x, sceneBorderSize + z, plane), 0).r * 8; 7 | } 8 | 9 | float getHeightInterp(vec2 pos, uint plane) { 10 | ivec2 ipos = ivec2(pos); 11 | int tileX = ipos.x >> tileSizeShift; 12 | int tileZ = ipos.y >> tileSizeShift; 13 | int offsetX = ipos.x & (tileSize - 1); 14 | int offsetZ = ipos.y & (tileSize - 1); 15 | int h00 = getTileHeight(tileX, tileZ, plane); 16 | int h10 = getTileHeight(tileX + 1, tileZ, plane); 17 | int h01 = getTileHeight(tileX, tileZ + 1, plane); 18 | int h11 = getTileHeight(tileX + 1, tileZ + 1, plane); 19 | int delta0 = (h00 * (tileSize - offsetX) + h10 * offsetX) >> tileSizeShift; 20 | int delta1 = (h01 * (tileSize - offsetX) + h11 * offsetX) >> tileSizeShift; 21 | return float((delta0 * (tileSize - offsetZ) + delta1 * offsetZ) >> tileSizeShift); 22 | } 23 | -------------------------------------------------------------------------------- /src/rs/config/seqtype/SeqTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheIndex } from "../../cache/CacheIndex"; 3 | import { CacheInfo } from "../../cache/CacheInfo"; 4 | import { ArchiveTypeLoader, DatTypeLoader, IndexTypeLoader, TypeLoader } from "../TypeLoader"; 5 | import { SeqType } from "./SeqType"; 6 | 7 | export type SeqTypeLoader = TypeLoader; 8 | 9 | export class DatSeqTypeLoader { 10 | static load(cacheInfo: CacheInfo, configArchive: Archive): SeqTypeLoader { 11 | return DatTypeLoader.load(SeqType, cacheInfo, configArchive, "seq"); 12 | } 13 | } 14 | 15 | export class ArchiveSeqTypeLoader extends ArchiveTypeLoader implements SeqTypeLoader { 16 | constructor(cacheInfo: CacheInfo, archive: Archive) { 17 | super(SeqType, cacheInfo, archive); 18 | } 19 | } 20 | 21 | export class IndexSeqTypeLoader extends IndexTypeLoader implements SeqTypeLoader { 22 | constructor(cacheInfo: CacheInfo, index: CacheIndex) { 23 | super(SeqType, cacheInfo, index, 7); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/rs/compression/Bzip2.ts: -------------------------------------------------------------------------------- 1 | import WasmBzip2 from "@foxglove/wasm-bz2"; 2 | 3 | const bzip2 = require("bzip2"); 4 | 5 | export class Bzip2 { 6 | static bzip2Header = new Uint8Array("BZh1".split("").map((char) => char.charCodeAt(0))); 7 | 8 | static wasmBzip: WasmBzip2; 9 | 10 | static async initWasm(): Promise { 11 | const bzip = await WasmBzip2.init(); 12 | Bzip2.wasmBzip = bzip; 13 | return bzip; 14 | } 15 | 16 | static decompress(compressed: Uint8Array, actualSize: number): Int8Array { 17 | const compressedBzip = new Uint8Array(compressed.length + 4); 18 | compressedBzip.set(Bzip2.bzip2Header, 0); 19 | compressedBzip.set(compressed, 4); 20 | 21 | if (Bzip2.wasmBzip) { 22 | const decompressed = Bzip2.wasmBzip.decompress(compressedBzip, actualSize, { 23 | small: false, 24 | }); 25 | return new Int8Array(decompressed.buffer); 26 | } 27 | 28 | return new Int8Array(bzip2.simple(bzip2.array(compressedBzip)).buffer); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/rs/model/seq/SeqBaseLoader.ts: -------------------------------------------------------------------------------- 1 | import { CacheIndex } from "../../cache/CacheIndex"; 2 | import { CacheInfo } from "../../cache/CacheInfo"; 3 | import { Dat2SeqBase, SeqBase } from "./SeqBase"; 4 | 5 | export interface SeqBaseLoader { 6 | load(id: number): SeqBase | undefined; 7 | 8 | clearCache(): void; 9 | } 10 | 11 | export class IndexSeqBaseLoader implements SeqBaseLoader { 12 | bases: Map = new Map(); 13 | 14 | constructor( 15 | readonly cacheInfo: CacheInfo, 16 | readonly index: CacheIndex, 17 | ) {} 18 | 19 | load(id: number): SeqBase | undefined { 20 | const cached = this.bases.get(id); 21 | if (cached) { 22 | return cached; 23 | } 24 | 25 | const file = this.index.getFile(id, 0); 26 | if (!file) { 27 | return undefined; 28 | } 29 | const base = Dat2SeqBase.load(this.cacheInfo, id, file.data); 30 | this.bases.set(id, base); 31 | return base; 32 | } 33 | 34 | clearCache(): void { 35 | this.bases.clear(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/GrayScaleOperation.ts: -------------------------------------------------------------------------------- 1 | import { TextureGenerator } from "../TextureGenerator"; 2 | import { TextureOperation } from "./TextureOperation"; 3 | 4 | export class GrayScaleOperation extends TextureOperation { 5 | constructor() { 6 | super(1, true); 7 | } 8 | 9 | override getMonochromeOutput(textureGenerator: TextureGenerator, line: number): Int32Array { 10 | if (!this.monochromeImageCache) { 11 | throw new Error("Monochrome image cache is not initialized"); 12 | } 13 | const output = this.monochromeImageCache.get(line); 14 | if (this.monochromeImageCache.dirty) { 15 | const input = this.getColourInput(textureGenerator, 0, line); 16 | const inputR = input[0]; 17 | const inputG = input[1]; 18 | const inputB = input[2]; 19 | for (let pixel = 0; pixel < textureGenerator.width; pixel++) { 20 | output[pixel] = (inputR[pixel] + inputG[pixel] + inputB[pixel]) / 3; 21 | } 22 | } 23 | return output; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/npc/NpcData.ts: -------------------------------------------------------------------------------- 1 | import { NpcSpawn } from "../../data/npc/NpcSpawn"; 2 | import { AnimationFrames } from "../AnimationFrames"; 3 | import { NpcSpawnGroup } from "./NpcSpawnGroup"; 4 | 5 | export type NpcData = { 6 | id: number; 7 | tileX: number; 8 | tileY: number; 9 | level: number; 10 | idleAnim: AnimationFrames; 11 | walkAnim: AnimationFrames | undefined; 12 | }; 13 | 14 | export function createNpcDatas(groups: NpcSpawnGroup[]): NpcData[] { 15 | const npcs: NpcData[] = []; 16 | 17 | for (const group of groups) { 18 | for (const spawn of group.spawns) { 19 | npcs.push(createNpcData(group, spawn)); 20 | } 21 | } 22 | 23 | return npcs; 24 | } 25 | 26 | export function createNpcData(group: NpcSpawnGroup, spawn: NpcSpawn): NpcData { 27 | const tileX = spawn.x % 64; 28 | const tileY = spawn.y % 64; 29 | const level = spawn.level; 30 | return { 31 | id: spawn.id, 32 | tileX, 33 | tileY, 34 | level, 35 | idleAnim: group.idleAnim, 36 | walkAnim: group.walkAnim, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/rs/scene/entity/EntityTag.ts: -------------------------------------------------------------------------------- 1 | export enum EntityType { 2 | PLAYER = 0, 3 | NPC = 1, 4 | LOC = 2, 5 | OBJ = 3, 6 | } 7 | 8 | export type EntityTag = bigint; 9 | 10 | export function calculateEntityTag( 11 | tileX: number, 12 | tileY: number, 13 | entityType: EntityType, 14 | notInteractive: boolean, 15 | id: number, 16 | ): EntityTag { 17 | let tag = 18 | BigInt(tileX & 0x7f) | 19 | (BigInt(tileY & 0x7f) << 7n) | 20 | (BigInt(entityType & 0x3) << 14n) | 21 | (BigInt(id) << 17n); 22 | if (notInteractive) { 23 | tag |= 0x10000n; 24 | } 25 | return tag; 26 | } 27 | 28 | export function isEntityInteractive(tag: EntityTag): boolean { 29 | let interactive = tag !== 0n; 30 | if (interactive) { 31 | interactive = (Number(tag >> 16n) & 0x1) === 0; 32 | } 33 | return interactive; 34 | } 35 | 36 | export function getIdFromTag(tag: EntityTag): number { 37 | return Number(tag >> 17n); 38 | } 39 | 40 | export function getEntityTypeFromTag(tag: EntityTag): EntityType { 41 | return Number(tag >> 14n) & 0x3; 42 | } 43 | -------------------------------------------------------------------------------- /src/rs/config/loctype/LocTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheIndex } from "../../cache/CacheIndex"; 3 | import { CacheInfo } from "../../cache/CacheInfo"; 4 | import { 5 | ArchiveTypeLoader, 6 | IndexTypeLoader, 7 | IndexedDatTypeLoader, 8 | TypeLoader, 9 | } from "../TypeLoader"; 10 | import { LocType } from "./LocType"; 11 | 12 | export type LocTypeLoader = TypeLoader; 13 | 14 | export class DatLocTypeLoader { 15 | static load(cacheInfo: CacheInfo, configArchive: Archive): LocTypeLoader { 16 | return IndexedDatTypeLoader.load(LocType, cacheInfo, configArchive, "loc"); 17 | } 18 | } 19 | 20 | export class ArchiveLocTypeLoader extends ArchiveTypeLoader implements LocTypeLoader { 21 | constructor(cacheInfo: CacheInfo, archive: Archive) { 22 | super(LocType, cacheInfo, archive); 23 | } 24 | } 25 | 26 | export class IndexLocTypeLoader extends IndexTypeLoader implements LocTypeLoader { 27 | constructor(cacheInfo: CacheInfo, index: CacheIndex) { 28 | super(LocType, cacheInfo, index); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/rs/config/npctype/NpcTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheIndex } from "../../cache/CacheIndex"; 3 | import { CacheInfo } from "../../cache/CacheInfo"; 4 | import { 5 | ArchiveTypeLoader, 6 | IndexTypeLoader, 7 | IndexedDatTypeLoader, 8 | TypeLoader, 9 | } from "../TypeLoader"; 10 | import { NpcType } from "./NpcType"; 11 | 12 | export type NpcTypeLoader = TypeLoader; 13 | 14 | export class DatNpcTypeLoader { 15 | static load(cacheInfo: CacheInfo, configArchive: Archive): NpcTypeLoader { 16 | return IndexedDatTypeLoader.load(NpcType, cacheInfo, configArchive, "npc"); 17 | } 18 | } 19 | 20 | export class ArchiveNpcTypeLoader extends ArchiveTypeLoader implements NpcTypeLoader { 21 | constructor(cacheInfo: CacheInfo, archive: Archive) { 22 | super(NpcType, cacheInfo, archive); 23 | } 24 | } 25 | 26 | export class IndexNpcTypeLoader extends IndexTypeLoader implements NpcTypeLoader { 27 | constructor(cacheInfo: CacheInfo, index: CacheIndex) { 28 | super(NpcType, cacheInfo, index, 7); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/rs/config/objtype/ObjTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheIndex } from "../../cache/CacheIndex"; 3 | import { CacheInfo } from "../../cache/CacheInfo"; 4 | import { 5 | ArchiveTypeLoader, 6 | IndexTypeLoader, 7 | IndexedDatTypeLoader, 8 | TypeLoader, 9 | } from "../TypeLoader"; 10 | import { ObjType } from "./ObjType"; 11 | 12 | export type ObjTypeLoader = TypeLoader; 13 | 14 | export class DatObjTypeLoader { 15 | static load(cacheInfo: CacheInfo, configArchive: Archive): ObjTypeLoader { 16 | return IndexedDatTypeLoader.load(ObjType, cacheInfo, configArchive, "obj"); 17 | } 18 | } 19 | 20 | export class ArchiveObjTypeLoader extends ArchiveTypeLoader implements ObjTypeLoader { 21 | constructor(cacheInfo: CacheInfo, archive: Archive) { 22 | super(ObjType, cacheInfo, archive); 23 | } 24 | } 25 | 26 | export class IndexObjTypeLoader extends IndexTypeLoader implements ObjTypeLoader { 27 | constructor(cacheInfo: CacheInfo, index: CacheIndex) { 28 | super(ObjType, cacheInfo, index); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/mapviewer/webgpu/shaders/fullscreenTexturedQuad.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) var mySampler : sampler; 2 | @group(0) @binding(1) var myTexture : texture_2d_array; 3 | 4 | struct VertexOutput { 5 | @builtin(position) Position : vec4, 6 | @location(0) fragUV : vec2, 7 | } 8 | 9 | @vertex 10 | fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { 11 | const pos = array( 12 | vec2( 1.0, 1.0), 13 | vec2( 1.0, -1.0), 14 | vec2(-1.0, -1.0), 15 | vec2( 1.0, 1.0), 16 | vec2(-1.0, -1.0), 17 | vec2(-1.0, 1.0), 18 | ); 19 | 20 | const uv = array( 21 | vec2(1.0, 0.0), 22 | vec2(1.0, 1.0), 23 | vec2(0.0, 1.0), 24 | vec2(1.0, 0.0), 25 | vec2(0.0, 1.0), 26 | vec2(0.0, 0.0), 27 | ); 28 | 29 | var output : VertexOutput; 30 | output.Position = vec4(pos[VertexIndex], 0.0, 1.0); 31 | output.fragUV = uv[VertexIndex]; 32 | return output; 33 | } 34 | 35 | @fragment 36 | fn frag_main(@location(0) fragUV : vec2) -> @location(0) vec4 { 37 | return textureSample(myTexture, mySampler, fragUV, 2).bgra; 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RuneScape Map Viewer 2 | 3 | [Website](https://osrs.world) | [Discord](https://discord.gg/WfEWPE5wUd) 4 | 5 | A webapp for exploring current and historical versions of RuneScape. 6 | 7 | Lumbridge 8 | 9 | ## Running locally 10 | 11 | ``` 12 | $ git clone https://github.com/dennisdev/rs-map-viewer.git 13 | $ cd rs-map-viewer 14 | $ yarn install 15 | $ yarn run download-caches 16 | $ yarn start 17 | ``` 18 | 19 | ## Credits 20 | 21 | - Jagex 22 | - [RuneLite](https://github.com/runelite/runelite) 23 | - [OpenRS2 Archive](https://archive.openrs2.org/) - Caches 24 | - [RuneScape Archive](https://rs-archive.github.io/) - Caches 25 | - [OSRS Wiki](https://oldschool.runescape.wiki/) - Item spawns 26 | - [2004scape](https://github.com/2004scape/Server) - Npc spawns 27 | - [2009scape](https://gitlab.com/2009scape/2009scape) - Npc spawns 28 | - [RuneStar](https://github.com/RuneStar/fonts) - Fonts 29 | - [Blurite](https://github.com/blurite/pathfinder) - Some pathfinding stuff 30 | - [RuneApps Model Viewer](https://github.com/skillbert/rsmv) - Some procedural texture stuff 31 | -------------------------------------------------------------------------------- /src/components/rs/minimap/MinimapContainer.css: -------------------------------------------------------------------------------- 1 | .minimap-container { 2 | position: relative; 3 | width: 181px; 4 | height: 165px; 5 | overflow: hidden; 6 | } 7 | 8 | .minimap-container .compass { 9 | cursor: pointer; 10 | clip-path: circle(18px); 11 | position: absolute; 12 | left: -4px; 13 | top: -4px; 14 | } 15 | 16 | .minimap-container .worldmap-icon { 17 | cursor: pointer; 18 | position: absolute; 19 | left: 144px; 20 | top: 128px; 21 | width: 32px; 22 | height: 32px; 23 | background-image: url(./worldmap-icon.png); 24 | background-size: 32px 32px; 25 | } 26 | 27 | .minimap-container .worldmap-icon:hover { 28 | background-image: url(./worldmap-icon-hover.png); 29 | } 30 | 31 | .minimap-container .minimap { 32 | clip-path: circle(77px at 384px 384px); 33 | position: absolute; 34 | left: -285px; 35 | top: -301px; 36 | transform-origin: 384px 384px; 37 | background-color: black; 38 | } 39 | 40 | .minimap-images { 41 | position: relative; 42 | } 43 | 44 | .minimap-image { 45 | position: absolute; 46 | padding: 0; 47 | margin: 0; 48 | } 49 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/ConstantMonochromeOperation.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { TextureGenerator } from "../TextureGenerator"; 3 | import { TextureOperation } from "./TextureOperation"; 4 | 5 | export class ConstantMonochromeOperation extends TextureOperation { 6 | constant: number; 7 | 8 | constructor(constant: number = 4096) { 9 | super(0, true); 10 | this.constant = constant; 11 | } 12 | 13 | override decode(field: number, buffer: ByteBuffer): void { 14 | if (field === 0) { 15 | this.constant = ((buffer.readUnsignedByte() << 12) / 255) | 0; 16 | } 17 | } 18 | 19 | override getMonochromeOutput(textureGenerator: TextureGenerator, line: number): Int32Array { 20 | if (!this.monochromeImageCache) { 21 | throw new Error("Monochrome image cache not initialized"); 22 | } 23 | const output = this.monochromeImageCache.get(line); 24 | if (this.monochromeImageCache.dirty) { 25 | output.fill(this.constant, 0, textureGenerator.width); 26 | } 27 | return output; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | 5 | import "./index.css"; 6 | import MapViewerApp from "./mapviewer/MapViewerApp"; 7 | import reportWebVitals from "./reportWebVitals"; 8 | import { Bzip2 } from "./rs/compression/Bzip2"; 9 | import { Gzip } from "./rs/compression/Gzip"; 10 | 11 | Bzip2.initWasm(); 12 | Gzip.initWasm(); 13 | 14 | window.wallpaperPropertyListener = { 15 | applyGeneralProperties: (properties: any) => { 16 | if (properties.fps) { 17 | window.wallpaperFpsLimit = properties.fps; 18 | } 19 | }, 20 | }; 21 | 22 | const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); 23 | root.render( 24 | // 25 | 26 | 27 | , 28 | // , 29 | ); 30 | 31 | // If you want to start measuring performance in your app, pass a function 32 | // to log results (for example: reportWebVitals(console.log)) 33 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 34 | reportWebVitals(); 35 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/vertex.glsl: -------------------------------------------------------------------------------- 1 | struct Vertex { 2 | vec3 pos; 3 | vec4 color; 4 | vec2 texCoord; 5 | uint textureId; 6 | uint priority; 7 | }; 8 | 9 | Vertex decodeVertex(uint v0, uint v1, uint v2, float brightness) { 10 | float x = float(int((v0 >> 17u) & 0x7FFFu) - 0x4000); 11 | float u = unpackFloat11(((v0 >> 11u) & 0x3Fu) | ((v2 & 0x1Fu) << 6u)); 12 | float v = unpackFloat11(v0 & 0x7FFu); 13 | 14 | float y = -float(int((v1) & 0x7FFFu) - 0x4000); 15 | int hsl = int((v1 >> 15u) & 0xFFFFu); 16 | float isTextured = float((v1 >> 31) & 0x1u); 17 | float textureId = float(((hsl >> 7) | int(((v2 >> 5u) & 0x1u) << 9u)) + 1) * isTextured; 18 | 19 | float z = float(int((v2 >> 17u) & 0x7FFFu) - 0x4000); 20 | float alpha = float((v2 >> 9u) & 0xFFu) / 255.0; 21 | uint priority = ((v2 >> 6u) & 0x7u); 22 | 23 | vec4 color = when_eq(textureId, 0.0) * vec4(hslToRgb(hsl, brightness), alpha) 24 | + when_neq(textureId, 0.0) * vec4(vec3(float(hsl & 0x7F) / 127.0), alpha); 25 | 26 | return Vertex(vec3(x, y, z), color, vec2(u, v), uint(textureId), priority); 27 | } 28 | -------------------------------------------------------------------------------- /src/rs/cache/CacheInfo.ts: -------------------------------------------------------------------------------- 1 | export type GameType = "classic" | "runescape" | "oldschool"; 2 | 3 | export type CacheInfo = { 4 | name: string; 5 | game: GameType; 6 | environment: string; 7 | revision: number; 8 | timestamp: string; 9 | size: number; 10 | }; 11 | 12 | export function sortCachesNewToOld(caches: CacheInfo[]): void { 13 | caches.sort((a, b) => { 14 | const isOsrsA = a.game === "oldschool"; 15 | const isOsrsB = b.game === "oldschool"; 16 | const isLiveA = a.environment === "live"; 17 | const isLiveB = b.environment === "live"; 18 | const dateA = Date.parse(a.timestamp); 19 | const dateB = Date.parse(b.timestamp); 20 | return ( 21 | (isOsrsB ? 1 : 0) - (isOsrsA ? 1 : 0) || 22 | (isLiveB ? 1 : 0) - (isLiveA ? 1 : 0) || 23 | b.revision - a.revision || 24 | dateB - dateA 25 | ); 26 | }); 27 | } 28 | 29 | export function getLatestCache(caches: CacheInfo[]): CacheInfo | undefined { 30 | if (caches.length === 0) { 31 | return undefined; 32 | } 33 | 34 | sortCachesNewToOld(caches); 35 | 36 | return caches[0]; 37 | } 38 | -------------------------------------------------------------------------------- /src/mapviewer/data/npc/npc-spawns-2004.json.LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 2004scape 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/TilingSpriteOperation.ts: -------------------------------------------------------------------------------- 1 | import { TextureGenerator } from "../TextureGenerator"; 2 | import { SpriteSourceOperation } from "./SpriteSourceOperation"; 3 | 4 | export class TilingSpriteOperation extends SpriteSourceOperation { 5 | override getColourOutput(textureGenerator: TextureGenerator, line: number): Int32Array[] { 6 | if (!this.colourImageCache) { 7 | throw new Error("Colour image cache is not initialized"); 8 | } 9 | const output = this.colourImageCache.get(line); 10 | if (this.colourImageCache.dirty && super.loadSprite(textureGenerator) && this.pixels) { 11 | const outputR = output[0]; 12 | const outputG = output[1]; 13 | const outputB = output[2]; 14 | const startY = this.height * (line % this.height); 15 | for (let x = 0; x < textureGenerator.width; x++) { 16 | const rgb = this.pixels[startY + (x % this.width)]; 17 | outputR[x] = (rgb >> 12) & 0xff0; 18 | outputG[x] = (rgb >> 4) & 0xff0; 19 | outputB[x] = (rgb & 0xff) << 4; 20 | } 21 | } 22 | return output; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/rs/MathConstants.ts: -------------------------------------------------------------------------------- 1 | export const TAU = Math.PI * 2; 2 | export const RS_TO_RADIANS = TAU / 2048.0; 3 | export const RS_TO_DEGREES = (RS_TO_RADIANS * 180) / Math.PI; 4 | export const DEGREES_TO_RADIANS = Math.PI / 180; 5 | 6 | function initBitMasks(): Int32Array { 7 | const masks = new Int32Array(32); 8 | 9 | for (let i = 0; i < 32; i++) { 10 | masks[i] = (2 << i) - 1; 11 | } 12 | 13 | return masks; 14 | } 15 | 16 | export const BIT_MASKS = initBitMasks(); 17 | 18 | export const SINE = new Int32Array(2048); 19 | export const COSINE = new Int32Array(2048); 20 | 21 | const CIRCULAR_ANGLE = 2048; 22 | const ANGULAR_RATIO = 360.0 / CIRCULAR_ANGLE; 23 | const ANGULAR_RATIO_RADIANS = ANGULAR_RATIO * DEGREES_TO_RADIANS; 24 | 25 | for (let i = 0; i < 2048; i++) { 26 | SINE[i] = (65536.0 * Math.sin(i * ANGULAR_RATIO_RADIANS)) | 0; 27 | COSINE[i] = (65536.0 * Math.cos(i * ANGULAR_RATIO_RADIANS)) | 0; 28 | } 29 | 30 | export const SINE_LARGE = new Int32Array(16384); 31 | export const COSINE_LARGE = new Int32Array(16384); 32 | 33 | const d = 3.834951969714103e-4; 34 | for (let i = 0; i < 16384; i++) { 35 | SINE_LARGE[i] = 16384.0 * Math.sin(i * d); 36 | COSINE_LARGE[i] = 16384.0 * Math.cos(i * d); 37 | } 38 | -------------------------------------------------------------------------------- /src/util/DeviceUtil.ts: -------------------------------------------------------------------------------- 1 | export const checkIphone = () => { 2 | const u = navigator.userAgent; 3 | return !!u.match(/iPhone/i); 4 | }; 5 | export const checkAndroid = () => { 6 | const u = navigator.userAgent; 7 | return !!u.match(/Android/i); 8 | }; 9 | export const checkIpad = () => { 10 | const u = navigator.userAgent; 11 | return !!u.match(/iPad/i); 12 | }; 13 | export const checkMobile = () => { 14 | const u = navigator.userAgent; 15 | return !!u.match(/Android/i) || !!u.match(/iPhone/i); 16 | }; 17 | 18 | export function checkIos() { 19 | // iPad on iOS 13 detection 20 | const isIpad = navigator.userAgent.includes("Macintosh") && navigator.maxTouchPoints >= 1; 21 | return /iPad|iPhone|iPod/.test(navigator.userAgent) || isIpad; 22 | } 23 | 24 | export const isIos = checkIos(); 25 | 26 | export const isWallpaperEngine = !!window.wallpaperRegisterAudioListener; 27 | 28 | export const isTouchDevice = !!( 29 | navigator.maxTouchPoints || "ontouchstart" in document.documentElement 30 | ); 31 | 32 | export const isWebGL2Supported = !!document.createElement("canvas").getContext("webgl2"); 33 | 34 | export const isWebGPUSupported = "gpu" in navigator; 35 | 36 | export const pixelRatio = window.devicePixelRatio || 1; 37 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/PseudoRandomNoiseOperation.ts: -------------------------------------------------------------------------------- 1 | import { TextureGenerator } from "../TextureGenerator"; 2 | import { TextureOperation } from "./TextureOperation"; 3 | 4 | export class PseudoRandomNoiseOperation extends TextureOperation { 5 | static noise(x: number, y: number): number { 6 | let n = x + y * 57; 7 | n ^= n << 1; 8 | return 4096 - ((((1376312589 + (789221 + 15731 * (n * n)) * n) & 0x7fffffff) / 262144) | 0); 9 | } 10 | 11 | constructor() { 12 | super(0, true); 13 | } 14 | 15 | override getMonochromeOutput(textureGenerator: TextureGenerator, line: number): Int32Array { 16 | if (!this.monochromeImageCache) { 17 | throw new Error("Monochrome image cache is not initialized"); 18 | } 19 | const output = this.monochromeImageCache.get(line); 20 | if (this.monochromeImageCache.dirty) { 21 | const vertGradient = textureGenerator.verticalGradient[line]; 22 | for (let pixel = 0; pixel < textureGenerator.width; pixel++) { 23 | const horzGradient = textureGenerator.horizontalGradient[pixel]; 24 | output[pixel] = PseudoRandomNoiseOperation.noise(horzGradient, vertGradient) % 4096; 25 | } 26 | } 27 | return output; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/mapviewer/buffer/DataBuffer.ts: -------------------------------------------------------------------------------- 1 | export class DataBuffer { 2 | stride: number; 3 | 4 | view: DataView; 5 | 6 | bytes: Uint8Array; 7 | 8 | offset: number; 9 | 10 | constructor(stride: number, count: number, offset: number = 0) { 11 | this.stride = stride; 12 | this.view = new DataView(new ArrayBuffer(count * this.stride)); 13 | this.bytes = new Uint8Array(this.view.buffer); 14 | this.offset = offset; 15 | } 16 | 17 | ensureSize(count: number): boolean { 18 | const byteOffset = this.offset * this.stride; 19 | if (byteOffset + count * this.stride >= this.view.byteLength) { 20 | const newLength = Math.max( 21 | this.view.byteLength * 2, 22 | this.stride * 128, 23 | byteOffset + count * this.stride, 24 | ); 25 | const newView = new DataView(new ArrayBuffer(newLength)); 26 | const newBytes = new Uint8Array(newView.buffer); 27 | newBytes.set(this.bytes, 0); 28 | this.view = newView; 29 | this.bytes = newBytes; 30 | return true; 31 | } 32 | return false; 33 | } 34 | 35 | byteOffset(): number { 36 | return this.offset * this.stride; 37 | } 38 | 39 | byteArray(): Uint8Array { 40 | return this.bytes.subarray(0, this.byteOffset()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/rs/cache/ref/ArchiveReference.ts: -------------------------------------------------------------------------------- 1 | import { ArchiveFileReference } from "./ArchiveFileReference"; 2 | 3 | export class ArchiveReference { 4 | constructor( 5 | readonly id: number, 6 | readonly nameHash: number, 7 | readonly whirlpool: Int8Array, 8 | readonly crc: number, 9 | readonly revision: number, 10 | readonly fileCount: number, 11 | readonly lastFileId: number, 12 | private readonly _fileIdIndexMap: Map, 13 | readonly fileIds: Int32Array, 14 | readonly fileNameHashes: Int32Array, 15 | ) {} 16 | 17 | getFileReference(id: number): ArchiveFileReference | undefined { 18 | const i = this._fileIdIndexMap.get(id); 19 | if (i === undefined) { 20 | return undefined; 21 | } 22 | 23 | return new ArchiveFileReference( 24 | this.fileIds[i], 25 | id, 26 | this.fileNameHashes ? this.fileNameHashes[i] : 0, 27 | ); 28 | } 29 | 30 | get fileReferences(): ArchiveFileReference[] { 31 | const refs = new Array(this.fileIds.length); 32 | for (let i = 0; i < this.fileIds.length; i++) { 33 | const ref = this.getFileReference(this.fileIds[i]); 34 | if (ref) { 35 | refs[i] = ref; 36 | } 37 | } 38 | return refs; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/rs/config/vartype/bit/VarBitTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../../cache/Archive"; 2 | import { CacheIndex } from "../../../cache/CacheIndex"; 3 | import { CacheInfo } from "../../../cache/CacheInfo"; 4 | import { 5 | ArchiveTypeLoader, 6 | DatTypeLoader, 7 | DummyTypeLoader, 8 | IndexTypeLoader, 9 | TypeLoader, 10 | } from "../../TypeLoader"; 11 | import { VarBitType } from "./VarBitType"; 12 | 13 | export type VarBitTypeLoader = TypeLoader; 14 | 15 | export class DummyVarBitTypeLoader extends DummyTypeLoader { 16 | constructor(cacheInfo: CacheInfo) { 17 | super(cacheInfo, VarBitType); 18 | } 19 | } 20 | 21 | export class DatVarBitTypeLoader { 22 | static load(cacheInfo: CacheInfo, configArchive: Archive): VarBitTypeLoader { 23 | return DatTypeLoader.load(VarBitType, cacheInfo, configArchive, "varbit"); 24 | } 25 | } 26 | 27 | export class ArchiveVarBitTypeLoader 28 | extends ArchiveTypeLoader 29 | implements VarBitTypeLoader 30 | { 31 | constructor(cacheInfo: CacheInfo, archive: Archive) { 32 | super(VarBitType, cacheInfo, archive); 33 | } 34 | } 35 | 36 | export class IndexVarBitTypeLoader extends IndexTypeLoader implements VarBitTypeLoader { 37 | constructor(cacheInfo: CacheInfo, index: CacheIndex) { 38 | super(VarBitType, cacheInfo, index, 10); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/rs/config/floortype/FloorTypeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheInfo } from "../../cache/CacheInfo"; 3 | import { ArchiveTypeLoader, DatTypeLoader, TypeLoader } from "../TypeLoader"; 4 | import { FloorType } from "./FloorType"; 5 | import { OverlayFloorType } from "./OverlayFloorType"; 6 | import { UnderlayFloorType } from "./UnderlayFloorType"; 7 | 8 | export type FloorTypeLoader = TypeLoader; 9 | 10 | export type UnderlayFloorTypeLoader = TypeLoader; 11 | export type OverlayFloorTypeLoader = TypeLoader; 12 | 13 | export class ArchiveUnderlayFloorTypeLoader 14 | extends ArchiveTypeLoader 15 | implements UnderlayFloorTypeLoader 16 | { 17 | constructor(cacheInfo: CacheInfo, archive: Archive) { 18 | super(UnderlayFloorType, cacheInfo, archive); 19 | } 20 | } 21 | 22 | export class ArchiveOverlayFloorTypeLoader 23 | extends ArchiveTypeLoader 24 | implements OverlayFloorTypeLoader 25 | { 26 | constructor(cacheInfo: CacheInfo, archive: Archive) { 27 | super(OverlayFloorType, cacheInfo, archive); 28 | } 29 | } 30 | 31 | export class DatFloorTypeLoader { 32 | static load(cacheInfo: CacheInfo, configArchive: Archive): OverlayFloorTypeLoader { 33 | return DatTypeLoader.load(OverlayFloorType, cacheInfo, configArchive, "flo"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/BinaryOperation.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { TextureGenerator } from "../TextureGenerator"; 3 | import { TextureOperation } from "./TextureOperation"; 4 | 5 | export class BinaryOperation extends TextureOperation { 6 | minValue: number = 0; 7 | maxValue: number = 4096; 8 | 9 | constructor() { 10 | super(1, true); 11 | } 12 | 13 | override decode(field: number, buffer: ByteBuffer): void { 14 | if (field === 0) { 15 | this.minValue = buffer.readUnsignedShort(); 16 | } else if (field === 1) { 17 | this.maxValue = buffer.readUnsignedShort(); 18 | } 19 | } 20 | 21 | override getMonochromeOutput(textureGenerator: TextureGenerator, line: number): Int32Array { 22 | if (!this.monochromeImageCache) { 23 | throw new Error("Monochrome image cache is not initialized"); 24 | } 25 | const output = this.monochromeImageCache.get(line); 26 | if (this.monochromeImageCache.dirty) { 27 | const input = this.getMonochromeInput(textureGenerator, 0, line); 28 | for (let x = 0; x < textureGenerator.width; x++) { 29 | const value = input[x]; 30 | output[x] = value >= this.minValue && value <= this.maxValue ? 4096 : 0; 31 | } 32 | } 33 | return output; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/rs/cache/IndexType.ts: -------------------------------------------------------------------------------- 1 | export class IndexType { 2 | static readonly LEGACY = { 3 | configs: 0, 4 | media: 1, 5 | textures: 2, 6 | models: 3, 7 | maps: 4, 8 | }; 9 | 10 | static readonly DAT = { 11 | configs: 0, 12 | models: 1, 13 | animations: 2, 14 | sounds: 3, 15 | maps: 4, 16 | }; 17 | 18 | static readonly DAT2 = { 19 | animations: 0, 20 | skeletons: 1, 21 | configs: 2, 22 | interfaces: 3, 23 | soundEffects: 4, 24 | maps: 5, 25 | musicTracks: 6, 26 | models: 7, 27 | sprites: 8, 28 | textures: 9, 29 | binary: 10, 30 | musicJingles: 11, 31 | clientScript: 12, 32 | fonts: 13, 33 | musicSamples: 14, 34 | musicPatches: 15, 35 | }; 36 | 37 | static readonly OSRS = { 38 | worldMapOld: 16, 39 | graphicDefaults: 17, 40 | worldMapGeography: 18, 41 | worldMap: 19, 42 | worldMapGround: 20, 43 | dbTableIndex: 21, 44 | animKeyFrames: 22, 45 | }; 46 | 47 | static readonly RS2 = { 48 | locs: 16, 49 | enums: 17, 50 | npcs: 18, 51 | objs: 19, 52 | seqs: 20, 53 | spotAnims: 21, 54 | varbits: 22, 55 | materials: 26, 56 | particles: 27, 57 | defaults: 28, 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022-2023, dennisdev 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /src/rs/crypto/Xtea.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../io/ByteBuffer"; 2 | 3 | export class Xtea { 4 | static readonly GOLDEN_RATIO = 0x9e3779b9; 5 | 6 | static readonly ROUNDS = 32; 7 | 8 | static readonly INITIAL_SUM = Math.imul(Xtea.GOLDEN_RATIO, Xtea.ROUNDS); 9 | 10 | static isValidKey(key: number[] | undefined): key is number[] { 11 | return ( 12 | key !== undefined && 13 | key.length === 4 && 14 | (key[0] !== 0 || key[1] !== 0 || key[2] !== 0 || key[3] !== 0) 15 | ); 16 | } 17 | 18 | static decrypt(buf: ByteBuffer, start: number, end: number, key: number[]): void { 19 | if (key.length !== 4) { 20 | throw new Error("Xtea: key is not 128 bits"); 21 | } 22 | 23 | const n = Math.floor((end - start) / 8); 24 | for (let i = 0; i < n; i++) { 25 | const offset = start + i * 8; 26 | let sum = Xtea.INITIAL_SUM; 27 | let v0 = buf.getInt(offset); 28 | let v1 = buf.getInt(offset + 4); 29 | for (let j = 0; j < Xtea.ROUNDS; j++) { 30 | v1 -= (((v0 << 4) ^ (v0 >>> 5)) + v0) ^ (sum + key[(sum >>> 11) & 3]); 31 | sum -= Xtea.GOLDEN_RATIO; 32 | v0 -= (((v1 << 4) ^ (v1 >>> 5)) + v1) ^ (sum + key[sum & 3]); 33 | } 34 | buf.setInt(offset, v0); 35 | buf.setInt(offset + 4, v1); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/Shaders.ts: -------------------------------------------------------------------------------- 1 | import { ProgramSource, prependDefines } from "./ShaderUtil"; 2 | import frameFxaaFragShader from "./frame-fxaa.frag.glsl"; 3 | import frameFxaaVertShader from "./frame-fxaa.vert.glsl"; 4 | import frameFragShader from "./frame.frag.glsl"; 5 | import frameVertShader from "./frame.vert.glsl"; 6 | import mainFragShader from "./main.frag.glsl"; 7 | import mainVertShader from "./main.vert.glsl"; 8 | import npcVertShader from "./npc.vert.glsl"; 9 | 10 | export function createProgram( 11 | vertShader: string, 12 | fragShader: string, 13 | hasMultiDraw: boolean, 14 | discardAlpha: boolean, 15 | ): ProgramSource { 16 | const defines: string[] = []; 17 | if (hasMultiDraw) { 18 | defines.push("MULTI_DRAW"); 19 | } 20 | if (discardAlpha) { 21 | defines.push("DISCARD_ALPHA"); 22 | } 23 | return [prependDefines(vertShader, defines), prependDefines(fragShader, defines)]; 24 | } 25 | 26 | export function createMainProgram(hasMultiDraw: boolean, discardAlpha: boolean): ProgramSource { 27 | return createProgram(mainVertShader, mainFragShader, hasMultiDraw, discardAlpha); 28 | } 29 | 30 | export function createNpcProgram(hasMultiDraw: boolean, discardAlpha: boolean): ProgramSource { 31 | return createProgram(npcVertShader, mainFragShader, hasMultiDraw, discardAlpha); 32 | } 33 | 34 | export const FRAME_PROGRAM = [frameVertShader, frameFragShader]; 35 | export const FRAME_FXAA_PROGRAM = [frameFxaaVertShader, frameFxaaFragShader]; 36 | -------------------------------------------------------------------------------- /src/mapviewer/data/npc/NpcSpawn.ts: -------------------------------------------------------------------------------- 1 | import { CacheInfo } from "../../../rs/cache/CacheInfo"; 2 | import npcSpawns2004Url from "./npc-spawns-2004.json?url"; 3 | import npcSpawns2009Url from "./npc-spawns-2009.json?url"; 4 | import npcSpawnsOsrsUrl from "./npc-spawns-osrs.json?url"; 5 | 6 | export interface NpcSpawn { 7 | id: number; 8 | name?: string; 9 | x: number; 10 | y: number; 11 | level: number; 12 | } 13 | 14 | export function getNpcSpawnsUrl(cacheInfo: CacheInfo): string { 15 | if (cacheInfo.game === "oldschool") { 16 | return npcSpawnsOsrsUrl; 17 | } else if (cacheInfo.revision > 474) { 18 | return npcSpawns2009Url; 19 | } else { 20 | return npcSpawns2004Url; 21 | } 22 | } 23 | 24 | export async function fetchNpcSpawns(url: string): Promise { 25 | const response = await fetch(url); 26 | return await response.json(); 27 | } 28 | 29 | export function fetchOsrsNpcSpawns(): Promise { 30 | return fetchNpcSpawns(npcSpawnsOsrsUrl); 31 | } 32 | 33 | export function fetchLegacyNpcSpawns(): Promise { 34 | return fetchNpcSpawns(npcSpawns2004Url); 35 | } 36 | 37 | export function getMapNpcSpawns( 38 | spawns: NpcSpawn[], 39 | maxLevel: number, 40 | mapX: number, 41 | mapY: number, 42 | ): NpcSpawn[] { 43 | return spawns.filter((obj) => { 44 | const npcMapX = (obj.x / 64) | 0; 45 | const npcMapY = (obj.y / 64) | 0; 46 | return mapX === npcMapX && mapY === npcMapY && obj.level <= maxLevel; 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/util/Hasher.ts: -------------------------------------------------------------------------------- 1 | import { xxHash32 } from "js-xxhash"; 2 | import xxhash, { XXHashAPI } from "xxhash-wasm"; 3 | 4 | export class Hasher { 5 | static hashApi: XXHashAPI | undefined; 6 | 7 | static async init(): Promise { 8 | Hasher.hashApi = await xxhash(); 9 | return Hasher.hashApi; 10 | } 11 | 12 | static hash32Int(n: number): number { 13 | const buf = new Int32Array([n]); 14 | return this.hash32(new Uint8Array(buf.buffer)); 15 | } 16 | 17 | static hash32(data: Uint8Array): number { 18 | if (Hasher.hashApi) { 19 | return Hasher.hashApi.h32Raw(data); 20 | } 21 | return Hasher.hash32js(data); 22 | } 23 | 24 | static hash32js(data: Uint8Array): number { 25 | return xxHash32(data); 26 | } 27 | 28 | static hash64(data: Uint8Array): bigint { 29 | if (Hasher.hashApi) { 30 | return Hasher.hashApi.h64Raw(data); 31 | } 32 | return Hasher.hash64js(data); 33 | } 34 | 35 | static hash64js(data: Uint8Array): bigint { 36 | const v0 = xxHash32(data, Math.random() * 0xffffff); 37 | const v1 = xxHash32(data, Math.random() * 0xffffff); 38 | return (BigInt(v0) << 32n) | BigInt(v1); 39 | } 40 | 41 | static bufToBigInt(data: Uint8Array): bigint { 42 | let bits = 8n; 43 | 44 | let ret = 0n; 45 | for (const i of data.values()) { 46 | const bi = BigInt(i); 47 | ret = (ret << bits) + bi; 48 | } 49 | return ret; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/mapviewer/MapViewerRenderers.ts: -------------------------------------------------------------------------------- 1 | import { MapViewer } from "./MapViewer"; 2 | import { MapViewerRenderer } from "./MapViewerRenderer"; 3 | import { WebGLMapViewerRenderer } from "./webgl/WebGLMapViewerRenderer"; 4 | import { WebGPUMapViewerRenderer } from "./webgpu/WebGPUMapViewerRenderer"; 5 | 6 | export type MapViewerRendererType = "webgl" | "webgpu"; 7 | export const WEBGL: MapViewerRendererType = "webgl"; 8 | export const WEBGPU: MapViewerRendererType = "webgpu"; 9 | 10 | export function getRendererName(type: MapViewerRendererType): string { 11 | switch (type) { 12 | case WEBGL: 13 | return "WebGL"; 14 | case WEBGPU: 15 | return "WebGPU"; 16 | default: 17 | throw new Error("Unknown renderer type"); 18 | } 19 | } 20 | 21 | export function createRenderer( 22 | type: MapViewerRendererType, 23 | mapViewer: MapViewer, 24 | ): MapViewerRenderer { 25 | switch (type) { 26 | case WEBGL: 27 | return new WebGLMapViewerRenderer(mapViewer); 28 | case WEBGPU: 29 | return new WebGPUMapViewerRenderer(mapViewer); 30 | default: 31 | throw new Error("Unknown renderer type"); 32 | } 33 | } 34 | 35 | export function getAvailableRenderers(): MapViewerRendererType[] { 36 | const renderers: MapViewerRendererType[] = []; 37 | 38 | if (WebGLMapViewerRenderer.isSupported()) { 39 | renderers.push(WEBGL); 40 | } 41 | 42 | if (WebGPUMapViewerRenderer.isSupported()) { 43 | renderers.push(WEBGPU); 44 | } 45 | 46 | return renderers; 47 | } 48 | -------------------------------------------------------------------------------- /src/components/rs/menu/OsrsMenu.css: -------------------------------------------------------------------------------- 1 | .context-menu-container { 2 | position: absolute; 3 | top: 100px; 4 | left: 100px; 5 | font-family: "OSRS Bold", ui-monospace, SFMono-Regular, Menlo, "Roboto Mono", monospace; 6 | display: flex; 7 | white-space: nowrap; 8 | border: 10px solid transparent; 9 | } 10 | 11 | .context-menu-container.tooltip { 12 | pointer-events: none; 13 | border: 0; 14 | } 15 | 16 | .context-menu { 17 | cursor: default; 18 | font-size: 16px; 19 | font-smooth: never; 20 | /* font-weight: normal; */ 21 | /* filter: opacity(100%); */ 22 | text-shadow: 1px 1px 0 black; 23 | /* display: flex; */ 24 | border: solid #5d5447 1px; 25 | } 26 | 27 | .title { 28 | padding-left: 2px; 29 | padding-right: 6px; 30 | color: #5d5447; 31 | background-color: black; 32 | border: solid black 1px; 33 | } 34 | 35 | .line { 36 | width: 100%; 37 | border-top: solid #5d5447 1px; 38 | } 39 | 40 | .options { 41 | color: white; 42 | background-color: #5d5447; 43 | border: solid black 1px; 44 | } 45 | 46 | .tooltip .options { 47 | background-color: #5d5447b2; 48 | } 49 | 50 | .option { 51 | padding-left: 2px; 52 | padding-right: 6px; 53 | padding-bottom: 1px; 54 | } 55 | 56 | .option:hover .option-name { 57 | color: #ffff00; 58 | } 59 | 60 | .object-name { 61 | color: #00ffff; 62 | } 63 | 64 | .npc-name { 65 | color: #ffff00; 66 | } 67 | 68 | .npc-level { 69 | color: #c0ff00; 70 | } 71 | 72 | .item-name { 73 | color: #ff9040; 74 | } 75 | 76 | .target-id { 77 | color: #ff5d40; 78 | } 79 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/buffer/ModelHashBuffer.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "../../../rs/model/Model"; 2 | import { Hasher } from "../../../util/Hasher"; 3 | import { DataBuffer } from "../../buffer/DataBuffer"; 4 | 5 | export function getModelHash(modelHashBuf: ModelHashBuffer, model: Model): number { 6 | const textureIds = 7 | (model.faceTextures && new Int32Array(model.faceTextures)) || new Int32Array(0); 8 | 9 | const datas = [ 10 | model.faceColors1, 11 | model.faceColors2, 12 | model.faceColors3, 13 | model.verticesX, 14 | model.verticesY, 15 | model.verticesZ, 16 | textureIds, 17 | ]; 18 | let dataLength = 0; 19 | for (const data of datas) { 20 | dataLength += data.length; 21 | } 22 | 23 | modelHashBuf.ensureSize(dataLength); 24 | 25 | let modelDataOffset = 0; 26 | for (const data of datas) { 27 | modelHashBuf.ints.set(data, modelDataOffset); 28 | modelDataOffset += data.length; 29 | } 30 | 31 | const hashData = modelHashBuf.bytes.subarray(0, modelDataOffset * 4); 32 | return Hasher.hash32(hashData); 33 | } 34 | 35 | export class ModelHashBuffer extends DataBuffer { 36 | ints: Int32Array; 37 | 38 | constructor(count: number) { 39 | super(4, count); 40 | this.ints = new Int32Array(this.bytes.buffer); 41 | } 42 | 43 | override ensureSize(count: number): boolean { 44 | const resized = super.ensureSize(count); 45 | if (resized) { 46 | this.ints = new Int32Array(this.bytes.buffer); 47 | } 48 | return resized; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/mapviewer/worker/MinimapData.ts: -------------------------------------------------------------------------------- 1 | import { CacheInfo } from "../../rs/cache/CacheInfo"; 2 | import { MapImageRenderer } from "../../rs/map/MapImageRenderer"; 3 | import { Scene } from "../../rs/scene/Scene"; 4 | 5 | export type MinimapData = { 6 | mapX: number; 7 | mapY: number; 8 | level: number; 9 | cacheInfo: CacheInfo; 10 | 11 | minimapBlob: Blob; 12 | }; 13 | 14 | export async function loadMinimapBlob( 15 | mapImageRenderer: MapImageRenderer, 16 | scene: Scene, 17 | level: number, 18 | borderSize: number, 19 | drawMapFunctions: boolean, 20 | ): Promise { 21 | const minimapPixels = mapImageRenderer.renderMinimapHd(scene, level, drawMapFunctions); 22 | 23 | const minimapView = new DataView(minimapPixels.buffer); 24 | for (let i = 0; i < minimapPixels.length; i++) { 25 | minimapView.setUint32(i * 4, (minimapPixels[i] << 8) | 0xff); 26 | } 27 | 28 | const widthExclBorder = (scene.sizeX - borderSize * 2) * 4; 29 | const heightExclBorder = (scene.sizeY - borderSize * 2) * 4; 30 | const canvas = new OffscreenCanvas(widthExclBorder, heightExclBorder); 31 | const ctx = canvas.getContext("2d"); 32 | if (!ctx) { 33 | throw new Error("Could not get canvas context"); 34 | } 35 | 36 | const pixelWidth = scene.sizeX * 4; 37 | const pixelHeight = scene.sizeY * 4; 38 | const imageData = new ImageData(pixelWidth, pixelHeight); 39 | imageData.data.set(new Uint8ClampedArray(minimapPixels.buffer)); 40 | 41 | ctx.putImageData(imageData, -borderSize * 4, -borderSize * 4); 42 | 43 | return canvas.convertToBlob(); 44 | } 45 | -------------------------------------------------------------------------------- /src/rs/model/skeletal/SkeletalSeqLoader.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../../cache/Archive"; 2 | import { CacheIndex } from "../../cache/CacheIndex"; 3 | import { SeqBaseLoader } from "../seq/SeqBaseLoader"; 4 | import { SkeletalSeq } from "./SkeletalSeq"; 5 | 6 | export interface SkeletalSeqLoader { 7 | load(id: number): SkeletalSeq | undefined; 8 | 9 | clearCache(): void; 10 | } 11 | 12 | export class IndexSkeletalSeqLoader implements SkeletalSeqLoader { 13 | seqs: Map = new Map(); 14 | 15 | archiveCache: Map = new Map(); 16 | 17 | constructor( 18 | readonly animIndex: CacheIndex, 19 | readonly baseLoader: SeqBaseLoader, 20 | ) {} 21 | 22 | load(id: number): SkeletalSeq | undefined { 23 | const cached = this.seqs.get(id); 24 | if (cached) { 25 | return cached; 26 | } 27 | 28 | const archiveId = id >> 16; 29 | const fileId = id & 0xffff; 30 | 31 | let archive = this.archiveCache.get(archiveId); 32 | if (!archive) { 33 | archive = this.animIndex.getArchive(archiveId); 34 | this.archiveCache.set(archiveId, archive); 35 | } 36 | 37 | const file = archive.getFile(fileId); 38 | if (!file) { 39 | return undefined; 40 | } 41 | 42 | const skeletalSeq = SkeletalSeq.load(this.baseLoader, id, file.data); 43 | this.seqs.set(id, skeletalSeq); 44 | return skeletalSeq; 45 | } 46 | 47 | clearCache(): void { 48 | this.seqs.clear(); 49 | this.archiveCache.clear(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/mapviewer/worker/RenderDataLoader.ts: -------------------------------------------------------------------------------- 1 | import { SerializerImplementation } from "threads"; 2 | 3 | import { SdMapDataLoader } from "../webgl/loader/SdMapDataLoader"; 4 | import { WorkerState } from "./RenderDataWorker"; 5 | 6 | export type RenderDataResult = { 7 | data: T; 8 | transferables: Transferable[]; 9 | }; 10 | 11 | export interface RenderDataLoader { 12 | // Used by serializer 13 | __type: keyof RenderDataLoaders; 14 | 15 | init(): void; 16 | 17 | load(state: WorkerState, input: I): Promise>; 18 | 19 | reset(): void; 20 | } 21 | 22 | const loaders = { 23 | sdMapDataLoader: new SdMapDataLoader(), 24 | }; 25 | 26 | type RenderDataLoaders = typeof loaders; 27 | 28 | const loaderSerializerNamePrefix = "$RenderDataLoader$"; 29 | 30 | export const renderDataLoaderSerializer: SerializerImplementation = { 31 | serialize(value, defaultHandler) { 32 | if (value && value.__type) { 33 | return loaderSerializerNamePrefix + value.__type; 34 | } else { 35 | return defaultHandler(value); 36 | } 37 | }, 38 | deserialize(value, defaultHandler) { 39 | if (value && typeof value === "string" && value.startsWith(loaderSerializerNamePrefix)) { 40 | const type = value.substring(loaderSerializerNamePrefix.length); 41 | if (type in loaders) { 42 | return loaders[type as keyof RenderDataLoaders]; 43 | } 44 | throw new Error("Unknown loader type: " + type); 45 | } else { 46 | return defaultHandler(value); 47 | } 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/loader/SdMapData.ts: -------------------------------------------------------------------------------- 1 | import { CollisionData } from "../../../rs/scene/CollisionMap"; 2 | import { DrawRange } from "../DrawRange"; 3 | import { LocAnimatedData } from "../loc/LocAnimatedData"; 4 | import { NpcData } from "../npc/NpcData"; 5 | 6 | export type SdMapData = { 7 | mapX: number; 8 | mapY: number; 9 | 10 | cacheName: string; 11 | 12 | maxLevel: number; 13 | loadObjs: boolean; 14 | loadNpcs: boolean; 15 | 16 | smoothTerrain: boolean; 17 | 18 | borderSize: number; 19 | 20 | tileRenderFlags: Uint8Array[][]; 21 | collisionDatas: CollisionData[]; 22 | 23 | minimapBlob: Blob; 24 | 25 | vertices: Uint8Array; 26 | indices: Int32Array; 27 | 28 | modelTextureData: Uint16Array; 29 | modelTextureDataAlpha: Uint16Array; 30 | 31 | modelTextureDataLod: Uint16Array; 32 | modelTextureDataLodAlpha: Uint16Array; 33 | 34 | modelTextureDataInteract: Uint16Array; 35 | modelTextureDataInteractAlpha: Uint16Array; 36 | 37 | modelTextureDataInteractLod: Uint16Array; 38 | modelTextureDataInteractLodAlpha: Uint16Array; 39 | 40 | heightMapTextureData: Int16Array; 41 | 42 | drawRanges: DrawRange[]; 43 | drawRangesAlpha: DrawRange[]; 44 | 45 | drawRangesLod: DrawRange[]; 46 | drawRangesLodAlpha: DrawRange[]; 47 | 48 | drawRangesInteract: DrawRange[]; 49 | drawRangesInteractAlpha: DrawRange[]; 50 | 51 | drawRangesInteractLod: DrawRange[]; 52 | drawRangesInteractLodAlpha: DrawRange[]; 53 | 54 | locsAnimated: LocAnimatedData[]; 55 | npcs: NpcData[]; 56 | 57 | loadedTextures: Map; 58 | }; 59 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/ConstantColourOperation.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { TextureGenerator } from "../TextureGenerator"; 3 | import { TextureOperation } from "./TextureOperation"; 4 | 5 | export class ConstantColourOperation extends TextureOperation { 6 | constantR: number = 0; 7 | constantG: number = 0; 8 | constantB: number = 0; 9 | 10 | constructor(rgb: number = 0) { 11 | super(0, false); 12 | } 13 | 14 | override decode(field: number, buffer: ByteBuffer): void { 15 | if (field === 0) { 16 | this.setConstant(buffer.readMedium()); 17 | } 18 | } 19 | 20 | setConstant(rgb: number) { 21 | this.constantR = ((rgb >> 16) & 0xff) * 16; 22 | this.constantG = ((rgb >> 8) & 0xff) * 16; 23 | this.constantB = (rgb & 0xff) * 16; 24 | } 25 | 26 | override getColourOutput(textureGenerator: TextureGenerator, line: number): Int32Array[] { 27 | if (!this.colourImageCache) { 28 | throw new Error("Colour image cache is not initialized"); 29 | } 30 | const output = this.colourImageCache.get(line); 31 | if (this.colourImageCache.dirty) { 32 | const outputR = output[0]; 33 | const outputG = output[1]; 34 | const outputB = output[2]; 35 | for (let pixel = 0; pixel < textureGenerator.width; pixel++) { 36 | outputR[pixel] = this.constantR; 37 | outputG[pixel] = this.constantG; 38 | outputB[pixel] = this.constantB; 39 | } 40 | } 41 | return output; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/rs/cache/store/Sector.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../io/ByteBuffer"; 2 | 3 | export class Sector { 4 | static readonly HEADER_SIZE = 8; 5 | 6 | static readonly DATA_SIZE = 512; 7 | 8 | static readonly EXTENDED_HEADER_SIZE = 10; 9 | 10 | static readonly EXTENDED_DATA_SIZE = 510; 11 | 12 | static readonly SIZE = Sector.HEADER_SIZE + Sector.DATA_SIZE; 13 | 14 | indexId!: number; 15 | archiveId!: number; 16 | chunk!: number; 17 | nextSector!: number; 18 | data!: Int8Array; 19 | 20 | static decodeNew(buffer: ByteBuffer): Sector { 21 | return Sector.decode(new Sector(), buffer); 22 | } 23 | 24 | static decodeExtendedNew(buffer: ByteBuffer): Sector { 25 | return Sector.decodeExtended(new Sector(), buffer); 26 | } 27 | 28 | static decode(sector: Sector, buffer: ByteBuffer, dataSize = Sector.DATA_SIZE): Sector { 29 | sector.archiveId = buffer.readUnsignedShort(); 30 | sector.chunk = buffer.readUnsignedShort(); 31 | sector.nextSector = buffer.readMedium(); 32 | sector.indexId = buffer.readUnsignedByte(); 33 | sector.data = buffer.readBytes(dataSize); 34 | return sector; 35 | } 36 | 37 | static decodeExtended( 38 | sector: Sector, 39 | buffer: ByteBuffer, 40 | dataSize = Sector.EXTENDED_DATA_SIZE, 41 | ): Sector { 42 | sector.archiveId = buffer.readInt(); 43 | sector.chunk = buffer.readUnsignedShort(); 44 | sector.nextSector = buffer.readMedium(); 45 | sector.indexId = buffer.readUnsignedByte(); 46 | sector.data = buffer.readBytes(dataSize); 47 | return sector; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/mapviewer/webgl/shaders/includes/hsl-to-rgb.glsl: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/17309861 2 | vec3 hslToRgb(int hsl, float brightness) { 3 | const float onethird = 1.0 / 3.0; 4 | const float twothird = 2.0 / 3.0; 5 | const float rcpsixth = 6.0; 6 | 7 | float hue = float(hsl >> 10) / 64.0 + 0.0078125; 8 | float sat = float((hsl >> 7) & 0x7) / 8.0 + 0.0625; 9 | float lum = float(hsl & 0x7f) / 128.0; 10 | 11 | vec3 xt = vec3( 12 | rcpsixth * (hue - twothird), 13 | 0.0, 14 | rcpsixth * (1.0 - hue) 15 | ); 16 | 17 | xt = mix(xt, vec3( 18 | 0.0, 19 | rcpsixth * (twothird - hue), 20 | rcpsixth * (hue - onethird) 21 | ), when_lt(hue, twothird)); 22 | 23 | xt = mix(xt, vec3( 24 | rcpsixth * (onethird - hue), 25 | rcpsixth * hue, 26 | 0.0 27 | ), when_lt(hue, onethird)); 28 | 29 | // if (hue < twothird) { 30 | // xt.r = 0.0; 31 | // xt.g = rcpsixth * (twothird - hue); 32 | // xt.b = rcpsixth * (hue - onethird); 33 | // } 34 | 35 | // if (hue < onethird) { 36 | // xt.r = rcpsixth * (onethird - hue); 37 | // xt.g = rcpsixth * hue; 38 | // xt.b = 0.0; 39 | // } 40 | 41 | xt = min( xt, 1.0 ); 42 | 43 | float sat2 = 2.0 * sat; 44 | float satinv = 1.0 - sat; 45 | float luminv = 1.0 - lum; 46 | float lum2m1 = (2.0 * lum) - 1.0; 47 | vec3 ct = (sat2 * xt) + satinv; 48 | 49 | // vec3 rgb; 50 | // if (lum >= 0.5) 51 | // rgb = (luminv * ct) + lum2m1; 52 | // else rgb = lum * ct; 53 | 54 | vec3 rgb = mix((luminv * ct) + lum2m1, lum * ct, when_lt(lum, 0.5)); 55 | 56 | return pow(rgb, vec3(brightness)); 57 | } 58 | -------------------------------------------------------------------------------- /src/rs/model/skeletal/SkeletalBase.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../io/ByteBuffer"; 2 | import { SkeletalBone } from "./SkeletalBone"; 3 | import { SkeletalSeq } from "./SkeletalSeq"; 4 | 5 | export class SkeletalBase { 6 | bones: SkeletalBone[]; 7 | poseCount: number; 8 | 9 | constructor(buffer: ByteBuffer, count: number) { 10 | this.bones = new Array(count); 11 | this.poseCount = buffer.readUnsignedByte(); 12 | 13 | for (let i = 0; i < this.bones.length; i++) { 14 | this.bones[i] = new SkeletalBone(this.poseCount, buffer, false); 15 | } 16 | 17 | this.linkBones(); 18 | } 19 | 20 | linkBones(): void { 21 | for (let i = 0; i < this.bones.length; i++) { 22 | const bone = this.bones[i]; 23 | if (bone.parentId >= 0) { 24 | bone.parent = this.bones[bone.parentId]; 25 | } 26 | } 27 | } 28 | 29 | updateAnimMatrices( 30 | skeletalSeq: SkeletalSeq, 31 | frame: number, 32 | masks: boolean[] | undefined = undefined, 33 | mask: boolean = false, 34 | ): void { 35 | const poseId = skeletalSeq.poseId; 36 | 37 | let boneIndex = 0; 38 | for (const bone of this.bones) { 39 | if (masks === undefined || masks[boneIndex] === mask) { 40 | skeletalSeq.updateAnimMatrix(frame, bone, boneIndex, poseId); 41 | } 42 | boneIndex++; 43 | } 44 | } 45 | 46 | getBoneCount(): number { 47 | return this.bones.length; 48 | } 49 | 50 | getBone(id: number): SkeletalBone | undefined { 51 | if (id >= this.getBoneCount()) { 52 | return undefined; 53 | } 54 | return this.bones[id]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/rs/worldmap/WorldMap.css: -------------------------------------------------------------------------------- 1 | .worldmap-container, 2 | .worldmap { 3 | display: flex; 4 | flex-direction: column; 5 | position: relative; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | .worldmap { 11 | overflow: hidden; 12 | } 13 | 14 | .worldmap-image { 15 | position: absolute; 16 | } 17 | 18 | .worldmap-drag { 19 | position: absolute; 20 | left: 0; 21 | top: 0; 22 | width: 100%; 23 | height: 100%; 24 | /* background-color: white; */ 25 | } 26 | 27 | .worldmap-drag.dragging { 28 | cursor: grabbing; 29 | } 30 | 31 | .worldmap-border { 32 | position: absolute; 33 | border: 2px solid #800000; 34 | } 35 | 36 | .worldmap-footer { 37 | display: flex; 38 | color: white; 39 | border-left: 0; 40 | border-right: 0; 41 | border-bottom: 0; 42 | border-image-width: 6px 0 0 0; 43 | /* padding-left: 4px; */ 44 | align-items: flex-end; 45 | } 46 | 47 | .worldmap-zoom-button { 48 | width: 36px; 49 | height: 24px; 50 | margin: 4px 4px 4px 0; 51 | cursor: pointer; 52 | } 53 | 54 | .worldmap-zoom-out { 55 | background-image: url(./zoom-out.png); 56 | } 57 | 58 | .worldmap-zoom-in { 59 | background-image: url(./zoom-in.png); 60 | } 61 | 62 | .worldmap-location-select { 63 | display: flex; 64 | align-items: center; 65 | height: 100%; 66 | } 67 | 68 | .osrs-select-container { 69 | margin: 0 4px 0 4px; 70 | } 71 | 72 | @media (max-width: 340px) { 73 | .hide-mobile { 74 | display: none; 75 | } 76 | .worldmap-location-select { 77 | flex: 1; 78 | padding-right: 8px; 79 | } 80 | .worldmap-zoom-buttons { 81 | flex: inherit; 82 | } 83 | .osrs-select-container { 84 | /* display: none; */ 85 | /* width: 100px !important; */ 86 | min-width: 100% !important; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /scripts/cache/export-textures.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import sharp from "sharp"; 3 | 4 | import { CacheSystem } from "../../src/rs/cache/CacheSystem"; 5 | import { getCacheLoaderFactory } from "../../src/rs/cache/loader/CacheLoaderFactory"; 6 | import { loadCache, loadCacheInfos, loadCacheList } from "./load-util"; 7 | 8 | function saveArgbArrayToPng(pixels: Int32Array, width: number, height: number, outputPath: string) { 9 | // Convert ARGB to RGBA 10 | const rgbaPixels = new Uint8Array(pixels.length * 4); 11 | for (let i = 0; i < pixels.length; i++) { 12 | rgbaPixels[i * 4 + 0] = (pixels[i] >> 16) & 0xff; // R 13 | rgbaPixels[i * 4 + 1] = (pixels[i] >> 8) & 0xff; // G 14 | rgbaPixels[i * 4 + 2] = pixels[i] & 0xff; // B 15 | rgbaPixels[i * 4 + 3] = (pixels[i] >> 24) & 0xff; // A 16 | } 17 | 18 | // Convert to PNG using sharp 19 | sharp(rgbaPixels, { 20 | raw: { 21 | width: width, 22 | height: height, 23 | channels: 4, 24 | }, 25 | }) 26 | .toFile(outputPath) 27 | .catch((err) => console.error(err)); 28 | } 29 | 30 | const SIZE = 128; 31 | 32 | const caches = loadCacheInfos(); 33 | const cacheList = loadCacheList(caches); 34 | 35 | const cacheInfo = cacheList.latest; 36 | 37 | const loadedCache = loadCache(cacheInfo); 38 | 39 | const cacheSystem = CacheSystem.fromFiles(loadedCache.type, loadedCache.files); 40 | const cacheLoaderFactory = getCacheLoaderFactory(cacheInfo, cacheSystem); 41 | 42 | const textureLoader = cacheLoaderFactory.getTextureLoader(); 43 | 44 | fs.mkdirSync("./textures", { recursive: true }); 45 | 46 | for (const id of textureLoader.getTextureIds()) { 47 | const pixels = textureLoader.getPixelsArgb(id, SIZE, false, 1.0); 48 | 49 | const outputPath = `./textures/${id}.png`; 50 | saveArgbArrayToPng(pixels, SIZE, SIZE, outputPath); 51 | } 52 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "OSRS Bold"; 3 | src: url(./media/RuneScape-Bold-12.ttf); 4 | } 5 | @font-face { 6 | font-family: "OSRS Small"; 7 | src: url(./media/RuneScape-Plain-11.ttf); 8 | } 9 | 10 | html, 11 | body { 12 | margin: 0; 13 | padding: 0; 14 | height: 100vh; 15 | 16 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 17 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | overflow: hidden; 21 | background-color: black; 22 | 23 | -webkit-touch-callout: none; /* iOS Safari */ 24 | -webkit-user-select: none; /* Safari */ 25 | -khtml-user-select: none; /* Konqueror HTML */ 26 | -moz-user-select: none; /* Old versions of Firefox */ 27 | -ms-user-select: none; /* Internet Explorer/Edge */ 28 | user-select: none; /* Non-prefixed version, currently 29 | supported by Chrome, Edge, Opera and Firefox */ 30 | } 31 | 32 | #root, 33 | .max-height { 34 | height: 100%; 35 | } 36 | 37 | canvas { 38 | outline: none; 39 | border: none; 40 | } 41 | 42 | .center-container { 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | } 47 | 48 | .content-text { 49 | font-family: ui-monospace, SFMono-Regular, Menlo, "Roboto Mono", monospace; 50 | font-weight: bold; 51 | color: white; 52 | text-shadow: 1px 1px 0 black; 53 | } 54 | 55 | .flex { 56 | display: flex; 57 | flex: 1; 58 | } 59 | 60 | .align-right { 61 | justify-content: right; 62 | } 63 | 64 | .rs-border { 65 | border: 6px solid; 66 | border-image: url(./media/interface-border.png) 6 / 6px / 0px repeat; 67 | } 68 | 69 | .rs-background { 70 | background-image: url(./media/interface-bg.png); 71 | } 72 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | OSRS Map Viewer 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/rs/config/Type.ts: -------------------------------------------------------------------------------- 1 | import { CacheInfo } from "../cache/CacheInfo"; 2 | import { CacheType, detectCacheType } from "../cache/CacheType"; 3 | import { ByteBuffer } from "../io/ByteBuffer"; 4 | 5 | export type ParamsMap = Map; 6 | 7 | export abstract class Type { 8 | readonly id: number; 9 | 10 | readonly cacheInfo: CacheInfo; 11 | 12 | readonly cacheType: CacheType; 13 | 14 | static readParamsMap(buf: ByteBuffer, params?: ParamsMap): ParamsMap { 15 | const count = buf.readUnsignedByte(); 16 | if (!params) { 17 | params = new Map(); 18 | } 19 | 20 | for (let i = 0; i < count; i++) { 21 | const isStringValue = buf.readUnsignedByte() === 1; 22 | const key = buf.readMedium(); 23 | if (isStringValue) { 24 | params.set(key, buf.readString()); 25 | } else { 26 | params.set(key, buf.readInt()); 27 | } 28 | } 29 | return params; 30 | } 31 | 32 | constructor(id: number, cacheInfo: CacheInfo) { 33 | this.id = id; 34 | this.cacheInfo = cacheInfo; 35 | this.cacheType = detectCacheType(cacheInfo); 36 | } 37 | 38 | readString(buffer: ByteBuffer): string { 39 | const stopValue = this.cacheType !== "dat2" ? 0xa : 0; 40 | return buffer.readString(stopValue); 41 | } 42 | 43 | decode(buffer: ByteBuffer): void { 44 | while (true) { 45 | if (buffer.offset > buffer.length - 1) { 46 | throw new Error("Buffer overflow"); 47 | } 48 | const opcode = buffer.readUnsignedByte(); 49 | if (opcode === 0) { 50 | break; 51 | } 52 | this.decodeOpcode(opcode, buffer); 53 | } 54 | } 55 | 56 | abstract decodeOpcode(opcode: number, buffer: ByteBuffer): void; 57 | 58 | post(): void {} 59 | } 60 | -------------------------------------------------------------------------------- /src/rs/util/HeightCalc.ts: -------------------------------------------------------------------------------- 1 | import { COSINE } from "../MathConstants"; 2 | 3 | function interpolate(i: number, i_4_: number, i_5_: number, freq: number): number { 4 | const i_8_ = (65536 - COSINE[(i_5_ * 1024) / freq]) >> 1; 5 | return ((i_8_ * i_4_) >> 16) + (((65536 - i_8_) * i) >> 16); 6 | } 7 | 8 | function noise(x: number, y: number): number { 9 | let n = y * 57 + x; 10 | n = (n << 13) ^ n; 11 | const n2 = (Math.imul(n, Math.imul(Math.imul(n, n), 15731) + 789221) + 1376312589) & 0x7fffffff; 12 | return (n2 >> 19) & 0xff; 13 | } 14 | 15 | function smoothedNoise1(x: number, y: number): number { 16 | const corners = 17 | noise(x - 1, y - 1) + noise(x + 1, y - 1) + noise(x - 1, y + 1) + noise(x + 1, y + 1); 18 | const sides = noise(x - 1, y) + noise(x + 1, y) + noise(x, y - 1) + noise(x, y + 1); 19 | const center = noise(x, y); 20 | return ((center / 4) | 0) + ((sides / 8) | 0) + ((corners / 16) | 0); 21 | } 22 | 23 | function interpolateNoise(x: number, y: number, freq: number): number { 24 | const intX = (x / freq) | 0; 25 | const fracX = x & (freq - 1); 26 | const intY = (y / freq) | 0; 27 | const fracY = y & (freq - 1); 28 | const v1 = smoothedNoise1(intX, intY); 29 | const v2 = smoothedNoise1(intX + 1, intY); 30 | const v3 = smoothedNoise1(intX, intY + 1); 31 | const v4 = smoothedNoise1(intX + 1, intY + 1); 32 | const i1 = interpolate(v1, v2, fracX, freq); 33 | const i2 = interpolate(v3, v4, fracX, freq); 34 | return interpolate(i1, i2, fracY, freq); 35 | } 36 | 37 | export function generateHeight(x: number, y: number) { 38 | let n = 39 | interpolateNoise(x + 45365, y + 91923, 4) - 40 | 128 + 41 | ((interpolateNoise(x + 10294, y + 37821, 2) - 128) >> 1) + 42 | ((interpolateNoise(x, y, 1) - 128) >> 2); 43 | n = ((0.3 * n) | 0) + 35; 44 | if (n < 10) { 45 | n = 10; 46 | } else if (n > 60) { 47 | n = 60; 48 | } 49 | return n; 50 | } 51 | -------------------------------------------------------------------------------- /src/rs/config/enumtype/EnumType.ts: -------------------------------------------------------------------------------- 1 | import { CacheInfo } from "../../cache/CacheInfo"; 2 | import { ByteBuffer } from "../../io/ByteBuffer"; 3 | import { Type } from "../Type"; 4 | 5 | export class EnumType extends Type { 6 | inputType!: string; 7 | outputType!: string; 8 | 9 | defaultString: string; 10 | defaultInt!: number; 11 | 12 | outputCount: number; 13 | 14 | keys!: number[]; 15 | 16 | intValues!: number[]; 17 | stringValues!: string[]; 18 | 19 | constructor(id: number, cacheInfo: CacheInfo) { 20 | super(id, cacheInfo); 21 | this.defaultString = "null"; 22 | this.outputCount = 0; 23 | } 24 | 25 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 26 | if (opcode === 1) { 27 | this.inputType = String.fromCharCode(buffer.readUnsignedByte()); 28 | } else if (opcode === 2) { 29 | this.outputType = String.fromCharCode(buffer.readUnsignedByte()); 30 | } else if (opcode === 3) { 31 | this.defaultString = buffer.readString(); 32 | } else if (opcode === 4) { 33 | this.defaultInt = buffer.readInt(); 34 | } else if (opcode === 5) { 35 | this.outputCount = buffer.readUnsignedShort(); 36 | this.keys = new Array(this.outputCount); 37 | this.stringValues = new Array(this.outputCount); 38 | 39 | for (let i = 0; i < this.outputCount; i++) { 40 | this.keys[i] = buffer.readInt(); 41 | this.stringValues[i] = buffer.readString(); 42 | } 43 | } else if (opcode === 6) { 44 | this.outputCount = buffer.readUnsignedShort(); 45 | this.keys = new Array(this.outputCount); 46 | this.intValues = new Array(this.outputCount); 47 | 48 | for (let i = 0; i < this.outputCount; i++) { 49 | this.keys[i] = buffer.readInt(); 50 | this.intValues[i] = buffer.readInt(); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/rs/config/paramtype/ParamType.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../io/ByteBuffer"; 2 | import { Type } from "../Type"; 3 | 4 | export class ParamType extends Type { 5 | private static SCRIPT_VAR_TYPES = [ 6 | "€", 7 | "\u0000", 8 | "‚", 9 | "ƒ", 10 | "„", 11 | "…", 12 | "†", 13 | "‡", 14 | "ˆ", 15 | "‰", 16 | "Š", 17 | "‹", 18 | "Œ", 19 | "\u0000", 20 | "Ž", 21 | "\u0000", 22 | "\u0000", 23 | "‘", 24 | "’", 25 | "“", 26 | "”", 27 | "•", 28 | "–", 29 | "—", 30 | "˜", 31 | "™", 32 | "š", 33 | "›", 34 | "œ", 35 | "\u0000", 36 | "ž", 37 | "Ÿ", 38 | ]; 39 | 40 | // ScriptVarType 41 | type!: string; 42 | 43 | defaultInt: number = 0; 44 | 45 | defaultString!: string; 46 | 47 | autoDisable: boolean = true; 48 | 49 | static getJagexChar(c: number): string { 50 | if (c === 0) { 51 | throw new Error("Invalid char: " + c); 52 | } else { 53 | if (c >= 128 && c < 160) { 54 | let s = ParamType.SCRIPT_VAR_TYPES[c - 128]; 55 | if (s === "\u0000") { 56 | s = "?"; 57 | } 58 | 59 | return s; 60 | } 61 | 62 | return String.fromCharCode(c); 63 | } 64 | } 65 | 66 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 67 | if (opcode === 1) { 68 | this.type = ParamType.getJagexChar(buffer.readUnsignedByte()); 69 | } else if (opcode === 2) { 70 | this.defaultInt = buffer.readInt(); 71 | } else if (opcode === 4) { 72 | this.autoDisable = false; 73 | } else if (opcode === 5) { 74 | this.defaultString = buffer.readString(); 75 | } 76 | } 77 | 78 | isString(): boolean { 79 | return this.type === "s"; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /scripts/cache/load-util.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | import { CacheList, LoadedCache, XteaMap } from "../../src/mapviewer/Caches"; 4 | import { CacheFiles } from "../../src/rs/cache/CacheFiles"; 5 | import { CacheInfo, getLatestCache } from "../../src/rs/cache/CacheInfo"; 6 | import { detectCacheType } from "../../src/rs/cache/CacheType"; 7 | 8 | export function loadCacheInfos(): CacheInfo[] { 9 | const json = fs.readFileSync("./caches/caches.json", "utf8"); 10 | return JSON.parse(json); 11 | } 12 | 13 | export function loadCacheList(caches: CacheInfo[]): CacheList { 14 | const latest = getLatestCache(caches); 15 | if (!latest) { 16 | throw new Error("No latest cache"); 17 | } 18 | return { 19 | caches, 20 | latest, 21 | }; 22 | } 23 | 24 | export function loadCacheFiles(cache: CacheInfo): CacheFiles { 25 | const cachePath = "./caches/" + cache.name + "/"; 26 | 27 | const files = new Map(); 28 | 29 | fs.readdirSync(cachePath).forEach((fileName: string) => { 30 | const buffer = fs.readFileSync(cachePath + fileName); 31 | 32 | const newBuffer = new ArrayBuffer(buffer.byteLength); 33 | const newView = new Uint8Array(newBuffer); 34 | for (let i = 0; i < buffer.byteLength; i++) { 35 | newView[i] = buffer[i]; 36 | } 37 | 38 | files.set(fileName, newBuffer); 39 | }); 40 | 41 | return new CacheFiles(files); 42 | } 43 | 44 | export function loadCache(info: CacheInfo): LoadedCache { 45 | const files = loadCacheFiles(info); 46 | const xteas = loadXteas(info); 47 | return { 48 | info, 49 | type: detectCacheType(info), 50 | files, 51 | xteas, 52 | }; 53 | } 54 | 55 | export function loadXteas(cache: CacheInfo): XteaMap { 56 | const cachePath = "./caches/" + cache.name + "/"; 57 | const json = fs.readFileSync(cachePath + "keys.json", "utf8"); 58 | const data: Record = JSON.parse(json); 59 | return new Map(Object.keys(data).map((key) => [parseInt(key), data[key]])); 60 | } 61 | -------------------------------------------------------------------------------- /src/mapviewer/Caches.ts: -------------------------------------------------------------------------------- 1 | import { CacheFiles, ProgressListener } from "../rs/cache/CacheFiles"; 2 | import { CacheInfo, getLatestCache } from "../rs/cache/CacheInfo"; 3 | import { CacheType, detectCacheType } from "../rs/cache/CacheType"; 4 | 5 | const CACHE_PATH = "/caches/"; 6 | 7 | export async function fetchCacheInfos(): Promise { 8 | const resp = await fetch(CACHE_PATH + "caches.json"); 9 | return resp.json(); 10 | } 11 | 12 | export type CacheList = { 13 | caches: CacheInfo[]; 14 | latest: CacheInfo; 15 | }; 16 | 17 | export async function fetchCacheList(): Promise { 18 | const caches = await fetchCacheInfos(); 19 | const latest = getLatestCache(caches); 20 | if (!latest) { 21 | return undefined; 22 | } 23 | return { 24 | caches, 25 | latest, 26 | }; 27 | } 28 | 29 | export type LoadedCache = { 30 | info: CacheInfo; 31 | type: CacheType; 32 | files: CacheFiles; 33 | xteas: XteaMap; 34 | }; 35 | 36 | export async function loadCacheFiles( 37 | info: CacheInfo, 38 | signal?: AbortSignal, 39 | progressListener?: ProgressListener, 40 | ): Promise { 41 | const cachePath = CACHE_PATH + info.name + "/"; 42 | 43 | const xteasPromise = fetchXteas(cachePath + "keys.json", signal); 44 | 45 | const cacheType = detectCacheType(info); 46 | const files = await CacheFiles.fetchFiles( 47 | cacheType, 48 | cachePath, 49 | info.name, 50 | true, 51 | signal, 52 | progressListener, 53 | ); 54 | 55 | const xteas = await xteasPromise; 56 | 57 | return { 58 | info, 59 | type: cacheType, 60 | files, 61 | xteas, 62 | }; 63 | } 64 | 65 | export type XteaMap = Map; 66 | 67 | export async function fetchXteas(url: RequestInfo, signal?: AbortSignal): Promise { 68 | const resp = await fetch(url, { 69 | signal, 70 | }); 71 | const data: Record = await resp.json(); 72 | return new Map(Object.keys(data).map((key) => [parseInt(key), data[key]])); 73 | } 74 | -------------------------------------------------------------------------------- /src/rs/pathfinder/CollisionStrategy.ts: -------------------------------------------------------------------------------- 1 | import { CollisionFlag } from "./flag/CollisionFlag"; 2 | 3 | export interface CollisionStrategy { 4 | canMove(tileFlag: number, blockFlag: number): boolean; 5 | } 6 | 7 | class NormalCollisionStrategy implements CollisionStrategy { 8 | canMove(tileFlag: number, blockFlag: number): boolean { 9 | return (tileFlag & blockFlag) === 0; 10 | } 11 | } 12 | 13 | class BlockedCollisionStrategy implements CollisionStrategy { 14 | canMove(tileFlag: number, blockFlag: number): boolean { 15 | const flag = blockFlag & ~CollisionFlag.FLOOR; 16 | return (tileFlag & flag) === 0 && (tileFlag & CollisionFlag.FLOOR) !== 0; 17 | } 18 | } 19 | 20 | const BLOCK_MOVEMENT = 21 | CollisionFlag.WALL_NORTH_WEST | 22 | CollisionFlag.WALL_NORTH | 23 | CollisionFlag.WALL_NORTH_EAST | 24 | CollisionFlag.WALL_EAST | 25 | CollisionFlag.WALL_SOUTH_EAST | 26 | CollisionFlag.WALL_SOUTH | 27 | CollisionFlag.WALL_SOUTH_WEST | 28 | CollisionFlag.WALL_WEST | 29 | CollisionFlag.OBJECT; 30 | 31 | const BLOCK_ROUTE = 32 | CollisionFlag.WALL_NORTH_WEST_ROUTE_BLOCKER | 33 | CollisionFlag.WALL_NORTH_ROUTE_BLOCKER | 34 | CollisionFlag.WALL_NORTH_EAST_ROUTE_BLOCKER | 35 | CollisionFlag.WALL_EAST_ROUTE_BLOCKER | 36 | CollisionFlag.WALL_SOUTH_EAST_ROUTE_BLOCKER | 37 | CollisionFlag.WALL_SOUTH_ROUTE_BLOCKER | 38 | CollisionFlag.WALL_SOUTH_WEST_ROUTE_BLOCKER | 39 | CollisionFlag.WALL_WEST_ROUTE_BLOCKER | 40 | CollisionFlag.OBJECT_ROUTE_BLOCKER; 41 | 42 | class LineOfSightBlockFlagCollision implements CollisionStrategy { 43 | canMove(tileFlag: number, blockFlag: number): boolean { 44 | const movementFlags = (blockFlag & BLOCK_MOVEMENT) << 9; 45 | const routeFlags = (blockFlag & BLOCK_ROUTE) >> 13; 46 | const finalBlockFlag = movementFlags | routeFlags; 47 | return (tileFlag & finalBlockFlag) === 0; 48 | } 49 | } 50 | 51 | export const NORMAL_STRATEGY = new NormalCollisionStrategy(); 52 | export const BLOCKED_STATEGY = new BlockedCollisionStrategy(); 53 | export const FLY_STRATEGY = new LineOfSightBlockFlagCollision(); 54 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/InvertOperation.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { TextureGenerator } from "../TextureGenerator"; 3 | import { TextureOperation } from "./TextureOperation"; 4 | 5 | export class InvertOperation extends TextureOperation { 6 | constructor() { 7 | super(1, false); 8 | } 9 | 10 | override decode(field: number, buffer: ByteBuffer): void { 11 | if (field === 0) { 12 | this.isMonochrome = buffer.readUnsignedByte() === 1; 13 | } 14 | } 15 | 16 | override getMonochromeOutput(textureGenerator: TextureGenerator, line: number): Int32Array { 17 | if (!this.monochromeImageCache) { 18 | throw new Error("Monochrome image cache is not initialized"); 19 | } 20 | const output = this.monochromeImageCache.get(line); 21 | if (this.monochromeImageCache.dirty) { 22 | const input = this.getMonochromeInput(textureGenerator, 0, line); 23 | for (let pixel = 0; pixel < textureGenerator.width; pixel++) { 24 | output[pixel] = 4096 - input[pixel]; 25 | } 26 | } 27 | return output; 28 | } 29 | 30 | override getColourOutput(textureGenerator: TextureGenerator, line: number): Int32Array[] { 31 | if (!this.colourImageCache) { 32 | throw new Error("Colour image cache is not initialized"); 33 | } 34 | const output = this.colourImageCache.get(line); 35 | if (this.colourImageCache.dirty) { 36 | const input = this.getColourInput(textureGenerator, 0, line); 37 | const inputR = input[0]; 38 | const inputG = input[1]; 39 | const inputB = input[2]; 40 | const outputR = output[0]; 41 | const outputG = output[1]; 42 | const outputB = output[2]; 43 | for (let pixel = 0; pixel < textureGenerator.width; pixel++) { 44 | outputR[pixel] = 4096 - inputR[pixel]; 45 | outputG[pixel] = 4096 - inputG[pixel]; 46 | outputB[pixel] = 4096 - inputB[pixel]; 47 | } 48 | } 49 | return output; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/util/MathUtil.ts: -------------------------------------------------------------------------------- 1 | import JavaRandom from "java-random"; 2 | 3 | export const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max); 4 | 5 | export const lerp = (start: number, end: number, progress: number) => { 6 | if (progress <= 0) { 7 | return start; 8 | } else if (progress >= 1) { 9 | return end; 10 | } 11 | return start + (end - start) * progress; 12 | }; 13 | 14 | /** 15 | * Circular lerp looping around after its range 16 | * A range of [0, 100] can jump between 100 and 0, so the shortest path will go through through 100 17 | * EX: 10, 2, .5, 10 will return `1`, as `10` is equal to `0` 18 | */ 19 | export const slerp = (start: number, end: number, progress: number, range: number) => { 20 | if (progress <= 0) { 21 | return start; 22 | } else if (progress >= 1) { 23 | return end; 24 | } 25 | const shortest_distance = ((((end - start) % range) + range * 1.5) % range) - range / 2; 26 | const movement = shortest_distance * progress; 27 | return start + movement; 28 | }; 29 | 30 | export function isPowerOfTwo(n: number) { 31 | return n === (-n & n); 32 | } 33 | 34 | export function nextPow2(i: number): number { 35 | i = --i | (i >>> 1); 36 | i |= i >>> 2; 37 | i |= i >>> 4; 38 | i |= i >>> 8; 39 | i |= i >>> 16; 40 | return i + 1; 41 | } 42 | 43 | export function toSigned16bit(n: number) { 44 | return (n << 16) >> 16; 45 | } 46 | 47 | export function nextIntJagex(random: JavaRandom, bound: number): number { 48 | if (bound <= 0) { 49 | throw new Error("bound must be positive"); 50 | } 51 | if (isPowerOfTwo(bound)) { 52 | return Number((BigInt(bound) * (BigInt(random.nextInt()) & 0xffffffffn)) >> 32n); 53 | } 54 | const maxValue = (-0x80000000 - (0x100000000 % bound | 0)) | 0; 55 | let rndValue: number; 56 | do { 57 | rndValue = random.nextInt(); 58 | } while (rndValue >= maxValue); 59 | return boundJagex(rndValue, bound); 60 | } 61 | 62 | function boundJagex(value: number, bound: number): number { 63 | const i_78_ = (value >> 31) & (bound - 1); 64 | return i_78_ + ((value + (value >>> 31)) % bound); 65 | } 66 | -------------------------------------------------------------------------------- /src/rs/graphics/Rasterizer2D.ts: -------------------------------------------------------------------------------- 1 | export class Rasterizer2D { 2 | static pixels: Int32Array; 3 | 4 | static width: number; 5 | static height: number; 6 | 7 | static xClipStart: number; 8 | static yClipStart: number; 9 | static xClipEnd: number; 10 | static yClipEnd: number; 11 | 12 | static setRaster(pixels: Int32Array, width: number, height: number) { 13 | Rasterizer2D.pixels = pixels; 14 | Rasterizer2D.width = width; 15 | Rasterizer2D.height = height; 16 | Rasterizer2D.setClip(0, 0, width, height); 17 | } 18 | 19 | static setClip(x: number, y: number, width: number, height: number) { 20 | if (x < 0) { 21 | x = 0; 22 | } 23 | 24 | if (y < 0) { 25 | y = 0; 26 | } 27 | 28 | if (width > Rasterizer2D.width) { 29 | width = Rasterizer2D.width; 30 | } 31 | 32 | if (height > Rasterizer2D.height) { 33 | height = Rasterizer2D.height; 34 | } 35 | 36 | Rasterizer2D.xClipStart = x; 37 | Rasterizer2D.yClipStart = y; 38 | Rasterizer2D.xClipEnd = width; 39 | Rasterizer2D.yClipEnd = height; 40 | } 41 | 42 | static fillRectangle(x: number, y: number, width: number, height: number, rgb: number) { 43 | if (x < Rasterizer2D.xClipStart) { 44 | width -= Rasterizer2D.xClipStart - x; 45 | x = Rasterizer2D.xClipStart; 46 | } 47 | 48 | if (y < Rasterizer2D.yClipStart) { 49 | height -= Rasterizer2D.yClipStart - y; 50 | y = Rasterizer2D.yClipStart; 51 | } 52 | 53 | if (x + width > Rasterizer2D.xClipEnd) { 54 | width = Rasterizer2D.xClipEnd - x; 55 | } 56 | 57 | if (height + y > Rasterizer2D.yClipEnd) { 58 | height = Rasterizer2D.yClipEnd - y; 59 | } 60 | 61 | const widthOffset = Rasterizer2D.width - width; 62 | let offset = x + Rasterizer2D.width * y; 63 | 64 | for (let h = -height; h < 0; h++) { 65 | for (let w = -width; w < 0; w++) { 66 | Rasterizer2D.pixels[offset++] = rgb; 67 | } 68 | 69 | offset += widthOffset; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/rs/select/OsrsSelect.css: -------------------------------------------------------------------------------- 1 | .osrs-select-container { 2 | min-width: 200px; 3 | /* max-width: 140px; */ 4 | border: 1px solid; 5 | border-color: #0e0e0c; 6 | } 7 | 8 | .osrs-select-control { 9 | border: 1px solid !important; 10 | border-color: #474745 !important; 11 | border-radius: 0 !important; 12 | box-shadow: none !important; 13 | min-height: 0 !important; 14 | background-color: #3e3529 !important; 15 | } 16 | 17 | .osrs-select-value-container { 18 | padding: 0 !important; 19 | text-align: center !important; 20 | font-size: 16px; 21 | font-family: "OSRS Small" !important; 22 | text-shadow: 1px 1px 0 black; 23 | /* text-shadow: 1px 1px 0 black; */ 24 | } 25 | 26 | .osrs-select-input { 27 | margin: 0 !important; 28 | /* padding-top: 1px !important; */ 29 | /* padding-bottom: 1px !important; */ 30 | text-align: center !important; 31 | color: white !important; 32 | /* min-width: 100% !important; */ 33 | } 34 | 35 | .osrs-select-input input { 36 | /* min-width: 100% !important; */ 37 | text-align: center !important; 38 | text-shadow: 1px 1px 0 black; 39 | /* grid-area: a !important; */ 40 | } 41 | 42 | .osrs-select-single-value { 43 | text-align: center !important; 44 | color: #ff981f !important; 45 | } 46 | 47 | .osrs-select-placeholder { 48 | text-align: center; 49 | color: #9f9f9f !important; 50 | /* text-shadow: 1px 1px 0 black; */ 51 | } 52 | 53 | .osrs-select-menu { 54 | border: 1px solid #474745; 55 | outline: 1px solid #0e0e0c; 56 | border-radius: 0 !important; 57 | /* margin: 0 !important; */ 58 | } 59 | 60 | .osrs-select-menu-list { 61 | padding: 0 !important; 62 | margin: 0 !important; 63 | } 64 | 65 | .osrs-select-option { 66 | font-size: 16px; 67 | font-family: "OSRS Small" !important; 68 | text-shadow: 1px 1px 0 black; 69 | color: #ff981f !important; 70 | /* background-color: #3e3529; */ 71 | } 72 | 73 | .osrs-select-option:hover { 74 | background-color: #787169; 75 | } 76 | 77 | .osrs-select-no-options-message { 78 | font-size: 16px; 79 | font-family: "OSRS Small" !important; 80 | text-shadow: 1px 1px 0 black; 81 | color: #ff981f !important; 82 | } 83 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/ColourStripOperation.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { TextureGenerator } from "../TextureGenerator"; 3 | import { TextureOperation } from "./TextureOperation"; 4 | 5 | export class ColourStripOperation extends TextureOperation { 6 | colourR: number = 4096; 7 | colourG: number = 4096; 8 | colourB: number = 4096; 9 | 10 | constructor() { 11 | super(1, false); 12 | } 13 | 14 | override decode(field: number, buffer: ByteBuffer): void { 15 | if (field === 0) { 16 | this.colourR = buffer.readUnsignedShort(); 17 | } else if (field === 1) { 18 | this.colourG = buffer.readUnsignedShort(); 19 | } else if (field === 2) { 20 | this.colourB = buffer.readUnsignedShort(); 21 | } 22 | } 23 | 24 | override getColourOutput(textureGenerator: TextureGenerator, line: number): Int32Array[] { 25 | if (!this.colourImageCache) { 26 | throw new Error("Colour image cache is not initialized"); 27 | } 28 | const output = this.colourImageCache.get(line); 29 | if (this.colourImageCache.dirty) { 30 | const input = this.getColourInput(textureGenerator, 0, line); 31 | const inputR = input[0]; 32 | const inputG = input[1]; 33 | const inputB = input[2]; 34 | const outputR = output[0]; 35 | const outputG = output[1]; 36 | const outputB = output[2]; 37 | for (let pixel = 0; pixel < textureGenerator.width; pixel++) { 38 | const valueR = inputR[pixel]; 39 | const valueG = inputG[pixel]; 40 | const valueB = inputB[pixel]; 41 | if (valueR !== valueB || valueB !== valueG) { 42 | outputR[pixel] = this.colourR; 43 | outputG[pixel] = this.colourG; 44 | outputB[pixel] = this.colourB; 45 | } else { 46 | outputR[pixel] = (this.colourR * valueR) >> 12; 47 | outputG[pixel] = (this.colourG * valueG) >> 12; 48 | outputB[pixel] = (this.colourB * valueB) >> 12; 49 | } 50 | } 51 | } 52 | return output; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/rs/cache/Container.ts: -------------------------------------------------------------------------------- 1 | // import { Xtea } from "../util/Xtea"; 2 | import { Bzip2 } from "../compression/Bzip2"; 3 | import { CompressionType } from "../compression/CompressionType"; 4 | import { Gzip } from "../compression/Gzip"; 5 | import { Xtea } from "../crypto/Xtea"; 6 | import { ByteBuffer } from "../io/ByteBuffer"; 7 | 8 | export class Container { 9 | static decode(buffer: ByteBuffer, key?: number[]): Container { 10 | if (buffer.remaining === 0) { 11 | throw new Error("Empty container"); 12 | } 13 | const compression: CompressionType = buffer.readUnsignedByte(); 14 | const size = buffer.readInt(); 15 | if (Xtea.isValidKey(key)) { 16 | Xtea.decrypt(buffer, buffer.offset, buffer.offset + 4 + size, key); 17 | } 18 | switch (compression) { 19 | case CompressionType.None: 20 | return new Container(compression, buffer.readBytes(size)); 21 | case CompressionType.Bzip2: 22 | case CompressionType.Gzip: 23 | const actualSize = buffer.readInt() & 0xffffffff; 24 | 25 | const data = buffer.readUnsignedBytes(size); 26 | 27 | let decompressed: Int8Array; 28 | 29 | if (compression === CompressionType.Bzip2) { 30 | decompressed = Bzip2.decompress(data, actualSize); 31 | } else { 32 | decompressed = Gzip.decompress(data); 33 | } 34 | 35 | if (decompressed.length !== actualSize) { 36 | throw new Error( 37 | "Container: Size mismatch. Compressed: " + 38 | actualSize + 39 | ", Decompressed: " + 40 | decompressed.length + 41 | ", Type: " + 42 | CompressionType[compression], 43 | ); 44 | } 45 | return new Container(compression, decompressed); 46 | default: 47 | throw new Error("Container: Unsupported compression: " + compression); 48 | } 49 | } 50 | 51 | constructor( 52 | readonly compression: CompressionType, 53 | readonly data: Int8Array, 54 | ) {} 55 | } 56 | -------------------------------------------------------------------------------- /src/rs/config/idktype/IdkType.ts: -------------------------------------------------------------------------------- 1 | import { CacheInfo } from "../../cache/CacheInfo"; 2 | import { ByteBuffer } from "../../io/ByteBuffer"; 3 | import { Type } from "../Type"; 4 | 5 | // Identity Kit 6 | export class IdkType extends Type { 7 | bodyPartyId: number; 8 | 9 | modelIds!: number[]; 10 | 11 | recolorFrom!: number[]; 12 | recolorTo!: number[]; 13 | 14 | retextureFrom!: number[]; 15 | retextureTo!: number[]; 16 | 17 | ifModelIds: number[]; 18 | 19 | nonSelectable: boolean; 20 | 21 | constructor(id: number, cacheInfo: CacheInfo) { 22 | super(id, cacheInfo); 23 | this.bodyPartyId = -1; 24 | this.ifModelIds = [-1, -1, -1, -1, -1]; 25 | this.nonSelectable = false; 26 | } 27 | 28 | override decodeOpcode(opcode: number, buffer: ByteBuffer): void { 29 | if (opcode === 1) { 30 | this.bodyPartyId = buffer.readUnsignedByte(); 31 | } else if (opcode === 2) { 32 | const modelCount = buffer.readUnsignedByte(); 33 | this.modelIds = new Array(modelCount); 34 | for (let i = 0; i < modelCount; i++) { 35 | this.modelIds[i] = buffer.readUnsignedShort(); 36 | } 37 | } else if (opcode === 3) { 38 | this.nonSelectable = true; 39 | } else if (opcode === 40) { 40 | const count = buffer.readUnsignedByte(); 41 | this.recolorFrom = new Array(count); 42 | this.recolorTo = new Array(count); 43 | for (let i = 0; i < count; i++) { 44 | this.recolorFrom[i] = buffer.readUnsignedShort(); 45 | this.recolorTo[i] = buffer.readUnsignedShort(); 46 | } 47 | } else if (opcode === 41) { 48 | const count = buffer.readUnsignedByte(); 49 | this.retextureFrom = new Array(count); 50 | this.retextureTo = new Array(count); 51 | for (let i = 0; i < count; i++) { 52 | this.retextureFrom[i] = buffer.readUnsignedShort(); 53 | this.retextureTo[i] = buffer.readUnsignedShort(); 54 | } 55 | } else if (opcode >= 60 && opcode < 70) { 56 | this.ifModelIds[opcode - 60] = buffer.readUnsignedShort(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/MonochromeEdgeDetectorOperation.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { TextureGenerator } from "../TextureGenerator"; 3 | import { TextureOperation } from "./TextureOperation"; 4 | 5 | export class MonochromeEdgeDetectorOperation extends TextureOperation { 6 | multiplier: number = 4096; 7 | 8 | constructor() { 9 | super(1, true); 10 | } 11 | 12 | override decode(field: number, buffer: ByteBuffer): void { 13 | if (field === 0) { 14 | this.multiplier = buffer.readUnsignedShort(); 15 | } 16 | } 17 | 18 | override getMonochromeOutput(textureGenerator: TextureGenerator, line: number): Int32Array { 19 | if (!this.monochromeImageCache) { 20 | throw new Error("Monochrome image cache is not initialized"); 21 | } 22 | const output = this.monochromeImageCache.get(line); 23 | if (this.monochromeImageCache.dirty) { 24 | const prevInput = this.getMonochromeInput( 25 | textureGenerator, 26 | 0, 27 | (line - 1) & textureGenerator.heightMask, 28 | ); 29 | const input = this.getMonochromeInput(textureGenerator, 0, line); 30 | const nextInput = this.getMonochromeInput( 31 | textureGenerator, 32 | 0, 33 | (line + 1) & textureGenerator.heightMask, 34 | ); 35 | for (let x = 0; x < textureGenerator.width; x++) { 36 | const dy = this.multiplier * (nextInput[x] - prevInput[x]); 37 | const dx = 38 | this.multiplier * 39 | (input[(x + 1) & textureGenerator.widthMask] - 40 | input[(x - 1) & textureGenerator.widthMask]); 41 | const dx0 = dx >> 12; 42 | const dy0 = dy >> 12; 43 | const dySquared = (dy0 * dy0) >> 12; 44 | const dxSquared = (dx0 * dx0) >> 12; 45 | const local117 = (Math.sqrt((dySquared + dxSquared + 4096) / 4096.0) * 4096.0) | 0; 46 | const local128 = local117 == 0 ? 0 : (16777216 / local117) | 0; 47 | output[x] = 4096 - local128; 48 | } 49 | } 50 | return output; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/rs/map/MapFileIndex.ts: -------------------------------------------------------------------------------- 1 | import { Archive } from "../cache/Archive"; 2 | import { CacheIndex } from "../cache/CacheIndex"; 3 | 4 | export function getMapSquareId(mapX: number, mapY: number): number { 5 | return (mapX << 8) + mapY; 6 | } 7 | 8 | export interface MapFileIndex { 9 | getTerrainArchiveId(mapX: number, mapY: number): number; 10 | getLocArchiveId(mapX: number, mapY: number): number; 11 | } 12 | 13 | class MapSquare { 14 | constructor( 15 | readonly mapId: number, 16 | readonly terrainArchiveId: number, 17 | readonly locArchiveId: number, 18 | readonly members: boolean, 19 | ) {} 20 | } 21 | 22 | export class DatMapFileIndex implements MapFileIndex { 23 | static load(versionListArchive: Archive): DatMapFileIndex { 24 | const file = versionListArchive.getFileNamed("map_index"); 25 | if (!file) { 26 | throw new Error("map_index not found"); 27 | } 28 | const buffer = file.getDataAsBuffer(); 29 | 30 | const mapSquares = new Map(); 31 | 32 | const count = (buffer.remaining / 7) | 0; 33 | for (let i = 0; i < count; i++) { 34 | const mapId = buffer.readUnsignedShort(); 35 | const terrainArchiveId = buffer.readUnsignedShort(); 36 | const locArchiveId = buffer.readUnsignedShort(); 37 | const members = buffer.readUnsignedByte() === 1; 38 | mapSquares.set(mapId, new MapSquare(mapId, terrainArchiveId, locArchiveId, members)); 39 | } 40 | 41 | return new DatMapFileIndex(mapSquares); 42 | } 43 | 44 | constructor(readonly mapSquares: Map) {} 45 | 46 | getTerrainArchiveId(mapX: number, mapY: number): number { 47 | return this.mapSquares.get(getMapSquareId(mapX, mapY))?.terrainArchiveId ?? -1; 48 | } 49 | 50 | getLocArchiveId(mapX: number, mapY: number): number { 51 | return this.mapSquares.get(getMapSquareId(mapX, mapY))?.locArchiveId ?? -1; 52 | } 53 | } 54 | 55 | export class Dat2MapIndex implements MapFileIndex { 56 | constructor(readonly mapIndex: CacheIndex) {} 57 | 58 | getTerrainArchiveId(mapX: number, mapY: number): number { 59 | return this.mapIndex.getArchiveId(`m${mapX}_${mapY}`); 60 | } 61 | 62 | getLocArchiveId(mapX: number, mapY: number): number { 63 | return this.mapIndex.getArchiveId(`l${mapX}_${mapY}`); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/rs/texture/procedural/operation/MandelbrotOperation.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from "../../../io/ByteBuffer"; 2 | import { TextureGenerator } from "../TextureGenerator"; 3 | import { TextureOperation } from "./TextureOperation"; 4 | 5 | export class MandelbrotOperation extends TextureOperation { 6 | field0 = 1365; 7 | field1 = 20; 8 | field2 = 0; 9 | field3 = 0; 10 | 11 | constructor() { 12 | super(0, true); 13 | } 14 | 15 | override decode(field: number, buffer: ByteBuffer): void { 16 | if (field === 0) { 17 | this.field0 = buffer.readUnsignedShort(); 18 | } else if (field === 1) { 19 | this.field1 = buffer.readUnsignedShort(); 20 | } else if (field === 2) { 21 | this.field2 = buffer.readUnsignedShort(); 22 | } else if (field === 3) { 23 | this.field3 = buffer.readUnsignedShort(); 24 | } 25 | } 26 | 27 | override getMonochromeOutput(textureGenerator: TextureGenerator, line: number): Int32Array { 28 | if (!this.monochromeImageCache) { 29 | throw new Error("Monochrome image cache is not initialized"); 30 | } 31 | const output = this.monochromeImageCache.get(line); 32 | if (this.monochromeImageCache.dirty) { 33 | for (let x = 0; x < textureGenerator.width; x++) { 34 | const local42 = 35 | (this.field2 + (textureGenerator.horizontalGradient[x] << 12) / this.field0) | 36 | 0; 37 | const local54 = 38 | (this.field3 + (textureGenerator.verticalGradient[line] << 12) / this.field0) | 39 | 0; 40 | let local58 = local54; 41 | let local60 = local42; 42 | let local64 = 0; 43 | let local70 = (local42 * local42) >> 12; 44 | let local76 = (local54 * local54) >> 12; 45 | while (local70 + local76 < 16384 && local64 < this.field1) { 46 | local64++; 47 | local58 = local54 + ((local58 * local60) >> 12) * 2; 48 | local60 = local42 + local70 - local76; 49 | local76 = (local58 * local58) >> 12; 50 | local70 = (local60 * local60) >> 12; 51 | } 52 | output[x] = local64 >= this.field1 - 1 ? 0 : ((local64 << 12) / this.field1) | 0; 53 | } 54 | } 55 | return output; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/rs/select/OsrsSelect.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import Select, { GroupBase, OptionProps, Props, components, createFilter } from "react-select"; 3 | 4 | import "./OsrsSelect.css"; 5 | 6 | function OsrsSelectOption>({ 7 | children, 8 | ...props 9 | }: OptionProps) { 10 | const { onMouseMove, onMouseOver, ...rest } = props.innerProps; 11 | const newProps = { ...props, innerProps: rest }; 12 | return {children}; 13 | } 14 | 15 | export const OsrsSelect = memo(function OsrsSelect< 16 | Option, 17 | IsMulti extends boolean = false, 18 | Group extends GroupBase