diff --git a/module/documents/Actor.mjs b/module/documents/Actor.mjs index acbe5e0..9969f7a 100644 --- a/module/documents/Actor.mjs +++ b/module/documents/Actor.mjs @@ -1,7 +1,7 @@ import { __ID__ } from "../consts.mjs"; const { Actor } = foundry.documents; -const { hasProperty } = foundry.utils; +const { hasProperty, setProperty } = foundry.utils; export class TAFActor extends Actor { @@ -33,6 +33,12 @@ export class TAFActor extends Actor { super._onEmbeddedDocumentChange(...args); this.#sortedTypes = null; }; + + static migrateData(data, ...args) { + if (Object.keys(data.system?.attr ?? {}).length > 0) { + setProperty(data, `flags.${__ID__}.convertAttributesIntoItems`, true); + }; + }; // #endregion Lifecycle // #region Token Attributes diff --git a/module/hooks/ready.mjs b/module/hooks/ready.mjs index 94c7249..2a8bf39 100644 --- a/module/hooks/ready.mjs +++ b/module/hooks/ready.mjs @@ -1,6 +1,10 @@ +import { checkMigrations } from "../migrations/checkMigrations.mjs"; + Hooks.on(`ready`, () => { // Remove with issue: Foundry/taf#52 if (game.release.generation < 14 && globalThis._loc == null) { globalThis._loc = game.i18n.format.bind(game.i18n); }; + + checkMigrations(); }); diff --git a/module/migrations/checkMigrations.mjs b/module/migrations/checkMigrations.mjs new file mode 100644 index 0000000..776b25c --- /dev/null +++ b/module/migrations/checkMigrations.mjs @@ -0,0 +1,24 @@ +import { __ID__ } from "../consts.mjs"; +import { Logger } from "../utils/Logger.mjs"; +import { migrateTo3_0_0 } from "./v3.0.0.mjs"; + +const { isNewerVersion } = foundry.utils; + +export async function checkMigrations() { + if (!game.user.isActiveGM) { + Logger.debug(`User not active GM, skipping data migrations`); + return; + }; + + const migrationVersion = game.settings.get(__ID__, `migrationVersion`); + let updateVersion = !migrationVersion; + + if (isNewerVersion("3.0.0", migrationVersion)) { + await migrateTo3_0_0(); + updateVersion = true; + }; + + if (updateVersion) { + game.settings.set(__ID__, `migrationVersion`, game.system.version); + }; +}; diff --git a/module/migrations/utils.mjs b/module/migrations/utils.mjs new file mode 100644 index 0000000..807e69a --- /dev/null +++ b/module/migrations/utils.mjs @@ -0,0 +1,79 @@ +import { __ID__ } from "../consts.mjs"; + +/** + * Migrate the documents within a collection based on what + * + * This function was originally reproduced from [Draw Steel's codebase](https://github.com/MetaMorphic-Digital/draw-steel/blob/82a0a050da7c0d6d28c0cd283cf3b6915f47ee2a/src/module/data/migrations.mjs#L206-L226), + * with modifications to it to work better without + * + * @param collection The Collection of documents to update. + * @param flag The flag name to reference for if the document should be migrated. + * @param convertor The function that takes the document and performs the. + * transformations to get the required update data. + * @param options Options to configure how the method behaves. + * @param options.pack The compendium pack to update. + * @param options.parent Parent of the collection for embedded collections. + * @param options.update Whether or not this method should perform the update, or pass back the array of DB operations. + * @returns An array of batch operations to perform. + */ +export async function migrateCollection( + collection, + flag, + convertor, + options = {} +) { + const toMigrate = collection + .filter(doc => { + console.log(`toMigrate.filter doc`, doc); + return doc.getFlag(__ID__, flag) + }) + .map(doc => { + const update = convertor(doc) ?? {}; + update[`_id`] = doc._id; + + // v13/v14+ compatibility shim + if (game.release.generation > 13) { + update[`flags.${__ID__}.${flag}`] = _del; + } else { + update[`flags.${__ID__}.-=${flag}`] = null; + }; + + return update; + }) + .filter(data => !!data); + // update in increments of 100 + // TODO: optionally return an array of DB operations for modifyBatch + const batches = Math.ceil(toMigrate.length / 100); + for (let i = 0; i < batches; i++) { + const updateData = toMigrate.slice(i * 100, (i + 1) * 100); + await collection.documentClass.updateDocuments( + updateData, + { + pack: options.pack, + parent: options.parent, + diff: false, + }, + ); + }; +}; + +/** + * Determine whether a compendium pack should be migrated during `migrateWorld`. + * + * This function was reproduced from [Draw Steel's codebase](https://github.com/MetaMorphic-Digital/draw-steel/blob/82a0a050da7c0d6d28c0cd283cf3b6915f47ee2a/src/module/data/migrations.mjs#L287-L302) + * + * @param pack The CompendiumPack document + * @returns {boolean} Whether or not the pack should be migrated + */ +export function shouldMigrateCompendium(pack) { + // We only care about actor and item migrations + if (!["Actor", "Item"].includes(pack.documentName)) return false; + + // World compendiums should all be migrated, system ones should never by migrated + if (pack.metadata.packageType === "world") return true; + if (pack.metadata.packageType === "system") return false; + + // Module compendiums should only be migrated if they don't have a download or manifest URL + const module = game.modules.get(pack.metadata.packageName); + return !module.download && !module.manifest; +} diff --git a/module/migrations/v3.0.0.mjs b/module/migrations/v3.0.0.mjs new file mode 100644 index 0000000..e1d3087 --- /dev/null +++ b/module/migrations/v3.0.0.mjs @@ -0,0 +1,72 @@ +import { Logger } from "../utils/Logger.mjs"; +import { migrateCollection, shouldMigrateCompendium } from "./utils.mjs"; + +const flag = `convertAttributesIntoItems`; +const operations = []; + +export async function migrateTo3_0_0() { + Logger.debug(`Starting v3.0.0 data migration`); + + operations.push( + ...await migrateCollection( + game.actors, + flag, + handleMigratingActor, + { update: false, }, + ), + ); + + // for (const pack of game.packs) { + // if ( + // pack.metadata.type !== "Actor" + // || !shouldMigrateCompendium(pack) + // ) { + // continue; + // }; + + // await pack.getDocuments(); + + // // TODO: unlock compendium if required then re-lock after finishing + // await migrateCollection( + // pack, + // flag, + // handleMigratingActor, + // { pack }, + // ); + // }; + + // TODO: create the item documents (batch them if possible) + Logger.debug(`Finished v3.0.0 migration, resulting operations:`); + console.log(operations); +}; + +function handleMigratingActor(actor) { + console.log(actor); + + const operation = { + action: `create`, + documentName: `Item`, + parent: actor, + data: [], + }; + + const attrs = actor.system.attr; + for (const [ key, attr ] of Object.entries(attrs)) { + operation.data.push(convertToItem(key, attr)); + }; + operations.push(operation); + + return null; +}; + +function convertToItem(key, attr) { + return { + name: attr.name, + type: "attribute", + system: { + key, + value: attr.value, + max: attr.isRange ? attr.max : null, + }, + }; +}; diff --git a/module/settings/world.mjs b/module/settings/world.mjs index bb0cd06..a8dd616 100644 --- a/module/settings/world.mjs +++ b/module/settings/world.mjs @@ -74,4 +74,10 @@ export function registerWorldSettings() { type: Object, scope: `world`, }); + + game.settings.register(__ID__, `migrationVersion`, { + config: false, + type: String, + scope: `world`, + }); };