Files
foundryvtt-socketlib/src/socketlib.js

354 lines
12 KiB
JavaScript

import {libWrapper} from "./libwrapper_shim.js";
import * as errors from "./errors.js";
const RECIPIENT_TYPES = {
ONE_GM: 0,
ALL_GMS: 1,
EVERYONE: 2,
}
const MESSAGE_TYPES = {
COMMAND: 0,
REQUEST: 1,
RESPONSE: 2,
RESULT: 3,
EXCEPTION: 4,
UNREGISTERED: 5,
}
Hooks.once("init", () => {
window.socketlib = new Socketlib();
libWrapper.register("socketlib", "Users.prototype.constructor._handleUserActivity", handleUserActivity);
Hooks.callAll("socketlib.ready");
}, "WRAPPER");
class Socketlib {
constructor() {
this.modules = new Map();
this.system = undefined;
this.errors = errors;
}
registerModule(moduleName) {
const existingSocket = this.modules.get(moduleName);
if (existingSocket)
return existingSocket;
const module = game.modules.get(moduleName);
if (!module?.active) {
console.error(`socketlib | Someone tried to register module '${moduleName}', but no module with that name is active. As a result the registration request has been ignored.`);
return undefined;
}
if (!module.data.socket) {
console.error(`socketlib | Failed to register socket for module '${moduleName}'. Please set '"socket":true' in your manifset and restart foundry (you need to reload your world - simply reloading your browser won't do).`);
return undefined;
}
const newSocket = new SocketlibSocket(moduleName, "module");
this.modules.set(moduleName, newSocket);
return newSocket;
}
registerSystem(systemId) {
if (game.system.id !== systemId) {
console.error(`socketlib | Someone tried to register system '${systemId}', but that system isn't active. As a result the registration request has been ignored.`);
return undefined;
}
const existingSocket = this.system;
if (existingSocket)
return existingSocket;
if (!game.system.data.socket) {
console.error(`socketlib | Failed to register socket for system '${systemId}'. Please set '"socket":true' in your manifest and restart foundry (you need to reload your world - simply reloading your browser won't do).`);
}
const newSocket = new SocketlibSocket(systemId, "system");
this.system = newSocket;
return newSocket;
}
}
class SocketlibSocket {
constructor(moduleName, moduleType) {
this.functions = new Map();
this.socketName = `${moduleType}.${moduleName}`;
this.pendingRequests = new Map();
game.socket.on(this.socketName, this._onSocketReceived.bind(this));
}
register(name, func) {
if (!(func instanceof Function)) {
console.error(`socketlib | Cannot register non-function as socket handler for '${name}' for '${this.socketName}'.`);
return;
}
if (this.functions.has(name)) {
console.warn(`socketlib | Function '${name}' is already registered for '${this.socketName}'. Ignoring registration request.`);
return;
}
this.functions.set(name, func);
}
async executeAsGM(handler, ...args) {
const [name, func] = this._resolveFunction(handler);
if (game.user.isGM) {
return this._executeLocal(func, ...args);
}
else {
if (!game.users.find(isActiveGM)) {
throw new errors.SocketlibNoGMConnectedError(`Could not execute handler '${name}' (${func.name}) as GM, because no GM is connected.`);
}
return this._sendRequest(name, args, RECIPIENT_TYPES.ONE_GM);
}
}
async executeAsUser(handler, userId, ...args) {
const [name, func] = this._resolveFunction(handler);
if (userId === game.userId)
return this._executeLocal(func, ...args);
const user = game.users.get(userId);
if (!user)
throw new errors.SocketlibInvalidUserError(`No user with id '${userId}' exists.`);
if (!user.active)
throw new errors.SocketlibInvalidUserError(`User '${user.name}' (${userId}) is not connected.`);
return this._sendRequest(name, args, [userId]);
}
async executeForAllGMs(handler, ...args) {
const [name, func] = this._resolveFunction(handler);
this._sendCommand(name, args, RECIPIENT_TYPES.ALL_GMS);
if (game.user.isGM) {
try {
this._executeLocal(func, ...args);
}
catch (e) {
console.error(e);
}
}
}
async executeForOtherGMs(handler, ...args) {
const [name, func] = this._resolveFunction(handler);
this._sendCommand(name, args, RECIPIENT_TYPES.ALL_GMS);
}
async executeForEveryone(handler, ...args) {
const [name, func] = this._resolveFunction(handler);
this._sendCommand(name, args, RECIPIENT_TYPES.EVERYONE);
try {
this._executeLocal(func, ...args);
} catch (e) {
console.error(e);
}
}
async executeForOthers(handler, ...args) {
const [name, func] = this._resolveFunction(handler);
this._sendCommand(name, args, RECIPIENT_TYPES.EVERYONE);
}
async executeForUsers(handler, recipients, ...args) {
if (!(recipients instanceof Array))
throw new TypeError("Recipients parameter must be an array of user ids.");
const [name, func] = this._resolveFunction(handler);
const currentUserIndex = recipients.indexOf(game.userId);
if (currentUserIndex >= 0)
recipients.splice(currentUserIndex, 1);
this._sendCommand(name, args, recipients);
if (currentUserIndex >= 0) {
try {
this._executeLocal(func, ...args);
}
catch (e) {
console.error(e);
}
}
}
_sendRequest(handlerName, args, recipient) {
const message = {handlerName, args, recipient};
message.id = randomID();
message.type = MESSAGE_TYPES.REQUEST;
const promise = new Promise((resolve, reject) => this.pendingRequests.set(message.id, {handlerName, resolve, reject, recipient}));
game.socket.emit(this.socketName, message);
return promise;
}
_sendCommand(handlerName, args, recipient) {
const message = {handlerName, args, recipient};
message.type = MESSAGE_TYPES.COMMAND;
game.socket.emit(this.socketName, message);
}
_sendResult(id, result) {
const message = {id, result};
message.type = MESSAGE_TYPES.RESULT;
game.socket.emit(this.socketName, message);
}
_sendError(id, type) {
const message = {id, type};
message.userId = game.userId;
game.socket.emit(this.socketName, message);
}
_executeLocal(func, ...args) {
const socketdata = {userId: game.userId};
return func.call({socketdata}, ...args);
}
_resolveFunction(func) {
if (func instanceof Function) {
const entry = Array.from(this.functions.entries()).find(([key, val]) => val === func);
if (!entry)
throw new errors.SocketlibUnregisteredHandlerError(`Function '${func.name}' has not been registered as a socket handler.`);
return [entry[0], func];
}
else {
const fn = this.functions.get(func);
if (!fn)
throw new errors.SocketlibUnregisteredHandlerError(`No socket handler with the name '${func}' has been registered.`)
return [func, fn];
}
}
_onSocketReceived(message, senderId) {
if (message.type === MESSAGE_TYPES.COMMAND || message.type === MESSAGE_TYPES.REQUEST)
this._handleRequest(message, senderId);
else
this._handleResponse(message, senderId);
}
async _handleRequest(message, senderId) {
const {handlerName, args, recipient, id, type} = message;
// Check if we're the recipient of the received message. If not, return early.
if (recipient instanceof Array) {
if (!recipient.includes(game.userId))
return;
}
else {
switch (recipient) {
case RECIPIENT_TYPES.ONE_GM:
if (!isResponsibleGM())
return;
break;
case RECIPIENT_TYPES.ALL_GMS:
if (!game.user.isGM)
return;
break;
case RECIPIENT_TYPES.EVERYONE:
break;
default:
console.error(`Unkown recipient '${recipient}' when trying to execute '${handlerName}' for '${this.socketName}'. This should never happen. If you see this message, please open an issue in the bug tracker of the socketlib repository.`);
return;
}
}
let name, func;
try {
[name, func] = this._resolveFunction(handlerName);
}
catch (e) {
if (e instanceof errors.SocketlibUnregisteredHandlerError && type === MESSAGE_TYPES.REQUEST) {
this._sendError(id, MESSAGE_TYPES.UNREGISTERED);
}
throw e;
}
const socketdata = {userId: senderId};
const _this = {socketdata};
if (type === MESSAGE_TYPES.COMMAND) {
func.call(_this, ...args);
}
else {
let result;
try {
result = await func.call(_this, ...args);
}
catch (e) {
console.error(`An exception occured while executing handler '${name}'.`);
this._sendError(id, MESSAGE_TYPES.EXCEPTION);
throw e;
}
this._sendResult(id, result);
}
}
_handleResponse(message, senderId) {
const {id, result, type} = message;
const request = this.pendingRequests.get(id);
if (!request)
return;
if (!this._isResponseSenderValid(senderId, request.recipient)) {
console.warn("socketlib | Dropped a response that was received from the wrong user. This means that either someone is inserting messages into the socket or this is a socketlib issue. If the latter is the case please file a bug report in the socketlib repository.")
console.info(senderId, request.recipient);
return;
}
switch (type) {
case MESSAGE_TYPES.RESULT:
request.resolve(result);
break;
case MESSAGE_TYPES.EXCEPTION:
request.reject(new errors.SocketlibRemoteException(`An exception occured during remote execution of handler '${request.handlerName}'. Please see ${game.users.get(message.userId).name}'s error console for details.`));
break;
case MESSAGE_TYPES.UNREGISTERED:
request.reject(new errors.SocketlibUnregisteredHandlerError(`Executing the handler '${request.handlerName}' has been refused by ${game.users.get(message.userId).name}'s client, because this handler hasn't been registered on that client.`));
break;
default:
request.reject(new errors.SocketlibInternalError(`Unknown result type '${type}' for handler '${request.handlerName}'. This should never happen. If you see this message, please open an issue in the bug tracker of the socketlib repository.`));
break;
}
this.pendingRequests.delete(id);
}
_isResponseSenderValid(senderId, recipients) {
if (recipients === RECIPIENT_TYPES.ONE_GM && game.users.get(senderId).isGM)
return true;
if (recipients instanceof Array && recipients.includes(senderId))
return true;
return false;
}
}
function isResponsibleGM() {
if (!game.user.isGM)
return false;
const connectedGMs = game.users.filter(isActiveGM);
return !connectedGMs.some(other => other.id < game.user.id);
}
function isActiveGM(user) {
return user.active && user.isGM;
}
function handleUserActivity(wrapper, userId, activityData={}) {
const user = game.users.get(userId);
const wasActive = user.active;
const result = wrapper(userId, activityData);
// If user disconnected
if (!user.active && wasActive) {
const modules = Array.from(socketlib.modules.values());
if (socketlib.system)
modules.concat(socketlib.system);
const GMConnected = Boolean(game.users.find(isActiveGM));
// Reject all promises that are still waiting for a response from this player
for (const socket of modules) {
const failedRequests = Array.from(socket.pendingRequests.entries()).filter(([id, request]) => {
const recipient = request.recipient;
const handlerName = request.handlerName;
if (recipient === RECIPIENT_TYPES.ONE_GM) {
if (!GMConnected) {
request.reject(new errors.SocketlibNoGMConnectedError(`Could not execute handler '${handlerName}' as GM, because all GMs disconnected while the execution was being dispatched.`));
return true;
}
}
else if (recipient instanceof Array) {
if (recipient.includes(userId)) {
request.reject(new errors.SocketlibInvalidUserError(`User '${game.users.get(userId).name}' (${userId}) disconnected while handler '${handlerName}' was being dispatched.`));
return true;
}
}
return false;
});
for (const [id, request] of failedRequests) {
socket.pendingRequests.delete(id);
}
}
}
return result;
}