All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

369 lines
13 KiB

1 year ago
  1. /*
  2. * This file is part of the warpgate module (https://github.com/trioderegion/warpgate)
  3. * Copyright (c) 2021 Matthew Haentschke.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, version 3.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. import {logger} from './logger.js'
  18. import {MODULE} from './module.js'
  19. import {Comms} from './comms.js'
  20. import {Mutator} from './mutator.js'
  21. const NAME = "RemoteMutator";
  22. export class RemoteMutator {
  23. static register() {
  24. RemoteMutator.settings();
  25. }
  26. static settings() {
  27. const config = true;
  28. const settingsData = {
  29. alwaysAccept: {
  30. scope: 'world', config, default: false, type: Boolean
  31. },
  32. suppressToast: {
  33. scope: 'world', config, default: false, type: Boolean
  34. },
  35. alwaysAcceptLocal: {
  36. scope: 'client', config, default: 0, type: Number,
  37. choices: {
  38. 0: MODULE.localize('setting.option.useWorld'),
  39. 1: MODULE.localize('setting.option.overrideTrue'),
  40. 2: MODULE.localize('setting.option.overrideFalse'),
  41. }
  42. },
  43. suppressToastLocal: {
  44. scope: 'client', config, default: 0, type: Number,
  45. choices: {
  46. 0: MODULE.localize('setting.option.useWorld'),
  47. 1: MODULE.localize('setting.option.overrideTrue'),
  48. 2: MODULE.localize('setting.option.overrideFalse'),
  49. }
  50. },
  51. };
  52. MODULE.applySettings(settingsData);
  53. }
  54. //responseData:
  55. //------
  56. //sceneId
  57. //userId
  58. //-------
  59. //accepted (bool)
  60. //tokenId
  61. //actorId
  62. //mutationId
  63. //updates (if mutate)
  64. /* create the needed trigger functions if there is a post callback to handle */
  65. static _createMutateTriggers( tokenDoc, {post = undefined}, options ) {
  66. const condition = (responseData) => {
  67. return responseData.tokenId === tokenDoc.id && responseData.mutationId === options.name;
  68. }
  69. /* craft the response handler
  70. * execute the post callback */
  71. const promise = new Promise( (resolve) => {
  72. const handleResponse = async (responseData) => {
  73. /* if accepted, run our post callback */
  74. const tokenDoc = game.scenes.get(responseData.sceneId).getEmbeddedDocument('Token', responseData.tokenId);
  75. if (responseData.accepted) {
  76. const info = MODULE.format('display.mutationAccepted', {mName: options.name, tName: tokenDoc.name});
  77. const {suppressToast} = MODULE.getFeedbackSettings(options.overrides);
  78. if(!suppressToast) ui.notifications.info(info);
  79. } else {
  80. const warn = MODULE.format('display.mutationRejected', {mName: options.name, tName: tokenDoc.name});
  81. if(!options.overrides?.suppressReject) ui.notifications.warn(warn);
  82. }
  83. /* only need to do this if we have a post callback */
  84. if (post) await post(tokenDoc, responseData.updates, responseData.accepted);
  85. resolve(responseData);
  86. return;
  87. }
  88. warpgate.event.trigger(warpgate.EVENT.MUTATE_RESPONSE, handleResponse, condition);
  89. });
  90. return promise;
  91. }
  92. static _createRevertTriggers( tokenDoc, mutationName = undefined, {callbacks={}, options = {}} ) {
  93. const condition = (responseData) => {
  94. return responseData.tokenId === tokenDoc.id && (responseData.mutationId === mutationName || !mutationName);
  95. }
  96. /* if no name provided, we are popping the last one */
  97. const mName = mutationName ? mutationName : warpgate.mutationStack(tokenDoc).last.name;
  98. /* craft the response handler
  99. * execute the post callback */
  100. const promise = new Promise(async (resolve) => {
  101. const handleResponse = async (responseData) => {
  102. const tokenDoc = game.scenes.get(responseData.sceneId).getEmbeddedDocument('Token', responseData.tokenId);
  103. /* if accepted, run our post callback */
  104. if (responseData.accepted) {
  105. const info = MODULE.format('display.revertAccepted', {mName , tName: tokenDoc.name});
  106. const {suppressToast} = MODULE.getFeedbackSettings(options.overrides);
  107. if(!suppressToast) ui.notifications.info(info);
  108. } else {
  109. const warn = MODULE.format('display.revertRejected', {mName , tName: tokenDoc.name});
  110. if(!options.overrides?.suppressReject) ui.notifications.warn(warn);
  111. }
  112. await callbacks.post?.(tokenDoc, responseData.updates, responseData.accepted);
  113. resolve(responseData);
  114. return;
  115. }
  116. warpgate.event.trigger(warpgate.EVENT.REVERT_RESPONSE, handleResponse, condition);
  117. });
  118. return promise;
  119. }
  120. static remoteMutate( tokenDoc, {updates, callbacks = {}, options = {}} ) {
  121. /* we need to make sure there is a user that can handle our resquest */
  122. if (!MODULE.firstOwner(tokenDoc)) {
  123. logger.error(MODULE.localize('error.noOwningUserMutate'));
  124. return false;
  125. }
  126. /* register our trigger for monitoring remote response.
  127. * This handles the post callback
  128. */
  129. const promise = RemoteMutator._createMutateTriggers( tokenDoc, callbacks, options );
  130. /* broadcast the request to mutate the token */
  131. Comms.requestMutate(tokenDoc.id, tokenDoc.parent.id, { updates, options });
  132. return promise;
  133. }
  134. /**
  135. *
  136. * @returns {Promise<Array<Object>>}
  137. */
  138. static async remoteBatchMutate( tokenDocs, {updates, callbacks = {}, options = {}} ) {
  139. /* follow normal protocol for initial requests.
  140. * if accepted, force accept and force suppress remaining token mutations
  141. * if rejected, bail on all further mutations for this owner */
  142. const firstToken = tokenDocs.shift();
  143. let results = [await warpgate.mutate(firstToken, updates, callbacks, options)];
  144. if (results[0].accepted) {
  145. const silentOptions = foundry.utils.mergeObject(options, { overrides: {alwaysAccept: true, suppressToast: true} }, {inplace: false});
  146. results = results.concat(tokenDocs.map( tokenDoc => {
  147. return warpgate.mutate(tokenDoc, updates, callbacks, silentOptions);
  148. }));
  149. } else {
  150. results = results.concat(tokenDocs.map( tokenDoc => ({sceneId: tokenDoc.parent.id, tokenId: tokenDoc.id, accepted: false})));
  151. }
  152. return results;
  153. }
  154. static remoteRevert( tokenDoc, {mutationId = null, callbacks={}, options = {}} = {} ) {
  155. /* we need to make sure there is a user that can handle our resquest */
  156. if (!MODULE.firstOwner(tokenDoc)) {
  157. logger.error(MODULE.format('error.noOwningUserRevert'));
  158. return false;
  159. }
  160. /* register our trigger for monitoring remote response.
  161. * This handles the post callback
  162. */
  163. const result = RemoteMutator._createRevertTriggers( tokenDoc, mutationId, {callbacks, options} );
  164. /* broadcast the request to mutate the token */
  165. Comms.requestRevert(tokenDoc.id, tokenDoc.parent.id, {mutationId, options});
  166. return result;
  167. }
  168. /**
  169. *
  170. * @returns {Promise<Array<Object>>}
  171. */
  172. static async remoteBatchRevert( tokenDocs, {mutationName = null, options = {}} = {} ) {
  173. /* follow normal protocol for initial requests.
  174. * if accepted, force accept and force suppress remaining token mutations
  175. * if rejected, bail on all further mutations for this owner */
  176. let firstToken = tokenDocs.shift();
  177. while( !!firstToken && warpgate.mutationStack(firstToken).stack.length == 0 ) firstToken = tokenDocs.shift();
  178. if(!firstToken) return [];
  179. const results = [await warpgate.revert(firstToken, mutationName, options)];
  180. if(results[0].accepted) {
  181. const silentOptions = foundry.utils.mergeObject(options, {
  182. overrides: {alwaysAccept: true, suppressToast: true}
  183. }, {inplace: false}
  184. );
  185. results.push(...(tokenDocs.map( tokenDoc => {
  186. return warpgate.revert(tokenDoc, mutationName, silentOptions);
  187. })))
  188. } else {
  189. results.push(...tokenDocs.map( tokenDoc => ({sceneId: tokenDoc.parent.id, tokenId: tokenDoc.id, accepted: false})));
  190. }
  191. return results;
  192. }
  193. static async handleMutationRequest(payload) {
  194. /* First, are we the first player owner? If not, stop, they will handle it */
  195. const tokenDoc = game.scenes.get(payload.sceneId).getEmbeddedDocument('Token', payload.tokenId);
  196. if (MODULE.isFirstOwner(tokenDoc.actor)) {
  197. let {alwaysAccept: accepted, suppressToast} = MODULE.getFeedbackSettings(payload.options.overrides);
  198. if(!accepted) {
  199. accepted = await RemoteMutator._queryRequest(tokenDoc, payload.userId, payload.options.description, payload.updates)
  200. /* if a dialog is shown, the user knows the outcome */
  201. suppressToast = true;
  202. }
  203. let responseData = {
  204. sceneId: payload.sceneId,
  205. userId: game.user.id,
  206. accepted,
  207. tokenId: payload.tokenId,
  208. mutationId: payload.options.name,
  209. options: payload.options,
  210. }
  211. await warpgate.event.notify(warpgate.EVENT.MUTATE_RESPONSE, responseData);
  212. if (accepted) {
  213. /* first owner accepts mutation -- apply it */
  214. /* requests will never have callbacks */
  215. await Mutator.mutate(tokenDoc, payload.updates, {}, payload.options);
  216. const message = MODULE.format('display.mutationRequestTitle', {userName: game.users.get(payload.userId).name, tokenName: tokenDoc.name});
  217. if(!suppressToast) ui.notifications.info(message);
  218. }
  219. }
  220. }
  221. static async handleRevertRequest(payload) {
  222. /* First, are we the first player owner? If not, stop, they will handle it */
  223. const tokenDoc = game.scenes.get(payload.sceneId).getEmbeddedDocument('Token', payload.tokenId);
  224. if (MODULE.isFirstOwner(tokenDoc.actor)) {
  225. const stack = warpgate.mutationStack(tokenDoc);
  226. if( (stack.stack ?? []).length == 0 ) return;
  227. const details = payload.mutationId ? stack.getName(payload.mutationId) : stack.last;
  228. const description = MODULE.format('display.revertRequestDescription', {mName: details.name, tName: tokenDoc.name});
  229. let {alwaysAccept: accepted, suppressToast} = MODULE.getFeedbackSettings(payload.options.overrides);
  230. if(!accepted) {
  231. accepted = await RemoteMutator._queryRequest(tokenDoc, payload.userId, description, details );
  232. suppressToast = true;
  233. }
  234. let responseData = {
  235. sceneId: payload.sceneId,
  236. userId: game.user.id,
  237. accepted,
  238. tokenId: payload.tokenId,
  239. mutationId: payload.mutationId
  240. }
  241. await warpgate.event.notify(warpgate.EVENT.REVERT_RESPONSE, responseData);
  242. /* if the request is accepted, do the revert */
  243. if (accepted) {
  244. await Mutator.revertMutation(tokenDoc, payload.mutationId, payload.options);
  245. if (!suppressToast) {
  246. ui.notifications.info(description);
  247. }
  248. }
  249. }
  250. }
  251. static async _queryRequest(tokenDoc, requestingUserId, description = 'warpgate.display.emptyDescription', detailsObject) {
  252. /* if this is update data, dont include the mutate data please, its huge */
  253. let displayObject = duplicate(detailsObject);
  254. if (displayObject.actor?.flags?.warpgate) {
  255. displayObject.actor.flags.warpgate = {};
  256. }
  257. displayObject = MODULE.removeEmptyObjects(displayObject);
  258. const details = RemoteMutator._convertObjToHTML(displayObject)
  259. const modeSwitch = {
  260. description: {label: MODULE.localize('display.inspectLabel'), value: 'inspect', content: `<p>${game.i18n.localize(description)}</p>`},
  261. inspect: {label: MODULE.localize('display.descriptionLabel'), value: 'description', content: details }
  262. }
  263. const title = MODULE.format('display.mutationRequestTitle', {userName: game.users.get(requestingUserId).name, tokenName: tokenDoc.name});
  264. let userResponse = false;
  265. let modeButton = modeSwitch.description;
  266. do {
  267. 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}});
  268. if (userResponse === 'select') {
  269. if (tokenDoc.object) {
  270. tokenDoc.object.control({releaseOthers: true});
  271. await canvas.animatePan({x: tokenDoc.object.x, y: tokenDoc.object.y});
  272. }
  273. } else if (userResponse !== false && userResponse !== true) {
  274. /* swap modes and re-render */
  275. modeButton = modeSwitch[userResponse];
  276. }
  277. } while (userResponse !== false && userResponse !== true)
  278. return userResponse;
  279. }
  280. static _convertObjToHTML(obj) {
  281. const stringified = JSON.stringify(obj, undefined, '$SPACING');
  282. return stringified.replaceAll('\n', '<br>').replaceAll('$SPACING', '&nbsp;&nbsp;&nbsp;&nbsp;');
  283. }
  284. }