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.

470 lines
16 KiB

  1. import { showArtSelect } from '../token-variants.mjs';
  2. import {
  3. BASE_IMAGE_CATEGORIES,
  4. SEARCH_TYPE,
  5. updateActorImage,
  6. updateTokenImage,
  7. userRequiresImageCache,
  8. } from '../scripts/utils.js';
  9. import { addToQueue, ArtSelect, renderFromQueue } from './artSelect.js';
  10. import { getSearchOptions, TVA_CONFIG, updateSettings } from '../scripts/settings.js';
  11. import ConfigureSettings from './configureSettings.js';
  12. import MissingImageConfig from './missingImageConfig.js';
  13. import { cacheImages, doImageSearch } from '../scripts/search.js';
  14. async function autoApply(actor, image1, image2, formData, typeOverride) {
  15. let portraitFound = formData.ignorePortrait;
  16. let tokenFound = formData.ignoreToken;
  17. if (formData.diffImages) {
  18. let results = [];
  19. if (!formData.ignorePortrait) {
  20. results = await doImageSearch(actor.name, {
  21. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT,
  22. simpleResults: true,
  23. searchOptions: formData.searchOptions,
  24. });
  25. if ((results ?? []).length != 0) {
  26. portraitFound = true;
  27. await updateActorImage(actor, results[0], false, formData.compendium);
  28. }
  29. }
  30. if (!formData.ignoreToken) {
  31. results = await doImageSearch(actor.prototypeToken.name, {
  32. searchType: SEARCH_TYPE.TOKEN,
  33. simpleResults: true,
  34. searchOptions: formData.searchOptions,
  35. });
  36. if ((results ?? []).length != 0) {
  37. tokenFound = true;
  38. updateTokenImage(results[0], {
  39. actor: actor,
  40. pack: formData.compendium,
  41. applyDefaultConfig: false,
  42. });
  43. }
  44. }
  45. } else {
  46. let results = await doImageSearch(actor.name, {
  47. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT_AND_TOKEN,
  48. simpleResults: true,
  49. searchOptions: formData.searchOptions,
  50. });
  51. if ((results ?? []).length != 0) {
  52. portraitFound = tokenFound = true;
  53. updateTokenImage(results[0], {
  54. actor: actor,
  55. actorUpdate: { img: results[0] },
  56. pack: formData.compendium,
  57. applyDefaultConfig: false,
  58. });
  59. }
  60. }
  61. if (!(tokenFound && portraitFound) && formData.autoDisplayArtSelect) {
  62. addToArtSelectQueue(actor, image1, image2, formData, typeOverride);
  63. }
  64. }
  65. function addToArtSelectQueue(actor, image1, image2, formData, typeOverride) {
  66. if (formData.diffImages) {
  67. if (!formData.ignorePortrait && !formData.ignoreToken) {
  68. addToQueue(actor.name, {
  69. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT,
  70. object: actor,
  71. preventClose: true,
  72. image1: image1,
  73. image2: image2,
  74. displayMode: ArtSelect.IMAGE_DISPLAY.PORTRAIT,
  75. searchOptions: formData.searchOptions,
  76. callback: async function (imgSrc, _) {
  77. await updateActorImage(actor, imgSrc);
  78. showArtSelect(actor.prototypeToken.name, {
  79. searchType: typeOverride ?? SEARCH_TYPE.TOKEN,
  80. object: actor,
  81. force: true,
  82. image1: imgSrc,
  83. image2: image2,
  84. displayMode: ArtSelect.IMAGE_DISPLAY.TOKEN,
  85. searchOptions: formData.searchOptions,
  86. callback: (imgSrc, name) =>
  87. updateTokenImage(imgSrc, {
  88. actor: actor,
  89. imgName: name,
  90. applyDefaultConfig: false,
  91. }),
  92. });
  93. },
  94. });
  95. } else if (formData.ignorePortrait) {
  96. addToQueue(actor.name, {
  97. searchType: typeOverride ?? SEARCH_TYPE.TOKEN,
  98. object: actor,
  99. image1: image1,
  100. image2: image2,
  101. displayMode: ArtSelect.IMAGE_DISPLAY.TOKEN,
  102. searchOptions: formData.searchOptions,
  103. callback: async function (imgSrc, name) {
  104. updateTokenImage(imgSrc, {
  105. actor: actor,
  106. imgName: name,
  107. applyDefaultConfig: false,
  108. });
  109. },
  110. });
  111. } else if (formData.ignoreToken) {
  112. addToQueue(actor.name, {
  113. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT,
  114. object: actor,
  115. image1: image1,
  116. image2: image2,
  117. displayMode: ArtSelect.IMAGE_DISPLAY.PORTRAIT,
  118. searchOptions: formData.searchOptions,
  119. callback: async function (imgSrc, name) {
  120. await updateActorImage(actor, imgSrc);
  121. },
  122. });
  123. }
  124. } else {
  125. addToQueue(actor.name, {
  126. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT_AND_TOKEN,
  127. object: actor,
  128. image1: image1,
  129. image2: image2,
  130. displayMode: ArtSelect.IMAGE_DISPLAY.PORTRAIT_TOKEN,
  131. searchOptions: formData.searchOptions,
  132. callback: async function (imgSrc, name) {
  133. await updateActorImage(actor, imgSrc);
  134. updateTokenImage(imgSrc, {
  135. actor: actor,
  136. imgName: name,
  137. applyDefaultConfig: false,
  138. });
  139. },
  140. });
  141. }
  142. }
  143. export default class CompendiumMapConfig extends FormApplication {
  144. constructor() {
  145. super({}, {});
  146. this.searchOptions = deepClone(getSearchOptions());
  147. mergeObject(this.searchOptions, deepClone(TVA_CONFIG.compendiumMapper.searchOptions));
  148. this._fixSearchPaths();
  149. }
  150. static get defaultOptions() {
  151. return mergeObject(super.defaultOptions, {
  152. id: 'token-variants-compendium-map-config',
  153. classes: ['sheet'],
  154. template: 'modules/token-variants/templates/compendiumMap.html',
  155. resizable: false,
  156. minimizable: false,
  157. title: game.i18n.localize('token-variants.settings.compendium-mapper.Name'),
  158. width: 500,
  159. });
  160. }
  161. async getData(options) {
  162. let data = super.getData(options);
  163. data = mergeObject(data, TVA_CONFIG.compendiumMapper);
  164. const supportedPacks = ['Actor', 'Cards', 'Item', 'Macro', 'RollTable'];
  165. data.supportedPacks = supportedPacks.join(', ');
  166. const packs = [];
  167. game.packs.forEach((pack) => {
  168. if (!pack.locked && supportedPacks.includes(pack.documentName)) {
  169. packs.push({ title: pack.title, id: pack.collection, type: pack.documentName });
  170. }
  171. });
  172. data.compendiums = packs;
  173. data.compendium = TVA_CONFIG.compendiumMapper.compendium;
  174. data.categories = BASE_IMAGE_CATEGORIES.concat(TVA_CONFIG.customImageCategories);
  175. data.category = TVA_CONFIG.compendiumMapper.category;
  176. return data;
  177. }
  178. /**
  179. * @param {JQuery} html
  180. */
  181. activateListeners(html) {
  182. super.activateListeners(html);
  183. html.find('.token-variants-override-category').change(this._onCategoryOverride).trigger('change');
  184. html.find('.token-variants-auto-apply').change(this._onAutoApply);
  185. html.find('.token-variants-diff-images').change(this._onDiffImages);
  186. html.find(`.token-variants-search-options`).on('click', this._onSearchOptions.bind(this));
  187. html.find(`.token-variants-missing-images`).on('click', this._onMissingImages.bind(this));
  188. $(html).find('[name="compendium"]').change(this._onCompendiumSelect.bind(this)).trigger('change');
  189. }
  190. async _onAutoApply(event) {
  191. $(event.target).closest('form').find('.token-variants-auto-art-select').prop('disabled', !event.target.checked);
  192. }
  193. async _onCategoryOverride(event) {
  194. $(event.target).closest('form').find('.token-variants-category').prop('disabled', !event.target.checked);
  195. }
  196. async _onDiffImages(event) {
  197. $(event.target).closest('form').find('.token-variants-tp-ignore').prop('disabled', !event.target.checked);
  198. }
  199. async _onCompendiumSelect(event) {
  200. const compendium = game.packs.get($(event.target).val());
  201. if (compendium) {
  202. $(event.target)
  203. .closest('form')
  204. .find('.token-specific')
  205. .css('visibility', compendium.documentName === 'Actor' ? 'visible' : 'hidden');
  206. }
  207. }
  208. _fixSearchPaths() {
  209. if (!this.searchOptions.searchPaths?.length) {
  210. this.searchOptions.searchPaths = deepClone(TVA_CONFIG.searchPaths);
  211. }
  212. }
  213. async _onSearchOptions(event) {
  214. this._fixSearchPaths();
  215. new ConfigureSettings(this.searchOptions, {
  216. searchPaths: true,
  217. searchFilters: true,
  218. searchAlgorithm: true,
  219. randomizer: false,
  220. features: false,
  221. popup: false,
  222. permissions: false,
  223. worldHud: false,
  224. misc: false,
  225. activeEffects: false,
  226. }).render(true);
  227. }
  228. async _onMissingImages(event) {
  229. new MissingImageConfig().render(true);
  230. }
  231. async startMapping(formData) {
  232. if (formData.diffImages && formData.ignoreToken && formData.ignorePortrait) {
  233. return;
  234. }
  235. const originalSearchPaths = TVA_CONFIG.searchPaths;
  236. if (formData.searchOptions.searchPaths?.length) {
  237. TVA_CONFIG.searchPaths = formData.searchOptions.searchPaths;
  238. }
  239. if (formData.cache || !userRequiresImageCache() || formData.searchOptions.searchPaths?.length) {
  240. console.log('TVA-Mapper: Starting Image caching.');
  241. await cacheImages();
  242. console.log('TVA-Mapper: Caching finished.');
  243. }
  244. const endMapping = function () {
  245. if (formData.searchOptions.searchPaths?.length) {
  246. TVA_CONFIG.searchPaths = originalSearchPaths;
  247. cacheImages();
  248. }
  249. };
  250. const compendium = game.packs.get(formData.compendium);
  251. let missingImageList = TVA_CONFIG.compendiumMapper.missingImages
  252. .filter((mi) => mi.document === 'all' || mi.document === compendium.documentName)
  253. .map((mi) => mi.image);
  254. const typeOverride = formData.overrideCategory ? formData.category : null;
  255. let artSelectDisplayed = false;
  256. let processItem;
  257. let consoleProcessedTracking = 0;
  258. if (compendium.documentName === 'Actor') {
  259. processItem = async function (item) {
  260. const actor = item;
  261. if (actor.name === '#[CF_tempEntity]') return; // Compendium Folders module's control entity
  262. let hasPortrait = actor.img !== CONST.DEFAULT_TOKEN && !missingImageList.includes(actor.img);
  263. let hasToken =
  264. actor.prototypeToken.texture.src !== CONST.DEFAULT_TOKEN &&
  265. !missingImageList.includes(actor.prototypeToken.texture.src);
  266. if (formData.syncImages && hasPortrait !== hasToken) {
  267. if (hasPortrait) {
  268. await updateTokenImage(actor.img, { actor: actor, applyDefaultConfig: false });
  269. } else {
  270. await updateActorImage(actor, actor.prototypeToken.texture.src);
  271. }
  272. hasPortrait = hasToken = true;
  273. }
  274. let includeThisActor = !(formData.missingOnly && hasPortrait) && !formData.ignorePortrait;
  275. let includeThisToken = !(formData.missingOnly && hasToken) && !formData.ignoreToken;
  276. const image1 = formData.showImages ? actor.img : '';
  277. const image2 = formData.showImages ? actor.prototypeToken.texture.src : '';
  278. if (includeThisActor || includeThisToken) {
  279. if (formData.autoApply) {
  280. await autoApply(actor, image1, image2, formData, typeOverride);
  281. } else {
  282. artSelectDisplayed = true;
  283. addToArtSelectQueue(actor, image1, image2, formData, typeOverride);
  284. }
  285. }
  286. consoleProcessedTracking++;
  287. if (consoleProcessedTracking % 100 === 0)
  288. console.log(`TVA-Mapper: Processed ${consoleProcessedTracking} ${compendium.documentName}s`);
  289. };
  290. } else {
  291. processItem = async function (item) {
  292. const doc = item;
  293. if (doc.name === '#[CF_tempEntity]') return; // Compendium Folders module's control entity
  294. let defaultImg = '';
  295. if (doc.schema.fields.img || doc.schema.fields.texture) {
  296. defaultImg = (doc.schema.fields.img ?? doc.schema.fields.texture).initial();
  297. }
  298. const hasImage = doc.img != null && doc.img !== defaultImg && !missingImageList.includes(doc.img);
  299. let imageFound = false;
  300. if (formData.missingOnly && hasImage) return;
  301. if (formData.autoApply) {
  302. let results = await doImageSearch(doc.name, {
  303. searchType: typeOverride ?? compendium.documentName,
  304. simpleResults: true,
  305. searchOptions: formData.searchOptions,
  306. });
  307. if ((results ?? []).length != 0) {
  308. imageFound = true;
  309. await updateActorImage(doc, results[0], false, formData.compendium);
  310. }
  311. }
  312. if (!formData.autoApply || (formData.autoDisplayArtSelect && !imageFound)) {
  313. artSelectDisplayed = true;
  314. addToQueue(doc.name, {
  315. searchType: typeOverride ?? compendium.documentName,
  316. object: doc,
  317. image1: formData.showImages ? doc.img : '',
  318. displayMode: ArtSelect.IMAGE_DISPLAY.IMAGE,
  319. searchOptions: formData.searchOptions,
  320. callback: async function (imgSrc, name) {
  321. await updateActorImage(doc, imgSrc);
  322. },
  323. });
  324. }
  325. consoleProcessedTracking++;
  326. if (consoleProcessedTracking % 100 === 0)
  327. console.log(`TVA-Mapper: Processed ${consoleProcessedTracking} ${compendium.documentName}s`);
  328. };
  329. }
  330. console.log(`TVA-Mapper: Starting Batch ${compendium.documentName} load.`);
  331. const documents = await compendium.getDocuments();
  332. console.log(`TVA-Mapper: Load finished. Beginning processing.`);
  333. if (formData.autoApply) {
  334. let processing = true;
  335. let stopProcessing = false;
  336. let processed = 0;
  337. let counter = $(`<p>CACHING 0/${documents.length}</p>`);
  338. let d;
  339. const startProcessing = async function () {
  340. while (processing && processed < documents.length) {
  341. await new Promise((resolve, reject) => {
  342. setTimeout(async () => {
  343. await processItem(documents[processed]);
  344. resolve();
  345. }, 10);
  346. });
  347. processed++;
  348. counter.html(`${processed}/${documents.length}`);
  349. }
  350. if (stopProcessing || processed === documents.length) {
  351. d?.close(true);
  352. addToQueue('DUMMY', { execute: endMapping });
  353. renderFromQueue();
  354. }
  355. };
  356. d = new Dialog({
  357. title: `Mapping: ${compendium.title}`,
  358. content: `
  359. <div style="text-align:center;" class="fa-3x"><i class="fas fa-spinner fa-pulse"></i></div>
  360. <div style="text-align:center;" class="counter"></div>
  361. <button style="width:100%;" class="pause"><i class="fas fa-play-circle"></i> Pause/Start</button>`,
  362. buttons: {
  363. cancel: {
  364. icon: '<i class="fas fa-stop-circle"></i>',
  365. label: 'Cancel',
  366. },
  367. },
  368. default: 'cancel',
  369. render: (html) => {
  370. html.find('.counter').append(counter);
  371. const spinner = html.find('.fa-spinner');
  372. html.find('.pause').on('click', () => {
  373. if (processing) {
  374. processing = false;
  375. spinner.removeClass('fa-pulse');
  376. } else {
  377. processing = true;
  378. startProcessing();
  379. spinner.addClass('fa-pulse');
  380. }
  381. });
  382. setTimeout(async () => startProcessing(), 1000);
  383. },
  384. close: () => {
  385. if (!stopProcessing) {
  386. stopProcessing = true;
  387. if (!processing) startProcessing();
  388. else processing = false;
  389. }
  390. },
  391. });
  392. d.render(true);
  393. } else {
  394. const tasks = documents.map(processItem);
  395. Promise.all(tasks).then(() => {
  396. addToQueue('DUMMY', { execute: endMapping });
  397. renderFromQueue();
  398. if (formData.missingOnly && !artSelectDisplayed) {
  399. ui.notifications.warn('Token Variant Art: No documents found containing missing images.');
  400. }
  401. });
  402. }
  403. }
  404. /**
  405. * @param {Event} event
  406. * @param {Object} formData
  407. */
  408. async _updateObject(event, formData) {
  409. // If search paths are the same, remove them from searchOptions
  410. if (
  411. !this.searchOptions.searchPaths?.length ||
  412. isEmpty(diffObject(this.searchOptions.searchPaths, TVA_CONFIG.searchPaths))
  413. ) {
  414. this.searchOptions.searchPaths = [];
  415. }
  416. formData.searchOptions = this.searchOptions;
  417. await updateSettings({ compendiumMapper: formData });
  418. if (formData.compendium) {
  419. this.startMapping(formData);
  420. }
  421. }
  422. }