duhhhhhhhh

This commit is contained in:
2025-09-11 13:47:30 -04:00
parent eef3667618
commit 317da3aaf7
53 changed files with 1395 additions and 212 deletions

4
.gitignore vendored
View File

@@ -1,5 +1,5 @@
/.env /.env
/persist/ /persist/
/worklist.txt
/node_modules /node_modules
/build /build
/todo.txt

View File

@@ -9,6 +9,7 @@
"@oneday/http-status": "jsr:@oneday/http-status@^0.2.0", "@oneday/http-status": "jsr:@oneday/http-status@^0.2.0",
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.5.0", "@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.5.0",
"@std/assert": "jsr:@std/assert@1", "@std/assert": "jsr:@std/assert@1",
"chalk": "npm:chalk@^5.6.2",
"sharp": "npm:sharp@^0.34.3", "sharp": "npm:sharp@^0.34.3",
"zod": "npm:zod@^4.0.5" "zod": "npm:zod@^4.0.5"
}, },

7
deno.lock generated
View File

@@ -18,6 +18,7 @@
"jsr:@std/path@^1.1.1": "1.1.2", "jsr:@std/path@^1.1.1": "1.1.2",
"npm:@types/node@*": "24.2.0", "npm:@types/node@*": "24.2.0",
"npm:chalk@^5.3.0": "5.6.0", "npm:chalk@^5.3.0": "5.6.0",
"npm:chalk@^5.6.2": "5.6.2",
"npm:sharp@~0.34.3": "0.34.3", "npm:sharp@~0.34.3": "0.34.3",
"npm:zod@^4.0.5": "4.1.5" "npm:zod@^4.0.5": "4.1.5"
}, },
@@ -53,7 +54,7 @@
"@proxnet/undead-logging@1.5.1": { "@proxnet/undead-logging@1.5.1": {
"integrity": "f858b6357d52c4bc1bbab279200dae86ed573ea45d945a8dfaf2f2cb23c4b649", "integrity": "f858b6357d52c4bc1bbab279200dae86ed573ea45d945a8dfaf2f2cb23c4b649",
"dependencies": [ "dependencies": [
"npm:chalk" "npm:chalk@^5.3.0"
] ]
}, },
"@std/assert@1.0.14": { "@std/assert@1.0.14": {
@@ -240,6 +241,9 @@
"chalk@5.6.0": { "chalk@5.6.0": {
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==" "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="
}, },
"chalk@5.6.2": {
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
},
"color-convert@2.0.1": { "color-convert@2.0.1": {
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": [ "dependencies": [
@@ -330,6 +334,7 @@
"jsr:@oneday/http-status@0.2", "jsr:@oneday/http-status@0.2",
"jsr:@proxnet/undead-logging@^1.5.0", "jsr:@proxnet/undead-logging@^1.5.0",
"jsr:@std/assert@1", "jsr:@std/assert@1",
"npm:chalk@^5.6.2",
"npm:sharp@~0.34.3", "npm:sharp@~0.34.3",
"npm:zod@^4.0.5" "npm:zod@^4.0.5"
] ]

254
res/consumables.json Normal file
View File

@@ -0,0 +1,254 @@
{
"m_GameObject": {
"m_FileID": 0,
"m_PathID": 0
},
"m_Enabled": 1,
"m_Script": {
"m_FileID": 1,
"m_PathID": 1574
},
"m_Name": "ConsumableCollectionRuntimeConfig",
"consumablePrefabData": [
{
"fileName": "[CameraFilterConsumable_BW]",
"consumableName": "frOMH6WxDEG1fBqC4_83vg"
},
{
"fileName": "[CameraFilterConsumable_Dawn]",
"consumableName": "m0bVLwWGj0GuIBSb6wCk6Q"
},
{
"fileName": "[CameraFilterConsumable_Dracula]",
"consumableName": "oG7CdvW7p0-S8sVe9w5vRw"
},
{
"fileName": "[CameraFilterConsumable_GhostBeard]",
"consumableName": "1c0Djlp090uBDczEobYNQw"
},
{
"fileName": "[CameraFilterConsumable_GoblinKing]",
"consumableName": "6SyCoJCgo0Wd6qlPlnMOtg"
},
{
"fileName": "[CameraFilterConsumable_Jumbotron]",
"consumableName": "Il4VmrnjDkqjmjzddqoIEw"
},
{
"fileName": "[CameraFilterConsumable_Sepia]",
"consumableName": "A5M-yf9tgUihq1uab3v58g"
},
{
"fileName": "[CameraFilterConsumable_Witch]",
"consumableName": "mL3zCEuy2UWZxAV4V-OsMA"
},
{
"fileName": "[HeadPotionConsumable_DKMode]",
"consumableName": "Tpxqe_lycUelySRHM8B0Vw"
},
{
"fileName": "[HandPotionConsumable_Goblin1]",
"consumableName": "-hy0qD-iUk-v4NHxNzanmg"
},
{
"fileName": "[HandPotionConsumable_Pirate1]",
"consumableName": "YEfbJTnsR0yT_p7e7tb_kQ"
},
{
"fileName": "[HandPotionConsumable_SciFi1]",
"consumableName": "xHOwjwpXd0GDkvBz2VqieA"
},
{
"fileName": "[HandPotionConsumable_Goblin2]",
"consumableName": "VQSgL2pTLkWx4B3kwYG7UA"
},
{
"fileName": "[HandPotionConsumable_Dracula1]",
"consumableName": "lag2tZyB90W04lQ7ol4vMw"
},
{
"fileName": "[FoodConsumable_Donut_Assorted]",
"consumableName": "ZuvkidodzkuOfGLDnTOFyg"
},
{
"fileName": "[FoodConsumable_Bubbly]",
"consumableName": "iiGTvhOCHkOTNJhb16Zbyw"
},
{
"fileName": "[FoodConsumable_CheesePizza]",
"consumableName": "5hIAZ9wg5EyG1cILf4FS2A"
},
{
"fileName": "[FoodConsumable_Donut_Frosted]",
"consumableName": "mMCGPgK3tki5S_15q2Z81A"
},
{
"fileName": "[FoodConsumable_Donut_Glazed]",
"consumableName": "7OZ5AE3uuUyqa0P-2W1ptg"
},
{
"fileName": "[FoodConsumable_HawaiianPizza]",
"consumableName": "_jnjYGBcyEWY5Ub4OezXcA"
},
{
"fileName": "[FoodConsumable_PepperoniPizza]",
"consumableName": "mq23W-RSP0G8iGNLdrcpUw"
},
{
"fileName": "[FoodConsumable_Popcorn_Butter]",
"consumableName": "QRx0aSTT9keMFdAJMQHdTg"
},
{
"fileName": "[FoodConsumable_RedApples]",
"consumableName": "uMHrUPLYFk2rJOW_uop5Aw"
},
{
"fileName": "[FoodConsumable_RootBeer]",
"consumableName": "JfnVXFmilU6ysv-VbTAe3A"
},
{
"fileName": "[FoodConsumable_Pretzel_Salted]",
"consumableName": "InQ25wQMGkG_bvuD5rf2Ag"
},
{
"fileName": "[FoodConsumable_SupremePizza]",
"consumableName": "wUCIKdJSvEmiQHYMyx4X4w"
},
{
"fileName": "[KOConsumable_BearClaw]",
"consumableName": "ZtZnYBpKkECJlhHmkj4MiA"
},
{
"fileName": "[KOConsumable_Bees]",
"consumableName": "g3kxdlJv5kO8PuBXveM48w"
},
{
"fileName": "[KOConsumable_Cola]",
"consumableName": "wx--2TPTdEuGAqLCjs9Qag"
},
{
"fileName": "[KOConsumable_Fangs]",
"consumableName": "XOZcxx-Klkyhe-MDbTqiwA"
},
{
"fileName": "[KOConsumable_FryingPan]",
"consumableName": "uGVFydNSokCXFAmXu3aceQ"
},
{
"fileName": "[KOConsumable_Grenade]",
"consumableName": "EAhk3ZZdXEmH5wRAXXT24Q"
},
{
"fileName": "[KOConsumable_Ranger]",
"consumableName": "Av-wvjXvvkmNVSz7ZZnTiA"
},
{
"fileName": "[KOConsumable_Shield]",
"consumableName": "5AJin8T2iEG7BzOPOgx2HA"
},
{
"fileName": "[KOConsumable_SkullWings]",
"consumableName": "U38Qe6rhEk6mFvArHfYjng"
},
{
"fileName": "[KOConsumable_StarPower]",
"consumableName": "yvqSbK2czkS2sUCRdrGaEw"
},
{
"fileName": "[KOConsumable_TireTrack]",
"consumableName": "J1WqFNUWo0OBi4LGKPDHWw"
},
{
"fileName": "[HairDyePotionConsumable_Orange]",
"consumableName": "xr45B5QC4EyKXThtwZto_A"
},
{
"fileName": "[HairDyePotionConsumable_Blue]",
"consumableName": "JwJeh15cjkOc9WWaGRibDQ"
},
{
"fileName": "[HairDyePotionConsumable_DarkRed]",
"consumableName": "3R1bzI35fkChoFFRFbponQ"
},
{
"fileName": "[HairDyePotionConsumable_Cyan]",
"consumableName": "x9ntSHto50GpNWPwYCLUEg"
},
{
"fileName": "[HairDyePotionConsumable_PastelTeal]",
"consumableName": "4L3JoaqkAUa1kz98nciPXw"
},
{
"fileName": "[HairDyePotionConsumable_Green]",
"consumableName": "YaVzcoefhk6zjbNDELLx8A"
},
{
"fileName": "[HairDyePotionConsumable_DarkPurple]",
"consumableName": "iEtWDLmv7ES87J4NE_TGJQ"
},
{
"fileName": "[HairDyePotionConsumable_PastelGreen]",
"consumableName": "jA3zzS31zEitBxtRCk7oew"
},
{
"fileName": "[HairDyePotionConsumable_PastelCyan]",
"consumableName": "P7HixsdkskmzgFMdFJ-WdA"
},
{
"fileName": "[HairDyePotionConsumable_Teal]",
"consumableName": "5Ugg8k19n0WH8GPFCDWTCQ"
},
{
"fileName": "[HairDyePotionConsumable_Red]",
"consumableName": "P1kCuPlRc0-NAg6JUlEH_g"
},
{
"fileName": "[HairDyePotionConsumable_PastelPurple]",
"consumableName": "Hh_O_RejGE-_PubyM8xHaw"
},
{
"fileName": "[HairDyePotionConsumable_PastelPink]",
"consumableName": "s2pxukIEt0uDeGVJmZayOw"
},
{
"fileName": "[HairDyePotionConsumable_Pink]",
"consumableName": "vXL0rrAk70SFxJlaJk0uqQ"
},
{
"fileName": "[HairDyePotionConsumable_PastelBlue]",
"consumableName": "UAOH6ccEbEmehJg8XW_D8w"
},
{
"fileName": "[HairDyePotionConsumable_Yellow]",
"consumableName": "o0hG5O_eaUO0R8vPyW-Hvg"
},
{
"fileName": "[HairDyePotionConsumable_Purple]",
"consumableName": "xRtKWR-D40GUW43R0EFpNg"
},
{
"fileName": "[HairDyePotionConsumable_PastelOrange]",
"consumableName": "AQTYAh1it0GIGdgGO5sUHQ"
},
{
"fileName": "[HairDyePotionConsumable_DarkBlue]",
"consumableName": "8vlVslaWaUyFS-iDHhxW9g"
},
{
"fileName": "[HairDyePotionConsumable_PastelRed]",
"consumableName": "STFKahjHJ0SQJPfoDe4S1g"
},
{
"fileName": "[HairDyePotionConsumable_DarkPink]",
"consumableName": "eJh_BQ5y4UWwiMJzBKcwMQ"
},
{
"fileName": "[HairDyePotionConsumable_PastelYellow]",
"consumableName": "inIuPzhhOEmz1RI8mZtSLg"
},
{
"fileName": "[CouponConsumable_20%Tokens]",
"consumableName": "50oCPkzd3EerBd7nYNdkCw"
}
]
}

View File

@@ -0,0 +1,38 @@
{
"f5fbd9c9-e853-4036-9d48-5f68e861af04": false,
"76d98498-60a1-430c-ab76-b54a29b7a163": false,
"cbad71af-0831-44d8-b8ef-69edafa841f6": true,
"4078dfed-24bb-4db7-863f-578ba48d726b": true,
"f6f7256c-e438-4299-b99e-d20bef8cf7e0": true,
"d9378c9f-80bc-46fb-ad1e-1bed8a674f55": true,
"3d474b26-26f7-45e9-9a36-9b02847d5e6f": true,
"d89f74fa-d51e-477a-a425-025a891dd499": false,
"e122fe98-e7db-49e8-a1b1-105424b6e1f0": true,
"a785267d-c579-42ea-be43-fec1992d1ca7": true,
"ff4c6427-7079-4f59-b22a-69b089420827": true,
"380d18b5-de9c-49f3-80f7-f4a95c1de161": true,
"58763055-2dfb-4814-80b8-16fac5c85709": true,
"65ddbb48-5a01-4e3e-972d-e5c7419e2bc3": true,
"91e16e35-f48f-4700-ab8a-a1b79e50e51b": false,
"acc06e66-c2d0-4361-b0cd-46246a4c455c": false,
"949fa41f-4347-45c0-b7ac-489129174045": false,
"7e01cfe0-820a-406f-b1b3-0a5bf575235c": false,
"6d5eea4b-f069-4ed0-9916-0e2f07df0d03": true,
"239e676c-f12f-489f-bf3a-d4c383d692c3": true,
"9d6456ce-6264-48b4-808d-2d96b3d91038": true,
"253fa009-6e65-4c90-91a1-7137a56a267f": false,
"b010171f-4875-4e89-baba-61e878cd41e1": false,
"a067557f-ca32-43e6-b6e5-daaec60b4f5a": true,
"9932f88f-3929-43a0-a012-a40b5128e346": true,
"a75f7547-79eb-47c6-8986-6767abcb4f92": true,
"0a864c86-5a71-4e18-8041-8124e4dc9d98": true,
"42699ed2-0c1b-4f3d-93a2-ce01dfce7a79": false,
"49cb8993-a956-43e2-86f4-1318f279b22a": false,
"ae929543-9a07-41d5-8ee9-dbbee8c36800": true,
"a95c349c-0f96-4c2d-a4c8-4969ffa8ea44": false,
"b7281665-a715-4051-826b-8e08e69c6172": true,
"3a636bd2-f896-424c-9225-c184522c0d87": false,
"882e9b96-7115-4b03-86f6-c0c9d8e22e00": true,
"cf61556d-68fd-4288-9ae5-7a512621e569": false,
"bf268f5f-b55b-41af-8628-32fa4b5d70b6": false
}

View File

@@ -182,7 +182,7 @@ Server.Commands.addRootCommand(new Command({
else return null; else return null;
}, },
zod: z.tuple([ zod: z.tuple([
z.string().transform(Number) z.coerce.number()
]), ]),
help: 'Get ping (in ms) to the server' help: 'Get ping (in ms) to the server'
})); }));

