A Typescript server emulator for Box Critters, a defunct virtual world.
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}