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.

814 lines
30 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 {RemoteMutator} from './remote-mutator.js'
  20. const NAME = "Mutator";
  21. /** @typedef {import('./api.js').ComparisonKeys} ComparisonKeys */
  22. /** @typedef {import('./api.js').NoticeConfig} NoticeConfig */
  23. /** @typedef {import('./mutation-stack.js').MutationData} MutationData */
  24. /** @typedef {import('./api.js').Shorthand} Shorthand */
  25. /** @typedef {import('./api.js').SpawningOptions} SpawningOptions */
  26. //TODO proper objects
  27. /** @typedef {Object} MutateInfo **/
  28. /**
  29. * Workflow options
  30. * @typedef {Object} WorkflowOptions
  31. * @property {Shorthand} [updateOpts] Options for the creation/deletion/updating of (embedded) documents related to this mutation
  32. * @property {string} [description] Description of this mutation for potential display to the remote owning user.
  33. * @property {NoticeConfig} [notice] Options for placing a ping or panning to the token after mutation
  34. * @property {boolean} [noMoveWait = false] If true, will not wait for potential token movement animation to complete before proceeding with remaining actor/embedded updates.
  35. * @property {Object} [overrides]
  36. * @property {boolean} [overrides.alwaysAccept = false] Force the receiving clients "auto-accept" state,
  37. * regardless of world/client settings
  38. * @property {boolean} [overrides.suppressToast = false] Force the initiating and receiving clients to suppress
  39. * the "call and response" UI toasts indicating the requests accepted/rejected response.
  40. * @property {boolean} [overrides.includeRawData = false] Force events produced from this operation to include the
  41. * raw data used for its operation (such as the final mutation data to be applied, or the resulting packed actor
  42. * data from a spawn). **Caution, use judiciously** -- enabling this option can result in potentially large
  43. * socket data transfers during warpgate operation.
  44. * @property {boolean} [overrides.preserveData = false] If enabled, the provided updates data object will
  45. * be modified in-place as needed for internal Warp Gate operations and will NOT be re-usable for a
  46. * subsequent operation. Otherwise, the provided data is copied and modified internally, preserving
  47. * the original input for subsequent re-use.
  48. *
  49. */
  50. /**
  51. *
  52. * @typedef {Object} MutationOptions
  53. * @property {boolean} [permanent=false] Indicates if this should be treated as a permanent change
  54. * to the actor, which does not store the update delta information required to revert mutation.
  55. * @property {string} [name=randomId()] User provided name, or identifier, for this particular
  56. * mutation operation. Used for reverting mutations by name, as opposed to popping last applied.
  57. * @property {Object} [delta]
  58. * @property {ComparisonKeys} [comparisonKeys]
  59. */
  60. /**
  61. * The post delta creation, pre mutate callback. Called after the update delta has been generated, but before
  62. * it is stored on the actor. Can be used to modify this delta for storage (ex. Current and Max HP are
  63. * increased by 10, but when reverted, you want to keep the extra Current HP applied. Update the delta object
  64. * with the desired HP to return to after revert, or remove it entirely.
  65. *
  66. * @typedef {(function(Shorthand,TokenDocument):Promise|undefined)} PostDelta
  67. * @param {Shorthand} delta Computed change of the actor based on `updates`. Used to "unroll" this mutation when reverted.
  68. * @param {TokenDocument} tokenDoc Token being modified.
  69. *
  70. * @returns {Promise<any>|any}
  71. */
  72. /**
  73. * The post mutate callback prototype. Called after the actor has been mutated and after the mutate event
  74. * has triggered. Useful for animations or changes that should not be tracked by the mutation system.
  75. *
  76. * @typedef {function(TokenDocument, Object, boolean):Promise|void} PostMutate
  77. * @param {TokenDocument} tokenDoc Token that has been modified.
  78. * @param {Shorthand} updates Current permutation of the original shorthand updates object provided, as
  79. * applied for this mutation
  80. * @param {boolean} accepted Whether or not the mutation was accepted by the first owner.
  81. *
  82. * @returns {Promise<any>|any}
  83. */
  84. export class Mutator {
  85. static register() {
  86. Mutator.defaults();
  87. Mutator.hooks();
  88. }
  89. static defaults(){
  90. MODULE[NAME] = {
  91. comparisonKey: 'name'
  92. }
  93. }
  94. static hooks() {
  95. if(!MODULE.isV10) Hooks.on('preUpdateToken', Mutator._correctActorLink)
  96. }
  97. static _correctActorLink(tokenDoc, update) {
  98. /* if the actorId has been updated AND its being set to null,
  99. * check if we can patch/fix this warpgate spawn
  100. */
  101. if (update.hasOwnProperty('actorId') && update.actorId === null) {
  102. const sourceActorId = tokenDoc.getFlag(MODULE.data.name, 'sourceActorId') ?? false;
  103. if (sourceActorId) {
  104. logger.debug(`Detected spawned token with unowned actor ${sourceActorId}. Correcting token update.`, tokenDoc, update);
  105. update.actorId = sourceActorId;
  106. }
  107. }
  108. }
  109. static #idByQuery( list, key, comparisonPath ) {
  110. const id = this.#findByQuery(list, key, comparisonPath)?.id ?? null;
  111. return id;
  112. }
  113. static #findByQuery( list, key, comparisonPath ) {
  114. return list.find( element => getProperty(MODULE.isV10 ? element : element.data, comparisonPath) === key )
  115. }
  116. //TODO change to reduce
  117. static _parseUpdateShorthand(collection, updates, comparisonKey) {
  118. let parsedUpdates = Object.keys(updates).map((key) => {
  119. if (updates[key] === warpgate.CONST.DELETE) return { _id: null };
  120. const _id = this.#idByQuery(collection, key, comparisonKey )
  121. return {
  122. ...updates[key],
  123. _id,
  124. }
  125. });
  126. parsedUpdates = parsedUpdates.filter( update => !!update._id);
  127. return parsedUpdates;
  128. }
  129. //TODO change to reduce
  130. static _parseDeleteShorthand(collection, updates, comparisonKey) {
  131. let parsedUpdates = Object.keys(updates).map((key) => {
  132. if (updates[key] !== warpgate.CONST.DELETE) return null;
  133. return this.#idByQuery(collection, key, comparisonKey);
  134. });
  135. parsedUpdates = parsedUpdates.filter( update => !!update);
  136. return parsedUpdates;
  137. }
  138. static _parseAddShorthand(collection, updates, comparisonKey){
  139. let parsedAdds = Object.keys(updates).reduce((acc, key) => {
  140. /* ignore deletes */
  141. if (updates[key] === warpgate.CONST.DELETE) return acc;
  142. /* ignore item updates for items that exist */
  143. if (this.#idByQuery(collection, key, comparisonKey)) return acc;
  144. let data = updates[key];
  145. setProperty(data, comparisonKey, key);
  146. acc.push(data);
  147. return acc;
  148. },[]);
  149. return parsedAdds;
  150. }
  151. static _invertShorthand(collection, updates, comparisonKey){
  152. let inverted = {};
  153. Object.keys(updates).forEach( (key) => {
  154. /* find this item currently and copy off its data */
  155. const currentData = this.#findByQuery(collection, key, comparisonKey);
  156. /* this is a delete */
  157. if (updates[key] === warpgate.CONST.DELETE) {
  158. /* hopefully we found something */
  159. if(currentData) setProperty(inverted, key, currentData.toObject());
  160. else logger.debug('Delta Creation: Could not locate shorthand identified document for deletion.', collection, key, updates[key]);
  161. return;
  162. }
  163. /* this is an update */
  164. if (currentData){
  165. /* grab the current value of any updated fields and store */
  166. const expandedUpdate = expandObject(updates[key]);
  167. const sourceData = currentData.toObject();
  168. const updatedData = mergeObject(sourceData, expandedUpdate, {inplace: false});
  169. const diff = MODULE.strictUpdateDiff(updatedData, sourceData);
  170. setProperty(inverted, updatedData[comparisonKey], diff);
  171. return;
  172. }
  173. /* must be an add, so we delete */
  174. setProperty(inverted, key, warpgate.CONST.DELETE);
  175. });
  176. return inverted;
  177. }
  178. static _errorCheckEmbeddedUpdates( embeddedName, updates ) {
  179. /* at the moment, the most pressing error is an Item creation without a 'type' field.
  180. * This typically indicates a failed lookup for an update operation
  181. */
  182. if( embeddedName == 'Item'){
  183. const badItemAdd = (updates.add ?? []).find( add => !add.type );
  184. if (badItemAdd) {
  185. logger.info(badItemAdd);
  186. const message = MODULE.format('error.badMutate.missing.type', {embeddedName});
  187. return {error: true, message}
  188. }
  189. }
  190. return {error:false};
  191. }
  192. /* run the provided updates for the given embedded collection name from the owner */
  193. static async _performEmbeddedUpdates(owner, embeddedName, updates, comparisonKey = 'name', updateOpts = {}){
  194. const collection = owner.getEmbeddedCollection(embeddedName);
  195. const parsedAdds = Mutator._parseAddShorthand(collection, updates, comparisonKey);
  196. const parsedUpdates = Mutator._parseUpdateShorthand(collection, updates, comparisonKey);
  197. const parsedDeletes = Mutator._parseDeleteShorthand(collection, updates, comparisonKey);
  198. logger.debug(`Modify embedded ${embeddedName} of ${owner.name} from`, {adds: parsedAdds, updates: parsedUpdates, deletes: parsedDeletes});
  199. const {error, message} = Mutator._errorCheckEmbeddedUpdates( embeddedName, {add: parsedAdds, update: parsedUpdates, delete: parsedDeletes} );
  200. if(error) {
  201. logger.error(message);
  202. return false;
  203. }
  204. try {
  205. if (parsedAdds.length > 0) await owner.createEmbeddedDocuments(embeddedName, parsedAdds, updateOpts);
  206. } catch (e) {
  207. logger.error(e);
  208. }
  209. try {
  210. if (parsedUpdates.length > 0) await owner.updateEmbeddedDocuments(embeddedName, parsedUpdates, updateOpts);
  211. } catch (e) {
  212. logger.error(e);
  213. }
  214. try {
  215. if (parsedDeletes.length > 0) await owner.deleteEmbeddedDocuments(embeddedName, parsedDeletes, updateOpts);
  216. } catch (e) {
  217. logger.error(e);
  218. }
  219. return true;
  220. }
  221. /* embeddedUpdates keyed by embedded name, contains shorthand */
  222. static async _updateEmbedded(owner, embeddedUpdates, comparisonKeys, updateOpts = {}){
  223. /* @TODO check for any recursive embeds*/
  224. if (embeddedUpdates?.embedded) delete embeddedUpdates.embedded;
  225. for(const embeddedName of Object.keys(embeddedUpdates ?? {})){
  226. await Mutator._performEmbeddedUpdates(owner, embeddedName, embeddedUpdates[embeddedName],
  227. comparisonKeys[embeddedName] ?? MODULE[NAME].comparisonKey,
  228. updateOpts[embeddedName] ?? {})
  229. }
  230. }
  231. /* updates the actor and any embedded documents of this actor */
  232. /* @TODO support embedded documents within embedded documents */
  233. static async _updateActor(actor, updates = {}, comparisonKeys = {}, updateOpts = {}) {
  234. logger.debug('Performing update on (actor/updates)',actor, updates, comparisonKeys, updateOpts);
  235. await warpgate.wait(MODULE.setting('updateDelay')); // @workaround for semaphore bug
  236. /** perform the updates */
  237. if (updates.actor) await actor.update(updates.actor, updateOpts.actor ?? {});
  238. await Mutator._updateEmbedded(actor, updates.embedded, comparisonKeys, updateOpts.embedded);
  239. return;
  240. }
  241. /**
  242. * Given an update argument identical to `warpgate.spawn` and a token document, will apply the changes listed
  243. * in the updates and (by default) store the change delta, which allows these updates to be reverted. Mutating
  244. * the same token multiple times will "stack" the delta changes, allowing the user to remove them as desired,
  245. * while preserving changes made "higher" in the stack.
  246. *
  247. * @param {TokenDocument} tokenDoc Token document to update, does not accept Token Placeable.
  248. * @param {Shorthand} [updates] As {@link warpgate.spawn}
  249. * @param {Object} [callbacks] Two provided callback locations: delta and post. Both are awaited.
  250. * @param {PostDelta} [callbacks.delta]
  251. * @param {PostMutate} [callbacks.post]
  252. * @param {WorkflowOptions & MutationOptions} [options]
  253. *
  254. * @return {Promise<MutationData|false>} The mutation stack entry produced by this mutation, if they are tracked (i.e. not permanent).
  255. */
  256. static async mutate(tokenDoc, updates = {}, callbacks = {}, options = {}) {
  257. const neededPerms = MODULE.canMutate(game.user)
  258. if(neededPerms.length > 0) {
  259. logger.warn(MODULE.format('error.missingPerms', {permList: neededPerms.join(', ')}));
  260. return false;
  261. }
  262. /* the provided update object will be mangled for our use -- copy it to
  263. * preserve the user's original input if requested (default).
  264. */
  265. if(!options.overrides?.preserveData) {
  266. updates = MODULE.copy(updates, 'error.badUpdate.complex');
  267. if(!updates) return false;
  268. options = foundry.utils.mergeObject(options, {overrides: {preserveData: true}}, {inplace: false});
  269. }
  270. /* ensure that we are working with clean data */
  271. await Mutator.clean(updates, options);
  272. /* providing a delta means you are managing the
  273. * entire data change (including mutation stack changes).
  274. * Typically used by remote requests */
  275. /* create a default mutation info assuming we were provided
  276. * with the final delta already or the change is permanent
  277. */
  278. let mutateInfo = Mutator._createMutateInfo( options.delta ?? {}, options );
  279. /* check that this mutation name is unique */
  280. const present = warpgate.mutationStack(tokenDoc).getName(mutateInfo.name);
  281. if(!!present) {
  282. logger.warn(MODULE.format('error.badMutate.duplicate', {name: mutateInfo.name}));
  283. return false;
  284. }
  285. /* ensure the options parameter has a name field if not provided */
  286. options.name = mutateInfo.name;
  287. /* expand the object to handle property paths correctly */
  288. MODULE.shimUpdate(updates);
  289. /* permanent changes are not tracked */
  290. if(!options.permanent) {
  291. /* if we have the delta provided, trust it */
  292. let delta = options.delta ?? Mutator._createDelta(tokenDoc, updates, options);
  293. /* allow user to modify delta if needed (remote updates will never have callbacks) */
  294. if (callbacks.delta) {
  295. const cont = await callbacks.delta(delta, tokenDoc);
  296. if(cont === false) return false;
  297. }
  298. /* update the mutation info with the final updates including mutate stack info */
  299. mutateInfo = Mutator._mergeMutateDelta(tokenDoc.actor, delta, updates, options);
  300. options.delta = mutateInfo.delta;
  301. } else if (callbacks.delta) {
  302. /* call the delta callback if provided, but there is no object to modify */
  303. const cont = await callbacks.delta({}, tokenDoc);
  304. if(cont === false) return false;
  305. }
  306. if (tokenDoc.actor.isOwner) {
  307. if(options.notice && tokenDoc.object) {
  308. const placement = {
  309. scene: tokenDoc.object.scene,
  310. ...tokenDoc.object.center,
  311. };
  312. warpgate.plugin.notice(placement, options.notice);
  313. }
  314. await Mutator._update(tokenDoc, updates, options);
  315. if(callbacks.post) await callbacks.post(tokenDoc, updates, true);
  316. await warpgate.event.notify(warpgate.EVENT.MUTATE, {
  317. uuid: tokenDoc.uuid,
  318. updates: (options.overrides?.includeRawData ?? false) ? updates : 'omitted',
  319. options
  320. });
  321. } else {
  322. /* this is a remote mutation request, hand it over to that system */
  323. return RemoteMutator.remoteMutate( tokenDoc, {updates, callbacks, options} );
  324. }
  325. return mutateInfo;
  326. }
  327. /**
  328. * Perform a managed, batch update of multple token documents. Heterogeneous ownership supported
  329. * and routed through the Remote Mutation system as needed. The same updates, callbacks and options
  330. * objects will be used for all mutations.
  331. *
  332. * Note: If a specific mutation name is not provided, a single random ID will be generated for all
  333. * resulting individual mutations.
  334. *
  335. * @static
  336. * @param {Array<TokenDocument>} tokenDocs List of tokens on which to apply the provided mutation.
  337. * @param {Object} details The details of this batch mutation operation.
  338. * @param {Shorthand} details.updates The updates to apply to each token; as {@link warpgate.spawn}
  339. * @param {Object} [details.callbacks] Delta and post mutation callbacks; as {@link warpgate.mutate}
  340. * @param {PostDelta} [details.callbacks.delta]
  341. * @param {PostMutate} [details.callbacks.post]
  342. * @param {WorkflowOptions & MutationOptions} [details.options]
  343. *
  344. * @returns {Promise<Array<MutateInfo>>} List of mutation results, which resolve
  345. * once all local mutations have been applied and when all remote mutations have been _accepted_
  346. * or _rejected_. Currently, local and remote mutations will contain differing object structures.
  347. * Notably, local mutations contain a `delta` field containing the revert data for
  348. * this mutation; whereas remote mutations will contain an `accepted` field,
  349. * indicating if the request was accepted.
  350. */
  351. static async batchMutate( tokenDocs, {updates, callbacks, options} ) {
  352. /* break token list into sublists by first owner */
  353. const tokenLists = MODULE.ownerSublist(tokenDocs);
  354. if((tokenLists['none'] ?? []).length > 0) {
  355. logger.warn(MODULE.localize('error.offlineOwnerBatch'));
  356. logger.debug('Affected UUIDs:', tokenLists['none'].map( t => t.uuid ));
  357. delete tokenLists['none'];
  358. }
  359. options.name ??= randomID();
  360. let promises = Reflect.ownKeys(tokenLists).flatMap( async (owner) => {
  361. if(owner == game.userId) {
  362. //self service mutate
  363. return await tokenLists[owner].map( tokenDoc => warpgate.mutate(tokenDoc, updates, callbacks, options) );
  364. }
  365. /* is a remote update */
  366. return await RemoteMutator.remoteBatchMutate( tokenLists[owner], {updates, callbacks, options} );
  367. })
  368. /* wait for each client batch of mutations to complete */
  369. promises = await Promise.all(promises);
  370. /* flatten all into a single array, and ensure all subqueries are complete */
  371. return Promise.all(promises.flat());
  372. }
  373. /**
  374. * Perform a managed, batch update of multple token documents. Heterogeneous ownership supported
  375. * and routed through the Remote Mutation system as needed. The same updates, callbacks and options
  376. * objects will be used for all mutations.
  377. *
  378. * Note: If a specific mutation name is not provided, a single random ID will be generated for all
  379. * resulting individual mutations.
  380. *
  381. * @static
  382. * @param {Array<TokenDocument>} tokenDocs List of tokens on which to perform the revert
  383. * @param {Object} details
  384. * @param {string} [details.mutationName] Specific mutation name to revert, or the latest mutation
  385. * for an individual token if not provided. Tokens without mutations or without the specific
  386. * mutation requested are not processed.
  387. * @param {WorkflowOptions & MutationOptions} [details.options]
  388. * @returns {Promise<Array<MutateInfo>>} List of mutation revert results, which resolve
  389. * once all local reverts have been applied and when all remote reverts have been _accepted_
  390. * or _rejected_. Currently, local and remote reverts will contain differing object structures.
  391. * Notably, local revert contain a `delta` field containing the revert data for
  392. * this mutation; whereas remote reverts will contain an `accepted` field,
  393. * indicating if the request was accepted.
  394. */
  395. static async batchRevert( tokenDocs, {mutationName = null, options = {}} = {} ) {
  396. const tokenLists = MODULE.ownerSublist(tokenDocs);
  397. if((tokenLists['none'] ?? []).length > 0) {
  398. logger.warn(MODULE.localize('error.offlineOwnerBatch'));
  399. logger.debug('Affected UUIDs:', tokenLists['none'].map( t => t.uuid ));
  400. delete tokenLists['none'];
  401. }
  402. let promises = Reflect.ownKeys(tokenLists).map( (owner) => {
  403. if(owner == game.userId) {
  404. //self service mutate
  405. return tokenLists[owner].map( tokenDoc => warpgate.revert(tokenDoc, mutationName, options) );
  406. }
  407. /* is a remote update */
  408. return RemoteMutator.remoteBatchRevert( tokenLists[owner], {mutationName, options} );
  409. })
  410. promises = await Promise.all(promises);
  411. return Promise.all(promises.flat());
  412. }
  413. /**
  414. * @returns {MutationData}
  415. */
  416. static _createMutateInfo( delta, options = {} ) {
  417. options.name ??= randomID();
  418. return {
  419. delta: MODULE.stripEmpty(delta),
  420. user: game.user.id,
  421. comparisonKeys: MODULE.stripEmpty(options.comparisonKeys ?? {}, false),
  422. name: options.name,
  423. updateOpts: MODULE.stripEmpty(options.updateOpts ?? {}, false),
  424. overrides: MODULE.stripEmpty(options.overrides ?? {}, false),
  425. };
  426. }
  427. static _cleanInner(single) {
  428. Object.keys(single).forEach( key => {
  429. /* dont process embedded */
  430. if(key == 'embedded') return;
  431. /* dont process delete identifiers */
  432. if(typeof single[key] == 'string') return;
  433. /* convert value to plain object if possible */
  434. if(single[key]?.toObject) single[key] = single[key].toObject();
  435. if(single[key] == undefined) {
  436. single[key] = {};
  437. }
  438. return;
  439. });
  440. }
  441. /**
  442. * Cleans and validates mutation data
  443. * @param {Shorthand} updates
  444. * @param {SpawningOptions & MutationOptions} [options]
  445. */
  446. static async clean(updates, options = undefined) {
  447. if(!!updates) {
  448. /* ensure we are working with raw objects */
  449. Mutator._cleanInner(updates);
  450. /* perform cleaning on shorthand embedded updates */
  451. Object.values(updates.embedded ?? {}).forEach( type => Mutator._cleanInner(type));
  452. /* if the token is getting an image update, preload it */
  453. let source;
  454. if('src' in (updates.token?.texture ?? {})) {
  455. source = updates.token.texture.src;
  456. }
  457. else if( 'img' in (updates.token ?? {})){
  458. source = updates.token.img;
  459. }
  460. /* load texture if provided */
  461. try {
  462. !!source ? await loadTexture(source) : null;
  463. } catch (err) {
  464. logger.debug(err);
  465. }
  466. }
  467. if(!!options) {
  468. /* insert the better ActiveEffect default ONLY IF
  469. * one wasn't provided in the options object initially
  470. */
  471. options.comparisonKeys = foundry.utils.mergeObject(
  472. options.comparisonKeys ?? {},
  473. {ActiveEffect: 'label'},
  474. {overwrite:false, inplace:false});
  475. /* if `id` is being used as the comparison key,
  476. * change it to `_id` and set the option to `keepId=true`
  477. * if either are present
  478. */
  479. options.comparisonKeys ??= {};
  480. options.updateOpts ??= {};
  481. Object.keys(options.comparisonKeys).forEach( embName => {
  482. /* switch to _id if needed */
  483. if(options.comparisonKeys[embName] == 'id') options.comparisonKeys[embName] = '_id'
  484. /* flag this update to preserve ids */
  485. if(options.comparisonKeys[embName] == '_id') {
  486. foundry.utils.mergeObject(options.updateOpts, {embedded: {[embName]: {keepId: true}}});
  487. }
  488. });
  489. }
  490. }
  491. static _mergeMutateDelta(actorDoc, delta, updates, options) {
  492. /* Grab the current stack (or make a new one) */
  493. let mutateStack = actorDoc.getFlag(MODULE.data.name, 'mutate') ?? [];
  494. /* create the information needed to revert this mutation and push
  495. * it onto the stack
  496. */
  497. const mutateInfo = Mutator._createMutateInfo( delta, options );
  498. mutateStack.push(mutateInfo);
  499. /* Create a new mutation stack flag data and store it in the update object */
  500. const flags = {warpgate: {mutate: mutateStack}};
  501. updates.actor = mergeObject(updates.actor ?? {}, {flags});
  502. return mutateInfo;
  503. }
  504. /* @return {Promise} */
  505. static async _update(tokenDoc, updates, options = {}) {
  506. /* update the token */
  507. await tokenDoc.update(updates.token ?? {}, options.updateOpts?.token ?? {});
  508. if(!options.noMoveWait && !!tokenDoc.object) {
  509. await CanvasAnimation.getAnimation(tokenDoc.object.animationName)?.promise
  510. }
  511. /* update the actor */
  512. return Mutator._updateActor(tokenDoc.actor, updates, options.comparisonKeys ?? {}, options.updateOpts ?? {});
  513. }
  514. /**
  515. * Will peel off the last applied mutation change from the provided token document
  516. *
  517. * @param {TokenDocument} tokenDoc Token document to revert the last applied mutation.
  518. * @param {String} [mutationName]. Specific mutation name to revert. optional.
  519. * @param {WorkflowOptions} [options]
  520. *
  521. * @return {Promise<MutationData|undefined>} The mutation data (updates) used for this
  522. * revert operation or `undefined` if none occured.
  523. */
  524. static async revertMutation(tokenDoc, mutationName = undefined, options = {}) {
  525. const mutateData = await Mutator._popMutation(tokenDoc?.actor, mutationName);
  526. if(!mutateData) {
  527. return;
  528. }
  529. if (tokenDoc.actor?.isOwner) {
  530. if(options.notice && tokenDoc.object) {
  531. const placement = {
  532. scene: tokenDoc.object.scene,
  533. ...tokenDoc.object.center,
  534. };
  535. warpgate.plugin.notice(placement, options.notice);
  536. }
  537. /* the provided options object will be mangled for our use -- copy it to
  538. * preserve the user's original input if requested (default).
  539. */
  540. if(!options.overrides?.preserveData) {
  541. options = MODULE.copy(options, 'error.badUpdate.complex');
  542. if(!options) return;
  543. options = foundry.utils.mergeObject(options, {overrides: {preserveData: true}}, {inplace: false});
  544. }
  545. /* perform the revert with the stored delta */
  546. MODULE.shimUpdate(mutateData.delta);
  547. mutateData.updateOpts ??= {};
  548. mutateData.overrides ??= {};
  549. foundry.utils.mergeObject(mutateData.updateOpts, options.updateOpts ?? {});
  550. foundry.utils.mergeObject(mutateData.overrides, options.overrides ?? {});
  551. await Mutator._update(tokenDoc, mutateData.delta, {
  552. overrides: mutateData.overrides,
  553. comparisonKeys: mutateData.comparisonKeys,
  554. updateOpts: mutateData.updateOpts
  555. });
  556. /* notify clients */
  557. warpgate.event.notify(warpgate.EVENT.REVERT, {
  558. uuid: tokenDoc.uuid,
  559. updates: (options.overrides?.includeRawData ?? false) ? mutateData : 'omitted',
  560. options});
  561. } else {
  562. return RemoteMutator.remoteRevert(tokenDoc, {mutationId: mutateData.name, options});
  563. }
  564. return mutateData;
  565. }
  566. static async _popMutation(actor, mutationName) {
  567. let mutateStack = actor?.getFlag(MODULE.data.name, 'mutate') ?? [];
  568. if (mutateStack.length == 0 || !actor){
  569. logger.debug(`Provided actor is undefined or has no mutation stack. Cannot pop.`);
  570. return undefined;
  571. }
  572. let mutateData = undefined;
  573. if (!!mutationName) {
  574. /* find specific mutation */
  575. const index = mutateStack.findIndex( mutation => mutation.name === mutationName );
  576. /* check for no result and log */
  577. if ( index < 0 ) {
  578. logger.debug(`Could not locate mutation named ${mutationName} in actor ${actor.name}`);
  579. return undefined;
  580. }
  581. /* otherwise, retrieve and remove */
  582. mutateData = mutateStack.splice(index, 1)[0];
  583. for( let i = index; i < mutateStack.length; i++){
  584. /* get the values stored in our delta and push any overlapping ones to
  585. * the mutation next in the stack
  586. */
  587. const stackUpdate = filterObject(mutateData.delta, mutateStack[i].delta);
  588. mergeObject(mutateStack[i].delta, stackUpdate);
  589. /* remove any changes that exist higher in the stack, we have
  590. * been overriden and should not restore these values
  591. */
  592. mutateData.delta = MODULE.unique(mutateData.delta, mutateStack[i].delta)
  593. }
  594. } else {
  595. /* pop the most recent mutation */
  596. mutateData = mutateStack.pop();
  597. }
  598. const newFlags = {[`${MODULE.data.name}.mutate`]: mutateStack};
  599. /* set the current mutation stack in the mutation data */
  600. foundry.utils.mergeObject(mutateData.delta, {actor: {flags: newFlags}});
  601. logger.debug(MODULE.localize('debug.finalRevertUpdate'), mutateData);
  602. return mutateData;
  603. }
  604. /* given a token document and the standard update object,
  605. * parse the changes that need to be applied to *reverse*
  606. * the mutate operation
  607. */
  608. static _createDelta(tokenDoc, updates, options) {
  609. /* get token changes */
  610. let tokenData = tokenDoc.toObject()
  611. tokenData.actorData = {};
  612. const tokenDelta = MODULE.strictUpdateDiff(updates.token ?? {}, tokenData);
  613. /* get the actor changes (no embeds) */
  614. const actorData = Mutator._getRootActorData(tokenDoc.actor);
  615. const actorDelta = MODULE.strictUpdateDiff(updates.actor ?? {}, actorData);
  616. /* get the changes from the embeds */
  617. let embeddedDelta = {};
  618. if(updates.embedded) {
  619. for( const embeddedName of Object.keys(updates.embedded) ) {
  620. const collection = tokenDoc.actor.getEmbeddedCollection(embeddedName);
  621. const invertedShorthand = Mutator._invertShorthand(collection, updates.embedded[embeddedName], getProperty(options.comparisonKeys, embeddedName) ?? 'name');
  622. embeddedDelta[embeddedName] = invertedShorthand;
  623. }
  624. }
  625. logger.debug(MODULE.localize('debug.tokenDelta'), tokenDelta, MODULE.localize('debug.actorDelta'), actorDelta, MODULE.localize('debug.embeddedDelta'), embeddedDelta);
  626. return {token: tokenDelta, actor: actorDelta, embedded: embeddedDelta}
  627. }
  628. /* returns the actor data sans ALL embedded collections */
  629. static _getRootActorData(actorDoc) {
  630. let actorData = actorDoc.toObject();
  631. /* get the key NAME of the embedded document type.
  632. * ex. not 'ActiveEffect' (the class name), 'effect' the collection's field name
  633. */
  634. let embeddedFields = Object.values(Actor.implementation.metadata.embedded);
  635. if(!MODULE.isV10) {
  636. embeddedFields = embeddedFields.map( thisClass => thisClass.metadata.collection );
  637. }
  638. /* delete any embedded fields from the actor data */
  639. embeddedFields.forEach( field => { delete actorData[field] } )
  640. return actorData;
  641. }
  642. }