View File

@@ -1,10 +1,11 @@
import { createHonoRoute } from "../../../util/import.ts"; import { createHonoRoute } from "../../../util/import.ts";
import { authenticate, galvanicError, GalvanicErrors, RateLimiter, recNetError } from "../../../util/api.ts"; import { authenticate, galvanicError, GalvanicErrors, RateLimiter, recNetError, statusResponse } from "../../../util/api.ts";
import Server from "../../../server/server.ts"; import Server from "../../../server/server.ts";
import z from "zod"; import z from "zod";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts"; import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
import { PlatformType } from "../../../server/platforms/types.ts"; import { PlatformType } from "../../../server/platforms/types.ts";
import Steam from "../../../util/steam/steam.ts"; import Steam from "../../../util/steam/steam.ts";
import { HTTPStatus } from "@oneday/http-status";
export const route = createHonoRoute('/account'); export const route = createHonoRoute('/account');
@@ -28,7 +29,7 @@ route.app.get('/bulk', typedZValidator('query', bulkAccountQuerySchema), async c
const postCreateRateLimiter = new RateLimiter(60, 3); const postCreateRateLimiter = new RateLimiter(60, 3);
const createAccountBodySchema = z.object({ const createAccountBodySchema = z.object({
platform: z.string().transform(transformStringToEnum<PlatformType>(PlatformType)), platform: z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
platformId: z.string().min(14).max(20), platformId: z.string().min(14).max(20),
deviceId: z.string().min(32).max(64) deviceId: z.string().min(32).max(64)
}); });
@@ -71,9 +72,16 @@ route.app.post('/create', postCreateRateLimiter.middle(), typedZValidator('form'
}); });
route.app.use(authenticate); route.app.get('/me', authenticate, c => {
route.app.get('/me', c => {
const profile = c.get('profile'); const profile = c.get('profile');
return c.json(profile.selfExport()); return c.json(profile.selfExport());
});
const getAccountByIdParamSchema = z.object({
id: z.coerce.number().max(Math.pow(2, 31))
});
route.app.get('/:id', typedZValidator('param', getAccountByIdParamSchema), async c => {
const prof = await Server.Profiles.get(c.req.valid('param').id);
if (prof) return c.json(prof.export());
else return statusResponse(c, HTTPStatus.NotFound);
}); });

View File

@@ -0,0 +1,8 @@
import Server from "../../../server/server.ts";
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/announcement");
route.app.get('/v1/get', c => {
return c.json(Server.getAnnouncements());
});

View File

@@ -24,4 +24,12 @@ route.app.post('/v2/set', typedZValidator('json', profileAvatarSchema), async c
const outfit = c.req.valid('json'); const outfit = c.req.valid('json');
await c.get('profile').Avatar.setAvatar(outfit); await c.get('profile').Avatar.setAvatar(outfit);
return c.status(200); return c.status(200);
});
route.app.get('/v3/saved', c => {
return c.json([]); // stub
});
route.app.get('/v2/gifts', c => {
return c.json([]); // stub
}); });

View File

@@ -0,0 +1,19 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/challenge');
route.app.get('/v2/getCurrent', c => {
return c.json({
ChallengeMapId: 0,
CompletedRequired: false,
StartAt: new Date(new Date().getTime() - 604_800_000).toISOString(),
EndAt: new Date(new Date().getTime() + 999_999_999_999).toISOString(),
ServerTime: new Date().toISOString(),
Challenges: [],
Gift: {
GiftDropId: 0,
Xp: 0,
Level: 0
}
}) // stub
});

View File

@@ -0,0 +1,171 @@
import { ObjectiveType } from "../../../server/objectives/base.ts";
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/checklist');
route.app.get('/v1/current', c => {
return c.json([
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
},
{
Order: 1,
Objective: ObjectiveType.CompleteAnyDaily,
Count: 1,
CreditAmount: -1
}
]); // stub
});

View File

@@ -0,0 +1,23 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/communityboard");
route.app.get('/v1/current', c => {
return c.json({
FeaturedPlayer: {
Id: 1,
TitleOverride: "",
UrlOverride: ""
},
FeaturedRoomGroup: {
Name: "",
FeaturedRooms: []
},
CurrentAnnouncement: {
Message: "Galvanic Corrosion",
MoreInfoUrl: ""
},
InstagramImages: [],
Videos: []
}); // stub
});

View File

@@ -0,0 +1,8 @@
import Server from "../../../server/server.ts";
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/consumables");
route.app.get('/v1/getUnlocked', c => {
return c.json(Server.Consumables.getAllDev());
});

View File

@@ -0,0 +1,7 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/equipment");
route.app.get('/v2/getUnlocked', c => {
return c.json([]); // stub
});

View File

@@ -0,0 +1,7 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/images');
route.app.get('/v2/named', c => {
return c.json([]); // stub
});

View File

@@ -0,0 +1,10 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/objectives');
route.app.get('/v1/myprogress', c => {
return c.json({
Objectives: [],
ObjectiveGroups: []
}); // stub
});

View File

@@ -0,0 +1,10 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/playerevents');
route.app.get('/v1/all', c => {
return c.json({
Created: [],
Responses: []
}) // stub
});

View File

@@ -14,14 +14,14 @@ const getProgParamSchema = z.object({
id: z.coerce.number() id: z.coerce.number()
}); });
route.app.get('/v1/progression/:id', authenticate, typedZValidator('param', getProgParamSchema), async c => { route.app.get('/v1/progression/:id', authenticate, typedZValidator('param', getProgParamSchema), async c => {
return c.json(await c.get('profile').Reputation.export()); return c.json(await c.get('profile').Progression.get());
}); });
const getProgBulkBodySchema = z.object({ const getProgBulkBodySchema = z.object({
Ids: z.union([z.array(z.coerce.number()), z.coerce.number()]) Ids: z.union([z.array(z.coerce.number()), z.coerce.number()])
}); });
route.app.post('/v1/progression/bulk', authenticate, typedZValidator('form', getProgBulkBodySchema), async c => { route.app.post('/v1/progression/bulk', authenticate, typedZValidator('json', getProgBulkBodySchema), async c => {
const ids = c.req.valid('form').Ids; const ids = c.req.valid('json').Ids;
if (typeof ids == 'object') { if (typeof ids == 'object') {
const profs = await Server.Profiles.getMany(...ids); const profs = await Server.Profiles.getMany(...ids);

View File

@@ -0,0 +1,7 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/playersubscriptions");
route.app.get('/v1/my', c => {
return c.json([]); // stub
});

View File

@@ -8,7 +8,6 @@ route.app.get('/v2/get', c => {
return c.json([]); return c.json([]);
}); });
// deno-lint-ignore require-await route.app.post('/v1/bulkignoreplatformusers', c => {
route.app.post('/v1/bulkignoreplatformusers', async c => {
return statusResponse(c, HTTPStatus.OK); return statusResponse(c, HTTPStatus.OK);
}); });

View File

@@ -2,6 +2,7 @@ import z from "zod";
import { createHonoRoute } from "../../../util/import.ts"; import { createHonoRoute } from "../../../util/import.ts";
import { typedZValidator } from "../../../util/validators.ts"; import { typedZValidator } from "../../../util/validators.ts";
import Server from "../../../server/server.ts"; import Server from "../../../server/server.ts";
import { authenticate } from "../../../util/api.ts";
export const route = createHonoRoute("/rooms"); export const route = createHonoRoute("/rooms");
@@ -23,4 +24,13 @@ route.app.get('/v2/name/:name', typedZValidator('param', getRoomByNameParamSchem
const room = await Server.Rooms.get(id); const room = await Server.Rooms.get(id);
if (room == null) return await nxt(); if (room == null) return await nxt();
else return c.json((await room.export()).Room); else return c.json((await room.export()).Room);
});
route.app.get('/v2/myrooms', authenticate, async c => {
const myrooms = c.get('profile').Rooms.getRooms().values().toArray();
const factories = await Server.Rooms.getMany(...myrooms);
const exs = await Promise.all(factories.map(factory => factory.export()));
const rooms = exs.map(ex => ex.Room);
return c.json(rooms);
}); });

View File

