diff --git a/lang/en.json b/lang/en.json index 6649b2b..b374123 100644 --- a/lang/en.json +++ b/lang/en.json @@ -33,6 +33,14 @@ "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." + }, + "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": { @@ -95,7 +103,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 +143,8 @@ }, "Folders": { "Familiar": "Familiars", - "Familiars": "Familiars" + "Familiars": "Familiars", + "PathmuncherTemp": "Pathmuncher Scratchpad" }, "CompendiumGroups": { "feats": "Feats", diff --git a/src/app/Pathmuncher.js b/src/app/Pathmuncher.js index 7b5976b..ce0b310 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 utils.deleteActor(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 utils.deleteActor(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 utils.deleteActor(tempActor); } } @@ -1176,7 +1179,7 @@ export class Pathmuncher { }); throw err; } finally { - await Actor.deleteDocuments([tempActor._id]); + await utils.deleteActor(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,19 @@ export class Pathmuncher { return actor; } + #setTempActorName(actorData) { + const formattedNum = this.tempActorCounter.toString().padStart(4, "0"); + actorData.name = `Mr Temp (${this.result.character.name}) ${formattedNum}`; + this.tempActorCounter += 1; + } + + async #setTempActorFolder(actorData) { + if (!utils.setting("USE_TEMP_FOLDER")) return; + + let tempActorFolder = await utils.getOrCreateFolder(null, "Actor", this.tempActorFolderName); + actorData.folder = tempActorFolder?.id; + } + async processCharacter() { if (!this.source) return; await this.#prepare(); @@ -3054,12 +3072,6 @@ 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(); - } - } - async updateActor() { await this.#removeDocumentsToBeUpdated(); @@ -3079,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 8b511b5..25ce8ea 100644 --- a/src/constants.js +++ b/src/constants.js @@ -12,6 +12,9 @@ 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", + USE_TEMP_FOLDER: "use-temp-folder", + ACTIVE_GM: "active-gm", }, FEAT_PRIORITY: [ @@ -165,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`, @@ -216,6 +238,13 @@ CONSTANTS.DEFAULT_SETTINGS = { default: "WARN", }, + [CONSTANTS.SETTINGS.ACTIVE_GM]: { + scope: "world", + config: false, + type: String, + default: "", + }, + }; CONSTANTS.PATH = `modules/${CONSTANTS.MODULE_NAME}`; diff --git a/src/hooks/folder.js b/src/hooks/folder.js new file mode 100644 index 0000000..44ddf15 --- /dev/null +++ b/src/hooks/folder.js @@ -0,0 +1,14 @@ +import CONSTANTS from "../constants.js"; +import utils from "../utils.js"; + +export function autoCreateFolders() { + 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 (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 f3beff6..a10d67b 100644 --- a/src/module.js +++ b/src/module.js @@ -1,12 +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 fca727b..c0350d3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -54,9 +54,20 @@ 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)) { + 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, @@ -96,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); + }, + };