A Typescript server emulator for Box Critters, a defunct virtual world.
at main 4.6 kB view raw
1// deno-lint-ignore-file no-explicit-any 2import { Server, Socket } from "https://deno.land/x/socket_io@0.2.0/mod.ts"; 3import z from "zod"; 4 5import * as world from "@/constants/world.ts"; 6import * as utils from "@/utils.ts"; 7import * as types from "@/types.ts"; 8 9export function listen( 10 io: Server, 11 socket: Socket, 12 ctx: types.SocketHandlerContext, 13) { 14 socket.once("login", async (ticket: string) => { 15 if ( 16 z.object({ 17 ticket: z.string(), 18 }).safeParse({ ticket: ticket }).success == false 19 ) return; 20 21 let playerData; 22 try { 23 playerData = await utils.verifyJWT(ticket); 24 } catch (_e) { 25 socket.disconnect(true); 26 return; 27 } 28 29 // TODO: make this just an inline function instead of having an onPropertyChange function, I'm just really lazy right now lol -index 30 function onPropertyChange(property: string, value: any) { 31 utils.updateAccount(ctx.localPlayer!.nickname, property, value); 32 } 33 34 const createArrayHandler = (propertyName: string) => ({ 35 get(target: any, property: string) { 36 if (typeof target[property] === "function") { 37 return function (...args: any[]) { 38 const result = target[property].apply(target, args); 39 onPropertyChange(propertyName, target); 40 return result; 41 }; 42 } 43 return target[property]; 44 }, 45 }); 46 47 const handler = { 48 set(target: any, property: string, value: any) { 49 if (Array.isArray(value)) { 50 target[property] = new Proxy(value, createArrayHandler(property)); 51 onPropertyChange(property, target[property]); 52 } else { 53 target[property] = value; 54 onPropertyChange(property, value); 55 } 56 return true; 57 }, 58 get(target: any, property: string) { 59 if (Array.isArray(target[property])) { 60 return new Proxy(target[property], createArrayHandler(property)); 61 } 62 return target[property]; 63 }, 64 }; 65 66 //@ts-ignore: I will fix the type errors with using a different JWT library eventually 67 const sub = playerData as { 68 playerId: string; 69 nickname: string; 70 critterId: types.CritterId; 71 partyId: string; 72 persistent: boolean; 73 mods: Array<string>; 74 }; 75 76 if ( 77 [ 78 "today2019", 79 "today2020", 80 "today2021", 81 ].includes(sub.partyId) 82 ) { 83 console.log("target year:", parseInt(sub.partyId.replace("today", ""))); 84 sub.partyId = utils.getCurrentEvent( 85 parseInt(sub.partyId.replace("today", "")), 86 ); 87 } 88 89 const persistentAccount = await utils.getAccount(sub.nickname); 90 if (!sub.persistent || persistentAccount.individual == null) { 91 ctx.localPlayer = { 92 playerId: sub.playerId, 93 nickname: sub.nickname, 94 critterId: sub.critterId, 95 ignore: [], 96 friends: [], 97 inventory: [], 98 gear: [], 99 eggs: [], 100 coins: 150, 101 isMember: false, 102 isGuest: false, 103 isTeam: false, 104 x: 0, 105 y: 0, 106 rotation: 0, 107 mutes: [], 108 109 _partyId: sub.partyId, // This key is replaced down the line anyway 110 _mods: sub.mods, 111 }; 112 113 if (sub.persistent) { 114 utils.createAccount(ctx.localPlayer); 115 ctx.localPlayer = new Proxy<types.LocalPlayer>( 116 utils.expandAccount(ctx.localPlayer), 117 handler, 118 ); 119 } 120 } else { 121 persistentAccount.individual.critterId = sub.critterId || "hamster"; 122 persistentAccount.individual._partyId = sub.partyId || "default"; 123 persistentAccount.individual._mods = sub.mods || []; 124 125 ctx.localPlayer = new Proxy<types.LocalPlayer>( 126 utils.expandAccount(persistentAccount.individual), 127 handler, 128 ); 129 } 130 131 ctx.localPlayer._partyId = socket.handshake.query.get("partyId") || 132 "default"; 133 world.queue.splice(world.queue.indexOf(ctx.localPlayer.nickname), 1); 134 135 ctx.localCrumb = utils.makeCrumb(ctx.localPlayer, world.spawnRoom); 136 socket.join(world.spawnRoom); 137 138 world.players[ctx.localPlayer.playerId] = ctx.localCrumb; 139 socket.emit("login", { 140 player: ctx.localPlayer, 141 spawnRoom: world.spawnRoom, 142 }); 143 }); 144 145 socket.on("beep", () => socket.emit("beep")); 146 147 socket.on("disconnect", (reason) => { 148 if (reason == "server namespace disconnect") return; 149 150 if (ctx.localPlayer && ctx.localCrumb) { 151 io.in(ctx.localCrumb._roomId).emit("R", ctx.localCrumb); 152 delete world.players[ctx.localPlayer.playerId]; 153 } 154 }); 155}