@@ -1,6 +1,6 @@
import z from "zod"; import z from "zod";
import { createHonoRoute } from "../../../util/import.ts"; import { createHonoRoute } from "../../../util/import.ts";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts"; import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
import { PlatformType } from "../../../server/platforms/types.ts"; import { PlatformType } from "../../../server/platforms/types.ts";
import Server from "../../../server/server.ts"; import Server from "../../../server/server.ts";
import { authenticate } from "../../../util/api.ts"; import { authenticate } from "../../../util/api.ts";
@@ -11,7 +11,7 @@ const log = new Logging("CachedLoginDebug");
export const route = createHonoRoute("/cachedlogin"); export const route = createHonoRoute("/cachedlogin");
const cachedLoginFetchParamSchema = z.object({ const cachedLoginFetchParamSchema = z.object({
platformType: z.string().transform(transformStringToEnum<PlatformType>(PlatformType)), platformType: z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
platformId: z.string().min(4) platformId: z.string().min(4)
}); });

View File

@@ -1,6 +1,6 @@
import { createHonoRoute } from "../../../util/import.ts"; import { createHonoRoute } from "../../../util/import.ts";
import z from "zod"; import z from "zod";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts"; import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
import { DeviceClass, PlatformType, TokenFormat, TokenType } from "../../../server/platforms/types.ts"; import { DeviceClass, PlatformType, TokenFormat, TokenType } from "../../../server/platforms/types.ts";
import { steamAuthTicketSchema } from "../../../server/platforms/base.ts"; import { steamAuthTicketSchema } from "../../../server/platforms/base.ts";
import { gameVerString } from "../../api/routes/versioncheck.ts"; import { gameVerString } from "../../api/routes/versioncheck.ts";
@@ -16,13 +16,13 @@ export const route = createHonoRoute("/connect");
const authBodyBaseSchema = z.object({ const authBodyBaseSchema = z.object({
client_id: z.literal("recroom"), client_id: z.literal("recroom"),
platform: z.string().transform(Number).transform((arg, ctx) => { // we only support steam right now platform: z.coerce.number().transform((arg, ctx) => { // we only support steam right now
if (arg !== PlatformType.Steam) ctx.addIssue("platform was not Steam"); if (arg !== PlatformType.Steam) ctx.addIssue("platform was not Steam");
else return PlatformType.Steam; else return PlatformType.Steam;
}), }),
platform_id: z.string().min(4), platform_id: z.string().min(4),
device_id: z.string().min(4), device_id: z.string().min(4),
device_class: z.string().transform(transformStringToEnum<DeviceClass>(DeviceClass)), device_class: z.string().transform(transformCheckEnum<DeviceClass>(DeviceClass)),
time: z.coerce.date(), time: z.coerce.date(),
ver: z.literal(gameVerString), ver: z.literal(gameVerString),
asid: z.coerce.number(), asid: z.coerce.number(),
@@ -39,7 +39,7 @@ const authBodyBaseSchema = z.object({
const cachedLoginGrantSchema = authBodyBaseSchema.extend({ const cachedLoginGrantSchema = authBodyBaseSchema.extend({
grant_type: z.literal('cached_login'), grant_type: z.literal('cached_login'),
account_id: z.string().transform(Number), account_id: z.coerce.number(),
}); });
const refreshTokenGrantSchema = authBodyBaseSchema.extend({ const refreshTokenGrantSchema = authBodyBaseSchema.extend({
grant_type: z.literal('refresh_token'), grant_type: z.literal('refresh_token'),
@@ -97,24 +97,25 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
const profile = await Server.Profiles.get(token.sub); const profile = await Server.Profiles.get(token.sub);
if (!profile) return error(TokenRequestError.AccessDenied); if (!profile) return error(TokenRequestError.AccessDenied);
const accessToken = await Server.Platforms.getToken(profile.getId(), TokenType.Access); const accessToken = await Server.Platforms.getToken(profile, TokenType.Access);
const refreshToken = await Server.Platforms.getToken(profile, TokenType.Refresh);
return c.json({ return c.json({
access_token: accessToken, access_token: accessToken,
refresh_token: form.refresh_token, refresh_token: refreshToken,
}); });
} catch (err) { } catch (err) {
log.w(`Authentication error (token req): ${(err as Error).stack}`); log.w(`Authentication error (token req): ${(err as Error).stack}`);
return error(TokenRequestError.InvalidClient); return error(TokenRequestError.InvalidClient);
} }
} }
if (logins.find(login => login.accountId === form.account_id)) { if (logins.find(login => login.accountId === form.account_id)) {
const profile = await Server.Profiles.get(form.account_id); const profile = await Server.Profiles.get(form.account_id);
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile"); if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id); await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id);
const accessToken = await Server.Platforms.getToken(profile.getId(), TokenType.Access); const accessToken = await Server.Platforms.getToken(profile, TokenType.Access);
const refreshToken = await Server.Platforms.getToken(profile.getId(), TokenType.Refresh); const refreshToken = await Server.Platforms.getToken(profile, TokenType.Refresh);
return c.json({ return c.json({
access_token: accessToken, access_token: accessToken,

View File

@@ -1,50 +1,7 @@
import { createHonoRoute, routeImporter } from "../../util/import.ts"; import { createHonoRoute, routeImporter } from "../../util/import.ts";
import { Context, Next } from "@hono/hono";
import z from "zod";
import { HonoEnv } from "../../util/types.ts";
import { statusResponse } from "../../util/api.ts";
import { HTTPStatus } from "@oneday/http-status";
import Logging from "@proxnet/undead-logging";
const log = new Logging("MatchRoute");
export const route = createHonoRoute('/match'); export const route = createHonoRoute('/match');
const loginLockBodySchema = z.object({
LoginLock: z.uuidv4()
});
export const loginLockMiddleware = async (c: Context<HonoEnv>, nxt: Next) => {
function unauthorized() {
return statusResponse(c, HTTPStatus.Unauthorized);
}
if (c.req.header("Content-Type") !== "application/x-www-form-urlencoded") return unauthorized();
try {
const form = await c.req.formData();
const body = await loginLockBodySchema.safeParseAsync(Object.fromEntries(form.entries()));
if (body.success) {
if (typeof c.get('profile') == 'undefined') {
log.w(`Profile was not set, cannot validate LoginLock. Was the request authorized?`);
return statusResponse(c, HTTPStatus.InternalServerError);
}
const profile = c.get('profile');
const loginLock = await profile.Matchmaking.getLoginLock();
if (!loginLock) await profile.Matchmaking.setLoginLock(body.data.LoginLock);
else if (body.data.LoginLock !== loginLock) {
log.w(`LoginLock did not match. The token for this profile could be compromised or the client is an unknown state.`);
return unauthorized();
}
return await nxt();
} else return unauthorized();
} catch {
return unauthorized();
}
}
await routeImporter(route.app, 'src/routes/match/', [ await routeImporter(route.app, 'src/routes/match/', [
'routes' 'routes'
]); ]);

View File

@@ -1,13 +1,76 @@
import { HTTPStatus } from "@oneday/http-status"; import { HTTPStatus } from "@oneday/http-status";
import { statusResponse } from "../../../util/api.ts"; import { authenticate, loginLockMiddleware, statusResponse } from "../../../util/api.ts";
import { createHonoRoute } from "../../../util/import.ts"; import { createHonoRoute } from "../../../util/import.ts";
import Server from "../../../server/server.ts";
import z from "zod";
import { typedZValidator } from "../../../util/validators.ts";
import { type MatchmakingResponse } from "../../../server/matchmaking/base.ts";
import { roomNameSchema } from "../../../server/rooms/base.ts";
export const route = createHonoRoute("/goto"); export const route = createHonoRoute("/goto");
route.app.post('/room/:roomName', c => { const gotoRoomBodySchema = z.object({
return statusResponse(c, HTTPStatus.NotImplemented); CreatePrivateInstance: z.boolean().optional(),
ExpectedPlayerIds: z.array(z.int()).optional(),
BypassMovementModeRestriction: z.boolean().optional()
}); });
route.app.post('/room/:roomName/:subRoomName', c => { const gotoRoomParamSchema = z.object({
return statusResponse(c, HTTPStatus.NotImplemented); roomName: roomNameSchema
}); });
route.app.post('/room/:roomName',
authenticate,
loginLockMiddleware,
typedZValidator('json', gotoRoomBodySchema),
typedZValidator('param', gotoRoomParamSchema),
async c => {
const body = c.req.valid("json");
const res = await Server.Matchmaking.matchmake({
roomName: c.req.param('roomName'),
private: body.CreatePrivateInstance,
profile: c.get('profile')
});
if (!res) return statusResponse(c, HTTPStatus.InternalServerError, "Matchmaking failed");
const m: MatchmakingResponse = {
roomInstance: res.roomInstance ? res.roomInstance.export() : undefined,
errorCode: res.errorCode
}
return c.json(m);
}
);
const gotoSubroomParamSchema = gotoRoomParamSchema.extend({
subRoomName: roomNameSchema
});
route.app.post('/room/:roomName/:subRoomName',
authenticate,
loginLockMiddleware,
typedZValidator('json', gotoRoomBodySchema),
typedZValidator('param', gotoSubroomParamSchema),
async c => {
const body = c.req.valid("json");
const res = await Server.Matchmaking.matchmake({
roomName: c.req.param('roomName'),
subRoomName: c.req.param('subRoomName'),
private: body.CreatePrivateInstance,
profile: c.get('profile')
});
if (!res) return statusResponse(c, HTTPStatus.InternalServerError, "Matchmaking failed");
const m: MatchmakingResponse = {
roomInstance: res.roomInstance ? res.roomInstance.export() : undefined,
errorCode: res.errorCode
}
return c.json(m);
}
);

View File

@@ -1,20 +1,70 @@
import { authenticate, statusResponse } from "../../../util/api.ts"; import z from "zod";
import Server from "../../../server/server.ts";
import { authenticate, loginLockMiddleware, statusResponse } from "../../../util/api.ts";
import { createHonoRoute } from "../../../util/import.ts"; import { createHonoRoute } from "../../../util/import.ts";
import { loginLockMiddleware } from "../root.ts";
import { HTTPStatus } from "@oneday/http-status"; import { HTTPStatus } from "@oneday/http-status";
import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
import { PlayerStatusVisibility, VRMovementMode } from "../../../server/presence/base.ts";
export const route = createHonoRoute("/player"); export const route = createHonoRoute("/player");
route.app.use(authenticate); const playerIdsQuerySchema = z.object({
id: z.union([z.coerce.number(), z.array(z.coerce.number())])
route.app.post('/login', authenticate, loginLockMiddleware, async c => { });
route.app.get('/', typedZValidator('query', playerIdsQuerySchema), async c => {
const id = c.req.valid('query').id;
if (typeof id == 'object') {
const profs = await Server.Profiles.getMany(...id);
return c.json(profs.map(prof => Server.Presence.getPresence(prof).export()));
} else {
const prof = await Server.Profiles.get(id);
if (!prof) return c.json([]);
return c.json([Server.Presence.getPresence(prof).export()]);
}
}); });
route.app.post('/player/vrmovementmode', authenticate, loginLockMiddleware, async c => { route.app.post('/login', authenticate, loginLockMiddleware, c => {
return statusResponse(c, HTTPStatus.OK); // stub const pres = Server.Presence.getPresence(c.get('profile'));
pres.updateLastSeen();
return statusResponse(c, HTTPStatus.OK);
});
route.app.post('/logout', authenticate, loginLockMiddleware, c => {
const pres = Server.Presence.getPresence(c.get('profile'));
pres.updateLastSeen();
pres.setStatusVisibility(PlayerStatusVisibility.Offline);
c.get('profile').updateInstance(null);
Server.Instances.clearEmptyInstances();
return statusResponse(c, HTTPStatus.OK);
}); });
route.app.post('/player/statusvisibility', authenticate, loginLockMiddleware, async c => { const vrMovementModeBodySchema = z.object({
return statusResponse(c, HTTPStatus.OK); // stub vrMovementMode: z.coerce.number().transform(transformCheckEnum<VRMovementMode>(VRMovementMode))
});
route.app.put('/vrmovementmode', authenticate, typedZValidator('form', vrMovementModeBodySchema), c => {
const pres = Server.Presence.getPresence(c.get('profile'));
pres.updateLastSeen();
return statusResponse(c, HTTPStatus.OK);
});
const statusVisibilityBodySchema = z.object({
statusVisibility: z.coerce.number().transform(transformCheckEnum<PlayerStatusVisibility>(PlayerStatusVisibility))
});
route.app.put('/statusvisibility', authenticate, typedZValidator('form', statusVisibilityBodySchema), c => {
const pres = Server.Presence.getPresence(c.get('profile'));
pres.updateLastSeen();
return statusResponse(c, HTTPStatus.OK);
});
route.app.post('/heartbeat', authenticate, loginLockMiddleware, c => {
const pres = Server.Presence.getPresence(c.get('profile'));
return c.json(pres.export());
}); });

View File

@@ -1,2 +1,20 @@
import type Profile from "../profiles/profile.ts";
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
export type CommandExec = (...args: any[]) => unknown | Promise<unknown>; export type CommandExec = (...args: any[]) => unknown | Promise<unknown>;
export enum CommandSenderType {
Console,
Profile
}
export interface CommandSenderBase {
type: CommandSenderType
}
export interface ConsoleSender extends CommandSenderBase {
type: CommandSenderType.Console
}
export interface ProfileSender extends CommandSenderBase {
type: CommandSenderType.Profile,
prof: Profile
}
export type CommandSender = ConsoleSender | ProfileSender;

View File

@@ -34,6 +34,10 @@ export default class Command {
return this.exec(...args); return this.exec(...args);
else if (!root) return new Error('No execution target for this root'); else if (!root) return new Error('No execution target for this root');
if (root === 'help') return JSON.stringify(this.subCmds.values()
.map(cmd => cmd.getKey()).toArray()
.reduce((prev, accumulator) => prev.concat(accumulator), []));
const cmd = this.subCmds.find(cmd => cmd.getKey().includes(root)); const cmd = this.subCmds.find(cmd => cmd.getKey().includes(root));
if (cmd) { if (cmd) {
const newArgs = args.slice(1); const newArgs = args.slice(1);

View File

@@ -1,4 +1,5 @@
import { ServerContentBase } from "../ContentBase.ts"; import { ServerContentBase } from "../ContentBase.ts";
import { CommandSender, CommandSenderType } from "./cmdtypes.ts";
import type Command from "./command.ts"; import type Command from "./command.ts";
export class CommandsBase extends ServerContentBase { export class CommandsBase extends ServerContentBase {
@@ -18,12 +19,20 @@ export class CommandsBase extends ServerContentBase {
this.#cmds.delete(cmd); this.#cmds.delete(cmd);
} }
async dispatch(...args: string[]): Promise<unknown> { async dispatch(sender: CommandSender, ...args: string[]): Promise<unknown> {
if (sender.type == CommandSenderType.Profile)
if (await sender.prof.getRole() !== "developer") return new Error("Unauthorized");
const root = args[0]; const root = args[0];
if (typeof root !== 'string') return new Error("Root command must be of primitive type 'string'"); if (typeof root !== 'string') return new Error("Root command must be of primitive type 'string'");
else { else {
if (root === "help") return JSON.stringify(this.#cmds.values()
.map(cmd => cmd.getKey()).toArray()
.reduce((prev, accumulator) => prev.concat(accumulator), []));
const cmd = this.#cmds.values().toArray().find(cmd => cmd.getKey().includes(root)); const cmd = this.#cmds.values().toArray().find(cmd => cmd.getKey().includes(root));
if (cmd) { if (cmd) {
const val = await cmd.dispatch(...args.slice(1)); const val = await cmd.dispatch(...args.slice(1));
if (val == null) return "null"; if (val == null) return "null";
else if (typeof val == 'string') return `"${val}"`; else if (typeof val == 'string') return `"${val}"`;

View File

@@ -0,0 +1,29 @@
import path from "node:path";
import { ServerContentBase } from "../ContentBase.ts";
import { Consumable, ConsumableCollectionRuntimeConfig } from "./types.ts";
import { RootPath } from "../../util/path.ts";
import { PlatformMask } from "../platforms/types.ts";
export class ServerConsumablesBase extends ServerContentBase {
#consumableConfig: ConsumableCollectionRuntimeConfig | null = null;
protected override start() {
this.#consumableConfig = JSON.parse(Deno.readTextFileSync(path.join(RootPath, '/res/consumables.json')));
}
getAllDev(): Consumable[] {
if (this.#consumableConfig == null) return [];
else return this.#consumableConfig.consumablePrefabData.map(cons => ({
Id: this.#consumableConfig!.consumablePrefabData.indexOf(cons),
ConsumableItemDesc: cons.consumableName,
PlatformMask: this.server.generateMask(PlatformMask.All),
CreatedAt: new Date().toISOString(),
Count: 99999999,
InitialCount: 99999999,
UnlockedLevel: 1,
IsActive: false
}));
}
}

View File

@@ -0,0 +1,19 @@
export interface ConsumablePrefab {
fileName: string,
consumableName: string
}
export interface ConsumableCollectionRuntimeConfig {
consumablePrefabData: ConsumablePrefab[]
}
export interface Consumable {
Id: number,
ConsumableItemDesc: string,
PlatformMask: number,
CreatedAt: string,
Count: number,
InitialCount: number,
UnlockedLevel: number,
IsActive: boolean,
ActiveDurationMinutes?: number
}

View File

@@ -1,45 +1,104 @@
import { CloudRegionCode } from "../../util/photon.ts"; import { CloudRegionCode } from "../../util/photon.ts";
import type Profile from "../profiles/profile.ts"; import type Profile from "../profiles/profile.ts";
import { RoomFactory } from "../rooms/internal/RoomFactory.ts";
import { SubroomFactory } from "../rooms/internal/SubroomFactory.ts";
import { type ServerBase } from "../server.ts";
import { RoomInstance, RoomLocation } from "./types.ts"; import { RoomInstance, RoomLocation } from "./types.ts";
export interface InstanceCreationOptions { export interface InstanceCreationOptions {
roomId: number, room: RoomFactory,
subRoomId: number, subroom: SubroomFactory,
name: string, name: string,
maxCapacity: number, private?: boolean,
private?: boolean eventId?: number
} }
export class Instance { export class Instance {
#createdAt = new Date(); #createdAt = new Date();
#players: Set<Profile> = new Set(); #players: Set<Profile> = new Set();
#server: ServerBase;
#init: boolean = true;
#instanceId: number; #instanceId: number;
#roomId: number; #roomId: number = -1;
#subRoomId: number; #subRoomId: number = -1;
#location: RoomLocation; #location: RoomLocation = RoomLocation.MakerRoom;
#name: string; #name: string = "Uninitialized Instance";
#maxCapacity: number; #maxCapacity: number = 8;
#isFull: boolean = false; #isFull: boolean = false;
#isPrivate: boolean; #isPrivate: boolean = false;
#isInProgress: boolean = false; #isInProgress: boolean = false;
#photonRegionId: string = CloudRegionCode.us; #photonRegionId: string = CloudRegionCode.us;
#photonRoomId: string; #photonRoomId: string = "uninit";
#dataBlob?: string; #dataBlob?: string;
#eventId?: number #eventId?: number;
constructor(id: number, location: RoomLocation, options: InstanceCreationOptions) { get instanceId() { return this.#instanceId }
get roomId() { return this.#roomId }
set roomId(data) { this.#roomId = data }
get subRoomId() { return this.#subRoomId }
set subRoomId(data) { this.#subRoomId = data }
get location() { return this.#location }
set location(data) { this.#location = data }
get name() { return this.#name }
set name(data) { this.#name = data }
get maxCapacity() { return this.#maxCapacity }
set maxCapacity(data) { this.#maxCapacity = data }
get isFull() { return this.#isFull }
set isFull(data) { this.#isFull = data }
get isPrivate() { return this.#isPrivate }
set isPrivate(data) { this.#isPrivate = data }
get isInProgress() { return this.#isInProgress }
set isInProgress(data) { this.#isInProgress = data }
get photonRegionId() { return this.#photonRegionId }
set photonRegionId(data) { this.#photonRegionId = data }
get photonRoomId() { return this.#photonRoomId }
set photonRoomId(data) { this.#photonRoomId = data }
get dataBlob() { return this.#dataBlob }
set dataBlob(data) { this.#dataBlob = data }
get eventId() { return this.#eventId }
set eventId(data) { this.#eventId = data }
supportsJoinInProgress: boolean;
constructor(server: ServerBase, id: number, options: InstanceCreationOptions) {
this.#instanceId = id; this.#instanceId = id;
this.#location = location; this.location = options.subroom.RoomSceneLocationId;
this.#server = server;
this.#roomId = options.roomId; this.roomId = options.room.getRoomId();
this.#subRoomId = options.subRoomId; this.subRoomId = options.subroom.RoomSceneId;
this.#isPrivate = typeof options.private == 'boolean' ? options.private : false; this.isPrivate = typeof options.private == 'boolean' ? options.private : false;
this.#name = options.name; this.name = options.name;
this.#maxCapacity = options.maxCapacity; this.maxCapacity = options.subroom.MaxPlayers;
this.#photonRoomId = `GCR-${this.#instanceId}`; this.photonRoomId = `GCR-${this.instanceId}`;
this.supportsJoinInProgress = server.Rooms.sceneSupportsJoinInProgress(options.room, options.subroom);
this.#init = false;
}
/**
* Can be heavy, so promise is used
*/
// deno-lint-ignore require-await
async #multiPresenceUpdate() {
if (this.#init) return;
for (const prof of this.#players)
prof.getSocketHandler()?.sendNotification("PresenceUpdate", this.#server.Presence.getPresence(prof).export());
} }
getPlayers() { getPlayers() {
@@ -61,21 +120,26 @@ export class Instance {
export() { export() {
const inst: RoomInstance = { const inst: RoomInstance = {
roomInstanceId: this.#instanceId, roomInstanceId: this.instanceId,
roomId: this.#roomId, roomId: this.roomId,
subRoomId: this.#subRoomId, subRoomId: this.subRoomId,
location: this.#location, location: this.location,
name: this.#name, name: this.name,
maxCapacity: this.#maxCapacity, maxCapacity: this.maxCapacity,
isFull: this.#isFull, isFull: this.isFull,
isPrivate: this.#isPrivate, isPrivate: this.isPrivate,
isInProgress: this.#isInProgress, isInProgress: this.isInProgress,
photonRegionId: this.#photonRegionId, photonRegionId: this.photonRegionId,
photonRoomId: this.#photonRoomId, photonRoomId: this.photonRoomId,
dataBlob: this.#dataBlob, dataBlob: this.dataBlob,
eventId: this.#eventId eventId: this.eventId
}; };
return inst; return inst;
} }
/**
* Current player count (instance size)
*/
get size() { return this.getPlayers().length }
} }

View File

@@ -1,6 +1,9 @@
import Logging from "@proxnet/undead-logging"; import Logging from "@proxnet/undead-logging";
import { ServerContentBase } from "../ContentBase.ts"; import { ServerContentBase } from "../ContentBase.ts";
import { type Instance } from "./Instance.ts"; import { Instance, InstanceCreationOptions } from "./Instance.ts";
import Command from "../commands/command.ts";
import z from "zod";
import { PushNotificationId } from "../socket/signalr/types.ts";
const log = new Logging("Instances"); const log = new Logging("Instances");
@@ -8,40 +11,80 @@ export class InstanceManager extends ServerContentBase {
#instances: Set<Instance> = new Set(); #instances: Set<Instance> = new Set();
clearEmptyInstances() { protected override start() {
this.server.Commands.addRootCommand(new Command({
key: ["inst", "i", "instance", "instances"],
subcommands: [
new Command({
key: ["getall", "all", "fetchall", "list"],
exec: () => {
return this.#instances.values().toArray().map(inst => inst.export());
},
zod: z.tuple([]),
help: "Get all instances"
}),
new Command({
key: ["kicklive", "modkick", "quitgame"],
exec: (id: number) => {
const inst = this.server.Instances.getInstance(id);
if (!inst) return false;
inst.getPlayers().forEach(prof => prof.getSocketHandler()?.sendNotification(PushNotificationId.ModerationKick));
return true;
},
zod: z.tuple([z.coerce.number()]),
help: "Send ModerationKick to all players in an instance. Returns true if successful."
})
]
}));
}
/**
* Can be heavy if instance count is high.
*/
// deno-lint-ignore require-await
async clearEmptyInstances() {
log.i(`Starting instance purge\n Before: ${ log.i(`Starting instance purge\n Before: ${
this.#instances.size this.#instances.size
} instances, ${ } instances, ${
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0) this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
} players`); } players`);
return new Promise(() => { for (const inst of this.#instances)
for (const inst of this.#instances) { if (inst.getPlayers().length === 0) this.deleteInstance(inst);
if (inst.getPlayers().length === 0) this.deleteInstance(inst);
}
log.i(`Instance purge complete\n After: ${ log.i(`Instance purge complete\n After: ${
this.#instances.size this.#instances.size
} instances, ${ } instances, ${
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0) this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
} players`); } players`);
});
} }
getAllInstances() { getAllInstances() {
return this.#instances;
} }
registerInstance(inst: Instance) { #generateAvailableId() {
let id = Math.round(Math.random() * Math.pow(2, 31));
while (this.#instances.values().find(inst => inst.instanceId === id)) id = this.#generateAvailableId();
return id;
}
#registerInstance(inst: Instance) {
if (!this.#instances.has(inst)) this.#instances.add(inst);
}
createInstance(options: InstanceCreationOptions) {
const inst = new Instance(this.server, this.#generateAvailableId(), options);
this.#instances.add(inst);
return inst;
} }
getInstance(id: number) { getInstance(id: number) {
return this.#instances.values().find(inst => inst.instanceId === id);
} }
deleteInstance(inst: Instance) { deleteInstance(inst: Instance) {
this.#instances.delete(inst);
} }
} }

View File

@@ -1,3 +1,5 @@
import { type Instance } from "./Instance.ts";
export enum RoomLocation { export enum RoomLocation {
Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04", Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04",
DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163", DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
@@ -52,4 +54,8 @@ export interface RoomInstance {
photonRoomId: string; photonRoomId: string;
dataBlob?: string; dataBlob?: string;
eventId?: number; eventId?: number;
}
export interface InstanceUpdatedEvent {
instance: Instance
} }

View File

@@ -1,7 +1,121 @@
import Logging from "@proxnet/undead-logging";
import { ServerContentBase } from "../ContentBase.ts"; import { ServerContentBase } from "../ContentBase.ts";
import { Instance } from "../instances/Instance.ts";
import { type RoomInstance } from "../instances/types.ts";
import type Profile from "../profiles/profile.ts";
import { RoomDataTypes } from "../rooms/internal/RoomDataTypes.ts";
import { type RoomFactory } from "../rooms/internal/RoomFactory.ts";
import { MatchmakingErrorCode } from "./types.ts";
const log = new Logging("Matchmaking");
export interface MatchmakingOptions {
roomName: string,
subRoomName?: string,
private?: boolean,
instanceId?: number,
profile: Profile
}
export interface InternalMatchmakingResponse {
errorCode: MatchmakingErrorCode,
roomInstance?: Instance
}
export interface MatchmakingResponse {
errorCode: MatchmakingErrorCode,
roomInstance?: RoomInstance
}
export class ServerMatchmakingBase extends ServerContentBase { export class ServerMatchmakingBase extends ServerContentBase {
async matchmake(options: MatchmakingOptions): Promise<InternalMatchmakingResponse | null> {
function getInstanceName() {
if (options.roomName === 'DormRoom')
return `@${options.profile.getUsername()}'s Dorm`;
else {
if (options.subRoomName || options.subRoomName !== "Home") return `^${options.roomName}.${options.subRoomName}`;
else return `^${options.roomName}`;
}
}
if (options.instanceId) {
const instance = this.server.Instances.getInstance(options.instanceId);
if (instance) {
if (instance.hasPlayer(options.profile)) return { errorCode: MatchmakingErrorCode.AlreadyInTargetInstance }
else {
if (instance.isFull) return { errorCode: MatchmakingErrorCode.InsufficientSpace }
else if (instance.isPrivate) return { errorCode: MatchmakingErrorCode.RoomInstanceIsPrivate }
options.profile.updateInstance(instance);
await this.server.Instances.clearEmptyInstances();
return { errorCode: MatchmakingErrorCode.Success, roomInstance: instance };
}
} else return { errorCode: MatchmakingErrorCode.NoSuchGame };
} else {
let targetRoom: RoomFactory | null = null;
if (options.roomName == 'DormRoom') targetRoom = await this.server.Rooms.getPlayerDorm(options.profile);
else targetRoom = await this.server.Rooms.getByName(options.roomName);
if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom };
if (targetRoom.Accessibility == RoomDataTypes.RoomAccessibility.Private && targetRoom.CreatorPlayerId !== options.profile.getId())
return { errorCode: MatchmakingErrorCode.RoomIsPrivate };
if (targetRoom.State !== RoomDataTypes.RoomState.Active) return { errorCode: MatchmakingErrorCode.RoomIsNotActive };
await this.server.Instances.clearEmptyInstances();
let allInstances = this.server.Instances.getAllInstances();
const subroom = (await targetRoom.getAllSubrooms()).values().find(factory => factory.Name == options.subRoomName);
// if a subroom was specified, filter instances that
if (subroom) allInstances = new Set(allInstances.values().filter(inst => inst.subRoomId == subroom.RoomSceneId));
// filter out instances that are in progress and do not support joininprogress
allInstances = new Set(allInstances.values().filter(inst => !(inst.isInProgress && !inst.supportsJoinInProgress)));
// sort instances
allInstances = new Set(allInstances.values().toArray().sort((a, b) => a.size + b.size));
const foundInstance = allInstances.values().toArray()[0];
if (!foundInstance) {
const matchmakeableSubrooms = (await targetRoom.getAllSubrooms()).values().filter(scene => scene.CanMatchmakeInto).toArray();
const index = Math.floor(Math.random() * matchmakeableSubrooms.length);
log.d(`Scene ${matchmakeableSubrooms[index].RoomSceneId} was chosen for matchmaking into new instance of room ${targetRoom.getRoomId()}`);
const newInst = this.server.Instances.createInstance({
room: targetRoom,
subroom: matchmakeableSubrooms[index],
private: options.private,
name: getInstanceName()
});
options.profile.updateInstance(newInst);
this.server.Instances.clearEmptyInstances();
return { errorCode: MatchmakingErrorCode.Success, roomInstance: newInst };
} else {
const currentInst = options.profile.getInstance();
if (currentInst?.instanceId === foundInstance.instanceId)
return { errorCode: MatchmakingErrorCode.AlreadyInBestInstance };
options.profile.updateInstance(foundInstance);
this.server.Instances.clearEmptyInstances();
return { errorCode: MatchmakingErrorCode.Success, roomInstance: foundInstance };
}
}
}
} }

View File

@@ -1,4 +1,4 @@
enum MatchmakingErrorCode { export enum MatchmakingErrorCode {
Success, Success,
NoSuchGame, NoSuchGame,
PlayerNotOnline, PlayerNotOnline,

View File

@@ -1,15 +1,19 @@
import z from "zod"; import z from "zod";
import Command from "../commands/command.ts"; import Command from "../commands/command.ts";
import { ServerContentBase } from "../ContentBase.ts"; import { ServerContentBase } from "../ContentBase.ts";
import { transformCheckEnum, transformStringToEnum } from "../../util/validators.ts"; import { transformCheckEnum } from "../../util/validators.ts";
import { sign } from "@hono/hono/jwt"; import { sign } from "@hono/hono/jwt";
import { CachedLogin, DbCachedLogin, PlatformMask, PlatformType, TokenFormat, TokenType } from "./types.ts"; import { CachedLogin, DbCachedLogin, PlatformMask, PlatformType, TokenFormat, TokenType } from "./types.ts";
import type Profile from "../profiles/profile.ts";
import { getNetConfig } from "../../net.ts";
export const steamAuthTicketSchema = z.object({ export const steamAuthTicketSchema = z.object({
Ticket: z.string().min(256), Ticket: z.string().min(256),
AppId: z.literal("471710") AppId: z.literal("471710")
}); });
const netConfig = getNetConfig();
export class PlatformsManager extends ServerContentBase { export class PlatformsManager extends ServerContentBase {
static platformsKey = "platforms"; static platformsKey = "platforms";
@@ -18,15 +22,18 @@ export class PlatformsManager extends ServerContentBase {
return [PlatformsManager.platformsKey, ...keys.filter(val => typeof val == 'string')]; return [PlatformsManager.platformsKey, ...keys.filter(val => typeof val == 'string')];
} }
async getToken(accountId: number, type: TokenType) { async getToken(prof: Profile, type: TokenType) {
const secret = Deno.env.get('SECRET'); const secret = Deno.env.get('SECRET');
if (!secret) throw new Error("No SECRET in env. Did you forget to set it?"); if (!secret) throw new Error("No SECRET in env. Did you forget to set it?");
const exp = type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952;
const token: TokenFormat = { const token: TokenFormat = {
typ: type, typ: type,
sub: accountId, sub: prof.getId(),
iss: "https://yarns.proxnet.dev/auth/", role: await prof.getRole(),
exp: type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952 iss: `${netConfig.securePublicHost ? "https" : "http"}://${netConfig.publicHost}/auth`,
iat: Math.round(Date.now() / 1000) - 5,
exp
} }
return await sign(JSON.parse(JSON.stringify(token)), secret); return await sign(JSON.parse(JSON.stringify(token)), secret);
} }
@@ -138,9 +145,9 @@ export class PlatformsManager extends ServerContentBase {
return await this.addCachedLogin(type, platformId, accountId); return await this.addCachedLogin(type, platformId, accountId);
}, },
zod: z.tuple([ zod: z.tuple([
z.string().transform(transformStringToEnum<PlatformType>(PlatformType)), z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
z.string(), z.string(),
z.string().transform(Number) z.coerce.number()
]), ]),
help: 'Add a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>' help: 'Add a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>'
}), }),
@@ -150,9 +157,9 @@ export class PlatformsManager extends ServerContentBase {
return await this.deleteCachedLogin(type, platformId, accountId); return await this.deleteCachedLogin(type, platformId, accountId);
}, },
zod: z.tuple([ zod: z.tuple([
z.string().transform(transformStringToEnum<PlatformType>(PlatformType)), z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
z.string(), z.string(),
z.string().transform(Number) z.coerce.number()
]), ]),
help: 'Remove a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>' help: 'Remove a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>'
}) })

View File

@@ -8,14 +8,13 @@ export interface TokenFormatBase {
export interface TokenFormat extends TokenFormatBase { export interface TokenFormat extends TokenFormatBase {
iss: string, iss: string,
exp: number, exp: number,
iat: number,
sub: number, sub: number,
role: ProfileRole
} }
export enum ProfileRole { export const ProfileRole = ["developer", "gameClient", "webClient"] as const;
Developer = 'developer', export type ProfileRole = typeof ProfileRole[number];
Web = 'webClient',
Game = 'gameClient'
}
export enum PlatformType { export enum PlatformType {
All = -1, All = -1,

View File

@@ -4,6 +4,9 @@ import { DeviceClass } from "../platforms/types.ts";
import Profile from "../profiles/profile.ts"; import Profile from "../profiles/profile.ts";
import { type ServerBase } from "../server.ts"; import { type ServerBase } from "../server.ts";
import { RoomInstance } from "../instances/types.ts"; import { RoomInstance } from "../instances/types.ts";
import Command from "../commands/command.ts";
import z from "zod";
import { PushNotificationId } from "../socket/signalr/types.ts";
export enum VRMovementMode { export enum VRMovementMode {
TELEPORT, TELEPORT,
@@ -33,7 +36,6 @@ export class Presence {
#statusVisibility: PlayerStatusVisibility = PlayerStatusVisibility.Offline; #statusVisibility: PlayerStatusVisibility = PlayerStatusVisibility.Offline;
#deviceClass: DeviceClass = DeviceClass.Unknown; #deviceClass: DeviceClass = DeviceClass.Unknown;
#roomInstance: RoomInstance | null = null;
#vrMovementMove: VRMovementMode | undefined; #vrMovementMove: VRMovementMode | undefined;
#lastExported: Date = new Date(); #lastExported: Date = new Date();
@@ -63,12 +65,16 @@ export class Presence {
this.#statusVisibility = sv; this.#statusVisibility = sv;
this.updateLastSeen(); this.updateLastSeen();
this.update(); this.update();
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
} }
setVRMovementMode(mm: VRMovementMode) { setVRMovementMode(mm: VRMovementMode) {
this.#vrMovementMove = mm; this.#vrMovementMove = mm;
this.updateLastSeen(); this.updateLastSeen();
this.update(); this.update();
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
} }
getLastExported() { getLastExported() {
@@ -87,14 +93,14 @@ export class Presence {
statusVisibility: this.#statusVisibility, statusVisibility: this.#statusVisibility,
deviceClass: this.#deviceClass, deviceClass: this.#deviceClass,
vrMovementMode: this.#vrMovementMove, vrMovementMode: this.#vrMovementMove,
roomInstance: this.#roomInstance roomInstance: this.#profile.getInstance()?.export() ?? null
} }
return e; return e;
} }
} }
export class PresenceBase extends ServerContentBase { export class ServerPresenceBase extends ServerContentBase {
#log = new Logging("Presence"); #log = new Logging("Presence");
@@ -119,9 +125,32 @@ export class PresenceBase extends ServerContentBase {
override start() { override start() {
this.#intervalId = setInterval(() => { this.#intervalId = setInterval(() => {
if (this.#presenceMap.size === 0) return;
this.#log.i('Clearing dead presences'); this.#log.i('Clearing dead presences');
this.#deleteDeadPresences(); this.#deleteDeadPresences();
}, 300_000); }, 300_000);
this.server.Commands.addRootCommand(new Command({
key: ["presence", "pres"],
subcommands: [
new Command({
key: ["quit", "quitgame"],
exec: async (pid: number) => {
const prof = await this.server.Profiles.get(pid);
if (!prof) return false;
const socket = prof.getSocketHandler();
if (!socket) return false;
socket.sendNotification(PushNotificationId.ModerationQuitGame);
return true;
},
zod: z.tuple([z.coerce.number()]),
help: "Sends ModerationQuitGame to a player's socket if it is connected. Returns true if successful."
})
]
}));
} }
override destroy() { override destroy() {

View File

@@ -4,7 +4,7 @@ import Profile from "./profile.ts";
import { SelfAccount, type RecNetAccount } from "./types/profile.ts"; import { SelfAccount, type RecNetAccount } from "./types/profile.ts";
import Command from "./../commands/command.ts"; import Command from "./../commands/command.ts";
import z from "zod"; import z from "zod";
import { PlatformMask, PlatformType, ProfileRole } from "../platforms/types.ts"; import { PlatformMask, PlatformType, ProfileRole, TokenType } from "../platforms/types.ts";
import Logging from "@proxnet/undead-logging"; import Logging from "@proxnet/undead-logging";
const profiles: Map<number, Profile> = new Map(); const profiles: Map<number, Profile> = new Map();
@@ -116,7 +116,7 @@ class ProfileManagerBase extends ServerContentBase {
else return prof.export(); else return prof.export();
}, },
zod: z.tuple([ zod: z.tuple([
z.string().transform(Number) z.coerce.number()
]), ]),
help: 'Fetch a profile: <id: number>' help: 'Fetch a profile: <id: number>'
}), }),
@@ -137,9 +137,9 @@ class ProfileManagerBase extends ServerContentBase {
else return await profile.getRole(); else return await profile.getRole();
}, },
zod: z.tuple([ zod: z.tuple([
z.string().transform(Number) z.coerce.number()
]), ]),
help: 'Set the profile role: <id: number>' help: 'Get profile role: <id: number>'
}), }),
new Command({ new Command({
key: ['setrole', 'sr'], key: ['setrole', 'sr'],
@@ -150,9 +150,9 @@ class ProfileManagerBase extends ServerContentBase {
}, },
zod: z.tuple([ zod: z.tuple([
z.coerce.number(), z.coerce.number(),
z.string() z.literal(ProfileRole)
]), ]),
help: 'Set the profile role: <id: number, role: "gameClient" | "webClient" | "developer">' help: 'Set profile role: <id: number, role: "gameClient" | "webClient" | "developer">'
}), }),
new Command({ new Command({
key: ['settings', 'setting'], key: ['settings', 'setting'],
@@ -163,6 +163,16 @@ class ProfileManagerBase extends ServerContentBase {
}, },
zod: z.tuple([z.coerce.number()]), zod: z.tuple([z.coerce.number()]),
help: "Get player settings" help: "Get player settings"
}),
new Command({
key: ["refreshtoken", "simutoken"],
exec: async (id: number) => {
const profile = await this.get(id);
if (profile) return await this.server.Platforms.getToken(profile, TokenType.Refresh);
else return null;
},
zod: z.tuple([z.coerce.number()]),
help: "Get a profile's refresh token / simulate generation of refresh token"
}) })
] ]
})); }));

View File

@@ -59,15 +59,15 @@ class Profile {
} }
constructProfilePropertyKey(...keys: (string | undefined)[]) { constructProfilePropertyKey(...keys: (string | undefined)[]) {
return [ ProfileManagerBase.profilesKey, this.#id, ...keys.filter(val => typeof val == 'string') ]; return [ProfileManagerBase.profilesKey, this.#id, ...keys.filter(val => typeof val == 'string')];
} }
getUsername() { getUsername() {
return this.#selfAcc.username; return this.#selfAcc.username;
} }
async setUsername(username: string) { async setUsername(username: string) {
this.#kv.getKv().delete([ ProfileManagerBase.profilesKey, this.#selfAcc.username ]); this.#kv.getKv().delete([ProfileManagerBase.profilesKey, this.#selfAcc.username]);
this.#kv.getKv().set([ ProfileManagerBase.profilesKey, username ], this.getId()); this.#kv.getKv().set([ProfileManagerBase.profilesKey, username], this.getId());
this.#selfAcc.username = username; this.#selfAcc.username = username;
await this.#saveSelfAcc(); await this.#saveSelfAcc();
@@ -81,7 +81,7 @@ class Profile {
await this.#saveSelfAcc(); await this.#saveSelfAcc();
} }
async getBio(){ async getBio() {
const key = this.constructProfilePropertyKey('bio'); const key = this.constructProfilePropertyKey('bio');
const val = await this.#kv.getKv().get<string>(key); const val = await this.#kv.getKv().get<string>(key);
if (!val.value) return null; if (!val.value) return null;
@@ -136,7 +136,17 @@ class Profile {
getInstance() { getInstance() {
return this.#instance; return this.#instance;
} }
setInstance(inst: Instance) { updateInstance(inst: Instance | null) {
if (inst == null) {
if (this.#instance) this.#instance.removePlayer(this);
this.#instance = null;
return;
}
if (this.#instance) this.#instance.removePlayer(this);
inst.addPlayer(this);
this.#server.emit('presence.update', { profile: this, presence: this.#server.Presence.getPresence(this) });
this.#instance = inst; this.#instance = inst;
} }

View File

@@ -8,8 +8,13 @@ import { AGRoom, AGRoomLocation, AGRoomRuntimeConfig } from "./internal/ClientRo
import Command from "../commands/command.ts"; import Command from "../commands/command.ts";
import z from "zod"; import z from "zod";
import { RoomLocation } from "../instances/types.ts"; import { RoomLocation } from "../instances/types.ts";
import { RootPath } from "../../util/path.ts";
import path from "node:path";
import { SubroomFactory } from "./internal/SubroomFactory.ts";
export const roomNameSchema = z.string().min(4).max(128).regex(/^[A-Za-z0-9._-]+$/);
export const roomIdSchema = z.coerce.number().min(1).max(Math.pow(2, 31));
const roomIdSchema = z.coerce.number().min(1).max(Math.pow(2, 31));
export class ServerRoomsBase extends ServerContentBase { export class ServerRoomsBase extends ServerContentBase {
#subroomKv = new KV('subrooms', true); #subroomKv = new KV('subrooms', true);
@@ -21,19 +26,24 @@ export class ServerRoomsBase extends ServerContentBase {
static roomNamesKey = "room_names"; static roomNamesKey = "room_names";
static playerDormsKey = "dorms"; static playerDormsKey = "dorms";
#agrooms: Set<number> = new Set(); #agroomIds: Set<number> = new Set();
#baserooms: Set<number> = new Set(); #baseroomIds: Set<number> = new Set();
#agRoomRuntimeConfig: AGRoomRuntimeConfig | null = null;
#joinInProgressLookup: Record<RoomLocation, boolean> | null = null;
override async start() { override async start() {
await this.#subroomKv.init(); await this.#subroomKv.init();
this.#log.i('[sub]rooms database initialized'); this.#log.i('[sub]rooms database initialized');
const agrooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.agRoomIdsKey]); const agrooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.agRoomIdsKey]);
if (agrooms.value !== null) this.#agrooms = agrooms.value; if (agrooms.value !== null) this.#agroomIds = agrooms.value;
this.#log.i(`${this.#agrooms.size} AG rooms exist`); this.#log.i(`${this.#agroomIds.size} AG rooms exist`);
const baserooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.baseRoomIdsKey]); const baserooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.baseRoomIdsKey]);
if (baserooms.value !== null) this.#baserooms = baserooms.value; if (baserooms.value !== null) this.#baseroomIds = baserooms.value;
this.#agRoomRuntimeConfig = JSON.parse(Deno.readTextFileSync(path.join(RootPath, "/res/rooms.json")));
this.#joinInProgressLookup = JSON.parse(Deno.readTextFileSync(path.join(RootPath, "/res/staticJoinInProgressLookup.json")));
this.server.Commands.addRootCommand(new Command({ this.server.Commands.addRootCommand(new Command({
key: ["rooms", "r", "room"], key: ["rooms", "r", "room"],
@@ -82,10 +92,17 @@ export class ServerRoomsBase extends ServerContentBase {
})) }))
} }
async #writeAgRooms() { async #writeAgRooms() {
await this.kv.getKv().set([ServerRoomsBase.agRoomIdsKey], this.#agrooms); await this.kv.getKv().set([ServerRoomsBase.agRoomIdsKey], this.#agroomIds);
} }
async #writeBaseRooms() { async #writeBaseRooms() {
await this.kv.getKv().set([ServerRoomsBase.baseRoomIdsKey], this.#baserooms); await this.kv.getKv().set([ServerRoomsBase.baseRoomIdsKey], this.#baseroomIds);
}
getAgRoomIds() {
return this.#agroomIds;
}
getAgRoomRuntimeConfig() {
if (!this.#agRoomRuntimeConfig) throw new Error("Config has not yet been initialized");
return this.#agRoomRuntimeConfig;
} }
async initBuiltinRooms(rooms: AGRoom[], locations: AGRoomLocation[]) { async initBuiltinRooms(rooms: AGRoom[], locations: AGRoomLocation[]) {
@@ -98,7 +115,10 @@ export class ServerRoomsBase extends ServerContentBase {
"ARRoom", "ARRoom",
"Registration", "Registration",
"DormRoom" "DormRoom"
].includes(room.Name)) return; ].includes(room.Name)) {
this.#log.w(`Room '${room.Name}' is not eligible for builtin room generation`);
return;
}
const roomFactory = await this.write(); const roomFactory = await this.write();
if (roomFactory == null) { if (roomFactory == null) {
@@ -118,7 +138,7 @@ export class ServerRoomsBase extends ServerContentBase {
roomFactory.Description = room.Description; roomFactory.Description = room.Description;
roomFactory.IsAGRoom = true; roomFactory.IsAGRoom = true;
roomFactory.CloningAllowed = room.CloningAllowed; roomFactory.CloningAllowed = room.CloningAllowed;
roomFactory.ImageName = `${room.Name}.png` roomFactory.ImageName = `${room.Name}.png`;
const supportPromises: Promise<unknown>[] = []; const supportPromises: Promise<unknown>[] = [];
roomFactory.removeAllHardwareSupport(); roomFactory.removeAllHardwareSupport();
@@ -134,7 +154,7 @@ export class ServerRoomsBase extends ServerContentBase {
subroomFactory.RoomId = roomFactory.getRoomId(); subroomFactory.RoomId = roomFactory.getRoomId();
subroomFactory.Name = scene.Name; subroomFactory.Name = scene.Name;
subroomFactory.RoomSceneLocationId = scene.RoomSceneLocationId; subroomFactory.RoomSceneLocationId = scene.RoomSceneLocationId as RoomLocation;
subroomFactory.IsSandbox = scene.IsSandbox; subroomFactory.IsSandbox = scene.IsSandbox;
subroomFactory.CanMatchmakeInto = scene.CanMatchmakeInto; subroomFactory.CanMatchmakeInto = scene.CanMatchmakeInto;
subroomFactory.MaxPlayers = scene.MaxPlayers; subroomFactory.MaxPlayers = scene.MaxPlayers;
@@ -146,15 +166,33 @@ export class ServerRoomsBase extends ServerContentBase {
await Promise.all(supportPromises); await Promise.all(supportPromises);
this.#agrooms.add(roomFactory.getRoomId()); this.#agroomIds.add(roomFactory.getRoomId());
await roomFactory.write(); await roomFactory.write();
if (room.CloningAllowed) this.#baserooms.add(roomFactory.getRoomId()); if (room.CloningAllowed) this.#baseroomIds.add(roomFactory.getRoomId());
})); }));
await this.#writeAgRooms(); await this.#writeAgRooms();
await this.#writeBaseRooms(); await this.#writeBaseRooms();
this.#log.i(`${this.#agrooms.size} AG rooms added: [${this.#agrooms.values().toArray().join(',')}]`); this.#log.i(`${this.#agroomIds.size} AG rooms added: [${this.#agroomIds.values().toArray().join(',')}]`);
}
sceneSupportsJoinInProgress(roomFactory: RoomFactory, subroomFactory: SubroomFactory) {
const agRoomRuntimeConfig = this.server.Rooms.getAgRoomRuntimeConfig();
const builtinScene = agRoomRuntimeConfig.Rooms.find(room => room.Name === roomFactory.Name)
?.Scenes.find(scene => scene.RoomSceneLocationId === subroomFactory.RoomSceneLocationId);
if (builtinScene) return builtinScene.SupportsJoinInProgress;
else {
if (!this.#joinInProgressLookup) throw new Error("JoinInProgress lookup table is not yet initialized");
const lookup = this.#joinInProgressLookup[subroomFactory.RoomSceneLocationId];
if (lookup) return lookup;
else return false;
}
}
async getMany(...ids: number[]) {
return (await Promise.all(ids.map(id => this.get(id)))).filter(val => val !== null);
} }
async getAvailableRoomId() { async getAvailableRoomId() {
@@ -163,6 +201,10 @@ export class ServerRoomsBase extends ServerContentBase {
return id; return id;
} }
async getByRoomSceneId(id: number) {
return await new SubroomFactory(this.server, this.#subroomKv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
}
getKv() { getKv() {
return this.kv; return this.kv;
} }
@@ -176,7 +218,7 @@ export class ServerRoomsBase extends ServerContentBase {
async getPlayerDorm(profile: Profile) { async getPlayerDorm(profile: Profile) {
const id = await this.kv.getKv().get<number>([ServerRoomsBase.playerDormsKey, profile.getId()]); const id = await this.kv.getKv().get<number>([ServerRoomsBase.playerDormsKey, profile.getId()]);
if (id.value == null) { if (id.value == null) {
const roomFactory = await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree, id: await this.getAvailableRoomId() }); const roomFactory = await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
if (!roomFactory) return null; if (!roomFactory) return null;
roomFactory.setRoomProperties({ roomFactory.setRoomProperties({
Name: `DormRoom`, Name: `DormRoom`,
@@ -216,7 +258,7 @@ export class ServerRoomsBase extends ServerContentBase {
return roomFactory; return roomFactory;
} }
else return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id.value }); else return await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Fetch, id: id.value });
} }
async getByName(name: string) { async getByName(name: string) {
@@ -226,11 +268,11 @@ export class ServerRoomsBase extends ServerContentBase {
} }
async get(id: number) { async get(id: number) {
return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id }); return await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Fetch, id: id });
} }
async write(mode?: WriteMode) { async write(mode?: WriteMode) {
return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: await this.getAvailableRoomId() }); return await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
} }
} }

View File

@@ -1,3 +1,5 @@
import { RoomLocation } from "../../instances/types.ts";
export enum WriteMode { export enum WriteMode {
Overwrite = "overwrite", Overwrite = "overwrite",
WriteIfFree = "if_free" WriteIfFree = "if_free"
@@ -34,7 +36,7 @@ export enum RoomAccessibility {
export interface RoomScene { export interface RoomScene {
RoomSceneId: number, RoomSceneId: number,
RoomId: number, RoomId: number,
RoomSceneLocationId: string, RoomSceneLocationId: RoomLocation,
Name: string, Name: string,
IsSandbox: boolean, IsSandbox: boolean,
DataBlobName: string, DataBlobName: string,
@@ -151,7 +153,7 @@ export interface DatabaseRoom {
export interface SubroomProps { export interface SubroomProps {
RoomId: number, RoomId: number,
RoomSceneLocationId: string, RoomSceneLocationId: RoomLocation,
Name: string, Name: string,
IsSandbox: boolean, IsSandbox: boolean,
MaxPlayers: number, MaxPlayers: number,

View File

@@ -29,6 +29,7 @@ export class RoomFactory {
#server: ServerBase; #server: ServerBase;
#kv: KV; #kv: KV;
#subroomKv: KV;
#roomId: number | undefined; #roomId: number | undefined;
@@ -43,10 +44,11 @@ export class RoomFactory {
#cannotAccessBeforeInitError = new Error("Cannot access properties before initialization"); #cannotAccessBeforeInitError = new Error("Cannot access properties before initialization");
#cannotWriteBeforeInitError = new Error("Cannot write before initialization"); #cannotWriteBeforeInitError = new Error("Cannot write before initialization");
constructor(server: ServerBase, kv: KV) { constructor(server: ServerBase, kv: KV, subroomKv: KV) {
this.#server = server; this.#server = server;
this.#kv = kv; this.#kv = kv;
this.#subroomKv = subroomKv;
} }
@@ -161,9 +163,7 @@ export class RoomFactory {
const autoTags = this.getTags(); const autoTags = this.getTags();
const galvTags = this.getGalvanicTags(); const galvTags = this.getGalvanicTags();
const subroomExports = (await Promise.all( const subroomExports = (await Promise.all((await this.getAllSubrooms()).values())).map(factory => factory.export());
this.getSubrooms().values().map(subroom => this.getSubroom(subroom))
)).map(factory => factory.export());
return { return {
Room: { Room: {
@@ -208,22 +208,27 @@ export class RoomFactory {
return this.#roomId; return this.#roomId;
} }
getSubrooms() { getSubroomIds() {
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError; if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
return this.#subrooms; return this.#subrooms;
} }
async getSubroom(id: number) { async getSubroom(id: number) {
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError; if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
if (!this.#subrooms.has(id)) throw new Error("Subroom not available to this room"); if (!this.#subrooms.has(id)) throw new Error("Subroom not available to this room");
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id }); return await new SubroomFactory(this.#server, this.#subroomKv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
} }
getAvailableSubroomId() { async getAllSubrooms() {
return new Set(await Promise.all(
this.getSubroomIds().values().map(subroom => this.getSubroom(subroom))
));
}
async getAvailableSubroomId() {
let id = Math.round(Math.random() * Math.pow(2, 31)); let id = Math.round(Math.random() * Math.pow(2, 31));
if (this.getSubrooms().has(id)) id = this.getAvailableSubroomId(); if ((await this.#subroomKv.getKv().get<unknown>([id])).value) id = await this.getAvailableSubroomId();
return id; return id;
} }
async newSubroom(mode?: WriteMode) { async newSubroom(mode?: WriteMode) {
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: this.getAvailableSubroomId() }); return await new SubroomFactory(this.#server, this.#subroomKv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: await this.getAvailableSubroomId() });
} }
addSubroom(id: number) { addSubroom(id: number) {
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError; if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;

View File

@@ -2,6 +2,7 @@ import Logging from "@proxnet/undead-logging";
import type KV from "../../persistence/kv.ts"; import type KV from "../../persistence/kv.ts";
import { type ServerBase } from "../../server.ts"; import { type ServerBase } from "../../server.ts";
import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, RoomSaveMap, WriteMode } from "./RoomDataTypes.ts"; import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, RoomSaveMap, WriteMode } from "./RoomDataTypes.ts";
import { RoomLocation } from "../../instances/types.ts";
export interface SubroomFactoryOptions { export interface SubroomFactoryOptions {
mode: FactoryMode, mode: FactoryMode,
@@ -44,7 +45,7 @@ export class SubroomFactory {
this.#obj = options.mode == FactoryMode.Fetch ? data.value : { this.#obj = options.mode == FactoryMode.Fetch ? data.value : {
RoomId: 0, RoomId: 0,
RoomSceneLocationId: "", RoomSceneLocationId: RoomLocation.MakerRoom,
Name: "Subroom data init failed, contact an admin!", Name: "Subroom data init failed, contact an admin!",
IsSandbox: false, IsSandbox: false,
MaxPlayers: 8, MaxPlayers: 8,
@@ -92,7 +93,7 @@ export class SubroomFactory {
get RoomSceneId() { if (!this.#subroomId) throw this.#cannotAccessBeforeInitError; else return this.#subroomId; } get RoomSceneId() { if (!this.#subroomId) throw this.#cannotAccessBeforeInitError; else return this.#subroomId; }
get RoomSceneLocationId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.RoomSceneLocationId; } get RoomSceneLocationId(): RoomLocation { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.RoomSceneLocationId; }
set RoomSceneLocationId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.RoomSceneLocationId = data } set RoomSceneLocationId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.RoomSceneLocationId = data }
get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Name; } get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Name; }

View File

@@ -3,11 +3,14 @@ import { ServerUpdateEvent } from "../serverevents.ts";
import { AvatarContentBase } from "./avatars/base.ts"; import { AvatarContentBase } from "./avatars/base.ts";
import { EventManager } from "./baseevent.ts"; import { EventManager } from "./baseevent.ts";
import { CommandsBase } from "./commands/commands.ts"; import { CommandsBase } from "./commands/commands.ts";
import { ServerConsumablesBase } from "./consumables/base.ts";
import { ServerContentManager } from "./content/base.ts"; import { ServerContentManager } from "./content/base.ts";
import GameConfigsBase from "./gameconfigs/base.ts"; import GameConfigsBase from "./gameconfigs/base.ts";
import { InstanceManager } from "./instances/base.ts"; import { InstanceManager } from "./instances/base.ts";
import { ServerMatchmakingBase } from "./matchmaking/base.ts";
import { Objective, ObjectiveType } from "./objectives/base.ts"; import { Objective, ObjectiveType } from "./objectives/base.ts";
import { PlatformsManager } from "./platforms/base.ts"; import { PlatformsManager } from "./platforms/base.ts";
import { ServerPresenceBase } from "./presence/base.ts";
import { type PresenceUpdateEvent } from "./presence/events/PresenceUpdateEvent.ts"; import { type PresenceUpdateEvent } from "./presence/events/PresenceUpdateEvent.ts";
import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts"; import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts";
import { ProfileUpdatedSettingEvent } from "./profiles/events/ProfileUpdatedSetting.ts"; import { ProfileUpdatedSettingEvent } from "./profiles/events/ProfileUpdatedSetting.ts";
@@ -15,6 +18,7 @@ import { type RoleUpdateEvent } from "./profiles/events/RoleUpdate.ts";
import ProfileManagerBase from "./profiles/manager.ts"; import ProfileManagerBase from "./profiles/manager.ts";
import { ServerRoomsBase } from "./rooms/base.ts"; import { ServerRoomsBase } from "./rooms/base.ts";
import { RoomUpdatedEvent, SubroomUpdatedEvent } from "./rooms/internal/RoomEvents.ts"; import { RoomUpdatedEvent, SubroomUpdatedEvent } from "./rooms/internal/RoomEvents.ts";
import { AnnouncementDTO } from "./types.ts";
interface ServerEvents { interface ServerEvents {
'profile.roleupdate': RoleUpdateEvent, 'profile.roleupdate': RoleUpdateEvent,
@@ -42,7 +46,7 @@ interface AutoMicMutingConfig {
MicSpamWarningStateVolumeMultiplier: number; MicSpamWarningStateVolumeMultiplier: number;
}; };
export type PublicConfig = { export interface PublicConfig {
ShareBaseUrl: string; ShareBaseUrl: string;
ServerMaintenance: { ServerMaintenance: {
StartsInMinutes: number; StartsInMinutes: number;
@@ -61,6 +65,9 @@ class ServerBase extends EventManager<ServerEvents> {
Instances = new InstanceManager(this, 'instances'); Instances = new InstanceManager(this, 'instances');
Content = new ServerContentManager(this, "content"); Content = new ServerContentManager(this, "content");
Rooms = new ServerRoomsBase(this, 'rooms', true); Rooms = new ServerRoomsBase(this, 'rooms', true);
Matchmaking = new ServerMatchmakingBase(this, "match");
Presence = new ServerPresenceBase(this, "pres");
Consumables = new ServerConsumablesBase(this, "consumables");
generateMask(...num: number[]) { generateMask(...num: number[]) {
return num.reduce((sum, val) => sum + val, 0); return num.reduce((sum, val) => sum + val, 0);
@@ -137,6 +144,11 @@ class ServerBase extends EventManager<ServerEvents> {
return conf; return conf;
} }
getAnnouncements(): AnnouncementDTO[] {
return [];
}
} }
const Server = new ServerBase(); const Server = new ServerBase();

View File

@@ -4,7 +4,8 @@ import { type ConsoleItem, ConsoleItemSchema } from "./zod.ts";
import Server from "../../server.ts"; import Server from "../../server.ts";
import { getSourceAddress } from "../../../util/net.ts"; import { getSourceAddress } from "../../../util/net.ts";
import { consoleSockets } from "../../../main.ts"; import { consoleSockets } from "../../../main.ts";
import chalk from "npm:chalk@^5.3.0"; import chalk from "chalk";
import { CommandSenderType } from "../../commands/cmdtypes.ts";
export default class SocketConsoleHandler { export default class SocketConsoleHandler {
@@ -55,7 +56,7 @@ export default class SocketConsoleHandler {
if (!zodParsed.success) this.destroy(); if (!zodParsed.success) this.destroy();
else if (zodParsed.data.e == ConsoleEvent.Command) { else if (zodParsed.data.e == ConsoleEvent.Command) {
const data = await Server.Commands.dispatch(...zodParsed.data.d.split(' ')); const data = await Server.Commands.dispatch({ type: CommandSenderType.Console }, ...zodParsed.data.d.split(' '));
if (data instanceof Error) throw data; if (data instanceof Error) throw data;
this.send(ConsoleEvent.Message, chalk.gray(`> ${chalk.yellow(data)}`)); this.send(ConsoleEvent.Message, chalk.gray(`> ${chalk.yellow(data)}`));

View File

@@ -18,6 +18,8 @@ import { SocketTarget } from "./targets/targetbase.ts";
import type Profile from "../../profiles/profile.ts"; import type Profile from "../../profiles/profile.ts";
import { detailedLog } from "../../../main.ts"; import { detailedLog } from "../../../main.ts";
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts"; import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
import Server from "../../server.ts";
import { PresenceUpdateEvent } from "../../presence/events/PresenceUpdateEvent.ts";
const logmessages = true; const logmessages = true;
@@ -32,6 +34,10 @@ export class SignalRSocketHandler {
#killed = false; #killed = false;
#presCb = (ev: PresenceUpdateEvent) => {
if (ev.profile == this.#profile) this.sendNotification("PresenceUpdate", ev.presence.export());
}
constructor(socket: WebSocket, player: Profile) { constructor(socket: WebSocket, player: Profile) {
this.#socket = socket; this.#socket = socket;
@@ -69,6 +75,8 @@ export class SignalRSocketHandler {
} }
async #onMessage(message: Message) { async #onMessage(message: Message) {
this.#profile.Matchmaking.updateLastSeen();
if (message.kind == MessageKind.Protocol) { if (message.kind == MessageKind.Protocol) {
this.sendRaw({}); this.sendRaw({});
return; return;
@@ -130,6 +138,8 @@ export class SignalRSocketHandler {
} }
}); });
Server.on('presence.update', this.#presCb);
this.#socket.addEventListener('close', this.destroy(this, true)); this.#socket.addEventListener('close', this.destroy(this, true));
} }
@@ -137,10 +147,12 @@ export class SignalRSocketHandler {
return (ev: CloseEvent) => { return (ev: CloseEvent) => {
handler.#killed = true; handler.#killed = true;
Server.off('presence.update', this.#presCb);
let errorReason = "Socket closed by server"; let errorReason = "Socket closed by server";
this.#log.d(`Socket close code: ${ev.code}`); this.#log.d(`Socket close code: ${ev.code}`);
if (ev.reason.includes('Bye!')) errorReason = "Socket closed by client request"; if (ev.reason.includes('Bye!')) errorReason = "Socket closed by client request";
handler.sendRaw({ type: 7, error: errorReason }); handler.sendRaw({ type: 7, error: errorReason });
if (!internal) handler.#socket.close(); if (!internal) handler.#socket.close();

View File

@@ -6,7 +6,7 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
#ids: number[] = []; #ids: number[] = [];
override zod = z.object({ override zod = z.object({
PlayerIds: z.array(z.number().nonnegative().max(2_147_483_647)) PlayerIds: z.array(z.number().nonnegative().max(Math.pow(2, 31)))
}); });
override exec(...ids: number[]) { override exec(...ids: number[]) {

29
src/server/types.ts Normal file
View File

@@ -0,0 +1,29 @@
import { PlatformType } from "./platforms/types.ts";
export enum AnnouncementType {
Update,
Contest,
Store,
Event,
Warning
}
enum AnnouncementLinkType {
Url,
AccountId,
EventId,
RoomName,
Storefront
}
export interface AnnouncementDTO {
AnnouncementId: number,
AnnouncementType: AnnouncementType,
Title: string,
Body: string,
ImageName: string,
LinkType: AnnouncementLinkType,
LinkName: string,
LinkUri: string,
Platform: PlatformType,
CreatedAt: string,
}

View File

@@ -134,4 +134,42 @@ export class RateLimiter {
#close() { #close() {
clearInterval(this.#intervalId); clearInterval(this.#intervalId);
} }
}
const loginLockBodySchema = z.object({
LoginLock: z.uuidv4()
});
export const loginLockMiddleware = async (c: Context<HonoEnv>, nxt: Next) => {
function unauthorized() {
return statusResponse(c, HTTPStatus.Unauthorized);
}
if (c.req.header("Content-Type") !== "application/x-www-form-urlencoded") return unauthorized();
try {
const form = await c.req.formData();
const body = await loginLockBodySchema.safeParseAsync(Object.fromEntries(form.entries()));
if (body.success) {
if (typeof c.get('profile') == 'undefined') {
log.w(`Profile was not set, cannot validate LoginLock. Was the request authorized?`);
return statusResponse(c, HTTPStatus.InternalServerError);
}
const profile = c.get('profile');
const loginLock = await profile.Matchmaking.getLoginLock();
if (!loginLock) await profile.Matchmaking.setLoginLock(body.data.LoginLock);
else if (body.data.LoginLock !== loginLock) {
log.w(`LoginLock did not match. The token for this profile could be compromised or the client is an unknown state.`);
return unauthorized();
}
return await nxt();
} else {
log.w(`LoginLock parse failed: ${JSON.stringify(body.error)}`);
return unauthorized();
}
} catch {
return unauthorized();
}
} }

View File

@@ -11,23 +11,23 @@ export async function routeImporter(hono: Hono<HonoEnv>, prefix: string, paths:
for (const route of items) hono.route(route.path, route.app); for (const route of items) hono.route(route.path, route.app);
} }
export async function importer<T>(importKey: string, prefix: string, paths: string[]): Promise<T[]> { export async function importer<T>(importKey: string, p: string, paths: string[]): Promise<T[]> {
const log = new Logging(`Importer:'${importKey}'-${prefix}`); const log = new Logging(`Importer:'${importKey}':${p}`);
const items: T[] = []; const items: T[] = [];
for (const pathStr of paths) { for (const pathStr of paths) {
const importPath = path.join(process.cwd(), prefix, pathStr); const importPath = path.join(process.cwd(), p, pathStr);
if (debug) log.d(`'${importKey}' found ${importPath}`); if (debug) log.d(`'${importKey}' found ${importPath}`);
for await (const localPath of Deno.readDir(importPath)) { for await (const localPath of Deno.readDir(importPath)) {
if (localPath.isDirectory) continue; if (localPath.isDirectory) continue;
if (localPath.isFile && localPath.name.endsWith('.ts')) { if (localPath.isFile && localPath.name.endsWith('.ts')) {
const fullPath = path.join('file://', importPath, localPath.name); const fullPath = path.join(importPath, localPath.name);
if (debug) log.d(`'${importKey}' importing ${fullPath}`); if (debug) log.d(`'${importKey}' importing ${fullPath}`);
await import(fullPath).then(val => { await import(`file://${fullPath}`).then(val => {
if (val[importKey]) items.push(val[importKey]); if (val[importKey]) items.push(val[importKey]);
else log.w(`Import key '${importKey}' not found on: '${fullPath}'`); else log.w(`Import key '${importKey}' not found on: '${fullPath}'`);

View File

@@ -43,9 +43,25 @@ export const transformStringToEnum = <T>(anEnum: { [s: string]: string | number
export const transformCheckEnum = <T>(anEnum: { [s: string]: string | number }) => { export const transformCheckEnum = <T>(anEnum: { [s: string]: string | number }) => {
return (arg: number | string, ctx: z.RefinementCtx<number | string>) => { return (arg: number | string, ctx: z.RefinementCtx<number | string>) => {
if (typeof anEnum[arg] == 'undefined') { function invalid() {
ctx.addIssue("Not an enum member"); ctx.addIssue("Invalid enum member");
}
if (typeof arg == 'string') {
if (typeof anEnum[arg] == 'number') return anEnum[arg] as T;
else {
invalid();
return null;
}
} else if (typeof arg == 'number') {
if (typeof anEnum[arg] == 'string') return arg as T;
else {
invalid();
return null;
}
} else {
invalid();
return null; return null;
} else return anEnum[arg] as T; }
} }
} }