- minor: add checks for public dir & env vars
- feat: `deno compile` support
- fix: use .default ver of room as backup with moveTo event to prevent server crashes
- style: restructure all code into a /src/ folder
- style: deno format
- style: update README and package.json
- feat: clean up dependencies & use Deno.serve instead of Hono
- feat: "Today in 2019/2020/2021" party switcher
- minor: --port CLI config
- fix: API responding parties in incorrect format
This commit is contained in:
Index 2025-04-09 01:04:19 -05:00
parent d87f5628e0
commit 9b12e23dbf
25 changed files with 1354 additions and 995 deletions

2
.env.build Normal file
View file

@ -0,0 +1,2 @@
EXECUTABLE=true
JWT_TOKEN="boxcritters"

View file

@ -1 +1,2 @@
EXECUTABLE=
JWT_TOKEN=

2
.gitignore vendored
View file

@ -3,3 +3,5 @@
Thumbs.db
accounts.json
/node_modules
Localbox
/public

15
Dockerfile Normal file
View file

@ -0,0 +1,15 @@
FROM denoland/deno:2.1.5
WORKDIR /app
COPY deno.json .
RUN deno install
COPY . .
RUN deno cache main.ts
ARG PORT=3257
EXPOSE $PORT
CMD ["task", "start"]

View file

