From ca9baaa199390901cb064ec8fcc494e4bbaf1318 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Wed, 27 May 2026 19:52:04 -0400 Subject: [PATCH] Preload class and subclass features for description --- module/data/item/class.mjs | 13 ++++++++---- module/data/item/subclass.mjs | 8 +++++-- module/helpers/utils.mjs | 40 +++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index 7014e011..470a1e3c 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs'; -import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; +import { addLinkedItemsDiff, fromUuids, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; export default class DHClass extends BaseDataItem { /** @inheritDoc */ @@ -73,15 +73,16 @@ export default class DHClass extends BaseDataItem { const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u); const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass)); for (const pack of game.packs) { + const packIds = []; const indexes = await pack.getIndex({ fields: ['system.linkedClass'] }); for (const index of indexes) { if (index.type !== 'subclass') continue; if (!uuids.includes(index.system?.linkedClass)) continue; if (subclasses.find(x => x.uuid === index.uuid)) continue; - - const subclass = await foundry.utils.fromUuid(index.uuid); - subclasses.push(subclass); + packIds.push(index._id); } + + if (packIds.length > 0) subclasses.push(...(await pack.getDocuments({ _id__in: packIds }))); } return subclasses; @@ -216,6 +217,10 @@ export default class DHClass extends BaseDataItem { classItems.push(contentLink.outerHTML); } + // Preload all class features for acquisition from the cache + // todo: make feature acquisition async and replace feature helpers for methods + await fromUuids(this._source.features.map(f => f.item)); + const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures); const classFeatures = await getFeaturesHTMLData(this.classFeatures); diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index ecf72de3..55b078c2 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -1,5 +1,4 @@ -import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; -import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; +import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs'; import BaseDataItem from './base.mjs'; @@ -91,6 +90,11 @@ export default class DHSubclass extends BaseDataItem { const spellcastTrait = this.spellcastingTrait ? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label) : null; + + // Preload all class features for acquisition from the cache + // todo: make feature acquisition async and replace feature helpers for methods + await fromUuids(this._source.features.map(f => f.item)); + const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures); const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures); const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 7bc5fa25..c9ee772e 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -864,3 +864,43 @@ export function camelize(str) { }) .replace(/\s+/g, ''); } + +/** Bulk load a list of documents using uuids. Returns the documents in the same order */ +export async function fromUuids(uuids) { + // Set up base entries. Each step works on a sublist of these objects + const entries = uuids.map(uuid => ({ + uuid, + parsed: foundry.utils.parseUuid(uuid), + value: foundry.utils.fromUuidSync(uuid) + })); + + // Handle missing uuids for embedded documents first + // A value may be index data, so we check if its a document + const packEmbeddedEntries = entries.filter( + e => + !(e.value instanceof Document) && + e.parsed.collection instanceof foundry.documents.collections.CompendiumCollection && + e.parsed.embedded.length > 0 + ); + await Promise.all( + packEmbeddedEntries.map(async e => { + e.value = await fromUuid(e.uuid); + return true; + }) + ); + + // Handle missing top level pack stuff, by batching per pack + const missingTopLevel = entries.filter(e => !(e.value instanceof Document) && e.value?.pack); + for (const packGroup of Object.values(Object.groupBy(missingTopLevel, e => e.value.pack))) { + const pack = game.packs.get(packGroup[0].value.pack); + if (!pack) continue; + + const ids = packGroup.map(p => p.parsed.id); + const documents = await pack.getDocuments({ _id__in: ids }); + for (const p of packGroup) { + p.value = documents.find(d => d.id === p.parsed.id) ?? p.value; + } + } + + return entries.map(e => e.value); +}