duhhhhhhhh
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
/.env
|
||||
/persist/
|
||||
/worklist.txt
|
||||
/node_modules
|
||||
/build
|
||||
/todo.txt
|
||||
@@ -9,6 +9,7 @@
|
||||
"@oneday/http-status": "jsr:@oneday/http-status@^0.2.0",
|
||||
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.5.0",
|
||||
"@std/assert": "jsr:@std/assert@1",
|
||||
"chalk": "npm:chalk@^5.6.2",
|
||||
"sharp": "npm:sharp@^0.34.3",
|
||||
"zod": "npm:zod@^4.0.5"
|
||||
},
|
||||
|
||||
7
deno.lock
generated
7
deno.lock
generated
@@ -18,6 +18,7 @@
|
||||
"jsr:@std/path@^1.1.1": "1.1.2",
|
||||
"npm:@types/node@*": "24.2.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:zod@^4.0.5": "4.1.5"
|
||||
},
|
||||
@@ -53,7 +54,7 @@
|
||||
"@proxnet/undead-logging@1.5.1": {
|
||||
"integrity": "f858b6357d52c4bc1bbab279200dae86ed573ea45d945a8dfaf2f2cb23c4b649",
|
||||
"dependencies": [
|
||||
"npm:chalk"
|
||||
"npm:chalk@^5.3.0"
|
||||
]
|
||||
},
|
||||
"@std/assert@1.0.14": {
|
||||
@@ -240,6 +241,9 @@
|
||||
"chalk@5.6.0": {
|
||||
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="
|
||||
},
|
||||
"chalk@5.6.2": {
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
|
||||
},
|
||||
"color-convert@2.0.1": {
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": [
|
||||
@@ -330,6 +334,7 @@
|
||||
"jsr:@oneday/http-status@0.2",
|
||||
"jsr:@proxnet/undead-logging@^1.5.0",
|
||||
"jsr:@std/assert@1",
|
||||
"npm:chalk@^5.6.2",
|
||||
"npm:sharp@~0.34.3",
|
||||
"npm:zod@^4.0.5"
|
||||
]
|
||||
|
||||
254
res/consumables.json
Normal file
254
res/consumables.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
38
res/staticJoinInProgressLookup.json
Normal file
38
res/staticJoinInProgressLookup.json
Normal 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
|
||||
}
|
||||
@@ -182,7 +182,7 @@ Server.Commands.addRootCommand(new Command({
|
||||
else return null;
|
||||
},
|
||||
zod: z.tuple([
|
||||
z.string().transform(Number)
|
||||
z.coerce.number()
|
||||
]),
|
||||
help: 'Get ping (in ms) to the server'
|
||||
}));
|
||||
@@ -1,10 +1,11 @@
|
||||
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 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 Steam from "../../../util/steam/steam.ts";
|
||||
import { HTTPStatus } from "@oneday/http-status";
|
||||
|
||||
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 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),
|
||||
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', c => {
|
||||
route.app.get('/me', authenticate, c => {
|
||||
const profile = c.get('profile');
|
||||
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);
|
||||
});
|
||||
8
src/routes/api/routes/announcement.ts
Normal file
8
src/routes/api/routes/announcement.ts
Normal 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());
|
||||
});
|
||||
@@ -25,3 +25,11 @@ route.app.post('/v2/set', typedZValidator('json', profileAvatarSchema), async c
|
||||
await c.get('profile').Avatar.setAvatar(outfit);
|
||||
return c.status(200);
|
||||
});
|
||||
|
||||
route.app.get('/v3/saved', c => {
|
||||
return c.json([]); // stub
|
||||
});
|
||||
|
||||
route.app.get('/v2/gifts', c => {
|
||||
return c.json([]); // stub
|
||||
});
|
||||
19
src/routes/api/routes/challenge.ts
Normal file
19
src/routes/api/routes/challenge.ts
Normal 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
|
||||
});
|
||||
171
src/routes/api/routes/checklist.ts
Normal file
171
src/routes/api/routes/checklist.ts
Normal 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
|
||||
});
|
||||
23
src/routes/api/routes/communityboard.ts
Normal file
23
src/routes/api/routes/communityboard.ts
Normal 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
|
||||
});
|
||||
8
src/routes/api/routes/consumables.ts
Normal file
8
src/routes/api/routes/consumables.ts
Normal 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());
|
||||
});
|
||||
7
src/routes/api/routes/equipment.ts
Normal file
7
src/routes/api/routes/equipment.ts
Normal 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
|
||||
});
|
||||
7
src/routes/api/routes/images.ts
Normal file
7
src/routes/api/routes/images.ts
Normal 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
|
||||
});
|
||||
10
src/routes/api/routes/objectives.ts
Normal file
10
src/routes/api/routes/objectives.ts
Normal 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
|
||||
});
|
||||
10
src/routes/api/routes/playerevents.ts
Normal file
10
src/routes/api/routes/playerevents.ts
Normal 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
|
||||
});
|
||||
@@ -14,14 +14,14 @@ const getProgParamSchema = z.object({
|
||||
id: z.coerce.number()
|
||||
});
|
||||
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({
|
||||
Ids: z.union([z.array(z.coerce.number()), z.coerce.number()])
|
||||
});
|
||||
route.app.post('/v1/progression/bulk', authenticate, typedZValidator('form', getProgBulkBodySchema), async c => {
|
||||
const ids = c.req.valid('form').Ids;
|
||||
route.app.post('/v1/progression/bulk', authenticate, typedZValidator('json', getProgBulkBodySchema), async c => {
|
||||
const ids = c.req.valid('json').Ids;
|
||||
|
||||
if (typeof ids == 'object') {
|
||||
const profs = await Server.Profiles.getMany(...ids);
|
||||
|
||||
7
src/routes/api/routes/playersubscriptions.ts
Normal file
7
src/routes/api/routes/playersubscriptions.ts
Normal 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
|
||||
});
|
||||
@@ -8,7 +8,6 @@ route.app.get('/v2/get', c => {
|
||||
return c.json([]);
|
||||
});
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
route.app.post('/v1/bulkignoreplatformusers', async c => {
|
||||
route.app.post('/v1/bulkignoreplatformusers', c => {
|
||||
return statusResponse(c, HTTPStatus.OK);
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import z from "zod";
|
||||
import { createHonoRoute } from "../../../util/import.ts";
|
||||
import { typedZValidator } from "../../../util/validators.ts";
|
||||
import Server from "../../../server/server.ts";
|
||||
import { authenticate } from "../../../util/api.ts";
|
||||
|
||||
export const route = createHonoRoute("/rooms");
|
||||
|
||||
@@ -24,3 +25,12 @@ route.app.get('/v2/name/:name', typedZValidator('param', getRoomByNameParamSchem
|
||||
if (room == null) return await nxt();
|
||||
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);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import z from "zod";
|
||||
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 Server from "../../../server/server.ts";
|
||||
import { authenticate } from "../../../util/api.ts";
|
||||
@@ -11,7 +11,7 @@ const log = new Logging("CachedLoginDebug");
|
||||
export const route = createHonoRoute("/cachedlogin");
|
||||
|
||||
const cachedLoginFetchParamSchema = z.object({
|
||||
platformType: z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
|
||||
platformType: z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
|
||||
platformId: z.string().min(4)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHonoRoute } from "../../../util/import.ts";
|
||||
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 { steamAuthTicketSchema } from "../../../server/platforms/base.ts";
|
||||
import { gameVerString } from "../../api/routes/versioncheck.ts";
|
||||
@@ -16,13 +16,13 @@ export const route = createHonoRoute("/connect");
|
||||
|
||||
const authBodyBaseSchema = z.object({
|
||||
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");
|
||||
else return PlatformType.Steam;
|
||||
}),
|
||||
platform_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(),
|
||||
ver: z.literal(gameVerString),
|
||||
asid: z.coerce.number(),
|
||||
@@ -39,7 +39,7 @@ const authBodyBaseSchema = z.object({
|
||||
|
||||
const cachedLoginGrantSchema = authBodyBaseSchema.extend({
|
||||
grant_type: z.literal('cached_login'),
|
||||
account_id: z.string().transform(Number),
|
||||
account_id: z.coerce.number(),
|
||||
});
|
||||
const refreshTokenGrantSchema = authBodyBaseSchema.extend({
|
||||
grant_type: z.literal('refresh_token'),
|
||||
@@ -97,11 +97,12 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
|
||||
|
||||
const profile = await Server.Profiles.get(token.sub);
|
||||
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({
|
||||
access_token: accessToken,
|
||||
refresh_token: form.refresh_token,
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
} catch (err) {
|
||||
log.w(`Authentication error (token req): ${(err as Error).stack}`);
|
||||
@@ -113,8 +114,8 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
|
||||
const profile = await Server.Profiles.get(form.account_id);
|
||||
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
|
||||
await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id);
|
||||
const accessToken = await Server.Platforms.getToken(profile.getId(), TokenType.Access);
|
||||
const refreshToken = await Server.Platforms.getToken(profile.getId(), TokenType.Refresh);
|
||||
const accessToken = await Server.Platforms.getToken(profile, TokenType.Access);
|
||||
const refreshToken = await Server.Platforms.getToken(profile, TokenType.Refresh);
|
||||
|
||||
return c.json({
|
||||
access_token: accessToken,
|
||||
|
||||
@@ -1,50 +1,7 @@
|
||||
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');
|
||||
|
||||
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/', [
|
||||
'routes'
|
||||
]);
|
||||
@@ -1,13 +1,76 @@
|
||||
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 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");
|
||||
|
||||
route.app.post('/room/:roomName', c => {
|
||||
return statusResponse(c, HTTPStatus.NotImplemented);
|
||||
const gotoRoomBodySchema = z.object({
|
||||
CreatePrivateInstance: z.boolean().optional(),
|
||||
ExpectedPlayerIds: z.array(z.int()).optional(),
|
||||
BypassMovementModeRestriction: z.boolean().optional()
|
||||
});
|
||||
|
||||
route.app.post('/room/:roomName/:subRoomName', c => {
|
||||
return statusResponse(c, HTTPStatus.NotImplemented);
|
||||
const gotoRoomParamSchema = z.object({
|
||||
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);
|
||||
}
|
||||
|
||||
);
|
||||
@@ -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 { loginLockMiddleware } from "../root.ts";
|
||||
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");
|
||||
|
||||
route.app.use(authenticate);
|
||||
const playerIdsQuerySchema = z.object({
|
||||
id: z.union([z.coerce.number(), z.array(z.coerce.number())])
|
||||
});
|
||||
route.app.get('/', typedZValidator('query', playerIdsQuerySchema), async c => {
|
||||
const id = c.req.valid('query').id;
|
||||
|
||||
route.app.post('/login', authenticate, loginLockMiddleware, async c => {
|
||||
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 => {
|
||||
return statusResponse(c, HTTPStatus.OK); // stub
|
||||
route.app.post('/login', authenticate, loginLockMiddleware, c => {
|
||||
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 => {
|
||||
return statusResponse(c, HTTPStatus.OK); // stub
|
||||
const vrMovementModeBodySchema = z.object({
|
||||
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());
|
||||
});
|
||||
@@ -1,2 +1,20 @@
|
||||
import type Profile from "../profiles/profile.ts";
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
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;
|
||||
@@ -34,6 +34,10 @@ export default class Command {
|
||||
return this.exec(...args);
|
||||
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));
|
||||
if (cmd) {
|
||||
const newArgs = args.slice(1);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { CommandSender, CommandSenderType } from "./cmdtypes.ts";
|
||||
import type Command from "./command.ts";
|
||||
|
||||
export class CommandsBase extends ServerContentBase {
|
||||
@@ -18,12 +19,20 @@ export class CommandsBase extends ServerContentBase {
|
||||
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];
|
||||
if (typeof root !== 'string') return new Error("Root command must be of primitive type 'string'");
|
||||
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));
|
||||
if (cmd) {
|
||||
|
||||
const val = await cmd.dispatch(...args.slice(1));
|
||||
if (val == null) return "null";
|
||||
else if (typeof val == 'string') return `"${val}"`;
|
||||
|
||||
29
src/server/consumables/base.ts
Normal file
29
src/server/consumables/base.ts
Normal 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
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
19
src/server/consumables/types.ts
Normal file
19
src/server/consumables/types.ts
Normal 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
|
||||
}
|
||||
@@ -1,45 +1,104 @@
|
||||
import { CloudRegionCode } from "../../util/photon.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";
|
||||
|
||||
export interface InstanceCreationOptions {
|
||||
roomId: number,
|
||||
subRoomId: number,
|
||||
room: RoomFactory,
|
||||
subroom: SubroomFactory,
|
||||
name: string,
|
||||
maxCapacity: number,
|
||||
private?: boolean
|
||||
private?: boolean,
|
||||
eventId?: number
|
||||
}
|
||||
|
||||
export class Instance {
|
||||
|
||||
#createdAt = new Date();
|
||||
|
||||
#players: Set<Profile> = new Set();
|
||||
#server: ServerBase;
|
||||
#init: boolean = true;
|
||||
|
||||
#instanceId: number;
|
||||
#roomId: number;
|
||||
#subRoomId: number;
|
||||
#location: RoomLocation;
|
||||
#name: string;
|
||||
#maxCapacity: number;
|
||||
#roomId: number = -1;
|
||||
#subRoomId: number = -1;
|
||||
#location: RoomLocation = RoomLocation.MakerRoom;
|
||||
#name: string = "Uninitialized Instance";
|
||||
#maxCapacity: number = 8;
|
||||
#isFull: boolean = false;
|
||||
#isPrivate: boolean;
|
||||
#isPrivate: boolean = false;
|
||||
#isInProgress: boolean = false;
|
||||
#photonRegionId: string = CloudRegionCode.us;
|
||||
#photonRoomId: string;
|
||||
#photonRoomId: string = "uninit";
|
||||
#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.#location = location;
|
||||
this.location = options.subroom.RoomSceneLocationId;
|
||||
this.#server = server;
|
||||
|
||||
this.#roomId = options.roomId;
|
||||
this.#subRoomId = options.subRoomId;
|
||||
this.#isPrivate = typeof options.private == 'boolean' ? options.private : false;
|
||||
this.#name = options.name;
|
||||
this.#maxCapacity = options.maxCapacity;
|
||||
this.#photonRoomId = `GCR-${this.#instanceId}`;
|
||||
this.roomId = options.room.getRoomId();
|
||||
this.subRoomId = options.subroom.RoomSceneId;
|
||||
this.isPrivate = typeof options.private == 'boolean' ? options.private : false;
|
||||
this.name = options.name;
|
||||
this.maxCapacity = options.subroom.MaxPlayers;
|
||||
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() {
|
||||
@@ -61,21 +120,26 @@ export class Instance {
|
||||
|
||||
export() {
|
||||
const inst: RoomInstance = {
|
||||
roomInstanceId: this.#instanceId,
|
||||
roomId: this.#roomId,
|
||||
subRoomId: this.#subRoomId,
|
||||
location: this.#location,
|
||||
name: this.#name,
|
||||
maxCapacity: this.#maxCapacity,
|
||||
isFull: this.#isFull,
|
||||
isPrivate: this.#isPrivate,
|
||||
isInProgress: this.#isInProgress,
|
||||
photonRegionId: this.#photonRegionId,
|
||||
photonRoomId: this.#photonRoomId,
|
||||
dataBlob: this.#dataBlob,
|
||||
eventId: this.#eventId
|
||||
roomInstanceId: this.instanceId,
|
||||
roomId: this.roomId,
|
||||
subRoomId: this.subRoomId,
|
||||
location: this.location,
|
||||
name: this.name,
|
||||
maxCapacity: this.maxCapacity,
|
||||
isFull: this.isFull,
|
||||
isPrivate: this.isPrivate,
|
||||
isInProgress: this.isInProgress,
|
||||
photonRegionId: this.photonRegionId,
|
||||
photonRoomId: this.photonRoomId,
|
||||
dataBlob: this.dataBlob,
|
||||
eventId: this.eventId
|
||||
};
|
||||
return inst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current player count (instance size)
|
||||
*/
|
||||
get size() { return this.getPlayers().length }
|
||||
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
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");
|
||||
|
||||
@@ -8,40 +11,80 @@ export class InstanceManager extends ServerContentBase {
|
||||
|
||||
#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: ${
|
||||
this.#instances.size
|
||||
} instances, ${
|
||||
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
||||
} players`);
|
||||
|
||||
return new Promise(() => {
|
||||
for (const inst of this.#instances) {
|
||||
if (inst.getPlayers().length === 0) this.deleteInstance(inst);
|
||||
}
|
||||
for (const inst of this.#instances)
|
||||
if (inst.getPlayers().length === 0) this.deleteInstance(inst);
|
||||
|
||||
log.i(`Instance purge complete\n After: ${
|
||||
this.#instances.size
|
||||
} instances, ${
|
||||
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
||||
} players`);
|
||||
});
|
||||
|
||||
log.i(`Instance purge complete\n After: ${
|
||||
this.#instances.size
|
||||
} instances, ${
|
||||
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
||||
} players`);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
return this.#instances.values().find(inst => inst.instanceId === id);
|
||||
}
|
||||
|
||||
deleteInstance(inst: Instance) {
|
||||
|
||||
this.#instances.delete(inst);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type Instance } from "./Instance.ts";
|
||||
|
||||
export enum RoomLocation {
|
||||
Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04",
|
||||
DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
|
||||
@@ -53,3 +55,7 @@ export interface RoomInstance {
|
||||
dataBlob?: string;
|
||||
eventId?: number;
|
||||
}
|
||||
|
||||
export interface InstanceUpdatedEvent {
|
||||
instance: Instance
|
||||
}
|
||||
@@ -1,7 +1,121 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
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 {
|
||||
|
||||
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 };
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
enum MatchmakingErrorCode {
|
||||
export enum MatchmakingErrorCode {
|
||||
Success,
|
||||
NoSuchGame,
|
||||
PlayerNotOnline,
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import z from "zod";
|
||||
import Command from "../commands/command.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 { 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({
|
||||
Ticket: z.string().min(256),
|
||||
AppId: z.literal("471710")
|
||||
});
|
||||
|
||||
const netConfig = getNetConfig();
|
||||
|
||||
export class PlatformsManager extends ServerContentBase {
|
||||
|
||||
static platformsKey = "platforms";
|
||||
@@ -18,15 +22,18 @@ export class PlatformsManager extends ServerContentBase {
|
||||
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');
|
||||
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 = {
|
||||
typ: type,
|
||||
sub: accountId,
|
||||
iss: "https://yarns.proxnet.dev/auth/",
|
||||
exp: type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952
|
||||
sub: prof.getId(),
|
||||
role: await prof.getRole(),
|
||||
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);
|
||||
}
|
||||
@@ -138,9 +145,9 @@ export class PlatformsManager extends ServerContentBase {
|
||||
return await this.addCachedLogin(type, platformId, accountId);
|
||||
},
|
||||
zod: z.tuple([
|
||||
z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
|
||||
z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
|
||||
z.string(),
|
||||
z.string().transform(Number)
|
||||
z.coerce.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);
|
||||
},
|
||||
zod: z.tuple([
|
||||
z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
|
||||
z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
|
||||
z.string(),
|
||||
z.string().transform(Number)
|
||||
z.coerce.number()
|
||||
]),
|
||||
help: 'Remove a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>'
|
||||
})
|
||||
|
||||
@@ -8,14 +8,13 @@ export interface TokenFormatBase {
|
||||
export interface TokenFormat extends TokenFormatBase {
|
||||
iss: string,
|
||||
exp: number,
|
||||
iat: number,
|
||||
sub: number,
|
||||
role: ProfileRole
|
||||
}
|
||||
|
||||
export enum ProfileRole {
|
||||
Developer = 'developer',
|
||||
Web = 'webClient',
|
||||
Game = 'gameClient'
|
||||
}
|
||||
export const ProfileRole = ["developer", "gameClient", "webClient"] as const;
|
||||
export type ProfileRole = typeof ProfileRole[number];
|
||||
|
||||
export enum PlatformType {
|
||||
All = -1,
|
||||
|
||||
@@ -4,6 +4,9 @@ import { DeviceClass } from "../platforms/types.ts";
|
||||
import Profile from "../profiles/profile.ts";
|
||||
import { type ServerBase } from "../server.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 {
|
||||
TELEPORT,
|
||||
@@ -33,7 +36,6 @@ export class Presence {
|
||||
|
||||
#statusVisibility: PlayerStatusVisibility = PlayerStatusVisibility.Offline;
|
||||
#deviceClass: DeviceClass = DeviceClass.Unknown;
|
||||
#roomInstance: RoomInstance | null = null;
|
||||
#vrMovementMove: VRMovementMode | undefined;
|
||||
|
||||
#lastExported: Date = new Date();
|
||||
@@ -63,12 +65,16 @@ export class Presence {
|
||||
this.#statusVisibility = sv;
|
||||
this.updateLastSeen();
|
||||
this.update();
|
||||
|
||||
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
|
||||
}
|
||||
|
||||
setVRMovementMode(mm: VRMovementMode) {
|
||||
this.#vrMovementMove = mm;
|
||||
this.updateLastSeen();
|
||||
this.update();
|
||||
|
||||
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
|
||||
}
|
||||
|
||||
getLastExported() {
|
||||
@@ -87,14 +93,14 @@ export class Presence {
|
||||
statusVisibility: this.#statusVisibility,
|
||||
deviceClass: this.#deviceClass,
|
||||
vrMovementMode: this.#vrMovementMove,
|
||||
roomInstance: this.#roomInstance
|
||||
roomInstance: this.#profile.getInstance()?.export() ?? null
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PresenceBase extends ServerContentBase {
|
||||
export class ServerPresenceBase extends ServerContentBase {
|
||||
|
||||
#log = new Logging("Presence");
|
||||
|
||||
@@ -119,9 +125,32 @@ export class PresenceBase extends ServerContentBase {
|
||||
|
||||
override start() {
|
||||
this.#intervalId = setInterval(() => {
|
||||
if (this.#presenceMap.size === 0) return;
|
||||
|
||||
this.#log.i('Clearing dead presences');
|
||||
this.#deleteDeadPresences();
|
||||
}, 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() {
|
||||
|
||||
@@ -4,7 +4,7 @@ import Profile from "./profile.ts";
|
||||
import { SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
||||
import Command from "./../commands/command.ts";
|
||||
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";
|
||||
|
||||
const profiles: Map<number, Profile> = new Map();
|
||||
@@ -116,7 +116,7 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
else return prof.export();
|
||||
},
|
||||
zod: z.tuple([
|
||||
z.string().transform(Number)
|
||||
z.coerce.number()
|
||||
]),
|
||||
help: 'Fetch a profile: <id: number>'
|
||||
}),
|
||||
@@ -137,9 +137,9 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
else return await profile.getRole();
|
||||
},
|
||||
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({
|
||||
key: ['setrole', 'sr'],
|
||||
@@ -150,9 +150,9 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
},
|
||||
zod: z.tuple([
|
||||
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({
|
||||
key: ['settings', 'setting'],
|
||||
@@ -163,6 +163,16 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
},
|
||||
zod: z.tuple([z.coerce.number()]),
|
||||
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"
|
||||
})
|
||||
]
|
||||
}));
|
||||
|
||||
@@ -59,15 +59,15 @@ class Profile {
|
||||
}
|
||||
|
||||
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() {
|
||||
return this.#selfAcc.username;
|
||||
}
|
||||
async setUsername(username: string) {
|
||||
this.#kv.getKv().delete([ ProfileManagerBase.profilesKey, this.#selfAcc.username ]);
|
||||
this.#kv.getKv().set([ ProfileManagerBase.profilesKey, username ], this.getId());
|
||||
this.#kv.getKv().delete([ProfileManagerBase.profilesKey, this.#selfAcc.username]);
|
||||
this.#kv.getKv().set([ProfileManagerBase.profilesKey, username], this.getId());
|
||||
|
||||
this.#selfAcc.username = username;
|
||||
await this.#saveSelfAcc();
|
||||
@@ -81,7 +81,7 @@ class Profile {
|
||||
await this.#saveSelfAcc();
|
||||
}
|
||||
|
||||
async getBio(){
|
||||
async getBio() {
|
||||
const key = this.constructProfilePropertyKey('bio');
|
||||
const val = await this.#kv.getKv().get<string>(key);
|
||||
if (!val.value) return null;
|
||||
@@ -136,7 +136,17 @@ class Profile {
|
||||
getInstance() {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,13 @@ import { AGRoom, AGRoomLocation, AGRoomRuntimeConfig } from "./internal/ClientRo
|
||||
import Command from "../commands/command.ts";
|
||||
import z from "zod";
|
||||
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 {
|
||||
|
||||
#subroomKv = new KV('subrooms', true);
|
||||
@@ -21,19 +26,24 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
static roomNamesKey = "room_names";
|
||||
static playerDormsKey = "dorms";
|
||||
|
||||
#agrooms: Set<number> = new Set();
|
||||
#baserooms: Set<number> = new Set();
|
||||
#agroomIds: Set<number> = new Set();
|
||||
#baseroomIds: Set<number> = new Set();
|
||||
#agRoomRuntimeConfig: AGRoomRuntimeConfig | null = null;
|
||||
#joinInProgressLookup: Record<RoomLocation, boolean> | null = null;
|
||||
|
||||
override async start() {
|
||||
await this.#subroomKv.init();
|
||||
this.#log.i('[sub]rooms database initialized');
|
||||
|
||||
const agrooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.agRoomIdsKey]);
|
||||
if (agrooms.value !== null) this.#agrooms = agrooms.value;
|
||||
this.#log.i(`${this.#agrooms.size} AG rooms exist`);
|
||||
if (agrooms.value !== null) this.#agroomIds = agrooms.value;
|
||||
this.#log.i(`${this.#agroomIds.size} AG rooms exist`);
|
||||
|
||||
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({
|
||||
key: ["rooms", "r", "room"],
|
||||
@@ -82,10 +92,17 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
}))
|
||||
}
|
||||
async #writeAgRooms() {
|
||||
await this.kv.getKv().set([ServerRoomsBase.agRoomIdsKey], this.#agrooms);
|
||||
await this.kv.getKv().set([ServerRoomsBase.agRoomIdsKey], this.#agroomIds);
|
||||
}
|
||||
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[]) {
|
||||
@@ -98,7 +115,10 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
"ARRoom",
|
||||
"Registration",
|
||||
"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();
|
||||
if (roomFactory == null) {
|
||||
@@ -118,7 +138,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
roomFactory.Description = room.Description;
|
||||
roomFactory.IsAGRoom = true;
|
||||
roomFactory.CloningAllowed = room.CloningAllowed;
|
||||
roomFactory.ImageName = `${room.Name}.png`
|
||||
roomFactory.ImageName = `${room.Name}.png`;
|
||||
|
||||
const supportPromises: Promise<unknown>[] = [];
|
||||
roomFactory.removeAllHardwareSupport();
|
||||
@@ -134,7 +154,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
|
||||
subroomFactory.RoomId = roomFactory.getRoomId();
|
||||
subroomFactory.Name = scene.Name;
|
||||
subroomFactory.RoomSceneLocationId = scene.RoomSceneLocationId;
|
||||
subroomFactory.RoomSceneLocationId = scene.RoomSceneLocationId as RoomLocation;
|
||||
subroomFactory.IsSandbox = scene.IsSandbox;
|
||||
subroomFactory.CanMatchmakeInto = scene.CanMatchmakeInto;
|
||||
subroomFactory.MaxPlayers = scene.MaxPlayers;
|
||||
@@ -146,15 +166,33 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
|
||||
await Promise.all(supportPromises);
|
||||
|
||||
this.#agrooms.add(roomFactory.getRoomId());
|
||||
this.#agroomIds.add(roomFactory.getRoomId());
|
||||
await roomFactory.write();
|
||||
|
||||
if (room.CloningAllowed) this.#baserooms.add(roomFactory.getRoomId());
|
||||
if (room.CloningAllowed) this.#baseroomIds.add(roomFactory.getRoomId());
|
||||
}));
|
||||
|
||||
await this.#writeAgRooms();
|
||||
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() {
|
||||
@@ -163,6 +201,10 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
return id;
|
||||
}
|
||||
|
||||
async getByRoomSceneId(id: number) {
|
||||
return await new SubroomFactory(this.server, this.#subroomKv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
||||
}
|
||||
|
||||
getKv() {
|
||||
return this.kv;
|
||||
}
|
||||
@@ -176,7 +218,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
async getPlayerDorm(profile: Profile) {
|
||||
const id = await this.kv.getKv().get<number>([ServerRoomsBase.playerDormsKey, profile.getId()]);
|
||||
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;
|
||||
roomFactory.setRoomProperties({
|
||||
Name: `DormRoom`,
|
||||
@@ -216,7 +258,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
|
||||
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) {
|
||||
@@ -226,11 +268,11 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
}
|
||||
|
||||
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) {
|
||||
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() });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { RoomLocation } from "../../instances/types.ts";
|
||||
|
||||
export enum WriteMode {
|
||||
Overwrite = "overwrite",
|
||||
WriteIfFree = "if_free"
|
||||
@@ -34,7 +36,7 @@ export enum RoomAccessibility {
|
||||
export interface RoomScene {
|
||||
RoomSceneId: number,
|
||||
RoomId: number,
|
||||
RoomSceneLocationId: string,
|
||||
RoomSceneLocationId: RoomLocation,
|
||||
Name: string,
|
||||
IsSandbox: boolean,
|
||||
DataBlobName: string,
|
||||
@@ -151,7 +153,7 @@ export interface DatabaseRoom {
|
||||
|
||||
export interface SubroomProps {
|
||||
RoomId: number,
|
||||
RoomSceneLocationId: string,
|
||||
RoomSceneLocationId: RoomLocation,
|
||||
Name: string,
|
||||
IsSandbox: boolean,
|
||||
MaxPlayers: number,
|
||||
|
||||
@@ -29,6 +29,7 @@ export class RoomFactory {
|
||||
|
||||
#server: ServerBase;
|
||||
#kv: KV;
|
||||
#subroomKv: KV;
|
||||
|
||||
#roomId: number | undefined;
|
||||
|
||||
@@ -43,10 +44,11 @@ export class RoomFactory {
|
||||
#cannotAccessBeforeInitError = new Error("Cannot access properties 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.#kv = kv;
|
||||
this.#subroomKv = subroomKv;
|
||||
|
||||
}
|
||||
|
||||
@@ -161,9 +163,7 @@ export class RoomFactory {
|
||||
const autoTags = this.getTags();
|
||||
const galvTags = this.getGalvanicTags();
|
||||
|
||||
const subroomExports = (await Promise.all(
|
||||
this.getSubrooms().values().map(subroom => this.getSubroom(subroom))
|
||||
)).map(factory => factory.export());
|
||||
const subroomExports = (await Promise.all((await this.getAllSubrooms()).values())).map(factory => factory.export());
|
||||
|
||||
return {
|
||||
Room: {
|
||||
@@ -208,22 +208,27 @@ export class RoomFactory {
|
||||
return this.#roomId;
|
||||
}
|
||||
|
||||
getSubrooms() {
|
||||
getSubroomIds() {
|
||||
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||
return this.#subrooms;
|
||||
}
|
||||
async getSubroom(id: number) {
|
||||
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||
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));
|
||||
if (this.getSubrooms().has(id)) id = this.getAvailableSubroomId();
|
||||
if ((await this.#subroomKv.getKv().get<unknown>([id])).value) id = await this.getAvailableSubroomId();
|
||||
return id;
|
||||
}
|
||||
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) {
|
||||
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||
|
||||
@@ -2,6 +2,7 @@ import Logging from "@proxnet/undead-logging";
|
||||
import type KV from "../../persistence/kv.ts";
|
||||
import { type ServerBase } from "../../server.ts";
|
||||
import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, RoomSaveMap, WriteMode } from "./RoomDataTypes.ts";
|
||||
import { RoomLocation } from "../../instances/types.ts";
|
||||
|
||||
export interface SubroomFactoryOptions {
|
||||
mode: FactoryMode,
|
||||
@@ -44,7 +45,7 @@ export class SubroomFactory {
|
||||
|
||||
this.#obj = options.mode == FactoryMode.Fetch ? data.value : {
|
||||
RoomId: 0,
|
||||
RoomSceneLocationId: "",
|
||||
RoomSceneLocationId: RoomLocation.MakerRoom,
|
||||
Name: "Subroom data init failed, contact an admin!",
|
||||
IsSandbox: false,
|
||||
MaxPlayers: 8,
|
||||
@@ -92,7 +93,7 @@ export class SubroomFactory {
|
||||
|
||||
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 }
|
||||
|
||||
get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Name; }
|
||||
|
||||
@@ -3,11 +3,14 @@ import { ServerUpdateEvent } from "../serverevents.ts";
|
||||
import { AvatarContentBase } from "./avatars/base.ts";
|
||||
import { EventManager } from "./baseevent.ts";
|
||||
import { CommandsBase } from "./commands/commands.ts";
|
||||
import { ServerConsumablesBase } from "./consumables/base.ts";
|
||||
import { ServerContentManager } from "./content/base.ts";
|
||||
import GameConfigsBase from "./gameconfigs/base.ts";
|
||||
import { InstanceManager } from "./instances/base.ts";
|
||||
import { ServerMatchmakingBase } from "./matchmaking/base.ts";
|
||||
import { Objective, ObjectiveType } from "./objectives/base.ts";
|
||||
import { PlatformsManager } from "./platforms/base.ts";
|
||||
import { ServerPresenceBase } from "./presence/base.ts";
|
||||
import { type PresenceUpdateEvent } from "./presence/events/PresenceUpdateEvent.ts";
|
||||
import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.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 { ServerRoomsBase } from "./rooms/base.ts";
|
||||
import { RoomUpdatedEvent, SubroomUpdatedEvent } from "./rooms/internal/RoomEvents.ts";
|
||||
import { AnnouncementDTO } from "./types.ts";
|
||||
|
||||
interface ServerEvents {
|
||||
'profile.roleupdate': RoleUpdateEvent,
|
||||
@@ -42,7 +46,7 @@ interface AutoMicMutingConfig {
|
||||
MicSpamWarningStateVolumeMultiplier: number;
|
||||
};
|
||||
|
||||
export type PublicConfig = {
|
||||
export interface PublicConfig {
|
||||
ShareBaseUrl: string;
|
||||
ServerMaintenance: {
|
||||
StartsInMinutes: number;
|
||||
@@ -61,6 +65,9 @@ class ServerBase extends EventManager<ServerEvents> {
|
||||
Instances = new InstanceManager(this, 'instances');
|
||||
Content = new ServerContentManager(this, "content");
|
||||
Rooms = new ServerRoomsBase(this, 'rooms', true);
|
||||
Matchmaking = new ServerMatchmakingBase(this, "match");
|
||||
Presence = new ServerPresenceBase(this, "pres");
|
||||
Consumables = new ServerConsumablesBase(this, "consumables");
|
||||
|
||||
generateMask(...num: number[]) {
|
||||
return num.reduce((sum, val) => sum + val, 0);
|
||||
@@ -137,6 +144,11 @@ class ServerBase extends EventManager<ServerEvents> {
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
getAnnouncements(): AnnouncementDTO[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Server = new ServerBase();
|
||||
|
||||
@@ -4,7 +4,8 @@ import { type ConsoleItem, ConsoleItemSchema } from "./zod.ts";
|
||||
import Server from "../../server.ts";
|
||||
import { getSourceAddress } from "../../../util/net.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 {
|
||||
|
||||
@@ -55,7 +56,7 @@ export default class SocketConsoleHandler {
|
||||
if (!zodParsed.success) this.destroy();
|
||||
|
||||
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;
|
||||
|
||||
this.send(ConsoleEvent.Message, chalk.gray(`> ${chalk.yellow(data)}`));
|
||||
|
||||
@@ -18,6 +18,8 @@ import { SocketTarget } from "./targets/targetbase.ts";
|
||||
import type Profile from "../../profiles/profile.ts";
|
||||
import { detailedLog } from "../../../main.ts";
|
||||
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
||||
import Server from "../../server.ts";
|
||||
import { PresenceUpdateEvent } from "../../presence/events/PresenceUpdateEvent.ts";
|
||||
|
||||
const logmessages = true;
|
||||
|
||||
@@ -32,6 +34,10 @@ export class SignalRSocketHandler {
|
||||
|
||||
#killed = false;
|
||||
|
||||
#presCb = (ev: PresenceUpdateEvent) => {
|
||||
if (ev.profile == this.#profile) this.sendNotification("PresenceUpdate", ev.presence.export());
|
||||
}
|
||||
|
||||
constructor(socket: WebSocket, player: Profile) {
|
||||
|
||||
this.#socket = socket;
|
||||
@@ -69,6 +75,8 @@ export class SignalRSocketHandler {
|
||||
}
|
||||
|
||||
async #onMessage(message: Message) {
|
||||
this.#profile.Matchmaking.updateLastSeen();
|
||||
|
||||
if (message.kind == MessageKind.Protocol) {
|
||||
this.sendRaw({});
|
||||
return;
|
||||
@@ -130,6 +138,8 @@ export class SignalRSocketHandler {
|
||||
}
|
||||
});
|
||||
|
||||
Server.on('presence.update', this.#presCb);
|
||||
|
||||
this.#socket.addEventListener('close', this.destroy(this, true));
|
||||
}
|
||||
|
||||
@@ -137,6 +147,8 @@ export class SignalRSocketHandler {
|
||||
return (ev: CloseEvent) => {
|
||||
handler.#killed = true;
|
||||
|
||||
Server.off('presence.update', this.#presCb);
|
||||
|
||||
let errorReason = "Socket closed by server";
|
||||
this.#log.d(`Socket close code: ${ev.code}`);
|
||||
if (ev.reason.includes('Bye!')) errorReason = "Socket closed by client request";
|
||||
|
||||
@@ -6,7 +6,7 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
||||
#ids: number[] = [];
|
||||
|
||||
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[]) {
|
||||
|
||||
29
src/server/types.ts
Normal file
29
src/server/types.ts
Normal 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,
|
||||
}
|
||||
@@ -135,3 +135,41 @@ export class RateLimiter {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
export async function importer<T>(importKey: string, prefix: string, paths: string[]): Promise<T[]> {
|
||||
const log = new Logging(`Importer:'${importKey}'-${prefix}`);
|
||||
export async function importer<T>(importKey: string, p: string, paths: string[]): Promise<T[]> {
|
||||
const log = new Logging(`Importer:'${importKey}':${p}`);
|
||||
const items: T[] = [];
|
||||
|
||||
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}`);
|
||||
|
||||
for await (const localPath of Deno.readDir(importPath)) {
|
||||
if (localPath.isDirectory) continue;
|
||||
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}`);
|
||||
await import(fullPath).then(val => {
|
||||
await import(`file://${fullPath}`).then(val => {
|
||||
|
||||
if (val[importKey]) items.push(val[importKey]);
|
||||
else log.w(`Import key '${importKey}' not found on: '${fullPath}'`);
|
||||
|
||||
@@ -43,9 +43,25 @@ export const transformStringToEnum = <T>(anEnum: { [s: string]: string | number
|
||||
|
||||
export const transformCheckEnum = <T>(anEnum: { [s: string]: string | number }) => {
|
||||
return (arg: number | string, ctx: z.RefinementCtx<number | string>) => {
|
||||
if (typeof anEnum[arg] == 'undefined') {
|
||||
ctx.addIssue("Not an enum member");
|
||||
function invalid() {
|
||||
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;
|
||||
} else return anEnum[arg] as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user