All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

569 lines
17 KiB

  1. import { BASE_IMAGE_CATEGORIES, userRequiresImageCache, waitForTokenTexture } from './utils.js';
  2. import { ForgeSearchPaths } from '../applications/forgeSearchPaths.js';
  3. import TokenHUDClientSettings from '../applications/tokenHUDClientSettings.js';
  4. import CompendiumMapConfig from '../applications/compendiumMap.js';
  5. import ImportExport from '../applications/importExport.js';
  6. import ConfigureSettings from '../applications/configureSettings.js';
  7. import { cacheImages, saveCache } from './search.js';
  8. import { registerAllHooks } from './hooks/hooks.js';
  9. import { registerAllWrappers } from './wrappers/wrappers.js';
  10. export const TVA_CONFIG = {
  11. debug: false,
  12. disableNotifs: false,
  13. searchPaths: [
  14. {
  15. text: 'modules/caeora-maps-tokens-assets/assets/tokens',
  16. cache: true,
  17. source: typeof ForgeAPI === 'undefined' ? 'data' : 'forge-bazaar',
  18. types: ['Portrait', 'Token', 'PortraitAndToken'],
  19. },
  20. ],
  21. forgeSearchPaths: {},
  22. worldHud: {
  23. displayOnlySharedImages: false,
  24. disableIfTHWEnabled: false,
  25. includeKeywords: false,
  26. updateActorImage: false,
  27. useNameSimilarity: false,
  28. includeWildcard: true,
  29. showFullPath: false,
  30. animate: true,
  31. },
  32. hud: {
  33. enableSideMenu: true,
  34. displayAsImage: true,
  35. imageOpacity: 50,
  36. },
  37. keywordSearch: true,
  38. excludedKeywords: 'and,for',
  39. runSearchOnPath: false,
  40. searchFilters: {},
  41. algorithm: {
  42. exact: false,
  43. fuzzy: true,
  44. fuzzyLimit: 100,
  45. fuzzyThreshold: 0.3,
  46. fuzzyArtSelectPercentSlider: true,
  47. },
  48. tokenConfigs: [],
  49. randomizer: {
  50. actorCreate: false,
  51. tokenCreate: false,
  52. tokenCopyPaste: false,
  53. tokenName: true,
  54. keywords: false,
  55. shared: false,
  56. wildcard: false,
  57. representedActorDisable: false,
  58. linkedActorDisable: true,
  59. popupOnDisable: false,
  60. diffImages: false,
  61. syncImages: false,
  62. nonRepeat: false,
  63. },
  64. popup: {
  65. disableAutoPopupOnActorCreate: true,
  66. disableAutoPopupOnTokenCreate: true,
  67. disableAutoPopupOnTokenCopyPaste: true,
  68. twoPopups: false,
  69. twoPopupsNoDialog: false,
  70. },
  71. imgurClientId: '',
  72. stackStatusConfig: true,
  73. mergeGroup: false,
  74. staticCache: false,
  75. staticCacheFile: 'modules/token-variants/token-variants-cache.json',
  76. tilesEnabled: true,
  77. compendiumMapper: {
  78. missingOnly: false,
  79. diffImages: false,
  80. showImages: true,
  81. cache: false,
  82. autoDisplayArtSelect: true,
  83. syncImages: false,
  84. overrideCategory: false,
  85. category: 'Token',
  86. missingImages: [{ document: 'all', image: CONST.DEFAULT_TOKEN }],
  87. searchOptions: {},
  88. },
  89. permissions: {
  90. popups: {
  91. 1: false,
  92. 2: false,
  93. 3: true,
  94. 4: true,
  95. },
  96. portrait_right_click: {
  97. 1: false,
  98. 2: false,
  99. 3: true,
  100. 4: true,
  101. },
  102. image_path_button: {
  103. 1: false,
  104. 2: false,
  105. 3: true,
  106. 4: true,
  107. },
  108. hud: {
  109. 1: true,
  110. 2: true,
  111. 3: true,
  112. 4: true,
  113. },
  114. hudFullAccess: {
  115. 1: false,
  116. 2: false,
  117. 3: true,
  118. 4: true,
  119. },
  120. statusConfig: {
  121. 1: false,
  122. 2: false,
  123. 3: true,
  124. 4: true,
  125. },
  126. },
  127. globalMappings: [],
  128. templateMappings: [],
  129. customImageCategories: [],
  130. displayEffectIconsOnHover: false,
  131. disableEffectIcons: false,
  132. filterEffectIcons: false,
  133. filterCustomEffectIcons: true,
  134. filterIconList: [],
  135. updateTokenProto: false,
  136. imgNameContainsDimensions: false,
  137. imgNameContainsFADimensions: false,
  138. playVideoOnHover: true,
  139. pauseVideoOnHoverOut: false,
  140. disableImageChangeOnPolymorphed: false,
  141. disableImageUpdateOnNonPrototype: false,
  142. disableTokenUpdateAnimation: false,
  143. mappingsCurrentSceneOnly: false,
  144. evaluateOverlayOnHover: true,
  145. invisibleImage: '',
  146. systemHpPath: '',
  147. internalEffects: {
  148. hpChange: { enabled: false, duration: null },
  149. },
  150. hideElevationTooltip: false,
  151. hideTokenBorder: false,
  152. };
  153. export const FEATURE_CONTROL = {
  154. EffectMappings: true,
  155. EffectIcons: true,
  156. Overlays: true,
  157. UserMappings: true,
  158. Wildcards: true,
  159. PopUpAndRandomize: true,
  160. HUD: true,
  161. HideElement: true,
  162. };
  163. export function registerSettings() {
  164. game.settings.register('token-variants', 'featureControl', {
  165. scope: 'world',
  166. config: false,
  167. type: Object,
  168. default: FEATURE_CONTROL,
  169. onChange: async (val) => {
  170. mergeObject(FEATURE_CONTROL, val);
  171. registerAllHooks();
  172. registerAllWrappers();
  173. },
  174. });
  175. mergeObject(FEATURE_CONTROL, game.settings.get('token-variants', 'featureControl'));
  176. game.settings.registerMenu('token-variants', 'settings', {
  177. name: 'Configure Settings',
  178. hint: 'Configure Token Variant Art settings',
  179. label: 'Settings',
  180. scope: 'world',
  181. icon: 'fas fa-cog',
  182. type: ConfigureSettings,
  183. restricted: true,
  184. });
  185. const systemHpPaths = {
  186. 'cyberpunk-red-core': 'derivedStats.hp',
  187. lfg: 'health',
  188. worldbuilding: 'health',
  189. twodsix: 'hits',
  190. };
  191. TVA_CONFIG.systemHpPath = systemHpPaths[game.system.id] ?? 'attributes.hp';
  192. game.settings.register('token-variants', 'effectMappingToggleGroups', {
  193. scope: 'world',
  194. config: false,
  195. type: Object,
  196. default: { Default: true },
  197. });
  198. game.settings.register('token-variants', 'settings', {
  199. scope: 'world',
  200. config: false,
  201. type: Object,
  202. default: TVA_CONFIG,
  203. onChange: async (val) => {
  204. // Generate a diff, it will be required when doing post-processing of the modified settings
  205. const diff = _arrayAwareDiffObject(TVA_CONFIG, val);
  206. // Check image re-cache required due to permission changes
  207. let requiresImageCache = false;
  208. if ('permissions' in diff) {
  209. if (!userRequiresImageCache(TVA_CONFIG.permissions) && userRequiresImageCache(val.permissions))
  210. requiresImageCache = true;
  211. }
  212. // Update live settings
  213. mergeObject(TVA_CONFIG, val);
  214. if (TVA_CONFIG.filterEffectIcons && ('filterCustomEffectIcons' in diff || 'filterIconList' in diff)) {
  215. for (const tkn of canvas.tokens.placeables) {
  216. waitForTokenTexture(tkn, (token) => {
  217. token.drawEffects();
  218. });
  219. }
  220. }
  221. // Check image re-cache required due to search path changes
  222. if ('searchPaths' in diff || 'forgeSearchPaths' in diff) {
  223. if (userRequiresImageCache(TVA_CONFIG.permissions)) requiresImageCache = true;
  224. }
  225. // Cache/re-cache images if necessary
  226. if (requiresImageCache) {
  227. await cacheImages();
  228. }
  229. if (diff.staticCache) {
  230. const cacheFile = diff.staticCacheFile ? diff.staticCacheFile : TVA_CONFIG.staticCacheFile;
  231. saveCache(cacheFile);
  232. }
  233. TVA_CONFIG.hud = game.settings.get('token-variants', 'hudSettings');
  234. registerAllHooks();
  235. registerAllWrappers();
  236. if ('displayEffectIconsOnHover' in diff) {
  237. for (const tkn of canvas.tokens.placeables) {
  238. if (tkn.effects) tkn.effects.visible = !diff.displayEffectIconsOnHover;
  239. }
  240. }
  241. if ('hideElevationTooltip' in diff) {
  242. for (const tkn of canvas.tokens.placeables) {
  243. if (tkn.tooltip) tkn.tooltip.text = tkn._getTooltipText();
  244. }
  245. }
  246. if ('hideTokenBorder' in diff) {
  247. for (const tkn of canvas.tokens.placeables) {
  248. if (tkn.border) tkn.border.visible = !diff.hideTokenBorder;
  249. }
  250. }
  251. if ('filterEffectIcons' in diff || 'disableEffectIcons' in diff) {
  252. for (const tkn of canvas.tokens.placeables) {
  253. tkn.drawEffects();
  254. }
  255. }
  256. },
  257. });
  258. game.settings.register('token-variants', 'debug', {
  259. scope: 'world',
  260. config: false,
  261. type: Boolean,
  262. default: TVA_CONFIG.debug,
  263. onChange: (val) => (TVA_CONFIG.debug = val),
  264. });
  265. if (typeof ForgeAPI !== 'undefined') {
  266. game.settings.registerMenu('token-variants', 'forgeSearchPaths', {
  267. name: game.i18n.localize('token-variants.settings.forge-search-paths.Name'),
  268. hint: game.i18n.localize('token-variants.settings.forge-search-paths.Hint'),
  269. icon: 'fas fa-search',
  270. type: ForgeSearchPaths,
  271. scope: 'client',
  272. restricted: false,
  273. });
  274. }
  275. game.settings.register('token-variants', 'tokenConfigs', {
  276. scope: 'world',
  277. config: false,
  278. type: Array,
  279. default: TVA_CONFIG.tokenConfigs,
  280. onChange: (val) => (TVA_CONFIG.tokenConfigs = val),
  281. });
  282. game.settings.registerMenu('token-variants', 'tokenHUDSettings', {
  283. name: game.i18n.localize('token-variants.settings.token-hud.Name'),
  284. hint: game.i18n.localize('token-variants.settings.token-hud.Hint'),
  285. scope: 'client',
  286. icon: 'fas fa-images',
  287. type: TokenHUDClientSettings,
  288. restricted: false,
  289. });
  290. game.settings.registerMenu('token-variants', 'compendiumMapper', {
  291. name: game.i18n.localize('token-variants.settings.compendium-mapper.Name'),
  292. hint: game.i18n.localize('token-variants.settings.compendium-mapper.Hint'),
  293. scope: 'world',
  294. icon: 'fas fa-cogs',
  295. type: CompendiumMapConfig,
  296. restricted: true,
  297. });
  298. game.settings.register('token-variants', 'compendiumMapper', {
  299. scope: 'world',
  300. config: false,
  301. type: Object,
  302. default: TVA_CONFIG.compendiumMapper,
  303. onChange: (val) => (TVA_CONFIG.compendiumMapper = val),
  304. });
  305. game.settings.register('token-variants', 'hudSettings', {
  306. scope: 'client',
  307. config: false,
  308. type: Object,
  309. default: TVA_CONFIG.hud,
  310. onChange: (val) => (TVA_CONFIG.hud = val),
  311. });
  312. game.settings.registerMenu('token-variants', 'importExport', {
  313. name: `Import/Export`,
  314. hint: game.i18n.localize('token-variants.settings.import-export.Hint'),
  315. scope: 'world',
  316. icon: 'fas fa-toolbox',
  317. type: ImportExport,
  318. restricted: true,
  319. });
  320. // Read settings
  321. const settings = game.settings.get('token-variants', 'settings');
  322. mergeObject(TVA_CONFIG, settings);
  323. if (isEmpty(TVA_CONFIG.searchFilters)) {
  324. BASE_IMAGE_CATEGORIES.forEach((cat) => {
  325. TVA_CONFIG.searchFilters[cat] = {
  326. include: '',
  327. exclude: '',
  328. regex: '',
  329. };
  330. });
  331. }
  332. for (let uid in TVA_CONFIG.forgeSearchPaths) {
  333. TVA_CONFIG.forgeSearchPaths[uid].paths = TVA_CONFIG.forgeSearchPaths[uid].paths.map((p) => {
  334. if (!p.source) {
  335. p.source = 'forgevtt';
  336. }
  337. if (!p.types) {
  338. if (p.tiles) p.types = ['Tile'];
  339. else p.types = ['Portrait', 'Token', 'PortraitAndToken'];
  340. }
  341. return p;
  342. });
  343. }
  344. // 20/07/2023 Convert globalMappings to a new format
  345. if (getType(settings.globalMappings) === 'Object') {
  346. Hooks.once('ready', () => {
  347. TVA_CONFIG.globalMappings = migrateMappings(settings.globalMappings);
  348. setTimeout(() => updateSettings({ globalMappings: TVA_CONFIG.globalMappings }), 10000);
  349. });
  350. }
  351. // Read client settings
  352. TVA_CONFIG.hud = game.settings.get('token-variants', 'hudSettings');
  353. }
  354. export function migrateMappings(mappings, globalMappings = []) {
  355. if (!mappings) return [];
  356. if (getType(mappings) === 'Object') {
  357. let nMappings = [];
  358. for (const [effect, mapping] of Object.entries(mappings)) {
  359. if (!mapping.label) mapping.label = effect.replaceAll('¶', '.');
  360. if (!mapping.expression) mapping.expression = effect.replaceAll('¶', '.');
  361. if (!mapping.id) mapping.id = randomID(8);
  362. delete mapping.effect;
  363. if (mapping.overlayConfig) mapping.overlayConfig.id = mapping.id;
  364. delete mapping.overlayConfig?.effect;
  365. nMappings.push(mapping);
  366. }
  367. // Convert parents to parentIDs
  368. let combMappings = nMappings.concat(globalMappings);
  369. for (const mapping of nMappings) {
  370. if (mapping.overlayConfig?.parent) {
  371. if (mapping.overlayConfig.parent === 'Token (Placeable)') {
  372. mapping.overlayConfig.parentID = 'TOKEN';
  373. } else {
  374. const parent = combMappings.find((m) => m.label === mapping.overlayConfig.parent);
  375. if (parent) mapping.overlayConfig.parentID = parent.id;
  376. else mapping.overlayConfig.parentID = '';
  377. }
  378. delete mapping.overlayConfig.parent;
  379. }
  380. }
  381. return nMappings;
  382. }
  383. return mappings;
  384. }
  385. export function getFlagMappings(object) {
  386. if (!object) return [];
  387. let doc = object.document ?? object;
  388. const actorId = doc.actor?.id;
  389. if (actorId) {
  390. doc = game.actors.get(actorId);
  391. if (!doc) return [];
  392. }
  393. // 23/07/2023
  394. let mappings = doc.getFlag('token-variants', 'effectMappings') ?? [];
  395. if (getType(mappings) === 'Object') {
  396. mappings = migrateMappings(mappings, TVA_CONFIG.globalMappings);
  397. doc.setFlag('token-variants', 'effectMappings', mappings);
  398. }
  399. return mappings;
  400. }
  401. export function exportSettingsToJSON() {
  402. const settings = deepClone(TVA_CONFIG);
  403. const filename = `token-variants-settings.json`;
  404. saveDataToFile(JSON.stringify(settings, null, 2), 'text/json', filename);
  405. }
  406. export async function importSettingsFromJSON(json) {
  407. if (typeof json === 'string') json = JSON.parse(json);
  408. if (json.forgeSearchPaths)
  409. for (let uid in json.forgeSearchPaths) {
  410. json.forgeSearchPaths[uid].paths = json.forgeSearchPaths[uid].paths.map((p) => {
  411. if (!p.source) {
  412. p.source = 'forgevtt';
  413. }
  414. if (!p.types) {
  415. if (p.tiles) p.types = ['Tile'];
  416. else p.types = ['Portrait', 'Token', 'PortraitAndToken'];
  417. }
  418. return p;
  419. });
  420. }
  421. // 09/07/2022 Convert filters to new format if old one is still in use
  422. if (json.searchFilters && json.searchFilters.portraitFilterInclude != null) {
  423. const filters = json.searchFilters;
  424. json.searchFilters = {
  425. Portrait: {
  426. include: filters.portraitFilterInclude ?? '',
  427. exclude: filters.portraitFilterExclude ?? '',
  428. regex: filters.portraitFilterRegex ?? '',
  429. },
  430. Token: {
  431. include: filters.tokenFilterInclude ?? '',
  432. exclude: filters.tokenFilterExclude ?? '',
  433. regex: filters.tokenFilterRegex ?? '',
  434. },
  435. PortraitAndToken: {
  436. include: filters.generalFilterInclude ?? '',
  437. exclude: filters.generalFilterExclude ?? '',
  438. regex: filters.generalFilterRegex ?? '',
  439. },
  440. };
  441. if (json.compendiumMapper) delete json.compendiumMapper.searchFilters;
  442. }
  443. // Global Mappings need special merge
  444. if (json.globalMappings) {
  445. const nMappings = migrateMappings(json.globalMappings);
  446. for (const m of nMappings) {
  447. const i = TVA_CONFIG.globalMappings.findIndex((mapping) => m.label === mapping.label);
  448. if (i === -1) TVA_CONFIG.globalMappings.push(m);
  449. else TVA_CONFIG.globalMappings[i] = m;
  450. }
  451. json.globalMappings = TVA_CONFIG.globalMappings;
  452. }
  453. updateSettings(json);
  454. }
  455. function _refreshFilters(filters, customCategories, updateTVAConfig = false) {
  456. const categories = BASE_IMAGE_CATEGORIES.concat(customCategories ?? TVA_CONFIG.customImageCategories);
  457. for (const filter in filters) {
  458. if (!categories.includes(filter)) {
  459. delete filters[filter];
  460. if (updateTVAConfig) delete TVA_CONFIG.searchFilters[filter];
  461. }
  462. }
  463. for (const category of customCategories) {
  464. if (filters[category] == null) {
  465. filters[category] = {
  466. include: '',
  467. exclude: '',
  468. regex: '',
  469. };
  470. }
  471. }
  472. }
  473. export async function updateSettings(newSettings) {
  474. const settings = mergeObject(deepClone(TVA_CONFIG), newSettings, { insertKeys: false });
  475. // Custom image categories might have changed, meaning we may have filters that are no longer relevant
  476. // or need to be added
  477. if ('customImageCategories' in newSettings) {
  478. _refreshFilters(settings.searchFilters, newSettings.customImageCategories, true);
  479. if (settings.compendiumMapper?.searchOptions?.searchFilters != null) {
  480. _refreshFilters(settings.compendiumMapper.searchOptions.searchFilters, newSettings.customImageCategories);
  481. TVA_CONFIG.compendiumMapper.searchOptions.searchFilters = settings.compendiumMapper.searchOptions.searchFilters;
  482. }
  483. }
  484. await game.settings.set('token-variants', 'settings', settings);
  485. }
  486. export function _arrayAwareDiffObject(original, other, { inner = false } = {}) {
  487. function _difference(v0, v1) {
  488. let t0 = getType(v0);
  489. let t1 = getType(v1);
  490. if (t0 !== t1) return [true, v1];
  491. if (t0 === 'Array') return [!_arrayEquality(v0, v1), v1];
  492. if (t0 === 'Object') {
  493. if (isEmpty(v0) !== isEmpty(v1)) return [true, v1];
  494. let d = _arrayAwareDiffObject(v0, v1, { inner });
  495. return [!isEmpty(d), d];
  496. }
  497. return [v0 !== v1, v1];
  498. }
  499. // Recursively call the _difference function
  500. return Object.keys(other).reduce((obj, key) => {
  501. if (inner && !(key in original)) return obj;
  502. let [isDifferent, difference] = _difference(original[key], other[key]);
  503. if (isDifferent) obj[key] = difference;
  504. return obj;
  505. }, {});
  506. }
  507. function _arrayEquality(a1, a2) {
  508. if (!(a2 instanceof Array) || a2.length !== a1.length) return false;
  509. return a1.every((v, i) => {
  510. if (getType(v) === 'Object') return Object.keys(_arrayAwareDiffObject(v, a2[i])).length === 0;
  511. return a2[i] === v;
  512. });
  513. }
  514. export function getSearchOptions() {
  515. return {
  516. keywordSearch: TVA_CONFIG.keywordSearch,
  517. excludedKeywords: TVA_CONFIG.excludedKeywords,
  518. runSearchOnPath: TVA_CONFIG.runSearchOnPath,
  519. algorithm: TVA_CONFIG.algorithm,
  520. searchFilters: TVA_CONFIG.searchFilters,
  521. };
  522. }