├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── docs ├── app.min.js ├── images │ ├── maps.webp │ └── maps │ │ ├── mp_rr_canyonlands_mu3.webp │ │ ├── mp_rr_desertlands_mu3.webp │ │ ├── mp_rr_olympus_mu2.webp │ │ └── mp_rr_tropic_island_mu1.webp └── index.html ├── package-lock.json ├── package.json ├── src ├── Runner.ts ├── app.tsx ├── lib │ ├── api │ │ ├── Channel.ts │ │ ├── Process.ts │ │ ├── Region.ts │ │ ├── Server.ts │ │ ├── abstracts │ │ │ ├── Adapter.ts │ │ │ ├── enums │ │ │ │ ├── DeltaType.ts │ │ │ │ └── PacketType.ts │ │ │ └── interfaces │ │ │ │ ├── IPacketProvider.ts │ │ │ │ └── IPacketWriter.ts │ │ ├── classes │ │ │ ├── Entity.ts │ │ │ └── EntityMember.ts │ │ ├── index.ts │ │ ├── packets │ │ │ ├── BasicAlive.ts │ │ │ ├── BasicSync.ts │ │ │ ├── EntityChange.ts │ │ │ ├── EntityChangeMember.ts │ │ │ ├── EntityChangeMemberDelta.ts │ │ │ ├── EntityCreate.ts │ │ │ ├── EntityCreateMember.ts │ │ │ ├── EntityDelete.ts │ │ │ ├── EntityUpdate.ts │ │ │ ├── EntityUpdateEntity.ts │ │ │ └── EntityUpdateEntityMember.ts │ │ ├── providers │ │ │ ├── AliveProvider.ts │ │ │ └── EntityProvider.ts │ │ └── streams │ │ │ ├── BinaryReader.ts │ │ │ └── BinaryWriter.ts │ ├── core │ │ ├── Core.ts │ │ ├── classes │ │ │ ├── ButtonList.ts │ │ │ ├── Entity.ts │ │ │ ├── EntityList.ts │ │ │ ├── EntityListFilter.ts │ │ │ ├── LevelName.ts │ │ │ ├── LocalPlayer.ts │ │ │ ├── Signifier.ts │ │ │ └── SignifierList.ts │ │ ├── entities │ │ │ ├── Item.ts │ │ │ ├── NPC.ts │ │ │ └── Player.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── GlowData.ts │ │ │ └── VectorData.ts │ │ ├── offsets.ts │ │ └── types │ │ │ ├── CString.ts │ │ │ ├── Float32.ts │ │ │ ├── Glow.ts │ │ │ ├── UInt32.ts │ │ │ ├── UInt64.ts │ │ │ ├── UInt8.ts │ │ │ └── Vector.ts │ ├── features │ │ ├── Map.ts │ │ ├── Radar.ts │ │ ├── Recoil.ts │ │ ├── Sense.ts │ │ └── index.ts │ ├── index.ts │ └── items │ │ ├── index.ts │ │ ├── list.ts │ │ └── types │ │ ├── All.ts │ │ ├── Ammo.ts │ │ ├── AmmoType.ts │ │ ├── Attachment.ts │ │ ├── AttachmentType.ts │ │ ├── Gear.ts │ │ ├── GearType.ts │ │ ├── Grenade.ts │ │ ├── Regen.ts │ │ ├── Weapon.ts │ │ └── WeaponType.ts └── ui │ ├── MainView.tsx │ ├── MainViewModel.ts │ ├── classes │ ├── BoolStorage.ts │ ├── NumberStorage.ts │ └── StringStorage.ts │ ├── index.ts │ ├── language.ts │ ├── main │ ├── MainView.tsx │ └── index.ts │ ├── settings │ ├── MainView.tsx │ ├── MainViewModel.ts │ ├── general │ │ ├── MainView.tsx │ │ ├── MainViewModel.ts │ │ ├── enums │ │ │ └── MainType.ts │ │ ├── index.ts │ │ ├── language.ts │ │ ├── viewModels │ │ │ ├── MapViewModel.ts │ │ │ ├── RadarViewModel.ts │ │ │ └── SenseViewModel.ts │ │ └── views │ │ │ ├── MapView.tsx │ │ │ ├── RadarView.tsx │ │ │ └── SenseView.tsx │ ├── index.ts │ ├── items │ │ ├── MainView.tsx │ │ ├── MainViewModel.ts │ │ ├── index.ts │ │ ├── language.ts │ │ ├── viewModels │ │ │ ├── AreaViewModel.ts │ │ │ └── ItemViewModel.ts │ │ └── views │ │ │ ├── AreaView.tsx │ │ │ └── ItemView.tsx │ ├── language.ts │ └── research │ │ ├── MainView.tsx │ │ ├── MainViewModel.ts │ │ ├── index.ts │ │ ├── language.ts │ │ ├── viewModels │ │ └── RecoilViewModel.ts │ │ └── views │ │ └── RecoilView.tsx │ └── utilities.ts ├── tsconfig.json ├── webpack.common.js ├── webpack.development.js └── webpack.production.js /.gitattributes: -------------------------------------------------------------------------------- 1 | /docs/*.min.js linguist-generated 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /res/mp_* 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [{ 4 | "name": "Client", 5 | "outFiles": ["${workspaceFolder}/dist/**/*"], 6 | "outputCapture": "std", 7 | "preLaunchTask": "Webpack", 8 | "request": "launch", 9 | "type": "pwa-chrome", 10 | "url": "http://localhost:3000/" 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "files.exclude": { 5 | "dist": true, 6 | "docs/*.min.js": true, 7 | "node_modules": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [{ 4 | "group": {"isDefault": true, "kind": "build"}, 5 | "isBackground": true, 6 | "label": "TypeScript", 7 | "option": "watch", 8 | "problemMatcher": ["$tsc-watch"], 9 | "tsconfig": "tsconfig.json", 10 | "type": "typescript" 11 | }, { 12 | "dependsOn": ["TypeScript"], 13 | "isBackground": true, 14 | "label": "Webpack", 15 | "script": "webpack:serve", 16 | "type": "npm", 17 | "problemMatcher": [{ 18 | "background": { 19 | "activeOnStart": true, 20 | "beginsPattern": ".", 21 | "endsPattern": "compiled (with|successfully)" 22 | }, 23 | "pattern": [{ 24 | "regexp": ".", 25 | "file": 1, 26 | "location": 2, 27 | "message": 3 28 | }] 29 | }] 30 | }] 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http-game-apex 2 | 3 | Linux *Apex Legends* connector powered by [http-driver](https://github.com/XRadius/http-driver). **This connector won't work without `http-driver`**. 4 | 5 | # Quick Start 6 | 7 | The easiest way to use `http-game-apex` is to navigate to your `http-driver` and load this connector: 8 | 9 | ``` 10 | https://xradius.github.io/http-game-apex/ 11 | ``` 12 | 13 | This is the *precompiled production connector*, which [updates automatically](https://github.com/XRadius/http-game-apex/deployments/). 14 | 15 | # Installation 16 | 17 | We'll setup a development environment to compile and run from source. 18 | 19 | 1. Install *Google Chrome*, *NodeJS* and *Visual Studio Code*: 20 | 21 | * Follow the instructions at https://www.google.com/intl/en_us/chrome/ 22 | * Follow the instructions at http://nodejs.org/ (`node` >= 16, `npm` >= 7) 23 | * Follow the instructions at https://code.visualstudio.com/ 24 | 25 | 2. Clone this repository: 26 | 27 | ``` 28 | git clone https://github.com/XRadius/http-game-apex/ 29 | ``` 30 | 31 | 3. Open the `http-game-apex` directory: 32 | 33 | ``` 34 | cd http-game-apex 35 | ``` 36 | 37 | 4. Install dependencies: 38 | 39 | ``` 40 | npm install 41 | ``` 42 | 43 | 5. Start debugging: 44 | 45 | * Open the `http-game-apex` directory with *Visual Studio Code*. 46 | * Press `F5`, wait for compilation, and eventually *Google Chrome* will launch. 47 | 48 | 6. Load as a *connector* in your `http-driver`: 49 | 50 | * Use the *Google Chrome* window to navigate to your `http-driver`. 51 | * Load *http://0.0.0.0:3000/* as the *connector*. Replace `0.0.0.0` for your *network-resolvable* IP. 52 | 53 | # Troubleshooting 54 | 55 | These are the most common problems when using `http-game-apex`. 56 | 57 | ## "Load this connector in your http-driver." 58 | 59 | You've loaded this *connector* in your browser without using `http-driver`. Because of [mixed content](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content#warnings_in_firefox_web_console) and [private network CORS](https://web.dev/cors-rfc1918-feedback/) policies, your browser will prevent the *connector* from reaching your `http-driver`. To prevent this, `http-driver` loads *connectors* through a [reverse proxy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling) mechanism to map the *connector* into its private network space. To solve this issue, navigate to your `http-driver` and enter the *connector* URL there. 60 | 61 | ## "Your http-driver is outdated." 62 | 63 | As it says on the box, your `http-driver` is outdated! Follow the [update instructions](https://github.com/XRadius/http-driver#updating) and try again. 64 | -------------------------------------------------------------------------------- /docs/images/maps.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XRadius/http-game-apex/f0e9db33e8b28ec722aec547bd1455c8895e9181/docs/images/maps.webp -------------------------------------------------------------------------------- /docs/images/maps/mp_rr_canyonlands_mu3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XRadius/http-game-apex/f0e9db33e8b28ec722aec547bd1455c8895e9181/docs/images/maps/mp_rr_canyonlands_mu3.webp -------------------------------------------------------------------------------- /docs/images/maps/mp_rr_desertlands_mu3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XRadius/http-game-apex/f0e9db33e8b28ec722aec547bd1455c8895e9181/docs/images/maps/mp_rr_desertlands_mu3.webp -------------------------------------------------------------------------------- /docs/images/maps/mp_rr_olympus_mu2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XRadius/http-game-apex/f0e9db33e8b28ec722aec547bd1455c8895e9181/docs/images/maps/mp_rr_olympus_mu2.webp -------------------------------------------------------------------------------- /docs/images/maps/mp_rr_tropic_island_mu1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XRadius/http-game-apex/f0e9db33e8b28ec722aec547bd1455c8895e9181/docs/images/maps/mp_rr_tropic_island_mu1.webp -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http-game-apex 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "http-game-apex", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@emotion/react": "11.10.0", 7 | "@emotion/styled": "11.10.0", 8 | "@mui/icons-material": "5.8.4", 9 | "@mui/material": "5.10.0", 10 | "mobx": "6.6.1", 11 | "mobx-react": "7.5.2", 12 | "react": "17.0.2", 13 | "react-dom": "17.0.2", 14 | "reflect-metadata": "0.1.13", 15 | "@types/react": "17.0.19", 16 | "@types/react-dom": "17.0.9", 17 | "pre-commit": "1.2.2", 18 | "source-map-loader": "3.0.1", 19 | "typescript": "4.6.2", 20 | "webpack": "5.70.0", 21 | "webpack-cli": "4.9.2", 22 | "webpack-dev-server": "4.7.4" 23 | }, 24 | "engines": { 25 | "node": "18.x", 26 | "npm": "8.x" 27 | }, 28 | "pre-commit": [ 29 | "prepare" 30 | ], 31 | "scripts": { 32 | "prepare": "tsc && npm run webpack:build && git add docs/*.min.js", 33 | "webpack:build": "webpack build --config webpack.production.js", 34 | "webpack:serve": "webpack serve --config webpack.development.js" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Runner.ts: -------------------------------------------------------------------------------- 1 | import * as app from 'lib'; 2 | import * as ui from 'ui'; 3 | 4 | export class Runner { 5 | private constructor( 6 | private readonly canvas = document.getElementById('canvas') as HTMLCanvasElement, 7 | private readonly map = new app.features.Map(canvas), 8 | private readonly radar = new app.features.Radar(canvas), 9 | private readonly recoil = new app.features.Recoil(), 10 | private readonly sense = new app.features.Sense()) {} 11 | 12 | static create() { 13 | const source = new Runner(); 14 | source.attach(); 15 | return source.run.bind(source); 16 | } 17 | 18 | run(core: app.core.Core, vm: ui.MainViewModel) { 19 | const localPlayer = core.playerList.get(core.localPlayer.value); 20 | this.updateResearch(core, vm, localPlayer); 21 | this.updateSense(core, vm, localPlayer); 22 | this.canvas.height = window.innerHeight; 23 | this.canvas.width = window.innerWidth; 24 | this.update(core, vm, localPlayer); 25 | } 26 | 27 | private attach() { 28 | this.canvas.addEventListener('dblclick', () => { 29 | (document.fullscreenElement 30 | ? document.exitFullscreen() 31 | : document.body.requestFullscreen()).catch(); 32 | }); 33 | } 34 | 35 | private update(core: app.core.Core, vm: ui.MainViewModel, localPlayer?: app.core.Player) { 36 | switch (vm.settings.general.viewType.value) { 37 | case ui.settings.general.MainType.Map: 38 | this.map.refresh(core.levelName.value); 39 | this.updateMap(core, vm, localPlayer); 40 | break; 41 | case ui.settings.general.MainType.Radar: 42 | this.radar.refresh(); 43 | this.updateRadar(core, vm, localPlayer); 44 | break; 45 | } 46 | } 47 | 48 | private updateMap(core: app.core.Core, vm: ui.MainViewModel, localPlayer?: app.core.Player) { 49 | if (vm.settings.general.map.showItems.value) 50 | this.map.renderItems(core.itemList.values(), vm.settings.itemSet); 51 | if (vm.settings.general.map.showPlayers.value && localPlayer) 52 | this.map.renderPlayers(localPlayer, core.playerList.values()); 53 | } 54 | 55 | private updateRadar(core: app.core.Core, vm: ui.MainViewModel, localPlayer?: app.core.Player) { 56 | if (vm.settings.general.radar.showItems.value && localPlayer) 57 | this.radar.renderItems(localPlayer, core.itemList.values(), vm.settings.itemSet); 58 | if (vm.settings.general.radar.showPlayers.value && localPlayer) 59 | this.radar.renderNpcs(localPlayer, core.npcList.values()); 60 | if (vm.settings.general.radar.showPlayers.value && localPlayer) 61 | this.radar.renderPlayers(localPlayer, core.playerList.values()); 62 | } 63 | 64 | private updateResearch(core: app.core.Core, vm: ui.MainViewModel, localPlayer?: app.core.Player) { 65 | if (vm.settings.research.recoil.enable.value && localPlayer && vm.settings.research.recoil.options) 66 | this.recoil.update(core.buttonList, localPlayer, vm.settings.research.recoil.options); 67 | } 68 | 69 | private updateSense(core: app.core.Core, vm: ui.MainViewModel, localPlayer?: app.core.Player) { 70 | const itemsFn = vm.settings.general.sense.highlightItems.value 71 | ? this.sense.updateItems.bind(this.sense) 72 | : this.sense.resetItems.bind(this.sense); 73 | const playersFn = vm.settings.general.sense.highlightPlayers.value 74 | ? this.sense.updatePlayers.bind(this.sense) 75 | : this.sense.resetPlayers.bind(this.sense); 76 | if (localPlayer) { 77 | itemsFn(localPlayer, core.itemList.values(), vm.settings.itemSet); 78 | playersFn(localPlayer, core.playerList.values()); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import * as mobx from 'mobx'; 3 | import * as React from 'react'; 4 | import * as ReactDOM from 'react-dom'; 5 | import * as ui from 'ui'; 6 | import {Runner} from './Runner'; 7 | const packageData = require('../package'); 8 | 9 | function App() { 10 | return ( 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | const theme = ui.material.createTheme({ 19 | palette: { 20 | mode: 'dark', 21 | primary: {main: '#4CAF50'} 22 | }, 23 | components: { 24 | MuiCssBaseline: { 25 | styleOverrides: { 26 | html: { 27 | backgroundColor: '#333' 28 | }, 29 | body: { 30 | backgroundColor: '#333', 31 | }, 32 | canvas: { 33 | display: 'none', 34 | position: 'fixed' 35 | }, 36 | '::-webkit-scrollbar': { 37 | width: '12px' 38 | }, 39 | '::-webkit-scrollbar-track': { 40 | backgroundColor: '#333' 41 | }, 42 | '::-webkit-scrollbar-thumb': { 43 | backgroundColor: '#4CAF50', 44 | border: '4px solid #333', 45 | borderRadius: '12px' 46 | } 47 | } 48 | } 49 | } 50 | }); 51 | 52 | (function() { 53 | document.title = `${packageData.name} (${packageData.version})`; 54 | mobx.configure({enforceActions: 'never'}); 55 | ReactDOM.render(, document.getElementById('container')); 56 | })(); 57 | -------------------------------------------------------------------------------- /src/lib/api/Channel.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class Channel { 4 | private readonly alive: app.AliveProvider; 5 | private readonly entities: app.EntityProvider; 6 | private readonly socket: WebSocket; 7 | private syncId = 0; 8 | 9 | constructor(pid: number) { 10 | this.alive = new app.AliveProvider(); 11 | this.entities = new app.EntityProvider(); 12 | this.socket = new WebSocket(`ws://${location.host}/ws/direct/${pid}`); 13 | this.socket.binaryType = 'arraybuffer'; 14 | this.socket.addEventListener('message', x => this.receive(x)); 15 | } 16 | 17 | create(adapter: app.Adapter) { 18 | this.entities.create(adapter.source); 19 | } 20 | 21 | delete(adapter: app.Adapter) { 22 | this.entities.delete(adapter.source); 23 | } 24 | 25 | async runAsync(renderFrame: () => void) { 26 | while (true) { 27 | switch (this.socket.readyState) { 28 | case this.socket.CONNECTING: 29 | await new Promise(requestAnimationFrame); 30 | break; 31 | case this.socket.OPEN: 32 | renderFrame(); 33 | this.update(); 34 | await new Promise(requestAnimationFrame); 35 | break; 36 | default: 37 | throw new Error('Invalid channel state!'); 38 | } 39 | } 40 | } 41 | 42 | private receive(ev: MessageEvent) { 43 | if (ev.data instanceof ArrayBuffer) { 44 | const stream = new app.BinaryReader(ev.data); 45 | while (stream.hasBytes()) { 46 | switch (stream.readUInt8() as app.PacketType) { 47 | case app.PacketType.BasicSync: 48 | this.entities.receive(app.BasicSync.create(stream)); 49 | break; 50 | case app.PacketType.EntityUpdate: 51 | this.entities.receive(app.EntityUpdate.create(stream)); 52 | break; 53 | } 54 | } 55 | } 56 | } 57 | 58 | private update() { 59 | const stream = new app.BinaryWriter(); 60 | this.alive.update(stream); 61 | this.entities.update(stream, this.syncId); 62 | if (stream.hasBytes()) { 63 | new app.BasicSync(this.syncId).write(stream); 64 | this.socket.send(stream.toBuffer()); 65 | this.syncId = this.syncId !== 255 ? this.syncId + 1 : 1; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/api/Process.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class Process { 4 | constructor(value: app.IProcess, 5 | readonly args = value.args, 6 | readonly command = value.command, 7 | readonly pid = value.pid) {} 8 | 9 | async regionsAsync() { 10 | const response = await fetch(`/api/proc/${this.pid}/maps`); 11 | const result = await response.json() as Array; 12 | return result.map(x => new app.Region(x)); 13 | } 14 | } 15 | 16 | export type IProcess = { 17 | args: Array, 18 | command: string, 19 | pid: number 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/api/Region.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class Region { 4 | constructor(value: app.IRegion, 5 | readonly start = parseHex(value.start), 6 | readonly end = parseHex(value.end), 7 | readonly perms = value.perms, 8 | readonly offset = parseHex(value.offset), 9 | readonly devMajor = value.devMajor, 10 | readonly devMinor = value.devMinor, 11 | readonly inode = parseHex(value.inode), 12 | readonly pathname = value.pathname) {} 13 | } 14 | 15 | function parseHex(value: string) { 16 | return /^0x/i.test(value) 17 | ? BigInt(value) 18 | : BigInt(`0x${value}`); 19 | } 20 | 21 | export type IRegion = { 22 | devMajor: number, 23 | devMinor: number, 24 | end: string, 25 | inode: string, 26 | offset: string, 27 | pathname: string, 28 | perms: number, 29 | start: string 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/api/Server.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class Server { 4 | async processesAsync() { 5 | const response = await fetch('/api/proc'); 6 | const result = await response.json() as Array; 7 | return result.map(x => new app.Process(x)); 8 | } 9 | 10 | async versionAsync() { 11 | const response = await fetch('/api/version'); 12 | const result = await response.json().catch(() => 0) as Number; 13 | return result; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/api/abstracts/Adapter.ts: -------------------------------------------------------------------------------- 1 | export abstract class Adapter { 2 | constructor( 3 | readonly source: T) {} 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/api/abstracts/enums/DeltaType.ts: -------------------------------------------------------------------------------- 1 | export enum DeltaType { 2 | Byte, 3 | Int16, 4 | Int32, 5 | Int64, 6 | Float32, 7 | Float64, 8 | UInt16, 9 | UInt32, 10 | UInt64 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/api/abstracts/enums/PacketType.ts: -------------------------------------------------------------------------------- 1 | export enum PacketType { 2 | BasicAlive, 3 | BasicSync, 4 | EntityChange, 5 | EntityCreate, 6 | EntityDelete, 7 | EntityUpdate 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/api/abstracts/interfaces/IPacketProvider.ts: -------------------------------------------------------------------------------- 1 | import * as app from '../..'; 2 | 3 | export interface IPacketProvider { 4 | update(stream: app.BinaryWriter, syncId: number): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/api/abstracts/interfaces/IPacketWriter.ts: -------------------------------------------------------------------------------- 1 | import * as app from '../..'; 2 | 3 | export interface IPacketWriter { 4 | write(stream: app.BinaryWriter): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/api/classes/Entity.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class Entity { 4 | readonly address: bigint; 5 | readonly members: Map; 6 | readonly options?: IOptions; 7 | 8 | constructor(address: bigint, members: Iterable>, options?: IOptions) { 9 | this.address = address; 10 | this.members = convertMembers(members); 11 | this.options = options; 12 | } 13 | 14 | receive(packet: app.BasicSync | app.EntityUpdateEntity) { 15 | if (packet instanceof app.EntityUpdateEntity) { 16 | this.receiveUpdate(packet); 17 | } else { 18 | this.receiveSync(packet); 19 | } 20 | } 21 | 22 | update(id: number, syncId: number) { 23 | const packets: Array = []; 24 | this.createUpdate(packets, syncId); 25 | return packets.length ? new app.EntityChange(id, packets) : undefined; 26 | } 27 | 28 | private createUpdate(packets: Array, syncId: number) { 29 | for (const member of this.members.values()) { 30 | const packet = member.update(syncId); 31 | if (!packet) continue; 32 | packets.push(packet); 33 | } 34 | } 35 | 36 | private receiveUpdate(packet: app.EntityUpdateEntity) { 37 | for (const child of packet.members) { 38 | const member = this.members.get(child.offset); 39 | if (!member) continue; 40 | member.receive(child); 41 | } 42 | } 43 | 44 | private receiveSync(packet: app.BasicSync) { 45 | for (const member of this.members.values()) { 46 | member.receive(packet); 47 | } 48 | } 49 | } 50 | 51 | function convertMembers(members: Iterable>) { 52 | const result = new Map(); 53 | for (const member of members) result.set(member.source.offset, member.source); 54 | return result; 55 | } 56 | 57 | type IOptions = { 58 | enableUpdate?: boolean; 59 | requestBatch?: boolean; 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/api/classes/EntityMember.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityMember { 4 | readonly buffer: DataView; 5 | readonly interval: number; 6 | readonly offset: number; 7 | deltas?: Array; 8 | send?: boolean; 9 | syncId?: number; 10 | 11 | constructor(offset: number, interval: number, size: number) { 12 | this.buffer = new DataView(new ArrayBuffer(size)); 13 | this.interval = interval; 14 | this.offset = offset; 15 | } 16 | 17 | receive(packet: app.BasicSync | app.EntityUpdateEntityMember) { 18 | if (packet instanceof app.EntityUpdateEntityMember) { 19 | this.receiveUpdate(packet); 20 | } else { 21 | this.receiveSync(packet); 22 | } 23 | } 24 | 25 | update(syncId: number) { 26 | if (!this.send) return; 27 | const packet = new app.EntityChangeMember(this.offset, this.buffer, this.deltas); 28 | this.deltas = undefined; 29 | this.send = false; 30 | this.syncId = syncId; 31 | return packet; 32 | } 33 | 34 | private receiveUpdate(packet: app.EntityUpdateEntityMember) { 35 | if (typeof this.syncId !== 'undefined') return; 36 | if (packet.buffer.byteLength !== this.buffer.byteLength) return; 37 | for (let i = 0; i < packet.buffer.byteLength; i++) this.buffer.setUint8(i, packet.buffer.getUint8(i)); 38 | } 39 | 40 | private receiveSync(packet: app.BasicSync) { 41 | if (this.syncId !== packet.id) return; 42 | this.syncId = undefined; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/api/index.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = 6; 2 | export * from './abstracts/enums/DeltaType'; 3 | export * from './abstracts/enums/PacketType'; 4 | export * from './abstracts/interfaces/IPacketProvider'; 5 | export * from './abstracts/interfaces/IPacketWriter'; 6 | export * from './abstracts/Adapter'; 7 | export * from './classes/Entity'; 8 | export * from './classes/EntityMember'; 9 | export * from './packets/BasicAlive'; 10 | export * from './packets/BasicSync'; 11 | export * from './packets/EntityChange'; 12 | export * from './packets/EntityChangeMember'; 13 | export * from './packets/EntityChangeMemberDelta'; 14 | export * from './packets/EntityCreate'; 15 | export * from './packets/EntityCreateMember'; 16 | export * from './packets/EntityDelete'; 17 | export * from './packets/EntityUpdate'; 18 | export * from './packets/EntityUpdateEntity'; 19 | export * from './packets/EntityUpdateEntityMember'; 20 | export * from './providers/AliveProvider'; 21 | export * from './providers/EntityProvider'; 22 | export * from './streams/BinaryReader'; 23 | export * from './streams/BinaryWriter'; 24 | export * from './Channel'; 25 | export * from './Process'; 26 | export * from './Region'; 27 | export * from './Server'; 28 | -------------------------------------------------------------------------------- /src/lib/api/packets/BasicAlive.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class BasicAlive implements app.IPacketWriter { 4 | write(stream: app.BinaryWriter) { 5 | stream.writeUInt8(app.PacketType.BasicAlive); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/api/packets/BasicSync.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class BasicSync implements app.IPacketWriter { 4 | constructor( 5 | readonly id: number) {} 6 | 7 | static create(stream: app.BinaryReader) { 8 | const id = stream.readUInt8(); 9 | return new BasicSync(id); 10 | } 11 | 12 | write(stream: app.BinaryWriter) { 13 | stream.writeUInt8(app.PacketType.BasicSync); 14 | stream.writeUInt8(this.id); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/api/packets/EntityChange.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityChange implements app.IPacketWriter { 4 | constructor( 5 | readonly id: number, 6 | readonly changes: Array) {} 7 | 8 | write(stream: app.BinaryWriter) { 9 | stream.writeUInt8(app.PacketType.EntityChange); 10 | stream.writeVariableLength(this.id); 11 | stream.writeVariableLength(this.changes.length); 12 | this.changes.forEach(x => x.write(stream)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/api/packets/EntityChangeMember.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityChangeMember implements app.IPacketWriter { 4 | constructor( 5 | readonly offset: number, 6 | readonly buffer: DataView, 7 | readonly deltas?: Array) {} 8 | 9 | write(stream: app.BinaryWriter) { 10 | stream.writeVariableLength(this.offset); 11 | if (this.deltas) { 12 | stream.writeVariableLength(this.deltas.length); 13 | this.deltas.forEach(x => x.write(stream)); 14 | } else { 15 | stream.writeUInt8(0); 16 | stream.writeVariableLength(this.buffer.byteLength); 17 | stream.writeBytes(this.buffer); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/api/packets/EntityChangeMemberDelta.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityChangeMemberDelta implements app.IPacketWriter { 4 | constructor( 5 | readonly offset: number, 6 | readonly type: app.DeltaType, 7 | readonly buffer: DataView) {} 8 | 9 | write(stream: app.BinaryWriter) { 10 | stream.writeVariableLength(this.offset); 11 | stream.writeUInt8(this.type); 12 | stream.writeBytes(this.buffer); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/api/packets/EntityCreate.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityCreate implements app.IPacketWriter { 4 | constructor( 5 | readonly id: number, 6 | readonly address: bigint, 7 | readonly members: Array, 8 | readonly requestBatch: boolean) {} 9 | 10 | write(stream: app.BinaryWriter) { 11 | stream.writeUInt8(app.PacketType.EntityCreate); 12 | stream.writeVariableLength(this.id); 13 | stream.writeUInt64(this.address); 14 | stream.writeVariableLength(this.members.length); 15 | this.members.forEach(x => x.write(stream)); 16 | stream.writeUInt8(Number(this.requestBatch)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/api/packets/EntityCreateMember.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityCreateMember implements app.IPacketWriter { 4 | constructor( 5 | readonly offset: number, 6 | readonly interval: number, 7 | readonly size: number) {} 8 | 9 | write(stream: app.BinaryWriter) { 10 | stream.writeVariableLength(this.offset); 11 | stream.writeVariableLength(this.interval); 12 | stream.writeVariableLength(this.size); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/api/packets/EntityDelete.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityDelete implements app.IPacketWriter { 4 | constructor( 5 | readonly id: number) {} 6 | 7 | write(stream: app.BinaryWriter) { 8 | stream.writeUInt8(app.PacketType.EntityDelete); 9 | stream.writeVariableLength(this.id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/api/packets/EntityUpdate.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityUpdate { 4 | private constructor( 5 | readonly entities: Array) {} 6 | 7 | static create(stream: app.BinaryReader) { 8 | const membersSize = stream.readVariableLength(); 9 | const members = process(stream, membersSize); 10 | return new EntityUpdate(members); 11 | } 12 | } 13 | 14 | function process(stream: app.BinaryReader, size: number) { 15 | const result = new Array(size); 16 | for (let i = 0; i < size; i++) result[i] = app.EntityUpdateEntity.create(stream); 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/api/packets/EntityUpdateEntity.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityUpdateEntity { 4 | private constructor( 5 | readonly id: number, 6 | readonly members: Array) {} 7 | 8 | static create(stream: app.BinaryReader) { 9 | const id = stream.readVariableLength(); 10 | const membersSize = stream.readVariableLength(); 11 | const members = process(stream, membersSize); 12 | return new EntityUpdateEntity(id, members); 13 | } 14 | } 15 | 16 | function process(stream: app.BinaryReader, size: number) { 17 | const result = new Array(size); 18 | for (let i = 0; i < size; i++) result[i] = app.EntityUpdateEntityMember.create(stream); 19 | return result; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/api/packets/EntityUpdateEntityMember.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityUpdateEntityMember { 4 | private constructor( 5 | readonly offset: number, 6 | readonly buffer: DataView) {} 7 | 8 | static create(stream: app.BinaryReader) { 9 | const offset = stream.readVariableLength(); 10 | const bufferSize = stream.readVariableLength(); 11 | const buffer = stream.readBytes(bufferSize); 12 | return new EntityUpdateEntityMember(offset, buffer); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/api/providers/AliveProvider.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class AliveProvider implements app.IPacketProvider { 4 | private nextActiveTime = this.getNextTime(); 5 | 6 | update(stream: app.BinaryWriter) { 7 | if (this.nextActiveTime >= Date.now()) return; 8 | new app.BasicAlive().write(stream); 9 | this.nextActiveTime = this.getNextTime(); 10 | } 11 | 12 | private getNextTime() { 13 | return Date.now() + 10000; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/api/providers/EntityProvider.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityProvider implements app.IPacketProvider { 4 | private readonly createEntities = new Map(); 5 | private readonly deleteEntities = new Map(); 6 | private readonly livingEntities = new Map(); 7 | private readonly lookupEntities = new Map(); 8 | private readonly releasedIds: Array = []; 9 | private nextId = 0; 10 | 11 | create(entity: app.Entity) { 12 | const id = this.releasedIds.pop(); 13 | if (typeof id !== 'undefined') { 14 | this.createEntities.set(id, entity); 15 | this.lookupEntities.set(entity, id); 16 | } else { 17 | const id = this.nextId; 18 | this.createEntities.set(id, entity); 19 | this.lookupEntities.set(entity, id); 20 | this.nextId++; 21 | } 22 | } 23 | 24 | delete(entity: app.Entity) { 25 | const id = this.lookupEntities.get(entity); 26 | if (typeof id === 'undefined') { 27 | throw new Error(); 28 | } else if (this.createEntities.has(id)) { 29 | this.createEntities.delete(id); 30 | this.lookupEntities.delete(entity); 31 | } else if (this.livingEntities.has(id)) { 32 | this.deleteEntities.set(id, entity); 33 | this.livingEntities.delete(id); 34 | this.lookupEntities.delete(entity); 35 | } 36 | } 37 | 38 | receive(packet: app.BasicSync | app.EntityUpdate) { 39 | if (packet instanceof app.EntityUpdate) { 40 | this.receiveUpdate(packet); 41 | } else { 42 | this.receiveSync(packet); 43 | } 44 | } 45 | 46 | update(stream: app.BinaryWriter, syncId: number) { 47 | for (const id of this.deleteEntities.keys()) { 48 | const packet = new app.EntityDelete(id); 49 | this.deleteEntities.delete(id); 50 | this.releasedIds.push(id); 51 | packet.write(stream); 52 | } 53 | for (const [id, entity] of this.createEntities) { 54 | const packet = new app.EntityCreate(id, entity.address, convertMembers(entity.members), Boolean(entity.options?.requestBatch)); 55 | this.createEntities.delete(id); 56 | this.livingEntities.set(id, entity); 57 | packet.write(stream); 58 | } 59 | for (const [id, entity] of this.livingEntities) { 60 | if (!entity.options || !entity.options.enableUpdate) continue; 61 | const packet = entity.update(id, syncId); 62 | if (!packet) continue; 63 | packet.write(stream); 64 | } 65 | } 66 | 67 | private receiveUpdate(packet: app.EntityUpdate) { 68 | for (const child of packet.entities) { 69 | const entity = this.livingEntities.get(child.id); 70 | if (!entity) continue; 71 | entity.receive(child); 72 | } 73 | } 74 | 75 | private receiveSync(packet: app.BasicSync) { 76 | for (const entity of this.livingEntities.values()) { 77 | if (!entity.options || !entity.options.enableUpdate) continue; 78 | entity.receive(packet); 79 | } 80 | } 81 | } 82 | 83 | function convertMembers(members: Map) { 84 | const result: Array = []; 85 | for (const [id, member] of members) result.push(new app.EntityCreateMember(id, member.interval, member.buffer.byteLength)); 86 | return result; 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/api/streams/BinaryReader.ts: -------------------------------------------------------------------------------- 1 | export class BinaryReader { 2 | private readonly buffer: DataView; 3 | private offset: number; 4 | 5 | constructor(buffer: ArrayBuffer) { 6 | this.buffer = new DataView(buffer); 7 | this.offset = 0; 8 | } 9 | 10 | hasBytes() { 11 | return this.offset < this.buffer.byteLength - this.buffer.byteOffset; 12 | } 13 | 14 | readBytes(size: number) { 15 | const result = new DataView(this.buffer.buffer, this.offset, size); 16 | this.offset += size; 17 | return result; 18 | } 19 | 20 | readUInt8() { 21 | const result = this.buffer.getUint8(this.offset); 22 | this.offset += 1; 23 | return result; 24 | } 25 | 26 | readUInt64() { 27 | const result = this.buffer.getBigUint64(this.offset, true); 28 | this.offset += 8; 29 | return result; 30 | } 31 | 32 | readVariableLength() { 33 | var more = true; 34 | var value = 0; 35 | var shift = 0; 36 | while (more) { 37 | var b = this.readUInt8(); 38 | more = (b & 0x80) !== 0; 39 | value |= (b & 0x7F) << shift; 40 | shift += 7; 41 | } 42 | return value; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/api/streams/BinaryWriter.ts: -------------------------------------------------------------------------------- 1 | export class BinaryWriter { 2 | private buffer: DataView; 3 | private offset: number; 4 | 5 | constructor() { 6 | this.buffer = new DataView(new ArrayBuffer(1024)); 7 | this.offset = 0; 8 | } 9 | 10 | hasBytes() { 11 | return Boolean(this.offset); 12 | } 13 | 14 | writeBytes(value: DataView) { 15 | for (let i = 0; i < value.byteLength; i++) { 16 | this.writeUInt8(value.getUint8(i)); 17 | } 18 | } 19 | 20 | writeUInt8(value: number) { 21 | this.prepare(1); 22 | this.buffer.setUint8(this.offset, value); 23 | this.offset += 1; 24 | } 25 | 26 | writeUInt64(value: bigint) { 27 | this.prepare(8); 28 | this.buffer.setBigUint64(this.offset, value, true); 29 | this.offset += 8; 30 | } 31 | 32 | writeVariableLength(value: number) { 33 | let more = true; 34 | while (more) { 35 | let chunk = value & 0x7F; 36 | value >>= 7; 37 | more = value != 0; 38 | chunk |= more ? 0x80 : 0; 39 | this.writeUInt8(chunk); 40 | } 41 | } 42 | 43 | toBuffer() { 44 | return new DataView(this.buffer.buffer, 0, this.offset); 45 | } 46 | 47 | private prepare(count: number) { 48 | while (this.offset + count > this.buffer.byteLength) { 49 | const result = new DataView(new ArrayBuffer(this.buffer.byteLength * 2)); 50 | for (let i = 0; i < this.offset; i++) result.setUint8(i, this.buffer.getUint8(i)); 51 | this.buffer = result; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/core/Core.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class Core { 4 | readonly buttonList = new app.ButtonList(this.address); 5 | readonly levelName = new app.LevelName(this.address + app.offsets.core.levelName); 6 | readonly localPlayer = new app.LocalPlayer(this.address + app.offsets.core.localPlayer); 7 | readonly itemList = this.itemFilter.map; 8 | readonly npcList = this.npcFilter.map; 9 | readonly playerList = this.playerFilter.map; 10 | 11 | private constructor( 12 | private readonly address: bigint, 13 | private readonly channel: app.api.Channel, 14 | private readonly entityList = new app.EntityList(address + app.offsets.core.clEntityList), 15 | private readonly itemFilter = new app.EntityListFilter(app.Item, 'prop_survival'), 16 | private readonly npcFilter = new app.EntityListFilter(app.NPC, 'npc_dummie'), 17 | private readonly playerFilter = new app.EntityListFilter(app.Player, 'player'), 18 | private readonly signifierList = new app.SignifierList(channel)) { 19 | this.channel.create(this.buttonList); 20 | this.channel.create(this.entityList); 21 | this.channel.create(this.levelName); 22 | this.channel.create(this.localPlayer); 23 | } 24 | 25 | static async createAsync(server: app.api.Server) { 26 | const processes = await server.processesAsync(); 27 | const targetProcess = processes.find(x => x.command.toLowerCase().endsWith('r5apex.exe')); 28 | if (!targetProcess) throw new Error('Invalid process!'); 29 | const regions = await targetProcess.regionsAsync(); 30 | const targetRegion = regions.find(x => x.pathname.toLowerCase().endsWith('r5apex.exe')) 31 | ?? regions.find(x => x.perms == 1 && x.pathname.startsWith('/memfd')) 32 | ?? regions.find(x => x.start === BigInt(0x140000000)); 33 | if (!targetRegion) throw new Error('Invalid region!'); 34 | return new Core(targetRegion.start, new app.api.Channel(targetProcess.pid)); 35 | } 36 | 37 | async runAsync(renderFrame: () => void) { 38 | const filters = [this.itemFilter, this.npcFilter, this.playerFilter]; 39 | await this.channel.runAsync(() => { 40 | this.entityList.update(this.channel); 41 | filters.forEach(x => x.update(this.channel, this.entityList, this.signifierList)); 42 | renderFrame(); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/core/classes/ButtonList.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class ButtonList extends app.api.Adapter { 4 | constructor(address: bigint, 5 | readonly inSpeed = new app.UInt8(app.offsets.button.inSpeed), 6 | readonly inAttack = new app.UInt8(app.offsets.button.inAttack)) { 7 | super(new app.api.Entity(address, [inSpeed, inAttack])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/core/classes/Entity.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class Entity extends app.api.Adapter { 4 | constructor(address: bigint, 5 | private readonly signifierName = new app.UInt64(app.offsets.entity.iSignifierName, 60000)) { 6 | super(new app.api.Entity(address, [signifierName])); 7 | } 8 | 9 | get value() { 10 | return this.signifierName.value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/core/classes/EntityList.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | const maxEntities = 0x10000; 3 | 4 | export class EntityList extends app.api.Adapter { 5 | private readonly entities = new Map(); 6 | private nextTime = 0; 7 | 8 | constructor(address: bigint, 9 | private readonly pointers = Array(maxEntities).fill(0).map((_, i) => new app.UInt64(i << 5, 1000))) { 10 | super(new app.api.Entity(address, pointers, {requestBatch: true})); 11 | } 12 | 13 | get map(): ReadonlyMap { 14 | return this.entities; 15 | } 16 | 17 | update(channel: app.api.Channel) { 18 | if (!this.nextTime || this.nextTime < Date.now()) { 19 | this.onUpdate(channel); 20 | this.nextTime = Date.now() + 1000; 21 | } 22 | } 23 | 24 | private checkCreate(address: bigint, channel: app.api.Channel, knownSet: Set) { 25 | if (!this.entities.has(address)) { 26 | const entity = new app.Entity(address); 27 | this.entities.set(address, entity); 28 | channel.create(entity); 29 | knownSet.add(address); 30 | } else { 31 | knownSet.add(address); 32 | } 33 | } 34 | 35 | private onUpdate(channel: app.api.Channel) { 36 | const knownSet: Set = new Set(); 37 | for (const pointer of this.pointers) { 38 | const address = pointer.value; 39 | if (!address) continue; 40 | this.checkCreate(address, channel, knownSet); 41 | } 42 | for (const [k, v] of this.entities) { 43 | if (knownSet.has(k)) continue; 44 | this.entities.delete(k); 45 | channel.delete(v); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/core/classes/EntityListFilter.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class EntityListFilter> { 4 | private readonly entities = new Map(); 5 | private nextTime = 0; 6 | 7 | constructor( 8 | private readonly Constructor: new (address: bigint) => T, 9 | private readonly signifier: string) {} 10 | 11 | get map(): ReadonlyMap { 12 | return this.entities; 13 | } 14 | 15 | update(channel: app.api.Channel, entityList: app.EntityList, signifierList: app.SignifierList) { 16 | if (!this.nextTime || this.nextTime < Date.now()) { 17 | this.onUpdate(channel, entityList, signifierList); 18 | this.nextTime = Date.now() + 1000; 19 | } 20 | } 21 | 22 | private checkCreate(address: bigint, channel: app.api.Channel, knownSet: Set) { 23 | if (!this.entities.has(address)) { 24 | const entity = new this.Constructor(address); 25 | this.entities.set(address, entity); 26 | channel.create(entity); 27 | knownSet.add(address); 28 | } else { 29 | knownSet.add(address); 30 | } 31 | } 32 | 33 | private onUpdate(channel: app.api.Channel, entityList: app.EntityList, signifierList: app.SignifierList) { 34 | const knownSet = new Set(); 35 | for (const x of entityList.map.values()) { 36 | const signifier = signifierList.get(x.value); 37 | if (signifier.value !== this.signifier) continue; 38 | this.checkCreate(x.source.address, channel, knownSet); 39 | } 40 | for (const [k, v] of this.entities) { 41 | if (knownSet.has(k)) continue; 42 | this.entities.delete(k); 43 | channel.delete(v); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/core/classes/LevelName.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class LevelName extends app.api.Adapter { 4 | constructor(address: bigint, 5 | private readonly levelName = new app.CString(0, 1000, 32)) { 6 | super(new app.api.Entity(address, [levelName])); 7 | } 8 | 9 | get value() { 10 | return this.levelName.value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/core/classes/LocalPlayer.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class LocalPlayer extends app.api.Adapter { 4 | constructor(address: bigint, 5 | private readonly localPlayer = new app.UInt64(0, 1000)) { 6 | super(new app.api.Entity(address, [localPlayer])); 7 | } 8 | 9 | get value() { 10 | return this.localPlayer.value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/core/classes/Signifier.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class Signifier extends app.api.Adapter { 4 | constructor(address: bigint, 5 | private readonly signifierName = new app.CString(0, 60000, 32)) { 6 | super(new app.api.Entity(address, [signifierName])); 7 | } 8 | 9 | get value() { 10 | return this.signifierName.value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/core/classes/SignifierList.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class SignifierList { 4 | constructor( 5 | private readonly channel: app.api.Channel, 6 | private readonly signifiers: Map = new Map()) {} 7 | 8 | get(address: bigint) { 9 | return this.signifiers.get(address) ?? this.create(address); 10 | } 11 | 12 | private create(address: bigint) { 13 | const signifier = new app.Signifier(address); 14 | this.signifiers.set(address, signifier); 15 | this.channel.create(signifier); 16 | return signifier; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/core/entities/Item.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class Item extends app.api.Adapter { 4 | constructor( 5 | readonly address: bigint, 6 | readonly localOrigin = new app.Vector(app.offsets.entity.localOrigin, 60000), 7 | readonly highlightFunctionBits = new app.Glow(app.offsets.item.highlightFunctionBits, 1000), 8 | readonly customScriptInt = new app.UInt32(app.offsets.item.customScriptInt, 60000)) { 9 | super(new app.api.Entity(address, [localOrigin, highlightFunctionBits, customScriptInt], {enableUpdate: true})); 10 | } 11 | 12 | toString() { 13 | return app.serialize(this); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/core/entities/NPC.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class NPC extends app.api.Adapter { 4 | constructor( 5 | readonly address: bigint, 6 | readonly localOrigin = new app.Vector(app.offsets.entity.localOrigin), 7 | readonly lastVisibleTime = new app.Float32(app.offsets.entity.lastVisibleTime)) { 8 | super(new app.api.Entity(address, [localOrigin, lastVisibleTime], {enableUpdate: true})); 9 | } 10 | 11 | createColor() { 12 | return '#FFF'; 13 | } 14 | 15 | toString() { 16 | return app.serialize(this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/core/entities/Player.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class Player extends app.api.Adapter { 4 | constructor( 5 | readonly address: bigint, 6 | readonly localOrigin = new app.Vector(app.offsets.entity.localOrigin), 7 | readonly glowEnable = new app.UInt8(app.offsets.player.glowEnable), 8 | readonly glowThroughWalls = new app.UInt8(app.offsets.player.glowThroughWall), 9 | readonly teamNum = new app.UInt8(app.offsets.player.iTeamNum, 1000), 10 | readonly name = new app.UInt64(app.offsets.player.iName), 11 | readonly lifeState = new app.UInt8(app.offsets.player.lifeState), 12 | readonly lastVisibleTime = new app.Float32(app.offsets.entity.lastVisibleTime), 13 | readonly vecPunchWeaponAngle = new app.Vector(app.offsets.player.vecPunchWeaponAngle), 14 | readonly viewAngle = new app.Vector(app.offsets.player.viewAngle), 15 | readonly bleedoutState = new app.UInt8(app.offsets.player.bleedoutState)) { 16 | super(new app.api.Entity(address, [localOrigin, glowEnable, glowThroughWalls, teamNum, name, lifeState, lastVisibleTime, vecPunchWeaponAngle, viewAngle, bleedoutState], {enableUpdate: true})); 17 | } 18 | 19 | get isValid() { 20 | return !this.lifeState.value 21 | && this.name.value 22 | && this.glowEnable.value !== 0 23 | && this.glowEnable.value !== 255; 24 | } 25 | 26 | createColor(otherPlayer: app.Player) { 27 | return this.isSameTeam(otherPlayer) 28 | ? (this.bleedoutState.value ? '#FFFF00' : '#00FF00') 29 | : (this.bleedoutState.value ? '#FFA500' : '#FF0000'); 30 | } 31 | 32 | isSameTeam(otherPlayer: app.Player) { 33 | return this.teamNum.value === otherPlayer.teamNum.value; 34 | } 35 | 36 | toString() { 37 | return app.serialize(this); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/core/helpers.ts: -------------------------------------------------------------------------------- 1 | export function serialize(item: T) { 2 | return `(${Object.entries(item) 3 | .sort(([a], [b]) => a.localeCompare(b)) 4 | .map(([k, v]) => `${k}=${v}`) 5 | .join(',')})`; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/core/index.ts: -------------------------------------------------------------------------------- 1 | export {api} from '..'; 2 | export * from './helpers'; 3 | export * from './offsets'; 4 | export * from './classes/ButtonList'; 5 | export * from './classes/Entity'; 6 | export * from './classes/EntityList'; 7 | export * from './classes/EntityListFilter'; 8 | export * from './classes/LevelName'; 9 | export * from './classes/LocalPlayer'; 10 | export * from './classes/Signifier'; 11 | export * from './classes/SignifierList'; 12 | export * from './entities/Item'; 13 | export * from './entities/NPC'; 14 | export * from './entities/Player'; 15 | export * from './models/GlowData'; 16 | export * from './models/VectorData'; 17 | export * from './types/CString'; 18 | export * from './types/Float32'; 19 | export * from './types/Glow'; 20 | export * from './types/UInt8'; 21 | export * from './types/UInt32'; 22 | export * from './types/UInt64'; 23 | export * from './types/Vector'; 24 | export * from './Core'; 25 | -------------------------------------------------------------------------------- /src/lib/core/models/GlowData.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class GlowData { 4 | constructor( 5 | readonly inside: number, 6 | readonly outline: number, 7 | readonly outlineRadius: number, 8 | readonly customState: number, 9 | readonly isDrawn: boolean, 10 | readonly isPostProcess: boolean) {} 11 | 12 | static fromUInt32(value: number) { 13 | const inside = value & 0xFF; 14 | const outline = value >> 8 & 0xFF; 15 | const outlineRadius = value >> 16 & 0xFF; 16 | const customState = value >> 24 & 0x3F; 17 | const isDrawn = Boolean(value >> 24 & 0x40); 18 | const isPostProcess = Boolean(value >> 24 & 0x80); 19 | return new GlowData(inside, outline, outlineRadius, customState, isDrawn, isPostProcess); 20 | } 21 | 22 | isSame(otherGlow: GlowData) { 23 | return this.inside === otherGlow.inside 24 | && this.outline === otherGlow.outline 25 | && this.outlineRadius === otherGlow.outlineRadius 26 | && this.customState === otherGlow.customState 27 | && this.isDrawn === otherGlow.isDrawn 28 | && this.isPostProcess === otherGlow.isPostProcess; 29 | } 30 | 31 | toUInt32() { 32 | let result = 0; 33 | result |= Math.min(0xFF, this.inside); 34 | result |= Math.min(0xFF, this.outline) << 8; 35 | result |= Math.min(0xFF, this.outlineRadius) << 16; 36 | result |= Math.min(0x3F, this.customState) << 24; 37 | result |= this.isDrawn ? 0x40 << 24 : 0; 38 | result |= this.isPostProcess ? 0x80 << 24 : 0; 39 | return result; 40 | } 41 | 42 | toString() { 43 | return app.serialize(this); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/core/models/VectorData.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class VectorData { 4 | static readonly none = new VectorData(0, 0, 0); 5 | 6 | constructor( 7 | readonly x: number, 8 | readonly y: number, 9 | readonly z: number) {} 10 | 11 | isSame(otherVector: VectorData) { 12 | return this.x === otherVector.x 13 | && this.y === otherVector.y 14 | && this.z === otherVector.z; 15 | } 16 | 17 | subtract(otherVector: VectorData) { 18 | return new VectorData( 19 | this.x - otherVector.x, 20 | this.y - otherVector.y, 21 | this.z - otherVector.z); 22 | } 23 | 24 | toString() { 25 | return app.serialize(this); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/core/offsets.ts: -------------------------------------------------------------------------------- 1 | export const offsets = { 2 | button: { 3 | inSpeed: 0x759bfa0, // [Buttons] -> in_speed 4 | inAttack: 0x759bff8 // [Buttons] -> in_attack 5 | }, 6 | core: { 7 | levelName: BigInt(0x13a17b8), // [Miscellaneous] -> LevelName 8 | clEntityList: BigInt(0x1a75038), // [Miscellaneous] -> cl_entitylist 9 | localPlayer: BigInt(0x1e25418) // [Miscellaneous] -> LocalPlayer 10 | }, 11 | entity: { 12 | localOrigin: 0x158, // [DataMap.CBaseViewModel] -> m_localOrigin 13 | iSignifierName: 0x580, // [RecvTable.DT_BaseEntity] -> m_iSignifierName 14 | lastVisibleTime: 0x1a44 // [Miscellaneous] -> CPlayer!lastVisibleTime 15 | }, 16 | item: { 17 | highlightFunctionBits: 0x2c0, // [RecvTable.DT_HighlightSettings] -> m_highlightFunctionBits 18 | customScriptInt: 0x1628 // [RecvTable.DT_PropSurvival] -> m_customScriptInt 19 | }, 20 | player: { 21 | glowEnable: 0x3c8, // [RecvTable.DT_HighlightSettings] -> m_highlightServerContextID + 0x8 22 | glowThroughWall: 0x3d0, // [RecvTable.DT_HighlightSettings] -> m_highlightServerContextID + 0x10 23 | iTeamNum: 0x448, // [RecvTable.DT_BaseEntity] -> m_iTeamNum 24 | iName: 0x589, // [RecvTable.DT_BaseEntity] -> m_iName 25 | lifeState: 0x798, // [RecvTable.DT_Player] -> m_lifeState 26 | vecPunchWeaponAngle: 0x23f8, // [DataMap.C_Player] -> m_currentFrameLocalPlayer.m_vecPunchWeapon_Angle 27 | viewAngle: 0x24e0, // [DataMap.C_Player] -> m_ammoPoolCapacity - 0x14 28 | bleedoutState: 0x2688 // [RecvTable.DT_Player] -> m_bleedoutState 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/core/types/CString.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class CString extends app.api.Adapter { 4 | constructor(offset: number, interval: number, size: number) { 5 | super(new app.api.EntityMember(offset, interval, size)); 6 | } 7 | 8 | get value() { 9 | let result = ''; 10 | iterateBytes(this.source.buffer, x => result += x); 11 | return result; 12 | } 13 | 14 | toString() { 15 | return this.value.toString(); 16 | } 17 | } 18 | 19 | function iterateBytes(buffer: DataView, add: (value: string) => void) { 20 | for (let i = 0; i < buffer.byteLength; i++) { 21 | const charCode = buffer.getUint8(i); 22 | if (charCode === 0) break; 23 | add(String.fromCharCode(charCode)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/core/types/Float32.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class Float32 extends app.api.Adapter { 4 | constructor(offset: number, interval = 0) { 5 | super(new app.api.EntityMember(offset, interval, 4)); 6 | } 7 | 8 | get value() { 9 | return this.source.buffer.getFloat32(0, true); 10 | } 11 | 12 | set value(value: number) { 13 | if (value === this.value) return; 14 | this.source.buffer.setFloat32(0, value, true); 15 | this.source.send = true; 16 | } 17 | 18 | toString() { 19 | return this.value.toString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/core/types/Glow.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class Glow extends app.api.Adapter { 4 | constructor(offset: number, interval = 0) { 5 | super(new app.api.EntityMember(offset, interval, 4)); 6 | } 7 | 8 | get value() { 9 | const value = this.source.buffer.getUint32(0, true); 10 | return app.GlowData.fromUInt32(value); 11 | } 12 | 13 | set value(value: app.GlowData) { 14 | if (this.value.isSame(value)) return; 15 | this.source.buffer.setUint32(0, value.toUInt32(), true); 16 | this.source.send = true; 17 | } 18 | 19 | toString() { 20 | return app.serialize(this.value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/core/types/UInt32.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class UInt32 extends app.api.Adapter { 4 | constructor(offset: number, interval = 0) { 5 | super(new app.api.EntityMember(offset, interval, 4)); 6 | } 7 | 8 | get value() { 9 | return this.source.buffer.getUint32(0, true); 10 | } 11 | 12 | set value(value: number) { 13 | if (value === this.value) return; 14 | this.source.buffer.setUint32(0, value, true); 15 | this.source.send = true; 16 | } 17 | 18 | toString() { 19 | return this.value.toString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/core/types/UInt64.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class UInt64 extends app.api.Adapter { 4 | constructor(offset: number, interval = 0) { 5 | super(new app.api.EntityMember(offset, interval, 8)); 6 | } 7 | 8 | get value() { 9 | return this.source.buffer.getBigUint64(0, true); 10 | } 11 | 12 | set value(value: bigint) { 13 | if (value === this.value) return; 14 | this.source.buffer.setBigUint64(0, value, true); 15 | this.source.send = true; 16 | } 17 | 18 | toString() { 19 | return this.value.toString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/core/types/UInt8.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class UInt8 extends app.api.Adapter { 4 | constructor(offset: number, interval = 0) { 5 | super(new app.api.EntityMember(offset, interval, 1)); 6 | } 7 | 8 | get value() { 9 | return this.source.buffer.getUint8(0); 10 | } 11 | 12 | set value(value: number) { 13 | if (value === this.value) return; 14 | this.source.buffer.setUint8(0, value); 15 | this.source.send = true; 16 | } 17 | 18 | toString() { 19 | return this.value.toString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/core/types/Vector.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export class Vector extends app.api.Adapter { 4 | constructor(offset: number, interval = 0) { 5 | super(new app.api.EntityMember(offset, interval, 12)); 6 | } 7 | 8 | delta(value: app.VectorData) { 9 | if (this.value.isSame(value)) return; 10 | const {x, y, z} = value.subtract(this.value); 11 | this.source.buffer.setFloat32(0, value.x, true); 12 | this.source.buffer.setFloat32(4, value.y, true); 13 | this.source.buffer.setFloat32(8, value.z, true); 14 | this.source.deltas = [delta(0, x), delta(4, y), delta(8, z)]; 15 | this.source.send = true; 16 | } 17 | 18 | get value() { 19 | const x = this.source.buffer.getFloat32(0, true); 20 | const y = this.source.buffer.getFloat32(4, true); 21 | const z = this.source.buffer.getFloat32(8, true); 22 | return new app.VectorData(x, y, z); 23 | } 24 | 25 | set value(value: app.VectorData) { 26 | if (this.value.isSame(value)) return; 27 | this.source.buffer.setFloat32(0, value.x, true); 28 | this.source.buffer.setFloat32(4, value.y, true); 29 | this.source.buffer.setFloat32(8, value.z, true); 30 | this.source.send = true; 31 | } 32 | 33 | toString() { 34 | return app.serialize(this.value); 35 | } 36 | } 37 | 38 | function delta(offset: number, value: number) { 39 | const data = new DataView(new ArrayBuffer(4)); 40 | data.setFloat32(0, value, true); 41 | return new app.api.EntityChangeMemberDelta(offset, app.api.DeltaType.Float32, data); 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/features/Map.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class Map { 4 | private readonly context: CanvasRenderingContext2D; 5 | private readonly image = new Image(); 6 | private map?: ReturnType; 7 | private ratioX = 0; 8 | private ratioY = 0; 9 | private scaleR = 0; 10 | private scaleX = 0; 11 | private scaleY = 0; 12 | private shiftX = 0; 13 | private shiftY = 0; 14 | 15 | constructor( 16 | private readonly canvas: HTMLCanvasElement) { 17 | this.context = canvas.getContext('2d')!; 18 | } 19 | 20 | refresh(levelName: string) { 21 | this.prepare(levelName); 22 | this.update(); 23 | this.renderBackground(); 24 | } 25 | 26 | renderItems(items: Iterable, itemSet: Set) { 27 | for (const item of items) { 28 | if (!itemSet.has(item.customScriptInt.value)) continue; 29 | const position = this.calculatePosition(item.localOrigin); 30 | if (position) { 31 | this.context.beginPath(); 32 | this.context.arc(position.x, position.y, this.scaleR * 4, 0, Math.PI * 2); 33 | this.context.fillStyle = '#FFF'; 34 | this.context.fill(); 35 | } 36 | } 37 | } 38 | 39 | renderPlayers(localPlayer: app.core.Player, players: Iterable) { 40 | for (const player of players) { 41 | if (!player.isValid) continue; 42 | const position = this.calculatePosition(player.localOrigin); 43 | if (position) { 44 | this.context.beginPath(); 45 | this.context.arc(position.x, position.y, this.scaleR * 8, 0, Math.PI * 2); 46 | this.context.fillStyle = player.createColor(localPlayer); 47 | this.context.fill(); 48 | } 49 | } 50 | } 51 | 52 | private calculatePosition(localOrigin: app.core.Vector) { 53 | if (this.map) { 54 | const x = this.shiftX + (1 / this.image.width * this.scaleX) * (localOrigin.value.x - this.map.x) / this.ratioX; 55 | const y = this.shiftY + (1 / this.image.height * this.scaleY) * (localOrigin.value.y - this.map.y) / -this.ratioY; 56 | return {x, y}; 57 | } else { 58 | return; 59 | } 60 | } 61 | 62 | private prepare(levelName: string) { 63 | this.map = getDataByLevelName(levelName); 64 | this.image.src = this.map ? `images/maps/${levelName}.webp` : 'images/maps.webp'; 65 | } 66 | 67 | private renderBackground() { 68 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 69 | this.context.drawImage(this.image, 0, 0, this.image.width, this.image.height, this.shiftX, this.shiftY, this.scaleX, this.scaleY); 70 | } 71 | 72 | private update() { 73 | this.ratioX = this.map ? (this.map.y - this.map.x) / this.image.width : 0; 74 | this.ratioY = this.map ? (this.map.y - this.map.x) / this.image.height : 0; 75 | this.scaleR = Math.min(this.canvas.width / this.image.width, this.canvas.height / this.image.height); 76 | this.scaleX = this.image.width * this.scaleR; 77 | this.scaleY = this.image.height * this.scaleR; 78 | this.shiftX = (this.canvas.width - this.scaleX) / 2; 79 | this.shiftY = (this.canvas.height - this.scaleY) / 2; 80 | } 81 | } 82 | 83 | function getDataByLevelName(levelName: string) { 84 | switch (levelName) { 85 | case 'mp_rr_canyonlands_mu3': 86 | return {x: -37541, y: 43886}; 87 | case 'mp_rr_desertlands_mu3': 88 | return {x: -45056, y: 45055}; 89 | case 'mp_rr_olympus_mu2': 90 | return {x: -52024, y: 48025}; 91 | case 'mp_rr_tropic_island_mu1': 92 | return {x: -50606, y: 52139}; 93 | default: 94 | return; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/lib/features/Radar.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class Radar { 4 | private readonly context: CanvasRenderingContext2D; 5 | private centerX = 0; 6 | private centerY = 0; 7 | private outerRadius = 0; 8 | 9 | constructor( 10 | private readonly canvas: HTMLCanvasElement, 11 | private readonly maximumDistance = 200, 12 | private readonly numberOfRings = 8) { 13 | this.context = canvas.getContext('2d')!; 14 | } 15 | 16 | refresh() { 17 | this.update(); 18 | this.renderBackground(); 19 | this.renderLines(); 20 | this.renderRings(); 21 | } 22 | 23 | renderItems(localPlayer: app.core.Player, items: Iterable, itemSet: Set) { 24 | for (const item of items) { 25 | if (!itemSet.has(item.customScriptInt.value)) continue; 26 | const position = this.calculatePosition(localPlayer, item.localOrigin); 27 | if (position) { 28 | this.context.beginPath(); 29 | this.context.arc(position.x, position.y, this.outerRadius / 80, 0, Math.PI * 2); 30 | this.context.fillStyle = '#FFF'; 31 | this.context.fill(); 32 | } 33 | } 34 | } 35 | 36 | renderNpcs(localPlayer: app.core.Player, npcs: Iterable) { 37 | for (const npc of npcs) { 38 | const position = this.calculatePosition(localPlayer, npc.localOrigin); 39 | if (position) { 40 | this.context.beginPath(); 41 | this.context.arc(position.x, position.y, this.outerRadius / 40, 0, Math.PI * 2); 42 | this.context.fillStyle = npc.createColor(); 43 | this.context.fill(); 44 | } 45 | } 46 | } 47 | 48 | renderPlayers(localPlayer: app.core.Player, players: Iterable) { 49 | for (const player of players) { 50 | if (!player.isValid || player === localPlayer) continue; 51 | const position = this.calculatePosition(localPlayer, player.localOrigin); 52 | if (position) { 53 | this.context.beginPath(); 54 | this.context.arc(position.x, position.y, this.outerRadius / 40, 0, Math.PI * 2); 55 | this.context.fillStyle = player.createColor(localPlayer); 56 | this.context.fill(); 57 | } 58 | } 59 | } 60 | 61 | private calculatePosition(localPlayer: app.core.Player, origin: app.core.Vector) { 62 | const dx = (localPlayer.localOrigin.value.x - origin.value.x) * 0.0254; 63 | const dy = (localPlayer.localOrigin.value.y - origin.value.y) * 0.0254; 64 | const r = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); 65 | if (r < this.maximumDistance) { 66 | const s = this.outerRadius / this.maximumDistance; 67 | const a = Math.sign(dy) * Math.acos(dx / r) - localPlayer.viewAngle.value.y * Math.PI / 180; 68 | const x = this.centerX + Math.sin(a) * r * s; 69 | const y = this.centerY + Math.cos(a) * r * s; 70 | return {x, y}; 71 | } else { 72 | return; 73 | } 74 | } 75 | 76 | private renderBackground() { 77 | this.context.beginPath(); 78 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 79 | this.context.arc(this.centerX, this.centerY, this.outerRadius, 0, Math.PI * 2); 80 | this.context.fillStyle = '#000'; 81 | this.context.fill(); 82 | } 83 | 84 | private renderLines() { 85 | this.context.strokeStyle = '#FFF'; 86 | for (let i = 0; i < 8; i++) { 87 | const x = this.centerX + this.outerRadius * Math.cos(i * Math.PI * 0.25); 88 | const y = this.centerY + this.outerRadius * Math.sin(i * Math.PI * 0.25); 89 | this.context.beginPath(); 90 | this.context.moveTo(this.centerX, this.centerY); 91 | this.context.lineTo(x, y); 92 | this.context.stroke(); 93 | } 94 | } 95 | 96 | private renderRings() { 97 | this.context.strokeStyle = '#FFF'; 98 | for (let i = 1; i <= this.numberOfRings; i++) { 99 | this.context.beginPath(); 100 | this.context.arc(this.centerX, this.centerY, this.outerRadius * i / this.numberOfRings, 0, Math.PI * 2); 101 | this.context.stroke(); 102 | } 103 | } 104 | 105 | private update() { 106 | this.centerX = this.canvas.width / 2; 107 | this.centerY = this.canvas.height / 2; 108 | this.outerRadius = (this.canvas.width > this.canvas.height ? this.canvas.height : this.canvas.width) / 2; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/lib/features/Recoil.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class Recoil { 4 | private vecPunchWeaponAngle = app.core.VectorData.none; 5 | 6 | update(buttonList: app.core.ButtonList, localPlayer: app.core.Player, options: IRecoilOptions) { 7 | if (localPlayer.viewAngle.source.syncId) { 8 | /* We sent an update, but did not receive confirmation! */ 9 | } else if (!buttonList.inAttack.value) { 10 | this.vecPunchWeaponAngle = app.core.VectorData.none; 11 | } else { 12 | const vecPunchWeaponAngle = localPlayer.vecPunchWeaponAngle.value; 13 | const viewAngle = localPlayer.viewAngle.value; 14 | if (Math.abs(vecPunchWeaponAngle.x) >= 0.5 || Math.abs(vecPunchWeaponAngle.y) >= 0.5 || Math.abs(vecPunchWeaponAngle.z) >= 0.5) { 15 | const x = viewAngle.x + (this.vecPunchWeaponAngle.x - vecPunchWeaponAngle.x) * options.percentage; 16 | const y = viewAngle.y + (this.vecPunchWeaponAngle.y - vecPunchWeaponAngle.y) * options.percentage; 17 | localPlayer.viewAngle.delta(new app.core.VectorData(x, y, viewAngle.z)); 18 | this.vecPunchWeaponAngle = vecPunchWeaponAngle; 19 | } 20 | } 21 | } 22 | } 23 | 24 | export type IRecoilOptions = { 25 | percentage: number // 0 <-> 1 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/features/Sense.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class Sense { 4 | constructor( 5 | private readonly itemDefault = new app.core.GlowData(0, 110, 225, 25, true, false), 6 | private readonly itemHighlight = new app.core.GlowData(137, 108, 64, 0, true, false), 7 | private readonly maximumDistance = 200) {} 8 | 9 | resetItems(_: app.core.Player, items: Iterable) { 10 | for (const item of items) { 11 | if (item.highlightFunctionBits.value.isSame(this.itemHighlight)) { 12 | item.highlightFunctionBits.value = this.itemDefault; 13 | } 14 | } 15 | } 16 | 17 | resetPlayers(localPlayer: app.core.Player, players: Iterable) { 18 | for (const player of players) { 19 | if (player.isValid && !player.isSameTeam(localPlayer)) { 20 | if (this.inRange(localPlayer, player.localOrigin)) { 21 | /* This could be a Bloodhound scan! */ 22 | } else if (player.glowEnable.value === 7) { 23 | player.glowEnable.value = 2; 24 | player.glowThroughWalls.value = 5; 25 | } 26 | } 27 | } 28 | } 29 | 30 | updateItems(localPlayer: app.core.Player, items: Iterable, itemSet: Set) { 31 | for (const item of items) { 32 | if (itemSet.has(item.customScriptInt.value) && this.inRange(localPlayer, item.localOrigin)) { 33 | item.highlightFunctionBits.value = this.itemHighlight; 34 | } else if (item.highlightFunctionBits.value.isSame(this.itemHighlight)) { 35 | item.highlightFunctionBits.value = this.itemDefault; 36 | } 37 | } 38 | } 39 | 40 | updatePlayers(localPlayer: app.core.Player, players: Iterable) { 41 | for (const player of players) { 42 | if (player.isValid && !player.isSameTeam(localPlayer)) { 43 | if (this.inRange(localPlayer, player.localOrigin)) { 44 | player.glowEnable.value = 7; 45 | player.glowThroughWalls.value = 2; 46 | } else if (player.glowEnable.value === 7) { 47 | player.glowEnable.value = 2; 48 | player.glowThroughWalls.value = 5; 49 | } 50 | } 51 | } 52 | } 53 | 54 | private inRange(localPlayer: app.core.Player, origin: app.core.Vector) { 55 | const dx = (localPlayer.localOrigin.value.x - origin.value.x) * 0.0254; 56 | const dy = (localPlayer.localOrigin.value.y - origin.value.y) * 0.0254; 57 | return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) < this.maximumDistance; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/features/index.ts: -------------------------------------------------------------------------------- 1 | export {core} from '..'; 2 | export * from './Map'; 3 | export * from './Radar'; 4 | export * from './Recoil'; 5 | export * from './Sense'; 6 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * as api from './api'; 2 | export * as core from './core'; 3 | export * as features from './features'; 4 | export * as items from './items'; 5 | -------------------------------------------------------------------------------- /src/lib/items/index.ts: -------------------------------------------------------------------------------- 1 | export * from './list'; 2 | export * from './types/All'; 3 | export * from './types/Ammo'; 4 | export * from './types/AmmoType'; 5 | export * from './types/Attachment'; 6 | export * from './types/AttachmentType'; 7 | export * from './types/Gear'; 8 | export * from './types/GearType'; 9 | export * from './types/Grenade'; 10 | export * from './types/Regen'; 11 | export * from './types/Weapon'; 12 | export * from './types/WeaponType'; 13 | -------------------------------------------------------------------------------- /src/lib/items/list.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | // TODO: Missing Heat Shield & Mobile Respawn Beacon 4 | export const list: Array = [{ 5 | itemId: 1, 6 | itemType: 'Weapon', 7 | name: 'Kraber .50-Cal Sniper', 8 | ammoType: 'Special', 9 | weaponType: 'Sniper', 10 | slots: [] 11 | }, { 12 | itemId: 2, 13 | itemType: 'Weapon', 14 | name: 'Mastiff Shotgun', 15 | ammoType: 'Special', 16 | weaponType: 'Shotgun', 17 | slots: ['Optics'] 18 | }, { 19 | itemId: 7, 20 | itemType: 'Weapon', 21 | name: 'L-STAR EMG', 22 | ammoType: 'Energy', 23 | weaponType: 'Marksman', 24 | slots: ['BarrelStabilizer', 'ExtendedEnergyMag', 'Optics', 'StandardStock'] 25 | }, { 26 | itemId: 12, 27 | itemType: 'Weapon', 28 | name: 'HAVOC Rifle', 29 | ammoType: 'Energy', 30 | weaponType: 'AR', 31 | slots: ['ExtendedEnergyMag', 'Optics', 'StandardStock', 'Turbocharger'] 32 | }, { 33 | itemId: 17, 34 | itemType: 'Weapon', 35 | name: 'Devotion LMG', 36 | ammoType: 'Energy', 37 | weaponType: 'LMG', 38 | slots: ['BarrelStabilizer', 'ExtendedEnergyMag', 'Optics', 'StandardStock', 'Turbocharger'] 39 | }, { 40 | itemId: 22, 41 | itemType: 'Weapon', 42 | name: 'Triple Take', 43 | ammoType: 'Energy', 44 | weaponType: 'Marksman', 45 | slots: ['ExtendedEnergyMag', 'Optics', 'SniperStock', 'KineticFeeder'] 46 | }, { 47 | itemId: 27, 48 | itemType: 'Weapon', 49 | name: 'VK-47 Flatline', 50 | ammoType: 'Heavy', 51 | weaponType: 'AR', 52 | slots: ['ExtendedHeavyMag', 'Optics', 'StandardStock'] 53 | }, { 54 | itemId: 32, 55 | itemType: 'Weapon', 56 | name: 'Hemlok Burst AR', 57 | ammoType: 'Heavy', 58 | weaponType: 'AR', 59 | slots: ['BarrelStabilizer', 'ExtendedHeavyMag', 'Optics', 'StandardStock', 'BoostedLoader'] 60 | }, { 61 | itemId: 37, 62 | itemType: 'Weapon', 63 | name: 'G7 Scout', 64 | ammoType: 'Light', 65 | weaponType: 'Marksman', 66 | slots: ['BarrelStabilizer', 'ExtendedLightMag', 'Optics', 'SniperStock', 'DoubleTapTrigger'] 67 | }, { 68 | itemId: 42, 69 | itemType: 'Weapon', 70 | name: 'Alternator SMG', 71 | ammoType: 'Light', 72 | weaponType: 'SMG', 73 | slots: ['LaserLight', 'ExtendedLightMag', 'Optics', 'StandardStock'] 74 | }, { 75 | itemId: 47, 76 | itemType: 'Weapon', 77 | name: 'R-99 SMG', 78 | ammoType: 'Light', 79 | weaponType: 'SMG', 80 | slots: ['LaserLight', 'ExtendedLightMag', 'Optics', 'StandardStock'] 81 | }, { 82 | itemId: 52, 83 | itemType: 'Weapon', 84 | name: 'Prowler Burst PDW', 85 | ammoType: 'Heavy', 86 | weaponType: 'SMG', 87 | slots: ['LaserLight', 'ExtendedHeavyMag', 'Optics', 'StandardStock'] 88 | }, { 89 | itemId: 57, 90 | itemType: 'Weapon', 91 | name: 'Volt SMG', 92 | ammoType: 'Energy', 93 | weaponType: 'SMG', 94 | slots: ['LaserLight', 'ExtendedEnergyMag', 'Optics', 'StandardStock'] 95 | }, { 96 | itemId: 62, 97 | itemType: 'Weapon', 98 | name: 'Longbow DMR', 99 | ammoType: 'Sniper', 100 | weaponType: 'Sniper', 101 | slots: ['BarrelStabilizer', 'ExtendedSniperMag', 'Optics', 'SniperStock', 'SkullpiercerRifling'] 102 | }, { 103 | itemId: 67, 104 | itemType: 'Weapon', 105 | name: 'Charge Rifle', 106 | ammoType: 'Sniper', 107 | weaponType: 'Sniper', 108 | slots: ['Optics', 'SniperStock'] 109 | }, { 110 | itemId: 72, 111 | itemType: 'Weapon', 112 | name: 'M600 Spitfire', 113 | ammoType: 'Light', 114 | weaponType: 'LMG', 115 | slots: ['ExtendedLightMag', 'Optics', 'StandardStock'] 116 | }, { 117 | itemId: 77, 118 | itemType: 'Weapon', 119 | name: 'R-301 Carbine', 120 | ammoType: 'Light', 121 | weaponType: 'AR', 122 | slots: ['BarrelStabilizer', 'ExtendedLightMag', 'Optics', 'StandardStock'] 123 | }, { 124 | itemId: 82, 125 | itemType: 'Weapon', 126 | name: 'EVA-8 Auto', 127 | ammoType: 'Shotgun', 128 | weaponType: 'Shotgun', 129 | slots: ['ShotgunBolt', 'Optics', 'StandardStock', 'DoubleTapTrigger'] 130 | }, { 131 | itemId: 87, 132 | itemType: 'Weapon', 133 | name: 'Peacekeeper', 134 | ammoType: 'Shotgun', 135 | weaponType: 'Shotgun', 136 | slots: ['ShotgunBolt', 'Optics', 'KineticFeeder'] 137 | }, { 138 | itemId: 92, 139 | itemType: 'Weapon', 140 | name: 'Mozambique Shotgun', 141 | ammoType: 'Shotgun', 142 | weaponType: 'Shotgun', 143 | slots: ['ShotgunBolt', 'Optics', 'HammerpointRounds'] 144 | }, { 145 | itemId: 97, 146 | itemType: 'Weapon', 147 | name: 'Wingman', 148 | ammoType: 'Sniper', 149 | weaponType: 'Pistol', 150 | slots: ['ExtendedSniperMag', 'Optics', 'BoostedLoader'] 151 | }, { 152 | itemId: 102, 153 | itemType: 'Weapon', 154 | name: 'P2020', 155 | ammoType: 'Light', 156 | weaponType: 'Pistol', 157 | slots: ['LaserLight', 'ExtendedLightMag', 'Optics', 'HammerpointRounds'] 158 | }, { 159 | itemId: 107, 160 | itemType: 'Weapon', 161 | name: 'RE-45 Auto', 162 | ammoType: 'Light', 163 | weaponType: 'Pistol', 164 | slots: ['LaserLight', 'ExtendedLightMag', 'Optics', 'HammerpointRounds'] 165 | }, { 166 | itemId: 112, 167 | itemType: 'Weapon', 168 | name: 'Sentinel', 169 | ammoType: 'Sniper', 170 | weaponType: 'Sniper', 171 | slots: ['ExtendedSniperMag', 'Optics', 'SniperStock'] 172 | }, { 173 | itemId: 117, 174 | itemType: 'Weapon', 175 | name: 'Bocek Compound Bow', 176 | ammoType: 'Special', 177 | weaponType: 'Marksman', 178 | slots: ['Optics'] 179 | }, { 180 | itemId: 118, 181 | itemType: 'Weapon', 182 | name: '30-30 Repeater', 183 | ammoType: 'Heavy', 184 | weaponType: 'Marksman', 185 | slots: ['ExtendedHeavyMag', 'Optics', 'SniperStock', 'SkullpiercerRifling'] 186 | }, { 187 | itemId: 124, 188 | itemType: 'Ammo', 189 | name: 'Light Rounds', 190 | ammoType: 'Light' 191 | }, { 192 | itemId: 125, 193 | itemType: 'Ammo', 194 | name: 'Energy Ammo', 195 | ammoType: 'Energy' 196 | }, { 197 | itemId: 126, 198 | itemType: 'Ammo', 199 | name: 'Shotgun Shells', 200 | ammoType: 'Shotgun' 201 | }, { 202 | itemId: 127, 203 | itemType: 'Ammo', 204 | name: 'Heavy Rounds', 205 | ammoType: 'Heavy' 206 | }, { 207 | itemId: 128, 208 | itemType: 'Ammo', 209 | name: 'Sniper Ammo', 210 | ammoType: 'Sniper' 211 | }, { 212 | itemId: 130, 213 | itemType: 'Weapon', 214 | name: 'Rampage LMG', 215 | ammoType: 'Special', 216 | weaponType: 'LMG', 217 | slots: ['Optics'] 218 | }, { 219 | itemId: 131, 220 | itemType: 'Weapon', 221 | name: 'C.A.R. SMG', 222 | ammoType: 'Heavy', // Or Light 223 | weaponType: 'SMG', 224 | slots: ['ExtendedHeavyMag', 'Optics', 'StandardStock'] 225 | }, { 226 | itemId: 160, 227 | itemType: 'Regen', 228 | name: 'Ultimate Accelerant' 229 | }, { 230 | itemId: 161, 231 | itemType: 'Regen', 232 | name: 'Phoenix Kit' 233 | }, { 234 | itemId: 162, 235 | itemType: 'Regen', 236 | name: 'Med Kit' 237 | }, { 238 | itemId: 163, 239 | itemType: 'Regen', 240 | name: 'Syringe' 241 | }, { 242 | itemId: 164, 243 | itemType: 'Regen', 244 | name: 'Shield Battery' 245 | }, { 246 | itemId: 165, 247 | itemType: 'Regen', 248 | name: 'Shield Cell' 249 | }, { 250 | itemId: 166, 251 | itemType: 'Gear', 252 | name: 'Helmet (Level 1 / White)', 253 | gearType: 'Helmet', 254 | level: 1 255 | }, { 256 | itemId: 167, 257 | itemType: 'Gear', 258 | name: 'Helmet (Level 2 / Blue)', 259 | gearType: 'Helmet', 260 | level: 2 261 | }, { 262 | itemId: 168, 263 | itemType: 'Gear', 264 | name: 'Helmet (Level 3 / Purple)', 265 | gearType: 'Helmet', 266 | level: 3 267 | }, { 268 | itemId: 169, 269 | itemType: 'Gear', 270 | name: 'Helmet (Level 4 / Gold)', 271 | gearType: 'Helmet', 272 | level: 4 273 | }, { 274 | itemId: 170, 275 | itemType: 'Gear', 276 | name: 'Body Shield (Level 1 / White)', 277 | gearType: 'BodyShield', 278 | level: 1 279 | }, { 280 | itemId: 171, 281 | itemType: 'Gear', 282 | name: 'Body Shield (Level 2 / Blue)', 283 | gearType: 'BodyShield', 284 | level: 2 285 | }, { 286 | itemId: 172, 287 | itemType: 'Gear', 288 | name: 'Body Shield (Level 3 / Purple)', 289 | gearType: 'BodyShield', 290 | level: 3 291 | }, { 292 | itemId: 173, 293 | itemType: 'Gear', 294 | name: 'Body Shield (Level 4 / Gold)', 295 | gearType: 'BodyShield', 296 | level: 4 297 | }, { 298 | itemId: 175, 299 | itemType: 'Gear', 300 | name: 'Evo Shield (Level 1 / White)', 301 | gearType: 'EvoShield', 302 | level: 1 303 | }, { 304 | itemId: 176, 305 | itemType: 'Gear', 306 | name: 'Evo Shield (Level 2 / Blue)', 307 | gearType: 'EvoShield', 308 | level: 2 309 | }, { 310 | itemId: 177, 311 | itemType: 'Gear', 312 | name: 'Evo Shield (Level 3 / Purple)', 313 | gearType: 'EvoShield', 314 | level: 3 315 | }, { 316 | itemId: 178, 317 | itemType: 'Gear', 318 | name: 'Evo Shield (Level 4 / Red)', 319 | gearType: 'EvoShield', 320 | level: 4 321 | }, { 322 | itemId: 180, 323 | itemType: 'Gear', 324 | name: 'Knockdown Shield (Level 1 / White)', 325 | gearType: 'KnockdownShield', 326 | level: 1 327 | }, { 328 | itemId: 181, 329 | itemType: 'Gear', 330 | name: 'Knockdown Shield (Level 2 / Blue)', 331 | gearType: 'KnockdownShield', 332 | level: 2 333 | }, { 334 | itemId: 182, 335 | itemType: 'Gear', 336 | name: 'Knockdown Shield (Level 3 / Purple)', 337 | gearType: 'KnockdownShield', 338 | level: 3 339 | }, { 340 | itemId: 183, 341 | itemType: 'Gear', 342 | name: 'Knockdown Shield (Level 4 / Gold)', 343 | gearType: 'KnockdownShield', 344 | level: 4 345 | }, { 346 | itemId: 184, 347 | itemType: 'Gear', 348 | name: 'Backpack (Level 1 / White)', 349 | gearType: 'Backpack', 350 | level: 1 351 | }, { 352 | itemId: 185, 353 | itemType: 'Gear', 354 | name: 'Backpack (Level 2 / Blue)', 355 | gearType: 'Backpack', 356 | level: 2 357 | }, { 358 | itemId: 186, 359 | itemType: 'Gear', 360 | name: 'Backpack (Level 3 / Purple)', 361 | gearType: 'Backpack', 362 | level: 3 363 | }, { 364 | itemId: 187, 365 | itemType: 'Gear', 366 | name: 'Backpack (Level 4 / Gold)', 367 | gearType: 'Backpack', 368 | level: 4 369 | }, { 370 | itemId: 188, 371 | itemType: 'Grenade', 372 | name: 'Thermite Grenade' 373 | }, { 374 | itemId: 189, 375 | itemType: 'Grenade', 376 | name: 'Frag Grenade' 377 | }, { 378 | itemId: 190, 379 | itemType: 'Grenade', 380 | name: 'Arc Star' 381 | }, { 382 | itemId: 191, 383 | itemType: 'Attachment', 384 | name: '1x HCOG "Classic"', 385 | attachmentType: 'Optics', 386 | level: 1 387 | }, { 388 | itemId: 192, 389 | itemType: 'Attachment', 390 | name: '2x HCOG "Bruiser"', 391 | attachmentType: 'Optics', 392 | level: 2 393 | }, { 394 | itemId: 193, 395 | itemType: 'Attachment', 396 | name: '1x Holo', 397 | attachmentType: 'Optics', 398 | level: 1 399 | }, { 400 | itemId: 194, 401 | itemType: 'Attachment', 402 | name: '1x-2x Variable Holo', 403 | attachmentType: 'Optics', 404 | level: 2 405 | }, { 406 | itemId: 195, 407 | itemType: 'Attachment', 408 | name: '1x Digital Threat', 409 | attachmentType: 'Optics', 410 | weaponTypes: ['Pistol', 'Shotgun', 'SMG'], 411 | level: 4 412 | }, { 413 | itemId: 196, 414 | itemType: 'Attachment', 415 | name: '3x HCOG "Ranger"', 416 | attachmentType: 'Optics', 417 | weaponTypes: ['AR', 'LMG', 'Marksman', 'Sniper'], // Except Bocek 418 | level: 3 419 | }, { 420 | itemId: 197, 421 | itemType: 'Attachment', 422 | name: '2x-4x Variable AOG', 423 | attachmentType: 'Optics', 424 | weaponTypes: ['AR', 'LMG', 'Marksman', 'Sniper'], 425 | level: 3 426 | }, { 427 | itemId: 198, 428 | itemType: 'Attachment', 429 | name: '6x Sniper', 430 | attachmentType: 'Optics', 431 | weaponTypes: ['Sniper'], 432 | level: 2 433 | }, { 434 | itemId: 199, 435 | itemType: 'Attachment', 436 | name: '4x-8x Variable Sniper', 437 | attachmentType: 'Optics', 438 | weaponTypes: ['Sniper'], 439 | level: 3 440 | }, { 441 | itemId: 200, 442 | itemType: 'Attachment', 443 | name: '4x-10x Digital Sniper Threat', 444 | attachmentType: 'Optics', 445 | weaponTypes: ['Sniper'], 446 | level: 4 447 | }, { 448 | itemId: 201, 449 | itemType: 'Attachment', 450 | name: 'Barrel Stabilizer (Level 1 / White)', 451 | attachmentType: 'BarrelStabilizer', 452 | level: 1 453 | }, { 454 | itemId: 202, 455 | itemType: 'Attachment', 456 | name: 'Barrel Stabilizer (Level 2 / Blue)', 457 | attachmentType: 'BarrelStabilizer', 458 | level: 2 459 | }, { 460 | itemId: 203, 461 | itemType: 'Attachment', 462 | name: 'Barrel Stabilizer (Level 3 / Purple)', 463 | attachmentType: 'BarrelStabilizer', 464 | level: 3 465 | }, { 466 | itemId: 205, 467 | itemType: 'Attachment', 468 | name: 'Laser Sight (Level 1 / White)', 469 | attachmentType: 'LaserLight', 470 | level: 1 471 | }, { 472 | itemId: 206, 473 | itemType: 'Attachment', 474 | name: 'Laser Sight (Level 2 / Blue)', 475 | attachmentType: 'LaserLight', 476 | level: 2 477 | }, { 478 | itemId: 207, 479 | itemType: 'Attachment', 480 | name: 'Laser Sight (Level 3 / Purple)', 481 | attachmentType: 'LaserLight', 482 | level: 3 483 | }, { 484 | itemId: 208, 485 | itemType: 'Attachment', 486 | name: 'Extended Light Mag (Level 1 / White)', 487 | attachmentType: 'ExtendedLightMag', 488 | level: 1 489 | }, { 490 | itemId: 209, 491 | itemType: 'Attachment', 492 | name: 'Extended Light Mag (Level 2 / Blue)', 493 | attachmentType: 'ExtendedLightMag', 494 | level: 2 495 | }, { 496 | itemId: 210, 497 | itemType: 'Attachment', 498 | name: 'Extended Light Mag (Level 3 / Purple)', 499 | attachmentType: 'ExtendedLightMag', 500 | level: 3 501 | }, { 502 | itemId: 211, 503 | itemType: 'Attachment', 504 | name: 'Extended Light Mag (Level 4 / Gold)', 505 | attachmentType: 'ExtendedLightMag', 506 | level: 4 507 | }, { 508 | itemId: 212, 509 | itemType: 'Attachment', 510 | name: 'Extended Heavy Mag (Level 1 / White)', 511 | attachmentType: 'ExtendedHeavyMag', 512 | level: 1 513 | }, { 514 | itemId: 213, 515 | itemType: 'Attachment', 516 | name: 'Extended Heavy Mag (Level 2 / Blue)', 517 | attachmentType: 'ExtendedHeavyMag', 518 | level: 2 519 | }, { 520 | itemId: 214, 521 | itemType: 'Attachment', 522 | name: 'Extended Heavy Mag (Level 3 / Purple)', 523 | attachmentType: 'ExtendedHeavyMag', 524 | level: 3 525 | }, { 526 | itemId: 215, 527 | itemType: 'Attachment', 528 | name: 'Extended Heavy Mag (Level 4 / Gold)', 529 | attachmentType: 'ExtendedHeavyMag', 530 | level: 4 531 | }, { 532 | itemId: 216, 533 | itemType: 'Attachment', 534 | name: 'Extended Energy Mag (Level 1 / White)', 535 | attachmentType: 'ExtendedEnergyMag', 536 | level: 1 537 | }, { 538 | itemId: 217, 539 | itemType: 'Attachment', 540 | name: 'Extended Energy Mag (Level 2 / Blue)', 541 | attachmentType: 'ExtendedEnergyMag', 542 | level: 2 543 | }, { 544 | itemId: 218, 545 | itemType: 'Attachment', 546 | name: 'Extended Energy Mag (Level 3 / Purple)', 547 | attachmentType: 'ExtendedEnergyMag', 548 | level: 3 549 | }, { 550 | itemId: 219, 551 | itemType: 'Attachment', 552 | name: 'Extended Energy Mag (Level 4 / Gold)', 553 | attachmentType: 'ExtendedEnergyMag', 554 | level: 4 555 | }, { 556 | itemId: 220, 557 | itemType: 'Attachment', 558 | name: 'Extended Sniper Mag (Level 1 / White)', 559 | attachmentType: 'ExtendedSniperMag', 560 | level: 1 561 | }, { 562 | itemId: 221, 563 | itemType: 'Attachment', 564 | name: 'Extended Sniper Mag (Level 2 / Blue)', 565 | attachmentType: 'ExtendedSniperMag', 566 | level: 2 567 | }, { 568 | itemId: 222, 569 | itemType: 'Attachment', 570 | name: 'Extended Sniper Mag (Level 3 / Purple)', 571 | attachmentType: 'ExtendedSniperMag', 572 | level: 3 573 | }, { 574 | itemId: 223, 575 | itemType: 'Attachment', 576 | name: 'Extended Sniper Mag (Level 4 / Gold)', 577 | attachmentType: 'ExtendedSniperMag', 578 | level: 4 579 | }, { 580 | itemId: 224, 581 | itemType: 'Attachment', 582 | name: 'Shotgun Bolt (Level 1 / White)', 583 | attachmentType: 'ShotgunBolt', 584 | level: 1 585 | }, { 586 | itemId: 225, 587 | itemType: 'Attachment', 588 | name: 'Shotgun Bolt (Level 2 / Blue)', 589 | attachmentType: 'ShotgunBolt', 590 | level: 2 591 | }, { 592 | itemId: 226, 593 | itemType: 'Attachment', 594 | name: 'Shotgun Bolt (Level 3 / Purple)', 595 | attachmentType: 'ShotgunBolt', 596 | level: 3 597 | }, { 598 | itemId: 227, 599 | itemType: 'Attachment', 600 | name: 'Standard Stock (Level 1 / White)', 601 | attachmentType: 'StandardStock', 602 | level: 1 603 | }, { 604 | itemId: 228, 605 | itemType: 'Attachment', 606 | name: 'Standard Stock (Level 2 / Blue)', 607 | attachmentType: 'StandardStock', 608 | level: 2 609 | }, { 610 | itemId: 229, 611 | itemType: 'Attachment', 612 | name: 'Standard Stock (Level 3 / Purple)', 613 | attachmentType: 'StandardStock', 614 | level: 3 615 | }, { 616 | itemId: 230, 617 | itemType: 'Attachment', 618 | name: 'Sniper Stock (Level 1 / White)', 619 | attachmentType: 'SniperStock', 620 | level: 1 621 | }, { 622 | itemId: 231, 623 | itemType: 'Attachment', 624 | name: 'Sniper Stock (Level 2 / Blue)', 625 | attachmentType: 'SniperStock', 626 | level: 2 627 | }, { 628 | itemId: 232, 629 | itemType: 'Attachment', 630 | name: 'Sniper Stock (Level 3 / Purple)', 631 | attachmentType: 'SniperStock', 632 | level: 3 633 | }, { 634 | itemId: 233, 635 | itemType: 'Attachment', 636 | name: 'Turbocharger', 637 | attachmentType: 'Turbocharger', 638 | level: 4 639 | }, { 640 | itemId: 235, 641 | itemType: 'Attachment', 642 | name: 'Skullpiercer Rifling', 643 | attachmentType: 'SkullpiercerRifling', 644 | level: 4 645 | }, { 646 | itemId: 237, 647 | itemType: 'Attachment', 648 | name: 'Hammerpoint Rounds', 649 | attachmentType: 'HammerpointRounds', 650 | level: 3 651 | }, { 652 | itemId: 239, 653 | itemType: 'Attachment', 654 | name: 'Double Tap Trigger', 655 | attachmentType: 'DoubleTapTrigger', 656 | level: 3 657 | }, { 658 | itemId: 246, 659 | itemType: 'Attachment', 660 | name: 'Kinetic Feeder', 661 | attachmentType: 'KineticFeeder', 662 | level: 3 663 | }, { 664 | itemId: 247, 665 | itemType: 'Attachment', 666 | name: 'Boosted Loader', 667 | attachmentType: 'BoostedLoader', 668 | level: 3 669 | }]; 670 | -------------------------------------------------------------------------------- /src/lib/items/types/All.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export type All = 4 | app.Ammo | 5 | app.Attachment | 6 | app.Gear | 7 | app.Grenade | 8 | app.Regen | 9 | app.Weapon; 10 | -------------------------------------------------------------------------------- /src/lib/items/types/Ammo.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export type Ammo = { 4 | itemId: number; 5 | itemType: 'Ammo'; 6 | name: string; 7 | ammoType: app.AmmoType; 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/items/types/AmmoType.ts: -------------------------------------------------------------------------------- 1 | export type AmmoType = 2 | 'Energy' | 3 | 'Heavy' | 4 | 'Light' | 5 | 'Shotgun' | 6 | 'Sniper' | 7 | 'Special'; 8 | -------------------------------------------------------------------------------- /src/lib/items/types/Attachment.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export type Attachment = { 4 | itemId: number; 5 | itemType: 'Attachment'; 6 | name: string; 7 | attachmentType: app.AttachmentType, 8 | weaponTypes?: Array; 9 | level: 1 | 2 | 3 | 4; 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/items/types/AttachmentType.ts: -------------------------------------------------------------------------------- 1 | export type AttachmentType = 2 | 'BarrelStabilizer' | 3 | 'ExtendedEnergyMag' | 4 | 'ExtendedHeavyMag' | 5 | 'ExtendedLightMag' | 6 | 'ExtendedSniperMag' | 7 | 'LaserLight' | 8 | 'Optics' | 9 | 'ShotgunBolt' | 10 | 'StandardStock' | 11 | 'SniperStock' | 12 | 'BoostedLoader' | 13 | 'DoubleTapTrigger' | 14 | 'HammerpointRounds' | 15 | 'KineticFeeder' | 16 | 'SkullpiercerRifling' | 17 | 'Turbocharger'; 18 | -------------------------------------------------------------------------------- /src/lib/items/types/Gear.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export type Gear = { 4 | itemId: number; 5 | itemType: 'Gear'; 6 | name: string; 7 | gearType: app.GearType; 8 | level: 1 | 2 | 3 | 4; 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/items/types/GearType.ts: -------------------------------------------------------------------------------- 1 | export type GearType = 2 | 'Backpack' | 3 | 'BodyShield' | 4 | 'EvoShield' | 5 | 'Helmet' | 6 | 'KnockdownShield'; 7 | -------------------------------------------------------------------------------- /src/lib/items/types/Grenade.ts: -------------------------------------------------------------------------------- 1 | export type Grenade = { 2 | itemId: number; 3 | itemType: 'Grenade'; 4 | name: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/items/types/Regen.ts: -------------------------------------------------------------------------------- 1 | export type Regen = { 2 | itemId: number; 3 | itemType: 'Regen'; 4 | name: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/items/types/Weapon.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | 3 | export type Weapon = { 4 | itemId: number; 5 | itemType: 'Weapon'; 6 | name: string; 7 | ammoType: app.AmmoType; 8 | weaponType: app.WeaponType; 9 | slots: Array; 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/items/types/WeaponType.ts: -------------------------------------------------------------------------------- 1 | export type WeaponType = 2 | 'AR' | 3 | 'LMG' | 4 | 'Marksman' | 5 | 'Pistol' | 6 | 'SMG' | 7 | 'Sniper' | 8 | 'Shotgun'; 9 | -------------------------------------------------------------------------------- /src/ui/MainView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ui from 'ui'; 3 | import Close from '@mui/icons-material/Close'; 4 | import Settings from '@mui/icons-material/Settings'; 5 | 6 | export const MainView = ui.createView<{vm: ui.MainViewModel}>(({vm}) => ( 7 | 8 | {vm.hasError && 9 | 10 | {vm.errorMessage} 11 | 12 | vm.connectAsync()}> 13 | {ui.language.reconnect} 14 | 15 | } 16 | {vm.isLoading && 17 | 18 | } 19 | {!vm.hasError && !vm.isLoading && 20 | {vm.showSettings 21 | ? 22 | : } 23 | vm.toggleSettings()}> 24 | {vm.showSettings ? : } 25 | 26 | } 27 | 28 | )); 29 | 30 | const styles = { 31 | container: { 32 | height: '100vh', 33 | overflowX: 'hidden', 34 | overflowY: 'scroll', 35 | userSelect: 'none' 36 | }, 37 | error: { 38 | textAlign: 'center', 39 | width: '100%', 40 | position: 'fixed', 41 | left: '50%', 42 | top: '50%', 43 | transform: 'translate(-50%, -50%)' 44 | }, 45 | progress: { 46 | position: 'fixed', 47 | left: 'calc(50% - 20px)', 48 | top: 'calc(50% - 20px)' 49 | }, 50 | toggle: { 51 | position: 'fixed', 52 | right: '12px', 53 | top: '12px' 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/ui/MainViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as app from 'lib'; 2 | import * as mobx from 'mobx'; 3 | import * as ui from 'ui'; 4 | 5 | export class MainViewModel { 6 | private constructor( 7 | private readonly renderFrame: (core: app.core.Core, vm: ui.MainViewModel) => void) { 8 | mobx.makeObservable(this); 9 | } 10 | 11 | static create(renderFrame: (core: app.core.Core, vm: ui.MainViewModel) => void) { 12 | const vm = new MainViewModel(renderFrame); 13 | vm.connectAsync(); 14 | return vm; 15 | } 16 | 17 | @mobx.action 18 | async connectAsync() { 19 | try { 20 | this.isLoading = true; 21 | const server = new app.api.Server(); 22 | const version = await server.versionAsync(); 23 | if (!version) { 24 | this.errorMessage = ui.language.errorDriver; 25 | } else if (version !== app.api.VERSION) { 26 | this.errorMessage = ui.language.errorVersion; 27 | } else if (!await this.startAsync(server)) { 28 | this.errorMessage = ui.language.errorProcess; 29 | } else { 30 | this.errorMessage = ''; 31 | } 32 | } catch (err) { 33 | this.disconnect(err); 34 | } finally { 35 | this.isLoading = false; 36 | } 37 | } 38 | 39 | @mobx.action 40 | toggleSettings() { 41 | this.showSettings = !this.showSettings; 42 | } 43 | 44 | @mobx.computed 45 | get hasError() { 46 | return Boolean(this.errorMessage); 47 | } 48 | 49 | @mobx.observable 50 | isLoading = true; 51 | 52 | @mobx.observable 53 | errorMessage = ''; 54 | 55 | @mobx.observable 56 | showSettings = false; 57 | 58 | @mobx.observable 59 | readonly settings = new ui.settings.MainViewModel(); 60 | 61 | @mobx.action 62 | private disconnect(reason: unknown) { 63 | this.errorMessage = ui.language.error; 64 | console.error(reason); 65 | } 66 | 67 | @mobx.action 68 | private async startAsync(server: app.api.Server) { 69 | const core = await app.core.Core 70 | .createAsync(server) 71 | .catch(() => {}); 72 | if (core) { 73 | core.runAsync(() => this.renderFrame(core, this)).catch(this.disconnect.bind(this)); 74 | return true; 75 | } else { 76 | return false; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ui/classes/BoolStorage.ts: -------------------------------------------------------------------------------- 1 | import * as mobx from 'mobx'; 2 | 3 | export class BoolStorage { 4 | constructor(private readonly key: string, defaultValue: boolean) { 5 | const value = localStorage.getItem(key); 6 | this.value = value ? /^true$/i.test(value) : defaultValue; 7 | mobx.makeObservable(this); 8 | } 9 | 10 | @mobx.action 11 | change(value: boolean) { 12 | this.value = value; 13 | localStorage.setItem(this.key, String(this.value)); 14 | } 15 | 16 | @mobx.observable 17 | value: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/classes/NumberStorage.ts: -------------------------------------------------------------------------------- 1 | import * as mobx from 'mobx'; 2 | 3 | export class NumberStorage { 4 | constructor(private readonly key: string, defaultValue: number) { 5 | const value = localStorage.getItem(key); 6 | this.value = value ? Number(value) : defaultValue; 7 | mobx.makeObservable(this); 8 | } 9 | 10 | @mobx.action 11 | change(value: number) { 12 | this.value = value; 13 | localStorage.setItem(this.key, String(this.value)); 14 | } 15 | 16 | @mobx.observable 17 | value: number; 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/classes/StringStorage.ts: -------------------------------------------------------------------------------- 1 | import * as mobx from 'mobx'; 2 | 3 | export class StringStorage { 4 | constructor(private readonly key: string, defaultValue: T) { 5 | const value = localStorage.getItem(key); 6 | this.value = (value ? value : defaultValue) as T; 7 | mobx.makeObservable(this); 8 | } 9 | 10 | @mobx.action 11 | change(value: T) { 12 | this.value = value; 13 | localStorage.setItem(this.key, this.value); 14 | } 15 | 16 | @mobx.observable 17 | value: T; 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utilities'; 2 | export * as material from '@mui/material'; 3 | export * as main from './main'; 4 | export * as settings from './settings'; 5 | export * from './classes/BoolStorage'; 6 | export * from './classes/NumberStorage'; 7 | export * from './classes/StringStorage'; 8 | export * from './language'; 9 | export * from './MainViewModel'; 10 | export * from './MainView'; 11 | -------------------------------------------------------------------------------- /src/ui/language.ts: -------------------------------------------------------------------------------- 1 | export const language = { 2 | error: 'An error occurred. Check your console.', 3 | errorDriver: 'Load this connector in your http-driver.', 4 | errorProcess: 'Game not found. Did you run it?', 5 | errorVersion: 'Your http-driver is outdated.', 6 | reconnect: 'Reconnect' 7 | }; 8 | -------------------------------------------------------------------------------- /src/ui/main/MainView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ui from 'ui'; 3 | 4 | export const MainView = ui.createView(() => { 5 | React.useEffect(() => { 6 | const canvas = document.getElementById('canvas') as HTMLCanvasElement; 7 | canvas.style.display = 'inline'; 8 | return () => onUnmount(canvas); 9 | }, []); 10 | return null; 11 | }); 12 | 13 | function onUnmount(canvas: HTMLElement) { 14 | canvas.style.display = 'none'; 15 | } 16 | -------------------------------------------------------------------------------- /src/ui/main/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MainView'; 2 | -------------------------------------------------------------------------------- /src/ui/settings/MainView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | import FilterList from '@mui/icons-material/FilterList'; 5 | import Science from '@mui/icons-material/Science'; 6 | import Settings from '@mui/icons-material/Settings'; 7 | 8 | export const MainView = ui.createView<{vm: app.MainViewModel}>(({vm}) => ( 9 | 10 | 11 | 12 | 13 | 14 | vm.changeView(0)} 16 | icon={} 17 | label={app.language.general} /> 18 | vm.changeView(1)} 20 | icon={} 21 | label={app.language.items} /> 22 | vm.changeView(2)} 24 | icon={} 25 | label={app.language.research} /> 26 | 27 | 28 | 29 | )); 30 | 31 | const SwitchView = ui.createView<{vm: app.MainViewModel}>(({vm}) => { 32 | switch (vm.currentView) { 33 | case 1: return ; 34 | case 2: return ; 35 | default: return ; 36 | } 37 | }); 38 | 39 | const styles = { 40 | container: { 41 | margin: '0 auto', 42 | maxWidth: '384px', 43 | padding: '16px', 44 | paddingBottom: '56px' 45 | }, 46 | navigation: { 47 | position: 'fixed', 48 | bottom: 0, 49 | left: 0, 50 | right: 0 51 | }, 52 | navigationAction: { 53 | maxWidth: '128px' 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/ui/settings/MainViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | import * as mobx from 'mobx'; 3 | 4 | export class MainViewModel { 5 | readonly general = new app.general.MainViewModel(); 6 | readonly items = new app.items.MainViewModel(); 7 | readonly research = new app.research.MainViewModel(); 8 | 9 | constructor() { 10 | mobx.makeObservable(this); 11 | mobx.autorun(this.updateItemSet.bind(this)); 12 | } 13 | 14 | @mobx.action 15 | changeView(view: number) { 16 | this.currentView = view; 17 | } 18 | 19 | @mobx.observable 20 | currentView = 0; 21 | 22 | @mobx.observable 23 | itemSet = new Set(); 24 | 25 | private updateItemSet() { 26 | const value = new Set(); 27 | this.items.toItemSet(value); 28 | this.itemSet = value; 29 | value.delete(0); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ui/settings/general/MainView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | 5 | export const MainView = ui.createView<{vm: app.MainViewModel}>(({vm}) => ( 6 | 7 | 8 | {app.language.general} 9 | 10 | 11 | {app.language.generalViewType} 12 | 13 | vm.viewType.change(x.target.value as app.MainType)} 15 | value={vm.viewType.value}> 16 | 17 | {app.language.generalViewTypeMap} 18 | 19 | 20 | {app.language.generalViewTypeRadar} 21 | 22 | 23 | 24 | 25 | 26 | 27 | )); 28 | -------------------------------------------------------------------------------- /src/ui/settings/general/MainViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | import * as ui from 'ui'; 3 | 4 | export class MainViewModel { 5 | readonly map = new app.MapViewModel(); 6 | readonly radar = new app.RadarViewModel(); 7 | readonly sense = new app.SenseViewModel(); 8 | readonly viewType = new ui.StringStorage('settings.viewType', app.MainType.Radar); 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/settings/general/enums/MainType.ts: -------------------------------------------------------------------------------- 1 | export enum MainType { 2 | Map = 'map', 3 | Radar = 'radar' 4 | } 5 | -------------------------------------------------------------------------------- /src/ui/settings/general/index.ts: -------------------------------------------------------------------------------- 1 | export * from './language'; 2 | export * from './enums/MainType'; 3 | export * from './MainView'; 4 | export * from './MainViewModel'; 5 | export * from './viewModels/MapViewModel'; 6 | export * from './viewModels/RadarViewModel'; 7 | export * from './viewModels/SenseViewModel'; 8 | export * from './views/MapView'; 9 | export * from './views/RadarView'; 10 | export * from './views/SenseView'; 11 | -------------------------------------------------------------------------------- /src/ui/settings/general/language.ts: -------------------------------------------------------------------------------- 1 | export const language = { 2 | general: 'General Settings', 3 | generalMap: 'Map', 4 | generalMapItems: 'Show Items', 5 | generalMapPlayers: 'Show Players', 6 | generalRadar: 'Radar', 7 | generalRadarItems: 'Show Items', 8 | generalRadarPlayers: 'Show Players', 9 | generalSense: 'Sense', 10 | generalSenseItems: 'Highlight Items', 11 | generalSensePlayers: 'Highlight Players', 12 | generalViewType: 'Main', 13 | generalViewTypeMap: 'Show Map', 14 | generalViewTypeRadar: 'Show Radar' 15 | }; 16 | -------------------------------------------------------------------------------- /src/ui/settings/general/viewModels/MapViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as ui from 'ui'; 2 | 3 | export class MapViewModel { 4 | readonly showItems = new ui.BoolStorage('settings.map.showItems', false); 5 | readonly showPlayers = new ui.BoolStorage('settings.map.showPlayers', true); 6 | } 7 | -------------------------------------------------------------------------------- /src/ui/settings/general/viewModels/RadarViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as ui from 'ui'; 2 | 3 | export class RadarViewModel { 4 | readonly showItems = new ui.BoolStorage('settings.radar.showItems', false); 5 | readonly showPlayers = new ui.BoolStorage('settings.radar.showPlayers', true); 6 | } 7 | -------------------------------------------------------------------------------- /src/ui/settings/general/viewModels/SenseViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as ui from 'ui'; 2 | 3 | export class SenseViewModel { 4 | readonly highlightItems = new ui.BoolStorage('settings.sense.highlightItems', false); 5 | readonly highlightPlayers = new ui.BoolStorage('settings.sense.highlightPlayers', false); 6 | } 7 | -------------------------------------------------------------------------------- /src/ui/settings/general/views/MapView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | 5 | export const MapView = ui.createView<{vm: app.MapViewModel}>(({vm}) => ( 6 | 7 | 8 | {app.language.generalMap} 9 | 10 | vm.showItems.change(x.target.checked)} 14 | checked={vm.showItems.value} />} /> 15 | vm.showPlayers.change(x.target.checked)} 19 | checked={vm.showPlayers.value} />} /> 20 | 21 | )); 22 | -------------------------------------------------------------------------------- /src/ui/settings/general/views/RadarView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | 5 | export const RadarView = ui.createView<{vm: app.RadarViewModel}>(({vm}) => ( 6 | 7 | 8 | {app.language.generalRadar} 9 | 10 | vm.showItems.change(x.target.checked)} 14 | checked={vm.showItems.value} />} /> 15 | vm.showPlayers.change(x.target.checked)} 19 | checked={vm.showPlayers.value} />} /> 20 | 21 | )); 22 | -------------------------------------------------------------------------------- /src/ui/settings/general/views/SenseView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | 5 | export const SenseView = ui.createView<{vm: app.SenseViewModel}>(({vm}) => ( 6 | 7 | 8 | {app.language.generalSense} 9 | 10 | vm.highlightItems.change(x.target.checked)} 14 | checked={vm.highlightItems.value} />} /> 15 | vm.highlightPlayers.change(x.target.checked)} 19 | checked={vm.highlightPlayers.value} />} /> 20 | 21 | )); 22 | -------------------------------------------------------------------------------- /src/ui/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * as general from './general'; 2 | export * as items from './items'; 3 | export * as research from './research'; 4 | export * from './language'; 5 | export * from './MainView'; 6 | export * from './MainViewModel'; 7 | -------------------------------------------------------------------------------- /src/ui/settings/items/MainView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | 5 | export const MainView = ui.createView<{vm: app.MainViewModel}>(({vm}) => ( 6 | 7 | 8 | {app.language.items} 9 | 10 | {vm.areas.map((x, i) => ( 11 | 12 | ))} 13 | 14 | )); 15 | -------------------------------------------------------------------------------- /src/ui/settings/items/MainViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class MainViewModel { 4 | readonly areas = [ 5 | new app.AreaViewModel(app.language.itemsAmmo, x => x.itemType === 'Ammo'), 6 | new app.AreaViewModel(app.language.itemsGearBackpack, x => x.itemType === 'Gear' && x.gearType === 'Backpack'), 7 | new app.AreaViewModel(app.language.itemsGearBodyShield, x => x.itemType === 'Gear' && x.gearType === 'BodyShield'), 8 | new app.AreaViewModel(app.language.itemsGearEvoShield, x => x.itemType === 'Gear' && x.gearType === 'EvoShield'), 9 | new app.AreaViewModel(app.language.itemsGearHelmet, x => x.itemType === 'Gear' && x.gearType === 'Helmet'), 10 | new app.AreaViewModel(app.language.itemsGearKnockdownShield, x => x.itemType === 'Gear' && x.gearType === 'KnockdownShield'), 11 | new app.AreaViewModel(app.language.itemsGrenade, x => x.itemType === 'Grenade'), 12 | new app.AreaViewModel(app.language.itemsRegen, x => x.itemType === 'Regen'), 13 | new app.AreaViewModel(app.language.itemsWeaponsEnergy, x => x.itemType === 'Weapon' && x.ammoType === 'Energy'), 14 | new app.AreaViewModel(app.language.itemsWeaponsHeavy, x => x.itemType === 'Weapon' && x.ammoType === 'Heavy'), 15 | new app.AreaViewModel(app.language.itemsWeaponsLight, x => x.itemType === 'Weapon' && x.ammoType === 'Light'), 16 | new app.AreaViewModel(app.language.itemsWeaponShotgun, x => x.itemType === 'Weapon' && x.ammoType === 'Shotgun'), 17 | new app.AreaViewModel(app.language.itemsWeaponsSniper, x => x.itemType === 'Weapon' && x.ammoType === 'Sniper'), 18 | new app.AreaViewModel(app.language.itemsWeaponsSpecial, x => x.itemType === 'Weapon' && x.ammoType === 'Special'), 19 | new app.AreaViewModel(app.language.itemsWeaponAttachments, x => x.itemType === 'Attachment') 20 | ]; 21 | 22 | toItemSet(value: Set) { 23 | for (const area of this.areas) { 24 | area.toItemSet(value); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ui/settings/items/index.ts: -------------------------------------------------------------------------------- 1 | export * from './language'; 2 | export * from './viewModels/AreaViewModel'; 3 | export * from './viewModels/ItemViewModel'; 4 | export * from './views/AreaView'; 5 | export * from './views/ItemView'; 6 | export * from './MainView'; 7 | export * from './MainViewModel'; 8 | -------------------------------------------------------------------------------- /src/ui/settings/items/language.ts: -------------------------------------------------------------------------------- 1 | export const language = { 2 | items: 'Item Settings', 3 | itemsAmmo: 'Ammo', 4 | itemsGearBackpack: 'Gear / Backpack', 5 | itemsGearBodyShield: 'Gear / Body Shield', 6 | itemsGearEvoShield: 'Gear / Evo Shield', 7 | itemsGearHelmet: 'Gear / Helmet', 8 | itemsGearKnockdownShield: 'Gear / Knockdown Shield', 9 | itemsGrenade: 'Grenades', 10 | itemsRegen: 'Regen', 11 | itemsWeaponsEnergy: 'Weapons / Energy', 12 | itemsWeaponsHeavy: 'Weapons / Heavy', 13 | itemsWeaponsLight: 'Weapons / Light', 14 | itemsWeaponShotgun: 'Weapons / Shotguns', 15 | itemsWeaponsSniper: 'Weapons / Snipers', 16 | itemsWeaponsSpecial: 'Weapons / Special', 17 | itemsWeaponAttachments: 'Weapon Attachments' 18 | }; 19 | -------------------------------------------------------------------------------- /src/ui/settings/items/viewModels/AreaViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | import * as lib from 'lib'; 3 | 4 | export class AreaViewModel { 5 | readonly items: Array; 6 | readonly name: string; 7 | 8 | constructor(name: string, selector: (item: lib.items.All) => boolean) { 9 | this.items = Array.from(fetch(selector)).map(x => new app.ItemViewModel(x)); 10 | this.name = name; 11 | } 12 | 13 | toItemSet(value: Set) { 14 | for (const entry of this.items) { 15 | if (!entry.store.value) continue; 16 | value.add(entry.value.itemId); 17 | } 18 | } 19 | } 20 | 21 | function *fetch(selector: (item: lib.items.All) => boolean) { 22 | for (const item of lib.items.list) { 23 | if (!selector(item)) continue; 24 | yield item; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ui/settings/items/viewModels/ItemViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as lib from 'lib'; 2 | import * as ui from 'ui'; 3 | 4 | export class ItemViewModel { 5 | constructor( 6 | readonly value: lib.items.All, 7 | readonly store = new ui.BoolStorage(`Items[${value.name}]`, false)) {} 8 | } 9 | -------------------------------------------------------------------------------- /src/ui/settings/items/views/AreaView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | 5 | export const AreaView = ui.createView<{vm: app.AreaViewModel}>(({vm}) => ( 6 | 7 | 8 | {vm.name} 9 | 10 | {vm.items.map(x => ( 11 | 12 | ))} 13 | 14 | )); 15 | -------------------------------------------------------------------------------- /src/ui/settings/items/views/ItemView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | 5 | export const ItemView = ui.createView<{vm: app.ItemViewModel}>(({vm}) => ( 6 | 7 | vm.store.change(y.target.checked)} 11 | checked={vm.store.value} />} /> 12 | 13 | )); 14 | 15 | const styles = { 16 | label: { 17 | overflow: 'hidden', 18 | whiteSpace: 'nowrap', 19 | textOverflow: 'ellipsis' 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/ui/settings/language.ts: -------------------------------------------------------------------------------- 1 | export const language = { 2 | general: 'General', 3 | items: 'Items', 4 | research: 'Research' 5 | }; 6 | -------------------------------------------------------------------------------- /src/ui/settings/research/MainView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | 5 | export const MainView = ui.createView<{vm: app.MainViewModel}>(({vm}) => ( 6 | 7 | 8 | {app.language.research} 9 | 10 | 11 | {app.language.researchWarning} 12 | 13 | 14 | 15 | )); 16 | -------------------------------------------------------------------------------- /src/ui/settings/research/MainViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as app from '.'; 2 | 3 | export class MainViewModel { 4 | readonly recoil = new app.RecoilViewModel(); 5 | } 6 | -------------------------------------------------------------------------------- /src/ui/settings/research/index.ts: -------------------------------------------------------------------------------- 1 | export * from './language'; 2 | export * from './viewModels/RecoilViewModel'; 3 | export * from './views/RecoilView'; 4 | export * from './MainView'; 5 | export * from './MainViewModel'; 6 | -------------------------------------------------------------------------------- /src/ui/settings/research/language.ts: -------------------------------------------------------------------------------- 1 | export const language = { 2 | research: 'Research Settings', 3 | researchRecoil: 'Recoil Compensation', 4 | researchRecoilEnable: 'Enable', 5 | researchRecoilPercentage: 'Recoil Compensation (%)', 6 | researchWarning: 'These features may be unstable and cause detection. Proceed with caution.' 7 | }; 8 | -------------------------------------------------------------------------------- /src/ui/settings/research/viewModels/RecoilViewModel.ts: -------------------------------------------------------------------------------- 1 | import * as lib from 'lib'; 2 | import * as mobx from 'mobx'; 3 | import * as ui from 'ui'; 4 | 5 | export class RecoilViewModel { 6 | readonly enable = new ui.BoolStorage('settings.recoil.enable', false); 7 | readonly percentage = new ui.NumberStorage('settings.recoil.percentage', 0.75); 8 | 9 | constructor() { 10 | mobx.makeObservable(this); 11 | mobx.autorun(this.updateOptions.bind(this)); 12 | } 13 | 14 | @mobx.observable 15 | options?: lib.features.IRecoilOptions; 16 | 17 | private updateOptions() { 18 | this.options = { 19 | percentage: this.percentage.value 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ui/settings/research/views/RecoilView.tsx: -------------------------------------------------------------------------------- 1 | import * as app from '..'; 2 | import * as React from 'react'; 3 | import * as ui from 'ui'; 4 | 5 | export const RecoilView = ui.createView<{vm: app.RecoilViewModel}>(({vm}) => ( 6 | 7 | 8 | {app.language.researchRecoil} 9 | 10 | vm.enable.change(x.target.checked)} 14 | checked={vm.enable.value} />} /> 15 | 16 | 17 | {app.language.researchRecoilPercentage} 18 | 19 | 20 | {vm.percentage.value} 21 | 22 | 23 | vm.percentage.change(Number(x))} 25 | value={vm.percentage.value} 26 | min={0} max={1} step={0.01} /> 27 | 28 | )); 29 | 30 | const styles = { 31 | sliderLabel: { 32 | position: 'relative' 33 | }, 34 | sliderValue: { 35 | position: 'absolute', 36 | right: 0, 37 | top: 0 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/ui/utilities.ts: -------------------------------------------------------------------------------- 1 | import * as mobxReact from 'mobx-react'; 2 | 3 | export function createView(fn: (props: T) => any) { 4 | return mobxReact.observer((props: T) => fn(props) || null); 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "baseUrl": "src", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "inlineSourceMap": true, 10 | "inlineSources": true, 11 | "jsx": "react", 12 | "lib": ["DOM", "ES2021"], 13 | "module": "CommonJS", 14 | "noImplicitAny": true, 15 | "noImplicitThis": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "outDir": "dist", 21 | "skipLibCheck": true, 22 | "strict": true, 23 | "strictBindCallApply": true, 24 | "strictFunctionTypes": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "target": "ES2021" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | app: './dist/app' 4 | }, 5 | output: { 6 | filename: '[name].min.js', 7 | path: `${__dirname}/docs` 8 | }, 9 | resolve: { 10 | alias: { 11 | 'lib': `${__dirname}/dist/lib`, 12 | 'ui': `${__dirname}/dist/ui` 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /webpack.development.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common'); 2 | 3 | module.exports = Object.assign(common, { 4 | mode: 'development', 5 | devtool: 'eval-source-map', 6 | devServer: { 7 | client: false, 8 | port: 3000, 9 | static: {directory: common.output.path} 10 | }, 11 | module: { 12 | rules: [{ 13 | test: /\.js$/, 14 | enforce: 'pre', 15 | use: ['source-map-loader'] 16 | }] 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /webpack.production.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | 4 | module.exports = Object.assign(common, { 5 | mode: 'production', 6 | performance: {hints: false}, 7 | optimization: { 8 | minimize: true, 9 | minimizer: [new TerserPlugin({ 10 | extractComments: false, 11 | terserOptions: {format: {comments: false}} 12 | })] 13 | } 14 | }); 15 | --------------------------------------------------------------------------------