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 });