All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 1m50s
* Commit hash shipped with builds
* Post & pre-build events
* Objective fixes
* Orientation challenge filler
* Custom Rooms base
- Currently cannot save rooms (CDN not set up)
* Moved root path to path.ts
* Room cloning
* Rewrote instances - the whole thing
* Relationships are still untested
* Charades Words
* AG Room fetch
* Private room matchmaking
* Socket fixes
201 lines
7.4 KiB
TypeScript
201 lines
7.4 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";
|
|
import Matchmaking from "../data/live/base.ts";
|
|
|
|
const logmessages = false;
|
|
|
|
export class SignalRSocketHandler {
|
|
|
|
#log: Logging = new Logging("SignalMock-");
|
|
|
|
#socket: WebSocket;
|
|
#profile: Profile;
|
|
|
|
#Targets: Map<string, SocketTarget> = new Map();
|
|
|
|
#PeriodicalId: number;
|
|
|
|
#killed = false;
|
|
|
|
constructor(socket: WebSocket, player: Profile) {
|
|
|
|
this.#socket = socket;
|
|
this.#profile = player;
|
|
|
|
this.#init();
|
|
|
|
player.setSocketHandler(this);
|
|
|
|
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
|
|
|
|
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 });
|
|
}
|
|
}, 8000);
|
|
|
|
this.#socket.onclose = (ev) => {
|
|
this.#log.d(`Close reason: ${ev.reason}`);
|
|
}
|
|
|
|
}
|
|
|
|
async #dispatchTarget<T = unknown>(target: string, args: unknown): Promise<TargetResult> {
|
|
if (this.#killed) {
|
|
const error = "Tried to dispatch socket target on dead socket";
|
|
this.#log.w(error);
|
|
return { type: TargetResultType.Failure, err: error };
|
|
}
|
|
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 {
|
|
if (logmessages) 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, 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();
|
|
|
|
for (const target of sock.#Targets.values()) target.destroy();
|
|
|
|
this.#profile.getInstance()?.removePlayer(this.#profile);
|
|
Matchmaking.deleteLoginLock(this.#profile);
|
|
}
|
|
}
|
|
|
|
sendRaw(data: object) {
|
|
this.#socket.send(`${JSON.stringify(data)}\u001e`);
|
|
// todo sometime: make the below less confusing
|
|
if (logmessages) {
|
|
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 ? args : {}
|
|
})]
|
|
}
|
|
this.sendRaw(msg);
|
|
}
|
|
|
|
} |