@ -1,6 +1,8 @@
# Event Recreation Progress
A checklist for each party of which rooms are finished. Not all parties will be possible due to them not being archived sadly. This list ignores tedious animations which can be done at a later date.
A checklist for each party of which rooms are finished. Not all parties will be
possible due to them not being archived sadly. This list ignores tedious
animations which can be done at a later date.
## Default (no party)
@ -25,7 +27,8 @@ A checklist for each party of which rooms are finished. Not all parties will be
## Battle Bears
This event had no significant changes. The Crash Site room became a default room, and is classified as that.
This event had no significant changes. The Crash Site room became a default
room, and is classified as that.
### Christmas
@ -64,7 +67,8 @@ This event had no significant changes. The Crash Site room became a default room
- [x] Tavern
- [x] Port
- [x] Shack
- [x] **Solar System** *(The background and foreground that were archived are corrupted)*
- [x] **Solar System** _(The background and foreground that were archived are
corrupted)_
### Halloween
@ -78,7 +82,8 @@ This event had no significant changes. The Crash Site room became a default room
### Club Penguin Celebration
- [x] Port (practically the same as `Halloween 2020`'s Port, just with a party hat on the outside of the `Shack`)
- [x] Port (practically the same as `Halloween 2020`'s Port, just with a party
hat on the outside of the `Shack`)
- [x] Shack
### CritterCon
@ -116,8 +121,8 @@ This event had no significant changes. The Crash Site room became a default room
### New Years
- [ ] Port
*The wiki only includes a screenshot of the Port, and no other rooms.*
- [ ] Port _The wiki only includes a screenshot of the Port, and no other
rooms._
### 2nd Anniversary
@ -139,7 +144,8 @@ This event had no significant changes. The Crash Site room became a default room
- [x] Port
- [x] Shack
- [x] Jungle
- [x] **Box Realm (work in progress)** *(the background is the only file found so far)*
- [x] **Box Realm (work in progress)** _(the background is the only file found
so far)_
### Easter
@ -172,7 +178,8 @@ This event had no significant changes. The Crash Site room became a default room
### Club Penguin Celebration
- [x] Port (practically the same as `Halloween 2021`'s Port, just with a party hat on the outside of the `Shack`)
- [x] Port (practically the same as `Halloween 2021`'s Port, just with a party
hat on the outside of the `Shack`)
- [x] Shack
### CritterCon
@ -186,4 +193,4 @@ This event had no significant changes. The Crash Site room became a default room
- [ ] Cellar
- [ ] Port
- [ ] Jungle
- [ ] Shack
- [ ] Shack

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Index
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

102
README.md
View file

@ -1,40 +1,116 @@
# Box Critters Localbox
# Localbox
Reopening the dusty box of the world of Box Critters! This is a Typescript server emulator using Deno.
Reopen the dusty box of the world of Box Critters! This repository features a
Typescript server emulator, **built using Deno**. The version of the game the
server is built around is **client version 161**.
This repository appears empty because it is a fork of my private repository so
that the assets didn't get included in the public version of the repository.
_project start: late November 2024_
## Assets
The assets for the game are not included in this repository, since I've been
told that Rocketsnail has taken down GitHub repositories for hosting the assets.
Though, I find it ironic since the modding community has been allowed to freely
host asset archives [here](https://github.com/boxcrittersmods/BCArchive) for 5
years at this point. As far as I can see, Localbox has the most comprehensive
archive of Box Critters assets, compiling several sources & custom spritesheet
JSON for spritesheet mis-matches.
### Archive Statistics
Below is a break down of the archive compilation, to be consider archived the
spritesheet has to be archived (spritesheet JSON is not taken into
consideration, as it is pretty easy to custom make). To get the assets, feel
free message me on Discord: @index.lua
> _Note:_ I do plan on manually cropping some rooms to make custom spritesheets,
> but when I do those will be marked as custom and not legit.
- **6/7** full-time rooms archived
- Full-time rooms are rooms that were always available, no matter the party.
- Missing: [_Jungle_](https://box-critters.fandom.com/wiki/Jungle)
- **6/10** party-exclusive rooms archived
- Party-exclusive rooms are rooms that were only available during specific
parties.
- Missing:
[_Holiday Cliff_](https://box-critters.fandom.com/wiki/Holiday_Cliff),
[_Holiday Forest_](https://box-critters.fandom.com/wiki/Holiday_Forest),
[_CritterCon Hall_](https://box-critters.fandom.com/wiki/Critter_Con_Hall),
[_Box Realm_](https://box-critters.fandom.com/wiki/Box_Realm)
- **465/590** released items archived
- **12/12** critters archived
- **2/2** mini-games archived
- This count excludes _Critter Ball_, because that requires an entirely
separate backend. If you want to relive _Critter Ball_, check out
[FarawayDrip30's Critter Ball server](https://farawaydrip30.itch.io/critterball-server).
## Party Switcher
A custom party switcher has been implemented, you can change the party on the log-in page, or using the `/party [ID]` command in-game. For a breakdown of party room recreation progress, go [here](Events.md).
A custom party switcher has been implemented, you can change the party on the
log-in page, or using the `/party [ID]` command in-game. For a breakdown of
party room recreation progress, go [here](Events.md).
## Development
### CLI
> Installation
```bash
deno install
```
> Serving
```bash
deno run start
> Listening on http://localhost:3257/
```
## APIs
> Building to an executable (shorthand invocation of `deno compile` using
> already-set flags & config)
The game has 4 APIs:
```bash
deno run build
> If the command is a success, a "Localbox" executable will appear in the project directory.
```
### (GET) `/api/server/players`
### APIs
This API returns information on the player(s) in-game, if any.
The game has 4 APIs for debugging or for use by the game client:
### (GET) `/api/server/rooms`
- (GET) `/api/server/players`
- This API returns information on the player(s) in-game, if any.
This API returns almost-identical information as the `/api/client/rooms` API, however it returns information on all hashes of all rooms with no required party ID URL parameter.
- (GET) `/api/server/rooms`
- This API returns almost-identical information as the `/api/client/rooms`
API, however it returns information on all hashes of all rooms with no
required party ID URL parameter.
### (POST) `/api/client/login`
- (POST) `/api/client/login`
- This API takes in all the information provided by the user on log-in and
generates a JWT for that session.
This API takes in all the information provided by the user on log-in and generates a JWT for that session.
- (GET) `/api/client/rooms?partyId=`
- This API returns information on the parties the game supports, and depending
on the party ID provided in the URL, information for each room that party
changes in some way. If the party does not change the room in any way, the
default version of that room will be returned. All the data is gathered by
the server reading the `/public/media/rooms/` directory of the game - and
cached for future requests.
### (GET) `/api/client/rooms?partyId=`
## Contributors
This API returns information on the parties the game supports, and depending on the party ID provided in the URL, information for each room that party changes in some way. If the party does not change the room in any way, the default version of that room will be returned. All the data is gathered by the server reading the `/public/media/rooms/` directory of the game - and cached for future requests.
- [jonastisell](https://github.com/jonastisell) - spritesheet extraction help &
moral support
- [Boo0](https://github.com/Boo6447) - provided archived assets of party room
versions & moral support
- [@boxcrittersmods/BCArchive](https://github.com/boxcrittersmods/BCArchive) -
provided a lot of archived assets of early rooms

10
chalk.d.ts vendored
View file

@ -1,10 +0,0 @@
/*
Typescript doesn't seem to recognize the functions of the chalk_deno module, so some are specified here to avoid type warnings.
*/
declare module "https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js" {
export function red(text: string): string;
export function green(text: string): string;
export function blue(text: string): string;
export function gray(text: string): string;
}

View file

@ -1,140 +1,140 @@
export const shop = {
lastItem: { itemId: "beard2_brown", cost: 20 },
freeItem: { itemId: "goggles_pink", cost: 0},
nextItem: { itemId: "scout_uniform", cost: 0},
collection: [
{ itemId: "santa_hat", cost: 100 }
]
}
lastItem: { itemId: "beard2_brown", cost: 20 },
freeItem: { itemId: "goggles_pink", cost: 0 },
nextItem: { itemId: "scout_uniform", cost: 0 },
collection: [
{ itemId: "santa_hat", cost: 100 },
],
};
export const codes = {
rocketsnail: ["viking", "rocket_red"],
andybulletin: "propeller",
cute: "toque_pink",
oommgames: "space_red",
boxcritters3d: ["3d_white", "3d_black"],
goodnight: "sleeping",
madeincanada: "toque_white",
thekeeper: "party_green",
bunnyhug: "hoodie_blue",
greenplumber: "ballcap_green",
duckhunter: "float_pink",
piratepack: "pirate_patch",
pickle: "pickle",
oscarproductions: "guitar_blue",
livestream: "headphones_black",
creative: "headphones_black",
fun: "propeller_pink",
marco: "keytar_red",
snowball: "hoodie_white",
critbits: "ballcap_pink",
esporte: "kit_yellow",
squeeze: "hoodie_orange",
imagination: "box_brown",
sparkle: "propeller_silver",
adventure: "snorkel_blue",
tamago: "sun_square",
redcross: "australian_hat",
discordcritterspt: "headphones_green",
scarletraven: "wings_black",
boxcritterswiki: "paperhat_colour",
wikicritters: "brain_green",
boxcrittersguild: "cardboard_sword_silver",
staysafe: "doctor_mask_blue",
glitter: "pirate_capt_pink"
}
rocketsnail: ["viking", "rocket_red"],
andybulletin: "propeller",
cute: "toque_pink",
oommgames: "space_red",
boxcritters3d: ["3d_white", "3d_black"],
goodnight: "sleeping",
madeincanada: "toque_white",
thekeeper: "party_green",
bunnyhug: "hoodie_blue",
greenplumber: "ballcap_green",
duckhunter: "float_pink",
piratepack: "pirate_patch",
pickle: "pickle",
oscarproductions: "guitar_blue",
livestream: "headphones_black",
creative: "headphones_black",
fun: "propeller_pink",
marco: "keytar_red",
snowball: "hoodie_white",
critbits: "ballcap_pink",
esporte: "kit_yellow",
squeeze: "hoodie_orange",
imagination: "box_brown",
sparkle: "propeller_silver",
adventure: "snorkel_blue",
tamago: "sun_square",
redcross: "australian_hat",
discordcritterspt: "headphones_green",
scarletraven: "wings_black",
boxcritterswiki: "paperhat_colour",
wikicritters: "brain_green",
boxcrittersguild: "cardboard_sword_silver",
staysafe: "doctor_mask_blue",
glitter: "pirate_capt_pink",
};
export const throwback = [
"goggles_black",
"pot",
"sombrero_yellow",
"cone",
"toque_purple",
"sun_orange",
"pirate_capt_black",
"lifejacket_red",
"ballcap_black",
"super_cape_red",
"tshirt_white",
"lei_red",
"ballcap_blue",
"viking_blue",
"overalls_orange",
"bunny_blue",
"messenger_brown",
"plaid_black",
"tinfoil_hat",
"monk",
"pirate_hat_black",
"hawaii_orange",
"space_black",
"traffic_cone",
"grass_yellow",
"ushanka",
"bandana_purple",
"ski_suit_blue",
"hotdog",
"space_blue",
"sweater_orange",
"tactical_headset",
"ballcap_yellow",
"ringmaster_suit_red",
"flight_helmet_red",
"rainhat_yellow",
"raincoat_yellow",
"ballcap_red",
"ballerina_pink",
"hawaii_blue",
"stripe_red_white",
"grass_green",
"plaid_blue",
"toque_orange",
"school_pack_orange",
"pirate_crew_blue",
"ringmaster_hat_black",
"super_mask_black",
"pirate_hat_pink",
"rainhat_red",
"tophat_black",
"scarf_red",
"skeleton_body",
"dracula_cloak",
"pumpkin",
"hotdog",
"beard3_black",
"scarf_purple",
"puffy_red",
"blockhead",
"hoodie_purple",
"winter_dress_red",
"bulb_blue",
"tacky_red",
"pirate_bandana_red"
]
"goggles_black",
"pot",
"sombrero_yellow",
"cone",
"toque_purple",
"sun_orange",
"pirate_capt_black",
"lifejacket_red",
"ballcap_black",
"super_cape_red",
"tshirt_white",
"lei_red",
"ballcap_blue",
"viking_blue",
"overalls_orange",
"bunny_blue",
"messenger_brown",
"plaid_black",
"tinfoil_hat",
"monk",
"pirate_hat_black",
"hawaii_orange",
"space_black",
"traffic_cone",
"grass_yellow",
"ushanka",
"bandana_purple",
"ski_suit_blue",
"hotdog",
"space_blue",
"sweater_orange",
"tactical_headset",
"ballcap_yellow",
"ringmaster_suit_red",
"flight_helmet_red",
"rainhat_yellow",
"raincoat_yellow",
"ballcap_red",
"ballerina_pink",
"hawaii_blue",
"stripe_red_white",
"grass_green",
"plaid_blue",
"toque_orange",
"school_pack_orange",
"pirate_crew_blue",
"ringmaster_hat_black",
"super_mask_black",
"pirate_hat_pink",
"rainhat_red",
"tophat_black",
"scarf_red",
"skeleton_body",
"dracula_cloak",
"pumpkin",
"hotdog",
"beard3_black",
"scarf_purple",
"puffy_red",
"blockhead",
"hoodie_purple",
"winter_dress_red",
"bulb_blue",
"tacky_red",
"pirate_bandana_red",
];
export const eventCodes = {
christmas2019: {
jinglebells: "elf_hat_green",
winter: "winter_dress",
joy: "bulb_yellow",
rudolph: "reindeer_head",
elf: "elf_suit_green",
cheer: "bulb_green",
glow: "bulb_red",
rednose: "reindeer_body",
blizzard: "wizard_blizzard",
family: "ornament_red",
shine: "angel_halo",
peace: "angel_wings",
jolly: "santa_beard",
merry: "santa_hat",
christmas: "santa_suit",
boxingday: "onsie_plaid_red",
warm: "sleeping_red",
tacky: "tacky_green",
snow: "goggles_white",
beautiful: "winter_dress_red",
newyear: "tuxedo_black",
"2020": "tophat_black"
}
}
christmas2019: {
jinglebells: "elf_hat_green",
winter: "winter_dress",
joy: "bulb_yellow",
rudolph: "reindeer_head",
elf: "elf_suit_green",
cheer: "bulb_green",
glow: "bulb_red",
rednose: "reindeer_body",
blizzard: "wizard_blizzard",
family: "ornament_red",
shine: "angel_halo",
peace: "angel_wings",
jolly: "santa_beard",
merry: "santa_hat",
christmas: "santa_suit",
boxingday: "onsie_plaid_red",
warm: "sleeping_red",
tacky: "tacky_green",
snow: "goggles_white",
beautiful: "winter_dress_red",
newyear: "tuxedo_black",
"2020": "tophat_black",
},
};

View file

@ -0,0 +1,14 @@
[
"default",
"easter2019",
"christmas2019",
"lucky2020",
"easter2020",
"space",
"grad2020",
"summer2020",
"halloween2019",
"halloween2020",
"fools2021",
"halloween2021"
]

View file

@ -1,14 +1,50 @@
[
"default",
"easter2019",
"christmas2019",
"lucky2020",
"easter2020",
"space",
"grad2020",
"summer2020",
"halloween2019",
"halloween2020",
"fools2021",
"halloween2021"
]
{
"default": {
"start": null,
"end": null
},
"easter2019": {
"start": "04-18-2019",
"end": "04-28-2019"
},
"halloween2019": {
"start": "10-20-2019",
"end": "11-06-2019"
},
"christmas2019": {
"start": "12-05-2019",
"end": "01-05-2020"
},
"lucky2020": {
"start": "03-11-2020",
"end": "04-01-2020"
},
"easter2020": {
"start": "04-10-2020",
"end": "04-19-2020"
},
"grad2020": {
"start": "06-03-2020",
"end": "06-20-2020"
},
"summer2020": {
"start": "07-15-2020",
"end": "08-16-2020"
},
"space": {
"start": "08-16-2020",
"end": "09-04-2020"
},
"halloween2020": {
"start": "10-07-2020",
"end": "11-08-2020"
},
"christmas2020": {
"start": "12-02-2020",
"end": "01-01-2021"
},
"fools2021": {
"start": "03-30-2021",
"end": "04-02-2021"
}
}

View file

@ -1,90 +1,91 @@
import { PlayerCrumb, Room } from "../types.ts"
import { indexRoomData } from "../utils.ts";
import { PlayerCrumb, Room } from "../src/types.ts";
import { indexRoomData } from "../src/utils.ts";
export const rooms: Record<string, Record<string, Room>> = await indexRoomData()
export const rooms: Record<string, Record<string, Room>> =
await indexRoomData();
export const spawnRoom = "tavern";
export const players: Record<string, PlayerCrumb> = {
"0": {
"i": "0",
"n": "Huggable",
"c": "huggable",
"x": 1670,
"y": 323,
"r": 180,
"g": [],
"m": "",
"e": "",
"_roomId": "crash_site"
}
}
export const queue: Array<string> = []
"0": {
"i": "0",
"n": "Huggable",
"c": "huggable",
"x": 1670,
"y": 323,
"r": 180,
"g": [],
"m": "",
"e": "",
"_roomId": "crash_site",
},
};
export const queue: Array<string> = [];
export const roomExits = {
"cellar->tavern": { x: 360, y: 410, r: 0 },
"crash_site->cellar": { x: 615, y: 400, r: 0 },
"shack->port": { x: 550, y: 235, r: 0 },
"jungle->port": { x: 650, y: 230, r: 0 },
"snowman_village->tavern": { x: 563, y: 368, r: 0 }
}
"cellar->tavern": { x: 360, y: 410, r: 0 },
"crash_site->cellar": { x: 615, y: 400, r: 0 },
"shack->port": { x: 550, y: 235, r: 0 },
"jungle->port": { x: 650, y: 230, r: 0 },
"snowman_village->tavern": { x: 563, y: 368, r: 0 },
};
// deno-lint-ignore no-explicit-any
export const npcs: { [key: string]: any } = {
snowman_village: [
{
"i": "NPC0",
"n": "Snow Girl",
"c": "snowgirl",
"x": 1289,
"y": 228,
"r": 180,
"g": [],
"m": "",
"e": ""
},
{
"i": "NPC1",
"n": "Snow Patrol",
"c": "snow_patrol",
"x": 1644,
"y": 221,
"r": 180,
"g": [],
"m": "",
"e": ""
},
{
"i": "NPC2",
"n": "Snow Greeter",
"c": "snow_greeter",
"x": 443,
"y": 317,
"r": 180,
"g": [],
"m": "",
"e": ""
},
{
"i": "NPC3",
"n": "Snow Grandma",
"c": "snowgrandma",
"x": 1938,
"y": 251,
"r": 180,
"g": [],
"m": "",
"e": ""
},
{
"i": "NPC4",
"n": "Snow Keeper",
"c": "snowkeeper",
"x": 893,
"y": 216,
"r": 180,
"g": [],
"m": "",
"e": ""
}
]
}
snowman_village: [
{
"i": "NPC0",
"n": "Snow Girl",
"c": "snowgirl",
"x": 1289,
"y": 228,
"r": 180,
"g": [],
"m": "",
"e": "",
},
{
"i": "NPC1",
"n": "Snow Patrol",
"c": "snow_patrol",
"x": 1644,
"y": 221,
"r": 180,
"g": [],
"m": "",
"e": "",
},
{
"i": "NPC2",
"n": "Snow Greeter",
"c": "snow_greeter",
"x": 443,
"y": 317,
"r": 180,
"g": [],
"m": "",
"e": "",
},
{
"i": "NPC3",
"n": "Snow Grandma",
"c": "snowgrandma",
"x": 1938,
"y": 251,
"r": 180,
"g": [],
"m": "",
"e": "",
},
{
"i": "NPC4",
"n": "Snow Keeper",
"c": "snowkeeper",
"x": 893,
"y": 216,
"r": 180,
"g": [],
"m": "",
"e": "",
},
],
};

View file

@ -1,9 +1,10 @@
{
"imports": {
"hono": "jsr:@hono/hono@^4.6.11"
"@std/fs": "jsr:@std/fs@^1.0.14"
},
"tasks": {
"start": "deno run --watch --allow-net --allow-read --allow-env --env-file=.env --allow-write main.ts"
"start": "deno run --watch --allow-net --allow-read --allow-env --env-file=.env --allow-write src/main.ts",
"build": "deno compile --allow-net --allow-read --allow-env --env-file=.env.build --allow-write --output Localbox src/main.ts"
},
"compilerOptions": {
"jsx": "precompile",

178
deno.lock
View file

@ -1,21 +1,30 @@
{
"version": "4",
"specifiers": {
"jsr:@hono/hono@^4.6.11": "4.6.11",
"jsr:@std/encoding@*": "1.0.5",
"jsr:@std/cli@*": "1.0.15",
"jsr:@std/fs@*": "1.0.15",
"jsr:@std/fs@^1.0.14": "1.0.15",
"jsr:@std/path@^1.0.8": "1.0.8",
"npm:fs@^0.0.1-security": "0.0.1-security",
"npm:http@^0.0.1-security": "0.0.1-security",
"npm:jose@5.9.6": "5.9.6",
"npm:nodemon@^3.1.7": "3.1.9",
"npm:path@~0.12.7": "0.12.7",
"npm:socket.io@^4.8.1": "4.8.1",
"npm:zod@^3.24.1": "3.24.2"
},
"jsr": {
"@hono/hono@4.6.11": {
"integrity": "07399d911f09e94b7dc1e0e0a0577d35fe66578af20163d513958364c4e9e702"
"@std/cli@1.0.15": {
"integrity": "e79ba3272ec710ca44d8342a7688e6288b0b88802703f3264184b52893d5e93f"
},
"@std/encoding@1.0.5": {
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
"@std/fs@1.0.15": {
"integrity": "c083fb479889d6440d768e498195c3fc499d426fbf9a6592f98f53884d1d3f41",
"dependencies": [
"jsr:@std/path"
]
},
"@std/path@1.0.8": {
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
}
},
"npm": {
@ -31,13 +40,13 @@
"@types/node@22.12.0": {
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
"dependencies": [
"undici-types"
"undici-types@6.20.0"
]
},
"@types/node@22.13.8": {
"integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==",
"@types/node@22.14.0": {
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
"dependencies": [
"undici-types"
"undici-types@6.21.0"
]
},
"accepts@1.3.8": {
@ -121,7 +130,7 @@
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"dependencies": [
"@types/cors",
"@types/node@22.13.8",
"@types/node@22.14.0",
"accepts",
"base64id",
"cookie",
@ -179,6 +188,9 @@
"is-number@7.0.0": {
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"jose@5.9.6": {
"integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="
},
"mime-db@1.52.0": {
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
@ -299,6 +311,9 @@
"undici-types@6.20.0": {
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"undici-types@6.21.0": {
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"util@0.10.4": {
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"dependencies": [
@ -315,9 +330,6 @@
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
}
},
"redirects": {
"https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts"
},
"remote": {
"https://deno.land/std@0.150.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
"https://deno.land/std@0.150.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06",
@ -362,38 +374,27 @@
"https://deno.land/std@0.162.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239",
"https://deno.land/std@0.162.0/async/tee.ts": "9af3a3e7612af75861308b52249e167f5ebc3dcfc8a1a4d45462d96606ee2b70",
"https://deno.land/std@0.162.0/http/server.ts": "e99c1bee8a3f6571ee4cdeb2966efad465b8f6fe62bec1bdb59c1f007cc4d155",
"https://deno.land/std@0.177.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
"https://deno.land/std@0.177.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
"https://deno.land/std@0.177.0/bytes/index_of_needle.ts": "65c939607df609374c4415598fa4dad04a2f14c4d98cd15775216f0aaf597f24",
"https://deno.land/std@0.177.0/encoding/base64.ts": "7de04c2f8aeeb41453b09b186480be90f2ff357613b988e99fabb91d2eeceba1",
"https://deno.land/std@0.177.0/encoding/base64url.ts": "3f1178f6446834457b16bfde8b559c1cd3481727fe384d3385e4a9995dc2d851",
"https://deno.land/std@0.177.0/node/_core.ts": "9a58c0ef98ee77e9b8fcc405511d1b37a003a705eb6a9b6e95f75434d8009adc",
"https://deno.land/std@0.177.0/node/_utils.ts": "7fd55872a0cf9275e3c080a60e2fa6d45b8de9e956ebcde9053e72a344185884",
"https://deno.land/std@0.177.0/node/buffer.ts": "85617be2063eccaf177dbb84c7580d1e32023724ed14bd9df4e453b152a26167",
"https://deno.land/std@0.177.0/node/internal/buffer.mjs": "e92303a3cc6d9aaabcd270a937ad9319825d9ba08cb332650944df4562029b27",
"https://deno.land/std@0.177.0/node/internal/crypto/_keys.ts": "8f3c3b5a141aa0331a53c205e9338655f1b3b307a08085fd6ff6dda6f7c4190b",
"https://deno.land/std@0.177.0/node/internal/crypto/constants.ts": "544d605703053218499b08214f2e25cf4310651d535b7ab995891c4b7a217693",
"https://deno.land/std@0.177.0/node/internal/error_codes.ts": "8495e33f448a484518d76fa3d41d34fc20fe03c14b30130ad8e936b0035d4b8b",
"https://deno.land/std@0.177.0/node/internal/errors.ts": "1c699b8a3cb93174f697a348c004b1c6d576b66688eac8a48ebb78e65c720aae",
"https://deno.land/std@0.177.0/node/internal/hide_stack_frames.ts": "9dd1bad0a6e62a1042ce3a51eb1b1ecee2f246907bff44835f86e8f021de679a",
"https://deno.land/std@0.177.0/node/internal/normalize_encoding.mjs": "fd1d9df61c44d7196432f6e8244621468715131d18cc79cd299fc78ac549f707",
"https://deno.land/std@0.177.0/node/internal/primordials.mjs": "a72d86b5aa55d3d50b8e916b6a59b7cc0dc5a31da8937114b4a113ad5aa08c74",
"https://deno.land/std@0.177.0/node/internal/util.mjs": "f7fe2e1ca5e66f550ad0856b9f5ee4d666f0c071fe212ea7fc7f37cfa81f97a5",
"https://deno.land/std@0.177.0/node/internal/util/inspect.mjs": "11d7c9cab514b8e485acc3978c74b837263ff9c08ae4537fa18ad56bae633259",
"https://deno.land/std@0.177.0/node/internal/util/types.ts": "0e587b44ec5e017cf228589fc5ce9983b75beece6c39409c34170cfad49d6417",
"https://deno.land/std@0.177.0/node/internal/validators.mjs": "e02f2b02dd072a5d623970292588d541204dc82207b4c58985d933a5f4b382e6",
"https://deno.land/std@0.177.0/node/internal_binding/_libuv_winerror.ts": "30c9569603d4b97a1f1a034d88a3f74800d5ea1f12fcc3d225c9899d4e1a518b",
"https://deno.land/std@0.177.0/node/internal_binding/_node.ts": "cb2389b0eab121df99853eb6a5e3a684e4537e065fb8bf2cca0cbf219ce4e32e",
"https://deno.land/std@0.177.0/node/internal_binding/_utils.ts": "7c58a2fbb031a204dee9583ba211cf9c67922112fe77e7f0b3226112469e9fe1",
"https://deno.land/std@0.177.0/node/internal_binding/_winerror.ts": "3e8cfdfe22e89f13d2b28529bab35155e6b1730c0221ec5a6fc7077dc037be13",
"https://deno.land/std@0.177.0/node/internal_binding/buffer.ts": "31729e0537921d6c730ad0afea44a7e8a0a1044d070ade8368226cb6f7390c8b",
"https://deno.land/std@0.177.0/node/internal_binding/constants.ts": "21ff9d1ee71d0a2086541083a7711842fc6ae25e264dbf45c73815aadce06f4c",
"https://deno.land/std@0.177.0/node/internal_binding/string_decoder.ts": "54c3c1cbd5a9254881be58bf22637965dc69535483014dab60487e299cb95445",
"https://deno.land/std@0.177.0/node/internal_binding/types.ts": "2187595a58d2cf0134f4db6cc2a12bf777f452f52b15b6c3aed73fa072aa5fc3",
"https://deno.land/std@0.177.0/node/internal_binding/util.ts": "808ff3b92740284184ab824adfc420e75398c88c8bccf5111f0c24ac18c48f10",
"https://deno.land/std@0.177.0/node/internal_binding/uv.ts": "eb0048e30af4db407fb3f95563e30d70efd6187051c033713b0a5b768593a3a3",
"https://deno.land/std@0.212.0/path/_common/assert_path.ts": "2ca275f36ac1788b2acb60fb2b79cb06027198bc2ba6fb7e163efaedde98c297",
"https://deno.land/std@0.212.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c",
"https://deno.land/std@0.212.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15",
"https://deno.land/std@0.212.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441",
"https://deno.land/std@0.212.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d",
"https://deno.land/std@0.212.0/path/posix/extname.ts": "8d36ae0082063c5e1191639699e6f77d3acf501600a3d87b74943f0ae5327427",
"https://deno.land/std@0.212.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808",
"https://deno.land/std@0.212.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef",
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
"https://deno.land/std@0.224.0/media_types/_db.ts": "19563a2491cd81b53b9c1c6ffd1a9145c355042d4a854c52f6e1424f73ff3923",
"https://deno.land/std@0.224.0/media_types/_util.ts": "e0b8da0c7d8ad2015cf27ac16ddf0809ac984b2f3ec79f7fa4206659d4f10deb",
"https://deno.land/std@0.224.0/media_types/content_type.ts": "ed3f2e1f243b418ad3f441edc95fd92efbadb0f9bde36219c7564c67f9639513",
"https://deno.land/std@0.224.0/media_types/extension.ts": "ec91e1818864cb84f8053ecafb270eaca702412c15c2086929ae34132e11c56a",
"https://deno.land/std@0.224.0/media_types/extensions_by_type.ts": "9db10797e09421815688c8f7a2fbfd5dcb040fa5c488278f1b9e04359369bd0b",
"https://deno.land/std@0.224.0/media_types/format_media_type.ts": "ffef4718afa2489530cb94021bb865a466eb02037609f7e82899c017959d288a",
"https://deno.land/std@0.224.0/media_types/get_charset.ts": "277ebfceb205bd34e616fe6764ef03fb277b77f040706272bea8680806ae3f11",
"https://deno.land/std@0.224.0/media_types/mod.ts": "c8acfa43ce3993e99f4d8aa60fb828a4eee3ab6920aaeb90f6a3d63f6f4f3435",
"https://deno.land/std@0.224.0/media_types/parse_media_type.ts": "487f000a38c230ccbac25420a50f600862e06796d0eee19d19631b9e84ee9654",
"https://deno.land/std@0.224.0/media_types/type_by_extension.ts": "bf4e3f5d6b58b624d5daa01cbb8b1e86d9939940a77e7c26e796a075b60ec73b",
"https://deno.land/std@0.224.0/media_types/vendor/mime-db.v1.52.0.ts": "0218d2c7d900e8cd6fa4a866e0c387712af4af9a1bae55d6b2546c73d273a1e6",
"https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8",
"https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2",
"https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c",
@ -471,90 +472,6 @@
"https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972",
"https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e",
"https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c",
"https://deno.land/std@0.94.0/node/tty.ts": "9fa7f7b461759774b4eeab00334ac5d25b69bf0de003c02814be01e65150da79",
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/ansi-styles/index.js": "7cc96ab93d1c9cfc0746e9dffb40be872e42ee242906f48e68df0d2c9669f737",
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/has-flag/index.js": "aed21e4eba656057e7b8c6024305f5354d2ebee2adc857a1d8cd5207923de7e5",
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js": "6339123f32f7eb4b17c5c9c926ecdf3dbc353fd4fda7811ad2d3c1d4b98a7420",
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/supports-color/index.js": "4d7f2d216b6ac9013d9ec7e004de21f5a7d00bf2be4075bab2d82638d0d41a86",
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/templates.js": "f2e12be18cb84710e341e5499528280278052909fa74a12cefc9e2cc26a597ac",
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/util.js": "cd08297ec411dcee91826ad01a00d3427235d4548ba605a59e64f0da83af8306",
"https://deno.land/x/hono@v3.0.0/adapter/deno/serve-static.ts": "d1c21498ced39849fa0bb23b372bf5d30677916fdbc875902735700ca1e789e3",
"https://deno.land/x/hono@v3.0.0/client/client.ts": "c720020a167139dfe8d6af7b91c0c5db38186722b380fb0bb251bae685a2103e",
"https://deno.land/x/hono@v3.0.0/client/index.ts": "7ad089b121f2613a0eaedd3d8aa8307a48bf3bf48f6c38281d6e73ee35f0d7ea",
"https://deno.land/x/hono@v3.0.0/client/types.ts": "47b83ab3dea4d6ef60a7c5ed11bfdb5113df7ded05403869e029f5317e44c827",
"https://deno.land/x/hono@v3.0.0/client/utils.ts": "781ec703f3e685cf02a201cfa717be111820ef89bebd7cb5968c6823310aebaa",
"https://deno.land/x/hono@v3.0.0/compose.ts": "9bc97f737857da061b2294eb9812b7e0ca773acc2e913c11bf93d5732c0469f0",
"https://deno.land/x/hono@v3.0.0/context.ts": "b053da7ec49c7a32f10572f951ae68347260ffab62fcce76cbc2ce544fe44d23",
"https://deno.land/x/hono@v3.0.0/hono.ts": "4929cefc738a4bd5efea2371f0c5338d27e9de91045b1c223ab98eda5a0364d5",
"https://deno.land/x/hono@v3.0.0/http-exception.ts": "e74a5e8504ecf795e9babdc2bab9d4052e1806a7396efc89f8e1ed83e813e3be",
"https://deno.land/x/hono@v3.0.0/middleware.ts": "d2c886e2f63b91b17f05a11f598ca101912c8fbfd66a25aef345b934ccfcd603",
"https://deno.land/x/hono@v3.0.0/middleware/basic-auth/index.ts": "0664ddf00c9f08a5109f92e93264678de55e90c341955c746bafa8f99ecab1eb",
"https://deno.land/x/hono@v3.0.0/middleware/bearer-auth/index.ts": "11d4ead9b57f5bcb2b6b4bf27076871f15da0e1e8828b2b79d90c15423357b47",
"https://deno.land/x/hono@v3.0.0/middleware/cache/index.ts": "2e86e089ebce01611ab5e231a6fbf7953ebabee3ab68362fcacea752ef48a0ab",
"https://deno.land/x/hono@v3.0.0/middleware/compress/index.ts": "0b8ddbd70688361d5ef4e9418afa28affe2500a7f41a231a87873dad26ef5548",
"https://deno.land/x/hono@v3.0.0/middleware/cors/index.ts": "10a743dcc793204835a5299070e2afaabb7e81cb742262faa9f1181f5e5d65ac",
"https://deno.land/x/hono@v3.0.0/middleware/etag/index.ts": "e679c30ddd2600087521a7c1dac3798c2319c09d203bb627b54357b984fc72e7",
"https://deno.land/x/hono@v3.0.0/middleware/html/index.ts": "a5028d8170dcc030d003749e743213e6532ff65798b741b81220207abc9af82d",
"https://deno.land/x/hono@v3.0.0/middleware/jsx/index.ts": "1925e4bf01ef1252b9bda6b0c4f79520b138ee319c41df3218cb3bf10c0ed248",
"https://deno.land/x/hono@v3.0.0/middleware/jwt/index.ts": "4af4649d9ae8ff2e767e53692d1974c956f523ab47a2560b869083cb493441ca",
"https://deno.land/x/hono@v3.0.0/middleware/logger/index.ts": "281b0fe431183a5d7b8d576645370efbd2737aeefaac7dc989d1c90dc03c52c0",
"https://deno.land/x/hono@v3.0.0/middleware/powered-by/index.ts": "7ec561885ac0410786f78aeb9789ed7869edb2d43c615cbc3d14f21baee87359",
"https://deno.land/x/hono@v3.0.0/middleware/pretty-json/index.ts": "f4a4b2fa2ecb73e23da6f0ef716fe7d6a7f05c69f64dc7f89fc68cbcb204a87f",
"https://deno.land/x/hono@v3.0.0/mod.ts": "e771d1c9f711b78f7540134e44a1ceda308fe0af58cb4c83ce7434c56c224670",
"https://deno.land/x/hono@v3.0.0/request.ts": "e3b38e76f7d13596266e644c8598d324c56ae73af263c3bf029f6ffeafdb221b",
"https://deno.land/x/hono@v3.0.0/router.ts": "21448bc2e6019574c10fae11237da4367037fa107e68bf3d049cd2fd0efd2adb",
"https://deno.land/x/hono@v3.0.0/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db",
"https://deno.land/x/hono@v3.0.0/router/reg-exp-router/node.ts": "8006b5bccb83d9fc98e0562a5545f6dd0be639ce445b089a6171c9c617aa8693",
"https://deno.land/x/hono@v3.0.0/router/reg-exp-router/router.ts": "35a405c855cf6d10c350a651169c2c728fd19aaf3d3a0c416e5a06752914e6bd",
"https://deno.land/x/hono@v3.0.0/router/reg-exp-router/trie.ts": "567493b301c44174f0895aedb8d055bbecf88f8a25626fa8ca61333bbd0c882c",
"https://deno.land/x/hono@v3.0.0/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef",
"https://deno.land/x/hono@v3.0.0/router/smart-router/router.ts": "1d54f5c87875d856ed5fc2d22a100e1ff31debe3e9d8e9b1cc18d8e5706239f2",
"https://deno.land/x/hono@v3.0.0/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41",
"https://deno.land/x/hono@v3.0.0/router/trie-router/node.ts": "ca5b6a1ce6b6dc01003809bfa9cb93a323b37b27feee14c64655015deff7d2a9",
"https://deno.land/x/hono@v3.0.0/router/trie-router/router.ts": "0a969528a0c1680b552b20f0ca90e484e968ac279be9d5fd952b61a804d680e7",
"https://deno.land/x/hono@v3.0.0/types.ts": "a96ee9693b0de61b42016b80d3cbd958fabb795d8e4becb455845d70f66e7c87",
"https://deno.land/x/hono@v3.0.0/utils/body.ts": "b6b5ed679122968a74845df4c5454c677f09adc4f3466d822f3b1397884e540e",
"https://deno.land/x/hono@v3.0.0/utils/buffer.ts": "d28ab08d2571e890ec2ad7ce4c0318a503094f8403eac3d5eb18a8e5c23b29b2",
"https://deno.land/x/hono@v3.0.0/utils/cookie.ts": "545872bd7af3b455c24fd386ecbccfd161e7d4a0038d6b09b1bb22723602f90a",
"https://deno.land/x/hono@v3.0.0/utils/crypto.ts": "bda0e141bbe46d3a4a20f8fbcb6380d473b617123d9fdfa93e4499410b537acc",
"https://deno.land/x/hono@v3.0.0/utils/encode.ts": "b628be2de7ab48cc806b1e5b93b3baf6886b7656d6d65dfa80f4fcd9c0f63a5f",
"https://deno.land/x/hono@v3.0.0/utils/filepath.ts": "5f708bb6a2f0b8e83a3333868ac86abdfba15e52b46acb5e13018fa2138f0826",
"https://deno.land/x/hono@v3.0.0/utils/html.ts": "636c4a04eaea1c52c14a37cd28d83cba66b293d1e31420a4475e951268901ae4",
"https://deno.land/x/hono@v3.0.0/utils/http-status.ts": "2d6003e352c1fe918db663fa4bd2b20bf0b9d4e1699ba5e163f317f00b29d938",
"https://deno.land/x/hono@v3.0.0/utils/jwt/index.ts": "5e4b82a42eb3603351dfce726cd781ca41cb57437395409d227131aec348d2d5",
"https://deno.land/x/hono@v3.0.0/utils/jwt/jwt.ts": "4fefadfb914ad88282949d0cce2be16bdbd0b0614d15ecbb340c4fc47a9929b9",
"https://deno.land/x/hono@v3.0.0/utils/jwt/types.ts": "715514fe59f0c37048b2940528bd4f4ec5795e04f0bc1d6d7e33f8d8bb1fe9de",
"https://deno.land/x/hono@v3.0.0/utils/mime.ts": "e17bdbac85b97c3d223c48874c2abe867e0720461e9f7e0c340141c080b9c6d6",
"https://deno.land/x/hono@v3.0.0/utils/types.ts": "173dedfe018b447cc6b067d2b6968c1f1dccba67ad50526d356b79e0465a5753",
"https://deno.land/x/hono@v3.0.0/utils/url.ts": "2cf0f38d976761296ccaf2622b6999b7c0c1d3a1f63f066a6f832288ea7c28d9",
"https://deno.land/x/hono@v3.0.0/validator/index.ts": "3dc2c9418dee74333ea8b98642ce112b6d449042bbcefe1e835047fcf2458170",
"https://deno.land/x/hono@v3.0.0/validator/validator.ts": "9b2c9983b6b8a31cbc1f687096e232fb159004c15421e39cb050da2ac0f78c95",
"https://deno.land/x/hono@v4.3.11/adapter/deno/serve-static.ts": "db226d30f08f1a8bb77653ead42a911357b2f8710d653e43c01eccebb424b295",
"https://deno.land/x/hono@v4.3.11/compose.ts": "37d6e33b7db80e4c43a0629b12fd3a1e3406e7d9e62a4bfad4b30426ea7ae4f1",
"https://deno.land/x/hono@v4.3.11/context.ts": "facfd749d823a645039571d66d9d228f5ae6836818b65d3b6c4c6891adfe071e",
"https://deno.land/x/hono@v4.3.11/hono-base.ts": "fd7e9c1bba1e13119e95158270011784da3a7c3014c149ba0700e700f840ae0d",
"https://deno.land/x/hono@v4.3.11/hono.ts": "23edd0140bf0bd5a68c14ae96e5856a5cec6b844277e853b91025e91ea74f416",
"https://deno.land/x/hono@v4.3.11/http-exception.ts": "f5dd375e61aa4b764eb9b99dd45a7160f8317fd36d3f79ae22585b9a5e8ad7c5",
"https://deno.land/x/hono@v4.3.11/middleware/serve-static/index.ts": "14b760bbbc4478cc3a7fb9728730bc6300581c890365b7101b80c16e70e4b21e",
"https://deno.land/x/hono@v4.3.11/request.ts": "7b08602858e642d1626c3106c0bedc2aa8d97e30691a079351d9acef7c5955e6",
"https://deno.land/x/hono@v4.3.11/router.ts": "880316f561918fc197481755aac2165fdbe2f530b925c5357a9f98d6e2cc85c7",
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db",
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/node.ts": "7efaa6f4301efc2aad0519c84973061be8555da02e5868409293a1fd98536aaf",
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/router.ts": "632f2fa426b3e45a66aeed03f7205dad6d13e8081bed6f8d1d987b6cad8fb455",
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/trie.ts": "852ce7207e6701e47fa30889a0d2b8bfcd56d8862c97e7bc9831e0a64bd8835f",
"https://deno.land/x/hono@v4.3.11/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef",
"https://deno.land/x/hono@v4.3.11/router/smart-router/router.ts": "dc22a8505a0f345476f07dca3054c0c50a64d7b81c9af5a904476490dfd5cbb4",
"https://deno.land/x/hono@v4.3.11/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41",
"https://deno.land/x/hono@v4.3.11/router/trie-router/node.ts": "d3e00e8f1ba7fb26896459d5bba882356891a07793387c4655d1864c519a91de",
"https://deno.land/x/hono@v4.3.11/router/trie-router/router.ts": "54ced78d35676302c8fcdda4204f7bdf5a7cc907fbf9967c75674b1e394f830d",
"https://deno.land/x/hono@v4.3.11/types.ts": "b561c3ee846121b33c2d81331246cdedf7781636ed72dad7406677105b4275de",
"https://deno.land/x/hono@v4.3.11/utils/body.ts": "774cb319dfbe886a9d39f12c43dea15a39f9d01e45de0323167cdd5d0aad14d4",
"https://deno.land/x/hono@v4.3.11/utils/filepath.ts": "a83e5fe87396bb291a6c5c28e13356fcbea0b5547bad2c3ba9660100ff964000",
"https://deno.land/x/hono@v4.3.11/utils/html.ts": "6ea4f6bf41587a51607dff7a6d2865ef4d5001e4203b07e5c8a45b63a098e871",
"https://deno.land/x/hono@v4.3.11/utils/http-status.ts": "f5b820f2793e45209f34deddf147b23e3133a89eb4c57dc643759a504706636b",
"https://deno.land/x/hono@v4.3.11/utils/mime.ts": "d1fc2c047191ccb01d736c6acf90df731324536298181dba0ecc2259e5f7d661",
"https://deno.land/x/hono@v4.3.11/utils/types.ts": "050bfa9dc6d0cc4b7c5069944a8bd60066c2f9f95ee69833623ad104f11f92bf",
"https://deno.land/x/hono@v4.3.11/utils/url.ts": "855169632c61d03703bd08cafb27664ba3fdb352892f01687d5cce8fd49e3cb1",
"https://deno.land/x/imagescript@1.3.0/ImageScript.js": "cf90773c966031edd781ed176c598f7ed495e7694cd9b86c986d2d97f783cca0",
"https://deno.land/x/imagescript@1.3.0/mod.ts": "18a6cb83c55e690c873505f6fe867364c678afb64934fe7aef593a6b92f79995",
"https://deno.land/x/imagescript@1.3.0/png/src/crc.mjs": "5cf50de181d61dd00e66a240d811018ba5070afa8bba302f393604404604de84",
@ -611,7 +528,6 @@
"https://deno.land/x/socket_io@0.2.0/packages/socket.io/mod.ts": "dfd465bdcf23161af0c4d79fb8fc8912418c46a20d15e8b314cec6d9fb508196",
"https://deno.land/x/socket_io@0.2.0/test_deps.ts": "1f9dfa07a1e806ccddc9fa5f7255338d9dff67c40d7e83795f4f0f7bd710bde9",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/backoff.ts": "33e4a6e245f8743fbae0ce583993a671a3ac2ecee433a3e7f0bd77b5dd541d84",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/command.ts": "802df3a1f49f6c49fe3e8fcf13fd0cc360b8a02369de0310a72d7f0c8e4ceaab",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/connection.ts": "c31d2e0cb360bc641e7286f1d53cf58790fbcda025c06887f84a821f39d0fdff",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/errors.ts": "bc8f7091cb9f36cdd31229660e0139350b02c26851e3ac69d592c066745feb27",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/executor.ts": "03e5f43df4e0c9c62b0e1be778811d45b6a1966ddf406e21ed5a227af70b7183",
@ -621,7 +537,6 @@
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/command.ts": "b1efd3b62fe5d1230e6d96b5c65ba7de1592a1eda2cc927161e5997a15f404ac",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/mod.ts": "f2601df31d8adc71785b5d19f6a7e43dfce94adbb6735c4dafc1fb129169d11a",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/reply.ts": "beac2061b03190bada179aef1a5d92b47a5104d9835e8c7468a55c24812ae9e4",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/types.ts": "40b0a568cb7fd4dc9107997062584d24e5c6ffa1f21acb6410aa19c92f89e9e1",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/pubsub.ts": "324b87dae0700e4cb350780ce3ae5bc02780f79f3de35e01366b894668b016c6",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/redis.ts": "a5c2cf8c72e7c92c9c8c6911f98227062649f6cba966938428c5414200f3aa54",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/stream.ts": "f116d73cfe04590ff9fa8a3c08be8ff85219d902ef2f6929b8c1e88d92a07810",
@ -631,12 +546,13 @@
},
"workspace": {
"dependencies": [
"jsr:@hono/hono@^4.6.11"
"jsr:@std/fs@^1.0.14"
],
"packageJson": {
"dependencies": [
"npm:fs@^0.0.1-security",
"npm:http@^0.0.1-security",
"npm:jose@5.9.6",
"npm:nodemon@^3.1.7",
"npm:path@~0.12.7",
"npm:socket.io@^4.8.1",

144
main.ts
View file

@ -1,144 +0,0 @@
import { serve } from "https://deno.land/std@0.162.0/http/server.ts";
import { sign } from 'hono/jwt';
import { Hono } from "https://deno.land/x/hono@v3.0.0/mod.ts";
import { serveStatic } from "https://deno.land/x/hono@v3.0.0/middleware.ts";
import { env } from 'hono/adapter';
import { validator } from 'hono/validator';
import { io } from "./io.ts";
import * as world from "./constants/world.ts";
import { Room } from "./types.ts";
import * as schemas from "./schema.ts";
import { getAccount } from "./utils.ts";
import parties from "./constants/parties.json" with { type: 'json' };
const app = new Hono();
app.get('/*', serveStatic({ root: './public' }));
// APIs for debugging and other purposes
app.get('/api/server/players', (c) => c.json({ players: world.players }));
app.get('/api/server/rooms', (c) => c.json(world.rooms));
app.get('/api/server/persistence', async (c) => {
const account = await getAccount();
return c.json({
success: true,
data: account
});
})
// APIs for use by the client
app.post('/api/client/login', validator('json', async (_value, c) => {
try {
const body = await c.req.json();
const parsed = schemas.login.safeParse(body);
if (!parsed.success) {
return c.json({
success: false,
message: "Validation failure",
error: parsed.error
}, 400);
};
return parsed.data;
} catch(_e) {
return c.json({
success: false,
message: "Bad request"
}, 400);
}
// deno-lint-ignore no-explicit-any
}) as any, async (c) => {
const body = c.req.valid('json') as {
nickname: string,
critterId: string,
partyId: string,
persistent: boolean,
mods: Array<string>
};
const _players = Object.values(world.players);
if (_players.find((player) => player.n == body.nickname) || world.queue.includes(body.nickname)) {
return c.json({
success: false,
message: "There is already a player with this nickname online."
});
}
const JWT_CONTENT = {
sub: {
playerId: crypto.randomUUID(),
...body // ZOD validator is set to make the body strict, so this expansion should be fine
},
exp: Math.floor(Date.now() / 1000) + 60 * 5 // 5 mins expiry
};
//@ts-ignore: Deno lint
const { JWT_TOKEN } = env<{ JWT_TOKEN: string }>(c);
const token = await sign(JWT_CONTENT, JWT_TOKEN);
world.queue.push(body.nickname);
return c.json({
success: true,
playerId: JWT_CONTENT.sub.playerId,
token: token
});
});
app.get('/api/client/rooms', (c) => {
const partyId = c.req.query('partyId') || 'default';
if (!parties.includes(partyId)) {
return c.json({
success: false,
message: "Invalid partyId hash provided."
});
}
let missing = 0;
const roomResponse = Object.keys(world.rooms).reduce((res: Array<Room>, roomId) => {
const room = world.rooms[roomId];
if (room[partyId]) {
if (!room[partyId].partyExclusive || room[partyId].partyExclusive.includes(partyId)) {
res.push(room[partyId]);
} else {
missing++;
}
} else {
if (!room.default.partyExclusive || room.default.partyExclusive.includes(partyId)) {
res.push(room.default);
} else {
missing++;
}
}
return res;
}, []);
if (missing == Object.keys(world.rooms).length) {
return c.json({
success: false,
message: "No rooms were fetched while indexxing using the specified partyId hash."
});
}
const res = roomResponse.filter((room) => room != null);
if (c.req.query('debug')) {
const roomNames = res.map((room) => room.name);
return c.json({
parties: parties,
data: roomNames
});
}
return c.json({
parties: parties,
data: res
});
});
const handler = io.handler(async (req) => {
return await app.fetch(req);
});
await serve(handler, { port: 3257 });

4
package-lock.json generated
View file

@ -1,11 +1,11 @@
{
"name": "box-critters-revival",
"name": "localbox",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "box-critters-revival",
"name": "localbox",
"version": "1.0.0",
"license": "ISC",
"dependencies": {

View file

@ -1,21 +1,23 @@
{
"name": "box-critters-revival",
"name": "localbox",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon main.js",
"dev": "nodemon main.js"
"description": "A Typescript server emulator for Box Critters, a defunct virtual world by RocketSnail games.",
"author": {
"name": "Index",
"url": "https://github.com/indexxing/"
},
"author": "",
"license": "ISC",
"license": "MIT",
"dependencies": {
"fs": "^0.0.1-security",
"http": "^0.0.1-security",
"jose": "5.9.6",
"nodemon": "^3.1.7",
"path": "^0.12.7",
"socket.io": "^4.8.1",
"zod": "^3.24.1"
}
},
"repository": "github:Box-Critters-Localbox/Localbox",
"contributors": [
"https://github.com/jonastisell"
]
}

View file

@ -1,25 +0,0 @@
import { z } from 'zod';
import parties from './constants/parties.json' with { type: 'json' };
/*
LOGIN API
*/
export const login = z.object({
nickname: z.string().nonempty().max(25),
critterId: z.enum([
"hamster",
"beaver",
"lizard",
"raccoon",
"penguin",
"snail",
"snow_greeter",
"snowkeeper",
"snowgirl",
"snow_patrol",
"snowgrandma"
]).default("hamster"),
partyId: z.enum(parties as [string, ...string[]]).default("default"),
persistent: z.boolean().default(false),
mods: z.array(z.enum(["roomExits"])).default([])
}).strict(); // Strict to disallow extra keys

View file

@ -1,16 +1,14 @@
// deno-lint-ignore-file no-explicit-any
import { Server } from "https://deno.land/x/socket_io@0.2.0/mod.ts";
import { decode } from 'hono/jwt';
import chalk from "https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js";
import { z } from "zod";
import * as world from "./constants/world.ts";
import * as items from "./constants/items.ts";
import * as utils from "./utils.ts";
import { LocalPlayer, PlayerCrumb, ShopData, CritterId } from "./types.ts";
import * as world from "../constants/world.ts";
import * as items from "../constants/items.ts";
import * as utils from "../src/utils.ts";
import { CritterId, LocalPlayer, PlayerCrumb, ShopData } from "../src/types.ts";
import parties from "./constants/parties.json" with { type: "json" };
import itemsJSON from "./public/base/items.json" with { type: "json" };
import parties from "../constants/parties.json" with { type: "json" };
import itemsJSON from "../public/base/items.json" with { type: "json" };
export const io = new Server();
io.on("connection", (socket) => {
@ -18,21 +16,23 @@ io.on("connection", (socket) => {
/** Condensed player data that is sufficient enough for other clients */
let localCrumb: PlayerCrumb;
// TODO: implement checking PlayFab API with ticket
socket.once("login", async (ticket: string) => {
if (z.object({
ticket: z.string()
}).safeParse({ ticket: ticket }).success == false) return;
if (
z.object({
ticket: z.string(),
}).safeParse({ ticket: ticket }).success == false
) return;
let playerData;
try {
playerData = decode(ticket);
} catch(_e) {
playerData = await utils.verifyJWT(ticket);
} catch (_e) {
socket.disconnect(true);
return
return;
}
// TODO: make this just an inline function instead of having an onPropertyChange function, I'm just really lazy right now lol -index
function onPropertyChange(property: string, value: any) {
utils.updateAccount(localPlayer.nickname, property, value);
@ -40,7 +40,7 @@ io.on("connection", (socket) => {
const createArrayHandler = (propertyName: string) => ({
get(target: any, property: string) {
if (typeof target[property] === 'function') {
if (typeof target[property] === "function") {
return function (...args: any[]) {
const result = target[property].apply(target, args);
onPropertyChange(propertyName, target);
@ -48,7 +48,7 @@ io.on("connection", (socket) => {
};
}
return target[property];
}
},
});
const handler = {
@ -67,18 +67,28 @@ io.on("connection", (socket) => {
return new Proxy(target[property], createArrayHandler(property));
}
return target[property];
}
},
};
const payload = playerData.payload;
const sub = payload.sub as {
playerId: string,
nickname: string,
critterId: CritterId,
partyId: string,
persistent: boolean,
mods: Array<string>
//@ts-ignore: I will fix the type errors with using a different JWT library eventually
const sub = playerData as {
playerId: string;
nickname: string;
critterId: CritterId;
partyId: string;
persistent: boolean;
mods: Array<string>;
};
if ([
"today2019",
"today2020",
"today2021"
].includes(sub.partyId)) {
console.log('target year:', parseInt(sub.partyId.replace('today', '')));
sub.partyId = utils.getCurrentEvent(parseInt(sub.partyId.replace('today', '')))
};
const persistentAccount = await utils.getAccount(sub.nickname);
if (!sub.persistent || persistentAccount.individual == null) {
localPlayer = {
@ -100,22 +110,28 @@ io.on("connection", (socket) => {
mutes: [],
_partyId: sub.partyId, // This key is replaced down the line anyway
_mods: []
_mods: [],
};
if (sub.persistent) {
utils.createAccount(localPlayer);
localPlayer = new Proxy<LocalPlayer>(utils.expandAccount(localPlayer), handler);
};
localPlayer = new Proxy<LocalPlayer>(
utils.expandAccount(localPlayer),
handler,
);
}
} else {
persistentAccount.individual.critterId = sub.critterId || "hamster";
persistentAccount.individual._partyId = sub.partyId || "default";
persistentAccount.individual._mods = sub.mods || [];
localPlayer = new Proxy<LocalPlayer>(utils.expandAccount(persistentAccount.individual), handler);
localPlayer = new Proxy<LocalPlayer>(
utils.expandAccount(persistentAccount.individual),
handler,
);
}
localPlayer._partyId = socket.handshake.query.get('partyId') || 'default';
localPlayer._partyId = socket.handshake.query.get("partyId") || "default";
world.queue.splice(world.queue.indexOf(localPlayer.nickname), 1);
localCrumb = utils.makeCrumb(localPlayer, world.spawnRoom);
@ -129,9 +145,11 @@ io.on("connection", (socket) => {
});
socket.on("joinRoom", (roomId: string) => {
if (z.object({
roomId: z.enum(Object.keys(world.rooms) as any)
}).safeParse({ roomId: roomId }).success == false) return;
if (
z.object({
roomId: z.enum(Object.keys(world.rooms) as any),
}).safeParse({ roomId: roomId }).success == false
) return;
const _room = (world.rooms[roomId] || { default: null }).default;
if (!_room) return;
@ -139,49 +157,54 @@ io.on("connection", (socket) => {
socket.leave(localCrumb._roomId);
socket.broadcast.in(localCrumb._roomId).emit("R", localCrumb);
const modEnabled = (localPlayer._mods || []).includes('roomExits');
const modEnabled = (localPlayer._mods || []).includes("roomExits");
//@ts-ignore: Index type is correct
const correctExit = world.roomExits[localCrumb._roomId + "->" + roomId]
const correctExit = world.roomExits[localCrumb._roomId + "->" + roomId];
if (modEnabled && correctExit) {
localPlayer.x = correctExit.x;
localPlayer.y = correctExit.y;
localPlayer.rotation = correctExit.r;
};
}
if (!modEnabled || !correctExit) {
localPlayer.x = _room.startX;
localPlayer.y = _room.startY;
localPlayer.rotation = _room.startR | 180;
};
}
localCrumb = utils.makeCrumb(localPlayer, roomId);
world.players[localPlayer.playerId] = localCrumb;
console.log(chalk.green('> ' + localPlayer.nickname + ' joined "' + roomId + '"!'));
console.log("> " + localPlayer.nickname + ' joined "' + roomId + '"!');
socket.join(roomId);
let playerCrumbs = Object.values(world.players).filter((crumb) => crumb._roomId == roomId);
let playerCrumbs = Object.values(world.players).filter((crumb) =>
crumb._roomId == roomId
);
if (world.npcs[roomId]) {
playerCrumbs = [
...playerCrumbs,
...world.npcs[roomId]
...world.npcs[roomId],
];
};
}
socket.emit("joinRoom", {
name: _room.name,
roomId: roomId,
playerCrumbs: playerCrumbs
playerCrumbs: playerCrumbs,
});
socket.broadcast.in(localCrumb._roomId).emit("A", localCrumb);
});
socket.on("moveTo", (x: number, y: number) => {
const roomData = world.rooms[localCrumb._roomId][localPlayer._partyId];
if (z.object({
x: z.number().min(0).max(roomData.width),
y: z.number().min(0).max(roomData.height)
}).safeParse({ x: x, y: y }).success == false) return;
const roomData = world.rooms[localCrumb._roomId][localPlayer._partyId] ||
world.rooms[localCrumb._roomId].default;
if (
z.object({
x: z.number().min(0).max(roomData.width),
y: z.number().min(0).max(roomData.height),
}).safeParse({ x: x, y: y }).success == false
) return;
const newDirection = utils.getDirection(localPlayer.x, localPlayer.y, x, y);
@ -197,21 +220,23 @@ io.on("connection", (socket) => {
i: localPlayer.playerId,
x: x,
y: y,
r: newDirection
r: newDirection,
});
});
socket.on("message", (text: string) => {
if (z.object({
text: z.string().nonempty()
}).safeParse({ text: text }).success == false) return;
if (
z.object({
text: z.string().nonempty(),
}).safeParse({ text: text }).success == false
) return;
console.log(chalk.gray(`> ${localPlayer.nickname} sent message: "%s"`), text);
console.log(`> ${localPlayer.nickname} sent message:`, text);
localCrumb.m = text;
socket.broadcast.in(localCrumb._roomId).emit("M", {
i: localPlayer.playerId,
m: text
m: text,
});
setTimeout(() => {
@ -221,16 +246,18 @@ io.on("connection", (socket) => {
});
socket.on("emote", (emote: string) => {
if (z.object({
emote: z.string().nonempty() // TODO: make this an enum
}).safeParse({ emote: emote }).success == false) return;
if (
z.object({
emote: z.string().nonempty(), // TODO: make this an enum
}).safeParse({ emote: emote }).success == false
) return;
console.log(chalk.gray(`> ${localPlayer.nickname} sent emote: %s`), emote);
console.log(`> ${localPlayer.nickname} sent emote:`, emote);
localCrumb.e = emote;
socket.broadcast.in(localCrumb._roomId).emit("E", {
i: localPlayer.playerId,
e: emote
e: emote,
});
setTimeout(() => {
@ -241,55 +268,65 @@ io.on("connection", (socket) => {
// ? Options is specified just because sometimes it is sent, but its always an empty string
socket.on("code", (code: string, _options?: string) => {
if (z.object({
command: z.enum([
"pop",
"freeitem",
"tbt",
"darkmode",
"spydar",
"allitems"
])
}).safeParse({
command: code
}).success == false) return;
if (
z.object({
command: z.enum([
"pop",
"freeitem",
"tbt",
"darkmode",
"spydar",
"allitems",
]),
}).safeParse({
command: code,
}).success == false
) return;
console.log(chalk.gray(`> ${localPlayer.nickname} sent code: %s`), code);
console.log(`> ${localPlayer.nickname} sent code:`, code);
const addItem = function(id: string, showGUI: boolean = false) {
const addItem = function (id: string, showGUI: boolean = false) {
if (!localPlayer.inventory.includes(id)) {
socket.emit("addItem", { itemId: id, showGUI: showGUI });
localPlayer.inventory.push(id);
}
}
};
// Misc. Codes
switch(code) {
case 'pop': {
socket.emit("pop", Object.values(world.players).filter((critter) => critter.c != "huggable").length);
switch (code) {
case "pop": {
socket.emit(
"pop",
Object.values(world.players).filter((critter) =>
critter.c != "huggable"
).length,
);
break;
}
case 'freeitem': {
case "freeitem": {
addItem(items.shop.freeItem.itemId, true);
break;
}
case 'tbt': {
const _throwbackItem = utils.getNewCodeItem(localPlayer, items.throwback);
case "tbt": {
const _throwbackItem = utils.getNewCodeItem(
localPlayer,
items.throwback,
);
if (_throwbackItem) addItem(_throwbackItem, true);
break;
}
case 'darkmode': {
case "darkmode": {
addItem("3d_black", true);
break;
}
case 'spydar': {
case "spydar": {
localPlayer.gear = [
"sun_orange",
"super_mask_black",
"toque_blue",
"dracula_cloak",
"headphones_black",
"hoodie_black"
"hoodie_black",
];
if (localCrumb._roomId == "tavern") {
@ -302,60 +339,65 @@ io.on("connection", (socket) => {
io.in(localCrumb._roomId).volatile.emit("X", {
i: localPlayer.playerId,
x: 216,
y: 118
y: 118,
});
}
io.in(localCrumb._roomId).emit("G", {
i: localPlayer.playerId,
g: localPlayer.gear
g: localPlayer.gear,
});
socket.emit("updateGear", localPlayer.gear);
break;
}
case 'allitems': {
case "allitems": {
for (const item of itemsJSON) {
addItem(item.itemId, false);
}
break;
}
};
}
// Item Codes
const _itemCodes = items.codes as Record<string, string|Array<string>>
const _itemCodes = items.codes as Record<string, string | Array<string>>;
const item = _itemCodes[code];
if (typeof(item) == "string") {
if (typeof item == "string") {
addItem(item, true);
} else if (typeof(item) == "object") {
} else if (typeof item == "object") {
for (const _ of item) {
addItem(_, true);
}
}
// Event Codes (eg. Christmas 2019)
const _eventItemCodes = items.eventCodes as Record<string, Record<string, string>>;
const _eventItemCodes = items.eventCodes as Record<
string,
Record<string, string>
>;
const eventItem = (_eventItemCodes[localPlayer._partyId] || {})[code];
if (eventItem) addItem(eventItem);
});
socket.on("updateGear", (gear: Array<string>) => {
if (z.object({
gear: z.array(z.string().nonempty()).default([])
}).strict().safeParse({ gear: gear }).success == false) return;
if (
z.object({
gear: z.array(z.string().nonempty()).default([]),
}).strict().safeParse({ gear: gear }).success == false
) return;
const _gear = [];
for (const itemId of gear) {
if (localPlayer.inventory.includes(itemId)) {
_gear.push(itemId)
_gear.push(itemId);
}
}
localPlayer.gear = _gear;
io.in(localCrumb._roomId).emit("G", {
i: localPlayer.playerId,
g: localPlayer.gear
g: localPlayer.gear,
});
socket.emit("updateGear", localPlayer.gear);
@ -367,27 +409,41 @@ io.on("connection", (socket) => {
lastItem: _shopItems.lastItem.itemId,
freeItem: _shopItems.freeItem.itemId,
nextItem: _shopItems.nextItem.itemId,
collection: _shopItems.collection.map((item) => item.itemId)
})
collection: _shopItems.collection.map((item) => item.itemId),
});
});
socket.on("buyItem", (itemId: string) => {
if (z.object({
itemId: z.string().nonempty()
}).strict().safeParse({ itemId: itemId }).success == false) return;
if (
z.object({
itemId: z.string().nonempty(),
}).strict().safeParse({ itemId: itemId }).success == false
) return;
// ? Free item is excluded from this list because the game just sends the "/freeitem" code
const currentShop = items.shop;
const _shopItems = [currentShop.lastItem, currentShop.nextItem, ...currentShop.collection]
const _shopItems = [
currentShop.lastItem,
currentShop.nextItem,
...currentShop.collection,
];
const target = _shopItems.find((item) => item.itemId == itemId)!;
if (!target) {
console.log(chalk.red("> There is no item in this week's shop with itemId: %s"), itemId);
console.log(
"> There is no item in this week's shop with itemId:",
itemId,
);
return;
};
}
if (localPlayer.coins >= target.cost && !localPlayer.inventory.includes(itemId)) {
console.log(chalk.green("[+] Bought item: %s for %d coins"), itemId, target.cost);
if (
localPlayer.coins >= target.cost &&
!localPlayer.inventory.includes(itemId)
) {
console.log(
"[+] Bought item: " + itemId + " for " + target.cost + " coins",
);
localPlayer.coins -= target.cost;
localPlayer.inventory.push(itemId);
@ -397,7 +453,11 @@ io.on("connection", (socket) => {
});
socket.on("trigger", async () => {
const activatedTrigger = await utils.getTrigger(localPlayer, localCrumb._roomId, localPlayer._partyId);
const activatedTrigger = await utils.getTrigger(
localPlayer,
localCrumb._roomId,
localPlayer._partyId,
);
if (!activatedTrigger) return;
if (activatedTrigger.hasItems) {
@ -408,7 +468,7 @@ io.on("connection", (socket) => {
if (activatedTrigger.grantItem) {
let items = activatedTrigger.grantItem;
if (typeof(items) == 'string') items = [items];
if (typeof items == "string") items = [items];
for (const item of items) {
if (!localPlayer.inventory.includes(item)) {
@ -426,23 +486,32 @@ io.on("connection", (socket) => {
});
socket.on("addIgnore", (playerId: string) => {
if (z.object({
playerId: z.enum(Object.keys(world.players) as any)
}).strict().safeParse({ playerId: playerId }).success == false) return;
if (
z.object({
playerId: z.enum(Object.keys(world.players) as any),
}).strict().safeParse({ playerId: playerId }).success == false
) return;
if (Object.keys(world.players).includes(playerId) && !localPlayer.ignore.includes(playerId)) {
if (
Object.keys(world.players).includes(playerId) &&
!localPlayer.ignore.includes(playerId)
) {
localPlayer.ignore.push(playerId);
};
}
});
socket.on("attack", (playerId: string) => {
if (z.object({
playerId: z.enum(Object.keys(world.players) as any)
}).strict().safeParse({ playerId: playerId }).success == false) return;
if (
z.object({
playerId: z.enum(Object.keys(world.players) as any),
}).strict().safeParse({ playerId: playerId }).success == false
) return;
if (!localPlayer.gear.includes("bb_beebee")) return;
const monster = Object.values(world.players).find((player) =>
player.i == playerId && player.c == "huggable"
);
if (!localPlayer.gear.includes('bb_beebee')) return;
const monster = Object.values(world.players).find((player) => player.i == playerId && player.c == "huggable");
if (monster) {
io.in(localCrumb._roomId).emit("R", monster);
@ -450,13 +519,15 @@ io.on("connection", (socket) => {
socket.emit("updateCoins", { balance: localPlayer.coins });
delete world.players[playerId];
};
}
});
socket.on("switchParty", (partyId: string) => {
if (z.object({
partyId: z.enum(parties as any)
}).strict().safeParse({ partyId: partyId }).success == false) return;
if (
z.object({
partyId: z.enum(Object.keys(parties) as any),
}).strict().safeParse({ partyId: partyId }).success == false
) return;
localPlayer._partyId = partyId;
socket.emit("switchParty");
@ -470,6 +541,6 @@ io.on("connection", (socket) => {
if (localPlayer && localCrumb) {
io.in(localCrumb._roomId).emit("R", localCrumb);
delete world.players[localPlayer.playerId];
};
}
});
});
});

232
src/main.ts Normal file
View file

@ -0,0 +1,232 @@
import { serve } from "https://deno.land/std@0.162.0/http/server.ts";
import { contentType } from "https://deno.land/std@0.224.0/media_types/mod.ts";
import {
dirname,
fromFileUrl,
join,
normalize,
} from "https://deno.land/std@0.224.0/path/mod.ts";
import { exists } from "jsr:@std/fs/exists";
import { io } from "./io.ts";
import * as world from "../constants/world.ts";
import { getAccount } from "./utils.ts";
import * as schemas from "./schema.ts";
import * as utils from "./utils.ts";
import parties from "../constants/parties.json" with { type: "json" };
import { extname } from "https://deno.land/std@0.212.0/path/extname.ts";
import { parseArgs } from "jsr:@std/cli/parse-args";
const EXECUTABLE = Deno.env.get("EXECUTABLE") == "true";
const BASE_DIR = EXECUTABLE
? dirname(Deno.execPath())
: dirname(dirname(fromFileUrl(Deno.mainModule)));
const PUBLIC_DIR = join(BASE_DIR, "public");
if (!EXECUTABLE) {
if (!await exists("./public") || !await exists(".env")) {
console.error("Missing files. Make sure you have `public/` and `.env`");
Deno.exit();
}
}
async function serveStatic(req: Request): Promise<Response> {
const url = new URL(req.url);
let pathname = url.pathname;
pathname = pathname.endsWith("/") ? pathname + "index.html" : pathname;
const fsPath = normalize(join(PUBLIC_DIR, pathname));
// Prevent directory traversal
if (!fsPath.startsWith(PUBLIC_DIR)) {
return new Response("Forbidden", { status: 403 });
}
try {
const file = await Deno.readFile(fsPath);
const mime = contentType(extname(fsPath)) || "application/octet-stream";
return new Response(file, {
headers: { "Content-Type": mime },
});
} catch {
return new Response("Not Found", { status: 404 });
}
}
async function handler(
req: Request,
connInfo: Deno.ServeHandlerInfo,
): Promise<Response> {
const url = new URL(req.url);
const pathname = url.pathname;
if (req.headers.get("upgrade") === "websocket") {
//@ts-ignore: The websocket successfully upgrades
return io.handler()(req, connInfo);
}
if (req.method == "POST" && pathname == "/api/client/login") {
try {
const body = await req.json();
const parsed = schemas.login.safeParse(body);
if (!parsed.success) {
return Response.json({
success: false,
message: "Validation failure",
error: parsed.error,
}, { status: 400 });
}
const data = parsed.data;
const _players = Object.values(world.players);
const nameInUse = _players.find((p) => p.n === data.nickname) ||
world.queue.includes(data.nickname);
if (nameInUse) {
return Response.json({
success: false,
message: "There is already a player with this nickname online.",
});
}
const JWT_CONTENT = {
playerId: crypto.randomUUID(),
...data,
};
const JWT_TOKEN = Deno.env.get("JWT_TOKEN");
if (!JWT_TOKEN) {
return new Response("JWT_TOKEN not set in env", { status: 500 });
}
const token = await utils.createJWT(JWT_CONTENT);
world.queue.push(data.nickname);
return Response.json({
success: true,
playerId: JWT_CONTENT.playerId,
token,
});
} catch {
return Response.json({
success: false,
message: "Bad request",
}, { status: 400 });
}
}
if (req.method == "GET") {
switch (pathname) {
case "/api/server/players": {
return Response.json({ players: world.players });
}
case "/api/server/rooms": {
return Response.json(world.rooms);
}
case "/api/server/persistence": {
const account = await getAccount();
return Response.json({
success: true,
data: account,
});
}
case "/api/client/rooms": {
const url = new URL(req.url);
let partyId = url.searchParams.get("partyId") || "default";
const debug = url.searchParams.has("debug");
if ([
"today2019",
"today2020",
"today2021"
].includes(partyId)) {
partyId = utils.getCurrentEvent(parseInt(partyId.replace('today', '')))
};
if (!Object.keys(parties).includes(partyId)) {
return Response.json({
success: false,
message: "Invalid partyId hash provided.",
});
}
let missing = 0;
const roomResponse = Object.keys(world.rooms).reduce(
(res, roomId) => {
const room = world.rooms[roomId];
if (room[partyId]) {
if (
!room[partyId].partyExclusive ||
room[partyId]?.partyExclusive?.includes(partyId)
) {
res.push(room[partyId]);
} else {
missing++;
}
} else {
if (
!room.default.partyExclusive ||
room.default.partyExclusive.includes(partyId)
) {
res.push(room.default);
} else {
missing++;
}
}
return res;
},
[] as Array<typeof world.rooms[string]["default"]>,
);
if (missing === Object.keys(world.rooms).length) {
return Response.json({
success: false,
message:
"No rooms were fetched while indexxing using the specified partyId hash.",
});
}
const partyIds = Object.keys(parties);
if (debug) {
const roomNames = roomResponse.map((room) => room.name);
return Response.json({
parties: partyIds,
data: roomNames,
});
}
return Response.json({
parties: partyIds,
data: roomResponse,
});
}
default: {
return serveStatic(req);
}
}
}
return new Response("Not Found", { status: 404 });
}
const args = parseArgs(Deno.args, {
string: ["port"],
default: {
port: "3257"
}
});
if (isNaN(Number(args.port))) {
console.log('Port provided is not valid.')
Deno.exit();
};
//@ts-ignore: Type issues occuring from upgrading websocket requests to Socket.io
await serve(handler, { port: args.port });

30
src/schema.ts Normal file
View file

@ -0,0 +1,30 @@
import { z } from "zod";
import parties from "../constants/parties.json" with { type: "json" };
/*
LOGIN API
*/
export const login = z.object({
nickname: z.string().nonempty().max(25),
critterId: z.enum([
"hamster",
"beaver",
"lizard",
"raccoon",
"penguin",
"snail",
"snow_greeter",
"snowkeeper",
"snowgirl",
"snow_patrol",
"snowgrandma",
]).default("hamster"),
partyId: z.enum([
...Object.keys(parties) as [string, ...string[]],
"today2019",
"today2020",
"today2021"
]).default("default"),
persistent: z.boolean().default(false),
mods: z.array(z.enum(["roomExits"])).default([]),
}).strict(); // Strict to disallow extra keys

140
src/types.ts Normal file
View file

@ -0,0 +1,140 @@
export type CritterId =
| "hamster"
| "snail"
| "lizard"
| "beaver"
| "raccoon"
| "penguin"
| "huggable";
export type Trigger = {
hex: string;
world?: { joinRoom: string };
room?: { hide: Array<string> };
server?: {
grantItem?: string | Array<string>;
hasItems?: Array<string>;
joinGame?: string;
addEgg?: string;
};
};
export type Room = {
roomId: string;
name: string;
width: number;
height: number;
startX: number;
startY: number;
startR: number;
media: {
background: string;
foreground?: string;
treasure?: string;
navMesh: string;
music?: string;
video?: string;
};
layout: string;
triggers: Array<Trigger>;
spriteSheet: string;
extra: null;
partyExclusive?: Array<string>;
};
export type LocalPlayer = {
playerId: string;
nickname: string;
critterId: CritterId;
ignore: Array<string>;
friends: Array<string>;
inventory: Array<string>;
gear: Array<string>;
/** Eggs is the term used to describe any object used in a scavenger hunt. Any prop name found in that list will be hidden and replaced with it's "_found" suffix prop counterpart */
eggs: Array<string>;
coins: number;
isMember: boolean | false;
isGuest: boolean | false;
isTeam: boolean | false;
x: number | 440;
y: number | 210;
rotation: number | 180;
mutes: Array<unknown>;
_partyId: string;
_mods: Array<string>;
// deno-lint-ignore no-explicit-any
[key: string]: any;
};
export type PlayerCrumb = {
/** Player ID */
i: string;
/** Player Nickname */
n: string;
/** Critter (Hamster, Beaver, Lizard, Snail, etc) */
c: CritterId;
x: number;
y: number;
r: number;
/** Gear (equipped items) */
g: Array<string>;
/** Message */
m: string;
/** Emote */
e: string;
_roomId: string;
};
export type ShopData = {
lastItem: { itemId: string; cost: number };
freeItem: { itemId: string; cost: number };
nextItem: { itemId: string; cost: number };
collection: Array<{ itemId: string; cost: number }>;
};
/*
Socket.io
*/
export interface ServerToClientEvents {
login: () => { player: LocalPlayer };
updateGear: () => { i: number; g: Array<string> };
updateCoins: () => { balance: number };
addItem: () => { itemId: string };
addEgg: () => string;
A: () => PlayerCrumb;
R: () => PlayerCrumb;
X: () => { i: number; x: number; y: number };
M: () => { i: number; m: string };
E: () => { i: number; e: string };
G: () => { i: number; g: Array<string> };
}
export interface ClientToServerEvents {
login: (ticket: string) => void;
joinLobby: () => unknown; // Unsure what this is for, I don't think the game had several servers
joinRoom: (
roomId: string,
) => { name: string; roomId: string; playerCrumbs: Array<PlayerCrumb> };
message: (text: string) => void;
emote: (emote: string) => void;
code: (code: string, options?: string) => void;
addIgnore: (id: number) => void;
addFriend: (id: number) => void;
moveTo: (x: number, y: number) => void;
updateCritter: () => unknown; // Unsure of what this is, maybe settings?
updateGear: (gear: Array<string>) => void;
getShop: () => ShopData;
buyItem: (itemId: string) => void;
trigger: () => void;
}
export type PartySchedule = {
[key: string]: {
start: string | null;
end: string | null;
};
};

262
src/utils.ts Normal file
View file

@ -0,0 +1,262 @@
import { Image } from "https://deno.land/x/imagescript@1.3.0/mod.ts";
import {
dirname,
fromFileUrl,
join,
} from "https://deno.land/std@0.224.0/path/mod.ts";
import { jwtVerify, SignJWT } from "npm:jose@5.9.6";
import { rooms, spawnRoom } from "../constants/world.ts";
import { LocalPlayer, PlayerCrumb, Room } from "./types.ts";
import parties from "../constants/parties.json" with { type: "json" };
const EXECUTABLE = Deno.env.get("EXECUTABLE") == "true";
const BASE_DIR = EXECUTABLE
? dirname(Deno.execPath())
: dirname(dirname(fromFileUrl(Deno.mainModule)));
const PUBLIC_DIR = join(BASE_DIR, "public");
// deno-lint-ignore no-explicit-any
export async function createJWT(payload: any): Promise<string> {
const jwt = await new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("1h")
.sign(new TextEncoder().encode(Deno.env.get("JWT_TOKEN")));
return jwt;
}
// deno-lint-ignore no-explicit-any
export async function verifyJWT(token: string): Promise<any | null> {
try {
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(Deno.env.get("JWT_TOKEN")),
);
return payload;
} catch (_e) {
return null;
}
}
/** Condenses the local player variable into data that is sufficient enough for other clients */
export function makeCrumb(player: LocalPlayer, roomId: string): PlayerCrumb {
return {
i: player.playerId,
n: player.nickname,
c: player.critterId,
x: player.x,
y: player.y,
r: player.rotation,
g: player.gear,
// message & emote
m: "",
e: "",
_roomId: roomId,
};
}
// TODO: use the correct triggers for the active party
export async function getTrigger(
player: LocalPlayer,
roomId: string,
partyId: string,
) {
const room = rooms[roomId][partyId];
if (!room) {
console.log(`[!] Cannot find room: "${roomId}@${partyId}"!`);
return;
}
try {
const treasureBuffer = await Deno.readFile(
//@ts-ignore: Deno lint
room.media.treasure?.replace("..", "public"),
);
const treasure = await Image.decode(treasureBuffer);
if (!treasure) {
throw new Error('Missing map server for room "' + roomId + '"!');
}
const pixel = treasure.getPixelAt(player.x, player.y);
const r = (pixel >> 24) & 0xFF,
g = (pixel >> 16) & 0xFF,
b = (pixel >> 8) & 0xFF;
const hexCode = ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0");
const trigger = room.triggers.find((trigger) => trigger.hex == hexCode);
if (trigger) {
return trigger.server;
} else {
return null;
}
} catch (e) {
console.warn("[!] Caught error while checking for activated trigger.", e);
}
}
export function getNewCodeItem(player: LocalPlayer, items: Array<string>) {
const itemsSet = new Set(player.inventory);
const available = items.filter((item) => !itemsSet.has(item));
return available.length === 0
? null
: available[Math.floor(Math.random() * available.length)];
}
/**
* Indexes the /media/rooms directory for all versions of all rooms
* @returns All versions of every room
*/
export async function indexRoomData() {
const _roomData: Record<string, Record<string, Room>> = {};
const basePath = join(PUBLIC_DIR, "media", "rooms");
const _rooms = Deno.readDir(basePath);
for await (const room of _rooms) {
if (room.isDirectory) {
_roomData[room.name] = {};
const roomPath = join(basePath, room.name);
const versions = Deno.readDir(roomPath);
for await (const version of versions) {
if (version.isDirectory) {
const versionPath = join(roomPath, version.name, "data.json");
try {
const data = await Deno.readTextFile(versionPath);
_roomData[room.name][version.name] = JSON.parse(data);
} catch (_) {
console.log(
`[!] "${room.name}@${version.name}" is missing a data.json file`,
);
}
}
}
}
}
return _roomData;
}
export async function getAccount(nickname?: string) {
let accounts = [];
try {
const data = await Deno.readTextFile("accounts.json");
accounts = JSON.parse(data);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
console.log("Persistent login JSON is missing, using blank JSON array..");
accounts = [];
} else {
console.log(
"[!] Failure to fetch persistent login data with nickname: ",
nickname,
);
throw error;
}
}
if (nickname) {
const existingAccount = accounts.find((player: { nickname: string }) =>
player.nickname == nickname
);
if (existingAccount) {
return {
all: accounts,
individual: existingAccount,
};
} else {
return {
all: accounts,
individual: null,
};
}
} else {
return accounts;
}
}
export async function updateAccount(
nickname: string,
property: string,
value: unknown,
) {
if (["x", "y", "rotation", "_partyId"].includes(property)) return;
const accounts = await getAccount(nickname);
accounts.individual[property] = value;
await Deno.writeTextFile(
"accounts.json",
JSON.stringify(accounts.all, null, 2),
);
}
export function trimAccount(player: LocalPlayer) {
for (
const key of [
"critterId",
"x",
"y",
"rotation",
"_partyId",
"_mods",
]
) {
delete player[key];
}
return player;
}
export function expandAccount(player: LocalPlayer) {
const defaultPos = rooms[spawnRoom].default;
player.x = defaultPos.startX;
player.y = defaultPos.startY;
player.rotation = defaultPos.startR;
return player;
}
export function getDirection(
x: number,
y: number,
targetX: number,
targetY: number,
) {
const a = Math.floor((180 * Math.atan2(targetX - x, y - targetY)) / Math.PI);
return a < 0 ? a + 360 : a;
}
export async function createAccount(player: LocalPlayer) {
const accounts = await getAccount();
accounts.push(trimAccount(player));
await Deno.writeTextFile("accounts.json", JSON.stringify(accounts, null, 2));
}
export function getCurrentEvent(year: number): string {
const today = new Date();
const testDate = new Date(year, today.getMonth(), today.getDate());
//@ts-ignore: Types are bugging out here for absolutely no reason
for (const [eventId, { start, end }] of Object.entries(parties)) {
if (!start || !end) continue;
const originalStart = new Date(start);
const originalEnd = new Date(end);
const adjustedStart = new Date(year, originalStart.getMonth(), originalStart.getDate());
const adjustedEnd = new Date(
// Handle roll over
originalEnd.getFullYear() > originalStart.getFullYear() ? year + 1 : year,
originalEnd.getMonth(),
originalEnd.getDate()
);
if (testDate >= adjustedStart && testDate <= adjustedEnd) {
return eventId;
}
}
return "default";
}

124
types.ts
View file

@ -1,124 +0,0 @@
export type CritterId = "hamster" | "snail" | "lizard" | "beaver" | "raccoon" | "penguin" | "huggable";
export type Trigger = {
hex: string,
world?: { joinRoom: string },
room?: { hide: Array<string> },
server?: {
grantItem?: string | Array<string>,
hasItems?: Array<string>,
joinGame?: string,
addEgg?: string
}
}
export type Room = {
roomId: string,
name: string,
width: number,
height: number,
startX: number,
startY: number,
startR: number,
media: {
background: string,
foreground?: string,
treasure?: string,
navMesh: string,
music?: string,
video?: string,
},
layout: string,
triggers: Array<Trigger>,
spriteSheet: string,
extra: null,
partyExclusive?: Array<string>
}
export type LocalPlayer = {
playerId: string,
nickname: string,
critterId: CritterId,
ignore: Array<string>,
friends: Array<string>,
inventory: Array<string>,
gear: Array<string>,
/** Eggs is the term used to describe any object used in a scavenger hunt. Any prop name found in that list will be hidden and replaced with it's "_found" suffix prop counterpart */
eggs: Array<string>,
coins: number,
isMember: boolean | false,
isGuest: boolean | false,
isTeam: boolean | false,
x: number | 440,
y: number | 210,
rotation: number | 180,
mutes: Array<unknown>,
_partyId: string,
_mods: Array<string>,
// deno-lint-ignore no-explicit-any
[key: string]: any
}
export type PlayerCrumb = {
/** Player ID */
i: string,
/** Player Nickname */
n: string,
/** Critter (Hamster, Beaver, Lizard, Snail, etc) */
c: CritterId,
x: number,
y: number,
r: number,
/** Gear (equipped items) */
g: Array<string>,
/** Message */
m: string,
/** Emote */
e: string,
_roomId: string
}
export type ShopData = {
lastItem: { itemId: string, cost: number },
freeItem: { itemId: string, cost: number },
nextItem: { itemId: string, cost: number },
collection: Array<{ itemId: string, cost: number }>
}
/*
Socket.io
*/
export interface ServerToClientEvents {
login: () => {player: LocalPlayer};
updateGear: () => { i: number, g: Array<string> };
updateCoins: () => { balance: number };
addItem: () => { itemId: string };
addEgg: () => string;
A: () => PlayerCrumb;
R: () => PlayerCrumb;
X: () => { i: number, x: number, y: number };
M: () => { i: number, m: string };
E: () => { i: number, e: string };
G: () => { i: number, g: Array<string> };
}
export interface ClientToServerEvents {
login: (ticket: string) => void;
joinLobby: () => unknown; // Unsure what this is for, I don't think the game had several servers
joinRoom: (roomId: string) => { name: string, roomId: string, playerCrumbs: Array<PlayerCrumb> };
message: (text: string) => void;
emote: (emote: string) => void;
code: (code: string, options?: string) => void;
addIgnore: (id: number) => void;
addFriend: (id: number) => void;
moveTo: (x: number, y: number) => void;
updateCritter: () => unknown; // Unsure of what this is, maybe settings?
updateGear: (gear: Array<string>) => void;
getShop: () => ShopData;
buyItem: (itemId: string) => void;
trigger: () => void;
}

167
utils.ts
View file

@ -1,167 +0,0 @@
import { Image } from 'https://deno.land/x/imagescript@1.3.0/mod.ts';
import chalk from "https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js"
import { join } from "https://deno.land/std@0.224.0/path/mod.ts";
import { rooms, spawnRoom } from "./constants/world.ts";
import { Room, LocalPlayer, PlayerCrumb } from "./types.ts";
/** Condenses the local player variable into data that is sufficient enough for other clients */
export function makeCrumb(player: LocalPlayer, roomId: string): PlayerCrumb {
return {
i: player.playerId,
n: player.nickname,
c: player.critterId,
x: player.x,
y: player.y,
r: player.rotation,
g: player.gear,
// message & emote
m: "",
e: "",
_roomId: roomId
}
}
// TODO: use the correct triggers for the active party
export async function getTrigger(player: LocalPlayer, roomId: string, partyId: string) {
const room = rooms[roomId][partyId];
if (!room) {
console.log(chalk.red(`[!] Cannot find room: "${roomId}@${partyId}"!`));
return;
}
try {
//@ts-ignore: Deno lint
const treasureBuffer = await Deno.readFile(room.media.treasure?.replace('..','public'));
const treasure = await Image.decode(treasureBuffer);
if (!treasure) throw new Error('Missing map server for room "' + roomId + '"!');
const pixel = treasure.getPixelAt(player.x, player.y);
const r = (pixel >> 24) & 0xFF, g = (pixel >> 16) & 0xFF, b = (pixel >> 8) & 0xFF;
const hexCode = ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0');
const trigger = room.triggers.find((trigger) => trigger.hex == hexCode);
if (trigger) {
return trigger.server;
} else {
return null;
}
} catch(e) {
console.warn(chalk.red('[!] Caught error while checking for activated trigger.'), e);
}
}
export function getNewCodeItem(player: LocalPlayer, items: Array<string>) {
const itemsSet = new Set(player.inventory);
const available = items.filter(item => !itemsSet.has(item));
return available.length === 0 ? null : available[Math.floor(Math.random() * available.length)];
}
/**
* Indexes the /media/rooms directory for all versions of all rooms
* @returns All versions of every room
*/
export async function indexRoomData() {
const _roomData: Record<string, Record<string, Room>> = {};
const basePath = join(Deno.cwd(), 'public', 'media', 'rooms');
const _rooms = Deno.readDir(basePath);
for await (const room of _rooms) {
if (room.isDirectory) {
_roomData[room.name] = {};
const roomPath = join(basePath, room.name);
const versions = Deno.readDir(roomPath);
for await (const version of versions) {
if (version.isDirectory) {
const versionPath = join(roomPath, version.name, 'data.json');
try {
const data = await Deno.readTextFile(versionPath);
_roomData[room.name][version.name] = JSON.parse(data);
} catch(_) {
console.log(chalk.red('[!] "%s@%s" is missing a data.json file'), room.name, version.name);
};
}
}
}
}
return _roomData
}
export async function getAccount(nickname?: string) {
let accounts = [];
try {
const data = await Deno.readTextFile('accounts.json');
accounts = JSON.parse(data);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
console.log(chalk.gray('Persistent login JSON is missing, using blank JSON array..'));
accounts = [];
} else {
console.log(chalk.red('[!] Failure to fetch persistent login data with nickname: '), nickname);
throw error;
};
}
if (nickname) {
const existingAccount = accounts.find((player: { nickname: string }) => player.nickname == nickname);
if (existingAccount) {
return {
all: accounts,
individual: existingAccount,
}
} else {
return {
all: accounts,
individual: null
}
}
} else {
return accounts;
}
}
export async function updateAccount(nickname: string, property: string, value: unknown) {
if (["x", "y", "rotation", "_partyId"].includes(property)) return;
const accounts = await getAccount(nickname);
accounts.individual[property] = value;
await Deno.writeTextFile('accounts.json', JSON.stringify(accounts.all, null, 2));
}
export function trimAccount(player: LocalPlayer) {
for (const key of [
"critterId",
"x",
"y",
"rotation",
"_partyId",
"_mods"
]) {
delete player[key];
}
return player;
}
export function expandAccount(player: LocalPlayer) {
const defaultPos = rooms[spawnRoom].default;
player.x = defaultPos.startX;
player.y = defaultPos.startY;
player.rotation = defaultPos.startR;
return player;
}
export function getDirection(x: number, y: number, targetX: number, targetY: number) {
const a = Math.floor((180 * Math.atan2(targetX - x, y - targetY)) / Math.PI);
return a < 0 ? a + 360 : a;
}
export async function createAccount(player: LocalPlayer) {
const accounts = await getAccount();
accounts.push(trimAccount(player));
await Deno.writeTextFile('accounts.json', JSON.stringify(accounts, null, 2));
}