From 7660aa9eb26b8994d157f31d1b0024d198ba053e Mon Sep 17 00:00:00 2001 From: Shri Akhil Chellapilla Date: Sat, 8 Nov 2025 22:07:39 -0500 Subject: [PATCH 1/2] Handles deletion and folder creation for v13 - Temp actors are created in a dedicated temp folder. - Temp actors are only deleted if user has the permission. - Setting to automatically create required Folders. Defaults to false. - Custom error message when failing to create a Folder. --- lang/en.json | 10 ++++++++-- src/app/Pathmuncher.js | 45 ++++++++++++++++++++++++++++++++++++------ src/constants.js | 13 ++++++++++++ src/hooks/folder.js | 18 +++++++++++++++++ src/module.js | 2 ++ src/utils.js | 14 +++++++++++++ 6 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 src/hooks/folder.js diff --git a/lang/en.json b/lang/en.json index 6649b2b..2fbc949 100644 --- a/lang/en.json +++ b/lang/en.json @@ -33,6 +33,10 @@ "Title": "Use custom compendium mappings?", "Label": "Custom Compendium Mappings", "Hint": "This is useful for things like Battlezoo or Cleric+ modules, or even if you have your own compendium of custom feats." + }, + "AutoCreateTempFolder": { + "Name": "Automatically create folders?", + "Hint": "Creates required folders whenever a GM user logs in." } }, "Dialogs": { @@ -95,7 +99,8 @@ }, "Chat": {}, "Notifications": { - "CreateActorPermission": "Pathmuncher requires the CREATE ACTOR permission for your user." + "CreateActorPermission": "Pathmuncher requires the CREATE ACTOR permission for your user.", + "CreateFolderError": "User {userName} lacks permission to create Folder {folderName}. Enable the setting or create it manually." }, "Labels": { "Character": "Character details", @@ -134,7 +139,8 @@ }, "Folders": { "Familiar": "Familiars", - "Familiars": "Familiars" + "Familiars": "Familiars", + "PathmuncherTemp": "PathmuncherTemp" }, "CompendiumGroups": { "feats": "Feats", diff --git a/src/app/Pathmuncher.js b/src/app/Pathmuncher.js index 7b5976b..b517e83 100644 --- a/src/app/Pathmuncher.js +++ b/src/app/Pathmuncher.js @@ -134,6 +134,9 @@ export class Pathmuncher { } this.immediateDiveAdd = utils.setting("USE_IMMEDIATE_DEEP_DIVE"); + + this.tempActorCounter = 1; + this.tempActorFolderName = game.i18n.localize(`${CONSTANTS.FLAG_NAME}.Folders.PathmuncherTemp`); } async #loadCompendiumMatchers() { @@ -1024,7 +1027,7 @@ export class Pathmuncher { }); throw err; } finally { - await Actor.deleteDocuments([tempActor._id]); + await this.#tryDeleteTempActor(tempActor); } logger.debug("Evaluate Choices failed", { choiceSet: cleansedChoiceSet, tempActor, document }); @@ -1085,7 +1088,7 @@ export class Pathmuncher { }); throw err; } finally { - await Actor.deleteDocuments([tempActor._id]); + await this.#tryDeleteTempActor(tempActor); } logger.debug("Evaluate UUID failed", { choiceSet: cleansedRuleEntry, tempActor, document }); @@ -1142,7 +1145,7 @@ export class Pathmuncher { }); throw err; } finally { - await Actor.deleteDocuments([tempActor._id]); + await this.#tryDeleteTempActor(tempActor); } } @@ -1176,7 +1179,7 @@ export class Pathmuncher { }); throw err; } finally { - await Actor.deleteDocuments([tempActor._id]); + await this.#tryDeleteTempActor(tempActor); } } @@ -2643,7 +2646,9 @@ export class Pathmuncher { removePassedDocuments = false } = {}, ) { const actorData = foundry.utils.mergeObject({ type: "character", flags: { pathmuncher: { temp: true } } }, this.result.character); - actorData.name = `Mr Temp (${this.result.character.name})`; + this.#setTempActorName(actorData); + await this.#setTempActorFolder(actorData); + if (documents.map((d) => d.name.split("(")[0].trim().toLowerCase()).includes("skill training")) { delete actorData.system.skills; } @@ -2835,6 +2840,32 @@ export class Pathmuncher { return actor; } + #setTempActorName(actorData) { + if (!foundry.utils.isNewerVersion(game.version, CONSTANTS.TEMP_FOLDER_FOUNDRY_MIN_VERSION)) { + actorData.name = `Mr Temp (${this.result.character.name})`; + return; + } + + const formattedNum = this.tempActorCounter.toString().padStart(4, "0"); + actorData.name = `Mr Temp (${this.result.character.name}) ${formattedNum}`; + this.tempActorCounter += 1; + } + + async #setTempActorFolder(actorData) { + if (!foundry.utils.isNewerVersion(game.version, CONSTANTS.TEMP_FOLDER_FOUNDRY_MIN_VERSION)) { + return; + } + + let tempActorFolder = await utils.getOrCreateFolder(null, "Actor", this.tempActorFolderName); + actorData.folder = tempActorFolder?.id; + } + + async #tryDeleteTempActor(tempActor) { + if (tempActor.canUserModify(game.user, "delete")) { + await Actor.deleteDocuments([tempActor._id]); + } + } + async processCharacter() { if (!this.source) return; await this.#prepare(); @@ -3056,7 +3087,9 @@ export class Pathmuncher { static async removeTempActors() { for (const actor of game.actors.filter((a) => foundry.utils.getProperty(a, "flags.pathmuncher.temp") === true)) { - await actor.delete(); + if (actor.canUserModify(game.user, "delete")) { + await actor.delete(); + } } } diff --git a/src/constants.js b/src/constants.js index 8b511b5..aa15fe6 100644 --- a/src/constants.js +++ b/src/constants.js @@ -12,6 +12,7 @@ const CONSTANTS = { CUSTOM_COMPENDIUM_MAPPINGS: "custom-compendium-mappings", USE_IMMEDIATE_DEEP_DIVE: "use-immediate-deep-dive", DISPLAY_TITLE: "display-title", + AUTO_CREATE_TEMP_FOLDER: "auto-create-temp-folder", }, FEAT_PRIORITY: [ @@ -148,6 +149,8 @@ const CONSTANTS = { backgrounds: ["pf2e.backgrounds", "sf2e-anachronism.backgrounds", "pf2e-legacy-content.backgrounds-legacy"], }, + TEMP_FOLDER_FOUNDRY_MIN_VERSION: "13", + GET_DEFAULT_SETTINGS() { return foundry.utils.deepClone(CONSTANTS.DEFAULT_SETTINGS); }, @@ -216,6 +219,16 @@ CONSTANTS.DEFAULT_SETTINGS = { default: "WARN", }, + [CONSTANTS.SETTINGS.AUTO_CREATE_TEMP_FOLDER]: { + name: `${CONSTANTS.FLAG_NAME}.Settings.AutoCreateTempFolder.Name`, + hint: `${CONSTANTS.FLAG_NAME}.Settings.AutoCreateTempFolder.Hint`, + scope: "world", + config: true, + type: Boolean, + default: false, + onChange: debouncedReload, + }, + }; CONSTANTS.PATH = `modules/${CONSTANTS.MODULE_NAME}`; diff --git a/src/hooks/folder.js b/src/hooks/folder.js new file mode 100644 index 0000000..0bad18d --- /dev/null +++ b/src/hooks/folder.js @@ -0,0 +1,18 @@ +import { PathmuncherImporter } from "../app/PathmuncherImporter.js"; +import CONSTANTS from "../constants.js"; +import utils from "../utils.js"; + +export function autoCreateFolders() { + + const autoCreateEnabled = utils.setting("AUTO_CREATE_TEMP_FOLDER"); + if (!autoCreateEnabled) return; + if (!game.user.isGM) return; + + utils.getOrCreateFolder(null, "Actor", game.i18n.localize(`${CONSTANTS.FLAG_NAME}.Folders.Familiars`)); + + if (foundry.utils.isNewerVersion(game.version, CONSTANTS.TEMP_FOLDER_FOUNDRY_MIN_VERSION)) + { + utils.getOrCreateFolder(null, "Actor", game.i18n.localize(`${CONSTANTS.FLAG_NAME}.Folders.PathmuncherTemp`)); + } + +} diff --git a/src/module.js b/src/module.js index f3beff6..aa7b71d 100644 --- a/src/module.js +++ b/src/module.js @@ -1,6 +1,7 @@ import { registerAPI } from "./hooks/api.js"; import { registerSettings } from "./hooks/settings.js"; import { registerSheetButton } from "./hooks/sheets.js"; +import { autoCreateFolders } from "./hooks/folder.js"; Hooks.once("init", () => { registerSettings(); @@ -9,4 +10,5 @@ Hooks.once("init", () => { Hooks.once("ready", () => { registerSheetButton(); registerAPI(); + autoCreateFolders(); }); diff --git a/src/utils.js b/src/utils.js index fca727b..8c5173c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -57,6 +57,20 @@ const utils = { // console.warn(`Looking for ${root} ${entityType} ${folderName}`); // console.warn(folder); if (folder) return folder; + + if (!Folder.canUserCreate(game.user)) + { + const errorMsg = game.i18n.format( + `${CONSTANTS.FLAG_NAME}.Notifications.CreateFolderError`, + { + userName: game.user.name, + folderName: folderName + } + ); + ui.notifications.error(errorMsg); + throw new Error(errorMsg); + } + folder = await Folder.create( { name: folderName, From cfd858fa3a7240eec6c5f2dc86207387dfe03ec6 Mon Sep 17 00:00:00 2001 From: Jack Holloway Date: Fri, 14 Nov 2025 21:24:34 +0000 Subject: [PATCH 2/2] Linting and small corrections --- lang/en.json | 6 +++++- src/app/Pathmuncher.js | 37 ++++++++----------------------------- src/constants.js | 34 +++++++++++++++++++++++++--------- src/hooks/folder.js | 10 +++------- src/hooks/settings.js | 11 +++++++++++ src/module.js | 10 ++++++++-- src/utils.js | 26 ++++++++++++++++++++------ 7 files changed, 80 insertions(+), 54 deletions(-) diff --git a/lang/en.json b/lang/en.json index 2fbc949..b374123 100644 --- a/lang/en.json +++ b/lang/en.json @@ -37,6 +37,10 @@ "AutoCreateTempFolder": { "Name": "Automatically create folders?", "Hint": "Creates required folders whenever a GM user logs in." + }, + "UseTempFolder": { + "Name": "Use temporary folder for imports?", + "Hint": "Imports will go into a temporary folder which you can then move things out of later. This is useful for allowing players to import their own characters without cluttering up the main actor directory with temporary actors created during munching." } }, "Dialogs": { @@ -140,7 +144,7 @@ "Folders": { "Familiar": "Familiars", "Familiars": "Familiars", - "PathmuncherTemp": "PathmuncherTemp" + "PathmuncherTemp": "Pathmuncher Scratchpad" }, "CompendiumGroups": { "feats": "Feats", diff --git a/src/app/Pathmuncher.js b/src/app/Pathmuncher.js index b517e83..ce0b310 100644 --- a/src/app/Pathmuncher.js +++ b/src/app/Pathmuncher.js @@ -134,7 +134,7 @@ export class Pathmuncher { } this.immediateDiveAdd = utils.setting("USE_IMMEDIATE_DEEP_DIVE"); - + this.tempActorCounter = 1; this.tempActorFolderName = game.i18n.localize(`${CONSTANTS.FLAG_NAME}.Folders.PathmuncherTemp`); } @@ -1027,7 +1027,7 @@ export class Pathmuncher { }); throw err; } finally { - await this.#tryDeleteTempActor(tempActor); + await utils.deleteActor(tempActor); } logger.debug("Evaluate Choices failed", { choiceSet: cleansedChoiceSet, tempActor, document }); @@ -1088,7 +1088,7 @@ export class Pathmuncher { }); throw err; } finally { - await this.#tryDeleteTempActor(tempActor); + await utils.deleteActor(tempActor); } logger.debug("Evaluate UUID failed", { choiceSet: cleansedRuleEntry, tempActor, document }); @@ -1145,7 +1145,7 @@ export class Pathmuncher { }); throw err; } finally { - await this.#tryDeleteTempActor(tempActor); + await utils.deleteActor(tempActor); } } @@ -1179,7 +1179,7 @@ export class Pathmuncher { }); throw err; } finally { - await this.#tryDeleteTempActor(tempActor); + await utils.deleteActor(tempActor); } } @@ -2841,31 +2841,18 @@ export class Pathmuncher { } #setTempActorName(actorData) { - if (!foundry.utils.isNewerVersion(game.version, CONSTANTS.TEMP_FOLDER_FOUNDRY_MIN_VERSION)) { - actorData.name = `Mr Temp (${this.result.character.name})`; - return; - } - const formattedNum = this.tempActorCounter.toString().padStart(4, "0"); actorData.name = `Mr Temp (${this.result.character.name}) ${formattedNum}`; this.tempActorCounter += 1; } async #setTempActorFolder(actorData) { - if (!foundry.utils.isNewerVersion(game.version, CONSTANTS.TEMP_FOLDER_FOUNDRY_MIN_VERSION)) { - return; - } - + if (!utils.setting("USE_TEMP_FOLDER")) return; + let tempActorFolder = await utils.getOrCreateFolder(null, "Actor", this.tempActorFolderName); actorData.folder = tempActorFolder?.id; } - async #tryDeleteTempActor(tempActor) { - if (tempActor.canUserModify(game.user, "delete")) { - await Actor.deleteDocuments([tempActor._id]); - } - } - async processCharacter() { if (!this.source) return; await this.#prepare(); @@ -3085,14 +3072,6 @@ export class Pathmuncher { }); } - static async removeTempActors() { - for (const actor of game.actors.filter((a) => foundry.utils.getProperty(a, "flags.pathmuncher.temp") === true)) { - if (actor.canUserModify(game.user, "delete")) { - await actor.delete(); - } - } - } - async updateActor() { await this.#removeDocumentsToBeUpdated(); @@ -3112,7 +3091,7 @@ export class Pathmuncher { await this.actor.update(this.result.character); await this.#createActorEmbeddedDocuments(); await this.#restoreEmbeddedRuleLogic(); - await Pathmuncher.removeTempActors(); + await utils.removeTempActors(); } async postImportCheck() { diff --git a/src/constants.js b/src/constants.js index aa15fe6..25ce8ea 100644 --- a/src/constants.js +++ b/src/constants.js @@ -13,6 +13,8 @@ const CONSTANTS = { USE_IMMEDIATE_DEEP_DIVE: "use-immediate-deep-dive", DISPLAY_TITLE: "display-title", AUTO_CREATE_TEMP_FOLDER: "auto-create-temp-folder", + USE_TEMP_FOLDER: "use-temp-folder", + ACTIVE_GM: "active-gm", }, FEAT_PRIORITY: [ @@ -149,8 +151,6 @@ const CONSTANTS = { backgrounds: ["pf2e.backgrounds", "sf2e-anachronism.backgrounds", "pf2e-legacy-content.backgrounds-legacy"], }, - TEMP_FOLDER_FOUNDRY_MIN_VERSION: "13", - GET_DEFAULT_SETTINGS() { return foundry.utils.deepClone(CONSTANTS.DEFAULT_SETTINGS); }, @@ -168,6 +168,25 @@ CONSTANTS.DEFAULT_SETTINGS = { default: true, }, + [CONSTANTS.SETTINGS.AUTO_CREATE_TEMP_FOLDER]: { + name: `${CONSTANTS.FLAG_NAME}.Settings.AutoCreateTempFolder.Name`, + hint: `${CONSTANTS.FLAG_NAME}.Settings.AutoCreateTempFolder.Hint`, + scope: "world", + config: true, + type: Boolean, + default: false, + onChange: debouncedReload, + }, + + [CONSTANTS.SETTINGS.USE_TEMP_FOLDER]: { + name: `${CONSTANTS.FLAG_NAME}.Settings.UseTempFolder.Name`, + hint: `${CONSTANTS.FLAG_NAME}.Settings.UseTempFolder.Hint`, + scope: "world", + config: true, + type: Boolean, + default: false, + }, + [CONSTANTS.SETTINGS.RESTRICT_TO_TRUSTED]: { name: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Name`, hint: `${CONSTANTS.FLAG_NAME}.Settings.RestrictToTrusted.Hint`, @@ -219,14 +238,11 @@ CONSTANTS.DEFAULT_SETTINGS = { default: "WARN", }, - [CONSTANTS.SETTINGS.AUTO_CREATE_TEMP_FOLDER]: { - name: `${CONSTANTS.FLAG_NAME}.Settings.AutoCreateTempFolder.Name`, - hint: `${CONSTANTS.FLAG_NAME}.Settings.AutoCreateTempFolder.Hint`, + [CONSTANTS.SETTINGS.ACTIVE_GM]: { scope: "world", - config: true, - type: Boolean, - default: false, - onChange: debouncedReload, + config: false, + type: String, + default: "", }, }; diff --git a/src/hooks/folder.js b/src/hooks/folder.js index 0bad18d..44ddf15 100644 --- a/src/hooks/folder.js +++ b/src/hooks/folder.js @@ -1,17 +1,13 @@ -import { PathmuncherImporter } from "../app/PathmuncherImporter.js"; import CONSTANTS from "../constants.js"; import utils from "../utils.js"; export function autoCreateFolders() { - - const autoCreateEnabled = utils.setting("AUTO_CREATE_TEMP_FOLDER"); - if (!autoCreateEnabled) return; + if (!utils.setting("AUTO_CREATE_TEMP_FOLDER")) return; if (!game.user.isGM) return; utils.getOrCreateFolder(null, "Actor", game.i18n.localize(`${CONSTANTS.FLAG_NAME}.Folders.Familiars`)); - - if (foundry.utils.isNewerVersion(game.version, CONSTANTS.TEMP_FOLDER_FOUNDRY_MIN_VERSION)) - { + + if (utils.setting("USE_TEMP_FOLDER")) { utils.getOrCreateFolder(null, "Actor", game.i18n.localize(`${CONSTANTS.FLAG_NAME}.Folders.PathmuncherTemp`)); } diff --git a/src/hooks/settings.js b/src/hooks/settings.js index a947514..55f1a9f 100644 --- a/src/hooks/settings.js +++ b/src/hooks/settings.js @@ -1,5 +1,6 @@ import { CompendiumSelector } from "../app/CompendiumSelector.js"; import CONSTANTS from "../constants.js"; +import utils from "../utils.js"; async function resetSettings() { for (const [name, data] of Object.entries(CONSTANTS.GET_DEFAULT_SETTINGS())) { @@ -36,6 +37,16 @@ class ResetSettingsDialog extends FormApplication { } } +export async function processActiveGM() { + // determine if this user is the active gm/first active in current session + if (game.user.isGM) { + const currentGMUser = game.users.get(utils.setting("ACTIVE_GM")); + if ((currentGMUser && !currentGMUser.active) || !currentGMUser) { + await utils.updateSetting("ACTIVE_GM", game.user.id); + } + } +} + export function registerSettings() { game.settings.registerMenu(CONSTANTS.MODULE_NAME, "resetToDefaults", { name: `${CONSTANTS.FLAG_NAME}.Settings.Reset.Title`, diff --git a/src/module.js b/src/module.js index aa7b71d..a10d67b 100644 --- a/src/module.js +++ b/src/module.js @@ -1,14 +1,20 @@ import { registerAPI } from "./hooks/api.js"; -import { registerSettings } from "./hooks/settings.js"; +import { registerSettings, processActiveGM } from "./hooks/settings.js"; import { registerSheetButton } from "./hooks/sheets.js"; import { autoCreateFolders } from "./hooks/folder.js"; +import utils from "./utils.js"; Hooks.once("init", () => { registerSettings(); }); -Hooks.once("ready", () => { +Hooks.once("ready", async () => { + await processActiveGM(); registerSheetButton(); registerAPI(); autoCreateFolders(); + // cleanup temp actors on startup, but only for the active GM + if (utils.setting("ACTIVE_GM") === game.user.id) { + await utils.removeTempActors(); + } }); diff --git a/src/utils.js b/src/utils.js index 8c5173c..c0350d3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -54,18 +54,15 @@ const utils = { // if a root folder we want to match the root id for the parent folder && (root ? root.id : null) === (f.folder?.id ?? null), ); - // console.warn(`Looking for ${root} ${entityType} ${folderName}`); - // console.warn(folder); if (folder) return folder; - if (!Folder.canUserCreate(game.user)) - { + if (!Folder.canUserCreate(game.user)) { const errorMsg = game.i18n.format( `${CONSTANTS.FLAG_NAME}.Notifications.CreateFolderError`, { userName: game.user.name, - folderName: folderName - } + folderName: folderName, + }, ); ui.notifications.error(errorMsg); throw new Error(errorMsg); @@ -110,6 +107,23 @@ const utils = { return (foundry.utils.isNewerVersion("5.9.0", game.version) && game.settings.get("pf2e", "ancestryParagonVariant")); }, + async deleteActor(actor) { + if (actor.canUserModify(game.user, "delete")) { + await Actor.deleteDocuments([actor._id]); + } + }, + + async removeTempActors() { + const actorIds = game.actors + .filter((a) => + foundry.utils.getProperty(a, "flags.pathmuncher.temp") === true + && a.canUserModify(game.user, "delete"), + ) + .map((a) => a._id); + if (actorIds.length === 0) return; + await Actor.deleteDocuments(actorIds); + }, + };