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.data._id < game.user.data._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; }