The Rewrite™️
- Discord bot removed, will return *eventually*
- Watchdog kills the server with a knife when it does not shut down in (default) 60 seconds
- New event system that works.. better.. and callbacks have types
- Removed a metric ton of circular dependencies that previously would not let the server start up
* This included splitting up some classes
- Other. internal stuff. I forgot.
This commit is contained in:
7
.madgerc
Normal file
7
.madgerc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"detectiveOptions": {
|
||||||
|
"ts": {
|
||||||
|
"skipTypeImports": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
CONFIG.md
28
CONFIG.md
@@ -3,7 +3,7 @@
|
|||||||
[<-- Click to return to README.md](./README.md)
|
[<-- Click to return to README.md](./README.md)
|
||||||
|
|
||||||
We recommend that you store the configuration file `config.json` in a safe place where Galvanic Corrosion can access it (the current directory).<br>
|
We recommend that you store the configuration file `config.json` in a safe place where Galvanic Corrosion can access it (the current directory).<br>
|
||||||
No other user on your server system should be able to access the file.
|
No other user on your host system should be able to access the configuration.
|
||||||
|
|
||||||
## Redis
|
## Redis
|
||||||
Redis is database software and must be installed for Galvanic Corrosion.
|
Redis is database software and must be installed for Galvanic Corrosion.
|
||||||
@@ -19,26 +19,30 @@ If you are unsure of what this does, leave it unchanged.
|
|||||||
|
|
||||||
## Network
|
## Network
|
||||||
|
|
||||||
Galvanic Corrosion listens on two ports:
|
### Some issues may appear when connecting directly to a GC server's listening address.
|
||||||
* 13370/tcp(http) - for web endpoints
|
Sockets behave erratically when connected directly to clients. This is a suspected issue with Deno websockets.<br>
|
||||||
* 13371/tcp(http+ws) - for websockets
|
For now, it is recommended that you use a middleman/proxy with your server. (see below)
|
||||||
|
|
||||||
|
Galvanic Corrosion listens on two ports by default:
|
||||||
|
* 13370/tcp (http)
|
||||||
|
* 13371/tcp (http+ws)
|
||||||
|
|
||||||
Currently, HTTPS and WSS are unsupported *directly* on GC. You can use a compatible reverse proxy solution to secure your server.<br>
|
Currently, HTTPS and WSS are unsupported *directly* on GC. You can use a compatible reverse proxy solution to secure your server.<br>
|
||||||
[Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) (requires a domain with Cloudflare) are recommended.
|
[Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) (requires a domain with Cloudflare) are recommended.
|
||||||
|
|
||||||
Port-forward or expose your server in some way. HTTPS is **strongly** recommended for your public address.
|
Port-forward or expose your server in some way. HTTPS is **strongly** recommended for your public address.
|
||||||
|
|
||||||
Once your server is reachable, the nameserver (and similar functions) need to know what the public "official" address of your server is.<br>
|
Once your server is reachable locally, the nameserver (and similar functions) need to know what the public "official" address of your server is.<br>
|
||||||
For example, your server listens on 10.0.0.6:13370(+13371) but is tunneled to my-gc-server(-socket).coolguy.xyz:
|
For example, your server listens on 10.0.0.6:13370 and 10.0.0.6:13371, but is tunneled to my-gc-server.coolguy.xyz and my-gc-server-socket.coolguy.xyz:
|
||||||
- Set the "public host" for `web` and `socket` in `config.json` to the "official" address of your server
|
- Set the "public host" for `web` and `socket` in `config.json` to the "official" address of your server
|
||||||
* In the example, my-gc-server.coolguy.xyz and my-gc-server-socket.coolguy.xyz
|
* In the example, my-gc-server.coolguy.xyz and my-gc-server-socket.coolguy.xyz
|
||||||
* This includes port numbers, but not the protocol
|
* This includes port numbers, but not the protocol
|
||||||
- If your public address uses HTTPS (it should for proper authentication), enable `securepublichost`
|
- If your public address uses HTTPS (it should for proper authentication), enable `securepublichost`
|
||||||
|
|
||||||
You can test your configuration by navigating to `https://your-server.coolguy.xyz/ns`.<br>
|
You can test your configuration by navigating to `https://my-gc-server.coolguy.xyz/ns`. (use your server host)<br>
|
||||||
Each field should contain your server's public address with an optional path at the end.
|
Each field should contain your server's public address with an optional path at the end.
|
||||||
|
|
||||||
## Public Configuration
|
## Public
|
||||||
This section contains basic information regarding your server.
|
This section contains basic information regarding your server.
|
||||||
|
|
||||||
`serverName`: Somewhat invisible to players, but is an official label your server could appear as (to future server lists?)
|
`serverName`: Somewhat invisible to players, but is an official label your server could appear as (to future server lists?)
|
||||||
@@ -63,8 +67,12 @@ this can be anything *except* for "none" or 4, since there is only one server to
|
|||||||
`initialRoom`: On game startup, redirects the player to this room name instead of their DormRoom. Set to null if a "natural" startup is preferred.<br>
|
`initialRoom`: On game startup, redirects the player to this room name instead of their DormRoom. Set to null if a "natural" startup is preferred.<br>
|
||||||
This room must not be private and must be matchmakeable.
|
This room must not be private and must be matchmakeable.
|
||||||
|
|
||||||
|
## General
|
||||||
|
`watchdogTimeout`: Terminate the server process after this number of milliseconds when SIGINT is emitted.<br>
|
||||||
|
This can help when your server does not shut down gracefully.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
These three values expose booleans you can change to enable/disable logging various messages used for debugging or troubleshooting purposes.
|
These three booleans enable/disable logging various messages used for debugging or troubleshooting purposes.
|
||||||
|
|
||||||
## Discord
|
## Discord
|
||||||
Can be `null`. Currently unused.
|
Can be `null`. Currently unused.
|
||||||
@@ -77,6 +85,8 @@ Parameters used by the server's authentication mechanisms.
|
|||||||
`secret`: Used to generate tokens. Should never be shared (the entire file) and can be a string of characters containing no words or patterns.
|
`secret`: Used to generate tokens. Should never be shared (the entire file) and can be a string of characters containing no words or patterns.
|
||||||
<br>Use secure cryptography APIs in programming languages to generate random strings.
|
<br>Use secure cryptography APIs in programming languages to generate random strings.
|
||||||
|
|
||||||
|
`console`: Key used to connect to the server console. Must be different than your `auth.secret`.
|
||||||
|
|
||||||
`timeout`: The maximum age for a token.
|
`timeout`: The maximum age for a token.
|
||||||
|
|
||||||
`steamkey`: When not `null`, checks the Steam authentication ticket given by the client with the Steam User Auth API. Recommended for public servers.
|
`steamkey`: When not `null`, checks the Steam authentication ticket given by the client with the Steam User Auth API. Recommended for public servers.
|
||||||
|
|||||||
@@ -7,17 +7,17 @@
|
|||||||
"cross-compile": "deno run prebuild && deno run compile-win-a && deno run compile-linux-a && deno run postbuild",
|
"cross-compile": "deno run prebuild && deno run compile-win-a && deno run compile-linux-a && deno run postbuild",
|
||||||
"dev": "deno run -A src/main.ts --dev",
|
"dev": "deno run -A src/main.ts --dev",
|
||||||
"prebuild": "deno run -A ./prebuild.ts",
|
"prebuild": "deno run -A ./prebuild.ts",
|
||||||
"postbuild": "deno run -A ./postbuild.ts"
|
"postbuild": "deno run -A ./postbuild.ts",
|
||||||
|
"depcheck": "deno run -A npm:madge --circular --extensions ts ./src"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@gz/jwt": "jsr:@gz/jwt@^0.1.0",
|
"@gz/jwt": "jsr:@gz/jwt@^0.1.0",
|
||||||
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0",
|
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.3.0",
|
||||||
"@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8",
|
"@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8",
|
||||||
"@types/express": "npm:@types/express@^5.0.0",
|
"@types/express": "npm:@types/express@^5.0.0",
|
||||||
"@types/multer": "npm:@types/multer@^1.4.12",
|
"@types/multer": "npm:@types/multer@^1.4.12",
|
||||||
"@types/validator": "npm:@types/validator@^13.12.2",
|
"@types/validator": "npm:@types/validator@^13.12.2",
|
||||||
"cookie-parser": "npm:cookie-parser@^1.4.7",
|
"cookie-parser": "npm:cookie-parser@^1.4.7",
|
||||||
"discord.js": "npm:discord.js@^14.16.3",
|
|
||||||
"express": "npm:express@^4.21.2",
|
"express": "npm:express@^4.21.2",
|
||||||
"ioredis": "npm:ioredis@^5.5.0",
|
"ioredis": "npm:ioredis@^5.5.0",
|
||||||
"multer": "npm:multer@^1.4.5-lts.2",
|
"multer": "npm:multer@^1.4.5-lts.2",
|
||||||
@@ -31,5 +31,5 @@
|
|||||||
"./src/types/http.ts"
|
"./src/types/http.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "0.1.0"
|
"version": "0.2.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Deno.removeSync('./ver.ts');
|
await Deno.remove('./ver.ts');
|
||||||
Deno.renameSync('./ver.ts.bak', 'ver.ts');
|
await Deno.rename('./ver.ts.bak', 'ver.ts');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Cannot post-build version information: ${err}`);
|
console.error(`Cannot post-build version information: ${err}`);
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ try {
|
|||||||
const newVerString = `${file.version}-${new TextDecoder().decode(commitHash.stdout).trim()}`;
|
const newVerString = `${file.version}-${new TextDecoder().decode(commitHash.stdout).trim()}`;
|
||||||
|
|
||||||
if (file.version) {
|
if (file.version) {
|
||||||
Deno.writeTextFileSync('./ver.ts.bak', devVer);
|
await Deno.writeTextFile('./ver.ts.bak', devVer);
|
||||||
Deno.writeTextFileSync('./ver.ts', devVer.replace('development', newVerString));
|
await Deno.writeTextFile('./ver.ts', devVer.replace('development', newVerString));
|
||||||
console.info('Built version information');
|
console.info(`Built version information: Commit ${newVerString}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Cannot build version information: ${err}`);
|
console.error(`Cannot build version information: ${err}`);
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { decode } from "@gz/jwt";
|
import { decode } from "@gz/jwt";
|
||||||
import { Config } from "./config.ts";
|
import { Config } from "./config/config.ts";
|
||||||
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
import { User } from "./data/users.ts";
|
||||||
import { ProfileTokenFormat } from "./data/profiles.ts";
|
import { AuthType } from "./data/UserTypes.ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import Matchmaking from "./data/live/base.ts";
|
import Matchmaking from "./data/live/base.ts";
|
||||||
import Server from "./data/server.ts";
|
import Server from "./data/server/server.ts";
|
||||||
|
import { TokenSchema } from "./data/auth/TokenSchema.ts";
|
||||||
|
import type { TokenFormat } from "./data/auth/TokenBaseFormat.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
@@ -48,19 +50,6 @@ export function setCacheAllowed(_rq: express.Request, rs: express.Response, nxt:
|
|||||||
nxt();
|
nxt();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateRandomString(length: number) {
|
|
||||||
const characters =
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
let randomString = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
const randomIndex = Math.floor(Math.random() * characters.length);
|
|
||||||
randomString += characters.charAt(randomIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return randomString;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkQueryTypes<T>(typeDef: T) {
|
export function checkQueryTypes<T>(typeDef: T) {
|
||||||
return (
|
return (
|
||||||
rq: express.Request,
|
rq: express.Request,
|
||||||
@@ -251,31 +240,6 @@ export class RateLimiter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TokenBaseFormat {
|
|
||||||
typ: AuthType;
|
|
||||||
iss: string;
|
|
||||||
exp: number;
|
|
||||||
}
|
|
||||||
export type TokenFormat = UserTokenFormat | ProfileTokenFormat;
|
|
||||||
|
|
||||||
const TokenBaseSchema = z.object({
|
|
||||||
typ: z.nativeEnum(AuthType),
|
|
||||||
iss: z.string().url(),
|
|
||||||
exp: z.number()
|
|
||||||
});
|
|
||||||
export const UserTokenSchema = TokenBaseSchema.extend({
|
|
||||||
sub: z.string(),
|
|
||||||
typ: z.literal(AuthType.Web)
|
|
||||||
});
|
|
||||||
export const ProfileTokenSchema = TokenBaseSchema.extend({
|
|
||||||
sub: z.number(),
|
|
||||||
typ: z.literal(AuthType.Game)
|
|
||||||
});
|
|
||||||
export const TokenSchema = z.discriminatedUnion('typ', [
|
|
||||||
UserTokenSchema,
|
|
||||||
ProfileTokenSchema
|
|
||||||
]);
|
|
||||||
|
|
||||||
export async function Authentication(
|
export async function Authentication(
|
||||||
rq: express.Request,
|
rq: express.Request,
|
||||||
rs: express.Response,
|
rs: express.Response,
|
||||||
@@ -375,4 +339,12 @@ export function stopTimer(_rq: express.Request, rs: express.Response) {
|
|||||||
log.n(`(${rs.locals.reqId}) Middleware took ${(performance.now() - rs.locals.timer).toString().substring(0, 6)} ms`);
|
log.n(`(${rs.locals.reqId}) Middleware took ${(performance.now() - rs.locals.timer).toString().substring(0, 6)} ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function requestDebug(rq: express.Request, _rs: express.Response, nxt: express.NextFunction) {
|
||||||
|
log.d(`URL: ${rq.originalUrl}`);
|
||||||
|
log.d(`From IP: ${getSrcIpDefault(rq)}`);
|
||||||
|
log.d(`Headers: ${Object.keys(rq.headers).map(val => `\n ${val}: ${rq.headers[val]}`)}`);
|
||||||
|
|
||||||
|
nxt();
|
||||||
|
}
|
||||||
|
|
||||||
export * as APIUtils from "./apiutils.ts";
|
export * as APIUtils from "./apiutils.ts";
|
||||||
|
|||||||
73
src/config/GalvanicConfiguration.ts
Normal file
73
src/config/GalvanicConfiguration.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import type { PhotonRegionCodeNumber, PhotonRegionCodeString } from "../data/live/PhotonTypes.ts";
|
||||||
|
|
||||||
|
type RedisConfiguration = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
db: number;
|
||||||
|
};
|
||||||
|
type WebConfiguration = {
|
||||||
|
port: number;
|
||||||
|
host: string;
|
||||||
|
publichost: string;
|
||||||
|
securepublichost: boolean;
|
||||||
|
};
|
||||||
|
type WebRootConfiguration = {
|
||||||
|
api: WebConfiguration;
|
||||||
|
socket: WebConfiguration;
|
||||||
|
};
|
||||||
|
type PublicConfiguration = {
|
||||||
|
serverName: string;
|
||||||
|
serverId: string;
|
||||||
|
owner: string;
|
||||||
|
motd: string;
|
||||||
|
levelScale: number;
|
||||||
|
maxLevels: number;
|
||||||
|
patches: string[];
|
||||||
|
photonRegionId: PhotonRegionCodeNumber | PhotonRegionCodeString;
|
||||||
|
initialRoom: string | null;
|
||||||
|
};
|
||||||
|
type LoggingConfiguration = {
|
||||||
|
notfound: boolean;
|
||||||
|
debug: boolean;
|
||||||
|
network: boolean;
|
||||||
|
};
|
||||||
|
type AuthConfiguration = {
|
||||||
|
secret: string;
|
||||||
|
/**
|
||||||
|
* In Hours
|
||||||
|
*/
|
||||||
|
timeout: number;
|
||||||
|
steamkey: string | null;
|
||||||
|
};
|
||||||
|
type GeneralConfiguration = {
|
||||||
|
/** In milliseconds */
|
||||||
|
watchdogTimeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GalvanicConfiguration = {
|
||||||
|
redis: RedisConfiguration;
|
||||||
|
web: WebRootConfiguration;
|
||||||
|
public: PublicConfiguration;
|
||||||
|
general: GeneralConfiguration;
|
||||||
|
logging: LoggingConfiguration;
|
||||||
|
auth: AuthConfiguration;
|
||||||
|
};
|
||||||
@@ -17,72 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import { PhotonRegionCodeNumber, PhotonRegionCodeString } from "./data/live/types.ts";
|
import { GalvanicConfiguration } from "./GalvanicConfiguration.ts";
|
||||||
|
import { PhotonRegionCodeNumber } from "../data/live/PhotonTypes.ts";
|
||||||
|
|
||||||
const log = new Logging("Config");
|
const log = new Logging("Config");
|
||||||
|
|
||||||
type RedisConfiguration = {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
db: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type WebConfiguration = {
|
|
||||||
port: number;
|
|
||||||
host: string;
|
|
||||||
publichost: string;
|
|
||||||
securepublichost: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebRootConfiguration = {
|
|
||||||
api: WebConfiguration,
|
|
||||||
socket: WebConfiguration
|
|
||||||
};
|
|
||||||
|
|
||||||
type PublicConfiguration = {
|
|
||||||
serverName: string;
|
|
||||||
serverId: string;
|
|
||||||
owner: string;
|
|
||||||
motd: string;
|
|
||||||
levelScale: number;
|
|
||||||
maxLevels: number;
|
|
||||||
patches: string[];
|
|
||||||
photonRegionId: PhotonRegionCodeString | PhotonRegionCodeNumber;
|
|
||||||
initialRoom: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoggingConfiguration = {
|
|
||||||
notfound: boolean;
|
|
||||||
debug: boolean;
|
|
||||||
network: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DiscordConfiguration = {
|
|
||||||
token: string;
|
|
||||||
clientId: string;
|
|
||||||
guildId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AuthConfiguration = {
|
|
||||||
secret: string;
|
|
||||||
/**
|
|
||||||
* In Hours
|
|
||||||
*/
|
|
||||||
timeout: number;
|
|
||||||
steamkey: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GalvanicConfiguration = {
|
|
||||||
redis: RedisConfiguration;
|
|
||||||
web: WebRootConfiguration;
|
|
||||||
public: PublicConfiguration;
|
|
||||||
logging: LoggingConfiguration;
|
|
||||||
discord: DiscordConfiguration | null;
|
|
||||||
auth: AuthConfiguration;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultConfig: GalvanicConfiguration = {
|
export const defaultConfig: GalvanicConfiguration = {
|
||||||
redis: {
|
redis: {
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
@@ -116,12 +55,14 @@ export const defaultConfig: GalvanicConfiguration = {
|
|||||||
photonRegionId: PhotonRegionCodeNumber.us,
|
photonRegionId: PhotonRegionCodeNumber.us,
|
||||||
initialRoom: null
|
initialRoom: null
|
||||||
},
|
},
|
||||||
|
general: {
|
||||||
|
watchdogTimeout: 60000
|
||||||
|
},
|
||||||
logging: {
|
logging: {
|
||||||
notfound: false,
|
notfound: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
network: false,
|
network: false,
|
||||||
},
|
},
|
||||||
discord: null,
|
|
||||||
auth: {
|
auth: {
|
||||||
secret: "CHANGE-ME-PLEASE",
|
secret: "CHANGE-ME-PLEASE",
|
||||||
timeout: 3,
|
timeout: 3,
|
||||||
@@ -140,12 +81,12 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Does the configuration file exist on the disk? */
|
/** Does the configuration file exist on the disk? */
|
||||||
export function configurationExists() {
|
function configurationExists() {
|
||||||
return fs.existsSync("./config.json");
|
return fs.existsSync("./config.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Place [or overwrite] the [existing] default configuration in the current directory */
|
/** Place [or overwrite] the [existing] default configuration in the current directory */
|
||||||
export function generateDefaultConfig() {
|
function generateDefaultConfig() {
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
"./config.json",
|
"./config.json",
|
||||||
JSON.stringify(defaultConfig, undefined, " "),
|
JSON.stringify(defaultConfig, undefined, " "),
|
||||||
@@ -15,8 +15,12 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
export class ProfileEventsManager {
|
export type UserInitOptions = {
|
||||||
|
client_id: string;
|
||||||
|
pubkey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum AuthType {
|
||||||
|
Game,
|
||||||
|
Web
|
||||||
}
|
}
|
||||||
43
src/data/auth/TokenBaseFormat.ts
Normal file
43
src/data/auth/TokenBaseFormat.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import { AuthType } from "../UserTypes.ts";
|
||||||
|
|
||||||
|
export type TokenFormat = UserTokenFormat | ProfileTokenFormat;
|
||||||
|
|
||||||
|
export interface TokenBaseFormat {
|
||||||
|
typ: AuthType;
|
||||||
|
iss: string;
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenBaseFormat {
|
||||||
|
typ: AuthType;
|
||||||
|
iss: string;
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserTokenFormat extends TokenBaseFormat {
|
||||||
|
sub: string;
|
||||||
|
typ: AuthType.Web;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileTokenFormat extends TokenBaseFormat {
|
||||||
|
sub: number;
|
||||||
|
role: "developer" | "user";
|
||||||
|
typ: AuthType.Game;
|
||||||
|
}
|
||||||
37
src/data/auth/TokenSchema.ts
Normal file
37
src/data/auth/TokenSchema.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import z from "zod";
|
||||||
|
import { AuthType } from "../UserTypes.ts";
|
||||||
|
|
||||||
|
const TokenBaseSchema = z.object({
|
||||||
|
typ: z.nativeEnum(AuthType),
|
||||||
|
iss: z.string().url(),
|
||||||
|
exp: z.number()
|
||||||
|
});
|
||||||
|
export const UserTokenSchema = TokenBaseSchema.extend({
|
||||||
|
sub: z.string(),
|
||||||
|
typ: z.literal(AuthType.Web)
|
||||||
|
});
|
||||||
|
export const ProfileTokenSchema = TokenBaseSchema.extend({
|
||||||
|
sub: z.number(),
|
||||||
|
typ: z.literal(AuthType.Game)
|
||||||
|
});
|
||||||
|
export const TokenSchema = z.discriminatedUnion('typ', [
|
||||||
|
UserTokenSchema,
|
||||||
|
ProfileTokenSchema
|
||||||
|
]);
|
||||||
@@ -15,51 +15,26 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
type Callback<T> = (event: T) => void;
|
||||||
|
|
||||||
const log = new Logging("BaseEvent");
|
export class EventManager<Events extends { [K in keyof Events]: unknown }> {
|
||||||
|
#listeners: {
|
||||||
|
[K in keyof Events]?: Set<Callback<Events[K]>>
|
||||||
|
} = {};
|
||||||
|
|
||||||
export interface Event {
|
on<K extends keyof Events>(eventName: K, callback: Callback<Events[K]>): void {
|
||||||
time: Date
|
if (!this.#listeners[eventName])
|
||||||
}
|
this.#listeners[eventName] = new Set();
|
||||||
|
this.#listeners[eventName]!.add(callback);
|
||||||
export class EventManager {
|
|
||||||
|
|
||||||
private eventCallbacks: Map<string, Set<(ev: unknown) => void>> = new Map();
|
|
||||||
|
|
||||||
private getSubSet(event: string): Set<(ev: unknown) => void> {
|
|
||||||
let subset = this.eventCallbacks.get(event);
|
|
||||||
if (!subset) {
|
|
||||||
subset = new Set();
|
|
||||||
this.eventCallbacks.set(event, subset);
|
|
||||||
}
|
|
||||||
return subset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
on<T extends Event>(event: string, cb: (ev: T) => void) {
|
|
||||||
const typeSafeCallback = ((ev: unknown) => {
|
|
||||||
cb(ev as T);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.getSubSet(event).add(typeSafeCallback);
|
|
||||||
|
|
||||||
return typeSafeCallback;
|
off<K extends keyof Events>(eventName: K, callback: Callback<Events[K]>): void {
|
||||||
|
this.#listeners[eventName]?.delete(callback);
|
||||||
|
if (this.#listeners[eventName]?.size === 0)
|
||||||
|
delete this.#listeners[eventName];
|
||||||
}
|
}
|
||||||
|
|
||||||
off(event: string, cb: (ev: unknown) => void) {
|
emit<K extends keyof Events>(eventName: K, payload: Events[K]): void {
|
||||||
const subset = this.getSubSet(event);
|
this.#listeners[eventName]?.forEach((callback) => callback(payload));
|
||||||
subset.delete(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit<T extends Event>(event: string, ev: T) {
|
|
||||||
const subset = this.getSubSet(event);
|
|
||||||
for (const cb of subset.values()) {
|
|
||||||
try {
|
|
||||||
cb(ev);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error) log.e(`Error when executing callback: ${err.stack}`);
|
|
||||||
else log.e(`Error when executing callback: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
|
||||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
|
||||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|
||||||
|
|
||||||
import { Config } from "../config.ts";
|
|
||||||
import { Redis } from "../db.ts";
|
|
||||||
import { Objectives, ObjectiveType } from "./objectives.ts";
|
|
||||||
|
|
||||||
export type LevelProgressionItem = {
|
|
||||||
Level: number;
|
|
||||||
RequiredXp: number;
|
|
||||||
};
|
|
||||||
export type AutoMicMutingConfig = {
|
|
||||||
MicSpamVolumeThreshold: number,
|
|
||||||
MicVolumeSampleInterval: number,
|
|
||||||
MicVolumeSampleRollingWindowLength: number,
|
|
||||||
MicSpamSamplePercentageForWarning: number,
|
|
||||||
MicSpamSamplePercentageForWarningToEnd: number,
|
|
||||||
MicSpamSamplePercentageForForceMute: number,
|
|
||||||
MicSpamSamplePercentageForForceMuteToEnd: number,
|
|
||||||
MicSpamWarningStateVolumeMultiplier: number
|
|
||||||
}
|
|
||||||
export type PublicConfig = {
|
|
||||||
ShareBaseUrl: string
|
|
||||||
ServerMaintenance: {
|
|
||||||
StartsInMinutes: number;
|
|
||||||
};
|
|
||||||
LevelProgressionMaps: LevelProgressionItem[];
|
|
||||||
DailyObjectives: Objectives.Objective[][];
|
|
||||||
AutoMicMutingConfig: AutoMicMutingConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plain public config, NOT GameConfigs
|
|
||||||
*/
|
|
||||||
export function getConfig() {
|
|
||||||
const c = Config.getConfig();
|
|
||||||
if (typeof c == "undefined") return null;
|
|
||||||
const config = c as Config.GalvanicConfiguration;
|
|
||||||
|
|
||||||
function generateLevelProgressionMap() {
|
|
||||||
const m: LevelProgressionItem[] = [];
|
|
||||||
for (let i = 0; i < config.public.maxLevels + 1; i++) {
|
|
||||||
m.push({
|
|
||||||
Level: i,
|
|
||||||
RequiredXp: Math.round(i * config.public.levelScale * 20),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
const conf: PublicConfig = {
|
|
||||||
ServerMaintenance: {
|
|
||||||
StartsInMinutes: 0,
|
|
||||||
},
|
|
||||||
LevelProgressionMaps: generateLevelProgressionMap(),
|
|
||||||
DailyObjectives: [
|
|
||||||
[
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0},
|
|
||||||
{type: ObjectiveType.Default, score: 0}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
AutoMicMutingConfig: {
|
|
||||||
MicSpamVolumeThreshold: 1.125,
|
|
||||||
MicVolumeSampleInterval: 0.25,
|
|
||||||
MicVolumeSampleRollingWindowLength: 7.0,
|
|
||||||
MicSpamSamplePercentageForWarning: 0.8,
|
|
||||||
MicSpamSamplePercentageForWarningToEnd: 0.2,
|
|
||||||
MicSpamSamplePercentageForForceMute: 0.8,
|
|
||||||
MicSpamSamplePercentageForForceMuteToEnd: 0.2,
|
|
||||||
MicSpamWarningStateVolumeMultiplier: 0.25
|
|
||||||
},
|
|
||||||
ShareBaseUrl: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}/{0}` // {0} is replaced by the game
|
|
||||||
};
|
|
||||||
|
|
||||||
return conf;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllGameConfigs() {
|
|
||||||
try {
|
|
||||||
const gameConfigs = new Map<string, string>();
|
|
||||||
const val = await Redis.Database.hgetall(
|
|
||||||
Redis.buildKey(
|
|
||||||
Redis.KeyGroups.Config.Root,
|
|
||||||
Redis.KeyGroups.Config.Game,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const key of Object.keys(val)) {
|
|
||||||
gameConfigs.set(key, val[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return gameConfigs;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching game configs:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setGameConfig(key: string, value: string) {
|
|
||||||
return Redis.Database.hset(
|
|
||||||
Redis.buildKey(
|
|
||||||
Redis.KeyGroups.Config.Root,
|
|
||||||
Redis.KeyGroups.Config.Game,
|
|
||||||
),
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export function getGameConfig(key: string) {
|
|
||||||
return Redis.Database.hget(
|
|
||||||
Redis.buildKey(
|
|
||||||
Redis.KeyGroups.Config.Root,
|
|
||||||
Redis.KeyGroups.Config.Game,
|
|
||||||
),
|
|
||||||
key,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export * as GameConfigs from "./config.ts";
|
|
||||||
61
src/data/config/GameConfigs.ts
Normal file
61
src/data/config/GameConfigs.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import { Redis } from "../../db.ts";
|
||||||
|
|
||||||
|
export async function getAllGameConfigs() {
|
||||||
|
try {
|
||||||
|
const gameConfigs = new Map<string, string>();
|
||||||
|
const val = await Redis.Database.hgetall(
|
||||||
|
Redis.buildKey(
|
||||||
|
Redis.KeyGroups.Config.Root,
|
||||||
|
Redis.KeyGroups.Config.Game,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const key of Object.keys(val)) {
|
||||||
|
gameConfigs.set(key, val[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gameConfigs;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching game configs:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGameConfig(key: string, value: string) {
|
||||||
|
return Redis.Database.hset(
|
||||||
|
Redis.buildKey(
|
||||||
|
Redis.KeyGroups.Config.Root,
|
||||||
|
Redis.KeyGroups.Config.Game,
|
||||||
|
),
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function getGameConfig(key: string) {
|
||||||
|
return Redis.Database.hget(
|
||||||
|
Redis.buildKey(
|
||||||
|
Redis.KeyGroups.Config.Root,
|
||||||
|
Redis.KeyGroups.Config.Game,
|
||||||
|
),
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export * as GameConfigs from "./GameConfigs.ts";
|
||||||
95
src/data/config/PublicConfig.ts
Normal file
95
src/data/config/PublicConfig.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import { Config } from "../../config/config.ts";
|
||||||
|
import type { GalvanicConfiguration } from "../../config/GalvanicConfiguration.ts";
|
||||||
|
import { ObjectiveType } from "../content/ObjectiveTypes.ts";
|
||||||
|
import { PublicConfig, LevelProgressionItem } from "./PublicConfigTypes.ts";
|
||||||
|
|
||||||
|
export function getPublicConfig() {
|
||||||
|
const c = Config.getConfig();
|
||||||
|
if (typeof c == "undefined") return null;
|
||||||
|
const config = c as GalvanicConfiguration;
|
||||||
|
|
||||||
|
function generateLevelProgressionMap() {
|
||||||
|
const m: LevelProgressionItem[] = [];
|
||||||
|
for (let i = 0; i < config.public.maxLevels + 1; i++) {
|
||||||
|
m.push({
|
||||||
|
Level: i,
|
||||||
|
RequiredXp: Math.round(i * config.public.levelScale * 20),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conf: PublicConfig = {
|
||||||
|
ServerMaintenance: {
|
||||||
|
StartsInMinutes: 0,
|
||||||
|
},
|
||||||
|
LevelProgressionMaps: generateLevelProgressionMap(),
|
||||||
|
DailyObjectives: [
|
||||||
|
[
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 },
|
||||||
|
{ type: ObjectiveType.Default, score: 0 }
|
||||||
|
]
|
||||||
|
],
|
||||||
|
AutoMicMutingConfig: {
|
||||||
|
MicSpamVolumeThreshold: 1.125,
|
||||||
|
MicVolumeSampleInterval: 0.25,
|
||||||
|
MicVolumeSampleRollingWindowLength: 7.0,
|
||||||
|
MicSpamSamplePercentageForWarning: 0.8,
|
||||||
|
MicSpamSamplePercentageForWarningToEnd: 0.2,
|
||||||
|
MicSpamSamplePercentageForForceMute: 0.8,
|
||||||
|
MicSpamSamplePercentageForForceMuteToEnd: 0.2,
|
||||||
|
MicSpamWarningStateVolumeMultiplier: 0.25
|
||||||
|
},
|
||||||
|
ShareBaseUrl: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}/{0}` // {0} is replaced by the game
|
||||||
|
};
|
||||||
|
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
43
src/data/config/PublicConfigTypes.ts
Normal file
43
src/data/config/PublicConfigTypes.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import type { Objective } from "../content/ObjectiveTypes.ts";
|
||||||
|
|
||||||
|
export interface LevelProgressionItem {
|
||||||
|
Level: number;
|
||||||
|
RequiredXp: number;
|
||||||
|
};
|
||||||
|
interface AutoMicMutingConfig {
|
||||||
|
MicSpamVolumeThreshold: number;
|
||||||
|
MicVolumeSampleInterval: number;
|
||||||
|
MicVolumeSampleRollingWindowLength: number;
|
||||||
|
MicSpamSamplePercentageForWarning: number;
|
||||||
|
MicSpamSamplePercentageForWarningToEnd: number;
|
||||||
|
MicSpamSamplePercentageForForceMute: number;
|
||||||
|
MicSpamSamplePercentageForForceMuteToEnd: number;
|
||||||
|
MicSpamWarningStateVolumeMultiplier: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PublicConfig = {
|
||||||
|
ShareBaseUrl: string;
|
||||||
|
ServerMaintenance: {
|
||||||
|
StartsInMinutes: number;
|
||||||
|
};
|
||||||
|
LevelProgressionMaps: LevelProgressionItem[];
|
||||||
|
DailyObjectives: Objective[][];
|
||||||
|
AutoMicMutingConfig: AutoMicMutingConfig;
|
||||||
|
};
|
||||||
@@ -108,6 +108,4 @@ export enum ObjectiveType {
|
|||||||
export type Objective = {
|
export type Objective = {
|
||||||
type: ObjectiveType;
|
type: ObjectiveType;
|
||||||
score: number;
|
score: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
export * as Objectives from "./objectives.ts";
|
|
||||||
@@ -16,10 +16,10 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { generateRandomString } from "../../apiutils.ts";
|
import { generateRandomString } from "../../utils.ts";
|
||||||
import { Profile } from "../profiles.ts";
|
import type { Profile } from "../profile/base/profiles.ts";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import Server from "../server.ts";
|
import Server from "../server/server.ts";
|
||||||
|
|
||||||
const log = new Logging("CDN");
|
const log = new Logging("CDN");
|
||||||
|
|
||||||
@@ -68,7 +68,6 @@ export class CDNBase {
|
|||||||
pathParts.pop();
|
pathParts.pop();
|
||||||
|
|
||||||
const dirPath = pathParts.join('/');
|
const dirPath = pathParts.join('/');
|
||||||
log.d(dirPath);
|
|
||||||
if (dirPath) await Deno.mkdir(dirPath, { recursive: true });
|
if (dirPath) await Deno.mkdir(dirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +112,7 @@ export class CDNBase {
|
|||||||
const metaData = await Deno.readTextFile(`${path}.gcmeta`);
|
const metaData = await Deno.readTextFile(`${path}.gcmeta`);
|
||||||
|
|
||||||
const parsedMeta = JSON.parse(metaData);
|
const parsedMeta = JSON.parse(metaData);
|
||||||
|
|
||||||
const meta: MetaFile = {
|
const meta: MetaFile = {
|
||||||
creationPlayer: Server.UnifiedProfile.get(parsedMeta.creationPlayer) || undefined,
|
creationPlayer: Server.UnifiedProfile.get(parsedMeta.creationPlayer) || undefined,
|
||||||
dateCreated: new Date(parsedMeta.dateCreated),
|
dateCreated: new Date(parsedMeta.dateCreated),
|
||||||
|
|||||||
@@ -15,14 +15,13 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { EventManager } from "./baseevent.ts";
|
import { RoomFactory } from "./RoomFactory.ts";
|
||||||
import { CDNBase } from "./content/cdn.ts";
|
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||||
import { UnifiedProfileBase } from "./profiles.ts";
|
|
||||||
|
|
||||||
class ServerBase extends EventManager {
|
export interface SubroomUpdatedEvent {
|
||||||
CDN = new CDNBase();
|
subroom: SubroomFactory
|
||||||
UnifiedProfile = new UnifiedProfileBase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Server = new ServerBase();
|
export interface RoomUpdatedEvent {
|
||||||
export default Server;
|
room: RoomFactory
|
||||||
|
}
|
||||||
@@ -15,11 +15,16 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { Redis } from "../../../db.ts";
|
import { Redis } from "../../../db.ts";
|
||||||
import Rooms from "../rooms.ts";
|
import Server from "../../server/server.ts";
|
||||||
import { FactoryMode, HardwareSupport, HardwareSupportStrings, RoomAccessibility, RoomDataTypes, RoomDetails, RoomState, WriteMode } from "./DataTypes.ts";
|
import { RoomDataTypes } from "./base/DataTypes.ts";
|
||||||
import { SubroomFactory } from "./SubroomFactory.ts";
|
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||||
|
|
||||||
|
export const roomdebug = false;
|
||||||
|
|
||||||
|
const log = new Logging("RoomFactory");
|
||||||
|
|
||||||
interface RoomFactoryOptions {
|
interface RoomFactoryOptions {
|
||||||
id?: number;
|
id?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -29,6 +34,14 @@ interface RoomFactoryOptions {
|
|||||||
|
|
||||||
export class RoomFactory {
|
export class RoomFactory {
|
||||||
|
|
||||||
|
static async getIdFromName(name: string) {
|
||||||
|
const unparsedId = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Room_Names, name));
|
||||||
|
if (!unparsedId) return null;
|
||||||
|
const parsedId = parseInt(unparsedId);
|
||||||
|
if (isNaN(parsedId)) return null;
|
||||||
|
return parsedId;
|
||||||
|
}
|
||||||
|
|
||||||
static Keys = {
|
static Keys = {
|
||||||
Meta: "roommeta",
|
Meta: "roommeta",
|
||||||
Subrooms: "subrooms",
|
Subrooms: "subrooms",
|
||||||
@@ -68,7 +81,7 @@ export class RoomFactory {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
|
||||||
if (this.factoryMode !== FactoryMode.Fetch) {
|
if (this.factoryMode !== RoomDataTypes.FactoryMode.Fetch) {
|
||||||
if (!this.#specifiedId) throw this.#mustSpecifyIdInWriteModeError;
|
if (!this.#specifiedId) throw this.#mustSpecifyIdInWriteModeError;
|
||||||
this.#resolvedId = this.#specifiedId;
|
this.#resolvedId = this.#specifiedId;
|
||||||
return this;
|
return this;
|
||||||
@@ -76,7 +89,7 @@ export class RoomFactory {
|
|||||||
|
|
||||||
if (!this.#specifiedId) {
|
if (!this.#specifiedId) {
|
||||||
if (!this.#specifiedName) throw this.#mustSpecifyEitherIdOrNameError;
|
if (!this.#specifiedName) throw this.#mustSpecifyEitherIdOrNameError;
|
||||||
const id = await Rooms.getIdFromName(this.#specifiedName);
|
const id = await RoomFactory.getIdFromName(this.#specifiedName);
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
this.#specifiedId = id;
|
this.#specifiedId = id;
|
||||||
}
|
}
|
||||||
@@ -89,6 +102,8 @@ export class RoomFactory {
|
|||||||
RoomFactory.Keys.Meta
|
RoomFactory.Keys.Meta
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if (roomdebug) log.d(`Init success, specifiedId: ${this.#specifiedId}`);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -119,6 +134,8 @@ export class RoomFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.Name !== 'DormRoom') await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, this.Name), this.RoomId);
|
if (this.Name !== 'DormRoom') await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, this.Name), this.RoomId);
|
||||||
|
|
||||||
|
Server.emit('room.updated', { room: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
async export() {
|
async export() {
|
||||||
@@ -128,7 +145,7 @@ export class RoomFactory {
|
|||||||
const subroomPromises = subroomIds.map(id => this.getSubroom(id).init());
|
const subroomPromises = subroomIds.map(id => this.getSubroom(id).init());
|
||||||
const subrooms = (await Promise.all(subroomPromises)).map(subroom => subroom.export());
|
const subrooms = (await Promise.all(subroomPromises)).map(subroom => subroom.export());
|
||||||
|
|
||||||
const details: RoomDetails = {
|
const details: RoomDataTypes.RoomDetails = {
|
||||||
Room: {
|
Room: {
|
||||||
RoomId: this.RoomId,
|
RoomId: this.RoomId,
|
||||||
Name: this.Name,
|
Name: this.Name,
|
||||||
@@ -160,13 +177,13 @@ export class RoomFactory {
|
|||||||
Tags: []
|
Tags: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (roomdebug) log.d(`Exported details for room ${this.RoomId}`);
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubroom(id: number, factoryMode?: FactoryMode, writeMode?: WriteMode) {
|
getSubroom(id: number, factoryMode?: RoomDataTypes.FactoryMode, writeMode?: RoomDataTypes.WriteMode) {
|
||||||
if (!this.#resolvedId) throw this.#unresolvedError;
|
if (!this.#resolvedId) throw this.#unresolvedError;
|
||||||
return new SubroomFactory({
|
return new SubroomFactory({
|
||||||
roomId: this.#resolvedId,
|
|
||||||
subroomId: id,
|
subroomId: id,
|
||||||
factoryMode: factoryMode ? factoryMode : undefined,
|
factoryMode: factoryMode ? factoryMode : undefined,
|
||||||
writeMode : writeMode ? writeMode : undefined
|
writeMode : writeMode ? writeMode : undefined
|
||||||
@@ -240,11 +257,11 @@ export class RoomFactory {
|
|||||||
set ImageName(data) { this.#setHashValue(this.#imageKey, data) }
|
set ImageName(data) { this.#setHashValue(this.#imageKey, data) }
|
||||||
|
|
||||||
#stateKey = 'State';
|
#stateKey = 'State';
|
||||||
get State(): RoomState { return this.#fetchNumberKey(this.#stateKey, RoomState.Active) }
|
get State(): RoomDataTypes.RoomState { return this.#fetchNumberKey(this.#stateKey, RoomDataTypes.RoomState.Active) }
|
||||||
set State(data) { this.#setHashValue(this.#stateKey, data) }
|
set State(data) { this.#setHashValue(this.#stateKey, data) }
|
||||||
|
|
||||||
#accessKey = 'RoomAccessibility';
|
#accessKey = 'RoomAccessibility';
|
||||||
get RoomAccessibility(): RoomAccessibility { return this.#fetchNumberKey(this.#accessKey, RoomAccessibility.Unlisted) }
|
get RoomAccessibility(): RoomDataTypes.RoomAccessibility { return this.#fetchNumberKey(this.#accessKey, RoomDataTypes.RoomAccessibility.Unlisted) }
|
||||||
set RoomAccessibility(data) { this.#setHashValue(this.#accessKey, data) }
|
set RoomAccessibility(data) { this.#setHashValue(this.#accessKey, data) }
|
||||||
|
|
||||||
#votingKey = 'SupportsLevelVoting';
|
#votingKey = 'SupportsLevelVoting';
|
||||||
@@ -279,20 +296,20 @@ export class RoomFactory {
|
|||||||
get DisableMicAutoMute() { return this.#fetchBooleanKey(this.#muteKey, false) }
|
get DisableMicAutoMute() { return this.#fetchBooleanKey(this.#muteKey, false) }
|
||||||
set DisableMicAutoMute(data) { this.#setHashValue(this.#muteKey, data) }
|
set DisableMicAutoMute(data) { this.#setHashValue(this.#muteKey, data) }
|
||||||
|
|
||||||
async getHardwareSupport(): Promise<HardwareSupport[]> {
|
async getHardwareSupport(): Promise<RoomDataTypes.HardwareSupport[]> {
|
||||||
if (!this.#resolvedId) throw this.#unresolvedError;
|
if (!this.#resolvedId) throw this.#unresolvedError;
|
||||||
return (await Redis.Database.smembers(Redis.buildKey(
|
return (await Redis.Database.smembers(Redis.buildKey(
|
||||||
Redis.KeyGroups.Rooms.Root,
|
Redis.KeyGroups.Rooms.Root,
|
||||||
this.#resolvedId.toString(),
|
this.#resolvedId.toString(),
|
||||||
RoomFactory.Keys.HardwareSupport
|
RoomFactory.Keys.HardwareSupport
|
||||||
))) as HardwareSupport[];
|
))) as RoomDataTypes.HardwareSupport[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async addHardwareSupport(hardware: HardwareSupport | HardwareSupport[] | '*') {
|
async addHardwareSupport(hardware: RoomDataTypes.HardwareSupport | RoomDataTypes.HardwareSupport[] | '*') {
|
||||||
if (!this.#resolvedId) throw this.#unresolvedError;
|
if (!this.#resolvedId) throw this.#unresolvedError;
|
||||||
|
|
||||||
if (hardware === '*') {
|
if (hardware === '*') {
|
||||||
await Promise.all(HardwareSupportStrings.map(str => this.addHardwareSupport(str as HardwareSupport) ));
|
await Promise.all(RoomDataTypes.HardwareSupportStrings.map(str => this.addHardwareSupport(str as RoomDataTypes.HardwareSupport) ));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +331,7 @@ export class RoomFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeHardwareSupport(hardware: HardwareSupport) {
|
async removeHardwareSupport(hardware: RoomDataTypes.HardwareSupport) {
|
||||||
if (!this.#resolvedId) throw this.#unresolvedError;
|
if (!this.#resolvedId) throw this.#unresolvedError;
|
||||||
await Redis.Database.srem(Redis.buildKey(
|
await Redis.Database.srem(Redis.buildKey(
|
||||||
Redis.KeyGroups.Rooms.Root,
|
Redis.KeyGroups.Rooms.Root,
|
||||||
|
|||||||
@@ -15,21 +15,27 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { Redis } from "../../db.ts";
|
import { Redis } from "../../../db.ts";
|
||||||
import { Profile } from "../profiles.ts";
|
import { Profile } from "../../profile/base/profiles.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { BuiltinRoom, FactoryMode, IntegratedRoomScene, RoomAccessibility, RoomDataTypes, RoomDetails, RoomState, WriteMode } from "./rooms/DataTypes.ts";
|
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||||
import { RoomFactory } from "./rooms/RoomFactory.ts";
|
import { RootPath } from "../../../path.ts";
|
||||||
import { SubroomFactory } from "./rooms/SubroomFactory.ts";
|
import { RoomFactory } from "./RoomFactory.ts";
|
||||||
import { RootPath } from "../../path.ts";
|
import { RoomDataTypes } from "../rooms/base/DataTypes.ts";
|
||||||
import { Instance } from "../live/instances.ts";
|
import Rooms from "./base/RoomsBase.ts";
|
||||||
import { PushNotificationId } from "../../socket/types.ts";
|
|
||||||
|
|
||||||
const log = new Logging("Rooms");
|
const log = new Logging("Rooms");
|
||||||
|
|
||||||
const rooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as BuiltinRoom[];
|
const builtinRooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as RoomDataTypes.BuiltinRoom[];
|
||||||
|
|
||||||
class RoomsBase {
|
const baseImageChanges = [
|
||||||
|
{ room: "DodgeballVR", image: "Dodgeball" },
|
||||||
|
{ room: "PaintballVR", image: "Paintball" },
|
||||||
|
{ room: "StuntRunnerBaseRoom", image: "StuntRunner" },
|
||||||
|
{ room: "BowlingAlley", image: "Bowling" },
|
||||||
|
];
|
||||||
|
|
||||||
|
class RoomsMiscBase {
|
||||||
|
|
||||||
static Keys = {
|
static Keys = {
|
||||||
BuiltinGenerated: "builtinrooms-done",
|
BuiltinGenerated: "builtinrooms-done",
|
||||||
@@ -37,33 +43,13 @@ class RoomsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllBuiltinRooms() {
|
getAllBuiltinRooms() {
|
||||||
return rooms;
|
return builtinRooms;
|
||||||
}
|
|
||||||
|
|
||||||
async get(id: number) {
|
|
||||||
try {
|
|
||||||
const factory = await new RoomFactory({ id: id }).init();
|
|
||||||
if (!factory) return null;
|
|
||||||
return factory.export();
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getByName(name: string) {
|
|
||||||
try {
|
|
||||||
const factory = await new RoomFactory({ name: name }).init();
|
|
||||||
if (!factory) return null;
|
|
||||||
return factory.export();
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllBuiltinRoomGenerations() {
|
async getAllBuiltinRoomGenerations() {
|
||||||
const ids = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.AGRooms));
|
const ids = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsMiscBase.Keys.AGRooms));
|
||||||
const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val));
|
const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val));
|
||||||
return (await Promise.all(parsedIds.map(id => this.get(id)))).filter(val => val !== null);
|
return (await Promise.all(parsedIds.map(id => Rooms.get(id)))).filter(val => val !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #getAvailableRoomId() {
|
async #getAvailableRoomId() {
|
||||||
@@ -77,9 +63,7 @@ class RoomsBase {
|
|||||||
let id = Math.round(Math.random() * Math.pow(2, 31));
|
let id = Math.round(Math.random() * Math.pow(2, 31));
|
||||||
while ((await Redis.Database.exists(
|
while ((await Redis.Database.exists(
|
||||||
Redis.buildKey(
|
Redis.buildKey(
|
||||||
Redis.KeyGroups.Rooms.Root,
|
Redis.KeyGroups.Subrooms.Root,
|
||||||
roomid.toString(),
|
|
||||||
RoomFactory.Keys.Subrooms,
|
|
||||||
id.toString(),
|
id.toString(),
|
||||||
SubroomFactory.Keys.Meta
|
SubroomFactory.Keys.Meta
|
||||||
))) >= 1)
|
))) >= 1)
|
||||||
@@ -93,20 +77,20 @@ class RoomsBase {
|
|||||||
result: RoomDataTypes.CreateModifyRoomStatus;
|
result: RoomDataTypes.CreateModifyRoomStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const factory = await new RoomFactory({ id: roomid, factoryMode: FactoryMode.Fetch }).init();
|
const factory = await new RoomFactory({ id: roomid, factoryMode: RoomDataTypes.FactoryMode.Fetch }).init();
|
||||||
if (!factory || !factory.CloningAllowed) return { result: RoomDataTypes.CreateModifyRoomStatus.PermissionDenied } as RoomClone;
|
if (!factory || !factory.CloningAllowed) return { result: RoomDataTypes.CreateModifyRoomStatus.PermissionDenied } as RoomClone;
|
||||||
if (factory.Name == 'DormRoom') return { result: RoomDataTypes.CreateModifyRoomStatus.ReservedName } as RoomClone;
|
if (factory.Name == 'DormRoom') return { result: RoomDataTypes.CreateModifyRoomStatus.ReservedName } as RoomClone;
|
||||||
if (factory.Name == newname) return { result: RoomDataTypes.CreateModifyRoomStatus.DuplicateName } as RoomClone;
|
if (factory.Name == newname) return { result: RoomDataTypes.CreateModifyRoomStatus.DuplicateName } as RoomClone;
|
||||||
|
|
||||||
const newFactory = await new RoomFactory({ id: await Rooms.#getAvailableRoomId(), factoryMode: FactoryMode.Write }).init();
|
const newFactory = await new RoomFactory({ id: await RoomsMisc.#getAvailableRoomId(), factoryMode: RoomDataTypes.FactoryMode.Write }).init();
|
||||||
if (!newFactory) return { result: RoomDataTypes.CreateModifyRoomStatus.Unknown } as RoomClone;
|
if (!newFactory) return { result: RoomDataTypes.CreateModifyRoomStatus.Unknown } as RoomClone;
|
||||||
|
|
||||||
newFactory.CreatorPlayerId = newowner.getId();
|
newFactory.CreatorPlayerId = newowner.getId();
|
||||||
newFactory.Description = factory.Description;
|
newFactory.Description = factory.Description;
|
||||||
newFactory.Name = newname;
|
newFactory.Name = newname;
|
||||||
newFactory.ImageName = factory.ImageName;
|
newFactory.ImageName = factory.ImageName;
|
||||||
newFactory.State = RoomState.Active;
|
newFactory.State = RoomDataTypes.RoomState.Active;
|
||||||
newFactory.RoomAccessibility = RoomAccessibility.Private;
|
newFactory.RoomAccessibility = RoomDataTypes.RoomDataTypes.RoomAccessibility.Private;
|
||||||
newFactory.SupportsLevelVoting = factory.SupportsLevelVoting;
|
newFactory.SupportsLevelVoting = factory.SupportsLevelVoting;
|
||||||
newFactory.IsAGRoom = false;
|
newFactory.IsAGRoom = false;
|
||||||
newFactory.IsDormRoom = factory.IsDormRoom;
|
newFactory.IsDormRoom = factory.IsDormRoom;
|
||||||
@@ -122,9 +106,10 @@ class RoomsBase {
|
|||||||
|
|
||||||
const oldSubroomIds = await factory.getAllSubroomIds();
|
const oldSubroomIds = await factory.getAllSubroomIds();
|
||||||
const promises = oldSubroomIds.map(async (id) => {
|
const promises = oldSubroomIds.map(async (id) => {
|
||||||
const newSubroomFactory = await newFactory.getSubroom(id, FactoryMode.Write, WriteMode.Overwrite).init();
|
const newSubroomFactory = await newFactory.getSubroom(id, RoomDataTypes.RoomDataTypes.FactoryMode.Write, RoomDataTypes.RoomDataTypes.WriteMode.Overwrite).init();
|
||||||
const oldSubroomFactory = await factory.getSubroom(id, FactoryMode.Fetch).init();
|
const oldSubroomFactory = await factory.getSubroom(id, RoomDataTypes.RoomDataTypes.FactoryMode.Fetch).init();
|
||||||
|
|
||||||
|
newSubroomFactory.RoomId = newFactory.RoomId;
|
||||||
newSubroomFactory.RoomSceneLocationId = oldSubroomFactory.RoomSceneLocationId;
|
newSubroomFactory.RoomSceneLocationId = oldSubroomFactory.RoomSceneLocationId;
|
||||||
newSubroomFactory.Name = oldSubroomFactory.Name;
|
newSubroomFactory.Name = oldSubroomFactory.Name;
|
||||||
newSubroomFactory.IsSandbox = oldSubroomFactory.IsSandbox;
|
newSubroomFactory.IsSandbox = oldSubroomFactory.IsSandbox;
|
||||||
@@ -137,7 +122,7 @@ class RoomsBase {
|
|||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
await newFactory.write();
|
await newFactory.write();
|
||||||
newFactory.factoryMode = FactoryMode.Fetch;
|
newFactory.factoryMode = RoomDataTypes.RoomDataTypes.FactoryMode.Fetch;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
factory: newFactory,
|
factory: newFactory,
|
||||||
@@ -176,15 +161,15 @@ class RoomsBase {
|
|||||||
async generateNewDorm(player: Profile) {
|
async generateNewDorm(player: Profile) {
|
||||||
const id = await this.#getAvailableRoomId();
|
const id = await this.#getAvailableRoomId();
|
||||||
|
|
||||||
const factory = await new RoomFactory({ id: id, factoryMode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree }).init();
|
const factory = await new RoomFactory({ id: id, factoryMode: RoomDataTypes.FactoryMode.Write, writeMode: RoomDataTypes.WriteMode.WriteIfFree }).init();
|
||||||
if (!factory) return null;
|
if (!factory) return null;
|
||||||
|
|
||||||
factory.Name = "DormRoom";
|
factory.Name = "DormRoom";
|
||||||
factory.Description = "Your private room.";
|
factory.Description = "Your private room.";
|
||||||
factory.CreatorPlayerId = player.getId();
|
factory.CreatorPlayerId = player.getId();
|
||||||
factory.ImageName = "DefaultProfileImage.png";
|
factory.ImageName = "DefaultProfileImage.png";
|
||||||
factory.State = RoomState.Active;
|
factory.State = RoomDataTypes.RoomState.Active;
|
||||||
factory.RoomAccessibility = RoomAccessibility.Private;
|
factory.RoomAccessibility = RoomDataTypes.RoomAccessibility.Private;
|
||||||
factory.SupportsLevelVoting = false;
|
factory.SupportsLevelVoting = false;
|
||||||
factory.IsAGRoom = false;
|
factory.IsAGRoom = false;
|
||||||
factory.IsDormRoom = true;
|
factory.IsDormRoom = true;
|
||||||
@@ -195,10 +180,11 @@ class RoomsBase {
|
|||||||
|
|
||||||
factory.addHardwareSupport('*');
|
factory.addHardwareSupport('*');
|
||||||
|
|
||||||
const subroomFactory = await factory.getSubroom(await this.#getAvailableSubRoomId(id), FactoryMode.Write, WriteMode.WriteIfFree).init();
|
const subroomFactory = await factory.getSubroom(await this.#getAvailableSubRoomId(id), RoomDataTypes.FactoryMode.Write, RoomDataTypes.WriteMode.WriteIfFree).init();
|
||||||
if (!subroomFactory) return null;
|
if (!subroomFactory) return null;
|
||||||
|
|
||||||
subroomFactory.RoomSceneLocationId = IntegratedRoomScene.DormRoom;
|
subroomFactory.RoomId = id;
|
||||||
|
subroomFactory.RoomSceneLocationId = RoomDataTypes.IntegratedRoomScene.DormRoom;
|
||||||
subroomFactory.Name = "Home";
|
subroomFactory.Name = "Home";
|
||||||
subroomFactory.IsSandbox = true;
|
subroomFactory.IsSandbox = true;
|
||||||
subroomFactory.DataBlobName = "";
|
subroomFactory.DataBlobName = "";
|
||||||
@@ -212,26 +198,20 @@ class RoomsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateBuiltinRooms() {
|
async generateBuiltinRooms() {
|
||||||
|
if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsMiscBase.Keys.BuiltinGenerated))) !== null) return true;
|
||||||
if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated))) !== null) return true;
|
|
||||||
await Promise.all(rooms.map(async builtinRoom => {
|
await Promise.all(builtinRooms.map(async builtinRoom => {
|
||||||
if (builtinRoom.Name == 'DormRoom') return;
|
if (builtinRoom.Name == 'DormRoom') return;
|
||||||
const newId = await this.#getAvailableRoomId();
|
const newId = await this.#getAvailableRoomId();
|
||||||
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.AGRooms), newId);
|
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsMiscBase.Keys.AGRooms), newId);
|
||||||
|
|
||||||
const factory = await new RoomFactory({ id: newId, factoryMode: FactoryMode.Write, writeMode: WriteMode.Overwrite }).init();
|
const factory = await new RoomFactory({ id: newId, factoryMode: RoomDataTypes.FactoryMode.Write, writeMode: RoomDataTypes.WriteMode.Overwrite }).init();
|
||||||
if (!factory) return;
|
if (!factory) return;
|
||||||
|
|
||||||
factory.Name = builtinRoom.Name;
|
factory.Name = builtinRoom.Name;
|
||||||
factory.Description = builtinRoom.Description;
|
factory.Description = builtinRoom.Description;
|
||||||
factory.CreatorPlayerId = 1;
|
factory.CreatorPlayerId = 1;
|
||||||
|
|
||||||
const baseImageChanges = [
|
|
||||||
{ room: "DodgeballVR", image: "Dodgeball" },
|
|
||||||
{ room: "PaintballVR", image: "Paintball" },
|
|
||||||
{ room: "StuntRunnerBaseRoom", image: "StuntRunner" },
|
|
||||||
{ room: "BowlingAlley", image: "Bowling" },
|
|
||||||
]
|
|
||||||
|
|
||||||
if (baseImageChanges.find(change => change.room == builtinRoom.Name)) {
|
if (baseImageChanges.find(change => change.room == builtinRoom.Name)) {
|
||||||
const image = baseImageChanges.find(change => change.room == builtinRoom.Name)!;
|
const image = baseImageChanges.find(change => change.room == builtinRoom.Name)!;
|
||||||
@@ -239,7 +219,7 @@ class RoomsBase {
|
|||||||
}
|
}
|
||||||
else factory.ImageName = `${builtinRoom.Name}.png`;
|
else factory.ImageName = `${builtinRoom.Name}.png`;
|
||||||
|
|
||||||
factory.State = RoomState.Active;
|
factory.State = RoomDataTypes.RoomState.Active;
|
||||||
factory.RoomAccessibility = builtinRoom.Accessibility;
|
factory.RoomAccessibility = builtinRoom.Accessibility;
|
||||||
factory.SupportsLevelVoting = builtinRoom.SupportsLevelVoting;
|
factory.SupportsLevelVoting = builtinRoom.SupportsLevelVoting;
|
||||||
factory.IsAGRoom = true;
|
factory.IsAGRoom = true;
|
||||||
@@ -254,9 +234,10 @@ class RoomsBase {
|
|||||||
|
|
||||||
await Promise.all(builtinRoom.Scenes.map(async subroom => {
|
await Promise.all(builtinRoom.Scenes.map(async subroom => {
|
||||||
const newSubroomId = await this.#getAvailableSubRoomId(newId);
|
const newSubroomId = await this.#getAvailableSubRoomId(newId);
|
||||||
const subroomFactory = await factory.getSubroom(newSubroomId, FactoryMode.Write, WriteMode.Overwrite).init();
|
const subroomFactory = await factory.getSubroom(newSubroomId, RoomDataTypes.FactoryMode.Write, RoomDataTypes.WriteMode.Overwrite).init();
|
||||||
if (!subroomFactory) return;
|
if (!subroomFactory) return;
|
||||||
|
|
||||||
|
subroomFactory.RoomId = newId;
|
||||||
subroomFactory.RoomSceneLocationId = subroom.RoomSceneLocationId;
|
subroomFactory.RoomSceneLocationId = subroom.RoomSceneLocationId;
|
||||||
subroomFactory.Name = subroom.Name;
|
subroomFactory.Name = subroom.Name;
|
||||||
subroomFactory.IsSandbox = subroom.IsSandbox;
|
subroomFactory.IsSandbox = subroom.IsSandbox;
|
||||||
@@ -269,53 +250,13 @@ class RoomsBase {
|
|||||||
|
|
||||||
await factory.write();
|
await factory.write();
|
||||||
}));
|
}));
|
||||||
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated), "1");
|
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsMiscBase.Keys.BuiltinGenerated), "1");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIdFromName(name: string) {
|
|
||||||
const unparsedId = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Room_Names, name));
|
|
||||||
if (!unparsedId) return null;
|
|
||||||
const parsedId = parseInt(unparsedId);
|
|
||||||
if (isNaN(parsedId)) return null;
|
|
||||||
return parsedId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSubroomIdsFromRoom(id: number): Promise<string[]>;
|
|
||||||
async getSubroomIdsFromRoom(id: number, stringify: false): Promise<number[]>;
|
|
||||||
async getSubroomIdsFromRoom(id: number, stringify: boolean | undefined = false): Promise<string[] | number[]> {
|
|
||||||
const ids = await Redis.Database.smembers(Redis.buildKey(
|
|
||||||
Redis.KeyGroups.Rooms.Root,
|
|
||||||
id.toString(),
|
|
||||||
RoomFactory.Keys.Subrooms
|
|
||||||
));
|
|
||||||
const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val));
|
|
||||||
|
|
||||||
if (!stringify) return parsedIds;
|
|
||||||
else return parsedIds.map(val => val.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubroomNameFromId(room: RoomDetails, subroomId: number) {
|
|
||||||
const subroom = room.Scenes.find(scene => scene.RoomSceneId == subroomId);
|
|
||||||
if (subroom) return subroom.Name;
|
|
||||||
else return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async socketUpdateRoom(instance: Instance) {
|
|
||||||
const room = await this.get(instance.roomId);
|
|
||||||
if (!room) return;
|
|
||||||
|
|
||||||
for (const player of instance.getAllPlayers()) {
|
|
||||||
const sock = player.getSocketHandler();
|
|
||||||
if (!sock) continue;
|
|
||||||
sock.sendNotification("RoomInstanceUpdate", instance.snapshot());
|
|
||||||
sock.sendNotification(PushNotificationId.SubscriptionUpdateRoom, room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Rooms = new RoomsBase();
|
const RoomsMisc = new RoomsMiscBase();
|
||||||
|
|
||||||
export { rooms as BuiltinRooms };
|
export { builtinRooms as BuiltinRooms };
|
||||||
export default Rooms;
|
export default RoomsMisc;
|
||||||
@@ -15,15 +15,18 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { Redis } from "../../../db.ts";
|
import { Redis } from "../../../db.ts";
|
||||||
import { RoomDataTypes, IntegratedRoomScene, RoomScene, WriteMode, FactoryMode } from "./DataTypes.ts";
|
import Server from "../../server/server.ts";
|
||||||
import { RoomFactory } from "./RoomFactory.ts";
|
import { RoomDataTypes } from "./base/DataTypes.ts";
|
||||||
|
import { RoomFactory, roomdebug } from "./RoomFactory.ts";
|
||||||
|
|
||||||
|
const log = new Logging("SubroomFactory");
|
||||||
|
|
||||||
interface SubroomFactoryOptions {
|
interface SubroomFactoryOptions {
|
||||||
roomId: number;
|
|
||||||
subroomId: number;
|
subroomId: number;
|
||||||
writeMode?: WriteMode;
|
writeMode?: RoomDataTypes.WriteMode;
|
||||||
factoryMode?: FactoryMode;
|
factoryMode?: RoomDataTypes.FactoryMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SubroomFactory {
|
export class SubroomFactory {
|
||||||
@@ -42,13 +45,11 @@ export class SubroomFactory {
|
|||||||
#writeMode: RoomDataTypes.WriteMode = RoomDataTypes.WriteMode.WriteIfFree;
|
#writeMode: RoomDataTypes.WriteMode = RoomDataTypes.WriteMode.WriteIfFree;
|
||||||
factoryMode: RoomDataTypes.FactoryMode = RoomDataTypes.FactoryMode.Fetch;
|
factoryMode: RoomDataTypes.FactoryMode = RoomDataTypes.FactoryMode.Fetch;
|
||||||
|
|
||||||
#roomId: number;
|
|
||||||
#subroomId: number;
|
#subroomId: number;
|
||||||
|
|
||||||
#hash: Record<string, string> | null = null;
|
#hash: Record<string, string> | null = null;
|
||||||
|
|
||||||
constructor(options: SubroomFactoryOptions) {
|
constructor(options: SubroomFactoryOptions) {
|
||||||
this.#roomId = options.roomId;
|
|
||||||
this.#subroomId = options.subroomId;
|
this.#subroomId = options.subroomId;
|
||||||
if (options.writeMode) this.#writeMode = options.writeMode;
|
if (options.writeMode) this.#writeMode = options.writeMode;
|
||||||
if (options.factoryMode) this.factoryMode = options.factoryMode;
|
if (options.factoryMode) this.factoryMode = options.factoryMode;
|
||||||
@@ -56,12 +57,10 @@ export class SubroomFactory {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
|
||||||
if (!this.#roomId || !this.#subroomId) throw this.#unspecifiedArguments;
|
if (!this.#subroomId) throw this.#unspecifiedArguments;
|
||||||
|
|
||||||
this.#hash = await Redis.Database.hgetall(Redis.buildKey(
|
this.#hash = await Redis.Database.hgetall(Redis.buildKey(
|
||||||
Redis.KeyGroups.Rooms.Root,
|
Redis.KeyGroups.Subrooms.Root,
|
||||||
this.#roomId.toString(),
|
|
||||||
RoomFactory.Keys.Subrooms,
|
|
||||||
this.#subroomId.toString(),
|
this.#subroomId.toString(),
|
||||||
SubroomFactory.Keys.Meta
|
SubroomFactory.Keys.Meta
|
||||||
));
|
));
|
||||||
@@ -76,9 +75,7 @@ export class SubroomFactory {
|
|||||||
else {
|
else {
|
||||||
|
|
||||||
const dbkey = Redis.buildKey(
|
const dbkey = Redis.buildKey(
|
||||||
Redis.KeyGroups.Rooms.Root,
|
Redis.KeyGroups.Subrooms.Root,
|
||||||
this.#roomId.toString(),
|
|
||||||
RoomFactory.Keys.Subrooms,
|
|
||||||
this.#subroomId.toString(),
|
this.#subroomId.toString(),
|
||||||
SubroomFactory.Keys.Meta
|
SubroomFactory.Keys.Meta
|
||||||
);
|
);
|
||||||
@@ -89,21 +86,25 @@ export class SubroomFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.#hash) throw this.#hashValuesNotSetError;
|
if (!this.#hash) throw this.#hashValuesNotSetError;
|
||||||
this.#hash['DataModifiedAt'] = new Date().toISOString();
|
this.#hash[this.#modifiedKey] = new Date().toISOString();
|
||||||
|
|
||||||
await Redis.Database.hset(dbkey, this.#hash);
|
await Redis.Database.hset(dbkey, this.#hash);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Redis.Database.sadd(Redis.buildKey(
|
if (this.#hash[this.#roomIdKey])
|
||||||
Redis.KeyGroups.Rooms.Root,
|
await Redis.Database.sadd(Redis.buildKey(
|
||||||
this.#roomId.toString(),
|
Redis.KeyGroups.Rooms.Root,
|
||||||
RoomFactory.Keys.Subrooms
|
this.#hash[this.#roomIdKey].toString(),
|
||||||
), this.RoomSceneId);
|
RoomFactory.Keys.Subrooms
|
||||||
|
), this.RoomSceneId);
|
||||||
|
|
||||||
|
if (roomdebug) log.d(`Writing subroom ${this.RoomSceneId}: ${JSON.stringify(this.#hash)}`);
|
||||||
|
Server.emit('room.subroom.updated', { subroom: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
export(): RoomScene {
|
export(): RoomDataTypes.RoomScene {
|
||||||
|
if (roomdebug) log.d(`Exported subroom details for room:subroom ${this.RoomId}:${this.RoomSceneId}`);
|
||||||
return {
|
return {
|
||||||
RoomSceneId: this.RoomSceneId,
|
RoomSceneId: this.RoomSceneId,
|
||||||
RoomId: this.RoomId,
|
RoomId: this.RoomId,
|
||||||
@@ -144,7 +145,7 @@ export class SubroomFactory {
|
|||||||
|
|
||||||
#setHashValue(key: string, value: string | number | boolean) {
|
#setHashValue(key: string, value: string | number | boolean) {
|
||||||
if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchSubroomFirstError;
|
if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchSubroomFirstError;
|
||||||
|
|
||||||
if (!this.#hash) this.#hash = {};
|
if (!this.#hash) this.#hash = {};
|
||||||
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (typeof value === 'object' && value !== null) {
|
||||||
@@ -158,10 +159,17 @@ export class SubroomFactory {
|
|||||||
|
|
||||||
get RoomSceneId() { return this.#subroomId }
|
get RoomSceneId() { return this.#subroomId }
|
||||||
|
|
||||||
get RoomId() { return this.#roomId }
|
#roomIdKey = 'RoomId';
|
||||||
|
get RoomId() { return this.#fetchNumberKey(this.#roomIdKey, 0) }
|
||||||
|
set RoomId(data) { this.#setHashValue(this.#roomIdKey, data) }
|
||||||
|
|
||||||
#locationKey = 'RoomSceneLocationId';
|
#locationKey = 'RoomSceneLocationId';
|
||||||
get RoomSceneLocationId(): IntegratedRoomScene { return this.#fetchStringKey(this.#locationKey, IntegratedRoomScene.PerformanceHall) as IntegratedRoomScene }
|
get RoomSceneLocationId(): RoomDataTypes.IntegratedRoomScene {
|
||||||
|
return this.#fetchStringKey(
|
||||||
|
this.#locationKey,
|
||||||
|
RoomDataTypes.IntegratedRoomScene.PerformanceHall
|
||||||
|
) as RoomDataTypes.IntegratedRoomScene;
|
||||||
|
}
|
||||||
set RoomSceneLocationId(data) { this.#setHashValue(this.#locationKey, data) }
|
set RoomSceneLocationId(data) { this.#setHashValue(this.#locationKey, data) }
|
||||||
|
|
||||||
#nameKey = 'Name';
|
#nameKey = 'Name';
|
||||||
@@ -189,9 +197,7 @@ export class SubroomFactory {
|
|||||||
|
|
||||||
async addBlobHistory(date: Date, filename: string) {
|
async addBlobHistory(date: Date, filename: string) {
|
||||||
await Redis.Database.hset(Redis.buildKey(
|
await Redis.Database.hset(Redis.buildKey(
|
||||||
Redis.KeyGroups.Rooms.Root,
|
Redis.KeyGroups.Subrooms.Root,
|
||||||
this.#roomId.toString(),
|
|
||||||
RoomFactory.Keys.Subrooms,
|
|
||||||
this.#subroomId.toString(),
|
this.#subroomId.toString(),
|
||||||
SubroomFactory.Keys.Blobs
|
SubroomFactory.Keys.Blobs
|
||||||
), date.toISOString(), filename);
|
), date.toISOString(), filename);
|
||||||
@@ -204,9 +210,7 @@ export class SubroomFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hist = await Redis.Database.hgetall(Redis.buildKey(
|
const hist = await Redis.Database.hgetall(Redis.buildKey(
|
||||||
Redis.KeyGroups.Rooms.Root,
|
Redis.KeyGroups.Subrooms.Root,
|
||||||
this.#roomId.toString(),
|
|
||||||
RoomFactory.Keys.Subrooms,
|
|
||||||
this.#subroomId.toString(),
|
this.#subroomId.toString(),
|
||||||
SubroomFactory.Keys.Blobs
|
SubroomFactory.Keys.Blobs
|
||||||
));
|
));
|
||||||
|
|||||||
68
src/data/content/rooms/base/RoomsBase.ts
Normal file
68
src/data/content/rooms/base/RoomsBase.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import { Redis } from "../../../../db.ts";
|
||||||
|
import { RoomDetails } from "./DataTypes.ts";
|
||||||
|
import { RoomFactory } from "../RoomFactory.ts";
|
||||||
|
|
||||||
|
export class RoomsBase {
|
||||||
|
|
||||||
|
async get(id: number) {
|
||||||
|
try {
|
||||||
|
const factory = await new RoomFactory({ id: id }).init();
|
||||||
|
if (!factory) return null;
|
||||||
|
return factory.export();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByName(name: string) {
|
||||||
|
try {
|
||||||
|
const factory = await new RoomFactory({ name: name }).init();
|
||||||
|
if (!factory) return null;
|
||||||
|
return factory.export();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSubroomIdsFromRoom(id: number): Promise<string[]>;
|
||||||
|
async getSubroomIdsFromRoom(id: number, stringify: false): Promise<number[]>;
|
||||||
|
async getSubroomIdsFromRoom(id: number, stringify: boolean | undefined = false): Promise<string[] | number[]> {
|
||||||
|
const ids = await Redis.Database.smembers(Redis.buildKey(
|
||||||
|
Redis.KeyGroups.Rooms.Root,
|
||||||
|
id.toString(),
|
||||||
|
RoomFactory.Keys.Subrooms
|
||||||
|
));
|
||||||
|
const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val));
|
||||||
|
|
||||||
|
if (!stringify) return parsedIds;
|
||||||
|
else return parsedIds.map(val => val.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubroomNameFromId(room: RoomDetails, subroomId: number) {
|
||||||
|
const subroom = room.Scenes.find(scene => scene.RoomSceneId == subroomId);
|
||||||
|
if (subroom) return subroom.Name;
|
||||||
|
else return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const Rooms = new RoomsBase();
|
||||||
|
|
||||||
|
export default Rooms;
|
||||||
188
src/data/live/Instance.ts
Normal file
188
src/data/live/Instance.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import { Config } from "../../config/config.ts";
|
||||||
|
import { PushNotificationId } from "../../socket/types.ts";
|
||||||
|
import { RoomDataTypes } from "../content/rooms/base/DataTypes.ts";
|
||||||
|
import { RoomFactory } from "../content/rooms/RoomFactory.ts";
|
||||||
|
import type { Profile } from "../profile/base/profiles.ts";
|
||||||
|
import Server from "../server/server.ts";
|
||||||
|
import Instances from "./instances.ts";
|
||||||
|
import Presence from "./presence.ts";
|
||||||
|
import type { InstanceOptions, RoomInstance } from "./types.ts";
|
||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
|
||||||
|
const config = Config.getConfig();
|
||||||
|
|
||||||
|
const log = new Logging("InstanceBase");
|
||||||
|
|
||||||
|
export class Instance {
|
||||||
|
|
||||||
|
#players = new Set<Profile>();
|
||||||
|
timeCreated = new Date().toISOString();
|
||||||
|
|
||||||
|
#id: number;
|
||||||
|
#room: RoomDataTypes.RoomDetails | undefined;
|
||||||
|
#subroom: RoomDataTypes.RoomScene | undefined;
|
||||||
|
|
||||||
|
#eventId?: number; // not yet implemented
|
||||||
|
#name?: string;
|
||||||
|
#priv?: boolean;
|
||||||
|
#inProgress?: boolean;
|
||||||
|
#blob?: string;
|
||||||
|
|
||||||
|
constructor(id: number) {
|
||||||
|
this.#id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(options: InstanceOptions) {
|
||||||
|
const scene = options.Room.Scenes[options.SceneIndex];
|
||||||
|
if (!scene) throw new Error("The specified scene does not exist.");
|
||||||
|
|
||||||
|
let instanceName;
|
||||||
|
if (scene.Name == 'Home' || scene.Name === options.Room.Room.Name) instanceName = `^${options.Room.Room.Name}`;
|
||||||
|
else instanceName = `^${options.Room.Room.Name}.${scene.Name}`;
|
||||||
|
if (options.IsDorm) {
|
||||||
|
const dormCreatorPlayer = Server.UnifiedProfile.get(options.Room.Room.CreatorPlayerId);
|
||||||
|
if (!dormCreatorPlayer) throw new Error("Creator of dorm does not exist.");
|
||||||
|
const player = await dormCreatorPlayer.export();
|
||||||
|
if (player) instanceName = `@${player.displayName}'s Dorm`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#room = options.Room;
|
||||||
|
this.#subroom = scene;
|
||||||
|
this.#name = instanceName;
|
||||||
|
this.#blob = scene.DataBlobName;
|
||||||
|
this.#inProgress = false;
|
||||||
|
this.#priv = options.Private ? options.Private : false;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
equalInstance(instance: RoomInstance) {
|
||||||
|
return instance.roomInstanceId == this.#id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllPlayers() {
|
||||||
|
return this.#players.values().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPlayer(player: Profile) {
|
||||||
|
return this.getAllPlayers().includes(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
removePlayer(player: Profile) {
|
||||||
|
if (!this.hasPlayer(player)) throw new Error(`Cannot remove player ${player.getId()} from instance ${this.#id} they are not in`);
|
||||||
|
this.#players.delete(player);
|
||||||
|
player.setInstance(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client has a push notification for game session updates, but the client
|
||||||
|
* is based on instances, not game sessions. Possibly simply just an alias for
|
||||||
|
* 'RoomInstanceUpdate' but was never (or not yet) renamed. It is unknown
|
||||||
|
* whether the game uses this or not, but it's better to send it just in case.
|
||||||
|
*/
|
||||||
|
updatePlayers() {
|
||||||
|
for (const player of this.#players.values()) player.getSocketHandler()?.sendNotification(PushNotificationId.SubscriptionUpdateGameSession, this.snapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
async addPlayer(player: Profile) {
|
||||||
|
const currentInstance = player.getInstance();
|
||||||
|
if (currentInstance && currentInstance.equalInstance(this)) return;
|
||||||
|
|
||||||
|
if (currentInstance) currentInstance.removePlayer(player);
|
||||||
|
|
||||||
|
if (!this.isFull) {
|
||||||
|
const instancePlayers = this.getAllPlayers();
|
||||||
|
const profileExport = await player.export();
|
||||||
|
log.i(`Player ${player.getId()} "${profileExport?.displayName}" went to '${this.name}' with ${instancePlayers.length} other players`);
|
||||||
|
|
||||||
|
this.#players.add(player);
|
||||||
|
player.setInstance(this);
|
||||||
|
|
||||||
|
const pres = await Presence.get(player);
|
||||||
|
pres.update();
|
||||||
|
|
||||||
|
const room = await new RoomFactory({ id: this.roomId }).init();
|
||||||
|
await room?.addVisit();
|
||||||
|
|
||||||
|
// move some of this to a dedicated "onPlayerMove" function
|
||||||
|
} else log.w(`Instance ${this.roomInstanceId} is full. Cannot add player ${player.getId()}`);
|
||||||
|
|
||||||
|
log.d(`Players in instance ${this.#id}: ${this.#players.values().toArray().map(prof => prof.getId()).join(',')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomInstanceId() { return this.#id }
|
||||||
|
|
||||||
|
get roomId() { return this.#room ? this.#room?.Room.RoomId : 0 }
|
||||||
|
|
||||||
|
get subRoomId() { return this.#subroom ? this.#subroom?.RoomSceneId : 0 }
|
||||||
|
|
||||||
|
get location() { return this.#subroom ? this.#subroom?.RoomSceneLocationId : "" }
|
||||||
|
|
||||||
|
get dataBlob() { return this.#blob ? this.#blob : undefined }
|
||||||
|
set dataBlob(data) { this.#blob = data }
|
||||||
|
|
||||||
|
get eventId() { return this.#eventId }
|
||||||
|
|
||||||
|
get photonRegionId() { return config.public.photonRegionId }
|
||||||
|
|
||||||
|
get photonRoomId() { return `GC20200306-${this.#id}` }
|
||||||
|
|
||||||
|
get name() { return this.#name ? this.#name : "InstanceNameError" }
|
||||||
|
|
||||||
|
get maxCapacity() { return this.#subroom ? this.#subroom.MaxPlayers : 8 }
|
||||||
|
|
||||||
|
get isFull() { return this.#players.size >= this.maxCapacity }
|
||||||
|
|
||||||
|
get isPrivate() { return this.#priv ? this.#priv : false }
|
||||||
|
set isPrivate(data) { this.#priv = data }
|
||||||
|
|
||||||
|
get isInProgress() { return this.#inProgress ? this.#inProgress : false }
|
||||||
|
set isInProgress(data) { this.#inProgress = data }
|
||||||
|
|
||||||
|
snapshot() {
|
||||||
|
const inst: RoomInstance = {
|
||||||
|
roomInstanceId: this.roomInstanceId,
|
||||||
|
roomId: this.roomId,
|
||||||
|
subRoomId: this.subRoomId,
|
||||||
|
location: this.location,
|
||||||
|
dataBlob: this.dataBlob,
|
||||||
|
eventId: this.eventId,
|
||||||
|
photonRegionId: this.photonRegionId,
|
||||||
|
photonRoomId: this.photonRoomId,
|
||||||
|
name: this.name,
|
||||||
|
maxCapacity: this.maxCapacity,
|
||||||
|
isFull: this.isFull,
|
||||||
|
isPrivate: this.isPrivate,
|
||||||
|
isInProgress: this.isInProgress
|
||||||
|
}
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
Instances.getAllInstances(true).delete(this);
|
||||||
|
if (this.#players.size !== 0) for (const player of this.#players) player.getSocketHandler()?.sendNotification(PushNotificationId.Logout);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSubroom(subroom: RoomDataTypes.RoomScene) {
|
||||||
|
this.#subroom = subroom;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
src/data/live/PhotonTypes.ts
Normal file
48
src/data/live/PhotonTypes.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
export enum PhotonRegionCodeString {
|
||||||
|
Europe = "eu",
|
||||||
|
UnitedStates = "us",
|
||||||
|
Asia = "asia",
|
||||||
|
Japan = "jp",
|
||||||
|
Australia = "au",
|
||||||
|
UnitedStates_West = "usw",
|
||||||
|
SouthAmerica = "sa",
|
||||||
|
CanadaEast = "cae",
|
||||||
|
SouthKorea = "kr",
|
||||||
|
India = "@in",
|
||||||
|
Russia = "ru",
|
||||||
|
RussiaEast = "rue",
|
||||||
|
None = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PhotonRegionCodeNumber {
|
||||||
|
eu,
|
||||||
|
us,
|
||||||
|
asia,
|
||||||
|
jp,
|
||||||
|
au = 5,
|
||||||
|
usw,
|
||||||
|
sa,
|
||||||
|
cae,
|
||||||
|
kr,
|
||||||
|
"@in",
|
||||||
|
ru,
|
||||||
|
rue,
|
||||||
|
none = 4
|
||||||
|
}
|
||||||
@@ -15,12 +15,16 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import Rooms from "../content/rooms.ts";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
|
import RoomsMisc from "../content/rooms/Rooms.ts";
|
||||||
import { Profile } from "../profiles.ts";
|
import { RoomDataTypes } from "../content/rooms/base/DataTypes.ts";
|
||||||
|
import Rooms from "../content/rooms/base/RoomsBase.ts";
|
||||||
|
import { Profile } from "../profile/base/profiles.ts";
|
||||||
import Instances from "./instances.ts";
|
import Instances from "./instances.ts";
|
||||||
import { MatchmakingErrorCode, RoomInstance } from "./types.ts";
|
import { MatchmakingErrorCode, RoomInstance } from "./types.ts";
|
||||||
|
|
||||||
|
const log = new Logging("MatchmakingBase");
|
||||||
|
|
||||||
const loginLocks: Map<number, string> = new Map();
|
const loginLocks: Map<number, string> = new Map();
|
||||||
|
|
||||||
interface MatchmakingOptions {
|
interface MatchmakingOptions {
|
||||||
@@ -49,6 +53,9 @@ class MatchmakingBase {
|
|||||||
else return null;
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This will be removed as login locks will be saved to the database and cannot then be changed
|
||||||
|
*/
|
||||||
deleteLoginLock(prof: Profile) {
|
deleteLoginLock(prof: Profile) {
|
||||||
loginLocks.delete(prof.getId());
|
loginLocks.delete(prof.getId());
|
||||||
}
|
}
|
||||||
@@ -75,7 +82,7 @@ class MatchmakingBase {
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
// check to make sure room exists, is not private, and is active
|
// check to make sure room exists, is not private, and is active
|
||||||
const targetRoom = options.roomName !== 'DormRoom' ? await Rooms.getByName(options.roomName) : await Rooms.getProfileDormDefault(options.profile);
|
const targetRoom = options.roomName !== 'DormRoom' ? await Rooms.getByName(options.roomName) : await RoomsMisc.getProfileDormDefault(options.profile);
|
||||||
if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom };
|
if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom };
|
||||||
if (targetRoom.Room.Accessibility == RoomDataTypes.RoomAccessibility.Private && targetRoom.Room.CreatorPlayerId !== options.profile.getId())
|
if (targetRoom.Room.Accessibility == RoomDataTypes.RoomAccessibility.Private && targetRoom.Room.CreatorPlayerId !== options.profile.getId())
|
||||||
return { errorCode: MatchmakingErrorCode.RoomIsPrivate };
|
return { errorCode: MatchmakingErrorCode.RoomIsPrivate };
|
||||||
@@ -90,7 +97,7 @@ class MatchmakingBase {
|
|||||||
if (subroomId) allInstances = allInstances.filter(instance => instance.subRoomId == subroomId);
|
if (subroomId) allInstances = allInstances.filter(instance => instance.subRoomId == subroomId);
|
||||||
|
|
||||||
// filter instances that do not support join in progress and are in progress
|
// filter instances that do not support join in progress and are in progress
|
||||||
const builtinRooms = Rooms.getAllBuiltinRooms();
|
const builtinRooms = RoomsMisc.getAllBuiltinRooms();
|
||||||
const joinInProgressSubrooms = builtinRooms.map(room =>
|
const joinInProgressSubrooms = builtinRooms.map(room =>
|
||||||
({Name: room.Name, Scenes: room.Scenes.map(scene =>
|
({Name: room.Name, Scenes: room.Scenes.map(scene =>
|
||||||
({Name: scene.Name, Supported: scene.SupportsJoinInProgress})
|
({Name: scene.Name, Supported: scene.SupportsJoinInProgress})
|
||||||
@@ -117,9 +124,11 @@ class MatchmakingBase {
|
|||||||
if (!foundInstance) {
|
if (!foundInstance) {
|
||||||
|
|
||||||
const matchmakeableSubrooms = targetRoom.Scenes.filter(scene => scene.CanMatchmakeInto);
|
const matchmakeableSubrooms = targetRoom.Scenes.filter(scene => scene.CanMatchmakeInto);
|
||||||
|
const index = Math.floor(Math.random() * matchmakeableSubrooms.length);
|
||||||
|
log.d(`Scene index ${index} (${targetRoom.Scenes[index] ? targetRoom.Scenes[index].RoomSceneId : "unknown"}) was chosen for matchmaking into ${targetRoom.Room.RoomId}`);
|
||||||
const newInstance = await Instances.createInstance({
|
const newInstance = await Instances.createInstance({
|
||||||
Room: targetRoom,
|
Room: targetRoom,
|
||||||
SceneIndex: Math.floor(Math.random() * matchmakeableSubrooms.length),
|
SceneIndex: index,
|
||||||
FirstPlayer: options.profile,
|
FirstPlayer: options.profile,
|
||||||
Private: options.private,
|
Private: options.private,
|
||||||
IsDorm: options.roomName == 'DormRoom'
|
IsDorm: options.roomName == 'DormRoom'
|
||||||
|
|||||||
@@ -16,171 +16,30 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { Profile } from "../profiles.ts";
|
import { InstanceOptions } from "./types.ts";
|
||||||
import { RoomInstance, InstanceOptions } from "./types.ts";
|
import Server from "../server/server.ts";
|
||||||
import { Config } from "../../config.ts";
|
import { Instance } from "./Instance.ts";
|
||||||
import Presence from "./presence.ts";
|
|
||||||
import { RoomFactory } from "../content/rooms/RoomFactory.ts";
|
|
||||||
import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
|
|
||||||
import { PushNotificationId } from "../../socket/types.ts";
|
|
||||||
import Server from "../server.ts";
|
|
||||||
|
|
||||||
const log = new Logging("Instances");
|
const log = new Logging("Instances");
|
||||||
|
|
||||||
const config = Config.getConfig();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `Map<roomId (number), Instance>`
|
* `Map<roomId (number), Instance>`
|
||||||
*/
|
*/
|
||||||
const instanceSet: Set<Instance> = new Set();
|
const instanceSet: Set<Instance> = new Set();
|
||||||
|
|
||||||
export class Instance {
|
Server.on('room.subroom.updated', (ev) => {
|
||||||
|
if (instanceSet.values().map(inst => inst.subRoomId).toArray().includes(ev.subroom.RoomSceneId)) {
|
||||||
|
const instance = instanceSet.values().toArray().find(inst => inst.subRoomId == ev.subroom.RoomSceneId);
|
||||||
|
if (!instance) return;
|
||||||
|
|
||||||
#players = new Set<Profile>();
|
instance.dataBlob = ev.subroom.DataBlobName;
|
||||||
timeCreated = new Date().toISOString();
|
instance.updateSubroom(ev.subroom.export());
|
||||||
|
for (const profile of instance.getAllPlayers())
|
||||||
#id: number;
|
profile.getSocketHandler()?.sendNotification('RoomInstanceUpdate', instance.snapshot());
|
||||||
#room: RoomDataTypes.RoomDetails | undefined;
|
|
||||||
#subroom: RoomDataTypes.RoomScene | undefined;
|
instance.updatePlayers(); // legacy
|
||||||
|
|
||||||
#eventId?: number; // not yet implemented
|
|
||||||
#name?: string;
|
|
||||||
#priv?: boolean;
|
|
||||||
#inProgress?: boolean;
|
|
||||||
#blob?: string;
|
|
||||||
|
|
||||||
constructor(id: number) {
|
|
||||||
this.#id = id;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
async init(options: InstanceOptions) {
|
|
||||||
|
|
||||||
const scene = options.Room.Scenes[options.SceneIndex];
|
|
||||||
if (!scene) throw new Error("The specified scene does not exist.");
|
|
||||||
|
|
||||||
let instanceName;
|
|
||||||
if (scene.Name == 'Home' || scene.Name === options.Room.Room.Name) instanceName = `^${options.Room.Room.Name}`;
|
|
||||||
else instanceName = `^${options.Room.Room.Name}.${scene.Name}`;
|
|
||||||
if (options.IsDorm) {
|
|
||||||
const dormCreatorPlayer = Server.UnifiedProfile.get(options.Room.Room.CreatorPlayerId);
|
|
||||||
if (!dormCreatorPlayer) throw new Error("Creator of dorm does not exist.");
|
|
||||||
const player = await dormCreatorPlayer.export();
|
|
||||||
if (player) instanceName = `@${player.displayName}'s Dorm`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#room = options.Room;
|
|
||||||
this.#subroom = scene;
|
|
||||||
this.#name = instanceName;
|
|
||||||
this.#blob = scene.DataBlobName;
|
|
||||||
this.#inProgress = false;
|
|
||||||
this.#priv = options.Private ? options.Private : false;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
equalInstance(instance: RoomInstance) {
|
|
||||||
return instance.roomInstanceId == this.#id;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllPlayers() {
|
|
||||||
return this.#players.values().toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPlayer(player: Profile) {
|
|
||||||
return this.getAllPlayers().includes(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
removePlayer(player: Profile) {
|
|
||||||
if (!this.hasPlayer(player)) throw new Error(`Cannot remove player ${player.getId()} from instance ${this.#id} they are not in`);
|
|
||||||
this.#players.delete(player);
|
|
||||||
player.setInstance(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlayers() {
|
|
||||||
for (const player of this.#players.values()) player.getSocketHandler()?.sendNotification(PushNotificationId.SubscriptionUpdateGameSession, this.snapshot());
|
|
||||||
}
|
|
||||||
|
|
||||||
async addPlayer(player: Profile) {
|
|
||||||
const currentInstance = player.getInstance();
|
|
||||||
if (currentInstance && currentInstance.equalInstance(this)) return;
|
|
||||||
|
|
||||||
if (currentInstance) currentInstance.removePlayer(player);
|
|
||||||
|
|
||||||
if (!this.isFull) {
|
|
||||||
const instancePlayers = this.getAllPlayers();
|
|
||||||
const profileExport = await player.export();
|
|
||||||
log.i(`Player ${player.getId()} "${profileExport?.displayName}" went to '${this.name}' with ${instancePlayers.length} other players`);
|
|
||||||
|
|
||||||
this.#players.add(player);
|
|
||||||
player.setInstance(this);
|
|
||||||
|
|
||||||
const pres = await Presence.get(player);
|
|
||||||
pres.update();
|
|
||||||
|
|
||||||
const room = await new RoomFactory({ id: this.roomId }).init();
|
|
||||||
await room?.addVisit();
|
|
||||||
|
|
||||||
// move some of this to a dedicated "onPlayerMove" function
|
|
||||||
} else log.w(`Instance ${this.roomInstanceId} is full. Cannot add player ${player.getId()}`);
|
|
||||||
|
|
||||||
log.d(`Players in instance ${this.#id}: ${this.#players.values().toArray().map(prof => prof.getId()).join(',')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
get roomInstanceId() { return this.#id }
|
|
||||||
|
|
||||||
get roomId() { return this.#room ? this.#room?.Room.RoomId : 0 }
|
|
||||||
|
|
||||||
get subRoomId() { return this.#subroom ? this.#subroom?.RoomSceneId : 0 }
|
|
||||||
|
|
||||||
get location() { return this.#subroom ? this.#subroom?.RoomSceneLocationId : "" }
|
|
||||||
|
|
||||||
get dataBlob() { return this.#blob ? this.#blob : undefined }
|
|
||||||
set dataBlob(data) { this.#blob = data }
|
|
||||||
|
|
||||||
get eventId() { return this.#eventId }
|
|
||||||
|
|
||||||
get photonRegionId() { return config.public.photonRegionId }
|
|
||||||
|
|
||||||
get photonRoomId() { return `GC20200306-${this.#id}` }
|
|
||||||
|
|
||||||
get name() { return this.#name ? this.#name : "InstanceNameError" }
|
|
||||||
|
|
||||||
get maxCapacity() { return this.#subroom ? this.#subroom.MaxPlayers : 8 }
|
|
||||||
|
|
||||||
get isFull() { return this.#players.size >= this.maxCapacity }
|
|
||||||
|
|
||||||
get isPrivate() { return this.#priv ? this.#priv : false }
|
|
||||||
set isPrivate(data) { this.#priv = data }
|
|
||||||
|
|
||||||
get isInProgress() { return this.#inProgress ? this.#inProgress : false }
|
|
||||||
set isInProgress(data) { this.#inProgress = data }
|
|
||||||
|
|
||||||
snapshot() {
|
|
||||||
const inst: RoomInstance = {
|
|
||||||
roomInstanceId: this.roomInstanceId,
|
|
||||||
roomId: this.roomId,
|
|
||||||
subRoomId: this.subRoomId,
|
|
||||||
location: this.location,
|
|
||||||
dataBlob: this.dataBlob,
|
|
||||||
eventId: this.eventId,
|
|
||||||
photonRegionId: this.photonRegionId,
|
|
||||||
photonRoomId: this.photonRoomId,
|
|
||||||
name: this.name,
|
|
||||||
maxCapacity: this.maxCapacity,
|
|
||||||
isFull: this.isFull,
|
|
||||||
isPrivate: this.isPrivate,
|
|
||||||
isInProgress: this.isInProgress
|
|
||||||
}
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
instanceSet.delete(this);
|
|
||||||
if (this.#players.size !== 0) for (const player of this.#players) player.getSocketHandler()?.sendNotification(PushNotificationId.Logout);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class InstancesBase {
|
class InstancesBase {
|
||||||
|
|
||||||
@@ -191,8 +50,9 @@ class InstancesBase {
|
|||||||
else return null;
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllInstances() {
|
getAllInstances(asSet: boolean | undefined = false) {
|
||||||
return new Set([...instanceSet.values().toArray()]);
|
if (!asSet) return new Set([...instanceSet.values().toArray()]);
|
||||||
|
else return instanceSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllRoomInstances(roomId: number) {
|
getAllRoomInstances(roomId: number) {
|
||||||
|
|||||||
@@ -17,10 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SettingKey } from "../content/settings.ts";
|
import { SettingKey } from "../content/settings.ts";
|
||||||
import { Profile } from "../profiles.ts";
|
import type { Profile } from "../profile/base/profiles.ts";
|
||||||
import { DeviceClass, PlayerStatusVisibility, RoomInstance, VRMovementMode } from "./types.ts";
|
import { DeviceClass, PlayerStatusVisibility, RoomInstance, VRMovementMode } from "./types.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { Instance } from "./instances.ts";
|
import { Instance } from "./Instance.ts";
|
||||||
|
import { RoomUpdatedEvent } from "../content/rooms/RoomEvents.ts";
|
||||||
|
import Server from "../server/server.ts";
|
||||||
|
import { PushNotificationId } from "../../socket/types.ts";
|
||||||
|
|
||||||
const log = new Logging("Presence");
|
const log = new Logging("Presence");
|
||||||
|
|
||||||
@@ -57,6 +60,7 @@ class PlayerPresence {
|
|||||||
this.updateOffline();
|
this.updateOffline();
|
||||||
}, 80000);
|
}, 80000);
|
||||||
|
|
||||||
|
Server.on('room.updated', this.#roomUpdatedEventCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
offline: boolean;
|
offline: boolean;
|
||||||
@@ -69,6 +73,20 @@ class PlayerPresence {
|
|||||||
|
|
||||||
lastSeen: Date;
|
lastSeen: Date;
|
||||||
|
|
||||||
|
#roomUpdatedEventCallback = (ev: RoomUpdatedEvent) => {
|
||||||
|
//log.d(`Room ${ev.room.RoomId} updated, notifying client.`);
|
||||||
|
ev.room.export().then(roomDetails => {
|
||||||
|
log.d(`${this.roomInstance?.roomId} == ${ev.room.RoomId}, P:${this.playerId}`);
|
||||||
|
if (this.roomInstance?.roomId == ev.room.RoomId) {
|
||||||
|
const socket = this.getProfile().getSocketHandler();
|
||||||
|
if (!socket) return;
|
||||||
|
|
||||||
|
socket.sendNotification(PushNotificationId.SubscriptionUpdateRoom, roomDetails);
|
||||||
|
socket.presenceUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateStatusVisibility() {
|
async updateStatusVisibility() {
|
||||||
const PlayerStatusVisibilityEnum = z.nativeEnum(PlayerStatusVisibility);
|
const PlayerStatusVisibilityEnum = z.nativeEnum(PlayerStatusVisibility);
|
||||||
|
|
||||||
@@ -122,6 +140,16 @@ class PlayerPresence {
|
|||||||
this.lastSeen = new Date();
|
this.lastSeen = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProfile() {
|
||||||
|
return this.#profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
presence.delete(this);
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
Server.off('room.updated', this.#roomUpdatedEventCallback);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const presence: Set<PlayerPresence> = new Set();
|
const presence: Set<PlayerPresence> = new Set();
|
||||||
@@ -164,10 +192,8 @@ class PresenceBase {
|
|||||||
|
|
||||||
deleteDeadPresences() {
|
deleteDeadPresences() {
|
||||||
for (const pres of presence.values())
|
for (const pres of presence.values())
|
||||||
if (Math.round(new Date().getTime() / 1000) - Math.round(pres.lastSeen.getTime() / 1000) >= 60) {
|
if (Math.round(new Date().getTime() / 1000) - Math.round(pres.lastSeen.getTime() / 1000) >= 60)
|
||||||
presence.delete(pres);
|
pres.destroy();
|
||||||
clearInterval(pres.intervalId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,40 +15,9 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
|
import { RoomDataTypes } from "../content/rooms/base/DataTypes.ts";
|
||||||
import { Profile } from "../profiles.ts";
|
import type { Profile } from "../profile/base/profiles.ts";
|
||||||
|
import { PhotonRegionCodeString, PhotonRegionCodeNumber } from "./PhotonTypes.ts";
|
||||||
export enum PhotonRegionCodeString {
|
|
||||||
Europe = "eu",
|
|
||||||
UnitedStates = "us",
|
|
||||||
Asia = "asia",
|
|
||||||
Japan = "jp",
|
|
||||||
Australia = "au",
|
|
||||||
UnitedStates_West = "usw",
|
|
||||||
SouthAmerica = "sa",
|
|
||||||
CanadaEast = "cae",
|
|
||||||
SouthKorea = "kr",
|
|
||||||
India = "@in",
|
|
||||||
Russia = "ru",
|
|
||||||
RussiaEast = "rue",
|
|
||||||
None = "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PhotonRegionCodeNumber {
|
|
||||||
eu,
|
|
||||||
us,
|
|
||||||
asia,
|
|
||||||
jp,
|
|
||||||
au = 5,
|
|
||||||
usw,
|
|
||||||
sa,
|
|
||||||
cae,
|
|
||||||
kr,
|
|
||||||
"@in",
|
|
||||||
ru,
|
|
||||||
rue,
|
|
||||||
none = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RoomInstance {
|
export interface RoomInstance {
|
||||||
|
|
||||||
|
|||||||
44
src/data/profile/ProfileAuth.ts
Normal file
44
src/data/profile/ProfileAuth.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import { Config } from "../../config/config.ts";
|
||||||
|
import { Redis } from "../../db.ts";
|
||||||
|
import type { ProfileTokenFormat } from "../auth/TokenBaseFormat.ts";
|
||||||
|
import { AuthType } from "../UserTypes.ts";
|
||||||
|
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
||||||
|
import * as JsonWebToken from "@gz/jwt";
|
||||||
|
|
||||||
|
const config = Config.getConfig();
|
||||||
|
|
||||||
|
export class ProfileAuth extends ProfileContentManager {
|
||||||
|
|
||||||
|
async getToken() {
|
||||||
|
const payload: ProfileTokenFormat = {
|
||||||
|
iss: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}`,
|
||||||
|
sub: this.profile.getId(),
|
||||||
|
role: (await this.getIsOperator()) ? 'developer' : 'user',
|
||||||
|
exp: Math.round(Date.now() / 1000) + Math.round(config.auth.timeout * 60 * 60),
|
||||||
|
typ: AuthType.Game,
|
||||||
|
};
|
||||||
|
return await JsonWebToken.encode(payload, config.auth.secret, { algorithm: "HS512" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIsOperator() {
|
||||||
|
return (await Redis.Database.sismember(Redis.buildKey(Redis.KeyGroups.Operators), this.profile.getId().toString())) >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ export class ProfileAvatarManager extends ProfileContentManager {
|
|||||||
|
|
||||||
#rootKey = Redis.buildKey(
|
#rootKey = Redis.buildKey(
|
||||||
Redis.KeyGroups.Profiles.Root,
|
Redis.KeyGroups.Profiles.Root,
|
||||||
this.profileId.toString(),
|
this.profile.getId().toString(),
|
||||||
Redis.KeyGroups.Profiles.Avatar.Root
|
Redis.KeyGroups.Profiles.Avatar.Root
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,18 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import type { Profile } from "./profiles.ts";
|
||||||
|
|
||||||
export class ProfileContentManager {
|
export class ProfileContentManager {
|
||||||
|
|
||||||
constructor(profileId: number) {
|
constructor(profile: Profile) {
|
||||||
this.profileId = profileId;
|
this.profile = profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
onProfileInit() {
|
onProfileInit() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
profileId: number;
|
profile: Profile;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,28 +15,23 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { Redis } from "../db.ts";
|
import { Redis } from "../../../db.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import Dictionary from "./usernames.ts";
|
import Dictionary from "../../usernames.ts";
|
||||||
import { Config } from "../config.ts";
|
import { DeviceClass, VRMovementMode } from "../../live/types.ts";
|
||||||
import { AuthType } from "./users.ts";
|
import { SettingKey } from "../../content/settings.ts";
|
||||||
import * as JsonWebToken from "@gz/jwt";
|
|
||||||
import { TokenBaseFormat } from "../apiutils.ts";
|
|
||||||
import { DeviceClass, VRMovementMode } from "./live/types.ts";
|
|
||||||
import { SettingKey } from "./content/settings.ts";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SignalRSocketHandler } from "../socket/socket.ts";
|
import { SignalRSocketHandler } from "../../../socket/socket.ts";
|
||||||
import { ProfileSettingsManager } from "./profile/settings.ts";
|
import { ProfileSettingsManager } from "../settings.ts";
|
||||||
import { ProfileProgressionManager } from "./profile/progression.ts";
|
import { ProfileProgressionManager } from "../progression.ts";
|
||||||
import { ProfileReputationManager } from "./profile/reputation.ts";
|
import { ProfileReputationManager } from "../reputation.ts";
|
||||||
import { ProfileRelationshipManager } from "./profile/relationships.ts";
|
import { ProfileRelationshipManager } from "../relationships.ts";
|
||||||
import { ProfileAvatarManager } from "./profile/avatar.ts";
|
import { ProfileAvatarManager } from "../avatar.ts";
|
||||||
import { EventManager } from "./baseevent.ts";
|
import type { ProfileUpdatedEvent } from "../../profileevents.ts";
|
||||||
import { ProfileEvents, ProfileUpdatedEvent } from "./profileevents.ts";
|
import { Instance } from "../../live/Instance.ts";
|
||||||
import { Instance } from "./live/instances.ts";
|
import { ProfileRoomsManager } from "../rooms.ts";
|
||||||
import { ProfileRoomsManager } from "./profile/rooms.ts";
|
import { type ServerBase } from "../../server/server.ts";
|
||||||
|
import { ProfileAuth as ProfileAuthManager } from "../ProfileAuth.ts";
|
||||||
const config = Config.getConfig();
|
|
||||||
|
|
||||||
const log = new Logging("Profiles");
|
const log = new Logging("Profiles");
|
||||||
|
|
||||||
@@ -63,15 +58,10 @@ export interface SelfAccountExport extends AccountExport {
|
|||||||
juniorState?: number,
|
juniorState?: number,
|
||||||
parentAccountId?: number
|
parentAccountId?: number
|
||||||
}
|
}
|
||||||
export interface ProfileTokenFormat extends TokenBaseFormat {
|
|
||||||
sub: number;
|
|
||||||
role: "developer" | "user";
|
|
||||||
typ: AuthType.Game;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reservedIds = [1, 2];
|
const reservedIds = [1, 2];
|
||||||
|
|
||||||
class Profile extends EventManager {
|
class Profile {
|
||||||
static async exists(id: number) {
|
static async exists(id: number) {
|
||||||
return (await Redis.Database.exists(
|
return (await Redis.Database.exists(
|
||||||
Redis.buildKey(
|
Redis.buildKey(
|
||||||
@@ -202,26 +192,25 @@ class Profile extends EventManager {
|
|||||||
Relationships: ProfileRelationshipManager;
|
Relationships: ProfileRelationshipManager;
|
||||||
Avatar: ProfileAvatarManager;
|
Avatar: ProfileAvatarManager;
|
||||||
Rooms: ProfileRoomsManager;
|
Rooms: ProfileRoomsManager;
|
||||||
|
Auth: ProfileAuthManager;
|
||||||
|
|
||||||
constructor(id: number) {
|
constructor(id: number) {
|
||||||
super();
|
|
||||||
|
|
||||||
this.#id = id;
|
this.#id = id;
|
||||||
|
|
||||||
this.Settings = new ProfileSettingsManager(this.#id);
|
this.Settings = new ProfileSettingsManager(this);
|
||||||
this.Progression = new ProfileProgressionManager(this.#id);
|
this.Progression = new ProfileProgressionManager(this);
|
||||||
this.Reputation = new ProfileReputationManager(this.#id);
|
this.Reputation = new ProfileReputationManager(this);
|
||||||
this.Relationships = new ProfileRelationshipManager(this.#id);
|
this.Relationships = new ProfileRelationshipManager(this);
|
||||||
this.Avatar = new ProfileAvatarManager(this.#id);
|
this.Avatar = new ProfileAvatarManager(this);
|
||||||
this.Rooms = new ProfileRoomsManager(this.#id);
|
this.Rooms = new ProfileRoomsManager(this);
|
||||||
|
this.Auth = new ProfileAuthManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
#emitProfileUpdated() {
|
#emitProfileUpdated() {
|
||||||
const ev: ProfileUpdatedEvent = {
|
const ev: ProfileUpdatedEvent = {
|
||||||
time: new Date(),
|
|
||||||
profile: this
|
profile: this
|
||||||
}
|
}
|
||||||
this.emit(ProfileEvents.BaseUpdated, ev);
|
if (Server) Server.emit('profile.updated', ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInstance(instance: Instance | null) {
|
setInstance(instance: Instance | null) {
|
||||||
@@ -236,10 +225,6 @@ class Profile extends EventManager {
|
|||||||
return this.#id;
|
return this.#id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIsOperator() {
|
|
||||||
return (await Redis.Database.sismember(Redis.buildKey(Redis.KeyGroups.Operators), this.#id.toString())) >= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBio() {
|
async getBio() {
|
||||||
const bio = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Bio));
|
const bio = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Bio));
|
||||||
if (!bio) return "";
|
if (!bio) return "";
|
||||||
@@ -330,26 +315,20 @@ class Profile extends EventManager {
|
|||||||
getSocketHandler() {
|
getSocketHandler() {
|
||||||
return this.#socket;
|
return this.#socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get, set instance
|
|
||||||
// this.#instance: RoomInstance
|
|
||||||
|
|
||||||
async getToken() {
|
|
||||||
const payload: ProfileTokenFormat = {
|
|
||||||
iss: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}`,
|
|
||||||
sub: this.#id,
|
|
||||||
role: (await this.getIsOperator()) ? 'developer' : 'user',
|
|
||||||
exp: Math.round(Date.now() / 1000) + Math.round(config.auth.timeout * 60 * 60),
|
|
||||||
typ: AuthType.Game,
|
|
||||||
};
|
|
||||||
return await JsonWebToken.encode(payload, config.auth.secret, { algorithm: "HS512" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const profiles: Map<number, Profile> = new Map()
|
const profiles: Map<number, Profile> = new Map();
|
||||||
|
|
||||||
|
// surely this can be fixed
|
||||||
|
let Server: ServerBase | undefined;
|
||||||
|
|
||||||
export class UnifiedProfileBase {
|
export class UnifiedProfileBase {
|
||||||
|
|
||||||
|
constructor(server: ServerBase) {
|
||||||
|
Server = server;
|
||||||
|
}
|
||||||
|
|
||||||
get(id: number) {
|
get(id: number) {
|
||||||
let profile = profiles.get(id);
|
let profile = profiles.get(id);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
@@ -357,7 +336,8 @@ export class UnifiedProfileBase {
|
|||||||
const inst = new Profile(id);
|
const inst = new Profile(id);
|
||||||
profiles.set(id, inst);
|
profiles.set(id, inst);
|
||||||
profile = inst;
|
profile = inst;
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
log.e(`Could not fetch profile: ${(err as Error).stack}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +345,10 @@ export class UnifiedProfileBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(options: ProfileInitOptions) {
|
async create(options: ProfileInitOptions) {
|
||||||
return await Profile.init(options);
|
const profile = await Profile.init(options);
|
||||||
|
if (!profile) return null;
|
||||||
|
profiles.set(profile.getId(), profile);
|
||||||
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
async exists(id: number) {
|
async exists(id: number) {
|
||||||
@@ -16,13 +16,13 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { GameConfigs } from "../config.ts";
|
|
||||||
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
||||||
import { Redis } from "../../db.ts";
|
import { Redis } from "../../db.ts";
|
||||||
|
import { getPublicConfig } from "../config/PublicConfig.ts";
|
||||||
|
|
||||||
const log = new Logging("ProfileProgression");
|
const log = new Logging("ProfileProgression");
|
||||||
|
|
||||||
const config = GameConfigs.getConfig();
|
const config = getPublicConfig();
|
||||||
|
|
||||||
interface PlayerProgressionExport {
|
interface PlayerProgressionExport {
|
||||||
PlayerId: number,
|
PlayerId: number,
|
||||||
@@ -34,7 +34,7 @@ export class ProfileProgressionManager extends ProfileContentManager {
|
|||||||
|
|
||||||
async export() {
|
async export() {
|
||||||
const ex: PlayerProgressionExport = {
|
const ex: PlayerProgressionExport = {
|
||||||
PlayerId: this.profileId,
|
PlayerId: this.profile.getId(),
|
||||||
Level: await this.getLevel(),
|
Level: await this.getLevel(),
|
||||||
XP: await this.getXp()
|
XP: await this.getXp()
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ export class ProfileProgressionManager extends ProfileContentManager {
|
|||||||
* @returns The new # of XP
|
* @returns The new # of XP
|
||||||
*/
|
*/
|
||||||
async setXp(xp: number) {
|
async setXp(xp: number) {
|
||||||
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Xp), xp.toString());
|
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Xp), xp.toString());
|
||||||
return xp;
|
return xp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +85,12 @@ export class ProfileProgressionManager extends ProfileContentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getXp() {
|
async getXp() {
|
||||||
let data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Xp));
|
let data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Xp));
|
||||||
if (data == null) data = (await this.setXp(0)).toString();
|
if (data == null) data = (await this.setXp(0)).toString();
|
||||||
|
|
||||||
const parsedData = parseInt(data);
|
const parsedData = parseInt(data);
|
||||||
if (isNaN(parsedData)) {
|
if (isNaN(parsedData)) {
|
||||||
log.w(`Parsed xp data for ${this.profileId} is NaN!`);
|
log.w(`Parsed xp data for ${this.profile.getId()} is NaN!`);
|
||||||
const one = config?.LevelProgressionMaps[1];
|
const one = config?.LevelProgressionMaps[1];
|
||||||
if (typeof one == 'undefined' && !one) return 0; // fallback since progression data is required
|
if (typeof one == 'undefined' && !one) return 0; // fallback since progression data is required
|
||||||
else return one.RequiredXp;
|
else return one.RequiredXp;
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { Redis } from "../../db.ts";
|
import { Redis } from "../../db.ts";
|
||||||
import { Profile } from "../profiles.ts";
|
import type { Profile } from "./base/profiles.ts";
|
||||||
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
||||||
import Server from "../server.ts";
|
//import Server from "../server.ts"; // see circular dep comment at bottom
|
||||||
|
|
||||||
enum RelationshipType {
|
enum RelationshipType {
|
||||||
None,
|
None,
|
||||||
@@ -64,7 +64,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
|||||||
|
|
||||||
#rootKey = Redis.buildKey(
|
#rootKey = Redis.buildKey(
|
||||||
Redis.KeyGroups.Profiles.Root,
|
Redis.KeyGroups.Profiles.Root,
|
||||||
this.profileId.toString(),
|
this.profile.getId().toString(),
|
||||||
Redis.KeyGroups.Profiles.Relationships.Root
|
Redis.KeyGroups.Profiles.Relationships.Root
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -144,8 +144,8 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
|||||||
async #clearAssociationWithRemote(remoteProfileId: number) {
|
async #clearAssociationWithRemote(remoteProfileId: number) {
|
||||||
const remoteRootKey = this.#createRemoteRootKey(remoteProfileId);
|
const remoteRootKey = this.#createRemoteRootKey(remoteProfileId);
|
||||||
await Redis.Database.srem(this.#incomingFriends, remoteProfileId);
|
await Redis.Database.srem(this.#incomingFriends, remoteProfileId);
|
||||||
await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.OutgoingFriendRequests), this.profileId);
|
await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.OutgoingFriendRequests), this.profile.getId());
|
||||||
await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId);
|
await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profile.getId());
|
||||||
await Redis.Database.srem(this.#outgoingFriends, remoteProfileId);
|
await Redis.Database.srem(this.#outgoingFriends, remoteProfileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
|||||||
const remoteRootKey = this.#createRemoteRootKey(remoteProfileId);
|
const remoteRootKey = this.#createRemoteRootKey(remoteProfileId);
|
||||||
|
|
||||||
await Redis.Database.sadd(this.#friendsKey, remoteProfileId);
|
await Redis.Database.sadd(this.#friendsKey, remoteProfileId);
|
||||||
await Redis.Database.sadd(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.Friends), this.profileId);
|
await Redis.Database.sadd(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.Friends), this.profile.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
async denyRequest(remoteProfileId: number) {
|
async denyRequest(remoteProfileId: number) {
|
||||||
@@ -182,7 +182,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
|||||||
const remoteKey = this.#createRemoteRootKey(remoteProfileId);
|
const remoteKey = this.#createRemoteRootKey(remoteProfileId);
|
||||||
|
|
||||||
const localMuted = (await this.getAllMuted()).includes(remoteProfileId);
|
const localMuted = (await this.getAllMuted()).includes(remoteProfileId);
|
||||||
const remoteMuted = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Muted), this.profileId);
|
const remoteMuted = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Muted), this.profile.getId());
|
||||||
|
|
||||||
if (localMuted && remoteMuted) return ReciprocalStatus.Mutual;
|
if (localMuted && remoteMuted) return ReciprocalStatus.Mutual;
|
||||||
else if (localMuted) return ReciprocalStatus.Local;
|
else if (localMuted) return ReciprocalStatus.Local;
|
||||||
@@ -194,7 +194,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
|||||||
const remoteKey = this.#createRemoteRootKey(remoteProfileId);
|
const remoteKey = this.#createRemoteRootKey(remoteProfileId);
|
||||||
|
|
||||||
const localIgnored = (await this.getAllMuted()).includes(remoteProfileId);
|
const localIgnored = (await this.getAllMuted()).includes(remoteProfileId);
|
||||||
const remoteIgnored = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Ignoring), this.profileId);
|
const remoteIgnored = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Ignoring), this.profile.getId());
|
||||||
|
|
||||||
if (localIgnored && remoteIgnored) return ReciprocalStatus.Mutual;
|
if (localIgnored && remoteIgnored) return ReciprocalStatus.Mutual;
|
||||||
else if (localIgnored) return ReciprocalStatus.Local;
|
else if (localIgnored) return ReciprocalStatus.Local;
|
||||||
@@ -213,22 +213,23 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendPlayerFriendRequest(player: Profile) {
|
async sendPlayerFriendRequest(player: Profile) {
|
||||||
await Redis.Database.sadd(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId);
|
await Redis.Database.sadd(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profile.getId());
|
||||||
await Redis.Database.sadd(this.#outgoingFriends, player.getId());
|
await Redis.Database.sadd(this.#outgoingFriends, player.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
async removePlayerFriendRequest(player: Profile) {
|
async removePlayerFriendRequest(player: Profile) {
|
||||||
await Redis.Database.srem(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId);
|
await Redis.Database.srem(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profile.getId());
|
||||||
await Redis.Database.srem(this.#outgoingFriends, player.getId());
|
await Redis.Database.srem(this.#outgoingFriends, player.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
async ignoreAllAssociatedPlatformUsers(platformid: string) {
|
async ignoreAllAssociatedPlatformUsers(_platformid: string) {
|
||||||
const ids = (await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid))).map(val => parseInt(val)).filter(val => !isNaN(val));
|
/*const ids = (await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid))).map(val => parseInt(val)).filter(val => !isNaN(val));
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const profile = Server.UnifiedProfile.get(id);
|
const profile = Server.UnifiedProfile.get(id);
|
||||||
if (!profile) continue;
|
if (!profile) continue;
|
||||||
this.setPlayerIgnored(profile);
|
this.setPlayerIgnored(profile);
|
||||||
}
|
}*/
|
||||||
|
// circular dep here. this will do nothing for now.
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ export class ProfileReputationManager extends ProfileContentManager {
|
|||||||
// deno-lint-ignore require-await
|
// deno-lint-ignore require-await
|
||||||
async getReputation() { // async temporary
|
async getReputation() { // async temporary
|
||||||
return {
|
return {
|
||||||
AccountId: this.profileId,
|
AccountId: this.profile.getId(),
|
||||||
Noteriety: 0.0,
|
Noteriety: 0.0,
|
||||||
CheerGeneral: 0,
|
CheerGeneral: 0,
|
||||||
CheerHelpful: 0,
|
CheerHelpful: 0,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class ProfileRoomsManager extends ProfileContentManager {
|
|||||||
|
|
||||||
#rootKey = Redis.buildKey(
|
#rootKey = Redis.buildKey(
|
||||||
Redis.KeyGroups.Profiles.Root,
|
Redis.KeyGroups.Profiles.Root,
|
||||||
this.profileId.toString(),
|
this.profile.getId().toString(),
|
||||||
Redis.KeyGroups.Profiles.Rooms
|
Redis.KeyGroups.Profiles.Rooms
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -31,30 +31,30 @@ export class ProfileSettingsManager extends ProfileContentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSettings() {
|
async getSettings() {
|
||||||
const settings = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings));
|
const settings = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings));
|
||||||
const returnSettings: Setting[] = [];
|
const returnSettings: Setting[] = [];
|
||||||
for (const key of Object.keys(settings)) returnSettings.push({ Key: key, Value: settings[key] });
|
for (const key of Object.keys(settings)) returnSettings.push({ Key: key, Value: settings[key] });
|
||||||
return returnSettings;
|
return returnSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSetting(key: SettingKey) {
|
async getSetting(key: SettingKey) {
|
||||||
return await Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key);
|
return await Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings), key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSetting(key: SettingKey, value: string | number) {
|
async setSetting(key: SettingKey, value: string | number) {
|
||||||
await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key, value);
|
await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings), key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSettingRaw(key: string, value: string | number) {
|
async setSettingRaw(key: string, value: string | number) {
|
||||||
await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key, value);
|
await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings), key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delSetting(key: SettingKey) {
|
async delSetting(key: SettingKey) {
|
||||||
await Redis.Database.hdel(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key);
|
await Redis.Database.hdel(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings), key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delAllSettings() {
|
async delAllSettings() {
|
||||||
await Redis.Database.del(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings));
|
await Redis.Database.del(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,13 +15,8 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { Event } from "./baseevent.ts";
|
import { Profile } from "./profile/base/profiles.ts";
|
||||||
import { Profile } from "./profiles.ts";
|
|
||||||
|
|
||||||
export enum ProfileEvents {
|
export interface ProfileUpdatedEvent {
|
||||||
BaseUpdated = "profile.updated"
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProfileUpdatedEvent extends Event {
|
|
||||||
profile: Profile
|
profile: Profile
|
||||||
}
|
}
|
||||||
29
src/data/server/server.ts
Normal file
29
src/data/server/server.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import { EventManager } from "../baseevent.ts";
|
||||||
|
import { CDNBase } from "../content/cdn.ts";
|
||||||
|
import { UnifiedProfileBase } from "../profile/base/profiles.ts";
|
||||||
|
import { ServerEvents } from "../server/serverevents.ts";
|
||||||
|
|
||||||
|
export class ServerBase extends EventManager<ServerEvents> {
|
||||||
|
CDN = new CDNBase();
|
||||||
|
UnifiedProfile = new UnifiedProfileBase(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Server = new ServerBase();
|
||||||
|
export default Server;
|
||||||
25
src/data/server/serverevents.ts
Normal file
25
src/data/server/serverevents.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import type { RoomUpdatedEvent, SubroomUpdatedEvent } from "../content/rooms/RoomEvents.ts";
|
||||||
|
import { ProfileUpdatedEvent } from "../profileevents.ts";
|
||||||
|
|
||||||
|
export interface ServerEvents {
|
||||||
|
'profile.updated': ProfileUpdatedEvent
|
||||||
|
'room.subroom.updated': SubroomUpdatedEvent
|
||||||
|
'room.updated': RoomUpdatedEvent
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
|
||||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
|
||||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|
||||||
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
|
||||||
import { Config } from "../config.ts";
|
|
||||||
|
|
||||||
const log = new Logging("Steam");
|
|
||||||
|
|
||||||
const config = Config.getConfig();
|
|
||||||
|
|
||||||
interface AuthenticateUserTicketSuccess {
|
|
||||||
result: 'OK',
|
|
||||||
steamid: string,
|
|
||||||
ownersteamid: string,
|
|
||||||
vacbanned: boolean,
|
|
||||||
publisherbanned: boolean
|
|
||||||
}
|
|
||||||
interface AuthenticateUserTicketError {
|
|
||||||
errorcode: number,
|
|
||||||
errordesc: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SteamRes {
|
|
||||||
response: {
|
|
||||||
error?: AuthenticateUserTicketError,
|
|
||||||
params?: AuthenticateUserTicketSuccess
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function AuthenticateUserTicket(ticket: string, userid: string) {
|
|
||||||
if (!config.auth.steamkey) return true; // always authenticate if no steam API key was found
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('key', config.auth.steamkey);
|
|
||||||
params.append('appid', "471710");
|
|
||||||
params.append('ticket', ticket);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`https://api.steampowered.com/ISteamUserAuth/AuthenticateUserTicket/v1?${params}`);
|
|
||||||
const resjson = (await res.json()) as SteamRes;
|
|
||||||
|
|
||||||
if (resjson.response.error) {
|
|
||||||
log.w(`Steam Authentication failed: (${resjson.response.error.errorcode}) ${resjson.response.error.errordesc}`);
|
|
||||||
|
|
||||||
// add more error codes later if needed
|
|
||||||
const conditions = [
|
|
||||||
resjson.response.error.errorcode == 100
|
|
||||||
].includes(true);
|
|
||||||
if (conditions) log.w('This error indicates a client problem.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.d(JSON.stringify(resjson.response));
|
|
||||||
if (resjson.response.params) return resjson.response.params.steamid === userid && resjson.response.params.ownersteamid === userid;
|
|
||||||
else {
|
|
||||||
log.w("Steam Authentication failed: Steam response did not contain params or error! This should never be logged!");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
log.w(`Steam Authentication failed: ${(err as Error).message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export * as Steam from "./steam.ts";
|
|
||||||
55
src/data/steam/SteamAuthTypes.ts
Normal file
55
src/data/steam/SteamAuthTypes.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
interface AuthenticateUserTicketSuccess {
|
||||||
|
result: 'OK';
|
||||||
|
steamid: string;
|
||||||
|
ownersteamid: string;
|
||||||
|
vacbanned: boolean;
|
||||||
|
publisherbanned: boolean;
|
||||||
|
}
|
||||||
|
interface AuthenticateUserTicketError {
|
||||||
|
errorcode: number;
|
||||||
|
errordesc: string;
|
||||||
|
}
|
||||||
|
export interface SteamAuthRes {
|
||||||
|
response: {
|
||||||
|
error?: AuthenticateUserTicketError;
|
||||||
|
params?: AuthenticateUserTicketSuccess;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SteamAuthResult {
|
||||||
|
Success,
|
||||||
|
Failure,
|
||||||
|
NotConfigured
|
||||||
|
}
|
||||||
|
interface SteamAuthBase {
|
||||||
|
valid: SteamAuthResult;
|
||||||
|
}
|
||||||
|
interface SteamAuthSuccess extends SteamAuthBase {
|
||||||
|
valid: SteamAuthResult.Success;
|
||||||
|
res: AuthenticateUserTicketSuccess;
|
||||||
|
}
|
||||||
|
interface SteamAuthFailure extends SteamAuthBase {
|
||||||
|
valid: SteamAuthResult.Failure;
|
||||||
|
res: AuthenticateUserTicketError;
|
||||||
|
}
|
||||||
|
interface SteamAuthNotConfigured extends SteamAuthBase {
|
||||||
|
valid: SteamAuthResult.NotConfigured;
|
||||||
|
}
|
||||||
|
export type SteamAuth = SteamAuthSuccess | SteamAuthFailure | SteamAuthNotConfigured;
|
||||||
34
src/data/steam/SteamCommonTypes.ts
Normal file
34
src/data/steam/SteamCommonTypes.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import type { PersonaState, CommunityVisibilityState } from "./steam.ts";
|
||||||
|
|
||||||
|
export interface SteamPlayer {
|
||||||
|
steamid: string;
|
||||||
|
personaname: string;
|
||||||
|
profileurl: string;
|
||||||
|
avatar: string;
|
||||||
|
avatarmedium: string;
|
||||||
|
avatarfull: string;
|
||||||
|
personastate: PersonaState;
|
||||||
|
communityvisibilitystate: CommunityVisibilityState;
|
||||||
|
profilestate?: 1;
|
||||||
|
lastlogoff?: number;
|
||||||
|
commentpermission?: 1;
|
||||||
|
loccountrycode?: string; // undocumented but is seen in API responses - may or may not be undefined
|
||||||
|
locstatecode?: string; // undocumented but is seen in API responses - may or may not be undefined
|
||||||
|
}
|
||||||
111
src/data/steam/steam.ts
Normal file
111
src/data/steam/steam.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
import { Config } from "../../config/config.ts";
|
||||||
|
import { SteamAuth, SteamAuthResult, SteamAuthRes } from "./SteamAuthTypes.ts";
|
||||||
|
import { SteamPlayer } from "./SteamCommonTypes.ts";
|
||||||
|
|
||||||
|
const log = new Logging("Steam");
|
||||||
|
|
||||||
|
const config = Config.getConfig();
|
||||||
|
|
||||||
|
function buildSteamUrl(steaminterface: string, endpoint: string) {
|
||||||
|
return `https://api.steampowered.com/${steaminterface}/${endpoint}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PersonaState {
|
||||||
|
Offline,
|
||||||
|
Online,
|
||||||
|
Busy,
|
||||||
|
Away,
|
||||||
|
Snooze,
|
||||||
|
LookingToTrade,
|
||||||
|
LookingToPlay
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CommunityVisibilityState {
|
||||||
|
NotVisible,
|
||||||
|
PubliclyVisible = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
class SteamBase {
|
||||||
|
|
||||||
|
async GetPlayerSummaries(steamids: string[]) {
|
||||||
|
if (!config.auth.steamkey) return null;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('key', config.auth.steamkey);
|
||||||
|
params.append('steamids', steamids.join(','))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${buildSteamUrl('ISteamUser', 'GetPlayerSummaries/v2')}?${params}`);
|
||||||
|
if (res.status !== 200) return null;
|
||||||
|
|
||||||
|
const resjson = await res.json() as { response: { players: SteamPlayer[] } };
|
||||||
|
return resjson.response.players;
|
||||||
|
} catch (err) {
|
||||||
|
log.e(`Could not fetch Steam player summaries: ${(err as Error).stack}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async AuthenticateUserTicket(ticket: string, userid: string): Promise<SteamAuth> {
|
||||||
|
if (!config.auth.steamkey) return { valid: SteamAuthResult.NotConfigured }; // always authenticate if no steam API key was found
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('key', config.auth.steamkey);
|
||||||
|
params.append('appid', "471710");
|
||||||
|
params.append('ticket', ticket);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${buildSteamUrl('ISteamUserAuth', 'AuthenticateUserTicket/v1')}?${params}`);
|
||||||
|
const resjson = (await res.json()) as SteamAuthRes;
|
||||||
|
|
||||||
|
if (resjson.response.error) {
|
||||||
|
log.w(`Steam Authentication failed: (${resjson.response.error.errorcode}) ${resjson.response.error.errordesc}`);
|
||||||
|
|
||||||
|
// add more error codes later if needed
|
||||||
|
const conditions = [
|
||||||
|
resjson.response.error.errorcode == 100
|
||||||
|
].includes(true);
|
||||||
|
if (conditions) log.w('This error indicates a client problem.');
|
||||||
|
return { valid: SteamAuthResult.Failure, res: resjson.response.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.d(JSON.stringify(resjson.response));
|
||||||
|
if (resjson.response.params) {
|
||||||
|
// since rec room is not eligible for family sharing on Steam
|
||||||
|
const valid = resjson.response.params.steamid === userid && resjson.response.params.ownersteamid === userid;
|
||||||
|
if (valid) return { valid: SteamAuthResult.Success, res: resjson.response.params }
|
||||||
|
else throw new Error('`ownersteamid` is not equal to `steamid`, report me to GC devs!');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.w("Steam Authentication failed: Steam response did not contain params or error! This should never be logged!");
|
||||||
|
return { valid: SteamAuthResult.Failure, res: { errorcode: -1, errordesc: 'Steam response error' } };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.w(`Steam Authentication failed: ${(err as Error).message}`);
|
||||||
|
return { valid: SteamAuthResult.Failure, res: { errorcode: -1, errordesc: 'Steam response error' } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const Steam = new SteamBase();
|
||||||
|
|
||||||
|
export default Steam;
|
||||||
@@ -17,24 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { Redis } from "../db.ts";
|
import { Redis } from "../db.ts";
|
||||||
import * as JsonWebToken from "@gz/jwt";
|
import * as JsonWebToken from "@gz/jwt";
|
||||||
import { Config } from "../config.ts";
|
import { Config } from "../config/config.ts";
|
||||||
import { Profile } from "./profiles.ts";
|
import { Profile } from "./profile/base/profiles.ts";
|
||||||
import { TokenBaseFormat } from "../apiutils.ts";
|
import type { UserTokenFormat } from "./auth/TokenBaseFormat.ts";
|
||||||
|
import { UserInitOptions, AuthType } from "./UserTypes.ts";
|
||||||
type UserInitOptions = {
|
|
||||||
client_id: string;
|
|
||||||
pubkey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum AuthType {
|
|
||||||
Game,
|
|
||||||
Web,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserTokenFormat extends TokenBaseFormat {
|
|
||||||
sub: string;
|
|
||||||
typ: AuthType.Web;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
import * as Config from "./config.ts";
|
import * as Config from "./config/config.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import chalk from "npm:chalk@^5.3.0";
|
import chalk from "npm:chalk@^5.3.0";
|
||||||
|
|
||||||
@@ -110,6 +110,9 @@ export const KeyGroups = {
|
|||||||
Root: "room",
|
Root: "room",
|
||||||
PlayerDorms: "player-dormids"
|
PlayerDorms: "player-dormids"
|
||||||
},
|
},
|
||||||
|
Subrooms: {
|
||||||
|
Root: "subroom"
|
||||||
|
},
|
||||||
Operators: "operators",
|
Operators: "operators",
|
||||||
Users: {
|
Users: {
|
||||||
Root: "users",
|
Root: "users",
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
|
||||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
|
||||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|
||||||
|
|
||||||
import * as discord from "discord.js";
|
|
||||||
import { Config } from "./config.ts";
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
|
||||||
|
|
||||||
const log = new Logging("Discord");
|
|
||||||
|
|
||||||
const config = Config.getConfig();
|
|
||||||
if (typeof config == "undefined") {
|
|
||||||
log.e(`Cannot start: Discord configuration is unavailable`);
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const client = new discord.Client({
|
|
||||||
intents: [
|
|
||||||
discord.GatewayIntentBits.Guilds,
|
|
||||||
discord.GatewayIntentBits.GuildPresences,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
client.once(discord.Events.ClientReady, (client) => {
|
|
||||||
log.i(`Logged in to Discord as "${client.user.tag}"`);
|
|
||||||
client.user?.setActivity(config.public.motd, {
|
|
||||||
type: discord.ActivityType.Custom,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let shuttingDown = false;
|
|
||||||
Deno.addSignalListener("SIGINT", () => {
|
|
||||||
if (client.readyTimestamp == null) return;
|
|
||||||
if (shuttingDown) return;
|
|
||||||
shuttingDown = true;
|
|
||||||
log.n("Disconnecting from Discord");
|
|
||||||
client.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
export function login() {
|
|
||||||
if (config.discord?.token == Config.defaultConfig.discord?.token) {
|
|
||||||
log.i("Discord not configured, ignoring");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.i(`Creating Discord connection..`);
|
|
||||||
client.login(config.discord?.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
export * as Discord from "./discord.ts";
|
|
||||||
50
src/main.ts
50
src/main.ts
@@ -16,42 +16,44 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import * as Log from "@proxnet/undead-logging";
|
import * as Log from "@proxnet/undead-logging";
|
||||||
import * as Config from "./config.ts";
|
import * as Config from "./config/config.ts";
|
||||||
import { Database } from "./db.ts";
|
import { Database } from "./db.ts";
|
||||||
import { APIUtils, ProfileTokenSchema } from "./apiutils.ts";
|
import { APIUtils } from "./apiutils.ts";
|
||||||
import { Discord } from "./discord.ts";
|
import { ProfileTokenSchema } from "./data/auth/TokenSchema.ts";
|
||||||
import { generateRandomString } from "./apiutils.ts";
|
import { generateRandomString } from "./utils.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { decode } from "@gz/jwt";
|
import { decode } from "@gz/jwt";
|
||||||
import { ProfileTokenFormat } from "./data/profiles.ts";
|
|
||||||
import { SocketHandoff } from "./socket/handoff.ts";
|
import { SocketHandoff } from "./socket/handoff.ts";
|
||||||
import { SignalRSocketHandler } from "./socket/socket.ts";
|
import { SignalRSocketHandler } from "./socket/socket.ts";
|
||||||
import { GameConfigs } from "./data/config.ts";
|
import { GameConfigs } from "./data/config/GameConfigs.ts";
|
||||||
import { getVersion } from "./ver.ts";
|
import { getVersion } from "./ver.ts";
|
||||||
import Rooms from "./data/content/rooms.ts";
|
import RoomsMisc from "./data/content/rooms/Rooms.ts";
|
||||||
import { PushNotificationId } from "./socket/types.ts";
|
import { PushNotificationId } from "./socket/types.ts";
|
||||||
import Server from "./data/server.ts";
|
import Server from "./data/server/server.ts";
|
||||||
|
import type { ProfileTokenFormat } from "./data/auth/TokenBaseFormat.ts";
|
||||||
|
import { addWatchdogListener } from "./watchdog.ts";
|
||||||
|
|
||||||
const instanceId = generateRandomString(64);
|
const instanceId = generateRandomString(64);
|
||||||
|
|
||||||
const log = new Log.default("Main");
|
const log = new Log.default("Main");
|
||||||
|
|
||||||
|
Log.LoggingConfiguration.logTiming = Log.LogTiming.Deferred;
|
||||||
|
|
||||||
log.i(`Galvanic Corrosion '${await getVersion()}'`);
|
log.i(`Galvanic Corrosion '${await getVersion()}'`);
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
if (typeof config == "undefined") {
|
function exitError(err: string, code?: number) {
|
||||||
log.e("Cannot start: Configuration was not found.");
|
log.e(`Cannot start: ${err}`);
|
||||||
Deno.exit(5);
|
if (code) Deno.exit(code);
|
||||||
}
|
else Deno.exit(5);
|
||||||
if (config.auth.secret == Config.defaultConfig.auth.secret) {
|
|
||||||
log.e(`Cannot start: Auth secret is default. Please change 'auth.secret' in 'config.json'`);
|
|
||||||
Deno.exit(5);
|
|
||||||
}
|
|
||||||
if (config.public.serverId == Config.defaultConfig.public.serverId) {
|
|
||||||
log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`);
|
|
||||||
Deno.exit(5);
|
|
||||||
}
|
}
|
||||||
|
if (typeof config == "undefined")
|
||||||
|
exitError("Configuration was not found.");
|
||||||
|
if (config.auth.secret == Config.defaultConfig.auth.secret)
|
||||||
|
exitError(`Auth secret is default. Please change 'auth.secret' in 'config.json'`);
|
||||||
|
if (config.public.serverId == Config.defaultConfig.public.serverId)
|
||||||
|
exitError(`Server ID is default. Please change 'public.serverId' in 'config.json'`);
|
||||||
|
|
||||||
Log.MessageTypeVisibility.Network = config.logging.network;
|
Log.MessageTypeVisibility.Network = config.logging.network;
|
||||||
Log.MessageTypeVisibility.Debug = config.logging.debug;
|
Log.MessageTypeVisibility.Debug = config.logging.debug;
|
||||||
@@ -59,8 +61,7 @@ Log.MessageTypeVisibility.Debug = config.logging.debug;
|
|||||||
try {
|
try {
|
||||||
Database.connect();
|
Database.connect();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.e(`Cannot start: Redis could not be initialized. ${err}`);
|
exitError(`Redis could not be initialized. ${(err as Error).stack}`);
|
||||||
Deno.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -118,7 +119,7 @@ app.use((rq: express.Request, rs: express.Response) => {
|
|||||||
rs.json(APIUtils.genericResponseFormat(true, "Endpoint not found. Check your syntax and/or method."));
|
rs.json(APIUtils.genericResponseFormat(true, "Endpoint not found. Check your syntax and/or method."));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!(await Rooms.generateBuiltinRooms())) log.i(`Generated built-in rooms`);
|
if (!(await RoomsMisc.generateBuiltinRooms())) log.i(`Generated built-in rooms`);
|
||||||
await Server.CDN.ensureUserDirectory();
|
await Server.CDN.ensureUserDirectory();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -227,6 +228,7 @@ try {
|
|||||||
Deno.addSignalListener("SIGINT", () => {
|
Deno.addSignalListener("SIGINT", () => {
|
||||||
for (const socket of Server.UnifiedProfile.getAllSockets()) socket.sendNotification(PushNotificationId.ModerationQuitGame); // untested
|
for (const socket of Server.UnifiedProfile.getAllSockets()) socket.sendNotification(PushNotificationId.ModerationQuitGame); // untested
|
||||||
});
|
});
|
||||||
|
addWatchdogListener();
|
||||||
|
|
||||||
if (!(await Server.UnifiedProfile.existsByName("Coach"))) Server.UnifiedProfile.create({ username: "Coach", id: 1 }); // create Coach id 1 if they do not exist
|
if (!(await Server.UnifiedProfile.existsByName("Coach"))) Server.UnifiedProfile.create({ username: "Coach", id: 1 }); // create Coach id 1 if they do not exist
|
||||||
if (!(await Server.UnifiedProfile.existsByName("Server"))) Server.UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 if they do not exist
|
if (!(await Server.UnifiedProfile.existsByName("Server"))) Server.UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 if they do not exist
|
||||||
@@ -245,6 +247,4 @@ try {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.e(`Cannot start: Network could not be initalized. ${err}`);
|
log.e(`Cannot start: Network could not be initalized. ${err}`);
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Discord.login();
|
|
||||||
@@ -17,10 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { Profile } from "../../data/profiles.ts";
|
import { Profile } from "../../data/profile/base/profiles.ts";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import Server from "../../data/server.ts";
|
import Server from "../../data/server/server.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/account");
|
export const route = APIUtils.createRouter("/account");
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/parentalcontrol');
|
export const route = APIUtils.createRouter('/parentalcontrol');
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/announcement");
|
export const route = APIUtils.createRouter("/announcement");
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { AvatarSettings } from "../../data/profile/avatar.ts";
|
import { AvatarSettings } from "../../data/profile/avatar.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/challenge");
|
export const route = APIUtils.createRouter("/challenge");
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { ObjectiveType } from "../../data/objectives.ts";
|
import { ObjectiveType } from "../../data/content/ObjectiveTypes.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/checklist');
|
export const route = APIUtils.createRouter('/checklist');
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { Config } from "../../config.ts";
|
import { Config } from "../../config/config.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { GameConfigs } from "../../data/config.ts";
|
import { getPublicConfig } from "../../data/config/PublicConfig.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/config");
|
export const route = APIUtils.createRouter("/config");
|
||||||
|
|
||||||
const rateLimit = new APIUtils.RateLimiter();
|
const rateLimit = new APIUtils.RateLimiter();
|
||||||
|
|
||||||
route.router.get("/v2", rateLimit.middle(), (_rq, rs) => {
|
route.router.get("/v2", rateLimit.middle(), (_rq, rs) => {
|
||||||
const config = GameConfigs.getConfig();
|
const config = getPublicConfig();
|
||||||
if (config == null) rs.sendStatus(500);
|
if (config == null) rs.sendStatus(500);
|
||||||
else rs.json(config);
|
else rs.json(config);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/consumables');
|
export const route = APIUtils.createRouter('/consumables');
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/equipment');
|
export const route = APIUtils.createRouter('/equipment');
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { GameConfigs } from "../../data/config.ts";
|
import { GameConfigs } from "../../data/config/GameConfigs.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/gameconfigs");
|
export const route = APIUtils.createRouter("/gameconfigs");
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/images');
|
export const route = APIUtils.createRouter('/images');
|
||||||
|
|
||||||
@@ -27,4 +27,17 @@ route.router.get('/v2/named',
|
|||||||
|
|
||||||
APIUtils.emptyArrayResponse
|
APIUtils.emptyArrayResponse
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
route.router.post('/v4/uploadsaved',
|
||||||
|
|
||||||
|
APIUtils.Authentication,
|
||||||
|
APIUtils.AuthenticationType(AuthType.Game),
|
||||||
|
|
||||||
|
APIUtils.requestDebug,
|
||||||
|
|
||||||
|
(_rq, rs) => {
|
||||||
|
rs.json({ImageName:"notsaved.png"});
|
||||||
|
},
|
||||||
|
|
||||||
);
|
);
|
||||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/objectives');
|
export const route = APIUtils.createRouter('/objectives');
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import Server from "../../data/server.ts";
|
import Server from "../../data/server/server.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/playerReputation");
|
export const route = APIUtils.createRouter("/playerReputation");
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/playerevents');
|
export const route = APIUtils.createRouter('/playerevents');
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Server from "../../data/server.ts";
|
import Server from "../../data/server/server.ts";
|
||||||
|
|
||||||
const log = new Logging("ProgressionRoute");
|
const log = new Logging("ProgressionRoute");
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { Config } from "../../config.ts";
|
import { Config } from "../../config/config.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/quickPlay");
|
export const route = APIUtils.createRouter("/quickPlay");
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/relationships");
|
export const route = APIUtils.createRouter("/relationships");
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import Rooms from "../../data/content/rooms.ts";
|
import RoomsMisc from "../../data/content/rooms/Rooms.ts";
|
||||||
import { FactoryMode, RoomDataTypes, WriteMode } from "../../data/content/rooms/DataTypes.ts";
|
import { FactoryMode, RoomDataTypes, WriteMode } from "../../data/content/rooms/base/DataTypes.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { RoomFactory } from "../../data/content/rooms/RoomFactory.ts";
|
import { RoomFactory } from "../../data/content/rooms/RoomFactory.ts";
|
||||||
import { SubroomFactory } from "../../data/content/rooms/SubroomFactory.ts";
|
import { SubroomFactory } from "../../data/content/rooms/SubroomFactory.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
import Rooms from "../../data/content/rooms/base/RoomsBase.ts";
|
||||||
|
|
||||||
const log = new Logging("RoomsRoute");
|
const log = new Logging("RoomsRoute");
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ route.router.get('/v1/hot',
|
|||||||
|
|
||||||
async (_rq, rs) => {
|
async (_rq, rs) => {
|
||||||
// temporary: return all public AG rooms for testing
|
// temporary: return all public AG rooms for testing
|
||||||
const rooms = await Rooms.getAllBuiltinRoomGenerations();
|
const rooms = await RoomsMisc.getAllBuiltinRoomGenerations();
|
||||||
rs.json(rooms.map(room => room.Room).filter(room => room.Accessibility == RoomDataTypes.RoomAccessibility.Public));
|
rs.json(rooms.map(room => room.Room).filter(room => room.Accessibility == RoomDataTypes.RoomAccessibility.Public));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ route.router.get('/v2/baserooms',
|
|||||||
APIUtils.AuthenticationType(AuthType.Game),
|
APIUtils.AuthenticationType(AuthType.Game),
|
||||||
|
|
||||||
async (_rq, rs) => {
|
async (_rq, rs) => {
|
||||||
const rooms = await Rooms.getAllBuiltinRoomGenerations();
|
const rooms = await RoomsMisc.getAllBuiltinRoomGenerations();
|
||||||
rs.json(rooms.map(room => room.Room).filter(room => room.CloningAllowed));
|
rs.json(rooms.map(room => room.Room).filter(room => room.CloningAllowed));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ route.router.get('/v2/name/:name',
|
|||||||
rs.json(room.Room);
|
rs.json(room.Room);
|
||||||
return;
|
return;
|
||||||
} else if (rq.params.name == 'DormRoom') {
|
} else if (rq.params.name == 'DormRoom') {
|
||||||
const dorm = await Rooms.getProfileDormDefault(rs.locals.profile);
|
const dorm = await RoomsMisc.getProfileDormDefault(rs.locals.profile);
|
||||||
if (dorm) rs.json(dorm.Room);
|
if (dorm) rs.json(dorm.Room);
|
||||||
else rs.sendStatus(404);
|
else rs.sendStatus(404);
|
||||||
return;
|
return;
|
||||||
@@ -150,7 +151,7 @@ route.router.get('/v1/agRoomIds',
|
|||||||
|
|
||||||
async (_rq, rs) => {
|
async (_rq, rs) => {
|
||||||
|
|
||||||
const rooms = await Rooms.getAllBuiltinRoomGenerations();
|
const rooms = await RoomsMisc.getAllBuiltinRoomGenerations();
|
||||||
rs.json(rooms.map(det => det.Room.RoomId));
|
rs.json(rooms.map(det => det.Room.RoomId));
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -175,7 +176,7 @@ route.router.post('/v1/clone',
|
|||||||
|
|
||||||
async (rq: express.Request<NoBody, NoBody, CloneRoomBody>, rs: express.Response) => {
|
async (rq: express.Request<NoBody, NoBody, CloneRoomBody>, rs: express.Response) => {
|
||||||
|
|
||||||
const room = await Rooms.cloneRoom(rq.body.RoomId, rq.body.Name, rs.locals.profile);
|
const room = await RoomsMisc.cloneRoom(rq.body.RoomId, rq.body.Name, rs.locals.profile);
|
||||||
|
|
||||||
const masterRoomFactory = await new RoomFactory({ id: rq.body.RoomId }).init();
|
const masterRoomFactory = await new RoomFactory({ id: rq.body.RoomId }).init();
|
||||||
|
|
||||||
@@ -231,7 +232,6 @@ route.router.post('/v4/saveData',
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subroomFactory = await new SubroomFactory({
|
const subroomFactory = await new SubroomFactory({
|
||||||
roomId: currentInstance.roomId,
|
|
||||||
subroomId: rq.body.RoomSceneId,
|
subroomId: rq.body.RoomSceneId,
|
||||||
factoryMode: FactoryMode.Write,
|
factoryMode: FactoryMode.Write,
|
||||||
writeMode: WriteMode.Overwrite
|
writeMode: WriteMode.Overwrite
|
||||||
@@ -249,10 +249,6 @@ route.router.post('/v4/saveData',
|
|||||||
await subroomFactory.write();
|
await subroomFactory.write();
|
||||||
|
|
||||||
rs.json(subroomFactory.export());
|
rs.json(subroomFactory.export());
|
||||||
|
|
||||||
currentInstance.dataBlob = newFilename;
|
|
||||||
currentInstance.updatePlayers();
|
|
||||||
Rooms.socketUpdateRoom(currentInstance);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import { StorefrontBalanceType } from "../../data/content/storefronts.ts";
|
import { StorefrontBalanceType } from "../../data/content/storefronts.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/storefronts');
|
export const route = APIUtils.createRouter('/storefronts');
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/account");
|
export const route = APIUtils.createRouter("/account");
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/cachedlogin");
|
export const route = APIUtils.createRouter("/cachedlogin");
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { decode } from "@gz/jwt";
|
import { decode } from "@gz/jwt";
|
||||||
import { Config } from "../../config.ts";
|
import { Config } from "../../config/config.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import { Redis } from "../../db.ts";
|
import { Redis } from "../../db.ts";
|
||||||
import { validVersions } from "../api/versioncheck.ts";
|
import { validVersions } from "../api/versioncheck.ts";
|
||||||
import { Steam } from "../../data/steam.ts";
|
import Steam from "../../data/steam/steam.ts";
|
||||||
import Server from "../../data/server.ts";
|
import Server from "../../data/server/server.ts";
|
||||||
|
import { DeviceClass } from "../../data/live/types.ts";
|
||||||
|
import { SteamAuthResult } from "../../data/steam/SteamAuthTypes.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
@@ -124,7 +126,7 @@ route.router.post("/token",
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const conditionsMet = ![
|
const conditions = [
|
||||||
rq.body.client_id === "recroom",
|
rq.body.client_id === "recroom",
|
||||||
rq.body.platform === "0",
|
rq.body.platform === "0",
|
||||||
validVersions.includes(rq.body.ver),
|
validVersions.includes(rq.body.ver),
|
||||||
@@ -136,7 +138,8 @@ route.router.post("/token",
|
|||||||
!(rq.body.time.length > 32),
|
!(rq.body.time.length > 32),
|
||||||
!(rq.body.asid.length > 32),
|
!(rq.body.asid.length > 32),
|
||||||
SteamPlatformParamsSchema.safeParse(JSON.parse(rq.body.platform_auth)).success
|
SteamPlatformParamsSchema.safeParse(JSON.parse(rq.body.platform_auth)).success
|
||||||
].includes(false);
|
];
|
||||||
|
const conditionsMet = !conditions.includes(false);
|
||||||
|
|
||||||
if (!conditionsMet) {
|
if (!conditionsMet) {
|
||||||
requestFailed();
|
requestFailed();
|
||||||
@@ -164,48 +167,56 @@ route.router.post("/token",
|
|||||||
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
|
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const platformAuth = (JSON.parse(rq.body.platform_auth)) as SteamPlatformParams;
|
const platformAuth = (JSON.parse(rq.body.platform_auth)) as SteamPlatformParams;
|
||||||
|
|
||||||
|
let platformid: string | null;
|
||||||
if (config.auth.steamkey) {
|
if (config.auth.steamkey) {
|
||||||
const steamAuthed = await Steam.AuthenticateUserTicket(platformAuth.Ticket, rq.body.platform_id);
|
const steamAuth = await Steam.AuthenticateUserTicket(platformAuth.Ticket, rq.body.platform_id);
|
||||||
if (!steamAuthed) {
|
if (steamAuth.valid == SteamAuthResult.Failure) {
|
||||||
requestFailed();
|
requestFailed();
|
||||||
return;
|
return;
|
||||||
}
|
} else if (steamAuth.valid == SteamAuthResult.Success) platformid = steamAuth.res.steamid;
|
||||||
}
|
else platformid = null;
|
||||||
|
} else platformid = null;
|
||||||
|
|
||||||
if (isNaN(targetAccount)) {
|
if (isNaN(targetAccount)) {
|
||||||
requestFailed();
|
requestFailed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = await rs.locals.user.getAssociatedProfiles();
|
const accounts = await rs.locals.user.getAssociatedProfiles();
|
||||||
if (!accounts.has(targetAccount)) {
|
if (!accounts.has(targetAccount)) {
|
||||||
requestFailed("access_denied");
|
requestFailed("access_denied");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
|
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
|
||||||
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
|
if (platformid) rs.locals.user.addAssociatedPlatformId(platformid);
|
||||||
Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, rq.body.platform_id), targetAccount);
|
if (platformid) Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid), targetAccount);
|
||||||
|
|
||||||
const profile = Server.UnifiedProfile.get(targetAccount);
|
const profile = Server.UnifiedProfile.get(targetAccount);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
requestFailed();
|
requestFailed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deviceClass = Number(rq.body.device_class);
|
||||||
|
if (typeof DeviceClass[deviceClass] == 'undefined') {
|
||||||
|
requestFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const details = await profile.export();
|
const details = await profile.export();
|
||||||
log.i(`Player ${details?.username} "${details?.displayName}" (${profile.getId()}) logged in`);
|
log.i(`Player ${details?.username} "${details?.displayName}" (${profile.getId()}) logged in`);
|
||||||
|
|
||||||
const token = await profile.getToken();
|
const token = await profile.Auth.getToken();
|
||||||
rs.json({
|
rs.json({
|
||||||
access_token: token,
|
access_token: token,
|
||||||
refresh_token: token,
|
refresh_token: token,
|
||||||
});
|
});
|
||||||
|
|
||||||
await profile.setKnownDeviceClass(Number(rq.body.device_class));
|
await profile.setKnownDeviceClass(deviceClass);
|
||||||
|
|
||||||
nxt();
|
nxt();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { APIUtils } from "../apiutils.ts";
|
import { APIUtils } from "../apiutils.ts";
|
||||||
import { File } from "../data/content/cdn.ts";
|
import { File } from "../data/content/cdn.ts";
|
||||||
import Server from "../data/server.ts";
|
import Server from "../data/server/server.ts";
|
||||||
import { AuthType } from "../data/users.ts";
|
import { AuthType } from "../data/UserTypes.ts";
|
||||||
import { route as ConfigRoute } from "./cdn/config.ts";
|
import { route as ConfigRoute } from "./cdn/config.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import Logging from "@proxnet/undead-logging";
|
|||||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import Matchmaking from "../../data/live/base.ts";
|
import Matchmaking from "../../data/live/base.ts";
|
||||||
import { MatchmakingErrorCode } from "../../data/live/types.ts";
|
import { MatchmakingErrorCode } from "../../data/live/types.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -33,10 +33,10 @@ interface MatchmakingParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ProperCaseBooleanSchema = z.preprocess((val) => {
|
const ProperCaseBooleanSchema = z.preprocess((val) => {
|
||||||
if (val === "True") return true;
|
if (val === "True") return true;
|
||||||
if (val === "False") return false;
|
if (val === "False") return false;
|
||||||
if (typeof val === "boolean") return val; // allow raw booleans too
|
if (typeof val === "boolean") return val;
|
||||||
return val; // will fail validation
|
return val;
|
||||||
}, z.boolean());
|
}, z.boolean());
|
||||||
|
|
||||||
interface MatchmakingOptions {
|
interface MatchmakingOptions {
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ import { APIUtils, NoBody } from "../../apiutils.ts";
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import Matchmaking from "../../data/live/base.ts";
|
import Matchmaking from "../../data/live/base.ts";
|
||||||
import Presence, { PresenceExport } from "../../data/live/presence.ts";
|
import Presence, { PresenceExport } from "../../data/live/presence.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import { PlayerStatusVisibility, VRMovementMode } from "../../data/live/types.ts";
|
import { PlayerStatusVisibility, VRMovementMode } from "../../data/live/types.ts";
|
||||||
import { SettingKey } from "../../data/content/settings.ts";
|
import { SettingKey } from "../../data/content/settings.ts";
|
||||||
import Server from "../../data/server.ts";
|
import Server from "../../data/server/server.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/player');
|
export const route = APIUtils.createRouter('/player');
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils } from "../../apiutils.ts";
|
||||||
import Instances from "../../data/live/instances.ts";
|
import Instances from "../../data/live/instances.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/UserTypes.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/room');
|
export const route = APIUtils.createRouter('/room');
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../apiutils.ts";
|
import { APIUtils } from "../apiutils.ts";
|
||||||
import { Config } from "../config.ts";
|
import { Config } from "../config/config.ts";
|
||||||
|
import type { GalvanicConfiguration } from "../config/GalvanicConfiguration.ts";
|
||||||
|
|
||||||
const config = Config.getConfig() as Config.GalvanicConfiguration;
|
const config = Config.getConfig() as GalvanicConfiguration;
|
||||||
const protocol = config.web.api.securepublichost ? "https" : "http";
|
const protocol = config.web.api.securepublichost ? "https" : "http";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/ns");
|
export const route = APIUtils.createRouter("/ns");
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { APIUtils, NoBody } from "../apiutils.ts";
|
import { APIUtils, NoBody } from "../apiutils.ts";
|
||||||
import { AuthType } from "../data/users.ts";
|
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import { FileType } from "../data/content/cdn.ts";
|
import { FileType } from "../data/content/cdn.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import Server from "../data/server.ts";
|
import { AuthType } from "../data/UserTypes.ts";
|
||||||
|
import Server from "../data/server/server.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/storage");
|
export const route = APIUtils.createRouter("/storage");
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
import { APIUtils, getSrcIpDefault, NoBody } from "../apiutils.ts";
|
import { APIUtils, getSrcIpDefault, NoBody } from "../apiutils.ts";
|
||||||
// @ts-types = "npm:@types/express"
|
// @ts-types = "npm:@types/express"
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { User, UserTokenFormat } from "../data/users.ts";
|
import { User } from "../data/users.ts";
|
||||||
import { Config } from "../config.ts";
|
import { Config } from "../config/config.ts";
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { decode } from "@gz/jwt";
|
import { decode } from "@gz/jwt";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import type { UserTokenFormat } from "../data/auth/TokenBaseFormat.ts";
|
||||||
|
|
||||||
const log = new Logging("UserRoute");
|
const log = new Logging("UserRoute");
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { APIUtils } from "../apiutils.ts";
|
import { APIUtils } from "../apiutils.ts";
|
||||||
import { Config } from "../config.ts";
|
import { Config } from "../config/config.ts";
|
||||||
import { AuthType } from "../data/users.ts";
|
import { AuthType } from "../data/UserTypes.ts";
|
||||||
import { SocketHandoff } from "./handoff.ts";
|
import { SocketHandoff } from "./handoff.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { Profile } from "../data/profiles.ts";
|
import type { Profile } from "../data/profile/base/profiles.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import {
|
import {
|
||||||
CompletionMessage,
|
CompletionMessage,
|
||||||
@@ -34,9 +34,8 @@ import {
|
|||||||
import { SocketTarget } from "./targets/targetbase.ts";
|
import { SocketTarget } from "./targets/targetbase.ts";
|
||||||
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
||||||
import Presence from "../data/live/presence.ts";
|
import Presence from "../data/live/presence.ts";
|
||||||
import Matchmaking from "../data/live/base.ts";
|
|
||||||
|
|
||||||
const logmessages = false;
|
const logmessages = true;
|
||||||
|
|
||||||
export class SignalRSocketHandler {
|
export class SignalRSocketHandler {
|
||||||
|
|
||||||
@@ -62,13 +61,11 @@ export class SignalRSocketHandler {
|
|||||||
|
|
||||||
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
|
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
|
||||||
|
|
||||||
|
for (const target of this.#Targets.values()) target.onInit();
|
||||||
|
|
||||||
this.#PeriodicalId = setInterval(async () => {
|
this.#PeriodicalId = setInterval(async () => {
|
||||||
if (this.#killed) return;
|
if (this.#killed) return;
|
||||||
if (this.#socket.readyState !== this.#socket.CLOSED) {
|
if (this.#socket.readyState == this.#socket.OPEN) await this.presenceUpdate();
|
||||||
const pres = await Presence.get(this.#profile);
|
|
||||||
this.sendNotification("PresenceUpdate", await pres.export());
|
|
||||||
this.sendRaw({ type: 6 });
|
|
||||||
}
|
|
||||||
}, 8000);
|
}, 8000);
|
||||||
|
|
||||||
this.#socket.onclose = (ev) => {
|
this.#socket.onclose = (ev) => {
|
||||||
@@ -77,6 +74,12 @@ export class SignalRSocketHandler {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async presenceUpdate() {
|
||||||
|
const pres = await Presence.get(this.#profile);
|
||||||
|
this.sendNotification("PresenceUpdate", await pres.export());
|
||||||
|
this.sendRaw({ type: 6 });
|
||||||
|
}
|
||||||
|
|
||||||
async #dispatchTarget<T = unknown>(target: string, args: unknown): Promise<TargetResult> {
|
async #dispatchTarget<T = unknown>(target: string, args: unknown): Promise<TargetResult> {
|
||||||
if (this.#killed) {
|
if (this.#killed) {
|
||||||
const error = "Tried to dispatch socket target on dead socket";
|
const error = "Tried to dispatch socket target on dead socket";
|
||||||
@@ -161,19 +164,24 @@ export class SignalRSocketHandler {
|
|||||||
this.#socket.addEventListener('close', this.destroy(this, true));
|
this.#socket.addEventListener('close', this.destroy(this, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(sock: SignalRSocketHandler, internal: boolean | undefined = false) {
|
destroy(handler: SignalRSocketHandler, internal: boolean | undefined = false) {
|
||||||
return () => {
|
return (ev: CloseEvent) => {
|
||||||
sock.#killed = true;
|
handler.#killed = true;
|
||||||
clearInterval(sock.#PeriodicalId);
|
clearInterval(handler.#PeriodicalId);
|
||||||
sock.sendRaw({ type: 7, error: "Socket closed" });
|
|
||||||
if (!internal) sock.#socket.close();
|
|
||||||
sock.#log.i(`Closed socket`);
|
|
||||||
sock.#profile.clearSocketHandler();
|
|
||||||
|
|
||||||
for (const target of sock.#Targets.values()) target.destroy();
|
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";
|
||||||
|
|
||||||
|
handler.sendRaw({ type: 7, error: errorReason });
|
||||||
|
|
||||||
|
if (!internal) handler.#socket.close();
|
||||||
|
handler.#log.i(`Closed socket`);
|
||||||
|
handler.#profile.clearSocketHandler();
|
||||||
|
|
||||||
|
for (const target of handler.#Targets.values()) target.onDestroy();
|
||||||
|
|
||||||
this.#profile.getInstance()?.removePlayer(this.#profile);
|
this.#profile.getInstance()?.removePlayer(this.#profile);
|
||||||
Matchmaking.deleteLoginLock(this.#profile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SocketTarget } from "./targetbase.ts";
|
import { SocketTarget } from "./targetbase.ts";
|
||||||
import { SelfAccountExport } from "../../data/profiles.ts";
|
import type { Profile } from "../../data/profile/base/profiles.ts";
|
||||||
import { ProfileEvents, ProfileUpdatedEvent } from "../../data/profileevents.ts";
|
import type { ProfileUpdatedEvent } from "../../data/profileevents.ts";
|
||||||
import { PushNotificationId } from "../types.ts";
|
import { PushNotificationId } from "../types.ts";
|
||||||
import Server from "../../data/server.ts";
|
import Server from "../../data/server/server.ts";
|
||||||
|
|
||||||
const ArgumentSchema = z.object({
|
const ArgumentSchema = z.object({
|
||||||
PlayerIds: z.array(z.number())
|
PlayerIds: z.array(z.number())
|
||||||
@@ -28,46 +28,30 @@ const ArgumentSchema = z.object({
|
|||||||
|
|
||||||
export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
||||||
|
|
||||||
updateSocket(profile: SelfAccountExport) {
|
async updateSocket(profile: Profile) {
|
||||||
this.socket.sendNotification(PushNotificationId.SubscriptionUpdateProfile, profile);
|
this.socket.sendNotification(PushNotificationId.SubscriptionUpdateProfile, await profile.export() || undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions: { id: number, callback: (ev: unknown) => void }[] = [];
|
subscriptions: number[] = [];
|
||||||
|
|
||||||
setSubscriptions(subs: number[]) {
|
|
||||||
|
|
||||||
this.clearSubscriptions();
|
|
||||||
|
|
||||||
for (const id of subs) {
|
|
||||||
const profile = Server.UnifiedProfile.get(id);
|
|
||||||
if (!profile) continue;
|
|
||||||
this.subscriptions.push({ id: id, callback: profile
|
|
||||||
.on<ProfileUpdatedEvent>(ProfileEvents.BaseUpdated, (async ev => {
|
|
||||||
const exported = await ev.profile.export();
|
|
||||||
if (exported) this.updateSocket(exported);
|
|
||||||
}
|
|
||||||
)) });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#callback = (ev: ProfileUpdatedEvent) => {
|
||||||
|
if (this.subscriptions.includes(ev.profile.getId()))
|
||||||
|
this.updateSocket(ev.profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSubscriptions() {
|
override onInit() {
|
||||||
for (const sub of this.subscriptions) {
|
Server.on('profile.updated', this.#callback);
|
||||||
const profile = Server.UnifiedProfile.get(sub.id);
|
|
||||||
if (profile)
|
|
||||||
profile.off(ProfileEvents.BaseUpdated, sub.callback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override destroy() {
|
override onDestroy() {
|
||||||
this.clearSubscriptions();
|
Server.off('profile.updated', this.#callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// deno-lint-ignore require-await
|
// deno-lint-ignore require-await
|
||||||
override async exec(args: unknown) {
|
override async exec(args: unknown) {
|
||||||
const parsed = ArgumentSchema.safeParse(args);
|
const parsed = ArgumentSchema.safeParse(args);
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
this.setSubscriptions(parsed.data.PlayerIds);
|
this.subscriptions = parsed.data.PlayerIds;
|
||||||
return;
|
return;
|
||||||
} else throw new Error("Invalid arguments");
|
} else throw new Error("Invalid arguments");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { SignalRSocketHandler } from "../socket.ts";
|
import type { SignalRSocketHandler } from "../socket.ts";
|
||||||
|
|
||||||
export class SocketTarget {
|
export class SocketTarget {
|
||||||
|
|
||||||
@@ -25,7 +25,11 @@ export class SocketTarget {
|
|||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
onInit() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { Profile } from "../data/profiles.ts";
|
import { Profile } from "../data/profile/base/profiles.ts";
|
||||||
import { User } from "../data/users.ts";
|
import { User } from "../data/users.ts";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ GNU Affero General Public License for more details.
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { ProfileTokenFormat } from "../data/profiles.ts";
|
import type { ProfileTokenFormat } from "../data/auth/TokenBaseFormat.ts";
|
||||||
|
|
||||||
declare module 'node:http' {
|
declare module 'node:http' {
|
||||||
interface IncomingMessage {
|
interface IncomingMessage {
|
||||||
|
|||||||
28
src/utils.ts
Normal file
28
src/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
export function generateRandomString(length: number) {
|
||||||
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let randomString = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||||
|
randomString += characters.charAt(randomIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return randomString;
|
||||||
|
}
|
||||||
34
src/watchdog.ts
Normal file
34
src/watchdog.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||||
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||||
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import Logging, { LoggingConfiguration, LogTiming } from "@proxnet/undead-logging";
|
||||||
|
|
||||||
|
const log = new Logging("Watchdog");
|
||||||
|
|
||||||
|
let added = false;
|
||||||
|
export function addWatchdogListener() {
|
||||||
|
if (added) return;
|
||||||
|
added = true;
|
||||||
|
Deno.addSignalListener('SIGINT', () => {
|
||||||
|
LoggingConfiguration.logTiming = LogTiming.Sync;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
log.e(`Server took too long (60s) to shut down! Exiting.`);
|
||||||
|
Deno.exit(2);
|
||||||
|
}, 60000);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user