/* * This file is part of the warpgate module (https://github.com/trioderegion/warpgate) * Copyright (c) 2021 Matthew Haentschke. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import {logger} from './logger.js' import {MODULE} from './module.js' import {Comms} from './comms.js' import {Mutator} from './mutator.js' const NAME = "RemoteMutator"; export class RemoteMutator { static register() { RemoteMutator.settings(); } static settings() { const config = true; const settingsData = { alwaysAccept: { scope: 'world', config, default: false, type: Boolean }, suppressToast: { scope: 'world', config, default: false, type: Boolean }, alwaysAcceptLocal: { scope: 'client', config, default: 0, type: Number, choices: { 0: MODULE.localize('setting.option.useWorld'), 1: MODULE.localize('setting.option.overrideTrue'), 2: MODULE.localize('setting.option.overrideFalse'), } }, suppressToastLocal: { scope: 'client', config, default: 0, type: Number, choices: { 0: MODULE.localize('setting.option.useWorld'), 1: MODULE.localize('setting.option.overrideTrue'), 2: MODULE.localize('setting.option.overrideFalse'), } }, }; MODULE.applySettings(settingsData); } //responseData: //------ //sceneId //userId //------- //accepted (bool) //tokenId //actorId //mutationId //updates (if mutate) /* create the needed trigger functions if there is a post callback to handle */ static _createMutateTriggers( tokenDoc, {post = undefined}, options ) { const condition = (responseData) => { return responseData.tokenId === tokenDoc.id && responseData.mutationId === options.name; } /* craft the response handler * execute the post callback */ const promise = new Promise( (resolve) => { const handleResponse = async (responseData) => { /* if accepted, run our post callback */ const tokenDoc = game.scenes.get(responseData.sceneId).getEmbeddedDocument('Token', responseData.tokenId); if (responseData.accepted) { const info = MODULE.format('display.mutationAccepted', {mName: options.name, tName: tokenDoc.name}); const {suppressToast} = MODULE.getFeedbackSettings(options.overrides); if(!suppressToast) ui.notifications.info(info); } else { const warn = MODULE.format('display.mutationRejected', {mName: options.name, tName: tokenDoc.name}); if(!options.overrides?.suppressReject) ui.notifications.warn(warn); } /* only need to do this if we have a post callback */ if (post) await post(tokenDoc, responseData.updates, responseData.accepted); resolve(responseData); return; } warpgate.event.trigger(warpgate.EVENT.MUTATE_RESPONSE, handleResponse, condition); }); return promise; } static _createRevertTriggers( tokenDoc, mutationName = undefined, {callbacks={}, options = {}} ) { const condition = (responseData) => { return responseData.tokenId === tokenDoc.id && (responseData.mutationId === mutationName || !mutationName); } /* if no name provided, we are popping the last one */ const mName = mutationName ? mutationName : warpgate.mutationStack(tokenDoc).last.name; /* craft the response handler * execute the post callback */ const promise = new Promise(async (resolve) => { const handleResponse = async (responseData) => { const tokenDoc = game.scenes.get(responseData.sceneId).getEmbeddedDocument('Token', responseData.tokenId); /* if accepted, run our post callback */ if (responseData.accepted) { const info = MODULE.format('display.revertAccepted', {mName , tName: tokenDoc.name}); const {suppressToast} = MODULE.getFeedbackSettings(options.overrides); if(!suppressToast) ui.notifications.info(info); } else { const warn = MODULE.format('display.revertRejected', {mName , tName: tokenDoc.name}); if(!options.overrides?.suppressReject) ui.notifications.warn(warn); } await callbacks.post?.(tokenDoc, responseData.updates, responseData.accepted); resolve(responseData); return; } warpgate.event.trigger(warpgate.EVENT.REVERT_RESPONSE, handleResponse, condition); }); return promise; } static remoteMutate( tokenDoc, {updates, callbacks = {}, options = {}} ) { /* we need to make sure there is a user that can handle our resquest */ if (!MODULE.firstOwner(tokenDoc)) { logger.error(MODULE.localize('error.noOwningUserMutate')); return false; } /* register our trigger for monitoring remote response. * This handles the post callback */ const promise = RemoteMutator._createMutateTriggers( tokenDoc, callbacks, options ); /* broadcast the request to mutate the token */ Comms.requestMutate(tokenDoc.id, tokenDoc.parent.id, { updates, options }); return promise; } /** * * @returns {Promise>} */ static async remoteBatchMutate( tokenDocs, {updates, callbacks = {}, options = {}} ) { /* follow normal protocol for initial requests. * if accepted, force accept and force suppress remaining token mutations * if rejected, bail on all further mutations for this owner */ const firstToken = tokenDocs.shift(); let results = [await warpgate.mutate(firstToken, updates, callbacks, options)]; if (results[0].accepted) { const silentOptions = foundry.utils.mergeObject(options, { overrides: {alwaysAccept: true, suppressToast: true} }, {inplace: false}); results = results.concat(tokenDocs.map( tokenDoc => { return warpgate.mutate(tokenDoc, updates, callbacks, silentOptions); })); } else { results = results.concat(tokenDocs.map( tokenDoc => ({sceneId: tokenDoc.parent.id, tokenId: tokenDoc.id, accepted: false}))); } return results; } static remoteRevert( tokenDoc, {mutationId = null, callbacks={}, options = {}} = {} ) { /* we need to make sure there is a user that can handle our resquest */ if (!MODULE.firstOwner(tokenDoc)) { logger.error(MODULE.format('error.noOwningUserRevert')); return false; } /* register our trigger for monitoring remote response. * This handles the post callback */ const result = RemoteMutator._createRevertTriggers( tokenDoc, mutationId, {callbacks, options} ); /* broadcast the request to mutate the token */ Comms.requestRevert(tokenDoc.id, tokenDoc.parent.id, {mutationId, options}); return result; } /** * * @returns {Promise>} */ static async remoteBatchRevert( tokenDocs, {mutationName = null, options = {}} = {} ) { /* follow normal protocol for initial requests. * if accepted, force accept and force suppress remaining token mutations * if rejected, bail on all further mutations for this owner */ let firstToken = tokenDocs.shift(); while( !!firstToken && warpgate.mutationStack(firstToken).stack.length == 0 ) firstToken = tokenDocs.shift(); if(!firstToken) return []; const results = [await warpgate.revert(firstToken, mutationName, options)]; if(results[0].accepted) { const silentOptions = foundry.utils.mergeObject(options, { overrides: {alwaysAccept: true, suppressToast: true} }, {inplace: false} ); results.push(...(tokenDocs.map( tokenDoc => { return warpgate.revert(tokenDoc, mutationName, silentOptions); }))) } else { results.push(...tokenDocs.map( tokenDoc => ({sceneId: tokenDoc.parent.id, tokenId: tokenDoc.id, accepted: false}))); } return results; } static async handleMutationRequest(payload) { /* First, are we the first player owner? If not, stop, they will handle it */ const tokenDoc = game.scenes.get(payload.sceneId).getEmbeddedDocument('Token', payload.tokenId); if (MODULE.isFirstOwner(tokenDoc.actor)) { let {alwaysAccept: accepted, suppressToast} = MODULE.getFeedbackSettings(payload.options.overrides); if(!accepted) { accepted = await RemoteMutator._queryRequest(tokenDoc, payload.userId, payload.options.description, payload.updates) /* if a dialog is shown, the user knows the outcome */ suppressToast = true; } let responseData = { sceneId: payload.sceneId, userId: game.user.id, accepted, tokenId: payload.tokenId, mutationId: payload.options.name, options: payload.options, } await warpgate.event.notify(warpgate.EVENT.MUTATE_RESPONSE, responseData); if (accepted) { /* first owner accepts mutation -- apply it */ /* requests will never have callbacks */ await Mutator.mutate(tokenDoc, payload.updates, {}, payload.options); const message = MODULE.format('display.mutationRequestTitle', {userName: game.users.get(payload.userId).name, tokenName: tokenDoc.name}); if(!suppressToast) ui.notifications.info(message); } } } static async handleRevertRequest(payload) { /* First, are we the first player owner? If not, stop, they will handle it */ const tokenDoc = game.scenes.get(payload.sceneId).getEmbeddedDocument('Token', payload.tokenId); if (MODULE.isFirstOwner(tokenDoc.actor)) { const stack = warpgate.mutationStack(tokenDoc); if( (stack.stack ?? []).length == 0 ) return; const details = payload.mutationId ? stack.getName(payload.mutationId) : stack.last; const description = MODULE.format('display.revertRequestDescription', {mName: details.name, tName: tokenDoc.name}); let {alwaysAccept: accepted, suppressToast} = MODULE.getFeedbackSettings(payload.options.overrides); if(!accepted) { accepted = await RemoteMutator._queryRequest(tokenDoc, payload.userId, description, details ); suppressToast = true; } let responseData = { sceneId: payload.sceneId, userId: game.user.id, accepted, tokenId: payload.tokenId, mutationId: payload.mutationId } await warpgate.event.notify(warpgate.EVENT.REVERT_RESPONSE, responseData); /* if the request is accepted, do the revert */ if (accepted) { await Mutator.revertMutation(tokenDoc, payload.mutationId, payload.options); if (!suppressToast) { ui.notifications.info(description); } } } } static async _queryRequest(tokenDoc, requestingUserId, description = 'warpgate.display.emptyDescription', detailsObject) { /* if this is update data, dont include the mutate data please, its huge */ let displayObject = duplicate(detailsObject); if (displayObject.actor?.flags?.warpgate) { displayObject.actor.flags.warpgate = {}; } displayObject = MODULE.removeEmptyObjects(displayObject); const details = RemoteMutator._convertObjToHTML(displayObject) const modeSwitch = { description: {label: MODULE.localize('display.inspectLabel'), value: 'inspect', content: `

${game.i18n.localize(description)}

`}, inspect: {label: MODULE.localize('display.descriptionLabel'), value: 'description', content: details } } const title = MODULE.format('display.mutationRequestTitle', {userName: game.users.get(requestingUserId).name, tokenName: tokenDoc.name}); let userResponse = false; let modeButton = modeSwitch.description; do { userResponse = await warpgate.buttonDialog({buttons: [{label: MODULE.localize('display.findTargetLabel'), value: 'select'}, {label: MODULE.localize('display.acceptLabel'), value: true}, {label: MODULE.localize('display.rejectLabel'), value: false}, modeButton], content: modeButton.content, title, options: {top: 100}}); if (userResponse === 'select') { if (tokenDoc.object) { tokenDoc.object.control({releaseOthers: true}); await canvas.animatePan({x: tokenDoc.object.x, y: tokenDoc.object.y}); } } else if (userResponse !== false && userResponse !== true) { /* swap modes and re-render */ modeButton = modeSwitch[userResponse]; } } while (userResponse !== false && userResponse !== true) return userResponse; } static _convertObjToHTML(obj) { const stringified = JSON.stringify(obj, undefined, '$SPACING'); return stringified.replaceAll('\n', '
').replaceAll('$SPACING', '    '); } }