|
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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<Array<Object>>}
|
|
*/
|
|
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<Array<Object>>}
|
|
*/
|
|
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: `<p>${game.i18n.localize(description)}</p>`},
|
|
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', '<br>').replaceAll('$SPACING', ' ');
|
|
}
|
|
|
|
}
|
|
|