From fec1a6c1ef2f0209a6e7f554773a59dc5dcb8f3f Mon Sep 17 00:00:00 2001 From: cosmo Date: Sun, 26 Apr 2026 18:29:09 +0200 Subject: [PATCH] extract ikonis feature injection into a reusable helper with recursion guard --- scripts/ikonis-data.js | 111 +++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/scripts/ikonis-data.js b/scripts/ikonis-data.js index bdce63a..a76ee4f 100644 --- a/scripts/ikonis-data.js +++ b/scripts/ikonis-data.js @@ -127,6 +127,59 @@ export function patchIkonisLogic() { }; } +// Global recursion guard for feature injection +let _isInjecting = false; + +/** + * Internal helper to generate and cache virtual features for an actor. + * Safe to call multiple times; uses a cache to keep it fast. + */ +function _injectIkonisFeatures(actor) { + if (!actor || _isInjecting) return []; + + // Use a short-lived cache for the current render cycle + if (!actor._ikonisCache) actor._ikonisCache = new Map(); + + const ikonisFeatures = []; + _isInjecting = true; + + try { + const weapons = actor.items.filter(i => i.type === 'weapon'); + for (const item of weapons) { + const isEquipped = item.system.equipped; + const installedIds = item.getFlag('dh-ikonis', 'installedAugments') || []; + const bondedUuid = item.getFlag('dh-ikonis', 'bondedFeatureUuid'); + + const processFeature = (uuid, type) => { + const feature = _featureCache.get(uuid) || fromUuidSync(uuid); + if (feature) { + const featureClone = feature.clone({ parent: actor }, { keepId: true }); + featureClone.system.type = "ikonis"; + + const virtualId = `ikonis-${item.id}-${feature.id}`; + Object.defineProperty(featureClone, "id", { value: virtualId, enumerable: true }); + + actor._ikonisCache.set(virtualId, featureClone); + if (isEquipped) ikonisFeatures.push(featureClone); + } + }; + + if (bondedUuid) processFeature(bondedUuid, "bonded"); + const allAugs = getAugments(); + for (const id of installedIds) { + const aug = allAugs.find(a => String(a.id) === String(id)); + if (aug?.featureUuid) processFeature(aug.featureUuid, "augment"); + } + } + } catch (err) { + console.error("DH-Ikonis | Error during feature injection:", err); + } finally { + _isInjecting = false; + } + + return Array.from(new Set(ikonisFeatures)); +} + /** * Patches the Character Data Model to show features in the UI lists. */ @@ -143,58 +196,16 @@ export function patchDhCharacter(DhCharacter) { Object.defineProperty(DhCharacter.prototype, 'sheetLists', { get: function() { const lists = originalSheetLists.call(this); - const ikonisFeatures = []; - if (!this.parent) return lists; - const weapons = this.parent.items.filter(i => i.type === 'weapon'); - if (weapons.length > 0) { - console.log(`DH-Ikonis | Checking ${weapons.length} weapons on ${this.parent.name} for augments...`); - } - - for (const item of weapons) { - const isEquipped = item.system.equipped; - const installedIds = item.getFlag('dh-ikonis', 'installedAugments') || []; - const bondedUuid = item.getFlag('dh-ikonis', 'bondedFeatureUuid'); - - const processFeature = (uuid, type) => { - const feature = _featureCache.get(uuid) || fromUuidSync(uuid); - if (feature) { - // Use clone() to preserve internal action state - const featureClone = feature.clone({ parent: this.parent }, { keepId: true }); - - // Set system type to 'ikonis' so the original sheetLists ignores it - featureClone.system.type = "ikonis"; - - // Use unique ID per weapon - const virtualId = `ikonis-${item.id}-${feature.id}`; - Object.defineProperty(featureClone, "id", { value: virtualId, enumerable: true }); - - // Store in a local cache on the actor for fast lookup by our patch - if (!this.parent._ikonisCache) this.parent._ikonisCache = new Map(); - this.parent._ikonisCache.set(virtualId, featureClone); - - if (isEquipped) { - ikonisFeatures.push(featureClone); - } - } - }; - - if (bondedUuid) processFeature(bondedUuid, "bonded"); - const allAugs = getAugments(); - for (const id of installedIds) { - const aug = allAugs.find(a => String(a.id) === String(id)); - if (aug?.featureUuid) processFeature(aug.featureUuid, "augment"); - } - } - + const ikonisFeatures = _injectIkonisFeatures(this.parent); + if (ikonisFeatures.length > 0) { - const uniqueFeatures = Array.from(new Set(ikonisFeatures)); if (!lists.features) lists.features = {}; lists.features["Ikonis Augments"] = { title: "Ikonis Augments", type: "ikonis", - values: uniqueFeatures + values: ikonisFeatures }; } @@ -204,17 +215,11 @@ export function patchDhCharacter(DhCharacter) { }); // Patch getEmbeddedDocument to resolve our virtual features - // This is the most compatible way to make fromUuid work without patching it directly const originalGetEmbedded = Actor.prototype.getEmbeddedDocument; Actor.prototype.getEmbeddedDocument = function(embeddedName, id, options) { if (embeddedName === "Item" && typeof id === "string" && id.startsWith("ikonis-")) { - // First check the cache we populated during sheetLists - if (this._ikonisCache?.has(id)) return this._ikonisCache.get(id); - - // If not in cache, trigger the getter to populate it - // Accessing system.sheetLists will run our patch above - const lists = this.system.sheetLists; - if (this._ikonisCache?.has(id)) return this._ikonisCache.get(id); + if (!this._ikonisCache?.has(id)) _injectIkonisFeatures(this); + return this._ikonisCache?.get(id); } return originalGetEmbedded.call(this, embeddedName, id, options); };