/* * 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 {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} */ /** * 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} */ 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} 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} 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>} 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} 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>} 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} 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; } }