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..f5f7f628 --- /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 Component from './jagex2/config/Component'; + +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 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 AnimBase from './jagex2/graphics/AnimBase'; +import AnimFrame from './jagex2/graphics/AnimFrame'; +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; // + const v0: number = 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); + AnimBase.unpack(models); + AnimFrame.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'); + Component.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); + 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.oplogic1 = 0; + Client.oplogic2 = 0; + Client.oplogic3 = 0; + Client.oplogic4 = 0; + Client.oplogic5 = 0; + Client.oplogic6 = 0; + Client.oplogic7 = 0; + Client.oplogic8 = 0; + Client.oplogic9 = 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: Component = Component.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.cyclelogic3++; + if (Client.cyclelogic3 > 127) { + Client.cyclelogic3 = 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.cyclelogic4++; + if (Client.cyclelogic4 > 110) { + Client.cyclelogic4 = 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); + 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.cyclelogic2++; + if (Client.cyclelogic2 > 1802) { + Client.cyclelogic2 = 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(Component.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(Component.instances[this.sidebarInterfaceId], 0, 0, 0); + } else if (this.tabInterfaceId[this.selectedTab] !== -1) { + this.drawInterface(Component.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(Component.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(Component.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.minimap) { + 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.id || shape === LocShape.WALL_L.id) { + 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.id) { + 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.id) { + 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.id) { + 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: Component = Component.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 (Component.instances[comId].layer === this.viewportInterfaceId) { + this.objDragArea = 1; + } + + if (Component.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.oplogic5++; + } + + if (Client.oplogic5 >= 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.oplogic3 += a; + if (Client.oplogic3 >= 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 (Component.instances[c].layer === this.viewportInterfaceId) { + this.selectedArea = 1; + } + + if (Component.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.oplogic2++; + } + + if (Client.oplogic2 >= 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.oplogic4++; + } + + if (Client.oplogic4 >= 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 (Component.instances[c].layer === this.viewportInterfaceId) { + this.selectedArea = 1; + } + + if (Component.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 (Component.instances[c].layer === this.viewportInterfaceId) { + this.selectedArea = 1; + } + + if (Component.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.oplogic8++; + if (Client.oplogic8 >= 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: Component = Component.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: Component = Component.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.oplogic7++; + } + + if (Client.oplogic7 >= 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.oplogic9++; + } + + if (Client.oplogic9 >= 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 (Component.instances[c].layer === this.viewportInterfaceId) { + this.selectedArea = 1; + } + + if (Component.instances[c].layer === this.chatInterfaceId) { + this.selectedArea = 3; + } + } else if (action === 581) { + if ((a & 0x3) === 0) { + Client.oplogic1++; + } + + if (Client.oplogic1 >= 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.oplogic6 += this.sceneBaseTileZ; + if (Client.oplogic6 >= 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: Component = Component.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 < Component.instances.length; i++) { + if (Component.instances[i] && Component.instances[i].clientCode === Component.CC_REPORT_INPUT) { + this.reportAbuseInterfaceID = this.viewportInterfaceId = Component.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: Component = Component.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: Component): boolean => { + const clientCode: number = com.clientCode; + if (clientCode === Component.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 === Component.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 === Component.CC_LOGOUT) { + this.idleTimeout = 250; + return true; + } + + if (clientCode === Component.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 === Component.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 >= Component.CC_CHANGE_HEAD_L && clientCode <= Component.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 >= Component.CC_RECOLOUR_HAIR_L && clientCode <= Component.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_IDK_COLORS[part].length - 1; + } + } + + if (direction === 1) { + color++; + if (color >= PlayerEntity.DESIGN_IDK_COLORS[part].length) { + color = 0; + } + } + + this.designColors[part] = color; + this.updateDesignModel = true; + } + + if (clientCode === Component.CC_SWITCH_TO_MALE && !this.designGenderMale) { + this.designGenderMale = true; + this.validateCharacterDesign(); + } + + if (clientCode === Component.CC_SWITCH_TO_FEMALE && this.designGenderMale) { + this.designGenderMale = false; + this.validateCharacterDesign(); + } + + if (clientCode === Component.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 === Component.CC_MOD_MUTE) { + this.reportAbuseMuteOption = !this.reportAbuseMuteOption; + } + + // reportabuse rules options + if (clientCode >= Component.CC_REPORT_RULE1 && clientCode <= Component.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.id || type === LocShape.CENTREPIECE_DIAGONAL.id || type === LocShape.GROUND_DECOR.id) { + 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.cyclelogic1++; + if (Client.cyclelogic1 > 150) { + Client.cyclelogic1 = 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 < Component.instances.length; i++) { + if (Component.instances[i] && Component.instances[i].clientCode === 600) { + this.reportAbuseInterfaceID = this.viewportInterfaceId = Component.instances[i].layer; + return; + } + } + } + } + }; + + private handleScrollInput = (mouseX: number, mouseY: number, scrollableHeight: number, height: number, redraw: boolean, left: number, top: number, component: Component): 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.head() 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.addHead(topObj); + + let bottomObjId: number = -1; + let middleObjId: number = -1; + let bottomObjCount: number = 0; + let middleObjCount: number = 0; + for (let obj: ObjStackEntity | null = objStacks.head() 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.head() 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 + Component.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; + Component.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: Component = Component.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: Component = Component.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 < Component.instances.length; i++) { + if (Component.instances[i] && Component.instances[i].clientCode === contentType) { + this.viewportInterfaceId = Component.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); + Component.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: Component = Component.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: Component = Component.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.head() 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; + Component.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); + Component.instances[com].model = obj.getInterfaceModel(50); + Component.instances[com].xan = obj.xan2d; + Component.instances[com].yan = obj.yan2d; + Component.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; + Component.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; + Component.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; + Component.instances[com].text = this.in.gjstr; + if (Component.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: Component = Component.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.head() 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.head() 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: Component = Component.instances[id]; + if (!parent.childId) { + return; + } + for (let i: number = 0; i < parent.childId.length && parent.childId[i] !== -1; i++) { + const child: Component = Component.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: Component, 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: Component = Component.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.id; + 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.id; + 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.iop && obj.iop[op]) { + this.menuOption[this.menuSize] = obj.iop[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.id; + 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.id; + 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.id; + this.menuParamB[this.menuSize] = slot; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } + + if (child.interactable && obj.iop) { + for (let op: number = 2; op >= 0; op--) { + if (obj.iop[op]) { + this.menuOption[this.menuSize] = obj.iop[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.id; + 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.id; + 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.id; + 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 === Component.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 === Component.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 === Component.BUTTON_CLOSE) { + this.menuOption[this.menuSize] = 'Close'; + this.menuAction[this.menuSize] = 947; + this.menuParamC[this.menuSize] = child.id; + this.menuSize++; + } else if (child.buttonType === Component.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 === Component.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 === Component.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: Component): boolean => { + let type: number = component.clientCode; + if (type >= Component.CC_FRIENDS_START && type <= Component.CC_FRIENDS_UPDATE_END) { + if (type >= Component.CC_FRIENDS_UPDATE_START) { + type -= Component.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 >= Component.CC_IGNORES_START && type <= Component.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.op) { + for (let op: number = 4; op >= 0; op--) { + if (loc.op[op]) { + this.menuOption[this.menuSize] = loc.op[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.tail() 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.op && type.op[op]) { + this.menuOption[this.menuSize] = type.op[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.op) { + for (type = 4; type >= 0; type--) { + if (npc.op[type] && npc.op[type]?.toLowerCase() !== 'attack') { + this.menuOption[this.menuSize] = npc.op[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.op) { + for (type = 4; type >= 0; type--) { + if (npc.op[type] && npc.op[type]?.toLowerCase() === 'attack') { + let action: number = 0; + if (this.localPlayer && npc.vislevel > this.localPlayer.combatLevel) { + action = 2000; + } + + this.menuOption[this.menuSize] = npc.op[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(Component.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(Component.instances[this.sidebarInterfaceId], this.mouseX, this.mouseY, 562, 231, 0); + } else if (this.tabInterfaceId[this.selectedTab] !== -1) { + this.handleInterfaceInput(Component.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(Component.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.id) { + if ((locShape < LocShape.WALLDECOR_STRAIGHT_OFFSET.id || locShape === LocShape.CENTREPIECE_STRAIGHT.id) && collisionMap.reachedWall(x, z, dx, dz, locShape - 1, locAngle)) { + arrived = true; + break; + } + + if (locShape < LocShape.CENTREPIECE_STRAIGHT.id && 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.cyclelogic6++; + if (Client.cyclelogic6 > 1406) { + Client.cyclelogic6 = 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.head() 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.head() 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.head() 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.id) { + shape = LocShape.CENTREPIECE_STRAIGHT.id; + } + + 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.id, 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.id) { + const nextRotation: number = (rotation + 1) & 0x3; + this.scene?.setWallModels( + x, + z, + level, + type.getModel(LocShape.WALL_L.id, rotation + 4, heightmapSW, heightmapSE, heightmapNE, heightmapNW, seqId), + type.getModel(LocShape.WALL_L.id, 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.id, 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.head() 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.cyclelogic5++; + if (Client.cyclelogic5 > 85) { + Client.cyclelogic5 = 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.walkmerge) { + 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 = LocShape.of(shape).layer; + 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.head() 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.id, otherId, otherAngle, otherShape); + this.spawnedLocations.addTail(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 = LocShape.of(shape).layer; + 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.addTail(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]?.addTail(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.head() as ObjStackEntity | null; next; next = list.prev() as ObjStackEntity | null) { + if (next.index === (id & 0x7fff)) { + next.unlink(); + break; + } + } + if (!list.head()) { + 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.addTail(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.addTail(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]?.addTail(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 = LocShape.of(shape).layer; + 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.addTail(loc1); + + const loc2: LocSpawned = new LocSpawned(this.currentLevel, layer, x, z, id, angle, shape, end + this.loopCycle); + this.temporaryLocs.addTail(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.head() 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 176ee21a..21ebea1a 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 {CanvasEnabledKeys, KeyCodes} 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 { @@ -62,6 +64,8 @@ export default abstract class GameShell { private nx: number = 0; private ny: number = 0; + protected renderMode: RenderMode = RenderMode.GPU; + constructor(resizetoFit: boolean = false) { canvas.tabIndex = -1; canvas2d.fillStyle = 'black'; @@ -72,6 +76,9 @@ export default abstract class GameShell { } else { this.resize(canvas.width, canvas.height); } + if (this.renderMode === RenderMode.GPU) { + DrawGL.init(); + } } abstract getTitleScreenState(): number; diff --git a/src/js/jagex2/dash3d/World3D.ts b/src/js/jagex2/dash3d/World3D.ts index 670f8a02..e39f5baa 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,13 @@ 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 +1008,8 @@ export default class World3D { eyeZ = this.maxTileZ * 128 - 1; } + const distance: number = DrawGL.GL_ENABLED ? DrawGL.renderDistance : 25; + World3D.cycle++; World3D.sinEyePitch = Draw3D.sin[eyePitch]; World3D.cosEyePitch = Draw3D.cos[eyePitch]; @@ -1015,22 +1024,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 +1056,8 @@ 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 +1073,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 +1081,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 +1127,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..9db628f0 100644 --- a/src/js/jagex2/graphics/Canvas.ts +++ b/src/js/jagex2/graphics/Canvas.ts @@ -1,6 +1,8 @@ +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 b32fbc07..02e6baf1 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..88ed4331 --- /dev/null +++ b/src/js/jagex2/graphics/DrawGL.ts @@ -0,0 +1,629 @@ +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'; +import Pix8 from './Pix8'; + +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: WebGLVertexArrayObject | null; + + private static interfaceTexture: WebGLTexture | null; + private static interfacePbo: WebGLVertexArrayObject; + + private static vaoUiHandle: WebGLVertexArrayObject | null; + private static vboUiHandle: WebGLBuffer | null; + + private static textureArrayId: WebGLTexture = -1; + private static tileHeightTex: WebGLVertexArrayObject; + + private static lastCanvasWidth: number; + private static lastCanvasHeight: number; + + 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; + + // debug + private static noDraw: boolean = false; + + 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); + + // prettier-ignore + 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}`); + }; + + private static updateTextures(textureArrayId: WebGLTexture): void { + const textures: (Pix8 | null)[] = Draw3D.textures; + + gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureArrayId); + + let cnt: number = 0; + for (let textureId: number = 0; textureId < textures.length; textureId++) { + const texture: Pix8 | null = textures[textureId]; + if (texture != null) { + const texturePixels: Int8Array = 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: Uint8Array = 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: (Pix8 | null)[] = Draw3D.textures; + + const textureArrayId: WebGLTexture = 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: number = 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); + }*/ + + static uniformBufferAlloc(): void { + // Bind the buffer + gl.bindBuffer(gl.UNIFORM_BUFFER, DrawGL.uniformBuffer.glBufferId); + + // Create a new Float32Array to hold your data + const data: Int32Array = new Int32Array(2048 * 4 + 9); + + const centerX: number = Draw3D.centerY; + const centerY: number = 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: number = 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 + const bindingPoint: number = 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): void { + // create a standard orthographic projection + const tx: number = -((right + left) / (right - left)); + const ty: number = -((top + bottom) / (top - bottom)); + const tz: number = -((far + near) / (far - near)); + + // TODO: refactor into main useProgram setup already in draw() + gl.useProgram(DrawGL.glProgram); + + const matrix: Float32Array = 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: number = 25; + const LOCAL_COORD_BITS: number = 7; + const SCENE_SIZE: number = 104; + const LOCAL_TILE_SIZE: number = 1 << LOCAL_COORD_BITS; // 128 - size of a tile in local coordinates + const width: number = gl.canvas.width; + const height: number = 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()*/ 0.8); + 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: Uint8Array = 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: GLIntBuffer | GLFloatBuffer, usage: number): void { + const buffer: Int32Array | Float32Array = data.getBuffer(); + const size: number = buffer.length; + gl.bindBuffer(target, glBuffer.glBufferId); + if (size > glBuffer.size) { + const newSize: number = 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 (;;) { + const err: number = gl.getError(); + if (err == gl.NO_ERROR) { + return; + } + + let errStr: string; + 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; + } + } + + static convertPixels(srcPixels: Int8Array, width: number, height: number, textureWidth: number, textureHeight: number): Uint8Array { + const pixels: Uint8Array = new Uint8Array(textureWidth * textureHeight * 4); + + let pixelIdx: number = 0; + let srcPixelIdx: number = 0; + + const offset: number = (textureWidth - width) * 4; + + for (let y: number = 0; y < height; y++) { + for (let x: number = 0; x < width; x++) { + const rgb: number = 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 { + const pixels: Uint8Array = new Uint8Array(arr.buffer); + for (let i: number = 0; i < pixels.length; i += 4) { + const temp: number = pixels[i]; + pixels[i] = pixels[i + 2]; + pixels[i + 2] = temp; + } + return pixels; + } +} diff --git a/src/js/jagex2/graphics/GLBuffer.ts b/src/js/jagex2/graphics/GLBuffer.ts new file mode 100644 index 00000000..d1f65378 --- /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; + } +} diff --git a/src/js/jagex2/graphics/GLFloatBuffer.ts b/src/js/jagex2/graphics/GLFloatBuffer.ts new file mode 100644 index 00000000..5ab0b9a2 --- /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: number = this.buffer.length; + const position: number = this.buffer.length; + if (capacity - position < size) { + let newCapacity: number = capacity; + while (newCapacity - position < size) { + newCapacity *= 2; + } + + const newBuffer: Float32Array = 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; + } +} diff --git a/src/js/jagex2/graphics/GLIntBuffer.ts b/src/js/jagex2/graphics/GLIntBuffer.ts new file mode 100644 index 00000000..93860dfa --- /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: number = 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: number = this.buffer.length; + const position: number = this.buffer.length; + if (capacity - position < size) { + let newCapacity: number = capacity; + while (newCapacity - position < size) { + newCapacity *= 2; + } + const newBuffer: Int32Array = 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; + } +} diff --git a/src/js/jagex2/graphics/GLManager.ts b/src/js/jagex2/graphics/GLManager.ts new file mode 100644 index 00000000..737e6916 --- /dev/null +++ b/src/js/jagex2/graphics/GLManager.ts @@ -0,0 +1,348 @@ +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(): void { + const MAX_VERTEX_COUNT: number = 6500; + const MAX_DIAMETER: number = 6000; + + this.distances = new Int32Array(MAX_VERTEX_COUNT); + this.distanceFaceCount = new Array(MAX_DIAMETER); + this.distanceToFaces = new Array(MAX_DIAMETER).fill(null).map((): string[] => 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(): void { + 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: number = model.vertexCount; + // vertices on X, Y, Z + const verticesX: Int32Array = model.vertexX; + const verticesY: Int32Array = model.vertexY; + const verticesZ: Int32Array = model.vertexZ; + + // face count + const faceCount: number = model.faceCount; + // faces + const faceVertexA: Int32Array = model.faceVertexA; + const faceVertexB: Int32Array = model.faceVertexB; + const faceVertexC: Int32Array = model.faceVertexC; + //face color + const faceColor: Int32Array | null = model.faceColor; + const facePriority: Int32Array | null = 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]; + + const 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: number = 0; + if (faceTransparencies != null && (faceTextures == null || faceTextures[face] == -1)) { + alpha = (faceTransparencies[face] & 0xff) << 24; + } + let priority: number = 0; + if (facePriorities != null) { + priority = (facePriorities[face] & 0xff) << 16; + } + return alpha | priority; + } + + static interpolateHSL(hsl: number, hue2: number, sat2: number, lum2: number, lerp: number): number { + let hue: number = (hsl >> 10) & 63; + let sat: number = (hsl >> 7) & 7; + let lum: number = hsl & 127; + const 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; + } +} diff --git a/src/js/jagex2/graphics/GLShader.ts b/src/js/jagex2/graphics/GLShader.ts new file mode 100644 index 00000000..6fd9cf2d --- /dev/null +++ b/src/js/jagex2/graphics/GLShader.ts @@ -0,0 +1,93 @@ +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); + + const resp: Response = await fetch(unit.getFilename); + const source: string = 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'; + } +} diff --git a/src/js/jagex2/graphics/Model.ts b/src/js/jagex2/graphics/Model.ts index b0dd86ab..2bba2eb2 100644 --- a/src/js/jagex2/graphics/Model.ts +++ b/src/js/jagex2/graphics/Model.ts @@ -8,6 +8,8 @@ import AnimBase from './AnimBase'; 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,7 @@ 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 } else { picking = true; } @@ -2157,7 +2164,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 +2174,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 +2197,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 +2224,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; @@ -2246,12 +2275,17 @@ export default class Model extends Hashable { 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 +2296,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 +2376,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 +2393,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 +2410,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 +2430,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 +2451,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 +2481,126 @@ 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: number = 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: number = 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 +2713,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 +2786,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, Model.clippedX[3], y2, Model.clippedY[3], Model.clippedColor[2]); @@ -2815,7 +2896,7 @@ export default class Model extends Hashable { ); } } - } + }; private applyTransform2(x: number, y: number, z: number, labels: Uint8Array | null, type: number): void { if (!labels) { diff --git a/src/js/jagex2/graphics/PixMap.ts b/src/js/jagex2/graphics/PixMap.ts index b1d87c5e..d9e07d46 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 @@ -30,7 +31,9 @@ export default class PixMap { draw(x: number, y: number): void { this.#setPixels(); - this.ctx.putImageData(this.image, x, y); + if (!DrawGL.GL_ENABLED) + // if not using WebGL + this.ctx.putImageData(this.image, x, y); } #setPixels(): void { diff --git a/src/js/jagex2/graphics/RenderMode.ts b/src/js/jagex2/graphics/RenderMode.ts new file mode 100644 index 00000000..230762dd --- /dev/null +++ b/src/js/jagex2/graphics/RenderMode.ts @@ -0,0 +1,5 @@ +export enum RenderMode { + CPU = 0, + CPU_WF = 1, // wireframe + GPU = 2 +} diff --git a/src/js/jagex2/graphics/ShaderTemplate.ts b/src/js/jagex2/graphics/ShaderTemplate.ts new file mode 100644 index 00000000..66382791 --- /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: string[] = str.split('\n'); + const processedLines: string[] = []; + + for (const line of lines) { + if (line.startsWith('#include ')) { + const resource: string = line.substring(9).trim(); + const resourceStr: string = 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: string | null = 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..a135434b --- /dev/null +++ b/src/js/pg2.ts @@ -0,0 +1,366 @@ +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 Component from './jagex2/config/Component'; + +import Draw3D from './jagex2/graphics/Draw3D'; +import PixFont from './jagex2/graphics/PixFont'; +import Model from './jagex2/graphics/Model'; +import AnimBase from './jagex2/graphics/AnimBase'; +import AnimFrame from './jagex2/graphics/AnimFrame'; + +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); + AnimBase.unpack(models); + AnimFrame.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'); + Component.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 + ); + + 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); + } + + DrawGL.draw(); + }; + + draw = async (): Promise => { + if (this.gpuRender && this.modelsLoaded) { + await this.drawGpu(); + } else if (!this.gpuRender) { + Draw2D.clear(); + + const startColor: number = 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 2c8574f9..d29411c5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,9 +11,7 @@ const stylesHandler = isProduction ? MiniCssExtractPlugin.loader : 'style-loader const pages = [ 'index', 'playground', 'viewer', 'mesanim', 'items', 'sounds', - 'interface-editor', - 'JagEd', - 'mapview' + 'interface-editor', 'JagEd', 'mapview', 'pg2', 'game_gl' ]; const htmlPlugins = pages.map(name => { return new HtmlWebpackPlugin({ @@ -33,7 +31,9 @@ const config = { sounds: './src/js/sounds.ts', ['interface-editor']: './src/js/interface-editor.ts', JagEd: './src/js/JagEd.ts', - mapview: './src/js/mapview.ts' + mapview: './src/js/mapview.ts', + pg2: './src/js/pg2.ts', + game_gl: './src/js/game_gl.ts', }, plugins: [