├── .prettierignore ├── src ├── io │ ├── OnDemandProvider.ts │ ├── OnDemandRequest.ts │ ├── Jagfile.ts │ ├── ServerProt.ts │ ├── ServerProtSize.ts │ ├── ClientProt.ts │ ├── Database.ts │ ├── Isaac.ts │ ├── ClientStream.ts │ └── Packet.ts ├── 3rdparty │ ├── bzip2-wasm │ │ └── bzip2.wasm │ ├── tinymidipcm │ │ └── tinymidipcm.wasm │ ├── deps.js │ ├── audio.js │ ├── bzip2-wasm.js │ └── tinymidipcm.js ├── dash3d │ ├── LocAngle.ts │ ├── DirectionFlag.ts │ ├── LocLayer.ts │ ├── VertexNormal.ts │ ├── OverlayShape.ts │ ├── LocChange.ts │ ├── ClientObj.ts │ ├── GroundDecor.ts │ ├── Decor.ts │ ├── QuickGround.ts │ ├── GroundObject.ts │ ├── Wall.ts │ ├── AnimBase.ts │ ├── ModelSource.ts │ ├── Occlude.ts │ ├── Sprite.ts │ ├── Square.ts │ ├── CollisionFlag.ts │ ├── ClientLocAnim.ts │ ├── MapSpotAnim.ts │ ├── ClientNpc.ts │ ├── LocShape.ts │ ├── AnimFrame.ts │ ├── ClientProj.ts │ └── ClientEntity.ts ├── datastruct │ ├── Linkable.ts │ ├── DoublyLinkable.ts │ ├── HashTable.ts │ ├── LruCache.ts │ ├── DoublyLinkList.ts │ ├── LinkList.ts │ └── JString.ts ├── graphics │ ├── Canvas.ts │ ├── Colors.ts │ ├── Jpeg.ts │ ├── PixMap.ts │ └── Pix2D.ts ├── client │ ├── MouseTracking.ts │ ├── ClientCode.ts │ ├── KeyCodes.ts │ └── InputTracking.ts ├── util │ ├── JavaRandom.ts │ ├── JsUtil.ts │ └── Arrays.ts ├── config │ ├── ConfigType.ts │ ├── VarpType.ts │ ├── SpotAnimType.ts │ ├── IdkType.ts │ ├── SeqType.ts │ ├── FloType.ts │ └── NpcType.ts ├── sound │ ├── Envelope.ts │ ├── Wave.ts │ └── Tone.ts └── wordenc │ └── WordPack.ts ├── .gitignore ├── .prettierrc ├── .gitmodules ├── README.md ├── rsa.ts ├── package.json ├── tsconfig.json ├── LICENSE └── bundle.ts /.prettierignore: -------------------------------------------------------------------------------- 1 | 3rdparty/* 2 | src/3rdparty/* 3 | -------------------------------------------------------------------------------- /src/io/OnDemandProvider.ts: -------------------------------------------------------------------------------- 1 | export default class OnDemandProvider { 2 | requestModel(id: number) {} 3 | } 4 | -------------------------------------------------------------------------------- /src/3rdparty/bzip2-wasm/bzip2.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostCityRS/Client-TS/HEAD/src/3rdparty/bzip2-wasm/bzip2.wasm -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | package-lock.json 3 | .env 4 | 5 | /out/ 6 | private.pem 7 | public.pem 8 | 9 | .idea 10 | -------------------------------------------------------------------------------- /src/3rdparty/tinymidipcm/tinymidipcm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostCityRS/Client-TS/HEAD/src/3rdparty/tinymidipcm/tinymidipcm.wasm -------------------------------------------------------------------------------- /src/dash3d/LocAngle.ts: -------------------------------------------------------------------------------- 1 | export const enum LocAngle { 2 | WEST = 0, 3 | NORTH = 1, 4 | EAST = 2, 5 | SOUTH = 3 6 | }; 7 | -------------------------------------------------------------------------------- /src/dash3d/DirectionFlag.ts: -------------------------------------------------------------------------------- 1 | export const enum DirectionFlag { 2 | NORTH = 0x1, 3 | EAST = 0x2, 4 | SOUTH = 0x4, 5 | WEST = 0x8 6 | } 7 | -------------------------------------------------------------------------------- /src/dash3d/LocLayer.ts: -------------------------------------------------------------------------------- 1 | export const enum LocLayer { 2 | WALL = 0, 3 | WALL_DECOR = 1, 4 | GROUND = 2, 5 | GROUND_DECOR = 3 6 | } 7 | -------------------------------------------------------------------------------- /src/dash3d/VertexNormal.ts: -------------------------------------------------------------------------------- 1 | export default class VertexNormal { 2 | x: number = 0; 3 | y: number = 0; 4 | z: number = 0; 5 | w: number = 0; 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 250, 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "quoteProps": "as-needed", 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid", 10 | "endOfLine": "auto" 11 | } 12 | -------------------------------------------------------------------------------- /src/io/OnDemandRequest.ts: -------------------------------------------------------------------------------- 1 | import DoublyLinkable from '#/datastruct/DoublyLinkable.js'; 2 | 3 | export default class OnDemandRequest extends DoublyLinkable { 4 | archive: number = 0; 5 | file: number = 0; 6 | data: Uint8Array | null = null; 7 | cycle: number = 0; 8 | urgent: boolean = true; 9 | } 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "3rdparty/tinymidipcm"] 2 | path = 3rdparty/tinymidipcm 3 | url = https://github.com/misterhat/tinymidipcm 4 | [submodule "3rdparty/bzip2-wasm"] 5 | path = 3rdparty/bzip2-wasm 6 | url = https://github.com/misterhat/bzip2-wasm 7 | [submodule "3rdparty/emsdk"] 8 | path = 3rdparty/emsdk 9 | url = https://github.com/emscripten-core/emsdk 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Lost City - July 13, 2004

3 |
4 | 5 | > [!NOTE] 6 | > Learn about our history and ethos on our forum: https://lostcity.rs/t/faq-what-is-lost-city/16 7 | 8 | ## License 9 | 10 | This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). See the [LICENSE](LICENSE) file for details. 11 | -------------------------------------------------------------------------------- /src/dash3d/OverlayShape.ts: -------------------------------------------------------------------------------- 1 | export const enum OverlayShape { 2 | PLAIN = 0, 3 | DIAGONAL = 1, 4 | LEFT_SEMI_DIAGONAL_SMALL = 2, 5 | RIGHT_SEMI_DIAGONAL_SMALL = 3, 6 | LEFT_SEMI_DIAGONAL_BIG = 4, 7 | RIGHT_SEMI_DIAGONAL_BIG = 5, 8 | HALF_SQUARE = 6, 9 | CORNER_SMALL = 7, 10 | CORNER_BIG = 8, 11 | FAN_SMALL = 9, 12 | FAN_BIG = 10, 13 | TRAPEZIUM = 11 14 | } 15 | -------------------------------------------------------------------------------- /src/3rdparty/deps.js: -------------------------------------------------------------------------------- 1 | import { gunzipSync, unzipSync } from 'fflate'; 2 | 3 | import { playWave, setWaveVolume } from '#3rdparty/audio.js'; 4 | import { playMidi, stopMidi, setMidiVolume } from '#3rdparty/tinymidipcm.js'; 5 | import BZip2 from '#3rdparty/bzip2-wasm.js'; 6 | 7 | export { 8 | gunzipSync, 9 | unzipSync, 10 | playWave, 11 | setWaveVolume, 12 | playMidi, 13 | stopMidi, 14 | setMidiVolume, 15 | BZip2 16 | }; 17 | -------------------------------------------------------------------------------- /src/datastruct/Linkable.ts: -------------------------------------------------------------------------------- 1 | export default class Linkable { 2 | key: bigint = 0n; 3 | next: Linkable | null = null; 4 | prev: Linkable | null = null; 5 | 6 | unlink(): void { 7 | if (this.prev != null) { 8 | this.prev.next = this.next; 9 | if (this.next) { 10 | this.next.prev = this.prev; 11 | } 12 | this.next = null; 13 | this.prev = null; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/graphics/Canvas.ts: -------------------------------------------------------------------------------- 1 | export const canvas: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement; 2 | export const canvas2d: CanvasRenderingContext2D = canvas.getContext('2d', { willReadFrequently: true })!; 3 | 4 | export const jpegCanvas: HTMLCanvasElement = document.createElement('canvas'); 5 | export const jpegImg: HTMLImageElement = document.createElement('img'); 6 | export const jpeg2d: CanvasRenderingContext2D = jpegCanvas.getContext('2d', { willReadFrequently: true })!; 7 | -------------------------------------------------------------------------------- /src/dash3d/LocChange.ts: -------------------------------------------------------------------------------- 1 | import Linkable from '#/datastruct/Linkable.js'; 2 | 3 | export default class LocChange extends Linkable { 4 | endTime: number = -1; 5 | 6 | newType: number = 0; 7 | newAngle: number = 0; 8 | newShape: number = 0; 9 | 10 | level: number = 0; 11 | layer: number = 0; 12 | x: number = 0; 13 | z: number = 0; 14 | 15 | oldType: number = 0; 16 | oldAngle: number = 0; 17 | oldShape: number = 0; 18 | 19 | startTime: number = 0; 20 | } 21 | -------------------------------------------------------------------------------- /src/datastruct/DoublyLinkable.ts: -------------------------------------------------------------------------------- 1 | import Linkable from '#/datastruct/Linkable.js'; 2 | 3 | export default class DoublyLinkable extends Linkable { 4 | next2: DoublyLinkable | null = null; 5 | prev2: DoublyLinkable | null = null; 6 | 7 | unlink2(): void { 8 | if (this.prev2 !== null) { 9 | this.prev2.next2 = this.next2; 10 | if (this.next2) { 11 | this.next2.prev2 = this.prev2; 12 | } 13 | this.next2 = null; 14 | this.prev2 = null; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rsa.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import forge from 'node-forge'; 4 | 5 | fs.writeFileSync('.env', ''); 6 | 7 | const key = forge.pki.rsa.generateKeyPair(1024); // RS3/OSRS still uses 1024 today 8 | const privkey = forge.pki.privateKeyToPem(key.privateKey); 9 | const pubkey = forge.pki.publicKeyToPem(key.publicKey); 10 | fs.writeFileSync('private.pem', privkey); 11 | fs.writeFileSync('public.pem', pubkey); 12 | 13 | fs.appendFileSync('.env', 'LOGIN_RSAE=' + key.publicKey.e.toString(10) + '\n'); 14 | fs.appendFileSync('.env', 'LOGIN_RSAN=' + key.publicKey.n.toString(10) + '\n'); 15 | -------------------------------------------------------------------------------- /src/dash3d/ClientObj.ts: -------------------------------------------------------------------------------- 1 | import ObjType from '#/config/ObjType.js'; 2 | import type Model from '#/dash3d/Model.js'; 3 | import ModelSource from '#/dash3d/ModelSource.js'; 4 | 5 | export default class ClientObj extends ModelSource { 6 | readonly index: number; 7 | count: number; 8 | 9 | constructor(index: number, count: number) { 10 | super(); 11 | this.index = index; 12 | this.count = count; 13 | } 14 | 15 | getModel(): Model | null { 16 | const obj = ObjType.get(this.index); 17 | return obj.getModel(this.count); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/MouseTracking.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from '#/client/Client.js'; 2 | 3 | export default class MouseTracking { 4 | app: Client; 5 | active: boolean = false; 6 | length: number = 0; 7 | x: number[] = new Array(500); 8 | y: number[] = new Array(500); 9 | 10 | constructor(app: Client) { 11 | this.app = app; 12 | } 13 | 14 | cycle() { 15 | if (this.length < 500) { 16 | this.x[this.length] = this.app.mouseX; 17 | this.y[this.length] = this.app.mouseY; 18 | this.length++; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/util/JavaRandom.ts: -------------------------------------------------------------------------------- 1 | export default class JavaRandom { 2 | private seed: bigint; 3 | 4 | constructor(seed: bigint) { 5 | this.seed = (seed ^ 0x5deece66dn) & ((1n << 48n) - 1n); 6 | } 7 | 8 | setSeed(seed: bigint): void { 9 | this.seed = (seed ^ 0x5deece66dn) & ((1n << 48n) - 1n); 10 | } 11 | 12 | nextInt(): number { 13 | return this.next(32); 14 | } 15 | 16 | next(bits: number): number { 17 | this.seed = (this.seed * 0x5deece66dn + 0xbn) & ((1n << 48n) - 1n); 18 | return Number(this.seed) >>> (48 - bits); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/dash3d/GroundDecor.ts: -------------------------------------------------------------------------------- 1 | import ModelSource from '#/dash3d/ModelSource.js'; 2 | 3 | export default class GroundDecor { 4 | readonly y: number; 5 | readonly x: number; 6 | readonly z: number; 7 | model: ModelSource | null; 8 | readonly typecode: number; 9 | readonly info: number; // byte 10 | 11 | constructor(y: number, x: number, z: number, model: ModelSource | null, typecode: number, info: number) { 12 | this.y = y; 13 | this.x = x; 14 | this.z = z; 15 | this.model = model; 16 | this.typecode = typecode; 17 | this.info = info; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/config/ConfigType.ts: -------------------------------------------------------------------------------- 1 | import Packet from '#/io/Packet.js'; 2 | 3 | export abstract class ConfigType { 4 | id: number; 5 | debugname: string | null = null; 6 | 7 | constructor(id: number) { 8 | this.id = id; 9 | } 10 | 11 | abstract unpack(code: number, dat: Packet): void; 12 | 13 | unpackType(dat: Packet): this { 14 | // eslint-disable-next-line no-constant-condition 15 | while (true) { 16 | const opcode: number = dat.g1(); 17 | if (opcode === 0) { 18 | break; 19 | } 20 | this.unpack(opcode, dat); 21 | } 22 | return this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/dash3d/Decor.ts: -------------------------------------------------------------------------------- 1 | import ModelSource from '#/dash3d/ModelSource.js'; 2 | 3 | export default class Decor { 4 | readonly y: number; 5 | x: number; 6 | z: number; 7 | readonly angle1: number; 8 | readonly angle2: number; 9 | model: ModelSource; 10 | readonly typecode: number; 11 | readonly info: number; // byte 12 | 13 | constructor(y: number, x: number, z: number, type: number, angle: number, model: ModelSource, typecode: number, info: number) { 14 | this.y = y; 15 | this.x = x; 16 | this.z = z; 17 | this.angle1 = type; 18 | this.angle2 = angle; 19 | this.model = model; 20 | this.typecode = typecode; 21 | this.info = info; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client2", 3 | "module": "src/client/Client.ts", 4 | "type": "module", 5 | "imports": { 6 | "#3rdparty/*": "./src/3rdparty/*", 7 | "#/*": "./src/*" 8 | }, 9 | "scripts": { 10 | "build": "bun run bundle.ts", 11 | "build:dev": "bun run bundle.ts dev" 12 | }, 13 | "devDependencies": { 14 | "@types/bun": "latest", 15 | "@types/node-forge": "^1.3.11", 16 | "@types/uglify-js": "^3.17.5", 17 | "axios": "^1.7.9", 18 | "fflate": "^0.8.2", 19 | "javascript-obfuscator": "^4.1.1", 20 | "node-forge": "^1.3.1", 21 | "prettier": "^3.4.2", 22 | "terser": "^5.37.0" 23 | }, 24 | "peerDependencies": { 25 | "typescript": "^5.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/dash3d/QuickGround.ts: -------------------------------------------------------------------------------- 1 | export default class QuickGround { 2 | readonly southwestColor: number; 3 | readonly southeastColor: number; 4 | readonly northeastColor: number; 5 | readonly northwestColor: number; 6 | readonly textureId: number; 7 | readonly colour: number; 8 | readonly flat: boolean; 9 | 10 | constructor(southwestColor: number, southeastColor: number, northeastColor: number, northwestColor: number, textureId: number, color: number, flat: boolean) { 11 | this.southwestColor = southwestColor; 12 | this.southeastColor = southeastColor; 13 | this.northeastColor = northeastColor; 14 | this.northwestColor = northwestColor; 15 | this.textureId = textureId; 16 | this.colour = color; 17 | this.flat = flat; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/graphics/Colors.ts: -------------------------------------------------------------------------------- 1 | export const enum Colors { 2 | RED = 0xff0000, 3 | GREEN = 0xff00, 4 | BLUE = 0xff, 5 | YELLOW = 0xffff00, 6 | CYAN = 0xffff, 7 | MAGENTA = 0xff00ff, 8 | WHITE = 0xffffff, 9 | BLACK = 0x0, 10 | LIGHTRED = 0xff9040, 11 | DARKRED = 0x800000, 12 | DARKBLUE = 0x80, 13 | ORANGE1 = 0xffb000, 14 | ORANGE2 = 0xff7000, 15 | ORANGE3 = 0xff3000, 16 | GREEN1 = 0xc0ff00, 17 | GREEN2 = 0x80ff00, 18 | GREEN3 = 0x40ff00, 19 | 20 | PROGRESS_RED = 0x8c1111, 21 | OPTIONS_MENU = 0x5d5447, 22 | SCROLLBAR_TRACK = 0x23201b, 23 | SCROLLBAR_GRIP_FOREGROUND = 0x4d4233, 24 | SCROLLBAR_GRIP_HIGHLIGHT = 0x766654, 25 | SCROLLBAR_GRIP_LOWLIGHT = 0x332d25, 26 | TRADE_MESSAGE = 0x800080, 27 | DUEL_MESSAGE = 0x7e3200 28 | } 29 | -------------------------------------------------------------------------------- /src/dash3d/GroundObject.ts: -------------------------------------------------------------------------------- 1 | import ModelSource from '#/dash3d/ModelSource.js'; 2 | 3 | export default class GroundObject { 4 | readonly y: number; 5 | readonly x: number; 6 | readonly z: number; 7 | readonly topObj: ModelSource | null; 8 | readonly middleObj: ModelSource | null; 9 | readonly bottomObj: ModelSource | null; 10 | readonly typecode: number; 11 | readonly offset: number; 12 | 13 | constructor(y: number, x: number, z: number, topObj: ModelSource | null, middleObj: ModelSource | null, bottomObj: ModelSource | null, typecode: number, offset: number) { 14 | this.y = y; 15 | this.x = x; 16 | this.z = z; 17 | this.topObj = topObj; 18 | this.middleObj = middleObj; 19 | this.bottomObj = bottomObj; 20 | this.typecode = typecode; 21 | this.offset = offset; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/dash3d/Wall.ts: -------------------------------------------------------------------------------- 1 | import ModelSource from '#/dash3d/ModelSource.js'; 2 | 3 | export default class Wall { 4 | readonly y: number; 5 | readonly x: number; 6 | readonly z: number; 7 | readonly angle1: number; 8 | readonly angle2: number; 9 | model1: ModelSource | null; 10 | model2: ModelSource | null; 11 | readonly typecode: number; 12 | readonly typecode2: number; 13 | 14 | constructor(y: number, x: number, z: number, angle1: number, angle2: number, model1: ModelSource | null, model2: ModelSource | null, typecode: number, typecode2: number) { 15 | this.y = y; 16 | this.x = x; 17 | this.z = z; 18 | this.angle1 = angle1; 19 | this.angle2 = angle2; 20 | this.model1 = model1; 21 | this.model2 = model2; 22 | this.typecode = typecode; 23 | this.typecode2 = typecode2; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/dash3d/AnimBase.ts: -------------------------------------------------------------------------------- 1 | import Packet from '#/io/Packet.js'; 2 | 3 | import { TypedArray1d } from '#/util/Arrays.js'; 4 | 5 | export default class AnimBase { 6 | length: number = 0; 7 | types: Uint8Array | null = null; 8 | labels: (Uint8Array | null)[] | null = null; 9 | 10 | constructor(buf: Packet) { 11 | this.length = buf.g1(); 12 | 13 | this.types = new Uint8Array(this.length); 14 | this.labels = new TypedArray1d(this.length, null); 15 | 16 | for (let i = 0; i < this.length; i++) { 17 | this.types[i] = buf.g1(); 18 | } 19 | 20 | for (let i = 0; i < this.length; i++) { 21 | const count = buf.g1(); 22 | this.labels[i] = new Uint8Array(count); 23 | 24 | for (let j = 0; j < count; j++) { 25 | this.labels[i]![j] = buf.g1(); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/dash3d/ModelSource.ts: -------------------------------------------------------------------------------- 1 | import DoublyLinkable from '#/datastruct/DoublyLinkable.js'; 2 | import type VertexNormal from '#/dash3d/VertexNormal.js'; 3 | import type Model from '#/dash3d/Model.js'; 4 | 5 | export default class ModelSource extends DoublyLinkable { 6 | public vertexNormal: (VertexNormal | null)[] | null = null; 7 | public minY: number = 1000; 8 | 9 | draw(loopCycle: number, yaw: number, sinEyePitch: number, cosEyePitch: number, sinEyeYaw: number, cosEyeYaw: number, relativeX: number, relativeY: number, relativeZ: number, typecode: number): void { 10 | const model = this.getModel(loopCycle); 11 | if (model) { 12 | this.minY = model.minY; 13 | model.draw(0, yaw, sinEyePitch, cosEyePitch, sinEyeYaw, cosEyeYaw, relativeX, relativeY, relativeZ, typecode); 14 | } 15 | } 16 | 17 | getModel(_loopCycle: number): Model | null { 18 | return null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | "baseUrl": "src", 12 | "paths": { 13 | "#3rdparty/*": ["3rdparty/*"], 14 | "#/*": ["*"] 15 | }, 16 | 17 | // Bundler mode 18 | "moduleResolution": "bundler", 19 | "allowImportingTsExtensions": true, 20 | "verbatimModuleSyntax": true, 21 | "noEmit": true, 22 | 23 | // Best practices 24 | "strict": true, 25 | "skipLibCheck": true, 26 | "noFallthroughCasesInSwitch": true, 27 | 28 | // Some stricter flags (disabled by default) 29 | "noUnusedLocals": false, 30 | "noUnusedParameters": false, 31 | "noPropertyAccessFromIndexSignature": false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/graphics/Jpeg.ts: -------------------------------------------------------------------------------- 1 | import { jpeg2d, jpegCanvas, jpegImg } from '#/graphics/Canvas.js'; 2 | 3 | export async function decodeJpeg(data: Uint8Array): Promise { 4 | if (data[0] !== 0xff) { 5 | // fix invalid JPEG header 6 | data[0] = 0xff; 7 | } 8 | 9 | URL.revokeObjectURL(jpegImg.src); // Remove previous decoded jpeg. 10 | jpegImg.src = URL.createObjectURL(new Blob([data], { type: 'image/jpeg' })); 11 | 12 | // wait for img to load 13 | await new Promise((resolve): (() => void) => (jpegImg.onload = (): void => resolve())); 14 | 15 | // Clear the canvas before drawing 16 | jpeg2d.clearRect(0, 0, jpegCanvas.width, jpegCanvas.height); 17 | 18 | const width: number = jpegImg.naturalWidth; 19 | const height: number = jpegImg.naturalHeight; 20 | jpegCanvas.width = width; 21 | jpegCanvas.height = height; 22 | 23 | // Draw the image 24 | jpeg2d.drawImage(jpegImg, 0, 0); 25 | return jpeg2d.getImageData(0, 0, width, height); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Lost City 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. 22 | -------------------------------------------------------------------------------- /src/dash3d/Occlude.ts: -------------------------------------------------------------------------------- 1 | export default class Occlude { 2 | // constructor 3 | readonly minTileX: number; 4 | readonly maxTileX: number; 5 | readonly minTileZ: number; 6 | readonly maxTileZ: number; 7 | readonly type: number; 8 | readonly minX: number; 9 | readonly maxX: number; 10 | readonly minZ: number; 11 | readonly maxZ: number; 12 | readonly minY: number; 13 | readonly maxY: number; 14 | 15 | // runtime 16 | mode: number = 0; 17 | minDeltaX: number = 0; 18 | maxDeltaX: number = 0; 19 | minDeltaZ: number = 0; 20 | maxDeltaZ: number = 0; 21 | minDeltaY: number = 0; 22 | maxDeltaY: number = 0; 23 | 24 | constructor(minTileX: number, maxTileX: number, minTileZ: number, maxTileZ: number, type: number, minX: number, maxX: number, minZ: number, maxZ: number, minY: number, maxY: number) { 25 | this.minTileX = minTileX; 26 | this.maxTileX = maxTileX; 27 | this.minTileZ = minTileZ; 28 | this.maxTileZ = maxTileZ; 29 | this.type = type; 30 | this.minX = minX; 31 | this.maxX = maxX; 32 | this.minZ = minZ; 33 | this.maxZ = maxZ; 34 | this.minY = minY; 35 | this.maxY = maxY; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/dash3d/Sprite.ts: -------------------------------------------------------------------------------- 1 | import type ModelSource from '#/dash3d/ModelSource.js'; 2 | 3 | export default class Sprite { 4 | // constructor 5 | readonly locLevel: number; 6 | readonly y: number; 7 | readonly x: number; 8 | readonly z: number; 9 | model: ModelSource | null; 10 | readonly yaw: number; 11 | readonly minSceneTileX: number; 12 | readonly maxSceneTileX: number; 13 | readonly minSceneTileZ: number; 14 | readonly maxSceneTileZ: number; 15 | readonly typecode: number; 16 | readonly info: number; // byte 17 | 18 | // runtime 19 | distance: number = 0; 20 | cycle: number = 0; 21 | 22 | constructor(level: number, y: number, x: number, z: number, model: ModelSource | null, yaw: number, minSceneTileX: number, maxSceneTileX: number, minSceneTileZ: number, maxSceneTileZ: number, typecode: number, info: number) { 23 | this.locLevel = level; 24 | this.y = y; 25 | this.x = x; 26 | this.z = z; 27 | this.model = model; 28 | this.yaw = yaw; 29 | this.minSceneTileX = minSceneTileX; 30 | this.maxSceneTileX = maxSceneTileX; 31 | this.minSceneTileZ = minSceneTileZ; 32 | this.maxSceneTileZ = maxSceneTileZ; 33 | this.typecode = typecode; 34 | this.info = info; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/datastruct/HashTable.ts: -------------------------------------------------------------------------------- 1 | import Linkable from '#/datastruct/Linkable.js'; 2 | 3 | export default class HashTable { 4 | readonly bucketCount: number; 5 | readonly buckets: Linkable[]; 6 | 7 | constructor(size: number) { 8 | this.buckets = new Array(size); 9 | this.bucketCount = size; 10 | for (let i: number = 0; i < size; i++) { 11 | const sentinel = (this.buckets[i] = new Linkable()); 12 | sentinel.next = sentinel; 13 | sentinel.prev = sentinel; 14 | } 15 | } 16 | 17 | get(key: bigint): Linkable | null { 18 | const start: Linkable = this.buckets[Number(key & BigInt(this.bucketCount - 1))]; 19 | 20 | for (let node: Linkable | null = start.next; node !== start; node = node.next) { 21 | if (!node) { 22 | continue; 23 | } 24 | if (node.key === key) { 25 | return node; 26 | } 27 | } 28 | 29 | return null; 30 | } 31 | 32 | put(key: bigint, value: Linkable): void { 33 | if (value.prev) { 34 | value.unlink(); 35 | } 36 | 37 | const sentinel: Linkable = this.buckets[Number(key & BigInt(this.bucketCount - 1))]; 38 | value.prev = sentinel.prev; 39 | value.next = sentinel; 40 | if (value.prev) { 41 | value.prev.next = value; 42 | } 43 | value.next.prev = value; 44 | value.key = key; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/util/JsUtil.ts: -------------------------------------------------------------------------------- 1 | export const sleep = async (ms: number): Promise => new Promise((resolve): NodeJS.Timeout => setTimeout(resolve, ms)); 2 | 3 | export const downloadUrl = async (url: string): Promise => new Uint8Array(await (await fetch(url)).arrayBuffer()); 4 | 5 | export const downloadText = async (url: string): Promise => (await fetch(url)).text(); 6 | 7 | export function arraycopy(src: Int32Array | Uint8Array, srcPos: number, dst: Int32Array | Uint8Array, dstPos: number, length: number): void { 8 | while (length--) dst[dstPos++] = src[srcPos++]; 9 | } 10 | 11 | export function bytesToBigInt(bytes: Uint8Array): bigint { 12 | let result: bigint = 0n; 13 | for (let index: number = 0; index < bytes.length; index++) { 14 | result = (result << 8n) | BigInt(bytes[index]); 15 | } 16 | return result; 17 | } 18 | 19 | export function bigIntToBytes(bigInt: bigint): Uint8Array { 20 | const bytes: number[] = []; 21 | while (bigInt > 0n) { 22 | bytes.unshift(Number(bigInt & 0xffn)); 23 | bigInt >>= 8n; 24 | } 25 | 26 | if (bytes[0] & 0x80) { 27 | bytes.unshift(0); 28 | } 29 | 30 | return new Uint8Array(bytes); 31 | } 32 | 33 | export function bigIntModPow(base: bigint, exponent: bigint, modulus: bigint): bigint { 34 | let result: bigint = 1n; 35 | while (exponent > 0n) { 36 | if (exponent % 2n === 1n) { 37 | result = (result * base) % modulus; 38 | } 39 | base = (base * base) % modulus; 40 | exponent >>= 1n; 41 | } 42 | return result; 43 | } 44 | -------------------------------------------------------------------------------- /src/graphics/PixMap.ts: -------------------------------------------------------------------------------- 1 | import { canvas2d } from '#/graphics/Canvas.js'; 2 | import Pix2D from '#/graphics/Pix2D.js'; 3 | 4 | export default class PixMap { 5 | private readonly image: ImageData; 6 | private readonly width2d: number; 7 | private readonly height2d: number; 8 | private readonly ctx: CanvasRenderingContext2D; 9 | private readonly paint: Uint32Array; 10 | readonly pixels: Int32Array; 11 | 12 | constructor(width: number, height: number, ctx: CanvasRenderingContext2D = canvas2d) { 13 | this.ctx = ctx; 14 | this.image = this.ctx.getImageData(0, 0, width, height); 15 | this.paint = new Uint32Array(this.image.data.buffer); 16 | this.pixels = new Int32Array(width * height); 17 | this.width2d = width; 18 | this.height2d = height; 19 | this.bind(); 20 | } 21 | 22 | clear(): void { 23 | this.pixels.fill(0); 24 | } 25 | 26 | bind(): void { 27 | Pix2D.bind(this.pixels, this.width2d, this.height2d); 28 | } 29 | 30 | draw(x: number, y: number): void { 31 | this.#setPixels(); 32 | this.ctx.putImageData(this.image, x, y); 33 | } 34 | 35 | #setPixels(): void { 36 | const length: number = this.pixels.length; 37 | const pixels: Int32Array = this.pixels; 38 | const paint: Uint32Array = this.paint; 39 | for (let i: number = 0; i < length; i++) { 40 | const pixel: number = pixels[i]; 41 | paint[i] = ((pixel >> 16) & 0xff) | (((pixel >> 8) & 0xff) << 8) | ((pixel & 0xff) << 16) | 0xff000000; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/datastruct/LruCache.ts: -------------------------------------------------------------------------------- 1 | import DoublyLinkable from '#/datastruct/DoublyLinkable.js'; 2 | import HashTable from '#/datastruct/HashTable.js'; 3 | import DoublyLinkList from '#/datastruct/DoublyLinkList.js'; 4 | 5 | export default class LruCache { 6 | readonly capacity: number; 7 | readonly hashtable: HashTable = new HashTable(1024); 8 | readonly cacheHistory: DoublyLinkList = new DoublyLinkList(); 9 | cacheAvailable: number; 10 | 11 | constructor(size: number) { 12 | this.capacity = size; 13 | this.cacheAvailable = size; 14 | } 15 | 16 | get(key: bigint): DoublyLinkable | null { 17 | const node: DoublyLinkable | null = this.hashtable.get(key) as DoublyLinkable | null; 18 | if (node) { 19 | this.cacheHistory.push(node); 20 | } 21 | return node; 22 | } 23 | 24 | put(key: bigint, value: DoublyLinkable): void { 25 | if (this.cacheAvailable === 0) { 26 | const node: DoublyLinkable | null = this.cacheHistory.pop(); 27 | node?.unlink(); 28 | node?.unlink2(); 29 | } else { 30 | this.cacheAvailable--; 31 | } 32 | this.hashtable.put(key, value); 33 | this.cacheHistory.push(value); 34 | } 35 | 36 | clear(): void { 37 | // eslint-disable-next-line no-constant-condition 38 | while (true) { 39 | const node: DoublyLinkable | null = this.cacheHistory.pop(); 40 | if (!node) { 41 | this.cacheAvailable = this.capacity; 42 | return; 43 | } 44 | node.unlink(); 45 | node.unlink2(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/dash3d/Square.ts: -------------------------------------------------------------------------------- 1 | import Linkable from '#/datastruct/Linkable.js'; 2 | 3 | import GroundDecor from '#/dash3d/GroundDecor.js'; 4 | import Sprite from '#/dash3d/Sprite.js'; 5 | import GroundObject from '#/dash3d/GroundObject.js'; 6 | import Ground from '#/dash3d/Ground.js'; 7 | import QuickGround from '#/dash3d/QuickGround.js'; 8 | import Wall from '#/dash3d/Wall.js'; 9 | import Decor from '#/dash3d/Decor.js'; 10 | 11 | import { TypedArray1d } from '#/util/Arrays.js'; 12 | 13 | export default class Square extends Linkable { 14 | // constructor 15 | level: number; 16 | readonly x: number; 17 | readonly z: number; 18 | readonly originalLevel: number; 19 | readonly locs: (Sprite | null)[]; 20 | readonly primaryExtendDirections: Int32Array; 21 | 22 | // runtime 23 | quickGround: QuickGround | null = null; 24 | ground: Ground | null = null; 25 | wall: Wall | null = null; 26 | decor: Decor | null = null; 27 | groundDecor: GroundDecor | null = null; 28 | groundObject: GroundObject | null = null; 29 | linkedSquare: Square | null = null; 30 | primaryCount: number = 0; 31 | combinedPrimaryExtendDirections: number = 0; 32 | drawLevel: number = 0; 33 | drawFront: boolean = false; 34 | drawBack: boolean = false; 35 | drawPrimaries: boolean = false; 36 | cornerSides: number = 0; 37 | sidesBeforeCorner: number = 0; 38 | sidesAfterCorner: number = 0; 39 | backWallTypes: number = 0; 40 | 41 | constructor(level: number, x: number, z: number) { 42 | super(); 43 | this.originalLevel = this.level = level; 44 | this.x = x; 45 | this.z = z; 46 | this.locs = new TypedArray1d(5, null); 47 | this.primaryExtendDirections = new Int32Array(5); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/dash3d/CollisionFlag.ts: -------------------------------------------------------------------------------- 1 | export const enum CollisionFlag { 2 | OPEN = 0x0, 3 | WALL_NORTH_WEST = 0x1, 4 | WALL_NORTH = 0x2, 5 | WALL_NORTH_EAST = 0x4, 6 | WALL_EAST = 0x8, 7 | WALL_SOUTH_EAST = 0x10, 8 | WALL_SOUTH = 0x20, 9 | WALL_SOUTH_WEST = 0x40, 10 | WALL_WEST = 0x80, 11 | 12 | LOC = 0x100, 13 | WALL_NORTH_WEST_PROJ_BLOCKER = 0x200, 14 | WALL_NORTH_PROJ_BLOCKER = 0x400, 15 | WALL_NORTH_EAST_PROJ_BLOCKER = 0x800, 16 | WALL_EAST_PROJ_BLOCKER = 0x1000, 17 | WALL_SOUTH_EAST_PROJ_BLOCKER = 0x2000, 18 | WALL_SOUTH_PROJ_BLOCKER = 0x4000, 19 | WALL_SOUTH_WEST_PROJ_BLOCKER = 0x8000, 20 | WALL_WEST_PROJ_BLOCKER = 0x10000, 21 | LOC_PROJ_BLOCKER = 0x20000, 22 | 23 | ANTIMACRO = 0x80000, 24 | FLOOR = 0x200000, 25 | 26 | FLOOR_BLOCKED = 0x280000, // CollisionFlag.FLOOR | CollisionFlag.ANTIMACRO 27 | WALK_BLOCKED = 0x280100, // CollisionFlag.LOC | CollisionFlag.FLOOR_BLOCKED 28 | 29 | BLOCK_SOUTH = 0x280102, // CollisionFlag.WALL_NORTH | CollisionFlag.WALK_BLOCKED 30 | BLOCK_WEST = 0x280108, // CollisionFlag.WALL_EAST | CollisionFlag.WALK_BLOCKED 31 | BLOCK_SOUTH_WEST = 0x28010E, // CollisionFlag.WALL_NORTH | CollisionFlag.WALL_NORTH_EAST | CollisionFlag.BLOCK_WEST 32 | BLOCK_NORTH = 0x280120, // CollisionFlag.WALL_SOUTH | CollisionFlag.WALK_BLOCKED 33 | BLOCK_NORTH_WEST = 0x280138, // CollisionFlag.WALL_EAST | CollisionFlag.WALL_SOUTH_EAST | CollisionFlag.BLOCK_NORTH 34 | BLOCK_EAST = 0x280180, // CollisionFlag.WALL_WEST | CollisionFlag.WALK_BLOCKED 35 | BLOCK_SOUTH_EAST = 0x280183, // CollisionFlag.WALL_NORTH_WEST | CollisionFlag.WALL_NORTH | CollisionFlag.BLOCK_EAST 36 | BLOCK_NORTH_EAST = 0x2801E0, // CollisionFlag.WALL_SOUTH | CollisionFlag.WALL_SOUTH_WEST | CollisionFlag.BLOCK_EAST 37 | 38 | BOUNDS = 0xffffff 39 | } 40 | -------------------------------------------------------------------------------- /src/datastruct/DoublyLinkList.ts: -------------------------------------------------------------------------------- 1 | import DoublyLinkable from '#/datastruct/DoublyLinkable.js'; 2 | 3 | export default class DoublyLinkList { 4 | readonly sentinel: DoublyLinkable = new DoublyLinkable(); 5 | cursor: DoublyLinkable | null = null; 6 | 7 | constructor() { 8 | this.sentinel.next2 = this.sentinel; 9 | this.sentinel.prev2 = this.sentinel; 10 | } 11 | 12 | push(node: DoublyLinkable): void { 13 | if (node.prev2) { 14 | node.unlink2(); 15 | } 16 | 17 | node.prev2 = this.sentinel.prev2; 18 | node.next2 = this.sentinel; 19 | if (node.prev2) { 20 | node.prev2.next2 = node; 21 | } 22 | node.next2.prev2 = node; 23 | } 24 | 25 | pop(): DoublyLinkable | null { 26 | const node: DoublyLinkable | null = this.sentinel.next2; 27 | if (node === this.sentinel) { 28 | return null; 29 | } else { 30 | node?.unlink2(); 31 | return node; 32 | } 33 | } 34 | 35 | head() { 36 | const node: DoublyLinkable | null = this.sentinel.next2; 37 | if (node === this.sentinel) { 38 | this.cursor = null; 39 | return null; 40 | } 41 | 42 | this.cursor = node?.next2 || null; 43 | return node; 44 | } 45 | 46 | next() { 47 | const node: DoublyLinkable | null = this.cursor; 48 | if (node === this.sentinel) { 49 | this.cursor = null; 50 | return null; 51 | } 52 | 53 | this.cursor = node?.next2 || null; 54 | return node; 55 | } 56 | 57 | size() { 58 | let count = 0; 59 | for (let node = this.sentinel.next2; node !== this.sentinel && node; node = node.next2) { 60 | count++; 61 | } 62 | return count; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/config/VarpType.ts: -------------------------------------------------------------------------------- 1 | import { ConfigType } from '#/config/ConfigType.js'; 2 | 3 | import Jagfile from '#/io/Jagfile.js'; 4 | import Packet from '#/io/Packet.js'; 5 | 6 | export default class VarpType extends ConfigType { 7 | static count: number = 0; 8 | static types: VarpType[] = []; 9 | static code3s: number[] = []; 10 | static code3Count: number = 0; 11 | code1: number = 0; 12 | code2: number = 0; 13 | code3: boolean = false; 14 | code4: boolean = true; 15 | clientcode: number = 0; 16 | code7: number = 0; 17 | code6: boolean = false; 18 | code8: boolean = false; 19 | code11: boolean = false; 20 | 21 | static unpack(config: Jagfile): void { 22 | const dat: Packet = new Packet(config.read('varp.dat')); 23 | this.count = dat.g2(); 24 | for (let i: number = 0; i < this.count; i++) { 25 | this.types[i] = new VarpType(i).unpackType(dat); 26 | } 27 | } 28 | 29 | unpack(code: number, dat: Packet): void { 30 | if (code === 1) { 31 | this.code1 = dat.g1(); 32 | } else if (code === 2) { 33 | this.code2 = dat.g1(); 34 | } else if (code === 3) { 35 | this.code3 = true; 36 | VarpType.code3s[VarpType.code3Count++] = this.id; 37 | } else if (code === 4) { 38 | this.code4 = false; 39 | } else if (code === 5) { 40 | this.clientcode = dat.g2(); 41 | } else if (code === 6) { 42 | this.code6 = true; 43 | } else if (code === 7) { 44 | this.code7 = dat.g4(); 45 | } else if (code === 8) { 46 | this.code8 = true; 47 | this.code11 = true; 48 | } else if (code === 10) { 49 | this.debugname = dat.gjstr(); 50 | } else if (code === 11) { 51 | this.code11 = true; 52 | } else { 53 | console.log('Error unrecognised config code: ', code); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/sound/Envelope.ts: -------------------------------------------------------------------------------- 1 | import Packet from '#/io/Packet.js'; 2 | 3 | export default class Envelope { 4 | private length: number = 0; 5 | private shapeDelta: Int32Array | null = null; 6 | private shapePeak: Int32Array | null = null; 7 | start: number = 0; 8 | end: number = 0; 9 | form: number = 0; 10 | private threshold: number = 0; 11 | private position: number = 0; 12 | private delta: number = 0; 13 | private amplitude: number = 0; 14 | private ticks: number = 0; 15 | 16 | unpack(dat: Packet): void { 17 | this.form = dat.g1(); 18 | this.start = dat.g4(); 19 | this.end = dat.g4(); 20 | this.length = dat.g1(); 21 | 22 | this.shapeDelta = new Int32Array(this.length); 23 | this.shapePeak = new Int32Array(this.length); 24 | 25 | for (let i: number = 0; i < this.length; i++) { 26 | this.shapeDelta[i] = dat.g2(); 27 | this.shapePeak[i] = dat.g2(); 28 | } 29 | } 30 | 31 | reset(): void { 32 | this.threshold = 0; 33 | this.position = 0; 34 | this.delta = 0; 35 | this.amplitude = 0; 36 | this.ticks = 0; 37 | } 38 | 39 | evaluate(delta: number): number { 40 | if (this.ticks >= this.threshold && this.shapePeak && this.shapeDelta) { 41 | this.amplitude = this.shapePeak[this.position++] << 15; 42 | 43 | if (this.position >= this.length) { 44 | this.position = this.length - 1; 45 | } 46 | 47 | this.threshold = ((this.shapeDelta[this.position] / 65536.0) * delta) | 0; 48 | if (this.threshold > this.ticks) { 49 | this.delta = (((this.shapePeak[this.position] << 15) - this.amplitude) / (this.threshold - this.ticks)) | 0; 50 | } 51 | } 52 | 53 | this.amplitude += this.delta; 54 | this.ticks++; 55 | return (this.amplitude - this.delta) >> 15; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/client/ClientCode.ts: -------------------------------------------------------------------------------- 1 | export const enum ClientCode { 2 | //// friends (1-203) 3 | CC_FRIENDS_START = 1, 4 | CC_FRIENDS_END = 100, 5 | CC_FRIENDS_UPDATE_START = 101, 6 | CC_FRIENDS_UPDATE_END = 200, 7 | CC_ADD_FRIEND = 201, 8 | CC_DEL_FRIEND = 202, 9 | CC_FRIENDS_SIZE = 203, 10 | 11 | //// logout 12 | CC_LOGOUT = 205, 13 | 14 | //// player design (300-327) 15 | CC_CHANGE_HEAD_L = 300, 16 | CC_CHANGE_HEAD_R = 301, 17 | CC_CHANGE_JAW_L = 302, 18 | CC_CHANGE_JAW_R = 303, 19 | CC_CHANGE_TORSO_L = 304, 20 | CC_CHANGE_TORSO_R = 305, 21 | CC_CHANGE_ARMS_L = 306, 22 | CC_CHANGE_ARMS_R = 307, 23 | CC_CHANGE_HANDS_L = 308, 24 | CC_CHANGE_HANDS_R = 309, 25 | CC_CHANGE_LEGS_L = 310, 26 | CC_CHANGE_LEGS_R = 311, 27 | CC_CHANGE_FEET_L = 312, 28 | CC_CHANGE_FEET_R = 313, 29 | CC_RECOLOUR_HAIR_L = 314, 30 | CC_RECOLOUR_HAIR_R = 315, 31 | CC_RECOLOUR_TORSO_L = 316, 32 | CC_RECOLOUR_TORSO_R = 317, 33 | CC_RECOLOUR_LEGS_L = 318, 34 | CC_RECOLOUR_LEGS_R = 319, 35 | CC_RECOLOUR_FEET_L = 320, 36 | CC_RECOLOUR_FEET_R = 321, 37 | CC_RECOLOUR_SKIN_L = 322, 38 | CC_RECOLOUR_SKIN_R = 323, 39 | CC_SWITCH_TO_MALE = 324, 40 | CC_SWITCH_TO_FEMALE = 325, 41 | CC_ACCEPT_DESIGN = 326, 42 | CC_DESIGN_PREVIEW = 327, 43 | 44 | //// ignores (401-503) 45 | CC_IGNORES_START = 401, 46 | CC_IGNORES_END = 500, 47 | CC_ADD_IGNORE = 501, 48 | CC_DEL_IGNORE = 502, 49 | CC_IGNORES_SIZE = 503, 50 | 51 | //// reportabuse (600-613) 52 | CC_REPORT_INPUT = 600, 53 | CC_REPORT_RULE1 = 601, 54 | CC_REPORT_RULE2 = 602, 55 | CC_REPORT_RULE3 = 603, 56 | CC_REPORT_RULE4 = 604, 57 | CC_REPORT_RULE5 = 605, 58 | CC_REPORT_RULE6 = 606, 59 | CC_REPORT_RULE7 = 607, 60 | CC_REPORT_RULE8 = 608, 61 | CC_REPORT_RULE9 = 609, 62 | CC_REPORT_RULE10 = 610, 63 | CC_REPORT_RULE11 = 611, 64 | CC_REPORT_RULE12 = 612, 65 | CC_MOD_MUTE = 613, 66 | 67 | //// welcome_screen/welcome_screen2 (650-655)? 68 | CC_LAST_LOGIN_INFO = 650, // has recovery questions 69 | CC_UNREAD_MESSAGES = 651, 70 | CC_RECOVERY1 = 652, 71 | CC_RECOVERY2 = 653, 72 | CC_RECOVERY3 = 654, 73 | CC_LAST_LOGIN_INFO2 = 655, // has no recovery questions 74 | }; 75 | -------------------------------------------------------------------------------- /src/3rdparty/audio.js: -------------------------------------------------------------------------------- 1 | // Fix iOS Audio Context by Blake Kus https://gist.github.com/kus/3f01d60569eeadefe3a1 2 | // MIT license 3 | (function () { 4 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 5 | if (window.AudioContext) { 6 | window.audioContext = new window.AudioContext(); 7 | } 8 | var fixAudioContext = function (e) { 9 | if (window.audioContext) { 10 | // Create empty buffer 11 | var buffer = window.audioContext.createBuffer(1, 1, 22050); 12 | var source = window.audioContext.createBufferSource(); 13 | source.buffer = buffer; 14 | // Connect to output (speakers) 15 | source.connect(window.audioContext.destination); 16 | // Play sound 17 | if (source.start) { 18 | source.start(0); 19 | } else if (source.play) { 20 | source.play(0); 21 | } else if (source.noteOn) { 22 | source.noteOn(0); 23 | } 24 | } 25 | // Remove events 26 | document.removeEventListener('touchstart', fixAudioContext); 27 | document.removeEventListener('touchend', fixAudioContext); 28 | document.removeEventListener('click', fixAudioContext); 29 | }; 30 | // iOS 6-8 31 | document.addEventListener('touchstart', fixAudioContext); 32 | // iOS 9 33 | document.addEventListener('touchend', fixAudioContext); 34 | // Safari 35 | document.addEventListener('click', fixAudioContext); 36 | document.addEventListener('visibilitychange', () => { 37 | if (!document.hidden) { 38 | if (window.audioContext) { 39 | window.audioContext.resume(); 40 | } 41 | } 42 | }); 43 | })(); 44 | 45 | let waveGain; 46 | 47 | export async function playWave(data) { 48 | try { 49 | const audioBuffer = await window.audioContext.decodeAudioData(new Uint8Array(data).buffer); 50 | let bufferSource = window.audioContext.createBufferSource(); 51 | bufferSource.buffer = audioBuffer; 52 | bufferSource.connect(waveGain); 53 | bufferSource.start(); 54 | } catch (err) { 55 | console.error(err); 56 | } 57 | } 58 | 59 | export function setWaveVolume(vol) { 60 | if (!waveGain) { 61 | waveGain = window.audioContext.createGain(); 62 | waveGain.connect(window.audioContext.destination); 63 | } 64 | 65 | waveGain.gain.value = vol / 128; 66 | } 67 | -------------------------------------------------------------------------------- /bundle.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | const define = { 5 | 'process.env.SECURE_ORIGIN': JSON.stringify(process.env.SECURE_ORIGIN ?? 'false'), 6 | // original key, used 2003-2010 7 | 'process.env.LOGIN_RSAE': JSON.stringify(process.env.LOGIN_RSAE ?? '58778699976184461502525193738213253649000149147835990136706041084440742975821'), 8 | 'process.env.LOGIN_RSAN': JSON.stringify(process.env.LOGIN_RSAN ?? '7162900525229798032761816791230527296329313291232324290237849263501208207972894053929065636522363163621000728841182238772712427862772219676577293600221789') 9 | }; 10 | 11 | // ---- 12 | 13 | type BunOutput = { 14 | source: string; 15 | sourcemap: string; 16 | } 17 | 18 | async function bunBuild(entry: string, external: string[] = [], minify = true, drop: string[] = []): Promise { 19 | const build = await Bun.build({ 20 | entrypoints: [entry], 21 | sourcemap: 'external', 22 | define, 23 | external, 24 | minify, 25 | drop, 26 | }); 27 | 28 | if (!build.success) { 29 | build.logs.forEach(x => console.log(x)); 30 | process.exit(1); 31 | } 32 | 33 | return { 34 | source: await build.outputs[0].text(), 35 | sourcemap: build.outputs[0].sourcemap ? await build.outputs[0].sourcemap.text() : '' 36 | }; 37 | } 38 | 39 | // todo: workaround due to a bun bug https://github.com/oven-sh/bun/issues/16509: not remapping external 40 | function replaceDepsUrl(source: string) { 41 | return source.replaceAll('#3rdparty', '.'); 42 | } 43 | 44 | // ---- 45 | 46 | if (!fs.existsSync('out')) { 47 | fs.mkdirSync('out'); 48 | } 49 | 50 | fs.copyFileSync('src/3rdparty/bzip2-wasm/bzip2.wasm', 'out/bzip2.wasm'); 51 | fs.copyFileSync('src/3rdparty/tinymidipcm/tinymidipcm.wasm', 'out/tinymidipcm.wasm'); 52 | 53 | const args = process.argv.slice(2); 54 | const prod = args[0] !== 'dev'; 55 | 56 | const entrypoints = [ 57 | 'src/client/Client.ts', 58 | 'src/mapview/MapView.ts' 59 | ]; 60 | 61 | const deps = await bunBuild('./src/3rdparty/deps.js', [], true, ['console']); 62 | fs.writeFileSync('out/deps.js', deps.source); 63 | 64 | for (const file of entrypoints) { 65 | const output = path.basename(file).replace('.ts', '.js').toLowerCase(); 66 | 67 | const script = await bunBuild(file, ['#3rdparty/*'], prod, prod ? ['console'] : []); 68 | 69 | if (script) { 70 | fs.writeFileSync('out/' + output, replaceDepsUrl(script.source)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/dash3d/ClientLocAnim.ts: -------------------------------------------------------------------------------- 1 | import LocType from '#/config/LocType.js'; 2 | import SeqType from '#/config/SeqType.js'; 3 | import type Model from '#/dash3d/Model.js'; 4 | 5 | import ModelSource from '#/dash3d/ModelSource.js'; 6 | 7 | export default class ClientLocAnim extends ModelSource { 8 | readonly index: number; 9 | readonly shape: number; 10 | readonly angle: number; 11 | readonly heightmapSW: number; 12 | readonly heightmapSE: number; 13 | readonly heightmapNE: number; 14 | readonly heightmapNW: number; 15 | seq: SeqType | null; 16 | seqFrame: number; 17 | seqCycle: number; 18 | 19 | constructor(loopCycle: number, index: number, shape: number, angle: number, heightmapSW: number, heightmapSE: number, heightmapNE: number, heightmapNW: number, seq: number, randomFrame: boolean) { 20 | super(); 21 | 22 | this.index = index; 23 | this.shape = shape; 24 | this.angle = angle; 25 | this.heightmapSW = heightmapSW; 26 | this.heightmapSE = heightmapSE; 27 | this.heightmapNE = heightmapNE; 28 | this.heightmapNW = heightmapNW; 29 | 30 | this.seq = SeqType.types[seq]; 31 | this.seqFrame = 0; 32 | this.seqCycle = loopCycle; 33 | 34 | if (randomFrame && this.seq.loops !== -1) { 35 | this.seqFrame = (Math.random() * this.seq.frameCount) | 0; 36 | this.seqCycle -= (Math.random() * this.seq.getFrameDuration(this.seqFrame)) | 0; 37 | } 38 | } 39 | 40 | getModel(loopCycle: number): Model | null { 41 | if (this.seq) { 42 | let delta = loopCycle - this.seqCycle; 43 | if (delta > 100 && this.seq.loops > 0) { 44 | delta = 100; 45 | } 46 | 47 | while (delta > this.seq.getFrameDuration(this.seqFrame)) { 48 | delta -= this.seq.getFrameDuration((this.seqFrame)); 49 | this.seqFrame++; 50 | 51 | if (this.seqFrame < this.seq.frameCount) { 52 | continue; 53 | } 54 | 55 | this.seqFrame -= this.seq.loops; 56 | 57 | if (this.seqFrame < 0 || this.seqFrame >= this.seq.frameCount) { 58 | this.seq = null; 59 | break; 60 | } 61 | } 62 | 63 | this.seqCycle = loopCycle - delta; 64 | } 65 | 66 | let transformId = -1; 67 | if (this.seq && this.seq.frames && typeof this.seq.frames[this.seqFrame] !== 'undefined') { 68 | transformId = this.seq.frames[this.seqFrame]; 69 | } 70 | 71 | const loc = LocType.get(this.index); 72 | return loc.getModel(this.shape, this.angle, this.heightmapSW, this.heightmapSE, this.heightmapNE, this.heightmapNW, transformId); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/dash3d/MapSpotAnim.ts: -------------------------------------------------------------------------------- 1 | import SpotAnimType from '#/config/SpotAnimType.js'; 2 | import Model from '#/dash3d/Model.js'; 3 | 4 | import ModelSource from '#/dash3d/ModelSource.js'; 5 | 6 | export default class MapSpotAnim extends ModelSource { 7 | readonly spotType: SpotAnimType; 8 | readonly spotLevel: number; 9 | readonly x: number; 10 | readonly z: number; 11 | readonly y: number; 12 | readonly startCycle: number; 13 | 14 | // runtime 15 | seqComplete: boolean = false; 16 | seqFrame: number = 0; 17 | seqCycle: number = 0; 18 | 19 | constructor(id: number, level: number, x: number, z: number, y: number, cycle: number, delay: number) { 20 | super(); 21 | 22 | this.spotType = SpotAnimType.types[id]; 23 | this.spotLevel = level; 24 | this.x = x; 25 | this.z = z; 26 | this.y = y; 27 | this.startCycle = cycle + delay; 28 | } 29 | 30 | update(delta: number): void { 31 | if (!this.spotType.seq) { 32 | return; 33 | } 34 | 35 | for (this.seqCycle += delta; this.seqCycle > this.spotType.seq.getFrameDuration(this.seqFrame); ) { 36 | this.seqCycle -= this.spotType.seq.getFrameDuration(this.seqFrame) + 1; 37 | this.seqFrame++; 38 | 39 | if (this.seqFrame >= this.spotType.seq.frameCount) { 40 | this.seqFrame = 0; 41 | this.seqComplete = true; 42 | } 43 | } 44 | } 45 | 46 | getModel(): Model | null { 47 | const tmp: Model | null = this.spotType.getModel(); 48 | if (!tmp) { 49 | return null; 50 | } 51 | 52 | const model: Model = Model.modelShareColored(tmp, true, !this.spotType.animHasAlpha, false); 53 | 54 | if (!this.seqComplete && this.spotType.seq && this.spotType.seq.frames) { 55 | model.createLabelReferences(); 56 | model.applyTransform(this.spotType.seq.frames[this.seqFrame]); 57 | model.labelFaces = null; 58 | model.labelVertices = null; 59 | } 60 | 61 | if (this.spotType.resizeh !== 128 || this.spotType.resizev !== 128) { 62 | model.scale(this.spotType.resizeh, this.spotType.resizev, this.spotType.resizeh); 63 | } 64 | 65 | if (this.spotType.angle !== 0) { 66 | if (this.spotType.angle === 90) { 67 | model.rotateY90(); 68 | } else if (this.spotType.angle === 180) { 69 | model.rotateY90(); 70 | model.rotateY90(); 71 | } else if (this.spotType.angle === 270) { 72 | model.rotateY90(); 73 | model.rotateY90(); 74 | model.rotateY90(); 75 | } 76 | } 77 | 78 | model.calculateNormals(64 + this.spotType.ambient, 850 + this.spotType.contrast, -30, -50, -30, true); 79 | return model; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/io/Jagfile.ts: -------------------------------------------------------------------------------- 1 | import { BZip2 } from '#3rdparty/deps.js'; 2 | 3 | import Packet from '#/io/Packet.js'; 4 | 5 | export default class Jagfile { 6 | static genHash(name: string): number { 7 | let hash: number = 0; 8 | name = name.toUpperCase(); 9 | for (let i: number = 0; i < name.length; i++) { 10 | hash = (hash * 61 + name.charCodeAt(i) - 32) | 0; // wtf? 11 | } 12 | return hash; 13 | } 14 | jagSrc: Uint8Array; 15 | compressedWhole: boolean; 16 | fileCount: number; 17 | fileHash: number[]; 18 | fileUnpackedSize: number[]; 19 | filePackedSize: number[]; 20 | fileOffset: number[]; 21 | fileUnpacked: Uint8Array[] = []; 22 | 23 | constructor(src: Uint8Array) { 24 | let data: Packet = new Packet(new Uint8Array(src)); 25 | const unpackedSize: number = data.g3(); 26 | const packedSize: number = data.g3(); 27 | 28 | if (unpackedSize === packedSize) { 29 | this.jagSrc = src; 30 | this.compressedWhole = false; 31 | } else { 32 | this.jagSrc = BZip2.decompress(src.subarray(6), unpackedSize, true); 33 | data = new Packet(new Uint8Array(this.jagSrc)); 34 | this.compressedWhole = true; 35 | } 36 | 37 | this.fileCount = data.g2(); 38 | this.fileHash = []; 39 | this.fileUnpackedSize = []; 40 | this.filePackedSize = []; 41 | this.fileOffset = []; 42 | 43 | let offset: number = data.pos + this.fileCount * 10; 44 | for (let i: number = 0; i < this.fileCount; i++) { 45 | this.fileHash.push(data.g4()); 46 | this.fileUnpackedSize.push(data.g3()); 47 | this.filePackedSize.push(data.g3()); 48 | this.fileOffset.push(offset); 49 | offset += this.filePackedSize[i]; 50 | } 51 | } 52 | 53 | read(name: string): Uint8Array | null { 54 | const hash: number = Jagfile.genHash(name); 55 | const index: number = this.fileHash.indexOf(hash); 56 | if (index === -1) { 57 | return null; 58 | } 59 | return this.readIndex(index); 60 | } 61 | 62 | readIndex(index: number): Uint8Array | null { 63 | if (index < 0 || index >= this.fileCount) { 64 | return null; 65 | } 66 | 67 | if (this.fileUnpacked[index]) { 68 | return this.fileUnpacked[index]; 69 | } 70 | 71 | const offset: number = this.fileOffset[index]; 72 | const length: number = this.filePackedSize[index]; 73 | const src: Uint8Array = new Uint8Array(this.jagSrc.subarray(offset, offset + length)); 74 | if (this.compressedWhole) { 75 | this.fileUnpacked[index] = src; 76 | return src; 77 | } else { 78 | const data: Uint8Array = BZip2.decompress(src, this.fileUnpackedSize[index], true); 79 | this.fileUnpacked[index] = data; 80 | return data; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/datastruct/LinkList.ts: -------------------------------------------------------------------------------- 1 | import Linkable from './Linkable'; 2 | 3 | export default class LinkList { 4 | private readonly sentinel: Linkable = new Linkable(); 5 | private cursor: Linkable | null = null; 6 | 7 | constructor() { 8 | this.sentinel.next = this.sentinel; 9 | this.sentinel.prev = this.sentinel; 10 | } 11 | 12 | push(node: Linkable): void { 13 | if (node.prev) { 14 | node.unlink(); 15 | } 16 | 17 | node.prev = this.sentinel.prev; 18 | node.next = this.sentinel; 19 | if (node.prev) { 20 | node.prev.next = node; 21 | } 22 | node.next.prev = node; 23 | } 24 | 25 | addHead(node: Linkable): void { 26 | if (node.prev) { 27 | node.unlink(); 28 | } 29 | 30 | node.prev = this.sentinel; 31 | node.next = this.sentinel.next; 32 | node.prev.next = node; 33 | if (node.next) { 34 | node.next.prev = node; 35 | } 36 | } 37 | 38 | pop(): Linkable | null { 39 | const node: Linkable | null = this.sentinel.next; 40 | if (node === this.sentinel) { 41 | return null; 42 | } 43 | 44 | node?.unlink(); 45 | return node; 46 | } 47 | 48 | head(): Linkable | null { 49 | const node: Linkable | null = this.sentinel.next; 50 | if (node === this.sentinel) { 51 | this.cursor = null; 52 | return null; 53 | } 54 | 55 | this.cursor = node?.next || null; 56 | return node; 57 | } 58 | 59 | tail(): Linkable | null { 60 | const node: Linkable | null = this.sentinel.prev; 61 | if (node === this.sentinel) { 62 | this.cursor = null; 63 | return null; 64 | } 65 | 66 | this.cursor = node?.prev || null; 67 | return node; 68 | } 69 | 70 | next(): Linkable | null { 71 | const node: Linkable | null = this.cursor; 72 | if (node === this.sentinel) { 73 | this.cursor = null; 74 | return null; 75 | } 76 | 77 | this.cursor = node?.next || null; 78 | return node; 79 | } 80 | 81 | prev(): Linkable | null { 82 | const node: Linkable | null = this.cursor; 83 | if (node === this.sentinel) { 84 | this.cursor = null; 85 | return null; 86 | } 87 | 88 | this.cursor = node?.prev || null; 89 | return node; 90 | } 91 | 92 | clear(): void { 93 | // eslint-disable-next-line no-constant-condition 94 | while (true) { 95 | const node: Linkable | null = this.sentinel.next; 96 | if (node === this.sentinel) { 97 | return; 98 | } 99 | 100 | node?.unlink(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/config/SpotAnimType.ts: -------------------------------------------------------------------------------- 1 | import { ConfigType } from '#/config/ConfigType.js'; 2 | import SeqType from '#/config/SeqType.js'; 3 | 4 | import LruCache from '#/datastruct/LruCache.js'; 5 | 6 | import Model from '#/dash3d/Model.js'; 7 | 8 | import Jagfile from '#/io/Jagfile.js'; 9 | import Packet from '#/io/Packet.js'; 10 | 11 | export default class SpotAnimType extends ConfigType { 12 | static count: number = 0; 13 | static types: SpotAnimType[] = []; 14 | model: number = 0; 15 | anim: number = -1; 16 | seq: SeqType | null = null; 17 | animHasAlpha: boolean = false; 18 | recol_s: Uint16Array = new Uint16Array(6); 19 | recol_d: Uint16Array = new Uint16Array(6); 20 | resizeh: number = 128; 21 | resizev: number = 128; 22 | angle: number = 0; 23 | ambient: number = 0; 24 | contrast: number = 0; 25 | static modelCache: LruCache | null = new LruCache(30); 26 | 27 | static unpack(config: Jagfile): void { 28 | const dat: Packet = new Packet(config.read('spotanim.dat')); 29 | this.count = dat.g2(); 30 | for (let i: number = 0; i < this.count; i++) { 31 | this.types[i] = new SpotAnimType(i).unpackType(dat); 32 | } 33 | } 34 | 35 | unpack(code: number, dat: Packet): void { 36 | if (code === 1) { 37 | this.model = dat.g2(); 38 | } else if (code === 2) { 39 | this.anim = dat.g2(); 40 | 41 | if (SeqType.types) { 42 | this.seq = SeqType.types[this.anim]; 43 | } 44 | } else if (code === 3) { 45 | this.animHasAlpha = true; 46 | } else if (code === 4) { 47 | this.resizeh = dat.g2(); 48 | } else if (code === 5) { 49 | this.resizev = dat.g2(); 50 | } else if (code === 6) { 51 | this.angle = dat.g2(); 52 | } else if (code === 7) { 53 | this.ambient = dat.g1(); 54 | } else if (code === 8) { 55 | this.contrast = dat.g1(); 56 | } else if (code >= 40 && code < 50) { 57 | this.recol_s[code - 40] = dat.g2(); 58 | } else if (code >= 50 && code < 60) { 59 | this.recol_d[code - 50] = dat.g2(); 60 | } else { 61 | console.log('Error unrecognised spotanim config code: ', code); 62 | } 63 | } 64 | 65 | getModel(): Model | null { 66 | let model: Model | null = null; 67 | 68 | if (SpotAnimType.modelCache) { 69 | model = SpotAnimType.modelCache.get(BigInt(this.id)) as Model | null; 70 | 71 | if (model) { 72 | return model; 73 | } 74 | } 75 | 76 | model = Model.tryGet(this.model); 77 | if (!model) { 78 | return null; 79 | } 80 | 81 | for (let i: number = 0; i < 6; i++) { 82 | if (this.recol_s[0] !== 0) { 83 | model.recolour(this.recol_s[i], this.recol_d[i]); 84 | } 85 | } 86 | 87 | if (SpotAnimType.modelCache) { 88 | SpotAnimType.modelCache.put(BigInt(this.id), model); 89 | } 90 | 91 | return model; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/util/Arrays.ts: -------------------------------------------------------------------------------- 1 | export class TypedArray1d extends Array { 2 | constructor(length: number, defaultValue: T) { 3 | super(length); 4 | for (let l: number = 0; l < length; l++) { 5 | this[l] = defaultValue; 6 | } 7 | } 8 | } 9 | 10 | export class TypedArray2d extends Array> { 11 | constructor(length: number, width: number, defaultValue: T) { 12 | super(length); 13 | for (let l: number = 0; l < length; l++) { 14 | this[l] = new Array(width); 15 | for (let w: number = 0; w < width; w++) { 16 | this[l][w] = defaultValue; 17 | } 18 | } 19 | } 20 | } 21 | 22 | export class TypedArray3d extends Array>> { 23 | constructor(length: number, width: number, height: number, defaultValue: T) { 24 | super(length); 25 | for (let l: number = 0; l < length; l++) { 26 | this[l] = new Array(width); 27 | for (let w: number = 0; w < width; w++) { 28 | this[l][w] = new Array(height); 29 | for (let h: number = 0; h < height; h++) { 30 | this[l][w][h] = defaultValue; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | export class TypedArray4d extends Array>>> { 38 | constructor(length: number, width: number, height: number, space: number, defaultValue: T) { 39 | super(length); 40 | for (let l: number = 0; l < length; l++) { 41 | this[l] = new Array(width); 42 | for (let w: number = 0; w < width; w++) { 43 | this[l][w] = new Array(height); 44 | for (let h: number = 0; h < height; h++) { 45 | this[l][w][h] = new Array(space); 46 | for (let s: number = 0; s < space; s++) { 47 | this[l][w][h][s] = defaultValue; 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | export class Uint8Array3d extends Array> { 56 | constructor(length: number, width: number, height: number) { 57 | super(length); 58 | for (let l: number = 0; l < length; l++) { 59 | this[l] = new Array(width); 60 | for (let w: number = 0; w < width; w++) { 61 | this[l][w] = new Uint8Array(height); 62 | } 63 | } 64 | } 65 | } 66 | 67 | export class Int32Array2d extends Array { 68 | constructor(length: number, width: number) { 69 | super(length); 70 | for (let l: number = 0; l < length; l++) { 71 | this[l] = new Int32Array(width); 72 | } 73 | } 74 | } 75 | 76 | export class Int32Array3d extends Array> { 77 | constructor(length: number, width: number, height: number) { 78 | super(length); 79 | for (let l: number = 0; l < length; l++) { 80 | this[l] = new Array(width); 81 | for (let w: number = 0; w < width; w++) { 82 | this[l][w] = new Int32Array(height); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/io/ServerProt.ts: -------------------------------------------------------------------------------- 1 | export const enum ServerProt { 2 | // interfaces 3 | IF_OPENCHAT = 7, 4 | IF_OPENMAIN_SIDE = 229, 5 | IF_CLOSE = 174, 6 | IF_SETTAB = 29, 7 | IF_OPENMAIN = 177, 8 | IF_OPENSIDE = 236, 9 | IF_SETTAB_ACTIVE = 8, 10 | 11 | // updating interfaces 12 | IF_SETCOLOUR = 135, 13 | IF_SETHIDE = 225, 14 | IF_SETOBJECT = 153, 15 | IF_SETMODEL = 60, 16 | IF_SETANIM = 69, 17 | IF_SETPLAYERHEAD = 83, 18 | IF_SETTEXT = 32, 19 | IF_SETNPCHEAD = 76, 20 | IF_SETPOSITION = 230, 21 | IF_SETSCROLLPOS = 226, 22 | 23 | // tutorial area 24 | TUT_FLASH = 132, 25 | TUT_OPEN = 152, 26 | 27 | // inventory 28 | UPDATE_INV_STOP_TRANSMIT = 143, 29 | UPDATE_INV_FULL = 156, 30 | UPDATE_INV_PARTIAL = 95, 31 | 32 | // camera control 33 | CAM_LOOKAT = 123, 34 | CAM_SHAKE = 103, 35 | CAM_MOVETO = 86, 36 | CAM_RESET = 134, 37 | 38 | // entity updates 39 | NPC_INFO = 105, 40 | PLAYER_INFO = 161, 41 | 42 | // input tracking 43 | FINISH_TRACKING = 165, 44 | ENABLE_TRACKING = 28, 45 | 46 | // social 47 | MESSAGE_GAME = 175, 48 | UPDATE_IGNORELIST = 181, 49 | CHAT_FILTER_SETTINGS = 2, 50 | MESSAGE_PRIVATE = 207, 51 | UPDATE_FRIENDLIST = 109, 52 | 53 | // misc 54 | UNSET_MAP_FLAG = 233, 55 | UPDATE_RUNWEIGHT = 70, 56 | HINT_ARROW = 243, 57 | UPDATE_REBOOT_TIMER = 26, 58 | UPDATE_STAT = 110, 59 | UPDATE_RUNENERGY = 208, 60 | RESET_ANIMS = 144, 61 | UPDATE_PID = 49, 62 | LAST_LOGIN_INFO = 238, 63 | LOGOUT = 36, 64 | P_COUNTDIALOG = 56, 65 | SET_MULTIWAY = 35, 66 | 67 | // maps 68 | REBUILD_NORMAL = 66, 69 | 70 | // vars 71 | VARP_SMALL = 192, 72 | VARP_LARGE = 75, 73 | RESET_CLIENT_VARCACHE = 25, 74 | 75 | // audio 76 | SYNTH_SOUND = 209, 77 | MIDI_SONG = 96, 78 | MIDI_JINGLE = 39, 79 | 80 | // zones 81 | UPDATE_ZONE_PARTIAL_FOLLOWS = 203, 82 | UPDATE_ZONE_FULL_FOLLOWS = 140, 83 | UPDATE_ZONE_PARTIAL_ENCLOSED = 15, 84 | 85 | // zone protocol 86 | LOC_MERGE = 188, 87 | LOC_ANIM = 71, 88 | OBJ_DEL = 13, 89 | OBJ_REVEAL = 190, 90 | LOC_ADD_CHANGE = 119, 91 | MAP_PROJANIM = 187, 92 | LOC_DEL = 198, 93 | OBJ_COUNT = 151, 94 | MAP_ANIM = 141, 95 | OBJ_ADD = 94 96 | }; 97 | 98 | // prettier-ignore 99 | export const ServerProtSizes = [ 100 | 0, 0, 3, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 3, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101 | 2, 0, 0, 3, 0, 0, -2, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 102 | 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 4, 2, 4, 0, 0, 0, 6, 4, 0, 0, 0, 103 | 0, 0, 0, 2, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 5, -2, 2, 0, 0, 0, 104 | 0, 0, 0, 4, 0, -2, 0, 0, 0, 9, 6, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 105 | 0, 0, 0, 0, 0, 0, 1, 0, 0, 4, 0, 0, 0, 0, 2, 6, 0, 2, 0, 0, 0, 0, 0, 0, 0, 7, 2, 106 | 6, 0, 0, -2, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 2, 0, 107 | 0, 0, -2, 0, 0, 0, 0, 0, 15, 14, 0, 7, 0, 3, 0, 0, 0, 0, 0, 2, 0, 108 | 0, 0, 0, 2, 0, 0, 0, -1, 1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 109 | 4, 0, 0, 4, 6, 0, 0, 0, 0, 0, 2, 0, 10, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 110 | 0, 0 111 | ]; 112 | -------------------------------------------------------------------------------- /src/io/ServerProtSize.ts: -------------------------------------------------------------------------------- 1 | import { ServerProt } from '#/io/ServerProt.ts'; 2 | 3 | const lengths: number[] = []; 4 | lengths[ServerProt.IF_OPENCHAT] = 2; 5 | lengths[ServerProt.IF_OPENMAIN_SIDE] = 4; 6 | lengths[ServerProt.IF_CLOSE] = 0; 7 | lengths[ServerProt.IF_SETTAB] = 3; 8 | lengths[ServerProt.IF_SETTAB_ACTIVE] = 1; 9 | lengths[ServerProt.IF_OPENMAIN] = 2; 10 | lengths[ServerProt.IF_OPENSIDE] = 2; 11 | 12 | lengths[ServerProt.IF_SETCOLOUR] = 4; 13 | lengths[ServerProt.IF_SETHIDE] = 3; 14 | lengths[ServerProt.IF_SETOBJECT] = 6; 15 | lengths[ServerProt.IF_SETMODEL] = 4; 16 | lengths[ServerProt.IF_SETANIM] = 4; 17 | lengths[ServerProt.IF_SETPLAYERHEAD] = 2; 18 | lengths[ServerProt.IF_SETTEXT] = -2; 19 | lengths[ServerProt.IF_SETNPCHEAD] = 4; 20 | lengths[ServerProt.IF_SETPOSITION] = 6; 21 | lengths[ServerProt.IF_SETSCROLLPOS] = 4; 22 | 23 | lengths[ServerProt.TUT_FLASH] = 1; 24 | lengths[ServerProt.TUT_OPEN] = 2; 25 | 26 | lengths[ServerProt.UPDATE_INV_STOP_TRANSMIT] = 2; 27 | lengths[ServerProt.UPDATE_INV_FULL] = -2; 28 | lengths[ServerProt.UPDATE_INV_PARTIAL] = -2; 29 | 30 | lengths[ServerProt.CAM_LOOKAT] = 6; 31 | lengths[ServerProt.CAM_SHAKE] = 4; 32 | lengths[ServerProt.CAM_MOVETO] = 6; 33 | lengths[ServerProt.CAM_RESET] = 0; 34 | 35 | lengths[ServerProt.NPC_INFO] = -2; 36 | lengths[ServerProt.PLAYER_INFO] = -2; 37 | 38 | lengths[ServerProt.FINISH_TRACKING] = 0; 39 | lengths[ServerProt.ENABLE_TRACKING] = 0; 40 | 41 | lengths[ServerProt.MESSAGE_GAME] = -1; 42 | lengths[ServerProt.UPDATE_IGNORELIST] = -2; 43 | lengths[ServerProt.CHAT_FILTER_SETTINGS] = 3; 44 | lengths[ServerProt.MESSAGE_PRIVATE] = -1; 45 | lengths[ServerProt.UPDATE_FRIENDLIST] = 9; 46 | 47 | lengths[ServerProt.UNSET_MAP_FLAG] = 0; 48 | lengths[ServerProt.UPDATE_RUNWEIGHT] = 2; 49 | lengths[ServerProt.HINT_ARROW] = 6; 50 | lengths[ServerProt.UPDATE_REBOOT_TIMER] = 2; 51 | lengths[ServerProt.UPDATE_STAT] = 6; 52 | lengths[ServerProt.UPDATE_RUNENERGY] = 1; 53 | lengths[ServerProt.RESET_ANIMS] = 0; 54 | lengths[ServerProt.UPDATE_PID] = 3; 55 | lengths[ServerProt.LAST_LOGIN_INFO] = 10; 56 | lengths[ServerProt.LOGOUT] = 0; 57 | lengths[ServerProt.P_COUNTDIALOG] = 0; 58 | lengths[ServerProt.SET_MULTIWAY] = 1; 59 | 60 | lengths[ServerProt.REBUILD_NORMAL] = 4; 61 | 62 | lengths[ServerProt.VARP_SMALL] = 3; 63 | lengths[ServerProt.VARP_LARGE] = 6; 64 | lengths[ServerProt.RESET_CLIENT_VARCACHE] = 0; 65 | 66 | lengths[ServerProt.SYNTH_SOUND] = 5; 67 | lengths[ServerProt.MIDI_SONG] = 2; 68 | lengths[ServerProt.MIDI_JINGLE] = 4; 69 | 70 | lengths[ServerProt.UPDATE_ZONE_PARTIAL_FOLLOWS] = 2; 71 | lengths[ServerProt.UPDATE_ZONE_FULL_FOLLOWS] = 2; 72 | lengths[ServerProt.UPDATE_ZONE_PARTIAL_ENCLOSED] = -2; 73 | 74 | lengths[ServerProt.LOC_MERGE] = 14; 75 | lengths[ServerProt.LOC_ANIM] = 4; 76 | lengths[ServerProt.OBJ_DEL] = 3; 77 | lengths[ServerProt.OBJ_REVEAL] = 7; 78 | lengths[ServerProt.LOC_ADD_CHANGE] = 4; 79 | lengths[ServerProt.MAP_PROJANIM] = 15; 80 | lengths[ServerProt.LOC_DEL] = 2; 81 | lengths[ServerProt.OBJ_COUNT] = 7; 82 | lengths[ServerProt.MAP_ANIM] = 6; 83 | lengths[ServerProt.OBJ_ADD] = 5; 84 | 85 | const organized = []; 86 | for (let i = 0; i < 255; i++) { 87 | if (typeof lengths[i] !== 'undefined') { 88 | organized[i] = lengths[i]; 89 | } else { 90 | organized[i] = 0; 91 | } 92 | } 93 | 94 | console.log(organized.slice(0, 100)); 95 | console.log(organized.slice(100, 200)); 96 | console.log(organized.slice(200)); 97 | -------------------------------------------------------------------------------- /src/wordenc/WordPack.ts: -------------------------------------------------------------------------------- 1 | import Packet from '#/io/Packet.js'; 2 | 3 | export default class WordPack { 4 | // prettier-ignore 5 | private static TABLE: string[] = [ 6 | ' ', 7 | 'e', 't', 'a', 'o', 'i', 'h', 'n', 's', 'r', 'd', 'l', 'u', 'm', 8 | 'w', 'c', 'y', 'f', 'g', 'p', 'b', 'v', 'k', 'x', 'j', 'q', 'z', 9 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 10 | ' ', '!', '?', '.', ',', ':', ';', '(', ')', '-', 11 | '&', '*', '\\', '\'', '@', '#', '+', '=', '£', '$', '%', '"', '[', ']' 12 | ]; 13 | 14 | private static charBuffer: string[] = []; 15 | 16 | static unpack(word: Packet, length: number): string { 17 | let pos: number = 0; 18 | let carry: number = -1; 19 | let nibble: number; 20 | for (let index: number = 0; index < length && pos < 100; index++) { 21 | const value: number = word.g1(); 22 | nibble = (value >> 4) & 0xf; 23 | if (carry !== -1) { 24 | this.charBuffer[pos++] = this.TABLE[(carry << 4) + nibble - 195]; 25 | carry = -1; 26 | } else if (nibble < 13) { 27 | this.charBuffer[pos++] = this.TABLE[nibble]; 28 | } else { 29 | carry = nibble; 30 | } 31 | nibble = value & 0xf; 32 | if (carry !== -1) { 33 | this.charBuffer[pos++] = this.TABLE[(carry << 4) + nibble - 195]; 34 | carry = -1; 35 | } else if (nibble < 13) { 36 | this.charBuffer[pos++] = this.TABLE[nibble]; 37 | } else { 38 | carry = nibble; 39 | } 40 | } 41 | let uppercase: boolean = true; 42 | for (let index: number = 0; index < pos; index++) { 43 | const char: string = this.charBuffer[index]; 44 | if (uppercase && char >= 'a' && char <= 'z') { 45 | this.charBuffer[index] = char.toUpperCase(); 46 | uppercase = false; 47 | } 48 | if (char === '.' || char === '!') { 49 | uppercase = true; 50 | } 51 | } 52 | return this.charBuffer.slice(0, pos).join(''); 53 | } 54 | 55 | static pack(word: Packet, str: string): void { 56 | if (str.length > 80) { 57 | str = str.substring(0, 80); 58 | } 59 | str = str.toLowerCase(); 60 | let carry: number = -1; 61 | for (let index: number = 0; index < str.length; index++) { 62 | const char: string = str.charAt(index); 63 | let currentChar: number = 0; 64 | for (let lookupIndex: number = 0; lookupIndex < this.TABLE.length; lookupIndex++) { 65 | if (char === this.TABLE[lookupIndex]) { 66 | currentChar = lookupIndex; 67 | break; 68 | } 69 | } 70 | if (currentChar > 12) { 71 | currentChar += 195; 72 | } 73 | if (carry === -1) { 74 | if (currentChar < 13) { 75 | carry = currentChar; 76 | } else { 77 | word.p1(currentChar); 78 | } 79 | } else if (currentChar < 13) { 80 | word.p1((carry << 4) + currentChar); 81 | carry = -1; 82 | } else { 83 | word.p1((carry << 4) + (currentChar >> 4)); 84 | carry = currentChar & 0xf; 85 | } 86 | } 87 | if (carry !== -1) { 88 | word.p1(carry << 4); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/dash3d/ClientNpc.ts: -------------------------------------------------------------------------------- 1 | import NpcType from '#/config/NpcType.js'; 2 | import SeqType from '#/config/SeqType.js'; 3 | import SpotAnimType from '#/config/SpotAnimType.js'; 4 | 5 | import ClientEntity from '#/dash3d/ClientEntity.js'; 6 | 7 | import Model from '#/dash3d/Model.js'; 8 | 9 | export const enum NpcUpdate { 10 | DAMAGE2 = 0x1, 11 | ANIM = 0x2, 12 | FACE_ENTITY = 0x4, 13 | SAY = 0x8, 14 | DAMAGE = 0x10, 15 | CHANGE_TYPE = 0x20, 16 | SPOTANIM = 0x40, 17 | FACE_COORD = 0x80 18 | } 19 | 20 | export default class ClientNpc extends ClientEntity { 21 | type: NpcType | null = null; 22 | 23 | getModel(): Model | null { 24 | if (this.type == null) { 25 | return null; 26 | } 27 | 28 | let model = this.getAnimatedModel(); 29 | if (model == null) { 30 | return null; 31 | } 32 | 33 | this.height = model.minY; 34 | 35 | if (this.spotanimId != -1 && this.spotanimFrame != -1) { 36 | let spot = SpotAnimType.types[this.spotanimId]; 37 | let spotModel = spot.getModel(); 38 | 39 | if (spotModel != null) { 40 | const temp: Model = Model.modelShareColored(spotModel, true, !spot.animHasAlpha, false); 41 | temp.translate(-this.spotanimHeight, 0, 0); 42 | temp.createLabelReferences(); 43 | if (spot.seq && spot.seq.frames) { 44 | temp.applyTransform(spot.seq.frames[this.spotanimFrame]); 45 | } 46 | 47 | temp.labelFaces = null; 48 | temp.labelVertices = null; 49 | 50 | if (spot.resizeh != 128 || spot.resizev != 128) { 51 | temp.scale(spot.resizev, spot.resizeh, spot.resizeh); 52 | } 53 | 54 | temp.calculateNormals(spot.ambient + 64, spot.contrast + 850, -30, -50, -30, true); 55 | 56 | const models: Model[] = [model, temp]; 57 | model = Model.modelFromModelsBounds(models, 2); 58 | } 59 | } 60 | 61 | if (this.type.size == 1) { 62 | model.picking = true; 63 | } 64 | 65 | return model; 66 | } 67 | 68 | private getAnimatedModel(): Model | null { 69 | if (!this.type) { 70 | return null; 71 | } 72 | 73 | if (this.primarySeqId < 0 || this.primarySeqDelay != 0) { 74 | const secondarySeq = SeqType.types[this.secondarySeqId]; 75 | let secondaryTransform = -1; 76 | if (this.secondarySeqId >= 0 && secondarySeq.frames) { 77 | secondaryTransform = secondarySeq.frames[this.secondarySeqFrame]; 78 | } 79 | 80 | return this.type.getModel(secondaryTransform, -1, null); 81 | } else { 82 | const primarySeq = SeqType.types[this.primarySeqId]; 83 | let primaryTransform = -1; 84 | if (primarySeq.frames) { 85 | primaryTransform = primarySeq.frames[this.primarySeqFrame]; 86 | } 87 | 88 | const secondarySeq = SeqType.types[this.secondarySeqId]; 89 | let secondaryTransform = -1; 90 | if (this.secondarySeqId >= 0 && this.secondarySeqId != this.readyanim && secondarySeq.frames) { 91 | secondaryTransform = secondarySeq.frames[this.secondarySeqFrame]; 92 | } 93 | 94 | return this.type.getModel(primaryTransform, secondaryTransform, primarySeq.walkmerge); 95 | } 96 | } 97 | 98 | isVisible(): boolean { 99 | return this.type !== null; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/dash3d/LocShape.ts: -------------------------------------------------------------------------------- 1 | import { LocLayer } from '#/dash3d/LocLayer.js'; 2 | 3 | export default class LocShape { 4 | static readonly WALL_STRAIGHT: LocShape = new LocShape(0, LocLayer.WALL); 5 | static readonly WALL_DIAGONAL_CORNER: LocShape = new LocShape(1, LocLayer.WALL); 6 | static readonly WALL_L: LocShape = new LocShape(2, LocLayer.WALL); 7 | static readonly WALL_SQUARE_CORNER: LocShape = new LocShape(3, LocLayer.WALL); 8 | static readonly WALLDECOR_STRAIGHT_NOOFFSET: LocShape = new LocShape(4, LocLayer.WALL_DECOR); 9 | static readonly WALLDECOR_STRAIGHT_OFFSET: LocShape = new LocShape(5, LocLayer.WALL_DECOR); 10 | static readonly WALLDECOR_DIAGONAL_OFFSET: LocShape = new LocShape(6, LocLayer.WALL_DECOR); 11 | static readonly WALLDECOR_DIAGONAL_NOOFFSET: LocShape = new LocShape(7, LocLayer.WALL_DECOR); 12 | static readonly WALLDECOR_DIAGONAL_BOTH: LocShape = new LocShape(8, LocLayer.WALL_DECOR); 13 | static readonly WALL_DIAGONAL: LocShape = new LocShape(9, LocLayer.GROUND); 14 | static readonly CENTREPIECE_STRAIGHT: LocShape = new LocShape(10, LocLayer.GROUND); 15 | static readonly CENTREPIECE_DIAGONAL: LocShape = new LocShape(11, LocLayer.GROUND); 16 | static readonly ROOF_STRAIGHT: LocShape = new LocShape(12, LocLayer.GROUND); 17 | static readonly ROOF_DIAGONAL_WITH_ROOFEDGE: LocShape = new LocShape(13, LocLayer.GROUND); 18 | static readonly ROOF_DIAGONAL: LocShape = new LocShape(14, LocLayer.GROUND); 19 | static readonly ROOF_L_CONCAVE: LocShape = new LocShape(15, LocLayer.GROUND); 20 | static readonly ROOF_L_CONVEX: LocShape = new LocShape(16, LocLayer.GROUND); 21 | static readonly ROOF_FLAT: LocShape = new LocShape(17, LocLayer.GROUND); 22 | static readonly ROOFEDGE_STRAIGHT: LocShape = new LocShape(18, LocLayer.GROUND); 23 | static readonly ROOFEDGE_DIAGONAL_CORNER: LocShape = new LocShape(19, LocLayer.GROUND); 24 | static readonly ROOFEDGE_L: LocShape = new LocShape(20, LocLayer.GROUND); 25 | static readonly ROOFEDGE_SQUARE_CORNER: LocShape = new LocShape(21, LocLayer.GROUND); 26 | static readonly GROUND_DECOR: LocShape = new LocShape(22, LocLayer.GROUND_DECOR); 27 | 28 | static values(): LocShape[] { 29 | return [ 30 | this.WALL_STRAIGHT, 31 | this.WALL_DIAGONAL_CORNER, 32 | this.ROOF_FLAT, 33 | this.ROOF_L_CONCAVE, 34 | this.WALL_L, 35 | this.ROOF_DIAGONAL, 36 | this.WALL_DIAGONAL, 37 | this.WALL_SQUARE_CORNER, 38 | this.GROUND_DECOR, 39 | this.ROOF_STRAIGHT, 40 | this.CENTREPIECE_DIAGONAL, 41 | this.WALLDECOR_DIAGONAL_OFFSET, 42 | this.ROOFEDGE_L, 43 | this.CENTREPIECE_STRAIGHT, 44 | this.WALLDECOR_STRAIGHT_OFFSET, 45 | this.ROOF_DIAGONAL_WITH_ROOFEDGE, 46 | this.WALLDECOR_DIAGONAL_NOOFFSET, 47 | this.WALLDECOR_STRAIGHT_NOOFFSET, 48 | this.ROOF_L_CONVEX, 49 | this.WALLDECOR_DIAGONAL_BOTH, 50 | this.ROOFEDGE_DIAGONAL_CORNER, 51 | this.ROOFEDGE_SQUARE_CORNER, 52 | this.ROOFEDGE_STRAIGHT 53 | ]; 54 | } 55 | 56 | static of(id: number): LocShape { 57 | const values: LocShape[] = this.values(); 58 | for (let index: number = 0; index < values.length; index++) { 59 | const shape: LocShape = values[index]; 60 | if (shape.id === id) { 61 | return shape; 62 | } 63 | } 64 | throw Error('shape not found'); 65 | } 66 | 67 | readonly id: number; 68 | readonly layer: number; 69 | 70 | private constructor(id: number, layer: number) { 71 | this.id = id; 72 | this.layer = layer; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/datastruct/JString.ts: -------------------------------------------------------------------------------- 1 | export default class JString { 2 | // prettier-ignore 3 | private static BASE37_LOOKUP: string[] = [ 4 | '_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 5 | 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 6 | 't', 'u', 'v', 'w', 'x', 'y', 'z', 7 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 8 | ]; 9 | 10 | static toBase37(string: string): bigint { 11 | string = string.trim(); 12 | let l: bigint = 0n; 13 | 14 | for (let i: number = 0; i < string.length && i < 12; i++) { 15 | const c: number = string.charCodeAt(i); 16 | l *= 37n; 17 | 18 | if (c >= 0x41 && c <= 0x5a) { 19 | // A-Z 20 | l += BigInt(c + 1 - 0x41); 21 | } else if (c >= 0x61 && c <= 0x7a) { 22 | // a-z 23 | l += BigInt(c + 1 - 0x61); 24 | } else if (c >= 0x30 && c <= 0x39) { 25 | // 0-9 26 | l += BigInt(c + 27 - 0x30); 27 | } 28 | } 29 | 30 | return l; 31 | } 32 | 33 | static fromBase37(value: bigint): string { 34 | // >= 37 to the 12th power 35 | if (value < 0n || value >= 6582952005840035281n) { 36 | return 'invalid_name'; 37 | } 38 | 39 | if (value % 37n === 0n) { 40 | return 'invalid_name'; 41 | } 42 | 43 | let len: number = 0; 44 | const chars: string[] = Array(12); 45 | while (value !== 0n) { 46 | const l1: bigint = value; 47 | value /= 37n; 48 | chars[11 - len++] = this.BASE37_LOOKUP[Number(l1 - value * 37n)]; 49 | } 50 | 51 | return chars.slice(12 - len).join(''); 52 | } 53 | 54 | static toSentenceCase(input: string): string { 55 | const chars: string[] = [...input.toLowerCase()]; 56 | let punctuation: boolean = true; 57 | for (let index: number = 0; index < chars.length; index++) { 58 | const char: string = chars[index]; 59 | if (punctuation && char >= 'a' && char <= 'z') { 60 | chars[index] = char.toUpperCase(); 61 | punctuation = false; 62 | } 63 | if (char === '.' || char === '!') { 64 | punctuation = true; 65 | } 66 | } 67 | return chars.join(''); 68 | } 69 | 70 | static toAsterisks(str: string): string { 71 | let temp: string = ''; 72 | for (let i: number = 0; i < str.length; i++) { 73 | temp = temp + '*'; 74 | } 75 | return temp; 76 | } 77 | 78 | static formatIPv4(ip: number): string { 79 | return ((ip >> 24) & 0xff) + '.' + ((ip >> 16) & 0xff) + '.' + ((ip >> 8) & 0xff) + '.' + (ip & 0xff); 80 | } 81 | 82 | static formatName(str: string): string { 83 | if (str.length === 0) { 84 | return str; 85 | } 86 | 87 | const chars: string[] = [...str]; 88 | for (let i: number = 0; i < chars.length; i++) { 89 | if (chars[i] === '_') { 90 | chars[i] = ' '; 91 | 92 | if (i + 1 < chars.length && chars[i + 1] >= 'a' && chars[i + 1] <= 'z') { 93 | chars[i + 1] = String.fromCharCode(chars[i + 1].charCodeAt(0) + 'A'.charCodeAt(0) - 97); 94 | } 95 | } 96 | } 97 | 98 | if (chars[0] >= 'a' && chars[0] <= 'z') { 99 | chars[0] = String.fromCharCode(chars[0].charCodeAt(0) + 'A'.charCodeAt(0) - 97); 100 | } 101 | 102 | return chars.join(''); 103 | } 104 | 105 | static hashCode(str: string): bigint { 106 | const upper: string = str.toUpperCase(); 107 | let hash: bigint = 0n; 108 | 109 | for (let i: number = 0; i < upper.length; i++) { 110 | hash = hash * 61n + BigInt(upper.charCodeAt(i)) - 32n; 111 | hash = (hash + (hash >> 56n)) & 0xffffffffffffffn; 112 | } 113 | 114 | return hash; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/config/IdkType.ts: -------------------------------------------------------------------------------- 1 | import { ConfigType } from '#/config/ConfigType.js'; 2 | 3 | import Model from '#/dash3d/Model.js'; 4 | 5 | import Jagfile from '#/io/Jagfile.js'; 6 | import Packet from '#/io/Packet.js'; 7 | 8 | import { TypedArray1d } from '#/util/Arrays.js'; 9 | 10 | export default class IdkType extends ConfigType { 11 | static count: number = 0; 12 | static types: IdkType[] = []; 13 | type: number = -1; 14 | models: Int32Array | null = null; 15 | recol_s: Int32Array = new Int32Array(6); 16 | recol_d: Int32Array = new Int32Array(6); 17 | heads: Int32Array = new Int32Array(5).fill(-1); 18 | disable: boolean = false; 19 | 20 | static unpack(config: Jagfile): void { 21 | const dat: Packet = new Packet(config.read('idk.dat')); 22 | 23 | this.count = dat.g2(); 24 | this.types = new Array(this.count); 25 | 26 | for (let id: number = 0; id < this.count; id++) { 27 | if (!this.types[id]) { 28 | this.types[id] = new IdkType(id); 29 | } 30 | 31 | this.types[id].unpackType(dat); 32 | } 33 | } 34 | 35 | unpack(code: number, dat: Packet): void { 36 | if (code === 1) { 37 | this.type = dat.g1(); 38 | } else if (code === 2) { 39 | const count: number = dat.g1(); 40 | this.models = new Int32Array(count); 41 | 42 | for (let i: number = 0; i < count; i++) { 43 | this.models[i] = dat.g2(); 44 | } 45 | } else if (code === 3) { 46 | this.disable = true; 47 | } else if (code >= 40 && code < 50) { 48 | this.recol_s[code - 40] = dat.g2(); 49 | } else if (code >= 50 && code < 60) { 50 | this.recol_d[code - 50] = dat.g2(); 51 | } else if (code >= 60 && code < 70) { 52 | this.heads[code - 60] = dat.g2(); 53 | } else { 54 | console.log('Error unrecognised config code: ', code); 55 | } 56 | } 57 | 58 | modelIsReady(): boolean { 59 | if (!this.models) { 60 | return true; 61 | } 62 | 63 | let ready = true; 64 | 65 | for (let i = 0; i < this.models.length; i++) { 66 | if (!Model.isReady(this.models[i])) { 67 | ready = false; 68 | } 69 | } 70 | 71 | return ready; 72 | } 73 | 74 | getModel(): Model | null { 75 | if (!this.models) { 76 | return null; 77 | } 78 | 79 | const models: (Model | null)[] = new TypedArray1d(this.models.length, null); 80 | for (let i: number = 0; i < this.models.length; i++) { 81 | models[i] = Model.tryGet(this.models[i]); 82 | } 83 | 84 | let model: Model | null; 85 | if (models.length === 1) { 86 | model = models[0]; 87 | } else { 88 | model = Model.modelFromModels(models, models.length); 89 | } 90 | 91 | for (let i: number = 0; i < 6 && this.recol_s[i] !== 0; i++) { 92 | model?.recolour(this.recol_s[i], this.recol_d[i]); 93 | } 94 | 95 | return model; 96 | } 97 | 98 | headModelIsReady(): boolean { 99 | let downloaded = true; 100 | 101 | for (let i = 0; i < this.heads.length; i++) { 102 | if (this.heads[i] != -1 && !Model.isReady(this.heads[i])) { 103 | downloaded = false; 104 | } 105 | } 106 | 107 | return downloaded; 108 | } 109 | 110 | getHeadModel(): Model { 111 | let count: number = 0; 112 | 113 | const models: (Model | null)[] = new TypedArray1d(5, null); 114 | for (let i: number = 0; i < 5; i++) { 115 | if (this.heads[i] !== -1) { 116 | models[count++] = Model.tryGet(this.heads[i]); 117 | } 118 | } 119 | 120 | const model: Model = Model.modelFromModels(models, count); 121 | for (let i: number = 0; i < 6 && this.recol_s[i] !== 0; i++) { 122 | model.recolour(this.recol_s[i], this.recol_d[i]); 123 | } 124 | 125 | return model; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/io/ClientProt.ts: -------------------------------------------------------------------------------- 1 | export const enum ClientProt { 2 | NO_TIMEOUT = 206, // index: 6 - NXT naming 3 | 4 | IDLE_TIMER = 102, // index: 30 5 | EVENT_TRACKING = 19, // index: 34 6 | 7 | // autogenerated as part of obfuscation process 8 | ANTICHEAT_OPLOGIC1 = 87, // index: 60 9 | ANTICHEAT_OPLOGIC2 = 95, // index: 61 10 | ANTICHEAT_OPLOGIC3 = 146, // index: 62 11 | ANTICHEAT_OPLOGIC4 = 186, // index: 63 12 | ANTICHEAT_OPLOGIC5 = 74, // index: 64 13 | ANTICHEAT_OPLOGIC6 = 250, // index: 65 14 | ANTICHEAT_OPLOGIC7 = 119, // index: 66 15 | ANTICHEAT_OPLOGIC8 = 171, // index: 67 16 | ANTICHEAT_OPLOGIC9 = 233, // index: 68 17 | 18 | // autogenerated as part of obfuscation process 19 | ANTICHEAT_CYCLELOGIC1 = 136, // index: 70 20 | ANTICHEAT_CYCLELOGIC2 = 223, // index: 71 21 | ANTICHEAT_CYCLELOGIC3 = 181, // index: 74 22 | ANTICHEAT_CYCLELOGIC4 = 94, // index: 72 23 | ANTICHEAT_CYCLELOGIC5 = 63, // index: 75 24 | ANTICHEAT_CYCLELOGIC6 = 112, // index: 73 25 | 26 | OPOBJ1 = 113, // index: 80 - NXT naming 27 | OPOBJ2 = 238, // index: 81 - NXT naming 28 | OPOBJ3 = 55, // index: 82 - NXT naming 29 | OPOBJ4 = 17, // index: 83 - NXT naming 30 | OPOBJ5 = 247, // index: 84 - NXT naming 31 | OPOBJT = 122, // index: 88 - NXT naming 32 | OPOBJU = 143, // index: 89 - NXT naming 33 | 34 | OPNPC1 = 180, // index: 100 - NXT naming 35 | OPNPC2 = 252, // index: 101 - NXT naming 36 | OPNPC3 = 196, // index: 102 - NXT naming 37 | OPNPC4 = 107, // index: 103 - NXT naming 38 | OPNPC5 = 43, // index: 104 - NXT naming 39 | OPNPCT = 141, // index: 108 - NXT naming 40 | OPNPCU = 14, // index: 109 - NXT naming 41 | 42 | OPLOC1 = 1, // index: 120 - NXT naming 43 | OPLOC2 = 219, // index: 121 - NXT naming 44 | OPLOC3 = 226, // index: 122 - NXT naming 45 | OPLOC4 = 204, // index: 123 - NXT naming 46 | OPLOC5 = 86, // index: 124 - NXT naming 47 | OPLOCT = 208, // index: 128 - NXT naming 48 | OPLOCU = 147, // index: 129 - NXT naming 49 | 50 | OPPLAYER1 = 135, // index: 140 - NXT naming 51 | OPPLAYER2 = 165, // index: 141 - NXT naming 52 | OPPLAYER3 = 172, // index: 142 - NXT naming 53 | OPPLAYER4 = 54, // index: 143 - NXT naming 54 | OPPLAYERT = 52, // index: 148 - NXT naming 55 | OPPLAYERU = 210, // index: 149 - NXT naming 56 | 57 | OPHELD1 = 104, // index: 160 - name based on runescript trigger 58 | OPHELD2 = 193, // index: 161 - name based on runescript trigger 59 | OPHELD3 = 115, // index: 162 - name based on runescript trigger 60 | OPHELD4 = 194, // index: 163 - name based on runescript trigger 61 | OPHELD5 = 9, // index: 164 - name based on runescript trigger 62 | OPHELDT = 188, // index: 168 - name based on runescript trigger 63 | OPHELDU = 126, // index: 169 - name based on runescript trigger 64 | 65 | INV_BUTTON1 = 13, // index: 190 - NXT has "IF_BUTTON1" but for our interface system, this makes more sense 66 | INV_BUTTON2 = 58, // index: 191 - NXT has "IF_BUTTON2" but for our interface system, this makes more sense 67 | INV_BUTTON3 = 48, // index: 192 - NXT has "IF_BUTTON3" but for our interface system, this makes more sense 68 | INV_BUTTON4 = 183, // index: 193 - NXT has "IF_BUTTON4" but for our interface system, this makes more sense 69 | INV_BUTTON5 = 242, // index: 194 - NXT has "IF_BUTTON5" but for our interface system, this makes more sense 70 | 71 | IF_BUTTON = 177, // index: 200 - NXT naming 72 | RESUME_PAUSEBUTTON = 239, // index: 201 - NXT naming 73 | CLOSE_MODAL = 245, // index: 202 - NXT naming 74 | RESUME_P_COUNTDIALOG = 241, // index: 203 - NXT naming 75 | TUTORIAL_CLICKSIDE = 243, // index: 204 76 | 77 | MOVE_OPCLICK = 216, // index: 242 - comes with OP packets, name based on other MOVE packets 78 | REPORT_ABUSE = 205, // index: 243 79 | MOVE_MINIMAPCLICK = 198, // index: 244 - NXT naming 80 | INV_BUTTOND = 7, // index: 245 - NXT has "IF_BUTTOND" but for our interface system, this makes more sense 81 | IGNORELIST_DEL = 4, // index: 246 - NXT naming 82 | IGNORELIST_ADD = 20, // index: 247 - NXT naming 83 | IF_PLAYERDESIGN = 150, // index: 248 84 | CHAT_SETMODE = 8, // index: 249 - NXT naming 85 | MESSAGE_PRIVATE = 99, // index: 250 - NXT naming 86 | FRIENDLIST_DEL = 61, // index: 251 - NXT naming 87 | FRIENDLIST_ADD = 116, // index: 252 - NXT naming 88 | CLIENT_CHEAT = 11, // index: 253 - NXT naming 89 | MESSAGE_PUBLIC = 78, // index: 254 - NXT naming 90 | MOVE_GAMECLICK = 182, // index: 255 - NXT naming 91 | }; 92 | -------------------------------------------------------------------------------- /src/dash3d/AnimFrame.ts: -------------------------------------------------------------------------------- 1 | import AnimBase from '#/dash3d/AnimBase.js'; 2 | 3 | import Packet from '#/io/Packet.js'; 4 | 5 | export default class AnimFrame { 6 | static instances: AnimFrame[] = []; 7 | delay: number = -1; 8 | base: AnimBase | null = null; 9 | length: number = 0; 10 | groups: Int32Array | null = null; 11 | x: Int32Array | null = null; 12 | y: Int32Array | null = null; 13 | z: Int32Array | null = null; 14 | 15 | static init(total: number) { 16 | this.instances = new Array(total + 1); 17 | } 18 | 19 | static unpack(data: Uint8Array) { 20 | const buf = new Packet(data); 21 | buf.pos = data.length - 8; 22 | 23 | const headLength = buf.g2(); 24 | const tran1Length = buf.g2(); 25 | const tran2Length = buf.g2(); 26 | const delLength = buf.g2(); 27 | let pos = 0; 28 | 29 | const head = new Packet(data); 30 | head.pos = pos; 31 | pos += headLength + 2; 32 | 33 | const tran1 = new Packet(data); 34 | tran1.pos = pos; 35 | pos += tran1Length; 36 | 37 | const tran2 = new Packet(data); 38 | tran2.pos = pos; 39 | pos += tran2Length; 40 | 41 | const del = new Packet(data); 42 | del.pos = pos; 43 | pos += delLength; 44 | 45 | const baseBuf = new Packet(data); 46 | baseBuf.pos = pos; 47 | const base = new AnimBase(baseBuf); 48 | 49 | const total = head.g2(); 50 | const labels: Int32Array = new Int32Array(500); 51 | const x: Int32Array = new Int32Array(500); 52 | const y: Int32Array = new Int32Array(500); 53 | const z: Int32Array = new Int32Array(500); 54 | 55 | for (let i: number = 0; i < total; i++) { 56 | const id: number = head.g2(); 57 | 58 | const frame: AnimFrame = (this.instances[id] = new AnimFrame()); 59 | frame.delay = del.g1(); 60 | frame.base = base; 61 | 62 | const groupCount: number = head.g1(); 63 | let lastGroup: number = -1; 64 | let current: number = 0; 65 | 66 | for (let j: number = 0; j < groupCount; j++) { 67 | if (!base.types) { 68 | throw new Error(); 69 | } 70 | 71 | const flags: number = tran1.g1(); 72 | if (flags > 0) { 73 | if (base.types[j] !== 0) { 74 | for (let group: number = j - 1; group > lastGroup; group--) { 75 | if (base.types[group] === 0) { 76 | labels[current] = group; 77 | x[current] = 0; 78 | y[current] = 0; 79 | z[current] = 0; 80 | current++; 81 | break; 82 | } 83 | } 84 | } 85 | 86 | labels[current] = j; 87 | 88 | let defaultValue: number = 0; 89 | if (base.types[labels[current]] === 3) { 90 | defaultValue = 128; 91 | } 92 | 93 | if ((flags & 0x1) === 0) { 94 | x[current] = defaultValue; 95 | } else { 96 | x[current] = tran2.gsmart(); 97 | } 98 | 99 | if ((flags & 0x2) === 0) { 100 | y[current] = defaultValue; 101 | } else { 102 | y[current] = tran2.gsmart(); 103 | } 104 | 105 | if ((flags & 0x4) === 0) { 106 | z[current] = defaultValue; 107 | } else { 108 | z[current] = tran2.gsmart(); 109 | } 110 | 111 | lastGroup = j; 112 | current++; 113 | } 114 | } 115 | 116 | frame.length = current; 117 | frame.groups = new Int32Array(current); 118 | frame.x = new Int32Array(current); 119 | frame.y = new Int32Array(current); 120 | frame.z = new Int32Array(current); 121 | 122 | for (let j: number = 0; j < current; j++) { 123 | frame.groups[j] = labels[j]; 124 | frame.x[j] = x[j]; 125 | frame.y[j] = y[j]; 126 | frame.z[j] = z[j]; 127 | } 128 | } 129 | } 130 | 131 | static get(id: number) { 132 | return AnimFrame.instances[id]; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/dash3d/ClientProj.ts: -------------------------------------------------------------------------------- 1 | import SpotAnimType from '#/config/SpotAnimType.js'; 2 | 3 | import Model from '#/dash3d/Model.js'; 4 | import ModelSource from '#/dash3d/ModelSource.js'; 5 | 6 | export default class ClientProj extends ModelSource { 7 | readonly spotanim: SpotAnimType; 8 | readonly projLevel: number; 9 | readonly srcX: number; 10 | readonly srcZ: number; 11 | readonly srcY: number; 12 | readonly projOffsetY: number; 13 | readonly startCycle: number; 14 | readonly lastCycle: number; 15 | readonly peakPitch: number; 16 | readonly projArc: number; 17 | readonly projTarget: number; 18 | 19 | // runtime 20 | mobile: boolean = false; 21 | x: number = 0.0; 22 | z: number = 0.0; 23 | y: number = 0.0; 24 | projVelocityX: number = 0.0; 25 | projVelocityZ: number = 0.0; 26 | projVelocity: number = 0.0; 27 | projVelocityY: number = 0.0; 28 | accelerationY: number = 0.0; 29 | yaw: number = 0; 30 | pitch: number = 0; 31 | seqFrame: number = 0; 32 | seqCycle: number = 0; 33 | 34 | constructor(spotanim: number, level: number, srcX: number, srcY: number, srcZ: number, startCycle: number, lastCycle: number, peakPitch: number, arc: number, target: number, offsetY: number) { 35 | super(); 36 | this.spotanim = SpotAnimType.types[spotanim]; 37 | this.projLevel = level; 38 | this.srcX = srcX; 39 | this.srcZ = srcZ; 40 | this.srcY = srcY; 41 | this.startCycle = startCycle; 42 | this.lastCycle = lastCycle; 43 | this.peakPitch = peakPitch; 44 | this.projArc = arc; 45 | this.projTarget = target; 46 | this.projOffsetY = offsetY; 47 | } 48 | 49 | updateVelocity(dstX: number, dstY: number, dstZ: number, cycle: number): void { 50 | if (!this.mobile) { 51 | const dx: number = dstX - this.srcX; 52 | const dz: number = dstZ - this.srcZ; 53 | const d: number = Math.sqrt(dx * dx + dz * dz); 54 | 55 | this.x = this.srcX + (dx * this.projArc) / d; 56 | this.z = this.srcZ + (dz * this.projArc) / d; 57 | this.y = this.srcY; 58 | } 59 | 60 | const dt: number = this.lastCycle + 1 - cycle; 61 | this.projVelocityX = (dstX - this.x) / dt; 62 | this.projVelocityZ = (dstZ - this.z) / dt; 63 | this.projVelocity = Math.sqrt(this.projVelocityX * this.projVelocityX + this.projVelocityZ * this.projVelocityZ); 64 | if (!this.mobile) { 65 | this.projVelocityY = -this.projVelocity * Math.tan(this.peakPitch * 0.02454369); 66 | } 67 | this.accelerationY = ((dstY - this.y - this.projVelocityY * dt) * 2.0) / (dt * dt); 68 | } 69 | 70 | update(delta: number): void { 71 | this.mobile = true; 72 | this.x += this.projVelocityX * delta; 73 | this.z += this.projVelocityZ * delta; 74 | this.y += this.projVelocityY * delta + this.accelerationY * 0.5 * delta * delta; 75 | this.projVelocityY += this.accelerationY * delta; 76 | this.yaw = ((Math.atan2(this.projVelocityX, this.projVelocityZ) * 325.949 + 1024) | 0) & 0x7ff; 77 | this.pitch = ((Math.atan2(this.projVelocityY, this.projVelocity) * 325.949) | 0) & 0x7ff; 78 | 79 | if (!this.spotanim.seq) { 80 | return; 81 | } 82 | 83 | this.seqCycle += delta; 84 | 85 | while (this.seqCycle > this.spotanim.seq.getFrameDuration(this.seqFrame)) { 86 | this.seqCycle -= this.spotanim.seq.getFrameDuration(this.seqFrame) + 1; 87 | this.seqFrame++; 88 | if (this.seqFrame >= this.spotanim.seq.frameCount) { 89 | this.seqFrame = 0; 90 | } 91 | } 92 | } 93 | 94 | getModel(): Model | null { 95 | const spotModel: Model | null = this.spotanim.getModel(); 96 | if (!spotModel) { 97 | return null; 98 | } 99 | 100 | const model: Model = Model.modelShareColored(spotModel, true, !this.spotanim.animHasAlpha, false); 101 | 102 | if (this.spotanim.seq && this.spotanim.seq.frames) { 103 | model.createLabelReferences(); 104 | model.applyTransform(this.spotanim.seq.frames[this.seqFrame]); 105 | model.labelFaces = null; 106 | model.labelVertices = null; 107 | } 108 | 109 | if (this.spotanim.resizeh !== 128 || this.spotanim.resizev !== 128) { 110 | model.scale(this.spotanim.resizeh, this.spotanim.resizev, this.spotanim.resizeh); 111 | } 112 | 113 | model.rotateX(this.pitch); 114 | model.calculateNormals(64 + this.spotanim.ambient, 850 + this.spotanim.contrast, -30, -50, -30, true); 115 | return model; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/io/Database.ts: -------------------------------------------------------------------------------- 1 | export default class Database { 2 | private readonly db: IDBDatabase; 3 | 4 | constructor(db: IDBDatabase) { 5 | db.onerror = this.onerror; 6 | db.onclose = this.onclose; 7 | this.db = db; 8 | } 9 | 10 | static async openDatabase(): Promise { 11 | return await new Promise((resolve, reject): void => { 12 | const request: IDBOpenDBRequest = indexedDB.open('lostcity', 1); 13 | 14 | request.onsuccess = (event: Event): void => { 15 | const target: IDBOpenDBRequest = event.target as IDBOpenDBRequest; 16 | resolve(target.result); 17 | }; 18 | 19 | request.onupgradeneeded = (event: Event): void => { 20 | const target: IDBOpenDBRequest = event.target as IDBOpenDBRequest; 21 | target.result.createObjectStore('cache'); 22 | }; 23 | 24 | request.onerror = (event: Event): void => { 25 | const target: IDBOpenDBRequest = event.target as IDBOpenDBRequest; 26 | reject(target.result); 27 | }; 28 | }); 29 | } 30 | 31 | async read(archive: number, file: number) { 32 | return await new Promise((resolve): void => { 33 | const transaction: IDBTransaction = this.db.transaction('cache', 'readonly'); 34 | const store: IDBObjectStore = transaction.objectStore('cache'); 35 | const request: IDBRequest = store.get(`${archive}.${file}`); 36 | 37 | request.onsuccess = (): void => { 38 | if (request.result) { 39 | resolve(new Uint8Array(request.result)); 40 | } else { 41 | // IDB will call onsuccess with "undefined" if key does not exist 42 | resolve(undefined); 43 | } 44 | }; 45 | 46 | request.onerror = (): void => { 47 | resolve(undefined); 48 | }; 49 | }); 50 | } 51 | 52 | async cacheload(name: string) { 53 | return await new Promise((resolve): void => { 54 | const transaction: IDBTransaction = this.db.transaction('cache', 'readonly'); 55 | const store: IDBObjectStore = transaction.objectStore('cache'); 56 | const request: IDBRequest = store.get(name); 57 | 58 | request.onsuccess = (): void => { 59 | if (request.result) { 60 | resolve(new Uint8Array(request.result)); 61 | } else { 62 | // IDB will call onsuccess with "undefined" if key does not exist 63 | resolve(undefined); 64 | } 65 | }; 66 | 67 | request.onerror = (): void => { 68 | resolve(undefined); 69 | }; 70 | }); 71 | } 72 | 73 | async write(archive: number, file: number, src: Uint8Array | Int8Array | null) { 74 | if (src === null) { 75 | return; 76 | } 77 | 78 | return await new Promise((resolve, reject): void => { 79 | const transaction: IDBTransaction = this.db.transaction('cache', 'readwrite'); 80 | const store: IDBObjectStore = transaction.objectStore('cache'); 81 | const request: IDBRequest = store.put(src, `${archive}.${file}`); 82 | 83 | request.onsuccess = (): void => { 84 | resolve(); 85 | }; 86 | 87 | request.onerror = (): void => { 88 | // not too worried if it doesn't save, it'll redownload later 89 | resolve(); 90 | }; 91 | }); 92 | } 93 | 94 | async cachesave(name: string, src: Uint8Array | Int8Array | null) { 95 | if (src === null) { 96 | return; 97 | } 98 | 99 | return await new Promise((resolve, reject): void => { 100 | const transaction: IDBTransaction = this.db.transaction('cache', 'readwrite'); 101 | const store: IDBObjectStore = transaction.objectStore('cache'); 102 | const request: IDBRequest = store.put(src, name); 103 | 104 | request.onsuccess = (): void => { 105 | resolve(); 106 | }; 107 | 108 | request.onerror = (): void => { 109 | // not too worried if it doesn't save, it'll redownload later 110 | resolve(); 111 | }; 112 | }); 113 | } 114 | 115 | private onclose = (event: Event): void => {}; 116 | 117 | private onerror = (event: Event): void => {}; 118 | } 119 | -------------------------------------------------------------------------------- /src/config/SeqType.ts: -------------------------------------------------------------------------------- 1 | import { ConfigType } from '#/config/ConfigType.js'; 2 | 3 | import AnimFrame from '#/dash3d/AnimFrame.js'; 4 | 5 | import Jagfile from '#/io/Jagfile.js'; 6 | import Packet from '#/io/Packet.js'; 7 | 8 | export const enum PreanimMove { 9 | DELAYMOVE = 0, 10 | DELAYANIM = 1, 11 | MERGE = 2 12 | } 13 | 14 | export const enum PostanimMove { 15 | DELAYMOVE = 0, 16 | ABORTANIM = 1, 17 | MERGE = 2 18 | } 19 | 20 | export const enum RestartMode { 21 | RESET = 1, 22 | RESETLOOP = 2 23 | } 24 | 25 | export default class SeqType extends ConfigType { 26 | static count: number = 0; 27 | static types: SeqType[] = []; 28 | 29 | frameCount: number = 0; 30 | frames: Int16Array | null = null; 31 | iframes: Int16Array | null = null; 32 | delay: Int16Array | null = null; 33 | loops: number = -1; 34 | walkmerge: Int32Array | null = null; 35 | stretches: boolean = false; 36 | priority: number = 5; 37 | replaceheldleft: number = -1; 38 | replaceheldright: number = -1; 39 | maxloops: number = 99; 40 | preanim_move: number = -1; 41 | postanim_move: number = -1; 42 | duplicatebehavior: number = -1; 43 | 44 | static unpack(config: Jagfile): void { 45 | const dat: Packet = new Packet(config.read('seq.dat')); 46 | 47 | this.count = dat.g2(); 48 | this.types = new Array(this.count); 49 | 50 | for (let id: number = 0; id < this.count; id++) { 51 | if (!this.types[id]) { 52 | this.types[id] = new SeqType(id); 53 | } 54 | 55 | const seq = this.types[id].unpackType(dat); 56 | 57 | if (seq.preanim_move === -1) { 58 | if (seq.walkmerge === null) { 59 | seq.preanim_move = PreanimMove.DELAYMOVE; 60 | } else { 61 | seq.preanim_move = PreanimMove.MERGE; 62 | } 63 | } 64 | 65 | if (seq.postanim_move === -1) { 66 | if (seq.walkmerge === null) { 67 | seq.postanim_move = PostanimMove.DELAYMOVE; 68 | } else { 69 | seq.postanim_move = PostanimMove.MERGE; 70 | } 71 | } 72 | 73 | if (seq.frameCount === 0) { 74 | seq.frameCount = 1; 75 | 76 | seq.frames = new Int16Array(1); 77 | seq.frames[0] = -1; 78 | 79 | seq.iframes = new Int16Array(1); 80 | seq.iframes[0] = -1; 81 | 82 | seq.delay = new Int16Array(1); 83 | seq.delay[0] = -1; 84 | } 85 | } 86 | } 87 | 88 | getFrameDuration(frame: number) { 89 | if (!this.delay || !this.frames) { 90 | return 0; 91 | } 92 | 93 | let duration = this.delay[frame]; 94 | 95 | if (duration === 0) { 96 | let transform = AnimFrame.get(this.frames[frame]); 97 | if (transform != null) { 98 | duration = this.delay[frame] = transform.delay; 99 | } 100 | } 101 | 102 | if (duration === 0) { 103 | duration = 1; 104 | } 105 | 106 | return duration; 107 | } 108 | 109 | unpack(code: number, dat: Packet): void { 110 | if (code === 1) { 111 | this.frameCount = dat.g1(); 112 | this.frames = new Int16Array(this.frameCount); 113 | this.iframes = new Int16Array(this.frameCount); 114 | this.delay = new Int16Array(this.frameCount); 115 | 116 | for (let i: number = 0; i < this.frameCount; i++) { 117 | this.frames[i] = dat.g2(); 118 | 119 | this.iframes[i] = dat.g2(); 120 | if (this.iframes[i] === 65535) { 121 | this.iframes[i] = -1; 122 | } 123 | 124 | this.delay[i] = dat.g2(); 125 | } 126 | } else if (code === 2) { 127 | this.loops = dat.g2(); 128 | } else if (code === 3) { 129 | const count: number = dat.g1(); 130 | this.walkmerge = new Int32Array(count + 1); 131 | 132 | for (let i: number = 0; i < count; i++) { 133 | this.walkmerge[i] = dat.g1(); 134 | } 135 | 136 | this.walkmerge[count] = 9999999; 137 | } else if (code === 4) { 138 | this.stretches = true; 139 | } else if (code === 5) { 140 | this.priority = dat.g1(); 141 | } else if (code === 6) { 142 | this.replaceheldleft = dat.g2(); 143 | } else if (code === 7) { 144 | this.replaceheldright = dat.g2(); 145 | } else if (code === 8) { 146 | this.maxloops = dat.g1(); 147 | } else if (code === 9) { 148 | this.preanim_move = dat.g1(); 149 | } else if (code === 10) { 150 | this.postanim_move = dat.g1(); 151 | } else if (code === 11) { 152 | this.duplicatebehavior = dat.g1(); 153 | } else { 154 | console.log('Error unrecognised seq config code: ', code); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/config/FloType.ts: -------------------------------------------------------------------------------- 1 | import { ConfigType } from '#/config/ConfigType.js'; 2 | 3 | import Jagfile from '#/io/Jagfile.js'; 4 | import Packet from '#/io/Packet.js'; 5 | 6 | export default class FloType extends ConfigType { 7 | static count: number = 0; 8 | static types: FloType[] = []; 9 | rgb: number = 0; 10 | texture: number = -1; 11 | overlay: boolean = false; 12 | occlude: boolean = true; 13 | hue: number = 0; 14 | saturation: number = 0; 15 | lightness: number = 0; 16 | luminance: number = 0; 17 | chroma: number = 0; 18 | hsl: number = 0; 19 | 20 | static unpack(config: Jagfile): void { 21 | const dat: Packet = new Packet(config.read('flo.dat')); 22 | 23 | this.count = dat.g2(); 24 | this.types = new Array(this.count); 25 | 26 | for (let id: number = 0; id < this.count; id++) { 27 | if (!this.types[id]) { 28 | this.types[id] = new FloType(id); 29 | } 30 | 31 | this.types[id].unpackType(dat); 32 | } 33 | } 34 | 35 | unpack(code: number, dat: Packet): void { 36 | if (code === 1) { 37 | this.rgb = dat.g3(); 38 | this.setColour(this.rgb); 39 | } else if (code === 2) { 40 | this.texture = dat.g1(); 41 | } else if (code === 3) { 42 | this.overlay = true; 43 | } else if (code === 5) { 44 | this.occlude = false; 45 | } else if (code === 6) { 46 | this.debugname = dat.gjstr(); 47 | } else { 48 | console.log('Error unrecognised config code: ', code); 49 | } 50 | } 51 | 52 | private setColour(rgb: number): void { 53 | const red: number = ((rgb >> 16) & 0xff) / 256.0; 54 | const green: number = ((rgb >> 8) & 0xff) / 256.0; 55 | const blue: number = (rgb & 0xff) / 256.0; 56 | 57 | let min: number = red; 58 | if (green < red) { 59 | min = green; 60 | } 61 | if (blue < min) { 62 | min = blue; 63 | } 64 | 65 | let max: number = red; 66 | if (green > red) { 67 | max = green; 68 | } 69 | if (blue > max) { 70 | max = blue; 71 | } 72 | 73 | let h: number = 0.0; 74 | let s: number = 0.0; 75 | const l: number = (min + max) / 2.0; 76 | 77 | if (min !== max) { 78 | if (l < 0.5) { 79 | s = (max - min) / (max + min); 80 | } 81 | if (l >= 0.5) { 82 | s = (max - min) / (2.0 - max - min); 83 | } 84 | 85 | if (red === max) { 86 | h = (green - blue) / (max - min); 87 | } else if (green === max) { 88 | h = (blue - red) / (max - min) + 2.0; 89 | } else if (blue === max) { 90 | h = (red - green) / (max - min) + 4.0; 91 | } 92 | } 93 | 94 | h /= 6.0; 95 | 96 | this.hue = (h * 256.0) | 0; 97 | this.saturation = (s * 256.0) | 0; 98 | this.lightness = (l * 256.0) | 0; 99 | 100 | if (this.saturation < 0) { 101 | this.saturation = 0; 102 | } else if (this.saturation > 255) { 103 | this.saturation = 255; 104 | } 105 | 106 | if (this.lightness < 0) { 107 | this.lightness = 0; 108 | } else if (this.lightness > 255) { 109 | this.lightness = 255; 110 | } 111 | 112 | if (l > 0.5) { 113 | this.luminance = ((1.0 - l) * s * 512.0) | 0; 114 | } else { 115 | this.luminance = (l * s * 512.0) | 0; 116 | } 117 | 118 | if (this.luminance < 1) { 119 | this.luminance = 1; 120 | } 121 | 122 | this.chroma = (h * this.luminance) | 0; 123 | 124 | let hue: number = this.hue + ((Math.random() * 16.0) | 0) - 8; 125 | if (hue < 0) { 126 | hue = 0; 127 | } else if (hue > 255) { 128 | hue = 255; 129 | } 130 | 131 | let saturation: number = this.saturation + ((Math.random() * 48.0) | 0) - 24; 132 | if (saturation < 0) { 133 | saturation = 0; 134 | } else if (saturation > 255) { 135 | saturation = 255; 136 | } 137 | 138 | let lightness: number = this.lightness + ((Math.random() * 48.0) | 0) - 24; 139 | if (lightness < 0) { 140 | lightness = 0; 141 | } else if (lightness > 255) { 142 | lightness = 255; 143 | } 144 | 145 | this.hsl = FloType.hsl24to16(hue, saturation, lightness); 146 | } 147 | 148 | static hsl24to16(hue: number, saturation: number, lightness: number): number { 149 | if (lightness > 179) { 150 | saturation = (saturation / 2) | 0; 151 | } 152 | 153 | if (lightness > 192) { 154 | saturation = (saturation / 2) | 0; 155 | } 156 | 157 | if (lightness > 217) { 158 | saturation = (saturation / 2) | 0; 159 | } 160 | 161 | if (lightness > 243) { 162 | saturation = (saturation / 2) | 0; 163 | } 164 | 165 | return (((hue / 4) | 0) << 10) + (((saturation / 32) | 0) << 7) + ((lightness / 2) | 0); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/io/Isaac.ts: -------------------------------------------------------------------------------- 1 | export default class Isaac { 2 | private count: number = 0; 3 | private rsl: Int32Array = new Int32Array(256); 4 | private mem: Int32Array = new Int32Array(256); 5 | private a: number = 0; 6 | private b: number = 0; 7 | private c: number = 0; 8 | 9 | constructor(seed: Int32Array) { 10 | for (let i: number = 0; i < seed.length; i++) { 11 | this.rsl[i] = seed[i]; 12 | } 13 | this.init(); 14 | } 15 | 16 | get nextInt(): number { 17 | if (this.count-- === 0) { 18 | this.isaac(); 19 | this.count = 255; 20 | } 21 | return this.rsl[this.count]; 22 | } 23 | 24 | private init(): void { 25 | let a: number = 0x9e3779b9, 26 | b: number = 0x9e3779b9, 27 | c: number = 0x9e3779b9, 28 | d: number = 0x9e3779b9, 29 | e: number = 0x9e3779b9, 30 | f: number = 0x9e3779b9, 31 | g: number = 0x9e3779b9, 32 | h: number = 0x9e3779b9; 33 | 34 | for (let i: number = 0; i < 4; i++) { 35 | a ^= b << 11; 36 | d += a; 37 | b += c; 38 | b ^= c >>> 2; 39 | e += b; 40 | c += d; 41 | c ^= d << 8; 42 | f += c; 43 | d += e; 44 | d ^= e >>> 16; 45 | g += d; 46 | e += f; 47 | e ^= f << 10; 48 | h += e; 49 | f += g; 50 | f ^= g >>> 4; 51 | a += f; 52 | g += h; 53 | g ^= h << 8; 54 | b += g; 55 | h += a; 56 | h ^= a >>> 9; 57 | c += h; 58 | a += b; 59 | } 60 | 61 | for (let i: number = 0; i < 256; i += 8) { 62 | a += this.rsl[i]; 63 | b += this.rsl[i + 1]; 64 | c += this.rsl[i + 2]; 65 | d += this.rsl[i + 3]; 66 | e += this.rsl[i + 4]; 67 | f += this.rsl[i + 5]; 68 | g += this.rsl[i + 6]; 69 | h += this.rsl[i + 7]; 70 | 71 | a ^= b << 11; 72 | d += a; 73 | b += c; 74 | b ^= c >>> 2; 75 | e += b; 76 | c += d; 77 | c ^= d << 8; 78 | f += c; 79 | d += e; 80 | d ^= e >>> 16; 81 | g += d; 82 | e += f; 83 | e ^= f << 10; 84 | h += e; 85 | f += g; 86 | f ^= g >>> 4; 87 | a += f; 88 | g += h; 89 | g ^= h << 8; 90 | b += g; 91 | h += a; 92 | h ^= a >>> 9; 93 | c += h; 94 | a += b; 95 | 96 | this.mem[i] = a; 97 | this.mem[i + 1] = b; 98 | this.mem[i + 2] = c; 99 | this.mem[i + 3] = d; 100 | this.mem[i + 4] = e; 101 | this.mem[i + 5] = f; 102 | this.mem[i + 6] = g; 103 | this.mem[i + 7] = h; 104 | } 105 | 106 | for (let i: number = 0; i < 256; i += 8) { 107 | a += this.mem[i]; 108 | b += this.mem[i + 1]; 109 | c += this.mem[i + 2]; 110 | d += this.mem[i + 3]; 111 | e += this.mem[i + 4]; 112 | f += this.mem[i + 5]; 113 | g += this.mem[i + 6]; 114 | h += this.mem[i + 7]; 115 | 116 | a ^= b << 11; 117 | d += a; 118 | b += c; 119 | b ^= c >>> 2; 120 | e += b; 121 | c += d; 122 | c ^= d << 8; 123 | f += c; 124 | d += e; 125 | d ^= e >>> 16; 126 | g += d; 127 | e += f; 128 | e ^= f << 10; 129 | h += e; 130 | f += g; 131 | f ^= g >>> 4; 132 | a += f; 133 | g += h; 134 | g ^= h << 8; 135 | b += g; 136 | h += a; 137 | h ^= a >>> 9; 138 | c += h; 139 | a += b; 140 | 141 | this.mem[i] = a; 142 | this.mem[i + 1] = b; 143 | this.mem[i + 2] = c; 144 | this.mem[i + 3] = d; 145 | this.mem[i + 4] = e; 146 | this.mem[i + 5] = f; 147 | this.mem[i + 6] = g; 148 | this.mem[i + 7] = h; 149 | } 150 | 151 | this.isaac(); 152 | this.count = 256; 153 | } 154 | 155 | isaac(): void { 156 | this.c++; 157 | this.b += this.c; 158 | 159 | for (let i: number = 0; i < 256; i++) { 160 | const x: number = this.mem[i]; 161 | 162 | const mem: number = i & 3; 163 | if (mem === 0) { 164 | this.a ^= this.a << 13; 165 | } else if (mem === 1) { 166 | this.a ^= this.a >>> 6; 167 | } else if (mem === 2) { 168 | this.a ^= this.a << 2; 169 | } else if (mem === 3) { 170 | this.a ^= this.a >>> 16; 171 | } 172 | 173 | this.a += this.mem[(i + 128) & 0xff]; 174 | 175 | let y: number; 176 | this.mem[i] = y = this.mem[(x >>> 2) & 0xff] + this.a + this.b; 177 | this.rsl[i] = this.b = this.mem[((y >>> 8) >>> 2) & 0xff] + x; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/dash3d/ClientEntity.ts: -------------------------------------------------------------------------------- 1 | import SeqType, { PostanimMove } from '#/config/SeqType.js'; 2 | 3 | import ModelSource from '#/dash3d/ModelSource.js'; 4 | 5 | import { TypedArray1d } from '#/util/Arrays.js'; 6 | 7 | export default abstract class ClientEntity extends ModelSource { 8 | x: number = 0; 9 | z: number = 0; 10 | yaw: number = 0; 11 | needsForwardDrawPadding: boolean = false; 12 | size: number = 1; 13 | readyanim: number = -1; 14 | turnanim: number = -1; 15 | walkanim: number = -1; 16 | walkanim_b: number = -1; 17 | walkanim_l: number = -1; 18 | walkanim_r: number = -1; 19 | runanim: number = -1; 20 | chatMessage: string | null = null; 21 | chatTimer: number = 100; 22 | chatColour: number = 0; 23 | chatEffect: number = 0; 24 | combatCycle: number = -1000; 25 | damageValues: Int32Array = new Int32Array(4); 26 | damageTypes: Int32Array = new Int32Array(4); 27 | damageCycles: Int32Array = new Int32Array(4); 28 | health: number = 0; 29 | totalHealth: number = 0; 30 | targetId: number = -1; 31 | targetTileX: number = 0; 32 | targetTileZ: number = 0; 33 | secondarySeqId: number = -1; 34 | secondarySeqFrame: number = 0; 35 | secondarySeqCycle: number = 0; 36 | primarySeqId: number = -1; 37 | primarySeqFrame: number = 0; 38 | primarySeqCycle: number = 0; 39 | primarySeqDelay: number = 0; 40 | primarySeqLoop: number = 0; 41 | spotanimId: number = -1; 42 | spotanimFrame: number = 0; 43 | spotanimCycle: number = 0; 44 | spotanimLastCycle: number = 0; 45 | spotanimHeight: number = 0; 46 | forceMoveStartSceneTileX: number = 0; 47 | forceMoveEndSceneTileX: number = 0; 48 | forceMoveStartSceneTileZ: number = 0; 49 | forceMoveEndSceneTileZ: number = 0; 50 | forceMoveEndCycle: number = 0; 51 | forceMoveStartCycle: number = 0; 52 | forceMoveFaceDirection: number = 0; 53 | cycle: number = 0; 54 | height: number = 0; 55 | dstYaw: number = 0; 56 | routeLength: number = 0; 57 | routeTileX: Int32Array = new Int32Array(10); 58 | routeTileZ: Int32Array = new Int32Array(10); 59 | routeRun: boolean[] = new TypedArray1d(10, false); 60 | seqDelayMove: number = 0; 61 | preanimRouteLength: number = 0; 62 | 63 | abstract isVisible(): boolean; 64 | 65 | move(teleport: boolean, x: number, z: number): void { 66 | if (this.primarySeqId !== -1 && SeqType.types[this.primarySeqId].postanim_move === PostanimMove.ABORTANIM) { 67 | this.primarySeqId = -1; 68 | } 69 | 70 | if (!teleport) { 71 | const dx: number = x - this.routeTileX[0]; 72 | const dz: number = z - this.routeTileZ[0]; 73 | 74 | if (dx >= -8 && dx <= 8 && dz >= -8 && dz <= 8) { 75 | if (this.routeLength < 9) { 76 | this.routeLength++; 77 | } 78 | 79 | for (let i: number = this.routeLength; i > 0; i--) { 80 | this.routeTileX[i] = this.routeTileX[i - 1]; 81 | this.routeTileZ[i] = this.routeTileZ[i - 1]; 82 | this.routeRun[i] = this.routeRun[i - 1]; 83 | } 84 | 85 | this.routeTileX[0] = x; 86 | this.routeTileZ[0] = z; 87 | this.routeRun[0] = false; 88 | return; 89 | } 90 | } 91 | 92 | this.routeLength = 0; 93 | this.preanimRouteLength = 0; 94 | this.seqDelayMove = 0; 95 | this.routeTileX[0] = x; 96 | this.routeTileZ[0] = z; 97 | this.x = this.routeTileX[0] * 128 + this.size * 64; 98 | this.z = this.routeTileZ[0] * 128 + this.size * 64; 99 | } 100 | 101 | step(running: boolean, direction: number): void { 102 | let nextX: number = this.routeTileX[0]; 103 | let nextZ: number = this.routeTileZ[0]; 104 | 105 | if (direction === 0) { 106 | nextX--; 107 | nextZ++; 108 | } else if (direction === 1) { 109 | nextZ++; 110 | } else if (direction === 2) { 111 | nextX++; 112 | nextZ++; 113 | } else if (direction === 3) { 114 | nextX--; 115 | } else if (direction === 4) { 116 | nextX++; 117 | } else if (direction === 5) { 118 | nextX--; 119 | nextZ--; 120 | } else if (direction === 6) { 121 | nextZ--; 122 | } else if (direction === 7) { 123 | nextX++; 124 | nextZ--; 125 | } 126 | 127 | if (this.primarySeqId !== -1 && SeqType.types[this.primarySeqId].postanim_move === PostanimMove.ABORTANIM) { 128 | this.primarySeqId = -1; 129 | } 130 | 131 | if (this.routeLength < 9) { 132 | this.routeLength++; 133 | } 134 | 135 | for (let i: number = this.routeLength; i > 0; i--) { 136 | this.routeTileX[i] = this.routeTileX[i - 1]; 137 | this.routeTileZ[i] = this.routeTileZ[i - 1]; 138 | this.routeRun[i] = this.routeRun[i - 1]; 139 | } 140 | 141 | this.routeTileX[0] = nextX; 142 | this.routeTileZ[0] = nextZ; 143 | this.routeRun[0] = running; 144 | } 145 | 146 | clearRoute() { 147 | this.routeLength = 0; 148 | this.preanimRouteLength = 0; 149 | } 150 | 151 | hit(loopCycle: number, type: number, value: number) { 152 | for (let i = 0; i < 4; i++) { 153 | if (this.damageCycles[i] <= loopCycle) { 154 | this.damageValues[i] = value; 155 | this.damageTypes[i] = type; 156 | this.damageCycles[i] = loopCycle + 70; 157 | return; 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/client/KeyCodes.ts: -------------------------------------------------------------------------------- 1 | export type JavaKeyCode = { 2 | code: number; 3 | ch: number; 4 | }; 5 | 6 | export const CanvasEnabledKeys: string[] = ['F11', 'F12']; 7 | 8 | // could be an array, but mangling properties is a PITA 9 | export const KeyCodes: Map = new Map(); 10 | KeyCodes.set('ArrowLeft', { code: 37, ch: 1 }); 11 | KeyCodes.set('ArrowRight', { code: 39, ch: 2 }); 12 | KeyCodes.set('ArrowUp', { code: 38, ch: 3 }); 13 | KeyCodes.set('ArrowDown', { code: 40, ch: 4 }); 14 | KeyCodes.set('Control', { code: 17, ch: 5 }); 15 | KeyCodes.set('Shift', { code: 16, ch: 6 }); 16 | KeyCodes.set('Alt', { code: 18, ch: 7 }); 17 | KeyCodes.set('Backspace', { code: 8, ch: 8 }); 18 | KeyCodes.set('Tab', { code: 9, ch: 9 }); 19 | KeyCodes.set('Enter', { code: 10, ch: 10 }); 20 | 21 | KeyCodes.set('Escape', { code: 27, ch: 27 }); 22 | KeyCodes.set(' ', { code: 32, ch: 32 }); 23 | 24 | KeyCodes.set('Delete', { code: 127, ch: 127 }); 25 | 26 | KeyCodes.set('Home', { code: 36, ch: 1000 }); 27 | KeyCodes.set('End', { code: 35, ch: 1001 }); 28 | KeyCodes.set('PageUp', { code: 33, ch: 1002 }); 29 | KeyCodes.set('PageDown', { code: 34, ch: 1003 }); 30 | 31 | KeyCodes.set('F1', { code: 112, ch: 1008 }); 32 | KeyCodes.set('F2', { code: 113, ch: 1009 }); 33 | KeyCodes.set('F3', { code: 114, ch: 1010 }); 34 | KeyCodes.set('F4', { code: 115, ch: 1011 }); 35 | KeyCodes.set('F5', { code: 116, ch: 1012 }); 36 | KeyCodes.set('F6', { code: 117, ch: 1013 }); 37 | KeyCodes.set('F7', { code: 118, ch: 1014 }); 38 | KeyCodes.set('F8', { code: 119, ch: 1015 }); 39 | KeyCodes.set('F9', { code: 120, ch: 1016 }); 40 | KeyCodes.set('F10', { code: 121, ch: 1017 }); 41 | KeyCodes.set('F11', { code: 122, ch: 1018 }); 42 | KeyCodes.set('F12', { code: 123, ch: 1019 }); 43 | 44 | KeyCodes.set('CapsLock', { code: 20, ch: 65535 }); 45 | KeyCodes.set('Meta', { code: 524, ch: 65535 }); 46 | KeyCodes.set('Insert', { code: 155, ch: 65535 }); 47 | 48 | KeyCodes.set('`', { code: 192, ch: 96 }); 49 | KeyCodes.set('~', { code: 192, ch: 126 }); 50 | KeyCodes.set('!', { code: 49, ch: 33 }); 51 | KeyCodes.set('@', { code: 50, ch: 64 }); 52 | KeyCodes.set('#', { code: 51, ch: 35 }); 53 | KeyCodes.set('£', { code: 51, ch: 163 }); 54 | KeyCodes.set('$', { code: 52, ch: 36 }); 55 | KeyCodes.set('%', { code: 53, ch: 37 }); 56 | KeyCodes.set('^', { code: 54, ch: 94 }); 57 | KeyCodes.set('&', { code: 55, ch: 38 }); 58 | KeyCodes.set('*', { code: 56, ch: 42 }); 59 | KeyCodes.set('(', { code: 57, ch: 40 }); 60 | KeyCodes.set(')', { code: 48, ch: 41 }); 61 | KeyCodes.set('-', { code: 45, ch: 45 }); 62 | KeyCodes.set('_', { code: 45, ch: 95 }); 63 | KeyCodes.set('=', { code: 61, ch: 61 }); 64 | KeyCodes.set('+', { code: 61, ch: 43 }); 65 | KeyCodes.set('[', { code: 91, ch: 91 }); 66 | KeyCodes.set('{', { code: 91, ch: 123 }); 67 | KeyCodes.set(']', { code: 93, ch: 93 }); 68 | KeyCodes.set('}', { code: 93, ch: 125 }); 69 | KeyCodes.set('\\', { code: 92, ch: 92 }); 70 | KeyCodes.set('|', { code: 92, ch: 124 }); 71 | KeyCodes.set(';', { code: 59, ch: 59 }); 72 | KeyCodes.set(':', { code: 59, ch: 58 }); 73 | KeyCodes.set("'", { code: 222, ch: 39 }); 74 | KeyCodes.set('"', { code: 222, ch: 34 }); 75 | KeyCodes.set(',', { code: 44, ch: 44 }); 76 | KeyCodes.set('<', { code: 44, ch: 60 }); 77 | KeyCodes.set('.', { code: 46, ch: 46 }); 78 | KeyCodes.set('>', { code: 46, ch: 62 }); 79 | KeyCodes.set('/', { code: 47, ch: 47 }); 80 | KeyCodes.set('?', { code: 47, ch: 63 }); 81 | 82 | KeyCodes.set('0', { code: 48, ch: 48 }); 83 | KeyCodes.set('1', { code: 49, ch: 49 }); 84 | KeyCodes.set('2', { code: 50, ch: 50 }); 85 | KeyCodes.set('3', { code: 51, ch: 51 }); 86 | KeyCodes.set('4', { code: 52, ch: 52 }); 87 | KeyCodes.set('5', { code: 53, ch: 53 }); 88 | KeyCodes.set('6', { code: 54, ch: 54 }); 89 | KeyCodes.set('7', { code: 55, ch: 55 }); 90 | KeyCodes.set('8', { code: 56, ch: 56 }); 91 | KeyCodes.set('9', { code: 57, ch: 57 }); 92 | 93 | KeyCodes.set('a', { code: 65, ch: 97 }); 94 | KeyCodes.set('b', { code: 66, ch: 98 }); 95 | KeyCodes.set('c', { code: 67, ch: 99 }); 96 | KeyCodes.set('d', { code: 68, ch: 100 }); 97 | KeyCodes.set('e', { code: 69, ch: 101 }); 98 | KeyCodes.set('f', { code: 70, ch: 102 }); 99 | KeyCodes.set('g', { code: 71, ch: 103 }); 100 | KeyCodes.set('h', { code: 72, ch: 104 }); 101 | KeyCodes.set('i', { code: 73, ch: 105 }); 102 | KeyCodes.set('j', { code: 74, ch: 106 }); 103 | KeyCodes.set('k', { code: 75, ch: 107 }); 104 | KeyCodes.set('l', { code: 76, ch: 108 }); 105 | KeyCodes.set('m', { code: 77, ch: 109 }); 106 | KeyCodes.set('n', { code: 78, ch: 110 }); 107 | KeyCodes.set('o', { code: 79, ch: 111 }); 108 | KeyCodes.set('p', { code: 80, ch: 112 }); 109 | KeyCodes.set('q', { code: 81, ch: 113 }); 110 | KeyCodes.set('r', { code: 82, ch: 114 }); 111 | KeyCodes.set('s', { code: 83, ch: 115 }); 112 | KeyCodes.set('t', { code: 84, ch: 116 }); 113 | KeyCodes.set('u', { code: 85, ch: 117 }); 114 | KeyCodes.set('v', { code: 86, ch: 118 }); 115 | KeyCodes.set('w', { code: 87, ch: 119 }); 116 | KeyCodes.set('x', { code: 88, ch: 120 }); 117 | KeyCodes.set('y', { code: 89, ch: 121 }); 118 | KeyCodes.set('z', { code: 90, ch: 122 }); 119 | 120 | KeyCodes.set('A', { code: 65, ch: 65 }); 121 | KeyCodes.set('B', { code: 66, ch: 66 }); 122 | KeyCodes.set('C', { code: 67, ch: 67 }); 123 | KeyCodes.set('D', { code: 68, ch: 68 }); 124 | KeyCodes.set('E', { code: 69, ch: 69 }); 125 | KeyCodes.set('F', { code: 70, ch: 70 }); 126 | KeyCodes.set('G', { code: 71, ch: 71 }); 127 | KeyCodes.set('H', { code: 72, ch: 72 }); 128 | KeyCodes.set('I', { code: 73, ch: 73 }); 129 | KeyCodes.set('J', { code: 74, ch: 74 }); 130 | KeyCodes.set('K', { code: 75, ch: 75 }); 131 | KeyCodes.set('L', { code: 76, ch: 76 }); 132 | KeyCodes.set('M', { code: 77, ch: 77 }); 133 | KeyCodes.set('N', { code: 78, ch: 78 }); 134 | KeyCodes.set('O', { code: 79, ch: 79 }); 135 | KeyCodes.set('P', { code: 80, ch: 80 }); 136 | KeyCodes.set('Q', { code: 81, ch: 81 }); 137 | KeyCodes.set('R', { code: 82, ch: 82 }); 138 | KeyCodes.set('S', { code: 83, ch: 83 }); 139 | KeyCodes.set('T', { code: 84, ch: 84 }); 140 | KeyCodes.set('U', { code: 85, ch: 85 }); 141 | KeyCodes.set('V', { code: 86, ch: 86 }); 142 | KeyCodes.set('W', { code: 87, ch: 87 }); 143 | KeyCodes.set('X', { code: 88, ch: 88 }); 144 | KeyCodes.set('Y', { code: 89, ch: 89 }); 145 | KeyCodes.set('Z', { code: 90, ch: 90 }); 146 | -------------------------------------------------------------------------------- /src/3rdparty/bzip2-wasm.js: -------------------------------------------------------------------------------- 1 | import loadBZip2WASM from '#3rdparty/bzip2-wasm/bzip2.mjs'; 2 | 3 | const ERROR_MESSAGES = { 4 | '-2': 'BZ_PARAM_ERROR: incorrect parameters', 5 | '-3': "BZ_MEM_ERROR: couldn't allocate enough memory", 6 | '-4': 'BZ_DATA_ERROR: data integrity error when decompressing', 7 | '-5': 'BZ_DATA_ERROR_MAGIC: compressed data has incorrect header', 8 | '-7': 'BZ_UNEXPECTED_EOF: compressed data ends too early', 9 | '-8': 'BZ_OUTBUFF_FULL: destination buffer is full' 10 | }; 11 | 12 | class BZ2Wasm { 13 | constructor() { 14 | this.wasmModule = undefined; 15 | } 16 | 17 | // fetch the wasm and initialize it 18 | async init() { 19 | if (this.wasmModule) { 20 | return; 21 | } 22 | 23 | // check if node 24 | // http://philiplassen.com/2021/08/11/node-es6-emscripten.html 25 | // if (typeof process !== 'undefined') { 26 | // const { dirname } = await import/* webpackIgnore: true */('path'); 27 | // const { createRequire } = await import(/* webpackIgnore: true */'module'); 28 | 29 | // globalThis.__dirname = dirname(import.meta.url); 30 | // globalThis.require = createRequire(import.meta.url); 31 | // } 32 | 33 | this.wasmModule = await loadBZip2WASM(); 34 | } 35 | 36 | ensureInitialized() { 37 | if (!this.wasmModule) { 38 | throw new Error(`${this.constructor.name} not initalized. call .init()`); 39 | } 40 | } 41 | 42 | // turn bzip's integer return values into an error message and throw it. 43 | // also free the destination pointers 44 | handleError(returnValue, destPtr, destLengthPtr) { 45 | if (returnValue === 0) { 46 | return; 47 | } 48 | 49 | this.wasmModule._free(destPtr); 50 | this.wasmModule._free(destLengthPtr); 51 | 52 | const errorMessage = ERROR_MESSAGES[returnValue]; 53 | 54 | if (errorMessage) { 55 | throw new Error(errorMessage); 56 | } 57 | 58 | throw new Error(`error code: ${returnValue}`); 59 | } 60 | 61 | // create source, destination and length buffers 62 | createWASMBuffers(source, destLength) { 63 | const { _malloc, setValue, HEAPU8 } = this.wasmModule; 64 | 65 | const sourcePtr = _malloc(source.length); 66 | HEAPU8.set(source, sourcePtr); 67 | 68 | const destPtr = _malloc(destLength); 69 | 70 | const destLengthPtr = _malloc(destLength); 71 | setValue(destLengthPtr, destLength, 'i32'); 72 | 73 | return { sourcePtr, destPtr, destLengthPtr }; 74 | } 75 | 76 | // read the length returned by bzip, create a new Uint8Array of that size 77 | // and copy the decompressed/compressed data into it 78 | createBuffer(destPtr, destLengthPtr) { 79 | const { _free, getValue, HEAPU8 } = this.wasmModule; 80 | 81 | const destLength = getValue(destLengthPtr, 'i32'); 82 | 83 | const dest = new Uint8Array(destLength); 84 | dest.set(HEAPU8.subarray(destPtr, destPtr + destLength)); 85 | 86 | _free(destPtr); 87 | _free(destLengthPtr); 88 | 89 | return dest; 90 | } 91 | 92 | decompress(src, decompressedLength, prependHeader = false, containsDecompressedLength = false) { 93 | let compressed = new Uint8Array(src); 94 | 95 | if (containsDecompressedLength) { 96 | decompressedLength = (compressed[0] << 24) | (compressed[1] << 16) | (compressed[2] << 8) | compressed[3]; 97 | compressed[0] = 'B'.charCodeAt(0); 98 | compressed[1] = 'Z'.charCodeAt(0); 99 | compressed[2] = 'h'.charCodeAt(0); 100 | compressed[3] = '1'.charCodeAt(0); 101 | prependHeader = false; 102 | } 103 | 104 | if (prependHeader) { 105 | const temp = new Uint8Array(compressed.length + 4); 106 | temp[0] = 'B'.charCodeAt(0); 107 | temp[1] = 'Z'.charCodeAt(0); 108 | temp[2] = 'h'.charCodeAt(0); 109 | temp[3] = '1'.charCodeAt(0); 110 | temp.set(compressed, 4); 111 | compressed = temp; 112 | } 113 | 114 | this.ensureInitialized(); 115 | 116 | const { sourcePtr: compressedPtr, destPtr: decompressedPtr, destLengthPtr: decompressedLengthPtr } = this.createWASMBuffers(compressed, decompressedLength); 117 | 118 | const returnValue = this.wasmModule._BZ2_bzBuffToBuffDecompress(decompressedPtr, decompressedLengthPtr, compressedPtr, compressed.length, 0, 0); 119 | 120 | this.wasmModule._free(compressedPtr); 121 | 122 | this.handleError(returnValue, decompressedPtr, decompressedLengthPtr); 123 | 124 | return this.createBuffer(decompressedPtr, decompressedLengthPtr); 125 | } 126 | 127 | compress(decompressed, prefixLength = false, removeHeader = false, blockSize = 1, compressedLength = 0) { 128 | this.ensureInitialized(); 129 | 130 | if (!compressedLength) { 131 | compressedLength = decompressed.length; 132 | } 133 | 134 | if (compressedLength < 128) { 135 | compressedLength = 128; 136 | } 137 | 138 | if (blockSize <= 0 || blockSize > 9) { 139 | throw new RangeError('blockSize should be between 1-9'); 140 | } 141 | 142 | const { sourcePtr: decompressedPtr, destPtr: compressedPtr, destLengthPtr: compressedLengthPtr } = this.createWASMBuffers(decompressed, compressedLength); 143 | 144 | const returnValue = this.wasmModule._BZ2_bzBuffToBuffCompress(compressedPtr, compressedLengthPtr, decompressedPtr, decompressed.length, blockSize, 0, 30); 145 | 146 | this.wasmModule._free(decompressedPtr); 147 | 148 | this.handleError(returnValue, compressedPtr, compressedLengthPtr); 149 | 150 | const buf = this.createBuffer(compressedPtr, compressedLengthPtr); 151 | 152 | if (prefixLength) { 153 | buf[0] = (decompressed.length >> 24) & 0xff; 154 | buf[1] = (decompressed.length >> 16) & 0xff; 155 | buf[2] = (decompressed.length >> 8) & 0xff; 156 | buf[3] = decompressed.length & 0xff; 157 | } 158 | 159 | if (removeHeader) { 160 | return buf.subarray(4); 161 | } 162 | 163 | return buf; 164 | } 165 | } 166 | 167 | const BZip2 = new BZ2Wasm(); 168 | await BZip2.init(); 169 | 170 | export default BZip2; 171 | -------------------------------------------------------------------------------- /src/io/ClientStream.ts: -------------------------------------------------------------------------------- 1 | export default class ClientStream { 2 | private readonly socket: WebSocket; 3 | private readonly wsin: WebSocketReader; 4 | private readonly wsout: WebSocketWriter; 5 | 6 | private closed: boolean = false; 7 | private ioerror: boolean = false; 8 | 9 | static async openSocket(host: string, secured: boolean): Promise { 10 | return await new Promise((resolve, reject): void => { 11 | const protocol: string = secured ? 'wss' : 'ws'; 12 | const ws: WebSocket = new WebSocket(`${protocol}://${host}`, 'binary'); 13 | 14 | ws.addEventListener('open', (): void => { 15 | resolve(ws); 16 | }); 17 | 18 | ws.addEventListener('error', (): void => { 19 | reject(ws); 20 | }); 21 | }); 22 | } 23 | 24 | constructor(socket: WebSocket) { 25 | socket.onclose = this.onclose; 26 | socket.onerror = this.onerror; 27 | this.wsin = new WebSocketReader(socket, 5000); 28 | this.wsout = new WebSocketWriter(socket, 5000); 29 | this.socket = socket; 30 | } 31 | 32 | get host(): string { 33 | return this.socket.url.split('/')[2]; 34 | } 35 | 36 | get port(): number { 37 | return parseInt(this.socket.url.split(':')[2], 10); 38 | } 39 | 40 | get available(): number { 41 | return this.closed ? 0 : this.wsin.available; 42 | } 43 | 44 | write(src: Uint8Array, len: number): void { 45 | if (!this.closed) { 46 | this.wsout.write(src, len); 47 | } 48 | } 49 | 50 | async read(): Promise { 51 | return this.closed ? 0 : await this.wsin.read(); 52 | } 53 | 54 | async readBytes(dst: Uint8Array, off: number, len: number): Promise { 55 | if (this.closed) { 56 | return; 57 | } 58 | 59 | await this.wsin.readBytes(dst, off, len); 60 | } 61 | 62 | close(): void { 63 | this.closed = true; 64 | this.socket.close(); 65 | this.wsin.close(); 66 | this.wsout.close(); 67 | } 68 | 69 | private onclose = (event: CloseEvent): void => { 70 | if (this.closed) { 71 | return; 72 | } 73 | this.close(); 74 | }; 75 | 76 | private onerror = (event: Event): void => { 77 | if (this.closed) { 78 | return; 79 | } 80 | this.ioerror = true; 81 | this.close(); 82 | }; 83 | } 84 | 85 | class WebSocketWriter { 86 | private readonly socket: WebSocket; 87 | private readonly limit: number; 88 | 89 | private closed: boolean = false; 90 | private ioerror: boolean = false; 91 | 92 | constructor(socket: WebSocket, limit: number) { 93 | this.socket = socket; 94 | this.limit = limit; 95 | } 96 | 97 | write(src: Uint8Array, len: number): void { 98 | if (this.closed) { 99 | return; 100 | } 101 | if (this.ioerror) { 102 | this.ioerror = false; 103 | throw new Error(); 104 | } 105 | if (len > this.limit || src.length > this.limit) { 106 | throw new Error(); 107 | } 108 | try { 109 | this.socket.send(src.slice(0, len)); 110 | } catch (e) { 111 | this.ioerror = true; 112 | } 113 | } 114 | 115 | close(): void { 116 | this.closed = true; 117 | } 118 | } 119 | 120 | class WebSocketEvent { 121 | private readonly bytes: Uint8Array; 122 | private position: number; 123 | 124 | constructor(bytes: Uint8Array) { 125 | this.bytes = bytes; 126 | this.position = 0; 127 | } 128 | 129 | get available(): number { 130 | return this.bytes.length - this.position; 131 | } 132 | 133 | get read(): number { 134 | return this.bytes[this.position++]; 135 | } 136 | 137 | get len(): number { 138 | return this.bytes.length; 139 | } 140 | } 141 | 142 | class WebSocketReader { 143 | private readonly limit: number; 144 | 145 | private queue: WebSocketEvent[] = []; 146 | private event: WebSocketEvent | null = null; 147 | private callback: ((data: WebSocketEvent) => void) | null = null; 148 | private closed: boolean = false; 149 | private total: number = 0; 150 | 151 | constructor(socket: WebSocket, limit: number) { 152 | this.limit = limit; 153 | socket.binaryType = 'arraybuffer'; 154 | socket.onmessage = this.onmessage; 155 | } 156 | 157 | get available(): number { 158 | return this.total; 159 | } 160 | 161 | private onmessage = (e: MessageEvent): void => { 162 | if (this.closed) { 163 | throw new Error(); 164 | } 165 | 166 | const event: WebSocketEvent = new WebSocketEvent(new Uint8Array(e.data)); 167 | 168 | this.total += event.available; 169 | 170 | if (this.callback) { 171 | const cb = this.callback; 172 | this.callback = null; 173 | cb(event); 174 | } else { 175 | this.queue.push(event); 176 | } 177 | }; 178 | 179 | async read(): Promise { 180 | if (this.closed) { 181 | throw new Error(); 182 | } 183 | return await Promise.race([ 184 | new Promise(resolve => { 185 | if (!this.event || this.event.available === 0) { 186 | this.event = this.queue.shift() ?? null; 187 | } 188 | if (this.event && this.event.available > 0) { 189 | resolve(this.event.read); 190 | this.total--; 191 | } else { 192 | this.callback = (event: WebSocketEvent) => { 193 | this.event = event; 194 | this.total--; 195 | resolve(event.read); 196 | }; 197 | } 198 | }), 199 | new Promise((_, reject) => { 200 | setTimeout(() => { 201 | if (this.closed) { 202 | reject(new Error()); 203 | } else { 204 | reject(new Error()); 205 | } 206 | }, 20000); 207 | }) 208 | ]); 209 | } 210 | 211 | async readBytes(dst: Uint8Array, off: number, len: number): Promise { 212 | if (this.closed) { 213 | throw new Error(); 214 | } 215 | for (let i = 0; i < len; i++) { 216 | dst[off + i] = await this.read(); 217 | } 218 | return dst; 219 | } 220 | 221 | close(): void { 222 | this.closed = true; 223 | this.callback = null; 224 | this.event = null; 225 | this.queue = []; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/sound/Wave.ts: -------------------------------------------------------------------------------- 1 | import Jagfile from '#/io/Jagfile.js'; 2 | import Packet from '#/io/Packet.js'; 3 | 4 | import Tone from '#/sound/Tone.js'; 5 | 6 | import { TypedArray1d } from '#/util/Arrays.js'; 7 | 8 | export default class Wave { 9 | private static readonly tracks: (Wave | null)[] = new TypedArray1d(1000, null); 10 | static readonly delays: Int32Array = new Int32Array(1000); 11 | static waveBytes: Uint8Array = new Uint8Array(44100 * 10); 12 | static waveBuffer: Packet | null = null; 13 | private readonly tones: (Tone | null)[] = new TypedArray1d(10, null); 14 | private loopBegin: number = 0; 15 | private loopEnd: number = 0; 16 | 17 | static unpack(sounds: Jagfile): void { 18 | const dat: Packet = new Packet(sounds.read('sounds.dat')); 19 | this.waveBytes = new Uint8Array(441000); 20 | this.waveBuffer = new Packet(this.waveBytes); 21 | Tone.init(); 22 | 23 | // eslint-disable-next-line no-constant-condition 24 | while (true) { 25 | const id: number = dat.g2(); 26 | if (id === 65535) { 27 | break; 28 | } 29 | 30 | this.tracks[id] = new Wave(); 31 | this.tracks[id]!.read(dat); 32 | this.delays[id] = this.tracks[id]!.trim(); 33 | } 34 | } 35 | 36 | static generate(id: number, loopCount: number): Packet | null { 37 | if (!this.tracks[id]) { 38 | return null; 39 | } 40 | 41 | const track: Wave | null = this.tracks[id]; 42 | return track?.getWave(loopCount) ?? null; 43 | } 44 | 45 | read(dat: Packet): void { 46 | for (let tone: number = 0; tone < 10; tone++) { 47 | if (dat.g1() !== 0) { 48 | dat.pos--; 49 | 50 | this.tones[tone] = new Tone(); 51 | this.tones[tone]?.unpack(dat); 52 | } 53 | } 54 | 55 | this.loopBegin = dat.g2(); 56 | this.loopEnd = dat.g2(); 57 | } 58 | 59 | trim(): number { 60 | let start: number = 9999999; 61 | for (let tone: number = 0; tone < 10; tone++) { 62 | if (this.tones[tone] && ((this.tones[tone]!.start / 20) | 0) < start) { 63 | start = (this.tones[tone]!.start / 20) | 0; 64 | } 65 | } 66 | 67 | if (this.loopBegin < this.loopEnd && ((this.loopBegin / 20) | 0) < start) { 68 | start = (this.loopBegin / 20) | 0; 69 | } 70 | 71 | if (start === 9999999 || start === 0) { 72 | return 0; 73 | } 74 | 75 | for (let tone: number = 0; tone < 10; tone++) { 76 | if (this.tones[tone]) { 77 | this.tones[tone]!.start -= start * 20; 78 | } 79 | } 80 | 81 | if (this.loopBegin < this.loopEnd) { 82 | this.loopBegin -= start * 20; 83 | this.loopEnd -= start * 20; 84 | } 85 | 86 | return start; 87 | } 88 | 89 | getWave(loopCount: number): Packet | null { 90 | if (!Wave.waveBuffer) { 91 | return null; 92 | } 93 | 94 | const length: number = this.generate(loopCount); 95 | Wave.waveBuffer.pos = 0; 96 | Wave.waveBuffer.p4(0x52494646); // "RIFF" ChunkID 97 | Wave.waveBuffer.ip4(length + 36); // ChunkSize 98 | Wave.waveBuffer.p4(0x57415645); // "WAVE" format 99 | Wave.waveBuffer.p4(0x666d7420); // "fmt " chunk id 100 | Wave.waveBuffer.ip4(16); // chunk size 101 | Wave.waveBuffer.ip2(1); // audio format 102 | Wave.waveBuffer.ip2(1); // num channels 103 | Wave.waveBuffer.ip4(22050); // sample rate 104 | Wave.waveBuffer.ip4(22050); // byte rate 105 | Wave.waveBuffer.ip2(1); // block align 106 | Wave.waveBuffer.ip2(8); // bits per sample 107 | Wave.waveBuffer.p4(0x64617461); // "data" 108 | Wave.waveBuffer.ip4(length); 109 | Wave.waveBuffer.pos += length; 110 | return Wave.waveBuffer; 111 | } 112 | 113 | private generate(loopCount: number): number { 114 | let duration: number = 0; 115 | for (let tone: number = 0; tone < 10; tone++) { 116 | if (this.tones[tone] && this.tones[tone]!.length + this.tones[tone]!.start > duration) { 117 | duration = this.tones[tone]!.length + this.tones[tone]!.start; 118 | } 119 | } 120 | 121 | if (duration === 0) { 122 | return 0; 123 | } 124 | 125 | let sampleCount: number = ((duration * 22050) / 1000) | 0; 126 | let loopStart: number = ((this.loopBegin * 22050) / 1000) | 0; 127 | let loopStop: number = ((this.loopEnd * 22050) / 1000) | 0; 128 | 129 | if (loopStart < 0 || loopStop < 0 || loopStop > sampleCount || loopStart >= loopStop) { 130 | loopCount = 0; 131 | } 132 | 133 | let totalSampleCount: number = sampleCount + (loopStop - loopStart) * (loopCount - 1); 134 | for (let sample: number = 44; sample < totalSampleCount + 44; sample++) { 135 | if (Wave.waveBytes) { 136 | Wave.waveBytes[sample] = -128; 137 | } 138 | } 139 | 140 | for (let tone: number = 0; tone < 10; tone++) { 141 | if (this.tones[tone]) { 142 | const toneSampleCount: number = ((this.tones[tone]!.length * 22050) / 1000) | 0; 143 | const start: number = ((this.tones[tone]!.start * 22050) / 1000) | 0; 144 | const samples: Int32Array = this.tones[tone]!.generate(toneSampleCount, this.tones[tone]!.length); 145 | 146 | for (let sample: number = 0; sample < toneSampleCount; sample++) { 147 | if (Wave.waveBytes) { 148 | Wave.waveBytes[sample + start + 44] += ((samples[sample] >> 8) << 24) >> 24; 149 | } 150 | } 151 | } 152 | } 153 | 154 | if (loopCount > 1) { 155 | loopStart += 44; 156 | loopStop += 44; 157 | sampleCount += 44; 158 | totalSampleCount += 44; 159 | 160 | const endOffset: number = totalSampleCount - sampleCount; 161 | for (let sample: number = sampleCount - 1; sample >= loopStop; sample--) { 162 | if (Wave.waveBytes) { 163 | Wave.waveBytes[sample + endOffset] = Wave.waveBytes[sample]; 164 | } 165 | } 166 | 167 | for (let loop: number = 1; loop < loopCount; loop++) { 168 | const offset: number = (loopStop - loopStart) * loop; 169 | 170 | for (let sample: number = loopStart; sample < loopStop; sample++) { 171 | if (Wave.waveBytes) { 172 | Wave.waveBytes[sample + offset] = Wave.waveBytes[sample]; 173 | } 174 | } 175 | } 176 | 177 | totalSampleCount -= 44; 178 | } 179 | 180 | return totalSampleCount; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/config/NpcType.ts: -------------------------------------------------------------------------------- 1 | import { ConfigType } from '#/config/ConfigType.js'; 2 | 3 | import LruCache from '#/datastruct/LruCache.js'; 4 | 5 | import Model from '#/dash3d/Model.js'; 6 | 7 | import Jagfile from '#/io/Jagfile.js'; 8 | import Packet from '#/io/Packet.js'; 9 | 10 | import { TypedArray1d } from '#/util/Arrays.js'; 11 | 12 | export default class NpcType extends ConfigType { 13 | static count: number = 0; 14 | static idx: Int32Array | null = null; 15 | static data: Packet | null = null; 16 | static cache: (NpcType | null)[] | null = null; 17 | static cachePos: number = 0; 18 | name: string | null = null; 19 | desc: string | null = null; 20 | size: number = 1; 21 | models: Uint16Array | null = null; 22 | heads: Uint16Array | null = null; 23 | readyanim: number = -1; 24 | walkanim: number = -1; 25 | walkanim_b: number = -1; 26 | walkanim_r: number = -1; 27 | walkanim_l: number = -1; 28 | animHasAlpha: boolean = false; 29 | recol_s: Uint16Array | null = null; 30 | recol_d: Uint16Array | null = null; 31 | op: (string | null)[] | null = null; 32 | resizex: number = -1; 33 | resizey: number = -1; 34 | resizez: number = -1; 35 | minimap: boolean = true; 36 | vislevel: number = -1; 37 | resizeh: number = 128; 38 | resizev: number = 128; 39 | alwaysontop: boolean = false; 40 | headicon: number = -1; 41 | static modelCache: LruCache | null = new LruCache(30); 42 | ambient: number = 0; 43 | contrast: number = 0; 44 | 45 | static unpack(config: Jagfile): void { 46 | this.data = new Packet(config.read('npc.dat')); 47 | const idx: Packet = new Packet(config.read('npc.idx')); 48 | 49 | this.count = idx.g2(); 50 | this.idx = new Int32Array(this.count); 51 | 52 | let offset: number = 2; 53 | for (let id: number = 0; id < this.count; id++) { 54 | this.idx[id] = offset; 55 | offset += idx.g2(); 56 | } 57 | 58 | this.cache = new TypedArray1d(20, null); 59 | for (let id: number = 0; id < 20; id++) { 60 | this.cache[id] = new NpcType(-1); 61 | } 62 | } 63 | 64 | static get(id: number): NpcType { 65 | if (!this.cache || !this.idx || !this.data) { 66 | throw new Error(); 67 | } 68 | 69 | for (let i: number = 0; i < 20; i++) { 70 | const type: NpcType | null = this.cache[i]; 71 | if (type && type.id === id) { 72 | return type; 73 | } 74 | } 75 | 76 | this.cachePos = (this.cachePos + 1) % 20; 77 | 78 | const loc: NpcType = (this.cache[this.cachePos] = new NpcType(id)); 79 | this.data.pos = this.idx[id]; 80 | loc.unpackType(this.data); 81 | return loc; 82 | } 83 | 84 | unpack(code: number, dat: Packet): void { 85 | if (code === 1) { 86 | const count: number = dat.g1(); 87 | this.models = new Uint16Array(count); 88 | 89 | for (let i: number = 0; i < count; i++) { 90 | this.models[i] = dat.g2(); 91 | } 92 | } else if (code === 2) { 93 | this.name = dat.gjstr(); 94 | } else if (code === 3) { 95 | this.desc = dat.gjstr(); 96 | } else if (code === 12) { 97 | this.size = dat.g1b(); 98 | } else if (code === 13) { 99 | this.readyanim = dat.g2(); 100 | } else if (code === 14) { 101 | this.walkanim = dat.g2(); 102 | } else if (code === 16) { 103 | this.animHasAlpha = true; 104 | } else if (code === 17) { 105 | this.walkanim = dat.g2(); 106 | this.walkanim_b = dat.g2(); 107 | this.walkanim_r = dat.g2(); 108 | this.walkanim_l = dat.g2(); 109 | } else if (code >= 30 && code < 40) { 110 | if (!this.op) { 111 | this.op = new TypedArray1d(5, null); 112 | } 113 | 114 | this.op[code - 30] = dat.gjstr(); 115 | if (this.op[code - 30]?.toLowerCase() === 'hidden') { 116 | this.op[code - 30] = null; 117 | } 118 | } else if (code === 40) { 119 | const count: number = dat.g1(); 120 | this.recol_s = new Uint16Array(count); 121 | this.recol_d = new Uint16Array(count); 122 | 123 | for (let i: number = 0; i < count; i++) { 124 | this.recol_s[i] = dat.g2(); 125 | this.recol_d[i] = dat.g2(); 126 | } 127 | } else if (code === 60) { 128 | const count: number = dat.g1(); 129 | this.heads = new Uint16Array(count); 130 | 131 | for (let i: number = 0; i < count; i++) { 132 | this.heads[i] = dat.g2(); 133 | } 134 | } else if (code === 90) { 135 | this.resizex = dat.g2(); 136 | } else if (code === 91) { 137 | this.resizey = dat.g2(); 138 | } else if (code === 92) { 139 | this.resizez = dat.g2(); 140 | } else if (code === 93) { 141 | this.minimap = false; 142 | } else if (code === 95) { 143 | this.vislevel = dat.g2(); 144 | } else if (code === 97) { 145 | this.resizeh = dat.g2(); 146 | } else if (code === 98) { 147 | this.resizev = dat.g2(); 148 | } else if (code === 99) { 149 | this.alwaysontop = true; 150 | } else if (code === 100) { 151 | this.ambient = dat.g1b(); 152 | } else if (code === 101) { 153 | this.contrast = dat.g1b() * 5; 154 | } else if (code === 102) { 155 | this.headicon = dat.g2(); 156 | } 157 | } 158 | 159 | getModel(primaryTransformId: number, secondaryTransformId: number, seqMask: Int32Array | null): Model | null { 160 | let model: Model | null = null; 161 | 162 | if (NpcType.modelCache) { 163 | model = NpcType.modelCache.get(BigInt(this.id)) as Model | null; 164 | 165 | if (!model && this.models) { 166 | let ready = false; 167 | for (let i = 0; i < this.models.length; i++) { 168 | if (!Model.isReady(this.models[i])) { 169 | ready = true; 170 | } 171 | } 172 | if (ready) { 173 | return null; 174 | } 175 | 176 | const models: (Model | null)[] = new TypedArray1d(this.models.length, null); 177 | for (let i: number = 0; i < this.models.length; i++) { 178 | models[i] = Model.tryGet(this.models[i]); 179 | } 180 | 181 | if (models.length === 1) { 182 | model = models[0]; 183 | } else { 184 | model = Model.modelFromModels(models, models.length); 185 | } 186 | 187 | if (model) { 188 | if (this.recol_s && this.recol_d) { 189 | for (let i: number = 0; i < this.recol_s.length; i++) { 190 | model.recolour(this.recol_s[i], this.recol_d[i]); 191 | } 192 | } 193 | 194 | model.createLabelReferences(); 195 | model.calculateNormals(64, 850, -30, -50, -30, true); 196 | NpcType.modelCache.put(BigInt(this.id), model); 197 | } 198 | } 199 | } 200 | 201 | if (!model) { 202 | return null; 203 | } 204 | 205 | const tmp = Model.empty; 206 | tmp.set(model, !this.animHasAlpha); 207 | 208 | if (primaryTransformId !== -1 && secondaryTransformId !== -1) { 209 | tmp.applyTransforms(primaryTransformId, secondaryTransformId, seqMask); 210 | } else if (primaryTransformId !== -1) { 211 | tmp.applyTransform(primaryTransformId); 212 | } 213 | 214 | if (this.resizeh !== 128 || this.resizev !== 128) { 215 | tmp.scale(this.resizeh, this.resizev, this.resizeh); 216 | } 217 | 218 | tmp.calculateBoundsCylinder(); 219 | tmp.labelFaces = null; 220 | tmp.labelVertices = null; 221 | 222 | if (this.size === 1) { 223 | tmp.picking = true; 224 | } 225 | 226 | return tmp; 227 | } 228 | 229 | getHeadModel(): Model | null { 230 | if (!this.heads) { 231 | return null; 232 | } 233 | 234 | let exists = false; 235 | for (let i = 0; i < this.heads.length; i++) { 236 | if (!Model.isReady(this.heads[i])) { 237 | exists = true; 238 | } 239 | } 240 | if (exists) { 241 | return null; 242 | } 243 | 244 | const models: (Model | null)[] = new TypedArray1d(this.heads.length, null); 245 | for (let i: number = 0; i < this.heads.length; i++) { 246 | models[i] = Model.tryGet(this.heads[i]); 247 | } 248 | 249 | let model: Model | null; 250 | if (models.length === 1) { 251 | model = models[0]; 252 | } else { 253 | model = Model.modelFromModels(models, models.length); 254 | } 255 | 256 | if (model && this.recol_s && this.recol_d) { 257 | for (let i: number = 0; i < this.recol_s.length; i++) { 258 | model.recolour(this.recol_s[i], this.recol_d[i]); 259 | } 260 | } 261 | 262 | return model; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/io/Packet.ts: -------------------------------------------------------------------------------- 1 | import DoublyLinkable from '#/datastruct/DoublyLinkable.js'; 2 | import LinkList from '#/datastruct/LinkList.js'; 3 | 4 | import Isaac from '#/io/Isaac.js'; 5 | 6 | import { bigIntModPow, bigIntToBytes, bytesToBigInt } from '#/util/JsUtil.js'; 7 | 8 | export default class Packet extends DoublyLinkable { 9 | private static readonly CRC32_POLYNOMIAL: number = 0xedb88320; 10 | 11 | private static readonly crctable: Int32Array = new Int32Array(256); 12 | private static readonly bitmask: Uint32Array = new Uint32Array(33); 13 | 14 | private static readonly cacheMin: LinkList = new LinkList(); 15 | private static readonly cacheMid: LinkList = new LinkList(); 16 | private static readonly cacheMax: LinkList = new LinkList(); 17 | 18 | private static cacheMinCount: number = 0; 19 | private static cacheMidCount: number = 0; 20 | private static cacheMaxCount: number = 0; 21 | 22 | static { 23 | for (let i: number = 0; i < 32; i++) { 24 | Packet.bitmask[i] = (1 << i) - 1; 25 | } 26 | Packet.bitmask[32] = 0xffffffff; 27 | 28 | for (let i: number = 0; i < 256; i++) { 29 | let remainder: number = i; 30 | 31 | for (let bit: number = 0; bit < 8; bit++) { 32 | if ((remainder & 1) === 1) { 33 | remainder = (remainder >>> 1) ^ Packet.CRC32_POLYNOMIAL; 34 | } else { 35 | remainder >>>= 1; 36 | } 37 | } 38 | 39 | Packet.crctable[i] = remainder; 40 | } 41 | } 42 | 43 | static getcrc(src: Uint8Array, offset: number, length: number): number { 44 | let crc = 0xffffffff; 45 | for (let i = offset; i < length; i++) { 46 | crc = (crc >>> 8) ^ this.crctable[(crc ^ src[i]) & 0xff]; 47 | } 48 | return ~crc; 49 | } 50 | 51 | static checkcrc(src: Uint8Array, offset: number, length: number, expected: number = 0): boolean { 52 | return Packet.getcrc(src, offset, length) == expected; 53 | } 54 | 55 | // constructor 56 | private readonly view: DataView; 57 | readonly data: Uint8Array; 58 | 59 | // runtime 60 | pos: number = 0; 61 | bitPos: number = 0; 62 | random: Isaac | null = null; 63 | 64 | constructor(src: Uint8Array | Int8Array | null) { 65 | if (!src) { 66 | throw new Error(); 67 | } 68 | 69 | super(); 70 | 71 | if (src instanceof Int8Array) { 72 | this.data = new Uint8Array(src); 73 | } else { 74 | this.data = src; 75 | } 76 | 77 | this.view = new DataView(this.data.buffer, this.data.byteOffset, this.data.byteLength); 78 | } 79 | 80 | get length(): number { 81 | return this.view.byteLength; 82 | } 83 | 84 | get available(): number { 85 | return this.view.byteLength - this.pos; 86 | } 87 | 88 | static alloc(type: number): Packet { 89 | let cached: Packet | null = null; 90 | if (type === 0 && Packet.cacheMinCount > 0) { 91 | Packet.cacheMinCount--; 92 | cached = Packet.cacheMin.pop() as Packet | null; 93 | } else if (type === 1 && Packet.cacheMidCount > 0) { 94 | Packet.cacheMidCount--; 95 | cached = Packet.cacheMid.pop() as Packet | null; 96 | } else if (type === 2 && Packet.cacheMaxCount > 0) { 97 | Packet.cacheMaxCount--; 98 | cached = Packet.cacheMax.pop() as Packet | null; 99 | } 100 | 101 | if (cached) { 102 | cached.pos = 0; 103 | return cached; 104 | } 105 | 106 | if (type === 0) { 107 | return new Packet(new Uint8Array(100)); 108 | } else if (type === 1) { 109 | return new Packet(new Uint8Array(5000)); 110 | } else { 111 | return new Packet(new Uint8Array(30000)); 112 | } 113 | } 114 | 115 | release(): void { 116 | this.pos = 0; 117 | 118 | if (this.length === 100 && Packet.cacheMinCount < 1000) { 119 | Packet.cacheMin.push(this); 120 | Packet.cacheMinCount++; 121 | } else if (this.length === 5000 && Packet.cacheMidCount < 250) { 122 | Packet.cacheMid.push(this); 123 | Packet.cacheMidCount++; 124 | } else if (this.length === 30000 && Packet.cacheMaxCount < 50) { 125 | Packet.cacheMax.push(this); 126 | Packet.cacheMaxCount++; 127 | } 128 | } 129 | 130 | g1(): number { 131 | return this.view.getUint8(this.pos++); 132 | } 133 | 134 | // signed 135 | g1b(): number { 136 | return this.view.getInt8(this.pos++); 137 | } 138 | 139 | g2(): number { 140 | const result: number = this.view.getUint16(this.pos); 141 | this.pos += 2; 142 | return result; 143 | } 144 | 145 | // signed 146 | g2b(): number { 147 | const result: number = this.view.getInt16(this.pos); 148 | this.pos += 2; 149 | return result; 150 | } 151 | 152 | g3(): number { 153 | const result: number = (this.view.getUint8(this.pos++) << 16) | this.view.getUint16(this.pos); 154 | this.pos += 2; 155 | return result; 156 | } 157 | 158 | g4(): number { 159 | const result: number = this.view.getInt32(this.pos); 160 | this.pos += 4; 161 | return result; 162 | } 163 | 164 | g8(): bigint { 165 | const result: bigint = this.view.getBigInt64(this.pos); 166 | this.pos += 8; 167 | return result; 168 | } 169 | 170 | gsmart(): number { 171 | return this.view.getUint8(this.pos) < 0x80 ? this.g1() - 0x40 : this.g2() - 0xc000; 172 | } 173 | 174 | // signed 175 | gsmarts(): number { 176 | return this.view.getUint8(this.pos) < 0x80 ? this.g1() : this.g2() - 0x8000; 177 | } 178 | 179 | gjstr(): string { 180 | const view: DataView = this.view; 181 | const length: number = view.byteLength; 182 | let str: string = ''; 183 | let b: number; 184 | while ((b = view.getUint8(this.pos++)) !== 10 && this.pos < length) { 185 | str += String.fromCharCode(b); 186 | } 187 | return str; 188 | } 189 | 190 | gdata(length: number, offset: number, dest: Uint8Array | Int8Array): void { 191 | dest.set(this.data.subarray(this.pos, this.pos + length), offset); 192 | this.pos += length; 193 | } 194 | 195 | p1isaac(opcode: number): void { 196 | this.view.setUint8(this.pos++, (opcode + (this.random?.nextInt ?? 0)) & 0xff); 197 | } 198 | 199 | p1(value: number): void { 200 | this.view.setUint8(this.pos++, value); 201 | } 202 | 203 | p2(value: number): void { 204 | this.view.setUint16(this.pos, value); 205 | this.pos += 2; 206 | } 207 | 208 | ip2(value: number): void { 209 | this.view.setUint16(this.pos, value, true); 210 | this.pos += 2; 211 | } 212 | 213 | p3(value: number): void { 214 | this.view.setUint8(this.pos++, value >> 16); 215 | this.view.setUint16(this.pos, value); 216 | this.pos += 2; 217 | } 218 | 219 | p4(value: number): void { 220 | this.view.setInt32(this.pos, value); 221 | this.pos += 4; 222 | } 223 | 224 | ip4(value: number): void { 225 | this.view.setInt32(this.pos, value, true); 226 | this.pos += 4; 227 | } 228 | 229 | p8(value: bigint): void { 230 | this.view.setBigInt64(this.pos, value); 231 | this.pos += 8; 232 | } 233 | 234 | pjstr(str: string): void { 235 | const view: DataView = this.view; 236 | const length: number = str.length; 237 | for (let i: number = 0; i < length; i++) { 238 | view.setUint8(this.pos++, str.charCodeAt(i)); 239 | } 240 | view.setUint8(this.pos++, 10); 241 | } 242 | 243 | pdata(src: Uint8Array, length: number, offset: number): void { 244 | this.data.set(src.subarray(offset, offset + length), this.pos); 245 | this.pos += length - offset; 246 | } 247 | 248 | psize1(size: number): void { 249 | this.view.setUint8(this.pos - size - 1, size); 250 | } 251 | 252 | bits(): void { 253 | this.bitPos = this.pos << 3; 254 | } 255 | 256 | bytes(): void { 257 | this.pos = (this.bitPos + 7) >>> 3; 258 | } 259 | 260 | gBit(n: number): number { 261 | let bytePos: number = this.bitPos >>> 3; 262 | let remaining: number = 8 - (this.bitPos & 7); 263 | let value: number = 0; 264 | this.bitPos += n; 265 | 266 | for (; n > remaining; remaining = 8) { 267 | value += (this.view.getUint8(bytePos++) & Packet.bitmask[remaining]) << (n - remaining); 268 | n -= remaining; 269 | } 270 | 271 | if (n === remaining) { 272 | value += this.view.getUint8(bytePos) & Packet.bitmask[remaining]; 273 | } else { 274 | value += (this.view.getUint8(bytePos) >>> (remaining - n)) & Packet.bitmask[n]; 275 | } 276 | 277 | return value; 278 | } 279 | 280 | rsaenc(mod: bigint, exp: bigint): void { 281 | const length: number = this.pos; 282 | this.pos = 0; 283 | 284 | const temp: Uint8Array = new Uint8Array(length); 285 | this.gdata(length, 0, temp); 286 | 287 | const bigRaw: bigint = bytesToBigInt(temp); 288 | const bigEnc: bigint = bigIntModPow(bigRaw, exp, mod); 289 | const rawEnc: Uint8Array = bigIntToBytes(bigEnc); 290 | 291 | this.pos = 0; 292 | this.p1(rawEnc.length); 293 | this.pdata(rawEnc, rawEnc.length, 0); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/client/InputTracking.ts: -------------------------------------------------------------------------------- 1 | import Packet from '#/io/Packet.js'; 2 | 3 | export default class InputTracking { 4 | static enabled: boolean = false; 5 | static outBuffer: Packet | null = null; 6 | static oldBuffer: Packet | null = null; 7 | static lastTime: number = 0; 8 | static trackedCount: number = 0; 9 | static lastMoveTime: number = 0; 10 | static lastX: number = 0; 11 | static lastY: number = 0; 12 | 13 | static setEnabled(): void { 14 | this.outBuffer = Packet.alloc(1); 15 | this.oldBuffer = null; 16 | this.lastTime = performance.now(); 17 | this.enabled = true; 18 | } 19 | 20 | static setDisabled(): void { 21 | this.enabled = false; 22 | this.outBuffer = null; 23 | this.oldBuffer = null; 24 | } 25 | 26 | static flush(): Packet | null { 27 | let buffer: Packet | null = null; 28 | 29 | if (this.oldBuffer && this.enabled) { 30 | buffer = this.oldBuffer; 31 | } 32 | 33 | this.oldBuffer = null; 34 | return buffer; 35 | } 36 | 37 | static stop(): Packet | null { 38 | let buffer: Packet | null = null; 39 | 40 | if (this.outBuffer && this.outBuffer.pos > 0 && this.enabled) { 41 | buffer = this.outBuffer; 42 | } 43 | 44 | this.setDisabled(); 45 | return buffer; 46 | } 47 | 48 | private static ensureCapacity(n: number): void { 49 | if (!this.outBuffer) { 50 | return; 51 | } 52 | 53 | if (this.outBuffer.pos + n >= 500) { 54 | const buffer: Packet = this.outBuffer; 55 | this.outBuffer = Packet.alloc(1); 56 | this.oldBuffer = buffer; 57 | } 58 | } 59 | 60 | static mousePressed(x: number, y: number, button: number, _pointerType: string): void { 61 | if (!this.outBuffer) { 62 | return; 63 | } 64 | 65 | if (!this.enabled && (x >= 0 && x < 789 && y >= 0 && y < 532)) { 66 | return; 67 | } 68 | 69 | this.trackedCount++; 70 | 71 | const now: number = performance.now(); 72 | let delta: number = ((now - this.lastTime) / 10) | 0; 73 | if (delta > 250) { 74 | delta = 250; 75 | } 76 | 77 | this.lastTime = now; 78 | this.ensureCapacity(5); 79 | 80 | if (button === 2) { 81 | this.outBuffer.p1(1); 82 | } else { 83 | this.outBuffer.p1(2); 84 | } 85 | 86 | this.outBuffer.p1(delta); 87 | this.outBuffer.p3(x + (y << 10)); 88 | } 89 | 90 | static mouseReleased(button: number, _pointerType: string): void { 91 | if (!this.outBuffer) { 92 | return; 93 | } 94 | 95 | if (!this.enabled) { 96 | return; 97 | } 98 | 99 | this.trackedCount++; 100 | 101 | const now: number = performance.now(); 102 | let delta: number = ((now - this.lastTime) / 10) | 0; 103 | if (delta > 250) { 104 | delta = 250; 105 | } 106 | 107 | this.lastTime = now; 108 | this.ensureCapacity(2); 109 | 110 | if (button === 2) { 111 | this.outBuffer.p1(3); 112 | } else { 113 | this.outBuffer.p1(4); 114 | } 115 | 116 | this.outBuffer.p1(delta); 117 | } 118 | 119 | static mouseMoved(x: number, y: number, _pointerType: string): void { 120 | if (!this.outBuffer) { 121 | return; 122 | } 123 | 124 | if (!this.enabled && (x >= 0 && x < 789 && y >= 0 && y < 532)) { 125 | return; 126 | } 127 | 128 | const now: number = performance.now(); 129 | if (now - this.lastMoveTime < 50) { 130 | return; 131 | } 132 | 133 | this.lastMoveTime = now; 134 | this.trackedCount++; 135 | 136 | let delta: number = ((now - this.lastTime) / 10) | 0; 137 | if (delta > 250) { 138 | delta = 250; 139 | } 140 | 141 | this.lastTime = now; 142 | 143 | if (x - this.lastX < 8 && x - this.lastX >= -8 && y - this.lastY < 8 && y - this.lastY >= -8) { 144 | this.ensureCapacity(3); 145 | this.outBuffer.p1(5); 146 | this.outBuffer.p1(delta); 147 | this.outBuffer.p1(x + ((y - this.lastY + 8) << 4) + 8 - this.lastX); 148 | } else if (x - this.lastX < 128 && x - this.lastX >= -128 && y - this.lastY < 128 && y - this.lastY >= -128) { 149 | this.ensureCapacity(4); 150 | this.outBuffer.p1(6); 151 | this.outBuffer.p1(delta); 152 | this.outBuffer.p1(x + 128 - this.lastX); 153 | this.outBuffer.p1(y + 128 - this.lastY); 154 | } else { 155 | this.ensureCapacity(5); 156 | this.outBuffer.p1(7); 157 | this.outBuffer.p1(delta); 158 | this.outBuffer.p3(x + (y << 10)); 159 | } 160 | 161 | this.lastX = x; 162 | this.lastY = y; 163 | } 164 | 165 | static keyPressed(key: number): void { 166 | if (!this.outBuffer) { 167 | return; 168 | } 169 | 170 | if (!this.enabled) { 171 | return; 172 | } 173 | 174 | this.trackedCount++; 175 | 176 | const now: number = performance.now(); 177 | let delta: number = ((now - this.lastTime) / 10) | 0; 178 | if (delta > 250) { 179 | delta = 250; 180 | } 181 | 182 | this.lastTime = now; 183 | 184 | if (key === 1000) { 185 | key = 11; 186 | } else if (key === 1001) { 187 | key = 12; 188 | } else if (key === 1002) { 189 | key = 14; 190 | } else if (key === 1003) { 191 | key = 15; 192 | } else if (key >= 1008) { 193 | key -= 992; 194 | } 195 | 196 | this.ensureCapacity(3); 197 | this.outBuffer.p1(8); 198 | this.outBuffer.p1(delta); 199 | this.outBuffer.p1(key); 200 | } 201 | 202 | static keyReleased(key: number): void { 203 | if (!this.outBuffer) { 204 | return; 205 | } 206 | 207 | if (!this.enabled) { 208 | return; 209 | } 210 | 211 | this.trackedCount++; 212 | 213 | const now: number = performance.now(); 214 | let delta: number = ((now - this.lastTime) / 10) | 0; 215 | if (delta > 250) { 216 | delta = 250; 217 | } 218 | 219 | this.lastTime = now; 220 | 221 | if (key === 1000) { 222 | key = 11; 223 | } else if (key === 1001) { 224 | key = 12; 225 | } else if (key === 1002) { 226 | key = 14; 227 | } else if (key === 1003) { 228 | key = 15; 229 | } else if (key >= 1008) { 230 | key -= 992; 231 | } 232 | 233 | this.ensureCapacity(3); 234 | this.outBuffer.p1(9); 235 | this.outBuffer.p1(delta); 236 | this.outBuffer.p1(key); 237 | } 238 | 239 | static focusGained(): void { 240 | if (!this.outBuffer) { 241 | return; 242 | } 243 | 244 | if (!this.enabled) { 245 | return; 246 | } 247 | 248 | this.trackedCount++; 249 | 250 | const now: number = performance.now(); 251 | let delta: number = ((now - this.lastTime) / 10) | 0; 252 | if (delta > 250) { 253 | delta = 250; 254 | } 255 | 256 | this.lastTime = now; 257 | 258 | this.ensureCapacity(2); 259 | this.outBuffer.p1(10); 260 | this.outBuffer.p1(delta); 261 | } 262 | 263 | static focusLost(): void { 264 | if (!this.outBuffer) { 265 | return; 266 | } 267 | 268 | if (!this.enabled) { 269 | return; 270 | } 271 | 272 | this.trackedCount++; 273 | 274 | const now: number = performance.now(); 275 | let delta: number = ((now - this.lastTime) / 10) | 0; 276 | if (delta > 250) { 277 | delta = 250; 278 | } 279 | 280 | this.lastTime = now; 281 | 282 | this.ensureCapacity(2); 283 | this.outBuffer.p1(11); 284 | this.outBuffer.p1(delta); 285 | } 286 | 287 | static mouseEntered(): void { 288 | if (!this.outBuffer) { 289 | return; 290 | } 291 | 292 | if (!this.enabled) { 293 | return; 294 | } 295 | 296 | this.trackedCount++; 297 | 298 | const now: number = performance.now(); 299 | let delta: number = ((now - this.lastTime) / 10) | 0; 300 | if (delta > 250) { 301 | delta = 250; 302 | } 303 | 304 | this.lastTime = now; 305 | 306 | this.ensureCapacity(2); 307 | this.outBuffer.p1(12); 308 | this.outBuffer.p1(delta); 309 | } 310 | 311 | static mouseExited(): void { 312 | if (!this.outBuffer) { 313 | return; 314 | } 315 | 316 | if (!this.enabled) { 317 | return; 318 | } 319 | 320 | this.trackedCount++; 321 | 322 | const now: number = performance.now(); 323 | let delta: number = ((now - this.lastTime) / 10) | 0; 324 | if (delta > 250) { 325 | delta = 250; 326 | } 327 | 328 | this.lastTime = now; 329 | 330 | this.ensureCapacity(2); 331 | this.outBuffer.p1(13); 332 | this.outBuffer.p1(delta); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/sound/Tone.ts: -------------------------------------------------------------------------------- 1 | import Packet from '#/io/Packet.js'; 2 | 3 | import Envelope from '#/sound/Envelope.js'; 4 | 5 | export default class Tone { 6 | frequencyBase: Envelope | null = null; 7 | amplitudeBase: Envelope | null = null; 8 | frequencyModRate: Envelope | null = null; 9 | frequencyModRange: Envelope | null = null; 10 | amplitudeModRate: Envelope | null = null; 11 | amplitudeModRange: Envelope | null = null; 12 | release: Envelope | null = null; 13 | attack: Envelope | null = null; 14 | 15 | harmonicVolume: Int32Array = new Int32Array(5); 16 | harmonicSemitone: Int32Array = new Int32Array(5); 17 | harmonicDelay: Int32Array = new Int32Array(5); 18 | 19 | reverbDelay: number = 0; 20 | reverbVolume: number = 100; 21 | start: number = 0; 22 | length: number = 500; 23 | 24 | static buffer: Int32Array | null = null; 25 | static noise: Int32Array | null = null; 26 | static sin: Int32Array | null = null; 27 | 28 | static tmpPhases: Int32Array = new Int32Array(5); 29 | static tmpDelays: Int32Array = new Int32Array(5); 30 | static tmpVolumes: Int32Array = new Int32Array(5); 31 | static tmpSemitones: Int32Array = new Int32Array(5); 32 | static tmpStarts: Int32Array = new Int32Array(5); 33 | 34 | static init(): void { 35 | this.noise = new Int32Array(32768); 36 | for (let i: number = 0; i < 32768; i++) { 37 | if (Math.random() > 0.5) { 38 | this.noise[i] = 1; 39 | } else { 40 | this.noise[i] = -1; 41 | } 42 | } 43 | 44 | this.sin = new Int32Array(32768); 45 | for (let i: number = 0; i < 32768; i++) { 46 | this.sin[i] = (Math.sin(i / 5215.1903) * 16384.0) | 0; 47 | } 48 | 49 | this.buffer = new Int32Array(22050 * 10); // 22050 KHz * 10s 50 | } 51 | 52 | generate(sampleCount: number, length: number): Int32Array { 53 | if (!this.frequencyBase || !this.amplitudeBase) { 54 | return Tone.buffer!; 55 | } 56 | 57 | for (let sample: number = 0; sample < sampleCount; sample++) { 58 | Tone.buffer![sample] = 0; 59 | } 60 | 61 | if (length < 10) { 62 | return Tone.buffer!; 63 | } 64 | 65 | const samplesPerStep: number = (sampleCount / length) | 0; 66 | 67 | this.frequencyBase.reset(); 68 | this.amplitudeBase.reset(); 69 | 70 | let frequencyStart: number = 0; 71 | let frequencyDuration: number = 0; 72 | let frequencyPhase: number = 0; 73 | 74 | if (this.frequencyModRate && this.frequencyModRange) { 75 | this.frequencyModRate.reset(); 76 | this.frequencyModRange.reset(); 77 | frequencyStart = (((this.frequencyModRate.end - this.frequencyModRate.start) * 32.768) / samplesPerStep) | 0; 78 | frequencyDuration = ((this.frequencyModRate.start * 32.768) / samplesPerStep) | 0; 79 | } 80 | 81 | let amplitudeStart: number = 0; 82 | let amplitudeDuration: number = 0; 83 | let amplitudePhase: number = 0; 84 | if (this.amplitudeModRate && this.amplitudeModRange) { 85 | this.amplitudeModRate.reset(); 86 | this.amplitudeModRange.reset(); 87 | amplitudeStart = (((this.amplitudeModRate.end - this.amplitudeModRate.start) * 32.768) / samplesPerStep) | 0; 88 | amplitudeDuration = ((this.amplitudeModRate.start * 32.768) / samplesPerStep) | 0; 89 | } 90 | 91 | for (let harmonic: number = 0; harmonic < 5; harmonic++) { 92 | if (this.frequencyBase && this.harmonicVolume[harmonic] !== 0) { 93 | Tone.tmpPhases[harmonic] = 0; 94 | Tone.tmpDelays[harmonic] = this.harmonicDelay[harmonic] * samplesPerStep; 95 | Tone.tmpVolumes[harmonic] = ((this.harmonicVolume[harmonic] << 14) / 100) | 0; 96 | Tone.tmpSemitones[harmonic] = (((this.frequencyBase.end - this.frequencyBase.start) * 32.768 * Math.pow(1.0057929410678534, this.harmonicSemitone[harmonic])) / samplesPerStep) | 0; 97 | Tone.tmpStarts[harmonic] = ((this.frequencyBase.start * 32.768) / samplesPerStep) | 0; 98 | } 99 | } 100 | 101 | for (let sample: number = 0; sample < sampleCount; sample++) { 102 | let frequency: number = this.frequencyBase.evaluate(sampleCount); 103 | let amplitude: number = this.amplitudeBase.evaluate(sampleCount); 104 | 105 | if (this.frequencyModRate && this.frequencyModRange) { 106 | const rate: number = this.frequencyModRate.evaluate(sampleCount); 107 | const range: number = this.frequencyModRange.evaluate(sampleCount); 108 | frequency += this.generate2(range, frequencyPhase, this.frequencyModRate.form) >> 1; 109 | frequencyPhase += ((rate * frequencyStart) >> 16) + frequencyDuration; 110 | } 111 | 112 | if (this.amplitudeModRate && this.amplitudeModRange) { 113 | const rate: number = this.amplitudeModRate.evaluate(sampleCount); 114 | const range: number = this.amplitudeModRange.evaluate(sampleCount); 115 | amplitude = (amplitude * ((this.generate2(range, amplitudePhase, this.amplitudeModRate.form) >> 1) + 32768)) >> 15; 116 | amplitudePhase += ((rate * amplitudeStart) >> 16) + amplitudeDuration; 117 | } 118 | 119 | for (let harmonic: number = 0; harmonic < 5; harmonic++) { 120 | if (this.harmonicVolume[harmonic] !== 0) { 121 | const position: number = sample + Tone.tmpDelays[harmonic]; 122 | 123 | if (position < sampleCount) { 124 | Tone.buffer![position] += this.generate2((amplitude * Tone.tmpVolumes[harmonic]) >> 15, Tone.tmpPhases[harmonic], this.frequencyBase.form); 125 | Tone.tmpPhases[harmonic] += ((frequency * Tone.tmpSemitones[harmonic]) >> 16) + Tone.tmpStarts[harmonic]; 126 | } 127 | } 128 | } 129 | } 130 | 131 | if (this.release && this.attack) { 132 | this.release.reset(); 133 | this.attack.reset(); 134 | 135 | let counter: number = 0; 136 | let muted: boolean = true; 137 | 138 | for (let sample: number = 0; sample < sampleCount; sample++) { 139 | const releaseValue: number = this.release.evaluate(sampleCount); 140 | const attackValue: number = this.attack.evaluate(sampleCount); 141 | 142 | let threshold: number; 143 | if (muted) { 144 | threshold = this.release.start + (((this.release.end - this.release.start) * releaseValue) >> 8); 145 | } else { 146 | threshold = this.release.start + (((this.release.end - this.release.start) * attackValue) >> 8); 147 | } 148 | 149 | counter += 256; 150 | if (counter >= threshold) { 151 | counter = 0; 152 | muted = !muted; 153 | } 154 | 155 | if (muted) { 156 | Tone.buffer![sample] = 0; 157 | } 158 | } 159 | } 160 | 161 | if (this.reverbDelay > 0 && this.reverbVolume > 0) { 162 | const start: number = this.reverbDelay * samplesPerStep; 163 | 164 | for (let sample: number = start; sample < sampleCount; sample++) { 165 | Tone.buffer![sample] += ((Tone.buffer![sample - start] * this.reverbVolume) / 100) | 0; 166 | Tone.buffer![sample] |= 0; 167 | } 168 | } 169 | 170 | for (let sample: number = 0; sample < sampleCount; sample++) { 171 | if (Tone.buffer![sample] < -32768) { 172 | Tone.buffer![sample] = -32768; 173 | } 174 | 175 | if (Tone.buffer![sample] > 32767) { 176 | Tone.buffer![sample] = 32767; 177 | } 178 | } 179 | 180 | return Tone.buffer!; 181 | } 182 | 183 | generate2(amplitude: number, phase: number, form: number): number { 184 | if (form === 1) { 185 | return (phase & 0x7fff) < 16384 ? amplitude : -amplitude; 186 | } else if (form === 2) { 187 | return (Tone.sin![phase & 0x7fff] * amplitude) >> 14; 188 | } else if (form === 3) { 189 | return (((phase & 0x7fff) * amplitude) >> 14) - amplitude; 190 | } else if (form === 4) { 191 | return Tone.noise![((phase / 2607) | 0) & 0x7fff] * amplitude; 192 | } else { 193 | return 0; 194 | } 195 | } 196 | 197 | unpack(dat: Packet): void { 198 | this.frequencyBase = new Envelope(); 199 | this.frequencyBase.unpack(dat); 200 | 201 | this.amplitudeBase = new Envelope(); 202 | this.amplitudeBase.unpack(dat); 203 | 204 | if (dat.g1() !== 0) { 205 | dat.pos--; 206 | 207 | this.frequencyModRate = new Envelope(); 208 | this.frequencyModRate.unpack(dat); 209 | 210 | this.frequencyModRange = new Envelope(); 211 | this.frequencyModRange.unpack(dat); 212 | } 213 | 214 | if (dat.g1() !== 0) { 215 | dat.pos--; 216 | 217 | this.amplitudeModRate = new Envelope(); 218 | this.amplitudeModRate.unpack(dat); 219 | 220 | this.amplitudeModRange = new Envelope(); 221 | this.amplitudeModRange.unpack(dat); 222 | } 223 | 224 | if (dat.g1() !== 0) { 225 | dat.pos--; 226 | 227 | this.release = new Envelope(); 228 | this.release.unpack(dat); 229 | 230 | this.attack = new Envelope(); 231 | this.attack.unpack(dat); 232 | } 233 | 234 | for (let harmonic: number = 0; harmonic < 10; harmonic++) { 235 | const volume: number = dat.gsmarts(); 236 | if (volume === 0) { 237 | break; 238 | } 239 | 240 | this.harmonicVolume[harmonic] = volume; 241 | this.harmonicSemitone[harmonic] = dat.gsmart(); 242 | this.harmonicDelay[harmonic] = dat.gsmarts(); 243 | } 244 | 245 | this.reverbDelay = dat.gsmarts(); 246 | this.reverbVolume = dat.gsmarts(); 247 | this.length = dat.g2(); 248 | this.start = dat.g2(); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/3rdparty/tinymidipcm.js: -------------------------------------------------------------------------------- 1 | import loadTinyMidiPCM from '#3rdparty/tinymidipcm/tinymidipcm.mjs'; 2 | 3 | class TinyMidiPCM { 4 | constructor(options = {}) { 5 | this.wasmModule = undefined; 6 | 7 | this.soundfontBufferPtr = 0; 8 | this.soundfontPtr = 0; 9 | 10 | this.midiBufferPtr = 0; 11 | 12 | this.renderInterval = options.renderInterval || 100; 13 | 14 | this.sampleRate = options.sampleRate || 44100; 15 | this.channels = options.channels || 2; 16 | this.gain = options.gain || 0; 17 | 18 | if (!options.bufferSize) { 19 | this.setBufferDuration(1); 20 | } else { 21 | this.bufferSize = options.bufferSize; 22 | } 23 | 24 | this.onPCMData = options.onPCMData || (() => {}); 25 | this.onRenderEnd = options.onRenderEnd || (() => {}); 26 | 27 | this.renderTimer = undefined; 28 | 29 | this.test = 0; 30 | } 31 | 32 | async init() { 33 | if (this.wasmModule) { 34 | return; 35 | } 36 | 37 | // check if node 38 | // http://philiplassen.com/2021/08/11/node-es6-emscripten.html 39 | // if (typeof process !== 'undefined') { 40 | // const { dirname } = await import(/* webpackIgnore: true */ 'path'); 41 | // const { createRequire } = await import(/* webpackIgnore: true */ 'module'); 42 | 43 | // globalThis.__dirname = dirname(import.meta.url); 44 | // globalThis.require = createRequire(import.meta.url); 45 | // } 46 | 47 | this.wasmModule = await loadTinyMidiPCM(); 48 | 49 | this.pcmBufferPtr = this.wasmModule._malloc(this.bufferSize); 50 | this.msecsPtr = this.wasmModule._malloc(8); 51 | } 52 | 53 | // set buffer size based on seconds 54 | setBufferDuration(seconds) { 55 | this.bufferSize = 4 * this.sampleRate * this.channels * seconds; 56 | } 57 | 58 | ensureInitialized() { 59 | if (!this.wasmModule) { 60 | throw new Error(`${this.constructor.name} not initalized. call .init()`); 61 | } 62 | } 63 | 64 | setSoundfont(buffer) { 65 | this.ensureInitialized(); 66 | 67 | const { _malloc, _free, _tsf_load_memory, _tsf_set_output } = this.wasmModule; 68 | 69 | _free(this.soundfontBufferPtr); 70 | 71 | this.soundfontBufferPtr = _malloc(buffer.length); 72 | this.wasmModule.HEAPU8.set(buffer, this.soundfontBufferPtr); 73 | 74 | this.soundfontPtr = _tsf_load_memory(this.soundfontBufferPtr, buffer.length); 75 | 76 | _tsf_set_output(this.soundfontPtr, this.channels === 2 ? 0 : 2, this.sampleRate, this.gain); 77 | } 78 | 79 | getPCMBuffer() { 80 | this.ensureInitialized(); 81 | 82 | const pcm = new Uint8Array(this.bufferSize); 83 | 84 | pcm.set(this.wasmModule.HEAPU8.subarray(this.pcmBufferPtr, this.pcmBufferPtr + this.bufferSize)); 85 | 86 | return pcm; 87 | } 88 | 89 | getMIDIMessagePtr(midiBuffer) { 90 | const { _malloc, _free, _tml_load_memory } = this.wasmModule; 91 | 92 | _free(this.midiBufferPtr); 93 | 94 | this.midiBufferPtr = _malloc(midiBuffer.length); 95 | this.wasmModule.HEAPU8.set(midiBuffer, this.midiBufferPtr); 96 | 97 | return _tml_load_memory(this.midiBufferPtr, midiBuffer.length); 98 | } 99 | 100 | renderMIDIMessage(midiMessagePtr) { 101 | const { _midi_render } = this.wasmModule; 102 | 103 | return _midi_render(this.soundfontPtr, midiMessagePtr, this.channels, this.sampleRate, this.pcmBufferPtr, this.bufferSize, this.msecsPtr); 104 | } 105 | 106 | render(midiBuffer) { 107 | this.ensureInitialized(); 108 | 109 | if (!this.soundfontPtr) { 110 | throw new Error('no soundfont buffer set. call .setSoundfont'); 111 | } 112 | 113 | window.clearTimeout(this.renderTimer); 114 | 115 | const { setValue, getValue, _tsf_reset, _tsf_channel_set_bank_preset } = this.wasmModule; 116 | 117 | setValue(this.msecsPtr, 0, 'double'); 118 | 119 | _tsf_reset(this.soundfontPtr); 120 | _tsf_channel_set_bank_preset(this.soundfontPtr, 9, 128, 0); 121 | 122 | let midiMessagePtr = this.getMIDIMessagePtr(midiBuffer); 123 | 124 | const boundRender = function () { 125 | midiMessagePtr = this.renderMIDIMessage(midiMessagePtr); 126 | 127 | const pcm = this.getPCMBuffer(); 128 | 129 | this.onPCMData(pcm); 130 | 131 | if (midiMessagePtr) { 132 | this.renderTimer = setTimeout(boundRender, this.renderInterval); 133 | } else { 134 | this.onRenderEnd(getValue(this.msecsPtr, 'double')); 135 | } 136 | }.bind(this); 137 | 138 | this.renderTimer = setTimeout(() => { 139 | boundRender(); 140 | }, 16); 141 | } 142 | } 143 | 144 | // controlling tinymidipcm: 145 | (async () => { 146 | const channels = 2; 147 | const sampleRate = 44100; 148 | const flushTime = 250; 149 | const renderInterval = 30; 150 | const fadeseconds = 2; 151 | 152 | let midiTimeout = null; 153 | let fadeTimeout = null; 154 | // let renderEndSeconds = 0; 155 | // let currentMidiBuffer = null; 156 | let samples = new Float32Array(); 157 | 158 | let gainNode = window.audioContext.createGain(); 159 | gainNode.gain.setValueAtTime(0.1, window.audioContext.currentTime); 160 | gainNode.connect(window.audioContext.destination); 161 | 162 | // let startTime = 0; 163 | let lastTime = window.audioContext.currentTime; 164 | let bufferSources = []; 165 | 166 | const tinyMidiPCM = new TinyMidiPCM({ 167 | renderInterval, 168 | onPCMData: pcm => { 169 | let float32 = new Float32Array(pcm.buffer); 170 | let temp = new Float32Array(samples.length + float32.length); 171 | temp.set(samples, 0); 172 | temp.set(float32, samples.length); 173 | samples = temp; 174 | }, 175 | onRenderEnd: ms => { 176 | // renderEndSeconds = Math.floor(startTime + Math.floor(ms / 1000)); 177 | }, 178 | bufferSize: 1024 * 100 179 | }); 180 | 181 | await tinyMidiPCM.init(); 182 | 183 | const soundfontRes = await fetch(new URL('SCC1_Florestan.sf2', import.meta.url)); 184 | const soundfontBuffer = new Uint8Array(await soundfontRes.arrayBuffer()); 185 | tinyMidiPCM.setSoundfont(soundfontBuffer); 186 | 187 | function flush() { 188 | if (!window.audioContext || !samples.length) { 189 | return; 190 | } 191 | 192 | let bufferSource = window.audioContext.createBufferSource(); 193 | // bufferSource.onended = function(event) { 194 | // const timeSeconds = Math.floor(window.audioContext.currentTime); 195 | 196 | // if (renderEndSeconds > 0 && Math.abs(timeSeconds - renderEndSeconds) <= 2) { 197 | // renderEndSeconds = 0; 198 | 199 | // if (currentMidiBuffer) { 200 | // // midi looping 201 | // // note: this was buggy with some midi files 202 | // window._tinyMidiPlay(currentMidiBuffer, -1); 203 | // } 204 | // } 205 | // } 206 | 207 | const length = samples.length / channels; 208 | const audioBuffer = window.audioContext.createBuffer(channels, length, sampleRate); 209 | 210 | for (let channel = 0; channel < channels; channel++) { 211 | const audioData = audioBuffer.getChannelData(channel); 212 | 213 | let offset = channel; 214 | for (let i = 0; i < length; i++) { 215 | audioData[i] = samples[offset]; 216 | offset += channels; 217 | } 218 | } 219 | 220 | if (lastTime < window.audioContext.currentTime) { 221 | lastTime = window.audioContext.currentTime; 222 | } 223 | 224 | bufferSource.buffer = audioBuffer; 225 | bufferSource.connect(gainNode); 226 | bufferSource.start(lastTime); 227 | bufferSources.push(bufferSource); 228 | 229 | lastTime += audioBuffer.duration; 230 | samples = new Float32Array(); 231 | } 232 | 233 | let flushInterval; 234 | 235 | function fadeOut(callback) { 236 | const currentTime = window.audioContext.currentTime; 237 | gainNode.gain.cancelScheduledValues(currentTime); 238 | gainNode.gain.setTargetAtTime(0, currentTime, 0.5); 239 | return setTimeout(callback, fadeseconds * 1000); 240 | } 241 | 242 | function stop() { 243 | if (flushInterval) { 244 | clearInterval(flushInterval); 245 | } 246 | 247 | // currentMidiBuffer = null; 248 | samples = new Float32Array(); 249 | 250 | if (bufferSources.length) { 251 | let temp = gainNode.gain.value; 252 | gainNode.gain.setValueAtTime(0, window.audioContext.currentTime); 253 | bufferSources.forEach(bufferSource => { 254 | bufferSource.stop(window.audioContext.currentTime); 255 | }); 256 | bufferSources = []; 257 | gainNode.gain.setValueAtTime(temp, window.audioContext.currentTime); 258 | } 259 | } 260 | 261 | function start(vol, midiBuffer) { 262 | // vol -1 = reuse last volume level 263 | if (vol !== -1) { 264 | window._tinyMidiVolume(vol); 265 | } 266 | 267 | // currentMidiBuffer = midiBuffer; 268 | // startTime = window.audioContext.currentTime; 269 | lastTime = window.audioContext.currentTime; 270 | flushInterval = setInterval(flush, flushTime); 271 | tinyMidiPCM.render(midiBuffer); 272 | } 273 | 274 | window._tinyMidiStop = async fade => { 275 | if (fade) { 276 | fadeTimeout = fadeOut(() => { 277 | stop(); 278 | }); 279 | } else { 280 | stop(); 281 | clearTimeout(midiTimeout); 282 | clearTimeout(fadeTimeout); 283 | } 284 | }; 285 | 286 | window._tinyMidiVolume = (vol = 1) => { 287 | gainNode.gain.setValueAtTime(vol, window.audioContext.currentTime); 288 | }; 289 | 290 | window._tinyMidiPlay = async (midiBuffer, vol, fade) => { 291 | if (!midiBuffer) { 292 | return; 293 | } 294 | 295 | await window._tinyMidiStop(fade); 296 | 297 | if (fade) { 298 | midiTimeout = setTimeout(() => { 299 | start(vol, midiBuffer); 300 | }, fadeseconds * 1000); 301 | } else { 302 | start(vol, midiBuffer); 303 | } 304 | }; 305 | })(); 306 | 307 | export function playMidi(data, vol, fade) { 308 | if (window._tinyMidiPlay) { 309 | window._tinyMidiPlay(data, vol / 128, fade); 310 | } 311 | } 312 | 313 | export function setMidiVolume(vol) { 314 | if (window._tinyMidiVolume) { 315 | window._tinyMidiVolume(vol / 128); 316 | } 317 | } 318 | 319 | export function stopMidi(fade) { 320 | if (window._tinyMidiStop) { 321 | window._tinyMidiStop(fade); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/graphics/Pix2D.ts: -------------------------------------------------------------------------------- 1 | import DoublyLinkable from '#/datastruct/DoublyLinkable.js'; 2 | 3 | export default class Pix2D extends DoublyLinkable { 4 | static pixels: Int32Array = new Int32Array(); 5 | 6 | static width2d: number = 0; 7 | static height2d: number = 0; 8 | 9 | static top: number = 0; 10 | static bottom: number = 0; 11 | static left: number = 0; 12 | static right: number = 0; 13 | static boundX: number = 0; 14 | 15 | static centerX2d: number = 0; 16 | static centerY2d: number = 0; 17 | 18 | static bind(pixels: Int32Array, width: number, height: number): void { 19 | this.pixels = pixels; 20 | this.width2d = width; 21 | this.height2d = height; 22 | this.setBounds(0, 0, width, height); 23 | } 24 | 25 | static resetBounds(): void { 26 | this.left = 0; 27 | this.top = 0; 28 | this.right = this.width2d; 29 | this.bottom = this.height2d; 30 | this.boundX = this.right - 1; 31 | this.centerX2d = (this.right / 2) | 0; 32 | } 33 | 34 | static setBounds(left: number, top: number, right: number, bottom: number): void { 35 | if (left < 0) { 36 | left = 0; 37 | } 38 | 39 | if (top < 0) { 40 | top = 0; 41 | } 42 | 43 | if (right > this.width2d) { 44 | right = this.width2d; 45 | } 46 | 47 | if (bottom > this.height2d) { 48 | bottom = this.height2d; 49 | } 50 | 51 | this.top = top; 52 | this.bottom = bottom; 53 | this.left = left; 54 | this.right = right; 55 | this.boundX = this.right - 1; 56 | this.centerX2d = (this.right / 2) | 0; 57 | this.centerY2d = (this.bottom / 2) | 0; 58 | } 59 | 60 | static clear(): void { 61 | const len: number = this.width2d * this.height2d; 62 | for (let i: number = 0; i < len; i++) { 63 | this.pixels[i] = 0; 64 | } 65 | } 66 | 67 | static drawRect(x: number, y: number, w: number, h: number, color: number): void { 68 | this.drawHorizontalLine(x, y, color, w); 69 | this.drawHorizontalLine(x, y + h - 1, color, w); 70 | this.drawVerticalLine(x, y, color, h); 71 | this.drawVerticalLine(x + w - 1, y, color, h); 72 | } 73 | 74 | static drawRectAlpha(x: number, y: number, w: number, h: number, color: number, alpha: number): void { 75 | this.drawHorizontalLineAlpha(x, y, color, w, alpha); 76 | this.drawHorizontalLineAlpha(x, y + h - 1, color, w, alpha); 77 | if (h >= 3) { 78 | this.drawVerticalLineAlpha(x, y, color, h, alpha); 79 | this.drawVerticalLineAlpha(x + w - 1, y, color, h, alpha); 80 | } 81 | } 82 | 83 | static drawHorizontalLine(x: number, y: number, color: number, width: number): void { 84 | if (y < this.top || y >= this.bottom) { 85 | return; 86 | } 87 | 88 | if (x < this.left) { 89 | width -= this.left - x; 90 | x = this.left; 91 | } 92 | 93 | if (x + width > this.right) { 94 | width = this.right - x; 95 | } 96 | 97 | const off: number = x + y * this.width2d; 98 | for (let i: number = 0; i < width; i++) { 99 | this.pixels[off + i] = color; 100 | } 101 | } 102 | 103 | static drawHorizontalLineAlpha = (x: number, y: number, color: number, width: number, alpha: number): void => { 104 | if (y < this.top || y >= this.bottom) { 105 | return; 106 | } 107 | 108 | if (x < this.left) { 109 | width -= this.left - x; 110 | x = this.left; 111 | } 112 | 113 | if (x + width > this.right) { 114 | width = this.right - x; 115 | } 116 | 117 | const invAlpha: number = 256 - alpha; 118 | const r0: number = ((color >> 16) & 0xff) * alpha; 119 | const g0: number = ((color >> 8) & 0xff) * alpha; 120 | const b0: number = (color & 0xff) * alpha; 121 | const step: number = this.width2d - width; 122 | let offset: number = x + y * this.width2d; 123 | for (let i: number = 0; i < width; i++) { 124 | const r1: number = ((this.pixels[offset] >> 16) & 0xff) * invAlpha; 125 | const g1: number = ((this.pixels[offset] >> 8) & 0xff) * invAlpha; 126 | const b1: number = (this.pixels[offset] & 0xff) * invAlpha; 127 | const color: number = (((r0 + r1) >> 8) << 16) + (((g0 + g1) >> 8) << 8) + ((b0 + b1) >> 8); 128 | this.pixels[offset++] = color; 129 | } 130 | }; 131 | 132 | static drawVerticalLine(x: number, y: number, color: number, height: number): void { 133 | if (x < this.left || x >= this.right) { 134 | return; 135 | } 136 | 137 | if (y < this.top) { 138 | height -= this.top - y; 139 | y = this.top; 140 | } 141 | 142 | if (y + height > this.bottom) { 143 | height = this.bottom - y; 144 | } 145 | 146 | const off: number = x + y * this.width2d; 147 | for (let i: number = 0; i < height; i++) { 148 | this.pixels[off + i * this.width2d] = color; 149 | } 150 | } 151 | 152 | static drawVerticalLineAlpha = (x: number, y: number, color: number, height: number, alpha: number): void => { 153 | if (x < this.left || x >= this.right) { 154 | return; 155 | } 156 | 157 | if (y < this.top) { 158 | height -= this.top - y; 159 | y = this.top; 160 | } 161 | 162 | if (y + height > this.bottom) { 163 | height = this.bottom - y; 164 | } 165 | 166 | const invAlpha: number = 256 - alpha; 167 | const r0: number = ((color >> 16) & 0xff) * alpha; 168 | const g0: number = ((color >> 8) & 0xff) * alpha; 169 | const b0: number = (color & 0xff) * alpha; 170 | let offset: number = x + y * this.width2d; 171 | for (let i: number = 0; i < height; i++) { 172 | const r1: number = ((this.pixels[offset] >> 16) & 0xff) * invAlpha; 173 | const g1: number = ((this.pixels[offset] >> 8) & 0xff) * invAlpha; 174 | const b1: number = (this.pixels[offset] & 0xff) * invAlpha; 175 | const color: number = (((r0 + r1) >> 8) << 16) + (((g0 + g1) >> 8) << 8) + ((b0 + b1) >> 8); 176 | this.pixels[offset] = color; 177 | offset += this.width2d; 178 | } 179 | }; 180 | 181 | static drawLine(x1: number, y1: number, x2: number, y2: number, color: number): void { 182 | const dx: number = Math.abs(x2 - x1); 183 | const dy: number = Math.abs(y2 - y1); 184 | 185 | const sx: number = x1 < x2 ? 1 : -1; 186 | const sy: number = y1 < y2 ? 1 : -1; 187 | 188 | let err: number = dx - dy; 189 | 190 | // eslint-disable-next-line no-constant-condition 191 | while (true) { 192 | if (x1 >= this.left && x1 < this.right && y1 >= this.top && y1 < this.bottom) { 193 | this.pixels[x1 + y1 * this.width2d] = color; 194 | } 195 | 196 | if (x1 === x2 && y1 === y2) { 197 | break; 198 | } 199 | 200 | const e2: number = 2 * err; 201 | 202 | if (e2 > -dy) { 203 | err = err - dy; 204 | x1 = x1 + sx; 205 | } 206 | 207 | if (e2 < dx) { 208 | err = err + dx; 209 | y1 = y1 + sy; 210 | } 211 | } 212 | } 213 | 214 | static fillRect2d(x: number, y: number, width: number, height: number, color: number): void { 215 | if (x < this.left) { 216 | width -= this.left - x; 217 | x = this.left; 218 | } 219 | 220 | if (y < this.top) { 221 | height -= this.top - y; 222 | y = this.top; 223 | } 224 | 225 | if (x + width > this.right) { 226 | width = this.right - x; 227 | } 228 | 229 | if (y + height > this.bottom) { 230 | height = this.bottom - y; 231 | } 232 | 233 | const step: number = this.width2d - width; 234 | let offset: number = x + y * this.width2d; 235 | for (let i: number = -height; i < 0; i++) { 236 | for (let j: number = -width; j < 0; j++) { 237 | this.pixels[offset++] = color; 238 | } 239 | 240 | offset += step; 241 | } 242 | } 243 | 244 | static fillRectAlpha(x: number, y: number, width: number, height: number, rgb: number, alpha: number): void { 245 | if (x < this.left) { 246 | width -= this.left - x; 247 | x = this.left; 248 | } 249 | 250 | if (y < this.top) { 251 | height -= this.top - y; 252 | y = this.top; 253 | } 254 | 255 | if (x + width > this.right) { 256 | width = this.right - x; 257 | } 258 | 259 | if (y + height > this.bottom) { 260 | height = this.bottom - y; 261 | } 262 | 263 | const invAlpha: number = 256 - alpha; 264 | const r0: number = ((rgb >> 16) & 0xff) * alpha; 265 | const g0: number = ((rgb >> 8) & 0xff) * alpha; 266 | const b0: number = (rgb & 0xff) * alpha; 267 | const step: number = this.width2d - width; 268 | let offset: number = x + y * this.width2d; 269 | for (let i: number = 0; i < height; i++) { 270 | for (let j: number = -width; j < 0; j++) { 271 | const r1: number = ((this.pixels[offset] >> 16) & 0xff) * invAlpha; 272 | const g1: number = ((this.pixels[offset] >> 8) & 0xff) * invAlpha; 273 | const b1: number = (this.pixels[offset] & 0xff) * invAlpha; 274 | const color: number = (((r0 + r1) >> 8) << 16) + (((g0 + g1) >> 8) << 8) + ((b0 + b1) >> 8); 275 | this.pixels[offset++] = color; 276 | } 277 | offset += step; 278 | } 279 | } 280 | 281 | static fillCircle(xCenter: number, yCenter: number, yRadius: number, rgb: number, alpha: number): void { 282 | const invAlpha: number = 256 - alpha; 283 | const r0: number = ((rgb >> 16) & 0xff) * alpha; 284 | const g0: number = ((rgb >> 8) & 0xff) * alpha; 285 | const b0: number = (rgb & 0xff) * alpha; 286 | 287 | let yStart: number = yCenter - yRadius; 288 | if (yStart < 0) { 289 | yStart = 0; 290 | } 291 | 292 | let yEnd: number = yCenter + yRadius; 293 | if (yEnd >= this.height2d) { 294 | yEnd = this.height2d - 1; 295 | } 296 | 297 | for (let y: number = yStart; y <= yEnd; y++) { 298 | const midpoint: number = y - yCenter; 299 | const xRadius: number = Math.sqrt(yRadius * yRadius - midpoint * midpoint) | 0; 300 | 301 | let xStart: number = xCenter - xRadius; 302 | if (xStart < 0) { 303 | xStart = 0; 304 | } 305 | 306 | let xEnd: number = xCenter + xRadius; 307 | if (xEnd >= this.width2d) { 308 | xEnd = this.width2d - 1; 309 | } 310 | 311 | let offset: number = xStart + y * this.width2d; 312 | for (let x: number = xStart; x <= xEnd; x++) { 313 | const r1: number = ((this.pixels[offset] >> 16) & 0xff) * invAlpha; 314 | const g1: number = ((this.pixels[offset] >> 8) & 0xff) * invAlpha; 315 | const b1: number = (this.pixels[offset] & 0xff) * invAlpha; 316 | const color: number = (((r0 + r1) >> 8) << 16) + (((g0 + g1) >> 8) << 8) + ((b0 + b1) >> 8); 317 | this.pixels[offset++] = color; 318 | } 319 | } 320 | } 321 | 322 | static setPixel(x: number, y: number, color: number): void { 323 | if (x < this.left || x >= this.right || y < this.top || y >= this.bottom) { 324 | return; 325 | } 326 | 327 | this.pixels[x + y * this.width2d] = color; 328 | } 329 | } 330 | --------------------------------------------------------------------------------