├── .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 |
--------------------------------------------------------------------------------