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.

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