|
|
- /*
- * 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 {RemoteMutator} from './remote-mutator.js'
-
- const NAME = "Mutator";
-
- /** @typedef {import('./api.js').ComparisonKeys} ComparisonKeys */
- /** @typedef {import('./api.js').NoticeConfig} NoticeConfig */
- /** @typedef {import('./mutation-stack.js').MutationData} MutationData */
- /** @typedef {import('./api.js').Shorthand} Shorthand */
- /** @typedef {import('./api.js').SpawningOptions} SpawningOptions */
-
- //TODO proper objects
- /** @typedef {Object} MutateInfo **/
-
- /**
- * Workflow options
- * @typedef {Object} WorkflowOptions
- * @property {Shorthand} [updateOpts] Options for the creation/deletion/updating of (embedded) documents related to this mutation
- * @property {string} [description] Description of this mutation for potential display to the remote owning user.
- * @property {NoticeConfig} [notice] Options for placing a ping or panning to the token after mutation
- * @property {boolean} [noMoveWait = false] If true, will not wait for potential token movement animation to complete before proceeding with remaining actor/embedded updates.
- * @property {Object} [overrides]
- * @property {boolean} [overrides.alwaysAccept = false] Force the receiving clients "auto-accept" state,
- * regardless of world/client settings
- * @property {boolean} [overrides.suppressToast = false] Force the initiating and receiving clients to suppress
- * the "call and response" UI toasts indicating the requests accepted/rejected response.
- * @property {boolean} [overrides.includeRawData = false] Force events produced from this operation to include the
- * raw data used for its operation (such as the final mutation data to be applied, or the resulting packed actor
- * data from a spawn). **Caution, use judiciously** -- enabling this option can result in potentially large
- * socket data transfers during warpgate operation.
- * @property {boolean} [overrides.preserveData = false] If enabled, the provided updates data object will
- * be modified in-place as needed for internal Warp Gate operations and will NOT be re-usable for a
- * subsequent operation. Otherwise, the provided data is copied and modified internally, preserving
- * the original input for subsequent re-use.
- *
- */
-
- /**
- *
- * @typedef {Object} MutationOptions
- * @property {boolean} [permanent=false] Indicates if this should be treated as a permanent change
- * to the actor, which does not store the update delta information required to revert mutation.
- * @property {string} [name=randomId()] User provided name, or identifier, for this particular
- * mutation operation. Used for reverting mutations by name, as opposed to popping last applied.
- * @property {Object} [delta]
- * @property {ComparisonKeys} [comparisonKeys]
- */
-
- /**
- * The post delta creation, pre mutate callback. Called after the update delta has been generated, but before
- * it is stored on the actor. Can be used to modify this delta for storage (ex. Current and Max HP are
- * increased by 10, but when reverted, you want to keep the extra Current HP applied. Update the delta object
- * with the desired HP to return to after revert, or remove it entirely.
- *
- * @typedef {(function(Shorthand,TokenDocument):Promise|undefined)} PostDelta
- * @param {Shorthand} delta Computed change of the actor based on `updates`. Used to "unroll" this mutation when reverted.
- * @param {TokenDocument} tokenDoc Token being modified.
- *
- * @returns {Promise<any>|any}
- */
-
- /**
- * The post mutate callback prototype. Called after the actor has been mutated and after the mutate event
- * has triggered. Useful for animations or changes that should not be tracked by the mutation system.
- *
- * @typedef {function(TokenDocument, Object, boolean):Promise|void} PostMutate
- * @param {TokenDocument} tokenDoc Token that has been modified.
- * @param {Shorthand} updates Current permutation of the original shorthand updates object provided, as
- * applied for this mutation
- * @param {boolean} accepted Whether or not the mutation was accepted by the first owner.
- *
- * @returns {Promise<any>|any}
- */
-
- export class Mutator {
- static register() {
- Mutator.defaults();
- Mutator.hooks();
- }
-
- static defaults(){
- MODULE[NAME] = {
- comparisonKey: 'name'
- }
- }
-
- static hooks() {
- if(!MODULE.isV10) Hooks.on('preUpdateToken', Mutator._correctActorLink)
- }
-
- static _correctActorLink(tokenDoc, update) {
-
- /* if the actorId has been updated AND its being set to null,
- * check if we can patch/fix this warpgate spawn
- */
- if (update.hasOwnProperty('actorId') && update.actorId === null) {
- const sourceActorId = tokenDoc.getFlag(MODULE.data.name, 'sourceActorId') ?? false;
- if (sourceActorId) {
- logger.debug(`Detected spawned token with unowned actor ${sourceActorId}. Correcting token update.`, tokenDoc, update);
- update.actorId = sourceActorId;
- }
- }
- }
-
- static #idByQuery( list, key, comparisonPath ) {
- const id = this.#findByQuery(list, key, comparisonPath)?.id ?? null;
-
- return id;
- }
-
- static #findByQuery( list, key, comparisonPath ) {
- return list.find( element => getProperty(MODULE.isV10 ? element : element.data, comparisonPath) === key )
- }
-
- //TODO change to reduce
- static _parseUpdateShorthand(collection, updates, comparisonKey) {
- let parsedUpdates = Object.keys(updates).map((key) => {
- if (updates[key] === warpgate.CONST.DELETE) return { _id: null };
- const _id = this.#idByQuery(collection, key, comparisonKey )
- return {
- ...updates[key],
- _id,
- }
- });
- parsedUpdates = parsedUpdates.filter( update => !!update._id);
- return parsedUpdates;
- }
-
- //TODO change to reduce
- static _parseDeleteShorthand(collection, updates, comparisonKey) {
- let parsedUpdates = Object.keys(updates).map((key) => {
- if (updates[key] !== warpgate.CONST.DELETE) return null;
- return this.#idByQuery(collection, key, comparisonKey);
- });
-
- parsedUpdates = parsedUpdates.filter( update => !!update);
- return parsedUpdates;
- }
-
- static _parseAddShorthand(collection, updates, comparisonKey){
-
- let parsedAdds = Object.keys(updates).reduce((acc, key) => {
-
- /* ignore deletes */
- if (updates[key] === warpgate.CONST.DELETE) return acc;
-
- /* ignore item updates for items that exist */
- if (this.#idByQuery(collection, key, comparisonKey)) return acc;
-
- let data = updates[key];
- setProperty(data, comparisonKey, key);
- acc.push(data);
- return acc;
- },[]);
-
- return parsedAdds;
-
- }
-
- static _invertShorthand(collection, updates, comparisonKey){
- let inverted = {};
- Object.keys(updates).forEach( (key) => {
-
- /* find this item currently and copy off its data */
- const currentData = this.#findByQuery(collection, key, comparisonKey);
-
- /* this is a delete */
- if (updates[key] === warpgate.CONST.DELETE) {
-
- /* hopefully we found something */
- if(currentData) setProperty(inverted, key, currentData.toObject());
- else logger.debug('Delta Creation: Could not locate shorthand identified document for deletion.', collection, key, updates[key]);
-
- return;
- }
-
- /* this is an update */
- if (currentData){
- /* grab the current value of any updated fields and store */
- const expandedUpdate = expandObject(updates[key]);
- const sourceData = currentData.toObject();
- const updatedData = mergeObject(sourceData, expandedUpdate, {inplace: false});
-
- const diff = MODULE.strictUpdateDiff(updatedData, sourceData);
-
- setProperty(inverted, updatedData[comparisonKey], diff);
- return;
- }
-
- /* must be an add, so we delete */
- setProperty(inverted, key, warpgate.CONST.DELETE);
-
- });
-
- return inverted;
- }
-
-
-
- static _errorCheckEmbeddedUpdates( embeddedName, updates ) {
-
- /* at the moment, the most pressing error is an Item creation without a 'type' field.
- * This typically indicates a failed lookup for an update operation
- */
- if( embeddedName == 'Item'){
- const badItemAdd = (updates.add ?? []).find( add => !add.type );
-
- if (badItemAdd) {
- logger.info(badItemAdd);
- const message = MODULE.format('error.badMutate.missing.type', {embeddedName});
-
- return {error: true, message}
- }
- }
-
- return {error:false};
- }
-
- /* run the provided updates for the given embedded collection name from the owner */
- static async _performEmbeddedUpdates(owner, embeddedName, updates, comparisonKey = 'name', updateOpts = {}){
-
- const collection = owner.getEmbeddedCollection(embeddedName);
-
- const parsedAdds = Mutator._parseAddShorthand(collection, updates, comparisonKey);
- const parsedUpdates = Mutator._parseUpdateShorthand(collection, updates, comparisonKey);
- const parsedDeletes = Mutator._parseDeleteShorthand(collection, updates, comparisonKey);
-
- logger.debug(`Modify embedded ${embeddedName} of ${owner.name} from`, {adds: parsedAdds, updates: parsedUpdates, deletes: parsedDeletes});
-
- const {error, message} = Mutator._errorCheckEmbeddedUpdates( embeddedName, {add: parsedAdds, update: parsedUpdates, delete: parsedDeletes} );
- if(error) {
- logger.error(message);
- return false;
- }
-
- try {
- if (parsedAdds.length > 0) await owner.createEmbeddedDocuments(embeddedName, parsedAdds, updateOpts);
- } catch (e) {
- logger.error(e);
- }
-
- try {
- if (parsedUpdates.length > 0) await owner.updateEmbeddedDocuments(embeddedName, parsedUpdates, updateOpts);
- } catch (e) {
- logger.error(e);
- }
-
- try {
- if (parsedDeletes.length > 0) await owner.deleteEmbeddedDocuments(embeddedName, parsedDeletes, updateOpts);
- } catch (e) {
- logger.error(e);
- }
-
- return true;
- }
-
- /* embeddedUpdates keyed by embedded name, contains shorthand */
- static async _updateEmbedded(owner, embeddedUpdates, comparisonKeys, updateOpts = {}){
-
- /* @TODO check for any recursive embeds*/
- if (embeddedUpdates?.embedded) delete embeddedUpdates.embedded;
-
- for(const embeddedName of Object.keys(embeddedUpdates ?? {})){
- await Mutator._performEmbeddedUpdates(owner, embeddedName, embeddedUpdates[embeddedName],
- comparisonKeys[embeddedName] ?? MODULE[NAME].comparisonKey,
- updateOpts[embeddedName] ?? {})
- }
-
- }
-
- /* updates the actor and any embedded documents of this actor */
- /* @TODO support embedded documents within embedded documents */
- static async _updateActor(actor, updates = {}, comparisonKeys = {}, updateOpts = {}) {
-
- logger.debug('Performing update on (actor/updates)',actor, updates, comparisonKeys, updateOpts);
- await warpgate.wait(MODULE.setting('updateDelay')); // @workaround for semaphore bug
-
- /** perform the updates */
- if (updates.actor) await actor.update(updates.actor, updateOpts.actor ?? {});
-
- await Mutator._updateEmbedded(actor, updates.embedded, comparisonKeys, updateOpts.embedded);
-
- return;
- }
-
-
- /**
- * Given an update argument identical to `warpgate.spawn` and a token document, will apply the changes listed
- * in the updates and (by default) store the change delta, which allows these updates to be reverted. Mutating
- * the same token multiple times will "stack" the delta changes, allowing the user to remove them as desired,
- * while preserving changes made "higher" in the stack.
- *
- * @param {TokenDocument} tokenDoc Token document to update, does not accept Token Placeable.
- * @param {Shorthand} [updates] As {@link warpgate.spawn}
- * @param {Object} [callbacks] Two provided callback locations: delta and post. Both are awaited.
- * @param {PostDelta} [callbacks.delta]
- * @param {PostMutate} [callbacks.post]
- * @param {WorkflowOptions & MutationOptions} [options]
- *
- * @return {Promise<MutationData|false>} The mutation stack entry produced by this mutation, if they are tracked (i.e. not permanent).
- */
- static async mutate(tokenDoc, updates = {}, callbacks = {}, options = {}) {
-
- const neededPerms = MODULE.canMutate(game.user)
- if(neededPerms.length > 0) {
- logger.warn(MODULE.format('error.missingPerms', {permList: neededPerms.join(', ')}));
- return false;
- }
-
- /* the provided update object will be mangled for our use -- copy it to
- * preserve the user's original input if requested (default).
- */
- if(!options.overrides?.preserveData) {
- updates = MODULE.copy(updates, 'error.badUpdate.complex');
- if(!updates) return false;
- options = foundry.utils.mergeObject(options, {overrides: {preserveData: true}}, {inplace: false});
- }
-
- /* ensure that we are working with clean data */
- await Mutator.clean(updates, options);
-
- /* providing a delta means you are managing the
- * entire data change (including mutation stack changes).
- * Typically used by remote requests */
-
- /* create a default mutation info assuming we were provided
- * with the final delta already or the change is permanent
- */
- let mutateInfo = Mutator._createMutateInfo( options.delta ?? {}, options );
-
- /* check that this mutation name is unique */
- const present = warpgate.mutationStack(tokenDoc).getName(mutateInfo.name);
- if(!!present) {
- logger.warn(MODULE.format('error.badMutate.duplicate', {name: mutateInfo.name}));
- return false;
- }
-
- /* ensure the options parameter has a name field if not provided */
- options.name = mutateInfo.name;
-
- /* expand the object to handle property paths correctly */
- MODULE.shimUpdate(updates);
-
- /* permanent changes are not tracked */
- if(!options.permanent) {
-
- /* if we have the delta provided, trust it */
- let delta = options.delta ?? Mutator._createDelta(tokenDoc, updates, options);
-
- /* allow user to modify delta if needed (remote updates will never have callbacks) */
- if (callbacks.delta) {
-
- const cont = await callbacks.delta(delta, tokenDoc);
- if(cont === false) return false;
-
- }
-
- /* update the mutation info with the final updates including mutate stack info */
- mutateInfo = Mutator._mergeMutateDelta(tokenDoc.actor, delta, updates, options);
-
- options.delta = mutateInfo.delta;
-
- } else if (callbacks.delta) {
- /* call the delta callback if provided, but there is no object to modify */
- const cont = await callbacks.delta({}, tokenDoc);
- if(cont === false) return false;
- }
-
- if (tokenDoc.actor.isOwner) {
-
- if(options.notice && tokenDoc.object) {
-
- const placement = {
- scene: tokenDoc.object.scene,
- ...tokenDoc.object.center,
- };
-
- warpgate.plugin.notice(placement, options.notice);
- }
-
- await Mutator._update(tokenDoc, updates, options);
-
- if(callbacks.post) await callbacks.post(tokenDoc, updates, true);
-
- await warpgate.event.notify(warpgate.EVENT.MUTATE, {
- uuid: tokenDoc.uuid,
- updates: (options.overrides?.includeRawData ?? false) ? updates : 'omitted',
- options
- });
-
- } else {
- /* this is a remote mutation request, hand it over to that system */
- return RemoteMutator.remoteMutate( tokenDoc, {updates, callbacks, options} );
- }
-
- return mutateInfo;
- }
-
- /**
- * Perform a managed, batch update of multple token documents. Heterogeneous ownership supported
- * and routed through the Remote Mutation system as needed. The same updates, callbacks and options
- * objects will be used for all mutations.
- *
- * Note: If a specific mutation name is not provided, a single random ID will be generated for all
- * resulting individual mutations.
- *
- * @static
- * @param {Array<TokenDocument>} tokenDocs List of tokens on which to apply the provided mutation.
- * @param {Object} details The details of this batch mutation operation.
- * @param {Shorthand} details.updates The updates to apply to each token; as {@link warpgate.spawn}
- * @param {Object} [details.callbacks] Delta and post mutation callbacks; as {@link warpgate.mutate}
- * @param {PostDelta} [details.callbacks.delta]
- * @param {PostMutate} [details.callbacks.post]
- * @param {WorkflowOptions & MutationOptions} [details.options]
- *
- * @returns {Promise<Array<MutateInfo>>} List of mutation results, which resolve
- * once all local mutations have been applied and when all remote mutations have been _accepted_
- * or _rejected_. Currently, local and remote mutations will contain differing object structures.
- * Notably, local mutations contain a `delta` field containing the revert data for
- * this mutation; whereas remote mutations will contain an `accepted` field,
- * indicating if the request was accepted.
- */
- static async batchMutate( tokenDocs, {updates, callbacks, options} ) {
-
- /* break token list into sublists by first owner */
- const tokenLists = MODULE.ownerSublist(tokenDocs);
-
- if((tokenLists['none'] ?? []).length > 0) {
- logger.warn(MODULE.localize('error.offlineOwnerBatch'));
- logger.debug('Affected UUIDs:', tokenLists['none'].map( t => t.uuid ));
- delete tokenLists['none'];
- }
-
- options.name ??= randomID();
-
- let promises = Reflect.ownKeys(tokenLists).flatMap( async (owner) => {
- if(owner == game.userId) {
- //self service mutate
- return await tokenLists[owner].map( tokenDoc => warpgate.mutate(tokenDoc, updates, callbacks, options) );
- }
-
- /* is a remote update */
- return await RemoteMutator.remoteBatchMutate( tokenLists[owner], {updates, callbacks, options} );
-
- })
-
- /* wait for each client batch of mutations to complete */
- promises = await Promise.all(promises);
-
- /* flatten all into a single array, and ensure all subqueries are complete */
- return Promise.all(promises.flat());
- }
-
- /**
- * Perform a managed, batch update of multple token documents. Heterogeneous ownership supported
- * and routed through the Remote Mutation system as needed. The same updates, callbacks and options
- * objects will be used for all mutations.
- *
- * Note: If a specific mutation name is not provided, a single random ID will be generated for all
- * resulting individual mutations.
- *
- * @static
- * @param {Array<TokenDocument>} tokenDocs List of tokens on which to perform the revert
- * @param {Object} details
- * @param {string} [details.mutationName] Specific mutation name to revert, or the latest mutation
- * for an individual token if not provided. Tokens without mutations or without the specific
- * mutation requested are not processed.
- * @param {WorkflowOptions & MutationOptions} [details.options]
- * @returns {Promise<Array<MutateInfo>>} List of mutation revert results, which resolve
- * once all local reverts have been applied and when all remote reverts have been _accepted_
- * or _rejected_. Currently, local and remote reverts will contain differing object structures.
- * Notably, local revert contain a `delta` field containing the revert data for
- * this mutation; whereas remote reverts will contain an `accepted` field,
- * indicating if the request was accepted.
-
- */
- static async batchRevert( tokenDocs, {mutationName = null, options = {}} = {} ) {
-
- const tokenLists = MODULE.ownerSublist(tokenDocs);
-
- if((tokenLists['none'] ?? []).length > 0) {
- logger.warn(MODULE.localize('error.offlineOwnerBatch'));
- logger.debug('Affected UUIDs:', tokenLists['none'].map( t => t.uuid ));
- delete tokenLists['none'];
- }
-
- let promises = Reflect.ownKeys(tokenLists).map( (owner) => {
- if(owner == game.userId) {
- //self service mutate
- return tokenLists[owner].map( tokenDoc => warpgate.revert(tokenDoc, mutationName, options) );
- }
-
- /* is a remote update */
- return RemoteMutator.remoteBatchRevert( tokenLists[owner], {mutationName, options} );
-
- })
-
- promises = await Promise.all(promises);
-
- return Promise.all(promises.flat());
- }
-
- /**
- * @returns {MutationData}
- */
- static _createMutateInfo( delta, options = {} ) {
- options.name ??= randomID();
- return {
- delta: MODULE.stripEmpty(delta),
- user: game.user.id,
- comparisonKeys: MODULE.stripEmpty(options.comparisonKeys ?? {}, false),
- name: options.name,
- updateOpts: MODULE.stripEmpty(options.updateOpts ?? {}, false),
- overrides: MODULE.stripEmpty(options.overrides ?? {}, false),
- };
- }
-
- static _cleanInner(single) {
- Object.keys(single).forEach( key => {
- /* dont process embedded */
- if(key == 'embedded') return;
-
- /* dont process delete identifiers */
- if(typeof single[key] == 'string') return;
-
- /* convert value to plain object if possible */
- if(single[key]?.toObject) single[key] = single[key].toObject();
-
- if(single[key] == undefined) {
- single[key] = {};
- }
-
- return;
- });
- }
-
- /**
- * Cleans and validates mutation data
- * @param {Shorthand} updates
- * @param {SpawningOptions & MutationOptions} [options]
- */
- static async clean(updates, options = undefined) {
-
- if(!!updates) {
- /* ensure we are working with raw objects */
- Mutator._cleanInner(updates);
-
- /* perform cleaning on shorthand embedded updates */
- Object.values(updates.embedded ?? {}).forEach( type => Mutator._cleanInner(type));
-
- /* if the token is getting an image update, preload it */
- let source;
- if('src' in (updates.token?.texture ?? {})) {
- source = updates.token.texture.src;
- }
- else if( 'img' in (updates.token ?? {})){
- source = updates.token.img;
- }
-
- /* load texture if provided */
- try {
- !!source ? await loadTexture(source) : null;
- } catch (err) {
- logger.debug(err);
- }
- }
-
- if(!!options) {
- /* insert the better ActiveEffect default ONLY IF
- * one wasn't provided in the options object initially
- */
- options.comparisonKeys = foundry.utils.mergeObject(
- options.comparisonKeys ?? {},
- {ActiveEffect: 'label'},
- {overwrite:false, inplace:false});
-
- /* if `id` is being used as the comparison key,
- * change it to `_id` and set the option to `keepId=true`
- * if either are present
- */
- options.comparisonKeys ??= {};
- options.updateOpts ??= {};
- Object.keys(options.comparisonKeys).forEach( embName => {
-
- /* switch to _id if needed */
- if(options.comparisonKeys[embName] == 'id') options.comparisonKeys[embName] = '_id'
-
- /* flag this update to preserve ids */
- if(options.comparisonKeys[embName] == '_id') {
- foundry.utils.mergeObject(options.updateOpts, {embedded: {[embName]: {keepId: true}}});
- }
- });
-
- }
-
- }
-
- static _mergeMutateDelta(actorDoc, delta, updates, options) {
-
- /* Grab the current stack (or make a new one) */
- let mutateStack = actorDoc.getFlag(MODULE.data.name, 'mutate') ?? [];
-
- /* create the information needed to revert this mutation and push
- * it onto the stack
- */
- const mutateInfo = Mutator._createMutateInfo( delta, options );
- mutateStack.push(mutateInfo);
-
- /* Create a new mutation stack flag data and store it in the update object */
- const flags = {warpgate: {mutate: mutateStack}};
- updates.actor = mergeObject(updates.actor ?? {}, {flags});
-
- return mutateInfo;
- }
-
- /* @return {Promise} */
- static async _update(tokenDoc, updates, options = {}) {
-
- /* update the token */
- await tokenDoc.update(updates.token ?? {}, options.updateOpts?.token ?? {});
-
- if(!options.noMoveWait && !!tokenDoc.object) {
- await CanvasAnimation.getAnimation(tokenDoc.object.animationName)?.promise
- }
-
- /* update the actor */
- return Mutator._updateActor(tokenDoc.actor, updates, options.comparisonKeys ?? {}, options.updateOpts ?? {});
- }
-
- /**
- * Will peel off the last applied mutation change from the provided token document
- *
- * @param {TokenDocument} tokenDoc Token document to revert the last applied mutation.
- * @param {String} [mutationName]. Specific mutation name to revert. optional.
- * @param {WorkflowOptions} [options]
- *
- * @return {Promise<MutationData|undefined>} The mutation data (updates) used for this
- * revert operation or `undefined` if none occured.
- */
- static async revertMutation(tokenDoc, mutationName = undefined, options = {}) {
-
- const mutateData = await Mutator._popMutation(tokenDoc?.actor, mutationName);
-
- if(!mutateData) {
- return;
- }
-
- if (tokenDoc.actor?.isOwner) {
- if(options.notice && tokenDoc.object) {
-
- const placement = {
- scene: tokenDoc.object.scene,
- ...tokenDoc.object.center,
- };
-
- warpgate.plugin.notice(placement, options.notice);
- }
-
- /* the provided options object will be mangled for our use -- copy it to
- * preserve the user's original input if requested (default).
- */
- if(!options.overrides?.preserveData) {
- options = MODULE.copy(options, 'error.badUpdate.complex');
- if(!options) return;
- options = foundry.utils.mergeObject(options, {overrides: {preserveData: true}}, {inplace: false});
- }
-
- /* perform the revert with the stored delta */
- MODULE.shimUpdate(mutateData.delta);
- mutateData.updateOpts ??= {};
- mutateData.overrides ??= {};
- foundry.utils.mergeObject(mutateData.updateOpts, options.updateOpts ?? {});
- foundry.utils.mergeObject(mutateData.overrides, options.overrides ?? {});
-
- await Mutator._update(tokenDoc, mutateData.delta, {
- overrides: mutateData.overrides,
- comparisonKeys: mutateData.comparisonKeys,
- updateOpts: mutateData.updateOpts
- });
-
- /* notify clients */
- warpgate.event.notify(warpgate.EVENT.REVERT, {
- uuid: tokenDoc.uuid,
- updates: (options.overrides?.includeRawData ?? false) ? mutateData : 'omitted',
- options});
-
- } else {
- return RemoteMutator.remoteRevert(tokenDoc, {mutationId: mutateData.name, options});
- }
-
- return mutateData;
- }
-
- static async _popMutation(actor, mutationName) {
-
- let mutateStack = actor?.getFlag(MODULE.data.name, 'mutate') ?? [];
-
- if (mutateStack.length == 0 || !actor){
- logger.debug(`Provided actor is undefined or has no mutation stack. Cannot pop.`);
- return undefined;
- }
-
- let mutateData = undefined;
-
- if (!!mutationName) {
- /* find specific mutation */
- const index = mutateStack.findIndex( mutation => mutation.name === mutationName );
-
- /* check for no result and log */
- if ( index < 0 ) {
- logger.debug(`Could not locate mutation named ${mutationName} in actor ${actor.name}`);
- return undefined;
- }
-
- /* otherwise, retrieve and remove */
- mutateData = mutateStack.splice(index, 1)[0];
-
- for( let i = index; i < mutateStack.length; i++){
-
- /* get the values stored in our delta and push any overlapping ones to
- * the mutation next in the stack
- */
- const stackUpdate = filterObject(mutateData.delta, mutateStack[i].delta);
- mergeObject(mutateStack[i].delta, stackUpdate);
-
- /* remove any changes that exist higher in the stack, we have
- * been overriden and should not restore these values
- */
- mutateData.delta = MODULE.unique(mutateData.delta, mutateStack[i].delta)
- }
-
- } else {
- /* pop the most recent mutation */
- mutateData = mutateStack.pop();
- }
-
- const newFlags = {[`${MODULE.data.name}.mutate`]: mutateStack};
-
- /* set the current mutation stack in the mutation data */
- foundry.utils.mergeObject(mutateData.delta, {actor: {flags: newFlags}});
-
- logger.debug(MODULE.localize('debug.finalRevertUpdate'), mutateData);
-
- return mutateData;
- }
-
- /* given a token document and the standard update object,
- * parse the changes that need to be applied to *reverse*
- * the mutate operation
- */
- static _createDelta(tokenDoc, updates, options) {
-
- /* get token changes */
- let tokenData = tokenDoc.toObject()
- tokenData.actorData = {};
-
- const tokenDelta = MODULE.strictUpdateDiff(updates.token ?? {}, tokenData);
-
- /* get the actor changes (no embeds) */
- const actorData = Mutator._getRootActorData(tokenDoc.actor);
- const actorDelta = MODULE.strictUpdateDiff(updates.actor ?? {}, actorData);
-
- /* get the changes from the embeds */
- let embeddedDelta = {};
- if(updates.embedded) {
-
- for( const embeddedName of Object.keys(updates.embedded) ) {
- const collection = tokenDoc.actor.getEmbeddedCollection(embeddedName);
- const invertedShorthand = Mutator._invertShorthand(collection, updates.embedded[embeddedName], getProperty(options.comparisonKeys, embeddedName) ?? 'name');
- embeddedDelta[embeddedName] = invertedShorthand;
- }
- }
-
- logger.debug(MODULE.localize('debug.tokenDelta'), tokenDelta, MODULE.localize('debug.actorDelta'), actorDelta, MODULE.localize('debug.embeddedDelta'), embeddedDelta);
-
- return {token: tokenDelta, actor: actorDelta, embedded: embeddedDelta}
- }
-
- /* returns the actor data sans ALL embedded collections */
- static _getRootActorData(actorDoc) {
- let actorData = actorDoc.toObject();
-
- /* get the key NAME of the embedded document type.
- * ex. not 'ActiveEffect' (the class name), 'effect' the collection's field name
- */
- let embeddedFields = Object.values(Actor.implementation.metadata.embedded);
- if(!MODULE.isV10) {
- embeddedFields = embeddedFields.map( thisClass => thisClass.metadata.collection );
- }
-
- /* delete any embedded fields from the actor data */
- embeddedFields.forEach( field => { delete actorData[field] } )
-
- return actorData;
- }
- }
|