From 35c85794ec1469502fe50d265e13789baa89e8f3 Mon Sep 17 00:00:00 2001 From: Zhell Date: Sun, 8 Dec 2024 13:20:45 +0100 Subject: [PATCH 1/4] Create a socket framework proof of concept --- dnd5e.mjs | 4 ++ module/sockets.mjs | 67 ++++++++++++++++++++++++++++++++ module/sockets/_module.mjs | 5 +++ module/sockets/grant-effect.mjs | 68 +++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 module/sockets.mjs create mode 100644 module/sockets/_module.mjs create mode 100644 module/sockets/grant-effect.mjs diff --git a/dnd5e.mjs b/dnd5e.mjs index b19508c203..2dac16fd80 100644 --- a/dnd5e.mjs +++ b/dnd5e.mjs @@ -25,6 +25,7 @@ import {default as registry} from "./module/registry.mjs"; import * as utils from "./module/utils.mjs"; import {ModuleArt} from "./module/module-art.mjs"; import registerModuleData from "./module/module-registration.mjs"; +import Sockets5e from "./module/sockets.mjs"; import Tooltips5e from "./module/tooltips.mjs"; /* -------------------------------------------- */ @@ -89,6 +90,9 @@ Hooks.once("init", function() { // Configure bastions game.dnd5e.bastion = new documents.Bastion(); + // Configure sockets + game.dnd5e.sockets = new Sockets5e(); + // Configure tooltips game.dnd5e.tooltips = new Tooltips5e(); diff --git a/module/sockets.mjs b/module/sockets.mjs new file mode 100644 index 0000000000..a47b2b83e7 --- /dev/null +++ b/module/sockets.mjs @@ -0,0 +1,67 @@ +import sockets from "./sockets/_module.mjs"; + +/** + * @typedef {object} SocketEventConfig + * @property {string} event The unique socket event name. + * @property {Function} initiate A function used to initiate the socket event. + * @property {Function} finalize A function that finalizes the socket event as the valid user(s). + */ + +export default class Sockets5e { + constructor() { + game.socket.on("system.dnd5e", this.#handleSocket.bind(this)); + for (const config of sockets) this.#register(config); + } + + /* -------------------------------------------- */ + + /** + * Store event configurations. + * @type {Map} + */ + #events = new Map(); + + /* -------------------------------------------- */ + + /** + * Register a sovket event config. + * @param {SocketEventConfig} config + */ + #register(config) { + this.#events.set(config.event, config); + } + + /* -------------------------------------------- */ + + /** + * Handle the socket event for the correct user(s). + * @param {object} data Socket data. + * @param {boolean} [data._emit] Internally used parameter for whether this was initiated or emitted. + * @returns {*} + */ + #handleSocket({ _emit, ...data }) { + if ( !data.userIds.includes(game.user.id) ) { + if (!_emit) game.socket.emit("system.dnd5e", { ...data, _emit: true }); + else return; + } + + const config = this.#events.get(data.event); + if ( config ) return config.finalize(data); + + throw new Error(`'${data.event}' is not a valid socket event action!`); + } + + /* -------------------------------------------- */ + + /** + * Initiate a given event, which may or may not be emitted. + * @param {string} event The event name. + * @param {any[]} args Function parameters for the handler. + */ + initiate(event, ...args) { + const config = this.#events.get(event); + if ( !config ) return; + const data = config.initiate(...args); + this.#handleSocket(data); + } +} diff --git a/module/sockets/_module.mjs b/module/sockets/_module.mjs new file mode 100644 index 0000000000..7cd58083db --- /dev/null +++ b/module/sockets/_module.mjs @@ -0,0 +1,5 @@ +import GrantEffect from "./grant-effect.mjs"; + +export default [ + GrantEffect +]; diff --git a/module/sockets/grant-effect.mjs b/module/sockets/grant-effect.mjs new file mode 100644 index 0000000000..a131d6f004 --- /dev/null +++ b/module/sockets/grant-effect.mjs @@ -0,0 +1,68 @@ +/** + * @typedef {object} GrantEffectConfig + * @property {string} event The event name. + * @property {string} actor The uuid of the actor to receive the effect. + * @property {object} effectData Data used to create the effect. + * @property {string[]} userIds The ids of the users that should perform the operation. + */ + +/** + * Name of this socket event. + * @type {string} + */ +const eventName = "grantEffect"; + +/* -------------------------------------------- */ + +/** + * Grant the effect to a given actor. + * @param {GrantEffectConfig} data + */ +async function grantEffect(data) { + const actor = await fromUuid(data.actor); + if ( actor?.documentName !== "Actor" ) return; + getDocumentClass("ActiveEffect").create(data.effectData, { parent: actor }); +} + +/* -------------------------------------------- */ + +/** + * Initiate the socket event. + * @param {Actor5e} actor The actor to receive the effect. + * @param {ActiveEffecet5e} effect The effect to be duplicated onto the actor. + * @returns {GrantEffectConfig|void} The event data. + */ +function initiate(actor, effect) { + if ( (actor?.documentName !== "Actor") || (effect?.documentName !== "ActiveEffect") ) { + throw new Error("You must supply both an actor and effect instance for this operation."); + } + + // Get a valid user to perform the operation. + // TODO: improve in v13 + const userId = actor.isOwner ? game.user.id : game.users.find(user => { + return user.active && actor.testUserPermission(user, "OWNER"); + })?.id; + + if (!userId) { + // TODO: add to i18n + ui.notifications.error("DND5E.SOCKETS.GrantEffect.Warning.MissingUser", { localize: true }); + return; + } + + const data = { + event: eventName, + userIds: [userId], + actor: actor.uuid, + effectData: effect.toObject() + }; + + return data; +} + +/* -------------------------------------------- */ + +export default { + event: eventName, + initiate: initiate, + finalize: grantEffect +}; From 26dbcdcf7704b84910639a7670efed1f9112d61c Mon Sep 17 00:00:00 2001 From: Zhell Date: Mon, 9 Dec 2024 23:58:18 +0100 Subject: [PATCH 2/4] Add adjustments from review. --- dnd5e.mjs | 2 +- module/sockets/grant-effect.mjs | 103 ++++++++++++++----------------- module/sockets/socket-event.mjs | 33 ++++++++++ module/{ => sockets}/sockets.mjs | 21 +++---- 4 files changed, 87 insertions(+), 72 deletions(-) create mode 100644 module/sockets/socket-event.mjs rename module/{ => sockets}/sockets.mjs (70%) diff --git a/dnd5e.mjs b/dnd5e.mjs index 2dac16fd80..bae910e0b7 100644 --- a/dnd5e.mjs +++ b/dnd5e.mjs @@ -25,7 +25,7 @@ import {default as registry} from "./module/registry.mjs"; import * as utils from "./module/utils.mjs"; import {ModuleArt} from "./module/module-art.mjs"; import registerModuleData from "./module/module-registration.mjs"; -import Sockets5e from "./module/sockets.mjs"; +import Sockets5e from "./module/sockets/sockets.mjs"; import Tooltips5e from "./module/tooltips.mjs"; /* -------------------------------------------- */ diff --git a/module/sockets/grant-effect.mjs b/module/sockets/grant-effect.mjs index a131d6f004..eeb6403d19 100644 --- a/module/sockets/grant-effect.mjs +++ b/module/sockets/grant-effect.mjs @@ -1,68 +1,57 @@ +import SocketEvent from "./socket-event.mjs"; + /** - * @typedef {object} GrantEffectConfig - * @property {string} event The event name. + * @typedef {import("./socket-event.mjs").EventData} GrantEffectData * @property {string} actor The uuid of the actor to receive the effect. * @property {object} effectData Data used to create the effect. - * @property {string[]} userIds The ids of the users that should perform the operation. - */ - -/** - * Name of this socket event. - * @type {string} */ -const eventName = "grantEffect"; -/* -------------------------------------------- */ - -/** - * Grant the effect to a given actor. - * @param {GrantEffectConfig} data - */ -async function grantEffect(data) { - const actor = await fromUuid(data.actor); - if ( actor?.documentName !== "Actor" ) return; - getDocumentClass("ActiveEffect").create(data.effectData, { parent: actor }); -} +export default class GrantEffectEvent extends SocketEvent { + /** @inheritDoc */ + static eventName = "grantEffect"; -/* -------------------------------------------- */ + /* -------------------------------------------- */ -/** - * Initiate the socket event. - * @param {Actor5e} actor The actor to receive the effect. - * @param {ActiveEffecet5e} effect The effect to be duplicated onto the actor. - * @returns {GrantEffectConfig|void} The event data. - */ -function initiate(actor, effect) { - if ( (actor?.documentName !== "Actor") || (effect?.documentName !== "ActiveEffect") ) { - throw new Error("You must supply both an actor and effect instance for this operation."); + /** + * As a user allowed to do so, perform the operation. + * @param {GrantEffectData} data The event data. + */ + static async finalize(data) { + const actor = await fromUuid(data.actor); + if ( actor?.documentName !== "Actor" ) return; + getDocumentClass("ActiveEffect").create(data.effectData, { parent: actor }); } - // Get a valid user to perform the operation. - // TODO: improve in v13 - const userId = actor.isOwner ? game.user.id : game.users.find(user => { - return user.active && actor.testUserPermission(user, "OWNER"); - })?.id; - - if (!userId) { - // TODO: add to i18n - ui.notifications.error("DND5E.SOCKETS.GrantEffect.Warning.MissingUser", { localize: true }); - return; + /* -------------------------------------------- */ + + /** + * Initiate the socket event. + * @param {Actor5e} actor The actor to receive the effect. + * @param {ActiveEffecet5e} effect The effect to be duplicated onto the actor. + * @returns {GrantEffectData|void} The event data. + */ + static initiate(actor, effect) { + if ( (actor?.documentName !== "Actor") || (effect?.documentName !== "ActiveEffect") ) { + throw new Error("You must supply both an actor and effect instance for this operation."); + } + + // Get a valid user to perform the operation. + // TODO: improve in v13 + const userId = actor.isOwner ? game.user.id : game.users.find(user => { + return user.active && actor.testUserPermission(user, "OWNER"); + })?.id; + + if ( !userId ) { + // TODO: add to i18n + ui.notifications.error("DND5E.SOCKETS.GrantEffect.Warning.MissingUser", { localize: true }); + return; + } + + return { + event: GrantEffectEvent.eventName, + userIds: [userId], + actor: actor.uuid, + effectData: effect.toObject() + }; } - - const data = { - event: eventName, - userIds: [userId], - actor: actor.uuid, - effectData: effect.toObject() - }; - - return data; } - -/* -------------------------------------------- */ - -export default { - event: eventName, - initiate: initiate, - finalize: grantEffect -}; diff --git a/module/sockets/socket-event.mjs b/module/sockets/socket-event.mjs new file mode 100644 index 0000000000..fa95a5c231 --- /dev/null +++ b/module/sockets/socket-event.mjs @@ -0,0 +1,33 @@ +/** + * @typedef {object} EventData + * @property {string} event The unique event data name. + * @property {string[]} userIds The ids of users that should perform the operation. + */ + +export default class SocketEvent { + /** + * The socket event name, which must be unique and is used to call the event. + * @type {string} + */ + static eventName; + + /* -------------------------------------------- */ + + /** + * As a user allowed to do so, perform the operation. + * @param {EventData} data The event data. + */ + static async finalize(data) { + throw new Error("The 'finalize' method of a socket event must be subclassed."); + } + + /* -------------------------------------------- */ + + /** + * Initiate the socket event. Subclasses can and should change the signature of this method. + * @returns {EventData} + */ + static initiate() { + throw new Error("The 'initiate' method of a socket event must be subclassed."); + } +} diff --git a/module/sockets.mjs b/module/sockets/sockets.mjs similarity index 70% rename from module/sockets.mjs rename to module/sockets/sockets.mjs index a47b2b83e7..6dea069359 100644 --- a/module/sockets.mjs +++ b/module/sockets/sockets.mjs @@ -1,34 +1,27 @@ -import sockets from "./sockets/_module.mjs"; - -/** - * @typedef {object} SocketEventConfig - * @property {string} event The unique socket event name. - * @property {Function} initiate A function used to initiate the socket event. - * @property {Function} finalize A function that finalizes the socket event as the valid user(s). - */ +import configs from "./_module.mjs"; export default class Sockets5e { constructor() { game.socket.on("system.dnd5e", this.#handleSocket.bind(this)); - for (const config of sockets) this.#register(config); + for (const config of configs) this.#register(config); } /* -------------------------------------------- */ /** * Store event configurations. - * @type {Map} + * @type {Map} */ #events = new Map(); /* -------------------------------------------- */ /** - * Register a sovket event config. - * @param {SocketEventConfig} config + * Register a socket event config. + * @param {SocketEvent} config */ #register(config) { - this.#events.set(config.event, config); + this.#events.set(config.eventName, config); } /* -------------------------------------------- */ @@ -42,7 +35,7 @@ export default class Sockets5e { #handleSocket({ _emit, ...data }) { if ( !data.userIds.includes(game.user.id) ) { if (!_emit) game.socket.emit("system.dnd5e", { ...data, _emit: true }); - else return; + return; } const config = this.#events.get(data.event); From f8717258895a0da3905d89f680f93a533235838e Mon Sep 17 00:00:00 2001 From: Zhell Date: Tue, 10 Dec 2024 02:44:55 +0100 Subject: [PATCH 3/4] PR review two: eclectic buggaloo --- module/sockets/grant-effect.mjs | 6 +++--- module/sockets/socket-event.mjs | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/module/sockets/grant-effect.mjs b/module/sockets/grant-effect.mjs index eeb6403d19..1437aff7d3 100644 --- a/module/sockets/grant-effect.mjs +++ b/module/sockets/grant-effect.mjs @@ -26,9 +26,9 @@ export default class GrantEffectEvent extends SocketEvent { /** * Initiate the socket event. - * @param {Actor5e} actor The actor to receive the effect. - * @param {ActiveEffecet5e} effect The effect to be duplicated onto the actor. - * @returns {GrantEffectData|void} The event data. + * @param {Actor5e} actor The actor to receive the effect. + * @param {ActiveEffect5e} effect The effect to be duplicated onto the actor. + * @returns {GrantEffectData|void} The event data. */ static initiate(actor, effect) { if ( (actor?.documentName !== "Actor") || (effect?.documentName !== "ActiveEffect") ) { diff --git a/module/sockets/socket-event.mjs b/module/sockets/socket-event.mjs index fa95a5c231..8d96dffb02 100644 --- a/module/sockets/socket-event.mjs +++ b/module/sockets/socket-event.mjs @@ -1,6 +1,6 @@ /** * @typedef {object} EventData - * @property {string} event The unique event data name. + * @property {string} event The unique event data name. * @property {string[]} userIds The ids of users that should perform the operation. */ @@ -16,6 +16,7 @@ export default class SocketEvent { /** * As a user allowed to do so, perform the operation. * @param {EventData} data The event data. + * @abstract */ static async finalize(data) { throw new Error("The 'finalize' method of a socket event must be subclassed."); @@ -26,6 +27,7 @@ export default class SocketEvent { /** * Initiate the socket event. Subclasses can and should change the signature of this method. * @returns {EventData} + * @abstract */ static initiate() { throw new Error("The 'initiate' method of a socket event must be subclassed."); From ff6c47a39183bc34fc6028354ffc89a8b212eebb Mon Sep 17 00:00:00 2001 From: Zhell Date: Sat, 14 Dec 2024 01:27:38 +0100 Subject: [PATCH 4/4] Add "add dependent" socket event --- module/sockets/_module.mjs | 2 ++ module/sockets/add-dependent.mjs | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 module/sockets/add-dependent.mjs diff --git a/module/sockets/_module.mjs b/module/sockets/_module.mjs index 7cd58083db..a4b077b2a9 100644 --- a/module/sockets/_module.mjs +++ b/module/sockets/_module.mjs @@ -1,5 +1,7 @@ +import AddDependent from "./add-dependent.mjs"; import GrantEffect from "./grant-effect.mjs"; export default [ + AddDependent, GrantEffect ]; diff --git a/module/sockets/add-dependent.mjs b/module/sockets/add-dependent.mjs new file mode 100644 index 0000000000..08d3359983 --- /dev/null +++ b/module/sockets/add-dependent.mjs @@ -0,0 +1,59 @@ +import SocketEvent from "./socket-event.mjs"; + +/** + * @typedef {import("./socket-event.mjs").EventData} AddDependentData + * @property {string} effect The uuid of the effect to add dependents to. + * @property {string[]} dependents The uuids of the effects to add as dependents. + */ + +export default class AddDependentData extends SocketEvent { + /** @inheritDoc */ + static eventName = "addDependent"; + + /* -------------------------------------------- */ + + /** + * As a user allowed to do so, perform the operation. + * @param {AddDependentData} data The event data. + */ + static async finalize(data) { + const uuids = [data.effect, ...data.dependents]; + const effects = await Promise.all(uuids.map(uuid => fromUuid(uuid))); + if ( effects.some(effect => effect?.documentName !== "ActiveEffect") ) return; + const effect = effects.shift(); + effect.addDependent(effects); + } + + /* -------------------------------------------- */ + + /** + * Initiate the socket event. + * @param {ActiveEffect5e} effect The effect to add dependents to. + * @param {...ActiveEffect5e} dependents The effects to add as dependents. + * @returns {AddDependentData|void} The event data. + */ + static initiate(effect, ...dependents) { + if ( [effect, ...dependents].some(effect => effect?.documentName !== "ActiveEffect") ) { + throw new Error("You must supply both an effect and dependent effect instances for this operation."); + } + + // Get a valid user to perform the operation. + // TODO: improve in v13 + const userId = effect.isOwner ? game.user.id : game.users.find(user => { + return user.active && effect.testUserPermission(user, "OWNER"); + })?.id; + + if ( !userId ) { + // TODO: add to i18n + ui.notifications.error("DND5E.SOCKETS.AddDependent.Warning.MissingUser", { localize: true }); + return; + } + + return { + event: AddDependentData.eventName, + userIds: [userId], + effect: effect.uuid, + dependents: dependents.map(effect => effect.uuid) + }; + } +}