import { BASE_IMAGE_CATEGORIES, userRequiresImageCache, waitForTokenTexture } from './utils.js'; import { ForgeSearchPaths } from '../applications/forgeSearchPaths.js'; import TokenHUDClientSettings from '../applications/tokenHUDClientSettings.js'; import CompendiumMapConfig from '../applications/compendiumMap.js'; import ImportExport from '../applications/importExport.js'; import ConfigureSettings from '../applications/configureSettings.js'; import { cacheImages, saveCache } from './search.js'; import { registerAllHooks } from './hooks/hooks.js'; import { registerAllWrappers } from './wrappers/wrappers.js'; export const TVA_CONFIG = { debug: false, disableNotifs: false, searchPaths: [ { text: 'modules/caeora-maps-tokens-assets/assets/tokens', cache: true, source: typeof ForgeAPI === 'undefined' ? 'data' : 'forge-bazaar', types: ['Portrait', 'Token', 'PortraitAndToken'], }, ], forgeSearchPaths: {}, worldHud: { displayOnlySharedImages: false, disableIfTHWEnabled: false, includeKeywords: false, updateActorImage: false, useNameSimilarity: false, includeWildcard: true, showFullPath: false, animate: true, }, hud: { enableSideMenu: true, displayAsImage: true, imageOpacity: 50, }, keywordSearch: true, excludedKeywords: 'and,for', runSearchOnPath: false, searchFilters: {}, algorithm: { exact: false, fuzzy: true, fuzzyLimit: 100, fuzzyThreshold: 0.3, fuzzyArtSelectPercentSlider: true, }, tokenConfigs: [], randomizer: { actorCreate: false, tokenCreate: false, tokenCopyPaste: false, tokenName: true, keywords: false, shared: false, wildcard: false, representedActorDisable: false, linkedActorDisable: true, popupOnDisable: false, diffImages: false, syncImages: false, }, popup: { disableAutoPopupOnActorCreate: true, disableAutoPopupOnTokenCreate: true, disableAutoPopupOnTokenCopyPaste: true, twoPopups: false, twoPopupsNoDialog: false, }, imgurClientId: '', stackStatusConfig: true, mergeGroup: false, staticCache: false, staticCacheFile: 'modules/token-variants/token-variants-cache.json', tilesEnabled: true, compendiumMapper: { missingOnly: false, diffImages: false, showImages: true, cache: false, autoDisplayArtSelect: true, syncImages: false, overrideCategory: false, category: 'Token', missingImages: [{ document: 'all', image: CONST.DEFAULT_TOKEN }], searchOptions: {}, }, permissions: { popups: { 1: false, 2: false, 3: true, 4: true, }, portrait_right_click: { 1: false, 2: false, 3: true, 4: true, }, image_path_button: { 1: false, 2: false, 3: true, 4: true, }, hud: { 1: true, 2: true, 3: true, 4: true, }, hudFullAccess: { 1: false, 2: false, 3: true, 4: true, }, statusConfig: { 1: false, 2: false, 3: true, 4: true, }, }, globalMappings: [], templateMappings: [], customImageCategories: [], displayEffectIconsOnHover: false, disableEffectIcons: false, filterEffectIcons: false, filterCustomEffectIcons: true, filterIconList: [], updateTokenProto: false, imgNameContainsDimensions: false, imgNameContainsFADimensions: false, playVideoOnHover: true, pauseVideoOnHoverOut: false, disableImageChangeOnPolymorphed: false, disableImageUpdateOnNonPrototype: false, disableTokenUpdateAnimation: false, mappingsCurrentSceneOnly: false, invisibleImage: '', systemHpPath: '', internalEffects: { hpChange: { enabled: false, duration: null }, }, hideElevationTooltip: false, hideTokenBorder: false, }; export const FEATURE_CONTROL = { EffectMappings: true, EffectIcons: true, Overlays: true, UserMappings: true, Wildcards: true, PopUpAndRandomize: true, HUD: true, HideElement: true, }; export function registerSettings() { game.settings.register('token-variants', 'featureControl', { scope: 'world', config: false, type: Object, default: FEATURE_CONTROL, onChange: async (val) => { mergeObject(FEATURE_CONTROL, val); registerAllHooks(); registerAllWrappers(); }, }); mergeObject(FEATURE_CONTROL, game.settings.get('token-variants', 'featureControl')); game.settings.registerMenu('token-variants', 'settings', { name: 'Configure Settings', hint: 'Configure Token Variant Art settings', label: 'Settings', scope: 'world', icon: 'fas fa-cog', type: ConfigureSettings, restricted: true, }); const systemHpPaths = { 'cyberpunk-red-core': 'derivedStats.hp', lfg: 'health', worldbuilding: 'health', twodsix: 'hits', }; TVA_CONFIG.systemHpPath = systemHpPaths[game.system.id] ?? 'attributes.hp'; game.settings.register('token-variants', 'effectMappingToggleGroups', { scope: 'world', config: false, type: Object, default: { Default: true }, }); game.settings.register('token-variants', 'settings', { scope: 'world', config: false, type: Object, default: TVA_CONFIG, onChange: async (val) => { // Generate a diff, it will be required when doing post-processing of the modified settings const diff = _arrayAwareDiffObject(TVA_CONFIG, val); // Check image re-cache required due to permission changes let requiresImageCache = false; if ('permissions' in diff) { if ( !userRequiresImageCache(TVA_CONFIG.permissions) && userRequiresImageCache(val.permissions) ) requiresImageCache = true; } // Update live settings mergeObject(TVA_CONFIG, val); if ( TVA_CONFIG.filterEffectIcons && ('filterCustomEffectIcons' in diff || 'filterIconList' in diff) ) { for (const tkn of canvas.tokens.placeables) { waitForTokenTexture(tkn, (token) => { token.drawEffects(); }); } } // Check image re-cache required due to search path changes if ('searchPaths' in diff || 'forgeSearchPaths' in diff) { if (userRequiresImageCache(TVA_CONFIG.permissions)) requiresImageCache = true; } // Cache/re-cache images if necessary if (requiresImageCache) { await cacheImages(); } if (diff.staticCache) { const cacheFile = diff.staticCacheFile ? diff.staticCacheFile : TVA_CONFIG.staticCacheFile; saveCache(cacheFile); } TVA_CONFIG.hud = game.settings.get('token-variants', 'hudSettings'); registerAllHooks(); registerAllWrappers(); if ('displayEffectIconsOnHover' in diff) { for (const tkn of canvas.tokens.placeables) { if (tkn.effects) tkn.effects.visible = !diff.displayEffectIconsOnHover; } } if ('hideElevationTooltip' in diff) { for (const tkn of canvas.tokens.placeables) { if (tkn.tooltip) tkn.tooltip.text = tkn._getTooltipText(); } } if ('hideTokenBorder' in diff) { for (const tkn of canvas.tokens.placeables) { if (tkn.border) tkn.border.visible = !diff.hideTokenBorder; } } if ('filterEffectIcons' in diff || 'disableEffectIcons' in diff) { for (const tkn of canvas.tokens.placeables) { tkn.drawEffects(); } } }, }); game.settings.register('token-variants', 'debug', { scope: 'world', config: false, type: Boolean, default: TVA_CONFIG.debug, onChange: (val) => (TVA_CONFIG.debug = val), }); if (typeof ForgeAPI !== 'undefined') { game.settings.registerMenu('token-variants', 'forgeSearchPaths', { name: game.i18n.localize('token-variants.settings.forge-search-paths.Name'), hint: game.i18n.localize('token-variants.settings.forge-search-paths.Hint'), icon: 'fas fa-search', type: ForgeSearchPaths, scope: 'client', restricted: false, }); } game.settings.register('token-variants', 'tokenConfigs', { scope: 'world', config: false, type: Array, default: TVA_CONFIG.tokenConfigs, onChange: (val) => (TVA_CONFIG.tokenConfigs = val), }); game.settings.registerMenu('token-variants', 'tokenHUDSettings', { name: game.i18n.localize('token-variants.settings.token-hud.Name'), hint: game.i18n.localize('token-variants.settings.token-hud.Hint'), scope: 'client', icon: 'fas fa-images', type: TokenHUDClientSettings, restricted: false, }); game.settings.registerMenu('token-variants', 'compendiumMapper', { name: game.i18n.localize('token-variants.settings.compendium-mapper.Name'), hint: game.i18n.localize('token-variants.settings.compendium-mapper.Hint'), scope: 'world', icon: 'fas fa-cogs', type: CompendiumMapConfig, restricted: true, }); game.settings.register('token-variants', 'compendiumMapper', { scope: 'world', config: false, type: Object, default: TVA_CONFIG.compendiumMapper, onChange: (val) => (TVA_CONFIG.compendiumMapper = val), }); game.settings.register('token-variants', 'hudSettings', { scope: 'client', config: false, type: Object, default: TVA_CONFIG.hud, onChange: (val) => (TVA_CONFIG.hud = val), }); game.settings.registerMenu('token-variants', 'importExport', { name: `Import/Export`, hint: game.i18n.localize('token-variants.settings.import-export.Hint'), scope: 'world', icon: 'fas fa-toolbox', type: ImportExport, restricted: true, }); // Read settings const settings = game.settings.get('token-variants', 'settings'); mergeObject(TVA_CONFIG, settings); if (isEmpty(TVA_CONFIG.searchFilters)) { BASE_IMAGE_CATEGORIES.forEach((cat) => { TVA_CONFIG.searchFilters[cat] = { include: '', exclude: '', regex: '', }; }); } for (let uid in TVA_CONFIG.forgeSearchPaths) { TVA_CONFIG.forgeSearchPaths[uid].paths = TVA_CONFIG.forgeSearchPaths[uid].paths.map((p) => { if (!p.source) { p.source = 'forgevtt'; } if (!p.types) { if (p.tiles) p.types = ['Tile']; else p.types = ['Portrait', 'Token', 'PortraitAndToken']; } return p; }); } // 20/07/2023 Convert globalMappings to a new format if (getType(settings.globalMappings) === 'Object') { Hooks.once('ready', () => { TVA_CONFIG.globalMappings = migrateMappings(settings.globalMappings); setTimeout(() => updateSettings({ globalMappings: TVA_CONFIG.globalMappings }), 10000); }); } // Read client settings TVA_CONFIG.hud = game.settings.get('token-variants', 'hudSettings'); } export function migrateMappings(mappings, globalMappings = []) { if (!mappings) return []; if (getType(mappings) === 'Object') { let nMappings = []; for (const [effect, mapping] of Object.entries(mappings)) { if (!mapping.label) mapping.label = effect.replaceAll('¶', '.'); if (!mapping.expression) mapping.expression = effect.replaceAll('¶', '.'); if (!mapping.id) mapping.id = randomID(8); delete mapping.effect; if (mapping.overlayConfig) mapping.overlayConfig.id = mapping.id; delete mapping.overlayConfig?.effect; nMappings.push(mapping); } // Convert parents to parentIDs let combMappings = nMappings.concat(globalMappings); for (const mapping of nMappings) { if (mapping.overlayConfig?.parent) { if (mapping.overlayConfig.parent === 'Token (Placeable)') { mapping.overlayConfig.parentID = 'TOKEN'; } else { const parent = combMappings.find((m) => m.label === mapping.overlayConfig.parent); if (parent) mapping.overlayConfig.parentID = parent.id; else mapping.overlayConfig.parentID = ''; } delete mapping.overlayConfig.parent; } } return nMappings; } return mappings; } export function getFlagMappings(object) { if (!object) return []; let doc = object.document ?? object; const actorId = doc.actor?.id; if (actorId) { doc = game.actors.get(actorId); if (!doc) return []; } // 23/07/2023 let mappings = doc.getFlag('token-variants', 'effectMappings') ?? []; if (getType(mappings) === 'Object') { mappings = migrateMappings(mappings, TVA_CONFIG.globalMappings); doc.setFlag('token-variants', 'effectMappings', mappings); } return mappings; } export function exportSettingsToJSON() { const settings = deepClone(TVA_CONFIG); const filename = `token-variants-settings.json`; saveDataToFile(JSON.stringify(settings, null, 2), 'text/json', filename); } export async function importSettingsFromJSON(json) { if (typeof json === 'string') json = JSON.parse(json); if (json.forgeSearchPaths) for (let uid in json.forgeSearchPaths) { json.forgeSearchPaths[uid].paths = json.forgeSearchPaths[uid].paths.map((p) => { if (!p.source) { p.source = 'forgevtt'; } if (!p.types) { if (p.tiles) p.types = ['Tile']; else p.types = ['Portrait', 'Token', 'PortraitAndToken']; } return p; }); } // 09/07/2022 Convert filters to new format if old one is still in use if (json.searchFilters && json.searchFilters.portraitFilterInclude != null) { const filters = json.searchFilters; json.searchFilters = { Portrait: { include: filters.portraitFilterInclude ?? '', exclude: filters.portraitFilterExclude ?? '', regex: filters.portraitFilterRegex ?? '', }, Token: { include: filters.tokenFilterInclude ?? '', exclude: filters.tokenFilterExclude ?? '', regex: filters.tokenFilterRegex ?? '', }, PortraitAndToken: { include: filters.generalFilterInclude ?? '', exclude: filters.generalFilterExclude ?? '', regex: filters.generalFilterRegex ?? '', }, }; if (json.compendiumMapper) delete json.compendiumMapper.searchFilters; } // Global Mappings need special merge if (json.globalMappings) { const nMappings = migrateMappings(json.globalMappings); for (const m of nMappings) { const i = TVA_CONFIG.globalMappings.findIndex((mapping) => m.label === mapping.label); if (i === -1) TVA_CONFIG.globalMappings.push(m); else TVA_CONFIG.globalMappings[i] = m; } json.globalMappings = TVA_CONFIG.globalMappings; } updateSettings(json); } function _refreshFilters(filters, customCategories, updateTVAConfig = false) { const categories = BASE_IMAGE_CATEGORIES.concat( customCategories ?? TVA_CONFIG.customImageCategories ); for (const filter in filters) { if (!categories.includes(filter)) { delete filters[filter]; if (updateTVAConfig) delete TVA_CONFIG.searchFilters[filter]; } } for (const category of customCategories) { if (filters[category] == null) { filters[category] = { include: '', exclude: '', regex: '', }; } } } export async function updateSettings(newSettings) { const settings = mergeObject(deepClone(TVA_CONFIG), newSettings, { insertKeys: false }); // Custom image categories might have changed, meaning we may have filters that are no longer relevant // or need to be added if ('customImageCategories' in newSettings) { _refreshFilters(settings.searchFilters, newSettings.customImageCategories, true); if (settings.compendiumMapper?.searchOptions?.searchFilters != null) { _refreshFilters( settings.compendiumMapper.searchOptions.searchFilters, newSettings.customImageCategories ); TVA_CONFIG.compendiumMapper.searchOptions.searchFilters = settings.compendiumMapper.searchOptions.searchFilters; } } await game.settings.set('token-variants', 'settings', settings); } export function _arrayAwareDiffObject(original, other, { inner = false } = {}) { function _difference(v0, v1) { let t0 = getType(v0); let t1 = getType(v1); if (t0 !== t1) return [true, v1]; if (t0 === 'Array') return [!_arrayEquality(v0, v1), v1]; if (t0 === 'Object') { if (isEmpty(v0) !== isEmpty(v1)) return [true, v1]; let d = _arrayAwareDiffObject(v0, v1, { inner }); return [!isEmpty(d), d]; } return [v0 !== v1, v1]; } // Recursively call the _difference function return Object.keys(other).reduce((obj, key) => { if (inner && !(key in original)) return obj; let [isDifferent, difference] = _difference(original[key], other[key]); if (isDifferent) obj[key] = difference; return obj; }, {}); } function _arrayEquality(a1, a2) { if (!(a2 instanceof Array) || a2.length !== a1.length) return false; return a1.every((v, i) => { if (getType(v) === 'Object') return Object.keys(_arrayAwareDiffObject(v, a2[i])).length === 0; return a2[i] === v; }); } export function getSearchOptions() { return { keywordSearch: TVA_CONFIG.keywordSearch, excludedKeywords: TVA_CONFIG.excludedKeywords, runSearchOnPath: TVA_CONFIG.runSearchOnPath, algorithm: TVA_CONFIG.algorithm, searchFilters: TVA_CONFIG.searchFilters, }; }