From edc1f978b8fe5f43ab13136ef70b1862c8bcac92 Mon Sep 17 00:00:00 2001 From: Battlerax Date: Tue, 19 Mar 2024 20:03:59 -0500 Subject: [PATCH 1/4] WebGL Progress WebGL code so far (very very very rough). There are many reasons this is a broken branch off the bat. * UVs are not mapped properly. * pg2 should mostly work. game_gl is wip. * rendering type swapping is not yet supported properly so the original renderer and its dependencies are all not working. Todo here is that the canvas needs to be swapped in some way or created on demand. You cannot share the canvas context. The game_gl module is not really the way forward for swappable contexts, but rather was intended as the start of the full-scene rendering testing. --- .husky/pre-commit | 0 src/html/game_gl.html | 30 + src/html/pg2.html | 30 + src/js/game_gl.ts | 9058 ++++++++++++++++++++++ src/js/jagex2/client/GameShell.ts | 7 + src/js/jagex2/dash3d/World3D.ts | 32 +- src/js/jagex2/graphics/Canvas.ts | 7 +- src/js/jagex2/graphics/Draw3D.ts | 16 + src/js/jagex2/graphics/DrawGL.ts | 663 ++ src/js/jagex2/graphics/GLBuffer.ts | 8 + src/js/jagex2/graphics/GLFloatBuffer.ts | 59 + src/js/jagex2/graphics/GLIntBuffer.ts | 71 + src/js/jagex2/graphics/GLManager.ts | 341 + src/js/jagex2/graphics/GLShader.ts | 90 + src/js/jagex2/graphics/Model.ts | 248 +- src/js/jagex2/graphics/PixMap.ts | 4 +- src/js/jagex2/graphics/RenderMode.ts | 5 + src/js/jagex2/graphics/ShaderTemplate.ts | 59 + src/js/pg2.ts | 370 + src/public/gpu/frag.glsl | 137 + src/public/gpu/fragui.glsl | 15 + src/public/gpu/vert.glsl | 198 + src/public/gpu/vertui.glsl | 11 + webpack.config.js | 6 +- 24 files changed, 11371 insertions(+), 94 deletions(-) mode change 100644 => 100755 .husky/pre-commit create mode 100644 src/html/game_gl.html create mode 100644 src/html/pg2.html create mode 100644 src/js/game_gl.ts create mode 100644 src/js/jagex2/graphics/DrawGL.ts create mode 100644 src/js/jagex2/graphics/GLBuffer.ts create mode 100644 src/js/jagex2/graphics/GLFloatBuffer.ts create mode 100644 src/js/jagex2/graphics/GLIntBuffer.ts create mode 100644 src/js/jagex2/graphics/GLManager.ts create mode 100644 src/js/jagex2/graphics/GLShader.ts create mode 100644 src/js/jagex2/graphics/RenderMode.ts create mode 100644 src/js/jagex2/graphics/ShaderTemplate.ts create mode 100644 src/js/pg2.ts create mode 100644 src/public/gpu/frag.glsl create mode 100644 src/public/gpu/fragui.glsl create mode 100644 src/public/gpu/vert.glsl create mode 100644 src/public/gpu/vertui.glsl diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/src/html/game_gl.html b/src/html/game_gl.html new file mode 100644 index 00000000..72aa6056 --- /dev/null +++ b/src/html/game_gl.html @@ -0,0 +1,30 @@ + + + WebGL + + + + + + + + + + + \ No newline at end of file diff --git a/src/html/pg2.html b/src/html/pg2.html new file mode 100644 index 00000000..d6e0fe5d --- /dev/null +++ b/src/html/pg2.html @@ -0,0 +1,30 @@ + + + RuneScape 2 Playground + + + + + + + + + + + \ No newline at end of file diff --git a/src/js/game_gl.ts b/src/js/game_gl.ts new file mode 100644 index 00000000..41c8db16 --- /dev/null +++ b/src/js/game_gl.ts @@ -0,0 +1,9058 @@ +import SeqType from './jagex2/config/SeqType'; +import LocType from './jagex2/config/LocType'; +import ObjType from './jagex2/config/ObjType'; +import NpcType from './jagex2/config/NpcType'; +import IdkType from './jagex2/config/IdkType'; +import SpotAnimType from './jagex2/config/SpotAnimType'; +import VarpType from './jagex2/config/VarpType'; +import ComType from './jagex2/config/ComType'; + +import PixMap from './jagex2/graphics/PixMap'; +import Draw2D from './jagex2/graphics/Draw2D'; +import Draw3D from './jagex2/graphics/Draw3D'; +import Pix8 from './jagex2/graphics/Pix8'; +import Pix24 from './jagex2/graphics/Pix24'; +import PixFont from './jagex2/graphics/PixFont'; +import Model from './jagex2/graphics/Model'; +import Colors from './jagex2/graphics/Colors'; + +import Jagfile from './jagex2/io/Jagfile'; +import Packet from './jagex2/io/Packet'; +import ClientStream from './jagex2/io/ClientStream'; +import Protocol from './jagex2/io/Protocol'; +import Isaac from './jagex2/io/Isaac'; +import Database from './jagex2/io/Database'; +import ServerProt from './jagex2/io/ServerProt'; +import ClientProt from './jagex2/io/ClientProt'; + +import WordFilter from './jagex2/wordenc/WordFilter'; +import WordPack from './jagex2/wordenc/WordPack'; + +import Wave from './jagex2/sound/Wave'; + +import './vendor/midi.js'; +import Bzip from './vendor/bzip'; + +import LinkList from './jagex2/datastruct/LinkList'; +import JString from './jagex2/datastruct/JString'; +import InputTracking from './jagex2/client/InputTracking'; + +import World3D from './jagex2/dash3d/World3D'; +import World from './jagex2/dash3d/World'; +import LocLayer from './jagex2/dash3d/LocLayer'; +import LocShape, {LocShapes} from './jagex2/dash3d/LocShape'; +import LocAngle from './jagex2/dash3d/LocAngle'; +import LocTemporary from './jagex2/dash3d/type/LocTemporary'; +import LocSpawned from './jagex2/dash3d/type/LocSpawned'; +import CollisionMap from './jagex2/dash3d/CollisionMap'; +import CollisionFlag from './jagex2/dash3d/CollisionFlag'; +import PlayerEntity from './jagex2/dash3d/entity/PlayerEntity'; +import NpcEntity from './jagex2/dash3d/entity/NpcEntity'; +import ObjStackEntity from './jagex2/dash3d/entity/ObjStackEntity'; +import LocEntity from './jagex2/dash3d/entity/LocEntity'; +import PathingEntity from './jagex2/dash3d/entity/PathingEntity'; +import ProjectileEntity from './jagex2/dash3d/entity/ProjectileEntity'; +import SpotAnimEntity from './jagex2/dash3d/entity/SpotAnimEntity'; + +import {playMidi, playWave, setMidiVolume, setWaveVolume, stopMidi} from './jagex2/util/AudioUtil.js'; +import {arraycopy, downloadUrl, sleep} from './jagex2/util/JsUtil'; +import {Int32Array3d, TypedArray1d, Uint8Array3d} from './jagex2/util/Arrays'; +import {Client} from './client'; +import SeqBase from './jagex2/graphics/SeqBase'; +import SeqFrame from './jagex2/graphics/SeqFrame'; +import FloType from './jagex2/config/FloType'; +import {setupConfiguration} from './configuration'; +import DrawGL from './jagex2/graphics/DrawGL'; + +// noinspection JSSuspiciousNameCombination +class Game extends Client { + load = async (): Promise => { + if (this.alreadyStarted) { + this.errorStarted = true; + return; + } + + // Enable webgl + DrawGL.GL_ENABLED = true;// + let v0 = 0; + + this.alreadyStarted = true; + + try { + await this.showProgress(10, 'Connecting to fileserver'); + + await Bzip.load(await (await fetch('bz2.wasm')).arrayBuffer()); + this.db = new Database(await Database.openDatabase()); + + const checksums: Packet = new Packet(new Uint8Array(await downloadUrl(`${Client.httpAddress}/crc`))); + for (let i: number = 0; i < 9; i++) { + this.archiveChecksums[i] = checksums.g4; + } + + if (!Client.lowMemory) { + await this.setMidi('scape_main', 12345678, 40000); + } + + const title: Jagfile = await this.loadArchive('title', 'title screen', this.archiveChecksums[1], 10); + this.titleArchive = title; + + this.fontPlain11 = PixFont.fromArchive(title, 'p11'); + this.fontPlain12 = PixFont.fromArchive(title, 'p12'); + this.fontBold12 = PixFont.fromArchive(title, 'b12'); + this.fontQuill8 = PixFont.fromArchive(title, 'q8'); + + await this.loadTitleBackground(); + this.loadTitleImages(); + + const config: Jagfile = await this.loadArchive('config', 'config', this.archiveChecksums[2], 15); + const interfaces: Jagfile = await this.loadArchive('interface', 'interface', this.archiveChecksums[3], 20); + const media: Jagfile = await this.loadArchive('media', '2d graphics', this.archiveChecksums[4], 30); + const models: Jagfile = await this.loadArchive('models', '3d graphics', this.archiveChecksums[5], 40); + const textures: Jagfile = await this.loadArchive('textures', 'textures', this.archiveChecksums[6], 60); + const wordenc: Jagfile = await this.loadArchive('wordenc', 'chat system', this.archiveChecksums[7], 65); + const sounds: Jagfile = await this.loadArchive('sounds', 'sound effects', this.archiveChecksums[8], 70); + + this.levelTileFlags = new Uint8Array3d(CollisionMap.LEVELS, CollisionMap.SIZE, CollisionMap.SIZE); + this.levelHeightmap = new Int32Array3d(CollisionMap.LEVELS, CollisionMap.SIZE + 1, CollisionMap.SIZE + 1); + if (this.levelHeightmap) { + this.scene = new World3D(this.levelHeightmap, CollisionMap.SIZE, CollisionMap.LEVELS, CollisionMap.SIZE); + } + for (let level: number = 0; level < CollisionMap.LEVELS; level++) { + this.levelCollisionMap[level] = new CollisionMap(); + } + this.imageMinimap = new Pix24(512, 512); + await this.showProgress(75, 'Unpacking media'); + this.imageInvback = Pix8.fromArchive(media, 'invback', 0); + this.imageChatback = Pix8.fromArchive(media, 'chatback', 0); + this.imageMapback = Pix8.fromArchive(media, 'mapback', 0); + this.imageBackbase1 = Pix8.fromArchive(media, 'backbase1', 0); + this.imageBackbase2 = Pix8.fromArchive(media, 'backbase2', 0); + this.imageBackhmid1 = Pix8.fromArchive(media, 'backhmid1', 0); + for (let i: number = 0; i < 13; i++) { + this.imageSideicons[i] = Pix8.fromArchive(media, 'sideicons', i); + } + this.imageCompass = Pix24.fromArchive(media, 'compass', 0); + + try { + for (let i: number = 0; i < 50; i++) { + this.imageMapscene[i] = Pix8.fromArchive(media, 'mapscene', i); + } + } catch (e) { + /* empty */ + } + + try { + for (let i: number = 0; i < 50; i++) { + this.imageMapfunction[i] = Pix24.fromArchive(media, 'mapfunction', i); + } + } catch (e) { + /* empty */ + } + + try { + for (let i: number = 0; i < 20; i++) { + this.imageHitmarks[i] = Pix24.fromArchive(media, 'hitmarks', i); + } + } catch (e) { + /* empty */ + } + + try { + for (let i: number = 0; i < 20; i++) { + this.imageHeadicons[i] = Pix24.fromArchive(media, 'headicons', i); + } + } catch (e) { + /* empty */ + } + + this.imageMapflag = Pix24.fromArchive(media, 'mapflag', 0); + for (let i: number = 0; i < 8; i++) { + this.imageCrosses[i] = Pix24.fromArchive(media, 'cross', i); + } + this.imageMapdot0 = Pix24.fromArchive(media, 'mapdots', 0); + this.imageMapdot1 = Pix24.fromArchive(media, 'mapdots', 1); + this.imageMapdot2 = Pix24.fromArchive(media, 'mapdots', 2); + this.imageMapdot3 = Pix24.fromArchive(media, 'mapdots', 3); + this.imageScrollbar0 = Pix8.fromArchive(media, 'scrollbar', 0); + this.imageScrollbar1 = Pix8.fromArchive(media, 'scrollbar', 1); + this.imageRedstone1 = Pix8.fromArchive(media, 'redstone1', 0); + this.imageRedstone2 = Pix8.fromArchive(media, 'redstone2', 0); + this.imageRedstone3 = Pix8.fromArchive(media, 'redstone3', 0); + this.imageRedstone1h = Pix8.fromArchive(media, 'redstone1', 0); + this.imageRedstone1h?.flipHorizontally(); + this.imageRedstone2h = Pix8.fromArchive(media, 'redstone2', 0); + this.imageRedstone2h?.flipHorizontally(); + this.imageRedstone1v = Pix8.fromArchive(media, 'redstone1', 0); + this.imageRedstone1v?.flipVertically(); + this.imageRedstone2v = Pix8.fromArchive(media, 'redstone2', 0); + this.imageRedstone2v?.flipVertically(); + this.imageRedstone3v = Pix8.fromArchive(media, 'redstone3', 0); + this.imageRedstone3v?.flipVertically(); + this.imageRedstone1hv = Pix8.fromArchive(media, 'redstone1', 0); + this.imageRedstone1hv?.flipHorizontally(); + this.imageRedstone1hv?.flipVertically(); + this.imageRedstone2hv = Pix8.fromArchive(media, 'redstone2', 0); + this.imageRedstone2hv?.flipHorizontally(); + this.imageRedstone2hv?.flipVertically(); + const backleft1: Pix24 = Pix24.fromArchive(media, 'backleft1', 0); + this.areaBackleft1 = new PixMap(backleft1.width, backleft1.height); + backleft1.blitOpaque(0, 0); + const backleft2: Pix24 = Pix24.fromArchive(media, 'backleft2', 0); + this.areaBackleft2 = new PixMap(backleft2.width, backleft2.height); + backleft2.blitOpaque(0, 0); + const backright1: Pix24 = Pix24.fromArchive(media, 'backright1', 0); + this.areaBackright1 = new PixMap(backright1.width, backright1.height); + backright1.blitOpaque(0, 0); + const backright2: Pix24 = Pix24.fromArchive(media, 'backright2', 0); + this.areaBackright2 = new PixMap(backright2.width, backright2.height); + backright2.blitOpaque(0, 0); + const backtop1: Pix24 = Pix24.fromArchive(media, 'backtop1', 0); + this.areaBacktop1 = new PixMap(backtop1.width, backtop1.height); + backtop1.blitOpaque(0, 0); + const backtop2: Pix24 = Pix24.fromArchive(media, 'backtop2', 0); + this.areaBacktop2 = new PixMap(backtop2.width, backtop2.height); + backtop2.blitOpaque(0, 0); + const backvmid1: Pix24 = Pix24.fromArchive(media, 'backvmid1', 0); + this.areaBackvmid1 = new PixMap(backvmid1.width, backvmid1.height); + backvmid1.blitOpaque(0, 0); + const backvmid2: Pix24 = Pix24.fromArchive(media, 'backvmid2', 0); + this.areaBackvmid2 = new PixMap(backvmid2.width, backvmid2.height); + backvmid2.blitOpaque(0, 0); + const backvmid3: Pix24 = Pix24.fromArchive(media, 'backvmid3', 0); + this.areaBackvmid3 = new PixMap(backvmid3.width, backvmid3.height); + backvmid3.blitOpaque(0, 0); + const backhmid2: Pix24 = Pix24.fromArchive(media, 'backhmid2', 0); + this.areaBackhmid2 = new PixMap(backhmid2.width, backhmid2.height); + backhmid2.blitOpaque(0, 0); + + const randR: number = ((Math.random() * 21.0) | 0) - 10; + const randG: number = ((Math.random() * 21.0) | 0) - 10; + const randB: number = ((Math.random() * 21.0) | 0) - 10; + const rand: number = ((Math.random() * 41.0) | 0) - 20; + for (let i: number = 0; i < 50; i++) { + if (this.imageMapfunction[i]) { + this.imageMapfunction[i]?.translate(randR + rand, randG + rand, randB + rand); + } + + if (this.imageMapscene[i]) { + this.imageMapscene[i]?.translate(randR + rand, randG + rand, randB + rand); + } + } + + await this.showProgress(80, 'Unpacking textures'); + Draw3D.unpackTextures(textures); + Draw3D.setBrightness(0.8); + Draw3D.initPool(20); + + await this.showProgress(83, 'Unpacking models'); + Model.unpack(models); + SeqBase.unpack(models); + SeqFrame.unpack(models); + + await this.showProgress(86, 'Unpacking config'); + SeqType.unpack(config); + LocType.unpack(config); + FloType.unpack(config); + ObjType.unpack(config, Client.members); + NpcType.unpack(config); + IdkType.unpack(config); + SpotAnimType.unpack(config); + VarpType.unpack(config); + + if (!Client.lowMemory) { + await this.showProgress(90, 'Unpacking sounds'); + Wave.unpack(sounds); + } + + await this.showProgress(92, 'Unpacking interfaces'); + ComType.unpack(interfaces, media, [this.fontPlain11, this.fontPlain12, this.fontBold12, this.fontQuill8]); + + await this.showProgress(97, 'Preparing game engine'); + for (let y: number = 0; y < 33; y++) { + let left: number = 999; + let right: number = 0; + for (let x: number = 0; x < 35; x++) { + if (this.imageMapback.pixels[x + y * this.imageMapback.width] === 0) { + if (left === 999) { + left = x; + } + } else if (left !== 999) { + right = x; + break; + } + } + this.compassMaskLineOffsets[y] = left; + this.compassMaskLineLengths[y] = right - left; + } + + for (let y: number = 9; y < 160; y++) { + let left: number = 999; + let right: number = 0; + for (let x: number = 10; x < 168; x++) { + if (this.imageMapback.pixels[x + y * this.imageMapback.width] === 0 && (x > 34 || y > 34)) { + if (left === 999) { + left = x; + } + } else if (left !== 999) { + right = x; + break; + } + } + this.minimapMaskLineOffsets[y - 9] = left - 21; + this.minimapMaskLineLengths[y - 9] = right - left; + } + + Draw3D.init3D(479, 96); + this.areaChatbackOffsets = Draw3D.lineOffset; + Draw3D.init3D(190, 261); + this.areaSidebarOffsets = Draw3D.lineOffset; + Draw3D.init3D(512, 334); + this.areaViewportOffsets = Draw3D.lineOffset; + + const distance: Int32Array = new Int32Array(9); + for (let x: number = 0; x < 9; x++) { + const angle: number = x * 32 + 128 + 15; + const offset: number = angle * 3 + 600; + const sin: number = Draw3D.sin[angle]; + distance[x] = (offset * sin) >> 16; + } + + World3D.init(512, 334, 500, 800, distance); + WordFilter.unpack(wordenc); + this.initializeLevelExperience(); + } catch (err) { + console.error(err); + this.errorLoading = true; + } + }; + + update = async (): Promise => { + if (this.errorStarted || this.errorLoading || this.errorHost) { + return; + } + this.loopCycle++; + if (this.ingame) { + await this.updateGame(); + } else { + await this.updateTitleScreen(); + } + }; + + draw = async (): Promise => { + if (this.errorStarted || this.errorLoading || this.errorHost) { + this.drawError(); + return; + } + DrawGL.draw(); + if (this.ingame) { + this.drawGame(); + } else { + await this.drawTitleScreen(); + } + this.dragCycles = 0; + }; + + refresh = (): void => { + this.redrawTitleBackground = true; + }; + + showProgress = async (progress: number, str: string): Promise => { + console.log(`${progress}%: ${str}`); + + await this.loadTitle(); + if (!this.titleArchive) { + await super.showProgress(progress, str); + return; + } + + this.imageTitle4?.bind(); + + const x: number = 360; + const y: number = 200; +`` + const offsetY: number = 20; + this.fontBold12?.drawStringCenter((x / 2) | 0, ((y / 2) | 0) - offsetY - 26, 'RuneScape is loading - please wait...', Colors.WHITE); + const midY: number = ((y / 2) | 0) - 18 - offsetY; + + Draw2D.drawRect(((x / 2) | 0) - 152, midY, 304, 34, Colors.PROGRESS_RED); + Draw2D.drawRect(((x / 2) | 0) - 151, midY + 1, 302, 32, Colors.BLACK); + Draw2D.fillRect(((x / 2) | 0) - 150, midY + 2, progress * 3, 30, Colors.PROGRESS_RED); + Draw2D.fillRect(((x / 2) | 0) - 150 + progress * 3, midY + 2, 300 - progress * 3, 30, Colors.BLACK); + + this.fontBold12?.drawStringCenter((x / 2) | 0, ((y / 2) | 0) + 5 - offsetY, str, Colors.WHITE); + this.imageTitle4?.draw(214, 186); + + if (this.redrawTitleBackground) { + this.redrawTitleBackground = false; + if (!this.flameActive) { + this.imageTitle0?.draw(0, 0); + this.imageTitle1?.draw(661, 0); + } + this.imageTitle2?.draw(128, 0); + this.imageTitle3?.draw(214, 386); + this.imageTitle5?.draw(0, 265); + this.imageTitle6?.draw(574, 265); + this.imageTitle7?.draw(128, 186); + this.imageTitle8?.draw(574, 186); + } + + await sleep(5); // return a slice of time to the main loop so it can update the progress bar + }; + + runFlames = (): void => { + if (!this.flameActive) { + return; + } + this.updateFlames(); + this.updateFlames(); + this.drawFlames(); + }; + + private loadTitle = async (): Promise => { + if (!this.imageTitle2) { + this.drawArea = null; + this.areaChatback = null; + this.areaMapback = null; + this.areaSidebar = null; + this.areaViewport = null; + this.areaBackbase1 = null; + this.areaBackbase2 = null; + this.areaBackhmid1 = null; + + this.imageTitle0 = new PixMap(128, 265); + Draw2D.clear(); + + this.imageTitle1 = new PixMap(128, 265); + Draw2D.clear(); + + this.imageTitle2 = new PixMap(533, 186); + Draw2D.clear(); + + this.imageTitle3 = new PixMap(360, 146); + Draw2D.clear(); + + this.imageTitle4 = new PixMap(360, 200); + Draw2D.clear(); + + this.imageTitle5 = new PixMap(214, 267); + Draw2D.clear(); + + this.imageTitle6 = new PixMap(215, 267); + Draw2D.clear(); + + this.imageTitle7 = new PixMap(86, 79); + Draw2D.clear(); + + this.imageTitle8 = new PixMap(87, 79); + Draw2D.clear(); + + if (this.titleArchive) { + await this.loadTitleBackground(); + this.loadTitleImages(); + } + this.redrawTitleBackground = true; + } + }; + + private loadTitleBackground = async (): Promise => { + if (!this.titleArchive) { + return; + } + const background: Pix24 = await Pix24.fromJpeg(this.titleArchive, 'title'); + + this.imageTitle0?.bind(); + background.blitOpaque(0, 0); + + this.imageTitle1?.bind(); + background.blitOpaque(-661, 0); + + this.imageTitle2?.bind(); + background.blitOpaque(-128, 0); + + this.imageTitle3?.bind(); + background.blitOpaque(-214, -386); + + this.imageTitle4?.bind(); + background.blitOpaque(-214, -186); + + this.imageTitle5?.bind(); + background.blitOpaque(0, -265); + + this.imageTitle6?.bind(); + background.blitOpaque(-128, -186); + + this.imageTitle7?.bind(); + background.blitOpaque(-128, -186); + + this.imageTitle8?.bind(); + background.blitOpaque(-574, -186); + + // draw right side (mirror image) + background.flipHorizontally(); + + this.imageTitle0?.bind(); + background.blitOpaque(394, 0); + + this.imageTitle1?.bind(); + background.blitOpaque(-267, 0); + + this.imageTitle2?.bind(); + background.blitOpaque(266, 0); + + this.imageTitle3?.bind(); + background.blitOpaque(180, -386); + + this.imageTitle4?.bind(); + background.blitOpaque(180, -186); + + this.imageTitle5?.bind(); + background.blitOpaque(394, -265); + + this.imageTitle6?.bind(); + background.blitOpaque(-180, -265); + + this.imageTitle7?.bind(); + background.blitOpaque(212, -186); + + this.imageTitle8?.bind(); + background.blitOpaque(-180, -186); + + const logo: Pix24 = Pix24.fromArchive(this.titleArchive, 'logo'); + this.imageTitle2?.bind(); + logo.draw(((this.width / 2) | 0) - ((logo.width / 2) | 0) - 128, 18); + }; + + private updateFlameBuffer = (image: Pix8 | null): void => { + if (!this.flameBuffer0 || !this.flameBuffer1) { + return; + } + + const flameHeight: number = 256; + + // Clears the initial flame buffer + this.flameBuffer0.fill(0); + + // Blends the fire at random + for (let i: number = 0; i < 5000; i++) { + const rand: number = (Math.random() * 128.0 * flameHeight) | 0; + this.flameBuffer0[rand] = (Math.random() * 256.0) | 0; + } + + // changes color between last few flames + for (let i: number = 0; i < 20; i++) { + for (let y: number = 1; y < flameHeight - 1; y++) { + for (let x: number = 1; x < 127; x++) { + const index: number = x + (y << 7); + this.flameBuffer1[index] = ((this.flameBuffer0[index - 1] + this.flameBuffer0[index + 1] + this.flameBuffer0[index - 128] + this.flameBuffer0[index + 128]) / 4) | 0; + } + } + + const last: Int32Array = this.flameBuffer0; + this.flameBuffer0 = this.flameBuffer1; + this.flameBuffer1 = last; + } + + // Renders the rune images + if (image) { + let off: number = 0; + + for (let y: number = 0; y < image.height; y++) { + for (let x: number = 0; x < image.width; x++) { + if (image.pixels[off++] !== 0) { + const x0: number = x + image.cropX + 16; + const y0: number = y + image.cropY + 16; + const index: number = x0 + (y0 << 7); + this.flameBuffer0[index] = 0; + } + } + } + } + }; + + private loadTitleImages = (): void => { + if (!this.titleArchive) { + return; + } + this.imageTitlebox = Pix8.fromArchive(this.titleArchive, 'titlebox'); + this.imageTitlebutton = Pix8.fromArchive(this.titleArchive, 'titlebutton'); + for (let i: number = 0; i < 12; i++) { + this.imageRunes[i] = Pix8.fromArchive(this.titleArchive, 'runes', i); + } + this.imageFlamesLeft = new Pix24(128, 265); + this.imageFlamesRight = new Pix24(128, 265); + + if (this.imageTitle0) arraycopy(this.imageTitle0.pixels, 0, this.imageFlamesLeft.pixels, 0, 33920); + if (this.imageTitle1) arraycopy(this.imageTitle1.pixels, 0, this.imageFlamesRight.pixels, 0, 33920); + + this.flameGradient0 = new Int32Array(256); + for (let index: number = 0; index < 64; index++) { + this.flameGradient0[index] = index * 262144; + } + for (let index: number = 0; index < 64; index++) { + this.flameGradient0[index + 64] = index * 1024 + Colors.RED; + } + for (let index: number = 0; index < 64; index++) { + this.flameGradient0[index + 128] = index * 4 + Colors.YELLOW; + } + for (let index: number = 0; index < 64; index++) { + this.flameGradient0[index + 192] = Colors.WHITE; + } + this.flameGradient1 = new Int32Array(256); + for (let index: number = 0; index < 64; index++) { + this.flameGradient1[index] = index * 1024; + } + for (let index: number = 0; index < 64; index++) { + this.flameGradient1[index + 64] = index * 4 + Colors.GREEN; + } + for (let index: number = 0; index < 64; index++) { + this.flameGradient1[index + 128] = index * 262144 + Colors.CYAN; + } + for (let index: number = 0; index < 64; index++) { + this.flameGradient1[index + 192] = Colors.WHITE; + } + this.flameGradient2 = new Int32Array(256); + for (let index: number = 0; index < 64; index++) { + this.flameGradient2[index] = index * 4; + } + for (let index: number = 0; index < 64; index++) { + this.flameGradient2[index + 64] = index * 262144 + Colors.BLUE; + } + for (let index: number = 0; index < 64; index++) { + this.flameGradient2[index + 128] = index * 1024 + Colors.MAGENTA; + } + for (let index: number = 0; index < 64; index++) { + this.flameGradient2[index + 192] = Colors.WHITE; + } + + this.flameGradient = new Int32Array(256); + this.flameBuffer0 = new Int32Array(32768); + this.flameBuffer1 = new Int32Array(32768); + this.updateFlameBuffer(null); + this.flameBuffer3 = new Int32Array(32768); + this.flameBuffer2 = new Int32Array(32768); + + this.showProgress(10, 'Connecting to fileserver').then((): void => { + if (!this.flameActive) { + this.flameActive = true; + this.flamesInterval = setInterval(this.runFlames, 35); + } + }); + }; + + private updateTitleScreen = async (): Promise => { + if (this.titleScreenState === 0) { + let x: number = ((this.width / 2) | 0) - 80; + let y: number = ((this.height / 2) | 0) + 20; + + y += 20; + if (this.mouseClickButton === 1 && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) { + this.titleScreenState = 3; + this.titleLoginField = 0; + } + + x = ((this.width / 2) | 0) + 80; + if (this.mouseClickButton === 1 && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) { + this.loginMessage0 = ''; + this.loginMessage1 = 'Enter your username & password.'; + this.titleScreenState = 2; + this.titleLoginField = 0; + } + } else if (this.titleScreenState === 2) { + let y: number = ((this.height / 2) | 0) - 40; + y += 30; + y += 25; + + if (this.mouseClickButton === 1 && this.mouseClickY >= y - 15 && this.mouseClickY < y) { + this.titleLoginField = 0; + } + y += 15; + + if (this.mouseClickButton === 1 && this.mouseClickY >= y - 15 && this.mouseClickY < y) { + this.titleLoginField = 1; + } + // y += 15; dead code + + let buttonX: number = ((this.width / 2) | 0) - 80; + let buttonY: number = ((this.height / 2) | 0) + 50; + buttonY += 20; + + if (this.mouseClickButton === 1 && this.mouseClickX >= buttonX - 75 && this.mouseClickX <= buttonX + 75 && this.mouseClickY >= buttonY - 20 && this.mouseClickY <= buttonY + 20) { + await this.login(this.username, this.password, false); + } + + buttonX = ((this.width / 2) | 0) + 80; + if (this.mouseClickButton === 1 && this.mouseClickX >= buttonX - 75 && this.mouseClickX <= buttonX + 75 && this.mouseClickY >= buttonY - 20 && this.mouseClickY <= buttonY + 20) { + this.titleScreenState = 0; + this.username = ''; + this.password = ''; + } + + // eslint-disable-next-line no-constant-condition + while (true) { + const key: number = this.pollKey(); + if (key === -1) { + return; + } + + let valid: boolean = false; + for (let i: number = 0; i < PixFont.CHARSET.length; i++) { + if (String.fromCharCode(key) === PixFont.CHARSET.charAt(i)) { + valid = true; + break; + } + } + + if (this.titleLoginField === 0) { + if (key === 8 && this.username.length > 0) { + this.username = this.username.substring(0, this.username.length - 1); + } + + if (key === 9 || key === 10 || key === 13) { + this.titleLoginField = 1; + } + + if (valid) { + this.username = this.username + String.fromCharCode(key); + } + + if (this.username.length > 12) { + this.username = this.username.substring(0, 12); + } + } else if (this.titleLoginField === 1) { + if (key === 8 && this.password.length > 0) { + this.password = this.password.substring(0, this.password.length - 1); + } + + if (key === 9 || key === 10 || key === 13) { + this.titleLoginField = 0; + } + + if (valid) { + this.password = this.password + String.fromCharCode(key); + } + + if (this.password.length > 20) { + this.password = this.password.substring(0, 20); + } + } + } + } else if (this.titleScreenState === 3) { + const x: number = (this.width / 2) | 0; + let y: number = ((this.height / 2) | 0) + 50; + y += 20; + + if (this.mouseClickButton === 1 && this.mouseClickX >= x - 75 && this.mouseClickX <= x + 75 && this.mouseClickY >= y - 20 && this.mouseClickY <= y + 20) { + this.titleScreenState = 0; + } + } + }; + + private drawTitleScreen = async (): Promise => { + await this.loadTitle(); + this.imageTitle4?.bind(); + this.imageTitlebox?.draw(0, 0); + + const w: number = 360; + const h: number = 200; + + if (this.titleScreenState === 0) { + let x: number = (w / 2) | 0; + let y: number = ((h / 2) | 0) - 20; + this.fontBold12?.drawStringTaggableCenter(x, y, 'Welcome to RuneScape', Colors.YELLOW, true); + + x = ((w / 2) | 0) - 80; + y = ((h / 2) | 0) + 20; + this.imageTitlebutton?.draw(x - 73, y - 20); + this.fontBold12?.drawStringTaggableCenter(x, y + 5, 'New user', Colors.WHITE, true); + + x = ((w / 2) | 0) + 80; + this.imageTitlebutton?.draw(x - 73, y - 20); + this.fontBold12?.drawStringTaggableCenter(x, y + 5, 'Existing User', Colors.WHITE, true); + } else if (this.titleScreenState === 2) { + let x: number = ((w / 2) | 0) - 80; + let y: number = ((h / 2) | 0) - 40; + if (this.loginMessage0.length > 0) { + this.fontBold12?.drawStringTaggableCenter(w / 2, y - 15, this.loginMessage0, Colors.YELLOW, true); + this.fontBold12?.drawStringTaggableCenter(w / 2, y, this.loginMessage1, Colors.YELLOW, true); + y += 30; + } else { + this.fontBold12?.drawStringTaggableCenter(w / 2, y - 7, this.loginMessage1, Colors.YELLOW, true); + y += 30; + } + + this.fontBold12?.drawStringTaggable(w / 2 - 90, y, `Username: ${this.username}${this.titleLoginField === 0 && this.loopCycle % 40 < 20 ? '@yel@|' : ''}`, Colors.WHITE, true); + y += 15; + + this.fontBold12?.drawStringTaggable(w / 2 - 88, y, `Password: ${JString.toAsterisks(this.password)}${this.titleLoginField === 1 && this.loopCycle % 40 < 20 ? '@yel@|' : ''}`, Colors.WHITE, true); + + // x = w / 2 - 80; dead code + y = ((h / 2) | 0) + 50; + this.imageTitlebutton?.draw(x - 73, y - 20); + this.fontBold12?.drawStringTaggableCenter(x, y + 5, 'Login', Colors.WHITE, true); + + x = ((w / 2) | 0) + 80; + this.imageTitlebutton?.draw(x - 73, y - 20); + this.fontBold12?.drawStringTaggableCenter(x, y + 5, 'Cancel', Colors.WHITE, true); + } else if (this.titleScreenState === 3) { + this.fontBold12?.drawStringTaggableCenter(w / 2, h / 2 - 60, 'Create a free account', Colors.YELLOW, true); + + const x: number = (w / 2) | 0; + let y: number = ((h / 2) | 0) - 35; + + this.fontBold12?.drawStringTaggableCenter((w / 2) | 0, y, 'To create a new account you need to', Colors.WHITE, true); + y += 15; + + this.fontBold12?.drawStringTaggableCenter((w / 2) | 0, y, 'go back to the main RuneScape webpage', Colors.WHITE, true); + y += 15; + + this.fontBold12?.drawStringTaggableCenter((w / 2) | 0, y, "and choose the red 'create account'", Colors.WHITE, true); + y += 15; + + this.fontBold12?.drawStringTaggableCenter((w / 2) | 0, y, 'button at the top right of that page.', Colors.WHITE, true); + // y += 15; dead code + + y = ((h / 2) | 0) + 50; + this.imageTitlebutton?.draw(x - 73, y - 20); + this.fontBold12?.drawStringTaggableCenter(x, y + 5, 'Cancel', Colors.WHITE, true); + } + + this.imageTitle4?.draw(214, 186); + if (this.redrawTitleBackground) { + this.redrawTitleBackground = false; + this.imageTitle2?.draw(128, 0); + this.imageTitle3?.draw(214, 386); + this.imageTitle5?.draw(0, 265); + this.imageTitle6?.draw(574, 265); + this.imageTitle7?.draw(128, 186); + this.imageTitle8?.draw(574, 186); + } + }; + + private login = async (username: string, password: string, reconnect: boolean): Promise => { + try { + if (!reconnect) { + this.loginMessage0 = ''; + this.loginMessage1 = 'Connecting to server...'; + await this.drawTitleScreen(); + } + this.stream = new ClientStream(await ClientStream.openSocket({host: Client.serverAddress, port: 43594 + Client.portOffset})); + await this.stream?.readBytes(this.in.data, 0, 8); + this.in.pos = 0; + this.serverSeed = this.in.g8; + const seed: Int32Array = new Int32Array([Math.floor(Math.random() * 99999999), Math.floor(Math.random() * 99999999), Number(this.serverSeed >> 32n), Number(this.serverSeed & BigInt(0xffffffff))]); + this.out.pos = 0; + this.out.p1(10); + this.out.p4(seed[0]); + this.out.p4(seed[1]); + this.out.p4(seed[2]); + this.out.p4(seed[3]); + this.out.p4(0); // TODO signlink UUID + this.out.pjstr(username); + this.out.pjstr(password); + this.out.rsaenc(Client.modulus, Client.exponent); + this.loginout.pos = 0; + if (reconnect) { + this.loginout.p1(18); + } else { + this.loginout.p1(16); + } + this.loginout.p1(this.out.pos + 36 + 1 + 1); + this.loginout.p1(Client.clientversion); + this.loginout.p1(Client.lowMemory ? 1 : 0); + for (let i: number = 0; i < 9; i++) { + this.loginout.p4(this.archiveChecksums[i]); + } + this.loginout.pdata(this.out.data, this.out.pos, 0); + this.out.random = new Isaac(seed); + for (let i: number = 0; i < 4; i++) { + seed[i] += 50; + } + this.randomIn = new Isaac(seed); + this.stream?.write(this.loginout.data, this.loginout.pos, 0); + const reply: number = await this.stream.read(); + + if (reply === 1) { + await sleep(2000); + await this.login(username, password, reconnect); + return; + } + if (reply === 2 || reply === 18) { + this.rights = reply === 18; + InputTracking.setDisabled(); + this.ingame = true; + this.out.pos = 0; + this.in.pos = 0; + this.packetType = -1; + this.lastPacketType0 = -1; + this.lastPacketType1 = -1; + this.lastPacketType2 = -1; + this.packetSize = 0; + this.idleNetCycles = 0; + this.systemUpdateTimer = 0; + this.idleTimeout = 0; + this.hintType = 0; + this.menuSize = 0; + this.menuVisible = false; + this.idleCycles = 0; + for (let i: number = 0; i < 100; i++) { + this.messageText[i] = null; + } + this.objSelected = 0; + this.spellSelected = 0; + this.sceneState = 0; + this.waveCount = 0; + this.cameraAnticheatOffsetX = ((Math.random() * 100.0) | 0) - 50; + this.cameraAnticheatOffsetZ = ((Math.random() * 110.0) | 0) - 55; + this.cameraAnticheatAngle = ((Math.random() * 80.0) | 0) - 40; + this.minimapAnticheatAngle = ((Math.random() * 120.0) | 0) - 60; + this.minimapZoom = ((Math.random() * 30.0) | 0) - 20; + this.orbitCameraYaw = (((Math.random() * 20.0) | 0) - 10) & 0x7ff; + this.minimapLevel = -1; + this.flagSceneTileX = 0; + this.flagSceneTileZ = 0; + this.playerCount = 0; + this.npcCount = 0; + for (let i: number = 0; i < this.MAX_PLAYER_COUNT; i++) { + this.players[i] = null; + this.playerAppearanceBuffer[i] = null; + } + for (let i: number = 0; i < 8192; i++) { + this.npcs[i] = null; + } + this.localPlayer = this.players[this.LOCAL_PLAYER_INDEX] = new PlayerEntity(); + this.projectiles.clear(); + this.spotanims.clear(); + this.temporaryLocs.clear(); + for (let level: number = 0; level < CollisionMap.LEVELS; level++) { + for (let x: number = 0; x < CollisionMap.SIZE; x++) { + for (let z: number = 0; z < CollisionMap.SIZE; z++) { + this.levelObjStacks[level][x][z] = null; + } + } + } + this.spawnedLocations = new LinkList(); + this.friendCount = 0; + this.stickyChatInterfaceId = -1; + this.chatInterfaceId = -1; + this.viewportInterfaceId = -1; + this.sidebarInterfaceId = -1; + this.pressedContinueOption = false; + this.selectedTab = 3; + this.chatbackInputOpen = false; + this.menuVisible = false; + this.showSocialInput = false; + this.modalMessage = null; + this.inMultizone = 0; + this.flashingTab = -1; + this.designGenderMale = true; + this.validateCharacterDesign(); + for (let i: number = 0; i < 5; i++) { + this.designColors[i] = 0; + } + Client.opLoc4Counter = 0; + Client.opNpc3Counter = 0; + Client.opHeld4Counter = 0; + Client.opNpc5Counter = 0; + Client.opHeld1Counter = 0; + Client.opLoc5Counter = 0; + Client.ifButton5Counter = 0; + Client.opPlayer2Counter = 0; + Client.opHeld9Counter = 0; + this.prepareGameScreen(); + return; + } + if (reply === 3) { + this.loginMessage0 = ''; + this.loginMessage1 = 'Invalid username or password.'; + return; + } + if (reply === 4) { + this.loginMessage0 = 'Your account has been disabled.'; + this.loginMessage1 = 'Please check your message-centre for details.'; + return; + } + if (reply === 5) { + this.loginMessage0 = 'Your account is already logged in.'; + this.loginMessage1 = 'Try again in 60 secs...'; + return; + } + if (reply === 6) { + this.loginMessage0 = 'RuneScape has been updated!'; + this.loginMessage1 = 'Please reload this page.'; + return; + } + if (reply === 7) { + this.loginMessage0 = 'This world is full.'; + this.loginMessage1 = 'Please use a different world.'; + return; + } + if (reply === 8) { + this.loginMessage0 = 'Unable to connect.'; + this.loginMessage1 = 'Login server offline.'; + return; + } + if (reply === 9) { + this.loginMessage0 = 'Login limit exceeded.'; + this.loginMessage1 = 'Too many connections from your address.'; + return; + } + if (reply === 10) { + this.loginMessage0 = 'Unable to connect.'; + this.loginMessage1 = 'Bad session id.'; + return; + } + if (reply === 11) { + this.loginMessage1 = 'Login server rejected session.'; + this.loginMessage1 = 'Please try again.'; + return; + } + if (reply === 12) { + this.loginMessage0 = 'You need a members account to login to this world.'; + this.loginMessage1 = 'Please subscribe, or use a different world.'; + return; + } + if (reply === 13) { + this.loginMessage0 = 'Could not complete login.'; + this.loginMessage1 = 'Please try using a different world.'; + return; + } + if (reply === 14) { + this.loginMessage0 = 'The server is being updated.'; + this.loginMessage1 = 'Please wait 1 minute and try again.'; + return; + } + if (reply === 15) { + this.ingame = true; + this.out.pos = 0; + this.in.pos = 0; + this.packetType = -1; + this.lastPacketType0 = -1; + this.lastPacketType1 = -1; + this.lastPacketType2 = -1; + this.packetSize = 0; + this.idleNetCycles = 0; + this.systemUpdateTimer = 0; + this.menuSize = 0; + this.menuVisible = false; + return; + } + if (reply === 16) { + this.loginMessage0 = 'Login attempts exceeded.'; + this.loginMessage1 = 'Please wait 1 minute and try again.'; + return; + } + if (reply === 17) { + this.loginMessage0 = 'You are standing in a members-only area.'; + this.loginMessage1 = 'To play on this world move to a free area first'; + } + } catch (err) { + console.log(err); + this.loginMessage0 = ''; + this.loginMessage1 = 'Error connecting to server.'; + } + }; + + private updateGame = async (): Promise => { + if (this.players === null) { + // client is unloading asynchronously + return; + } + + if (this.systemUpdateTimer > 1) { + this.systemUpdateTimer--; + } + + if (this.idleTimeout > 0) { + this.idleTimeout--; + } + + for (let i: number = 0; i < 5 && (await this.read()); i++) { + /* empty */ + } + + if (this.ingame) { + for (let wave: number = 0; wave < this.waveCount; wave++) { + if (this.waveDelay[wave] <= 0) { + try { + // if (this.waveIds[wave] !== this.lastWaveId || this.waveLoops[wave] !== this.lastWaveLoops) { + // todo: reuse buffer? + const buf: Packet | null = Wave.generate(this.waveIds[wave], this.waveLoops[wave]); + if (!buf) { + throw new Error(); + } + + if (Date.now() + ((buf.pos / 22) | 0) > this.lastWaveStartTime + ((this.lastWaveLength / 22) | 0)) { + this.lastWaveLength = buf.pos; + this.lastWaveStartTime = Date.now(); + this.lastWaveId = this.waveIds[wave]; + this.lastWaveLoops = this.waveLoops[wave]; + await playWave(buf.data.slice(0, buf.pos), this.waveVolume); + } + // else if (!this.waveReplay()) { // this logic just re-plays the old buffer + } catch (e) { + console.error(e); + /* empty */ + } + + // remove current wave + this.waveCount--; + for (let i: number = wave; i < this.waveCount; i++) { + this.waveIds[i] = this.waveIds[i + 1]; + this.waveLoops[i] = this.waveLoops[i + 1]; + this.waveDelay[i] = this.waveDelay[i + 1]; + } + wave--; + } else { + this.waveDelay[wave]--; + } + } + + if (this.nextMusicDelay > 0) { + this.nextMusicDelay -= 20; + + if (this.nextMusicDelay < 0) { + this.nextMusicDelay = 0; + } + + if (this.nextMusicDelay === 0 && this.midiActive && !Client.lowMemory && this.currentMidi) { + await this.setMidi(this.currentMidi, this.midiCrc, this.midiSize); + } + } + + const tracking: Packet | null = InputTracking.flush(); + if (tracking) { + this.out.p1isaac(ClientProt.EVENT_TRACKING); + this.out.p2(tracking.pos); + this.out.pdata(tracking.data, tracking.pos, 0); + tracking.release(); + } + + this.idleNetCycles++; + if (this.idleNetCycles > 750) { + await this.tryReconnect(); + } + + this.updatePlayers(); + this.updateNpcs(); + this.updateEntityChats(); + this.updateTemporaryLocs(); + + if ((this.actionKey[1] === 1 || this.actionKey[2] === 1 || this.actionKey[3] === 1 || this.actionKey[4] === 1) && this.cameraMovedWrite++ > 5) { + this.cameraMovedWrite = 0; + this.out.p1isaac(ClientProt.EVENT_CAMERA_POSITION); + this.out.p2(this.orbitCameraPitch); + this.out.p2(this.orbitCameraYaw); + this.out.p1(this.minimapAnticheatAngle); + this.out.p1(this.minimapZoom); + } + + this.sceneDelta++; + if (this.crossMode !== 0) { + this.crossCycle += 20; + if (this.crossCycle >= 400) { + this.crossMode = 0; + } + } + + if (this.selectedArea !== 0) { + this.selectedCycle++; + if (this.selectedCycle >= 15) { + if (this.selectedArea === 2) { + this.redrawSidebar = true; + } + if (this.selectedArea === 3) { + this.redrawChatback = true; + } + this.selectedArea = 0; + } + } + + if (this.objDragArea !== 0) { + this.objDragCycles++; + if (this.mouseX > this.objGrabX + 5 || this.mouseX < this.objGrabX - 5 || this.mouseY > this.objGrabY + 5 || this.mouseY < this.objGrabY - 5) { + this.objGrabThreshold = true; + } + + if (this.mouseButton === 0) { + if (this.objDragArea === 2) { + this.redrawSidebar = true; + } + if (this.objDragArea === 3) { + this.redrawChatback = true; + } + + this.objDragArea = 0; + if (this.objGrabThreshold && this.objDragCycles >= 5) { + this.hoveredSlotParentId = -1; + this.handleInput(); + if (this.hoveredSlotParentId === this.objDragInterfaceId && this.hoveredSlot !== this.objDragSlot) { + const com: ComType = ComType.instances[this.objDragInterfaceId]; + if (com.invSlotObjId) { + const obj: number = com.invSlotObjId[this.hoveredSlot]; + com.invSlotObjId[this.hoveredSlot] = com.invSlotObjId[this.objDragSlot]; + com.invSlotObjId[this.objDragSlot] = obj; + } + + if (com.invSlotObjCount) { + const count: number = com.invSlotObjCount[this.hoveredSlot]; + com.invSlotObjCount[this.hoveredSlot] = com.invSlotObjCount[this.objDragSlot]; + com.invSlotObjCount[this.objDragSlot] = count; + } + + this.out.p1isaac(ClientProt.INV_BUTTOND); + this.out.p2(this.objDragInterfaceId); + this.out.p2(this.objDragSlot); + this.out.p2(this.hoveredSlot); + } + } else if ((this.mouseButtonsOption === 1 || this.isAddFriendOption(this.menuSize - 1)) && this.menuSize > 2) { + this.showContextMenu(); + } else if (this.menuSize > 0) { + await this.useMenuOption(this.menuSize - 1); + } + + this.selectedCycle = 10; + this.mouseClickButton = 0; + } + } + + Client.updateCounter++; + if (Client.updateCounter > 127) { + Client.updateCounter = 0; + this.out.p1isaac(ClientProt.ANTICHEAT_CYCLELOGIC3); + this.out.p3(4991788); + } + + if (World3D.clickTileX !== -1) { + if (this.localPlayer) { + const x: number = World3D.clickTileX; + const z: number = World3D.clickTileZ; + const success: boolean = this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], x, z, 0, 0, 0, 0, 0, 0, true); + World3D.clickTileX = -1; + + if (success) { + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 1; + this.crossCycle = 0; + } + } + } + + if (this.mouseClickButton === 1 && this.modalMessage) { + this.modalMessage = null; + this.redrawChatback = true; + this.mouseClickButton = 0; + } + + await this.handleMouseInput(); // this is because of varps that set midi that we have to wait... + this.handleMinimapInput(); + this.handleTabInput(); + this.handleChatSettingsInput(); + + if (this.mouseButton === 1 || this.mouseClickButton === 1) { + this.dragCycles++; + } + + if (this.sceneState === 2) { + this.updateOrbitCamera(); + } + if (this.sceneState === 2 && this.cutscene) { + this.applyCutscene(); + } + + for (let i: number = 0; i < 5; i++) { + this.cameraModifierCycle[i]++; + } + + await this.handleInputKey(); + this.idleCycles++; + if (this.idleCycles > 4500) { + this.idleTimeout = 250; + this.idleCycles -= 500; + this.out.p1isaac(ClientProt.IDLE_TIMER); + } + + this.cameraOffsetCycle++; + if (this.cameraOffsetCycle > 500) { + this.cameraOffsetCycle = 0; + const rand: number = (Math.random() * 8.0) | 0; + if ((rand & 0x1) === 1) { + this.cameraAnticheatOffsetX += this.cameraOffsetXModifier; + } + if ((rand & 0x2) === 2) { + this.cameraAnticheatOffsetZ += this.cameraOffsetZModifier; + } + if ((rand & 0x4) === 4) { + this.cameraAnticheatAngle += this.cameraOffsetYawModifier; + } + } + + if (this.cameraAnticheatOffsetX < -50) { + this.cameraOffsetXModifier = 2; + } + if (this.cameraAnticheatOffsetX > 50) { + this.cameraOffsetXModifier = -2; + } + if (this.cameraAnticheatOffsetZ < -55) { + this.cameraOffsetZModifier = 2; + } + if (this.cameraAnticheatOffsetZ > 55) { + this.cameraOffsetZModifier = -2; + } + if (this.cameraAnticheatAngle < -40) { + this.cameraOffsetYawModifier = 1; + } + if (this.cameraAnticheatAngle > 40) { + this.cameraOffsetYawModifier = -1; + } + + this.minimapOffsetCycle++; + if (this.minimapOffsetCycle > 500) { + this.minimapOffsetCycle = 0; + const rand: number = (Math.random() * 8.0) | 0; + if ((rand & 0x1) === 1) { + this.minimapAnticheatAngle += this.minimapAngleModifier; + } + if ((rand & 0x2) === 2) { + this.minimapZoom += this.minimapZoomModifier; + } + } + + if (this.minimapAnticheatAngle < -60) { + this.minimapAngleModifier = 2; + } + if (this.minimapAnticheatAngle > 60) { + this.minimapAngleModifier = -2; + } + + if (this.minimapZoom < -20) { + this.minimapZoomModifier = 1; + } + if (this.minimapZoom > 10) { + this.minimapZoomModifier = -1; + } + + Client.update2Counter++; + if (Client.update2Counter > 110) { + Client.update2Counter = 0; + this.out.p1isaac(ClientProt.ANTICHEAT_CYCLELOGIC4); + this.out.p4(0); + } + + this.heartbeatTimer++; + if (this.heartbeatTimer > 50) { + this.out.p1isaac(ClientProt.NO_TIMEOUT); + } + + try { + if (this.stream && this.out.pos > 0) { + this.stream.write(this.out.data, this.out.pos, 0); + this.out.pos = 0; + this.heartbeatTimer = 0; + } + } catch (e) { + console.log(e); + await this.tryReconnect(); + // TODO extra logic for logout?? + } + } + }; + + private drawGame = (): void => { + if (this.players === null) { + // client is unloading asynchronously + return; + } + + if (this.redrawTitleBackground) { + this.redrawTitleBackground = false; + this.areaBackleft1?.draw(0, 11); + this.areaBackleft2?.draw(0, 375); + this.areaBackright1?.draw(729, 5); + this.areaBackright2?.draw(752, 231); + this.areaBacktop1?.draw(0, 0); + this.areaBacktop2?.draw(561, 0); + this.areaBackvmid1?.draw(520, 11); + this.areaBackvmid2?.draw(520, 231); + this.areaBackvmid3?.draw(501, 375); + this.areaBackhmid2?.draw(0, 345); + this.redrawSidebar = true; + this.redrawChatback = true; + this.redrawSideicons = true; + this.redrawPrivacySettings = true; + if (this.sceneState !== 2) { + this.areaViewport?.draw(8, 11); + this.areaMapback?.draw(561, 5); + } + } + if (this.sceneState === 2) { + this.drawScene(); + } + if (this.menuVisible && this.menuArea === 1) { + this.redrawSidebar = true; + } + let redraw: boolean = false; + if (this.sidebarInterfaceId !== -1) { + redraw = this.updateInterfaceAnimation(this.sidebarInterfaceId, this.sceneDelta); + if (redraw) { + this.redrawSidebar = true; + } + } + if (this.selectedArea === 2) { + this.redrawSidebar = true; + } + if (this.objDragArea === 2) { + this.redrawSidebar = true; + } + if (this.redrawSidebar) { + this.drawSidebar(); + this.redrawSidebar = false; + } + if (this.chatInterfaceId === -1) { + this.chatInterface.scrollPosition = this.chatScrollHeight - this.chatScrollOffset - 77; + if (this.mouseX > 453 && this.mouseX < 565 && this.mouseY > 350) { + this.handleScrollInput(this.mouseX - 22, this.mouseY - 375, this.chatScrollHeight, 77, false, 463, 0, this.chatInterface); + } + + let offset: number = this.chatScrollHeight - this.chatInterface.scrollPosition - 77; + if (offset < 0) { + offset = 0; + } + + if (offset > this.chatScrollHeight - 77) { + offset = this.chatScrollHeight - 77; + } + + if (this.chatScrollOffset !== offset) { + this.chatScrollOffset = offset; + this.redrawChatback = true; + } + } + + if (this.chatInterfaceId !== -1) { + redraw = this.updateInterfaceAnimation(this.chatInterfaceId, this.sceneDelta); + if (redraw) { + this.redrawChatback = true; + } + } + + if (this.selectedArea === 3) { + this.redrawChatback = true; + } + + if (this.objDragArea === 3) { + this.redrawChatback = true; + } + + if (this.modalMessage) { + this.redrawChatback = true; + } + + if (this.menuVisible && this.menuArea === 2) { + this.redrawChatback = true; + } + + if (this.redrawChatback) { + this.drawChatback(); + this.redrawChatback = false; + } + + if (this.sceneState === 2) { + this.drawMinimap(); + this.areaMapback?.draw(561, 5); + } + + if (this.flashingTab !== -1) { + this.redrawSideicons = true; + } + + if (this.redrawSideicons) { + if (this.flashingTab !== -1 && this.flashingTab === this.selectedTab) { + this.flashingTab = -1; + this.out.p1isaac(ClientProt.TUTORIAL_CLICKSIDE); + this.out.p1(this.selectedTab); + } + + this.redrawSideicons = false; + this.areaBackhmid1?.bind(); + this.imageBackhmid1?.draw(0, 0); + + if (this.sidebarInterfaceId === -1) { + if (this.tabInterfaceId[this.selectedTab] !== -1) { + if (this.selectedTab === 0) { + this.imageRedstone1?.draw(29, 30); + } else if (this.selectedTab === 1) { + this.imageRedstone2?.draw(59, 29); + } else if (this.selectedTab === 2) { + this.imageRedstone2?.draw(87, 29); + } else if (this.selectedTab === 3) { + this.imageRedstone3?.draw(115, 29); + } else if (this.selectedTab === 4) { + this.imageRedstone2h?.draw(156, 29); + } else if (this.selectedTab === 5) { + this.imageRedstone2h?.draw(184, 29); + } else if (this.selectedTab === 6) { + this.imageRedstone1h?.draw(212, 30); + } + } + + if (this.tabInterfaceId[0] !== -1 && (this.flashingTab !== 0 || this.loopCycle % 20 < 10)) { + this.imageSideicons[0]?.draw(35, 34); + } + + if (this.tabInterfaceId[1] !== -1 && (this.flashingTab !== 1 || this.loopCycle % 20 < 10)) { + this.imageSideicons[1]?.draw(59, 32); + } + + if (this.tabInterfaceId[2] !== -1 && (this.flashingTab !== 2 || this.loopCycle % 20 < 10)) { + this.imageSideicons[2]?.draw(86, 32); + } + + if (this.tabInterfaceId[3] !== -1 && (this.flashingTab !== 3 || this.loopCycle % 20 < 10)) { + this.imageSideicons[3]?.draw(121, 33); + } + + if (this.tabInterfaceId[4] !== -1 && (this.flashingTab !== 4 || this.loopCycle % 20 < 10)) { + this.imageSideicons[4]?.draw(157, 34); + } + + if (this.tabInterfaceId[5] !== -1 && (this.flashingTab !== 5 || this.loopCycle % 20 < 10)) { + this.imageSideicons[5]?.draw(185, 32); + } + + if (this.tabInterfaceId[6] !== -1 && (this.flashingTab !== 6 || this.loopCycle % 20 < 10)) { + this.imageSideicons[6]?.draw(212, 34); + } + } + + this.areaBackhmid1?.draw(520, 165); + this.areaBackbase2?.bind(); + this.imageBackbase2?.draw(0, 0); + + if (this.sidebarInterfaceId === -1) { + if (this.tabInterfaceId[this.selectedTab] !== -1) { + if (this.selectedTab === 7) { + this.imageRedstone1v?.draw(49, 0); + } else if (this.selectedTab === 8) { + this.imageRedstone2v?.draw(81, 0); + } else if (this.selectedTab === 9) { + this.imageRedstone2v?.draw(108, 0); + } else if (this.selectedTab === 10) { + this.imageRedstone3v?.draw(136, 1); + } else if (this.selectedTab === 11) { + this.imageRedstone2hv?.draw(178, 0); + } else if (this.selectedTab === 12) { + this.imageRedstone2hv?.draw(205, 0); + } else if (this.selectedTab === 13) { + this.imageRedstone1hv?.draw(233, 0); + } + } + + if (this.tabInterfaceId[8] !== -1 && (this.flashingTab !== 8 || this.loopCycle % 20 < 10)) { + this.imageSideicons[7]?.draw(80, 2); + } + + if (this.tabInterfaceId[9] !== -1 && (this.flashingTab !== 9 || this.loopCycle % 20 < 10)) { + this.imageSideicons[8]?.draw(107, 3); + } + + if (this.tabInterfaceId[10] !== -1 && (this.flashingTab !== 10 || this.loopCycle % 20 < 10)) { + this.imageSideicons[9]?.draw(142, 4); + } + + if (this.tabInterfaceId[11] !== -1 && (this.flashingTab !== 11 || this.loopCycle % 20 < 10)) { + this.imageSideicons[10]?.draw(179, 2); + } + + if (this.tabInterfaceId[12] !== -1 && (this.flashingTab !== 12 || this.loopCycle % 20 < 10)) { + this.imageSideicons[11]?.draw(206, 2); + } + + if (this.tabInterfaceId[13] !== -1 && (this.flashingTab !== 13 || this.loopCycle % 20 < 10)) { + this.imageSideicons[12]?.draw(230, 2); + } + } + this.areaBackbase2?.draw(501, 492); + this.areaViewport?.bind(); + } + + if (this.redrawPrivacySettings) { + this.redrawPrivacySettings = false; + this.areaBackbase1?.bind(); + this.imageBackbase1?.draw(0, 0); + + this.fontPlain12?.drawStringTaggableCenter(57, 33, 'Public chat', Colors.WHITE, true); + if (this.publicChatSetting === 0) { + this.fontPlain12?.drawStringTaggableCenter(57, 46, 'On', Colors.GREEN, true); + } + if (this.publicChatSetting === 1) { + this.fontPlain12?.drawStringTaggableCenter(57, 46, 'Friends', Colors.YELLOW, true); + } + if (this.publicChatSetting === 2) { + this.fontPlain12?.drawStringTaggableCenter(57, 46, 'Off', Colors.RED, true); + } + if (this.publicChatSetting === 3) { + this.fontPlain12?.drawStringTaggableCenter(57, 46, 'Hide', Colors.CYAN, true); + } + + this.fontPlain12?.drawStringTaggableCenter(186, 33, 'Private chat', Colors.WHITE, true); + if (this.privateChatSetting === 0) { + this.fontPlain12?.drawStringTaggableCenter(186, 46, 'On', Colors.GREEN, true); + } + if (this.privateChatSetting === 1) { + this.fontPlain12?.drawStringTaggableCenter(186, 46, 'Friends', Colors.YELLOW, true); + } + if (this.privateChatSetting === 2) { + this.fontPlain12?.drawStringTaggableCenter(186, 46, 'Off', Colors.RED, true); + } + + this.fontPlain12?.drawStringTaggableCenter(326, 33, 'Trade/duel', Colors.WHITE, true); + if (this.tradeChatSetting === 0) { + this.fontPlain12?.drawStringTaggableCenter(326, 46, 'On', Colors.GREEN, true); + } + if (this.tradeChatSetting === 1) { + this.fontPlain12?.drawStringTaggableCenter(326, 46, 'Friends', Colors.YELLOW, true); + } + if (this.tradeChatSetting === 2) { + this.fontPlain12?.drawStringTaggableCenter(326, 46, 'Off', Colors.RED, true); + } + + this.fontPlain12?.drawStringTaggableCenter(462, 38, 'Report abuse', Colors.WHITE, true); + this.areaBackbase1?.draw(0, 471); + this.areaViewport?.bind(); + } + + this.sceneDelta = 0; + }; + + private drawScene = (): void => { + this.sceneCycle++; + this.pushPlayers(); + this.pushNpcs(); + this.pushProjectiles(); + this.pushSpotanims(); + this.pushLocs(); + + if (!this.cutscene) { + let pitch: number = this.orbitCameraPitch; + + if (((this.cameraPitchClamp / 256) | 0) > pitch) { + pitch = (this.cameraPitchClamp / 256) | 0; + } + + if (this.cameraModifierEnabled[4] && this.cameraModifierWobbleScale[4] + 128 > pitch) { + pitch = this.cameraModifierWobbleScale[4] + 128; + } + + const yaw: number = (this.orbitCameraYaw + this.cameraAnticheatAngle) & 0x7ff; + if (this.localPlayer) { + this.orbitCamera(this.orbitCameraX, this.getHeightmapY(this.currentLevel, this.localPlayer.x, this.localPlayer.z) - 50, this.orbitCameraZ, yaw, pitch, pitch * 3 + 600); + } + + Client.drawCounter++; + if (Client.drawCounter > 1802) { + Client.drawCounter = 0; + this.out.p1isaac(ClientProt.ANTICHEAT_CYCLELOGIC2); + this.out.p1(0); + const start: number = this.out.pos; + this.out.p2(29711); + this.out.p1(70); + this.out.p1((Math.random() * 256.0) | 0); + this.out.p1(242); + this.out.p1(186); + this.out.p1(39); + this.out.p1(61); + if (((Math.random() * 2.0) | 0) === 0) { + this.out.p1(13); + } + if (((Math.random() * 2.0) | 0) === 0) { + this.out.p2(57856); + } + this.out.p2((Math.random() * 65536.0) | 0); + this.out.psize1(this.out.pos - start); + } + } + + let level: number; + if (this.cutscene) { + level = this.getTopLevelCutscene(); + } else { + level = this.getTopLevel(); + } + + const cameraX: number = this.cameraX; + const cameraY: number = this.cameraY; + const cameraZ: number = this.cameraZ; + const cameraPitch: number = this.cameraPitch; + const cameraYaw: number = this.cameraYaw; + let jitter: number; + for (let type: number = 0; type < 5; type++) { + if (this.cameraModifierEnabled[type]) { + jitter = + (Math.random() * (this.cameraModifierJitter[type] * 2 + 1) - this.cameraModifierJitter[type] + Math.sin(this.cameraModifierCycle[type] * (this.cameraModifierWobbleSpeed[type] / 100.0)) * this.cameraModifierWobbleScale[type]) | 0; + + if (type === 0) { + this.cameraX += jitter; + } + if (type === 1) { + this.cameraY += jitter; + } + if (type === 2) { + this.cameraZ += jitter; + } + if (type === 3) { + this.cameraYaw = (this.cameraYaw + jitter) & 0x7ff; + } + if (type === 4) { + this.cameraPitch += jitter; + if (this.cameraPitch < 128) { + this.cameraPitch = 128; + } + if (this.cameraPitch > 383) { + this.cameraPitch = 383; + } + } + } + } + jitter = Draw3D.cycle; + Model.checkHover = true; + Model.pickedCount = 0; + Model.mouseX = this.mouseX - 8; + Model.mouseY = this.mouseY - 11; + Draw2D.clear(); + this.scene?.draw(this.cameraX, this.cameraY, this.cameraZ, level, this.cameraYaw, this.cameraPitch, this.loopCycle); + this.scene?.clearTemporaryLocs(); + this.draw2DEntityElements(); + this.drawTileHint(); + if (Client.showDebug) { + this.drawDebug(); + } + this.updateTextures(jitter); + this.draw3DEntityElements(); + this.areaViewport?.draw(8, 11); + this.cameraX = cameraX; + this.cameraY = cameraY; + this.cameraZ = cameraZ; + this.cameraPitch = cameraPitch; + this.cameraYaw = cameraYaw; + }; + + private clearCaches = (): void => { + LocType.modelCacheStatic?.clear(); + LocType.modelCacheDynamic?.clear(); + NpcType.modelCache?.clear(); + ObjType.modelCache?.clear(); + ObjType.iconCache?.clear(); + PlayerEntity.modelCache?.clear(); + SpotAnimType.modelCache?.clear(); + }; + + private projectFromEntity = (entity: PathingEntity, height: number): void => { + this.projectFromGround(entity.x, height, entity.z); + }; + + private projectFromGround = (x: number, height: number, z: number): void => { + if (x < 128 || z < 128 || x > 13056 || z > 13056) { + this.projectX = -1; + this.projectY = -1; + return; + } + + const y: number = this.getHeightmapY(this.currentLevel, x, z) - height; + this.project(x, y, z); + }; + + private project = (x: number, y: number, z: number): void => { + let dx: number = x - this.cameraX; + let dy: number = y - this.cameraY; + let dz: number = z - this.cameraZ; + + const sinPitch: number = Draw3D.sin[this.cameraPitch]; + const cosPitch: number = Draw3D.cos[this.cameraPitch]; + const sinYaw: number = Draw3D.sin[this.cameraYaw]; + const cosYaw: number = Draw3D.cos[this.cameraYaw]; + + let tmp: number = (dz * sinYaw + dx * cosYaw) >> 16; + dz = (dz * cosYaw - dx * sinYaw) >> 16; + dx = tmp; + + tmp = (dy * cosPitch - dz * sinPitch) >> 16; + dz = (dy * sinPitch + dz * cosPitch) >> 16; + dy = tmp; + + if (dz >= 50) { + this.projectX = Draw3D.centerX + (((dx << 9) / dz) | 0); + this.projectY = Draw3D.centerY + (((dy << 9) / dz) | 0); + } else { + this.projectX = -1; + this.projectY = -1; + } + }; + + private draw2DEntityElements = (): void => { + this.chatCount = 0; + + for (let index: number = -1; index < this.playerCount + this.npcCount; index++) { + let entity: PathingEntity | null = null; + if (index === -1) { + entity = this.localPlayer; + } else if (index < this.playerCount) { + entity = this.players[this.playerIds[index]]; + } else { + entity = this.npcs[this.npcIds[index - this.playerCount]]; + } + + if (!entity || !entity.isVisible()) { + continue; + } + + if (index < this.playerCount) { + let y: number = 30; + + const player: PlayerEntity = entity as PlayerEntity; + if (player.headicons !== 0) { + this.projectFromEntity(entity, entity.height + 15); + + if (this.projectX > -1) { + for (let icon: number = 0; icon < 8; icon++) { + if ((player.headicons & (0x1 << icon)) !== 0 && this.imageHeadicons[icon]) { + this.imageHeadicons[icon]!.draw(this.projectX - 12, this.projectY - y); + y -= 25; + } + } + } + } + + if (index >= 0 && this.hintType === 10 && this.hintPlayer === this.playerIds[index]) { + this.projectFromEntity(entity, entity.height + 15); + + if (this.projectX > -1 && this.imageHeadicons[7]) { + this.imageHeadicons[7].draw(this.projectX - 12, this.projectY - y); + } + } + } else if (this.hintType === 1 && this.hintNpc === this.npcIds[index - this.playerCount] && this.loopCycle % 20 < 10) { + this.projectFromEntity(entity, entity.height + 15); + + if (this.projectX > -1 && this.imageHeadicons[2]) { + this.imageHeadicons[2].draw(this.projectX - 12, this.projectY - 28); + } + } + + if (entity.chat && (index >= this.playerCount || this.publicChatSetting === 0 || this.publicChatSetting === 3 || (this.publicChatSetting === 1 && this.isFriend((entity as PlayerEntity).name)))) { + this.projectFromEntity(entity, entity.height); + + if (this.projectX > -1 && this.chatCount < Client.MAX_CHATS && this.fontBold12) { + this.chatWidth[this.chatCount] = (this.fontBold12.stringWidth(entity.chat) / 2) | 0; + this.chatHeight[this.chatCount] = this.fontBold12.height; + this.chatX[this.chatCount] = this.projectX; + this.chatY[this.chatCount] = this.projectY; + + this.chatColors[this.chatCount] = entity.chatColor; + this.chatStyles[this.chatCount] = entity.chatStyle; + this.chatTimers[this.chatCount] = entity.chatTimer; + this.chats[this.chatCount++] = entity.chat as string; + + if (this.chatEffects === 0 && entity.chatStyle === 1) { + this.chatHeight[this.chatCount] += 10; + this.chatY[this.chatCount] += 5; + } + + if (this.chatEffects === 0 && entity.chatStyle === 2) { + this.chatWidth[this.chatCount] = 60; + } + } + } + + if (entity.combatCycle > this.loopCycle + 100) { + this.projectFromEntity(entity, entity.height + 15); + + if (this.projectX > -1) { + let w: number = ((entity.health * 30) / entity.totalHealth) | 0; + if (w > 30) { + w = 30; + } + Draw2D.fillRect(this.projectX - 15, this.projectY - 3, w, 5, Colors.GREEN); + Draw2D.fillRect(this.projectX - 15 + w, this.projectY - 3, 30 - w, 5, Colors.RED); + } + } + + if (entity.combatCycle > this.loopCycle + 330) { + this.projectFromEntity(entity, (entity.height / 2) | 0); + + if (this.projectX > -1 && this.imageHitmarks[entity.damageType]) { + this.imageHitmarks[entity.damageType]!.draw(this.projectX - 12, this.projectY - 12); + this.fontPlain11?.drawStringCenter(this.projectX, this.projectY + 4, entity.damage.toString(), Colors.BLACK); + this.fontPlain11?.drawStringCenter(this.projectX - 1, this.projectY + 3, entity.damage.toString(), Colors.WHITE); + } + } + } + + for (let i: number = 0; i < this.chatCount; i++) { + const x: number = this.chatX[i]; + let y: number = this.chatY[i]; + const padding: number = this.chatWidth[i]; + const height: number = this.chatHeight[i]; + let sorting: boolean = true; + while (sorting) { + sorting = false; + for (let j: number = 0; j < i; j++) { + if (y + 2 > this.chatY[j] - this.chatHeight[j] && y - height < this.chatY[j] + 2 && x - padding < this.chatX[j] + this.chatWidth[j] && x + padding > this.chatX[j] - this.chatWidth[j] && this.chatY[j] - this.chatHeight[j] < y) { + y = this.chatY[j] - this.chatHeight[j]; + sorting = true; + } + } + } + this.projectX = this.chatX[i]; + this.projectY = this.chatY[i] = y; + const message: string | null = this.chats[i]; + if (this.chatEffects == 0) { + let color: number = Colors.YELLOW; + if (this.chatColors[i] < 6) { + color = Colors.CHAT_COLORS[this.chatColors[i]]; + } + if (this.chatColors[i] == 6) { + color = this.sceneCycle % 20 < 10 ? Colors.RED : Colors.YELLOW; + } + if (this.chatColors[i] == 7) { + color = this.sceneCycle % 20 < 10 ? Colors.BLUE : Colors.CYAN; + } + if (this.chatColors[i] == 8) { + color = this.sceneCycle % 20 < 10 ? 0xb000 : 0x80ff80; + } + if (this.chatColors[i] == 9) { + const delta: number = 150 - this.chatTimers[i]; + if (delta < 50) { + color = delta * 1280 + Colors.RED; + } else if (delta < 100) { + color = Colors.YELLOW - (delta - 50) * 327680; + } else if (delta < 150) { + color = (delta - 100) * 5 + Colors.GREEN; + } + } + if (this.chatColors[i] == 10) { + const delta: number = 150 - this.chatTimers[i]; + if (delta < 50) { + color = delta * 5 + Colors.RED; + } else if (delta < 100) { + color = Colors.MAGENTA - (delta - 50) * 327680; + } else if (delta < 150) { + color = (delta - 100) * 327680 + Colors.BLUE - (delta - 100) * 5; + } + } + if (this.chatColors[i] == 11) { + const delta: number = 150 - this.chatTimers[i]; + if (delta < 50) { + color = Colors.WHITE - delta * 327685; + } else if (delta < 100) { + color = (delta - 50) * 327685 + Colors.GREEN; + } else if (delta < 150) { + color = Colors.WHITE - (delta - 100) * 327680; + } + } + if (this.chatStyles[i] == 0) { + this.fontBold12?.drawStringCenter(this.projectX, this.projectY + 1, message, Colors.BLACK); + this.fontBold12?.drawStringCenter(this.projectX, this.projectY, message, color); + } + if (this.chatStyles[i] == 1) { + this.fontBold12?.drawCenteredWave(this.projectX, this.projectY + 1, message, Colors.BLACK, this.sceneCycle); + this.fontBold12?.drawCenteredWave(this.projectX, this.projectY, message, color, this.sceneCycle); + } + if (this.chatStyles[i] == 2) { + const w: number = this.fontBold12?.stringWidth(message) ?? 0; + const offsetX: number = ((150 - this.chatTimers[i]) * (w + 100)) / 150; + Draw2D.setBounds(334, this.projectX + 50, 0, this.projectX - 50); + this.fontBold12?.drawString(this.projectX + 50 - offsetX, this.projectY + 1, message, Colors.BLACK); + this.fontBold12?.drawString(this.projectX + 50 - offsetX, this.projectY, message, color); + Draw2D.resetBounds(); + } + } else { + this.fontBold12?.drawStringCenter(this.projectX, this.projectY + 1, message, Colors.BLACK); + this.fontBold12?.drawStringCenter(this.projectX, this.projectY, message, Colors.YELLOW); + } + } + }; + + private drawTileHint = (): void => { + if (this.hintType !== 2 || !this.imageHeadicons[2]) { + return; + } + + this.projectFromGround(((this.hintTileX - this.sceneBaseTileX) << 7) + this.hintOffsetX, this.hintHeight * 2, ((this.hintTileZ - this.sceneBaseTileZ) << 7) + this.hintOffsetZ); + + if (this.projectX > -1 && this.loopCycle % 20 < 10) { + this.imageHeadicons[2].draw(this.projectX - 12, this.projectY - 28); + } + }; + + private drawDebug = (): void => { + const x: number = 507; + let y: number = 20; + this.fontPlain11?.drawStringRight(x, y, `FPS: ${this.fps}`, Colors.YELLOW, true); + y += 13; + this.fontPlain11?.drawStringRight(x, y, `Speed: ${this.ms.toFixed(4)} ms`, Colors.YELLOW, true); + y += 13; + this.fontPlain11?.drawStringRight(x, y, `Average: ${this.msAvg.toFixed(4)} ms`, Colors.YELLOW, true); + y += 13; + this.fontPlain11?.drawStringRight(x, y, `Slowest: ${this.slowestMS.toFixed(4)} ms`, Colors.YELLOW, true); + y += 13; + this.fontPlain11?.drawStringRight(x, y, `Occluders: ${World3D.activeOccluderCount}`, Colors.YELLOW, true); + // this.fontPlain11?.drawRight(x, y, `Rate: ${this.deltime} ms`, Colors.YELLOW, true); + }; + + private draw3DEntityElements = (): void => { + this.drawPrivateMessages(); + if (this.crossMode === 1) { + this.imageCrosses[(this.crossCycle / 100) | 0]?.draw(this.crossX - 8 - 8, this.crossY - 8 - 11); + } + + if (this.crossMode === 2) { + this.imageCrosses[((this.crossCycle / 100) | 0) + 4]?.draw(this.crossX - 8 - 8, this.crossY - 8 - 11); + } + + if (this.viewportInterfaceId !== -1) { + this.updateInterfaceAnimation(this.viewportInterfaceId, this.sceneDelta); + this.drawInterface(ComType.instances[this.viewportInterfaceId], 0, 0, 0); + } + + this.drawWildyLevel(); + + if (!this.menuVisible) { + this.handleInput(); + this.drawTooltip(); + } else if (this.menuArea === 0) { + this.drawMenu(); + } + + if (this.inMultizone === 1) { + if (this.wildernessLevel > 0 || this.worldLocationState === 1) { + this.imageHeadicons[1]?.draw(472, 258); + } else { + this.imageHeadicons[1]?.draw(472, 296); + } + } + + if (this.wildernessLevel > 0) { + this.imageHeadicons[0]?.draw(472, 296); + this.fontPlain12?.drawStringCenter(484, 329, 'Level: ' + this.wildernessLevel, Colors.YELLOW); + } + + if (this.worldLocationState === 1) { + this.imageHeadicons[6]?.draw(472, 296); + this.fontPlain12?.drawStringCenter(484, 329, 'Arena', Colors.YELLOW); + } + + if (this.systemUpdateTimer !== 0) { + let seconds: number = (this.systemUpdateTimer / 50) | 0; + const minutes: number = (seconds / 60) | 0; + seconds %= 60; + + if (seconds < 10) { + this.fontPlain12?.drawString(4, 329, 'System update in: ' + minutes + ':0' + seconds, Colors.YELLOW); + } else { + this.fontPlain12?.drawString(4, 329, 'System update in: ' + minutes + ':' + seconds, Colors.YELLOW); + } + } + }; + + private drawPrivateMessages = (): void => { + if (this.splitPrivateChat === 0) { + return; + } + + const font: PixFont | null = this.fontPlain12; + let lineOffset: number = 0; + if (this.systemUpdateTimer !== 0) { + lineOffset = 1; + } + + for (let i: number = 0; i < 100; i++) { + if (!this.messageText[i]) { + continue; + } + + const type: number = this.messageType[i]; + let y: number; + if ((type === 3 || type === 7) && (type === 7 || this.privateChatSetting === 0 || (this.privateChatSetting === 1 && this.isFriend(this.messageSender[i])))) { + y = 329 - lineOffset * 13; + font?.drawString(4, y, 'From ' + this.messageSender[i] + ': ' + this.messageText[i], Colors.BLACK); + font?.drawString(4, y - 1, 'From ' + this.messageSender[i] + ': ' + this.messageText[i], Colors.CYAN); + + lineOffset++; + if (lineOffset >= 5) { + return; + } + } + + if (type === 5 && this.privateChatSetting < 2) { + y = 329 - lineOffset * 13; + font?.drawString(4, y, this.messageText[i], Colors.BLACK); + font?.drawString(4, y - 1, this.messageText[i], Colors.CYAN); + + lineOffset++; + if (lineOffset >= 5) { + return; + } + } + + if (type === 6 && this.privateChatSetting < 2) { + y = 329 - lineOffset * 13; + font?.drawString(4, y, 'To ' + this.messageSender[i] + ': ' + this.messageText[i], Colors.BLACK); + font?.drawString(4, y - 1, 'To ' + this.messageSender[i] + ': ' + this.messageText[i], Colors.CYAN); + + lineOffset++; + if (lineOffset >= 5) { + return; + } + } + } + }; + + private drawWildyLevel = (): void => { + if (!this.localPlayer) { + return; + } + + const x: number = (this.localPlayer.x >> 7) + this.sceneBaseTileX; + const z: number = (this.localPlayer.z >> 7) + this.sceneBaseTileZ; + + if (x >= 2944 && x < 3392 && z >= 3520 && z < 6400) { + this.wildernessLevel = (((z - 3520) / 8) | 0) + 1; + } else if (x >= 2944 && x < 3392 && z >= 9920 && z < 12800) { + this.wildernessLevel = (((z - 9920) / 8) | 0) + 1; + } else { + this.wildernessLevel = 0; + } + + this.worldLocationState = 0; + if (x >= 3328 && x < 3392 && z >= 3200 && z < 3264) { + const localX: number = x & 63; + const localZ: number = z & 63; + + if (localX >= 4 && localX <= 29 && localZ >= 44 && localZ <= 58) { + this.worldLocationState = 1; + } else if (localX >= 36 && localX <= 61 && localZ >= 44 && localZ <= 58) { + this.worldLocationState = 1; + } else if (localX >= 4 && localX <= 29 && localZ >= 25 && localZ <= 39) { + this.worldLocationState = 1; + } else if (localX >= 36 && localX <= 61 && localZ >= 25 && localZ <= 39) { + this.worldLocationState = 1; + } else if (localX >= 4 && localX <= 29 && localZ >= 6 && localZ <= 20) { + this.worldLocationState = 1; + } else if (localX >= 36 && localX <= 61 && localZ >= 6 && localZ <= 20) { + this.worldLocationState = 1; + } + } + + if (this.worldLocationState === 0 && x >= 3328 && x <= 3393 && z >= 3203 && z <= 3325) { + this.worldLocationState = 2; + } + + this.overrideChat = 0; + if (x >= 3053 && x <= 3156 && z >= 3056 && z <= 3136) { + this.overrideChat = 1; + } else if (x >= 3072 && x <= 3118 && z >= 9492 && z <= 9535) { + this.overrideChat = 1; + } + + if (this.overrideChat === 1 && x >= 3139 && x <= 3199 && z >= 3008 && z <= 3062) { + this.overrideChat = 0; + } + }; + + private drawSidebar = (): void => { + this.areaSidebar?.bind(); + if (this.areaSidebarOffsets) { + Draw3D.lineOffset = this.areaSidebarOffsets; + } + this.imageInvback?.draw(0, 0); + if (this.sidebarInterfaceId !== -1) { + this.drawInterface(ComType.instances[this.sidebarInterfaceId], 0, 0, 0); + } else if (this.tabInterfaceId[this.selectedTab] !== -1) { + this.drawInterface(ComType.instances[this.tabInterfaceId[this.selectedTab]], 0, 0, 0); + } + if (this.menuVisible && this.menuArea === 1) { + this.drawMenu(); + } + this.areaSidebar?.draw(562, 231); + this.areaViewport?.bind(); + if (this.areaViewportOffsets) { + Draw3D.lineOffset = this.areaViewportOffsets; + } + }; + + private drawChatback = (): void => { + this.areaChatback?.bind(); + if (this.areaChatbackOffsets) { + Draw3D.lineOffset = this.areaChatbackOffsets; + } + this.imageChatback?.draw(0, 0); + if (this.showSocialInput) { + this.fontBold12?.drawStringCenter(239, 40, this.socialMessage, Colors.BLACK); + this.fontBold12?.drawStringCenter(239, 60, this.socialInput + '*', Colors.DARKBLUE); + } else if (this.chatbackInputOpen) { + this.fontBold12?.drawStringCenter(239, 40, 'Enter amount:', Colors.BLACK); + this.fontBold12?.drawStringCenter(239, 60, this.chatbackInput + '*', Colors.DARKBLUE); + } else if (this.modalMessage) { + this.fontBold12?.drawStringCenter(239, 40, this.modalMessage, Colors.BLACK); + this.fontBold12?.drawStringCenter(239, 60, 'Click to continue', Colors.DARKBLUE); + } else if (this.chatInterfaceId !== -1) { + this.drawInterface(ComType.instances[this.chatInterfaceId], 0, 0, 0); + } else if (this.stickyChatInterfaceId === -1) { + const font: PixFont | null = this.fontPlain12; + let line: number = 0; + Draw2D.setBounds(0, 0, 463, 77); + for (let i: number = 0; i < 100; i++) { + const message: string | null = this.messageText[i]; + if (!message) { + continue; + } + const type: number = this.messageType[i]; + const offset: number = this.chatScrollOffset + 70 - line * 14; + if (type === 0) { + if (offset > 0 && offset < 110) { + font?.drawString(4, offset, message, Colors.BLACK); + } + line++; + } + if (type === 1) { + if (offset > 0 && offset < 110) { + font?.drawString(4, offset, this.messageSender[i] + ':', Colors.WHITE); + font?.drawString(font.stringWidth(this.messageSender[i]) + 12, offset, message, Colors.BLUE); + } + line++; + } + if (type === 2 && (this.publicChatSetting === 0 || (this.publicChatSetting === 1 && this.isFriend(this.messageSender[i])))) { + if (offset > 0 && offset < 110) { + font?.drawString(4, offset, this.messageSender[i] + ':', Colors.BLACK); + font?.drawString(font.stringWidth(this.messageSender[i]) + 12, offset, message, Colors.BLUE); + } + line++; + } + if ((type === 3 || type === 7) && this.splitPrivateChat === 0 && (type === 7 || this.privateChatSetting === 0 || (this.privateChatSetting === 1 && this.isFriend(this.messageSender[i])))) { + if (offset > 0 && offset < 110) { + font?.drawString(4, offset, 'From ' + this.messageSender[i] + ':', Colors.BLACK); + font?.drawString(font.stringWidth('From ' + this.messageSender[i]) + 12, offset, message, Colors.DARKRED); + } + line++; + } + if (type === 4 && (this.tradeChatSetting === 0 || (this.tradeChatSetting === 1 && this.isFriend(this.messageSender[i])))) { + if (offset > 0 && offset < 110) { + font?.drawString(4, offset, this.messageSender[i] + ' ' + this.messageText[i], Colors.TRADE_MESSAGE); + } + line++; + } + if (type === 5 && this.splitPrivateChat === 0 && this.privateChatSetting < 2) { + if (offset > 0 && offset < 110) { + font?.drawString(4, offset, message, Colors.DARKRED); + } + line++; + } + if (type === 6 && this.splitPrivateChat === 0 && this.privateChatSetting < 2) { + if (offset > 0 && offset < 110) { + font?.drawString(4, offset, 'To ' + this.messageSender[i] + ':', Colors.BLACK); + font?.drawString(font.stringWidth('To ' + this.messageSender[i]) + 12, offset, message, Colors.DARKRED); + } + line++; + } + if (type === 8 && (this.tradeChatSetting === 0 || (this.tradeChatSetting === 1 && this.isFriend(this.messageSender[i])))) { + if (offset > 0 && offset < 110) { + font?.drawString(4, offset, this.messageSender[i] + ' ' + this.messageText[i], Colors.DUEL_MESSAGE); + } + line++; + } + } + Draw2D.resetBounds(); + this.chatScrollHeight = line * 14 + 7; + if (this.chatScrollHeight < 78) { + this.chatScrollHeight = 78; + } + this.drawScrollbar(463, 0, this.chatScrollHeight - this.chatScrollOffset - 77, this.chatScrollHeight, 77); + font?.drawString(4, 90, JString.formatName(this.username) + ':', Colors.BLACK); + font?.drawString(font.stringWidth(this.username + ': ') + 6, 90, this.chatTyped + '*', Colors.BLUE); + Draw2D.drawHorizontalLine(0, 77, Colors.BLACK, 479); + } else { + this.drawInterface(ComType.instances[this.stickyChatInterfaceId], 0, 0, 0); + } + if (this.menuVisible && this.menuArea === 2) { + this.drawMenu(); + } + this.areaChatback?.draw(22, 375); + this.areaViewport?.bind(); + if (this.areaViewportOffsets) { + Draw3D.lineOffset = this.areaViewportOffsets; + } + }; + + private drawMinimap = (): void => { + this.areaMapback?.bind(); + if (!this.localPlayer) { + return; + } + + const angle: number = (this.orbitCameraYaw + this.minimapAnticheatAngle) & 0x7ff; + let anchorX: number = ((this.localPlayer.x / 32) | 0) + 48; + let anchorY: number = 464 - ((this.localPlayer.z / 32) | 0); + + this.imageMinimap?.drawRotatedMasked(21, 9, 146, 151, this.minimapMaskLineOffsets, this.minimapMaskLineLengths, anchorX, anchorY, angle, this.minimapZoom + 256); + this.imageCompass?.drawRotatedMasked(0, 0, 33, 33, this.compassMaskLineOffsets, this.compassMaskLineLengths, 25, 25, this.orbitCameraYaw, 256); + for (let i: number = 0; i < this.activeMapFunctionCount; i++) { + anchorX = this.activeMapFunctionX[i] * 4 + 2 - ((this.localPlayer.x / 32) | 0); + anchorY = this.activeMapFunctionZ[i] * 4 + 2 - ((this.localPlayer.z / 32) | 0); + this.drawOnMinimap(anchorY, this.activeMapFunctions[i], anchorX); + } + + for (let ltx: number = 0; ltx < CollisionMap.SIZE; ltx++) { + for (let ltz: number = 0; ltz < CollisionMap.SIZE; ltz++) { + const stack: LinkList | null = this.levelObjStacks[this.currentLevel][ltx][ltz]; + if (stack) { + anchorX = ltx * 4 + 2 - ((this.localPlayer.x / 32) | 0); + anchorY = ltz * 4 + 2 - ((this.localPlayer.z / 32) | 0); + this.drawOnMinimap(anchorY, this.imageMapdot0, anchorX); + } + } + } + + for (let i: number = 0; i < this.npcCount; i++) { + const npc: NpcEntity | null = this.npcs[this.npcIds[i]]; + if (npc && npc.isVisible() && npc.type && npc.type.visonmap) { + anchorX = ((npc.x / 32) | 0) - ((this.localPlayer.x / 32) | 0); + anchorY = ((npc.z / 32) | 0) - ((this.localPlayer.z / 32) | 0); + this.drawOnMinimap(anchorY, this.imageMapdot1, anchorX); + } + } + + for (let i: number = 0; i < this.playerCount; i++) { + const player: PlayerEntity | null = this.players[this.playerIds[i]]; + if (player && player.isVisible() && player.name) { + anchorX = ((player.x / 32) | 0) - ((this.localPlayer.x / 32) | 0); + anchorY = ((player.z / 32) | 0) - ((this.localPlayer.z / 32) | 0); + + let friend: boolean = false; + const name37: bigint = JString.toBase37(player.name); + for (let j: number = 0; j < this.friendCount; j++) { + if (name37 === this.friendName37[j] && this.friendWorld[j] !== 0) { + friend = true; + break; + } + } + + if (friend) { + this.drawOnMinimap(anchorY, this.imageMapdot3, anchorX); + } else { + this.drawOnMinimap(anchorY, this.imageMapdot2, anchorX); + } + } + } + + if (this.flagSceneTileX !== 0) { + anchorX = this.flagSceneTileX * 4 + 2 - ((this.localPlayer.x / 32) | 0); + anchorY = this.flagSceneTileZ * 4 + 2 - ((this.localPlayer.z / 32) | 0); + this.drawOnMinimap(anchorY, this.imageMapflag, anchorX); + } + // the white square local player position in the center of the minimap. + Draw2D.fillRect(93, 82, 3, 3, Colors.WHITE); + this.areaViewport?.bind(); + }; + + private drawOnMinimap = (dy: number, image: Pix24 | null, dx: number): void => { + if (!image) { + return; + } + + const angle: number = (this.orbitCameraYaw + this.minimapAnticheatAngle) & 0x7ff; + const distance: number = dx * dx + dy * dy; + if (distance > 6400) { + return; + } + + let sinAngle: number = Draw3D.sin[angle]; + let cosAngle: number = Draw3D.cos[angle]; + + sinAngle = ((sinAngle * 256) / (this.minimapZoom + 256)) | 0; + cosAngle = ((cosAngle * 256) / (this.minimapZoom + 256)) | 0; + + const x: number = (dy * sinAngle + dx * cosAngle) >> 16; + const y: number = (dy * cosAngle - dx * sinAngle) >> 16; + + if (distance > 2500 && this.imageMapback) { + image.drawMasked(x + 94 - ((image.cropW / 2) | 0), 83 - y - ((image.cropH / 2) | 0), this.imageMapback); + } else { + image.draw(x + 94 - ((image.cropW / 2) | 0), 83 - y - ((image.cropH / 2) | 0)); + } + }; + + private createMinimap = (level: number): void => { + if (!this.imageMinimap) { + return; + } + + const pixels: Int32Array = this.imageMinimap.pixels; + const length: number = pixels.length; + for (let i: number = 0; i < length; i++) { + pixels[i] = 0; + } + + for (let z: number = 1; z < CollisionMap.SIZE - 1; z++) { + let offset: number = (CollisionMap.SIZE - 1 - z) * 512 * 4 + 24628; + + for (let x: number = 1; x < CollisionMap.SIZE - 1; x++) { + if (this.levelTileFlags && (this.levelTileFlags[level][x][z] & 0x18) === 0) { + this.scene?.drawMinimapTile(level, x, z, pixels, offset, 512); + } + + if (level < 3 && this.levelTileFlags && (this.levelTileFlags[level + 1][x][z] & 0x8) !== 0) { + this.scene?.drawMinimapTile(level + 1, x, z, pixels, offset, 512); + } + + offset += 4; + } + } + + const wallRgb: number = ((((Math.random() * 20.0) | 0) + 238 - 10) << 16) + ((((Math.random() * 20.0) | 0) + 238 - 10) << 8) + ((Math.random() * 20.0) | 0) + 238 - 10; + const doorRgb: number = (((Math.random() * 20.0) | 0) + 238 - 10) << 16; + + this.imageMinimap.bind(); + + for (let z: number = 1; z < CollisionMap.SIZE - 1; z++) { + for (let x: number = 1; x < CollisionMap.SIZE - 1; x++) { + if (this.levelTileFlags && (this.levelTileFlags[level][x][z] & 0x18) === 0) { + this.drawMinimapLoc(x, z, level, wallRgb, doorRgb); + } + + if (level < 3 && this.levelTileFlags && (this.levelTileFlags[level + 1][x][z] & 0x8) !== 0) { + this.drawMinimapLoc(x, z, level + 1, wallRgb, doorRgb); + } + } + } + + this.areaViewport?.bind(); + this.activeMapFunctionCount = 0; + + for (let x: number = 0; x < CollisionMap.SIZE; x++) { + for (let z: number = 0; z < CollisionMap.SIZE; z++) { + let bitset: number = this.scene?.getGroundDecorationBitset(this.currentLevel, x, z) ?? 0; + if (bitset === 0) { + continue; + } + + bitset = (bitset >> 14) & 0x7fff; + + const func: number = LocType.get(bitset).mapfunction; + if (func < 0) { + continue; + } + + let stx: number = x; + let stz: number = z; + + if (func !== 22 && func !== 29 && func !== 34 && func !== 36 && func !== 46 && func !== 47 && func !== 48) { + const maxX: number = CollisionMap.SIZE; + const maxZ: number = CollisionMap.SIZE; + const collisionmap: CollisionMap | null = this.levelCollisionMap[this.currentLevel]; + if (collisionmap) { + const flags: Int32Array = collisionmap.flags; + + for (let i: number = 0; i < 10; i++) { + const rand: number = (Math.random() * 4.0) | 0; + if (rand === 0 && stx > 0 && stx > x - 3 && (flags[CollisionMap.index(stx - 1, stz)] & CollisionFlag.BLOCK_WEST) === CollisionFlag.OPEN) { + stx--; + } + + if (rand === 1 && stx < maxX - 1 && stx < x + 3 && (flags[CollisionMap.index(stx + 1, stz)] & CollisionFlag.BLOCK_EAST) === CollisionFlag.OPEN) { + stx++; + } + + if (rand === 2 && stz > 0 && stz > z - 3 && (flags[CollisionMap.index(stx, stz - 1)] & CollisionFlag.BLOCK_SOUTH) === CollisionFlag.OPEN) { + stz--; + } + + if (rand === 3 && stz < maxZ - 1 && stz < z + 3 && (flags[CollisionMap.index(stx, stz + 1)] & CollisionFlag.BLOCK_NORTH) === CollisionFlag.OPEN) { + stz++; + } + } + } + } + + this.activeMapFunctions[this.activeMapFunctionCount] = this.imageMapfunction[func]; + this.activeMapFunctionX[this.activeMapFunctionCount] = stx; + this.activeMapFunctionZ[this.activeMapFunctionCount] = stz; + this.activeMapFunctionCount++; + } + } + }; + + private drawMinimapLoc = (tileX: number, tileZ: number, level: number, wallRgb: number, doorRgb: number): void => { + if (!this.scene || !this.imageMinimap) { + return; + } + let bitset: number = this.scene.getWallBitset(level, tileX, tileZ); + if (bitset !== 0) { + const info: number = this.scene.getInfo(level, tileX, tileZ, bitset); + const angle: number = (info >> 6) & 0x3; + const shape: number = info & 0x1f; + let rgb: number = wallRgb; + if (bitset > 0) { + rgb = doorRgb; + } + + const dst: Int32Array = this.imageMinimap.pixels; + const offset: number = tileX * 4 + (103 - tileZ) * 512 * 4 + 24624; + const locId: number = (bitset >> 14) & 0x7fff; + + const loc: LocType = LocType.get(locId); + if (loc.mapscene === -1) { + if (shape === LocShape.WALL_STRAIGHT || shape === LocShape.WALL_L) { + if (angle === LocAngle.WEST) { + dst[offset] = rgb; + dst[offset + 512] = rgb; + dst[offset + 1024] = rgb; + dst[offset + 1536] = rgb; + } else if (angle === LocAngle.NORTH) { + dst[offset] = rgb; + dst[offset + 1] = rgb; + dst[offset + 2] = rgb; + dst[offset + 3] = rgb; + } else if (angle === LocAngle.EAST) { + dst[offset + 3] = rgb; + dst[offset + 3 + 512] = rgb; + dst[offset + 3 + 1024] = rgb; + dst[offset + 3 + 1536] = rgb; + } else if (angle === LocAngle.SOUTH) { + dst[offset + 1536] = rgb; + dst[offset + 1536 + 1] = rgb; + dst[offset + 1536 + 2] = rgb; + dst[offset + 1536 + 3] = rgb; + } + } + + if (shape === LocShape.WALL_SQUARE_CORNER) { + if (angle === LocAngle.WEST) { + dst[offset] = rgb; + } else if (angle === LocAngle.NORTH) { + dst[offset + 3] = rgb; + } else if (angle === LocAngle.EAST) { + dst[offset + 3 + 1536] = rgb; + } else if (angle === LocAngle.SOUTH) { + dst[offset + 1536] = rgb; + } + } + + if (shape === LocShape.WALL_L) { + if (angle === LocAngle.SOUTH) { + dst[offset] = rgb; + dst[offset + 512] = rgb; + dst[offset + 1024] = rgb; + dst[offset + 1536] = rgb; + } else if (angle === LocAngle.WEST) { + dst[offset] = rgb; + dst[offset + 1] = rgb; + dst[offset + 2] = rgb; + dst[offset + 3] = rgb; + } else if (angle === LocAngle.NORTH) { + dst[offset + 3] = rgb; + dst[offset + 3 + 512] = rgb; + dst[offset + 3 + 1024] = rgb; + dst[offset + 3 + 1536] = rgb; + } else if (angle === LocAngle.EAST) { + dst[offset + 1536] = rgb; + dst[offset + 1536 + 1] = rgb; + dst[offset + 1536 + 2] = rgb; + dst[offset + 1536 + 3] = rgb; + } + } + } else { + const scene: Pix8 | null = this.imageMapscene[loc.mapscene]; + if (scene) { + const offsetX: number = ((loc.width * 4 - scene.width) / 2) | 0; + const offsetY: number = ((loc.length * 4 - scene.height) / 2) | 0; + scene.draw(tileX * 4 + 48 + offsetX, (CollisionMap.SIZE - tileZ - loc.length) * 4 + offsetY + 48); + } + } + } + + bitset = this.scene.getLocBitset(level, tileX, tileZ); + if (bitset !== 0) { + const info: number = this.scene.getInfo(level, tileX, tileZ, bitset); + const angle: number = (info >> 6) & 0x3; + const shape: number = info & 0x1f; + const locId: number = (bitset >> 14) & 0x7fff; + const loc: LocType = LocType.get(locId); + + if (loc.mapscene !== -1) { + const scene: Pix8 | null = this.imageMapscene[loc.mapscene]; + if (scene) { + const offsetX: number = ((loc.width * 4 - scene.width) / 2) | 0; + const offsetY: number = ((loc.length * 4 - scene.height) / 2) | 0; + scene.draw(tileX * 4 + 48 + offsetX, (CollisionMap.SIZE - tileZ - loc.length) * 4 + offsetY + 48); + } + } else if (shape === LocShape.WALL_DIAGONAL) { + let rgb: number = 0xeeeeee; + if (bitset > 0) { + rgb = 0xee0000; + } + + const dst: Int32Array = this.imageMinimap.pixels; + const offset: number = tileX * 4 + (CollisionMap.SIZE - 1 - tileZ) * 512 * 4 + 24624; + + if (angle === LocAngle.WEST || angle === LocAngle.EAST) { + dst[offset + 1536] = rgb; + dst[offset + 1024 + 1] = rgb; + dst[offset + 512 + 2] = rgb; + dst[offset + 3] = rgb; + } else { + dst[offset] = rgb; + dst[offset + 512 + 1] = rgb; + dst[offset + 1024 + 2] = rgb; + dst[offset + 1536 + 3] = rgb; + } + } + } + + bitset = this.scene.getGroundDecorationBitset(level, tileX, tileZ); + if (bitset !== 0) { + const loc: LocType = LocType.get((bitset >> 14) & 0x7fff); + if (loc.mapscene !== -1) { + const scene: Pix8 | null = this.imageMapscene[loc.mapscene]; + if (scene) { + const offsetX: number = ((loc.width * 4 - scene.width) / 2) | 0; + const offsetY: number = ((loc.length * 4 - scene.height) / 2) | 0; + scene.draw(tileX * 4 + 48 + offsetX, (CollisionMap.SIZE - tileZ - loc.length) * 4 + offsetY + 48); + } + } + } + }; + + private drawTooltip = (): void => { + if (this.menuSize < 2 && this.objSelected === 0 && this.spellSelected === 0) { + return; + } + + let tooltip: string; + if (this.objSelected === 1 && this.menuSize < 2) { + tooltip = 'Use ' + this.objSelectedName + ' with...'; + } else if (this.spellSelected === 1 && this.menuSize < 2) { + tooltip = this.spellCaption + '...'; + } else { + tooltip = this.menuOption[this.menuSize - 1]; + } + + if (this.menuSize > 2) { + tooltip = tooltip + '@whi@ / ' + (this.menuSize - 2) + ' more options'; + } + + this.fontBold12?.drawStringTooltip(4, 15, tooltip, Colors.WHITE, true, (this.loopCycle / 1000) | 0); + }; + + private drawMenu = (): void => { + const x: number = this.menuX; + const y: number = this.menuY; + const w: number = this.menuWidth; + const h: number = this.menuHeight; + const background: number = Colors.OPTIONS_MENU; + + // the menu area square. + Draw2D.fillRect(x, y, w, h, background); + Draw2D.fillRect(x + 1, y + 1, w - 2, 16, Colors.BLACK); + Draw2D.drawRect(x + 1, y + 18, w - 2, h - 19, Colors.BLACK); + + // the menu title header at the top. + this.fontBold12?.drawString(x + 3, y + 14, 'Choose Option', background); + let mouseX: number = this.mouseX; + let mouseY: number = this.mouseY; + if (this.menuArea === 0) { + mouseX -= 8; + mouseY -= 11; + } + if (this.menuArea === 1) { + mouseX -= 562; + mouseY -= 231; + } + if (this.menuArea === 2) { + mouseX -= 22; + mouseY -= 375; + } + + for (let i: number = 0; i < this.menuSize; i++) { + const optionY: number = y + (this.menuSize - 1 - i) * 15 + 31; + let rgb: number = Colors.WHITE; + if (mouseX > x && mouseX < x + w && mouseY > optionY - 13 && mouseY < optionY + 3) { + rgb = Colors.YELLOW; + } + this.fontBold12?.drawStringTaggable(x + 3, optionY, this.menuOption[i], rgb, true); + } + }; + + private handleMouseInput = async (): Promise => { + if (this.objDragArea !== 0) { + return; + } + + let button: number = this.mouseClickButton; + if (this.spellSelected === 1 && this.mouseClickX >= 520 && this.mouseClickY >= 165 && this.mouseClickX <= 788 && this.mouseClickY <= 230) { + button = 0; + } + + if (this.menuVisible) { + if (button !== 1) { + let x: number = this.mouseX; + let y: number = this.mouseY; + + if (this.menuArea === 0) { + x -= 8; + y -= 11; + } else if (this.menuArea === 1) { + x -= 562; + y -= 231; + } else if (this.menuArea === 2) { + x -= 22; + y -= 375; + } + + if (x < this.menuX - 10 || x > this.menuX + this.menuWidth + 10 || y < this.menuY - 10 || y > this.menuY + this.menuHeight + 10) { + this.menuVisible = false; + if (this.menuArea === 1) { + this.redrawSidebar = true; + } + if (this.menuArea === 2) { + this.redrawChatback = true; + } + } + } + + if (button === 1) { + const menuX: number = this.menuX; + const menuY: number = this.menuY; + const menuWidth: number = this.menuWidth; + + let clickX: number = this.mouseClickX; + let clickY: number = this.mouseClickY; + + if (this.menuArea === 0) { + clickX -= 8; + clickY -= 11; + } else if (this.menuArea === 1) { + clickX -= 562; + clickY -= 231; + } else if (this.menuArea === 2) { + clickX -= 22; + clickY -= 375; + } + + let option: number = -1; + for (let i: number = 0; i < this.menuSize; i++) { + const optionY: number = menuY + (this.menuSize - 1 - i) * 15 + 31; + if (clickX > menuX && clickX < menuX + menuWidth && clickY > optionY - 13 && clickY < optionY + 3) { + option = i; + } + } + + if (option !== -1) { + await this.useMenuOption(option); + } + + this.menuVisible = false; + if (this.menuArea === 1) { + this.redrawSidebar = true; + } else if (this.menuArea === 2) { + this.redrawChatback = true; + } + } + } else { + if (button === 1 && this.menuSize > 0) { + const action: number = this.menuAction[this.menuSize - 1]; + + if (action === 602 || action === 596 || action === 22 || action === 892 || action === 415 || action === 405 || action === 38 || action === 422 || action === 478 || action === 347 || action === 188) { + const slot: number = this.menuParamB[this.menuSize - 1]; + const comId: number = this.menuParamC[this.menuSize - 1]; + const com: ComType = ComType.instances[comId]; + + if (com.draggable) { + this.objGrabThreshold = false; + this.objDragCycles = 0; + this.objDragInterfaceId = comId; + this.objDragSlot = slot; + this.objDragArea = 2; + this.objGrabX = this.mouseClickX; + this.objGrabY = this.mouseClickY; + + if (ComType.instances[comId].layer === this.viewportInterfaceId) { + this.objDragArea = 1; + } + + if (ComType.instances[comId].layer === this.chatInterfaceId) { + this.objDragArea = 3; + } + + return; + } + } + } + + if (button === 1 && (this.mouseButtonsOption === 1 || this.isAddFriendOption(this.menuSize - 1)) && this.menuSize > 2) { + button = 2; + } + + if (button === 1 && this.menuSize > 0) { + await this.useMenuOption(this.menuSize - 1); + } + + if (button !== 2 || this.menuSize <= 0) { + return; + } + + this.showContextMenu(); + } + }; + + handleMinimapInput = (): void => { + if (this.mouseClickButton === 1 && this.localPlayer) { + let x: number = this.mouseClickX - 21 - 561; + let y: number = this.mouseClickY - 9 - 5; + + if (x >= 0 && y >= 0 && x < 146 && y < 151) { + x -= 73; + y -= 75; + + const yaw: number = (this.orbitCameraYaw + this.minimapAnticheatAngle) & 0x7ff; + let sinYaw: number = Draw3D.sin[yaw]; + let cosYaw: number = Draw3D.cos[yaw]; + + sinYaw = (sinYaw * (this.minimapZoom + 256)) >> 8; + cosYaw = (cosYaw * (this.minimapZoom + 256)) >> 8; + + const relX: number = (y * sinYaw + x * cosYaw) >> 11; + const relY: number = (y * cosYaw - x * sinYaw) >> 11; + + const tileX: number = (this.localPlayer.x + relX) >> 7; + const tileZ: number = (this.localPlayer.z - relY) >> 7; + + if (this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], tileX, tileZ, 1, 0, 0, 0, 0, 0, true)) { + // the additional 14-bytes in MOVE_MINIMAPCLICK + this.out.p1(x); + this.out.p1(y); + this.out.p2(this.orbitCameraYaw); + this.out.p1(57); + this.out.p1(this.minimapAnticheatAngle); + this.out.p1(this.minimapZoom); + this.out.p1(89); + this.out.p2(this.localPlayer.x); + this.out.p2(this.localPlayer.z); + this.out.p1(this.tryMoveNearest); + this.out.p1(63); + } + } + } + }; + + private isAddFriendOption = (option: number): boolean => { + if (option < 0) { + return false; + } + let action: number = this.menuAction[option]; + if (action >= 2000) { + action -= 2000; + } + return action === 406; + }; + + private useMenuOption = async (optionId: number): Promise => { + if (optionId < 0) { + return; + } + + if (this.chatbackInputOpen) { + this.chatbackInputOpen = false; + this.redrawChatback = true; + } + + let action: number = this.menuAction[optionId]; + const a: number = this.menuParamA[optionId]; + const b: number = this.menuParamB[optionId]; + const c: number = this.menuParamC[optionId]; + + if (action >= 2000) { + action -= 2000; + } + + if (action === 903 || action === 363) { + let option: string = this.menuOption[optionId]; + const tag: number = option.indexOf('@whi@'); + + if (tag !== -1) { + option = option.substring(tag + 5).trim(); + const name: string = JString.formatName(JString.fromBase37(JString.toBase37(option))); + let found: boolean = false; + + for (let i: number = 0; i < this.playerCount; i++) { + const player: PlayerEntity | null = this.players[this.playerIds[i]]; + + if (player && player.name && player.name.toLowerCase() === name.toLowerCase() && this.localPlayer) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], player.pathTileX[0], player.pathTileZ[0], 2, 1, 1, 0, 0, 0, false); + + if (action === 903) { + // OPPLAYER4 + this.out.p1isaac(ClientProt.OPPLAYER4); + } else if (action === 363) { + // OPPLAYER1 + this.out.p1isaac(ClientProt.OPPLAYER1); + } + + this.out.p2(this.playerIds[i]); + found = true; + break; + } + } + + if (!found) { + this.addMessage(0, 'Unable to find ' + name, ''); + } + } + } else if (action === 450 && this.interactWithLoc(ClientProt.OPLOCU, b, c, a)) { + // OPLOCU + this.out.p2(this.objInterface); + this.out.p2(this.objSelectedSlot); + this.out.p2(this.objSelectedInterface); + } else if (action === 405 || action === 38 || action === 422 || action === 478 || action === 347) { + if (action === 478) { + if ((b & 0x3) === 0) { + Client.opHeld1Counter++; + } + + if (Client.opHeld1Counter >= 90) { + // ANTICHEAT_OPLOGIC5 + this.out.p1isaac(ClientProt.ANTICHEAT_OPLOGIC5); + } + + // OPHELD4 + this.out.p1isaac(ClientProt.OPHELD4); + } else if (action === 347) { + // OPHELD5 + this.out.p1isaac(ClientProt.OPHELD5); + } else if (action === 422) { + // OPHELD3 + this.out.p1isaac(ClientProt.OPHELD3); + } else if (action === 405) { + Client.opHeld4Counter += a; + if (Client.opHeld4Counter >= 97) { + // ANTICHEAT_OPLOGIC3 + this.out.p1isaac(ClientProt.ANTICHEAT_OPLOGIC3); + this.out.p3(14953816); + } + + // OPHELD1 + this.out.p1isaac(ClientProt.OPHELD1); + } else if (action === 38) { + // OPHELD2 + this.out.p1isaac(ClientProt.OPHELD2); + } + + this.out.p2(a); + this.out.p2(b); + this.out.p2(c); + this.selectedCycle = 0; + this.selectedInterface = c; + this.selectedItem = b; + this.selectedArea = 2; + + if (ComType.instances[c].layer === this.viewportInterfaceId) { + this.selectedArea = 1; + } + + if (ComType.instances[c].layer === this.chatInterfaceId) { + this.selectedArea = 3; + } + } else if (action === 728 || action === 542 || action === 6 || action === 963 || action === 245) { + const npc: NpcEntity | null = this.npcs[a]; + if (npc && this.localPlayer) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], npc.pathTileX[0], npc.pathTileZ[0], 2, 1, 1, 0, 0, 0, false); + + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + + if (action === 542) { + // OPNPC2 + this.out.p1isaac(ClientProt.OPNPC2); + } else if (action === 6) { + if ((a & 0x3) === 0) { + Client.opNpc3Counter++; + } + + if (Client.opNpc3Counter >= 124) { + // ANTICHEAT_OPLOGIC2 + this.out.p1isaac(ClientProt.ANTICHEAT_OPLOGIC2); + this.out.p4(0); + } + + // OPNPC3 + this.out.p1isaac(ClientProt.OPNPC3); + } else if (action === 963) { + // OPNPC4 + this.out.p1isaac(ClientProt.OPNPC4); + } else if (action === 728) { + // OPNPC1 + this.out.p1isaac(ClientProt.OPNPC1); + } else if (action === 245) { + if ((a & 0x3) === 0) { + Client.opNpc5Counter++; + } + + if (Client.opNpc5Counter >= 85) { + // ANTICHEAT_OPLOGIC4 + this.out.p1isaac(ClientProt.ANTICHEAT_OPLOGIC4); + this.out.p2(39596); + } + + // OPNPC5 + this.out.p1isaac(ClientProt.OPNPC5); + } + + this.out.p2(a); + } + } else if (action === 217) { + if (this.localPlayer) { + const success: boolean = this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], b, c, 2, 0, 0, 0, 0, 0, false); + if (!success) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], b, c, 2, 1, 1, 0, 0, 0, false); + } + + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + + // OPOBJU + this.out.p1isaac(ClientProt.OPOBJU); + this.out.p2(b + this.sceneBaseTileX); + this.out.p2(c + this.sceneBaseTileZ); + this.out.p2(a); + this.out.p2(this.objInterface); + this.out.p2(this.objSelectedSlot); + this.out.p2(this.objSelectedInterface); + } + } else if (action === 1175) { + // loc examine + const locId: number = (a >> 14) & 0x7fff; + const loc: LocType = LocType.get(locId); + + let examine: string; + if (!loc.desc) { + examine = "It's a " + loc.name + '.'; + } else { + examine = loc.desc; + } + + this.addMessage(0, examine, ''); + } else if (action === 285) { + // OPLOC1 + this.interactWithLoc(ClientProt.OPLOC1, b, c, a); + } else if (action === 881) { + // OPHELDU + this.out.p1isaac(ClientProt.OPHELDU); + this.out.p2(a); + this.out.p2(b); + this.out.p2(c); + this.out.p2(this.objInterface); + this.out.p2(this.objSelectedSlot); + this.out.p2(this.objSelectedInterface); + + this.selectedCycle = 0; + this.selectedInterface = c; + this.selectedItem = b; + this.selectedArea = 2; + + if (ComType.instances[c].layer === this.viewportInterfaceId) { + this.selectedArea = 1; + } + + if (ComType.instances[c].layer === this.chatInterfaceId) { + this.selectedArea = 3; + } + } else if (action === 391) { + // OPHELDT + this.out.p1isaac(ClientProt.OPHELDT); + this.out.p2(a); + this.out.p2(b); + this.out.p2(c); + this.out.p2(this.activeSpellId); + + this.selectedCycle = 0; + this.selectedInterface = c; + this.selectedItem = b; + this.selectedArea = 2; + + if (ComType.instances[c].layer === this.viewportInterfaceId) { + this.selectedArea = 1; + } + + if (ComType.instances[c].layer === this.chatInterfaceId) { + this.selectedArea = 3; + } + } else if (action === 660) { + if (this.menuVisible) { + this.scene?.click(b - 8, c - 11); + } else { + this.scene?.click(this.mouseClickX - 8, this.mouseClickY - 11); + } + } else if (action === 188) { + // select obj interface + this.objSelected = 1; + this.objSelectedSlot = b; + this.objSelectedInterface = c; + this.objInterface = a; + this.objSelectedName = ObjType.get(a).name; + this.spellSelected = 0; + return; + } else if (action === 44) { + // RESUME_PAUSEBUTTON + if (!this.pressedContinueOption) { + this.out.p1isaac(ClientProt.RESUME_PAUSEBUTTON); + this.out.p2(c); + this.pressedContinueOption = true; + } + } else if (action === 1773) { + // loc examine + const obj: ObjType = ObjType.get(a); + let examine: string; + + if (c >= 100000) { + examine = c + ' x ' + obj.name; + } else if (!obj.desc) { + examine = "It's a " + obj.name + '.'; + } else { + examine = obj.desc; + } + this.addMessage(0, examine, ''); + } else if (action === 900) { + const npc: NpcEntity | null = this.npcs[a]; + + if (npc && this.localPlayer) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], npc.pathTileX[0], npc.pathTileZ[0], 2, 1, 1, 0, 0, 0, false); + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + // OPNPCU + this.out.p1isaac(ClientProt.OPNPCU); + this.out.p2(a); + this.out.p2(this.objInterface); + this.out.p2(this.objSelectedSlot); + this.out.p2(this.objSelectedInterface); + } + } else if (action === 1373 || action === 1544 || action === 151 || action === 1101) { + const player: PlayerEntity | null = this.players[a]; + if (player && this.localPlayer) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], player.pathTileX[0], player.pathTileZ[0], 2, 1, 1, 0, 0, 0, false); + + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + + if (action === 1101) { + // OPPLAYER1 + this.out.p1isaac(ClientProt.OPPLAYER1); + } else if (action === 151) { + Client.opPlayer2Counter++; + if (Client.opPlayer2Counter >= 90) { + // ANTICHEAT_OPLOGIC8 + this.out.p1isaac(ClientProt.ANTICHEAT_OPLOGIC8); + this.out.p2(31114); + } + + // OPPLAYER2 + this.out.p1isaac(ClientProt.OPPLAYER2); + } else if (action === 1373) { + // OPPLAYER4 + this.out.p1isaac(ClientProt.OPPLAYER4); + } else if (action === 1544) { + // OPPLAYER3 + this.out.p1isaac(ClientProt.OPPLAYER3); + } + + this.out.p2(a); + } + } else if (action === 265) { + const npc: NpcEntity | null = this.npcs[a]; + if (npc && this.localPlayer) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], npc.pathTileX[0], npc.pathTileZ[0], 2, 1, 1, 0, 0, 0, false); + + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + + // OPNPCT + this.out.p1isaac(ClientProt.OPNPCT); + this.out.p2(a); + this.out.p2(this.activeSpellId); + } + } else if (action === 679) { + const option: string = this.menuOption[optionId]; + const tag: number = option.indexOf('@whi@'); + + if (tag !== -1) { + const name37: bigint = JString.toBase37(option.substring(tag + 5).trim()); + let friend: number = -1; + for (let i: number = 0; i < this.friendCount; i++) { + if (this.friendName37[i] === name37) { + friend = i; + break; + } + } + + if (friend !== -1 && this.friendWorld[friend] > 0) { + this.redrawChatback = true; + this.chatbackInputOpen = false; + this.showSocialInput = true; + this.socialInput = ''; + this.socialAction = 3; + this.socialName37 = this.friendName37[friend]; + this.socialMessage = 'Enter message to send to ' + this.friendName[friend]; + } + } + } else if (action === 55) { + // OPLOCT + if (this.interactWithLoc(ClientProt.OPLOCT, b, c, a)) { + this.out.p2(this.activeSpellId); + } + } else if (action === 224 || action === 993 || action === 99 || action === 746 || action === 877) { + if (this.localPlayer) { + const success: boolean = this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], b, c, 2, 0, 0, 0, 0, 0, false); + if (!success) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], b, c, 2, 1, 1, 0, 0, 0, false); + } + + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + + if (action === 224) { + // OPOBJ1 + this.out.p1isaac(ClientProt.OPOBJ1); + } else if (action === 746) { + // OPOBJ4 + this.out.p1isaac(ClientProt.OPOBJ4); + } else if (action === 877) { + // OPOBJ5 + this.out.p1isaac(ClientProt.OPOBJ5); + } else if (action === 99) { + // OPOBJ3 + this.out.p1isaac(ClientProt.OPOBJ3); + } else if (action === 993) { + // OPOBJ2 + this.out.p1isaac(ClientProt.OPOBJ2); + } + + this.out.p2(b + this.sceneBaseTileX); + this.out.p2(c + this.sceneBaseTileZ); + this.out.p2(a); + } + } else if (action === 1607) { + // npc examine + const npc: NpcEntity | null = this.npcs[a]; + if (npc && npc.type) { + let examine: string; + + if (!npc.type.desc) { + examine = "It's a " + npc.type.name + '.'; + } else { + examine = npc.type.desc; + } + + this.addMessage(0, examine, ''); + } + } else if (action === 504) { + // OPLOC2 + this.interactWithLoc(ClientProt.OPLOC2, b, c, a); + } else if (action === 930) { + const com: ComType = ComType.instances[c]; + this.spellSelected = 1; + this.activeSpellId = c; + this.activeSpellFlags = com.actionTarget; + this.objSelected = 0; + + let prefix: string | null = com.actionVerb; + if (prefix && prefix.indexOf(' ') !== -1) { + prefix = prefix.substring(0, prefix.indexOf(' ')); + } + + let suffix: string | null = com.actionVerb; + if (suffix && suffix.indexOf(' ') !== -1) { + suffix = suffix.substring(suffix.indexOf(' ') + 1); + } + + this.spellCaption = prefix + ' ' + com.action + ' ' + suffix; + if (this.activeSpellFlags === 16) { + this.redrawSidebar = true; + this.selectedTab = 3; + this.redrawSideicons = true; + } + + return; + } else if (action === 951) { + const com: ComType = ComType.instances[c]; + let notify: boolean = true; + + if (com.clientCode > 0) { + notify = this.handleInterfaceAction(com); + } + + if (notify) { + // IF_BUTTON + this.out.p1isaac(ClientProt.IF_BUTTON); + this.out.p2(c); + } + } else if (action === 602 || action === 596 || action === 22 || action === 892 || action === 415) { + if (action === 22) { + // INV_BUTTON3 + this.out.p1isaac(ClientProt.INV_BUTTON3); + } else if (action === 415) { + if ((c & 0x3) === 0) { + Client.ifButton5Counter++; + } + + if (Client.ifButton5Counter >= 55) { + // ANTICHEAT_OPLOGIC7 + this.out.p1isaac(ClientProt.ANTICHEAT_OPLOGIC7); + this.out.p4(0); + } + + // INV_BUTTON5 + this.out.p1isaac(ClientProt.INV_BUTTON5); + } else if (action === 602) { + // INV_BUTTON1 + this.out.p1isaac(ClientProt.INV_BUTTON1); + } else if (action === 892) { + if ((b & 0x3) === 0) { + Client.opHeld9Counter++; + } + + if (Client.opHeld9Counter >= 130) { + // ANTICHEAT_OPLOGIC9 + this.out.p1isaac(ClientProt.ANTICHEAT_OPLOGIC9); + this.out.p1(177); + } + + // INV_BUTTON4 + this.out.p1isaac(ClientProt.INV_BUTTON4); + } else if (action === 596) { + // INV_BUTTON2 + this.out.p1isaac(ClientProt.INV_BUTTON2); + } + + this.out.p2(a); + this.out.p2(b); + this.out.p2(c); + + this.selectedCycle = 0; + this.selectedInterface = c; + this.selectedItem = b; + this.selectedArea = 2; + + if (ComType.instances[c].layer === this.viewportInterfaceId) { + this.selectedArea = 1; + } + + if (ComType.instances[c].layer === this.chatInterfaceId) { + this.selectedArea = 3; + } + } else if (action === 581) { + if ((a & 0x3) === 0) { + Client.opLoc4Counter++; + } + + if (Client.opLoc4Counter >= 99) { + // ANTICHEAT_OPLOGIC1 + this.out.p1isaac(ClientProt.ANTICHEAT_OPLOGIC1); + this.out.p4(0); + } + + // OPLOC4 + this.interactWithLoc(ClientProt.OPLOC4, b, c, a); + } else if (action === 965) { + if (this.localPlayer) { + const success: boolean = this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], b, c, 2, 0, 0, 0, 0, 0, false); + if (!success) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], b, c, 2, 1, 1, 0, 0, 0, false); + } + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + + // OPOBJT + this.out.p1isaac(ClientProt.OPOBJT); + this.out.p2(b + this.sceneBaseTileX); + this.out.p2(c + this.sceneBaseTileZ); + this.out.p2(a); + this.out.p2(this.activeSpellId); + } + } else if (action === 1501) { + Client.opLoc5Counter += this.sceneBaseTileZ; + if (Client.opLoc5Counter >= 92) { + // ANTICHEAT_OPLOGIC6 + this.out.p1isaac(ClientProt.ANTICHEAT_OPLOGIC6); + this.out.p4(0); + } + + // OPLOC5 + this.interactWithLoc(ClientProt.OPLOC5, b, c, a); + } else if (action === 364) { + // OPLOC3 + this.interactWithLoc(ClientProt.OPLOC3, b, c, a); + } else if (action === 1102) { + // obj examine + const obj: ObjType = ObjType.get(a); + let examine: string; + + if (!obj.desc) { + examine = "It's a " + obj.name + '.'; + } else { + examine = obj.desc; + } + this.addMessage(0, examine, ''); + } else if (action === 960) { + // IF_BUTTON + this.out.p1isaac(ClientProt.IF_BUTTON); + this.out.p2(c); + + const com: ComType = ComType.instances[c]; + if (com.scripts && com.scripts[0] && com.scripts[0][0] === 5) { + const varp: number = com.scripts[0][1]; + if (com.scriptOperand && this.varps[varp] !== com.scriptOperand[0]) { + this.varps[varp] = com.scriptOperand[0]; + await this.updateVarp(varp); + this.redrawSidebar = true; + } + } + } else if (action === 34) { + // reportabuse input + const option: string = this.menuOption[optionId]; + const tag: number = option.indexOf('@whi@'); + + if (tag !== -1) { + this.closeInterfaces(); + + this.reportAbuseInput = option.substring(tag + 5).trim(); + this.reportAbuseMuteOption = false; + + for (let i: number = 0; i < ComType.instances.length; i++) { + if (ComType.instances[i] && ComType.instances[i].clientCode === ComType.CC_REPORT_INPUT) { + this.reportAbuseInterfaceID = this.viewportInterfaceId = ComType.instances[i].layer; + break; + } + } + } + } else if (action === 947) { + // close interfaces + this.closeInterfaces(); + } else if (action === 367) { + const player: PlayerEntity | null = this.players[a]; + if (player && this.localPlayer) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], player.pathTileX[0], player.pathTileZ[0], 2, 1, 1, 0, 0, 0, false); + + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + + // OPPLAYERU + this.out.p1isaac(ClientProt.OPPLAYERU); + this.out.p2(a); + this.out.p2(this.objInterface); + this.out.p2(this.objSelectedSlot); + this.out.p2(this.objSelectedInterface); + } + } else if (action === 465) { + // IF_BUTTON + this.out.p1isaac(ClientProt.IF_BUTTON); + this.out.p2(c); + + const com: ComType = ComType.instances[c]; + if (com.scripts && com.scripts[0] && com.scripts[0][0] === 5) { + const varp: number = com.scripts[0][1]; + this.varps[varp] = 1 - this.varps[varp]; + await this.updateVarp(varp); + this.redrawSidebar = true; + } + } else if (action === 406 || action === 436 || action === 557 || action === 556) { + const option: string = this.menuOption[optionId]; + const tag: number = option.indexOf('@whi@'); + + if (tag !== -1) { + const username: bigint = JString.toBase37(option.substring(tag + 5).trim()); + if (action === 406) { + this.addFriend(username); + } else if (action === 436) { + this.addIgnore(username); + } else if (action === 557) { + this.removeFriend(username); + } else if (action === 556) { + this.removeIgnore(username); + } + } + } else if (action === 651) { + const player: PlayerEntity | null = this.players[a]; + + if (player && this.localPlayer) { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], player.pathTileX[0], player.pathTileZ[0], 2, 1, 1, 0, 0, 0, false); + + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + + // OPPLAYERT + this.out.p1isaac(ClientProt.OPPLAYERT); + this.out.p2(a); + this.out.p2(this.activeSpellId); + } + } + + this.objSelected = 0; + this.spellSelected = 0; + }; + + private handleInterfaceAction = (com: ComType): boolean => { + const clientCode: number = com.clientCode; + if (clientCode === ComType.CC_ADD_FRIEND) { + this.redrawChatback = true; + this.chatbackInputOpen = false; + this.showSocialInput = true; + this.socialInput = ''; + this.socialAction = 1; + this.socialMessage = 'Enter name of friend to add to list'; + } + + if (clientCode === ComType.CC_DEL_FRIEND) { + this.redrawChatback = true; + this.chatbackInputOpen = false; + this.showSocialInput = true; + this.socialInput = ''; + this.socialAction = 2; + this.socialMessage = 'Enter name of friend to delete from list'; + } + + if (clientCode === ComType.CC_LOGOUT) { + this.idleTimeout = 250; + return true; + } + + if (clientCode === ComType.CC_ADD_IGNORE) { + this.redrawChatback = true; + this.chatbackInputOpen = false; + this.showSocialInput = true; + this.socialInput = ''; + this.socialAction = 4; + this.socialMessage = 'Enter name of player to add to list'; + } + + if (clientCode === ComType.CC_DEL_IGNORE) { + this.redrawChatback = true; + this.chatbackInputOpen = false; + this.showSocialInput = true; + this.socialInput = ''; + this.socialAction = 5; + this.socialMessage = 'Enter name of player to delete from list'; + } + + // physical parts + if (clientCode >= ComType.CC_CHANGE_HEAD_L && clientCode <= ComType.CC_CHANGE_FEET_R) { + const part: number = ((clientCode - 300) / 2) | 0; + const direction: number = clientCode & 0x1; + let kit: number = this.designIdentikits[part]; + + if (kit !== -1) { + // eslint-disable-next-line no-constant-condition + while (true) { + if (direction === 0) { + kit--; + if (kit < 0) { + kit = IdkType.count - 1; + } + } + + if (direction === 1) { + kit++; + if (kit >= IdkType.count) { + kit = 0; + } + } + + if (!IdkType.instances[kit].disable && IdkType.instances[kit].type === part + (this.designGenderMale ? 0 : 7)) { + this.designIdentikits[part] = kit; + this.updateDesignModel = true; + break; + } + } + } + } + + // recoloring parts + if (clientCode >= ComType.CC_RECOLOUR_HAIR_L && clientCode <= ComType.CC_RECOLOUR_SKIN_R) { + const part: number = ((clientCode - 314) / 2) | 0; + const direction: number = clientCode & 0x1; + let color: number = this.designColors[part]; + + if (direction === 0) { + color--; + if (color < 0) { + color = PlayerEntity.DESIGN_BODY_COLOR[part].length - 1; + } + } + + if (direction === 1) { + color++; + if (color >= PlayerEntity.DESIGN_BODY_COLOR[part].length) { + color = 0; + } + } + + this.designColors[part] = color; + this.updateDesignModel = true; + } + + if (clientCode === ComType.CC_SWITCH_TO_MALE && !this.designGenderMale) { + this.designGenderMale = true; + this.validateCharacterDesign(); + } + + if (clientCode === ComType.CC_SWITCH_TO_FEMALE && this.designGenderMale) { + this.designGenderMale = false; + this.validateCharacterDesign(); + } + + if (clientCode === ComType.CC_ACCEPT_DESIGN) { + this.out.p1isaac(ClientProt.IF_PLAYERDESIGN); + this.out.p1(this.designGenderMale ? 0 : 1); + for (let i: number = 0; i < 7; i++) { + this.out.p1(this.designIdentikits[i]); + } + for (let i: number = 0; i < 5; i++) { + this.out.p1(this.designColors[i]); + } + return true; + } + + if (clientCode === ComType.CC_MOD_MUTE) { + this.reportAbuseMuteOption = !this.reportAbuseMuteOption; + } + + // reportabuse rules options + if (clientCode >= ComType.CC_REPORT_RULE1 && clientCode <= ComType.CC_REPORT_RULE12) { + this.closeInterfaces(); + + if (this.reportAbuseInput.length > 0) { + this.out.p1isaac(ClientProt.BUG_REPORT); + this.out.p8(JString.toBase37(this.reportAbuseInput)); + this.out.p1(clientCode - 601); + this.out.p1(this.reportAbuseMuteOption ? 1 : 0); + } + } + return false; + }; + + private validateCharacterDesign = (): void => { + this.updateDesignModel = true; + + for (let i: number = 0; i < 7; i++) { + this.designIdentikits[i] = -1; + + for (let j: number = 0; j < IdkType.count; j++) { + if (!IdkType.instances[j].disable && IdkType.instances[j].type === i + (this.designGenderMale ? 0 : 7)) { + this.designIdentikits[i] = j; + break; + } + } + } + }; + + private interactWithLoc = (opcode: number, x: number, z: number, bitset: number): boolean => { + if (!this.localPlayer || !this.scene) { + return false; + } + + const locId: number = (bitset >> 14) & 0x7fff; + const info: number = this.scene.getInfo(this.currentLevel, x, z, bitset); + if (info === -1) { + return false; + } + + const type: number = info & 0x1f; + const angle: number = (info >> 6) & 0x3; + if (type === LocShape.CENTREPIECE_STRAIGHT || type === LocShape.CENTREPIECE_DIAGONAL || type === LocShape.GROUND_DECOR) { + const loc: LocType = LocType.get(locId); + let width: number; + let height: number; + + if (angle === LocAngle.WEST || angle === LocAngle.EAST) { + width = loc.width; + height = loc.length; + } else { + width = loc.length; + height = loc.width; + } + + let forceapproach: number = loc.forceapproach; + if (angle !== 0) { + forceapproach = ((forceapproach << angle) & 0xf) + (forceapproach >> (4 - angle)); + } + + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], x, z, 2, width, height, 0, 0, forceapproach, false); + } else { + this.tryMove(this.localPlayer.pathTileX[0], this.localPlayer.pathTileZ[0], x, z, 2, 0, 0, angle, type + 1, 0, false); + } + + this.crossX = this.mouseClickX; + this.crossY = this.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + + this.out.p1isaac(opcode); + this.out.p2(x + this.sceneBaseTileX); + this.out.p2(z + this.sceneBaseTileZ); + this.out.p2(locId); + return true; + }; + + private handleTabInput = (): void => { + if (this.mouseClickButton === 1) { + if (this.mouseClickX >= 549 && this.mouseClickX <= 583 && this.mouseClickY >= 195 && this.mouseClickY < 231 && this.tabInterfaceId[0] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 0; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 579 && this.mouseClickX <= 609 && this.mouseClickY >= 194 && this.mouseClickY < 231 && this.tabInterfaceId[1] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 1; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 607 && this.mouseClickX <= 637 && this.mouseClickY >= 194 && this.mouseClickY < 231 && this.tabInterfaceId[2] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 2; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 635 && this.mouseClickX <= 679 && this.mouseClickY >= 194 && this.mouseClickY < 229 && this.tabInterfaceId[3] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 3; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 676 && this.mouseClickX <= 706 && this.mouseClickY >= 194 && this.mouseClickY < 231 && this.tabInterfaceId[4] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 4; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 704 && this.mouseClickX <= 734 && this.mouseClickY >= 194 && this.mouseClickY < 231 && this.tabInterfaceId[5] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 5; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 732 && this.mouseClickX <= 766 && this.mouseClickY >= 195 && this.mouseClickY < 231 && this.tabInterfaceId[6] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 6; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 550 && this.mouseClickX <= 584 && this.mouseClickY >= 492 && this.mouseClickY < 528 && this.tabInterfaceId[7] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 7; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 582 && this.mouseClickX <= 612 && this.mouseClickY >= 492 && this.mouseClickY < 529 && this.tabInterfaceId[8] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 8; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 609 && this.mouseClickX <= 639 && this.mouseClickY >= 492 && this.mouseClickY < 529 && this.tabInterfaceId[9] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 9; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 637 && this.mouseClickX <= 681 && this.mouseClickY >= 493 && this.mouseClickY < 528 && this.tabInterfaceId[10] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 10; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 679 && this.mouseClickX <= 709 && this.mouseClickY >= 492 && this.mouseClickY < 529 && this.tabInterfaceId[11] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 11; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 706 && this.mouseClickX <= 736 && this.mouseClickY >= 492 && this.mouseClickY < 529 && this.tabInterfaceId[12] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 12; + this.redrawSideicons = true; + } else if (this.mouseClickX >= 734 && this.mouseClickX <= 768 && this.mouseClickY >= 492 && this.mouseClickY < 528 && this.tabInterfaceId[13] !== -1) { + this.redrawSidebar = true; + this.selectedTab = 13; + this.redrawSideicons = true; + } + + Client.sidebarInputCounter++; + if (Client.sidebarInputCounter > 150) { + Client.sidebarInputCounter = 0; + this.out.p1isaac(ClientProt.ANTICHEAT_CYCLELOGIC1); + this.out.p1(43); + } + } + }; + + private handleInputKey = async (): Promise => { + // eslint-disable-next-line no-constant-condition + while (true) { + let key: number; + do { + // eslint-disable-next-line no-constant-condition + while (true) { + key = this.pollKey(); + if (key === -1) { + return; + } + + if (this.viewportInterfaceId !== -1 && this.viewportInterfaceId === this.reportAbuseInterfaceID) { + if (key === 8 && this.reportAbuseInput.length > 0) { + this.reportAbuseInput = this.reportAbuseInput.substring(0, this.reportAbuseInput.length - 1); + } + break; + } + + if (this.showSocialInput) { + if (key >= 32 && key <= 122 && this.socialInput.length < 80) { + this.socialInput = this.socialInput + String.fromCharCode(key); + this.redrawChatback = true; + } + + if (key === 8 && this.socialInput.length > 0) { + this.socialInput = this.socialInput.substring(0, this.socialInput.length - 1); + this.redrawChatback = true; + } + + if (key === 13 || key === 10) { + this.showSocialInput = false; + this.redrawChatback = true; + + let username: bigint; + if (this.socialAction === 1) { + username = JString.toBase37(this.socialInput); + this.addFriend(username); + } + + if (this.socialAction === 2 && this.friendCount > 0) { + username = JString.toBase37(this.socialInput); + this.removeFriend(username); + } + + if (this.socialAction === 3 && this.socialInput.length > 0 && this.socialName37) { + // MESSAGE_PRIVATE + this.out.p1isaac(ClientProt.MESSAGE_PRIVATE); + this.out.p1(0); + const start: number = this.out.pos; + this.out.p8(this.socialName37); + WordPack.pack(this.out, this.socialInput); + this.out.psize1(this.out.pos - start); + this.socialInput = JString.toSentenceCase(this.socialInput); + this.socialInput = WordFilter.filter(this.socialInput); + this.addMessage(6, this.socialInput, JString.formatName(JString.fromBase37(this.socialName37))); + if (this.privateChatSetting === 2) { + this.privateChatSetting = 1; + this.redrawPrivacySettings = true; + // CHAT_SETMODE + this.out.p1isaac(ClientProt.CHAT_SETMODE); + this.out.p1(this.publicChatSetting); + this.out.p1(this.privateChatSetting); + this.out.p1(this.tradeChatSetting); + } + } + + if (this.socialAction === 4 && this.ignoreCount < 100) { + username = JString.toBase37(this.socialInput); + this.addIgnore(username); + } + + if (this.socialAction === 5 && this.ignoreCount > 0) { + username = JString.toBase37(this.socialInput); + this.removeIgnore(username); + } + } + } else if (this.chatbackInputOpen) { + if (key >= 48 && key <= 57 && this.chatbackInput.length < 10) { + this.chatbackInput = this.chatbackInput + String.fromCharCode(key); + this.redrawChatback = true; + } + + if (key === 8 && this.chatbackInput.length > 0) { + this.chatbackInput = this.chatbackInput.substring(0, this.chatbackInput.length - 1); + this.redrawChatback = true; + } + + if (key === 13 || key === 10) { + if (this.chatbackInput.length > 0) { + let value: number = 0; + try { + value = parseInt(this.chatbackInput, 10); + } catch (e) { + /* empty */ + } + // RESUME_P_COUNTDIALOG + this.out.p1isaac(ClientProt.RESUME_P_COUNTDIALOG); + this.out.p4(value); + } + this.chatbackInputOpen = false; + this.redrawChatback = true; + } + } else if (this.chatInterfaceId === -1) { + if (key >= 32 && key <= 122 && this.chatTyped.length < 80) { + this.chatTyped = this.chatTyped + String.fromCharCode(key); + this.redrawChatback = true; + } + + if (key === 8 && this.chatTyped.length > 0) { + this.chatTyped = this.chatTyped.substring(0, this.chatTyped.length - 1); + this.redrawChatback = true; + } + + if ((key === 13 || key === 10) && this.chatTyped.length > 0) { + // if (this.rights) { + if (this.chatTyped === '::clientdrop' /* && super.frame*/) { + await this.tryReconnect(); + } else if (this.chatTyped === '::noclip') { + for (let level: number = 0; level < CollisionMap.LEVELS; level++) { + for (let x: number = 1; x < CollisionMap.SIZE - 1; x++) { + for (let z: number = 1; z < CollisionMap.SIZE - 1; z++) { + const collisionMap: CollisionMap | null = this.levelCollisionMap[level]; + if (collisionMap) { + collisionMap.flags[CollisionMap.index(x, z)] = 0; + } + } + } + } + } else if (this.chatTyped === '::debug') { + Client.showDebug = !Client.showDebug; + } + + if (this.chatTyped.startsWith('::')) { + // CLIENT_CHEAT + this.out.p1isaac(ClientProt.CLIENT_CHEAT); + this.out.p1(this.chatTyped.length - 1); + this.out.pjstr(this.chatTyped.substring(2)); + } else { + let color: number = 0; + if (this.chatTyped.startsWith('yellow:')) { + color = 0; + this.chatTyped = this.chatTyped.substring(7); + } else if (this.chatTyped.startsWith('red:')) { + color = 1; + this.chatTyped = this.chatTyped.substring(4); + } else if (this.chatTyped.startsWith('green:')) { + color = 2; + this.chatTyped = this.chatTyped.substring(6); + } else if (this.chatTyped.startsWith('cyan:')) { + color = 3; + this.chatTyped = this.chatTyped.substring(5); + } else if (this.chatTyped.startsWith('purple:')) { + color = 4; + this.chatTyped = this.chatTyped.substring(7); + } else if (this.chatTyped.startsWith('white:')) { + color = 5; + this.chatTyped = this.chatTyped.substring(6); + } else if (this.chatTyped.startsWith('flash1:')) { + color = 6; + this.chatTyped = this.chatTyped.substring(7); + } else if (this.chatTyped.startsWith('flash2:')) { + color = 7; + this.chatTyped = this.chatTyped.substring(7); + } else if (this.chatTyped.startsWith('flash3:')) { + color = 8; + this.chatTyped = this.chatTyped.substring(7); + } else if (this.chatTyped.startsWith('glow1:')) { + color = 9; + this.chatTyped = this.chatTyped.substring(6); + } else if (this.chatTyped.startsWith('glow2:')) { + color = 10; + this.chatTyped = this.chatTyped.substring(6); + } else if (this.chatTyped.startsWith('glow3:')) { + color = 11; + this.chatTyped = this.chatTyped.substring(6); + } + + let effect: number = 0; + if (this.chatTyped.startsWith('wave:')) { + effect = 1; + this.chatTyped = this.chatTyped.substring(5); + } + if (this.chatTyped.startsWith('scroll:')) { + effect = 2; + this.chatTyped = this.chatTyped.substring(7); + } + + // MESSAGE_PUBLIC + this.out.p1isaac(ClientProt.MESSAGE_PUBLIC); + this.out.p1(0); + const start: number = this.out.pos; + this.out.p1(color); + this.out.p1(effect); + WordPack.pack(this.out, this.chatTyped); + this.out.psize1(this.out.pos - start); + + this.chatTyped = JString.toSentenceCase(this.chatTyped); + this.chatTyped = WordFilter.filter(this.chatTyped); + + if (this.localPlayer && this.localPlayer.name) { + this.localPlayer.chat = this.chatTyped; + this.localPlayer.chatColor = color; + this.localPlayer.chatStyle = effect; + this.localPlayer.chatTimer = 150; + this.addMessage(2, this.localPlayer.chat, this.localPlayer.name); + } + + if (this.publicChatSetting === 2) { + this.publicChatSetting = 3; + this.redrawPrivacySettings = true; + // CHAT_SETMODE + this.out.p1isaac(ClientProt.CHAT_SETMODE); + this.out.p1(this.publicChatSetting); + this.out.p1(this.privateChatSetting); + this.out.p1(this.tradeChatSetting); + } + } + + this.chatTyped = ''; + this.redrawChatback = true; + } + } + } + } while ((key < 97 || key > 122) && (key < 65 || key > 90) && (key < 48 || key > 57) && key !== 32); + + if (this.reportAbuseInput.length < 12) { + this.reportAbuseInput = this.reportAbuseInput + String.fromCharCode(key); + } + } + }; + + private handleChatSettingsInput = (): void => { + if (this.mouseClickButton === 1) { + if (this.mouseClickX >= 8 && this.mouseClickX <= 108 && this.mouseClickY >= 490 && this.mouseClickY <= 522) { + this.publicChatSetting = (this.publicChatSetting + 1) % 4; + this.redrawPrivacySettings = true; + this.redrawChatback = true; + + this.out.p1isaac(ClientProt.CHAT_SETMODE); + this.out.p1(this.publicChatSetting); + this.out.p1(this.privateChatSetting); + this.out.p1(this.tradeChatSetting); + } else if (this.mouseClickX >= 137 && this.mouseClickX <= 237 && this.mouseClickY >= 490 && this.mouseClickY <= 522) { + this.privateChatSetting = (this.privateChatSetting + 1) % 3; + this.redrawPrivacySettings = true; + this.redrawChatback = true; + + this.out.p1isaac(ClientProt.CHAT_SETMODE); + this.out.p1(this.publicChatSetting); + this.out.p1(this.privateChatSetting); + this.out.p1(this.tradeChatSetting); + } else if (this.mouseClickX >= 275 && this.mouseClickX <= 375 && this.mouseClickY >= 490 && this.mouseClickY <= 522) { + this.tradeChatSetting = (this.tradeChatSetting + 1) % 3; + this.redrawPrivacySettings = true; + this.redrawChatback = true; + + this.out.p1isaac(ClientProt.CHAT_SETMODE); + this.out.p1(this.publicChatSetting); + this.out.p1(this.privateChatSetting); + this.out.p1(this.tradeChatSetting); + } else if (this.mouseClickX >= 416 && this.mouseClickX <= 516 && this.mouseClickY >= 490 && this.mouseClickY <= 522) { + this.closeInterfaces(); + + this.reportAbuseInput = ''; + this.reportAbuseMuteOption = false; + + for (let i: number = 0; i < ComType.instances.length; i++) { + if (ComType.instances[i] && ComType.instances[i].clientCode === 600) { + this.reportAbuseInterfaceID = this.viewportInterfaceId = ComType.instances[i].layer; + return; + } + } + } + } + }; + + private handleScrollInput = (mouseX: number, mouseY: number, scrollableHeight: number, height: number, redraw: boolean, left: number, top: number, component: ComType): void => { + if (this.scrollGrabbed) { + this.scrollInputPadding = 32; + } else { + this.scrollInputPadding = 0; + } + + this.scrollGrabbed = false; + + if (mouseX >= left && mouseX < left + 16 && mouseY >= top && mouseY < top + 16) { + component.scrollPosition -= this.dragCycles * 4; + if (redraw) { + this.redrawSidebar = true; + } + } else if (mouseX >= left && mouseX < left + 16 && mouseY >= top + height - 16 && mouseY < top + height) { + component.scrollPosition += this.dragCycles * 4; + if (redraw) { + this.redrawSidebar = true; + } + } else if (mouseX >= left - this.scrollInputPadding && mouseX < left + this.scrollInputPadding + 16 && mouseY >= top + 16 && mouseY < top + height - 16 && this.dragCycles > 0) { + let gripSize: number = (((height - 32) * height) / scrollableHeight) | 0; + if (gripSize < 8) { + gripSize = 8; + } + const gripY: number = mouseY - top - ((gripSize / 2) | 0) - 16; + const maxY: number = height - gripSize - 32; + component.scrollPosition = (((scrollableHeight - height) * gripY) / maxY) | 0; + if (redraw) { + this.redrawSidebar = true; + } + this.scrollGrabbed = true; + } + }; + + private prepareGameScreen = (): void => { + if (!this.areaChatback) { + this.unloadTitle(); + this.drawArea = null; + this.imageTitle2 = null; + this.imageTitle3 = null; + this.imageTitle4 = null; + this.imageTitle0 = null; + this.imageTitle1 = null; + this.imageTitle5 = null; + this.imageTitle6 = null; + this.imageTitle7 = null; + this.imageTitle8 = null; + this.areaChatback = new PixMap(479, 96); + this.areaMapback = new PixMap(168, 160); + Draw2D.clear(); + this.imageMapback?.draw(0, 0); + this.areaSidebar = new PixMap(190, 261); + this.areaViewport = new PixMap(512, 334); + Draw2D.clear(); + this.areaBackbase1 = new PixMap(501, 61); + this.areaBackbase2 = new PixMap(288, 40); + this.areaBackhmid1 = new PixMap(269, 66); + this.redrawTitleBackground = true; + } + }; + + private isFriend = (username: string | null): boolean => { + if (!username) { + return false; + } + + for (let i: number = 0; i < this.friendCount; i++) { + if (username.toLowerCase() === this.friendName[i]?.toLowerCase()) { + return true; + } + } + + if (!this.localPlayer) { + return false; + } + + return username.toLowerCase() === this.localPlayer.name?.toLowerCase(); + }; + + private addFriend = (username: bigint): void => { + if (username === 0n) { + return; + } + + if (this.friendCount >= 100) { + this.addMessage(0, 'Your friends list is full. Max of 100 hit', ''); + return; + } + + const displayName: string = JString.formatName(JString.fromBase37(username)); + for (let i: number = 0; i < this.friendCount; i++) { + if (this.friendName37[i] === username) { + this.addMessage(0, displayName + ' is already on your friend list', ''); + return; + } + } + + for (let i: number = 0; i < this.ignoreCount; i++) { + if (this.ignoreName37[i] === username) { + this.addMessage(0, 'Please remove ' + displayName + ' from your ignore list first', ''); + return; + } + } + + if (!this.localPlayer || !this.localPlayer.name) { + return; + } + if (displayName !== this.localPlayer.name) { + this.friendName[this.friendCount] = displayName; + this.friendName37[this.friendCount] = username; + this.friendWorld[this.friendCount] = 0; + this.friendCount++; + this.redrawSidebar = true; + + // FRIENDLIST_ADD + this.out.p1isaac(ClientProt.FRIENDLIST_ADD); + this.out.p8(username); + } + }; + + private removeFriend = (username: bigint): void => { + if (username === 0n) { + return; + } + + for (let i: number = 0; i < this.friendCount; i++) { + if (this.friendName37[i] === username) { + this.friendCount--; + this.redrawSidebar = true; + for (let j: number = i; j < this.friendCount; j++) { + this.friendName[j] = this.friendName[j + 1]; + this.friendWorld[j] = this.friendWorld[j + 1]; + this.friendName37[j] = this.friendName37[j + 1]; + } + // FRIENDLIST_DEL + this.out.p1isaac(ClientProt.FRIENDLIST_DEL); + this.out.p8(username); + return; + } + } + }; + + private addIgnore = (username: bigint): void => { + if (username === 0n) { + return; + } + + if (this.ignoreCount >= 100) { + this.addMessage(0, 'Your ignore list is full. Max of 100 hit', ''); + return; + } + + const displayName: string = JString.formatName(JString.fromBase37(username)); + for (let i: number = 0; i < this.ignoreCount; i++) { + if (this.ignoreName37[i] === username) { + this.addMessage(0, displayName + ' is already on your ignore list', ''); + return; + } + } + + for (let i: number = 0; i < this.friendCount; i++) { + if (this.friendName37[i] === username) { + this.addMessage(0, 'Please remove ' + displayName + ' from your friend list first', ''); + return; + } + } + + this.ignoreName37[this.ignoreCount++] = username; + this.redrawSidebar = true; + // IGNORELIST_ADD + this.out.p1isaac(ClientProt.IGNORELIST_ADD); + this.out.p8(username); + }; + + private removeIgnore = (username: bigint): void => { + if (username === 0n) { + return; + } + + for (let i: number = 0; i < this.ignoreCount; i++) { + if (this.ignoreName37[i] === username) { + this.ignoreCount--; + this.redrawSidebar = true; + for (let j: number = i; j < this.ignoreCount; j++) { + this.ignoreName37[j] = this.ignoreName37[j + 1]; + } + // IGNORELIST_DEL + this.out.p1isaac(ClientProt.IGNORELIST_DEL); + this.out.p8(username); + return; + } + } + }; + + private sortObjStacks = (x: number, z: number): void => { + const objStacks: LinkList | null = this.levelObjStacks[this.currentLevel][x][z]; + if (!objStacks) { + this.scene?.removeObjStack(this.currentLevel, x, z); + return; + } + + let topCost: number = -99999999; + let topObj: ObjStackEntity | null = null; + + for (let obj: ObjStackEntity | null = objStacks.peekFront() as ObjStackEntity | null; obj; obj = objStacks.prev() as ObjStackEntity | null) { + const type: ObjType = ObjType.get(obj.index); + let cost: number = type.cost; + + if (type.stackable) { + cost *= obj.count + 1; + } + + if (cost > topCost) { + topCost = cost; + topObj = obj; + } + } + + if (!topObj) { + return; // custom + } + + objStacks.pushFront(topObj); + + let bottomObjId: number = -1; + let middleObjId: number = -1; + let bottomObjCount: number = 0; + let middleObjCount: number = 0; + for (let obj: ObjStackEntity | null = objStacks.peekFront() as ObjStackEntity | null; obj; obj = objStacks.prev() as ObjStackEntity | null) { + if (obj.index !== topObj.index && bottomObjId === -1) { + bottomObjId = obj.index; + bottomObjCount = obj.count; + } + + if (obj.index !== topObj.index && obj.index !== bottomObjId && middleObjId === -1) { + middleObjId = obj.index; + middleObjCount = obj.count; + } + } + + let bottomObj: Model | null = null; + if (bottomObjId !== -1) { + bottomObj = ObjType.get(bottomObjId).getInterfaceModel(bottomObjCount); + } + + let middleObj: Model | null = null; + if (middleObjId !== -1) { + middleObj = ObjType.get(middleObjId).getInterfaceModel(middleObjCount); + } + + const bitset: number = (x + (z << 7) + 0x60000000) | 0; + const type: ObjType = ObjType.get(topObj.index); + this.scene?.addObjStack(x, z, this.getHeightmapY(this.currentLevel, x * 128 + 64, z * 128 + 64), this.currentLevel, bitset, type.getInterfaceModel(topObj.count), middleObj, bottomObj); + }; + + private addLoc = (level: number, x: number, z: number, id: number, angle: number, shape: number, layer: number): void => { + if (x < 1 || z < 1 || x > 102 || z > 102) { + return; + } + + if (Client.lowMemory && level !== this.currentLevel) { + return; + } + + if (!this.scene) { + return; + } + + let bitset: number = 0; + + if (layer === LocLayer.WALL) { + bitset = this.scene.getWallBitset(level, x, z); + } + + if (layer === LocLayer.WALL_DECOR) { + bitset = this.scene.getWallDecorationBitset(level, z, x); + } + + if (layer === LocLayer.GROUND) { + bitset = this.scene.getLocBitset(level, x, z); + } + + if (layer === LocLayer.GROUND_DECOR) { + bitset = this.scene.getGroundDecorationBitset(level, x, z); + } + + if (bitset !== 0) { + const otherInfo: number = this.scene.getInfo(level, x, z, bitset); + const otherId: number = (bitset >> 14) & 0x7fff; + const otherShape: number = otherInfo & 0x1f; + const otherRotation: number = otherInfo >> 6; + + if (layer === LocLayer.WALL) { + this.scene?.removeWall(level, x, z, 1); + const type: LocType = LocType.get(otherId); + + if (type.blockwalk) { + this.levelCollisionMap[level]?.removeWall(x, z, otherShape, otherRotation, type.blockrange); + } + } + + if (layer === LocLayer.WALL_DECOR) { + this.scene?.removeWallDecoration(level, x, z); + } + + if (layer === LocLayer.GROUND) { + this.scene.removeLoc(level, x, z); + const type: LocType = LocType.get(otherId); + + if (x + type.width > CollisionMap.SIZE - 1 || z + type.width > CollisionMap.SIZE - 1 || x + type.length > CollisionMap.SIZE - 1 || z + type.length > CollisionMap.SIZE - 1) { + return; + } + + if (type.blockwalk) { + this.levelCollisionMap[level]?.removeLoc(x, z, type.width, type.length, otherRotation, type.blockrange); + } + } + + if (layer === LocLayer.GROUND_DECOR) { + this.scene?.removeGroundDecoration(level, x, z); + const type: LocType = LocType.get(otherId); + + if (type.blockwalk && type.active) { + this.levelCollisionMap[level]?.removeFloor(x, z); + } + } + } + + if (id >= 0) { + let tileLevel: number = level; + + if (this.levelTileFlags && level < 3 && (this.levelTileFlags[1][x][z] & 0x2) === 2) { + tileLevel = level + 1; + } + + World.addLoc(level, x, z, this.scene, this.levelHeightmap!, this.locList, this.levelCollisionMap[level]!, id, shape, angle, tileLevel); // wrapped in a try catch + } + }; + + private closeInterfaces = (): void => { + this.out.p1isaac(ClientProt.CLOSE_MODAL); + + if (this.sidebarInterfaceId !== -1) { + this.sidebarInterfaceId = -1; + this.redrawSidebar = true; + this.pressedContinueOption = false; + this.redrawSideicons = true; + } + + if (this.chatInterfaceId !== -1) { + this.chatInterfaceId = -1; + this.redrawChatback = true; + this.pressedContinueOption = false; + } + + this.viewportInterfaceId = -1; + }; + + private tryReconnect = async (): Promise => { + if (this.idleTimeout > 0) { + await this.logout(); + } else { + this.areaViewport?.bind(); + this.fontPlain12?.drawStringCenter(257, 144, 'Connection lost', Colors.BLACK); + this.fontPlain12?.drawStringCenter(256, 143, 'Connection lost', Colors.WHITE); + this.fontPlain12?.drawStringCenter(257, 159, 'Please wait - attempting to reestablish', Colors.BLACK); + this.fontPlain12?.drawStringCenter(256, 158, 'Please wait - attempting to reestablish', Colors.WHITE); + this.areaViewport?.draw(8, 11); + this.flagSceneTileX = 0; + const stream: ClientStream | null = this.stream; + this.ingame = false; + await this.login(this.username, this.password, true); + if (!this.ingame) { + await this.logout(); + } + stream?.close(); + } + }; + + private logout = async (): Promise => { + if (this.stream) { + this.stream.close(); + } + + this.stream = null; + this.ingame = false; + this.titleScreenState = 0; + this.username = ''; + this.password = ''; + + InputTracking.setDisabled(); + this.clearCaches(); + this.scene?.reset(); + + for (let level: number = 0; level < CollisionMap.LEVELS; level++) { + this.levelCollisionMap[level]?.reset(); + } + + stopMidi(); + this.currentMidi = null; + this.nextMusicDelay = 0; + if (!Client.lowMemory) { + await this.setMidi('scape_main', 12345678, 40000); + } + }; + + private read = async (): Promise => { + if (!this.stream) { + return false; + } + + try { + let available: number = this.stream.available; + if (available === 0) { + return false; + } + + if (this.packetType === -1) { + await this.stream.readBytes(this.in.data, 0, 1); + this.packetType = this.in.data[0] & 0xff; + if (this.randomIn) { + this.packetType = (this.packetType - this.randomIn.nextInt) & 0xff; + } + this.packetSize = Protocol.SERVERPROT_SIZES[this.packetType]; + available--; + } + + if (this.packetSize === -1) { + if (available <= 0) { + return false; + } + + await this.stream.readBytes(this.in.data, 0, 1); + this.packetSize = this.in.data[0] & 0xff; + available--; + } + + if (this.packetSize === -2) { + if (available <= 1) { + return false; + } + + await this.stream.readBytes(this.in.data, 0, 2); + this.in.pos = 0; + this.packetSize = this.in.g2; + available -= 2; + } + + if (available < this.packetSize) { + return false; + } + + this.in.pos = 0; + await this.stream.readBytes(this.in.data, 0, this.packetSize); + this.idleNetCycles = 0; + this.lastPacketType2 = this.lastPacketType1; + this.lastPacketType1 = this.lastPacketType0; + this.lastPacketType0 = this.packetType; + + // console.log(`Incoming packet: ${this.packetType}`); + + if (this.packetType === ServerProt.VARP_SMALL) { + // VARP_SMALL + const varp: number = this.in.g2; + const value: number = this.in.g1b; + this.varCache[varp] = value; + if (this.varps[varp] !== value) { + this.varps[varp] = value; + await this.updateVarp(varp); + this.redrawSidebar = true; + if (this.stickyChatInterfaceId !== -1) { + this.redrawChatback = true; + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_FRIENDLIST) { + // UPDATE_FRIENDLIST + const username: bigint = this.in.g8; + const world: number = this.in.g1; + let displayName: string | null = JString.formatName(JString.fromBase37(username)); + for (let i: number = 0; i < this.friendCount; i++) { + if (username === this.friendName37[i]) { + if (this.friendWorld[i] !== world) { + this.friendWorld[i] = world; + this.redrawSidebar = true; + if (world > 0) { + this.addMessage(5, displayName + ' has logged in.', ''); + } + if (world === 0) { + this.addMessage(5, displayName + ' has logged out.', ''); + } + } + displayName = null; + break; + } + } + if (displayName && this.friendCount < 100) { + this.friendName37[this.friendCount] = username; + this.friendName[this.friendCount] = displayName; + this.friendWorld[this.friendCount] = world; + this.friendCount++; + this.redrawSidebar = true; + } + let sorted: boolean = false; + while (!sorted) { + sorted = true; + for (let i: number = 0; i < this.friendCount - 1; i++) { + if ((this.friendWorld[i] !== Client.nodeId && this.friendWorld[i + 1] === Client.nodeId) || (this.friendWorld[i] === 0 && this.friendWorld[i + 1] !== 0)) { + const oldWorld: number = this.friendWorld[i]; + this.friendWorld[i] = this.friendWorld[i + 1]; + this.friendWorld[i + 1] = oldWorld; + + const oldName: string | null = this.friendName[i]; + this.friendName[i] = this.friendName[i + 1]; + this.friendName[i + 1] = oldName; + + const oldName37: bigint = this.friendName37[i]; + this.friendName37[i] = this.friendName37[i + 1]; + this.friendName37[i + 1] = oldName37; + this.redrawSidebar = true; + sorted = false; + } + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_REBOOT_TIMER) { + // UPDATE_REBOOT_TIMER + this.systemUpdateTimer = this.in.g2 * 30; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.DATA_LAND_DONE) { + // DATA_LAND_DONE + const x: number = this.in.g1; + const z: number = this.in.g1; + let index: number = -1; + if (this.sceneMapIndex) { + for (let i: number = 0; i < this.sceneMapIndex.length; i++) { + if (this.sceneMapIndex[i] === (x << 8) + z) { + index = i; + } + } + } + if (index !== -1) { + const mapdata: (Int8Array | null)[] | null = this.sceneMapLandData; + if (mapdata) { + const data: Int8Array | null = mapdata[index]; + if (index !== -1 && data) { + this.db?.cachesave(`m${x}_${z}`, data); + this.sceneState = 1; + } + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.NPC_INFO) { + // NPC_INFO + this.readNpcInfo(this.in, this.packetSize); + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.REBUILD_NORMAL) { + // LOAD_AREA + const zoneX: number = this.in.g2; + const zoneZ: number = this.in.g2; + + if (this.sceneCenterZoneX === zoneX && this.sceneCenterZoneZ === zoneZ && this.sceneState !== 0) { + this.packetType = -1; + return true; + } + this.sceneCenterZoneX = zoneX; + this.sceneCenterZoneZ = zoneZ; + this.sceneBaseTileX = (this.sceneCenterZoneX - 6) * 8; + this.sceneBaseTileZ = (this.sceneCenterZoneZ - 6) * 8; + this.sceneState = 1; + this.areaViewport?.bind(); + this.fontPlain12?.drawStringCenter(257, 151, 'Loading - please wait.', Colors.BLACK); + this.fontPlain12?.drawStringCenter(256, 150, 'Loading - please wait.', Colors.WHITE); + this.areaViewport?.draw(8, 11); + // signlink.looprate(5); + + const regions: number = ((this.packetSize - 2) / 10) | 0; + + this.sceneMapLandData = new TypedArray1d(regions, null); + this.sceneMapLocData = new TypedArray1d(regions, null); + this.sceneMapIndex = new Int32Array(regions); + + this.out.p1isaac(ClientProt.REBUILD_GETMAPS); + this.out.p1(0); + + let mapCount: number = 0; + + for (let i: number = 0; i < regions; i++) { + const mapsquareX: number = this.in.g1; + const mapsquareZ: number = this.in.g1; + const landCrc: number = this.in.g4; + const locCrc: number = this.in.g4; + this.sceneMapIndex[i] = (mapsquareX << 8) + mapsquareZ; + + let data: Int8Array | undefined; + if (landCrc !== 0) { + data = await this.db?.cacheload(`m${mapsquareX}_${mapsquareZ}`); + if (data && Packet.crc32(data) !== landCrc) { + data = undefined; + } + if (!data) { + this.sceneState = 0; + this.out.p1(0); // map request + this.out.p1(mapsquareX); + this.out.p1(mapsquareZ); + mapCount += 3; + } else { + this.sceneMapLandData[i] = data; + } + } + if (locCrc !== 0) { + data = await this.db?.cacheload(`l${mapsquareX}_${mapsquareZ}`); + if (data && Packet.crc32(data) !== locCrc) { + data = undefined; + } + if (!data) { + this.sceneState = 0; + this.out.p1(1); // loc request + this.out.p1(mapsquareX); + this.out.p1(mapsquareZ); + mapCount += 3; + } else { + this.sceneMapLocData[i] = data; + } + } + } + this.out.psize1(mapCount); + // signlink.looprate(50); + this.areaViewport?.bind(); + if (this.sceneState === 0) { + this.fontPlain12?.drawStringCenter(257, 166, 'Map area updated since last visit, so load will take longer this time only', Colors.BLACK); + this.fontPlain12?.drawStringCenter(256, 165, 'Map area updated since last visit, so load will take longer this time only', Colors.WHITE); + } + this.areaViewport?.draw(8, 11); + const dx: number = this.sceneBaseTileX - this.mapLastBaseX; + const dz: number = this.sceneBaseTileZ - this.mapLastBaseZ; + this.mapLastBaseX = this.sceneBaseTileX; + this.mapLastBaseZ = this.sceneBaseTileZ; + for (let i: number = 0; i < 8192; i++) { + const npc: NpcEntity | null = this.npcs[i]; + if (npc) { + for (let j: number = 0; j < 10; j++) { + npc.pathTileX[j] -= dx; + npc.pathTileZ[j] -= dz; + } + npc.x -= dx * 128; + npc.z -= dz * 128; + } + } + for (let i: number = 0; i < this.MAX_PLAYER_COUNT; i++) { + const player: PlayerEntity | null = this.players[i]; + if (player) { + for (let j: number = 0; j < 10; j++) { + player.pathTileX[j] -= dx; + player.pathTileZ[j] -= dz; + } + player.x -= dx * 128; + player.z -= dz * 128; + } + } + let startTileX: number = 0; + let endTileX: number = CollisionMap.SIZE; + let dirX: number = 1; + if (dx < 0) { + startTileX = CollisionMap.SIZE - 1; + endTileX = -1; + dirX = -1; + } + let startTileZ: number = 0; + let endTileZ: number = CollisionMap.SIZE; + let dirZ: number = 1; + if (dz < 0) { + startTileZ = CollisionMap.SIZE - 1; + endTileZ = -1; + dirZ = -1; + } + for (let x: number = startTileX; x !== endTileX; x += dirX) { + for (let z: number = startTileZ; z !== endTileZ; z += dirZ) { + const lastX: number = x + dx; + const lastZ: number = z + dz; + for (let level: number = 0; level < CollisionMap.LEVELS; level++) { + if (lastX >= 0 && lastZ >= 0 && lastX < CollisionMap.SIZE && lastZ < CollisionMap.SIZE) { + this.levelObjStacks[level][x][z] = this.levelObjStacks[level][lastX][lastZ]; + } else { + this.levelObjStacks[level][x][z] = null; + } + } + } + } + for (let loc: LocTemporary | null = this.spawnedLocations.peekFront() as LocTemporary | null; loc; loc = this.spawnedLocations.prev() as LocTemporary | null) { + loc.x -= dx; + loc.z -= dz; + if (loc.x < 0 || loc.z < 0 || loc.x >= CollisionMap.SIZE || loc.z >= CollisionMap.SIZE) { + loc.unlink(); + } + } + if (this.flagSceneTileX !== 0) { + this.flagSceneTileX -= dx; + this.flagSceneTileZ -= dz; + } + this.cutscene = false; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETPLAYERHEAD) { + // IF_SETPLAYERHEAD + ComType.instances[this.in.g2].model = this.localPlayer?.getHeadModel() || null; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.HINT_ARROW) { + this.hintType = this.in.g1; + if (this.hintType === 1) { + this.hintNpc = this.in.g2; + } + if (this.hintType >= 2 && this.hintType <= 6) { + if (this.hintType === 2) { + this.hintOffsetX = 64; + this.hintOffsetZ = 64; + } + if (this.hintType === 3) { + this.hintOffsetX = 0; + this.hintOffsetZ = 64; + } + if (this.hintType === 4) { + this.hintOffsetX = 128; + this.hintOffsetZ = 64; + } + if (this.hintType === 5) { + this.hintOffsetX = 64; + this.hintOffsetZ = 0; + } + if (this.hintType === 6) { + this.hintOffsetX = 64; + this.hintOffsetZ = 128; + } + this.hintType = 2; + this.hintTileX = this.in.g2; + this.hintTileZ = this.in.g2; + this.hintHeight = this.in.g1; + } + if (this.hintType === 10) { + this.hintPlayer = this.in.g2; + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.MIDI_SONG) { + // MIDI_SONG + const name: string = this.in.gjstr; + const crc: number = this.in.g4; + const length: number = this.in.g4; + if (!(name === this.currentMidi) && this.midiActive && !Client.lowMemory) { + await this.setMidi(name, crc, length); + } + this.currentMidi = name; + this.midiCrc = crc; + this.midiSize = length; + this.nextMusicDelay = 0; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.LOGOUT) { + // LOGOUT + await this.logout(); + this.packetType = -1; + return false; + } + if (this.packetType === ServerProt.DATA_LOC_DONE) { + // DATA_LOC_DONE + const x: number = this.in.g1; + const z: number = this.in.g1; + let index: number = -1; + if (this.sceneMapIndex) { + for (let i: number = 0; i < this.sceneMapIndex.length; i++) { + if (this.sceneMapIndex[i] === (x << 8) + z) { + index = i; + } + } + } + if (index !== -1) { + const mapdata: (Int8Array | null)[] | null = this.sceneMapLocData; + if (mapdata) { + const data: Int8Array | null = mapdata[index]; + if (index !== -1 && data) { + this.db?.cachesave(`l${x}_${z}`, data); + this.sceneState = 1; + } + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UNSET_MAP_FLAG) { + // CLEAR_WALKING_QUEUE + this.flagSceneTileX = 0; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_UID192) { + // UPDATE_UID192 + this.localPid = this.in.g2; + this.packetType = -1; + return true; + } + if ( + this.packetType === ServerProt.OBJ_COUNT || + this.packetType === ServerProt.LOC_MERGE || + this.packetType === ServerProt.OBJ_REVEAL || + this.packetType === ServerProt.MAP_ANIM || + this.packetType === ServerProt.MAP_PROJANIM || + this.packetType === ServerProt.OBJ_DEL || + this.packetType === ServerProt.OBJ_ADD || + this.packetType === ServerProt.LOC_ANIM || + this.packetType === ServerProt.LOC_DEL || + this.packetType === ServerProt.LOC_ADD_CHANGE + ) { + // Zone Protocol + this.readZonePacket(this.in, this.packetType); + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_OPENMAINSIDEMODAL) { + // IF_OPENMAINMODALSIDEOVERLAY + const main: number = this.in.g2; + const side: number = this.in.g2; + if (this.chatInterfaceId !== -1) { + this.chatInterfaceId = -1; + this.redrawChatback = true; + } + if (this.chatbackInputOpen) { + this.chatbackInputOpen = false; + this.redrawChatback = true; + } + this.viewportInterfaceId = main; + this.sidebarInterfaceId = side; + this.redrawSidebar = true; + this.redrawSideicons = true; + this.pressedContinueOption = false; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.VARP_LARGE) { + // VARP_LARGE + const varp: number = this.in.g2; + const value: number = this.in.g4; + this.varCache[varp] = value; + if (this.varps[varp] !== value) { + this.varps[varp] = value; + await this.updateVarp(varp); + this.redrawSidebar = true; + if (this.stickyChatInterfaceId !== -1) { + this.redrawChatback = true; + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETANIM) { + // IF_SETANIM + const com: number = this.in.g2; + ComType.instances[com].anim = this.in.g2; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_OPENSIDEOVERLAY) { + // IF_SETTAB + let com: number = this.in.g2; + const tab: number = this.in.g1; + if (com === 65535) { + com = -1; + } + this.tabInterfaceId[tab] = com; + this.redrawSidebar = true; + this.redrawSideicons = true; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.DATA_LOC) { + // DATA_LOC + const x: number = this.in.g1; + const z: number = this.in.g1; + const off: number = this.in.g2; + const length: number = this.in.g2; + let index: number = -1; + if (this.sceneMapIndex) { + for (let i: number = 0; i < this.sceneMapIndex.length; i++) { + if (this.sceneMapIndex[i] === (x << 8) + z) { + index = i; + } + } + } + if (index !== -1 && this.sceneMapLocData) { + if (!this.sceneMapLocData[index] || this.sceneMapLocData[index]?.length !== length) { + this.sceneMapLocData[index] = new Int8Array(length); + } + const data: Int8Array | null = this.sceneMapLocData[index]; + if (data) { + this.in.gdata(this.packetSize - 6, off, data); + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.FINISH_TRACKING) { + // FINISH_TRACKING + const tracking: Packet | null = InputTracking.stop(); + if (tracking) { + this.out.p1isaac(ClientProt.EVENT_TRACKING); + this.out.p2(tracking.pos); + this.out.pdata(tracking.data, tracking.pos, 0); + tracking.release(); + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_INV_FULL) { + // UPDATE_INV_FULL + this.redrawSidebar = true; + const com: number = this.in.g2; + const inv: ComType = ComType.instances[com]; + const size: number = this.in.g1; + if (inv.invSlotObjId && inv.invSlotObjCount) { + for (let i: number = 0; i < size; i++) { + inv.invSlotObjId[i] = this.in.g2; + let count: number = this.in.g1; + if (count === 255) { + count = this.in.g4; + } + inv.invSlotObjCount[i] = count; + } + for (let i: number = size; i < inv.invSlotObjId.length; i++) { + inv.invSlotObjId[i] = 0; + inv.invSlotObjCount[i] = 0; + } + } else { + for (let i: number = 0; i < size; i++) { + this.in.g2; + if (this.in.g1 === 255) { + this.in.g4; + } + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.ENABLE_TRACKING) { + // ENABLE_TRACKING + InputTracking.setEnabled(); + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.P_COUNTDIALOG) { + // IF_IAMOUNT + this.showSocialInput = false; + this.chatbackInputOpen = true; + this.chatbackInput = ''; + this.redrawChatback = true; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_INV_STOP_TRANSMIT) { + // UPDATE_INV_STOP_TRANSMIT + const inv: ComType = ComType.instances[this.in.g2]; + if (inv.invSlotObjId) { + for (let i: number = 0; i < inv.invSlotObjId.length; i++) { + inv.invSlotObjId[i] = -1; + inv.invSlotObjId[i] = 0; + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.LAST_LOGIN_INFO) { + // LAST_LOGIN_INFO + this.lastAddress = this.in.g4; + this.daysSinceLastLogin = this.in.g2; + this.daysSinceRecoveriesChanged = this.in.g1; + this.unreadMessages = this.in.g2; + if (this.lastAddress !== 0 && this.viewportInterfaceId === -1) { + // signlink.dnslookup(JString.formatIPv4(this.lastAddress)); // TODO? + this.closeInterfaces(); + let contentType: number = 650; + if (this.daysSinceRecoveriesChanged !== 201) { + contentType = 655; + } + this.reportAbuseInput = ''; + this.reportAbuseMuteOption = false; + for (let i: number = 0; i < ComType.instances.length; i++) { + if (ComType.instances[i] && ComType.instances[i].clientCode === contentType) { + this.viewportInterfaceId = ComType.instances[i].layer; + break; + } + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.TUTORIAL_FLASHSIDE) { + // IF_SETTAB_FLASH + this.flashingTab = this.in.g1; + if (this.flashingTab === this.selectedTab) { + if (this.flashingTab === 3) { + this.selectedTab = 1; + } else { + this.selectedTab = 3; + } + this.redrawSidebar = true; + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.MIDI_JINGLE) { + // MIDI_JINGLE + if (this.midiActive && !Client.lowMemory) { + const delay: number = this.in.g2; + const length: number = this.in.g4; + const remaining: number = this.packetSize - 6; + const uncompressed: Int8Array = Bzip.read(length, Int8Array.from(this.in.data), remaining, this.in.pos); + playMidi(uncompressed, this.midiVolume); + this.nextMusicDelay = delay; + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.SET_MULTIWAY) { + // IF_MULTIZONE + this.inMultizone = this.in.g1; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.SYNTH_SOUND) { + // SYNTH_SOUND + const id: number = this.in.g2; + const loop: number = this.in.g1; + const delay: number = this.in.g2; + if (this.waveEnabled && !Client.lowMemory && this.waveCount < 50) { + this.waveIds[this.waveCount] = id; + this.waveLoops[this.waveCount] = loop; + this.waveDelay[this.waveCount] = delay + Wave.delays[id]; + this.waveCount++; + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETNPCHEAD) { + // IF_SETNPCHEAD + const com: number = this.in.g2; + const npcId: number = this.in.g2; + const npc: NpcType = NpcType.get(npcId); + ComType.instances[com].model = npc.getHeadModel(); + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_ZONE_PARTIAL_FOLLOWS) { + // UPDATE_ZONE_PARTIAL_FOLLOWS + this.baseX = this.in.g1; + this.baseZ = this.in.g1; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETRECOL) { + // IF_SETMODEL_COLOUR + const com: number = this.in.g2; + const src: number = this.in.g2; + const dst: number = this.in.g2; + const inter: ComType = ComType.instances[com]; + const model: Model | null = inter.model; + model?.recolor(src, dst); + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.CHAT_FILTER_SETTINGS) { + // CHAT_FILTER_SETTINGS + this.publicChatSetting = this.in.g1; + this.privateChatSetting = this.in.g1; + this.tradeChatSetting = this.in.g1; + this.redrawPrivacySettings = true; + this.redrawChatback = true; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_OPENSIDEMODAL) { + // IF_OPENSIDEOVERLAY + const com: number = this.in.g2; + this.resetInterfaceAnimation(com); + if (this.chatInterfaceId !== -1) { + this.chatInterfaceId = -1; + this.redrawChatback = true; + } + if (this.chatbackInputOpen) { + this.chatbackInputOpen = false; + this.redrawChatback = true; + } + this.sidebarInterfaceId = com; + this.redrawSidebar = true; + this.redrawSideicons = true; + this.viewportInterfaceId = -1; + this.pressedContinueOption = false; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_OPENCHATMODAL) { + // IF_OPENCHAT + const com: number = this.in.g2; + this.resetInterfaceAnimation(com); + if (this.sidebarInterfaceId !== -1) { + this.sidebarInterfaceId = -1; + this.redrawSidebar = true; + this.redrawSideicons = true; + } + this.chatInterfaceId = com; + this.redrawChatback = true; + this.viewportInterfaceId = -1; + this.pressedContinueOption = false; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETPOSITION) { + // IF_SETPOSITION + const com: number = this.in.g2; + const x: number = this.in.g2b; + const z: number = this.in.g2b; + const inter: ComType = ComType.instances[com]; + inter.x = x; + inter.y = z; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.CAM_LOOKAT) { + // CAM_LOOKAT + this.cutscene = true; + this.cutsceneSrcLocalTileX = this.in.g1; + this.cutsceneSrcLocalTileZ = this.in.g1; + this.cutsceneSrcHeight = this.in.g2; + this.cutsceneMoveSpeed = this.in.g1; + this.cutsceneMoveAcceleration = this.in.g1; + if (this.cutsceneMoveAcceleration >= 100) { + this.cameraX = this.cutsceneSrcLocalTileX * 128 + 64; + this.cameraZ = this.cutsceneSrcLocalTileZ * 128 + 64; + this.cameraY = this.getHeightmapY(this.currentLevel, this.cutsceneSrcLocalTileX, this.cutsceneSrcLocalTileZ) - this.cutsceneSrcHeight; + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_ZONE_FULL_FOLLOWS) { + // UPDATE_ZONE_FULL_FOLLOWS + this.baseX = this.in.g1; + this.baseZ = this.in.g1; + for (let x: number = this.baseX; x < this.baseX + 8; x++) { + for (let z: number = this.baseZ; z < this.baseZ + 8; z++) { + if (this.levelObjStacks[this.currentLevel][x][z]) { + this.levelObjStacks[this.currentLevel][x][z] = null; + this.sortObjStacks(x, z); + } + } + } + for (let loc: LocTemporary | null = this.spawnedLocations.peekFront() as LocTemporary | null; loc; loc = this.spawnedLocations.prev() as LocTemporary | null) { + if (loc.x >= this.baseX && loc.x < this.baseX + 8 && loc.z >= this.baseZ && loc.z < this.baseZ + 8 && loc.plane === this.currentLevel) { + this.addLoc(loc.plane, loc.x, loc.z, loc.lastLocIndex, loc.lastAngle, loc.lastShape, loc.layer); + loc.unlink(); + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.DATA_LAND) { + // DATA_LAND + const x: number = this.in.g1; + const z: number = this.in.g1; + const off: number = this.in.g2; + const length: number = this.in.g2; + let index: number = -1; + if (this.sceneMapIndex) { + for (let i: number = 0; i < this.sceneMapIndex.length; i++) { + if (this.sceneMapIndex[i] === (x << 8) + z) { + index = i; + } + } + } + if (index !== -1 && this.sceneMapLandData) { + if (!this.sceneMapLandData[index] || this.sceneMapLandData[index]?.length !== length) { + this.sceneMapLandData[index] = new Int8Array(length); + } + const data: Int8Array | null = this.sceneMapLandData[index]; + if (data) { + this.in.gdata(this.packetSize - 6, off, data); + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.MESSAGE_PRIVATE) { + // MESSAGE_PRIVATE + const from: bigint = this.in.g8; + const messageId: number = this.in.g4; + const staffModLevel: number = this.in.g1; + let ignored: boolean = false; + for (let i: number = 0; i < 100; i++) { + if (this.messageIds[i] === messageId) { + ignored = true; + break; + } + } + if (staffModLevel <= 1) { + for (let i: number = 0; i < this.ignoreCount; i++) { + if (this.ignoreName37[i] === from) { + ignored = true; + break; + } + } + } + if (!ignored && this.overrideChat === 0) { + try { + this.messageIds[this.privateMessageCount] = messageId; + this.privateMessageCount = (this.privateMessageCount + 1) % 100; + const uncompressed: string = WordPack.unpack(this.in, this.packetSize - 13); + const filtered: string = WordFilter.filter(uncompressed); + if (staffModLevel > 1) { + this.addMessage(7, filtered, JString.formatName(JString.fromBase37(from))); + } else { + this.addMessage(3, filtered, JString.formatName(JString.fromBase37(from))); + } + } catch (e) { + // signlink.reporterror("cde1"); TODO? + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.RESET_CLIENT_VARCACHE) { + // RESET_CLIENT_VARCACHE + for (let i: number = 0; i < this.varps.length; i++) { + if (this.varps[i] !== this.varCache[i]) { + this.varps[i] = this.varCache[i]; + await this.updateVarp(i); + this.redrawSidebar = true; + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETMODEL) { + // IF_SETMODEL + const com: number = this.in.g2; + const model: number = this.in.g2; + ComType.instances[com].model = Model.model(model); + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.TUTORIAL_OPENCHAT) { + // IF_OPENCHATSTICKY + this.stickyChatInterfaceId = this.in.g2b; + this.redrawChatback = true; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_RUNENERGY) { + // UPDATE_RUNENERGY + if (this.selectedTab === 12) { + this.redrawSidebar = true; + } + this.energy = this.in.g1; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.CAM_MOVETO) { + // CAM_MOVETO + this.cutscene = true; + this.cutsceneDstLocalTileX = this.in.g1; + this.cutsceneDstLocalTileZ = this.in.g1; + this.cutsceneDstHeight = this.in.g2; + this.cutsceneRotateSpeed = this.in.g1; + this.cutsceneRotateAcceleration = this.in.g1; + if (this.cutsceneRotateAcceleration >= 100) { + const sceneX: number = this.cutsceneDstLocalTileX * 128 + 64; + const sceneZ: number = this.cutsceneDstLocalTileZ * 128 + 64; + const sceneY: number = this.getHeightmapY(this.currentLevel, this.cutsceneDstLocalTileX, this.cutsceneDstLocalTileZ) - this.cutsceneDstHeight; + const deltaX: number = sceneX - this.cameraX; + const deltaY: number = sceneY - this.cameraY; + const deltaZ: number = sceneZ - this.cameraZ; + const distance: number = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ) | 0; + this.cameraPitch = ((Math.atan2(deltaY, distance) * 325.949) | 0) & 0x7ff; + this.cameraYaw = ((Math.atan2(deltaX, deltaZ) * -325.949) | 0) & 0x7ff; + if (this.cameraPitch < 128) { + this.cameraPitch = 128; + } + if (this.cameraPitch > 383) { + this.cameraPitch = 383; + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SHOWSIDE) { + // IF_SETTAB_ACTIVE + this.selectedTab = this.in.g1; + this.redrawSidebar = true; + this.redrawSideicons = true; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.MESSAGE_GAME) { + // MESSAGE_GAME + const message: string = this.in.gjstr; + let username: bigint; + if (message.endsWith(':tradereq:')) { + const player: string = message.substring(0, message.indexOf(':')); + username = JString.toBase37(player); + let ignored: boolean = false; + for (let i: number = 0; i < this.ignoreCount; i++) { + if (this.ignoreName37[i] === username) { + ignored = true; + break; + } + } + if (!ignored && this.overrideChat === 0) { + this.addMessage(4, 'wishes to trade with you.', player); + } + } else if (message.endsWith(':duelreq:')) { + const player: string = message.substring(0, message.indexOf(':')); + username = JString.toBase37(player); + let ignored: boolean = false; + for (let i: number = 0; i < this.ignoreCount; i++) { + if (this.ignoreName37[i] === username) { + ignored = true; + break; + } + } + if (!ignored && this.overrideChat === 0) { + this.addMessage(8, 'wishes to duel with you.', player); + } + } else { + this.addMessage(0, message, ''); + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETOBJECT) { + // IF_SETOBJECT + const com: number = this.in.g2; + const objId: number = this.in.g2; + const zoom: number = this.in.g2; + const obj: ObjType = ObjType.get(objId); + ComType.instances[com].model = obj.getInterfaceModel(50); + ComType.instances[com].xan = obj.xan2d; + ComType.instances[com].yan = obj.yan2d; + ComType.instances[com].zoom = ((obj.zoom2d * 100) / zoom) | 0; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_OPENMAINMODAL) { + // IF_OPENMAIN + const com: number = this.in.g2; + this.resetInterfaceAnimation(com); + if (this.sidebarInterfaceId !== -1) { + this.sidebarInterfaceId = -1; + this.redrawSidebar = true; + this.redrawSideicons = true; + } + if (this.chatInterfaceId !== -1) { + this.chatInterfaceId = -1; + this.redrawChatback = true; + } + if (this.chatbackInputOpen) { + this.chatbackInputOpen = false; + this.redrawChatback = true; + } + this.viewportInterfaceId = com; + this.pressedContinueOption = false; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETCOLOUR) { + // IF_SETCOLOUR + const com: number = this.in.g2; + const color: number = this.in.g2; + const r: number = (color >> 10) & 0x1f; + const g: number = (color >> 5) & 0x1f; + const b: number = color & 0x1f; + ComType.instances[com].colour = (r << 19) + (g << 11) + (b << 3); + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.RESET_ANIMS) { + // RESET_ANIMS + for (let i: number = 0; i < this.players.length; i++) { + const player: PlayerEntity | null = this.players[i]; + if (!player) { + continue; + } + player.primarySeqId = -1; + } + for (let i: number = 0; i < this.npcs.length; i++) { + const npc: NpcEntity | null = this.npcs[i]; + if (!npc) { + continue; + } + npc.primarySeqId = -1; + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETHIDE) { + // IF_SETHIDE + const com: number = this.in.g2; + ComType.instances[com].hide = this.in.g1 === 1; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_IGNORELIST) { + // UPDATE_IGNORELIST + this.ignoreCount = (this.packetSize / 8) | 0; + for (let i: number = 0; i < this.ignoreCount; i++) { + this.ignoreName37[i] = this.in.g8; + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.CAM_RESET) { + // CAM_RESET + this.cutscene = false; + for (let i: number = 0; i < 5; i++) { + this.cameraModifierEnabled[i] = false; + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_CLOSE) { + // IF_CLOSE + if (this.sidebarInterfaceId !== -1) { + this.sidebarInterfaceId = -1; + this.redrawSidebar = true; + this.redrawSideicons = true; + } + if (this.chatInterfaceId !== -1) { + this.chatInterfaceId = -1; + this.redrawChatback = true; + } + if (this.chatbackInputOpen) { + this.chatbackInputOpen = false; + this.redrawChatback = true; + } + this.viewportInterfaceId = -1; + this.pressedContinueOption = false; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.IF_SETTEXT) { + // IF_SETTEXT + const com: number = this.in.g2; + ComType.instances[com].text = this.in.gjstr; + if (ComType.instances[com].layer === this.tabInterfaceId[this.selectedTab]) { + this.redrawSidebar = true; + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_STAT) { + // UPDATE_STAT + this.redrawSidebar = true; + const stat: number = this.in.g1; + const xp: number = this.in.g4; + const level: number = this.in.g1; + this.skillExperience[stat] = xp; + this.skillLevel[stat] = level; + this.skillBaseLevel[stat] = 1; + for (let i: number = 0; i < 98; i++) { + if (xp >= this.levelExperience[i]) { + this.skillBaseLevel[stat] = i + 2; + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_ZONE_PARTIAL_ENCLOSED) { + // UPDATE_ZONE_PARTIAL_ENCLOSED + this.baseX = this.in.g1; + this.baseZ = this.in.g1; + while (this.in.pos < this.packetSize) { + const opcode: number = this.in.g1; + this.readZonePacket(this.in, opcode); + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_RUNWEIGHT) { + // UPDATE_RUNWEIGHT + if (this.selectedTab === 12) { + this.redrawSidebar = true; + } + this.weightCarried = this.in.g2b; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.CAM_SHAKE) { + // CAM_SHAKE + const type: number = this.in.g1; + const jitter: number = this.in.g1; + const wobbleScale: number = this.in.g1; + const wobbleSpeed: number = this.in.g1; + this.cameraModifierEnabled[type] = true; + this.cameraModifierJitter[type] = jitter; + this.cameraModifierWobbleScale[type] = wobbleScale; + this.cameraModifierWobbleSpeed[type] = wobbleSpeed; + this.cameraModifierCycle[type] = 0; + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.UPDATE_INV_PARTIAL) { + // UPDATE_INV_PARTIAL + this.redrawSidebar = true; + const com: number = this.in.g2; + const inv: ComType = ComType.instances[com]; + while (this.in.pos < this.packetSize) { + const slot: number = this.in.g1; + const id: number = this.in.g2; + let count: number = this.in.g1; + if (count === 255) { + count = this.in.g4; + } + if (inv.invSlotObjId && inv.invSlotObjCount && slot >= 0 && slot < inv.invSlotObjId.length) { + inv.invSlotObjId[slot] = id; + inv.invSlotObjCount[slot] = count; + } + } + this.packetType = -1; + return true; + } + if (this.packetType === ServerProt.PLAYER_INFO) { + // PLAYER_INFO + this.readPlayerInfo(this.in, this.packetSize); + if (this.sceneState === 1) { + this.sceneState = 2; + World.levelBuilt = this.currentLevel; + this.buildScene(); + } + if (Client.lowMemory && this.sceneState === 2 && World.levelBuilt !== this.currentLevel) { + this.areaViewport?.bind(); + this.fontPlain12?.drawStringCenter(257, 151, 'Loading - please wait.', Colors.BLACK); + this.fontPlain12?.drawStringCenter(256, 150, 'Loading - please wait.', Colors.WHITE); + this.areaViewport?.draw(8, 11); + World.levelBuilt = this.currentLevel; + this.buildScene(); + } + if (this.currentLevel !== this.minimapLevel && this.sceneState === 2) { + this.minimapLevel = this.currentLevel; + this.createMinimap(this.currentLevel); + } + this.packetType = -1; + return true; + } + await this.logout(); + } catch (e) { + console.log(e); + await this.tryReconnect(); + // TODO extra logic for logout?? + } + return true; + }; + + private buildScene = (): void => { + try { + this.minimapLevel = -1; + this.temporaryLocs.clear(); + this.locList.clear(); + this.spotanims.clear(); + this.projectiles.clear(); + Draw3D.clearTexels(); + this.clearCaches(); + this.scene?.reset(); + for (let level: number = 0; level < CollisionMap.LEVELS; level++) { + this.levelCollisionMap[level]?.reset(); + } + + const world: World = new World(CollisionMap.SIZE, CollisionMap.SIZE, this.levelHeightmap!, this.levelTileFlags!); // has try catch here + World.lowMemory = Client.lowMemory; + + const maps: number = this.sceneMapLandData?.length ?? 0; + + if (this.sceneMapIndex) { + for (let index: number = 0; index < maps; index++) { + const mapsquareX: number = this.sceneMapIndex[index] >> 8; + const mapsquareZ: number = this.sceneMapIndex[index] & 0xff; + + // underground pass check + if (mapsquareX === 33 && mapsquareZ >= 71 && mapsquareZ <= 73) { + World.lowMemory = false; + break; + } + } + } + + if (Client.lowMemory) { + this.scene?.setMinLevel(this.currentLevel); + } else { + this.scene?.setMinLevel(0); + } + + if (this.sceneMapIndex && this.sceneMapLandData) { + // NO_TIMEOUT + this.out.p1isaac(ClientProt.NO_TIMEOUT); + for (let i: number = 0; i < maps; i++) { + const x: number = (this.sceneMapIndex[i] >> 8) * 64 - this.sceneBaseTileX; + const z: number = (this.sceneMapIndex[i] & 0xff) * 64 - this.sceneBaseTileZ; + const src: Int8Array | null = this.sceneMapLandData[i]; + if (src) { + const length: number = new Packet(new Uint8Array(src)).g4; + const data: Int8Array = Bzip.read(length, src, src.length - 4, 4); + world.readLandscape((this.sceneCenterZoneX - 6) * 8, (this.sceneCenterZoneZ - 6) * 8, x, z, data); + } else if (this.sceneCenterZoneZ < 800) { + world.clearLandscape(z, x, 64, 64); + } + } + } + + if (this.sceneMapIndex && this.sceneMapLocData) { + // NO_TIMEOUT + this.out.p1isaac(ClientProt.NO_TIMEOUT); + for (let i: number = 0; i < maps; i++) { + const src: Int8Array | null = this.sceneMapLocData[i]; + if (src) { + const length: number = new Packet(new Uint8Array(src)).g4; + const data: Int8Array = Bzip.read(length, src, src.length - 4, 4); + const x: number = (this.sceneMapIndex[i] >> 8) * 64 - this.sceneBaseTileX; + const z: number = (this.sceneMapIndex[i] & 0xff) * 64 - this.sceneBaseTileZ; + world.readLocs(this.scene, this.locList, this.levelCollisionMap, data, x, z); + } + } + } + + // NO_TIMEOUT + this.out.p1isaac(ClientProt.NO_TIMEOUT); + world.build(this.scene, this.levelCollisionMap); + this.areaViewport?.bind(); + + // NO_TIMEOUT + this.out.p1isaac(ClientProt.NO_TIMEOUT); + for (let loc: LocEntity | null = this.locList.peekFront() as LocEntity | null; loc; loc = this.locList.prev() as LocEntity | null) { + if ((this.levelTileFlags && this.levelTileFlags[1][loc.heightmapNE][loc.heightmapNW] & 0x2) === 2) { + loc.heightmapSW--; + if (loc.heightmapSW < 0) { + loc.unlink(); + } + } + } + + for (let x: number = 0; x < CollisionMap.SIZE; x++) { + for (let z: number = 0; z < CollisionMap.SIZE; z++) { + this.sortObjStacks(x, z); + } + } + + for (let loc: LocTemporary | null = this.spawnedLocations.peekFront() as LocTemporary | null; loc; loc = this.spawnedLocations.prev() as LocTemporary | null) { + this.addLoc(loc.plane, loc.x, loc.z, loc.locIndex, loc.angle, loc.shape, loc.layer); + } + } catch (e) { + /* empty */ + } + LocType.modelCacheStatic?.clear(); + Draw3D.initPool(20); + }; + + private resetInterfaceAnimation = (id: number): void => { + const parent: ComType = ComType.instances[id]; + if (!parent.childId) { + return; + } + for (let i: number = 0; i < parent.childId.length && parent.childId[i] !== -1; i++) { + const child: ComType = ComType.instances[parent.childId[i]]; + if (child.type === 1) { + this.resetInterfaceAnimation(child.id); + } + child.seqFrame = 0; + child.seqCycle = 0; + } + }; + + private initializeLevelExperience = (): void => { + let acc: number = 0; + for (let i: number = 0; i < 99; i++) { + const level: number = i + 1; + const delta: number = (level + Math.pow(2.0, level / 7.0) * 300.0) | 0; + acc += delta; + this.levelExperience[i] = (acc / 4) | 0; + } + }; + + private addMessage = (type: number, text: string, sender: string): void => { + if (type === 0 && this.stickyChatInterfaceId !== -1) { + this.modalMessage = text; + this.mouseClickButton = 0; + } + if (this.chatInterfaceId === -1) { + this.redrawChatback = true; + } + for (let i: number = 99; i > 0; i--) { + this.messageType[i] = this.messageType[i - 1]; + this.messageSender[i] = this.messageSender[i - 1]; + this.messageText[i] = this.messageText[i - 1]; + } + this.messageType[0] = type; + this.messageSender[0] = sender; + this.messageText[0] = text; + }; + + private updateVarp = async (id: number): Promise => { + const clientcode: number = VarpType.instances[id].clientcode; + if (clientcode !== 0) { + const value: number = this.varps[id]; + if (clientcode === 1) { + if (value === 1) { + Draw3D.setBrightness(0.9); + } + if (value === 2) { + Draw3D.setBrightness(0.8); + } + if (value === 3) { + Draw3D.setBrightness(0.7); + } + if (value === 4) { + Draw3D.setBrightness(0.6); + } + ObjType.iconCache?.clear(); + this.redrawTitleBackground = true; + } + if (clientcode === 3) { + const lastMidiActive: boolean = this.midiActive; + if (value === 0) { + this.midiVolume = 256; + setMidiVolume(256); + this.midiActive = true; + } + if (value === 1) { + this.midiVolume = 192; + setMidiVolume(192); + this.midiActive = true; + } + if (value === 2) { + this.midiVolume = 128; + setMidiVolume(128); + this.midiActive = true; + } + if (value === 3) { + this.midiVolume = 64; + setMidiVolume(64); + this.midiActive = true; + } + if (value === 4) { + this.midiActive = false; + } + if (this.midiActive !== lastMidiActive) { + if (this.midiActive && this.currentMidi) { + await this.setMidi(this.currentMidi, this.midiCrc, this.midiSize); + } else { + stopMidi(); + } + this.nextMusicDelay = 0; + } + } + if (clientcode === 4) { + if (value === 0) { + this.waveVolume = 256; + setWaveVolume(256); + this.waveEnabled = true; + } + if (value === 1) { + this.waveVolume = 192; + setWaveVolume(192); + this.waveEnabled = true; + } + if (value === 2) { + this.waveVolume = 128; + setWaveVolume(128); + this.waveEnabled = true; + } + if (value === 3) { + this.waveVolume = 64; + setWaveVolume(64); + this.waveEnabled = true; + } + if (value === 4) { + this.waveEnabled = false; + } + } + if (clientcode === 5) { + this.mouseButtonsOption = value; + } + if (clientcode === 6) { + this.chatEffects = value; + } + if (clientcode === 8) { + this.splitPrivateChat = value; + this.redrawChatback = true; + } + } + }; + + private handleChatMouseInput = (_mouseX: number, mouseY: number): void => { + let line: number = 0; + for (let i: number = 0; i < 100; i++) { + if (!this.messageText[i]) { + continue; + } + + const type: number = this.messageType[i]; + const y: number = this.chatScrollOffset + 70 + 4 - line * 14; + if (y < -20) { + break; + } + + if (type === 0) { + line++; + } + + if ((type === 1 || type === 2) && (type === 1 || this.publicChatSetting === 0 || (this.publicChatSetting === 1 && this.isFriend(this.messageSender[i])))) { + if (mouseY > y - 14 && mouseY <= y && this.localPlayer && this.messageSender[i] !== this.localPlayer.name) { + if (this.rights) { + this.menuOption[this.menuSize] = 'Report abuse @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 34; + this.menuSize++; + } + + this.menuOption[this.menuSize] = 'Add ignore @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 436; + this.menuSize++; + this.menuOption[this.menuSize] = 'Add friend @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 406; + this.menuSize++; + } + + line++; + } + + if ((type === 3 || type === 7) && this.splitPrivateChat === 0 && (type === 7 || this.privateChatSetting === 0 || (this.privateChatSetting === 1 && this.isFriend(this.messageSender[i])))) { + if (mouseY > y - 14 && mouseY <= y) { + if (this.rights) { + this.menuOption[this.menuSize] = 'Report abuse @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 34; + this.menuSize++; + } + + this.menuOption[this.menuSize] = 'Add ignore @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 436; + this.menuSize++; + this.menuOption[this.menuSize] = 'Add friend @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 406; + this.menuSize++; + } + + line++; + } + + if (type === 4 && (this.tradeChatSetting === 0 || (this.tradeChatSetting === 1 && this.isFriend(this.messageSender[i])))) { + if (mouseY > y - 14 && mouseY <= y) { + this.menuOption[this.menuSize] = 'Accept trade @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 903; + this.menuSize++; + } + + line++; + } + + if ((type === 5 || type === 6) && this.splitPrivateChat === 0 && this.privateChatSetting < 2) { + line++; + } + + if (type === 8 && (this.tradeChatSetting === 0 || (this.tradeChatSetting === 1 && this.isFriend(this.messageSender[i])))) { + if (mouseY > y - 14 && mouseY <= y) { + this.menuOption[this.menuSize] = 'Accept duel @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 363; + this.menuSize++; + } + + line++; + } + } + }; + + private handlePrivateChatInput = (mouseY: number): void => { + if (this.splitPrivateChat == 0) { + return; + } + + let lineOffset: number = 0; + if (this.systemUpdateTimer != 0) { + lineOffset = 1; + } + + for (let i: number = 0; i < 100; i++) { + if (this.messageText[i] != null) { + const type: number = this.messageType[i]; + if ((type == 3 || type == 7) && (type == 7 || this.privateChatSetting == 0 || (this.privateChatSetting == 1 && this.isFriend(this.messageSender[i])))) { + const y: number = 329 - lineOffset * 13; + if (this.mouseX > 8 && this.mouseX < 520 && mouseY - 11 > y - 10 && mouseY - 11 <= y + 3) { + if (this.rights) { + this.menuOption[this.menuSize] = 'Report abuse @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 2034; + this.menuSize++; + } + this.menuOption[this.menuSize] = 'Add ignore @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 2436; + this.menuSize++; + this.menuOption[this.menuSize] = 'Add friend @whi@' + this.messageSender[i]; + this.menuAction[this.menuSize] = 2406; + this.menuSize++; + } + + lineOffset++; + if (lineOffset >= 5) { + return; + } + } + + if ((type == 5 || type == 6) && this.privateChatSetting < 2) { + lineOffset++; + if (lineOffset >= 5) { + return; + } + } + } + } + }; + + private handleInterfaceInput = (com: ComType, mouseX: number, mouseY: number, x: number, y: number, scrollPosition: number): void => { + if (com.type !== 0 || !com.childId || com.hide || mouseX < x || mouseY < y || mouseX > x + com.width || mouseY > y + com.height || !com.childX || !com.childY) { + return; + } + + const children: number = com.childId.length; + for (let i: number = 0; i < children; i++) { + let childX: number = com.childX[i] + x; + let childY: number = com.childY[i] + y - scrollPosition; + const child: ComType = ComType.instances[com.childId[i]]; + + childX += child.x; + childY += child.y; + + if ((child.overLayer >= 0 || child.overColour !== 0) && mouseX >= childX && mouseY >= childY && mouseX < childX + child.width && mouseY < childY + child.height) { + if (child.overLayer >= 0) { + this.lastHoveredInterfaceId = child.overLayer; + } else { + this.lastHoveredInterfaceId = child.id; + } + } + + if (child.type === 0) { + this.handleInterfaceInput(child, mouseX, mouseY, childX, childY, child.scrollPosition); + + if (child.scroll > child.height) { + this.handleScrollInput(mouseX, mouseY, child.scroll, child.height, true, childX + child.width, childY, child); + } + } else if (child.type === 2) { + let slot: number = 0; + + for (let row: number = 0; row < child.height; row++) { + for (let col: number = 0; col < child.width; col++) { + let slotX: number = childX + col * (child.marginX + 32); + let slotY: number = childY + row * (child.marginY + 32); + + if (slot < 20 && child.invSlotOffsetX && child.invSlotOffsetY) { + slotX += child.invSlotOffsetX[slot]; + slotY += child.invSlotOffsetY[slot]; + } + + if (mouseX < slotX || mouseY < slotY || mouseX >= slotX + 32 || mouseY >= slotY + 32) { + slot++; + continue; + } + + this.hoveredSlot = slot; + this.hoveredSlotParentId = child.id; + + if (!child.invSlotObjId || child.invSlotObjId[slot] <= 0) { + slot++; + continue; + } + + const obj: ObjType = ObjType.get(child.invSlotObjId[slot] - 1); + + if (this.objSelected === 1 && child.interactable) { + if (child.id !== this.objSelectedInterface || slot !== this.objSelectedSlot) { + this.menuOption[this.menuSize] = 'Use ' + this.objSelectedName + ' with @lre@' + obj.name; + this.menuAction[this.menuSize] = 881; + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = slot; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } + } else if (this.spellSelected === 1 && child.interactable) { + if ((this.activeSpellFlags & 0x10) === 16) { + this.menuOption[this.menuSize] = this.spellCaption + ' @lre@' + obj.name; + this.menuAction[this.menuSize] = 391; + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = slot; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } + } else { + if (child.interactable) { + for (let op: number = 4; op >= 3; op--) { + if (obj.iops && obj.iops[op]) { + this.menuOption[this.menuSize] = obj.iops[op] + ' @lre@' + obj.name; + if (op === 3) { + this.menuAction[this.menuSize] = 478; + } else if (op === 4) { + this.menuAction[this.menuSize] = 347; + } + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = slot; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } else if (op === 4) { + this.menuOption[this.menuSize] = 'Drop @lre@' + obj.name; + this.menuAction[this.menuSize] = 347; + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = slot; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } + } + } + + if (child.usable) { + this.menuOption[this.menuSize] = 'Use @lre@' + obj.name; + this.menuAction[this.menuSize] = 188; + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = slot; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } + + if (child.interactable && obj.iops) { + for (let op: number = 2; op >= 0; op--) { + if (obj.iops[op]) { + this.menuOption[this.menuSize] = obj.iops[op] + ' @lre@' + obj.name; + if (op === 0) { + this.menuAction[this.menuSize] = 405; + } else if (op === 1) { + this.menuAction[this.menuSize] = 38; + } else if (op === 2) { + this.menuAction[this.menuSize] = 422; + } + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = slot; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } + } + } + + if (child.iops) { + for (let op: number = 4; op >= 0; op--) { + if (child.iops[op]) { + this.menuOption[this.menuSize] = child.iops[op] + ' @lre@' + obj.name; + if (op === 0) { + this.menuAction[this.menuSize] = 602; + } else if (op === 1) { + this.menuAction[this.menuSize] = 596; + } else if (op === 2) { + this.menuAction[this.menuSize] = 22; + } else if (op === 3) { + this.menuAction[this.menuSize] = 892; + } else if (op === 4) { + this.menuAction[this.menuSize] = 415; + } + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = slot; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } + } + } + + this.menuOption[this.menuSize] = 'Examine @lre@' + obj.name; + this.menuAction[this.menuSize] = 1773; + this.menuParamA[this.menuSize] = obj.index; + if (child.invSlotObjCount) { + this.menuParamC[this.menuSize] = child.invSlotObjCount[slot]; + } + this.menuSize++; + } + + slot++; + } + } + } else if (mouseX >= childX && mouseY >= childY && mouseX < childX + child.width && mouseY < childY + child.height) { + if (child.buttonType === ComType.BUTTON_OK) { + let override: boolean = false; + if (child.clientCode !== 0) { + override = this.handleSocialMenuOption(child); + } + + if (!override && child.option) { + this.menuOption[this.menuSize] = child.option; + this.menuAction[this.menuSize] = 951; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } + } else if (child.buttonType === ComType.BUTTON_TARGET && this.spellSelected === 0) { + let prefix: string | null = child.actionVerb; + if (prefix && prefix.indexOf(' ') !== -1) { + prefix = prefix.substring(0, prefix.indexOf(' ')); + } + + this.menuOption[this.menuSize] = prefix + ' @gre@' + child.action; + this.menuAction[this.menuSize] = 930; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } else if (child.buttonType === ComType.BUTTON_CLOSE) { + this.menuOption[this.menuSize] = 'Close'; + this.menuAction[this.menuSize] = 947; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } else if (child.buttonType === ComType.BUTTON_TOGGLE && child.option) { + this.menuOption[this.menuSize] = child.option; + this.menuAction[this.menuSize] = 465; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } else if (child.buttonType === ComType.BUTTON_SELECT && child.option) { + this.menuOption[this.menuSize] = child.option; + this.menuAction[this.menuSize] = 960; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } else if (child.buttonType === ComType.BUTTON_CONTINUE && !this.pressedContinueOption && child.option) { + this.menuOption[this.menuSize] = child.option; + this.menuAction[this.menuSize] = 44; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } + } + } + }; + + private handleSocialMenuOption = (component: ComType): boolean => { + let type: number = component.clientCode; + if (type >= ComType.CC_FRIENDS_START && type <= ComType.CC_FRIENDS_UPDATE_END) { + if (type >= ComType.CC_FRIENDS_UPDATE_START) { + type -= ComType.CC_FRIENDS_UPDATE_START; + } else { + type--; + } + this.menuOption[this.menuSize] = 'Remove @whi@' + this.friendName[type]; + this.menuAction[this.menuSize] = 557; + this.menuSize++; + this.menuOption[this.menuSize] = 'Message @whi@' + this.friendName[type]; + this.menuAction[this.menuSize] = 679; + this.menuSize++; + return true; + } else if (type >= ComType.CC_IGNORES_START && type <= ComType.CC_IGNORES_END) { + this.menuOption[this.menuSize] = 'Remove @whi@' + component.text; + this.menuAction[this.menuSize] = 556; + this.menuSize++; + return true; + } + return false; + }; + + private handleViewportOptions = (): void => { + if (this.objSelected === 0 && this.spellSelected === 0) { + this.menuOption[this.menuSize] = 'Walk here'; + this.menuAction[this.menuSize] = 660; + this.menuParamB[this.menuSize] = this.mouseX; + this.menuParamC[this.menuSize] = this.mouseY; + this.menuSize++; + } + + let lastBitset: number = -1; + for (let picked: number = 0; picked < Model.pickedCount; picked++) { + const bitset: number = Model.pickedBitsets[picked]; + const x: number = bitset & 0x7f; + const z: number = (bitset >> 7) & 0x7f; + const entityType: number = (bitset >> 29) & 0x3; + const typeId: number = (bitset >> 14) & 0x7fff; + + if (bitset === lastBitset) { + continue; + } + + lastBitset = bitset; + + if (entityType === 2 && this.scene && this.scene.getInfo(this.currentLevel, x, z, bitset) >= 0) { + const loc: LocType = LocType.get(typeId); + if (this.objSelected === 1) { + this.menuOption[this.menuSize] = 'Use ' + this.objSelectedName + ' with @cya@' + loc.name; + this.menuAction[this.menuSize] = 450; + this.menuParamA[this.menuSize] = bitset; + this.menuParamB[this.menuSize] = x; + this.menuParamC[this.menuSize] = z; + this.menuSize++; + } else if (this.spellSelected !== 1) { + if (loc.ops) { + for (let op: number = 4; op >= 0; op--) { + if (loc.ops[op]) { + this.menuOption[this.menuSize] = loc.ops[op] + ' @cya@' + loc.name; + if (op === 0) { + this.menuAction[this.menuSize] = 285; + } + + if (op === 1) { + this.menuAction[this.menuSize] = 504; + } + + if (op === 2) { + this.menuAction[this.menuSize] = 364; + } + + if (op === 3) { + this.menuAction[this.menuSize] = 581; + } + + if (op === 4) { + this.menuAction[this.menuSize] = 1501; + } + + this.menuParamA[this.menuSize] = bitset; + this.menuParamB[this.menuSize] = x; + this.menuParamC[this.menuSize] = z; + this.menuSize++; + } + } + } + + this.menuOption[this.menuSize] = 'Examine @cya@' + loc.name; + this.menuAction[this.menuSize] = 1175; + this.menuParamA[this.menuSize] = bitset; + this.menuParamB[this.menuSize] = x; + this.menuParamC[this.menuSize] = z; + this.menuSize++; + } else if ((this.activeSpellFlags & 0x4) === 4) { + this.menuOption[this.menuSize] = this.spellCaption + ' @cya@' + loc.name; + this.menuAction[this.menuSize] = 55; + this.menuParamA[this.menuSize] = bitset; + this.menuParamB[this.menuSize] = x; + this.menuParamC[this.menuSize] = z; + this.menuSize++; + } + } + + if (entityType === 1) { + const npc: NpcEntity | null = this.npcs[typeId]; + if (npc && npc.type && npc.type.size === 1 && (npc.x & 0x7f) === 64 && (npc.z & 0x7f) === 64) { + for (let i: number = 0; i < this.npcCount; i++) { + const other: NpcEntity | null = this.npcs[this.npcIds[i]]; + + if (other && other !== npc && other.type && other.type.size === 1 && other.x === npc.x && other.z === npc.z) { + this.addNpcOptions(other.type, this.npcIds[i], x, z); + } + } + } + + if (npc && npc.type) { + this.addNpcOptions(npc.type, typeId, x, z); + } + } + + if (entityType === 0) { + const player: PlayerEntity | null = this.players[typeId]; + if (player && (player.x & 0x7f) === 64 && (player.z & 0x7f) === 64) { + for (let i: number = 0; i < this.npcCount; i++) { + const other: NpcEntity | null = this.npcs[this.npcIds[i]]; + + if (other && other.type && other.type.size === 1 && other.x === player.x && other.z === player.z) { + this.addNpcOptions(other.type, this.npcIds[i], x, z); + } + } + + for (let i: number = 0; i < this.playerCount; i++) { + const other: PlayerEntity | null = this.players[this.playerIds[i]]; + + if (other && other !== player && other.x === player.x && other.z === player.z) { + this.addPlayerOptions(other, this.playerIds[i], x, z); + } + } + } + + if (player) { + this.addPlayerOptions(player, typeId, x, z); + } + } + + if (entityType === 3) { + const objs: LinkList | null = this.levelObjStacks[this.currentLevel][x][z]; + if (!objs) { + continue; + } + + for (let obj: ObjStackEntity | null = objs.peekBack() as ObjStackEntity | null; obj; obj = objs.next() as ObjStackEntity | null) { + const type: ObjType = ObjType.get(obj.index); + if (this.objSelected === 1) { + this.menuOption[this.menuSize] = 'Use ' + this.objSelectedName + ' with @lre@' + type.name; + this.menuAction[this.menuSize] = 217; + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = x; + this.menuParamC[this.menuSize] = z; + this.menuSize++; + } else if (this.spellSelected !== 1) { + for (let op: number = 4; op >= 0; op--) { + if (type.ops && type.ops[op]) { + this.menuOption[this.menuSize] = type.ops[op] + ' @lre@' + type.name; + if (op === 0) { + this.menuAction[this.menuSize] = 224; + } + + if (op === 1) { + this.menuAction[this.menuSize] = 993; + } + + if (op === 2) { + this.menuAction[this.menuSize] = 99; + } + + if (op === 3) { + this.menuAction[this.menuSize] = 746; + } + + if (op === 4) { + this.menuAction[this.menuSize] = 877; + } + + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = x; + this.menuParamC[this.menuSize] = z; + this.menuSize++; + } else if (op === 2) { + this.menuOption[this.menuSize] = 'Take @lre@' + type.name; + this.menuAction[this.menuSize] = 99; + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = x; + this.menuParamC[this.menuSize] = z; + this.menuSize++; + } + } + + this.menuOption[this.menuSize] = 'Examine @lre@' + type.name; + this.menuAction[this.menuSize] = 1102; + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = x; + this.menuParamC[this.menuSize] = z; + this.menuSize++; + } else if ((this.activeSpellFlags & 0x1) === 1) { + this.menuOption[this.menuSize] = this.spellCaption + ' @lre@' + type.name; + this.menuAction[this.menuSize] = 965; + this.menuParamA[this.menuSize] = obj.index; + this.menuParamB[this.menuSize] = x; + this.menuParamC[this.menuSize] = z; + this.menuSize++; + } + } + } + } + }; + + private addNpcOptions = (npc: NpcType, a: number, b: number, c: number): void => { + if (this.menuSize >= 400) { + return; + } + + let tooltip: string | null = npc.name; + if (npc.vislevel !== 0 && this.localPlayer) { + tooltip = tooltip + this.getCombatLevelColorTag(this.localPlayer.combatLevel, npc.vislevel) + ' (level-' + npc.vislevel + ')'; + } + + if (this.objSelected === 1) { + this.menuOption[this.menuSize] = 'Use ' + this.objSelectedName + ' with @yel@' + tooltip; + this.menuAction[this.menuSize] = 900; + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } else if (this.spellSelected !== 1) { + let type: number; + if (npc.ops) { + for (type = 4; type >= 0; type--) { + if (npc.ops[type] && npc.ops[type]?.toLowerCase() !== 'attack') { + this.menuOption[this.menuSize] = npc.ops[type] + ' @yel@' + tooltip; + + if (type === 0) { + this.menuAction[this.menuSize] = 728; + } else if (type === 1) { + this.menuAction[this.menuSize] = 542; + } else if (type === 2) { + this.menuAction[this.menuSize] = 6; + } else if (type === 3) { + this.menuAction[this.menuSize] = 963; + } else if (type === 4) { + this.menuAction[this.menuSize] = 245; + } + + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } + } + } + + if (npc.ops) { + for (type = 4; type >= 0; type--) { + if (npc.ops[type] && npc.ops[type]?.toLowerCase() === 'attack') { + let action: number = 0; + if (this.localPlayer && npc.vislevel > this.localPlayer.combatLevel) { + action = 2000; + } + + this.menuOption[this.menuSize] = npc.ops[type] + ' @yel@' + tooltip; + + if (type === 0) { + this.menuAction[this.menuSize] = action + 728; + } else if (type === 1) { + this.menuAction[this.menuSize] = action + 542; + } else if (type === 2) { + this.menuAction[this.menuSize] = action + 6; + } else if (type === 3) { + this.menuAction[this.menuSize] = action + 963; + } else if (type === 4) { + this.menuAction[this.menuSize] = action + 245; + } + + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } + } + } + + this.menuOption[this.menuSize] = 'Examine @yel@' + tooltip; + this.menuAction[this.menuSize] = 1607; + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } else if ((this.activeSpellFlags & 0x2) === 2) { + this.menuOption[this.menuSize] = this.spellCaption + ' @yel@' + tooltip; + this.menuAction[this.menuSize] = 265; + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } + }; + + private addPlayerOptions = (player: PlayerEntity, a: number, b: number, c: number): void => { + if (player === this.localPlayer || this.menuSize >= 400) { + return; + } + + let tooltip: string | null = null; + if (this.localPlayer) { + tooltip = player.name + this.getCombatLevelColorTag(this.localPlayer.combatLevel, player.combatLevel) + ' (level-' + player.combatLevel + ')'; + } + if (this.objSelected === 1) { + this.menuOption[this.menuSize] = 'Use ' + this.objSelectedName + ' with @whi@' + tooltip; + this.menuAction[this.menuSize] = 367; + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } else if (this.spellSelected !== 1) { + this.menuOption[this.menuSize] = 'Follow @whi@' + tooltip; + this.menuAction[this.menuSize] = 1544; + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + + if (this.overrideChat === 0) { + this.menuOption[this.menuSize] = 'Trade with @whi@' + tooltip; + this.menuAction[this.menuSize] = 1373; + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } + + if (this.wildernessLevel > 0) { + this.menuOption[this.menuSize] = 'Attack @whi@' + tooltip; + if (this.localPlayer && this.localPlayer.combatLevel >= player.combatLevel) { + this.menuAction[this.menuSize] = 151; + } else { + this.menuAction[this.menuSize] = 2151; + } + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } + + if (this.worldLocationState === 1) { + this.menuOption[this.menuSize] = 'Fight @whi@' + tooltip; + this.menuAction[this.menuSize] = 151; + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } + + if (this.worldLocationState === 2) { + this.menuOption[this.menuSize] = 'Duel-with @whi@' + tooltip; + this.menuAction[this.menuSize] = 1101; + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } + } else if ((this.activeSpellFlags & 0x8) === 8) { + this.menuOption[this.menuSize] = this.spellCaption + ' @whi@' + tooltip; + this.menuAction[this.menuSize] = 651; + this.menuParamA[this.menuSize] = a; + this.menuParamB[this.menuSize] = b; + this.menuParamC[this.menuSize] = c; + this.menuSize++; + } + + for (let i: number = 0; i < this.menuSize; i++) { + if (this.menuAction[i] === 660) { + this.menuOption[i] = 'Walk here @whi@' + tooltip; + return; + } + } + }; + + private getCombatLevelColorTag = (viewerLevel: number, otherLevel: number): string => { + const diff: number = viewerLevel - otherLevel; + if (diff < -9) { + return '@red@'; + } else if (diff < -6) { + return '@or3@'; + } else if (diff < -3) { + return '@or2@'; + } else if (diff < 0) { + return '@or1@'; + } else if (diff > 9) { + return '@gre@'; + } else if (diff > 6) { + return '@gr3@'; + } else if (diff > 3) { + return '@gr2@'; + } else if (diff > 0) { + return '@gr1@'; + } else { + return '@yel@'; + } + }; + + private handleInput = (): void => { + if (this.objDragArea === 0) { + this.menuOption[0] = 'Cancel'; + this.menuAction[0] = 1252; + this.menuSize = 1; + this.handlePrivateChatInput(this.mouseY); + this.lastHoveredInterfaceId = 0; + + // the main viewport area + if (this.mouseX > 8 && this.mouseY > 11 && this.mouseX < 520 && this.mouseY < 345) { + if (this.viewportInterfaceId === -1) { + this.handleViewportOptions(); + } else { + this.handleInterfaceInput(ComType.instances[this.viewportInterfaceId], this.mouseX, this.mouseY, 8, 11, 0); + } + } + + if (this.lastHoveredInterfaceId !== this.viewportHoveredInterfaceIndex) { + this.viewportHoveredInterfaceIndex = this.lastHoveredInterfaceId; + } + + this.lastHoveredInterfaceId = 0; + + // the sidebar/tabs area + if (this.mouseX > 562 && this.mouseY > 231 && this.mouseX < 752 && this.mouseY < 492) { + if (this.sidebarInterfaceId !== -1) { + this.handleInterfaceInput(ComType.instances[this.sidebarInterfaceId], this.mouseX, this.mouseY, 562, 231, 0); + } else if (this.tabInterfaceId[this.selectedTab] !== -1) { + this.handleInterfaceInput(ComType.instances[this.tabInterfaceId[this.selectedTab]], this.mouseX, this.mouseY, 562, 231, 0); + } + } + + if (this.lastHoveredInterfaceId !== this.sidebarHoveredInterfaceIndex) { + this.redrawSidebar = true; + this.sidebarHoveredInterfaceIndex = this.lastHoveredInterfaceId; + } + + this.lastHoveredInterfaceId = 0; + + // the chatbox area + if (this.mouseX > 22 && this.mouseY > 375 && this.mouseX < 431 && this.mouseY < 471) { + if (this.chatInterfaceId === -1) { + this.handleChatMouseInput(this.mouseX - 22, this.mouseY - 375); + } else { + this.handleInterfaceInput(ComType.instances[this.chatInterfaceId], this.mouseX, this.mouseY, 22, 375, 0); + } + } + + if (this.chatInterfaceId !== -1 && this.lastHoveredInterfaceId !== this.chatHoveredInterfaceIndex) { + this.redrawChatback = true; + this.chatHoveredInterfaceIndex = this.lastHoveredInterfaceId; + } + + let done: boolean = false; + while (!done) { + done = true; + + for (let i: number = 0; i < this.menuSize - 1; i++) { + if (this.menuAction[i] < 1000 && this.menuAction[i + 1] > 1000) { + const tmp0: string = this.menuOption[i]; + this.menuOption[i] = this.menuOption[i + 1]; + this.menuOption[i + 1] = tmp0; + + const tmp1: number = this.menuAction[i]; + this.menuAction[i] = this.menuAction[i + 1]; + this.menuAction[i + 1] = tmp1; + + const tmp2: number = this.menuParamB[i]; + this.menuParamB[i] = this.menuParamB[i + 1]; + this.menuParamB[i + 1] = tmp2; + + const tmp3: number = this.menuParamC[i]; + this.menuParamC[i] = this.menuParamC[i + 1]; + this.menuParamC[i + 1] = tmp3; + + const tmp4: number = this.menuParamA[i]; + this.menuParamA[i] = this.menuParamA[i + 1]; + this.menuParamA[i + 1] = tmp4; + + done = false; + } + } + } + } + }; + + private showContextMenu = (): void => { + let width: number = 0; + if (this.fontBold12) { + width = this.fontBold12.stringWidth('Choose Option'); + let maxWidth: number; + for (let i: number = 0; i < this.menuSize; i++) { + maxWidth = this.fontBold12.stringWidth(this.menuOption[i]); + if (maxWidth > width) { + width = maxWidth; + } + } + } + width += 8; + + const height: number = this.menuSize * 15 + 21; + + let x: number; + let y: number; + + // the main viewport area + if (this.mouseClickX > 8 && this.mouseClickY > 11 && this.mouseClickX < 520 && this.mouseClickY < 345) { + x = this.mouseClickX - ((width / 2) | 0) - 8; + if (x + width > 512) { + x = 512 - width; + } else if (x < 0) { + x = 0; + } + + y = this.mouseClickY - 11; + if (y + height > 334) { + y = 334 - height; + } else if (y < 0) { + y = 0; + } + + this.menuVisible = true; + this.menuArea = 0; + this.menuX = x; + this.menuY = y; + this.menuWidth = width; + this.menuHeight = this.menuSize * 15 + 22; + } + + // the sidebar/tabs area + if (this.mouseClickX > 562 && this.mouseClickY > 231 && this.mouseClickX < 752 && this.mouseClickY < 492) { + x = this.mouseClickX - ((width / 2) | 0) - 562; + if (x < 0) { + x = 0; + } else if (x + width > 190) { + x = 190 - width; + } + + y = this.mouseClickY - 231; + if (y < 0) { + y = 0; + } else if (y + height > 261) { + y = 261 - height; + } + + this.menuVisible = true; + this.menuArea = 1; + this.menuX = x; + this.menuY = y; + this.menuWidth = width; + this.menuHeight = this.menuSize * 15 + 22; + } + + // the chatbox area + if (this.mouseClickX > 22 && this.mouseClickY > 375 && this.mouseClickX < 501 && this.mouseClickY < 471) { + x = this.mouseClickX - ((width / 2) | 0) - 22; + if (x < 0) { + x = 0; + } else if (x + width > 479) { + x = 479 - width; + } + + y = this.mouseClickY - 375; + if (y < 0) { + y = 0; + } else if (y + height > 96) { + y = 96 - height; + } + + this.menuVisible = true; + this.menuArea = 2; + this.menuX = x; + this.menuY = y; + this.menuWidth = width; + this.menuHeight = this.menuSize * 15 + 22; + } + }; + + private tryMove = (srcX: number, srcZ: number, dx: number, dz: number, type: number, locWidth: number, locLength: number, locAngle: number, locShape: number, forceapproach: number, tryNearest: boolean): boolean => { + const collisionMap: CollisionMap | null = this.levelCollisionMap[this.currentLevel]; + if (!collisionMap) { + return false; + } + + const sceneWidth: number = CollisionMap.SIZE; + const sceneLength: number = CollisionMap.SIZE; + + for (let x: number = 0; x < sceneWidth; x++) { + for (let z: number = 0; z < sceneLength; z++) { + const index: number = CollisionMap.index(x, z); + this.bfsDirection[index] = 0; + this.bfsCost[index] = 99999999; + } + } + + let x: number = srcX; + let z: number = srcZ; + + const srcIndex: number = CollisionMap.index(srcX, srcZ); + this.bfsDirection[srcIndex] = 99; + this.bfsCost[srcIndex] = 0; + + let steps: number = 0; + let length: number = 0; + + this.bfsStepX[steps] = srcX; + this.bfsStepZ[steps++] = srcZ; + + let arrived: boolean = false; + let bufferSize: number = this.bfsStepX.length; + const flags: Int32Array = collisionMap.flags; + + while (length !== steps) { + x = this.bfsStepX[length]; + z = this.bfsStepZ[length]; + length = (length + 1) % bufferSize; + + if (x === dx && z === dz) { + arrived = true; + break; + } + + if (locShape !== LocShape.WALL_STRAIGHT) { + if ((locShape < LocShape.WALLDECOR_STRAIGHT_OFFSET || locShape === LocShape.CENTREPIECE_STRAIGHT) && collisionMap.reachedWall(x, z, dx, dz, locShape - 1, locAngle)) { + arrived = true; + break; + } + + if (locShape < LocShape.CENTREPIECE_STRAIGHT && collisionMap.reachedWallDecoration(x, z, dx, dz, locShape - 1, locAngle)) { + arrived = true; + break; + } + } + + if (locWidth !== 0 && locLength !== 0 && collisionMap.reachedLoc(x, z, dx, dz, locWidth, locLength, forceapproach)) { + arrived = true; + break; + } + + const nextCost: number = this.bfsCost[CollisionMap.index(x, z)] + 1; + let index: number = CollisionMap.index(x - 1, z); + if (x > 0 && this.bfsDirection[index] === 0 && (flags[index] & CollisionFlag.BLOCK_WEST) === CollisionFlag.OPEN) { + this.bfsStepX[steps] = x - 1; + this.bfsStepZ[steps] = z; + steps = (steps + 1) % bufferSize; + this.bfsDirection[index] = 2; + this.bfsCost[index] = nextCost; + } + + index = CollisionMap.index(x + 1, z); + if (x < sceneWidth - 1 && this.bfsDirection[index] === 0 && (flags[index] & CollisionFlag.BLOCK_EAST) === CollisionFlag.OPEN) { + this.bfsStepX[steps] = x + 1; + this.bfsStepZ[steps] = z; + steps = (steps + 1) % bufferSize; + this.bfsDirection[index] = 8; + this.bfsCost[index] = nextCost; + } + + index = CollisionMap.index(x, z - 1); + if (z > 0 && this.bfsDirection[index] === 0 && (flags[index] & CollisionFlag.BLOCK_SOUTH) === CollisionFlag.OPEN) { + this.bfsStepX[steps] = x; + this.bfsStepZ[steps] = z - 1; + steps = (steps + 1) % bufferSize; + this.bfsDirection[index] = 1; + this.bfsCost[index] = nextCost; + } + + index = CollisionMap.index(x, z + 1); + if (z < sceneLength - 1 && this.bfsDirection[index] === 0 && (flags[index] & CollisionFlag.BLOCK_NORTH) === CollisionFlag.OPEN) { + this.bfsStepX[steps] = x; + this.bfsStepZ[steps] = z + 1; + steps = (steps + 1) % bufferSize; + this.bfsDirection[index] = 4; + this.bfsCost[index] = nextCost; + } + + index = CollisionMap.index(x - 1, z - 1); + if ( + x > 0 && + z > 0 && + this.bfsDirection[index] === 0 && + (flags[index] & CollisionFlag.BLOCK_SOUTH_WEST) === 0 && + (flags[CollisionMap.index(x - 1, z)] & CollisionFlag.BLOCK_WEST) === CollisionFlag.OPEN && + (flags[CollisionMap.index(x, z - 1)] & CollisionFlag.BLOCK_SOUTH) === CollisionFlag.OPEN + ) { + this.bfsStepX[steps] = x - 1; + this.bfsStepZ[steps] = z - 1; + steps = (steps + 1) % bufferSize; + this.bfsDirection[index] = 3; + this.bfsCost[index] = nextCost; + } + + index = CollisionMap.index(x + 1, z - 1); + if ( + x < sceneWidth - 1 && + z > 0 && + this.bfsDirection[index] === 0 && + (flags[index] & CollisionFlag.BLOCK_SOUTH_EAST) === 0 && + (flags[CollisionMap.index(x + 1, z)] & CollisionFlag.BLOCK_EAST) === CollisionFlag.OPEN && + (flags[CollisionMap.index(x, z - 1)] & CollisionFlag.BLOCK_SOUTH) === CollisionFlag.OPEN + ) { + this.bfsStepX[steps] = x + 1; + this.bfsStepZ[steps] = z - 1; + steps = (steps + 1) % bufferSize; + this.bfsDirection[index] = 9; + this.bfsCost[index] = nextCost; + } + + index = CollisionMap.index(x - 1, z + 1); + if ( + x > 0 && + z < sceneLength - 1 && + this.bfsDirection[index] === 0 && + (flags[index] & CollisionFlag.BLOCK_NORTH_WEST) === 0 && + (flags[CollisionMap.index(x - 1, z)] & CollisionFlag.BLOCK_WEST) === CollisionFlag.OPEN && + (flags[CollisionMap.index(x, z + 1)] & CollisionFlag.BLOCK_NORTH) === CollisionFlag.OPEN + ) { + this.bfsStepX[steps] = x - 1; + this.bfsStepZ[steps] = z + 1; + steps = (steps + 1) % bufferSize; + this.bfsDirection[index] = 6; + this.bfsCost[index] = nextCost; + } + + index = CollisionMap.index(x + 1, z + 1); + if ( + x < sceneWidth - 1 && + z < sceneLength - 1 && + this.bfsDirection[index] === 0 && + (flags[index] & CollisionFlag.BLOCK_NORTH_EAST) === 0 && + (flags[CollisionMap.index(x + 1, z)] & CollisionFlag.BLOCK_EAST) === CollisionFlag.OPEN && + (flags[CollisionMap.index(x, z + 1)] & CollisionFlag.BLOCK_NORTH) === CollisionFlag.OPEN + ) { + this.bfsStepX[steps] = x + 1; + this.bfsStepZ[steps] = z + 1; + steps = (steps + 1) % bufferSize; + this.bfsDirection[index] = 12; + this.bfsCost[index] = nextCost; + } + } + + this.tryMoveNearest = 0; + + if (!arrived) { + if (tryNearest) { + let min: number = 100; + for (let padding: number = 1; padding < 2; padding++) { + for (let px: number = dx - padding; px <= dx + padding; px++) { + for (let pz: number = dz - padding; pz <= dz + padding; pz++) { + const index: number = CollisionMap.index(px, pz); + if (px >= 0 && pz >= 0 && px < CollisionMap.SIZE && pz < CollisionMap.SIZE && this.bfsCost[index] < min) { + min = this.bfsCost[index]; + x = px; + z = pz; + this.tryMoveNearest = 1; + arrived = true; + } + } + } + + if (arrived) { + break; + } + } + } + + if (!arrived) { + return false; + } + } + + length = 0; + this.bfsStepX[length] = x; + this.bfsStepZ[length++] = z; + + let dir: number = this.bfsDirection[CollisionMap.index(x, z)]; + let next: number = dir; + while (x !== srcX || z !== srcZ) { + if (next !== dir) { + dir = next; + this.bfsStepX[length] = x; + this.bfsStepZ[length++] = z; + } + + if ((next & 0x2) !== 0) { + x++; + } else if ((next & 0x8) !== 0) { + x--; + } + + if ((next & 0x1) !== 0) { + z++; + } else if ((next & 0x4) !== 0) { + z--; + } + + next = this.bfsDirection[CollisionMap.index(x, z)]; + } + + if (length > 0) { + bufferSize = Math.min(length, 25); // max number of turns in a single pf request + length--; + + const startX: number = this.bfsStepX[length]; + const startZ: number = this.bfsStepZ[length]; + + if (type === 0) { + this.out.p1isaac(ClientProt.MOVE_GAMECLICK); + this.out.p1(bufferSize + bufferSize + 3); + } else if (type === 1) { + this.out.p1isaac(ClientProt.MOVE_MINIMAPCLICK); + this.out.p1(bufferSize + bufferSize + 3 + 14); + } else if (type === 2) { + this.out.p1isaac(ClientProt.MOVE_OPCLICK); + this.out.p1(bufferSize + bufferSize + 3); + } + + if (this.actionKey[5] === 1) { + this.out.p1(1); + } else { + this.out.p1(0); + } + + this.out.p2(startX + this.sceneBaseTileX); + this.out.p2(startZ + this.sceneBaseTileZ); + this.flagSceneTileX = this.bfsStepX[0]; + this.flagSceneTileZ = this.bfsStepZ[0]; + + for (let i: number = 1; i < bufferSize; i++) { + length--; + this.out.p1(this.bfsStepX[length] - startX); + this.out.p1(this.bfsStepZ[length] - startZ); + } + + return true; + } + + return type !== 1; + }; + + private readPlayerInfo = (buf: Packet, size: number): void => { + this.entityRemovalCount = 0; + this.entityUpdateCount = 0; + + this.readLocalPlayer(buf); + this.readPlayers(buf); + this.readNewPlayers(buf, size); + this.readPlayerUpdates(buf); + + for (let i: number = 0; i < this.entityRemovalCount; i++) { + const index: number = this.entityRemovalIds[i]; + const player: PlayerEntity | null = this.players[index]; + if (!player) { + continue; + } + if (player.cycle !== this.loopCycle) { + this.players[index] = null; + } + } + + if (buf.pos !== size) { + throw new Error(`eek! Error packet size mismatch in getplayer pos:${buf.pos} psize:${size}`); + } + for (let index: number = 0; index < this.playerCount; index++) { + if (!this.players[this.playerIds[index]]) { + throw new Error(`eek! ${this.username} null entry in pl list - pos:${index} size:${this.playerCount}`); + } + } + }; + + private readLocalPlayer = (buf: Packet): void => { + buf.bits(); + + const hasUpdate: number = buf.gBit(1); + if (hasUpdate !== 0) { + const updateType: number = buf.gBit(2); + + if (updateType === 0) { + this.entityUpdateIds[this.entityUpdateCount++] = this.LOCAL_PLAYER_INDEX; + } else if (updateType === 1) { + const walkDir: number = buf.gBit(3); + this.localPlayer?.step(false, walkDir); + + const hasMaskUpdate: number = buf.gBit(1); + if (hasMaskUpdate === 1) { + this.entityUpdateIds[this.entityUpdateCount++] = this.LOCAL_PLAYER_INDEX; + } + } else if (updateType === 2) { + const walkDir: number = buf.gBit(3); + this.localPlayer?.step(true, walkDir); + const runDir: number = buf.gBit(3); + this.localPlayer?.step(true, runDir); + + const hasMaskUpdate: number = buf.gBit(1); + if (hasMaskUpdate === 1) { + this.entityUpdateIds[this.entityUpdateCount++] = this.LOCAL_PLAYER_INDEX; + } + } else if (updateType === 3) { + this.currentLevel = buf.gBit(2); + const localX: number = buf.gBit(7); + const localZ: number = buf.gBit(7); + const jump: number = buf.gBit(1); + this.localPlayer?.move(jump === 1, localX, localZ); + + const hasMaskUpdate: number = buf.gBit(1); + if (hasMaskUpdate === 1) { + this.entityUpdateIds[this.entityUpdateCount++] = this.LOCAL_PLAYER_INDEX; + } + } + } + }; + + private readPlayers = (buf: Packet): void => { + const count: number = buf.gBit(8); + + if (count < this.playerCount) { + for (let i: number = count; i < this.playerCount; i++) { + this.entityRemovalIds[this.entityRemovalCount++] = this.playerIds[i]; + } + } + + if (count > this.playerCount) { + throw new Error(`eek! ${this.username} Too many players`); + } + + this.playerCount = 0; + for (let i: number = 0; i < count; i++) { + const index: number = this.playerIds[i]; + const player: PlayerEntity | null = this.players[index]; + + const hasUpdate: number = buf.gBit(1); + if (hasUpdate === 0) { + this.playerIds[this.playerCount++] = index; + if (player) { + player.cycle = this.loopCycle; + } + } else { + const updateType: number = buf.gBit(2); + + if (updateType === 0) { + this.playerIds[this.playerCount++] = index; + if (player) { + player.cycle = this.loopCycle; + } + this.entityUpdateIds[this.entityUpdateCount++] = index; + } else if (updateType === 1) { + this.playerIds[this.playerCount++] = index; + if (player) { + player.cycle = this.loopCycle; + } + + const walkDir: number = buf.gBit(3); + player?.step(false, walkDir); + + const hasMaskUpdate: number = buf.gBit(1); + if (hasMaskUpdate === 1) { + this.entityUpdateIds[this.entityUpdateCount++] = index; + } + } else if (updateType === 2) { + this.playerIds[this.playerCount++] = index; + if (player) { + player.cycle = this.loopCycle; + } + + const walkDir: number = buf.gBit(3); + player?.step(true, walkDir); + const runDir: number = buf.gBit(3); + player?.step(true, runDir); + + const hasMaskUpdate: number = buf.gBit(1); + if (hasMaskUpdate === 1) { + this.entityUpdateIds[this.entityUpdateCount++] = index; + } + } else if (updateType === 3) { + this.entityRemovalIds[this.entityRemovalCount++] = index; + } + } + } + }; + + private readNewPlayers = (buf: Packet, size: number): void => { + let index: number; + while (buf.bitPos + 10 < size * 8) { + index = buf.gBit(11); + if (index === 2047) { + break; + } + + if (!this.players[index]) { + this.players[index] = new PlayerEntity(); + const appearance: Packet | null = this.playerAppearanceBuffer[index]; + if (appearance) { + this.players[index]?.read(appearance); + } + } + + this.playerIds[this.playerCount++] = index; + const player: PlayerEntity | null = this.players[index]; + if (player) { + player.cycle = this.loopCycle; + } + let dx: number = buf.gBit(5); + if (dx > 15) { + dx -= 32; + } + let dz: number = buf.gBit(5); + if (dz > 15) { + dz -= 32; + } + const jump: number = buf.gBit(1); + if (this.localPlayer) { + player?.move(jump === 1, this.localPlayer.pathTileX[0] + dx, this.localPlayer.pathTileZ[0] + dz); + } + + const hasMaskUpdate: number = buf.gBit(1); + if (hasMaskUpdate === 1) { + this.entityUpdateIds[this.entityUpdateCount++] = index; + } + } + + buf.bytes(); + }; + + private readPlayerUpdates = (buf: Packet): void => { + for (let i: number = 0; i < this.entityUpdateCount; i++) { + const index: number = this.entityUpdateIds[i]; + const player: PlayerEntity | null = this.players[index]; + if (!player) { + continue; // its fine cos buffer gets out of pos and throws error which is ok + } + let mask: number = buf.g1; + if ((mask & 0x80) === 128) { + mask += buf.g1 << 8; + } + this.readPlayerUpdatesBlocks(player, index, mask, buf); + } + }; + + private readPlayerUpdatesBlocks = (player: PlayerEntity, index: number, mask: number, buf: Packet): void => { + player.lastMask = mask; + player.lastMaskCycle = this.loopCycle; + + if ((mask & 0x1) === 1) { + const length: number = buf.g1; + const data: Uint8Array = new Uint8Array(length); + const appearance: Packet = new Packet(data); + buf.gdata(length, 0, data); + this.playerAppearanceBuffer[index] = appearance; + player.read(appearance); + } + if ((mask & 0x2) === 2) { + let seqId: number = buf.g2; + if (seqId === 65535) { + seqId = -1; + } + if (seqId === player.primarySeqId) { + player.primarySeqLoop = 0; + } + const delay: number = buf.g1; + if (seqId === -1 || player.primarySeqId === -1 || SeqType.instances[seqId].priority > SeqType.instances[player.primarySeqId].priority || SeqType.instances[player.primarySeqId].priority === 0) { + player.primarySeqId = seqId; + player.primarySeqFrame = 0; + player.primarySeqCycle = 0; + player.primarySeqDelay = delay; + player.primarySeqLoop = 0; + } + } + if ((mask & 0x4) === 4) { + player.targetId = buf.g2; + if (player.targetId === 65535) { + player.targetId = -1; + } + } + if ((mask & 0x8) === 8) { + player.chat = buf.gjstr; + player.chatColor = 0; + player.chatStyle = 0; + player.chatTimer = 150; + if (player.name) { + this.addMessage(2, player.chat, player.name); + } + } + if ((mask & 0x10) === 16) { + player.damage = buf.g1; + player.damageType = buf.g1; + player.combatCycle = this.loopCycle + 400; + player.health = buf.g1; + player.totalHealth = buf.g1; + } + if ((mask & 0x20) === 32) { + player.targetTileX = buf.g2; + player.targetTileZ = buf.g2; + player.lastFaceX = player.targetTileX; + player.lastFaceZ = player.targetTileZ; + } + if ((mask & 0x40) === 64) { + const colorEffect: number = buf.g2; + const type: number = buf.g1; + const length: number = buf.g1; + const start: number = buf.pos; + if (player.name) { + const username: bigint = JString.toBase37(player.name); + let ignored: boolean = false; + if (type <= 1) { + for (let i: number = 0; i < this.ignoreCount; i++) { + if (this.ignoreName37[i] === username) { + ignored = true; + break; + } + } + } + if (!ignored && this.overrideChat === 0) { + try { + const uncompressed: string = WordPack.unpack(buf, length); + const filtered: string = WordFilter.filter(uncompressed); + player.chat = filtered; + player.chatColor = colorEffect >> 8; + player.chatStyle = colorEffect & 0xff; + player.chatTimer = 150; + if (type > 1) { + this.addMessage(1, filtered, player.name); + } else { + this.addMessage(2, filtered, player.name); + } + } catch (e) { + // signlink.reporterror("cde2"); + } + } + } + buf.pos = start + length; + } + if ((mask & 0x100) === 256) { + player.spotanimId = buf.g2; + const heightDelay: number = buf.g4; + player.spotanimOffset = heightDelay >> 16; + player.spotanimLastCycle = this.loopCycle + (heightDelay & 0xffff); + player.spotanimFrame = 0; + player.spotanimCycle = 0; + if (player.spotanimLastCycle > this.loopCycle) { + player.spotanimFrame = -1; + } + if (player.spotanimId === 65535) { + player.spotanimId = -1; + } + } + if ((mask & 0x200) === 512) { + player.forceMoveStartSceneTileX = buf.g1; + player.forceMoveStartSceneTileZ = buf.g1; + player.forceMoveEndSceneTileX = buf.g1; + player.forceMoveEndSceneTileZ = buf.g1; + player.forceMoveEndCycle = buf.g2 + this.loopCycle; + player.forceMoveStartCycle = buf.g2 + this.loopCycle; + player.forceMoveFaceDirection = buf.g1; + player.pathLength = 0; + player.pathTileX[0] = player.forceMoveEndSceneTileX; + player.pathTileZ[0] = player.forceMoveEndSceneTileZ; + } + }; + + private readNpcInfo = (buf: Packet, size: number): void => { + this.entityRemovalCount = 0; + this.entityUpdateCount = 0; + + this.readNpcs(buf); + this.readNewNpcs(buf, size); + this.readNpcUpdates(buf); + + for (let i: number = 0; i < this.entityRemovalCount; i++) { + const index: number = this.entityRemovalIds[i]; + const npc: NpcEntity | null = this.npcs[index]; + if (!npc) { + continue; + } + if (npc.cycle !== this.loopCycle) { + npc.type = null; + this.npcs[index] = null; + } + } + + if (buf.pos !== size) { + throw new Error(`eek! ${this.username} size mismatch in getnpcpos - pos:${buf.pos} psize:${size}`); + } + + for (let i: number = 0; i < this.npcCount; i++) { + if (!this.npcs[this.npcIds[i]]) { + throw new Error(`eek! ${this.username} null entry in npc list - pos:${i} size:${this.npcCount}`); + } + } + }; + + private readNpcs = (buf: Packet): void => { + buf.bits(); + + const count: number = buf.gBit(8); + if (count < this.npcCount) { + for (let i: number = count; i < this.npcCount; i++) { + this.entityRemovalIds[this.entityRemovalCount++] = this.npcIds[i]; + } + } + + if (count > this.npcCount) { + throw new Error(`eek! ${this.username} Too many npc!`); + } + + this.npcCount = 0; + for (let i: number = 0; i < count; i++) { + const index: number = this.npcIds[i]; + const npc: NpcEntity | null = this.npcs[index]; + + const hasUpdate: number = buf.gBit(1); + if (hasUpdate === 0) { + this.npcIds[this.npcCount++] = index; + if (npc) { + npc.cycle = this.loopCycle; + } + } else { + const updateType: number = buf.gBit(2); + + if (updateType === 0) { + this.npcIds[this.npcCount++] = index; + if (npc) { + npc.cycle = this.loopCycle; + } + this.entityUpdateIds[this.entityUpdateCount++] = index; + } else if (updateType === 1) { + this.npcIds[this.npcCount++] = index; + if (npc) { + npc.cycle = this.loopCycle; + } + + const walkDir: number = buf.gBit(3); + npc?.step(false, walkDir); + + const hasMaskUpdate: number = buf.gBit(1); + if (hasMaskUpdate === 1) { + this.entityUpdateIds[this.entityUpdateCount++] = index; + } + } else if (updateType === 2) { + this.npcIds[this.npcCount++] = index; + if (npc) { + npc.cycle = this.loopCycle; + } + + const walkDir: number = buf.gBit(3); + npc?.step(true, walkDir); + const runDir: number = buf.gBit(3); + npc?.step(true, runDir); + + const hasMaskUpdate: number = buf.gBit(1); + if (hasMaskUpdate === 1) { + this.entityUpdateIds[this.entityUpdateCount++] = index; + } + } else if (updateType === 3) { + this.entityRemovalIds[this.entityRemovalCount++] = index; + } + } + } + }; + + private readNewNpcs = (buf: Packet, size: number): void => { + while (buf.bitPos + 21 < size * 8) { + const index: number = buf.gBit(13); + if (index === 8191) { + break; + } + if (!this.npcs[index]) { + this.npcs[index] = new NpcEntity(); + } + const npc: NpcEntity | null = this.npcs[index]; + this.npcIds[this.npcCount++] = index; + if (npc) { + npc.cycle = this.loopCycle; + npc.type = NpcType.get(buf.gBit(11)); + npc.size = npc.type.size; + npc.seqWalkId = npc.type.walkanim; + npc.seqTurnAroundId = npc.type.walkanim_b; + npc.seqTurnLeftId = npc.type.walkanim_r; + npc.seqTurnRightId = npc.type.walkanim_l; + npc.seqStandId = npc.type.readyanim; + } else { + buf.gBit(11); + } + let dx: number = buf.gBit(5); + if (dx > 15) { + dx -= 32; + } + let dz: number = buf.gBit(5); + if (dz > 15) { + dz -= 32; + } + if (this.localPlayer) { + npc?.move(false, this.localPlayer.pathTileX[0] + dx, this.localPlayer.pathTileZ[0] + dz); + } + const update: number = buf.gBit(1); + if (update === 1) { + this.entityUpdateIds[this.entityUpdateCount++] = index; + } + } + buf.bytes(); + }; + + private readNpcUpdates = (buf: Packet): void => { + for (let i: number = 0; i < this.entityUpdateCount; i++) { + const id: number = this.entityUpdateIds[i]; + const npc: NpcEntity | null = this.npcs[id]; + if (!npc) { + continue; // its fine cos buffer gets out of pos and throws error which is ok + } + const mask: number = buf.g1; + + npc.lastMask = mask; + npc.lastMaskCycle = this.loopCycle; + + if ((mask & 0x2) === 2) { + let seqId: number = buf.g2; + if (seqId === 65535) { + seqId = -1; + } + if (seqId === npc.primarySeqId) { + npc.primarySeqLoop = 0; + } + const delay: number = buf.g1; + if (seqId === -1 || npc.primarySeqId === -1 || SeqType.instances[seqId].priority > SeqType.instances[npc.primarySeqId].priority || SeqType.instances[npc.primarySeqId].priority === 0) { + npc.primarySeqId = seqId; + npc.primarySeqFrame = 0; + npc.primarySeqCycle = 0; + npc.primarySeqDelay = delay; + npc.primarySeqLoop = 0; + } + } + if ((mask & 0x4) === 4) { + npc.targetId = buf.g2; + if (npc.targetId === 65535) { + npc.targetId = -1; + } + } + if ((mask & 0x8) === 8) { + npc.chat = buf.gjstr; + npc.chatTimer = 100; + } + if ((mask & 0x10) === 16) { + npc.damage = buf.g1; + npc.damageType = buf.g1; + npc.combatCycle = this.loopCycle + 400; + npc.health = buf.g1; + npc.totalHealth = buf.g1; + } + if ((mask & 0x20) === 32) { + npc.type = NpcType.get(buf.g2); + npc.seqWalkId = npc.type.walkanim; + npc.seqTurnAroundId = npc.type.walkanim_b; + npc.seqTurnLeftId = npc.type.walkanim_r; + npc.seqTurnRightId = npc.type.walkanim_l; + npc.seqStandId = npc.type.readyanim; + } + if ((mask & 0x40) === 64) { + npc.spotanimId = buf.g2; + const info: number = buf.g4; + npc.spotanimOffset = info >> 16; + npc.spotanimLastCycle = this.loopCycle + (info & 0xffff); + npc.spotanimFrame = 0; + npc.spotanimCycle = 0; + if (npc.spotanimLastCycle > this.loopCycle) { + npc.spotanimFrame = -1; + } + if (npc.spotanimId === 65535) { + npc.spotanimId = -1; + } + } + if ((mask & 0x80) === 128) { + npc.targetTileX = buf.g2; + npc.targetTileZ = buf.g2; + npc.lastFaceX = npc.targetTileX; + npc.lastFaceZ = npc.targetTileZ; + } + } + }; + + private updatePlayers = (): void => { + for (let i: number = -1; i < this.playerCount; i++) { + let index: number; + if (i === -1) { + index = this.LOCAL_PLAYER_INDEX; + } else { + index = this.playerIds[i]; + } + + const player: PlayerEntity | null = this.players[index]; + if (player) { + this.updateEntity(player); + } + } + + Client.updatePlayersCounter++; + if (Client.updatePlayersCounter > 1406) { + Client.updatePlayersCounter = 0; + // ANTICHEAT_CYCLELOGIC6 + this.out.p1isaac(ClientProt.ANTICHEAT_CYCLELOGIC6); + this.out.p1(0); + const start: number = this.out.pos; + this.out.p1(162); + this.out.p1(22); + if (((Math.random() * 2.0) | 0) === 0) { + this.out.p1(84); + } + this.out.p2(31824); + this.out.p2(13490); + if (((Math.random() * 2.0) | 0) === 0) { + this.out.p1(123); + } + if (((Math.random() * 2.0) | 0) === 0) { + this.out.p1(134); + } + this.out.p1(100); + this.out.p1(94); + this.out.p2(35521); + this.out.psize1(this.out.pos - start); + } + }; + + private updateEntity = (entity: PathingEntity): void => { + if (entity.x < 128 || entity.z < 128 || entity.x >= 13184 || entity.z >= 13184) { + entity.primarySeqId = -1; + entity.spotanimId = -1; + entity.forceMoveEndCycle = 0; + entity.forceMoveStartCycle = 0; + entity.x = entity.pathTileX[0] * 128 + entity.size * 64; + entity.z = entity.pathTileZ[0] * 128 + entity.size * 64; + entity.pathLength = 0; + } + + if (entity === this.localPlayer && (entity.x < 1536 || entity.z < 1536 || entity.x >= 11776 || entity.z >= 11776)) { + entity.primarySeqId = -1; + entity.spotanimId = -1; + entity.forceMoveEndCycle = 0; + entity.forceMoveStartCycle = 0; + entity.x = entity.pathTileX[0] * 128 + entity.size * 64; + entity.z = entity.pathTileZ[0] * 128 + entity.size * 64; + entity.pathLength = 0; + } + + if (entity.forceMoveEndCycle > this.loopCycle) { + this.updateForceMovement(entity); + } else if (entity.forceMoveStartCycle >= this.loopCycle) { + this.startForceMovement(entity); + } else { + this.updateMovement(entity); + } + + this.updateFacingDirection(entity); + this.updateSequences(entity); + }; + + private pushPlayers = (): void => { + if (!this.localPlayer) { + return; + } + + if (this.localPlayer.x >> 7 === this.flagSceneTileX && this.localPlayer.z >> 7 === this.flagSceneTileZ) { + this.flagSceneTileX = 0; + } + + for (let i: number = -1; i < this.playerCount; i++) { + let player: PlayerEntity | null; + let id: number; + if (i === -1) { + player = this.localPlayer; + id = this.LOCAL_PLAYER_INDEX << 14; + } else { + player = this.players[this.playerIds[i]]; + id = this.playerIds[i] << 14; + } + + if (!player || !player.isVisible()) { + continue; + } + + player.lowMemory = ((Client.lowMemory && this.playerCount > 50) || this.playerCount > 200) && i !== -1 && player.secondarySeqId === player.seqStandId; + const stx: number = player.x >> 7; + const stz: number = player.z >> 7; + + if (stx < 0 || stx >= CollisionMap.SIZE || stz < 0 || stz >= CollisionMap.SIZE) { + continue; + } + + if (!player.locModel || this.loopCycle < player.locStartCycle || this.loopCycle >= player.locStopCycle) { + if ((player.x & 0x7f) === 64 && (player.z & 0x7f) === 64) { + if (this.tileLastOccupiedCycle[stx][stz] === this.sceneCycle) { + continue; + } + + this.tileLastOccupiedCycle[stx][stz] = this.sceneCycle; + } + + player.y = this.getHeightmapY(this.currentLevel, player.x, player.z); + this.scene?.addTemporary(this.currentLevel, player.x, player.y, player.z, null, player, id, player.yaw, 60, player.seqStretches); + } else { + player.lowMemory = false; + player.y = this.getHeightmapY(this.currentLevel, player.x, player.z); + this.scene?.addTemporary2(this.currentLevel, player.x, player.y, player.z, player.minTileX, player.minTileZ, player.maxTileX, player.maxTileZ, null, player, id, player.yaw); + } + } + }; + private updateNpcs = (): void => { + for (let i: number = 0; i < this.npcCount; i++) { + const id: number = this.npcIds[i]; + const npc: NpcEntity | null = this.npcs[id]; + if (npc && npc.type) { + this.updateEntity(npc); + } + } + }; + + private pushNpcs = (): void => { + for (let i: number = 0; i < this.npcCount; i++) { + const npc: NpcEntity | null = this.npcs[this.npcIds[i]]; + const bitset: number = ((this.npcIds[i] << 14) + 0x20000000) | 0; + + if (!npc || !npc.isVisible()) { + continue; + } + + const x: number = npc.x >> 7; + const z: number = npc.z >> 7; + + if (x < 0 || x >= CollisionMap.SIZE || z < 0 || z >= CollisionMap.SIZE) { + continue; + } + + if (npc.size === 1 && (npc.x & 0x7f) === 64 && (npc.z & 0x7f) === 64) { + if (this.tileLastOccupiedCycle[x][z] === this.sceneCycle) { + continue; + } + + this.tileLastOccupiedCycle[x][z] = this.sceneCycle; + } + + this.scene?.addTemporary(this.currentLevel, npc.x, this.getHeightmapY(this.currentLevel, npc.x, npc.z), npc.z, null, npc, bitset, npc.yaw, (npc.size - 1) * 64 + 60, npc.seqStretches); + } + }; + + private pushProjectiles = (): void => { + for (let proj: ProjectileEntity | null = this.projectiles.peekFront() as ProjectileEntity | null; proj; proj = this.projectiles.prev() as ProjectileEntity | null) { + if (proj.level !== this.currentLevel || this.loopCycle > proj.lastCycle) { + proj.unlink(); + } else if (this.loopCycle >= proj.startCycle) { + if (proj.target > 0) { + const npc: NpcEntity | null = this.npcs[proj.target - 1]; + if (npc) { + proj.updateVelocity(npc.x, this.getHeightmapY(proj.level, npc.x, npc.z) - proj.offsetY, npc.z, this.loopCycle); + } + } + + if (proj.target < 0) { + const index: number = -proj.target - 1; + let player: PlayerEntity | null; + if (index === this.localPid) { + player = this.localPlayer; + } else { + player = this.players[index]; + } + if (player) { + proj.updateVelocity(player.x, this.getHeightmapY(proj.level, player.x, player.z) - proj.offsetY, player.z, this.loopCycle); + } + } + + proj.update(this.sceneDelta); + this.scene?.addTemporary(this.currentLevel, proj.x | 0, proj.y | 0, proj.z | 0, null, proj, -1, proj.yaw, 60, false); + } + } + }; + + private pushSpotanims = (): void => { + for (let entity: SpotAnimEntity | null = this.spotanims.peekFront() as SpotAnimEntity | null; entity; entity = this.spotanims.prev() as SpotAnimEntity | null) { + if (entity.level !== this.currentLevel || entity.seqComplete) { + entity.unlink(); + } else if (this.loopCycle >= entity.startCycle) { + entity.update(this.sceneDelta); + if (entity.seqComplete) { + entity.unlink(); + } else { + this.scene?.addTemporary(entity.level, entity.x, entity.y, entity.z, null, entity, -1, 0, 60, false); + } + } + } + }; + + private pushLocs = (): void => { + for (let loc: LocEntity | null = this.locList.peekFront() as LocEntity | null; loc; loc = this.locList.prev() as LocEntity | null) { + let append: boolean = false; + loc.seqCycle += this.sceneDelta; + if (loc.seqFrame === -1) { + loc.seqFrame = 0; + append = true; + } + + if (loc.seq.delay) { + while (loc.seqCycle > loc.seq.delay[loc.seqFrame]) { + loc.seqCycle -= loc.seq.delay[loc.seqFrame] + 1; + loc.seqFrame++; + + append = true; + + if (loc.seqFrame >= loc.seq.frameCount) { + loc.seqFrame -= loc.seq.replayoff; + + if (loc.seqFrame < 0 || loc.seqFrame >= loc.seq.frameCount) { + loc.unlink(); + append = false; + break; + } + } + } + } + + if (append && this.scene) { + const level: number = loc.heightmapSW; + const x: number = loc.heightmapNE; + const z: number = loc.heightmapNW; + + let bitset: number = 0; + if (loc.heightmapSE === 0) { + bitset = this.scene.getWallBitset(level, x, z); + } else if (loc.heightmapSE === 1) { + bitset = this.scene.getWallDecorationBitset(level, z, x); + } else if (loc.heightmapSE === 2) { + bitset = this.scene.getLocBitset(level, x, z); + } else if (loc.heightmapSE === 3) { + bitset = this.scene.getGroundDecorationBitset(level, x, z); + } + + if (this.levelHeightmap && bitset !== 0 && ((bitset >> 14) & 0x7fff) === loc.index) { + const heightmapSW: number = this.levelHeightmap[level][x][z]; + const heightmapSE: number = this.levelHeightmap[level][x + 1][z]; + const heightmapNE: number = this.levelHeightmap[level][x + 1][z + 1]; + const heightmapNW: number = this.levelHeightmap[level][x][z + 1]; + + const type: LocType = LocType.get(loc.index); + let seqId: number = -1; + if (loc.seqFrame !== -1 && loc.seq.frames) { + seqId = loc.seq.frames[loc.seqFrame]; + } + + if (loc.heightmapSE === 2) { + const info: number = this.scene.getInfo(level, x, z, bitset); + let shape: number = info & 0x1f; + const rotation: number = info >> 6; + + if (shape === LocShape.CENTREPIECE_DIAGONAL) { + shape = LocShape.CENTREPIECE_STRAIGHT; + } + + this.scene?.setLocModel(level, x, z, type.getModel(shape, rotation, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId)); + } else if (loc.heightmapSE === 1) { + this.scene?.setWallDecorationModel(level, x, z, type.getModel(LocShape.WALLDECOR_STRAIGHT_NOOFFSET, 0, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId)); + } else if (loc.heightmapSE === 0) { + const info: number = this.scene.getInfo(level, x, z, bitset); + const shape: number = info & 0x1f; + const rotation: number = info >> 6; + + if (shape === LocShape.WALL_L) { + const nextRotation: number = (rotation + 1) & 0x3; + this.scene?.setWallModels( + x, + z, + level, + type.getModel(LocShape.WALL_L, rotation + 4, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId), + type.getModel(LocShape.WALL_L, nextRotation, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId) + ); + } else { + this.scene?.setWallModel(level, x, z, type.getModel(shape, rotation, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId)); + } + } else if (loc.heightmapSE === 3) { + const info: number = this.scene.getInfo(level, x, z, bitset); + const rotation: number = info >> 6; + this.scene?.setGroundDecorationModel(level, x, z, type.getModel(LocShape.GROUND_DECOR, rotation, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId)); + } + } else { + loc.unlink(); + } + } + } + }; + + private updateEntityChats = (): void => { + for (let i: number = -1; i < this.playerCount; i++) { + let index: number; + if (i === -1) { + index = this.LOCAL_PLAYER_INDEX; + } else { + index = this.playerIds[i]; + } + + const player: PlayerEntity | null = this.players[index]; + if (player && player.chatTimer > 0) { + player.chatTimer--; + + if (player.chatTimer === 0) { + player.chat = null; + } + } + } + + for (let i: number = 0; i < this.npcCount; i++) { + const index: number = this.npcIds[i]; + const npc: NpcEntity | null = this.npcs[index]; + + if (npc && npc.chatTimer > 0) { + npc.chatTimer--; + + if (npc.chatTimer === 0) { + npc.chat = null; + } + } + } + }; + + private updateTemporaryLocs = (): void => { + if (this.sceneState === 2) { + for (let loc: LocSpawned | null = this.temporaryLocs.peekFront() as LocSpawned | null; loc; loc = this.temporaryLocs.prev() as LocSpawned | null) { + if (this.loopCycle >= loc.lastCycle) { + this.addLoc(loc.plane, loc.x, loc.z, loc.locIndex, loc.angle, loc.shape, loc.layer); + loc.unlink(); + } + } + + Client.updateLocCounter++; + if (Client.updateLocCounter > 85) { + Client.updateLocCounter = 0; + // ANTICHEAT_CYCLELOGIC5 + this.out.p1isaac(ClientProt.ANTICHEAT_CYCLELOGIC5); + } + } + }; + + private updateForceMovement = (entity: PathingEntity): void => { + const delta: number = entity.forceMoveEndCycle - this.loopCycle; + const dstX: number = entity.forceMoveStartSceneTileX * 128 + entity.size * 64; + const dstZ: number = entity.forceMoveStartSceneTileZ * 128 + entity.size * 64; + + entity.x += ((dstX - entity.x) / delta) | 0; + entity.z += ((dstZ - entity.z) / delta) | 0; + + entity.seqTrigger = 0; + + if (entity.forceMoveFaceDirection == 0) { + entity.dstYaw = 1024; + } + + if (entity.forceMoveFaceDirection == 1) { + entity.dstYaw = 1536; + } + + if (entity.forceMoveFaceDirection == 2) { + entity.dstYaw = 0; + } + + if (entity.forceMoveFaceDirection == 3) { + entity.dstYaw = 512; + } + }; + + private startForceMovement = (entity: PathingEntity): void => { + if (entity.forceMoveStartCycle == this.loopCycle || entity.primarySeqId == -1 || entity.primarySeqDelay != 0 || entity.primarySeqCycle + 1 > SeqType.instances[entity.primarySeqId].delay![entity.primarySeqFrame]) { + const duration: number = entity.forceMoveStartCycle - entity.forceMoveEndCycle; + const delta: number = this.loopCycle - entity.forceMoveEndCycle; + const dx0: number = entity.forceMoveStartSceneTileX * 128 + entity.size * 64; + const dz0: number = entity.forceMoveStartSceneTileZ * 128 + entity.size * 64; + const dx1: number = entity.forceMoveEndSceneTileX * 128 + entity.size * 64; + const dz1: number = entity.forceMoveEndSceneTileZ * 128 + entity.size * 64; + entity.x = ((dx0 * (duration - delta) + dx1 * delta) / duration) | 0; + entity.z = ((dz0 * (duration - delta) + dz1 * delta) / duration) | 0; + } + + entity.seqTrigger = 0; + + if (entity.forceMoveFaceDirection == 0) { + entity.dstYaw = 1024; + } + + if (entity.forceMoveFaceDirection == 1) { + entity.dstYaw = 1536; + } + + if (entity.forceMoveFaceDirection == 2) { + entity.dstYaw = 0; + } + + if (entity.forceMoveFaceDirection == 3) { + entity.dstYaw = 512; + } + + entity.yaw = entity.dstYaw; + }; + + private updateFacingDirection = (e: PathingEntity): void => { + if (e.targetId !== -1 && e.targetId < 32768) { + const npc: NpcEntity | null = this.npcs[e.targetId]; + if (npc) { + const dstX: number = e.x - npc.x; + const dstZ: number = e.z - npc.z; + + if (dstX !== 0 || dstZ !== 0) { + e.dstYaw = ((Math.atan2(dstX, dstZ) * 325.949) | 0) & 0x7ff; + } + } + } + + if (e.targetId >= 32768) { + let index: number = e.targetId - 32768; + if (index === this.localPid) { + index = this.LOCAL_PLAYER_INDEX; + } + + const player: PlayerEntity | null = this.players[index]; + if (player) { + const dstX: number = e.x - player.x; + const dstZ: number = e.z - player.z; + + if (dstX !== 0 || dstZ !== 0) { + e.dstYaw = ((Math.atan2(dstX, dstZ) * 325.949) | 0) & 0x7ff; + } + } + } + + if ((e.targetTileX !== 0 || e.targetTileZ !== 0) && (e.pathLength === 0 || e.seqTrigger > 0)) { + const dstX: number = e.x - (e.targetTileX - this.sceneBaseTileX - this.sceneBaseTileX) * 64; + const dstZ: number = e.z - (e.targetTileZ - this.sceneBaseTileZ - this.sceneBaseTileZ) * 64; + + if (dstX !== 0 || dstZ !== 0) { + e.dstYaw = ((Math.atan2(dstX, dstZ) * 325.949) | 0) & 0x7ff; + } + + e.targetTileX = 0; + e.targetTileZ = 0; + } + + const remainingYaw: number = (e.dstYaw - e.yaw) & 0x7ff; + + if (remainingYaw !== 0) { + if (remainingYaw < 32 || remainingYaw > 2016) { + e.yaw = e.dstYaw; + } else if (remainingYaw > 1024) { + e.yaw -= 32; + } else { + e.yaw += 32; + } + + e.yaw &= 0x7ff; + + if (e.secondarySeqId === e.seqStandId && e.yaw !== e.dstYaw) { + if (e.seqTurnId !== -1) { + e.secondarySeqId = e.seqTurnId; + return; + } + + e.secondarySeqId = e.seqWalkId; + } + } + }; + + private updateSequences = (e: PathingEntity): void => { + e.seqStretches = false; + + let seq: SeqType | null; + if (e.secondarySeqId !== -1) { + seq = SeqType.instances[e.secondarySeqId]; + e.secondarySeqCycle++; + if (seq.delay && e.secondarySeqFrame < seq.frameCount && e.secondarySeqCycle > seq.delay[e.secondarySeqFrame]) { + e.secondarySeqCycle = 0; + e.secondarySeqFrame++; + } + if (e.secondarySeqFrame >= seq.frameCount) { + e.secondarySeqCycle = 0; + e.secondarySeqFrame = 0; + } + } + + if (e.primarySeqId !== -1 && e.primarySeqDelay === 0) { + seq = SeqType.instances[e.primarySeqId]; + e.primarySeqCycle++; + while (seq.delay && e.primarySeqFrame < seq.frameCount && e.primarySeqCycle > seq.delay[e.primarySeqFrame]) { + e.primarySeqCycle -= seq.delay[e.primarySeqFrame]; + e.primarySeqFrame++; + } + + if (e.primarySeqFrame >= seq.frameCount) { + e.primarySeqFrame -= seq.replayoff; + e.primarySeqLoop++; + if (e.primarySeqLoop >= seq.replaycount) { + e.primarySeqId = -1; + } + if (e.primarySeqFrame < 0 || e.primarySeqFrame >= seq.frameCount) { + e.primarySeqId = -1; + } + } + + e.seqStretches = seq.stretches; + } + + if (e.primarySeqDelay > 0) { + e.primarySeqDelay--; + } + + if (e.spotanimId !== -1 && this.loopCycle >= e.spotanimLastCycle) { + if (e.spotanimFrame < 0) { + e.spotanimFrame = 0; + } + + seq = SpotAnimType.instances[e.spotanimId].seq; + e.spotanimCycle++; + while (seq && seq.delay && e.spotanimFrame < seq.frameCount && e.spotanimCycle > seq.delay[e.spotanimFrame]) { + e.spotanimCycle -= seq.delay[e.spotanimFrame]; + e.spotanimFrame++; + } + + if (seq && e.spotanimFrame >= seq.frameCount) { + if (e.spotanimFrame < 0 || e.spotanimFrame >= seq.frameCount) { + e.spotanimId = -1; + } + } + } + }; + + private updateMovement = (entity: PathingEntity): void => { + entity.secondarySeqId = entity.seqStandId; + + if (entity.pathLength === 0) { + entity.seqTrigger = 0; + return; + } + + if (entity.primarySeqId !== -1 && entity.primarySeqDelay === 0) { + const seq: SeqType = SeqType.instances[entity.primarySeqId]; + if (!seq.labelGroups) { + entity.seqTrigger++; + return; + } + } + + const x: number = entity.x; + const z: number = entity.z; + const dstX: number = entity.pathTileX[entity.pathLength - 1] * 128 + entity.size * 64; + const dstZ: number = entity.pathTileZ[entity.pathLength - 1] * 128 + entity.size * 64; + + if (dstX - x <= 256 && dstX - x >= -256 && dstZ - z <= 256 && dstZ - z >= -256) { + if (x < dstX) { + if (z < dstZ) { + entity.dstYaw = 1280; + } else if (z > dstZ) { + entity.dstYaw = 1792; + } else { + entity.dstYaw = 1536; + } + } else if (x > dstX) { + if (z < dstZ) { + entity.dstYaw = 768; + } else if (z > dstZ) { + entity.dstYaw = 256; + } else { + entity.dstYaw = 512; + } + } else if (z < dstZ) { + entity.dstYaw = 1024; + } else { + entity.dstYaw = 0; + } + + let deltaYaw: number = (entity.dstYaw - entity.yaw) & 0x7ff; + if (deltaYaw > 1024) { + deltaYaw -= 2048; + } + + let seqId: number = entity.seqTurnAroundId; + if (deltaYaw >= -256 && deltaYaw <= 256) { + seqId = entity.seqWalkId; + } else if (deltaYaw >= 256 && deltaYaw < 768) { + seqId = entity.seqTurnRightId; + } else if (deltaYaw >= -768 && deltaYaw <= -256) { + seqId = entity.seqTurnLeftId; + } + + if (seqId === -1) { + seqId = entity.seqWalkId; + } + + entity.secondarySeqId = seqId; + let moveSpeed: number = 4; + if (entity.yaw !== entity.dstYaw && entity.targetId === -1) { + moveSpeed = 2; + } + + if (entity.pathLength > 2) { + moveSpeed = 6; + } + + if (entity.pathLength > 3) { + moveSpeed = 8; + } + + if (entity.seqTrigger > 0 && entity.pathLength > 1) { + moveSpeed = 8; + entity.seqTrigger--; + } + + if (entity.pathRunning[entity.pathLength - 1]) { + moveSpeed <<= 0x1; + } + + if (moveSpeed >= 8 && entity.secondarySeqId === entity.seqWalkId && entity.seqRunId !== -1) { + entity.secondarySeqId = entity.seqRunId; + } + + if (x < dstX) { + entity.x += moveSpeed; + if (entity.x > dstX) { + entity.x = dstX; + } + } else if (x > dstX) { + entity.x -= moveSpeed; + if (entity.x < dstX) { + entity.x = dstX; + } + } + if (z < dstZ) { + entity.z += moveSpeed; + if (entity.z > dstZ) { + entity.z = dstZ; + } + } else if (z > dstZ) { + entity.z -= moveSpeed; + if (entity.z < dstZ) { + entity.z = dstZ; + } + } + + if (entity.x === dstX && entity.z === dstZ) { + entity.pathLength--; + } + } else { + entity.x = dstX; + entity.z = dstZ; + } + }; + + private getTopLevel = (): number => { + let top: number = 3; + if (this.cameraPitch < 310 && this.localPlayer) { + let cameraLocalTileX: number = this.cameraX >> 7; + let cameraLocalTileZ: number = this.cameraZ >> 7; + const playerLocalTileX: number = this.localPlayer.x >> 7; + const playerLocalTileZ: number = this.localPlayer.z >> 7; + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + top = this.currentLevel; + } + let tileDeltaX: number; + if (playerLocalTileX > cameraLocalTileX) { + tileDeltaX = playerLocalTileX - cameraLocalTileX; + } else { + tileDeltaX = cameraLocalTileX - playerLocalTileX; + } + let tileDeltaZ: number; + if (playerLocalTileZ > cameraLocalTileZ) { + tileDeltaZ = playerLocalTileZ - cameraLocalTileZ; + } else { + tileDeltaZ = cameraLocalTileZ - playerLocalTileZ; + } + let delta: number; + let accumulator: number; + if (tileDeltaX > tileDeltaZ) { + delta = ((tileDeltaZ * 65536) / tileDeltaX) | 0; + accumulator = 32768; + while (cameraLocalTileX !== playerLocalTileX) { + if (cameraLocalTileX < playerLocalTileX) { + cameraLocalTileX++; + } else if (cameraLocalTileX > playerLocalTileX) { + cameraLocalTileX--; + } + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + top = this.currentLevel; + } + accumulator += delta; + if (accumulator >= 65536) { + accumulator -= 65536; + if (cameraLocalTileZ < playerLocalTileZ) { + cameraLocalTileZ++; + } else if (cameraLocalTileZ > playerLocalTileZ) { + cameraLocalTileZ--; + } + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + top = this.currentLevel; + } + } + } + } else { + delta = ((tileDeltaX * 65536) / tileDeltaZ) | 0; + accumulator = 32768; + while (cameraLocalTileZ !== playerLocalTileZ) { + if (cameraLocalTileZ < playerLocalTileZ) { + cameraLocalTileZ++; + } else if (cameraLocalTileZ > playerLocalTileZ) { + cameraLocalTileZ--; + } + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + top = this.currentLevel; + } + accumulator += delta; + if (accumulator >= 65536) { + accumulator -= 65536; + if (cameraLocalTileX < playerLocalTileX) { + cameraLocalTileX++; + } else if (cameraLocalTileX > playerLocalTileX) { + cameraLocalTileX--; + } + if (this.levelTileFlags && (this.levelTileFlags[this.currentLevel][cameraLocalTileX][cameraLocalTileZ] & 0x4) !== 0) { + top = this.currentLevel; + } + } + } + } + } + if (this.localPlayer && this.levelTileFlags && (this.levelTileFlags[this.currentLevel][this.localPlayer.x >> 7][this.localPlayer.z >> 7] & 0x4) !== 0) { + top = this.currentLevel; + } + return top; + }; + + private getTopLevelCutscene = (): number => { + if (!this.levelTileFlags) { + return 0; // custom + } + const y: number = this.getHeightmapY(this.currentLevel, this.cameraX, this.cameraZ); + return y - this.cameraY >= 800 || (this.levelTileFlags[this.currentLevel][this.cameraX >> 7][this.cameraZ >> 7] & 0x4) === 0 ? 3 : this.currentLevel; + }; + + private getHeightmapY = (level: number, sceneX: number, sceneZ: number): number => { + if (!this.levelHeightmap) { + return 0; // custom + } + const tileX: number = Math.min(sceneX >> 7, CollisionMap.SIZE - 1); + const tileZ: number = Math.min(sceneZ >> 7, CollisionMap.SIZE - 1); + let realLevel: number = level; + if (level < 3 && this.levelTileFlags && (this.levelTileFlags[1][tileX][tileZ] & 0x2) === 2) { + realLevel = level + 1; + } + + const tileLocalX: number = sceneX & 0x7f; + const tileLocalZ: number = sceneZ & 0x7f; + const y00: number = (this.levelHeightmap[realLevel][tileX][tileZ] * (128 - tileLocalX) + this.levelHeightmap[realLevel][tileX + 1][tileZ] * tileLocalX) >> 7; + const y11: number = (this.levelHeightmap[realLevel][tileX][tileZ + 1] * (128 - tileLocalX) + this.levelHeightmap[realLevel][tileX + 1][tileZ + 1] * tileLocalX) >> 7; + return (y00 * (128 - tileLocalZ) + y11 * tileLocalZ) >> 7; + }; + + private orbitCamera = (targetX: number, targetY: number, targetZ: number, yaw: number, pitch: number, distance: number): void => { + const invPitch: number = (2048 - pitch) & 0x7ff; + const invYaw: number = (2048 - yaw) & 0x7ff; + let x: number = 0; + let z: number = 0; + let y: number = distance; + let sin: number; + let cos: number; + let tmp: number; + + if (invPitch !== 0) { + sin = Draw3D.sin[invPitch]; + cos = Draw3D.cos[invPitch]; + tmp = (z * cos - distance * sin) >> 16; + y = (z * sin + distance * cos) >> 16; + z = tmp; + } + + if (invYaw !== 0) { + sin = Draw3D.sin[invYaw]; + cos = Draw3D.cos[invYaw]; + tmp = (y * sin + x * cos) >> 16; + y = (y * cos - x * sin) >> 16; + x = tmp; + } + + this.cameraX = targetX - x; + this.cameraY = targetY - z; + this.cameraZ = targetZ - y; + this.cameraPitch = pitch; + this.cameraYaw = yaw; + }; + + private updateOrbitCamera = (): void => { + if (!this.localPlayer) { + return; // custom + } + const orbitX: number = this.localPlayer.x + this.cameraAnticheatOffsetX; + const orbitZ: number = this.localPlayer.z + this.cameraAnticheatOffsetZ; + if (this.orbitCameraX - orbitX < -500 || this.orbitCameraX - orbitX > 500 || this.orbitCameraZ - orbitZ < -500 || this.orbitCameraZ - orbitZ > 500) { + this.orbitCameraX = orbitX; + this.orbitCameraZ = orbitZ; + } + if (this.orbitCameraX !== orbitX) { + this.orbitCameraX += ((orbitX - this.orbitCameraX) / 16) | 0; + } + if (this.orbitCameraZ !== orbitZ) { + this.orbitCameraZ += ((orbitZ - this.orbitCameraZ) / 16) | 0; + } + if (this.actionKey[1] === 1) { + this.orbitCameraYawVelocity += ((-this.orbitCameraYawVelocity - 24) / 2) | 0; + } else if (this.actionKey[2] === 1) { + this.orbitCameraYawVelocity += ((24 - this.orbitCameraYawVelocity) / 2) | 0; + } else { + this.orbitCameraYawVelocity = (this.orbitCameraYawVelocity / 2) | 0; + } + if (this.actionKey[3] === 1) { + this.orbitCameraPitchVelocity += ((12 - this.orbitCameraPitchVelocity) / 2) | 0; + } else if (this.actionKey[4] === 1) { + this.orbitCameraPitchVelocity += ((-this.orbitCameraPitchVelocity - 12) / 2) | 0; + } else { + this.orbitCameraPitchVelocity = (this.orbitCameraPitchVelocity / 2) | 0; + } + this.orbitCameraYaw = ((this.orbitCameraYaw + this.orbitCameraYawVelocity / 2) | 0) & 0x7ff; + this.orbitCameraPitch += (this.orbitCameraPitchVelocity / 2) | 0; + if (this.orbitCameraPitch < 128) { + this.orbitCameraPitch = 128; + } + if (this.orbitCameraPitch > 383) { + this.orbitCameraPitch = 383; + } + + const orbitTileX: number = this.orbitCameraX >> 7; + const orbitTileZ: number = this.orbitCameraZ >> 7; + const orbitY: number = this.getHeightmapY(this.currentLevel, this.orbitCameraX, this.orbitCameraZ); + let maxY: number = 0; + + if (this.levelHeightmap) { + if (orbitTileX > 3 && orbitTileZ > 3 && orbitTileX < 100 && orbitTileZ < 100) { + for (let x: number = orbitTileX - 4; x <= orbitTileX + 4; x++) { + for (let z: number = orbitTileZ - 4; z <= orbitTileZ + 4; z++) { + let level: number = this.currentLevel; + if (level < 3 && this.levelTileFlags && (this.levelTileFlags[1][x][z] & 0x2) === 2) { + level++; + } + + const y: number = orbitY - this.levelHeightmap[level][x][z]; + if (y > maxY) { + maxY = y; + } + } + } + } + } + + let clamp: number = maxY * 192; + if (clamp > 98048) { + clamp = 98048; + } + + if (clamp < 32768) { + clamp = 32768; + } + + if (clamp > this.cameraPitchClamp) { + this.cameraPitchClamp += ((clamp - this.cameraPitchClamp) / 24) | 0; + } else if (clamp < this.cameraPitchClamp) { + this.cameraPitchClamp += ((clamp - this.cameraPitchClamp) / 80) | 0; + } + }; + + private applyCutscene = (): void => { + let x: number = this.cutsceneSrcLocalTileX * 128 + 64; + let z: number = this.cutsceneSrcLocalTileZ * 128 + 64; + let y: number = this.getHeightmapY(this.currentLevel, this.cutsceneSrcLocalTileX, this.cutsceneSrcLocalTileZ) - this.cutsceneSrcHeight; + + if (this.cameraX < x) { + this.cameraX += this.cutsceneMoveSpeed + ((((x - this.cameraX) * this.cutsceneMoveAcceleration) / 1000) | 0); + if (this.cameraX > x) { + this.cameraX = x; + } + } + + if (this.cameraX > x) { + this.cameraX -= this.cutsceneMoveSpeed + ((((this.cameraX - x) * this.cutsceneMoveAcceleration) / 1000) | 0); + if (this.cameraX < x) { + this.cameraX = x; + } + } + + if (this.cameraY < y) { + this.cameraY += this.cutsceneMoveSpeed + ((((y - this.cameraY) * this.cutsceneMoveAcceleration) / 1000) | 0); + if (this.cameraY > y) { + this.cameraY = y; + } + } + + if (this.cameraY > y) { + this.cameraY -= this.cutsceneMoveSpeed + ((((this.cameraY - y) * this.cutsceneMoveAcceleration) / 1000) | 0); + if (this.cameraY < y) { + this.cameraY = y; + } + } + + if (this.cameraZ < z) { + this.cameraZ += this.cutsceneMoveSpeed + ((((z - this.cameraZ) * this.cutsceneMoveAcceleration) / 1000) | 0); + if (this.cameraZ > z) { + this.cameraZ = z; + } + } + + if (this.cameraZ > z) { + this.cameraZ -= this.cutsceneMoveSpeed + ((((this.cameraZ - z) * this.cutsceneMoveAcceleration) / 1000) | 0); + if (this.cameraZ < z) { + this.cameraZ = z; + } + } + + x = this.cutsceneDstLocalTileX * 128 + 64; + z = this.cutsceneDstLocalTileZ * 128 + 64; + y = this.getHeightmapY(this.currentLevel, this.cutsceneDstLocalTileX, this.cutsceneDstLocalTileZ) - this.cutsceneDstHeight; + + const deltaX: number = x - this.cameraX; + const deltaY: number = y - this.cameraY; + const deltaZ: number = z - this.cameraZ; + + const distance: number = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ) | 0; + let pitch: number = ((Math.atan2(deltaY, distance) * 325.949) | 0) & 0x7ff; + const yaw: number = ((Math.atan2(deltaX, deltaZ) * -325.949) | 0) & 0x7ff; + + if (pitch < 128) { + pitch = 128; + } + + if (pitch > 383) { + pitch = 383; + } + + if (this.cameraPitch < pitch) { + this.cameraPitch += this.cutsceneRotateSpeed + ((((pitch - this.cameraPitch) * this.cutsceneRotateAcceleration) / 1000) | 0); + if (this.cameraPitch > pitch) { + this.cameraPitch = pitch; + } + } + + if (this.cameraPitch > pitch) { + this.cameraPitch -= this.cutsceneRotateSpeed + ((((this.cameraPitch - pitch) * this.cutsceneRotateAcceleration) / 1000) | 0); + if (this.cameraPitch < pitch) { + this.cameraPitch = pitch; + } + } + + let deltaYaw: number = yaw - this.cameraYaw; + if (deltaYaw > 1024) { + deltaYaw -= 2048; + } + + if (deltaYaw < -1024) { + deltaYaw += 2048; + } + + if (deltaYaw > 0) { + this.cameraYaw += this.cutsceneRotateSpeed + (((deltaYaw * this.cutsceneRotateAcceleration) / 1000) | 0); + this.cameraYaw &= 0x7ff; + } + + if (deltaYaw < 0) { + this.cameraYaw -= this.cutsceneRotateSpeed + (((-deltaYaw * this.cutsceneRotateAcceleration) / 1000) | 0); + this.cameraYaw &= 0x7ff; + } + + let tmp: number = yaw - this.cameraYaw; + if (tmp > 1024) { + tmp -= 2048; + } + + if (tmp < -1024) { + tmp += 2048; + } + + if ((tmp < 0 && deltaYaw > 0) || (tmp > 0 && deltaYaw < 0)) { + this.cameraYaw = yaw; + } + }; + + private readZonePacket = (buf: Packet, opcode: number): void => { + const pos: number = buf.g1; + let x: number = this.baseX + ((pos >> 4) & 0x7); + let z: number = this.baseZ + (pos & 0x7); + + if (opcode === ServerProt.LOC_ADD_CHANGE || opcode === ServerProt.LOC_DEL) { + // LOC_ADD_CHANGE || LOC_DEL + const info: number = buf.g1; + const shape: number = info >> 2; + const angle: number = info & 0x3; + const layer: number = LocShapes.layer(shape); + let id: number; + if (opcode === ServerProt.LOC_DEL) { + id = -1; + } else { + id = buf.g2; + } + if (x >= 0 && z >= 0 && x < CollisionMap.SIZE && z < CollisionMap.SIZE) { + let loc: LocTemporary | null = null; + for (let next: LocTemporary | null = this.spawnedLocations.peekFront() as LocTemporary | null; next; next = this.spawnedLocations.prev() as LocTemporary | null) { + if (next.plane === this.currentLevel && next.x === x && next.z === z && next.layer === layer) { + loc = next; + break; + } + } + if (!loc && this.scene) { + let bitset: number = 0; + let otherId: number = -1; + let otherShape: number = 0; + let otherAngle: number = 0; + if (layer === LocLayer.WALL) { + bitset = this.scene.getWallBitset(this.currentLevel, x, z); + } else if (layer === LocLayer.WALL_DECOR) { + bitset = this.scene.getWallDecorationBitset(this.currentLevel, z, x); + } else if (layer === LocLayer.GROUND) { + bitset = this.scene.getLocBitset(this.currentLevel, x, z); + } else if (layer === LocLayer.GROUND_DECOR) { + bitset = this.scene.getGroundDecorationBitset(this.currentLevel, x, z); + } + if (bitset !== 0) { + const otherInfo: number = this.scene.getInfo(this.currentLevel, x, z, bitset); + otherId = (bitset >> 14) & 0x7fff; + otherShape = otherInfo & 0x1f; + otherAngle = otherInfo >> 6; + } + loc = new LocTemporary(this.currentLevel, layer, x, z, 0, LocAngle.WEST, LocShape.WALL_STRAIGHT, otherId, otherAngle, otherShape); + this.spawnedLocations.pushBack(loc); + } + if (loc) { + loc.locIndex = id; + loc.shape = shape; + loc.angle = angle; + } + this.addLoc(this.currentLevel, x, z, id, angle, shape, layer); + } + } else if (opcode === ServerProt.LOC_ANIM) { + // LOC_ANIM + const info: number = buf.g1; + const shape: number = info >> 2; + const layer: number = LocShapes.layer(shape); + const id: number = buf.g2; + if (x >= 0 && z >= 0 && x < CollisionMap.SIZE && z < CollisionMap.SIZE && this.scene) { + let bitset: number = 0; + if (layer === LocLayer.WALL) { + bitset = this.scene.getWallBitset(this.currentLevel, x, z); + } else if (layer === LocLayer.WALL_DECOR) { + bitset = this.scene.getWallDecorationBitset(this.currentLevel, z, x); + } else if (layer === LocLayer.GROUND) { + bitset = this.scene.getLocBitset(this.currentLevel, x, z); + } else if (layer === LocLayer.GROUND_DECOR) { + bitset = this.scene.getGroundDecorationBitset(this.currentLevel, x, z); + } + if (bitset !== 0) { + const loc: LocEntity = new LocEntity((bitset >> 14) & 0x7fff, this.currentLevel, layer, x, z, SeqType.instances[id], false); + this.locList.pushBack(loc); + } + } + } else if (opcode === ServerProt.OBJ_ADD) { + // OBJ_ADD + const id: number = buf.g2; + const count: number = buf.g2; + if (x >= 0 && z >= 0 && x < CollisionMap.SIZE && z < CollisionMap.SIZE) { + const obj: ObjStackEntity = new ObjStackEntity(id, count); + if (!this.levelObjStacks[this.currentLevel][x][z]) { + this.levelObjStacks[this.currentLevel][x][z] = new LinkList(); + } + this.levelObjStacks[this.currentLevel][x][z]?.pushBack(obj); + this.sortObjStacks(x, z); + } + } else if (opcode === ServerProt.OBJ_DEL) { + // OBJ_DEL + const id: number = buf.g2; + if (x >= 0 && z >= 0 && x < CollisionMap.SIZE && z < CollisionMap.SIZE) { + const list: LinkList | null = this.levelObjStacks[this.currentLevel][x][z]; + if (list) { + for (let next: ObjStackEntity | null = list.peekFront() as ObjStackEntity | null; next; next = list.prev() as ObjStackEntity | null) { + if (next.index === (id & 0x7fff)) { + next.unlink(); + break; + } + } + if (!list.peekFront()) { + this.levelObjStacks[this.currentLevel][x][z] = null; + } + this.sortObjStacks(x, z); + } + } + } else if (opcode === ServerProt.MAP_PROJANIM) { + // MAP_PROJANIM + let dx: number = x + buf.g1b; + let dz: number = z + buf.g1b; + const target: number = buf.g2b; + const spotanim: number = buf.g2; + const srcHeight: number = buf.g1; + const dstHeight: number = buf.g1; + const startDelay: number = buf.g2; + const endDelay: number = buf.g2; + const peak: number = buf.g1; + const arc: number = buf.g1; + if (x >= 0 && z >= 0 && x < CollisionMap.SIZE && z < CollisionMap.SIZE && dx >= 0 && dz >= 0 && dx < CollisionMap.SIZE && dz < CollisionMap.SIZE) { + x = x * 128 + 64; + z = z * 128 + 64; + dx = dx * 128 + 64; + dz = dz * 128 + 64; + const proj: ProjectileEntity = new ProjectileEntity(spotanim, this.currentLevel, x, this.getHeightmapY(this.currentLevel, x, z) - srcHeight, z, startDelay + this.loopCycle, endDelay + this.loopCycle, peak, arc, target, dstHeight); + proj.updateVelocity(dx, this.getHeightmapY(this.currentLevel, dx, dz) - dstHeight, dz, startDelay + this.loopCycle); + this.projectiles.pushBack(proj); + } + } else if (opcode === ServerProt.MAP_ANIM) { + // MAP_ANIM + const id: number = buf.g2; + const height: number = buf.g1; + const delay: number = buf.g2; + if (x >= 0 && z >= 0 && x < CollisionMap.SIZE && z < CollisionMap.SIZE) { + x = x * 128 + 64; + z = z * 128 + 64; + const spotanim: SpotAnimEntity = new SpotAnimEntity(id, this.currentLevel, x, z, this.getHeightmapY(this.currentLevel, x, z) - height, this.loopCycle, delay); + this.spotanims.pushBack(spotanim); + } + } else if (opcode === ServerProt.OBJ_REVEAL) { + // OBJ_REVEAL + const id: number = buf.g2; + const count: number = buf.g2; + const receiver: number = buf.g2; + if (x >= 0 && z >= 0 && x < CollisionMap.SIZE && z < CollisionMap.SIZE && receiver !== this.localPid) { + const obj: ObjStackEntity = new ObjStackEntity(id, count); + if (!this.levelObjStacks[this.currentLevel][x][z]) { + this.levelObjStacks[this.currentLevel][x][z] = new LinkList(); + } + this.levelObjStacks[this.currentLevel][x][z]?.pushBack(obj); + this.sortObjStacks(x, z); + } + } else if (opcode === ServerProt.LOC_MERGE) { + // LOC_MERGE + const info: number = buf.g1; + const shape: number = info >> 2; + const angle: number = info & 0x3; + const layer: number = LocShapes.layer(shape); + const id: number = buf.g2; + const start: number = buf.g2; + const end: number = buf.g2; + const pid: number = buf.g2; + let east: number = buf.g1b; + let south: number = buf.g1b; + let west: number = buf.g1b; + let north: number = buf.g1b; + + let player: PlayerEntity | null; + if (pid === this.localPid) { + player = this.localPlayer; + } else { + player = this.players[pid]; + } + + if (player && this.levelHeightmap) { + const loc1: LocSpawned = new LocSpawned(this.currentLevel, layer, x, z, -1, angle, shape, start + this.loopCycle); + this.temporaryLocs.pushBack(loc1); + + const loc2: LocSpawned = new LocSpawned(this.currentLevel, layer, x, z, id, angle, shape, end + this.loopCycle); + this.temporaryLocs.pushBack(loc2); + + const y0: number = this.levelHeightmap[this.currentLevel][x][z]; + const y1: number = this.levelHeightmap[this.currentLevel][x + 1][z]; + const y2: number = this.levelHeightmap[this.currentLevel][x + 1][z + 1]; + const y3: number = this.levelHeightmap[this.currentLevel][x][z + 1]; + const loc: LocType = LocType.get(id); + + player.locStartCycle = start + this.loopCycle; + player.locStopCycle = end + this.loopCycle; + player.locModel = loc.getModel(shape, angle, y0, y1, y2, y3, -1); + + let width: number = loc.width; + let height: number = loc.length; + if (angle === LocAngle.NORTH || angle === LocAngle.SOUTH) { + width = loc.length; + height = loc.width; + } + + player.locOffsetX = x * 128 + width * 64; + player.locOffsetZ = z * 128 + height * 64; + player.locOffsetY = this.getHeightmapY(this.currentLevel, player.locOffsetX, player.locOffsetZ); + + let tmp: number; + if (east > west) { + tmp = east; + east = west; + west = tmp; + } + + if (south > north) { + tmp = south; + south = north; + north = tmp; + } + + player.minTileX = x + east; + player.maxTileX = x + west; + player.minTileZ = z + south; + player.maxTileZ = z + north; + } + } else if (opcode === ServerProt.OBJ_COUNT) { + // OBJ_COUNT + const id: number = buf.g2; + const oldCount: number = buf.g2; + const newCount: number = buf.g2; + if (x >= 0 && z >= 0 && x < CollisionMap.SIZE && z < CollisionMap.SIZE) { + const list: LinkList | null = this.levelObjStacks[this.currentLevel][x][z]; + if (list) { + for (let next: ObjStackEntity | null = list.peekFront() as ObjStackEntity | null; next; next = list.prev() as ObjStackEntity | null) { + if (next.index === (id & 0x7fff) && next.count === oldCount) { + next.count = newCount; + break; + } + } + this.sortObjStacks(x, z); + } + } + } + }; + + private updateTextures = (cycle: number): void => { + if (!Client.lowMemory) { + if (Draw3D.textureCycle[17] >= cycle) { + const texture: Pix8 | null = Draw3D.textures[17]; + if (!texture) { + return; + } + const bottom: number = texture.width * texture.height - 1; + const adjustment: number = texture.width * this.sceneDelta * 2; + + const src: Int8Array = texture.pixels; + const dst: Int8Array = this.textureBuffer; + for (let i: number = 0; i <= bottom; i++) { + dst[i] = src[(i - adjustment) & bottom]; + } + + texture.pixels = dst; + this.textureBuffer = src; + Draw3D.pushTexture(17); + } + + if (Draw3D.textureCycle[24] >= cycle) { + const texture: Pix8 | null = Draw3D.textures[24]; + if (!texture) { + return; + } + const bottom: number = texture.width * texture.height - 1; + const adjustment: number = texture.width * this.sceneDelta * 2; + + const src: Int8Array = texture.pixels; + const dst: Int8Array = this.textureBuffer; + for (let i: number = 0; i <= bottom; i++) { + dst[i] = src[(i - adjustment) & bottom]; + } + + texture.pixels = dst; + this.textureBuffer = src; + Draw3D.pushTexture(24); + } + } + }; + + private updateFlames = (): void => { + if (!this.flameBuffer3 || !this.flameBuffer2 || !this.flameBuffer0 || !this.flameLineOffset) { + return; + } + + const height: number = 256; + + for (let x: number = 10; x < 117; x++) { + const rand: number = (Math.random() * 100.0) | 0; + if (rand < 50) this.flameBuffer3[x + ((height - 2) << 7)] = 255; + } + + for (let l: number = 0; l < 100; l++) { + const x: number = ((Math.random() * 124.0) | 0) + 2; + const y: number = ((Math.random() * 128.0) | 0) + 128; + const index: number = x + (y << 7); + this.flameBuffer3[index] = 192; + } + + for (let y: number = 1; y < height - 1; y++) { + for (let x: number = 1; x < 127; x++) { + const index: number = x + (y << 7); + this.flameBuffer2[index] = ((this.flameBuffer3[index - 1] + this.flameBuffer3[index + 1] + this.flameBuffer3[index - 128] + this.flameBuffer3[index + 128]) / 4) | 0; + } + } + + this.flameCycle0 += 128; + if (this.flameCycle0 > this.flameBuffer0.length) { + this.flameCycle0 -= this.flameBuffer0.length; + this.updateFlameBuffer(this.imageRunes[(Math.random() * 12.0) | 0]); + } + + for (let y: number = 1; y < height - 1; y++) { + for (let x: number = 1; x < 127; x++) { + const index: number = x + (y << 7); + let intensity: number = this.flameBuffer2[index + 128] - ((this.flameBuffer0[(index + this.flameCycle0) & (this.flameBuffer0.length - 1)] / 5) | 0); + if (intensity < 0) { + intensity = 0; + } + this.flameBuffer3[index] = intensity; + } + } + + for (let y: number = 0; y < height - 1; y++) { + this.flameLineOffset[y] = this.flameLineOffset[y + 1]; + } + + this.flameLineOffset[height - 1] = (Math.sin(this.loopCycle / 14.0) * 16.0 + Math.sin(this.loopCycle / 15.0) * 14.0 + Math.sin(this.loopCycle / 16.0) * 12.0) | 0; + + if (this.flameGradientCycle0 > 0) { + this.flameGradientCycle0 -= 4; + } + + if (this.flameGradientCycle1 > 0) { + this.flameGradientCycle1 -= 4; + } + + if (this.flameGradientCycle0 === 0 && this.flameGradientCycle1 === 0) { + const rand: number = (Math.random() * 2000.0) | 0; + + if (rand === 0) { + this.flameGradientCycle0 = 1024; + } else if (rand === 1) { + this.flameGradientCycle1 = 1024; + } + } + }; + + private mix = (src: number, alpha: number, dst: number): number => { + const invAlpha: number = 256 - alpha; + return ((((src & 0xff00ff) * invAlpha + (dst & 0xff00ff) * alpha) & 0xff00ff00) + (((src & 0xff00) * invAlpha + (dst & 0xff00) * alpha) & 0xff0000)) >> 8; + }; + + private drawFlames = (): void => { + if (!this.flameGradient || !this.flameGradient0 || !this.flameGradient1 || !this.flameGradient2 || !this.flameLineOffset || !this.flameBuffer3) { + return; + } + + const height: number = 256; + + // just colors + if (this.flameGradientCycle0 > 0) { + for (let i: number = 0; i < 256; i++) { + if (this.flameGradientCycle0 > 768) { + this.flameGradient[i] = this.mix(this.flameGradient0[i], 1024 - this.flameGradientCycle0, this.flameGradient1[i]); + } else if (this.flameGradientCycle0 > 256) { + this.flameGradient[i] = this.flameGradient1[i]; + } else { + this.flameGradient[i] = this.mix(this.flameGradient1[i], 256 - this.flameGradientCycle0, this.flameGradient0[i]); + } + } + } else if (this.flameGradientCycle1 > 0) { + for (let i: number = 0; i < 256; i++) { + if (this.flameGradientCycle1 > 768) { + this.flameGradient[i] = this.mix(this.flameGradient0[i], 1024 - this.flameGradientCycle1, this.flameGradient2[i]); + } else if (this.flameGradientCycle1 > 256) { + this.flameGradient[i] = this.flameGradient2[i]; + } else { + this.flameGradient[i] = this.mix(this.flameGradient2[i], 256 - this.flameGradientCycle1, this.flameGradient0[i]); + } + } + } else { + for (let i: number = 0; i < 256; i++) { + this.flameGradient[i] = this.flameGradient0[i]; + } + } + for (let i: number = 0; i < 33920; i++) { + if (this.imageTitle0 && this.imageFlamesLeft) this.imageTitle0.pixels[i] = this.imageFlamesLeft.pixels[i]; + } + + let srcOffset: number = 0; + let dstOffset: number = 1152; + + for (let y: number = 1; y < height - 1; y++) { + const offset: number = ((this.flameLineOffset[y] * (height - y)) / height) | 0; + let step: number = offset + 22; + if (step < 0) { + step = 0; + } + srcOffset += step; + for (let x: number = step; x < 128; x++) { + let value: number = this.flameBuffer3[srcOffset++]; + if (value === 0) { + dstOffset++; + } else { + const alpha: number = value; + const invAlpha: number = 256 - value; + value = this.flameGradient[value]; + if (this.imageTitle0) { + const background: number = this.imageTitle0.pixels[dstOffset]; + this.imageTitle0.pixels[dstOffset++] = ((((value & 0xff00ff) * alpha + (background & 0xff00ff) * invAlpha) & 0xff00ff00) + (((value & 0xff00) * alpha + (background & 0xff00) * invAlpha) & 0xff0000)) >> 8; + } + } + } + dstOffset += step; + } + + this.imageTitle0?.draw(0, 0); + + for (let i: number = 0; i < 33920; i++) { + if (this.imageTitle1 && this.imageFlamesRight) { + this.imageTitle1.pixels[i] = this.imageFlamesRight.pixels[i]; + } + } + + srcOffset = 0; + dstOffset = 1176; + for (let y: number = 1; y < height - 1; y++) { + const offset: number = ((this.flameLineOffset[y] * (height - y)) / height) | 0; + const step: number = 103 - offset; + dstOffset += offset; + for (let x: number = 0; x < step; x++) { + let value: number = this.flameBuffer3[srcOffset++]; + if (value === 0) { + dstOffset++; + } else { + const alpha: number = value; + const invAlpha: number = 256 - value; + value = this.flameGradient[value]; + if (this.imageTitle1) { + const background: number = this.imageTitle1.pixels[dstOffset]; + this.imageTitle1.pixels[dstOffset++] = ((((value & 0xff00ff) * alpha + (background & 0xff00ff) * invAlpha) & 0xff00ff00) + (((value & 0xff00) * alpha + (background & 0xff00) * invAlpha) & 0xff0000)) >> 8; + } + } + } + srcOffset += 128 - step; + dstOffset += 128 - step - offset; + } + + this.imageTitle1?.draw(661, 0); + }; +} + +console.log(`RS2 user client - release #${Client.clientversion}`); +await setupConfiguration(); +new Game().run().then((): void => {}); diff --git a/src/js/jagex2/client/GameShell.ts b/src/js/jagex2/client/GameShell.ts index 58e7d548..6fd07aed 100644 --- a/src/js/jagex2/client/GameShell.ts +++ b/src/js/jagex2/client/GameShell.ts @@ -5,6 +5,8 @@ import {sleep} from '../util/JsUtil'; import {CANVAS_PREVENTED, KEY_CODES} from './KeyCodes'; import InputTracking from './InputTracking'; import {canvas, canvas2d} from '../graphics/Canvas'; +import DrawGL from '../graphics/DrawGL'; +import { RenderMode } from '../graphics/RenderMode'; export default abstract class GameShell { static getParameter(name: string): string { @@ -45,6 +47,8 @@ export default abstract class GameShell { protected keyQueueReadPos: number = 0; protected keyQueueWritePos: number = 0; + protected renderMode: RenderMode = RenderMode.GPU; + constructor(resizetoFit: boolean = false) { canvas2d.fillStyle = 'black'; canvas2d.fillRect(0, 0, canvas.width, canvas.height); @@ -54,6 +58,9 @@ export default abstract class GameShell { } else { this.resize(canvas.width, canvas.height); } + if(this.renderMode === RenderMode.GPU) { + DrawGL.init(); + } } get width(): number { diff --git a/src/js/jagex2/dash3d/World3D.ts b/src/js/jagex2/dash3d/World3D.ts index c458bb54..bc163907 100644 --- a/src/js/jagex2/dash3d/World3D.ts +++ b/src/js/jagex2/dash3d/World3D.ts @@ -16,6 +16,7 @@ import TileOverlay from './type/TileOverlay'; import TileOverlayShape from './type/TileOverlayShape'; import LocAngle from './LocAngle'; import {Int32Array3d, TypedArray1d, TypedArray2d, TypedArray3d, TypedArray4d} from '../util/Arrays'; +import DrawGL from '../graphics/DrawGL'; export default class World3D { private static visibilityMatrix: boolean[][][][] = new TypedArray4d(8, 32, 51, 51, false); @@ -988,7 +989,14 @@ export default class World3D { World3D.clickTileZ = -1; }; + // WebGL change -> draw scene draw = (eyeX: number, eyeY: number, eyeZ: number, topLevel: number, eyeYaw: number, eyePitch: number, loopCycle: number): void => { + + // WebGL change -> pre-draw scene callback + if(DrawGL.GL_ENABLED) { + DrawGL.preDrawScene(eyeX, eyeY, eyeZ, topLevel, eyeYaw, eyePitch); + } + if (eyeX < 0) { eyeX = 0; } else if (eyeX >= this.maxTileX * 128) { @@ -1001,6 +1009,8 @@ export default class World3D { eyeZ = this.maxTileZ * 128 - 1; } + const distance = (DrawGL.GL_ENABLED ? DrawGL.renderDistance : 25); + World3D.cycle++; World3D.sinEyePitch = Draw3D.sin[eyePitch]; World3D.cosEyePitch = Draw3D.cos[eyePitch]; @@ -1015,22 +1025,22 @@ export default class World3D { World3D.eyeTileZ = (eyeZ / 128) | 0; World3D.topLevel = topLevel; - World3D.minDrawTileX = World3D.eyeTileX - 25; + World3D.minDrawTileX = World3D.eyeTileX - distance; if (World3D.minDrawTileX < 0) { World3D.minDrawTileX = 0; } - World3D.minDrawTileZ = World3D.eyeTileZ - 25; + World3D.minDrawTileZ = World3D.eyeTileZ - distance; if (World3D.minDrawTileZ < 0) { World3D.minDrawTileZ = 0; } - World3D.maxDrawTileX = World3D.eyeTileX + 25; + World3D.maxDrawTileX = World3D.eyeTileX + distance; if (World3D.maxDrawTileX > this.maxTileX) { World3D.maxDrawTileX = this.maxTileX; } - World3D.maxDrawTileZ = World3D.eyeTileZ + 25; + World3D.maxDrawTileZ = World3D.eyeTileZ + distance; if (World3D.maxDrawTileZ > this.maxTileZ) { World3D.maxDrawTileZ = this.maxTileZ; } @@ -1047,7 +1057,11 @@ export default class World3D { continue; } - if (tile.drawLevel <= topLevel && (World3D.visibilityMap[x + 25 - World3D.eyeTileX][z + 25 - World3D.eyeTileZ] || this.levelHeightmaps[level][x][z] - eyeY >= 2000)) { + // WebGL change -> visibility check (increase draw distance) + if (tile.drawLevel <= topLevel + && (!DrawGL.GL_ENABLED + && World3D.visibilityMap[x + distance - World3D.eyeTileX][z + distance - World3D.eyeTileZ] + || this.levelHeightmaps[level][x][z] - eyeY >= 2000)) { tile.visible = true; tile.update = true; tile.containsLocs = tile.locCount > 0; @@ -1063,7 +1077,7 @@ export default class World3D { for (let level: number = this.minLevel; level < this.maxLevel; level++) { const tiles: (Tile | null)[][] = this.levelTiles[level]; - for (let dx: number = -25; dx <= 0; dx++) { + for (let dx: number = -distance; dx <= 0; dx++) { const rightTileX: number = World3D.eyeTileX + dx; const leftTileX: number = World3D.eyeTileX - dx; @@ -1071,7 +1085,7 @@ export default class World3D { continue; } - for (let dz: number = -25; dz <= 0; dz++) { + for (let dz: number = -distance; dz <= 0; dz++) { const forwardTileZ: number = World3D.eyeTileZ + dz; const backwardTileZ: number = World3D.eyeTileZ - dz; let tile: Tile | null; @@ -1117,14 +1131,14 @@ export default class World3D { for (let level: number = this.minLevel; level < this.maxLevel; level++) { const tiles: (Tile | null)[][] = this.levelTiles[level]; - for (let dx: number = -25; dx <= 0; dx++) { + for (let dx: number = -distance; dx <= 0; dx++) { const rightTileX: number = World3D.eyeTileX + dx; const leftTileX: number = World3D.eyeTileX - dx; if (rightTileX < World3D.minDrawTileX && leftTileX >= World3D.maxDrawTileX) { continue; } - for (let dz: number = -25; dz <= 0; dz++) { + for (let dz: number = -distance; dz <= 0; dz++) { const forwardTileZ: number = World3D.eyeTileZ + dz; const backgroundTileZ: number = World3D.eyeTileZ - dz; let tile: Tile | null; diff --git a/src/js/jagex2/graphics/Canvas.ts b/src/js/jagex2/graphics/Canvas.ts index 38aa9b34..b2e5b4c1 100644 --- a/src/js/jagex2/graphics/Canvas.ts +++ b/src/js/jagex2/graphics/Canvas.ts @@ -1,6 +1,9 @@ +export const canvasFake: HTMLCanvasElement = document.createElement('canvas') as HTMLCanvasElement; export const canvas: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement; -export const canvas2d: CanvasRenderingContext2D = canvas.getContext('2d', {willReadFrequently: true})!; - +export const canvas2d: CanvasRenderingContext2D = canvasFake.getContext('2d', {willReadFrequently: true})!; export const jpegCanvas: HTMLCanvasElement = document.createElement('canvas'); export const jpegImg: HTMLImageElement = document.createElement('img'); export const jpeg2d: CanvasRenderingContext2D = jpegCanvas.getContext('2d', {willReadFrequently: true})!; +export const glCanvas: HTMLCanvasElement = document.createElement('canvas'); +export const gl: WebGL2RenderingContext = + canvas.getContext('webgl2',{willReadFrequently: true})! as WebGL2RenderingContext; diff --git a/src/js/jagex2/graphics/Draw3D.ts b/src/js/jagex2/graphics/Draw3D.ts index 966dfba2..2bfad00c 100644 --- a/src/js/jagex2/graphics/Draw3D.ts +++ b/src/js/jagex2/graphics/Draw3D.ts @@ -2,6 +2,7 @@ import Draw2D from './Draw2D'; import Pix8 from './Pix8'; import Jagfile from '../io/Jagfile'; import {Int32Array2d, TypedArray1d} from '../util/Arrays'; +import DrawGL from './DrawGL'; // noinspection JSSuspiciousNameCombination,DuplicatedCode export default class Draw3D extends Draw2D { @@ -15,6 +16,7 @@ export default class Draw3D extends Draw2D { static textures: (Pix8 | null)[] = new TypedArray1d(50, null); static textureCount: number = 0; + static textureBrightness: number = 1; static lineOffset: Int32Array = new Int32Array(); static centerX: number = 0; @@ -152,6 +154,7 @@ export default class Draw3D extends Draw2D { }; static setBrightness = (brightness: number): void => { + this.textureBrightness = brightness; const randomBrightness: number = brightness + Math.random() * 0.03 - 0.015; let offset: number = 0; for (let y: number = 0; y < 512; y++) { @@ -261,6 +264,10 @@ export default class Draw3D extends Draw2D { }; static fillGouraudTriangle = (xA: number, xB: number, xC: number, yA: number, yB: number, yC: number, colorA: number, colorB: number, colorC: number): void => { + //WebGL change -> don't draw on CPU if GL is enabled + if(DrawGL.GL_ENABLED) { + return; + } let xStepAB: number = 0; let colorStepAB: number = 0; if (yB !== yA) { @@ -884,6 +891,10 @@ export default class Draw3D extends Draw2D { }; static fillTriangle = (x0: number, x1: number, x2: number, y0: number, y1: number, y2: number, color: number): void => { + //WebGL change -> don't draw on CPU if GL is enabled + if(DrawGL.GL_ENABLED) { + return; + } let xStepAB: number = 0; if (y1 !== y0) { xStepAB = (((x1 - x0) << 16) / (y1 - y0)) | 0; @@ -1326,6 +1337,11 @@ export default class Draw3D extends Draw2D { tzC: number, texture: number ): void => { + //WebGL change -> don't draw on CPU if GL is enabled + if(DrawGL.GL_ENABLED) { + return; + } + const texels: Int32Array | null = this.getTexels(texture); this.opaque = !this.textureTranslucent[texture]; diff --git a/src/js/jagex2/graphics/DrawGL.ts b/src/js/jagex2/graphics/DrawGL.ts new file mode 100644 index 00000000..2e49c3ea --- /dev/null +++ b/src/js/jagex2/graphics/DrawGL.ts @@ -0,0 +1,663 @@ +import { gl } from "./Canvas"; +import Draw2D from "./Draw2D"; +import Draw3D from "./Draw3D"; +import GLBuffer from "./GLBuffer"; +import GLFloatBuffer from "./GLFloatBuffer"; +import GLIntBuffer from "./GLIntBuffer"; +import { GLShader } from "./GLShader"; +import Model from "./Model"; + +export default class DrawGL { + + static GL_ENABLED: boolean = true; + + static uniformIntBuffer: GLIntBuffer; + static vertexBuffer: GLIntBuffer; + static uvBuffer: GLFloatBuffer; + + static targetBufferOffset : number; + static glInitted : boolean = false; + static renderDistance : number = 25; + + private static readonly TEXTURE_SIZE = 128; + + private static readonly tmpVertexBuffer: GLBuffer = new GLBuffer("vertex buffer"); + private static readonly tmpUvBuffer: GLBuffer = new GLBuffer("uv buffer"); + private static readonly uniformBuffer : GLBuffer = new GLBuffer("uniform buffer"); + + private static readonly GameShaderProgram : GLShader = new GLShader() + .add(gl.VERTEX_SHADER, "gpu/vert.glsl") + .add(gl.FRAGMENT_SHADER, "gpu/frag.glsl"); + + private static readonly UIShaderProgram : GLShader = new GLShader() + .add(gl.VERTEX_SHADER, "gpu/vertui.glsl") + .add(gl.FRAGMENT_SHADER, "gpu/fragui.glsl"); + + private static glProgram: WebGLProgram; + private static glUiProgram: WebGLProgram; + private static vaoTemp : any; + + private static interfaceTexture : WebGLTexture|null; + private static interfacePbo : any; + + private static vaoUiHandle : any; + private static vboUiHandle : any; + + private static textureArrayId : WebGLTexture = -1; + private static tileHeightTex : any; + + private static lastCanvasWidth : any; + private static lastCanvasHeight : any; + + private static cameraX : number = 1; + private static cameraY : number = 1; + private static cameraZ : number = 10; + private static cameraYaw : number = 128; + private static cameraPitch : number = 128; + + + private static uniProjectionMatrix: WebGLUniformLocation; + private static uniBrightness: WebGLUniformLocation; + private static uniSmoothBanding: WebGLUniformLocation; + private static uniUseFog: WebGLUniformLocation; + private static uniFogColor: WebGLUniformLocation; + private static uniFogDepth: WebGLUniformLocation; + private static uniDrawDistance: WebGLUniformLocation; + private static uniExpandedMapLoadingChunks: WebGLUniformLocation; + private static uniTextureLightMode: WebGLUniformLocation; + private static uniTick: WebGLUniformLocation; + private static uniBlockMain: number; + private static uniTextures: WebGLUniformLocation; + private static uniTextureAnimations: WebGLUniformLocation; + + private static uniTex: WebGLUniformLocation; + private static uniTexSamplingMode: WebGLUniformLocation; + private static uniTexTargetDimensions: WebGLUniformLocation; + private static uniTexSourceDimensions: WebGLUniformLocation; + private static uniUiAlphaOverlay: WebGLUniformLocation; + + private static glRenderer: string; + private static glVersion: string; + + static init = async (): Promise => { + if (!gl) { + throw new Error('WebGL 2.0 not supported'); + } + else { + DrawGL.glVersion = gl.getParameter(gl.VERSION); + DrawGL.glRenderer = gl.getParameter(gl.RENDERER); + DrawGL.GL_ENABLED = true; + } + + DrawGL.targetBufferOffset = 0; + + //GLManager.initSortingBuffers(); + + console.log('DrawGL.init()'); + // check errors + DrawGL.checkGLErrors(); + + // buffers + DrawGL.uniformIntBuffer = new GLIntBuffer(); + DrawGL.vertexBuffer = new GLIntBuffer(); + DrawGL.uvBuffer = new GLFloatBuffer(); + + + //sync mode (TODO: is this needed? it is for frame rate limiting/unlocking but original code swaps the AWT context. not sure if valid in webgl context) + console.log('DrawGL buffers created'); + + // init buffers + DrawGL.initBuffers(); + console.log('DrawGL initBuffers() done'); + + // init vao + DrawGL.initVao(); + console.log('DrawGL initVao() done'); + + + // init program + await DrawGL.initProgram(); + + console.log('DrawGL initProgram() done'); + + + // init textures + DrawGL.initInterfaceTexture(); + + // init uniform buffer + //DrawGL.initUniformBuffer2(); + + console.log('DrawGL initUniformBuffer() !'); + + DrawGL.lastCanvasHeight = -1; + DrawGL.lastCanvasWidth = -1; + DrawGL.textureArrayId = -1; + + DrawGL.glInitted = true; + DrawGL.checkGLErrors(); + console.log('DrawGL.init() done'); + }; + + static initVao = (): void => { + // Create temp VAO + DrawGL.vaoTemp = gl.createVertexArray(); + gl.bindVertexArray(DrawGL.vaoTemp); + + gl.enableVertexAttribArray(0); + gl.bindBuffer(gl.ARRAY_BUFFER, DrawGL.tmpVertexBuffer.glBufferId); + gl.vertexAttribIPointer(0, 4, gl.INT, 0, 0); + + gl.enableVertexAttribArray(1); + gl.bindBuffer(gl.ARRAY_BUFFER, DrawGL.tmpUvBuffer.glBufferId); + gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0); + + gl.bindVertexArray(null); + + // Create UI VAO + DrawGL.vaoUiHandle = gl.createVertexArray(); + // Create UI buffer + DrawGL.vboUiHandle = gl.createBuffer(); + gl.bindVertexArray(DrawGL.vaoUiHandle); + + const vboUiBuf: Float32Array = new Float32Array([ + // positions // texture coords + 1.0, 1.0, 0.0, 1.0, 0.0, // top right + 1.0, -1.0, 0.0, 1.0, 1.0, // bottom right + -1.0, -1.0, 0.0, 0.0, 1.0, // bottom left + -1.0, 1.0, 0.0, 0.0, 0.0 // top left + ]); + gl.bindBuffer(gl.ARRAY_BUFFER, DrawGL.vboUiHandle); + gl.bufferData(gl.ARRAY_BUFFER, vboUiBuf, gl.STATIC_DRAW); + + // position attribute + gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 5 * Float32Array.BYTES_PER_ELEMENT, 0); + gl.enableVertexAttribArray(0); + + // texture coord attribute + gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 5 * Float32Array.BYTES_PER_ELEMENT, 3 * Float32Array.BYTES_PER_ELEMENT); + gl.enableVertexAttribArray(1); + + // unbind VBO + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindVertexArray(null); + }; + + static shutdownVbo (): void { + gl.deleteVertexArray(DrawGL.vaoTemp); + DrawGL.vaoTemp = null; + } + + static initBuffers = (): void => { + DrawGL.initGlBuffer(DrawGL.tmpVertexBuffer); + DrawGL.initGlBuffer(DrawGL.tmpUvBuffer); + DrawGL.initGlBuffer(DrawGL.uniformBuffer); + }; + + static initGlBuffer = (glBuffer:GLBuffer): void => { + glBuffer.glBufferId = gl.createBuffer()!; + console.log(`initGlBuffer: ${glBuffer.name} ${glBuffer.glBufferId}`) + } + + static convertPixels(srcPixels:Int8Array, width:number, height:number, textureWidth:number, textureHeight:number) : Uint8Array { + const pixels = new Uint8Array(textureWidth * textureHeight * 4); + + let pixelIdx = 0; + let srcPixelIdx = 0; + + let offset = (textureWidth - width) * 4; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let rgb = srcPixels[srcPixelIdx++]; + if (rgb != 0) { + pixels[pixelIdx++] = (rgb >> 16); + pixels[pixelIdx++] = (rgb >> 8); + pixels[pixelIdx++] = rgb; + pixels[pixelIdx++] = -1; + } else { + pixelIdx += 4; + } + } + pixelIdx += offset; + } + return pixels; + } + + private static updateTextures(textureArrayId:WebGLTexture) :void{ + const textures = Draw3D.textures; + + gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureArrayId); + + let cnt = 0; + for (let textureId = 0; textureId < textures.length; textureId++) { + let texture = textures[textureId]; + if (texture != null) { + const texturePixels = texture.pixels; + if (texturePixels.length === 0) { + continue; // this can't happen + } + + ++cnt; + + if (texturePixels.length != DrawGL.TEXTURE_SIZE * DrawGL.TEXTURE_SIZE) { + // The texture storage is 128x128 bytes, and will only work correctly with the + // 128x128 textures from high detail mode + //continue; + } + + const pixels = DrawGL.convertPixels(texturePixels, DrawGL.TEXTURE_SIZE, DrawGL.TEXTURE_SIZE, DrawGL.TEXTURE_SIZE, DrawGL.TEXTURE_SIZE); + // = new Uint8Array(texturePixels);//DrawGL.getPixelsAsUint8ArrayFromSigned(texturePixels); + gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, textureId, DrawGL.TEXTURE_SIZE, DrawGL.TEXTURE_SIZE, + 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + } + } + } + + static initTextureArray():void { + if (!Draw3D.textures === null || Draw3D.textures.length === 0) { + return; + } + + const textures = Draw3D.textures; + + const textureArrayId = gl.createTexture()!; + gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureArrayId); + gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, DrawGL.TEXTURE_SIZE, DrawGL.TEXTURE_SIZE, textures.length); + gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + + // Set brightness to 1.0 to upload unmodified textures to GPU + const save = Draw3D.textureBrightness; + Draw3D.setBrightness(1.0); + + DrawGL.updateTextures(textureArrayId); + + Draw3D.setBrightness(save); + + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureArrayId); + gl.activeTexture(gl.TEXTURE0); + + DrawGL.textureArrayId = textureArrayId; + } + + static shutdownBuffers = (): void => { + DrawGL.destroyGlBuffer(DrawGL.tmpVertexBuffer); + DrawGL.destroyGlBuffer(DrawGL.tmpUvBuffer); + DrawGL.destroyGlBuffer(DrawGL.uniformBuffer); + } + + static destroyGlBuffer = (glBuffer:GLBuffer): void => { + if (glBuffer.glBufferId != -1) { + + gl.deleteBuffer(glBuffer.glBufferId); + glBuffer.glBufferId = -1; + } + glBuffer.size = -1; + } + + static async initProgram(): Promise { + DrawGL.glProgram = await DrawGL.GameShaderProgram.compile(); + DrawGL.glUiProgram = await DrawGL.UIShaderProgram.compile(); + DrawGL.initUniforms(); + } + + static initUniforms(): void { + DrawGL.uniProjectionMatrix = gl.getUniformLocation(DrawGL.glProgram, "projectionMatrix")!; + DrawGL.uniBrightness = gl.getUniformLocation(DrawGL.glProgram, "brightness")!; + DrawGL.uniSmoothBanding = gl.getUniformLocation(DrawGL.glProgram, "smoothBanding")!; + DrawGL.uniUseFog = gl.getUniformLocation(DrawGL.glProgram, "useFog")!; + DrawGL.uniFogColor = gl.getUniformLocation(DrawGL.glProgram, "fogColor")!; + DrawGL.uniFogDepth = gl.getUniformLocation(DrawGL.glProgram, "fogDepth")!; + DrawGL.uniDrawDistance = gl.getUniformLocation(DrawGL.glProgram, "drawDistance")!; + DrawGL.uniExpandedMapLoadingChunks = gl.getUniformLocation(DrawGL.glProgram, "expandedMapLoadingChunks")!; + DrawGL.uniTextureLightMode = gl.getUniformLocation(DrawGL.glProgram, "textureLightMode")!; + //DrawGL.uniTick = gl.getUniformLocation(DrawGL.glProgram, "tick")!; + DrawGL.uniBlockMain = gl.getUniformBlockIndex(DrawGL.glProgram, "uniforms")!; + DrawGL.uniTextures = gl.getUniformLocation(DrawGL.glProgram, "textures")!; + //DrawGL.uniTextureAnimations = gl.getUniformLocation(DrawGL.glProgram, "textureAnimations")!; + + DrawGL.uniTex = gl.getUniformLocation(DrawGL.glUiProgram, "tex")!; + // DrawGL.uniTexSamplingMode = gl.getUniformLocation(DrawGL.glUiProgram, "samplingMode")!; + //DrawGL.uniTexTargetDimensions = gl.getUniformLocation(DrawGL.glUiProgram, "targetDimensions")!; + //DrawGL.uniTexSourceDimensions = gl.getUniformLocation(DrawGL.glUiProgram, "sourceDimensions")!; + //DrawGL.uniUiAlphaOverlay = gl.getUniformLocation(DrawGL.glUiProgram, "alphaOverlay")!; + } + + static shutdownProgram(): void { + gl.deleteProgram(DrawGL.glProgram); + DrawGL.glProgram = -1; + gl.deleteProgram(DrawGL.glUiProgram); + DrawGL.glUiProgram = -1; + } + + private static initInterfaceTexture(): void { + DrawGL.interfacePbo = gl.createBuffer()!; + DrawGL.interfaceTexture = gl.createTexture()!; + gl.bindTexture(gl.TEXTURE_2D, DrawGL.interfaceTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); // optional: gl.REPEAT + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // optional: gl.REPEAT + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.bindTexture(gl.TEXTURE_2D, null); + } + + private static shutdownInterfaceTexture(): void { + gl.deleteBuffer(DrawGL.interfacePbo); + gl.deleteTexture(DrawGL.interfaceTexture); + DrawGL.interfaceTexture = null; + } + + /*private static prepareInterfaceTexture(canvasWidth: number, canvasHeight: number): void { + if (canvasWidth != DrawGL.lastCanvasWidth || canvasHeight != DrawGL.lastCanvasHeight) { + DrawGL.lastCanvasWidth = canvasWidth; + DrawGL.lastCanvasHeight = canvasHeight; + + gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, DrawGL.interfacePbo); + gl.bufferData(gl.PIXEL_UNPACK_BUFFER, canvasWidth * canvasHeight * 4, gl.STATIC_DRAW); + gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null); + + gl.bindTexture(gl.TEXTURE_2D, DrawGL.interfaceTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, canvasWidth, canvasHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.bindTexture(gl.TEXTURE_2D, null); + } + const pixels = Draw2D.pixels; + const pixelBuffer:Uint8Array = DrawGL.getPixelsAsUint8Array(pixels); + + // Upload the pixel buffer to the PBO + gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, DrawGL.interfacePbo); + gl.bufferSubData(gl.PIXEL_UNPACK_BUFFER, 0, pixelBuffer); + gl.bindTexture(gl.TEXTURE_2D, DrawGL.interfaceTexture); + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, DrawGL.lastCanvasWidth, DrawGL.lastCanvasHeight, gl.RGBA, gl.UNSIGNED_BYTE, 0); + gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null); + gl.bindTexture(gl.TEXTURE_2D, null); + }*/ + + private static noDraw:boolean = false; + + /// + /// Float.floatToIntBits (Java) equivalent + /// + private static floatToIntBitsEq(f:number) : number + { + let buffer = new ArrayBuffer(4); + let view = new DataView(buffer); + view.setFloat32(0, f, false); // false for big-endian + return view.getInt32(0, false); // false for big-endian + } + + private static getPixelsAsUint8Array(arr: Int32Array): Uint8Array { + let pixels:Uint8Array = new Uint8Array(arr.buffer); + for (let i = 0; i < pixels.length; i += 4) { + let temp = pixels[i]; + pixels[i] = pixels[i + 2]; + pixels[i + 2] = temp; + } + return pixels; + } + + static uniformBufferAlloc(): void { + + // Bind the buffer + gl.bindBuffer(gl.UNIFORM_BUFFER, DrawGL.uniformBuffer.glBufferId); + + // Create a new Float32Array to hold your data + let data = new Int32Array(2048 * 4 + 9); + + const centerX = Draw3D.centerY; + const centerY = Draw3D.centerX; + // Fill the data array + data[2] = centerX; + data[3] = centerY; + data[4] = 1.0; + data[5] = DrawGL.cameraX; + data[6] = DrawGL.cameraY; + data[7] = DrawGL.cameraZ; + for (let i = 0; i < 2048; i++) { + data[8 + i * 2] = Draw3D.sin[i]; + data[9 + i * 2] = Draw3D.cos[i]; + } + + // Fill the buffer with data + gl.bufferData(gl.UNIFORM_BUFFER, data, gl.DYNAMIC_DRAW); + // DrawGL.updateBufferWithData(DrawGL.uniformBuffer, gl.UNIFORM_BUFFER, uniformBuf, gl.DYNAMIC_DRAW); + + // Bind the buffer to a specific binding point + let bindingPoint = 2; + gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPoint, DrawGL.uniformBuffer.glBufferId); + } + + static preDrawScene(eyeX: number, eyeY: number, eyeZ: number, topLevel: number, eyeYaw: number, eyePitch: number): void { + if(typeof DrawGL.glProgram === 'undefined' || DrawGL.glProgram === -1 || this.noDraw){ + return; + } + + // To be implemented, if necessary (unused callback) + } + + static postDrawScene(): void { + + if(typeof DrawGL.glProgram === 'undefined' || DrawGL.glProgram === -1 || this.noDraw){ + return; + } + + // Reverse buffers for update + DrawGL.vertexBuffer.flip(); + DrawGL.uvBuffer.flip(); + + // Update the vertex and uv buffers + DrawGL.updateBufferWithData(DrawGL.tmpVertexBuffer, gl.ARRAY_BUFFER, DrawGL.vertexBuffer, gl.STREAM_DRAW); + DrawGL.updateBufferWithData(DrawGL.tmpUvBuffer, gl.ARRAY_BUFFER, DrawGL.uvBuffer, gl.STREAM_DRAW); + + DrawGL.checkGLErrors(); + } + + static createProjectionMatrix(left:number, right:number, bottom:number, top:number, near:number, far:number) { + // create a standard orthographic projection + let tx = -((right + left) / (right - left)); + let ty = -((top + bottom) / (top - bottom)); + let tz = -((far + near) / (far - near)); + + // TODO: refactor into main useProgram setup already in draw() + gl.useProgram(DrawGL.glProgram); + + const matrix = new Float32Array([ + 2 / (right - left), 0, 0, 0, + 0, 2 / (top - bottom), 0, 0, + 0, 0, -2 / (far - near), 0, + tx, ty, tz, 1 + ]); + gl.uniformMatrix4fv(DrawGL.uniProjectionMatrix, false, matrix, 0); + gl.useProgram(null); + } + + static draw(): void { + + if(typeof DrawGL.glProgram === 'undefined' || DrawGL.glProgram === -1 || this.noDraw){ + return; + } + + const drawDistance = 25; + const LOCAL_COORD_BITS = 7; + const SCENE_SIZE = 104; + const LOCAL_TILE_SIZE = 1 << LOCAL_COORD_BITS; // 128 - size of a tile in local coordinates + const width = gl.canvas.width; + const height = gl.canvas.height; + + if (width != DrawGL.lastCanvasWidth || height != DrawGL.lastCanvasHeight) { + DrawGL.createProjectionMatrix(0, width, height, 0, 0, SCENE_SIZE * LOCAL_TILE_SIZE); + DrawGL.lastCanvasWidth = width; + DrawGL.lastCanvasHeight = height; + } + + // currently a misnomer. this simply uploads our array buffers + DrawGL.postDrawScene(); + DrawGL.uniformBufferAlloc(); + + // textures + if (DrawGL.textureArrayId == -1) { + // lazy init textures as they may not be loaded at plugin start. + // this will return -1 and retry if not all textures are loaded yet, too. + DrawGL.initTextureArray(); + } + + const sky: number= 0x87CEEB;//0x555555;//0x87CEEB; + //gl.clearColor((sky >> 16 & 0xFF) / 255.0, (sky >> 8 & 0xFF) / 255.0, (sky & 0xFF) / 255.0, 1.0); + gl.clearColor(0.0, 0.0, 0.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.viewport(0, 0, DrawGL.lastCanvasWidth, DrawGL.lastCanvasHeight); + + // Draw the 3D scene + gl.useProgram(DrawGL.glProgram); + + //const LOCAL_HALF_TILE_SIZE = LOCAL_TILE_SIZE / 2; + gl.uniform1i(DrawGL.uniUseFog, 0);//fogDepth > 0 ? 1 : 0 + gl.uniform4f(DrawGL.uniFogColor, (sky >> 16 & 0xFF) / 255.0, (sky >> 8 & 0xFF) / 255.0, (sky & 0xFF) / 255.0, 1.0); + gl.uniform1i(DrawGL.uniFogDepth, 0);//fogDepth + gl.uniform1i(DrawGL.uniDrawDistance, drawDistance * LOCAL_TILE_SIZE); + gl.uniform1i(DrawGL.uniExpandedMapLoadingChunks, /*client.getExpandedMapLoading()*/1.0); + + // Brightness happens to also be stored in the texture provider, so we use that + gl.uniform1f(DrawGL.uniBrightness, /*(float) textureProvider.getBrightness()*/.80); + gl.uniform1f(DrawGL.uniSmoothBanding, 1.0/*config.smoothBanding() ? 0f : 1f*/); + gl.uniform1f(DrawGL.uniTextureLightMode, 1); // BRIGHT TEXTURES CONFIG + /*if (gameState == GameState.LOGGED_IN) + { + // avoid textures animating during loading + gl.uniform1i(uniTick, client.getGameCycle()); + }*/ + + // Bind uniforms + gl.uniformBlockBinding(DrawGL.glProgram, DrawGL.uniBlockMain, 2); + gl.uniform1i(DrawGL.uniTextures, 1); // texture sampler array is bound to texture1 + + // We just allow the GL to do face culling. Note this requires the priority renderer + // to have logic to disregard culled faces in the priority depth testing. + gl.enable(gl.CULL_FACE); // Enable face culling + gl.enable(gl.BLEND); + + //TODO; this may be the source of the texture issues. + // Enable blending for alpha + ///gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + // Draw buffers + // Only use the temporary buffers, which will contain the full scene + gl.bindVertexArray(DrawGL.vaoTemp); + + //console.log(`DrawGL.targetBufferOffset: ${DrawGL.targetBufferOffset} | VertexBuffer Length: ${DrawGL.vertexBuffer.getSize()} | UVBuffer Length: ${DrawGL.uvBuffer.getSize()}`); + //console.dir(DrawGL.vertexBuffer.getBuffer()); + gl.drawArrays(gl.TRIANGLES, 0, DrawGL.targetBufferOffset); + + gl.disable(gl.BLEND); + gl.disable(gl.CULL_FACE); + + gl.useProgram(null); + + DrawGL.checkGLErrors(); + + DrawGL.uniformIntBuffer.clear(); + DrawGL.vertexBuffer.clear(); + DrawGL.uvBuffer.clear(); + DrawGL.targetBufferOffset = 0; + + // Draw the UI + DrawGL.drawUi(0); + } + + private static drawUi(overlayColor: number): void { + + const canvasWidth: number = gl.canvas.width; + const canvasHeight: number = gl.canvas.height; + + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + + gl.bindTexture(gl.TEXTURE_2D, DrawGL.interfaceTexture); + const pixBuf = DrawGL.getPixelsAsUint8Array(Draw2D.pixels); + // if (!DrawGL.textureInit || canvasWidth !== DrawGL.lastCanvasWidth || canvasHeight !== DrawGL.lastCanvasHeight) { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, canvasWidth, canvasHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixBuf); + DrawGL.lastCanvasWidth = canvasWidth; + DrawGL.lastCanvasHeight = canvasHeight; + // DrawGL.textureInit = true; + // console.log('DrawGL: textureInit'); + //} else { + // gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, canvasWidth, canvasHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixBuf); + //} + + gl.useProgram(DrawGL.glUiProgram); + gl.uniform1i(DrawGL.uniTex, 0); + gl.bindVertexArray(DrawGL.vaoUiHandle); + gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); + + // Reset + Draw3D.clear(); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.bindVertexArray(null); + gl.useProgram(null); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.disable(gl.BLEND); + + DrawGL.vertexBuffer.clear(); + + DrawGL.checkGLErrors(); + } + + // todo: circlular dependency with Model + private static drawModel(model:Model, orientation: number, pitchSine: number, pitchCos: number, yawSin: number, yawCos: number, offsetX: number, offsetY: number, offsetZ: number, bitset: number): void { + if(typeof DrawGL.glProgram === 'undefined' || DrawGL.glProgram === -1 || this.noDraw){ + return; + } + } + + private static updateBufferWithData(glBuffer:GLBuffer, target: number, data: any, usage: number) : void + { + const buffer = data.getBuffer(); + const size = buffer.length; + gl.bindBuffer(target, glBuffer.glBufferId); + if (size > glBuffer.size) + { + let newSize = Math.max(size, glBuffer.size * 2); + glBuffer.size = newSize; + gl.bufferData(target, newSize * buffer.BYTES_PER_ELEMENT, usage); + } + gl.bufferSubData(target, 0, buffer); + } + + private static checkGLErrors(): void{ + // Check for GL errors + for (; ; ) + { + let err = gl.getError(); + if (err == gl.NO_ERROR) + { + return; + } + + let errStr; + switch (err) + { + case gl.INVALID_ENUM: + errStr = "INVALID_ENUM"; + break; + case gl.INVALID_VALUE: + errStr = "INVALID_VALUE"; + break; + case gl.INVALID_OPERATION: + errStr = "INVALID_OPERATION"; + break; + case gl.INVALID_FRAMEBUFFER_OPERATION: + errStr = "INVALID_FRAMEBUFFER_OPERATION"; + break; + default: + errStr = "" + err; + break; + } + + console.log("glGetError:", new Error(errStr)); + this.noDraw = true; + return; + } + } +} \ No newline at end of file diff --git a/src/js/jagex2/graphics/GLBuffer.ts b/src/js/jagex2/graphics/GLBuffer.ts new file mode 100644 index 00000000..0e0f54b8 --- /dev/null +++ b/src/js/jagex2/graphics/GLBuffer.ts @@ -0,0 +1,8 @@ +export default class GLBuffer { + name: string; + glBufferId: WebGLBuffer = -1; + size: number = -1; + constructor(name: string) { + this.name = name; + } +} \ No newline at end of file diff --git a/src/js/jagex2/graphics/GLFloatBuffer.ts b/src/js/jagex2/graphics/GLFloatBuffer.ts new file mode 100644 index 00000000..640f7541 --- /dev/null +++ b/src/js/jagex2/graphics/GLFloatBuffer.ts @@ -0,0 +1,59 @@ +export default class GLFloatBuffer { + private buffer: Float32Array; + private position: number = 0; + + constructor(allocation: number = 65536) { + this.buffer = new Float32Array(allocation); + } + + put(x: number, y: number, z: number): GLFloatBuffer { + this.buffer[this.position++] = x; + this.buffer[this.position++] = y; + this.buffer[this.position++] = z; + return this; + } + + putC(x: number, y: number, z: number, c: number): GLFloatBuffer { + this.buffer[this.position++] = x; + this.buffer[this.position++] = y; + this.buffer[this.position++] = z; + this.buffer[this.position++] = c; + return this; + } + + flip(): void { + this.buffer = this.buffer.slice(0, this.position); + } + + clear(): void { + this.buffer = new Float32Array(65536); + this.position = 0; + } + + ensureCapacity(size: number): void { + const capacity = this.buffer.length; + const position = this.buffer.length; + if (capacity - position < size) { + let newCapacity = capacity; + while (newCapacity - position < size) { + newCapacity *= 2; + } + + const newBuffer = new Float32Array(newCapacity); + newBuffer.set(this.buffer); + this.buffer = newBuffer; + } + } + + remaining(): number { + return this.buffer.length - this.position; + } + + getBuffer(): Float32Array { + return this.buffer; + } + + getSize(): number { + return this.buffer.length; + } +} \ No newline at end of file diff --git a/src/js/jagex2/graphics/GLIntBuffer.ts b/src/js/jagex2/graphics/GLIntBuffer.ts new file mode 100644 index 00000000..e16f0aa1 --- /dev/null +++ b/src/js/jagex2/graphics/GLIntBuffer.ts @@ -0,0 +1,71 @@ +export default class GLIntBuffer { + private buffer: Int32Array; + private position: number = 0; + + constructor(allocation: number = 65536) { + this.buffer = new Int32Array(allocation); + } + + put(x: number, y: number, z: number): GLIntBuffer { + this.buffer[this.position++] = x; + this.buffer[this.position++] = y; + this.buffer[this.position++] = z; + return this; + } + + putC(x: number, y: number, z: number, c: number): GLIntBuffer { + this.buffer[this.position++] = x; + this.buffer[this.position++] = y; + this.buffer[this.position++] = z; + this.buffer[this.position++] = c; + return this; + } + + putArray(array: Int32Array): GLIntBuffer { + for(let i = 0; i < array.length; i++) { + this.buffer[this.position++] = array[i]; + } + return this; + } + + putVal(value: number): GLIntBuffer { + this.buffer[this.position++] = value; + return this; + } + + flip(): void { + this.buffer = this.buffer.slice(0, this.position); + } + + clear(): void { + this.buffer = new Int32Array(65536); + this.position = 0; + } + + ensureCapacity(size: number): void { + const capacity = this.buffer.length; + const position = this.buffer.length; + if (capacity - position < size) { + let newCapacity = capacity; + while (newCapacity - position < size) { + newCapacity *= 2; + } + const newBuffer = new Int32Array(newCapacity); + newBuffer.set(this.buffer); + this.buffer = newBuffer; + } + } + + // return the remaining space in the buffer + remaining(): number { + return this.buffer.length - this.position; + } + + getBuffer(): Int32Array { + return this.buffer; + } + + getSize(): number { + return this.buffer.length; + } +} \ No newline at end of file diff --git a/src/js/jagex2/graphics/GLManager.ts b/src/js/jagex2/graphics/GLManager.ts new file mode 100644 index 00000000..cb3b812d --- /dev/null +++ b/src/js/jagex2/graphics/GLManager.ts @@ -0,0 +1,341 @@ +import { Int32Array2d } from "../util/Arrays"; +import { canvas } from "./Canvas"; +import Draw2D from "./Draw2D"; +import Draw3D from "./Draw3D"; +import GLFloatBuffer from "./GLFloatBuffer"; +import GLIntBuffer from "./GLIntBuffer"; +import Model from "./Model"; + +export default class GLManager { + private static distances: Int32Array|null; + private static distanceFaceCount: string[]|null; + private static distanceToFaces: string[][]|null; + + private static modelCanvasX: Float32Array|null; + private static modelCanvasY: Float32Array|null; + + private static modelLocalX: Int32Array|null; + private static modelLocalY: Int32Array|null; + private static modelLocalZ: Int32Array|null; + + private static numOfPriority: Int32Array|null; + private static eq10: Int32Array|null; + private static eq11: Int32Array|null; + private static lt10: Int32Array|null; + private static orderedFaces: Int32Array2d|null; + + static initSortingBuffers() { + const MAX_VERTEX_COUNT = 6500; + const MAX_DIAMETER = 6000; + + this.distances = new Int32Array(MAX_VERTEX_COUNT); + this.distanceFaceCount = new Array(MAX_DIAMETER); + this.distanceToFaces = new Array(MAX_DIAMETER).fill(null).map(() => new Array(512)); + + this.modelCanvasX = new Float32Array(MAX_VERTEX_COUNT); + this.modelCanvasY = new Float32Array(MAX_VERTEX_COUNT); + + this.modelLocalX = new Int32Array(MAX_VERTEX_COUNT); + this.modelLocalY = new Int32Array(MAX_VERTEX_COUNT); + this.modelLocalZ = new Int32Array(MAX_VERTEX_COUNT); + + this.numOfPriority = new Int32Array(12); + this.eq10 = new Int32Array(2000); + this.eq11 = new Int32Array(2000); + this.lt10 = new Int32Array(12); + this.orderedFaces = new Int32Array2d(12, 2000); + } + + static releaseSortingBuffers() { + this.distances = null; + this.distanceFaceCount = null; + this.distanceToFaces = null; + + this.modelCanvasX = null; + this.modelCanvasY = null; + + this.modelLocalX = null; + this.modelLocalY = null; + this.modelLocalZ = null; + + this.numOfPriority = null; + this.eq10 = null; + this.eq11 = null; + this.lt10 = null; + this.orderedFaces = null; + } + + static pushSortedModel(model: Model, yaw: number, sinEyePitch: number, cosEyePitch: number, sinEyeYaw: number, cosEyeYaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number, vertexBuffer: GLIntBuffer, uvBuffer: GLFloatBuffer): number { + + const zPrime: number = (relativeZ * cosEyeYaw - relativeX * sinEyeYaw) >> 16; + const midZ: number = (relativeY * sinEyePitch + zPrime * cosEyePitch) >> 16; + const radiusCosEyePitch: number = (model.radius * cosEyePitch) >> 16; + + const maxZ: number = midZ + radiusCosEyePitch; + if (maxZ <= 50 || midZ >= 3500) { + return 0; + } + + const midX: number = (relativeZ * sinEyeYaw + relativeX * cosEyeYaw) >> 16; + let leftX: number = (midX - model.radius) << 9; + if (((leftX / maxZ) | 0) >= Draw2D.centerX2d) { + return 0; + } + + let rightX: number = (midX + model.radius) << 9; + if (((rightX / maxZ) | 0) <= -Draw2D.centerX2d) { + return 0; + } + + const midY: number = (relativeY * cosEyePitch - zPrime * sinEyePitch) >> 16; + const radiusSinEyePitch: number = (model.radius * sinEyePitch) >> 16; + + let bottomY: number = (midY + radiusSinEyePitch) << 9; + if (((bottomY / maxZ) | 0) <= -Draw2D.centerY2d) { + return 0; + } + + const yPrime: number = radiusSinEyePitch + ((model.maxY * cosEyePitch) >> 16); + let topY: number = (midY - yPrime) << 9; + if (((topY / maxZ) | 0) >= Draw2D.centerY2d) { + return 0; + } + + const radiusZ: number = radiusCosEyePitch + ((model.maxY * sinEyePitch) >> 16); + + let clipped: boolean = midZ - radiusZ <= 50; + let picking: boolean = false; + + if (bitset > 0 && Model.checkHover) { + let z: number = midZ - radiusCosEyePitch; + if (z <= 50) { + z = 50; + } + + if (midX > 0) { + leftX = (leftX / maxZ) | 0; + rightX = (rightX / z) | 0; + } else { + rightX = (rightX / maxZ) | 0; + leftX = (leftX / z) | 0; + } + + if (midY > 0) { + topY = (topY / maxZ) | 0; + bottomY = (bottomY / z) | 0; + } else { + bottomY = (bottomY / maxZ) | 0; + topY = (topY / z) | 0; + } + + const mouseX: number = Model.mouseX - Draw3D.centerX; + const mouseY: number = Model.mouseY - Draw3D.centerY; + if (mouseX > leftX && mouseX < rightX && mouseY > topY && mouseY < bottomY) { + if (model.pickable) { + Model.pickedBitsets[Model.pickedCount++] = bitset; + } else { + picking = true; + } + } + } + + // vertex count + const vertexCount = model.vertexCount; + // vertices on X, Y, Z + const verticesX = model.vertexX; + const verticesY = model.vertexY; + const verticesZ = model.vertexZ; + + // face count + const faceCount = model.faceCount; + // faces + const faceVertexA = model.faceVertexA; + const faceVertexB = model.faceVertexB; + const faceVertexC = model.faceVertexC; + //face color + const faceColor = model.faceColor; + const facePriority = model.facePriority; + + const zoom: number = 1; + + // camera X, Y, Z + const centerX: number = Draw3D.centerX; + const centerY: number = Draw3D.centerY; + + let sinYaw: number = 0; + let cosYaw: number = 0; + if (yaw !== 0) { + sinYaw = Draw3D.sin[yaw]; + cosYaw = Draw3D.cos[yaw]; + } + + for (let v: number = 0; v < model.vertexCount; v++) { + let x: number = model.vertexX[v]; + let y: number = model.vertexY[v]; + let z: number = model.vertexZ[v]; + + let temp: number; + if (yaw !== 0) { + temp = (z * sinYaw + x * cosYaw) >> 16; + z = (z * cosYaw - x * sinYaw) >> 16; + x = temp; + } + + x += relativeX; + y += relativeY; + z += relativeZ; + + this.modelLocalX![v] = x; + this.modelLocalY![v] = y; + this.modelLocalZ![v] = z; + + temp = (z * sinEyeYaw + x * cosEyeYaw) >> 16; + z = (z * cosEyeYaw - x * sinEyeYaw) >> 16; + x = temp; + + temp = (y * cosEyePitch - z * sinEyePitch) >> 16; + z = (y * sinEyePitch + z * cosEyePitch) >> 16; + y = temp; + + if (Model.vertexScreenZ) { + Model.vertexScreenZ[v] = z - midZ; + } + + if (z >= 50 && Model.vertexScreenX && Model.vertexScreenY) { + Model.vertexScreenX[v] = centerX + (((x << 9) / z) | 0); + Model.vertexScreenY[v] = centerY + (((y << 9) / z) | 0); + } else if (Model.vertexScreenX) { + Model.vertexScreenX[v] = -5000; + clipped = true; + } + + if ((clipped || model.texturedFaceCount > 0) && Model.vertexViewSpaceX && Model.vertexViewSpaceY && Model.vertexViewSpaceZ) { + Model.vertexViewSpaceX[v] = x; + Model.vertexViewSpaceY[v] = y; + Model.vertexViewSpaceZ[v] = z; + } + } + + try { + // try catch for example a model being drawn from 3d can crash like at baxtorian falls + //this.draw2(clipped, picking, bitset); + } catch (err) { + /* empty */ + } + + return 0; + } + + private static pushFace(model: Model, face: number, vertexBuffer: GLIntBuffer, uvBuffer: GLFloatBuffer): number { + const indices1: Int32Array = model.faceVertexA; + const indices2: Int32Array = model.faceVertexB; + const indices3: Int32Array = model.faceVertexC; + + const faceColors1: Int32Array = model.faceColorA!; + const faceColors2: Int32Array = model.faceColorB!; + const faceColors3: Int32Array = model.faceColorC!; + + /*const overrideAmount: number = model.getOverrideAmount(); + const overrideHue: number = model.getOverrideHue(); + const overrideSat: number = model.getOverrideSaturation(); + const overrideLum: number = model.getOverrideLuminance();*/ + + const faceTextures: Int32Array = model.faceInfo!; + const textureFaces: Int32Array = model.faceInfo!; + const texIndices1: Int32Array = model.texturedVertexA; + const texIndices2: Int32Array = model.texturedVertexB; + const texIndices3: Int32Array = model.texturedVertexC; + + const faceRenderPriorities: Int32Array = model.facePriority!; + const transparencies: Int32Array = model.faceAlpha!; + + const packAlphaPriority: number = this.packAlphaPriority(faceTextures, transparencies, faceRenderPriorities, face); + + const triangleA: number = indices1[face]; + const triangleB: number = indices2[face]; + const triangleC: number = indices3[face]; + + let color1: number = faceColors1[face]; + let color2: number = faceColors2[face]; + let color3: number = faceColors3[face]; + + if (color3 === -1) { + color2 = color3 = color1; + } + + // HSL override is not applied to textured faces + /*if (faceTextures === null || faceTextures[face] === -1) { + if (overrideAmount > 0) { + color1 = this.interpolateHSL(color1, overrideHue, overrideSat, overrideLum, overrideAmount); + color2 = this.interpolateHSL(color2, overrideHue, overrideSat, overrideLum, overrideAmount); + color3 = this.interpolateHSL(color3, overrideHue, overrideSat, overrideLum, overrideAmount); + } + }*/ + + vertexBuffer.putC(this.modelLocalX![triangleA], this.modelLocalY![triangleA], this.modelLocalZ![triangleA], packAlphaPriority | color1); + vertexBuffer.putC(this.modelLocalX![triangleB], this.modelLocalY![triangleB], this.modelLocalZ![triangleB], packAlphaPriority | color2); + vertexBuffer.putC(this.modelLocalX![triangleC], this.modelLocalY![triangleC], this.modelLocalZ![triangleC], packAlphaPriority | color3); + + if (faceTextures !== null && faceTextures[face] !== -1) { + let texA: number, texB: number, texC: number; + + if (textureFaces !== null && textureFaces[face] !== -1) { + const tfaceIdx: number = textureFaces[face] & 0xff; + texA = texIndices1[tfaceIdx]; + texB = texIndices2[tfaceIdx]; + texC = texIndices3[tfaceIdx]; + } else { + texA = triangleA; + texB = triangleB; + texC = triangleC; + } + + const texture: number = faceTextures[face] + 1; + uvBuffer.putC(texture, this.modelLocalX![texA], this.modelLocalY![texA], this.modelLocalZ![texA]); + uvBuffer.putC(texture, this.modelLocalX![texB], this.modelLocalY![texB], this.modelLocalZ![texB]); + uvBuffer.putC(texture, this.modelLocalX![texC], this.modelLocalY![texC], this.modelLocalZ![texC]); + } else { + uvBuffer.putC(0, 0, 0, 0); + uvBuffer.putC(0, 0, 0, 0); + uvBuffer.putC(0, 0, 0, 0); + } + return 3; + } + + static packAlphaPriority(faceTextures:Int32Array, faceTransparencies:Int32Array, facePriorities:Int32Array, face:number):number { + let alpha = 0; + if (faceTransparencies != null && (faceTextures == null || faceTextures[face] == -1)) + { + alpha = (faceTransparencies[face] & 0xFF) << 24; + } + let priority = 0; + if (facePriorities != null) + { + priority = (facePriorities[face] & 0xff) << 16; + } + return alpha | priority; + } + + static interpolateHSL(hsl:number, hue2:any, sat2:any, lum2:any, lerp:any):number { + let hue:number = hsl >> 10 & 63; + let sat:number = hsl >> 7 & 7; + let lum:number = hsl & 127; + let var9:number = lerp & 255; + if (hue2 != -1) + { + hue += var9 * (hue2 - hue) >> 7; + } + + if (sat2 != -1) + { + sat += var9 * (sat2 - sat) >> 7; + } + + if (lum2 != -1) + { + lum += var9 * (lum2 - lum) >> 7; + } + + return (hue << 10 | sat << 7 | lum) & 65535; + } +} \ No newline at end of file diff --git a/src/js/jagex2/graphics/GLShader.ts b/src/js/jagex2/graphics/GLShader.ts new file mode 100644 index 00000000..31dd5769 --- /dev/null +++ b/src/js/jagex2/graphics/GLShader.ts @@ -0,0 +1,90 @@ +import { gl } from "./Canvas"; + +export class GLShader { + private units: Unit[] = []; + + public add(type: number, filename: string): GLShader { + this.units.push(new Unit(type, filename)); + return this; + } + + public async compile(): Promise { + const program: WebGLProgram = gl.createProgram()!; + const shaders: WebGLShader[] = new Array(this.units.length); + let i: number = 0; + let ok: boolean = false; + + try { + while (i < shaders.length) { + const unit: Unit = this.units[i]; + const shader: WebGLShader | null = gl.createShader(unit.getType); + + if (shader === null) { + throw new ShaderException(`Unable to create shader of type ${unit.getType}`); + } + console.log(unit.getFilename); + + let resp = await fetch(unit.getFilename); + let source = await resp.text(); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + const err: string | null = gl.getShaderInfoLog(shader); + gl.deleteShader(shader); + throw new ShaderException(err); + } + + gl.attachShader(program, shader); + shaders[i++] = shader; + } + + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + const err: string | null = gl.getProgramInfoLog(program); + throw new ShaderException(err); + } + + gl.validateProgram(program); + + if (!gl.getProgramParameter(program, gl.VALIDATE_STATUS)) { + const err: string | null = gl.getProgramInfoLog(program); + throw new ShaderException(err); + } + + ok = true; + } finally { + while (i > 0) { + const shader: WebGLShader = shaders[--i]; + gl.detachShader(program, shader); + gl.deleteShader(shader); + } + + if (!ok) { + gl.deleteProgram(program); + } + } + + return program; + } +} + +class Unit { + constructor(private readonly type: number, private readonly filename: string) {} + + public get getType(): number { + return this.type; + } + + public get getFilename(): string { + return this.filename; + } +} + +class ShaderException extends Error { + constructor(message: string | null) { + super(message ?? ''); + this.name = 'ShaderException'; + } +} \ No newline at end of file diff --git a/src/js/jagex2/graphics/Model.ts b/src/js/jagex2/graphics/Model.ts index 801f592f..4620328e 100644 --- a/src/js/jagex2/graphics/Model.ts +++ b/src/js/jagex2/graphics/Model.ts @@ -8,6 +8,8 @@ import SeqBase from './SeqBase'; import Hashable from '../datastruct/Hashable'; import {Int32Array2d, TypedArray1d} from '../util/Arrays'; +import { RenderMode } from './RenderMode'; +import DrawGL from './DrawGL'; class Metadata { vertexCount: number = 0; @@ -1041,11 +1043,13 @@ export default class Model extends Hashable { static model = (id: number): Model => { if (!Model.metadata) { + console.log('Error loading model metadata 1'); throw new Error('cant loading model metadata!!!!!'); } const meta: Metadata | null = Model.metadata[id]; if (!meta) { + console.log('Error loading model metadata 2'); console.log(`Error model:${id} not found!`); throw new Error('cant loading model metadata!!!!!'); } @@ -2038,25 +2042,27 @@ export default class Model extends Hashable { }; // todo: better name, Java relies on overloads - draw = (yaw: number, sinEyePitch: number, cosEyePitch: number, sinEyeYaw: number, cosEyeYaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number): void => { + draw = (yaw: number, sinEyePitch: number, cosEyePitch: number, sinEyeYaw: number, cosEyeYaw: number, relativeX: number, relativeY: number, relativeZ: number, bitset: number, renderMode: RenderMode = RenderMode.CPU): number => { const zPrime: number = (relativeZ * cosEyeYaw - relativeX * sinEyeYaw) >> 16; const midZ: number = (relativeY * sinEyePitch + zPrime * cosEyePitch) >> 16; const radiusCosEyePitch: number = (this.radius * cosEyePitch) >> 16; const maxZ: number = midZ + radiusCosEyePitch; - if (maxZ <= 50 || midZ >= 3500) { - return; + + // WebGL Change: Allow more rendering distance for WebGL + if (maxZ <= 50 || (midZ >= 3500 && !DrawGL.GL_ENABLED)) { + return 0; } const midX: number = (relativeZ * sinEyeYaw + relativeX * cosEyeYaw) >> 16; let leftX: number = (midX - this.radius) << 9; if (((leftX / maxZ) | 0) >= Draw2D.centerX2d) { - return; + return 0; } let rightX: number = (midX + this.radius) << 9; if (((rightX / maxZ) | 0) <= -Draw2D.centerX2d) { - return; + return 0; } const midY: number = (relativeY * cosEyePitch - zPrime * sinEyePitch) >> 16; @@ -2064,13 +2070,13 @@ export default class Model extends Hashable { let bottomY: number = (midY + radiusSinEyePitch) << 9; if (((bottomY / maxZ) | 0) <= -Draw2D.centerY2d) { - return; + return 0; } const yPrime: number = radiusSinEyePitch + ((this.maxY * cosEyePitch) >> 16); let topY: number = (midY - yPrime) << 9; if (((topY / maxZ) | 0) >= Draw2D.centerY2d) { - return; + return 0; } const radiusZ: number = radiusCosEyePitch + ((this.maxY * sinEyePitch) >> 16); @@ -2105,6 +2111,9 @@ export default class Model extends Hashable { if (mouseX > leftX && mouseX < rightX && mouseY > topY && mouseY < bottomY) { if (this.pickable) { Model.pickedBitsets[Model.pickedCount++] = bitset; + // WebGL change: -> render model here + + return 0; } else { picking = true; } @@ -2157,7 +2166,8 @@ export default class Model extends Hashable { clipped = true; } - if ((clipped || this.texturedFaceCount > 0) && Model.vertexViewSpaceX && Model.vertexViewSpaceY && Model.vertexViewSpaceZ) { + // WebGL change -> do not assign vertexViewSpace if GL is enabled + if ((clipped || this.texturedFaceCount > 0) && Model.vertexViewSpaceX && Model.vertexViewSpaceY && Model.vertexViewSpaceZ && !DrawGL.GL_ENABLED) { Model.vertexViewSpaceX[v] = x; Model.vertexViewSpaceY[v] = y; Model.vertexViewSpaceZ[v] = z; @@ -2166,14 +2176,18 @@ export default class Model extends Hashable { try { // try catch for example a model being drawn from 3d can crash like at baxtorian falls - this.draw2(clipped, picking, bitset); + return this.draw2(clipped, picking, bitset, renderMode); } catch (err) { /* empty */ + console.log(err); } + //if err, return 0 (no draw count) + return 0; }; // todo: better name, Java relies on overloads - private draw2 = (clipped: boolean, picking: boolean, bitset: number, wireframe: boolean = false): void => { + private draw2 = (clipped: boolean, picking: boolean, bitset: number, renderMode: RenderMode = RenderMode.CPU): number => { + let drawCount: number = 0; if (Model.checkHoverFace) { this.pickedFace = -1; this.pickedFaceDepth = -1; @@ -2185,6 +2199,11 @@ export default class Model extends Hashable { } } + if(DrawGL.GL_ENABLED) { + DrawGL.vertexBuffer.ensureCapacity(12 * this.faceCount); + DrawGL.uvBuffer.ensureCapacity(12 * this.faceCount); + } + for (let f: number = 0; f < this.faceCount; f++) { if (this.faceInfo && this.faceInfo[f] === -1) { continue; @@ -2207,6 +2226,18 @@ export default class Model extends Hashable { const zB: number = Model.vertexScreenZ[b]; const zC: number = Model.vertexScreenZ[c]; + //WebGL change: + if (DrawGL.GL_ENABLED) { + if (xA == -5000 || xB == -5000 || xC == -5000) { + continue; + } + if (picking && this.pointWithinTriangle(Model.mouseX, Model.mouseY, yA, yB, yC, xA, xB, xC)) { + Model.pickedBitsets[Model.pickedCount++] = bitset; + //picking = false; // explicitly not set in 317deob-gpu + } + continue; + } + if (clipped && (xA === -5000 || xB === -5000 || xC === -5000)) { if (Model.faceNearClipped) { Model.faceNearClipped[f] = true; @@ -2243,15 +2274,22 @@ export default class Model extends Hashable { Model.tmpDepthFaces[depthAverage][Model.tmpDepthFaceCount[depthAverage]++] = f; // todo: better check (depth avg isn't always accurate) - if (Model.checkHoverFace && this.pointWithinTriangle(Model.mouseX, Model.mouseY, yA, yB, yC, xA, xB, xC) && this.pickedFaceDepth < depthAverage) { + if (Model.checkHoverFace + && this.pointWithinTriangle(Model.mouseX, Model.mouseY, yA, yB, yC, xA, xB, xC) + && this.pickedFaceDepth < depthAverage) { this.pickedFace = f; this.pickedFaceDepth = depthAverage; + // WebGL change -> stop execution here. We don't need to check the rest of the faces } } } } } + if(DrawGL.GL_ENABLED) { + return 0; + } + if (!this.facePriority && Model.tmpDepthFaceCount) { for (let depth: number = this.maxDepth - 1; depth >= 0; depth--) { const count: number = Model.tmpDepthFaceCount[depth]; @@ -2262,12 +2300,12 @@ export default class Model extends Hashable { if (Model.tmpDepthFaces) { const faces: Int32Array = Model.tmpDepthFaces[depth]; for (let f: number = 0; f < count; f++) { - this.drawFace(faces[f], wireframe); + drawCount += this.drawFace(faces[f], renderMode); } } } - return; + return drawCount; } for (let priority: number = 0; priority < 12; priority++) { @@ -2342,7 +2380,7 @@ export default class Model extends Hashable { for (let priority: number = 0; priority < 10; priority++) { while (priority === 0 && priorityDepth > averagePriorityDepthSum1_2) { - this.drawFace(priorityFaces[priorityFace++], wireframe); + drawCount += this.drawFace(priorityFaces[priorityFace++], renderMode); if (priorityFace === priorityFaceCount && priorityFaces !== Model.tmpPriorityFaces[11]) { priorityFace = 0; @@ -2359,7 +2397,7 @@ export default class Model extends Hashable { } while (priority === 3 && priorityDepth > averagePriorityDepthSum3_4) { - this.drawFace(priorityFaces[priorityFace++], wireframe); + drawCount += this.drawFace(priorityFaces[priorityFace++], renderMode); if (priorityFace === priorityFaceCount && priorityFaces !== Model.tmpPriorityFaces[11]) { priorityFace = 0; @@ -2376,7 +2414,7 @@ export default class Model extends Hashable { } while (priority === 5 && priorityDepth > averagePriorityDepthSum6_8) { - this.drawFace(priorityFaces[priorityFace++], wireframe); + drawCount += this.drawFace(priorityFaces[priorityFace++], renderMode); if (priorityFace === priorityFaceCount && priorityFaces !== Model.tmpPriorityFaces[11]) { priorityFace = 0; @@ -2396,12 +2434,12 @@ export default class Model extends Hashable { const faces: Int32Array = Model.tmpPriorityFaces[priority]; for (let i: number = 0; i < count; i++) { - this.drawFace(faces[i], wireframe); + drawCount += this.drawFace(faces[i], renderMode); } } while (priorityDepth !== -1000) { - this.drawFace(priorityFaces[priorityFace++], wireframe); + drawCount += this.drawFace(priorityFaces[priorityFace++], renderMode); if (priorityFace === priorityFaceCount && priorityFaces !== Model.tmpPriorityFaces[11]) { priorityFace = 0; @@ -2417,12 +2455,14 @@ export default class Model extends Hashable { } } } + return drawCount; }; - private drawFace = (face: number, wireframe: boolean = false): void => { + private drawFace = (face: number, renderMode: RenderMode = RenderMode.CPU): number => { if (Model.faceNearClipped && Model.faceNearClipped[face]) { - this.drawNearClippedFace(face, wireframe); - return; + console.log(`near clipped face: ${face}`); + this.drawNearClippedFace(face, renderMode); + return 0; // TODO: near clipped GPU (so return 3 when implemented) } const a: number = this.faceVertexA[face]; @@ -2445,81 +2485,128 @@ export default class Model extends Hashable { } else { type = this.faceInfo[face] & 0x3; } - - if (wireframe && Model.vertexScreenX && Model.vertexScreenY && this.faceColorA && this.faceColorB && this.faceColorC) { + if (renderMode === RenderMode.CPU_WF && Model.vertexScreenX && Model.vertexScreenY && this.faceColorA && this.faceColorB && this.faceColorC) { Draw3D.drawLine(Model.vertexScreenX[a], Model.vertexScreenY[a], Model.vertexScreenX[b], Model.vertexScreenY[b], Draw3D.palette[this.faceColorA[face]]); Draw3D.drawLine(Model.vertexScreenX[b], Model.vertexScreenY[b], Model.vertexScreenX[c], Model.vertexScreenY[c], Draw3D.palette[this.faceColorB[face]]); Draw3D.drawLine(Model.vertexScreenX[c], Model.vertexScreenY[c], Model.vertexScreenX[a], Model.vertexScreenY[a], Draw3D.palette[this.faceColorC[face]]); } else if (type === 0 && this.faceColorA && this.faceColorB && this.faceColorC && Model.vertexScreenX && Model.vertexScreenY) { - Draw3D.fillGouraudTriangle( - Model.vertexScreenX[a], - Model.vertexScreenX[b], - Model.vertexScreenX[c], - Model.vertexScreenY[a], - Model.vertexScreenY[b], - Model.vertexScreenY[c], - this.faceColorA[face], - this.faceColorB[face], - this.faceColorC[face] - ); + if(renderMode === RenderMode.GPU){ + //console.log(`Model.vertexScreenX[a]: ${Model.vertexScreenX[a]}, Model.vertexScreenY[a]: ${Model.vertexScreenY[a]}`); + DrawGL.vertexBuffer.putC(Model.vertexScreenX[a], Model.vertexScreenY[a], 0, this.faceColorA[face]); + DrawGL.vertexBuffer.putC(Model.vertexScreenX[b], Model.vertexScreenY[b], 0, this.faceColorB[face]); + DrawGL.vertexBuffer.putC(Model.vertexScreenX[c], Model.vertexScreenY[c], 0, this.faceColorC[face]); + + DrawGL.uvBuffer.putC(0, 0, 0, 1); + DrawGL.uvBuffer.putC(0, 0, 0, 1); + DrawGL.uvBuffer.putC(0, 0, 0, 1); + } else { + Draw3D.fillGouraudTriangle( + Model.vertexScreenX[a], + Model.vertexScreenX[b], + Model.vertexScreenX[c], + Model.vertexScreenY[a], + Model.vertexScreenY[b], + Model.vertexScreenY[c], + this.faceColorA[face], + this.faceColorB[face], + this.faceColorC[face] + ); + } } else if (type === 1 && this.faceColorA && Model.vertexScreenX && Model.vertexScreenY) { - Draw3D.fillTriangle(Model.vertexScreenX[a], Model.vertexScreenX[b], Model.vertexScreenX[c], Model.vertexScreenY[a], Model.vertexScreenY[b], Model.vertexScreenY[c], Draw3D.palette[this.faceColorA[face]]); + if(renderMode === RenderMode.GPU){ + DrawGL.vertexBuffer.putC(Model.vertexScreenX[a], Model.vertexScreenY[a], 0, Draw3D.palette[this.faceColorA[face]]); + DrawGL.vertexBuffer.putC(Model.vertexScreenX[b], Model.vertexScreenY[b], 0, Draw3D.palette[this.faceColorA[face]]); + DrawGL.vertexBuffer.putC(Model.vertexScreenX[c], Model.vertexScreenY[c], 0, Draw3D.palette[this.faceColorA[face]]); + + DrawGL.uvBuffer.putC(0, 0, 0, 0); + DrawGL.uvBuffer.putC(0, 0, 0, 0); + DrawGL.uvBuffer.putC(0, 0, 0, 0); + } else { + Draw3D.fillTriangle(Model.vertexScreenX[a], Model.vertexScreenX[b], Model.vertexScreenX[c], Model.vertexScreenY[a], Model.vertexScreenY[b], Model.vertexScreenY[c], Draw3D.palette[this.faceColorA[face]]); + } } else if (type === 2 && this.faceInfo && this.faceColor && this.faceColorA && this.faceColorB && this.faceColorC && Model.vertexScreenX && Model.vertexScreenY && Model.vertexViewSpaceX && Model.vertexViewSpaceY && Model.vertexViewSpaceZ) { const texturedFace: number = this.faceInfo[face] >> 2; const tA: number = this.texturedVertexA[texturedFace]; const tB: number = this.texturedVertexB[texturedFace]; const tC: number = this.texturedVertexC[texturedFace]; - Draw3D.fillTexturedTriangle( - Model.vertexScreenX[a], - Model.vertexScreenX[b], - Model.vertexScreenX[c], - Model.vertexScreenY[a], - Model.vertexScreenY[b], - Model.vertexScreenY[c], - this.faceColorA[face], - this.faceColorB[face], - this.faceColorC[face], - Model.vertexViewSpaceX[tA], - Model.vertexViewSpaceY[tA], - Model.vertexViewSpaceZ[tA], - Model.vertexViewSpaceX[tB], - Model.vertexViewSpaceX[tC], - Model.vertexViewSpaceY[tB], - Model.vertexViewSpaceY[tC], - Model.vertexViewSpaceZ[tB], - Model.vertexViewSpaceZ[tC], - this.faceColor[face] - ); + + if(renderMode === RenderMode.GPU){ + DrawGL.vertexBuffer.putC(Model.vertexScreenX[a], Model.vertexScreenY[a], 0, this.faceColorA[face]); + DrawGL.vertexBuffer.putC(Model.vertexScreenX[b], Model.vertexScreenY[b], 0, this.faceColorB[face]); + DrawGL.vertexBuffer.putC(Model.vertexScreenX[c], Model.vertexScreenY[c], 0, this.faceColorC[face]); + + const texture = this.faceColor[face]; + DrawGL.uvBuffer.putC(texture, Model.vertexViewSpaceX[tA], Model.vertexViewSpaceY[tA], Model.vertexViewSpaceZ[tA]); + DrawGL.uvBuffer.putC(texture, Model.vertexViewSpaceX[tB], Model.vertexViewSpaceY[tB], Model.vertexViewSpaceZ[tB]); + DrawGL.uvBuffer.putC(texture, Model.vertexViewSpaceX[tC], Model.vertexViewSpaceY[tC], Model.vertexViewSpaceZ[tC]); + } + else { + Draw3D.fillTexturedTriangle( + Model.vertexScreenX[a], + Model.vertexScreenX[b], + Model.vertexScreenX[c], + Model.vertexScreenY[a], + Model.vertexScreenY[b], + Model.vertexScreenY[c], + this.faceColorA[face], + this.faceColorB[face], + this.faceColorC[face], + Model.vertexViewSpaceX[tA], + Model.vertexViewSpaceY[tA], + Model.vertexViewSpaceZ[tA], + Model.vertexViewSpaceX[tB], + Model.vertexViewSpaceX[tC], + Model.vertexViewSpaceY[tB], + Model.vertexViewSpaceY[tC], + Model.vertexViewSpaceZ[tB], + Model.vertexViewSpaceZ[tC], + this.faceColor[face] + ); + } } else if (type === 3 && this.faceInfo && this.faceColor && this.faceColorA && Model.vertexScreenX && Model.vertexScreenY && Model.vertexViewSpaceX && Model.vertexViewSpaceY && Model.vertexViewSpaceZ) { const texturedFace: number = this.faceInfo[face] >> 2; const tA: number = this.texturedVertexA[texturedFace]; const tB: number = this.texturedVertexB[texturedFace]; const tC: number = this.texturedVertexC[texturedFace]; - Draw3D.fillTexturedTriangle( - Model.vertexScreenX[a], - Model.vertexScreenX[b], - Model.vertexScreenX[c], - Model.vertexScreenY[a], - Model.vertexScreenY[b], - Model.vertexScreenY[c], - this.faceColorA[face], - this.faceColorA[face], - this.faceColorA[face], - Model.vertexViewSpaceX[tA], - Model.vertexViewSpaceY[tA], - Model.vertexViewSpaceZ[tA], - Model.vertexViewSpaceX[tB], - Model.vertexViewSpaceX[tC], - Model.vertexViewSpaceY[tB], - Model.vertexViewSpaceY[tC], - Model.vertexViewSpaceZ[tB], - Model.vertexViewSpaceZ[tC], - this.faceColor[face] - ); + + if(renderMode === RenderMode.GPU){ + DrawGL.vertexBuffer.putC(Model.vertexScreenX[a], Model.vertexScreenY[a], Model.vertexScreenZ![a], this.faceColorA[face]); + DrawGL.vertexBuffer.putC(Model.vertexScreenX[b], Model.vertexScreenY[b], Model.vertexScreenZ![b], this.faceColorA[face]); + DrawGL.vertexBuffer.putC(Model.vertexScreenX[c], Model.vertexScreenY[c], Model.vertexScreenZ![c], this.faceColorA[face]); + + const texture = this.faceColor[face]; + DrawGL.uvBuffer.putC(texture, Model.vertexViewSpaceX[tA], Model.vertexViewSpaceY[tA], Model.vertexViewSpaceZ[tA]); + DrawGL.uvBuffer.putC(texture, Model.vertexViewSpaceX[tB], Model.vertexViewSpaceY[tB], Model.vertexViewSpaceZ[tB]); + DrawGL.uvBuffer.putC(texture, Model.vertexViewSpaceX[tC], Model.vertexViewSpaceY[tC], Model.vertexViewSpaceZ[tC]); + } + else { + Draw3D.fillTexturedTriangle( + Model.vertexScreenX[a], + Model.vertexScreenX[b], + Model.vertexScreenX[c], + Model.vertexScreenY[a], + Model.vertexScreenY[b], + Model.vertexScreenY[c], + this.faceColorA[face], + this.faceColorA[face], + this.faceColorA[face], + Model.vertexViewSpaceX[tA], + Model.vertexViewSpaceY[tA], + Model.vertexViewSpaceZ[tA], + Model.vertexViewSpaceX[tB], + Model.vertexViewSpaceX[tC], + Model.vertexViewSpaceY[tB], + Model.vertexViewSpaceY[tC], + Model.vertexViewSpaceZ[tB], + Model.vertexViewSpaceZ[tC], + this.faceColor[face] + ); + } } + return 3;// 1 triangle drawn, offset buffer by this count }; - private drawNearClippedFace = (face: number, wireframe: boolean = false): void => { + private drawNearClippedFace = (face: number, renderMode: RenderMode = RenderMode.CPU): void => { let elements: number = 0; if (Model.vertexViewSpaceZ) { @@ -2632,7 +2719,7 @@ export default class Model extends Hashable { type = this.faceInfo[face] & 0x3; } - if (wireframe) { + if (renderMode === RenderMode.CPU_WF) { Draw3D.drawLine(x0, x1, y0, y1, Model.clippedColor[0]); Draw3D.drawLine(x1, x2, y1, y2, Model.clippedColor[1]); Draw3D.drawLine(x2, x0, y2, y0, Model.clippedColor[2]); @@ -2705,7 +2792,8 @@ export default class Model extends Hashable { type = this.faceInfo[face] & 0x3; } - if (wireframe) { + + if (renderMode === RenderMode.CPU_WF) { Draw3D.drawLine(x0, x1, y0, y1, Model.clippedColor[0]); Draw3D.drawLine(x1, x2, y1, y2, Model.clippedColor[1]); Draw3D.drawLine(x2, Model.clippedX[3], y2, Model.clippedY[3], Model.clippedColor[2]); diff --git a/src/js/jagex2/graphics/PixMap.ts b/src/js/jagex2/graphics/PixMap.ts index 50664e55..fc941bd4 100644 --- a/src/js/jagex2/graphics/PixMap.ts +++ b/src/js/jagex2/graphics/PixMap.ts @@ -1,5 +1,6 @@ import Draw2D from './Draw2D'; import {canvas2d} from './Canvas'; +import DrawGL from './DrawGL'; export default class PixMap { // constructor @@ -28,7 +29,8 @@ export default class PixMap { draw = (width: number, height: number): void => { this.#setPixels(); - this.ctx.putImageData(this.image, width, height); + if(!DrawGL.GL_ENABLED) // if not using WebGL + this.ctx.putImageData(this.image, width, height); }; #setPixels = (): void => { diff --git a/src/js/jagex2/graphics/RenderMode.ts b/src/js/jagex2/graphics/RenderMode.ts new file mode 100644 index 00000000..cb948861 --- /dev/null +++ b/src/js/jagex2/graphics/RenderMode.ts @@ -0,0 +1,5 @@ +export enum RenderMode { + CPU = 0, + CPU_WF = 1, // wireframe + GPU = 2, +} \ No newline at end of file diff --git a/src/js/jagex2/graphics/ShaderTemplate.ts b/src/js/jagex2/graphics/ShaderTemplate.ts new file mode 100644 index 00000000..0ac09e41 --- /dev/null +++ b/src/js/jagex2/graphics/ShaderTemplate.ts @@ -0,0 +1,59 @@ +export default class Template { + private readonly resourceLoaders: Array<(filename: string) => string | null> = []; + + public process(str: string): string { + const lines = str.split("\n"); + const processedLines: string[] = []; + + for (const line of lines) { + if (line.startsWith("#include ")) { + const resource = line.substring(9).trim(); + const resourceStr = this.load(resource); + processedLines.push(resourceStr); + } else { + processedLines.push(line); + } + } + + return processedLines.join("\n"); + } + + public load(filename: string): string { + for (const loader of this.resourceLoaders) { + const value = loader(filename); + if (value !== null) { + return this.process(value); + } + } + + return ""; + } + + public add(fn: (filename: string) => string | null): Template { + this.resourceLoaders.push(fn); + return this; + } + + public addInclude(clazz: any): Template { + return this.add((filename) => { + try { + // todo: what does getResourceAsStream do in Java when provided a class? + const is = clazz.getResourceAsStream(filename); + if (is !== null) { + return this.inputStreamToString(is); + } + } catch (ex) { + console.warn(ex); + } + return null; + }); + } + + private inputStreamToString(inStream: any): string { + try { + return inStream.toString(); + } catch (e:any) { + throw new Error(e); + } + } +} diff --git a/src/js/pg2.ts b/src/js/pg2.ts new file mode 100644 index 00000000..971da4b2 --- /dev/null +++ b/src/js/pg2.ts @@ -0,0 +1,370 @@ +import GameShell from './jagex2/client/GameShell'; + +import SeqType from './jagex2/config/SeqType'; +import LocType from './jagex2/config/LocType'; +import FloType from './jagex2/config/FloType'; +import ObjType from './jagex2/config/ObjType'; +import NpcType from './jagex2/config/NpcType'; +import IdkType from './jagex2/config/IdkType'; +import SpotAnimType from './jagex2/config/SpotAnimType'; +import VarpType from './jagex2/config/VarpType'; +import ComType from './jagex2/config/ComType'; + +import Draw3D from './jagex2/graphics/Draw3D'; +import PixFont from './jagex2/graphics/PixFont'; +import Model from './jagex2/graphics/Model'; +import SeqBase from './jagex2/graphics/SeqBase'; +import SeqFrame from './jagex2/graphics/SeqFrame'; + +import Jagfile from './jagex2/io/Jagfile'; + +import WordFilter from './jagex2/wordenc/WordFilter'; +import {downloadUrl, sleep} from './jagex2/util/JsUtil'; +import Draw2D from './jagex2/graphics/Draw2D'; +import Packet from './jagex2/io/Packet'; +import Wave from './jagex2/sound/Wave'; +import Database from './jagex2/io/Database'; +import Bzip from './vendor/bzip'; +import Colors from './jagex2/graphics/Colors'; +import { canvas, glCanvas, gl } from './jagex2/graphics/Canvas'; +import {Client} from './client'; +import {setupConfiguration} from './configuration'; +import DrawGL from './jagex2/graphics/DrawGL'; +import GLManager from './jagex2/graphics/GLManager'; +import { RenderMode } from './jagex2/graphics/RenderMode'; + +// noinspection JSSuspiciousNameCombination +class Playground2 extends Client { + lastHistoryRefresh = 0; + historyRefresh = true; + + private eyeX: number = 0; + private eyeY: number = 0; + private eyeZ: number = 0; + private eyePitch: number = 0; + private eyeYaw: number = 0; + private glRenderer: string = ''; + private glVersion: string = ''; + private gpuRender: boolean = true; + + modifier = 2; + model = { + id: parseInt(GameShell.getParameter('model')) || 0, + x: 0, + y: 0, + z: 420, + yaw: 0 + }; + + constructor() { + super(true); + } + + showProgress = async (progress: number, message: string): Promise=>{ + console.log(`${progress}% ov: ${message}`); + + const x: number = 360; + const y: number = 200; + const offsetY: number = 20; + this.fontBold12?.drawStringCenter((x / 2) | 0, ((y / 2) | 0) - offsetY - 26, 'RuneScape is loading - please wait...', Colors.WHITE); + const midY: number = ((y / 2) | 0) - 18 - offsetY; + + Draw2D.drawRect(((x / 2) | 0) - 152, midY, 304, 34, Colors.PROGRESS_RED); + Draw2D.drawRect(((x / 2) | 0) - 151, midY + 1, 302, 32, Colors.BLACK); + Draw2D.fillRect(((x / 2) | 0) - 150, midY + 2, progress * 3, 30, Colors.PROGRESS_RED); + Draw2D.fillRect(((x / 2) | 0) - 150 + progress * 3, midY + 2, 300 - progress * 3, 30, Colors.BLACK); + + this.fontBold12?.drawStringCenter((x / 2) | 0, ((y / 2) | 0) + 5 - offsetY, message, Colors.WHITE); + + await sleep(5); // return a slice of time to the main loop so it can update the progress bar + }; + + private modelsLoaded = false; + load = async (): Promise => { + + if (!gl) { + this.glVersion = 'WebGL 2.0 not supported'; + this.glRenderer = 'WebGL 2.0 not supported'; + } + else { + this.glVersion = gl.getParameter(gl.VERSION); + this.glRenderer = gl.getParameter(gl.RENDERER); + } + + //Draw3D.init2D(); + //DrawGL.init(); + + await this.showProgress(10, 'Connecting to fileserver'); + + + await Bzip.load(await (await fetch('bz2.wasm')).arrayBuffer()); + this.db = new Database(await Database.openDatabase()); + + const checksums: Packet = new Packet(new Uint8Array(await downloadUrl(`${Client.httpAddress}/crc`))); + const archiveChecksums: number[] = []; + for (let i: number = 0; i < 9; i++) { + archiveChecksums[i] = checksums.g4; + } + + await this.showProgress(75, 'Unpacking media'); + + const title: Jagfile = await this.loadArchive('title', 'title screen', archiveChecksums[1], 10); + + this.fontPlain11 = PixFont.fromArchive(title, 'p11'); + this.fontPlain12 = PixFont.fromArchive(title, 'p12'); + this.fontBold12 = PixFont.fromArchive(title, 'b12'); + this.fontQuill8 = PixFont.fromArchive(title, 'q8'); + + const config: Jagfile = await this.loadArchive('config', 'config', archiveChecksums[2], 15); + const interfaces: Jagfile = await this.loadArchive('interface', 'interface', archiveChecksums[3], 20); + const media: Jagfile = await this.loadArchive('media', '2d graphics', archiveChecksums[4], 30); + const models: Jagfile = await this.loadArchive('models', '3d graphics', archiveChecksums[5], 40); + const textures: Jagfile = await this.loadArchive('textures', 'textures', archiveChecksums[6], 60); + const wordenc: Jagfile = await this.loadArchive('wordenc', 'chat system', archiveChecksums[7], 65); + const sounds: Jagfile = await this.loadArchive('sounds', 'sound effects', archiveChecksums[8], 70); + + await this.showProgress(80, 'Unpacking textures'); + Draw3D.unpackTextures(textures); + Draw3D.setBrightness(0.8); + Draw3D.initPool(20); + await this.showProgress(83, 'Unpacking models'); + Model.unpack(models); + SeqBase.unpack(models); + SeqFrame.unpack(models); + + await this.showProgress(86, 'Unpacking config'); + SeqType.unpack(config); + LocType.unpack(config); + FloType.unpack(config); + ObjType.unpack(config, true); + NpcType.unpack(config); + IdkType.unpack(config); + SpotAnimType.unpack(config); + VarpType.unpack(config); + + await this.showProgress(90, 'Unpacking sounds'); + Wave.unpack(sounds); + + await this.showProgress(92, 'Unpacking interfaces'); + ComType.unpack(interfaces, media, [this.fontPlain11, this.fontPlain12, this.fontBold12, this.fontQuill8]); + + await this.showProgress(97, 'Preparing game engine'); + //await sleep(1000 * 10); + WordFilter.unpack(wordenc); + + // this.setLoopRate(1); + //this.drawArea?.bind(); + + this.modelsLoaded = true; + }; + + update = async (): Promise => { + this.updateKeysPressed(); + this.updateKeysHeld(); + + this.lastHistoryRefresh++; + + if (this.lastHistoryRefresh > 50) { + if (this.historyRefresh) { + GameShell.setParameter('model', this.model.id.toString()); + + this.historyRefresh = false; + } + + this.lastHistoryRefresh = 0; + } + }; + + drawGpu = async (): Promise => { + if(!DrawGL.glInitted) return; + + const model: Model = Model.model(this.model.id); + model.calculateNormals(64, 850, -30, -50, -30, true); + + DrawGL.targetBufferOffset += + model.draw(this.model.yaw, + Draw3D.sin[this.eyePitch], + Draw3D.cos[this.eyePitch], + Draw3D.sin[this.eyeYaw], + Draw3D.cos[this.eyeYaw], + this.model.x - this.eyeX, + this.model.y - this.eyeY, + this.model.z - this.eyeZ, + 0, + RenderMode.GPU); + + DrawGL.draw(); + + if (this.fontBold12) { + this.fontBold12.drawStringRight(this.width, this.fontBold12.height, `FPS: ${this.fps}`, Colors.YELLOW); + + // controls + let leftY: number = this.fontBold12.height; + this.fontBold12.drawString(0, leftY, `WebGL Edition`, Colors.WHITE); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, `Renderer: ${this.glRenderer}`, Colors.WHITE); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, `Version: ${this.glVersion}`, Colors.WHITE); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, `Model: ${this.model.id}`, Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'Controls:', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'r - reset camera and model rotation + movement speed', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, '1 and 2 - change model', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, '[ and ] - adjust movement speed', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'left and right - adjust model yaw', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'up and down - adjust model pitch', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, '. and / - adjust model roll', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'w and s - move camera along z axis', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'a and d - move camera along x axis', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'q and e - move camera along y axis', Colors.YELLOW); + } + }; + + draw = async (): Promise => { + if(this.gpuRender && this.modelsLoaded) { + await this.drawGpu(); + } + else if(!this.gpuRender) { + Draw2D.clear(); + + const startColor = 0x555555; + Draw2D.fillRect(0, 0, this.width, this.height, startColor); + // draw a model + const model: Model = Model.model(this.model.id); + model.calculateNormals(64, 850, -30, -50, -30, true); + model.draw(this.model.yaw, Draw3D.sin[this.eyePitch], Draw3D.cos[this.eyePitch], Draw3D.sin[this.eyeYaw], Draw3D.cos[this.eyeYaw], this.model.x - this.eyeX, this.model.y - this.eyeY, this.model.z - this.eyeZ, 0); + + // debug + if (this.fontBold12) { + this.fontBold12.drawStringRight(this.width, this.fontBold12.height, `FPS: ${this.fps}`, Colors.YELLOW); + + // controls + let leftY: number = this.fontBold12.height; + this.fontBold12.drawString(0, leftY, `WebGL Edition`, Colors.WHITE); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, `Renderer: ${this.glRenderer}`, Colors.WHITE); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, `Version: ${this.glVersion}`, Colors.WHITE); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, `Model: ${this.model.id}`, Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'Controls:', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'r - reset camera and model rotation + movement speed', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, '1 and 2 - change model', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, '[ and ] - adjust movement speed', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'left and right - adjust model yaw', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'up and down - adjust model pitch', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, '. and / - adjust model roll', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'w and s - move camera along z axis', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'a and d - move camera along x axis', Colors.YELLOW); + leftY += this.fontBold12.height; + this.fontBold12.drawString(0, leftY, 'q and e - move camera along y axis', Colors.YELLOW); + } + + this.drawArea?.draw(0, 0); + } + }; + + // ---- + + updateKeysPressed(): void { + // eslint-disable-next-line no-constant-condition + while (true) { + const key: number = this.pollKey(); + if (key === -1) { + break; + } + + if (key === 'r'.charCodeAt(0)) { + this.modifier = 2; + this.historyRefresh = true; + } else if (key === '1'.charCodeAt(0)) { + this.model.id--; + if (this.model.id < 0 && Model.metadata) { + this.model.id = Model.metadata.length - 100 - 1; + } + this.historyRefresh = true; + } else if (key === '2'.charCodeAt(0)) { + this.model.id++; + if (Model.metadata && this.model.id >= Model.metadata.length - 100) { + this.model.id = 0; + } + this.historyRefresh = true; + } + else if (key === '.'.charCodeAt(0)) { + console.log('roll' + this.gpuRender); + this.gpuRender = !this.gpuRender; + glCanvas.style.display = this.gpuRender ? 'block' : 'none'; + canvas.style.display = !this.gpuRender ? 'block' : 'none'; + this.historyRefresh = true; + } + } + } + + updateKeysHeld(): void { + if (this.actionKey['['.charCodeAt(0)]) { + this.modifier--; + } else if (this.actionKey[']'.charCodeAt(0)]) { + this.modifier++; + } + + if (this.actionKey[1]) { + // left arrow + this.model.yaw += this.modifier; + this.historyRefresh = true; + } else if (this.actionKey[2]) { + // right arrow + this.model.yaw -= this.modifier; + this.historyRefresh = true; + } + + if (this.actionKey['w'.charCodeAt(0)]) { + this.model.z -= this.modifier; + this.historyRefresh = true; + } else if (this.actionKey['s'.charCodeAt(0)]) { + this.model.z += this.modifier; + this.historyRefresh = true; + } + + if (this.actionKey['a'.charCodeAt(0)]) { + this.model.x -= this.modifier; + this.historyRefresh = true; + } else if (this.actionKey['d'.charCodeAt(0)]) { + this.model.x += this.modifier; + this.historyRefresh = true; + } + + if (this.actionKey['q'.charCodeAt(0)]) { + this.model.y += this.modifier; + this.historyRefresh = true; + } else if (this.actionKey['e'.charCodeAt(0)]) { + this.model.y -= this.modifier; + this.historyRefresh = true; + } + + this.eyePitch = this.eyePitch & 2047; + this.eyeYaw = this.eyeYaw & 2047; + this.model.yaw = this.model.yaw & 2047; + } +} + +await setupConfiguration(); +new Playground2().run().then((): void => {}); diff --git a/src/public/gpu/frag.glsl b/src/public/gpu/frag.glsl new file mode 100644 index 00000000..b5762b37 --- /dev/null +++ b/src/public/gpu/frag.glsl @@ -0,0 +1,137 @@ +#version 300 es + +// original license. +/* + * Copyright (c) 2018, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +precision highp float; +precision highp sampler2DArray; + +uniform sampler2DArray textures; +uniform float brightness; +uniform float smoothBanding; +uniform vec4 fogColor; +uniform float textureLightMode; + +in vec4 fColor; +in float fHsl; +//flat in int fTextureId; +in vec4 fUv; +in float fFogAmount; + +out vec4 FragColor; + +vec3 hslToRgb(int hsl) { + int var5 = hsl / 128; + float var6 = float(var5 >> 3) / 64.0f + 0.0078125f; + float var8 = float(var5 & 7) / 8.0f + 0.0625f; + + int var10 = hsl % 128; + + float var11 = float(var10) / 128.0f; + float var13 = var11; + float var15 = var11; + float var17 = var11; + + if (var8 != 0.0f) { + float var19; + if (var11 < 0.5f) { + var19 = var11 * (1.0f + var8); + } else { + var19 = var11 + var8 - var11 * var8; + } + + float var21 = 2.0f * var11 - var19; + float var23 = var6 + 0.3333333333333333f; + if (var23 > 1.0f) { + var23 -= 1.f; + } + + float var27 = var6 - 0.3333333333333333f; + if (var27 < 0.0f) { + var27 += 1.f; + } + + if (6.0f * var23 < 1.0f) { + var13 = var21 + (var19 - var21) * 6.0f * var23; + } else if (2.0f * var23 < 1.0f) { + var13 = var19; + } else if (3.0f * var23 < 2.0f) { + var13 = var21 + (var19 - var21) * (0.6666666666666666f - var23) * 6.0f; + } else { + var13 = var21; + } + + if (6.0f * var6 < 1.0f) { + var15 = var21 + (var19 - var21) * 6.0f * var6; + } else if (2.0f * var6 < 1.0f) { + var15 = var19; + } else if (3.0f * var6 < 2.0f) { + var15 = var21 + (var19 - var21) * (0.6666666666666666f - var6) * 6.0f; + } else { + var15 = var21; + } + + if (6.0f * var27 < 1.0f) { + var17 = var21 + (var19 - var21) * 6.0f * var27; + } else if (2.0f * var27 < 1.0f) { + var17 = var19; + } else if (3.0f * var27 < 2.0f) { + var17 = var21 + (var19 - var21) * (0.6666666666666666f - var27) * 6.0f; + } else { + var17 = var21; + } + } + + vec3 rgb = vec3(pow(var13, brightness), pow(var15, brightness), pow(var17, brightness)); + + return rgb; +} + +void main() { + vec4 c; + + float n = fUv.x; + + int hsl = int(fHsl); + vec3 rgb = hslToRgb(hsl) * smoothBanding + fColor.rgb * (1.f - smoothBanding); + vec4 smoothColor = vec4(rgb, fColor.a); + + if (n > 0.0) { + n -= 1.0; + int textureIdx = int(n); + + vec2 uv = fUv.yz; + //vec2 animatedUv = uv + textureOffsets[textureIdx]; + + vec4 textureColor = texture(textures, vec3(uv, n)); + vec4 textureColorBrightness = pow(textureColor, vec4(brightness, brightness, brightness, 1.0f)); + + smoothColor = textureColorBrightness * smoothColor; + } + + vec3 mixedColor = mix(smoothColor.rgb, fogColor.rgb, fFogAmount); + FragColor = vec4(mixedColor, smoothColor.a); +} diff --git a/src/public/gpu/fragui.glsl b/src/public/gpu/fragui.glsl new file mode 100644 index 00000000..60b33dbb --- /dev/null +++ b/src/public/gpu/fragui.glsl @@ -0,0 +1,15 @@ +#version 300 es + +precision highp float; +precision highp sampler2D; + +uniform sampler2D tex; + +in vec2 TexCoord; + +out vec4 FragColor; + +void main() { + vec4 c = texture(tex, TexCoord); + FragColor = c; +} \ No newline at end of file diff --git a/src/public/gpu/vert.glsl b/src/public/gpu/vert.glsl new file mode 100644 index 00000000..d2f72cf1 --- /dev/null +++ b/src/public/gpu/vert.glsl @@ -0,0 +1,198 @@ +#version 300 es + +// original license +/* + * Copyright (c) 2018, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#define TILE_SIZE 128 + +#define FOG_SCENE_EDGE_MIN ((-expandedMapLoadingChunks * 8 + 1) * TILE_SIZE) +#define FOG_SCENE_EDGE_MAX ((104 + expandedMapLoadingChunks * 8 - 1) * TILE_SIZE) +#define FOG_CORNER_ROUNDING 1.5 +#define FOG_CORNER_ROUNDING_SQUARED (FOG_CORNER_ROUNDING * FOG_CORNER_ROUNDING) + +layout(location = 0) in ivec4 VertexPosition; +layout(location = 1) in vec4 uv; + +layout(std140) uniform uniforms { + int cameraYaw; + int cameraPitch; + int centerX; + int centerY; + int zoom; + int cameraX; + int cameraY; + int cameraZ; + ivec2 sinCosTable[2048]; +}; + +uniform float brightness; +uniform int useFog; +uniform int fogDepth; +uniform int drawDistance; +uniform int expandedMapLoadingChunks; +uniform mat4 projectionMatrix; + +//out ivec3 fVertex; +out vec4 fColor; +out float fHsl; +//flat out int fTextureId; +//out vec3 fTexPos; +out float fFogAmount; +out vec4 fUv; + +//#include "hsl_to_rgb.glsl" +vec3 hslToRgb(int hsl) { + int var5 = hsl / 128; + float var6 = float(var5 >> 3) / 64.0f + 0.0078125f; + float var8 = float(var5 & 7) / 8.0f + 0.0625f; + + int var10 = hsl % 128; + + float var11 = float(var10) / 128.0f; + float var13 = var11; + float var15 = var11; + float var17 = var11; + + if (var8 != 0.0f) { + float var19; + if (var11 < 0.5f) { + var19 = var11 * (1.0f + var8); + } else { + var19 = var11 + var8 - var11 * var8; + } + + float var21 = 2.0f * var11 - var19; + float var23 = var6 + 0.3333333333333333f; + if (var23 > 1.0f) { + var23 -= 1.f; + } + + float var27 = var6 - 0.3333333333333333f; + if (var27 < 0.0f) { + var27 += 1.f; + } + + if (6.0f * var23 < 1.0f) { + var13 = var21 + (var19 - var21) * 6.0f * var23; + } else if (2.0f * var23 < 1.0f) { + var13 = var19; + } else if (3.0f * var23 < 2.0f) { + var13 = var21 + (var19 - var21) * (0.6666666666666666f - var23) * 6.0f; + } else { + var13 = var21; + } + + if (6.0f * var6 < 1.0f) { + var15 = var21 + (var19 - var21) * 6.0f * var6; + } else if (2.0f * var6 < 1.0f) { + var15 = var19; + } else if (3.0f * var6 < 2.0f) { + var15 = var21 + (var19 - var21) * (0.6666666666666666f - var6) * 6.0f; + } else { + var15 = var21; + } + + if (6.0f * var27 < 1.0f) { + var17 = var21 + (var19 - var21) * 6.0f * var27; + } else if (2.0f * var27 < 1.0f) { + var17 = var19; + } else if (3.0f * var27 < 2.0f) { + var17 = var21 + (var19 - var21) * (0.6666666666666666f - var27) * 6.0f; + } else { + var17 = var21; + } + } + + vec3 rgb = vec3(pow(var13, brightness), pow(var15, brightness), pow(var17, brightness)); + + return rgb; +} + +float fogFactorLinear(const float dist, const float start, const float end) { + return 1.0 - clamp((dist - start) / (end - start), 0.0, 1.0); +} + +void main() { + /*ivec3 vertex = VertexPosition.xyz; + int ahsl = VertexPosition.w; + int hsl = ahsl & 0xffff; + float a = float(ahsl >> 24 & 0xff) / 255.f; + + vec3 rgb = hslToRgb(hsl); + + //fVertex = vertex; + + fColor = vec4(rgb, 1.f - a); + fHsl = float(hsl); + + //fTextureId = int(uv.x); // the texture id + 1; + //fTexPos = uv.yzw; + fUv = uv; + + // the client draws one less tile to the north and east than it does to the south + // and west, so subtract a tiles width from the north and east edges. + int fogWest = max(FOG_SCENE_EDGE_MIN, int(cameraX) - drawDistance); + int fogEast = min(FOG_SCENE_EDGE_MAX, int(cameraX) + drawDistance - TILE_SIZE); + int fogSouth = max(FOG_SCENE_EDGE_MIN, int(cameraZ) - drawDistance); + int fogNorth = min(FOG_SCENE_EDGE_MAX, int(cameraZ) + drawDistance - TILE_SIZE); + + // Calculate distance from the scene edge + float xDist = min(float(VertexPosition.x - fogWest), float(fogEast - VertexPosition.x)); + float zDist = min(float(VertexPosition.z - fogSouth), float(fogNorth - VertexPosition.z)); + float nearestEdgeDistance = min(xDist, zDist); + float secondNearestEdgeDistance = max(xDist, zDist); + float fogDistance = + nearestEdgeDistance - FOG_CORNER_ROUNDING * float(TILE_SIZE) * + max(0.f, (nearestEdgeDistance + FOG_CORNER_ROUNDING_SQUARED) / + (secondNearestEdgeDistance + FOG_CORNER_ROUNDING_SQUARED)); + + fFogAmount = fogFactorLinear(fogDistance, 0.f, float(fogDepth) * float(TILE_SIZE)) * float(useFog);*/ + + ivec3 vertex = VertexPosition.xyz; + int ahsl = VertexPosition.w; + int hsl = ahsl & 0xffff; + float a = float(ahsl >> 24 & 0xff) / 255.f; + + vec3 rgb = hslToRgb(hsl); + + fColor = vec4(rgb, 1.f - a); + fHsl = float(hsl); + fUv = uv; + + int fogWest = max(FOG_SCENE_EDGE_MIN, cameraX - drawDistance); + int fogEast = min(FOG_SCENE_EDGE_MAX, cameraX + drawDistance - TILE_SIZE); + int fogSouth = max(FOG_SCENE_EDGE_MIN, cameraZ - drawDistance); + int fogNorth = min(FOG_SCENE_EDGE_MAX, cameraZ + drawDistance - TILE_SIZE); + + // Calculate distance from the scene edge + int fogDistance = min(min(vertex.x - fogWest, fogEast - vertex.x), min(vertex.z - fogSouth, fogNorth - vertex.z)); + + fFogAmount = fogFactorLinear(float(fogDistance), 0.f, float(fogDepth * TILE_SIZE)) * float(useFog); + + vec4 pos = projectionMatrix * vec4(vertex, 1); + gl_Position = pos; + +} \ No newline at end of file diff --git a/src/public/gpu/vertui.glsl b/src/public/gpu/vertui.glsl new file mode 100644 index 00000000..7edfab12 --- /dev/null +++ b/src/public/gpu/vertui.glsl @@ -0,0 +1,11 @@ +#version 300 es + +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec2 aTexCoord; + +out vec2 TexCoord; + +void main() { + gl_Position = vec4(aPos, 1.0); + TexCoord = aTexCoord; +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index bd7e0182..f229e6de 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,7 +12,7 @@ const stylesHandler = isProduction ? MiniCssExtractPlugin.loader : 'style-loader const pages = [ 'index', 'playground', 'viewer', 'mesanim', 'items', 'sounds', 'interface-editor', - 'JagEd' + 'JagEd', 'pg2', 'game_gl' ]; const htmlPlugins = pages.map(name => { return new HtmlWebpackPlugin({ @@ -31,7 +31,9 @@ const config = { items: './src/js/items.ts', sounds: './src/js/sounds.ts', ['interface-editor']: './src/js/interface-editor.ts', - JagEd: './src/js/JagEd.ts' + JagEd: './src/js/JagEd.ts', + pg2: './src/js/pg2.ts', + game_gl: './src/js/game_gl.ts' }, plugins: [ From 9079210799348c01e0c7213981d18b81dd037c49 Mon Sep 17 00:00:00 2001 From: Battlerax Date: Tue, 19 Mar 2024 21:35:28 -0500 Subject: [PATCH 2/4] disable breaking changes to allow pg2 to render again --- src/js/jagex2/graphics/DrawGL.ts | 94 +++++++++++++++----------------- src/js/jagex2/graphics/Model.ts | 9 ++- src/js/pg2.ts | 6 +- 3 files changed, 51 insertions(+), 58 deletions(-) diff --git a/src/js/jagex2/graphics/DrawGL.ts b/src/js/jagex2/graphics/DrawGL.ts index 2e49c3ea..577070b7 100644 --- a/src/js/jagex2/graphics/DrawGL.ts +++ b/src/js/jagex2/graphics/DrawGL.ts @@ -79,6 +79,9 @@ export default class DrawGL { private static glRenderer: string; private static glVersion: string; + // debug + private static noDraw:boolean = false; + static init = async (): Promise => { if (!gl) { throw new Error('WebGL 2.0 not supported'); @@ -198,31 +201,6 @@ export default class DrawGL { console.log(`initGlBuffer: ${glBuffer.name} ${glBuffer.glBufferId}`) } - static convertPixels(srcPixels:Int8Array, width:number, height:number, textureWidth:number, textureHeight:number) : Uint8Array { - const pixels = new Uint8Array(textureWidth * textureHeight * 4); - - let pixelIdx = 0; - let srcPixelIdx = 0; - - let offset = (textureWidth - width) * 4; - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - let rgb = srcPixels[srcPixelIdx++]; - if (rgb != 0) { - pixels[pixelIdx++] = (rgb >> 16); - pixels[pixelIdx++] = (rgb >> 8); - pixels[pixelIdx++] = rgb; - pixels[pixelIdx++] = -1; - } else { - pixelIdx += 4; - } - } - pixelIdx += offset; - } - return pixels; - } - private static updateTextures(textureArrayId:WebGLTexture) :void{ const textures = Draw3D.textures; @@ -239,11 +217,11 @@ export default class DrawGL { ++cnt; - if (texturePixels.length != DrawGL.TEXTURE_SIZE * DrawGL.TEXTURE_SIZE) { + //if (texturePixels.length != DrawGL.TEXTURE_SIZE * DrawGL.TEXTURE_SIZE) { // The texture storage is 128x128 bytes, and will only work correctly with the // 128x128 textures from high detail mode //continue; - } + //} const pixels = DrawGL.convertPixels(texturePixels, DrawGL.TEXTURE_SIZE, DrawGL.TEXTURE_SIZE, DrawGL.TEXTURE_SIZE, DrawGL.TEXTURE_SIZE); // = new Uint8Array(texturePixels);//DrawGL.getPixelsAsUint8ArrayFromSigned(texturePixels); @@ -282,13 +260,13 @@ export default class DrawGL { DrawGL.textureArrayId = textureArrayId; } - static shutdownBuffers = (): void => { + static shutdownBuffers(): void { DrawGL.destroyGlBuffer(DrawGL.tmpVertexBuffer); DrawGL.destroyGlBuffer(DrawGL.tmpUvBuffer); DrawGL.destroyGlBuffer(DrawGL.uniformBuffer); } - static destroyGlBuffer = (glBuffer:GLBuffer): void => { + static destroyGlBuffer(glBuffer:GLBuffer): void { if (glBuffer.glBufferId != -1) { gl.deleteBuffer(glBuffer.glBufferId); @@ -374,28 +352,9 @@ export default class DrawGL { gl.bindTexture(gl.TEXTURE_2D, null); }*/ - private static noDraw:boolean = false; + - /// - /// Float.floatToIntBits (Java) equivalent - /// - private static floatToIntBitsEq(f:number) : number - { - let buffer = new ArrayBuffer(4); - let view = new DataView(buffer); - view.setFloat32(0, f, false); // false for big-endian - return view.getInt32(0, false); // false for big-endian - } - - private static getPixelsAsUint8Array(arr: Int32Array): Uint8Array { - let pixels:Uint8Array = new Uint8Array(arr.buffer); - for (let i = 0; i < pixels.length; i += 4) { - let temp = pixels[i]; - pixels[i] = pixels[i + 2]; - pixels[i + 2] = temp; - } - return pixels; - } + static uniformBufferAlloc(): void { @@ -660,4 +619,39 @@ export default class DrawGL { return; } } + + static convertPixels(srcPixels:Int8Array, width:number, height:number, textureWidth:number, textureHeight:number) : Uint8Array { + const pixels = new Uint8Array(textureWidth * textureHeight * 4); + + let pixelIdx = 0; + let srcPixelIdx = 0; + + let offset = (textureWidth - width) * 4; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let rgb = srcPixels[srcPixelIdx++]; + if (rgb != 0) { + pixels[pixelIdx++] = (rgb >> 16); + pixels[pixelIdx++] = (rgb >> 8); + pixels[pixelIdx++] = rgb; + pixels[pixelIdx++] = -1; + } else { + pixelIdx += 4; + } + } + pixelIdx += offset; + } + return pixels; + } + + private static getPixelsAsUint8Array(arr: Int32Array): Uint8Array { + let pixels:Uint8Array = new Uint8Array(arr.buffer); + for (let i = 0; i < pixels.length; i += 4) { + let temp = pixels[i]; + pixels[i] = pixels[i + 2]; + pixels[i + 2] = temp; + } + return pixels; + } } \ No newline at end of file diff --git a/src/js/jagex2/graphics/Model.ts b/src/js/jagex2/graphics/Model.ts index 4620328e..9fb66388 100644 --- a/src/js/jagex2/graphics/Model.ts +++ b/src/js/jagex2/graphics/Model.ts @@ -2113,7 +2113,6 @@ export default class Model extends Hashable { Model.pickedBitsets[Model.pickedCount++] = bitset; // WebGL change: -> render model here - return 0; } else { picking = true; } @@ -2227,7 +2226,7 @@ export default class Model extends Hashable { const zC: number = Model.vertexScreenZ[c]; //WebGL change: - if (DrawGL.GL_ENABLED) { + /*if (DrawGL.GL_ENABLED) { if (xA == -5000 || xB == -5000 || xC == -5000) { continue; } @@ -2236,7 +2235,7 @@ export default class Model extends Hashable { //picking = false; // explicitly not set in 317deob-gpu } continue; - } + }*/ if (clipped && (xA === -5000 || xB === -5000 || xC === -5000)) { if (Model.faceNearClipped) { @@ -2286,9 +2285,9 @@ export default class Model extends Hashable { } } - if(DrawGL.GL_ENABLED) { + /*if(DrawGL.GL_ENABLED) { return 0; - } + }*/ if (!this.facePriority && Model.tmpDepthFaceCount) { for (let depth: number = this.maxDepth - 1; depth >= 0; depth--) { diff --git a/src/js/pg2.ts b/src/js/pg2.ts index 971da4b2..bd190de4 100644 --- a/src/js/pg2.ts +++ b/src/js/pg2.ts @@ -177,7 +177,7 @@ class Playground2 extends Client { drawGpu = async (): Promise => { if(!DrawGL.glInitted) return; - + const model: Model = Model.model(this.model.id); model.calculateNormals(64, 850, -30, -50, -30, true); @@ -193,8 +193,6 @@ class Playground2 extends Client { 0, RenderMode.GPU); - DrawGL.draw(); - if (this.fontBold12) { this.fontBold12.drawStringRight(this.width, this.fontBold12.height, `FPS: ${this.fps}`, Colors.YELLOW); @@ -228,6 +226,8 @@ class Playground2 extends Client { leftY += this.fontBold12.height; this.fontBold12.drawString(0, leftY, 'q and e - move camera along y axis', Colors.YELLOW); } + + DrawGL.draw(); }; draw = async (): Promise => { From 59447f0b620f492870d813bf6241610cf277d237 Mon Sep 17 00:00:00 2001 From: Jordan Date: Mon, 8 Apr 2024 01:02:58 -0400 Subject: [PATCH 3/4] merge: from origin main --- src/js/jagex2/graphics/Canvas.ts | 3 +-- src/js/jagex2/graphics/GLBuffer.ts | 14 +++++++------- src/js/jagex2/graphics/RenderMode.ts | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/js/jagex2/graphics/Canvas.ts b/src/js/jagex2/graphics/Canvas.ts index b2e5b4c1..9db628f0 100644 --- a/src/js/jagex2/graphics/Canvas.ts +++ b/src/js/jagex2/graphics/Canvas.ts @@ -5,5 +5,4 @@ export const jpegCanvas: HTMLCanvasElement = document.createElement('canvas'); export const jpegImg: HTMLImageElement = document.createElement('img'); export const jpeg2d: CanvasRenderingContext2D = jpegCanvas.getContext('2d', {willReadFrequently: true})!; export const glCanvas: HTMLCanvasElement = document.createElement('canvas'); -export const gl: WebGL2RenderingContext = - canvas.getContext('webgl2',{willReadFrequently: true})! as WebGL2RenderingContext; +export const gl: WebGL2RenderingContext = canvas.getContext('webgl2', {willReadFrequently: true})! as WebGL2RenderingContext; diff --git a/src/js/jagex2/graphics/GLBuffer.ts b/src/js/jagex2/graphics/GLBuffer.ts index 0e0f54b8..d1f65378 100644 --- a/src/js/jagex2/graphics/GLBuffer.ts +++ b/src/js/jagex2/graphics/GLBuffer.ts @@ -1,8 +1,8 @@ export default class GLBuffer { - name: string; - glBufferId: WebGLBuffer = -1; - size: number = -1; - constructor(name: string) { - this.name = name; - } -} \ No newline at end of file + name: string; + glBufferId: WebGLBuffer = -1; + size: number = -1; + constructor(name: string) { + this.name = name; + } +} diff --git a/src/js/jagex2/graphics/RenderMode.ts b/src/js/jagex2/graphics/RenderMode.ts index cb948861..230762dd 100644 --- a/src/js/jagex2/graphics/RenderMode.ts +++ b/src/js/jagex2/graphics/RenderMode.ts @@ -1,5 +1,5 @@ export enum RenderMode { CPU = 0, CPU_WF = 1, // wireframe - GPU = 2, -} \ No newline at end of file + GPU = 2 +} From 7973eb96f78c05a525473be4853cf71062a033a8 Mon Sep 17 00:00:00 2001 From: lesleyrs <19632758+lesleyrs@users.noreply.github.com> Date: Sun, 23 Jun 2024 18:10:51 +0200 Subject: [PATCH 4/4] fix merge --- src/js/pg2.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/js/pg2.ts b/src/js/pg2.ts index b2045832..a135434b 100644 --- a/src/js/pg2.ts +++ b/src/js/pg2.ts @@ -8,13 +8,13 @@ import NpcType from './jagex2/config/NpcType'; import IdkType from './jagex2/config/IdkType'; import SpotAnimType from './jagex2/config/SpotAnimType'; import VarpType from './jagex2/config/VarpType'; -import ComType from './jagex2/config/ComType'; +import Component from './jagex2/config/Component'; import Draw3D from './jagex2/graphics/Draw3D'; import PixFont from './jagex2/graphics/PixFont'; import Model from './jagex2/graphics/Model'; -import SeqBase from './jagex2/graphics/SeqBase'; -import SeqFrame from './jagex2/graphics/SeqFrame'; +import AnimBase from './jagex2/graphics/AnimBase'; +import AnimFrame from './jagex2/graphics/AnimFrame'; import Jagfile from './jagex2/io/Jagfile'; @@ -126,8 +126,8 @@ class Playground2 extends Client { Draw3D.initPool(20); await this.showProgress(83, 'Unpacking models'); Model.unpack(models); - SeqBase.unpack(models); - SeqFrame.unpack(models); + AnimBase.unpack(models); + AnimFrame.unpack(models); await this.showProgress(86, 'Unpacking config'); SeqType.unpack(config); @@ -143,7 +143,7 @@ class Playground2 extends Client { Wave.unpack(sounds); await this.showProgress(92, 'Unpacking interfaces'); - ComType.unpack(interfaces, media, [this.fontPlain11, this.fontPlain12, this.fontBold12, this.fontQuill8]); + Component.unpack(interfaces, media, [this.fontPlain11, this.fontPlain12, this.fontBold12, this.fontQuill8]); await this.showProgress(97, 'Preparing game engine'); //await sleep(1000 * 10);