A Typescript server emulator for Box Critters, a defunct virtual world.
1import { serve } from "https://deno.land/std@0.162.0/http/server.ts";
2
3import { sign } from 'hono/jwt';
4import { Hono } from "https://deno.land/x/hono@v3.0.0/mod.ts";
5import { serveStatic } from "https://deno.land/x/hono@v3.0.0/middleware.ts";
6import { env } from 'hono/adapter';
7import { validator } from 'hono/validator';
8
9import { io } from "./io.ts";
10import * as world from "./constants/world.ts";
11import { Room } from "./types.ts";
12import * as schemas from "./schema.ts";
13import { getAccount } from "./utils.ts";
14import parties from "./constants/parties.json" with { type: 'json' };
15
16const app = new Hono();
17app.get('/*', serveStatic({ root: './public' }));
18
19// APIs for debugging and other purposes
20app.get('/api/server/players', (c) => c.json({ players: world.players }));
21
22app.get('/api/server/rooms', (c) => c.json(world.rooms));
23
24app.get('/api/server/persistence', async (c) => {
25 const account = await getAccount();
26 return c.json({
27 success: true,
28 data: account
29 });
30})
31
32// APIs for use by the client
33app.post('/api/client/login', validator('json', async (_value, c) => {
34 try {
35 const body = await c.req.json();
36 const parsed = schemas.login.safeParse(body);
37 if (!parsed.success) {
38 return c.json({
39 success: false,
40 message: "Validation failure",
41 error: parsed.error
42 }, 400);
43 };
44 return parsed.data;
45 } catch(_e) {
46 return c.json({
47 success: false,
48 message: "Bad request"
49 }, 400);
50 }
51// deno-lint-ignore no-explicit-any
52}) as any, async (c) => {
53 const body = c.req.valid('json') as {
54 nickname: string,
55 critterId: string,
56 partyId: string,
57 persistent: boolean,
58 mods: Array<string>
59 };
60
61 const _players = Object.values(world.players);
62 if (_players.find((player) => player.n == body.nickname) || world.queue.includes(body.nickname)) {
63 return c.json({
64 success: false,
65 message: "There is already a player with this nickname online."
66 });
67 }
68
69 const JWT_CONTENT = {
70 sub: {
71 playerId: crypto.randomUUID(),
72 ...body // ZOD validator is set to make the body strict, so this expansion should be fine
73 },
74 exp: Math.floor(Date.now() / 1000) + 60 * 5 // 5 mins expiry
75 };
76
77 //@ts-ignore: Deno lint
78 const { JWT_TOKEN } = env<{ JWT_TOKEN: string }>(c);
79 const token = await sign(JWT_CONTENT, JWT_TOKEN);
80
81 world.queue.push(body.nickname);
82 return c.json({
83 success: true,
84 playerId: JWT_CONTENT.sub.playerId,
85 token: token
86 });
87});
88
89app.get('/api/client/rooms', (c) => {
90 const partyId = c.req.query('partyId') || 'default';
91 if (!parties.includes(partyId)) {
92 return c.json({
93 success: false,
94 message: "Invalid partyId hash provided."
95 });
96 }
97
98 let missing = 0;
99 const roomResponse = Object.keys(world.rooms).reduce((res: Array<Room>, roomId) => {
100 const room = world.rooms[roomId];
101
102 if (room[partyId]) {
103 if (!room[partyId].partyExclusive || room[partyId].partyExclusive.includes(partyId)) {
104 res.push(room[partyId]);
105 } else {
106 missing++;
107 }
108 } else {
109 if (!room.default.partyExclusive || room.default.partyExclusive.includes(partyId)) {
110 res.push(room.default);
111 } else {
112 missing++;
113 }
114 }
115 return res;
116 }, []);
117
118 if (missing == Object.keys(world.rooms).length) {
119 return c.json({
120 success: false,
121 message: "No rooms were fetched while indexxing using the specified partyId hash."
122 });
123 }
124
125 const res = roomResponse.filter((room) => room != null);
126 if (c.req.query('debug')) {
127 const roomNames = res.map((room) => room.name);
128 return c.json({
129 parties: parties,
130 data: roomNames
131 });
132 }
133
134 return c.json({
135 parties: parties,
136 data: res
137 });
138});
139
140const handler = io.handler(async (req) => {
141 return await app.fetch(req);
142});
143
144await serve(handler, { port: 3257 });