This repository has been archived on 2026-03-19. You can view files and clone it, but cannot push or open issues or pull requests.
Files
galvanic-corrosion/src/socket/socket.ts
zombieb 1cfd0426dd FLINT AND STEEL!!!! THE NETHER!!!!!! RELEASE!!!!!!!!!
* Account bio support (fetch only route right now)
* Room cloning fixes
    - Dorm Room cloning is still broken
* Instance changing fixes
* Presence: VRMovementMode and StatusVisibility updates automatically
* Routes for the above two properties
* Settings can take numbers, too (enums)
* No microtransations in my game (parental controls)
* A whole lotta routes for various unfinished but planned features
    - Equipment
    - Consumables
    - Objectives
    - Checklist (orientation rewards)
    - Objectives (three daily tasks)
    - Image metadata
    - Community Board
    - Player Events
    - Storefronts
* Matchmaking instance querying
    - Empty instances are not yet cleared
* Avatar items, saved avatars, save current avatar routes
* No loading screen tips for now
* Send presence at an interval over the socket
    - Error FROSTBITE is reported in the game logs during bootup sometimes. Maybe due to the lack of ping messages?
* Socket push notifications

Note to self: Set up deno compilation in runners on gitea
2025-04-02 23:56:18 -04:00

174 lines
6.5 KiB
TypeScript

/* 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 { Profile } from "../data/profiles.ts";
import Logging from "@proxnet/undead-logging";
import {
CompletionMessage,
Message,
MessageKind,
PushNotificationId,
SignalMessageType,
SignalRMessage,
SignalRMessageSchema,
TargetResult,
TargetResultFailure,
TargetResultNotATarget,
TargetResultSuccess,
TargetResultType
} from "./types.ts";
import { SocketTarget } from "./targets/targetbase.ts";
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
import Presence from "../data/live/presence.ts";
export class SignalRSocketHandler {
#log: Logging = new Logging("SignalMock-");
#socket: WebSocket;
#profile: Profile;
#Targets: Map<string, SocketTarget> = new Map();
#PresenceUpdateId: number;
constructor(socket: WebSocket, player: Profile) {
this.#socket = socket;
this.#profile = player;
this.#init();
player.setSocketHandler(this);
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget());
this.#PresenceUpdateId = setInterval(async () => {
const pres = await Presence.get(this.#profile);
this.sendNotification("PresenceUpdate", await pres.export());
}, 8000);
}
async #dispatchTarget<T = unknown>(target: string, args: unknown): Promise<TargetResult> {
const targetExec = this.#Targets.get(target);
if (!targetExec) return { type: TargetResultType.NotATarget } as TargetResultNotATarget;
else {
try {
return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
} catch (err) {
this.#log.w(`Target '${target}' function error: ${err}`);
if (err instanceof Error) return { type: TargetResultType.Failure, err: err } as TargetResultFailure;
else return { type: TargetResultType.Failure, err: `${err}` } as TargetResultFailure;
}
}
}
async #onMessage(message: Message) {
if (message.kind == MessageKind.Protocol) {
this.sendRaw({});
return;
} else {
this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
if (message.data.type == SignalMessageType.Invocation && message.data.invocationId) { // don't send completion messages for nonblocking invocations
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
if (res.type == TargetResultType.Success) {
const signalRes: CompletionMessage = {
type: SignalMessageType.Completion,
invocationId: message.data.invocationId,
result: JSON.stringify(res.data)
}
this.sendRaw(signalRes);
} else if (res.type == TargetResultType.Failure) {
const signalRes: CompletionMessage = {
type: SignalMessageType.Completion,
invocationId: message.data.invocationId,
error: res.err instanceof Error ? res.err.message : res.err
}
this.sendRaw(signalRes);
} else {
const signalRes: CompletionMessage = {
type: SignalMessageType.Completion,
invocationId: message.data.invocationId,
error: "Target not found"
}
this.sendRaw(signalRes);
}
}
}
}
#init() {
this.#log.source += this.#profile.getId().toString();
this.#log.i(`Created hub socket`);
this.#socket.addEventListener('message', message => {
try {
const dec = new TextDecoder();
const str = dec.decode(message.data);
const data = JSON.parse(str.substring(0, str.length - 1));
const parseResult = SignalRMessageSchema.safeParse(data);
if (parseResult.success) this.#onMessage({
kind: MessageKind.Data,
data: parseResult.data as SignalRMessage
});
else {
this.#onMessage({
kind: MessageKind.Protocol
});
}
} catch (err) {
this.#log.e(`Socket error: ${err}`);
}
});
this.#socket.addEventListener('close', this.destroy(this));
}
destroy(sock: SignalRSocketHandler) {
return () => {
clearInterval(sock.#PresenceUpdateId);
sock.sendRaw({ type: 7, error: "Socket closed" });
sock.#socket.close();
sock.#log.i(`Closed hub socket`);
}
}
sendRaw(data: object) {
this.#socket.send(`${JSON.stringify(data)}\u001e`);
// todo sometime: make this less confusing
const type = `Type: ${JSON.stringify(data) == '{}' ? 'Protocol Message' : `${(data as SignalRMessage).type} (${SignalMessageType[(data as SignalRMessage).type]})`}`;
this.#log.d(`SERVER MESSAGE\n ${type}\n ${JSON.stringify(data as SignalRMessage)}`);
}
sendNotification(id: PushNotificationId | string, args: object) {
const msg: SignalRMessage = {
type: SignalMessageType.Invocation,
target: "Notification",
arguments: [JSON.stringify({
Id: id,
Msg: args
})]
}
this.sendRaw(msg);
}
}