This commit is contained in:
2025-07-27 19:49:35 -04:00
parent 2302290d34
commit 941c8400c0
23 changed files with 293 additions and 50 deletions

View File

@@ -36,7 +36,8 @@ await routeImporter(AppRoot.app, 'src/', [
'routes/root',
'routes/api',
'routes/auth',
'routes/accounts'
'routes/accounts',
'routes/match'
]);
// deno-lint-ignore require-await
@@ -71,8 +72,8 @@ const server = Deno.serve({ hostname: "10.0.1.39", port: 13370, onListen: addr =
availableTransports: [{transport:"WebSockets",transferFormats:["Text"]}]
}), { headers: { 'Content-Type': 'application/json' }});
}
if (req.headers.get('Connection')?.includes('Upgrade') && req.headers.get('Upgrade') === 'websocket') {
const isSignalR = req.headers.has('id');
if (req.headers.get('Connection')?.includes('Upgrade') && req.headers.get('Upgrade')?.includes('websocket')) {
const isSignalR = url.searchParams.has('id');
if (isSignalR) {
try {
const authHeader = req.headers.get('Authorization');

View File

@@ -3,9 +3,6 @@ import { authenticate } from "../../../util/api.ts";
import Server from "../../../server/server.ts";
import z from "zod";
import { typedZValidator } from "../../../util/validators.ts";
import Logging from "@proxnet/undead-logging";
const log = new Logging("AccountDebug");
export const route = createHonoRoute('/account');

View File

@@ -0,0 +1,18 @@
import { authenticate } from "../../../util/api.ts";
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/PlayerReporting');
route.app.use(authenticate);
route.app.get('/v1/moderationBlockDetails', c => {
return c.json({
ReportCategory: 0,
Duration: 0,
GameSessionId: 0,
IsHostKick: false,
VoteKickReason: "",
Message: "",
IsBan: false
});
});

View File

@@ -1,7 +1,27 @@
import { profileAvatarSchema } from "../../../server/profiles/content/Avatar.ts";
import Server from "../../../server/server.ts";
import { authenticate } from "../../../util/api.ts";
import { createHonoRoute } from "../../../util/import.ts";
import { typedZValidator } from "../../../util/validators.ts";
export const route = createHonoRoute("/avatar");
route.app.get('/v1/defaultunlocked', c => {
return c.json([]);
return c.json(Server.Avatars.getAllPossibleCombinations());
});
route.app.get('/v4/items', c => {
return c.json(Server.Avatars.getAllPossibleCombinations());
});
route.app.use(authenticate);
route.app.get('/v2', async c => {
const outfit = await c.get('profile').Avatar.getAvatar();
return c.json(outfit);
});
route.app.post('/v2/set', typedZValidator('json', profileAvatarSchema), async c => {
const outfit = c.req.valid('json');
await c.get('profile').Avatar.setAvatar(outfit);
return c.status(200);
});

View File

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

View File

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

View File

@@ -3,17 +3,20 @@ import { authenticate, genericResponse } from "../../../util/api.ts";
import { createHonoRoute } from "../../../util/import.ts";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
import { ProfileSetting } from "../../../server/profiles/content/Settings.ts";
import { trimTrailingSlash } from "@hono/hono/trailing-slash";
import { HonoEnv } from "../../../util/types.ts";
import { Context } from "@hono/hono";
export const route = createHonoRoute('/settings');
route.app.use(authenticate, trimTrailingSlash());
route.app.use(authenticate);
route.app.get('/v2', async c => {
const getSettingsMiddleware = async (c: Context<HonoEnv>) => {
const profile = c.get('profile');
const settings = await profile.Settings.getAllSettings();
c.json(settings);
});
return c.json(settings);
};
route.app.get('/v2/', getSettingsMiddleware);
route.app.get('/v2', getSettingsMiddleware);
const settingsSetSchema = z.object({
Key: z.string().transform(transformStringToEnum<ProfileSetting>(ProfileSetting, true)),

View File

@@ -3,6 +3,10 @@ import { createHonoRoute } from "../../../util/import.ts";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
import { PlatformType } from "../../../server/platforms/base.ts";
import Server from "../../../server/server.ts";
import { authenticate } from "../../../util/api.ts";
import Logging from "@proxnet/undead-logging";
const log = new Logging("CachedLoginDebug");
export const route = createHonoRoute("/cachedlogin");
@@ -16,3 +20,14 @@ route.app.get('/forplatformid/:platformType/:platformId', typedZValidator('param
return c.json((await Server.Platforms.getCachedLogins(platformType || PlatformType.Steam, platformId, true)) || []);
});
route.app.use(authenticate);
const forPlatformIdsReqSchema = z.object({
id: z.string()
});
route.app.post('/forplatformids', typedZValidator('form', forPlatformIdsReqSchema), async c => {
const { id } = c.req.valid('form');
const ids = await Server.Platforms.getCachedLogins(PlatformType.Steam, id, true);
return c.json(ids || []);
});

View File

@@ -117,6 +117,7 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
if (logins && logins.find(login => login.accountId === form.account_id)) {
const profile = await Server.Profiles.get(form.account_id);
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id);
const token = await Server.Platforms.getToken(profile.getId(), await profile.getRole() || 'user');
return c.json({

7
src/routes/match/root.ts Normal file
View File

@@ -0,0 +1,7 @@
import { createHonoRoute, routeImporter } from "../../util/import.ts";
export const route = createHonoRoute('/match');
await routeImporter(route.app, 'src/routes/match/', [
'routes'
]);

View File

@@ -0,0 +1,10 @@
import { authenticate } from "../../../util/api.ts";
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/player");
route.app.use(authenticate);
route.app.post('/login', async c => {
return c.status(200);
});

View File

@@ -1,5 +1,72 @@
import path from "node:path";
import { ServerContentBase } from "../ContentBase.ts";
import { RootPath } from "../../util/path.ts";
interface AvatarImport {
allPossibleCombinations: {
_avatarItemData: {
Name: string
},
_avatarItemVisualData: {
prefabGuid: string,
maskGuid: string,
swatchGuid: string,
decalGuid: string
}
}[]
}
interface AvatarItemExport {
AvatarItemType: AvatarItemType,
AvatarItemDesc: string,
FriendlyName: string,
Tooltip: string,
Rarity: ItemRarity
}
export enum AvatarItemType {
Outfit,
HairDye
}
export enum ItemRarity {
None = -1,
Common,
Uncommon = 10,
Rare = 20,
Epic = 30,
Legendary = 50
}
export class AvatarContentBase extends ServerContentBase {
#rawImport: AvatarItemExport[] = [];
getAllPossibleCombinations() {
return this.#rawImport;
}
formatAllPossibleCombinations() {
const parsed = JSON.parse(Deno.readTextFileSync(path.join(RootPath, '/res/avatar.json'))) as AvatarImport;
function formatVisualData(data: {
prefabGuid: string,
maskGuid: string,
swatchGuid: string,
decalGuid: string
}) {
const p = data.prefabGuid ? data.prefabGuid : '';
const m = data.maskGuid ? data.maskGuid : '';
const s = data.swatchGuid ? data.swatchGuid : '';
const d = data.decalGuid ? data.decalGuid : '';
return `${p},${s},${m},${d},`
}
this.#rawImport = parsed.allPossibleCombinations.map(data => ({
AvatarItemType: AvatarItemType.Outfit,
AvatarItemDesc: formatVisualData(data._avatarItemVisualData),
FriendlyName: data._avatarItemData.Name,
Tooltip: "pre-avatar update item",
Rarity: ItemRarity.None
}));
}
override start() {
this.formatAllPossibleCombinations();
}
}

View File

@@ -1,6 +1,5 @@
import z from "zod";
import { CommandExec } from "./cmdtypes.ts";
export interface CommandOptions {
key: string[],
subcommands?: Command[],
@@ -16,7 +15,6 @@ export default class Command {
exec: CommandExec | null;
validate: z.ZodTuple | null;
help: string | null;
#argsLength: number;
constructor(options: CommandOptions) {
this.subCmds = options.subcommands || [];
@@ -24,7 +22,6 @@ export default class Command {
this.exec = options.exec || null;
this.validate = options.zod || null;
this.help = options.help || null;
this.#argsLength = options.zod ? options.zod.def.items.length : 0;
}
getKey() {
@@ -40,12 +37,10 @@ export default class Command {
const cmd = this.subCmds.find(cmd => cmd.getKey().includes(root));
if (cmd) {
const newArgs = args.slice(1);
if (cmd.#argsLength && newArgs.length !== cmd.#argsLength && cmd.help) return new Error(cmd.help);
if (cmd.validate) {
const res = cmd.validate.safeParse(newArgs);
if (res.success) return cmd.dispatch(...res.data);
else if (cmd.help) return new Error(cmd.help);
else if (cmd.#argsLength) return new Error(`'${root}' validation error: expected ${cmd.#argsLength} args, got ${newArgs.length}`);
else return new Error(`'${root}' validation error`);
}
}

View File

@@ -6,8 +6,6 @@ export default class GameConfigsBase extends ServerContentBase {
#kvKey = 'gameconfigs';
#usageError = new Error("Usage: <key: string> [<value: string>]");
async getGameConfig(key: string) {
return (await this.kv.getKv().get<string>([this.#kvKey, key])).value;
}
@@ -25,17 +23,16 @@ export default class GameConfigsBase extends ServerContentBase {
this.kv.getKv().list<string>({ prefix: [this.#kvKey] })
)).map(val =>
({ Key: val.key[1] as string, Value: val.value })
).filter(gc => gc.Key && gc.Value); // ensure both the key and value exist
).filter(gc => gc.Key && typeof gc.Value == 'string');
}
override start(): void {
override start() {
this.server.Commands.addRootCommand(new Command({
key: ['gameconfigs', 'gameconfig', 'gc'],
subcommands: [
new Command({
key: ['get', 'g'],
exec: async (key: string) => {
if (!key) return this.#usageError;
return await this.getGameConfig(key);
},
zod: z.tuple([
@@ -45,20 +42,18 @@ export default class GameConfigsBase extends ServerContentBase {
}),
new Command({
key: ['set', 's'],
exec: async (key: string, value: string) => {
if (!key || !value) return this.#usageError;
exec: async (key: string, ...values: string[]) => {
const value = values.join(' ');
return await this.setGameConfig(key, value);
},
zod: z.tuple([
z.string(),
z.string()
]),
help: 'Set a new GameConfig: <key: string>, <value: string>'
]).rest(z.string().optional()),
help: 'Set a new GameConfig: <key: string>, ...<values: string[] (joined by " ")>'
}),
new Command({
key: ['del', 'rem', 'd', 'remove'],
exec: async (key: string) => {
if (!key) return this.#usageError;
return await this.delGameConfig(key);
},
zod: z.tuple([
@@ -72,6 +67,7 @@ export default class GameConfigsBase extends ServerContentBase {
const configs = await this.getAllGameConfigs();
return configs;
},
zod: z.tuple([]),
help: 'List all saved GameConfigs'
}),
]

View File

@@ -0,0 +1,16 @@
import { ServerContentBase } from "../ContentBase.ts";
import type Profile from "../profiles/profile.ts";
class Presence {
}
export class PresenceBase extends ServerContentBase {
#presenceMap: Map<Profile, Presence> = new Map();
getPresence() {
}
}

View File

@@ -0,0 +1,35 @@
import z from "zod";
import ProfileContentManager from "./base.ts";
export const profileAvatarSchema = z.object({
OutfitSelections: z.string(),
HairColor: z.string(),
SkinColor: z.string(),
FaceFeatures: z.string(),
});
export type ProfileAvatar = z.infer<typeof profileAvatarSchema>;
export class ProfileAvatarManager extends ProfileContentManager {
#key = this.profile.constructProfilePropertyKey('avatar');
#noAvatar: ProfileAvatar = {
OutfitSelections: "",
HairColor: "",
SkinColor: "",
FaceFeatures: ""
}
async getAvatar(): Promise<ProfileAvatar> {
const item = await this.kv.getKv().get(this.#key);
if (item.value) {
const parsed = profileAvatarSchema.safeParse(item.value);
if (parsed.success) return parsed.data;
else return this.#noAvatar;
} else return this.#noAvatar;
}
async setAvatar(outfit: ProfileAvatar) {
await this.kv.getKv().set(this.#key, outfit);
}
}

View File

@@ -0,0 +1,34 @@
import ProfileContentManager from "./base.ts";
export enum DeviceClass {
Unknown,
VR,
Screen,
Mobile,
VRLow,
Quest2
}
export class ProfileMatchmakingManager extends ProfileContentManager {
#deviceClassKey = this.profile.constructProfilePropertyKey('deviceclass');
async setLastDeviceClass(dc: DeviceClass) {
await this.kv.getKv().set(this.#deviceClassKey, dc);
}
async getLastDeviceClass(): Promise<DeviceClass | null> {
return (await this.kv.getKv().get<DeviceClass>(this.#deviceClassKey)).value || null;
}
#loginLockKey = this.profile.constructProfilePropertyKey('loginlock');
async setLoginLock(lock: string) {
await this.kv.getKv().set(this.#deviceClassKey, lock);
}
async getLoginLock(): Promise<string> {
return (await this.kv.getKv().get<string>(this.#loginLockKey)).value || "";
}
async hasLoginLock() {
return (await this.kv.getKv().get<string>(this.#loginLockKey)).value ? true : false;
}
}

View File

@@ -2,6 +2,7 @@ import KV from "../persistence/kv.ts";
import { ProfileRole } from "../platforms/base.ts";
import { type ServerBase } from "../server.ts";
import { type SignalRSocketHandler } from "../socket/signalr/socket.ts";
import { ProfileAvatarManager } from "./content/Avatar.ts";
import { ProfileSettingsManager } from "./content/Settings.ts";
import ProfileManagerBase from "./manager.ts";
import { recNetAccountSchema, SelfAccount, type RecNetAccount } from "./types/profile.ts";
@@ -17,6 +18,7 @@ class Profile {
#selfAcc: SelfAccount;
Settings: ProfileSettingsManager;
Avatar: ProfileAvatarManager;
constructor(acc: SelfAccount, kv: KV, server: ServerBase) {
this.#id = acc.accountId;
@@ -25,6 +27,7 @@ class Profile {
this.#server = server;
this.Settings = new ProfileSettingsManager(this, this.#kv);
this.Avatar = new ProfileAvatarManager(this, this.#kv);
}
async #saveSelfAcc() {

View File

@@ -1,3 +1,4 @@
import { AvatarContentBase } from "./avatars/base.ts";
import { EventManager } from "./baseevent.ts";
import { CommandsBase } from "./commands/commands.ts";
import GameConfigsBase from "./gameconfigs/base.ts";
@@ -16,6 +17,7 @@ class ServerBase extends EventManager<ServerEvents> {
GameConfigs = new GameConfigsBase(this, 'gameconfigs', true);
Commands = new CommandsBase(this, 'commands');
Platforms = new PlatformsManager(this, 'platforms', true);
Avatars = new AvatarContentBase(this, 'avatars');
}
const Server = new ServerBase();

View File

@@ -16,6 +16,7 @@ import {
import { SocketTarget } from "./targets/targetbase.ts";
import type Profile from "../../profiles/profile.ts";
import { detailedLog } from "../../../main.ts";
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
const logmessages = true;
@@ -39,7 +40,7 @@ export class SignalRSocketHandler {
player.setSocketHandler(this);
//this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
for (const target of this.#Targets.values()) target.onInit();
@@ -55,7 +56,9 @@ export class SignalRSocketHandler {
if (!targetExec) return { type: TargetResultType.NotATarget } as TargetResultNotATarget;
else {
try {
return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
const parsed = targetExec.zod.safeParse(args);
if (parsed.success) return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
else return { type: TargetResultType.Failure, err: "Argument parse failure" } as TargetResultFailure;
} catch (err) {
this.#log.w(`Target '${target}' function error: ${err}`);
if (err instanceof Error) return { type: TargetResultType.Failure, err: err } as TargetResultFailure;

View File

@@ -0,0 +1,18 @@
import z from "zod";
import { SocketTarget } from "./targetbase.ts";
export class PlayerSocketSubscriptionTarget extends SocketTarget {
#ids: number[] = [];
override zod = z.tuple([]).rest(z.number());
override exec(...ids: number[]) {
this.#ids = ids;
}
getIds() {
return this.#ids;
}
}

View File

@@ -1,26 +1,12 @@
/* 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 type { SignalRSocketHandler } from "../socket.ts";
export class SocketTarget {
socket: SignalRSocketHandler;
zod: z.ZodTuple = z.tuple([]);
constructor(socket: SignalRSocketHandler) {
this.socket = socket;
}
@@ -33,8 +19,7 @@ export class SocketTarget {
return;
}
// deno-lint-ignore require-await
async exec(_args: unknown) {
exec(_args: unknown): Promise<unknown> | unknown {
throw new Error("Execution for this target is not set.");
}

3
src/util/path.ts Normal file
View File

@@ -0,0 +1,3 @@
import { platform } from "node:process";
export const RootPath = Deno.mainModule.substring(platform == 'win32' ? 8 : 7, Deno.mainModule.length - 11);