├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── README.md ├── bun.lockb ├── drizzle.config.ts ├── package.json ├── src ├── client.ts ├── db │ ├── index.ts │ └── schema.ts ├── env.ts ├── handlers │ ├── c2u │ │ ├── connection │ │ │ └── c2uRegisterPacketTypeIdHandler.ts │ │ ├── cosmetic │ │ │ ├── c2uCosmeticAnimationTrigger.ts │ │ │ ├── emote │ │ │ │ ├── c2uCosmeticEmoteWheelSelect.ts │ │ │ │ └── c2uCosmeticEmoteWheelUpdate.ts │ │ │ └── outfit │ │ │ │ ├── c2uCosmeticOutfitCosmeticSettingsUpdate.ts │ │ │ │ ├── c2uCosmeticOutfitCreate.ts │ │ │ │ ├── c2uCosmeticOutfitDelete.ts │ │ │ │ ├── c2uCosmeticOutfitEquippedCosmeticsUpdate.ts │ │ │ │ ├── c2uCosmeticOutfitNameUpdate.ts │ │ │ │ ├── c2uCosmeticOutfitSelect.ts │ │ │ │ ├── c2uCosmeticOutfitSkinUpdate.ts │ │ │ │ └── c2uCosmeticOutfitUpdateFavoriteState.ts │ │ ├── mod │ │ │ └── c2uModsAnnounceHandler.ts │ │ ├── subscription │ │ │ └── c2uSubscriptionUpdatePacket.ts │ │ └── telemetry │ │ │ └── c2uTelemetryHandler.ts │ ├── index.ts │ └── u2c │ │ ├── connection │ │ └── u2cRegisterPacketTypeIdHandler.ts │ │ ├── cosmetic │ │ ├── emote │ │ │ └── u2cCosmeticEmoteWheelPopulate.ts │ │ ├── outfit │ │ │ ├── u2cCosmeticOutfitPopulate.ts │ │ │ └── u2cCosmeticOutfitSelectedResponse.ts │ │ ├── u2cCosmeticsPopulateHandler.ts │ │ └── u2cCosmeticsUserUnlockedHandler.ts │ │ └── notices │ │ └── u2cNoticesPopulateHandler.ts ├── index.ts ├── protocol │ ├── common.ts │ ├── index.ts │ ├── packets.ts │ └── packets │ │ ├── connection │ │ └── registerPacketTypeId.ts │ │ ├── cosmetic │ │ ├── clientCosmeticAnimationTrigger.ts │ │ ├── cosmeticsPopulate.ts │ │ ├── cosmeticsUserUnlocked.ts │ │ ├── emote │ │ │ ├── cosmeticEmoteWheelPopulate.ts │ │ │ ├── cosmeticEmoteWheelSelect.ts │ │ │ └── cosmeticEmoteWheelUpdate.ts │ │ ├── outfit │ │ │ ├── cosmeticOutfitCosmeticSettingsUpdate.ts │ │ │ ├── cosmeticOutfitCreate.ts │ │ │ ├── cosmeticOutfitDelete.ts │ │ │ ├── cosmeticOutfitEquippedCosmeticsUpdate.ts │ │ │ ├── cosmeticOutfitNameUpdate.ts │ │ │ ├── cosmeticOutfitPopulate.ts │ │ │ ├── cosmeticOutfitSelect.ts │ │ │ ├── cosmeticOutfitSelectedResponse.ts │ │ │ ├── cosmeticOutfitSkinUpdate.ts │ │ │ └── cosmeticOutfitUpdateFavoriteState.ts │ │ └── serverCosmeticAnimationTrigger.ts │ │ ├── mod │ │ └── modsAnnounce.ts │ │ ├── notices │ │ └── noticesPopulate.ts │ │ ├── response │ │ └── responseActionPacket.ts │ │ ├── subscription │ │ └── subscriptionUpdatePacket.ts │ │ └── telemetry │ │ └── telemetry.ts └── utils │ ├── generic.ts │ └── http.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | parser: "@typescript-eslint/parser", 5 | parserOptions: { 6 | project: "./tsconfig.lint.json", 7 | tsconfigRootDir: __dirname, 8 | }, 9 | extends: ["eslint:recommended", "prettier"], 10 | plugins: ["only-warn"], 11 | globals: { 12 | React: true, 13 | JSX: true, 14 | }, 15 | env: { 16 | node: true, 17 | }, 18 | settings: { 19 | "import/resolver": { 20 | typescript: { 21 | project, 22 | }, 23 | }, 24 | }, 25 | ignorePatterns: [ 26 | // Ignore dotfiles 27 | ".*.js", 28 | "node_modules/", 29 | "dist/", 30 | ], 31 | overrides: [ 32 | { 33 | files: ["*.js?(x)", "*.ts?(x)"], 34 | }, 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1 2 | 3 | # Adjust BUN_VERSION as desired 4 | ARG BUN_VERSION=1.1.26 5 | FROM oven/bun:${BUN_VERSION}-slim as base 6 | 7 | LABEL fly_launch_runtime="Bun" 8 | 9 | # Bun app lives here 10 | WORKDIR /app 11 | 12 | # Set production environment 13 | ENV NODE_ENV="production" 14 | 15 | 16 | # Throw-away build stage to reduce size of final image 17 | FROM base as build 18 | 19 | # Install packages needed to build node modules 20 | RUN apt-get update -qq && \ 21 | apt-get install --no-install-recommends -y build-essential pkg-config python-is-python3 22 | 23 | # Install node modules 24 | COPY bun.lockb package.json ./ 25 | RUN bun install --ci 26 | 27 | # Copy application code 28 | COPY . . 29 | 30 | 31 | # Final stage for app image 32 | FROM base 33 | 34 | # Copy built application 35 | COPY --from=build /app /app 36 | 37 | # Start the server by default, this can be overwritten at runtime 38 | EXPOSE 3000 39 | CMD [ "bun", "src/index.ts" ] 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cm-proxy 2 | 3 | This is a proxy server that sits inbetween the [Essential Mod](https://essential.gg) and the Essential content manager to disable telemetry and data collection, alongside unlocking all cosmetics for free -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiemc/cm-proxy/aeb26ba8950c94dafdb4d775155a01423832d562/bun.lockb -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | 3 | export default defineConfig({ 4 | out: "./drizzle", 5 | schema: "./src/db/schema.ts", 6 | dialect: "postgresql", 7 | dbCredentials: { 8 | url: process.env.DATABASE_URL!, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cm-proxy", 3 | "module": "src/index.ts", 4 | "type": "module", 5 | "scripts": { 6 | "run": "bun src/index.ts", 7 | "dev": "bun --watch src/index.ts" 8 | }, 9 | "devDependencies": { 10 | "@flydotio/dockerfile": "^0.5.9", 11 | "@types/bun": "latest", 12 | "@types/pg": "^8.11.10", 13 | "drizzle-kit": "^0.25.0", 14 | "tsx": "^4.19.1" 15 | }, 16 | "peerDependencies": { 17 | "typescript": "^5.0.0" 18 | }, 19 | "dependencies": { 20 | "@t3-oss/env-core": "^0.11.1", 21 | "dotenv": "^16.4.5", 22 | "drizzle-orm": "^0.34.0", 23 | "https-proxy-agent": "^7.0.6", 24 | "ioredis": "^5.4.1", 25 | "ky": "^1.7.2", 26 | "postgres": "^3.4.4", 27 | "proxy-agent": "^6.5.0", 28 | "socks-proxy-agent": "^8.0.5", 29 | "ws": "^8.18.0", 30 | "zod": "^3.23.8" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { ServerWebSocket } from "bun"; 2 | import { and, eq, inArray } from "drizzle-orm"; 3 | import { Redis } from "ioredis"; 4 | import { db } from "./db/index.js"; 5 | import { outfits, users } from "./db/schema.js"; 6 | import { env } from "./env.js"; 7 | import { 8 | clientToUpstreamHandlers, 9 | upstreamToClientHandlers, 10 | } from "./handlers/index.js"; 11 | import { WebSocketData } from "./index.js"; 12 | import { decodePacket, encodePacket, Packet } from "./protocol/index.js"; 13 | import registerPacketTypeId from "./protocol/packets/connection/registerPacketTypeId.js"; 14 | import cosmeticOutfitPopulate from "./protocol/packets/cosmetic/outfit/cosmeticOutfitPopulate.js"; 15 | import cosmeticOutfitSelectedResponse from "./protocol/packets/cosmetic/outfit/cosmeticOutfitSelectedResponse.js"; 16 | import responseActionPacket from "./protocol/packets/response/responseActionPacket.js"; 17 | import { reverseObj } from "./utils/generic.js"; 18 | import { WebSocket } from "ws"; 19 | 20 | export class Client { 21 | profile: { 22 | id: string; 23 | name: string; 24 | }; 25 | #ws: ServerWebSocket | null = null; 26 | #upstreamWs: WebSocket; 27 | clientPackets: Record = { 28 | 0: "connection.ConnectionRegisterPacketTypeIdPacket", 29 | }; 30 | upstreamPackets: Record = { 31 | 0: "connection.ConnectionRegisterPacketTypeIdPacket", 32 | }; 33 | startupPackets: Buffer[] = []; 34 | subscribedTo = new Set(); 35 | #redisSubscriber = new Redis(env.REDIS_URL); 36 | #redisPublisher = new Redis(env.REDIS_URL); 37 | 38 | initialized = false; 39 | 40 | constructor(profile: { id: string; name: string }, upstreamWs: WebSocket) { 41 | this.profile = profile; 42 | this.#upstreamWs = upstreamWs; 43 | } 44 | async open(ws: ServerWebSocket) { 45 | this.#ws = ws; 46 | 47 | console.log(`${this.profile.name} connected`); 48 | 49 | await db 50 | .insert(users) 51 | .values({ id: this.profile.id, username: this.profile.name }) 52 | .onConflictDoUpdate({ 53 | target: users.id, 54 | set: { username: this.profile.name }, 55 | }); 56 | 57 | this.#upstreamWs.addEventListener("message", async (event) => { 58 | if (!(event.data instanceof Buffer)) return; 59 | await this.onUpstreamMessage(event.data); 60 | }); 61 | this.initialized = true; 62 | 63 | for (const packet of this.startupPackets) { 64 | await this.onUpstreamMessage(packet); 65 | } 66 | 67 | this.#redisSubscriber.on("message", async (channel, msg) => { 68 | await this.sendClientPacket(JSON.parse(msg)); 69 | }); 70 | } 71 | 72 | async subscribe(id: string) { 73 | if (this.subscribedTo.has(id)) return; 74 | await this.#redisSubscriber.subscribe("player:" + id); 75 | this.subscribedTo.add(id); 76 | } 77 | 78 | async unsubscribe(id: string) { 79 | if (!this.subscribedTo.has(id)) return; 80 | this.subscribedTo.delete(id); 81 | await this.#redisSubscriber.unsubscribe("player:" + id); 82 | } 83 | 84 | async sendOutfitToSubscribers() { 85 | const selectedOutfit = ( 86 | await db 87 | .select() 88 | .from(outfits) 89 | .where( 90 | and(eq(outfits.ownerId, this.profile.id), eq(outfits.selected, true)) 91 | ) 92 | )[0]; 93 | 94 | if (!selectedOutfit) return; 95 | 96 | await this.sendToSubscribed({ 97 | className: cosmeticOutfitSelectedResponse.className, 98 | body: { 99 | uuid: this.profile.id, 100 | cosmeticSettings: selectedOutfit.cosmeticSettings, 101 | equippedCosmetics: selectedOutfit.equippedCosmetics, 102 | skinTexture: selectedOutfit.skinTexture, 103 | }, 104 | }); 105 | } 106 | 107 | async sendToSubscribed(packet: Packet) { 108 | this.#redisPublisher.publish( 109 | "player:" + this.profile.id, 110 | JSON.stringify(packet) 111 | ); 112 | } 113 | 114 | async sendResponse( 115 | packet: Packet, 116 | success: boolean = true, 117 | error: string | null = null 118 | ) { 119 | this.sendClientPacket({ 120 | uuid: packet.uuid, 121 | className: responseActionPacket.className, 122 | body: { a: success, b: error }, 123 | }); 124 | } 125 | 126 | async selectOutfit(newOutfit: string) { 127 | await db 128 | .update(outfits) 129 | .set({ selected: false }) 130 | .where( 131 | and(eq(outfits.selected, true), eq(outfits.ownerId, this.profile.id)) 132 | ); 133 | 134 | await db 135 | .update(outfits) 136 | .set({ selected: true }) 137 | .where( 138 | and(eq(outfits.id, newOutfit), eq(outfits.ownerId, this.profile.id)) 139 | ); 140 | 141 | await this.sendOutfitToSubscribers(); 142 | } 143 | 144 | async resendOutfits( 145 | replyId: string | undefined = undefined, 146 | outfitIds: string[] | undefined = undefined 147 | ) { 148 | const outfitList = await db 149 | .select() 150 | .from(outfits) 151 | .where( 152 | and( 153 | eq(outfits.ownerId, this.profile.id), 154 | outfitIds && inArray(outfits.id, outfitIds) 155 | ) 156 | ); 157 | 158 | await this.sendClientPacket({ 159 | uuid: replyId, 160 | className: cosmeticOutfitPopulate.className, 161 | body: { 162 | outfits: outfitList.map((o) => ({ 163 | a: o.id, 164 | b: o.name, 165 | c: o.skinTexture, 166 | d: o.equippedCosmetics as any, 167 | e: o.cosmeticSettings as any, 168 | f: o.selected, 169 | g: o.createdAt.getTime(), 170 | h: o.favoritedAt, 171 | j: o.skinId, 172 | })), 173 | }, 174 | }); 175 | } 176 | 177 | async onClientMessage(message: Buffer) { 178 | let packet = decodePacket(this.upstreamPackets, message); 179 | 180 | if (!packet) { 181 | this.#upstreamWs.send(message); 182 | return null; 183 | } 184 | 185 | const handler = clientToUpstreamHandlers.find( 186 | (a) => a.def.className == packet!.className 187 | ); 188 | if (handler) { 189 | const resultingPacket = await handler.handle(this, packet); 190 | 191 | if (resultingPacket) { 192 | if ("className" in resultingPacket) { 193 | packet = resultingPacket; 194 | } 195 | 196 | if ("cancelled" in resultingPacket && resultingPacket.cancelled) return; 197 | } 198 | } 199 | 200 | await this.sendUpstreamPacket(packet); 201 | } 202 | 203 | async onUpstreamMessage(message: Buffer) { 204 | let packet = decodePacket(this.clientPackets, message); 205 | if (!packet) { 206 | this.#ws!.sendBinary(message); 207 | return null; 208 | } 209 | 210 | const handler = upstreamToClientHandlers.find( 211 | (a) => a.def.className == packet!.className 212 | ); 213 | if (!handler) { 214 | this.#ws!.sendBinary(message); 215 | return null; 216 | } 217 | 218 | const resultingPacket = await handler.handle(this, packet); 219 | if (!resultingPacket) { 220 | this.#ws!.sendBinary(message); 221 | return null; 222 | } 223 | if ("className" in resultingPacket) { 224 | packet = resultingPacket; 225 | } 226 | 227 | if ("cancelled" in resultingPacket && resultingPacket.cancelled) return; 228 | 229 | await this.sendClientPacket(packet); 230 | } 231 | 232 | async sendUpstreamPacket(packet: Packet) { 233 | const invertedOutgoingMap = reverseObj(this.upstreamPackets) as Record< 234 | string, 235 | number 236 | >; 237 | 238 | const buffer = encodePacket(invertedOutgoingMap, packet); 239 | 240 | if (!buffer) 241 | return console.log( 242 | "warn: failed to send packet to upstream because the client has not registered it yet" 243 | ); 244 | 245 | this.#upstreamWs.send(buffer); 246 | } 247 | 248 | async sendClientPacket(packet: Packet) { 249 | let invertedOutgoingMap = reverseObj(this.clientPackets) as Record< 250 | string, 251 | number 252 | >; 253 | if (!invertedOutgoingMap[packet.className]) { 254 | const id = this.getNextPacketId(this.clientPackets); 255 | await this.sendClientPacket({ 256 | className: registerPacketTypeId.className, 257 | body: { 258 | a: packet.className, 259 | b: id, 260 | }, 261 | }); 262 | this.clientPackets[id] = packet.className; 263 | invertedOutgoingMap = reverseObj(this.clientPackets) as Record< 264 | string, 265 | number 266 | >; 267 | } 268 | 269 | let buffer = encodePacket(invertedOutgoingMap, packet); 270 | if (!buffer) { 271 | return false; 272 | } 273 | 274 | this.#ws!.sendBinary(buffer); 275 | 276 | return true; 277 | } 278 | 279 | async close() { 280 | await this.#redisPublisher.quit(); 281 | await this.#redisSubscriber.quit(); 282 | } 283 | 284 | getNextPacketId(packetMap: Record) { 285 | const ids = Object.keys(packetMap).map((a) => parseInt(a)); 286 | return ids[ids.length - 1]! + 1; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/postgres-js"; 2 | import postgres from "postgres"; 3 | 4 | import { env } from "~/env.js"; 5 | import * as schema from "./schema.js"; 6 | 7 | /** 8 | * Cache the database connection in development. This avoids creating a new connection on every HMR 9 | * update. 10 | */ 11 | const globalForDb = globalThis as unknown as { 12 | conn: postgres.Sql | undefined; 13 | }; 14 | 15 | const conn = globalForDb.conn ?? postgres(env.DATABASE_URL); 16 | if (env.NODE_ENV !== "production") globalForDb.conn = conn; 17 | 18 | export const db = drizzle(conn, { schema }); 19 | -------------------------------------------------------------------------------- /src/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | boolean, 3 | index, 4 | jsonb, 5 | pgTable, 6 | text, 7 | timestamp, 8 | uuid, 9 | varchar, 10 | } from "drizzle-orm/pg-core"; 11 | 12 | export const users = pgTable( 13 | "users", 14 | { 15 | id: uuid("id").notNull().primaryKey(), 16 | username: varchar({ length: 17 }).notNull(), 17 | }, 18 | (user) => ({ 19 | uuidIdx: index("users_id_idx").on(user.id), 20 | usernameIdx: index("users_username_idx").on(user.username), 21 | }) 22 | ); 23 | 24 | export const outfits = pgTable( 25 | "outfits", 26 | { 27 | id: uuid().notNull().primaryKey().defaultRandom(), 28 | ownerId: uuid("owner_id") 29 | .notNull() 30 | .references(() => users.id), 31 | name: text().notNull(), 32 | skinTexture: text("skin_texture"), 33 | equippedCosmetics: jsonb("equipped_cosmetics"), 34 | cosmeticSettings: jsonb("cosmetic_settings"), 35 | selected: boolean().notNull(), 36 | createdAt: timestamp("created_at").notNull().defaultNow(), 37 | favoritedAt: timestamp("favorited_at"), 38 | skinId: text("skin_id"), 39 | }, 40 | (outfit) => ({ 41 | idIdx: index("outfits_id_idx").on(outfit.id), 42 | ownerIdIdx: index("outfits_owner_id_idx").on(outfit.ownerId), 43 | selectedIdx: index("outfits_selected_idx").on(outfit.selected), 44 | }) 45 | ); 46 | 47 | export const emoteWheels = pgTable( 48 | "emote_wheels", 49 | { 50 | id: uuid().notNull().primaryKey().defaultRandom(), 51 | ownerId: uuid("owner_id") 52 | .notNull() 53 | .references(() => users.id), 54 | selected: boolean().notNull(), 55 | slots: jsonb().notNull(), 56 | createdAt: timestamp("created_at").notNull().defaultNow(), 57 | updatedAt: timestamp("updated_at"), 58 | }, 59 | (emoteWheel) => ({ 60 | idIdx: index("emote_wheels_id_idx").on(emoteWheel.id), 61 | ownerIdIdx: index("emote_wheels_owner_id_idx").on(emoteWheel.ownerId), 62 | selectedIdx: index("emote_wheels_selected_idx").on(emoteWheel.selected), 63 | }) 64 | ); 65 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | server: { 6 | NODE_ENV: z.string().nullish(), 7 | DATABASE_URL: z.string().url(), 8 | REDIS_URL: z.string().url(), 9 | PACKET_LOG: z.enum(["yes", "no"]).nullish(), 10 | PORT: z.number().default(8000), 11 | WS_PROXY: z.string().url().nullish() 12 | }, 13 | 14 | /** 15 | * What object holds the environment variables at runtime. This is usually 16 | * `process.env` or `import.meta.env`. 17 | */ 18 | runtimeEnv: process.env, 19 | 20 | /** 21 | * By default, this library will feed the environment variables directly to 22 | * the Zod validator. 23 | * 24 | * This means that if you have an empty string for a value that is supposed 25 | * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag 26 | * it as a type mismatch violation. Additionally, if you have an empty string 27 | * for a value that is supposed to be a string with a default value (e.g. 28 | * `DOMAIN=` in an ".env" file), the default value will never be applied. 29 | * 30 | * In order to solve these issues, we recommend that all new projects 31 | * explicitly specify this option as true. 32 | */ 33 | emptyStringAsUndefined: true, 34 | }); 35 | -------------------------------------------------------------------------------- /src/handlers/c2u/connection/c2uRegisterPacketTypeIdHandler.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import registerPacketTypeId from "~/protocol/packets/connection/registerPacketTypeId.js"; 3 | export default { 4 | def: registerPacketTypeId, 5 | async handle(client, packet) { 6 | const { body } = packet; 7 | client.upstreamPackets[body!.b] = body!.a; 8 | }, 9 | } as Handler; 10 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/c2uCosmeticAnimationTrigger.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import cosmeticAnimationTrigger from "~/protocol/packets/cosmetic/clientCosmeticAnimationTrigger.js"; 3 | import serverCosmeticAnimationTrigger from "~/protocol/packets/cosmetic/serverCosmeticAnimationTrigger.js"; 4 | 5 | export default { 6 | def: cosmeticAnimationTrigger, 7 | async handle(client, packet) { 8 | await client.sendToSubscribed({ 9 | className: serverCosmeticAnimationTrigger.className, 10 | body: { 11 | a: client.profile.id, 12 | b: packet.body!.a, 13 | c: packet.body!.b, 14 | }, 15 | }); 16 | return { cancelled: true }; 17 | }, 18 | } as Handler; 19 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/emote/c2uCosmeticEmoteWheelSelect.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { emoteWheels } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticEmoteWheelSelect from "~/protocol/packets/cosmetic/emote/cosmeticEmoteWheelSelect.js"; 6 | 7 | export default { 8 | def: cosmeticEmoteWheelSelect, 9 | async handle(client, packet) { 10 | const { a: id } = packet.body!; 11 | 12 | await db 13 | .update(emoteWheels) 14 | .set({ selected: false }) 15 | .where( 16 | and( 17 | eq(emoteWheels.ownerId, client.profile.id), 18 | eq(emoteWheels.selected, true) 19 | ) 20 | ); 21 | 22 | await db 23 | .update(emoteWheels) 24 | .set({ selected: true }) 25 | .where( 26 | and(eq(emoteWheels.ownerId, client.profile.id), eq(emoteWheels.id, id)) 27 | ); 28 | 29 | return { cancelled: true }; 30 | }, 31 | } as Handler; 32 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/emote/c2uCosmeticEmoteWheelUpdate.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { emoteWheels } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticEmoteWheelUpdate from "~/protocol/packets/cosmetic/emote/cosmeticEmoteWheelUpdate.js"; 6 | 7 | export default { 8 | def: cosmeticEmoteWheelUpdate, 9 | async handle(client, packet) { 10 | const { a: id, b: index, c: value } = packet.body!; 11 | 12 | const emoteWheel = ( 13 | await db 14 | .select() 15 | .from(emoteWheels) 16 | .where( 17 | and( 18 | eq(emoteWheels.ownerId, client.profile.id), 19 | eq(emoteWheels.id, id) 20 | ) 21 | ) 22 | )[0]; 23 | if (!emoteWheel) return; 24 | (emoteWheel.slots as any)[index] = value; 25 | 26 | await db 27 | .update(emoteWheels) 28 | .set({ slots: emoteWheel.slots, updatedAt: new Date() }) 29 | .where(eq(emoteWheels.id, id)); 30 | return { cancelled: true }; 31 | }, 32 | } as Handler; 33 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/outfit/c2uCosmeticOutfitCosmeticSettingsUpdate.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { outfits } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import { cosmeticIds } from "~/index.js"; 6 | import cosmeticOutfitCosmeticSettingsUpdate from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitCosmeticSettingsUpdate.js"; 7 | 8 | export default { 9 | def: cosmeticOutfitCosmeticSettingsUpdate, 10 | async handle(client, packet) { 11 | const { a: outfitId, b: cosmeticId, c: settings } = packet.body!; 12 | 13 | const outfit = ( 14 | await db 15 | .select() 16 | .from(outfits) 17 | .where( 18 | and(eq(outfits.id, outfitId), eq(outfits.ownerId, client.profile.id)) 19 | ) 20 | .limit(1) 21 | )[0]; 22 | 23 | if (!outfit) 24 | return await client.sendResponse(packet, false, "Outfit not found"); 25 | if (!cosmeticIds.has(cosmeticId)) 26 | return await client.sendResponse(packet, false, "Invalid cosmetic id"); 27 | 28 | (outfit!.cosmeticSettings as any)[cosmeticId] = settings; 29 | 30 | await db 31 | .update(outfits) 32 | .set({ cosmeticSettings: outfit!.cosmeticSettings }) 33 | .where( 34 | and(eq(outfits.id, outfitId), eq(outfits.ownerId, client.profile.id)) 35 | ); 36 | 37 | await client.sendResponse(packet); 38 | 39 | await client.sendOutfitToSubscribers(); 40 | 41 | return { cancelled: true }; 42 | }, 43 | } as Handler; 44 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/outfit/c2uCosmeticOutfitCreate.ts: -------------------------------------------------------------------------------- 1 | import { db } from "~/db/index.js"; 2 | import { outfits } from "~/db/schema.js"; 3 | import { Handler } from "~/handlers/index.js"; 4 | import cosmeticOutfitCreate from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitCreate.js"; 5 | 6 | export default { 7 | def: cosmeticOutfitCreate, 8 | async handle(client, packet) { 9 | const { 10 | name, 11 | skin_id: skinId, 12 | equipped_cosmetics: equippedCosmetics, 13 | cosmetic_settings: cosmeticSettings, 14 | } = packet.body!; 15 | const newOutfit = ( 16 | await db 17 | .insert(outfits) 18 | .values({ 19 | ownerId: client.profile.id, 20 | name, 21 | skinId, 22 | equippedCosmetics, 23 | cosmeticSettings, 24 | selected: false, 25 | }) 26 | .returning({ id: outfits.id }) 27 | )[0]!; 28 | 29 | await client.resendOutfits(packet.uuid, [newOutfit.id]); 30 | return { cancelled: true }; 31 | }, 32 | } as Handler; 33 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/outfit/c2uCosmeticOutfitDelete.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { outfits } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticOutfitDelete from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitDelete.js"; 6 | 7 | export default { 8 | def: cosmeticOutfitDelete, 9 | async handle(client, packet) { 10 | const { id } = packet.body!; 11 | 12 | await db 13 | .delete(outfits) 14 | .where(and(eq(outfits.id, id), eq(outfits.ownerId, client.profile.id))); 15 | await client.sendResponse(packet); 16 | 17 | return { cancelled: true }; 18 | }, 19 | } as Handler; 20 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/outfit/c2uCosmeticOutfitEquippedCosmeticsUpdate.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { outfits } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticOutfitEquippedCosmeticsUpdate from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitEquippedCosmeticsUpdate.js"; 6 | 7 | export default { 8 | def: cosmeticOutfitEquippedCosmeticsUpdate, 9 | async handle(client, packet) { 10 | const { a: outfitId, b: slot, c: cosmetic } = packet.body!; 11 | 12 | const outfit = ( 13 | await db 14 | .select() 15 | .from(outfits) 16 | .where( 17 | and(eq(outfits.id, outfitId), eq(outfits.ownerId, client.profile.id)) 18 | ) 19 | .limit(1) 20 | )[0]; 21 | if (!outfit) 22 | return await client.sendResponse(packet, false, "Outfit not found"); 23 | 24 | (outfit!.equippedCosmetics as any)[slot] = cosmetic; 25 | 26 | await db 27 | .update(outfits) 28 | .set({ equippedCosmetics: outfit!.equippedCosmetics }) 29 | .where( 30 | and(eq(outfits.id, outfitId), eq(outfits.ownerId, client.profile.id)) 31 | ); 32 | await client.sendResponse(packet); 33 | 34 | await client.sendOutfitToSubscribers(); 35 | return { cancelled: true }; 36 | }, 37 | } as Handler; 38 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/outfit/c2uCosmeticOutfitNameUpdate.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { outfits } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticOutfitNameUpdate from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitNameUpdate.js"; 6 | 7 | export default { 8 | def: cosmeticOutfitNameUpdate, 9 | async handle(client, packet) { 10 | const { id, name } = packet.body!; 11 | 12 | await db 13 | .update(outfits) 14 | .set({ name }) 15 | .where(and(eq(outfits.id, id), eq(outfits.ownerId, client.profile.id))); 16 | await client.sendResponse(packet); 17 | return { cancelled: true }; 18 | }, 19 | } as Handler; 20 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/outfit/c2uCosmeticOutfitSelect.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import cosmeticOutfitSelect from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitSelect.js"; 3 | 4 | export default { 5 | def: cosmeticOutfitSelect, 6 | async handle(client, packet) { 7 | const { a: id } = packet.body!; 8 | 9 | await client.selectOutfit(id); 10 | await client.sendResponse(packet); 11 | return { cancelled: true }; 12 | }, 13 | } as Handler; 14 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/outfit/c2uCosmeticOutfitSkinUpdate.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { outfits } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticOutfitSkinUpdate from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitSkinUpdate.js"; 6 | 7 | export default { 8 | def: cosmeticOutfitSkinUpdate, 9 | async handle(client, packet) { 10 | const { a: outfitId, b: skinTexture, c: skinId } = packet.body!; 11 | console.log(skinId, skinTexture); 12 | 13 | const outfit = ( 14 | await db 15 | .select() 16 | .from(outfits) 17 | .where( 18 | and(eq(outfits.id, outfitId), eq(outfits.ownerId, client.profile.id)) 19 | ) 20 | .limit(1) 21 | )[0]; 22 | 23 | if (!outfit) 24 | return await client.sendResponse(packet, false, "Outfit not found"); 25 | 26 | await db 27 | .update(outfits) 28 | .set({ skinId, skinTexture }) 29 | .where( 30 | and(eq(outfits.id, outfitId), eq(outfits.ownerId, client.profile.id)) 31 | ); 32 | 33 | await client.sendResponse(packet); 34 | 35 | await client.sendOutfitToSubscribers(); 36 | 37 | return { cancelled: true }; 38 | }, 39 | } as Handler; 40 | -------------------------------------------------------------------------------- /src/handlers/c2u/cosmetic/outfit/c2uCosmeticOutfitUpdateFavoriteState.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { outfits } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticOutfitUpdateFavoriteState from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitUpdateFavoriteState.js"; 6 | 7 | export default { 8 | def: cosmeticOutfitUpdateFavoriteState, 9 | async handle(client, packet) { 10 | const { id, state } = packet.body!; 11 | 12 | await db 13 | .update(outfits) 14 | .set({ favoritedAt: state ? new Date() : null }) 15 | .where(and(eq(outfits.id, id), eq(outfits.ownerId, client.profile.id))); 16 | await client.sendResponse(packet); 17 | return { cancelled: true }; 18 | }, 19 | } as Handler; 20 | -------------------------------------------------------------------------------- /src/handlers/c2u/mod/c2uModsAnnounceHandler.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import modsAnnounce from "~/protocol/packets/mod/modsAnnounce.js"; 3 | export default { 4 | def: modsAnnounce, 5 | async handle() { 6 | return { cancelled: true }; 7 | }, 8 | } as Handler; 9 | -------------------------------------------------------------------------------- /src/handlers/c2u/subscription/c2uSubscriptionUpdatePacket.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import responseActionPacket from "~/protocol/packets/response/responseActionPacket.js"; 3 | import subscriptionUpdatePacket from "~/protocol/packets/subscription/subscriptionUpdatePacket.js"; 4 | export default { 5 | def: subscriptionUpdatePacket, 6 | async handle(client, packet) { 7 | const { a: uuids, b: unsubscribeFromAll, c: subscribe } = packet.body!; 8 | if (unsubscribeFromAll) { 9 | for (const subscription of client.subscribedTo) { 10 | await client.unsubscribe(subscription); 11 | } 12 | 13 | await client.sendClientPacket({ 14 | uuid: packet.uuid, 15 | className: responseActionPacket.className, 16 | body: { a: true }, 17 | }); 18 | return; 19 | } 20 | 21 | for (const newSubscription of uuids ?? []) { 22 | subscribe 23 | ? await client.subscribe(newSubscription) 24 | : await client.unsubscribe(newSubscription); 25 | } 26 | 27 | await client.sendClientPacket({ 28 | uuid: packet.uuid, 29 | className: responseActionPacket.className, 30 | body: { a: true }, 31 | }); 32 | }, 33 | } as Handler; 34 | -------------------------------------------------------------------------------- /src/handlers/c2u/telemetry/c2uTelemetryHandler.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import telemetry from "~/protocol/packets/telemetry/telemetry.js"; 3 | export default { 4 | def: telemetry, 5 | async handle() { 6 | return { cancelled: true }; 7 | }, 8 | } as Handler; 9 | -------------------------------------------------------------------------------- /src/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Client } from "~/client.js"; 3 | import { Packet } from "~/protocol/index.js"; 4 | import { PacketDefinition } from "~/protocol/packets.js"; 5 | import c2uRegisterPacketTypeIdHandler from "./c2u/connection/c2uRegisterPacketTypeIdHandler.js"; 6 | import c2uCosmeticAnimationTrigger from "./c2u/cosmetic/c2uCosmeticAnimationTrigger.js"; 7 | import c2uCosmeticEmoteWheelSelect from "./c2u/cosmetic/emote/c2uCosmeticEmoteWheelSelect.js"; 8 | import c2uCosmeticEmoteWheelUpdate from "./c2u/cosmetic/emote/c2uCosmeticEmoteWheelUpdate.js"; 9 | import c2uCosmeticOutfitCosmeticSettingsUpdate from "./c2u/cosmetic/outfit/c2uCosmeticOutfitCosmeticSettingsUpdate.js"; 10 | import c2uCosmeticOutfitCreate from "./c2u/cosmetic/outfit/c2uCosmeticOutfitCreate.js"; 11 | import c2uCosmeticOutfitDelete from "./c2u/cosmetic/outfit/c2uCosmeticOutfitDelete.js"; 12 | import c2uCosmeticOutfitEquippedCosmeticsUpdate from "./c2u/cosmetic/outfit/c2uCosmeticOutfitEquippedCosmeticsUpdate.js"; 13 | import c2uCosmeticOutfitNameUpdate from "./c2u/cosmetic/outfit/c2uCosmeticOutfitNameUpdate.js"; 14 | import c2uCosmeticOutfitSelect from "./c2u/cosmetic/outfit/c2uCosmeticOutfitSelect.js"; 15 | import c2uCosmeticOutfitSkinUpdate from "./c2u/cosmetic/outfit/c2uCosmeticOutfitSkinUpdate.js"; 16 | import c2uCosmeticOutfitUpdateFavoriteState from "./c2u/cosmetic/outfit/c2uCosmeticOutfitUpdateFavoriteState.js"; 17 | import c2uModsAnnounceHandler from "./c2u/mod/c2uModsAnnounceHandler.js"; 18 | import c2uSubscriptionUpdatePacket from "./c2u/subscription/c2uSubscriptionUpdatePacket.js"; 19 | import c2uTelemetry from "./c2u/telemetry/c2uTelemetryHandler.js"; 20 | import u2cRegisterPacketTypeIdHandler from "./u2c/connection/u2cRegisterPacketTypeIdHandler.js"; 21 | import u2cCosmeticEmoteWheelPopulate from "./u2c/cosmetic/emote/u2cCosmeticEmoteWheelPopulate.js"; 22 | import u2cCosmeticOutfitPopulate from "./u2c/cosmetic/outfit/u2cCosmeticOutfitPopulate.js"; 23 | import u2cCosmeticOutfitSelectedResponse from "./u2c/cosmetic/outfit/u2cCosmeticOutfitSelectedResponse.js"; 24 | import u2cCosmeticsPopulateHandler from "./u2c/cosmetic/u2cCosmeticsPopulateHandler.js"; 25 | import u2cCosmeticsUserUnlockedHandler from "./u2c/cosmetic/u2cCosmeticsUserUnlockedHandler.js"; 26 | import u2cNoticesPopulateHandler from "./u2c/notices/u2cNoticesPopulateHandler.js"; 27 | export type Handler> = { 28 | def: T; 29 | handle: ( 30 | client: Client, 31 | packet: Packet> 32 | ) => Promise> | { cancelled: boolean } | undefined>; 33 | }; 34 | 35 | export const clientToUpstreamHandlers = [ 36 | c2uRegisterPacketTypeIdHandler, 37 | c2uModsAnnounceHandler, 38 | c2uTelemetry, 39 | c2uSubscriptionUpdatePacket, 40 | c2uCosmeticAnimationTrigger, 41 | c2uCosmeticOutfitEquippedCosmeticsUpdate, 42 | c2uCosmeticOutfitCosmeticSettingsUpdate, 43 | c2uCosmeticOutfitDelete, 44 | c2uCosmeticOutfitCreate, 45 | c2uCosmeticOutfitSelect, 46 | c2uCosmeticOutfitNameUpdate, 47 | c2uCosmeticOutfitSkinUpdate, 48 | 49 | c2uCosmeticOutfitUpdateFavoriteState, 50 | c2uCosmeticEmoteWheelUpdate, 51 | c2uCosmeticEmoteWheelSelect, 52 | ]; 53 | export const upstreamToClientHandlers = [ 54 | u2cRegisterPacketTypeIdHandler, 55 | u2cCosmeticsPopulateHandler, 56 | u2cCosmeticsUserUnlockedHandler, 57 | u2cCosmeticOutfitPopulate, 58 | u2cCosmeticOutfitSelectedResponse, 59 | u2cCosmeticEmoteWheelPopulate, 60 | u2cNoticesPopulateHandler, 61 | ]; 62 | -------------------------------------------------------------------------------- /src/handlers/u2c/connection/u2cRegisterPacketTypeIdHandler.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import registerPacketTypeId from "~/protocol/packets/connection/registerPacketTypeId.js"; 3 | export default { 4 | def: registerPacketTypeId, 5 | async handle(client, packet) { 6 | const { body } = packet; 7 | client.clientPackets[body!.b] = body!.a; 8 | }, 9 | } as Handler; 10 | -------------------------------------------------------------------------------- /src/handlers/u2c/cosmetic/emote/u2cCosmeticEmoteWheelPopulate.ts: -------------------------------------------------------------------------------- 1 | import { asc, count, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { emoteWheels } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticEmoteWheelPopulate from "~/protocol/packets/cosmetic/emote/cosmeticEmoteWheelPopulate.js"; 6 | 7 | export default { 8 | def: cosmeticEmoteWheelPopulate, 9 | async handle(client, packet) { 10 | if ( 11 | ( 12 | await db 13 | .select({ value: count() }) 14 | .from(emoteWheels) 15 | .where(eq(emoteWheels.ownerId, client.profile.id)) 16 | ).reduce((partialSum, a) => partialSum + a.value, 0) == 0 17 | ) { 18 | await db.insert(emoteWheels).values( 19 | packet.body!.a.map((o) => ({ 20 | ownerId: client.profile.id, 21 | selected: o.b, 22 | slots: o.c, 23 | createdAt: new Date(o.d), 24 | })) 25 | ); 26 | } 27 | 28 | const emoteWheelList = await db 29 | .select() 30 | .from(emoteWheels) 31 | .where(eq(emoteWheels.ownerId, client.profile.id)) 32 | .orderBy(asc(emoteWheels.createdAt)); 33 | 34 | packet.body!.a = emoteWheelList.map((o) => ({ 35 | a: o.id, 36 | b: o.selected, 37 | c: o.slots as any, 38 | d: o.createdAt.getTime(), 39 | })); 40 | return packet; 41 | }, 42 | } as Handler; 43 | -------------------------------------------------------------------------------- /src/handlers/u2c/cosmetic/outfit/u2cCosmeticOutfitPopulate.ts: -------------------------------------------------------------------------------- 1 | import { asc, count, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { outfits } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticOutfitPopulate from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitPopulate.js"; 6 | 7 | export default { 8 | def: cosmeticOutfitPopulate, 9 | async handle(client, packet) { 10 | if ( 11 | ( 12 | await db 13 | .select({ value: count() }) 14 | .from(outfits) 15 | .where(eq(outfits.ownerId, client.profile.id)) 16 | ).reduce((partialSum, a) => partialSum + a.value, 0) == 0 17 | ) { 18 | await db.insert(outfits).values( 19 | packet.body!.outfits.map((o) => ({ 20 | ownerId: client.profile.id, 21 | name: o.b, 22 | skinTexture: o.c, 23 | equippedCosmetics: o.d, 24 | cosmeticSettings: o.e, 25 | selected: o.f, 26 | favoritedAt: o.h ? new Date(o.h) : null, 27 | skinId: o.j, 28 | })) 29 | ); 30 | } 31 | 32 | const outfitList = await db 33 | .select() 34 | .from(outfits) 35 | .where(eq(outfits.ownerId, client.profile.id)) 36 | .orderBy(asc(outfits.createdAt)); 37 | 38 | packet.body!.outfits = outfitList.map((o) => ({ 39 | a: o.id, 40 | b: o.name, 41 | c: o.skinTexture, 42 | d: o.equippedCosmetics as any, 43 | e: o.cosmeticSettings as any, 44 | f: o.selected, 45 | g: o.createdAt.getTime(), 46 | h: o.favoritedAt?.getTime(), 47 | j: o.skinId, 48 | })); 49 | return packet; 50 | }, 51 | } as Handler; 52 | -------------------------------------------------------------------------------- /src/handlers/u2c/cosmetic/outfit/u2cCosmeticOutfitSelectedResponse.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { db } from "~/db/index.js"; 3 | import { outfits } from "~/db/schema.js"; 4 | import { Handler } from "~/handlers/index.js"; 5 | import cosmeticOutfitSelectedResponse from "~/protocol/packets/cosmetic/outfit/cosmeticOutfitSelectedResponse.js"; 6 | 7 | export default { 8 | def: cosmeticOutfitSelectedResponse, 9 | async handle(client, packet) { 10 | const { uuid } = packet.body!; 11 | const selectedOutfit = ( 12 | await db 13 | .select() 14 | .from(outfits) 15 | .where(and(eq(outfits.ownerId, uuid), eq(outfits.selected, true))) 16 | )[0]; 17 | 18 | if (!selectedOutfit) return; 19 | 20 | packet.body!.cosmeticSettings = selectedOutfit.cosmeticSettings as any; 21 | packet.body!.equippedCosmetics = selectedOutfit.equippedCosmetics as any; 22 | packet.body!.skinTexture = selectedOutfit.skinTexture; 23 | 24 | return packet; 25 | }, 26 | } as Handler; 27 | -------------------------------------------------------------------------------- /src/handlers/u2c/cosmetic/u2cCosmeticsPopulateHandler.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import { addCosmeticIds } from "~/index.js"; 3 | import cosmeticsPopulate from "~/protocol/packets/cosmetic/cosmeticsPopulate.js"; 4 | 5 | export default { 6 | def: cosmeticsPopulate, 7 | async handle(client, packet) { 8 | addCosmeticIds( 9 | // this is we dont have emote support yet, why? because i dont want to handle subscriptions yet 10 | packet.body!.a.map((a) => a.a) 11 | ); 12 | }, 13 | } as Handler; 14 | -------------------------------------------------------------------------------- /src/handlers/u2c/cosmetic/u2cCosmeticsUserUnlockedHandler.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import { cosmeticIds } from "~/index.js"; 3 | import cosmeticsUserUnlocked from "~/protocol/packets/cosmetic/cosmeticsUserUnlocked.js"; 4 | 5 | export default { 6 | def: cosmeticsUserUnlocked, 7 | async handle(client, packet) { 8 | for (const cosmeticId of cosmeticIds) { 9 | packet.body!.d[cosmeticId] = { 10 | gifted_by: null, 11 | unlocked_at: Date.now(), 12 | wardrobe_unlock: true, 13 | }; 14 | } 15 | 16 | return packet; 17 | }, 18 | } as Handler; 19 | -------------------------------------------------------------------------------- /src/handlers/u2c/notices/u2cNoticesPopulateHandler.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "~/handlers/index.js"; 2 | import noticesPopulate from "~/protocol/packets/notices/noticesPopulate.js"; 3 | export default { 4 | def: noticesPopulate, 5 | async handle(client, packet) { 6 | packet.body!.a.push({ 7 | a: "pixie", 8 | b: "SALE", 9 | c: { 10 | sale_name: "pixie.rip", 11 | sale_name_compact: "pixie", 12 | discount: 100, 13 | display_time: false, 14 | tooltip: "Pixie is active\n and has given\n you every cosmetic.", 15 | }, 16 | d: false, 17 | e: Date.now(), 18 | f: 95646663366000, 19 | }); 20 | return packet; 21 | }, 22 | } as Handler; 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "./client.js"; 2 | import { env } from "./env.js"; 3 | import { generalHttpClient } from "./utils/http.js"; 4 | import { WebSocket } from "ws"; 5 | import { SocksProxyAgent } from "socks-proxy-agent"; 6 | 7 | export let cosmeticIds = new Set(); 8 | 9 | export const addCosmeticIds = (ids: string[]) => 10 | ids.forEach((id) => cosmeticIds.add(id)); 11 | export type WebSocketData = { 12 | client: Client; 13 | }; 14 | Bun.serve({ 15 | port: env.PORT, 16 | async fetch(req, server) { 17 | const authHeader = req.headers.get("authorization"); 18 | if (!authHeader || !authHeader.startsWith("Basic ")) { 19 | return new Response(null, { status: 401 }); 20 | } 21 | 22 | const userPassRaw = atob(authHeader.split(" ")[1]!); 23 | if (!userPassRaw.includes(":")) { 24 | return new Response(null, { status: 401 }); 25 | } 26 | 27 | const username = userPassRaw.split(":")[0]; 28 | console.log(username + " sent us a connect request!"); 29 | const profile: { id: string; name: string } = await generalHttpClient 30 | .get("https://mowojang.matdoes.dev/" + username) 31 | .json(); 32 | 33 | console.log(username + " (" + profile.id + ") is connecting"); 34 | 35 | profile.id = profile.id.replace( 36 | /(.{8})(.{4})(.{4})(.{4})(.{12})/, 37 | "$1-$2-$3-$4-$5" 38 | ); 39 | 40 | const upstreamWs = new WebSocket( 41 | env.WS_PROXY ?? "wss://connect.essential.gg/v1", 42 | { 43 | headers: { 44 | "essential-max-protocol-version": 45 | req.headers.get("essential-max-protocol-version") ?? undefined, 46 | authorization: req.headers.get("authorization") ?? undefined, 47 | "user-agent": req.headers.get("user-agent") ?? undefined, 48 | }, 49 | } 50 | ); 51 | const client = new Client(profile, upstreamWs); 52 | 53 | upstreamWs.addEventListener("message", (event) => { 54 | if (!(event.data instanceof Buffer)) return; 55 | if (client.initialized) return; 56 | client.startupPackets.push(event.data); 57 | }); 58 | 59 | upstreamWs.addEventListener("close", console.log); 60 | upstreamWs.addEventListener("error", console.log); 61 | 62 | await new Promise((resolve) => 63 | upstreamWs.addEventListener("open", resolve) 64 | ); 65 | 66 | if ( 67 | server.upgrade(req, { 68 | headers: { 69 | "essential-protocol-version": "5", 70 | }, 71 | data: { 72 | client, 73 | }, 74 | }) 75 | ) { 76 | return; // do not return a Response 77 | } 78 | return new Response("Upgrade failed", { status: 500 }); 79 | }, 80 | websocket: { 81 | open: async (ws) => await ws.data.client.open(ws), 82 | message: async (ws, message) => { 83 | if (message instanceof Buffer) 84 | await ws.data.client.onClientMessage(message); 85 | }, 86 | close: async (ws) => ws.data.client.close(), 87 | }, // handlers 88 | }); 89 | 90 | console.log("Listening on " + env.PORT); 91 | -------------------------------------------------------------------------------- /src/protocol/common.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | export const cosmeticSlot = z.enum([ 3 | "BACK", 4 | "EARS", 5 | "FACE", 6 | "FULL_BODY", 7 | "HAT", 8 | "PET", 9 | "TAIL", 10 | "ARMS", 11 | "SHOULDERS", 12 | "SUITS", 13 | "SHOES", 14 | "PANTS", 15 | "WINGS", 16 | "EFFECT", 17 | "CAPE", 18 | "EMOTE", 19 | "ICON", 20 | "TOP", 21 | "ACCESSORY", 22 | "HEAD", 23 | ]); 24 | 25 | export const cosmeticSetting = z.object({ 26 | a: z.string().nullish(), // id 27 | b: z.string(), // type 28 | c: z.boolean(), // enabled 29 | d: z.record(z.string(), z.any()), // data 30 | }); 31 | export const cosmeticOutfit = z.object({ 32 | a: z.string(), // id 33 | b: z.string(), // name 34 | c: z.string().nullish(), // skin texture 35 | d: z.record(cosmeticSlot, z.string()).nullish(), // equipped cosmetics 36 | e: z.record(z.string(), z.array(cosmeticSetting)).nullish(), // cosmetic settings 37 | f: z.boolean(), // selected 38 | g: z.number(), // created at 39 | h: z.number().nullish(), // favorited at, 40 | j: z.string().nullish(), // skin id 41 | }); 42 | export const emoteWheel = z.object({ 43 | a: z.string(), // id 44 | b: z.boolean(), // selected 45 | c: z.record(z.string(), z.string()), // slots 46 | d: z.number(), // created at 47 | e: z.number().nullish(), // updated at 48 | }); 49 | export const notice = z.object({ 50 | a: z.string(), // id 51 | b: z.enum([ 52 | "NEW_BANNER", 53 | "SALE", 54 | "FRIEND_REQUEST_TOAST", 55 | "FRIEND_REQUEST_NEW_INDICATOR", 56 | "DISMISSIBLE_TOAST", 57 | "WARDROBE_BANNER", 58 | "GIFTED_COSMETIC_TOAST", 59 | ]), // type 60 | c: z.record(z.string(), z.any()), // metadata 61 | d: z.boolean(), // dismissible 62 | e: z.number().nullish(), // active after 63 | f: z.number().nullish(), // expires at 64 | }); 65 | 66 | function clamp(num: number, min: number, max: number) { 67 | return num <= min ? min : num >= max ? max : num; 68 | } 69 | 70 | export const readString = (view: DataView, offset: number) => { 71 | const length = view.getInt32(offset); 72 | offset += 4; 73 | 74 | const byteArray = new Uint8Array( 75 | view.buffer, 76 | offset, 77 | clamp(length, 0, view.buffer.byteLength - offset) 78 | ); 79 | 80 | return new TextDecoder().decode(byteArray); 81 | }; 82 | 83 | export const writeString = (view: DataView, offset: number, str: string) => { 84 | view.setInt32(offset, str.length); 85 | offset += 4; 86 | 87 | new Uint8Array(view.buffer, offset, str.length).set( 88 | new TextEncoder().encode(str) 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /src/protocol/index.ts: -------------------------------------------------------------------------------- 1 | import { env } from "~/env.js"; 2 | import { readString, writeString } from "./common.js"; 3 | import { packetDefinitions } from "./packets.js"; 4 | 5 | export type Packet = { 6 | className: string; 7 | uuid?: string; 8 | body?: T; 9 | }; 10 | export function bigIntReviver(key: string, value: any): any { 11 | if (typeof value === "string" && value.startsWith("bigint:")) { 12 | return BigInt(value.substring("bigint:".length)); 13 | } 14 | return value; 15 | } 16 | 17 | (BigInt.prototype as any).toJSON = function () { 18 | return "bigint:" + this.toString(); 19 | }; 20 | 21 | export const decodePacket = ( 22 | packetIdMap: Record, 23 | buffer: Buffer 24 | ) => { 25 | try { 26 | const view = new DataView(buffer.buffer); 27 | const id = view.getInt32(0); 28 | const className = packetIdMap[id]; 29 | 30 | if (!className) return null; 31 | const uuid = readString(view, 4); 32 | const bodyStr = readString(view, 8 + uuid.length).replace( 33 | /:\d{15,}/g, 34 | (num) => ':"bigint' + num + '"' 35 | ); 36 | if (env.PACKET_LOG == "yes") 37 | console.log(className + "[" + id + "]: " + bodyStr); 38 | const packetDefinition = packetDefinitions.find( 39 | (a) => a.className == className 40 | ); 41 | 42 | if (!packetDefinition) return null; 43 | 44 | const body = packetDefinition.body.safeParse( 45 | JSON.parse(bodyStr, bigIntReviver) 46 | ).data; 47 | 48 | if (!body) return null; 49 | 50 | return { 51 | className, 52 | uuid, 53 | body, 54 | } as Packet; 55 | } catch (error) { 56 | return null; 57 | } 58 | }; 59 | 60 | export const encodePacket = ( 61 | packetIdMap: Record, 62 | packet: Packet 63 | ) => { 64 | try { 65 | const id = packetIdMap[packet.className]; 66 | if (!id) return null; 67 | const bodyStr = JSON.stringify(packet.body).replace( 68 | /"bigint:(\d{15,})"/g, 69 | (_, num) => num 70 | ); 71 | 72 | const view = new DataView( 73 | Buffer.alloc( 74 | 4 + // id 75 | 4 + // uuid len 76 | (packet.uuid ?? "").length + // uuid 77 | 4 + // body len 78 | bodyStr.length // body 79 | ).buffer 80 | ); 81 | 82 | if (env.PACKET_LOG == "yes") 83 | console.log("P->C: " + packet.className + "[" + id + "]: " + bodyStr); 84 | 85 | view.setInt32(0, parseInt(id as any)); 86 | writeString(view, 4, packet.uuid ?? ""); 87 | writeString(view, 8 + (packet.uuid?.length ?? 0), bodyStr); 88 | 89 | return view.buffer; 90 | } catch (error) { 91 | return null; 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/protocol/packets.ts: -------------------------------------------------------------------------------- 1 | import { ZodObject, ZodType } from "zod"; 2 | import registerPacketTypeId from "./packets/connection/registerPacketTypeId.js"; 3 | import clientCosmeticAnimationTrigger from "./packets/cosmetic/clientCosmeticAnimationTrigger.js"; 4 | import cosmeticsPopulate from "./packets/cosmetic/cosmeticsPopulate.js"; 5 | import cosmeticsUserUnlocked from "./packets/cosmetic/cosmeticsUserUnlocked.js"; 6 | import cosmeticEmoteWheelPopulate from "./packets/cosmetic/emote/cosmeticEmoteWheelPopulate.js"; 7 | import cosmeticEmoteWheelSelect from "./packets/cosmetic/emote/cosmeticEmoteWheelSelect.js"; 8 | import cosmeticEmoteWheelUpdate from "./packets/cosmetic/emote/cosmeticEmoteWheelUpdate.js"; 9 | import cosmeticOutfitCosmeticSettingsUpdate from "./packets/cosmetic/outfit/cosmeticOutfitCosmeticSettingsUpdate.js"; 10 | import cosmeticOutfitCreate from "./packets/cosmetic/outfit/cosmeticOutfitCreate.js"; 11 | import cosmeticOutfitDelete from "./packets/cosmetic/outfit/cosmeticOutfitDelete.js"; 12 | import cosmeticOutfitEquippedCosmeticsUpdate from "./packets/cosmetic/outfit/cosmeticOutfitEquippedCosmeticsUpdate.js"; 13 | import cosmeticOutfitNameUpdate from "./packets/cosmetic/outfit/cosmeticOutfitNameUpdate.js"; 14 | import cosmeticOutfitPopulate from "./packets/cosmetic/outfit/cosmeticOutfitPopulate.js"; 15 | import cosmeticOutfitSelect from "./packets/cosmetic/outfit/cosmeticOutfitSelect.js"; 16 | import cosmeticOutfitSelectedResponse from "./packets/cosmetic/outfit/cosmeticOutfitSelectedResponse.js"; 17 | import cosmeticOutfitSkinUpdate from "./packets/cosmetic/outfit/cosmeticOutfitSkinUpdate.js"; 18 | import cosmeticOutfitUpdateFavoriteState from "./packets/cosmetic/outfit/cosmeticOutfitUpdateFavoriteState.js"; 19 | import serverCosmeticAnimationTrigger from "./packets/cosmetic/serverCosmeticAnimationTrigger.js"; 20 | import modsAnnounce from "./packets/mod/modsAnnounce.js"; 21 | import noticesPopulate from "./packets/notices/noticesPopulate.js"; 22 | import responseActionPacket from "./packets/response/responseActionPacket.js"; 23 | import subscriptionUpdatePacket from "./packets/subscription/subscriptionUpdatePacket.js"; 24 | import telemetry from "./packets/telemetry/telemetry.js"; 25 | 26 | export type PacketDefinition> = { 27 | className: string; 28 | body: Schema; 29 | }; 30 | 31 | export const packetDefinitions: PacketDefinition>[] = [ 32 | registerPacketTypeId, 33 | modsAnnounce, 34 | telemetry, 35 | subscriptionUpdatePacket, 36 | responseActionPacket, 37 | 38 | cosmeticsUserUnlocked, 39 | cosmeticsPopulate, 40 | serverCosmeticAnimationTrigger, 41 | clientCosmeticAnimationTrigger, 42 | cosmeticOutfitPopulate, 43 | cosmeticOutfitEquippedCosmeticsUpdate, 44 | cosmeticOutfitCosmeticSettingsUpdate, 45 | cosmeticOutfitDelete, 46 | cosmeticOutfitCreate, 47 | cosmeticOutfitSelect, 48 | cosmeticOutfitNameUpdate, 49 | cosmeticOutfitSkinUpdate, 50 | cosmeticOutfitUpdateFavoriteState, 51 | cosmeticOutfitSelectedResponse, 52 | cosmeticEmoteWheelPopulate, 53 | cosmeticEmoteWheelUpdate, 54 | cosmeticEmoteWheelSelect, 55 | 56 | noticesPopulate, 57 | ]; 58 | -------------------------------------------------------------------------------- /src/protocol/packets/connection/registerPacketTypeId.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | a: z.string(), 6 | b: z.number(), 7 | }); 8 | 9 | export default { 10 | className: "connection.ConnectionRegisterPacketTypeIdPacket", 11 | body: schema, 12 | } as PacketDefinition; 13 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/clientCosmeticAnimationTrigger.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { cosmeticSlot } from "~/protocol/common.js"; 3 | import { PacketDefinition } from "~/protocol/packets.js"; 4 | 5 | const schema = z.object({ 6 | a: cosmeticSlot, 7 | b: z.string() 8 | }); 9 | 10 | export default { 11 | className: "cosmetic.ClientCosmeticAnimationTriggerPacket", 12 | body: schema, 13 | } as PacketDefinition; 14 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/cosmeticsPopulate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | a: z.array( 6 | z.object({ 7 | a: z.string(), 8 | b: z.string(), 9 | }) 10 | ), 11 | }); 12 | 13 | export default { 14 | className: "cosmetic.ServerCosmeticsPopulatePacket", 15 | body: schema, 16 | } as PacketDefinition; 17 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/cosmeticsUserUnlocked.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | b: z.boolean(), 6 | c: z.string().uuid().nullish(), 7 | d: z.record( 8 | z.string(), 9 | z.object({ 10 | unlocked_at: z.number(), 11 | gifted_by: z.string().uuid().nullish(), 12 | wardrobe_unlock: z.boolean(), 13 | }) 14 | ), 15 | }); 16 | 17 | export default { 18 | className: "cosmetic.ServerCosmeticsUserUnlockedPacket", 19 | body: schema, 20 | } as PacketDefinition; 21 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/emote/cosmeticEmoteWheelPopulate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { emoteWheel } from "~/protocol/common.js"; 3 | import { PacketDefinition } from "~/protocol/packets.js"; 4 | 5 | const schema = z.object({ 6 | a: z.array(emoteWheel), 7 | }); 8 | 9 | export default { 10 | className: "cosmetic.emote.ServerCosmeticEmoteWheelPopulatePacket", 11 | body: schema, 12 | } as PacketDefinition; 13 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/emote/cosmeticEmoteWheelSelect.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | a: z.string(), // id 6 | }); 7 | 8 | export default { 9 | className: "cosmetic.emote.ClientCosmeticEmoteWheelSelectPacket", 10 | body: schema, 11 | } as PacketDefinition; 12 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/emote/cosmeticEmoteWheelUpdate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | a: z.string(), // id 6 | b: z.number(), // index 7 | c: z.string().nullish(), // value 8 | }); 9 | 10 | export default { 11 | className: "cosmetic.emote.ClientCosmeticEmoteWheelUpdatePacket", 12 | body: schema, 13 | } as PacketDefinition; 14 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitCosmeticSettingsUpdate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { cosmeticSetting } from "~/protocol/common.js"; 3 | import { PacketDefinition } from "~/protocol/packets.js"; 4 | 5 | const schema = z.object({ 6 | a: z.string(), // outfit id 7 | b: z.string(), // cosmetic id 8 | c: z.array(cosmeticSetting), // settings 9 | }); 10 | 11 | export default { 12 | className: "cosmetic.outfit.ClientCosmeticOutfitCosmeticSettingsUpdatePacket", 13 | body: schema, 14 | } as PacketDefinition; 15 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitCreate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { cosmeticSetting, cosmeticSlot } from "~/protocol/common.js"; 3 | import { PacketDefinition } from "~/protocol/packets.js"; 4 | 5 | const schema = z.object({ 6 | name: z.string(), 7 | skin_id: z.string(), 8 | equipped_cosmetics: z.record(cosmeticSlot, z.string()), 9 | cosmetic_settings: z.record(z.string(), z.array(cosmeticSetting)), 10 | }); 11 | 12 | export default { 13 | className: "cosmetic.outfit.ClientCosmeticOutfitCreatePacket", 14 | body: schema, 15 | } as PacketDefinition; 16 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitDelete.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | id: z.string(), 6 | }); 7 | 8 | export default { 9 | className: "cosmetic.outfit.ClientCosmeticOutfitDeletePacket", 10 | body: schema, 11 | } as PacketDefinition; 12 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitEquippedCosmeticsUpdate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { cosmeticSlot } from "~/protocol/common.js"; 3 | import { PacketDefinition } from "~/protocol/packets.js"; 4 | 5 | const schema = z.object({ 6 | a: z.string(), // outfit id 7 | b: cosmeticSlot, // slot 8 | c: z.string().nullish(), // cosmetic id 9 | }); 10 | 11 | export default { 12 | className: 13 | "cosmetic.outfit.ClientCosmeticOutfitEquippedCosmeticsUpdatePacket", 14 | body: schema, 15 | } as PacketDefinition; 16 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitNameUpdate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | id: z.string(), 6 | name: z.string().max(22), 7 | }); 8 | 9 | export default { 10 | className: "cosmetic.outfit.ClientCosmeticOutfitNameUpdatePacket", 11 | body: schema, 12 | } as PacketDefinition; 13 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitPopulate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { cosmeticOutfit } from "~/protocol/common.js"; 3 | import { PacketDefinition } from "~/protocol/packets.js"; 4 | 5 | const schema = z.object({ 6 | outfits: z.array(cosmeticOutfit), 7 | }); 8 | 9 | export default { 10 | className: "cosmetic.outfit.ServerCosmeticOutfitPopulatePacket", 11 | body: schema, 12 | } as PacketDefinition; 13 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitSelect.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | a: z.string(), // outfit id 6 | }); 7 | 8 | export default { 9 | className: "cosmetic.outfit.ClientCosmeticOutfitSelectPacket", 10 | body: schema, 11 | } as PacketDefinition; 12 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitSelectedResponse.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { cosmeticSetting, cosmeticSlot } from "~/protocol/common.js"; 3 | import { PacketDefinition } from "~/protocol/packets.js"; 4 | 5 | const schema = z.object({ 6 | uuid: z.string().uuid(), 7 | skinTexture: z.string().nullish(), 8 | equippedCosmetics: z.record(cosmeticSlot, z.string()).nullish(), 9 | cosmeticSettings: z.record(z.string(), z.array(cosmeticSetting)).nullish(), 10 | }); 11 | 12 | export default { 13 | className: "cosmetic.outfit.ServerCosmeticOutfitSelectedResponsePacket", 14 | body: schema, 15 | } as PacketDefinition; 16 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitSkinUpdate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | a: z.string(), // outfit id 6 | b: z.string().nullish(), // skin texture 7 | c: z.string().nullish(), // skin id 8 | }); 9 | 10 | export default { 11 | className: "cosmetic.outfit.ClientCosmeticOutfitSkinUpdatePacket", 12 | body: schema, 13 | } as PacketDefinition; 14 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/outfit/cosmeticOutfitUpdateFavoriteState.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | id: z.string(), 6 | state: z.boolean(), 7 | }); 8 | 9 | export default { 10 | className: "cosmetic.outfit.ClientCosmeticOutfitUpdateFavoriteStatePacket", 11 | body: schema, 12 | } as PacketDefinition; 13 | -------------------------------------------------------------------------------- /src/protocol/packets/cosmetic/serverCosmeticAnimationTrigger.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { cosmeticSlot } from "~/protocol/common.js"; 3 | import { PacketDefinition } from "~/protocol/packets.js"; 4 | 5 | const schema = z.object({ 6 | a: z.string().uuid(), 7 | b: cosmeticSlot, 8 | c: z.string() 9 | }); 10 | 11 | export default { 12 | className: "cosmetic.ServerCosmeticAnimationTriggerPacket", 13 | body: schema, 14 | } as PacketDefinition; 15 | -------------------------------------------------------------------------------- /src/protocol/packets/mod/modsAnnounce.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | b: z.string(), 6 | a: z.array(z.string()), 7 | c: z.enum(["FABRIC", "FORGE"]), 8 | d: z.string(), 9 | modpackId: z.string().nullish(), 10 | }); 11 | 12 | export default { 13 | className: "mod.ClientModsAnnouncePacket", 14 | body: schema, 15 | } as PacketDefinition; 16 | -------------------------------------------------------------------------------- /src/protocol/packets/notices/noticesPopulate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { notice } from "~/protocol/common.js"; 3 | import { PacketDefinition } from "~/protocol/packets.js"; 4 | 5 | const schema = z.object({ 6 | a: z.array(notice), 7 | }); 8 | 9 | export default { 10 | className: "notices.ServerNoticePopulatePacket", 11 | body: schema, 12 | } as PacketDefinition; 13 | -------------------------------------------------------------------------------- /src/protocol/packets/response/responseActionPacket.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | a: z.boolean(), // success 6 | b: z.string().nullish(), // error message 7 | }); 8 | 9 | export default { 10 | className: "response.ResponseActionPacket", 11 | body: schema, 12 | } as PacketDefinition; 13 | -------------------------------------------------------------------------------- /src/protocol/packets/subscription/subscriptionUpdatePacket.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | a: z.array(z.string().uuid()).nullish(), // uuids 6 | b: z.boolean(), // unsubscribe from all 7 | c: z.boolean() // new subscription 8 | }); 9 | 10 | export default { 11 | className: "subscription.SubscriptionUpdatePacket", 12 | body: schema, 13 | } as PacketDefinition; 14 | -------------------------------------------------------------------------------- /src/protocol/packets/telemetry/telemetry.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PacketDefinition } from "~/protocol/packets.js"; 3 | 4 | const schema = z.object({ 5 | key: z.string(), 6 | metadata: z.record(z.string(), z.any()), 7 | }); 8 | 9 | export default { 10 | className: "telemetry.ClientTelemetryPacket", 11 | body: schema, 12 | } as PacketDefinition; 13 | -------------------------------------------------------------------------------- /src/utils/generic.ts: -------------------------------------------------------------------------------- 1 | export const reverseObj = (obj: any) => { 2 | return Object.entries(obj).reduce((acc: any, [key, value]: [any, any]) => { 3 | acc[value] = key; 4 | return acc; 5 | }, {}); 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/http.ts: -------------------------------------------------------------------------------- 1 | import ky from "ky"; 2 | 3 | export const generalHttpClient = ky.extend({ 4 | headers: { 5 | "User-Agent": "Acute (github.com/acutegg/acute)", 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "lib": ["es2022", "DOM", "DOM.Iterable"], 10 | "module": "NodeNext", 11 | "moduleDetection": "force", 12 | "moduleResolution": "NodeNext", 13 | "noUncheckedIndexedAccess": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "ES2022", 18 | "outDir": "dist", 19 | "paths": { 20 | "~/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["src"], 24 | "exclude": ["node_modules", "dist"] 25 | } 26 | --------------------------------------------------------------------------------