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)
|
||||
|
||||
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 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
|
||||
|
||||
Galvanic Corrosion listens on two ports:
|
||||
* 13370/tcp(http) - for web endpoints
|
||||
* 13371/tcp(http+ws) - for websockets
|
||||
### Some issues may appear when connecting directly to a GC server's listening address.
|
||||
Sockets behave erratically when connected directly to clients. This is a suspected issue with Deno websockets.<br>
|
||||
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>
|
||||
[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.
|
||||
|
||||
Once your server is reachable, 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:
|
||||
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 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
|
||||
* In the example, my-gc-server.coolguy.xyz and my-gc-server-socket.coolguy.xyz
|
||||
* This includes port numbers, but not the protocol
|
||||
- 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.
|
||||
|
||||
## Public Configuration
|
||||
## Public
|
||||
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?)
|
||||
@@ -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>
|
||||
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
|
||||
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
|
||||
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.
|
||||
<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.
|
||||
|
||||
`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",
|
||||
"dev": "deno run -A src/main.ts --dev",
|
||||
"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": {
|
||||
"@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/express": "npm:@types/express@^5.0.0",
|
||||
"@types/multer": "npm:@types/multer@^1.4.12",
|
||||
"@types/validator": "npm:@types/validator@^13.12.2",
|
||||
"cookie-parser": "npm:cookie-parser@^1.4.7",
|
||||
"discord.js": "npm:discord.js@^14.16.3",
|
||||
"express": "npm:express@^4.21.2",
|
||||
"ioredis": "npm:ioredis@^5.5.0",
|
||||
"multer": "npm:multer@^1.4.5-lts.2",
|
||||
@@ -31,5 +31,5 @@
|
||||
"./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/>. */
|
||||
|
||||
try {
|
||||
Deno.removeSync('./ver.ts');
|
||||
Deno.renameSync('./ver.ts.bak', 'ver.ts');
|
||||
await Deno.remove('./ver.ts');
|
||||
await Deno.rename('./ver.ts.bak', 'ver.ts');
|
||||
} catch (err) {
|
||||
console.error(`Cannot post-build version information: ${err}`);
|
||||
Deno.exit(1);
|
||||
|
||||
@@ -28,9 +28,9 @@ try {
|
||||
const newVerString = `${file.version}-${new TextDecoder().decode(commitHash.stdout).trim()}`;
|
||||
|
||||
if (file.version) {
|
||||
Deno.writeTextFileSync('./ver.ts.bak', devVer);
|
||||
Deno.writeTextFileSync('./ver.ts', devVer.replace('development', newVerString));
|
||||
console.info('Built version information');
|
||||
await Deno.writeTextFile('./ver.ts.bak', devVer);
|
||||
await Deno.writeTextFile('./ver.ts', devVer.replace('development', newVerString));
|
||||
console.info(`Built version information: Commit ${newVerString}`);
|
||||
}
|
||||
} catch (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 Logging from "@proxnet/undead-logging";
|
||||
import { decode } from "@gz/jwt";
|
||||
import { Config } from "./config.ts";
|
||||
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
||||
import { ProfileTokenFormat } from "./data/profiles.ts";
|
||||
import { Config } from "./config/config.ts";
|
||||
import { User } from "./data/users.ts";
|
||||
import { AuthType } from "./data/UserTypes.ts";
|
||||
import z from "zod";
|
||||
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();
|
||||
|
||||
@@ -48,19 +50,6 @@ export function setCacheAllowed(_rq: express.Request, rs: express.Response, 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) {
|
||||
return (
|
||||
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(
|
||||
rq: express.Request,
|
||||
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`);
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
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 * 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");
|
||||
|
||||
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 = {
|
||||
redis: {
|
||||
host: "127.0.0.1",
|
||||
@@ -116,12 +55,14 @@ export const defaultConfig: GalvanicConfiguration = {
|
||||
photonRegionId: PhotonRegionCodeNumber.us,
|
||||
initialRoom: null
|
||||
},
|
||||
general: {
|
||||
watchdogTimeout: 60000
|
||||
},
|
||||
logging: {
|
||||
notfound: false,
|
||||
debug: false,
|
||||
network: false,
|
||||
},
|
||||
discord: null,
|
||||
auth: {
|
||||
secret: "CHANGE-ME-PLEASE",
|
||||
timeout: 3,
|
||||
@@ -140,12 +81,12 @@ try {
|
||||
}
|
||||
|
||||
/** Does the configuration file exist on the disk? */
|
||||
export function configurationExists() {
|
||||
function configurationExists() {
|
||||
return fs.existsSync("./config.json");
|
||||
}
|
||||
|
||||
/** Place [or overwrite] the [existing] default configuration in the current directory */
|
||||
export function generateDefaultConfig() {
|
||||
function generateDefaultConfig() {
|
||||
fs.writeFileSync(
|
||||
"./config.json",
|
||||
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
|
||||
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
|
||||
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 {
|
||||
time: Date
|
||||
}
|
||||
|
||||
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<K extends keyof Events>(eventName: K, callback: Callback<Events[K]>): void {
|
||||
if (!this.#listeners[eventName])
|
||||
this.#listeners[eventName] = new Set();
|
||||
this.#listeners[eventName]!.add(callback);
|
||||
}
|
||||
|
||||
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) {
|
||||
const subset = this.getSubSet(event);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
emit<K extends keyof Events>(eventName: K, payload: Events[K]): void {
|
||||
this.#listeners[eventName]?.forEach((callback) => callback(payload));
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
type: ObjectiveType;
|
||||
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/>. */
|
||||
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { generateRandomString } from "../../apiutils.ts";
|
||||
import { Profile } from "../profiles.ts";
|
||||
import { generateRandomString } from "../../utils.ts";
|
||||
import type { Profile } from "../profile/base/profiles.ts";
|
||||
import * as fs from "node:fs";
|
||||
import Server from "../server.ts";
|
||||
import Server from "../server/server.ts";
|
||||
|
||||
const log = new Logging("CDN");
|
||||
|
||||
@@ -68,7 +68,6 @@ export class CDNBase {
|
||||
pathParts.pop();
|
||||
|
||||
const dirPath = pathParts.join('/');
|
||||
log.d(dirPath);
|
||||
if (dirPath) await Deno.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
@@ -113,6 +112,7 @@ export class CDNBase {
|
||||
const metaData = await Deno.readTextFile(`${path}.gcmeta`);
|
||||
|
||||
const parsedMeta = JSON.parse(metaData);
|
||||
|
||||
const meta: MetaFile = {
|
||||
creationPlayer: Server.UnifiedProfile.get(parsedMeta.creationPlayer) || undefined,
|
||||
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
|
||||
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 "./profiles.ts";
|
||||
import { RoomFactory } from "./RoomFactory.ts";
|
||||
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||
|
||||
class ServerBase extends EventManager {
|
||||
CDN = new CDNBase();
|
||||
UnifiedProfile = new UnifiedProfileBase();
|
||||
export interface SubroomUpdatedEvent {
|
||||
subroom: SubroomFactory
|
||||
}
|
||||
|
||||
const Server = new ServerBase();
|
||||
export default Server;
|
||||
export interface RoomUpdatedEvent {
|
||||
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
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { Redis } from "../../../db.ts";
|
||||
import Rooms from "../rooms.ts";
|
||||
import { FactoryMode, HardwareSupport, HardwareSupportStrings, RoomAccessibility, RoomDataTypes, RoomDetails, RoomState, WriteMode } from "./DataTypes.ts";
|
||||
import Server from "../../server/server.ts";
|
||||
import { RoomDataTypes } from "./base/DataTypes.ts";
|
||||
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||
|
||||
export const roomdebug = false;
|
||||
|
||||
const log = new Logging("RoomFactory");
|
||||
|
||||
interface RoomFactoryOptions {
|
||||
id?: number;
|
||||
name?: string;
|
||||
@@ -29,6 +34,14 @@ interface RoomFactoryOptions {
|
||||
|
||||
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 = {
|
||||
Meta: "roommeta",
|
||||
Subrooms: "subrooms",
|
||||
@@ -68,7 +81,7 @@ export class RoomFactory {
|
||||
|
||||
async init() {
|
||||
|
||||
if (this.factoryMode !== FactoryMode.Fetch) {
|
||||
if (this.factoryMode !== RoomDataTypes.FactoryMode.Fetch) {
|
||||
if (!this.#specifiedId) throw this.#mustSpecifyIdInWriteModeError;
|
||||
this.#resolvedId = this.#specifiedId;
|
||||
return this;
|
||||
@@ -76,7 +89,7 @@ export class RoomFactory {
|
||||
|
||||
if (!this.#specifiedId) {
|
||||
if (!this.#specifiedName) throw this.#mustSpecifyEitherIdOrNameError;
|
||||
const id = await Rooms.getIdFromName(this.#specifiedName);
|
||||
const id = await RoomFactory.getIdFromName(this.#specifiedName);
|
||||
if (!id) return null;
|
||||
this.#specifiedId = id;
|
||||
}
|
||||
@@ -89,6 +102,8 @@ export class RoomFactory {
|
||||
RoomFactory.Keys.Meta
|
||||
));
|
||||
|
||||
if (roomdebug) log.d(`Init success, specifiedId: ${this.#specifiedId}`);
|
||||
|
||||
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);
|
||||
|
||||
Server.emit('room.updated', { room: this });
|
||||
}
|
||||
|
||||
async export() {
|
||||
@@ -128,7 +145,7 @@ export class RoomFactory {
|
||||
const subroomPromises = subroomIds.map(id => this.getSubroom(id).init());
|
||||
const subrooms = (await Promise.all(subroomPromises)).map(subroom => subroom.export());
|
||||
|
||||
const details: RoomDetails = {
|
||||
const details: RoomDataTypes.RoomDetails = {
|
||||
Room: {
|
||||
RoomId: this.RoomId,
|
||||
Name: this.Name,
|
||||
@@ -160,13 +177,13 @@ export class RoomFactory {
|
||||
Tags: []
|
||||
}
|
||||
|
||||
if (roomdebug) log.d(`Exported details for room ${this.RoomId}`);
|
||||
return details;
|
||||
}
|
||||
|
||||
getSubroom(id: number, factoryMode?: FactoryMode, writeMode?: WriteMode) {
|
||||
getSubroom(id: number, factoryMode?: RoomDataTypes.FactoryMode, writeMode?: RoomDataTypes.WriteMode) {
|
||||
if (!this.#resolvedId) throw this.#unresolvedError;
|
||||
return new SubroomFactory({
|
||||
roomId: this.#resolvedId,
|
||||
subroomId: id,
|
||||
factoryMode: factoryMode ? factoryMode : undefined,
|
||||
writeMode : writeMode ? writeMode : undefined
|
||||
@@ -240,11 +257,11 @@ export class RoomFactory {
|
||||
set ImageName(data) { this.#setHashValue(this.#imageKey, data) }
|
||||
|
||||
#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) }
|
||||
|
||||
#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) }
|
||||
|
||||
#votingKey = 'SupportsLevelVoting';
|
||||
@@ -279,20 +296,20 @@ export class RoomFactory {
|
||||
get DisableMicAutoMute() { return this.#fetchBooleanKey(this.#muteKey, false) }
|
||||
set DisableMicAutoMute(data) { this.#setHashValue(this.#muteKey, data) }
|
||||
|
||||
async getHardwareSupport(): Promise<HardwareSupport[]> {
|
||||
async getHardwareSupport(): Promise<RoomDataTypes.HardwareSupport[]> {
|
||||
if (!this.#resolvedId) throw this.#unresolvedError;
|
||||
return (await Redis.Database.smembers(Redis.buildKey(
|
||||
Redis.KeyGroups.Rooms.Root,
|
||||
this.#resolvedId.toString(),
|
||||
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 (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;
|
||||
}
|
||||
|
||||
@@ -314,7 +331,7 @@ export class RoomFactory {
|
||||
}
|
||||
}
|
||||
|
||||
async removeHardwareSupport(hardware: HardwareSupport) {
|
||||
async removeHardwareSupport(hardware: RoomDataTypes.HardwareSupport) {
|
||||
if (!this.#resolvedId) throw this.#unresolvedError;
|
||||
await Redis.Database.srem(Redis.buildKey(
|
||||
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
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Redis } from "../../db.ts";
|
||||
import { Profile } from "../profiles.ts";
|
||||
import { Redis } from "../../../db.ts";
|
||||
import { Profile } from "../../profile/base/profiles.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { BuiltinRoom, FactoryMode, IntegratedRoomScene, RoomAccessibility, RoomDataTypes, RoomDetails, RoomState, WriteMode } from "./rooms/DataTypes.ts";
|
||||
import { RoomFactory } from "./rooms/RoomFactory.ts";
|
||||
import { SubroomFactory } from "./rooms/SubroomFactory.ts";
|
||||
import { RootPath } from "../../path.ts";
|
||||
import { Instance } from "../live/instances.ts";
|
||||
import { PushNotificationId } from "../../socket/types.ts";
|
||||
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||
import { RootPath } from "../../../path.ts";
|
||||
import { RoomFactory } from "./RoomFactory.ts";
|
||||
import { RoomDataTypes } from "../rooms/base/DataTypes.ts";
|
||||
import Rooms from "./base/RoomsBase.ts";
|
||||
|
||||
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 = {
|
||||
BuiltinGenerated: "builtinrooms-done",
|
||||
@@ -37,33 +43,13 @@ class RoomsBase {
|
||||
}
|
||||
|
||||
getAllBuiltinRooms() {
|
||||
return rooms;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return builtinRooms;
|
||||
}
|
||||
|
||||
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));
|
||||
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() {
|
||||
@@ -77,9 +63,7 @@ class RoomsBase {
|
||||
let id = Math.round(Math.random() * Math.pow(2, 31));
|
||||
while ((await Redis.Database.exists(
|
||||
Redis.buildKey(
|
||||
Redis.KeyGroups.Rooms.Root,
|
||||
roomid.toString(),
|
||||
RoomFactory.Keys.Subrooms,
|
||||
Redis.KeyGroups.Subrooms.Root,
|
||||
id.toString(),
|
||||
SubroomFactory.Keys.Meta
|
||||
))) >= 1)
|
||||
@@ -93,20 +77,20 @@ class RoomsBase {
|
||||
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.Name == 'DormRoom') return { result: RoomDataTypes.CreateModifyRoomStatus.ReservedName } 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;
|
||||
|
||||
newFactory.CreatorPlayerId = newowner.getId();
|
||||
newFactory.Description = factory.Description;
|
||||
newFactory.Name = newname;
|
||||
newFactory.ImageName = factory.ImageName;
|
||||
newFactory.State = RoomState.Active;
|
||||
newFactory.RoomAccessibility = RoomAccessibility.Private;
|
||||
newFactory.State = RoomDataTypes.RoomState.Active;
|
||||
newFactory.RoomAccessibility = RoomDataTypes.RoomDataTypes.RoomAccessibility.Private;
|
||||
newFactory.SupportsLevelVoting = factory.SupportsLevelVoting;
|
||||
newFactory.IsAGRoom = false;
|
||||
newFactory.IsDormRoom = factory.IsDormRoom;
|
||||
@@ -122,9 +106,10 @@ class RoomsBase {
|
||||
|
||||
const oldSubroomIds = await factory.getAllSubroomIds();
|
||||
const promises = oldSubroomIds.map(async (id) => {
|
||||
const newSubroomFactory = await newFactory.getSubroom(id, FactoryMode.Write, WriteMode.Overwrite).init();
|
||||
const oldSubroomFactory = await factory.getSubroom(id, FactoryMode.Fetch).init();
|
||||
const newSubroomFactory = await newFactory.getSubroom(id, RoomDataTypes.RoomDataTypes.FactoryMode.Write, RoomDataTypes.RoomDataTypes.WriteMode.Overwrite).init();
|
||||
const oldSubroomFactory = await factory.getSubroom(id, RoomDataTypes.RoomDataTypes.FactoryMode.Fetch).init();
|
||||
|
||||
newSubroomFactory.RoomId = newFactory.RoomId;
|
||||
newSubroomFactory.RoomSceneLocationId = oldSubroomFactory.RoomSceneLocationId;
|
||||
newSubroomFactory.Name = oldSubroomFactory.Name;
|
||||
newSubroomFactory.IsSandbox = oldSubroomFactory.IsSandbox;
|
||||
@@ -137,7 +122,7 @@ class RoomsBase {
|
||||
await Promise.all(promises);
|
||||
|
||||
await newFactory.write();
|
||||
newFactory.factoryMode = FactoryMode.Fetch;
|
||||
newFactory.factoryMode = RoomDataTypes.RoomDataTypes.FactoryMode.Fetch;
|
||||
|
||||
return {
|
||||
factory: newFactory,
|
||||
@@ -176,15 +161,15 @@ class RoomsBase {
|
||||
async generateNewDorm(player: Profile) {
|
||||
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;
|
||||
|
||||
factory.Name = "DormRoom";
|
||||
factory.Description = "Your private room.";
|
||||
factory.CreatorPlayerId = player.getId();
|
||||
factory.ImageName = "DefaultProfileImage.png";
|
||||
factory.State = RoomState.Active;
|
||||
factory.RoomAccessibility = RoomAccessibility.Private;
|
||||
factory.State = RoomDataTypes.RoomState.Active;
|
||||
factory.RoomAccessibility = RoomDataTypes.RoomAccessibility.Private;
|
||||
factory.SupportsLevelVoting = false;
|
||||
factory.IsAGRoom = false;
|
||||
factory.IsDormRoom = true;
|
||||
@@ -195,10 +180,11 @@ class RoomsBase {
|
||||
|
||||
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;
|
||||
|
||||
subroomFactory.RoomSceneLocationId = IntegratedRoomScene.DormRoom;
|
||||
subroomFactory.RoomId = id;
|
||||
subroomFactory.RoomSceneLocationId = RoomDataTypes.IntegratedRoomScene.DormRoom;
|
||||
subroomFactory.Name = "Home";
|
||||
subroomFactory.IsSandbox = true;
|
||||
subroomFactory.DataBlobName = "";
|
||||
@@ -212,26 +198,20 @@ class RoomsBase {
|
||||
}
|
||||
|
||||
async generateBuiltinRooms() {
|
||||
|
||||
if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated))) !== null) return true;
|
||||
await Promise.all(rooms.map(async builtinRoom => {
|
||||
if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsMiscBase.Keys.BuiltinGenerated))) !== null) return true;
|
||||
|
||||
await Promise.all(builtinRooms.map(async builtinRoom => {
|
||||
if (builtinRoom.Name == 'DormRoom') return;
|
||||
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;
|
||||
|
||||
factory.Name = builtinRoom.Name;
|
||||
factory.Description = builtinRoom.Description;
|
||||
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)) {
|
||||
const image = baseImageChanges.find(change => change.room == builtinRoom.Name)!;
|
||||
@@ -239,7 +219,7 @@ class RoomsBase {
|
||||
}
|
||||
else factory.ImageName = `${builtinRoom.Name}.png`;
|
||||
|
||||
factory.State = RoomState.Active;
|
||||
factory.State = RoomDataTypes.RoomState.Active;
|
||||
factory.RoomAccessibility = builtinRoom.Accessibility;
|
||||
factory.SupportsLevelVoting = builtinRoom.SupportsLevelVoting;
|
||||
factory.IsAGRoom = true;
|
||||
@@ -254,9 +234,10 @@ class RoomsBase {
|
||||
|
||||
await Promise.all(builtinRoom.Scenes.map(async subroom => {
|
||||
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;
|
||||
|
||||
subroomFactory.RoomId = newId;
|
||||
subroomFactory.RoomSceneLocationId = subroom.RoomSceneLocationId;
|
||||
subroomFactory.Name = subroom.Name;
|
||||
subroomFactory.IsSandbox = subroom.IsSandbox;
|
||||
@@ -269,53 +250,13 @@ class RoomsBase {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 default Rooms;
|
||||
export { builtinRooms as BuiltinRooms };
|
||||
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
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { Redis } from "../../../db.ts";
|
||||
import { RoomDataTypes, IntegratedRoomScene, RoomScene, WriteMode, FactoryMode } from "./DataTypes.ts";
|
||||
import { RoomFactory } from "./RoomFactory.ts";
|
||||
import Server from "../../server/server.ts";
|
||||
import { RoomDataTypes } from "./base/DataTypes.ts";
|
||||
import { RoomFactory, roomdebug } from "./RoomFactory.ts";
|
||||
|
||||
const log = new Logging("SubroomFactory");
|
||||
|
||||
interface SubroomFactoryOptions {
|
||||
roomId: number;
|
||||
subroomId: number;
|
||||
writeMode?: WriteMode;
|
||||
factoryMode?: FactoryMode;
|
||||
writeMode?: RoomDataTypes.WriteMode;
|
||||
factoryMode?: RoomDataTypes.FactoryMode;
|
||||
}
|
||||
|
||||
export class SubroomFactory {
|
||||
@@ -42,13 +45,11 @@ export class SubroomFactory {
|
||||
#writeMode: RoomDataTypes.WriteMode = RoomDataTypes.WriteMode.WriteIfFree;
|
||||
factoryMode: RoomDataTypes.FactoryMode = RoomDataTypes.FactoryMode.Fetch;
|
||||
|
||||
#roomId: number;
|
||||
#subroomId: number;
|
||||
|
||||
#hash: Record<string, string> | null = null;
|
||||
|
||||
constructor(options: SubroomFactoryOptions) {
|
||||
this.#roomId = options.roomId;
|
||||
this.#subroomId = options.subroomId;
|
||||
if (options.writeMode) this.#writeMode = options.writeMode;
|
||||
if (options.factoryMode) this.factoryMode = options.factoryMode;
|
||||
@@ -56,12 +57,10 @@ export class SubroomFactory {
|
||||
|
||||
async init() {
|
||||
|
||||
if (!this.#roomId || !this.#subroomId) throw this.#unspecifiedArguments;
|
||||
if (!this.#subroomId) throw this.#unspecifiedArguments;
|
||||
|
||||
this.#hash = await Redis.Database.hgetall(Redis.buildKey(
|
||||
Redis.KeyGroups.Rooms.Root,
|
||||
this.#roomId.toString(),
|
||||
RoomFactory.Keys.Subrooms,
|
||||
Redis.KeyGroups.Subrooms.Root,
|
||||
this.#subroomId.toString(),
|
||||
SubroomFactory.Keys.Meta
|
||||
));
|
||||
@@ -76,9 +75,7 @@ export class SubroomFactory {
|
||||
else {
|
||||
|
||||
const dbkey = Redis.buildKey(
|
||||
Redis.KeyGroups.Rooms.Root,
|
||||
this.#roomId.toString(),
|
||||
RoomFactory.Keys.Subrooms,
|
||||
Redis.KeyGroups.Subrooms.Root,
|
||||
this.#subroomId.toString(),
|
||||
SubroomFactory.Keys.Meta
|
||||
);
|
||||
@@ -89,21 +86,25 @@ export class SubroomFactory {
|
||||
}
|
||||
|
||||
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.sadd(Redis.buildKey(
|
||||
Redis.KeyGroups.Rooms.Root,
|
||||
this.#roomId.toString(),
|
||||
RoomFactory.Keys.Subrooms
|
||||
), this.RoomSceneId);
|
||||
if (this.#hash[this.#roomIdKey])
|
||||
await Redis.Database.sadd(Redis.buildKey(
|
||||
Redis.KeyGroups.Rooms.Root,
|
||||
this.#hash[this.#roomIdKey].toString(),
|
||||
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 {
|
||||
RoomSceneId: this.RoomSceneId,
|
||||
RoomId: this.RoomId,
|
||||
@@ -144,7 +145,7 @@ export class SubroomFactory {
|
||||
|
||||
#setHashValue(key: string, value: string | number | boolean) {
|
||||
if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchSubroomFirstError;
|
||||
|
||||
|
||||
if (!this.#hash) this.#hash = {};
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
@@ -158,10 +159,17 @@ export class SubroomFactory {
|
||||
|
||||
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';
|
||||
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) }
|
||||
|
||||
#nameKey = 'Name';
|
||||
@@ -189,9 +197,7 @@ export class SubroomFactory {
|
||||
|
||||
async addBlobHistory(date: Date, filename: string) {
|
||||
await Redis.Database.hset(Redis.buildKey(
|
||||
Redis.KeyGroups.Rooms.Root,
|
||||
this.#roomId.toString(),
|
||||
RoomFactory.Keys.Subrooms,
|
||||
Redis.KeyGroups.Subrooms.Root,
|
||||
this.#subroomId.toString(),
|
||||
SubroomFactory.Keys.Blobs
|
||||
), date.toISOString(), filename);
|
||||
@@ -204,9 +210,7 @@ export class SubroomFactory {
|
||||
}
|
||||
|
||||
const hist = await Redis.Database.hgetall(Redis.buildKey(
|
||||
Redis.KeyGroups.Rooms.Root,
|
||||
this.#roomId.toString(),
|
||||
RoomFactory.Keys.Subrooms,
|
||||
Redis.KeyGroups.Subrooms.Root,
|
||||
this.#subroomId.toString(),
|
||||
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
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import Rooms from "../content/rooms.ts";
|
||||
import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
|
||||
import { Profile } from "../profiles.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import RoomsMisc from "../content/rooms/Rooms.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 { MatchmakingErrorCode, RoomInstance } from "./types.ts";
|
||||
|
||||
const log = new Logging("MatchmakingBase");
|
||||
|
||||
const loginLocks: Map<number, string> = new Map();
|
||||
|
||||
interface MatchmakingOptions {
|
||||
@@ -49,6 +53,9 @@ class MatchmakingBase {
|
||||
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) {
|
||||
loginLocks.delete(prof.getId());
|
||||
}
|
||||
@@ -75,7 +82,7 @@ class MatchmakingBase {
|
||||
} else {
|
||||
|
||||
// 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.Room.Accessibility == RoomDataTypes.RoomAccessibility.Private && targetRoom.Room.CreatorPlayerId !== options.profile.getId())
|
||||
return { errorCode: MatchmakingErrorCode.RoomIsPrivate };
|
||||
@@ -90,7 +97,7 @@ class MatchmakingBase {
|
||||
if (subroomId) allInstances = allInstances.filter(instance => instance.subRoomId == subroomId);
|
||||
|
||||
// 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 =>
|
||||
({Name: room.Name, Scenes: room.Scenes.map(scene =>
|
||||
({Name: scene.Name, Supported: scene.SupportsJoinInProgress})
|
||||
@@ -117,9 +124,11 @@ class MatchmakingBase {
|
||||
if (!foundInstance) {
|
||||
|
||||
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({
|
||||
Room: targetRoom,
|
||||
SceneIndex: Math.floor(Math.random() * matchmakeableSubrooms.length),
|
||||
SceneIndex: index,
|
||||
FirstPlayer: options.profile,
|
||||
Private: options.private,
|
||||
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/>. */
|
||||
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { Profile } from "../profiles.ts";
|
||||
import { RoomInstance, InstanceOptions } from "./types.ts";
|
||||
import { Config } from "../../config.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";
|
||||
import { InstanceOptions } from "./types.ts";
|
||||
import Server from "../server/server.ts";
|
||||
import { Instance } from "./Instance.ts";
|
||||
|
||||
const log = new Logging("Instances");
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
/**
|
||||
* `Map<roomId (number), Instance>`
|
||||
*/
|
||||
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>();
|
||||
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;
|
||||
instance.dataBlob = ev.subroom.DataBlobName;
|
||||
instance.updateSubroom(ev.subroom.export());
|
||||
for (const profile of instance.getAllPlayers())
|
||||
profile.getSocketHandler()?.sendNotification('RoomInstanceUpdate', instance.snapshot());
|
||||
|
||||
instance.updatePlayers(); // legacy
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -191,8 +50,9 @@ class InstancesBase {
|
||||
else return null;
|
||||
}
|
||||
|
||||
getAllInstances() {
|
||||
return new Set([...instanceSet.values().toArray()]);
|
||||
getAllInstances(asSet: boolean | undefined = false) {
|
||||
if (!asSet) return new Set([...instanceSet.values().toArray()]);
|
||||
else return instanceSet;
|
||||
}
|
||||
|
||||
getAllRoomInstances(roomId: number) {
|
||||
|
||||
@@ -17,10 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { z } from "zod";
|
||||
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 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");
|
||||
|
||||
@@ -57,6 +60,7 @@ class PlayerPresence {
|
||||
this.updateOffline();
|
||||
}, 80000);
|
||||
|
||||
Server.on('room.updated', this.#roomUpdatedEventCallback);
|
||||
}
|
||||
|
||||
offline: boolean;
|
||||
@@ -69,6 +73,20 @@ class PlayerPresence {
|
||||
|
||||
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() {
|
||||
const PlayerStatusVisibilityEnum = z.nativeEnum(PlayerStatusVisibility);
|
||||
|
||||
@@ -122,6 +140,16 @@ class PlayerPresence {
|
||||
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();
|
||||
@@ -164,10 +192,8 @@ class PresenceBase {
|
||||
|
||||
deleteDeadPresences() {
|
||||
for (const pres of presence.values())
|
||||
if (Math.round(new Date().getTime() / 1000) - Math.round(pres.lastSeen.getTime() / 1000) >= 60) {
|
||||
presence.delete(pres);
|
||||
clearInterval(pres.intervalId);
|
||||
}
|
||||
if (Math.round(new Date().getTime() / 1000) - Math.round(pres.lastSeen.getTime() / 1000) >= 60)
|
||||
pres.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
|
||||
import { Profile } from "../profiles.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
|
||||
}
|
||||
import { RoomDataTypes } from "../content/rooms/base/DataTypes.ts";
|
||||
import type { Profile } from "../profile/base/profiles.ts";
|
||||
import { PhotonRegionCodeString, PhotonRegionCodeNumber } from "./PhotonTypes.ts";
|
||||
|
||||
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(
|
||||
Redis.KeyGroups.Profiles.Root,
|
||||
this.profileId.toString(),
|
||||
this.profile.getId().toString(),
|
||||
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
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import type { Profile } from "./profiles.ts";
|
||||
|
||||
export class ProfileContentManager {
|
||||
|
||||
constructor(profileId: number) {
|
||||
this.profileId = profileId;
|
||||
constructor(profile: Profile) {
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
onProfileInit() {
|
||||
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
|
||||
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 Dictionary from "./usernames.ts";
|
||||
import { Config } from "../config.ts";
|
||||
import { AuthType } from "./users.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 Dictionary from "../../usernames.ts";
|
||||
import { DeviceClass, VRMovementMode } from "../../live/types.ts";
|
||||
import { SettingKey } from "../../content/settings.ts";
|
||||
import { z } from "zod";
|
||||
import { SignalRSocketHandler } from "../socket/socket.ts";
|
||||
import { ProfileSettingsManager } from "./profile/settings.ts";
|
||||
import { ProfileProgressionManager } from "./profile/progression.ts";
|
||||
import { ProfileReputationManager } from "./profile/reputation.ts";
|
||||
import { ProfileRelationshipManager } from "./profile/relationships.ts";
|
||||
import { ProfileAvatarManager } from "./profile/avatar.ts";
|
||||
import { EventManager } from "./baseevent.ts";
|
||||
import { ProfileEvents, ProfileUpdatedEvent } from "./profileevents.ts";
|
||||
import { Instance } from "./live/instances.ts";
|
||||
import { ProfileRoomsManager } from "./profile/rooms.ts";
|
||||
|
||||
const config = Config.getConfig();
|
||||
import { SignalRSocketHandler } from "../../../socket/socket.ts";
|
||||
import { ProfileSettingsManager } from "../settings.ts";
|
||||
import { ProfileProgressionManager } from "../progression.ts";
|
||||
import { ProfileReputationManager } from "../reputation.ts";
|
||||
import { ProfileRelationshipManager } from "../relationships.ts";
|
||||
import { ProfileAvatarManager } from "../avatar.ts";
|
||||
import type { ProfileUpdatedEvent } from "../../profileevents.ts";
|
||||
import { Instance } from "../../live/Instance.ts";
|
||||
import { ProfileRoomsManager } from "../rooms.ts";
|
||||
import { type ServerBase } from "../../server/server.ts";
|
||||
import { ProfileAuth as ProfileAuthManager } from "../ProfileAuth.ts";
|
||||
|
||||
const log = new Logging("Profiles");
|
||||
|
||||
@@ -63,15 +58,10 @@ export interface SelfAccountExport extends AccountExport {
|
||||
juniorState?: number,
|
||||
parentAccountId?: number
|
||||
}
|
||||
export interface ProfileTokenFormat extends TokenBaseFormat {
|
||||
sub: number;
|
||||
role: "developer" | "user";
|
||||
typ: AuthType.Game;
|
||||
}
|
||||
|
||||
const reservedIds = [1, 2];
|
||||
|
||||
class Profile extends EventManager {
|
||||
class Profile {
|
||||
static async exists(id: number) {
|
||||
return (await Redis.Database.exists(
|
||||
Redis.buildKey(
|
||||
@@ -202,26 +192,25 @@ class Profile extends EventManager {
|
||||
Relationships: ProfileRelationshipManager;
|
||||
Avatar: ProfileAvatarManager;
|
||||
Rooms: ProfileRoomsManager;
|
||||
Auth: ProfileAuthManager;
|
||||
|
||||
constructor(id: number) {
|
||||
super();
|
||||
|
||||
this.#id = id;
|
||||
|
||||
this.Settings = new ProfileSettingsManager(this.#id);
|
||||
this.Progression = new ProfileProgressionManager(this.#id);
|
||||
this.Reputation = new ProfileReputationManager(this.#id);
|
||||
this.Relationships = new ProfileRelationshipManager(this.#id);
|
||||
this.Avatar = new ProfileAvatarManager(this.#id);
|
||||
this.Rooms = new ProfileRoomsManager(this.#id);
|
||||
this.Settings = new ProfileSettingsManager(this);
|
||||
this.Progression = new ProfileProgressionManager(this);
|
||||
this.Reputation = new ProfileReputationManager(this);
|
||||
this.Relationships = new ProfileRelationshipManager(this);
|
||||
this.Avatar = new ProfileAvatarManager(this);
|
||||
this.Rooms = new ProfileRoomsManager(this);
|
||||
this.Auth = new ProfileAuthManager(this);
|
||||
}
|
||||
|
||||
#emitProfileUpdated() {
|
||||
const ev: ProfileUpdatedEvent = {
|
||||
time: new Date(),
|
||||
profile: this
|
||||
}
|
||||
this.emit(ProfileEvents.BaseUpdated, ev);
|
||||
if (Server) Server.emit('profile.updated', ev);
|
||||
}
|
||||
|
||||
setInstance(instance: Instance | null) {
|
||||
@@ -236,10 +225,6 @@ class Profile extends EventManager {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
async getIsOperator() {
|
||||
return (await Redis.Database.sismember(Redis.buildKey(Redis.KeyGroups.Operators), this.#id.toString())) >= 1;
|
||||
}
|
||||
|
||||
async getBio() {
|
||||
const bio = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Bio));
|
||||
if (!bio) return "";
|
||||
@@ -330,26 +315,20 @@ class Profile extends EventManager {
|
||||
getSocketHandler() {
|
||||
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 {
|
||||
|
||||
constructor(server: ServerBase) {
|
||||
Server = server;
|
||||
}
|
||||
|
||||
get(id: number) {
|
||||
let profile = profiles.get(id);
|
||||
if (!profile) {
|
||||
@@ -357,7 +336,8 @@ export class UnifiedProfileBase {
|
||||
const inst = new Profile(id);
|
||||
profiles.set(id, inst);
|
||||
profile = inst;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
log.e(`Could not fetch profile: ${(err as Error).stack}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -365,7 +345,10 @@ export class UnifiedProfileBase {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -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/>. */
|
||||
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { GameConfigs } from "../config.ts";
|
||||
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
||||
import { Redis } from "../../db.ts";
|
||||
import { getPublicConfig } from "../config/PublicConfig.ts";
|
||||
|
||||
const log = new Logging("ProfileProgression");
|
||||
|
||||
const config = GameConfigs.getConfig();
|
||||
const config = getPublicConfig();
|
||||
|
||||
interface PlayerProgressionExport {
|
||||
PlayerId: number,
|
||||
@@ -34,7 +34,7 @@ export class ProfileProgressionManager extends ProfileContentManager {
|
||||
|
||||
async export() {
|
||||
const ex: PlayerProgressionExport = {
|
||||
PlayerId: this.profileId,
|
||||
PlayerId: this.profile.getId(),
|
||||
Level: await this.getLevel(),
|
||||
XP: await this.getXp()
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class ProfileProgressionManager extends ProfileContentManager {
|
||||
* @returns The new # of XP
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -85,12 +85,12 @@ export class ProfileProgressionManager extends ProfileContentManager {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
const parsedData = parseInt(data);
|
||||
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];
|
||||
if (typeof one == 'undefined' && !one) return 0; // fallback since progression data is required
|
||||
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/>. */
|
||||
|
||||
import { Redis } from "../../db.ts";
|
||||
import { Profile } from "../profiles.ts";
|
||||
import type { Profile } from "./base/profiles.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 {
|
||||
None,
|
||||
@@ -64,7 +64,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
|
||||
#rootKey = Redis.buildKey(
|
||||
Redis.KeyGroups.Profiles.Root,
|
||||
this.profileId.toString(),
|
||||
this.profile.getId().toString(),
|
||||
Redis.KeyGroups.Profiles.Relationships.Root
|
||||
);
|
||||
|
||||
@@ -144,8 +144,8 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
async #clearAssociationWithRemote(remoteProfileId: number) {
|
||||
const remoteRootKey = this.#createRemoteRootKey(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.IncomingFriendRequests), 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.profile.getId());
|
||||
await Redis.Database.srem(this.#outgoingFriends, remoteProfileId);
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
const remoteRootKey = this.#createRemoteRootKey(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) {
|
||||
@@ -182,7 +182,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
const remoteKey = this.#createRemoteRootKey(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;
|
||||
else if (localMuted) return ReciprocalStatus.Local;
|
||||
@@ -194,7 +194,7 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
const remoteKey = this.#createRemoteRootKey(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;
|
||||
else if (localIgnored) return ReciprocalStatus.Local;
|
||||
@@ -213,22 +213,23 @@ export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
async ignoreAllAssociatedPlatformUsers(platformid: string) {
|
||||
const ids = (await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid))).map(val => parseInt(val)).filter(val => !isNaN(val));
|
||||
async ignoreAllAssociatedPlatformUsers(_platformid: string) {
|
||||
/*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) {
|
||||
const profile = Server.UnifiedProfile.get(id);
|
||||
if (!profile) continue;
|
||||
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
|
||||
async getReputation() { // async temporary
|
||||
return {
|
||||
AccountId: this.profileId,
|
||||
AccountId: this.profile.getId(),
|
||||
Noteriety: 0.0,
|
||||
CheerGeneral: 0,
|
||||
CheerHelpful: 0,
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ProfileRoomsManager extends ProfileContentManager {
|
||||
|
||||
#rootKey = Redis.buildKey(
|
||||
Redis.KeyGroups.Profiles.Root,
|
||||
this.profileId.toString(),
|
||||
this.profile.getId().toString(),
|
||||
Redis.KeyGroups.Profiles.Rooms
|
||||
);
|
||||
|
||||
|
||||
@@ -31,30 +31,30 @@ export class ProfileSettingsManager extends ProfileContentManager {
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
for (const key of Object.keys(settings)) returnSettings.push({ Key: key, Value: settings[key] });
|
||||
return returnSettings;
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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() {
|
||||
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
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { Event } from "./baseevent.ts";
|
||||
import { Profile } from "./profiles.ts";
|
||||
import { Profile } from "./profile/base/profiles.ts";
|
||||
|
||||
export enum ProfileEvents {
|
||||
BaseUpdated = "profile.updated"
|
||||
}
|
||||
|
||||
export interface ProfileUpdatedEvent extends Event {
|
||||
export interface ProfileUpdatedEvent {
|
||||
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 * as JsonWebToken from "@gz/jwt";
|
||||
import { Config } from "../config.ts";
|
||||
import { Profile } from "./profiles.ts";
|
||||
import { TokenBaseFormat } from "../apiutils.ts";
|
||||
|
||||
type UserInitOptions = {
|
||||
client_id: string;
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
export enum AuthType {
|
||||
Game,
|
||||
Web,
|
||||
}
|
||||
|
||||
export interface UserTokenFormat extends TokenBaseFormat {
|
||||
sub: string;
|
||||
typ: AuthType.Web;
|
||||
}
|
||||
import { Config } from "../config/config.ts";
|
||||
import { Profile } from "./profile/base/profiles.ts";
|
||||
import type { UserTokenFormat } from "./auth/TokenBaseFormat.ts";
|
||||
import { UserInitOptions, AuthType } from "./UserTypes.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { Redis } from "ioredis";
|
||||
import * as Config from "./config.ts";
|
||||
import * as Config from "./config/config.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import chalk from "npm:chalk@^5.3.0";
|
||||
|
||||
@@ -110,6 +110,9 @@ export const KeyGroups = {
|
||||
Root: "room",
|
||||
PlayerDorms: "player-dormids"
|
||||
},
|
||||
Subrooms: {
|
||||
Root: "subroom"
|
||||
},
|
||||
Operators: "operators",
|
||||
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/>. */
|
||||
|
||||
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 { APIUtils, ProfileTokenSchema } from "./apiutils.ts";
|
||||
import { Discord } from "./discord.ts";
|
||||
import { generateRandomString } from "./apiutils.ts";
|
||||
import { APIUtils } from "./apiutils.ts";
|
||||
import { ProfileTokenSchema } from "./data/auth/TokenSchema.ts";
|
||||
import { generateRandomString } from "./utils.ts";
|
||||
import express from "express";
|
||||
import { decode } from "@gz/jwt";
|
||||
import { ProfileTokenFormat } from "./data/profiles.ts";
|
||||
import { SocketHandoff } from "./socket/handoff.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 Rooms from "./data/content/rooms.ts";
|
||||
import RoomsMisc from "./data/content/rooms/Rooms.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 log = new Log.default("Main");
|
||||
|
||||
Log.LoggingConfiguration.logTiming = Log.LogTiming.Deferred;
|
||||
|
||||
log.i(`Galvanic Corrosion '${await getVersion()}'`);
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
if (typeof config == "undefined") {
|
||||
log.e("Cannot start: Configuration was not found.");
|
||||
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);
|
||||
function exitError(err: string, code?: number) {
|
||||
log.e(`Cannot start: ${err}`);
|
||||
if (code) Deno.exit(code);
|
||||
else 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.Debug = config.logging.debug;
|
||||
@@ -59,8 +61,7 @@ Log.MessageTypeVisibility.Debug = config.logging.debug;
|
||||
try {
|
||||
Database.connect();
|
||||
} catch (err) {
|
||||
log.e(`Cannot start: Redis could not be initialized. ${err}`);
|
||||
Deno.exit(1);
|
||||
exitError(`Redis could not be initialized. ${(err as Error).stack}`);
|
||||
}
|
||||
|
||||
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."));
|
||||
});
|
||||
|
||||
if (!(await Rooms.generateBuiltinRooms())) log.i(`Generated built-in rooms`);
|
||||
if (!(await RoomsMisc.generateBuiltinRooms())) log.i(`Generated built-in rooms`);
|
||||
await Server.CDN.ensureUserDirectory();
|
||||
|
||||
try {
|
||||
@@ -227,6 +228,7 @@ try {
|
||||
Deno.addSignalListener("SIGINT", () => {
|
||||
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("Server"))) Server.UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 if they do not exist
|
||||
@@ -245,6 +247,4 @@ try {
|
||||
} catch (err) {
|
||||
log.e(`Cannot start: Network could not be initalized. ${err}`);
|
||||
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 express from "express";
|
||||
import { Profile } from "../../data/profiles.ts";
|
||||
import { Profile } from "../../data/profile/base/profiles.ts";
|
||||
import { z } from "zod";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import Server from "../../data/server.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import Server from "../../data/server/server.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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 { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import express from "express";
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { ObjectiveType } from "../../data/objectives.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { ObjectiveType } from "../../data/content/ObjectiveTypes.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { Config } from "../../config.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { Config } from "../../config/config.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { GameConfigs } from "../../data/config.ts";
|
||||
import { getPublicConfig } from "../../data/config/PublicConfig.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/config");
|
||||
|
||||
const rateLimit = new APIUtils.RateLimiter();
|
||||
|
||||
route.router.get("/v2", rateLimit.middle(), (_rq, rs) => {
|
||||
const config = GameConfigs.getConfig();
|
||||
const config = getPublicConfig();
|
||||
if (config == null) rs.sendStatus(500);
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { GameConfigs } from "../../data/config.ts";
|
||||
import { GameConfigs } from "../../data/config/GameConfigs.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/images');
|
||||
|
||||
@@ -27,4 +27,17 @@ route.router.get('/v2/named',
|
||||
|
||||
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 { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import express from "express";
|
||||
|
||||
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 { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import express from "express";
|
||||
import Server from "../../data/server.ts";
|
||||
import Server from "../../data/server/server.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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 { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import express from "express";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import { z } from "zod";
|
||||
import Server from "../../data/server.ts";
|
||||
import Server from "../../data/server/server.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { Config } from "../../config.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { Config } from "../../config/config.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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 { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import express from "express";
|
||||
|
||||
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 { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import Rooms from "../../data/content/rooms.ts";
|
||||
import { FactoryMode, RoomDataTypes, WriteMode } from "../../data/content/rooms/DataTypes.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import RoomsMisc from "../../data/content/rooms/Rooms.ts";
|
||||
import { FactoryMode, RoomDataTypes, WriteMode } from "../../data/content/rooms/base/DataTypes.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import express from "express";
|
||||
import { RoomFactory } from "../../data/content/rooms/RoomFactory.ts";
|
||||
import { SubroomFactory } from "../../data/content/rooms/SubroomFactory.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import Rooms from "../../data/content/rooms/base/RoomsBase.ts";
|
||||
|
||||
const log = new Logging("RoomsRoute");
|
||||
|
||||
@@ -83,7 +84,7 @@ route.router.get('/v1/hot',
|
||||
|
||||
async (_rq, rs) => {
|
||||
// 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));
|
||||
},
|
||||
|
||||
@@ -95,7 +96,7 @@ route.router.get('/v2/baserooms',
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
async (_rq, rs) => {
|
||||
const rooms = await Rooms.getAllBuiltinRoomGenerations();
|
||||
const rooms = await RoomsMisc.getAllBuiltinRoomGenerations();
|
||||
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);
|
||||
return;
|
||||
} 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);
|
||||
else rs.sendStatus(404);
|
||||
return;
|
||||
@@ -150,7 +151,7 @@ route.router.get('/v1/agRoomIds',
|
||||
|
||||
async (_rq, rs) => {
|
||||
|
||||
const rooms = await Rooms.getAllBuiltinRoomGenerations();
|
||||
const rooms = await RoomsMisc.getAllBuiltinRoomGenerations();
|
||||
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) => {
|
||||
|
||||
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();
|
||||
|
||||
@@ -231,7 +232,6 @@ route.router.post('/v4/saveData',
|
||||
}
|
||||
|
||||
const subroomFactory = await new SubroomFactory({
|
||||
roomId: currentInstance.roomId,
|
||||
subroomId: rq.body.RoomSceneId,
|
||||
factoryMode: FactoryMode.Write,
|
||||
writeMode: WriteMode.Overwrite
|
||||
@@ -249,10 +249,6 @@ route.router.post('/v4/saveData',
|
||||
await subroomFactory.write();
|
||||
|
||||
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 { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import z from "zod";
|
||||
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 express from "express";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import { StorefrontBalanceType } from "../../data/content/storefronts.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
|
||||
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 express from "express";
|
||||
import { decode } from "@gz/jwt";
|
||||
import { Config } from "../../config.ts";
|
||||
import { Config } from "../../config/config.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { z } from "zod";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import { Redis } from "../../db.ts";
|
||||
import { validVersions } from "../api/versioncheck.ts";
|
||||
import { Steam } from "../../data/steam.ts";
|
||||
import Server from "../../data/server.ts";
|
||||
import Steam from "../../data/steam/steam.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();
|
||||
|
||||
@@ -124,7 +126,7 @@ route.router.post("/token",
|
||||
});
|
||||
}
|
||||
|
||||
const conditionsMet = ![
|
||||
const conditions = [
|
||||
rq.body.client_id === "recroom",
|
||||
rq.body.platform === "0",
|
||||
validVersions.includes(rq.body.ver),
|
||||
@@ -136,7 +138,8 @@ route.router.post("/token",
|
||||
!(rq.body.time.length > 32),
|
||||
!(rq.body.asid.length > 32),
|
||||
SteamPlatformParamsSchema.safeParse(JSON.parse(rq.body.platform_auth)).success
|
||||
].includes(false);
|
||||
];
|
||||
const conditionsMet = !conditions.includes(false);
|
||||
|
||||
if (!conditionsMet) {
|
||||
requestFailed();
|
||||
@@ -164,48 +167,56 @@ route.router.post("/token",
|
||||
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
|
||||
|
||||
}
|
||||
|
||||
|
||||
const platformAuth = (JSON.parse(rq.body.platform_auth)) as SteamPlatformParams;
|
||||
|
||||
let platformid: string | null;
|
||||
if (config.auth.steamkey) {
|
||||
const steamAuthed = await Steam.AuthenticateUserTicket(platformAuth.Ticket, rq.body.platform_id);
|
||||
if (!steamAuthed) {
|
||||
const steamAuth = await Steam.AuthenticateUserTicket(platformAuth.Ticket, rq.body.platform_id);
|
||||
if (steamAuth.valid == SteamAuthResult.Failure) {
|
||||
requestFailed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (steamAuth.valid == SteamAuthResult.Success) platformid = steamAuth.res.steamid;
|
||||
else platformid = null;
|
||||
} else platformid = null;
|
||||
|
||||
if (isNaN(targetAccount)) {
|
||||
requestFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const accounts = await rs.locals.user.getAssociatedProfiles();
|
||||
if (!accounts.has(targetAccount)) {
|
||||
requestFailed("access_denied");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
|
||||
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
|
||||
Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, rq.body.platform_id), targetAccount);
|
||||
|
||||
if (platformid) rs.locals.user.addAssociatedPlatformId(platformid);
|
||||
if (platformid) Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid), targetAccount);
|
||||
|
||||
const profile = Server.UnifiedProfile.get(targetAccount);
|
||||
if (!profile) {
|
||||
requestFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceClass = Number(rq.body.device_class);
|
||||
if (typeof DeviceClass[deviceClass] == 'undefined') {
|
||||
requestFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
const details = await profile.export();
|
||||
log.i(`Player ${details?.username} "${details?.displayName}" (${profile.getId()}) logged in`);
|
||||
|
||||
const token = await profile.getToken();
|
||||
const token = await profile.Auth.getToken();
|
||||
rs.json({
|
||||
access_token: token,
|
||||
refresh_token: token,
|
||||
});
|
||||
|
||||
await profile.setKnownDeviceClass(Number(rq.body.device_class));
|
||||
await profile.setKnownDeviceClass(deviceClass);
|
||||
|
||||
nxt();
|
||||
},
|
||||
|
||||
@@ -17,8 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { APIUtils } from "../apiutils.ts";
|
||||
import { File } from "../data/content/cdn.ts";
|
||||
import Server from "../data/server.ts";
|
||||
import { AuthType } from "../data/users.ts";
|
||||
import Server from "../data/server/server.ts";
|
||||
import { AuthType } from "../data/UserTypes.ts";
|
||||
import { route as ConfigRoute } from "./cdn/config.ts";
|
||||
import express from "express";
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
@@ -19,7 +19,7 @@ import Logging from "@proxnet/undead-logging";
|
||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import Matchmaking from "../../data/live/base.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 { z } from "zod";
|
||||
|
||||
@@ -33,10 +33,10 @@ interface MatchmakingParams {
|
||||
}
|
||||
|
||||
const ProperCaseBooleanSchema = z.preprocess((val) => {
|
||||
if (val === "True") return true;
|
||||
if (val === "False") return false;
|
||||
if (typeof val === "boolean") return val; // allow raw booleans too
|
||||
return val; // will fail validation
|
||||
if (val === "True") return true;
|
||||
if (val === "False") return false;
|
||||
if (typeof val === "boolean") return val;
|
||||
return val;
|
||||
}, z.boolean());
|
||||
|
||||
interface MatchmakingOptions {
|
||||
|
||||
@@ -20,10 +20,10 @@ import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import express from "express";
|
||||
import Matchmaking from "../../data/live/base.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 { 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');
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
import Instances from "../../data/live/instances.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import { AuthType } from "../../data/UserTypes.ts";
|
||||
import express from "express";
|
||||
|
||||
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/>. */
|
||||
|
||||
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";
|
||||
|
||||
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 { APIUtils, NoBody } from "../apiutils.ts";
|
||||
import { AuthType } from "../data/users.ts";
|
||||
import { Buffer } from "node:buffer";
|
||||
import multer from "multer";
|
||||
import { FileType } from "../data/content/cdn.ts";
|
||||
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");
|
||||
|
||||
|
||||
@@ -18,12 +18,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
import { APIUtils, getSrcIpDefault, NoBody } from "../apiutils.ts";
|
||||
// @ts-types = "npm:@types/express"
|
||||
import express from "express";
|
||||
import { User, UserTokenFormat } from "../data/users.ts";
|
||||
import { Config } from "../config.ts";
|
||||
import { User } from "../data/users.ts";
|
||||
import { Config } from "../config/config.ts";
|
||||
import crypto from "node:crypto";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { decode } from "@gz/jwt";
|
||||
import z from "zod";
|
||||
import type { UserTokenFormat } from "../data/auth/TokenBaseFormat.ts";
|
||||
|
||||
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/>. */
|
||||
|
||||
import { APIUtils } from "../apiutils.ts";
|
||||
import { Config } from "../config.ts";
|
||||
import { AuthType } from "../data/users.ts";
|
||||
import { Config } from "../config/config.ts";
|
||||
import { AuthType } from "../data/UserTypes.ts";
|
||||
import { SocketHandoff } from "./handoff.ts";
|
||||
|
||||
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
|
||||
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 {
|
||||
CompletionMessage,
|
||||
@@ -34,9 +34,8 @@ import {
|
||||
import { SocketTarget } from "./targets/targetbase.ts";
|
||||
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
||||
import Presence from "../data/live/presence.ts";
|
||||
import Matchmaking from "../data/live/base.ts";
|
||||
|
||||
const logmessages = false;
|
||||
const logmessages = true;
|
||||
|
||||
export class SignalRSocketHandler {
|
||||
|
||||
@@ -62,13 +61,11 @@ export class SignalRSocketHandler {
|
||||
|
||||
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
|
||||
|
||||
for (const target of this.#Targets.values()) target.onInit();
|
||||
|
||||
this.#PeriodicalId = setInterval(async () => {
|
||||
if (this.#killed) return;
|
||||
if (this.#socket.readyState !== this.#socket.CLOSED) {
|
||||
const pres = await Presence.get(this.#profile);
|
||||
this.sendNotification("PresenceUpdate", await pres.export());
|
||||
this.sendRaw({ type: 6 });
|
||||
}
|
||||
if (this.#socket.readyState == this.#socket.OPEN) await this.presenceUpdate();
|
||||
}, 8000);
|
||||
|
||||
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> {
|
||||
if (this.#killed) {
|
||||
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));
|
||||
}
|
||||
|
||||
destroy(sock: SignalRSocketHandler, internal: boolean | undefined = false) {
|
||||
return () => {
|
||||
sock.#killed = true;
|
||||
clearInterval(sock.#PeriodicalId);
|
||||
sock.sendRaw({ type: 7, error: "Socket closed" });
|
||||
if (!internal) sock.#socket.close();
|
||||
sock.#log.i(`Closed socket`);
|
||||
sock.#profile.clearSocketHandler();
|
||||
destroy(handler: SignalRSocketHandler, internal: boolean | undefined = false) {
|
||||
return (ev: CloseEvent) => {
|
||||
handler.#killed = true;
|
||||
clearInterval(handler.#PeriodicalId);
|
||||
|
||||
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);
|
||||
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 { SocketTarget } from "./targetbase.ts";
|
||||
import { SelfAccountExport } from "../../data/profiles.ts";
|
||||
import { ProfileEvents, ProfileUpdatedEvent } from "../../data/profileevents.ts";
|
||||
import type { Profile } from "../../data/profile/base/profiles.ts";
|
||||
import type { ProfileUpdatedEvent } from "../../data/profileevents.ts";
|
||||
import { PushNotificationId } from "../types.ts";
|
||||
import Server from "../../data/server.ts";
|
||||
import Server from "../../data/server/server.ts";
|
||||
|
||||
const ArgumentSchema = z.object({
|
||||
PlayerIds: z.array(z.number())
|
||||
@@ -28,46 +28,30 @@ const ArgumentSchema = z.object({
|
||||
|
||||
export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
||||
|
||||
updateSocket(profile: SelfAccountExport) {
|
||||
this.socket.sendNotification(PushNotificationId.SubscriptionUpdateProfile, profile);
|
||||
async updateSocket(profile: Profile) {
|
||||
this.socket.sendNotification(PushNotificationId.SubscriptionUpdateProfile, await profile.export() || undefined);
|
||||
}
|
||||
|
||||
subscriptions: { id: number, callback: (ev: unknown) => void }[] = [];
|
||||
|
||||
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);
|
||||
}
|
||||
)) });
|
||||
}
|
||||
subscriptions: number[] = [];
|
||||
|
||||
#callback = (ev: ProfileUpdatedEvent) => {
|
||||
if (this.subscriptions.includes(ev.profile.getId()))
|
||||
this.updateSocket(ev.profile);
|
||||
}
|
||||
|
||||
clearSubscriptions() {
|
||||
for (const sub of this.subscriptions) {
|
||||
const profile = Server.UnifiedProfile.get(sub.id);
|
||||
if (profile)
|
||||
profile.off(ProfileEvents.BaseUpdated, sub.callback);
|
||||
}
|
||||
override onInit() {
|
||||
Server.on('profile.updated', this.#callback);
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
this.clearSubscriptions();
|
||||
override onDestroy() {
|
||||
Server.off('profile.updated', this.#callback);
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
override async exec(args: unknown) {
|
||||
const parsed = ArgumentSchema.safeParse(args);
|
||||
if (parsed.success) {
|
||||
this.setSubscriptions(parsed.data.PlayerIds);
|
||||
this.subscriptions = parsed.data.PlayerIds;
|
||||
return;
|
||||
} 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
|
||||
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 {
|
||||
|
||||
@@ -25,7 +25,11 @@ export class SocketTarget {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
onInit() {
|
||||
return;
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
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
|
||||
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";
|
||||
|
||||
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
|
||||
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' {
|
||||
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